@seorii/tiptap 0.1.1 → 0.2.0

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,20 @@
1
+ import { PluginKey } from "prosemirror-state";
2
+ import type { Editor } from "@tiptap/core";
3
+ export declare const emoji: {
4
+ pluginKey: PluginKey<any>;
5
+ char: string;
6
+ items: ({ query }: {
7
+ query: any;
8
+ }) => {
9
+ title: string;
10
+ command: {};
11
+ }[];
12
+ render: () => {
13
+ onStart: (props: any) => void;
14
+ onUpdate(props: any): void;
15
+ onKeyDown(props: any): true | undefined;
16
+ onExit(): void;
17
+ };
18
+ };
19
+ declare const _default: (editor: Editor) => import("prosemirror-state").Plugin<any>;
20
+ export default _default;
@@ -0,0 +1,81 @@
1
+ import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from './stores';
2
+ import { PluginKey } from "prosemirror-state";
3
+ import Suggestion from "@tiptap/suggestion";
4
+ //@ts-ignore
5
+ import emojis from 'emojis-list';
6
+ //@ts-ignore
7
+ import tags from 'emojis-keywords';
8
+ const max = 10;
9
+ function fixRange(editor, range, split = '/') {
10
+ const { state } = editor.view, { selection, doc } = state;
11
+ if (selection.$to.nodeBefore?.text?.includes?.(split)) {
12
+ range.from = range.to;
13
+ while (range.from > 0 && doc.textBetween(range.from - 1, range.from) !== split) {
14
+ try {
15
+ range.from -= 1;
16
+ }
17
+ catch (e) {
18
+ range.from += 2;
19
+ break;
20
+ }
21
+ }
22
+ range.from -= 1;
23
+ }
24
+ while (range.to < selection.to && doc.textBetween(range.to, range.to + 1) !== ' ') {
25
+ try {
26
+ range.to += 1;
27
+ }
28
+ catch (e) {
29
+ range.to -= 1;
30
+ break;
31
+ }
32
+ }
33
+ return range;
34
+ }
35
+ export const emoji = {
36
+ pluginKey: new PluginKey('slash-emoji'),
37
+ char: ':',
38
+ items: ({ query }) => {
39
+ query = ':' + query.toLowerCase();
40
+ const filtered = [];
41
+ for (let i = 0; i < emojis.length; i++) {
42
+ if (tags[i]?.includes?.(query))
43
+ filtered.push({
44
+ title: emojis[i] + ' ' + tags[i],
45
+ command: ({ editor, range }) => {
46
+ editor.chain().deleteRange(fixRange(editor, range, ':')).insertContent(emojis[i]).run();
47
+ }
48
+ });
49
+ if (filtered.length >= max)
50
+ break;
51
+ }
52
+ return filtered;
53
+ },
54
+ render: () => {
55
+ return {
56
+ onStart: (props) => {
57
+ let editor = props.editor;
58
+ let range = props.range;
59
+ let location = props.clientRect();
60
+ slashProps.set({ editor, range });
61
+ slashVisible.set(true);
62
+ slashLocaltion.set({ x: location.x, y: location.y, height: location.height });
63
+ slashItems.set(props.items);
64
+ slashDetail.set('emoji');
65
+ },
66
+ onUpdate(props) {
67
+ slashItems.set(props.items);
68
+ },
69
+ onKeyDown(props) {
70
+ if (props.event.key === 'Escape') {
71
+ slashVisible.set(false);
72
+ return true;
73
+ }
74
+ },
75
+ onExit() {
76
+ slashVisible.set(false);
77
+ }
78
+ };
79
+ }
80
+ };
81
+ export default (editor) => Suggestion({ ...emoji, editor });
@@ -11,3 +11,4 @@ export declare const slashProps: import("svelte/store").Writable<{
11
11
  range: null;
12
12
  }>;
13
13
  export declare const slashDetail: import("svelte/store").Writable<null>;
14
+ export declare const slashSelection: import("svelte/store").Writable<null>;
@@ -4,3 +4,4 @@ export const slashItems = writable([]);
4
4
  export const slashLocaltion = writable({ x: 0, y: 0, height: 0 });
