@vaadin/rich-text-editor 24.2.0-beta1 → 24.2.0-beta3

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,
@@ -154,40 +93,12 @@ const TAB_KEY = 9;
154
93
  * @customElement
155
94
  * @extends HTMLElement
156
95
  * @mixes ElementMixin
96
+ * @mixes RichTextEditorMixin
157
97
  * @mixes ThemableMixin
158
98
  */
159
- class RichTextEditor extends ElementMixin(ThemableMixin(PolymerElement)) {
99
+ class RichTextEditor extends RichTextEditorMixin(ElementMixin(ThemableMixin(PolymerElement))) {
160
100
  static get template() {
161
101
  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
102
  <div class="vaadin-rich-text-editor-container">
192
103
  <!-- Create toolbar container -->
193
104
  <div part="toolbar" role="toolbar">
@@ -404,775 +315,6 @@ class RichTextEditor extends ElementMixin(ThemableMixin(PolymerElement)) {
404
315
  return 'vaadin-rich-text-editor';
405
316
  }
406
317
 
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
318
  /**
1177
319
  * Fired when the user commits a value change.
1178
320
  *