@seorii/tiptap 0.4.1 → 0.4.2

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,51 @@
1
+ declare const _default: {
2
+ target: string[];
3
+ lang: string;
4
+ country: string;
5
+ text: string;
6
+ block: string;
7
+ loading: string;
8
+ delete: string;
9
+ close: string;
10
+ cancel: string;
11
+ insert: string;
12
+ noResult: string;
13
+ default: string;
14
+ auto: string;
15
+ title: string;
16
+ paragraph: string;
17
+ link: string;
18
+ alignLeft: string;
19
+ alignCenter: string;
20
+ alignRight: string;
21
+ alignJustify: string;
22
+ unorderedList: string;
23
+ numberList: string;
24
+ codeBlock: string;
25
+ mathBlock: string;
26
+ table: string;
27
+ twoColumns: string;
28
+ threeColumns: string;
29
+ image: string;
30
+ iframe: string;
31
+ youtube: string;
32
+ blockquote: string;
33
+ title1Info: string;
34
+ title2Info: string;
35
+ title3Info: string;
36
+ unorderedListInfo: string;
37
+ numberListInfo: string;
38
+ codeBlockInfo: string;
39
+ mathBlockInfo: string;
40
+ tableInfo: string;
41
+ twoColumnsInfo: string;
42
+ threeColumnsInfo: string;
43
+ imageInfo: string;
44
+ iframeInfo: string;
45
+ youtubeInfo: string;
46
+ blockquoteInfo: string;
47
+ newLineInfo: string;
48
+ placeholder: string;
49
+ insertCode: string;
50
+ };
51
+ export default _default;
@@ -0,0 +1,50 @@
1
+ export default {
2
+ target: ['es', 'es-ES', 'es-419', 'es-MX'],
3
+ lang: 'es',
4
+ country: 'ES',
5
+ text: 'Texto',
6
+ block: 'Bloque',
7
+ loading: 'Cargando...',
8
+ delete: 'Eliminar',
9
+ close: 'Cerrar',
10
+ cancel: 'Cancelar',
11
+ insert: 'Insertar',
12
+ noResult: 'Sin resultados',
13
+ default: 'predeterminado',
14
+ auto: 'auto',
15
+ title: 'Título',
16
+ paragraph: 'Párrafo',
17
+ link: 'Enlace',
18
+ alignLeft: 'Alinear a la izquierda',
19
+ alignCenter: 'Centrar',
20
+ alignRight: 'Alinear a la derecha',
21
+ alignJustify: 'Justificar',
22
+ unorderedList: 'Lista con viñetas',
23
+ numberList: 'Lista numerada',
24
+ codeBlock: 'Bloque de código',
25
+ mathBlock: 'Bloque matemático',
26
+ table: 'Tabla',
27
+ twoColumns: '2 columnas',
28
+ threeColumns: '3 columnas',
29
+ image: 'Imagen',
30
+ iframe: 'iframe',
31
+ youtube: 'YouTube',
32
+ blockquote: 'Cita',
33
+ title1Info: 'Título grande',
34
+ title2Info: 'Título más pequeño',
35
+ title3Info: 'Título mediano',
36
+ unorderedListInfo: 'Lista sin orden',
37
+ numberListInfo: '1, 2, 3, 4',
38
+ codeBlockInfo: 'Bloque de código con resaltado de sintaxis',
39
+ mathBlockInfo: 'Bloque matemático',
40
+ tableInfo: 'Tabla',
41
+ twoColumnsInfo: 'Diseño de dos columnas',
42
+ threeColumnsInfo: 'Diseño de tres columnas',
43
+ imageInfo: 'Imagen',
44
+ iframeInfo: 'Insertar otro sitio web',
45
+ youtubeInfo: 'Insertar video de YouTube',
46
+ blockquoteInfo: 'Cita',
47
+ newLineInfo: 'Pulsa / para abrir comandos. O',
48
+ placeholder: 'Escribe contenido aquí...',
49
+ insertCode: 'Insertar código'
50
+ };
@@ -96,6 +96,202 @@ declare const locales: readonly [{
96
96
  newLineInfo: string;
97
97
  placeholder: string;
98
98
  insertCode: string;
99
+ }, {
100
+ target: string[];
101
+ lang: string;
102
+ country: string;
103
+ text: string;
104
+ block: string;
105
+ loading: string;
106
+ delete: string;
107
+ close: string;
108
+ cancel: string;
109
+ insert: string;
110
+ noResult: string;
111
+ default: string;
112
+ auto: string;
113
+ title: string;
114
+ paragraph: string;
115
+ link: string;
116
+ alignLeft: string;
117
+ alignCenter: string;
118
+ alignRight: string;
119
+ alignJustify: string;
120
+ unorderedList: string;
121
+ numberList: string;
122
+ codeBlock: string;
123
+ mathBlock: string;
124
+ table: string;
125
+ twoColumns: string;
126
+ threeColumns: string;
127
+ image: string;
128
+ iframe: string;
129
+ youtube: string;
130
+ blockquote: string;
131
+ title1Info: string;
132
+ title2Info: string;
133
+ title3Info: string;
134
+ unorderedListInfo: string;
135
+ numberListInfo: string;
136
+ codeBlockInfo: string;
137
+ mathBlockInfo: string;
138
+ tableInfo: string;
139
+ twoColumnsInfo: string;
140
+ threeColumnsInfo: string;
141
+ imageInfo: string;
142
+ iframeInfo: string;
143
+ youtubeInfo: string;
144
+ blockquoteInfo: string;
145
+ newLineInfo: string;
146
+ placeholder: string;
147
+ insertCode: string;
148
+ }, {
149
+ target: string[];
150
+ lang: string;
151
+ country: string;
152
+ text: string;
153
+ block: string;
154
+ loading: string;
155
+ delete: string;
156
+ close: string;
157
+ cancel: string;
158
+ insert: string;
159
+ noResult: string;
160
+ default: string;
161
+ auto: string;
162
+ title: string;
163
+ paragraph: string;
164
+ link: string;
165
+ alignLeft: string;
166
+ alignCenter: string;
167
+ alignRight: string;
168
+ alignJustify: string;
169
+ unorderedList: string;
170
+ numberList: string;
171
+ codeBlock: string;
172
+ mathBlock: string;
173
+ table: string;
174
+ twoColumns: string;
175
+ threeColumns: string;
176
+ image: string;
177
+ iframe: string;
178
+ youtube: string;
179
+ blockquote: string;
180
+ title1Info: string;
181
+ title2Info: string;
182
+ title3Info: string;
183
+ unorderedListInfo: string;
184
+ numberListInfo: string;
185
+ codeBlockInfo: string;
186
+ mathBlockInfo: string;
187
+ tableInfo: string;
188
+ twoColumnsInfo: string;
189
+ threeColumnsInfo: string;
190
+ imageInfo: string;
191
+ iframeInfo: string;
192
+ youtubeInfo: string;
193
+ blockquoteInfo: string;
194
+ newLineInfo: string;
195
+ placeholder: string;
196
+ insertCode: string;
197
+ }, {
198
+ target: string[];
199
+ lang: string;
200
+ country: string;
201
+ text: string;
202
+ block: string;
203
+ loading: string;
204
+ delete: string;
205
+ close: string;
206
+ cancel: string;
207
+ insert: string;
208
+ noResult: string;
209
+ default: string;
210
+ auto: string;
211
+ title: string;
212
+ paragraph: string;
213
+ link: string;
214
+ alignLeft: string;
215
+ alignCenter: string;
216
+ alignRight: string;
217
+ alignJustify: string;
218
+ unorderedList: string;
219
+ numberList: string;
220
+ codeBlock: string;
221
+ mathBlock: string;
222
+ table: string;
223
+ twoColumns: string;
224
+ threeColumns: string;
225
+ image: string;
226
+ iframe: string;
227
+ youtube: string;
228
+ blockquote: string;
229
+ title1Info: string;
230
+ title2Info: string;
231
+ title3Info: string;
232
+ unorderedListInfo: string;
233
+ numberListInfo: string;
234
+ codeBlockInfo: string;
235
+ mathBlockInfo: string;
236
+ tableInfo: string;
237
+ twoColumnsInfo: string;
238
+ threeColumnsInfo: string;
239
+ imageInfo: string;
240
+ iframeInfo: string;
241
+ youtubeInfo: string;
242
+ blockquoteInfo: string;
243
+ newLineInfo: string;
244
+ placeholder: string;
245
+ insertCode: string;
246
+ }, {
247
+ target: string[];
248
+ lang: string;
249
+ country: string;
250
+ text: string;
251
+ block: string;
252
+ loading: string;
253
+ delete: string;
254
+ close: string;
255
+ cancel: string;
256
+ insert: string;
257
+ noResult: string;
258
+ default: string;
259
+ auto: string;
260
+ title: string;
261
+ paragraph: string;
262
+ link: string;
263
+ alignLeft: string;
264
+ alignCenter: string;
265
+ alignRight: string;
266
+ alignJustify: string;
267
+ unorderedList: string;
268
+ numberList: string;
269
+ codeBlock: string;
270
+ mathBlock: string;
271
+ table: string;
272
+ twoColumns: string;
273
+ threeColumns: string;
274
+ image: string;
275
+ iframe: string;
276
+ youtube: string;
277
+ blockquote: string;
278
+ title1Info: string;
279
+ title2Info: string;
280
+ title3Info: string;
281
+ unorderedListInfo: string;
282
+ numberListInfo: string;
283
+ codeBlockInfo: string;
284
+ mathBlockInfo: string;
285
+ tableInfo: string;
286
+ twoColumnsInfo: string;
287
+ threeColumnsInfo: string;
288
+ imageInfo: string;
289
+ iframeInfo: string;
290
+ youtubeInfo: string;
291
+ blockquoteInfo: string;
292
+ newLineInfo: string;
293
+ placeholder: string;
294
+ insertCode: string;
99
295
  }];
