]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/javascript/tiny_mce/classes/EditorCommands.js
Release 6.5.0
[Github/sugarcrm.git] / include / javascript / tiny_mce / classes / EditorCommands.js
1 /**
2  * EditorCommands.js
3  *
4  * Copyright 2009, Moxiecode Systems AB
5  * Released under LGPL License.
6  *
7  * License: http://tinymce.moxiecode.com/license
8  * Contributing: http://tinymce.moxiecode.com/contributing
9  */
10
11 (function(tinymce) {
12         // Added for compression purposes
13         var each = tinymce.each, undefined, TRUE = true, FALSE = false;
14
15         /**
16          * This class enables you to add custom editor commands and it contains
17          * overrides for native browser commands to address various bugs and issues.
18          *
19          * @class tinymce.EditorCommands
20          */
21         tinymce.EditorCommands = function(editor) {
22                 var dom = editor.dom,
23                         selection = editor.selection,
24                         commands = {state: {}, exec : {}, value : {}},
25                         settings = editor.settings,
26                         bookmark;
27
28                 /**
29                  * Executes the specified command.
30                  *
31                  * @method execCommand
32                  * @param {String} command Command to execute.
33                  * @param {Boolean} ui Optional user interface state.
34                  * @param {Object} value Optional value for command.
35                  * @return {Boolean} true/false if the command was found or not.
36                  */
37                 function execCommand(command, ui, value) {
38                         var func;
39
40                         command = command.toLowerCase();
41                         if (func = commands.exec[command]) {
42                                 func(command, ui, value);
43                                 return TRUE;
44                         }
45
46                         return FALSE;
47                 };
48
49                 /**
50                  * Queries the current state for a command for example if the current selection is "bold".
51                  *
52                  * @method queryCommandState
53                  * @param {String} command Command to check the state of.
54                  * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
55                  */
56                 function queryCommandState(command) {
57                         var func;
58
59                         command = command.toLowerCase();
60                         if (func = commands.state[command])
61                                 return func(command);
62
63                         return -1;
64                 };
65
66                 /**
67                  * Queries the command value for example the current fontsize.
68                  *
69                  * @method queryCommandValue
70                  * @param {String} command Command to check the value of.
71                  * @return {Object} Command value of false if it's not found.
72                  */
73                 function queryCommandValue(command) {
74                         var func;
75
76                         command = command.toLowerCase();
77                         if (func = commands.value[command])
78                                 return func(command);
79
80                         return FALSE;
81                 };
82
83                 /**
84                  * Adds commands to the command collection.
85                  *
86                  * @method addCommands
87                  * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated.
88                  * @param {String} type Optional type to add, defaults to exec. Can be value or state as well.
89                  */
90                 function addCommands(command_list, type) {
91                         type = type || 'exec';
92
93                         each(command_list, function(callback, command) {
94                                 each(command.toLowerCase().split(','), function(command) {
95                                         commands[type][command] = callback;
96                                 });
97                         });
98                 };
99
100                 // Expose public methods
101                 tinymce.extend(this, {
102                         execCommand : execCommand,
103                         queryCommandState : queryCommandState,
104                         queryCommandValue : queryCommandValue,
105                         addCommands : addCommands
106                 });
107
108                 // Private methods
109
110                 function execNativeCommand(command, ui, value) {
111                         if (ui === undefined)
112                                 ui = FALSE;
113
114                         if (value === undefined)
115                                 value = null;
116
117                         return editor.getDoc().execCommand(command, ui, value);
118                 };
119
120                 function isFormatMatch(name) {
121                         return editor.formatter.match(name);
122                 };
123
124                 function toggleFormat(name, value) {
125                         editor.formatter.toggle(name, value ? {value : value} : undefined);
126                 };
127
128                 function storeSelection(type) {
129                         bookmark = selection.getBookmark(type);
130                 };
131
132                 function restoreSelection() {
133                         selection.moveToBookmark(bookmark);
134                 };
135
136                 // Add execCommand overrides
137                 addCommands({
138                         // Ignore these, added for compatibility
139                         'mceResetDesignMode,mceBeginUndoLevel' : function() {},
140
141                         // Add undo manager logic
142                         'mceEndUndoLevel,mceAddUndoLevel' : function() {
143                                 editor.undoManager.add();
144                         },
145
146                         'Cut,Copy,Paste' : function(command) {
147                                 var doc = editor.getDoc(), failed;
148
149                                 // Try executing the native command
150                                 try {
151                                         execNativeCommand(command);
152                                 } catch (ex) {
153                                         // Command failed
154                                         failed = TRUE;
155                                 }
156
157                                 // Present alert message about clipboard access not being available
158                                 if (failed || !doc.queryCommandSupported(command)) {
159                                         if (tinymce.isGecko) {
160                                                 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
161                                                         if (state)
162                                                                 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
163                                                 });
164                                         } else
165                                                 editor.windowManager.alert(editor.getLang('clipboard_no_support'));
166                                 }
167                         },
168
169                         // Override unlink command
170                         unlink : function(command) {
171                                 if (selection.isCollapsed())
172                                         selection.select(selection.getNode());
173
174                                 execNativeCommand(command);
175                                 selection.collapse(FALSE);
176                         },
177
178                         // Override justify commands to use the text formatter engine
179                         'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
180                                 var align = command.substring(7);
181
182                                 // Remove all other alignments first
183                                 each('left,center,right,full'.split(','), function(name) {
184                                         if (align != name)
185                                                 editor.formatter.remove('align' + name);
186                                 });
187
188                                 toggleFormat('align' + align);
189                                 execCommand('mceRepaint');
190                         },
191
192                         // Override list commands to fix WebKit bug
193                         'InsertUnorderedList,InsertOrderedList' : function(command) {
194                                 var listElm, listParent;
195
196                                 execNativeCommand(command);
197
198                                 // WebKit produces lists within block elements so we need to split them
199                                 // we will replace the native list creation logic to custom logic later on
200                                 // TODO: Remove this when the list creation logic is removed
201                                 listElm = dom.getParent(selection.getNode(), 'ol,ul');
202                                 if (listElm) {
203                                         listParent = listElm.parentNode;
204
205                                         // If list is within a text block then split that block
206                                         if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
207                                                 storeSelection();
208                                                 dom.split(listParent, listElm);
209                                                 restoreSelection();
210                                         }
211                                 }
212                         },
213
214                         // Override commands to use the text formatter engine
215                         'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
216                                 toggleFormat(command);
217                         },
218
219                         // Override commands to use the text formatter engine
220                         'ForeColor,HiliteColor,FontName' : function(command, ui, value) {
221                                 toggleFormat(command, value);
222                         },
223
224                         FontSize : function(command, ui, value) {
225                                 var fontClasses, fontSizes;
226
227                                 // Convert font size 1-7 to styles
228                                 if (value >= 1 && value <= 7) {
229                                         fontSizes = tinymce.explode(settings.font_size_style_values);
230                                         fontClasses = tinymce.explode(settings.font_size_classes);
231
232                                         if (fontClasses)
233                                                 value = fontClasses[value - 1] || value;
234                                         else
235                                                 value = fontSizes[value - 1] || value;
236                                 }
237
238                                 toggleFormat(command, value);
239                         },
240
241                         RemoveFormat : function(command) {
242                                 editor.formatter.remove(command);
243                         },
244
245                         mceBlockQuote : function(command) {
246                                 toggleFormat('blockquote');
247                         },
248
249                         FormatBlock : function(command, ui, value) {
250                                 return toggleFormat(value || 'p');
251                         },
252
253                         mceCleanup : function() {
254                                 var bookmark = selection.getBookmark();
255
256                                 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});
257
258                                 selection.moveToBookmark(bookmark);
259                         },
260
261                         mceRemoveNode : function(command, ui, value) {
262                                 var node = value || selection.getNode();
263
264                                 // Make sure that the body node isn't removed
265                                 if (node != editor.getBody()) {
266                                         storeSelection();
267                                         editor.dom.remove(node, TRUE);
268                                         restoreSelection();
269                                 }
270                         },
271
272                         mceSelectNodeDepth : function(command, ui, value) {
273                                 var counter = 0;
274
275                                 dom.getParent(selection.getNode(), function(node) {
276                                         if (node.nodeType == 1 && counter++ == value) {
277                                                 selection.select(node);
278                                                 return FALSE;
279                                         }
280                                 }, editor.getBody());
281                         },
282
283                         mceSelectNode : function(command, ui, value) {
284                                 selection.select(value);
285                         },
286
287                         mceInsertContent : function(command, ui, value) {
288                                 var parser, serializer, parentNode, rootNode, fragment, args,
289                                         marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;
290
291                                 // Setup parser and serializer
292                                 parser = editor.parser;
293                                 serializer = new tinymce.html.Serializer({}, editor.schema);
294                                 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>';
295
296                                 // Run beforeSetContent handlers on the HTML to be inserted
297                                 args = {content: value, format: 'html'};
298                                 selection.onBeforeSetContent.dispatch(selection, args);
299                                 value = args.content;
300
301                                 // Add caret at end of contents if it's missing
302                                 if (value.indexOf('{$caret}') == -1)
303                                         value += '{$caret}';
304
305                                 // Replace the caret marker with a span bookmark element
306                                 value = value.replace(/\{\$caret\}/, bookmarkHtml);
307
308                                 // Insert node maker where we will insert the new HTML and get it's parent
309                                 if (!selection.isCollapsed())
310                                         editor.getDoc().execCommand('Delete', false, null);
311
312                                 parentNode = selection.getNode();
313
314                                 // Parse the fragment within the context of the parent node
315                                 args = {context : parentNode.nodeName.toLowerCase()};
316                                 fragment = parser.parse(value, args);
317
318                                 // Move the caret to a more suitable location
319                                 node = fragment.lastChild;
320                                 if (node.attr('id') == 'mce_marker') {
321                                         marker = node;
322
323                                         for (node = node.prev; node; node = node.walk(true)) {
324                                                 if (node.type == 3 || !dom.isBlock(node.name)) {
325                                                         node.parent.insert(marker, node, node.name === 'br');
326                                                         break;
327                                                 }
328                                         }
329                                 }
330
331                                 // If parser says valid we can insert the contents into that parent
332                                 if (!args.invalid) {
333                                         value = serializer.serialize(fragment);
334
335                                         // Check if parent is empty or only has one BR element then set the innerHTML of that parent
336                                         node = parentNode.firstChild;
337                                         node2 = parentNode.lastChild;
338                                         if (!node || (node === node2 && node.nodeName === 'BR'))
339                                                 dom.setHTML(parentNode, value);
340                                         else
341                                                 selection.setContent(value);
342                                 } else {
343                                         // If the fragment was invalid within that context then we need
344                                         // to parse and process the parent it's inserted into
345
346                                         // Insert bookmark node and get the parent
347                                         selection.setContent(bookmarkHtml);
348                                         parentNode = editor.selection.getNode();
349                                         rootNode = editor.getBody();
350
351                                         // Opera will return the document node when selection is in root
352                                         if (parentNode.nodeType == 9)
353                                                 parentNode = node = rootNode;
354                                         else
355                                                 node = parentNode;
356
357                                         // Find the ancestor just before the root element
358                                         while (node !== rootNode) {
359                                                 parentNode = node;
360                                                 node = node.parentNode;
361                                         }
362
363                                         // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
364                                         value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
365                                         value = serializer.serialize(
366                                                 parser.parse(
367                                                         // Need to replace by using a function since $ in the contents would otherwise be a problem
368                                                         value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
369                                                                 return serializer.serialize(fragment);
370                                                         })
371                                                 )
372                                         );
373
374                                         // Set the inner/outer HTML depending on if we are in the root or not
375                                         if (parentNode == rootNode)
376                                                 dom.setHTML(rootNode, value);
377                                         else
378                                                 dom.setOuterHTML(parentNode, value);
379                                 }
380
381                                 marker = dom.get('mce_marker');
382
383                                 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
384                                 nodeRect = dom.getRect(marker);
385                                 viewPortRect = dom.getViewPort(editor.getWin());
386
387                                 // Check if node is out side the viewport if it is then scroll to it
388                                 if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
389                                         (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
390                                         viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody();
391                                         viewportBodyElement.scrollLeft = nodeRect.x;
392                                         viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25;
393                                 }
394
395                                 // Move selection before marker and remove it
396                                 rng = dom.createRng();
397
398                                 // If previous sibling is a text node set the selection to the end of that node
399                                 node = marker.previousSibling;
400                                 if (node && node.nodeType == 3) {
401                                         rng.setStart(node, node.nodeValue.length);
402                                 } else {
403                                         // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
404                                         rng.setStartBefore(marker);
405                                         rng.setEndBefore(marker);
406                                 }
407
408                                 // Remove the marker node and set the new range
409                                 dom.remove(marker);
410                                 selection.setRng(rng);
411
412                                 // Dispatch after event and add any visual elements needed
413                                 selection.onSetContent.dispatch(selection, args);
414                                 editor.addVisual();
415                         },
416
417                         mceInsertRawHTML : function(command, ui, value) {
418                                 selection.setContent('tiny_mce_marker');
419                                 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
420                         },
421
422                         mceSetContent : function(command, ui, value) {
423                                 editor.setContent(value);
424                         },
425
426                         'Indent,Outdent' : function(command) {
427                                 var intentValue, indentUnit, value;
428
429                                 // Setup indent level
430                                 intentValue = settings.indentation;
431                                 indentUnit = /[a-z%]+$/i.exec(intentValue);
432                                 intentValue = parseInt(intentValue);
433
434                                 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
435                                         each(selection.getSelectedBlocks(), function(element) {
436                                                 if (command == 'outdent') {
437                                                         value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
438                                                         dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
439                                                 } else
440                                                         dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
441                                         });
442                                 } else
443                                         execNativeCommand(command);
444                         },
445
446                         mceRepaint : function() {
447                                 var bookmark;
448
449                                 if (tinymce.isGecko) {
450                                         try {
451                                                 storeSelection(TRUE);
452
453                                                 if (selection.getSel())
454                                                         selection.getSel().selectAllChildren(editor.getBody());
455
456                                                 selection.collapse(TRUE);
457                                                 restoreSelection();
458                                         } catch (ex) {
459                                                 // Ignore
460                                         }
461                                 }
462                         },
463
464                         mceToggleFormat : function(command, ui, value) {
465                                 editor.formatter.toggle(value);
466                         },
467
468                         InsertHorizontalRule : function() {
469                                 editor.execCommand('mceInsertContent', false, '<hr />');
470                         },
471
472                         mceToggleVisualAid : function() {
473                                 editor.hasVisual = !editor.hasVisual;
474                                 editor.addVisual();
475                         },
476
477                         mceReplaceContent : function(command, ui, value) {
478                                 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
479                         },
480
481                         mceInsertLink : function(command, ui, value) {
482                                 var link = dom.getParent(selection.getNode(), 'a'), img, style, cls;
483
484                                 if (tinymce.is(value, 'string'))
485                                         value = {href : value};
486
487                                 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
488                                 value.href = value.href.replace(' ', '%20');
489
490                                 if (!link) {
491                                         // WebKit can't create links on floated images for some odd reason
492                                         // So, just remove styles and restore it later
493                                         if (tinymce.isWebKit) {
494                                                 img = dom.getParent(selection.getNode(), 'img');
495
496                                                 if (img) {
497                                                         style = img.style.cssText;
498                                                         cls = img.className;
499                                                         img.style.cssText = null;
500                                                         img.className = null;
501                                                 }
502                                         }
503
504                                         execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);');
505
506                                         // Restore styles
507                                         if (style)
508                                                 img.style.cssText = style;
509                                         if (cls)
510                                                 img.className = cls;
511
512                                         each(dom.select("a[href='javascript:mctmp(0);']"), function(link) {
513                                                 dom.setAttribs(link, value);
514                                         });
515                                 } else {
516                                         if (value.href)
517                                                 dom.setAttribs(link, value);
518                                         else
519                                                 editor.dom.remove(link, TRUE);
520                                 }
521                         },
522                         
523                         selectAll : function() {
524                                 var root = dom.getRoot(), rng = dom.createRng();
525
526                                 rng.setStart(root, 0);
527                                 rng.setEnd(root, root.childNodes.length);
528
529                                 editor.selection.setRng(rng);
530                         }
531                 });
532
533                 // Add queryCommandState overrides
534                 addCommands({
535                         // Override justify commands
536                         'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
537                                 return isFormatMatch('align' + command.substring(7));
538                         },
539
540                         'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
541                                 return isFormatMatch(command);
542                         },
543
544                         mceBlockQuote : function() {
545                                 return isFormatMatch('blockquote');
546                         },
547
548                         Outdent : function() {
549                                 var node;
550
551                                 if (settings.inline_styles) {
552                                         if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
553                                                 return TRUE;
554
555                                         if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
556                                                 return TRUE;
557                                 }
558
559                                 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
560                         },
561
562                         'InsertUnorderedList,InsertOrderedList' : function(command) {
563                                 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');
564                         }
565                 }, 'state');
566
567                 // Add queryCommandValue overrides
568                 addCommands({
569                         'FontSize,FontName' : function(command) {
570                                 var value = 0, parent;
571
572                                 if (parent = dom.getParent(selection.getNode(), 'span')) {
573                                         if (command == 'fontsize')
574                                                 value = parent.style.fontSize;
575                                         else
576                                                 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
577                                 }
578
579                                 return value;
580                         }
581                 }, 'value');
582
583                 // Add undo manager logic
584                 if (settings.custom_undo_redo) {
585                         addCommands({
586                                 Undo : function() {
587                                         editor.undoManager.undo();
588                                 },
589
590                                 Redo : function() {
591                                         editor.undoManager.redo();
592                                 }
593                         });
594                 }
595         };
596 })(tinymce);