sprintify-ui 0.11.34 → 0.12.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.
@@ -3,33 +3,144 @@
3
3
  class="border rounded-md overflow-hidden bg-white || tiptap"
4
4
  :class="[
5
5
  hasError ? 'border-red-500' : 'border-slate-300',
6
+ noMargin ? 'tip-tap-no-margin' : ''
6
7
  ]"
7
8
  >
8
- <div class="divide-x px-1 py-1 border-b flex">
9
- <button
9
+ <div class="px-1 py-1 border-b flex items-center">
10
+ <template
10
11
  v-for="action in filteredActions"
11
12
  :key="action.name"
12
- :class="[buttonClass, editor.isActive(action.name) ? 'bg-slate-200' : '']"
13
- type="button"
14
- :disabled="disabled"
15
- @click="action.command()"
16
13
  >
17
- <BaseIcon
18
- class="h-5 w-5"
19
- :icon="action.icon"
20
- />
21
- </button>
14
+ <BaseDropdown
15
+ v-if="action.name === 'highlight'"
16
+ placement="bottom"
17
+ tw-button="block"
18
+ >
19
+ <template #button>
20
+ <div :class="[buttonClass, editor.isActive('highlight') ? 'bg-slate-200' : '']">
21
+ <BaseIcon
22
+ class="h-4 w-4 text-slate-600"
23
+ :icon="action.icon"
24
+ />
25
+ </div>
26
+ </template>
27
+ <template #dropdown="{ close }">
28
+ <div class="bg-white rounded-full shadow-lg border border-slate-200 px-4 py-3 flex items-center gap-2">
29
+ <button
30
+ v-for="color in highlightColors"
31
+ :key="color"
32
+ type="button"
33
+ class="w-5 h-5 rounded-full ring-inset ring-1 ring-black/20"
34
+ :style="{ backgroundColor: color }"
35
+ @click="setHighlight(color); close()"
36
+ />
37
+ <div class="w-px h-5 bg-slate-200" />
38
+ <button
39
+ type="button"
40
+ class="w-5 h-5 flex items-center justify-center bg-white "
41
+ @click="unsetHighlight(); close()"
42
+ >
43
+ <BaseIcon
44
+ class="h-5 w-5 text-slate-600"
45
+ icon="lucide:ban"
46
+ />
47
+ </button>
48
+ </div>
49
+ </template>
50
+ </BaseDropdown>
51
+ <BaseDropdown
52
+ v-else-if="action.name === 'link'"
53
+ placement="bottom"
54
+ tw-button="block"
55
+ >
56
+ <template #button>
57
+ <div
58
+ :class="[buttonClass, editor.isActive('link') ? 'bg-slate-200' : '']"
59
+ @click="openLinkDropdown"
60
+ >
61
+ <BaseIcon
62
+ class="h-4 w-4 text-slate-600"
63
+ :icon="action.icon"
64
+ />
65
+ </div>
66
+ </template>
67
+ <template #dropdown="{ close }">
68
+ <div class="bg-white rounded-full shadow-lg border border-slate-200 pl-4 pr-3 py-2 flex items-center gap-2 focus:outline-none outline-none">
69
+ <input
70
+ :ref="focusLinkInput"
71
+ v-model="linkUrl"
72
+ type="text"
73
+ class="bg-transparent border-none border-0 border-transparent shadow-none focus:border-none focus:outline-none focus:ring-0 focus:shadow-none outline-none ring-0 text-sm text-slate-700 placeholder-slate-400 w-56 py-1 px-2"
74
+ :placeholder="t('sui.paste_link')"
75
+ @keydown.enter.prevent="applyLink(); close()"
76
+ >
77
+ <button
78
+ type="button"
79
+ class="w-7 h-7 flex items-center justify-center rounded-full hover:bg-slate-100"
80
+ @click="applyLink(); close()"
81
+ >
82
+ <BaseIcon
83
+ class="h-4 w-4 text-slate-600"
84
+ icon="lucide:corner-down-left"
85
+ />
86
+ </button>
87
+ <div class="w-px h-5 bg-slate-200" />
88
+ <button
89
+ type="button"
90
+ class="w-7 h-7 flex items-center justify-center rounded-full hover:bg-slate-100 disabled:opacity-40 disabled:hover:bg-transparent"
91
+ :disabled="!linkUrl"
92
+ @click="openLink(linkUrl)"
93
+ >
94
+ <BaseIcon
95
+ class="h-4 w-4 text-slate-600"
96
+ icon="lucide:external-link"
97
+ />
98
+ </button>
99
+ <button
100
+ type="button"
101
+ class="w-7 h-7 flex items-center justify-center rounded-full hover:bg-slate-100"
102
+ @click="removeLink(); close()"
103
+ >
104
+ <BaseIcon
105
+ class="h-4 w-4 text-slate-600"
106
+ icon="lucide:trash-2"
107
+ />
108
+ </button>
109
+ </div>
110
+ </template>
111
+ </BaseDropdown>
112
+ <button
113
+ v-else
114
+ :class="[buttonClass, editor.isActive(action.name) ? 'bg-slate-200' : '']"
115
+ type="button"
116
+ :disabled="disabled"
117
+ @click="action.command()"
118
+ >
119
+ <BaseIcon
120
+ class="h-4 w-4 text-slate-600"
121
+ :icon="action.icon"
122
+ />
123
+ </button>
124
+ </template>
22
125
  </div>
