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/
8 YUI.add('editor-base', function (Y, NAME) {
12 * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
14 * var editor = new Y.EditorBase({
17 * editor.render('#demo');
23 * @submodule editor-base
29 EditorBase = function() {
30 EditorBase.superclass.constructor.apply(this, arguments);
31 }, LAST_CHILD = ':last-child';
33 Y.extend(EditorBase, Y.Base, {
35 * Internal reference to the Y.ContentEditable instance
40 initializer: function() {
41 this.publish('nodeChange', {
44 defaultFn: this._defNodeChangeFn
47 //this.plug(Y.Plugin.EditorPara);
49 destructor: function() {
53 * Copy certain styles from one node instance to another (used for new paragraph creation mainly)
55 * @param {Node} from The Node instance to copy the styles from
56 * @param {Node} to The Node instance to copy the styles to
58 copyStyles: function(from, to) {
60 //Don't carry the A styles
63 var styles = ['color', 'fontSize', 'fontFamily', 'backgroundColor', 'fontStyle' ],
66 Y.each(styles, function(v) {
67 newStyles[v] = from.getStyle(v);
69 if (from.ancestor('b,strong')) {
70 newStyles.fontWeight = 'bold';
72 if (from.ancestor('u')) {
73 if (!newStyles.textDecoration) {
74 newStyles.textDecoration = 'underline';
77 to.setStyles(newStyles);
80 * Holder for the selection bookmark in IE.
81 * @property _lastBookmark
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
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) {
102 if (inst && n && n.test('html')) {
103 lc = root.one(LAST_CHILD);
106 lc2 = lc.one(LAST_CHILD);
121 lc = lc.get('parentNode');
130 //Fallback to make sure a node is attached to the event
136 * Resolves the ROOT editor element.
140 _getRoot: function() {
141 return this.getInstance().EditorSelection.ROOT;
144 * The default handler for the nodeChange event.
145 * @method _defNodeChangeFn
146 * @param {Event} e The event
149 _defNodeChangeFn: function(e) {
150 var startTime = (new Date()).getTime(),
151 inst = this.getInstance(), sel,
153 cmds = {}, family, fsize, classes = [],
154 fColor = '', bColor = '', bq,
156 root = this._getRoot();
160 sel = inst.config.doc.selection.createRange();
161 if (sel.getBookmark) {
162 this._lastBookmark = sel.getBookmark();
167 e.changedNode = this._resolveChangedNode(e.changedNode);
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..
177 switch (e.changedType) {
179 if (!e.changedNode.test('li, li *') && !e.changedEvent.shiftKey) {
180 e.changedEvent.frameEvent.preventDefault();
181 Y.log('Overriding TAB key to insert HTML: HALTING', 'info', 'editor');
183 this.execCommand('inserttext', '\t');
184 } else if (Y.UA.gecko) {
185 this.frame.exec._command('inserthtml', EditorBase.TABKEY);
186 } else if (Y.UA.ie) {
187 this.execCommand('inserthtml', EditorBase.TABKEY);
192 // Fixes #2531090 - Joins text node strings so they become one for bidi
193 if (Y.UA.webkit && e.changedNode) {
194 e.changedNode.set('innerHTML', e.changedNode.get('innerHTML'));
198 if (Y.UA.webkit && e.commands && (e.commands.indent || e.commands.outdent)) {
200 * When executing execCommand 'indent or 'outdent' Webkit applies
201 * a class to the BLOCKQUOTE that adds left/right margin to it
202 * This strips that style so it is just a normal BLOCKQUOTE
204 bq = root.all('.webkit-indent-blockquote, blockquote');
206 bq.setStyle('margin', '');
210 changed = this.getDomPath(e.changedNode, false);
217 Y.each(changed, function(el) {
218 var tag = el.tagName.toLowerCase(),
219 cmd = EditorBase.TAG2CMD[tag], s,
220 n, family2, cls, bColor2;
226 //Bold and Italic styles
227 s = el.currentStyle || el.style;
229 if ((''+s.fontWeight) === 'normal') {
232 if ((''+s.fontWeight) === 'bold') { //Cast this to a string
236 if (s.fontWeight > 400) {
240 if (s.fontStyle === 'italic') {
244 if (s.textDecoration.indexOf('underline') > -1) {
247 if (s.textDecoration.indexOf('line-through') > -1) {
248 cmds.strikethrough = 1;
252 if (n.getStyle('fontFamily')) {
253 family2 = n.getStyle('fontFamily').split(',')[0].toLowerCase();
258 family = family.replace(/'/g, '').replace(/"/g, '');
262 fsize = EditorBase.NORMALIZE_FONTSIZE(n);
265 cls = el.className.split(' ');
266 Y.each(cls, function(v) {
267 if (v !== '' && (v.substr(0, 4) !== 'yui_')) {
272 fColor = EditorBase.FILTER_RGB(n.getStyle('color'));
273 bColor2 = EditorBase.FILTER_RGB(s.backgroundColor);
274 if (bColor2 !== 'transparent') {
275 if (bColor2 !== '') {
287 e.dompath = inst.all(changed);
288 e.classNames = classes;
291 //TODO Dont' like this, not dynamic enough..
293 e.fontFamily = family;
299 e.fontColor = fColor;
301 if (!e.backgroundColor) {
302 e.backgroundColor = bColor;
305 endTime = (new Date()).getTime();
306 Y.log('_defNodeChangeTimer 2: ' + (endTime - startTime) + 'ms', 'info', 'selection');
309 * Walk the dom tree from this node up to body, returning a reversed array of parents.
311 * @param {Node} node The Node to start from
313 getDomPath: function(node, nodeList) {
314 var domPath = [], domNode, rootNode,
315 root = this._getRoot(),
316 inst = this.frame.getInstance();
318 domNode = inst.Node.getDOMNode(node);
319 rootNode = inst.Node.getDOMNode(root);
320 //return inst.all(domNode);
322 while (domNode !== null) {
324 if ((domNode === inst.config.doc.documentElement) || (domNode === inst.config.doc) || !domNode.tagName) {
329 if (!inst.DOM.inDoc(domNode)) {
334 //Check to see if we get el.nodeName and nodeType
335 if (domNode.nodeName && domNode.nodeType && (domNode.nodeType === 1)) {
336 domPath.push(domNode);
339 if (domNode === rootNode) {
344 domNode = domNode.parentNode;
348 while (node !== null) {
349 if (node.test('html') || node.test('doc') || !node.get('tagName')) {
357 //Check to see if we get el.nodeName and nodeType
358 if (node.get('nodeName') && node.get('nodeType') && (node.get('nodeType') == 1)) {
359 domPath.push(inst.Node.getDOMNode(node));
362 if (node.test('body')) {
367 node = node.get('parentNode');
371 if (domPath.length === 0) {
372 domPath[0] = inst.config.doc.body;
376 return inst.all(domPath.reverse());
378 return domPath.reverse();
383 * After frame ready, bind mousedown & keyup listeners
384 * @method _afterFrameReady
387 _afterFrameReady: function() {
388 var inst = this.frame.getInstance();
390 this.frame.on('dom:mouseup', Y.bind(this._onFrameMouseUp, this));
391 this.frame.on('dom:mousedown', Y.bind(this._onFrameMouseDown, this));
392 this.frame.on('dom:keydown', Y.bind(this._onFrameKeyDown, this));
395 this.frame.on('dom:activate', Y.bind(this._onFrameActivate, this));
396 this.frame.on('dom:beforedeactivate', Y.bind(this._beforeFrameDeactivate, this));
398 this.frame.on('dom:keyup', Y.bind(this._onFrameKeyUp, this));
399 this.frame.on('dom:keypress', Y.bind(this._onFrameKeyPress, this));
400 this.frame.on('dom:paste', Y.bind(this._onPaste, this));
402 inst.EditorSelection.filter();
406 * Caches the current cursor position in IE.
407 * @method _beforeFrameDeactivate
410 _beforeFrameDeactivate: function(e) {
411 if (e.frameTarget.test('html')) { //Means it came from a scrollbar
414 var inst = this.getInstance(),
415 sel = inst.config.doc.selection.createRange();
417 if (sel.compareEndPoints && !sel.compareEndPoints('StartToEnd', sel)) {
418 sel.pasteHTML('<var id="yui-ie-cursor">');
422 * Moves the cached selection bookmark back so IE can place the cursor in the right place.
423 * @method _onFrameActivate
426 _onFrameActivate: function(e) {
427 if (e.frameTarget.test('html')) { //Means it came from a scrollbar
430 var inst = this.getInstance(),
431 sel = new inst.EditorSelection(),
432 range = sel.createRange(),
433 root = this._getRoot(),
434 cur = root.all('#yui-ie-cursor');
437 cur.each(function(n) {
439 if (range.moveToElementText) {
441 range.moveToElementText(n._node);
442 var moved = range.move('character', -1);
443 if (moved === -1) { //Only move up if we actually moved back.
444 range.move('character', 1);
455 * Fires nodeChange event
459 _onPaste: function(e) {
460 this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'paste', changedEvent: e.frameEvent });
463 * Fires nodeChange event
464 * @method _onFrameMouseUp
467 _onFrameMouseUp: function(e) {
468 this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mouseup', changedEvent: e.frameEvent });
471 * Fires nodeChange event
472 * @method _onFrameMouseDown
475 _onFrameMouseDown: function(e) {
476 this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mousedown', changedEvent: e.frameEvent });
479 * Caches a copy of the selection for key events. Only creating the selection on keydown
480 * @property _currentSelection
483 _currentSelection: null,
485 * Holds the timer for selection clearing
486 * @property _currentSelectionTimer
489 _currentSelectionTimer: null,
491 * Flag to determine if we can clear the selection or not.
492 * @property _currentSelectionClear
495 _currentSelectionClear: null,
497 * Fires nodeChange event
498 * @method _onFrameKeyDown
501 _onFrameKeyDown: function(e) {
503 if (!this._currentSelection) {
504 if (this._currentSelectionTimer) {
505 this._currentSelectionTimer.cancel();
507 this._currentSelectionTimer = Y.later(850, this, function() {
508 this._currentSelectionClear = true;
511 inst = this.frame.getInstance();
512 sel = new inst.EditorSelection(e);
514 this._currentSelection = sel;
516 sel = this._currentSelection;
519 inst = this.frame.getInstance();
520 sel = new inst.EditorSelection();
522 this._currentSelection = sel;
524 if (sel && sel.anchorNode) {
525 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keydown', changedEvent: e.frameEvent });
526 if (EditorBase.NC_KEYS[e.keyCode]) {
527 this.fire('nodeChange', {
528 changedNode: sel.anchorNode,
529 changedType: EditorBase.NC_KEYS[e.keyCode],
530 changedEvent: e.frameEvent
532 this.fire('nodeChange', {
533 changedNode: sel.anchorNode,
534 changedType: EditorBase.NC_KEYS[e.keyCode] + '-down',
535 changedEvent: e.frameEvent
541 * Fires nodeChange event
542 * @method _onFrameKeyPress
545 _onFrameKeyPress: function(e) {
546 var sel = this._currentSelection;
548 if (sel && sel.anchorNode) {
549 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keypress', changedEvent: e.frameEvent });
550 if (EditorBase.NC_KEYS[e.keyCode]) {
551 this.fire('nodeChange', {
552 changedNode: sel.anchorNode,
553 changedType: EditorBase.NC_KEYS[e.keyCode] + '-press',
554 changedEvent: e.frameEvent
560 * Fires nodeChange event for keyup on specific keys
561 * @method _onFrameKeyUp
564 _onFrameKeyUp: function(e) {
565 var inst = this.frame.getInstance(),
566 sel = new inst.EditorSelection(e);
568 if (sel && sel.anchorNode) {
569 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keyup', selection: sel, changedEvent: e.frameEvent });
570 if (EditorBase.NC_KEYS[e.keyCode]) {
571 this.fire('nodeChange', {
572 changedNode: sel.anchorNode,
573 changedType: EditorBase.NC_KEYS[e.keyCode] + '-up',
575 changedEvent: e.frameEvent
579 if (this._currentSelectionClear) {
580 this._currentSelectionClear = this._currentSelection = null;
584 * Validates linkedcss property
586 * @method _validateLinkedCSS
589 _validateLinkedCSS: function(value) {
590 return Lang.isString(value) || Lang.isArray(value);
593 * Pass through to the frame.execCommand method
594 * @method execCommand
595 * @param {String} cmd The command to pass: inserthtml, insertimage, bold
596 * @param {String} val The optional value of the command: Helvetica
597 * @return {Node/NodeList} The Node or Nodelist affected by the command. Only returns on override commands, not browser defined commands.
599 execCommand: function(cmd, val) {
600 var ret = this.frame.execCommand(cmd, val),
601 inst = this.frame.getInstance(),
602 sel = new inst.EditorSelection(), cmds = {},
603 e = { changedNode: sel.anchorNode, changedType: 'execcommand', nodes: ret };
610 e.backgroundColor = val;
623 this.fire('nodeChange', e);
628 * Get the YUI instance of the frame
629 * @method getInstance
630 * @return {YUI} The YUI instance bound to the frame.
632 getInstance: function() {
633 return this.frame.getInstance();
636 * Renders the Y.ContentEditable to the passed node.
638 * @param {Selector/HTMLElement/Node} node The node to append the Editor to
639 * @return {EditorBase}
642 render: function(node) {
643 var frame = this.frame;
646 this.plug(Y.Plugin.Frame, {
648 title: EditorBase.STRINGS.title,
650 dir: this.get('dir'),
651 extracss: this.get('extracss'),
652 linkedcss: this.get('linkedcss'),
653 defaultblock: this.get('defaultblock')
659 if (!frame.hasPlugin('exec')) {
660 frame.plug(Y.Plugin.ExecCommand);
663 frame.after('ready', Y.bind(this._afterFrameReady, this));
665 frame.addTarget(this);
667 frame.set('content', this.get('content'));
674 * Focus the contentWindow of the iframe
676 * @param {Function} fn Callback function to execute after focus happens
677 * @return {EditorBase}
680 focus: function(fn) {
681 this.frame.focus(fn);
685 * Handles the showing of the Editor instance. Currently only handles the iframe
687 * @return {EditorBase}
695 * Handles the hiding of the Editor instance. Currently only handles the iframe
697 * @return {EditorBase}
705 * (Un)Filters the content of the Editor, cleaning YUI related code. //TODO better filtering
707 * @return {String} The filtered content of the Editor
709 getContent: function() {
710 var html = '', inst = this.getInstance();
711 if (inst && inst.EditorSelection) {
712 html = inst.EditorSelection.unfilter();
714 //Removing the _yuid from the objects in IE
715 html = html.replace(/ _yuid="([^>]*)"/g, '');
721 * @method NORMALIZE_FONTSIZE
722 * @description Pulls the fontSize from a node, then checks for string values (x-large, x-small)
723 * and converts them to pixel sizes. If the parsed size is different from the original, it calls
724 * node.setStyle to update the node with a pixel size for normalization.
726 NORMALIZE_FONTSIZE: function(n) {
727 var size = n.getStyle('fontSize'), oSize = size;
730 case '-webkit-xxx-large':
752 if (oSize !== size) {
753 n.setStyle('fontSize', size);
760 * @description The HTML markup to use for the tabkey
762 TABKEY: '<span class="tab"> </span>',
766 * @param String css The CSS string containing rgb(#,#,#);
767 * @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00
770 FILTER_RGB: function(css) {
771 if (css.toLowerCase().indexOf('rgb') !== -1) {
772 var exp = new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)", "gi"),
773 rgb = css.replace(exp, "$1,$2,$3,$4,$5").split(','),
776 if (rgb.length === 5) {
777 r = parseInt(rgb[1], 10).toString(16);
778 g = parseInt(rgb[2], 10).toString(16);
779 b = parseInt(rgb[3], 10).toString(16);
781 r = r.length === 1 ? '0' + r : r;
782 g = g.length === 1 ? '0' + g : g;
783 b = b.length === 1 ? '0' + b : b;
785 css = "#" + r + g + b;
793 * @description A hash table of tags to their execcomand's
801 'sup': 'superscript',
803 'img': 'insertimage',
805 'ul' : 'insertunorderedlist',
806 'ol' : 'insertorderedlist'
809 * Hash table of keys to fire a nodeChange event for.
830 * The default modules to use inside the Frame
835 USE: ['node', 'selector-css3', 'editor-selection', 'stylesheet'],
837 * The Class Name: editorBase
843 * Editor Strings. By default contains only the `title` property for the
844 * Title of frame document (default "Rich Text Editor").
850 title: 'Rich Text Editor'
854 * The content to load into the Editor Frame
858 validator: Lang.isString,
859 value: '<br class="yui-cursor">',
860 setter: function(str) {
861 if (str.substr(0, 1) === "\n") {
862 Y.log('Stripping first carriage return from content before injecting', 'warn', 'editor');
866 str = '<br class="yui-cursor">';
870 str = '<br class="yui-cursor">';
873 return this.frame.set('content', str);
876 return this.frame.get('content');
880 * The value of the dir attribute on the HTML element of the frame. Default: ltr
884 validator: Lang.isString,
889 * @attribute linkedcss
890 * @description An array of url's to external linked style sheets
894 validator: '_validateLinkedCSS',
896 setter: function(css) {
898 this.frame.set('linkedcss', css);
904 * @attribute extracss
905 * @description A string of CSS to add to the Head of the Editor
909 validator: Lang.isString,
911 setter: function(css) {
913 this.frame.set('extracss', css);
919 * @attribute defaultblock
920 * @description The default tag to use for block level items, defaults to: p
924 validator: Lang.isString,
930 Y.EditorBase = EditorBase;
934 * @description Fired from several mouse/key/paste event points.
935 * @param {Event.Facade} event An Event Facade object with the following specific properties added:
937 * <dt>changedEvent</dt><dd>The event that caused the nodeChange</dd>
938 * <dt>changedNode</dt><dd>The node that was interacted with</dd>
939 * <dt>changedType</dt><dd>The type of change: mousedown, mouseup, right, left, backspace, tab, enter, etc..</dd>
940 * <dt>commands</dt><dd>The list of execCommands that belong to this change and the dompath that's associated with the changedNode</dd>
941 * <dt>classNames</dt><dd>An array of classNames that are applied to the changedNode and all of it's parents</dd>
942 * <dt>dompath</dt><dd>A sorted array of node instances that make up the DOM path from the changedNode to body.</dd>
943 * <dt>backgroundColor</dt><dd>The cascaded backgroundColor of the changedNode</dd>
944 * <dt>fontColor</dt><dd>The cascaded fontColor of the changedNode</dd>
945 * <dt>fontFamily</dt><dd>The cascaded fontFamily of the changedNode</dd>
946 * <dt>fontSize</dt><dd>The cascaded fontSize of the changedNode</dd>
948 * @type {Event.Custom}
953 * @description Fired after the frame is ready.
954 * @param {Event.Facade} event An Event Facade object.
955 * @type {Event.Custom}
962 }, '3.13.0', {"requires": ["base", "frame", "node", "exec-command", "editor-selection"]});