aboutsummaryrefslogtreecommitdiff
path: root/node_modules/montage/ui/rich-text-editor/rich-text-editor.reel/rich-text-editor.js
blob: 13ba05186d58eeca8b90bbfefd177cbd1dcbbabf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
/* <copyright>
 This file contains proprietary software owned by Motorola Mobility, Inc.<br/>
 No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/>
 (c) Copyright 2011 Motorola Mobility, Inc.  All Rights Reserved.
 </copyright> */
/**
    @module "montage/ui/rich-text-editor/rich-text-editor.reel"
    @requires montage/core/core
    @requires montage/core/event/mutable-event
    @requires montage/core/event/event-manager
    @requires rich-text-sanitizer
*/
var Montage = require("montage").Montage,
    RichTextEditorBase = require("./rich-text-editor-base").RichTextEditorBase,
    Sanitizer = require("./rich-text-sanitizer").Sanitizer,
    MutableEvent = require("core/event/mutable-event").MutableEvent,
    defaultEventManager = require("core/event/event-manager").defaultEventManager;

/**
    @classdesc The RichTextEditor component is a lightweight Montage component that provides basic HTML editing capability. It wraps the HTML5 <code>contentEditable</code> property and largely relies on the browser's support of <code><a href="http://www.quirksmode.org/dom/execCommand.html" target="_blank">execCommand</a></code>.
    @class module:"montage/ui/rich-text-editor/rich-text-editor.reel".RichTextEditor
    @extends module:montage/ui/component.Component
    @summary
The easiest way to create a RichTextEditor is with a serialization and a &lt;div> tag:<p>

<em>Serialization</em>
<pre class="sh_javascript">
{
"editor": {
   "prototype": "montage/ui/rich-text-editor/rich-text-editor.reel",
   "properties": {
      "element": {"#": "editor" }
   }
}
</pre>
<em>HTML</em>
<pre class="sh_javascript">
&lt;body&gt;
&lt;div data-montage-id="editor"&gt;
    &lt;span&gt;Hello World!&lt;/span&gt;
&lt;/div&gt;
&lt;/body&gt;
</pre>
*/
exports.RichTextEditor = Montage.create(RichTextEditorBase,/** @lends module:"montage/ui/rich-text-editor/rich-text-editor.reel".RichTextEditor# */ {

/**
    Returns <code>true</code> if the edtior has focus, otherwise returns <code>false</code>.
    @type {boolean}
    @readonly
*/
    hasFocus: {
        enumerable: true,
        get: function() {
            return this._hasFocus;
        }
    },

/**
    Returns the editor's inner element, which is the element that is editable.
     @type {Element}
    @readonly
*/
    innerElement: {
        enumerable: true,
        get: function() {
            return this._innerElement;
        }
    },


    /**
      Sets the focus on the editor's element. The editor will also become the <code>activeElement</code>.
      @function
    */
    focus: {
        enumerable: true,
        value: function() {
            this._needsFocus = true;
            this.needsDraw = true;
        }
    },

    /**
      Returns <code>true</code> when the editor is the active element, otherwise return <code>false</code>. Normally the active element has also focus. However, in a multiple window environment it’s possible to be the active element without having focus. Typically, a toolbar item my steal the focus but not become the active element.

     @type {boolean}
    @readonly
    */
    isActiveElement: {
        enumerable: true,
        get: function() {
            return this._isActiveElement;
        }
    },

    /**
     Returns <code>true</code> if the content is read only, otherwise returns <code>false</code>. When the editor is set to read only, the user is not able to modify the content. However it still possible to set the content programmatically with by setting the <code>value</code> or <code>textValue</code> properties.
     @type {boolean}
    */
    readOnly: {
        enumerable: true,
        get: function() {
            return this._readOnly;
        },
        set: function(value) {
            if (this._readOnly !== value) {
                this._readOnly = value;
                if (value) {
                    // Remove any overlay
                    this.hideOverlay();
                }
                this.needsDraw = true;
            }
        }
    },

    /**
      Gets or sets the editor's content as HTML. By default, the HTML content assigned to the editor's DOM element is used.
      The new value is passed through the editor's sanitizer before being assigned.
     @type {string}
    */
    value: {
        enumerable: true,
        serializable: true,
        get: function() {
            var contentNode = this._innerElement,
                content = "",
                overlayElement = null,
                overlayParent,
                overlayNextSibling;

            if (this._dirtyValue) {
                if (contentNode) {
                    // Temporary orphran the overlay slot while retrieving the content
                    overlayElement = contentNode.querySelector(".montage-editor-overlay");
                    if (overlayElement) {
                        overlayParent = overlayElement.parentNode;
                        overlayNextSibling = overlayElement.nextSibling;
                        overlayParent.removeChild(overlayElement);
                    }
                    content = contentNode.innerHTML;
                }

                if (content == "<br>") {
                    // when the contentEditable div is emptied, Chrome add a <br>, let's filter it out
                    content = "";
                }
                if (this._sanitizer === undefined) {
                    this._sanitizer = Sanitizer.create();
                }
                if (this._sanitizer) {
                    content = this._sanitizer.didGetValue(content, this._uniqueId);
                }

                // restore the overlay
                if (overlayElement) {
                    overlayParent.insertBefore(overlayElement, overlayNextSibling);
                }

                this._value = content;
                this._dirtyValue = false;
            }
            return this._value;
        },
        set: function(value) {
            if (this._value !== value || this._dirtyValue) {
                // Remove any overlay
                this.hideOverlay();

                if (this._sanitizer === undefined) {
                    this._sanitizer = Sanitizer.create();
                }
                if (this._sanitizer) {
                    value = this._sanitizer.willSetValue(value, this._uniqueId);
                }
                this._value = value;
                this._dirtyValue = false;
                this._dirtyTextValue = true;
                this._needsAssingValue = true;
                this.needsDraw = true;
            }
            this._needsOriginalContent = false;
        }
    },

/**
    Gets or sets the editor's content as plain text. By default, the text content assigned to the editor's DOM element is used.
    @type {string}
*/
    textValue: {
        enumerable: true,
        get: function() {
            var contentNode = this._innerElement,
                overlayElement = null,
                overlayParent,
                overlayNextSibling;

            if (this._dirtyTextValue) {
                if (contentNode) {
                    // Temporary orphran the overlay slot in order to retrieve the content
                    overlayElement = contentNode.querySelector(".montage-editor-overlay");
                    if (overlayElement) {
                        overlayParent = overlayElement.parentNode;
                        overlayNextSibling = overlayElement.nextSibling;
                        overlayParent.removeChild(overlayElement);
                    }

                    this._textValue = this._innerText(contentNode);

                     // restore the overlay
                    if (overlayElement) {
                        overlayParent.insertBefore(overlayElement, overlayNextSibling);
                    }
                } else {
                    this._textValue = "";
                }

                this._dirtyTextValue = false;
            }
            return this._textValue;
        },
        set: function (value) {
            if (this._textValue !== value || this._dirtyTextValue) {
                // Remove any overlay
                this.hideOverlay();

                this._textValue = value;
                this._dirtyTextValue = false;
                this._dirtyValue = true;
                this._needsAssingValue = true;
                this.needsDraw = true;
            }
            this._needsOriginalContent = false;
        }
    },

    /**
      Gets or sets the editor's delegate object that can define one or more delegate methods that a consumer can implement. For a list of delegate methods, see [Delegate methods]{@link  http://tetsubo.org/docs/montage/using-the-rich…itor-component#Delegate_methods}.
     @type {object}
    */
    delegate: {
        enumerable: true,
        value: null
    },

    /**
    The role of the sanitizer is to cleanup any data before its inserted, or extracted, from the editor. The default sanitizer removes any JavaScript, and scopes any CSS before injecting any data into the editor. However, JavaScript is not removed when the initial value is set using <code>editor.value</code>.
     @type {object}
    */
    sanitizer: {
        enumerable: false,
        get: function() {
            return  this._sanitizer;
        },
        set: function(value) {
            this._sanitizer = value;
        }
    },

    /**
      An array of overlay objects available to the editor. Overlays are UI components that are displayed on top of the editor based on the context.
     @type {array}
    */
    overlays: {
        enumerable: false,
        get: function() {
            return  this._overlays;
        },
        set: function(value) {
            this.hideOverlay();
            if (value instanceof Array) {
                this._overlays = value;
                this._callOverlays("initWithEditor", this, true);
            } else {
                this._overlays = null;
            }
        }
    },

    /**
      Returns the overlay currently being displayed.
     @type {object}
    */
    activeOverlay: {
        get: function() {
            return this._activeOverlay;
        }
    },

    /**
      Displays the specified overlay.
     @function
     @param {object} overlay The overlay to display.
    */
    showOverlay: {
        value: function(overlay) {
            var slot = this._overlaySlot,
                slotElem = slot ? slot.element : null;

            if (slotElem) {
                this._activeOverlay = overlay;
                this._innerElement.appendChild(slotElem.parentNode ? slotElem.parentNode.removeChild(slotElem) : slotElem);
                slot.attachToParentComponent();
                slot.content = overlay;
            }
        }
    },

    /**
     Hides the active overlay.
     @function
    */
    hideOverlay: {
        value: function(a) {
            var slot = this._overlaySlot,
                slotElem = slot ? slot.element : null;

            if (slotElem) {
                if (slotElem.parentNode) {
                    slotElem.parentNode.removeChild(slotElem);
                }
                this._activeOverlay = null;
                slot.content = null;
            }
        }
    },


    // Edit Actions & Properties

    /**
      Returns <code>true</code> if the current text selection is bold. If the selected text contains some text in bold and some not, the return value depends on the browser’s implementation. When set to <code>true</code>, adds the bold attribute to the selected text; when set to <code>false</code>, removes the bold attribute from the selected text.
     @type {boolean}
    */
    bold: {
        enumerable: true,
        get: function() { return this._genericCommandGetter("bold", "bold"); },
        set: function(value) { this._genericCommandSetter("bold", "bold", value); }
    },

    /**
      Returns <code>true</code> if the current text selection is underlined. If the selected text contains some text in underline and some not, the return value depends on the browser’s implementation. When set to <code>true</code>, adds the underline attribute to the selected text; when set to <code>false</code>, removes the underline attribute from the selected text.
    @type boolean
    */
    underline: {
        enumerable: true,
        get: function() { return this._genericCommandGetter("underline", "underline"); },
        set: function(value) { this._genericCommandSetter("underline", "underline", value); }
    },

    /**
    Returns <code>true</code> if the current text selection is italicized. If the selected text contains some text in italics and some not, the return value depends on the browser’s implementation. When set to <code>true</code>, adds the italic attribute to the selected text; when set to <code>false</code>, removes the italic attribute from the selected text.
    @type boolean
    */
    italic: {
        enumerable: true,
        get: function() { return this._genericCommandGetter("italic", "italic"); },
        set: function(value) { this._genericCommandSetter("italic", "italic", value); }
    },

    /**
    Returns <code>true</code> if the current text selection has the strikethrough style applied. If the selected text contains some text with strikethrough and some not, the return value depends on the browser’s implementation. When set to <code>true</code>, adds the italic attribute to the selected text; when set to <code>false</code>, removes the italic attribute from the selected text.
    @type boolean
    */

    strikeThrough: {
        enumerable: false,
        get: function() { return this._genericCommandGetter("strikeThrough", "strikethrough"); },
        set: function(value) { this._genericCommandSetter("strikeThrough", "strikethrough", value); }
    },

    /**
    Gets and sets the baseline shift for the currently selected text. Valid values are "baseline", "subscript", or "superscript".
     @type {string}
     @default "baseline"
    */
    baselineShift: {
        enumerable: true,
        get: function() {
            this._baselineShift = this._baselineShiftGetState();
            return this._baselineShift;
        },
        set: function(value) {
            var state = this._baselineShiftGetState();

            if (state != value) {
                if (value == "baseline") {
                    if (state == "subscript") {
                        this.doAction("subscript");
                    } else if (state == "superscript") {
                        this.doAction("superscript");
                    }
                } else if (value == "subscript") {
                    this.doAction("subscript");
                } else if (value == "superscript") {
                    this.doAction("superscript");
                }
            }
        }
    },

    /**
    Indent the selected text. If the selected text is inside a list, calling this method moves the selection into a sub-list.
    @function
    */
    indent: {
        enumerable: true,
        value: function() { this.doAction("indent"); }
    },

    /**
    Indent the selected text. If the selected text is inside a list, calling this method moves the selection either out of the list, or into the parent list.
    @function
    */
    outdent: {
        enumerable: true,
        value: function() { this.doAction("outdent"); }
    },

    /**
      Gets and sets the list style for the selected text. Valid values are "none", "unordered", "ordered". This property can be used in combination with the [indent]{@link indent} and [outdent]{@link outdent} methods to create a list hierarchy.
     @type {string}
    */
    listStyle: {
        enumerable: true,
        get: function() {
            this._liststyle = this._listStyleGetState();
            return this._liststyle;
        },
        set: function(value) {
            var state = this._listStyleGetState();

            if (state != value) {
                if (value == "none") {
                    this.doAction(state == "ordered" ? "insertorderedlist" : "insertunorderedlist");
                } else if (value == "ordered") {
                    this.doAction("insertorderedlist");
                } else if (value == "unordered") {
                    this.doAction("insertunorderedlist");
                }
            }
        }
    },

    /**
        Gets and sets the justification on the selected text. Valid values are "left", "center", "right", and "full". If the current selection is across multiple lines with different justifications, the value of this property depends of the browser’s implementation.
        @type {string}
    */
    justify: {
        enumerable: true,
        get: function() {
            this._justify = this._justifyGetState();
            return this._justify;
        },
        set: function(value) {
            var state = this._justifyGetState();
            if (state != value && ["left", "center", "right", "full"].indexOf(value) !== -1) {
                this.doAction("justify" + value);
            }
        }
    },

    /**
      Gets and sets the font name for the currently selected text as a CSS font-family. Can be set to any valid CSS font-family value, including multiple values. If the current selection is across multiple font-family elements, the specific return value depends of the browser’s implementation.
     @type {string}
    */
    fontName: {
        enumerable: true,
        get: function() {
            this._fontName = this._fontNameGetState();
            return this._fontName;
        },
        set: function(value) { this._genericCommandSetter("fontName", "fontname", value); }
    },

    /**
      Gets and sets the font size for the current text selection. Only HTML font size values 1 through 7 are supported. If the current selection is a mix of font size, the return value depends of the browser’s implementation.
     @type {string}
    */
    fontSize: {
        enumerable: true,
        get: function() { return this._genericCommandGetter("fontSize", "fontsize"); },
        set: function(value) { this._genericCommandSetter("fontSize", "fontsize", value); }
    },

    /**
      Gets and sets the background color of the currently selected text. This property can be set to any valid CSS color value; however, the color is always returned as an RGB color. If the current selection spans across elements with different background colors, the return value depends on the browser’s implementation.
     @type {string}
    */
    backColor: {
        enumerable: true,
        get: function() { return this._genericCommandGetter("backColor", "backcolor"); },
        set: function(value) { this._genericCommandSetter("backColor", "backcolor", value === null ? "inherit" : value); }
    },

    /**
      Gets and sets the background color of the currently selected text. This property can be set to any valid CSS color value; however, the color is always returned as an RGB color. If the current selection spans across elements with different background colors, the return value depends on the browser’s implementation. To remove a background color, set it to <code>null</code>.
     @type {string}
    */
    foreColor: {
        enumerable: true,
        get: function() { return this._genericCommandGetter("foreColor", "forecolor"); },
        set: function(value) {  this._genericCommandSetter("foreColor", "forecolor", value === null ? "inherit" : value); }
    },

    /**
      Selects the all the content contained by the editor. Depending on the browser's implementation, some of the outer elements without direct text nodes won't be selected. Consequently, if the user presses the delete key after all the text is selected with this method, selecting all, some markup might still be there, you will have to Select all again to get rid of it.
      @function
    */
    selectAll: {
        enumerable: true,
        value: function() { this.doAction("selectall"); }
    },

    /**
      Selects the specified DOM element.
     @function
     @param {element} element The element to select.
    */
    selectElement: {
        enumerable: true,
        value: function(element) {
            var offset,
                range;

            offset = this._nodeOffset(element);
            if (offset !== -1) {
                range = document.createRange();
                range.setStart(element.parentNode, offset);
                range.setEnd(element.parentNode, offset + 1);
                this._selectedRange = range;
            }
        }
    },

    /**
    Gets and sets the Montage undo manager for the editor. By default, it's assigned an instance of the default Montage UndoManager. The component also works with the native Undo Manager provided by the browser. To use the native undo manager, set this property to <code>null</code>
    @type {object}
    */
    undoManager: {
        enumerable: true,
        get: function() { return this._undoManager },
        set: function(value) { this._undoManager = value }
    },

    /**
    Undo the last editing operation.
    @function
    */
    undo: {
        enumerable: true,
        value: function() {
            if (this.undoManager) {
                this.undoManager.undo();
            } else {
                this._undo();
            }
        }
    },

    /**
    Redo the last editing operation that was canceled by calling <code>undo()</code>.
    @function
    */
    redo: {
        enumerable: true,
        value: function() {
            if (this.undoManager) {
                this.undoManager.redo();
            } else {
                this._redo();
            }
        }
    },

    /**
    Equivalent to the native <code><a href="https://developer.mozilla.org/en/Rich-Text_Editing_in_Mozilla#Executing%5FCommands" target="_blank">document.execCommand</a></code> method, it also sets the focus on the editor before executing the command, marks the editor’s content as dirty, and add the command to the Montage Undo Manager stack using the label provided.

    You should only use this method if you are extending the editor’s functionality, or writing your own overlay. The typical usage would be to insert HTML via the <code>insertHTML</code> command. All other <code>execCommand</code> commands are exposed as bindable properties on the editor, like <code>bold</code> or <code>italic</code>, and those puse the editor property instead.
    @function
    @param {string} command The command to execute.
    @param {boolean} showUI Specifies whether the default user interface should be drawn.
    @param {string|number} value The value to pass as an argument to the command. Possible values depend on the command.
    @param {string} label The label to use when adding this command to the undo stack managed by the Montage UndoManager.
    */
    execCommand: {
        enumerable: false,
        value: function(command, showUI, value, label) {
            var savedActiveElement = document.activeElement,
                editorElement = this._innerElement,
                retValue = false;

            if (!editorElement) {
                return false;
            }

            // Make sure we are the active element before calling execCommand
            if (editorElement != savedActiveElement) {
                editorElement.focus();
            }

            if (value === undefined) {
                value = false;
            }

            label = label || this._execCommandLabel[command] || "Typing";

            this._executingCommand = true;
            if (document.execCommand(command, showUI, value)) {
                this._executingCommand = false;
                if (["selectall"].indexOf(command) == -1) {
                    if (this.undoManager ) {
                        this._stopTyping();
                        this.undoManager.add(label, this._undo, this, label, this._innerElement);
                    }
                } else {
                    this.markDirty();
                }

                this.handleSelectionchange();
                retValue = true;
            } else {
                this._executingCommand = false;
            }

            // Reset the focus
            if (editorElement != savedActiveElement) {
                savedActiveElement.focus();
            }

            return retValue;
        }
    },

    /**
    Marks the editor content as dirty, causing the editor to generate an <code>editorChange</code> event, and update the editor's <code>value</code> and <code>textValue</code> properties. This method should only be called if you are extending the editor or writing an overlay.
    @private
    @function
    */
    markDirty: {
        enumerable: false,
        value: function() {
            var thisRef = this,
                prevValue;
                var updateValues = function() {
                    clearTimeout(thisRef._forceUpdateValuesTimeout);
                    delete thisRef._forceUpdateValuesTimeout;
                    clearTimeout(thisRef._updateValuesTimeout);
                    delete thisRef._updateValuesTimeout;

                    if (defaultEventManager.registeredEventListenersForEventType_onTarget_("change@value", this)) {
                        prevValue = thisRef._value;
                        if (thisRef.value !== prevValue) {
                            thisRef.dispatchEvent(MutableEvent.changeEventForKeyAndValue("value" , prevValue).withPlusValue(thisRef.value));
                        }
                    }
                    if (defaultEventManager.registeredEventListenersForEventType_onTarget_("change@textValue", this)) {
                        prevValue = thisRef._textValue;
                        if (thisRef.textValue !== prevValue) {
                            thisRef.dispatchEvent(MutableEvent.changeEventForKeyAndValue("textValue" , prevValue).withPlusValue(thisRef.textValue));
                        }
                    }
                    thisRef._dispatchEditorEvent("editorChange");
                };

            if (!this._needsAssingValue) {
                // Clear the cached value
                this._dirtyValue = true;
                this._dirtyTextValue = true;
            }

            if (!this._forceUpdateValuesTimeout) {
                this._forceUpdateValuesTimeout = setTimeout(updateValues, 1000);
            }
            if (this._updateValuesTimeout) {
                clearTimeout(this._updateValuesTimeout);
            }
            this._updateValuesTimeout = setTimeout(updateValues, 100);
        }
    }

});