23
126
 
24
127
  <div
25
128
  class="bg-white p-5"
26
- :class="[disabled ? 'cursor-not-allowed' : '']"
129
+ :class="[disabled ? 'cursor-not-allowed opacity-80' : '']"
27
130
  >
28
131
  <editor-content
29
132
  v-if="editor"
30
133
  :editor="editor"
31
134
  />
32
135
  </div>
136
+
137
+ <input
138
+ ref="imageInput"
139
+ type="file"
140
+ accept="image/*"
141
+ class="hidden"
142
+ @change="onImageSelected"
143
+ >
33
144
  </div>
34
145
  </template>
35
146
 
@@ -39,9 +150,15 @@ import Document from '@tiptap/extension-document';
39
150
  import Link from '@tiptap/extension-link';
40
151
  import Superscript from '@tiptap/extension-superscript';
41
152
  import Subscript from '@tiptap/extension-subscript';
153
+ import Strike from '@tiptap/extension-strike'
154
+ import Image from '@tiptap/extension-image';
155
+ import Highlight from '@tiptap/extension-highlight';
42
156
  import { Placeholder } from '@tiptap/extensions'
43
157
  import { Footnotes, FootnoteReference, Footnote } from "tiptap-footnotes";
44
158
  import StarterKit from '@tiptap/starter-kit';
159
+ import resizeImageForUpload from '@/utils/resizeImageForUpload';
160
+ import BaseDropdown from './BaseDropdown.vue';
161
+ import { t } from '@/i18n';
45
162
 
46
163
  const emit = defineEmits(['update:modelValue']);
47
164
 
@@ -51,7 +168,8 @@ const props = defineProps<{
51
168
  hasError?: boolean;
52
169
  disabled?: boolean;
53
170
  placeholder?: string;
54
- toolbar?: string[];
171
+ toolbar?: string[] | 'simple' | 'complete';
172
+ noMargin?: boolean;
55
173
  }>();
56
174
 
