your personal website on atproto - mirror blento.app
at fix-cached-posts 199 lines 5.2 kB view raw
1import { Extension } from '@tiptap/core'; 2import Suggestion from '@tiptap/suggestion'; 3import type { Editor, Range } from '@tiptap/core'; 4import { PluginKey } from '@tiptap/pm/state'; 5import type { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion'; 6import SuggestionSelect from './SuggestionSelect.svelte'; 7import { mount, unmount } from 'svelte'; 8import { computePosition, flip, shift, offset } from '@floating-ui/dom'; 9import type { RichTextTypes } from '..'; 10 11export default Extension.create({ 12 name: 'slash', 13 14 addOptions() { 15 return { 16 suggestion: { 17 char: '/', 18 19 command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { 20 props.command({ editor, range }); 21 } 22 } 23 }; 24 }, 25 26 addProseMirrorPlugins() { 27 return [ 28 Suggestion({ 29 editor: this.editor, 30 ...this.options.suggestion 31 }) 32 ]; 33 } 34}); 35 36export function suggestion({ 37 char, 38 pluginKey, 39 switchTo, 40 processImageFile 41}: { 42 char: string; 43 pluginKey: string; 44 switchTo: (value: RichTextTypes) => void; 45 processImageFile: (file: File) => void; 46}) { 47 return { 48 char, 49 pluginKey: new PluginKey(pluginKey), 50 51 items: ({ query }: { query: string }) => { 52 return [ 53 { 54 value: 'paragraph', 55 label: 'Paragraph', 56 command: ({ editor, range }: { editor: Editor; range: Range }) => { 57 editor.chain().focus().deleteRange(range).run(); 58 switchTo('paragraph'); 59 } 60 }, 61 { 62 value: 'heading-1', 63 label: 'Heading 1', 64 command: ({ editor, range }: { editor: Editor; range: Range }) => { 65 editor.chain().focus().deleteRange(range).run(); 66 switchTo('heading-1'); 67 } 68 }, 69 { 70 value: 'heading-2', 71 label: 'Heading 2', 72 command: ({ editor, range }: { editor: Editor; range: Range }) => { 73 editor.chain().focus().deleteRange(range).run(); 74 switchTo('heading-2'); 75 } 76 }, 77 { 78 value: 'heading-3', 79 label: 'Heading 3', 80 command: ({ editor, range }: { editor: Editor; range: Range }) => { 81 editor.chain().focus().deleteRange(range).run(); 82 switchTo('heading-3'); 83 } 84 }, 85 { 86 value: 'blockquote', 87 label: 'Blockquote', 88 command: ({ editor, range }: { editor: Editor; range: Range }) => { 89 editor.chain().focus().deleteRange(range).run(); 90 switchTo('blockquote'); 91 } 92 }, 93 { 94 value: 'code', 95 label: 'Code Block', 96 command: ({ editor, range }: { editor: Editor; range: Range }) => { 97 editor.chain().focus().deleteRange(range).run(); 98 switchTo('code'); 99 } 100 }, 101 { 102 value: 'bullet-list', 103 label: 'Bullet List', 104 command: ({ editor, range }: { editor: Editor; range: Range }) => { 105 editor.chain().focus().deleteRange(range).run(); 106 switchTo('bullet-list'); 107 } 108 }, 109 { 110 value: 'ordered-list', 111 label: 'Ordered List', 112 command: ({ editor, range }: { editor: Editor; range: Range }) => { 113 editor.chain().focus().deleteRange(range).run(); 114 switchTo('ordered-list'); 115 } 116 }, 117 { 118 value: 'image', 119 label: 'Add Image', 120 command: ({ editor, range }: { editor: Editor; range: Range }) => { 121 editor.chain().focus().deleteRange(range).run(); 122 123 const fileInput = document.createElement('input'); 124 fileInput.type = 'file'; 125 fileInput.click(); 126 fileInput.addEventListener('change', (event) => { 127 const input = event.target as HTMLInputElement; 128 if (!input.files?.length) return; 129 const file = input.files[0]; 130 if (!file?.type.startsWith('image/')) return; 131 processImageFile(file); 132 133 input.remove(); 134 }); 135 } 136 } 137 ].filter((item) => item.label.toLowerCase().includes(query.toLowerCase())); 138 }, 139 140 render: () => { 141 let component: ReturnType<typeof SuggestionSelect>; 142 let floatingEl: HTMLElement; 143 144 function updatePosition(clientRect: (() => DOMRect | null) | null | undefined) { 145 if (!clientRect || !floatingEl) return; 146 const rect = clientRect(); 147 if (!rect) return; 148 149 // Create a virtual reference element for floating-ui 150 const virtualRef = { 151 getBoundingClientRect: () => rect 152 }; 153 154 computePosition(virtualRef, floatingEl, { 155 placement: 'bottom-start', 156 middleware: [offset(8), flip(), shift({ padding: 8 })] 157 }).then(({ x, y }) => { 158 Object.assign(floatingEl.style, { 159 left: `${x}px`, 160 top: `${y}px` 161 }); 162 }); 163 } 164 165 return { 166 onStart: (props: SuggestionProps) => { 167 floatingEl = document.createElement('div'); 168 floatingEl.style.position = 'absolute'; 169 floatingEl.style.zIndex = '50'; 170 document.body.appendChild(floatingEl); 171 172 component = mount(SuggestionSelect, { 173 target: floatingEl, 174 props 175 }); 176 177 updatePosition(props.clientRect); 178 }, 179 onUpdate: (props: SuggestionProps) => { 180 component.setItems(props.items); 181 component.setRange(props.range); 182 updatePosition(props.clientRect); 183 }, 184 onKeyDown: (props: SuggestionKeyDownProps) => { 185 if (props.event.key === 'Escape') { 186 floatingEl.style.display = 'none'; 187 return true; 188 } 189 190 return component.onKeyDown(props.event); 191 }, 192 onExit: () => { 193 unmount(component); 194 floatingEl.remove(); 195 } 196 }; 197 } 198 }; 199}