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('frame', function(Y) {
11 * Creates a wrapper around an iframe. It loads the content either from a local
12 * file or from script and creates a local YUI instance bound to that new window and document.
17 * Creates a wrapper around an iframe. It loads the content either from a local
18 * file or from script and creates a local YUI instance bound to that new window and document.
25 var Frame = function() {
26 Frame.superclass.constructor.apply(this, arguments);
29 Y.extend(Frame, Y.Base, {
33 * @description Internal reference set when the content is ready.
40 * @description Internal reference set when render is called.
47 * @description Internal Node reference to the iFrame or the window
54 * @description Internal reference to the YUI instance bound to the iFrame or window
61 * @description Create the iframe or Window and get references to the Document & Window
62 * @return {Object} Hash table containing references to the new Document & Window
64 _create: function(cb) {
65 var win, doc, res, node;
67 this._iframe = Y.Node.create(Frame.HTML);
68 this._iframe.setStyle('visibility', 'hidden');
69 this._iframe.set('src', this.get('src'));
70 this.get('container').append(this._iframe);
72 this._iframe.set('height', '99%');
76 extra_css = ((this.get('extracss')) ? '<style id="extra_css">' + this.get('extracss') + '</style>' : '');
78 html = Y.substitute(Frame.PAGE_HTML, {
80 LANG: this.get('lang'),
81 TITLE: this.get('title'),
83 LINKED_CSS: this.get('linkedcss'),
84 CONTENT: this.get('content'),
85 BASE_HREF: this.get('basehref'),
86 DEFAULT_CSS: Frame.DEFAULT_CSS,
89 if (Y.config.doc.compatMode != 'BackCompat') {
91 //html = Frame.DOC_TYPE + "\n" + html;
92 html = Frame.getDocType() + "\n" + html;
98 res = this._resolveWinDoc();
103 if (this.get('designMode')) {
104 res.doc.designMode = 'on';
107 if (!res.doc.documentElement) {
108 var timer = Y.later(1, this, function() {
109 if (res.doc && res.doc.documentElement) {
121 * @method _resolveWinDoc
122 * @description Resolves the document and window from an iframe or window instance
123 * @param {Object} c The YUI Config to add the window and document to
124 * @return {Object} Object hash of window and document references, if a YUI config was passed, it is returned.
126 _resolveWinDoc: function(c) {
127 var config = (c) ? c : {};
128 config.win = Y.Node.getDOMNode(this._iframe.get('contentWindow'));
129 config.doc = Y.Node.getDOMNode(this._iframe.get('contentWindow.document'));
131 config.doc = Y.config.doc;
134 config.win = Y.config.win;
140 * @method _onDomEvent
141 * @description Generic handler for all DOM events fired by the iframe or window. This handler
142 * takes the current EventFacade and augments it to fire on the Frame host. It adds two new properties
143 * to the EventFacade called frameX and frameY which adds the scroll and xy position of the iframe
144 * to the original pageX and pageY of the event so external nodes can be positioned over the frame.
145 * @param {Event.Facade} e
147 _onDomEvent: function(e) {
150 e.frameX = e.frameY = 0;
152 if (e.pageX > 0 || e.pageY > 0) {
153 if (e.type.substring(0, 3) !== 'key') {
154 node = this._instance.one('win');
155 xy = this._iframe.getXY();
156 e.frameX = xy[0] + e.pageX - node.get('scrollLeft');
157 e.frameY = xy[1] + e.pageY - node.get('scrollTop');
161 e.frameTarget = e.target;
162 e.frameCurrentTarget = e.currentTarget;
165 this.fire('dom:' + e.type, e);
167 initializer: function() {
168 this.publish('ready', {
170 defaultFn: this._defReadyFn
173 destructor: function() {
174 var inst = this.getInstance();
176 inst.one('doc').detachAll();
178 this._iframe.remove();
183 * @description Simple pass thru handler for the paste event so we can do content cleanup
184 * @param {Event.Facade} e
186 _DOMPaste: function(e) {
187 var inst = this.getInstance(),
188 data = '', win = inst.config.win;
190 if (e._event.originalTarget) {
191 data = e._event.originalTarget;
193 if (e._event.clipboardData) {
194 data = e._event.clipboardData.getData('Text');
197 if (win.clipboardData) {
198 data = win.clipboardData.getData('Text');
199 if (data === '') { // Could be empty, or failed
201 if (!win.clipboardData.setData('Text', data)) {
208 e.frameTarget = e.target;
209 e.frameCurrentTarget = e.currentTarget;
215 getData: function() {
220 e.clipboardData = null;
223 this.fire('dom:paste', e);
227 * @method _defReadyFn
228 * @description Binds DOM events, sets the iframe to visible and fires the ready event
230 _defReadyFn: function() {
231 var inst = this.getInstance(),
232 fn = Y.bind(this._onDomEvent, this),
233 kfn = ((Y.UA.ie) ? Y.throttle(fn, 200) : fn);
235 inst.Node.DOM_EVENTS.activate = 1;
236 inst.Node.DOM_EVENTS.beforedeactivate = 1;
237 inst.Node.DOM_EVENTS.focusin = 1;
238 inst.Node.DOM_EVENTS.deactivate = 1;
239 inst.Node.DOM_EVENTS.focusout = 1;
241 //Y.each(inst.Node.DOM_EVENTS, function(v, k) {
242 Y.each(Frame.DOM_EVENTS, function(v, k) {
244 if (k !== 'focus' && k !== 'blur' && k !== 'paste') {
245 if (k.substring(0, 3) === 'key') {
246 if (k === 'keydown') {
247 inst.on(k, fn, inst.config.doc);
249 inst.on(k, kfn, inst.config.doc);
252 inst.on(k, fn, inst.config.doc);
258 inst.Node.DOM_EVENTS.paste = 1;
260 inst.on('paste', Y.bind(this._DOMPaste, this), inst.one('body'));
262 //Adding focus/blur to the window object
263 inst.on('focus', fn, inst.config.win);
264 inst.on('blur', fn, inst.config.win);
266 inst._use = inst.use;
267 inst.use = Y.bind(this.use, this);
268 this._iframe.setStyles({
269 visibility: 'inherit'
271 inst.one('body').setStyle('display', 'block');
273 this._fixIECursors();
277 * It appears that having a BR tag anywhere in the source "below" a table with a percentage width (in IE 7 & 8)
278 * if there is any TEXTINPUT's outside the iframe, the cursor will rapidly flickr and the CPU would occasionally
279 * spike. This method finds all <BR>'s below the sourceIndex of the first table. Does some checks to see if they
280 * can be modified and replaces then with a <WBR> so the layout will remain in tact, but the flickering will
282 * @method _fixIECursors
285 _fixIECursors: function() {
286 var inst = this.getInstance(),
287 tables = inst.all('table'),
288 brs = inst.all('br'), si;
290 if (tables.size() && brs.size()) {
292 si = tables.item(0).get('sourceIndex');
293 brs.each(function(n) {
294 var p = n.get('parentNode'),
295 c = p.get('children'), b = p.all('>br');
299 n.replace(inst.Node.create('<wbr>'));
301 if (n.get('sourceIndex') > si) {
303 n.replace(inst.Node.create('<wbr>'));
307 n.replace(inst.Node.create('<wbr>'));
318 * @method _onContentReady
319 * @description Called once the content is available in the frame/window and calls the final use call
320 * on the internal instance so that the modules are loaded properly.
322 _onContentReady: function(e) {
325 var inst = this.getInstance(),
326 args = Y.clone(this.get('use'));
328 this.fire('contentready');
331 inst.config.doc = Y.Node.getDOMNode(e.target);
333 //TODO Circle around and deal with CSS loading...
334 args.push(Y.bind(function() {
335 if (inst.Selection) {
336 inst.Selection.DEFAULT_BLOCK_TAG = this.get('defaultblock');
341 inst.use.apply(inst, args);
343 inst.one('doc').get('documentElement').addClass('yui-js-enabled');
348 * @method _resolveBaseHref
349 * @description Resolves the basehref of the page the frame is created on. Only applies to dynamic content.
350 * @param {String} href The new value to use, if empty it will be resolved from the current url.
353 _resolveBaseHref: function(href) {
354 if (!href || href === '') {
355 href = Y.config.doc.location.href;
356 if (href.indexOf('?') !== -1) { //Remove the query string
357 href = href.substring(0, href.indexOf('?'));
359 href = href.substring(0, href.lastIndexOf('/')) + '/';
366 * @description Get the content from the iframe
367 * @param {String} html The raw HTML from the body of the iframe.
370 _getHTML: function(html) {
372 var inst = this.getInstance();
373 html = inst.one('body').get('innerHTML');
380 * @description Set the content of the iframe
381 * @param {String} html The raw HTML to set the body of the iframe to.
384 _setHTML: function(html) {
386 var inst = this.getInstance();
387 inst.one('body').set('innerHTML', html);
389 //This needs to be wrapped in a contentready callback for the !_ready state
390 this.on('contentready', Y.bind(function(html, e) {
391 var inst = this.getInstance();
392 inst.one('body').set('innerHTML', html);
399 * @method _setLinkedCSS
400 * @description Set's the linked CSS on the instance..
402 _getLinkedCSS: function(urls) {
403 if (!Y.Lang.isArray(urls)) {
408 Y.each(urls, function(v) {
410 str += '<link rel="stylesheet" href="' + v + '" type="text/css">';
420 * @method _setLinkedCSS
421 * @description Set's the linked CSS on the instance..
423 _setLinkedCSS: function(css) {
425 var inst = this.getInstance();
432 * @method _setExtraCSS
433 * @description Set's the extra CSS on the instance..
435 _setExtraCSS: function(css) {
437 var inst = this.getInstance(),
438 node = inst.get('#extra_css');
441 inst.one('head').append('<style id="extra_css">' + css + '</style>');
447 * @method _instanceLoaded
448 * @description Called from the first YUI instance that sets up the internal instance.
449 * This loads the content into the window/frame and attaches the contentready event.
450 * @param {YUI} inst The internal YUI instance bound to the frame/window
452 _instanceLoaded: function(inst) {
453 this._instance = inst;
454 this._onContentReady();
456 var doc = this._instance.config.doc;
458 if (this.get('designMode')) {
461 //Force other browsers into non CSS styling
462 doc.execCommand('styleWithCSS', false, false);
463 doc.execCommand('insertbronreturn', false, false);
468 //BEGIN PUBLIC METHODS
471 * @description This is a scoped version of the normal YUI.use method & is bound to this frame/window.
472 * At setup, the inst.use method is mapped to this method.
475 var inst = this.getInstance(),
476 args = Y.Array(arguments),
479 if (Y.Lang.isFunction(args[args.length - 1])) {
483 args.push(function() {
484 cb.apply(inst, arguments);
488 inst._use.apply(inst, args);
492 * @description A delegate method passed to the instance's delegate method
493 * @param {String} type The type of event to listen for
494 * @param {Function} fn The method to attach
495 * @param {String} cont The container to act as a delegate, if no "sel" passed, the body is assumed as the container.
496 * @param {String} sel The selector to match in the event (optional)
497 * @return {EventHandle} The Event handle returned from Y.delegate
499 delegate: function(type, fn, cont, sel) {
500 var inst = this.getInstance();
508 return inst.delegate(type, fn, cont, sel);
511 * @method getInstance
512 * @description Get a reference to the internal YUI instance.
513 * @return {YUI} The internal YUI instance
515 getInstance: function() {
516 return this._instance;
520 * @description Render the iframe into the container config option or open the window.
521 * @param {String/HTMLElement/Node} node The node to render to
525 render: function(node) {
526 if (this._rendered) {
529 this._rendered = true;
531 this.set('container', node);
534 this._create(Y.bind(function(res) {
537 cb = Y.bind(function(i) {
538 this._instanceLoaded(i);
540 args = Y.clone(this.get('use')),
546 fn = Y.bind(function() {
547 config = this._resolveWinDoc(config);
551 inst.use('node-base', cb);
553 clearInterval(timer);
556 timer = setInterval(function() {
564 Y.use.apply(Y, args);
572 * @method _handleFocus
573 * @description Does some tricks on focus to set the proper cursor position.
575 _handleFocus: function() {
576 var inst = this.getInstance(),
577 sel = new inst.Selection();
579 if (sel.anchorNode) {
580 var n = sel.anchorNode,
581 c = n.get('childNodes');
584 if (c.item(0).test('br')) {
585 sel.selectNode(n, true, false);
587 if (c.item(0).test('p')) {
588 n = c.item(0).one('br.yui-cursor').get('parentNode');
589 sel.selectNode(n, true, false);
596 * @description Set the focus to the iframe
597 * @param {Function} fn Callback function to execute after focus happens
601 focus: function(fn) {
604 Y.one('win').focus();
605 this.getInstance().one('win').focus();
611 if (Y.Lang.isFunction(fn)) {
616 Y.one('win').focus();
617 Y.later(100, this, function() {
618 this.getInstance().one('win').focus();
622 if (Y.Lang.isFunction(fn)) {
633 * @description Show the iframe instance
638 this._iframe.setStyles({
644 this._instance.config.doc.designMode = 'on';
652 * @description Hide the iframe instance
657 this._iframe.setStyles({
658 position: 'absolute',
667 * @property DOM_EVENTS
668 * @description The DomEvents that the frame automatically attaches and bubbles
689 * @property DEFAULT_CSS
690 * @description The default css used when creating the document.
693 //DEFAULT_CSS: 'html { height: 95%; } body { padding: 7px; background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; } a, a:visited, a:hover { color: blue !important; text-decoration: underline !important; cursor: text !important; } img { cursor: pointer !important; border: none; }',
694 DEFAULT_CSS: 'body { background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; } a, a:visited, a:hover { color: blue !important; text-decoration: underline !important; cursor: text !important; } img { cursor: pointer !important; border: none; }',
698 * @description The template string used to create the iframe
701 //HTML: '<iframe border="0" frameBorder="0" marginWidth="0" marginHeight="0" leftMargin="0" topMargin="0" allowTransparency="true" width="100%" height="99%"></iframe>',
702 HTML: '<iframe border="0" frameBorder="0" marginWidth="0" marginHeight="0" leftMargin="0" topMargin="0" allowTransparency="true" width="100%" height="99%"></iframe>',
705 * @property PAGE_HTML
706 * @description The template used to create the page when created dynamically.
709 PAGE_HTML: '<html dir="{DIR}" lang="{LANG}"><head><title>{TITLE}</title>{META}<base href="{BASE_HREF}"/>{LINKED_CSS}<style id="editor_css">{DEFAULT_CSS}</style>{EXTRA_CSS}</head><body>{CONTENT}</body></html>',
714 * @description Parses document.doctype and generates a DocType to match the parent page, if supported.
715 * For IE8, it grabs document.all[0].nodeValue and uses that. For IE < 8, it falls back to Frame.DOC_TYPE.
716 * @returns {String} The normalized DocType to apply to the iframe
718 getDocType: function() {
719 var dt = Y.config.doc.doctype,
720 str = Frame.DOC_TYPE;
723 str = '<!DOCTYPE ' + dt.name + ((dt.publicId) ? ' ' + dt.publicId : '') + ((dt.systemId) ? ' ' + dt.systemId : '') + '>';
725 if (Y.config.doc.all) {
726 dt = Y.config.doc.all[0];
728 if (dt.nodeType === 8) {
730 if (dt.nodeValue.toLowerCase().indexOf('doctype') !== -1) {
731 str = '<!' + dt.nodeValue + '>';
743 * @description The DOCTYPE to prepend to the new document when created. Should match the one on the page being served.
746 DOC_TYPE: '<!DOCTYPE HTML PUBLIC "-/'+'/W3C/'+'/DTD HTML 4.01/'+'/EN" "http:/'+'/www.w3.org/TR/html4/strict.dtd">',
750 * @description The meta-tag for Content-Type to add to the dynamic document
753 //META: '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">',
754 META: '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>',
758 * @description The name of the class (frame)
765 * @description The title to give the blank page.
773 * @description The default text direction for this new frame. Default: ltr
781 * @description The default language. Default: en-US
789 * @description The src of the iframe/window. Defaults to javascript:;
793 //Hackish, IE needs the false in the Javascript URL
794 value: 'javascript' + ((Y.UA.ie) ? ':false' : ':') + ';'
797 * @attribute designMode
798 * @description Should designMode be turned on after creation.
808 * @description The string to inject into the body of the new frame/window.
817 * @attribute basehref
818 * @description The base href to use in the iframe.
823 getter: '_resolveBaseHref'
827 * @description Array of modules to include in the scoped YUI instance at render time. Default: ['none', 'selector-css2']
833 value: ['substitute', 'node', 'node-style', 'selector-css3']
836 * @attribute container
837 * @description The container to append the iFrame to on render.
838 * @type String/HTMLElement/Node
842 setter: function(n) {
848 * @description The Node instance of the iframe.
860 * @description Set the id of the new Node. (optional)
866 getter: function(id) {
868 id = 'iframe-' + Y.guid();
874 * @attribute linkedcss
875 * @description An array of url's to external linked style sheets
880 getter: '_getLinkedCSS',
881 setter: '_setLinkedCSS'
884 * @attribute extracss
885 * @description A string of CSS to add to the Head of the Editor
890 setter: '_setExtraCSS'
894 * @description A reference to the Editor instance
901 * @attribute defaultblock
902 * @description The default tag to use for block level items, defaults to: p
916 }, '3.3.0' ,{requires:['base', 'node', 'selector-css3', 'substitute'], skinnable:false});
917 YUI.add('selection', function(Y) {
920 * Wraps some common Selection/Range functionality into a simple object
922 * @submodule selection
925 * Wraps some common Selection/Range functionality into a simple object
931 //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
932 var textContent = 'textContent',
933 INNER_HTML = 'innerHTML',
934 FONT_FAMILY = 'fontFamily';
937 textContent = 'nodeValue';
940 Y.Selection = function(domEvent) {
941 var sel, par, ieNode, nodes, rng, i;
943 if (Y.config.win.getSelection) {
944 sel = Y.config.win.getSelection();
945 } else if (Y.config.doc.selection) {
946 sel = Y.config.doc.selection.createRange();
948 this._selection = sel;
951 this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
952 if (this.isCollapsed) {
953 this.anchorNode = this.focusNode = Y.one(sel.parentElement());
956 ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
959 par = sel.parentElement();
960 nodes = par.childNodes;
961 rng = sel.duplicate();
963 for (i = 0; i < nodes.length; i++) {
964 //This causes IE to not allow a selection on a doubleclick
965 //rng.select(nodes[i]);
966 if (rng.inRange(sel)) {
974 this.ieNode = ieNode;
977 if (ieNode.nodeType !== 3) {
978 if (ieNode.firstChild) {
979 ieNode = ieNode.firstChild;
981 if (ieNode && ieNode.tagName && ieNode.tagName.toLowerCase() === 'body') {
982 if (ieNode.firstChild) {
983 ieNode = ieNode.firstChild;
987 this.anchorNode = this.focusNode = Y.Selection.resolve(ieNode);
989 this.anchorOffset = this.focusOffset = (this.anchorNode.nodeValue) ? this.anchorNode.nodeValue.length : 0 ;
991 this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
996 //This helps IE deal with a selection and nodeChange events
998 var n = Y.Node.create(sel.htmlText);
1000 var id = n.get('id');
1001 this.anchorNode = this.focusNode = Y.one('#' + id);
1003 n = n.get('childNodes');
1004 this.anchorNode = this.focusNode = n.item(0);
1012 this.isCollapsed = sel.isCollapsed;
1013 this.anchorNode = Y.Selection.resolve(sel.anchorNode);
1014 this.focusNode = Y.Selection.resolve(sel.focusNode);
1015 this.anchorOffset = sel.anchorOffset;
1016 this.focusOffset = sel.focusOffset;
1018 this.anchorTextNode = Y.one(sel.anchorNode);
1019 this.focusTextNode = Y.one(sel.focusNode);
1021 if (Y.Lang.isString(sel.text)) {
1022 this.text = sel.text;
1025 this.text = sel.toString();
1033 * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
1034 * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
1035 * the fontFamily when selecting nodes.
1039 Y.Selection.filter = function(blocks) {
1040 var startTime = (new Date()).getTime();
1042 var nodes = Y.all(Y.Selection.ALL),
1043 baseNodes = Y.all('strong,em'),
1045 hrs = doc.getElementsByTagName('hr'),
1046 classNames = {}, cssString = '',
1049 var startTime1 = (new Date()).getTime();
1050 nodes.each(function(n) {
1051 var raw = Y.Node.getDOMNode(n);
1052 if (raw.style[FONT_FAMILY]) {
1053 classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
1054 n.addClass(n._yuid);
1055 raw.style[FONT_FAMILY] = 'inherit';
1057 raw.removeAttribute('face');
1058 if (raw.getAttribute('style') === '') {
1059 raw.removeAttribute('style');
1062 if (raw.getAttribute('style')) {
1063 if (raw.getAttribute('style').toLowerCase() === 'font-family: ') {
1064 raw.removeAttribute('style');
1069 if (n.getStyle(FONT_FAMILY)) {
1070 classNames['.' + n._yuid] = n.getStyle(FONT_FAMILY);
1071 n.addClass(n._yuid);
1072 n.removeAttribute('face');
1073 n.setStyle(FONT_FAMILY, '');
1074 if (n.getAttribute('style') === '') {
1075 n.removeAttribute('style');
1078 if (n.getAttribute('style').toLowerCase() === 'font-family: ') {
1079 n.removeAttribute('style');
1084 var endTime1 = (new Date()).getTime();
1086 Y.all('.hr').addClass('yui-skip').addClass('yui-non');
1088 Y.each(hrs, function(hr) {
1089 var el = doc.createElement('div');
1090 el.className = 'hr yui-non yui-skip';
1092 el.setAttribute('readonly', true);
1093 el.setAttribute('contenteditable', false); //Keep it from being Edited
1094 if (hr.parentNode) {
1095 hr.parentNode.replaceChild(el, hr);
1097 //Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style');
1099 s.border = '1px solid #ccc';
1102 s.marginTop = '5px';
1103 s.marginBottom = '5px';
1104 s.marginLeft = '0px';
1105 s.marginRight = '0px';
1110 Y.each(classNames, function(v, k) {
1111 cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
1113 Y.StyleSheet(cssString, 'editor');
1116 //Not sure about this one?
1117 baseNodes.each(function(n, k) {
1118 var t = n.get('tagName').toLowerCase(),
1120 if (t === 'strong') {
1123 Y.Selection.prototype._swap(baseNodes.item(k), newTag);
1126 //Filter out all the empty UL/OL's
1127 ls = Y.all('ol,ul');
1128 ls.each(function(v, k) {
1129 var lis = v.all('li');
1136 Y.Selection.filterBlocks();
1138 var endTime = (new Date()).getTime();
1142 * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
1144 * @method filterBlocks
1146 Y.Selection.filterBlocks = function() {
1147 var startTime = (new Date()).getTime();
1148 var childs = Y.config.doc.body.childNodes, i, node, wrapped = false, doit = true,
1149 sel, single, br, divs, spans, c, s;
1152 for (i = 0; i < childs.length; i++) {
1153 node = Y.one(childs[i]);
1154 if (!node.test(Y.Selection.BLOCKS)) {
1156 if (childs[i].nodeType == 3) {
1157 c = childs[i][textContent].match(Y.Selection.REG_CHAR);
1158 s = childs[i][textContent].match(Y.Selection.REG_NON);
1159 if (c === null && s) {
1168 wrapped.push(childs[i]);
1171 wrapped = Y.Selection._wrapBlock(wrapped);
1174 wrapped = Y.Selection._wrapBlock(wrapped);
1177 single = Y.all(Y.Selection.DEFAULT_BLOCK_TAG);
1178 if (single.size() === 1) {
1179 br = single.item(0).all('br');
1180 if (br.size() === 1) {
1181 if (!br.item(0).test('.yui-cursor')) {
1182 br.item(0).remove();
1184 var html = single.item(0).get('innerHTML');
1185 if (html === '' || html === ' ') {
1186 single.set('innerHTML', Y.Selection.CURSOR);
1187 sel = new Y.Selection();
1188 sel.focusCursor(true, true);
1192 single.each(function(p) {
1193 var html = p.get('innerHTML');
1202 divs = Y.all('div, p');
1203 divs.each(function(d) {
1204 if (d.hasClass('yui-non')) {
1207 var html = d.get('innerHTML');
1211 if (d.get('childNodes').size() == 1) {
1212 if (d.ancestor('p')) {
1213 d.replace(d.get('firstChild'));
1219 /** Removed this, as it was causing Pasting to be funky in Safari
1220 spans = Y.all('.Apple-style-span, .apple-style-span');
1221 spans.each(function(s) {
1222 s.setAttribute('style', '');
1228 var endTime = (new Date()).getTime();
1232 * Regular Expression to determine if a string has a character in it
1234 * @property REG_CHAR
1236 Y.Selection.REG_CHAR = /[a-zA-Z-0-9_]/gi;
1239 * Regular Expression to determine if a string has a non-character in it
1243 Y.Selection.REG_NON = /[\s\S|\n|\t]/gi;
1246 * Regular Expression to remove all HTML from a string
1248 * @property REG_NOHTML
1250 Y.Selection.REG_NOHTML = /<\S[^><]*>/g;
1254 * Wraps an array of elements in a Block level tag
1257 * @method _wrapBlock
1259 Y.Selection._wrapBlock = function(wrapped) {
1261 var newChild = Y.Node.create('<' + Y.Selection.DEFAULT_BLOCK_TAG + '></' + Y.Selection.DEFAULT_BLOCK_TAG + '>'),
1262 firstChild = Y.one(wrapped[0]), i;
1264 for (i = 1; i < wrapped.length; i++) {
1265 newChild.append(wrapped[i]);
1267 firstChild.replace(newChild);
1268 newChild.prepend(firstChild);
1274 * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
1277 * @return {String} The filtered HTML
1279 Y.Selection.unfilter = function() {
1280 var nodes = Y.all('body [class]'),
1281 html = '', nons, ids,
1282 body = Y.one('body');
1285 nodes.each(function(n) {
1286 if (n.hasClass(n._yuid)) {
1288 n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
1289 n.removeClass(n._yuid);
1290 if (n.getAttribute('class') === '') {
1291 n.removeAttribute('class');
1296 nons = Y.all('.yui-non');
1297 nons.each(function(n) {
1298 if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') {
1301 n.removeClass('yui-non').removeClass('yui-skip');
1305 ids = Y.all('body [id]');
1306 ids.each(function(n) {
1307 if (n.get('id').indexOf('yui_3_') === 0) {
1308 n.removeAttribute('id');
1309 n.removeAttribute('_yuid');
1314 html = body.get('innerHTML');
1317 Y.all('.hr').addClass('yui-skip').addClass('yui-non');
1320 nodes.each(function(n) {
1321 n.addClass(n._yuid);
1322 n.setStyle(FONT_FAMILY, '');
1323 if (n.getAttribute('style') === '') {
1324 n.removeAttribute('style');
1332 * Resolve a node from the selection object and return a Node instance
1335 * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
1336 * @return {Node} The Resolved node
1338 Y.Selection.resolve = function(n) {
1339 if (n && n.nodeType === 3) {
1340 //Adding a try/catch here because in rare occasions IE will
1341 //Throw a error accessing the parentNode of a stranded text node.
1342 //In the case of Ctrl+Z (Undo)
1353 * Returns the innerHTML of a node with all HTML tags removed.
1356 * @param {Node} node The Node instance to remove the HTML from
1357 * @return {String} The string of text
1359 Y.Selection.getText = function(node) {
1360 var txt = node.get('innerHTML').replace(Y.Selection.REG_NOHTML, '');
1361 //Clean out the cursor subs to see if the Node is empty
1362 txt = txt.replace('<span><br></span>', '').replace('<br>', '');
1366 //Y.Selection.DEFAULT_BLOCK_TAG = 'div';
1367 Y.Selection.DEFAULT_BLOCK_TAG = 'p';
1370 * The selector to use when looking for Nodes to cache the value of: [style],font[face]
1374 Y.Selection.ALL = '[style],font[face]';
1377 * The selector to use when looking for block level items.
1381 Y.Selection.BLOCKS = 'p,div,ul,ol,table,style';
1383 * The temporary fontname applied to a selection to retrieve their values: yui-tmp
1387 Y.Selection.TMP = 'yui-tmp';
1389 * The default tag to use when creating elements: span
1391 * @property DEFAULT_TAG
1393 Y.Selection.DEFAULT_TAG = 'span';
1396 * The id of the outer cursor wrapper
1398 * @property DEFAULT_TAG
1400 Y.Selection.CURID = 'yui-cursor';
1403 * The id used to wrap the inner space of the cursor position
1405 * @property CUR_WRAPID
1407 Y.Selection.CUR_WRAPID = 'yui-cursor-wrapper';
1410 * The default HTML used to focus the cursor..
1414 Y.Selection.CURSOR = '<span><br class="yui-cursor"></span>';
1416 Y.Selection.hasCursor = function() {
1417 var cur = Y.all('#' + Y.Selection.CUR_WRAPID);
1422 * Called from Editor keydown to remove the "extra" space before the cursor.
1424 * @method cleanCursor
1426 Y.Selection.cleanCursor = function() {
1427 var cur, sel = 'br.yui-cursor';
1430 cur.each(function(b) {
1431 var c = b.get('parentNode.parentNode.childNodes'), html;
1435 html = Y.Selection.getText(c.item(0));
1443 var cur = Y.all('#' + Y.Selection.CUR_WRAPID);
1445 cur.each(function(c) {
1446 var html = c.get('innerHTML');
1447 if (html == ' ' || html == '<br>') {
1448 if (c.previous() || c.next()) {
1457 Y.Selection.prototype = {
1465 * Flag to show if the range is collapsed or not
1466 * @property isCollapsed
1471 * A Node instance of the parentNode of the anchorNode of the range
1472 * @property anchorNode
1477 * The offset from the range object
1478 * @property anchorOffset
1483 * A Node instance of the actual textNode of the range.
1484 * @property anchorTextNode
1487 anchorTextNode: null,
1489 * A Node instance of the parentNode of the focusNode of the range
1490 * @property focusNode
1495 * The offset from the range object
1496 * @property focusOffset
1501 * A Node instance of the actual textNode of the range.
1502 * @property focusTextNode
1505 focusTextNode: null,
1507 * The actual Selection/Range object
1508 * @property _selection
1513 * Wrap an element, with another element
1516 * @param {HTMLElement} n The node to wrap
1517 * @param {String} tag The tag to use when creating the new element.
1518 * @return {HTMLElement} The wrapped node
1520 _wrap: function(n, tag) {
1521 var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
1522 tmp.set(INNER_HTML, n.get(INNER_HTML));
1523 n.set(INNER_HTML, '');
1525 return Y.Node.getDOMNode(tmp);
1528 * Swap an element, with another element
1531 * @param {HTMLElement} n The node to swap
1532 * @param {String} tag The tag to use when creating the new element.
1533 * @return {HTMLElement} The new node
1535 _swap: function(n, tag) {
1536 var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
1537 tmp.set(INNER_HTML, n.get(INNER_HTML));
1539 return Y.Node.getDOMNode(tmp);
1542 * Get all the nodes in the current selection. This method will actually perform a filter first.
1543 * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
1544 * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
1545 * @method getSelected
1546 * @return {NodeList} A NodeList of all items in the selection.
1548 getSelected: function() {
1549 Y.Selection.filter();
1550 Y.config.doc.execCommand('fontname', null, Y.Selection.TMP);
1551 var nodes = Y.all(Y.Selection.ALL),
1554 nodes.each(function(n, k) {
1555 if (n.getStyle(FONT_FAMILY) == Y.Selection.TMP) {
1556 n.setStyle(FONT_FAMILY, '');
1557 n.removeAttribute('face');
1558 if (n.getAttribute('style') === '') {
1559 n.removeAttribute('style');
1561 if (!n.test('body')) {
1562 items.push(Y.Node.getDOMNode(nodes.item(k)));
1566 return Y.all(items);
1569 * Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
1570 * @method insertContent
1571 * @param {String} html The HTML to insert.
1572 * @return {Node} The inserted Node.
1574 insertContent: function(html) {
1575 return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
1578 * Insert HTML at the current cursor position, this method gives you control over the text node to insert into and the offset where to put it.
1579 * @method insertAtCursor
1580 * @param {String} html The HTML to insert.
1581 * @param {Node} node The text node to break when inserting.
1582 * @param {Number} offset The left offset of the text node to break and insert the new content.
1583 * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
1584 * @return {Node} The inserted Node.
1586 insertAtCursor: function(html, node, offset, collapse) {
1587 var cur = Y.Node.create('<' + Y.Selection.DEFAULT_TAG + ' class="yui-non"></' + Y.Selection.DEFAULT_TAG + '>'),
1588 inHTML, txt, txt2, newNode, range = this.createRange(), b;
1590 if (node && node.test('body')) {
1591 b = Y.Node.create('<span></span>');
1597 if (range.pasteHTML) {
1598 newNode = Y.Node.create(html);
1600 range.pasteHTML('<span id="rte-insert"></span>');
1602 inHTML = Y.one('#rte-insert');
1604 inHTML.set('id', '');
1605 inHTML.replace(newNode);
1608 Y.on('available', function() {
1609 inHTML.set('id', '');
1610 inHTML.replace(newNode);
1614 //TODO using Y.Node.create here throws warnings & strips first white space character
1615 //txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
1616 //txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
1618 inHTML = node.get(textContent);
1620 txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
1621 txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
1623 node.replace(txt, node);
1624 newNode = Y.Node.create(html);
1625 if (newNode.get('nodeType') === 11) {
1626 b = Y.Node.create('<span></span>');
1630 txt.insert(newNode, 'after');
1631 //if (txt2 && txt2.get('length')) {
1633 newNode.insert(cur, 'after');
1634 cur.insert(txt2, 'after');
1635 this.selectNode(cur, collapse);
1638 if (node.get('nodeType') === 3) {
1639 node = node.get('parentNode');
1641 newNode = Y.Node.create(html);
1642 html = node.get('innerHTML').replace(/\n/gi, '');
1643 if (html === '' || html === '<br>') {
1644 node.append(newNode);
1646 if (newNode.get('parentNode')) {
1647 node.insert(newNode, 'before');
1649 Y.one('body').prepend(newNode);
1652 if (node.get('firstChild').test('br')) {
1653 node.get('firstChild').remove();
1660 * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
1661 * @method wrapContent
1662 * @param {String} tag The tag to wrap all selected items with.
1663 * @return {NodeList} A NodeList of all items in the selection.
1665 wrapContent: function(tag) {
1666 tag = (tag) ? tag : Y.Selection.DEFAULT_TAG;
1668 if (!this.isCollapsed) {
1669 var items = this.getSelected(),
1670 changed = [], range, last, first, range2;
1672 items.each(function(n, k) {
1673 var t = n.get('tagName').toLowerCase();
1675 changed.push(this._swap(items.item(k), tag));
1677 changed.push(this._wrap(items.item(k), tag));
1681 range = this.createRange();
1683 last = changed[changed.length - 1];
1684 if (this._selection.removeAllRanges) {
1685 range.setStart(changed[0], 0);
1686 range.setEnd(last, last.childNodes.length);
1687 this._selection.removeAllRanges();
1688 this._selection.addRange(range);
1690 range.moveToElementText(Y.Node.getDOMNode(first));
1691 range2 = this.createRange();
1692 range2.moveToElementText(Y.Node.getDOMNode(last));
1693 range.setEndPoint('EndToEnd', range2);
1697 changed = Y.all(changed);
1706 * Find and replace a string inside a text node and replace it with HTML focusing the node after
1707 * to allow you to continue to type.
1709 * @param {String} se The string to search for.
1710 * @param {String} re The string of HTML to replace it with.
1711 * @return {Node} The node inserted.
1713 replace: function(se,re) {
1714 var range = this.createRange(), node, txt, index, newNode;
1716 if (range.getBookmark) {
1717 index = range.getBookmark();
1718 txt = this.anchorNode.get('innerHTML').replace(se, re);
1719 this.anchorNode.set('innerHTML', txt);
1720 range.moveToBookmark(index);
1721 newNode = Y.one(range.parentElement());
1723 node = this.anchorTextNode;
1724 txt = node.get(textContent);
1725 index = txt.indexOf(se);
1727 txt = txt.replace(se, '');
1728 node.set(textContent, txt);
1729 newNode = this.insertAtCursor(re, node, index, true);
1734 * Destroy the range.
1737 * @return {Y.Selection}
1739 remove: function() {
1740 this._selection.removeAllRanges();
1744 * Wrapper for the different range creation methods.
1745 * @method createRange
1746 * @return {RangeObject}
1748 createRange: function() {
1749 if (Y.config.doc.selection) {
1750 return Y.config.doc.selection.createRange();
1752 return Y.config.doc.createRange();
1756 * Select a Node (hilighting it).
1757 * @method selectNode
1758 * @param {Node} node The node to select
1759 * @param {Boolean} collapse Should the range be collapsed after insertion. default: false
1761 * @return {Y.Selection}
1763 selectNode: function(node, collapse, end) {
1768 node = Y.Node.getDOMNode(node);
1769 var range = this.createRange();
1770 if (range.selectNode) {
1771 range.selectNode(node);
1772 this._selection.removeAllRanges();
1773 this._selection.addRange(range);
1776 this._selection.collapse(node, end);
1778 this._selection.collapse(node, 0);
1782 if (node.nodeType === 3) {
1783 node = node.parentNode;
1786 range.moveToElementText(node);
1789 range.collapse(((end) ? false : true));
1796 * Put a placeholder in the DOM at the current cursor position.
1800 setCursor: function() {
1801 this.removeCursor(false);
1802 return this.insertContent(Y.Selection.CURSOR);
1805 * Get the placeholder in the DOM at the current cursor position.
1809 getCursor: function() {
1810 return Y.all('#' + Y.Selection.CURID);
1813 * Remove the cursor placeholder from the DOM.
1814 * @method removeCursor
1815 * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor.
1818 removeCursor: function(keep) {
1819 var cur = this.getCursor();
1822 cur.removeAttribute('id');
1823 cur.set('innerHTML', '<br class="yui-cursor">');
1831 * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor
1832 * @method focusCursor
1835 focusCursor: function(collapse, end) {
1836 if (collapse !== false) {
1839 if (end !== false) {
1842 var cur = this.removeCursor(true);
1844 cur.each(function(c) {
1845 this.selectNode(c, collapse, end);
1850 * Generic toString for logging.
1854 toString: function() {
1855 return 'Selection Object';
1860 }, '3.3.0' ,{requires:['node'], skinnable:false});
1861 YUI.add('exec-command', function(Y) {
1865 * Plugin for the frame module to handle execCommands for Editor
1867 * @submodule exec-command
1870 * Plugin for the frame module to handle execCommands for Editor
1871 * @class Plugin.ExecCommand
1875 var ExecCommand = function() {
1876 ExecCommand.superclass.constructor.apply(this, arguments);
1879 Y.extend(ExecCommand, Y.Base, {
1881 * An internal reference to the keyCode of the last key that was pressed.
1883 * @property _lastKey
1887 * An internal reference to the instance of the frame plugged into.
1893 * Execute a command on the frame's document.
1895 * @param {String} action The action to perform (bold, italic, fontname)
1896 * @param {String} value The optional value (helvetica)
1897 * @return {Node/NodeList} Should return the Node/Nodelist affected
1899 command: function(action, value) {
1900 var fn = ExecCommand.COMMANDS[action];
1903 if (action !== 'insertbr') {
1904 Y.later(0, this, function() {
1905 var inst = this.getInstance();
1906 if (inst && inst.Selection) {
1907 inst.Selection.cleanCursor();
1914 return fn.call(this, action, value);
1916 return this._command(action, value);
1920 * The private version of execCommand that doesn't filter for overrides.
1923 * @param {String} action The action to perform (bold, italic, fontname)
1924 * @param {String} value The optional value (helvetica)
1926 _command: function(action, value) {
1927 var inst = this.getInstance();
1930 inst.config.doc.execCommand('styleWithCSS', null, 1);
1933 inst.config.doc.execCommand('useCSS', null, 0);
1937 inst.config.doc.execCommand(action, null, value);
1942 * Get's the instance of YUI bound to the parent frame
1943 * @method getInstance
1944 * @return {YUI} The YUI instance bound to the parent frame
1946 getInstance: function() {
1948 this._inst = this.get('host').getInstance();
1952 initializer: function() {
1953 Y.mix(this.get('host'), {
1954 execCommand: function(action, value) {
1955 return this.exec.command(action, value);
1957 _execCommand: function(action, value) {
1958 return this.exec._command(action, value);
1962 this.get('host').on('dom:keypress', Y.bind(function(e) {
1963 this._lastKey = e.keyCode;
1972 NAME: 'execCommand',
1985 * Static object literal of execCommand overrides
1986 * @property COMMANDS
1991 * Wraps the content with a new element of type (tag)
1992 * @method COMMANDS.wrap
1994 * @param {String} cmd The command executed: wrap
1995 * @param {String} tag The tag to wrap the selection with
1996 * @return {NodeList} NodeList of the items touched by this command.
1998 wrap: function(cmd, tag) {
1999 var inst = this.getInstance();
2000 return (new inst.Selection()).wrapContent(tag);
2003 * Inserts the provided HTML at the cursor, should be a single element.
2004 * @method COMMANDS.inserthtml
2006 * @param {String} cmd The command executed: inserthtml
2007 * @param {String} html The html to insert
2008 * @return {Node} Node instance of the item touched by this command.
2010 inserthtml: function(cmd, html) {
2011 var inst = this.getInstance();
2012 if (inst.Selection.hasCursor() || Y.UA.ie) {
2013 return (new inst.Selection()).insertContent(html);
2015 this._command('inserthtml', html);
2019 * Inserts the provided HTML at the cursor, and focuses the cursor afterwards.
2020 * @method COMMANDS.insertandfocus
2022 * @param {String} cmd The command executed: insertandfocus
2023 * @param {String} html The html to insert
2024 * @return {Node} Node instance of the item touched by this command.
2026 insertandfocus: function(cmd, html) {
2027 var inst = this.getInstance(), out, sel;
2028 if (inst.Selection.hasCursor()) {
2029 html += inst.Selection.CURSOR;
2030 out = this.command('inserthtml', html);
2031 sel = new inst.Selection();
2032 sel.focusCursor(true, true);
2034 this.command('inserthtml', html);
2039 * Inserts a BR at the current cursor position
2040 * @method COMMANDS.insertbr
2042 * @param {String} cmd The command executed: insertbr
2044 insertbr: function(cmd) {
2045 var inst = this.getInstance(), cur,
2046 sel = new inst.Selection();
2049 cur = sel.getCursor();
2050 cur.insert('<br>', 'before');
2051 sel.focusCursor(true, false);
2052 return ((cur && cur.previous) ? cur.previous() : null);
2055 * Inserts an image at the cursor position
2056 * @method COMMANDS.insertimage
2058 * @param {String} cmd The command executed: insertimage
2059 * @param {String} img The url of the image to be inserted
2060 * @return {Node} Node instance of the item touched by this command.
2062 insertimage: function(cmd, img) {
2063 return this.command('inserthtml', '<img src="' + img + '">');
2066 * Add a class to all of the elements in the selection
2067 * @method COMMANDS.addclass
2069 * @param {String} cmd The command executed: addclass
2070 * @param {String} cls The className to add
2071 * @return {NodeList} NodeList of the items touched by this command.
2073 addclass: function(cmd, cls) {
2074 var inst = this.getInstance();
2075 return (new inst.Selection()).getSelected().addClass(cls);
2078 * Remove a class from all of the elements in the selection
2079 * @method COMMANDS.removeclass
2081 * @param {String} cmd The command executed: removeclass
2082 * @param {String} cls The className to remove
2083 * @return {NodeList} NodeList of the items touched by this command.
2085 removeclass: function(cmd, cls) {
2086 var inst = this.getInstance();
2087 return (new inst.Selection()).getSelected().removeClass(cls);
2090 * Adds a forecolor to the current selection, or creates a new element and applies it
2091 * @method COMMANDS.forecolor
2093 * @param {String} cmd The command executed: forecolor
2094 * @param {String} val The color value to apply
2095 * @return {NodeList} NodeList of the items touched by this command.
2097 forecolor: function(cmd, val) {
2098 var inst = this.getInstance(),
2099 sel = new inst.Selection(), n;
2102 this._command('useCSS', false);
2104 if (inst.Selection.hasCursor()) {
2105 if (sel.isCollapsed) {
2106 if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) {
2107 sel.anchorNode.setStyle('color', val);
2110 n = this.command('inserthtml', '<span style="color: ' + val + '">' + inst.Selection.CURSOR + '</span>');
2111 sel.focusCursor(true, true);
2115 return this._command(cmd, val);
2118 this._command(cmd, val);
2122 * Adds a background color to the current selection, or creates a new element and applies it
2123 * @method COMMANDS.backcolor
2125 * @param {String} cmd The command executed: backcolor
2126 * @param {String} val The color value to apply
2127 * @return {NodeList} NodeList of the items touched by this command.
2129 backcolor: function(cmd, val) {
2130 var inst = this.getInstance(),
2131 sel = new inst.Selection(), n;
2133 if (Y.UA.gecko || Y.UA.opera) {
2134 cmd = 'hilitecolor';
2137 this._command('useCSS', false);
2139 if (inst.Selection.hasCursor()) {
2140 if (sel.isCollapsed) {
2141 if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) {
2142 sel.anchorNode.setStyle('backgroundColor', val);
2145 n = this.command('inserthtml', '<span style="background-color: ' + val + '">' + inst.Selection.CURSOR + '</span>');
2146 sel.focusCursor(true, true);
2150 return this._command(cmd, val);
2153 this._command(cmd, val);
2157 * Sugar method, calles backcolor
2158 * @method COMMANDS.hilitecolor
2160 * @param {String} cmd The command executed: backcolor
2161 * @param {String} val The color value to apply
2162 * @return {NodeList} NodeList of the items touched by this command.
2164 hilitecolor: function() {
2165 return ExecCommand.COMMANDS.backcolor.apply(this, arguments);
2168 * Adds a font name to the current selection, or creates a new element and applies it
2169 * @method COMMANDS.fontname
2171 * @param {String} cmd The command executed: fontname
2172 * @param {String} val The font name to apply
2173 * @return {NodeList} NodeList of the items touched by this command.
2175 fontname: function(cmd, val) {
2176 this._command('fontname', val);
2177 var inst = this.getInstance(),
2178 sel = new inst.Selection();
2180 if (sel.isCollapsed && (this._lastKey != 32)) {
2181 if (sel.anchorNode.test('font')) {
2182 sel.anchorNode.set('face', val);
2187 * Adds a fontsize to the current selection, or creates a new element and applies it
2188 * @method COMMANDS.fontsize
2190 * @param {String} cmd The command executed: fontsize
2191 * @param {String} val The font size to apply
2192 * @return {NodeList} NodeList of the items touched by this command.
2194 fontsize: function(cmd, val) {
2195 this._command('fontsize', val);
2197 var inst = this.getInstance(),
2198 sel = new inst.Selection();
2200 if (sel.isCollapsed && sel.anchorNode && (this._lastKey != 32)) {
2202 if (sel.anchorNode.getStyle('lineHeight')) {
2203 sel.anchorNode.setStyle('lineHeight', '');
2206 if (sel.anchorNode.test('font')) {
2207 sel.anchorNode.set('size', val);
2208 } else if (Y.UA.gecko) {
2209 var p = sel.anchorNode.ancestor(inst.Selection.DEFAULT_BLOCK_TAG);
2211 p.setStyle('fontSize', '');
2220 * This method is meant to normalize IE's in ability to exec the proper command on elements with CSS styling.
2223 * @param {String} cmd The command to execute
2224 * @param {String} tag The tag to create
2225 * @param {String} rule The rule that we are looking for.
2227 var fixIETags = function(cmd, tag, rule) {
2228 var inst = this.getInstance(),
2229 doc = inst.config.doc,
2230 sel = doc.selection.createRange(),
2231 o = doc.queryCommandValue(cmd),
2232 html, reg, m, p, d, s, c;
2235 html = sel.htmlText;
2236 reg = new RegExp(rule, 'g');
2237 m = html.match(reg);
2240 html = html.replace(rule + ';', '').replace(rule, '');
2242 sel.pasteHTML('<var id="yui-ie-bs">');
2244 p = doc.getElementById('yui-ie-bs');
2245 d = doc.createElement('div');
2246 s = doc.createElement(tag);
2249 if (p.parentNode !== inst.config.doc.body) {
2255 p.parentNode.replaceChild(s, p);
2257 Y.each(c, function(f) {
2261 sel.moveToElementText(s);
2269 ExecCommand.COMMANDS.bold = function() {
2270 fixIETags.call(this, 'bold', 'b', 'FONT-WEIGHT: bold');
2272 ExecCommand.COMMANDS.italic = function() {
2273 fixIETags.call(this, 'italic', 'i', 'FONT-STYLE: italic');
2275 ExecCommand.COMMANDS.underline = function() {
2276 fixIETags.call(this, 'underline', 'u', 'TEXT-DECORATION: underline');
2280 Y.namespace('Plugin');
2281 Y.Plugin.ExecCommand = ExecCommand;
2285 }, '3.3.0' ,{requires:['frame'], skinnable:false});
2286 YUI.add('editor-tab', function(Y) {
2289 * Handles tab and shift-tab indent/outdent support.
2291 * @submodule editor-tab
2294 * Handles tab and shift-tab indent/outdent support.
2295 * @class Plugin.EditorTab
2300 var EditorTab = function() {
2301 EditorTab.superclass.constructor.apply(this, arguments);
2304 Y.extend(EditorTab, Y.Base, {
2306 * Listener for host's nodeChange event and captures the tabkey interaction.
2308 * @method _onNodeChange
2309 * @param {Event} e The Event facade passed from the host.
2311 _onNodeChange: function(e) {
2312 var action = 'indent';
2314 if (e.changedType === 'tab') {
2315 if (!e.changedNode.test('li, li *')) {
2316 e.changedEvent.halt();
2318 if (e.changedEvent.shiftKey) {
2322 this.get(HOST).execCommand(action, '');
2326 initializer: function() {
2327 this.get(HOST).on('nodeChange', Y.bind(this._onNodeChange, this));
2350 Y.namespace('Plugin');
2352 Y.Plugin.EditorTab = EditorTab;
2355 }, '3.3.0' ,{requires:['editor-base'], skinnable:false});
2356 YUI.add('createlink-base', function(Y) {
2359 * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
2361 * @submodule createlink-base
2364 * Adds prompt style link creation. Adds an override for the <a href="Plugin.ExecCommand.html#method_COMMANDS.createlink">createlink execCommand</a>.
2365 * @class Plugin.CreateLinkBase
2369 var CreateLinkBase = {};
2371 * Strings used by the plugin
2375 CreateLinkBase.STRINGS = {
2377 * String used for the Prompt
2381 PROMPT: 'Please enter the URL for the link to point to:',
2383 * String used as the default value of the Prompt
2390 Y.namespace('Plugin');
2391 Y.Plugin.CreateLinkBase = CreateLinkBase;
2393 Y.mix(Y.Plugin.ExecCommand.COMMANDS, {
2395 * Override for the createlink method from the <a href="Plugin.CreateLinkBase.html">CreateLinkBase</a> plugin.
2397 * @method COMMANDS.createlink
2399 * @param {String} cmd The command executed: createlink
2400 * @return {Node} Node instance of the item touched by this command.
2402 createlink: function(cmd) {
2403 var inst = this.get('host').getInstance(), out, a, sel, holder,
2404 url = prompt(CreateLinkBase.STRINGS.PROMPT, CreateLinkBase.STRINGS.DEFAULT);
2407 holder = inst.config.doc.createElement('div');
2408 url = inst.config.doc.createTextNode(url);
2409 holder.appendChild(url);
2410 url = holder.innerHTML;
2413 this.get('host')._execCommand(cmd, url);
2414 sel = new inst.Selection();
2415 out = sel.getSelected();
2416 if (!sel.isCollapsed && out.size()) {
2417 //We have a selection
2418 a = out.item(0).one('a');
2420 out.item(0).replace(a);
2423 if (a.get('parentNode').test('span')) {
2424 if (a.get('parentNode').one('br.yui-cursor')) {
2425 a.get('parentNode').insert(a, 'before');
2430 //No selection, insert a new node..
2431 this.get('host').execCommand('inserthtml', '<a href="' + url + '">' + url + '</a>');
2440 }, '3.3.0' ,{requires:['editor-base'], skinnable:false});
2441 YUI.add('editor-base', function(Y) {
2445 * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
2447 * @submodule editor-base
2450 * Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
2457 var EditorBase = function() {
2458 EditorBase.superclass.constructor.apply(this, arguments);
2459 }, LAST_CHILD = ':last-child', BODY = 'body';
2461 Y.extend(EditorBase, Y.Base, {
2463 * Internal reference to the Y.Frame instance
2467 initializer: function() {
2468 var frame = new Y.Frame({
2470 title: EditorBase.STRINGS.title,
2471 use: EditorBase.USE,
2472 dir: this.get('dir'),
2473 extracss: this.get('extracss'),
2474 linkedcss: this.get('linkedcss'),
2475 defaultblock: this.get('defaultblock'),
2477 }).plug(Y.Plugin.ExecCommand);
2480 frame.after('ready', Y.bind(this._afterFrameReady, this));
2481 frame.addTarget(this);
2485 this.publish('nodeChange', {
2488 defaultFn: this._defNodeChangeFn
2491 //this.plug(Y.Plugin.EditorPara);
2493 destructor: function() {
2494 this.frame.destroy();
2499 * Copy certain styles from one node instance to another (used for new paragraph creation mainly)
2500 * @method copyStyles
2501 * @param {Node} from The Node instance to copy the styles from
2502 * @param {Node} to The Node instance to copy the styles to
2504 copyStyles: function(from, to) {
2505 if (from.test('a')) {
2506 //Don't carry the A styles
2509 var styles = ['color', 'fontSize', 'fontFamily', 'backgroundColor', 'fontStyle' ],
2512 Y.each(styles, function(v) {
2513 newStyles[v] = from.getStyle(v);
2515 if (from.ancestor('b,strong')) {
2516 newStyles.fontWeight = 'bold';
2518 if (from.ancestor('u')) {
2519 if (!newStyles.textDecoration) {
2520 newStyles.textDecoration = 'underline';
2523 to.setStyles(newStyles);
2526 * Holder for the selection bookmark in IE.
2527 * @property _lastBookmark
2530 _lastBookmark: null,
2532 * Resolves the e.changedNode in the nodeChange event if it comes from the document. If
2533 * the event came from the document, it will get the last child of the last child of the document
2534 * and return that instead.
2535 * @method _resolveChangedNode
2536 * @param {Node} n The node to resolve
2539 _resolveChangedNode: function(n) {
2540 var inst = this.getInstance(), lc, lc2, found;
2541 if (inst && n && n.test('html')) {
2542 lc = inst.one(BODY).one(LAST_CHILD);
2545 lc2 = lc.one(LAST_CHILD);
2556 if (lc.test('br')) {
2557 if (lc.previous()) {
2560 lc = lc.get('parentNode');
2572 * The default handler for the nodeChange event.
2573 * @method _defNodeChangeFn
2574 * @param {Event} e The event
2577 _defNodeChangeFn: function(e) {
2578 var startTime = (new Date()).getTime();
2579 var inst = this.getInstance(), sel, cur,
2580 btag = inst.Selection.DEFAULT_BLOCK_TAG;
2584 sel = inst.config.doc.selection.createRange();
2585 if (sel.getBookmark) {
2586 this._lastBookmark = sel.getBookmark();
2591 e.changedNode = this._resolveChangedNode(e.changedNode);
2595 * This whole method needs to be fixed and made more dynamic.
2596 * Maybe static functions for the e.changeType and an object bag
2597 * to walk through and filter to pass off the event to before firing..
2600 switch (e.changedType) {
2603 if (!EditorBase.NC_KEYS[e.changedEvent.keyCode] && !e.changedEvent.shiftKey && !e.changedEvent.ctrlKey && (e.changedEvent.keyCode !== 13)) {
2604 //inst.later(100, inst, inst.Selection.cleanCursor);
2609 if (!e.changedNode.test('li, li *') && !e.changedEvent.shiftKey) {
2610 e.changedEvent.frameEvent.preventDefault();
2612 this.execCommand('inserttext', '\t');
2613 } else if (Y.UA.gecko) {
2614 this.frame.exec._command('inserthtml', '<span> </span>');
2615 } else if (Y.UA.ie) {
2616 sel = new inst.Selection();
2617 sel._selection.pasteHTML(EditorBase.TABKEY);
2622 if (Y.UA.webkit && e.commands && (e.commands.indent || e.commands.outdent)) {
2624 * When executing execCommand 'indent or 'outdent' Webkit applies
2625 * a class to the BLOCKQUOTE that adds left/right margin to it
2626 * This strips that style so it is just a normal BLOCKQUOTE
2628 var bq = inst.all('.webkit-indent-blockquote');
2630 bq.setStyle('margin', '');
2634 var changed = this.getDomPath(e.changedNode, false),
2635 cmds = {}, family, fsize, classes = [],
2636 fColor = '', bColor = '';
2642 Y.each(changed, function(el) {
2643 var tag = el.tagName.toLowerCase(),
2644 cmd = EditorBase.TAG2CMD[tag];
2650 //Bold and Italic styles
2651 var s = el.currentStyle || el.style;
2652 if ((''+s.fontWeight) == 'bold') { //Cast this to a string
2656 if (s.fontWeight > 400) {
2660 if (s.fontStyle == 'italic') {
2663 if (s.textDecoration == 'underline') {
2666 if (s.textDecoration == 'line-through') {
2667 cmds.strikethrough = 1;
2670 var n = inst.one(el);
2671 if (n.getStyle('fontFamily')) {
2672 var family2 = n.getStyle('fontFamily').split(',')[0].toLowerCase();
2677 family = family.replace(/'/g, '').replace(/"/g, '');
2681 fsize = EditorBase.NORMALIZE_FONTSIZE(n);
2684 var cls = el.className.split(' ');
2686 Y.each(cls, function(v) {
2687 if (v !== '' && (v.substr(0, 4) !== 'yui_')) {
2692 fColor = EditorBase.FILTER_RGB(n.getStyle('color'));
2693 var bColor2 = EditorBase.FILTER_RGB(s.backgroundColor);
2694 if (bColor2 !== 'transparent') {
2695 if (bColor2 !== '') {
2702 e.dompath = inst.all(changed);
2703 e.classNames = classes;
2706 //TODO Dont' like this, not dynamic enough..
2707 if (!e.fontFamily) {
2708 e.fontFamily = family;
2714 e.fontColor = fColor;
2716 if (!e.backgroundColor) {
2717 e.backgroundColor = bColor;
2720 var endTime = (new Date()).getTime();
2723 * Walk the dom tree from this node up to body, returning a reversed array of parents.
2724 * @method getDomPath
2725 * @param {Node} node The Node to start from
2727 getDomPath: function(node, nodeList) {
2728 var domPath = [], domNode,
2729 inst = this.frame.getInstance();
2731 domNode = inst.Node.getDOMNode(node);
2732 //return inst.all(domNode);
2734 while (domNode !== null) {
2736 if ((domNode === inst.config.doc.documentElement) || (domNode === inst.config.doc) || !domNode.tagName) {
2741 if (!inst.DOM.inDoc(domNode)) {
2746 //Check to see if we get el.nodeName and nodeType
2747 if (domNode.nodeName && domNode.nodeType && (domNode.nodeType == 1)) {
2748 domPath.push(domNode);
2751 if (domNode == inst.config.doc.body) {
2756 domNode = domNode.parentNode;
2760 while (node !== null) {
2761 if (node.test('html') || node.test('doc') || !node.get('tagName')) {
2765 if (!node.inDoc()) {
2769 //Check to see if we get el.nodeName and nodeType
2770 if (node.get('nodeName') && node.get('nodeType') && (node.get('nodeType') == 1)) {
2771 domPath.push(inst.Node.getDOMNode(node));
2774 if (node.test('body')) {
2779 node = node.get('parentNode');
2783 if (domPath.length === 0) {
2784 domPath[0] = inst.config.doc.body;
2788 return inst.all(domPath.reverse());
2790 return domPath.reverse();
2795 * After frame ready, bind mousedown & keyup listeners
2796 * @method _afterFrameReady
2799 _afterFrameReady: function() {
2800 var inst = this.frame.getInstance();
2802 this.frame.on('dom:mouseup', Y.bind(this._onFrameMouseUp, this));
2803 this.frame.on('dom:mousedown', Y.bind(this._onFrameMouseDown, this));
2804 this.frame.on('dom:keydown', Y.bind(this._onFrameKeyDown, this));
2807 this.frame.on('dom:activate', Y.bind(this._onFrameActivate, this));
2808 this.frame.on('dom:beforedeactivate', Y.bind(this._beforeFrameDeactivate, this));
2810 this.frame.on('dom:keyup', Y.bind(this._onFrameKeyUp, this));
2811 this.frame.on('dom:keypress', Y.bind(this._onFrameKeyPress, this));
2813 inst.Selection.filter();
2817 * Caches the current cursor position in IE.
2818 * @method _beforeFrameDeactivate
2821 _beforeFrameDeactivate: function() {
2822 var inst = this.getInstance(),
2823 sel = inst.config.doc.selection.createRange();
2825 if ((!sel.compareEndPoints('StartToEnd', sel))) {
2826 sel.pasteHTML('<var id="yui-ie-cursor">');
2830 * Moves the cached selection bookmark back so IE can place the cursor in the right place.
2831 * @method _onFrameActivate
2834 _onFrameActivate: function() {
2835 var inst = this.getInstance(),
2836 sel = new inst.Selection(),
2837 range = sel.createRange(),
2838 cur = inst.all('#yui-ie-cursor');
2841 cur.each(function(n) {
2843 range.moveToElementText(n._node);
2844 range.move('character', -1);
2845 range.move('character', 1);
2853 * Fires nodeChange event
2854 * @method _onFrameMouseUp
2857 _onFrameMouseUp: function(e) {
2858 this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mouseup', changedEvent: e.frameEvent });
2861 * Fires nodeChange event
2862 * @method _onFrameMouseDown
2865 _onFrameMouseDown: function(e) {
2866 this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mousedown', changedEvent: e.frameEvent });
2869 * Caches a copy of the selection for key events. Only creating the selection on keydown
2870 * @property _currentSelection
2873 _currentSelection: null,
2875 * Holds the timer for selection clearing
2876 * @property _currentSelectionTimer
2879 _currentSelectionTimer: null,
2881 * Flag to determine if we can clear the selection or not.
2882 * @property _currentSelectionClear
2885 _currentSelectionClear: null,
2887 * Fires nodeChange event
2888 * @method _onFrameKeyDown
2891 _onFrameKeyDown: function(e) {
2893 if (!this._currentSelection) {
2894 if (this._currentSelectionTimer) {
2895 this._currentSelectionTimer.cancel();
2897 this._currentSelectionTimer = Y.later(850, this, function() {
2898 this._currentSelectionClear = true;
2901 inst = this.frame.getInstance();
2902 sel = new inst.Selection(e);
2904 this._currentSelection = sel;
2906 sel = this._currentSelection;
2909 inst = this.frame.getInstance();
2910 sel = new inst.Selection();
2912 this._currentSelection = sel;
2914 if (sel && sel.anchorNode) {
2915 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keydown', changedEvent: e.frameEvent });
2916 if (EditorBase.NC_KEYS[e.keyCode]) {
2917 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode], changedEvent: e.frameEvent });
2918 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-down', changedEvent: e.frameEvent });
2923 * Fires nodeChange event
2924 * @method _onFrameKeyPress
2927 _onFrameKeyPress: function(e) {
2928 var sel = this._currentSelection;
2930 if (sel && sel.anchorNode) {
2931 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keypress', changedEvent: e.frameEvent });
2932 if (EditorBase.NC_KEYS[e.keyCode]) {
2933 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-press', changedEvent: e.frameEvent });
2938 * Fires nodeChange event for keyup on specific keys
2939 * @method _onFrameKeyUp
2942 _onFrameKeyUp: function(e) {
2943 var sel = this._currentSelection;
2945 if (sel && sel.anchorNode) {
2946 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keyup', selection: sel, changedEvent: e.frameEvent });
2947 if (EditorBase.NC_KEYS[e.keyCode]) {
2948 this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-up', selection: sel, changedEvent: e.frameEvent });
2951 if (this._currentSelectionClear) {
2952 this._currentSelectionClear = this._currentSelection = null;
2956 * Pass through to the frame.execCommand method
2957 * @method execCommand
2958 * @param {String} cmd The command to pass: inserthtml, insertimage, bold
2959 * @param {String} val The optional value of the command: Helvetica
2960 * @return {Node/NodeList} The Node or Nodelist affected by the command. Only returns on override commands, not browser defined commands.
2962 execCommand: function(cmd, val) {
2963 var ret = this.frame.execCommand(cmd, val),
2964 inst = this.frame.getInstance(),
2965 sel = new inst.Selection(), cmds = {},
2966 e = { changedNode: sel.anchorNode, changedType: 'execcommand', nodes: ret };
2973 e.backgroundColor = val;
2986 this.fire('nodeChange', e);
2991 * Get the YUI instance of the frame
2992 * @method getInstance
2993 * @return {YUI} The YUI instance bound to the frame.
2995 getInstance: function() {
2996 return this.frame.getInstance();
2999 * Renders the Y.Frame to the passed node.
3001 * @param {Selector/HTMLElement/Node} node The node to append the Editor to
3002 * @return {EditorBase}
3005 render: function(node) {
3006 this.frame.set('content', this.get('content'));
3007 this.frame.render(node);
3011 * Focus the contentWindow of the iframe
3013 * @param {Function} fn Callback function to execute after focus happens
3014 * @return {EditorBase}
3017 focus: function(fn) {
3018 this.frame.focus(fn);
3022 * Handles the showing of the Editor instance. Currently only handles the iframe
3024 * @return {EditorBase}
3032 * Handles the hiding of the Editor instance. Currently only handles the iframe
3034 * @return {EditorBase}
3042 * (Un)Filters the content of the Editor, cleaning YUI related code. //TODO better filtering
3043 * @method getContent
3044 * @return {String} The filtered content of the Editor
3046 getContent: function() {
3047 var html = '', inst = this.getInstance();
3048 if (inst && inst.Selection) {
3049 html = inst.Selection.unfilter();
3051 //Removing the _yuid from the objects in IE
3052 html = html.replace(/ _yuid="([^>]*)"/g, '');
3058 * @method NORMALIZE_FONTSIZE
3059 * @description Pulls the fontSize from a node, then checks for string values (x-large, x-small)
3060 * and converts them to pixel sizes. If the parsed size is different from the original, it calls
3061 * node.setStyle to update the node with a pixel size for normalization.
3063 NORMALIZE_FONTSIZE: function(n) {
3064 var size = n.getStyle('fontSize'), oSize = size;
3067 case '-webkit-xxx-large':
3089 if (oSize !== size) {
3090 n.setStyle('fontSize', size);
3097 * @description The HTML markup to use for the tabkey
3099 TABKEY: '<span class="tab"> </span>',
3102 * @method FILTER_RGB
3103 * @param String css The CSS string containing rgb(#,#,#);
3104 * @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00
3107 FILTER_RGB: function(css) {
3108 if (css.toLowerCase().indexOf('rgb') != -1) {
3109 var exp = new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)", "gi");
3110 var rgb = css.replace(exp, "$1,$2,$3,$4,$5").split(',');
3112 if (rgb.length == 5) {
3113 var r = parseInt(rgb[1], 10).toString(16);
3114 var g = parseInt(rgb[2], 10).toString(16);
3115 var b = parseInt(rgb[3], 10).toString(16);
3117 r = r.length == 1 ? '0' + r : r;
3118 g = g.length == 1 ? '0' + g : g;
3119 b = b.length == 1 ? '0' + b : b;
3121 css = "#" + r + g + b;
3129 * @description A hash table of tags to their execcomand's
3137 'sup': 'superscript',
3139 'img': 'insertimage',
3141 'ul' : 'insertunorderedlist',
3142 'ol' : 'insertorderedlist'
3145 * Hash table of keys to fire a nodeChange event for.
3166 * The default modules to use inside the Frame
3171 USE: ['substitute', 'node', 'selector-css3', 'selection', 'stylesheet'],
3173 * The Class Name: editorBase
3185 * Title of frame document: Rich Text Editor
3187 * @property STRINGS.title
3189 title: 'Rich Text Editor'
3193 * The content to load into the Editor Frame
3194 * @attribute content
3197 value: '<br class="yui-cursor">',
3198 setter: function(str) {
3199 if (str.substr(0, 1) === "\n") {
3200 str = str.substr(1);
3203 str = '<br class="yui-cursor">';
3207 str = '<br class="yui-cursor">';
3210 return this.frame.set('content', str);
3212 getter: function() {
3213 return this.frame.get('content');
3217 * The value of the dir attribute on the HTML element of the frame. Default: ltr
3225 * @attribute linkedcss
3226 * @description An array of url's to external linked style sheets
3231 setter: function(css) {
3233 this.frame.set('linkedcss', css);
3239 * @attribute extracss
3240 * @description A string of CSS to add to the Head of the Editor
3245 setter: function(css) {
3247 this.frame.set('extracss', css);
3253 * @attribute defaultblock
3254 * @description The default tag to use for block level items, defaults to: p
3263 Y.EditorBase = EditorBase;
3267 * @description Fired from mouseup & keyup.
3268 * @param {Event.Facade} event An Event Facade object with the following specific properties added:
3270 * <dt>changedEvent</dt><dd>The event that caused the nodeChange</dd>
3271 * <dt>changedNode</dt><dd>The node that was interacted with</dd>
3272 * <dt>changedType</dt><dd>The type of change: mousedown, mouseup, right, left, backspace, tab, enter, etc..</dd>
3273 * <dt>commands</dt><dd>The list of execCommands that belong to this change and the dompath that's associated with the changedNode</dd>
3274 * <dt>classNames</dt><dd>An array of classNames that are applied to the changedNode and all of it's parents</dd>
3275 * <dt>dompath</dt><dd>A sorted array of node instances that make up the DOM path from the changedNode to body.</dd>
3276 * <dt>backgroundColor</dt><dd>The cascaded backgroundColor of the changedNode</dd>
3277 * <dt>fontColor</dt><dd>The cascaded fontColor of the changedNode</dd>
3278 * <dt>fontFamily</dt><dd>The cascaded fontFamily of the changedNode</dd>
3279 * <dt>fontSize</dt><dd>The cascaded fontSize of the changedNode</dd>
3281 * @type {Event.Custom}
3286 * @description Fired after the frame is ready.
3287 * @param {Event.Facade} event An Event Facade object.
3288 * @type {Event.Custom}
3295 }, '3.3.0' ,{requires:['base', 'frame', 'node', 'exec-command', 'selection', 'editor-para'], skinnable:false});
3296 YUI.add('editor-lists', function(Y) {
3299 * Handles list manipulation inside the Editor. Adds keyboard manipulation and execCommand support. Adds overrides for the <a href="Plugin.ExecCommand.html#method_COMMANDS.insertorderedlist">insertorderedlist</a> and <a href="Plugin.ExecCommand.html#method_COMMANDS.insertunorderedlist">insertunorderedlist</a> execCommands.
3301 * @submodule editor-lists
3304 * Handles list manipulation inside the Editor. Adds keyboard manipulation and execCommand support. Adds overrides for the <a href="Plugin.ExecCommand.html#method_COMMANDS.insertorderedlist">insertorderedlist</a> and <a href="Plugin.ExecCommand.html#method_COMMANDS.insertunorderedlist">insertunorderedlist</a> execCommands.
3305 * @class Plugin.EditorLists
3310 var EditorLists = function() {
3311 EditorLists.superclass.constructor.apply(this, arguments);
3312 }, LI = 'li', OL = 'ol', UL = 'ul', HOST = 'host';
3314 Y.extend(EditorLists, Y.Base, {
3316 * Listener for host's nodeChange event and captures the tabkey interaction only when inside a list node.
3318 * @method _onNodeChange
3319 * @param {Event} e The Event facade passed from the host.
3321 _onNodeChange: function(e) {
3322 var inst = this.get(HOST).getInstance(), sel, li,
3323 newLi, newList, sTab, par, moved = false, tag, focusEnd = false;
3325 if (Y.UA.ie && e.changedType === 'enter') {
3326 if (e.changedNode.test(LI + ', ' + LI + ' *')) {
3327 e.changedEvent.halt();
3330 newLi = inst.Node.create('<' + LI + '>' + EditorLists.NON + '</' + LI + '>');
3333 li = li.ancestor(LI);
3335 li.insert(newLi, 'after');
3337 sel = new inst.Selection();
3338 sel.selectNode(newLi.get('firstChild'), true, false);
3341 if (e.changedType === 'tab') {
3342 if (e.changedNode.test(LI + ', ' + LI + ' *')) {
3343 e.changedEvent.halt();
3346 sTab = e.changedEvent.shiftKey;
3347 par = li.ancestor(OL + ',' + UL);
3351 if (par.get('tagName').toLowerCase() === OL) {
3356 li = li.ancestor(LI);
3359 if (li.ancestor(LI)) {
3360 li.ancestor(LI).insert(li, 'after');
3365 //li.setStyle('border', '1px solid red');
3366 if (li.previous(LI)) {
3367 newList = inst.Node.create('<' + tag + '></' + tag + '>');
3368 li.previous(LI).append(newList);
3376 li = li.ancestor(LI);
3378 li.all(EditorLists.REMOVE).remove();
3380 li = li.append(EditorLists.NON).one(EditorLists.NON_SEL);
3383 (new inst.Selection()).selectNode(li, true, focusEnd);
3387 initializer: function() {
3388 this.get(HOST).on('nodeChange', Y.bind(this._onNodeChange, this));
3392 * The non element placeholder, used for positioning the cursor and filling empty items
3396 NON: '<span class="yui-non"> </span>',
3398 * The selector query to get all non elements
3402 NON_SEL: 'span.yui-non',
3404 * The items to removed from a list when a list item is moved, currently removes BR nodes
3414 NAME: 'editorLists',
3429 Y.namespace('Plugin');
3431 Y.Plugin.EditorLists = EditorLists;
3433 Y.mix(Y.Plugin.ExecCommand.COMMANDS, {
3435 * Override for the insertunorderedlist method from the <a href="Plugin.EditorLists.html">EditorLists</a> plugin.
3437 * @method COMMANDS.insertunorderedlist
3439 * @param {String} cmd The command executed: insertunorderedlist
3440 * @return {Node} Node instance of the item touched by this command.
3442 insertunorderedlist: function(cmd) {
3443 var inst = this.get('host').getInstance(), out;
3444 this.get('host')._execCommand(cmd, '');
3447 * Override for the insertorderedlist method from the <a href="Plugin.EditorLists.html">EditorLists</a> plugin.
3449 * @method COMMANDS.insertorderedlist
3451 * @param {String} cmd The command executed: insertorderedlist
3452 * @return {Node} Node instance of the item touched by this command.
3454 insertorderedlist: function(cmd) {
3455 var inst = this.get('host').getInstance(), out;
3456 this.get('host')._execCommand(cmd, '');
3463 }, '3.3.0' ,{requires:['editor-base'], skinnable:false});
3464 YUI.add('editor-bidi', function(Y) {
3469 * Plugin for Editor to support BiDirectional (bidi) text operations.
3471 * @submodule editor-bidi
3474 * Plugin for Editor to support BiDirectional (bidi) text operations.
3475 * @class Plugin.EditorBidi
3481 var EditorBidi = function() {
3482 EditorBidi.superclass.constructor.apply(this, arguments);
3483 }, HOST = 'host', DIR = 'dir', BODY = 'BODY', NODE_CHANGE = 'nodeChange',
3484 B_C_CHANGE = 'bidiContextChange', FIRST_P = BODY + ' > p';
3486 Y.extend(EditorBidi, Y.Base, {
3488 * Place holder for the last direction when checking for a switch
3490 * @property lastDirection
3492 lastDirection: null,
3494 * Tells us that an initial bidi check has already been performed
3496 * @property firstEvent
3501 * Method checks to see if the direction of the text has changed based on a nodeChange event.
3503 * @method _checkForChange
3505 _checkForChange: function() {
3506 var host = this.get(HOST),
3507 inst = host.getInstance(),
3508 sel = new inst.Selection(),
3511 if (sel.isCollapsed) {
3512 node = EditorBidi.blockParent(sel.focusNode);
3513 direction = node.getStyle('direction');
3514 if (direction !== this.lastDirection) {
3515 host.fire(B_C_CHANGE, { changedTo: direction });
3516 this.lastDirection = direction;
3519 host.fire(B_C_CHANGE, { changedTo: 'select' });
3520 this.lastDirection = null;
3525 * Checked for a change after a specific nodeChange event has been fired.
3527 * @method _afterNodeChange
3529 _afterNodeChange: function(e) {
3530 // If this is the first event ever, or an event that can result in a context change
3531 if (this.firstEvent || EditorBidi.EVENTS[e.changedType]) {
3532 this._checkForChange();
3533 this.firstEvent = false;
3538 * Checks for a direction change after a mouseup occurs.
3540 * @method _afterMouseUp
3542 _afterMouseUp: function(e) {
3543 this._checkForChange();
3544 this.firstEvent = false;
3546 initializer: function() {
3547 var host = this.get(HOST);
3549 this.firstEvent = true;
3551 host.after(NODE_CHANGE, Y.bind(this._afterNodeChange, this));
3552 host.after('dom:mouseup', Y.bind(this._afterMouseUp, this));
3556 * The events to check for a direction change on
3561 'backspace-up': true,
3563 'pagedown-down': true,
3574 * More elements may be needed. BODY *must* be in the list to take care of the special case.
3576 * blockParent could be changed to use inst.Selection.BLOCKS
3577 * instead, but that would make Y.Plugin.EditorBidi.blockParent
3578 * unusable in non-RTE contexts (it being usable is a nice
3583 BLOCKS: Y.Selection.BLOCKS+',LI,HR,' + BODY,
3585 * Template for creating a block element
3587 * @property DIV_WRAPPER
3589 DIV_WRAPPER: '<DIV></DIV>',
3591 * Returns a block parent for a given element
3593 * @method blockParent
3595 blockParent: function(node, wrap) {
3596 var parent = node, divNode, firstChild;
3599 parent = Y.one(BODY);
3602 if (!parent.test(EditorBidi.BLOCKS)) {
3603 parent = parent.ancestor(EditorBidi.BLOCKS);
3605 if (wrap && parent.test(BODY)) {
3606 // This shouldn't happen if the RTE handles everything
3607 // according to spec: we should get to a P before BODY. But
3608 // we don't want to set the direction of BODY even if that
3609 // happens, so we wrap everything in a DIV.
3611 // The code is based on YUI3's Y.Selection._wrapBlock function.
3612 divNode = Y.Node.create(EditorBidi.DIV_WRAPPER);
3613 parent.get('children').each(function(node, index) {
3617 divNode.append(node);
3620 firstChild.replace(divNode);
3621 divNode.prepend(firstChild);
3627 * The data key to store on the node.
3629 * @property _NODE_SELECTED
3631 _NODE_SELECTED: 'bidiSelected',
3633 * Generates a list of all the block parents of the current NodeList
3635 * @method addParents
3637 addParents: function(nodeArray) {
3638 var i, parent, addParent;
3640 for (i = 0; i < nodeArray.length; i += 1) {
3641 nodeArray[i].setData(EditorBidi._NODE_SELECTED, true);
3644 // This works automagically, since new parents added get processed
3645 // later themselves. So if there's a node early in the process that
3646 // we haven't discovered some of its siblings yet, thus resulting in
3647 // its parent not added, the parent will be added later, since those
3648 // siblings will be added to the array and then get processed.
3649 for (i = 0; i < nodeArray.length; i += 1) {
3650 parent = nodeArray[i].get('parentNode');
3652 // Don't add the parent if the parent is the BODY element.
3653 // We don't want to change the direction of BODY. Also don't
3654 // do it if the parent is already in the list.
3655 if (!parent.test(BODY) && !parent.getData(EditorBidi._NODE_SELECTED)) {
3657 parent.get('children').some(function(sibling) {
3658 if (!sibling.getData(EditorBidi._NODE_SELECTED)) {
3660 return true; // stop more processing
3664 nodeArray.push(parent);
3665 parent.setData(EditorBidi._NODE_SELECTED, true);
3670 for (i = 0; i < nodeArray.length; i += 1) {
3671 nodeArray[i].clearData(EditorBidi._NODE_SELECTED);
3697 Y.namespace('Plugin');
3699 Y.Plugin.EditorBidi = EditorBidi;
3702 * bidi execCommand override for setting the text direction of a node.
3703 * @for Plugin.ExecCommand
3704 * @property COMMANDS.bidi
3707 Y.Plugin.ExecCommand.COMMANDS.bidi = function(cmd, direction) {
3708 var inst = this.getInstance(),
3709 sel = new inst.Selection(),
3711 selected, selectedBlocks, dir;
3713 inst.Selection.filterBlocks();
3714 if (sel.isCollapsed) { // No selection
3715 block = EditorBidi.blockParent(sel.anchorNode);
3717 //If no direction is set, auto-detect the proper setting to make it "toggle"
3718 dir = block.getAttribute(DIR);
3719 if (!dir || dir == 'ltr') {
3725 block.setAttribute(DIR, direction);
3726 returnValue = block;
3727 } else { // some text is selected
3728 selected = sel.getSelected();
3729 selectedBlocks = [];
3730 selected.each(function(node) {
3732 * Temporarily removed this check, should already be fixed
3733 * in Y.Selection.getSelected()
3735 //if (!node.test(BODY)) { // workaround for a YUI bug
3736 selectedBlocks.push(EditorBidi.blockParent(node));
3739 selectedBlocks = inst.all(EditorBidi.addParents(selectedBlocks));
3740 selectedBlocks.setAttribute(DIR, direction);
3741 returnValue = selectedBlocks;
3744 this.get(HOST).get(HOST).editorBidi._checkForChange();
3751 }, '3.3.0' ,{requires:['editor-base'], skinnable:false});
3752 YUI.add('editor-para', function(Y) {
3757 * Plugin for Editor to paragraph auto wrapping and correction.
3759 * @submodule editor-para
3762 * Plugin for Editor to paragraph auto wrapping and correction.
3763 * @class Plugin.EditorPara
3769 var EditorPara = function() {
3770 EditorPara.superclass.constructor.apply(this, arguments);
3771 }, HOST = 'host', BODY = 'body', NODE_CHANGE = 'nodeChange', PARENT_NODE = 'parentNode',
3772 FIRST_P = BODY + ' > p', P = 'p', BR = '<br>', FC = 'firstChild', LI = 'li';
3775 Y.extend(EditorPara, Y.Base, {
3777 * Utility method to create an empty paragraph when the document is empty.
3779 * @method _fixFirstPara
3781 _fixFirstPara: function() {
3782 var host = this.get(HOST), inst = host.getInstance(), sel;
3783 inst.one('body').set('innerHTML', '<' + P + '>' + inst.Selection.CURSOR + '</' + P + '>');
3784 var n = inst.one(FIRST_P);
3785 sel = new inst.Selection();
3786 sel.selectNode(n, true, false);
3790 * nodeChange handler to handle fixing an empty document.
3792 * @method _onNodeChange
3794 _onNodeChange: function(e) {
3795 var host = this.get(HOST), inst = host.getInstance(),
3796 html, txt, par , d, sel, btag = inst.Selection.DEFAULT_BLOCK_TAG,
3797 inHTML, txt2, childs, aNode, index, node2, top, n, sib,
3798 ps, br, item, p, imgs, t, LAST_CHILD = ':last-child';
3800 switch (e.changedType) {
3802 var para = ((this._lastPara) ? this._lastPara : e.changedNode),
3803 b = para.one('br.yui-cursor');
3805 if (this._lastPara) {
3806 delete this._lastPara;
3810 if (b.previous() || b.next()) {
3814 if (!para.test(btag)) {
3815 var para2 = para.ancestor(btag);
3821 if (para.test(btag)) {
3822 var prev = para.previous(), lc, lc2, found = false;
3824 lc = prev.one(LAST_CHILD);
3827 lc2 = lc.one(LAST_CHILD);
3838 host.copyStyles(lc, para);
3845 //Webkit doesn't support shift+enter as a BR, this fixes that.
3846 if (e.changedEvent.shiftKey) {
3847 host.execCommand('insertbr');
3848 e.changedEvent.preventDefault();
3851 //TODO Move this to a GECKO MODULE - Can't for the moment, requires no change to metadata (YMAIL)
3852 if (Y.UA.gecko && host.get('defaultblock') !== 'p') {
3853 par = e.changedNode;
3855 if (!par.test(LI) && !par.ancestor(LI)) {
3856 if (!par.test(btag)) {
3857 par = par.ancestor(btag);
3859 d = inst.Node.create('<' + btag + '></' + btag + '>');
3860 par.insert(d, 'after');
3861 sel = new inst.Selection();
3862 if (sel.anchorOffset) {
3863 inHTML = sel.anchorNode.get('textContent');
3865 txt = inst.one(inst.config.doc.createTextNode(inHTML.substr(0, sel.anchorOffset)));
3866 txt2 = inst.one(inst.config.doc.createTextNode(inHTML.substr(sel.anchorOffset)));
3868 aNode = sel.anchorNode;
3869 aNode.setContent(''); //I
3870 node2 = aNode.cloneNode(); //I
3871 node2.append(txt2); //text
3875 sib = sib.get(PARENT_NODE); //B
3876 if (sib && !sib.test(btag)) {
3877 n = sib.cloneNode();
3878 n.set('innerHTML', '');
3882 childs = sib.get('childNodes');
3884 childs.each(function(c) {
3893 aNode = sib; //Top sibling
3900 sel.anchorNode.append(txt);
3909 d.prepend(inst.Selection.CURSOR);
3910 sel.focusCursor(true, true);
3911 html = inst.Selection.getText(d);
3913 inst.Selection.cleanCursor();
3915 e.changedEvent.preventDefault();
3920 if (inst.config.doc.childNodes.length < 2) {
3921 var cont = inst.config.doc.body.innerHTML;
3922 if (cont && cont.length < 5 && cont.toLowerCase() == BR) {
3923 this._fixFirstPara();
3927 case 'backspace-up':
3928 case 'backspace-down':
3931 ps = inst.all(FIRST_P);
3932 item = inst.one(BODY);
3936 br = item.one('br');
3938 br.removeAttribute('id');
3939 br.removeAttribute('class');
3942 txt = inst.Selection.getText(item);
3943 txt = txt.replace(/ /g, '').replace(/\n/g, '');
3944 imgs = item.all('img');
3946 if (txt.length === 0 && !imgs.size()) {
3947 //God this is horrible..
3948 if (!item.test(P)) {
3949 this._fixFirstPara();
3952 if (e.changedNode && e.changedNode.test(P)) {
3955 if (!p && host._lastPara && host._lastPara.inDoc()) {
3958 if (p && !p.test(P)) {
3962 if (!p.previous() && p.get(PARENT_NODE) && p.get(PARENT_NODE).test(BODY)) {
3963 e.changedEvent.frameEvent.halt();
3968 if (e.changedNode) {
3969 item = e.changedNode;
3970 if (item.test('li') && (!item.previous() && !item.next())) {
3971 html = item.get('innerHTML').replace(BR, '');
3973 if (item.get(PARENT_NODE)) {
3974 item.get(PARENT_NODE).replace(inst.Node.create(BR));
3975 e.changedEvent.frameEvent.halt();
3977 inst.Selection.filterBlocks();
3986 * This forced FF to redraw the content on backspace.
3987 * On some occasions FF will leave a cursor residue after content has been deleted.
3988 * Dropping in the empty textnode and then removing it causes FF to redraw and
3989 * remove the "ghost cursors"
3992 t = inst.config.doc.createTextNode(' ');
3999 if (e.changedNode && !e.changedNode.test(btag)) {
4000 var p = e.changedNode.ancestor(btag);
4009 * Performs a block element filter when the Editor is first ready
4011 * @method _afterEditorReady
4013 _afterEditorReady: function() {
4014 var host = this.get(HOST), inst = host.getInstance(), btag;
4016 inst.Selection.filterBlocks();
4017 btag = inst.Selection.DEFAULT_BLOCK_TAG;
4018 FIRST_P = BODY + ' > ' + btag;
4023 * Performs a block element filter when the Editor after an content change
4025 * @method _afterContentChange
4027 _afterContentChange: function() {
4028 var host = this.get(HOST), inst = host.getInstance();
4029 if (inst && inst.Selection) {
4030 inst.Selection.filterBlocks();
4034 * Performs block/paste filtering after paste.
4036 * @method _afterPaste
4038 _afterPaste: function() {
4039 var host = this.get(HOST), inst = host.getInstance(),
4040 sel = new inst.Selection();
4042 Y.later(50, host, function() {
4043 inst.Selection.filterBlocks();
4047 initializer: function() {
4048 var host = this.get(HOST);
4049 if (host.editorBR) {
4050 Y.error('Can not plug EditorPara and EditorBR at the same time.');
4054 host.on(NODE_CHANGE, Y.bind(this._onNodeChange, this));
4055 host.after('ready', Y.bind(this._afterEditorReady, this));
4056 host.after('contentChange', Y.bind(this._afterContentChange, this));
4058 host.after('dom:paste', Y.bind(this._afterPaste, this));
4081 Y.namespace('Plugin');
4083 Y.Plugin.EditorPara = EditorPara;
4087 }, '3.3.0' ,{requires:['node'], skinnable:false});
4088 YUI.add('editor-br', function(Y) {
4093 * Plugin for Editor to normalize BR's.
4095 * @submodule editor-br
4098 * Plugin for Editor to normalize BR's.
4099 * @class Plugin.EditorBR
4105 var EditorBR = function() {
4106 EditorBR.superclass.constructor.apply(this, arguments);
4107 }, HOST = 'host', LI = 'li';
4110 Y.extend(EditorBR, Y.Base, {
4112 * Frame keyDown handler that normalizes BR's when pressing ENTER.
4114 * @method _onKeyDown
4116 _onKeyDown: function(e) {
4121 if (e.keyCode == 13) {
4122 var host = this.get(HOST), inst = host.getInstance(),
4123 sel = new inst.Selection(),
4128 if (!sel.anchorNode || (!sel.anchorNode.test(LI) && !sel.anchorNode.ancestor(LI))) {
4129 sel._selection.pasteHTML('<br>');
4130 sel._selection.collapse(false);
4131 sel._selection.select();
4136 if (!sel.anchorNode.test(LI) && !sel.anchorNode.ancestor(LI)) {
4137 host.frame._execCommand('insertlinebreak', null);
4145 * Adds listeners for keydown in IE and Webkit. Also fires insertbeonreturn for supporting browsers.
4147 * @method _afterEditorReady
4149 _afterEditorReady: function() {
4150 var inst = this.get(HOST).getInstance();
4152 inst.config.doc.execCommand('insertbronreturn', null, true);
4155 if (Y.UA.ie || Y.UA.webkit) {
4156 inst.on('keydown', Y.bind(this._onKeyDown, this), inst.config.doc);
4160 * Adds a nodeChange listener only for FF, in the event of a backspace or delete, it creates an empy textNode
4161 * inserts it into the DOM after the e.changedNode, then removes it. Causing FF to redraw the content.
4163 * @method _onNodeChange
4164 * @param {Event} e The nodeChange event.
4166 _onNodeChange: function(e) {
4167 switch (e.changedType) {
4168 case 'backspace-up':
4169 case 'backspace-down':
4172 * This forced FF to redraw the content on backspace.
4173 * On some occasions FF will leave a cursor residue after content has been deleted.
4174 * Dropping in the empty textnode and then removing it causes FF to redraw and
4175 * remove the "ghost cursors"
4177 var inst = this.get(HOST).getInstance();
4178 var d = e.changedNode;
4179 var t = inst.config.doc.createTextNode(' ');
4185 initializer: function() {
4186 var host = this.get(HOST);
4187 if (host.editorPara) {
4188 Y.error('Can not plug EditorBR and EditorPara at the same time.');
4191 host.after('ready', Y.bind(this._afterEditorReady, this));
4193 host.on('nodeChange', Y.bind(this._onNodeChange, this));
4216 Y.namespace('Plugin');
4218 Y.Plugin.EditorBR = EditorBR;
4222 }, '3.3.0' ,{requires:['node'], skinnable:false});
4225 YUI.add('editor', function(Y){}, '3.3.0' ,{skinnable:false, use:['frame', 'selection', 'exec-command', 'editor-base', 'editor-para', 'editor-br', 'editor-bidi', 'createlink-base']});