@seorii/tiptap 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core';
2
+ import { NodeSelection } from '@tiptap/pm/state';
3
+ import { SvelteNodeViewRenderer } from 'svelte-tiptap';
4
+ import UploadSkeleton from './UploadSkeleton.svelte';
5
+ export const UPLOAD_SKELETON_NODE = 'tiptap-upload-skeleton';
6
+ const defaultHeight = {
7
+ image: 220,
8
+ file: 56,
9
+ pdf: 420,
10
+ embed: 420,
11
+ block: 180
12
+ };
13
+ function findUploadSkeleton(doc, id) {
14
+ let foundPos = null;
15
+ let foundNode = null;
16
+ doc.descendants((node, pos) => {
17
+ if (node.type.name !== UPLOAD_SKELETON_NODE)
18
+ return;
19
+ if (node.attrs.uploadId !== id)
20
+ return;
21
+ foundPos = pos;
22
+ foundNode = node;
23
+ return false;
24
+ });
25
+ if (foundPos === null || foundNode === null)
26
+ return null;
27
+ return { pos: foundPos, node: foundNode };
28
+ }
29
+ function tryCreateNodeSelection(doc, pos) {
30
+ if (pos < 0 || pos > doc.content.size)
31
+ return null;
32
+ const node = doc.nodeAt(pos);
33
+ if (!node || node.type.spec.selectable === false)
34
+ return null;
35
+ try {
36
+ return NodeSelection.create(doc, pos);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ export function insertUploadSkeleton(editor, { kind = 'block', height = defaultHeight[kind], at, select = true, insertParagraph = true } = {}) {
43
+ const skeletonType = editor.state.schema.nodes[UPLOAD_SKELETON_NODE];
44
+ if (!skeletonType)
45
+ return null;
46
+ const clampedHeight = Math.max(44, Math.min(1200, Math.round(height)));
47
+ const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
48
+ const node = skeletonType.create({ uploadId, kind, height: clampedHeight });
49
+ const paragraph = editor.state.schema.nodes.paragraph?.create();
50
+ const safePos = Math.max(0, Math.min(at ?? editor.state.selection.from, editor.state.doc.content.size));
51
+ const tr = editor.state.tr.insert(safePos, node);
52
+ if (insertParagraph && paragraph) {
53
+ tr.insert(safePos + node.nodeSize, paragraph);
54
+ }
55
+ if (select) {
56
+ const nodeSelection = tryCreateNodeSelection(tr.doc, safePos);
57
+ if (nodeSelection)
58
+ tr.setSelection(nodeSelection);
59
+ }
60
+ editor.view.dispatch(tr);
61
+ return {
62
+ id: uploadId,
63
+ exists: () => Boolean(findUploadSkeleton(editor.state.doc, uploadId)),
64
+ replaceWith: (content, options = {}) => {
65
+ const target = findUploadSkeleton(editor.state.doc, uploadId);
66
+ if (!target)
67
+ return false;
68
+ if (!content?.type)
69
+ return false;
70
+ let nextNode;
71
+ try {
72
+ nextNode = editor.state.schema.nodeFromJSON(content);
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ const tr = editor.state.tr.replaceWith(target.pos, target.pos + target.node.nodeSize, nextNode);
78
+ if (options.select ?? true) {
79
+ const nodeSelection = tryCreateNodeSelection(tr.doc, target.pos);
80
+ if (nodeSelection)
81
+ tr.setSelection(nodeSelection);
82
+ }
83
+ editor.view.dispatch(tr);
84
+ return true;
85
+ },
86
+ remove: () => {
87
+ const target = findUploadSkeleton(editor.state.doc, uploadId);
88
+ if (!target)
89
+ return false;
90
+ const removeFrom = target.pos;
91
+ let removeTo = target.pos + target.node.nodeSize;
92
+ const nextNode = editor.state.doc.nodeAt(removeTo);
93
+ if (nextNode?.type.name === 'paragraph' && nextNode.content.size === 0) {
94
+ removeTo += nextNode.nodeSize;
95
+ }
96
+ editor.view.dispatch(editor.state.tr.deleteRange(removeFrom, removeTo));
97
+ return true;
98
+ }
99
+ };
100
+ }
101
+ export default Node.create({
102
+ name: UPLOAD_SKELETON_NODE,
103
+ group: 'block',
104
+ atom: true,
105
+ draggable: false,
106
+ selectable: true,
107
+ addAttributes() {
108
+ return {
109
+ uploadId: {
110
+ default: null,
111
+ parseHTML: (element) => element.getAttribute('data-upload-id'),
112
+ renderHTML: (attributes) => attributes.uploadId ? { 'data-upload-id': attributes.uploadId } : {}
113
+ },
114
+ kind: {
115
+ default: 'block',
116
+ parseHTML: (element) => element.getAttribute('data-upload-kind') || 'block',
117
+ renderHTML: (attributes) => attributes.kind ? { 'data-upload-kind': attributes.kind } : {}
118
+ },
119
+ height: {
120
+ default: defaultHeight.block,
121
+ parseHTML: (element) => {
122
+ const value = Number.parseInt(element.getAttribute('data-upload-height') || '', 10);
123
+ return Number.isFinite(value) ? value : defaultHeight.block;
124
+ },
125
+ renderHTML: (attributes) => {
126
+ const raw = Number.parseFloat(String(attributes.height ?? defaultHeight.block));
127
+ const height = Number.isFinite(raw) ? Math.max(44, Math.min(1200, Math.round(raw))) : 180;
128
+ return { 'data-upload-height': String(height) };
129
+ }
130
+ }
131
+ };
132
+ },
133
+ parseHTML() {
134
+ return [{ tag: UPLOAD_SKELETON_NODE }];
135
+ },
136
+ renderHTML({ HTMLAttributes }) {
137
+ return [
138
+ UPLOAD_SKELETON_NODE,
139
+ mergeAttributes(HTMLAttributes, {
140
+ 'data-bubble-menu': 'false'
141
+ })
142
+ ];
143
+ },
144
+ addNodeView() {
145
+ return SvelteNodeViewRenderer(UploadSkeleton);
146
+ }
147
+ });
@@ -5,7 +5,7 @@ const youtubeExtractId = (url) => {
5
5
  const match = youtubeRegExp.exec(url.trim());
6
6
  return match ? match[1] : false;
7
7
  };
8
- const videoPlayerStaticAttributes = { nocookie: true };
8
+ const videoPlayerStaticAttributes = { nocookie: true, 'data-bubble-menu': 'false' };
9
9
  export default Node.create({
10
10
  name: 'lite-youtube',
11
11
  content: '',
@@ -10,23 +10,33 @@
10
10
  import defaultI18n, { I18N_CONTEXT, type I18nTranslate } from '../i18n';
11
11
  import ColorPicker from 'svelte-awesome-color-picker';
12
12
  import { isTextSelection } from '@tiptap/core';
13
- import type { EditorState, Selection } from '@tiptap/pm/state';
13
+ import { NodeSelection, type EditorState, type Selection } from '@tiptap/pm/state';
14
14
  import type { EditorView } from '@tiptap/pm/view';
15
15
 
16
16
  type Props = {
17
17
  colors?: string[];
18
18
  editable?: boolean;
19
19
  override?: any;
20
+ docked?: boolean;
20
21
  children?: any;
21
22
  };
22
23
 
23
- let { colors = [], editable, override, children }: Props = $props();
24
+ let { colors = [], editable, override, docked = false, children }: Props = $props();
24
25
 
25
26
  const editor = getContext<{ v: any; c: number }>('editor');
26
27
  const i18nFromContext = getContext<I18nTranslate | undefined>(I18N_CONTEXT);
27
28
  const i18n: I18nTranslate = (...args) =>
28
29
  i18nFromContext ? i18nFromContext(...args) : defaultI18n(...args);
29
30
  const tiptap = $derived(editor.v);
31
+ const currentTextColor = $derived.by(() => {
32
+ editor.c;
33
+ const color = tiptap?.getAttributes('textStyle')?.color;
34
+ return typeof color === 'string' ? color.trim().toLowerCase() : '';
35
+ });
36
+ const hasTextColor = $derived.by(() => {
37
+ editor.c;
38
+ return !!tiptap?.getAttributes('textStyle')?.color;
39
+ });
30
40
 
31
41
  let selection = $state<Selection | null>(null);
32
42
  let table = $state<number[] | false>(false);
@@ -72,6 +82,7 @@
72
82
 
73
83
  const shouldShow = ({
74
84
  state,
85
+ view,
75
86
  from,
76
87
  to
77
88
  }: {
@@ -85,6 +96,18 @@
85
96
  const { doc, selection } = state;
86
97
  const { empty } = selection;
87
98
 
99
+ if (selection instanceof NodeSelection && selection.node.isBlock) {
100
+ const nodeDom = view.nodeDOM(from);
101
+ if (
102
+ nodeDom instanceof Element &&
103
+ (nodeDom.hasAttribute('data-hide-bubble-menu') ||
104
+ nodeDom.getAttribute('data-bubble-menu') === 'false' ||
105
+ Boolean(nodeDom.querySelector('[data-hide-bubble-menu], [data-bubble-menu="false"]')))
106
+ ) {
107
+ return false;
108
+ }
109
+ }
110
+
88
111
  const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection);
89
112
 
90
113
  return !(empty || isEmptyTextBlock);
@@ -92,22 +115,13 @@
92
115
  </script>
93
116
 
94
117
  {#if tiptap}
95
- <BubbleMenu
96
- editor={tiptap}
97
- updateDelay={50}
98
- {shouldShow}
99
- tippyOptions={{
100
- moveTransition: 'transform 0.2s cubic-bezier(1,.5,0,.85)',
101
- animation: 'shift-away-subtle',
102
- duration: [200, 50]
103
- }}
104
- >
118
+ {#snippet toolbar()}
105
119
  {#if override}
106
- <main>
120
+ <main class:docked>
107
121
  <Render it={override} />
108
122
  </main>
109
123
  {:else}
110
- <main>
124
+ <main class:docked>
111
125
  {#if link}
112
126
  <div class="link">
113
127
  <p>
@@ -157,11 +171,11 @@
157
171
  <ToolbarButton icon="close" handler={() => deleteTable({ editor: tiptap })} />
158
172
  {:else}
159
173
  {#if editable}
160
- <Paper hover bl>
174
+ <Paper hover bl remap>
161
175
  {#snippet target()}
162
176
  <IconButton size="1.2em" icon="format_align_left" />
163
177
  {/snippet}
164
- <div style="margin: -6px;font-size: 0.6em">
178
+ <div class="menu-list align-menu">
165
179
  <List>
166
180
  <OneLine
167
181
  icon="format_align_left"
@@ -201,13 +215,19 @@
201
215
  {#if editable}
202
216
  <ToolbarButton icon="functions" prop="math_inline" handler={() => setMath(tiptap)} />
203
217
  {/if}
204
- <Paper bl block>
218
+ <Paper bl remap>
205
219
  {#snippet target()}
206
220
  <IconButton size="1.2em" icon="palette" />
207
221
  {/snippet}
208
- <div style="font-size: 0.6em">
222
+ <div class="menu-list">
209
223
  <div class="colors">
210
- <Button small outlined onclick={() => tiptap.chain().focus().unsetColor().run()}>
224
+ <Button
225
+ small
226
+ outlined
227
+ active={!hasTextColor}
228
+ class={!hasTextColor ? 'color-active' : undefined}
229
+ onclick={() => tiptap.chain().focus().unsetColor().run()}
230
+ >
211
231
  {i18n('default')}
212
232
  </Button>
213
233
  <Paper bl remap block>
@@ -216,15 +236,14 @@
216
236
  small
217
237
  full
218
238
  outlined
239
+ active={hasTextColor}
240
+ class={hasTextColor ? 'color-active' : undefined}
219
241
  onclick={() => tiptap.chain().focus().unsetColor().run()}
220
242
  >
221
243
  <Icon icon="palette" />
222
244
  </Button>
223
245
  {/snippet}
224
- <div
225
- onclick={(event) => event.stopPropagation()}
226
- onmousedown={(event) => event.stopPropagation()}
227
- >
246
+ <div role="presentation" onmousedown={(event) => event.stopPropagation()}>
228
247
  <ColorPicker
229
248
  isDialog={false}
230
249
  onInput={(event) => {
@@ -233,10 +252,12 @@
233
252
  />
234
253
  </div>
235
254
  </Paper>
236
- {#each colors as color}
255
+ {#each colors as color (color)}
237
256
  <Button
238
257
  small
239
258
  outlined
259
+ active={currentTextColor === color.toLowerCase()}
260
+ class={currentTextColor === color.toLowerCase() ? 'color-active' : undefined}
240
261
  onclick={() => tiptap.chain().focus().setColor(color).run()}
241
262
  >
242
263
  <span style:background={color} class="pal"></span>
@@ -253,30 +274,104 @@
253
274
  <Render it={children} />
254
275
  </main>
255
276
  {/if}
256
- </BubbleMenu>
277
+ {/snippet}
278
+
279
+ {#if docked && tiptap.isEditable}
280
+ <div class="docked-menu">
281
+ {@render toolbar()}
282
+ </div>
283
+ {:else}
284
+ <BubbleMenu
285
+ editor={tiptap}
286
+ updateDelay={50}
287
+ {shouldShow}
288
+ tippyOptions={{
289
+ moveTransition: 'transform 0.2s cubic-bezier(1,.5,0,.85)',
290
+ animation: 'shift-away-subtle',
291
+ duration: [200, 50]
292
+ }}
293
+ >
294
+ {@render toolbar()}
295
+ </BubbleMenu>
296
+ {/if}
257
297
  {/if}
258
298
 
259
299
  <style>
260
300
  main {
261
- box-shadow: var(--shadow);
301
+ box-shadow:
302
+ 0 10px 24px rgba(22, 37, 63, 0.14),
303
+ 0 2px 8px rgba(22, 37, 63, 0.1);
262
304
  background: var(--surface, #fff);
305
+ border: 1px solid var(--primary-light2, #d5dff3);
263
306
  color: var(--on-surface, #000);
264
- padding: 8px;
265
- border-radius: 4px;
307
+ padding: 8px 10px;
308
+ border-radius: 12px;
266
309
  display: flex;
310
+ flex-wrap: nowrap;
311
+ gap: 4px;
267
312
  align-items: center;
268
313
  justify-content: center;
269
314
  font-size: 1.2em;
270
315
  & > :global(*) {
271
- margin: 0 2px;
316
+ margin: 0;
272
317
  }
318
+ }
273
319
 
274
- & > :global(*:first-child) {
275
- margin-left: 0;
276
- }
320
+ .docked-menu {
321
+ position: sticky;
322
+ top: var(--tiptap-toolbar-sticky-top, 8px);
323
+ z-index: 40;
324
+ display: flex;
325
+ justify-content: center;
326
+ margin-bottom: 10px;
327
+ padding: 4px 0;
328
+ }
329
+
330
+ main.docked {
331
+ width: 100%;
332
+ justify-content: flex-start;
333
+ border-radius: 14px;
334
+ overflow-x: auto;
335
+ overflow-y: hidden;
336
+ backdrop-filter: blur(10px) saturate(1.15);
337
+ -webkit-backdrop-filter: blur(10px) saturate(1.15);
338
+ }
339
+
340
+ main:not(.docked) {
341
+ overflow-x: auto;
342
+ overflow-y: hidden;
343
+ }
277
344
 
278
- & > :global(*:last-child) {
279
- margin-right: 0;
345
+ main.docked > :global(*) {
346
+ flex-shrink: 0;
347
+ }
348
+
349
+ main.docked :global(button) {
350
+ font-size: 1.08em;
351
+ }
352
+
353
+ main.docked > :global(main.i) {
354
+ display: flex;
355
+ align-items: center;
356
+ line-height: 1;
357
+ transform: translateY(-1px);
358
+ }
359
+
360
+ .menu-list {
361
+ font-size: 0.6em;
362
+ }
363
+
364
+ .align-menu {
365
+ margin: -6px;
366
+ }
367
+
368
+ main.docked .menu-list {
369
+ font-size: 0.72em;
370
+ }
371
+
372
+ @media (max-width: 768px) {
373
+ main {
374
+ font-size: 1.05em;
280
375
  }
281
376
  }
282
377
 
@@ -304,6 +399,16 @@
304
399
  & > :global(:first-child) {
305
400
  grid-column: 1/3;
306
401
  }
402
+
403
+ & :global(button.color-active) {
404
+ box-shadow: inset 0 0 0 1px var(--primary-dark2, #2d4b8f);
405
+ background: color-mix(in srgb, var(--primary-light1, #eef3ff) 65%, var(--surface, #fff));
406
+ }
407
+
408
+ & :global(button.color-active .pal) {
409
+ outline: 1px solid color-mix(in srgb, var(--on-surface, #111) 65%, transparent);
410
+ outline-offset: 1px;
411
+ }
307
412
  }
308
413
 
309
414
  .pal {
@@ -3,6 +3,7 @@ type Props = {
3
3
  colors?: string[];
4
4
  editable?: boolean;
5
5
  override?: any;
6
+ docked?: boolean;
6
7
  children?: any;
7
8
  };
8
9
  declare const Bubble: import("svelte").Component<Props, {}, "">;
@@ -19,6 +19,7 @@
19
19
  } from '../i18n';
20
20
  import type { UploadFn } from '../plugin/image/dragdrop';
21
21
  import { fallbackUpload } from '../plugin/image/dragdrop';
22
+ import MediaResize, { type ResizeOptions } from '../plugin/resize';
22
23
  import { Render } from 'nunui';
23
24
 
24
25
  type Props = {
@@ -36,9 +37,11 @@
36
37
  sanitize?: Record<string, any>;
37
38
  colors?: string[];
38
39
  bubble?: any;
40
+ bubbleDocked?: boolean;
39
41
  preloader?: any;
40
42
  crossorigin?: 'anonymous' | 'use-credentials';
41
43
  codeBlockLanguageLabels?: Record<string, string>;
44
+ resize?: boolean | ResizeOptions;
42
45
  };
43
46
 
44
47
  let {
@@ -65,12 +68,22 @@
65
68
  '#ab47bc' //purple
66
69
  ],
67
70
  bubble = null,
71
+ bubbleDocked = false,
68
72
  preloader,
69
73
  crossorigin = 'anonymous',
70
- codeBlockLanguageLabels = {}
74
+ codeBlockLanguageLabels = {},
75
+ resize = true
71
76
  }: Props = $props();
72
77
 
73
78
  const scopedI18n: I18nTranslate = (...args) => translateWithLocale(locale, ...args);
79
+ const resizeDataAttrs = [
80
+ 'data-resize-handler',
81
+ 'data-resize-target',
82
+ 'data-resize-min-height',
83
+ 'data-resize-max-height',
84
+ 'data-bubble-menu',
85
+ 'data-hide-bubble-menu'
86
+ ];
74
87
 
75
88
  const san = (body: string) =>
76
89
  sanitizeHtml(body || '', {
@@ -85,18 +98,26 @@
85
98
  'embed',
86
99
  'mark',
87
100
  'code',
101
+ 'tiptap-upload-skeleton',
88
102
  ...(sanitize.allowedTags || [])
89
103
  ]),
90
104
  allowedStyles: '*' as any,
91
105
  allowedAttributes: {
92
106
  '*': ['style', 'class'],
93
107
  a: ['href', 'name', 'target'],
94
- img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading'],
95
- iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
108
+ img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading', ...resizeDataAttrs],
109
+ iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen', ...resizeDataAttrs],
96
110
  th: ['colwidth', 'colspan', 'rowspan'],
97
111
  td: ['colwidth', 'colspan', 'rowspan'],
98
- 'lite-youtube': ['videoid', 'params', 'nocookie', 'title', 'provider'],
99
- embed: ['src', 'type', 'frameborder', 'allowfullscreen'],
112
+ 'lite-youtube': ['videoid', 'params', 'nocookie', 'title', 'provider', ...resizeDataAttrs],
113
+ embed: ['src', 'type', 'frameborder', 'allowfullscreen', ...resizeDataAttrs],
114
+ 'tiptap-upload-skeleton': [
115
+ 'data-upload-id',
116
+ 'data-upload-kind',
117
+ 'data-upload-height',
118
+ 'data-bubble-menu',
119
+ 'data-hide-bubble-menu'
120
+ ],
100
121
  mark: ['style', 'data-color'],
101
122
  code: ['class'],
102
123
  ...(sanitize.allowedAttributes || [])
@@ -133,6 +154,15 @@
133
154
  ([{ default: tt }]) => {
134
155
  if (!untrack(() => mounted)) return;
135
156
  const editorPlaceholder = placeholder ?? scopedI18n('placeholder');
157
+ const optionPlugins = Array.isArray(options.plugins)
158
+ ? [...options.plugins]
159
+ : options.plugins
160
+ ? [options.plugins]
161
+ : [];
162
+ if (resize) {
163
+ const resizeOptions = typeof resize === 'object' ? resize : {};
164
+ optionPlugins.unshift(MediaResize.configure(resizeOptions));
165
+ }
136
166
  tiptap.v = ref = tt(element, r, {
137
167
  placeholder: editorPlaceholder,
138
168
  editable,
@@ -142,7 +172,8 @@
142
172
  },
143
173
  crossorigin,
144
174
  codeBlockLanguageLabels,
145
- ...options
175
+ ...options,
176
+ plugins: optionPlugins
146
177
  });
147
178
  tiptap.v.on('update', ({ editor: tiptap }: any) => {
148
179
  let content = tiptap.getHTML(),
@@ -217,6 +248,11 @@
217
248
  </script>
218
249
 
219
250
  <main class:fullscreen class:editable {style}>
251
+ {#if bubbleDocked && (editable || mark)}
252
+ <Bubble {colors} {editable} override={bubble} docked={bubbleDocked}>
253
+ <Render it={bubble} />
254
+ </Bubble>
255
+ {/if}
220
256
  <div class="wrapper">
221
257
  <!-- svelte-ignore a11y_no_static_element_interactions -->
222
258
  <div bind:this={element} class="target" onkeydown={handleKeydown}></div>
@@ -236,8 +272,8 @@
236
272
  <Command />
237
273
  <Floating />
238
274
  {/if}
239
- {#if editable || mark}
240
- <Bubble {colors} {editable} override={bubble}>
275
+ {#if !bubbleDocked && (editable || mark)}
276
+ <Bubble {colors} {editable} override={bubble} docked={bubbleDocked}>
241
277
  <Render it={bubble} />
242
278
  </Bubble>
243
279
  {/if}
@@ -291,7 +327,9 @@
291
327
 
292
328
  .editable :global(.ProseMirror-selectednode img) {
293
329
  transition: all 0.2s ease-in-out;
294
- filter: drop-shadow(0 0 0.75rem var(--primary-light13));
330
+ outline: 3px solid var(--primary);
331
+ outline-offset: 2px;
332
+ filter: none;
295
333
  }
296
334
 
297
335
  .editable :global(.iframe-wrapper.ProseMirror-selectednode) {
@@ -302,6 +340,53 @@
302
340
  outline: 3px solid var(--primary);
303
341
  }
304
342
 
343
+ .editable :global(.tiptap-media-resize-anchor) {
344
+ width: 100%;
345
+ display: flex;
346
+ justify-content: center;
347
+ margin: 6px 0 2px;
348
+ line-height: 0;
349
+ pointer-events: none;
350
+ }
351
+
352
+ .editable :global(.tiptap-media-resize-handle) {
353
+ appearance: none;
354
+ -webkit-appearance: none;
355
+ display: block;
356
+ width: 42px;
357
+ height: 8px;
358
+ margin: 0;
359
+ padding: 0;
360
+ border: 1px solid var(--primary-light3, rgba(120, 120, 120, 0.45));
361
+ border-radius: 999px;
362
+ background: var(--primary-light6, rgba(120, 120, 120, 0.2));
363
+ cursor: ns-resize;
364
+ pointer-events: auto;
365
+ transition:
366
+ background-color 0.15s ease,
367
+ border-color 0.15s ease,
368
+ transform 0.15s ease;
369
+ }
370
+
371
+ .editable :global(.tiptap-media-resize-handle:hover),
372
+ .editable :global(.tiptap-media-resize-handle:focus-visible) {
373
+ background: var(--primary-light4, rgba(120, 120, 120, 0.35));
374
+ border-color: var(--primary-light2, rgba(100, 100, 100, 0.55));
375
+ outline: none;
376
+ }
377
+
378
+ .editable :global(.tiptap-media-resize-handle:active) {
379
+ transform: translateY(1px);
380
+ }
381
+
382
+ .editable :global(.tiptap-media-resize-proxy) {
383
+ width: 100%;
384
+ border-radius: 12px;
385
+ background: var(--primary-light4, rgba(120, 120, 120, 0.35));
386
+ opacity: 0.55;
387
+ pointer-events: none;
388
+ }
389
+
305
390
  div > :global(div) {
306
391
  outline: none !important;
307
392
  & :global(.ProseMirror) :global(p.is-editor-empty:first-child::before) {
@@ -325,6 +410,7 @@
325
410
  & :global(img) {
326
411
  transition: all 0.2s ease-in-out;
327
412
  max-width: 100%;
413
+ object-fit: contain;
328
414
  border-radius: 12px;
329
415
  position: relative;
330
416
  }
@@ -1,5 +1,6 @@
1
1
  import '@seorii/prosemirror-math/style.css';
2
2
  import type { UploadFn } from '../plugin/image/dragdrop';
3
+ import { type ResizeOptions } from '../plugin/resize';
3
4
  type Props = {
4
5
  body: string;
5
6
  editable?: boolean;
@@ -15,9 +16,11 @@ type Props = {
15
16
  sanitize?: Record<string, any>;
16
17
  colors?: string[];
17
18
  bubble?: any;
19
+ bubbleDocked?: boolean;
18
20
  preloader?: any;
19
21
  crossorigin?: 'anonymous' | 'use-credentials';
20
22
  codeBlockLanguageLabels?: Record<string, string>;
23
+ resize?: boolean | ResizeOptions;
21
24
  };
22
25
  declare const TipTap: import("svelte").Component<Props, {}, "body" | "ref" | "loaded">;
23
26
  type TipTap = ReturnType<typeof TipTap>;
@@ -21,10 +21,12 @@ import { Color } from '@tiptap/extension-color';
21
21
  import { TextStyle } from '@tiptap/extension-text-style';
22
22
  import Iframe from '../plugin/iframe';
23
23
  import Embed from '../plugin/embed';
24
+ import UploadSkeleton from '../plugin/upload/skeleton';
24
25
  // @ts-ignore
25
26
  import { MathInline, MathBlock } from '@seorii/prosemirror-math/tiptap';
26
27
  import Youtube from '../plugin/youtube';
27
28
  import Placeholder from '@tiptap/extension-placeholder';
29
+ import columns from '../plugin/columns';
28
30
  import command from '../plugin/command/suggest';
29
31
  import emoji from '../plugin/command/emoji';
30
32
  import { countSlashItems, moveSlashSelection, runSlashItemAt, slashState } from '../plugin/command/stores.svelte';
@@ -238,6 +240,7 @@ const extensions = (placeholder, plugins, crossorigin, codeBlockLanguageLabels)
238
240
  orderedlist,
239
241
  MathInline,
240
242
  MathBlock,
243
+ ...columns,
241
244
  table,
242
245
  tableHeader,
243
246
  tableRow,
@@ -247,6 +250,7 @@ const extensions = (placeholder, plugins, crossorigin, codeBlockLanguageLabels)
247
250
  Indent,
248
251
  Color,
249
252
  TextStyle,
253
+ UploadSkeleton,
250
254
  Iframe,
251
255
  Embed,
252
256
  Code.extend({