|
1 /** |
|
2 * Formatter.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /** |
|
12 * Text formatter engine class. This class is used to apply formats like bold, italic, font size |
|
13 * etc to the current selection or specific nodes. This engine was build to replace the browsers |
|
14 * default formatting logic for execCommand due to it's inconsistent and buggy behavior. |
|
15 * |
|
16 * @class tinymce.Formatter |
|
17 * @example |
|
18 * tinymce.activeEditor.formatter.register('mycustomformat', { |
|
19 * inline: 'span', |
|
20 * styles: {color: '#ff0000'} |
|
21 * }); |
|
22 * |
|
23 * tinymce.activeEditor.formatter.apply('mycustomformat'); |
|
24 */ |
|
25 define("tinymce/Formatter", [ |
|
26 "tinymce/dom/TreeWalker", |
|
27 "tinymce/dom/RangeUtils", |
|
28 "tinymce/dom/BookmarkManager", |
|
29 "tinymce/dom/ElementUtils", |
|
30 "tinymce/util/Tools", |
|
31 "tinymce/fmt/Preview" |
|
32 ], function(TreeWalker, RangeUtils, BookmarkManager, ElementUtils, Tools, Preview) { |
|
33 /** |
|
34 * Constructs a new formatter instance. |
|
35 * |
|
36 * @constructor Formatter |
|
37 * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. |
|
38 */ |
|
39 return function(ed) { |
|
40 var formats = {}, |
|
41 dom = ed.dom, |
|
42 selection = ed.selection, |
|
43 rangeUtils = new RangeUtils(dom), |
|
44 isValid = ed.schema.isValidChild, |
|
45 isBlock = dom.isBlock, |
|
46 forcedRootBlock = ed.settings.forced_root_block, |
|
47 nodeIndex = dom.nodeIndex, |
|
48 INVISIBLE_CHAR = '\uFEFF', |
|
49 MCE_ATTR_RE = /^(src|href|style)$/, |
|
50 FALSE = false, |
|
51 TRUE = true, |
|
52 formatChangeData, |
|
53 undef, |
|
54 getContentEditable = dom.getContentEditable, |
|
55 disableCaretContainer, |
|
56 markCaretContainersBogus, |
|
57 isBookmarkNode = BookmarkManager.isBookmarkNode; |
|
58 |
|
59 var each = Tools.each, |
|
60 grep = Tools.grep, |
|
61 walk = Tools.walk, |
|
62 extend = Tools.extend; |
|
63 |
|
64 function isTextBlock(name) { |
|
65 if (name.nodeType) { |
|
66 name = name.nodeName; |
|
67 } |
|
68 |
|
69 return !!ed.schema.getTextBlockElements()[name.toLowerCase()]; |
|
70 } |
|
71 |
|
72 function isTableCell(node) { |
|
73 return /^(TH|TD)$/.test(node.nodeName); |
|
74 } |
|
75 |
|
76 function getParents(node, selector) { |
|
77 return dom.getParents(node, selector, dom.getRoot()); |
|
78 } |
|
79 |
|
80 function isCaretNode(node) { |
|
81 return node.nodeType === 1 && node.id === '_mce_caret'; |
|
82 } |
|
83 |
|
84 function defaultFormats() { |
|
85 register({ |
|
86 valigntop: [ |
|
87 {selector: 'td,th', styles: {'verticalAlign': 'top'}} |
|
88 ], |
|
89 |
|
90 valignmiddle: [ |
|
91 {selector: 'td,th', styles: {'verticalAlign': 'middle'}} |
|
92 ], |
|
93 |
|
94 valignbottom: [ |
|
95 {selector: 'td,th', styles: {'verticalAlign': 'bottom'}} |
|
96 ], |
|
97 |
|
98 alignleft: [ |
|
99 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'}, |
|
100 {selector: 'img,table', collapsed: false, styles: {'float': 'left'}} |
|
101 ], |
|
102 |
|
103 aligncenter: [ |
|
104 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}, defaultBlock: 'div'}, |
|
105 {selector: 'img', collapsed: false, styles: {display: 'block', marginLeft: 'auto', marginRight: 'auto'}}, |
|
106 {selector: 'table', collapsed: false, styles: {marginLeft: 'auto', marginRight: 'auto'}} |
|
107 ], |
|
108 |
|
109 alignright: [ |
|
110 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}, defaultBlock: 'div'}, |
|
111 {selector: 'img,table', collapsed: false, styles: {'float': 'right'}} |
|
112 ], |
|
113 |
|
114 alignjustify: [ |
|
115 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'justify'}, defaultBlock: 'div'} |
|
116 ], |
|
117 |
|
118 bold: [ |
|
119 {inline: 'strong', remove: 'all'}, |
|
120 {inline: 'span', styles: {fontWeight: 'bold'}}, |
|
121 {inline: 'b', remove: 'all'} |
|
122 ], |
|
123 |
|
124 italic: [ |
|
125 {inline: 'em', remove: 'all'}, |
|
126 {inline: 'span', styles: {fontStyle: 'italic'}}, |
|
127 {inline: 'i', remove: 'all'} |
|
128 ], |
|
129 |
|
130 underline: [ |
|
131 {inline: 'span', styles: {textDecoration: 'underline'}, exact: true}, |
|
132 {inline: 'u', remove: 'all'} |
|
133 ], |
|
134 |
|
135 strikethrough: [ |
|
136 {inline: 'span', styles: {textDecoration: 'line-through'}, exact: true}, |
|
137 {inline: 'strike', remove: 'all'} |
|
138 ], |
|
139 |
|
140 forecolor: {inline: 'span', styles: {color: '%value'}, links: true, remove_similar: true}, |
|
141 hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, links: true, remove_similar: true}, |
|
142 fontname: {inline: 'span', styles: {fontFamily: '%value'}}, |
|
143 fontsize: {inline: 'span', styles: {fontSize: '%value'}}, |
|
144 fontsize_class: {inline: 'span', attributes: {'class': '%value'}}, |
|
145 blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'}, |
|
146 subscript: {inline: 'sub'}, |
|
147 superscript: {inline: 'sup'}, |
|
148 code: {inline: 'code'}, |
|
149 |
|
150 link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, |
|
151 onmatch: function() { |
|
152 return true; |
|
153 }, |
|
154 |
|
155 onformat: function(elm, fmt, vars) { |
|
156 each(vars, function(value, key) { |
|
157 dom.setAttrib(elm, key, value); |
|
158 }); |
|
159 } |
|
160 }, |
|
161 |
|
162 removeformat: [ |
|
163 { |
|
164 selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins', |
|
165 remove: 'all', |
|
166 split: true, |
|
167 expand: false, |
|
168 block_expand: true, |
|
169 deep: true |
|
170 }, |
|
171 {selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true}, |
|
172 {selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true} |
|
173 ] |
|
174 }); |
|
175 |
|
176 // Register default block formats |
|
177 each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) { |
|
178 register(name, {block: name, remove: 'all'}); |
|
179 }); |
|
180 |
|
181 // Register user defined formats |
|
182 register(ed.settings.formats); |
|
183 } |
|
184 |
|
185 function addKeyboardShortcuts() { |
|
186 // Add some inline shortcuts |
|
187 ed.addShortcut('meta+b', 'bold_desc', 'Bold'); |
|
188 ed.addShortcut('meta+i', 'italic_desc', 'Italic'); |
|
189 ed.addShortcut('meta+u', 'underline_desc', 'Underline'); |
|
190 |
|
191 // BlockFormat shortcuts keys |
|
192 for (var i = 1; i <= 6; i++) { |
|
193 ed.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]); |
|
194 } |
|
195 |
|
196 ed.addShortcut('access+7', '', ['FormatBlock', false, 'p']); |
|
197 ed.addShortcut('access+8', '', ['FormatBlock', false, 'div']); |
|
198 ed.addShortcut('access+9', '', ['FormatBlock', false, 'address']); |
|
199 } |
|
200 |
|
201 // Public functions |
|
202 |
|
203 /** |
|
204 * Returns the format by name or all formats if no name is specified. |
|
205 * |
|
206 * @method get |
|
207 * @param {String} name Optional name to retrive by. |
|
208 * @return {Array/Object} Array/Object with all registred formats or a specific format. |
|
209 */ |
|
210 function get(name) { |
|
211 return name ? formats[name] : formats; |
|
212 } |
|
213 |
|
214 /** |
|
215 * Registers a specific format by name. |
|
216 * |
|
217 * @method register |
|
218 * @param {Object/String} name Name of the format for example "bold". |
|
219 * @param {Object/Array} format Optional format object or array of format variants |
|
220 * can only be omitted if the first arg is an object. |
|
221 */ |
|
222 function register(name, format) { |
|
223 if (name) { |
|
224 if (typeof name !== 'string') { |
|
225 each(name, function(format, name) { |
|
226 register(name, format); |
|
227 }); |
|
228 } else { |
|
229 // Force format into array and add it to internal collection |
|
230 format = format.length ? format : [format]; |
|
231 |
|
232 each(format, function(format) { |
|
233 // Set deep to false by default on selector formats this to avoid removing |
|
234 // alignment on images inside paragraphs when alignment is changed on paragraphs |
|
235 if (format.deep === undef) { |
|
236 format.deep = !format.selector; |
|
237 } |
|
238 |
|
239 // Default to true |
|
240 if (format.split === undef) { |
|
241 format.split = !format.selector || format.inline; |
|
242 } |
|
243 |
|
244 // Default to true |
|
245 if (format.remove === undef && format.selector && !format.inline) { |
|
246 format.remove = 'none'; |
|
247 } |
|
248 |
|
249 // Mark format as a mixed format inline + block level |
|
250 if (format.selector && format.inline) { |
|
251 format.mixed = true; |
|
252 format.block_expand = true; |
|
253 } |
|
254 |
|
255 // Split classes if needed |
|
256 if (typeof format.classes === 'string') { |
|
257 format.classes = format.classes.split(/\s+/); |
|
258 } |
|
259 }); |
|
260 |
|
261 formats[name] = format; |
|
262 } |
|
263 } |
|
264 } |
|
265 |
|
266 /** |
|
267 * Unregister a specific format by name. |
|
268 * |
|
269 * @method unregister |
|
270 * @param {String} name Name of the format for example "bold". |
|
271 */ |
|
272 function unregister(name) { |
|
273 if (name && formats[name]) { |
|
274 delete formats[name]; |
|
275 } |
|
276 |
|
277 return formats; |
|
278 } |
|
279 |
|
280 function getTextDecoration(node) { |
|
281 var decoration; |
|
282 |
|
283 ed.dom.getParent(node, function(n) { |
|
284 decoration = ed.dom.getStyle(n, 'text-decoration'); |
|
285 return decoration && decoration !== 'none'; |
|
286 }); |
|
287 |
|
288 return decoration; |
|
289 } |
|
290 |
|
291 function processUnderlineAndColor(node) { |
|
292 var textDecoration; |
|
293 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { |
|
294 textDecoration = getTextDecoration(node.parentNode); |
|
295 if (ed.dom.getStyle(node, 'color') && textDecoration) { |
|
296 ed.dom.setStyle(node, 'text-decoration', textDecoration); |
|
297 } else if (ed.dom.getStyle(node, 'text-decoration') === textDecoration) { |
|
298 ed.dom.setStyle(node, 'text-decoration', null); |
|
299 } |
|
300 } |
|
301 } |
|
302 |
|
303 /** |
|
304 * Applies the specified format to the current selection or specified node. |
|
305 * |
|
306 * @method apply |
|
307 * @param {String} name Name of format to apply. |
|
308 * @param {Object} vars Optional list of variables to replace within format before applying it. |
|
309 * @param {Node} node Optional node to apply the format to defaults to current selection. |
|
310 */ |
|
311 function apply(name, vars, node) { |
|
312 var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed(); |
|
313 |
|
314 function setElementFormat(elm, fmt) { |
|
315 fmt = fmt || format; |
|
316 |
|
317 if (elm) { |
|
318 if (fmt.onformat) { |
|
319 fmt.onformat(elm, fmt, vars, node); |
|
320 } |
|
321 |
|
322 each(fmt.styles, function(value, name) { |
|
323 dom.setStyle(elm, name, replaceVars(value, vars)); |
|
324 }); |
|
325 |
|
326 // Needed for the WebKit span spam bug |
|
327 // TODO: Remove this once WebKit/Blink fixes this |
|
328 if (fmt.styles) { |
|
329 var styleVal = dom.getAttrib(elm, 'style'); |
|
330 |
|
331 if (styleVal) { |
|
332 elm.setAttribute('data-mce-style', styleVal); |
|
333 } |
|
334 } |
|
335 |
|
336 each(fmt.attributes, function(value, name) { |
|
337 dom.setAttrib(elm, name, replaceVars(value, vars)); |
|
338 }); |
|
339 |
|
340 each(fmt.classes, function(value) { |
|
341 value = replaceVars(value, vars); |
|
342 |
|
343 if (!dom.hasClass(elm, value)) { |
|
344 dom.addClass(elm, value); |
|
345 } |
|
346 }); |
|
347 } |
|
348 } |
|
349 |
|
350 function adjustSelectionToVisibleSelection() { |
|
351 function findSelectionEnd(start, end) { |
|
352 var walker = new TreeWalker(end); |
|
353 for (node = walker.current(); node; node = walker.prev()) { |
|
354 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { |
|
355 return node; |
|
356 } |
|
357 } |
|
358 } |
|
359 |
|
360 // Adjust selection so that a end container with a end offset of zero is not included in the selection |
|
361 // as this isn't visible to the user. |
|
362 var rng = ed.selection.getRng(); |
|
363 var start = rng.startContainer; |
|
364 var end = rng.endContainer; |
|
365 |
|
366 if (start != end && rng.endOffset === 0) { |
|
367 var newEnd = findSelectionEnd(start, end); |
|
368 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; |
|
369 |
|
370 rng.setEnd(newEnd, endOffset); |
|
371 } |
|
372 |
|
373 return rng; |
|
374 } |
|
375 |
|
376 function applyRngStyle(rng, bookmark, node_specific) { |
|
377 var newWrappers = [], wrapName, wrapElm, contentEditable = true; |
|
378 |
|
379 // Setup wrapper element |
|
380 wrapName = format.inline || format.block; |
|
381 wrapElm = dom.create(wrapName); |
|
382 setElementFormat(wrapElm); |
|
383 |
|
384 rangeUtils.walk(rng, function(nodes) { |
|
385 var currentWrapElm; |
|
386 |
|
387 /** |
|
388 * Process a list of nodes wrap them. |
|
389 */ |
|
390 function process(node) { |
|
391 var nodeName, parentName, found, hasContentEditableState, lastContentEditable; |
|
392 |
|
393 lastContentEditable = contentEditable; |
|
394 nodeName = node.nodeName.toLowerCase(); |
|
395 parentName = node.parentNode.nodeName.toLowerCase(); |
|
396 |
|
397 // Node has a contentEditable value |
|
398 if (node.nodeType === 1 && getContentEditable(node)) { |
|
399 lastContentEditable = contentEditable; |
|
400 contentEditable = getContentEditable(node) === "true"; |
|
401 hasContentEditableState = true; // We don't want to wrap the container only it's children |
|
402 } |
|
403 |
|
404 // Stop wrapping on br elements |
|
405 if (isEq(nodeName, 'br')) { |
|
406 currentWrapElm = 0; |
|
407 |
|
408 // Remove any br elements when we wrap things |
|
409 if (format.block) { |
|
410 dom.remove(node); |
|
411 } |
|
412 |
|
413 return; |
|
414 } |
|
415 |
|
416 // If node is wrapper type |
|
417 if (format.wrapper && matchNode(node, name, vars)) { |
|
418 currentWrapElm = 0; |
|
419 return; |
|
420 } |
|
421 |
|
422 // Can we rename the block |
|
423 // TODO: Break this if up, too complex |
|
424 if (contentEditable && !hasContentEditableState && format.block && |
|
425 !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { |
|
426 node = dom.rename(node, wrapName); |
|
427 setElementFormat(node); |
|
428 newWrappers.push(node); |
|
429 currentWrapElm = 0; |
|
430 return; |
|
431 } |
|
432 |
|
433 // Handle selector patterns |
|
434 if (format.selector) { |
|
435 // Look for matching formats |
|
436 each(formatList, function(format) { |
|
437 // Check collapsed state if it exists |
|
438 if ('collapsed' in format && format.collapsed !== isCollapsed) { |
|
439 return; |
|
440 } |
|
441 |
|
442 if (dom.is(node, format.selector) && !isCaretNode(node)) { |
|
443 setElementFormat(node, format); |
|
444 found = true; |
|
445 } |
|
446 }); |
|
447 |
|
448 // Continue processing if a selector match wasn't found and a inline element is defined |
|
449 if (!format.inline || found) { |
|
450 currentWrapElm = 0; |
|
451 return; |
|
452 } |
|
453 } |
|
454 |
|
455 // Is it valid to wrap this item |
|
456 // TODO: Break this if up, too complex |
|
457 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && |
|
458 !(!node_specific && node.nodeType === 3 && |
|
459 node.nodeValue.length === 1 && |
|
460 node.nodeValue.charCodeAt(0) === 65279) && |
|
461 !isCaretNode(node) && |
|
462 (!format.inline || !isBlock(node))) { |
|
463 // Start wrapping |
|
464 if (!currentWrapElm) { |
|
465 // Wrap the node |
|
466 currentWrapElm = dom.clone(wrapElm, FALSE); |
|
467 node.parentNode.insertBefore(currentWrapElm, node); |
|
468 newWrappers.push(currentWrapElm); |
|
469 } |
|
470 |
|
471 currentWrapElm.appendChild(node); |
|
472 } else { |
|
473 // Start a new wrapper for possible children |
|
474 currentWrapElm = 0; |
|
475 |
|
476 each(grep(node.childNodes), process); |
|
477 |
|
478 if (hasContentEditableState) { |
|
479 contentEditable = lastContentEditable; // Restore last contentEditable state from stack |
|
480 } |
|
481 |
|
482 // End the last wrapper |
|
483 currentWrapElm = 0; |
|
484 } |
|
485 } |
|
486 |
|
487 // Process siblings from range |
|
488 each(nodes, process); |
|
489 }); |
|
490 |
|
491 // Apply formats to links as well to get the color of the underline to change as well |
|
492 if (format.links === true) { |
|
493 each(newWrappers, function(node) { |
|
494 function process(node) { |
|
495 if (node.nodeName === 'A') { |
|
496 setElementFormat(node, format); |
|
497 } |
|
498 |
|
499 each(grep(node.childNodes), process); |
|
500 } |
|
501 |
|
502 process(node); |
|
503 }); |
|
504 } |
|
505 |
|
506 // Cleanup |
|
507 each(newWrappers, function(node) { |
|
508 var childCount; |
|
509 |
|
510 function getChildCount(node) { |
|
511 var count = 0; |
|
512 |
|
513 each(node.childNodes, function(node) { |
|
514 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { |
|
515 count++; |
|
516 } |
|
517 }); |
|
518 |
|
519 return count; |
|
520 } |
|
521 |
|
522 function mergeStyles(node) { |
|
523 var child, clone; |
|
524 |
|
525 each(node.childNodes, function(node) { |
|
526 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { |
|
527 child = node; |
|
528 return FALSE; // break loop |
|
529 } |
|
530 }); |
|
531 |
|
532 // If child was found and of the same type as the current node |
|
533 if (child && !isBookmarkNode(child) && matchName(child, format)) { |
|
534 clone = dom.clone(child, FALSE); |
|
535 setElementFormat(clone); |
|
536 |
|
537 dom.replace(clone, node, TRUE); |
|
538 dom.remove(child, 1); |
|
539 } |
|
540 |
|
541 return clone || node; |
|
542 } |
|
543 |
|
544 childCount = getChildCount(node); |
|
545 |
|
546 // Remove empty nodes but only if there is multiple wrappers and they are not block |
|
547 // elements so never remove single <h1></h1> since that would remove the |
|
548 // currrent empty block element where the caret is at |
|
549 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { |
|
550 dom.remove(node, 1); |
|
551 return; |
|
552 } |
|
553 |
|
554 if (format.inline || format.wrapper) { |
|
555 // Merges the current node with it's children of similar type to reduce the number of elements |
|
556 if (!format.exact && childCount === 1) { |
|
557 node = mergeStyles(node); |
|
558 } |
|
559 |
|
560 // Remove/merge children |
|
561 each(formatList, function(format) { |
|
562 // Merge all children of similar type will move styles from child to parent |
|
563 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> |
|
564 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> |
|
565 each(dom.select(format.inline, node), function(child) { |
|
566 if (isBookmarkNode(child)) { |
|
567 return; |
|
568 } |
|
569 |
|
570 removeFormat(format, vars, child, format.exact ? child : null); |
|
571 }); |
|
572 }); |
|
573 |
|
574 // Remove child if direct parent is of same type |
|
575 if (matchNode(node.parentNode, name, vars)) { |
|
576 dom.remove(node, 1); |
|
577 node = 0; |
|
578 return TRUE; |
|
579 } |
|
580 |
|
581 // Look for parent with similar style format |
|
582 if (format.merge_with_parents) { |
|
583 dom.getParent(node.parentNode, function(parent) { |
|
584 if (matchNode(parent, name, vars)) { |
|
585 dom.remove(node, 1); |
|
586 node = 0; |
|
587 return TRUE; |
|
588 } |
|
589 }); |
|
590 } |
|
591 |
|
592 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> |
|
593 if (node && format.merge_siblings !== false) { |
|
594 node = mergeSiblings(getNonWhiteSpaceSibling(node), node); |
|
595 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); |
|
596 } |
|
597 } |
|
598 }); |
|
599 } |
|
600 |
|
601 if (format) { |
|
602 if (node) { |
|
603 if (node.nodeType) { |
|
604 rng = dom.createRng(); |
|
605 rng.setStartBefore(node); |
|
606 rng.setEndAfter(node); |
|
607 applyRngStyle(expandRng(rng, formatList), null, true); |
|
608 } else { |
|
609 applyRngStyle(node, null, true); |
|
610 } |
|
611 } else { |
|
612 if (!isCollapsed || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { |
|
613 // Obtain selection node before selection is unselected by applyRngStyle() |
|
614 var curSelNode = ed.selection.getNode(); |
|
615 |
|
616 // If the formats have a default block and we can't find a parent block then |
|
617 // start wrapping it with a DIV this is for forced_root_blocks: false |
|
618 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way |
|
619 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { |
|
620 apply(formatList[0].defaultBlock); |
|
621 } |
|
622 |
|
623 // Apply formatting to selection |
|
624 ed.selection.setRng(adjustSelectionToVisibleSelection()); |
|
625 bookmark = selection.getBookmark(); |
|
626 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); |
|
627 |
|
628 // Colored nodes should be underlined so that the color of the underline matches the text color. |
|
629 if (format.styles && (format.styles.color || format.styles.textDecoration)) { |
|
630 walk(curSelNode, processUnderlineAndColor, 'childNodes'); |
|
631 processUnderlineAndColor(curSelNode); |
|
632 } |
|
633 |
|
634 selection.moveToBookmark(bookmark); |
|
635 moveStart(selection.getRng(TRUE)); |
|
636 ed.nodeChanged(); |
|
637 } else { |
|
638 performCaretAction('apply', name, vars); |
|
639 } |
|
640 } |
|
641 } |
|
642 } |
|
643 |
|
644 /** |
|
645 * Removes the specified format from the current selection or specified node. |
|
646 * |
|
647 * @method remove |
|
648 * @param {String} name Name of format to remove. |
|
649 * @param {Object} vars Optional list of variables to replace within format before removing it. |
|
650 * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. |
|
651 */ |
|
652 function remove(name, vars, node, similar) { |
|
653 var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; |
|
654 |
|
655 // Merges the styles for each node |
|
656 function process(node) { |
|
657 var children, i, l, lastContentEditable, hasContentEditableState; |
|
658 |
|
659 // Node has a contentEditable value |
|
660 if (node.nodeType === 1 && getContentEditable(node)) { |
|
661 lastContentEditable = contentEditable; |
|
662 contentEditable = getContentEditable(node) === "true"; |
|
663 hasContentEditableState = true; // We don't want to wrap the container only it's children |
|
664 } |
|
665 |
|
666 // Grab the children first since the nodelist might be changed |
|
667 children = grep(node.childNodes); |
|
668 |
|
669 // Process current node |
|
670 if (contentEditable && !hasContentEditableState) { |
|
671 for (i = 0, l = formatList.length; i < l; i++) { |
|
672 if (removeFormat(formatList[i], vars, node, node)) { |
|
673 break; |
|
674 } |
|
675 } |
|
676 } |
|
677 |
|
678 // Process the children |
|
679 if (format.deep) { |
|
680 if (children.length) { |
|
681 for (i = 0, l = children.length; i < l; i++) { |
|
682 process(children[i]); |
|
683 } |
|
684 |
|
685 if (hasContentEditableState) { |
|
686 contentEditable = lastContentEditable; // Restore last contentEditable state from stack |
|
687 } |
|
688 } |
|
689 } |
|
690 } |
|
691 |
|
692 function findFormatRoot(container) { |
|
693 var formatRoot; |
|
694 |
|
695 // Find format root |
|
696 each(getParents(container.parentNode).reverse(), function(parent) { |
|
697 var format; |
|
698 |
|
699 // Find format root element |
|
700 if (!formatRoot && parent.id != '_start' && parent.id != '_end') { |
|
701 // Is the node matching the format we are looking for |
|
702 format = matchNode(parent, name, vars, similar); |
|
703 if (format && format.split !== false) { |
|
704 formatRoot = parent; |
|
705 } |
|
706 } |
|
707 }); |
|
708 |
|
709 return formatRoot; |
|
710 } |
|
711 |
|
712 function wrapAndSplit(formatRoot, container, target, split) { |
|
713 var parent, clone, lastClone, firstClone, i, formatRootParent; |
|
714 |
|
715 // Format root found then clone formats and split it |
|
716 if (formatRoot) { |
|
717 formatRootParent = formatRoot.parentNode; |
|
718 |
|
719 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { |
|
720 clone = dom.clone(parent, FALSE); |
|
721 |
|
722 for (i = 0; i < formatList.length; i++) { |
|
723 if (removeFormat(formatList[i], vars, clone, clone)) { |
|
724 clone = 0; |
|
725 break; |
|
726 } |
|
727 } |
|
728 |
|
729 // Build wrapper node |
|
730 if (clone) { |
|
731 if (lastClone) { |
|
732 clone.appendChild(lastClone); |
|
733 } |
|
734 |
|
735 if (!firstClone) { |
|
736 firstClone = clone; |
|
737 } |
|
738 |
|
739 lastClone = clone; |
|
740 } |
|
741 } |
|
742 |
|
743 // Never split block elements if the format is mixed |
|
744 if (split && (!format.mixed || !isBlock(formatRoot))) { |
|
745 container = dom.split(formatRoot, container); |
|
746 } |
|
747 |
|
748 // Wrap container in cloned formats |
|
749 if (lastClone) { |
|
750 target.parentNode.insertBefore(lastClone, target); |
|
751 firstClone.appendChild(target); |
|
752 } |
|
753 } |
|
754 |
|
755 return container; |
|
756 } |
|
757 |
|
758 function splitToFormatRoot(container) { |
|
759 return wrapAndSplit(findFormatRoot(container), container, container, true); |
|
760 } |
|
761 |
|
762 function unwrap(start) { |
|
763 var node = dom.get(start ? '_start' : '_end'), |
|
764 out = node[start ? 'firstChild' : 'lastChild']; |
|
765 |
|
766 // If the end is placed within the start the result will be removed |
|
767 // So this checks if the out node is a bookmark node if it is it |
|
768 // checks for another more suitable node |
|
769 if (isBookmarkNode(out)) { |
|
770 out = out[start ? 'firstChild' : 'lastChild']; |
|
771 } |
|
772 |
|
773 // Since dom.remove removes empty text nodes then we need to try to find a better node |
|
774 if (out.nodeType == 3 && out.data.length === 0) { |
|
775 out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling; |
|
776 } |
|
777 |
|
778 dom.remove(node, true); |
|
779 |
|
780 return out; |
|
781 } |
|
782 |
|
783 function removeRngStyle(rng) { |
|
784 var startContainer, endContainer; |
|
785 var commonAncestorContainer = rng.commonAncestorContainer; |
|
786 |
|
787 rng = expandRng(rng, formatList, TRUE); |
|
788 |
|
789 if (format.split) { |
|
790 startContainer = getContainer(rng, TRUE); |
|
791 endContainer = getContainer(rng); |
|
792 |
|
793 if (startContainer != endContainer) { |
|
794 // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN |
|
795 // so let's see if we can use the first child instead |
|
796 // This will happen if you triple click a table cell and use remove formatting |
|
797 if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { |
|
798 if (startContainer.nodeName == "TR") { |
|
799 startContainer = startContainer.firstChild.firstChild || startContainer; |
|
800 } else { |
|
801 startContainer = startContainer.firstChild || startContainer; |
|
802 } |
|
803 } |
|
804 |
|
805 // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 |
|
806 if (commonAncestorContainer && |
|
807 /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && |
|
808 isTableCell(endContainer) && endContainer.firstChild) { |
|
809 endContainer = endContainer.firstChild || endContainer; |
|
810 } |
|
811 |
|
812 if (dom.isChildOf(startContainer, endContainer) && !isTableCell(startContainer) && !isTableCell(endContainer)) { |
|
813 startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); |
|
814 splitToFormatRoot(startContainer); |
|
815 startContainer = unwrap(TRUE); |
|
816 return; |
|
817 } else { |
|
818 // Wrap start/end nodes in span element since these might be cloned/moved |
|
819 startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); |
|
820 endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'}); |
|
821 |
|
822 // Split start/end |
|
823 splitToFormatRoot(startContainer); |
|
824 splitToFormatRoot(endContainer); |
|
825 |
|
826 // Unwrap start/end to get real elements again |
|
827 startContainer = unwrap(TRUE); |
|
828 endContainer = unwrap(); |
|
829 } |
|
830 } else { |
|
831 startContainer = endContainer = splitToFormatRoot(startContainer); |
|
832 } |
|
833 |
|
834 // Update range positions since they might have changed after the split operations |
|
835 rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer; |
|
836 rng.startOffset = nodeIndex(startContainer); |
|
837 rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; |
|
838 rng.endOffset = nodeIndex(endContainer) + 1; |
|
839 } |
|
840 |
|
841 // Remove items between start/end |
|
842 rangeUtils.walk(rng, function(nodes) { |
|
843 each(nodes, function(node) { |
|
844 process(node); |
|
845 |
|
846 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. |
|
847 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && |
|
848 node.parentNode && getTextDecoration(node.parentNode) === 'underline') { |
|
849 removeFormat({ |
|
850 'deep': false, |
|
851 'exact': true, |
|
852 'inline': 'span', |
|
853 'styles': { |
|
854 'textDecoration': 'underline' |
|
855 } |
|
856 }, null, node); |
|
857 } |
|
858 }); |
|
859 }); |
|
860 } |
|
861 |
|
862 // Handle node |
|
863 if (node) { |
|
864 if (node.nodeType) { |
|
865 rng = dom.createRng(); |
|
866 rng.setStartBefore(node); |
|
867 rng.setEndAfter(node); |
|
868 removeRngStyle(rng); |
|
869 } else { |
|
870 removeRngStyle(node); |
|
871 } |
|
872 |
|
873 return; |
|
874 } |
|
875 |
|
876 if (!selection.isCollapsed() || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { |
|
877 bookmark = selection.getBookmark(); |
|
878 removeRngStyle(selection.getRng(TRUE)); |
|
879 selection.moveToBookmark(bookmark); |
|
880 |
|
881 // Check if start element still has formatting then we are at: "<b>text|</b>text" |
|
882 // and need to move the start into the next text node |
|
883 if (format.inline && match(name, vars, selection.getStart())) { |
|
884 moveStart(selection.getRng(true)); |
|
885 } |
|
886 |
|
887 ed.nodeChanged(); |
|
888 } else { |
|
889 performCaretAction('remove', name, vars, similar); |
|
890 } |
|
891 } |
|
892 |
|
893 /** |
|
894 * Toggles the specified format on/off. |
|
895 * |
|
896 * @method toggle |
|
897 * @param {String} name Name of format to apply/remove. |
|
898 * @param {Object} vars Optional list of variables to replace within format before applying/removing it. |
|
899 * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. |
|
900 */ |
|
901 function toggle(name, vars, node) { |
|
902 var fmt = get(name); |
|
903 |
|
904 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { |
|
905 remove(name, vars, node); |
|
906 } else { |
|
907 apply(name, vars, node); |
|
908 } |
|
909 } |
|
910 |
|
911 /** |
|
912 * Return true/false if the specified node has the specified format. |
|
913 * |
|
914 * @method matchNode |
|
915 * @param {Node} node Node to check the format on. |
|
916 * @param {String} name Format name to check. |
|
917 * @param {Object} vars Optional list of variables to replace before checking it. |
|
918 * @param {Boolean} similar Match format that has similar properties. |
|
919 * @return {Object} Returns the format object it matches or undefined if it doesn't match. |
|
920 */ |
|
921 function matchNode(node, name, vars, similar) { |
|
922 var formatList = get(name), format, i, classes; |
|
923 |
|
924 function matchItems(node, format, item_name) { |
|
925 var key, value, items = format[item_name], i; |
|
926 |
|
927 // Custom match |
|
928 if (format.onmatch) { |
|
929 return format.onmatch(node, format, item_name); |
|
930 } |
|
931 |
|
932 // Check all items |
|
933 if (items) { |
|
934 // Non indexed object |
|
935 if (items.length === undef) { |
|
936 for (key in items) { |
|
937 if (items.hasOwnProperty(key)) { |
|
938 if (item_name === 'attributes') { |
|
939 value = dom.getAttrib(node, key); |
|
940 } else { |
|
941 value = getStyle(node, key); |
|
942 } |
|
943 |
|
944 if (similar && !value && !format.exact) { |
|
945 return; |
|
946 } |
|
947 |
|
948 if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { |
|
949 return; |
|
950 } |
|
951 } |
|
952 } |
|
953 } else { |
|
954 // Only one match needed for indexed arrays |
|
955 for (i = 0; i < items.length; i++) { |
|
956 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { |
|
957 return format; |
|
958 } |
|
959 } |
|
960 } |
|
961 } |
|
962 |
|
963 return format; |
|
964 } |
|
965 |
|
966 if (formatList && node) { |
|
967 // Check each format in list |
|
968 for (i = 0; i < formatList.length; i++) { |
|
969 format = formatList[i]; |
|
970 |
|
971 // Name name, attributes, styles and classes |
|
972 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { |
|
973 // Match classes |
|
974 if ((classes = format.classes)) { |
|
975 for (i = 0; i < classes.length; i++) { |
|
976 if (!dom.hasClass(node, classes[i])) { |
|
977 return; |
|
978 } |
|
979 } |
|
980 } |
|
981 |
|
982 return format; |
|
983 } |
|
984 } |
|
985 } |
|
986 } |
|
987 |
|
988 /** |
|
989 * Matches the current selection or specified node against the specified format name. |
|
990 * |
|
991 * @method match |
|
992 * @param {String} name Name of format to match. |
|
993 * @param {Object} vars Optional list of variables to replace before checking it. |
|
994 * @param {Node} node Optional node to check. |
|
995 * @return {boolean} true/false if the specified selection/node matches the format. |
|
996 */ |
|
997 function match(name, vars, node) { |
|
998 var startNode; |
|
999 |
|
1000 function matchParents(node) { |
|
1001 var root = dom.getRoot(); |
|
1002 |
|
1003 if (node === root) { |
|
1004 return false; |
|
1005 } |
|
1006 |
|
1007 // Find first node with similar format settings |
|
1008 node = dom.getParent(node, function(node) { |
|
1009 return node.parentNode === root || !!matchNode(node, name, vars, true); |
|
1010 }); |
|
1011 |
|
1012 // Do an exact check on the similar format element |
|
1013 return matchNode(node, name, vars); |
|
1014 } |
|
1015 |
|
1016 // Check specified node |
|
1017 if (node) { |
|
1018 return matchParents(node); |
|
1019 } |
|
1020 |
|
1021 // Check selected node |
|
1022 node = selection.getNode(); |
|
1023 if (matchParents(node)) { |
|
1024 return TRUE; |
|
1025 } |
|
1026 |
|
1027 // Check start node if it's different |
|
1028 startNode = selection.getStart(); |
|
1029 if (startNode != node) { |
|
1030 if (matchParents(startNode)) { |
|
1031 return TRUE; |
|
1032 } |
|
1033 } |
|
1034 |
|
1035 return FALSE; |
|
1036 } |
|
1037 |
|
1038 /** |
|
1039 * Matches the current selection against the array of formats and returns a new array with matching formats. |
|
1040 * |
|
1041 * @method matchAll |
|
1042 * @param {Array} names Name of format to match. |
|
1043 * @param {Object} vars Optional list of variables to replace before checking it. |
|
1044 * @return {Array} Array with matched formats. |
|
1045 */ |
|
1046 function matchAll(names, vars) { |
|
1047 var startElement, matchedFormatNames = [], checkedMap = {}; |
|
1048 |
|
1049 // Check start of selection for formats |
|
1050 startElement = selection.getStart(); |
|
1051 dom.getParent(startElement, function(node) { |
|
1052 var i, name; |
|
1053 |
|
1054 for (i = 0; i < names.length; i++) { |
|
1055 name = names[i]; |
|
1056 |
|
1057 if (!checkedMap[name] && matchNode(node, name, vars)) { |
|
1058 checkedMap[name] = true; |
|
1059 matchedFormatNames.push(name); |
|
1060 } |
|
1061 } |
|
1062 }, dom.getRoot()); |
|
1063 |
|
1064 return matchedFormatNames; |
|
1065 } |
|
1066 |
|
1067 /** |
|
1068 * Returns true/false if the specified format can be applied to the current selection or not. It |
|
1069 * will currently only check the state for selector formats, it returns true on all other format types. |
|
1070 * |
|
1071 * @method canApply |
|
1072 * @param {String} name Name of format to check. |
|
1073 * @return {boolean} true/false if the specified format can be applied to the current selection/node. |
|
1074 */ |
|
1075 function canApply(name) { |
|
1076 var formatList = get(name), startNode, parents, i, x, selector; |
|
1077 |
|
1078 if (formatList) { |
|
1079 startNode = selection.getStart(); |
|
1080 parents = getParents(startNode); |
|
1081 |
|
1082 for (x = formatList.length - 1; x >= 0; x--) { |
|
1083 selector = formatList[x].selector; |
|
1084 |
|
1085 // Format is not selector based then always return TRUE |
|
1086 // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line |
|
1087 if (!selector || formatList[x].defaultBlock) { |
|
1088 return TRUE; |
|
1089 } |
|
1090 |
|
1091 for (i = parents.length - 1; i >= 0; i--) { |
|
1092 if (dom.is(parents[i], selector)) { |
|
1093 return TRUE; |
|
1094 } |
|
1095 } |
|
1096 } |
|
1097 } |
|
1098 |
|
1099 return FALSE; |
|
1100 } |
|
1101 |
|
1102 /** |
|
1103 * Executes the specified callback when the current selection matches the formats or not. |
|
1104 * |
|
1105 * @method formatChanged |
|
1106 * @param {String} formats Comma separated list of formats to check for. |
|
1107 * @param {function} callback Callback with state and args when the format is changed/toggled on/off. |
|
1108 * @param {Boolean} similar True/false state if the match should handle similar or exact formats. |
|
1109 */ |
|
1110 function formatChanged(formats, callback, similar) { |
|
1111 var currentFormats; |
|
1112 |
|
1113 // Setup format node change logic |
|
1114 if (!formatChangeData) { |
|
1115 formatChangeData = {}; |
|
1116 currentFormats = {}; |
|
1117 |
|
1118 ed.on('NodeChange', function(e) { |
|
1119 var parents = getParents(e.element), matchedFormats = {}; |
|
1120 |
|
1121 // Ignore bogus nodes like the <a> tag created by moveStart() |
|
1122 parents = Tools.grep(parents, function(node) { |
|
1123 return node.nodeType == 1 && !node.getAttribute('data-mce-bogus'); |
|
1124 }); |
|
1125 |
|
1126 // Check for new formats |
|
1127 each(formatChangeData, function(callbacks, format) { |
|
1128 each(parents, function(node) { |
|
1129 if (matchNode(node, format, {}, callbacks.similar)) { |
|
1130 if (!currentFormats[format]) { |
|
1131 // Execute callbacks |
|
1132 each(callbacks, function(callback) { |
|
1133 callback(true, {node: node, format: format, parents: parents}); |
|
1134 }); |
|
1135 |
|
1136 currentFormats[format] = callbacks; |
|
1137 } |
|
1138 |
|
1139 matchedFormats[format] = callbacks; |
|
1140 return false; |
|
1141 } |
|
1142 }); |
|
1143 }); |
|
1144 |
|
1145 // Check if current formats still match |
|
1146 each(currentFormats, function(callbacks, format) { |
|
1147 if (!matchedFormats[format]) { |
|
1148 delete currentFormats[format]; |
|
1149 |
|
1150 each(callbacks, function(callback) { |
|
1151 callback(false, {node: e.element, format: format, parents: parents}); |
|
1152 }); |
|
1153 } |
|
1154 }); |
|
1155 }); |
|
1156 } |
|
1157 |
|
1158 // Add format listeners |
|
1159 each(formats.split(','), function(format) { |
|
1160 if (!formatChangeData[format]) { |
|
1161 formatChangeData[format] = []; |
|
1162 formatChangeData[format].similar = similar; |
|
1163 } |
|
1164 |
|
1165 formatChangeData[format].push(callback); |
|
1166 }); |
|
1167 |
|
1168 return this; |
|
1169 } |
|
1170 |
|
1171 /** |
|
1172 * Returns a preview css text for the specified format. |
|
1173 * |
|
1174 * @method getCssText |
|
1175 * @param {String/Object} format Format to generate preview css text for. |
|
1176 * @return {String} Css text for the specified format. |
|
1177 * @example |
|
1178 * var cssText1 = editor.formatter.getCssText('bold'); |
|
1179 * var cssText2 = editor.formatter.getCssText({inline: 'b'}); |
|
1180 */ |
|
1181 function getCssText(format) { |
|
1182 return Preview.getCssText(ed, format); |
|
1183 } |
|
1184 |
|
1185 // Expose to public |
|
1186 extend(this, { |
|
1187 get: get, |
|
1188 register: register, |
|
1189 unregister: unregister, |
|
1190 apply: apply, |
|
1191 remove: remove, |
|
1192 toggle: toggle, |
|
1193 match: match, |
|
1194 matchAll: matchAll, |
|
1195 matchNode: matchNode, |
|
1196 canApply: canApply, |
|
1197 formatChanged: formatChanged, |
|
1198 getCssText: getCssText |
|
1199 }); |
|
1200 |
|
1201 // Initialize |
|
1202 defaultFormats(); |
|
1203 addKeyboardShortcuts(); |
|
1204 ed.on('BeforeGetContent', function(e) { |
|
1205 if (markCaretContainersBogus && e.format != 'raw') { |
|
1206 markCaretContainersBogus(); |
|
1207 } |
|
1208 }); |
|
1209 ed.on('mouseup keydown', function(e) { |
|
1210 if (disableCaretContainer) { |
|
1211 disableCaretContainer(e); |
|
1212 } |
|
1213 }); |
|
1214 |
|
1215 // Private functions |
|
1216 |
|
1217 /** |
|
1218 * Checks if the specified nodes name matches the format inline/block or selector. |
|
1219 * |
|
1220 * @private |
|
1221 * @param {Node} node Node to match against the specified format. |
|
1222 * @param {Object} format Format object o match with. |
|
1223 * @return {boolean} true/false if the format matches. |
|
1224 */ |
|
1225 function matchName(node, format) { |
|
1226 // Check for inline match |
|
1227 if (isEq(node, format.inline)) { |
|
1228 return TRUE; |
|
1229 } |
|
1230 |
|
1231 // Check for block match |
|
1232 if (isEq(node, format.block)) { |
|
1233 return TRUE; |
|
1234 } |
|
1235 |
|
1236 // Check for selector match |
|
1237 if (format.selector) { |
|
1238 return node.nodeType == 1 && dom.is(node, format.selector); |
|
1239 } |
|
1240 } |
|
1241 |
|
1242 /** |
|
1243 * Compares two string/nodes regardless of their case. |
|
1244 * |
|
1245 * @private |
|
1246 * @param {String/Node} Node or string to compare. |
|
1247 * @param {String/Node} Node or string to compare. |
|
1248 * @return {boolean} True/false if they match. |
|
1249 */ |
|
1250 function isEq(str1, str2) { |
|
1251 str1 = str1 || ''; |
|
1252 str2 = str2 || ''; |
|
1253 |
|
1254 str1 = '' + (str1.nodeName || str1); |
|
1255 str2 = '' + (str2.nodeName || str2); |
|
1256 |
|
1257 return str1.toLowerCase() == str2.toLowerCase(); |
|
1258 } |
|
1259 |
|
1260 /** |
|
1261 * Returns the style by name on the specified node. This method modifies the style |
|
1262 * contents to make it more easy to match. This will resolve a few browser issues. |
|
1263 * |
|
1264 * @private |
|
1265 * @param {Node} node to get style from. |
|
1266 * @param {String} name Style name to get. |
|
1267 * @return {String} Style item value. |
|
1268 */ |
|
1269 function getStyle(node, name) { |
|
1270 return normalizeStyleValue(dom.getStyle(node, name), name); |
|
1271 } |
|
1272 |
|
1273 /** |
|
1274 * Normalize style value by name. This method modifies the style contents |
|
1275 * to make it more easy to match. This will resolve a few browser issues. |
|
1276 * |
|
1277 * @private |
|
1278 * @param {Node} node to get style from. |
|
1279 * @param {String} name Style name to get. |
|
1280 * @return {String} Style item value. |
|
1281 */ |
|
1282 function normalizeStyleValue(value, name) { |
|
1283 // Force the format to hex |
|
1284 if (name == 'color' || name == 'backgroundColor') { |
|
1285 value = dom.toHex(value); |
|
1286 } |
|
1287 |
|
1288 // Opera will return bold as 700 |
|
1289 if (name == 'fontWeight' && value == 700) { |
|
1290 value = 'bold'; |
|
1291 } |
|
1292 |
|
1293 // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" |
|
1294 if (name == 'fontFamily') { |
|
1295 value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); |
|
1296 } |
|
1297 |
|
1298 return '' + value; |
|
1299 } |
|
1300 |
|
1301 /** |
|
1302 * Replaces variables in the value. The variable format is %var. |
|
1303 * |
|
1304 * @private |
|
1305 * @param {String} value Value to replace variables in. |
|
1306 * @param {Object} vars Name/value array with variables to replace. |
|
1307 * @return {String} New value with replaced variables. |
|
1308 */ |
|
1309 function replaceVars(value, vars) { |
|
1310 if (typeof value != "string") { |
|
1311 value = value(vars); |
|
1312 } else if (vars) { |
|
1313 value = value.replace(/%(\w+)/g, function(str, name) { |
|
1314 return vars[name] || str; |
|
1315 }); |
|
1316 } |
|
1317 |
|
1318 return value; |
|
1319 } |
|
1320 |
|
1321 function isWhiteSpaceNode(node) { |
|
1322 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); |
|
1323 } |
|
1324 |
|
1325 function wrap(node, name, attrs) { |
|
1326 var wrapper = dom.create(name, attrs); |
|
1327 |
|
1328 node.parentNode.insertBefore(wrapper, node); |
|
1329 wrapper.appendChild(node); |
|
1330 |
|
1331 return wrapper; |
|
1332 } |
|
1333 |
|
1334 /** |
|
1335 * Expands the specified range like object to depending on format. |
|
1336 * |
|
1337 * For example on block formats it will move the start/end position |
|
1338 * to the beginning of the current block. |
|
1339 * |
|
1340 * @private |
|
1341 * @param {Object} rng Range like object. |
|
1342 * @param {Array} formats Array with formats to expand by. |
|
1343 * @return {Object} Expanded range like object. |
|
1344 */ |
|
1345 function expandRng(rng, format, remove) { |
|
1346 var lastIdx, leaf, endPoint, |
|
1347 startContainer = rng.startContainer, |
|
1348 startOffset = rng.startOffset, |
|
1349 endContainer = rng.endContainer, |
|
1350 endOffset = rng.endOffset; |
|
1351 |
|
1352 // This function walks up the tree if there is no siblings before/after the node |
|
1353 function findParentContainer(start) { |
|
1354 var container, parent, sibling, siblingName, root; |
|
1355 |
|
1356 container = parent = start ? startContainer : endContainer; |
|
1357 siblingName = start ? 'previousSibling' : 'nextSibling'; |
|
1358 root = dom.getRoot(); |
|
1359 |
|
1360 function isBogusBr(node) { |
|
1361 return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; |
|
1362 } |
|
1363 |
|
1364 // If it's a text node and the offset is inside the text |
|
1365 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { |
|
1366 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { |
|
1367 return container; |
|
1368 } |
|
1369 } |
|
1370 |
|
1371 /*eslint no-constant-condition:0 */ |
|
1372 while (true) { |
|
1373 // Stop expanding on block elements |
|
1374 if (!format[0].block_expand && isBlock(parent)) { |
|
1375 return parent; |
|
1376 } |
|
1377 |
|
1378 // Walk left/right |
|
1379 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { |
|
1380 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { |
|
1381 return parent; |
|
1382 } |
|
1383 } |
|
1384 |
|
1385 // Check if we can move up are we at root level or body level |
|
1386 if (parent.parentNode == root) { |
|
1387 container = parent; |
|
1388 break; |
|
1389 } |
|
1390 |
|
1391 parent = parent.parentNode; |
|
1392 } |
|
1393 |
|
1394 return container; |
|
1395 } |
|
1396 |
|
1397 // This function walks down the tree to find the leaf at the selection. |
|
1398 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. |
|
1399 function findLeaf(node, offset) { |
|
1400 if (offset === undef) { |
|
1401 offset = node.nodeType === 3 ? node.length : node.childNodes.length; |
|
1402 } |
|
1403 |
|
1404 while (node && node.hasChildNodes()) { |
|
1405 node = node.childNodes[offset]; |
|
1406 if (node) { |
|
1407 offset = node.nodeType === 3 ? node.length : node.childNodes.length; |
|
1408 } |
|
1409 } |
|
1410 return {node: node, offset: offset}; |
|
1411 } |
|
1412 |
|
1413 // If index based start position then resolve it |
|
1414 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { |
|
1415 lastIdx = startContainer.childNodes.length - 1; |
|
1416 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; |
|
1417 |
|
1418 if (startContainer.nodeType == 3) { |
|
1419 startOffset = 0; |
|
1420 } |
|
1421 } |
|
1422 |
|
1423 // If index based end position then resolve it |
|
1424 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { |
|
1425 lastIdx = endContainer.childNodes.length - 1; |
|
1426 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; |
|
1427 |
|
1428 if (endContainer.nodeType == 3) { |
|
1429 endOffset = endContainer.nodeValue.length; |
|
1430 } |
|
1431 } |
|
1432 |
|
1433 // Expands the node to the closes contentEditable false element if it exists |
|
1434 function findParentContentEditable(node) { |
|
1435 var parent = node; |
|
1436 |
|
1437 while (parent) { |
|
1438 if (parent.nodeType === 1 && getContentEditable(parent)) { |
|
1439 return getContentEditable(parent) === "false" ? parent : node; |
|
1440 } |
|
1441 |
|
1442 parent = parent.parentNode; |
|
1443 } |
|
1444 |
|
1445 return node; |
|
1446 } |
|
1447 |
|
1448 function findWordEndPoint(container, offset, start) { |
|
1449 var walker, node, pos, lastTextNode; |
|
1450 |
|
1451 function findSpace(node, offset) { |
|
1452 var pos, pos2, str = node.nodeValue; |
|
1453 |
|
1454 if (typeof offset == "undefined") { |
|
1455 offset = start ? str.length : 0; |
|
1456 } |
|
1457 |
|
1458 if (start) { |
|
1459 pos = str.lastIndexOf(' ', offset); |
|
1460 pos2 = str.lastIndexOf('\u00a0', offset); |
|
1461 pos = pos > pos2 ? pos : pos2; |
|
1462 |
|
1463 // Include the space on remove to avoid tag soup |
|
1464 if (pos !== -1 && !remove) { |
|
1465 pos++; |
|
1466 } |
|
1467 } else { |
|
1468 pos = str.indexOf(' ', offset); |
|
1469 pos2 = str.indexOf('\u00a0', offset); |
|
1470 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; |
|
1471 } |
|
1472 |
|
1473 return pos; |
|
1474 } |
|
1475 |
|
1476 if (container.nodeType === 3) { |
|
1477 pos = findSpace(container, offset); |
|
1478 |
|
1479 if (pos !== -1) { |
|
1480 return {container: container, offset: pos}; |
|
1481 } |
|
1482 |
|
1483 lastTextNode = container; |
|
1484 } |
|
1485 |
|
1486 // Walk the nodes inside the block |
|
1487 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); |
|
1488 while ((node = walker[start ? 'prev' : 'next']())) { |
|
1489 if (node.nodeType === 3) { |
|
1490 lastTextNode = node; |
|
1491 pos = findSpace(node); |
|
1492 |
|
1493 if (pos !== -1) { |
|
1494 return {container: node, offset: pos}; |
|
1495 } |
|
1496 } else if (isBlock(node)) { |
|
1497 break; |
|
1498 } |
|
1499 } |
|
1500 |
|
1501 if (lastTextNode) { |
|
1502 if (start) { |
|
1503 offset = 0; |
|
1504 } else { |
|
1505 offset = lastTextNode.length; |
|
1506 } |
|
1507 |
|
1508 return {container: lastTextNode, offset: offset}; |
|
1509 } |
|
1510 } |
|
1511 |
|
1512 function findSelectorEndPoint(container, sibling_name) { |
|
1513 var parents, i, y, curFormat; |
|
1514 |
|
1515 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) { |
|
1516 container = container[sibling_name]; |
|
1517 } |
|
1518 |
|
1519 parents = getParents(container); |
|
1520 for (i = 0; i < parents.length; i++) { |
|
1521 for (y = 0; y < format.length; y++) { |
|
1522 curFormat = format[y]; |
|
1523 |
|
1524 // If collapsed state is set then skip formats that doesn't match that |
|
1525 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { |
|
1526 continue; |
|
1527 } |
|
1528 |
|
1529 if (dom.is(parents[i], curFormat.selector)) { |
|
1530 return parents[i]; |
|
1531 } |
|
1532 } |
|
1533 } |
|
1534 |
|
1535 return container; |
|
1536 } |
|
1537 |
|
1538 function findBlockEndPoint(container, sibling_name) { |
|
1539 var node, root = dom.getRoot(); |
|
1540 |
|
1541 // Expand to block of similar type |
|
1542 if (!format[0].wrapper) { |
|
1543 node = dom.getParent(container, format[0].block, root); |
|
1544 } |
|
1545 |
|
1546 // Expand to first wrappable block element or any block element |
|
1547 if (!node) { |
|
1548 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) { |
|
1549 // Fixes #6183 where it would expand to editable parent element in inline mode |
|
1550 return node != root && isTextBlock(node); |
|
1551 }); |
|
1552 } |
|
1553 |
|
1554 // Exclude inner lists from wrapping |
|
1555 if (node && format[0].wrapper) { |
|
1556 node = getParents(node, 'ul,ol').reverse()[0] || node; |
|
1557 } |
|
1558 |
|
1559 // Didn't find a block element look for first/last wrappable element |
|
1560 if (!node) { |
|
1561 node = container; |
|
1562 |
|
1563 while (node[sibling_name] && !isBlock(node[sibling_name])) { |
|
1564 node = node[sibling_name]; |
|
1565 |
|
1566 // Break on BR but include it will be removed later on |
|
1567 // we can't remove it now since we need to check if it can be wrapped |
|
1568 if (isEq(node, 'br')) { |
|
1569 break; |
|
1570 } |
|
1571 } |
|
1572 } |
|
1573 |
|
1574 return node || container; |
|
1575 } |
|
1576 |
|
1577 // Expand to closest contentEditable element |
|
1578 startContainer = findParentContentEditable(startContainer); |
|
1579 endContainer = findParentContentEditable(endContainer); |
|
1580 |
|
1581 // Exclude bookmark nodes if possible |
|
1582 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { |
|
1583 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; |
|
1584 startContainer = startContainer.nextSibling || startContainer; |
|
1585 |
|
1586 if (startContainer.nodeType == 3) { |
|
1587 startOffset = 0; |
|
1588 } |
|
1589 } |
|
1590 |
|
1591 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { |
|
1592 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; |
|
1593 endContainer = endContainer.previousSibling || endContainer; |
|
1594 |
|
1595 if (endContainer.nodeType == 3) { |
|
1596 endOffset = endContainer.length; |
|
1597 } |
|
1598 } |
|
1599 |
|
1600 if (format[0].inline) { |
|
1601 if (rng.collapsed) { |
|
1602 // Expand left to closest word boundary |
|
1603 endPoint = findWordEndPoint(startContainer, startOffset, true); |
|
1604 if (endPoint) { |
|
1605 startContainer = endPoint.container; |
|
1606 startOffset = endPoint.offset; |
|
1607 } |
|
1608 |
|
1609 // Expand right to closest word boundary |
|
1610 endPoint = findWordEndPoint(endContainer, endOffset); |
|
1611 if (endPoint) { |
|
1612 endContainer = endPoint.container; |
|
1613 endOffset = endPoint.offset; |
|
1614 } |
|
1615 } |
|
1616 |
|
1617 // Avoid applying formatting to a trailing space. |
|
1618 leaf = findLeaf(endContainer, endOffset); |
|
1619 if (leaf.node) { |
|
1620 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { |
|
1621 leaf = findLeaf(leaf.node.previousSibling); |
|
1622 } |
|
1623 |
|
1624 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && |
|
1625 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { |
|
1626 |
|
1627 if (leaf.offset > 1) { |
|
1628 endContainer = leaf.node; |
|
1629 endContainer.splitText(leaf.offset - 1); |
|
1630 } |
|
1631 } |
|
1632 } |
|
1633 } |
|
1634 |
|
1635 // Move start/end point up the tree if the leaves are sharp and if we are in different containers |
|
1636 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! |
|
1637 // This will reduce the number of wrapper elements that needs to be created |
|
1638 // Move start point up the tree |
|
1639 if (format[0].inline || format[0].block_expand) { |
|
1640 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { |
|
1641 startContainer = findParentContainer(true); |
|
1642 } |
|
1643 |
|
1644 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { |
|
1645 endContainer = findParentContainer(); |
|
1646 } |
|
1647 } |
|
1648 |
|
1649 // Expand start/end container to matching selector |
|
1650 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { |
|
1651 // Find new startContainer/endContainer if there is better one |
|
1652 startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); |
|
1653 endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); |
|
1654 } |
|
1655 |
|
1656 // Expand start/end container to matching block element or text node |
|
1657 if (format[0].block || format[0].selector) { |
|
1658 // Find new startContainer/endContainer if there is better one |
|
1659 startContainer = findBlockEndPoint(startContainer, 'previousSibling'); |
|
1660 endContainer = findBlockEndPoint(endContainer, 'nextSibling'); |
|
1661 |
|
1662 // Non block element then try to expand up the leaf |
|
1663 if (format[0].block) { |
|
1664 if (!isBlock(startContainer)) { |
|
1665 startContainer = findParentContainer(true); |
|
1666 } |
|
1667 |
|
1668 if (!isBlock(endContainer)) { |
|
1669 endContainer = findParentContainer(); |
|
1670 } |
|
1671 } |
|
1672 } |
|
1673 |
|
1674 // Setup index for startContainer |
|
1675 if (startContainer.nodeType == 1) { |
|
1676 startOffset = nodeIndex(startContainer); |
|
1677 startContainer = startContainer.parentNode; |
|
1678 } |
|
1679 |
|
1680 // Setup index for endContainer |
|
1681 if (endContainer.nodeType == 1) { |
|
1682 endOffset = nodeIndex(endContainer) + 1; |
|
1683 endContainer = endContainer.parentNode; |
|
1684 } |
|
1685 |
|
1686 // Return new range like object |
|
1687 return { |
|
1688 startContainer: startContainer, |
|
1689 startOffset: startOffset, |
|
1690 endContainer: endContainer, |
|
1691 endOffset: endOffset |
|
1692 }; |
|
1693 } |
|
1694 |
|
1695 function isColorFormatAndAnchor(node, format) { |
|
1696 return format.links && node.tagName == 'A'; |
|
1697 } |
|
1698 |
|
1699 /** |
|
1700 * Removes the specified format for the specified node. It will also remove the node if it doesn't have |
|
1701 * any attributes if the format specifies it to do so. |
|
1702 * |
|
1703 * @private |
|
1704 * @param {Object} format Format object with items to remove from node. |
|
1705 * @param {Object} vars Name/value object with variables to apply to format. |
|
1706 * @param {Node} node Node to remove the format styles on. |
|
1707 * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. |
|
1708 * @return {Boolean} True/false if the node was removed or not. |
|
1709 */ |
|
1710 function removeFormat(format, vars, node, compare_node) { |
|
1711 var i, attrs, stylesModified; |
|
1712 |
|
1713 // Check if node matches format |
|
1714 if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) { |
|
1715 return FALSE; |
|
1716 } |
|
1717 |
|
1718 // Should we compare with format attribs and styles |
|
1719 if (format.remove != 'all') { |
|
1720 // Remove styles |
|
1721 each(format.styles, function(value, name) { |
|
1722 value = normalizeStyleValue(replaceVars(value, vars), name); |
|
1723 |
|
1724 // Indexed array |
|
1725 if (typeof name === 'number') { |
|
1726 name = value; |
|
1727 compare_node = 0; |
|
1728 } |
|
1729 |
|
1730 if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) { |
|
1731 dom.setStyle(node, name, ''); |
|
1732 } |
|
1733 |
|
1734 stylesModified = 1; |
|
1735 }); |
|
1736 |
|
1737 // Remove style attribute if it's empty |
|
1738 if (stylesModified && dom.getAttrib(node, 'style') === '') { |
|
1739 node.removeAttribute('style'); |
|
1740 node.removeAttribute('data-mce-style'); |
|
1741 } |
|
1742 |
|
1743 // Remove attributes |
|
1744 each(format.attributes, function(value, name) { |
|
1745 var valueOut; |
|
1746 |
|
1747 value = replaceVars(value, vars); |
|
1748 |
|
1749 // Indexed array |
|
1750 if (typeof name === 'number') { |
|
1751 name = value; |
|
1752 compare_node = 0; |
|
1753 } |
|
1754 |
|
1755 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { |
|
1756 // Keep internal classes |
|
1757 if (name == 'class') { |
|
1758 value = dom.getAttrib(node, name); |
|
1759 if (value) { |
|
1760 // Build new class value where everything is removed except the internal prefixed classes |
|
1761 valueOut = ''; |
|
1762 each(value.split(/\s+/), function(cls) { |
|
1763 if (/mce\-\w+/.test(cls)) { |
|
1764 valueOut += (valueOut ? ' ' : '') + cls; |
|
1765 } |
|
1766 }); |
|
1767 |
|
1768 // We got some internal classes left |
|
1769 if (valueOut) { |
|
1770 dom.setAttrib(node, name, valueOut); |
|
1771 return; |
|
1772 } |
|
1773 } |
|
1774 } |
|
1775 |
|
1776 // IE6 has a bug where the attribute doesn't get removed correctly |
|
1777 if (name == "class") { |
|
1778 node.removeAttribute('className'); |
|
1779 } |
|
1780 |
|
1781 // Remove mce prefixed attributes |
|
1782 if (MCE_ATTR_RE.test(name)) { |
|
1783 node.removeAttribute('data-mce-' + name); |
|
1784 } |
|
1785 |
|
1786 node.removeAttribute(name); |
|
1787 } |
|
1788 }); |
|
1789 |
|
1790 // Remove classes |
|
1791 each(format.classes, function(value) { |
|
1792 value = replaceVars(value, vars); |
|
1793 |
|
1794 if (!compare_node || dom.hasClass(compare_node, value)) { |
|
1795 dom.removeClass(node, value); |
|
1796 } |
|
1797 }); |
|
1798 |
|
1799 // Check for non internal attributes |
|
1800 attrs = dom.getAttribs(node); |
|
1801 for (i = 0; i < attrs.length; i++) { |
|
1802 if (attrs[i].nodeName.indexOf('_') !== 0) { |
|
1803 return FALSE; |
|
1804 } |
|
1805 } |
|
1806 } |
|
1807 |
|
1808 // Remove the inline child if it's empty for example <b> or <span> |
|
1809 if (format.remove != 'none') { |
|
1810 removeNode(node, format); |
|
1811 return TRUE; |
|
1812 } |
|
1813 } |
|
1814 |
|
1815 /** |
|
1816 * Removes the node and wrap it's children in paragraphs before doing so or |
|
1817 * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. |
|
1818 * |
|
1819 * If the div in the node below gets removed: |
|
1820 * text<div>text</div>text |
|
1821 * |
|
1822 * Output becomes: |
|
1823 * text<div><br />text<br /></div>text |
|
1824 * |
|
1825 * So when the div is removed the result is: |
|
1826 * text<br />text<br />text |
|
1827 * |
|
1828 * @private |
|
1829 * @param {Node} node Node to remove + apply BR/P elements to. |
|
1830 * @param {Object} format Format rule. |
|
1831 * @return {Node} Input node. |
|
1832 */ |
|
1833 function removeNode(node, format) { |
|
1834 var parentNode = node.parentNode, rootBlockElm; |
|
1835 |
|
1836 function find(node, next, inc) { |
|
1837 node = getNonWhiteSpaceSibling(node, next, inc); |
|
1838 |
|
1839 return !node || (node.nodeName == 'BR' || isBlock(node)); |
|
1840 } |
|
1841 |
|
1842 if (format.block) { |
|
1843 if (!forcedRootBlock) { |
|
1844 // Append BR elements if needed before we remove the block |
|
1845 if (isBlock(node) && !isBlock(parentNode)) { |
|
1846 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) { |
|
1847 node.insertBefore(dom.create('br'), node.firstChild); |
|
1848 } |
|
1849 |
|
1850 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) { |
|
1851 node.appendChild(dom.create('br')); |
|
1852 } |
|
1853 } |
|
1854 } else { |
|
1855 // Wrap the block in a forcedRootBlock if we are at the root of document |
|
1856 if (parentNode == dom.getRoot()) { |
|
1857 if (!format.list_block || !isEq(node, format.list_block)) { |
|
1858 each(grep(node.childNodes), function(node) { |
|
1859 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { |
|
1860 if (!rootBlockElm) { |
|
1861 rootBlockElm = wrap(node, forcedRootBlock); |
|
1862 dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs); |
|
1863 } else { |
|
1864 rootBlockElm.appendChild(node); |
|
1865 } |
|
1866 } else { |
|
1867 rootBlockElm = 0; |
|
1868 } |
|
1869 }); |
|
1870 } |
|
1871 } |
|
1872 } |
|
1873 } |
|
1874 |
|
1875 // Never remove nodes that isn't the specified inline element if a selector is specified too |
|
1876 if (format.selector && format.inline && !isEq(format.inline, node)) { |
|
1877 return; |
|
1878 } |
|
1879 |
|
1880 dom.remove(node, 1); |
|
1881 } |
|
1882 |
|
1883 /** |
|
1884 * Returns the next/previous non whitespace node. |
|
1885 * |
|
1886 * @private |
|
1887 * @param {Node} node Node to start at. |
|
1888 * @param {boolean} next (Optional) Include next or previous node defaults to previous. |
|
1889 * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false. |
|
1890 * @return {Node} Next or previous node or undefined if it wasn't found. |
|
1891 */ |
|
1892 function getNonWhiteSpaceSibling(node, next, inc) { |
|
1893 if (node) { |
|
1894 next = next ? 'nextSibling' : 'previousSibling'; |
|
1895 |
|
1896 for (node = inc ? node : node[next]; node; node = node[next]) { |
|
1897 if (node.nodeType == 1 || !isWhiteSpaceNode(node)) { |
|
1898 return node; |
|
1899 } |
|
1900 } |
|
1901 } |
|
1902 } |
|
1903 |
|
1904 /** |
|
1905 * Merges the next/previous sibling element if they match. |
|
1906 * |
|
1907 * @private |
|
1908 * @param {Node} prev Previous node to compare/merge. |
|
1909 * @param {Node} next Next node to compare/merge. |
|
1910 * @return {Node} Next node if we didn't merge and prev node if we did. |
|
1911 */ |
|
1912 function mergeSiblings(prev, next) { |
|
1913 var sibling, tmpSibling, elementUtils = new ElementUtils(dom); |
|
1914 |
|
1915 function findElementSibling(node, sibling_name) { |
|
1916 for (sibling = node; sibling; sibling = sibling[sibling_name]) { |
|
1917 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) { |
|
1918 return node; |
|
1919 } |
|
1920 |
|
1921 if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) { |
|
1922 return sibling; |
|
1923 } |
|
1924 } |
|
1925 |
|
1926 return node; |
|
1927 } |
|
1928 |
|
1929 // Check if next/prev exists and that they are elements |
|
1930 if (prev && next) { |
|
1931 // If previous sibling is empty then jump over it |
|
1932 prev = findElementSibling(prev, 'previousSibling'); |
|
1933 next = findElementSibling(next, 'nextSibling'); |
|
1934 |
|
1935 // Compare next and previous nodes |
|
1936 if (elementUtils.compare(prev, next)) { |
|
1937 // Append nodes between |
|
1938 for (sibling = prev.nextSibling; sibling && sibling != next;) { |
|
1939 tmpSibling = sibling; |
|
1940 sibling = sibling.nextSibling; |
|
1941 prev.appendChild(tmpSibling); |
|
1942 } |
|
1943 |
|
1944 // Remove next node |
|
1945 dom.remove(next); |
|
1946 |
|
1947 // Move children into prev node |
|
1948 each(grep(next.childNodes), function(node) { |
|
1949 prev.appendChild(node); |
|
1950 }); |
|
1951 |
|
1952 return prev; |
|
1953 } |
|
1954 } |
|
1955 |
|
1956 return next; |
|
1957 } |
|
1958 |
|
1959 function getContainer(rng, start) { |
|
1960 var container, offset, lastIdx; |
|
1961 |
|
1962 container = rng[start ? 'startContainer' : 'endContainer']; |
|
1963 offset = rng[start ? 'startOffset' : 'endOffset']; |
|
1964 |
|
1965 if (container.nodeType == 1) { |
|
1966 lastIdx = container.childNodes.length - 1; |
|
1967 |
|
1968 if (!start && offset) { |
|
1969 offset--; |
|
1970 } |
|
1971 |
|
1972 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; |
|
1973 } |
|
1974 |
|
1975 // If start text node is excluded then walk to the next node |
|
1976 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { |
|
1977 container = new TreeWalker(container, ed.getBody()).next() || container; |
|
1978 } |
|
1979 |
|
1980 // If end text node is excluded then walk to the previous node |
|
1981 if (container.nodeType === 3 && !start && offset === 0) { |
|
1982 container = new TreeWalker(container, ed.getBody()).prev() || container; |
|
1983 } |
|
1984 |
|
1985 return container; |
|
1986 } |
|
1987 |
|
1988 function performCaretAction(type, name, vars, similar) { |
|
1989 var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; |
|
1990 |
|
1991 // Creates a caret container bogus element |
|
1992 function createCaretContainer(fill) { |
|
1993 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); |
|
1994 |
|
1995 if (fill) { |
|
1996 caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); |
|
1997 } |
|
1998 |
|
1999 return caretContainer; |
|
2000 } |
|
2001 |
|
2002 function isCaretContainerEmpty(node, nodes) { |
|
2003 while (node) { |
|
2004 if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { |
|
2005 return false; |
|
2006 } |
|
2007 |
|
2008 // Collect nodes |
|
2009 if (nodes && node.nodeType === 1) { |
|
2010 nodes.push(node); |
|
2011 } |
|
2012 |
|
2013 node = node.firstChild; |
|
2014 } |
|
2015 |
|
2016 return true; |
|
2017 } |
|
2018 |
|
2019 // Returns any parent caret container element |
|
2020 function getParentCaretContainer(node) { |
|
2021 while (node) { |
|
2022 if (node.id === caretContainerId) { |
|
2023 return node; |
|
2024 } |
|
2025 |
|
2026 node = node.parentNode; |
|
2027 } |
|
2028 } |
|
2029 |
|
2030 // Finds the first text node in the specified node |
|
2031 function findFirstTextNode(node) { |
|
2032 var walker; |
|
2033 |
|
2034 if (node) { |
|
2035 walker = new TreeWalker(node, node); |
|
2036 |
|
2037 for (node = walker.current(); node; node = walker.next()) { |
|
2038 if (node.nodeType === 3) { |
|
2039 return node; |
|
2040 } |
|
2041 } |
|
2042 } |
|
2043 } |
|
2044 |
|
2045 // Removes the caret container for the specified node or all on the current document |
|
2046 function removeCaretContainer(node, move_caret) { |
|
2047 var child, rng; |
|
2048 |
|
2049 if (!node) { |
|
2050 node = getParentCaretContainer(selection.getStart()); |
|
2051 |
|
2052 if (!node) { |
|
2053 while ((node = dom.get(caretContainerId))) { |
|
2054 removeCaretContainer(node, false); |
|
2055 } |
|
2056 } |
|
2057 } else { |
|
2058 rng = selection.getRng(true); |
|
2059 |
|
2060 if (isCaretContainerEmpty(node)) { |
|
2061 if (move_caret !== false) { |
|
2062 rng.setStartBefore(node); |
|
2063 rng.setEndBefore(node); |
|
2064 } |
|
2065 |
|
2066 dom.remove(node); |
|
2067 } else { |
|
2068 child = findFirstTextNode(node); |
|
2069 |
|
2070 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { |
|
2071 child.deleteData(0, 1); |
|
2072 |
|
2073 // Fix for bug #6976 |
|
2074 if (rng.startContainer == child && rng.startOffset > 0) { |
|
2075 rng.setStart(child, rng.startOffset - 1); |
|
2076 } |
|
2077 |
|
2078 if (rng.endContainer == child && rng.endOffset > 0) { |
|
2079 rng.setEnd(child, rng.endOffset - 1); |
|
2080 } |
|
2081 } |
|
2082 |
|
2083 dom.remove(node, 1); |
|
2084 } |
|
2085 |
|
2086 selection.setRng(rng); |
|
2087 } |
|
2088 } |
|
2089 |
|
2090 // Applies formatting to the caret postion |
|
2091 function applyCaretFormat() { |
|
2092 var rng, caretContainer, textNode, offset, bookmark, container, text; |
|
2093 |
|
2094 rng = selection.getRng(true); |
|
2095 offset = rng.startOffset; |
|
2096 container = rng.startContainer; |
|
2097 text = container.nodeValue; |
|
2098 |
|
2099 caretContainer = getParentCaretContainer(selection.getStart()); |
|
2100 if (caretContainer) { |
|
2101 textNode = findFirstTextNode(caretContainer); |
|
2102 } |
|
2103 |
|
2104 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character |
|
2105 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { |
|
2106 // Get bookmark of caret position |
|
2107 bookmark = selection.getBookmark(); |
|
2108 |
|
2109 // Collapse bookmark range (WebKit) |
|
2110 rng.collapse(true); |
|
2111 |
|
2112 // Expand the range to the closest word and split it at those points |
|
2113 rng = expandRng(rng, get(name)); |
|
2114 rng = rangeUtils.split(rng); |
|
2115 |
|
2116 // Apply the format to the range |
|
2117 apply(name, vars, rng); |
|
2118 |
|
2119 // Move selection back to caret position |
|
2120 selection.moveToBookmark(bookmark); |
|
2121 } else { |
|
2122 if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { |
|
2123 caretContainer = createCaretContainer(true); |
|
2124 textNode = caretContainer.firstChild; |
|
2125 |
|
2126 rng.insertNode(caretContainer); |
|
2127 offset = 1; |
|
2128 |
|
2129 apply(name, vars, caretContainer); |
|
2130 } else { |
|
2131 apply(name, vars, caretContainer); |
|
2132 } |
|
2133 |
|
2134 // Move selection to text node |
|
2135 selection.setCursorLocation(textNode, offset); |
|
2136 } |
|
2137 } |
|
2138 |
|
2139 function removeCaretFormat() { |
|
2140 var rng = selection.getRng(true), container, offset, bookmark, |
|
2141 hasContentAfter, node, formatNode, parents = [], i, caretContainer; |
|
2142 |
|
2143 container = rng.startContainer; |
|
2144 offset = rng.startOffset; |
|
2145 node = container; |
|
2146 |
|
2147 if (container.nodeType == 3) { |
|
2148 if (offset != container.nodeValue.length) { |
|
2149 hasContentAfter = true; |
|
2150 } |
|
2151 |
|
2152 node = node.parentNode; |
|
2153 } |
|
2154 |
|
2155 while (node) { |
|
2156 if (matchNode(node, name, vars, similar)) { |
|
2157 formatNode = node; |
|
2158 break; |
|
2159 } |
|
2160 |
|
2161 if (node.nextSibling) { |
|
2162 hasContentAfter = true; |
|
2163 } |
|
2164 |
|
2165 parents.push(node); |
|
2166 node = node.parentNode; |
|
2167 } |
|
2168 |
|
2169 // Node doesn't have the specified format |
|
2170 if (!formatNode) { |
|
2171 return; |
|
2172 } |
|
2173 |
|
2174 // Is there contents after the caret then remove the format on the element |
|
2175 if (hasContentAfter) { |
|
2176 // Get bookmark of caret position |
|
2177 bookmark = selection.getBookmark(); |
|
2178 |
|
2179 // Collapse bookmark range (WebKit) |
|
2180 rng.collapse(true); |
|
2181 |
|
2182 // Expand the range to the closest word and split it at those points |
|
2183 rng = expandRng(rng, get(name), true); |
|
2184 rng = rangeUtils.split(rng); |
|
2185 |
|
2186 // Remove the format from the range |
|
2187 remove(name, vars, rng); |
|
2188 |
|
2189 // Move selection back to caret position |
|
2190 selection.moveToBookmark(bookmark); |
|
2191 } else { |
|
2192 caretContainer = createCaretContainer(); |
|
2193 |
|
2194 node = caretContainer; |
|
2195 for (i = parents.length - 1; i >= 0; i--) { |
|
2196 node.appendChild(dom.clone(parents[i], false)); |
|
2197 node = node.firstChild; |
|
2198 } |
|
2199 |
|
2200 // Insert invisible character into inner most format element |
|
2201 node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); |
|
2202 node = node.firstChild; |
|
2203 |
|
2204 var block = dom.getParent(formatNode, isTextBlock); |
|
2205 |
|
2206 if (block && dom.isEmpty(block)) { |
|
2207 // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p> |
|
2208 formatNode.parentNode.replaceChild(caretContainer, formatNode); |
|
2209 } else { |
|
2210 // Insert caret container after the formated node |
|
2211 dom.insertAfter(caretContainer, formatNode); |
|
2212 } |
|
2213 |
|
2214 // Move selection to text node |
|
2215 selection.setCursorLocation(node, 1); |
|
2216 |
|
2217 // If the formatNode is empty, we can remove it safely. |
|
2218 if (dom.isEmpty(formatNode)) { |
|
2219 dom.remove(formatNode); |
|
2220 } |
|
2221 } |
|
2222 } |
|
2223 |
|
2224 // Checks if the parent caret container node isn't empty if that is the case it |
|
2225 // will remove the bogus state on all children that isn't empty |
|
2226 function unmarkBogusCaretParents() { |
|
2227 var caretContainer; |
|
2228 |
|
2229 caretContainer = getParentCaretContainer(selection.getStart()); |
|
2230 if (caretContainer && !dom.isEmpty(caretContainer)) { |
|
2231 walk(caretContainer, function(node) { |
|
2232 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { |
|
2233 dom.setAttrib(node, 'data-mce-bogus', null); |
|
2234 } |
|
2235 }, 'childNodes'); |
|
2236 } |
|
2237 } |
|
2238 |
|
2239 // Only bind the caret events once |
|
2240 if (!ed._hasCaretEvents) { |
|
2241 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements |
|
2242 markCaretContainersBogus = function() { |
|
2243 var nodes = [], i; |
|
2244 |
|
2245 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { |
|
2246 // Mark children |
|
2247 i = nodes.length; |
|
2248 while (i--) { |
|
2249 dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); |
|
2250 } |
|
2251 } |
|
2252 }; |
|
2253 |
|
2254 disableCaretContainer = function(e) { |
|
2255 var keyCode = e.keyCode; |
|
2256 |
|
2257 removeCaretContainer(); |
|
2258 |
|
2259 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys |
|
2260 // Backspace key needs to check if the range is collapsed due to bug #6780 |
|
2261 if ((keyCode == 8 && selection.isCollapsed()) || keyCode == 37 || keyCode == 39) { |
|
2262 removeCaretContainer(getParentCaretContainer(selection.getStart())); |
|
2263 } |
|
2264 |
|
2265 unmarkBogusCaretParents(); |
|
2266 }; |
|
2267 |
|
2268 // Remove bogus state if they got filled by contents using editor.selection.setContent |
|
2269 ed.on('SetContent', function(e) { |
|
2270 if (e.selection) { |
|
2271 unmarkBogusCaretParents(); |
|
2272 } |
|
2273 }); |
|
2274 ed._hasCaretEvents = true; |
|
2275 } |
|
2276 |
|
2277 // Do apply or remove caret format |
|
2278 if (type == "apply") { |
|
2279 applyCaretFormat(); |
|
2280 } else { |
|
2281 removeCaretFormat(); |
|
2282 } |
|
2283 } |
|
2284 |
|
2285 /** |
|
2286 * Moves the start to the first suitable text node. |
|
2287 */ |
|
2288 function moveStart(rng) { |
|
2289 var container = rng.startContainer, |
|
2290 offset = rng.startOffset, isAtEndOfText, |
|
2291 walker, node, nodes, tmpNode; |
|
2292 |
|
2293 // Convert text node into index if possible |
|
2294 if (container.nodeType == 3 && offset >= container.nodeValue.length) { |
|
2295 // Get the parent container location and walk from there |
|
2296 offset = nodeIndex(container); |
|
2297 container = container.parentNode; |
|
2298 isAtEndOfText = true; |
|
2299 } |
|
2300 |
|
2301 // Move startContainer/startOffset in to a suitable node |
|
2302 if (container.nodeType == 1) { |
|
2303 nodes = container.childNodes; |
|
2304 container = nodes[Math.min(offset, nodes.length - 1)]; |
|
2305 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); |
|
2306 |
|
2307 // If offset is at end of the parent node walk to the next one |
|
2308 if (offset > nodes.length - 1 || isAtEndOfText) { |
|
2309 walker.next(); |
|
2310 } |
|
2311 |
|
2312 for (node = walker.current(); node; node = walker.next()) { |
|
2313 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { |
|
2314 // IE has a "neat" feature where it moves the start node into the closest element |
|
2315 // we can avoid this by inserting an element before it and then remove it after we set the selection |
|
2316 tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR); |
|
2317 node.parentNode.insertBefore(tmpNode, node); |
|
2318 |
|
2319 // Set selection and remove tmpNode |
|
2320 rng.setStart(node, 0); |
|
2321 selection.setRng(rng); |
|
2322 dom.remove(tmpNode); |
|
2323 |
|
2324 return; |
|
2325 } |
|
2326 } |
|
2327 } |
|
2328 } |
|
2329 }; |
|
2330 }); |