1 define( 2 //['aloha/ecma5'], 3 ['aloha/ecma5shims', 'jquery'], 4 function($_, jQuery) { 5 "use strict"; 6 7 function hasAttribute(obj, attr){ 8 var native_method = obj.hasAttribute; 9 if(native_method){ 10 return obj.hasAttribute(attr); 11 } 12 else { 13 return (typeof obj.attributes[attr] != "undefined") 14 } 15 } 16 17 var htmlNamespace = "http://www.w3.org/1999/xhtml"; 18 19 var cssStylingFlag = false; 20 21 // This is bad :( 22 var globalRange = null; 23 24 // Commands are stored in a dictionary where we call their actions and such 25 var commands = {}; 26 27 /////////////////////////////////////////////////////////////////////////////// 28 ////////////////////////////// Utility functions ////////////////////////////// 29 /////////////////////////////////////////////////////////////////////////////// 30 //@{ 31 32 /** 33 * Method to count the number of styles in the given style 34 */ 35 function getStyleLength(node) { 36 if (!node) { 37 return 0; 38 } else if (!node.style) { 39 return 0; 40 } 41 42 // some browsers support .length on styles 43 if (typeof node.style.length !== 'undefined') { 44 return node.style.length; 45 } else { 46 // others don't, so we will count 47 var styleLength = 0; 48 for (var s in node.style) { 49 if (node.style[s] && node.style[s] !== 0 && node.style[s] !== 'false') { 50 styleLength++; 51 } 52 } 53 54 return styleLength; 55 } 56 } 57 58 function toArray(obj) { 59 if (!obj) { 60 return null; 61 } 62 var array = [], i, l = obj.length; 63 // iterate backwards ensuring that length is an UInt32 64 for (i = l >>> 0; i--;) { 65 array[i] = obj[i]; 66 } 67 return array; 68 } 69 70 function nextNode(node) { 71 if (node.hasChildNodes()) { 72 return node.firstChild; 73 } 74 return nextNodeDescendants(node); 75 } 76 77 function previousNode(node) { 78 if (node.previousSibling) { 79 node = node.previousSibling; 80 while (node.hasChildNodes()) { 81 node = node.lastChild; 82 } 83 return node; 84 } 85 if (node.parentNode 86 && node.parentNode.nodeType == $_.Node.ELEMENT_NODE) { 87 return node.parentNode; 88 } 89 return null; 90 } 91 92 function nextNodeDescendants(node) { 93 while (node && !node.nextSibling) { 94 node = node.parentNode; 95 } 96 if (!node) { 97 return null; 98 } 99 return node.nextSibling; 100 } 101 102 /** 103 * Returns true if ancestor is an ancestor of descendant, false otherwise. 104 */ 105 function isAncestor(ancestor, descendant) { 106 return ancestor 107 && descendant 108 && Boolean($_.compareDocumentPosition(ancestor, descendant) & $_.Node.DOCUMENT_POSITION_CONTAINED_BY); 109 } 110 111 /** 112 * Returns true if ancestor is an ancestor of or equal to descendant, false 113 * otherwise. 114 */ 115 function isAncestorContainer(ancestor, descendant) { 116 return (ancestor || descendant) 117 && (ancestor == descendant || isAncestor(ancestor, descendant)); 118 } 119 120 /** 121 * Returns true if descendant is a descendant of ancestor, false otherwise. 122 */ 123 function isDescendant(descendant, ancestor) { 124 return ancestor 125 && descendant 126 && Boolean($_.compareDocumentPosition(ancestor, descendant) & $_.Node.DOCUMENT_POSITION_CONTAINED_BY); 127 } 128 129 /** 130 * Returns true if node1 is before node2 in tree order, false otherwise. 131 */ 132 function isBefore(node1, node2) { 133 return Boolean($_.compareDocumentPosition(node1,node2) & $_.Node.DOCUMENT_POSITION_FOLLOWING); 134 } 135 136 /** 137 * Returns true if node1 is after node2 in tree order, false otherwise. 138 */ 139 function isAfter(node1, node2) { 140 return Boolean($_.compareDocumentPosition(node1,node2) & $_.Node.DOCUMENT_POSITION_PRECEDING); 141 } 142 143 function getAncestors(node) { 144 var ancestors = []; 145 while (node.parentNode) { 146 ancestors.unshift(node.parentNode); 147 node = node.parentNode; 148 } 149 return ancestors; 150 } 151 152 function getDescendants(node) { 153 var descendants = []; 154 var stop = nextNodeDescendants(node); 155 while ((node = nextNode(node)) 156 && node != stop) { 157 descendants.push(node); 158 } 159 return descendants; 160 } 161 162 function convertProperty(property) { 163 // Special-case for now 164 var map = { 165 "fontFamily": "font-family", 166 "fontSize": "font-size", 167 "fontStyle": "font-style", 168 "fontWeight": "font-weight", 169 "textDecoration": "text-decoration" 170 }; 171 if (typeof map[property] != "undefined") { 172 return map[property]; 173 } 174 175 return property; 176 } 177 178 // Return the <font size=X> value for the given CSS size, or undefined if there 179 // is none. 180 function cssSizeToLegacy(cssVal) { 181 return { 182 "xx-small": 1, 183 "small": 2, 184 "medium": 3, 185 "large": 4, 186 "x-large": 5, 187 "xx-large": 6, 188 "xxx-large": 7 189 }[cssVal]; 190 } 191 192 // Return the CSS size given a legacy size. 193 function legacySizeToCss(legacyVal) { 194 return { 195 1: "xx-small", 196 2: "small", 197 3: "medium", 198 4: "large", 199 5: "x-large", 200 6: "xx-large", 201 7: "xxx-large" 202 }[legacyVal]; 203 } 204 205 // Opera 11 puts HTML elements in the null namespace, it seems. 206 function isHtmlNamespace(ns) { 207 return ns === null 208 || !ns 209 || ns === htmlNamespace; 210 } 211 212 // "the directionality" from HTML. I don't bother caring about non-HTML 213 // elements. 214 // 215 // "The directionality of an element is either 'ltr' or 'rtl', and is 216 // determined as per the first appropriate set of steps from the following 217 // list:" 218 function getDirectionality(element) { 219 // "If the element's dir attribute is in the ltr state 220 // The directionality of the element is 'ltr'." 221 if (element.dir == "ltr") { 222 return "ltr"; 223 } 224 225 // "If the element's dir attribute is in the rtl state 226 // The directionality of the element is 'rtl'." 227 if (element.dir == "rtl") { 228 return "rtl"; 229 } 230 231 // "If the element's dir attribute is in the auto state 232 // "If the element is a bdi element and the dir attribute is not in a 233 // defined state (i.e. it is not present or has an invalid value) 234 // [lots of complicated stuff] 235 // 236 // Skip this, since no browser implements it anyway. 237 238 // "If the element is a root element and the dir attribute is not in a 239 // defined state (i.e. it is not present or has an invalid value) 240 // The directionality of the element is 'ltr'." 241 if (!isAnyHtmlElement(element.parentNode)) { 242 return "ltr"; 243 } 244 245 // "If the element has a parent element and the dir attribute is not in a 246 // defined state (i.e. it is not present or has an invalid value) 247 // The directionality of the element is the same as the element's 248 // parent element's directionality." 249 return getDirectionality(element.parentNode); 250 } 251 252 //@} 253 254 /////////////////////////////////////////////////////////////////////////////// 255 ///////////////////////////// DOM Range functions ///////////////////////////// 256 /////////////////////////////////////////////////////////////////////////////// 257 //@{ 258 259 function getNodeIndex(node) { 260 var ret = 0; 261 while (node.previousSibling) { 262 ret++; 263 node = node.previousSibling; 264 } 265 return ret; 266 } 267 268 // "The length of a Node node is the following, depending on node: 269 // 270 // ProcessingInstruction 271 // DocumentType 272 // Always 0. 273 // Text 274 // Comment 275 // node's length. 276 // Any other node 277 // node's childNodes's length." 278 function getNodeLength(node) { 279 switch (node.nodeType) { 280 case $_.Node.PROCESSING_INSTRUCTION_NODE: 281 case $_.Node.DOCUMENT_TYPE_NODE: 282 return 0; 283 284 case $_.Node.TEXT_NODE: 285 case $_.Node.COMMENT_NODE: 286 return node.length; 287 288 default: 289 return node.childNodes.length; 290 } 291 } 292 293 /** 294 * The position of two boundary points relative to one another, as defined by 295 * DOM Range. 296 */ 297 function getPosition(nodeA, offsetA, nodeB, offsetB) { 298 // "If node A is the same as node B, return equal if offset A equals offset 299 // B, before if offset A is less than offset B, and after if offset A is 300 // greater than offset B." 301 if (nodeA == nodeB) { 302 if (offsetA == offsetB) { 303 return "equal"; 304 } 305 if (offsetA < offsetB) { 306 return "before"; 307 } 308 if (offsetA > offsetB) { 309 return "after"; 310 } 311 } 312 313 var documentPosition = $_.compareDocumentPosition(nodeB, nodeA); 314 // "If node A is after node B in tree order, compute the position of (node 315 // B, offset B) relative to (node A, offset A). If it is before, return 316 // after. If it is after, return before." 317 if (documentPosition & $_.Node.DOCUMENT_POSITION_FOLLOWING) { 318 var pos = getPosition(nodeB, offsetB, nodeA, offsetA); 319 if (pos == "before") { 320 return "after"; 321 } 322 if (pos == "after") { 323 return "before"; 324 } 325 } 326 327 // "If node A is an ancestor of node B:" 328 if (documentPosition & $_.Node.DOCUMENT_POSITION_CONTAINS) { 329 // "Let child equal node B." 330 var child = nodeB; 331 332 // "While child is not a child of node A, set child to its parent." 333 while (child.parentNode != nodeA) { 334 child = child.parentNode; 335 } 336 337 // "If the index of child is less than offset A, return after." 338 if (getNodeIndex(child) < offsetA) { 339 return "after"; 340 } 341 } 342 343 // "Return before." 344 return "before"; 345 } 346 347 /** 348 * Returns the furthest ancestor of a Node as defined by DOM Range. 349 */ 350 function getFurthestAncestor(node) { 351 352 var root = node; 353 while (root.parentNode != null) { 354 root = root.parentNode; 355 } 356 return root; 357 } 358 359 /** 360 * "contained" as defined by DOM Range: "A Node node is contained in a range 361 * range if node's furthest ancestor is the same as range's root, and (node, 0) 362 * is after range's start, and (node, length of node) is before range's end." 363 */ 364 function isContained(node, range) { 365 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); 366 if (pos1 !== "after") { 367 return false; 368 } 369 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset); 370 if (pos2 !== "before") { 371 return false; 372 } 373 return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer); 374 } 375 376 /** 377 * Return all nodes contained in range that the provided function returns true 378 * for, omitting any with an ancestor already being returned. 379 */ 380 function getContainedNodes(range, condition) { 381 if (typeof condition == "undefined") { 382 condition = function() { return true }; 383 } 384 var node = range.startContainer; 385 if (node.hasChildNodes() 386 && range.startOffset < node.childNodes.length) { 387 // A child is contained 388 node = node.childNodes[range.startOffset]; 389 } else if (range.startOffset == getNodeLength(node)) { 390 // No descendant can be contained 391 node = nextNodeDescendants(node); 392 } else { 393 // No children; this node at least can't be contained 394 node = nextNode(node); 395 } 396 397 var stop = range.endContainer; 398 if (stop.hasChildNodes() 399 && range.endOffset < stop.childNodes.length) { 400 // The node after the last contained node is a child 401 stop = stop.childNodes[range.endOffset]; 402 } else { 403 // This node and/or some of its children might be contained 404 stop = nextNodeDescendants(stop); 405 } 406 407 var nodeList = []; 408 while (isBefore(node, stop)) { 409 if (isContained(node, range) 410 && condition(node)) { 411 nodeList.push(node); 412 node = nextNodeDescendants(node); 413 continue; 414 } 415 node = nextNode(node); 416 } 417 return nodeList; 418 } 419 420 /** 421 * As above, but includes nodes with an ancestor that's already been returned. 422 */ 423 function getAllContainedNodes(range, condition) { 424 if (typeof condition == "undefined") { 425 condition = function() { return true }; 426 } 427 var node = range.startContainer; 428 if (node.hasChildNodes() 429 && range.startOffset < node.childNodes.length) { 430 // A child is contained 431 node = node.childNodes[range.startOffset]; 432 } else if (range.startOffset == getNodeLength(node)) { 433 // No descendant can be contained 434 node = nextNodeDescendants(node); 435 } else { 436 // No children; this node at least can't be contained 437 node = nextNode(node); 438 } 439 440 var stop = range.endContainer; 441 if (stop.hasChildNodes() 442 && range.endOffset < stop.childNodes.length) { 443 // The node after the last contained node is a child 444 stop = stop.childNodes[range.endOffset]; 445 } else { 446 // This node and/or some of its children might be contained 447 stop = nextNodeDescendants(stop); 448 } 449 450 var nodeList = []; 451 while (isBefore(node, stop)) { 452 if (isContained(node, range) 453 && condition(node)) { 454 nodeList.push(node); 455 } 456 node = nextNode(node); 457 } 458 return nodeList; 459 } 460 461 // Returns either null, or something of the form rgb(x, y, z), or something of 462 // the form rgb(x, y, z, w) with w != 0. 463 function normalizeColor(color) { 464 if (color.toLowerCase() == "currentcolor") { 465 return null; 466 } 467 468 var outerSpan = document.createElement("span"); 469 document.body.appendChild(outerSpan); 470 outerSpan.style.color = "black"; 471 472 var innerSpan = document.createElement("span"); 473 outerSpan.appendChild(innerSpan); 474 innerSpan.style.color = color; 475 color = $_.getComputedStyle(innerSpan).color; 476 477 if (color == "rgb(0, 0, 0)") { 478 // Maybe it's really black, maybe it's invalid. 479 outerSpan.color = "white"; 480 color = $_.getComputedStyle(innerSpan).color; 481 if (color != "rgb(0, 0, 0)") { 482 return null; 483 } 484 } 485 486 document.body.removeChild(outerSpan); 487 488 // I rely on the fact that browsers generally provide consistent syntax for 489 // getComputedStyle(), although it's not standardized. There are only two 490 // exceptions I found: 491 if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) { 492 // IE10PP2 seems to do this sometimes. 493 return color.replace("rgba", "rgb").replace(", 1)", ")"); 494 } 495 if (color == "transparent") { 496 // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if 497 // the specified value is "transparent". 498 return "rgba(0, 0, 0, 0)"; 499 } 500 return color; 501 } 502 503 // Returns either null, or something of the form #xxxxxx, or the color itself 504 // if it's a valid keyword. 505 function parseSimpleColor(color) { 506 color = color.toLowerCase(); 507 if ($_(["aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", 508 "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", 509 510 "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", 511 "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", 512 "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki", 513 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", 514 "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", 515 "darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", 516 "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite", 517 "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", 518 "gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred", 519 "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", 520 "lemonchiffon", "lightblue", "lightcoral", "lightcyan", 521 "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", 522 "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", 523 "lightslategray", "lightslategrey", "lightsteelblue", "lightyellow", 524 "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", 525 "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", 526 "mediumslateblue", "mediumspringgreen", "mediumturquoise", 527 "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", 528 "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", 529 "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", 530 "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", 531 "powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown", 532 "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", 533 "skyblue", "slateblue", "slategray", "slategrey", "snow", "springgreen", 534 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", 535 "wheat", "white", "whitesmoke", "yellow", "yellowgreen"]).indexOf(color) != -1) { 536 return color; 537 } 538 539 color = normalizeColor(color); 540 var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color); 541 if (matches) { 542 return "#" 543 + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&") 544 + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&") 545 + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&"); 546 } 547 return null; 548 } 549 550 //@} 551 552 ////////////////////////////////////////////////////////////////////////////// 553 /////////////////////////// Edit command functions /////////////////////////// 554 ////////////////////////////////////////////////////////////////////////////// 555 556 ///////////////////////////////////////////////// 557 ///// Methods of the HTMLDocument interface ///// 558 ///////////////////////////////////////////////// 559 //@{ 560 561 var executionStackDepth = 0; 562 563 // Helper function for common behavior. 564 function editCommandMethod(command, prop, range, callback) { 565 // Set up our global range magic, but only if we're the outermost function 566 if (executionStackDepth == 0 && typeof range != "undefined") { 567 globalRange = range; 568 } else if (executionStackDepth == 0) { 569 globalRange = null; 570 globalRange = range; 571 } 572 573 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 574 // 575 // We can't throw a real one, but a string will do for our purposes. 576 if (!(command in commands)) { 577 throw "NOT_SUPPORTED_ERR"; 578 } 579 580 // "If command has no action, raise an INVALID_ACCESS_ERR exception." 581 // "If command has no indeterminacy, raise an INVALID_ACCESS_ERR 582 // exception." 583 // "If command has no state, raise an INVALID_ACCESS_ERR exception." 584 // "If command has no value, raise an INVALID_ACCESS_ERR exception." 585 if (prop != "enabled" 586 && !(prop in commands[command])) { 587 throw "INVALID_ACCESS_ERR"; 588 } 589 590 executionStackDepth++; 591 try { 592 var ret = callback(); 593 } catch(e) { 594 executionStackDepth--; 595 throw e; 596 } 597 executionStackDepth--; 598 return ret; 599 } 600 601 function myExecCommand(command, showUi, value, range) { 602 // "All of these methods must treat their command argument ASCII 603 // case-insensitively." 604 command = command.toLowerCase(); 605 606 // "If only one argument was provided, let show UI be false." 607 // 608 // If range was passed, I can't actually detect how many args were passed 609 // . . . 610 if (arguments.length == 1 611 || (arguments.length >=4 && typeof showUi == "undefined")) { 612 showUi = false; 613 } 614 615 // "If only one or two arguments were provided, let value be the empty 616 // string." 617 if (arguments.length <= 2 618 || (arguments.length >=4 && typeof value == "undefined")) { 619 value = ""; 620 } 621 622 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 623 // 624 // "If command has no action, raise an INVALID_ACCESS_ERR exception." 625 return editCommandMethod(command, "action", range, (function(command, showUi, value) { return function() { 626 // "If command is not enabled, return false." 627 if (!myQueryCommandEnabled(command)) { 628 return false; 629 } 630 631 // "Take the action for command, passing value to the instructions as an 632 // argument." 633 commands[command].action(value, range); 634 635 // always fix the range after the command is complete 636 setActiveRange(range); 637 638 // "Return true." 639 return true; 640 }})(command, showUi, value)); 641 } 642 643 function myQueryCommandEnabled(command, range) { 644 // "All of these methods must treat their command argument ASCII 645 // case-insensitively." 646 command = command.toLowerCase(); 647 648 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 649 return editCommandMethod(command, "action", range, (function(command) { return function() { 650 // "Among commands defined in this specification, those listed in 651 // Miscellaneous commands are always enabled. The other commands defined 652 // here are enabled if the active range is not null, and disabled 653 // otherwise." 654 return jQuery.inArray(command, ["copy", "cut", "paste", "selectall", "stylewithcss", "usecss"]) != -1 655 || range !== null; 656 }})(command)); 657 } 658 659 function myQueryCommandIndeterm(command, range) { 660 // "All of these methods must treat their command argument ASCII 661 // case-insensitively." 662 command = command.toLowerCase(); 663 664 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 665 // 666 // "If command has no indeterminacy, raise an INVALID_ACCESS_ERR 667 // exception." 668 return editCommandMethod(command, "indeterm", range, (function(command) { return function() { 669 // "If command is not enabled, return false." 670 if (!myQueryCommandEnabled(command, range)) { 671 return false; 672 } 673 674 // "Return true if command is indeterminate, otherwise false." 675 return commands[command].indeterm( range ); 676 }})(command)); 677 } 678 679 function myQueryCommandState(command, range) { 680 // "All of these methods must treat their command argument ASCII 681 // case-insensitively." 682 command = command.toLowerCase(); 683 684 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 685 // 686 // "If command has no state, raise an INVALID_ACCESS_ERR exception." 687 return editCommandMethod(command, "state", range, (function(command) { return function() { 688 // "If command is not enabled, return false." 689 if (!myQueryCommandEnabled(command, range)) { 690 return false; 691 } 692 693 // "If the state override for command is set, return it." 694 if (typeof getStateOverride(command, range) != "undefined") { 695 return getStateOverride(command, range); 696 } 697 698 // "Return true if command's state is true, otherwise false." 699 return commands[command].state( range ); 700 }})(command)); 701 } 702 703 // "When the queryCommandSupported(command) method on the HTMLDocument 704 // interface is invoked, the user agent must return true if command is 705 // supported, and false otherwise." 706 function myQueryCommandSupported(command) { 707 // "All of these methods must treat their command argument ASCII 708 // case-insensitively." 709 command = command.toLowerCase(); 710 711 return command in commands; 712 } 713 714 function myQueryCommandValue(command, range) { 715 // "All of these methods must treat their command argument ASCII 716 // case-insensitively." 717 command = command.toLowerCase(); 718 719 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 720 // 721 // "If command has no value, raise an INVALID_ACCESS_ERR exception." 722 return editCommandMethod(command, "value", range, function() { 723 // "If command is not enabled, return the empty string." 724 if (!myQueryCommandEnabled(command, range)) { 725 return ""; 726 } 727 728 // "If command is "fontSize" and its value override is set, convert the 729 // value override to an integer number of pixels and return the legacy 730 // font size for the result." 731 if (command == "fontsize" 732 && getValueOverride("fontsize", range) !== undefined) { 733 return getLegacyFontSize(getValueOverride("fontsize", range)); 734 } 735 736 // "If the value override for command is set, return it." 737 if (typeof getValueOverride(command, range) != "undefined") { 738 return getValueOverride(command, range); 739 740 } 741 742 // "Return command's value." 743 return commands[command].value(range); 744 }); 745 } 746 //@} 747 748 ////////////////////////////// 749 ///// Common definitions ///// 750 ////////////////////////////// 751 //@{ 752 753 // "An HTML element is an Element whose namespace is the HTML namespace." 754 // 755 // I allow an extra argument to more easily check whether something is a 756 // particular HTML element, like isNamedHtmlElement(node, 'OL'). It accepts arrays 757 // too, like isHtmlElementInArray(node, ["OL", "UL"]) to check if it's an ol or ul. 758 // TODO This function was prominent during profiling. Remove it 759 // and replace with calls to isAnyHtmlElement, isNamedHtmlElement 760 // and is isMappedHtmlElement. 761 function isHtmlElement_obsolete(node, tags) { 762 if (typeof tags == "string") { 763 tags = [tags]; 764 } 765 if (typeof tags == "object") { 766 tags = $_( tags ).map(function(tag) { return tag.toUpperCase() }); 767 } 768 return node 769 && node.nodeType == 1 770 && isHtmlNamespace(node.namespaceURI) 771 && (typeof tags == "undefined" || $_( tags ).indexOf(node.tagName) != -1); 772 } 773 774 function isAnyHtmlElement(node) { 775 return node 776 && node.nodeType == 1 777 && isHtmlNamespace(node.namespaceURI); 778 } 779 780 // name should be uppercase 781 function isNamedHtmlElement(node, name) { 782 return node 783 && node.nodeType == 1 784 && isHtmlNamespace(node.namespaceURI) 785 // This function is passed in a mix of upper and lower case names 786 && name.toUpperCase() === node.nodeName; 787 } 788 789 // TODO remove when isHtmlElementInArray is removed 790 function arrayContainsInsensitive(array, str) { 791 var i, len; 792 str = str.toUpperCase(); 793 for (i = 0, len = array.length; i < len; i++) { 794 if (array[i].toUpperCase() === str) { 795 return true; 796 } 797 } 798 return false; 799 } 800 // TODO replace calls to this function with calls to isMappedHtmlElement() 801 function isHtmlElementInArray(node, array) { 802 return node 803 && node.nodeType == 1 804 && isHtmlNamespace(node.namespaceURI) 805 // This function is passed in a mix of upper and lower case names 806 && arrayContainsInsensitive(array, node.nodeName); 807 } 808 809 // map must have all-uppercase keys 810 function isMappedHtmlElement(node, map) { 811 return node 812 && node.nodeType == 1 813 && isHtmlNamespace(node.namespaceURI) 814 && map[node.nodeName]; 815 } 816 817 // "A prohibited paragraph child name is "address", "article", "aside", 818 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details", 819 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", 820 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", 821 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", 822 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or 823 // "xmp"." 824 var prohibitedParagraphChildNamesMap = { 825 "ADDRESS": true, "ARTICLE": true, "ASIDE": true, 826 "BLOCKQUOTE": true, "CAPTION": true, "CENTER": true, "COL": true, "COLGROUP": true, "DD": true, "DETAILS": true, 827 "DIR": true, "DIV": true, "DL": true, "DT": true, "FIELDSET": true, "FIGCAPTION": true, "FIGURE": true, "FOOTER": true, 828 "FORM": true, "H1": true, "H2": true, "H3": true, "H4": true, "H5": true, "H6": true, "HEADER": true, "HGROUP": true, "HR": true, "LI": true, 829 "LISTING": true, "MENU": true, "NAV": true, "OL": true, "P": true, "PLAINTEXT": true, "PRE": true, "SECTION": true, 830 "SUMMARY": true, "TABLE": true, "TBODY": true, "TD": true, "TFOOT": true, "TH": true, "THEAD": true, "TR": true, "UL": true, 831 "XMP": true 832 }; 833 834 // "A prohibited paragraph child is an HTML element whose local name is a 835 // prohibited paragraph child name." 836 function isProhibitedParagraphChild(node) { 837 return isMappedHtmlElement(node, prohibitedParagraphChildNamesMap); 838 } 839 840 var nonBlockDisplayValuesMap = { 841 "inline": true, 842 "inline-block": true, 843 "inline-table": true, 844 "none": true 845 }; 846 847 // "A block node is either an Element whose "display" property does not have 848 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a 849 // Document, or a DocumentFragment." 850 function isBlockNode(node) { 851 return node 852 && ((node.nodeType == $_.Node.ELEMENT_NODE && !nonBlockDisplayValuesMap[$_.getComputedStyle(node).display]) 853 || node.nodeType == $_.Node.DOCUMENT_NODE 854 || node.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE); 855 } 856 857 // "An inline node is a node that is not a block node." 858 function isInlineNode(node) { 859 return node && !isBlockNode(node); 860 } 861 862 // "An editing host is a node that is either an Element with a contenteditable 863 // attribute set to the true state, or the Element child of a Document whose 864 // designMode is enabled." 865 function isEditingHost(node) { 866 return node 867 && node.nodeType == $_.Node.ELEMENT_NODE 868 && (node.contentEditable == "true" 869 || (node.parentNode 870 && node.parentNode.nodeType == $_.Node.DOCUMENT_NODE 871 && node.parentNode.designMode == "on")); 872 } 873 874 // "Something is editable if it is a node which is not an editing host, does 875 // not have a contenteditable attribute set to the false state, and whose 876 // parent is an editing host or editable." 877 function isEditable(node) { 878 // This is slightly a lie, because we're excluding non-HTML elements with 879 // contentEditable attributes. 880 return node 881 && !isEditingHost(node) 882 && (node.nodeType != $_.Node.ELEMENT_NODE || node.contentEditable != "false" || jQuery(node).hasClass('aloha-table-wrapper')) 883 && (isEditingHost(node.parentNode) || isEditable(node.parentNode)); 884 } 885 886 // Helper function, not defined in the spec 887 function hasEditableDescendants(node) { 888 for (var i = 0; i < node.childNodes.length; i++) { 889 if (isEditable(node.childNodes[i]) 890 || hasEditableDescendants(node.childNodes[i])) { 891 return true; 892 } 893 } 894 return false; 895 } 896 897 // "The editing host of node is null if node is neither editable nor an editing 898 // host; node itself, if node is an editing host; or the nearest ancestor of 899 // node that is an editing host, if node is editable." 900 function getEditingHostOf(node) { 901 if (isEditingHost(node)) { 902 return node; 903 } else if (isEditable(node)) { 904 var ancestor = node.parentNode; 905 while (!isEditingHost(ancestor)) { 906 ancestor = ancestor.parentNode; 907 } 908 return ancestor; 909 } else { 910 return null; 911 } 912 } 913 914 // "Two nodes are in the same editing host if the editing host of the first is 915 // non-null and the same as the editing host of the second." 916 function inSameEditingHost(node1, node2) { 917 return getEditingHostOf(node1) 918 && getEditingHostOf(node1) == getEditingHostOf(node2); 919 } 920 921 // "A collapsed line break is a br that begins a line box which has nothing 922 // else in it, and therefore has zero height." 923 function isCollapsedLineBreak(br) { 924 if (!isNamedHtmlElement(br, 'br')) { 925 return false; 926 } 927 928 // Add a zwsp after it and see if that changes the height of the nearest 929 // non-inline parent. Note: this is not actually reliable, because the 930 // parent might have a fixed height or something. 931 var ref = br.parentNode; 932 while ($_.getComputedStyle(ref).display == "inline") { 933 ref = ref.parentNode; 934 } 935 936 var origStyle = { 937 height: ref.style.height, 938 maxHeight: ref.style.maxHeight, 939 minHeight: ref.style.minHeight 940 }; 941 942 ref.style.height = 'auto'; 943 ref.style.maxHeight = 'none'; 944 if (!(jQuery.browser.msie && jQuery.browser.version < 8)) { 945 ref.style.minHeight = '0'; 946 } 947 var space = document.createTextNode('\u200b'); 948 var origHeight = ref.offsetHeight; 949 if (origHeight == 0) { 950 throw 'isCollapsedLineBreak: original height is zero, bug?'; 951 } 952 br.parentNode.insertBefore(space, br.nextSibling); 953 var finalHeight = ref.offsetHeight; 954 space.parentNode.removeChild(space); 955 956 ref.style.height = origStyle.height; 957 ref.style.maxHeight = origStyle.maxHeight; 958 if (!(jQuery.browser.msie && jQuery.browser.version < 8)) { 959 ref.style.minHeight = origStyle.minHeight; 960 } 961 962 // Allow some leeway in case the zwsp didn't create a whole new line, but 963 // only made an existing line slightly higher. Firefox 6.0a2 shows this 964 // behavior when the first line is bold. 965 return origHeight < finalHeight - 5; 966 } 967 968 // "An extraneous line break is a br that has no visual effect, in that 969 // removing it from the DOM would not change layout, except that a br that is 970 // the sole child of an li is not extraneous." 971 function isExtraneousLineBreak(br) { 972 973 if (!isNamedHtmlElement(br, 'br')) { 974 return false; 975 } 976 977 if (isNamedHtmlElement(br.parentNode, "li") 978 && br.parentNode.childNodes.length == 1) { 979 return false; 980 } 981 982 // Make the line break disappear and see if that changes the block's 983 // height. Yes, this is an absurd hack. We have to reset height etc. on 984 // the reference node because otherwise its height won't change if it's not 985 // auto. 986 var ref = br.parentNode; 987 while ($_.getComputedStyle(ref).display == "inline") { 988 ref = ref.parentNode; 989 } 990 991 var origStyle = { 992 height: ref.style.height, 993 maxHeight: ref.style.maxHeight, 994 minHeight: ref.style.minHeight, 995 contentEditable: ref.contentEditable 996 }; 997 998 ref.style.height = 'auto'; 999 ref.style.maxHeight = 'none'; 1000 ref.style.minHeight = '0'; 1001 // IE7 would ignore display:none in contentEditable, so we temporarily set it to false 1002 if (jQuery.browser.msie && jQuery.browser.version <= 7) { 1003 ref.contentEditable = 'false'; 1004 } 1005 1006 var origHeight = ref.offsetHeight; 1007 if (origHeight == 0) { 1008 throw "isExtraneousLineBreak: original height is zero, bug?"; 1009 } 1010 1011 var origBrDisplay = br.style.display; 1012 br.style.display = 'none'; 1013 var finalHeight = ref.offsetHeight; 1014 1015 // Restore original styles to the touched elements. 1016 ref.style.height = origStyle.height; 1017 ref.style.maxHeight = origStyle.maxHeight; 1018 ref.style.minHeight = origStyle.minHeight; 1019 // reset contentEditable for IE7 1020 if (jQuery.browser.msie && jQuery.browser.version <= 7) { 1021 ref.contentEditable = origStyle.contentEditable; 1022 } 1023 br.style.display = origBrDisplay; 1024 1025 // https://github.com/alohaeditor/Aloha-Editor/issues/516 1026 // look like it works in msie > 7 1027 1028 /* if (jQuery.browser.msie && jQuery.browser.version < 8) { 1029 br.removeAttribute("style"); 1030 ref.removeAttribute("style"); 1031 } */ 1032 1033 return origHeight == finalHeight; 1034 } 1035 1036 // "A whitespace node is either a Text node whose data is the empty string; or 1037 // a Text node whose data consists only of one or more tabs (0x0009), line 1038 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose 1039 // parent is an Element whose resolved value for "white-space" is "normal" or 1040 // "nowrap"; or a Text node whose data consists only of one or more tabs 1041 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose 1042 // parent is an Element whose resolved value for "white-space" is "pre-line"." 1043 function isWhitespaceNode(node) { 1044 return node 1045 && node.nodeType == $_.Node.TEXT_NODE 1046 && (node.data == "" 1047 || ( 1048 /^[\t\n\r ]+$/.test(node.data) 1049 && node.parentNode 1050 && node.parentNode.nodeType == $_.Node.ELEMENT_NODE 1051 && jQuery.inArray($_.getComputedStyle(node.parentNode).whiteSpace, ["normal", "nowrap"]) != -1 1052 ) || ( 1053 /^[\t\r ]+$/.test(node.data) 1054 && node.parentNode 1055 1056 && node.parentNode.nodeType == $_.Node.ELEMENT_NODE 1057 && $_.getComputedStyle(node.parentNode).whiteSpace == "pre-line" 1058 ) || ( 1059 /^[\t\n\r ]+$/.test(node.data) 1060 && node.parentNode 1061 && node.parentNode.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE 1062 )); 1063 } 1064 1065 /** 1066 * Collapse sequences of ignorable whitespace (tab (0x0009), line feed (0x000A), carriage return (0x000D), space (0x0020)) to only one space. 1067 * Preserve the given range if necessary. 1068 * @param node text node 1069 * @param range range 1070 */ 1071 function collapseWhitespace(node, range) { 1072 // "If node is neither editable nor an editing host, abort these steps." 1073 if (!isEditable(node) && !isEditingHost(node)) { 1074 return; 1075 } 1076 1077 // if the given node is not a text node, return 1078 if (!node || node.nodeType !== $_.Node.TEXT_NODE) { 1079 return; 1080 } 1081 1082 // if the node is in a pre or pre-wrap node, return 1083 if (jQuery.inArray($_.getComputedStyle(node.parentNode).whiteSpace, ["pre", "pre-wrap"]) != -1) { 1084 return; 1085 } 1086 1087 // if the given node does not contain sequences of at least two consecutive ignorable whitespace characters, return 1088 if (!/[\t\n\r ]{2,}/.test(node.data)) { 1089 return; 1090 } 1091 1092 var newData = ''; 1093 var correctStart = range.startContainer == node; 1094 var correctEnd = range.endContainer == node; 1095 var wsFound = false; 1096 1097 // iterate through the node data 1098 for (var i = 0; i < node.data.length; ++i) { 1099 if (/[\t\n\r ]/.test(node.data.substr(i, 1))) { 1100 // found a whitespace 1101 if (!wsFound) { 1102 // this is the first whitespace in the current sequence 1103 // add a whitespace to the new data sequence 1104 newData += ' '; 1105 // remember that we found a whitespace 1106 wsFound = true; 1107 } else { 1108 // this is not the first whitespace in the sequence, so omit this character 1109 if (correctStart && newData.length < range.startOffset) { 1110 range.startOffset--; 1111 } 1112 if (correctEnd && newData.length < range.endOffset) { 1113 range.endOffset--; 1114 } 1115 } 1116 } else { 1117 newData += node.data.substr(i, 1); 1118 wsFound = false; 1119 } 1120 } 1121 1122 // set the new data 1123 node.data = newData; 1124 } 1125 1126 // "node is a collapsed whitespace node if the following algorithm returns 1127 // true:" 1128 function isCollapsedWhitespaceNode(node) { 1129 // "If node is not a whitespace node, return false." 1130 if (!isWhitespaceNode(node)) { 1131 return false; 1132 } 1133 1134 // "If node's data is the empty string, return true." 1135 if (node.data == "") { 1136 return true; 1137 } 1138 1139 // "Let ancestor be node's parent." 1140 var ancestor = node.parentNode; 1141 1142 // "If ancestor is null, return true." 1143 if (!ancestor) { 1144 return true; 1145 } 1146 1147 // "If the "display" property of some ancestor of node has resolved value 1148 // "none", return true." 1149 if ($_( getAncestors(node) ).some(function(ancestor) { 1150 return ancestor.nodeType == $_.Node.ELEMENT_NODE 1151 && $_.getComputedStyle(ancestor).display == "none"; 1152 })) { 1153 return true; 1154 } 1155 1156 // "While ancestor is not a block node and its parent is not null, set 1157 // ancestor to its parent." 1158 while (!isBlockNode(ancestor) 1159 && ancestor.parentNode) { 1160 ancestor = ancestor.parentNode; 1161 } 1162 1163 // "Let reference be node." 1164 var reference = node; 1165 1166 // "While reference is a descendant of ancestor:" 1167 while (reference != ancestor) { 1168 // "Let reference be the node before it in tree order." 1169 reference = previousNode(reference); 1170 1171 // "If reference is a block node or a br, return true." 1172 if (isBlockNode(reference) 1173 || isNamedHtmlElement(reference, 'br')) { 1174 return true; 1175 } 1176 1177 // "If reference is a Text node that is not a whitespace node, or is an 1178 // img, break from this loop." 1179 if ((reference.nodeType == $_.Node.TEXT_NODE && !isWhitespaceNode(reference)) 1180 || isNamedHtmlElement(reference, 'img')) { 1181 break; 1182 } 1183 } 1184 1185 // "Let reference be node." 1186 reference = node; 1187 1188 // "While reference is a descendant of ancestor:" 1189 var stop = nextNodeDescendants(ancestor); 1190 while (reference != stop) { 1191 // "Let reference be the node after it in tree order, or null if there 1192 // is no such node." 1193 reference = nextNode(reference); 1194 1195 // "If reference is a block node or a br, return true." 1196 if (isBlockNode(reference) 1197 || isNamedHtmlElement(reference, 'br')) { 1198 return true; 1199 } 1200 1201 // "If reference is a Text node that is not a whitespace node, or is an 1202 // img, break from this loop." 1203 if ((reference && reference.nodeType == $_.Node.TEXT_NODE && !isWhitespaceNode(reference)) 1204 || isNamedHtmlElement(reference, 'img')) { 1205 break; 1206 } 1207 } 1208 1209 // "Return false." 1210 return false; 1211 } 1212 1213 // "Something is visible if it is a node that either is a block node, or a Text 1214 // node that is not a collapsed whitespace node, or an img, or a br that is not 1215 // an extraneous line break, or any node with a visible descendant; excluding 1216 // any node with an ancestor container Element whose "display" property has 1217 // resolved value "none"." 1218 function isVisible(node) { 1219 if (!node) { 1220 return false; 1221 } 1222 1223 if ($_( getAncestors(node).concat(node) ) 1224 .filter(function(node) { return node.nodeType == $_.Node.ELEMENT_NODE }, true) 1225 .some(function(node) { return $_.getComputedStyle(node).display == "none" })) { 1226 return false; 1227 } 1228 1229 if (isBlockNode(node) 1230 || (node.nodeType == $_.Node.TEXT_NODE && !isCollapsedWhitespaceNode(node)) 1231 || isNamedHtmlElement(node, 'img') 1232 || (isNamedHtmlElement(node, 'br') && !isExtraneousLineBreak(node))) { 1233 return true; 1234 } 1235 1236 for (var i = 0; i < node.childNodes.length; i++) { 1237 if (isVisible(node.childNodes[i])) { 1238 return true; 1239 } 1240 } 1241 1242 return false; 1243 } 1244 1245 // "Something is invisible if it is a node that is not visible." 1246 function isInvisible(node) { 1247 return node && !isVisible(node); 1248 } 1249 1250 // "A collapsed block prop is either a collapsed line break that is not an 1251 // extraneous line break, or an Element that is an inline node and whose 1252 // children are all either invisible or collapsed block props and that has at 1253 // least one child that is a collapsed block prop." 1254 function isCollapsedBlockProp(node) { 1255 if (isCollapsedLineBreak(node) 1256 && !isExtraneousLineBreak(node)) { 1257 return true; 1258 } 1259 1260 if (!isInlineNode(node) 1261 || node.nodeType != $_.Node.ELEMENT_NODE) { 1262 return false; 1263 } 1264 1265 var hasCollapsedBlockPropChild = false; 1266 for (var i = 0; i < node.childNodes.length; i++) { 1267 if (!isInvisible(node.childNodes[i]) 1268 && !isCollapsedBlockProp(node.childNodes[i])) { 1269 return false; 1270 } 1271 if (isCollapsedBlockProp(node.childNodes[i])) { 1272 hasCollapsedBlockPropChild = true; 1273 } 1274 } 1275 1276 return hasCollapsedBlockPropChild; 1277 } 1278 1279 function setActiveRange( range ) { 1280 var rangeObject = new window.GENTICS.Utils.RangeObject(); 1281 1282 rangeObject.startContainer = range.startContainer; 1283 rangeObject.startOffset = range.startOffset; 1284 rangeObject.endContainer = range.endContainer; 1285 rangeObject.endOffset = range.endOffset; 1286 1287 rangeObject.select(); 1288 } 1289 1290 // Please note: This method is deprecated and will be removed. 1291 // Every command should use the value and range parameter. 1292 // 1293 // "The active range is the first range in the Selection given by calling 1294 // getSelection() on the context object, or null if there is no such range." 1295 // 1296 // We cheat and return globalRange if that's defined. We also ensure that the 1297 // active range meets the requirements that selection boundary points are 1298 // supposed to meet, i.e., that the nodes are both Text or Element nodes that 1299 // descend from a Document. 1300 function getActiveRange() { 1301 var ret; 1302 if (globalRange) { 1303 ret = globalRange; 1304 } else if (Aloha.getSelection().rangeCount) { 1305 ret = Aloha.getSelection().getRangeAt(0); 1306 } else { 1307 return null; 1308 } 1309 if (jQuery.inArray(ret.startContainer.nodeType, [$_.Node.TEXT_NODE, $_.Node.ELEMENT_NODE]) == -1 1310 || jQuery.inArray(ret.endContainer.nodeType, [$_.Node.TEXT_NODE, $_.Node.ELEMENT_NODE]) == -1 1311 || !ret.startContainer.ownerDocument 1312 || !ret.endContainer.ownerDocument 1313 || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument) 1314 || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) { 1315 throw "Invalid active range; test bug?"; 1316 } 1317 return ret; 1318 } 1319 1320 // "For some commands, each HTMLDocument must have a boolean state override 1321 // and/or a string value override. These do not change the command's state or 1322 // value, but change the way some algorithms behave, as specified in those 1323 // algorithms' definitions. Initially, both must be unset for every command. 1324 // Whenever the number of ranges in the Selection changes to something 1325 // different, and whenever a boundary point of the range at a given index in 1326 // the Selection changes to something different, the state override and value 1327 // override must be unset for every command." 1328 // 1329 // We implement this crudely by using setters and getters. To verify that the 1330 // selection hasn't changed, we copy the active range and just check the 1331 // endpoints match. This isn't really correct, but it's good enough for us. 1332 // Unset state/value overrides are undefined. We put everything in a function 1333 // so no one can access anything except via the provided functions, since 1334 // otherwise callers might mistakenly use outdated overrides (if the selection 1335 // has changed). 1336 var getStateOverride, setStateOverride, unsetStateOverride, 1337 getValueOverride, setValueOverride, unsetValueOverride; 1338 (function() { 1339 var stateOverrides = {}; 1340 var valueOverrides = {}; 1341 var storedRange = null; 1342 1343 function resetOverrides(range) { 1344 if (!storedRange 1345 || storedRange.startContainer != range.startContainer 1346 || storedRange.endContainer != range.endContainer 1347 || storedRange.startOffset != range.startOffset 1348 || storedRange.endOffset != range.endOffset) { 1349 stateOverrides = {}; 1350 valueOverrides = {}; 1351 storedRange = range.cloneRange(); 1352 } 1353 } 1354 1355 getStateOverride = function(command, range) { 1356 resetOverrides(range); 1357 return stateOverrides[command]; 1358 }; 1359 1360 setStateOverride = function(command, newState, range) { 1361 resetOverrides(range); 1362 stateOverrides[command] = newState; 1363 }; 1364 1365 unsetStateOverride = function(command, range) { 1366 resetOverrides(range); 1367 delete stateOverrides[command]; 1368 } 1369 1370 getValueOverride = function(command, range) { 1371 resetOverrides(range); 1372 return valueOverrides[command]; 1373 } 1374 1375 // "The value override for the backColor command must be the same as the 1376 // value override for the hiliteColor command, such that setting one sets 1377 // the other to the same thing and unsetting one unsets the other." 1378 setValueOverride = function(command, newValue, range) { 1379 resetOverrides(range); 1380 valueOverrides[command] = newValue; 1381 if (command == "backcolor") { 1382 valueOverrides.hilitecolor = newValue; 1383 } else if (command == "hilitecolor") { 1384 valueOverrides.backcolor = newValue; 1385 } 1386 } 1387 1388 unsetValueOverride = function(command, range) { 1389 resetOverrides(range); 1390 delete valueOverrides[command]; 1391 if (command == "backcolor") { 1392 delete valueOverrides.hilitecolor; 1393 } else if (command == "hilitecolor") { 1394 delete valueOverrides.backcolor; 1395 } 1396 } 1397 })(); 1398 1399 //@} 1400 1401 ///////////////////////////// 1402 ///// Common algorithms ///// 1403 ///////////////////////////// 1404 1405 ///// Assorted common algorithms ///// 1406 //@{ 1407 1408 function movePreservingRanges(node, newParent, newIndex, range) { 1409 // For convenience, I allow newIndex to be -1 to mean "insert at the end". 1410 if (newIndex == -1) { 1411 newIndex = newParent.childNodes.length; 1412 } 1413 1414 // "When the user agent is to move a Node to a new location, preserving 1415 // ranges, it must remove the Node from its original parent (if any), then 1416 // insert it in the new location. In doing so, however, it must ignore the 1417 // regular range mutation rules, and instead follow these rules:" 1418 1419 // "Let node be the moved Node, old parent and old index be the old parent 1420 // (which may be null) and index, and new parent and new index be the new 1421 // parent and index." 1422 var oldParent = node.parentNode; 1423 var oldIndex = getNodeIndex(node); 1424 1425 // We only even attempt to preserve the global range object and the ranges 1426 // in the selection, not every range out there (the latter is probably 1427 // impossible). 1428 var ranges = [range]; 1429 for (var i = 0; i < Aloha.getSelection().rangeCount; i++) { 1430 ranges.push(Aloha.getSelection().getRangeAt(i)); 1431 } 1432 var boundaryPoints = []; 1433 $_( ranges ).forEach(function(range) { 1434 boundaryPoints.push([range.startContainer, range.startOffset]); 1435 boundaryPoints.push([range.endContainer, range.endOffset]); 1436 }); 1437 1438 $_( boundaryPoints ).forEach(function(boundaryPoint) { 1439 // "If a boundary point's node is the same as or a descendant of node, 1440 // leave it unchanged, so it moves to the new location." 1441 // 1442 // No modifications necessary. 1443 1444 // "If a boundary point's node is new parent and its offset is greater 1445 // than new index, add one to its offset." 1446 if (boundaryPoint[0] == newParent 1447 && boundaryPoint[1] > newIndex) { 1448 boundaryPoint[1]++; 1449 } 1450 1451 // "If a boundary point's node is old parent and its offset is old index or 1452 // old index + 1, set its node to new parent and add new index − old index 1453 // to its offset." 1454 if (boundaryPoint[0] == oldParent 1455 && (boundaryPoint[1] == oldIndex 1456 || boundaryPoint[1] == oldIndex + 1)) { 1457 boundaryPoint[0] = newParent; 1458 boundaryPoint[1] += newIndex - oldIndex; 1459 } 1460 1461 // "If a boundary point's node is old parent and its offset is greater than 1462 // old index + 1, subtract one from its offset." 1463 if (boundaryPoint[0] == oldParent 1464 && boundaryPoint[1] > oldIndex + 1) { 1465 boundaryPoint[1]--; 1466 } 1467 }); 1468 1469 // Now actually move it and preserve the ranges. 1470 if (newParent.childNodes.length == newIndex) { 1471 newParent.appendChild(node); 1472 } else { 1473 newParent.insertBefore(node, newParent.childNodes[newIndex]); 1474 } 1475 1476 // if we're off actual node boundaries this implies that the move was 1477 // part of a deletion process (backspace). If that's the case we 1478 // attempt to fix this by restoring the range to the first index of 1479 // the node that has been moved 1480 if (boundaryPoints[0][1] > boundaryPoints[0][0].childNodes.length 1481 && boundaryPoints[1][1] > boundaryPoints[1][0].childNodes.length) { 1482 range.setStart(node, 0); 1483 range.setEnd(node, 0); 1484 } else { 1485 range.setStart(boundaryPoints[0][0], boundaryPoints[0][1]); 1486 range.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]); 1487 1488 Aloha.getSelection().removeAllRanges(); 1489 for (var i = 1; i < ranges.length; i++) { 1490 var newRange = Aloha.createRange(); 1491 newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]); 1492 newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]); 1493 Aloha.getSelection().addRange(newRange); 1494 } 1495 if (newRange) { 1496 range = newRange; 1497 } 1498 } 1499 } 1500 1501 /** 1502 * Copy all non empty attributes from an existing to a new element 1503 * 1504 * @param {dom} element The source DOM element 1505 * @param {dom} newElement The new DOM element which will get the attributes of the source DOM element 1506 * @return void 1507 */ 1508 function copyAttributes( element, newElement ) { 1509 1510 // This is an IE7 workaround. We identified three places that were connected 1511 // to the mysterious ie7 crash: 1512 // 1. Add attribute to dom element (Initialization of jquery-ui sortable) 1513 // 2. Access the jquery expando attribute. Just reading the name is 1514 // sufficient to make the browser vulnerable for the crash (Press enter) 1515 // 3. On editable blur the Aloha.editables[0].getContents(); gets invoked. 1516 // This invokation somehow crashes the ie7. We assume that the access of 1517 // shared expando attribute updates internal references which are not 1518 // correclty handled during clone(); 1519 if ( jQuery.browser.msie && jQuery.browser.version >=7 && typeof element.attributes[jQuery.expando] !== 'undefined' ) { 1520 jQuery(element).removeAttr(jQuery.expando); 1521 } 1522 1523 var attrs = element.attributes; 1524 for ( var i = 0; i < attrs.length; i++ ) { 1525 var attr = attrs[i]; 1526 // attr.specified is an IE specific check to exclude attributes that were never really set. 1527 if (typeof attr.specified === "undefined" || attr.specified) { 1528 if ( typeof newElement.setAttributeNS === 'function' ) { 1529 newElement.setAttributeNS( attr.namespaceURI, attr.name, attr.value ); 1530 } else { 1531 // fixes https://github.com/alohaeditor/Aloha-Editor/issues/515 1532 newElement.setAttribute( attr.name, attr.value ); 1533 } 1534 } 1535 } 1536 } 1537 1538 function setTagName(element, newName, range) { 1539 // "If element is an HTML element with local name equal to new name, return 1540 // element." 1541 if (isNamedHtmlElement(element, newName)) { 1542 1543 return element; 1544 } 1545 1546 1547 // "If element's parent is null, return element." 1548 if (!element.parentNode) { 1549 return element; 1550 } 1551 1552 // "Let replacement element be the result of calling createElement(new 1553 // name) on the ownerDocument of element." 1554 var replacementElement = element.ownerDocument.createElement(newName); 1555 1556 // "Insert replacement element into element's parent immediately before 1557 // element." 1558 element.parentNode.insertBefore(replacementElement, element); 1559 1560 // "Copy all attributes of element to replacement element, in order." 1561 copyAttributes( element, replacementElement ); 1562 1563 // "While element has children, append the first child of element as the 1564 // last child of replacement element, preserving ranges." 1565 while (element.childNodes.length) { 1566 movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length, range); 1567 } 1568 1569 // "Remove element from its parent." 1570 element.parentNode.removeChild(element); 1571 1572 // if the range still uses the old element, we modify it to the new one 1573 if (range.startContainer === element) { 1574 range.startContainer = replacementElement; 1575 } 1576 if (range.endContainer === element) { 1577 range.endContainer = replacementElement; 1578 } 1579 1580 // "Return replacement element." 1581 return replacementElement; 1582 } 1583 1584 function removeExtraneousLineBreaksBefore(node) { 1585 // "Let ref be the previousSibling of node." 1586 var ref = node.previousSibling; 1587 1588 // "If ref is null, abort these steps." 1589 if (!ref) { 1590 return; 1591 } 1592 1593 // "While ref has children, set ref to its lastChild." 1594 while (ref.hasChildNodes()) { 1595 ref = ref.lastChild; 1596 } 1597 1598 // "While ref is invisible but not an extraneous line break, and ref does 1599 // not equal node's parent, set ref to the node before it in tree order." 1600 while (isInvisible(ref) 1601 && !isExtraneousLineBreak(ref) 1602 && ref != node.parentNode) { 1603 ref = previousNode(ref); 1604 } 1605 1606 // "If ref is an editable extraneous line break, remove it from its 1607 // parent." 1608 1609 if (isEditable(ref) 1610 && isExtraneousLineBreak(ref)) { 1611 ref.parentNode.removeChild(ref); 1612 } 1613 } 1614 1615 function removeExtraneousLineBreaksAtTheEndOf(node) { 1616 // "Let ref be node." 1617 var ref = node; 1618 1619 // "While ref has children, set ref to its lastChild." 1620 while (ref.hasChildNodes()) { 1621 ref = ref.lastChild; 1622 } 1623 1624 // "While ref is invisible but not an extraneous line break, and ref does 1625 // not equal node, set ref to the node before it in tree order." 1626 while (isInvisible(ref) 1627 && !isExtraneousLineBreak(ref) 1628 && ref != node) { 1629 ref = previousNode(ref); 1630 } 1631 1632 // "If ref is an editable extraneous line break, remove it from its 1633 // parent." 1634 if (isEditable(ref) 1635 && isExtraneousLineBreak(ref)) { 1636 ref.parentNode.removeChild(ref); 1637 } 1638 } 1639 1640 // "To remove extraneous line breaks from a node, first remove extraneous line 1641 // breaks before it, then remove extraneous line breaks at the end of it." 1642 function removeExtraneousLineBreaksFrom(node) { 1643 removeExtraneousLineBreaksBefore(node); 1644 removeExtraneousLineBreaksAtTheEndOf(node); 1645 } 1646 1647 //@} 1648 ///// Wrapping a list of nodes ///// 1649 //@{ 1650 1651 function wrap(nodeList, siblingCriteria, newParentInstructions, range) { 1652 // "If not provided, sibling criteria returns false and new parent 1653 // instructions returns null." 1654 if (typeof siblingCriteria == "undefined") { 1655 siblingCriteria = function() { return false }; 1656 } 1657 if (typeof newParentInstructions == "undefined") { 1658 newParentInstructions = function() { return null }; 1659 } 1660 1661 // "If node list is empty, or the first member of node list is not 1662 // editable, return null and abort these steps." 1663 if (!nodeList.length 1664 || !isEditable(nodeList[0])) { 1665 return null; 1666 } 1667 1668 // "If node list's last member is an inline node that's not a br, and node 1669 // list's last member's nextSibling is a br, append that br to node list." 1670 if (isInlineNode(nodeList[nodeList.length - 1]) 1671 && !isNamedHtmlElement(nodeList[nodeList.length - 1], "br") 1672 && isNamedHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) { 1673 nodeList.push(nodeList[nodeList.length - 1].nextSibling); 1674 } 1675 1676 // "If the previousSibling of the first member of node list is editable and 1677 // running sibling criteria on it returns true, let new parent be the 1678 // previousSibling of the first member of node list." 1679 var newParent; 1680 if (isEditable(nodeList[0].previousSibling) 1681 && siblingCriteria(nodeList[0].previousSibling)) { 1682 newParent = nodeList[0].previousSibling; 1683 1684 // "Otherwise, if the nextSibling of the last member of node list is 1685 // editable and running sibling criteria on it returns true, let new parent 1686 // be the nextSibling of the last member of node list." 1687 } else if (isEditable(nodeList[nodeList.length - 1].nextSibling) 1688 && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) { 1689 newParent = nodeList[nodeList.length - 1].nextSibling; 1690 1691 // "Otherwise, run new parent instructions, and let new parent be the 1692 // result." 1693 } else { 1694 newParent = newParentInstructions(); 1695 } 1696 1697 // "If new parent is null, abort these steps and return null." 1698 if (!newParent) { 1699 return null; 1700 } 1701 1702 // "If new parent's parent is null:" 1703 if (!newParent.parentNode) { 1704 // "Insert new parent into the parent of the first member of node list 1705 // immediately before the first member of node list." 1706 nodeList[0].parentNode.insertBefore(newParent, nodeList[0]); 1707 1708 // "If any range has a boundary point with node equal to the parent of 1709 // new parent and offset equal to the index of new parent, add one to 1710 // that boundary point's offset." 1711 // 1712 // Try to fix range 1713 var startContainer = range.startContainer, startOffset = range.startOffset, 1714 endContainer = range.endContainer, endOffset = range.endOffset; 1715 if (startContainer == newParent.parentNode 1716 && startOffset >= getNodeIndex(newParent)) { 1717 range.setStart(startContainer, startOffset + 1); 1718 } 1719 if (endContainer == newParent.parentNode 1720 && endOffset >= getNodeIndex(newParent)) { 1721 range.setEnd(endContainer, endOffset + 1); 1722 } 1723 1724 // Only try to fix the global range. TODO remove globalRange here 1725 if (globalRange && globalRange !== range) { 1726 startContainer = globalRange.startContainer, startOffset = globalRange.startOffset, 1727 endContainer = globalRange.endContainer, endOffset = globalRange.endOffset; 1728 if (startContainer == newParent.parentNode 1729 && startOffset >= getNodeIndex(newParent)) { 1730 globalRange.setStart(startContainer, startOffset + 1); 1731 } 1732 if (endContainer == newParent.parentNode 1733 && endOffset >= getNodeIndex(newParent)) { 1734 globalRange.setEnd(endContainer, endOffset + 1); 1735 } 1736 } 1737 } 1738 1739 // "Let original parent be the parent of the first member of node list." 1740 var originalParent = nodeList[0].parentNode; 1741 1742 // "If new parent is before the first member of node list in tree order:" 1743 if (isBefore(newParent, nodeList[0])) { 1744 // "If new parent is not an inline node, but the last child of new 1745 // parent and the first member of node list are both inline nodes, and 1746 // the last child of new parent is not a br, call createElement("br") 1747 // on the ownerDocument of new parent and append the result as the last 1748 // child of new parent." 1749 if (!isInlineNode(newParent) 1750 && isInlineNode(newParent.lastChild) 1751 && isInlineNode(nodeList[0]) 1752 && !isNamedHtmlElement(newParent.lastChild, "BR")) { 1753 newParent.appendChild(newParent.ownerDocument.createElement("br")); 1754 } 1755 1756 // "For each node in node list, append node as the last child of new 1757 // parent, preserving ranges." 1758 for (var i = 0; i < nodeList.length; i++) { 1759 movePreservingRanges(nodeList[i], newParent, -1, range); 1760 } 1761 1762 // "Otherwise:" 1763 } else { 1764 // "If new parent is not an inline node, but the first child of new 1765 // parent and the last member of node list are both inline nodes, and 1766 // the last member of node list is not a br, call createElement("br") 1767 // on the ownerDocument of new parent and insert the result as the 1768 // first child of new parent." 1769 if (!isInlineNode(newParent) 1770 && isInlineNode(newParent.firstChild) 1771 && isInlineNode(nodeList[nodeList.length - 1]) 1772 && !isNamedHtmlElement(nodeList[nodeList.length - 1], "BR")) { 1773 newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild); 1774 } 1775 1776 // "For each node in node list, in reverse order, insert node as the 1777 // first child of new parent, preserving ranges." 1778 for (var i = nodeList.length - 1; i >= 0; i--) { 1779 movePreservingRanges(nodeList[i], newParent, 0, range); 1780 } 1781 } 1782 1783 // "If original parent is editable and has no children, remove it from its 1784 // parent." 1785 if (isEditable(originalParent) && !originalParent.hasChildNodes()) { 1786 originalParent.parentNode.removeChild(originalParent); 1787 } 1788 1789 // "If new parent's nextSibling is editable and running sibling criteria on 1790 // it returns true:" 1791 if (isEditable(newParent.nextSibling) 1792 && siblingCriteria(newParent.nextSibling)) { 1793 // "If new parent is not an inline node, but new parent's last child 1794 // and new parent's nextSibling's first child are both inline nodes, 1795 // and new parent's last child is not a br, call createElement("br") on 1796 // the ownerDocument of new parent and append the result as the last 1797 // child of new parent." 1798 if (!isInlineNode(newParent) 1799 && isInlineNode(newParent.lastChild) 1800 && isInlineNode(newParent.nextSibling.firstChild) 1801 && !isNamedHtmlElement(newParent.lastChild, "BR")) { 1802 newParent.appendChild(newParent.ownerDocument.createElement("br")); 1803 } 1804 1805 // "While new parent's nextSibling has children, append its first child 1806 // as the last child of new parent, preserving ranges." 1807 while (newParent.nextSibling.hasChildNodes()) { 1808 movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1, range); 1809 } 1810 1811 // "Remove new parent's nextSibling from its parent." 1812 newParent.parentNode.removeChild(newParent.nextSibling); 1813 } 1814 1815 // "Remove extraneous line breaks from new parent." 1816 removeExtraneousLineBreaksFrom(newParent); 1817 1818 // "Return new parent." 1819 return newParent; 1820 } 1821 1822 1823 //@} 1824 ///// Allowed children ///// 1825 //@{ 1826 1827 // "A name of an element with inline contents is "a", "abbr", "b", "bdi", 1828 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", 1829 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", 1830 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", 1831 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"." 1832 var namesOfElementsWithInlineContentsMap = { 1833 "A": true, "ABBR": true, "B": true, "BDI": true, "BDO": true, 1834 "CITE": true, "CODE": true, "DFN": true, "EM": true, "H1": true, "H2": true, "H3": true, "H4": true, "H5": true, "H6": true, "I": true, 1835 "KBD": true, "MARK": true, "P": true, "PRE": true, "Q": true, "RP": true, "RT": true, "RUBY": true, "S": true, "SAMP": true, "SMALL": true, 1836 "SPAN": true, "STRONG": true, "SUB": true, "SUP": true, "U": true, "VAR": true, "ACRONYM": true, "LISTING": true, "STRIKE": true, 1837 "XMP": true, "BIG": true, "BLINK": true, "FONT": true, "MARQUEE": true, "NOBR": true, "TT": true 1838 }; 1839 1840 1841 var tableRelatedElements = { 1842 "colgroup": true, 1843 "table": true, 1844 "tbody": true, 1845 "tfoot": true, 1846 "thead": true, 1847 "tr": true 1848 }; 1849 1850 var scriptRelatedElements = { 1851 "script": true, 1852 "style": true, 1853 "plaintext": true, 1854 "xmp": true 1855 }; 1856 1857 var prohibitedHeadingNestingMap = jQuery.extend({ 1858 "H1": true, 1859 "H2": true, 1860 "H3": true, 1861 "H4": true, 1862 "H5": true, 1863 "H6": true 1864 }, prohibitedParagraphChildNamesMap); 1865 var prohibitedTableNestingMap = { 1866 "CAPTION": true, 1867 "COL": true, 1868 "COLGROUP": true, 1869 "TBODY": true, 1870 "TD": true, 1871 "TFOOT": true, 1872 "TH": true, 1873 "THEAD": true, 1874 "TR": true 1875 }; 1876 var prohibitedDefNestingMap = { 1877 "DD": true, 1878 "DT": true 1879 }; 1880 var prohibitedNestingCombinationsMap = { 1881 "A": jQuery.extend({"A": true}, prohibitedParagraphChildNamesMap), 1882 "DD": prohibitedDefNestingMap, 1883 "DT": prohibitedDefNestingMap, 1884 "LI": {"LI": true}, 1885 "NOBR": jQuery.extend({"NOBR": true}, prohibitedParagraphChildNamesMap), 1886 "H1": prohibitedHeadingNestingMap, 1887 "H2": prohibitedHeadingNestingMap, 1888 "H3": prohibitedHeadingNestingMap, 1889 "H4": prohibitedHeadingNestingMap, 1890 "H5": prohibitedHeadingNestingMap, 1891 "H6": prohibitedHeadingNestingMap, 1892 "TD": prohibitedTableNestingMap, 1893 "TH": prohibitedTableNestingMap, 1894 // this is the same as namesOfElementsWithInlineContentsMap excluding a and h1-h6 elements above 1895 "ABBR": prohibitedParagraphChildNamesMap, 1896 "B": prohibitedParagraphChildNamesMap, 1897 "BDI": prohibitedParagraphChildNamesMap, 1898 "BDO": prohibitedParagraphChildNamesMap, 1899 "CITE": prohibitedParagraphChildNamesMap, 1900 "CODE": prohibitedParagraphChildNamesMap, 1901 "DFN": prohibitedParagraphChildNamesMap, 1902 "EM": prohibitedParagraphChildNamesMap, 1903 "I": prohibitedParagraphChildNamesMap, 1904 "KBD": prohibitedParagraphChildNamesMap, 1905 "MARK": prohibitedParagraphChildNamesMap, 1906 "P": prohibitedParagraphChildNamesMap, 1907 "PRE": prohibitedParagraphChildNamesMap, 1908 "Q": prohibitedParagraphChildNamesMap, 1909 "RP": prohibitedParagraphChildNamesMap, 1910 "RT": prohibitedParagraphChildNamesMap, 1911 "RUBY": prohibitedParagraphChildNamesMap, 1912 "S": prohibitedParagraphChildNamesMap, 1913 "SAMP": prohibitedParagraphChildNamesMap, 1914 "SMALL": prohibitedParagraphChildNamesMap, 1915 "SPAN": prohibitedParagraphChildNamesMap, 1916 "STRONG": prohibitedParagraphChildNamesMap, 1917 "SUB": prohibitedParagraphChildNamesMap, 1918 "SUP": prohibitedParagraphChildNamesMap, 1919 "U": prohibitedParagraphChildNamesMap, 1920 "VAR": prohibitedParagraphChildNamesMap, 1921 "ACRONYM": prohibitedParagraphChildNamesMap, 1922 "LISTING": prohibitedParagraphChildNamesMap, 1923 "STRIKE": prohibitedParagraphChildNamesMap, 1924 "XMP": prohibitedParagraphChildNamesMap, 1925 "BIG": prohibitedParagraphChildNamesMap, 1926 "BLINK": prohibitedParagraphChildNamesMap, 1927 "FONT": prohibitedParagraphChildNamesMap, 1928 "MARQUEE": prohibitedParagraphChildNamesMap, 1929 "TT": prohibitedParagraphChildNamesMap 1930 }; 1931 1932 // "An element with inline contents is an HTML element whose local name is a 1933 // name of an element with inline contents." 1934 function isElementWithInlineContents(node) { 1935 return isMappedHtmlElement(node, namesOfElementsWithInlineContentsMap); 1936 } 1937 1938 function isAllowedChild(child, parent_) { 1939 // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or 1940 // an HTML element with local name equal to one of those, and child is a 1941 // Text node whose data does not consist solely of space characters, return 1942 // false." 1943 if ((tableRelatedElements[parent_] 1944 || isHtmlElementInArray(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"])) 1945 && typeof child == "object" 1946 && child.nodeType == $_.Node.TEXT_NODE 1947 && !/^[ \t\n\f\r]*$/.test(child.data)) { 1948 return false; 1949 1950 } 1951 1952 // "If parent is "script", "style", "plaintext", or "xmp", or an HTML 1953 // element with local name equal to one of those, and child is not a Text 1954 // node, return false." 1955 if ((scriptRelatedElements[parent_] 1956 || isHtmlElementInArray(parent_, ["script", "style", "plaintext", "xmp"])) 1957 && (typeof child != "object" || child.nodeType != $_.Node.TEXT_NODE)) { 1958 return false; 1959 } 1960 1961 // "If child is a Document, DocumentFragment, or DocumentType, return 1962 // false." 1963 if (typeof child == "object" 1964 && (child.nodeType == $_.Node.DOCUMENT_NODE 1965 || child.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE 1966 || child.nodeType == $_.Node.DOCUMENT_TYPE_NODE)) { 1967 return false; 1968 } 1969 1970 // "If child is an HTML element, set child to the local name of child." 1971 if (isAnyHtmlElement(child)) { 1972 child = child.tagName.toLowerCase(); 1973 } 1974 1975 // "If child is not a string, return true." 1976 if (typeof child != "string") { 1977 return true; 1978 } 1979 1980 // "If parent is an HTML element:" 1981 if (isAnyHtmlElement(parent_)) { 1982 // "If child is "a", and parent or some ancestor of parent is an a, 1983 // return false." 1984 // 1985 // "If child is a prohibited paragraph child name and parent or some 1986 // ancestor of parent is an element with inline contents, return 1987 // false." 1988 // 1989 // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or 1990 // some ancestor of parent is an HTML element with local name "h1", 1991 // "h2", "h3", "h4", "h5", or "h6", return false." 1992 var ancestor = parent_; 1993 while (ancestor) { 1994 if (child == "a" && isNamedHtmlElement(ancestor, 'a')) { 1995 return false; 1996 1997 } 1998 if (prohibitedParagraphChildNamesMap[child.toUpperCase()] 1999 && isElementWithInlineContents(ancestor)) { 2000 return false; 2001 } 2002 if (/^h[1-6]$/.test(child) 2003 && isAnyHtmlElement(ancestor) 2004 && /^H[1-6]$/.test(ancestor.tagName)) { 2005 return false; 2006 } 2007 ancestor = ancestor.parentNode; 2008 } 2009 2010 // "Let parent be the local name of parent." 2011 parent_ = parent_.tagName.toLowerCase(); 2012 } 2013 2014 // "If parent is an Element or DocumentFragment, return true." 2015 if (typeof parent_ == "object" 2016 && (parent_.nodeType == $_.Node.ELEMENT_NODE 2017 || parent_.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE)) { 2018 return true; 2019 } 2020 2021 // "If parent is not a string, return false." 2022 if (typeof parent_ != "string") { 2023 return false; 2024 } 2025 2026 // "If parent is on the left-hand side of an entry on the following list, 2027 // then return true if child is listed on the right-hand side of that 2028 // entry, and false otherwise." 2029 switch (parent_) { 2030 case "colgroup": 2031 return child == "col"; 2032 case "table": 2033 return jQuery.inArray(child, ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]) != -1; 2034 case "tbody": 2035 case "thead": 2036 case "tfoot": 2037 return jQuery.inArray(child, ["td", "th", "tr"]) != -1; 2038 case "tr": 2039 return jQuery.inArray(child, ["td", "th"]) != -1; 2040 case "dl": 2041 return jQuery.inArray(child, ["dt", "dd"]) != -1; 2042 case "dir": 2043 case "ol": 2044 case "ul": 2045 return jQuery.inArray(child, ["dir", "li", "ol", "ul"]) != -1; 2046 case "hgroup": 2047 return /^h[1-6]$/.test(child); 2048 } 2049 2050 // "If child is "body", "caption", "col", "colgroup", "frame", "frameset", 2051 // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return 2052 // false." 2053 if (jQuery.inArray(child, ["body", "caption", "col", "colgroup", "frame", "frameset", "head", 2054 "html", "tbody", "td", "tfoot", "th", "thead", "tr"]) != -1) { 2055 return false; 2056 } 2057 2058 // "If child is "dd" or "dt" and parent is not "dl", return false." 2059 if (jQuery.inArray(child, ["dd", "dt"]) != -1 2060 && parent_ != "dl") { 2061 return false; 2062 } 2063 2064 // "If child is "li" and parent is not "ol" or "ul", return false." 2065 if (child == "li" 2066 && parent_ != "ol" 2067 && parent_ != "ul") { 2068 return false; 2069 } 2070 2071 // "If parent is on the left-hand side of an entry on the following list 2072 // and child is listed on the right-hand side of that entry, return false." 2073 var leftSide = prohibitedNestingCombinationsMap[parent_.toUpperCase()]; 2074 if (leftSide) { 2075 var rightSide = leftSide[child.toUpperCase()]; 2076 if (rightSide) { 2077 return false; 2078 } 2079 } 2080 2081 // "Return true." 2082 return true; 2083 } 2084 2085 2086 //@} 2087 2088 ////////////////////////////////////// 2089 ///// Inline formatting commands ///// 2090 ////////////////////////////////////// 2091 2092 ///// Inline formatting command definitions ///// 2093 //@{ 2094 2095 // "A node node is effectively contained in a range range if range is not 2096 // collapsed, and at least one of the following holds:" 2097 function isEffectivelyContained(node, range) { 2098 if (range.collapsed) { 2099 return false; 2100 } 2101 2102 // "node is contained in range." 2103 if (isContained(node, range)) { 2104 return true; 2105 } 2106 2107 // "node is range's start node, it is a Text node, and its length is 2108 // different from range's start offset." 2109 if (node == range.startContainer 2110 && node.nodeType == $_.Node.TEXT_NODE 2111 && getNodeLength(node) != range.startOffset) { 2112 return true; 2113 } 2114 2115 // "node is range's end node, it is a Text node, and range's end offset is 2116 // not 0." 2117 if (node == range.endContainer 2118 && node.nodeType == $_.Node.TEXT_NODE 2119 && range.endOffset != 0) { 2120 return true; 2121 } 2122 2123 // "node has at least one child; and all its children are effectively 2124 // contained in range; and either range's start node is not a descendant of 2125 // node or is not a Text node or range's start offset is zero; and either 2126 // range's end node is not a descendant of node or is not a Text node or 2127 // range's end offset is its end node's length." 2128 if (node.hasChildNodes() 2129 && $_(node.childNodes).every(function(child) { return isEffectivelyContained(child, range) }) 2130 && (!isDescendant(range.startContainer, node) 2131 || range.startContainer.nodeType != $_.Node.TEXT_NODE 2132 || range.startOffset == 0) 2133 && (!isDescendant(range.endContainer, node) 2134 || range.endContainer.nodeType != $_.Node.TEXT_NODE 2135 || range.endOffset == getNodeLength(range.endContainer))) { 2136 return true; 2137 } 2138 2139 return false; 2140 } 2141 2142 // Like get(All)ContainedNodes(), but for effectively contained nodes. 2143 function getEffectivelyContainedNodes(range, condition) { 2144 if (typeof condition == "undefined") { 2145 condition = function() { return true }; 2146 } 2147 var node = range.startContainer; 2148 while (isEffectivelyContained(node.parentNode, range)) { 2149 node = node.parentNode; 2150 } 2151 2152 var stop = nextNodeDescendants(range.endContainer); 2153 2154 var nodeList = []; 2155 while (isBefore(node, stop)) { 2156 if (isEffectivelyContained(node, range) 2157 && condition(node)) { 2158 nodeList.push(node); 2159 node = nextNodeDescendants(node); 2160 continue; 2161 } 2162 node = nextNode(node); 2163 } 2164 return nodeList; 2165 } 2166 2167 function getAllEffectivelyContainedNodes(range, condition) { 2168 if (typeof condition == "undefined") { 2169 condition = function() { return true }; 2170 } 2171 var node = range.startContainer; 2172 while (isEffectivelyContained(node.parentNode, range)) { 2173 node = node.parentNode; 2174 } 2175 2176 var stop = nextNodeDescendants(range.endContainer); 2177 2178 var nodeList = []; 2179 while (isBefore(node, stop)) { 2180 if (isEffectivelyContained(node, range) 2181 && condition(node)) { 2182 nodeList.push(node); 2183 } 2184 node = nextNode(node); 2185 } 2186 return nodeList; 2187 } 2188 2189 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element 2190 // with no attributes except possibly style; or a font element with no 2191 // attributes except possibly style, color, face, and/or size; or an a element 2192 // with no attributes except possibly style and/or href." 2193 function isModifiableElement(node) { 2194 if (!isAnyHtmlElement(node)) { 2195 return false; 2196 } 2197 2198 if (jQuery.inArray(node.tagName, ["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"]) != -1) { 2199 if (node.attributes.length == 0) { 2200 return true; 2201 } 2202 2203 if (node.attributes.length == 1 2204 && hasAttribute(node, "style")) { 2205 return true; 2206 } 2207 } 2208 2209 if (node.tagName == "FONT" || node.tagName == "A") { 2210 var numAttrs = node.attributes.length; 2211 2212 if (hasAttribute(node, "style")) { 2213 numAttrs--; 2214 } 2215 2216 if (node.tagName == "FONT") { 2217 if (hasAttribute(node, "color")) { 2218 numAttrs--; 2219 } 2220 2221 if (hasAttribute(node, "face")) { 2222 numAttrs--; 2223 } 2224 2225 if (hasAttribute(node, "size")) { 2226 numAttrs--; 2227 } 2228 } 2229 2230 if (node.tagName == "A" 2231 && hasAttribute(node, "href")) { 2232 numAttrs--; 2233 } 2234 2235 if (numAttrs == 0) { 2236 return true; 2237 } 2238 } 2239 2240 return false; 2241 } 2242 2243 function isSimpleModifiableElement(node) { 2244 // "A simple modifiable element is an HTML element for which at least one 2245 // of the following holds:" 2246 if (!isAnyHtmlElement(node)) { 2247 return false; 2248 } 2249 2250 // Only these elements can possibly be a simple modifiable element. 2251 if (jQuery.inArray(node.tagName, ["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"]) == -1) { 2252 return false; 2253 } 2254 2255 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u 2256 // element with no attributes." 2257 if (node.attributes.length == 0) { 2258 return true; 2259 } 2260 2261 // If it's got more than one attribute, everything after this fails. 2262 if (node.attributes.length > 1) { 2263 return false; 2264 } 2265 2266 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u 2267 // element with exactly one attribute, which is style, which sets no CSS 2268 // properties (including invalid or unrecognized properties)." 2269 // 2270 // Not gonna try for invalid or unrecognized. 2271 if (hasAttribute(node, "style") 2272 && getStyleLength(node) == 0) { 2273 return true; 2274 } 2275 2276 // "It is an a element with exactly one attribute, which is href." 2277 if (node.tagName == "A" 2278 && hasAttribute(node, "href")) { 2279 return true; 2280 } 2281 2282 // "It is a font element with exactly one attribute, which is either color, 2283 // face, or size." 2284 if (node.tagName == "FONT" 2285 && (hasAttribute(node, "color") 2286 || hasAttribute(node, "face") 2287 || hasAttribute(node, "size") 2288 )) { 2289 return true; 2290 } 2291 2292 // "It is a b or strong element with exactly one attribute, which is style, 2293 // and the style attribute sets exactly one CSS property (including invalid 2294 // or unrecognized properties), which is "font-weight"." 2295 if ((node.tagName == "B" || node.tagName == "STRONG") 2296 && hasAttribute(node, "style") 2297 && getStyleLength(node) == 1 2298 && node.style.fontWeight != "") { 2299 return true; 2300 } 2301 2302 // "It is an i or em element with exactly one attribute, which is style, 2303 // and the style attribute sets exactly one CSS property (including invalid 2304 // or unrecognized properties), which is "font-style"." 2305 if ((node.tagName == "I" || node.tagName == "EM") 2306 && hasAttribute(node, "style") 2307 && getStyleLength(node) == 1 2308 && node.style.fontStyle != "") { 2309 return true; 2310 } 2311 2312 // "It is an a, font, or span element with exactly one attribute, which is 2313 // style, and the style attribute sets exactly one CSS property (including 2314 // invalid or unrecognized properties), and that property is not 2315 // "text-decoration"." 2316 if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN") 2317 && hasAttribute(node, "style") 2318 && getStyleLength(node) == 1 2319 && node.style.textDecoration == "") { 2320 return true; 2321 } 2322 2323 // "It is an a, font, s, span, strike, or u element with exactly one 2324 // attribute, which is style, and the style attribute sets exactly one CSS 2325 // property (including invalid or unrecognized properties), which is 2326 // "text-decoration", which is set to "line-through" or "underline" or 2327 // "overline" or "none"." 2328 if (jQuery.inArray(node.tagName, ["A", "FONT", "S", "SPAN", "STRIKE", "U"]) != -1 2329 && hasAttribute(node, "style") 2330 && getStyleLength(node) == 1 2331 && (node.style.textDecoration == "line-through" 2332 || node.style.textDecoration == "underline" 2333 || node.style.textDecoration == "overline" 2334 || node.style.textDecoration == "none")) { 2335 return true; 2336 } 2337 2338 return false; 2339 } 2340 2341 // "Two quantities are equivalent values for a command if either both are null, 2342 // or both are strings and they're equal and the command does not define any 2343 // equivalent values, or both are strings and the command defines equivalent 2344 // values and they match the definition." 2345 function areEquivalentValues(command, val1, val2) { 2346 if (val1 === null && val2 === null) { 2347 return true; 2348 } 2349 2350 if (typeof val1 == "string" 2351 && typeof val2 == "string" 2352 && val1 == val2 2353 && !("equivalentValues" in commands[command])) { 2354 return true; 2355 } 2356 2357 if (typeof val1 == "string" 2358 && typeof val2 == "string" 2359 && "equivalentValues" in commands[command] 2360 && commands[command].equivalentValues(val1, val2)) { 2361 return true; 2362 } 2363 2364 return false; 2365 } 2366 2367 // "Two quantities are loosely equivalent values for a command if either they 2368 // are equivalent values for the command, or if the command is the fontSize 2369 // command; one of the quantities is one of "xx-small", "small", "medium", 2370 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is 2371 // the resolved value of "font-size" on a font element whose size attribute has 2372 // the corresponding value set ("1" through "7" respectively)." 2373 function areLooselyEquivalentValues(command, val1, val2) { 2374 if (areEquivalentValues(command, val1, val2)) { 2375 return true; 2376 } 2377 2378 if (command != "fontsize" 2379 || typeof val1 != "string" 2380 || typeof val2 != "string") { 2381 return false; 2382 } 2383 2384 // Static variables in JavaScript? 2385 var callee = areLooselyEquivalentValues; 2386 if (callee.sizeMap === undefined) { 2387 callee.sizeMap = {}; 2388 var font = document.createElement("font"); 2389 document.body.appendChild(font); 2390 $_( ["xx-small", "small", "medium", "large", "x-large", "xx-large", 2391 "xxx-large"] ).forEach(function(keyword) { 2392 font.size = cssSizeToLegacy(keyword); 2393 callee.sizeMap[keyword] = $_.getComputedStyle(font).fontSize; 2394 }); 2395 document.body.removeChild(font); 2396 } 2397 2398 return val1 === callee.sizeMap[val2] 2399 || val2 === callee.sizeMap[val1]; 2400 } 2401 2402 //@} 2403 ///// Assorted inline formatting command algorithms ///// 2404 //@{ 2405 2406 function getEffectiveCommandValue(node, command) { 2407 // "If neither node nor its parent is an Element, return null." 2408 if (node.nodeType != $_.Node.ELEMENT_NODE 2409 && (!node.parentNode || node.parentNode.nodeType != $_.Node.ELEMENT_NODE)) { 2410 return null; 2411 } 2412 2413 // "If node is not an Element, return the effective command value of its 2414 // parent for command." 2415 if (node.nodeType != $_.Node.ELEMENT_NODE) { 2416 return getEffectiveCommandValue(node.parentNode, command); 2417 } 2418 2419 // "If command is "createLink" or "unlink":" 2420 if (command == "createlink" || command == "unlink") { 2421 // "While node is not null, and is not an a element that has an href 2422 // attribute, set node to its parent." 2423 while (node 2424 && (!isAnyHtmlElement(node) 2425 || node.tagName != "A" 2426 || !hasAttribute(node, "href"))) { 2427 node = node.parentNode; 2428 } 2429 2430 // "If node is null, return null." 2431 if (!node) { 2432 return null; 2433 } 2434 2435 // "Return the value of node's href attribute." 2436 return node.getAttribute("href"); 2437 } 2438 2439 // "If command is "backColor" or "hiliteColor":" 2440 if (command == "backcolor" 2441 || command == "hilitecolor") { 2442 // "While the resolved value of "background-color" on node is any 2443 // fully transparent value, and node's parent is an Element, set 2444 // node to its parent." 2445 // 2446 // Another lame hack to avoid flawed APIs. 2447 while (($_.getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" 2448 || $_.getComputedStyle(node).backgroundColor === "" 2449 || $_.getComputedStyle(node).backgroundColor == "transparent") 2450 && node.parentNode 2451 && node.parentNode.nodeType == $_.Node.ELEMENT_NODE) { 2452 node = node.parentNode; 2453 } 2454 2455 // "If the resolved value of "background-color" on node is a fully 2456 // transparent value, return "rgb(255, 255, 255)"." 2457 if ($_.getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" 2458 || $_.getComputedStyle(node).backgroundColor === "" 2459 || $_.getComputedStyle(node).backgroundColor == "transparent") { 2460 return "rgb(255, 255, 255)"; 2461 } 2462 2463 // "Otherwise, return the resolved value of "background-color" for 2464 // node." 2465 return $_.getComputedStyle(node).backgroundColor; 2466 } 2467 2468 // "If command is "subscript" or "superscript":" 2469 if (command == "subscript" || command == "superscript") { 2470 // "Let affected by subscript and affected by superscript be two 2471 // boolean variables, both initially false." 2472 var affectedBySubscript = false; 2473 var affectedBySuperscript = false; 2474 2475 // "While node is an inline node:" 2476 while (isInlineNode(node)) { 2477 var verticalAlign = $_.getComputedStyle(node).verticalAlign; 2478 2479 // "If node is a sub, set affected by subscript to true." 2480 if (isNamedHtmlElement(node, 'sub')) { 2481 affectedBySubscript = true; 2482 // "Otherwise, if node is a sup, set affected by superscript to 2483 // true." 2484 } else if (isNamedHtmlElement(node, 'sup')) { 2485 affectedBySuperscript = true; 2486 } 2487 2488 // "Set node to its parent." 2489 node = node.parentNode; 2490 } 2491 2492 // "If affected by subscript and affected by superscript are both true, 2493 // return the string "mixed"." 2494 if (affectedBySubscript && affectedBySuperscript) { 2495 return "mixed"; 2496 } 2497 2498 // "If affected by subscript is true, return "subscript"." 2499 if (affectedBySubscript) { 2500 return "subscript"; 2501 } 2502 2503 // "If affected by superscript is true, return "superscript"." 2504 if (affectedBySuperscript) { 2505 return "superscript"; 2506 } 2507 2508 // "Return null." 2509 return null; 2510 } 2511 2512 // "If command is "strikethrough", and the "text-decoration" property of 2513 // node or any of its ancestors has resolved value containing 2514 // "line-through", return "line-through". Otherwise, return null." 2515 if (command == "strikethrough") { 2516 do { 2517 if ($_.getComputedStyle(node).textDecoration.indexOf("line-through") != -1) { 2518 return "line-through"; 2519 } 2520 node = node.parentNode; 2521 } while (node && node.nodeType == $_.Node.ELEMENT_NODE); 2522 return null; 2523 } 2524 2525 // "If command is "underline", and the "text-decoration" property of node 2526 // or any of its ancestors has resolved value containing "underline", 2527 // return "underline". Otherwise, return null." 2528 if (command == "underline") { 2529 do { 2530 if ($_.getComputedStyle(node).textDecoration.indexOf("underline") != -1) { 2531 return "underline"; 2532 } 2533 node = node.parentNode; 2534 } while (node && node.nodeType == $_.Node.ELEMENT_NODE); 2535 return null; 2536 } 2537 2538 if (!("relevantCssProperty" in commands[command])) { 2539 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue"; 2540 } 2541 2542 // "Return the resolved value for node of the relevant CSS property for 2543 // command." 2544 return $_.getComputedStyle(node)[commands[command].relevantCssProperty].toString(); 2545 } 2546 2547 function getSpecifiedCommandValue(element, command) { 2548 // "If command is "backColor" or "hiliteColor" and element's display 2549 // property does not have resolved value "inline", return null." 2550 if ((command == "backcolor" || command == "hilitecolor") 2551 && $_.getComputedStyle(element).display != "inline") { 2552 return null; 2553 } 2554 2555 // "If command is "createLink" or "unlink":" 2556 if (command == "createlink" || command == "unlink") { 2557 // "If element is an a element and has an href attribute, return the 2558 // value of that attribute." 2559 if (isAnyHtmlElement(element) 2560 && element.tagName == "A" 2561 && hasAttribute(element, "href")) { 2562 return element.getAttribute("href"); 2563 } 2564 2565 // "Return null." 2566 return null; 2567 } 2568 2569 // "If command is "subscript" or "superscript":" 2570 if (command == "subscript" || command == "superscript") { 2571 // "If element is a sup, return "superscript"." 2572 if (isNamedHtmlElement(element, 'sup')) { 2573 return "superscript"; 2574 } 2575 2576 // "If element is a sub, return "subscript"." 2577 if (isNamedHtmlElement(element, 'sub')) { 2578 return "subscript"; 2579 } 2580 2581 // "Return null." 2582 return null; 2583 } 2584 2585 // "If command is "strikethrough", and element has a style attribute set, 2586 // and that attribute sets "text-decoration":" 2587 if (command == "strikethrough" 2588 && element.style.textDecoration != "") { 2589 // "If element's style attribute sets "text-decoration" to a value 2590 // containing "line-through", return "line-through"." 2591 if (element.style.textDecoration.indexOf("line-through") != -1) { 2592 return "line-through"; 2593 } 2594 2595 // "Return null." 2596 return null; 2597 } 2598 2599 // "If command is "strikethrough" and element is a s or strike element, 2600 // return "line-through"." 2601 if (command == "strikethrough" 2602 && isHtmlElementInArray(element, ["S", "STRIKE"])) { 2603 return "line-through"; 2604 } 2605 2606 // "If command is "underline", and element has a style attribute set, and 2607 // that attribute sets "text-decoration":" 2608 if (command == "underline" 2609 && element.style.textDecoration != "") { 2610 // "If element's style attribute sets "text-decoration" to a value 2611 // containing "underline", return "underline"." 2612 if (element.style.textDecoration.indexOf("underline") != -1) { 2613 return "underline"; 2614 } 2615 2616 // "Return null." 2617 return null; 2618 } 2619 2620 // "If command is "underline" and element is a u element, return 2621 // "underline"." 2622 if (command == "underline" 2623 && isNamedHtmlElement(element, 'U')) { 2624 return "underline"; 2625 } 2626 2627 // "Let property be the relevant CSS property for command." 2628 var property = commands[command].relevantCssProperty; 2629 2630 // "If property is null, return null." 2631 if (property === null) { 2632 return null; 2633 } 2634 2635 // "If element has a style attribute set, and that attribute has the 2636 // effect of setting property, return the value that it sets property to." 2637 if (element.style[property] != "") { 2638 return element.style[property]; 2639 } 2640 2641 // "If element is a font element that has an attribute whose effect is 2642 // to create a presentational hint for property, return the value that the 2643 // hint sets property to. (For a size of 7, this will be the non-CSS value 2644 // "xxx-large".)" 2645 if (isHtmlNamespace(element.namespaceURI) 2646 && element.tagName == "FONT") { 2647 if (property == "color" && hasAttribute(element, "color")) { 2648 return element.color; 2649 } 2650 if (property == "fontFamily" && hasAttribute(element, "face")) { 2651 return element.face; 2652 } 2653 if (property == "fontSize" && hasAttribute(element, "size")) { 2654 // This is not even close to correct in general. 2655 var size = parseInt(element.size); 2656 if (size < 1) { 2657 size = 1; 2658 } 2659 if (size > 7) { 2660 size = 7; 2661 } 2662 return { 2663 1: "xx-small", 2664 2: "small", 2665 3: "medium", 2666 4: "large", 2667 5: "x-large", 2668 6: "xx-large", 2669 7: "xxx-large" 2670 }[size]; 2671 } 2672 } 2673 2674 // "If element is in the following list, and property is equal to the 2675 // CSS property name listed for it, return the string listed for it." 2676 // 2677 // A list follows, whose meaning is copied here. 2678 if (property == "fontWeight" 2679 && (element.tagName == "B" || element.tagName == "STRONG")) { 2680 return "bold"; 2681 } 2682 if (property == "fontStyle" 2683 && (element.tagName == "I" || element.tagName == "EM")) { 2684 return "italic"; 2685 } 2686 2687 // "Return null." 2688 return null; 2689 } 2690 2691 function reorderModifiableDescendants(node, command, newValue, range) { 2692 // "Let candidate equal node." 2693 var candidate = node; 2694 2695 // "While candidate is a modifiable element, and candidate has exactly one 2696 // child, and that child is also a modifiable element, and candidate is not 2697 // a simple modifiable element or candidate's specified command value for 2698 // command is not equivalent to new value, set candidate to its child." 2699 while (isModifiableElement(candidate) 2700 && candidate.childNodes.length == 1 2701 && isModifiableElement(candidate.firstChild) 2702 && (!isSimpleModifiableElement(candidate) 2703 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) { 2704 candidate = candidate.firstChild; 2705 } 2706 2707 // "If candidate is node, or is not a simple modifiable element, or its 2708 // specified command value is not equivalent to new value, or its effective 2709 // command value is not loosely equivalent to new value, abort these 2710 // steps." 2711 if (candidate == node 2712 || !isSimpleModifiableElement(candidate) 2713 || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue) 2714 || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) { 2715 return; 2716 } 2717 2718 // "While candidate has children, insert the first child of candidate into 2719 // candidate's parent immediately before candidate, preserving ranges." 2720 while (candidate.hasChildNodes()) { 2721 movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate), range); 2722 } 2723 2724 // "Insert candidate into node's parent immediately after node." 2725 node.parentNode.insertBefore(candidate, node.nextSibling); 2726 2727 // "Append the node as the last child of candidate, preserving ranges." 2728 movePreservingRanges(node, candidate, -1, range); 2729 } 2730 2731 var recordValuesCommands = [ 2732 "subscript", "bold", "fontname", "fontsize", "forecolor", 2733 "hilitecolor", "italic", "strikethrough", "underline" 2734 ]; 2735 2736 function recordValues(nodeList) { 2737 // "Let values be a list of (node, command, specified command value) 2738 // triples, initially empty." 2739 var values = []; 2740 2741 // "For each node in node list, for each command in the list "subscript", 2742 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic", 2743 // "strikethrough", and "underline" in that order:" 2744 2745 // Ensure we have a plain array to avoid the potential performance 2746 // overhead of a NodeList 2747 var nodes = jQuery.makeArray(nodeList); 2748 for (var i = 0; i < nodes.length; i++) { 2749 var node = nodes[i]; 2750 for (var j = 0; j < recordValuesCommands.length; j++) { 2751 var command = recordValuesCommands[j]; 2752 2753 // "Let ancestor equal node." 2754 var ancestor = node; 2755 2756 // "If ancestor is not an Element, set it to its parent." 2757 if (ancestor.nodeType != 1) { 2758 ancestor = ancestor.parentNode; 2759 } 2760 2761 // "While ancestor is an Element and its specified command value 2762 // for command is null, set it to its parent." 2763 var specifiedCommandValue = null; 2764 while (ancestor 2765 && ancestor.nodeType == 1 2766 && (specifiedCommandValue = getSpecifiedCommandValue(ancestor, command)) === null) { 2767 ancestor = ancestor.parentNode; 2768 } 2769 2770 // "If ancestor is an Element, add (node, command, ancestor's 2771 // specified command value for command) to values. Otherwise add 2772 // (node, command, null) to values." 2773 values.push([node, command, specifiedCommandValue]); 2774 } 2775 } 2776 2777 // "Return values." 2778 return values; 2779 } 2780 2781 function restoreValues(values, range) { 2782 // "For each (node, command, value) triple in values:" 2783 $_( values ).forEach(function(triple) { 2784 var node = triple[0]; 2785 var command = triple[1]; 2786 var value = triple[2]; 2787 2788 // "Let ancestor equal node." 2789 var ancestor = node; 2790 2791 // "If ancestor is not an Element, set it to its parent." 2792 if (!ancestor || ancestor.nodeType != $_.Node.ELEMENT_NODE) { 2793 ancestor = ancestor.parentNode; 2794 } 2795 2796 // "While ancestor is an Element and its specified command value for 2797 // command is null, set it to its parent." 2798 while (ancestor 2799 && ancestor.nodeType == $_.Node.ELEMENT_NODE 2800 && getSpecifiedCommandValue(ancestor, command) === null) { 2801 ancestor = ancestor.parentNode; 2802 } 2803 2804 // "If value is null and ancestor is an Element, push down values on 2805 // node for command, with new value null." 2806 if (value === null 2807 && ancestor 2808 && ancestor.nodeType == $_.Node.ELEMENT_NODE) { 2809 pushDownValues(node, command, null, range); 2810 2811 // "Otherwise, if ancestor is an Element and its specified command 2812 // value for command is not equivalent to value, or if ancestor is not 2813 // an Element and value is not null, force the value of command to 2814 // value on node." 2815 } else if ((ancestor 2816 && ancestor.nodeType == $_.Node.ELEMENT_NODE 2817 && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value)) 2818 || ((!ancestor || ancestor.nodeType != $_.Node.ELEMENT_NODE) 2819 && value !== null)) { 2820 forceValue(node, command, value, range); 2821 } 2822 }); 2823 } 2824 2825 2826 //@} 2827 ///// Clearing an element's value ///// 2828 //@{ 2829 2830 function clearValue(element, command, range) { 2831 // "If element is not editable, return the empty list." 2832 if (!isEditable(element)) { 2833 return []; 2834 } 2835 2836 // "If element's specified command value for command is null, return the 2837 // empty list." 2838 if (getSpecifiedCommandValue(element, command) === null) { 2839 return []; 2840 } 2841 2842 // "If element is a simple modifiable element:" 2843 if (isSimpleModifiableElement(element)) { 2844 // "Let children be the children of element." 2845 var children = Array.prototype.slice.call(toArray(element.childNodes)); 2846 2847 // "For each child in children, insert child into element's parent 2848 // immediately before element, preserving ranges." 2849 for (var i = 0; i < children.length; i++) { 2850 movePreservingRanges(children[i], element.parentNode, getNodeIndex(element), range); 2851 } 2852 2853 // "Remove element from its parent." 2854 element.parentNode.removeChild(element); 2855 2856 // "Return children." 2857 return children; 2858 } 2859 2860 // "If command is "strikethrough", and element has a style attribute that 2861 // sets "text-decoration" to some value containing "line-through", delete 2862 // "line-through" from the value." 2863 if (command == "strikethrough" 2864 && element.style.textDecoration.indexOf("line-through") != -1) { 2865 if (element.style.textDecoration == "line-through") { 2866 element.style.textDecoration = ""; 2867 } else { 2868 element.style.textDecoration = element.style.textDecoration.replace("line-through", ""); 2869 } 2870 if (element.getAttribute("style") == "") { 2871 element.removeAttribute("style"); 2872 } 2873 } 2874 2875 // "If command is "underline", and element has a style attribute that sets 2876 // "text-decoration" to some value containing "underline", delete 2877 // "underline" from the value." 2878 if (command == "underline" 2879 && element.style.textDecoration.indexOf("underline") != -1) { 2880 if (element.style.textDecoration == "underline") { 2881 element.style.textDecoration = ""; 2882 } else { 2883 element.style.textDecoration = element.style.textDecoration.replace("underline", ""); 2884 } 2885 if (element.getAttribute("style") == "") { 2886 element.removeAttribute("style"); 2887 } 2888 } 2889 2890 // "If the relevant CSS property for command is not null, unset the CSS 2891 // property property of element." 2892 if (commands[command].relevantCssProperty !== null) { 2893 element.style[commands[command].relevantCssProperty] = ''; 2894 if (element.getAttribute("style") == "") { 2895 element.removeAttribute("style"); 2896 } 2897 } 2898 2899 // "If element is a font element:" 2900 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") { 2901 // "If command is "foreColor", unset element's color attribute, if set." 2902 if (command == "forecolor") { 2903 element.removeAttribute("color"); 2904 } 2905 2906 // "If command is "fontName", unset element's face attribute, if set." 2907 if (command == "fontname") { 2908 element.removeAttribute("face"); 2909 2910 } 2911 2912 // "If command is "fontSize", unset element's size attribute, if set." 2913 if (command == "fontsize") { 2914 element.removeAttribute("size"); 2915 } 2916 } 2917 2918 // "If element is an a element and command is "createLink" or "unlink", 2919 // unset the href property of element." 2920 if (isNamedHtmlElement(element, 'A') 2921 && (command == "createlink" || command == "unlink")) { 2922 element.removeAttribute("href"); 2923 } 2924 2925 // "If element's specified command value for command is null, return the 2926 // empty list." 2927 if (getSpecifiedCommandValue(element, command) === null) { 2928 return []; 2929 } 2930 2931 // "Set the tag name of element to "span", and return the one-node list 2932 // consisting of the result." 2933 return [setTagName(element, "span", range)]; 2934 } 2935 2936 2937 //@} 2938 ///// Pushing down values ///// 2939 //@{ 2940 2941 function pushDownValues(node, command, newValue, range) { 2942 // "If node's parent is not an Element, abort this algorithm." 2943 if (!node.parentNode 2944 || node.parentNode.nodeType != $_.Node.ELEMENT_NODE) { 2945 return; 2946 } 2947 2948 // "If the effective command value of command is loosely equivalent to new 2949 // value on node, abort this algorithm." 2950 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 2951 return; 2952 } 2953 2954 // "Let current ancestor be node's parent." 2955 var currentAncestor = node.parentNode; 2956 2957 // "Let ancestor list be a list of Nodes, initially empty." 2958 var ancestorList = []; 2959 2960 // "While current ancestor is an editable Element and the effective command 2961 // value of command is not loosely equivalent to new value on it, append 2962 // current ancestor to ancestor list, then set current ancestor to its 2963 // parent." 2964 while (isEditable(currentAncestor) 2965 && currentAncestor.nodeType == $_.Node.ELEMENT_NODE 2966 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) { 2967 ancestorList.push(currentAncestor); 2968 currentAncestor = currentAncestor.parentNode; 2969 } 2970 2971 // "If ancestor list is empty, abort this algorithm." 2972 if (!ancestorList.length) { 2973 return; 2974 } 2975 2976 // "Let propagated value be the specified command value of command on the 2977 // last member of ancestor list." 2978 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command); 2979 2980 // "If propagated value is null and is not equal to new value, abort this 2981 // algorithm." 2982 if (propagatedValue === null && propagatedValue != newValue) { 2983 return; 2984 2985 } 2986 2987 // "If the effective command value for the parent of the last member of 2988 // ancestor list is not loosely equivalent to new value, and new value is 2989 // not null, abort this algorithm." 2990 if (newValue !== null 2991 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) { 2992 return; 2993 } 2994 2995 // "While ancestor list is not empty:" 2996 while (ancestorList.length) { 2997 // "Let current ancestor be the last member of ancestor list." 2998 // "Remove the last member from ancestor list." 2999 var currentAncestor = ancestorList.pop(); 3000 3001 // "If the specified command value of current ancestor for command is 3002 // not null, set propagated value to that value." 3003 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { 3004 propagatedValue = getSpecifiedCommandValue(currentAncestor, command); 3005 } 3006 3007 // "Let children be the children of current ancestor." 3008 var children = Array.prototype.slice.call(toArray(currentAncestor.childNodes)); 3009 3010 // "If the specified command value of current ancestor for command is 3011 // not null, clear the value of current ancestor." 3012 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { 3013 clearValue(currentAncestor, command, range); 3014 } 3015 3016 // "For every child in children:" 3017 for (var i = 0; i < children.length; i++) { 3018 var child = children[i]; 3019 3020 // "If child is node, continue with the next child." 3021 if (child == node) { 3022 continue; 3023 } 3024 3025 // "If child is an Element whose specified command value for 3026 // command is neither null nor equivalent to propagated value, 3027 // continue with the next child." 3028 if (child.nodeType == $_.Node.ELEMENT_NODE 3029 && getSpecifiedCommandValue(child, command) !== null 3030 && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) { 3031 continue; 3032 } 3033 3034 // "If child is the last member of ancestor list, continue with the 3035 // next child." 3036 if (child == ancestorList[ancestorList.length - 1]) { 3037 continue; 3038 } 3039 3040 // "Force the value of child, with command as in this algorithm 3041 // and new value equal to propagated value." 3042 forceValue(child, command, propagatedValue, range); 3043 } 3044 } 3045 } 3046 3047 3048 //@} 3049 ///// Forcing the value of a node ///// 3050 //@{ 3051 3052 function forceValue(node, command, newValue, range) { 3053 // "If node's parent is null, abort this algorithm." 3054 if (!node.parentNode) { 3055 return; 3056 } 3057 3058 // "If new value is null, abort this algorithm." 3059 if (newValue === null) { 3060 return; 3061 } 3062 3063 // "If node is an allowed child of "span":" 3064 if (isAllowedChild(node, "span")) { 3065 // "Reorder modifiable descendants of node's previousSibling." 3066 reorderModifiableDescendants(node.previousSibling, command, newValue, range); 3067 3068 // "Reorder modifiable descendants of node's nextSibling." 3069 reorderModifiableDescendants(node.nextSibling, command, newValue, range); 3070 3071 // "Wrap the one-node list consisting of node, with sibling criteria 3072 // returning true for a simple modifiable element whose specified 3073 // command value is equivalent to new value and whose effective command 3074 // value is loosely equivalent to new value and false otherwise, and 3075 // with new parent instructions returning null." 3076 wrap([node], 3077 function(node) { 3078 return isSimpleModifiableElement(node) 3079 && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue) 3080 && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue); 3081 }, 3082 function() { return null }, 3083 range 3084 ); 3085 } 3086 3087 // "If the effective command value of command is loosely equivalent to new 3088 // value on node, abort this algorithm." 3089 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 3090 return; 3091 } 3092 3093 // "If node is not an allowed child of "span":" 3094 if (!isAllowedChild(node, "span")) { 3095 // "Let children be all children of node, omitting any that are 3096 // Elements whose specified command value for command is neither null 3097 // nor equivalent to new value." 3098 var children = []; 3099 for (var i = 0; i < node.childNodes.length; i++) { 3100 if (node.childNodes[i].nodeType == $_.Node.ELEMENT_NODE) { 3101 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); 3102 3103 if (specifiedValue !== null 3104 && !areEquivalentValues(command, newValue, specifiedValue)) { 3105 continue; 3106 } 3107 } 3108 children.push(node.childNodes[i]); 3109 } 3110 3111 // "Force the value of each Node in children, with command and new 3112 // value as in this invocation of the algorithm." 3113 for (var i = 0; i < children.length; i++) { 3114 forceValue(children[i], command, newValue, range); 3115 } 3116 3117 // "Abort this algorithm." 3118 return; 3119 } 3120 3121 // "If the effective command value of command is loosely equivalent to new 3122 // value on node, abort this algorithm." 3123 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 3124 return; 3125 } 3126 3127 // "Let new parent be null." 3128 var newParent = null; 3129 3130 // "If the CSS styling flag is false:" 3131 if (!cssStylingFlag) { 3132 // "If command is "bold" and new value is "bold", let new parent be the 3133 // result of calling createElement("b") on the ownerDocument of node." 3134 if (command == "bold" && (newValue == "bold" || newValue == "700")) { 3135 newParent = node.ownerDocument.createElement("b"); 3136 } 3137 3138 // "If command is "italic" and new value is "italic", let new parent be 3139 // the result of calling createElement("i") on the ownerDocument of 3140 // node." 3141 if (command == "italic" && newValue == "italic") { 3142 newParent = node.ownerDocument.createElement("i"); 3143 } 3144 3145 // "If command is "strikethrough" and new value is "line-through", let 3146 // new parent be the result of calling createElement("s") on the 3147 // ownerDocument of node." 3148 if (command == "strikethrough" && newValue == "line-through") { 3149 newParent = node.ownerDocument.createElement("s"); 3150 } 3151 3152 // "If command is "underline" and new value is "underline", let new 3153 // parent be the result of calling createElement("u") on the 3154 // ownerDocument of node." 3155 if (command == "underline" && newValue == "underline") { 3156 newParent = node.ownerDocument.createElement("u"); 3157 } 3158 3159 // "If command is "foreColor", and new value is fully opaque with red, 3160 // green, and blue components in the range 0 to 255:" 3161 if (command == "forecolor" && parseSimpleColor(newValue)) { 3162 // "Let new parent be the result of calling createElement("span") 3163 // on the ownerDocument of node." 3164 // NOTE: modified this process to create span elements with style attributes 3165 // instead of oldschool font tags with color attributes 3166 newParent = node.ownerDocument.createElement("span"); 3167 3168 // "If new value is an extended color keyword, set the color 3169 // attribute of new parent to new value." 3170 // 3171 // "Otherwise, set the color attribute of new parent to the result 3172 // of applying the rules for serializing simple color values to new 3173 // value (interpreted as a simple color)." 3174 jQuery(newParent).css('color', parseSimpleColor(newValue)); 3175 } 3176 3177 // "If command is "fontName", let new parent be the result of calling 3178 // createElement("font") on the ownerDocument of node, then set the 3179 // face attribute of new parent to new value." 3180 if (command == "fontname") { 3181 newParent = node.ownerDocument.createElement("font"); 3182 newParent.face = newValue; 3183 } 3184 } 3185 3186 // "If command is "createLink" or "unlink":" 3187 if (command == "createlink" || command == "unlink") { 3188 // "Let new parent be the result of calling createElement("a") on the 3189 // ownerDocument of node." 3190 newParent = node.ownerDocument.createElement("a"); 3191 3192 // "Set the href attribute of new parent to new value." 3193 newParent.setAttribute("href", newValue); 3194 3195 // "Let ancestor be node's parent." 3196 var ancestor = node.parentNode; 3197 3198 // "While ancestor is not null:" 3199 while (ancestor) { 3200 // "If ancestor is an a, set the tag name of ancestor to "span", 3201 // and let ancestor be the result." 3202 if (isNamedHtmlElement(ancestor, 'A')) { 3203 ancestor = setTagName(ancestor, "span", range); 3204 } 3205 3206 // "Set ancestor to its parent." 3207 ancestor = ancestor.parentNode; 3208 } 3209 } 3210 3211 // "If command is "fontSize"; and new value is one of "xx-small", "small", 3212 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the 3213 // CSS styling flag is false, or new value is "xxx-large": let new parent 3214 // be the result of calling createElement("font") on the ownerDocument of 3215 // node, then set the size attribute of new parent to the number from the 3216 // following table based on new value: [table omitted]" 3217 if (command == "fontsize" 3218 && jQuery.inArray(newValue, ["xx-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) != -1 3219 && (!cssStylingFlag || newValue == "xxx-large")) { 3220 newParent = node.ownerDocument.createElement("font"); 3221 newParent.size = cssSizeToLegacy(newValue); 3222 } 3223 3224 // "If command is "subscript" or "superscript" and new value is 3225 // "subscript", let new parent be the result of calling 3226 // createElement("sub") on the ownerDocument of node." 3227 if ((command == "subscript" || command == "superscript") 3228 && newValue == "subscript") { 3229 newParent = node.ownerDocument.createElement("sub"); 3230 } 3231 3232 // "If command is "subscript" or "superscript" and new value is 3233 // "superscript", let new parent be the result of calling 3234 // createElement("sup") on the ownerDocument of node." 3235 if ((command == "subscript" || command == "superscript") 3236 && newValue == "superscript") { 3237 newParent = node.ownerDocument.createElement("sup"); 3238 } 3239 3240 // "If new parent is null, let new parent be the result of calling 3241 // createElement("span") on the ownerDocument of node." 3242 if (!newParent) { 3243 newParent = node.ownerDocument.createElement("span"); 3244 } 3245 3246 // "Insert new parent in node's parent before node." 3247 node.parentNode.insertBefore(newParent, node); 3248 3249 // "If the effective command value of command for new parent is not loosely 3250 // equivalent to new value, and the relevant CSS property for command is 3251 // not null, set that CSS property of new parent to new value (if the new 3252 // value would be valid)." 3253 var property = commands[command].relevantCssProperty; 3254 if (property !== null 3255 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) { 3256 newParent.style[property] = newValue; 3257 } 3258 3259 // "If command is "strikethrough", and new value is "line-through", and the 3260 // effective command value of "strikethrough" for new parent is not 3261 // "line-through", set the "text-decoration" property of new parent to 3262 // "line-through"." 3263 if (command == "strikethrough" 3264 && newValue == "line-through" 3265 && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") { 3266 newParent.style.textDecoration = "line-through"; 3267 } 3268 3269 // "If command is "underline", and new value is "underline", and the 3270 // effective command value of "underline" for new parent is not 3271 // "underline", set the "text-decoration" property of new parent to 3272 // "underline"." 3273 if (command == "underline" 3274 && newValue == "underline" 3275 && getEffectiveCommandValue(newParent, "underline") != "underline") { 3276 newParent.style.textDecoration = "underline"; 3277 } 3278 3279 // "Append node to new parent as its last child, preserving ranges." 3280 movePreservingRanges(node, newParent, newParent.childNodes.length, range); 3281 3282 // "If node is an Element and the effective command value of command for 3283 // node is not loosely equivalent to new value:" 3284 if (node.nodeType == $_.Node.ELEMENT_NODE 3285 && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 3286 // "Insert node into the parent of new parent before new parent, 3287 // preserving ranges." 3288 movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent), range); 3289 3290 // "Remove new parent from its parent." 3291 newParent.parentNode.removeChild(newParent); 3292 3293 // "Let children be all children of node, omitting any that are 3294 // Elements whose specified command value for command is neither null 3295 // nor equivalent to new value." 3296 var children = []; 3297 for (var i = 0; i < node.childNodes.length; i++) { 3298 if (node.childNodes[i].nodeType == $_.Node.ELEMENT_NODE) { 3299 var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); 3300 3301 if (specifiedValue !== null 3302 && !areEquivalentValues(command, newValue, specifiedValue)) { 3303 continue; 3304 } 3305 } 3306 children.push(node.childNodes[i]); 3307 } 3308 3309 // "Force the value of each Node in children, with command and new 3310 // value as in this invocation of the algorithm." 3311 for (var i = 0; i < children.length; i++) { 3312 forceValue(children[i], command, newValue, range); 3313 } 3314 } 3315 } 3316 3317 3318 //@} 3319 ///// Setting the selection's value ///// 3320 //@{ 3321 3322 function setSelectionValue(command, newValue, range) { 3323 3324 // Use current selected range if no range passed 3325 range = range || getActiveRange(); 3326 3327 // "If there is no editable text node effectively contained in the active 3328 // range:" 3329 if (!$_( getAllEffectivelyContainedNodes(range) ) 3330 .filter(function(node) { return node.nodeType == $_.Node.TEXT_NODE}, true) 3331 .some(isEditable)) { 3332 // "If command has inline command activated values, set the state 3333 // override to true if new value is among them and false if it's not." 3334 if ("inlineCommandActivatedValues" in commands[command]) { 3335 setStateOverride(command, 3336 $_(commands[command].inlineCommandActivatedValues).indexOf(newValue) != -1, 3337 range); 3338 } 3339 3340 // "If command is "subscript", unset the state override for 3341 // "superscript"." 3342 if (command == "subscript") { 3343 unsetStateOverride("superscript", range); 3344 } 3345 3346 // "If command is "superscript", unset the state override for 3347 // "subscript"." 3348 if (command == "superscript") { 3349 unsetStateOverride("subscript", range); 3350 } 3351 3352 // "If new value is null, unset the value override (if any)." 3353 if (newValue === null) { 3354 unsetValueOverride(command, range); 3355 3356 // "Otherwise, if command has a value specified, set the value override 3357 // to new value." 3358 } else if ("value" in commands[command]) { 3359 setValueOverride(command, newValue, range); 3360 } 3361 3362 // "Abort these steps." 3363 return; 3364 } 3365 3366 // "If the active range's start node is an editable Text node, and its 3367 // start offset is neither zero nor its start node's length, call 3368 // splitText() on the active range's start node, with argument equal to the 3369 // active range's start offset. Then set the active range's start node to 3370 // the result, and its start offset to zero." 3371 if (isEditable(range.startContainer) 3372 && range.startContainer.nodeType == $_.Node.TEXT_NODE 3373 && range.startOffset != 0 3374 && range.startOffset != getNodeLength(range.startContainer)) { 3375 // Account for browsers not following range mutation rules 3376 var newNode = range.startContainer.splitText(range.startOffset); 3377 var newActiveRange = Aloha.createRange(); 3378 if (range.startContainer == range.endContainer) { 3379 var newEndOffset = range.endOffset - range.startOffset; 3380 newActiveRange.setEnd(newNode, newEndOffset); 3381 range.setEnd(newNode, newEndOffset); 3382 } 3383 newActiveRange.setStart(newNode, 0); 3384 Aloha.getSelection().removeAllRanges(); 3385 Aloha.getSelection().addRange(newActiveRange); 3386 3387 range.setStart(newNode, 0); 3388 } 3389 3390 // "If the active range's end node is an editable Text node, and its end 3391 // offset is neither zero nor its end node's length, call splitText() on 3392 // the active range's end node, with argument equal to the active range's 3393 // end offset." 3394 if (isEditable(range.endContainer) 3395 && range.endContainer.nodeType == $_.Node.TEXT_NODE 3396 && range.endOffset != 0 3397 && range.endOffset != getNodeLength(range.endContainer)) { 3398 // IE seems to mutate the range incorrectly here, so we need correction 3399 // here as well. The active range will be temporarily in orphaned 3400 // nodes, so calling getActiveRange() after splitText() but before 3401 // fixing the range will throw an exception. 3402 // TODO: check if this is still neccessary 3403 var activeRange = range; 3404 var newStart = [activeRange.startContainer, activeRange.startOffset]; 3405 var newEnd = [activeRange.endContainer, activeRange.endOffset]; 3406 activeRange.endContainer.splitText(activeRange.endOffset); 3407 activeRange.setStart(newStart[0], newStart[1]); 3408 activeRange.setEnd(newEnd[0], newEnd[1]); 3409 3410 Aloha.getSelection().removeAllRanges(); 3411 Aloha.getSelection().addRange(activeRange); 3412 } 3413 3414 // "Let element list be all editable Elements effectively contained in the 3415 // active range. 3416 // 3417 // "For each element in element list, clear the value of element." 3418 $_( getAllEffectivelyContainedNodes(getActiveRange(), function(node) { 3419 return isEditable(node) && node.nodeType == $_.Node.ELEMENT_NODE; 3420 }) ).forEach(function(element) { 3421 clearValue(element, command, range); 3422 }); 3423 3424 // "Let node list be all editable nodes effectively contained in the active 3425 // range. 3426 // 3427 // "For each node in node list:" 3428 $_( getAllEffectivelyContainedNodes(range, isEditable) ).forEach(function(node) { 3429 // "Push down values on node." 3430 pushDownValues(node, command, newValue, range); 3431 3432 // "Force the value of node." 3433 forceValue(node, command, newValue, range); 3434 }); 3435 } 3436 3437 /** 3438 * attempt to retrieve a block like a table or an Aloha Block 3439 * which is located one step right of the current caret position. 3440 * If an appropriate element is found it will be returned or 3441 * false otherwise 3442 * 3443 * @param {element} node current node we're in 3444 * @param {number} offset current offset within that node 3445 * 3446 * @return the dom node if found or false if no appropriate 3447 * element was found 3448 */ 3449 function getBlockAtNextPosition(node, offset) { 3450 // if we're inside a text node we first have to check 3451 // if there is nothing but tabs, newlines or the like 3452 // after our current cursor position 3453 if (node.nodeType === $_.Node.TEXT_NODE && 3454 offset < node.length) { 3455 for (var i = offset; i < node.length; i++) { 3456 if ((node.data.charAt(i) !== '\t' && 3457 node.data.charAt(i) !== '\r' && 3458 node.data.charAt(i) !== '\n') || 3459 node.data.charCodeAt(i) === 160) { // 3460 // this is a character that has to be deleted first 3461 return false; 3462 } 3463 } 3464 } 3465 3466 // try the most simple approach first: the next sibling 3467 // is a table 3468 if (node.nextSibling && 3469 node.nextSibling.className && 3470 node.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) { 3471 return node.nextSibling; 3472 } 3473 3474 // since we got only ignorable whitespace here determine if 3475 // our nodes parents next sibling is a table 3476 if (node.parentNode && 3477 node.parentNode.nextSibling && 3478 node.parentNode.nextSibling.className && 3479 node.parentNode.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) { 3480 return node.parentNode.nextSibling; 3481 } 3482 3483 // our parents nextsibling is a pure whitespace node such as 3484 // generated by sourcecode indentation so we'll check for 3485 // the next next sibling 3486 if (node.parentNode && 3487 node.parentNode.nextSibling && 3488 isWhitespaceNode(node.parentNode.nextSibling) && 3489 node.parentNode.nextSibling.nextSibling && 3490 node.parentNode.nextSibling.nextSibling.className && 3491 node.parentNode.nextSibling.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) { 3492 return node.parentNode.nextSibling.nextSibling; 3493 } 3494 3495 // Note: the search above works for tables, since they cannot be 3496 // nested deeply in paragraphs and other formatting tags. If this code 3497 // is extended to work also for other blocks, the search probably needs to be adapted 3498 } 3499 3500 /** 3501 * Attempt to retrieve a block like a table or an Aloha Block 3502 * which is located right before the current position. 3503 * If an appropriate element is found, it will be returned or 3504 * false otherwise 3505 * 3506 * @param {element} node current node 3507 * @param {offset} offset current offset 3508 * 3509 * @return dom node of found or false if no appropriate 3510 * element was found 3511 */ 3512 function getBlockAtPreviousPosition(node, offset) { 3513 if (node.nodeType === $_.Node.TEXT_NODE && offset > 0) { 3514 for (var i = offset-1; i >= 0; i--) { 3515 if ((node.data.charAt(i) !== '\t' && 3516 node.data.charAt(i) !== '\r' && 3517 node.data.charAt(i) !== '\n') || 3518 node.data.charCodeAt(i) === 160) { // 3519 // this is a character that has to be deleted first 3520 return false; 3521 } 3522 } 3523 } 3524 3525 // try the previous sibling 3526 if (node.previousSibling && 3527 node.previousSibling.className && 3528 node.previousSibling.className.indexOf("aloha-table-wrapper") >= 0) { 3529 return node.previousSibling; 3530 } 3531 3532 // try the parent's previous sibling 3533 if (node.parentNode && 3534 node.parentNode.previousSibling && 3535 node.parentNode.previousSibling.className && 3536 node.parentNode.previousSibling.className.indexOf("aloha-table-wrapper") >= 0) { 3537 return node.parentNode.previousSibling; 3538 } 3539 3540 // the parent's previous sibling might be a whitespace node 3541 if (node.parentNode && 3542 node.parentNode.previousSibling && 3543 isWhitespaceNode(node.parentNode.previousSibling) && 3544 node.parentNode.previousSibling.previousSibling && 3545 node.parentNode.previousSibling.previousSibling.className && 3546 node.parentNode.previousSibling.previousSibling.className.indexOf('aloha-table-wrapper') >= 0) { 3547 return node.parentNode.previousSibling.previousSibling; 3548 } 3549 3550 // Note: the search above works for tables, since they cannot be 3551 // nested deeply in paragraphs and other formatting tags. If this code 3552 // is extended to work also for other blocks, the search probably needs to be adapted 3553 3554 return false; 3555 } 3556 3557 3558 //@} 3559 ///// The backColor command ///// 3560 //@{ 3561 commands.backcolor = { 3562 // Copy-pasted, same as hiliteColor 3563 action: function(value) { 3564 // Action is further copy-pasted, same as foreColor 3565 3566 // "If value is not a valid CSS color, prepend "#" to it." 3567 // 3568 // "If value is still not a valid CSS color, or if it is currentColor, 3569 // abort these steps and do nothing." 3570 // 3571 // Cheap hack for testing, no attempt to be comprehensive. 3572 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 3573 value = "#" + value; 3574 } 3575 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) 3576 && !parseSimpleColor(value) 3577 && value.toLowerCase() != "transparent") { 3578 return; 3579 } 3580 3581 // "Set the selection's value to value." 3582 setSelectionValue("backcolor", value); 3583 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", 3584 equivalentValues: function(val1, val2) { 3585 // "Either both strings are valid CSS colors and have the same red, 3586 // green, blue, and alpha components, or neither string is a valid CSS 3587 // color." 3588 return normalizeColor(val1) === normalizeColor(val2); 3589 } 3590 }; 3591 3592 //@} 3593 ///// The bold command ///// 3594 //@{ 3595 commands.bold = { 3596 action: function(value, range) { 3597 // "If queryCommandState("bold") returns true, set the selection's 3598 // value to "normal". Otherwise set the selection's value to "bold"." 3599 if (myQueryCommandState("bold", range)) { 3600 setSelectionValue("bold", "normal", range); 3601 } else { 3602 setSelectionValue("bold", "bold", range); 3603 } 3604 }, 3605 inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"], 3606 relevantCssProperty: "fontWeight", 3607 equivalentValues: function(val1, val2) { 3608 // "Either the two strings are equal, or one is "bold" and the other is 3609 // "700", or one is "normal" and the other is "400"." 3610 return val1 == val2 3611 || (val1 == "bold" && val2 == "700") 3612 || (val1 == "700" && val2 == "bold") 3613 || (val1 == "normal" && val2 == "400") 3614 || (val1 == "400" && val2 == "normal"); 3615 } 3616 }; 3617 3618 //@} 3619 ///// The createLink command ///// 3620 //@{ 3621 commands.createlink = { 3622 action: function(value) { 3623 // "If value is the empty string, abort these steps and do nothing." 3624 if (value === "") { 3625 return; 3626 } 3627 3628 // "For each editable a element that has an href attribute and is an 3629 // ancestor of some node effectively contained in the active range, set 3630 // that a element's href attribute to value." 3631 // 3632 // TODO: We don't actually do this in tree order, not that it matters 3633 // unless you're spying with mutation events. 3634 $_( getAllEffectivelyContainedNodes(getActiveRange()) ).forEach(function(node) { 3635 $_( getAncestors(node) ).forEach(function(ancestor) { 3636 if (isEditable(ancestor) 3637 && isNamedHtmlElement(ancestor, 'a') 3638 && hasAttribute(ancestor, "href")) { 3639 ancestor.setAttribute("href", value); 3640 } 3641 }); 3642 }); 3643 3644 // "Set the selection's value to value." 3645 setSelectionValue("createlink", value); 3646 }, standardInlineValueCommand: true 3647 }; 3648 3649 //@} 3650 ///// The fontName command ///// 3651 //@{ 3652 commands.fontname = { 3653 action: function(value) { 3654 // "Set the selection's value to value." 3655 setSelectionValue("fontname", value); 3656 }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily" 3657 }; 3658 3659 //@} 3660 ///// The fontSize command ///// 3661 //@{ 3662 3663 // Helper function for fontSize's action plus queryOutputHelper. It's just the 3664 // middle of fontSize's action, ripped out into its own function. 3665 function normalizeFontSize(value) { 3666 // "Strip leading and trailing whitespace from value." 3667 // 3668 // Cheap hack, not following the actual algorithm. 3669 value = $_(value).trim(); 3670 3671 // "If value is a valid floating point number, or would be a valid 3672 // floating point number if a single leading "+" character were 3673 // stripped:" 3674 if (/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) { 3675 var mode; 3676 3677 // "If the first character of value is "+", delete the character 3678 // and let mode be "relative-plus"." 3679 if (value[0] == "+") { 3680 value = value.slice(1); 3681 mode = "relative-plus"; 3682 // "Otherwise, if the first character of value is "-", delete the 3683 // character and let mode be "relative-minus"." 3684 } else if (value[0] == "-") { 3685 value = value.slice(1); 3686 mode = "relative-minus"; 3687 // "Otherwise, let mode be "absolute"." 3688 } else { 3689 mode = "absolute"; 3690 } 3691 3692 // "Apply the rules for parsing non-negative integers to value, and 3693 // let number be the result." 3694 // 3695 // Another cheap hack. 3696 var num = parseInt(value); 3697 3698 // "If mode is "relative-plus", add three to number." 3699 if (mode == "relative-plus") { 3700 num += 3; 3701 } 3702 3703 // "If mode is "relative-minus", negate number, then add three to 3704 // it." 3705 if (mode == "relative-minus") { 3706 num = 3 - num; 3707 } 3708 3709 // "If number is less than one, let number equal 1." 3710 if (num < 1) { 3711 num = 1; 3712 } 3713 3714 // "If number is greater than seven, let number equal 7." 3715 if (num > 7) { 3716 num = 7; 3717 } 3718 3719 // "Set value to the string here corresponding to number:" [table 3720 // omitted] 3721 value = { 3722 1: "xx-small", 3723 2: "small", 3724 3: "medium", 3725 4: "large", 3726 5: "x-large", 3727 6: "xx-large", 3728 7: "xxx-large" 3729 }[num]; 3730 } 3731 3732 return value; 3733 } 3734 3735 commands.fontsize = { 3736 action: function(value) { 3737 // "If value is the empty string, abort these steps and do nothing." 3738 if (value === "") { 3739 return; 3740 } 3741 3742 value = normalizeFontSize(value); 3743 3744 // "If value is not one of the strings "xx-small", "x-small", "small", 3745 // "medium", "large", "x-large", "xx-large", "xxx-large", and is not a 3746 // valid CSS absolute length, then abort these steps and do nothing." 3747 // 3748 // More cheap hacks to skip valid CSS absolute length checks. 3749 if (jQuery.inArray(value, ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) == -1 3750 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc)$/.test(value)) { 3751 return; 3752 } 3753 3754 // "Set the selection's value to value." 3755 setSelectionValue("fontsize", value); 3756 }, 3757 indeterm: function() { 3758 // "True if among editable Text nodes that are effectively contained in 3759 // the active range, there are two that have distinct effective command 3760 // values. Otherwise false." 3761 return $_( getAllEffectivelyContainedNodes(getActiveRange(), function(node) { 3762 return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE; 3763 }) ).map(function(node) { 3764 return getEffectiveCommandValue(node, "fontsize"); 3765 }, true).filter(function(value, i, arr) { 3766 return $_(arr.slice(0, i)).indexOf(value) == -1; 3767 }).length >= 2; 3768 }, 3769 value: function(range) { 3770 // "Let pixel size be the effective command value of the first editable 3771 // Text node that is effectively contained in the active range, or if 3772 // there is no such node, the effective command value of the active 3773 // range's start node, in either case interpreted as a number of 3774 // pixels." 3775 var node = getAllEffectivelyContainedNodes(range, function(node) { 3776 return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE; 3777 })[0]; 3778 if (node === undefined) { 3779 node = range.startContainer; 3780 } 3781 var pixelSize = getEffectiveCommandValue(node, "fontsize"); 3782 3783 // "Return the legacy font size for pixel size." 3784 return getLegacyFontSize(pixelSize); 3785 }, relevantCssProperty: "fontSize" 3786 }; 3787 3788 function getLegacyFontSize(size) { 3789 // For convenience in other places in my code, I handle all sizes, not just 3790 // pixel sizes as the spec says. This means pixel sizes have to be passed 3791 // in suffixed with "px", not as plain numbers. 3792 size = normalizeFontSize(size); 3793 3794 if (jQuery.inArray(size, ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) == -1 3795 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) { 3796 // There is no sensible legacy size for things like "2em". 3797 return null; 3798 } 3799 3800 var font = document.createElement("font"); 3801 document.body.appendChild(font); 3802 if (size == "xxx-large") { 3803 font.size = 7; 3804 } else { 3805 font.style.fontSize = size; 3806 } 3807 var pixelSize = parseInt($_.getComputedStyle(font).fontSize); 3808 document.body.removeChild(font); 3809 3810 // "Let returned size be 1." 3811 var returnedSize = 1; 3812 3813 // "While returned size is less than 7:" 3814 while (returnedSize < 7) { 3815 3816 // "Let lower bound be the resolved value of "font-size" in pixels 3817 // of a font element whose size attribute is set to returned size." 3818 var font = document.createElement("font"); 3819 font.size = returnedSize; 3820 document.body.appendChild(font); 3821 var lowerBound = parseInt($_.getComputedStyle(font).fontSize); 3822 3823 // "Let upper bound be the resolved value of "font-size" in pixels 3824 // of a font element whose size attribute is set to one plus 3825 // returned size." 3826 font.size = 1 + returnedSize; 3827 var upperBound = parseInt($_.getComputedStyle(font).fontSize); 3828 document.body.removeChild(font); 3829 3830 // "Let average be the average of upper bound and lower bound." 3831 var average = (upperBound + lowerBound)/2; 3832 3833 // "If pixel size is less than average, return the one-element 3834 // string consisting of the digit returned size." 3835 if (pixelSize < average) { 3836 return String(returnedSize); 3837 } 3838 3839 // "Add one to returned size." 3840 returnedSize++; 3841 } 3842 3843 // "Return "7"." 3844 return "7"; 3845 } 3846 3847 //@} 3848 ///// The foreColor command ///// 3849 //@{ 3850 commands.forecolor = { 3851 action: function(value) { 3852 // Copy-pasted, same as backColor and hiliteColor 3853 3854 // "If value is not a valid CSS color, prepend "#" to it." 3855 // 3856 // "If value is still not a valid CSS color, or if it is currentColor, 3857 // abort these steps and do nothing." 3858 // 3859 // Cheap hack for testing, no attempt to be comprehensive. 3860 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 3861 value = "#" + value; 3862 } 3863 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) 3864 && !parseSimpleColor(value) 3865 && value.toLowerCase() != "transparent") { 3866 return; 3867 } 3868 3869 // "Set the selection's value to value." 3870 setSelectionValue("forecolor", value); 3871 }, standardInlineValueCommand: true, relevantCssProperty: "color", 3872 equivalentValues: function(val1, val2) { 3873 // "Either both strings are valid CSS colors and have the same red, 3874 // green, blue, and alpha components, or neither string is a valid CSS 3875 // color." 3876 return normalizeColor(val1) === normalizeColor(val2); 3877 } 3878 }; 3879 3880 //@} 3881 ///// The hiliteColor command ///// 3882 //@{ 3883 commands.hilitecolor = { 3884 // Copy-pasted, same as backColor 3885 action: function(value) { 3886 // Action is further copy-pasted, same as foreColor 3887 3888 // "If value is not a valid CSS color, prepend "#" to it." 3889 // 3890 // "If value is still not a valid CSS color, or if it is currentColor, 3891 // abort these steps and do nothing." 3892 // 3893 // Cheap hack for testing, no attempt to be comprehensive. 3894 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 3895 value = "#" + value; 3896 } 3897 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) 3898 && !parseSimpleColor(value) 3899 && value.toLowerCase() != "transparent") { 3900 return; 3901 } 3902 3903 // "Set the selection's value to value." 3904 setSelectionValue("hilitecolor", value); 3905 }, indeterm: function() { 3906 // "True if among editable Text nodes that are effectively contained in 3907 // the active range, there are two that have distinct effective command 3908 // values. Otherwise false." 3909 return $_( getAllEffectivelyContainedNodes(getActiveRange(), function(node) { 3910 return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE; 3911 }) ).map(function(node) { 3912 return getEffectiveCommandValue(node, "hilitecolor"); 3913 }, true).filter(function(value, i, arr) { 3914 return $_(arr.slice(0, i)).indexOf(value) == -1; 3915 }).length >= 2; 3916 }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", 3917 equivalentValues: function(val1, val2) { 3918 // "Either both strings are valid CSS colors and have the same red, 3919 // green, blue, and alpha components, or neither string is a valid CSS 3920 // color." 3921 return normalizeColor(val1) === normalizeColor(val2); 3922 } 3923 }; 3924 3925 //@} 3926 ///// The italic command ///// 3927 //@{ 3928 commands.italic = { 3929 action: function( value, range ) { 3930 // "If queryCommandState("italic") returns true, set the selection's 3931 // value to "normal". Otherwise set the selection's value to "italic"." 3932 if (myQueryCommandState("italic", range)) { 3933 setSelectionValue("italic", "normal", range); 3934 } else { 3935 setSelectionValue("italic", "italic", range); 3936 } 3937 }, inlineCommandActivatedValues: ["italic", "oblique"], 3938 relevantCssProperty: "fontStyle" 3939 }; 3940 3941 //@} 3942 ///// The removeFormat command ///// 3943 //@{ 3944 commands.removeformat = { 3945 action: function() { 3946 // "A removeFormat candidate is an editable HTML element with local 3947 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", 3948 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", 3949 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup", 3950 // "tt", "u", or "var"." 3951 function isRemoveFormatCandidate(node) { 3952 return isEditable(node) 3953 && isHtmlElementInArray(node, ["abbr", "acronym", "b", "bdi", "bdo", 3954 "big", "blink", "cite", "code", "dfn", "em", "font", "i", 3955 "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", 3956 "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]); 3957 } 3958 3959 // "Let elements to remove be a list of every removeFormat candidate 3960 // effectively contained in the active range." 3961 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate); 3962 3963 // "For each element in elements to remove:" 3964 $_( elementsToRemove ).forEach(function(element) { 3965 // "While element has children, insert the first child of element 3966 // into the parent of element immediately before element, 3967 // preserving ranges." 3968 while (element.hasChildNodes()) { 3969 movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element), range); 3970 } 3971 3972 // "Remove element from its parent." 3973 element.parentNode.removeChild(element); 3974 }); 3975 3976 // "If the active range's start node is an editable Text node, and its 3977 // start offset is neither zero nor its start node's length, call 3978 // splitText() on the active range's start node, with argument equal to 3979 // the active range's start offset. Then set the active range's start 3980 // node to the result, and its start offset to zero." 3981 if (isEditable(getActiveRange().startContainer) 3982 && getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE 3983 && getActiveRange().startOffset != 0 3984 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { 3985 // Account for browsers not following range mutation rules 3986 if (getActiveRange().startContainer == getActiveRange().endContainer) { 3987 var newEnd = getActiveRange().endOffset - getActiveRange().startOffset; 3988 var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); 3989 getActiveRange().setStart(newNode, 0); 3990 getActiveRange().setEnd(newNode, newEnd); 3991 } else { 3992 getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0); 3993 } 3994 } 3995 3996 // "If the active range's end node is an editable Text node, and its 3997 // end offset is neither zero nor its end node's length, call 3998 // splitText() on the active range's end node, with argument equal to 3999 // the active range's end offset." 4000 if (isEditable(getActiveRange().endContainer) 4001 && getActiveRange().endContainer.nodeType == $_.Node.TEXT_NODE 4002 && getActiveRange().endOffset != 0 4003 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { 4004 // IE seems to mutate the range incorrectly here, so we need 4005 // correction here as well. Have to be careful to set the range to 4006 // something not including the text node so that getActiveRange() 4007 // doesn't throw an exception due to a temporarily detached 4008 // endpoint. 4009 var newStart = [getActiveRange().startContainer, getActiveRange().startOffset]; 4010 var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset]; 4011 getActiveRange().setEnd(document.documentElement, 0); 4012 newEnd[0].splitText(newEnd[1]); 4013 getActiveRange().setStart(newStart[0], newStart[1]); 4014 getActiveRange().setEnd(newEnd[0], newEnd[1]); 4015 } 4016 4017 // "Let node list consist of all editable nodes effectively contained 4018 // in the active range." 4019 // 4020 // "For each node in node list, while node's parent is a removeFormat 4021 // candidate in the same editing host as node, split the parent of the 4022 // one-node list consisting of node." 4023 $_( getAllEffectivelyContainedNodes(getActiveRange(), isEditable) ).forEach(function(node) { 4024 while (isRemoveFormatCandidate(node.parentNode) 4025 && inSameEditingHost(node.parentNode, node)) { 4026 splitParent([node], range); 4027 } 4028 }); 4029 4030 // "For each of the entries in the following list, in the given order, 4031 // set the selection's value to null, with command as given." 4032 $_( [ 4033 "subscript", 4034 "bold", 4035 "fontname", 4036 "fontsize", 4037 "forecolor", 4038 "hilitecolor", 4039 "italic", 4040 "strikethrough", 4041 "underline" 4042 ] ).forEach(function(command) { 4043 setSelectionValue(command, null); 4044 }); 4045 } 4046 }; 4047 4048 //@} 4049 ///// The strikethrough command ///// 4050 //@{ 4051 commands.strikethrough = { 4052 action: function() { 4053 // "If queryCommandState("strikethrough") returns true, set the 4054 // selection's value to null. Otherwise set the selection's value to 4055 // "line-through"." 4056 if (myQueryCommandState("strikethrough")) { 4057 setSelectionValue("strikethrough", null); 4058 } else { 4059 setSelectionValue("strikethrough", "line-through"); 4060 } 4061 }, inlineCommandActivatedValues: ["line-through"] 4062 }; 4063 4064 //@} 4065 ///// The subscript command ///// 4066 //@{ 4067 commands.subscript = { 4068 action: function() { 4069 // "Call queryCommandState("subscript"), and let state be the result." 4070 var state = myQueryCommandState("subscript"); 4071 4072 // "Set the selection's value to null." 4073 setSelectionValue("subscript", null); 4074 4075 // "If state is false, set the selection's value to "subscript"." 4076 if (!state) { 4077 setSelectionValue("subscript", "subscript"); 4078 } 4079 }, indeterm: function() { 4080 // "True if either among editable Text nodes that are effectively 4081 // contained in the active range, there is at least one with effective 4082 // command value "subscript" and at least one with some other effective 4083 // command value; or if there is some editable Text node effectively 4084 // contained in the active range with effective command value "mixed". 4085 // Otherwise false." 4086 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), function(node) { 4087 return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE; 4088 }); 4089 return ($_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" }) 4090 && $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" })) 4091 || $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" }); 4092 }, inlineCommandActivatedValues: ["subscript"] 4093 }; 4094 4095 //@} 4096 ///// The superscript command ///// 4097 //@{ 4098 commands.superscript = { 4099 action: function() { 4100 // "Call queryCommandState("superscript"), and let state be the 4101 // result." 4102 var state = myQueryCommandState("superscript"); 4103 4104 // "Set the selection's value to null." 4105 setSelectionValue("superscript", null); 4106 4107 // "If state is false, set the selection's value to "superscript"." 4108 if (!state) { 4109 setSelectionValue("superscript", "superscript"); 4110 } 4111 }, indeterm: function() { 4112 // "True if either among editable Text nodes that are effectively 4113 // contained in the active range, there is at least one with effective 4114 // command value "superscript" and at least one with some other 4115 // effective command value; or if there is some editable Text node 4116 // effectively contained in the active range with effective command 4117 // value "mixed". Otherwise false." 4118 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), 4119 function(node) { 4120 return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE; 4121 }); 4122 return ($_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" }) 4123 && $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" })) 4124 || $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" }); 4125 }, inlineCommandActivatedValues: ["superscript"] 4126 }; 4127 4128 //@} 4129 ///// The underline command ///// 4130 //@{ 4131 commands.underline = { 4132 action: function() { 4133 // "If queryCommandState("underline") returns true, set the selection's 4134 // value to null. Otherwise set the selection's value to "underline"." 4135 if (myQueryCommandState("underline")) { 4136 setSelectionValue("underline", null); 4137 4138 } else { 4139 setSelectionValue("underline", "underline"); 4140 } 4141 }, inlineCommandActivatedValues: ["underline"] 4142 }; 4143 4144 //@} 4145 ///// The unlink command ///// 4146 //@{ 4147 commands.unlink = { 4148 action: function() { 4149 // "Let hyperlinks be a list of every a element that has an href 4150 // attribute and is contained in the active range or is an ancestor of 4151 // one of its boundary points." 4152 // 4153 // As usual, take care to ensure it's tree order. The correctness of 4154 // the following is left as an exercise for the reader. 4155 var range = getActiveRange(); 4156 var hyperlinks = []; 4157 for ( 4158 var node = range.startContainer; 4159 node; 4160 node = node.parentNode 4161 ) { 4162 if (isNamedHtmlElement(node, 'A') 4163 && hasAttribute(node, "href")) { 4164 hyperlinks.unshift(node); 4165 } 4166 } 4167 for ( 4168 var node = range.startContainer; 4169 node != nextNodeDescendants(range.endContainer); 4170 node = nextNode(node) 4171 ) { 4172 if (isNamedHtmlElement(node, 'A') 4173 && hasAttribute(node, "href") 4174 && (isContained(node, range) 4175 || isAncestor(node, range.endContainer) 4176 || node == range.endContainer)) { 4177 hyperlinks.push(node); 4178 } 4179 } 4180 4181 // "Clear the value of each member of hyperlinks." 4182 for (var i = 0; i < hyperlinks.length; i++) { 4183 clearValue(hyperlinks[i], "unlink", range); 4184 } 4185 }, standardInlineValueCommand: true 4186 }; 4187 4188 //@} 4189 4190 ///////////////////////////////////// 4191 ///// Block formatting commands ///// 4192 ///////////////////////////////////// 4193 4194 ///// Block formatting command definitions ///// 4195 //@{ 4196 4197 // "An indentation element is either a blockquote, or a div that has a style 4198 // attribute that sets "margin" or some subproperty of it." 4199 function isIndentationElement(node) { 4200 if (!isAnyHtmlElement(node)) { 4201 return false; 4202 } 4203 4204 if (node.tagName == "BLOCKQUOTE") { 4205 return true; 4206 } 4207 4208 if (node.tagName != "DIV") { 4209 return false; 4210 } 4211 4212 if (typeof node.style.length !== 'undefined') { 4213 for (var i = 0; i < node.style.length; i++) { 4214 // Approximate check 4215 if (/^(-[a-z]+-)?margin/.test(node.style[i])) { 4216 return true; 4217 } 4218 } 4219 } else { 4220 for (var s in node.style) { 4221 if (/^(-[a-z]+-)?margin/.test(s) && node.style[s] && node.style[s] !== 0) { 4222 return true; 4223 } 4224 } 4225 } 4226 4227 return false; 4228 } 4229 4230 // "A simple indentation element is an indentation element that has no 4231 // attributes other than one or more of 4232 // 4233 // * "a style attribute that sets no properties other than "margin", "border", 4234 // "padding", or subproperties of those; 4235 // * "a class attribute; 4236 // * "a dir attribute." 4237 function isSimpleIndentationElement(node) { 4238 if (!isIndentationElement(node)) { 4239 return false; 4240 } 4241 4242 if (node.tagName != "BLOCKQUOTE" && node.tagName != "DIV") { 4243 return false; 4244 } 4245 4246 for (var i = 0; i < node.attributes.length; i++) { 4247 if (!isHtmlNamespace(node.attributes[i].namespaceURI) 4248 || jQuery.inArray(node.attributes[i].name, ["style", "class", "dir"]) == -1) { 4249 return false; 4250 } 4251 } 4252 4253 if (typeof node.style.length !== 'undefined') { 4254 for (var i = 0; i < node.style.length; i++) { 4255 // This is approximate, but it works well enough for my purposes. 4256 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) { 4257 return false; 4258 } 4259 } 4260 } else { 4261 for (var s in node.style) { 4262 // This is approximate, but it works well enough for my purposes. 4263 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(s) && node.style[s] && node.style[s] !== 0 && node.style[s] !== 'false') { 4264 return false; 4265 } 4266 } 4267 } 4268 4269 return true; 4270 } 4271 4272 // "A non-list single-line container is an HTML element with local name 4273 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", 4274 // or "xmp"." 4275 function isNonListSingleLineContainer(node) { 4276 return isHtmlElementInArray(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", 4277 "h6", "listing", "p", "pre", "xmp"]); 4278 } 4279 4280 // "A single-line container is either a non-list single-line container, or an 4281 // HTML element with local name "li", "dt", or "dd"." 4282 function isSingleLineContainer(node) { 4283 return isNonListSingleLineContainer(node) 4284 || isHtmlElementInArray(node, ["li", "dt", "dd"]); 4285 } 4286 4287 // "The default single-line container name is "p"." 4288 var defaultSingleLineContainerName = "p"; 4289 4290 4291 //@} 4292 ///// Assorted block formatting command algorithms ///// 4293 //@{ 4294 4295 function fixDisallowedAncestors(node, range) { 4296 // "If node is not editable, abort these steps." 4297 if (!isEditable(node)) { 4298 return; 4299 } 4300 4301 // "If node is not an allowed child of any of its ancestors in the same 4302 // editing host, and is not an HTML element with local name equal to the 4303 // default single-line container name:" 4304 if ($_(getAncestors(node)).every(function(ancestor) { 4305 return !inSameEditingHost(node, ancestor) 4306 || !isAllowedChild(node, ancestor) 4307 }) 4308 && !isHtmlElement_obsolete(node, defaultSingleLineContainerName)) { 4309 // "If node is a dd or dt, wrap the one-node list consisting of node, 4310 // with sibling criteria returning true for any dl with no attributes 4311 // and false otherwise, and new parent instructions returning the 4312 // result of calling createElement("dl") on the context object. Then 4313 // abort these steps." 4314 if (isHtmlElementInArray(node, ["dd", "dt"])) { 4315 wrap([node], 4316 function(sibling) { return isNamedHtmlElement(sibling, 'dl') && !sibling.attributes.length }, 4317 function() { return document.createElement("dl") }, 4318 range 4319 ); 4320 return; 4321 } 4322 4323 // "If node is not a prohibited paragraph child, abort these steps." 4324 if (!isProhibitedParagraphChild(node)) { 4325 return; 4326 } 4327 4328 // "Set the tag name of node to the default single-line container name, 4329 // and let node be the result." 4330 node = setTagName(node, defaultSingleLineContainerName, range); 4331 4332 ensureContainerEditable(node); 4333 4334 // "Fix disallowed ancestors of node." 4335 fixDisallowedAncestors(node, range); 4336 4337 // "Let descendants be all descendants of node." 4338 var descendants = getDescendants(node); 4339 4340 // "Fix disallowed ancestors of each member of descendants." 4341 for (var i = 0; i < descendants.length; i++) { 4342 fixDisallowedAncestors(descendants[i], range); 4343 } 4344 4345 // "Abort these steps." 4346 return; 4347 } 4348 4349 // "Record the values of the one-node list consisting of node, and let 4350 // values be the result." 4351 var values = recordValues([node]); 4352 4353 // "While node is not an allowed child of its parent, split the parent of 4354 // the one-node list consisting of node." 4355 while (!isAllowedChild(node, node.parentNode)) { 4356 // If the parent contains only this node and possibly empty text nodes, we rather want to unwrap the node, instead of splitting. 4357 // With splitting, we would get empty nodes, like: 4358 // split: <p><p>foo</p></p> -> <p></p><p>foo</p> (bad) 4359 // unwrap: <p><p>foo</p></p> -> <p>foo</p> (good) 4360 4361 // First remove empty text nodes that are children of the parent and correct the range if necessary 4362 // we do this to have the node being the only child of its parent, so that we can replace the parent with the node 4363 for (var i = node.parentNode.childNodes.length - 1; i >= 0; --i) { 4364 if (node.parentNode.childNodes[i].nodeType == 3 && node.parentNode.childNodes[i].data.length == 0) { 4365 // we remove the empty text node 4366 node.parentNode.removeChild(node.parentNode.childNodes[i]); 4367 4368 // if the range points to somewhere behind the removed text node, we reduce the offset 4369 if (range.startContainer == node.parentNode && range.startOffset > i) { 4370 range.startOffset--; 4371 } 4372 if (range.endContainer == node.parentNode && range.endOffset > i) { 4373 range.endOffset--; 4374 } 4375 } 4376 } 4377 4378 // now that the parent has only the node as child (because we 4379 // removed any existing empty text nodes), we can safely unwrap the 4380 // node's contents, and correct the range if necessary 4381 if (node.parentNode.childNodes.length == 1) { 4382 var newStartOffset = range.startOffset; 4383 var newEndOffset = range.endOffset; 4384 4385 if (range.startContainer === node.parentNode && range.startOffset > getNodeIndex(node)) { 4386 // the node (1 element) will be replaced by its contents (contents().length elements) 4387 newStartOffset = range.startOffset + (jQuery(node).contents().length - 1); 4388 } 4389 if (range.endContainer === node.parentNode && range.endOffset > getNodeIndex(node)) { 4390 // the node (1 element) will be replaced by its contents (contents().length elements) 4391 newEndOffset = range.endOffset + (jQuery(node).contents().length - 1); 4392 } 4393 jQuery(node).contents().unwrap(); 4394 range.startOffset = newStartOffset; 4395 range.endOffset = newEndOffset; 4396 // after unwrapping, we are done 4397 break; 4398 } else { 4399 // store the original parent 4400 var originalParent = node.parentNode; 4401 splitParent([node], range); 4402 // check whether the parent did not change, so the split did not work, e.g. 4403 // because we already reached the editing host itself. 4404 // this situation can occur, e.g. when we insert a paragraph into an contenteditable span 4405 // in such cases, we just unwrap the contents of the paragraph 4406 if (originalParent === node.parentNode) { 4407 // so we unwrap now 4408 var newStartOffset = range.startOffset; 4409 var newEndOffset = range.endOffset; 4410 4411 if (range.startContainer === node.parentNode && range.startOffset > getNodeIndex(node)) { 4412 // the node (1 element) will be replaced by its contents (contents().length elements) 4413 newStartOffset = range.startOffset + (jQuery(node).contents().length - 1); 4414 } 4415 if (range.endContainer === node.parentNode && range.endOffset > getNodeIndex(node)) { 4416 // the node (1 element) will be replaced by its contents (contents().length elements) 4417 newEndOffset = range.endOffset + (jQuery(node).contents().length - 1); 4418 } 4419 jQuery(node).contents().unwrap(); 4420 range.startOffset = newStartOffset; 4421 range.endOffset = newEndOffset; 4422 // after unwrapping, we are done 4423 break; 4424 } 4425 } 4426 } 4427 4428 // "Restore the values from values." 4429 restoreValues(values, range); 4430 } 4431 4432 /** 4433 * This method "normalizes" sublists of the given item (which is supposed to be a LI): 4434 * If sublists are found in the LI element, they are moved directly into the outer list. 4435 * @param item item 4436 * @param range range, which will be modified if necessary 4437 */ 4438 function normalizeSublists(item, range) { 4439 // "If item is not an li or it is not editable or its parent is not 4440 // editable, abort these steps." 4441 4442 if (!isNamedHtmlElement(item, 'LI') 4443 || !isEditable(item) 4444 || !isEditable(item.parentNode)) { 4445 return; 4446 } 4447 4448 // "Let new item be null." 4449 var newItem = null; 4450 4451 // "While item has an ol or ul child:" 4452 while ($_(item.childNodes).some( function (node) { return isHtmlElementInArray(node, ["OL", "UL"]) })) { 4453 // "Let child be the last child of item." 4454 var child = item.lastChild; 4455 4456 // "If child is an ol or ul, or new item is null and child is a Text 4457 // node whose data consists of zero of more space characters:" 4458 if (isHtmlElementInArray(child, ["OL", "UL"]) 4459 || (!newItem && child.nodeType == $_.Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) { 4460 // "Set new item to null." 4461 newItem = null; 4462 4463 // "Insert child into the parent of item immediately following 4464 // item, preserving ranges." 4465 movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item), range); 4466 4467 // "Otherwise:" 4468 } else { 4469 // "If new item is null, let new item be the result of calling 4470 // createElement("li") on the ownerDocument of item, then insert 4471 // new item into the parent of item immediately after item." 4472 if (!newItem) { 4473 newItem = item.ownerDocument.createElement("li"); 4474 item.parentNode.insertBefore(newItem, item.nextSibling); 4475 } 4476 4477 // "Insert child into new item as its first child, preserving 4478 // ranges." 4479 movePreservingRanges(child, newItem, 0, range); 4480 } 4481 } 4482 } 4483 4484 /** 4485 * This method is the exact opposite of normalizeSublists. 4486 * List nodes directly nested into each other are corrected to be nested in li elements (so that the resulting lists conform the html5 specification) 4487 * @param item list node 4488 * @param range range, which is preserved when modifying the list 4489 */ 4490 function unNormalizeSublists(item, range) { 4491 // "If item is not an ol or ol or it is not editable or its parent is not 4492 // editable, abort these steps." 4493 if (!isHtmlElementInArray(item, ["OL", "UL"]) 4494 || !isEditable(item)) { 4495 return; 4496 } 4497 4498 var $list = jQuery(item); 4499 $list.children("ol,ul").each(function(index, sublist) { 4500 if (isNamedHtmlElement(sublist.previousSibling, "LI")) { 4501 // move the sublist into the LI 4502 movePreservingRanges(sublist, sublist.previousSibling, sublist.previousSibling.childNodes.length, range); 4503 } 4504 }); 4505 } 4506 4507 function getSelectionListState() { 4508 // "Block-extend the active range, and let new range be the result." 4509 var newRange = blockExtend(getActiveRange()); 4510 4511 // "Let node list be a list of nodes, initially empty." 4512 // 4513 // "For each node contained in new range, append node to node list if the 4514 // last member of node list (if any) is not an ancestor of node; node is 4515 // editable; node is not an indentation element; and node is either an ol 4516 // or ul, or the child of an ol or ul, or an allowed child of "li"." 4517 var nodeList = getContainedNodes(newRange, function(node) { 4518 return isEditable(node) 4519 && !isIndentationElement(node) 4520 && (isHtmlElementInArray(node, ["ol", "ul"]) 4521 || isHtmlElementInArray(node.parentNode, ["ol", "ul"]) 4522 || isAllowedChild(node, "li")); 4523 }); 4524 4525 // "If node list is empty, return "none"." 4526 if (!nodeList.length) { 4527 return "none"; 4528 } 4529 4530 // "If every member of node list is either an ol or the child of an ol or 4531 // the child of an li child of an ol, and none is a ul or an ancestor of a 4532 // ul, return "ol"." 4533 if ($_(nodeList).every(function(node) { 4534 return isNamedHtmlElement(node, 'ol') 4535 || isNamedHtmlElement(node.parentNode, "ol") 4536 || (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ol")); 4537 }) 4538 && !$_( nodeList ).some(function(node) { return isNamedHtmlElement(node, 'ul') || ("querySelector" in node && node.querySelector("ul")) })) { 4539 return "ol"; 4540 } 4541 4542 // "If every member of node list is either a ul or the child of a ul or the 4543 4544 // child of an li child of a ul, and none is an ol or an ancestor of an ol, 4545 // return "ul"." 4546 if ($_(nodeList).every(function(node) { 4547 return isNamedHtmlElement(node, 'ul') 4548 || isNamedHtmlElement(node.parentNode, "ul") 4549 || (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ul")); 4550 }) 4551 && !$_( nodeList ).some(function(node) { return isNamedHtmlElement(node, 'ol') || ("querySelector" in node && node.querySelector("ol")) })) { 4552 return "ul"; 4553 } 4554 4555 var hasOl = $_( nodeList ).some(function(node) { 4556 return isNamedHtmlElement(node, 'ol') 4557 || isNamedHtmlElement(node.parentNode, "ol") 4558 || ("querySelector" in node && node.querySelector("ol")) 4559 || (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ol")); 4560 }); 4561 var hasUl = $_( nodeList ).some(function(node) { 4562 return isNamedHtmlElement(node, 'ul') 4563 || isNamedHtmlElement(node.parentNode, "ul") 4564 || ("querySelector" in node && node.querySelector("ul")) 4565 || (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ul")); 4566 }); 4567 // "If some member of node list is either an ol or the child or ancestor of 4568 // an ol or the child of an li child of an ol, and some member of node list 4569 // is either a ul or the child or ancestor of a ul or the child of an li 4570 // child of a ul, return "mixed"." 4571 if (hasOl && hasUl) { 4572 return "mixed"; 4573 } 4574 4575 // "If some member of node list is either an ol or the child or ancestor of 4576 // an ol or the child of an li child of an ol, return "mixed ol"." 4577 if (hasOl) { 4578 return "mixed ol"; 4579 } 4580 4581 // "If some member of node list is either a ul or the child or ancestor of 4582 // a ul or the child of an li child of a ul, return "mixed ul"." 4583 if (hasUl) { 4584 return "mixed ul"; 4585 } 4586 4587 // "Return "none"." 4588 return "none"; 4589 } 4590 4591 function getAlignmentValue(node) { 4592 // "While node is neither null nor an Element, or it is an Element but its 4593 // "display" property has resolved value "inline" or "none", set node to 4594 // its parent." 4595 while ((node && node.nodeType != $_.Node.ELEMENT_NODE) 4596 || (node.nodeType == $_.Node.ELEMENT_NODE 4597 && jQuery.inArray($_.getComputedStyle(node).display, ["inline", "none"]) != -1)) { 4598 node = node.parentNode; 4599 } 4600 4601 // "If node is not an Element, return "left"." 4602 if (!node || node.nodeType != $_.Node.ELEMENT_NODE) { 4603 return "left"; 4604 } 4605 4606 var resolvedValue = $_.getComputedStyle(node).textAlign 4607 // Hack around browser non-standardness 4608 .replace(/^-(moz|webkit)-/, "") 4609 .replace(/^auto$/, "start"); 4610 4611 // "If node's "text-align" property has resolved value "start", return 4612 // "left" if the directionality of node is "ltr", "right" if it is "rtl"." 4613 if (resolvedValue == "start") { 4614 return getDirectionality(node) == "ltr" ? "left" : "right"; 4615 } 4616 4617 // "If node's "text-align" property has resolved value "end", return 4618 // "right" if the directionality of node is "ltr", "left" if it is "rtl"." 4619 if (resolvedValue == "end") { 4620 return getDirectionality(node) == "ltr" ? "right" : "left"; 4621 } 4622 4623 // "If node's "text-align" property has resolved value "center", "justify", 4624 // "left", or "right", return that value." 4625 if (jQuery.inArray(resolvedValue, ["center", "justify", "left", "right"]) != -1) { 4626 return resolvedValue; 4627 } 4628 4629 // "Return "left"." 4630 return "left"; 4631 } 4632 4633 //@} 4634 ///// Block-extending a range ///// 4635 //@{ 4636 4637 // "A boundary point (node, offset) is a block start point if either node's 4638 // parent is null and offset is zero; or node has a child with index offset − 4639 // 1, and that child is either a visible block node or a visible br." 4640 function isBlockStartPoint(node, offset) { 4641 return (!node.parentNode && offset == 0) 4642 || (0 <= offset - 1 4643 && offset - 1 < node.childNodes.length 4644 && isVisible(node.childNodes[offset - 1]) 4645 && (isBlockNode(node.childNodes[offset - 1]) 4646 || isNamedHtmlElement(node.childNodes[offset - 1], "br"))); 4647 } 4648 4649 // "A boundary point (node, offset) is a block end point if either node's 4650 // parent is null and offset is node's length; or node has a child with index 4651 // offset, and that child is a visible block node." 4652 function isBlockEndPoint(node, offset) { 4653 return (!node.parentNode && offset == getNodeLength(node)) 4654 || (offset < node.childNodes.length 4655 && isVisible(node.childNodes[offset]) 4656 && isBlockNode(node.childNodes[offset])); 4657 } 4658 4659 // "A boundary point is a block boundary point if it is either a block start 4660 // point or a block end point." 4661 function isBlockBoundaryPoint(node, offset) { 4662 return isBlockStartPoint(node, offset) 4663 || isBlockEndPoint(node, offset); 4664 } 4665 4666 function blockExtend(range) { 4667 // "Let start node, start offset, end node, and end offset be the start 4668 // and end nodes and offsets of the range." 4669 var startNode = range.startContainer; 4670 var startOffset = range.startOffset; 4671 var endNode = range.endContainer; 4672 var endOffset = range.endOffset; 4673 4674 // "If some ancestor container of start node is an li, set start offset to 4675 // the index of the last such li in tree order, and set start node to that 4676 // li's parent." 4677 var liAncestors = $_( getAncestors(startNode).concat(startNode) ) 4678 .filter(function(ancestor) { return isNamedHtmlElement(ancestor, 'li') }) 4679 .slice(-1); 4680 if (liAncestors.length) { 4681 startOffset = getNodeIndex(liAncestors[0]); 4682 startNode = liAncestors[0].parentNode; 4683 } 4684 4685 // "If (start node, start offset) is not a block start point, repeat the 4686 // following steps:" 4687 if (!isBlockStartPoint(startNode, startOffset)) do { 4688 // "If start offset is zero, set it to start node's index, then set 4689 // start node to its parent." 4690 if (startOffset == 0) { 4691 startOffset = getNodeIndex(startNode); 4692 startNode = startNode.parentNode; 4693 4694 // "Otherwise, subtract one from start offset." 4695 } else { 4696 startOffset--; 4697 } 4698 4699 // "If (start node, start offset) is a block boundary point, break from 4700 // this loop." 4701 } while (!isBlockBoundaryPoint(startNode, startOffset)); 4702 4703 // "While start offset is zero and start node's parent is not null, set 4704 // start offset to start node's index, then set start node to its parent." 4705 while (startOffset == 0 4706 && startNode.parentNode) { 4707 startOffset = getNodeIndex(startNode); 4708 startNode = startNode.parentNode; 4709 } 4710 4711 // "If some ancestor container of end node is an li, set end offset to one 4712 // plus the index of the last such li in tree order, and set end node to 4713 // that li's parent." 4714 var liAncestors = $_( getAncestors(endNode).concat(endNode) ) 4715 .filter(function(ancestor) { return isNamedHtmlElement(ancestor, 'li') }) 4716 .slice(-1); 4717 if (liAncestors.length) { 4718 endOffset = 1 + getNodeIndex(liAncestors[0]); 4719 endNode = liAncestors[0].parentNode; 4720 } 4721 4722 // "If (end node, end offset) is not a block end point, repeat the 4723 // following steps:" 4724 if (!isBlockEndPoint(endNode, endOffset)) do { 4725 // "If end offset is end node's length, set it to one plus end node's 4726 // index, then set end node to its parent." 4727 if (endOffset == getNodeLength(endNode)) { 4728 endOffset = 1 + getNodeIndex(endNode); 4729 endNode = endNode.parentNode; 4730 4731 // "Otherwise, add one to end offset. 4732 } else { 4733 endOffset++; 4734 } 4735 4736 // "If (end node, end offset) is a block boundary point, break from 4737 // this loop." 4738 } while (!isBlockBoundaryPoint(endNode, endOffset)); 4739 4740 // "While end offset is end node's length and end node's parent is not 4741 // null, set end offset to one plus end node's index, then set end node to 4742 // its parent." 4743 while (endOffset == getNodeLength(endNode) 4744 && endNode.parentNode) { 4745 endOffset = 1 + getNodeIndex(endNode); 4746 endNode = endNode.parentNode; 4747 } 4748 4749 // "Let new range be a new range whose start and end nodes and offsets 4750 // are start node, start offset, end node, and end offset." 4751 var newRange = Aloha.createRange(); 4752 newRange.setStart(startNode, startOffset); 4753 newRange.setEnd(endNode, endOffset); 4754 4755 // "Return new range." 4756 return newRange; 4757 } 4758 4759 function followsLineBreak(node) { 4760 // "Let offset be zero." 4761 var offset = 0; 4762 4763 // "While (node, offset) is not a block boundary point:" 4764 while (!isBlockBoundaryPoint(node, offset)) { 4765 // "If node has a visible child with index offset minus one, return 4766 // false." 4767 if (0 <= offset - 1 4768 && offset - 1 < node.childNodes.length 4769 && isVisible(node.childNodes[offset - 1])) { 4770 return false; 4771 } 4772 4773 // "If offset is zero or node has no children, set offset to node's 4774 // index, then set node to its parent." 4775 if (offset == 0 4776 || !node.hasChildNodes()) { 4777 offset = getNodeIndex(node); 4778 node = node.parentNode; 4779 4780 // "Otherwise, set node to its child with index offset minus one, then 4781 // set offset to node's length." 4782 } else { 4783 node = node.childNodes[offset - 1]; 4784 offset = getNodeLength(node); 4785 } 4786 } 4787 4788 // "Return true." 4789 return true; 4790 } 4791 4792 function precedesLineBreak(node) { 4793 // "Let offset be node's length." 4794 var offset = getNodeLength(node); 4795 4796 // "While (node, offset) is not a block boundary point:" 4797 while (!isBlockBoundaryPoint(node, offset)) { 4798 // "If node has a visible child with index offset, return false." 4799 if (offset < node.childNodes.length 4800 && isVisible(node.childNodes[offset])) { 4801 return false; 4802 } 4803 4804 // "If offset is node's length or node has no children, set offset to 4805 // one plus node's index, then set node to its parent." 4806 if (offset == getNodeLength(node) 4807 || !node.hasChildNodes()) { 4808 offset = 1 + getNodeIndex(node); 4809 node = node.parentNode; 4810 4811 // "Otherwise, set node to its child with index offset and set offset 4812 // to zero." 4813 } else { 4814 node = node.childNodes[offset]; 4815 offset = 0; 4816 } 4817 } 4818 4819 // "Return true." 4820 return true; 4821 } 4822 4823 //@} 4824 ///// Recording and restoring overrides ///// 4825 //@{ 4826 4827 function recordCurrentOverrides( range ) { 4828 // "Let overrides be a list of (string, string or boolean) ordered pairs, 4829 // initially empty." 4830 var overrides = []; 4831 4832 // "If there is a value override for "createLink", add ("createLink", value 4833 // override for "createLink") to overrides." 4834 if (getValueOverride("createlink" ,range) !== undefined) { 4835 overrides.push(["createlink", getValueOverride("createlink", range)]); 4836 } 4837 4838 // "For each command in the list "bold", "italic", "strikethrough", 4839 // "subscript", "superscript", "underline", in order: if there is a state 4840 // override for command, add (command, command's state override) to 4841 // overrides." 4842 $_( ["bold", "italic", "strikethrough", "subscript", "superscript", 4843 "underline"] ).forEach(function(command) { 4844 if (getStateOverride(command, range) !== undefined) { 4845 overrides.push([command, getStateOverride(command, range)]); 4846 } 4847 }); 4848 4849 // "For each command in the list "fontName", "fontSize", "foreColor", 4850 // "hiliteColor", in order: if there is a value override for command, add 4851 // (command, command's value override) to overrides." 4852 $_( ["fontname", "fontsize", "forecolor", 4853 "hilitecolor"] ).forEach(function(command) { 4854 if (getValueOverride(command, range) !== undefined) { 4855 overrides.push([command, getValueOverride(command, range)]); 4856 } 4857 }); 4858 4859 // "Return overrides." 4860 return overrides; 4861 } 4862 4863 function recordCurrentStatesAndValues(range) { 4864 // "Let overrides be a list of (string, string or boolean) ordered pairs, 4865 // initially empty." 4866 var overrides = []; 4867 4868 // "Let node be the first editable Text node effectively contained in the 4869 // active range, or null if there is none." 4870 var node = $_( getAllEffectivelyContainedNodes(range) ) 4871 .filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE })[0]; 4872 4873 // "If node is null, return overrides." 4874 if (!node) { 4875 return overrides; 4876 } 4877 4878 // "Add ("createLink", value for "createLink") to overrides." 4879 overrides.push(["createlink", commands.createlink.value(range)]); 4880 4881 // "For each command in the list "bold", "italic", "strikethrough", 4882 // "subscript", "superscript", "underline", in order: if node's effective 4883 // command value for command is one of its inline command activated values, 4884 // add (command, true) to overrides, and otherwise add (command, false) to 4885 // overrides." 4886 $_( ["bold", "italic", "strikethrough", "subscript", "superscript", 4887 "underline"] ).forEach(function(command) { 4888 if ($_(commands[command].inlineCommandActivatedValues) 4889 .indexOf(getEffectiveCommandValue(node, command)) != -1) { 4890 overrides.push([command, true]); 4891 } else { 4892 overrides.push([command, false]); 4893 } 4894 }); 4895 4896 // "For each command in the list "fontName", "foreColor", "hiliteColor", in 4897 // order: add (command, command's value) to overrides." 4898 4899 $_( ["fontname", "fontsize", "forecolor", "hilitecolor"] ).forEach(function(command) { 4900 overrides.push([command, commands[command].value(range)]); 4901 }); 4902 4903 // "Add ("fontSize", node's effective command value for "fontSize") to 4904 // overrides." 4905 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]); 4906 4907 // "Return overrides." 4908 return overrides; 4909 } 4910 4911 function restoreStatesAndValues(overrides, range) { 4912 // "Let node be the first editable Text node effectively contained in the 4913 // active range, or null if there is none." 4914 var node = $_( getAllEffectivelyContainedNodes(range) ) 4915 .filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE })[0]; 4916 4917 // "If node is not null, then for each (command, override) pair in 4918 // overrides, in order:" 4919 if (node) { 4920 for (var i = 0; i < overrides.length; i++) { 4921 4922 var command = overrides[i][0]; 4923 var override = overrides[i][1]; 4924 4925 // "If override is a boolean, and queryCommandState(command) 4926 // returns something different from override, call 4927 // execCommand(command)." 4928 if (typeof override == "boolean" 4929 && myQueryCommandState(command, range) != override) { 4930 myExecCommand(command); 4931 4932 // "Otherwise, if override is a string, and command is not 4933 // "fontSize", and queryCommandValue(command) returns something not 4934 // equivalent to override, call execCommand(command, false, 4935 // override)." 4936 } else if (typeof override == "string" 4937 && command != "fontsize" 4938 && !areEquivalentValues(command, myQueryCommandValue(command, range), override)) { 4939 myExecCommand(command, false, override, range); 4940 4941 // "Otherwise, if override is a string; and command is "fontSize"; 4942 // and either there is a value override for "fontSize" that is not 4943 // equal to override, or there is no value override for "fontSize" 4944 // and node's effective command value for "fontSize" is not loosely 4945 // equivalent to override: call execCommand("fontSize", false, 4946 // override)." 4947 } else if (typeof override == "string" 4948 && command == "fontsize" 4949 && ( 4950 ( 4951 getValueOverride("fontsize", range) !== undefined 4952 && getValueOverride("fontsize", range) !== override 4953 ) || ( 4954 getValueOverride("fontsize", range) === undefined 4955 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override) 4956 ) 4957 )) { 4958 myExecCommand("fontsize", false, override, range); 4959 4960 // "Otherwise, continue this loop from the beginning." 4961 } else { 4962 continue; 4963 } 4964 4965 // "Set node to the first editable Text node effectively contained 4966 // in the active range, if there is one." 4967 node = $_( getAllEffectivelyContainedNodes(range) ) 4968 .filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE })[0] 4969 || node; 4970 } 4971 4972 // "Otherwise, for each (command, override) pair in overrides, in order:" 4973 } else { 4974 for (var i = 0; i < overrides.length; i++) { 4975 var command = overrides[i][0]; 4976 var override = overrides[i][1]; 4977 4978 // "If override is a boolean, set the state override for command to 4979 // override." 4980 if (typeof override == "boolean") { 4981 setStateOverride(command, override, range); 4982 } 4983 4984 // "If override is a string, set the value override for command to 4985 // override." 4986 if (typeof override == "string") { 4987 setValueOverride(command, override, range); 4988 } 4989 } 4990 } 4991 } 4992 4993 //@} 4994 ///// Deleting the contents of a range ///// 4995 //@{ 4996 4997 function deleteContents() { 4998 // We accept several different calling conventions: 4999 // 5000 // 1) A single argument, which is a range. 5001 // 5002 // 2) Two arguments, the first being a range and the second flags. 5003 // 5004 // 3) Four arguments, the start and end of a range. 5005 // 5006 // 4) Five arguments, the start and end of a range plus flags. 5007 // 5008 // The flags argument is a dictionary that can have up to two keys, 5009 // blockMerging and stripWrappers, whose corresponding values are 5010 // interpreted as boolean. E.g., {stripWrappers: false}. 5011 var range; 5012 var flags = {}; 5013 5014 if (arguments.length < 3) { 5015 range = arguments[0]; 5016 } else { 5017 range = Aloha.createRange(); 5018 range.setStart(arguments[0], arguments[1]); 5019 range.setEnd(arguments[2], arguments[3]); 5020 } 5021 if (arguments.length == 2) { 5022 flags = arguments[1]; 5023 } 5024 if (arguments.length == 5) { 5025 flags = arguments[4]; 5026 } 5027 5028 var blockMerging = "blockMerging" in flags ? !!flags.blockMerging : true; 5029 var stripWrappers = "stripWrappers" in flags ? !!flags.stripWrappers : true; 5030 5031 // "If range is null, abort these steps and do nothing." 5032 if (!range) { 5033 return; 5034 } 5035 5036 // "Let start node, start offset, end node, and end offset be range's start 5037 // and end nodes and offsets." 5038 var startNode = range.startContainer; 5039 var startOffset = range.startOffset; 5040 var endNode = range.endContainer; 5041 var endOffset = range.endOffset; 5042 5043 // "While start node has at least one child:" 5044 while (startNode.hasChildNodes()) { 5045 // "If start offset is start node's length, and start node's parent is 5046 // in the same editing host, and start node is an inline node, set 5047 // start offset to one plus the index of start node, then set start 5048 // node to its parent and continue this loop from the beginning." 5049 if (startOffset == getNodeLength(startNode) 5050 && inSameEditingHost(startNode, startNode.parentNode) 5051 && isInlineNode(startNode)) { 5052 startOffset = 1 + getNodeIndex(startNode); 5053 startNode = startNode.parentNode; 5054 continue; 5055 } 5056 5057 // "If start offset is start node's length, break from this loop." 5058 if (startOffset == getNodeLength(startNode)) { 5059 break; 5060 } 5061 5062 // "Let reference node be the child of start node with index equal to 5063 // start offset." 5064 var referenceNode = startNode.childNodes[startOffset]; 5065 5066 // "If reference node is a block node or an Element with no children, 5067 // or is neither an Element nor a Text node, break from this loop." 5068 if (isBlockNode(referenceNode) 5069 || (referenceNode.nodeType == $_.Node.ELEMENT_NODE 5070 && !referenceNode.hasChildNodes()) 5071 || (referenceNode.nodeType != $_.Node.ELEMENT_NODE 5072 && referenceNode.nodeType != $_.Node.TEXT_NODE)) { 5073 break; 5074 } 5075 5076 // "Set start node to reference node and start offset to 0." 5077 startNode = referenceNode; 5078 startOffset = 0; 5079 } 5080 5081 // "While end node has at least one child:" 5082 while (endNode.hasChildNodes()) { 5083 // "If end offset is 0, and end node's parent is in the same editing 5084 // host, and end node is an inline node, set end offset to the index of 5085 // end node, then set end node to its parent and continue this loop 5086 // from the beginning." 5087 if (endOffset == 0 5088 && inSameEditingHost(endNode, endNode.parentNode) 5089 && isInlineNode(endNode)) { 5090 endOffset = getNodeIndex(endNode); 5091 endNode = endNode.parentNode; 5092 continue; 5093 } 5094 5095 // "If end offset is 0, break from this loop." 5096 if (endOffset == 0) { 5097 break; 5098 } 5099 5100 // "Let reference node be the child of end node with index equal to end 5101 // offset minus one." 5102 var referenceNode = endNode.childNodes[endOffset - 1]; 5103 5104 // "If reference node is a block node or an Element with no children, 5105 // or is neither an Element nor a Text node, break from this loop." 5106 if (isBlockNode(referenceNode) 5107 || (referenceNode.nodeType == $_.Node.ELEMENT_NODE 5108 && !referenceNode.hasChildNodes()) 5109 || (referenceNode.nodeType != $_.Node.ELEMENT_NODE 5110 && referenceNode.nodeType != $_.Node.TEXT_NODE)) { 5111 break; 5112 } 5113 5114 // "Set end node to reference node and end offset to the length of 5115 // reference node." 5116 endNode = referenceNode; 5117 endOffset = getNodeLength(referenceNode); 5118 } 5119 5120 // "If (end node, end offset) is not after (start node, start offset), set 5121 // range's end to its start and abort these steps." 5122 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") { 5123 range.setEnd(range.startContainer, range.startOffset); 5124 return; 5125 } 5126 5127 // "If start node is a Text node and start offset is 0, set start offset to 5128 // the index of start node, then set start node to its parent." 5129 if (startNode.nodeType == $_.Node.TEXT_NODE 5130 && startOffset == 0 5131 && startNode != endNode) { 5132 // startOffset = getNodeIndex(startNode); 5133 // startNode = startNode.parentNode; 5134 } 5135 5136 // "If end node is a Text node and end offset is its length, set end offset 5137 // to one plus the index of end node, then set end node to its parent." 5138 if (endNode.nodeType == $_.Node.TEXT_NODE 5139 && endOffset == getNodeLength(endNode) 5140 && startNode != endNode) { 5141 endOffset = 1 + getNodeIndex(endNode); 5142 endNode = endNode.parentNode; 5143 } 5144 5145 // "Set range's start to (start node, start offset) and its end to (end 5146 // node, end offset)." 5147 range.setStart(startNode, startOffset); 5148 range.setEnd(endNode, endOffset); 5149 5150 // "Let start block be the start node of range." 5151 var startBlock = range.startContainer; 5152 5153 // "While start block's parent is in the same editing host and start block 5154 // is an inline node, set start block to its parent." 5155 while (inSameEditingHost(startBlock, startBlock.parentNode) 5156 && isInlineNode(startBlock)) { 5157 startBlock = startBlock.parentNode; 5158 } 5159 5160 // "If start block is neither a block node nor an editing host, or "span" 5161 // is not an allowed child of start block, or start block is a td or th, 5162 // set start block to null." 5163 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock)) 5164 || !isAllowedChild("span", startBlock) 5165 || isHtmlElementInArray(startBlock, ["td", "th"])) { 5166 startBlock = null; 5167 } 5168 5169 // "Let end block be the end node of range." 5170 var endBlock = range.endContainer; 5171 5172 // "While end block's parent is in the same editing host and end block is 5173 // an inline node, set end block to its parent." 5174 while (inSameEditingHost(endBlock, endBlock.parentNode) 5175 && isInlineNode(endBlock)) { 5176 endBlock = endBlock.parentNode; 5177 } 5178 5179 // "If end block is neither a block node nor an editing host, or "span" is 5180 // not an allowed child of end block, or end block is a td or th, set end 5181 // block to null." 5182 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock)) 5183 || !isAllowedChild("span", endBlock) 5184 || isHtmlElementInArray(endBlock, ["td", "th"])) { 5185 endBlock = null; 5186 } 5187 5188 // "Record current states and values, and let overrides be the result." 5189 var overrides = recordCurrentStatesAndValues(range); 5190 // "If start node and end node are the same, and start node is an editable 5191 // Text node:" 5192 if (startNode == endNode 5193 && isEditable(startNode) 5194 && startNode.nodeType == $_.Node.TEXT_NODE) { 5195 // "Let parent be the parent of node." 5196 var parent_ = startNode.parentNode; 5197 5198 // "Call deleteData(start offset, end offset − start offset) on start 5199 // node." 5200 startNode.deleteData(startOffset, endOffset - startOffset); 5201 5202 // if deleting the text moved two spaces together, we replace the left one by a , which makes the two spaces a visible 5203 // two space sequence 5204 if (startOffset > 0 && startNode.data.substr(startOffset - 1, 1) === ' ' 5205 && startOffset < startNode.data.length && startNode.data.substr(startOffset, 1) === ' ') { 5206 startNode.replaceData(startOffset - 1, 1, '\xa0'); 5207 } 5208 5209 // "Canonicalize whitespace at (start node, start offset)." 5210 canonicalizeWhitespace(startNode, startOffset); 5211 5212 // "Set range's end to its start." 5213 // Ok, also set the range's start to its start, because modifying the text 5214 // might have somehow corrupted the range 5215 range.setStart(range.startContainer, range.startOffset); 5216 range.setEnd(range.startContainer, range.startOffset); 5217 5218 // "Restore states and values from overrides." 5219 restoreStatesAndValues(overrides, range); 5220 5221 // "If parent is editable or an editing host, is not an inline node, 5222 // and has no children, call createElement("br") on the context object 5223 // and append the result as the last child of parent." 5224 // only do this, if the offsetHeight is 0 5225 if ((isEditable(parent_) || isEditingHost(parent_)) 5226 && !isInlineNode(parent_)) { 5227 // TODO is this always correct? 5228 ensureContainerEditable(parent_); 5229 } 5230 5231 // "Abort these steps." 5232 return; 5233 } 5234 5235 // "If start node is an editable Text node, call deleteData() on it, with 5236 // start offset as the first argument and (length of start node − start 5237 // offset) as the second argument." 5238 if (isEditable(startNode) 5239 && startNode.nodeType == $_.Node.TEXT_NODE) { 5240 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset); 5241 } 5242 5243 // "Let node list be a list of nodes, initially empty." 5244 // 5245 // "For each node contained in range, append node to node list if the last 5246 // member of node list (if any) is not an ancestor of node; node is 5247 // editable; and node is not a thead, tbody, tfoot, tr, th, or td." 5248 var nodeList = getContainedNodes(range, 5249 function(node) { 5250 return isEditable(node) 5251 && !isHtmlElementInArray(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]); 5252 } 5253 ); 5254 5255 // "For each node in node list:" 5256 for (var i = 0; i < nodeList.length; i++) { 5257 var node = nodeList[i]; 5258 5259 // "Let parent be the parent of node." 5260 var parent_ = node.parentNode; 5261 5262 // "Remove node from parent." 5263 parent_.removeChild(node); 5264 5265 // "If strip wrappers is true or parent is not an ancestor container of 5266 // start node, while parent is an editable inline node with length 0, 5267 // let grandparent be the parent of parent, then remove parent from 5268 // grandparent, then set parent to grandparent." 5269 if (stripWrappers 5270 || (!isAncestor(parent_, startNode) && parent_ != startNode)) { 5271 5272 while (isEditable(parent_) 5273 && isInlineNode(parent_) 5274 && getNodeLength(parent_) == 0) { 5275 var grandparent = parent_.parentNode; 5276 grandparent.removeChild(parent_); 5277 parent_ = grandparent; 5278 } 5279 } 5280 5281 // "If parent is editable or an editing host, is not an inline node, 5282 // and has no children, call createElement("br") on the context object 5283 // and append the result as the last child of parent." 5284 // only do this, if the offsetHeight is 0 5285 if ((isEditable(parent_) || isEditingHost(parent_)) 5286 && !isInlineNode(parent_)) { 5287 ensureContainerEditable(parent_); 5288 } 5289 } 5290 5291 // "If end node is an editable Text node, call deleteData(0, end offset) on 5292 // it." 5293 if (isEditable(endNode) 5294 && endNode.nodeType == $_.Node.TEXT_NODE) { 5295 endNode.deleteData(0, endOffset); 5296 } 5297 5298 // "Canonicalize whitespace at range's start." 5299 canonicalizeWhitespace(range.startContainer, range.startOffset); 5300 5301 // "Canonicalize whitespace at range's end." 5302 canonicalizeWhitespace(range.endContainer, range.endOffset); 5303 5304 // "If block merging is false, or start block or end block is null, or 5305 // start block is not in the same editing host as end block, or start block 5306 // and end block are the same:" 5307 if (!blockMerging 5308 || !startBlock 5309 || !endBlock 5310 || !inSameEditingHost(startBlock, endBlock) 5311 || startBlock == endBlock) { 5312 // "Set range's end to its start." 5313 range.setEnd(range.startContainer, range.startOffset); 5314 5315 // "Restore states and values from overrides." 5316 restoreStatesAndValues(overrides, range); 5317 5318 // "Abort these steps." 5319 return; 5320 } 5321 5322 // "If start block has one child, which is a collapsed block prop, remove 5323 // its child from it." 5324 if (startBlock.children.length == 1 5325 && isCollapsedBlockProp(startBlock.firstChild)) { 5326 startBlock.removeChild(startBlock.firstChild); 5327 } 5328 5329 // "If end block has one child, which is a collapsed block prop, remove its 5330 // child from it." 5331 if (endBlock.children.length == 1 5332 && isCollapsedBlockProp(endBlock.firstChild)) { 5333 endBlock.removeChild(endBlock.firstChild); 5334 } 5335 5336 5337 // "If start block is an ancestor of end block:" 5338 if (isAncestor(startBlock, endBlock)) { 5339 // "Let reference node be end block." 5340 var referenceNode = endBlock; 5341 5342 // "While reference node is not a child of start block, set reference 5343 // node to its parent." 5344 while (referenceNode.parentNode != startBlock) { 5345 referenceNode = referenceNode.parentNode; 5346 } 5347 5348 // "Set the start and end of range to (start block, index of reference 5349 // node)." 5350 range.setStart(startBlock, getNodeIndex(referenceNode)); 5351 range.setEnd(startBlock, getNodeIndex(referenceNode)); 5352 5353 // "If end block has no children:" 5354 if (!endBlock.hasChildNodes()) { 5355 // "While end block is editable and is the only child of its parent 5356 // and is not a child of start block, let parent equal end block, 5357 // then remove end block from parent, then set end block to 5358 // parent." 5359 while (isEditable(endBlock) 5360 && endBlock.parentNode.childNodes.length == 1 5361 && endBlock.parentNode != startBlock) { 5362 var parent_ = endBlock; 5363 parent_.removeChild(endBlock); 5364 endBlock = parent_; 5365 } 5366 5367 // "If end block is editable and is not an inline node, and its 5368 // previousSibling and nextSibling are both inline nodes, call 5369 // createElement("br") on the context object and insert it into end 5370 // block's parent immediately after end block." 5371 5372 if (isEditable(endBlock) 5373 && !isInlineNode(endBlock) 5374 && isInlineNode(endBlock.previousSibling) 5375 && isInlineNode(endBlock.nextSibling)) { 5376 endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling); 5377 } 5378 5379 // "If end block is editable, remove it from its parent." 5380 if (isEditable(endBlock)) { 5381 endBlock.parentNode.removeChild(endBlock); 5382 } 5383 5384 // "Restore states and values from overrides." 5385 restoreStatesAndValues(overrides, range); 5386 5387 // "Abort these steps." 5388 return; 5389 } 5390 5391 // "If end block's firstChild is not an inline node, restore states and 5392 // values from overrides, then abort these steps." 5393 if (!isInlineNode(endBlock.firstChild)) { 5394 restoreStatesAndValues(overrides, range); 5395 return; 5396 } 5397 5398 // "Let children be a list of nodes, initially empty." 5399 var children = []; 5400 5401 // "Append the first child of end block to children." 5402 children.push(endBlock.firstChild); 5403 5404 // "While children's last member is not a br, and children's last 5405 // member's nextSibling is an inline node, append children's last 5406 // member's nextSibling to children." 5407 while (!isNamedHtmlElement(children[children.length - 1], "br") 5408 && isInlineNode(children[children.length - 1].nextSibling)) { 5409 children.push(children[children.length - 1].nextSibling); 5410 } 5411 5412 // "Record the values of children, and let values be the result." 5413 var values = recordValues(children); 5414 5415 // "While children's first member's parent is not start block, split 5416 // the parent of children." 5417 while (children[0].parentNode != startBlock) { 5418 splitParent(children, range); 5419 } 5420 5421 // "If children's first member's previousSibling is an editable br, 5422 // remove that br from its parent." 5423 if (isEditable(children[0].previousSibling) 5424 && isNamedHtmlElement(children[0].previousSibling, "br")) { 5425 children[0].parentNode.removeChild(children[0].previousSibling); 5426 } 5427 5428 // "Otherwise, if start block is a descendant of end block:" 5429 } else if (isDescendant(startBlock, endBlock)) { 5430 // "Set the start and end of range to (start block, length of start 5431 // block)." 5432 range.setStart(startBlock, getNodeLength(startBlock)); 5433 range.setEnd(startBlock, getNodeLength(startBlock)); 5434 5435 // "Let reference node be start block." 5436 var referenceNode = startBlock; 5437 5438 // "While reference node is not a child of end block, set reference 5439 // node to its parent." 5440 while (referenceNode.parentNode != endBlock) { 5441 referenceNode = referenceNode.parentNode; 5442 } 5443 5444 // "If reference node's nextSibling is an inline node and start block's 5445 // lastChild is a br, remove start block's lastChild from it." 5446 if (isInlineNode(referenceNode.nextSibling) 5447 && isNamedHtmlElement(startBlock.lastChild, "br")) { 5448 startBlock.removeChild(startBlock.lastChild); 5449 } 5450 5451 // "Let nodes to move be a list of nodes, initially empty." 5452 var nodesToMove = []; 5453 5454 // "If reference node's nextSibling is neither null nor a br nor a 5455 // block node, append it to nodes to move." 5456 if (referenceNode.nextSibling 5457 && !isNamedHtmlElement(referenceNode.nextSibling, "br") 5458 && !isBlockNode(referenceNode.nextSibling)) { 5459 nodesToMove.push(referenceNode.nextSibling); 5460 } 5461 5462 // "While nodes to move is nonempty and its last member's nextSibling 5463 // is neither null nor a br nor a block node, append it to nodes to 5464 // move." 5465 if (nodesToMove.length 5466 && nodesToMove[nodesToMove.length - 1].nextSibling 5467 && !isNamedHtmlElement(nodesToMove[nodesToMove.length - 1].nextSibling, "br") 5468 && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) { 5469 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); 5470 } 5471 5472 // "Record the values of nodes to move, and let values be the result." 5473 var values = recordValues(nodesToMove); 5474 5475 // "For each node in nodes to move, append node as the last child of 5476 // start block, preserving ranges." 5477 $_( nodesToMove ).forEach(function(node) { 5478 movePreservingRanges(node, startBlock, -1, range); 5479 }); 5480 5481 // "If the nextSibling of reference node is a br, remove it from its 5482 // parent." 5483 if (isNamedHtmlElement(referenceNode.nextSibling, "br")) { 5484 referenceNode.parentNode.removeChild(referenceNode.nextSibling); 5485 } 5486 5487 // "Otherwise:" 5488 } else { 5489 // "Set the start and end of range to (start block, length of start 5490 // block)." 5491 range.setStart(startBlock, getNodeLength(startBlock)); 5492 range.setEnd(startBlock, getNodeLength(startBlock)); 5493 5494 // "If end block's firstChild is an inline node and start block's 5495 // lastChild is a br, remove start block's lastChild from it." 5496 if (isInlineNode(endBlock.firstChild) 5497 && isNamedHtmlElement(startBlock.lastChild, "br")) { 5498 startBlock.removeChild(startBlock.lastChild); 5499 } 5500 5501 // "Record the values of end block's children, and let values be the 5502 // result." 5503 var values = recordValues([].slice.call(toArray(endBlock.childNodes))); 5504 5505 // "While end block has children, append the first child of end block 5506 // to start block, preserving ranges." 5507 while (endBlock.hasChildNodes()) { 5508 movePreservingRanges(endBlock.firstChild, startBlock, -1, range); 5509 } 5510 5511 // "While end block has no children, let parent be the parent of end 5512 // block, then remove end block from parent, then set end block to 5513 // parent." 5514 while (!endBlock.hasChildNodes()) { 5515 var parent_ = endBlock.parentNode; 5516 parent_.removeChild(endBlock); 5517 endBlock = parent_; 5518 } 5519 } 5520 5521 // "Restore the values from values." 5522 restoreValues(values, range); 5523 5524 // "If start block has no children, call createElement("br") on the context 5525 // object and append the result as the last child of start block." 5526 ensureContainerEditable(startBlock); 5527 5528 // "Restore states and values from overrides." 5529 restoreStatesAndValues(overrides, range); 5530 } 5531 5532 5533 //@} 5534 ///// Splitting a node list's parent ///// 5535 //@{ 5536 5537 function splitParent(nodeList, range) { 5538 // "Let original parent be the parent of the first member of node list." 5539 var originalParent = nodeList[0].parentNode; 5540 5541 // "If original parent is not editable or its parent is null, do nothing 5542 // and abort these steps." 5543 if (!isEditable(originalParent) 5544 || !originalParent.parentNode) { 5545 return; 5546 } 5547 5548 // "If the first child of original parent is in node list, remove 5549 // extraneous line breaks before original parent." 5550 if (jQuery.inArray(originalParent.firstChild, nodeList) != -1) { 5551 removeExtraneousLineBreaksBefore(originalParent); 5552 } 5553 5554 var firstChildInNodeList = jQuery.inArray(originalParent.firstChild, nodeList) != -1; 5555 var lastChildInNodeList = jQuery.inArray(originalParent.lastChild, nodeList) != -1; 5556 5557 // "If the first child of original parent is in node list, and original 5558 // parent follows a line break, set follows line break to true. Otherwise, 5559 // set follows line break to false." 5560 var followsLineBreak_ = firstChildInNodeList && followsLineBreak(originalParent); 5561 5562 // "If the last child of original parent is in node list, and original 5563 // parent precedes a line break, set precedes line break to true. 5564 // Otherwise, set precedes line break to false." 5565 var precedesLineBreak_ = lastChildInNodeList && precedesLineBreak(originalParent); 5566 5567 // "If the first child of original parent is not in node list, but its last 5568 // child is:" 5569 if (!firstChildInNodeList && lastChildInNodeList) { 5570 // "For each node in node list, in reverse order, insert node into the 5571 // parent of original parent immediately after original parent, 5572 // preserving ranges." 5573 for (var i = nodeList.length - 1; i >= 0; i--) { 5574 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent), range); 5575 } 5576 5577 // "If precedes line break is true, and the last member of node list 5578 // does not precede a line break, call createElement("br") on the 5579 // context object and insert the result immediately after the last 5580 // member of node list." 5581 if (precedesLineBreak_ 5582 && !precedesLineBreak(nodeList[nodeList.length - 1])) { 5583 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); 5584 } 5585 5586 // "Remove extraneous line breaks at the end of original parent." 5587 removeExtraneousLineBreaksAtTheEndOf(originalParent); 5588 5589 // "Abort these steps." 5590 return; 5591 } 5592 5593 // "If the first child of original parent is not in node list:" 5594 if (!firstChildInNodeList) { 5595 // "Let cloned parent be the result of calling cloneNode(false) on 5596 // original parent." 5597 var clonedParent = originalParent.cloneNode(false); 5598 5599 // "If original parent has an id attribute, unset it." 5600 originalParent.removeAttribute("id"); 5601 5602 // "Insert cloned parent into the parent of original parent immediately 5603 // before original parent." 5604 originalParent.parentNode.insertBefore(clonedParent, originalParent); 5605 5606 // "While the previousSibling of the first member of node list is not 5607 // null, append the first child of original parent as the last child of 5608 // cloned parent, preserving ranges." 5609 while (nodeList[0].previousSibling) { 5610 movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length, range); 5611 } 5612 } 5613 5614 // "For each node in node list, insert node into the parent of original 5615 // parent immediately before original parent, preserving ranges." 5616 for (var i = 0; i < nodeList.length; i++) { 5617 movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent), range); 5618 } 5619 5620 // "If follows line break is true, and the first member of node list does 5621 // not follow a line break, call createElement("br") on the context object 5622 // and insert the result immediately before the first member of node list." 5623 if (followsLineBreak_ 5624 && !followsLineBreak(nodeList[0])) { 5625 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]); 5626 } 5627 5628 // "If the last member of node list is an inline node other than a br, and 5629 // the first child of original parent is a br, and original parent is not 5630 // an inline node, remove the first child of original parent from original 5631 // parent." 5632 if (isInlineNode(nodeList[nodeList.length - 1]) 5633 && !isNamedHtmlElement(nodeList[nodeList.length - 1], "br") 5634 && isNamedHtmlElement(originalParent.firstChild, "br") 5635 && !isInlineNode(originalParent)) { 5636 originalParent.removeChild(originalParent.firstChild); 5637 } 5638 5639 // "If original parent has no children:" 5640 if (!originalParent.hasChildNodes()) { 5641 // if the current range is collapsed and at the end of the originalParent.parentNode 5642 // the offset will not be available anymore after the next step (remove child) 5643 // that's why we need to fix the range to prevent a bogus offset 5644 if (originalParent.parentNode === range.startContainer 5645 && originalParent.parentNode === range.endContainer 5646 && range.startContainer === range.endContainer 5647 && range.startOffset === range.endOffset 5648 && originalParent.parentNode.childNodes.length === range.startOffset) { 5649 range.startOffset = originalParent.parentNode.childNodes.length - 1; 5650 range.endOffset = range.startOffset; 5651 } 5652 5653 // "Remove original parent from its parent." 5654 originalParent.parentNode.removeChild(originalParent); 5655 5656 // "If precedes line break is true, and the last member of node list 5657 // does not precede a line break, call createElement("br") on the 5658 // context object and insert the result immediately after the last 5659 // member of node list." 5660 if (precedesLineBreak_ 5661 && !precedesLineBreak(nodeList[nodeList.length - 1])) { 5662 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); 5663 } 5664 5665 // "Otherwise, remove extraneous line breaks before original parent." 5666 } else { 5667 removeExtraneousLineBreaksBefore(originalParent); 5668 } 5669 5670 // "If node list's last member's nextSibling is null, but its parent is not 5671 // null, remove extraneous line breaks at the end of node list's last 5672 // member's parent." 5673 if (!nodeList[nodeList.length - 1].nextSibling 5674 && nodeList[nodeList.length - 1].parentNode) { 5675 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode); 5676 } 5677 } 5678 5679 // "To remove a node node while preserving its descendants, split the parent of 5680 // node's children if it has any. If it has no children, instead remove it from 5681 // its parent." 5682 function removePreservingDescendants(node, range) { 5683 if (node.hasChildNodes()) { 5684 splitParent([].slice.call(toArray(node.childNodes)), range); 5685 } else { 5686 node.parentNode.removeChild(node); 5687 } 5688 } 5689 5690 5691 //@} 5692 ///// Canonical space sequences ///// 5693 //@{ 5694 5695 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) { 5696 // "If n is zero, return the empty string." 5697 if (n == 0) { 5698 return ""; 5699 } 5700 5701 // "If n is one and both non-breaking start and non-breaking end are false, 5702 // return a single space (U+0020)." 5703 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) { 5704 return " "; 5705 } 5706 5707 // "If n is one, return a single non-breaking space (U+00A0)." 5708 if (n == 1) { 5709 return "\xa0"; 5710 } 5711 5712 // "Let buffer be the empty string." 5713 var buffer = ""; 5714 5715 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020. 5716 // Otherwise, let it be U+0020 U+00A0." 5717 var repeatedPair; 5718 if (nonBreakingStart) { 5719 repeatedPair = "\xa0 "; 5720 } else { 5721 repeatedPair = " \xa0"; 5722 } 5723 5724 // "While n is greater than three, append repeated pair to buffer and 5725 // subtract two from n." 5726 while (n > 3) { 5727 buffer += repeatedPair; 5728 n -= 2; 5729 } 5730 5731 // "If n is three, append a three-element string to buffer depending on 5732 // non-breaking start and non-breaking end:" 5733 if (n == 3) { 5734 buffer += 5735 !nonBreakingStart && !nonBreakingEnd ? " \xa0 " 5736 : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 " 5737 : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0" 5738 : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0" 5739 : "impossible"; 5740 5741 // "Otherwise, append a two-element string to buffer depending on 5742 // non-breaking start and non-breaking end:" 5743 } else { 5744 buffer += 5745 !nonBreakingStart && !nonBreakingEnd ? "\xa0 " 5746 : nonBreakingStart && !nonBreakingEnd ? "\xa0 " 5747 : !nonBreakingStart && nonBreakingEnd ? " \xa0" 5748 : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0" 5749 : "impossible"; 5750 } 5751 5752 // "Return buffer." 5753 return buffer; 5754 } 5755 5756 function canonicalizeWhitespace(node, offset) { 5757 // "If node is neither editable nor an editing host, abort these steps." 5758 if (!isEditable(node) && !isEditingHost(node)) { 5759 return; 5760 } 5761 5762 // "Let start node equal node and let start offset equal offset." 5763 var startNode = node; 5764 var startOffset = offset; 5765 5766 // "Repeat the following steps:" 5767 while (true) { 5768 // "If start node has a child in the same editing host with index start 5769 // offset minus one, set start node to that child, then set start 5770 // offset to start node's length." 5771 if (0 <= startOffset - 1 5772 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) { 5773 startNode = startNode.childNodes[startOffset - 1]; 5774 startOffset = getNodeLength(startNode); 5775 5776 // "Otherwise, if start offset is zero and start node does not follow a 5777 // line break and start node's parent is in the same editing host, set 5778 // start offset to start node's index, then set start node to its 5779 // parent." 5780 } else if (startOffset == 0 5781 && !followsLineBreak(startNode) 5782 && inSameEditingHost(startNode, startNode.parentNode)) { 5783 startOffset = getNodeIndex(startNode); 5784 startNode = startNode.parentNode; 5785 5786 // "Otherwise, if start node is a Text node and its parent's resolved 5787 // value for "white-space" is neither "pre" nor "pre-wrap" and start 5788 // offset is not zero and the (start offset − 1)st element of start 5789 // node's data is a space (0x0020) or non-breaking space (0x00A0), 5790 5791 // subtract one from start offset." 5792 } else if (startNode.nodeType == $_.Node.TEXT_NODE 5793 && jQuery.inArray($_.getComputedStyle(startNode.parentNode).whiteSpace, ["pre", "pre-wrap"]) == -1 5794 && startOffset != 0 5795 && /[ \xa0]/.test(startNode.data[startOffset - 1])) { 5796 startOffset--; 5797 5798 // "Otherwise, break from this loop." 5799 } else { 5800 break; 5801 } 5802 } 5803 5804 // "Let end node equal start node and end offset equal start offset." 5805 var endNode = startNode; 5806 var endOffset = startOffset; 5807 5808 // "Let length equal zero." 5809 var length = 0; 5810 5811 // "Let follows space be false." 5812 var followsSpace = false; 5813 5814 // "Repeat the following steps:" 5815 while (true) { 5816 // "If end node has a child in the same editing host with index end 5817 // offset, set end node to that child, then set end offset to zero." 5818 if (endOffset < endNode.childNodes.length 5819 && inSameEditingHost(endNode, endNode.childNodes[endOffset])) { 5820 endNode = endNode.childNodes[endOffset]; 5821 endOffset = 0; 5822 5823 // "Otherwise, if end offset is end node's length and end node does not 5824 // precede a line break and end node's parent is in the same editing 5825 // host, set end offset to one plus end node's index, then set end node 5826 // to its parent." 5827 } else if (endOffset == getNodeLength(endNode) 5828 && !precedesLineBreak(endNode) 5829 && inSameEditingHost(endNode, endNode.parentNode)) { 5830 endOffset = 1 + getNodeIndex(endNode); 5831 endNode = endNode.parentNode; 5832 5833 // "Otherwise, if end node is a Text node and its parent's resolved 5834 // value for "white-space" is neither "pre" nor "pre-wrap" and end 5835 // offset is not end node's length and the end offsetth element of 5836 // end node's data is a space (0x0020) or non-breaking space (0x00A0):" 5837 } else if (endNode.nodeType == $_.Node.TEXT_NODE 5838 && jQuery.inArray($_.getComputedStyle(endNode.parentNode).whiteSpace, ["pre", "pre-wrap"]) == -1 5839 && endOffset != getNodeLength(endNode) 5840 && /[ \xa0]/.test(endNode.data[endOffset])) { 5841 // "If follows space is true and the end offsetth element of end 5842 // node's data is a space (0x0020), call deleteData(end offset, 1) 5843 // on end node, then continue this loop from the beginning." 5844 if (followsSpace 5845 && " " == endNode.data[endOffset]) { 5846 endNode.deleteData(endOffset, 1); 5847 continue; 5848 } 5849 5850 // "Set follows space to true if the end offsetth element of end 5851 // node's data is a space (0x0020), false otherwise." 5852 followsSpace = " " == endNode.data[endOffset]; 5853 5854 // "Add one to end offset." 5855 endOffset++; 5856 5857 // "Add one to length." 5858 length++; 5859 5860 // "Otherwise, break from this loop." 5861 } else { 5862 break; 5863 } 5864 } 5865 5866 // "Let replacement whitespace be the canonical space sequence of length 5867 // length. non-breaking start is true if start offset is zero and start 5868 // node follows a line break, and false otherwise. non-breaking end is true 5869 // if end offset is end node's length and end node precedes a line break, 5870 // and false otherwise." 5871 var replacementWhitespace = canonicalSpaceSequence(length, 5872 startOffset == 0 && followsLineBreak(startNode), 5873 endOffset == getNodeLength(endNode) && precedesLineBreak(endNode)); 5874 5875 // "While (start node, start offset) is before (end node, end offset):" 5876 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { 5877 // "If start node has a child with index start offset, set start node 5878 // to that child, then set start offset to zero." 5879 if (startOffset < startNode.childNodes.length) { 5880 startNode = startNode.childNodes[startOffset]; 5881 startOffset = 0; 5882 5883 // "Otherwise, if start node is not a Text node or if start offset is 5884 // start node's length, set start offset to one plus start node's 5885 // index, then set start node to its parent." 5886 } else if (startNode.nodeType != $_.Node.TEXT_NODE 5887 || startOffset == getNodeLength(startNode)) { 5888 startOffset = 1 + getNodeIndex(startNode); 5889 startNode = startNode.parentNode; 5890 5891 // "Otherwise:" 5892 } else { 5893 // "Remove the first element from replacement whitespace, and let 5894 // element be that element." 5895 var element = replacementWhitespace[0]; 5896 replacementWhitespace = replacementWhitespace.slice(1); 5897 5898 // "If element is not the same as the start offsetth element of 5899 // start node's data:" 5900 if (element != startNode.data[startOffset]) { 5901 // "Call insertData(start offset, element) on start node." 5902 startNode.insertData(startOffset, element); 5903 5904 // "Call deleteData(start offset + 1, 1) on start node." 5905 startNode.deleteData(startOffset + 1, 1); 5906 } 5907 5908 // "Add one to start offset." 5909 startOffset++; 5910 } 5911 } 5912 } 5913 5914 5915 //@} 5916 ///// Indenting and outdenting ///// 5917 //@{ 5918 5919 function cleanLists(node, range) { 5920 // remove any whitespace nodes around list nodes 5921 if (node) { 5922 jQuery(node).find('ul,ol,li').each(function () { 5923 jQuery(this).contents().each(function () { 5924 if (isWhitespaceNode(this)) { 5925 var index = getNodeIndex(this); 5926 5927 // if the range points to somewhere behind the removed text node, we reduce the offset 5928 if (range.startContainer === this.parentNode && range.startOffset > index) { 5929 range.startOffset--; 5930 } else if (range.startContainer === this) { 5931 // the range starts in the removed text node, let it start right before 5932 range.startContainer = this.parentNode; 5933 range.startOffset = index; 5934 } 5935 // same thing for end of the range 5936 if (range.endContainer === this.parentNode && range.endOffset > index) { 5937 range.endOffset--; 5938 } else if (range.endContainer === this) { 5939 range.endContainer = this.parentNode; 5940 range.endOffset = index; 5941 } 5942 // finally remove the whitespace node 5943 jQuery(this).remove(); 5944 } 5945 }); 5946 }); 5947 } 5948 } 5949 5950 5951 //@} 5952 ///// Indenting and outdenting ///// 5953 //@{ 5954 5955 function indentNodes(nodeList, range) { 5956 // "If node list is empty, do nothing and abort these steps." 5957 if (!nodeList.length) { 5958 return; 5959 } 5960 5961 // "Let first node be the first member of node list." 5962 var firstNode = nodeList[0]; 5963 5964 // "If first node's parent is an ol or ul:" 5965 if (isHtmlElementInArray(firstNode.parentNode, ["OL", "UL"])) { 5966 // "Let tag be the local name of the parent of first node." 5967 var tag = firstNode.parentNode.tagName; 5968 5969 // "Wrap node list, with sibling criteria returning true for an HTML 5970 // element with local name tag and false otherwise, and new parent 5971 // instructions returning the result of calling createElement(tag) on 5972 // the ownerDocument of first node." 5973 wrap(nodeList, 5974 function(node) { return isHtmlElement_obsolete(node, tag) }, 5975 function() { return firstNode.ownerDocument.createElement(tag) }, 5976 range 5977 ); 5978 5979 // "Abort these steps." 5980 return; 5981 } 5982 5983 // "Wrap node list, with sibling criteria returning true for a simple 5984 // indentation element and false otherwise, and new parent instructions 5985 // returning the result of calling createElement("blockquote") on the 5986 // ownerDocument of first node. Let new parent be the result." 5987 var newParent = wrap(nodeList, 5988 function(node) { return isSimpleIndentationElement(node) }, 5989 function() { return firstNode.ownerDocument.createElement("blockquote") }, 5990 range 5991 ); 5992 5993 // "Fix disallowed ancestors of new parent." 5994 fixDisallowedAncestors(newParent, range); 5995 } 5996 5997 function outdentNode(node, range) { 5998 // "If node is not editable, abort these steps." 5999 if (!isEditable(node)) { 6000 return; 6001 } 6002 6003 // "If node is a simple indentation element, remove node, preserving its 6004 // descendants. Then abort these steps." 6005 if (isSimpleIndentationElement(node)) { 6006 removePreservingDescendants(node, range); 6007 return; 6008 } 6009 6010 // "If node is an indentation element:" 6011 if (isIndentationElement(node)) { 6012 // "Unset the class and dir attributes of node, if any." 6013 node.removeAttribute("class"); 6014 node.removeAttribute("dir"); 6015 6016 // "Unset the margin, padding, and border CSS properties of node." 6017 6018 node.style.margin = ""; 6019 node.style.padding = ""; 6020 node.style.border = ""; 6021 if (node.getAttribute("style") == "") { 6022 node.removeAttribute("style"); 6023 } 6024 6025 // "Set the tag name of node to "div"." 6026 setTagName(node, "div", range); 6027 6028 // "Abort these steps." 6029 return; 6030 } 6031 6032 // "Let current ancestor be node's parent." 6033 var currentAncestor = node.parentNode; 6034 6035 // "Let ancestor list be a list of nodes, initially empty." 6036 6037 var ancestorList = []; 6038 6039 // "While current ancestor is an editable Element that is neither a simple 6040 // indentation element nor an ol nor a ul, append current ancestor to 6041 // ancestor list and then set current ancestor to its parent." 6042 while (isEditable(currentAncestor) 6043 && currentAncestor.nodeType == $_.Node.ELEMENT_NODE 6044 && !isSimpleIndentationElement(currentAncestor) 6045 && !isHtmlElementInArray(currentAncestor, ["ol", "ul"])) { 6046 ancestorList.push(currentAncestor); 6047 currentAncestor = currentAncestor.parentNode; 6048 } 6049 6050 // "If current ancestor is not an editable simple indentation element:" 6051 if (!isEditable(currentAncestor) 6052 || !isSimpleIndentationElement(currentAncestor)) { 6053 // "Let current ancestor be node's parent." 6054 currentAncestor = node.parentNode; 6055 6056 // "Let ancestor list be the empty list." 6057 ancestorList = []; 6058 6059 // "While current ancestor is an editable Element that is neither an 6060 // indentation element nor an ol nor a ul, append current ancestor to 6061 // ancestor list and then set current ancestor to its parent." 6062 while (isEditable(currentAncestor) 6063 && currentAncestor.nodeType == $_.Node.ELEMENT_NODE 6064 && !isIndentationElement(currentAncestor) 6065 && !isHtmlElementInArray(currentAncestor, ["ol", "ul"])) { 6066 ancestorList.push(currentAncestor); 6067 currentAncestor = currentAncestor.parentNode; 6068 } 6069 } 6070 6071 // "If node is an ol or ul and current ancestor is not an editable 6072 // indentation element:" 6073 if (isHtmlElementInArray(node, ["OL", "UL"]) 6074 && (!isEditable(currentAncestor) 6075 || !isIndentationElement(currentAncestor))) { 6076 // "Unset the reversed, start, and type attributes of node, if any are 6077 // set." 6078 node.removeAttribute("reversed"); 6079 node.removeAttribute("start"); 6080 node.removeAttribute("type"); 6081 6082 // "Let children be the children of node." 6083 var children = [].slice.call(toArray(node.childNodes)); 6084 6085 // "If node has attributes, and its parent is not an ol or ul, set the 6086 // tag name of node to "div"." 6087 if (node.attributes.length 6088 && !isHtmlElementInArray(node.parentNode, ["OL", "UL"])) { 6089 setTagName(node, "div", range); 6090 6091 // "Otherwise:" 6092 } else { 6093 // "Record the values of node's children, and let values be the 6094 // result." 6095 var values = recordValues([].slice.call(toArray(node.childNodes))); 6096 6097 // "Remove node, preserving its descendants." 6098 removePreservingDescendants(node, range); 6099 6100 // "Restore the values from values." 6101 restoreValues(values, range); 6102 } 6103 6104 // "Fix disallowed ancestors of each member of children." 6105 for (var i = 0; i < children.length; i++) { 6106 fixDisallowedAncestors(children[i], range); 6107 } 6108 6109 // "Abort these steps." 6110 return; 6111 } 6112 6113 // "If current ancestor is not an editable indentation element, abort these 6114 // steps." 6115 if (!isEditable(currentAncestor) 6116 || !isIndentationElement(currentAncestor)) { 6117 return; 6118 } 6119 6120 // "Append current ancestor to ancestor list." 6121 ancestorList.push(currentAncestor); 6122 6123 // "Let original ancestor be current ancestor." 6124 var originalAncestor = currentAncestor; 6125 6126 // "While ancestor list is not empty:" 6127 while (ancestorList.length) { 6128 // "Let current ancestor be the last member of ancestor list." 6129 // 6130 // "Remove the last member of ancestor list." 6131 currentAncestor = ancestorList.pop(); 6132 6133 // "Let target be the child of current ancestor that is equal to either 6134 // node or the last member of ancestor list." 6135 var target = node.parentNode == currentAncestor 6136 ? node 6137 : ancestorList[ancestorList.length - 1]; 6138 6139 // "If target is an inline node that is not a br, and its nextSibling 6140 // is a br, remove target's nextSibling from its parent." 6141 if (isInlineNode(target) 6142 && !isNamedHtmlElement(target, 'BR') 6143 && isNamedHtmlElement(target.nextSibling, "BR")) { 6144 target.parentNode.removeChild(target.nextSibling); 6145 } 6146 6147 // "Let preceding siblings be the preceding siblings of target, and let 6148 // following siblings be the following siblings of target." 6149 var precedingSiblings = [].slice.call(toArray(currentAncestor.childNodes), 0, getNodeIndex(target)); 6150 var followingSiblings = [].slice.call(toArray(currentAncestor.childNodes), 1 + getNodeIndex(target)); 6151 6152 // "Indent preceding siblings." 6153 indentNodes(precedingSiblings, range); 6154 6155 // "Indent following siblings." 6156 indentNodes(followingSiblings, range); 6157 } 6158 6159 // "Outdent original ancestor." 6160 outdentNode(originalAncestor, range); 6161 } 6162 6163 6164 //@} 6165 ///// Toggling lists ///// 6166 //@{ 6167 6168 function toggleLists(tagName, range) { 6169 // "Let mode be "disable" if the selection's list state is tag name, and 6170 // "enable" otherwise." 6171 var mode = getSelectionListState() == tagName ? "disable" : "enable"; 6172 6173 tagName = tagName.toUpperCase(); 6174 6175 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is 6176 // "ol"." 6177 var otherTagName = tagName == "OL" ? "UL" : "OL"; 6178 6179 // "Let items be a list of all lis that are ancestor containers of the 6180 // range's start and/or end node." 6181 // 6182 // It's annoying to get this in tree order using functional stuff without 6183 // doing getDescendants(document), which is slow, so I do it imperatively. 6184 var items = []; 6185 (function(){ 6186 for ( 6187 var ancestorContainer = range.endContainer; 6188 ancestorContainer != range.commonAncestorContainer; 6189 ancestorContainer = ancestorContainer.parentNode 6190 ) { 6191 if (isNamedHtmlElement(ancestorContainer, "li")) { 6192 items.unshift(ancestorContainer); 6193 } 6194 } 6195 for ( 6196 var ancestorContainer = range.startContainer; 6197 ancestorContainer; 6198 ancestorContainer = ancestorContainer.parentNode 6199 ) { 6200 if (isNamedHtmlElement(ancestorContainer, "li")) { 6201 items.unshift(ancestorContainer); 6202 } 6203 } 6204 })(); 6205 6206 // "For each item in items, normalize sublists of item." 6207 $_( items ).forEach( function( thisArg ) { 6208 normalizeSublists( thisArg, range); 6209 }); 6210 6211 // "Block-extend the range, and let new range be the result." 6212 var newRange = blockExtend(range); 6213 6214 // "If mode is "enable", then let lists to convert consist of every 6215 // editable HTML element with local name other tag name that is contained 6216 // in new range, and for every list in lists to convert:" 6217 if (mode == "enable") { 6218 $_( getAllContainedNodes(newRange, function(node) { 6219 return isEditable(node) 6220 && isHtmlElement_obsolete(node, otherTagName); 6221 }) ).forEach(function(list) { 6222 // "If list's previousSibling or nextSibling is an editable HTML 6223 // element with local name tag name:" 6224 if ((isEditable(list.previousSibling) && isHtmlElement_obsolete(list.previousSibling, tagName)) 6225 || (isEditable(list.nextSibling) && isHtmlElement_obsolete(list.nextSibling, tagName))) { 6226 // "Let children be list's children." 6227 var children = [].slice.call(toArray(list.childNodes)); 6228 6229 // "Record the values of children, and let values be the 6230 // result." 6231 var values = recordValues(children); 6232 6233 // "Split the parent of children." 6234 splitParent(children, range); 6235 6236 // "Wrap children, with sibling criteria returning true for an 6237 // HTML element with local name tag name and false otherwise." 6238 wrap(children, 6239 function(node) { return isHtmlElement_obsolete(node, tagName) }, 6240 function() {return null }, 6241 range 6242 ); 6243 6244 // "Restore the values from values." 6245 restoreValues(values, range); 6246 6247 // "Otherwise, set the tag name of list to tag name." 6248 } else { 6249 setTagName(list, tagName, range); 6250 } 6251 }); 6252 } 6253 6254 // "Let node list be a list of nodes, initially empty." 6255 // 6256 // "For each node node contained in new range, if node is editable; the 6257 // last member of node list (if any) is not an ancestor of node; node 6258 // is not an indentation element; and either node is an ol or ul, or its 6259 // parent is an ol or ul, or it is an allowed child of "li"; then append 6260 // node to node list." 6261 var nodeList = getContainedNodes(newRange, function(node) { 6262 return isEditable(node) 6263 && !isIndentationElement(node) 6264 && (isHtmlElementInArray(node, ["OL", "UL"]) 6265 || isHtmlElementInArray(node.parentNode, ["OL", "UL"]) 6266 || isAllowedChild(node, "li")); 6267 }); 6268 6269 // "If mode is "enable", remove from node list any ol or ul whose parent is 6270 // not also an ol or ul." 6271 if (mode == "enable") { 6272 nodeList = $_( nodeList ).filter(function(node) { 6273 return !isHtmlElementInArray(node, ["ol", "ul"]) 6274 || isHtmlElementInArray(node.parentNode, ["ol", "ul"]); 6275 }); 6276 } 6277 6278 // "If mode is "disable", then while node list is not empty:" 6279 if (mode == "disable") { 6280 while (nodeList.length) { 6281 // "Let sublist be an empty list of nodes." 6282 var sublist = []; 6283 6284 // "Remove the first member from node list and append it to 6285 // sublist." 6286 sublist.push(nodeList.shift()); 6287 6288 // "If the first member of sublist is an HTML element with local 6289 // name tag name, outdent it and continue this loop from the 6290 // beginning." 6291 if (isHtmlElement_obsolete(sublist[0], tagName)) { 6292 outdentNode(sublist[0], range); 6293 continue; 6294 } 6295 6296 // "While node list is not empty, and the first member of node list 6297 // is the nextSibling of the last member of sublist and is not an 6298 // HTML element with local name tag name, remove the first member 6299 // from node list and append it to sublist." 6300 while (nodeList.length 6301 && nodeList[0] == sublist[sublist.length - 1].nextSibling 6302 && !isHtmlElement_obsolete(nodeList[0], tagName)) { 6303 sublist.push(nodeList.shift()); 6304 } 6305 6306 // "Record the values of sublist, and let values be the result." 6307 var values = recordValues(sublist); 6308 6309 // "Split the parent of sublist." 6310 splitParent(sublist, range); 6311 6312 // "Fix disallowed ancestors of each member of sublist." 6313 for (var i = 0; i < sublist.length; i++) { 6314 fixDisallowedAncestors(sublist[i], range); 6315 } 6316 6317 // "Restore the values from values." 6318 restoreValues(values, range); 6319 } 6320 6321 // "Otherwise, while node list is not empty:" 6322 } else { 6323 while (nodeList.length) { 6324 // "Let sublist be an empty list of nodes." 6325 var sublist = []; 6326 6327 // "While either sublist is empty, or node list is not empty and 6328 // its first member is the nextSibling of sublist's last member:" 6329 while (!sublist.length 6330 || (nodeList.length 6331 && nodeList[0] == sublist[sublist.length - 1].nextSibling)) { 6332 // "If node list's first member is a p or div, set the tag name 6333 // of node list's first member to "li", and append the result 6334 // to sublist. Remove the first member from node list." 6335 if (isHtmlElementInArray(nodeList[0], ["p", "div"])) { 6336 sublist.push(setTagName(nodeList[0], "li", range)); 6337 nodeList.shift(); 6338 6339 // "Otherwise, if the first member of node list is an li or ol 6340 // or ul, remove it from node list and append it to sublist." 6341 } else if (isHtmlElementInArray(nodeList[0], ["li", "ol", "ul"])) { 6342 sublist.push(nodeList.shift()); 6343 6344 // "Otherwise:" 6345 } else { 6346 // "Let nodes to wrap be a list of nodes, initially empty." 6347 var nodesToWrap = []; 6348 6349 // "While nodes to wrap is empty, or node list is not empty 6350 // and its first member is the nextSibling of nodes to 6351 // wrap's last member and the first member of node list is 6352 // an inline node and the last member of nodes to wrap is 6353 // an inline node other than a br, remove the first member 6354 // from node list and append it to nodes to wrap." 6355 while (!nodesToWrap.length 6356 || (nodeList.length 6357 && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling 6358 && isInlineNode(nodeList[0]) 6359 && isInlineNode(nodesToWrap[nodesToWrap.length - 1]) 6360 && !isNamedHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) { 6361 nodesToWrap.push(nodeList.shift()); 6362 } 6363 6364 // "Wrap nodes to wrap, with new parent instructions 6365 // returning the result of calling createElement("li") on 6366 // the context object. Append the result to sublist." 6367 sublist.push( 6368 wrap(nodesToWrap, 6369 undefined, 6370 function() { return document.createElement("li") }, 6371 range 6372 ) 6373 ); 6374 } 6375 } 6376 6377 // "If sublist's first member's parent is an HTML element with 6378 // local name tag name, or if every member of sublist is an ol or 6379 // ul, continue this loop from the beginning." 6380 if (isHtmlElement_obsolete(sublist[0].parentNode, tagName) 6381 || $_(sublist).every(function(node) { return isHtmlElementInArray(node, ["ol", "ul"]) })) { 6382 continue; 6383 } 6384 6385 // "If sublist's first member's parent is an HTML element with 6386 // local name other tag name:" 6387 if (isHtmlElement_obsolete(sublist[0].parentNode, otherTagName)) { 6388 // "Record the values of sublist, and let values be the 6389 // result." 6390 var values = recordValues(sublist); 6391 6392 // "Split the parent of sublist." 6393 splitParent(sublist, range); 6394 6395 // "Wrap sublist, with sibling criteria returning true for an 6396 // HTML element with local name tag name and false otherwise, 6397 // and new parent instructions returning the result of calling 6398 // createElement(tag name) on the context object." 6399 wrap(sublist, 6400 function(node) { return isHtmlElement_obsolete(node, tagName) }, 6401 function() { return document.createElement(tagName) }, 6402 range 6403 ); 6404 6405 // "Restore the values from values." 6406 restoreValues(values, range); 6407 6408 // "Continue this loop from the beginning." 6409 continue; 6410 } 6411 6412 // "Wrap sublist, with sibling criteria returning true for an HTML 6413 // element with local name tag name and false otherwise, and new 6414 // parent instructions being the following:" 6415 // . . . 6416 // "Fix disallowed ancestors of the previous step's result." 6417 fixDisallowedAncestors( 6418 wrap(sublist, 6419 function(node) { return isHtmlElement_obsolete(node, tagName) }, 6420 function() { 6421 // "If sublist's first member's parent is not an editable 6422 // simple indentation element, or sublist's first member's 6423 // parent's previousSibling is not an editable HTML element 6424 // with local name tag name, call createElement(tag name) 6425 // on the context object and return the result." 6426 if (!isEditable(sublist[0].parentNode) 6427 || !isSimpleIndentationElement(sublist[0].parentNode) 6428 || !isEditable(sublist[0].parentNode.previousSibling) 6429 || !isHtmlElement_obsolete(sublist[0].parentNode.previousSibling, tagName)) { 6430 return document.createElement(tagName); 6431 } 6432 6433 // "Let list be sublist's first member's parent's 6434 // previousSibling." 6435 var list = sublist[0].parentNode.previousSibling; 6436 6437 // "Normalize sublists of list's lastChild." 6438 normalizeSublists(list.lastChild, range); 6439 6440 // "If list's lastChild is not an editable HTML element 6441 // with local name tag name, call createElement(tag name) 6442 // on the context object, and append the result as the last 6443 // child of list." 6444 6445 if (!isEditable(list.lastChild) 6446 || !isHtmlElement_obsolete(list.lastChild, tagName)) { 6447 list.appendChild(document.createElement(tagName)); 6448 } 6449 6450 // "Return the last child of list." 6451 return list.lastChild; 6452 }, 6453 range 6454 ) 6455 , range 6456 ); 6457 } 6458 } 6459 } 6460 6461 6462 //@} 6463 ///// Justifying the selection ///// 6464 //@{ 6465 6466 function justifySelection(alignment, range) { 6467 6468 // "Block-extend the active range, and let new range be the result." 6469 var newRange = blockExtend(range); 6470 6471 // "Let element list be a list of all editable Elements contained in new 6472 // range that either has an attribute in the HTML namespace whose local 6473 // name is "align", or has a style attribute that sets "text-align", or is 6474 // a center." 6475 var elementList = getAllContainedNodes(newRange, function(node) { 6476 return node.nodeType == $_.Node.ELEMENT_NODE 6477 && isEditable(node) 6478 // Ignoring namespaces here 6479 && ( 6480 hasAttribute(node, "align") 6481 || node.style.textAlign != "" 6482 || isNamedHtmlElement(node, 'center') 6483 ); 6484 }); 6485 6486 // "For each element in element list:" 6487 for (var i = 0; i < elementList.length; i++) { 6488 var element = elementList[i]; 6489 6490 // "If element has an attribute in the HTML namespace whose local name 6491 // is "align", remove that attribute." 6492 element.removeAttribute("align"); 6493 6494 // "Unset the CSS property "text-align" on element, if it's set by a 6495 // style attribute." 6496 element.style.textAlign = ""; 6497 if (element.getAttribute("style") == "") { 6498 element.removeAttribute("style"); 6499 } 6500 6501 // "If element is a div or span or center with no attributes, remove 6502 // it, preserving its descendants." 6503 if (isHtmlElementInArray(element, ["div", "span", "center"]) 6504 && !element.attributes.length) { 6505 removePreservingDescendants(element, range); 6506 } 6507 6508 // "If element is a center with one or more attributes, set the tag 6509 // name of element to "div"." 6510 if (isNamedHtmlElement(element, 'center') 6511 && element.attributes.length) { 6512 setTagName(element, "div", range); 6513 } 6514 } 6515 6516 // "Block-extend the active range, and let new range be the result." 6517 newRange = blockExtend(globalRange); 6518 6519 // "Let node list be a list of nodes, initially empty." 6520 var nodeList = []; 6521 6522 // "For each node node contained in new range, append node to node list if 6523 // the last member of node list (if any) is not an ancestor of node; node 6524 // is editable; node is an allowed child of "div"; and node's alignment 6525 // value is not alignment." 6526 nodeList = getContainedNodes(newRange, function(node) { 6527 return isEditable(node) 6528 && isAllowedChild(node, "div") 6529 && getAlignmentValue(node) != alignment; 6530 }); 6531 6532 // "While node list is not empty:" 6533 while (nodeList.length) { 6534 // "Let sublist be a list of nodes, initially empty." 6535 var sublist = []; 6536 6537 // "Remove the first member of node list and append it to sublist." 6538 sublist.push(nodeList.shift()); 6539 6540 // "While node list is not empty, and the first member of node list is 6541 // the nextSibling of the last member of sublist, remove the first 6542 // member of node list and append it to sublist." 6543 while (nodeList.length 6544 && nodeList[0] == sublist[sublist.length - 1].nextSibling) { 6545 sublist.push(nodeList.shift()); 6546 } 6547 6548 // "Wrap sublist. Sibling criteria returns true for any div that has 6549 // one or both of the following two attributes and no other attributes, 6550 // and false otherwise:" 6551 // 6552 // * "An align attribute whose value is an ASCII case-insensitive 6553 // match for alignment. 6554 // * "A style attribute which sets exactly one CSS property 6555 // (including unrecognized or invalid attributes), which is 6556 // "text-align", which is set to alignment. 6557 // 6558 // "New parent instructions are to call createElement("div") on the 6559 // context object, then set its CSS property "text-align" to alignment 6560 // and return the result." 6561 wrap(sublist, 6562 function(node) { 6563 return isNamedHtmlElement(node, 'div') 6564 && $_(node.attributes).every(function(attr) { 6565 return (attr.name == "align" && attr.value.toLowerCase() == alignment) 6566 || (attr.name == "style" && getStyleLength(node) == 1 && node.style.textAlign == alignment); 6567 }); 6568 }, 6569 function() { 6570 var newParent = document.createElement("div"); 6571 newParent.setAttribute("style", "text-align: " + alignment); 6572 return newParent; 6573 }, 6574 range 6575 ); 6576 } 6577 } 6578 6579 //@} 6580 ///// Check whether the given element is an end break ///// 6581 //@{ 6582 function isEndBreak(element) { 6583 if (!isNamedHtmlElement(element, 'br')) { 6584 return false; 6585 } 6586 return jQuery(element).hasClass('aloha-end-br'); 6587 } 6588 6589 //@} 6590 ///// Create an end break ///// 6591 //@{ 6592 function createEndBreak() { 6593 var endBr = document.createElement("br"); 6594 endBr.setAttribute("class", "aloha-end-br"); 6595 6596 return endBr; 6597 } 6598 6599 /** 6600 * Ensure the container is editable 6601 * E.g. when called for an empty paragraph or header, and the browser is not IE, 6602 * we need to append a br (marked with class aloha-end-br) 6603 * For IE7, there is a special behaviour that will append zero-width whitespace 6604 * @param {DOMNode} container 6605 */ 6606 function ensureContainerEditable(container) { 6607 if (!container) { 6608 return; 6609 } 6610 6611 if (isNamedHtmlElement(container.lastChild, "br")) { 6612 return; 6613 } 6614 6615 if ($_(container.childNodes).some(isVisible)) { 6616 return; 6617 } 6618 6619 if (!jQuery.browser.msie) { 6620 // for normal browsers, the end-br will do 6621 container.appendChild(createEndBreak()); 6622 } else if (jQuery.browser.msie && jQuery.browser.version <= 7 && 6623 isHtmlElementInArray(container, ["p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "blockquote"])) { 6624 // for IE7, we need to insert a text node containing a single zero-width whitespace character 6625 if (!container.firstChild) { 6626 container.appendChild(document.createTextNode('\u200b')); 6627 } 6628 } 6629 } 6630 6631 //@} 6632 ///// Move the given collapsed range over adjacent zero-width whitespace characters. 6633 ///// The range is 6634 //@{ 6635 /** 6636 * Move the given collapsed range over adjacent zero-width whitespace characters. 6637 * If the range is not collapsed or is not contained in a text node, it is not modified 6638 * @param range range to modify 6639 * @param forward {Boolean} true to move forward, false to move backward 6640 */ 6641 function moveOverZWSP(range, forward) { 6642 var offset; 6643 if (!range.collapsed) { 6644 return; 6645 } 6646 6647 offset = range.startOffset; 6648 6649 if (forward) { 6650 // check whether the range starts in a text node 6651 if (range.startContainer && range.startContainer.nodeType === $_.Node.TEXT_NODE) { 6652 // move forward (i.e. increase offset) as long as we stay in the text node and have zwsp characters to the right 6653 while (offset < range.startContainer.data.length && range.startContainer.data.charAt(offset) === '\u200b') { 6654 offset++; 6655 } 6656 } 6657 } else { 6658 // check whether the range starts in a text node 6659 if (range.startContainer && range.startContainer.nodeType === $_.Node.TEXT_NODE) { 6660 // move backward (i.e. decrease offset) as long as we stay in the text node and have zwsp characters to the left 6661 while (offset > 0 && range.startContainer.data.charAt(offset - 1) === '\u200b') { 6662 offset--; 6663 } 6664 } 6665 } 6666 6667 // if the offset was changed, set it back to the collapsed range 6668 if (offset !== range.startOffset) { 6669 range.setStart(range.startContainer, offset); 6670 range.setEnd(range.startContainer, offset); 6671 } 6672 } 6673 6674 /** 6675 * implementation of the delete command 6676 * will attempt to delete contents within range if non-collapsed 6677 * or delete the character left of the cursor position if range 6678 * is collapsed. Is used to define the behaviour of the backspace 6679 * button. 6680 * 6681 * @param value is just there for compatibility with the commands api. parameter is ignored. 6682 * @param range the range to execute the delete command for 6683 * @return void 6684 */ 6685 commands["delete"] = { 6686 action: function(value, range) { 6687 // special behaviour for skipping zero-width whitespaces in IE7 6688 if (jQuery.browser.msie && jQuery.browser.version <= 7) { 6689 moveOverZWSP(range, false); 6690 } 6691 6692 // "If the active range is not collapsed, delete the contents of the 6693 // active range and abort these steps." 6694 if (!range.collapsed) { 6695 deleteContents(range); 6696 return; 6697 } 6698 6699 // "Canonicalize whitespace at (active range's start node, active 6700 // range's start offset)." 6701 canonicalizeWhitespace(range.startContainer, range.startOffset); 6702 6703 // "Let node and offset be the active range's start node and offset." 6704 var node = range.startContainer; 6705 var offset = range.startOffset; 6706 var isBr = false; 6707 var isHr = false; 6708 6709 // "Repeat the following steps:" 6710 while ( true ) { 6711 // we need to reset isBr and isHr on every interation of the loop 6712 if ( offset > 0 ) { 6713 isBr = isNamedHtmlElement(node.childNodes[offset - 1], "br") || false; 6714 isHr = isNamedHtmlElement(node.childNodes[offset - 1], "hr") || false; 6715 } 6716 6717 // "If offset is zero and node's previousSibling is an editable 6718 // invisible node, remove node's previousSibling from its parent." 6719 if (offset == 0 6720 && isEditable(node.previousSibling) 6721 && isInvisible(node.previousSibling)) { 6722 node.parentNode.removeChild(node.previousSibling); 6723 6724 // "Otherwise, if node has a child with index offset − 1 and that 6725 // child is an editable invisible node, remove that child from 6726 // node, then subtract one from offset." 6727 } else if (0 <= offset - 1 6728 && offset - 1 < node.childNodes.length 6729 && isEditable(node.childNodes[offset - 1]) 6730 && (isInvisible(node.childNodes[offset - 1]) || isBr || isHr )) { 6731 node.removeChild(node.childNodes[offset - 1]); 6732 offset--; 6733 if (isBr || isHr) { 6734 range.setStart(node, offset); 6735 range.setEnd(node, offset); 6736 return; 6737 } 6738 6739 // "Otherwise, if offset is zero and node is an inline node, or if 6740 // node is an invisible node, set offset to the index of node, then 6741 // set node to its parent." 6742 } else if ((offset == 0 6743 && isInlineNode(node)) 6744 || isInvisible(node)) { 6745 offset = getNodeIndex(node); 6746 node = node.parentNode; 6747 6748 // "Otherwise, if node has a child with index offset − 1 and that 6749 // child is an editable a, remove that child from node, preserving 6750 // its descendants. Then abort these steps." 6751 } else if (0 <= offset - 1 6752 && offset - 1 < node.childNodes.length 6753 && isEditable(node.childNodes[offset - 1]) 6754 && isNamedHtmlElement(node.childNodes[offset - 1], "a")) { 6755 removePreservingDescendants(node.childNodes[offset - 1], range); 6756 return; 6757 6758 // "Otherwise, if node has a child with index offset − 1 and that 6759 // child is not a block node or a br or an img, set node to that 6760 // child, then set offset to the length of node." 6761 } else if (0 <= offset - 1 6762 && offset - 1 < node.childNodes.length 6763 && !isBlockNode(node.childNodes[offset - 1]) 6764 && !isHtmlElementInArray(node.childNodes[offset - 1], ["br", "img"])) { 6765 node = node.childNodes[offset - 1]; 6766 offset = getNodeLength(node); 6767 6768 // "Otherwise, break from this loop." 6769 } else { 6770 break; 6771 } 6772 } 6773 6774 // if the previous node is an aloha-table we want to delete it 6775 var delBlock; 6776 if (delBlock = getBlockAtPreviousPosition(node, offset)) { 6777 delBlock.parentNode.removeChild(delBlock); 6778 return; 6779 } 6780 6781 // "If node is a Text node and offset is not zero, call collapse(node, 6782 // offset) on the Selection. Then delete the contents of the range with 6783 // start (node, offset − 1) and end (node, offset) and abort these 6784 // steps." 6785 if (node.nodeType == $_.Node.TEXT_NODE 6786 && offset != 0) { 6787 range.setStart(node, offset - 1); 6788 range.setEnd(node, offset - 1); 6789 deleteContents(node, offset - 1, node, offset); 6790 return; 6791 } 6792 6793 // @iebug 6794 // when inserting a special char via the plugin 6795 // there where problems deleting them again with backspace after insertation 6796 // see https://github.com/alohaeditor/Aloha-Editor/issues/517 6797 if (node.nodeType == $_.Node.TEXT_NODE 6798 && offset == 0 && jQuery.browser.msie) { 6799 offset = 1; 6800 range.setStart(node, offset); 6801 range.setEnd(node, offset); 6802 range.startOffset = 0; 6803 deleteContents(range); 6804 return; 6805 } 6806 6807 // "If node is an inline node, abort these steps." 6808 if (isInlineNode(node)) { 6809 return; 6810 } 6811 6812 // "If node has a child with index offset − 1 and that child is a br or 6813 // hr or img, call collapse(node, offset) on the Selection. Then delete 6814 // the contents of the range with start (node, offset − 1) and end 6815 // (node, offset) and abort these steps." 6816 if (0 <= offset - 1 6817 && offset - 1 < node.childNodes.length 6818 && isHtmlElementInArray(node.childNodes[offset - 1], ["br", "hr", "img"])) { 6819 range.setStart(node, offset); 6820 range.setEnd(node, offset); 6821 deleteContents(range); 6822 return; 6823 } 6824 6825 // "If node is an li or dt or dd and is the first child of its parent, 6826 // and offset is zero:" 6827 if (isHtmlElementInArray(node, ["li", "dt", "dd"]) 6828 && node == node.parentNode.firstChild 6829 && offset == 0) { 6830 // "Let items be a list of all lis that are ancestors of node." 6831 // 6832 // Remember, must be in tree order. 6833 var items = []; 6834 for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) { 6835 if (isNamedHtmlElement(ancestor, 'li')) { 6836 items.unshift(ancestor); 6837 } 6838 } 6839 6840 // "Normalize sublists of each item in items." 6841 for (var i = 0; i < items.length; i++) { 6842 normalizeSublists(items[i], range); 6843 } 6844 6845 // "Record the values of the one-node list consisting of node, and 6846 // let values be the result." 6847 var values = recordValues([node]); 6848 6849 // "Split the parent of the one-node list consisting of node." 6850 splitParent([node], range); 6851 6852 // "Restore the values from values." 6853 restoreValues(values, range); 6854 6855 // "If node is a dd or dt, and it is not an allowed child of any of 6856 // its ancestors in the same editing host, set the tag name of node 6857 // to the default single-line container name and let node be the 6858 // result." 6859 if (isHtmlElementInArray(node, ["dd", "dt"]) 6860 && $_(getAncestors(node)).every(function(ancestor) { 6861 return !inSameEditingHost(node, ancestor) 6862 || !isAllowedChild(node, ancestor) 6863 })) { 6864 node = setTagName(node, defaultSingleLineContainerName, range); 6865 } 6866 6867 // "Fix disallowed ancestors of node." 6868 fixDisallowedAncestors(node, range); 6869 6870 // fix the lists to be html5 conformant 6871 for (var i = 0; i < items.length; i++) { 6872 unNormalizeSublists(items[i].parentNode, range); 6873 } 6874 6875 // "Abort these steps." 6876 return; 6877 } 6878 6879 // "Let start node equal node and let start offset equal offset." 6880 var startNode = node; 6881 var startOffset = offset; 6882 6883 // "Repeat the following steps:" 6884 while (true) { 6885 // "If start offset is zero, set start offset to the index of start 6886 // node and then set start node to its parent." 6887 if (startOffset == 0) { 6888 startOffset = getNodeIndex(startNode); 6889 startNode = startNode.parentNode; 6890 6891 // "Otherwise, if start node has an editable invisible child with 6892 // index start offset minus one, remove it from start node and 6893 // subtract one from start offset." 6894 } else if (0 <= startOffset - 1 6895 && startOffset - 1 < startNode.childNodes.length 6896 && isEditable(startNode.childNodes[startOffset - 1]) 6897 && isInvisible(startNode.childNodes[startOffset - 1])) { 6898 startNode.removeChild(startNode.childNodes[startOffset - 1]); 6899 startOffset--; 6900 6901 // "Otherwise, break from this loop." 6902 } else { 6903 break; 6904 } 6905 } 6906 6907 // "If offset is zero, and node has an editable ancestor container in 6908 // the same editing host that's an indentation element:" 6909 if (offset == 0 6910 && $_( getAncestors(node).concat(node) ).filter(function(ancestor) { 6911 return isEditable(ancestor) 6912 && inSameEditingHost(ancestor, node) 6913 && isIndentationElement(ancestor); 6914 }).length) { 6915 // "Block-extend the range whose start and end are both (node, 0), 6916 // and let new range be the result." 6917 var newRange = Aloha.createRange(); 6918 newRange.setStart(node, 0); 6919 newRange.setEnd(node, 0); 6920 newRange = blockExtend(newRange); 6921 6922 // "Let node list be a list of nodes, initially empty." 6923 // 6924 // "For each node current node contained in new range, append 6925 // current node to node list if the last member of node list (if 6926 // any) is not an ancestor of current node, and current node is 6927 // editable but has no editable descendants." 6928 var nodeList = getContainedNodes(newRange, function(currentNode) { 6929 return isEditable(currentNode) 6930 && !hasEditableDescendants(currentNode); 6931 }); 6932 6933 // "Outdent each node in node list." 6934 for (var i = 0; i < nodeList.length; i++) { 6935 outdentNode(nodeList[i], range); 6936 } 6937 6938 // "Abort these steps." 6939 return; 6940 } 6941 6942 // "If the child of start node with index start offset is a table, 6943 // abort these steps." 6944 if (isNamedHtmlElement(startNode.childNodes[startOffset], "table")) { 6945 return; 6946 } 6947 6948 // "If start node has a child with index start offset − 1, and that 6949 // child is a table:" 6950 if (0 <= startOffset - 1 6951 && startOffset - 1 < startNode.childNodes.length 6952 && isNamedHtmlElement(startNode.childNodes[startOffset - 1], "table")) { 6953 // "Call collapse(start node, start offset − 1) on the context 6954 // object's Selection." 6955 range.setStart(startNode, startOffset - 1); 6956 6957 // "Call extend(start node, start offset) on the context object's 6958 // Selection." 6959 range.setEnd(startNode, startOffset); 6960 6961 // "Abort these steps." 6962 return; 6963 } 6964 6965 // "If offset is zero; and either the child of start node with index 6966 // start offset minus one is an hr, or the child is a br whose 6967 // previousSibling is either a br or not an inline node:" 6968 if (offset == 0 6969 && (isNamedHtmlElement(startNode.childNodes[startOffset - 1], "hr") 6970 || ( 6971 isNamedHtmlElement(startNode.childNodes[startOffset - 1], "br") 6972 && ( 6973 isNamedHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br") 6974 || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling) 6975 ) 6976 ) 6977 )) { 6978 // "Call collapse(node, offset) on the Selection." 6979 range.setStart(node, offset); 6980 range.setEnd(node, offset); 6981 6982 // "Delete the contents of the range with start (start node, start 6983 // offset − 1) and end (start node, start offset)." 6984 deleteContents(startNode, startOffset - 1, startNode, startOffset); 6985 6986 // "Abort these steps." 6987 return; 6988 } 6989 6990 // "If the child of start node with index start offset is an li or dt 6991 // or dd, and that child's firstChild is an inline node, and start 6992 // offset is not zero:" 6993 if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"]) 6994 && isInlineNode(startNode.childNodes[startOffset].firstChild) 6995 && startOffset != 0) { 6996 // "Let previous item be the child of start node with index start 6997 // offset minus one." 6998 var previousItem = startNode.childNodes[startOffset - 1]; 6999 7000 // "If previous item's lastChild is an inline node other than a br, 7001 // call createElement("br") on the context object and append the 7002 // result as the last child of previous item." 7003 if (isInlineNode(previousItem.lastChild) 7004 && !isNamedHtmlElement(previousItem.lastChild, "br")) { 7005 previousItem.appendChild(document.createElement("br")); 7006 } 7007 7008 // "If previous item's lastChild is an inline node, call 7009 // createElement("br") on the context object and append the result 7010 // as the last child of previous item." 7011 if (isInlineNode(previousItem.lastChild)) { 7012 previousItem.appendChild(document.createElement("br")); 7013 } 7014 } 7015 7016 // "If the child of start node with index start offset is an li or dt 7017 // or dd, and its previousSibling is also an li or dt or dd, set start 7018 // node to its child with index start offset − 1, then set start offset 7019 // to start node's length, then set node to start node's nextSibling, 7020 // then set offset to 0." 7021 if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"]) 7022 && isHtmlElementInArray(startNode.childNodes[startOffset - 1], ["li", "dt", "dd"])) { 7023 startNode = startNode.childNodes[startOffset - 1]; 7024 startOffset = getNodeLength(startNode); 7025 node = startNode.nextSibling; 7026 offset = 0; 7027 7028 // "Otherwise, while start node has a child with index start offset 7029 // minus one:" 7030 } else { 7031 while (0 <= startOffset - 1 7032 && startOffset - 1 < startNode.childNodes.length) { 7033 // "If start node's child with index start offset minus one is 7034 // editable and invisible, remove it from start node, then 7035 // subtract one from start offset." 7036 if (isEditable(startNode.childNodes[startOffset - 1]) 7037 && isInvisible(startNode.childNodes[startOffset - 1])) { 7038 startNode.removeChild(startNode.childNodes[startOffset - 1]); 7039 startOffset--; 7040 7041 // "Otherwise, set start node to its child with index start 7042 // offset minus one, then set start offset to the length of 7043 // start node." 7044 } else { 7045 startNode = startNode.childNodes[startOffset - 1]; 7046 startOffset = getNodeLength(startNode); 7047 } 7048 } 7049 } 7050 7051 // "Delete the contents of the range with start (start node, start 7052 // offset) and end (node, offset)." 7053 var delRange = Aloha.createRange(); 7054 delRange.setStart(startNode, startOffset); 7055 delRange.setEnd(node, offset); 7056 deleteContents(delRange); 7057 7058 if (!isAncestorContainer(document.body, range.startContainer)) { 7059 if (delRange.startContainer.hasChildNodes() || delRange.startContainer.nodeType == $_.Node.TEXT_NODE) { 7060 range.setStart(delRange.startContainer, delRange.startOffset); 7061 range.setEnd(delRange.startContainer, delRange.startOffset); 7062 } else { 7063 range.setStart(delRange.startContainer.parentNode, getNodeIndex(delRange.startContainer)); 7064 range.setEnd(delRange.startContainer.parentNode, getNodeIndex(delRange.startContainer)); 7065 } 7066 } 7067 } 7068 }; 7069 7070 //@} 7071 ///// The formatBlock command ///// 7072 //@{ 7073 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3", 7074 // "h4", "h5", "h6", "p", or "pre"." 7075 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3", 7076 "h4", "h5", "h6", "p", "pre"]; 7077 7078 commands.formatblock = { 7079 action: function(value) { 7080 // "If value begins with a "<" character and ends with a ">" character, 7081 // remove the first and last characters from it." 7082 if (/^<.*>$/.test(value)) { 7083 value = value.slice(1, -1); 7084 } 7085 7086 // "Let value be converted to ASCII lowercase." 7087 value = value.toLowerCase(); 7088 7089 // "If value is not a formattable block name, abort these steps and do 7090 // nothing." 7091 if ($_(formattableBlockNames).indexOf(value) == -1) { 7092 return; 7093 } 7094 7095 // "Block-extend the active range, and let new range be the result." 7096 var newRange = blockExtend(getActiveRange()); 7097 7098 // "Let node list be an empty list of nodes." 7099 // 7100 // "For each node node contained in new range, append node to node list 7101 // if it is editable, the last member of original node list (if any) is 7102 // not an ancestor of node, node is either a non-list single-line 7103 // container or an allowed child of "p" or a dd or dt, and node is not 7104 // the ancestor of a prohibited paragraph child." 7105 var nodeList = getContainedNodes(newRange, function(node) { 7106 return isEditable(node) 7107 && (isNonListSingleLineContainer(node) 7108 || isAllowedChild(node, "p") 7109 || isHtmlElementInArray(node, ["dd", "dt"])) 7110 && !$_( getDescendants(node) ).some(isProhibitedParagraphChild); 7111 }); 7112 7113 // "Record the values of node list, and let values be the result." 7114 var values = recordValues(nodeList); 7115 7116 // "For each node in node list, while node is the descendant of an 7117 // editable HTML element in the same editing host, whose local name is 7118 // a formattable block name, and which is not the ancestor of a 7119 // prohibited paragraph child, split the parent of the one-node list 7120 // consisting of node." 7121 for (var i = 0; i < nodeList.length; i++) { 7122 var node = nodeList[i]; 7123 while ($_( getAncestors(node) ).some(function(ancestor) { 7124 return isEditable(ancestor) 7125 && inSameEditingHost(ancestor, node) 7126 && isHtmlElement_obsolete(ancestor, formattableBlockNames) 7127 && !$_( getDescendants(ancestor) ).some(isProhibitedParagraphChild); 7128 })) { 7129 splitParent([node], range); 7130 } 7131 } 7132 7133 // "Restore the values from values." 7134 restoreValues(values, range); 7135 7136 // "While node list is not empty:" 7137 while (nodeList.length) { 7138 var sublist; 7139 7140 // "If the first member of node list is a single-line 7141 // container:" 7142 if (isSingleLineContainer(nodeList[0])) { 7143 // "Let sublist be the children of the first member of node 7144 // list." 7145 sublist = [].slice.call(toArray(nodeList[0].childNodes)); 7146 7147 // "Record the values of sublist, and let values be the 7148 // result." 7149 var values = recordValues(sublist); 7150 7151 // "Remove the first member of node list from its parent, 7152 // preserving its descendants." 7153 removePreservingDescendants(nodeList[0], range); 7154 7155 // "Restore the values from values." 7156 restoreValues(values, range); 7157 7158 // "Remove the first member from node list." 7159 nodeList.shift(); 7160 7161 // "Otherwise:" 7162 } else { 7163 // "Let sublist be an empty list of nodes." 7164 sublist = []; 7165 7166 // "Remove the first member of node list and append it to 7167 // sublist." 7168 sublist.push(nodeList.shift()); 7169 7170 // "While node list is not empty, and the first member of 7171 // node list is the nextSibling of the last member of 7172 // sublist, and the first member of node list is not a 7173 // single-line container, and the last member of sublist is 7174 // not a br, remove the first member of node list and 7175 // append it to sublist." 7176 while (nodeList.length 7177 && nodeList[0] == sublist[sublist.length - 1].nextSibling 7178 && !isSingleLineContainer(nodeList[0]) 7179 && !isNamedHtmlElement(sublist[sublist.length - 1], "BR")) { 7180 sublist.push(nodeList.shift()); 7181 } 7182 } 7183 7184 // "Wrap sublist. If value is "div" or "p", sibling criteria 7185 // returns false; otherwise it returns true for an HTML element 7186 // with local name value and no attributes, and false otherwise. 7187 // New parent instructions return the result of running 7188 // createElement(value) on the context object. Then fix disallowed 7189 // ancestors of the result." 7190 fixDisallowedAncestors( 7191 wrap(sublist, 7192 jQuery.inArray(value, ["div", "p"]) == - 1 7193 ? function(node) { return isHtmlElement_obsolete(node, value) && !node.attributes.length } 7194 : function() { return false }, 7195 function() { return document.createElement(value) }, 7196 range 7197 ), 7198 range 7199 ); 7200 } 7201 }, indeterm: function() { 7202 // "Block-extend the active range, and let new range be the result." 7203 var newRange = blockExtend(getActiveRange()); 7204 7205 // "Let node list be all visible editable nodes that are contained in 7206 // new range and have no children." 7207 var nodeList = getAllContainedNodes(newRange, function(node) { 7208 return isVisible(node) 7209 && isEditable(node) 7210 && !node.hasChildNodes(); 7211 }); 7212 7213 // "If node list is empty, return false." 7214 if (!nodeList.length) { 7215 return false; 7216 } 7217 7218 // "Let type be null." 7219 var type = null; 7220 7221 // "For each node in node list:" 7222 for (var i = 0; i < nodeList.length; i++) { 7223 var node = nodeList[i]; 7224 7225 // "While node's parent is editable and in the same editing host as 7226 // node, and node is not an HTML element whose local name is a 7227 // formattable block name, set node to its parent." 7228 while (isEditable(node.parentNode) 7229 && inSameEditingHost(node, node.parentNode) 7230 && !isHtmlElement_obsolete(node, formattableBlockNames)) { 7231 node = node.parentNode; 7232 } 7233 7234 // "Let current type be the empty string." 7235 var currentType = ""; 7236 7237 // "If node is an editable HTML element whose local name is a 7238 // formattable block name, and node is not the ancestor of a 7239 // prohibited paragraph child, set current type to node's local 7240 // name." 7241 if (isEditable(node) 7242 && isHtmlElement_obsolete(node, formattableBlockNames) 7243 && !$_( getDescendants(node) ).some(isProhibitedParagraphChild)) { 7244 currentType = node.tagName; 7245 } 7246 7247 // "If type is null, set type to current type." 7248 if (type === null) { 7249 type = currentType; 7250 7251 // "Otherwise, if type does not equal current type, return true." 7252 } else if (type != currentType) { 7253 return true; 7254 } 7255 } 7256 7257 // "Return false." 7258 return false; 7259 }, value: function() { 7260 // "Block-extend the active range, and let new range be the result." 7261 var newRange = blockExtend(getActiveRange()); 7262 7263 // "Let node be the first visible editable node that is contained in 7264 // new range and has no children. If there is no such node, return the 7265 // empty string." 7266 var nodes = getAllContainedNodes(newRange, function(node) { 7267 return isVisible(node) 7268 && isEditable(node) 7269 && !node.hasChildNodes(); 7270 }); 7271 if (!nodes.length) { 7272 return ""; 7273 } 7274 var node = nodes[0]; 7275 7276 // "While node's parent is editable and in the same editing host as 7277 // node, and node is not an HTML element whose local name is a 7278 // formattable block name, set node to its parent." 7279 while (isEditable(node.parentNode) 7280 && inSameEditingHost(node, node.parentNode) 7281 && !isHtmlElement_obsolete(node, formattableBlockNames)) { 7282 node = node.parentNode; 7283 } 7284 7285 // "If node is an editable HTML element whose local name is a 7286 // formattable block name, and node is not the ancestor of a prohibited 7287 // paragraph child, return node's local name, converted to ASCII 7288 // lowercase." 7289 if (isEditable(node) 7290 && isHtmlElement_obsolete(node, formattableBlockNames) 7291 && !$_( getDescendants(node) ).some(isProhibitedParagraphChild)) { 7292 return node.tagName.toLowerCase(); 7293 } 7294 7295 // "Return the empty string." 7296 return ""; 7297 } 7298 }; 7299 7300 //@} 7301 ///// The forwardDelete command ///// 7302 //@{ 7303 commands.forwarddelete = { 7304 action: function(value, range) { 7305 // special behaviour for skipping zero-width whitespaces in IE7 7306 if (jQuery.browser.msie && jQuery.browser.version <= 7) { 7307 moveOverZWSP(range, true); 7308 } 7309 7310 // "If the active range is not collapsed, delete the contents of the 7311 // active range and abort these steps." 7312 if (!range.collapsed) { 7313 deleteContents(range); 7314 return; 7315 } 7316 7317 // "Canonicalize whitespace at (active range's start node, active 7318 // range's start offset)." 7319 canonicalizeWhitespace(range.startContainer, range.startOffset); 7320 7321 // "Let node and offset be the active range's start node and offset." 7322 var node = range.startContainer; 7323 var offset = range.startOffset; 7324 var isBr = false; 7325 var isHr = false; 7326 7327 // "Repeat the following steps:" 7328 while (true) { 7329 // check whether the next element is a br or hr 7330 if ( offset < node.childNodes.length ) { 7331 // isBr = isHtmlElement_obsolete(node.childNodes[offset], "br") || false; 7332 // isHr = isHtmlElement_obsolete(node.childNodes[offset], "hr") || false; 7333 } 7334 7335 // "If offset is the length of node and node's nextSibling is an 7336 // editable invisible node, remove node's nextSibling from its 7337 // parent." 7338 if (offset == getNodeLength(node) 7339 && isEditable(node.nextSibling) 7340 && isInvisible(node.nextSibling)) { 7341 node.parentNode.removeChild(node.nextSibling); 7342 7343 // "Otherwise, if node has a child with index offset and that child 7344 // is an editable invisible node, remove that child from node." 7345 } else if (offset < node.childNodes.length 7346 && isEditable(node.childNodes[offset]) 7347 && (isInvisible(node.childNodes[offset]) || isBr || isHr )) { 7348 node.removeChild(node.childNodes[offset]); 7349 if (isBr || isHr) { 7350 ensureContainerEditable(node); 7351 range.setStart(node, offset); 7352 range.setEnd(node, offset); 7353 return; 7354 } 7355 7356 // "Otherwise, if node has a child with index offset and that child 7357 // is a collapsed block prop, add one to offset." 7358 } else if (offset < node.childNodes.length 7359 && isCollapsedBlockProp(node.childNodes[offset])) { 7360 offset++; 7361 7362 // "Otherwise, if offset is the length of node and node is an 7363 // inline node, or if node is invisible, set offset to one plus the 7364 // index of node, then set node to its parent." 7365 } else if ((offset == getNodeLength(node) 7366 && isInlineNode(node)) 7367 || isInvisible(node)) { 7368 offset = 1 + getNodeIndex(node); 7369 node = node.parentNode; 7370 7371 // "Otherwise, if node has a child with index offset and that child 7372 // is not a block node or a br or an img, set node to that child, 7373 // then set offset to zero." 7374 } else if (offset < node.childNodes.length 7375 && !isBlockNode(node.childNodes[offset]) 7376 && !isHtmlElementInArray(node.childNodes[offset], ["br", "img"])) { 7377 node = node.childNodes[offset]; 7378 offset = 0; 7379 7380 // "Otherwise, break from this loop." 7381 } else { 7382 break; 7383 } 7384 } 7385 7386 // collapse whitespace in the node, if it is a text node 7387 canonicalizeWhitespace(range.startContainer, range.startOffset); 7388 7389 // if the next node is an aloha-table we want to delete it 7390 var delBlock; 7391 if (delBlock = getBlockAtNextPosition(node, offset)) { 7392 delBlock.parentNode.removeChild(delBlock); 7393 return; 7394 } 7395 7396 // "If node is a Text node and offset is not node's length:" 7397 if (node.nodeType == $_.Node.TEXT_NODE 7398 && offset != getNodeLength(node)) { 7399 // "Call collapse(node, offset) on the Selection." 7400 range.setStart(node, offset); 7401 range.setEnd(node, offset); 7402 7403 // "Let end offset be offset plus one." 7404 var endOffset = offset + 1; 7405 7406 // "While end offset is not node's length and the end offsetth 7407 // element of node's data has general category M when interpreted 7408 // as a Unicode code point, add one to end offset." 7409 // 7410 // TODO: Not even going to try handling anything beyond the most 7411 // basic combining marks, since I couldn't find a good list. I 7412 // special-case a few Hebrew diacritics too to test basic coverage 7413 // of non-Latin stuff. 7414 while (endOffset != node.length 7415 && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) { 7416 endOffset++; 7417 } 7418 7419 // "Delete the contents of the range with start (node, offset) and 7420 // end (node, end offset)." 7421 deleteContents(node, offset, node, endOffset); 7422 7423 // "Abort these steps." 7424 return; 7425 } 7426 7427 // "If node is an inline node, abort these steps." 7428 if (isInlineNode(node)) { 7429 return; 7430 } 7431 7432 // "If node has a child with index offset and that child is a br or hr 7433 // or img, call collapse(node, offset) on the Selection. Then delete 7434 // the contents of the range with start (node, offset) and end (node, 7435 // offset + 1) and abort these steps." 7436 if (offset < node.childNodes.length 7437 && isHtmlElementInArray(node.childNodes[offset], ["br", "hr", "img"])) { 7438 range.setStart(node, offset); 7439 range.setEnd(node, offset); 7440 deleteContents(node, offset, node, offset + 1); 7441 return; 7442 } 7443 7444 // "Let end node equal node and let end offset equal offset." 7445 var endNode = node; 7446 var endOffset = offset; 7447 7448 // "Repeat the following steps:" 7449 while (true) { 7450 // "If end offset is the length of end node, set end offset to one 7451 // plus the index of end node and then set end node to its parent." 7452 if (endOffset == getNodeLength(endNode)) { 7453 endOffset = 1 + getNodeIndex(endNode); 7454 endNode = endNode.parentNode; 7455 7456 // "Otherwise, if end node has a an editable invisible child with 7457 // index end offset, remove it from end node." 7458 } else if (endOffset < endNode.childNodes.length 7459 && isEditable(endNode.childNodes[endOffset]) 7460 && isInvisible(endNode.childNodes[endOffset])) { 7461 endNode.removeChild(endNode.childNodes[endOffset]); 7462 7463 // "Otherwise, break from this loop." 7464 } else { 7465 break; 7466 } 7467 } 7468 7469 // "If the child of end node with index end offset minus one is a 7470 // table, abort these steps." 7471 if (isNamedHtmlElement(endNode.childNodes[endOffset - 1], "table")) { 7472 return; 7473 } 7474 7475 // "If the child of end node with index end offset is a table:" 7476 if (isNamedHtmlElement(endNode.childNodes[endOffset], "table")) { 7477 // "Call collapse(end node, end offset) on the context object's 7478 // Selection." 7479 range.setStart(endNode, endOffset); 7480 7481 // "Call extend(end node, end offset + 1) on the context object's 7482 // Selection." 7483 range.setEnd(endNode, endOffset + 1); 7484 7485 // "Abort these steps." 7486 return; 7487 } 7488 7489 // "If offset is the length of node, and the child of end node with 7490 // index end offset is an hr or br:" 7491 if (offset == getNodeLength(node) 7492 && isHtmlElementInArray(endNode.childNodes[endOffset], ["br", "hr"])) { 7493 // "Call collapse(node, offset) on the Selection." 7494 range.setStart(node, offset); 7495 range.setEnd(node, offset); 7496 7497 // "Delete the contents of the range with end (end node, end 7498 // offset) and end (end node, end offset + 1)." 7499 deleteContents(endNode, endOffset, endNode, endOffset + 1); 7500 7501 // "Abort these steps." 7502 return; 7503 } 7504 7505 // "While end node has a child with index end offset:" 7506 while (endOffset < endNode.childNodes.length) { 7507 // "If end node's child with index end offset is editable and 7508 // invisible, remove it from end node." 7509 if (isEditable(endNode.childNodes[endOffset]) 7510 && isInvisible(endNode.childNodes[endOffset])) { 7511 endNode.removeChild(endNode.childNodes[endOffset]); 7512 7513 // "Otherwise, set end node to its child with index end offset and 7514 // set end offset to zero." 7515 } else { 7516 endNode = endNode.childNodes[endOffset]; 7517 endOffset = 0; 7518 } 7519 } 7520 7521 // "Delete the contents of the range with start (node, offset) and end 7522 // (end node, end offset)." 7523 deleteContents(node, offset, endNode, endOffset); 7524 } 7525 }; 7526 7527 //@} 7528 ///// The indent command ///// 7529 //@{ 7530 commands.indent = { 7531 action: function() { 7532 // "Let items be a list of all lis that are ancestor containers of the 7533 // active range's start and/or end node." 7534 // 7535 // Has to be in tree order, remember! 7536 var items = []; 7537 for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { 7538 if (isNamedHtmlElement(node, "LI")) { 7539 items.unshift(node); 7540 } 7541 } 7542 for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { 7543 if (isNamedHtmlElement(node, "LI")) { 7544 items.unshift(node); 7545 } 7546 } 7547 for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) { 7548 if (isNamedHtmlElement(node, "LI")) { 7549 items.unshift(node); 7550 } 7551 } 7552 7553 // "For each item in items, normalize sublists of item." 7554 for (var i = 0; i < items.length; i++) { 7555 normalizeSublists(items[i, range]); 7556 } 7557 7558 // "Block-extend the active range, and let new range be the result." 7559 var newRange = blockExtend(getActiveRange()); 7560 7561 // "Let node list be a list of nodes, initially empty." 7562 var nodeList = []; 7563 7564 // "For each node node contained in new range, if node is editable and 7565 // is an allowed child of "div" or "ol" and if the last member of node 7566 // list (if any) is not an ancestor of node, append node to node list." 7567 nodeList = getContainedNodes(newRange, function(node) { 7568 return isEditable(node) 7569 && (isAllowedChild(node, "div") 7570 || isAllowedChild(node, "ol")); 7571 }); 7572 7573 // "If the first member of node list is an li whose parent is an ol or 7574 // ul, and its previousSibling is an li as well, normalize sublists of 7575 // its previousSibling." 7576 if (nodeList.length 7577 && isNamedHtmlElement(nodeList[0], "LI") 7578 && isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"]) 7579 && isNamedHtmlElement(nodeList[0].previousSibling, "LI")) { 7580 normalizeSublists(nodeList[0].previousSibling, range); 7581 } 7582 7583 // "While node list is not empty:" 7584 while (nodeList.length) { 7585 // "Let sublist be a list of nodes, initially empty." 7586 var sublist = []; 7587 7588 // "Remove the first member of node list and append it to sublist." 7589 sublist.push(nodeList.shift()); 7590 7591 // "While the first member of node list is the nextSibling of the 7592 // last member of sublist, remove the first member of node list and 7593 // append it to sublist." 7594 while (nodeList.length 7595 && nodeList[0] == sublist[sublist.length - 1].nextSibling) { 7596 sublist.push(nodeList.shift()); 7597 } 7598 7599 // "Indent sublist." 7600 indentNodes(sublist, range); 7601 } 7602 } 7603 }; 7604 7605 //@} 7606 ///// The insertHorizontalRule command ///// 7607 //@{ 7608 commands.inserthorizontalrule = { 7609 action: function(value, range) { 7610 7611 // "While range's start offset is 0 and its start node's parent is not 7612 // null, set range's start to (parent of start node, index of start 7613 // node)." 7614 while (range.startOffset == 0 7615 && range.startContainer.parentNode) { 7616 range.setStart(range.startContainer.parentNode, getNodeIndex(range.startContainer)); 7617 } 7618 7619 // "While range's end offset is the length of its end node, and its end 7620 // node's parent is not null, set range's end to (parent of end node, 1 7621 // + index of start node)." 7622 while (range.endOffset == getNodeLength(range.endContainer) 7623 && range.endContainer.parentNode) { 7624 range.setEnd(range.endContainer.parentNode, 1 + getNodeIndex(range.endContainer)); 7625 } 7626 7627 // "Delete the contents of range, with block merging false." 7628 deleteContents(range, {blockMerging: false}); 7629 7630 // "If the active range's start node is neither editable nor an editing 7631 // host, abort these steps." 7632 if (!isEditable(getActiveRange().startContainer) 7633 && !isEditingHost(getActiveRange().startContainer)) { 7634 return; 7635 } 7636 7637 // "If the active range's start node is a Text node and its start 7638 // offset is zero, set the active range's start and end to (parent of 7639 // start node, index of start node)." 7640 if (getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE 7641 && getActiveRange().startOffset == 0) { 7642 getActiveRange().setStart(getActiveRange().startContainer.parentNode, getNodeIndex(getActiveRange().startContainer)); 7643 getActiveRange().collapse(true); 7644 } 7645 7646 // "If the active range's start node is a Text node and its start 7647 // offset is the length of its start node, set the active range's start 7648 // and end to (parent of start node, 1 + index of start node)." 7649 if (getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE 7650 && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { 7651 getActiveRange().setStart(getActiveRange().startContainer.parentNode, 1 + getNodeIndex(getActiveRange().startContainer)); 7652 getActiveRange().collapse(true); 7653 } 7654 7655 // "Let hr be the result of calling createElement("hr") on the 7656 // context object." 7657 var hr = document.createElement("hr"); 7658 7659 // "Run insertNode(hr) on the range." 7660 range.insertNode(hr); 7661 7662 // "Fix disallowed ancestors of hr." 7663 fixDisallowedAncestors(hr, range); 7664 7665 // "Run collapse() on the Selection, with first argument equal to the 7666 // parent of hr and the second argument equal to one plus the index of 7667 // hr." 7668 // 7669 // Not everyone actually supports collapse(), so we do it manually 7670 // instead. Also, we need to modify the actual range we're given as 7671 // well, for the sake of autoimplementation.html's range-filling-in. 7672 range.setStart(hr.parentNode, 1 + getNodeIndex(hr)); 7673 range.setEnd(hr.parentNode, 1 + getNodeIndex(hr)); 7674 Aloha.getSelection().removeAllRanges(); 7675 Aloha.getSelection().addRange(range); 7676 } 7677 }; 7678 7679 //@} 7680 ///// The insertHTML command ///// 7681 //@{ 7682 commands.inserthtml = { 7683 action: function(value, range) { 7684 7685 7686 // "Delete the contents of the active range." 7687 deleteContents(range); 7688 7689 // "If the active range's start node is neither editable nor an editing 7690 // host, abort these steps." 7691 if (!isEditable(range.startContainer) 7692 && !isEditingHost(range.startContainer)) { 7693 return; 7694 } 7695 7696 // "Let frag be the result of calling createContextualFragment(value) 7697 // on the active range." 7698 var frag = range.createContextualFragment(value); 7699 7700 // "Let last child be the lastChild of frag." 7701 var lastChild = frag.lastChild; 7702 7703 // "If last child is null, abort these steps." 7704 if (!lastChild) { 7705 return; 7706 } 7707 7708 // "Let descendants be all descendants of frag." 7709 var descendants = getDescendants(frag); 7710 7711 // "If the active range's start node is a block node:" 7712 if (isBlockNode(range.startContainer)) { 7713 // "Let collapsed block props be all editable collapsed block prop 7714 // children of the active range's start node that have index 7715 // greater than or equal to the active range's start offset." 7716 // 7717 // "For each node in collapsed block props, remove node from its 7718 // parent." 7719 $_(range.startContainer.childNodes).filter(function(node, range) { 7720 return isEditable(node) 7721 && isCollapsedBlockProp(node) 7722 && getNodeIndex(node) >= range.startOffset; 7723 }, true).forEach(function(node) { 7724 node.parentNode.removeChild(node); 7725 }); 7726 } 7727 7728 // "Call insertNode(frag) on the active range." 7729 range.insertNode(frag); 7730 7731 // "If the active range's start node is a block node with no visible 7732 // children, call createElement("br") on the context object and append 7733 // the result as the last child of the active range's start node." 7734 if (isBlockNode(range.startContainer)) { 7735 ensureContainerEditable(range.startContainer); 7736 } 7737 7738 // "Call collapse() on the context object's Selection, with last 7739 // child's parent as the first argument and one plus its index as the 7740 // second." 7741 range.setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild)); 7742 range.setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild)); 7743 7744 // "Fix disallowed ancestors of each member of descendants." 7745 for (var i = 0; i < descendants.length; i++) { 7746 fixDisallowedAncestors(descendants[i], range); 7747 } 7748 7749 setActiveRange( range ); 7750 } 7751 }; 7752 7753 //@} 7754 ///// The insertImage command ///// 7755 //@{ 7756 commands.insertimage = { 7757 action: function(value) { 7758 // "If value is the empty string, abort these steps and do nothing." 7759 if (value === "") { 7760 return; 7761 } 7762 7763 // "Let range be the active range." 7764 var range = getActiveRange(); 7765 7766 // "Delete the contents of range, with strip wrappers false." 7767 deleteContents(range, {stripWrappers: false}); 7768 7769 // "If the active range's start node is neither editable nor an editing 7770 // host, abort these steps." 7771 if (!isEditable(getActiveRange().startContainer) 7772 && !isEditingHost(getActiveRange().startContainer)) { 7773 return; 7774 } 7775 7776 // "If range's start node is a block node whose sole child is a br, and 7777 // its start offset is 0, remove its start node's child from it." 7778 if (isBlockNode(range.startContainer) 7779 && range.startContainer.childNodes.length == 1 7780 && isNamedHtmlElement(range.startContainer.firstChild, "br") 7781 && range.startOffset == 0) { 7782 range.startContainer.removeChild(range.startContainer.firstChild); 7783 } 7784 7785 // "Let img be the result of calling createElement("img") on the 7786 // context object." 7787 var img = document.createElement("img"); 7788 7789 // "Run setAttribute("src", value) on img." 7790 img.setAttribute("src", value); 7791 7792 // "Run insertNode(img) on the range." 7793 range.insertNode(img); 7794 7795 // "Run collapse() on the Selection, with first argument equal to the 7796 // parent of img and the second argument equal to one plus the index of 7797 // img." 7798 // 7799 // Not everyone actually supports collapse(), so we do it manually 7800 // instead. Also, we need to modify the actual range we're given as 7801 // well, for the sake of autoimplementation.html's range-filling-in. 7802 range.setStart(img.parentNode, 1 + getNodeIndex(img)); 7803 range.setEnd(img.parentNode, 1 + getNodeIndex(img)); 7804 Aloha.getSelection().removeAllRanges(); 7805 Aloha.getSelection().addRange(range); 7806 7807 // IE adds width and height attributes for some reason, so remove those 7808 // to actually do what the spec says. 7809 img.removeAttribute("width"); 7810 img.removeAttribute("height"); 7811 } 7812 }; 7813 7814 //@} 7815 ///// The insertLineBreak command ///// 7816 //@{ 7817 commands.insertlinebreak = { 7818 action: function(value, range) { 7819 // "Delete the contents of the active range, with strip wrappers false." 7820 deleteContents(range, {stripWrappers: false}); 7821 7822 // "If the active range's start node is neither editable nor an editing 7823 // host, abort these steps." 7824 if (!isEditable(range.startContainer) 7825 && !isEditingHost(range.startContainer)) { 7826 return; 7827 } 7828 7829 // "If the active range's start node is an Element, and "br" is not an 7830 // allowed child of it, abort these steps." 7831 if (range.startContainer.nodeType == $_.Node.ELEMENT_NODE 7832 && !isAllowedChild("br", range.startContainer)) { 7833 return; 7834 7835 } 7836 7837 // "If the active range's start node is not an Element, and "br" is not 7838 // an allowed child of the active range's start node's parent, abort 7839 // these steps." 7840 if (range.startContainer.nodeType != $_.Node.ELEMENT_NODE 7841 && !isAllowedChild("br", range.startContainer.parentNode)) { 7842 return; 7843 } 7844 7845 // "If the active range's start node is a Text node and its start 7846 // offset is zero, call collapse() on the context object's Selection, 7847 // with first argument equal to the active range's start node's parent 7848 // and second argument equal to the active range's start node's index." 7849 if (range.startContainer.nodeType == $_.Node.TEXT_NODE 7850 && range.startOffset == 0) { 7851 var newNode = range.startContainer.parentNode; 7852 var newOffset = getNodeIndex(range.startContainer); 7853 Aloha.getSelection().collapse(newNode, newOffset); 7854 range.setStart(newNode, newOffset); 7855 range.setEnd(newNode, newOffset); 7856 } 7857 7858 // "If the active range's start node is a Text node and its start 7859 // offset is the length of its start node, call collapse() on the 7860 // context object's Selection, with first argument equal to the active 7861 // range's start node's parent and second argument equal to one plus 7862 // the active range's start node's index." 7863 if (range.startContainer.nodeType == $_.Node.TEXT_NODE 7864 && range.startOffset == getNodeLength(range.startContainer)) { 7865 var newNode = range.startContainer.parentNode; 7866 var newOffset = 1 + getNodeIndex(range.startContainer); 7867 Aloha.getSelection().collapse(newNode, newOffset); 7868 range.setStart(newNode, newOffset); 7869 range.setEnd(newNode, newOffset); 7870 } 7871 7872 // "Let br be the result of calling createElement("br") on the context 7873 // object." 7874 var br = document.createElement("br"); 7875 7876 // "Call insertNode(br) on the active range." 7877 range.insertNode(br); 7878 7879 // "Call collapse() on the context object's Selection, with br's parent 7880 // as the first argument and one plus br's index as the second 7881 // argument." 7882 Aloha.getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); 7883 range.setStart(br.parentNode, 1 + getNodeIndex(br)); 7884 range.setEnd(br.parentNode, 1 + getNodeIndex(br)); 7885 7886 // "If br is a collapsed line break, call createElement("br") on the 7887 // context object and let extra br be the result, then call 7888 // insertNode(extra br) on the active range." 7889 if (isCollapsedLineBreak(br)) { 7890 // TODO 7891 range.insertNode(createEndBreak()); 7892 7893 // Compensate for nonstandard implementations of insertNode 7894 Aloha.getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); 7895 range.setStart(br.parentNode, 1 + getNodeIndex(br)); 7896 range.setEnd(br.parentNode, 1 + getNodeIndex(br)); 7897 } 7898 7899 // IE7 is adding this styles: height: auto; min-height: 0px; max-height: none; 7900 // with that there is the ugly "IE-editable-outline" 7901 if (jQuery.browser.msie && jQuery.browser.version < 8) { 7902 br.parentNode.removeAttribute("style"); 7903 } 7904 } 7905 }; 7906 7907 //@} 7908 ///// The insertOrderedList command ///// 7909 //@{ 7910 commands.insertorderedlist = { 7911 // "Toggle lists with tag name "ol"." 7912 action: function() { toggleLists("ol") }, 7913 // "True if the selection's list state is "mixed" or "mixed ol", false 7914 // otherwise." 7915 indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) }, 7916 // "True if the selection's list state is "ol", false otherwise." 7917 state: function() { return getSelectionListState() == "ol" } 7918 }; 7919 7920 var listRelatedElements = {"LI": true, "DT": true, "DD": true}; 7921 7922 //@} 7923 ///// The insertParagraph command ///// 7924 //@{ 7925 commands.insertparagraph = { 7926 action: function(value, range) { 7927 7928 // "Delete the contents of the active range." 7929 deleteContents(range); 7930 7931 // clean lists in the editing host, this will remove any whitespace nodes around lists 7932 // because the following algorithm is not prepared to deal with them 7933 cleanLists(getEditingHostOf(range.startContainer), range); 7934 7935 // "If the active range's start node is neither editable nor an editing 7936 // host, abort these steps." 7937 if (!isEditable(range.startContainer) 7938 && !isEditingHost(range.startContainer)) { 7939 return; 7940 } 7941 7942 // "Let node and offset be the active range's start node and offset." 7943 var node = range.startContainer; 7944 var offset = range.startOffset; 7945 7946 // "If node is a Text node, and offset is neither 0 nor the length of 7947 // node, call splitText(offset) on node." 7948 if (node.nodeType == $_.Node.TEXT_NODE 7949 && offset != 0 7950 && offset != getNodeLength(node)) { 7951 node.splitText(offset); 7952 } 7953 7954 // "If node is a Text node and offset is its length, set offset to one 7955 // plus the index of node, then set node to its parent." 7956 if (node.nodeType == $_.Node.TEXT_NODE 7957 && offset == getNodeLength(node)) { 7958 offset = 1 + getNodeIndex(node); 7959 node = node.parentNode; 7960 } 7961 7962 // "If node is a Text or Comment node, set offset to the index of node, 7963 // then set node to its parent." 7964 if (node.nodeType == $_.Node.TEXT_NODE 7965 || node.nodeType == $_.Node.COMMENT_NODE) { 7966 offset = getNodeIndex(node); 7967 node = node.parentNode; 7968 } 7969 7970 // "Call collapse(node, offset) on the context object's Selection." 7971 Aloha.getSelection().collapse(node, offset); 7972 range.setStart(node, offset); 7973 range.setEnd(node, offset); 7974 7975 // "Let container equal node." 7976 var container = node; 7977 7978 // "While container is not a single-line container, and container's 7979 // parent is editable and in the same editing host as node, set 7980 // container to its parent." 7981 while (!isSingleLineContainer(container) 7982 && isEditable(container.parentNode) 7983 && inSameEditingHost(node, container.parentNode)) { 7984 container = container.parentNode; 7985 } 7986 7987 // "If container is not editable or not in the same editing host as 7988 // node or is not a single-line container:" 7989 if (!isEditable(container) 7990 || !inSameEditingHost(container, node) 7991 || !isSingleLineContainer(container)) { 7992 // "Let tag be the default single-line container name." 7993 var tag = defaultSingleLineContainerName; 7994 7995 // "Block-extend the active range, and let new range be the 7996 // result." 7997 var newRange = blockExtend(range); 7998 7999 // "Let node list be a list of nodes, initially empty." 8000 // 8001 // "Append to node list the first node in tree order that is 8002 // contained in new range and is an allowed child of "p", if any." 8003 var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") }) 8004 .slice(0, 1); 8005 8006 // "If node list is empty:" 8007 if (!nodeList.length) { 8008 // "If tag is not an allowed child of the active range's start 8009 // node, abort these steps." 8010 if (!isAllowedChild(tag, range.startContainer)) { 8011 return; 8012 } 8013 8014 // "Set container to the result of calling createElement(tag) 8015 // on the context object." 8016 container = document.createElement(tag); 8017 8018 // "Call insertNode(container) on the active range." 8019 range.insertNode(container); 8020 8021 // "Call createElement("br") on the context object, and append 8022 // the result as the last child of container." 8023 // TODO not always 8024 container.appendChild(createEndBreak()); 8025 8026 // "Call collapse(container, 0) on the context object's 8027 // Selection." 8028 // TODO: remove selection from command 8029 Aloha.getSelection().collapse(container, 0); 8030 range.setStart(container, 0); 8031 range.setEnd(container, 0); 8032 8033 // "Abort these steps." 8034 return; 8035 } 8036 8037 // "While the nextSibling of the last member of node list is not 8038 // null and is an allowed child of "p", append it to node list." 8039 while (nodeList[nodeList.length - 1].nextSibling 8040 && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) { 8041 nodeList.push(nodeList[nodeList.length - 1].nextSibling); 8042 } 8043 8044 // "Wrap node list, with sibling criteria returning false and new 8045 // parent instructions returning the result of calling 8046 // createElement(tag) on the context object. Set container to the 8047 8048 // result." 8049 container = wrap(nodeList, 8050 function() { return false }, 8051 function() { return document.createElement(tag) }, 8052 range 8053 ); 8054 } 8055 8056 // "If container's local name is "address", "listing", or "pre":" 8057 if (container.tagName == "ADDRESS" 8058 || container.tagName == "LISTING" 8059 || container.tagName == "PRE") { 8060 // "Let br be the result of calling createElement("br") on the 8061 // context object." 8062 var br = document.createElement("br"); 8063 8064 // remember the old height 8065 var oldHeight = container.offsetHeight; 8066 8067 // "Call insertNode(br) on the active range." 8068 range.insertNode(br); 8069 8070 // determine the new height 8071 var newHeight = container.offsetHeight; 8072 8073 // "Call collapse(node, offset + 1) on the context object's 8074 // Selection." 8075 Aloha.getSelection().collapse(node, offset + 1); 8076 range.setStart(node, offset + 1); 8077 range.setEnd(node, offset + 1); 8078 8079 // "If br is the last descendant of container, let br be the result 8080 // of calling createElement("br") on the context object, then call 8081 // insertNode(br) on the active range." (Fix: only do this, if the container height did not change by inserting a single <br/>) 8082 // 8083 // Work around browser bugs: some browsers select the 8084 // newly-inserted node, not per spec. 8085 if (oldHeight == newHeight && !isDescendant(nextNode(br), container)) { 8086 // TODO check 8087 range.insertNode(createEndBreak()); 8088 Aloha.getSelection().collapse(node, offset + 1); 8089 range.setEnd(node, offset + 1); 8090 } 8091 8092 // "Abort these steps." 8093 return; 8094 } 8095 8096 // "If container's local name is "li", "dt", or "dd"; and either it has 8097 // no children or it has a single child and that child is a br:" 8098 if (listRelatedElements[container.tagName] 8099 && (!container.hasChildNodes() 8100 || (container.childNodes.length == 1 8101 && isNamedHtmlElement(container.firstChild, "br")))) { 8102 // "Split the parent of the one-node list consisting of container." 8103 splitParent([container], range); 8104 8105 // "If container has no children, call createElement("br") on the 8106 // context object and append the result as the last child of 8107 // container." 8108 // only do this, if inserting the br does NOT modify the offset height of the container 8109 // if (!container.hasChildNodes()) { 8110 // var oldHeight = container.offsetHeight, endBr = createEndBreak(); 8111 // container.appendChild(endBr); 8112 // if (container.offsetHeight !== oldHeight) { 8113 // container.removeChild(endBr); 8114 // } 8115 // } 8116 8117 // "If container is a dd or dt, and it is not an allowed child of 8118 // any of its ancestors in the same editing host, set the tag name 8119 // of container to the default single-line container name and let 8120 // container be the result." 8121 if (isHtmlElementInArray(container, ["dd", "dt"]) 8122 && $_( getAncestors(container) ).every(function(ancestor) { 8123 return !inSameEditingHost(container, ancestor) 8124 || !isAllowedChild(container, ancestor) 8125 })) { 8126 container = setTagName(container, defaultSingleLineContainerName, range); 8127 } 8128 8129 // "Fix disallowed ancestors of container." 8130 fixDisallowedAncestors(container, range); 8131 8132 // fix invalid nested lists 8133 if (isNamedHtmlElement(container, 'li') 8134 && isNamedHtmlElement(container.nextSibling, "li") 8135 && isHtmlElementInArray(container.nextSibling.firstChild, ["ol", "ul"])) { 8136 // we found a li containing only a br followed by a li containing a list as first element: merge the two li's 8137 var listParent = container.nextSibling, length = container.nextSibling.childNodes.length; 8138 for (var i = 0; i < length; i++) { 8139 // we always move the first child into the container 8140 container.appendChild(listParent.childNodes[0]); 8141 } 8142 listParent.parentNode.removeChild(listParent); 8143 } 8144 8145 // "Abort these steps." 8146 return; 8147 } 8148 8149 // "Let new line range be a new range whose start is the same as 8150 // the active range's, and whose end is (container, length of 8151 // container)." 8152 var newLineRange = Aloha.createRange(); 8153 newLineRange.setStart(range.startContainer, range.startOffset); 8154 newLineRange.setEnd(container, getNodeLength(container)); 8155 8156 // "While new line range's start offset is zero and its start node is 8157 // not container, set its start to (parent of start node, index of 8158 // start node)." 8159 while (newLineRange.startOffset == 0 8160 && newLineRange.startContainer != container) { 8161 newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer)); 8162 } 8163 8164 // "While new line range's start offset is the length of its start node 8165 // and its start node is not container, set its start to (parent of 8166 // start node, 1 + index of start node)." 8167 while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer) 8168 && newLineRange.startContainer != container) { 8169 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer)); 8170 } 8171 8172 // "Let end of line be true if new line range contains either nothing 8173 // or a single br, and false otherwise." 8174 var containedInNewLineRange = getContainedNodes(newLineRange); 8175 var endOfLine = !containedInNewLineRange.length 8176 || (containedInNewLineRange.length == 1 8177 && isNamedHtmlElement(containedInNewLineRange[0], "br")); 8178 8179 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or 8180 // "h6", and end of line is true, let new container name be the default 8181 // single-line container name." 8182 var newContainerName; 8183 if (/^H[1-6]$/.test(container.tagName) 8184 && endOfLine) { 8185 newContainerName = defaultSingleLineContainerName; 8186 8187 // "Otherwise, if the local name of container is "dt" and end of line 8188 // is true, let new container name be "dd"." 8189 } else if (container.tagName == "DT" 8190 && endOfLine) { 8191 newContainerName = "dd"; 8192 8193 // "Otherwise, if the local name of container is "dd" and end of line 8194 // is true, let new container name be "dt"." 8195 } else if (container.tagName == "DD" 8196 && endOfLine) { 8197 newContainerName = "dt"; 8198 8199 // "Otherwise, let new container name be the local name of container." 8200 } else { 8201 newContainerName = container.tagName.toLowerCase(); 8202 } 8203 8204 // "Let new container be the result of calling createElement(new 8205 // container name) on the context object." 8206 var newContainer = document.createElement(newContainerName); 8207 8208 // "Copy all non empty attributes of the container to new container." 8209 copyAttributes( container, newContainer ); 8210 8211 // "If new container has an id attribute, unset it." 8212 newContainer.removeAttribute("id"); 8213 8214 // "Insert new container into the parent of container immediately after 8215 // container." 8216 container.parentNode.insertBefore(newContainer, container.nextSibling); 8217 8218 // "Let contained nodes be all nodes contained in new line range." 8219 var containedNodes = getAllContainedNodes(newLineRange); 8220 8221 // "Let frag be the result of calling extractContents() on new line 8222 // range." 8223 var frag = newLineRange.extractContents(); 8224 8225 // "Unset the id attribute (if any) of each Element descendant of frag 8226 // that is not in contained nodes." 8227 var descendants = getDescendants(frag); 8228 for (var i = 0; i < descendants.length; i++) { 8229 if (descendants[i].nodeType == $_.Node.ELEMENT_NODE 8230 && $_(containedNodes).indexOf(descendants[i]) == -1) { 8231 descendants[i].removeAttribute("id"); 8232 } 8233 } 8234 8235 var fragChildren = [], fragChild = frag.firstChild; 8236 if (fragChild) { 8237 do { 8238 if (!isWhitespaceNode(fragChild)) { 8239 fragChildren.push(fragChild); 8240 } 8241 } while(fragChild = fragChild.nextSibling); 8242 } 8243 8244 // if newContainer is a li and frag contains only a list, we add a br in the li (but only if the height would not change) 8245 if (isNamedHtmlElement(newContainer, 'li') && fragChildren.length && isHtmlElementInArray(fragChildren[0], ["ul", "ol"])) { 8246 var oldHeight = newContainer.offsetHeight; 8247 var endBr = createEndBreak(); 8248 newContainer.appendChild(endBr); 8249 var newHeight = newContainer.offsetHeight; 8250 if (oldHeight !== newHeight) { 8251 newContainer.removeChild(endBr); 8252 } 8253 } 8254 8255 // "Call appendChild(frag) on new container." 8256 newContainer.appendChild(frag); 8257 8258 // "If container has no visible children, call createElement("br") on 8259 // the context object, and append the result as the last child of 8260 // container." 8261 ensureContainerEditable(container); 8262 8263 // "If new container has no visible children, call createElement("br") 8264 // on the context object, and append the result as the last child of 8265 // new container." 8266 ensureContainerEditable(newContainer); 8267 8268 // "Call collapse(new container, 0) on the context object's Selection." 8269 Aloha.getSelection().collapse(newContainer, 0); 8270 range.setStart(newContainer, 0); 8271 range.setEnd(newContainer, 0); 8272 } 8273 }; 8274 8275 //@} 8276 ///// The insertText command ///// 8277 //@{ 8278 commands.inserttext = { 8279 action: function(value, range) { 8280 // "Delete the contents of the active range, with strip wrappers 8281 // false." 8282 deleteContents(range, {stripWrappers: false}); 8283 8284 // "If the active range's start node is neither editable nor an editing 8285 // host, abort these steps." 8286 if (!isEditable(range.startContainer) 8287 && !isEditingHost(range.startContainer)) { 8288 return; 8289 } 8290 8291 // "If value's length is greater than one:" 8292 if (value.length > 1) { 8293 // "For each element el in value, take the action for the 8294 // insertText command, with value equal to el." 8295 for (var i = 0; i < value.length; i++) { 8296 commands.inserttext.action( value[i], range ); 8297 } 8298 8299 // "Abort these steps." 8300 return; 8301 } 8302 8303 // "If value is the empty string, abort these steps." 8304 if (value == "") { 8305 return; 8306 } 8307 8308 // "If value is a newline (U+00A0), take the action for the 8309 // insertParagraph command and abort these steps." 8310 if (value == "\n") { 8311 commands.insertparagraph.action( '', range ); 8312 return; 8313 } 8314 8315 // "Let node and offset be the active range's start node and offset." 8316 var node = range.startContainer; 8317 var offset = range.startOffset; 8318 8319 // "If node has a child whose index is offset − 1, and that child is a 8320 // Text node, set node to that child, then set offset to node's 8321 // length." 8322 if (0 <= offset - 1 8323 && offset - 1 < node.childNodes.length 8324 && node.childNodes[offset - 1].nodeType == $_.Node.TEXT_NODE) { 8325 node = node.childNodes[offset - 1]; 8326 offset = getNodeLength(node); 8327 } 8328 8329 // "If node has a child whose index is offset, and that child is a Text 8330 // node, set node to that child, then set offset to zero." 8331 if (0 <= offset 8332 && offset < node.childNodes.length 8333 && node.childNodes[offset].nodeType == $_.Node.TEXT_NODE) { 8334 node = node.childNodes[offset]; 8335 offset = 0; 8336 } 8337 8338 // "If value is a space (U+0020), and either node is an Element whose 8339 // resolved value for "white-space" is neither "pre" nor "pre-wrap" or 8340 // node is not an Element but its parent is an Element whose resolved 8341 // value for "white-space" is neither "pre" nor "pre-wrap", set value 8342 // to a non-breaking space (U+00A0)." 8343 var refElement = node.nodeType == $_.Node.ELEMENT_NODE ? node : node.parentNode; 8344 if (value == " " 8345 && refElement.nodeType == $_.Node.ELEMENT_NODE 8346 && jQuery.inArray($_.getComputedStyle(refElement).whiteSpace, ["pre", "pre-wrap"]) == -1) { 8347 value = "\xa0"; 8348 } 8349 8350 // "Record current overrides, and let overrides be the result." 8351 var overrides = recordCurrentOverrides( range ); 8352 8353 // "If node is a Text node:" 8354 if (node.nodeType == $_.Node.TEXT_NODE) { 8355 // "Call insertData(offset, value) on node." 8356 node.insertData(offset, value); 8357 8358 // "Call collapse(node, offset) on the context object's Selection." 8359 Aloha.getSelection().collapse(node, offset); 8360 range.setStart(node, offset); 8361 8362 // "Call extend(node, offset + 1) on the context object's 8363 // Selection." 8364 Aloha.getSelection().extend(node, offset + 1); 8365 range.setEnd(node, offset + 1); 8366 8367 // "Otherwise:" 8368 } else { 8369 // "If node has only one child, which is a collapsed line break, 8370 // remove its child from it." 8371 // 8372 // FIXME: IE incorrectly returns false here instead of true 8373 // sometimes? 8374 if (node.childNodes.length == 1 8375 && isCollapsedLineBreak(node.firstChild)) { 8376 node.removeChild(node.firstChild); 8377 } 8378 8379 // "Let text be the result of calling createTextNode(value) on the 8380 // context object." 8381 var text = document.createTextNode(value); 8382 8383 // "Call insertNode(text) on the active range." 8384 range.insertNode(text); 8385 8386 // "Call collapse(text, 0) on the context object's Selection." 8387 Aloha.getSelection().collapse(text, 0); 8388 range.setStart(text, 0); 8389 8390 // "Call extend(text, 1) on the context object's Selection." 8391 Aloha.getSelection().extend(text, 1); 8392 range.setEnd(text, 1); 8393 } 8394 8395 // "Restore states and values from overrides." 8396 restoreStatesAndValues(overrides); 8397 8398 // "Canonicalize whitespace at the active range's start." 8399 canonicalizeWhitespace(range.startContainer, range.startOffset); 8400 8401 // "Canonicalize whitespace at the active range's end." 8402 canonicalizeWhitespace(range.endContainer, range.endOffset); 8403 8404 // "Call collapseToEnd() on the context object's Selection." 8405 Aloha.getSelection().collapseToEnd(); 8406 range.collapse(false); 8407 } 8408 }; 8409 8410 //@} 8411 ///// The insertUnorderedList command ///// 8412 //@{ 8413 commands.insertunorderedlist = { 8414 // "Toggle lists with tag name "ul"." 8415 action: function() { toggleLists("ul") }, 8416 // "True if the selection's list state is "mixed" or "mixed ul", false 8417 // otherwise." 8418 indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) }, 8419 // "True if the selection's list state is "ul", false otherwise." 8420 state: function() { return getSelectionListState() == "ul" } 8421 }; 8422 8423 //@} 8424 ///// The justifyCenter command ///// 8425 //@{ 8426 commands.justifycenter = { 8427 // "Justify the selection with alignment "center"." 8428 action: function(value, range) { justifySelection("center", range) }, 8429 indeterm: function() { 8430 // "Block-extend the active range. Return true if among visible 8431 // editable nodes that are contained in the result and have no 8432 // children, at least one has alignment value "center" and at least one 8433 // does not. Otherwise return false." 8434 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8435 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8436 }); 8437 return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "center" }) 8438 && $_( nodes ).some(function(node) { return getAlignmentValue(node) != "center" }); 8439 }, state: function() { 8440 // "Block-extend the active range. Return true if there is at least one 8441 // visible editable node that is contained in the result and has no 8442 // children, and all such nodes have alignment value "center". 8443 // Otherwise return false." 8444 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8445 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8446 }); 8447 return nodes.length 8448 && $_( nodes ).every(function(node) { return getAlignmentValue(node) == "center" }); 8449 }, value: function() { 8450 // "Block-extend the active range, and return the alignment value of 8451 // the first visible editable node that is contained in the result and 8452 // has no children. If there is no such node, return "left"." 8453 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8454 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8455 }); 8456 if (nodes.length) { 8457 return getAlignmentValue(nodes[0]); 8458 } else { 8459 return "left"; 8460 } 8461 } 8462 }; 8463 8464 //@} 8465 ///// The justifyFull command ///// 8466 //@{ 8467 commands.justifyfull = { 8468 // "Justify the selection with alignment "justify"." 8469 action: function(value, range) { justifySelection("justify", range) }, 8470 indeterm: function() { 8471 // "Block-extend the active range. Return true if among visible 8472 // editable nodes that are contained in the result and have no 8473 // children, at least one has alignment value "justify" and at least 8474 // one does not. Otherwise return false." 8475 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8476 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8477 }); 8478 return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "justify" }) 8479 && $_( nodes ).some(function(node) { return getAlignmentValue(node) != "justify" }); 8480 }, state: function() { 8481 // "Block-extend the active range. Return true if there is at least one 8482 // visible editable node that is contained in the result and has no 8483 // children, and all such nodes have alignment value "justify". 8484 // Otherwise return false." 8485 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8486 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8487 }); 8488 return nodes.length 8489 && $_( nodes ).every(function(node) { return getAlignmentValue(node) == "justify" }); 8490 }, value: function() { 8491 // "Block-extend the active range, and return the alignment value of 8492 // the first visible editable node that is contained in the result and 8493 // has no children. If there is no such node, return "left"." 8494 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8495 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8496 }); 8497 if (nodes.length) { 8498 return getAlignmentValue(nodes[0]); 8499 } else { 8500 return "left"; 8501 } 8502 } 8503 }; 8504 8505 //@} 8506 ///// The justifyLeft command ///// 8507 //@{ 8508 commands.justifyleft = { 8509 // "Justify the selection with alignment "left"." 8510 action: function(value, range) { justifySelection("left", range) }, 8511 indeterm: function() { 8512 // "Block-extend the active range. Return true if among visible 8513 // editable nodes that are contained in the result and have no 8514 // children, at least one has alignment value "left" and at least one 8515 // does not. Otherwise return false." 8516 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8517 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8518 }); 8519 return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "left" }) 8520 && $_( nodes ).some(function(node) { return getAlignmentValue(node) != "left" }); 8521 }, state: function() { 8522 // "Block-extend the active range. Return true if there is at least one 8523 // visible editable node that is contained in the result and has no 8524 // children, and all such nodes have alignment value "left". Otherwise 8525 // return false." 8526 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8527 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8528 }); 8529 return nodes.length 8530 && $_( nodes ).every(function(node) { return getAlignmentValue(node) == "left" }); 8531 }, value: function() { 8532 // "Block-extend the active range, and return the alignment value of 8533 // the first visible editable node that is contained in the result and 8534 // has no children. If there is no such node, return "left"." 8535 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8536 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8537 }); 8538 if (nodes.length) { 8539 return getAlignmentValue(nodes[0]); 8540 } else { 8541 return "left"; 8542 } 8543 } 8544 }; 8545 8546 //@} 8547 ///// The justifyRight command ///// 8548 //@{ 8549 commands.justifyright = { 8550 // "Justify the selection with alignment "right"." 8551 action: function(value, range) { justifySelection("right", range) }, 8552 indeterm: function() { 8553 // "Block-extend the active range. Return true if among visible 8554 // editable nodes that are contained in the result and have no 8555 // children, at least one has alignment value "right" and at least one 8556 // does not. Otherwise return false." 8557 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8558 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8559 }); 8560 return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "right" }) 8561 && $_( nodes ).some(function(node) { return getAlignmentValue(node) != "right" }); 8562 }, state: function() { 8563 // "Block-extend the active range. Return true if there is at least one 8564 // visible editable node that is contained in the result and has no 8565 8566 // children, and all such nodes have alignment value "right". 8567 // Otherwise return false." 8568 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8569 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8570 }); 8571 return nodes.length 8572 && $_( nodes ).every(function(node) { return getAlignmentValue(node) == "right" }); 8573 }, value: function() { 8574 // "Block-extend the active range, and return the alignment value of 8575 // the first visible editable node that is contained in the result and 8576 // has no children. If there is no such node, return "left"." 8577 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { 8578 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 8579 }); 8580 if (nodes.length) { 8581 return getAlignmentValue(nodes[0]); 8582 } else { 8583 return "left"; 8584 } 8585 } 8586 }; 8587 8588 //@} 8589 ///// The outdent command ///// 8590 //@{ 8591 commands.outdent = { 8592 action: function() { 8593 // "Let items be a list of all lis that are ancestor containers of the 8594 // range's start and/or end node." 8595 // 8596 // It's annoying to get this in tree order using functional stuff 8597 // without doing getDescendants(document), which is slow, so I do it 8598 // imperatively. 8599 var items = []; 8600 (function(){ 8601 for ( 8602 var ancestorContainer = getActiveRange().endContainer; 8603 ancestorContainer != getActiveRange().commonAncestorContainer; 8604 ancestorContainer = ancestorContainer.parentNode 8605 ) { 8606 if (isNamedHtmlElement(ancestorContainer, "li")) { 8607 items.unshift(ancestorContainer); 8608 } 8609 } 8610 for ( 8611 var ancestorContainer = getActiveRange().startContainer; 8612 ancestorContainer; 8613 ancestorContainer = ancestorContainer.parentNode 8614 ) { 8615 if (isNamedHtmlElement(ancestorContainer, "li")) { 8616 items.unshift(ancestorContainer); 8617 } 8618 } 8619 })(); 8620 8621 // "For each item in items, normalize sublists of item." 8622 $_( items ).forEach( function( thisArg) { 8623 normalizeSublists( thisArg, range); 8624 }); 8625 8626 // "Block-extend the active range, and let new range be the result." 8627 var newRange = blockExtend(getActiveRange()); 8628 8629 // "Let node list be a list of nodes, initially empty." 8630 // 8631 // "For each node node contained in new range, append node to node list 8632 // if the last member of node list (if any) is not an ancestor of node; 8633 // node is editable; and either node has no editable descendants, or is 8634 // an ol or ul, or is an li whose parent is an ol or ul." 8635 var nodeList = getContainedNodes(newRange, function(node) { 8636 return isEditable(node) 8637 && (!$_( getDescendants(node) ).some(isEditable) 8638 || isHtmlElementInArray(node, ["ol", "ul"]) 8639 || (isNamedHtmlElement(node, 'li') && isHtmlElementInArray(node.parentNode, ["ol", "ul"]))); 8640 }); 8641 8642 // "While node list is not empty:" 8643 while (nodeList.length) { 8644 // "While the first member of node list is an ol or ul or is not 8645 // the child of an ol or ul, outdent it and remove it from node 8646 // list." 8647 while (nodeList.length 8648 && (isHtmlElementInArray(nodeList[0], ["OL", "UL"]) 8649 || !isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"]))) { 8650 outdentNode(nodeList.shift(), range); 8651 } 8652 8653 // "If node list is empty, break from these substeps." 8654 if (!nodeList.length) { 8655 break; 8656 } 8657 8658 // "Let sublist be a list of nodes, initially empty." 8659 var sublist = []; 8660 8661 // "Remove the first member of node list and append it to sublist." 8662 sublist.push(nodeList.shift()); 8663 8664 // "While the first member of node list is the nextSibling of the 8665 // last member of sublist, and the first member of node list is not 8666 // an ol or ul, remove the first member of node list and append it 8667 // to sublist." 8668 while (nodeList.length 8669 && nodeList[0] == sublist[sublist.length - 1].nextSibling 8670 && !isHtmlElementInArray(nodeList[0], ["OL", "UL"])) { 8671 sublist.push(nodeList.shift()); 8672 } 8673 8674 // "Record the values of sublist, and let values be the result." 8675 var values = recordValues(sublist); 8676 8677 // "Split the parent of sublist, with new parent null." 8678 splitParent(sublist, range); 8679 8680 // "Fix disallowed ancestors of each member of sublist." 8681 $_( sublist ).forEach(fixDisallowedAncestors); 8682 8683 // "Restore the values from values." 8684 restoreValues(values, range); 8685 } 8686 } 8687 }; 8688 8689 //@} 8690 8691 ////////////////////////////////// 8692 ///// Miscellaneous commands ///// 8693 ////////////////////////////////// 8694 8695 ///// The selectAll command ///// 8696 //@{ 8697 commands.selectall = { 8698 // Note, this ignores the whole globalRange/getActiveRange() thing and 8699 // works with actual selections. Not suitable for autoimplementation.html. 8700 action: function() { 8701 // "Let target be the body element of the context object." 8702 var target = document.body; 8703 8704 // "If target is null, let target be the context object's 8705 // documentElement." 8706 if (!target) { 8707 target = document.documentElement; 8708 } 8709 8710 // "If target is null, call getSelection() on the context object, and 8711 // call removeAllRanges() on the result." 8712 if (!target) { 8713 Aloha.getSelection().removeAllRanges(); 8714 8715 // "Otherwise, call getSelection() on the context object, and call 8716 // selectAllChildren(target) on the result." 8717 } else { 8718 Aloha.getSelection().selectAllChildren(target); 8719 } 8720 } 8721 }; 8722 8723 //@} 8724 ///// The styleWithCSS command ///// 8725 //@{ 8726 commands.stylewithcss = { 8727 action: function(value) { 8728 // "If value is an ASCII case-insensitive match for the string 8729 // "false", set the CSS styling flag to false. Otherwise, set the 8730 // CSS styling flag to true." 8731 cssStylingFlag = String(value).toLowerCase() != "false"; 8732 }, state: function() { return cssStylingFlag } 8733 }; 8734 8735 //@} 8736 ///// The useCSS command ///// 8737 //@{ 8738 commands.usecss = { 8739 action: function(value) { 8740 // "If value is an ASCII case-insensitive match for the string "false", 8741 // set the CSS styling flag to true. Otherwise, set the CSS styling 8742 // flag to false." 8743 cssStylingFlag = String(value).toLowerCase() == "false"; 8744 } 8745 }; 8746 //@} 8747 8748 // Some final setup 8749 //@{ 8750 (function() { 8751 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit 8752 // temporary, which means I need an extra closure to not leak the temporaries 8753 // into the global namespace. >:( 8754 var commandNames = []; 8755 for (var command in commands) { 8756 commandNames.push(command); 8757 } 8758 $_( commandNames ).forEach(function(command) { 8759 // "If a command does not have a relevant CSS property specified, it 8760 // defaults to null." 8761 if (!("relevantCssProperty" in commands[command])) { 8762 commands[command].relevantCssProperty = null; 8763 } 8764 8765 // "If a command has inline command activated values defined but 8766 // nothing else defines when it is indeterminate, it is indeterminate 8767 // if among editable Text nodes effectively contained in the active 8768 // range, there is at least one whose effective command value is one of 8769 // the given values and at least one whose effective command value is 8770 // not one of the given values." 8771 if ("inlineCommandActivatedValues" in commands[command] 8772 && !("indeterm" in commands[command])) { 8773 commands[command].indeterm = function( range ) { 8774 var values = $_( getAllEffectivelyContainedNodes(range, function(node) { 8775 return isEditable(node) 8776 && node.nodeType == $_.Node.TEXT_NODE; 8777 }) ).map(function(node) { return getEffectiveCommandValue(node, command) }); 8778 8779 var matchingValues = $_( values ).filter(function(value) { 8780 return $_( commands[command].inlineCommandActivatedValues ).indexOf(value) != -1; 8781 }); 8782 8783 return matchingValues.length >= 1 8784 && values.length - matchingValues.length >= 1; 8785 }; 8786 } 8787 8788 // "If a command has inline command activated values defined, its state 8789 // is true if either no editable Text node is effectively contained in 8790 // the active range, and the active range's start node's effective 8791 // command value is one of the given values; or if there is at least 8792 // one editable Text node effectively contained in the active range, 8793 // and all of them have an effective command value equal to one of the 8794 // given values." 8795 if ("inlineCommandActivatedValues" in commands[command]) { 8796 commands[command].state = function(range) { 8797 var nodes = getAllEffectivelyContainedNodes(range, function(node) { 8798 return isEditable(node) 8799 && node.nodeType == $_.Node.TEXT_NODE; 8800 }); 8801 8802 if (nodes.length == 0) { 8803 return $_( commands[command].inlineCommandActivatedValues ) 8804 .indexOf(getEffectiveCommandValue(range.startContainer, command)) != -1; 8805 return ret; 8806 } else { 8807 return $_( nodes ).every(function(node) { 8808 return $_( commands[command].inlineCommandActivatedValues ) 8809 .indexOf(getEffectiveCommandValue(node, command)) != -1; 8810 }); 8811 } 8812 }; 8813 } 8814 8815 // "If a command is a standard inline value command, it is 8816 // indeterminate if among editable Text nodes that are effectively 8817 // contained in the active range, there are two that have distinct 8818 // effective command values. Its value is the effective command value 8819 // of the first editable Text node that is effectively contained in the 8820 // active range, or if there is no such node, the effective command 8821 // value of the active range's start node." 8822 if ("standardInlineValueCommand" in commands[command]) { 8823 commands[command].indeterm = function() { 8824 var values = $_(getAllEffectivelyContainedNodes(getActiveRange())) 8825 .filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE }, true) 8826 .map(function(node) { return getEffectiveCommandValue(node, command) }); 8827 for (var i = 1; i < values.length; i++) { 8828 if (values[i] != values[i - 1]) { 8829 return true; 8830 } 8831 } 8832 return false; 8833 }; 8834 8835 commands[command].value = function(range) { 8836 var refNode = getAllEffectivelyContainedNodes(range, function(node) { 8837 return isEditable(node) 8838 && node.nodeType == $_.Node.TEXT_NODE; 8839 })[0]; 8840 8841 if (typeof refNode == "undefined") { 8842 refNode = range.startContainer; 8843 } 8844 8845 return getEffectiveCommandValue(refNode, command); 8846 }; 8847 } 8848 }); 8849 })(); 8850 //@} 8851 return { 8852 commands: commands, 8853 execCommand: myExecCommand, 8854 queryCommandIndeterm: myQueryCommandIndeterm, 8855 queryCommandState: myQueryCommandState, 8856 queryCommandValue: myQueryCommandValue, 8857 queryCommandEnabled: myQueryCommandEnabled, 8858 queryCommandSupported: myQueryCommandSupported, 8859 copyAttributes: copyAttributes, 8860 createEndBreak: createEndBreak, 8861 isEndBreak: isEndBreak, 8862 ensureContainerEditable: ensureContainerEditable, 8863 isEditingHost: isEditingHost, 8864 isEditable: isEditable 8865 } 8866 }); // end define 8867 // vim: foldmarker=@{,@} foldmethod=marker 8868