@kvass/rich-text 1.4.18 → 1.5.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.
Files changed (3) hide show
  1. package/index.js +38 -2
  2. package/package.json +5 -1
  3. package/src/Component.vue +183 -46
package/index.js CHANGED
@@ -1,4 +1,13 @@
1
- import Component from './src/Component.vue'
1
+ import Component, { Tiptap } from './src/Component.vue'
2
+ import Link from '@tiptap/extension-link'
3
+ import Superscript from '@tiptap/extension-superscript'
4
+ import Table from '@tiptap/extension-table'
5
+ import TableRow from '@tiptap/extension-table-row'
6
+ import TableCell from '@tiptap/extension-table-cell'
7
+ import TableHeader from '@tiptap/extension-table-header'
8
+
9
+ import { NodeViewWrapper, NodeViewContent, nodeViewProps, VueNodeViewRenderer } from '@tiptap/vue-2'
10
+
2
11
  export default Component
3
12
 
4
13
  const Options = {
@@ -6,6 +15,16 @@ const Options = {
6
15
  primary: 'Primærfarge',
7
16
  secondary: 'Sekundærfarge',
8
17
  },
18
+ commands: [
19
+ 'heading',
20
+ 'heading-h3',
21
+ 'bold',
22
+ 'italic',
23
+ 'superscript',
24
+ 'orderedList',
25
+ 'bulletList',
26
+ 'link',
27
+ ],
9
28
  }
10
29
  const setup = options => {
11
30
  for (let key in options) {
@@ -14,4 +33,21 @@ const setup = options => {
14
33
  }
15
34
  }
16
35
 
17
- export { setup, Options }
36
+ const DefaultCommands = Options.commands
37
+
38
+ export {
39
+ setup,
40
+ Options,
41
+ DefaultCommands,
42
+ Tiptap,
43
+ Link,
44
+ Superscript,
45
+ Table,
46
+ TableRow,
47
+ TableCell,
48
+ TableHeader,
49
+ NodeViewWrapper,
50
+ NodeViewContent,
51
+ nodeViewProps,
52
+ VueNodeViewRenderer,
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvass/rich-text",
3
- "version": "1.4.18",
3
+ "version": "1.5.1",
4
4
  "description": "rich text component for kvass",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -37,6 +37,10 @@
37
37
  "@tiptap/extension-image": "^2.2.2",
38
38
  "@tiptap/extension-link": "^2.2.2",
39
39
  "@tiptap/extension-superscript": "^2.2.2",
40
+ "@tiptap/extension-table": "^2.27.2",
41
+ "@tiptap/extension-table-cell": "^2.27.2",
42
+ "@tiptap/extension-table-header": "^2.27.2",
43
+ "@tiptap/extension-table-row": "^2.27.2",
40
44
  "@tiptap/pm": "^2.2.2",
41
45
  "@tiptap/starter-kit": "^2.2.2",
42
46
  "@tiptap/vue-2": "^2.2.2"
package/src/Component.vue CHANGED
@@ -36,11 +36,7 @@
36
36
  'is-active': editor.isActive(item.isActiveKey || item.stateKey, item.commandProps),
37
37
  }"
38
38
  :disabled="disabled || item.disabled"
39
- @click="
40
- item.customFunc
41
- ? item.customFunc()
42
- : editor.chain().focus()[item.commandKey](item.commandProps).run()
43
- "
39
+ @click="() => onMenuBarButtonClick(item)"
44
40
  type="button"
45
41
  >
46
42
  <FontAwesomeIcon :icon="item.icon" />
@@ -61,20 +57,39 @@
61
57
  'is-active': editor.isActive(item.isActiveKey || item.stateKey, item.commandProps),
62
58
  }"
63
59
  :disabled="disabled || item.disabled"
64
- @click="
65
- item.customFunc
66
- ? item.customFunc()
67
- : editor.chain().focus()[item.commandKey](item.commandProps).run()
68
- "
60
+ @click="() => onMenuBarButtonClick(item)"
69
61
  >
