@vaadin/rich-text-editor 24.2.0-dev.f254716fe → 24.3.0-alpha2

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