3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('exec-command', function(Y) {
11 * Plugin for the frame module to handle execCommands for Editor
12 * @class Plugin.ExecCommand
16 * @submodule exec-command
18 var ExecCommand = function() {
19 ExecCommand.superclass.constructor.apply(this, arguments);
22 Y.extend(ExecCommand, Y.Base, {
24 * An internal reference to the keyCode of the last key that was pressed.
30 * An internal reference to the instance of the frame plugged into.
36 * Execute a command on the frame's document.
38 * @param {String} action The action to perform (bold, italic, fontname)
39 * @param {String} value The optional value (helvetica)
40 * @return {Node/NodeList} Should return the Node/Nodelist affected
42 command: function(action, value) {
43 var fn = ExecCommand.COMMANDS[action];
46 return fn.call(this, action, value);
48 return this._command(action, value);
52 * The private version of execCommand that doesn't filter for overrides.
55 * @param {String} action The action to perform (bold, italic, fontname)
56 * @param {String} value The optional value (helvetica)
58 _command: function(action, value) {
59 var inst = this.getInstance();
62 inst.config.doc.execCommand('styleWithCSS', null, 1);
65 inst.config.doc.execCommand('useCSS', null, 0);
69 inst.config.doc.execCommand(action, null, value);
74 * Get's the instance of YUI bound to the parent frame
76 * @return {YUI} The YUI instance bound to the parent frame
78 getInstance: function() {
80 this._inst = this.get('host').getInstance();
84 initializer: function() {
85 Y.mix(this.get('host'), {
86 execCommand: function(action, value) {
87 return this.exec.command(action, value);
89 _execCommand: function(action, value) {
90 return this.exec._command(action, value);
94 this.get('host').on('dom:keypress', Y.bind(function(e) {
95 this._lastKey = e.keyCode;
98 _wrapContent: function(str, override) {
99 var useP = (this.getInstance().host.editorPara && !override ? true : false);
102 str = '<p>' + str + '</p>';
127 * Static object literal of execCommand overrides
133 * Wraps the content with a new element of type (tag)
134 * @method COMMANDS.wrap
136 * @param {String} cmd The command executed: wrap
137 * @param {String} tag The tag to wrap the selection with
138 * @return {NodeList} NodeList of the items touched by this command.
140 wrap: function(cmd, tag) {
141 var inst = this.getInstance();
142 return (new inst.EditorSelection()).wrapContent(tag);
145 * Inserts the provided HTML at the cursor, should be a single element.
146 * @method COMMANDS.inserthtml
148 * @param {String} cmd The command executed: inserthtml
149 * @param {String} html The html to insert
150 * @return {Node} Node instance of the item touched by this command.
152 inserthtml: function(cmd, html) {
153 var inst = this.getInstance();
154 if (inst.EditorSelection.hasCursor() || Y.UA.ie) {
155 return (new inst.EditorSelection()).insertContent(html);
157 this._command('inserthtml', html);
161 * Inserts the provided HTML at the cursor, and focuses the cursor afterwards.
162 * @method COMMANDS.insertandfocus
164 * @param {String} cmd The command executed: insertandfocus
165 * @param {String} html The html to insert
166 * @return {Node} Node instance of the item touched by this command.
168 insertandfocus: function(cmd, html) {
169 var inst = this.getInstance(), out, sel;
170 if (inst.EditorSelection.hasCursor()) {
171 html += inst.EditorSelection.CURSOR;
172 out = this.command('inserthtml', html);
173 sel = new inst.EditorSelection();
174 sel.focusCursor(true, true);
176 this.command('inserthtml', html);
181 * Inserts a BR at the current cursor position
182 * @method COMMANDS.insertbr
184 * @param {String} cmd The command executed: insertbr
186 insertbr: function(cmd) {
187 var inst = this.getInstance(),
188 sel = new inst.EditorSelection(),
189 html = '<var>|</var>', last = null,
190 q = (Y.UA.webkit) ? 'span.Apple-style-span,var' : 'var';
192 if (sel._selection.pasteHTML) {
193 sel._selection.pasteHTML(html);
195 this._command('inserthtml', html);
198 var insert = function(n) {
199 var c = inst.Node.create('<br>');
200 n.insert(c, 'before');
204 inst.all(q).each(function(n) {
208 if (n.get('innerHTML') === '|') {
214 if ((!last.previous() || !last.previous().test('br')) && Y.UA.gecko) {
215 var s = last.cloneNode();
216 last.insert(s, 'after');
222 if (Y.UA.webkit && last) {
224 sel.selectNode(last);
228 * Inserts an image at the cursor position
229 * @method COMMANDS.insertimage
231 * @param {String} cmd The command executed: insertimage
232 * @param {String} img The url of the image to be inserted
233 * @return {Node} Node instance of the item touched by this command.
235 insertimage: function(cmd, img) {
236 return this.command('inserthtml', '<img src="' + img + '">');
239 * Add a class to all of the elements in the selection
240 * @method COMMANDS.addclass
242 * @param {String} cmd The command executed: addclass
243 * @param {String} cls The className to add
244 * @return {NodeList} NodeList of the items touched by this command.
246 addclass: function(cmd, cls) {
247 var inst = this.getInstance();
248 return (new inst.EditorSelection()).getSelected().addClass(cls);
251 * Remove a class from all of the elements in the selection
252 * @method COMMANDS.removeclass
254 * @param {String} cmd The command executed: removeclass
255 * @param {String} cls The className to remove
256 * @return {NodeList} NodeList of the items touched by this command.
258 removeclass: function(cmd, cls) {
259 var inst = this.getInstance();
260 return (new inst.EditorSelection()).getSelected().removeClass(cls);
263 * Adds a forecolor to the current selection, or creates a new element and applies it
264 * @method COMMANDS.forecolor
266 * @param {String} cmd The command executed: forecolor
267 * @param {String} val The color value to apply
268 * @return {NodeList} NodeList of the items touched by this command.
270 forecolor: function(cmd, val) {
271 var inst = this.getInstance(),
272 sel = new inst.EditorSelection(), n;
275 this._command('useCSS', false);
277 if (inst.EditorSelection.hasCursor()) {
278 if (sel.isCollapsed) {
279 if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) {
280 sel.anchorNode.setStyle('color', val);
283 n = this.command('inserthtml', '<span style="color: ' + val + '">' + inst.EditorSelection.CURSOR + '</span>');
284 sel.focusCursor(true, true);
288 return this._command(cmd, val);
291 this._command(cmd, val);
295 * Adds a background color to the current selection, or creates a new element and applies it
296 * @method COMMANDS.backcolor
298 * @param {String} cmd The command executed: backcolor
299 * @param {String} val The color value to apply
300 * @return {NodeList} NodeList of the items touched by this command.
302 backcolor: function(cmd, val) {
303 var inst = this.getInstance(),
304 sel = new inst.EditorSelection(), n;
306 if (Y.UA.gecko || Y.UA.opera) {
310 this._command('useCSS', false);
312 if (inst.EditorSelection.hasCursor()) {
313 if (sel.isCollapsed) {
314 if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) {
315 sel.anchorNode.setStyle('backgroundColor', val);
318 n = this.command('inserthtml', '<span style="background-color: ' + val + '">' + inst.EditorSelection.CURSOR + '</span>');
319 sel.focusCursor(true, true);
323 return this._command(cmd, val);
326 this._command(cmd, val);
330 * Sugar method, calles backcolor
331 * @method COMMANDS.hilitecolor
333 * @param {String} cmd The command executed: backcolor
334 * @param {String} val The color value to apply
335 * @return {NodeList} NodeList of the items touched by this command.
337 hilitecolor: function() {
338 return ExecCommand.COMMANDS.backcolor.apply(this, arguments);
341 * Adds a font name to the current selection, or creates a new element and applies it
342 * @method COMMANDS.fontname2
345 * @param {String} cmd The command executed: fontname
346 * @param {String} val The font name to apply
347 * @return {NodeList} NodeList of the items touched by this command.
349 fontname2: function(cmd, val) {
350 this._command('fontname', val);
351 var inst = this.getInstance(),
352 sel = new inst.EditorSelection();
354 if (sel.isCollapsed && (this._lastKey != 32)) {
355 if (sel.anchorNode.test('font')) {
356 sel.anchorNode.set('face', val);
361 * Adds a fontsize to the current selection, or creates a new element and applies it
362 * @method COMMANDS.fontsize2
365 * @param {String} cmd The command executed: fontsize
366 * @param {String} val The font size to apply
367 * @return {NodeList} NodeList of the items touched by this command.
369 fontsize2: function(cmd, val) {
370 this._command('fontsize', val);
372 var inst = this.getInstance(),
373 sel = new inst.EditorSelection();
375 if (sel.isCollapsed && sel.anchorNode && (this._lastKey != 32)) {
377 if (sel.anchorNode.getStyle('lineHeight')) {
378 sel.anchorNode.setStyle('lineHeight', '');
381 if (sel.anchorNode.test('font')) {
382 sel.anchorNode.set('size', val);
383 } else if (Y.UA.gecko) {
384 var p = sel.anchorNode.ancestor(inst.EditorSelection.DEFAULT_BLOCK_TAG);
386 p.setStyle('fontSize', '');
392 * Overload for COMMANDS.list
393 * @method COMMANDS.insertorderedlist
395 * @param {String} cmd The command executed: list, ul
397 insertunorderedlist: function(cmd) {
398 this.command('list', 'ul');
401 * Overload for COMMANDS.list
402 * @method COMMANDS.insertunorderedlist
404 * @param {String} cmd The command executed: list, ol
406 insertorderedlist: function(cmd) {
407 this.command('list', 'ol');
410 * Noramlizes lists creation/destruction for IE. All others pass through to native calls
411 * @method COMMANDS.list
413 * @param {String} cmd The command executed: list (not used)
414 * @param {String} tag The tag to deal with
416 list: function(cmd, tag) {
417 var inst = this.getInstance(), html, self = this,
418 DIR = 'dir', cls = 'yui3-touched',
419 dir, range, div, elm, n, str, s, par, list, lis,
420 useP = (inst.host.editorPara ? true : false),
421 sel = new inst.EditorSelection();
423 cmd = 'insert' + ((tag === 'ul') ? 'un' : '') + 'orderedlist';
425 if (Y.UA.ie && !sel.isCollapsed) {
426 range = sel._selection;
427 html = range.htmlText;
428 div = inst.Node.create(html) || inst.one('body');
430 if (div.test('li') || div.one('li')) {
431 this._command(cmd, null);
435 elm = range.item ? range.item(0) : range.parentElement();
440 lis.each(function(l) {
441 str = self._wrapContent(l.get('innerHTML'));
444 s = inst.Node.create(str);
445 if (n.get('parentNode').test('div')) {
446 n = n.get('parentNode');
448 if (n && n.hasAttribute(DIR)) {
450 s.all('p').setAttribute(DIR, n.getAttribute(DIR));
452 s.setAttribute(DIR, n.getAttribute(DIR));
456 n.replace(s.get('innerHTML'));
460 if (range.moveToElementText) {
461 range.moveToElementText(s._node);
465 par = Y.one(range.parentElement());
466 if (!par.test(inst.EditorSelection.BLOCKS)) {
467 par = par.ancestor(inst.EditorSelection.BLOCKS);
470 if (par.hasAttribute(DIR)) {
471 dir = par.getAttribute(DIR);
474 if (html.indexOf('<br>') > -1) {
475 html = html.split(/<br>/i);
477 var tmp = inst.Node.create(html),
478 ps = tmp ? tmp.all('p') : null;
480 if (ps && ps.size()) {
482 ps.each(function(n) {
483 html.push(n.get('innerHTML'));
489 list = '<' + tag + ' id="ie-list">';
490 Y.each(html, function(v) {
491 var a = inst.Node.create(v);
492 if (a && a.test('p')) {
493 if (a.hasAttribute(DIR)) {
494 dir = a.getAttribute(DIR);
496 v = a.get('innerHTML');
498 list += '<li>' + v + '</li>';
500 list += '</' + tag + '>';
501 range.pasteHTML(list);
502 elm = inst.config.doc.getElementById('ie-list');
505 elm.setAttribute(DIR, dir);
507 if (range.moveToElementText) {
508 range.moveToElementText(elm);
512 } else if (Y.UA.ie) {
513 par = inst.one(sel._selection.parentElement());
515 if (par && par.hasAttribute(DIR)) {
516 dir = par.getAttribute(DIR);
518 html = Y.EditorSelection.getText(par);
522 sdir = ' dir="' + dir + '"';
524 list = inst.Node.create(Y.Lang.sub('<{tag}{dir}><li></li></{tag}>', { tag: tag, dir: sdir }));
526 sel.selectNode(list.one('li'));
528 this._command(cmd, null);
531 this._command(cmd, null);
534 inst.all(tag).addClass(cls);
535 if (sel.anchorNode.test(inst.EditorSelection.BLOCKS)) {
536 par = sel.anchorNode;
538 par = sel.anchorNode.ancestor(inst.EditorSelection.BLOCKS);
540 if (!par) { //No parent, find the first block under the anchorNode
541 par = sel.anchorNode.one(inst.EditorSelection.BLOCKS);
544 if (par && par.hasAttribute(DIR)) {
545 dir = par.getAttribute(DIR);
547 if (par && par.test(tag)) {
548 var hasPParent = par.ancestor('p');
549 html = inst.Node.create('<div/>');
551 elm.each(function(h) {
552 html.append(self._wrapContent(h.get('innerHTML'), hasPParent));
556 html.all('p').setAttribute(DIR, dir);
558 html.setAttribute(DIR, dir);
562 html = inst.Node.create(html.get('innerHTML'));
564 var fc = html.get('firstChild');
568 this._command(cmd, null);
570 list = inst.all(tag);
574 list.each(function(n) {
575 if (!n.hasClass(cls)) {
576 n.setAttribute(DIR, dir);
582 list.removeClass(cls);
586 * Noramlizes alignment for Webkit Browsers
587 * @method COMMANDS.justify
589 * @param {String} cmd The command executed: justify (not used)
590 * @param {String} val The actual command from the justify{center,all,left,right} stubs
592 justify: function(cmd, val) {
594 var inst = this.getInstance(),
595 sel = new inst.EditorSelection(),
596 aNode = sel.anchorNode;
598 var bgColor = aNode.getStyle('backgroundColor');
600 sel = new inst.EditorSelection();
601 if (sel.anchorNode.test('div')) {
602 var html = '<span>' + sel.anchorNode.get('innerHTML') + '</span>';
603 sel.anchorNode.set('innerHTML', html);
604 sel.anchorNode.one('span').setStyle('backgroundColor', bgColor);
605 sel.selectNode(sel.anchorNode.one('span'));
612 * Override method for COMMANDS.justify
613 * @method COMMANDS.justifycenter
616 justifycenter: function(cmd) {
617 this.command('justify', 'justifycenter');
620 * Override method for COMMANDS.justify
621 * @method COMMANDS.justifyleft
624 justifyleft: function(cmd) {
625 this.command('justify', 'justifyleft');
628 * Override method for COMMANDS.justify
629 * @method COMMANDS.justifyright
632 justifyright: function(cmd) {
633 this.command('justify', 'justifyright');
636 * Override method for COMMANDS.justify
637 * @method COMMANDS.justifyfull
640 justifyfull: function(cmd) {
641 this.command('justify', 'justifyfull');
647 * This method is meant to normalize IE's in ability to exec the proper command on elements with CSS styling.
650 * @param {String} cmd The command to execute
651 * @param {String} tag The tag to create
652 * @param {String} rule The rule that we are looking for.
654 var fixIETags = function(cmd, tag, rule) {
655 var inst = this.getInstance(),
656 doc = inst.config.doc,
657 sel = doc.selection.createRange(),
658 o = doc.queryCommandValue(cmd),
659 html, reg, m, p, d, s, c;
663 reg = new RegExp(rule, 'g');
667 html = html.replace(rule + ';', '').replace(rule, '');
669 sel.pasteHTML('<var id="yui-ie-bs">');
671 p = doc.getElementById('yui-ie-bs');
672 d = doc.createElement('div');
673 s = doc.createElement(tag);
676 if (p.parentNode !== inst.config.doc.body) {
682 p.parentNode.replaceChild(s, p);
684 Y.each(c, function(f) {
688 if (sel.moveToElementText) {
689 sel.moveToElementText(s);
698 ExecCommand.COMMANDS.bold = function() {
699 fixIETags.call(this, 'bold', 'b', 'FONT-WEIGHT: bold');
701 ExecCommand.COMMANDS.italic = function() {
702 fixIETags.call(this, 'italic', 'i', 'FONT-STYLE: italic');
704 ExecCommand.COMMANDS.underline = function() {
705 fixIETags.call(this, 'underline', 'u', 'TEXT-DECORATION: underline');
709 Y.namespace('Plugin');
710 Y.Plugin.ExecCommand = ExecCommand;
714 }, '3.5.0' ,{skinnable:false, requires:['frame']});