70
62
  <FontAwesomeIcon :icon="item.icon" size="sm" />
71
63
  </button>
72
64
  </div>
65
+
66
+ <transition name="toolbar">
67
+ <div v-if="editor.isActive('table') && tableExtension" class="richtext__menubar-table">
68
+ <div class="richtext__menubar-buttons">
69
+ <template v-for="(ctrl, index) in tableExtension.controls">
70
+ <div v-if="ctrl.separator" :key="'sep-' + index" class="richtext__separator"></div>
71
+ <button
72
+ v-else
73
+ :key="'tbl-' + index"
74
+ type="button"
75
+ :class="{ 'is-confirming': confirmationIndex === index }"
76
+ :disabled="!editor.can()[ctrl.command]()"
77
+ @mousedown.prevent
78
+ @click="handleTableControl(ctrl, index)"
79
+ :title="ctrl.label"
80
+ >
81
+ <Icon :icon="ctrl.icon" size="lg" />
82
+ </button>
83
+ </template>
84
+ </div>
85
+ </div>
86
+ </transition>
73
87
  </div>
74
88
  </div>
75
89
  </template>
76
90
 
77
91
  <script>
92
+ import * as Tiptap from '@tiptap/core'
78
93
  import { Editor, EditorContent, BubbleMenu } from '@tiptap/vue-2'
79
94
  import Link from '@tiptap/extension-link'
80
95
  import Image from '@tiptap/extension-image'
@@ -82,22 +97,12 @@ import Superscript from '@tiptap/extension-superscript'
82
97
  import CallToAction from './extensions/cta'
83
98
  import StarterKit from '@tiptap/starter-kit'
84
99
 
85
- const DefaultCommands = [
86
- 'heading',
87
- 'heading-h3',
88
- 'bold',
89
- 'italic',
90
- 'superscript',
91
- 'orderedList',
92
- 'bulletList',
93
- 'link',
94
- 'cta',
95
- ]
96
-
97
100
  import { Options } from '../index'
98
101
 
