@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.
@@ -24,6 +24,8 @@ declare const _default: {
24
24
  codeBlock: string;
25
25
  mathBlock: string;
26
26
  table: string;
27
+ twoColumns: string;
28
+ threeColumns: string;
27
29
  image: string;
28
30
  iframe: string;
29
31
  youtube: string;
@@ -36,6 +38,8 @@ declare const _default: {
36
38
  codeBlockInfo: string;
37
39
  mathBlockInfo: string;
38
40
  tableInfo: string;
41
+ twoColumnsInfo: string;
42
+ threeColumnsInfo: string;
39
43
  imageInfo: string;
40
44
  iframeInfo: string;
41
45
  youtubeInfo: string;
@@ -24,6 +24,8 @@ export default {
24
24
  codeBlock: 'Code block',
25
25
  mathBlock: 'Math block',
26
26
  table: 'Table',
27
+ twoColumns: '2 Columns',
28
+ threeColumns: '3 Columns',
27
29
  image: 'Image',
28
30
  iframe: 'iframe',
29
31
  youtube: 'Youtube',
@@ -36,6 +38,8 @@ export default {
36
38
  codeBlockInfo: 'Code block with syntax highlighting',
37
39
  mathBlockInfo: 'Math block',
38
40
  tableInfo: 'Table',
41
+ twoColumnsInfo: 'Two-column layout',
42
+ threeColumnsInfo: 'Three-column layout',
39
43
  imageInfo: 'Image',
40
44
  iframeInfo: 'Embed another website',
41
45
  youtubeInfo: 'Embed Youtube video',
@@ -24,6 +24,8 @@ declare const locales: readonly [{
24
24
  codeBlock: string;
25
25
  mathBlock: string;
26
26
  table: string;
27
+ twoColumns: string;
28
+ threeColumns: string;
27
29
  image: string;
28
30
  iframe: string;
29
31
  youtube: string;
@@ -36,6 +38,8 @@ declare const locales: readonly [{
36
38
  codeBlockInfo: string;
37
39
  mathBlockInfo: string;
38
40
  tableInfo: string;
41
+ twoColumnsInfo: string;
42
+ threeColumnsInfo: string;
39
43
  imageInfo: string;
40
44
  iframeInfo: string;
41
45
  youtubeInfo: string;
@@ -69,6 +73,8 @@ declare const locales: readonly [{
69
73
  codeBlock: string;
70
74
  mathBlock: string;
71
75
  table: string;
76
+ twoColumns: string;
77
+ threeColumns: string;
72
78
  image: string;
73
79
  iframe: string;
74
80
  youtube: string;
@@ -81,6 +87,8 @@ declare const locales: readonly [{
81
87
  codeBlockInfo: string;
82
88
  mathBlockInfo: string;
83
89
  tableInfo: string;
90
+ twoColumnsInfo: string;
91
+ threeColumnsInfo: string;
84
92
  imageInfo: string;
85
93
  iframeInfo: string;
86
94
  youtubeInfo: string;
@@ -24,6 +24,8 @@ declare const _default: {
24
24
  codeBlock: string;
25
25
  mathBlock: string;
26
26
  table: string;
27
+ twoColumns: string;
28
+ threeColumns: string;
27
29
  image: string;
28
30
  iframe: string;
29
31
  youtube: string;
@@ -36,6 +38,8 @@ declare const _default: {
36
38
  codeBlockInfo: string;
37
39
  mathBlockInfo: string;
38
40
  tableInfo: string;
41
+ twoColumnsInfo: string;
42
+ threeColumnsInfo: string;
39
43
  imageInfo: string;
40
44
  iframeInfo: string;
41
45
  youtubeInfo: string;
@@ -24,6 +24,8 @@ export default {
24
24
  codeBlock: '코드 블록',
25
25
  mathBlock: '수식 블록',
26
26
  table: '테이블',
27
+ twoColumns: '2열',
28
+ threeColumns: '3열',
27
29
  image: '이미지',
28
30
  iframe: 'iframe',
29
31
  youtube: '유튜브',
@@ -36,6 +38,8 @@ export default {
36
38
  codeBlockInfo: '하이라이팅되는 코드 블록',
37
39
  mathBlockInfo: '가운데로 정렬되는 수식 블록',
38
40
  tableInfo: '표 삽입',
41
+ twoColumnsInfo: '2열 레이아웃',
42
+ threeColumnsInfo: '3열 레이아웃',
39
43
  imageInfo: '이미지',
40
44
  iframeInfo: '다른 웹사이트 삽입',
41
45
  youtubeInfo: 'Youtube 동영상 삽입',
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export default TipTap;
2
- export { getDetail };
3
2
  import TipTap from './tiptap/index.js';
4
3
  import { getDetail } from './plugin/command/suggest.js';
4
+ import { insertUploadSkeleton } from './plugin/upload/skeleton/index.js';
5
+ export { getDetail, insertUploadSkeleton };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Reexport your entry components here
2
2
  import TipTap from './tiptap/index.js';
3
3
  import { getDetail } from './plugin/command/suggest.js';
4
+ import { insertUploadSkeleton } from './plugin/upload/skeleton/index.js';
4
5
 
5
6
  export default TipTap;
6
- export { getDetail };
7
+ export { getDetail, insertUploadSkeleton };
@@ -0,0 +1,14 @@
1
+ import { Node } from '@tiptap/core';
2
+ import './style.css';
3
+ type ColumnCount = 2 | 3;
4
+ declare module '@tiptap/core' {
5
+ interface Commands<ReturnType> {
6
+ columnLayout: {
7
+ setColumns: (count: ColumnCount) => ReturnType;
8
+ setTwoColumns: () => ReturnType;
9
+ setThreeColumns: () => ReturnType;
10
+ };
11
+ }
12
+ }
13
+ declare const _default: Node<any, any>[];
14
+ export default _default;
@@ -0,0 +1,88 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core';
2
+ import './style.css';
3
+ const normalizeColumnCount = (value) => (Number(value) === 3 ? 3 : 2);
4
+ const Column = Node.create({
5
+ name: 'column',
6
+ content: 'block+',
7
+ isolating: true,
8
+ defining: true,
9
+ addOptions() {
10
+ return {
11
+ HTMLAttributes: {}
12
+ };
13
+ },
14
+ parseHTML() {
15
+ return [{ tag: 'div.tiptap-column' }];
16
+ },
17
+ renderHTML({ HTMLAttributes }) {
18
+ return [
19
+ 'div',
20
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
21
+ class: 'tiptap-column'
22
+ }),
23
+ 0
24
+ ];
25
+ }
26
+ });
27
+ const Columns = Node.create({
28
+ name: 'columns',
29
+ group: 'block',
30
+ content: 'column{2,3}',
31
+ isolating: true,
32
+ defining: true,
33
+ draggable: true,
34
+ addOptions() {
35
+ return {
36
+ HTMLAttributes: {}
37
+ };
38
+ },
39
+ addAttributes() {
40
+ return {
41
+ count: {
42
+ default: 2,
43
+ parseHTML: (element) => {
44
+ if (!(element instanceof HTMLElement))
45
+ return 2;
46
+ if (element.classList.contains('columns-3'))
47
+ return 3;
48
+ if (element.classList.contains('columns-2'))
49
+ return 2;
50
+ const columnCount = Array.from(element.children).filter((child) => child instanceof HTMLElement && child.classList.contains('tiptap-column')).length;
51
+ return columnCount >= 3 ? 3 : 2;
52
+ },
53
+ renderHTML: () => ({})
54
+ }
55
+ };
56
+ },
57
+ parseHTML() {
58
+ return [{ tag: 'div.tiptap-columns' }];
59
+ },
60
+ renderHTML({ node, HTMLAttributes }) {
61
+ const count = normalizeColumnCount(node.attrs.count);
62
+ return [
63
+ 'div',
64
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
65
+ class: `tiptap-columns columns-${count}`
66
+ }),
67
+ 0
68
+ ];
69
+ },
70
+ addCommands() {
71
+ return {
72
+ setColumns: (count) => ({ commands }) => {
73
+ const normalizedCount = normalizeColumnCount(count);
74
+ return commands.insertContent({
75
+ type: this.name,
76
+ attrs: { count: normalizedCount },
77
+ content: Array.from({ length: normalizedCount }, () => ({
78
+ type: 'column',
79
+ content: [{ type: 'paragraph' }]
80
+ }))
81
+ });
82
+ },
83
+ setTwoColumns: () => ({ commands }) => commands.setColumns(2),
84
+ setThreeColumns: () => ({ commands }) => commands.setColumns(3)
85
+ };
86
+ }
87
+ });
88
+ export default [Columns, Column];
@@ -0,0 +1,37 @@
1
+ .tiptap-columns {
2
+ display: grid;
3
+ gap: 12px;
4
+ margin: 12px 0;
5
+ }
6
+
7
+ .tiptap-columns.columns-2 {
8
+ grid-template-columns: repeat(2, minmax(0, 1fr));
9
+ }
10
+
11
+ .tiptap-columns.columns-3 {
12
+ grid-template-columns: repeat(3, minmax(0, 1fr));
13
+ }
14
+
15
+ .tiptap-column {
16
+ min-width: 0;
17
+ padding: 10px 12px;
18
+ border: 0;
19
+ border-radius: 10px;
20
+ background: transparent;
21
+ }
22
+
23
+ .tiptap-column > :first-child {
24
+ margin-top: 0;
25
+ }
26
+
27
+ .tiptap-column > :last-child {
28
+ margin-bottom: 0;
29
+ }
30
+
31
+ .editable .tiptap-column {
32
+ border: 1px dashed var(--primary-light3, rgba(112, 112, 112, 0.35));
33
+ }
34
+
35
+ .editable .tiptap-column:hover {
36
+ border-color: var(--primary-light7, rgba(112, 112, 112, 0.65));
37
+ }
@@ -3,6 +3,7 @@ import i18n from '../../i18n';
3
3
  import enUs from '../../i18n/en-us/index';
4
4
  import koKr from '../../i18n/ko-kr/index';
5
5
  import { fallbackUpload, releaseObjectUrlOnImageSettled } from '../image/dragdrop';
6
+ import { insertUploadSkeleton } from '../upload/skeleton';
6
7
  import { PluginKey, TextSelection } from '@tiptap/pm/state';
7
8
  import Suggestion, {} from '@tiptap/suggestion';
8
9
  const normalizeSearch = (value) => value.toLowerCase().trim();
@@ -156,10 +157,27 @@ export const suggest = {
156
157
  const file = input.files[0];
157
158
  if (!file)
158
159
  return;
159
- const upload = window.__image_uploader ?? fallbackUpload;
160
- const src = await upload(file);
161
- editor.chain().focus().deleteRange(range).setImage({ src }).run();
162
- releaseObjectUrlOnImageSettled(editor.view, src);
160
+ const skeleton = insertUploadSkeleton(editor, {
161
+ kind: 'image',
162
+ height: 220
163
+ });
164
+ try {
165
+ const upload = window.__image_uploader ?? fallbackUpload;
166
+ const src = await upload(file);
167
+ if (skeleton) {
168
+ skeleton.replaceWith({
169
+ type: 'image',
170
+ attrs: { src }
171
+ });
172
+ }
173
+ else {
174
+ editor.chain().focus().setImage({ src }).run();
175
+ }
176
+ releaseObjectUrlOnImageSettled(editor.view, src);
177
+ }
178
+ catch {
179
+ skeleton?.remove();
180
+ }
163
181
  };
164
182
  input.click();
165
183
  }
@@ -230,6 +248,24 @@ export const suggest = {
230
248
  .run();
231
249
  }
232
250
  },
251
+ {
252
+ icon: 'view_column_2',
253
+ title: i18n('twoColumns'),
254
+ subtitle: i18n('twoColumnsInfo'),
255
+ keywords: createKeywords(['twoColumns', 'twoColumnsInfo'], ['2 columns', 'two columns', 'notion']),
256
+ command: ({ editor, range }) => {
257
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setTwoColumns().run();
258
+ }
259
+ },
260
+ {
261
+ icon: 'view_week',
262
+ title: i18n('threeColumns'),
263
+ subtitle: i18n('threeColumnsInfo'),
264
+ keywords: createKeywords(['threeColumns', 'threeColumnsInfo'], ['3 columns', 'three columns', 'notion']),
265
+ command: ({ editor, range }) => {
266
+ editor.chain().focus().deleteRange(fixRange(editor, range)).setThreeColumns().run();
267
+ }
268
+ },
233
269
  {
234
270
  icon: 'format_quote',
235
271
  title: i18n('blockquote'),
@@ -34,7 +34,7 @@ export default Node.create({
34
34
  renderHTML({ HTMLAttributes }) {
35
35
  return [
36
36
  'div',
37
- this.options.HTMLAttributes,
37
+ mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
38
38
  ['embed', mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })]
39
39
  ];
40
40
  },
@@ -31,7 +31,7 @@ export default Node.create({
31
31
  renderHTML({ HTMLAttributes }) {
32
32
  return [
33
33
  'div',
34
- this.options.HTMLAttributes,
34
+ mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
35
35
  [
36
36
  'iframe',
37
37
  mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })
@@ -1,4 +1,5 @@
1
1
  import { Plugin } from 'prosemirror-state';
2
+ import { insertUploadSkeleton } from '../upload/skeleton';
2
3
  export const fallbackUpload = async (image) => URL.createObjectURL(image);
3
4
  const OBJECT_URL_PREFIX = 'blob:';
4
5
  const OBJECT_URL_REVOKE_TIMEOUT_MS = 30_000;
@@ -73,14 +74,31 @@ export const dropImagePlugin = () => {
73
74
  const image = item.getAsFile();
74
75
  if (item.type.indexOf('image') === 0) {
75
76
  event.preventDefault();
77
+ const skeleton = insertUploadSkeleton({
78
+ state: view.state,
79
+ view
80
+ }, {
81
+ kind: 'image',
82
+ height: 220
83
+ });
76
84
  if (upload && image) {
77
- upload(image).then((src) => {
78
- const node = schema.nodes.image.create({
79
- src: src
80
- });
81
- const transaction = view.state.tr.replaceSelectionWith(node);
82
- view.dispatch(transaction);
85
+ upload(image)
86
+ .then((src) => {
87
+ if (skeleton) {
88
+ skeleton.replaceWith({
89
+ type: 'image',
90
+ attrs: { src }
91
+ });
92
+ }
93
+ else {
94
+ const node = schema.nodes.image.create({ src });
95
+ const transaction = view.state.tr.replaceSelectionWith(node);
96
+ view.dispatch(transaction);
97
+ }
83
98
  releaseObjectUrlOnImageSettled(view, src);
99
+ })
100
+ .catch(() => {
101
+ skeleton?.remove();
84
102
  });
85
103
  }
86
104
  }
@@ -120,20 +138,49 @@ export const dropImagePlugin = () => {
120
138
  return false;
121
139
  images.forEach(async (image) => {
122
140
  const reader = new FileReader();
141
+ const skeleton = insertUploadSkeleton({
142
+ state: view.state,
143
+ view
144
+ }, {
145
+ kind: 'image',
146
+ height: 220,
147
+ at: coordinates.pos
148
+ });
123
149
  if (upload) {
124
- const src = await upload(image);
125
- const node = schema.nodes.image.create({
126
- src
127
- });
128
- const transaction = view.state.tr.insert(coordinates.pos, node);
129
- view.dispatch(transaction);
130
- releaseObjectUrlOnImageSettled(view, src);
150
+ try {
151
+ const src = await upload(image);
152
+ if (skeleton) {
153
+ skeleton.replaceWith({
154
+ type: 'image',
155
+ attrs: { src }
156
+ });
157
+ }
158
+ else {
159
+ const node = schema.nodes.image.create({ src });
160
+ const transaction = view.state.tr.insert(coordinates.pos, node);
161
+ view.dispatch(transaction);
162
+ }
163
+ releaseObjectUrlOnImageSettled(view, src);
164
+ }
165
+ catch {
166
+ skeleton?.remove();
167
+ }
131
168
  }
132
169
  else {
133
170
  reader.onload = (readerEvent) => {
134
- const node = schema.nodes.image.create({
135
- src: readerEvent.target?.result
136
- });
171
+ const src = readerEvent.target?.result;
172
+ if (typeof src !== 'string') {
173
+ skeleton?.remove();
174
+ return;
175
+ }
176
+ if (skeleton) {
177
+ skeleton.replaceWith({
178
+ type: 'image',
179
+ attrs: { src }
180
+ });
181
+ return;
182
+ }
183
+ const node = schema.nodes.image.create({ src });
137
184
  const transaction = view.state.tr.insert(coordinates.pos, node);
138
185
  view.dispatch(transaction);
139
186
  };
@@ -14,7 +14,7 @@ export default (crossorigin = 'anonymous') => Image.extend({
14
14
  const style = HTMLAttributes.style;
15
15
  return [
16
16
  'figure',
17
- { style },
17
+ { style, 'data-bubble-menu': 'false' },
18
18
  ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
19
19
  ];
20
20
  },
@@ -0,0 +1,8 @@
1
+ import { Extension } from '@tiptap/core';
2
+ export type ResizeOptions = {
3
+ attributeTypes: string[];
4
+ showHandleAlways?: boolean;
5
+ showHandleOnActive?: boolean;
6
+ };
7
+ declare const _default: Extension<ResizeOptions, any>;
8
+ export default _default;