MDL-35616 import YUI 3.7.2
[moodle.git] / lib / yuilib / 3.7.2 / build / exec-command / exec-command.js
blob4f65f9a401fcc8f970eeccb68ead14fa7c3e2cab
1 /*
2 YUI 3.7.2 (build 5639)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('exec-command', function (Y, NAME) {
10     /**
11      * Plugin for the frame module to handle execCommands for Editor
12      * @class Plugin.ExecCommand
13      * @extends Base
14      * @constructor
15      * @module editor
16      * @submodule exec-command
17      */
18         var ExecCommand = function() {
19             ExecCommand.superclass.constructor.apply(this, arguments);
20         };
22         Y.extend(ExecCommand, Y.Base, {
23             /**
24             * An internal reference to the keyCode of the last key that was pressed.
25             * @private
26             * @property _lastKey
27             */
28             _lastKey: null,
29             /**
30             * An internal reference to the instance of the frame plugged into.
31             * @private
32             * @property _inst
33             */
34             _inst: null,
35             /**
36             * Execute a command on the frame's document.
37             * @method command
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
41             */
42             command: function(action, value) {
43                 var fn = ExecCommand.COMMANDS[action];
44                 
45                 if (fn) {
46                     return fn.call(this, action, value);
47                 } else {
48                     return this._command(action, value);
49                 }
50             },
51             /**
52             * The private version of execCommand that doesn't filter for overrides.
53             * @private
54             * @method _command
55             * @param {String} action The action to perform (bold, italic, fontname)
56             * @param {String} value The optional value (helvetica)
57             */
58             _command: function(action, value) {
59                 var inst = this.getInstance();
60                 try {
61                     try {
62                         inst.config.doc.execCommand('styleWithCSS', null, 1);
63                     } catch (e1) {
64                         try {
65                             inst.config.doc.execCommand('useCSS', null, 0);
66                         } catch (e2) {
67                         }
68                     }
69                     inst.config.doc.execCommand(action, null, value);
70                 } catch (e) {
71                 }
72             },
73             /**
74             * Get's the instance of YUI bound to the parent frame
75             * @method getInstance
76             * @return {YUI} The YUI instance bound to the parent frame
77             */
78             getInstance: function() {
79                 if (!this._inst) {
80                     this._inst = this.get('host').getInstance();
81                 }
82                 return this._inst;
83             },
84             initializer: function() {
85                 Y.mix(this.get('host'), {
86                     execCommand: function(action, value) {
87                         return this.exec.command(action, value);
88                     },
89                     _execCommand: function(action, value) {
90                         return this.exec._command(action, value);
91                     }
92                 });
94                 this.get('host').on('dom:keypress', Y.bind(function(e) {
95                     this._lastKey = e.keyCode;
96                 }, this));
97             },
98             _wrapContent: function(str, override) {
99                 var useP = (this.getInstance().host.editorPara && !override ? true : false);
100                 
101                 if (useP) {
102                     str = '<p>' + str + '</p>';
103                 } else {
104                     str = str + '<br>';
105                 }
106                 return str;
107             }
108         }, {
109             /**
110             * execCommand
111             * @property NAME
112             * @static
113             */
114             NAME: 'execCommand',
115             /**
116             * exec
117             * @property NS
118             * @static
119             */
120             NS: 'exec',
121             ATTRS: {
122                 host: {
123                     value: false
124                 }
125             },
126             /**
127             * Static object literal of execCommand overrides
128             * @property COMMANDS
129             * @static
130             */
131             COMMANDS: {
132                 /**
133                 * Wraps the content with a new element of type (tag)
134                 * @method COMMANDS.wrap
135                 * @static
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.
139                 */
140                 wrap: function(cmd, tag) {
141                     var inst = this.getInstance();
142                     return (new inst.EditorSelection()).wrapContent(tag);
143                 },
144                 /**
145                 * Inserts the provided HTML at the cursor, should be a single element.
146                 * @method COMMANDS.inserthtml
147                 * @static
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.
151                 */
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);
156                     } else {
157                         this._command('inserthtml', html);
158                     }
159                 },
160                 /**
161                 * Inserts the provided HTML at the cursor, and focuses the cursor afterwards.
162                 * @method COMMANDS.insertandfocus
163                 * @static
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.
167                 */
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);
175                     } else {
176                         this.command('inserthtml', html);
177                     }
178                     return out;
179                 },
180                 /**
181                 * Inserts a BR at the current cursor position
182                 * @method COMMANDS.insertbr
183                 * @static
184                 * @param {String} cmd The command executed: insertbr
185                 */
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);
194                     } else {
195                         this._command('inserthtml', html);
196                     }
198                     var insert = function(n) {
199                         var c = inst.Node.create('<br>');
200                         n.insert(c, 'before');
201                         return c;
202                     };
204                     inst.all(q).each(function(n) {
205                         var g = true;   
206                         if (Y.UA.webkit) {
207                             g = false;
208                             if (n.get('innerHTML') === '|') {
209                                 g = true;
210                             }
211                         }
212                         if (g) {
213                             last = insert(n);
214                             if ((!last.previous() || !last.previous().test('br')) && Y.UA.gecko) {
215                                 var s = last.cloneNode();
216                                 last.insert(s, 'after');
217                                 last = s;
218                             }
219                             n.remove();
220                         }
221                     });
222                     if (Y.UA.webkit && last) {
223                         insert(last);
224                         sel.selectNode(last);
225                     }
226                 },
227                 /**
228                 * Inserts an image at the cursor position
229                 * @method COMMANDS.insertimage
230                 * @static
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.
234                 */
235                 insertimage: function(cmd, img) {
236                     return this.command('inserthtml', '<img src="' + img + '">');
237                 },
238                 /**
239                 * Add a class to all of the elements in the selection
240                 * @method COMMANDS.addclass
241                 * @static
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.
245                 */
246                 addclass: function(cmd, cls) {
247                     var inst = this.getInstance();
248                     return (new inst.EditorSelection()).getSelected().addClass(cls);
249                 },
250                 /**
251                 * Remove a class from all of the elements in the selection
252                 * @method COMMANDS.removeclass
253                 * @static
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.
257                 */
258                 removeclass: function(cmd, cls) {
259                     var inst = this.getInstance();
260                     return (new inst.EditorSelection()).getSelected().removeClass(cls);
261                 },
262                 /**
263                 * Adds a forecolor to the current selection, or creates a new element and applies it
264                 * @method COMMANDS.forecolor
265                 * @static
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.
269                 */
270                 forecolor: function(cmd, val) {
271                     var inst = this.getInstance(),
272                         sel = new inst.EditorSelection(), n;
274                     if (!Y.UA.ie) {
275                         this._command('useCSS', false);
276                     }
277                     if (inst.EditorSelection.hasCursor()) {
278                         if (sel.isCollapsed) {
279                             if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === '&nbsp;')) {
280                                 sel.anchorNode.setStyle('color', val);
281                                 n = sel.anchorNode;
282                             } else {
283                                 n = this.command('inserthtml', '<span style="color: ' + val + '">' + inst.EditorSelection.CURSOR + '</span>');
284                                 sel.focusCursor(true, true);
285                             }
286                             return n;
287                         } else {
288                             return this._command(cmd, val);
289                         }
290                     } else {
291                         this._command(cmd, val);
292                     }
293                 },
294                 /**
295                 * Adds a background color to the current selection, or creates a new element and applies it
296                 * @method COMMANDS.backcolor
297                 * @static
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.
301                 */
302                 backcolor: function(cmd, val) {
303                     var inst = this.getInstance(),
304                         sel = new inst.EditorSelection(), n;
305                     
306                     if (Y.UA.gecko || Y.UA.opera) {
307                         cmd = 'hilitecolor';
308                     }
309                     if (!Y.UA.ie) {
310                         this._command('useCSS', false);
311                     }
312                     if (inst.EditorSelection.hasCursor()) {
313                         if (sel.isCollapsed) {
314                             if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === '&nbsp;')) {
315                                 sel.anchorNode.setStyle('backgroundColor', val);
316                                 n = sel.anchorNode;
317                             } else {
318                                 n = this.command('inserthtml', '<span style="background-color: ' + val + '">' + inst.EditorSelection.CURSOR + '</span>');
319                                 sel.focusCursor(true, true);
320                             }
321                             return n;
322                         } else {
323                             return this._command(cmd, val);
324                         }
325                     } else {
326                         this._command(cmd, val);
327                     }
328                 },
329                 /**
330                 * Sugar method, calles backcolor
331                 * @method COMMANDS.hilitecolor
332                 * @static
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.
336                 */
337                 hilitecolor: function() {
338                     return ExecCommand.COMMANDS.backcolor.apply(this, arguments);
339                 },
340                 /**
341                 * Adds a font name to the current selection, or creates a new element and applies it
342                 * @method COMMANDS.fontname2
343                 * @deprecated
344                 * @static
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.
348                 */
349                 fontname2: function(cmd, val) {
350                     this._command('fontname', val);
351                     var inst = this.getInstance(),
352                         sel = new inst.EditorSelection();
353                     
354                     if (sel.isCollapsed && (this._lastKey != 32)) {
355                         if (sel.anchorNode.test('font')) {
356                             sel.anchorNode.set('face', val);
357                         }
358                     }
359                 },
360                 /**
361                 * Adds a fontsize to the current selection, or creates a new element and applies it
362                 * @method COMMANDS.fontsize2
363                 * @deprecated
364                 * @static
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.
368                 */
369                 fontsize2: function(cmd, val) {
370                     this._command('fontsize', val);
372                     var inst = this.getInstance(),
373                         sel = new inst.EditorSelection();
374                     
375                     if (sel.isCollapsed && sel.anchorNode && (this._lastKey != 32)) {
376                         if (Y.UA.webkit) {
377                             if (sel.anchorNode.getStyle('lineHeight')) {
378                                 sel.anchorNode.setStyle('lineHeight', '');
379                             }
380                         }
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);
385                             if (p) {
386                                 p.setStyle('fontSize', '');
387                             }
388                         }
389                     }
390                 },
391                 /**
392                 * Overload for COMMANDS.list
393                 * @method COMMANDS.insertorderedlist
394                 * @static
395                 * @param {String} cmd The command executed: list, ul
396                 */
397                 insertunorderedlist: function(cmd) {
398                     this.command('list', 'ul');
399                 },
400                 /**
401                 * Overload for COMMANDS.list
402                 * @method COMMANDS.insertunorderedlist
403                 * @static
404                 * @param {String} cmd The command executed: list, ol
405                 */
406                 insertorderedlist: function(cmd) {
407                     this.command('list', 'ol');
408                 },
409                 /**
410                 * Noramlizes lists creation/destruction for IE. All others pass through to native calls
411                 * @method COMMANDS.list
412                 * @static
413                 * @param {String} cmd The command executed: list (not used)
414                 * @param {String} tag The tag to deal with
415                 */
416                 list: function(cmd, tag) {
417                     var inst = this.getInstance(), html, self = this,
418                         /*
419                         The yui3- class name below is not a skinnable class,
420                         it's a utility class used internally by editor and 
421                         stripped when completed, calling getClassName on this
422                         is a waste of resources.
423                         */
424                         DIR = 'dir', cls = 'yui3-touched',
425                         dir, range, div, elm, n, str, s, par, list, lis,
426                         useP = (inst.host.editorPara ? true : false),
427                         sel = new inst.EditorSelection();
429                     cmd = 'insert' + ((tag === 'ul') ? 'un' : '') + 'orderedlist';
430                     
431                     if (Y.UA.ie && !sel.isCollapsed) {
432                         range = sel._selection;
433                         html = range.htmlText;
434                         div = inst.Node.create(html) || inst.one('body');
436                         if (div.test('li') || div.one('li')) {
437                             this._command(cmd, null);
438                             return;
439                         }
440                         if (div.test(tag)) {
441                             elm = range.item ? range.item(0) : range.parentElement();
442                             n = inst.one(elm);
443                             lis = n.all('li');
445                             str = '<div>';
446                             lis.each(function(l) {
447                                 str = self._wrapContent(l.get('innerHTML'));
448                             });
449                             str += '</div>';
450                             s = inst.Node.create(str);
451                             if (n.get('parentNode').test('div')) {
452                                 n = n.get('parentNode');
453                             }
454                             if (n && n.hasAttribute(DIR)) {
455                                 if (useP) {
456                                     s.all('p').setAttribute(DIR, n.getAttribute(DIR));
457                                 } else {
458                                     s.setAttribute(DIR, n.getAttribute(DIR));
459                                 }
460                             }
461                             if (useP) {
462                                 n.replace(s.get('innerHTML'));
463                             } else {
464                                 n.replace(s);
465                             }
466                             if (range.moveToElementText) {
467                                 range.moveToElementText(s._node);
468                             }
469                             range.select();
470                         } else {
471                             par = Y.one(range.parentElement());
472                             if (!par.test(inst.EditorSelection.BLOCKS)) {
473                                 par = par.ancestor(inst.EditorSelection.BLOCKS);
474                             }
475                             if (par) {
476                                 if (par.hasAttribute(DIR)) {
477                                     dir = par.getAttribute(DIR);
478                                 }
479                             }
480                             if (html.indexOf('<br>') > -1) {
481                                 html = html.split(/<br>/i);
482                             } else {
483                                 var tmp = inst.Node.create(html),
484                                 ps = tmp ? tmp.all('p') : null;
486                                 if (ps && ps.size()) {
487                                     html = [];
488                                     ps.each(function(n) {
489                                         html.push(n.get('innerHTML'));
490                                     });
491                                 } else {
492                                     html = [html];
493                                 }
494                             }
495                             list = '<' + tag + ' id="ie-list">';
496                             Y.each(html, function(v) {
497                                 var a = inst.Node.create(v);
498                                 if (a && a.test('p')) {
499                                     if (a.hasAttribute(DIR)) {
500                                         dir = a.getAttribute(DIR);
501                                     }
502                                     v = a.get('innerHTML');
503                                 }
504                                 list += '<li>' + v + '</li>';
505                             });
506                             list += '</' + tag + '>';
507                             range.pasteHTML(list);
508                             elm = inst.config.doc.getElementById('ie-list');
509                             elm.id = '';
510                             if (dir) {
511                                 elm.setAttribute(DIR, dir);
512                             }
513                             if (range.moveToElementText) {
514                                 range.moveToElementText(elm);
515                             }
516                             range.select();
517                         }
518                     } else if (Y.UA.ie) {
519                         par = inst.one(sel._selection.parentElement());
520                         if (par.test('p')) {
521                             if (par && par.hasAttribute(DIR)) {
522                                 dir = par.getAttribute(DIR);
523                             }
524                             html = Y.EditorSelection.getText(par);
525                             if (html === '') {
526                                 var sdir = '';
527                                 if (dir) {
528                                     sdir = ' dir="' + dir + '"';
529                                 }
530                                 list = inst.Node.create(Y.Lang.sub('<{tag}{dir}><li></li></{tag}>', { tag: tag, dir: sdir }));
531                                 par.replace(list);
532                                 sel.selectNode(list.one('li'));
533                             } else {
534                                 this._command(cmd, null);
535                             }
536                         } else {
537                             this._command(cmd, null);
538                         }
539                     } else {
540                         inst.all(tag).addClass(cls);
541                         if (sel.anchorNode.test(inst.EditorSelection.BLOCKS)) {
542                             par = sel.anchorNode;
543                         } else {
544                             par = sel.anchorNode.ancestor(inst.EditorSelection.BLOCKS);
545                         }
546                         if (!par) { //No parent, find the first block under the anchorNode
547                             par = sel.anchorNode.one(inst.EditorSelection.BLOCKS);
548                         }
550                         if (par && par.hasAttribute(DIR)) {
551                             dir = par.getAttribute(DIR);
552                         }
553                         if (par && par.test(tag)) {
554                             var hasPParent = par.ancestor('p');
555                             html = inst.Node.create('<div/>');
556                             elm = par.all('li');
557                             elm.each(function(h) {
558                                 html.append(self._wrapContent(h.get('innerHTML'), hasPParent));
559                             });
560                             if (dir) {
561                                 if (useP) {
562                                     html.all('p').setAttribute(DIR, dir);
563                                 } else {
564                                     html.setAttribute(DIR, dir);
565                                 }
566                             }
567                             if (useP) {
568                                 html = inst.Node.create(html.get('innerHTML'));
569                             }
570                             var fc = html.get('firstChild');
571                             par.replace(html);
572                             sel.selectNode(fc);
573                         } else {
574                             this._command(cmd, null);
575                         }
576                         list = inst.all(tag);
577                         if (dir) {
578                             if (list.size()) {
579                                 //Changed to a List
580                                 list.each(function(n) {
581                                     if (!n.hasClass(cls)) {
582                                         n.setAttribute(DIR, dir);
583                                     }
584                                 });
585                             }
586                         }
588                         list.removeClass(cls);
589                     }
590                 },
591                 /**
592                 * Noramlizes alignment for Webkit Browsers
593                 * @method COMMANDS.justify
594                 * @static
595                 * @param {String} cmd The command executed: justify (not used)
596                 * @param {String} val The actual command from the justify{center,all,left,right} stubs
597                 */
598                 justify: function(cmd, val) {
599                     if (Y.UA.webkit) {
600                         var inst = this.getInstance(),
601                             sel = new inst.EditorSelection(),
602                             aNode = sel.anchorNode;
604                             var bgColor = aNode.getStyle('backgroundColor');
605                             this._command(val);
606                             sel = new inst.EditorSelection();
607                             if (sel.anchorNode.test('div')) {
608                                 var html = '<span>' + sel.anchorNode.get('innerHTML') + '</span>';
609                                 sel.anchorNode.set('innerHTML', html);
610                                 sel.anchorNode.one('span').setStyle('backgroundColor', bgColor);
611                                 sel.selectNode(sel.anchorNode.one('span'));
612                             }
613                     } else {
614                         this._command(val);
615                     }
616                 },
617                 /**
618                 * Override method for COMMANDS.justify
619                 * @method COMMANDS.justifycenter
620                 * @static
621                 */
622                 justifycenter: function(cmd) {
623                     this.command('justify', 'justifycenter');
624                 },
625                 /**
626                 * Override method for COMMANDS.justify
627                 * @method COMMANDS.justifyleft
628                 * @static
629                 */
630                 justifyleft: function(cmd) {
631                     this.command('justify', 'justifyleft');
632                 },
633                 /**
634                 * Override method for COMMANDS.justify
635                 * @method COMMANDS.justifyright
636                 * @static
637                 */
638                 justifyright: function(cmd) {
639                     this.command('justify', 'justifyright');
640                 },
641                 /**
642                 * Override method for COMMANDS.justify
643                 * @method COMMANDS.justifyfull
644                 * @static
645                 */
646                 justifyfull: function(cmd) {
647                     this.command('justify', 'justifyfull');
648                 }
649             }
650         });
651         
652         /**
653         * This method is meant to normalize IE's in ability to exec the proper command on elements with CSS styling.
654         * @method fixIETags
655         * @protected
656         * @param {String} cmd The command to execute
657         * @param {String} tag The tag to create
658         * @param {String} rule The rule that we are looking for.
659         */
660         var fixIETags = function(cmd, tag, rule) {
661             var inst = this.getInstance(),
662                 doc = inst.config.doc,
663                 sel = doc.selection.createRange(),
664                 o = doc.queryCommandValue(cmd),
665                 html, reg, m, p, d, s, c;
667             if (o) {
668                 html = sel.htmlText;
669                 reg = new RegExp(rule, 'g');
670                 m = html.match(reg);
672                 if (m) {
673                     html = html.replace(rule + ';', '').replace(rule, '');
675                     sel.pasteHTML('<var id="yui-ie-bs">');
677                     p = doc.getElementById('yui-ie-bs');
678                     d = doc.createElement('div');
679                     s = doc.createElement(tag);
680                     
681                     d.innerHTML = html;
682                     if (p.parentNode !== inst.config.doc.body) {
683                         p = p.parentNode;
684                     }
686                     c = d.childNodes;
688                     p.parentNode.replaceChild(s, p);
690                     Y.each(c, function(f) {
691                         s.appendChild(f);
692                     });
693                     sel.collapse();
694                     if (sel.moveToElementText) {
695                         sel.moveToElementText(s);
696                     }
697                     sel.select();
698                 }
699             }
700             this._command(cmd);
701         };
703         if (Y.UA.ie) {
704             ExecCommand.COMMANDS.bold = function() {
705                 fixIETags.call(this, 'bold', 'b', 'FONT-WEIGHT: bold');
706             };
707             ExecCommand.COMMANDS.italic = function() {
708                 fixIETags.call(this, 'italic', 'i', 'FONT-STYLE: italic');
709             };
710             ExecCommand.COMMANDS.underline = function() {
711                 fixIETags.call(this, 'underline', 'u', 'TEXT-DECORATION: underline');
712             };
713         }
715         Y.namespace('Plugin');
716         Y.Plugin.ExecCommand = ExecCommand;
720 }, '3.7.2', {"requires": ["frame"]});