5
5
  export const slashProps = writable({ editor: null, range: null });
6
6
  export const slashDetail = writable(null);
7
+ export const slashSelection = writable(null);
@@ -1,4 +1,8 @@
1
- declare const _default: {
1
+ import { PluginKey } from "prosemirror-state";
2
+ import { Editor } from "@tiptap/core";
3
+ export declare const suggest: {
4
+ pluginKey: PluginKey<any>;
5
+ char: string;
2
6
  items: ({ query }: {
3
7
  query: any;
4
8
  }) => {
@@ -20,4 +24,5 @@ declare const _default: {
20
24
  onExit(): void;
21
25
  };
22
26
  };
27
+ declare const _default: (editor: Editor) => import("prosemirror-state").Plugin<any>;
23
28
  export default _default;
@@ -1,6 +1,38 @@
1
- import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from './stores';
1
+ import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail, slashSelection } from './stores';
2
2
  import i18n from "../../i18n";
3
- export default {
3
+ import { fallbackUpload } from "../image/dragdrop";
4
+ import { PluginKey } from "prosemirror-state";
5
+ import { Editor } from "@tiptap/core";
6
+ import Suggestion from "@tiptap/suggestion";
7
+ function fixRange(editor, range, split = '/') {
8
+ const { state } = editor.view, { selection, doc } = state;
9
+ if (selection.$to.nodeBefore?.text?.includes?.(split)) {
10
+ range.from = range.to;
11
+ while (range.from > 0 && doc.textBetween(range.from - 1, range.from) !== split) {
12
+ try {
13
+ range.from -= 1;
14
+ }
15
+ catch (e) {
16
+ range.from += 2;
17
+ break;
18
+ }
19
+ }
20
+ range.from -= 1;
21
+ }
22
+ while (range.to < selection.to && doc.textBetween(range.to, range.to + 1) !== ' ') {
23
+ try {
24
+ range.to += 1;
25
+ }
26
+ catch (e) {
27
+ range.to -= 1;
28
+ break;
29
+ }
30
+ }
31
+ return range;
32
+ }
33
+ export const suggest = {
34
+ pluginKey: new PluginKey('slash-suggest'),
35
+ char: '/',
4
36
  items: ({ query }) => {
5
37
  const raw = [
6
38
  {
@@ -10,7 +42,7 @@ export default {
10
42
  title: i18n('title') + ' 1',
11
43
  subtitle: i18n('title1Info'),
12
44
  command: ({ editor, range }) => {
13
- editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run();
45
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('heading', { level: 1 }).run();
14
46
  }
15
47
  },
16
48
  {
@@ -18,7 +50,7 @@ export default {
18
50
  title: i18n('title') + ' 2',
19
51
  subtitle: i18n('title2Info'),
20
52
  command: ({ editor, range }) => {
21
- editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run();
53
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('heading', { level: 2 }).run();
22
54
  }
23
55
  },
24
56
  {
@@ -26,7 +58,7 @@ export default {
26
58
  title: i18n('title') + ' 3',
27
59
  subtitle: i18n('title3Info'),
28
60
  command: ({ editor, range }) => {
29
- editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run();
61
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('heading', { level: 3 }).run();
30
62
  }
31
63
  },
32
64
  {
@@ -34,7 +66,7 @@ export default {
34
66
  title: i18n('unorderedList'),
35
67
  subtitle: i18n('unorderedListInfo'),
36
68
  command: ({ editor, range }) => {
37
- editor.commands.deleteRange(range);
69
+ editor.commands.deleteRange(fixRange(editor, range));
38
70
  editor.commands.toggleBulletList();
39
71
  }
40
72
  },
@@ -43,7 +75,7 @@ export default {
43
75
  title: i18n('numberList'),
44
76
  subtitle: i18n('numberListInfo'),
45
77
  command: ({ editor, range }) => {
46
- editor.commands.deleteRange(range);
78
+ editor.commands.deleteRange(fixRange(editor, range));
47
79
  editor.commands.toggleOrderedList();
48
80
  }
49
81
  }
@@ -51,12 +83,34 @@ export default {
51
83
  },
52
84
  {
53
85
  section: i18n('block'), list: [
86
+ {
87
+ icon: 'image',
88
+ title: i18n('image'),
89
+ subtitle: i18n('imageInfo'),
90
+ command: ({ editor, range }) => {
91
+ editor.chain().focus().deleteRange(fixRange(editor, range)).run();
92
+ const input = document.createElement('input');
93
+ input.type = 'file';
94
+ input.accept = 'image/*';
95
+ input.onchange = async () => {
96
+ if (input.files) {
97
+ const file = input.files[0];
98
+ if (file) {
99
+ const upload = window.__image_uploader || fallbackUpload;
100
+ const src = await upload(file);
101
+ editor.chain().focus().deleteRange(range).setImage({ src }).run();
102
+ }
103
+ }
104
+ };
105
+ input.click();
106
+ }
107
+ },
54
108
  {
55
109
  icon: 'code',
56
110
  title: i18n('codeBlock'),
57
111
  subtitle: i18n('codeBlockInfo'),
58
112
  command: ({ editor, range }) => {
59
- editor.chain().focus().deleteRange(range).setNode('codeBlock').run();
113
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('codeBlock').run();
60
114
  }
61
115
  },
62
116
  {
@@ -65,7 +119,7 @@ export default {
65
119
  subtitle: i18n('mathBlockInfo'),
66
120
  command: ({ editor, range }) => {
67
121
  const { to } = range;
68
- editor.chain().focus().deleteRange(range).setNode('math_display').focus().run();
122
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('math_display').focus().run();
69
123
  }
70
124
  },
71
125
  {
@@ -73,7 +127,7 @@ export default {
73
127
  title: i18n('table'),
74
128
  subtitle: i18n('tableInfo'),
75
129
  command: ({ editor, range }) => {
76
- editor.chain().focus().insertTable({ rows: 2, cols: 3 }).run();
130
+ editor.chain().focus().deleteRange(fixRange(editor, range)).insertTable({ rows: 2, cols: 3 }).run();
77
131
  }
78
132
  },
79
133
  {
@@ -81,7 +135,7 @@ export default {
81
135
  title: i18n('blockquote'),
82
136
  subtitle: i18n('blockquoteInfo'),
83
137
  command: ({ editor, range }) => {
84
- editor.chain().focus().deleteRange(range).setBlockquote().focus().run();
138
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setBlockquote().focus().run();
85
139
  }
86
140
  },
87
141
  {
@@ -89,6 +143,7 @@ export default {
89
143
  title: i18n('iframe'),
90
144
  subtitle: i18n('iframeInfo'),
91
145
  command: ({ editor, range }) => {
146
+ slashSelection.set(() => editor.chain().focus().deleteRange(fixRange(editor, range)).run());
92
147
  slashDetail.set('iframe');
93
148
  }
94
149
  },
@@ -97,6 +152,7 @@ export default {
97
152
  title: i18n('youtube'),
98
153
  subtitle: i18n('youtubeInfo'),
99
154
  command: ({ editor, range }) => {
155
+ slashSelection.set(() => editor.chain().focus().deleteRange(fixRange(editor, range)).run());
100
156
  slashDetail.set('youtube');
101
157
  }
102
158
  }
@@ -136,3 +192,4 @@ export default {
136
192
  };
137
193
  }
138
194
  };
195
+ export default (editor) => Suggestion({ ...suggest, editor });
@@ -0,0 +1,4 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ export type UploadFn = (image: File) => Promise<string>;
3
+ export declare const fallbackUpload: (image: File) => Promise<string>;
4
+ export declare const dropImagePlugin: () => Plugin<any>;
@@ -0,0 +1,86 @@
1
+ import { Plugin, PluginKey } from 'prosemirror-state';
2
+ export const fallbackUpload = (async (image) => URL.createObjectURL(image));
3
+ export const dropImagePlugin = () => {
4
+ return new Plugin({
5
+ props: {
6
+ handleDOMEvents: {
7
+ paste(view, event) {
8
+ const upload = window.__image_uploader || fallbackUpload;
9
+ const items = Array.from(event.clipboardData?.items || []);
10
+ const { schema } = view.state;
11
+ items.forEach((item) => {
12
+ const image = item.getAsFile();
13
+ if (item.type.indexOf('image') === 0) {
14
+ event.preventDefault();
15
+ if (upload && image) {
16
+ upload(image).then((src) => {
17
+ const node = schema.nodes.image.create({
18
+ src: src,
19
+ });
20
+ const transaction = view.state.tr.replaceSelectionWith(node);
21
+ view.dispatch(transaction);
22
+ });
23
+ }
24
+ }
25
+ else {
26
+ const reader = new FileReader();
27
+ reader.onload = (readerEvent) => {
28
+ const node = schema.nodes.image.create({
29
+ src: readerEvent.target?.result,
30
+ });
31
+ const transaction = view.state.tr.replaceSelectionWith(node);
32
+ view.dispatch(transaction);
33
+ };
34
+ if (!image)
35
+ return;
36
+ reader.readAsDataURL(image);
37
+ }
38
+ });
39
+ return false;
40
+ },
41
+ drop: (view, event) => {
42
+ const upload = window.__image_uploader || fallbackUpload;
43
+ const hasFiles = event.dataTransfer &&
44
+ event.dataTransfer.files &&
45
+ event.dataTransfer.files.length;
46
+ if (!hasFiles) {
47
+ return false;
48
+ }
49
+ const images = Array.from(event.dataTransfer?.files ?? []).filter((file) => /image/i.test(file.type));
50
+ if (images.length === 0) {
51
+ return false;
52
+ }
53
+ event.preventDefault();
54
+ const { schema } = view.state;
55
+ const coordinates = view.posAtCoords({
56
+ left: event.clientX,
57
+ top: event.clientY,
58
+ });
59
+ if (!coordinates)
60
+ return false;
61
+ images.forEach(async (image) => {
62
+ const reader = new FileReader();
63
+ if (upload) {
64
+ const node = schema.nodes.image.create({
65
+ src: await upload(image),
66
+ });
67
+ const transaction = view.state.tr.insert(coordinates.pos, node);
68
+ view.dispatch(transaction);
69
+ }
70
+ else {
71
+ reader.onload = (readerEvent) => {
72
+ const node = schema.nodes.image.create({
73
+ src: readerEvent.target?.result,
74
+ });
75
+ const transaction = view.state.tr.insert(coordinates.pos, node);
76
+ view.dispatch(transaction);
77
+ };
78
+ reader.readAsDataURL(image);
79
+ }
80
+ });
81
+ return true;
82
+ },
83
+ },
84
+ },
85
+ });
86
+ };
@@ -1,5 +1,6 @@
1
1
  import Image from "@tiptap/extension-image";
2
2
  import { mergeAttributes } from "@tiptap/core";
3
+ import { dropImagePlugin } from "./dragdrop";
3
4
  export default Image.extend({
4
5
  addOptions() {
5
6
  return {
@@ -11,5 +12,8 @@ export default Image.extend({
11
12
  renderHTML({ HTMLAttributes }) {
12
13
  const { style } = HTMLAttributes;
13
14
  return ["figure", { style }, ["img", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]];
14
- }
15
+ },
16
+ addProseMirrorPlugins() {
17
+ return [dropImagePlugin()];
18
+ },
15
19
  }).configure({ HTMLAttributes: { crossorigin: 'anonymous' } });
@@ -63,7 +63,7 @@ $grip-margin: 3px;
63
63
  }
64
64
  }
65
65
 
66
- .editor & {
66
+ .editable & {
67
67
  .grip-column, .grip-row, .grip-table {
68
68
  opacity: 1;
69
69
  cursor: pointer;
@@ -9,7 +9,7 @@
9
9
  background-color: var(--primary-light6);
10
10
  opacity: 0;
11
11
 
12
- .editor & {
12
+ .editable & {
13
13
  opacity: 1;
14
14
  }
15
15
  }
@@ -17,7 +17,7 @@
17
17
  &.resize-cursor {
18
18
  pointer-events: none;
19
19
 
20
- .editor & {
20
+ .editable & {
21
21
  pointer-events: initial;
22
22
  cursor: ew-resize;
23
23
  cursor: col-resize; /* stylelint-disable declaration-block-no-duplicate-properties */
@@ -69,7 +69,7 @@
69
69
  }
70
70
  }
71
71
 
72
- .editor {
72
+ .editable {
73
73
  .ProseMirror {
74
74
  .tableWrapper {
75
75
  padding: 10px;
@@ -1,5 +1,4 @@
1
1
  @import './style/grip';
2
2
  @import './style/table';
3
- @import './style/theme';
4
3
  @import './style/cell';
5
4
  @import './style/resize';
@@ -1,16 +1,17 @@
1
- <script>import { Button, IconButton, Input, List, TwoLine } from "nunui";
1
+ <script>import { Button, IconButton, Input, List, OneLine, TwoLine } from "nunui";
2
2
  import { getContext } from "svelte";
3
- import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from '../plugin/command/stores';
3
+ import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail, slashSelection } from '../plugin/command/stores';
4
4
  import { fly, slide } from "svelte/transition";
5
5
  import { quartOut } from "svelte/easing";
6
6
  import i18n from "../i18n";
7
7
  const tiptap = getContext('editor');
8
8
  export let selectedIndex = 0;
9
9
  let height = 0, elements = [];
10
- let iframe = '';
10
+ let iframe = '', focus;
11
11
  $: if ($slashVisible) {
12
12
  iframe = '';
13
13
  }
14
+ $: setTimeout(() => focus?.focus?.(), 100);
14
15
  </script>
15
16
 
16
17
  <svelte:window bind:innerHeight={height}/>
@@ -26,16 +27,19 @@ $: if ($slashVisible) {
26
27
  <IconButton icon="arrow_back" on:click={() => $slashDetail = ''}/>
27
28
  <div class="title">iframe</div>
28
29
  </header>
29
- <Input placeholder="url" fullWidth bind:value={iframe} autofocus
30
+ <Input placeholder="url" fullWidth bind:value={iframe} bind:input={focus}
30
31
  on:submit={() => $tiptap.commands.insertContent({type: 'iframe', attrs: {src: iframe}})}/>
31
32
  <footer>
32
33
  <Button tabindex="0" transparent small on:click={() => {
33
- iframe = ''
34
- $slashDetail = ''
35
- }}>{i18n('cancel')}
34
+ iframe = ''
35
+ $slashDetail = ''
36
+ }}>{i18n('cancel')}
36
37
  </Button>
37
38
  <Button tabindex="0" transparent small
38
- on:click={() => $tiptap.commands.insertContent({type: 'iframe', attrs: {src: iframe}})}>{i18n('insert')}
39
+ on:click={() => {
40
+ $slashSelection?.();
41
+ $tiptap.commands.insertContent({type: 'iframe', attrs: {src: iframe}})}
42
+ }>{i18n('insert')}
39
43
  </Button>
40
44
  </footer>
41
45
  </div>
@@ -45,31 +49,52 @@ $: if ($slashVisible) {
45
49
  <IconButton icon="arrow_back" on:click={() => $slashDetail = ''}/>
46
50
  <div class="title">Youtube</div>
47
51
  </header>
48
- <Input placeholder="url" fullWidth bind:value={iframe} autofocus
52
+ <Input placeholder="url" fullWidth bind:value={iframe} bind:input={focus}
49
53
  on:submit={() => $tiptap.commands.insertVideoPlayer({url: iframe})}/>
50
54
  <footer>
51
55
  <Button tabindex="0" transparent small on:click={() => {
52
- iframe = ''
53
- $slashDetail = ''
54
- }}>{i18n('cancel')}
56
+ iframe = ''
57
+ $slashDetail = ''
58
+ }}>{i18n('cancel')}
55
59
  </Button>
56
- <Button tabindex="0" transparent small
57
- on:click={() => $tiptap.commands.insertVideoPlayer({url: iframe})}>{i18n('insert')}
60
+ <Button tabindex="0" transparent small on:click={() => {
61
+ $slashSelection?.();
62
+ $tiptap.commands.insertVideoPlayer({url: iframe});
63
+ }}>{i18n('insert')}
58
64
  </Button>
59
65
  </footer>
60
66
  </div>
67
+ {:else if $slashDetail === 'emoji'}
68
+ <div class="list">
69
+ <List>
70
+ {#each $slashItems as {title, command}, i(title)}
71
+ <div transition:slide={{duration: 400, easing: quartOut}}>
72
+ <OneLine on:click={() => {
73
+ command?.($slashProps);
74
+ setTimeout(() => $tiptap.commands.focus());
75
+ }} bind:this={elements[i]} {title} active={selectedIndex === i}/>
76
+ </div>
77
+ {/each}
78
+ {#if !$slashItems.length}
79
+ <div class="section"
80
+ transition:slide={{duration: 400, easing: quartOut}}>{i18n('noResult')}</div>
81
+ {/if}
82
+ </List>
83
+ </div>
61
84
  {:else}
62
85
  <div class="list">
63
86
  <List>
64
- {#each $slashItems as {section, list}(section)}
87
+ {#each $slashItems as {section, list}, j(section)}
88
+ {@const lastCount = $slashItems.slice(0, j).reduce((acc, cur) => acc + cur.list.length, 0)}
65
89
  <div class="section" transition:slide={{duration: 400, easing: quartOut}}>{section}</div>
66
90
  <div transition:slide={{duration: 400, easing: quartOut}}>
67
91
  {#each list || [] as {title, subtitle, icon, command, section}, i(title)}
68
92
  <div transition:slide={{duration: 400, easing: quartOut}}>
69
- <TwoLine on:mouseenter={() => (selectedIndex = i)} on:click={() => {
93
+ <TwoLine on:mouseenter={() => (selectedIndex = i + lastCount)} on:click={() => {
70
94
  command?.($slashProps);
71
95
  setTimeout(() => $tiptap.commands.focus());
72
- }} bind:this={elements[i]} {icon} {title} subtitle={subtitle || ''}/>
96
+ }} bind:this={elements[i + lastCount]} {icon} {title} subtitle={subtitle || ''}
97
+ active={selectedIndex === i + lastCount}/>
73
98
  </div>
74
99
  {/each}
75
100
  </div>
@@ -98,6 +123,7 @@ main {
98
123
  position: fixed;
99
124
  background: var(--surface, #fff);
100
125
  width: 220px;
126
+ max-height: 384px;
101
127
  border-radius: 4px;
102
128
  overflow-y: scroll;
103
129
  z-index: 10;
@@ -8,6 +8,7 @@ import Floating from "./Floating.svelte";
8
8
  import Command from "./Command.svelte";
9
9
  import { slashItems, slashProps, slashVisible } from "../plugin/command/stores";
10
10
  import i18n from "../i18n";
11
+ import { fallbackUpload } from "../plugin/image/dragdrop";
11
12
  const san = (body) => sanitizeHtml(body, {
12
13
  allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'math-inline', 'math-node', 'iframe', 'tiptap-file', 'lite-youtube', 'blockquote']),
13
14
  allowedStyles: '*', allowedAttributes: {
@@ -21,11 +22,12 @@ const san = (body) => sanitizeHtml(body, {
21
22
  'lite-youtube': ['videoid', 'params', 'nocookie', 'title', 'provider'],
22
23
  },
23
24
  });
24
- export let body = '', editable = false, style = '', ref = null, options = {};
25
+ export let body = '', editable = false, ref = null, options = {};
26
+ export const imageUpload = fallbackUpload, style = '';
25
27
  const tiptap = setContext('editor', writable(null));
26
28
  let element, fullscreen = false, mounted = false, last = '';
27
- $: ref = $tiptap;
28
29
  $: $tiptap && $tiptap.setEditable(editable);
30
+ $: browser && (window.__image_uploader = imageUpload);
29
31
  if (browser) {
30
32
  onMount(() => {
31
33
  body = last = san(body);
@@ -33,9 +35,9 @@ if (browser) {
33
35
  Promise.all([import('./tiptap'), import("@justinribeiro/lite-youtube")]).then(([{ default: tt }]) => {
34
36
  if (!mounted)
35
37
  return;
36
- $tiptap = tt(element, body, {
38
+ ref = $tiptap = tt(element, body, {
37
39
  editable: editable,
38
- onTransaction: () => $tiptap = $tiptap,
40
+ onTransaction: () => ref = $tiptap = $tiptap,
39
41
  ...options,
40
42
  });
41
43
  $tiptap.on('update', ({ editor: tiptap }) => {
@@ -62,14 +64,17 @@ $: selectedIndex = $slashVisible ? selectedIndex : 0;
62
64
  function handleKeydown(event) {
63
65
  if (!$slashVisible)
64
66
  return;
67
+ let count = $slashItems.length;
68
+ if ($slashItems[0]?.list)
69
+ count = $slashItems.reduce((acc, item) => acc + item.list.length, 0);
65
70
  if (event.key === 'ArrowUp') {
66
71
  event.preventDefault();
67
- selectedIndex = (selectedIndex + $slashItems.length - 1) % $slashItems.length;
72
+ selectedIndex = (selectedIndex + count - 1) % count;
68
73
  return true;
69
74
  }
70
75
  if (event.key === 'ArrowDown') {
71
76
  event.preventDefault();
72
- selectedIndex = (selectedIndex + 1) % $slashItems.length;
77
+ selectedIndex = (selectedIndex + 1) % count;
73
78
  return true;
74
79
  }
75
80
  if (event.key === 'Enter') {
@@ -80,10 +85,10 @@ function handleKeydown(event) {
80
85
  return false;
81
86
  }
82
87
  function selectItem(index) {
83
- const item = $slashItems[index];
88
+ const item = $slashItems[0]?.list ? $slashItems.map(i => i.list).flat()[index] : $slashItems[index];
84
89
  if (item) {
85
90
  let range = $slashProps.range;
86
- item.command({ editor: editable, range });
91
+ item.command({ editor: $tiptap, range });
87
92
  }
88
93
  }
89
94
  </script>
@@ -1,12 +1,14 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  import "@seorii/prosemirror-math/style.css";
3
+ import type { UploadFn } from "../plugin/image/dragdrop";
3
4
  declare const __propDef: {
4
5
  props: {
5
6
  body?: string | undefined;
6
7
  editable?: boolean | undefined;
7
- style?: string | undefined;
8
- ref?: any;
8
+ ref?: null | undefined;
9
9
  options?: {} | undefined;
10
+ imageUpload?: UploadFn | undefined;
11
+ style?: "" | undefined;
10
12
  };
11
13
  events: {
12
14
  [evt: string]: CustomEvent<any>;
@@ -19,5 +21,7 @@ export type TipTapProps = typeof __propDef.props;
19
21
  export type TipTapEvents = typeof __propDef.events;
20
22
  export type TipTapSlots = typeof __propDef.slots;
21
23
  export default class TipTap extends SvelteComponentTyped<TipTapProps, TipTapEvents, TipTapSlots> {
24
+ get imageUpload(): UploadFn;
25
+ get style(): "";
22
26
  }
23
27
  export {};
@@ -23,55 +23,60 @@ import Iframe from "../plugin/iframe";
23
23
  // @ts-ignore
24
24
  import { MathInline, MathBlock } from "@seorii/prosemirror-math/tiptap";
25
25
  import Youtube from "../plugin/youtube";
26
- import command from "../plugin/command";
27
- export default (element, content, { plugins = [], ...props } = {}) => new Editor({
28
- element, content, ...props,
29
- extensions: [
30
- CodeBlockLowlight.extend({
31
- addKeyboardShortcuts() {
32
- return {
33
- ...this.parent?.(),
34
- 'Tab': () => {
35
- if (this.editor.isActive('codeBlock')) {
36
- return this.editor.commands.insertContent(' ');
26
+ import command from "../plugin/command/suggest";
27
+ import emoji from "../plugin/command/emoji";
28
+ export default (element, content, { plugins = [], ...props } = {}) => {
29
+ const tt = new Editor({
30
+ element, content, ...props,
31
+ extensions: [
32
+ CodeBlockLowlight.extend({
33
+ addKeyboardShortcuts() {
34
+ return {
35
+ ...this.parent?.(),
36
+ 'Tab': () => {
37
+ if (this.editor.isActive('codeBlock')) {
38
+ return this.editor.commands.insertContent(' ');
39
+ }
40
+ return true;
37
41
  }
38
- return true;
39
- }
40
- };
41
- }
42
- }).configure({ lowlight }),
43
- Image,
44
- Youtube,
45
- StarterKit,
46
- Underline,
47
- Highlight.configure({ multicolor: true }),
48
- Link.configure({
49
- openOnClick: true, protocols: ['ftp', 'mailto', {
50
- scheme: 'tel',
51
- optionalSlashes: true
52
- }]
53
- }),
54
- TextAlign.configure({ types: ['heading', 'paragraph', 'image'] }),
55
- DropCursor,
56
- orderedlist,
57
- MathInline,
58
- MathBlock,
59
- table,
60
- tableHeader,
61
- tableRow,
62
- tableCell,
63
- Superscript,
64
- Subscript,
65
- Indent,
66
- Color,
67
- TextStyle,
68
- Iframe,
69
- Code.extend({
70
- renderHTML({ HTMLAttributes }) {
71
- return ['code', mergeAttributes(HTMLAttributes, { class: 'inline' })];
72
- }
73
- }),
74
- command,
75
- ...plugins,
76
- ],
77
- });
42
+ };
43
+ }
44
+ }).configure({ lowlight }),
45
+ Image,
46
+ Youtube,
47
+ StarterKit,
48
+ Underline,
49
+ Highlight.configure({ multicolor: true }),
50
+ Link.configure({
51
+ openOnClick: true, protocols: ['ftp', 'mailto', {
52
+ scheme: 'tel',
53
+ optionalSlashes: true
54
+ }]
55
+ }),
56
+ TextAlign.configure({ types: ['heading', 'paragraph', 'image'] }),
57
+ DropCursor,
58
+ orderedlist,
59
+ MathInline,
60
+ MathBlock,
61
+ table,
62
+ tableHeader,
63
+ tableRow,
64
+ tableCell,
65
+ Superscript,
66
+ Subscript,
67
+ Indent,
68
+ Color,
69
+ TextStyle,
70
+ Iframe,
71
+ Code.extend({
72
+ renderHTML({ HTMLAttributes }) {
73
+ return ['code', mergeAttributes(HTMLAttributes, { class: 'inline' })];
74
+ }
75
+ }),
76
+ ...plugins,
77
+ ],
78
+ });
79
+ tt.registerPlugin(emoji(tt));
80
+ tt.registerPlugin(command(tt));
81
+ return tt;
82
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seorii/tiptap",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "svelte-kit sync && svelte-package",
@@ -60,6 +60,8 @@
60
60
  "@tiptap/pm": "^2.0.4",
61
61
  "@tiptap/starter-kit": "^2.0.4",
62
62
  "@tiptap/suggestion": "^2.0.4",
63
+ "emojis-keywords": "2.0.0",
64
+ "emojis-list": "2.0.0",
63
65
  "lowlight": "^2.9.0",
64
66
  "nunui": "^0.0.101",
65
67
  "prosemirror-commands": "^1.5.2",
@@ -1,4 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- export declare const Command: Extension<any, any>;
3
- declare const _default: Extension<any, any>;
4
- export default _default;
@@ -1,27 +0,0 @@
1
- import { Extension } from '@tiptap/core';
2
- import Suggestion from '@tiptap/suggestion';
3
- import suggestion from "./suggest";
4
- export const Command = Extension.create({
5
- name: 'slash',
6
- addOptions() {
7
- return {
8
- suggestion: {
9
- char: '/',
10
- command: ({ editor, range, props }) => {
11
- props.command({ editor, range });
12
- }
13
- }
14
- };
15
- },
16
- addProseMirrorPlugins() {
17
- return [
18
- Suggestion({
19
- editor: this.editor,
20
- ...this.options.suggestion
21
- })
22
- ];
23
- }
24
- });
25
- export default Command.configure({
26
- suggestion
27
- });
@@ -1,6 +0,0 @@
1
- $highlight-color: #00bcd4;
2
-
3
- .ProseMirror {
4
- --highlight-color: #{$highlight-color};
5
- --highlight-bg-color: #{$highlight-color}33;
6
- }
File without changes