@vaadin/rich-text-editor 24.3.0-alpha1 → 24.3.0-alpha10

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.
@@ -12,77 +12,16 @@ import '@vaadin/button/src/vaadin-button.js';
12
12
  import '@vaadin/confirm-dialog/src/vaadin-confirm-dialog.js';
13
13
  import '@vaadin/text-field/src/vaadin-text-field.js';
14
14
  import '@vaadin/tooltip/src/vaadin-tooltip.js';
15
- import '../vendor/vaadin-quill.js';
16
15
  import './vaadin-rich-text-editor-toolbar-styles.js';
17
16
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
18
- import { timeOut } from '@vaadin/component-base/src/async.js';
19
- import { isFirefox } from '@vaadin/component-base/src/browser-utils.js';
20
- import { Debouncer } from '@vaadin/component-base/src/debounce.js';
21
17
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
22
18
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
23
19
  import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
20
+ import { RichTextEditorMixin } from './vaadin-rich-text-editor-mixin.js';
24
21
  import { richTextEditorStyles } from './vaadin-rich-text-editor-styles.js';
25
22
 
26
23
  registerStyles('vaadin-rich-text-editor', richTextEditorStyles, { moduleId: 'vaadin-rich-text-editor-styles' });
27
24
 
28
- const Quill = window.Quill;
29
-
30
- // Workaround for text disappearing when accepting spellcheck suggestion
31
- // See https://github.com/quilljs/quill/issues/2096#issuecomment-399576957
32
- const Inline = Quill.import('blots/inline');
33
-
34
- class CustomColor extends Inline {
35
- constructor(domNode, value) {
36
- super(domNode, value);
37
-
38
- // Map <font> properties
39
- domNode.style.color = domNode.color;
40
-
41
- const span = this.replaceWith(new Inline(Inline.create()));
42
-
43
- span.children.forEach((child) => {
44
- if (child.attributes) child.attributes.copy(span);
45
- if (child.unwrap) child.unwrap();
46
- });
47
-
48
- this.remove();
49
-
50
- return span; // eslint-disable-line no-constructor-return
51
- }
52
- }
53
-
54
- CustomColor.blotName = 'customColor';
55
- CustomColor.tagName = 'FONT';
56
-
57
- Quill.register(CustomColor, true);
58
-
59
- const HANDLERS = [
60
- 'bold',
61
- 'italic',
62
- 'underline',
63
- 'strike',
64
- 'header',
65
- 'script',
66
- 'list',
67
- 'align',
68
- 'blockquote',
69
- 'code-block',
70
- ];
71
-
72
- const SOURCE = {
73
- API: 'api',
74
- USER: 'user',
75
- SILENT: 'silent',
76
- };
77
-
78
- const STATE = {
79
- DEFAULT: 0,
80
- FOCUSED: 1,
81
- CLICKED: 2,
82
- };
83
-
84
- const TAB_KEY = 9;
85
-
86
25
  /**
87
26
  * `<vaadin-rich-text-editor>` is a Web Component for rich text editing.
88
27
  * It provides a set of toolbar controls to apply formatting on the content,
@@ -123,6 +62,7 @@ const TAB_KEY = 9;
123
62
  * `toolbar-group-block` | The group for preformatted block controls
124
63
  * `toolbar-group-format` | The group for format controls
125
64
  * `toolbar-button` | The toolbar button (applies to all buttons)
65
+ * `toolbar-button-pressed` | The toolbar button in pressed state (applies to all buttons)
126
66
  * `toolbar-button-undo` | The "undo" button
127
67
  * `toolbar-button-redo` | The "redo" button
128
68
  * `toolbar-button-bold` | The "bold" button
@@ -154,40 +94,12 @@ const TAB_KEY = 9;
154
94
  * @customElement
155
95
  * @extends HTMLElement
156
96
  * @mixes ElementMixin
97
+ * @mixes RichTextEditorMixin
157
98
  * @mixes ThemableMixin
158
99
  */
