NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / editor-base / editor-base.js
blob00b3453322a75ab9629be54008c6f437a89aea3d
1 /*
2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
8 YUI.add('editor-base', function (Y, NAME) {
11     /**
12      * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
13      *
14      *      var editor = new Y.EditorBase({
15      *          content: 'Foo'
16      *      });
17      *      editor.render('#demo');
18      *
19      * @class EditorBase
20      * @extends Base
21      * @module editor
22      * @main editor
23      * @submodule editor-base
24      * @constructor
25      */
27     var Lang = Y.Lang,
29     EditorBase = function() {
30         EditorBase.superclass.constructor.apply(this, arguments);
31     }, LAST_CHILD = ':last-child';
33     Y.extend(EditorBase, Y.Base, {
34         /**
35         * Internal reference to the Y.ContentEditable instance
36         * @property frame
37         */
38         frame: null,
40         initializer: function() {
41             this.publish('nodeChange', {
42                 emitFacade: true,
43                 bubbles: true,
44                 defaultFn: this._defNodeChangeFn
45             });
47             //this.plug(Y.Plugin.EditorPara);
48         },
49         destructor: function() {
50             this.detachAll();
51         },
52         /**
53         * Copy certain styles from one node instance to another (used for new paragraph creation mainly)
54         * @method copyStyles
55         * @param {Node} from The Node instance to copy the styles from
56         * @param {Node} to The Node instance to copy the styles to
57         */
58         copyStyles: function(from, to) {
59             if (from.test('a')) {
60                 //Don't carry the A styles
61                 return;
62             }
63             var styles = ['color', 'fontSize', 'fontFamily', 'backgroundColor', 'fontStyle' ],
64                 newStyles = {};
66             Y.each(styles, function(v) {
67                 newStyles[v] = from.getStyle(v);
68             });
69             if (from.ancestor('b,strong')) {
70                 newStyles.fontWeight = 'bold';
71             }
72             if (from.ancestor('u')) {
73                 if (!newStyles.textDecoration) {
74                     newStyles.textDecoration = 'underline';
75                 }
76             }
77             to.setStyles(newStyles);
78         },
79         /**
80         * Holder for the selection bookmark in IE.
81         * @property _lastBookmark
82         * @private
83         */
84         _lastBookmark: null,
85         /**
86         * Resolves the e.changedNode in the nodeChange event if it comes from the document. If
87         * the event came from the document, it will get the last child of the last child of the document
88         * and return that instead.
89         * @method _resolveChangedNode
90         * @param {Node} n The node to resolve
91         * @private
92         */
93         _resolveChangedNode: function(n) {
94             var inst = this.getInstance(), lc, lc2, found, root = this._getRoot(), sel;
96             if (n && n.compareTo(root)) {
97                 sel = new inst.EditorSelection();
98                 if (sel && sel.anchorNode) {
99                     n = sel.anchorNode;
100                 }
101             }
102             if (inst && n && n.test('html')) {
103                 lc = root.one(LAST_CHILD);
104                 while (!found) {
105                     if (lc) {
106                         lc2 = lc.one(LAST_CHILD);
107                         if (lc2) {
108                             lc = lc2;
109                         } else {
110                             found = true;
111                         }
112                     } else {
113                         found = true;
114                     }
115                 }
116                 if (lc) {
117                     if (lc.test('br')) {
118                         if (lc.previous()) {
119                             lc = lc.previous();
120                         } else {
121                             lc = lc.get('parentNode');
122                         }
123                     }
124                     if (lc) {
125                         n = lc;
126                     }
127                 }
128             }
129             if (!n) {
130                 //Fallback to make sure a node is attached to the event
131                 n = root;
132             }
133             return n;
134         },
135         /**
136         * Resolves the ROOT editor element.
137         * @method _getRoot
138         * @private
139         */
140         _getRoot: function() {
141             return this.getInstance().EditorSelection.ROOT;
142         },
143         /**
144         * The default handler for the nodeChange event.
145         * @method _defNodeChangeFn
146         * @param {Event} e The event
147         * @private
148         */
149         _defNodeChangeFn: function(e) {
150             var startTime = (new Date()).getTime(),
151                 inst = this.getInstance(), sel,
152                 changed, endTime,
153                 cmds = {}, family, fsize, classes = [],
154                 fColor = '', bColor = '', bq,
155                 normal = false,
156                 root = this._getRoot();
158             if (Y.UA.ie) {
159                 try {
160                     sel = inst.config.doc.selection.createRange();
161                     if (sel.getBookmark) {
162                         this._lastBookmark = sel.getBookmark();
163                     }
164                 } catch (ie) {}
165             }
167             e.changedNode = this._resolveChangedNode(e.changedNode);
170             /*
171             * @TODO
172             * This whole method needs to be fixed and made more dynamic.
173             * Maybe static functions for the e.changeType and an object bag
174             * to walk through and filter to pass off the event to before firing..
175             */
177             switch (e.changedType) {
178                 case 'tab':
179                     if (!e.changedNode.test('li, li *') && !e.changedEvent.shiftKey) {
180                         e.changedEvent.frameEvent.preventDefault();
181                         if (Y.UA.webkit) {
182                             this.execCommand('inserttext', '\t');
183                         } else if (Y.UA.gecko) {
184                             this.frame.exec._command('inserthtml', EditorBase.TABKEY);
185                         } else if (Y.UA.ie) {
186                             this.execCommand('inserthtml', EditorBase.TABKEY);
187                         }
188                     }
189                     break;
190                 case 'backspace-up':
191                     // Fixes #2531090 - Joins text node strings so they become one for bidi
192                     if (Y.UA.webkit && e.changedNode) {
193                         e.changedNode.set('innerHTML', e.changedNode.get('innerHTML'));
194                     }
195                     break;
196             }
197             if (Y.UA.webkit && e.commands && (e.commands.indent || e.commands.outdent)) {
198                 /*
199                 * When executing execCommand 'indent or 'outdent' Webkit applies
200                 * a class to the BLOCKQUOTE that adds left/right margin to it
201                 * This strips that style so it is just a normal BLOCKQUOTE
202                 */
203                 bq = root.all('.webkit-indent-blockquote, blockquote');
204                 if (bq.size()) {
205                     bq.setStyle('margin', '');
206                 }
207             }
209             changed = this.getDomPath(e.changedNode, false);
211             if (e.commands) {
212                 cmds = e.commands;
213             }
216             Y.each(changed, function(el) {
217                 var tag = el.tagName.toLowerCase(),
218                     cmd = EditorBase.TAG2CMD[tag], s,
219                     n, family2, cls, bColor2;
221                 if (cmd) {
222                     cmds[cmd] = 1;
223                 }
225                 //Bold and Italic styles
226                 s = el.currentStyle || el.style;
228                 if ((''+s.fontWeight) === 'normal') {
229                     normal = true;
230                 }
231                 if ((''+s.fontWeight) === 'bold') { //Cast this to a string
232                     cmds.bold = 1;
233                 }
234                 if (Y.UA.ie) {
235                     if (s.fontWeight > 400) {
236                         cmds.bold = 1;
237                     }
238                 }
239                 if (s.fontStyle === 'italic') {
240                     cmds.italic = 1;
241                 }
243                 if (s.textDecoration.indexOf('underline') > -1) {
244                     cmds.underline = 1;
245                 }
246                 if (s.textDecoration.indexOf('line-through') > -1) {
247                     cmds.strikethrough = 1;
248                 }
250                 n = inst.one(el);
251                 if (n.getStyle('fontFamily')) {
252                     family2 = n.getStyle('fontFamily').split(',')[0].toLowerCase();
253                     if (family2) {
254                         family = family2;
255                     }
256                     if (family) {
257                         family = family.replace(/'/g, '').replace(/"/g, '');
258                     }
259                 }
261                 fsize = EditorBase.NORMALIZE_FONTSIZE(n);
264                 cls = el.className.split(' ');
265                 Y.each(cls, function(v) {
266                     if (v !== '' && (v.substr(0, 4) !== 'yui_')) {
267                         classes.push(v);
268                     }
269                 });
271                 fColor = EditorBase.FILTER_RGB(n.getStyle('color'));
272                 bColor2 = EditorBase.FILTER_RGB(s.backgroundColor);
273                 if (bColor2 !== 'transparent') {
274                     if (bColor2 !== '') {
275                         bColor = bColor2;
276                     }
277                 }
279             });
281             if (normal) {
282                 delete cmds.bold;
283                 delete cmds.italic;
284             }
286             e.dompath = inst.all(changed);
287             e.classNames = classes;
288             e.commands = cmds;
290             //TODO Dont' like this, not dynamic enough..
291             if (!e.fontFamily) {
292                 e.fontFamily = family;
293             }
294             if (!e.fontSize) {
295                 e.fontSize = fsize;
296             }
297             if (!e.fontColor) {
298                 e.fontColor = fColor;
299             }
300             if (!e.backgroundColor) {
301                 e.backgroundColor = bColor;
302             }
304             endTime = (new Date()).getTime();
305         },
306         /**
307         * Walk the dom tree from this node up to body, returning a reversed array of parents.
308         * @method getDomPath
309         * @param {Node} node The Node to start from
310         */
311         getDomPath: function(node, nodeList) {
312             var domPath = [], domNode, rootNode,
313                 root = this._getRoot(),
314                 inst = this.frame.getInstance();
316             domNode = inst.Node.getDOMNode(node);
317             rootNode = inst.Node.getDOMNode(root);
318             //return inst.all(domNode);
320             while (domNode !== null) {
322                 if ((domNode === inst.config.doc.documentElement) || (domNode === inst.config.doc) || !domNode.tagName) {
323                     domNode = null;
324                     break;
325                 }
327                 if (!inst.DOM.inDoc(domNode)) {
328                     domNode = null;
329                     break;
330                 }
332                 //Check to see if we get el.nodeName and nodeType
333                 if (domNode.nodeName && domNode.nodeType && (domNode.nodeType === 1)) {
334                     domPath.push(domNode);
335                 }
337                 if (domNode === rootNode) {
338                     domNode = null;
339                     break;
340                 }
342                 domNode = domNode.parentNode;
343             }
345             /*{{{ Using Node
346             while (node !== null) {
347                 if (node.test('html') || node.test('doc') || !node.get('tagName')) {
348                     node = null;
349                     break;
350                 }
351                 if (!node.inDoc()) {
352                     node = null;
353                     break;
354                 }
355                 //Check to see if we get el.nodeName and nodeType
356                 if (node.get('nodeName') && node.get('nodeType') && (node.get('nodeType') == 1)) {
357                     domPath.push(inst.Node.getDOMNode(node));
358                 }
360                 if (node.test('body')) {
361                     node = null;
362                     break;
363                 }
365                 node = node.get('parentNode');
366             }
367             }}}*/
369             if (domPath.length === 0) {
370                 domPath[0] = inst.config.doc.body;
371             }
373             if (nodeList) {
374                 return inst.all(domPath.reverse());
375             } else {
376                 return domPath.reverse();
377             }
379         },
380         /**
381         * After frame ready, bind mousedown & keyup listeners
382         * @method _afterFrameReady
383         * @private
384         */
385         _afterFrameReady: function() {
386             var inst = this.frame.getInstance();
388             this.frame.on('dom:mouseup', Y.bind(this._onFrameMouseUp, this));
389             this.frame.on('dom:mousedown', Y.bind(this._onFrameMouseDown, this));
390             this.frame.on('dom:keydown', Y.bind(this._onFrameKeyDown, this));
392             if (Y.UA.ie) {
393                 this.frame.on('dom:activate', Y.bind(this._onFrameActivate, this));
394                 this.frame.on('dom:beforedeactivate', Y.bind(this._beforeFrameDeactivate, this));
395             }
396             this.frame.on('dom:keyup', Y.bind(this._onFrameKeyUp, this));
397             this.frame.on('dom:keypress', Y.bind(this._onFrameKeyPress, this));
398             this.frame.on('dom:paste', Y.bind(this._onPaste, this));
400             inst.EditorSelection.filter();
401             this.fire('ready');
402         },
403         /**
404         * Caches the current cursor position in IE.
405         * @method _beforeFrameDeactivate
406         * @private
407         */
408         _beforeFrameDeactivate: function(e) {
409             if (e.frameTarget.test('html')) { //Means it came from a scrollbar
410                 return;
411             }
412             var inst = this.getInstance(),
413                 sel = inst.config.doc.selection.createRange();
415             if (sel.compareEndPoints && !sel.compareEndPoints('StartToEnd', sel)) {
416                 sel.pasteHTML('<var id="yui-ie-cursor">');
417             }
418         },
419         /**
420         * Moves the cached selection bookmark back so IE can place the cursor in the right place.
421         * @method _onFrameActivate
422         * @private
423         */
424         _onFrameActivate: function(e) {
425             if (e.frameTarget.test('html')) { //Means it came from a scrollbar
426                 return;
427             }
428             var inst = this.getInstance(),
429                 sel = new inst.EditorSelection(),
430                 range = sel.createRange(),
431                 root = this._getRoot(),
432                 cur = root.all('#yui-ie-cursor');
434             if (cur.size()) {
435                 cur.each(function(n) {
436                     n.set('id', '');
437                     if (range.moveToElementText) {
438                         try {
439                             range.moveToElementText(n._node);
440                             var moved = range.move('character', -1);
441                             if (moved === -1) { //Only move up if we actually moved back.
442                                 range.move('character', 1);
443                             }
444                             range.select();
445                             range.text = '';
446                         } catch (e) {}
447                     }
448                     n.remove();
449                 });
450             }
451         },
452         /**
453         * Fires nodeChange event
454         * @method _onPaste
455         * @private
456         */
457         _onPaste: function(e) {
458             this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'paste', changedEvent: e.frameEvent });
459         },
460         /**
461         * Fires nodeChange event
462         * @method _onFrameMouseUp
463         * @private
464         */
465         _onFrameMouseUp: function(e) {
466             this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mouseup', changedEvent: e.frameEvent  });
467         },
468         /**
469         * Fires nodeChange event
470         * @method _onFrameMouseDown
471         * @private
472         */
473         _onFrameMouseDown: function(e) {
474             this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mousedown', changedEvent: e.frameEvent  });
475         },
476         /**
477         * Caches a copy of the selection for key events. Only creating the selection on keydown
478         * @property _currentSelection
479         * @private
480         */
481         _currentSelection: null,
482         /**
483         * Holds the timer for selection clearing
484         * @property _currentSelectionTimer
485         * @private
486         */
487         _currentSelectionTimer: null,
488         /**
489         * Flag to determine if we can clear the selection or not.
490         * @property _currentSelectionClear
491         * @private
492         */
493         _currentSelectionClear: null,
494         /**
495         * Fires nodeChange event
496         * @method _onFrameKeyDown
497         * @private
498         */
499         _onFrameKeyDown: function(e) {
500             var inst, sel;
501             if (!this._currentSelection) {
502                 if (this._currentSelectionTimer) {
503                     this._currentSelectionTimer.cancel();
504                 }
505                 this._currentSelectionTimer = Y.later(850, this, function() {
506                     this._currentSelectionClear = true;
507                 });
509                 inst = this.frame.getInstance();
510                 sel = new inst.EditorSelection(e);
512                 this._currentSelection = sel;
513             } else {
514                 sel = this._currentSelection;
515             }
517             inst = this.frame.getInstance();
518             sel = new inst.EditorSelection();
520             this._currentSelection = sel;
522             if (sel && sel.anchorNode) {
523                 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keydown', changedEvent: e.frameEvent });
524                 if (EditorBase.NC_KEYS[e.keyCode]) {
525                     this.fire('nodeChange', {
526                         changedNode: sel.anchorNode,
527                         changedType: EditorBase.NC_KEYS[e.keyCode],
528                         changedEvent: e.frameEvent
529                     });
530                     this.fire('nodeChange', {
531                         changedNode: sel.anchorNode,
532                         changedType: EditorBase.NC_KEYS[e.keyCode] + '-down',
533                         changedEvent: e.frameEvent
534                     });
535                 }
536             }
537         },
538         /**
539         * Fires nodeChange event
540         * @method _onFrameKeyPress
541         * @private
542         */
543         _onFrameKeyPress: function(e) {
544             var sel = this._currentSelection;
546             if (sel && sel.anchorNode) {
547                 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keypress', changedEvent: e.frameEvent });
548                 if (EditorBase.NC_KEYS[e.keyCode]) {
549                     this.fire('nodeChange', {
550                         changedNode: sel.anchorNode,
551                         changedType: EditorBase.NC_KEYS[e.keyCode] + '-press',
552                         changedEvent: e.frameEvent
553                     });
554                 }
555             }
556         },
557         /**
558         * Fires nodeChange event for keyup on specific keys
559         * @method _onFrameKeyUp
560         * @private
561         */
562         _onFrameKeyUp: function(e) {
563             var inst = this.frame.getInstance(),
564                 sel = new inst.EditorSelection(e);
566             if (sel && sel.anchorNode) {
567                 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keyup', selection: sel, changedEvent: e.frameEvent  });
568                 if (EditorBase.NC_KEYS[e.keyCode]) {
569                     this.fire('nodeChange', {
570                         changedNode: sel.anchorNode,
571                         changedType: EditorBase.NC_KEYS[e.keyCode] + '-up',
572                         selection: sel,
573                         changedEvent: e.frameEvent
574                     });
575                 }
576             }
577             if (this._currentSelectionClear) {
578                 this._currentSelectionClear = this._currentSelection = null;
579             }
580         },
581         /**
582         * Validates linkedcss property
583         *
584         * @method _validateLinkedCSS
585         * @private
586         */
587         _validateLinkedCSS: function(value) {
588             return Lang.isString(value) || Lang.isArray(value);
589         },
590         /**
591         * Pass through to the frame.execCommand method
592         * @method execCommand
593         * @param {String} cmd The command to pass: inserthtml, insertimage, bold
594         * @param {String} val The optional value of the command: Helvetica
595         * @return {Node/NodeList} The Node or Nodelist affected by the command. Only returns on override commands, not browser defined commands.
596         */
597         execCommand: function(cmd, val) {
598             var ret = this.frame.execCommand(cmd, val),
599                 inst = this.frame.getInstance(),
600                 sel = new inst.EditorSelection(), cmds = {},
601                 e = { changedNode: sel.anchorNode, changedType: 'execcommand', nodes: ret };
603             switch (cmd) {
604                 case 'forecolor':
605                     e.fontColor = val;
606                     break;
607                 case 'backcolor':
608                     e.backgroundColor = val;
609                     break;
610                 case 'fontsize':
611                     e.fontSize = val;
612                     break;
613                 case 'fontname':
614                     e.fontFamily = val;
615                     break;
616             }
618             cmds[cmd] = 1;
619             e.commands = cmds;
621             this.fire('nodeChange', e);
623             return ret;
624         },
625         /**
626         * Get the YUI instance of the frame
627         * @method getInstance
628         * @return {YUI} The YUI instance bound to the frame.
629         */
630         getInstance: function() {
631             return this.frame.getInstance();
632         },
633         /**
634         * Renders the Y.ContentEditable to the passed node.
635         * @method render
636         * @param {Selector/HTMLElement/Node} node The node to append the Editor to
637         * @return {EditorBase}
638         * @chainable
639         */
640         render: function(node) {
641             var frame = this.frame;
643             if (!frame) {
644                 this.plug(Y.Plugin.Frame, {
645                     designMode: true,
646                     title: EditorBase.STRINGS.title,
647                     use: EditorBase.USE,
648                     dir: this.get('dir'),
649                     extracss: this.get('extracss'),
650                     linkedcss: this.get('linkedcss'),
651                     defaultblock: this.get('defaultblock')
652                 });
654                 frame = this.frame;
655             }
657             if (!frame.hasPlugin('exec')) {
658                 frame.plug(Y.Plugin.ExecCommand);
659             }
661             frame.after('ready', Y.bind(this._afterFrameReady, this));
663             frame.addTarget(this);
665             frame.set('content', this.get('content'));
667             frame.render(node);
669             return this;
670         },
671         /**
672         * Focus the contentWindow of the iframe
673         * @method focus
674         * @param {Function} fn Callback function to execute after focus happens
675         * @return {EditorBase}
676         * @chainable
677         */
678         focus: function(fn) {
679             this.frame.focus(fn);
680             return this;
681         },
682         /**
683         * Handles the showing of the Editor instance. Currently only handles the iframe
684         * @method show
685         * @return {EditorBase}
686         * @chainable
687         */
688         show: function() {
689             this.frame.show();
690             return this;
691         },
692         /**
693         * Handles the hiding of the Editor instance. Currently only handles the iframe
694         * @method hide
695         * @return {EditorBase}
696         * @chainable
697         */
698         hide: function() {
699             this.frame.hide();
700             return this;
701         },
702         /**
703         * (Un)Filters the content of the Editor, cleaning YUI related code. //TODO better filtering
704         * @method getContent
705         * @return {String} The filtered content of the Editor
706         */
707         getContent: function() {
708             var html = '', inst = this.getInstance();
709             if (inst && inst.EditorSelection) {
710                 html = inst.EditorSelection.unfilter();
711             }
712             //Removing the _yuid from the objects in IE
713             html = html.replace(/ _yuid="([^>]*)"/g, '');
714             return html;
715         }
716     }, {
717         /**
718         * @static
719         * @method NORMALIZE_FONTSIZE
720         * @description Pulls the fontSize from a node, then checks for string values (x-large, x-small)
721         * and converts them to pixel sizes. If the parsed size is different from the original, it calls
722         * node.setStyle to update the node with a pixel size for normalization.
723         */
724         NORMALIZE_FONTSIZE: function(n) {
725             var size = n.getStyle('fontSize'), oSize = size;
727             switch (size) {
728                 case '-webkit-xxx-large':
729                     size = '48px';
730                     break;
731                 case 'xx-large':
732                     size = '32px';
733                     break;
734                 case 'x-large':
735                     size = '24px';
736                     break;
737                 case 'large':
738                     size = '18px';
739                     break;
740                 case 'medium':
741                     size = '16px';
742                     break;
743                 case 'small':
744                     size = '13px';
745                     break;
746                 case 'x-small':
747                     size = '10px';
748                     break;
749             }
750             if (oSize !== size) {
751                 n.setStyle('fontSize', size);
752             }
753             return size;
754         },
755         /**
756         * @static
757         * @property TABKEY
758         * @description The HTML markup to use for the tabkey
759         */
760         TABKEY: '<span class="tab">&nbsp;&nbsp;&nbsp;&nbsp;</span>',
761         /**
762         * @static
763         * @method FILTER_RGB
764         * @param String css The CSS string containing rgb(#,#,#);
765         * @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00
766         * @return String
767         */
768         FILTER_RGB: function(css) {
769             if (css.toLowerCase().indexOf('rgb') !== -1) {
770                 var exp = new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)", "gi"),
771                     rgb = css.replace(exp, "$1,$2,$3,$4,$5").split(','),
772                     r, g, b;
774                 if (rgb.length === 5) {
775                     r = parseInt(rgb[1], 10).toString(16);
776                     g = parseInt(rgb[2], 10).toString(16);
777                     b = parseInt(rgb[3], 10).toString(16);
779                     r = r.length === 1 ? '0' + r : r;
780                     g = g.length === 1 ? '0' + g : g;
781                     b = b.length === 1 ? '0' + b : b;
783                     css = "#" + r + g + b;
784                 }
785             }
786             return css;
787         },
788         /**
789         * @static
790         * @property TAG2CMD
791         * @description A hash table of tags to their execcomand's
792         */
793         TAG2CMD: {
794             'b': 'bold',
795             'strong': 'bold',
796             'i': 'italic',
797             'em': 'italic',
798             'u': 'underline',
799             'sup': 'superscript',
800             'sub': 'subscript',
801             'img': 'insertimage',
802             'a' : 'createlink',
803             'ul' : 'insertunorderedlist',
804             'ol' : 'insertorderedlist'
805         },
806         /**
807         * Hash table of keys to fire a nodeChange event for.
808         * @static
809         * @property NC_KEYS
810         * @type Object
811         */
812         NC_KEYS: {
813             8: 'backspace',
814             9: 'tab',
815             13: 'enter',
816             32: 'space',
817             33: 'pageup',
818             34: 'pagedown',
819             35: 'end',
820             36: 'home',
821             37: 'left',
822             38: 'up',
823             39: 'right',
824             40: 'down',
825             46: 'delete'
826         },
827         /**
828         * The default modules to use inside the Frame
829         * @static
830         * @property USE
831         * @type Array
832         */
833         USE: ['node', 'selector-css3', 'editor-selection', 'stylesheet'],
834         /**
835         * The Class Name: editorBase
836         * @static
837         * @property NAME
838         */
839         NAME: 'editorBase',
840         /**
841         * Editor Strings.  By default contains only the `title` property for the
842         * Title of frame document (default "Rich Text Editor").
843         *
844         * @static
845         * @property STRINGS
846         */
847         STRINGS: {
848             title: 'Rich Text Editor'
849         },
850         ATTRS: {
851             /**
852             * The content to load into the Editor Frame
853             * @attribute content
854             */
855             content: {
856                 validator: Lang.isString,
857                 value: '<br class="yui-cursor">',
858                 setter: function(str) {
859                     if (str.substr(0, 1) === "\n") {
860                         str = str.substr(1);
861                     }
862                     if (str === '') {
863                         str = '<br class="yui-cursor">';
864                     }
865                     if (str === ' ') {
866                         if (Y.UA.gecko) {
867                             str = '<br class="yui-cursor">';
868                         }
869                     }
870                     return this.frame.set('content', str);
871                 },
872                 getter: function() {
873                     return this.frame.get('content');
874                 }
875             },
876             /**
877             * The value of the dir attribute on the HTML element of the frame. Default: ltr
878             * @attribute dir
879             */
880             dir: {
881                 validator: Lang.isString,
882                 writeOnce: true,
883                 value: 'ltr'
884             },
885             /**
886             * @attribute linkedcss
887             * @description An array of url's to external linked style sheets
888             * @type String|Array
889             */
890             linkedcss: {
891                 validator: '_validateLinkedCSS',
892                 value: '',
893                 setter: function(css) {
894                     if (this.frame) {
895                         this.frame.set('linkedcss', css);
896                     }
897                     return css;
898                 }
899             },
900             /**
901             * @attribute extracss
902             * @description A string of CSS to add to the Head of the Editor
903             * @type String
904             */
905             extracss: {
906                 validator: Lang.isString,
907                 value: '',
908                 setter: function(css) {
909                     if (this.frame) {
910                         this.frame.set('extracss', css);
911                     }
912                     return css;
913                 }
914             },
915             /**
916             * @attribute defaultblock
917             * @description The default tag to use for block level items, defaults to: p
918             * @type String
919             */
920             defaultblock: {
921                 validator: Lang.isString,
922                 value: 'p'
923             }
924         }
925     });
927     Y.EditorBase = EditorBase;
929     /**
930     * @event nodeChange
931     * @description Fired from several mouse/key/paste event points.
932     * @param {Event.Facade} event An Event Facade object with the following specific properties added:
933     * <dl>
934     *   <dt>changedEvent</dt><dd>The event that caused the nodeChange</dd>
935     *   <dt>changedNode</dt><dd>The node that was interacted with</dd>
936     *   <dt>changedType</dt><dd>The type of change: mousedown, mouseup, right, left, backspace, tab, enter, etc..</dd>
937     *   <dt>commands</dt><dd>The list of execCommands that belong to this change and the dompath that's associated with the changedNode</dd>
938     *   <dt>classNames</dt><dd>An array of classNames that are applied to the changedNode and all of it's parents</dd>
939     *   <dt>dompath</dt><dd>A sorted array of node instances that make up the DOM path from the changedNode to body.</dd>
940     *   <dt>backgroundColor</dt><dd>The cascaded backgroundColor of the changedNode</dd>
941     *   <dt>fontColor</dt><dd>The cascaded fontColor of the changedNode</dd>
942     *   <dt>fontFamily</dt><dd>The cascaded fontFamily of the changedNode</dd>
943     *   <dt>fontSize</dt><dd>The cascaded fontSize of the changedNode</dd>
944     * </dl>
945     * @type {Event.Custom}
946     */
948     /**
949     * @event ready
950     * @description Fired after the frame is ready.
951     * @param {Event.Facade} event An Event Facade object.
952     * @type {Event.Custom}
953     */
959 }, '3.13.0', {"requires": ["base", "frame", "node", "exec-command", "editor-selection"]});