|
1 "use strict"; |
|
2 |
|
3 var htmlNamespace = "http://www.w3.org/1999/xhtml"; |
|
4 |
|
5 var cssStylingFlag = false; |
|
6 |
|
7 var defaultSingleLineContainerName = "p"; |
|
8 |
|
9 // This is bad :( |
|
10 var globalRange = null; |
|
11 |
|
12 // Commands are stored in a dictionary where we call their actions and such |
|
13 var commands = {}; |
|
14 |
|
15 /////////////////////////////////////////////////////////////////////////////// |
|
16 ////////////////////////////// Utility functions ////////////////////////////// |
|
17 /////////////////////////////////////////////////////////////////////////////// |
|
18 //@{ |
|
19 |
|
20 function nextNode(node) { |
|
21 if (node.hasChildNodes()) { |
|
22 return node.firstChild; |
|
23 } |
|
24 return nextNodeDescendants(node); |
|
25 } |
|
26 |
|
27 function previousNode(node) { |
|
28 if (node.previousSibling) { |
|
29 node = node.previousSibling; |
|
30 while (node.hasChildNodes()) { |
|
31 node = node.lastChild; |
|
32 } |
|
33 return node; |
|
34 } |
|
35 if (node.parentNode |
|
36 && node.parentNode.nodeType == Node.ELEMENT_NODE) { |
|
37 return node.parentNode; |
|
38 } |
|
39 return null; |
|
40 } |
|
41 |
|
42 function nextNodeDescendants(node) { |
|
43 while (node && !node.nextSibling) { |
|
44 node = node.parentNode; |
|
45 } |
|
46 if (!node) { |
|
47 return null; |
|
48 } |
|
49 return node.nextSibling; |
|
50 } |
|
51 |
|
52 /** |
|
53 * Returns true if ancestor is an ancestor of descendant, false otherwise. |
|
54 */ |
|
55 function isAncestor(ancestor, descendant) { |
|
56 return ancestor |
|
57 && descendant |
|
58 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); |
|
59 } |
|
60 |
|
61 /** |
|
62 * Returns true if ancestor is an ancestor of or equal to descendant, false |
|
63 * otherwise. |
|
64 */ |
|
65 function isAncestorContainer(ancestor, descendant) { |
|
66 return (ancestor || descendant) |
|
67 && (ancestor == descendant || isAncestor(ancestor, descendant)); |
|
68 } |
|
69 |
|
70 /** |
|
71 * Returns true if descendant is a descendant of ancestor, false otherwise. |
|
72 */ |
|
73 function isDescendant(descendant, ancestor) { |
|
74 return ancestor |
|
75 && descendant |
|
76 && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); |
|
77 } |
|
78 |
|
79 /** |
|
80 * Returns true if node1 is before node2 in tree order, false otherwise. |
|
81 */ |
|
82 function isBefore(node1, node2) { |
|
83 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING); |
|
84 } |
|
85 |
|
86 /** |
|
87 * Returns true if node1 is after node2 in tree order, false otherwise. |
|
88 */ |
|
89 function isAfter(node1, node2) { |
|
90 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING); |
|
91 } |
|
92 |
|
93 function getAncestors(node) { |
|
94 var ancestors = []; |
|
95 while (node.parentNode) { |
|
96 ancestors.unshift(node.parentNode); |
|
97 node = node.parentNode; |
|
98 } |
|
99 return ancestors; |
|
100 } |
|
101 |
|
102 function getInclusiveAncestors(node) { |
|
103 return getAncestors(node).concat(node); |
|
104 } |
|
105 |
|
106 function getDescendants(node) { |
|
107 var descendants = []; |
|
108 var stop = nextNodeDescendants(node); |
|
109 while ((node = nextNode(node)) |
|
110 && node != stop) { |
|
111 descendants.push(node); |
|
112 } |
|
113 return descendants; |
|
114 } |
|
115 |
|
116 function getInclusiveDescendants(node) { |
|
117 return [node].concat(getDescendants(node)); |
|
118 } |
|
119 |
|
120 function convertProperty(property) { |
|
121 // Special-case for now |
|
122 var map = { |
|
123 "fontFamily": "font-family", |
|
124 "fontSize": "font-size", |
|
125 "fontStyle": "font-style", |
|
126 "fontWeight": "font-weight", |
|
127 "textDecoration": "text-decoration", |
|
128 }; |
|
129 if (typeof map[property] != "undefined") { |
|
130 return map[property]; |
|
131 } |
|
132 |
|
133 return property; |
|
134 } |
|
135 |
|
136 // Return the <font size=X> value for the given CSS size, or undefined if there |
|
137 // is none. |
|
138 function cssSizeToLegacy(cssVal) { |
|
139 return { |
|
140 "x-small": 1, |
|
141 "small": 2, |
|
142 "medium": 3, |
|
143 "large": 4, |
|
144 "x-large": 5, |
|
145 "xx-large": 6, |
|
146 "xxx-large": 7 |
|
147 }[cssVal]; |
|
148 } |
|
149 |
|
150 // Return the CSS size given a legacy size. |
|
151 function legacySizeToCss(legacyVal) { |
|
152 return { |
|
153 1: "x-small", |
|
154 2: "small", |
|
155 3: "medium", |
|
156 4: "large", |
|
157 5: "x-large", |
|
158 6: "xx-large", |
|
159 7: "xxx-large", |
|
160 }[legacyVal]; |
|
161 } |
|
162 |
|
163 // Opera 11 puts HTML elements in the null namespace, it seems. |
|
164 function isHtmlNamespace(ns) { |
|
165 return ns === null |
|
166 || ns === htmlNamespace; |
|
167 } |
|
168 |
|
169 // "the directionality" from HTML. I don't bother caring about non-HTML |
|
170 // elements. |
|
171 // |
|
172 // "The directionality of an element is either 'ltr' or 'rtl', and is |
|
173 // determined as per the first appropriate set of steps from the following |
|
174 // list:" |
|
175 function getDirectionality(element) { |
|
176 // "If the element's dir attribute is in the ltr state |
|
177 // The directionality of the element is 'ltr'." |
|
178 if (element.dir == "ltr") { |
|
179 return "ltr"; |
|
180 } |
|
181 |
|
182 // "If the element's dir attribute is in the rtl state |
|
183 // The directionality of the element is 'rtl'." |
|
184 if (element.dir == "rtl") { |
|
185 return "rtl"; |
|
186 } |
|
187 |
|
188 // "If the element's dir attribute is in the auto state |
|
189 // "If the element is a bdi element and the dir attribute is not in a |
|
190 // defined state (i.e. it is not present or has an invalid value) |
|
191 // [lots of complicated stuff] |
|
192 // |
|
193 // Skip this, since no browser implements it anyway. |
|
194 |
|
195 // "If the element is a root element and the dir attribute is not in a |
|
196 // defined state (i.e. it is not present or has an invalid value) |
|
197 // The directionality of the element is 'ltr'." |
|
198 if (!isHtmlElement(element.parentNode)) { |
|
199 return "ltr"; |
|
200 } |
|
201 |
|
202 // "If the element has a parent element and the dir attribute is not in a |
|
203 // defined state (i.e. it is not present or has an invalid value) |
|
204 // The directionality of the element is the same as the element's |
|
205 // parent element's directionality." |
|
206 return getDirectionality(element.parentNode); |
|
207 } |
|
208 |
|
209 //@} |
|
210 |
|
211 /////////////////////////////////////////////////////////////////////////////// |
|
212 ///////////////////////////// DOM Range functions ///////////////////////////// |
|
213 /////////////////////////////////////////////////////////////////////////////// |
|
214 //@{ |
|
215 |
|
216 function getNodeIndex(node) { |
|
217 var ret = 0; |
|
218 while (node.previousSibling) { |
|
219 ret++; |
|
220 node = node.previousSibling; |
|
221 } |
|
222 return ret; |
|
223 } |
|
224 |
|
225 // "The length of a Node node is the following, depending on node: |
|
226 // |
|
227 // ProcessingInstruction |
|
228 // DocumentType |
|
229 // Always 0. |
|
230 // Text |
|
231 // Comment |
|
232 // node's length. |
|
233 // Any other node |
|
234 // node's childNodes's length." |
|
235 function getNodeLength(node) { |
|
236 switch (node.nodeType) { |
|
237 case Node.PROCESSING_INSTRUCTION_NODE: |
|
238 case Node.DOCUMENT_TYPE_NODE: |
|
239 return 0; |
|
240 |
|
241 case Node.TEXT_NODE: |
|
242 case Node.COMMENT_NODE: |
|
243 return node.length; |
|
244 |
|
245 default: |
|
246 return node.childNodes.length; |
|
247 } |
|
248 } |
|
249 |
|
250 /** |
|
251 * The position of two boundary points relative to one another, as defined by |
|
252 * DOM Range. |
|
253 */ |
|
254 function getPosition(nodeA, offsetA, nodeB, offsetB) { |
|
255 // "If node A is the same as node B, return equal if offset A equals offset |
|
256 // B, before if offset A is less than offset B, and after if offset A is |
|
257 // greater than offset B." |
|
258 if (nodeA == nodeB) { |
|
259 if (offsetA == offsetB) { |
|
260 return "equal"; |
|
261 } |
|
262 if (offsetA < offsetB) { |
|
263 return "before"; |
|
264 } |
|
265 if (offsetA > offsetB) { |
|
266 return "after"; |
|
267 } |
|
268 } |
|
269 |
|
270 // "If node A is after node B in tree order, compute the position of (node |
|
271 // B, offset B) relative to (node A, offset A). If it is before, return |
|
272 // after. If it is after, return before." |
|
273 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) { |
|
274 var pos = getPosition(nodeB, offsetB, nodeA, offsetA); |
|
275 if (pos == "before") { |
|
276 return "after"; |
|
277 } |
|
278 if (pos == "after") { |
|
279 return "before"; |
|
280 } |
|
281 } |
|
282 |
|
283 // "If node A is an ancestor of node B:" |
|
284 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) { |
|
285 // "Let child equal node B." |
|
286 var child = nodeB; |
|
287 |
|
288 // "While child is not a child of node A, set child to its parent." |
|
289 while (child.parentNode != nodeA) { |
|
290 child = child.parentNode; |
|
291 } |
|
292 |
|
293 // "If the index of child is less than offset A, return after." |
|
294 if (getNodeIndex(child) < offsetA) { |
|
295 return "after"; |
|
296 } |
|
297 } |
|
298 |
|
299 // "Return before." |
|
300 return "before"; |
|
301 } |
|
302 |
|
303 /** |
|
304 * Returns the furthest ancestor of a Node as defined by DOM Range. |
|
305 */ |
|
306 function getFurthestAncestor(node) { |
|
307 var root = node; |
|
308 while (root.parentNode != null) { |
|
309 root = root.parentNode; |
|
310 } |
|
311 return root; |
|
312 } |
|
313 |
|
314 /** |
|
315 * "contained" as defined by DOM Range: "A Node node is contained in a range |
|
316 * range if node's furthest ancestor is the same as range's root, and (node, 0) |
|
317 * is after range's start, and (node, length of node) is before range's end." |
|
318 */ |
|
319 function isContained(node, range) { |
|
320 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); |
|
321 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset); |
|
322 |
|
323 return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer) |
|
324 && pos1 == "after" |
|
325 && pos2 == "before"; |
|
326 } |
|
327 |
|
328 /** |
|
329 * Return all nodes contained in range that the provided function returns true |
|
330 * for, omitting any with an ancestor already being returned. |
|
331 */ |
|
332 function getContainedNodes(range, condition) { |
|
333 if (typeof condition == "undefined") { |
|
334 condition = function() { return true }; |
|
335 } |
|
336 var node = range.startContainer; |
|
337 if (node.hasChildNodes() |
|
338 && range.startOffset < node.childNodes.length) { |
|
339 // A child is contained |
|
340 node = node.childNodes[range.startOffset]; |
|
341 } else if (range.startOffset == getNodeLength(node)) { |
|
342 // No descendant can be contained |
|
343 node = nextNodeDescendants(node); |
|
344 } else { |
|
345 // No children; this node at least can't be contained |
|
346 node = nextNode(node); |
|
347 } |
|
348 |
|
349 var stop = range.endContainer; |
|
350 if (stop.hasChildNodes() |
|
351 && range.endOffset < stop.childNodes.length) { |
|
352 // The node after the last contained node is a child |
|
353 stop = stop.childNodes[range.endOffset]; |
|
354 } else { |
|
355 // This node and/or some of its children might be contained |
|
356 stop = nextNodeDescendants(stop); |
|
357 } |
|
358 |
|
359 var nodeList = []; |
|
360 while (isBefore(node, stop)) { |
|
361 if (isContained(node, range) |
|
362 && condition(node)) { |
|
363 nodeList.push(node); |
|
364 node = nextNodeDescendants(node); |
|
365 continue; |
|
366 } |
|
367 node = nextNode(node); |
|
368 } |
|
369 return nodeList; |
|
370 } |
|
371 |
|
372 /** |
|
373 * As above, but includes nodes with an ancestor that's already been returned. |
|
374 */ |
|
375 function getAllContainedNodes(range, condition) { |
|
376 if (typeof condition == "undefined") { |
|
377 condition = function() { return true }; |
|
378 } |
|
379 var node = range.startContainer; |
|
380 if (node.hasChildNodes() |
|
381 && range.startOffset < node.childNodes.length) { |
|
382 // A child is contained |
|
383 node = node.childNodes[range.startOffset]; |
|
384 } else if (range.startOffset == getNodeLength(node)) { |
|
385 // No descendant can be contained |
|
386 node = nextNodeDescendants(node); |
|
387 } else { |
|
388 // No children; this node at least can't be contained |
|
389 node = nextNode(node); |
|
390 } |
|
391 |
|
392 var stop = range.endContainer; |
|
393 if (stop.hasChildNodes() |
|
394 && range.endOffset < stop.childNodes.length) { |
|
395 // The node after the last contained node is a child |
|
396 stop = stop.childNodes[range.endOffset]; |
|
397 } else { |
|
398 // This node and/or some of its children might be contained |
|
399 stop = nextNodeDescendants(stop); |
|
400 } |
|
401 |
|
402 var nodeList = []; |
|
403 while (isBefore(node, stop)) { |
|
404 if (isContained(node, range) |
|
405 && condition(node)) { |
|
406 nodeList.push(node); |
|
407 } |
|
408 node = nextNode(node); |
|
409 } |
|
410 return nodeList; |
|
411 } |
|
412 |
|
413 // Returns either null, or something of the form rgb(x, y, z), or something of |
|
414 // the form rgb(x, y, z, w) with w != 0. |
|
415 function normalizeColor(color) { |
|
416 if (color.toLowerCase() == "currentcolor") { |
|
417 return null; |
|
418 } |
|
419 |
|
420 if (normalizeColor.resultCache === undefined) { |
|
421 normalizeColor.resultCache = {}; |
|
422 } |
|
423 |
|
424 if (normalizeColor.resultCache[color] !== undefined) { |
|
425 return normalizeColor.resultCache[color]; |
|
426 } |
|
427 |
|
428 var originalColor = color; |
|
429 |
|
430 var outerSpan = document.createElement("span"); |
|
431 document.body.appendChild(outerSpan); |
|
432 outerSpan.style.color = "black"; |
|
433 |
|
434 var innerSpan = document.createElement("span"); |
|
435 outerSpan.appendChild(innerSpan); |
|
436 innerSpan.style.color = color; |
|
437 color = getComputedStyle(innerSpan).color; |
|
438 |
|
439 if (color == "rgb(0, 0, 0)") { |
|
440 // Maybe it's really black, maybe it's invalid. |
|
441 outerSpan.color = "white"; |
|
442 color = getComputedStyle(innerSpan).color; |
|
443 if (color != "rgb(0, 0, 0)") { |
|
444 return normalizeColor.resultCache[originalColor] = null; |
|
445 } |
|
446 } |
|
447 |
|
448 document.body.removeChild(outerSpan); |
|
449 |
|
450 // I rely on the fact that browsers generally provide consistent syntax for |
|
451 // getComputedStyle(), although it's not standardized. There are only |
|
452 // three exceptions I found: |
|
453 if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) { |
|
454 // IE10PP2 seems to do this sometimes. |
|
455 return normalizeColor.resultCache[originalColor] = |
|
456 color.replace("rgba", "rgb").replace(", 1)", ")"); |
|
457 } |
|
458 if (color == "transparent") { |
|
459 // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if |
|
460 // the specified value is "transparent". |
|
461 return normalizeColor.resultCache[originalColor] = |
|
462 "rgba(0, 0, 0, 0)"; |
|
463 } |
|
464 // Chrome 15 dev adds way too many significant figures. This isn't a full |
|
465 // fix, it just fixes one case that comes up in tests. |
|
466 color = color.replace(/, 0.496094\)$/, ", 0.5)"); |
|
467 return normalizeColor.resultCache[originalColor] = color; |
|
468 } |
|
469 |
|
470 // Returns either null, or something of the form #xxxxxx. |
|
471 function parseSimpleColor(color) { |
|
472 color = normalizeColor(color); |
|
473 var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color); |
|
474 if (matches) { |
|
475 return "#" |
|
476 + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&") |
|
477 + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&") |
|
478 + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&"); |
|
479 } |
|
480 return null; |
|
481 } |
|
482 |
|
483 //@} |
|
484 |
|
485 ////////////////////////////////////////////////////////////////////////////// |
|
486 /////////////////////////// Edit command functions /////////////////////////// |
|
487 ////////////////////////////////////////////////////////////////////////////// |
|
488 |
|
489 ///////////////////////////////////////////////// |
|
490 ///// Methods of the HTMLDocument interface ///// |
|
491 ///////////////////////////////////////////////// |
|
492 //@{ |
|
493 |
|
494 var executionStackDepth = 0; |
|
495 |
|
496 // Helper function for common behavior. |
|
497 function editCommandMethod(command, range, callback) { |
|
498 // Set up our global range magic, but only if we're the outermost function |
|
499 if (executionStackDepth == 0 && typeof range != "undefined") { |
|
500 globalRange = range; |
|
501 } else if (executionStackDepth == 0) { |
|
502 globalRange = null; |
|
503 globalRange = getActiveRange(); |
|
504 } |
|
505 |
|
506 executionStackDepth++; |
|
507 try { |
|
508 var ret = callback(); |
|
509 } catch(e) { |
|
510 executionStackDepth--; |
|
511 throw e; |
|
512 } |
|
513 executionStackDepth--; |
|
514 return ret; |
|
515 } |
|
516 |
|
517 function myExecCommand(command, showUi, value, range) { |
|
518 // "All of these methods must treat their command argument ASCII |
|
519 // case-insensitively." |
|
520 command = command.toLowerCase(); |
|
521 |
|
522 // "If only one argument was provided, let show UI be false." |
|
523 // |
|
524 // If range was passed, I can't actually detect how many args were passed |
|
525 // . . . |
|
526 if (arguments.length == 1 |
|
527 || (arguments.length >=4 && typeof showUi == "undefined")) { |
|
528 showUi = false; |
|
529 } |
|
530 |
|
531 // "If only one or two arguments were provided, let value be the empty |
|
532 // string." |
|
533 if (arguments.length <= 2 |
|
534 || (arguments.length >=4 && typeof value == "undefined")) { |
|
535 value = ""; |
|
536 } |
|
537 |
|
538 return editCommandMethod(command, range, (function(command, showUi, value) { return function() { |
|
539 // "If command is not supported or not enabled, return false." |
|
540 if (!(command in commands) || !myQueryCommandEnabled(command)) { |
|
541 return false; |
|
542 } |
|
543 |
|
544 // "Take the action for command, passing value to the instructions as an |
|
545 // argument." |
|
546 var ret = commands[command].action(value); |
|
547 |
|
548 // Check for bugs |
|
549 if (ret !== true && ret !== false) { |
|
550 throw "execCommand() didn't return true or false: " + ret; |
|
551 } |
|
552 |
|
553 // "If the previous step returned false, return false." |
|
554 if (ret === false) { |
|
555 return false; |
|
556 } |
|
557 |
|
558 // "Return true." |
|
559 return true; |
|
560 }})(command, showUi, value)); |
|
561 } |
|
562 |
|
563 function myQueryCommandEnabled(command, range) { |
|
564 // "All of these methods must treat their command argument ASCII |
|
565 // case-insensitively." |
|
566 command = command.toLowerCase(); |
|
567 |
|
568 return editCommandMethod(command, range, (function(command) { return function() { |
|
569 // "Return true if command is both supported and enabled, false |
|
570 // otherwise." |
|
571 if (!(command in commands)) { |
|
572 return false; |
|
573 } |
|
574 |
|
575 // "Among commands defined in this specification, those listed in |
|
576 // Miscellaneous commands are always enabled, except for the cut |
|
577 // command and the paste command. The other commands defined here are |
|
578 // enabled if the active range is not null, its start node is either |
|
579 // editable or an editing host, its end node is either editable or an |
|
580 // editing host, and there is some editing host that is an inclusive |
|
581 // ancestor of both its start node and its end node." |
|
582 return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss", |
|
583 "usecss"].indexOf(command) != -1 |
|
584 || ( |
|
585 getActiveRange() !== null |
|
586 && (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer)) |
|
587 && (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer)) |
|
588 && (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost)) |
|
589 ); |
|
590 }})(command)); |
|
591 } |
|
592 |
|
593 function myQueryCommandIndeterm(command, range) { |
|
594 // "All of these methods must treat their command argument ASCII |
|
595 // case-insensitively." |
|
596 command = command.toLowerCase(); |
|
597 |
|
598 return editCommandMethod(command, range, (function(command) { return function() { |
|
599 // "If command is not supported or has no indeterminacy, return false." |
|
600 if (!(command in commands) || !("indeterm" in commands[command])) { |
|
601 return false; |
|
602 } |
|
603 |
|
604 // "Return true if command is indeterminate, otherwise false." |
|
605 return commands[command].indeterm(); |
|
606 }})(command)); |
|
607 } |
|
608 |
|
609 function myQueryCommandState(command, range) { |
|
610 // "All of these methods must treat their command argument ASCII |
|
611 // case-insensitively." |
|
612 command = command.toLowerCase(); |
|
613 |
|
614 return editCommandMethod(command, range, (function(command) { return function() { |
|
615 // "If command is not supported or has no state, return false." |
|
616 if (!(command in commands) || !("state" in commands[command])) { |
|
617 return false; |
|
618 } |
|
619 |
|
620 // "If the state override for command is set, return it." |
|
621 if (typeof getStateOverride(command) != "undefined") { |
|
622 return getStateOverride(command); |
|
623 } |
|
624 |
|
625 // "Return true if command's state is true, otherwise false." |
|
626 return commands[command].state(); |
|
627 }})(command)); |
|
628 } |
|
629 |
|
630 // "When the queryCommandSupported(command) method on the HTMLDocument |
|
631 // interface is invoked, the user agent must return true if command is |
|
632 // supported, and false otherwise." |
|
633 function myQueryCommandSupported(command) { |
|
634 // "All of these methods must treat their command argument ASCII |
|
635 // case-insensitively." |
|
636 command = command.toLowerCase(); |
|
637 |
|
638 return command in commands; |
|
639 } |
|
640 |
|
641 function myQueryCommandValue(command, range) { |
|
642 // "All of these methods must treat their command argument ASCII |
|
643 // case-insensitively." |
|
644 command = command.toLowerCase(); |
|
645 |
|
646 return editCommandMethod(command, range, function() { |
|
647 // "If command is not supported or has no value, return the empty string." |
|
648 if (!(command in commands) || !("value" in commands[command])) { |
|
649 return ""; |
|
650 } |
|
651 |
|
652 // "If command is "fontSize" and its value override is set, convert the |
|
653 // value override to an integer number of pixels and return the legacy |
|
654 // font size for the result." |
|
655 if (command == "fontsize" |
|
656 && getValueOverride("fontsize") !== undefined) { |
|
657 return getLegacyFontSize(getValueOverride("fontsize")); |
|
658 } |
|
659 |
|
660 // "If the value override for command is set, return it." |
|
661 if (typeof getValueOverride(command) != "undefined") { |
|
662 return getValueOverride(command); |
|
663 } |
|
664 |
|
665 // "Return command's value." |
|
666 return commands[command].value(); |
|
667 }); |
|
668 } |
|
669 //@} |
|
670 |
|
671 ////////////////////////////// |
|
672 ///// Common definitions ///// |
|
673 ////////////////////////////// |
|
674 //@{ |
|
675 |
|
676 // "An HTML element is an Element whose namespace is the HTML namespace." |
|
677 // |
|
678 // I allow an extra argument to more easily check whether something is a |
|
679 // particular HTML element, like isHtmlElement(node, "OL"). It accepts arrays |
|
680 // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul. |
|
681 function isHtmlElement(node, tags) { |
|
682 if (typeof tags == "string") { |
|
683 tags = [tags]; |
|
684 } |
|
685 if (typeof tags == "object") { |
|
686 tags = tags.map(function(tag) { return tag.toUpperCase() }); |
|
687 } |
|
688 return node |
|
689 && node.nodeType == Node.ELEMENT_NODE |
|
690 && isHtmlNamespace(node.namespaceURI) |
|
691 && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1); |
|
692 } |
|
693 |
|
694 // "A prohibited paragraph child name is "address", "article", "aside", |
|
695 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details", |
|
696 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", |
|
697 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", |
|
698 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", |
|
699 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or |
|
700 // "xmp"." |
|
701 var prohibitedParagraphChildNames = ["address", "article", "aside", |
|
702 "blockquote", "caption", "center", "col", "colgroup", "dd", "details", |
|
703 "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", |
|
704 "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", |
|
705 "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", |
|
706 "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", |
|
707 "xmp"]; |
|
708 |
|
709 // "A prohibited paragraph child is an HTML element whose local name is a |
|
710 // prohibited paragraph child name." |
|
711 function isProhibitedParagraphChild(node) { |
|
712 return isHtmlElement(node, prohibitedParagraphChildNames); |
|
713 } |
|
714 |
|
715 // "A block node is either an Element whose "display" property does not have |
|
716 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a |
|
717 // Document, or a DocumentFragment." |
|
718 function isBlockNode(node) { |
|
719 return node |
|
720 && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1) |
|
721 || node.nodeType == Node.DOCUMENT_NODE |
|
722 || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE); |
|
723 } |
|
724 |
|
725 // "An inline node is a node that is not a block node." |
|
726 function isInlineNode(node) { |
|
727 return node && !isBlockNode(node); |
|
728 } |
|
729 |
|
730 // "An editing host is a node that is either an HTML element with a |
|
731 // contenteditable attribute set to the true state, or the HTML element child |
|
732 // of a Document whose designMode is enabled." |
|
733 function isEditingHost(node) { |
|
734 return node |
|
735 && isHtmlElement(node) |
|
736 && (node.contentEditable == "true" |
|
737 || (node.parentNode |
|
738 && node.parentNode.nodeType == Node.DOCUMENT_NODE |
|
739 && node.parentNode.designMode == "on")); |
|
740 } |
|
741 |
|
742 // "Something is editable if it is a node; it is not an editing host; it does |
|
743 // not have a contenteditable attribute set to the false state; its parent is |
|
744 // an editing host or editable; and either it is an HTML element, or it is an |
|
745 // svg or math element, or it is not an Element and its parent is an HTML |
|
746 // element." |
|
747 function isEditable(node) { |
|
748 return node |
|
749 && !isEditingHost(node) |
|
750 && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false") |
|
751 && (isEditingHost(node.parentNode) || isEditable(node.parentNode)) |
|
752 && (isHtmlElement(node) |
|
753 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg") |
|
754 || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math") |
|
755 || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode))); |
|
756 } |
|
757 |
|
758 // Helper function, not defined in the spec |
|
759 function hasEditableDescendants(node) { |
|
760 for (var i = 0; i < node.childNodes.length; i++) { |
|
761 if (isEditable(node.childNodes[i]) |
|
762 || hasEditableDescendants(node.childNodes[i])) { |
|
763 return true; |
|
764 } |
|
765 } |
|
766 return false; |
|
767 } |
|
768 |
|
769 // "The editing host of node is null if node is neither editable nor an editing |
|
770 // host; node itself, if node is an editing host; or the nearest ancestor of |
|
771 // node that is an editing host, if node is editable." |
|
772 function getEditingHostOf(node) { |
|
773 if (isEditingHost(node)) { |
|
774 return node; |
|
775 } else if (isEditable(node)) { |
|
776 var ancestor = node.parentNode; |
|
777 while (!isEditingHost(ancestor)) { |
|
778 ancestor = ancestor.parentNode; |
|
779 } |
|
780 return ancestor; |
|
781 } else { |
|
782 return null; |
|
783 } |
|
784 } |
|
785 |
|
786 // "Two nodes are in the same editing host if the editing host of the first is |
|
787 // non-null and the same as the editing host of the second." |
|
788 function inSameEditingHost(node1, node2) { |
|
789 return getEditingHostOf(node1) |
|
790 && getEditingHostOf(node1) == getEditingHostOf(node2); |
|
791 } |
|
792 |
|
793 // "A collapsed line break is a br that begins a line box which has nothing |
|
794 // else in it, and therefore has zero height." |
|
795 function isCollapsedLineBreak(br) { |
|
796 if (!isHtmlElement(br, "br")) { |
|
797 return false; |
|
798 } |
|
799 |
|
800 // Add a zwsp after it and see if that changes the height of the nearest |
|
801 // non-inline parent. Note: this is not actually reliable, because the |
|
802 // parent might have a fixed height or something. |
|
803 var ref = br.parentNode; |
|
804 while (getComputedStyle(ref).display == "inline") { |
|
805 ref = ref.parentNode; |
|
806 } |
|
807 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; |
|
808 ref.style.height = "auto"; |
|
809 ref.style.maxHeight = "none"; |
|
810 ref.style.minHeight = "0"; |
|
811 var space = document.createTextNode("\u200b"); |
|
812 var origHeight = ref.offsetHeight; |
|
813 if (origHeight == 0) { |
|
814 throw "isCollapsedLineBreak: original height is zero, bug?"; |
|
815 } |
|
816 br.parentNode.insertBefore(space, br.nextSibling); |
|
817 var finalHeight = ref.offsetHeight; |
|
818 space.parentNode.removeChild(space); |
|
819 if (refStyle === null) { |
|
820 // Without the setAttribute() line, removeAttribute() doesn't work in |
|
821 // Chrome 14 dev. I have no idea why. |
|
822 ref.setAttribute("style", ""); |
|
823 ref.removeAttribute("style"); |
|
824 } else { |
|
825 ref.setAttribute("style", refStyle); |
|
826 } |
|
827 |
|
828 // Allow some leeway in case the zwsp didn't create a whole new line, but |
|
829 // only made an existing line slightly higher. Firefox 6.0a2 shows this |
|
830 // behavior when the first line is bold. |
|
831 return origHeight < finalHeight - 5; |
|
832 } |
|
833 |
|
834 // "An extraneous line break is a br that has no visual effect, in that |
|
835 // removing it from the DOM would not change layout, except that a br that is |
|
836 // the sole child of an li is not extraneous." |
|
837 // |
|
838 // FIXME: This doesn't work in IE, since IE ignores display: none in |
|
839 // contenteditable. |
|
840 function isExtraneousLineBreak(br) { |
|
841 if (!isHtmlElement(br, "br")) { |
|
842 return false; |
|
843 } |
|
844 |
|
845 if (isHtmlElement(br.parentNode, "li") |
|
846 && br.parentNode.childNodes.length == 1) { |
|
847 return false; |
|
848 } |
|
849 |
|
850 // Make the line break disappear and see if that changes the block's |
|
851 // height. Yes, this is an absurd hack. We have to reset height etc. on |
|
852 // the reference node because otherwise its height won't change if it's not |
|
853 // auto. |
|
854 var ref = br.parentNode; |
|
855 while (getComputedStyle(ref).display == "inline") { |
|
856 ref = ref.parentNode; |
|
857 } |
|
858 var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; |
|
859 ref.style.height = "auto"; |
|
860 ref.style.maxHeight = "none"; |
|
861 ref.style.minHeight = "0"; |
|
862 var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null; |
|
863 var origHeight = ref.offsetHeight; |
|
864 if (origHeight == 0) { |
|
865 throw "isExtraneousLineBreak: original height is zero, bug?"; |
|
866 } |
|
867 br.setAttribute("style", "display:none"); |
|
868 var finalHeight = ref.offsetHeight; |
|
869 if (refStyle === null) { |
|
870 // Without the setAttribute() line, removeAttribute() doesn't work in |
|
871 // Chrome 14 dev. I have no idea why. |
|
872 ref.setAttribute("style", ""); |
|
873 ref.removeAttribute("style"); |
|
874 } else { |
|
875 ref.setAttribute("style", refStyle); |
|
876 } |
|
877 if (brStyle === null) { |
|
878 br.removeAttribute("style"); |
|
879 } else { |
|
880 br.setAttribute("style", brStyle); |
|
881 } |
|
882 |
|
883 return origHeight == finalHeight; |
|
884 } |
|
885 |
|
886 // "A whitespace node is either a Text node whose data is the empty string; or |
|
887 // a Text node whose data consists only of one or more tabs (0x0009), line |
|
888 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose |
|
889 // parent is an Element whose resolved value for "white-space" is "normal" or |
|
890 // "nowrap"; or a Text node whose data consists only of one or more tabs |
|
891 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose |
|
892 // parent is an Element whose resolved value for "white-space" is "pre-line"." |
|
893 function isWhitespaceNode(node) { |
|
894 return node |
|
895 && node.nodeType == Node.TEXT_NODE |
|
896 && (node.data == "" |
|
897 || ( |
|
898 /^[\t\n\r ]+$/.test(node.data) |
|
899 && node.parentNode |
|
900 && node.parentNode.nodeType == Node.ELEMENT_NODE |
|
901 && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1 |
|
902 ) || ( |
|
903 /^[\t\r ]+$/.test(node.data) |
|
904 && node.parentNode |
|
905 && node.parentNode.nodeType == Node.ELEMENT_NODE |
|
906 && getComputedStyle(node.parentNode).whiteSpace == "pre-line" |
|
907 )); |
|
908 } |
|
909 |
|
910 // "node is a collapsed whitespace node if the following algorithm returns |
|
911 // true:" |
|
912 function isCollapsedWhitespaceNode(node) { |
|
913 // "If node is not a whitespace node, return false." |
|
914 if (!isWhitespaceNode(node)) { |
|
915 return false; |
|
916 } |
|
917 |
|
918 // "If node's data is the empty string, return true." |
|
919 if (node.data == "") { |
|
920 return true; |
|
921 } |
|
922 |
|
923 // "Let ancestor be node's parent." |
|
924 var ancestor = node.parentNode; |
|
925 |
|
926 // "If ancestor is null, return true." |
|
927 if (!ancestor) { |
|
928 return true; |
|
929 } |
|
930 |
|
931 // "If the "display" property of some ancestor of node has resolved value |
|
932 // "none", return true." |
|
933 if (getAncestors(node).some(function(ancestor) { |
|
934 return ancestor.nodeType == Node.ELEMENT_NODE |
|
935 && getComputedStyle(ancestor).display == "none"; |
|
936 })) { |
|
937 return true; |
|
938 } |
|
939 |
|
940 // "While ancestor is not a block node and its parent is not null, set |
|
941 // ancestor to its parent." |
|
942 while (!isBlockNode(ancestor) |
|
943 && ancestor.parentNode) { |
|
944 ancestor = ancestor.parentNode; |
|
945 } |
|
946 |
|
947 // "Let reference be node." |
|
948 var reference = node; |
|
949 |
|
950 // "While reference is a descendant of ancestor:" |
|
951 while (reference != ancestor) { |
|
952 // "Let reference be the node before it in tree order." |
|
953 reference = previousNode(reference); |
|
954 |
|
955 // "If reference is a block node or a br, return true." |
|
956 if (isBlockNode(reference) |
|
957 || isHtmlElement(reference, "br")) { |
|
958 return true; |
|
959 } |
|
960 |
|
961 // "If reference is a Text node that is not a whitespace node, or is an |
|
962 // img, break from this loop." |
|
963 if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) |
|
964 || isHtmlElement(reference, "img")) { |
|
965 break; |
|
966 } |
|
967 } |
|
968 |
|
969 // "Let reference be node." |
|
970 reference = node; |
|
971 |
|
972 // "While reference is a descendant of ancestor:" |
|
973 var stop = nextNodeDescendants(ancestor); |
|
974 while (reference != stop) { |
|
975 // "Let reference be the node after it in tree order, or null if there |
|
976 // is no such node." |
|
977 reference = nextNode(reference); |
|
978 |
|
979 // "If reference is a block node or a br, return true." |
|
980 if (isBlockNode(reference) |
|
981 || isHtmlElement(reference, "br")) { |
|
982 return true; |
|
983 } |
|
984 |
|
985 // "If reference is a Text node that is not a whitespace node, or is an |
|
986 // img, break from this loop." |
|
987 if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) |
|
988 || isHtmlElement(reference, "img")) { |
|
989 break; |
|
990 } |
|
991 } |
|
992 |
|
993 // "Return false." |
|
994 return false; |
|
995 } |
|
996 |
|
997 // "Something is visible if it is a node that either is a block node, or a Text |
|
998 // node that is not a collapsed whitespace node, or an img, or a br that is not |
|
999 // an extraneous line break, or any node with a visible descendant; excluding |
|
1000 // any node with an ancestor container Element whose "display" property has |
|
1001 // resolved value "none"." |
|
1002 function isVisible(node) { |
|
1003 if (!node) { |
|
1004 return false; |
|
1005 } |
|
1006 |
|
1007 if (getAncestors(node).concat(node) |
|
1008 .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE }) |
|
1009 .some(function(node) { return getComputedStyle(node).display == "none" })) { |
|
1010 return false; |
|
1011 } |
|
1012 |
|
1013 if (isBlockNode(node) |
|
1014 || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node)) |
|
1015 || isHtmlElement(node, "img") |
|
1016 || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) { |
|
1017 return true; |
|
1018 } |
|
1019 |
|
1020 for (var i = 0; i < node.childNodes.length; i++) { |
|
1021 if (isVisible(node.childNodes[i])) { |
|
1022 return true; |
|
1023 } |
|
1024 } |
|
1025 |
|
1026 return false; |
|
1027 } |
|
1028 |
|
1029 // "Something is invisible if it is a node that is not visible." |
|
1030 function isInvisible(node) { |
|
1031 return node && !isVisible(node); |
|
1032 } |
|
1033 |
|
1034 // "A collapsed block prop is either a collapsed line break that is not an |
|
1035 // extraneous line break, or an Element that is an inline node and whose |
|
1036 // children are all either invisible or collapsed block props and that has at |
|
1037 // least one child that is a collapsed block prop." |
|
1038 function isCollapsedBlockProp(node) { |
|
1039 if (isCollapsedLineBreak(node) |
|
1040 && !isExtraneousLineBreak(node)) { |
|
1041 return true; |
|
1042 } |
|
1043 |
|
1044 if (!isInlineNode(node) |
|
1045 || node.nodeType != Node.ELEMENT_NODE) { |
|
1046 return false; |
|
1047 } |
|
1048 |
|
1049 var hasCollapsedBlockPropChild = false; |
|
1050 for (var i = 0; i < node.childNodes.length; i++) { |
|
1051 if (!isInvisible(node.childNodes[i]) |
|
1052 && !isCollapsedBlockProp(node.childNodes[i])) { |
|
1053 return false; |
|
1054 } |
|
1055 if (isCollapsedBlockProp(node.childNodes[i])) { |
|
1056 hasCollapsedBlockPropChild = true; |
|
1057 } |
|
1058 } |
|
1059 |
|
1060 return hasCollapsedBlockPropChild; |
|
1061 } |
|
1062 |
|
1063 // "The active range is the range of the selection given by calling |
|
1064 // getSelection() on the context object. (Thus the active range may be null.)" |
|
1065 // |
|
1066 // We cheat and return globalRange if that's defined. We also ensure that the |
|
1067 // active range meets the requirements that selection boundary points are |
|
1068 // supposed to meet, i.e., that the nodes are both Text or Element nodes that |
|
1069 // descend from a Document. |
|
1070 function getActiveRange() { |
|
1071 var ret; |
|
1072 if (globalRange) { |
|
1073 ret = globalRange; |
|
1074 } else if (getSelection().rangeCount) { |
|
1075 ret = getSelection().getRangeAt(0); |
|
1076 } else { |
|
1077 return null; |
|
1078 } |
|
1079 if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1 |
|
1080 || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1 |
|
1081 || !ret.startContainer.ownerDocument |
|
1082 || !ret.endContainer.ownerDocument |
|
1083 || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument) |
|
1084 || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) { |
|
1085 throw "Invalid active range; test bug?"; |
|
1086 } |
|
1087 return ret; |
|
1088 } |
|
1089 |
|
1090 // "For some commands, each HTMLDocument must have a boolean state override |
|
1091 // and/or a string value override. These do not change the command's state or |
|
1092 // value, but change the way some algorithms behave, as specified in those |
|
1093 // algorithms' definitions. Initially, both must be unset for every command. |
|
1094 // Whenever the number of ranges in the Selection changes to something |
|
1095 // different, and whenever a boundary point of the range at a given index in |
|
1096 // the Selection changes to something different, the state override and value |
|
1097 // override must be unset for every command." |
|
1098 // |
|
1099 // We implement this crudely by using setters and getters. To verify that the |
|
1100 // selection hasn't changed, we copy the active range and just check the |
|
1101 // endpoints match. This isn't really correct, but it's good enough for us. |
|
1102 // Unset state/value overrides are undefined. We put everything in a function |
|
1103 // so no one can access anything except via the provided functions, since |
|
1104 // otherwise callers might mistakenly use outdated overrides (if the selection |
|
1105 // has changed). |
|
1106 var getStateOverride, setStateOverride, unsetStateOverride, |
|
1107 getValueOverride, setValueOverride, unsetValueOverride; |
|
1108 (function() { |
|
1109 var stateOverrides = {}; |
|
1110 var valueOverrides = {}; |
|
1111 var storedRange = null; |
|
1112 |
|
1113 function resetOverrides() { |
|
1114 if (!storedRange |
|
1115 || storedRange.startContainer != getActiveRange().startContainer |
|
1116 || storedRange.endContainer != getActiveRange().endContainer |
|
1117 || storedRange.startOffset != getActiveRange().startOffset |
|
1118 || storedRange.endOffset != getActiveRange().endOffset) { |
|
1119 stateOverrides = {}; |
|
1120 valueOverrides = {}; |
|
1121 storedRange = getActiveRange().cloneRange(); |
|
1122 } |
|
1123 } |
|
1124 |
|
1125 getStateOverride = function(command) { |
|
1126 resetOverrides(); |
|
1127 return stateOverrides[command]; |
|
1128 }; |
|
1129 |
|
1130 setStateOverride = function(command, newState) { |
|
1131 resetOverrides(); |
|
1132 stateOverrides[command] = newState; |
|
1133 }; |
|
1134 |
|
1135 unsetStateOverride = function(command) { |
|
1136 resetOverrides(); |
|
1137 delete stateOverrides[command]; |
|
1138 } |
|
1139 |
|
1140 getValueOverride = function(command) { |
|
1141 resetOverrides(); |
|
1142 return valueOverrides[command]; |
|
1143 } |
|
1144 |
|
1145 // "The value override for the backColor command must be the same as the |
|
1146 // value override for the hiliteColor command, such that setting one sets |
|
1147 // the other to the same thing and unsetting one unsets the other." |
|
1148 setValueOverride = function(command, newValue) { |
|
1149 resetOverrides(); |
|
1150 valueOverrides[command] = newValue; |
|
1151 if (command == "backcolor") { |
|
1152 valueOverrides.hilitecolor = newValue; |
|
1153 } else if (command == "hilitecolor") { |
|
1154 valueOverrides.backcolor = newValue; |
|
1155 } |
|
1156 } |
|
1157 |
|
1158 unsetValueOverride = function(command) { |
|
1159 resetOverrides(); |
|
1160 delete valueOverrides[command]; |
|
1161 if (command == "backcolor") { |
|
1162 delete valueOverrides.hilitecolor; |
|
1163 } else if (command == "hilitecolor") { |
|
1164 delete valueOverrides.backcolor; |
|
1165 } |
|
1166 } |
|
1167 })(); |
|
1168 |
|
1169 //@} |
|
1170 |
|
1171 ///////////////////////////// |
|
1172 ///// Common algorithms ///// |
|
1173 ///////////////////////////// |
|
1174 |
|
1175 ///// Assorted common algorithms ///// |
|
1176 //@{ |
|
1177 |
|
1178 // Magic array of extra ranges whose endpoints we want to preserve. |
|
1179 var extraRanges = []; |
|
1180 |
|
1181 function movePreservingRanges(node, newParent, newIndex) { |
|
1182 // For convenience, I allow newIndex to be -1 to mean "insert at the end". |
|
1183 if (newIndex == -1) { |
|
1184 newIndex = newParent.childNodes.length; |
|
1185 } |
|
1186 |
|
1187 // "When the user agent is to move a Node to a new location, preserving |
|
1188 // ranges, it must remove the Node from its original parent (if any), then |
|
1189 // insert it in the new location. In doing so, however, it must ignore the |
|
1190 // regular range mutation rules, and instead follow these rules:" |
|
1191 |
|
1192 // "Let node be the moved Node, old parent and old index be the old parent |
|
1193 // (which may be null) and index, and new parent and new index be the new |
|
1194 // parent and index." |
|
1195 var oldParent = node.parentNode; |
|
1196 var oldIndex = getNodeIndex(node); |
|
1197 |
|
1198 // We preserve the global range object, the ranges in the selection, and |
|
1199 // any range that's in the extraRanges array. Any other ranges won't get |
|
1200 // updated, because we have no references to them. |
|
1201 var ranges = [globalRange].concat(extraRanges); |
|
1202 for (var i = 0; i < getSelection().rangeCount; i++) { |
|
1203 ranges.push(getSelection().getRangeAt(i)); |
|
1204 } |
|
1205 var boundaryPoints = []; |
|
1206 ranges.forEach(function(range) { |
|
1207 boundaryPoints.push([range.startContainer, range.startOffset]); |
|
1208 boundaryPoints.push([range.endContainer, range.endOffset]); |
|
1209 }); |
|
1210 |
|
1211 boundaryPoints.forEach(function(boundaryPoint) { |
|
1212 // "If a boundary point's node is the same as or a descendant of node, |
|
1213 // leave it unchanged, so it moves to the new location." |
|
1214 // |
|
1215 // No modifications necessary. |
|
1216 |
|
1217 // "If a boundary point's node is new parent and its offset is greater |
|
1218 // than new index, add one to its offset." |
|
1219 if (boundaryPoint[0] == newParent |
|
1220 && boundaryPoint[1] > newIndex) { |
|
1221 boundaryPoint[1]++; |
|
1222 } |
|
1223 |
|
1224 // "If a boundary point's node is old parent and its offset is old index or |
|
1225 // old index + 1, set its node to new parent and add new index − old index |
|
1226 // to its offset." |
|
1227 if (boundaryPoint[0] == oldParent |
|
1228 && (boundaryPoint[1] == oldIndex |
|
1229 || boundaryPoint[1] == oldIndex + 1)) { |
|
1230 boundaryPoint[0] = newParent; |
|
1231 boundaryPoint[1] += newIndex - oldIndex; |
|
1232 } |
|
1233 |
|
1234 // "If a boundary point's node is old parent and its offset is greater than |
|
1235 // old index + 1, subtract one from its offset." |
|
1236 if (boundaryPoint[0] == oldParent |
|
1237 && boundaryPoint[1] > oldIndex + 1) { |
|
1238 boundaryPoint[1]--; |
|
1239 } |
|
1240 }); |
|
1241 |
|
1242 // Now actually move it and preserve the ranges. |
|
1243 if (newParent.childNodes.length == newIndex) { |
|
1244 newParent.appendChild(node); |
|
1245 } else { |
|
1246 newParent.insertBefore(node, newParent.childNodes[newIndex]); |
|
1247 } |
|
1248 |
|
1249 globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]); |
|
1250 globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]); |
|
1251 |
|
1252 for (var i = 0; i < extraRanges.length; i++) { |
|
1253 extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]); |
|
1254 extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]); |
|
1255 } |
|
1256 |
|
1257 getSelection().removeAllRanges(); |
|
1258 for (var i = 1 + extraRanges.length; i < ranges.length; i++) { |
|
1259 var newRange = document.createRange(); |
|
1260 newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]); |
|
1261 newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]); |
|
1262 getSelection().addRange(newRange); |
|
1263 } |
|
1264 } |
|
1265 |
|
1266 function setTagName(element, newName) { |
|
1267 // "If element is an HTML element with local name equal to new name, return |
|
1268 // element." |
|
1269 if (isHtmlElement(element, newName.toUpperCase())) { |
|
1270 return element; |
|
1271 } |
|
1272 |
|
1273 // "If element's parent is null, return element." |
|
1274 if (!element.parentNode) { |
|
1275 return element; |
|
1276 } |
|
1277 |
|
1278 // "Let replacement element be the result of calling createElement(new |
|
1279 // name) on the ownerDocument of element." |
|
1280 var replacementElement = element.ownerDocument.createElement(newName); |
|
1281 |
|
1282 // "Insert replacement element into element's parent immediately before |
|
1283 // element." |
|
1284 element.parentNode.insertBefore(replacementElement, element); |
|
1285 |
|
1286 // "Copy all attributes of element to replacement element, in order." |
|
1287 for (var i = 0; i < element.attributes.length; i++) { |
|
1288 replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value); |
|
1289 } |
|
1290 |
|
1291 // "While element has children, append the first child of element as the |
|
1292 // last child of replacement element, preserving ranges." |
|
1293 while (element.childNodes.length) { |
|
1294 movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length); |
|
1295 } |
|
1296 |
|
1297 // "Remove element from its parent." |
|
1298 element.parentNode.removeChild(element); |
|
1299 |
|
1300 // "Return replacement element." |
|
1301 return replacementElement; |
|
1302 } |
|
1303 |
|
1304 function removeExtraneousLineBreaksBefore(node) { |
|
1305 // "Let ref be the previousSibling of node." |
|
1306 var ref = node.previousSibling; |
|
1307 |
|
1308 // "If ref is null, abort these steps." |
|
1309 if (!ref) { |
|
1310 return; |
|
1311 } |
|
1312 |
|
1313 // "While ref has children, set ref to its lastChild." |
|
1314 while (ref.hasChildNodes()) { |
|
1315 ref = ref.lastChild; |
|
1316 } |
|
1317 |
|
1318 // "While ref is invisible but not an extraneous line break, and ref does |
|
1319 // not equal node's parent, set ref to the node before it in tree order." |
|
1320 while (isInvisible(ref) |
|
1321 && !isExtraneousLineBreak(ref) |
|
1322 && ref != node.parentNode) { |
|
1323 ref = previousNode(ref); |
|
1324 } |
|
1325 |
|
1326 // "If ref is an editable extraneous line break, remove it from its |
|
1327 // parent." |
|
1328 if (isEditable(ref) |
|
1329 && isExtraneousLineBreak(ref)) { |
|
1330 ref.parentNode.removeChild(ref); |
|
1331 } |
|
1332 } |
|
1333 |
|
1334 function removeExtraneousLineBreaksAtTheEndOf(node) { |
|
1335 // "Let ref be node." |
|
1336 var ref = node; |
|
1337 |
|
1338 // "While ref has children, set ref to its lastChild." |
|
1339 while (ref.hasChildNodes()) { |
|
1340 ref = ref.lastChild; |
|
1341 } |
|
1342 |
|
1343 // "While ref is invisible but not an extraneous line break, and ref does |
|
1344 // not equal node, set ref to the node before it in tree order." |
|
1345 while (isInvisible(ref) |
|
1346 && !isExtraneousLineBreak(ref) |
|
1347 && ref != node) { |
|
1348 ref = previousNode(ref); |
|
1349 } |
|
1350 |
|
1351 // "If ref is an editable extraneous line break:" |
|
1352 if (isEditable(ref) |
|
1353 && isExtraneousLineBreak(ref)) { |
|
1354 // "While ref's parent is editable and invisible, set ref to its |
|
1355 // parent." |
|
1356 while (isEditable(ref.parentNode) |
|
1357 && isInvisible(ref.parentNode)) { |
|
1358 ref = ref.parentNode; |
|
1359 } |
|
1360 |
|
1361 // "Remove ref from its parent." |
|
1362 ref.parentNode.removeChild(ref); |
|
1363 } |
|
1364 } |
|
1365 |
|
1366 // "To remove extraneous line breaks from a node, first remove extraneous line |
|
1367 // breaks before it, then remove extraneous line breaks at the end of it." |
|
1368 function removeExtraneousLineBreaksFrom(node) { |
|
1369 removeExtraneousLineBreaksBefore(node); |
|
1370 removeExtraneousLineBreaksAtTheEndOf(node); |
|
1371 } |
|
1372 |
|
1373 //@} |
|
1374 ///// Wrapping a list of nodes ///// |
|
1375 //@{ |
|
1376 |
|
1377 function wrap(nodeList, siblingCriteria, newParentInstructions) { |
|
1378 // "If not provided, sibling criteria returns false and new parent |
|
1379 // instructions returns null." |
|
1380 if (typeof siblingCriteria == "undefined") { |
|
1381 siblingCriteria = function() { return false }; |
|
1382 } |
|
1383 if (typeof newParentInstructions == "undefined") { |
|
1384 newParentInstructions = function() { return null }; |
|
1385 } |
|
1386 |
|
1387 // "If every member of node list is invisible, and none is a br, return |
|
1388 // null and abort these steps." |
|
1389 if (nodeList.every(isInvisible) |
|
1390 && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) { |
|
1391 return null; |
|
1392 } |
|
1393 |
|
1394 // "If node list's first member's parent is null, return null and abort |
|
1395 // these steps." |
|
1396 if (!nodeList[0].parentNode) { |
|
1397 return null; |
|
1398 } |
|
1399 |
|
1400 // "If node list's last member is an inline node that's not a br, and node |
|
1401 // list's last member's nextSibling is a br, append that br to node list." |
|
1402 if (isInlineNode(nodeList[nodeList.length - 1]) |
|
1403 && !isHtmlElement(nodeList[nodeList.length - 1], "br") |
|
1404 && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) { |
|
1405 nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
|
1406 } |
|
1407 |
|
1408 // "While node list's first member's previousSibling is invisible, prepend |
|
1409 // it to node list." |
|
1410 while (isInvisible(nodeList[0].previousSibling)) { |
|
1411 nodeList.unshift(nodeList[0].previousSibling); |
|
1412 } |
|
1413 |
|
1414 // "While node list's last member's nextSibling is invisible, append it to |
|
1415 // node list." |
|
1416 while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) { |
|
1417 nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
|
1418 } |
|
1419 |
|
1420 // "If the previousSibling of the first member of node list is editable and |
|
1421 // running sibling criteria on it returns true, let new parent be the |
|
1422 // previousSibling of the first member of node list." |
|
1423 var newParent; |
|
1424 if (isEditable(nodeList[0].previousSibling) |
|
1425 && siblingCriteria(nodeList[0].previousSibling)) { |
|
1426 newParent = nodeList[0].previousSibling; |
|
1427 |
|
1428 // "Otherwise, if the nextSibling of the last member of node list is |
|
1429 // editable and running sibling criteria on it returns true, let new parent |
|
1430 // be the nextSibling of the last member of node list." |
|
1431 } else if (isEditable(nodeList[nodeList.length - 1].nextSibling) |
|
1432 && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) { |
|
1433 newParent = nodeList[nodeList.length - 1].nextSibling; |
|
1434 |
|
1435 // "Otherwise, run new parent instructions, and let new parent be the |
|
1436 // result." |
|
1437 } else { |
|
1438 newParent = newParentInstructions(); |
|
1439 } |
|
1440 |
|
1441 // "If new parent is null, abort these steps and return null." |
|
1442 if (!newParent) { |
|
1443 return null; |
|
1444 } |
|
1445 |
|
1446 // "If new parent's parent is null:" |
|
1447 if (!newParent.parentNode) { |
|
1448 // "Insert new parent into the parent of the first member of node list |
|
1449 // immediately before the first member of node list." |
|
1450 nodeList[0].parentNode.insertBefore(newParent, nodeList[0]); |
|
1451 |
|
1452 // "If any range has a boundary point with node equal to the parent of |
|
1453 // new parent and offset equal to the index of new parent, add one to |
|
1454 // that boundary point's offset." |
|
1455 // |
|
1456 // Only try to fix the global range. |
|
1457 if (globalRange.startContainer == newParent.parentNode |
|
1458 && globalRange.startOffset == getNodeIndex(newParent)) { |
|
1459 globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1); |
|
1460 } |
|
1461 if (globalRange.endContainer == newParent.parentNode |
|
1462 && globalRange.endOffset == getNodeIndex(newParent)) { |
|
1463 globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1); |
|
1464 } |
|
1465 } |
|
1466 |
|
1467 // "Let original parent be the parent of the first member of node list." |
|
1468 var originalParent = nodeList[0].parentNode; |
|
1469 |
|
1470 // "If new parent is before the first member of node list in tree order:" |
|
1471 if (isBefore(newParent, nodeList[0])) { |
|
1472 // "If new parent is not an inline node, but the last visible child of |
|
1473 // new parent and the first visible member of node list are both inline |
|
1474 // nodes, and the last child of new parent is not a br, call |
|
1475 // createElement("br") on the ownerDocument of new parent and append |
|
1476 // the result as the last child of new parent." |
|
1477 if (!isInlineNode(newParent) |
|
1478 && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0]) |
|
1479 && isInlineNode(nodeList.filter(isVisible)[0]) |
|
1480 && !isHtmlElement(newParent.lastChild, "BR")) { |
|
1481 newParent.appendChild(newParent.ownerDocument.createElement("br")); |
|
1482 } |
|
1483 |
|
1484 // "For each node in node list, append node as the last child of new |
|
1485 // parent, preserving ranges." |
|
1486 for (var i = 0; i < nodeList.length; i++) { |
|
1487 movePreservingRanges(nodeList[i], newParent, -1); |
|
1488 } |
|
1489 |
|
1490 // "Otherwise:" |
|
1491 } else { |
|
1492 // "If new parent is not an inline node, but the first visible child of |
|
1493 // new parent and the last visible member of node list are both inline |
|
1494 // nodes, and the last member of node list is not a br, call |
|
1495 // createElement("br") on the ownerDocument of new parent and insert |
|
1496 // the result as the first child of new parent." |
|
1497 if (!isInlineNode(newParent) |
|
1498 && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0]) |
|
1499 && isInlineNode(nodeList.filter(isVisible).slice(-1)[0]) |
|
1500 && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) { |
|
1501 newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild); |
|
1502 } |
|
1503 |
|
1504 // "For each node in node list, in reverse order, insert node as the |
|
1505 // first child of new parent, preserving ranges." |
|
1506 for (var i = nodeList.length - 1; i >= 0; i--) { |
|
1507 movePreservingRanges(nodeList[i], newParent, 0); |
|
1508 } |
|
1509 } |
|
1510 |
|
1511 // "If original parent is editable and has no children, remove it from its |
|
1512 // parent." |
|
1513 if (isEditable(originalParent) && !originalParent.hasChildNodes()) { |
|
1514 originalParent.parentNode.removeChild(originalParent); |
|
1515 } |
|
1516 |
|
1517 // "If new parent's nextSibling is editable and running sibling criteria on |
|
1518 // it returns true:" |
|
1519 if (isEditable(newParent.nextSibling) |
|
1520 && siblingCriteria(newParent.nextSibling)) { |
|
1521 // "If new parent is not an inline node, but new parent's last child |
|
1522 // and new parent's nextSibling's first child are both inline nodes, |
|
1523 // and new parent's last child is not a br, call createElement("br") on |
|
1524 // the ownerDocument of new parent and append the result as the last |
|
1525 // child of new parent." |
|
1526 if (!isInlineNode(newParent) |
|
1527 && isInlineNode(newParent.lastChild) |
|
1528 && isInlineNode(newParent.nextSibling.firstChild) |
|
1529 && !isHtmlElement(newParent.lastChild, "BR")) { |
|
1530 newParent.appendChild(newParent.ownerDocument.createElement("br")); |
|
1531 } |
|
1532 |
|
1533 // "While new parent's nextSibling has children, append its first child |
|
1534 // as the last child of new parent, preserving ranges." |
|
1535 while (newParent.nextSibling.hasChildNodes()) { |
|
1536 movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1); |
|
1537 } |
|
1538 |
|
1539 // "Remove new parent's nextSibling from its parent." |
|
1540 newParent.parentNode.removeChild(newParent.nextSibling); |
|
1541 } |
|
1542 |
|
1543 // "Remove extraneous line breaks from new parent." |
|
1544 removeExtraneousLineBreaksFrom(newParent); |
|
1545 |
|
1546 // "Return new parent." |
|
1547 return newParent; |
|
1548 } |
|
1549 |
|
1550 |
|
1551 //@} |
|
1552 ///// Allowed children ///// |
|
1553 //@{ |
|
1554 |
|
1555 // "A name of an element with inline contents is "a", "abbr", "b", "bdi", |
|
1556 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", |
|
1557 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", |
|
1558 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", |
|
1559 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"." |
|
1560 var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo", |
|
1561 "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", |
|
1562 "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", |
|
1563 "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", |
|
1564 "xmp", "big", "blink", "font", "marquee", "nobr", "tt"]; |
|
1565 |
|
1566 // "An element with inline contents is an HTML element whose local name is a |
|
1567 // name of an element with inline contents." |
|
1568 function isElementWithInlineContents(node) { |
|
1569 return isHtmlElement(node, namesOfElementsWithInlineContents); |
|
1570 } |
|
1571 |
|
1572 function isAllowedChild(child, parent_) { |
|
1573 // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or |
|
1574 // an HTML element with local name equal to one of those, and child is a |
|
1575 // Text node whose data does not consist solely of space characters, return |
|
1576 // false." |
|
1577 if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1 |
|
1578 || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"])) |
|
1579 && typeof child == "object" |
|
1580 && child.nodeType == Node.TEXT_NODE |
|
1581 && !/^[ \t\n\f\r]*$/.test(child.data)) { |
|
1582 return false; |
|
1583 } |
|
1584 |
|
1585 // "If parent is "script", "style", "plaintext", or "xmp", or an HTML |
|
1586 // element with local name equal to one of those, and child is not a Text |
|
1587 // node, return false." |
|
1588 if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1 |
|
1589 || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"])) |
|
1590 && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) { |
|
1591 return false; |
|
1592 } |
|
1593 |
|
1594 // "If child is a Document, DocumentFragment, or DocumentType, return |
|
1595 // false." |
|
1596 if (typeof child == "object" |
|
1597 && (child.nodeType == Node.DOCUMENT_NODE |
|
1598 || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE |
|
1599 || child.nodeType == Node.DOCUMENT_TYPE_NODE)) { |
|
1600 return false; |
|
1601 } |
|
1602 |
|
1603 // "If child is an HTML element, set child to the local name of child." |
|
1604 if (isHtmlElement(child)) { |
|
1605 child = child.tagName.toLowerCase(); |
|
1606 } |
|
1607 |
|
1608 // "If child is not a string, return true." |
|
1609 if (typeof child != "string") { |
|
1610 return true; |
|
1611 } |
|
1612 |
|
1613 // "If parent is an HTML element:" |
|
1614 if (isHtmlElement(parent_)) { |
|
1615 // "If child is "a", and parent or some ancestor of parent is an a, |
|
1616 // return false." |
|
1617 // |
|
1618 // "If child is a prohibited paragraph child name and parent or some |
|
1619 // ancestor of parent is an element with inline contents, return |
|
1620 // false." |
|
1621 // |
|
1622 // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or |
|
1623 // some ancestor of parent is an HTML element with local name "h1", |
|
1624 // "h2", "h3", "h4", "h5", or "h6", return false." |
|
1625 var ancestor = parent_; |
|
1626 while (ancestor) { |
|
1627 if (child == "a" && isHtmlElement(ancestor, "a")) { |
|
1628 return false; |
|
1629 } |
|
1630 if (prohibitedParagraphChildNames.indexOf(child) != -1 |
|
1631 && isElementWithInlineContents(ancestor)) { |
|
1632 return false; |
|
1633 } |
|
1634 if (/^h[1-6]$/.test(child) |
|
1635 && isHtmlElement(ancestor) |
|
1636 && /^H[1-6]$/.test(ancestor.tagName)) { |
|
1637 return false; |
|
1638 } |
|
1639 ancestor = ancestor.parentNode; |
|
1640 } |
|
1641 |
|
1642 // "Let parent be the local name of parent." |
|
1643 parent_ = parent_.tagName.toLowerCase(); |
|
1644 } |
|
1645 |
|
1646 // "If parent is an Element or DocumentFragment, return true." |
|
1647 if (typeof parent_ == "object" |
|
1648 && (parent_.nodeType == Node.ELEMENT_NODE |
|
1649 || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) { |
|
1650 return true; |
|
1651 } |
|
1652 |
|
1653 // "If parent is not a string, return false." |
|
1654 if (typeof parent_ != "string") { |
|
1655 return false; |
|
1656 } |
|
1657 |
|
1658 // "If parent is on the left-hand side of an entry on the following list, |
|
1659 // then return true if child is listed on the right-hand side of that |
|
1660 // entry, and false otherwise." |
|
1661 switch (parent_) { |
|
1662 case "colgroup": |
|
1663 return child == "col"; |
|
1664 case "table": |
|
1665 return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1; |
|
1666 case "tbody": |
|
1667 case "thead": |
|
1668 case "tfoot": |
|
1669 return ["td", "th", "tr"].indexOf(child) != -1; |
|
1670 case "tr": |
|
1671 return ["td", "th"].indexOf(child) != -1; |
|
1672 case "dl": |
|
1673 return ["dt", "dd"].indexOf(child) != -1; |
|
1674 case "dir": |
|
1675 case "ol": |
|
1676 case "ul": |
|
1677 return ["dir", "li", "ol", "ul"].indexOf(child) != -1; |
|
1678 case "hgroup": |
|
1679 return /^h[1-6]$/.test(child); |
|
1680 } |
|
1681 |
|
1682 // "If child is "body", "caption", "col", "colgroup", "frame", "frameset", |
|
1683 // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return |
|
1684 // false." |
|
1685 if (["body", "caption", "col", "colgroup", "frame", "frameset", "head", |
|
1686 "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) { |
|
1687 return false; |
|
1688 } |
|
1689 |
|
1690 // "If child is "dd" or "dt" and parent is not "dl", return false." |
|
1691 if (["dd", "dt"].indexOf(child) != -1 |
|
1692 && parent_ != "dl") { |
|
1693 return false; |
|
1694 } |
|
1695 |
|
1696 // "If child is "li" and parent is not "ol" or "ul", return false." |
|
1697 if (child == "li" |
|
1698 && parent_ != "ol" |
|
1699 && parent_ != "ul") { |
|
1700 return false; |
|
1701 } |
|
1702 |
|
1703 // "If parent is on the left-hand side of an entry on the following list |
|
1704 // and child is listed on the right-hand side of that entry, return false." |
|
1705 var table = [ |
|
1706 [["a"], ["a"]], |
|
1707 [["dd", "dt"], ["dd", "dt"]], |
|
1708 [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]], |
|
1709 [["li"], ["li"]], |
|
1710 [["nobr"], ["nobr"]], |
|
1711 [namesOfElementsWithInlineContents, prohibitedParagraphChildNames], |
|
1712 [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]], |
|
1713 ]; |
|
1714 for (var i = 0; i < table.length; i++) { |
|
1715 if (table[i][0].indexOf(parent_) != -1 |
|
1716 && table[i][1].indexOf(child) != -1) { |
|
1717 return false; |
|
1718 } |
|
1719 } |
|
1720 |
|
1721 // "Return true." |
|
1722 return true; |
|
1723 } |
|
1724 |
|
1725 |
|
1726 //@} |
|
1727 |
|
1728 ////////////////////////////////////// |
|
1729 ///// Inline formatting commands ///// |
|
1730 ////////////////////////////////////// |
|
1731 |
|
1732 ///// Inline formatting command definitions ///// |
|
1733 //@{ |
|
1734 |
|
1735 // "A node node is effectively contained in a range range if range is not |
|
1736 // collapsed, and at least one of the following holds:" |
|
1737 function isEffectivelyContained(node, range) { |
|
1738 if (range.collapsed) { |
|
1739 return false; |
|
1740 } |
|
1741 |
|
1742 // "node is contained in range." |
|
1743 if (isContained(node, range)) { |
|
1744 return true; |
|
1745 } |
|
1746 |
|
1747 // "node is range's start node, it is a Text node, and its length is |
|
1748 // different from range's start offset." |
|
1749 if (node == range.startContainer |
|
1750 && node.nodeType == Node.TEXT_NODE |
|
1751 && getNodeLength(node) != range.startOffset) { |
|
1752 return true; |
|
1753 } |
|
1754 |
|
1755 // "node is range's end node, it is a Text node, and range's end offset is |
|
1756 // not 0." |
|
1757 if (node == range.endContainer |
|
1758 && node.nodeType == Node.TEXT_NODE |
|
1759 && range.endOffset != 0) { |
|
1760 return true; |
|
1761 } |
|
1762 |
|
1763 // "node has at least one child; and all its children are effectively |
|
1764 // contained in range; and either range's start node is not a descendant of |
|
1765 // node or is not a Text node or range's start offset is zero; and either |
|
1766 // range's end node is not a descendant of node or is not a Text node or |
|
1767 // range's end offset is its end node's length." |
|
1768 if (node.hasChildNodes() |
|
1769 && [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) }) |
|
1770 && (!isDescendant(range.startContainer, node) |
|
1771 || range.startContainer.nodeType != Node.TEXT_NODE |
|
1772 || range.startOffset == 0) |
|
1773 && (!isDescendant(range.endContainer, node) |
|
1774 || range.endContainer.nodeType != Node.TEXT_NODE |
|
1775 || range.endOffset == getNodeLength(range.endContainer))) { |
|
1776 return true; |
|
1777 } |
|
1778 |
|
1779 return false; |
|
1780 } |
|
1781 |
|
1782 // Like get(All)ContainedNodes(), but for effectively contained nodes. |
|
1783 function getEffectivelyContainedNodes(range, condition) { |
|
1784 if (typeof condition == "undefined") { |
|
1785 condition = function() { return true }; |
|
1786 } |
|
1787 var node = range.startContainer; |
|
1788 while (isEffectivelyContained(node.parentNode, range)) { |
|
1789 node = node.parentNode; |
|
1790 } |
|
1791 |
|
1792 var stop = nextNodeDescendants(range.endContainer); |
|
1793 |
|
1794 var nodeList = []; |
|
1795 while (isBefore(node, stop)) { |
|
1796 if (isEffectivelyContained(node, range) |
|
1797 && condition(node)) { |
|
1798 nodeList.push(node); |
|
1799 node = nextNodeDescendants(node); |
|
1800 continue; |
|
1801 } |
|
1802 node = nextNode(node); |
|
1803 } |
|
1804 return nodeList; |
|
1805 } |
|
1806 |
|
1807 function getAllEffectivelyContainedNodes(range, condition) { |
|
1808 if (typeof condition == "undefined") { |
|
1809 condition = function() { return true }; |
|
1810 } |
|
1811 var node = range.startContainer; |
|
1812 while (isEffectivelyContained(node.parentNode, range)) { |
|
1813 node = node.parentNode; |
|
1814 } |
|
1815 |
|
1816 var stop = nextNodeDescendants(range.endContainer); |
|
1817 |
|
1818 var nodeList = []; |
|
1819 while (isBefore(node, stop)) { |
|
1820 if (isEffectivelyContained(node, range) |
|
1821 && condition(node)) { |
|
1822 nodeList.push(node); |
|
1823 } |
|
1824 node = nextNode(node); |
|
1825 } |
|
1826 return nodeList; |
|
1827 } |
|
1828 |
|
1829 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element |
|
1830 // with no attributes except possibly style; or a font element with no |
|
1831 // attributes except possibly style, color, face, and/or size; or an a element |
|
1832 // with no attributes except possibly style and/or href." |
|
1833 function isModifiableElement(node) { |
|
1834 if (!isHtmlElement(node)) { |
|
1835 return false; |
|
1836 } |
|
1837 |
|
1838 if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) { |
|
1839 if (node.attributes.length == 0) { |
|
1840 return true; |
|
1841 } |
|
1842 |
|
1843 if (node.attributes.length == 1 |
|
1844 && node.hasAttribute("style")) { |
|
1845 return true; |
|
1846 } |
|
1847 } |
|
1848 |
|
1849 if (node.tagName == "FONT" || node.tagName == "A") { |
|
1850 var numAttrs = node.attributes.length; |
|
1851 |
|
1852 if (node.hasAttribute("style")) { |
|
1853 numAttrs--; |
|
1854 } |
|
1855 |
|
1856 if (node.tagName == "FONT") { |
|
1857 if (node.hasAttribute("color")) { |
|
1858 numAttrs--; |
|
1859 } |
|
1860 |
|
1861 if (node.hasAttribute("face")) { |
|
1862 numAttrs--; |
|
1863 } |
|
1864 |
|
1865 if (node.hasAttribute("size")) { |
|
1866 numAttrs--; |
|
1867 } |
|
1868 } |
|
1869 |
|
1870 if (node.tagName == "A" |
|
1871 && node.hasAttribute("href")) { |
|
1872 numAttrs--; |
|
1873 } |
|
1874 |
|
1875 if (numAttrs == 0) { |
|
1876 return true; |
|
1877 } |
|
1878 } |
|
1879 |
|
1880 return false; |
|
1881 } |
|
1882 |
|
1883 function isSimpleModifiableElement(node) { |
|
1884 // "A simple modifiable element is an HTML element for which at least one |
|
1885 // of the following holds:" |
|
1886 if (!isHtmlElement(node)) { |
|
1887 return false; |
|
1888 } |
|
1889 |
|
1890 // Only these elements can possibly be a simple modifiable element. |
|
1891 if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) { |
|
1892 return false; |
|
1893 } |
|
1894 |
|
1895 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u |
|
1896 // element with no attributes." |
|
1897 if (node.attributes.length == 0) { |
|
1898 return true; |
|
1899 } |
|
1900 |
|
1901 // If it's got more than one attribute, everything after this fails. |
|
1902 if (node.attributes.length > 1) { |
|
1903 return false; |
|
1904 } |
|
1905 |
|
1906 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u |
|
1907 // element with exactly one attribute, which is style, which sets no CSS |
|
1908 // properties (including invalid or unrecognized properties)." |
|
1909 // |
|
1910 // Not gonna try for invalid or unrecognized. |
|
1911 if (node.hasAttribute("style") |
|
1912 && node.style.length == 0) { |
|
1913 return true; |
|
1914 } |
|
1915 |
|
1916 // "It is an a element with exactly one attribute, which is href." |
|
1917 if (node.tagName == "A" |
|
1918 && node.hasAttribute("href")) { |
|
1919 return true; |
|
1920 } |
|
1921 |
|
1922 // "It is a font element with exactly one attribute, which is either color, |
|
1923 // face, or size." |
|
1924 if (node.tagName == "FONT" |
|
1925 && (node.hasAttribute("color") |
|
1926 || node.hasAttribute("face") |
|
1927 || node.hasAttribute("size") |
|
1928 )) { |
|
1929 return true; |
|
1930 } |
|
1931 |
|
1932 // "It is a b or strong element with exactly one attribute, which is style, |
|
1933 // and the style attribute sets exactly one CSS property (including invalid |
|
1934 // or unrecognized properties), which is "font-weight"." |
|
1935 if ((node.tagName == "B" || node.tagName == "STRONG") |
|
1936 && node.hasAttribute("style") |
|
1937 && node.style.length == 1 |
|
1938 && node.style.fontWeight != "") { |
|
1939 return true; |
|
1940 } |
|
1941 |
|
1942 // "It is an i or em element with exactly one attribute, which is style, |
|
1943 // and the style attribute sets exactly one CSS property (including invalid |
|
1944 // or unrecognized properties), which is "font-style"." |
|
1945 if ((node.tagName == "I" || node.tagName == "EM") |
|
1946 && node.hasAttribute("style") |
|
1947 && node.style.length == 1 |
|
1948 && node.style.fontStyle != "") { |
|
1949 return true; |
|
1950 } |
|
1951 |
|
1952 // "It is an a, font, or span element with exactly one attribute, which is |
|
1953 // style, and the style attribute sets exactly one CSS property (including |
|
1954 // invalid or unrecognized properties), and that property is not |
|
1955 // "text-decoration"." |
|
1956 if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN") |
|
1957 && node.hasAttribute("style") |
|
1958 && node.style.length == 1 |
|
1959 && node.style.textDecoration == "") { |
|
1960 return true; |
|
1961 } |
|
1962 |
|
1963 // "It is an a, font, s, span, strike, or u element with exactly one |
|
1964 // attribute, which is style, and the style attribute sets exactly one CSS |
|
1965 // property (including invalid or unrecognized properties), which is |
|
1966 // "text-decoration", which is set to "line-through" or "underline" or |
|
1967 // "overline" or "none"." |
|
1968 // |
|
1969 // The weird extra node.style.length check is for Firefox, which as of |
|
1970 // 8.0a2 has annoying and weird behavior here. |
|
1971 if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1 |
|
1972 && node.hasAttribute("style") |
|
1973 && (node.style.length == 1 |
|
1974 || (node.style.length == 4 |
|
1975 && "MozTextBlink" in node.style |
|
1976 && "MozTextDecorationColor" in node.style |
|
1977 && "MozTextDecorationLine" in node.style |
|
1978 && "MozTextDecorationStyle" in node.style) |
|
1979 ) |
|
1980 && (node.style.textDecoration == "line-through" |
|
1981 || node.style.textDecoration == "underline" |
|
1982 || node.style.textDecoration == "overline" |
|
1983 || node.style.textDecoration == "none")) { |
|
1984 return true; |
|
1985 } |
|
1986 |
|
1987 return false; |
|
1988 } |
|
1989 |
|
1990 // "A formattable node is an editable visible node that is either a Text node, |
|
1991 // an img, or a br." |
|
1992 function isFormattableNode(node) { |
|
1993 return isEditable(node) |
|
1994 && isVisible(node) |
|
1995 && (node.nodeType == Node.TEXT_NODE |
|
1996 || isHtmlElement(node, ["img", "br"])); |
|
1997 } |
|
1998 |
|
1999 // "Two quantities are equivalent values for a command if either both are null, |
|
2000 // or both are strings and they're equal and the command does not define any |
|
2001 // equivalent values, or both are strings and the command defines equivalent |
|
2002 // values and they match the definition." |
|
2003 function areEquivalentValues(command, val1, val2) { |
|
2004 if (val1 === null && val2 === null) { |
|
2005 return true; |
|
2006 } |
|
2007 |
|
2008 if (typeof val1 == "string" |
|
2009 && typeof val2 == "string" |
|
2010 && val1 == val2 |
|
2011 && !("equivalentValues" in commands[command])) { |
|
2012 return true; |
|
2013 } |
|
2014 |
|
2015 if (typeof val1 == "string" |
|
2016 && typeof val2 == "string" |
|
2017 && "equivalentValues" in commands[command] |
|
2018 && commands[command].equivalentValues(val1, val2)) { |
|
2019 return true; |
|
2020 } |
|
2021 |
|
2022 return false; |
|
2023 } |
|
2024 |
|
2025 // "Two quantities are loosely equivalent values for a command if either they |
|
2026 // are equivalent values for the command, or if the command is the fontSize |
|
2027 // command; one of the quantities is one of "x-small", "small", "medium", |
|
2028 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is |
|
2029 // the resolved value of "font-size" on a font element whose size attribute has |
|
2030 // the corresponding value set ("1" through "7" respectively)." |
|
2031 function areLooselyEquivalentValues(command, val1, val2) { |
|
2032 if (areEquivalentValues(command, val1, val2)) { |
|
2033 return true; |
|
2034 } |
|
2035 |
|
2036 if (command != "fontsize" |
|
2037 || typeof val1 != "string" |
|
2038 || typeof val2 != "string") { |
|
2039 return false; |
|
2040 } |
|
2041 |
|
2042 // Static variables in JavaScript? |
|
2043 var callee = areLooselyEquivalentValues; |
|
2044 if (callee.sizeMap === undefined) { |
|
2045 callee.sizeMap = {}; |
|
2046 var font = document.createElement("font"); |
|
2047 document.body.appendChild(font); |
|
2048 ["x-small", "small", "medium", "large", "x-large", "xx-large", |
|
2049 "xxx-large"].forEach(function(keyword) { |
|
2050 font.size = cssSizeToLegacy(keyword); |
|
2051 callee.sizeMap[keyword] = getComputedStyle(font).fontSize; |
|
2052 }); |
|
2053 document.body.removeChild(font); |
|
2054 } |
|
2055 |
|
2056 return val1 === callee.sizeMap[val2] |
|
2057 || val2 === callee.sizeMap[val1]; |
|
2058 } |
|
2059 |
|
2060 //@} |
|
2061 ///// Assorted inline formatting command algorithms ///// |
|
2062 //@{ |
|
2063 |
|
2064 function getEffectiveCommandValue(node, command) { |
|
2065 // "If neither node nor its parent is an Element, return null." |
|
2066 if (node.nodeType != Node.ELEMENT_NODE |
|
2067 && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) { |
|
2068 return null; |
|
2069 } |
|
2070 |
|
2071 // "If node is not an Element, return the effective command value of its |
|
2072 // parent for command." |
|
2073 if (node.nodeType != Node.ELEMENT_NODE) { |
|
2074 return getEffectiveCommandValue(node.parentNode, command); |
|
2075 } |
|
2076 |
|
2077 // "If command is "createLink" or "unlink":" |
|
2078 if (command == "createlink" || command == "unlink") { |
|
2079 // "While node is not null, and is not an a element that has an href |
|
2080 // attribute, set node to its parent." |
|
2081 while (node |
|
2082 && (!isHtmlElement(node) |
|
2083 || node.tagName != "A" |
|
2084 || !node.hasAttribute("href"))) { |
|
2085 node = node.parentNode; |
|
2086 } |
|
2087 |
|
2088 // "If node is null, return null." |
|
2089 if (!node) { |
|
2090 return null; |
|
2091 } |
|
2092 |
|
2093 // "Return the value of node's href attribute." |
|
2094 return node.getAttribute("href"); |
|
2095 } |
|
2096 |
|
2097 // "If command is "backColor" or "hiliteColor":" |
|
2098 if (command == "backcolor" |
|
2099 || command == "hilitecolor") { |
|
2100 // "While the resolved value of "background-color" on node is any |
|
2101 // fully transparent value, and node's parent is an Element, set |
|
2102 // node to its parent." |
|
2103 // |
|
2104 // Another lame hack to avoid flawed APIs. |
|
2105 while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" |
|
2106 || getComputedStyle(node).backgroundColor === "" |
|
2107 || getComputedStyle(node).backgroundColor == "transparent") |
|
2108 && node.parentNode |
|
2109 && node.parentNode.nodeType == Node.ELEMENT_NODE) { |
|
2110 node = node.parentNode; |
|
2111 } |
|
2112 |
|
2113 // "Return the resolved value of "background-color" for node." |
|
2114 return getComputedStyle(node).backgroundColor; |
|
2115 } |
|
2116 |
|
2117 // "If command is "subscript" or "superscript":" |
|
2118 if (command == "subscript" || command == "superscript") { |
|
2119 // "Let affected by subscript and affected by superscript be two |
|
2120 // boolean variables, both initially false." |
|
2121 var affectedBySubscript = false; |
|
2122 var affectedBySuperscript = false; |
|
2123 |
|
2124 // "While node is an inline node:" |
|
2125 while (isInlineNode(node)) { |
|
2126 var verticalAlign = getComputedStyle(node).verticalAlign; |
|
2127 |
|
2128 // "If node is a sub, set affected by subscript to true." |
|
2129 if (isHtmlElement(node, "sub")) { |
|
2130 affectedBySubscript = true; |
|
2131 // "Otherwise, if node is a sup, set affected by superscript to |
|
2132 // true." |
|
2133 } else if (isHtmlElement(node, "sup")) { |
|
2134 affectedBySuperscript = true; |
|
2135 } |
|
2136 |
|
2137 // "Set node to its parent." |
|
2138 node = node.parentNode; |
|
2139 } |
|
2140 |
|
2141 // "If affected by subscript and affected by superscript are both true, |
|
2142 // return the string "mixed"." |
|
2143 if (affectedBySubscript && affectedBySuperscript) { |
|
2144 return "mixed"; |
|
2145 } |
|
2146 |
|
2147 // "If affected by subscript is true, return "subscript"." |
|
2148 if (affectedBySubscript) { |
|
2149 return "subscript"; |
|
2150 } |
|
2151 |
|
2152 // "If affected by superscript is true, return "superscript"." |
|
2153 if (affectedBySuperscript) { |
|
2154 return "superscript"; |
|
2155 } |
|
2156 |
|
2157 // "Return null." |
|
2158 return null; |
|
2159 } |
|
2160 |
|
2161 // "If command is "strikethrough", and the "text-decoration" property of |
|
2162 // node or any of its ancestors has resolved value containing |
|
2163 // "line-through", return "line-through". Otherwise, return null." |
|
2164 if (command == "strikethrough") { |
|
2165 do { |
|
2166 if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) { |
|
2167 return "line-through"; |
|
2168 } |
|
2169 node = node.parentNode; |
|
2170 } while (node && node.nodeType == Node.ELEMENT_NODE); |
|
2171 return null; |
|
2172 } |
|
2173 |
|
2174 // "If command is "underline", and the "text-decoration" property of node |
|
2175 // or any of its ancestors has resolved value containing "underline", |
|
2176 // return "underline". Otherwise, return null." |
|
2177 if (command == "underline") { |
|
2178 do { |
|
2179 if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) { |
|
2180 return "underline"; |
|
2181 } |
|
2182 node = node.parentNode; |
|
2183 } while (node && node.nodeType == Node.ELEMENT_NODE); |
|
2184 return null; |
|
2185 } |
|
2186 |
|
2187 if (!("relevantCssProperty" in commands[command])) { |
|
2188 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue"; |
|
2189 } |
|
2190 |
|
2191 // "Return the resolved value for node of the relevant CSS property for |
|
2192 // command." |
|
2193 return getComputedStyle(node)[commands[command].relevantCssProperty]; |
|
2194 } |
|
2195 |
|
2196 function getSpecifiedCommandValue(element, command) { |
|
2197 // "If command is "backColor" or "hiliteColor" and element's display |
|
2198 // property does not have resolved value "inline", return null." |
|
2199 if ((command == "backcolor" || command == "hilitecolor") |
|
2200 && getComputedStyle(element).display != "inline") { |
|
2201 return null; |
|
2202 } |
|
2203 |
|
2204 // "If command is "createLink" or "unlink":" |
|
2205 if (command == "createlink" || command == "unlink") { |
|
2206 // "If element is an a element and has an href attribute, return the |
|
2207 // value of that attribute." |
|
2208 if (isHtmlElement(element) |
|
2209 && element.tagName == "A" |
|
2210 && element.hasAttribute("href")) { |
|
2211 return element.getAttribute("href"); |
|
2212 } |
|
2213 |
|
2214 // "Return null." |
|
2215 return null; |
|
2216 } |
|
2217 |
|
2218 // "If command is "subscript" or "superscript":" |
|
2219 if (command == "subscript" || command == "superscript") { |
|
2220 // "If element is a sup, return "superscript"." |
|
2221 if (isHtmlElement(element, "sup")) { |
|
2222 return "superscript"; |
|
2223 } |
|
2224 |
|
2225 // "If element is a sub, return "subscript"." |
|
2226 if (isHtmlElement(element, "sub")) { |
|
2227 return "subscript"; |
|
2228 } |
|
2229 |
|
2230 // "Return null." |
|
2231 return null; |
|
2232 } |
|
2233 |
|
2234 // "If command is "strikethrough", and element has a style attribute set, |
|
2235 // and that attribute sets "text-decoration":" |
|
2236 if (command == "strikethrough" |
|
2237 && element.style.textDecoration != "") { |
|
2238 // "If element's style attribute sets "text-decoration" to a value |
|
2239 // containing "line-through", return "line-through"." |
|
2240 if (element.style.textDecoration.indexOf("line-through") != -1) { |
|
2241 return "line-through"; |
|
2242 } |
|
2243 |
|
2244 // "Return null." |
|
2245 return null; |
|
2246 } |
|
2247 |
|
2248 // "If command is "strikethrough" and element is a s or strike element, |
|
2249 // return "line-through"." |
|
2250 if (command == "strikethrough" |
|
2251 && isHtmlElement(element, ["S", "STRIKE"])) { |
|
2252 return "line-through"; |
|
2253 } |
|
2254 |
|
2255 // "If command is "underline", and element has a style attribute set, and |
|
2256 // that attribute sets "text-decoration":" |
|
2257 if (command == "underline" |
|
2258 && element.style.textDecoration != "") { |
|
2259 // "If element's style attribute sets "text-decoration" to a value |
|
2260 // containing "underline", return "underline"." |
|
2261 if (element.style.textDecoration.indexOf("underline") != -1) { |
|
2262 return "underline"; |
|
2263 } |
|
2264 |
|
2265 // "Return null." |
|
2266 return null; |
|
2267 } |
|
2268 |
|
2269 // "If command is "underline" and element is a u element, return |
|
2270 // "underline"." |
|
2271 if (command == "underline" |
|
2272 && isHtmlElement(element, "U")) { |
|
2273 return "underline"; |
|
2274 } |
|
2275 |
|
2276 // "Let property be the relevant CSS property for command." |
|
2277 var property = commands[command].relevantCssProperty; |
|
2278 |
|
2279 // "If property is null, return null." |
|
2280 if (property === null) { |
|
2281 return null; |
|
2282 } |
|
2283 |
|
2284 // "If element has a style attribute set, and that attribute has the |
|
2285 // effect of setting property, return the value that it sets property to." |
|
2286 if (element.style[property] != "") { |
|
2287 return element.style[property]; |
|
2288 } |
|
2289 |
|
2290 // "If element is a font element that has an attribute whose effect is |
|
2291 // to create a presentational hint for property, return the value that the |
|
2292 // hint sets property to. (For a size of 7, this will be the non-CSS value |
|
2293 // "xxx-large".)" |
|
2294 if (isHtmlNamespace(element.namespaceURI) |
|
2295 && element.tagName == "FONT") { |
|
2296 if (property == "color" && element.hasAttribute("color")) { |
|
2297 return element.color; |
|
2298 } |
|
2299 if (property == "fontFamily" && element.hasAttribute("face")) { |
|
2300 return element.face; |
|
2301 } |
|
2302 if (property == "fontSize" && element.hasAttribute("size")) { |
|
2303 // This is not even close to correct in general. |
|
2304 var size = parseInt(element.size); |
|
2305 if (size < 1) { |
|
2306 size = 1; |
|
2307 } |
|
2308 if (size > 7) { |
|
2309 size = 7; |
|
2310 } |
|
2311 return { |
|
2312 1: "x-small", |
|
2313 2: "small", |
|
2314 3: "medium", |
|
2315 4: "large", |
|
2316 5: "x-large", |
|
2317 6: "xx-large", |
|
2318 7: "xxx-large" |
|
2319 }[size]; |
|
2320 } |
|
2321 } |
|
2322 |
|
2323 // "If element is in the following list, and property is equal to the |
|
2324 // CSS property name listed for it, return the string listed for it." |
|
2325 // |
|
2326 // A list follows, whose meaning is copied here. |
|
2327 if (property == "fontWeight" |
|
2328 && (element.tagName == "B" || element.tagName == "STRONG")) { |
|
2329 return "bold"; |
|
2330 } |
|
2331 if (property == "fontStyle" |
|
2332 && (element.tagName == "I" || element.tagName == "EM")) { |
|
2333 return "italic"; |
|
2334 } |
|
2335 |
|
2336 // "Return null." |
|
2337 return null; |
|
2338 } |
|
2339 |
|
2340 function reorderModifiableDescendants(node, command, newValue) { |
|
2341 // "Let candidate equal node." |
|
2342 var candidate = node; |
|
2343 |
|
2344 // "While candidate is a modifiable element, and candidate has exactly one |
|
2345 // child, and that child is also a modifiable element, and candidate is not |
|
2346 // a simple modifiable element or candidate's specified command value for |
|
2347 // command is not equivalent to new value, set candidate to its child." |
|
2348 while (isModifiableElement(candidate) |
|
2349 && candidate.childNodes.length == 1 |
|
2350 && isModifiableElement(candidate.firstChild) |
|
2351 && (!isSimpleModifiableElement(candidate) |
|
2352 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) { |
|
2353 candidate = candidate.firstChild; |
|
2354 } |
|
2355 |
|
2356 // "If candidate is node, or is not a simple modifiable element, or its |
|
2357 // specified command value is not equivalent to new value, or its effective |
|
2358 // command value is not loosely equivalent to new value, abort these |
|
2359 // steps." |
|
2360 if (candidate == node |
|
2361 || !isSimpleModifiableElement(candidate) |
|
2362 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue) |
|
2363 || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) { |
|
2364 return; |
|
2365 } |
|
2366 |
|
2367 // "While candidate has children, insert the first child of candidate into |
|
2368 // candidate's parent immediately before candidate, preserving ranges." |
|
2369 while (candidate.hasChildNodes()) { |
|
2370 movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate)); |
|
2371 } |
|
2372 |
|
2373 // "Insert candidate into node's parent immediately after node." |
|
2374 node.parentNode.insertBefore(candidate, node.nextSibling); |
|
2375 |
|
2376 // "Append the node as the last child of candidate, preserving ranges." |
|
2377 movePreservingRanges(node, candidate, -1); |
|
2378 } |
|
2379 |
|
2380 function recordValues(nodeList) { |
|
2381 // "Let values be a list of (node, command, specified command value) |
|
2382 // triples, initially empty." |
|
2383 var values = []; |
|
2384 |
|
2385 // "For each node in node list, for each command in the list "subscript", |
|
2386 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic", |
|
2387 // "strikethrough", and "underline" in that order:" |
|
2388 nodeList.forEach(function(node) { |
|
2389 ["subscript", "bold", "fontname", "fontsize", "forecolor", |
|
2390 "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) { |
|
2391 // "Let ancestor equal node." |
|
2392 var ancestor = node; |
|
2393 |
|
2394 // "If ancestor is not an Element, set it to its parent." |
|
2395 if (ancestor.nodeType != Node.ELEMENT_NODE) { |
|
2396 ancestor = ancestor.parentNode; |
|
2397 } |
|
2398 |
|
2399 // "While ancestor is an Element and its specified command value |
|
2400 // for command is null, set it to its parent." |
|
2401 while (ancestor |
|
2402 && ancestor.nodeType == Node.ELEMENT_NODE |
|
2403 && getSpecifiedCommandValue(ancestor, command) === null) { |
|
2404 ancestor = ancestor.parentNode; |
|
2405 } |
|
2406 |
|
2407 // "If ancestor is an Element, add (node, command, ancestor's |
|
2408 // specified command value for command) to values. Otherwise add |
|
2409 // (node, command, null) to values." |
|
2410 if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) { |
|
2411 values.push([node, command, getSpecifiedCommandValue(ancestor, command)]); |
|
2412 } else { |
|
2413 values.push([node, command, null]); |
|
2414 } |
|
2415 }); |
|
2416 }); |
|
2417 |
|
2418 // "Return values." |
|
2419 return values; |
|
2420 } |
|
2421 |
|
2422 function restoreValues(values) { |
|
2423 // "For each (node, command, value) triple in values:" |
|
2424 values.forEach(function(triple) { |
|
2425 var node = triple[0]; |
|
2426 var command = triple[1]; |
|
2427 var value = triple[2]; |
|
2428 |
|
2429 // "Let ancestor equal node." |
|
2430 var ancestor = node; |
|
2431 |
|
2432 // "If ancestor is not an Element, set it to its parent." |
|
2433 if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) { |
|
2434 ancestor = ancestor.parentNode; |
|
2435 } |
|
2436 |
|
2437 // "While ancestor is an Element and its specified command value for |
|
2438 // command is null, set it to its parent." |
|
2439 while (ancestor |
|
2440 && ancestor.nodeType == Node.ELEMENT_NODE |
|
2441 && getSpecifiedCommandValue(ancestor, command) === null) { |
|
2442 ancestor = ancestor.parentNode; |
|
2443 } |
|
2444 |
|
2445 // "If value is null and ancestor is an Element, push down values on |
|
2446 // node for command, with new value null." |
|
2447 if (value === null |
|
2448 && ancestor |
|
2449 && ancestor.nodeType == Node.ELEMENT_NODE) { |
|
2450 pushDownValues(node, command, null); |
|
2451 |
|
2452 // "Otherwise, if ancestor is an Element and its specified command |
|
2453 // value for command is not equivalent to value, or if ancestor is not |
|
2454 // an Element and value is not null, force the value of command to |
|
2455 // value on node." |
|
2456 } else if ((ancestor |
|
2457 && ancestor.nodeType == Node.ELEMENT_NODE |
|
2458 && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value)) |
|
2459 || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) |
|
2460 && value !== null)) { |
|
2461 forceValue(node, command, value); |
|
2462 } |
|
2463 }); |
|
2464 } |
|
2465 |
|
2466 |
|
2467 //@} |
|
2468 ///// Clearing an element's value ///// |
|
2469 //@{ |
|
2470 |
|
2471 function clearValue(element, command) { |
|
2472 // "If element is not editable, return the empty list." |
|
2473 if (!isEditable(element)) { |
|
2474 return []; |
|
2475 } |
|
2476 |
|
2477 // "If element's specified command value for command is null, return the |
|
2478 // empty list." |
|
2479 if (getSpecifiedCommandValue(element, command) === null) { |
|
2480 return []; |
|
2481 } |
|
2482 |
|
2483 // "If element is a simple modifiable element:" |
|
2484 if (isSimpleModifiableElement(element)) { |
|
2485 // "Let children be the children of element." |
|
2486 var children = Array.prototype.slice.call(element.childNodes); |
|
2487 |
|
2488 // "For each child in children, insert child into element's parent |
|
2489 // immediately before element, preserving ranges." |
|
2490 for (var i = 0; i < children.length; i++) { |
|
2491 movePreservingRanges(children[i], element.parentNode, getNodeIndex(element)); |
|
2492 } |
|
2493 |
|
2494 // "Remove element from its parent." |
|
2495 element.parentNode.removeChild(element); |
|
2496 |
|
2497 // "Return children." |
|
2498 return children; |
|
2499 } |
|
2500 |
|
2501 // "If command is "strikethrough", and element has a style attribute that |
|
2502 // sets "text-decoration" to some value containing "line-through", delete |
|
2503 // "line-through" from the value." |
|
2504 if (command == "strikethrough" |
|
2505 && element.style.textDecoration.indexOf("line-through") != -1) { |
|
2506 if (element.style.textDecoration == "line-through") { |
|
2507 element.style.textDecoration = ""; |
|
2508 } else { |
|
2509 element.style.textDecoration = element.style.textDecoration.replace("line-through", ""); |
|
2510 } |
|
2511 if (element.getAttribute("style") == "") { |
|
2512 element.removeAttribute("style"); |
|
2513 } |
|
2514 } |
|
2515 |
|
2516 // "If command is "underline", and element has a style attribute that sets |
|
2517 // "text-decoration" to some value containing "underline", delete |
|
2518 // "underline" from the value." |
|
2519 if (command == "underline" |
|
2520 && element.style.textDecoration.indexOf("underline") != -1) { |
|
2521 if (element.style.textDecoration == "underline") { |
|
2522 element.style.textDecoration = ""; |
|
2523 } else { |
|
2524 element.style.textDecoration = element.style.textDecoration.replace("underline", ""); |
|
2525 } |
|
2526 if (element.getAttribute("style") == "") { |
|
2527 element.removeAttribute("style"); |
|
2528 } |
|
2529 } |
|
2530 |
|
2531 // "If the relevant CSS property for command is not null, unset the CSS |
|
2532 // property property of element." |
|
2533 if (commands[command].relevantCssProperty !== null) { |
|
2534 element.style[commands[command].relevantCssProperty] = ''; |
|
2535 if (element.getAttribute("style") == "") { |
|
2536 element.removeAttribute("style"); |
|
2537 } |
|
2538 } |
|
2539 |
|
2540 // "If element is a font element:" |
|
2541 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") { |
|
2542 // "If command is "foreColor", unset element's color attribute, if set." |
|
2543 if (command == "forecolor") { |
|
2544 element.removeAttribute("color"); |
|
2545 } |
|
2546 |
|
2547 // "If command is "fontName", unset element's face attribute, if set." |
|
2548 if (command == "fontname") { |
|
2549 element.removeAttribute("face"); |
|
2550 } |
|
2551 |
|
2552 // "If command is "fontSize", unset element's size attribute, if set." |
|
2553 if (command == "fontsize") { |
|
2554 element.removeAttribute("size"); |
|
2555 } |
|
2556 } |
|
2557 |
|
2558 // "If element is an a element and command is "createLink" or "unlink", |
|
2559 // unset the href property of element." |
|
2560 if (isHtmlElement(element, "A") |
|
2561 && (command == "createlink" || command == "unlink")) { |
|
2562 element.removeAttribute("href"); |
|
2563 } |
|
2564 |
|
2565 // "If element's specified command value for command is null, return the |
|
2566 // empty list." |
|
2567 if (getSpecifiedCommandValue(element, command) === null) { |
|
2568 return []; |
|
2569 } |
|
2570 |
|
2571 // "Set the tag name of element to "span", and return the one-node list |
|
2572 // consisting of the result." |
|
2573 return [setTagName(element, "span")]; |
|
2574 } |
|
2575 |
|
2576 |
|
2577 //@} |
|
2578 ///// Pushing down values ///// |
|
2579 //@{ |
|
2580 |
|
2581 function pushDownValues(node, command, newValue) { |
|
2582 // "If node's parent is not an Element, abort this algorithm." |
|
2583 if (!node.parentNode |
|
2584 || node.parentNode.nodeType != Node.ELEMENT_NODE) { |
|
2585 return; |
|
2586 } |
|
2587 |
|
2588 // "If the effective command value of command is loosely equivalent to new |
|
2589 // value on node, abort this algorithm." |
|
2590 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
|
2591 return; |
|
2592 } |
|
2593 |
|
2594 // "Let current ancestor be node's parent." |
|
2595 var currentAncestor = node.parentNode; |
|
2596 |
|
2597 // "Let ancestor list be a list of Nodes, initially empty." |
|
2598 var ancestorList = []; |
|
2599 |
|
2600 // "While current ancestor is an editable Element and the effective command |
|
2601 // value of command is not loosely equivalent to new value on it, append |
|
2602 // current ancestor to ancestor list, then set current ancestor to its |
|
2603 // parent." |
|
2604 while (isEditable(currentAncestor) |
|
2605 && currentAncestor.nodeType == Node.ELEMENT_NODE |
|
2606 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) { |
|
2607 ancestorList.push(currentAncestor); |
|
2608 currentAncestor = currentAncestor.parentNode; |
|
2609 } |
|
2610 |
|
2611 // "If ancestor list is empty, abort this algorithm." |
|
2612 if (!ancestorList.length) { |
|
2613 return; |
|
2614 } |
|
2615 |
|
2616 // "Let propagated value be the specified command value of command on the |
|
2617 // last member of ancestor list." |
|
2618 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command); |
|
2619 |
|
2620 // "If propagated value is null and is not equal to new value, abort this |
|
2621 // algorithm." |
|
2622 if (propagatedValue === null && propagatedValue != newValue) { |
|
2623 return; |
|
2624 } |
|
2625 |
|
2626 // "If the effective command value for the parent of the last member of |
|
2627 // ancestor list is not loosely equivalent to new value, and new value is |
|
2628 // not null, abort this algorithm." |
|
2629 if (newValue !== null |
|
2630 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) { |
|
2631 return; |
|
2632 } |
|
2633 |
|
2634 // "While ancestor list is not empty:" |
|
2635 while (ancestorList.length) { |
|
2636 // "Let current ancestor be the last member of ancestor list." |
|
2637 // "Remove the last member from ancestor list." |
|
2638 var currentAncestor = ancestorList.pop(); |
|
2639 |
|
2640 // "If the specified command value of current ancestor for command is |
|
2641 // not null, set propagated value to that value." |
|
2642 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { |
|
2643 propagatedValue = getSpecifiedCommandValue(currentAncestor, command); |
|
2644 } |
|
2645 |
|
2646 // "Let children be the children of current ancestor." |
|
2647 var children = Array.prototype.slice.call(currentAncestor.childNodes); |
|
2648 |
|
2649 // "If the specified command value of current ancestor for command is |
|
2650 // not null, clear the value of current ancestor." |
|
2651 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { |
|
2652 clearValue(currentAncestor, command); |
|
2653 } |
|
2654 |
|
2655 // "For every child in children:" |
|
2656 for (var i = 0; i < children.length; i++) { |
|
2657 var child = children[i]; |
|
2658 |
|
2659 // "If child is node, continue with the next child." |
|
2660 if (child == node) { |
|
2661 continue; |
|
2662 } |
|
2663 |
|
2664 // "If child is an Element whose specified command value for |
|
2665 // command is neither null nor equivalent to propagated value, |
|
2666 // continue with the next child." |
|
2667 if (child.nodeType == Node.ELEMENT_NODE |
|
2668 && getSpecifiedCommandValue(child, command) !== null |
|
2669 && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) { |
|
2670 continue; |
|
2671 } |
|
2672 |
|
2673 // "If child is the last member of ancestor list, continue with the |
|
2674 // next child." |
|
2675 if (child == ancestorList[ancestorList.length - 1]) { |
|
2676 continue; |
|
2677 } |
|
2678 |
|
2679 // "Force the value of child, with command as in this algorithm |
|
2680 // and new value equal to propagated value." |
|
2681 forceValue(child, command, propagatedValue); |
|
2682 } |
|
2683 } |
|
2684 } |
|
2685 |
|
2686 |
|
2687 //@} |
|
2688 ///// Forcing the value of a node ///// |
|
2689 //@{ |
|
2690 |
|
2691 function forceValue(node, command, newValue) { |
|
2692 // "If node's parent is null, abort this algorithm." |
|
2693 if (!node.parentNode) { |
|
2694 return; |
|
2695 } |
|
2696 |
|
2697 // "If new value is null, abort this algorithm." |
|
2698 if (newValue === null) { |
|
2699 return; |
|
2700 } |
|
2701 |
|
2702 // "If node is an allowed child of "span":" |
|
2703 if (isAllowedChild(node, "span")) { |
|
2704 // "Reorder modifiable descendants of node's previousSibling." |
|
2705 reorderModifiableDescendants(node.previousSibling, command, newValue); |
|
2706 |
|
2707 // "Reorder modifiable descendants of node's nextSibling." |
|
2708 reorderModifiableDescendants(node.nextSibling, command, newValue); |
|
2709 |
|
2710 // "Wrap the one-node list consisting of node, with sibling criteria |
|
2711 // returning true for a simple modifiable element whose specified |
|
2712 // command value is equivalent to new value and whose effective command |
|
2713 // value is loosely equivalent to new value and false otherwise, and |
|
2714 // with new parent instructions returning null." |
|
2715 wrap([node], |
|
2716 function(node) { |
|
2717 return isSimpleModifiableElement(node) |
|
2718 && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue) |
|
2719 && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue); |
|
2720 }, |
|
2721 function() { return null } |
|
2722 ); |
|
2723 } |
|
2724 |
|
2725 // "If node is invisible, abort this algorithm." |
|
2726 if (isInvisible(node)) { |
|
2727 return; |
|
2728 } |
|
2729 |
|
2730 // "If the effective command value of command is loosely equivalent to new |
|
2731 // value on node, abort this algorithm." |
|
2732 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
|
2733 return; |
|
2734 } |
|
2735 |
|
2736 // "If node is not an allowed child of "span":" |
|
2737 if (!isAllowedChild(node, "span")) { |
|
2738 // "Let children be all children of node, omitting any that are |
|
2739 // Elements whose specified command value for command is neither null |
|
2740 // nor equivalent to new value." |
|
2741 var children = []; |
|
2742 for (var i = 0; i < node.childNodes.length; i++) { |
|
2743 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { |
|
2744 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); |
|
2745 |
|
2746 if (specifiedValue !== null |
|
2747 && !areEquivalentValues(command, newValue, specifiedValue)) { |
|
2748 continue; |
|
2749 } |
|
2750 } |
|
2751 children.push(node.childNodes[i]); |
|
2752 } |
|
2753 |
|
2754 // "Force the value of each Node in children, with command and new |
|
2755 // value as in this invocation of the algorithm." |
|
2756 for (var i = 0; i < children.length; i++) { |
|
2757 forceValue(children[i], command, newValue); |
|
2758 } |
|
2759 |
|
2760 // "Abort this algorithm." |
|
2761 return; |
|
2762 } |
|
2763 |
|
2764 // "If the effective command value of command is loosely equivalent to new |
|
2765 // value on node, abort this algorithm." |
|
2766 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
|
2767 return; |
|
2768 } |
|
2769 |
|
2770 // "Let new parent be null." |
|
2771 var newParent = null; |
|
2772 |
|
2773 // "If the CSS styling flag is false:" |
|
2774 if (!cssStylingFlag) { |
|
2775 // "If command is "bold" and new value is "bold", let new parent be the |
|
2776 // result of calling createElement("b") on the ownerDocument of node." |
|
2777 if (command == "bold" && (newValue == "bold" || newValue == "700")) { |
|
2778 newParent = node.ownerDocument.createElement("b"); |
|
2779 } |
|
2780 |
|
2781 // "If command is "italic" and new value is "italic", let new parent be |
|
2782 // the result of calling createElement("i") on the ownerDocument of |
|
2783 // node." |
|
2784 if (command == "italic" && newValue == "italic") { |
|
2785 newParent = node.ownerDocument.createElement("i"); |
|
2786 } |
|
2787 |
|
2788 // "If command is "strikethrough" and new value is "line-through", let |
|
2789 // new parent be the result of calling createElement("s") on the |
|
2790 // ownerDocument of node." |
|
2791 if (command == "strikethrough" && newValue == "line-through") { |
|
2792 newParent = node.ownerDocument.createElement("s"); |
|
2793 } |
|
2794 |
|
2795 // "If command is "underline" and new value is "underline", let new |
|
2796 // parent be the result of calling createElement("u") on the |
|
2797 // ownerDocument of node." |
|
2798 if (command == "underline" && newValue == "underline") { |
|
2799 newParent = node.ownerDocument.createElement("u"); |
|
2800 } |
|
2801 |
|
2802 // "If command is "foreColor", and new value is fully opaque with red, |
|
2803 // green, and blue components in the range 0 to 255:" |
|
2804 if (command == "forecolor" && parseSimpleColor(newValue)) { |
|
2805 // "Let new parent be the result of calling createElement("font") |
|
2806 // on the ownerDocument of node." |
|
2807 newParent = node.ownerDocument.createElement("font"); |
|
2808 |
|
2809 // "Set the color attribute of new parent to the result of applying |
|
2810 // the rules for serializing simple color values to new value |
|
2811 // (interpreted as a simple color)." |
|
2812 newParent.setAttribute("color", parseSimpleColor(newValue)); |
|
2813 } |
|
2814 |
|
2815 // "If command is "fontName", let new parent be the result of calling |
|
2816 // createElement("font") on the ownerDocument of node, then set the |
|
2817 // face attribute of new parent to new value." |
|
2818 if (command == "fontname") { |
|
2819 newParent = node.ownerDocument.createElement("font"); |
|
2820 newParent.face = newValue; |
|
2821 } |
|
2822 } |
|
2823 |
|
2824 // "If command is "createLink" or "unlink":" |
|
2825 if (command == "createlink" || command == "unlink") { |
|
2826 // "Let new parent be the result of calling createElement("a") on the |
|
2827 // ownerDocument of node." |
|
2828 newParent = node.ownerDocument.createElement("a"); |
|
2829 |
|
2830 // "Set the href attribute of new parent to new value." |
|
2831 newParent.setAttribute("href", newValue); |
|
2832 |
|
2833 // "Let ancestor be node's parent." |
|
2834 var ancestor = node.parentNode; |
|
2835 |
|
2836 // "While ancestor is not null:" |
|
2837 while (ancestor) { |
|
2838 // "If ancestor is an a, set the tag name of ancestor to "span", |
|
2839 // and let ancestor be the result." |
|
2840 if (isHtmlElement(ancestor, "A")) { |
|
2841 ancestor = setTagName(ancestor, "span"); |
|
2842 } |
|
2843 |
|
2844 // "Set ancestor to its parent." |
|
2845 ancestor = ancestor.parentNode; |
|
2846 } |
|
2847 } |
|
2848 |
|
2849 // "If command is "fontSize"; and new value is one of "x-small", "small", |
|
2850 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the |
|
2851 // CSS styling flag is false, or new value is "xxx-large": let new parent |
|
2852 // be the result of calling createElement("font") on the ownerDocument of |
|
2853 // node, then set the size attribute of new parent to the number from the |
|
2854 // following table based on new value: [table omitted]" |
|
2855 if (command == "fontsize" |
|
2856 && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1 |
|
2857 && (!cssStylingFlag || newValue == "xxx-large")) { |
|
2858 newParent = node.ownerDocument.createElement("font"); |
|
2859 newParent.size = cssSizeToLegacy(newValue); |
|
2860 } |
|
2861 |
|
2862 // "If command is "subscript" or "superscript" and new value is |
|
2863 // "subscript", let new parent be the result of calling |
|
2864 // createElement("sub") on the ownerDocument of node." |
|
2865 if ((command == "subscript" || command == "superscript") |
|
2866 && newValue == "subscript") { |
|
2867 newParent = node.ownerDocument.createElement("sub"); |
|
2868 } |
|
2869 |
|
2870 // "If command is "subscript" or "superscript" and new value is |
|
2871 // "superscript", let new parent be the result of calling |
|
2872 // createElement("sup") on the ownerDocument of node." |
|
2873 if ((command == "subscript" || command == "superscript") |
|
2874 && newValue == "superscript") { |
|
2875 newParent = node.ownerDocument.createElement("sup"); |
|
2876 } |
|
2877 |
|
2878 // "If new parent is null, let new parent be the result of calling |
|
2879 // createElement("span") on the ownerDocument of node." |
|
2880 if (!newParent) { |
|
2881 newParent = node.ownerDocument.createElement("span"); |
|
2882 } |
|
2883 |
|
2884 // "Insert new parent in node's parent before node." |
|
2885 node.parentNode.insertBefore(newParent, node); |
|
2886 |
|
2887 // "If the effective command value of command for new parent is not loosely |
|
2888 // equivalent to new value, and the relevant CSS property for command is |
|
2889 // not null, set that CSS property of new parent to new value (if the new |
|
2890 // value would be valid)." |
|
2891 var property = commands[command].relevantCssProperty; |
|
2892 if (property !== null |
|
2893 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) { |
|
2894 newParent.style[property] = newValue; |
|
2895 } |
|
2896 |
|
2897 // "If command is "strikethrough", and new value is "line-through", and the |
|
2898 // effective command value of "strikethrough" for new parent is not |
|
2899 // "line-through", set the "text-decoration" property of new parent to |
|
2900 // "line-through"." |
|
2901 if (command == "strikethrough" |
|
2902 && newValue == "line-through" |
|
2903 && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") { |
|
2904 newParent.style.textDecoration = "line-through"; |
|
2905 } |
|
2906 |
|
2907 // "If command is "underline", and new value is "underline", and the |
|
2908 // effective command value of "underline" for new parent is not |
|
2909 // "underline", set the "text-decoration" property of new parent to |
|
2910 // "underline"." |
|
2911 if (command == "underline" |
|
2912 && newValue == "underline" |
|
2913 && getEffectiveCommandValue(newParent, "underline") != "underline") { |
|
2914 newParent.style.textDecoration = "underline"; |
|
2915 } |
|
2916 |
|
2917 // "Append node to new parent as its last child, preserving ranges." |
|
2918 movePreservingRanges(node, newParent, newParent.childNodes.length); |
|
2919 |
|
2920 // "If node is an Element and the effective command value of command for |
|
2921 // node is not loosely equivalent to new value:" |
|
2922 if (node.nodeType == Node.ELEMENT_NODE |
|
2923 && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
|
2924 // "Insert node into the parent of new parent before new parent, |
|
2925 // preserving ranges." |
|
2926 movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent)); |
|
2927 |
|
2928 // "Remove new parent from its parent." |
|
2929 newParent.parentNode.removeChild(newParent); |
|
2930 |
|
2931 // "Let children be all children of node, omitting any that are |
|
2932 // Elements whose specified command value for command is neither null |
|
2933 // nor equivalent to new value." |
|
2934 var children = []; |
|
2935 for (var i = 0; i < node.childNodes.length; i++) { |
|
2936 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { |
|
2937 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); |
|
2938 |
|
2939 if (specifiedValue !== null |
|
2940 && !areEquivalentValues(command, newValue, specifiedValue)) { |
|
2941 continue; |
|
2942 } |
|
2943 } |
|
2944 children.push(node.childNodes[i]); |
|
2945 } |
|
2946 |
|
2947 // "Force the value of each Node in children, with command and new |
|
2948 // value as in this invocation of the algorithm." |
|
2949 for (var i = 0; i < children.length; i++) { |
|
2950 forceValue(children[i], command, newValue); |
|
2951 } |
|
2952 } |
|
2953 } |
|
2954 |
|
2955 |
|
2956 //@} |
|
2957 ///// Setting the selection's value ///// |
|
2958 //@{ |
|
2959 |
|
2960 function setSelectionValue(command, newValue) { |
|
2961 // "If there is no formattable node effectively contained in the active |
|
2962 // range:" |
|
2963 if (!getAllEffectivelyContainedNodes(getActiveRange()) |
|
2964 .some(isFormattableNode)) { |
|
2965 // "If command has inline command activated values, set the state |
|
2966 // override to true if new value is among them and false if it's not." |
|
2967 if ("inlineCommandActivatedValues" in commands[command]) { |
|
2968 setStateOverride(command, commands[command].inlineCommandActivatedValues |
|
2969 .indexOf(newValue) != -1); |
|
2970 } |
|
2971 |
|
2972 // "If command is "subscript", unset the state override for |
|
2973 // "superscript"." |
|
2974 if (command == "subscript") { |
|
2975 unsetStateOverride("superscript"); |
|
2976 } |
|
2977 |
|
2978 // "If command is "superscript", unset the state override for |
|
2979 // "subscript"." |
|
2980 if (command == "superscript") { |
|
2981 unsetStateOverride("subscript"); |
|
2982 } |
|
2983 |
|
2984 // "If new value is null, unset the value override (if any)." |
|
2985 if (newValue === null) { |
|
2986 unsetValueOverride(command); |
|
2987 |
|
2988 // "Otherwise, if command is "createLink" or it has a value specified, |
|
2989 // set the value override to new value." |
|
2990 } else if (command == "createlink" || "value" in commands[command]) { |
|
2991 setValueOverride(command, newValue); |
|
2992 } |
|
2993 |
|
2994 // "Abort these steps." |
|
2995 return; |
|
2996 } |
|
2997 |
|
2998 // "If the active range's start node is an editable Text node, and its |
|
2999 // start offset is neither zero nor its start node's length, call |
|
3000 // splitText() on the active range's start node, with argument equal to the |
|
3001 // active range's start offset. Then set the active range's start node to |
|
3002 // the result, and its start offset to zero." |
|
3003 if (isEditable(getActiveRange().startContainer) |
|
3004 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
|
3005 && getActiveRange().startOffset != 0 |
|
3006 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { |
|
3007 // Account for browsers not following range mutation rules |
|
3008 var newActiveRange = document.createRange(); |
|
3009 var newNode; |
|
3010 if (getActiveRange().startContainer == getActiveRange().endContainer) { |
|
3011 var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset; |
|
3012 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); |
|
3013 newActiveRange.setEnd(newNode, newEndOffset); |
|
3014 getActiveRange().setEnd(newNode, newEndOffset); |
|
3015 } else { |
|
3016 newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); |
|
3017 } |
|
3018 newActiveRange.setStart(newNode, 0); |
|
3019 getSelection().removeAllRanges(); |
|
3020 getSelection().addRange(newActiveRange); |
|
3021 |
|
3022 getActiveRange().setStart(newNode, 0); |
|
3023 } |
|
3024 |
|
3025 // "If the active range's end node is an editable Text node, and its end |
|
3026 // offset is neither zero nor its end node's length, call splitText() on |
|
3027 // the active range's end node, with argument equal to the active range's |
|
3028 // end offset." |
|
3029 if (isEditable(getActiveRange().endContainer) |
|
3030 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE |
|
3031 && getActiveRange().endOffset != 0 |
|
3032 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { |
|
3033 // IE seems to mutate the range incorrectly here, so we need correction |
|
3034 // here as well. The active range will be temporarily in orphaned |
|
3035 // nodes, so calling getActiveRange() after splitText() but before |
|
3036 // fixing the range will throw an exception. |
|
3037 var activeRange = getActiveRange(); |
|
3038 var newStart = [activeRange.startContainer, activeRange.startOffset]; |
|
3039 var newEnd = [activeRange.endContainer, activeRange.endOffset]; |
|
3040 activeRange.endContainer.splitText(activeRange.endOffset); |
|
3041 activeRange.setStart(newStart[0], newStart[1]); |
|
3042 activeRange.setEnd(newEnd[0], newEnd[1]); |
|
3043 |
|
3044 getSelection().removeAllRanges(); |
|
3045 getSelection().addRange(activeRange); |
|
3046 } |
|
3047 |
|
3048 // "Let element list be all editable Elements effectively contained in the |
|
3049 // active range. |
|
3050 // |
|
3051 // "For each element in element list, clear the value of element." |
|
3052 getAllEffectivelyContainedNodes(getActiveRange(), function(node) { |
|
3053 return isEditable(node) && node.nodeType == Node.ELEMENT_NODE; |
|
3054 }).forEach(function(element) { |
|
3055 clearValue(element, command); |
|
3056 }); |
|
3057 |
|
3058 // "Let node list be all editable nodes effectively contained in the active |
|
3059 // range. |
|
3060 // |
|
3061 // "For each node in node list:" |
|
3062 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) { |
|
3063 // "Push down values on node." |
|
3064 pushDownValues(node, command, newValue); |
|
3065 |
|
3066 // "If node is an allowed child of span, force the value of node." |
|
3067 if (isAllowedChild(node, "span")) { |
|
3068 forceValue(node, command, newValue); |
|
3069 } |
|
3070 }); |
|
3071 } |
|
3072 |
|
3073 |
|
3074 //@} |
|
3075 ///// The backColor command ///// |
|
3076 //@{ |
|
3077 commands.backcolor = { |
|
3078 // Copy-pasted, same as hiliteColor |
|
3079 action: function(value) { |
|
3080 // Action is further copy-pasted, same as foreColor |
|
3081 |
|
3082 // "If value is not a valid CSS color, prepend "#" to it." |
|
3083 // |
|
3084 // "If value is still not a valid CSS color, or if it is currentColor, |
|
3085 // return false." |
|
3086 // |
|
3087 // Cheap hack for testing, no attempt to be comprehensive. |
|
3088 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
|
3089 value = "#" + value; |
|
3090 } |
|
3091 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
|
3092 && !parseSimpleColor(value) |
|
3093 && value.toLowerCase() != "transparent") { |
|
3094 return false; |
|
3095 } |
|
3096 |
|
3097 // "Set the selection's value to value." |
|
3098 setSelectionValue("backcolor", value); |
|
3099 |
|
3100 // "Return true." |
|
3101 return true; |
|
3102 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", |
|
3103 equivalentValues: function(val1, val2) { |
|
3104 // "Either both strings are valid CSS colors and have the same red, |
|
3105 // green, blue, and alpha components, or neither string is a valid CSS |
|
3106 // color." |
|
3107 return normalizeColor(val1) === normalizeColor(val2); |
|
3108 }, |
|
3109 }; |
|
3110 |
|
3111 //@} |
|
3112 ///// The bold command ///// |
|
3113 //@{ |
|
3114 commands.bold = { |
|
3115 action: function() { |
|
3116 // "If queryCommandState("bold") returns true, set the selection's |
|
3117 // value to "normal". Otherwise set the selection's value to "bold". |
|
3118 // Either way, return true." |
|
3119 if (myQueryCommandState("bold")) { |
|
3120 setSelectionValue("bold", "normal"); |
|
3121 } else { |
|
3122 setSelectionValue("bold", "bold"); |
|
3123 } |
|
3124 return true; |
|
3125 }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"], |
|
3126 relevantCssProperty: "fontWeight", |
|
3127 equivalentValues: function(val1, val2) { |
|
3128 // "Either the two strings are equal, or one is "bold" and the other is |
|
3129 // "700", or one is "normal" and the other is "400"." |
|
3130 return val1 == val2 |
|
3131 || (val1 == "bold" && val2 == "700") |
|
3132 || (val1 == "700" && val2 == "bold") |
|
3133 || (val1 == "normal" && val2 == "400") |
|
3134 || (val1 == "400" && val2 == "normal"); |
|
3135 }, |
|
3136 }; |
|
3137 |
|
3138 //@} |
|
3139 ///// The createLink command ///// |
|
3140 //@{ |
|
3141 commands.createlink = { |
|
3142 action: function(value) { |
|
3143 // "If value is the empty string, return false." |
|
3144 if (value === "") { |
|
3145 return false; |
|
3146 } |
|
3147 |
|
3148 // "For each editable a element that has an href attribute and is an |
|
3149 // ancestor of some node effectively contained in the active range, set |
|
3150 // that a element's href attribute to value." |
|
3151 // |
|
3152 // TODO: We don't actually do this in tree order, not that it matters |
|
3153 // unless you're spying with mutation events. |
|
3154 getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) { |
|
3155 getAncestors(node).forEach(function(ancestor) { |
|
3156 if (isEditable(ancestor) |
|
3157 && isHtmlElement(ancestor, "a") |
|
3158 && ancestor.hasAttribute("href")) { |
|
3159 ancestor.setAttribute("href", value); |
|
3160 } |
|
3161 }); |
|
3162 }); |
|
3163 |
|
3164 // "Set the selection's value to value." |
|
3165 setSelectionValue("createlink", value); |
|
3166 |
|
3167 // "Return true." |
|
3168 return true; |
|
3169 } |
|
3170 }; |
|
3171 |
|
3172 //@} |
|
3173 ///// The fontName command ///// |
|
3174 //@{ |
|
3175 commands.fontname = { |
|
3176 action: function(value) { |
|
3177 // "Set the selection's value to value, then return true." |
|
3178 setSelectionValue("fontname", value); |
|
3179 return true; |
|
3180 }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily" |
|
3181 }; |
|
3182 |
|
3183 //@} |
|
3184 ///// The fontSize command ///// |
|
3185 //@{ |
|
3186 |
|
3187 // Helper function for fontSize's action plus queryOutputHelper. It's just the |
|
3188 // middle of fontSize's action, ripped out into its own function. Returns null |
|
3189 // if the size is invalid. |
|
3190 function normalizeFontSize(value) { |
|
3191 // "Strip leading and trailing whitespace from value." |
|
3192 // |
|
3193 // Cheap hack, not following the actual algorithm. |
|
3194 value = value.trim(); |
|
3195 |
|
3196 // "If value is not a valid floating point number, and would not be a valid |
|
3197 // floating point number if a single leading "+" character were stripped, |
|
3198 // return false." |
|
3199 if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) { |
|
3200 return null; |
|
3201 } |
|
3202 |
|
3203 var mode; |
|
3204 |
|
3205 // "If the first character of value is "+", delete the character and let |
|
3206 // mode be "relative-plus"." |
|
3207 if (value[0] == "+") { |
|
3208 value = value.slice(1); |
|
3209 mode = "relative-plus"; |
|
3210 // "Otherwise, if the first character of value is "-", delete the character |
|
3211 // and let mode be "relative-minus"." |
|
3212 } else if (value[0] == "-") { |
|
3213 value = value.slice(1); |
|
3214 mode = "relative-minus"; |
|
3215 // "Otherwise, let mode be "absolute"." |
|
3216 } else { |
|
3217 mode = "absolute"; |
|
3218 } |
|
3219 |
|
3220 // "Apply the rules for parsing non-negative integers to value, and let |
|
3221 // number be the result." |
|
3222 // |
|
3223 // Another cheap hack. |
|
3224 var num = parseInt(value); |
|
3225 |
|
3226 // "If mode is "relative-plus", add three to number." |
|
3227 if (mode == "relative-plus") { |
|
3228 num += 3; |
|
3229 } |
|
3230 |
|
3231 // "If mode is "relative-minus", negate number, then add three to it." |
|
3232 if (mode == "relative-minus") { |
|
3233 num = 3 - num; |
|
3234 } |
|
3235 |
|
3236 // "If number is less than one, let number equal 1." |
|
3237 if (num < 1) { |
|
3238 num = 1; |
|
3239 } |
|
3240 |
|
3241 // "If number is greater than seven, let number equal 7." |
|
3242 if (num > 7) { |
|
3243 num = 7; |
|
3244 } |
|
3245 |
|
3246 // "Set value to the string here corresponding to number:" [table omitted] |
|
3247 value = { |
|
3248 1: "x-small", |
|
3249 2: "small", |
|
3250 3: "medium", |
|
3251 4: "large", |
|
3252 5: "x-large", |
|
3253 6: "xx-large", |
|
3254 7: "xxx-large" |
|
3255 }[num]; |
|
3256 |
|
3257 return value; |
|
3258 } |
|
3259 |
|
3260 commands.fontsize = { |
|
3261 action: function(value) { |
|
3262 value = normalizeFontSize(value); |
|
3263 if (value === null) { |
|
3264 return false; |
|
3265 } |
|
3266 |
|
3267 // "Set the selection's value to value." |
|
3268 setSelectionValue("fontsize", value); |
|
3269 |
|
3270 // "Return true." |
|
3271 return true; |
|
3272 }, indeterm: function() { |
|
3273 // "True if among formattable nodes that are effectively contained in |
|
3274 // the active range, there are two that have distinct effective command |
|
3275 // values. Otherwise false." |
|
3276 return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode) |
|
3277 .map(function(node) { |
|
3278 return getEffectiveCommandValue(node, "fontsize"); |
|
3279 }).filter(function(value, i, arr) { |
|
3280 return arr.slice(0, i).indexOf(value) == -1; |
|
3281 }).length >= 2; |
|
3282 }, value: function() { |
|
3283 // "If the active range is null, return the empty string." |
|
3284 if (!getActiveRange()) { |
|
3285 return ""; |
|
3286 } |
|
3287 |
|
3288 // "Let pixel size be the effective command value of the first |
|
3289 // formattable node that is effectively contained in the active range, |
|
3290 // or if there is no such node, the effective command value of the |
|
3291 // active range's start node, in either case interpreted as a number of |
|
3292 // pixels." |
|
3293 var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0]; |
|
3294 if (node === undefined) { |
|
3295 node = getActiveRange().startContainer; |
|
3296 } |
|
3297 var pixelSize = getEffectiveCommandValue(node, "fontsize"); |
|
3298 |
|
3299 // "Return the legacy font size for pixel size." |
|
3300 return getLegacyFontSize(pixelSize); |
|
3301 }, relevantCssProperty: "fontSize" |
|
3302 }; |
|
3303 |
|
3304 function getLegacyFontSize(size) { |
|
3305 if (getLegacyFontSize.resultCache === undefined) { |
|
3306 getLegacyFontSize.resultCache = {}; |
|
3307 } |
|
3308 |
|
3309 if (getLegacyFontSize.resultCache[size] !== undefined) { |
|
3310 return getLegacyFontSize.resultCache[size]; |
|
3311 } |
|
3312 |
|
3313 // For convenience in other places in my code, I handle all sizes, not just |
|
3314 // pixel sizes as the spec says. This means pixel sizes have to be passed |
|
3315 // in suffixed with "px", not as plain numbers. |
|
3316 if (normalizeFontSize(size) !== null) { |
|
3317 return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size)); |
|
3318 } |
|
3319 |
|
3320 if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1 |
|
3321 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) { |
|
3322 // There is no sensible legacy size for things like "2em". |
|
3323 return getLegacyFontSize.resultCache[size] = null; |
|
3324 } |
|
3325 |
|
3326 var font = document.createElement("font"); |
|
3327 document.body.appendChild(font); |
|
3328 if (size == "xxx-large") { |
|
3329 font.size = 7; |
|
3330 } else { |
|
3331 font.style.fontSize = size; |
|
3332 } |
|
3333 var pixelSize = parseInt(getComputedStyle(font).fontSize); |
|
3334 document.body.removeChild(font); |
|
3335 |
|
3336 // "Let returned size be 1." |
|
3337 var returnedSize = 1; |
|
3338 |
|
3339 // "While returned size is less than 7:" |
|
3340 while (returnedSize < 7) { |
|
3341 // "Let lower bound be the resolved value of "font-size" in pixels |
|
3342 // of a font element whose size attribute is set to returned size." |
|
3343 var font = document.createElement("font"); |
|
3344 font.size = returnedSize; |
|
3345 document.body.appendChild(font); |
|
3346 var lowerBound = parseInt(getComputedStyle(font).fontSize); |
|
3347 |
|
3348 // "Let upper bound be the resolved value of "font-size" in pixels |
|
3349 // of a font element whose size attribute is set to one plus |
|
3350 // returned size." |
|
3351 font.size = 1 + returnedSize; |
|
3352 var upperBound = parseInt(getComputedStyle(font).fontSize); |
|
3353 document.body.removeChild(font); |
|
3354 |
|
3355 // "Let average be the average of upper bound and lower bound." |
|
3356 var average = (upperBound + lowerBound)/2; |
|
3357 |
|
3358 // "If pixel size is less than average, return the one-element |
|
3359 // string consisting of the digit returned size." |
|
3360 if (pixelSize < average) { |
|
3361 return getLegacyFontSize.resultCache[size] = String(returnedSize); |
|
3362 } |
|
3363 |
|
3364 // "Add one to returned size." |
|
3365 returnedSize++; |
|
3366 } |
|
3367 |
|
3368 // "Return "7"." |
|
3369 return getLegacyFontSize.resultCache[size] = "7"; |
|
3370 } |
|
3371 |
|
3372 //@} |
|
3373 ///// The foreColor command ///// |
|
3374 //@{ |
|
3375 commands.forecolor = { |
|
3376 action: function(value) { |
|
3377 // Copy-pasted, same as backColor and hiliteColor |
|
3378 |
|
3379 // "If value is not a valid CSS color, prepend "#" to it." |
|
3380 // |
|
3381 // "If value is still not a valid CSS color, or if it is currentColor, |
|
3382 // return false." |
|
3383 // |
|
3384 // Cheap hack for testing, no attempt to be comprehensive. |
|
3385 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
|
3386 value = "#" + value; |
|
3387 } |
|
3388 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
|
3389 && !parseSimpleColor(value) |
|
3390 && value.toLowerCase() != "transparent") { |
|
3391 return false; |
|
3392 } |
|
3393 |
|
3394 // "Set the selection's value to value." |
|
3395 setSelectionValue("forecolor", value); |
|
3396 |
|
3397 // "Return true." |
|
3398 return true; |
|
3399 }, standardInlineValueCommand: true, relevantCssProperty: "color", |
|
3400 equivalentValues: function(val1, val2) { |
|
3401 // "Either both strings are valid CSS colors and have the same red, |
|
3402 // green, blue, and alpha components, or neither string is a valid CSS |
|
3403 // color." |
|
3404 return normalizeColor(val1) === normalizeColor(val2); |
|
3405 }, |
|
3406 }; |
|
3407 |
|
3408 //@} |
|
3409 ///// The hiliteColor command ///// |
|
3410 //@{ |
|
3411 commands.hilitecolor = { |
|
3412 // Copy-pasted, same as backColor |
|
3413 action: function(value) { |
|
3414 // Action is further copy-pasted, same as foreColor |
|
3415 |
|
3416 // "If value is not a valid CSS color, prepend "#" to it." |
|
3417 // |
|
3418 // "If value is still not a valid CSS color, or if it is currentColor, |
|
3419 // return false." |
|
3420 // |
|
3421 // Cheap hack for testing, no attempt to be comprehensive. |
|
3422 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
|
3423 value = "#" + value; |
|
3424 } |
|
3425 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
|
3426 && !parseSimpleColor(value) |
|
3427 && value.toLowerCase() != "transparent") { |
|
3428 return false; |
|
3429 } |
|
3430 |
|
3431 // "Set the selection's value to value." |
|
3432 setSelectionValue("hilitecolor", value); |
|
3433 |
|
3434 // "Return true." |
|
3435 return true; |
|
3436 }, indeterm: function() { |
|
3437 // "True if among editable Text nodes that are effectively contained in |
|
3438 // the active range, there are two that have distinct effective command |
|
3439 // values. Otherwise false." |
|
3440 return getAllEffectivelyContainedNodes(getActiveRange(), function(node) { |
|
3441 return isEditable(node) && node.nodeType == Node.TEXT_NODE; |
|
3442 }).map(function(node) { |
|
3443 return getEffectiveCommandValue(node, "hilitecolor"); |
|
3444 }).filter(function(value, i, arr) { |
|
3445 return arr.slice(0, i).indexOf(value) == -1; |
|
3446 }).length >= 2; |
|
3447 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", |
|
3448 equivalentValues: function(val1, val2) { |
|
3449 // "Either both strings are valid CSS colors and have the same red, |
|
3450 // green, blue, and alpha components, or neither string is a valid CSS |
|
3451 // color." |
|
3452 return normalizeColor(val1) === normalizeColor(val2); |
|
3453 }, |
|
3454 }; |
|
3455 |
|
3456 //@} |
|
3457 ///// The italic command ///// |
|
3458 //@{ |
|
3459 commands.italic = { |
|
3460 action: function() { |
|
3461 // "If queryCommandState("italic") returns true, set the selection's |
|
3462 // value to "normal". Otherwise set the selection's value to "italic". |
|
3463 // Either way, return true." |
|
3464 if (myQueryCommandState("italic")) { |
|
3465 setSelectionValue("italic", "normal"); |
|
3466 } else { |
|
3467 setSelectionValue("italic", "italic"); |
|
3468 } |
|
3469 return true; |
|
3470 }, inlineCommandActivatedValues: ["italic", "oblique"], |
|
3471 relevantCssProperty: "fontStyle" |
|
3472 }; |
|
3473 |
|
3474 //@} |
|
3475 ///// The removeFormat command ///// |
|
3476 //@{ |
|
3477 commands.removeformat = { |
|
3478 action: function() { |
|
3479 // "A removeFormat candidate is an editable HTML element with local |
|
3480 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", |
|
3481 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", |
|
3482 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup", |
|
3483 // "tt", "u", or "var"." |
|
3484 function isRemoveFormatCandidate(node) { |
|
3485 return isEditable(node) |
|
3486 && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo", |
|
3487 "big", "blink", "cite", "code", "dfn", "em", "font", "i", |
|
3488 "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", |
|
3489 "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]); |
|
3490 } |
|
3491 |
|
3492 // "Let elements to remove be a list of every removeFormat candidate |
|
3493 // effectively contained in the active range." |
|
3494 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate); |
|
3495 |
|
3496 // "For each element in elements to remove:" |
|
3497 elementsToRemove.forEach(function(element) { |
|
3498 // "While element has children, insert the first child of element |
|
3499 // into the parent of element immediately before element, |
|
3500 // preserving ranges." |
|
3501 while (element.hasChildNodes()) { |
|
3502 movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element)); |
|
3503 } |
|
3504 |
|
3505 // "Remove element from its parent." |
|
3506 element.parentNode.removeChild(element); |
|
3507 }); |
|
3508 |
|
3509 // "If the active range's start node is an editable Text node, and its |
|
3510 // start offset is neither zero nor its start node's length, call |
|
3511 // splitText() on the active range's start node, with argument equal to |
|
3512 // the active range's start offset. Then set the active range's start |
|
3513 // node to the result, and its start offset to zero." |
|
3514 if (isEditable(getActiveRange().startContainer) |
|
3515 && getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
|
3516 && getActiveRange().startOffset != 0 |
|
3517 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { |
|
3518 // Account for browsers not following range mutation rules |
|
3519 if (getActiveRange().startContainer == getActiveRange().endContainer) { |
|
3520 var newEnd = getActiveRange().endOffset - getActiveRange().startOffset; |
|
3521 var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); |
|
3522 getActiveRange().setStart(newNode, 0); |
|
3523 getActiveRange().setEnd(newNode, newEnd); |
|
3524 } else { |
|
3525 getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0); |
|
3526 } |
|
3527 } |
|
3528 |
|
3529 // "If the active range's end node is an editable Text node, and its |
|
3530 // end offset is neither zero nor its end node's length, call |
|
3531 // splitText() on the active range's end node, with argument equal to |
|
3532 // the active range's end offset." |
|
3533 if (isEditable(getActiveRange().endContainer) |
|
3534 && getActiveRange().endContainer.nodeType == Node.TEXT_NODE |
|
3535 && getActiveRange().endOffset != 0 |
|
3536 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { |
|
3537 // IE seems to mutate the range incorrectly here, so we need |
|
3538 // correction here as well. Have to be careful to set the range to |
|
3539 // something not including the text node so that getActiveRange() |
|
3540 // doesn't throw an exception due to a temporarily detached |
|
3541 // endpoint. |
|
3542 var newStart = [getActiveRange().startContainer, getActiveRange().startOffset]; |
|
3543 var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset]; |
|
3544 getActiveRange().setEnd(document.documentElement, 0); |
|
3545 newEnd[0].splitText(newEnd[1]); |
|
3546 getActiveRange().setStart(newStart[0], newStart[1]); |
|
3547 getActiveRange().setEnd(newEnd[0], newEnd[1]); |
|
3548 } |
|
3549 |
|
3550 // "Let node list consist of all editable nodes effectively contained |
|
3551 // in the active range." |
|
3552 // |
|
3553 // "For each node in node list, while node's parent is a removeFormat |
|
3554 // candidate in the same editing host as node, split the parent of the |
|
3555 // one-node list consisting of node." |
|
3556 getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) { |
|
3557 while (isRemoveFormatCandidate(node.parentNode) |
|
3558 && inSameEditingHost(node.parentNode, node)) { |
|
3559 splitParent([node]); |
|
3560 } |
|
3561 }); |
|
3562 |
|
3563 // "For each of the entries in the following list, in the given order, |
|
3564 // set the selection's value to null, with command as given." |
|
3565 [ |
|
3566 "subscript", |
|
3567 "bold", |
|
3568 "fontname", |
|
3569 "fontsize", |
|
3570 "forecolor", |
|
3571 "hilitecolor", |
|
3572 "italic", |
|
3573 "strikethrough", |
|
3574 "underline", |
|
3575 ].forEach(function(command) { |
|
3576 setSelectionValue(command, null); |
|
3577 }); |
|
3578 |
|
3579 // "Return true." |
|
3580 return true; |
|
3581 } |
|
3582 }; |
|
3583 |
|
3584 //@} |
|
3585 ///// The strikethrough command ///// |
|
3586 //@{ |
|
3587 commands.strikethrough = { |
|
3588 action: function() { |
|
3589 // "If queryCommandState("strikethrough") returns true, set the |
|
3590 // selection's value to null. Otherwise set the selection's value to |
|
3591 // "line-through". Either way, return true." |
|
3592 if (myQueryCommandState("strikethrough")) { |
|
3593 setSelectionValue("strikethrough", null); |
|
3594 } else { |
|
3595 setSelectionValue("strikethrough", "line-through"); |
|
3596 } |
|
3597 return true; |
|
3598 }, inlineCommandActivatedValues: ["line-through"] |
|
3599 }; |
|
3600 |
|
3601 //@} |
|
3602 ///// The subscript command ///// |
|
3603 //@{ |
|
3604 commands.subscript = { |
|
3605 action: function() { |
|
3606 // "Call queryCommandState("subscript"), and let state be the result." |
|
3607 var state = myQueryCommandState("subscript"); |
|
3608 |
|
3609 // "Set the selection's value to null." |
|
3610 setSelectionValue("subscript", null); |
|
3611 |
|
3612 // "If state is false, set the selection's value to "subscript"." |
|
3613 if (!state) { |
|
3614 setSelectionValue("subscript", "subscript"); |
|
3615 } |
|
3616 |
|
3617 // "Return true." |
|
3618 return true; |
|
3619 }, indeterm: function() { |
|
3620 // "True if either among formattable nodes that are effectively |
|
3621 // contained in the active range, there is at least one with effective |
|
3622 // command value "subscript" and at least one with some other effective |
|
3623 // command value; or if there is some formattable node effectively |
|
3624 // contained in the active range with effective command value "mixed". |
|
3625 // Otherwise false." |
|
3626 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); |
|
3627 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" }) |
|
3628 && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" })) |
|
3629 || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" }); |
|
3630 }, inlineCommandActivatedValues: ["subscript"], |
|
3631 }; |
|
3632 |
|
3633 //@} |
|
3634 ///// The superscript command ///// |
|
3635 //@{ |
|
3636 commands.superscript = { |
|
3637 action: function() { |
|
3638 // "Call queryCommandState("superscript"), and let state be the |
|
3639 // result." |
|
3640 var state = myQueryCommandState("superscript"); |
|
3641 |
|
3642 // "Set the selection's value to null." |
|
3643 setSelectionValue("superscript", null); |
|
3644 |
|
3645 // "If state is false, set the selection's value to "superscript"." |
|
3646 if (!state) { |
|
3647 setSelectionValue("superscript", "superscript"); |
|
3648 } |
|
3649 |
|
3650 // "Return true." |
|
3651 return true; |
|
3652 }, indeterm: function() { |
|
3653 // "True if either among formattable nodes that are effectively |
|
3654 // contained in the active range, there is at least one with effective |
|
3655 // command value "superscript" and at least one with some other |
|
3656 // effective command value; or if there is some formattable node |
|
3657 // effectively contained in the active range with effective command |
|
3658 // value "mixed". Otherwise false." |
|
3659 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); |
|
3660 return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" }) |
|
3661 && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" })) |
|
3662 || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" }); |
|
3663 }, inlineCommandActivatedValues: ["superscript"], |
|
3664 }; |
|
3665 |
|
3666 //@} |
|
3667 ///// The underline command ///// |
|
3668 //@{ |
|
3669 commands.underline = { |
|
3670 action: function() { |
|
3671 // "If queryCommandState("underline") returns true, set the selection's |
|
3672 // value to null. Otherwise set the selection's value to "underline". |
|
3673 // Either way, return true." |
|
3674 if (myQueryCommandState("underline")) { |
|
3675 setSelectionValue("underline", null); |
|
3676 } else { |
|
3677 setSelectionValue("underline", "underline"); |
|
3678 } |
|
3679 return true; |
|
3680 }, inlineCommandActivatedValues: ["underline"] |
|
3681 }; |
|
3682 |
|
3683 //@} |
|
3684 ///// The unlink command ///// |
|
3685 //@{ |
|
3686 commands.unlink = { |
|
3687 action: function() { |
|
3688 // "Let hyperlinks be a list of every a element that has an href |
|
3689 // attribute and is contained in the active range or is an ancestor of |
|
3690 // one of its boundary points." |
|
3691 // |
|
3692 // As usual, take care to ensure it's tree order. The correctness of |
|
3693 // the following is left as an exercise for the reader. |
|
3694 var range = getActiveRange(); |
|
3695 var hyperlinks = []; |
|
3696 for ( |
|
3697 var node = range.startContainer; |
|
3698 node; |
|
3699 node = node.parentNode |
|
3700 ) { |
|
3701 if (isHtmlElement(node, "A") |
|
3702 && node.hasAttribute("href")) { |
|
3703 hyperlinks.unshift(node); |
|
3704 } |
|
3705 } |
|
3706 for ( |
|
3707 var node = range.startContainer; |
|
3708 node != nextNodeDescendants(range.endContainer); |
|
3709 node = nextNode(node) |
|
3710 ) { |
|
3711 if (isHtmlElement(node, "A") |
|
3712 && node.hasAttribute("href") |
|
3713 && (isContained(node, range) |
|
3714 || isAncestor(node, range.endContainer) |
|
3715 || node == range.endContainer)) { |
|
3716 hyperlinks.push(node); |
|
3717 } |
|
3718 } |
|
3719 |
|
3720 // "Clear the value of each member of hyperlinks." |
|
3721 for (var i = 0; i < hyperlinks.length; i++) { |
|
3722 clearValue(hyperlinks[i], "unlink"); |
|
3723 } |
|
3724 |
|
3725 // "Return true." |
|
3726 return true; |
|
3727 } |
|
3728 }; |
|
3729 |
|
3730 //@} |
|
3731 |
|
3732 ///////////////////////////////////// |
|
3733 ///// Block formatting commands ///// |
|
3734 ///////////////////////////////////// |
|
3735 |
|
3736 ///// Block formatting command definitions ///// |
|
3737 //@{ |
|
3738 |
|
3739 // "An indentation element is either a blockquote, or a div that has a style |
|
3740 // attribute that sets "margin" or some subproperty of it." |
|
3741 function isIndentationElement(node) { |
|
3742 if (!isHtmlElement(node)) { |
|
3743 return false; |
|
3744 } |
|
3745 |
|
3746 if (node.tagName == "BLOCKQUOTE") { |
|
3747 return true; |
|
3748 } |
|
3749 |
|
3750 if (node.tagName != "DIV") { |
|
3751 return false; |
|
3752 } |
|
3753 |
|
3754 for (var i = 0; i < node.style.length; i++) { |
|
3755 // Approximate check |
|
3756 if (/^(-[a-z]+-)?margin/.test(node.style[i])) { |
|
3757 return true; |
|
3758 } |
|
3759 } |
|
3760 |
|
3761 return false; |
|
3762 } |
|
3763 |
|
3764 // "A simple indentation element is an indentation element that has no |
|
3765 // attributes except possibly |
|
3766 // |
|
3767 // * "a style attribute that sets no properties other than "margin", |
|
3768 // "border", "padding", or subproperties of those; and/or |
|
3769 // * "a dir attribute." |
|
3770 function isSimpleIndentationElement(node) { |
|
3771 if (!isIndentationElement(node)) { |
|
3772 return false; |
|
3773 } |
|
3774 |
|
3775 for (var i = 0; i < node.attributes.length; i++) { |
|
3776 if (!isHtmlNamespace(node.attributes[i].namespaceURI) |
|
3777 || ["style", "dir"].indexOf(node.attributes[i].name) == -1) { |
|
3778 return false; |
|
3779 } |
|
3780 } |
|
3781 |
|
3782 for (var i = 0; i < node.style.length; i++) { |
|
3783 // This is approximate, but it works well enough for my purposes. |
|
3784 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) { |
|
3785 return false; |
|
3786 } |
|
3787 } |
|
3788 |
|
3789 return true; |
|
3790 } |
|
3791 |
|
3792 // "A non-list single-line container is an HTML element with local name |
|
3793 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", |
|
3794 // or "xmp"." |
|
3795 function isNonListSingleLineContainer(node) { |
|
3796 return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", |
|
3797 "h6", "listing", "p", "pre", "xmp"]); |
|
3798 } |
|
3799 |
|
3800 // "A single-line container is either a non-list single-line container, or an |
|
3801 // HTML element with local name "li", "dt", or "dd"." |
|
3802 function isSingleLineContainer(node) { |
|
3803 return isNonListSingleLineContainer(node) |
|
3804 || isHtmlElement(node, ["li", "dt", "dd"]); |
|
3805 } |
|
3806 |
|
3807 function getBlockNodeOf(node) { |
|
3808 // "While node is an inline node, set node to its parent." |
|
3809 while (isInlineNode(node)) { |
|
3810 node = node.parentNode; |
|
3811 } |
|
3812 |
|
3813 // "Return node." |
|
3814 return node; |
|
3815 } |
|
3816 |
|
3817 //@} |
|
3818 ///// Assorted block formatting command algorithms ///// |
|
3819 //@{ |
|
3820 |
|
3821 function fixDisallowedAncestors(node) { |
|
3822 // "If node is not editable, abort these steps." |
|
3823 if (!isEditable(node)) { |
|
3824 return; |
|
3825 } |
|
3826 |
|
3827 // "If node is not an allowed child of any of its ancestors in the same |
|
3828 // editing host:" |
|
3829 if (getAncestors(node).every(function(ancestor) { |
|
3830 return !inSameEditingHost(node, ancestor) |
|
3831 || !isAllowedChild(node, ancestor) |
|
3832 })) { |
|
3833 // "If node is a dd or dt, wrap the one-node list consisting of node, |
|
3834 // with sibling criteria returning true for any dl with no attributes |
|
3835 // and false otherwise, and new parent instructions returning the |
|
3836 // result of calling createElement("dl") on the context object. Then |
|
3837 // abort these steps." |
|
3838 if (isHtmlElement(node, ["dd", "dt"])) { |
|
3839 wrap([node], |
|
3840 function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length }, |
|
3841 function() { return document.createElement("dl") }); |
|
3842 return; |
|
3843 } |
|
3844 |
|
3845 // "If "p" is not an allowed child of the editing host of node, abort |
|
3846 // these steps." |
|
3847 if (!isAllowedChild("p", getEditingHostOf(node))) { |
|
3848 return; |
|
3849 } |
|
3850 |
|
3851 // "If node is not a prohibited paragraph child, abort these steps." |
|
3852 if (!isProhibitedParagraphChild(node)) { |
|
3853 return; |
|
3854 } |
|
3855 |
|
3856 // "Set the tag name of node to the default single-line container name, |
|
3857 // and let node be the result." |
|
3858 node = setTagName(node, defaultSingleLineContainerName); |
|
3859 |
|
3860 // "Fix disallowed ancestors of node." |
|
3861 fixDisallowedAncestors(node); |
|
3862 |
|
3863 // "Let children be node's children." |
|
3864 var children = [].slice.call(node.childNodes); |
|
3865 |
|
3866 // "For each child in children, if child is a prohibited paragraph |
|
3867 // child:" |
|
3868 children.filter(isProhibitedParagraphChild) |
|
3869 .forEach(function(child) { |
|
3870 // "Record the values of the one-node list consisting of child, and |
|
3871 // let values be the result." |
|
3872 var values = recordValues([child]); |
|
3873 |
|
3874 // "Split the parent of the one-node list consisting of child." |
|
3875 splitParent([child]); |
|
3876 |
|
3877 // "Restore the values from values." |
|
3878 restoreValues(values); |
|
3879 }); |
|
3880 |
|
3881 // "Abort these steps." |
|
3882 return; |
|
3883 } |
|
3884 |
|
3885 // "Record the values of the one-node list consisting of node, and let |
|
3886 // values be the result." |
|
3887 var values = recordValues([node]); |
|
3888 |
|
3889 // "While node is not an allowed child of its parent, split the parent of |
|
3890 // the one-node list consisting of node." |
|
3891 while (!isAllowedChild(node, node.parentNode)) { |
|
3892 splitParent([node]); |
|
3893 } |
|
3894 |
|
3895 // "Restore the values from values." |
|
3896 restoreValues(values); |
|
3897 } |
|
3898 |
|
3899 function normalizeSublists(item) { |
|
3900 // "If item is not an li or it is not editable or its parent is not |
|
3901 // editable, abort these steps." |
|
3902 if (!isHtmlElement(item, "LI") |
|
3903 || !isEditable(item) |
|
3904 || !isEditable(item.parentNode)) { |
|
3905 return; |
|
3906 } |
|
3907 |
|
3908 // "Let new item be null." |
|
3909 var newItem = null; |
|
3910 |
|
3911 // "While item has an ol or ul child:" |
|
3912 while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) { |
|
3913 // "Let child be the last child of item." |
|
3914 var child = item.lastChild; |
|
3915 |
|
3916 // "If child is an ol or ul, or new item is null and child is a Text |
|
3917 // node whose data consists of zero of more space characters:" |
|
3918 if (isHtmlElement(child, ["OL", "UL"]) |
|
3919 || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) { |
|
3920 // "Set new item to null." |
|
3921 newItem = null; |
|
3922 |
|
3923 // "Insert child into the parent of item immediately following |
|
3924 // item, preserving ranges." |
|
3925 movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item)); |
|
3926 |
|
3927 // "Otherwise:" |
|
3928 } else { |
|
3929 // "If new item is null, let new item be the result of calling |
|
3930 // createElement("li") on the ownerDocument of item, then insert |
|
3931 // new item into the parent of item immediately after item." |
|
3932 if (!newItem) { |
|
3933 newItem = item.ownerDocument.createElement("li"); |
|
3934 item.parentNode.insertBefore(newItem, item.nextSibling); |
|
3935 } |
|
3936 |
|
3937 // "Insert child into new item as its first child, preserving |
|
3938 // ranges." |
|
3939 movePreservingRanges(child, newItem, 0); |
|
3940 } |
|
3941 } |
|
3942 } |
|
3943 |
|
3944 function getSelectionListState() { |
|
3945 // "If the active range is null, return "none"." |
|
3946 if (!getActiveRange()) { |
|
3947 return "none"; |
|
3948 } |
|
3949 |
|
3950 // "Block-extend the active range, and let new range be the result." |
|
3951 var newRange = blockExtend(getActiveRange()); |
|
3952 |
|
3953 // "Let node list be a list of nodes, initially empty." |
|
3954 // |
|
3955 // "For each node contained in new range, append node to node list if the |
|
3956 // last member of node list (if any) is not an ancestor of node; node is |
|
3957 // editable; node is not an indentation element; and node is either an ol |
|
3958 // or ul, or the child of an ol or ul, or an allowed child of "li"." |
|
3959 var nodeList = getContainedNodes(newRange, function(node) { |
|
3960 return isEditable(node) |
|
3961 && !isIndentationElement(node) |
|
3962 && (isHtmlElement(node, ["ol", "ul"]) |
|
3963 || isHtmlElement(node.parentNode, ["ol", "ul"]) |
|
3964 || isAllowedChild(node, "li")); |
|
3965 }); |
|
3966 |
|
3967 // "If node list is empty, return "none"." |
|
3968 if (!nodeList.length) { |
|
3969 return "none"; |
|
3970 } |
|
3971 |
|
3972 // "If every member of node list is either an ol or the child of an ol or |
|
3973 // the child of an li child of an ol, and none is a ul or an ancestor of a |
|
3974 // ul, return "ol"." |
|
3975 if (nodeList.every(function(node) { |
|
3976 return isHtmlElement(node, "ol") |
|
3977 || isHtmlElement(node.parentNode, "ol") |
|
3978 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol")); |
|
3979 }) |
|
3980 && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) { |
|
3981 return "ol"; |
|
3982 } |
|
3983 |
|
3984 // "If every member of node list is either a ul or the child of a ul or the |
|
3985 // child of an li child of a ul, and none is an ol or an ancestor of an ol, |
|
3986 // return "ul"." |
|
3987 if (nodeList.every(function(node) { |
|
3988 return isHtmlElement(node, "ul") |
|
3989 || isHtmlElement(node.parentNode, "ul") |
|
3990 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul")); |
|
3991 }) |
|
3992 && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) { |
|
3993 return "ul"; |
|
3994 } |
|
3995 |
|
3996 var hasOl = nodeList.some(function(node) { |
|
3997 return isHtmlElement(node, "ol") |
|
3998 || isHtmlElement(node.parentNode, "ol") |
|
3999 || ("querySelector" in node && node.querySelector("ol")) |
|
4000 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol")); |
|
4001 }); |
|
4002 var hasUl = nodeList.some(function(node) { |
|
4003 return isHtmlElement(node, "ul") |
|
4004 || isHtmlElement(node.parentNode, "ul") |
|
4005 || ("querySelector" in node && node.querySelector("ul")) |
|
4006 || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul")); |
|
4007 }); |
|
4008 // "If some member of node list is either an ol or the child or ancestor of |
|
4009 // an ol or the child of an li child of an ol, and some member of node list |
|
4010 // is either a ul or the child or ancestor of a ul or the child of an li |
|
4011 // child of a ul, return "mixed"." |
|
4012 if (hasOl && hasUl) { |
|
4013 return "mixed"; |
|
4014 } |
|
4015 |
|
4016 // "If some member of node list is either an ol or the child or ancestor of |
|
4017 // an ol or the child of an li child of an ol, return "mixed ol"." |
|
4018 if (hasOl) { |
|
4019 return "mixed ol"; |
|
4020 } |
|
4021 |
|
4022 // "If some member of node list is either a ul or the child or ancestor of |
|
4023 // a ul or the child of an li child of a ul, return "mixed ul"." |
|
4024 if (hasUl) { |
|
4025 return "mixed ul"; |
|
4026 } |
|
4027 |
|
4028 // "Return "none"." |
|
4029 return "none"; |
|
4030 } |
|
4031 |
|
4032 function getAlignmentValue(node) { |
|
4033 // "While node is neither null nor an Element, or it is an Element but its |
|
4034 // "display" property has resolved value "inline" or "none", set node to |
|
4035 // its parent." |
|
4036 while ((node && node.nodeType != Node.ELEMENT_NODE) |
|
4037 || (node.nodeType == Node.ELEMENT_NODE |
|
4038 && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) { |
|
4039 node = node.parentNode; |
|
4040 } |
|
4041 |
|
4042 // "If node is not an Element, return "left"." |
|
4043 if (!node || node.nodeType != Node.ELEMENT_NODE) { |
|
4044 return "left"; |
|
4045 } |
|
4046 |
|
4047 var resolvedValue = getComputedStyle(node).textAlign |
|
4048 // Hack around browser non-standardness |
|
4049 .replace(/^-(moz|webkit)-/, "") |
|
4050 .replace(/^auto$/, "start"); |
|
4051 |
|
4052 // "If node's "text-align" property has resolved value "start", return |
|
4053 // "left" if the directionality of node is "ltr", "right" if it is "rtl"." |
|
4054 if (resolvedValue == "start") { |
|
4055 return getDirectionality(node) == "ltr" ? "left" : "right"; |
|
4056 } |
|
4057 |
|
4058 // "If node's "text-align" property has resolved value "end", return |
|
4059 // "right" if the directionality of node is "ltr", "left" if it is "rtl"." |
|
4060 if (resolvedValue == "end") { |
|
4061 return getDirectionality(node) == "ltr" ? "right" : "left"; |
|
4062 } |
|
4063 |
|
4064 // "If node's "text-align" property has resolved value "center", "justify", |
|
4065 // "left", or "right", return that value." |
|
4066 if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) { |
|
4067 return resolvedValue; |
|
4068 } |
|
4069 |
|
4070 // "Return "left"." |
|
4071 return "left"; |
|
4072 } |
|
4073 |
|
4074 function getNextEquivalentPoint(node, offset) { |
|
4075 // "If node's length is zero, return null." |
|
4076 if (getNodeLength(node) == 0) { |
|
4077 return null; |
|
4078 } |
|
4079 |
|
4080 // "If offset is node's length, and node's parent is not null, and node is |
|
4081 // an inline node, return (node's parent, 1 + node's index)." |
|
4082 if (offset == getNodeLength(node) |
|
4083 && node.parentNode |
|
4084 && isInlineNode(node)) { |
|
4085 return [node.parentNode, 1 + getNodeIndex(node)]; |
|
4086 } |
|
4087 |
|
4088 // "If node has a child with index offset, and that child's length is not |
|
4089 // zero, and that child is an inline node, return (that child, 0)." |
|
4090 if (0 <= offset |
|
4091 && offset < node.childNodes.length |
|
4092 && getNodeLength(node.childNodes[offset]) != 0 |
|
4093 && isInlineNode(node.childNodes[offset])) { |
|
4094 return [node.childNodes[offset], 0]; |
|
4095 } |
|
4096 |
|
4097 // "Return null." |
|
4098 return null; |
|
4099 } |
|
4100 |
|
4101 function getPreviousEquivalentPoint(node, offset) { |
|
4102 // "If node's length is zero, return null." |
|
4103 if (getNodeLength(node) == 0) { |
|
4104 return null; |
|
4105 } |
|
4106 |
|
4107 // "If offset is 0, and node's parent is not null, and node is an inline |
|
4108 // node, return (node's parent, node's index)." |
|
4109 if (offset == 0 |
|
4110 && node.parentNode |
|
4111 && isInlineNode(node)) { |
|
4112 return [node.parentNode, getNodeIndex(node)]; |
|
4113 } |
|
4114 |
|
4115 // "If node has a child with index offset − 1, and that child's length is |
|
4116 // not zero, and that child is an inline node, return (that child, that |
|
4117 // child's length)." |
|
4118 if (0 <= offset - 1 |
|
4119 && offset - 1 < node.childNodes.length |
|
4120 && getNodeLength(node.childNodes[offset - 1]) != 0 |
|
4121 && isInlineNode(node.childNodes[offset - 1])) { |
|
4122 return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])]; |
|
4123 } |
|
4124 |
|
4125 // "Return null." |
|
4126 return null; |
|
4127 } |
|
4128 |
|
4129 function getFirstEquivalentPoint(node, offset) { |
|
4130 // "While (node, offset)'s previous equivalent point is not null, set |
|
4131 // (node, offset) to its previous equivalent point." |
|
4132 var prev; |
|
4133 while (prev = getPreviousEquivalentPoint(node, offset)) { |
|
4134 node = prev[0]; |
|
4135 offset = prev[1]; |
|
4136 } |
|
4137 |
|
4138 // "Return (node, offset)." |
|
4139 return [node, offset]; |
|
4140 } |
|
4141 |
|
4142 function getLastEquivalentPoint(node, offset) { |
|
4143 // "While (node, offset)'s next equivalent point is not null, set (node, |
|
4144 // offset) to its next equivalent point." |
|
4145 var next; |
|
4146 while (next = getNextEquivalentPoint(node, offset)) { |
|
4147 node = next[0]; |
|
4148 offset = next[1]; |
|
4149 } |
|
4150 |
|
4151 // "Return (node, offset)." |
|
4152 return [node, offset]; |
|
4153 } |
|
4154 |
|
4155 //@} |
|
4156 ///// Block-extending a range ///// |
|
4157 //@{ |
|
4158 |
|
4159 // "A boundary point (node, offset) is a block start point if either node's |
|
4160 // parent is null and offset is zero; or node has a child with index offset − |
|
4161 // 1, and that child is either a visible block node or a visible br." |
|
4162 function isBlockStartPoint(node, offset) { |
|
4163 return (!node.parentNode && offset == 0) |
|
4164 || (0 <= offset - 1 |
|
4165 && offset - 1 < node.childNodes.length |
|
4166 && isVisible(node.childNodes[offset - 1]) |
|
4167 && (isBlockNode(node.childNodes[offset - 1]) |
|
4168 || isHtmlElement(node.childNodes[offset - 1], "br"))); |
|
4169 } |
|
4170 |
|
4171 // "A boundary point (node, offset) is a block end point if either node's |
|
4172 // parent is null and offset is node's length; or node has a child with index |
|
4173 // offset, and that child is a visible block node." |
|
4174 function isBlockEndPoint(node, offset) { |
|
4175 return (!node.parentNode && offset == getNodeLength(node)) |
|
4176 || (offset < node.childNodes.length |
|
4177 && isVisible(node.childNodes[offset]) |
|
4178 && isBlockNode(node.childNodes[offset])); |
|
4179 } |
|
4180 |
|
4181 // "A boundary point is a block boundary point if it is either a block start |
|
4182 // point or a block end point." |
|
4183 function isBlockBoundaryPoint(node, offset) { |
|
4184 return isBlockStartPoint(node, offset) |
|
4185 || isBlockEndPoint(node, offset); |
|
4186 } |
|
4187 |
|
4188 function blockExtend(range) { |
|
4189 // "Let start node, start offset, end node, and end offset be the start |
|
4190 // and end nodes and offsets of the range." |
|
4191 var startNode = range.startContainer; |
|
4192 var startOffset = range.startOffset; |
|
4193 var endNode = range.endContainer; |
|
4194 var endOffset = range.endOffset; |
|
4195 |
|
4196 // "If some ancestor container of start node is an li, set start offset to |
|
4197 // the index of the last such li in tree order, and set start node to that |
|
4198 // li's parent." |
|
4199 var liAncestors = getAncestors(startNode).concat(startNode) |
|
4200 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) |
|
4201 .slice(-1); |
|
4202 if (liAncestors.length) { |
|
4203 startOffset = getNodeIndex(liAncestors[0]); |
|
4204 startNode = liAncestors[0].parentNode; |
|
4205 } |
|
4206 |
|
4207 // "If (start node, start offset) is not a block start point, repeat the |
|
4208 // following steps:" |
|
4209 if (!isBlockStartPoint(startNode, startOffset)) do { |
|
4210 // "If start offset is zero, set it to start node's index, then set |
|
4211 // start node to its parent." |
|
4212 if (startOffset == 0) { |
|
4213 startOffset = getNodeIndex(startNode); |
|
4214 startNode = startNode.parentNode; |
|
4215 |
|
4216 // "Otherwise, subtract one from start offset." |
|
4217 } else { |
|
4218 startOffset--; |
|
4219 } |
|
4220 |
|
4221 // "If (start node, start offset) is a block boundary point, break from |
|
4222 // this loop." |
|
4223 } while (!isBlockBoundaryPoint(startNode, startOffset)); |
|
4224 |
|
4225 // "While start offset is zero and start node's parent is not null, set |
|
4226 // start offset to start node's index, then set start node to its parent." |
|
4227 while (startOffset == 0 |
|
4228 && startNode.parentNode) { |
|
4229 startOffset = getNodeIndex(startNode); |
|
4230 startNode = startNode.parentNode; |
|
4231 } |
|
4232 |
|
4233 // "If some ancestor container of end node is an li, set end offset to one |
|
4234 // plus the index of the last such li in tree order, and set end node to |
|
4235 // that li's parent." |
|
4236 var liAncestors = getAncestors(endNode).concat(endNode) |
|
4237 .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) |
|
4238 .slice(-1); |
|
4239 if (liAncestors.length) { |
|
4240 endOffset = 1 + getNodeIndex(liAncestors[0]); |
|
4241 endNode = liAncestors[0].parentNode; |
|
4242 } |
|
4243 |
|
4244 // "If (end node, end offset) is not a block end point, repeat the |
|
4245 // following steps:" |
|
4246 if (!isBlockEndPoint(endNode, endOffset)) do { |
|
4247 // "If end offset is end node's length, set it to one plus end node's |
|
4248 // index, then set end node to its parent." |
|
4249 if (endOffset == getNodeLength(endNode)) { |
|
4250 endOffset = 1 + getNodeIndex(endNode); |
|
4251 endNode = endNode.parentNode; |
|
4252 |
|
4253 // "Otherwise, add one to end offset. |
|
4254 } else { |
|
4255 endOffset++; |
|
4256 } |
|
4257 |
|
4258 // "If (end node, end offset) is a block boundary point, break from |
|
4259 // this loop." |
|
4260 } while (!isBlockBoundaryPoint(endNode, endOffset)); |
|
4261 |
|
4262 // "While end offset is end node's length and end node's parent is not |
|
4263 // null, set end offset to one plus end node's index, then set end node to |
|
4264 // its parent." |
|
4265 while (endOffset == getNodeLength(endNode) |
|
4266 && endNode.parentNode) { |
|
4267 endOffset = 1 + getNodeIndex(endNode); |
|
4268 endNode = endNode.parentNode; |
|
4269 } |
|
4270 |
|
4271 // "Let new range be a new range whose start and end nodes and offsets |
|
4272 // are start node, start offset, end node, and end offset." |
|
4273 var newRange = startNode.ownerDocument.createRange(); |
|
4274 newRange.setStart(startNode, startOffset); |
|
4275 newRange.setEnd(endNode, endOffset); |
|
4276 |
|
4277 // "Return new range." |
|
4278 return newRange; |
|
4279 } |
|
4280 |
|
4281 function followsLineBreak(node) { |
|
4282 // "Let offset be zero." |
|
4283 var offset = 0; |
|
4284 |
|
4285 // "While (node, offset) is not a block boundary point:" |
|
4286 while (!isBlockBoundaryPoint(node, offset)) { |
|
4287 // "If node has a visible child with index offset minus one, return |
|
4288 // false." |
|
4289 if (0 <= offset - 1 |
|
4290 && offset - 1 < node.childNodes.length |
|
4291 && isVisible(node.childNodes[offset - 1])) { |
|
4292 return false; |
|
4293 } |
|
4294 |
|
4295 // "If offset is zero or node has no children, set offset to node's |
|
4296 // index, then set node to its parent." |
|
4297 if (offset == 0 |
|
4298 || !node.hasChildNodes()) { |
|
4299 offset = getNodeIndex(node); |
|
4300 node = node.parentNode; |
|
4301 |
|
4302 // "Otherwise, set node to its child with index offset minus one, then |
|
4303 // set offset to node's length." |
|
4304 } else { |
|
4305 node = node.childNodes[offset - 1]; |
|
4306 offset = getNodeLength(node); |
|
4307 } |
|
4308 } |
|
4309 |
|
4310 // "Return true." |
|
4311 return true; |
|
4312 } |
|
4313 |
|
4314 function precedesLineBreak(node) { |
|
4315 // "Let offset be node's length." |
|
4316 var offset = getNodeLength(node); |
|
4317 |
|
4318 // "While (node, offset) is not a block boundary point:" |
|
4319 while (!isBlockBoundaryPoint(node, offset)) { |
|
4320 // "If node has a visible child with index offset, return false." |
|
4321 if (offset < node.childNodes.length |
|
4322 && isVisible(node.childNodes[offset])) { |
|
4323 return false; |
|
4324 } |
|
4325 |
|
4326 // "If offset is node's length or node has no children, set offset to |
|
4327 // one plus node's index, then set node to its parent." |
|
4328 if (offset == getNodeLength(node) |
|
4329 || !node.hasChildNodes()) { |
|
4330 offset = 1 + getNodeIndex(node); |
|
4331 node = node.parentNode; |
|
4332 |
|
4333 // "Otherwise, set node to its child with index offset and set offset |
|
4334 // to zero." |
|
4335 } else { |
|
4336 node = node.childNodes[offset]; |
|
4337 offset = 0; |
|
4338 } |
|
4339 } |
|
4340 |
|
4341 // "Return true." |
|
4342 return true; |
|
4343 } |
|
4344 |
|
4345 //@} |
|
4346 ///// Recording and restoring overrides ///// |
|
4347 //@{ |
|
4348 |
|
4349 function recordCurrentOverrides() { |
|
4350 // "Let overrides be a list of (string, string or boolean) ordered pairs, |
|
4351 // initially empty." |
|
4352 var overrides = []; |
|
4353 |
|
4354 // "If there is a value override for "createLink", add ("createLink", value |
|
4355 // override for "createLink") to overrides." |
|
4356 if (getValueOverride("createlink") !== undefined) { |
|
4357 overrides.push(["createlink", getValueOverride("createlink")]); |
|
4358 } |
|
4359 |
|
4360 // "For each command in the list "bold", "italic", "strikethrough", |
|
4361 // "subscript", "superscript", "underline", in order: if there is a state |
|
4362 // override for command, add (command, command's state override) to |
|
4363 // overrides." |
|
4364 ["bold", "italic", "strikethrough", "subscript", "superscript", |
|
4365 "underline"].forEach(function(command) { |
|
4366 if (getStateOverride(command) !== undefined) { |
|
4367 overrides.push([command, getStateOverride(command)]); |
|
4368 } |
|
4369 }); |
|
4370 |
|
4371 // "For each command in the list "fontName", "fontSize", "foreColor", |
|
4372 // "hiliteColor", in order: if there is a value override for command, add |
|
4373 // (command, command's value override) to overrides." |
|
4374 ["fontname", "fontsize", "forecolor", |
|
4375 "hilitecolor"].forEach(function(command) { |
|
4376 if (getValueOverride(command) !== undefined) { |
|
4377 overrides.push([command, getValueOverride(command)]); |
|
4378 } |
|
4379 }); |
|
4380 |
|
4381 // "Return overrides." |
|
4382 return overrides; |
|
4383 } |
|
4384 |
|
4385 function recordCurrentStatesAndValues() { |
|
4386 // "Let overrides be a list of (string, string or boolean) ordered pairs, |
|
4387 // initially empty." |
|
4388 var overrides = []; |
|
4389 |
|
4390 // "Let node be the first formattable node effectively contained in the |
|
4391 // active range, or null if there is none." |
|
4392 var node = getAllEffectivelyContainedNodes(getActiveRange()) |
|
4393 .filter(isFormattableNode)[0]; |
|
4394 |
|
4395 // "If node is null, return overrides." |
|
4396 if (!node) { |
|
4397 return overrides; |
|
4398 } |
|
4399 |
|
4400 // "Add ("createLink", node's effective command value for "createLink") to |
|
4401 // overrides." |
|
4402 overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]); |
|
4403 |
|
4404 // "For each command in the list "bold", "italic", "strikethrough", |
|
4405 // "subscript", "superscript", "underline", in order: if node's effective |
|
4406 // command value for command is one of its inline command activated values, |
|
4407 // add (command, true) to overrides, and otherwise add (command, false) to |
|
4408 // overrides." |
|
4409 ["bold", "italic", "strikethrough", "subscript", "superscript", |
|
4410 "underline"].forEach(function(command) { |
|
4411 if (commands[command].inlineCommandActivatedValues |
|
4412 .indexOf(getEffectiveCommandValue(node, command)) != -1) { |
|
4413 overrides.push([command, true]); |
|
4414 } else { |
|
4415 overrides.push([command, false]); |
|
4416 } |
|
4417 }); |
|
4418 |
|
4419 // "For each command in the list "fontName", "foreColor", "hiliteColor", in |
|
4420 // order: add (command, command's value) to overrides." |
|
4421 ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) { |
|
4422 overrides.push([command, commands[command].value()]); |
|
4423 }); |
|
4424 |
|
4425 // "Add ("fontSize", node's effective command value for "fontSize") to |
|
4426 // overrides." |
|
4427 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]); |
|
4428 |
|
4429 // "Return overrides." |
|
4430 return overrides; |
|
4431 } |
|
4432 |
|
4433 function restoreStatesAndValues(overrides) { |
|
4434 // "Let node be the first formattable node effectively contained in the |
|
4435 // active range, or null if there is none." |
|
4436 var node = getAllEffectivelyContainedNodes(getActiveRange()) |
|
4437 .filter(isFormattableNode)[0]; |
|
4438 |
|
4439 // "If node is not null, then for each (command, override) pair in |
|
4440 // overrides, in order:" |
|
4441 if (node) { |
|
4442 for (var i = 0; i < overrides.length; i++) { |
|
4443 var command = overrides[i][0]; |
|
4444 var override = overrides[i][1]; |
|
4445 |
|
4446 // "If override is a boolean, and queryCommandState(command) |
|
4447 // returns something different from override, take the action for |
|
4448 // command, with value equal to the empty string." |
|
4449 if (typeof override == "boolean" |
|
4450 && myQueryCommandState(command) != override) { |
|
4451 commands[command].action(""); |
|
4452 |
|
4453 // "Otherwise, if override is a string, and command is neither |
|
4454 // "createLink" nor "fontSize", and queryCommandValue(command) |
|
4455 // returns something not equivalent to override, take the action |
|
4456 // for command, with value equal to override." |
|
4457 } else if (typeof override == "string" |
|
4458 && command != "createlink" |
|
4459 && command != "fontsize" |
|
4460 && !areEquivalentValues(command, myQueryCommandValue(command), override)) { |
|
4461 commands[command].action(override); |
|
4462 |
|
4463 // "Otherwise, if override is a string; and command is |
|
4464 // "createLink"; and either there is a value override for |
|
4465 // "createLink" that is not equal to override, or there is no value |
|
4466 // override for "createLink" and node's effective command value for |
|
4467 // "createLink" is not equal to override: take the action for |
|
4468 // "createLink", with value equal to override." |
|
4469 } else if (typeof override == "string" |
|
4470 && command == "createlink" |
|
4471 && ( |
|
4472 ( |
|
4473 getValueOverride("createlink") !== undefined |
|
4474 && getValueOverride("createlink") !== override |
|
4475 ) || ( |
|
4476 getValueOverride("createlink") === undefined |
|
4477 && getEffectiveCommandValue(node, "createlink") !== override |
|
4478 ) |
|
4479 )) { |
|
4480 commands.createlink.action(override); |
|
4481 |
|
4482 // "Otherwise, if override is a string; and command is "fontSize"; |
|
4483 // and either there is a value override for "fontSize" that is not |
|
4484 // equal to override, or there is no value override for "fontSize" |
|
4485 // and node's effective command value for "fontSize" is not loosely |
|
4486 // equivalent to override:" |
|
4487 } else if (typeof override == "string" |
|
4488 && command == "fontsize" |
|
4489 && ( |
|
4490 ( |
|
4491 getValueOverride("fontsize") !== undefined |
|
4492 && getValueOverride("fontsize") !== override |
|
4493 ) || ( |
|
4494 getValueOverride("fontsize") === undefined |
|
4495 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override) |
|
4496 ) |
|
4497 )) { |
|
4498 // "Convert override to an integer number of pixels, and set |
|
4499 // override to the legacy font size for the result." |
|
4500 override = getLegacyFontSize(override); |
|
4501 |
|
4502 // "Take the action for "fontSize", with value equal to |
|
4503 // override." |
|
4504 commands.fontsize.action(override); |
|
4505 |
|
4506 // "Otherwise, continue this loop from the beginning." |
|
4507 } else { |
|
4508 continue; |
|
4509 } |
|
4510 |
|
4511 // "Set node to the first formattable node effectively contained in |
|
4512 // the active range, if there is one." |
|
4513 node = getAllEffectivelyContainedNodes(getActiveRange()) |
|
4514 .filter(isFormattableNode)[0] |
|
4515 || node; |
|
4516 } |
|
4517 |
|
4518 // "Otherwise, for each (command, override) pair in overrides, in order:" |
|
4519 } else { |
|
4520 for (var i = 0; i < overrides.length; i++) { |
|
4521 var command = overrides[i][0]; |
|
4522 var override = overrides[i][1]; |
|
4523 |
|
4524 // "If override is a boolean, set the state override for command to |
|
4525 // override." |
|
4526 if (typeof override == "boolean") { |
|
4527 setStateOverride(command, override); |
|
4528 } |
|
4529 |
|
4530 // "If override is a string, set the value override for command to |
|
4531 // override." |
|
4532 if (typeof override == "string") { |
|
4533 setValueOverride(command, override); |
|
4534 } |
|
4535 } |
|
4536 } |
|
4537 } |
|
4538 |
|
4539 //@} |
|
4540 ///// Deleting the selection ///// |
|
4541 //@{ |
|
4542 |
|
4543 // The flags argument is a dictionary that can have blockMerging, |
|
4544 // stripWrappers, and/or direction as keys. |
|
4545 function deleteSelection(flags) { |
|
4546 if (flags === undefined) { |
|
4547 flags = {}; |
|
4548 } |
|
4549 |
|
4550 var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true; |
|
4551 var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true; |
|
4552 var direction = "direction" in flags ? flags.direction : "forward"; |
|
4553 |
|
4554 // "If the active range is null, abort these steps and do nothing." |
|
4555 if (!getActiveRange()) { |
|
4556 return; |
|
4557 } |
|
4558 |
|
4559 // "Canonicalize whitespace at the active range's start." |
|
4560 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); |
|
4561 |
|
4562 // "Canonicalize whitespace at the active range's end." |
|
4563 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset); |
|
4564 |
|
4565 // "Let (start node, start offset) be the last equivalent point for the |
|
4566 // active range's start." |
|
4567 var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset); |
|
4568 var startNode = start[0]; |
|
4569 var startOffset = start[1]; |
|
4570 |
|
4571 // "Let (end node, end offset) be the first equivalent point for the active |
|
4572 // range's end." |
|
4573 var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset); |
|
4574 var endNode = end[0]; |
|
4575 var endOffset = end[1]; |
|
4576 |
|
4577 // "If (end node, end offset) is not after (start node, start offset):" |
|
4578 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") { |
|
4579 // "If direction is "forward", call collapseToStart() on the context |
|
4580 // object's Selection." |
|
4581 // |
|
4582 // Here and in a few other places, we check rangeCount to work around a |
|
4583 // WebKit bug: it will sometimes incorrectly remove ranges from the |
|
4584 // selection if nodes are removed, so collapseToStart() will throw. |
|
4585 // This will break everything if we're using an actual selection, but |
|
4586 // if getActiveRange() is really just returning globalRange and that's |
|
4587 // all we care about, it will work fine. I only add the extra check |
|
4588 // for errors I actually hit in testing. |
|
4589 if (direction == "forward") { |
|
4590 if (getSelection().rangeCount) { |
|
4591 getSelection().collapseToStart(); |
|
4592 } |
|
4593 getActiveRange().collapse(true); |
|
4594 |
|
4595 // "Otherwise, call collapseToEnd() on the context object's Selection." |
|
4596 } else { |
|
4597 getSelection().collapseToEnd(); |
|
4598 getActiveRange().collapse(false); |
|
4599 } |
|
4600 |
|
4601 // "Abort these steps." |
|
4602 return; |
|
4603 } |
|
4604 |
|
4605 // "If start node is a Text node and start offset is 0, set start offset to |
|
4606 // the index of start node, then set start node to its parent." |
|
4607 if (startNode.nodeType == Node.TEXT_NODE |
|
4608 && startOffset == 0) { |
|
4609 startOffset = getNodeIndex(startNode); |
|
4610 startNode = startNode.parentNode; |
|
4611 } |
|
4612 |
|
4613 // "If end node is a Text node and end offset is its length, set end offset |
|
4614 // to one plus the index of end node, then set end node to its parent." |
|
4615 if (endNode.nodeType == Node.TEXT_NODE |
|
4616 && endOffset == getNodeLength(endNode)) { |
|
4617 endOffset = 1 + getNodeIndex(endNode); |
|
4618 endNode = endNode.parentNode; |
|
4619 } |
|
4620 |
|
4621 // "Call collapse(start node, start offset) on the context object's |
|
4622 // Selection." |
|
4623 getSelection().collapse(startNode, startOffset); |
|
4624 getActiveRange().setStart(startNode, startOffset); |
|
4625 |
|
4626 // "Call extend(end node, end offset) on the context object's Selection." |
|
4627 getSelection().extend(endNode, endOffset); |
|
4628 getActiveRange().setEnd(endNode, endOffset); |
|
4629 |
|
4630 // "Let start block be the active range's start node." |
|
4631 var startBlock = getActiveRange().startContainer; |
|
4632 |
|
4633 // "While start block's parent is in the same editing host and start block |
|
4634 // is an inline node, set start block to its parent." |
|
4635 while (inSameEditingHost(startBlock, startBlock.parentNode) |
|
4636 && isInlineNode(startBlock)) { |
|
4637 startBlock = startBlock.parentNode; |
|
4638 } |
|
4639 |
|
4640 // "If start block is neither a block node nor an editing host, or "span" |
|
4641 // is not an allowed child of start block, or start block is a td or th, |
|
4642 // set start block to null." |
|
4643 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock)) |
|
4644 || !isAllowedChild("span", startBlock) |
|
4645 || isHtmlElement(startBlock, ["td", "th"])) { |
|
4646 startBlock = null; |
|
4647 } |
|
4648 |
|
4649 // "Let end block be the active range's end node." |
|
4650 var endBlock = getActiveRange().endContainer; |
|
4651 |
|
4652 // "While end block's parent is in the same editing host and end block is |
|
4653 // an inline node, set end block to its parent." |
|
4654 while (inSameEditingHost(endBlock, endBlock.parentNode) |
|
4655 && isInlineNode(endBlock)) { |
|
4656 endBlock = endBlock.parentNode; |
|
4657 } |
|
4658 |
|
4659 // "If end block is neither a block node nor an editing host, or "span" is |
|
4660 // not an allowed child of end block, or end block is a td or th, set end |
|
4661 // block to null." |
|
4662 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock)) |
|
4663 || !isAllowedChild("span", endBlock) |
|
4664 || isHtmlElement(endBlock, ["td", "th"])) { |
|
4665 endBlock = null; |
|
4666 } |
|
4667 |
|
4668 // "Record current states and values, and let overrides be the result." |
|
4669 var overrides = recordCurrentStatesAndValues(); |
|
4670 |
|
4671 // "If start node and end node are the same, and start node is an editable |
|
4672 // Text node:" |
|
4673 if (startNode == endNode |
|
4674 && isEditable(startNode) |
|
4675 && startNode.nodeType == Node.TEXT_NODE) { |
|
4676 // "Call deleteData(start offset, end offset − start offset) on start |
|
4677 // node." |
|
4678 startNode.deleteData(startOffset, endOffset - startOffset); |
|
4679 |
|
4680 // "Canonicalize whitespace at (start node, start offset), with fix |
|
4681 // collapsed space false." |
|
4682 canonicalizeWhitespace(startNode, startOffset, false); |
|
4683 |
|
4684 // "If direction is "forward", call collapseToStart() on the context |
|
4685 // object's Selection." |
|
4686 if (direction == "forward") { |
|
4687 if (getSelection().rangeCount) { |
|
4688 getSelection().collapseToStart(); |
|
4689 } |
|
4690 getActiveRange().collapse(true); |
|
4691 |
|
4692 // "Otherwise, call collapseToEnd() on the context object's Selection." |
|
4693 } else { |
|
4694 getSelection().collapseToEnd(); |
|
4695 getActiveRange().collapse(false); |
|
4696 } |
|
4697 |
|
4698 // "Restore states and values from overrides." |
|
4699 restoreStatesAndValues(overrides); |
|
4700 |
|
4701 // "Abort these steps." |
|
4702 return; |
|
4703 } |
|
4704 |
|
4705 // "If start node is an editable Text node, call deleteData() on it, with |
|
4706 // start offset as the first argument and (length of start node − start |
|
4707 // offset) as the second argument." |
|
4708 if (isEditable(startNode) |
|
4709 && startNode.nodeType == Node.TEXT_NODE) { |
|
4710 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset); |
|
4711 } |
|
4712 |
|
4713 // "Let node list be a list of nodes, initially empty." |
|
4714 // |
|
4715 // "For each node contained in the active range, append node to node list |
|
4716 // if the last member of node list (if any) is not an ancestor of node; |
|
4717 // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td." |
|
4718 var nodeList = getContainedNodes(getActiveRange(), |
|
4719 function(node) { |
|
4720 return isEditable(node) |
|
4721 && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]); |
|
4722 } |
|
4723 ); |
|
4724 |
|
4725 // "For each node in node list:" |
|
4726 for (var i = 0; i < nodeList.length; i++) { |
|
4727 var node = nodeList[i]; |
|
4728 |
|
4729 // "Let parent be the parent of node." |
|
4730 var parent_ = node.parentNode; |
|
4731 |
|
4732 // "Remove node from parent." |
|
4733 parent_.removeChild(node); |
|
4734 |
|
4735 // "If the block node of parent has no visible children, and parent is |
|
4736 // editable or an editing host, call createElement("br") on the context |
|
4737 // object and append the result as the last child of parent." |
|
4738 if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible) |
|
4739 && (isEditable(parent_) || isEditingHost(parent_))) { |
|
4740 parent_.appendChild(document.createElement("br")); |
|
4741 } |
|
4742 |
|
4743 // "If strip wrappers is true or parent is not an ancestor container of |
|
4744 // start node, while parent is an editable inline node with length 0, |
|
4745 // let grandparent be the parent of parent, then remove parent from |
|
4746 // grandparent, then set parent to grandparent." |
|
4747 if (stripWrappers |
|
4748 || (!isAncestor(parent_, startNode) && parent_ != startNode)) { |
|
4749 while (isEditable(parent_) |
|
4750 && isInlineNode(parent_) |
|
4751 && getNodeLength(parent_) == 0) { |
|
4752 var grandparent = parent_.parentNode; |
|
4753 grandparent.removeChild(parent_); |
|
4754 parent_ = grandparent; |
|
4755 } |
|
4756 } |
|
4757 } |
|
4758 |
|
4759 // "If end node is an editable Text node, call deleteData(0, end offset) on |
|
4760 // it." |
|
4761 if (isEditable(endNode) |
|
4762 && endNode.nodeType == Node.TEXT_NODE) { |
|
4763 endNode.deleteData(0, endOffset); |
|
4764 } |
|
4765 |
|
4766 // "Canonicalize whitespace at the active range's start, with fix collapsed |
|
4767 // space false." |
|
4768 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false); |
|
4769 |
|
4770 // "Canonicalize whitespace at the active range's end, with fix collapsed |
|
4771 // space false." |
|
4772 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false); |
|
4773 |
|
4774 // "If block merging is false, or start block or end block is null, or |
|
4775 // start block is not in the same editing host as end block, or start block |
|
4776 // and end block are the same:" |
|
4777 if (!blockMerging |
|
4778 || !startBlock |
|
4779 || !endBlock |
|
4780 || !inSameEditingHost(startBlock, endBlock) |
|
4781 || startBlock == endBlock) { |
|
4782 // "If direction is "forward", call collapseToStart() on the context |
|
4783 // object's Selection." |
|
4784 if (direction == "forward") { |
|
4785 if (getSelection().rangeCount) { |
|
4786 getSelection().collapseToStart(); |
|
4787 } |
|
4788 getActiveRange().collapse(true); |
|
4789 |
|
4790 // "Otherwise, call collapseToEnd() on the context object's Selection." |
|
4791 } else { |
|
4792 if (getSelection().rangeCount) { |
|
4793 getSelection().collapseToEnd(); |
|
4794 } |
|
4795 getActiveRange().collapse(false); |
|
4796 } |
|
4797 |
|
4798 // "Restore states and values from overrides." |
|
4799 restoreStatesAndValues(overrides); |
|
4800 |
|
4801 // "Abort these steps." |
|
4802 return; |
|
4803 } |
|
4804 |
|
4805 // "If start block has one child, which is a collapsed block prop, remove |
|
4806 // its child from it." |
|
4807 if (startBlock.children.length == 1 |
|
4808 && isCollapsedBlockProp(startBlock.firstChild)) { |
|
4809 startBlock.removeChild(startBlock.firstChild); |
|
4810 } |
|
4811 |
|
4812 // "If start block is an ancestor of end block:" |
|
4813 if (isAncestor(startBlock, endBlock)) { |
|
4814 // "Let reference node be end block." |
|
4815 var referenceNode = endBlock; |
|
4816 |
|
4817 // "While reference node is not a child of start block, set reference |
|
4818 // node to its parent." |
|
4819 while (referenceNode.parentNode != startBlock) { |
|
4820 referenceNode = referenceNode.parentNode; |
|
4821 } |
|
4822 |
|
4823 // "Call collapse() on the context object's Selection, with first |
|
4824 // argument start block and second argument the index of reference |
|
4825 // node." |
|
4826 getSelection().collapse(startBlock, getNodeIndex(referenceNode)); |
|
4827 getActiveRange().setStart(startBlock, getNodeIndex(referenceNode)); |
|
4828 getActiveRange().collapse(true); |
|
4829 |
|
4830 // "If end block has no children:" |
|
4831 if (!endBlock.hasChildNodes()) { |
|
4832 // "While end block is editable and is the only child of its parent |
|
4833 // and is not a child of start block, let parent equal end block, |
|
4834 // then remove end block from parent, then set end block to |
|
4835 // parent." |
|
4836 while (isEditable(endBlock) |
|
4837 && endBlock.parentNode.childNodes.length == 1 |
|
4838 && endBlock.parentNode != startBlock) { |
|
4839 var parent_ = endBlock; |
|
4840 parent_.removeChild(endBlock); |
|
4841 endBlock = parent_; |
|
4842 } |
|
4843 |
|
4844 // "If end block is editable and is not an inline node, and its |
|
4845 // previousSibling and nextSibling are both inline nodes, call |
|
4846 // createElement("br") on the context object and insert it into end |
|
4847 // block's parent immediately after end block." |
|
4848 if (isEditable(endBlock) |
|
4849 && !isInlineNode(endBlock) |
|
4850 && isInlineNode(endBlock.previousSibling) |
|
4851 && isInlineNode(endBlock.nextSibling)) { |
|
4852 endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling); |
|
4853 } |
|
4854 |
|
4855 // "If end block is editable, remove it from its parent." |
|
4856 if (isEditable(endBlock)) { |
|
4857 endBlock.parentNode.removeChild(endBlock); |
|
4858 } |
|
4859 |
|
4860 // "Restore states and values from overrides." |
|
4861 restoreStatesAndValues(overrides); |
|
4862 |
|
4863 // "Abort these steps." |
|
4864 return; |
|
4865 } |
|
4866 |
|
4867 // "If end block's firstChild is not an inline node, restore states and |
|
4868 // values from overrides, then abort these steps." |
|
4869 if (!isInlineNode(endBlock.firstChild)) { |
|
4870 restoreStatesAndValues(overrides); |
|
4871 return; |
|
4872 } |
|
4873 |
|
4874 // "Let children be a list of nodes, initially empty." |
|
4875 var children = []; |
|
4876 |
|
4877 // "Append the first child of end block to children." |
|
4878 children.push(endBlock.firstChild); |
|
4879 |
|
4880 // "While children's last member is not a br, and children's last |
|
4881 // member's nextSibling is an inline node, append children's last |
|
4882 // member's nextSibling to children." |
|
4883 while (!isHtmlElement(children[children.length - 1], "br") |
|
4884 && isInlineNode(children[children.length - 1].nextSibling)) { |
|
4885 children.push(children[children.length - 1].nextSibling); |
|
4886 } |
|
4887 |
|
4888 // "Record the values of children, and let values be the result." |
|
4889 var values = recordValues(children); |
|
4890 |
|
4891 // "While children's first member's parent is not start block, split |
|
4892 // the parent of children." |
|
4893 while (children[0].parentNode != startBlock) { |
|
4894 splitParent(children); |
|
4895 } |
|
4896 |
|
4897 // "If children's first member's previousSibling is an editable br, |
|
4898 // remove that br from its parent." |
|
4899 if (isEditable(children[0].previousSibling) |
|
4900 && isHtmlElement(children[0].previousSibling, "br")) { |
|
4901 children[0].parentNode.removeChild(children[0].previousSibling); |
|
4902 } |
|
4903 |
|
4904 // "Otherwise, if start block is a descendant of end block:" |
|
4905 } else if (isDescendant(startBlock, endBlock)) { |
|
4906 // "Call collapse() on the context object's Selection, with first |
|
4907 // argument start block and second argument start block's length." |
|
4908 getSelection().collapse(startBlock, getNodeLength(startBlock)); |
|
4909 getActiveRange().setStart(startBlock, getNodeLength(startBlock)); |
|
4910 getActiveRange().collapse(true); |
|
4911 |
|
4912 // "Let reference node be start block." |
|
4913 var referenceNode = startBlock; |
|
4914 |
|
4915 // "While reference node is not a child of end block, set reference |
|
4916 // node to its parent." |
|
4917 while (referenceNode.parentNode != endBlock) { |
|
4918 referenceNode = referenceNode.parentNode; |
|
4919 } |
|
4920 |
|
4921 // "If reference node's nextSibling is an inline node and start block's |
|
4922 // lastChild is a br, remove start block's lastChild from it." |
|
4923 if (isInlineNode(referenceNode.nextSibling) |
|
4924 && isHtmlElement(startBlock.lastChild, "br")) { |
|
4925 startBlock.removeChild(startBlock.lastChild); |
|
4926 } |
|
4927 |
|
4928 // "Let nodes to move be a list of nodes, initially empty." |
|
4929 var nodesToMove = []; |
|
4930 |
|
4931 // "If reference node's nextSibling is neither null nor a block node, |
|
4932 // append it to nodes to move." |
|
4933 if (referenceNode.nextSibling |
|
4934 && !isBlockNode(referenceNode.nextSibling)) { |
|
4935 nodesToMove.push(referenceNode.nextSibling); |
|
4936 } |
|
4937 |
|
4938 // "While nodes to move is nonempty and its last member isn't a br and |
|
4939 // its last member's nextSibling is neither null nor a block node, |
|
4940 // append its last member's nextSibling to nodes to move." |
|
4941 if (nodesToMove.length |
|
4942 && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br") |
|
4943 && nodesToMove[nodesToMove.length - 1].nextSibling |
|
4944 && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) { |
|
4945 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); |
|
4946 } |
|
4947 |
|
4948 // "Record the values of nodes to move, and let values be the result." |
|
4949 var values = recordValues(nodesToMove); |
|
4950 |
|
4951 // "For each node in nodes to move, append node as the last child of |
|
4952 // start block, preserving ranges." |
|
4953 nodesToMove.forEach(function(node) { |
|
4954 movePreservingRanges(node, startBlock, -1); |
|
4955 }); |
|
4956 |
|
4957 // "Otherwise:" |
|
4958 } else { |
|
4959 // "Call collapse() on the context object's Selection, with first |
|
4960 // argument start block and second argument start block's length." |
|
4961 getSelection().collapse(startBlock, getNodeLength(startBlock)); |
|
4962 getActiveRange().setStart(startBlock, getNodeLength(startBlock)); |
|
4963 getActiveRange().collapse(true); |
|
4964 |
|
4965 // "If end block's firstChild is an inline node and start block's |
|
4966 // lastChild is a br, remove start block's lastChild from it." |
|
4967 if (isInlineNode(endBlock.firstChild) |
|
4968 && isHtmlElement(startBlock.lastChild, "br")) { |
|
4969 startBlock.removeChild(startBlock.lastChild); |
|
4970 } |
|
4971 |
|
4972 // "Record the values of end block's children, and let values be the |
|
4973 // result." |
|
4974 var values = recordValues([].slice.call(endBlock.childNodes)); |
|
4975 |
|
4976 // "While end block has children, append the first child of end block |
|
4977 // to start block, preserving ranges." |
|
4978 while (endBlock.hasChildNodes()) { |
|
4979 movePreservingRanges(endBlock.firstChild, startBlock, -1); |
|
4980 } |
|
4981 |
|
4982 // "While end block has no children, let parent be the parent of end |
|
4983 // block, then remove end block from parent, then set end block to |
|
4984 // parent." |
|
4985 while (!endBlock.hasChildNodes()) { |
|
4986 var parent_ = endBlock.parentNode; |
|
4987 parent_.removeChild(endBlock); |
|
4988 endBlock = parent_; |
|
4989 } |
|
4990 } |
|
4991 |
|
4992 // "Let ancestor be start block." |
|
4993 var ancestor = startBlock; |
|
4994 |
|
4995 // "While ancestor has an inclusive ancestor ol in the same editing host |
|
4996 // whose nextSibling is also an ol in the same editing host, or an |
|
4997 // inclusive ancestor ul in the same editing host whose nextSibling is also |
|
4998 // a ul in the same editing host:" |
|
4999 while (getInclusiveAncestors(ancestor).some(function(node) { |
|
5000 return inSameEditingHost(ancestor, node) |
|
5001 && ( |
|
5002 (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol")) |
|
5003 || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul")) |
|
5004 ) && inSameEditingHost(ancestor, node.nextSibling); |
|
5005 })) { |
|
5006 // "While ancestor and its nextSibling are not both ols in the same |
|
5007 // editing host, and are also not both uls in the same editing host, |
|
5008 // set ancestor to its parent." |
|
5009 while (!( |
|
5010 isHtmlElement(ancestor, "ol") |
|
5011 && isHtmlElement(ancestor.nextSibling, "ol") |
|
5012 && inSameEditingHost(ancestor, ancestor.nextSibling) |
|
5013 ) && !( |
|
5014 isHtmlElement(ancestor, "ul") |
|
5015 && isHtmlElement(ancestor.nextSibling, "ul") |
|
5016 && inSameEditingHost(ancestor, ancestor.nextSibling) |
|
5017 )) { |
|
5018 ancestor = ancestor.parentNode; |
|
5019 } |
|
5020 |
|
5021 // "While ancestor's nextSibling has children, append ancestor's |
|
5022 // nextSibling's firstChild as the last child of ancestor, preserving |
|
5023 // ranges." |
|
5024 while (ancestor.nextSibling.hasChildNodes()) { |
|
5025 movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1); |
|
5026 } |
|
5027 |
|
5028 // "Remove ancestor's nextSibling from its parent." |
|
5029 ancestor.parentNode.removeChild(ancestor.nextSibling); |
|
5030 } |
|
5031 |
|
5032 // "Restore the values from values." |
|
5033 restoreValues(values); |
|
5034 |
|
5035 // "If start block has no children, call createElement("br") on the context |
|
5036 // object and append the result as the last child of start block." |
|
5037 if (!startBlock.hasChildNodes()) { |
|
5038 startBlock.appendChild(document.createElement("br")); |
|
5039 } |
|
5040 |
|
5041 // "Remove extraneous line breaks at the end of start block." |
|
5042 removeExtraneousLineBreaksAtTheEndOf(startBlock); |
|
5043 |
|
5044 // "Restore states and values from overrides." |
|
5045 restoreStatesAndValues(overrides); |
|
5046 } |
|
5047 |
|
5048 |
|
5049 //@} |
|
5050 ///// Splitting a node list's parent ///// |
|
5051 //@{ |
|
5052 |
|
5053 function splitParent(nodeList) { |
|
5054 // "Let original parent be the parent of the first member of node list." |
|
5055 var originalParent = nodeList[0].parentNode; |
|
5056 |
|
5057 // "If original parent is not editable or its parent is null, do nothing |
|
5058 // and abort these steps." |
|
5059 if (!isEditable(originalParent) |
|
5060 || !originalParent.parentNode) { |
|
5061 return; |
|
5062 } |
|
5063 |
|
5064 // "If the first child of original parent is in node list, remove |
|
5065 // extraneous line breaks before original parent." |
|
5066 if (nodeList.indexOf(originalParent.firstChild) != -1) { |
|
5067 removeExtraneousLineBreaksBefore(originalParent); |
|
5068 } |
|
5069 |
|
5070 // "If the first child of original parent is in node list, and original |
|
5071 // parent follows a line break, set follows line break to true. Otherwise, |
|
5072 // set follows line break to false." |
|
5073 var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1 |
|
5074 && followsLineBreak(originalParent); |
|
5075 |
|
5076 // "If the last child of original parent is in node list, and original |
|
5077 // parent precedes a line break, set precedes line break to true. |
|
5078 // Otherwise, set precedes line break to false." |
|
5079 var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1 |
|
5080 && precedesLineBreak(originalParent); |
|
5081 |
|
5082 // "If the first child of original parent is not in node list, but its last |
|
5083 // child is:" |
|
5084 if (nodeList.indexOf(originalParent.firstChild) == -1 |
|
5085 && nodeList.indexOf(originalParent.lastChild) != -1) { |
|
5086 // "For each node in node list, in reverse order, insert node into the |
|
5087 // parent of original parent immediately after original parent, |
|
5088 // preserving ranges." |
|
5089 for (var i = nodeList.length - 1; i >= 0; i--) { |
|
5090 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent)); |
|
5091 } |
|
5092 |
|
5093 // "If precedes line break is true, and the last member of node list |
|
5094 // does not precede a line break, call createElement("br") on the |
|
5095 // context object and insert the result immediately after the last |
|
5096 // member of node list." |
|
5097 if (precedesLineBreak_ |
|
5098 && !precedesLineBreak(nodeList[nodeList.length - 1])) { |
|
5099 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); |
|
5100 } |
|
5101 |
|
5102 // "Remove extraneous line breaks at the end of original parent." |
|
5103 removeExtraneousLineBreaksAtTheEndOf(originalParent); |
|
5104 |
|
5105 // "Abort these steps." |
|
5106 return; |
|
5107 } |
|
5108 |
|
5109 // "If the first child of original parent is not in node list:" |
|
5110 if (nodeList.indexOf(originalParent.firstChild) == -1) { |
|
5111 // "Let cloned parent be the result of calling cloneNode(false) on |
|
5112 // original parent." |
|
5113 var clonedParent = originalParent.cloneNode(false); |
|
5114 |
|
5115 // "If original parent has an id attribute, unset it." |
|
5116 originalParent.removeAttribute("id"); |
|
5117 |
|
5118 // "Insert cloned parent into the parent of original parent immediately |
|
5119 // before original parent." |
|
5120 originalParent.parentNode.insertBefore(clonedParent, originalParent); |
|
5121 |
|
5122 // "While the previousSibling of the first member of node list is not |
|
5123 // null, append the first child of original parent as the last child of |
|
5124 // cloned parent, preserving ranges." |
|
5125 while (nodeList[0].previousSibling) { |
|
5126 movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length); |
|
5127 } |
|
5128 } |
|
5129 |
|
5130 // "For each node in node list, insert node into the parent of original |
|
5131 // parent immediately before original parent, preserving ranges." |
|
5132 for (var i = 0; i < nodeList.length; i++) { |
|
5133 movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent)); |
|
5134 } |
|
5135 |
|
5136 // "If follows line break is true, and the first member of node list does |
|
5137 // not follow a line break, call createElement("br") on the context object |
|
5138 // and insert the result immediately before the first member of node list." |
|
5139 if (followsLineBreak_ |
|
5140 && !followsLineBreak(nodeList[0])) { |
|
5141 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]); |
|
5142 } |
|
5143 |
|
5144 // "If the last member of node list is an inline node other than a br, and |
|
5145 // the first child of original parent is a br, and original parent is not |
|
5146 // an inline node, remove the first child of original parent from original |
|
5147 // parent." |
|
5148 if (isInlineNode(nodeList[nodeList.length - 1]) |
|
5149 && !isHtmlElement(nodeList[nodeList.length - 1], "br") |
|
5150 && isHtmlElement(originalParent.firstChild, "br") |
|
5151 && !isInlineNode(originalParent)) { |
|
5152 originalParent.removeChild(originalParent.firstChild); |
|
5153 } |
|
5154 |
|
5155 // "If original parent has no children:" |
|
5156 if (!originalParent.hasChildNodes()) { |
|
5157 // "Remove original parent from its parent." |
|
5158 originalParent.parentNode.removeChild(originalParent); |
|
5159 |
|
5160 // "If precedes line break is true, and the last member of node list |
|
5161 // does not precede a line break, call createElement("br") on the |
|
5162 // context object and insert the result immediately after the last |
|
5163 // member of node list." |
|
5164 if (precedesLineBreak_ |
|
5165 && !precedesLineBreak(nodeList[nodeList.length - 1])) { |
|
5166 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); |
|
5167 } |
|
5168 |
|
5169 // "Otherwise, remove extraneous line breaks before original parent." |
|
5170 } else { |
|
5171 removeExtraneousLineBreaksBefore(originalParent); |
|
5172 } |
|
5173 |
|
5174 // "If node list's last member's nextSibling is null, but its parent is not |
|
5175 // null, remove extraneous line breaks at the end of node list's last |
|
5176 // member's parent." |
|
5177 if (!nodeList[nodeList.length - 1].nextSibling |
|
5178 && nodeList[nodeList.length - 1].parentNode) { |
|
5179 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode); |
|
5180 } |
|
5181 } |
|
5182 |
|
5183 // "To remove a node node while preserving its descendants, split the parent of |
|
5184 // node's children if it has any. If it has no children, instead remove it from |
|
5185 // its parent." |
|
5186 function removePreservingDescendants(node) { |
|
5187 if (node.hasChildNodes()) { |
|
5188 splitParent([].slice.call(node.childNodes)); |
|
5189 } else { |
|
5190 node.parentNode.removeChild(node); |
|
5191 } |
|
5192 } |
|
5193 |
|
5194 |
|
5195 //@} |
|
5196 ///// Canonical space sequences ///// |
|
5197 //@{ |
|
5198 |
|
5199 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) { |
|
5200 // "If n is zero, return the empty string." |
|
5201 if (n == 0) { |
|
5202 return ""; |
|
5203 } |
|
5204 |
|
5205 // "If n is one and both non-breaking start and non-breaking end are false, |
|
5206 // return a single space (U+0020)." |
|
5207 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) { |
|
5208 return " "; |
|
5209 } |
|
5210 |
|
5211 // "If n is one, return a single non-breaking space (U+00A0)." |
|
5212 if (n == 1) { |
|
5213 return "\xa0"; |
|
5214 } |
|
5215 |
|
5216 // "Let buffer be the empty string." |
|
5217 var buffer = ""; |
|
5218 |
|
5219 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020. |
|
5220 // Otherwise, let it be U+0020 U+00A0." |
|
5221 var repeatedPair; |
|
5222 if (nonBreakingStart) { |
|
5223 repeatedPair = "\xa0 "; |
|
5224 } else { |
|
5225 repeatedPair = " \xa0"; |
|
5226 } |
|
5227 |
|
5228 // "While n is greater than three, append repeated pair to buffer and |
|
5229 // subtract two from n." |
|
5230 while (n > 3) { |
|
5231 buffer += repeatedPair; |
|
5232 n -= 2; |
|
5233 } |
|
5234 |
|
5235 // "If n is three, append a three-element string to buffer depending on |
|
5236 // non-breaking start and non-breaking end:" |
|
5237 if (n == 3) { |
|
5238 buffer += |
|
5239 !nonBreakingStart && !nonBreakingEnd ? " \xa0 " |
|
5240 : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 " |
|
5241 : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0" |
|
5242 : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0" |
|
5243 : "impossible"; |
|
5244 |
|
5245 // "Otherwise, append a two-element string to buffer depending on |
|
5246 // non-breaking start and non-breaking end:" |
|
5247 } else { |
|
5248 buffer += |
|
5249 !nonBreakingStart && !nonBreakingEnd ? "\xa0 " |
|
5250 : nonBreakingStart && !nonBreakingEnd ? "\xa0 " |
|
5251 : !nonBreakingStart && nonBreakingEnd ? " \xa0" |
|
5252 : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0" |
|
5253 : "impossible"; |
|
5254 } |
|
5255 |
|
5256 // "Return buffer." |
|
5257 return buffer; |
|
5258 } |
|
5259 |
|
5260 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) { |
|
5261 if (fixCollapsedSpace === undefined) { |
|
5262 // "an optional boolean argument fix collapsed space that defaults to |
|
5263 // true" |
|
5264 fixCollapsedSpace = true; |
|
5265 } |
|
5266 |
|
5267 // "If node is neither editable nor an editing host, abort these steps." |
|
5268 if (!isEditable(node) && !isEditingHost(node)) { |
|
5269 return; |
|
5270 } |
|
5271 |
|
5272 // "Let start node equal node and let start offset equal offset." |
|
5273 var startNode = node; |
|
5274 var startOffset = offset; |
|
5275 |
|
5276 // "Repeat the following steps:" |
|
5277 while (true) { |
|
5278 // "If start node has a child in the same editing host with index start |
|
5279 // offset minus one, set start node to that child, then set start |
|
5280 // offset to start node's length." |
|
5281 if (0 <= startOffset - 1 |
|
5282 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) { |
|
5283 startNode = startNode.childNodes[startOffset - 1]; |
|
5284 startOffset = getNodeLength(startNode); |
|
5285 |
|
5286 // "Otherwise, if start offset is zero and start node does not follow a |
|
5287 // line break and start node's parent is in the same editing host, set |
|
5288 // start offset to start node's index, then set start node to its |
|
5289 // parent." |
|
5290 } else if (startOffset == 0 |
|
5291 && !followsLineBreak(startNode) |
|
5292 && inSameEditingHost(startNode, startNode.parentNode)) { |
|
5293 startOffset = getNodeIndex(startNode); |
|
5294 startNode = startNode.parentNode; |
|
5295 |
|
5296 // "Otherwise, if start node is a Text node and its parent's resolved |
|
5297 // value for "white-space" is neither "pre" nor "pre-wrap" and start |
|
5298 // offset is not zero and the (start offset − 1)st element of start |
|
5299 // node's data is a space (0x0020) or non-breaking space (0x00A0), |
|
5300 // subtract one from start offset." |
|
5301 } else if (startNode.nodeType == Node.TEXT_NODE |
|
5302 && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1 |
|
5303 && startOffset != 0 |
|
5304 && /[ \xa0]/.test(startNode.data[startOffset - 1])) { |
|
5305 startOffset--; |
|
5306 |
|
5307 // "Otherwise, break from this loop." |
|
5308 } else { |
|
5309 break; |
|
5310 } |
|
5311 } |
|
5312 |
|
5313 // "Let end node equal start node and end offset equal start offset." |
|
5314 var endNode = startNode; |
|
5315 var endOffset = startOffset; |
|
5316 |
|
5317 // "Let length equal zero." |
|
5318 var length = 0; |
|
5319 |
|
5320 // "Let collapse spaces be true if start offset is zero and start node |
|
5321 // follows a line break, otherwise false." |
|
5322 var collapseSpaces = startOffset == 0 && followsLineBreak(startNode); |
|
5323 |
|
5324 // "Repeat the following steps:" |
|
5325 while (true) { |
|
5326 // "If end node has a child in the same editing host with index end |
|
5327 // offset, set end node to that child, then set end offset to zero." |
|
5328 if (endOffset < endNode.childNodes.length |
|
5329 && inSameEditingHost(endNode, endNode.childNodes[endOffset])) { |
|
5330 endNode = endNode.childNodes[endOffset]; |
|
5331 endOffset = 0; |
|
5332 |
|
5333 // "Otherwise, if end offset is end node's length and end node does not |
|
5334 // precede a line break and end node's parent is in the same editing |
|
5335 // host, set end offset to one plus end node's index, then set end node |
|
5336 // to its parent." |
|
5337 } else if (endOffset == getNodeLength(endNode) |
|
5338 && !precedesLineBreak(endNode) |
|
5339 && inSameEditingHost(endNode, endNode.parentNode)) { |
|
5340 endOffset = 1 + getNodeIndex(endNode); |
|
5341 endNode = endNode.parentNode; |
|
5342 |
|
5343 // "Otherwise, if end node is a Text node and its parent's resolved |
|
5344 // value for "white-space" is neither "pre" nor "pre-wrap" and end |
|
5345 // offset is not end node's length and the end offsetth element of |
|
5346 // end node's data is a space (0x0020) or non-breaking space (0x00A0):" |
|
5347 } else if (endNode.nodeType == Node.TEXT_NODE |
|
5348 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1 |
|
5349 && endOffset != getNodeLength(endNode) |
|
5350 && /[ \xa0]/.test(endNode.data[endOffset])) { |
|
5351 // "If fix collapsed space is true, and collapse spaces is true, |
|
5352 // and the end offsetth code unit of end node's data is a space |
|
5353 // (0x0020): call deleteData(end offset, 1) on end node, then |
|
5354 // continue this loop from the beginning." |
|
5355 if (fixCollapsedSpace |
|
5356 && collapseSpaces |
|
5357 && " " == endNode.data[endOffset]) { |
|
5358 endNode.deleteData(endOffset, 1); |
|
5359 continue; |
|
5360 } |
|
5361 |
|
5362 // "Set collapse spaces to true if the end offsetth element of end |
|
5363 // node's data is a space (0x0020), false otherwise." |
|
5364 collapseSpaces = " " == endNode.data[endOffset]; |
|
5365 |
|
5366 // "Add one to end offset." |
|
5367 endOffset++; |
|
5368 |
|
5369 // "Add one to length." |
|
5370 length++; |
|
5371 |
|
5372 // "Otherwise, break from this loop." |
|
5373 } else { |
|
5374 break; |
|
5375 } |
|
5376 } |
|
5377 |
|
5378 // "If fix collapsed space is true, then while (start node, start offset) |
|
5379 // is before (end node, end offset):" |
|
5380 if (fixCollapsedSpace) { |
|
5381 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { |
|
5382 // "If end node has a child in the same editing host with index end |
|
5383 // offset − 1, set end node to that child, then set end offset to end |
|
5384 // node's length." |
|
5385 if (0 <= endOffset - 1 |
|
5386 && endOffset - 1 < endNode.childNodes.length |
|
5387 && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) { |
|
5388 endNode = endNode.childNodes[endOffset - 1]; |
|
5389 endOffset = getNodeLength(endNode); |
|
5390 |
|
5391 // "Otherwise, if end offset is zero and end node's parent is in the |
|
5392 // same editing host, set end offset to end node's index, then set end |
|
5393 // node to its parent." |
|
5394 } else if (endOffset == 0 |
|
5395 && inSameEditingHost(endNode, endNode.parentNode)) { |
|
5396 endOffset = getNodeIndex(endNode); |
|
5397 endNode = endNode.parentNode; |
|
5398 |
|
5399 // "Otherwise, if end node is a Text node and its parent's resolved |
|
5400 // value for "white-space" is neither "pre" nor "pre-wrap" and end |
|
5401 // offset is end node's length and the last code unit of end node's |
|
5402 // data is a space (0x0020) and end node precedes a line break:" |
|
5403 } else if (endNode.nodeType == Node.TEXT_NODE |
|
5404 && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1 |
|
5405 && endOffset == getNodeLength(endNode) |
|
5406 && endNode.data[endNode.data.length - 1] == " " |
|
5407 && precedesLineBreak(endNode)) { |
|
5408 // "Subtract one from end offset." |
|
5409 endOffset--; |
|
5410 |
|
5411 // "Subtract one from length." |
|
5412 length--; |
|
5413 |
|
5414 // "Call deleteData(end offset, 1) on end node." |
|
5415 endNode.deleteData(endOffset, 1); |
|
5416 |
|
5417 // "Otherwise, break from this loop." |
|
5418 } else { |
|
5419 break; |
|
5420 } |
|
5421 } |
|
5422 } |
|
5423 |
|
5424 // "Let replacement whitespace be the canonical space sequence of length |
|
5425 // length. non-breaking start is true if start offset is zero and start |
|
5426 // node follows a line break, and false otherwise. non-breaking end is true |
|
5427 // if end offset is end node's length and end node precedes a line break, |
|
5428 // and false otherwise." |
|
5429 var replacementWhitespace = canonicalSpaceSequence(length, |
|
5430 startOffset == 0 && followsLineBreak(startNode), |
|
5431 endOffset == getNodeLength(endNode) && precedesLineBreak(endNode)); |
|
5432 |
|
5433 // "While (start node, start offset) is before (end node, end offset):" |
|
5434 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { |
|
5435 // "If start node has a child with index start offset, set start node |
|
5436 // to that child, then set start offset to zero." |
|
5437 if (startOffset < startNode.childNodes.length) { |
|
5438 startNode = startNode.childNodes[startOffset]; |
|
5439 startOffset = 0; |
|
5440 |
|
5441 // "Otherwise, if start node is not a Text node or if start offset is |
|
5442 // start node's length, set start offset to one plus start node's |
|
5443 // index, then set start node to its parent." |
|
5444 } else if (startNode.nodeType != Node.TEXT_NODE |
|
5445 || startOffset == getNodeLength(startNode)) { |
|
5446 startOffset = 1 + getNodeIndex(startNode); |
|
5447 startNode = startNode.parentNode; |
|
5448 |
|
5449 // "Otherwise:" |
|
5450 } else { |
|
5451 // "Remove the first element from replacement whitespace, and let |
|
5452 // element be that element." |
|
5453 var element = replacementWhitespace[0]; |
|
5454 replacementWhitespace = replacementWhitespace.slice(1); |
|
5455 |
|
5456 // "If element is not the same as the start offsetth element of |
|
5457 // start node's data:" |
|
5458 if (element != startNode.data[startOffset]) { |
|
5459 // "Call insertData(start offset, element) on start node." |
|
5460 startNode.insertData(startOffset, element); |
|
5461 |
|
5462 // "Call deleteData(start offset + 1, 1) on start node." |
|
5463 startNode.deleteData(startOffset + 1, 1); |
|
5464 } |
|
5465 |
|
5466 // "Add one to start offset." |
|
5467 startOffset++; |
|
5468 } |
|
5469 } |
|
5470 } |
|
5471 |
|
5472 |
|
5473 //@} |
|
5474 ///// Indenting and outdenting ///// |
|
5475 //@{ |
|
5476 |
|
5477 function indentNodes(nodeList) { |
|
5478 // "If node list is empty, do nothing and abort these steps." |
|
5479 if (!nodeList.length) { |
|
5480 return; |
|
5481 } |
|
5482 |
|
5483 // "Let first node be the first member of node list." |
|
5484 var firstNode = nodeList[0]; |
|
5485 |
|
5486 // "If first node's parent is an ol or ul:" |
|
5487 if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) { |
|
5488 // "Let tag be the local name of the parent of first node." |
|
5489 var tag = firstNode.parentNode.tagName; |
|
5490 |
|
5491 // "Wrap node list, with sibling criteria returning true for an HTML |
|
5492 // element with local name tag and false otherwise, and new parent |
|
5493 // instructions returning the result of calling createElement(tag) on |
|
5494 // the ownerDocument of first node." |
|
5495 wrap(nodeList, |
|
5496 function(node) { return isHtmlElement(node, tag) }, |
|
5497 function() { return firstNode.ownerDocument.createElement(tag) }); |
|
5498 |
|
5499 // "Abort these steps." |
|
5500 return; |
|
5501 } |
|
5502 |
|
5503 // "Wrap node list, with sibling criteria returning true for a simple |
|
5504 // indentation element and false otherwise, and new parent instructions |
|
5505 // returning the result of calling createElement("blockquote") on the |
|
5506 // ownerDocument of first node. Let new parent be the result." |
|
5507 var newParent = wrap(nodeList, |
|
5508 function(node) { return isSimpleIndentationElement(node) }, |
|
5509 function() { return firstNode.ownerDocument.createElement("blockquote") }); |
|
5510 |
|
5511 // "Fix disallowed ancestors of new parent." |
|
5512 fixDisallowedAncestors(newParent); |
|
5513 } |
|
5514 |
|
5515 function outdentNode(node) { |
|
5516 // "If node is not editable, abort these steps." |
|
5517 if (!isEditable(node)) { |
|
5518 return; |
|
5519 } |
|
5520 |
|
5521 // "If node is a simple indentation element, remove node, preserving its |
|
5522 // descendants. Then abort these steps." |
|
5523 if (isSimpleIndentationElement(node)) { |
|
5524 removePreservingDescendants(node); |
|
5525 return; |
|
5526 } |
|
5527 |
|
5528 // "If node is an indentation element:" |
|
5529 if (isIndentationElement(node)) { |
|
5530 // "Unset the dir attribute of node, if any." |
|
5531 node.removeAttribute("dir"); |
|
5532 |
|
5533 // "Unset the margin, padding, and border CSS properties of node." |
|
5534 node.style.margin = ""; |
|
5535 node.style.padding = ""; |
|
5536 node.style.border = ""; |
|
5537 if (node.getAttribute("style") == "" |
|
5538 // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551 |
|
5539 || node.getAttribute("style") == "border-width: initial; border-color: initial; ") { |
|
5540 node.removeAttribute("style"); |
|
5541 } |
|
5542 |
|
5543 // "Set the tag name of node to "div"." |
|
5544 setTagName(node, "div"); |
|
5545 |
|
5546 // "Abort these steps." |
|
5547 return; |
|
5548 } |
|
5549 |
|
5550 // "Let current ancestor be node's parent." |
|
5551 var currentAncestor = node.parentNode; |
|
5552 |
|
5553 // "Let ancestor list be a list of nodes, initially empty." |
|
5554 var ancestorList = []; |
|
5555 |
|
5556 // "While current ancestor is an editable Element that is neither a simple |
|
5557 // indentation element nor an ol nor a ul, append current ancestor to |
|
5558 // ancestor list and then set current ancestor to its parent." |
|
5559 while (isEditable(currentAncestor) |
|
5560 && currentAncestor.nodeType == Node.ELEMENT_NODE |
|
5561 && !isSimpleIndentationElement(currentAncestor) |
|
5562 && !isHtmlElement(currentAncestor, ["ol", "ul"])) { |
|
5563 ancestorList.push(currentAncestor); |
|
5564 currentAncestor = currentAncestor.parentNode; |
|
5565 } |
|
5566 |
|
5567 // "If current ancestor is not an editable simple indentation element:" |
|
5568 if (!isEditable(currentAncestor) |
|
5569 || !isSimpleIndentationElement(currentAncestor)) { |
|
5570 // "Let current ancestor be node's parent." |
|
5571 currentAncestor = node.parentNode; |
|
5572 |
|
5573 // "Let ancestor list be the empty list." |
|
5574 ancestorList = []; |
|
5575 |
|
5576 // "While current ancestor is an editable Element that is neither an |
|
5577 // indentation element nor an ol nor a ul, append current ancestor to |
|
5578 // ancestor list and then set current ancestor to its parent." |
|
5579 while (isEditable(currentAncestor) |
|
5580 && currentAncestor.nodeType == Node.ELEMENT_NODE |
|
5581 && !isIndentationElement(currentAncestor) |
|
5582 && !isHtmlElement(currentAncestor, ["ol", "ul"])) { |
|
5583 ancestorList.push(currentAncestor); |
|
5584 currentAncestor = currentAncestor.parentNode; |
|
5585 } |
|
5586 } |
|
5587 |
|
5588 // "If node is an ol or ul and current ancestor is not an editable |
|
5589 // indentation element:" |
|
5590 if (isHtmlElement(node, ["OL", "UL"]) |
|
5591 && (!isEditable(currentAncestor) |
|
5592 || !isIndentationElement(currentAncestor))) { |
|
5593 // "Unset the reversed, start, and type attributes of node, if any are |
|
5594 // set." |
|
5595 node.removeAttribute("reversed"); |
|
5596 node.removeAttribute("start"); |
|
5597 node.removeAttribute("type"); |
|
5598 |
|
5599 // "Let children be the children of node." |
|
5600 var children = [].slice.call(node.childNodes); |
|
5601 |
|
5602 // "If node has attributes, and its parent is not an ol or ul, set the |
|
5603 // tag name of node to "div"." |
|
5604 if (node.attributes.length |
|
5605 && !isHtmlElement(node.parentNode, ["OL", "UL"])) { |
|
5606 setTagName(node, "div"); |
|
5607 |
|
5608 // "Otherwise:" |
|
5609 } else { |
|
5610 // "Record the values of node's children, and let values be the |
|
5611 // result." |
|
5612 var values = recordValues([].slice.call(node.childNodes)); |
|
5613 |
|
5614 // "Remove node, preserving its descendants." |
|
5615 removePreservingDescendants(node); |
|
5616 |
|
5617 // "Restore the values from values." |
|
5618 restoreValues(values); |
|
5619 } |
|
5620 |
|
5621 // "Fix disallowed ancestors of each member of children." |
|
5622 for (var i = 0; i < children.length; i++) { |
|
5623 fixDisallowedAncestors(children[i]); |
|
5624 } |
|
5625 |
|
5626 // "Abort these steps." |
|
5627 return; |
|
5628 } |
|
5629 |
|
5630 // "If current ancestor is not an editable indentation element, abort these |
|
5631 // steps." |
|
5632 if (!isEditable(currentAncestor) |
|
5633 || !isIndentationElement(currentAncestor)) { |
|
5634 return; |
|
5635 } |
|
5636 |
|
5637 // "Append current ancestor to ancestor list." |
|
5638 ancestorList.push(currentAncestor); |
|
5639 |
|
5640 // "Let original ancestor be current ancestor." |
|
5641 var originalAncestor = currentAncestor; |
|
5642 |
|
5643 // "While ancestor list is not empty:" |
|
5644 while (ancestorList.length) { |
|
5645 // "Let current ancestor be the last member of ancestor list." |
|
5646 // |
|
5647 // "Remove the last member of ancestor list." |
|
5648 currentAncestor = ancestorList.pop(); |
|
5649 |
|
5650 // "Let target be the child of current ancestor that is equal to either |
|
5651 // node or the last member of ancestor list." |
|
5652 var target = node.parentNode == currentAncestor |
|
5653 ? node |
|
5654 : ancestorList[ancestorList.length - 1]; |
|
5655 |
|
5656 // "If target is an inline node that is not a br, and its nextSibling |
|
5657 // is a br, remove target's nextSibling from its parent." |
|
5658 if (isInlineNode(target) |
|
5659 && !isHtmlElement(target, "BR") |
|
5660 && isHtmlElement(target.nextSibling, "BR")) { |
|
5661 target.parentNode.removeChild(target.nextSibling); |
|
5662 } |
|
5663 |
|
5664 // "Let preceding siblings be the preceding siblings of target, and let |
|
5665 // following siblings be the following siblings of target." |
|
5666 var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target)); |
|
5667 var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target)); |
|
5668 |
|
5669 // "Indent preceding siblings." |
|
5670 indentNodes(precedingSiblings); |
|
5671 |
|
5672 // "Indent following siblings." |
|
5673 indentNodes(followingSiblings); |
|
5674 } |
|
5675 |
|
5676 // "Outdent original ancestor." |
|
5677 outdentNode(originalAncestor); |
|
5678 } |
|
5679 |
|
5680 |
|
5681 //@} |
|
5682 ///// Toggling lists ///// |
|
5683 //@{ |
|
5684 |
|
5685 function toggleLists(tagName) { |
|
5686 // "Let mode be "disable" if the selection's list state is tag name, and |
|
5687 // "enable" otherwise." |
|
5688 var mode = getSelectionListState() == tagName ? "disable" : "enable"; |
|
5689 |
|
5690 var range = getActiveRange(); |
|
5691 tagName = tagName.toUpperCase(); |
|
5692 |
|
5693 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is |
|
5694 // "ol"." |
|
5695 var otherTagName = tagName == "OL" ? "UL" : "OL"; |
|
5696 |
|
5697 // "Let items be a list of all lis that are ancestor containers of the |
|
5698 // range's start and/or end node." |
|
5699 // |
|
5700 // It's annoying to get this in tree order using functional stuff without |
|
5701 // doing getDescendants(document), which is slow, so I do it imperatively. |
|
5702 var items = []; |
|
5703 (function(){ |
|
5704 for ( |
|
5705 var ancestorContainer = range.endContainer; |
|
5706 ancestorContainer != range.commonAncestorContainer; |
|
5707 ancestorContainer = ancestorContainer.parentNode |
|
5708 ) { |
|
5709 if (isHtmlElement(ancestorContainer, "li")) { |
|
5710 items.unshift(ancestorContainer); |
|
5711 } |
|
5712 } |
|
5713 for ( |
|
5714 var ancestorContainer = range.startContainer; |
|
5715 ancestorContainer; |
|
5716 ancestorContainer = ancestorContainer.parentNode |
|
5717 ) { |
|
5718 if (isHtmlElement(ancestorContainer, "li")) { |
|
5719 items.unshift(ancestorContainer); |
|
5720 } |
|
5721 } |
|
5722 })(); |
|
5723 |
|
5724 // "For each item in items, normalize sublists of item." |
|
5725 items.forEach(normalizeSublists); |
|
5726 |
|
5727 // "Block-extend the range, and let new range be the result." |
|
5728 var newRange = blockExtend(range); |
|
5729 |
|
5730 // "If mode is "enable", then let lists to convert consist of every |
|
5731 // editable HTML element with local name other tag name that is contained |
|
5732 // in new range, and for every list in lists to convert:" |
|
5733 if (mode == "enable") { |
|
5734 getAllContainedNodes(newRange, function(node) { |
|
5735 return isEditable(node) |
|
5736 && isHtmlElement(node, otherTagName); |
|
5737 }).forEach(function(list) { |
|
5738 // "If list's previousSibling or nextSibling is an editable HTML |
|
5739 // element with local name tag name:" |
|
5740 if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName)) |
|
5741 || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) { |
|
5742 // "Let children be list's children." |
|
5743 var children = [].slice.call(list.childNodes); |
|
5744 |
|
5745 // "Record the values of children, and let values be the |
|
5746 // result." |
|
5747 var values = recordValues(children); |
|
5748 |
|
5749 // "Split the parent of children." |
|
5750 splitParent(children); |
|
5751 |
|
5752 // "Wrap children, with sibling criteria returning true for an |
|
5753 // HTML element with local name tag name and false otherwise." |
|
5754 wrap(children, function(node) { return isHtmlElement(node, tagName) }); |
|
5755 |
|
5756 // "Restore the values from values." |
|
5757 restoreValues(values); |
|
5758 |
|
5759 // "Otherwise, set the tag name of list to tag name." |
|
5760 } else { |
|
5761 setTagName(list, tagName); |
|
5762 } |
|
5763 }); |
|
5764 } |
|
5765 |
|
5766 // "Let node list be a list of nodes, initially empty." |
|
5767 // |
|
5768 // "For each node node contained in new range, if node is editable; the |
|
5769 // last member of node list (if any) is not an ancestor of node; node |
|
5770 // is not an indentation element; and either node is an ol or ul, or its |
|
5771 // parent is an ol or ul, or it is an allowed child of "li"; then append |
|
5772 // node to node list." |
|
5773 var nodeList = getContainedNodes(newRange, function(node) { |
|
5774 return isEditable(node) |
|
5775 && !isIndentationElement(node) |
|
5776 && (isHtmlElement(node, ["OL", "UL"]) |
|
5777 || isHtmlElement(node.parentNode, ["OL", "UL"]) |
|
5778 || isAllowedChild(node, "li")); |
|
5779 }); |
|
5780 |
|
5781 // "If mode is "enable", remove from node list any ol or ul whose parent is |
|
5782 // not also an ol or ul." |
|
5783 if (mode == "enable") { |
|
5784 nodeList = nodeList.filter(function(node) { |
|
5785 return !isHtmlElement(node, ["ol", "ul"]) |
|
5786 || isHtmlElement(node.parentNode, ["ol", "ul"]); |
|
5787 }); |
|
5788 } |
|
5789 |
|
5790 // "If mode is "disable", then while node list is not empty:" |
|
5791 if (mode == "disable") { |
|
5792 while (nodeList.length) { |
|
5793 // "Let sublist be an empty list of nodes." |
|
5794 var sublist = []; |
|
5795 |
|
5796 // "Remove the first member from node list and append it to |
|
5797 // sublist." |
|
5798 sublist.push(nodeList.shift()); |
|
5799 |
|
5800 // "If the first member of sublist is an HTML element with local |
|
5801 // name tag name, outdent it and continue this loop from the |
|
5802 // beginning." |
|
5803 if (isHtmlElement(sublist[0], tagName)) { |
|
5804 outdentNode(sublist[0]); |
|
5805 continue; |
|
5806 } |
|
5807 |
|
5808 // "While node list is not empty, and the first member of node list |
|
5809 // is the nextSibling of the last member of sublist and is not an |
|
5810 // HTML element with local name tag name, remove the first member |
|
5811 // from node list and append it to sublist." |
|
5812 while (nodeList.length |
|
5813 && nodeList[0] == sublist[sublist.length - 1].nextSibling |
|
5814 && !isHtmlElement(nodeList[0], tagName)) { |
|
5815 sublist.push(nodeList.shift()); |
|
5816 } |
|
5817 |
|
5818 // "Record the values of sublist, and let values be the result." |
|
5819 var values = recordValues(sublist); |
|
5820 |
|
5821 // "Split the parent of sublist." |
|
5822 splitParent(sublist); |
|
5823 |
|
5824 // "Fix disallowed ancestors of each member of sublist." |
|
5825 for (var i = 0; i < sublist.length; i++) { |
|
5826 fixDisallowedAncestors(sublist[i]); |
|
5827 } |
|
5828 |
|
5829 // "Restore the values from values." |
|
5830 restoreValues(values); |
|
5831 } |
|
5832 |
|
5833 // "Otherwise, while node list is not empty:" |
|
5834 } else { |
|
5835 while (nodeList.length) { |
|
5836 // "Let sublist be an empty list of nodes." |
|
5837 var sublist = []; |
|
5838 |
|
5839 // "While either sublist is empty, or node list is not empty and |
|
5840 // its first member is the nextSibling of sublist's last member:" |
|
5841 while (!sublist.length |
|
5842 || (nodeList.length |
|
5843 && nodeList[0] == sublist[sublist.length - 1].nextSibling)) { |
|
5844 // "If node list's first member is a p or div, set the tag name |
|
5845 // of node list's first member to "li", and append the result |
|
5846 // to sublist. Remove the first member from node list." |
|
5847 if (isHtmlElement(nodeList[0], ["p", "div"])) { |
|
5848 sublist.push(setTagName(nodeList[0], "li")); |
|
5849 nodeList.shift(); |
|
5850 |
|
5851 // "Otherwise, if the first member of node list is an li or ol |
|
5852 // or ul, remove it from node list and append it to sublist." |
|
5853 } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) { |
|
5854 sublist.push(nodeList.shift()); |
|
5855 |
|
5856 // "Otherwise:" |
|
5857 } else { |
|
5858 // "Let nodes to wrap be a list of nodes, initially empty." |
|
5859 var nodesToWrap = []; |
|
5860 |
|
5861 // "While nodes to wrap is empty, or node list is not empty |
|
5862 // and its first member is the nextSibling of nodes to |
|
5863 // wrap's last member and the first member of node list is |
|
5864 // an inline node and the last member of nodes to wrap is |
|
5865 // an inline node other than a br, remove the first member |
|
5866 // from node list and append it to nodes to wrap." |
|
5867 while (!nodesToWrap.length |
|
5868 || (nodeList.length |
|
5869 && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling |
|
5870 && isInlineNode(nodeList[0]) |
|
5871 && isInlineNode(nodesToWrap[nodesToWrap.length - 1]) |
|
5872 && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) { |
|
5873 nodesToWrap.push(nodeList.shift()); |
|
5874 } |
|
5875 |
|
5876 // "Wrap nodes to wrap, with new parent instructions |
|
5877 // returning the result of calling createElement("li") on |
|
5878 // the context object. Append the result to sublist." |
|
5879 sublist.push(wrap(nodesToWrap, |
|
5880 undefined, |
|
5881 function() { return document.createElement("li") })); |
|
5882 } |
|
5883 } |
|
5884 |
|
5885 // "If sublist's first member's parent is an HTML element with |
|
5886 // local name tag name, or if every member of sublist is an ol or |
|
5887 // ul, continue this loop from the beginning." |
|
5888 if (isHtmlElement(sublist[0].parentNode, tagName) |
|
5889 || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) { |
|
5890 continue; |
|
5891 } |
|
5892 |
|
5893 // "If sublist's first member's parent is an HTML element with |
|
5894 // local name other tag name:" |
|
5895 if (isHtmlElement(sublist[0].parentNode, otherTagName)) { |
|
5896 // "Record the values of sublist, and let values be the |
|
5897 // result." |
|
5898 var values = recordValues(sublist); |
|
5899 |
|
5900 // "Split the parent of sublist." |
|
5901 splitParent(sublist); |
|
5902 |
|
5903 // "Wrap sublist, with sibling criteria returning true for an |
|
5904 // HTML element with local name tag name and false otherwise, |
|
5905 // and new parent instructions returning the result of calling |
|
5906 // createElement(tag name) on the context object." |
|
5907 wrap(sublist, |
|
5908 function(node) { return isHtmlElement(node, tagName) }, |
|
5909 function() { return document.createElement(tagName) }); |
|
5910 |
|
5911 // "Restore the values from values." |
|
5912 restoreValues(values); |
|
5913 |
|
5914 // "Continue this loop from the beginning." |
|
5915 continue; |
|
5916 } |
|
5917 |
|
5918 // "Wrap sublist, with sibling criteria returning true for an HTML |
|
5919 // element with local name tag name and false otherwise, and new |
|
5920 // parent instructions being the following:" |
|
5921 // . . . |
|
5922 // "Fix disallowed ancestors of the previous step's result." |
|
5923 fixDisallowedAncestors(wrap(sublist, |
|
5924 function(node) { return isHtmlElement(node, tagName) }, |
|
5925 function() { |
|
5926 // "If sublist's first member's parent is not an editable |
|
5927 // simple indentation element, or sublist's first member's |
|
5928 // parent's previousSibling is not an editable HTML element |
|
5929 // with local name tag name, call createElement(tag name) |
|
5930 // on the context object and return the result." |
|
5931 if (!isEditable(sublist[0].parentNode) |
|
5932 || !isSimpleIndentationElement(sublist[0].parentNode) |
|
5933 || !isEditable(sublist[0].parentNode.previousSibling) |
|
5934 || !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) { |
|
5935 return document.createElement(tagName); |
|
5936 } |
|
5937 |
|
5938 // "Let list be sublist's first member's parent's |
|
5939 // previousSibling." |
|
5940 var list = sublist[0].parentNode.previousSibling; |
|
5941 |
|
5942 // "Normalize sublists of list's lastChild." |
|
5943 normalizeSublists(list.lastChild); |
|
5944 |
|
5945 // "If list's lastChild is not an editable HTML element |
|
5946 // with local name tag name, call createElement(tag name) |
|
5947 // on the context object, and append the result as the last |
|
5948 // child of list." |
|
5949 if (!isEditable(list.lastChild) |
|
5950 || !isHtmlElement(list.lastChild, tagName)) { |
|
5951 list.appendChild(document.createElement(tagName)); |
|
5952 } |
|
5953 |
|
5954 // "Return the last child of list." |
|
5955 return list.lastChild; |
|
5956 } |
|
5957 )); |
|
5958 } |
|
5959 } |
|
5960 } |
|
5961 |
|
5962 |
|
5963 //@} |
|
5964 ///// Justifying the selection ///// |
|
5965 //@{ |
|
5966 |
|
5967 function justifySelection(alignment) { |
|
5968 // "Block-extend the active range, and let new range be the result." |
|
5969 var newRange = blockExtend(globalRange); |
|
5970 |
|
5971 // "Let element list be a list of all editable Elements contained in new |
|
5972 // range that either has an attribute in the HTML namespace whose local |
|
5973 // name is "align", or has a style attribute that sets "text-align", or is |
|
5974 // a center." |
|
5975 var elementList = getAllContainedNodes(newRange, function(node) { |
|
5976 return node.nodeType == Node.ELEMENT_NODE |
|
5977 && isEditable(node) |
|
5978 // Ignoring namespaces here |
|
5979 && ( |
|
5980 node.hasAttribute("align") |
|
5981 || node.style.textAlign != "" |
|
5982 || isHtmlElement(node, "center") |
|
5983 ); |
|
5984 }); |
|
5985 |
|
5986 // "For each element in element list:" |
|
5987 for (var i = 0; i < elementList.length; i++) { |
|
5988 var element = elementList[i]; |
|
5989 |
|
5990 // "If element has an attribute in the HTML namespace whose local name |
|
5991 // is "align", remove that attribute." |
|
5992 element.removeAttribute("align"); |
|
5993 |
|
5994 // "Unset the CSS property "text-align" on element, if it's set by a |
|
5995 // style attribute." |
|
5996 element.style.textAlign = ""; |
|
5997 if (element.getAttribute("style") == "") { |
|
5998 element.removeAttribute("style"); |
|
5999 } |
|
6000 |
|
6001 // "If element is a div or span or center with no attributes, remove |
|
6002 // it, preserving its descendants." |
|
6003 if (isHtmlElement(element, ["div", "span", "center"]) |
|
6004 && !element.attributes.length) { |
|
6005 removePreservingDescendants(element); |
|
6006 } |
|
6007 |
|
6008 // "If element is a center with one or more attributes, set the tag |
|
6009 // name of element to "div"." |
|
6010 if (isHtmlElement(element, "center") |
|
6011 && element.attributes.length) { |
|
6012 setTagName(element, "div"); |
|
6013 } |
|
6014 } |
|
6015 |
|
6016 // "Block-extend the active range, and let new range be the result." |
|
6017 newRange = blockExtend(globalRange); |
|
6018 |
|
6019 // "Let node list be a list of nodes, initially empty." |
|
6020 var nodeList = []; |
|
6021 |
|
6022 // "For each node node contained in new range, append node to node list if |
|
6023 // the last member of node list (if any) is not an ancestor of node; node |
|
6024 // is editable; node is an allowed child of "div"; and node's alignment |
|
6025 // value is not alignment." |
|
6026 nodeList = getContainedNodes(newRange, function(node) { |
|
6027 return isEditable(node) |
|
6028 && isAllowedChild(node, "div") |
|
6029 && getAlignmentValue(node) != alignment; |
|
6030 }); |
|
6031 |
|
6032 // "While node list is not empty:" |
|
6033 while (nodeList.length) { |
|
6034 // "Let sublist be a list of nodes, initially empty." |
|
6035 var sublist = []; |
|
6036 |
|
6037 // "Remove the first member of node list and append it to sublist." |
|
6038 sublist.push(nodeList.shift()); |
|
6039 |
|
6040 // "While node list is not empty, and the first member of node list is |
|
6041 // the nextSibling of the last member of sublist, remove the first |
|
6042 // member of node list and append it to sublist." |
|
6043 while (nodeList.length |
|
6044 && nodeList[0] == sublist[sublist.length - 1].nextSibling) { |
|
6045 sublist.push(nodeList.shift()); |
|
6046 } |
|
6047 |
|
6048 // "Wrap sublist. Sibling criteria returns true for any div that has |
|
6049 // one or both of the following two attributes and no other attributes, |
|
6050 // and false otherwise:" |
|
6051 // |
|
6052 // * "An align attribute whose value is an ASCII case-insensitive |
|
6053 // match for alignment. |
|
6054 // * "A style attribute which sets exactly one CSS property |
|
6055 // (including unrecognized or invalid attributes), which is |
|
6056 // "text-align", which is set to alignment. |
|
6057 // |
|
6058 // "New parent instructions are to call createElement("div") on the |
|
6059 // context object, then set its CSS property "text-align" to alignment |
|
6060 // and return the result." |
|
6061 wrap(sublist, |
|
6062 function(node) { |
|
6063 return isHtmlElement(node, "div") |
|
6064 && [].every.call(node.attributes, function(attr) { |
|
6065 return (attr.name == "align" && attr.value.toLowerCase() == alignment) |
|
6066 || (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment); |
|
6067 }); |
|
6068 }, |
|
6069 function() { |
|
6070 var newParent = document.createElement("div"); |
|
6071 newParent.setAttribute("style", "text-align: " + alignment); |
|
6072 return newParent; |
|
6073 } |
|
6074 ); |
|
6075 } |
|
6076 } |
|
6077 |
|
6078 |
|
6079 //@} |
|
6080 ///// Automatic linking ///// |
|
6081 //@{ |
|
6082 // "An autolinkable URL is a string of the following form:" |
|
6083 var autolinkableUrlRegexp = |
|
6084 // "Either a string matching the scheme pattern from RFC 3986 section 3.1 |
|
6085 // followed by the literal string ://, or the literal string mailto:; |
|
6086 // followed by" |
|
6087 // |
|
6088 // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) |
|
6089 "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)" |
|
6090 // "Zero or more characters other than space characters; followed by" |
|
6091 + "[^ \t\n\f\r]*" |
|
6092 // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}." |
|
6093 + "[^!\"'(),\\-.:;<>[\\]`{}]"; |
|
6094 |
|
6095 // "A valid e-mail address is a string that matches the ABNF production 1*( |
|
6096 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC |
|
6097 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5." |
|
6098 // |
|
6099 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / |
|
6100 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" |
|
6101 // |
|
6102 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> |
|
6103 //<let-dig-hyp> ::= <let-dig> | "-" |
|
6104 //<let-dig> ::= <letter> | <digit> |
|
6105 var validEmailRegexp = |
|
6106 "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*"; |
|
6107 |
|
6108 function autolink(node, endOffset) { |
|
6109 // "While (node, end offset)'s previous equivalent point is not null, set |
|
6110 // it to its previous equivalent point." |
|
6111 while (getPreviousEquivalentPoint(node, endOffset)) { |
|
6112 var prev = getPreviousEquivalentPoint(node, endOffset); |
|
6113 node = prev[0]; |
|
6114 endOffset = prev[1]; |
|
6115 } |
|
6116 |
|
6117 // "If node is not a Text node, or has an a ancestor, do nothing and abort |
|
6118 // these steps." |
|
6119 if (node.nodeType != Node.TEXT_NODE |
|
6120 || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) { |
|
6121 return; |
|
6122 } |
|
6123 |
|
6124 // "Let search be the largest substring of node's data whose end is end |
|
6125 // offset and that contains no space characters." |
|
6126 var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0]; |
|
6127 |
|
6128 // "If some substring of search is an autolinkable URL:" |
|
6129 if (new RegExp(autolinkableUrlRegexp).test(search)) { |
|
6130 // "While there is no substring of node's data ending at end offset |
|
6131 // that is an autolinkable URL, decrement end offset." |
|
6132 while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) { |
|
6133 endOffset--; |
|
6134 } |
|
6135 |
|
6136 // "Let start offset be the start index of the longest substring of |
|
6137 // node's data that is an autolinkable URL ending at end offset." |
|
6138 var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index; |
|
6139 |
|
6140 // "Let href be the substring of node's data starting at start offset |
|
6141 // and ending at end offset." |
|
6142 var href = node.substringData(startOffset, endOffset - startOffset); |
|
6143 |
|
6144 // "Otherwise, if some substring of search is a valid e-mail address:" |
|
6145 } else if (new RegExp(validEmailRegexp).test(search)) { |
|
6146 // "While there is no substring of node's data ending at end offset |
|
6147 // that is a valid e-mail address, decrement end offset." |
|
6148 while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) { |
|
6149 endOffset--; |
|
6150 } |
|
6151 |
|
6152 // "Let start offset be the start index of the longest substring of |
|
6153 // node's data that is a valid e-mail address ending at end offset." |
|
6154 var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index; |
|
6155 |
|
6156 // "Let href be "mailto:" concatenated with the substring of node's |
|
6157 // data starting at start offset and ending at end offset." |
|
6158 var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset); |
|
6159 |
|
6160 // "Otherwise, do nothing and abort these steps." |
|
6161 } else { |
|
6162 return; |
|
6163 } |
|
6164 |
|
6165 // "Let original range be the active range." |
|
6166 var originalRange = getActiveRange(); |
|
6167 |
|
6168 // "Create a new range with start (node, start offset) and end (node, end |
|
6169 // offset), and set the context object's selection's range to it." |
|
6170 var newRange = document.createRange(); |
|
6171 newRange.setStart(node, startOffset); |
|
6172 newRange.setEnd(node, endOffset); |
|
6173 getSelection().removeAllRanges(); |
|
6174 getSelection().addRange(newRange); |
|
6175 globalRange = newRange; |
|
6176 |
|
6177 // "Take the action for "createLink", with value equal to href." |
|
6178 commands.createlink.action(href); |
|
6179 |
|
6180 // "Set the context object's selection's range to original range." |
|
6181 getSelection().removeAllRanges(); |
|
6182 getSelection().addRange(originalRange); |
|
6183 globalRange = originalRange; |
|
6184 } |
|
6185 //@} |
|
6186 ///// The delete command ///// |
|
6187 //@{ |
|
6188 commands["delete"] = { |
|
6189 preservesOverrides: true, |
|
6190 action: function() { |
|
6191 // "If the active range is not collapsed, delete the selection and |
|
6192 // return true." |
|
6193 if (!getActiveRange().collapsed) { |
|
6194 deleteSelection(); |
|
6195 return true; |
|
6196 } |
|
6197 |
|
6198 // "Canonicalize whitespace at the active range's start." |
|
6199 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); |
|
6200 |
|
6201 // "Let node and offset be the active range's start node and offset." |
|
6202 var node = getActiveRange().startContainer; |
|
6203 var offset = getActiveRange().startOffset; |
|
6204 |
|
6205 // "Repeat the following steps:" |
|
6206 while (true) { |
|
6207 // "If offset is zero and node's previousSibling is an editable |
|
6208 // invisible node, remove node's previousSibling from its parent." |
|
6209 if (offset == 0 |
|
6210 && isEditable(node.previousSibling) |
|
6211 && isInvisible(node.previousSibling)) { |
|
6212 node.parentNode.removeChild(node.previousSibling); |
|
6213 |
|
6214 // "Otherwise, if node has a child with index offset − 1 and that |
|
6215 // child is an editable invisible node, remove that child from |
|
6216 // node, then subtract one from offset." |
|
6217 } else if (0 <= offset - 1 |
|
6218 && offset - 1 < node.childNodes.length |
|
6219 && isEditable(node.childNodes[offset - 1]) |
|
6220 && isInvisible(node.childNodes[offset - 1])) { |
|
6221 node.removeChild(node.childNodes[offset - 1]); |
|
6222 offset--; |
|
6223 |
|
6224 // "Otherwise, if offset is zero and node is an inline node, or if |
|
6225 // node is an invisible node, set offset to the index of node, then |
|
6226 // set node to its parent." |
|
6227 } else if ((offset == 0 |
|
6228 && isInlineNode(node)) |
|
6229 || isInvisible(node)) { |
|
6230 offset = getNodeIndex(node); |
|
6231 node = node.parentNode; |
|
6232 |
|
6233 // "Otherwise, if node has a child with index offset − 1 and that |
|
6234 // child is an editable a, remove that child from node, preserving |
|
6235 // its descendants. Then return true." |
|
6236 } else if (0 <= offset - 1 |
|
6237 && offset - 1 < node.childNodes.length |
|
6238 && isEditable(node.childNodes[offset - 1]) |
|
6239 && isHtmlElement(node.childNodes[offset - 1], "a")) { |
|
6240 removePreservingDescendants(node.childNodes[offset - 1]); |
|
6241 return true; |
|
6242 |
|
6243 // "Otherwise, if node has a child with index offset − 1 and that |
|
6244 // child is not a block node or a br or an img, set node to that |
|
6245 // child, then set offset to the length of node." |
|
6246 } else if (0 <= offset - 1 |
|
6247 && offset - 1 < node.childNodes.length |
|
6248 && !isBlockNode(node.childNodes[offset - 1]) |
|
6249 && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) { |
|
6250 node = node.childNodes[offset - 1]; |
|
6251 offset = getNodeLength(node); |
|
6252 |
|
6253 // "Otherwise, break from this loop." |
|
6254 } else { |
|
6255 break; |
|
6256 } |
|
6257 } |
|
6258 |
|
6259 // "If node is a Text node and offset is not zero, or if node is a |
|
6260 // block node that has a child with index offset − 1 and that child is |
|
6261 // a br or hr or img:" |
|
6262 if ((node.nodeType == Node.TEXT_NODE |
|
6263 && offset != 0) |
|
6264 || (isBlockNode(node) |
|
6265 && 0 <= offset - 1 |
|
6266 && offset - 1 < node.childNodes.length |
|
6267 && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) { |
|
6268 // "Call collapse(node, offset) on the context object's Selection." |
|
6269 getSelection().collapse(node, offset); |
|
6270 getActiveRange().setEnd(node, offset); |
|
6271 |
|
6272 // "Call extend(node, offset − 1) on the context object's |
|
6273 // Selection." |
|
6274 getSelection().extend(node, offset - 1); |
|
6275 getActiveRange().setStart(node, offset - 1); |
|
6276 |
|
6277 // "Delete the selection." |
|
6278 deleteSelection(); |
|
6279 |
|
6280 // "Return true." |
|
6281 return true; |
|
6282 } |
|
6283 |
|
6284 // "If node is an inline node, return true." |
|
6285 if (isInlineNode(node)) { |
|
6286 return true; |
|
6287 } |
|
6288 |
|
6289 // "If node is an li or dt or dd and is the first child of its parent, |
|
6290 // and offset is zero:" |
|
6291 if (isHtmlElement(node, ["li", "dt", "dd"]) |
|
6292 && node == node.parentNode.firstChild |
|
6293 && offset == 0) { |
|
6294 // "Let items be a list of all lis that are ancestors of node." |
|
6295 // |
|
6296 // Remember, must be in tree order. |
|
6297 var items = []; |
|
6298 for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) { |
|
6299 if (isHtmlElement(ancestor, "li")) { |
|
6300 items.unshift(ancestor); |
|
6301 } |
|
6302 } |
|
6303 |
|
6304 // "Normalize sublists of each item in items." |
|
6305 for (var i = 0; i < items.length; i++) { |
|
6306 normalizeSublists(items[i]); |
|
6307 } |
|
6308 |
|
6309 // "Record the values of the one-node list consisting of node, and |
|
6310 // let values be the result." |
|
6311 var values = recordValues([node]); |
|
6312 |
|
6313 // "Split the parent of the one-node list consisting of node." |
|
6314 splitParent([node]); |
|
6315 |
|
6316 // "Restore the values from values." |
|
6317 restoreValues(values); |
|
6318 |
|
6319 // "If node is a dd or dt, and it is not an allowed child of any of |
|
6320 // its ancestors in the same editing host, set the tag name of node |
|
6321 // to the default single-line container name and let node be the |
|
6322 // result." |
|
6323 if (isHtmlElement(node, ["dd", "dt"]) |
|
6324 && getAncestors(node).every(function(ancestor) { |
|
6325 return !inSameEditingHost(node, ancestor) |
|
6326 || !isAllowedChild(node, ancestor) |
|
6327 })) { |
|
6328 node = setTagName(node, defaultSingleLineContainerName); |
|
6329 } |
|
6330 |
|
6331 // "Fix disallowed ancestors of node." |
|
6332 fixDisallowedAncestors(node); |
|
6333 |
|
6334 // "Return true." |
|
6335 return true; |
|
6336 } |
|
6337 |
|
6338 // "Let start node equal node and let start offset equal offset." |
|
6339 var startNode = node; |
|
6340 var startOffset = offset; |
|
6341 |
|
6342 // "Repeat the following steps:" |
|
6343 while (true) { |
|
6344 // "If start offset is zero, set start offset to the index of start |
|
6345 // node and then set start node to its parent." |
|
6346 if (startOffset == 0) { |
|
6347 startOffset = getNodeIndex(startNode); |
|
6348 startNode = startNode.parentNode; |
|
6349 |
|
6350 // "Otherwise, if start node has an editable invisible child with |
|
6351 // index start offset minus one, remove it from start node and |
|
6352 // subtract one from start offset." |
|
6353 } else if (0 <= startOffset - 1 |
|
6354 && startOffset - 1 < startNode.childNodes.length |
|
6355 && isEditable(startNode.childNodes[startOffset - 1]) |
|
6356 && isInvisible(startNode.childNodes[startOffset - 1])) { |
|
6357 startNode.removeChild(startNode.childNodes[startOffset - 1]); |
|
6358 startOffset--; |
|
6359 |
|
6360 // "Otherwise, break from this loop." |
|
6361 } else { |
|
6362 break; |
|
6363 } |
|
6364 } |
|
6365 |
|
6366 // "If offset is zero, and node has an editable ancestor container in |
|
6367 // the same editing host that's an indentation element:" |
|
6368 if (offset == 0 |
|
6369 && getAncestors(node).concat(node).filter(function(ancestor) { |
|
6370 return isEditable(ancestor) |
|
6371 && inSameEditingHost(ancestor, node) |
|
6372 && isIndentationElement(ancestor); |
|
6373 }).length) { |
|
6374 // "Block-extend the range whose start and end are both (node, 0), |
|
6375 // and let new range be the result." |
|
6376 var newRange = document.createRange(); |
|
6377 newRange.setStart(node, 0); |
|
6378 newRange = blockExtend(newRange); |
|
6379 |
|
6380 // "Let node list be a list of nodes, initially empty." |
|
6381 // |
|
6382 // "For each node current node contained in new range, append |
|
6383 // current node to node list if the last member of node list (if |
|
6384 // any) is not an ancestor of current node, and current node is |
|
6385 // editable but has no editable descendants." |
|
6386 var nodeList = getContainedNodes(newRange, function(currentNode) { |
|
6387 return isEditable(currentNode) |
|
6388 && !hasEditableDescendants(currentNode); |
|
6389 }); |
|
6390 |
|
6391 // "Outdent each node in node list." |
|
6392 for (var i = 0; i < nodeList.length; i++) { |
|
6393 outdentNode(nodeList[i]); |
|
6394 } |
|
6395 |
|
6396 // "Return true." |
|
6397 return true; |
|
6398 } |
|
6399 |
|
6400 // "If the child of start node with index start offset is a table, |
|
6401 // return true." |
|
6402 if (isHtmlElement(startNode.childNodes[startOffset], "table")) { |
|
6403 return true; |
|
6404 } |
|
6405 |
|
6406 // "If start node has a child with index start offset − 1, and that |
|
6407 // child is a table:" |
|
6408 if (0 <= startOffset - 1 |
|
6409 && startOffset - 1 < startNode.childNodes.length |
|
6410 && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) { |
|
6411 // "Call collapse(start node, start offset − 1) on the context |
|
6412 // object's Selection." |
|
6413 getSelection().collapse(startNode, startOffset - 1); |
|
6414 getActiveRange().setStart(startNode, startOffset - 1); |
|
6415 |
|
6416 // "Call extend(start node, start offset) on the context object's |
|
6417 // Selection." |
|
6418 getSelection().extend(startNode, startOffset); |
|
6419 getActiveRange().setEnd(startNode, startOffset); |
|
6420 |
|
6421 // "Return true." |
|
6422 return true; |
|
6423 } |
|
6424 |
|
6425 // "If offset is zero; and either the child of start node with index |
|
6426 // start offset minus one is an hr, or the child is a br whose |
|
6427 // previousSibling is either a br or not an inline node:" |
|
6428 if (offset == 0 |
|
6429 && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr") |
|
6430 || ( |
|
6431 isHtmlElement(startNode.childNodes[startOffset - 1], "br") |
|
6432 && ( |
|
6433 isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br") |
|
6434 || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling) |
|
6435 ) |
|
6436 ) |
|
6437 )) { |
|
6438 // "Call collapse(start node, start offset − 1) on the context |
|
6439 // object's Selection." |
|
6440 getSelection().collapse(startNode, startOffset - 1); |
|
6441 getActiveRange().setStart(startNode, startOffset - 1); |
|
6442 |
|
6443 // "Call extend(start node, start offset) on the context object's |
|
6444 // Selection." |
|
6445 getSelection().extend(startNode, startOffset); |
|
6446 getActiveRange().setEnd(startNode, startOffset); |
|
6447 |
|
6448 // "Delete the selection." |
|
6449 deleteSelection(); |
|
6450 |
|
6451 // "Call collapse(node, offset) on the Selection." |
|
6452 getSelection().collapse(node, offset); |
|
6453 getActiveRange().setStart(node, offset); |
|
6454 getActiveRange().collapse(true); |
|
6455 |
|
6456 // "Return true." |
|
6457 return true; |
|
6458 } |
|
6459 |
|
6460 // "If the child of start node with index start offset is an li or dt |
|
6461 // or dd, and that child's firstChild is an inline node, and start |
|
6462 // offset is not zero:" |
|
6463 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) |
|
6464 && isInlineNode(startNode.childNodes[startOffset].firstChild) |
|
6465 && startOffset != 0) { |
|
6466 // "Let previous item be the child of start node with index start |
|
6467 // offset minus one." |
|
6468 var previousItem = startNode.childNodes[startOffset - 1]; |
|
6469 |
|
6470 // "If previous item's lastChild is an inline node other than a br, |
|
6471 // call createElement("br") on the context object and append the |
|
6472 // result as the last child of previous item." |
|
6473 if (isInlineNode(previousItem.lastChild) |
|
6474 && !isHtmlElement(previousItem.lastChild, "br")) { |
|
6475 previousItem.appendChild(document.createElement("br")); |
|
6476 } |
|
6477 |
|
6478 // "If previous item's lastChild is an inline node, call |
|
6479 // createElement("br") on the context object and append the result |
|
6480 // as the last child of previous item." |
|
6481 if (isInlineNode(previousItem.lastChild)) { |
|
6482 previousItem.appendChild(document.createElement("br")); |
|
6483 } |
|
6484 } |
|
6485 |
|
6486 // "If start node's child with index start offset is an li or dt or dd, |
|
6487 // and that child's previousSibling is also an li or dt or dd:" |
|
6488 if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) |
|
6489 && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) { |
|
6490 // "Call cloneRange() on the active range, and let original range |
|
6491 // be the result." |
|
6492 // |
|
6493 // We need to add it to extraRanges so it will actually get updated |
|
6494 // when moving preserving ranges. |
|
6495 var originalRange = getActiveRange().cloneRange(); |
|
6496 extraRanges.push(originalRange); |
|
6497 |
|
6498 // "Set start node to its child with index start offset − 1." |
|
6499 startNode = startNode.childNodes[startOffset - 1]; |
|
6500 |
|
6501 // "Set start offset to start node's length." |
|
6502 startOffset = getNodeLength(startNode); |
|
6503 |
|
6504 // "Set node to start node's nextSibling." |
|
6505 node = startNode.nextSibling; |
|
6506 |
|
6507 // "Call collapse(start node, start offset) on the context object's |
|
6508 // Selection." |
|
6509 getSelection().collapse(startNode, startOffset); |
|
6510 getActiveRange().setStart(startNode, startOffset); |
|
6511 |
|
6512 // "Call extend(node, 0) on the context object's Selection." |
|
6513 getSelection().extend(node, 0); |
|
6514 getActiveRange().setEnd(node, 0); |
|
6515 |
|
6516 // "Delete the selection." |
|
6517 deleteSelection(); |
|
6518 |
|
6519 // "Call removeAllRanges() on the context object's Selection." |
|
6520 getSelection().removeAllRanges(); |
|
6521 |
|
6522 // "Call addRange(original range) on the context object's |
|
6523 // Selection." |
|
6524 getSelection().addRange(originalRange); |
|
6525 getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset); |
|
6526 getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset); |
|
6527 |
|
6528 // "Return true." |
|
6529 extraRanges.pop(); |
|
6530 return true; |
|
6531 } |
|
6532 |
|
6533 // "While start node has a child with index start offset minus one:" |
|
6534 while (0 <= startOffset - 1 |
|
6535 && startOffset - 1 < startNode.childNodes.length) { |
|
6536 // "If start node's child with index start offset minus one is |
|
6537 // editable and invisible, remove it from start node, then subtract |
|
6538 // one from start offset." |
|
6539 if (isEditable(startNode.childNodes[startOffset - 1]) |
|
6540 && isInvisible(startNode.childNodes[startOffset - 1])) { |
|
6541 startNode.removeChild(startNode.childNodes[startOffset - 1]); |
|
6542 startOffset--; |
|
6543 |
|
6544 // "Otherwise, set start node to its child with index start offset |
|
6545 // minus one, then set start offset to the length of start node." |
|
6546 } else { |
|
6547 startNode = startNode.childNodes[startOffset - 1]; |
|
6548 startOffset = getNodeLength(startNode); |
|
6549 } |
|
6550 } |
|
6551 |
|
6552 // "Call collapse(start node, start offset) on the context object's |
|
6553 // Selection." |
|
6554 getSelection().collapse(startNode, startOffset); |
|
6555 getActiveRange().setStart(startNode, startOffset); |
|
6556 |
|
6557 // "Call extend(node, offset) on the context object's Selection." |
|
6558 getSelection().extend(node, offset); |
|
6559 getActiveRange().setEnd(node, offset); |
|
6560 |
|
6561 // "Delete the selection, with direction "backward"." |
|
6562 deleteSelection({direction: "backward"}); |
|
6563 |
|
6564 // "Return true." |
|
6565 return true; |
|
6566 } |
|
6567 }; |
|
6568 |
|
6569 //@} |
|
6570 ///// The formatBlock command ///// |
|
6571 //@{ |
|
6572 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3", |
|
6573 // "h4", "h5", "h6", "p", or "pre"." |
|
6574 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3", |
|
6575 "h4", "h5", "h6", "p", "pre"]; |
|
6576 |
|
6577 commands.formatblock = { |
|
6578 preservesOverrides: true, |
|
6579 action: function(value) { |
|
6580 // "If value begins with a "<" character and ends with a ">" character, |
|
6581 // remove the first and last characters from it." |
|
6582 if (/^<.*>$/.test(value)) { |
|
6583 value = value.slice(1, -1); |
|
6584 } |
|
6585 |
|
6586 // "Let value be converted to ASCII lowercase." |
|
6587 value = value.toLowerCase(); |
|
6588 |
|
6589 // "If value is not a formattable block name, return false." |
|
6590 if (formattableBlockNames.indexOf(value) == -1) { |
|
6591 return false; |
|
6592 } |
|
6593 |
|
6594 // "Block-extend the active range, and let new range be the result." |
|
6595 var newRange = blockExtend(getActiveRange()); |
|
6596 |
|
6597 // "Let node list be an empty list of nodes." |
|
6598 // |
|
6599 // "For each node node contained in new range, append node to node list |
|
6600 // if it is editable, the last member of original node list (if any) is |
|
6601 // not an ancestor of node, node is either a non-list single-line |
|
6602 // container or an allowed child of "p" or a dd or dt, and node is not |
|
6603 // the ancestor of a prohibited paragraph child." |
|
6604 var nodeList = getContainedNodes(newRange, function(node) { |
|
6605 return isEditable(node) |
|
6606 && (isNonListSingleLineContainer(node) |
|
6607 || isAllowedChild(node, "p") |
|
6608 || isHtmlElement(node, ["dd", "dt"])) |
|
6609 && !getDescendants(node).some(isProhibitedParagraphChild); |
|
6610 }); |
|
6611 |
|
6612 // "Record the values of node list, and let values be the result." |
|
6613 var values = recordValues(nodeList); |
|
6614 |
|
6615 // "For each node in node list, while node is the descendant of an |
|
6616 // editable HTML element in the same editing host, whose local name is |
|
6617 // a formattable block name, and which is not the ancestor of a |
|
6618 // prohibited paragraph child, split the parent of the one-node list |
|
6619 // consisting of node." |
|
6620 for (var i = 0; i < nodeList.length; i++) { |
|
6621 var node = nodeList[i]; |
|
6622 while (getAncestors(node).some(function(ancestor) { |
|
6623 return isEditable(ancestor) |
|
6624 && inSameEditingHost(ancestor, node) |
|
6625 && isHtmlElement(ancestor, formattableBlockNames) |
|
6626 && !getDescendants(ancestor).some(isProhibitedParagraphChild); |
|
6627 })) { |
|
6628 splitParent([node]); |
|
6629 } |
|
6630 } |
|
6631 |
|
6632 // "Restore the values from values." |
|
6633 restoreValues(values); |
|
6634 |
|
6635 // "While node list is not empty:" |
|
6636 while (nodeList.length) { |
|
6637 var sublist; |
|
6638 |
|
6639 // "If the first member of node list is a single-line |
|
6640 // container:" |
|
6641 if (isSingleLineContainer(nodeList[0])) { |
|
6642 // "Let sublist be the children of the first member of node |
|
6643 // list." |
|
6644 sublist = [].slice.call(nodeList[0].childNodes); |
|
6645 |
|
6646 // "Record the values of sublist, and let values be the |
|
6647 // result." |
|
6648 var values = recordValues(sublist); |
|
6649 |
|
6650 // "Remove the first member of node list from its parent, |
|
6651 // preserving its descendants." |
|
6652 removePreservingDescendants(nodeList[0]); |
|
6653 |
|
6654 // "Restore the values from values." |
|
6655 restoreValues(values); |
|
6656 |
|
6657 // "Remove the first member from node list." |
|
6658 nodeList.shift(); |
|
6659 |
|
6660 // "Otherwise:" |
|
6661 } else { |
|
6662 // "Let sublist be an empty list of nodes." |
|
6663 sublist = []; |
|
6664 |
|
6665 // "Remove the first member of node list and append it to |
|
6666 // sublist." |
|
6667 sublist.push(nodeList.shift()); |
|
6668 |
|
6669 // "While node list is not empty, and the first member of |
|
6670 // node list is the nextSibling of the last member of |
|
6671 // sublist, and the first member of node list is not a |
|
6672 // single-line container, and the last member of sublist is |
|
6673 // not a br, remove the first member of node list and |
|
6674 // append it to sublist." |
|
6675 while (nodeList.length |
|
6676 && nodeList[0] == sublist[sublist.length - 1].nextSibling |
|
6677 && !isSingleLineContainer(nodeList[0]) |
|
6678 && !isHtmlElement(sublist[sublist.length - 1], "BR")) { |
|
6679 sublist.push(nodeList.shift()); |
|
6680 } |
|
6681 } |
|
6682 |
|
6683 // "Wrap sublist. If value is "div" or "p", sibling criteria |
|
6684 // returns false; otherwise it returns true for an HTML element |
|
6685 // with local name value and no attributes, and false otherwise. |
|
6686 // New parent instructions return the result of running |
|
6687 // createElement(value) on the context object. Then fix disallowed |
|
6688 // ancestors of the result." |
|
6689 fixDisallowedAncestors(wrap(sublist, |
|
6690 ["div", "p"].indexOf(value) == - 1 |
|
6691 ? function(node) { return isHtmlElement(node, value) && !node.attributes.length } |
|
6692 : function() { return false }, |
|
6693 function() { return document.createElement(value) })); |
|
6694 } |
|
6695 |
|
6696 // "Return true." |
|
6697 return true; |
|
6698 }, indeterm: function() { |
|
6699 // "If the active range is null, return false." |
|
6700 if (!getActiveRange()) { |
|
6701 return false; |
|
6702 } |
|
6703 |
|
6704 // "Block-extend the active range, and let new range be the result." |
|
6705 var newRange = blockExtend(getActiveRange()); |
|
6706 |
|
6707 // "Let node list be all visible editable nodes that are contained in |
|
6708 // new range and have no children." |
|
6709 var nodeList = getAllContainedNodes(newRange, function(node) { |
|
6710 return isVisible(node) |
|
6711 && isEditable(node) |
|
6712 && !node.hasChildNodes(); |
|
6713 }); |
|
6714 |
|
6715 // "If node list is empty, return false." |
|
6716 if (!nodeList.length) { |
|
6717 return false; |
|
6718 } |
|
6719 |
|
6720 // "Let type be null." |
|
6721 var type = null; |
|
6722 |
|
6723 // "For each node in node list:" |
|
6724 for (var i = 0; i < nodeList.length; i++) { |
|
6725 var node = nodeList[i]; |
|
6726 |
|
6727 // "While node's parent is editable and in the same editing host as |
|
6728 // node, and node is not an HTML element whose local name is a |
|
6729 // formattable block name, set node to its parent." |
|
6730 while (isEditable(node.parentNode) |
|
6731 && inSameEditingHost(node, node.parentNode) |
|
6732 && !isHtmlElement(node, formattableBlockNames)) { |
|
6733 node = node.parentNode; |
|
6734 } |
|
6735 |
|
6736 // "Let current type be the empty string." |
|
6737 var currentType = ""; |
|
6738 |
|
6739 // "If node is an editable HTML element whose local name is a |
|
6740 // formattable block name, and node is not the ancestor of a |
|
6741 // prohibited paragraph child, set current type to node's local |
|
6742 // name." |
|
6743 if (isEditable(node) |
|
6744 && isHtmlElement(node, formattableBlockNames) |
|
6745 && !getDescendants(node).some(isProhibitedParagraphChild)) { |
|
6746 currentType = node.tagName; |
|
6747 } |
|
6748 |
|
6749 // "If type is null, set type to current type." |
|
6750 if (type === null) { |
|
6751 type = currentType; |
|
6752 |
|
6753 // "Otherwise, if type does not equal current type, return true." |
|
6754 } else if (type != currentType) { |
|
6755 return true; |
|
6756 } |
|
6757 } |
|
6758 |
|
6759 // "Return false." |
|
6760 return false; |
|
6761 }, value: function() { |
|
6762 // "If the active range is null, return the empty string." |
|
6763 if (!getActiveRange()) { |
|
6764 return ""; |
|
6765 } |
|
6766 |
|
6767 // "Block-extend the active range, and let new range be the result." |
|
6768 var newRange = blockExtend(getActiveRange()); |
|
6769 |
|
6770 // "Let node be the first visible editable node that is contained in |
|
6771 // new range and has no children. If there is no such node, return the |
|
6772 // empty string." |
|
6773 var nodes = getAllContainedNodes(newRange, function(node) { |
|
6774 return isVisible(node) |
|
6775 && isEditable(node) |
|
6776 && !node.hasChildNodes(); |
|
6777 }); |
|
6778 if (!nodes.length) { |
|
6779 return ""; |
|
6780 } |
|
6781 var node = nodes[0]; |
|
6782 |
|
6783 // "While node's parent is editable and in the same editing host as |
|
6784 // node, and node is not an HTML element whose local name is a |
|
6785 // formattable block name, set node to its parent." |
|
6786 while (isEditable(node.parentNode) |
|
6787 && inSameEditingHost(node, node.parentNode) |
|
6788 && !isHtmlElement(node, formattableBlockNames)) { |
|
6789 node = node.parentNode; |
|
6790 } |
|
6791 |
|
6792 // "If node is an editable HTML element whose local name is a |
|
6793 // formattable block name, and node is not the ancestor of a prohibited |
|
6794 // paragraph child, return node's local name, converted to ASCII |
|
6795 // lowercase." |
|
6796 if (isEditable(node) |
|
6797 && isHtmlElement(node, formattableBlockNames) |
|
6798 && !getDescendants(node).some(isProhibitedParagraphChild)) { |
|
6799 return node.tagName.toLowerCase(); |
|
6800 } |
|
6801 |
|
6802 // "Return the empty string." |
|
6803 return ""; |
|
6804 } |
|
6805 }; |
|
6806 |
|
6807 //@} |
|
6808 ///// The forwardDelete command ///// |
|
6809 //@{ |
|
6810 commands.forwarddelete = { |
|
6811 preservesOverrides: true, |
|
6812 action: function() { |
|
6813 // "If the active range is not collapsed, delete the selection and |
|
6814 // return true." |
|
6815 if (!getActiveRange().collapsed) { |
|
6816 deleteSelection(); |
|
6817 return true; |
|
6818 } |
|
6819 |
|
6820 // "Canonicalize whitespace at the active range's start." |
|
6821 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); |
|
6822 |
|
6823 // "Let node and offset be the active range's start node and offset." |
|
6824 var node = getActiveRange().startContainer; |
|
6825 var offset = getActiveRange().startOffset; |
|
6826 |
|
6827 // "Repeat the following steps:" |
|
6828 while (true) { |
|
6829 // "If offset is the length of node and node's nextSibling is an |
|
6830 // editable invisible node, remove node's nextSibling from its |
|
6831 // parent." |
|
6832 if (offset == getNodeLength(node) |
|
6833 && isEditable(node.nextSibling) |
|
6834 && isInvisible(node.nextSibling)) { |
|
6835 node.parentNode.removeChild(node.nextSibling); |
|
6836 |
|
6837 // "Otherwise, if node has a child with index offset and that child |
|
6838 // is an editable invisible node, remove that child from node." |
|
6839 } else if (offset < node.childNodes.length |
|
6840 && isEditable(node.childNodes[offset]) |
|
6841 && isInvisible(node.childNodes[offset])) { |
|
6842 node.removeChild(node.childNodes[offset]); |
|
6843 |
|
6844 // "Otherwise, if offset is the length of node and node is an |
|
6845 // inline node, or if node is invisible, set offset to one plus the |
|
6846 // index of node, then set node to its parent." |
|
6847 } else if ((offset == getNodeLength(node) |
|
6848 && isInlineNode(node)) |
|
6849 || isInvisible(node)) { |
|
6850 offset = 1 + getNodeIndex(node); |
|
6851 node = node.parentNode; |
|
6852 |
|
6853 // "Otherwise, if node has a child with index offset and that child |
|
6854 // is neither a block node nor a br nor an img nor a collapsed |
|
6855 // block prop, set node to that child, then set offset to zero." |
|
6856 } else if (offset < node.childNodes.length |
|
6857 && !isBlockNode(node.childNodes[offset]) |
|
6858 && !isHtmlElement(node.childNodes[offset], ["br", "img"]) |
|
6859 && !isCollapsedBlockProp(node.childNodes[offset])) { |
|
6860 node = node.childNodes[offset]; |
|
6861 offset = 0; |
|
6862 |
|
6863 // "Otherwise, break from this loop." |
|
6864 } else { |
|
6865 break; |
|
6866 } |
|
6867 } |
|
6868 |
|
6869 // "If node is a Text node and offset is not node's length:" |
|
6870 if (node.nodeType == Node.TEXT_NODE |
|
6871 && offset != getNodeLength(node)) { |
|
6872 // "Let end offset be offset plus one." |
|
6873 var endOffset = offset + 1; |
|
6874 |
|
6875 // "While end offset is not node's length and the end offsetth |
|
6876 // element of node's data has general category M when interpreted |
|
6877 // as a Unicode code point, add one to end offset." |
|
6878 // |
|
6879 // TODO: Not even going to try handling anything beyond the most |
|
6880 // basic combining marks, since I couldn't find a good list. I |
|
6881 // special-case a few Hebrew diacritics too to test basic coverage |
|
6882 // of non-Latin stuff. |
|
6883 while (endOffset != node.length |
|
6884 && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) { |
|
6885 endOffset++; |
|
6886 } |
|
6887 |
|
6888 // "Call collapse(node, offset) on the context object's Selection." |
|
6889 getSelection().collapse(node, offset); |
|
6890 getActiveRange().setStart(node, offset); |
|
6891 |
|
6892 // "Call extend(node, end offset) on the context object's |
|
6893 // Selection." |
|
6894 getSelection().extend(node, endOffset); |
|
6895 getActiveRange().setEnd(node, endOffset); |
|
6896 |
|
6897 // "Delete the selection." |
|
6898 deleteSelection(); |
|
6899 |
|
6900 // "Return true." |
|
6901 return true; |
|
6902 } |
|
6903 |
|
6904 // "If node is an inline node, return true." |
|
6905 if (isInlineNode(node)) { |
|
6906 return true; |
|
6907 } |
|
6908 |
|
6909 // "If node has a child with index offset and that child is a br or hr |
|
6910 // or img, but is not a collapsed block prop:" |
|
6911 if (offset < node.childNodes.length |
|
6912 && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"]) |
|
6913 && !isCollapsedBlockProp(node.childNodes[offset])) { |
|
6914 // "Call collapse(node, offset) on the context object's Selection." |
|
6915 getSelection().collapse(node, offset); |
|
6916 getActiveRange().setStart(node, offset); |
|
6917 |
|
6918 // "Call extend(node, offset + 1) on the context object's |
|
6919 // Selection." |
|
6920 getSelection().extend(node, offset + 1); |
|
6921 getActiveRange().setEnd(node, offset + 1); |
|
6922 |
|
6923 // "Delete the selection." |
|
6924 deleteSelection(); |
|
6925 |
|
6926 // "Return true." |
|
6927 return true; |
|
6928 } |
|
6929 |
|
6930 // "Let end node equal node and let end offset equal offset." |
|
6931 var endNode = node; |
|
6932 var endOffset = offset; |
|
6933 |
|
6934 // "If end node has a child with index end offset, and that child is a |
|
6935 // collapsed block prop, add one to end offset." |
|
6936 if (endOffset < endNode.childNodes.length |
|
6937 && isCollapsedBlockProp(endNode.childNodes[endOffset])) { |
|
6938 endOffset++; |
|
6939 } |
|
6940 |
|
6941 // "Repeat the following steps:" |
|
6942 while (true) { |
|
6943 // "If end offset is the length of end node, set end offset to one |
|
6944 // plus the index of end node and then set end node to its parent." |
|
6945 if (endOffset == getNodeLength(endNode)) { |
|
6946 endOffset = 1 + getNodeIndex(endNode); |
|
6947 endNode = endNode.parentNode; |
|
6948 |
|
6949 // "Otherwise, if end node has a an editable invisible child with |
|
6950 // index end offset, remove it from end node." |
|
6951 } else if (endOffset < endNode.childNodes.length |
|
6952 && isEditable(endNode.childNodes[endOffset]) |
|
6953 && isInvisible(endNode.childNodes[endOffset])) { |
|
6954 endNode.removeChild(endNode.childNodes[endOffset]); |
|
6955 |
|
6956 // "Otherwise, break from this loop." |
|
6957 } else { |
|
6958 break; |
|
6959 } |
|
6960 } |
|
6961 |
|
6962 // "If the child of end node with index end offset minus one is a |
|
6963 // table, return true." |
|
6964 if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) { |
|
6965 return true; |
|
6966 } |
|
6967 |
|
6968 // "If the child of end node with index end offset is a table:" |
|
6969 if (isHtmlElement(endNode.childNodes[endOffset], "table")) { |
|
6970 // "Call collapse(end node, end offset) on the context object's |
|
6971 // Selection." |
|
6972 getSelection().collapse(endNode, endOffset); |
|
6973 getActiveRange().setStart(endNode, endOffset); |
|
6974 |
|
6975 // "Call extend(end node, end offset + 1) on the context object's |
|
6976 // Selection." |
|
6977 getSelection().extend(endNode, endOffset + 1); |
|
6978 getActiveRange().setEnd(endNode, endOffset + 1); |
|
6979 |
|
6980 // "Return true." |
|
6981 return true; |
|
6982 } |
|
6983 |
|
6984 // "If offset is the length of node, and the child of end node with |
|
6985 // index end offset is an hr or br:" |
|
6986 if (offset == getNodeLength(node) |
|
6987 && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) { |
|
6988 // "Call collapse(end node, end offset) on the context object's |
|
6989 // Selection." |
|
6990 getSelection().collapse(endNode, endOffset); |
|
6991 getActiveRange().setStart(endNode, endOffset); |
|
6992 |
|
6993 // "Call extend(end node, end offset + 1) on the context object's |
|
6994 // Selection." |
|
6995 getSelection().extend(endNode, endOffset + 1); |
|
6996 getActiveRange().setEnd(endNode, endOffset + 1); |
|
6997 |
|
6998 // "Delete the selection." |
|
6999 deleteSelection(); |
|
7000 |
|
7001 // "Call collapse(node, offset) on the Selection." |
|
7002 getSelection().collapse(node, offset); |
|
7003 getActiveRange().setStart(node, offset); |
|
7004 getActiveRange().collapse(true); |
|
7005 |
|
7006 // "Return true." |
|
7007 return true; |
|
7008 } |
|
7009 |
|
7010 // "While end node has a child with index end offset:" |
|
7011 while (endOffset < endNode.childNodes.length) { |
|
7012 // "If end node's child with index end offset is editable and |
|
7013 // invisible, remove it from end node." |
|
7014 if (isEditable(endNode.childNodes[endOffset]) |
|
7015 && isInvisible(endNode.childNodes[endOffset])) { |
|
7016 endNode.removeChild(endNode.childNodes[endOffset]); |
|
7017 |
|
7018 // "Otherwise, set end node to its child with index end offset and |
|
7019 // set end offset to zero." |
|
7020 } else { |
|
7021 endNode = endNode.childNodes[endOffset]; |
|
7022 endOffset = 0; |
|
7023 } |
|
7024 } |
|
7025 |
|
7026 // "Call collapse(node, offset) on the context object's Selection." |
|
7027 getSelection().collapse(node, offset); |
|
7028 getActiveRange().setStart(node, offset); |
|
7029 |
|
7030 // "Call extend(end node, end offset) on the context object's |
|
7031 // Selection." |
|
7032 getSelection().extend(endNode, endOffset); |
|
7033 getActiveRange().setEnd(endNode, endOffset); |
|
7034 |
|
7035 // "Delete the selection." |
|
7036 deleteSelection(); |
|
7037 |
|
7038 // "Return true." |
|
7039 return true; |
|
7040 } |
|
7041 }; |
|
7042 |
|
7043 //@} |
|
7044 ///// The indent command ///// |
|
7045 //@{ |
|
7046 commands.indent = { |
|
7047 preservesOverrides: true, |
|
7048 action: function() { |
|
7049 // "Let items be a list of all lis that are ancestor containers of the |
|
7050 // active range's start and/or end node." |
|
7051 // |
|
7052 // Has to be in tree order, remember! |
|
7053 var items = []; |
|
7054 for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { |
|
7055 if (isHtmlElement(node, "LI")) { |
|
7056 items.unshift(node); |
|
7057 } |
|
7058 } |
|
7059 for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { |
|
7060 if (isHtmlElement(node, "LI")) { |
|
7061 items.unshift(node); |
|
7062 } |
|
7063 } |
|
7064 for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) { |
|
7065 if (isHtmlElement(node, "LI")) { |
|
7066 items.unshift(node); |
|
7067 } |
|
7068 } |
|
7069 |
|
7070 // "For each item in items, normalize sublists of item." |
|
7071 for (var i = 0; i < items.length; i++) { |
|
7072 normalizeSublists(items[i]); |
|
7073 } |
|
7074 |
|
7075 // "Block-extend the active range, and let new range be the result." |
|
7076 var newRange = blockExtend(getActiveRange()); |
|
7077 |
|
7078 // "Let node list be a list of nodes, initially empty." |
|
7079 var nodeList = []; |
|
7080 |
|
7081 // "For each node node contained in new range, if node is editable and |
|
7082 // is an allowed child of "div" or "ol" and if the last member of node |
|
7083 // list (if any) is not an ancestor of node, append node to node list." |
|
7084 nodeList = getContainedNodes(newRange, function(node) { |
|
7085 return isEditable(node) |
|
7086 && (isAllowedChild(node, "div") |
|
7087 || isAllowedChild(node, "ol")); |
|
7088 }); |
|
7089 |
|
7090 // "If the first visible member of node list is an li whose parent is |
|
7091 // an ol or ul:" |
|
7092 if (isHtmlElement(nodeList.filter(isVisible)[0], "li") |
|
7093 && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) { |
|
7094 // "Let sibling be node list's first visible member's |
|
7095 // previousSibling." |
|
7096 var sibling = nodeList.filter(isVisible)[0].previousSibling; |
|
7097 |
|
7098 // "While sibling is invisible, set sibling to its |
|
7099 // previousSibling." |
|
7100 while (isInvisible(sibling)) { |
|
7101 sibling = sibling.previousSibling; |
|
7102 } |
|
7103 |
|
7104 // "If sibling is an li, normalize sublists of sibling." |
|
7105 if (isHtmlElement(sibling, "li")) { |
|
7106 normalizeSublists(sibling); |
|
7107 } |
|
7108 } |
|
7109 |
|
7110 // "While node list is not empty:" |
|
7111 while (nodeList.length) { |
|
7112 // "Let sublist be a list of nodes, initially empty." |
|
7113 var sublist = []; |
|
7114 |
|
7115 // "Remove the first member of node list and append it to sublist." |
|
7116 sublist.push(nodeList.shift()); |
|
7117 |
|
7118 // "While the first member of node list is the nextSibling of the |
|
7119 // last member of sublist, remove the first member of node list and |
|
7120 // append it to sublist." |
|
7121 while (nodeList.length |
|
7122 && nodeList[0] == sublist[sublist.length - 1].nextSibling) { |
|
7123 sublist.push(nodeList.shift()); |
|
7124 } |
|
7125 |
|
7126 // "Indent sublist." |
|
7127 indentNodes(sublist); |
|
7128 } |
|
7129 |
|
7130 // "Return true." |
|
7131 return true; |
|
7132 } |
|
7133 }; |
|
7134 |
|
7135 //@} |
|
7136 ///// The insertHorizontalRule command ///// |
|
7137 //@{ |
|
7138 commands.inserthorizontalrule = { |
|
7139 preservesOverrides: true, |
|
7140 action: function() { |
|
7141 // "Let start node, start offset, end node, and end offset be the |
|
7142 // active range's start and end nodes and offsets." |
|
7143 var startNode = getActiveRange().startContainer; |
|
7144 var startOffset = getActiveRange().startOffset; |
|
7145 var endNode = getActiveRange().endContainer; |
|
7146 var endOffset = getActiveRange().endOffset; |
|
7147 |
|
7148 // "While start offset is 0 and start node's parent is not null, set |
|
7149 // start offset to start node's index, then set start node to its |
|
7150 // parent." |
|
7151 while (startOffset == 0 |
|
7152 && startNode.parentNode) { |
|
7153 startOffset = getNodeIndex(startNode); |
|
7154 startNode = startNode.parentNode; |
|
7155 } |
|
7156 |
|
7157 // "While end offset is end node's length, and end node's parent is not |
|
7158 // null, set end offset to one plus end node's index, then set end node |
|
7159 // to its parent." |
|
7160 while (endOffset == getNodeLength(endNode) |
|
7161 && endNode.parentNode) { |
|
7162 endOffset = 1 + getNodeIndex(endNode); |
|
7163 endNode = endNode.parentNode; |
|
7164 } |
|
7165 |
|
7166 // "Call collapse(start node, start offset) on the context object's |
|
7167 // Selection." |
|
7168 getSelection().collapse(startNode, startOffset); |
|
7169 getActiveRange().setStart(startNode, startOffset); |
|
7170 |
|
7171 // "Call extend(end node, end offset) on the context object's |
|
7172 // Selection." |
|
7173 getSelection().extend(endNode, endOffset); |
|
7174 getActiveRange().setEnd(endNode, endOffset); |
|
7175 |
|
7176 // "Delete the selection, with block merging false." |
|
7177 deleteSelection({blockMerging: false}); |
|
7178 |
|
7179 // "If the active range's start node is neither editable nor an editing |
|
7180 // host, return true." |
|
7181 if (!isEditable(getActiveRange().startContainer) |
|
7182 && !isEditingHost(getActiveRange().startContainer)) { |
|
7183 return true; |
|
7184 } |
|
7185 |
|
7186 // "If the active range's start node is a Text node and its start |
|
7187 // offset is zero, call collapse() on the context object's Selection, |
|
7188 // with first argument the active range's start node's parent and |
|
7189 // second argument the active range's start node's index." |
|
7190 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
|
7191 && getActiveRange().startOffset == 0) { |
|
7192 var newNode = getActiveRange().startContainer.parentNode; |
|
7193 var newOffset = getNodeIndex(getActiveRange().startContainer); |
|
7194 getSelection().collapse(newNode, newOffset); |
|
7195 getActiveRange().setStart(newNode, newOffset); |
|
7196 getActiveRange().collapse(true); |
|
7197 } |
|
7198 |
|
7199 // "If the active range's start node is a Text node and its start |
|
7200 // offset is the length of its start node, call collapse() on the |
|
7201 // context object's Selection, with first argument the active range's |
|
7202 // start node's parent, and the second argument one plus the active |
|
7203 // range's start node's index." |
|
7204 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
|
7205 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { |
|
7206 var newNode = getActiveRange().startContainer.parentNode; |
|
7207 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); |
|
7208 getSelection().collapse(newNode, newOffset); |
|
7209 getActiveRange().setStart(newNode, newOffset); |
|
7210 getActiveRange().collapse(true); |
|
7211 } |
|
7212 |
|
7213 // "Let hr be the result of calling createElement("hr") on the |
|
7214 // context object." |
|
7215 var hr = document.createElement("hr"); |
|
7216 |
|
7217 // "Run insertNode(hr) on the active range." |
|
7218 getActiveRange().insertNode(hr); |
|
7219 |
|
7220 // "Fix disallowed ancestors of hr." |
|
7221 fixDisallowedAncestors(hr); |
|
7222 |
|
7223 // "Run collapse() on the context object's Selection, with first |
|
7224 // argument hr's parent and the second argument equal to one plus hr's |
|
7225 // index." |
|
7226 getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr)); |
|
7227 getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr)); |
|
7228 getActiveRange().collapse(true); |
|
7229 |
|
7230 // "Return true." |
|
7231 return true; |
|
7232 } |
|
7233 }; |
|
7234 |
|
7235 //@} |
|
7236 ///// The insertHTML command ///// |
|
7237 //@{ |
|
7238 commands.inserthtml = { |
|
7239 preservesOverrides: true, |
|
7240 action: function(value) { |
|
7241 // "Delete the selection." |
|
7242 deleteSelection(); |
|
7243 |
|
7244 // "If the active range's start node is neither editable nor an editing |
|
7245 // host, return true." |
|
7246 if (!isEditable(getActiveRange().startContainer) |
|
7247 && !isEditingHost(getActiveRange().startContainer)) { |
|
7248 return true; |
|
7249 } |
|
7250 |
|
7251 // "Let frag be the result of calling createContextualFragment(value) |
|
7252 // on the active range." |
|
7253 var frag = getActiveRange().createContextualFragment(value); |
|
7254 |
|
7255 // "Let last child be the lastChild of frag." |
|
7256 var lastChild = frag.lastChild; |
|
7257 |
|
7258 // "If last child is null, return true." |
|
7259 if (!lastChild) { |
|
7260 return true; |
|
7261 } |
|
7262 |
|
7263 // "Let descendants be all descendants of frag." |
|
7264 var descendants = getDescendants(frag); |
|
7265 |
|
7266 // "If the active range's start node is a block node:" |
|
7267 if (isBlockNode(getActiveRange().startContainer)) { |
|
7268 // "Let collapsed block props be all editable collapsed block prop |
|
7269 // children of the active range's start node that have index |
|
7270 // greater than or equal to the active range's start offset." |
|
7271 // |
|
7272 // "For each node in collapsed block props, remove node from its |
|
7273 // parent." |
|
7274 [].filter.call(getActiveRange().startContainer.childNodes, function(node) { |
|
7275 return isEditable(node) |
|
7276 && isCollapsedBlockProp(node) |
|
7277 && getNodeIndex(node) >= getActiveRange().startOffset; |
|
7278 }).forEach(function(node) { |
|
7279 node.parentNode.removeChild(node); |
|
7280 }); |
|
7281 } |
|
7282 |
|
7283 // "Call insertNode(frag) on the active range." |
|
7284 getActiveRange().insertNode(frag); |
|
7285 |
|
7286 // "If the active range's start node is a block node with no visible |
|
7287 // children, call createElement("br") on the context object and append |
|
7288 // the result as the last child of the active range's start node." |
|
7289 if (isBlockNode(getActiveRange().startContainer) |
|
7290 && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) { |
|
7291 getActiveRange().startContainer.appendChild(document.createElement("br")); |
|
7292 } |
|
7293 |
|
7294 // "Call collapse() on the context object's Selection, with last |
|
7295 // child's parent as the first argument and one plus its index as the |
|
7296 // second." |
|
7297 getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild)); |
|
7298 getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild)); |
|
7299 |
|
7300 // "Fix disallowed ancestors of each member of descendants." |
|
7301 for (var i = 0; i < descendants.length; i++) { |
|
7302 fixDisallowedAncestors(descendants[i]); |
|
7303 } |
|
7304 |
|
7305 // "Return true." |
|
7306 return true; |
|
7307 } |
|
7308 }; |
|
7309 |
|
7310 //@} |
|
7311 ///// The insertImage command ///// |
|
7312 //@{ |
|
7313 commands.insertimage = { |
|
7314 preservesOverrides: true, |
|
7315 action: function(value) { |
|
7316 // "If value is the empty string, return false." |
|
7317 if (value === "") { |
|
7318 return false; |
|
7319 } |
|
7320 |
|
7321 // "Delete the selection, with strip wrappers false." |
|
7322 deleteSelection({stripWrappers: false}); |
|
7323 |
|
7324 // "Let range be the active range." |
|
7325 var range = getActiveRange(); |
|
7326 |
|
7327 // "If the active range's start node is neither editable nor an editing |
|
7328 // host, return true." |
|
7329 if (!isEditable(getActiveRange().startContainer) |
|
7330 && !isEditingHost(getActiveRange().startContainer)) { |
|
7331 return true; |
|
7332 } |
|
7333 |
|
7334 // "If range's start node is a block node whose sole child is a br, and |
|
7335 // its start offset is 0, remove its start node's child from it." |
|
7336 if (isBlockNode(range.startContainer) |
|
7337 && range.startContainer.childNodes.length == 1 |
|
7338 && isHtmlElement(range.startContainer.firstChild, "br") |
|
7339 && range.startOffset == 0) { |
|
7340 range.startContainer.removeChild(range.startContainer.firstChild); |
|
7341 } |
|
7342 |
|
7343 // "Let img be the result of calling createElement("img") on the |
|
7344 // context object." |
|
7345 var img = document.createElement("img"); |
|
7346 |
|
7347 // "Run setAttribute("src", value) on img." |
|
7348 img.setAttribute("src", value); |
|
7349 |
|
7350 // "Run insertNode(img) on the range." |
|
7351 range.insertNode(img); |
|
7352 |
|
7353 // "Run collapse() on the Selection, with first argument equal to the |
|
7354 // parent of img and the second argument equal to one plus the index of |
|
7355 // img." |
|
7356 // |
|
7357 // Not everyone actually supports collapse(), so we do it manually |
|
7358 // instead. Also, we need to modify the actual range we're given as |
|
7359 // well, for the sake of autoimplementation.html's range-filling-in. |
|
7360 range.setStart(img.parentNode, 1 + getNodeIndex(img)); |
|
7361 range.setEnd(img.parentNode, 1 + getNodeIndex(img)); |
|
7362 getSelection().removeAllRanges(); |
|
7363 getSelection().addRange(range); |
|
7364 |
|
7365 // IE adds width and height attributes for some reason, so remove those |
|
7366 // to actually do what the spec says. |
|
7367 img.removeAttribute("width"); |
|
7368 img.removeAttribute("height"); |
|
7369 |
|
7370 // "Return true." |
|
7371 return true; |
|
7372 } |
|
7373 }; |
|
7374 |
|
7375 //@} |
|
7376 ///// The insertLineBreak command ///// |
|
7377 //@{ |
|
7378 commands.insertlinebreak = { |
|
7379 preservesOverrides: true, |
|
7380 action: function(value) { |
|
7381 // "Delete the selection, with strip wrappers false." |
|
7382 deleteSelection({stripWrappers: false}); |
|
7383 |
|
7384 // "If the active range's start node is neither editable nor an editing |
|
7385 // host, return true." |
|
7386 if (!isEditable(getActiveRange().startContainer) |
|
7387 && !isEditingHost(getActiveRange().startContainer)) { |
|
7388 return true; |
|
7389 } |
|
7390 |
|
7391 // "If the active range's start node is an Element, and "br" is not an |
|
7392 // allowed child of it, return true." |
|
7393 if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE |
|
7394 && !isAllowedChild("br", getActiveRange().startContainer)) { |
|
7395 return true; |
|
7396 } |
|
7397 |
|
7398 // "If the active range's start node is not an Element, and "br" is not |
|
7399 // an allowed child of the active range's start node's parent, return |
|
7400 // true." |
|
7401 if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE |
|
7402 && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) { |
|
7403 return true; |
|
7404 } |
|
7405 |
|
7406 // "If the active range's start node is a Text node and its start |
|
7407 // offset is zero, call collapse() on the context object's Selection, |
|
7408 // with first argument equal to the active range's start node's parent |
|
7409 // and second argument equal to the active range's start node's index." |
|
7410 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
|
7411 && getActiveRange().startOffset == 0) { |
|
7412 var newNode = getActiveRange().startContainer.parentNode; |
|
7413 var newOffset = getNodeIndex(getActiveRange().startContainer); |
|
7414 getSelection().collapse(newNode, newOffset); |
|
7415 getActiveRange().setStart(newNode, newOffset); |
|
7416 getActiveRange().setEnd(newNode, newOffset); |
|
7417 } |
|
7418 |
|
7419 // "If the active range's start node is a Text node and its start |
|
7420 // offset is the length of its start node, call collapse() on the |
|
7421 // context object's Selection, with first argument equal to the active |
|
7422 // range's start node's parent and second argument equal to one plus |
|
7423 // the active range's start node's index." |
|
7424 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
|
7425 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { |
|
7426 var newNode = getActiveRange().startContainer.parentNode; |
|
7427 var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); |
|
7428 getSelection().collapse(newNode, newOffset); |
|
7429 getActiveRange().setStart(newNode, newOffset); |
|
7430 getActiveRange().setEnd(newNode, newOffset); |
|
7431 } |
|
7432 |
|
7433 // "Let br be the result of calling createElement("br") on the context |
|
7434 // object." |
|
7435 var br = document.createElement("br"); |
|
7436 |
|
7437 // "Call insertNode(br) on the active range." |
|
7438 getActiveRange().insertNode(br); |
|
7439 |
|
7440 // "Call collapse() on the context object's Selection, with br's parent |
|
7441 // as the first argument and one plus br's index as the second |
|
7442 // argument." |
|
7443 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); |
|
7444 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); |
|
7445 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); |
|
7446 |
|
7447 // "If br is a collapsed line break, call createElement("br") on the |
|
7448 // context object and let extra br be the result, then call |
|
7449 // insertNode(extra br) on the active range." |
|
7450 if (isCollapsedLineBreak(br)) { |
|
7451 getActiveRange().insertNode(document.createElement("br")); |
|
7452 |
|
7453 // Compensate for nonstandard implementations of insertNode |
|
7454 getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); |
|
7455 getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); |
|
7456 getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); |
|
7457 } |
|
7458 |
|
7459 // "Return true." |
|
7460 return true; |
|
7461 } |
|
7462 }; |
|
7463 |
|
7464 //@} |
|
7465 ///// The insertOrderedList command ///// |
|
7466 //@{ |
|
7467 commands.insertorderedlist = { |
|
7468 preservesOverrides: true, |
|
7469 // "Toggle lists with tag name "ol", then return true." |
|
7470 action: function() { toggleLists("ol"); return true }, |
|
7471 // "True if the selection's list state is "mixed" or "mixed ol", false |
|
7472 // otherwise." |
|
7473 indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) }, |
|
7474 // "True if the selection's list state is "ol", false otherwise." |
|
7475 state: function() { return getSelectionListState() == "ol" }, |
|
7476 }; |
|
7477 |
|
7478 //@} |
|
7479 ///// The insertParagraph command ///// |
|
7480 //@{ |
|
7481 commands.insertparagraph = { |
|
7482 preservesOverrides: true, |
|
7483 action: function() { |
|
7484 // "Delete the selection." |
|
7485 deleteSelection(); |
|
7486 |
|
7487 // "If the active range's start node is neither editable nor an editing |
|
7488 // host, return true." |
|
7489 if (!isEditable(getActiveRange().startContainer) |
|
7490 && !isEditingHost(getActiveRange().startContainer)) { |
|
7491 return true; |
|
7492 } |
|
7493 |
|
7494 // "Let node and offset be the active range's start node and offset." |
|
7495 var node = getActiveRange().startContainer; |
|
7496 var offset = getActiveRange().startOffset; |
|
7497 |
|
7498 // "If node is a Text node, and offset is neither 0 nor the length of |
|
7499 // node, call splitText(offset) on node." |
|
7500 if (node.nodeType == Node.TEXT_NODE |
|
7501 && offset != 0 |
|
7502 && offset != getNodeLength(node)) { |
|
7503 node.splitText(offset); |
|
7504 } |
|
7505 |
|
7506 // "If node is a Text node and offset is its length, set offset to one |
|
7507 // plus the index of node, then set node to its parent." |
|
7508 if (node.nodeType == Node.TEXT_NODE |
|
7509 && offset == getNodeLength(node)) { |
|
7510 offset = 1 + getNodeIndex(node); |
|
7511 node = node.parentNode; |
|
7512 } |
|
7513 |
|
7514 // "If node is a Text or Comment node, set offset to the index of node, |
|
7515 // then set node to its parent." |
|
7516 if (node.nodeType == Node.TEXT_NODE |
|
7517 || node.nodeType == Node.COMMENT_NODE) { |
|
7518 offset = getNodeIndex(node); |
|
7519 node = node.parentNode; |
|
7520 } |
|
7521 |
|
7522 // "Call collapse(node, offset) on the context object's Selection." |
|
7523 getSelection().collapse(node, offset); |
|
7524 getActiveRange().setStart(node, offset); |
|
7525 getActiveRange().setEnd(node, offset); |
|
7526 |
|
7527 // "Let container equal node." |
|
7528 var container = node; |
|
7529 |
|
7530 // "While container is not a single-line container, and container's |
|
7531 // parent is editable and in the same editing host as node, set |
|
7532 // container to its parent." |
|
7533 while (!isSingleLineContainer(container) |
|
7534 && isEditable(container.parentNode) |
|
7535 && inSameEditingHost(node, container.parentNode)) { |
|
7536 container = container.parentNode; |
|
7537 } |
|
7538 |
|
7539 // "If container is an editable single-line container in the same |
|
7540 // editing host as node, and its local name is "p" or "div":" |
|
7541 if (isEditable(container) |
|
7542 && isSingleLineContainer(container) |
|
7543 && inSameEditingHost(node, container.parentNode) |
|
7544 && (container.tagName == "P" || container.tagName == "DIV")) { |
|
7545 // "Let outer container equal container." |
|
7546 var outerContainer = container; |
|
7547 |
|
7548 // "While outer container is not a dd or dt or li, and outer |
|
7549 // container's parent is editable, set outer container to its |
|
7550 // parent." |
|
7551 while (!isHtmlElement(outerContainer, ["dd", "dt", "li"]) |
|
7552 && isEditable(outerContainer.parentNode)) { |
|
7553 outerContainer = outerContainer.parentNode; |
|
7554 } |
|
7555 |
|
7556 // "If outer container is a dd or dt or li, set container to outer |
|
7557 // container." |
|
7558 if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) { |
|
7559 container = outerContainer; |
|
7560 } |
|
7561 } |
|
7562 |
|
7563 // "If container is not editable or not in the same editing host as |
|
7564 // node or is not a single-line container:" |
|
7565 if (!isEditable(container) |
|
7566 || !inSameEditingHost(container, node) |
|
7567 || !isSingleLineContainer(container)) { |
|
7568 // "Let tag be the default single-line container name." |
|
7569 var tag = defaultSingleLineContainerName; |
|
7570 |
|
7571 // "Block-extend the active range, and let new range be the |
|
7572 // result." |
|
7573 var newRange = blockExtend(getActiveRange()); |
|
7574 |
|
7575 // "Let node list be a list of nodes, initially empty." |
|
7576 // |
|
7577 // "Append to node list the first node in tree order that is |
|
7578 // contained in new range and is an allowed child of "p", if any." |
|
7579 var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") }) |
|
7580 .slice(0, 1); |
|
7581 |
|
7582 // "If node list is empty:" |
|
7583 if (!nodeList.length) { |
|
7584 // "If tag is not an allowed child of the active range's start |
|
7585 // node, return true." |
|
7586 if (!isAllowedChild(tag, getActiveRange().startContainer)) { |
|
7587 return true; |
|
7588 } |
|
7589 |
|
7590 // "Set container to the result of calling createElement(tag) |
|
7591 // on the context object." |
|
7592 container = document.createElement(tag); |
|
7593 |
|
7594 // "Call insertNode(container) on the active range." |
|
7595 getActiveRange().insertNode(container); |
|
7596 |
|
7597 // "Call createElement("br") on the context object, and append |
|
7598 // the result as the last child of container." |
|
7599 container.appendChild(document.createElement("br")); |
|
7600 |
|
7601 // "Call collapse(container, 0) on the context object's |
|
7602 // Selection." |
|
7603 getSelection().collapse(container, 0); |
|
7604 getActiveRange().setStart(container, 0); |
|
7605 getActiveRange().setEnd(container, 0); |
|
7606 |
|
7607 // "Return true." |
|
7608 return true; |
|
7609 } |
|
7610 |
|
7611 // "While the nextSibling of the last member of node list is not |
|
7612 // null and is an allowed child of "p", append it to node list." |
|
7613 while (nodeList[nodeList.length - 1].nextSibling |
|
7614 && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) { |
|
7615 nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
|
7616 } |
|
7617 |
|
7618 // "Wrap node list, with sibling criteria returning false and new |
|
7619 // parent instructions returning the result of calling |
|
7620 // createElement(tag) on the context object. Set container to the |
|
7621 // result." |
|
7622 container = wrap(nodeList, |
|
7623 function() { return false }, |
|
7624 function() { return document.createElement(tag) } |
|
7625 ); |
|
7626 } |
|
7627 |
|
7628 // "If container's local name is "address", "listing", or "pre":" |
|
7629 if (container.tagName == "ADDRESS" |
|
7630 || container.tagName == "LISTING" |
|
7631 || container.tagName == "PRE") { |
|
7632 // "Let br be the result of calling createElement("br") on the |
|
7633 // context object." |
|
7634 var br = document.createElement("br"); |
|
7635 |
|
7636 // "Call insertNode(br) on the active range." |
|
7637 getActiveRange().insertNode(br); |
|
7638 |
|
7639 // "Call collapse(node, offset + 1) on the context object's |
|
7640 // Selection." |
|
7641 getSelection().collapse(node, offset + 1); |
|
7642 getActiveRange().setStart(node, offset + 1); |
|
7643 getActiveRange().setEnd(node, offset + 1); |
|
7644 |
|
7645 // "If br is the last descendant of container, let br be the result |
|
7646 // of calling createElement("br") on the context object, then call |
|
7647 // insertNode(br) on the active range." |
|
7648 // |
|
7649 // Work around browser bugs: some browsers select the |
|
7650 // newly-inserted node, not per spec. |
|
7651 if (!isDescendant(nextNode(br), container)) { |
|
7652 getActiveRange().insertNode(document.createElement("br")); |
|
7653 getSelection().collapse(node, offset + 1); |
|
7654 getActiveRange().setEnd(node, offset + 1); |
|
7655 } |
|
7656 |
|
7657 // "Return true." |
|
7658 return true; |
|
7659 } |
|
7660 |
|
7661 // "If container's local name is "li", "dt", or "dd"; and either it has |
|
7662 // no children or it has a single child and that child is a br:" |
|
7663 if (["LI", "DT", "DD"].indexOf(container.tagName) != -1 |
|
7664 && (!container.hasChildNodes() |
|
7665 || (container.childNodes.length == 1 |
|
7666 && isHtmlElement(container.firstChild, "br")))) { |
|
7667 // "Split the parent of the one-node list consisting of container." |
|
7668 splitParent([container]); |
|
7669 |
|
7670 // "If container has no children, call createElement("br") on the |
|
7671 // context object and append the result as the last child of |
|
7672 // container." |
|
7673 if (!container.hasChildNodes()) { |
|
7674 container.appendChild(document.createElement("br")); |
|
7675 } |
|
7676 |
|
7677 // "If container is a dd or dt, and it is not an allowed child of |
|
7678 // any of its ancestors in the same editing host, set the tag name |
|
7679 // of container to the default single-line container name and let |
|
7680 // container be the result." |
|
7681 if (isHtmlElement(container, ["dd", "dt"]) |
|
7682 && getAncestors(container).every(function(ancestor) { |
|
7683 return !inSameEditingHost(container, ancestor) |
|
7684 || !isAllowedChild(container, ancestor) |
|
7685 })) { |
|
7686 container = setTagName(container, defaultSingleLineContainerName); |
|
7687 } |
|
7688 |
|
7689 // "Fix disallowed ancestors of container." |
|
7690 fixDisallowedAncestors(container); |
|
7691 |
|
7692 // "Return true." |
|
7693 return true; |
|
7694 } |
|
7695 |
|
7696 // "Let new line range be a new range whose start is the same as |
|
7697 // the active range's, and whose end is (container, length of |
|
7698 // container)." |
|
7699 var newLineRange = document.createRange(); |
|
7700 newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset); |
|
7701 newLineRange.setEnd(container, getNodeLength(container)); |
|
7702 |
|
7703 // "While new line range's start offset is zero and its start node is |
|
7704 // not a prohibited paragraph child, set its start to (parent of start |
|
7705 // node, index of start node)." |
|
7706 while (newLineRange.startOffset == 0 |
|
7707 && !isProhibitedParagraphChild(newLineRange.startContainer)) { |
|
7708 newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer)); |
|
7709 } |
|
7710 |
|
7711 // "While new line range's start offset is the length of its start node |
|
7712 // and its start node is not a prohibited paragraph child, set its |
|
7713 // start to (parent of start node, 1 + index of start node)." |
|
7714 while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer) |
|
7715 && !isProhibitedParagraphChild(newLineRange.startContainer)) { |
|
7716 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer)); |
|
7717 } |
|
7718 |
|
7719 // "Let end of line be true if new line range contains either nothing |
|
7720 // or a single br, and false otherwise." |
|
7721 var containedInNewLineRange = getContainedNodes(newLineRange); |
|
7722 var endOfLine = !containedInNewLineRange.length |
|
7723 || (containedInNewLineRange.length == 1 |
|
7724 && isHtmlElement(containedInNewLineRange[0], "br")); |
|
7725 |
|
7726 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or |
|
7727 // "h6", and end of line is true, let new container name be the default |
|
7728 // single-line container name." |
|
7729 var newContainerName; |
|
7730 if (/^H[1-6]$/.test(container.tagName) |
|
7731 && endOfLine) { |
|
7732 newContainerName = defaultSingleLineContainerName; |
|
7733 |
|
7734 // "Otherwise, if the local name of container is "dt" and end of line |
|
7735 // is true, let new container name be "dd"." |
|
7736 } else if (container.tagName == "DT" |
|
7737 && endOfLine) { |
|
7738 newContainerName = "dd"; |
|
7739 |
|
7740 // "Otherwise, if the local name of container is "dd" and end of line |
|
7741 // is true, let new container name be "dt"." |
|
7742 } else if (container.tagName == "DD" |
|
7743 && endOfLine) { |
|
7744 newContainerName = "dt"; |
|
7745 |
|
7746 // "Otherwise, let new container name be the local name of container." |
|
7747 } else { |
|
7748 newContainerName = container.tagName.toLowerCase(); |
|
7749 } |
|
7750 |
|
7751 // "Let new container be the result of calling createElement(new |
|
7752 // container name) on the context object." |
|
7753 var newContainer = document.createElement(newContainerName); |
|
7754 |
|
7755 // "Copy all attributes of container to new container." |
|
7756 for (var i = 0; i < container.attributes.length; i++) { |
|
7757 newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value); |
|
7758 } |
|
7759 |
|
7760 // "If new container has an id attribute, unset it." |
|
7761 newContainer.removeAttribute("id"); |
|
7762 |
|
7763 // "Insert new container into the parent of container immediately after |
|
7764 // container." |
|
7765 container.parentNode.insertBefore(newContainer, container.nextSibling); |
|
7766 |
|
7767 // "Let contained nodes be all nodes contained in new line range." |
|
7768 var containedNodes = getAllContainedNodes(newLineRange); |
|
7769 |
|
7770 // "Let frag be the result of calling extractContents() on new line |
|
7771 // range." |
|
7772 var frag = newLineRange.extractContents(); |
|
7773 |
|
7774 // "Unset the id attribute (if any) of each Element descendant of frag |
|
7775 // that is not in contained nodes." |
|
7776 var descendants = getDescendants(frag); |
|
7777 for (var i = 0; i < descendants.length; i++) { |
|
7778 if (descendants[i].nodeType == Node.ELEMENT_NODE |
|
7779 && containedNodes.indexOf(descendants[i]) == -1) { |
|
7780 descendants[i].removeAttribute("id"); |
|
7781 } |
|
7782 } |
|
7783 |
|
7784 // "Call appendChild(frag) on new container." |
|
7785 newContainer.appendChild(frag); |
|
7786 |
|
7787 // "While container's lastChild is a prohibited paragraph child, set |
|
7788 // container to its lastChild." |
|
7789 while (isProhibitedParagraphChild(container.lastChild)) { |
|
7790 container = container.lastChild; |
|
7791 } |
|
7792 |
|
7793 // "While new container's lastChild is a prohibited paragraph child, |
|
7794 // set new container to its lastChild." |
|
7795 while (isProhibitedParagraphChild(newContainer.lastChild)) { |
|
7796 newContainer = newContainer.lastChild; |
|
7797 } |
|
7798 |
|
7799 // "If container has no visible children, call createElement("br") on |
|
7800 // the context object, and append the result as the last child of |
|
7801 // container." |
|
7802 if (![].some.call(container.childNodes, isVisible)) { |
|
7803 container.appendChild(document.createElement("br")); |
|
7804 } |
|
7805 |
|
7806 // "If new container has no visible children, call createElement("br") |
|
7807 // on the context object, and append the result as the last child of |
|
7808 // new container." |
|
7809 if (![].some.call(newContainer.childNodes, isVisible)) { |
|
7810 newContainer.appendChild(document.createElement("br")); |
|
7811 } |
|
7812 |
|
7813 // "Call collapse(new container, 0) on the context object's Selection." |
|
7814 getSelection().collapse(newContainer, 0); |
|
7815 getActiveRange().setStart(newContainer, 0); |
|
7816 getActiveRange().setEnd(newContainer, 0); |
|
7817 |
|
7818 // "Return true." |
|
7819 return true; |
|
7820 } |
|
7821 }; |
|
7822 |
|
7823 //@} |
|
7824 ///// The insertText command ///// |
|
7825 //@{ |
|
7826 commands.inserttext = { |
|
7827 action: function(value) { |
|
7828 // "Delete the selection, with strip wrappers false." |
|
7829 deleteSelection({stripWrappers: false}); |
|
7830 |
|
7831 // "If the active range's start node is neither editable nor an editing |
|
7832 // host, return true." |
|
7833 if (!isEditable(getActiveRange().startContainer) |
|
7834 && !isEditingHost(getActiveRange().startContainer)) { |
|
7835 return true; |
|
7836 } |
|
7837 |
|
7838 // "If value's length is greater than one:" |
|
7839 if (value.length > 1) { |
|
7840 // "For each element el in value, take the action for the |
|
7841 // insertText command, with value equal to el." |
|
7842 for (var i = 0; i < value.length; i++) { |
|
7843 commands.inserttext.action(value[i]); |
|
7844 } |
|
7845 |
|
7846 // "Return true." |
|
7847 return true; |
|
7848 } |
|
7849 |
|
7850 // "If value is the empty string, return true." |
|
7851 if (value == "") { |
|
7852 return true; |
|
7853 } |
|
7854 |
|
7855 // "If value is a newline (U+00A0), take the action for the |
|
7856 // insertParagraph command and return true." |
|
7857 if (value == "\n") { |
|
7858 commands.insertparagraph.action(); |
|
7859 return true; |
|
7860 } |
|
7861 |
|
7862 // "Let node and offset be the active range's start node and offset." |
|
7863 var node = getActiveRange().startContainer; |
|
7864 var offset = getActiveRange().startOffset; |
|
7865 |
|
7866 // "If node has a child whose index is offset − 1, and that child is a |
|
7867 // Text node, set node to that child, then set offset to node's |
|
7868 // length." |
|
7869 if (0 <= offset - 1 |
|
7870 && offset - 1 < node.childNodes.length |
|
7871 && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) { |
|
7872 node = node.childNodes[offset - 1]; |
|
7873 offset = getNodeLength(node); |
|
7874 } |
|
7875 |
|
7876 // "If node has a child whose index is offset, and that child is a Text |
|
7877 // node, set node to that child, then set offset to zero." |
|
7878 if (0 <= offset |
|
7879 && offset < node.childNodes.length |
|
7880 && node.childNodes[offset].nodeType == Node.TEXT_NODE) { |
|
7881 node = node.childNodes[offset]; |
|
7882 offset = 0; |
|
7883 } |
|
7884 |
|
7885 // "Record current overrides, and let overrides be the result." |
|
7886 var overrides = recordCurrentOverrides(); |
|
7887 |
|
7888 // "Call collapse(node, offset) on the context object's Selection." |
|
7889 getSelection().collapse(node, offset); |
|
7890 getActiveRange().setStart(node, offset); |
|
7891 getActiveRange().setEnd(node, offset); |
|
7892 |
|
7893 // "Canonicalize whitespace at (node, offset)." |
|
7894 canonicalizeWhitespace(node, offset); |
|
7895 |
|
7896 // "Let (node, offset) be the active range's start." |
|
7897 node = getActiveRange().startContainer; |
|
7898 offset = getActiveRange().startOffset; |
|
7899 |
|
7900 // "If node is a Text node:" |
|
7901 if (node.nodeType == Node.TEXT_NODE) { |
|
7902 // "Call insertData(offset, value) on node." |
|
7903 node.insertData(offset, value); |
|
7904 |
|
7905 // "Call collapse(node, offset) on the context object's Selection." |
|
7906 getSelection().collapse(node, offset); |
|
7907 getActiveRange().setStart(node, offset); |
|
7908 |
|
7909 // "Call extend(node, offset + 1) on the context object's |
|
7910 // Selection." |
|
7911 // |
|
7912 // Work around WebKit bug: the extend() can throw if the text we're |
|
7913 // adding is trailing whitespace. |
|
7914 try { getSelection().extend(node, offset + 1); } catch(e) {} |
|
7915 getActiveRange().setEnd(node, offset + 1); |
|
7916 |
|
7917 // "Otherwise:" |
|
7918 } else { |
|
7919 // "If node has only one child, which is a collapsed line break, |
|
7920 // remove its child from it." |
|
7921 // |
|
7922 // FIXME: IE incorrectly returns false here instead of true |
|
7923 // sometimes? |
|
7924 if (node.childNodes.length == 1 |
|
7925 && isCollapsedLineBreak(node.firstChild)) { |
|
7926 node.removeChild(node.firstChild); |
|
7927 } |
|
7928 |
|
7929 // "Let text be the result of calling createTextNode(value) on the |
|
7930 // context object." |
|
7931 var text = document.createTextNode(value); |
|
7932 |
|
7933 // "Call insertNode(text) on the active range." |
|
7934 getActiveRange().insertNode(text); |
|
7935 |
|
7936 // "Call collapse(text, 0) on the context object's Selection." |
|
7937 getSelection().collapse(text, 0); |
|
7938 getActiveRange().setStart(text, 0); |
|
7939 |
|
7940 // "Call extend(text, 1) on the context object's Selection." |
|
7941 getSelection().extend(text, 1); |
|
7942 getActiveRange().setEnd(text, 1); |
|
7943 } |
|
7944 |
|
7945 // "Restore states and values from overrides." |
|
7946 restoreStatesAndValues(overrides); |
|
7947 |
|
7948 // "Canonicalize whitespace at the active range's start, with fix |
|
7949 // collapsed space false." |
|
7950 canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false); |
|
7951 |
|
7952 // "Canonicalize whitespace at the active range's end, with fix |
|
7953 // collapsed space false." |
|
7954 canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false); |
|
7955 |
|
7956 // "If value is a space character, autolink the active range's start." |
|
7957 if (/^[ \t\n\f\r]$/.test(value)) { |
|
7958 autolink(getActiveRange().startContainer, getActiveRange().startOffset); |
|
7959 } |
|
7960 |
|
7961 // "Call collapseToEnd() on the context object's Selection." |
|
7962 // |
|
7963 // Work around WebKit bug: sometimes it blows up the selection and |
|
7964 // throws, which we don't want. |
|
7965 try { getSelection().collapseToEnd(); } catch(e) {} |
|
7966 getActiveRange().collapse(false); |
|
7967 |
|
7968 // "Return true." |
|
7969 return true; |
|
7970 } |
|
7971 }; |
|
7972 |
|
7973 //@} |
|
7974 ///// The insertUnorderedList command ///// |
|
7975 //@{ |
|
7976 commands.insertunorderedlist = { |
|
7977 preservesOverrides: true, |
|
7978 // "Toggle lists with tag name "ul", then return true." |
|
7979 action: function() { toggleLists("ul"); return true }, |
|
7980 // "True if the selection's list state is "mixed" or "mixed ul", false |
|
7981 // otherwise." |
|
7982 indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) }, |
|
7983 // "True if the selection's list state is "ul", false otherwise." |
|
7984 state: function() { return getSelectionListState() == "ul" }, |
|
7985 }; |
|
7986 |
|
7987 //@} |
|
7988 ///// The justifyCenter command ///// |
|
7989 //@{ |
|
7990 commands.justifycenter = { |
|
7991 preservesOverrides: true, |
|
7992 // "Justify the selection with alignment "center", then return true." |
|
7993 action: function() { justifySelection("center"); return true }, |
|
7994 indeterm: function() { |
|
7995 // "Return false if the active range is null. Otherwise, block-extend |
|
7996 // the active range. Return true if among visible editable nodes that |
|
7997 // are contained in the result and have no children, at least one has |
|
7998 // alignment value "center" and at least one does not. Otherwise return |
|
7999 // false." |
|
8000 if (!getActiveRange()) { |
|
8001 return false; |
|
8002 } |
|
8003 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8004 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8005 }); |
|
8006 return nodes.some(function(node) { return getAlignmentValue(node) == "center" }) |
|
8007 && nodes.some(function(node) { return getAlignmentValue(node) != "center" }); |
|
8008 }, state: function() { |
|
8009 // "Return false if the active range is null. Otherwise, block-extend |
|
8010 // the active range. Return true if there is at least one visible |
|
8011 // editable node that is contained in the result and has no children, |
|
8012 // and all such nodes have alignment value "center". Otherwise return |
|
8013 // false." |
|
8014 if (!getActiveRange()) { |
|
8015 return false; |
|
8016 } |
|
8017 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8018 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8019 }); |
|
8020 return nodes.length |
|
8021 && nodes.every(function(node) { return getAlignmentValue(node) == "center" }); |
|
8022 }, value: function() { |
|
8023 // "Return the empty string if the active range is null. Otherwise, |
|
8024 // block-extend the active range, and return the alignment value of the |
|
8025 // first visible editable node that is contained in the result and has |
|
8026 // no children. If there is no such node, return "left"." |
|
8027 if (!getActiveRange()) { |
|
8028 return ""; |
|
8029 } |
|
8030 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8031 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8032 }); |
|
8033 if (nodes.length) { |
|
8034 return getAlignmentValue(nodes[0]); |
|
8035 } else { |
|
8036 return "left"; |
|
8037 } |
|
8038 }, |
|
8039 }; |
|
8040 |
|
8041 //@} |
|
8042 ///// The justifyFull command ///// |
|
8043 //@{ |
|
8044 commands.justifyfull = { |
|
8045 preservesOverrides: true, |
|
8046 // "Justify the selection with alignment "justify", then return true." |
|
8047 action: function() { justifySelection("justify"); return true }, |
|
8048 indeterm: function() { |
|
8049 // "Return false if the active range is null. Otherwise, block-extend |
|
8050 // the active range. Return true if among visible editable nodes that |
|
8051 // are contained in the result and have no children, at least one has |
|
8052 // alignment value "justify" and at least one does not. Otherwise |
|
8053 // return false." |
|
8054 if (!getActiveRange()) { |
|
8055 return false; |
|
8056 } |
|
8057 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8058 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8059 }); |
|
8060 return nodes.some(function(node) { return getAlignmentValue(node) == "justify" }) |
|
8061 && nodes.some(function(node) { return getAlignmentValue(node) != "justify" }); |
|
8062 }, state: function() { |
|
8063 // "Return false if the active range is null. Otherwise, block-extend |
|
8064 // the active range. Return true if there is at least one visible |
|
8065 // editable node that is contained in the result and has no children, |
|
8066 // and all such nodes have alignment value "justify". Otherwise return |
|
8067 // false." |
|
8068 if (!getActiveRange()) { |
|
8069 return false; |
|
8070 } |
|
8071 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8072 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8073 }); |
|
8074 return nodes.length |
|
8075 && nodes.every(function(node) { return getAlignmentValue(node) == "justify" }); |
|
8076 }, value: function() { |
|
8077 // "Return the empty string if the active range is null. Otherwise, |
|
8078 // block-extend the active range, and return the alignment value of the |
|
8079 // first visible editable node that is contained in the result and has |
|
8080 // no children. If there is no such node, return "left"." |
|
8081 if (!getActiveRange()) { |
|
8082 return ""; |
|
8083 } |
|
8084 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8085 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8086 }); |
|
8087 if (nodes.length) { |
|
8088 return getAlignmentValue(nodes[0]); |
|
8089 } else { |
|
8090 return "left"; |
|
8091 } |
|
8092 }, |
|
8093 }; |
|
8094 |
|
8095 //@} |
|
8096 ///// The justifyLeft command ///// |
|
8097 //@{ |
|
8098 commands.justifyleft = { |
|
8099 preservesOverrides: true, |
|
8100 // "Justify the selection with alignment "left", then return true." |
|
8101 action: function() { justifySelection("left"); return true }, |
|
8102 indeterm: function() { |
|
8103 // "Return false if the active range is null. Otherwise, block-extend |
|
8104 // the active range. Return true if among visible editable nodes that |
|
8105 // are contained in the result and have no children, at least one has |
|
8106 // alignment value "left" and at least one does not. Otherwise return |
|
8107 // false." |
|
8108 if (!getActiveRange()) { |
|
8109 return false; |
|
8110 } |
|
8111 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8112 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8113 }); |
|
8114 return nodes.some(function(node) { return getAlignmentValue(node) == "left" }) |
|
8115 && nodes.some(function(node) { return getAlignmentValue(node) != "left" }); |
|
8116 }, state: function() { |
|
8117 // "Return false if the active range is null. Otherwise, block-extend |
|
8118 // the active range. Return true if there is at least one visible |
|
8119 // editable node that is contained in the result and has no children, |
|
8120 // and all such nodes have alignment value "left". Otherwise return |
|
8121 // false." |
|
8122 if (!getActiveRange()) { |
|
8123 return false; |
|
8124 } |
|
8125 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8126 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8127 }); |
|
8128 return nodes.length |
|
8129 && nodes.every(function(node) { return getAlignmentValue(node) == "left" }); |
|
8130 }, value: function() { |
|
8131 // "Return the empty string if the active range is null. Otherwise, |
|
8132 // block-extend the active range, and return the alignment value of the |
|
8133 // first visible editable node that is contained in the result and has |
|
8134 // no children. If there is no such node, return "left"." |
|
8135 if (!getActiveRange()) { |
|
8136 return ""; |
|
8137 } |
|
8138 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8139 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8140 }); |
|
8141 if (nodes.length) { |
|
8142 return getAlignmentValue(nodes[0]); |
|
8143 } else { |
|
8144 return "left"; |
|
8145 } |
|
8146 }, |
|
8147 }; |
|
8148 |
|
8149 //@} |
|
8150 ///// The justifyRight command ///// |
|
8151 //@{ |
|
8152 commands.justifyright = { |
|
8153 preservesOverrides: true, |
|
8154 // "Justify the selection with alignment "right", then return true." |
|
8155 action: function() { justifySelection("right"); return true }, |
|
8156 indeterm: function() { |
|
8157 // "Return false if the active range is null. Otherwise, block-extend |
|
8158 // the active range. Return true if among visible editable nodes that |
|
8159 // are contained in the result and have no children, at least one has |
|
8160 // alignment value "right" and at least one does not. Otherwise return |
|
8161 // false." |
|
8162 if (!getActiveRange()) { |
|
8163 return false; |
|
8164 } |
|
8165 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8166 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8167 }); |
|
8168 return nodes.some(function(node) { return getAlignmentValue(node) == "right" }) |
|
8169 && nodes.some(function(node) { return getAlignmentValue(node) != "right" }); |
|
8170 }, state: function() { |
|
8171 // "Return false if the active range is null. Otherwise, block-extend |
|
8172 // the active range. Return true if there is at least one visible |
|
8173 // editable node that is contained in the result and has no children, |
|
8174 // and all such nodes have alignment value "right". Otherwise return |
|
8175 // false." |
|
8176 if (!getActiveRange()) { |
|
8177 return false; |
|
8178 } |
|
8179 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8180 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8181 }); |
|
8182 return nodes.length |
|
8183 && nodes.every(function(node) { return getAlignmentValue(node) == "right" }); |
|
8184 }, value: function() { |
|
8185 // "Return the empty string if the active range is null. Otherwise, |
|
8186 // block-extend the active range, and return the alignment value of the |
|
8187 // first visible editable node that is contained in the result and has |
|
8188 // no children. If there is no such node, return "left"." |
|
8189 if (!getActiveRange()) { |
|
8190 return ""; |
|
8191 } |
|
8192 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
|
8193 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
|
8194 }); |
|
8195 if (nodes.length) { |
|
8196 return getAlignmentValue(nodes[0]); |
|
8197 } else { |
|
8198 return "left"; |
|
8199 } |
|
8200 }, |
|
8201 }; |
|
8202 |
|
8203 //@} |
|
8204 ///// The outdent command ///// |
|
8205 //@{ |
|
8206 commands.outdent = { |
|
8207 preservesOverrides: true, |
|
8208 action: function() { |
|
8209 // "Let items be a list of all lis that are ancestor containers of the |
|
8210 // range's start and/or end node." |
|
8211 // |
|
8212 // It's annoying to get this in tree order using functional stuff |
|
8213 // without doing getDescendants(document), which is slow, so I do it |
|
8214 // imperatively. |
|
8215 var items = []; |
|
8216 (function(){ |
|
8217 for ( |
|
8218 var ancestorContainer = getActiveRange().endContainer; |
|
8219 ancestorContainer != getActiveRange().commonAncestorContainer; |
|
8220 ancestorContainer = ancestorContainer.parentNode |
|
8221 ) { |
|
8222 if (isHtmlElement(ancestorContainer, "li")) { |
|
8223 items.unshift(ancestorContainer); |
|
8224 } |
|
8225 } |
|
8226 for ( |
|
8227 var ancestorContainer = getActiveRange().startContainer; |
|
8228 ancestorContainer; |
|
8229 ancestorContainer = ancestorContainer.parentNode |
|
8230 ) { |
|
8231 if (isHtmlElement(ancestorContainer, "li")) { |
|
8232 items.unshift(ancestorContainer); |
|
8233 } |
|
8234 } |
|
8235 })(); |
|
8236 |
|
8237 // "For each item in items, normalize sublists of item." |
|
8238 items.forEach(normalizeSublists); |
|
8239 |
|
8240 // "Block-extend the active range, and let new range be the result." |
|
8241 var newRange = blockExtend(getActiveRange()); |
|
8242 |
|
8243 // "Let node list be a list of nodes, initially empty." |
|
8244 // |
|
8245 // "For each node node contained in new range, append node to node list |
|
8246 // if the last member of node list (if any) is not an ancestor of node; |
|
8247 // node is editable; and either node has no editable descendants, or is |
|
8248 // an ol or ul, or is an li whose parent is an ol or ul." |
|
8249 var nodeList = getContainedNodes(newRange, function(node) { |
|
8250 return isEditable(node) |
|
8251 && (!getDescendants(node).some(isEditable) |
|
8252 || isHtmlElement(node, ["ol", "ul"]) |
|
8253 || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"]))); |
|
8254 }); |
|
8255 |
|
8256 // "While node list is not empty:" |
|
8257 while (nodeList.length) { |
|
8258 // "While the first member of node list is an ol or ul or is not |
|
8259 // the child of an ol or ul, outdent it and remove it from node |
|
8260 // list." |
|
8261 while (nodeList.length |
|
8262 && (isHtmlElement(nodeList[0], ["OL", "UL"]) |
|
8263 || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) { |
|
8264 outdentNode(nodeList.shift()); |
|
8265 } |
|
8266 |
|
8267 // "If node list is empty, break from these substeps." |
|
8268 if (!nodeList.length) { |
|
8269 break; |
|
8270 } |
|
8271 |
|
8272 // "Let sublist be a list of nodes, initially empty." |
|
8273 var sublist = []; |
|
8274 |
|
8275 // "Remove the first member of node list and append it to sublist." |
|
8276 sublist.push(nodeList.shift()); |
|
8277 |
|
8278 // "While the first member of node list is the nextSibling of the |
|
8279 // last member of sublist, and the first member of node list is not |
|
8280 // an ol or ul, remove the first member of node list and append it |
|
8281 // to sublist." |
|
8282 while (nodeList.length |
|
8283 && nodeList[0] == sublist[sublist.length - 1].nextSibling |
|
8284 && !isHtmlElement(nodeList[0], ["OL", "UL"])) { |
|
8285 sublist.push(nodeList.shift()); |
|
8286 } |
|
8287 |
|
8288 // "Record the values of sublist, and let values be the result." |
|
8289 var values = recordValues(sublist); |
|
8290 |
|
8291 // "Split the parent of sublist, with new parent null." |
|
8292 splitParent(sublist); |
|
8293 |
|
8294 // "Fix disallowed ancestors of each member of sublist." |
|
8295 sublist.forEach(fixDisallowedAncestors); |
|
8296 |
|
8297 // "Restore the values from values." |
|
8298 restoreValues(values); |
|
8299 } |
|
8300 |
|
8301 // "Return true." |
|
8302 return true; |
|
8303 } |
|
8304 }; |
|
8305 |
|
8306 //@} |
|
8307 |
|
8308 ////////////////////////////////// |
|
8309 ///// Miscellaneous commands ///// |
|
8310 ////////////////////////////////// |
|
8311 |
|
8312 ///// The defaultParagraphSeparator command ///// |
|
8313 //@{ |
|
8314 commands.defaultparagraphseparator = { |
|
8315 action: function(value) { |
|
8316 // "Let value be converted to ASCII lowercase. If value is then equal |
|
8317 // to "p" or "div", set the context object's default single-line |
|
8318 // container name to value and return true. Otherwise, return false." |
|
8319 value = value.toLowerCase(); |
|
8320 if (value == "p" || value == "div") { |
|
8321 defaultSingleLineContainerName = value; |
|
8322 return true; |
|
8323 } |
|
8324 return false; |
|
8325 }, value: function() { |
|
8326 // "Return the context object's default single-line container name." |
|
8327 return defaultSingleLineContainerName; |
|
8328 }, |
|
8329 }; |
|
8330 |
|
8331 //@} |
|
8332 ///// The selectAll command ///// |
|
8333 //@{ |
|
8334 commands.selectall = { |
|
8335 // Note, this ignores the whole globalRange/getActiveRange() thing and |
|
8336 // works with actual selections. Not suitable for autoimplementation.html. |
|
8337 action: function() { |
|
8338 // "Let target be the body element of the context object." |
|
8339 var target = document.body; |
|
8340 |
|
8341 // "If target is null, let target be the context object's |
|
8342 // documentElement." |
|
8343 if (!target) { |
|
8344 target = document.documentElement; |
|
8345 } |
|
8346 |
|
8347 // "If target is null, call getSelection() on the context object, and |
|
8348 // call removeAllRanges() on the result." |
|
8349 if (!target) { |
|
8350 getSelection().removeAllRanges(); |
|
8351 |
|
8352 // "Otherwise, call getSelection() on the context object, and call |
|
8353 // selectAllChildren(target) on the result." |
|
8354 } else { |
|
8355 getSelection().selectAllChildren(target); |
|
8356 } |
|
8357 |
|
8358 // "Return true." |
|
8359 return true; |
|
8360 } |
|
8361 }; |
|
8362 |
|
8363 //@} |
|
8364 ///// The styleWithCSS command ///// |
|
8365 //@{ |
|
8366 commands.stylewithcss = { |
|
8367 action: function(value) { |
|
8368 // "If value is an ASCII case-insensitive match for the string |
|
8369 // "false", set the CSS styling flag to false. Otherwise, set the |
|
8370 // CSS styling flag to true. Either way, return true." |
|
8371 cssStylingFlag = String(value).toLowerCase() != "false"; |
|
8372 return true; |
|
8373 }, state: function() { return cssStylingFlag } |
|
8374 }; |
|
8375 |
|
8376 //@} |
|
8377 ///// The useCSS command ///// |
|
8378 //@{ |
|
8379 commands.usecss = { |
|
8380 action: function(value) { |
|
8381 // "If value is an ASCII case-insensitive match for the string "false", |
|
8382 // set the CSS styling flag to true. Otherwise, set the CSS styling |
|
8383 // flag to false. Either way, return true." |
|
8384 cssStylingFlag = String(value).toLowerCase() == "false"; |
|
8385 return true; |
|
8386 } |
|
8387 }; |
|
8388 //@} |
|
8389 |
|
8390 // Some final setup |
|
8391 //@{ |
|
8392 (function() { |
|
8393 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit |
|
8394 // temporary, which means I need an extra closure to not leak the temporaries |
|
8395 // into the global namespace. >:( |
|
8396 var commandNames = []; |
|
8397 for (var command in commands) { |
|
8398 commandNames.push(command); |
|
8399 } |
|
8400 commandNames.forEach(function(command) { |
|
8401 // "If a command does not have a relevant CSS property specified, it |
|
8402 // defaults to null." |
|
8403 if (!("relevantCssProperty" in commands[command])) { |
|
8404 commands[command].relevantCssProperty = null; |
|
8405 } |
|
8406 |
|
8407 // "If a command has inline command activated values defined but nothing |
|
8408 // else defines when it is indeterminate, it is indeterminate if among |
|
8409 // formattable nodes effectively contained in the active range, there is at |
|
8410 // least one whose effective command value is one of the given values and |
|
8411 // at least one whose effective command value is not one of the given |
|
8412 // values." |
|
8413 if ("inlineCommandActivatedValues" in commands[command] |
|
8414 && !("indeterm" in commands[command])) { |
|
8415 commands[command].indeterm = function() { |
|
8416 if (!getActiveRange()) { |
|
8417 return false; |
|
8418 } |
|
8419 |
|
8420 var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode) |
|
8421 .map(function(node) { return getEffectiveCommandValue(node, command) }); |
|
8422 |
|
8423 var matchingValues = values.filter(function(value) { |
|
8424 return commands[command].inlineCommandActivatedValues.indexOf(value) != -1; |
|
8425 }); |
|
8426 |
|
8427 return matchingValues.length >= 1 |
|
8428 && values.length - matchingValues.length >= 1; |
|
8429 }; |
|
8430 } |
|
8431 |
|
8432 // "If a command has inline command activated values defined, its state is |
|
8433 // true if either no formattable node is effectively contained in the |
|
8434 // active range, and the active range's start node's effective command |
|
8435 // value is one of the given values; or if there is at least one |
|
8436 // formattable node effectively contained in the active range, and all of |
|
8437 // them have an effective command value equal to one of the given values." |
|
8438 if ("inlineCommandActivatedValues" in commands[command]) { |
|
8439 commands[command].state = function() { |
|
8440 if (!getActiveRange()) { |
|
8441 return false; |
|
8442 } |
|
8443 |
|
8444 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); |
|
8445 |
|
8446 if (nodes.length == 0) { |
|
8447 return commands[command].inlineCommandActivatedValues |
|
8448 .indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1; |
|
8449 } else { |
|
8450 return nodes.every(function(node) { |
|
8451 return commands[command].inlineCommandActivatedValues |
|
8452 .indexOf(getEffectiveCommandValue(node, command)) != -1; |
|
8453 }); |
|
8454 } |
|
8455 }; |
|
8456 } |
|
8457 |
|
8458 // "If a command is a standard inline value command, it is indeterminate if |
|
8459 // among formattable nodes that are effectively contained in the active |
|
8460 // range, there are two that have distinct effective command values. Its |
|
8461 // value is the effective command value of the first formattable node that |
|
8462 // is effectively contained in the active range; or if there is no such |
|
8463 // node, the effective command value of the active range's start node; or |
|
8464 // if that is null, the empty string." |
|
8465 if ("standardInlineValueCommand" in commands[command]) { |
|
8466 commands[command].indeterm = function() { |
|
8467 if (!getActiveRange()) { |
|
8468 return false; |
|
8469 } |
|
8470 |
|
8471 var values = getAllEffectivelyContainedNodes(getActiveRange()) |
|
8472 .filter(isFormattableNode) |
|
8473 .map(function(node) { return getEffectiveCommandValue(node, command) }); |
|
8474 for (var i = 1; i < values.length; i++) { |
|
8475 if (values[i] != values[i - 1]) { |
|
8476 return true; |
|
8477 } |
|
8478 } |
|
8479 return false; |
|
8480 }; |
|
8481 |
|
8482 commands[command].value = function() { |
|
8483 if (!getActiveRange()) { |
|
8484 return ""; |
|
8485 } |
|
8486 |
|
8487 var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0]; |
|
8488 |
|
8489 if (typeof refNode == "undefined") { |
|
8490 refNode = getActiveRange().startContainer; |
|
8491 } |
|
8492 |
|
8493 var ret = getEffectiveCommandValue(refNode, command); |
|
8494 if (ret === null) { |
|
8495 return ""; |
|
8496 } |
|
8497 return ret; |
|
8498 }; |
|
8499 } |
|
8500 |
|
8501 // "If a command preserves overrides, then before taking its action, the |
|
8502 // user agent must record current overrides. After taking the action, if |
|
8503 // the active range is collapsed, it must restore states and values from |
|
8504 // the recorded list." |
|
8505 if ("preservesOverrides" in commands[command]) { |
|
8506 var oldAction = commands[command].action; |
|
8507 |
|
8508 commands[command].action = function(value) { |
|
8509 var overrides = recordCurrentOverrides(); |
|
8510 var ret = oldAction(value); |
|
8511 if (getActiveRange().collapsed) { |
|
8512 restoreStatesAndValues(overrides); |
|
8513 } |
|
8514 return ret; |
|
8515 }; |
|
8516 } |
|
8517 }); |
|
8518 })(); |
|
8519 //@} |
|
8520 |
|
8521 // vim: foldmarker=@{,@} foldmethod=marker |