159
- class RichTextEditor extends ElementMixin(ThemableMixin(PolymerElement)) {
100
+ class RichTextEditor extends RichTextEditorMixin(ElementMixin(ThemableMixin(PolymerElement))) {
160
101
  static get template() {
161
102
  return html`
162
- <style>
163
- :host {
164
- display: flex;
165
- flex-direction: column;
166
- box-sizing: border-box;
167
- }
168
-
169
- :host([hidden]) {
170
- display: none !important;
171
- }
172
-
173
- .announcer {
174
- position: fixed;
175
- clip: rect(0, 0, 0, 0);
176
- }
177
-
178
- input[type='file'] {
179
- display: none;
180
- }
181
-
182
- .vaadin-rich-text-editor-container {
183
- display: flex;
184
- flex-direction: column;
185
- min-height: inherit;
186
- max-height: inherit;
187
- flex: auto;
188
- }
189
- </style>
190
-
191
103
  <div class="vaadin-rich-text-editor-container">
192
104
  <!-- Create toolbar container -->
193
105
  <div part="toolbar" role="toolbar">
@@ -404,775 +316,6 @@ class RichTextEditor extends ElementMixin(ThemableMixin(PolymerElement)) {
404
316
  return 'vaadin-rich-text-editor';
405
317
  }
406
318
 
407
- static get properties() {
408
- return {
409
- /**
410
- * Value is a list of the operations which describe change to the document.
411
- * Each of those operations describe the change at the current index.
412
- * They can be an `insert`, `delete` or `retain`. The format is as follows:
413
- *
414
- * ```js
415
- * [
416
- * { insert: 'Hello World' },
417
- * { insert: '!', attributes: { bold: true }}
418
- * ]
419
- * ```
420
- *
421
- * See also https://github.com/quilljs/delta for detailed documentation.
422
- * @type {string}
423
- */
424
- value: {
425
- type: String,
426
- notify: true,
427
- value: '',
428
- },
429
-
430
- /**
431
- * HTML representation of the rich text editor content.
432
- */
433
- htmlValue: {
434
- type: String,
435
- notify: true,
436
- readOnly: true,
437
- },
438
-
439
- /**
440
- * When true, the user can not modify, nor copy the editor content.
441
- * @type {boolean}
442
- */
443
- disabled: {
444
- type: Boolean,
445
- value: false,
446
- reflectToAttribute: true,
447
- },
448
-
449
- /**
450
- * When true, the user can not modify the editor content, but can copy it.
451
- * @type {boolean}
452
- */
453
- readonly: {
454
- type: Boolean,
455
- value: false,
456
- reflectToAttribute: true,
457
- },
458
-
459
- /**
460
- * An object used to localize this component. The properties are used
461
- * e.g. as the tooltips for the editor toolbar buttons.
462
- *
463
- * @type {!RichTextEditorI18n}
464
- * @default {English/US}
465
- */
466
- i18n: {
467
- type: Object,
468
- value: () => {
469
- return {
470
- undo: 'undo',
471
- redo: 'redo',
472
- bold: 'bold',
473
- italic: 'italic',
474
- underline: 'underline',
475
- strike: 'strike',
476
- h1: 'h1',
477
- h2: 'h2',
478
- h3: 'h3',
479
- subscript: 'subscript',
480
- superscript: 'superscript',
481
- listOrdered: 'list ordered',
482
- listBullet: 'list bullet',
483
- alignLeft: 'align left',
484
- alignCenter: 'align center',
485
- alignRight: 'align right',
486
- image: 'image',
487
- link: 'link',
488
- blockquote: 'blockquote',
489
- codeBlock: 'code block',
490
- clean: 'clean',
491
- linkDialogTitle: 'Link address',
492
- ok: 'OK',
493
- cancel: 'Cancel',
494
- remove: 'Remove',
495
- };
496
- },
497
- },
498
-
499
- /** @private */
500
- _editor: {
501
- type: Object,
502
- },
503
-
504
- /**
505
- * Stores old value
506
- * @private
507
- */
508
- __oldValue: String,
509
-
510
- /** @private */
511
- __lastCommittedChange: {
512
- type: String,
513
- value: '',
514
- },
515
-
516
- /** @private */
517
- _linkEditing: {
518
- type: Boolean,
519
- },
520
-
521
- /** @private */
522
- _linkRange: {
523
- type: Object,
524
- value: null,
525
- },
526
-
527
- /** @private */
528
- _linkIndex: {
529
- type: Number,
530
- value: null,
531
- },
532
-
533
- /** @private */
534
- _linkUrl: {
535
- type: String,
536
- value: '',
537
- },
538
- };
539
- }
540
-
541
- static get observers() {
542
- return ['_valueChanged(value, _editor)', '_disabledChanged(disabled, readonly, _editor)'];
543
- }
544
-
545
- /** @private */
546
- get _toolbarButtons() {
547
- return Array.from(this.shadowRoot.querySelectorAll('[part="toolbar"] button')).filter((btn) => {
548
- return btn.clientHeight > 0;
549
- });
550
- }
551
-
552
- /**
553
- * @param {string} prop
554
- * @param {?string} oldVal
555
- * @param {?string} newVal
556
- * @protected
557
- */
558
- attributeChangedCallback(prop, oldVal, newVal) {
559
- super.attributeChangedCallback(prop, oldVal, newVal);
560
-
561
- if (prop === 'dir') {
562
- this.__dir = newVal;
563
- this.__setDirection(newVal);
564
- }
565
- }
566
-
567
- /** @protected */
568
- disconnectedCallback() {
569
- super.disconnectedCallback();
570
-
571
- // Ensure that htmlValue property set before attach
572
- // gets applied in case of detach and re-attach.
573
- if (this.__debounceSetValue && this.__debounceSetValue.isActive()) {
574
- this.__debounceSetValue.flush();
575
- }
576
-
577
- this._editor.emitter.removeAllListeners();
578
- this._editor.emitter.listeners = {};
579
- }
580
-
581
- /** @private */
582
- __setDirection(dir) {
583
- // Needed for proper `ql-align` class to be set and activate the toolbar align button
584
- const alignAttributor = Quill.import('attributors/class/align');
585
- alignAttributor.whitelist = [dir === 'rtl' ? 'left' : 'right', 'center', 'justify'];
586
- Quill.register(alignAttributor, true);
587
-
588
- const alignGroup = this._toolbar.querySelector('[part~="toolbar-group-alignment"]');
589
-
590
- if (dir === 'rtl') {
591
- alignGroup.querySelector('[part~="toolbar-button-align-left"]').value = 'left';
592
- alignGroup.querySelector('[part~="toolbar-button-align-right"]').value = '';
593
- } else {
594
- alignGroup.querySelector('[part~="toolbar-button-align-left"]').value = '';
595
- alignGroup.querySelector('[part~="toolbar-button-align-right"]').value = 'right';
596
- }
597
-
598
- this._editor.getModule('toolbar').update(this._editor.getSelection());
599
- }
600
-
601
- /** @protected */
602
- connectedCallback() {
603
- super.connectedCallback();
604
-
605
- const editor = this.shadowRoot.querySelector('[part="content"]');
606
-
607
- this._editor = new Quill(editor, {
608
- modules: {
609
- toolbar: this._toolbarConfig,
610
- },
611
- });
612
-
613
- this.__patchToolbar();
614
- this.__patchKeyboard();
615
-
616
- /* c8 ignore next 3 */
617
- if (isFirefox) {
618
- this.__patchFirefoxFocus();
619
- }
620
-
621
- const editorContent = editor.querySelector('.ql-editor');
622
-
623
- editorContent.setAttribute('role', 'textbox');
624
- editorContent.setAttribute('aria-multiline', 'true');
625
-
626
- this._editor.on('text-change', () => {
627
- const timeout = 200;
628
- this.__debounceSetValue = Debouncer.debounce(this.__debounceSetValue, timeOut.after(timeout), () => {
629
- this.value = JSON.stringify(this._editor.getContents().ops);
630
- });
631
- });
632
-
633
- const TAB_KEY = 9;
634
-
635
- editorContent.addEventListener('keydown', (e) => {
636
- if (e.key === 'Escape') {
637
- if (!this.__tabBindings) {
638
- this.__tabBindings = this._editor.keyboard.bindings[TAB_KEY];
639
- this._editor.keyboard.bindings[TAB_KEY] = null;
640
- }
641
- } else if (this.__tabBindings) {
642
- this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
643
- this.__tabBindings = null;
644
- }
645
- });
646
-
647
- editorContent.addEventListener('blur', () => {
648
- if (this.__tabBindings) {
649
- this._editor.keyboard.bindings[TAB_KEY] = this.__tabBindings;
650
- this.__tabBindings = null;
651
- }
652
- });
653
-
654
- editorContent.addEventListener('focusout', () => {
655
- if (this._toolbarState === STATE.FOCUSED) {
656
- this._cleanToolbarState();
657
- } else {
658
- this.__emitChangeEvent();
659
- }
660
- });
661
-
662
- editorContent.addEventListener('focus', () => {
663
- // Format changed, but no value changed happened
664
- if (this._toolbarState === STATE.CLICKED) {
665
- this._cleanToolbarState();
666
- }
667
- });
668
-
669
- this._editor.on('selection-change', this.__announceFormatting.bind(this));
670
- }
671
-
672
- /** @protected */
673
- ready() {
674
- super.ready();
675
-
676
- this._toolbarConfig = this._prepareToolbar();
677
- this._toolbar = this._toolbarConfig.container;
678
-
679
- this._addToolbarListeners();
680
-
681
- this.$.linkDialog.$.dialog.$.overlay.addEventListener('vaadin-overlay-open', () => {
682
- this.$.linkUrl.focus();
683
- });
684
- }
685
-
686
- /** @private */
687
- _prepareToolbar() {
688
- const clean = Quill.imports['modules/toolbar'].DEFAULTS.handlers.clean;
689
- // eslint-disable-next-line @typescript-eslint/no-this-alias
690
- const self = this;
691
-
692
- const toolbar = {
693
- container: this.shadowRoot.querySelector('[part="toolbar"]'),
694
- handlers: {
695
- clean() {
696
- self._markToolbarClicked();
697
- clean.call(this);
698
- },
699
- },
700
- };
701
-
702
- HANDLERS.forEach((handler) => {
703
- toolbar.handlers[handler] = (value) => {
704
- this._markToolbarClicked();
705
- this._editor.format(handler, value, SOURCE.USER);
706
- };
707
- });
708
-
709
- return toolbar;
710
- }
711
-
712
- /** @private */
713
- _addToolbarListeners() {
714
- const buttons = this._toolbarButtons;
715
- const toolbar = this._toolbar;
716
-
717
- // Disable tabbing to all buttons but the first one
718
- buttons.forEach((button, index) => index > 0 && button.setAttribute('tabindex', '-1'));
719
-
720
- toolbar.addEventListener('keydown', (e) => {
721
- // Use roving tab-index for the toolbar buttons
722
- if ([37, 39].indexOf(e.keyCode) > -1) {
723
- e.preventDefault();
724
- let index = buttons.indexOf(e.target);
725
- buttons[index].setAttribute('tabindex', '-1');
726
-
727
- let step;
728
- if (e.keyCode === 39) {
729
- step = 1;
730
- } else if (e.keyCode === 37) {
731
- step = -1;
732
- }
733
- index = (buttons.length + index + step) % buttons.length;
734
- buttons[index].removeAttribute('tabindex');
735
- buttons[index].focus();
736
- }
737
- // Esc and Tab focuses the content
738
- if (e.keyCode === 27 || (e.keyCode === TAB_KEY && !e.shiftKey)) {
739
- e.preventDefault();
740
- this._editor.focus();
741
- }
742
- });
743
-
744
- // Mousedown happens before editor focusout
745
- toolbar.addEventListener('mousedown', (e) => {
746
- if (buttons.indexOf(e.composedPath()[0]) > -1) {
747
- this._markToolbarFocused();
748
- }
749
- });
750
- }
751
-
752
- /** @private */
753
- _markToolbarClicked() {
754
- this._toolbarState = STATE.CLICKED;
755
- }
756
-
757
- /** @private */
758
- _markToolbarFocused() {
759
- this._toolbarState = STATE.FOCUSED;
760
- }
761
-
762
- /** @private */
763
- _cleanToolbarState() {
764
- this._toolbarState = STATE.DEFAULT;
765
- }
766
-
767
- /** @private */
768
- __createFakeFocusTarget() {
769
- const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
770
- const elem = document.createElement('textarea');
771
- // Reset box model
772
- elem.style.border = '0';
773
- elem.style.padding = '0';
774
- elem.style.margin = '0';
775
- // Move element out of screen horizontally
776
- elem.style.position = 'absolute';
777
- elem.style[isRTL ? 'right' : 'left'] = '-9999px';
778
- // Move element to the same position vertically
779
- const yPosition = window.pageYOffset || document.documentElement.scrollTop;
780
- elem.style.top = `${yPosition}px`;
781
- return elem;
782
- }
783
-
784
- /** @private */
785
- __patchFirefoxFocus() {
786
- // In Firefox 63+ with native Shadow DOM, when moving focus out of
787
- // contenteditable and back again within same shadow root, cursor
788
- // disappears. See https://bugzilla.mozilla.org/show_bug.cgi?id=1496769
789
- const editorContent = this.shadowRoot.querySelector('.ql-editor');
790
- let isFake = false;
791
-
792
- const focusFake = () => {
793
- isFake = true;
794
- this.__fakeTarget = this.__createFakeFocusTarget();
795
- document.body.appendChild(this.__fakeTarget);
796
- // Let the focus step out of shadow root!
797
- this.__fakeTarget.focus();
798
- return new Promise((resolve) => {
799
- setTimeout(resolve);
800
- });
801
- };
802
-
803
- const focusBack = (offsetNode, offset) => {
804
- this._editor.focus();
805
- if (offsetNode) {
806
- this._editor.selection.setNativeRange(offsetNode, offset);
807
- }
808
- document.body.removeChild(this.__fakeTarget);
809
- delete this.__fakeTarget;
810
- isFake = false;
811
- };
812
-
813
- editorContent.addEventListener('mousedown', (e) => {
814
- if (!this._editor.hasFocus()) {
815
- const { x, y } = e;
816
- const { offset, offsetNode } = document.caretPositionFromPoint(x, y);
817
- focusFake().then(() => {
818
- focusBack(offsetNode, offset);
819
- });
820
- }
821
- });
822
-
823
- editorContent.addEventListener('focusin', () => {
824
- if (isFake === false) {
825
- focusFake().then(() => focusBack());
826
- }
827
- });
828
- }
829
-
830
- /** @private */
831
- __patchToolbar() {
832
- const toolbar = this._editor.getModule('toolbar');
833
- const update = toolbar.update;
834
-
835
- // Add custom link button to toggle state attribute
836
- toolbar.controls.push(['link', this.shadowRoot.querySelector('[part~="toolbar-button-link"]')]);
837
-
838
- toolbar.update = function (range) {
839
- update.call(toolbar, range);
840
-
841
- toolbar.controls.forEach((pair) => {
842
- const input = pair[1];
843
- if (input.classList.contains('ql-active')) {
844
- input.setAttribute('on', '');
845
- } else {
846
- input.removeAttribute('on');
847
- }
848
- });
849
- };
850
- }
851
-
852
- /** @private */
853
- __patchKeyboard() {
854
- const focusToolbar = () => {
855
- this._markToolbarFocused();
856
- this._toolbar.querySelector('button:not([tabindex])').focus();
857
- };
858
-
859
- const keyboard = this._editor.getModule('keyboard');
860
- const bindings = keyboard.bindings[TAB_KEY];
861
-
862
- // Exclude Quill shift-tab bindings, except for code block,
863
- // as some of those are breaking when on a newline in the list
864
- // https://github.com/vaadin/vaadin-rich-text-editor/issues/67
865
- const originalBindings = bindings.filter((b) => !b.shiftKey || (b.format && b.format['code-block']));
866
- const moveFocusBinding = { key: TAB_KEY, shiftKey: true, handler: focusToolbar };
867
-
868
- keyboard.bindings[TAB_KEY] = [...originalBindings, moveFocusBinding];
869
-
870
- // Alt-f10 focuses a toolbar button
871
- keyboard.addBinding({ key: 121, altKey: true, handler: focusToolbar });
872
- }
873
-
874
- /** @private */
875
- __emitChangeEvent() {
876
- let lastCommittedChange = this.__lastCommittedChange;
877
-
878
- if (this.__debounceSetValue && this.__debounceSetValue.isActive()) {
879
- lastCommittedChange = this.value;
880
- this.__debounceSetValue.flush();
881
- }
882
-
883
- if (lastCommittedChange !== this.value) {
884
- this.dispatchEvent(new CustomEvent('change', { bubbles: true, cancelable: false }));
885
- this.__lastCommittedChange = this.value;
886
- }
887
- }
888
-
889
- /** @private */
890
- _onLinkClick() {
891
- const range = this._editor.getSelection();
892
- if (range) {
893
- const LinkBlot = Quill.imports['formats/link'];
894
- const [link, offset] = this._editor.scroll.descendant(LinkBlot, range.index);
895
- if (link != null) {
896
- // Existing link
897
- this._linkRange = { index: range.index - offset, length: link.length() };
898
- this._linkUrl = LinkBlot.formats(link.domNode);
899
- } else if (range.length === 0) {
900
- this._linkIndex = range.index;
901
- }
902
- this._linkEditing = true;
903
- }
904
- }
905
-
906
- /** @private */
907
- _applyLink(link) {
908
- if (link) {
909
- this._markToolbarClicked();
910
- this._editor.format('link', link, SOURCE.USER);
911
- this._editor.getModule('toolbar').update(this._editor.selection.savedRange);
912
- }
913
- this._closeLinkDialog();
914
- }
915
-
916
- /** @private */
917
- _insertLink(link, position) {
918
- if (link) {
919
- this._markToolbarClicked();
920
- this._editor.insertText(position, link, { link });
921
- this._editor.setSelection(position, link.length);
922
- }
923
- this._closeLinkDialog();
924
- }
925
-
926
- /** @private */
927
- _updateLink(link, range) {
928
- this._markToolbarClicked();
929
- this._editor.formatText(range, 'link', link, SOURCE.USER);
930
- this._closeLinkDialog();
931
- }
932
-
933
- /** @private */
934
- _removeLink() {
935
- this._markToolbarClicked();
936
- if (this._linkRange != null) {
937
- this._editor.formatText(this._linkRange, { link: false, color: false }, SOURCE.USER);
938
- }
939
- this._closeLinkDialog();
940
- }
941
-
942
- /** @private */
943
- _closeLinkDialog() {
944
- this._linkEditing = false;
945
- this._linkUrl = '';
946
- this._linkIndex = null;
947
- this._linkRange = null;
948
- }
949
-
950
- /** @private */
951
- _onLinkEditConfirm() {
952
- if (this._linkIndex != null) {
953
- this._insertLink(this._linkUrl, this._linkIndex);
954
- } else if (this._linkRange) {
955
- this._updateLink(this._linkUrl, this._linkRange);
956
- } else {
957
- this._applyLink(this._linkUrl);
958
- }
959
- }
960
-
961
- /** @private */
962
- _onLinkEditCancel() {
963
- this._closeLinkDialog();
964
- this._editor.focus();
965
- }
966
-
967
- /** @private */
968
- _onLinkEditRemove() {
969
- this._removeLink();
970
- this._closeLinkDialog();
971
- }
972
-
973
- /** @private */
974
- _onLinkKeydown(e) {
975
- if (e.keyCode === 13) {
976
- e.preventDefault();
977
- e.stopPropagation();
978
- this.$.confirmLink.click();
979
- }
980
- }
981
-
982
- /** @private */
983
- __updateHtmlValue() {
984
- const editor = this.shadowRoot.querySelector('.ql-editor');
985
- let content = editor.innerHTML;
986
-
987
- // Remove Quill classes, e.g. ql-syntax, except for align
988
- content = content.replace(/\s*ql-(?!align)[\w-]*\s*/gu, '');
989
- // Remove meta spans, e.g. cursor which are empty after Quill classes removed
990
- content = content.replace(/<\/?span[^>]*>/gu, '');
991
-
992
- // Replace Quill align classes with inline styles
993
- [this.__dir === 'rtl' ? 'left' : 'right', 'center', 'justify'].forEach((align) => {
994
- content = content.replace(
995
- new RegExp(` class=[\\\\]?"\\s?ql-align-${align}[\\\\]?"`, 'gu'),
996
- ` style="text-align: ${align}"`,
997
- );
998
- });
999
-
1000
- content = content.replace(/ class=""/gu, '');
1001
-
1002
- this._setHtmlValue(content);
1003
- }
1004
-
1005
- /**
1006
- * Sets content represented by HTML snippet into the editor.
1007
- * The snippet is interpreted by [Quill's Clipboard matchers](https://quilljs.com/docs/modules/clipboard/#matchers),
1008
- * which may not produce the exactly input HTML.
1009
- *
1010
- * **NOTE:** Improper handling of HTML can lead to cross site scripting (XSS) and failure to sanitize
1011
- * properly is both notoriously error-prone and a leading cause of web vulnerabilities.
1012
- * This method is aptly named to ensure the developer has taken the necessary precautions.
1013
- * @param {string} htmlValue
1014
- */
1015
- dangerouslySetHtmlValue(htmlValue) {
1016
- const deltaFromHtml = this._editor.clipboard.convert(htmlValue);
1017
- this._editor.setContents(deltaFromHtml, SOURCE.API);
1018
- }
1019
-
1020
- /** @private */
1021
- __announceFormatting() {
1022
- const timeout = 200;
1023
-
1024
- const announcer = this.shadowRoot.querySelector('.announcer');
1025
- announcer.textContent = '';
1026
-
1027
- this.__debounceAnnounceFormatting = Debouncer.debounce(
1028
- this.__debounceAnnounceFormatting,
1029
- timeOut.after(timeout),
1030
- () => {
1031
- const formatting = Array.from(this.shadowRoot.querySelectorAll('[part="toolbar"] .ql-active'))
1032
- .map((button) => {
1033
- const tooltip = this.shadowRoot.querySelector(`[for="${button.id}"]`);
1034
- return tooltip.text;
1035
- })
1036
- .join(', ');
1037
- announcer.textContent = formatting;
1038
- },
1039
- );
1040
- }
1041
-
1042
- /** @private */
1043
- _clear() {
1044
- this._editor.deleteText(0, this._editor.getLength(), SOURCE.SILENT);
1045
- this.__updateHtmlValue();
1046
- }
1047
-
1048
- /** @private */
1049
- _undo(e) {
1050
- e.preventDefault();
1051
- this._editor.history.undo();
1052
- this._editor.focus();
1053
- }
1054
-
1055
- /** @private */
1056
- _redo(e) {
1057
- e.preventDefault();
1058
- this._editor.history.redo();
1059
- this._editor.focus();
1060
- }
1061
-
1062
- /** @private */
1063
- _toggleToolbarDisabled(disable) {
1064
- const buttons = this._toolbarButtons;
1065
- if (disable) {
1066
- buttons.forEach((btn) => btn.setAttribute('disabled', 'true'));
1067
- } else {
1068
- buttons.forEach((btn) => btn.removeAttribute('disabled'));
1069
- }
1070
- }
1071
-
1072
- /** @private */
1073
- _onImageTouchEnd(e) {
1074
- // Cancel the event to avoid the following click event
1075
- e.preventDefault();
1076
- this._onImageClick();
1077
- }
1078
-
1079
- /** @private */
1080
- _onImageClick() {
1081
- this.$.fileInput.value = '';
1082
- this.$.fileInput.click();
1083
- }
1084
-
1085
- /** @private */
1086
- _uploadImage(e) {
1087
- const fileInput = e.target;
1088
- // NOTE: copied from https://github.com/quilljs/quill/blob/1.3.6/themes/base.js#L128
1089
- // needs to be updated in case of switching to Quill 2.0.0
1090
- if (fileInput.files != null && fileInput.files[0] != null) {
1091
- const reader = new FileReader();
1092
- reader.onload = (e) => {
1093
- const image = e.target.result;
1094
- const range = this._editor.getSelection(true);
1095
- this._editor.updateContents(
1096
- new Quill.imports.delta().retain(range.index).delete(range.length).insert({ image }),
1097
- SOURCE.USER,
1098
- );
1099
- this._markToolbarClicked();
1100
- this._editor.setSelection(range.index + 1, SOURCE.SILENT);
1101
- fileInput.value = '';
1102
- };
1103
- reader.readAsDataURL(fileInput.files[0]);
1104
- }
1105
- }
1106
-
1107
- /** @private */
1108
- _disabledChanged(disabled, readonly, editor) {
1109
- if (disabled === undefined || readonly === undefined || editor === undefined) {
1110
- return;
1111
- }
1112
-
1113
- if (disabled || readonly) {
1114
- editor.enable(false);
1115
-
1116
- if (disabled) {
1117
- this._toggleToolbarDisabled(true);
1118
- }
1119
- } else {
1120
- editor.enable();
1121
-
1122
- if (this.__oldDisabled) {
1123
- this._toggleToolbarDisabled(false);
1124
- }
1125
- }
1126
-
1127
- this.__oldDisabled = disabled;
1128
- }
1129
-
1130
- /** @private */
1131
- _valueChanged(value, editor) {
1132
- if (editor === undefined) {
1133
- return;
1134
- }
1135
-
1136
- if (value == null || value === '[{"insert":"\\n"}]') {
1137
- this.value = '';
1138
- return;
1139
- }
1140
-
1141
- if (value === '') {
1142
- this._clear();
1143
- return;
1144
- }
1145
-
1146
- let parsedValue;
1147
- try {
1148
- parsedValue = JSON.parse(value);
1149
- if (Array.isArray(parsedValue)) {
1150
- this.__oldValue = value;
1151
- } else {
1152
- throw new Error(`expected JSON string with array of objects, got: ${value}`);
1153
- }
1154
- } catch (err) {
1155
- // Use old value in case new one is not suitable
1156
- this.value = this.__oldValue;
1157
- console.error('Invalid value set to rich-text-editor:', err);
1158
- return;
1159
- }
1160
- const delta = new Quill.imports.delta(parsedValue);
1161
- // Suppress text-change event to prevent infinite loop
1162
- if (JSON.stringify(editor.getContents()) !== JSON.stringify(delta)) {
1163
- editor.setContents(delta, SOURCE.SILENT);
1164
- }
1165
- this.__updateHtmlValue();
1166
-
1167
- if (this._toolbarState === STATE.CLICKED) {
1168
- this._cleanToolbarState();
1169
- this.__emitChangeEvent();
1170
- } else if (!this._editor.hasFocus()) {
1171
- // Value changed from outside
1172
- this.__lastCommittedChange = this.value;
1173
- }
1174
- }
1175
-
1176
319
  /**
1177
320
  * Fired when the user commits a value change.
1178
321
  *