99
- export { DefaultCommands }
102
+ export { Tiptap }
100
103
  export default {
104
+ name: 'RichText',
105
+ inject: { RichTextOptions: { default: null } },
101
106
  props: {
102
107
  value: {
103
108
  type: String,
@@ -106,7 +111,10 @@ export default {
106
111
  disabled: Boolean,
107
112
  commands: {
108
113
  type: Array,
109
- default: () => DefaultCommands,
114
+ default() {
115
+ const hasInject = this.RichTextOptions && this.RichTextOptions.commands
116
+ return hasInject ? this.RichTextOptions.commands : Options.commands
117
+ },
110
118
  },
111
119
  labels: {
112
120
  type: Object,
@@ -134,6 +142,7 @@ export default {
134
142
  linkUrl: null,
135
143
  bubbleState: null,
136
144
  bubbleOptions: { theme: 'primary' },
145
+ confirmationIndex: null,
137
146
  }
138
147
  },
139
148
  computed: {
@@ -217,11 +226,24 @@ export default {
217
226
  },
218
227
  disabled: this.editor && this.editor.state.selection.empty,
219
228
  icon: ['far', 'link'],
220
- extension: Link,
229
+ extension: Link.extend({
230
+ priority: 100,
231
+ parseHTML() {
232
+ return [{ tag: 'a[href]:not([data-cta])' }]
233
+ },
234
+ }).configure({
235
+ openOnClick: false,
236
+ autolink: true,
237
+ enableClickSelection: true,
238
+ HTMLAttributes: {
239
+ rel: 'noopener noreferrer',
240
+ },
241
+ }),
221
242
  appearance: {
222
243
  menu: true,
223
244
  bubble: true,
224
245
  },
246
+ priority: 100,
225
247
  },
226
248
  {
227
249
  stateKey: 'image',
@@ -234,31 +256,28 @@ export default {
234
256
  bubble: false,
235
257
  },
236
258
  },
237
- {
238
- stateKey: 'cta',
239
- commandKey: 'toggleCta',
240
- customFunc: () => {
241
- this.bubbleState = 'cta'
242
- },
243
- disabled: this.editor && this.editor.state.selection.empty,
244
- icon: ['far', 'mouse-pointer'],
245
- extension: CallToAction,
246
- appearance: {
247
- menu: true,
248
- bubble: true,
249
- },
250
- },
251
259
  ]
252
260
  },
261
+ tableExtension() {
262
+ return this.commands.find(cmd => cmd.stateKey === 'table')
263
+ },
253
264
  },
254
265
  methods: {
255
266
  getCommands(appearance) {
256
267
  return this.commands
257
- .map(cmd => this.availableCommands.find(a => a.stateKey === cmd))
268
+ .map(cmd => {
269
+ if (typeof cmd === 'string') return this.availableCommands.find(a => a.stateKey === cmd)
270
+ if (typeof cmd === 'function') return cmd.call(this)
271
+ return cmd
272
+ })
258
273
  .filter(Boolean)
259
274
  .filter(cmd => !appearance || cmd.appearance[appearance])
260
275
  },
276
+ onMenuBarButtonClick(item) {
277
+ if (item.customFunc) return item.customFunc.call(this, { editor: this.editor })
261
278
 
279
+ return this.editor.chain().focus()[item.commandKey](item.commandProps).run()
280
+ },
262
281
  setLink(value) {
263
282
  let editor = this.editor.chain().focus()
264
283
  switch (this.bubbleState) {
@@ -304,6 +323,19 @@ export default {
304
323
  if (!typeof value === 'string') return
305
324
  return ['/', '#'].some(v => value.startsWith(v))
306
325
  },
326
+ handleTableControl(ctrl, index) {
327
+ if (ctrl.requiresConfirmation) {
328
+ if (this.confirmationIndex === index) {
329
+ this.editor.chain().focus()[ctrl.command]().run()
330
+ this.confirmationIndex = null
331
+ } else {
332
+ this.confirmationIndex = index
333
+ }
334
+ } else {
335
+ this.confirmationIndex = null
336
+ this.editor.chain().focus()[ctrl.command]().run()
337
+ }
338
+ },
307
339
  },
308
340
  mounted() {
309
341
  this.editor = new Editor({
@@ -312,13 +344,12 @@ export default {
312
344
  StarterKit,
313
345
  ...this.getCommands()
314
346
  .map(({ extension }) => extension)
347
+ .flat()
315
348
  .filter(Boolean),
316
349
  ],
317
350
  content: this.value,
318
351
  onUpdate: () => {
319
- let html = this.editor.getHTML().replace(/^<p>\s*<\/p>$/, '')
320
-
321
- this.$emit('input', html)
352
+ this.$emit('input', this.editor.getHTML())
322
353
  },
323
354
  onSelectionUpdate: ({ editor }) => {
324
355
  if (!this.bubbleShouldShow({ editor })) this.bubbleState = null
@@ -331,8 +362,6 @@ export default {
331
362
  components: {
332
363
  EditorContent,
333
364
  BubbleMenu,
334
- // EditorMenuBar,
335
- // EditorMenuBubble,
336
365
  },
337
366
  }
338
367
  </script>
@@ -343,6 +372,8 @@ export default {
343
372
  flex-grow: 1;
344
373
  display: flex;
345
374
  flex-direction: column-reverse;
375
+ background-color: #ffffff;
376
+ border-radius: var(--border-radius, 3px);
346
377
 
347
378
  &--disabled {
348
379
  background-color: var(--vue-elder-input-color, #fafafa);
@@ -397,7 +428,8 @@ export default {
397
428
  border-radius: var(--border-radius);
398
429
  text-decoration: none;
399
430
 
400
- display: inline-block;
431
+ display: inline-flex;
432
+ gap: 0.5em;
401
433
  }
402
434
 
403
435
  a[data-cta='primary'] {
@@ -481,17 +513,41 @@ export default {
481
513
  }
482
514
  }
483
515
 
516
+ &__separator {
517
+ width: 1px;
518
+ height: 1.25rem;
519
+ background-color: var(--border-color);
520
+ margin: 0 0.5rem;
521
+ align-self: center;
522
+ }
523
+
484
524
  &__menubar {
485
525
  padding: 0.75rem;
486
526
  position: sticky;
487
527
  top: 0;
488
528
  z-index: 1;
489
529
 
530
+ display: flex;
531
+ flex-direction: column;
532
+
490
533
  &-buttons {
491
534
  width: max-content;
492
535
  box-shadow: 0 2px 4px -2px rgba(black, 0.1);
493
536
  }
494
537
 
538
+ &-table {
539
+ animation: fadeIn 0.2s ease-in-out;
540
+ margin-top: 0.5rem;
541
+
542
+ .richtext__menubar-buttons {
543
+ background-color: #f8f9fa;
544
+ border: 1px solid var(--border-color);
545
+ border-radius: var(--border-radius);
546
+ display: flex;
547
+ margin-top: 0.5rem;
548
+ }
549
+ }
550
+
495
551
  button {
496
552
  padding: 0.5rem;
497
553
  min-width: 2.5rem;
@@ -527,6 +583,16 @@ export default {
527
583
  &.is-active {
528
584
  background-color: darken(#ffffff, 3%);
529
585
  }
586
+
587
+ &.is-confirming {
588
+ background-color: #e3342f;
589
+ color: white !important;
590
+ border-color: #e3342f !important;
591
+
592
+ &:hover {
593
+ background-color: darken($color: #e3342f, $amount: 10%) !important;
594
+ }
595
+ }
530
596
  }
531
597
  }
532
598
 
@@ -537,5 +603,76 @@ export default {
537
603
  .tippy-box {
538
604
  max-width: initial !important;
539
605
  }
606
+
607
+ .toolbar-enter-active,
608
+ .toolbar-leave-active {
609
+ transition: all 0.25s ease-out;
610
+ max-height: 50px;
611
+ opacity: 1;
612
+ transform: translateY(0);
613
+ }
614
+
615
+ .toolbar-enter,
616
+ .toolbar-leave-to {
617
+ max-height: 0;
618
+ opacity: 0;
619
+ margin-top: 0;
620
+ transform: translateY(-5px);
621
+ overflow: hidden;
622
+ }
623
+
624
+ &__editor {
625
+ .ProseMirror {
626
+ table {
627
+ border-collapse: collapse;
628
+ table-layout: fixed;
629
+ width: 100%;
630
+ margin: 0;
631
+ overflow: hidden;
632
+
633
+ td,
634
+ th {
635
+ min-width: 1em;
636
+ border: 2px solid #ced4da;
637
+ padding: 3px 5px;
638
+ vertical-align: top;
639
+ box-sizing: border-box;
640
+ position: relative;
641
+
642
+ > * {
643
+ margin-bottom: 0;
644
+ }
645
+ }
646
+
647
+ th {
648
+ font-weight: bold;
649
+ text-align: left;
650
+ background-color: #f1f3f5;
651
+ }
652
+
653
+ .selectedCell:after {
654
+ z-index: 2;
655
+ position: absolute;
656
+ content: '';
657
+ left: 0;
658
+ right: 0;
659
+ top: 0;
660
+ bottom: 0;
661
+ background: rgba(200, 200, 255, 0.4);
662
+ pointer-events: none;
663
+ }
664
+
665
+ .column-resize-handle {
666
+ position: absolute;
667
+ right: -2px;
668
+ top: 0;
669
+ bottom: 0;
670
+ width: 4px;
671
+ background-color: #adf;
672
+ pointer-events: none;
673
+ }
674
+ }
675
+ }
676
+ }
540
677
  }
541
678
  </style>