Thomas Bruederli
2012-07-30 6843acc9fced3dc1261e019ad4552b6e4c340ec7
commit | author | age
d6284b 1 /**
T 2  * editor_plugin_src.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() {
e6e0d4 12     var TreeWalker = tinymce.dom.TreeWalker;
AM 13     var externalName = 'contenteditable', internalName = 'data-mce-' + externalName;
14     var VK = tinymce.VK;
15
16     function handleContentEditableSelection(ed) {
750fcf 17         var dom = ed.dom, selection = ed.selection, invisibleChar, caretContainerId = 'mce_noneditablecaret', invisibleChar = '\uFEFF';
e6e0d4 18
AM 19         // Returns the content editable state of a node "true/false" or null
20         function getContentEditable(node) {
21             var contentEditable;
22
23             // Ignore non elements
24             if (node.nodeType === 1) {
25                 // Check for fake content editable
26                 contentEditable = node.getAttribute(internalName);
27                 if (contentEditable && contentEditable !== "inherit") {
28                     return contentEditable;
29                 }
30
31                 // Check for real content editable
32                 contentEditable = node.contentEditable;
33                 if (contentEditable !== "inherit") {
34                     return contentEditable;
35                 }
36             }
37
38             return null;
39         };
40
41         // Returns the noneditable parent or null if there is a editable before it or if it wasn't found
42         function getNonEditableParent(node) {
43             var state;
44
45             while (node) {
46                 state = getContentEditable(node);
47                 if (state) {
48                     return state  === "false" ? node : null;
49                 }
50
51                 node = node.parentNode;
52             }
53         };
54
55         // Get caret container parent for the specified node
56         function getParentCaretContainer(node) {
57             while (node) {
58                 if (node.id === caretContainerId) {
59                     return node;
60                 }
61
62                 node = node.parentNode;
63             }
64         };
65
66         // Finds the first text node in the specified node
67         function findFirstTextNode(node) {
68             var walker;
69
70             if (node) {
71                 walker = new TreeWalker(node, node);
72
73                 for (node = walker.current(); node; node = walker.next()) {
74                     if (node.nodeType === 3) {
75                         return node;
76                     }
77                 }
78             }
79         };
80
81         // Insert caret container before/after target or expand selection to include block
82         function insertCaretContainerOrExpandToBlock(target, before) {
83             var caretContainer, rng;
84
85             // Select block
86             if (getContentEditable(target) === "false") {
87                 if (dom.isBlock(target)) {
88                     selection.select(target);
89                     return;
90                 }
91             }
92
93             rng = dom.createRng();
94
95             if (getContentEditable(target) === "true") {
96                 if (!target.firstChild) {
97                     target.appendChild(ed.getDoc().createTextNode('\u00a0'));
98                 }
99
100                 target = target.firstChild;
101                 before = true;
102             }
103
104             //caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style:'border: 1px solid red'}, invisibleChar);
105             caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true}, invisibleChar);
106
107             if (before) {
108                 target.parentNode.insertBefore(caretContainer, target);
109             } else {
110                 dom.insertAfter(caretContainer, target);
111             }
112
113             rng.setStart(caretContainer.firstChild, 1);
114             rng.collapse(true);
115             selection.setRng(rng);
116
117             return caretContainer;
118         };
119
120         // Removes any caret container except the one we might be in
121         function removeCaretContainer(caretContainer) {
122             var child, currentCaretContainer, lastContainer;
123
124             if (caretContainer) {
125                     rng = selection.getRng(true);
126                     rng.setStartBefore(caretContainer);
127                     rng.setEndBefore(caretContainer);
128
129                     child = findFirstTextNode(caretContainer);
130                     if (child && child.nodeValue.charAt(0) == invisibleChar) {
131                         child = child.deleteData(0, 1);
132                     }
133
134                     dom.remove(caretContainer, true);
135
136                     selection.setRng(rng);
137             } else {
138                 currentCaretContainer = getParentCaretContainer(selection.getStart());
139                 while ((caretContainer = dom.get(caretContainerId)) && caretContainer !== lastContainer) {
140                     if (currentCaretContainer !== caretContainer) {
141                         child = findFirstTextNode(caretContainer);
142                         if (child && child.nodeValue.charAt(0) == invisibleChar) {
143                             child = child.deleteData(0, 1);
144                         }
145
146                         dom.remove(caretContainer, true);
147                     }
148
149                     lastContainer = caretContainer;
150                 }
151             }
152         };
153
154         // Modifies the selection to include contentEditable false elements or insert caret containers
155         function moveSelection() {
156             var nonEditableStart, nonEditableEnd, isCollapsed, rng, element;
157
158             // Checks if there is any contents to the left/right side of caret returns the noneditable element or any editable element if it finds one inside
159             function hasSideContent(element, left) {
160                 var container, offset, walker, node, len;
161
162                 container = rng.startContainer;
163                 offset = rng.startOffset;
164
165                 // If endpoint is in middle of text node then expand to beginning/end of element
166                 if (container.nodeType == 3) {
167                     len = container.nodeValue.length;
168                     if ((offset > 0 && offset < len) || (left ? offset == len : offset == 0)) {
169                         return;
170                     }
171                 } else {
172                     // Can we resolve the node by index
173                     if (offset < container.childNodes.length) {
174                         // Browser represents caret position as the offset at the start of an element. When moving right
175                         // this is the element we are moving into so we consider our container to be child node at offset-1
176                         var pos = !left && offset > 0 ? offset-1 : offset;
177                         container = container.childNodes[pos];
178                         if (container.hasChildNodes()) {
179                             container = container.firstChild;
180                         }
181                     } else {
182                         // If not then the caret is at the last position in it's container and the caret container should be inserted after the noneditable element
183                         return !left ? element : null;
184                     }
185                 }
186
187                 // Walk left/right to look for contents
188                 walker = new TreeWalker(container, element);
189                 while (node = walker[left ? 'prev' : 'next']()) {
190                     if (node.nodeType === 3 && node.nodeValue.length > 0) {
191                         return;
192                     } else if (getContentEditable(node) === "true") {
193                         // Found contentEditable=true element return this one to we can move the caret inside it
194                         return node;
195                     }
196                 }
197
198                 return element;
199             };
200
201             // Remove any existing caret containers
202             removeCaretContainer();
203
204             // Get noneditable start/end elements
205             isCollapsed = selection.isCollapsed();
206             nonEditableStart = getNonEditableParent(selection.getStart());
207             nonEditableEnd = getNonEditableParent(selection.getEnd());
208
209             // Is any fo the range endpoints noneditable
210             if (nonEditableStart || nonEditableEnd) {
211                 rng = selection.getRng(true);
212
213                 // If it's a caret selection then look left/right to see if we need to move the caret out side or expand
214                 if (isCollapsed) {
215                     nonEditableStart = nonEditableStart || nonEditableEnd;
216                     var start = selection.getStart();
217                     if (element = hasSideContent(nonEditableStart, true)) {
218                         // We have no contents to the left of the caret then insert a caret container before the noneditable element
219                         insertCaretContainerOrExpandToBlock(element, true);
220                     } else if (element = hasSideContent(nonEditableStart, false)) {
221                         // We have no contents to the right of the caret then insert a caret container after the noneditable element
222                         insertCaretContainerOrExpandToBlock(element, false);
223                     } else {
224                         // We are in the middle of a noneditable so expand to select it
225                         selection.select(nonEditableStart);
226                     }
227                 } else {
228                     rng = selection.getRng(true);
229
230                     // Expand selection to include start non editable element
231                     if (nonEditableStart) {
232                         rng.setStartBefore(nonEditableStart);
233                     }
234
235                     // Expand selection to include end non editable element
236                     if (nonEditableEnd) {
237                         rng.setEndAfter(nonEditableEnd);
238                     }
239
240                     selection.setRng(rng);
241                 }
242             }
243         };
244
245         function handleKey(ed, e) {
246             var keyCode = e.keyCode, nonEditableParent, caretContainer, startElement, endElement;
247
248             function getNonEmptyTextNodeSibling(node, prev) {
249                 while (node = node[prev ? 'previousSibling' : 'nextSibling']) {
250                     if (node.nodeType !== 3 || node.nodeValue.length > 0) {
251                         return node;
252                     }
253                 }
254             };
255
256             function positionCaretOnElement(element, start) {
257                 selection.select(element);
258                 selection.collapse(start);
259             }
260
261             function canDelete(backspace) {
262                 var rng, container, offset, nonEditableParent;
263
264                 function removeNodeIfNotParent(node) {
265                     var parent = container;
266
267                     while (parent) {
268                         if (parent === node) {
269                             return;
270                         }
271
272                         parent = parent.parentNode;
273                     }
274
275                     dom.remove(node);
276                     moveSelection();
277                 }
278
279                 function isNextPrevTreeNodeNonEditable() {
280                     var node, walker, nonEmptyElements = ed.schema.getNonEmptyElements();
281
282                     walker = new tinymce.dom.TreeWalker(container, ed.getBody());
283                     while (node = (backspace ? walker.prev() : walker.next())) {
284                         // Found IMG/INPUT etc
285                         if (nonEmptyElements[node.nodeName.toLowerCase()]) {
286                             break;
287                         }
288
289                         // Found text node with contents
290                         if (node.nodeType === 3 && tinymce.trim(node.nodeValue).length > 0) {
291                             break;
292                         }
293
294                         // Found non editable node
295                         if (getContentEditable(node) === "false") {
296                             removeNodeIfNotParent(node);
297                             return true;
298                         }
299                     }
300
301                     // Check if the content node is within a non editable parent
302                     if (getNonEditableParent(node)) {
303                         return true;
304                     }
305
306                     return false;
307                 }
308
309                 if (selection.isCollapsed()) {
310                     rng = selection.getRng(true);
311                     container = rng.startContainer;
312                     offset = rng.startOffset;
313                     container = getParentCaretContainer(container) || container;
314
315                     // Is in noneditable parent
316                     if (nonEditableParent = getNonEditableParent(container)) {
317                         removeNodeIfNotParent(nonEditableParent);
318                         return false;
319                     }
320
321                     // Check if the caret is in the middle of a text node
322                     if (container.nodeType == 3 && (backspace ? offset > 0 : offset < container.nodeValue.length)) {
323                         return true;
324                     }
325
326                     // Resolve container index
327                     if (container.nodeType == 1) {
328                         container = container.childNodes[offset] || container;
329                     }
330
331                     // Check if previous or next tree node is non editable then block the event
332                     if (isNextPrevTreeNodeNonEditable()) {
333                         return false;
334                     }
335                 }
336
337                 return true;
338             }
339
340             startElement = selection.getStart()
341             endElement = selection.getEnd();
342
343             // Disable all key presses in contentEditable=false except delete or backspace
344             nonEditableParent = getNonEditableParent(startElement) || getNonEditableParent(endElement);
345             if (nonEditableParent && (keyCode < 112 || keyCode > 124) && keyCode != VK.DELETE && keyCode != VK.BACKSPACE) {
346                 // Is Ctrl+c, Ctrl+v or Ctrl+x then use default browser behavior
347                 if ((tinymce.isMac ? e.metaKey : e.ctrlKey) && (keyCode == 67 || keyCode == 88 || keyCode == 86)) {
348                     return;
349                 }
350
351                 e.preventDefault();
352
353                 // Arrow left/right select the element and collapse left/right
354                 if (keyCode == VK.LEFT || keyCode == VK.RIGHT) {
355                     var left = keyCode == VK.LEFT;
356                     // If a block element find previous or next element to position the caret
357                     if (ed.dom.isBlock(nonEditableParent)) {
358                         var targetElement = left ? nonEditableParent.previousSibling : nonEditableParent.nextSibling;
359                         var walker = new TreeWalker(targetElement, targetElement);
360                         var caretElement = left ? walker.prev() : walker.next();
361                         positionCaretOnElement(caretElement, !left);
362                     } else {
363                         positionCaretOnElement(nonEditableParent, left);
364                     }
365                 }
366             } else {
367                 // Is arrow left/right, backspace or delete
368                 if (keyCode == VK.LEFT || keyCode == VK.RIGHT || keyCode == VK.BACKSPACE || keyCode == VK.DELETE) {
369                     caretContainer = getParentCaretContainer(startElement);
370                     if (caretContainer) {
371                         // Arrow left or backspace
372                         if (keyCode == VK.LEFT || keyCode == VK.BACKSPACE) {
373                             nonEditableParent = getNonEmptyTextNodeSibling(caretContainer, true);
374
375                             if (nonEditableParent && getContentEditable(nonEditableParent) === "false") {
376                                 e.preventDefault();
377
378                                 if (keyCode == VK.LEFT) {
379                                     positionCaretOnElement(nonEditableParent, true);
380                                 } else {
381                                     dom.remove(nonEditableParent);
382                                     return;
383                                 }
384                             } else {
385                                 removeCaretContainer(caretContainer);
386                             }
387                         }
388
389                         // Arrow right or delete
390                         if (keyCode == VK.RIGHT || keyCode == VK.DELETE) {
391                             nonEditableParent = getNonEmptyTextNodeSibling(caretContainer);
392
393                             if (nonEditableParent && getContentEditable(nonEditableParent) === "false") {
394                                 e.preventDefault();
395
396                                 if (keyCode == VK.RIGHT) {
397                                     positionCaretOnElement(nonEditableParent, false);
398                                 } else {
399                                     dom.remove(nonEditableParent);
400                                     return;
401                                 }
402                             } else {
403                                 removeCaretContainer(caretContainer);
404                             }
405                         }
406                     }
407
408                     if ((keyCode == VK.BACKSPACE || keyCode == VK.DELETE) && !canDelete(keyCode == VK.BACKSPACE)) {
409                         e.preventDefault();
410                         return false;
411                     }
412                 }
413             }
414         };
415
416         ed.onMouseDown.addToTop(function(ed, e) {
417             var node = ed.selection.getNode();
418
419             if (getContentEditable(node) === "false" && node == e.target) {
420                 // Expand selection on mouse down we can't block the default event since it's used for drag/drop
421                 moveSelection();
422             }
423         });
424
425         ed.onMouseUp.addToTop(moveSelection);
426         ed.onKeyDown.addToTop(handleKey);
427         ed.onKeyUp.addToTop(moveSelection);
428     };
d6284b 429
T 430     tinymce.create('tinymce.plugins.NonEditablePlugin', {
431         init : function(ed, url) {
e6e0d4 432             var editClass, nonEditClass, nonEditableRegExps;
d6284b 433
e6e0d4 434             // Converts configured regexps to noneditable span items
AM 435             function convertRegExpsToNonEditable(ed, args) {
436                 var i = nonEditableRegExps.length, content = args.content, cls = tinymce.trim(nonEditClass);
d6284b 437
e6e0d4 438                 // Don't replace the variables when raw is used for example on undo/redo
AM 439                 if (args.format == "raw") {
440                     return;
d6284b 441                 }
e6e0d4 442
AM 443                 while (i--) {
444                     content = content.replace(nonEditableRegExps[i], function(match) {
445                         var args = arguments, index = args[args.length - 2];
446
447                         // Is value inside an attribute then don't replace
448                         if (index > 0 && content.charAt(index - 1) == '"') {
449                             return match;
450                         }
451
452                         return '<span class="' + cls + '" data-mce-content="' + ed.dom.encode(args[0]) + '">' + ed.dom.encode(typeof(args[1]) === "string" ? args[1] : args[0]) + '</span>';
453                     });
454                 }
455
456                 args.content = content;
457             };
458             
459             editClass = " " + tinymce.trim(ed.getParam("noneditable_editable_class", "mceEditable")) + " ";
460             nonEditClass = " " + tinymce.trim(ed.getParam("noneditable_noneditable_class", "mceNonEditable")) + " ";
461
462             // Setup noneditable regexps array
463             nonEditableRegExps = ed.getParam("noneditable_regexp");
464             if (nonEditableRegExps && !nonEditableRegExps.length) {
465                 nonEditableRegExps = [nonEditableRegExps];
466             }
467
468             ed.onPreInit.add(function() {
469                 handleContentEditableSelection(ed);
470
471                 if (nonEditableRegExps) {
472                     ed.selection.onBeforeSetContent.add(convertRegExpsToNonEditable);
473                     ed.onBeforeSetContent.add(convertRegExpsToNonEditable);
474                 }
475
476                 // Apply contentEditable true/false on elements with the noneditable/editable classes
477                 ed.parser.addAttributeFilter('class', function(nodes) {
478                     var i = nodes.length, className, node;
479
480                     while (i--) {
481                         node = nodes[i];
482                         className = " " + node.attr("class") + " ";
483
484                         if (className.indexOf(editClass) !== -1) {
485                             node.attr(internalName, "true");
486                         } else if (className.indexOf(nonEditClass) !== -1) {
487                             node.attr(internalName, "false");
488                         }
489                     }
490                 });
491
492                 // Remove internal name
493                 ed.serializer.addAttributeFilter(internalName, function(nodes, name) {
494                     var i = nodes.length, node;
495
496                     while (i--) {
497                         node = nodes[i];
498
499                         if (nonEditableRegExps && node.attr('data-mce-content')) {
500                             node.name = "#text";
501                             node.type = 3;
502                             node.raw = true;
503                             node.value = node.attr('data-mce-content');
504                         } else {
505                             node.attr(externalName, null);
506                             node.attr(internalName, null);
507                         }
508                     }
509                 });
510
511                 // Convert external name into internal name
512                 ed.parser.addAttributeFilter(externalName, function(nodes, name) {
513                     var i = nodes.length, node;
514
515                     while (i--) {
516                         node = nodes[i];
517                         node.attr(internalName, node.attr(externalName));
518                         node.attr(externalName, null);
519                     }
520                 });
d6284b 521             });
T 522         },
523
524         getInfo : function() {
525             return {
526                 longname : 'Non editable elements',
527                 author : 'Moxiecode Systems AB',
528                 authorurl : 'http://tinymce.moxiecode.com',
529                 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/noneditable',
530                 version : tinymce.majorVersion + "." + tinymce.minorVersion
531             };
532         }
533     });
534
535     // Register plugin
536     tinymce.PluginManager.add('noneditable', tinymce.plugins.NonEditablePlugin);
537 })();