2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('editor-base', function(Y) {
12 * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
14 * @submodule editor-base
17 * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
24 var EditorBase = function() {
25 EditorBase.superclass.constructor.apply(this, arguments);
26 }, LAST_CHILD = ':last-child', BODY = 'body';
28 Y.extend(EditorBase, Y.Base, {
30 * Internal reference to the Y.Frame instance
34 initializer: function() {
35 var frame = new Y.Frame({
37 title: EditorBase.STRINGS.title,
40 extracss: this.get('extracss'),
41 linkedcss: this.get('linkedcss'),
42 defaultblock: this.get('defaultblock'),
44 }).plug(Y.Plugin.ExecCommand);
47 frame.after('ready', Y.bind(this._afterFrameReady, this));
48 frame.addTarget(this);
52 this.publish('nodeChange', {
55 defaultFn: this._defNodeChangeFn
58 //this.plug(Y.Plugin.EditorPara);
60 destructor: function() {
66 * Copy certain styles from one node instance to another (used for new paragraph creation mainly)
68 * @param {Node} from The Node instance to copy the styles from
69 * @param {Node} to The Node instance to copy the styles to
71 copyStyles: function(from, to) {
73 //Don't carry the A styles
76 var styles = ['color', 'fontSize', 'fontFamily', 'backgroundColor', 'fontStyle' ],
79 Y.each(styles, function(v) {
80 newStyles[v] = from.getStyle(v);
82 if (from.ancestor('b,strong')) {
83 newStyles.fontWeight = 'bold';
85 if (from.ancestor('u')) {
86 if (!newStyles.textDecoration) {
87 newStyles.textDecoration = 'underline';
90 to.setStyles(newStyles);
93 * Holder for the selection bookmark in IE.
94 * @property _lastBookmark
99 * Resolves the e.changedNode in the nodeChange event if it comes from the document. If
100 * the event came from the document, it will get the last child of the last child of the document
101 * and return that instead.
102 * @method _resolveChangedNode
103 * @param {Node} n The node to resolve
106 _resolveChangedNode: function(n) {
107 var inst = this.getInstance(), lc, lc2, found;
108 if (inst && n && n.test('html')) {
109 lc = inst.one(BODY).one(LAST_CHILD);
112 lc2 = lc.one(LAST_CHILD);
127 lc = lc.get('parentNode');
139 * The default handler for the nodeChange event.
140 * @method _defNodeChangeFn
141 * @param {Event} e The event
144 _defNodeChangeFn: function(e) {
145 var startTime = (new Date()).getTime();
146 var inst = this.getInstance(), sel, cur,
147 btag = inst.Selection.DEFAULT_BLOCK_TAG;
151 sel = inst.config.doc.selection.createRange();
152 if (sel.getBookmark) {
153 this._lastBookmark = sel.getBookmark();
158 e.changedNode = this._resolveChangedNode(e.changedNode);
162 * This whole method needs to be fixed and made more dynamic.
163 * Maybe static functions for the e.changeType and an object bag
164 * to walk through and filter to pass off the event to before firing..
167 switch (e.changedType) {
170 if (!EditorBase.NC_KEYS[e.changedEvent.keyCode] && !e.changedEvent.shiftKey && !e.changedEvent.ctrlKey && (e.changedEvent.keyCode !== 13)) {
171 //inst.later(100, inst, inst.Selection.cleanCursor);
176 if (!e.changedNode.test('li, li *') && !e.changedEvent.shiftKey) {
177 e.changedEvent.frameEvent.preventDefault();
179 this.execCommand('inserttext', '\t');
180 } else if (Y.UA.gecko) {
181 this.frame.exec._command('inserthtml', '<span> </span>');
182 } else if (Y.UA.ie) {
183 sel = new inst.Selection();
184 sel._selection.pasteHTML(EditorBase.TABKEY);
189 if (Y.UA.webkit && e.commands && (e.commands.indent || e.commands.outdent)) {
191 * When executing execCommand 'indent or 'outdent' Webkit applies
192 * a class to the BLOCKQUOTE that adds left/right margin to it
193 * This strips that style so it is just a normal BLOCKQUOTE
195 var bq = inst.all('.webkit-indent-blockquote');
197 bq.setStyle('margin', '');
201 var changed = this.getDomPath(e.changedNode, false),
202 cmds = {}, family, fsize, classes = [],
203 fColor = '', bColor = '';
209 Y.each(changed, function(el) {
210 var tag = el.tagName.toLowerCase(),
211 cmd = EditorBase.TAG2CMD[tag];
217 //Bold and Italic styles
218 var s = el.currentStyle || el.style;
219 if ((''+s.fontWeight) == 'bold') { //Cast this to a string
223 if (s.fontWeight > 400) {
227 if (s.fontStyle == 'italic') {
230 if (s.textDecoration == 'underline') {
233 if (s.textDecoration == 'line-through') {
234 cmds.strikethrough = 1;
237 var n = inst.one(el);
238 if (n.getStyle('fontFamily')) {
239 var family2 = n.getStyle('fontFamily').split(',')[0].toLowerCase();
244 family = family.replace(/'/g, '').replace(/"/g, '');
248 fsize = EditorBase.NORMALIZE_FONTSIZE(n);
251 var cls = el.className.split(' ');
253 Y.each(cls, function(v) {
254 if (v !== '' && (v.substr(0, 4) !== 'yui_')) {
259 fColor = EditorBase.FILTER_RGB(n.getStyle('color'));
260 var bColor2 = EditorBase.FILTER_RGB(s.backgroundColor);
261 if (bColor2 !== 'transparent') {
262 if (bColor2 !== '') {
269 e.dompath = inst.all(changed);
270 e.classNames = classes;
273 //TODO Dont' like this, not dynamic enough..
275 e.fontFamily = family;
281 e.fontColor = fColor;
283 if (!e.backgroundColor) {
284 e.backgroundColor = bColor;
287 var endTime = (new Date()).getTime();
290 * Walk the dom tree from this node up to body, returning a reversed array of parents.
292 * @param {Node} node The Node to start from
294 getDomPath: function(node, nodeList) {
295 var domPath = [], domNode,
296 inst = this.frame.getInstance();
298 domNode = inst.Node.getDOMNode(node);
299 //return inst.all(domNode);
301 while (domNode !== null) {
303 if ((domNode === inst.config.doc.documentElement) || (domNode === inst.config.doc) || !domNode.tagName) {
308 if (!inst.DOM.inDoc(domNode)) {
313 //Check to see if we get el.nodeName and nodeType
314 if (domNode.nodeName && domNode.nodeType && (domNode.nodeType == 1)) {
315 domPath.push(domNode);
318 if (domNode == inst.config.doc.body) {
323 domNode = domNode.parentNode;
327 while (node !== null) {
328 if (node.test('html') || node.test('doc') || !node.get('tagName')) {
336 //Check to see if we get el.nodeName and nodeType
337 if (node.get('nodeName') && node.get('nodeType') && (node.get('nodeType') == 1)) {
338 domPath.push(inst.Node.getDOMNode(node));
341 if (node.test('body')) {
346 node = node.get('parentNode');
350 if (domPath.length === 0) {
351 domPath[0] = inst.config.doc.body;
355 return inst.all(domPath.reverse());
357 return domPath.reverse();
362 * After frame ready, bind mousedown & keyup listeners
363 * @method _afterFrameReady
366 _afterFrameReady: function() {
367 var inst = this.frame.getInstance();
369 this.frame.on('dom:mouseup', Y.bind(this._onFrameMouseUp, this));
370 this.frame.on('dom:mousedown', Y.bind(this._onFrameMouseDown, this));
371 this.frame.on('dom:keydown', Y.bind(this._onFrameKeyDown, this));
374 this.frame.on('dom:activate', Y.bind(this._onFrameActivate, this));
375 this.frame.on('dom:beforedeactivate', Y.bind(this._beforeFrameDeactivate, this));
377 this.frame.on('dom:keyup', Y.bind(this._onFrameKeyUp, this));
378 this.frame.on('dom:keypress', Y.bind(this._onFrameKeyPress, this));
380 inst.Selection.filter();
384 * Caches the current cursor position in IE.
385 * @method _beforeFrameDeactivate
388 _beforeFrameDeactivate: function() {
389 var inst = this.getInstance(),
390 sel = inst.config.doc.selection.createRange();
392 if ((!sel.compareEndPoints('StartToEnd', sel))) {
393 sel.pasteHTML('<var id="yui-ie-cursor">');
397 * Moves the cached selection bookmark back so IE can place the cursor in the right place.
398 * @method _onFrameActivate
401 _onFrameActivate: function() {
402 var inst = this.getInstance(),
403 sel = new inst.Selection(),
404 range = sel.createRange(),
405 cur = inst.all('#yui-ie-cursor');
408 cur.each(function(n) {
410 range.moveToElementText(n._node);
411 range.move('character', -1);
412 range.move('character', 1);
420 * Fires nodeChange event
421 * @method _onFrameMouseUp
424 _onFrameMouseUp: function(e) {
425 this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mouseup', changedEvent: e.frameEvent });
428 * Fires nodeChange event
429 * @method _onFrameMouseDown
432 _onFrameMouseDown: function(e) {
433 this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mousedown', changedEvent: e.frameEvent });
436 * Caches a copy of the selection for key events. Only creating the selection on keydown
437 * @property _currentSelection
440 _currentSelection: null,
442 * Holds the timer for selection clearing
443 * @property _currentSelectionTimer
446 _currentSelectionTimer: null,
448 * Flag to determine if we can clear the selection or not.
449 * @property _currentSelectionClear
452 _currentSelectionClear: null,
454 * Fires nodeChange event
455 * @method _onFrameKeyDown
458 _onFrameKeyDown: function(e) {
460 if (!this._currentSelection) {
461 if (this._currentSelectionTimer) {
462 this._currentSelectionTimer.cancel();
464 this._currentSelectionTimer = Y.later(850, this, function() {
465 this._currentSelectionClear = true;
468 inst = this.frame.getInstance();
469 sel = new inst.Selection(e);
471 this._currentSelection = sel;
473 sel = this._currentSelection;
476 inst = this.frame.getInstance();
477 sel = new inst.Selection();
479 this._currentSelection = sel;
481 if (sel && sel.anchorNode) {
482 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keydown', changedEvent: e.frameEvent });
483 if (EditorBase.NC_KEYS[e.keyCode]) {
484 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode], changedEvent: e.frameEvent });
485 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-down', changedEvent: e.frameEvent });
490 * Fires nodeChange event
491 * @method _onFrameKeyPress
494 _onFrameKeyPress: function(e) {
495 var sel = this._currentSelection;
497 if (sel && sel.anchorNode) {
498 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keypress', changedEvent: e.frameEvent });
499 if (EditorBase.NC_KEYS[e.keyCode]) {
500 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-press', changedEvent: e.frameEvent });
505 * Fires nodeChange event for keyup on specific keys
506 * @method _onFrameKeyUp
509 _onFrameKeyUp: function(e) {
510 var sel = this._currentSelection;
512 if (sel && sel.anchorNode) {
513 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keyup', selection: sel, changedEvent: e.frameEvent });
514 if (EditorBase.NC_KEYS[e.keyCode]) {
515 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-up', selection: sel, changedEvent: e.frameEvent });
518 if (this._currentSelectionClear) {
519 this._currentSelectionClear = this._currentSelection = null;
523 * Pass through to the frame.execCommand method
524 * @method execCommand
525 * @param {String} cmd The command to pass: inserthtml, insertimage, bold
526 * @param {String} val The optional value of the command: Helvetica
527 * @return {Node/NodeList} The Node or Nodelist affected by the command. Only returns on override commands, not browser defined commands.
529 execCommand: function(cmd, val) {
530 var ret = this.frame.execCommand(cmd, val),
531 inst = this.frame.getInstance(),
532 sel = new inst.Selection(), cmds = {},
533 e = { changedNode: sel.anchorNode, changedType: 'execcommand', nodes: ret };
540 e.backgroundColor = val;
553 this.fire('nodeChange', e);
558 * Get the YUI instance of the frame
559 * @method getInstance
560 * @return {YUI} The YUI instance bound to the frame.
562 getInstance: function() {
563 return this.frame.getInstance();
566 * Renders the Y.Frame to the passed node.
568 * @param {Selector/HTMLElement/Node} node The node to append the Editor to
569 * @return {EditorBase}
572 render: function(node) {
573 this.frame.set('content', this.get('content'));
574 this.frame.render(node);
578 * Focus the contentWindow of the iframe
580 * @param {Function} fn Callback function to execute after focus happens
581 * @return {EditorBase}
584 focus: function(fn) {
585 this.frame.focus(fn);
589 * Handles the showing of the Editor instance. Currently only handles the iframe
591 * @return {EditorBase}
599 * Handles the hiding of the Editor instance. Currently only handles the iframe
601 * @return {EditorBase}
609 * (Un)Filters the content of the Editor, cleaning YUI related code. //TODO better filtering
611 * @return {String} The filtered content of the Editor
613 getContent: function() {
614 var html = '', inst = this.getInstance();
615 if (inst && inst.Selection) {
616 html = inst.Selection.unfilter();
618 //Removing the _yuid from the objects in IE
619 html = html.replace(/ _yuid="([^>]*)"/g, '');
625 * @method NORMALIZE_FONTSIZE
626 * @description Pulls the fontSize from a node, then checks for string values (x-large, x-small)
627 * and converts them to pixel sizes. If the parsed size is different from the original, it calls
628 * node.setStyle to update the node with a pixel size for normalization.
630 NORMALIZE_FONTSIZE: function(n) {
631 var size = n.getStyle('fontSize'), oSize = size;
634 case '-webkit-xxx-large':
656 if (oSize !== size) {
657 n.setStyle('fontSize', size);
664 * @description The HTML markup to use for the tabkey
666 TABKEY: '<span class="tab"> </span>',
670 * @param String css The CSS string containing rgb(#,#,#);
671 * @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00
674 FILTER_RGB: function(css) {
675 if (css.toLowerCase().indexOf('rgb') != -1) {
676 var exp = new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)", "gi");
677 var rgb = css.replace(exp, "$1,$2,$3,$4,$5").split(',');
679 if (rgb.length == 5) {
680 var r = parseInt(rgb[1], 10).toString(16);
681 var g = parseInt(rgb[2], 10).toString(16);
682 var b = parseInt(rgb[3], 10).toString(16);
684 r = r.length == 1 ? '0' + r : r;
685 g = g.length == 1 ? '0' + g : g;
686 b = b.length == 1 ? '0' + b : b;
688 css = "#" + r + g + b;
696 * @description A hash table of tags to their execcomand's
704 'sup': 'superscript',
706 'img': 'insertimage',
708 'ul' : 'insertunorderedlist',
709 'ol' : 'insertorderedlist'
712 * Hash table of keys to fire a nodeChange event for.
733 * The default modules to use inside the Frame
738 USE: ['substitute', 'node', 'selector-css3', 'selection', 'stylesheet'],
740 * The Class Name: editorBase
752 * Title of frame document: Rich Text Editor
754 * @property STRINGS.title
756 title: 'Rich Text Editor'
760 * The content to load into the Editor Frame
764 value: '<br class="yui-cursor">',
765 setter: function(str) {
766 if (str.substr(0, 1) === "\n") {
770 str = '<br class="yui-cursor">';
774 str = '<br class="yui-cursor">';
777 return this.frame.set('content', str);
780 return this.frame.get('content');
784 * The value of the dir attribute on the HTML element of the frame. Default: ltr
792 * @attribute linkedcss
793 * @description An array of url's to external linked style sheets
798 setter: function(css) {
800 this.frame.set('linkedcss', css);
806 * @attribute extracss
807 * @description A string of CSS to add to the Head of the Editor
812 setter: function(css) {
814 this.frame.set('extracss', css);
820 * @attribute defaultblock
821 * @description The default tag to use for block level items, defaults to: p
830 Y.EditorBase = EditorBase;
834 * @description Fired from mouseup & keyup.
835 * @param {Event.Facade} event An Event Facade object with the following specific properties added:
837 * <dt>changedEvent</dt><dd>The event that caused the nodeChange</dd>
838 * <dt>changedNode</dt><dd>The node that was interacted with</dd>
839 * <dt>changedType</dt><dd>The type of change: mousedown, mouseup, right, left, backspace, tab, enter, etc..</dd>
840 * <dt>commands</dt><dd>The list of execCommands that belong to this change and the dompath that's associated with the changedNode</dd>
841 * <dt>classNames</dt><dd>An array of classNames that are applied to the changedNode and all of it's parents</dd>
842 * <dt>dompath</dt><dd>A sorted array of node instances that make up the DOM path from the changedNode to body.</dd>
843 * <dt>backgroundColor</dt><dd>The cascaded backgroundColor of the changedNode</dd>
844 * <dt>fontColor</dt><dd>The cascaded fontColor of the changedNode</dd>
845 * <dt>fontFamily</dt><dd>The cascaded fontFamily of the changedNode</dd>
846 * <dt>fontSize</dt><dd>The cascaded fontSize of the changedNode</dd>
848 * @type {Event.Custom}
853 * @description Fired after the frame is ready.
854 * @param {Event.Facade} event An Event Facade object.
855 * @type {Event.Custom}
862 }, '3.3.0' ,{requires:['base', 'frame', 'node', 'exec-command', 'selection', 'editor-para'], skinnable:false});