100
296
  type Locale = (typeof locales)[number];
101
297
  export type LocaleInput = string | Locale | null | undefined;
@@ -1,8 +1,25 @@
1
1
  import enUs from './en-us/index';
2
2
  import koKr from './ko-kr/index';
3
- const locales = [enUs, koKr];
3
+ import es from './es/index';
4
+ import ja from './ja/index';
5
+ import zhHans from './zh-hans/index';
6
+ import zhHant from './zh-hant/index';
7
+ const locales = [enUs, koKr, es, ja, zhHans, zhHant];
4
8
  export const I18N_CONTEXT = Symbol('tiptap-i18n');
5
- const normalizeLocaleCode = (value) => (value ?? '').trim().replace(/_/g, '-').toLowerCase();
9
+ const normalizeLocaleCode = (value) => {
10
+ const normalized = (value ?? '').trim().replace(/_/g, '-').toLowerCase();
11
+ if (!normalized)
12
+ return '';
13
+ if (!normalized.startsWith('zh'))
14
+ return normalized;
15
+ if (normalized.includes('hant') ||
16
+ normalized.startsWith('zh-tw') ||
17
+ normalized.startsWith('zh-hk') ||
18
+ normalized.startsWith('zh-mo')) {
19
+ return 'zh-hant';
20
+ }
21
+ return 'zh-hans';
22
+ };
6
23
  const findLocale = (localeList, language) => {
7
24
  const normalized = normalizeLocaleCode(language);
8
25
  if (!normalized)
@@ -0,0 +1,51 @@
1
+ declare const _default: {
2
+ target: string[];
3
+ lang: string;
4
+ country: string;
5
+ text: string;
6
+ block: string;
7
+ loading: string;
8
+ delete: string;
9
+ close: string;
10
+ cancel: string;
11
+ insert: string;
12
+ noResult: string;
13
+ default: string;
14
+ auto: string;
15
+ title: string;
16
+ paragraph: string;
17
+ link: string;
18
+ alignLeft: string;
19
+ alignCenter: string;
20
+ alignRight: string;
21
+ alignJustify: string;
22
+ unorderedList: string;
23
+ numberList: string;
24
+ codeBlock: string;
25
+ mathBlock: string;
26
+ table: string;
27
+ twoColumns: string;
28
+ threeColumns: string;
29
+ image: string;
30
+ iframe: string;
31
+ youtube: string;
32
+ blockquote: string;
33
+ title1Info: string;
34
+ title2Info: string;
35
+ title3Info: string;
36
+ unorderedListInfo: string;
37
+ numberListInfo: string;
38
+ codeBlockInfo: string;
39
+ mathBlockInfo: string;
40
+ tableInfo: string;
41
+ twoColumnsInfo: string;
42
+ threeColumnsInfo: string;
43
+ imageInfo: string;
44
+ iframeInfo: string;
45
+ youtubeInfo: string;
46
+ blockquoteInfo: string;
47
+ newLineInfo: string;
48
+ placeholder: string;
49
+ insertCode: string;
50
+ };
51
+ export default _default;
@@ -0,0 +1,50 @@
1
+ export default {
2
+ target: ['ja', 'ja-JP'],
3
+ lang: 'ja',
4
+ country: 'JP',
5
+ text: 'テキスト',
6
+ block: 'ブロック',
7
+ loading: '読み込み中...',
8
+ delete: '削除',
9
+ close: '閉じる',
10
+ cancel: 'キャンセル',
11
+ insert: '挿入',
12
+ noResult: '結果なし',
13
+ default: 'デフォルト',
14
+ auto: '自動',
15
+ title: '見出し',
16
+ paragraph: '本文',
17
+ link: 'リンク',
18
+ alignLeft: '左揃え',
19
+ alignCenter: '中央揃え',
20
+ alignRight: '右揃え',
21
+ alignJustify: '両端揃え',
22
+ unorderedList: '箇条書き',
23
+ numberList: '番号付きリスト',
24
+ codeBlock: 'コードブロック',
25
+ mathBlock: '数式ブロック',
26
+ table: '表',
27
+ twoColumns: '2カラム',
28
+ threeColumns: '3カラム',
29
+ image: '画像',
30
+ iframe: 'iframe',
31
+ youtube: 'YouTube',
32
+ blockquote: '引用',
33
+ title1Info: '大きな見出し',
34
+ title2Info: 'やや小さい見出し',
35
+ title3Info: '中くらいの見出し',
36
+ unorderedListInfo: '順序なしリスト',
37
+ numberListInfo: '1, 2, 3, 4',
38
+ codeBlockInfo: 'シンタックスハイライト付きコードブロック',
39
+ mathBlockInfo: '数式ブロック',
40
+ tableInfo: '表',
41
+ twoColumnsInfo: '2カラムレイアウト',
42
+ threeColumnsInfo: '3カラムレイアウト',
43
+ imageInfo: '画像',
44
+ iframeInfo: '別のウェブサイトを埋め込む',
45
+ youtubeInfo: 'YouTube動画を埋め込む',
46
+ blockquoteInfo: '引用ブロック',
47
+ newLineInfo: '/ でコマンドを開く。または',
48
+ placeholder: 'ここに内容を入力...',
49
+ insertCode: 'コードを挿入'
50
+ };
@@ -0,0 +1,51 @@
1
+ declare const _default: {
2
+ target: string[];
3
+ lang: string;
4
+ country: string;
5
+ text: string;
6
+ block: string;
7
+ loading: string;
8
+ delete: string;
9
+ close: string;
10
+ cancel: string;
11
+ insert: string;
12
+ noResult: string;
13
+ default: string;
14
+ auto: string;
15
+ title: string;
16
+ paragraph: string;
17
+ link: string;
18
+ alignLeft: string;
19
+ alignCenter: string;
20
+ alignRight: string;
21
+ alignJustify: string;
22
+ unorderedList: string;
23
+ numberList: string;
24
+ codeBlock: string;
25
+ mathBlock: string;
26
+ table: string;
27
+ twoColumns: string;
28
+ threeColumns: string;
29
+ image: string;
30
+ iframe: string;
31
+ youtube: string;
32
+ blockquote: string;
33
+ title1Info: string;
34
+ title2Info: string;
35
+ title3Info: string;
36
+ unorderedListInfo: string;
37
+ numberListInfo: string;
38
+ codeBlockInfo: string;
39
+ mathBlockInfo: string;
40
+ tableInfo: string;
41
+ twoColumnsInfo: string;
42
+ threeColumnsInfo: string;
43
+ imageInfo: string;
44
+ iframeInfo: string;
45
+ youtubeInfo: string;
46
+ blockquoteInfo: string;
47
+ newLineInfo: string;
48
+ placeholder: string;
49
+ insertCode: string;
50
+ };
51
+ export default _default;
@@ -0,0 +1,50 @@
1
+ export default {
2
+ target: ['zh', 'zh-CN', 'zh-SG', 'zh-Hans'],
3
+ lang: 'zh',
4
+ country: 'CN',
5
+ text: '文本',
6
+ block: '区块',
7
+ loading: '加载中...',
8
+ delete: '删除',
9
+ close: '关闭',
10
+ cancel: '取消',
11
+ insert: '插入',
12
+ noResult: '无结果',
13
+ default: '默认',
14
+ auto: '自动',
15
+ title: '标题',
16
+ paragraph: '正文',
17
+ link: '链接',
18
+ alignLeft: '左对齐',
19
+ alignCenter: '居中对齐',
20
+ alignRight: '右对齐',
21
+ alignJustify: '两端对齐',
22
+ unorderedList: '无序列表',
23
+ numberList: '有序列表',
24
+ codeBlock: '代码块',
25
+ mathBlock: '公式块',
26
+ table: '表格',
27
+ twoColumns: '2 列',
28
+ threeColumns: '3 列',
29
+ image: '图片',
30
+ iframe: 'iframe',
31
+ youtube: 'YouTube',
32
+ blockquote: '引用',
33
+ title1Info: '大标题',
34
+ title2Info: '较小标题',
35
+ title3Info: '中等标题',
36
+ unorderedListInfo: '无序列表',
37
+ numberListInfo: '1, 2, 3, 4',
38
+ codeBlockInfo: '带语法高亮的代码块',
39
+ mathBlockInfo: '公式块',
40
+ tableInfo: '插入表格',
41
+ twoColumnsInfo: '两列布局',
42
+ threeColumnsInfo: '三列布局',
43
+ imageInfo: '图片',
44
+ iframeInfo: '嵌入其他网站',
45
+ youtubeInfo: '嵌入 YouTube 视频',
46
+ blockquoteInfo: '引用块',
47
+ newLineInfo: '按 / 输入命令,或',
48
+ placeholder: '在此输入内容...',
49
+ insertCode: '插入代码'
50
+ };
@@ -0,0 +1,51 @@
1
+ declare const _default: {
2
+ target: string[];
3
+ lang: string;
4
+ country: string;
5
+ text: string;
6
+ block: string;
7
+ loading: string;
8
+ delete: string;
9
+ close: string;
10
+ cancel: string;
11
+ insert: string;
12
+ noResult: string;
13
+ default: string;
14
+ auto: string;
15
+ title: string;
16
+ paragraph: string;
17
+ link: string;
18
+ alignLeft: string;
19
+ alignCenter: string;
20
+ alignRight: string;
21
+ alignJustify: string;
22
+ unorderedList: string;
23
+ numberList: string;
24
+ codeBlock: string;
25
+ mathBlock: string;
26
+ table: string;
27
+ twoColumns: string;
28
+ threeColumns: string;
29
+ image: string;
30
+ iframe: string;
31
+ youtube: string;
32
+ blockquote: string;
33
+ title1Info: string;
34
+ title2Info: string;
35
+ title3Info: string;
36
+ unorderedListInfo: string;
37
+ numberListInfo: string;
38
+ codeBlockInfo: string;
39
+ mathBlockInfo: string;
40
+ tableInfo: string;
41
+ twoColumnsInfo: string;
42
+ threeColumnsInfo: string;
43
+ imageInfo: string;
44
+ iframeInfo: string;
45
+ youtubeInfo: string;
46
+ blockquoteInfo: string;
47
+ newLineInfo: string;
48
+ placeholder: string;
49
+ insertCode: string;
50
+ };
51
+ export default _default;
@@ -0,0 +1,50 @@
1
+ export default {
2
+ target: ['zh-TW', 'zh-HK', 'zh-MO', 'zh-Hant'],
3
+ lang: 'zh',
4
+ country: 'TW',
5
+ text: '文字',
6
+ block: '區塊',
7
+ loading: '載入中...',
8
+ delete: '刪除',
9
+ close: '關閉',
10
+ cancel: '取消',
11
+ insert: '插入',
12
+ noResult: '無結果',
13
+ default: '預設',
14
+ auto: '自動',
15
+ title: '標題',
16
+ paragraph: '段落',
17
+ link: '連結',
18
+ alignLeft: '靠左對齊',
19
+ alignCenter: '置中對齊',
20
+ alignRight: '靠右對齊',
21
+ alignJustify: '左右對齊',
22
+ unorderedList: '項目清單',
23
+ numberList: '編號清單',
24
+ codeBlock: '程式碼區塊',
25
+ mathBlock: '數學區塊',
26
+ table: '表格',
27
+ twoColumns: '2 欄',
28
+ threeColumns: '3 欄',
29
+ image: '圖片',
30
+ iframe: 'iframe',
31
+ youtube: 'YouTube',
32
+ blockquote: '引用',
33
+ title1Info: '大標題',
34
+ title2Info: '較小標題',
35
+ title3Info: '中等標題',
36
+ unorderedListInfo: '無序清單',
37
+ numberListInfo: '1, 2, 3, 4',
38
+ codeBlockInfo: '含語法高亮的程式碼區塊',
39
+ mathBlockInfo: '數學區塊',
40
+ tableInfo: '插入表格',
41
+ twoColumnsInfo: '雙欄版面',
42
+ threeColumnsInfo: '三欄版面',
43
+ imageInfo: '圖片',
44
+ iframeInfo: '嵌入其他網站',
45
+ youtubeInfo: '嵌入 YouTube 影片',
46
+ blockquoteInfo: '引用區塊',
47
+ newLineInfo: '按 / 輸入指令,或',
48
+ placeholder: '在此輸入內容...',
49
+ insertCode: '插入程式碼'
50
+ };
@@ -19,6 +19,15 @@ const minHeight = {
19
19
  attr: 160
20
20
  };
21
21
  const maxHeight = 1600;
22
+ const aspectRatioOptions = [
23
+ { label: 'Auto', value: null },
24
+ { label: '16:9', value: '16:9' },
25
+ { label: '3:2', value: '3:2' },
26
+ { label: '21:9', value: '21:9' },
27
+ { label: '1:1', value: '1:1' },
28
+ { label: '4:3', value: '4:3' },
29
+ { label: '9:16', value: '9:16' }
30
+ ];
22
31
  function isResizableType(value) {
23
32
  return typeSet.has(value);
24
33
  }
@@ -37,6 +46,69 @@ function parseNumericSize(value) {
37
46
  const parsed = Number.parseFloat(normalized);
38
47
  return Number.isFinite(parsed) ? parsed : null;
39
48
  }
49
+ function parseAspectRatio(value) {
50
+ if (typeof value === 'number')
51
+ return Number.isFinite(value) && value > 0 ? value : null;
52
+ if (typeof value !== 'string')
53
+ return null;
54
+ const trimmed = value.trim();
55
+ if (!trimmed)
56
+ return null;
57
+ const ratioMatch = /^([0-9]+(?:\.[0-9]+)?)\s*:\s*([0-9]+(?:\.[0-9]+)?)$/.exec(trimmed);
58
+ if (ratioMatch) {
59
+ const widthPart = Number.parseFloat(ratioMatch[1]);
60
+ const heightPart = Number.parseFloat(ratioMatch[2]);
61
+ if (!Number.isFinite(widthPart) || !Number.isFinite(heightPart))
62
+ return null;
63
+ if (widthPart <= 0 || heightPart <= 0)
64
+ return null;
65
+ return widthPart / heightPart;
66
+ }
67
+ const parsed = Number.parseFloat(trimmed);
68
+ if (!Number.isFinite(parsed) || parsed <= 0)
69
+ return null;
70
+ return parsed;
71
+ }
72
+ function formatDecimal(value, precision = 6) {
73
+ return String(Number(value.toFixed(precision)));
74
+ }
75
+ function normalizeAspectRatioAttr(value) {
76
+ if (typeof value === 'number') {
77
+ if (!Number.isFinite(value) || value <= 0)
78
+ return null;
79
+ return formatDecimal(value);
80
+ }
81
+ if (typeof value !== 'string')
82
+ return null;
83
+ const trimmed = value.trim();
84
+ if (!trimmed)
85
+ return null;
86
+ const ratioMatch = /^([0-9]+(?:\.[0-9]+)?)\s*:\s*([0-9]+(?:\.[0-9]+)?)$/.exec(trimmed);
87
+ if (ratioMatch) {
88
+ const widthPart = Number.parseFloat(ratioMatch[1]);
89
+ const heightPart = Number.parseFloat(ratioMatch[2]);
90
+ if (!Number.isFinite(widthPart) || !Number.isFinite(heightPart))
91
+ return null;
92
+ if (widthPart <= 0 || heightPart <= 0)
93
+ return null;
94
+ return `${formatDecimal(widthPart, 4)}:${formatDecimal(heightPart, 4)}`;
95
+ }
96
+ const parsed = Number.parseFloat(trimmed);
97
+ if (!Number.isFinite(parsed) || parsed <= 0)
98
+ return null;
99
+ return formatDecimal(parsed);
100
+ }
101
+ function sameAspectRatio(left, right) {
102
+ if (!left && !right)
103
+ return true;
104
+ if (!left || !right)
105
+ return false;
106
+ const leftRatio = parseAspectRatio(left);
107
+ const rightRatio = parseAspectRatio(right);
108
+ if (leftRatio === null || rightRatio === null)
109
+ return false;
110
+ return Math.abs(leftRatio - rightRatio) <= 0.001;
111
+ }
40
112
  function normalizeStringAttr(value) {
41
113
  if (typeof value !== 'string')
42
114
  return null;
@@ -113,6 +185,15 @@ function resolveStartHeight(kind, node, element) {
113
185
  return fromAttr;
114
186
  return defaultHeight[kind];
115
187
  }
188
+ function resolveElementWidth(node, element) {
189
+ const rect = element.getBoundingClientRect();
190
+ if (rect.width > 0)
191
+ return rect.width;
192
+ const fromAttr = parseNumericSize(node.attrs.width);
193
+ if (fromAttr !== null)
194
+ return fromAttr;
195
+ return 0;
196
+ }
116
197
  function resolveTargetElement(view, pos, resizeMeta, node) {
117
198
  const nodeDom = view.nodeDOM(pos);
118
199
  if (!(nodeDom instanceof HTMLElement))
@@ -153,7 +234,7 @@ function resolveTargetElement(view, pos, resizeMeta, node) {
153
234
  nodeDom.querySelector('iframe') ||
154
235
  nodeDom);
155
236
  }
156
- function buildResizeAttrs(kind, node, height, imageRatio) {
237
+ function buildResizeAttrs(kind, node, height, imageRatio, aspectRatio = normalizeAspectRatioAttr(node.attrs.aspectRatio)) {
157
238
  const attrs = { ...node.attrs };
158
239
  const roundedHeight = String(Math.round(height));
159
240
  if (kind === 'image') {
@@ -162,11 +243,11 @@ function buildResizeAttrs(kind, node, height, imageRatio) {
162
243
  return { ...attrs, width: roundedWidth, height: null };
163
244
  }
164
245
  if (kind === 'iframe' || kind === 'embed') {
165
- return { ...attrs, width: attrs.width || '100%', height: roundedHeight };
246
+ return { ...attrs, width: attrs.width || '100%', height: roundedHeight, aspectRatio };
166
247
  }
167
- return { ...attrs, height: roundedHeight };
248
+ return { ...attrs, height: roundedHeight, aspectRatio };
168
249
  }
169
- function createResizeHandleDecoration(nodePos, widgetPos, resizeMeta) {
250
+ function createResizeHandleDecoration(nodePos, widgetPos, resizeMeta, node) {
170
251
  return Decoration.widget(widgetPos, () => {
171
252
  const anchor = document.createElement('div');
172
253
  anchor.className = 'tiptap-media-resize-anchor';
@@ -175,8 +256,30 @@ function createResizeHandleDecoration(nodePos, widgetPos, resizeMeta) {
175
256
  button.className = 'tiptap-media-resize-handle';
176
257
  button.dataset.resizePos = String(nodePos);
177
258
  button.dataset.resizeKind = resizeMeta.kind;
178
- button.setAttribute('aria-label', 'Resize media height');
259
+ button.setAttribute('aria-label', 'Resize media height (click for aspect ratio)');
179
260
  anchor.append(button);
261
+ if (resizeMeta.kind !== 'image') {
262
+ const selectedAspectRatio = normalizeAspectRatioAttr(node.attrs.aspectRatio);
263
+ const toolbar = document.createElement('div');
264
+ toolbar.className = 'tiptap-media-aspect-ratio-toolbar';
265
+ toolbar.setAttribute('role', 'toolbar');
266
+ toolbar.setAttribute('aria-label', 'Aspect ratio presets');
267
+ toolbar.dataset.resizePos = String(nodePos);
268
+ for (const option of aspectRatioOptions) {
269
+ const optionButton = document.createElement('button');
270
+ optionButton.type = 'button';
271
+ optionButton.className = 'tiptap-media-aspect-ratio-option';
272
+ optionButton.dataset.resizePos = String(nodePos);
273
+ optionButton.dataset.aspectRatio = option.value ?? 'auto';
274
+ optionButton.textContent = option.label;
275
+ const isActive = option.value
276
+ ? sameAspectRatio(option.value, selectedAspectRatio)
277
+ : !selectedAspectRatio;
278
+ optionButton.setAttribute('aria-pressed', isActive ? 'true' : 'false');
279
+ toolbar.append(optionButton);
280
+ }
281
+ anchor.append(toolbar);
282
+ }
180
283
  return anchor;
181
284
  }, { side: 1, key: `media-resize-${nodePos}-${resizeMeta.typeName}-${resizeMeta.kind}` });
182
285
  }
@@ -254,9 +357,7 @@ export default Extension.create({
254
357
  default: false,
255
358
  parseHTML: (element) => hasResizeHandler(element.getAttribute('data-resize-handler') ||
256
359
  element.getAttribute('resize-handler')),
257
- renderHTML: (attributes) => hasResizeHandler(attributes.resizeHandler)
258
- ? { 'data-resize-handler': 'true' }
259
- : {}
360
+ renderHTML: (attributes) => hasResizeHandler(attributes.resizeHandler) ? { 'data-resize-handler': 'true' } : {}
260
361
  },
261
362
  resizeTarget: {
262
363
  default: null,
@@ -281,6 +382,14 @@ export default Extension.create({
281
382
  const maxHeight = normalizeNumericAttr(attributes.maxHeight);
282
383
  return maxHeight ? { 'data-resize-max-height': maxHeight } : {};
283
384
  }
385
+ },
386
+ aspectRatio: {
387
+ default: null,
388
+ parseHTML: (element) => normalizeAspectRatioAttr(element.getAttribute('data-resize-aspect-ratio')),
389
+ renderHTML: (attributes) => {
390
+ const aspectRatio = normalizeAspectRatioAttr(attributes.aspectRatio);
391
+ return aspectRatio ? { 'data-resize-aspect-ratio': aspectRatio } : {};
392
+ }
284
393
  }
285
394
  }
286
395
  }
@@ -288,6 +397,15 @@ export default Extension.create({
288
397
  },
289
398
  addProseMirrorPlugins() {
290
399
  let removeDragListeners = null;
400
+ const closeOpenToolbars = (view, except = null) => {
401
+ view.dom
402
+ .querySelectorAll('.tiptap-media-resize-anchor.is-toolbar-open')
403
+ .forEach((anchor) => {
404
+ if (except && anchor === except)
405
+ return;
406
+ anchor.classList.remove('is-toolbar-open');
407
+ });
408
+ };
291
409
  return [
292
410
  new Plugin({
293
411
  key: pluginKey,
@@ -328,7 +446,7 @@ export default Extension.create({
328
446
  const resizeMeta = resolveResizeMeta(node);
329
447
  if (!resizeMeta || resizeMeta.kind === 'image' || node.isInline)
330
448
  return;
331
- decorations.push(createResizeHandleDecoration(pos, pos + node.nodeSize, resizeMeta));
449
+ decorations.push(createResizeHandleDecoration(pos, pos + node.nodeSize, resizeMeta, node));
332
450
  handled.add(pos);
333
451
  });
334
452
  }
@@ -336,7 +454,7 @@ export default Extension.create({
336
454
  const pos = state.selection.from;
337
455
  const resizeMeta = resolveResizeMeta(state.selection.node);
338
456
  if (resizeMeta && !handled.has(pos)) {
339
- decorations.push(createResizeHandleDecoration(pos, pos + state.selection.node.nodeSize, resizeMeta));
457
+ decorations.push(createResizeHandleDecoration(pos, pos + state.selection.node.nodeSize, resizeMeta, state.selection.node));
340
458
  }
341
459
  }
342
460
  if (!decorations.length)
@@ -349,9 +467,50 @@ export default Extension.create({
349
467
  return false;
350
468
  if (!(event.target instanceof HTMLElement))
351
469
  return false;
470
+ const ratioOption = event.target.closest('.tiptap-media-aspect-ratio-option');
471
+ if (ratioOption) {
472
+ event.preventDefault();
473
+ event.stopPropagation();
474
+ const pos = Number.parseInt(ratioOption.dataset.resizePos || '', 10);
475
+ if (!Number.isFinite(pos))
476
+ return true;
477
+ const node = view.state.doc.nodeAt(pos);
478
+ if (!node)
479
+ return true;
480
+ const resizeMeta = resolveResizeMeta(node);
481
+ if (!resizeMeta || resizeMeta.kind === 'image')
482
+ return true;
483
+ const target = resolveTargetElement(view, pos, resizeMeta, node);
484
+ if (!target)
485
+ return true;
486
+ const selectedRatio = ratioOption.dataset.aspectRatio || 'auto';
487
+ const normalizedAspectRatio = selectedRatio === 'auto' ? null : normalizeAspectRatioAttr(selectedRatio);
488
+ if (selectedRatio !== 'auto' && !normalizedAspectRatio)
489
+ return true;
490
+ let nextHeight = resolveStartHeight(resizeMeta.kind, node, target);
491
+ const ratioValue = parseAspectRatio(normalizedAspectRatio);
492
+ if (ratioValue !== null) {
493
+ const width = resolveElementWidth(node, target);
494
+ if (width > 0) {
495
+ nextHeight = clamp(width / ratioValue, resizeMeta.minHeight, resizeMeta.maxHeight);
496
+ }
497
+ }
498
+ const nextAttrs = buildResizeAttrs(resizeMeta.kind, node, nextHeight, 1, normalizedAspectRatio);
499
+ const nextWidth = 'width' in nextAttrs ? nextAttrs.width : node.attrs.width;
500
+ const nextAspectRatio = 'aspectRatio' in nextAttrs ? nextAttrs.aspectRatio : null;
501
+ if (nextAttrs.height !== node.attrs.height ||
502
+ nextWidth !== node.attrs.width ||
503
+ nextAspectRatio !== node.attrs.aspectRatio) {
504
+ view.dispatch(view.state.tr.setNodeMarkup(pos, node.type, nextAttrs));
505
+ }
506
+ closeOpenToolbars(view);
507
+ return true;
508
+ }
352
509
  const handle = event.target.closest('.tiptap-media-resize-handle');
353
- if (!handle)
510
+ if (!handle) {
511
+ closeOpenToolbars(view);
354
512
  return false;
513
+ }
355
514
  const pos = Number.parseInt(handle.dataset.resizePos || '', 10);
356
515
  if (!Number.isFinite(pos))
357
516
  return false;
@@ -369,6 +528,8 @@ export default Extension.create({
369
528
  return false;
370
529
  event.preventDefault();
371
530
  event.stopPropagation();
531
+ const anchor = handle.closest('.tiptap-media-resize-anchor');
532
+ const startX = event.clientX;
372
533
  const startY = event.clientY;
373
534
  const startHeight = resolveStartHeight(resizeMeta.kind, node, target);
374
535
  const imageRatio = resizeMeta.kind === 'image' ? resolveImageRatio(node, target) : 1;
@@ -377,21 +538,10 @@ export default Extension.create({
377
538
  let restoreTarget = null;
378
539
  let frame = 0;
379
540
  let pendingHeight = startHeight;
380
- if (shouldShowProxy && target.parentElement) {
381
- const targetElement = target;
382
- const originalDisplay = targetElement.style.display;
383
- resizeProxy = document.createElement('div');
384
- resizeProxy.className = 'tiptap-media-resize-proxy';
385
- resizeProxy.style.height = `${Math.round(startHeight)}px`;
386
- target.parentElement.insertBefore(resizeProxy, targetElement);
387
- targetElement.style.display = 'none';
388
- restoreTarget = () => {
389
- targetElement.style.display = originalDisplay;
390
- resizeProxy?.remove();
391
- resizeProxy = null;
392
- restoreTarget = null;
393
- };
394
- }
541
+ let isDragging = false;
542
+ let appliedDragCursor = false;
543
+ const previousCursor = document.body.style.cursor;
544
+ const previousSelect = document.body.style.userSelect;
395
545
  const dispatchHeight = (height) => {
396
546
  const current = view.state.doc.nodeAt(pos);
397
547
  if (!current || current.type.name !== resizeMeta.typeName)
@@ -399,19 +549,51 @@ export default Extension.create({
399
549
  const currentMeta = resolveResizeMeta(current);
400
550
  if (!currentMeta)
401
551
  return;
402
- const nextAttrs = buildResizeAttrs(currentMeta.kind, current, height, imageRatio);
403
- const nextWidth = 'width' in nextAttrs ? nextAttrs.width : current.attrs.width;
404
- if (nextAttrs.height === current.attrs.height && nextWidth === current.attrs.width)
552
+ // Manual drag should unlock fixed aspect ratio.
553
+ const aspectRatioForDrag = null;
554
+ const nextAttrs = buildResizeAttrs(currentMeta.kind, current, height, imageRatio, aspectRatioForDrag);
555
+ const nextWidth = 'width' in nextAttrs
556
+ ? nextAttrs.width
557
+ : current.attrs.width;
558
+ const nextAspectRatio = 'aspectRatio' in nextAttrs ? nextAttrs.aspectRatio : null;
559
+ if (nextAttrs.height === current.attrs.height &&
560
+ nextWidth === current.attrs.width &&
561
+ nextAspectRatio === current.attrs.aspectRatio)
405
562
  return;
406
- const tr = view.state.tr.setNodeMarkup(pos, current.type, nextAttrs);
407
- view.dispatch(tr);
563
+ view.dispatch(view.state.tr.setNodeMarkup(pos, current.type, nextAttrs));
564
+ };
565
+ const beginDrag = () => {
566
+ if (isDragging)
567
+ return;
568
+ isDragging = true;
569
+ closeOpenToolbars(view);
570
+ if (shouldShowProxy && target.parentElement) {
571
+ const targetElement = target;
572
+ const originalDisplay = targetElement.style.display;
573
+ resizeProxy = document.createElement('div');
574
+ resizeProxy.className = 'tiptap-media-resize-proxy';
575
+ resizeProxy.style.height = `${Math.round(startHeight)}px`;
576
+ target.parentElement.insertBefore(resizeProxy, targetElement);
577
+ targetElement.style.display = 'none';
578
+ restoreTarget = () => {
579
+ targetElement.style.display = originalDisplay;
580
+ resizeProxy?.remove();
581
+ resizeProxy = null;
582
+ restoreTarget = null;
583
+ };
584
+ }
585
+ document.body.style.cursor = 'ns-resize';
586
+ document.body.style.userSelect = 'none';
587
+ appliedDragCursor = true;
408
588
  };
409
- const previousCursor = document.body.style.cursor;
410
- const previousSelect = document.body.style.userSelect;
411
- document.body.style.cursor = 'ns-resize';
412
- document.body.style.userSelect = 'none';
413
589
  const onMove = (moveEvent) => {
414
- const nextHeight = clamp(startHeight + moveEvent.clientY - startY, resizeMeta.minHeight, resizeMeta.maxHeight);
590
+ const deltaX = moveEvent.clientX - startX;
591
+ const deltaY = moveEvent.clientY - startY;
592
+ if (!isDragging && Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4)
593
+ return;
594
+ if (!isDragging)
595
+ beginDrag();
596
+ const nextHeight = clamp(startHeight + deltaY, resizeMeta.minHeight, resizeMeta.maxHeight);
415
597
  pendingHeight = nextHeight;
416
598
  if (resizeProxy)
417
599
  resizeProxy.style.height = `${Math.round(nextHeight)}px`;
@@ -426,12 +608,21 @@ export default Extension.create({
426
608
  cancelAnimationFrame(frame);
427
609
  window.removeEventListener('mousemove', onMove);
428
610
  window.removeEventListener('mouseup', onUp);
429
- document.body.style.cursor = previousCursor;
430
- document.body.style.userSelect = previousSelect;
611
+ if (appliedDragCursor) {
612
+ document.body.style.cursor = previousCursor;
613
+ document.body.style.userSelect = previousSelect;
614
+ }
431
615
  restoreTarget?.();
432
616
  removeDragListeners = null;
433
617
  };
434
618
  const onUp = () => {
619
+ if (!isDragging) {
620
+ const shouldOpen = Boolean(anchor) && !(anchor?.classList.contains('is-toolbar-open') ?? false);
621
+ closeOpenToolbars(view, shouldOpen && anchor ? anchor : null);
622
+ anchor?.classList.toggle('is-toolbar-open', shouldOpen);
623
+ cleanup();
624
+ return;
625
+ }
435
626
  if (shouldShowProxy)
436
627
  dispatchHeight(pendingHeight);
437
628
  cleanup();
@@ -81,6 +81,7 @@
81
81
  'data-resize-target',
82
82
  'data-resize-min-height',
83
83
  'data-resize-max-height',
84
+ 'data-resize-aspect-ratio',
84
85
  'data-bubble-menu',
85
86
  'data-hide-bubble-menu'
86
87
  ];
@@ -343,10 +344,13 @@
343
344
  .editable :global(.tiptap-media-resize-anchor) {
344
345
  width: 100%;
345
346
  display: flex;
346
- justify-content: center;
347
+ flex-direction: column;
348
+ align-items: center;
347
349
  margin: 6px 0 2px;
348
350
  line-height: 0;
349
351
  pointer-events: none;
352
+ position: relative;
353
+ overflow: visible;
350
354
  }
351
355
 
352
356
  .editable :global(.tiptap-media-resize-handle) {
@@ -379,6 +383,56 @@
379
383
  transform: translateY(1px);
380
384
  }
381
385
 
386
+ .editable :global(.tiptap-media-aspect-ratio-toolbar) {
387
+ display: none;
388
+ align-items: center;
389
+ gap: 4px;
390
+ padding: 4px;
391
+ border: 1px solid var(--primary-light3, rgba(120, 120, 120, 0.4));
392
+ border-radius: 999px;
393
+ background: var(--surface, #fff);
394
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
395
+ pointer-events: auto;
396
+ line-height: 1;
397
+ position: absolute;
398
+ top: calc(100% + 6px);
399
+ left: 50%;
400
+ transform: translateX(-50%);
401
+ z-index: 4;
402
+ white-space: nowrap;
403
+ }
404
+
405
+ .editable
406
+ :global(.tiptap-media-resize-anchor.is-toolbar-open .tiptap-media-aspect-ratio-toolbar) {
407
+ display: flex;
408
+ }
409
+
410
+ .editable :global(.tiptap-media-aspect-ratio-option) {
411
+ appearance: none;
412
+ -webkit-appearance: none;
413
+ margin: 0;
414
+ padding: 2px 8px;
415
+ border: 0;
416
+ border-radius: 999px;
417
+ background: transparent;
418
+ color: var(--on-surface, #000);
419
+ font-size: 11px;
420
+ font-weight: 600;
421
+ cursor: pointer;
422
+ transition: background-color 0.15s ease;
423
+ }
424
+
425
+ .editable :global(.tiptap-media-aspect-ratio-option:hover),
426
+ .editable :global(.tiptap-media-aspect-ratio-option:focus-visible) {
427
+ background: var(--primary-light1, rgba(120, 120, 120, 0.14));
428
+ outline: none;
429
+ }
430
+
431
+ .editable :global(.tiptap-media-aspect-ratio-option[aria-pressed='true']) {
432
+ background: var(--primary-light4, rgba(120, 120, 120, 0.3));
433
+ color: var(--on-primary, #000);
434
+ }
435
+
382
436
  .editable :global(.tiptap-media-resize-proxy) {
383
437
  width: 100%;
384
438
  border-radius: 12px;
@@ -471,21 +525,58 @@
471
525
  border-radius: 12px;
472
526
  }
473
527
 
474
- & :global(.iframe-wrapper) {
475
- position: relative;
476
- padding-bottom: 12px;
528
+ & :global(.iframe-wrapper),
529
+ & :global(.embed-wrapper) {
477
530
  overflow: hidden;
478
531
  width: 100%;
479
- height: 600px;
480
532
  border-radius: 12px;
481
533
  }
482
534
 
483
- & :global(iframe) {
484
- position: absolute;
485
- top: 0;
486
- left: 0;
535
+ & :global(iframe),
536
+ & :global(embed) {
537
+ display: block;
487
538
  width: 100%;
488
- height: 100%;
539
+ max-width: 100%;
540
+ }
541
+
542
+ & :global([data-resize-aspect-ratio='16:9']) {
543
+ --tiptap-media-aspect-ratio: 16 / 9;
544
+ }
545
+
546
+ & :global([data-resize-aspect-ratio='3:2']) {
547
+ --tiptap-media-aspect-ratio: 3 / 2;
548
+ }
549
+
550
+ & :global([data-resize-aspect-ratio='21:9']) {
551
+ --tiptap-media-aspect-ratio: 21 / 9;
552
+ }
553
+
554
+ & :global([data-resize-aspect-ratio='1:1']) {
555
+ --tiptap-media-aspect-ratio: 1 / 1;
556
+ }
557
+
558
+ & :global([data-resize-aspect-ratio='4:3']) {
559
+ --tiptap-media-aspect-ratio: 4 / 3;
560
+ }
561
+
562
+ & :global([data-resize-aspect-ratio='9:16']) {
563
+ --tiptap-media-aspect-ratio: 9 / 16;
564
+ }
565
+
566
+ & :global(iframe[data-resize-aspect-ratio='16:9']),
567
+ & :global(iframe[data-resize-aspect-ratio='3:2']),
568
+ & :global(iframe[data-resize-aspect-ratio='21:9']),
569
+ & :global(iframe[data-resize-aspect-ratio='1:1']),
570
+ & :global(iframe[data-resize-aspect-ratio='4:3']),
571
+ & :global(iframe[data-resize-aspect-ratio='9:16']),
572
+ & :global(embed[data-resize-aspect-ratio='16:9']),
573
+ & :global(embed[data-resize-aspect-ratio='3:2']),
574
+ & :global(embed[data-resize-aspect-ratio='21:9']),
575
+ & :global(embed[data-resize-aspect-ratio='1:1']),
576
+ & :global(embed[data-resize-aspect-ratio='4:3']),
577
+ & :global(embed[data-resize-aspect-ratio='9:16']) {
578
+ aspect-ratio: var(--tiptap-media-aspect-ratio, auto);
579
+ height: auto;
489
580
  }
490
581
  }
491
582
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seorii/tiptap",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "svelte-kit sync && svelte-package",