57
175
  const editor = new Editor({
@@ -72,15 +190,23 @@ const editor = new Editor({
72
190
  Placeholder.configure({
73
191
  placeholder: props.placeholder || 'Start typing here...',
74
192
  }),
193
+ Strike,
75
194
  Superscript,
76
195
  Subscript,
196
+ Image.configure({
197
+ inline: false,
198
+ allowBase64: true,
199
+ }),
200
+ Highlight.configure({
201
+ multicolor: true,
202
+ }),
77
203
  Footnotes,
78
204
  Footnote,
79
205
  FootnoteReference,
80
206
  ],
81
207
  editorProps: {
82
208
  attributes: {
83
- class: 'prose prose-sm mx-auto max-w-full focus:outline-none',
209
+ class: 'prose prose-sm focus:outline-none',
84
210
  },
85
211
  },
86
212
  content: props.modelValue,
@@ -139,37 +265,117 @@ onBeforeUnmount(() => {
139
265
  }
140
266
  });
141
267
 
142
- function setLink() {
143
- const previousUrl = editor.getAttributes('link').href;
144
- const url = window.prompt('URL', previousUrl);
268
+ const imageInput = ref<HTMLInputElement | null>(null);
269
+
270
+ function pickImage() {
271
+ imageInput.value?.click();
272
+ }
273
+
274
+ const highlightColors = [
275
+ '#BBF7D0',
276
+ '#BFDBFE',
277
+ '#FECACA',
278
+ '#E9D5FF',
279
+ '#FEF08A',
280
+ ];
281
+
282
+ function setHighlight(color: string) {
283
+ editor.chain().focus().setHighlight({ color }).run();
284
+ }
285
+
286
+ function unsetHighlight() {
287
+ editor.chain().focus().unsetHighlight().run();
288
+ }
289
+
290
+ const MAX_IMAGE_BYTES = 1024 * 1024;
145
291
 
146
- // cancelled
147
- if (url === null) {
292
+ async function onImageSelected(event: Event) {
293
+ const target = event.target as HTMLInputElement;
294
+ const file = target.files?.[0];
295
+ target.value = '';
296
+
297
+ if (!file) {
148
298
  return;
149
299
  }
150
300
 
151
- // empty
301
+ let blob: Blob = file;
302
+
303
+ try {
304
+ blob = await resizeImageForUpload(file, { maxBytes: MAX_IMAGE_BYTES });
305
+ } catch (error) {
306
+ console.error(error);
307
+ }
308
+
309
+ const reader = new FileReader();
310
+
311
+ reader.onload = () => {
312
+ const src = reader.result;
313
+
314
+ if (typeof src === 'string') {
315
+ editor.chain().focus().setImage({ src }).run();
316
+ }
317
+ };
318
+
319
+ reader.readAsDataURL(blob);
320
+ }
321
+
322
+ const linkUrl = ref('');
323
+
324
+ function openLinkDropdown() {
325
+ linkUrl.value = editor.getAttributes('link').href ?? '';
326
+ }
327
+
328
+ let focusedLinkInput: HTMLInputElement | null = null;
329
+
330
+ function focusLinkInput(el: unknown) {
331
+ if (el instanceof HTMLInputElement) {
332
+ if (focusedLinkInput === el) {
333
+ return;
334
+ }
335
+
336
+ focusedLinkInput = el;
337
+ el.focus();
338
+ el.select();
339
+ } else {
340
+ focusedLinkInput = null;
341
+ }
342
+ }
343
+
344
+ function applyLink() {
345
+ const url = linkUrl.value.trim();
346
+
152
347
  if (url === '') {
153
348
  editor.chain().focus().extendMarkRange('link').unsetLink().run();
154
349
 
155
350
  return;
156
351
  }
157
352
 
158
- // update link
159
353
  editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
160
354
  }
161
355
 
356
+ function removeLink() {
357
+ editor.chain().focus().extendMarkRange('link').unsetLink().run();
358
+ }
359
+
360
+ function openLink(url: string) {
361
+ if (!url) {
362
+ return;
363
+ }
364
+
365
+ window.open(url, '_blank', 'noopener,noreferrer');
366
+ }
367
+
162
368
  const buttonClass = computed(() => {
163
369
  const sizeClass = {
164
370
  xs: 'p-1',
165
371
  sm: 'p-1.5',
166
- md: 'p-2',
167
- lg: 'p-2.5',
168
- xl: 'p-3',
372
+ md: 'px-2 py-1.5',
373
+ lg: 'px-2.5 py-2',
374
+ xl: 'px-3 py-2.5',
169
375
  }
170
376
 
171
377
  return [
172
- 'hover:bg-slate-100',
378
+ 'block hover:bg-slate-100 rounded-lg',
173
379
  'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white',
174
380
  sizeClass[props.size || 'md'] || sizeClass['md'],
175
381
  ];
@@ -185,94 +391,132 @@ const actions = computed<Action[]>(() => {
185
391
  return [
186
392
  {
187
393
  name: 'undo',
188
- icon: 'mdi:undo-variant',
394
+ icon: 'lucide:undo-2',
189
395
  command: () => editor.chain().focus().undo().run(),
190
396
  },
191
397
  {
192
398
  name: 'redo',
193
- icon: 'mdi:redo-variant',
399
+ icon: 'lucide:redo-2',
194
400
  command: () => editor.chain().focus().redo().run(),
195
401
  },
196
402
  {
197
403
  name: 'paragraph',
198
- icon: 'material-symbols:format-paragraph',
404
+ icon: 'lucide:pilcrow',
199
405
  command: () => editor.chain().focus().setParagraph().run(),
200
406
  },
201
407
  {
202
408
  name: 'heading-2',
203
- icon: 'material-symbols:format-h2',
409
+ icon: 'lucide:heading-2',
204
410
  command: () => editor.chain().focus().setHeading({ level: 2 }).run(),
205
411
  },
206
412
  {
207
413
  name: 'heading-3',
208
- icon: 'material-symbols:format-h3',
414
+ icon: 'lucide:heading-3',
209
415
  command: () => editor.chain().focus().setHeading({ level: 3 }).run(),
210
416
  },
211
417
  {
212
418
  name: 'bold',
213
- icon: 'material-symbols:format-bold',
419
+ icon: 'lucide:bold',
214
420
  command: () => editor.chain().focus().toggleBold().run(),
215
421
  },
216
422
  {
217
423
  name: 'italic',
218
- icon: 'material-symbols:format-italic',
424
+ icon: 'lucide:italic',
219
425
  command: () => editor.chain().focus().toggleItalic().run(),
220
426
  },
221
427
  {
222
428
  name: 'underline',
223
- icon: 'material-symbols:format-underlined',
429
+ icon: 'lucide:underline',
224
430
  command: () => editor.chain().focus().toggleUnderline().run(),
225
431
  },
432
+ {
433
+ name: 'strikethrough',
434
+ icon: 'lucide:strikethrough',
435
+ command: () => editor.chain().focus().toggleStrike().run(),
436
+ },
226
437
  {
227
438
  name: 'superscript',
228
- icon: 'material-symbols:superscript',
439
+ icon: 'lucide:superscript',
229
440
  command: () => editor.chain().focus().toggleSuperscript().run(),
230
441
  },
231
442
  {
232
443
  name: 'subscript',
233
- icon: 'material-symbols:subscript',
444
+ icon: 'lucide:subscript',
234
445
  command: () => editor.chain().focus().toggleSubscript().run(),
235
446
  },
447
+ {
448
+ name: 'highlight',
449
+ icon: 'lucide:highlighter',
450
+ command: () => { },
451
+ },
236
452
  {
237
453
  name: 'link',
238
- icon: 'material-symbols:link',
239
- command: () => setLink(),
454
+ icon: 'lucide:link',
455
+ command: () => { },
240
456
  },
241
457
  {
242
- name: 'unlink',
243
- icon: 'material-symbols:link-off',
244
- command: () => editor.chain().focus().unsetLink().run(),
458
+ name: 'image',
459
+ icon: 'lucide:image',
460
+ command: () => pickImage(),
245
461
  },
246
462
  {
247
463
  name: 'bulletList',
248
- icon: 'material-symbols:format-list-bulleted',
464
+ icon: 'lucide:list',
249
465
  command: () => editor.chain().focus().toggleBulletList().run(),
250
466
  },
251
467
  {
252
468
  name: 'orderedList',
253
- icon: 'material-symbols:format-list-numbered',
469
+ icon: 'lucide:list-ordered',
254
470
  command: () => editor.chain().focus().toggleOrderedList().run(),
255
471
  },
256
472
  {
257
473
  name: 'footnote',
258
- icon: 'material-symbols:edit-note-outline',
474
+ icon: 'lucide:asterisk',
259
475
  command: () => editor?.commands.addFootnote(),
260
476
  },
261
477
  {
262
478
  name: 'nonBreakingSpace',
263
- icon: 'material-symbols:space-bar',
479
+ icon: 'lucide:space',
264
480
  command: () => editor.chain().focus().insertContent('\u00A0').run(),
265
481
  },
266
482
 
267
483
  ];
268
484
  });
269
485
 
486
+ const toolbarTemplates: Record<string, string[]> = {
487
+ simple: ['bold', 'italic', 'underline', 'link'],
488
+ complete: [
489
+ 'undo',
490
+ 'redo',
491
+ 'paragraph',
492
+ 'heading-2',
493
+ 'heading-3',
494
+ 'bold',
495
+ 'italic',
496
+ 'underline',
497
+ 'superscript',
498
+ 'subscript',
499
+ 'highlight',
500
+ 'link',
501
+ 'unlink',
502
+ 'image',
503
+ 'bulletList',
504
+ 'orderedList',
505
+ 'footnote',
506
+ 'nonBreakingSpace',
507
+ ],
508
+ };
509
+
270
510
  const filteredActions = computed(() => {
271
511
  if (!props.toolbar || props.toolbar.length === 0) {
272
512
  return actions.value;
273
513
  }
274
514
 
275
- return actions.value.filter(action => props.toolbar?.includes(action.name));
515
+ const toolbar = typeof props.toolbar === 'string'
516
+ ? toolbarTemplates[props.toolbar] ?? []
517
+ : props.toolbar;
518
+
519
+ return actions.value.filter(action => toolbar.includes(action.name));
276
520
  });
277
521
 
278
522
  </script>
package/src/lang/en.json CHANGED
@@ -60,6 +60,7 @@
60
60
  "page": "Page",
61
61
  "pagination_detail_1": "Viewing",
62
62
  "pagination_detail_2": "of",
63
+ "paste_link": "Paste link...",
63
64
  "postal_code_zip_code": "Postal Code / Zip Code",
64
65
  "previous": "Previous",
65
66
  "previous_month": "Previous month",
package/src/lang/fr.json CHANGED
@@ -60,6 +60,7 @@
60
60
  "page": "Page",
61
61
  "pagination_detail_1": "Affichage de",
62
62
  "pagination_detail_2": "de",
63
+ "paste_link": "Coller le lien...",
63
64
  "postal_code_zip_code": "Code postal",
64
65
  "previous": "Précédent",
65
66
  "previous_month": "Mois précédent",
@@ -1,143 +0,0 @@
1
- import { defineComponent as c, computed as i, openBlock as f, createBlock as I, unref as n } from "vue";
2
- import { ClassicEditor as B, BalloonEditor as k, InlineEditor as y, Essentials as h, SourceEditing as z, Paragraph as C, Bold as S, Italic as v, Underline as E, Strikethrough as T, Subscript as V, Superscript as w, Code as F, CodeBlock as L, List as R, Indent as A, IndentBlock as x, Link as D, Autoformat as P, Heading as U, Image as G, AutoImage as O, ImageStyle as H, ImageResize as K, ImageToolbar as M, ImageInsert as N, Base64UploadAdapter as W, Table as j, TableToolbar as q, MediaEmbed as J, PasteFromOffice as Q, BlockToolbar as X, Font as Y, FindAndReplace as Z, RemoveFormat as $ } from "ckeditor5";
3
- import { Ckeditor as _ } from "@ckeditor/ckeditor5-vue";
4
- const te = /* @__PURE__ */ c({
5
- __name: "BaseCkeditor",
6
- props: {
7
- modelValue: {},
8
- editor: { default: "classic" },
9
- size: { default: "md" },
10
- toolbar: { default() {
11
- return [
12
- "undo",
13
- "redo",
14
- "|",
15
- "heading",
16
- "bold",
17
- "italic",
18
- "underline",
19
- "fontBackgroundColor",
20
- "|",
21
- "link",
22
- "insertImage",
23
- "|",
24
- "numberedList",
25
- "bulletedList",
26
- "|",
27
- "insertTable"
28
- ];
29
- } },
30
- placeholder: { default: "" },
31
- disabled: { type: Boolean, default: !1 }
32
- },
33
- emits: ["update:modelValue", "focus", "blur", "input", "ready"],
34
- setup(d, { expose: u, emit: s }) {
35
- const o = d, t = s;
36
- let a = null;
37
- function m(r) {
38
- a = r, t("ready", r);
39
- }
40
- const g = i(() => {
41
- switch (o.editor) {
42
- case "inline":
43
- return y;
44
- case "balloon":
45
- return k;
46
- default:
47
- return B;
48
- }
49
- }), p = i(() => o.modelValue === null ? "" : o.modelValue), b = i(() => ({
50
- licenseKey: "GPL",
51
- plugins: [
52
- h,
53
- z,
54
- C,
55
- S,
56
- v,
57
- E,
58
- T,
59
- V,
60
- w,
61
- F,
62
- L,
63
- R,
64
- A,
65
- x,
66
- D,
67
- P,
68
- U,
69
- G,
70
- O,
71
- H,
72
- K,
73
- M,
74
- N,
75
- W,
76
- j,
77
- q,
78
- J,
79
- Q,
80
- X,
81
- Y,
82
- Z,
83
- $
84
- ],
85
- toolbar: {
86
- items: o.toolbar,
87
- shouldNotGroupWhenFull: !0
88
- },
89
- table: {
90
- contentToolbar: ["tableColumn", "tableRow"]
91
- },
92
- image: {
93
- resizeOptions: [
94
- {
95
- name: "resizeImage:original",
96
- label: "Default image width",
97
- value: null
98
- },
99
- {
100
- name: "resizeImage:50",
101
- label: "50% page width",
102
- value: "50"
103
- },
104
- {
105
- name: "resizeImage:75",
106
- label: "75% page width",
107
- value: "75"
108
- },
109
- {
110
- name: "resizeImage:100",
111
- label: "100% page width",
112
- value: "100"
113
- }
114
- ],
115
- toolbar: [
116
- "imageStyle:alignBlockLeft",
117
- "imageStyle:block",
118
- "imageStyle:alignBlockRight",
119
- "resizeImage"
120
- ]
121
- },
122
- placeholder: o.placeholder
123
- }));
124
- return u({
125
- getEditorData() {
126
- return (a == null ? void 0 : a.getData()) || "";
127
- }
128
- }), (r, e) => (f(), I(n(_), {
129
- "model-value": n(p),
130
- editor: n(g),
131
- config: n(b),
132
- disabled: d.disabled,
133
- "onUpdate:modelValue": e[0] || (e[0] = (l) => t("update:modelValue", l)),
134
- onFocus: e[1] || (e[1] = (l) => t("focus", l)),
135
- onBlur: e[2] || (e[2] = (l) => t("blur", l)),
136
- onInput: e[3] || (e[3] = (l) => t("input", l)),
137
- onReady: m
138
- }, null, 8, ["model-value", "editor", "config", "disabled"]));
139
- }
140
- });
141
- export {
142
- te as default
143
- };
@@ -1,32 +0,0 @@
1
- import { Size } from '@/utils/sizes';
2
- import { ToolbarOption } from '@/types/ToolbarOption';
3
- type __VLS_Props = {
4
- modelValue: string | null | undefined;
5
- editor?: 'classic' | 'inline' | 'balloon';
6
- size?: Size;
7
- toolbar?: ToolbarOption[] | string[];
8
- placeholder?: string;
9
- disabled?: boolean;
10
- };
11
- declare const _default: import("vue").DefineComponent<__VLS_Props, {
12
- getEditorData(): string;
13
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
- blur: (...args: any[]) => void;
15
- focus: (...args: any[]) => void;
16
- input: (...args: any[]) => void;
17
- "update:modelValue": (...args: any[]) => void;
18
- ready: (...args: any[]) => void;
19
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
20
- onBlur?: ((...args: any[]) => any) | undefined;
21
- onFocus?: ((...args: any[]) => any) | undefined;
22
- onInput?: ((...args: any[]) => any) | undefined;
23
- "onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
24
- onReady?: ((...args: any[]) => any) | undefined;
25
- }>, {
26
- placeholder: string;
27
- size: Size;
28
- disabled: boolean;
29
- toolbar: ToolbarOption[] | string[];
30
- editor: "classic" | "inline" | "balloon";
31
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
- export default _default;