Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | "use strict"; |
michael@0 | 2 | |
michael@0 | 3 | var htmlNamespace = "http://www.w3.org/1999/xhtml"; |
michael@0 | 4 | |
michael@0 | 5 | var cssStylingFlag = false; |
michael@0 | 6 | |
michael@0 | 7 | var defaultSingleLineContainerName = "p"; |
michael@0 | 8 | |
michael@0 | 9 | // This is bad :( |
michael@0 | 10 | var globalRange = null; |
michael@0 | 11 | |
michael@0 | 12 | // Commands are stored in a dictionary where we call their actions and such |
michael@0 | 13 | var commands = {}; |
michael@0 | 14 | |
michael@0 | 15 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 16 | ////////////////////////////// Utility functions ////////////////////////////// |
michael@0 | 17 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 18 | //@{ |
michael@0 | 19 | |
michael@0 | 20 | function nextNode(node) { |
michael@0 | 21 | if (node.hasChildNodes()) { |
michael@0 | 22 | return node.firstChild; |
michael@0 | 23 | } |
michael@0 | 24 | return nextNodeDescendants(node); |
michael@0 | 25 | } |
michael@0 | 26 | |
michael@0 | 27 | function previousNode(node) { |
michael@0 | 28 | if (node.previousSibling) { |
michael@0 | 29 | node = node.previousSibling; |
michael@0 | 30 | while (node.hasChildNodes()) { |
michael@0 | 31 | node = node.lastChild; |
michael@0 | 32 | } |
michael@0 | 33 | return node; |
michael@0 | 34 | } |
michael@0 | 35 | if (node.parentNode |
michael@0 | 36 | && node.parentNode.nodeType == Node.ELEMENT_NODE) { |
michael@0 | 37 | return node.parentNode; |
michael@0 | 38 | } |
michael@0 | 39 | return null; |
michael@0 | 40 | } |
michael@0 | 41 | |
michael@0 | 42 | function nextNodeDescendants(node) { |
michael@0 | 43 | while (node && !node.nextSibling) { |
michael@0 | 44 | node = node.parentNode; |
michael@0 | 45 | } |
michael@0 | 46 | if (!node) { |
michael@0 | 47 | return null; |
michael@0 | 48 | } |
michael@0 | 49 | return node.nextSibling; |
michael@0 | 50 | } |
michael@0 | 51 | |
michael@0 | 52 | /** |
michael@0 | 53 | * Returns true if ancestor is an ancestor of descendant, false otherwise. |
michael@0 | 54 | */ |
michael@0 | 55 | function isAncestor(ancestor, descendant) { |
michael@0 | 56 | return ancestor |
michael@0 | 57 | && descendant |
michael@0 | 58 | && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); |
michael@0 | 59 | } |
michael@0 | 60 | |
michael@0 | 61 | /** |
michael@0 | 62 | * Returns true if ancestor is an ancestor of or equal to descendant, false |
michael@0 | 63 | * otherwise. |
michael@0 | 64 | */ |
michael@0 | 65 | function isAncestorContainer(ancestor, descendant) { |
michael@0 | 66 | return (ancestor || descendant) |
michael@0 | 67 | && (ancestor == descendant || isAncestor(ancestor, descendant)); |
michael@0 | 68 | } |
michael@0 | 69 | |
michael@0 | 70 | /** |
michael@0 | 71 | * Returns true if descendant is a descendant of ancestor, false otherwise. |
michael@0 | 72 | */ |
michael@0 | 73 | function isDescendant(descendant, ancestor) { |
michael@0 | 74 | return ancestor |
michael@0 | 75 | && descendant |
michael@0 | 76 | && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); |
michael@0 | 77 | } |
michael@0 | 78 | |
michael@0 | 79 | /** |
michael@0 | 80 | * Returns true if node1 is before node2 in tree order, false otherwise. |
michael@0 | 81 | */ |
michael@0 | 82 | function isBefore(node1, node2) { |
michael@0 | 83 | return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING); |
michael@0 | 84 | } |
michael@0 | 85 | |
michael@0 | 86 | /** |
michael@0 | 87 | * Returns true if node1 is after node2 in tree order, false otherwise. |
michael@0 | 88 | */ |
michael@0 | 89 | function isAfter(node1, node2) { |
michael@0 | 90 | return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING); |
michael@0 | 91 | } |
michael@0 | 92 | |
michael@0 | 93 | function getAncestors(node) { |
michael@0 | 94 | var ancestors = []; |
michael@0 | 95 | while (node.parentNode) { |
michael@0 | 96 | ancestors.unshift(node.parentNode); |
michael@0 | 97 | node = node.parentNode; |
michael@0 | 98 | } |
michael@0 | 99 | return ancestors; |
michael@0 | 100 | } |
michael@0 | 101 | |
michael@0 | 102 | function getInclusiveAncestors(node) { |
michael@0 | 103 | return getAncestors(node).concat(node); |
michael@0 | 104 | } |
michael@0 | 105 | |
michael@0 | 106 | function getDescendants(node) { |
michael@0 | 107 | var descendants = []; |
michael@0 | 108 | var stop = nextNodeDescendants(node); |
michael@0 | 109 | while ((node = nextNode(node)) |
michael@0 | 110 | && node != stop) { |
michael@0 | 111 | descendants.push(node); |
michael@0 | 112 | } |
michael@0 | 113 | return descendants; |
michael@0 | 114 | } |
michael@0 | 115 | |
michael@0 | 116 | function getInclusiveDescendants(node) { |
michael@0 | 117 | return [node].concat(getDescendants(node)); |
michael@0 | 118 | } |
michael@0 | 119 | |
michael@0 | 120 | function convertProperty(property) { |
michael@0 | 121 | // Special-case for now |
michael@0 | 122 | var map = { |
michael@0 | 123 | "fontFamily": "font-family", |
michael@0 | 124 | "fontSize": "font-size", |
michael@0 | 125 | "fontStyle": "font-style", |
michael@0 | 126 | "fontWeight": "font-weight", |
michael@0 | 127 | "textDecoration": "text-decoration", |
michael@0 | 128 | }; |
michael@0 | 129 | if (typeof map[property] != "undefined") { |
michael@0 | 130 | return map[property]; |
michael@0 | 131 | } |
michael@0 | 132 | |
michael@0 | 133 | return property; |
michael@0 | 134 | } |
michael@0 | 135 | |
michael@0 | 136 | // Return the <font size=X> value for the given CSS size, or undefined if there |
michael@0 | 137 | // is none. |
michael@0 | 138 | function cssSizeToLegacy(cssVal) { |
michael@0 | 139 | return { |
michael@0 | 140 | "x-small": 1, |
michael@0 | 141 | "small": 2, |
michael@0 | 142 | "medium": 3, |
michael@0 | 143 | "large": 4, |
michael@0 | 144 | "x-large": 5, |
michael@0 | 145 | "xx-large": 6, |
michael@0 | 146 | "xxx-large": 7 |
michael@0 | 147 | }[cssVal]; |
michael@0 | 148 | } |
michael@0 | 149 | |
michael@0 | 150 | // Return the CSS size given a legacy size. |
michael@0 | 151 | function legacySizeToCss(legacyVal) { |
michael@0 | 152 | return { |
michael@0 | 153 | 1: "x-small", |
michael@0 | 154 | 2: "small", |
michael@0 | 155 | 3: "medium", |
michael@0 | 156 | 4: "large", |
michael@0 | 157 | 5: "x-large", |
michael@0 | 158 | 6: "xx-large", |
michael@0 | 159 | 7: "xxx-large", |
michael@0 | 160 | }[legacyVal]; |
michael@0 | 161 | } |
michael@0 | 162 | |
michael@0 | 163 | // Opera 11 puts HTML elements in the null namespace, it seems. |
michael@0 | 164 | function isHtmlNamespace(ns) { |
michael@0 | 165 | return ns === null |
michael@0 | 166 | || ns === htmlNamespace; |
michael@0 | 167 | } |
michael@0 | 168 | |
michael@0 | 169 | // "the directionality" from HTML. I don't bother caring about non-HTML |
michael@0 | 170 | // elements. |
michael@0 | 171 | // |
michael@0 | 172 | // "The directionality of an element is either 'ltr' or 'rtl', and is |
michael@0 | 173 | // determined as per the first appropriate set of steps from the following |
michael@0 | 174 | // list:" |
michael@0 | 175 | function getDirectionality(element) { |
michael@0 | 176 | // "If the element's dir attribute is in the ltr state |
michael@0 | 177 | // The directionality of the element is 'ltr'." |
michael@0 | 178 | if (element.dir == "ltr") { |
michael@0 | 179 | return "ltr"; |
michael@0 | 180 | } |
michael@0 | 181 | |
michael@0 | 182 | // "If the element's dir attribute is in the rtl state |
michael@0 | 183 | // The directionality of the element is 'rtl'." |
michael@0 | 184 | if (element.dir == "rtl") { |
michael@0 | 185 | return "rtl"; |
michael@0 | 186 | } |
michael@0 | 187 | |
michael@0 | 188 | // "If the element's dir attribute is in the auto state |
michael@0 | 189 | // "If the element is a bdi element and the dir attribute is not in a |
michael@0 | 190 | // defined state (i.e. it is not present or has an invalid value) |
michael@0 | 191 | // [lots of complicated stuff] |
michael@0 | 192 | // |
michael@0 | 193 | // Skip this, since no browser implements it anyway. |
michael@0 | 194 | |
michael@0 | 195 | // "If the element is a root element and the dir attribute is not in a |
michael@0 | 196 | // defined state (i.e. it is not present or has an invalid value) |
michael@0 | 197 | // The directionality of the element is 'ltr'." |
michael@0 | 198 | if (!isHtmlElement(element.parentNode)) { |
michael@0 | 199 | return "ltr"; |
michael@0 | 200 | } |
michael@0 | 201 | |
michael@0 | 202 | // "If the element has a parent element and the dir attribute is not in a |
michael@0 | 203 | // defined state (i.e. it is not present or has an invalid value) |
michael@0 | 204 | // The directionality of the element is the same as the element's |
michael@0 | 205 | // parent element's directionality." |
michael@0 | 206 | return getDirectionality(element.parentNode); |
michael@0 | 207 | } |
michael@0 | 208 | |
michael@0 | 209 | //@} |
michael@0 | 210 | |
michael@0 | 211 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 212 | ///////////////////////////// DOM Range functions ///////////////////////////// |
michael@0 | 213 | /////////////////////////////////////////////////////////////////////////////// |
michael@0 | 214 | //@{ |
michael@0 | 215 | |
michael@0 | 216 | function getNodeIndex(node) { |
michael@0 | 217 | var ret = 0; |
michael@0 | 218 | while (node.previousSibling) { |
michael@0 | 219 | ret++; |
michael@0 | 220 | node = node.previousSibling; |
michael@0 | 221 | } |
michael@0 | 222 | return ret; |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | // "The length of a Node node is the following, depending on node: |
michael@0 | 226 | // |
michael@0 | 227 | // ProcessingInstruction |
michael@0 | 228 | // DocumentType |
michael@0 | 229 | // Always 0. |
michael@0 | 230 | // Text |
michael@0 | 231 | // Comment |
michael@0 | 232 | // node's length. |
michael@0 | 233 | // Any other node |
michael@0 | 234 | // node's childNodes's length." |
michael@0 | 235 | function getNodeLength(node) { |
michael@0 | 236 | switch (node.nodeType) { |
michael@0 | 237 | case Node.PROCESSING_INSTRUCTION_NODE: |
michael@0 | 238 | case Node.DOCUMENT_TYPE_NODE: |
michael@0 | 239 | return 0; |
michael@0 | 240 | |
michael@0 | 241 | case Node.TEXT_NODE: |
michael@0 | 242 | case Node.COMMENT_NODE: |
michael@0 | 243 | return node.length; |
michael@0 | 244 | |
michael@0 | 245 | default: |
michael@0 | 246 | return node.childNodes.length; |
michael@0 | 247 | } |
michael@0 | 248 | } |
michael@0 | 249 | |
michael@0 | 250 | /** |
michael@0 | 251 | * The position of two boundary points relative to one another, as defined by |
michael@0 | 252 | * DOM Range. |
michael@0 | 253 | */ |
michael@0 | 254 | function getPosition(nodeA, offsetA, nodeB, offsetB) { |
michael@0 | 255 | // "If node A is the same as node B, return equal if offset A equals offset |
michael@0 | 256 | // B, before if offset A is less than offset B, and after if offset A is |
michael@0 | 257 | // greater than offset B." |
michael@0 | 258 | if (nodeA == nodeB) { |
michael@0 | 259 | if (offsetA == offsetB) { |
michael@0 | 260 | return "equal"; |
michael@0 | 261 | } |
michael@0 | 262 | if (offsetA < offsetB) { |
michael@0 | 263 | return "before"; |
michael@0 | 264 | } |
michael@0 | 265 | if (offsetA > offsetB) { |
michael@0 | 266 | return "after"; |
michael@0 | 267 | } |
michael@0 | 268 | } |
michael@0 | 269 | |
michael@0 | 270 | // "If node A is after node B in tree order, compute the position of (node |
michael@0 | 271 | // B, offset B) relative to (node A, offset A). If it is before, return |
michael@0 | 272 | // after. If it is after, return before." |
michael@0 | 273 | if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) { |
michael@0 | 274 | var pos = getPosition(nodeB, offsetB, nodeA, offsetA); |
michael@0 | 275 | if (pos == "before") { |
michael@0 | 276 | return "after"; |
michael@0 | 277 | } |
michael@0 | 278 | if (pos == "after") { |
michael@0 | 279 | return "before"; |
michael@0 | 280 | } |
michael@0 | 281 | } |
michael@0 | 282 | |
michael@0 | 283 | // "If node A is an ancestor of node B:" |
michael@0 | 284 | if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) { |
michael@0 | 285 | // "Let child equal node B." |
michael@0 | 286 | var child = nodeB; |
michael@0 | 287 | |
michael@0 | 288 | // "While child is not a child of node A, set child to its parent." |
michael@0 | 289 | while (child.parentNode != nodeA) { |
michael@0 | 290 | child = child.parentNode; |
michael@0 | 291 | } |
michael@0 | 292 | |
michael@0 | 293 | // "If the index of child is less than offset A, return after." |
michael@0 | 294 | if (getNodeIndex(child) < offsetA) { |
michael@0 | 295 | return "after"; |
michael@0 | 296 | } |
michael@0 | 297 | } |
michael@0 | 298 | |
michael@0 | 299 | // "Return before." |
michael@0 | 300 | return "before"; |
michael@0 | 301 | } |
michael@0 | 302 | |
michael@0 | 303 | /** |
michael@0 | 304 | * Returns the furthest ancestor of a Node as defined by DOM Range. |
michael@0 | 305 | */ |
michael@0 | 306 | function getFurthestAncestor(node) { |
michael@0 | 307 | var root = node; |
michael@0 | 308 | while (root.parentNode != null) { |
michael@0 | 309 | root = root.parentNode; |
michael@0 | 310 | } |
michael@0 | 311 | return root; |
michael@0 | 312 | } |
michael@0 | 313 | |
michael@0 | 314 | /** |
michael@0 | 315 | * "contained" as defined by DOM Range: "A Node node is contained in a range |
michael@0 | 316 | * range if node's furthest ancestor is the same as range's root, and (node, 0) |
michael@0 | 317 | * is after range's start, and (node, length of node) is before range's end." |
michael@0 | 318 | */ |
michael@0 | 319 | function isContained(node, range) { |
michael@0 | 320 | var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); |
michael@0 | 321 | var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset); |
michael@0 | 322 | |
michael@0 | 323 | return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer) |
michael@0 | 324 | && pos1 == "after" |
michael@0 | 325 | && pos2 == "before"; |
michael@0 | 326 | } |
michael@0 | 327 | |
michael@0 | 328 | /** |
michael@0 | 329 | * Return all nodes contained in range that the provided function returns true |
michael@0 | 330 | * for, omitting any with an ancestor already being returned. |
michael@0 | 331 | */ |
michael@0 | 332 | function getContainedNodes(range, condition) { |
michael@0 | 333 | if (typeof condition == "undefined") { |
michael@0 | 334 | condition = function() { return true }; |
michael@0 | 335 | } |
michael@0 | 336 | var node = range.startContainer; |
michael@0 | 337 | if (node.hasChildNodes() |
michael@0 | 338 | && range.startOffset < node.childNodes.length) { |
michael@0 | 339 | // A child is contained |
michael@0 | 340 | node = node.childNodes[range.startOffset]; |
michael@0 | 341 | } else if (range.startOffset == getNodeLength(node)) { |
michael@0 | 342 | // No descendant can be contained |
michael@0 | 343 | node = nextNodeDescendants(node); |
michael@0 | 344 | } else { |
michael@0 | 345 | // No children; this node at least can't be contained |
michael@0 | 346 | node = nextNode(node); |
michael@0 | 347 | } |
michael@0 | 348 | |
michael@0 | 349 | var stop = range.endContainer; |
michael@0 | 350 | if (stop.hasChildNodes() |
michael@0 | 351 | && range.endOffset < stop.childNodes.length) { |
michael@0 | 352 | // The node after the last contained node is a child |
michael@0 | 353 | stop = stop.childNodes[range.endOffset]; |
michael@0 | 354 | } else { |
michael@0 | 355 | // This node and/or some of its children might be contained |
michael@0 | 356 | stop = nextNodeDescendants(stop); |
michael@0 | 357 | } |
michael@0 | 358 | |
michael@0 | 359 | var nodeList = []; |
michael@0 | 360 | while (isBefore(node, stop)) { |
michael@0 | 361 | if (isContained(node, range) |
michael@0 | 362 | && condition(node)) { |
michael@0 | 363 | nodeList.push(node); |
michael@0 | 364 | node = nextNodeDescendants(node); |
michael@0 | 365 | continue; |
michael@0 | 366 | } |
michael@0 | 367 | node = nextNode(node); |
michael@0 | 368 | } |
michael@0 | 369 | return nodeList; |
michael@0 | 370 | } |
michael@0 | 371 | |
michael@0 | 372 | /** |
michael@0 | 373 | * As above, but includes nodes with an ancestor that's already been returned. |
michael@0 | 374 | */ |
michael@0 | 375 | function getAllContainedNodes(range, condition) { |
michael@0 | 376 | if (typeof condition == "undefined") { |
michael@0 | 377 | condition = function() { return true }; |
michael@0 | 378 | } |
michael@0 | 379 | var node = range.startContainer; |
michael@0 | 380 | if (node.hasChildNodes() |
michael@0 | 381 | && range.startOffset < node.childNodes.length) { |
michael@0 | 382 | // A child is contained |
michael@0 | 383 | node = node.childNodes[range.startOffset]; |
michael@0 | 384 | } else if (range.startOffset == getNodeLength(node)) { |
michael@0 | 385 | // No descendant can be contained |
michael@0 | 386 | node = nextNodeDescendants(node); |
michael@0 | 387 | } else { |
michael@0 | 388 | // No children; this node at least can't be contained |
michael@0 | 389 | node = nextNode(node); |
michael@0 | 390 | } |
michael@0 | 391 | |
michael@0 | 392 | var stop = range.endContainer; |
michael@0 | 393 | if (stop.hasChildNodes() |
michael@0 | 394 | && range.endOffset < stop.childNodes.length) { |
michael@0 | 395 | // The node after the last contained node is a child |
michael@0 | 396 | stop = stop.childNodes[range.endOffset]; |
michael@0 | 397 | } else { |
michael@0 | 398 | // This node and/or some of its children might be contained |
michael@0 | 399 | stop = nextNodeDescendants(stop); |
michael@0 | 400 | } |
michael@0 | 401 | |
michael@0 | 402 | var nodeList = []; |
michael@0 | 403 | while (isBefore(node, stop)) { |
michael@0 | 404 | if (isContained(node, range) |
michael@0 | 405 | && condition(node)) { |
michael@0 | 406 | nodeList.push(node); |
michael@0 | 407 | } |
michael@0 | 408 | node = nextNode(node); |
michael@0 | 409 | } |
michael@0 | 410 | return nodeList; |
michael@0 | 411 | } |
michael@0 | 412 | |
michael@0 | 413 | // Returns either null, or something of the form rgb(x, y, z), or something of |
michael@0 | 414 | // the form rgb(x, y, z, w) with w != 0. |
michael@0 | 415 | function normalizeColor(color) { |
michael@0 | 416 | if (color.toLowerCase() == "currentcolor") { |
michael@0 | 417 | return null; |
michael@0 | 418 | } |
michael@0 | 419 | |
michael@0 | 420 | if (normalizeColor.resultCache === undefined) { |
michael@0 | 421 | normalizeColor.resultCache = {}; |
michael@0 | 422 | } |
michael@0 | 423 | |
michael@0 | 424 | if (normalizeColor.resultCache[color] !== undefined) { |
michael@0 | 425 | return normalizeColor.resultCache[color]; |
michael@0 | 426 | } |
michael@0 | 427 | |
michael@0 | 428 | var originalColor = color; |
michael@0 | 429 | |
michael@0 | 430 | var outerSpan = document.createElement("span"); |
michael@0 | 431 | document.body.appendChild(outerSpan); |
michael@0 | 432 | outerSpan.style.color = "black"; |
michael@0 | 433 | |
michael@0 | 434 | var innerSpan = document.createElement("span"); |
michael@0 | 435 | outerSpan.appendChild(innerSpan); |
michael@0 | 436 | innerSpan.style.color = color; |
michael@0 | 437 | color = getComputedStyle(innerSpan).color; |
michael@0 | 438 | |
michael@0 | 439 | if (color == "rgb(0, 0, 0)") { |
michael@0 | 440 | // Maybe it's really black, maybe it's invalid. |
michael@0 | 441 | outerSpan.color = "white"; |
michael@0 | 442 | color = getComputedStyle(innerSpan).color; |
michael@0 | 443 | if (color != "rgb(0, 0, 0)") { |
michael@0 | 444 | return normalizeColor.resultCache[originalColor] = null; |
michael@0 | 445 | } |
michael@0 | 446 | } |
michael@0 | 447 | |
michael@0 | 448 | document.body.removeChild(outerSpan); |
michael@0 | 449 | |
michael@0 | 450 | // I rely on the fact that browsers generally provide consistent syntax for |
michael@0 | 451 | // getComputedStyle(), although it's not standardized. There are only |
michael@0 | 452 | // three exceptions I found: |
michael@0 | 453 | if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) { |
michael@0 | 454 | // IE10PP2 seems to do this sometimes. |
michael@0 | 455 | return normalizeColor.resultCache[originalColor] = |
michael@0 | 456 | color.replace("rgba", "rgb").replace(", 1)", ")"); |
michael@0 | 457 | } |
michael@0 | 458 | if (color == "transparent") { |
michael@0 | 459 | // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if |
michael@0 | 460 | // the specified value is "transparent". |
michael@0 | 461 | return normalizeColor.resultCache[originalColor] = |
michael@0 | 462 | "rgba(0, 0, 0, 0)"; |
michael@0 | 463 | } |
michael@0 | 464 | // Chrome 15 dev adds way too many significant figures. This isn't a full |
michael@0 | 465 | // fix, it just fixes one case that comes up in tests. |
michael@0 | 466 | color = color.replace(/, 0.496094\)$/, ", 0.5)"); |
michael@0 | 467 | return normalizeColor.resultCache[originalColor] = color; |
michael@0 | 468 | } |
michael@0 | 469 | |
michael@0 | 470 | // Returns either null, or something of the form #xxxxxx. |
michael@0 | 471 | function parseSimpleColor(color) { |
michael@0 | 472 | color = normalizeColor(color); |
michael@0 | 473 | var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color); |
michael@0 | 474 | if (matches) { |
michael@0 | 475 | return "#" |
michael@0 | 476 | + parseInt(matches[1]).toString(16).replace(/^.$/, "0$&") |
michael@0 | 477 | + parseInt(matches[2]).toString(16).replace(/^.$/, "0$&") |
michael@0 | 478 | + parseInt(matches[3]).toString(16).replace(/^.$/, "0$&"); |
michael@0 | 479 | } |
michael@0 | 480 | return null; |
michael@0 | 481 | } |
michael@0 | 482 | |
michael@0 | 483 | //@} |
michael@0 | 484 | |
michael@0 | 485 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 486 | /////////////////////////// Edit command functions /////////////////////////// |
michael@0 | 487 | ////////////////////////////////////////////////////////////////////////////// |
michael@0 | 488 | |
michael@0 | 489 | ///////////////////////////////////////////////// |
michael@0 | 490 | ///// Methods of the HTMLDocument interface ///// |
michael@0 | 491 | ///////////////////////////////////////////////// |
michael@0 | 492 | //@{ |
michael@0 | 493 | |
michael@0 | 494 | var executionStackDepth = 0; |
michael@0 | 495 | |
michael@0 | 496 | // Helper function for common behavior. |
michael@0 | 497 | function editCommandMethod(command, range, callback) { |
michael@0 | 498 | // Set up our global range magic, but only if we're the outermost function |
michael@0 | 499 | if (executionStackDepth == 0 && typeof range != "undefined") { |
michael@0 | 500 | globalRange = range; |
michael@0 | 501 | } else if (executionStackDepth == 0) { |
michael@0 | 502 | globalRange = null; |
michael@0 | 503 | globalRange = getActiveRange(); |
michael@0 | 504 | } |
michael@0 | 505 | |
michael@0 | 506 | executionStackDepth++; |
michael@0 | 507 | try { |
michael@0 | 508 | var ret = callback(); |
michael@0 | 509 | } catch(e) { |
michael@0 | 510 | executionStackDepth--; |
michael@0 | 511 | throw e; |
michael@0 | 512 | } |
michael@0 | 513 | executionStackDepth--; |
michael@0 | 514 | return ret; |
michael@0 | 515 | } |
michael@0 | 516 | |
michael@0 | 517 | function myExecCommand(command, showUi, value, range) { |
michael@0 | 518 | // "All of these methods must treat their command argument ASCII |
michael@0 | 519 | // case-insensitively." |
michael@0 | 520 | command = command.toLowerCase(); |
michael@0 | 521 | |
michael@0 | 522 | // "If only one argument was provided, let show UI be false." |
michael@0 | 523 | // |
michael@0 | 524 | // If range was passed, I can't actually detect how many args were passed |
michael@0 | 525 | // . . . |
michael@0 | 526 | if (arguments.length == 1 |
michael@0 | 527 | || (arguments.length >=4 && typeof showUi == "undefined")) { |
michael@0 | 528 | showUi = false; |
michael@0 | 529 | } |
michael@0 | 530 | |
michael@0 | 531 | // "If only one or two arguments were provided, let value be the empty |
michael@0 | 532 | // string." |
michael@0 | 533 | if (arguments.length <= 2 |
michael@0 | 534 | || (arguments.length >=4 && typeof value == "undefined")) { |
michael@0 | 535 | value = ""; |
michael@0 | 536 | } |
michael@0 | 537 | |
michael@0 | 538 | return editCommandMethod(command, range, (function(command, showUi, value) { return function() { |
michael@0 | 539 | // "If command is not supported or not enabled, return false." |
michael@0 | 540 | if (!(command in commands) || !myQueryCommandEnabled(command)) { |
michael@0 | 541 | return false; |
michael@0 | 542 | } |
michael@0 | 543 | |
michael@0 | 544 | // "Take the action for command, passing value to the instructions as an |
michael@0 | 545 | // argument." |
michael@0 | 546 | var ret = commands[command].action(value); |
michael@0 | 547 | |
michael@0 | 548 | // Check for bugs |
michael@0 | 549 | if (ret !== true && ret !== false) { |
michael@0 | 550 | throw "execCommand() didn't return true or false: " + ret; |
michael@0 | 551 | } |
michael@0 | 552 | |
michael@0 | 553 | // "If the previous step returned false, return false." |
michael@0 | 554 | if (ret === false) { |
michael@0 | 555 | return false; |
michael@0 | 556 | } |
michael@0 | 557 | |
michael@0 | 558 | // "Return true." |
michael@0 | 559 | return true; |
michael@0 | 560 | }})(command, showUi, value)); |
michael@0 | 561 | } |
michael@0 | 562 | |
michael@0 | 563 | function myQueryCommandEnabled(command, range) { |
michael@0 | 564 | // "All of these methods must treat their command argument ASCII |
michael@0 | 565 | // case-insensitively." |
michael@0 | 566 | command = command.toLowerCase(); |
michael@0 | 567 | |
michael@0 | 568 | return editCommandMethod(command, range, (function(command) { return function() { |
michael@0 | 569 | // "Return true if command is both supported and enabled, false |
michael@0 | 570 | // otherwise." |
michael@0 | 571 | if (!(command in commands)) { |
michael@0 | 572 | return false; |
michael@0 | 573 | } |
michael@0 | 574 | |
michael@0 | 575 | // "Among commands defined in this specification, those listed in |
michael@0 | 576 | // Miscellaneous commands are always enabled, except for the cut |
michael@0 | 577 | // command and the paste command. The other commands defined here are |
michael@0 | 578 | // enabled if the active range is not null, its start node is either |
michael@0 | 579 | // editable or an editing host, its end node is either editable or an |
michael@0 | 580 | // editing host, and there is some editing host that is an inclusive |
michael@0 | 581 | // ancestor of both its start node and its end node." |
michael@0 | 582 | return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss", |
michael@0 | 583 | "usecss"].indexOf(command) != -1 |
michael@0 | 584 | || ( |
michael@0 | 585 | getActiveRange() !== null |
michael@0 | 586 | && (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer)) |
michael@0 | 587 | && (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer)) |
michael@0 | 588 | && (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost)) |
michael@0 | 589 | ); |
michael@0 | 590 | }})(command)); |
michael@0 | 591 | } |
michael@0 | 592 | |
michael@0 | 593 | function myQueryCommandIndeterm(command, range) { |
michael@0 | 594 | // "All of these methods must treat their command argument ASCII |
michael@0 | 595 | // case-insensitively." |
michael@0 | 596 | command = command.toLowerCase(); |
michael@0 | 597 | |
michael@0 | 598 | return editCommandMethod(command, range, (function(command) { return function() { |
michael@0 | 599 | // "If command is not supported or has no indeterminacy, return false." |
michael@0 | 600 | if (!(command in commands) || !("indeterm" in commands[command])) { |
michael@0 | 601 | return false; |
michael@0 | 602 | } |
michael@0 | 603 | |
michael@0 | 604 | // "Return true if command is indeterminate, otherwise false." |
michael@0 | 605 | return commands[command].indeterm(); |
michael@0 | 606 | }})(command)); |
michael@0 | 607 | } |
michael@0 | 608 | |
michael@0 | 609 | function myQueryCommandState(command, range) { |
michael@0 | 610 | // "All of these methods must treat their command argument ASCII |
michael@0 | 611 | // case-insensitively." |
michael@0 | 612 | command = command.toLowerCase(); |
michael@0 | 613 | |
michael@0 | 614 | return editCommandMethod(command, range, (function(command) { return function() { |
michael@0 | 615 | // "If command is not supported or has no state, return false." |
michael@0 | 616 | if (!(command in commands) || !("state" in commands[command])) { |
michael@0 | 617 | return false; |
michael@0 | 618 | } |
michael@0 | 619 | |
michael@0 | 620 | // "If the state override for command is set, return it." |
michael@0 | 621 | if (typeof getStateOverride(command) != "undefined") { |
michael@0 | 622 | return getStateOverride(command); |
michael@0 | 623 | } |
michael@0 | 624 | |
michael@0 | 625 | // "Return true if command's state is true, otherwise false." |
michael@0 | 626 | return commands[command].state(); |
michael@0 | 627 | }})(command)); |
michael@0 | 628 | } |
michael@0 | 629 | |
michael@0 | 630 | // "When the queryCommandSupported(command) method on the HTMLDocument |
michael@0 | 631 | // interface is invoked, the user agent must return true if command is |
michael@0 | 632 | // supported, and false otherwise." |
michael@0 | 633 | function myQueryCommandSupported(command) { |
michael@0 | 634 | // "All of these methods must treat their command argument ASCII |
michael@0 | 635 | // case-insensitively." |
michael@0 | 636 | command = command.toLowerCase(); |
michael@0 | 637 | |
michael@0 | 638 | return command in commands; |
michael@0 | 639 | } |
michael@0 | 640 | |
michael@0 | 641 | function myQueryCommandValue(command, range) { |
michael@0 | 642 | // "All of these methods must treat their command argument ASCII |
michael@0 | 643 | // case-insensitively." |
michael@0 | 644 | command = command.toLowerCase(); |
michael@0 | 645 | |
michael@0 | 646 | return editCommandMethod(command, range, function() { |
michael@0 | 647 | // "If command is not supported or has no value, return the empty string." |
michael@0 | 648 | if (!(command in commands) || !("value" in commands[command])) { |
michael@0 | 649 | return ""; |
michael@0 | 650 | } |
michael@0 | 651 | |
michael@0 | 652 | // "If command is "fontSize" and its value override is set, convert the |
michael@0 | 653 | // value override to an integer number of pixels and return the legacy |
michael@0 | 654 | // font size for the result." |
michael@0 | 655 | if (command == "fontsize" |
michael@0 | 656 | && getValueOverride("fontsize") !== undefined) { |
michael@0 | 657 | return getLegacyFontSize(getValueOverride("fontsize")); |
michael@0 | 658 | } |
michael@0 | 659 | |
michael@0 | 660 | // "If the value override for command is set, return it." |
michael@0 | 661 | if (typeof getValueOverride(command) != "undefined") { |
michael@0 | 662 | return getValueOverride(command); |
michael@0 | 663 | } |
michael@0 | 664 | |
michael@0 | 665 | // "Return command's value." |
michael@0 | 666 | return commands[command].value(); |
michael@0 | 667 | }); |
michael@0 | 668 | } |
michael@0 | 669 | //@} |
michael@0 | 670 | |
michael@0 | 671 | ////////////////////////////// |
michael@0 | 672 | ///// Common definitions ///// |
michael@0 | 673 | ////////////////////////////// |
michael@0 | 674 | //@{ |
michael@0 | 675 | |
michael@0 | 676 | // "An HTML element is an Element whose namespace is the HTML namespace." |
michael@0 | 677 | // |
michael@0 | 678 | // I allow an extra argument to more easily check whether something is a |
michael@0 | 679 | // particular HTML element, like isHtmlElement(node, "OL"). It accepts arrays |
michael@0 | 680 | // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul. |
michael@0 | 681 | function isHtmlElement(node, tags) { |
michael@0 | 682 | if (typeof tags == "string") { |
michael@0 | 683 | tags = [tags]; |
michael@0 | 684 | } |
michael@0 | 685 | if (typeof tags == "object") { |
michael@0 | 686 | tags = tags.map(function(tag) { return tag.toUpperCase() }); |
michael@0 | 687 | } |
michael@0 | 688 | return node |
michael@0 | 689 | && node.nodeType == Node.ELEMENT_NODE |
michael@0 | 690 | && isHtmlNamespace(node.namespaceURI) |
michael@0 | 691 | && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1); |
michael@0 | 692 | } |
michael@0 | 693 | |
michael@0 | 694 | // "A prohibited paragraph child name is "address", "article", "aside", |
michael@0 | 695 | // "blockquote", "caption", "center", "col", "colgroup", "dd", "details", |
michael@0 | 696 | // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", |
michael@0 | 697 | // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", |
michael@0 | 698 | // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", |
michael@0 | 699 | // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or |
michael@0 | 700 | // "xmp"." |
michael@0 | 701 | var prohibitedParagraphChildNames = ["address", "article", "aside", |
michael@0 | 702 | "blockquote", "caption", "center", "col", "colgroup", "dd", "details", |
michael@0 | 703 | "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", |
michael@0 | 704 | "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", |
michael@0 | 705 | "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", |
michael@0 | 706 | "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", |
michael@0 | 707 | "xmp"]; |
michael@0 | 708 | |
michael@0 | 709 | // "A prohibited paragraph child is an HTML element whose local name is a |
michael@0 | 710 | // prohibited paragraph child name." |
michael@0 | 711 | function isProhibitedParagraphChild(node) { |
michael@0 | 712 | return isHtmlElement(node, prohibitedParagraphChildNames); |
michael@0 | 713 | } |
michael@0 | 714 | |
michael@0 | 715 | // "A block node is either an Element whose "display" property does not have |
michael@0 | 716 | // resolved value "inline" or "inline-block" or "inline-table" or "none", or a |
michael@0 | 717 | // Document, or a DocumentFragment." |
michael@0 | 718 | function isBlockNode(node) { |
michael@0 | 719 | return node |
michael@0 | 720 | && ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1) |
michael@0 | 721 | || node.nodeType == Node.DOCUMENT_NODE |
michael@0 | 722 | || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE); |
michael@0 | 723 | } |
michael@0 | 724 | |
michael@0 | 725 | // "An inline node is a node that is not a block node." |
michael@0 | 726 | function isInlineNode(node) { |
michael@0 | 727 | return node && !isBlockNode(node); |
michael@0 | 728 | } |
michael@0 | 729 | |
michael@0 | 730 | // "An editing host is a node that is either an HTML element with a |
michael@0 | 731 | // contenteditable attribute set to the true state, or the HTML element child |
michael@0 | 732 | // of a Document whose designMode is enabled." |
michael@0 | 733 | function isEditingHost(node) { |
michael@0 | 734 | return node |
michael@0 | 735 | && isHtmlElement(node) |
michael@0 | 736 | && (node.contentEditable == "true" |
michael@0 | 737 | || (node.parentNode |
michael@0 | 738 | && node.parentNode.nodeType == Node.DOCUMENT_NODE |
michael@0 | 739 | && node.parentNode.designMode == "on")); |
michael@0 | 740 | } |
michael@0 | 741 | |
michael@0 | 742 | // "Something is editable if it is a node; it is not an editing host; it does |
michael@0 | 743 | // not have a contenteditable attribute set to the false state; its parent is |
michael@0 | 744 | // an editing host or editable; and either it is an HTML element, or it is an |
michael@0 | 745 | // svg or math element, or it is not an Element and its parent is an HTML |
michael@0 | 746 | // element." |
michael@0 | 747 | function isEditable(node) { |
michael@0 | 748 | return node |
michael@0 | 749 | && !isEditingHost(node) |
michael@0 | 750 | && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false") |
michael@0 | 751 | && (isEditingHost(node.parentNode) || isEditable(node.parentNode)) |
michael@0 | 752 | && (isHtmlElement(node) |
michael@0 | 753 | || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg") |
michael@0 | 754 | || (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math") |
michael@0 | 755 | || (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode))); |
michael@0 | 756 | } |
michael@0 | 757 | |
michael@0 | 758 | // Helper function, not defined in the spec |
michael@0 | 759 | function hasEditableDescendants(node) { |
michael@0 | 760 | for (var i = 0; i < node.childNodes.length; i++) { |
michael@0 | 761 | if (isEditable(node.childNodes[i]) |
michael@0 | 762 | || hasEditableDescendants(node.childNodes[i])) { |
michael@0 | 763 | return true; |
michael@0 | 764 | } |
michael@0 | 765 | } |
michael@0 | 766 | return false; |
michael@0 | 767 | } |
michael@0 | 768 | |
michael@0 | 769 | // "The editing host of node is null if node is neither editable nor an editing |
michael@0 | 770 | // host; node itself, if node is an editing host; or the nearest ancestor of |
michael@0 | 771 | // node that is an editing host, if node is editable." |
michael@0 | 772 | function getEditingHostOf(node) { |
michael@0 | 773 | if (isEditingHost(node)) { |
michael@0 | 774 | return node; |
michael@0 | 775 | } else if (isEditable(node)) { |
michael@0 | 776 | var ancestor = node.parentNode; |
michael@0 | 777 | while (!isEditingHost(ancestor)) { |
michael@0 | 778 | ancestor = ancestor.parentNode; |
michael@0 | 779 | } |
michael@0 | 780 | return ancestor; |
michael@0 | 781 | } else { |
michael@0 | 782 | return null; |
michael@0 | 783 | } |
michael@0 | 784 | } |
michael@0 | 785 | |
michael@0 | 786 | // "Two nodes are in the same editing host if the editing host of the first is |
michael@0 | 787 | // non-null and the same as the editing host of the second." |
michael@0 | 788 | function inSameEditingHost(node1, node2) { |
michael@0 | 789 | return getEditingHostOf(node1) |
michael@0 | 790 | && getEditingHostOf(node1) == getEditingHostOf(node2); |
michael@0 | 791 | } |
michael@0 | 792 | |
michael@0 | 793 | // "A collapsed line break is a br that begins a line box which has nothing |
michael@0 | 794 | // else in it, and therefore has zero height." |
michael@0 | 795 | function isCollapsedLineBreak(br) { |
michael@0 | 796 | if (!isHtmlElement(br, "br")) { |
michael@0 | 797 | return false; |
michael@0 | 798 | } |
michael@0 | 799 | |
michael@0 | 800 | // Add a zwsp after it and see if that changes the height of the nearest |
michael@0 | 801 | // non-inline parent. Note: this is not actually reliable, because the |
michael@0 | 802 | // parent might have a fixed height or something. |
michael@0 | 803 | var ref = br.parentNode; |
michael@0 | 804 | while (getComputedStyle(ref).display == "inline") { |
michael@0 | 805 | ref = ref.parentNode; |
michael@0 | 806 | } |
michael@0 | 807 | var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; |
michael@0 | 808 | ref.style.height = "auto"; |
michael@0 | 809 | ref.style.maxHeight = "none"; |
michael@0 | 810 | ref.style.minHeight = "0"; |
michael@0 | 811 | var space = document.createTextNode("\u200b"); |
michael@0 | 812 | var origHeight = ref.offsetHeight; |
michael@0 | 813 | if (origHeight == 0) { |
michael@0 | 814 | throw "isCollapsedLineBreak: original height is zero, bug?"; |
michael@0 | 815 | } |
michael@0 | 816 | br.parentNode.insertBefore(space, br.nextSibling); |
michael@0 | 817 | var finalHeight = ref.offsetHeight; |
michael@0 | 818 | space.parentNode.removeChild(space); |
michael@0 | 819 | if (refStyle === null) { |
michael@0 | 820 | // Without the setAttribute() line, removeAttribute() doesn't work in |
michael@0 | 821 | // Chrome 14 dev. I have no idea why. |
michael@0 | 822 | ref.setAttribute("style", ""); |
michael@0 | 823 | ref.removeAttribute("style"); |
michael@0 | 824 | } else { |
michael@0 | 825 | ref.setAttribute("style", refStyle); |
michael@0 | 826 | } |
michael@0 | 827 | |
michael@0 | 828 | // Allow some leeway in case the zwsp didn't create a whole new line, but |
michael@0 | 829 | // only made an existing line slightly higher. Firefox 6.0a2 shows this |
michael@0 | 830 | // behavior when the first line is bold. |
michael@0 | 831 | return origHeight < finalHeight - 5; |
michael@0 | 832 | } |
michael@0 | 833 | |
michael@0 | 834 | // "An extraneous line break is a br that has no visual effect, in that |
michael@0 | 835 | // removing it from the DOM would not change layout, except that a br that is |
michael@0 | 836 | // the sole child of an li is not extraneous." |
michael@0 | 837 | // |
michael@0 | 838 | // FIXME: This doesn't work in IE, since IE ignores display: none in |
michael@0 | 839 | // contenteditable. |
michael@0 | 840 | function isExtraneousLineBreak(br) { |
michael@0 | 841 | if (!isHtmlElement(br, "br")) { |
michael@0 | 842 | return false; |
michael@0 | 843 | } |
michael@0 | 844 | |
michael@0 | 845 | if (isHtmlElement(br.parentNode, "li") |
michael@0 | 846 | && br.parentNode.childNodes.length == 1) { |
michael@0 | 847 | return false; |
michael@0 | 848 | } |
michael@0 | 849 | |
michael@0 | 850 | // Make the line break disappear and see if that changes the block's |
michael@0 | 851 | // height. Yes, this is an absurd hack. We have to reset height etc. on |
michael@0 | 852 | // the reference node because otherwise its height won't change if it's not |
michael@0 | 853 | // auto. |
michael@0 | 854 | var ref = br.parentNode; |
michael@0 | 855 | while (getComputedStyle(ref).display == "inline") { |
michael@0 | 856 | ref = ref.parentNode; |
michael@0 | 857 | } |
michael@0 | 858 | var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null; |
michael@0 | 859 | ref.style.height = "auto"; |
michael@0 | 860 | ref.style.maxHeight = "none"; |
michael@0 | 861 | ref.style.minHeight = "0"; |
michael@0 | 862 | var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null; |
michael@0 | 863 | var origHeight = ref.offsetHeight; |
michael@0 | 864 | if (origHeight == 0) { |
michael@0 | 865 | throw "isExtraneousLineBreak: original height is zero, bug?"; |
michael@0 | 866 | } |
michael@0 | 867 | br.setAttribute("style", "display:none"); |
michael@0 | 868 | var finalHeight = ref.offsetHeight; |
michael@0 | 869 | if (refStyle === null) { |
michael@0 | 870 | // Without the setAttribute() line, removeAttribute() doesn't work in |
michael@0 | 871 | // Chrome 14 dev. I have no idea why. |
michael@0 | 872 | ref.setAttribute("style", ""); |
michael@0 | 873 | ref.removeAttribute("style"); |
michael@0 | 874 | } else { |
michael@0 | 875 | ref.setAttribute("style", refStyle); |
michael@0 | 876 | } |
michael@0 | 877 | if (brStyle === null) { |
michael@0 | 878 | br.removeAttribute("style"); |
michael@0 | 879 | } else { |
michael@0 | 880 | br.setAttribute("style", brStyle); |
michael@0 | 881 | } |
michael@0 | 882 | |
michael@0 | 883 | return origHeight == finalHeight; |
michael@0 | 884 | } |
michael@0 | 885 | |
michael@0 | 886 | // "A whitespace node is either a Text node whose data is the empty string; or |
michael@0 | 887 | // a Text node whose data consists only of one or more tabs (0x0009), line |
michael@0 | 888 | // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose |
michael@0 | 889 | // parent is an Element whose resolved value for "white-space" is "normal" or |
michael@0 | 890 | // "nowrap"; or a Text node whose data consists only of one or more tabs |
michael@0 | 891 | // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose |
michael@0 | 892 | // parent is an Element whose resolved value for "white-space" is "pre-line"." |
michael@0 | 893 | function isWhitespaceNode(node) { |
michael@0 | 894 | return node |
michael@0 | 895 | && node.nodeType == Node.TEXT_NODE |
michael@0 | 896 | && (node.data == "" |
michael@0 | 897 | || ( |
michael@0 | 898 | /^[\t\n\r ]+$/.test(node.data) |
michael@0 | 899 | && node.parentNode |
michael@0 | 900 | && node.parentNode.nodeType == Node.ELEMENT_NODE |
michael@0 | 901 | && ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1 |
michael@0 | 902 | ) || ( |
michael@0 | 903 | /^[\t\r ]+$/.test(node.data) |
michael@0 | 904 | && node.parentNode |
michael@0 | 905 | && node.parentNode.nodeType == Node.ELEMENT_NODE |
michael@0 | 906 | && getComputedStyle(node.parentNode).whiteSpace == "pre-line" |
michael@0 | 907 | )); |
michael@0 | 908 | } |
michael@0 | 909 | |
michael@0 | 910 | // "node is a collapsed whitespace node if the following algorithm returns |
michael@0 | 911 | // true:" |
michael@0 | 912 | function isCollapsedWhitespaceNode(node) { |
michael@0 | 913 | // "If node is not a whitespace node, return false." |
michael@0 | 914 | if (!isWhitespaceNode(node)) { |
michael@0 | 915 | return false; |
michael@0 | 916 | } |
michael@0 | 917 | |
michael@0 | 918 | // "If node's data is the empty string, return true." |
michael@0 | 919 | if (node.data == "") { |
michael@0 | 920 | return true; |
michael@0 | 921 | } |
michael@0 | 922 | |
michael@0 | 923 | // "Let ancestor be node's parent." |
michael@0 | 924 | var ancestor = node.parentNode; |
michael@0 | 925 | |
michael@0 | 926 | // "If ancestor is null, return true." |
michael@0 | 927 | if (!ancestor) { |
michael@0 | 928 | return true; |
michael@0 | 929 | } |
michael@0 | 930 | |
michael@0 | 931 | // "If the "display" property of some ancestor of node has resolved value |
michael@0 | 932 | // "none", return true." |
michael@0 | 933 | if (getAncestors(node).some(function(ancestor) { |
michael@0 | 934 | return ancestor.nodeType == Node.ELEMENT_NODE |
michael@0 | 935 | && getComputedStyle(ancestor).display == "none"; |
michael@0 | 936 | })) { |
michael@0 | 937 | return true; |
michael@0 | 938 | } |
michael@0 | 939 | |
michael@0 | 940 | // "While ancestor is not a block node and its parent is not null, set |
michael@0 | 941 | // ancestor to its parent." |
michael@0 | 942 | while (!isBlockNode(ancestor) |
michael@0 | 943 | && ancestor.parentNode) { |
michael@0 | 944 | ancestor = ancestor.parentNode; |
michael@0 | 945 | } |
michael@0 | 946 | |
michael@0 | 947 | // "Let reference be node." |
michael@0 | 948 | var reference = node; |
michael@0 | 949 | |
michael@0 | 950 | // "While reference is a descendant of ancestor:" |
michael@0 | 951 | while (reference != ancestor) { |
michael@0 | 952 | // "Let reference be the node before it in tree order." |
michael@0 | 953 | reference = previousNode(reference); |
michael@0 | 954 | |
michael@0 | 955 | // "If reference is a block node or a br, return true." |
michael@0 | 956 | if (isBlockNode(reference) |
michael@0 | 957 | || isHtmlElement(reference, "br")) { |
michael@0 | 958 | return true; |
michael@0 | 959 | } |
michael@0 | 960 | |
michael@0 | 961 | // "If reference is a Text node that is not a whitespace node, or is an |
michael@0 | 962 | // img, break from this loop." |
michael@0 | 963 | if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) |
michael@0 | 964 | || isHtmlElement(reference, "img")) { |
michael@0 | 965 | break; |
michael@0 | 966 | } |
michael@0 | 967 | } |
michael@0 | 968 | |
michael@0 | 969 | // "Let reference be node." |
michael@0 | 970 | reference = node; |
michael@0 | 971 | |
michael@0 | 972 | // "While reference is a descendant of ancestor:" |
michael@0 | 973 | var stop = nextNodeDescendants(ancestor); |
michael@0 | 974 | while (reference != stop) { |
michael@0 | 975 | // "Let reference be the node after it in tree order, or null if there |
michael@0 | 976 | // is no such node." |
michael@0 | 977 | reference = nextNode(reference); |
michael@0 | 978 | |
michael@0 | 979 | // "If reference is a block node or a br, return true." |
michael@0 | 980 | if (isBlockNode(reference) |
michael@0 | 981 | || isHtmlElement(reference, "br")) { |
michael@0 | 982 | return true; |
michael@0 | 983 | } |
michael@0 | 984 | |
michael@0 | 985 | // "If reference is a Text node that is not a whitespace node, or is an |
michael@0 | 986 | // img, break from this loop." |
michael@0 | 987 | if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) |
michael@0 | 988 | || isHtmlElement(reference, "img")) { |
michael@0 | 989 | break; |
michael@0 | 990 | } |
michael@0 | 991 | } |
michael@0 | 992 | |
michael@0 | 993 | // "Return false." |
michael@0 | 994 | return false; |
michael@0 | 995 | } |
michael@0 | 996 | |
michael@0 | 997 | // "Something is visible if it is a node that either is a block node, or a Text |
michael@0 | 998 | // node that is not a collapsed whitespace node, or an img, or a br that is not |
michael@0 | 999 | // an extraneous line break, or any node with a visible descendant; excluding |
michael@0 | 1000 | // any node with an ancestor container Element whose "display" property has |
michael@0 | 1001 | // resolved value "none"." |
michael@0 | 1002 | function isVisible(node) { |
michael@0 | 1003 | if (!node) { |
michael@0 | 1004 | return false; |
michael@0 | 1005 | } |
michael@0 | 1006 | |
michael@0 | 1007 | if (getAncestors(node).concat(node) |
michael@0 | 1008 | .filter(function(node) { return node.nodeType == Node.ELEMENT_NODE }) |
michael@0 | 1009 | .some(function(node) { return getComputedStyle(node).display == "none" })) { |
michael@0 | 1010 | return false; |
michael@0 | 1011 | } |
michael@0 | 1012 | |
michael@0 | 1013 | if (isBlockNode(node) |
michael@0 | 1014 | || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node)) |
michael@0 | 1015 | || isHtmlElement(node, "img") |
michael@0 | 1016 | || (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) { |
michael@0 | 1017 | return true; |
michael@0 | 1018 | } |
michael@0 | 1019 | |
michael@0 | 1020 | for (var i = 0; i < node.childNodes.length; i++) { |
michael@0 | 1021 | if (isVisible(node.childNodes[i])) { |
michael@0 | 1022 | return true; |
michael@0 | 1023 | } |
michael@0 | 1024 | } |
michael@0 | 1025 | |
michael@0 | 1026 | return false; |
michael@0 | 1027 | } |
michael@0 | 1028 | |
michael@0 | 1029 | // "Something is invisible if it is a node that is not visible." |
michael@0 | 1030 | function isInvisible(node) { |
michael@0 | 1031 | return node && !isVisible(node); |
michael@0 | 1032 | } |
michael@0 | 1033 | |
michael@0 | 1034 | // "A collapsed block prop is either a collapsed line break that is not an |
michael@0 | 1035 | // extraneous line break, or an Element that is an inline node and whose |
michael@0 | 1036 | // children are all either invisible or collapsed block props and that has at |
michael@0 | 1037 | // least one child that is a collapsed block prop." |
michael@0 | 1038 | function isCollapsedBlockProp(node) { |
michael@0 | 1039 | if (isCollapsedLineBreak(node) |
michael@0 | 1040 | && !isExtraneousLineBreak(node)) { |
michael@0 | 1041 | return true; |
michael@0 | 1042 | } |
michael@0 | 1043 | |
michael@0 | 1044 | if (!isInlineNode(node) |
michael@0 | 1045 | || node.nodeType != Node.ELEMENT_NODE) { |
michael@0 | 1046 | return false; |
michael@0 | 1047 | } |
michael@0 | 1048 | |
michael@0 | 1049 | var hasCollapsedBlockPropChild = false; |
michael@0 | 1050 | for (var i = 0; i < node.childNodes.length; i++) { |
michael@0 | 1051 | if (!isInvisible(node.childNodes[i]) |
michael@0 | 1052 | && !isCollapsedBlockProp(node.childNodes[i])) { |
michael@0 | 1053 | return false; |
michael@0 | 1054 | } |
michael@0 | 1055 | if (isCollapsedBlockProp(node.childNodes[i])) { |
michael@0 | 1056 | hasCollapsedBlockPropChild = true; |
michael@0 | 1057 | } |
michael@0 | 1058 | } |
michael@0 | 1059 | |
michael@0 | 1060 | return hasCollapsedBlockPropChild; |
michael@0 | 1061 | } |
michael@0 | 1062 | |
michael@0 | 1063 | // "The active range is the range of the selection given by calling |
michael@0 | 1064 | // getSelection() on the context object. (Thus the active range may be null.)" |
michael@0 | 1065 | // |
michael@0 | 1066 | // We cheat and return globalRange if that's defined. We also ensure that the |
michael@0 | 1067 | // active range meets the requirements that selection boundary points are |
michael@0 | 1068 | // supposed to meet, i.e., that the nodes are both Text or Element nodes that |
michael@0 | 1069 | // descend from a Document. |
michael@0 | 1070 | function getActiveRange() { |
michael@0 | 1071 | var ret; |
michael@0 | 1072 | if (globalRange) { |
michael@0 | 1073 | ret = globalRange; |
michael@0 | 1074 | } else if (getSelection().rangeCount) { |
michael@0 | 1075 | ret = getSelection().getRangeAt(0); |
michael@0 | 1076 | } else { |
michael@0 | 1077 | return null; |
michael@0 | 1078 | } |
michael@0 | 1079 | if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1 |
michael@0 | 1080 | || [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1 |
michael@0 | 1081 | || !ret.startContainer.ownerDocument |
michael@0 | 1082 | || !ret.endContainer.ownerDocument |
michael@0 | 1083 | || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument) |
michael@0 | 1084 | || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) { |
michael@0 | 1085 | throw "Invalid active range; test bug?"; |
michael@0 | 1086 | } |
michael@0 | 1087 | return ret; |
michael@0 | 1088 | } |
michael@0 | 1089 | |
michael@0 | 1090 | // "For some commands, each HTMLDocument must have a boolean state override |
michael@0 | 1091 | // and/or a string value override. These do not change the command's state or |
michael@0 | 1092 | // value, but change the way some algorithms behave, as specified in those |
michael@0 | 1093 | // algorithms' definitions. Initially, both must be unset for every command. |
michael@0 | 1094 | // Whenever the number of ranges in the Selection changes to something |
michael@0 | 1095 | // different, and whenever a boundary point of the range at a given index in |
michael@0 | 1096 | // the Selection changes to something different, the state override and value |
michael@0 | 1097 | // override must be unset for every command." |
michael@0 | 1098 | // |
michael@0 | 1099 | // We implement this crudely by using setters and getters. To verify that the |
michael@0 | 1100 | // selection hasn't changed, we copy the active range and just check the |
michael@0 | 1101 | // endpoints match. This isn't really correct, but it's good enough for us. |
michael@0 | 1102 | // Unset state/value overrides are undefined. We put everything in a function |
michael@0 | 1103 | // so no one can access anything except via the provided functions, since |
michael@0 | 1104 | // otherwise callers might mistakenly use outdated overrides (if the selection |
michael@0 | 1105 | // has changed). |
michael@0 | 1106 | var getStateOverride, setStateOverride, unsetStateOverride, |
michael@0 | 1107 | getValueOverride, setValueOverride, unsetValueOverride; |
michael@0 | 1108 | (function() { |
michael@0 | 1109 | var stateOverrides = {}; |
michael@0 | 1110 | var valueOverrides = {}; |
michael@0 | 1111 | var storedRange = null; |
michael@0 | 1112 | |
michael@0 | 1113 | function resetOverrides() { |
michael@0 | 1114 | if (!storedRange |
michael@0 | 1115 | || storedRange.startContainer != getActiveRange().startContainer |
michael@0 | 1116 | || storedRange.endContainer != getActiveRange().endContainer |
michael@0 | 1117 | || storedRange.startOffset != getActiveRange().startOffset |
michael@0 | 1118 | || storedRange.endOffset != getActiveRange().endOffset) { |
michael@0 | 1119 | stateOverrides = {}; |
michael@0 | 1120 | valueOverrides = {}; |
michael@0 | 1121 | storedRange = getActiveRange().cloneRange(); |
michael@0 | 1122 | } |
michael@0 | 1123 | } |
michael@0 | 1124 | |
michael@0 | 1125 | getStateOverride = function(command) { |
michael@0 | 1126 | resetOverrides(); |
michael@0 | 1127 | return stateOverrides[command]; |
michael@0 | 1128 | }; |
michael@0 | 1129 | |
michael@0 | 1130 | setStateOverride = function(command, newState) { |
michael@0 | 1131 | resetOverrides(); |
michael@0 | 1132 | stateOverrides[command] = newState; |
michael@0 | 1133 | }; |
michael@0 | 1134 | |
michael@0 | 1135 | unsetStateOverride = function(command) { |
michael@0 | 1136 | resetOverrides(); |
michael@0 | 1137 | delete stateOverrides[command]; |
michael@0 | 1138 | } |
michael@0 | 1139 | |
michael@0 | 1140 | getValueOverride = function(command) { |
michael@0 | 1141 | resetOverrides(); |
michael@0 | 1142 | return valueOverrides[command]; |
michael@0 | 1143 | } |
michael@0 | 1144 | |
michael@0 | 1145 | // "The value override for the backColor command must be the same as the |
michael@0 | 1146 | // value override for the hiliteColor command, such that setting one sets |
michael@0 | 1147 | // the other to the same thing and unsetting one unsets the other." |
michael@0 | 1148 | setValueOverride = function(command, newValue) { |
michael@0 | 1149 | resetOverrides(); |
michael@0 | 1150 | valueOverrides[command] = newValue; |
michael@0 | 1151 | if (command == "backcolor") { |
michael@0 | 1152 | valueOverrides.hilitecolor = newValue; |
michael@0 | 1153 | } else if (command == "hilitecolor") { |
michael@0 | 1154 | valueOverrides.backcolor = newValue; |
michael@0 | 1155 | } |
michael@0 | 1156 | } |
michael@0 | 1157 | |
michael@0 | 1158 | unsetValueOverride = function(command) { |
michael@0 | 1159 | resetOverrides(); |
michael@0 | 1160 | delete valueOverrides[command]; |
michael@0 | 1161 | if (command == "backcolor") { |
michael@0 | 1162 | delete valueOverrides.hilitecolor; |
michael@0 | 1163 | } else if (command == "hilitecolor") { |
michael@0 | 1164 | delete valueOverrides.backcolor; |
michael@0 | 1165 | } |
michael@0 | 1166 | } |
michael@0 | 1167 | })(); |
michael@0 | 1168 | |
michael@0 | 1169 | //@} |
michael@0 | 1170 | |
michael@0 | 1171 | ///////////////////////////// |
michael@0 | 1172 | ///// Common algorithms ///// |
michael@0 | 1173 | ///////////////////////////// |
michael@0 | 1174 | |
michael@0 | 1175 | ///// Assorted common algorithms ///// |
michael@0 | 1176 | //@{ |
michael@0 | 1177 | |
michael@0 | 1178 | // Magic array of extra ranges whose endpoints we want to preserve. |
michael@0 | 1179 | var extraRanges = []; |
michael@0 | 1180 | |
michael@0 | 1181 | function movePreservingRanges(node, newParent, newIndex) { |
michael@0 | 1182 | // For convenience, I allow newIndex to be -1 to mean "insert at the end". |
michael@0 | 1183 | if (newIndex == -1) { |
michael@0 | 1184 | newIndex = newParent.childNodes.length; |
michael@0 | 1185 | } |
michael@0 | 1186 | |
michael@0 | 1187 | // "When the user agent is to move a Node to a new location, preserving |
michael@0 | 1188 | // ranges, it must remove the Node from its original parent (if any), then |
michael@0 | 1189 | // insert it in the new location. In doing so, however, it must ignore the |
michael@0 | 1190 | // regular range mutation rules, and instead follow these rules:" |
michael@0 | 1191 | |
michael@0 | 1192 | // "Let node be the moved Node, old parent and old index be the old parent |
michael@0 | 1193 | // (which may be null) and index, and new parent and new index be the new |
michael@0 | 1194 | // parent and index." |
michael@0 | 1195 | var oldParent = node.parentNode; |
michael@0 | 1196 | var oldIndex = getNodeIndex(node); |
michael@0 | 1197 | |
michael@0 | 1198 | // We preserve the global range object, the ranges in the selection, and |
michael@0 | 1199 | // any range that's in the extraRanges array. Any other ranges won't get |
michael@0 | 1200 | // updated, because we have no references to them. |
michael@0 | 1201 | var ranges = [globalRange].concat(extraRanges); |
michael@0 | 1202 | for (var i = 0; i < getSelection().rangeCount; i++) { |
michael@0 | 1203 | ranges.push(getSelection().getRangeAt(i)); |
michael@0 | 1204 | } |
michael@0 | 1205 | var boundaryPoints = []; |
michael@0 | 1206 | ranges.forEach(function(range) { |
michael@0 | 1207 | boundaryPoints.push([range.startContainer, range.startOffset]); |
michael@0 | 1208 | boundaryPoints.push([range.endContainer, range.endOffset]); |
michael@0 | 1209 | }); |
michael@0 | 1210 | |
michael@0 | 1211 | boundaryPoints.forEach(function(boundaryPoint) { |
michael@0 | 1212 | // "If a boundary point's node is the same as or a descendant of node, |
michael@0 | 1213 | // leave it unchanged, so it moves to the new location." |
michael@0 | 1214 | // |
michael@0 | 1215 | // No modifications necessary. |
michael@0 | 1216 | |
michael@0 | 1217 | // "If a boundary point's node is new parent and its offset is greater |
michael@0 | 1218 | // than new index, add one to its offset." |
michael@0 | 1219 | if (boundaryPoint[0] == newParent |
michael@0 | 1220 | && boundaryPoint[1] > newIndex) { |
michael@0 | 1221 | boundaryPoint[1]++; |
michael@0 | 1222 | } |
michael@0 | 1223 | |
michael@0 | 1224 | // "If a boundary point's node is old parent and its offset is old index or |
michael@0 | 1225 | // old index + 1, set its node to new parent and add new index − old index |
michael@0 | 1226 | // to its offset." |
michael@0 | 1227 | if (boundaryPoint[0] == oldParent |
michael@0 | 1228 | && (boundaryPoint[1] == oldIndex |
michael@0 | 1229 | || boundaryPoint[1] == oldIndex + 1)) { |
michael@0 | 1230 | boundaryPoint[0] = newParent; |
michael@0 | 1231 | boundaryPoint[1] += newIndex - oldIndex; |
michael@0 | 1232 | } |
michael@0 | 1233 | |
michael@0 | 1234 | // "If a boundary point's node is old parent and its offset is greater than |
michael@0 | 1235 | // old index + 1, subtract one from its offset." |
michael@0 | 1236 | if (boundaryPoint[0] == oldParent |
michael@0 | 1237 | && boundaryPoint[1] > oldIndex + 1) { |
michael@0 | 1238 | boundaryPoint[1]--; |
michael@0 | 1239 | } |
michael@0 | 1240 | }); |
michael@0 | 1241 | |
michael@0 | 1242 | // Now actually move it and preserve the ranges. |
michael@0 | 1243 | if (newParent.childNodes.length == newIndex) { |
michael@0 | 1244 | newParent.appendChild(node); |
michael@0 | 1245 | } else { |
michael@0 | 1246 | newParent.insertBefore(node, newParent.childNodes[newIndex]); |
michael@0 | 1247 | } |
michael@0 | 1248 | |
michael@0 | 1249 | globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]); |
michael@0 | 1250 | globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]); |
michael@0 | 1251 | |
michael@0 | 1252 | for (var i = 0; i < extraRanges.length; i++) { |
michael@0 | 1253 | extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]); |
michael@0 | 1254 | extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]); |
michael@0 | 1255 | } |
michael@0 | 1256 | |
michael@0 | 1257 | getSelection().removeAllRanges(); |
michael@0 | 1258 | for (var i = 1 + extraRanges.length; i < ranges.length; i++) { |
michael@0 | 1259 | var newRange = document.createRange(); |
michael@0 | 1260 | newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]); |
michael@0 | 1261 | newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]); |
michael@0 | 1262 | getSelection().addRange(newRange); |
michael@0 | 1263 | } |
michael@0 | 1264 | } |
michael@0 | 1265 | |
michael@0 | 1266 | function setTagName(element, newName) { |
michael@0 | 1267 | // "If element is an HTML element with local name equal to new name, return |
michael@0 | 1268 | // element." |
michael@0 | 1269 | if (isHtmlElement(element, newName.toUpperCase())) { |
michael@0 | 1270 | return element; |
michael@0 | 1271 | } |
michael@0 | 1272 | |
michael@0 | 1273 | // "If element's parent is null, return element." |
michael@0 | 1274 | if (!element.parentNode) { |
michael@0 | 1275 | return element; |
michael@0 | 1276 | } |
michael@0 | 1277 | |
michael@0 | 1278 | // "Let replacement element be the result of calling createElement(new |
michael@0 | 1279 | // name) on the ownerDocument of element." |
michael@0 | 1280 | var replacementElement = element.ownerDocument.createElement(newName); |
michael@0 | 1281 | |
michael@0 | 1282 | // "Insert replacement element into element's parent immediately before |
michael@0 | 1283 | // element." |
michael@0 | 1284 | element.parentNode.insertBefore(replacementElement, element); |
michael@0 | 1285 | |
michael@0 | 1286 | // "Copy all attributes of element to replacement element, in order." |
michael@0 | 1287 | for (var i = 0; i < element.attributes.length; i++) { |
michael@0 | 1288 | replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value); |
michael@0 | 1289 | } |
michael@0 | 1290 | |
michael@0 | 1291 | // "While element has children, append the first child of element as the |
michael@0 | 1292 | // last child of replacement element, preserving ranges." |
michael@0 | 1293 | while (element.childNodes.length) { |
michael@0 | 1294 | movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length); |
michael@0 | 1295 | } |
michael@0 | 1296 | |
michael@0 | 1297 | // "Remove element from its parent." |
michael@0 | 1298 | element.parentNode.removeChild(element); |
michael@0 | 1299 | |
michael@0 | 1300 | // "Return replacement element." |
michael@0 | 1301 | return replacementElement; |
michael@0 | 1302 | } |
michael@0 | 1303 | |
michael@0 | 1304 | function removeExtraneousLineBreaksBefore(node) { |
michael@0 | 1305 | // "Let ref be the previousSibling of node." |
michael@0 | 1306 | var ref = node.previousSibling; |
michael@0 | 1307 | |
michael@0 | 1308 | // "If ref is null, abort these steps." |
michael@0 | 1309 | if (!ref) { |
michael@0 | 1310 | return; |
michael@0 | 1311 | } |
michael@0 | 1312 | |
michael@0 | 1313 | // "While ref has children, set ref to its lastChild." |
michael@0 | 1314 | while (ref.hasChildNodes()) { |
michael@0 | 1315 | ref = ref.lastChild; |
michael@0 | 1316 | } |
michael@0 | 1317 | |
michael@0 | 1318 | // "While ref is invisible but not an extraneous line break, and ref does |
michael@0 | 1319 | // not equal node's parent, set ref to the node before it in tree order." |
michael@0 | 1320 | while (isInvisible(ref) |
michael@0 | 1321 | && !isExtraneousLineBreak(ref) |
michael@0 | 1322 | && ref != node.parentNode) { |
michael@0 | 1323 | ref = previousNode(ref); |
michael@0 | 1324 | } |
michael@0 | 1325 | |
michael@0 | 1326 | // "If ref is an editable extraneous line break, remove it from its |
michael@0 | 1327 | // parent." |
michael@0 | 1328 | if (isEditable(ref) |
michael@0 | 1329 | && isExtraneousLineBreak(ref)) { |
michael@0 | 1330 | ref.parentNode.removeChild(ref); |
michael@0 | 1331 | } |
michael@0 | 1332 | } |
michael@0 | 1333 | |
michael@0 | 1334 | function removeExtraneousLineBreaksAtTheEndOf(node) { |
michael@0 | 1335 | // "Let ref be node." |
michael@0 | 1336 | var ref = node; |
michael@0 | 1337 | |
michael@0 | 1338 | // "While ref has children, set ref to its lastChild." |
michael@0 | 1339 | while (ref.hasChildNodes()) { |
michael@0 | 1340 | ref = ref.lastChild; |
michael@0 | 1341 | } |
michael@0 | 1342 | |
michael@0 | 1343 | // "While ref is invisible but not an extraneous line break, and ref does |
michael@0 | 1344 | // not equal node, set ref to the node before it in tree order." |
michael@0 | 1345 | while (isInvisible(ref) |
michael@0 | 1346 | && !isExtraneousLineBreak(ref) |
michael@0 | 1347 | && ref != node) { |
michael@0 | 1348 | ref = previousNode(ref); |
michael@0 | 1349 | } |
michael@0 | 1350 | |
michael@0 | 1351 | // "If ref is an editable extraneous line break:" |
michael@0 | 1352 | if (isEditable(ref) |
michael@0 | 1353 | && isExtraneousLineBreak(ref)) { |
michael@0 | 1354 | // "While ref's parent is editable and invisible, set ref to its |
michael@0 | 1355 | // parent." |
michael@0 | 1356 | while (isEditable(ref.parentNode) |
michael@0 | 1357 | && isInvisible(ref.parentNode)) { |
michael@0 | 1358 | ref = ref.parentNode; |
michael@0 | 1359 | } |
michael@0 | 1360 | |
michael@0 | 1361 | // "Remove ref from its parent." |
michael@0 | 1362 | ref.parentNode.removeChild(ref); |
michael@0 | 1363 | } |
michael@0 | 1364 | } |
michael@0 | 1365 | |
michael@0 | 1366 | // "To remove extraneous line breaks from a node, first remove extraneous line |
michael@0 | 1367 | // breaks before it, then remove extraneous line breaks at the end of it." |
michael@0 | 1368 | function removeExtraneousLineBreaksFrom(node) { |
michael@0 | 1369 | removeExtraneousLineBreaksBefore(node); |
michael@0 | 1370 | removeExtraneousLineBreaksAtTheEndOf(node); |
michael@0 | 1371 | } |
michael@0 | 1372 | |
michael@0 | 1373 | //@} |
michael@0 | 1374 | ///// Wrapping a list of nodes ///// |
michael@0 | 1375 | //@{ |
michael@0 | 1376 | |
michael@0 | 1377 | function wrap(nodeList, siblingCriteria, newParentInstructions) { |
michael@0 | 1378 | // "If not provided, sibling criteria returns false and new parent |
michael@0 | 1379 | // instructions returns null." |
michael@0 | 1380 | if (typeof siblingCriteria == "undefined") { |
michael@0 | 1381 | siblingCriteria = function() { return false }; |
michael@0 | 1382 | } |
michael@0 | 1383 | if (typeof newParentInstructions == "undefined") { |
michael@0 | 1384 | newParentInstructions = function() { return null }; |
michael@0 | 1385 | } |
michael@0 | 1386 | |
michael@0 | 1387 | // "If every member of node list is invisible, and none is a br, return |
michael@0 | 1388 | // null and abort these steps." |
michael@0 | 1389 | if (nodeList.every(isInvisible) |
michael@0 | 1390 | && !nodeList.some(function(node) { return isHtmlElement(node, "br") })) { |
michael@0 | 1391 | return null; |
michael@0 | 1392 | } |
michael@0 | 1393 | |
michael@0 | 1394 | // "If node list's first member's parent is null, return null and abort |
michael@0 | 1395 | // these steps." |
michael@0 | 1396 | if (!nodeList[0].parentNode) { |
michael@0 | 1397 | return null; |
michael@0 | 1398 | } |
michael@0 | 1399 | |
michael@0 | 1400 | // "If node list's last member is an inline node that's not a br, and node |
michael@0 | 1401 | // list's last member's nextSibling is a br, append that br to node list." |
michael@0 | 1402 | if (isInlineNode(nodeList[nodeList.length - 1]) |
michael@0 | 1403 | && !isHtmlElement(nodeList[nodeList.length - 1], "br") |
michael@0 | 1404 | && isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) { |
michael@0 | 1405 | nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
michael@0 | 1406 | } |
michael@0 | 1407 | |
michael@0 | 1408 | // "While node list's first member's previousSibling is invisible, prepend |
michael@0 | 1409 | // it to node list." |
michael@0 | 1410 | while (isInvisible(nodeList[0].previousSibling)) { |
michael@0 | 1411 | nodeList.unshift(nodeList[0].previousSibling); |
michael@0 | 1412 | } |
michael@0 | 1413 | |
michael@0 | 1414 | // "While node list's last member's nextSibling is invisible, append it to |
michael@0 | 1415 | // node list." |
michael@0 | 1416 | while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) { |
michael@0 | 1417 | nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
michael@0 | 1418 | } |
michael@0 | 1419 | |
michael@0 | 1420 | // "If the previousSibling of the first member of node list is editable and |
michael@0 | 1421 | // running sibling criteria on it returns true, let new parent be the |
michael@0 | 1422 | // previousSibling of the first member of node list." |
michael@0 | 1423 | var newParent; |
michael@0 | 1424 | if (isEditable(nodeList[0].previousSibling) |
michael@0 | 1425 | && siblingCriteria(nodeList[0].previousSibling)) { |
michael@0 | 1426 | newParent = nodeList[0].previousSibling; |
michael@0 | 1427 | |
michael@0 | 1428 | // "Otherwise, if the nextSibling of the last member of node list is |
michael@0 | 1429 | // editable and running sibling criteria on it returns true, let new parent |
michael@0 | 1430 | // be the nextSibling of the last member of node list." |
michael@0 | 1431 | } else if (isEditable(nodeList[nodeList.length - 1].nextSibling) |
michael@0 | 1432 | && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) { |
michael@0 | 1433 | newParent = nodeList[nodeList.length - 1].nextSibling; |
michael@0 | 1434 | |
michael@0 | 1435 | // "Otherwise, run new parent instructions, and let new parent be the |
michael@0 | 1436 | // result." |
michael@0 | 1437 | } else { |
michael@0 | 1438 | newParent = newParentInstructions(); |
michael@0 | 1439 | } |
michael@0 | 1440 | |
michael@0 | 1441 | // "If new parent is null, abort these steps and return null." |
michael@0 | 1442 | if (!newParent) { |
michael@0 | 1443 | return null; |
michael@0 | 1444 | } |
michael@0 | 1445 | |
michael@0 | 1446 | // "If new parent's parent is null:" |
michael@0 | 1447 | if (!newParent.parentNode) { |
michael@0 | 1448 | // "Insert new parent into the parent of the first member of node list |
michael@0 | 1449 | // immediately before the first member of node list." |
michael@0 | 1450 | nodeList[0].parentNode.insertBefore(newParent, nodeList[0]); |
michael@0 | 1451 | |
michael@0 | 1452 | // "If any range has a boundary point with node equal to the parent of |
michael@0 | 1453 | // new parent and offset equal to the index of new parent, add one to |
michael@0 | 1454 | // that boundary point's offset." |
michael@0 | 1455 | // |
michael@0 | 1456 | // Only try to fix the global range. |
michael@0 | 1457 | if (globalRange.startContainer == newParent.parentNode |
michael@0 | 1458 | && globalRange.startOffset == getNodeIndex(newParent)) { |
michael@0 | 1459 | globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1); |
michael@0 | 1460 | } |
michael@0 | 1461 | if (globalRange.endContainer == newParent.parentNode |
michael@0 | 1462 | && globalRange.endOffset == getNodeIndex(newParent)) { |
michael@0 | 1463 | globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1); |
michael@0 | 1464 | } |
michael@0 | 1465 | } |
michael@0 | 1466 | |
michael@0 | 1467 | // "Let original parent be the parent of the first member of node list." |
michael@0 | 1468 | var originalParent = nodeList[0].parentNode; |
michael@0 | 1469 | |
michael@0 | 1470 | // "If new parent is before the first member of node list in tree order:" |
michael@0 | 1471 | if (isBefore(newParent, nodeList[0])) { |
michael@0 | 1472 | // "If new parent is not an inline node, but the last visible child of |
michael@0 | 1473 | // new parent and the first visible member of node list are both inline |
michael@0 | 1474 | // nodes, and the last child of new parent is not a br, call |
michael@0 | 1475 | // createElement("br") on the ownerDocument of new parent and append |
michael@0 | 1476 | // the result as the last child of new parent." |
michael@0 | 1477 | if (!isInlineNode(newParent) |
michael@0 | 1478 | && isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0]) |
michael@0 | 1479 | && isInlineNode(nodeList.filter(isVisible)[0]) |
michael@0 | 1480 | && !isHtmlElement(newParent.lastChild, "BR")) { |
michael@0 | 1481 | newParent.appendChild(newParent.ownerDocument.createElement("br")); |
michael@0 | 1482 | } |
michael@0 | 1483 | |
michael@0 | 1484 | // "For each node in node list, append node as the last child of new |
michael@0 | 1485 | // parent, preserving ranges." |
michael@0 | 1486 | for (var i = 0; i < nodeList.length; i++) { |
michael@0 | 1487 | movePreservingRanges(nodeList[i], newParent, -1); |
michael@0 | 1488 | } |
michael@0 | 1489 | |
michael@0 | 1490 | // "Otherwise:" |
michael@0 | 1491 | } else { |
michael@0 | 1492 | // "If new parent is not an inline node, but the first visible child of |
michael@0 | 1493 | // new parent and the last visible member of node list are both inline |
michael@0 | 1494 | // nodes, and the last member of node list is not a br, call |
michael@0 | 1495 | // createElement("br") on the ownerDocument of new parent and insert |
michael@0 | 1496 | // the result as the first child of new parent." |
michael@0 | 1497 | if (!isInlineNode(newParent) |
michael@0 | 1498 | && isInlineNode([].filter.call(newParent.childNodes, isVisible)[0]) |
michael@0 | 1499 | && isInlineNode(nodeList.filter(isVisible).slice(-1)[0]) |
michael@0 | 1500 | && !isHtmlElement(nodeList[nodeList.length - 1], "BR")) { |
michael@0 | 1501 | newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild); |
michael@0 | 1502 | } |
michael@0 | 1503 | |
michael@0 | 1504 | // "For each node in node list, in reverse order, insert node as the |
michael@0 | 1505 | // first child of new parent, preserving ranges." |
michael@0 | 1506 | for (var i = nodeList.length - 1; i >= 0; i--) { |
michael@0 | 1507 | movePreservingRanges(nodeList[i], newParent, 0); |
michael@0 | 1508 | } |
michael@0 | 1509 | } |
michael@0 | 1510 | |
michael@0 | 1511 | // "If original parent is editable and has no children, remove it from its |
michael@0 | 1512 | // parent." |
michael@0 | 1513 | if (isEditable(originalParent) && !originalParent.hasChildNodes()) { |
michael@0 | 1514 | originalParent.parentNode.removeChild(originalParent); |
michael@0 | 1515 | } |
michael@0 | 1516 | |
michael@0 | 1517 | // "If new parent's nextSibling is editable and running sibling criteria on |
michael@0 | 1518 | // it returns true:" |
michael@0 | 1519 | if (isEditable(newParent.nextSibling) |
michael@0 | 1520 | && siblingCriteria(newParent.nextSibling)) { |
michael@0 | 1521 | // "If new parent is not an inline node, but new parent's last child |
michael@0 | 1522 | // and new parent's nextSibling's first child are both inline nodes, |
michael@0 | 1523 | // and new parent's last child is not a br, call createElement("br") on |
michael@0 | 1524 | // the ownerDocument of new parent and append the result as the last |
michael@0 | 1525 | // child of new parent." |
michael@0 | 1526 | if (!isInlineNode(newParent) |
michael@0 | 1527 | && isInlineNode(newParent.lastChild) |
michael@0 | 1528 | && isInlineNode(newParent.nextSibling.firstChild) |
michael@0 | 1529 | && !isHtmlElement(newParent.lastChild, "BR")) { |
michael@0 | 1530 | newParent.appendChild(newParent.ownerDocument.createElement("br")); |
michael@0 | 1531 | } |
michael@0 | 1532 | |
michael@0 | 1533 | // "While new parent's nextSibling has children, append its first child |
michael@0 | 1534 | // as the last child of new parent, preserving ranges." |
michael@0 | 1535 | while (newParent.nextSibling.hasChildNodes()) { |
michael@0 | 1536 | movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1); |
michael@0 | 1537 | } |
michael@0 | 1538 | |
michael@0 | 1539 | // "Remove new parent's nextSibling from its parent." |
michael@0 | 1540 | newParent.parentNode.removeChild(newParent.nextSibling); |
michael@0 | 1541 | } |
michael@0 | 1542 | |
michael@0 | 1543 | // "Remove extraneous line breaks from new parent." |
michael@0 | 1544 | removeExtraneousLineBreaksFrom(newParent); |
michael@0 | 1545 | |
michael@0 | 1546 | // "Return new parent." |
michael@0 | 1547 | return newParent; |
michael@0 | 1548 | } |
michael@0 | 1549 | |
michael@0 | 1550 | |
michael@0 | 1551 | //@} |
michael@0 | 1552 | ///// Allowed children ///// |
michael@0 | 1553 | //@{ |
michael@0 | 1554 | |
michael@0 | 1555 | // "A name of an element with inline contents is "a", "abbr", "b", "bdi", |
michael@0 | 1556 | // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", |
michael@0 | 1557 | // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", |
michael@0 | 1558 | // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", |
michael@0 | 1559 | // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"." |
michael@0 | 1560 | var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo", |
michael@0 | 1561 | "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", |
michael@0 | 1562 | "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", |
michael@0 | 1563 | "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", |
michael@0 | 1564 | "xmp", "big", "blink", "font", "marquee", "nobr", "tt"]; |
michael@0 | 1565 | |
michael@0 | 1566 | // "An element with inline contents is an HTML element whose local name is a |
michael@0 | 1567 | // name of an element with inline contents." |
michael@0 | 1568 | function isElementWithInlineContents(node) { |
michael@0 | 1569 | return isHtmlElement(node, namesOfElementsWithInlineContents); |
michael@0 | 1570 | } |
michael@0 | 1571 | |
michael@0 | 1572 | function isAllowedChild(child, parent_) { |
michael@0 | 1573 | // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or |
michael@0 | 1574 | // an HTML element with local name equal to one of those, and child is a |
michael@0 | 1575 | // Text node whose data does not consist solely of space characters, return |
michael@0 | 1576 | // false." |
michael@0 | 1577 | if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1 |
michael@0 | 1578 | || isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"])) |
michael@0 | 1579 | && typeof child == "object" |
michael@0 | 1580 | && child.nodeType == Node.TEXT_NODE |
michael@0 | 1581 | && !/^[ \t\n\f\r]*$/.test(child.data)) { |
michael@0 | 1582 | return false; |
michael@0 | 1583 | } |
michael@0 | 1584 | |
michael@0 | 1585 | // "If parent is "script", "style", "plaintext", or "xmp", or an HTML |
michael@0 | 1586 | // element with local name equal to one of those, and child is not a Text |
michael@0 | 1587 | // node, return false." |
michael@0 | 1588 | if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1 |
michael@0 | 1589 | || isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"])) |
michael@0 | 1590 | && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) { |
michael@0 | 1591 | return false; |
michael@0 | 1592 | } |
michael@0 | 1593 | |
michael@0 | 1594 | // "If child is a Document, DocumentFragment, or DocumentType, return |
michael@0 | 1595 | // false." |
michael@0 | 1596 | if (typeof child == "object" |
michael@0 | 1597 | && (child.nodeType == Node.DOCUMENT_NODE |
michael@0 | 1598 | || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE |
michael@0 | 1599 | || child.nodeType == Node.DOCUMENT_TYPE_NODE)) { |
michael@0 | 1600 | return false; |
michael@0 | 1601 | } |
michael@0 | 1602 | |
michael@0 | 1603 | // "If child is an HTML element, set child to the local name of child." |
michael@0 | 1604 | if (isHtmlElement(child)) { |
michael@0 | 1605 | child = child.tagName.toLowerCase(); |
michael@0 | 1606 | } |
michael@0 | 1607 | |
michael@0 | 1608 | // "If child is not a string, return true." |
michael@0 | 1609 | if (typeof child != "string") { |
michael@0 | 1610 | return true; |
michael@0 | 1611 | } |
michael@0 | 1612 | |
michael@0 | 1613 | // "If parent is an HTML element:" |
michael@0 | 1614 | if (isHtmlElement(parent_)) { |
michael@0 | 1615 | // "If child is "a", and parent or some ancestor of parent is an a, |
michael@0 | 1616 | // return false." |
michael@0 | 1617 | // |
michael@0 | 1618 | // "If child is a prohibited paragraph child name and parent or some |
michael@0 | 1619 | // ancestor of parent is an element with inline contents, return |
michael@0 | 1620 | // false." |
michael@0 | 1621 | // |
michael@0 | 1622 | // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or |
michael@0 | 1623 | // some ancestor of parent is an HTML element with local name "h1", |
michael@0 | 1624 | // "h2", "h3", "h4", "h5", or "h6", return false." |
michael@0 | 1625 | var ancestor = parent_; |
michael@0 | 1626 | while (ancestor) { |
michael@0 | 1627 | if (child == "a" && isHtmlElement(ancestor, "a")) { |
michael@0 | 1628 | return false; |
michael@0 | 1629 | } |
michael@0 | 1630 | if (prohibitedParagraphChildNames.indexOf(child) != -1 |
michael@0 | 1631 | && isElementWithInlineContents(ancestor)) { |
michael@0 | 1632 | return false; |
michael@0 | 1633 | } |
michael@0 | 1634 | if (/^h[1-6]$/.test(child) |
michael@0 | 1635 | && isHtmlElement(ancestor) |
michael@0 | 1636 | && /^H[1-6]$/.test(ancestor.tagName)) { |
michael@0 | 1637 | return false; |
michael@0 | 1638 | } |
michael@0 | 1639 | ancestor = ancestor.parentNode; |
michael@0 | 1640 | } |
michael@0 | 1641 | |
michael@0 | 1642 | // "Let parent be the local name of parent." |
michael@0 | 1643 | parent_ = parent_.tagName.toLowerCase(); |
michael@0 | 1644 | } |
michael@0 | 1645 | |
michael@0 | 1646 | // "If parent is an Element or DocumentFragment, return true." |
michael@0 | 1647 | if (typeof parent_ == "object" |
michael@0 | 1648 | && (parent_.nodeType == Node.ELEMENT_NODE |
michael@0 | 1649 | || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) { |
michael@0 | 1650 | return true; |
michael@0 | 1651 | } |
michael@0 | 1652 | |
michael@0 | 1653 | // "If parent is not a string, return false." |
michael@0 | 1654 | if (typeof parent_ != "string") { |
michael@0 | 1655 | return false; |
michael@0 | 1656 | } |
michael@0 | 1657 | |
michael@0 | 1658 | // "If parent is on the left-hand side of an entry on the following list, |
michael@0 | 1659 | // then return true if child is listed on the right-hand side of that |
michael@0 | 1660 | // entry, and false otherwise." |
michael@0 | 1661 | switch (parent_) { |
michael@0 | 1662 | case "colgroup": |
michael@0 | 1663 | return child == "col"; |
michael@0 | 1664 | case "table": |
michael@0 | 1665 | return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1; |
michael@0 | 1666 | case "tbody": |
michael@0 | 1667 | case "thead": |
michael@0 | 1668 | case "tfoot": |
michael@0 | 1669 | return ["td", "th", "tr"].indexOf(child) != -1; |
michael@0 | 1670 | case "tr": |
michael@0 | 1671 | return ["td", "th"].indexOf(child) != -1; |
michael@0 | 1672 | case "dl": |
michael@0 | 1673 | return ["dt", "dd"].indexOf(child) != -1; |
michael@0 | 1674 | case "dir": |
michael@0 | 1675 | case "ol": |
michael@0 | 1676 | case "ul": |
michael@0 | 1677 | return ["dir", "li", "ol", "ul"].indexOf(child) != -1; |
michael@0 | 1678 | case "hgroup": |
michael@0 | 1679 | return /^h[1-6]$/.test(child); |
michael@0 | 1680 | } |
michael@0 | 1681 | |
michael@0 | 1682 | // "If child is "body", "caption", "col", "colgroup", "frame", "frameset", |
michael@0 | 1683 | // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return |
michael@0 | 1684 | // false." |
michael@0 | 1685 | if (["body", "caption", "col", "colgroup", "frame", "frameset", "head", |
michael@0 | 1686 | "html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) { |
michael@0 | 1687 | return false; |
michael@0 | 1688 | } |
michael@0 | 1689 | |
michael@0 | 1690 | // "If child is "dd" or "dt" and parent is not "dl", return false." |
michael@0 | 1691 | if (["dd", "dt"].indexOf(child) != -1 |
michael@0 | 1692 | && parent_ != "dl") { |
michael@0 | 1693 | return false; |
michael@0 | 1694 | } |
michael@0 | 1695 | |
michael@0 | 1696 | // "If child is "li" and parent is not "ol" or "ul", return false." |
michael@0 | 1697 | if (child == "li" |
michael@0 | 1698 | && parent_ != "ol" |
michael@0 | 1699 | && parent_ != "ul") { |
michael@0 | 1700 | return false; |
michael@0 | 1701 | } |
michael@0 | 1702 | |
michael@0 | 1703 | // "If parent is on the left-hand side of an entry on the following list |
michael@0 | 1704 | // and child is listed on the right-hand side of that entry, return false." |
michael@0 | 1705 | var table = [ |
michael@0 | 1706 | [["a"], ["a"]], |
michael@0 | 1707 | [["dd", "dt"], ["dd", "dt"]], |
michael@0 | 1708 | [["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]], |
michael@0 | 1709 | [["li"], ["li"]], |
michael@0 | 1710 | [["nobr"], ["nobr"]], |
michael@0 | 1711 | [namesOfElementsWithInlineContents, prohibitedParagraphChildNames], |
michael@0 | 1712 | [["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]], |
michael@0 | 1713 | ]; |
michael@0 | 1714 | for (var i = 0; i < table.length; i++) { |
michael@0 | 1715 | if (table[i][0].indexOf(parent_) != -1 |
michael@0 | 1716 | && table[i][1].indexOf(child) != -1) { |
michael@0 | 1717 | return false; |
michael@0 | 1718 | } |
michael@0 | 1719 | } |
michael@0 | 1720 | |
michael@0 | 1721 | // "Return true." |
michael@0 | 1722 | return true; |
michael@0 | 1723 | } |
michael@0 | 1724 | |
michael@0 | 1725 | |
michael@0 | 1726 | //@} |
michael@0 | 1727 | |
michael@0 | 1728 | ////////////////////////////////////// |
michael@0 | 1729 | ///// Inline formatting commands ///// |
michael@0 | 1730 | ////////////////////////////////////// |
michael@0 | 1731 | |
michael@0 | 1732 | ///// Inline formatting command definitions ///// |
michael@0 | 1733 | //@{ |
michael@0 | 1734 | |
michael@0 | 1735 | // "A node node is effectively contained in a range range if range is not |
michael@0 | 1736 | // collapsed, and at least one of the following holds:" |
michael@0 | 1737 | function isEffectivelyContained(node, range) { |
michael@0 | 1738 | if (range.collapsed) { |
michael@0 | 1739 | return false; |
michael@0 | 1740 | } |
michael@0 | 1741 | |
michael@0 | 1742 | // "node is contained in range." |
michael@0 | 1743 | if (isContained(node, range)) { |
michael@0 | 1744 | return true; |
michael@0 | 1745 | } |
michael@0 | 1746 | |
michael@0 | 1747 | // "node is range's start node, it is a Text node, and its length is |
michael@0 | 1748 | // different from range's start offset." |
michael@0 | 1749 | if (node == range.startContainer |
michael@0 | 1750 | && node.nodeType == Node.TEXT_NODE |
michael@0 | 1751 | && getNodeLength(node) != range.startOffset) { |
michael@0 | 1752 | return true; |
michael@0 | 1753 | } |
michael@0 | 1754 | |
michael@0 | 1755 | // "node is range's end node, it is a Text node, and range's end offset is |
michael@0 | 1756 | // not 0." |
michael@0 | 1757 | if (node == range.endContainer |
michael@0 | 1758 | && node.nodeType == Node.TEXT_NODE |
michael@0 | 1759 | && range.endOffset != 0) { |
michael@0 | 1760 | return true; |
michael@0 | 1761 | } |
michael@0 | 1762 | |
michael@0 | 1763 | // "node has at least one child; and all its children are effectively |
michael@0 | 1764 | // contained in range; and either range's start node is not a descendant of |
michael@0 | 1765 | // node or is not a Text node or range's start offset is zero; and either |
michael@0 | 1766 | // range's end node is not a descendant of node or is not a Text node or |
michael@0 | 1767 | // range's end offset is its end node's length." |
michael@0 | 1768 | if (node.hasChildNodes() |
michael@0 | 1769 | && [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) }) |
michael@0 | 1770 | && (!isDescendant(range.startContainer, node) |
michael@0 | 1771 | || range.startContainer.nodeType != Node.TEXT_NODE |
michael@0 | 1772 | || range.startOffset == 0) |
michael@0 | 1773 | && (!isDescendant(range.endContainer, node) |
michael@0 | 1774 | || range.endContainer.nodeType != Node.TEXT_NODE |
michael@0 | 1775 | || range.endOffset == getNodeLength(range.endContainer))) { |
michael@0 | 1776 | return true; |
michael@0 | 1777 | } |
michael@0 | 1778 | |
michael@0 | 1779 | return false; |
michael@0 | 1780 | } |
michael@0 | 1781 | |
michael@0 | 1782 | // Like get(All)ContainedNodes(), but for effectively contained nodes. |
michael@0 | 1783 | function getEffectivelyContainedNodes(range, condition) { |
michael@0 | 1784 | if (typeof condition == "undefined") { |
michael@0 | 1785 | condition = function() { return true }; |
michael@0 | 1786 | } |
michael@0 | 1787 | var node = range.startContainer; |
michael@0 | 1788 | while (isEffectivelyContained(node.parentNode, range)) { |
michael@0 | 1789 | node = node.parentNode; |
michael@0 | 1790 | } |
michael@0 | 1791 | |
michael@0 | 1792 | var stop = nextNodeDescendants(range.endContainer); |
michael@0 | 1793 | |
michael@0 | 1794 | var nodeList = []; |
michael@0 | 1795 | while (isBefore(node, stop)) { |
michael@0 | 1796 | if (isEffectivelyContained(node, range) |
michael@0 | 1797 | && condition(node)) { |
michael@0 | 1798 | nodeList.push(node); |
michael@0 | 1799 | node = nextNodeDescendants(node); |
michael@0 | 1800 | continue; |
michael@0 | 1801 | } |
michael@0 | 1802 | node = nextNode(node); |
michael@0 | 1803 | } |
michael@0 | 1804 | return nodeList; |
michael@0 | 1805 | } |
michael@0 | 1806 | |
michael@0 | 1807 | function getAllEffectivelyContainedNodes(range, condition) { |
michael@0 | 1808 | if (typeof condition == "undefined") { |
michael@0 | 1809 | condition = function() { return true }; |
michael@0 | 1810 | } |
michael@0 | 1811 | var node = range.startContainer; |
michael@0 | 1812 | while (isEffectivelyContained(node.parentNode, range)) { |
michael@0 | 1813 | node = node.parentNode; |
michael@0 | 1814 | } |
michael@0 | 1815 | |
michael@0 | 1816 | var stop = nextNodeDescendants(range.endContainer); |
michael@0 | 1817 | |
michael@0 | 1818 | var nodeList = []; |
michael@0 | 1819 | while (isBefore(node, stop)) { |
michael@0 | 1820 | if (isEffectivelyContained(node, range) |
michael@0 | 1821 | && condition(node)) { |
michael@0 | 1822 | nodeList.push(node); |
michael@0 | 1823 | } |
michael@0 | 1824 | node = nextNode(node); |
michael@0 | 1825 | } |
michael@0 | 1826 | return nodeList; |
michael@0 | 1827 | } |
michael@0 | 1828 | |
michael@0 | 1829 | // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element |
michael@0 | 1830 | // with no attributes except possibly style; or a font element with no |
michael@0 | 1831 | // attributes except possibly style, color, face, and/or size; or an a element |
michael@0 | 1832 | // with no attributes except possibly style and/or href." |
michael@0 | 1833 | function isModifiableElement(node) { |
michael@0 | 1834 | if (!isHtmlElement(node)) { |
michael@0 | 1835 | return false; |
michael@0 | 1836 | } |
michael@0 | 1837 | |
michael@0 | 1838 | if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) { |
michael@0 | 1839 | if (node.attributes.length == 0) { |
michael@0 | 1840 | return true; |
michael@0 | 1841 | } |
michael@0 | 1842 | |
michael@0 | 1843 | if (node.attributes.length == 1 |
michael@0 | 1844 | && node.hasAttribute("style")) { |
michael@0 | 1845 | return true; |
michael@0 | 1846 | } |
michael@0 | 1847 | } |
michael@0 | 1848 | |
michael@0 | 1849 | if (node.tagName == "FONT" || node.tagName == "A") { |
michael@0 | 1850 | var numAttrs = node.attributes.length; |
michael@0 | 1851 | |
michael@0 | 1852 | if (node.hasAttribute("style")) { |
michael@0 | 1853 | numAttrs--; |
michael@0 | 1854 | } |
michael@0 | 1855 | |
michael@0 | 1856 | if (node.tagName == "FONT") { |
michael@0 | 1857 | if (node.hasAttribute("color")) { |
michael@0 | 1858 | numAttrs--; |
michael@0 | 1859 | } |
michael@0 | 1860 | |
michael@0 | 1861 | if (node.hasAttribute("face")) { |
michael@0 | 1862 | numAttrs--; |
michael@0 | 1863 | } |
michael@0 | 1864 | |
michael@0 | 1865 | if (node.hasAttribute("size")) { |
michael@0 | 1866 | numAttrs--; |
michael@0 | 1867 | } |
michael@0 | 1868 | } |
michael@0 | 1869 | |
michael@0 | 1870 | if (node.tagName == "A" |
michael@0 | 1871 | && node.hasAttribute("href")) { |
michael@0 | 1872 | numAttrs--; |
michael@0 | 1873 | } |
michael@0 | 1874 | |
michael@0 | 1875 | if (numAttrs == 0) { |
michael@0 | 1876 | return true; |
michael@0 | 1877 | } |
michael@0 | 1878 | } |
michael@0 | 1879 | |
michael@0 | 1880 | return false; |
michael@0 | 1881 | } |
michael@0 | 1882 | |
michael@0 | 1883 | function isSimpleModifiableElement(node) { |
michael@0 | 1884 | // "A simple modifiable element is an HTML element for which at least one |
michael@0 | 1885 | // of the following holds:" |
michael@0 | 1886 | if (!isHtmlElement(node)) { |
michael@0 | 1887 | return false; |
michael@0 | 1888 | } |
michael@0 | 1889 | |
michael@0 | 1890 | // Only these elements can possibly be a simple modifiable element. |
michael@0 | 1891 | if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) { |
michael@0 | 1892 | return false; |
michael@0 | 1893 | } |
michael@0 | 1894 | |
michael@0 | 1895 | // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u |
michael@0 | 1896 | // element with no attributes." |
michael@0 | 1897 | if (node.attributes.length == 0) { |
michael@0 | 1898 | return true; |
michael@0 | 1899 | } |
michael@0 | 1900 | |
michael@0 | 1901 | // If it's got more than one attribute, everything after this fails. |
michael@0 | 1902 | if (node.attributes.length > 1) { |
michael@0 | 1903 | return false; |
michael@0 | 1904 | } |
michael@0 | 1905 | |
michael@0 | 1906 | // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u |
michael@0 | 1907 | // element with exactly one attribute, which is style, which sets no CSS |
michael@0 | 1908 | // properties (including invalid or unrecognized properties)." |
michael@0 | 1909 | // |
michael@0 | 1910 | // Not gonna try for invalid or unrecognized. |
michael@0 | 1911 | if (node.hasAttribute("style") |
michael@0 | 1912 | && node.style.length == 0) { |
michael@0 | 1913 | return true; |
michael@0 | 1914 | } |
michael@0 | 1915 | |
michael@0 | 1916 | // "It is an a element with exactly one attribute, which is href." |
michael@0 | 1917 | if (node.tagName == "A" |
michael@0 | 1918 | && node.hasAttribute("href")) { |
michael@0 | 1919 | return true; |
michael@0 | 1920 | } |
michael@0 | 1921 | |
michael@0 | 1922 | // "It is a font element with exactly one attribute, which is either color, |
michael@0 | 1923 | // face, or size." |
michael@0 | 1924 | if (node.tagName == "FONT" |
michael@0 | 1925 | && (node.hasAttribute("color") |
michael@0 | 1926 | || node.hasAttribute("face") |
michael@0 | 1927 | || node.hasAttribute("size") |
michael@0 | 1928 | )) { |
michael@0 | 1929 | return true; |
michael@0 | 1930 | } |
michael@0 | 1931 | |
michael@0 | 1932 | // "It is a b or strong element with exactly one attribute, which is style, |
michael@0 | 1933 | // and the style attribute sets exactly one CSS property (including invalid |
michael@0 | 1934 | // or unrecognized properties), which is "font-weight"." |
michael@0 | 1935 | if ((node.tagName == "B" || node.tagName == "STRONG") |
michael@0 | 1936 | && node.hasAttribute("style") |
michael@0 | 1937 | && node.style.length == 1 |
michael@0 | 1938 | && node.style.fontWeight != "") { |
michael@0 | 1939 | return true; |
michael@0 | 1940 | } |
michael@0 | 1941 | |
michael@0 | 1942 | // "It is an i or em element with exactly one attribute, which is style, |
michael@0 | 1943 | // and the style attribute sets exactly one CSS property (including invalid |
michael@0 | 1944 | // or unrecognized properties), which is "font-style"." |
michael@0 | 1945 | if ((node.tagName == "I" || node.tagName == "EM") |
michael@0 | 1946 | && node.hasAttribute("style") |
michael@0 | 1947 | && node.style.length == 1 |
michael@0 | 1948 | && node.style.fontStyle != "") { |
michael@0 | 1949 | return true; |
michael@0 | 1950 | } |
michael@0 | 1951 | |
michael@0 | 1952 | // "It is an a, font, or span element with exactly one attribute, which is |
michael@0 | 1953 | // style, and the style attribute sets exactly one CSS property (including |
michael@0 | 1954 | // invalid or unrecognized properties), and that property is not |
michael@0 | 1955 | // "text-decoration"." |
michael@0 | 1956 | if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN") |
michael@0 | 1957 | && node.hasAttribute("style") |
michael@0 | 1958 | && node.style.length == 1 |
michael@0 | 1959 | && node.style.textDecoration == "") { |
michael@0 | 1960 | return true; |
michael@0 | 1961 | } |
michael@0 | 1962 | |
michael@0 | 1963 | // "It is an a, font, s, span, strike, or u element with exactly one |
michael@0 | 1964 | // attribute, which is style, and the style attribute sets exactly one CSS |
michael@0 | 1965 | // property (including invalid or unrecognized properties), which is |
michael@0 | 1966 | // "text-decoration", which is set to "line-through" or "underline" or |
michael@0 | 1967 | // "overline" or "none"." |
michael@0 | 1968 | // |
michael@0 | 1969 | // The weird extra node.style.length check is for Firefox, which as of |
michael@0 | 1970 | // 8.0a2 has annoying and weird behavior here. |
michael@0 | 1971 | if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1 |
michael@0 | 1972 | && node.hasAttribute("style") |
michael@0 | 1973 | && (node.style.length == 1 |
michael@0 | 1974 | || (node.style.length == 4 |
michael@0 | 1975 | && "MozTextBlink" in node.style |
michael@0 | 1976 | && "MozTextDecorationColor" in node.style |
michael@0 | 1977 | && "MozTextDecorationLine" in node.style |
michael@0 | 1978 | && "MozTextDecorationStyle" in node.style) |
michael@0 | 1979 | ) |
michael@0 | 1980 | && (node.style.textDecoration == "line-through" |
michael@0 | 1981 | || node.style.textDecoration == "underline" |
michael@0 | 1982 | || node.style.textDecoration == "overline" |
michael@0 | 1983 | || node.style.textDecoration == "none")) { |
michael@0 | 1984 | return true; |
michael@0 | 1985 | } |
michael@0 | 1986 | |
michael@0 | 1987 | return false; |
michael@0 | 1988 | } |
michael@0 | 1989 | |
michael@0 | 1990 | // "A formattable node is an editable visible node that is either a Text node, |
michael@0 | 1991 | // an img, or a br." |
michael@0 | 1992 | function isFormattableNode(node) { |
michael@0 | 1993 | return isEditable(node) |
michael@0 | 1994 | && isVisible(node) |
michael@0 | 1995 | && (node.nodeType == Node.TEXT_NODE |
michael@0 | 1996 | || isHtmlElement(node, ["img", "br"])); |
michael@0 | 1997 | } |
michael@0 | 1998 | |
michael@0 | 1999 | // "Two quantities are equivalent values for a command if either both are null, |
michael@0 | 2000 | // or both are strings and they're equal and the command does not define any |
michael@0 | 2001 | // equivalent values, or both are strings and the command defines equivalent |
michael@0 | 2002 | // values and they match the definition." |
michael@0 | 2003 | function areEquivalentValues(command, val1, val2) { |
michael@0 | 2004 | if (val1 === null && val2 === null) { |
michael@0 | 2005 | return true; |
michael@0 | 2006 | } |
michael@0 | 2007 | |
michael@0 | 2008 | if (typeof val1 == "string" |
michael@0 | 2009 | && typeof val2 == "string" |
michael@0 | 2010 | && val1 == val2 |
michael@0 | 2011 | && !("equivalentValues" in commands[command])) { |
michael@0 | 2012 | return true; |
michael@0 | 2013 | } |
michael@0 | 2014 | |
michael@0 | 2015 | if (typeof val1 == "string" |
michael@0 | 2016 | && typeof val2 == "string" |
michael@0 | 2017 | && "equivalentValues" in commands[command] |
michael@0 | 2018 | && commands[command].equivalentValues(val1, val2)) { |
michael@0 | 2019 | return true; |
michael@0 | 2020 | } |
michael@0 | 2021 | |
michael@0 | 2022 | return false; |
michael@0 | 2023 | } |
michael@0 | 2024 | |
michael@0 | 2025 | // "Two quantities are loosely equivalent values for a command if either they |
michael@0 | 2026 | // are equivalent values for the command, or if the command is the fontSize |
michael@0 | 2027 | // command; one of the quantities is one of "x-small", "small", "medium", |
michael@0 | 2028 | // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is |
michael@0 | 2029 | // the resolved value of "font-size" on a font element whose size attribute has |
michael@0 | 2030 | // the corresponding value set ("1" through "7" respectively)." |
michael@0 | 2031 | function areLooselyEquivalentValues(command, val1, val2) { |
michael@0 | 2032 | if (areEquivalentValues(command, val1, val2)) { |
michael@0 | 2033 | return true; |
michael@0 | 2034 | } |
michael@0 | 2035 | |
michael@0 | 2036 | if (command != "fontsize" |
michael@0 | 2037 | || typeof val1 != "string" |
michael@0 | 2038 | || typeof val2 != "string") { |
michael@0 | 2039 | return false; |
michael@0 | 2040 | } |
michael@0 | 2041 | |
michael@0 | 2042 | // Static variables in JavaScript? |
michael@0 | 2043 | var callee = areLooselyEquivalentValues; |
michael@0 | 2044 | if (callee.sizeMap === undefined) { |
michael@0 | 2045 | callee.sizeMap = {}; |
michael@0 | 2046 | var font = document.createElement("font"); |
michael@0 | 2047 | document.body.appendChild(font); |
michael@0 | 2048 | ["x-small", "small", "medium", "large", "x-large", "xx-large", |
michael@0 | 2049 | "xxx-large"].forEach(function(keyword) { |
michael@0 | 2050 | font.size = cssSizeToLegacy(keyword); |
michael@0 | 2051 | callee.sizeMap[keyword] = getComputedStyle(font).fontSize; |
michael@0 | 2052 | }); |
michael@0 | 2053 | document.body.removeChild(font); |
michael@0 | 2054 | } |
michael@0 | 2055 | |
michael@0 | 2056 | return val1 === callee.sizeMap[val2] |
michael@0 | 2057 | || val2 === callee.sizeMap[val1]; |
michael@0 | 2058 | } |
michael@0 | 2059 | |
michael@0 | 2060 | //@} |
michael@0 | 2061 | ///// Assorted inline formatting command algorithms ///// |
michael@0 | 2062 | //@{ |
michael@0 | 2063 | |
michael@0 | 2064 | function getEffectiveCommandValue(node, command) { |
michael@0 | 2065 | // "If neither node nor its parent is an Element, return null." |
michael@0 | 2066 | if (node.nodeType != Node.ELEMENT_NODE |
michael@0 | 2067 | && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) { |
michael@0 | 2068 | return null; |
michael@0 | 2069 | } |
michael@0 | 2070 | |
michael@0 | 2071 | // "If node is not an Element, return the effective command value of its |
michael@0 | 2072 | // parent for command." |
michael@0 | 2073 | if (node.nodeType != Node.ELEMENT_NODE) { |
michael@0 | 2074 | return getEffectiveCommandValue(node.parentNode, command); |
michael@0 | 2075 | } |
michael@0 | 2076 | |
michael@0 | 2077 | // "If command is "createLink" or "unlink":" |
michael@0 | 2078 | if (command == "createlink" || command == "unlink") { |
michael@0 | 2079 | // "While node is not null, and is not an a element that has an href |
michael@0 | 2080 | // attribute, set node to its parent." |
michael@0 | 2081 | while (node |
michael@0 | 2082 | && (!isHtmlElement(node) |
michael@0 | 2083 | || node.tagName != "A" |
michael@0 | 2084 | || !node.hasAttribute("href"))) { |
michael@0 | 2085 | node = node.parentNode; |
michael@0 | 2086 | } |
michael@0 | 2087 | |
michael@0 | 2088 | // "If node is null, return null." |
michael@0 | 2089 | if (!node) { |
michael@0 | 2090 | return null; |
michael@0 | 2091 | } |
michael@0 | 2092 | |
michael@0 | 2093 | // "Return the value of node's href attribute." |
michael@0 | 2094 | return node.getAttribute("href"); |
michael@0 | 2095 | } |
michael@0 | 2096 | |
michael@0 | 2097 | // "If command is "backColor" or "hiliteColor":" |
michael@0 | 2098 | if (command == "backcolor" |
michael@0 | 2099 | || command == "hilitecolor") { |
michael@0 | 2100 | // "While the resolved value of "background-color" on node is any |
michael@0 | 2101 | // fully transparent value, and node's parent is an Element, set |
michael@0 | 2102 | // node to its parent." |
michael@0 | 2103 | // |
michael@0 | 2104 | // Another lame hack to avoid flawed APIs. |
michael@0 | 2105 | while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" |
michael@0 | 2106 | || getComputedStyle(node).backgroundColor === "" |
michael@0 | 2107 | || getComputedStyle(node).backgroundColor == "transparent") |
michael@0 | 2108 | && node.parentNode |
michael@0 | 2109 | && node.parentNode.nodeType == Node.ELEMENT_NODE) { |
michael@0 | 2110 | node = node.parentNode; |
michael@0 | 2111 | } |
michael@0 | 2112 | |
michael@0 | 2113 | // "Return the resolved value of "background-color" for node." |
michael@0 | 2114 | return getComputedStyle(node).backgroundColor; |
michael@0 | 2115 | } |
michael@0 | 2116 | |
michael@0 | 2117 | // "If command is "subscript" or "superscript":" |
michael@0 | 2118 | if (command == "subscript" || command == "superscript") { |
michael@0 | 2119 | // "Let affected by subscript and affected by superscript be two |
michael@0 | 2120 | // boolean variables, both initially false." |
michael@0 | 2121 | var affectedBySubscript = false; |
michael@0 | 2122 | var affectedBySuperscript = false; |
michael@0 | 2123 | |
michael@0 | 2124 | // "While node is an inline node:" |
michael@0 | 2125 | while (isInlineNode(node)) { |
michael@0 | 2126 | var verticalAlign = getComputedStyle(node).verticalAlign; |
michael@0 | 2127 | |
michael@0 | 2128 | // "If node is a sub, set affected by subscript to true." |
michael@0 | 2129 | if (isHtmlElement(node, "sub")) { |
michael@0 | 2130 | affectedBySubscript = true; |
michael@0 | 2131 | // "Otherwise, if node is a sup, set affected by superscript to |
michael@0 | 2132 | // true." |
michael@0 | 2133 | } else if (isHtmlElement(node, "sup")) { |
michael@0 | 2134 | affectedBySuperscript = true; |
michael@0 | 2135 | } |
michael@0 | 2136 | |
michael@0 | 2137 | // "Set node to its parent." |
michael@0 | 2138 | node = node.parentNode; |
michael@0 | 2139 | } |
michael@0 | 2140 | |
michael@0 | 2141 | // "If affected by subscript and affected by superscript are both true, |
michael@0 | 2142 | // return the string "mixed"." |
michael@0 | 2143 | if (affectedBySubscript && affectedBySuperscript) { |
michael@0 | 2144 | return "mixed"; |
michael@0 | 2145 | } |
michael@0 | 2146 | |
michael@0 | 2147 | // "If affected by subscript is true, return "subscript"." |
michael@0 | 2148 | if (affectedBySubscript) { |
michael@0 | 2149 | return "subscript"; |
michael@0 | 2150 | } |
michael@0 | 2151 | |
michael@0 | 2152 | // "If affected by superscript is true, return "superscript"." |
michael@0 | 2153 | if (affectedBySuperscript) { |
michael@0 | 2154 | return "superscript"; |
michael@0 | 2155 | } |
michael@0 | 2156 | |
michael@0 | 2157 | // "Return null." |
michael@0 | 2158 | return null; |
michael@0 | 2159 | } |
michael@0 | 2160 | |
michael@0 | 2161 | // "If command is "strikethrough", and the "text-decoration" property of |
michael@0 | 2162 | // node or any of its ancestors has resolved value containing |
michael@0 | 2163 | // "line-through", return "line-through". Otherwise, return null." |
michael@0 | 2164 | if (command == "strikethrough") { |
michael@0 | 2165 | do { |
michael@0 | 2166 | if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) { |
michael@0 | 2167 | return "line-through"; |
michael@0 | 2168 | } |
michael@0 | 2169 | node = node.parentNode; |
michael@0 | 2170 | } while (node && node.nodeType == Node.ELEMENT_NODE); |
michael@0 | 2171 | return null; |
michael@0 | 2172 | } |
michael@0 | 2173 | |
michael@0 | 2174 | // "If command is "underline", and the "text-decoration" property of node |
michael@0 | 2175 | // or any of its ancestors has resolved value containing "underline", |
michael@0 | 2176 | // return "underline". Otherwise, return null." |
michael@0 | 2177 | if (command == "underline") { |
michael@0 | 2178 | do { |
michael@0 | 2179 | if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) { |
michael@0 | 2180 | return "underline"; |
michael@0 | 2181 | } |
michael@0 | 2182 | node = node.parentNode; |
michael@0 | 2183 | } while (node && node.nodeType == Node.ELEMENT_NODE); |
michael@0 | 2184 | return null; |
michael@0 | 2185 | } |
michael@0 | 2186 | |
michael@0 | 2187 | if (!("relevantCssProperty" in commands[command])) { |
michael@0 | 2188 | throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue"; |
michael@0 | 2189 | } |
michael@0 | 2190 | |
michael@0 | 2191 | // "Return the resolved value for node of the relevant CSS property for |
michael@0 | 2192 | // command." |
michael@0 | 2193 | return getComputedStyle(node)[commands[command].relevantCssProperty]; |
michael@0 | 2194 | } |
michael@0 | 2195 | |
michael@0 | 2196 | function getSpecifiedCommandValue(element, command) { |
michael@0 | 2197 | // "If command is "backColor" or "hiliteColor" and element's display |
michael@0 | 2198 | // property does not have resolved value "inline", return null." |
michael@0 | 2199 | if ((command == "backcolor" || command == "hilitecolor") |
michael@0 | 2200 | && getComputedStyle(element).display != "inline") { |
michael@0 | 2201 | return null; |
michael@0 | 2202 | } |
michael@0 | 2203 | |
michael@0 | 2204 | // "If command is "createLink" or "unlink":" |
michael@0 | 2205 | if (command == "createlink" || command == "unlink") { |
michael@0 | 2206 | // "If element is an a element and has an href attribute, return the |
michael@0 | 2207 | // value of that attribute." |
michael@0 | 2208 | if (isHtmlElement(element) |
michael@0 | 2209 | && element.tagName == "A" |
michael@0 | 2210 | && element.hasAttribute("href")) { |
michael@0 | 2211 | return element.getAttribute("href"); |
michael@0 | 2212 | } |
michael@0 | 2213 | |
michael@0 | 2214 | // "Return null." |
michael@0 | 2215 | return null; |
michael@0 | 2216 | } |
michael@0 | 2217 | |
michael@0 | 2218 | // "If command is "subscript" or "superscript":" |
michael@0 | 2219 | if (command == "subscript" || command == "superscript") { |
michael@0 | 2220 | // "If element is a sup, return "superscript"." |
michael@0 | 2221 | if (isHtmlElement(element, "sup")) { |
michael@0 | 2222 | return "superscript"; |
michael@0 | 2223 | } |
michael@0 | 2224 | |
michael@0 | 2225 | // "If element is a sub, return "subscript"." |
michael@0 | 2226 | if (isHtmlElement(element, "sub")) { |
michael@0 | 2227 | return "subscript"; |
michael@0 | 2228 | } |
michael@0 | 2229 | |
michael@0 | 2230 | // "Return null." |
michael@0 | 2231 | return null; |
michael@0 | 2232 | } |
michael@0 | 2233 | |
michael@0 | 2234 | // "If command is "strikethrough", and element has a style attribute set, |
michael@0 | 2235 | // and that attribute sets "text-decoration":" |
michael@0 | 2236 | if (command == "strikethrough" |
michael@0 | 2237 | && element.style.textDecoration != "") { |
michael@0 | 2238 | // "If element's style attribute sets "text-decoration" to a value |
michael@0 | 2239 | // containing "line-through", return "line-through"." |
michael@0 | 2240 | if (element.style.textDecoration.indexOf("line-through") != -1) { |
michael@0 | 2241 | return "line-through"; |
michael@0 | 2242 | } |
michael@0 | 2243 | |
michael@0 | 2244 | // "Return null." |
michael@0 | 2245 | return null; |
michael@0 | 2246 | } |
michael@0 | 2247 | |
michael@0 | 2248 | // "If command is "strikethrough" and element is a s or strike element, |
michael@0 | 2249 | // return "line-through"." |
michael@0 | 2250 | if (command == "strikethrough" |
michael@0 | 2251 | && isHtmlElement(element, ["S", "STRIKE"])) { |
michael@0 | 2252 | return "line-through"; |
michael@0 | 2253 | } |
michael@0 | 2254 | |
michael@0 | 2255 | // "If command is "underline", and element has a style attribute set, and |
michael@0 | 2256 | // that attribute sets "text-decoration":" |
michael@0 | 2257 | if (command == "underline" |
michael@0 | 2258 | && element.style.textDecoration != "") { |
michael@0 | 2259 | // "If element's style attribute sets "text-decoration" to a value |
michael@0 | 2260 | // containing "underline", return "underline"." |
michael@0 | 2261 | if (element.style.textDecoration.indexOf("underline") != -1) { |
michael@0 | 2262 | return "underline"; |
michael@0 | 2263 | } |
michael@0 | 2264 | |
michael@0 | 2265 | // "Return null." |
michael@0 | 2266 | return null; |
michael@0 | 2267 | } |
michael@0 | 2268 | |
michael@0 | 2269 | // "If command is "underline" and element is a u element, return |
michael@0 | 2270 | // "underline"." |
michael@0 | 2271 | if (command == "underline" |
michael@0 | 2272 | && isHtmlElement(element, "U")) { |
michael@0 | 2273 | return "underline"; |
michael@0 | 2274 | } |
michael@0 | 2275 | |
michael@0 | 2276 | // "Let property be the relevant CSS property for command." |
michael@0 | 2277 | var property = commands[command].relevantCssProperty; |
michael@0 | 2278 | |
michael@0 | 2279 | // "If property is null, return null." |
michael@0 | 2280 | if (property === null) { |
michael@0 | 2281 | return null; |
michael@0 | 2282 | } |
michael@0 | 2283 | |
michael@0 | 2284 | // "If element has a style attribute set, and that attribute has the |
michael@0 | 2285 | // effect of setting property, return the value that it sets property to." |
michael@0 | 2286 | if (element.style[property] != "") { |
michael@0 | 2287 | return element.style[property]; |
michael@0 | 2288 | } |
michael@0 | 2289 | |
michael@0 | 2290 | // "If element is a font element that has an attribute whose effect is |
michael@0 | 2291 | // to create a presentational hint for property, return the value that the |
michael@0 | 2292 | // hint sets property to. (For a size of 7, this will be the non-CSS value |
michael@0 | 2293 | // "xxx-large".)" |
michael@0 | 2294 | if (isHtmlNamespace(element.namespaceURI) |
michael@0 | 2295 | && element.tagName == "FONT") { |
michael@0 | 2296 | if (property == "color" && element.hasAttribute("color")) { |
michael@0 | 2297 | return element.color; |
michael@0 | 2298 | } |
michael@0 | 2299 | if (property == "fontFamily" && element.hasAttribute("face")) { |
michael@0 | 2300 | return element.face; |
michael@0 | 2301 | } |
michael@0 | 2302 | if (property == "fontSize" && element.hasAttribute("size")) { |
michael@0 | 2303 | // This is not even close to correct in general. |
michael@0 | 2304 | var size = parseInt(element.size); |
michael@0 | 2305 | if (size < 1) { |
michael@0 | 2306 | size = 1; |
michael@0 | 2307 | } |
michael@0 | 2308 | if (size > 7) { |
michael@0 | 2309 | size = 7; |
michael@0 | 2310 | } |
michael@0 | 2311 | return { |
michael@0 | 2312 | 1: "x-small", |
michael@0 | 2313 | 2: "small", |
michael@0 | 2314 | 3: "medium", |
michael@0 | 2315 | 4: "large", |
michael@0 | 2316 | 5: "x-large", |
michael@0 | 2317 | 6: "xx-large", |
michael@0 | 2318 | 7: "xxx-large" |
michael@0 | 2319 | }[size]; |
michael@0 | 2320 | } |
michael@0 | 2321 | } |
michael@0 | 2322 | |
michael@0 | 2323 | // "If element is in the following list, and property is equal to the |
michael@0 | 2324 | // CSS property name listed for it, return the string listed for it." |
michael@0 | 2325 | // |
michael@0 | 2326 | // A list follows, whose meaning is copied here. |
michael@0 | 2327 | if (property == "fontWeight" |
michael@0 | 2328 | && (element.tagName == "B" || element.tagName == "STRONG")) { |
michael@0 | 2329 | return "bold"; |
michael@0 | 2330 | } |
michael@0 | 2331 | if (property == "fontStyle" |
michael@0 | 2332 | && (element.tagName == "I" || element.tagName == "EM")) { |
michael@0 | 2333 | return "italic"; |
michael@0 | 2334 | } |
michael@0 | 2335 | |
michael@0 | 2336 | // "Return null." |
michael@0 | 2337 | return null; |
michael@0 | 2338 | } |
michael@0 | 2339 | |
michael@0 | 2340 | function reorderModifiableDescendants(node, command, newValue) { |
michael@0 | 2341 | // "Let candidate equal node." |
michael@0 | 2342 | var candidate = node; |
michael@0 | 2343 | |
michael@0 | 2344 | // "While candidate is a modifiable element, and candidate has exactly one |
michael@0 | 2345 | // child, and that child is also a modifiable element, and candidate is not |
michael@0 | 2346 | // a simple modifiable element or candidate's specified command value for |
michael@0 | 2347 | // command is not equivalent to new value, set candidate to its child." |
michael@0 | 2348 | while (isModifiableElement(candidate) |
michael@0 | 2349 | && candidate.childNodes.length == 1 |
michael@0 | 2350 | && isModifiableElement(candidate.firstChild) |
michael@0 | 2351 | && (!isSimpleModifiableElement(candidate) |
michael@0 | 2352 | || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) { |
michael@0 | 2353 | candidate = candidate.firstChild; |
michael@0 | 2354 | } |
michael@0 | 2355 | |
michael@0 | 2356 | // "If candidate is node, or is not a simple modifiable element, or its |
michael@0 | 2357 | // specified command value is not equivalent to new value, or its effective |
michael@0 | 2358 | // command value is not loosely equivalent to new value, abort these |
michael@0 | 2359 | // steps." |
michael@0 | 2360 | if (candidate == node |
michael@0 | 2361 | || !isSimpleModifiableElement(candidate) |
michael@0 | 2362 | || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue) |
michael@0 | 2363 | || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) { |
michael@0 | 2364 | return; |
michael@0 | 2365 | } |
michael@0 | 2366 | |
michael@0 | 2367 | // "While candidate has children, insert the first child of candidate into |
michael@0 | 2368 | // candidate's parent immediately before candidate, preserving ranges." |
michael@0 | 2369 | while (candidate.hasChildNodes()) { |
michael@0 | 2370 | movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate)); |
michael@0 | 2371 | } |
michael@0 | 2372 | |
michael@0 | 2373 | // "Insert candidate into node's parent immediately after node." |
michael@0 | 2374 | node.parentNode.insertBefore(candidate, node.nextSibling); |
michael@0 | 2375 | |
michael@0 | 2376 | // "Append the node as the last child of candidate, preserving ranges." |
michael@0 | 2377 | movePreservingRanges(node, candidate, -1); |
michael@0 | 2378 | } |
michael@0 | 2379 | |
michael@0 | 2380 | function recordValues(nodeList) { |
michael@0 | 2381 | // "Let values be a list of (node, command, specified command value) |
michael@0 | 2382 | // triples, initially empty." |
michael@0 | 2383 | var values = []; |
michael@0 | 2384 | |
michael@0 | 2385 | // "For each node in node list, for each command in the list "subscript", |
michael@0 | 2386 | // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic", |
michael@0 | 2387 | // "strikethrough", and "underline" in that order:" |
michael@0 | 2388 | nodeList.forEach(function(node) { |
michael@0 | 2389 | ["subscript", "bold", "fontname", "fontsize", "forecolor", |
michael@0 | 2390 | "hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) { |
michael@0 | 2391 | // "Let ancestor equal node." |
michael@0 | 2392 | var ancestor = node; |
michael@0 | 2393 | |
michael@0 | 2394 | // "If ancestor is not an Element, set it to its parent." |
michael@0 | 2395 | if (ancestor.nodeType != Node.ELEMENT_NODE) { |
michael@0 | 2396 | ancestor = ancestor.parentNode; |
michael@0 | 2397 | } |
michael@0 | 2398 | |
michael@0 | 2399 | // "While ancestor is an Element and its specified command value |
michael@0 | 2400 | // for command is null, set it to its parent." |
michael@0 | 2401 | while (ancestor |
michael@0 | 2402 | && ancestor.nodeType == Node.ELEMENT_NODE |
michael@0 | 2403 | && getSpecifiedCommandValue(ancestor, command) === null) { |
michael@0 | 2404 | ancestor = ancestor.parentNode; |
michael@0 | 2405 | } |
michael@0 | 2406 | |
michael@0 | 2407 | // "If ancestor is an Element, add (node, command, ancestor's |
michael@0 | 2408 | // specified command value for command) to values. Otherwise add |
michael@0 | 2409 | // (node, command, null) to values." |
michael@0 | 2410 | if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) { |
michael@0 | 2411 | values.push([node, command, getSpecifiedCommandValue(ancestor, command)]); |
michael@0 | 2412 | } else { |
michael@0 | 2413 | values.push([node, command, null]); |
michael@0 | 2414 | } |
michael@0 | 2415 | }); |
michael@0 | 2416 | }); |
michael@0 | 2417 | |
michael@0 | 2418 | // "Return values." |
michael@0 | 2419 | return values; |
michael@0 | 2420 | } |
michael@0 | 2421 | |
michael@0 | 2422 | function restoreValues(values) { |
michael@0 | 2423 | // "For each (node, command, value) triple in values:" |
michael@0 | 2424 | values.forEach(function(triple) { |
michael@0 | 2425 | var node = triple[0]; |
michael@0 | 2426 | var command = triple[1]; |
michael@0 | 2427 | var value = triple[2]; |
michael@0 | 2428 | |
michael@0 | 2429 | // "Let ancestor equal node." |
michael@0 | 2430 | var ancestor = node; |
michael@0 | 2431 | |
michael@0 | 2432 | // "If ancestor is not an Element, set it to its parent." |
michael@0 | 2433 | if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) { |
michael@0 | 2434 | ancestor = ancestor.parentNode; |
michael@0 | 2435 | } |
michael@0 | 2436 | |
michael@0 | 2437 | // "While ancestor is an Element and its specified command value for |
michael@0 | 2438 | // command is null, set it to its parent." |
michael@0 | 2439 | while (ancestor |
michael@0 | 2440 | && ancestor.nodeType == Node.ELEMENT_NODE |
michael@0 | 2441 | && getSpecifiedCommandValue(ancestor, command) === null) { |
michael@0 | 2442 | ancestor = ancestor.parentNode; |
michael@0 | 2443 | } |
michael@0 | 2444 | |
michael@0 | 2445 | // "If value is null and ancestor is an Element, push down values on |
michael@0 | 2446 | // node for command, with new value null." |
michael@0 | 2447 | if (value === null |
michael@0 | 2448 | && ancestor |
michael@0 | 2449 | && ancestor.nodeType == Node.ELEMENT_NODE) { |
michael@0 | 2450 | pushDownValues(node, command, null); |
michael@0 | 2451 | |
michael@0 | 2452 | // "Otherwise, if ancestor is an Element and its specified command |
michael@0 | 2453 | // value for command is not equivalent to value, or if ancestor is not |
michael@0 | 2454 | // an Element and value is not null, force the value of command to |
michael@0 | 2455 | // value on node." |
michael@0 | 2456 | } else if ((ancestor |
michael@0 | 2457 | && ancestor.nodeType == Node.ELEMENT_NODE |
michael@0 | 2458 | && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value)) |
michael@0 | 2459 | || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) |
michael@0 | 2460 | && value !== null)) { |
michael@0 | 2461 | forceValue(node, command, value); |
michael@0 | 2462 | } |
michael@0 | 2463 | }); |
michael@0 | 2464 | } |
michael@0 | 2465 | |
michael@0 | 2466 | |
michael@0 | 2467 | //@} |
michael@0 | 2468 | ///// Clearing an element's value ///// |
michael@0 | 2469 | //@{ |
michael@0 | 2470 | |
michael@0 | 2471 | function clearValue(element, command) { |
michael@0 | 2472 | // "If element is not editable, return the empty list." |
michael@0 | 2473 | if (!isEditable(element)) { |
michael@0 | 2474 | return []; |
michael@0 | 2475 | } |
michael@0 | 2476 | |
michael@0 | 2477 | // "If element's specified command value for command is null, return the |
michael@0 | 2478 | // empty list." |
michael@0 | 2479 | if (getSpecifiedCommandValue(element, command) === null) { |
michael@0 | 2480 | return []; |
michael@0 | 2481 | } |
michael@0 | 2482 | |
michael@0 | 2483 | // "If element is a simple modifiable element:" |
michael@0 | 2484 | if (isSimpleModifiableElement(element)) { |
michael@0 | 2485 | // "Let children be the children of element." |
michael@0 | 2486 | var children = Array.prototype.slice.call(element.childNodes); |
michael@0 | 2487 | |
michael@0 | 2488 | // "For each child in children, insert child into element's parent |
michael@0 | 2489 | // immediately before element, preserving ranges." |
michael@0 | 2490 | for (var i = 0; i < children.length; i++) { |
michael@0 | 2491 | movePreservingRanges(children[i], element.parentNode, getNodeIndex(element)); |
michael@0 | 2492 | } |
michael@0 | 2493 | |
michael@0 | 2494 | // "Remove element from its parent." |
michael@0 | 2495 | element.parentNode.removeChild(element); |
michael@0 | 2496 | |
michael@0 | 2497 | // "Return children." |
michael@0 | 2498 | return children; |
michael@0 | 2499 | } |
michael@0 | 2500 | |
michael@0 | 2501 | // "If command is "strikethrough", and element has a style attribute that |
michael@0 | 2502 | // sets "text-decoration" to some value containing "line-through", delete |
michael@0 | 2503 | // "line-through" from the value." |
michael@0 | 2504 | if (command == "strikethrough" |
michael@0 | 2505 | && element.style.textDecoration.indexOf("line-through") != -1) { |
michael@0 | 2506 | if (element.style.textDecoration == "line-through") { |
michael@0 | 2507 | element.style.textDecoration = ""; |
michael@0 | 2508 | } else { |
michael@0 | 2509 | element.style.textDecoration = element.style.textDecoration.replace("line-through", ""); |
michael@0 | 2510 | } |
michael@0 | 2511 | if (element.getAttribute("style") == "") { |
michael@0 | 2512 | element.removeAttribute("style"); |
michael@0 | 2513 | } |
michael@0 | 2514 | } |
michael@0 | 2515 | |
michael@0 | 2516 | // "If command is "underline", and element has a style attribute that sets |
michael@0 | 2517 | // "text-decoration" to some value containing "underline", delete |
michael@0 | 2518 | // "underline" from the value." |
michael@0 | 2519 | if (command == "underline" |
michael@0 | 2520 | && element.style.textDecoration.indexOf("underline") != -1) { |
michael@0 | 2521 | if (element.style.textDecoration == "underline") { |
michael@0 | 2522 | element.style.textDecoration = ""; |
michael@0 | 2523 | } else { |
michael@0 | 2524 | element.style.textDecoration = element.style.textDecoration.replace("underline", ""); |
michael@0 | 2525 | } |
michael@0 | 2526 | if (element.getAttribute("style") == "") { |
michael@0 | 2527 | element.removeAttribute("style"); |
michael@0 | 2528 | } |
michael@0 | 2529 | } |
michael@0 | 2530 | |
michael@0 | 2531 | // "If the relevant CSS property for command is not null, unset the CSS |
michael@0 | 2532 | // property property of element." |
michael@0 | 2533 | if (commands[command].relevantCssProperty !== null) { |
michael@0 | 2534 | element.style[commands[command].relevantCssProperty] = ''; |
michael@0 | 2535 | if (element.getAttribute("style") == "") { |
michael@0 | 2536 | element.removeAttribute("style"); |
michael@0 | 2537 | } |
michael@0 | 2538 | } |
michael@0 | 2539 | |
michael@0 | 2540 | // "If element is a font element:" |
michael@0 | 2541 | if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") { |
michael@0 | 2542 | // "If command is "foreColor", unset element's color attribute, if set." |
michael@0 | 2543 | if (command == "forecolor") { |
michael@0 | 2544 | element.removeAttribute("color"); |
michael@0 | 2545 | } |
michael@0 | 2546 | |
michael@0 | 2547 | // "If command is "fontName", unset element's face attribute, if set." |
michael@0 | 2548 | if (command == "fontname") { |
michael@0 | 2549 | element.removeAttribute("face"); |
michael@0 | 2550 | } |
michael@0 | 2551 | |
michael@0 | 2552 | // "If command is "fontSize", unset element's size attribute, if set." |
michael@0 | 2553 | if (command == "fontsize") { |
michael@0 | 2554 | element.removeAttribute("size"); |
michael@0 | 2555 | } |
michael@0 | 2556 | } |
michael@0 | 2557 | |
michael@0 | 2558 | // "If element is an a element and command is "createLink" or "unlink", |
michael@0 | 2559 | // unset the href property of element." |
michael@0 | 2560 | if (isHtmlElement(element, "A") |
michael@0 | 2561 | && (command == "createlink" || command == "unlink")) { |
michael@0 | 2562 | element.removeAttribute("href"); |
michael@0 | 2563 | } |
michael@0 | 2564 | |
michael@0 | 2565 | // "If element's specified command value for command is null, return the |
michael@0 | 2566 | // empty list." |
michael@0 | 2567 | if (getSpecifiedCommandValue(element, command) === null) { |
michael@0 | 2568 | return []; |
michael@0 | 2569 | } |
michael@0 | 2570 | |
michael@0 | 2571 | // "Set the tag name of element to "span", and return the one-node list |
michael@0 | 2572 | // consisting of the result." |
michael@0 | 2573 | return [setTagName(element, "span")]; |
michael@0 | 2574 | } |
michael@0 | 2575 | |
michael@0 | 2576 | |
michael@0 | 2577 | //@} |
michael@0 | 2578 | ///// Pushing down values ///// |
michael@0 | 2579 | //@{ |
michael@0 | 2580 | |
michael@0 | 2581 | function pushDownValues(node, command, newValue) { |
michael@0 | 2582 | // "If node's parent is not an Element, abort this algorithm." |
michael@0 | 2583 | if (!node.parentNode |
michael@0 | 2584 | || node.parentNode.nodeType != Node.ELEMENT_NODE) { |
michael@0 | 2585 | return; |
michael@0 | 2586 | } |
michael@0 | 2587 | |
michael@0 | 2588 | // "If the effective command value of command is loosely equivalent to new |
michael@0 | 2589 | // value on node, abort this algorithm." |
michael@0 | 2590 | if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
michael@0 | 2591 | return; |
michael@0 | 2592 | } |
michael@0 | 2593 | |
michael@0 | 2594 | // "Let current ancestor be node's parent." |
michael@0 | 2595 | var currentAncestor = node.parentNode; |
michael@0 | 2596 | |
michael@0 | 2597 | // "Let ancestor list be a list of Nodes, initially empty." |
michael@0 | 2598 | var ancestorList = []; |
michael@0 | 2599 | |
michael@0 | 2600 | // "While current ancestor is an editable Element and the effective command |
michael@0 | 2601 | // value of command is not loosely equivalent to new value on it, append |
michael@0 | 2602 | // current ancestor to ancestor list, then set current ancestor to its |
michael@0 | 2603 | // parent." |
michael@0 | 2604 | while (isEditable(currentAncestor) |
michael@0 | 2605 | && currentAncestor.nodeType == Node.ELEMENT_NODE |
michael@0 | 2606 | && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) { |
michael@0 | 2607 | ancestorList.push(currentAncestor); |
michael@0 | 2608 | currentAncestor = currentAncestor.parentNode; |
michael@0 | 2609 | } |
michael@0 | 2610 | |
michael@0 | 2611 | // "If ancestor list is empty, abort this algorithm." |
michael@0 | 2612 | if (!ancestorList.length) { |
michael@0 | 2613 | return; |
michael@0 | 2614 | } |
michael@0 | 2615 | |
michael@0 | 2616 | // "Let propagated value be the specified command value of command on the |
michael@0 | 2617 | // last member of ancestor list." |
michael@0 | 2618 | var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command); |
michael@0 | 2619 | |
michael@0 | 2620 | // "If propagated value is null and is not equal to new value, abort this |
michael@0 | 2621 | // algorithm." |
michael@0 | 2622 | if (propagatedValue === null && propagatedValue != newValue) { |
michael@0 | 2623 | return; |
michael@0 | 2624 | } |
michael@0 | 2625 | |
michael@0 | 2626 | // "If the effective command value for the parent of the last member of |
michael@0 | 2627 | // ancestor list is not loosely equivalent to new value, and new value is |
michael@0 | 2628 | // not null, abort this algorithm." |
michael@0 | 2629 | if (newValue !== null |
michael@0 | 2630 | && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) { |
michael@0 | 2631 | return; |
michael@0 | 2632 | } |
michael@0 | 2633 | |
michael@0 | 2634 | // "While ancestor list is not empty:" |
michael@0 | 2635 | while (ancestorList.length) { |
michael@0 | 2636 | // "Let current ancestor be the last member of ancestor list." |
michael@0 | 2637 | // "Remove the last member from ancestor list." |
michael@0 | 2638 | var currentAncestor = ancestorList.pop(); |
michael@0 | 2639 | |
michael@0 | 2640 | // "If the specified command value of current ancestor for command is |
michael@0 | 2641 | // not null, set propagated value to that value." |
michael@0 | 2642 | if (getSpecifiedCommandValue(currentAncestor, command) !== null) { |
michael@0 | 2643 | propagatedValue = getSpecifiedCommandValue(currentAncestor, command); |
michael@0 | 2644 | } |
michael@0 | 2645 | |
michael@0 | 2646 | // "Let children be the children of current ancestor." |
michael@0 | 2647 | var children = Array.prototype.slice.call(currentAncestor.childNodes); |
michael@0 | 2648 | |
michael@0 | 2649 | // "If the specified command value of current ancestor for command is |
michael@0 | 2650 | // not null, clear the value of current ancestor." |
michael@0 | 2651 | if (getSpecifiedCommandValue(currentAncestor, command) !== null) { |
michael@0 | 2652 | clearValue(currentAncestor, command); |
michael@0 | 2653 | } |
michael@0 | 2654 | |
michael@0 | 2655 | // "For every child in children:" |
michael@0 | 2656 | for (var i = 0; i < children.length; i++) { |
michael@0 | 2657 | var child = children[i]; |
michael@0 | 2658 | |
michael@0 | 2659 | // "If child is node, continue with the next child." |
michael@0 | 2660 | if (child == node) { |
michael@0 | 2661 | continue; |
michael@0 | 2662 | } |
michael@0 | 2663 | |
michael@0 | 2664 | // "If child is an Element whose specified command value for |
michael@0 | 2665 | // command is neither null nor equivalent to propagated value, |
michael@0 | 2666 | // continue with the next child." |
michael@0 | 2667 | if (child.nodeType == Node.ELEMENT_NODE |
michael@0 | 2668 | && getSpecifiedCommandValue(child, command) !== null |
michael@0 | 2669 | && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) { |
michael@0 | 2670 | continue; |
michael@0 | 2671 | } |
michael@0 | 2672 | |
michael@0 | 2673 | // "If child is the last member of ancestor list, continue with the |
michael@0 | 2674 | // next child." |
michael@0 | 2675 | if (child == ancestorList[ancestorList.length - 1]) { |
michael@0 | 2676 | continue; |
michael@0 | 2677 | } |
michael@0 | 2678 | |
michael@0 | 2679 | // "Force the value of child, with command as in this algorithm |
michael@0 | 2680 | // and new value equal to propagated value." |
michael@0 | 2681 | forceValue(child, command, propagatedValue); |
michael@0 | 2682 | } |
michael@0 | 2683 | } |
michael@0 | 2684 | } |
michael@0 | 2685 | |
michael@0 | 2686 | |
michael@0 | 2687 | //@} |
michael@0 | 2688 | ///// Forcing the value of a node ///// |
michael@0 | 2689 | //@{ |
michael@0 | 2690 | |
michael@0 | 2691 | function forceValue(node, command, newValue) { |
michael@0 | 2692 | // "If node's parent is null, abort this algorithm." |
michael@0 | 2693 | if (!node.parentNode) { |
michael@0 | 2694 | return; |
michael@0 | 2695 | } |
michael@0 | 2696 | |
michael@0 | 2697 | // "If new value is null, abort this algorithm." |
michael@0 | 2698 | if (newValue === null) { |
michael@0 | 2699 | return; |
michael@0 | 2700 | } |
michael@0 | 2701 | |
michael@0 | 2702 | // "If node is an allowed child of "span":" |
michael@0 | 2703 | if (isAllowedChild(node, "span")) { |
michael@0 | 2704 | // "Reorder modifiable descendants of node's previousSibling." |
michael@0 | 2705 | reorderModifiableDescendants(node.previousSibling, command, newValue); |
michael@0 | 2706 | |
michael@0 | 2707 | // "Reorder modifiable descendants of node's nextSibling." |
michael@0 | 2708 | reorderModifiableDescendants(node.nextSibling, command, newValue); |
michael@0 | 2709 | |
michael@0 | 2710 | // "Wrap the one-node list consisting of node, with sibling criteria |
michael@0 | 2711 | // returning true for a simple modifiable element whose specified |
michael@0 | 2712 | // command value is equivalent to new value and whose effective command |
michael@0 | 2713 | // value is loosely equivalent to new value and false otherwise, and |
michael@0 | 2714 | // with new parent instructions returning null." |
michael@0 | 2715 | wrap([node], |
michael@0 | 2716 | function(node) { |
michael@0 | 2717 | return isSimpleModifiableElement(node) |
michael@0 | 2718 | && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue) |
michael@0 | 2719 | && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue); |
michael@0 | 2720 | }, |
michael@0 | 2721 | function() { return null } |
michael@0 | 2722 | ); |
michael@0 | 2723 | } |
michael@0 | 2724 | |
michael@0 | 2725 | // "If node is invisible, abort this algorithm." |
michael@0 | 2726 | if (isInvisible(node)) { |
michael@0 | 2727 | return; |
michael@0 | 2728 | } |
michael@0 | 2729 | |
michael@0 | 2730 | // "If the effective command value of command is loosely equivalent to new |
michael@0 | 2731 | // value on node, abort this algorithm." |
michael@0 | 2732 | if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
michael@0 | 2733 | return; |
michael@0 | 2734 | } |
michael@0 | 2735 | |
michael@0 | 2736 | // "If node is not an allowed child of "span":" |
michael@0 | 2737 | if (!isAllowedChild(node, "span")) { |
michael@0 | 2738 | // "Let children be all children of node, omitting any that are |
michael@0 | 2739 | // Elements whose specified command value for command is neither null |
michael@0 | 2740 | // nor equivalent to new value." |
michael@0 | 2741 | var children = []; |
michael@0 | 2742 | for (var i = 0; i < node.childNodes.length; i++) { |
michael@0 | 2743 | if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { |
michael@0 | 2744 | var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); |
michael@0 | 2745 | |
michael@0 | 2746 | if (specifiedValue !== null |
michael@0 | 2747 | && !areEquivalentValues(command, newValue, specifiedValue)) { |
michael@0 | 2748 | continue; |
michael@0 | 2749 | } |
michael@0 | 2750 | } |
michael@0 | 2751 | children.push(node.childNodes[i]); |
michael@0 | 2752 | } |
michael@0 | 2753 | |
michael@0 | 2754 | // "Force the value of each Node in children, with command and new |
michael@0 | 2755 | // value as in this invocation of the algorithm." |
michael@0 | 2756 | for (var i = 0; i < children.length; i++) { |
michael@0 | 2757 | forceValue(children[i], command, newValue); |
michael@0 | 2758 | } |
michael@0 | 2759 | |
michael@0 | 2760 | // "Abort this algorithm." |
michael@0 | 2761 | return; |
michael@0 | 2762 | } |
michael@0 | 2763 | |
michael@0 | 2764 | // "If the effective command value of command is loosely equivalent to new |
michael@0 | 2765 | // value on node, abort this algorithm." |
michael@0 | 2766 | if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
michael@0 | 2767 | return; |
michael@0 | 2768 | } |
michael@0 | 2769 | |
michael@0 | 2770 | // "Let new parent be null." |
michael@0 | 2771 | var newParent = null; |
michael@0 | 2772 | |
michael@0 | 2773 | // "If the CSS styling flag is false:" |
michael@0 | 2774 | if (!cssStylingFlag) { |
michael@0 | 2775 | // "If command is "bold" and new value is "bold", let new parent be the |
michael@0 | 2776 | // result of calling createElement("b") on the ownerDocument of node." |
michael@0 | 2777 | if (command == "bold" && (newValue == "bold" || newValue == "700")) { |
michael@0 | 2778 | newParent = node.ownerDocument.createElement("b"); |
michael@0 | 2779 | } |
michael@0 | 2780 | |
michael@0 | 2781 | // "If command is "italic" and new value is "italic", let new parent be |
michael@0 | 2782 | // the result of calling createElement("i") on the ownerDocument of |
michael@0 | 2783 | // node." |
michael@0 | 2784 | if (command == "italic" && newValue == "italic") { |
michael@0 | 2785 | newParent = node.ownerDocument.createElement("i"); |
michael@0 | 2786 | } |
michael@0 | 2787 | |
michael@0 | 2788 | // "If command is "strikethrough" and new value is "line-through", let |
michael@0 | 2789 | // new parent be the result of calling createElement("s") on the |
michael@0 | 2790 | // ownerDocument of node." |
michael@0 | 2791 | if (command == "strikethrough" && newValue == "line-through") { |
michael@0 | 2792 | newParent = node.ownerDocument.createElement("s"); |
michael@0 | 2793 | } |
michael@0 | 2794 | |
michael@0 | 2795 | // "If command is "underline" and new value is "underline", let new |
michael@0 | 2796 | // parent be the result of calling createElement("u") on the |
michael@0 | 2797 | // ownerDocument of node." |
michael@0 | 2798 | if (command == "underline" && newValue == "underline") { |
michael@0 | 2799 | newParent = node.ownerDocument.createElement("u"); |
michael@0 | 2800 | } |
michael@0 | 2801 | |
michael@0 | 2802 | // "If command is "foreColor", and new value is fully opaque with red, |
michael@0 | 2803 | // green, and blue components in the range 0 to 255:" |
michael@0 | 2804 | if (command == "forecolor" && parseSimpleColor(newValue)) { |
michael@0 | 2805 | // "Let new parent be the result of calling createElement("font") |
michael@0 | 2806 | // on the ownerDocument of node." |
michael@0 | 2807 | newParent = node.ownerDocument.createElement("font"); |
michael@0 | 2808 | |
michael@0 | 2809 | // "Set the color attribute of new parent to the result of applying |
michael@0 | 2810 | // the rules for serializing simple color values to new value |
michael@0 | 2811 | // (interpreted as a simple color)." |
michael@0 | 2812 | newParent.setAttribute("color", parseSimpleColor(newValue)); |
michael@0 | 2813 | } |
michael@0 | 2814 | |
michael@0 | 2815 | // "If command is "fontName", let new parent be the result of calling |
michael@0 | 2816 | // createElement("font") on the ownerDocument of node, then set the |
michael@0 | 2817 | // face attribute of new parent to new value." |
michael@0 | 2818 | if (command == "fontname") { |
michael@0 | 2819 | newParent = node.ownerDocument.createElement("font"); |
michael@0 | 2820 | newParent.face = newValue; |
michael@0 | 2821 | } |
michael@0 | 2822 | } |
michael@0 | 2823 | |
michael@0 | 2824 | // "If command is "createLink" or "unlink":" |
michael@0 | 2825 | if (command == "createlink" || command == "unlink") { |
michael@0 | 2826 | // "Let new parent be the result of calling createElement("a") on the |
michael@0 | 2827 | // ownerDocument of node." |
michael@0 | 2828 | newParent = node.ownerDocument.createElement("a"); |
michael@0 | 2829 | |
michael@0 | 2830 | // "Set the href attribute of new parent to new value." |
michael@0 | 2831 | newParent.setAttribute("href", newValue); |
michael@0 | 2832 | |
michael@0 | 2833 | // "Let ancestor be node's parent." |
michael@0 | 2834 | var ancestor = node.parentNode; |
michael@0 | 2835 | |
michael@0 | 2836 | // "While ancestor is not null:" |
michael@0 | 2837 | while (ancestor) { |
michael@0 | 2838 | // "If ancestor is an a, set the tag name of ancestor to "span", |
michael@0 | 2839 | // and let ancestor be the result." |
michael@0 | 2840 | if (isHtmlElement(ancestor, "A")) { |
michael@0 | 2841 | ancestor = setTagName(ancestor, "span"); |
michael@0 | 2842 | } |
michael@0 | 2843 | |
michael@0 | 2844 | // "Set ancestor to its parent." |
michael@0 | 2845 | ancestor = ancestor.parentNode; |
michael@0 | 2846 | } |
michael@0 | 2847 | } |
michael@0 | 2848 | |
michael@0 | 2849 | // "If command is "fontSize"; and new value is one of "x-small", "small", |
michael@0 | 2850 | // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the |
michael@0 | 2851 | // CSS styling flag is false, or new value is "xxx-large": let new parent |
michael@0 | 2852 | // be the result of calling createElement("font") on the ownerDocument of |
michael@0 | 2853 | // node, then set the size attribute of new parent to the number from the |
michael@0 | 2854 | // following table based on new value: [table omitted]" |
michael@0 | 2855 | if (command == "fontsize" |
michael@0 | 2856 | && ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1 |
michael@0 | 2857 | && (!cssStylingFlag || newValue == "xxx-large")) { |
michael@0 | 2858 | newParent = node.ownerDocument.createElement("font"); |
michael@0 | 2859 | newParent.size = cssSizeToLegacy(newValue); |
michael@0 | 2860 | } |
michael@0 | 2861 | |
michael@0 | 2862 | // "If command is "subscript" or "superscript" and new value is |
michael@0 | 2863 | // "subscript", let new parent be the result of calling |
michael@0 | 2864 | // createElement("sub") on the ownerDocument of node." |
michael@0 | 2865 | if ((command == "subscript" || command == "superscript") |
michael@0 | 2866 | && newValue == "subscript") { |
michael@0 | 2867 | newParent = node.ownerDocument.createElement("sub"); |
michael@0 | 2868 | } |
michael@0 | 2869 | |
michael@0 | 2870 | // "If command is "subscript" or "superscript" and new value is |
michael@0 | 2871 | // "superscript", let new parent be the result of calling |
michael@0 | 2872 | // createElement("sup") on the ownerDocument of node." |
michael@0 | 2873 | if ((command == "subscript" || command == "superscript") |
michael@0 | 2874 | && newValue == "superscript") { |
michael@0 | 2875 | newParent = node.ownerDocument.createElement("sup"); |
michael@0 | 2876 | } |
michael@0 | 2877 | |
michael@0 | 2878 | // "If new parent is null, let new parent be the result of calling |
michael@0 | 2879 | // createElement("span") on the ownerDocument of node." |
michael@0 | 2880 | if (!newParent) { |
michael@0 | 2881 | newParent = node.ownerDocument.createElement("span"); |
michael@0 | 2882 | } |
michael@0 | 2883 | |
michael@0 | 2884 | // "Insert new parent in node's parent before node." |
michael@0 | 2885 | node.parentNode.insertBefore(newParent, node); |
michael@0 | 2886 | |
michael@0 | 2887 | // "If the effective command value of command for new parent is not loosely |
michael@0 | 2888 | // equivalent to new value, and the relevant CSS property for command is |
michael@0 | 2889 | // not null, set that CSS property of new parent to new value (if the new |
michael@0 | 2890 | // value would be valid)." |
michael@0 | 2891 | var property = commands[command].relevantCssProperty; |
michael@0 | 2892 | if (property !== null |
michael@0 | 2893 | && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) { |
michael@0 | 2894 | newParent.style[property] = newValue; |
michael@0 | 2895 | } |
michael@0 | 2896 | |
michael@0 | 2897 | // "If command is "strikethrough", and new value is "line-through", and the |
michael@0 | 2898 | // effective command value of "strikethrough" for new parent is not |
michael@0 | 2899 | // "line-through", set the "text-decoration" property of new parent to |
michael@0 | 2900 | // "line-through"." |
michael@0 | 2901 | if (command == "strikethrough" |
michael@0 | 2902 | && newValue == "line-through" |
michael@0 | 2903 | && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") { |
michael@0 | 2904 | newParent.style.textDecoration = "line-through"; |
michael@0 | 2905 | } |
michael@0 | 2906 | |
michael@0 | 2907 | // "If command is "underline", and new value is "underline", and the |
michael@0 | 2908 | // effective command value of "underline" for new parent is not |
michael@0 | 2909 | // "underline", set the "text-decoration" property of new parent to |
michael@0 | 2910 | // "underline"." |
michael@0 | 2911 | if (command == "underline" |
michael@0 | 2912 | && newValue == "underline" |
michael@0 | 2913 | && getEffectiveCommandValue(newParent, "underline") != "underline") { |
michael@0 | 2914 | newParent.style.textDecoration = "underline"; |
michael@0 | 2915 | } |
michael@0 | 2916 | |
michael@0 | 2917 | // "Append node to new parent as its last child, preserving ranges." |
michael@0 | 2918 | movePreservingRanges(node, newParent, newParent.childNodes.length); |
michael@0 | 2919 | |
michael@0 | 2920 | // "If node is an Element and the effective command value of command for |
michael@0 | 2921 | // node is not loosely equivalent to new value:" |
michael@0 | 2922 | if (node.nodeType == Node.ELEMENT_NODE |
michael@0 | 2923 | && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { |
michael@0 | 2924 | // "Insert node into the parent of new parent before new parent, |
michael@0 | 2925 | // preserving ranges." |
michael@0 | 2926 | movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent)); |
michael@0 | 2927 | |
michael@0 | 2928 | // "Remove new parent from its parent." |
michael@0 | 2929 | newParent.parentNode.removeChild(newParent); |
michael@0 | 2930 | |
michael@0 | 2931 | // "Let children be all children of node, omitting any that are |
michael@0 | 2932 | // Elements whose specified command value for command is neither null |
michael@0 | 2933 | // nor equivalent to new value." |
michael@0 | 2934 | var children = []; |
michael@0 | 2935 | for (var i = 0; i < node.childNodes.length; i++) { |
michael@0 | 2936 | if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { |
michael@0 | 2937 | var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); |
michael@0 | 2938 | |
michael@0 | 2939 | if (specifiedValue !== null |
michael@0 | 2940 | && !areEquivalentValues(command, newValue, specifiedValue)) { |
michael@0 | 2941 | continue; |
michael@0 | 2942 | } |
michael@0 | 2943 | } |
michael@0 | 2944 | children.push(node.childNodes[i]); |
michael@0 | 2945 | } |
michael@0 | 2946 | |
michael@0 | 2947 | // "Force the value of each Node in children, with command and new |
michael@0 | 2948 | // value as in this invocation of the algorithm." |
michael@0 | 2949 | for (var i = 0; i < children.length; i++) { |
michael@0 | 2950 | forceValue(children[i], command, newValue); |
michael@0 | 2951 | } |
michael@0 | 2952 | } |
michael@0 | 2953 | } |
michael@0 | 2954 | |
michael@0 | 2955 | |
michael@0 | 2956 | //@} |
michael@0 | 2957 | ///// Setting the selection's value ///// |
michael@0 | 2958 | //@{ |
michael@0 | 2959 | |
michael@0 | 2960 | function setSelectionValue(command, newValue) { |
michael@0 | 2961 | // "If there is no formattable node effectively contained in the active |
michael@0 | 2962 | // range:" |
michael@0 | 2963 | if (!getAllEffectivelyContainedNodes(getActiveRange()) |
michael@0 | 2964 | .some(isFormattableNode)) { |
michael@0 | 2965 | // "If command has inline command activated values, set the state |
michael@0 | 2966 | // override to true if new value is among them and false if it's not." |
michael@0 | 2967 | if ("inlineCommandActivatedValues" in commands[command]) { |
michael@0 | 2968 | setStateOverride(command, commands[command].inlineCommandActivatedValues |
michael@0 | 2969 | .indexOf(newValue) != -1); |
michael@0 | 2970 | } |
michael@0 | 2971 | |
michael@0 | 2972 | // "If command is "subscript", unset the state override for |
michael@0 | 2973 | // "superscript"." |
michael@0 | 2974 | if (command == "subscript") { |
michael@0 | 2975 | unsetStateOverride("superscript"); |
michael@0 | 2976 | } |
michael@0 | 2977 | |
michael@0 | 2978 | // "If command is "superscript", unset the state override for |
michael@0 | 2979 | // "subscript"." |
michael@0 | 2980 | if (command == "superscript") { |
michael@0 | 2981 | unsetStateOverride("subscript"); |
michael@0 | 2982 | } |
michael@0 | 2983 | |
michael@0 | 2984 | // "If new value is null, unset the value override (if any)." |
michael@0 | 2985 | if (newValue === null) { |
michael@0 | 2986 | unsetValueOverride(command); |
michael@0 | 2987 | |
michael@0 | 2988 | // "Otherwise, if command is "createLink" or it has a value specified, |
michael@0 | 2989 | // set the value override to new value." |
michael@0 | 2990 | } else if (command == "createlink" || "value" in commands[command]) { |
michael@0 | 2991 | setValueOverride(command, newValue); |
michael@0 | 2992 | } |
michael@0 | 2993 | |
michael@0 | 2994 | // "Abort these steps." |
michael@0 | 2995 | return; |
michael@0 | 2996 | } |
michael@0 | 2997 | |
michael@0 | 2998 | // "If the active range's start node is an editable Text node, and its |
michael@0 | 2999 | // start offset is neither zero nor its start node's length, call |
michael@0 | 3000 | // splitText() on the active range's start node, with argument equal to the |
michael@0 | 3001 | // active range's start offset. Then set the active range's start node to |
michael@0 | 3002 | // the result, and its start offset to zero." |
michael@0 | 3003 | if (isEditable(getActiveRange().startContainer) |
michael@0 | 3004 | && getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
michael@0 | 3005 | && getActiveRange().startOffset != 0 |
michael@0 | 3006 | && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { |
michael@0 | 3007 | // Account for browsers not following range mutation rules |
michael@0 | 3008 | var newActiveRange = document.createRange(); |
michael@0 | 3009 | var newNode; |
michael@0 | 3010 | if (getActiveRange().startContainer == getActiveRange().endContainer) { |
michael@0 | 3011 | var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset; |
michael@0 | 3012 | newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); |
michael@0 | 3013 | newActiveRange.setEnd(newNode, newEndOffset); |
michael@0 | 3014 | getActiveRange().setEnd(newNode, newEndOffset); |
michael@0 | 3015 | } else { |
michael@0 | 3016 | newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); |
michael@0 | 3017 | } |
michael@0 | 3018 | newActiveRange.setStart(newNode, 0); |
michael@0 | 3019 | getSelection().removeAllRanges(); |
michael@0 | 3020 | getSelection().addRange(newActiveRange); |
michael@0 | 3021 | |
michael@0 | 3022 | getActiveRange().setStart(newNode, 0); |
michael@0 | 3023 | } |
michael@0 | 3024 | |
michael@0 | 3025 | // "If the active range's end node is an editable Text node, and its end |
michael@0 | 3026 | // offset is neither zero nor its end node's length, call splitText() on |
michael@0 | 3027 | // the active range's end node, with argument equal to the active range's |
michael@0 | 3028 | // end offset." |
michael@0 | 3029 | if (isEditable(getActiveRange().endContainer) |
michael@0 | 3030 | && getActiveRange().endContainer.nodeType == Node.TEXT_NODE |
michael@0 | 3031 | && getActiveRange().endOffset != 0 |
michael@0 | 3032 | && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { |
michael@0 | 3033 | // IE seems to mutate the range incorrectly here, so we need correction |
michael@0 | 3034 | // here as well. The active range will be temporarily in orphaned |
michael@0 | 3035 | // nodes, so calling getActiveRange() after splitText() but before |
michael@0 | 3036 | // fixing the range will throw an exception. |
michael@0 | 3037 | var activeRange = getActiveRange(); |
michael@0 | 3038 | var newStart = [activeRange.startContainer, activeRange.startOffset]; |
michael@0 | 3039 | var newEnd = [activeRange.endContainer, activeRange.endOffset]; |
michael@0 | 3040 | activeRange.endContainer.splitText(activeRange.endOffset); |
michael@0 | 3041 | activeRange.setStart(newStart[0], newStart[1]); |
michael@0 | 3042 | activeRange.setEnd(newEnd[0], newEnd[1]); |
michael@0 | 3043 | |
michael@0 | 3044 | getSelection().removeAllRanges(); |
michael@0 | 3045 | getSelection().addRange(activeRange); |
michael@0 | 3046 | } |
michael@0 | 3047 | |
michael@0 | 3048 | // "Let element list be all editable Elements effectively contained in the |
michael@0 | 3049 | // active range. |
michael@0 | 3050 | // |
michael@0 | 3051 | // "For each element in element list, clear the value of element." |
michael@0 | 3052 | getAllEffectivelyContainedNodes(getActiveRange(), function(node) { |
michael@0 | 3053 | return isEditable(node) && node.nodeType == Node.ELEMENT_NODE; |
michael@0 | 3054 | }).forEach(function(element) { |
michael@0 | 3055 | clearValue(element, command); |
michael@0 | 3056 | }); |
michael@0 | 3057 | |
michael@0 | 3058 | // "Let node list be all editable nodes effectively contained in the active |
michael@0 | 3059 | // range. |
michael@0 | 3060 | // |
michael@0 | 3061 | // "For each node in node list:" |
michael@0 | 3062 | getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) { |
michael@0 | 3063 | // "Push down values on node." |
michael@0 | 3064 | pushDownValues(node, command, newValue); |
michael@0 | 3065 | |
michael@0 | 3066 | // "If node is an allowed child of span, force the value of node." |
michael@0 | 3067 | if (isAllowedChild(node, "span")) { |
michael@0 | 3068 | forceValue(node, command, newValue); |
michael@0 | 3069 | } |
michael@0 | 3070 | }); |
michael@0 | 3071 | } |
michael@0 | 3072 | |
michael@0 | 3073 | |
michael@0 | 3074 | //@} |
michael@0 | 3075 | ///// The backColor command ///// |
michael@0 | 3076 | //@{ |
michael@0 | 3077 | commands.backcolor = { |
michael@0 | 3078 | // Copy-pasted, same as hiliteColor |
michael@0 | 3079 | action: function(value) { |
michael@0 | 3080 | // Action is further copy-pasted, same as foreColor |
michael@0 | 3081 | |
michael@0 | 3082 | // "If value is not a valid CSS color, prepend "#" to it." |
michael@0 | 3083 | // |
michael@0 | 3084 | // "If value is still not a valid CSS color, or if it is currentColor, |
michael@0 | 3085 | // return false." |
michael@0 | 3086 | // |
michael@0 | 3087 | // Cheap hack for testing, no attempt to be comprehensive. |
michael@0 | 3088 | if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
michael@0 | 3089 | value = "#" + value; |
michael@0 | 3090 | } |
michael@0 | 3091 | if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
michael@0 | 3092 | && !parseSimpleColor(value) |
michael@0 | 3093 | && value.toLowerCase() != "transparent") { |
michael@0 | 3094 | return false; |
michael@0 | 3095 | } |
michael@0 | 3096 | |
michael@0 | 3097 | // "Set the selection's value to value." |
michael@0 | 3098 | setSelectionValue("backcolor", value); |
michael@0 | 3099 | |
michael@0 | 3100 | // "Return true." |
michael@0 | 3101 | return true; |
michael@0 | 3102 | }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", |
michael@0 | 3103 | equivalentValues: function(val1, val2) { |
michael@0 | 3104 | // "Either both strings are valid CSS colors and have the same red, |
michael@0 | 3105 | // green, blue, and alpha components, or neither string is a valid CSS |
michael@0 | 3106 | // color." |
michael@0 | 3107 | return normalizeColor(val1) === normalizeColor(val2); |
michael@0 | 3108 | }, |
michael@0 | 3109 | }; |
michael@0 | 3110 | |
michael@0 | 3111 | //@} |
michael@0 | 3112 | ///// The bold command ///// |
michael@0 | 3113 | //@{ |
michael@0 | 3114 | commands.bold = { |
michael@0 | 3115 | action: function() { |
michael@0 | 3116 | // "If queryCommandState("bold") returns true, set the selection's |
michael@0 | 3117 | // value to "normal". Otherwise set the selection's value to "bold". |
michael@0 | 3118 | // Either way, return true." |
michael@0 | 3119 | if (myQueryCommandState("bold")) { |
michael@0 | 3120 | setSelectionValue("bold", "normal"); |
michael@0 | 3121 | } else { |
michael@0 | 3122 | setSelectionValue("bold", "bold"); |
michael@0 | 3123 | } |
michael@0 | 3124 | return true; |
michael@0 | 3125 | }, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"], |
michael@0 | 3126 | relevantCssProperty: "fontWeight", |
michael@0 | 3127 | equivalentValues: function(val1, val2) { |
michael@0 | 3128 | // "Either the two strings are equal, or one is "bold" and the other is |
michael@0 | 3129 | // "700", or one is "normal" and the other is "400"." |
michael@0 | 3130 | return val1 == val2 |
michael@0 | 3131 | || (val1 == "bold" && val2 == "700") |
michael@0 | 3132 | || (val1 == "700" && val2 == "bold") |
michael@0 | 3133 | || (val1 == "normal" && val2 == "400") |
michael@0 | 3134 | || (val1 == "400" && val2 == "normal"); |
michael@0 | 3135 | }, |
michael@0 | 3136 | }; |
michael@0 | 3137 | |
michael@0 | 3138 | //@} |
michael@0 | 3139 | ///// The createLink command ///// |
michael@0 | 3140 | //@{ |
michael@0 | 3141 | commands.createlink = { |
michael@0 | 3142 | action: function(value) { |
michael@0 | 3143 | // "If value is the empty string, return false." |
michael@0 | 3144 | if (value === "") { |
michael@0 | 3145 | return false; |
michael@0 | 3146 | } |
michael@0 | 3147 | |
michael@0 | 3148 | // "For each editable a element that has an href attribute and is an |
michael@0 | 3149 | // ancestor of some node effectively contained in the active range, set |
michael@0 | 3150 | // that a element's href attribute to value." |
michael@0 | 3151 | // |
michael@0 | 3152 | // TODO: We don't actually do this in tree order, not that it matters |
michael@0 | 3153 | // unless you're spying with mutation events. |
michael@0 | 3154 | getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) { |
michael@0 | 3155 | getAncestors(node).forEach(function(ancestor) { |
michael@0 | 3156 | if (isEditable(ancestor) |
michael@0 | 3157 | && isHtmlElement(ancestor, "a") |
michael@0 | 3158 | && ancestor.hasAttribute("href")) { |
michael@0 | 3159 | ancestor.setAttribute("href", value); |
michael@0 | 3160 | } |
michael@0 | 3161 | }); |
michael@0 | 3162 | }); |
michael@0 | 3163 | |
michael@0 | 3164 | // "Set the selection's value to value." |
michael@0 | 3165 | setSelectionValue("createlink", value); |
michael@0 | 3166 | |
michael@0 | 3167 | // "Return true." |
michael@0 | 3168 | return true; |
michael@0 | 3169 | } |
michael@0 | 3170 | }; |
michael@0 | 3171 | |
michael@0 | 3172 | //@} |
michael@0 | 3173 | ///// The fontName command ///// |
michael@0 | 3174 | //@{ |
michael@0 | 3175 | commands.fontname = { |
michael@0 | 3176 | action: function(value) { |
michael@0 | 3177 | // "Set the selection's value to value, then return true." |
michael@0 | 3178 | setSelectionValue("fontname", value); |
michael@0 | 3179 | return true; |
michael@0 | 3180 | }, standardInlineValueCommand: true, relevantCssProperty: "fontFamily" |
michael@0 | 3181 | }; |
michael@0 | 3182 | |
michael@0 | 3183 | //@} |
michael@0 | 3184 | ///// The fontSize command ///// |
michael@0 | 3185 | //@{ |
michael@0 | 3186 | |
michael@0 | 3187 | // Helper function for fontSize's action plus queryOutputHelper. It's just the |
michael@0 | 3188 | // middle of fontSize's action, ripped out into its own function. Returns null |
michael@0 | 3189 | // if the size is invalid. |
michael@0 | 3190 | function normalizeFontSize(value) { |
michael@0 | 3191 | // "Strip leading and trailing whitespace from value." |
michael@0 | 3192 | // |
michael@0 | 3193 | // Cheap hack, not following the actual algorithm. |
michael@0 | 3194 | value = value.trim(); |
michael@0 | 3195 | |
michael@0 | 3196 | // "If value is not a valid floating point number, and would not be a valid |
michael@0 | 3197 | // floating point number if a single leading "+" character were stripped, |
michael@0 | 3198 | // return false." |
michael@0 | 3199 | if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) { |
michael@0 | 3200 | return null; |
michael@0 | 3201 | } |
michael@0 | 3202 | |
michael@0 | 3203 | var mode; |
michael@0 | 3204 | |
michael@0 | 3205 | // "If the first character of value is "+", delete the character and let |
michael@0 | 3206 | // mode be "relative-plus"." |
michael@0 | 3207 | if (value[0] == "+") { |
michael@0 | 3208 | value = value.slice(1); |
michael@0 | 3209 | mode = "relative-plus"; |
michael@0 | 3210 | // "Otherwise, if the first character of value is "-", delete the character |
michael@0 | 3211 | // and let mode be "relative-minus"." |
michael@0 | 3212 | } else if (value[0] == "-") { |
michael@0 | 3213 | value = value.slice(1); |
michael@0 | 3214 | mode = "relative-minus"; |
michael@0 | 3215 | // "Otherwise, let mode be "absolute"." |
michael@0 | 3216 | } else { |
michael@0 | 3217 | mode = "absolute"; |
michael@0 | 3218 | } |
michael@0 | 3219 | |
michael@0 | 3220 | // "Apply the rules for parsing non-negative integers to value, and let |
michael@0 | 3221 | // number be the result." |
michael@0 | 3222 | // |
michael@0 | 3223 | // Another cheap hack. |
michael@0 | 3224 | var num = parseInt(value); |
michael@0 | 3225 | |
michael@0 | 3226 | // "If mode is "relative-plus", add three to number." |
michael@0 | 3227 | if (mode == "relative-plus") { |
michael@0 | 3228 | num += 3; |
michael@0 | 3229 | } |
michael@0 | 3230 | |
michael@0 | 3231 | // "If mode is "relative-minus", negate number, then add three to it." |
michael@0 | 3232 | if (mode == "relative-minus") { |
michael@0 | 3233 | num = 3 - num; |
michael@0 | 3234 | } |
michael@0 | 3235 | |
michael@0 | 3236 | // "If number is less than one, let number equal 1." |
michael@0 | 3237 | if (num < 1) { |
michael@0 | 3238 | num = 1; |
michael@0 | 3239 | } |
michael@0 | 3240 | |
michael@0 | 3241 | // "If number is greater than seven, let number equal 7." |
michael@0 | 3242 | if (num > 7) { |
michael@0 | 3243 | num = 7; |
michael@0 | 3244 | } |
michael@0 | 3245 | |
michael@0 | 3246 | // "Set value to the string here corresponding to number:" [table omitted] |
michael@0 | 3247 | value = { |
michael@0 | 3248 | 1: "x-small", |
michael@0 | 3249 | 2: "small", |
michael@0 | 3250 | 3: "medium", |
michael@0 | 3251 | 4: "large", |
michael@0 | 3252 | 5: "x-large", |
michael@0 | 3253 | 6: "xx-large", |
michael@0 | 3254 | 7: "xxx-large" |
michael@0 | 3255 | }[num]; |
michael@0 | 3256 | |
michael@0 | 3257 | return value; |
michael@0 | 3258 | } |
michael@0 | 3259 | |
michael@0 | 3260 | commands.fontsize = { |
michael@0 | 3261 | action: function(value) { |
michael@0 | 3262 | value = normalizeFontSize(value); |
michael@0 | 3263 | if (value === null) { |
michael@0 | 3264 | return false; |
michael@0 | 3265 | } |
michael@0 | 3266 | |
michael@0 | 3267 | // "Set the selection's value to value." |
michael@0 | 3268 | setSelectionValue("fontsize", value); |
michael@0 | 3269 | |
michael@0 | 3270 | // "Return true." |
michael@0 | 3271 | return true; |
michael@0 | 3272 | }, indeterm: function() { |
michael@0 | 3273 | // "True if among formattable nodes that are effectively contained in |
michael@0 | 3274 | // the active range, there are two that have distinct effective command |
michael@0 | 3275 | // values. Otherwise false." |
michael@0 | 3276 | return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode) |
michael@0 | 3277 | .map(function(node) { |
michael@0 | 3278 | return getEffectiveCommandValue(node, "fontsize"); |
michael@0 | 3279 | }).filter(function(value, i, arr) { |
michael@0 | 3280 | return arr.slice(0, i).indexOf(value) == -1; |
michael@0 | 3281 | }).length >= 2; |
michael@0 | 3282 | }, value: function() { |
michael@0 | 3283 | // "If the active range is null, return the empty string." |
michael@0 | 3284 | if (!getActiveRange()) { |
michael@0 | 3285 | return ""; |
michael@0 | 3286 | } |
michael@0 | 3287 | |
michael@0 | 3288 | // "Let pixel size be the effective command value of the first |
michael@0 | 3289 | // formattable node that is effectively contained in the active range, |
michael@0 | 3290 | // or if there is no such node, the effective command value of the |
michael@0 | 3291 | // active range's start node, in either case interpreted as a number of |
michael@0 | 3292 | // pixels." |
michael@0 | 3293 | var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0]; |
michael@0 | 3294 | if (node === undefined) { |
michael@0 | 3295 | node = getActiveRange().startContainer; |
michael@0 | 3296 | } |
michael@0 | 3297 | var pixelSize = getEffectiveCommandValue(node, "fontsize"); |
michael@0 | 3298 | |
michael@0 | 3299 | // "Return the legacy font size for pixel size." |
michael@0 | 3300 | return getLegacyFontSize(pixelSize); |
michael@0 | 3301 | }, relevantCssProperty: "fontSize" |
michael@0 | 3302 | }; |
michael@0 | 3303 | |
michael@0 | 3304 | function getLegacyFontSize(size) { |
michael@0 | 3305 | if (getLegacyFontSize.resultCache === undefined) { |
michael@0 | 3306 | getLegacyFontSize.resultCache = {}; |
michael@0 | 3307 | } |
michael@0 | 3308 | |
michael@0 | 3309 | if (getLegacyFontSize.resultCache[size] !== undefined) { |
michael@0 | 3310 | return getLegacyFontSize.resultCache[size]; |
michael@0 | 3311 | } |
michael@0 | 3312 | |
michael@0 | 3313 | // For convenience in other places in my code, I handle all sizes, not just |
michael@0 | 3314 | // pixel sizes as the spec says. This means pixel sizes have to be passed |
michael@0 | 3315 | // in suffixed with "px", not as plain numbers. |
michael@0 | 3316 | if (normalizeFontSize(size) !== null) { |
michael@0 | 3317 | return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size)); |
michael@0 | 3318 | } |
michael@0 | 3319 | |
michael@0 | 3320 | if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1 |
michael@0 | 3321 | && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) { |
michael@0 | 3322 | // There is no sensible legacy size for things like "2em". |
michael@0 | 3323 | return getLegacyFontSize.resultCache[size] = null; |
michael@0 | 3324 | } |
michael@0 | 3325 | |
michael@0 | 3326 | var font = document.createElement("font"); |
michael@0 | 3327 | document.body.appendChild(font); |
michael@0 | 3328 | if (size == "xxx-large") { |
michael@0 | 3329 | font.size = 7; |
michael@0 | 3330 | } else { |
michael@0 | 3331 | font.style.fontSize = size; |
michael@0 | 3332 | } |
michael@0 | 3333 | var pixelSize = parseInt(getComputedStyle(font).fontSize); |
michael@0 | 3334 | document.body.removeChild(font); |
michael@0 | 3335 | |
michael@0 | 3336 | // "Let returned size be 1." |
michael@0 | 3337 | var returnedSize = 1; |
michael@0 | 3338 | |
michael@0 | 3339 | // "While returned size is less than 7:" |
michael@0 | 3340 | while (returnedSize < 7) { |
michael@0 | 3341 | // "Let lower bound be the resolved value of "font-size" in pixels |
michael@0 | 3342 | // of a font element whose size attribute is set to returned size." |
michael@0 | 3343 | var font = document.createElement("font"); |
michael@0 | 3344 | font.size = returnedSize; |
michael@0 | 3345 | document.body.appendChild(font); |
michael@0 | 3346 | var lowerBound = parseInt(getComputedStyle(font).fontSize); |
michael@0 | 3347 | |
michael@0 | 3348 | // "Let upper bound be the resolved value of "font-size" in pixels |
michael@0 | 3349 | // of a font element whose size attribute is set to one plus |
michael@0 | 3350 | // returned size." |
michael@0 | 3351 | font.size = 1 + returnedSize; |
michael@0 | 3352 | var upperBound = parseInt(getComputedStyle(font).fontSize); |
michael@0 | 3353 | document.body.removeChild(font); |
michael@0 | 3354 | |
michael@0 | 3355 | // "Let average be the average of upper bound and lower bound." |
michael@0 | 3356 | var average = (upperBound + lowerBound)/2; |
michael@0 | 3357 | |
michael@0 | 3358 | // "If pixel size is less than average, return the one-element |
michael@0 | 3359 | // string consisting of the digit returned size." |
michael@0 | 3360 | if (pixelSize < average) { |
michael@0 | 3361 | return getLegacyFontSize.resultCache[size] = String(returnedSize); |
michael@0 | 3362 | } |
michael@0 | 3363 | |
michael@0 | 3364 | // "Add one to returned size." |
michael@0 | 3365 | returnedSize++; |
michael@0 | 3366 | } |
michael@0 | 3367 | |
michael@0 | 3368 | // "Return "7"." |
michael@0 | 3369 | return getLegacyFontSize.resultCache[size] = "7"; |
michael@0 | 3370 | } |
michael@0 | 3371 | |
michael@0 | 3372 | //@} |
michael@0 | 3373 | ///// The foreColor command ///// |
michael@0 | 3374 | //@{ |
michael@0 | 3375 | commands.forecolor = { |
michael@0 | 3376 | action: function(value) { |
michael@0 | 3377 | // Copy-pasted, same as backColor and hiliteColor |
michael@0 | 3378 | |
michael@0 | 3379 | // "If value is not a valid CSS color, prepend "#" to it." |
michael@0 | 3380 | // |
michael@0 | 3381 | // "If value is still not a valid CSS color, or if it is currentColor, |
michael@0 | 3382 | // return false." |
michael@0 | 3383 | // |
michael@0 | 3384 | // Cheap hack for testing, no attempt to be comprehensive. |
michael@0 | 3385 | if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
michael@0 | 3386 | value = "#" + value; |
michael@0 | 3387 | } |
michael@0 | 3388 | if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
michael@0 | 3389 | && !parseSimpleColor(value) |
michael@0 | 3390 | && value.toLowerCase() != "transparent") { |
michael@0 | 3391 | return false; |
michael@0 | 3392 | } |
michael@0 | 3393 | |
michael@0 | 3394 | // "Set the selection's value to value." |
michael@0 | 3395 | setSelectionValue("forecolor", value); |
michael@0 | 3396 | |
michael@0 | 3397 | // "Return true." |
michael@0 | 3398 | return true; |
michael@0 | 3399 | }, standardInlineValueCommand: true, relevantCssProperty: "color", |
michael@0 | 3400 | equivalentValues: function(val1, val2) { |
michael@0 | 3401 | // "Either both strings are valid CSS colors and have the same red, |
michael@0 | 3402 | // green, blue, and alpha components, or neither string is a valid CSS |
michael@0 | 3403 | // color." |
michael@0 | 3404 | return normalizeColor(val1) === normalizeColor(val2); |
michael@0 | 3405 | }, |
michael@0 | 3406 | }; |
michael@0 | 3407 | |
michael@0 | 3408 | //@} |
michael@0 | 3409 | ///// The hiliteColor command ///// |
michael@0 | 3410 | //@{ |
michael@0 | 3411 | commands.hilitecolor = { |
michael@0 | 3412 | // Copy-pasted, same as backColor |
michael@0 | 3413 | action: function(value) { |
michael@0 | 3414 | // Action is further copy-pasted, same as foreColor |
michael@0 | 3415 | |
michael@0 | 3416 | // "If value is not a valid CSS color, prepend "#" to it." |
michael@0 | 3417 | // |
michael@0 | 3418 | // "If value is still not a valid CSS color, or if it is currentColor, |
michael@0 | 3419 | // return false." |
michael@0 | 3420 | // |
michael@0 | 3421 | // Cheap hack for testing, no attempt to be comprehensive. |
michael@0 | 3422 | if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { |
michael@0 | 3423 | value = "#" + value; |
michael@0 | 3424 | } |
michael@0 | 3425 | if (!/^(rgba?|hsla?)\(.*\)$/.test(value) |
michael@0 | 3426 | && !parseSimpleColor(value) |
michael@0 | 3427 | && value.toLowerCase() != "transparent") { |
michael@0 | 3428 | return false; |
michael@0 | 3429 | } |
michael@0 | 3430 | |
michael@0 | 3431 | // "Set the selection's value to value." |
michael@0 | 3432 | setSelectionValue("hilitecolor", value); |
michael@0 | 3433 | |
michael@0 | 3434 | // "Return true." |
michael@0 | 3435 | return true; |
michael@0 | 3436 | }, indeterm: function() { |
michael@0 | 3437 | // "True if among editable Text nodes that are effectively contained in |
michael@0 | 3438 | // the active range, there are two that have distinct effective command |
michael@0 | 3439 | // values. Otherwise false." |
michael@0 | 3440 | return getAllEffectivelyContainedNodes(getActiveRange(), function(node) { |
michael@0 | 3441 | return isEditable(node) && node.nodeType == Node.TEXT_NODE; |
michael@0 | 3442 | }).map(function(node) { |
michael@0 | 3443 | return getEffectiveCommandValue(node, "hilitecolor"); |
michael@0 | 3444 | }).filter(function(value, i, arr) { |
michael@0 | 3445 | return arr.slice(0, i).indexOf(value) == -1; |
michael@0 | 3446 | }).length >= 2; |
michael@0 | 3447 | }, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor", |
michael@0 | 3448 | equivalentValues: function(val1, val2) { |
michael@0 | 3449 | // "Either both strings are valid CSS colors and have the same red, |
michael@0 | 3450 | // green, blue, and alpha components, or neither string is a valid CSS |
michael@0 | 3451 | // color." |
michael@0 | 3452 | return normalizeColor(val1) === normalizeColor(val2); |
michael@0 | 3453 | }, |
michael@0 | 3454 | }; |
michael@0 | 3455 | |
michael@0 | 3456 | //@} |
michael@0 | 3457 | ///// The italic command ///// |
michael@0 | 3458 | //@{ |
michael@0 | 3459 | commands.italic = { |
michael@0 | 3460 | action: function() { |
michael@0 | 3461 | // "If queryCommandState("italic") returns true, set the selection's |
michael@0 | 3462 | // value to "normal". Otherwise set the selection's value to "italic". |
michael@0 | 3463 | // Either way, return true." |
michael@0 | 3464 | if (myQueryCommandState("italic")) { |
michael@0 | 3465 | setSelectionValue("italic", "normal"); |
michael@0 | 3466 | } else { |
michael@0 | 3467 | setSelectionValue("italic", "italic"); |
michael@0 | 3468 | } |
michael@0 | 3469 | return true; |
michael@0 | 3470 | }, inlineCommandActivatedValues: ["italic", "oblique"], |
michael@0 | 3471 | relevantCssProperty: "fontStyle" |
michael@0 | 3472 | }; |
michael@0 | 3473 | |
michael@0 | 3474 | //@} |
michael@0 | 3475 | ///// The removeFormat command ///// |
michael@0 | 3476 | //@{ |
michael@0 | 3477 | commands.removeformat = { |
michael@0 | 3478 | action: function() { |
michael@0 | 3479 | // "A removeFormat candidate is an editable HTML element with local |
michael@0 | 3480 | // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", |
michael@0 | 3481 | // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", |
michael@0 | 3482 | // "s", "samp", "small", "span", "strike", "strong", "sub", "sup", |
michael@0 | 3483 | // "tt", "u", or "var"." |
michael@0 | 3484 | function isRemoveFormatCandidate(node) { |
michael@0 | 3485 | return isEditable(node) |
michael@0 | 3486 | && isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo", |
michael@0 | 3487 | "big", "blink", "cite", "code", "dfn", "em", "font", "i", |
michael@0 | 3488 | "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", |
michael@0 | 3489 | "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]); |
michael@0 | 3490 | } |
michael@0 | 3491 | |
michael@0 | 3492 | // "Let elements to remove be a list of every removeFormat candidate |
michael@0 | 3493 | // effectively contained in the active range." |
michael@0 | 3494 | var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate); |
michael@0 | 3495 | |
michael@0 | 3496 | // "For each element in elements to remove:" |
michael@0 | 3497 | elementsToRemove.forEach(function(element) { |
michael@0 | 3498 | // "While element has children, insert the first child of element |
michael@0 | 3499 | // into the parent of element immediately before element, |
michael@0 | 3500 | // preserving ranges." |
michael@0 | 3501 | while (element.hasChildNodes()) { |
michael@0 | 3502 | movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element)); |
michael@0 | 3503 | } |
michael@0 | 3504 | |
michael@0 | 3505 | // "Remove element from its parent." |
michael@0 | 3506 | element.parentNode.removeChild(element); |
michael@0 | 3507 | }); |
michael@0 | 3508 | |
michael@0 | 3509 | // "If the active range's start node is an editable Text node, and its |
michael@0 | 3510 | // start offset is neither zero nor its start node's length, call |
michael@0 | 3511 | // splitText() on the active range's start node, with argument equal to |
michael@0 | 3512 | // the active range's start offset. Then set the active range's start |
michael@0 | 3513 | // node to the result, and its start offset to zero." |
michael@0 | 3514 | if (isEditable(getActiveRange().startContainer) |
michael@0 | 3515 | && getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
michael@0 | 3516 | && getActiveRange().startOffset != 0 |
michael@0 | 3517 | && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { |
michael@0 | 3518 | // Account for browsers not following range mutation rules |
michael@0 | 3519 | if (getActiveRange().startContainer == getActiveRange().endContainer) { |
michael@0 | 3520 | var newEnd = getActiveRange().endOffset - getActiveRange().startOffset; |
michael@0 | 3521 | var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset); |
michael@0 | 3522 | getActiveRange().setStart(newNode, 0); |
michael@0 | 3523 | getActiveRange().setEnd(newNode, newEnd); |
michael@0 | 3524 | } else { |
michael@0 | 3525 | getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0); |
michael@0 | 3526 | } |
michael@0 | 3527 | } |
michael@0 | 3528 | |
michael@0 | 3529 | // "If the active range's end node is an editable Text node, and its |
michael@0 | 3530 | // end offset is neither zero nor its end node's length, call |
michael@0 | 3531 | // splitText() on the active range's end node, with argument equal to |
michael@0 | 3532 | // the active range's end offset." |
michael@0 | 3533 | if (isEditable(getActiveRange().endContainer) |
michael@0 | 3534 | && getActiveRange().endContainer.nodeType == Node.TEXT_NODE |
michael@0 | 3535 | && getActiveRange().endOffset != 0 |
michael@0 | 3536 | && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { |
michael@0 | 3537 | // IE seems to mutate the range incorrectly here, so we need |
michael@0 | 3538 | // correction here as well. Have to be careful to set the range to |
michael@0 | 3539 | // something not including the text node so that getActiveRange() |
michael@0 | 3540 | // doesn't throw an exception due to a temporarily detached |
michael@0 | 3541 | // endpoint. |
michael@0 | 3542 | var newStart = [getActiveRange().startContainer, getActiveRange().startOffset]; |
michael@0 | 3543 | var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset]; |
michael@0 | 3544 | getActiveRange().setEnd(document.documentElement, 0); |
michael@0 | 3545 | newEnd[0].splitText(newEnd[1]); |
michael@0 | 3546 | getActiveRange().setStart(newStart[0], newStart[1]); |
michael@0 | 3547 | getActiveRange().setEnd(newEnd[0], newEnd[1]); |
michael@0 | 3548 | } |
michael@0 | 3549 | |
michael@0 | 3550 | // "Let node list consist of all editable nodes effectively contained |
michael@0 | 3551 | // in the active range." |
michael@0 | 3552 | // |
michael@0 | 3553 | // "For each node in node list, while node's parent is a removeFormat |
michael@0 | 3554 | // candidate in the same editing host as node, split the parent of the |
michael@0 | 3555 | // one-node list consisting of node." |
michael@0 | 3556 | getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) { |
michael@0 | 3557 | while (isRemoveFormatCandidate(node.parentNode) |
michael@0 | 3558 | && inSameEditingHost(node.parentNode, node)) { |
michael@0 | 3559 | splitParent([node]); |
michael@0 | 3560 | } |
michael@0 | 3561 | }); |
michael@0 | 3562 | |
michael@0 | 3563 | // "For each of the entries in the following list, in the given order, |
michael@0 | 3564 | // set the selection's value to null, with command as given." |
michael@0 | 3565 | [ |
michael@0 | 3566 | "subscript", |
michael@0 | 3567 | "bold", |
michael@0 | 3568 | "fontname", |
michael@0 | 3569 | "fontsize", |
michael@0 | 3570 | "forecolor", |
michael@0 | 3571 | "hilitecolor", |
michael@0 | 3572 | "italic", |
michael@0 | 3573 | "strikethrough", |
michael@0 | 3574 | "underline", |
michael@0 | 3575 | ].forEach(function(command) { |
michael@0 | 3576 | setSelectionValue(command, null); |
michael@0 | 3577 | }); |
michael@0 | 3578 | |
michael@0 | 3579 | // "Return true." |
michael@0 | 3580 | return true; |
michael@0 | 3581 | } |
michael@0 | 3582 | }; |
michael@0 | 3583 | |
michael@0 | 3584 | //@} |
michael@0 | 3585 | ///// The strikethrough command ///// |
michael@0 | 3586 | //@{ |
michael@0 | 3587 | commands.strikethrough = { |
michael@0 | 3588 | action: function() { |
michael@0 | 3589 | // "If queryCommandState("strikethrough") returns true, set the |
michael@0 | 3590 | // selection's value to null. Otherwise set the selection's value to |
michael@0 | 3591 | // "line-through". Either way, return true." |
michael@0 | 3592 | if (myQueryCommandState("strikethrough")) { |
michael@0 | 3593 | setSelectionValue("strikethrough", null); |
michael@0 | 3594 | } else { |
michael@0 | 3595 | setSelectionValue("strikethrough", "line-through"); |
michael@0 | 3596 | } |
michael@0 | 3597 | return true; |
michael@0 | 3598 | }, inlineCommandActivatedValues: ["line-through"] |
michael@0 | 3599 | }; |
michael@0 | 3600 | |
michael@0 | 3601 | //@} |
michael@0 | 3602 | ///// The subscript command ///// |
michael@0 | 3603 | //@{ |
michael@0 | 3604 | commands.subscript = { |
michael@0 | 3605 | action: function() { |
michael@0 | 3606 | // "Call queryCommandState("subscript"), and let state be the result." |
michael@0 | 3607 | var state = myQueryCommandState("subscript"); |
michael@0 | 3608 | |
michael@0 | 3609 | // "Set the selection's value to null." |
michael@0 | 3610 | setSelectionValue("subscript", null); |
michael@0 | 3611 | |
michael@0 | 3612 | // "If state is false, set the selection's value to "subscript"." |
michael@0 | 3613 | if (!state) { |
michael@0 | 3614 | setSelectionValue("subscript", "subscript"); |
michael@0 | 3615 | } |
michael@0 | 3616 | |
michael@0 | 3617 | // "Return true." |
michael@0 | 3618 | return true; |
michael@0 | 3619 | }, indeterm: function() { |
michael@0 | 3620 | // "True if either among formattable nodes that are effectively |
michael@0 | 3621 | // contained in the active range, there is at least one with effective |
michael@0 | 3622 | // command value "subscript" and at least one with some other effective |
michael@0 | 3623 | // command value; or if there is some formattable node effectively |
michael@0 | 3624 | // contained in the active range with effective command value "mixed". |
michael@0 | 3625 | // Otherwise false." |
michael@0 | 3626 | var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); |
michael@0 | 3627 | return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" }) |
michael@0 | 3628 | && nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" })) |
michael@0 | 3629 | || nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" }); |
michael@0 | 3630 | }, inlineCommandActivatedValues: ["subscript"], |
michael@0 | 3631 | }; |
michael@0 | 3632 | |
michael@0 | 3633 | //@} |
michael@0 | 3634 | ///// The superscript command ///// |
michael@0 | 3635 | //@{ |
michael@0 | 3636 | commands.superscript = { |
michael@0 | 3637 | action: function() { |
michael@0 | 3638 | // "Call queryCommandState("superscript"), and let state be the |
michael@0 | 3639 | // result." |
michael@0 | 3640 | var state = myQueryCommandState("superscript"); |
michael@0 | 3641 | |
michael@0 | 3642 | // "Set the selection's value to null." |
michael@0 | 3643 | setSelectionValue("superscript", null); |
michael@0 | 3644 | |
michael@0 | 3645 | // "If state is false, set the selection's value to "superscript"." |
michael@0 | 3646 | if (!state) { |
michael@0 | 3647 | setSelectionValue("superscript", "superscript"); |
michael@0 | 3648 | } |
michael@0 | 3649 | |
michael@0 | 3650 | // "Return true." |
michael@0 | 3651 | return true; |
michael@0 | 3652 | }, indeterm: function() { |
michael@0 | 3653 | // "True if either among formattable nodes that are effectively |
michael@0 | 3654 | // contained in the active range, there is at least one with effective |
michael@0 | 3655 | // command value "superscript" and at least one with some other |
michael@0 | 3656 | // effective command value; or if there is some formattable node |
michael@0 | 3657 | // effectively contained in the active range with effective command |
michael@0 | 3658 | // value "mixed". Otherwise false." |
michael@0 | 3659 | var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); |
michael@0 | 3660 | return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" }) |
michael@0 | 3661 | && nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" })) |
michael@0 | 3662 | || nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" }); |
michael@0 | 3663 | }, inlineCommandActivatedValues: ["superscript"], |
michael@0 | 3664 | }; |
michael@0 | 3665 | |
michael@0 | 3666 | //@} |
michael@0 | 3667 | ///// The underline command ///// |
michael@0 | 3668 | //@{ |
michael@0 | 3669 | commands.underline = { |
michael@0 | 3670 | action: function() { |
michael@0 | 3671 | // "If queryCommandState("underline") returns true, set the selection's |
michael@0 | 3672 | // value to null. Otherwise set the selection's value to "underline". |
michael@0 | 3673 | // Either way, return true." |
michael@0 | 3674 | if (myQueryCommandState("underline")) { |
michael@0 | 3675 | setSelectionValue("underline", null); |
michael@0 | 3676 | } else { |
michael@0 | 3677 | setSelectionValue("underline", "underline"); |
michael@0 | 3678 | } |
michael@0 | 3679 | return true; |
michael@0 | 3680 | }, inlineCommandActivatedValues: ["underline"] |
michael@0 | 3681 | }; |
michael@0 | 3682 | |
michael@0 | 3683 | //@} |
michael@0 | 3684 | ///// The unlink command ///// |
michael@0 | 3685 | //@{ |
michael@0 | 3686 | commands.unlink = { |
michael@0 | 3687 | action: function() { |
michael@0 | 3688 | // "Let hyperlinks be a list of every a element that has an href |
michael@0 | 3689 | // attribute and is contained in the active range or is an ancestor of |
michael@0 | 3690 | // one of its boundary points." |
michael@0 | 3691 | // |
michael@0 | 3692 | // As usual, take care to ensure it's tree order. The correctness of |
michael@0 | 3693 | // the following is left as an exercise for the reader. |
michael@0 | 3694 | var range = getActiveRange(); |
michael@0 | 3695 | var hyperlinks = []; |
michael@0 | 3696 | for ( |
michael@0 | 3697 | var node = range.startContainer; |
michael@0 | 3698 | node; |
michael@0 | 3699 | node = node.parentNode |
michael@0 | 3700 | ) { |
michael@0 | 3701 | if (isHtmlElement(node, "A") |
michael@0 | 3702 | && node.hasAttribute("href")) { |
michael@0 | 3703 | hyperlinks.unshift(node); |
michael@0 | 3704 | } |
michael@0 | 3705 | } |
michael@0 | 3706 | for ( |
michael@0 | 3707 | var node = range.startContainer; |
michael@0 | 3708 | node != nextNodeDescendants(range.endContainer); |
michael@0 | 3709 | node = nextNode(node) |
michael@0 | 3710 | ) { |
michael@0 | 3711 | if (isHtmlElement(node, "A") |
michael@0 | 3712 | && node.hasAttribute("href") |
michael@0 | 3713 | && (isContained(node, range) |
michael@0 | 3714 | || isAncestor(node, range.endContainer) |
michael@0 | 3715 | || node == range.endContainer)) { |
michael@0 | 3716 | hyperlinks.push(node); |
michael@0 | 3717 | } |
michael@0 | 3718 | } |
michael@0 | 3719 | |
michael@0 | 3720 | // "Clear the value of each member of hyperlinks." |
michael@0 | 3721 | for (var i = 0; i < hyperlinks.length; i++) { |
michael@0 | 3722 | clearValue(hyperlinks[i], "unlink"); |
michael@0 | 3723 | } |
michael@0 | 3724 | |
michael@0 | 3725 | // "Return true." |
michael@0 | 3726 | return true; |
michael@0 | 3727 | } |
michael@0 | 3728 | }; |
michael@0 | 3729 | |
michael@0 | 3730 | //@} |
michael@0 | 3731 | |
michael@0 | 3732 | ///////////////////////////////////// |
michael@0 | 3733 | ///// Block formatting commands ///// |
michael@0 | 3734 | ///////////////////////////////////// |
michael@0 | 3735 | |
michael@0 | 3736 | ///// Block formatting command definitions ///// |
michael@0 | 3737 | //@{ |
michael@0 | 3738 | |
michael@0 | 3739 | // "An indentation element is either a blockquote, or a div that has a style |
michael@0 | 3740 | // attribute that sets "margin" or some subproperty of it." |
michael@0 | 3741 | function isIndentationElement(node) { |
michael@0 | 3742 | if (!isHtmlElement(node)) { |
michael@0 | 3743 | return false; |
michael@0 | 3744 | } |
michael@0 | 3745 | |
michael@0 | 3746 | if (node.tagName == "BLOCKQUOTE") { |
michael@0 | 3747 | return true; |
michael@0 | 3748 | } |
michael@0 | 3749 | |
michael@0 | 3750 | if (node.tagName != "DIV") { |
michael@0 | 3751 | return false; |
michael@0 | 3752 | } |
michael@0 | 3753 | |
michael@0 | 3754 | for (var i = 0; i < node.style.length; i++) { |
michael@0 | 3755 | // Approximate check |
michael@0 | 3756 | if (/^(-[a-z]+-)?margin/.test(node.style[i])) { |
michael@0 | 3757 | return true; |
michael@0 | 3758 | } |
michael@0 | 3759 | } |
michael@0 | 3760 | |
michael@0 | 3761 | return false; |
michael@0 | 3762 | } |
michael@0 | 3763 | |
michael@0 | 3764 | // "A simple indentation element is an indentation element that has no |
michael@0 | 3765 | // attributes except possibly |
michael@0 | 3766 | // |
michael@0 | 3767 | // * "a style attribute that sets no properties other than "margin", |
michael@0 | 3768 | // "border", "padding", or subproperties of those; and/or |
michael@0 | 3769 | // * "a dir attribute." |
michael@0 | 3770 | function isSimpleIndentationElement(node) { |
michael@0 | 3771 | if (!isIndentationElement(node)) { |
michael@0 | 3772 | return false; |
michael@0 | 3773 | } |
michael@0 | 3774 | |
michael@0 | 3775 | for (var i = 0; i < node.attributes.length; i++) { |
michael@0 | 3776 | if (!isHtmlNamespace(node.attributes[i].namespaceURI) |
michael@0 | 3777 | || ["style", "dir"].indexOf(node.attributes[i].name) == -1) { |
michael@0 | 3778 | return false; |
michael@0 | 3779 | } |
michael@0 | 3780 | } |
michael@0 | 3781 | |
michael@0 | 3782 | for (var i = 0; i < node.style.length; i++) { |
michael@0 | 3783 | // This is approximate, but it works well enough for my purposes. |
michael@0 | 3784 | if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) { |
michael@0 | 3785 | return false; |
michael@0 | 3786 | } |
michael@0 | 3787 | } |
michael@0 | 3788 | |
michael@0 | 3789 | return true; |
michael@0 | 3790 | } |
michael@0 | 3791 | |
michael@0 | 3792 | // "A non-list single-line container is an HTML element with local name |
michael@0 | 3793 | // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", |
michael@0 | 3794 | // or "xmp"." |
michael@0 | 3795 | function isNonListSingleLineContainer(node) { |
michael@0 | 3796 | return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", |
michael@0 | 3797 | "h6", "listing", "p", "pre", "xmp"]); |
michael@0 | 3798 | } |
michael@0 | 3799 | |
michael@0 | 3800 | // "A single-line container is either a non-list single-line container, or an |
michael@0 | 3801 | // HTML element with local name "li", "dt", or "dd"." |
michael@0 | 3802 | function isSingleLineContainer(node) { |
michael@0 | 3803 | return isNonListSingleLineContainer(node) |
michael@0 | 3804 | || isHtmlElement(node, ["li", "dt", "dd"]); |
michael@0 | 3805 | } |
michael@0 | 3806 | |
michael@0 | 3807 | function getBlockNodeOf(node) { |
michael@0 | 3808 | // "While node is an inline node, set node to its parent." |
michael@0 | 3809 | while (isInlineNode(node)) { |
michael@0 | 3810 | node = node.parentNode; |
michael@0 | 3811 | } |
michael@0 | 3812 | |
michael@0 | 3813 | // "Return node." |
michael@0 | 3814 | return node; |
michael@0 | 3815 | } |
michael@0 | 3816 | |
michael@0 | 3817 | //@} |
michael@0 | 3818 | ///// Assorted block formatting command algorithms ///// |
michael@0 | 3819 | //@{ |
michael@0 | 3820 | |
michael@0 | 3821 | function fixDisallowedAncestors(node) { |
michael@0 | 3822 | // "If node is not editable, abort these steps." |
michael@0 | 3823 | if (!isEditable(node)) { |
michael@0 | 3824 | return; |
michael@0 | 3825 | } |
michael@0 | 3826 | |
michael@0 | 3827 | // "If node is not an allowed child of any of its ancestors in the same |
michael@0 | 3828 | // editing host:" |
michael@0 | 3829 | if (getAncestors(node).every(function(ancestor) { |
michael@0 | 3830 | return !inSameEditingHost(node, ancestor) |
michael@0 | 3831 | || !isAllowedChild(node, ancestor) |
michael@0 | 3832 | })) { |
michael@0 | 3833 | // "If node is a dd or dt, wrap the one-node list consisting of node, |
michael@0 | 3834 | // with sibling criteria returning true for any dl with no attributes |
michael@0 | 3835 | // and false otherwise, and new parent instructions returning the |
michael@0 | 3836 | // result of calling createElement("dl") on the context object. Then |
michael@0 | 3837 | // abort these steps." |
michael@0 | 3838 | if (isHtmlElement(node, ["dd", "dt"])) { |
michael@0 | 3839 | wrap([node], |
michael@0 | 3840 | function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length }, |
michael@0 | 3841 | function() { return document.createElement("dl") }); |
michael@0 | 3842 | return; |
michael@0 | 3843 | } |
michael@0 | 3844 | |
michael@0 | 3845 | // "If "p" is not an allowed child of the editing host of node, abort |
michael@0 | 3846 | // these steps." |
michael@0 | 3847 | if (!isAllowedChild("p", getEditingHostOf(node))) { |
michael@0 | 3848 | return; |
michael@0 | 3849 | } |
michael@0 | 3850 | |
michael@0 | 3851 | // "If node is not a prohibited paragraph child, abort these steps." |
michael@0 | 3852 | if (!isProhibitedParagraphChild(node)) { |
michael@0 | 3853 | return; |
michael@0 | 3854 | } |
michael@0 | 3855 | |
michael@0 | 3856 | // "Set the tag name of node to the default single-line container name, |
michael@0 | 3857 | // and let node be the result." |
michael@0 | 3858 | node = setTagName(node, defaultSingleLineContainerName); |
michael@0 | 3859 | |
michael@0 | 3860 | // "Fix disallowed ancestors of node." |
michael@0 | 3861 | fixDisallowedAncestors(node); |
michael@0 | 3862 | |
michael@0 | 3863 | // "Let children be node's children." |
michael@0 | 3864 | var children = [].slice.call(node.childNodes); |
michael@0 | 3865 | |
michael@0 | 3866 | // "For each child in children, if child is a prohibited paragraph |
michael@0 | 3867 | // child:" |
michael@0 | 3868 | children.filter(isProhibitedParagraphChild) |
michael@0 | 3869 | .forEach(function(child) { |
michael@0 | 3870 | // "Record the values of the one-node list consisting of child, and |
michael@0 | 3871 | // let values be the result." |
michael@0 | 3872 | var values = recordValues([child]); |
michael@0 | 3873 | |
michael@0 | 3874 | // "Split the parent of the one-node list consisting of child." |
michael@0 | 3875 | splitParent([child]); |
michael@0 | 3876 | |
michael@0 | 3877 | // "Restore the values from values." |
michael@0 | 3878 | restoreValues(values); |
michael@0 | 3879 | }); |
michael@0 | 3880 | |
michael@0 | 3881 | // "Abort these steps." |
michael@0 | 3882 | return; |
michael@0 | 3883 | } |
michael@0 | 3884 | |
michael@0 | 3885 | // "Record the values of the one-node list consisting of node, and let |
michael@0 | 3886 | // values be the result." |
michael@0 | 3887 | var values = recordValues([node]); |
michael@0 | 3888 | |
michael@0 | 3889 | // "While node is not an allowed child of its parent, split the parent of |
michael@0 | 3890 | // the one-node list consisting of node." |
michael@0 | 3891 | while (!isAllowedChild(node, node.parentNode)) { |
michael@0 | 3892 | splitParent([node]); |
michael@0 | 3893 | } |
michael@0 | 3894 | |
michael@0 | 3895 | // "Restore the values from values." |
michael@0 | 3896 | restoreValues(values); |
michael@0 | 3897 | } |
michael@0 | 3898 | |
michael@0 | 3899 | function normalizeSublists(item) { |
michael@0 | 3900 | // "If item is not an li or it is not editable or its parent is not |
michael@0 | 3901 | // editable, abort these steps." |
michael@0 | 3902 | if (!isHtmlElement(item, "LI") |
michael@0 | 3903 | || !isEditable(item) |
michael@0 | 3904 | || !isEditable(item.parentNode)) { |
michael@0 | 3905 | return; |
michael@0 | 3906 | } |
michael@0 | 3907 | |
michael@0 | 3908 | // "Let new item be null." |
michael@0 | 3909 | var newItem = null; |
michael@0 | 3910 | |
michael@0 | 3911 | // "While item has an ol or ul child:" |
michael@0 | 3912 | while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) { |
michael@0 | 3913 | // "Let child be the last child of item." |
michael@0 | 3914 | var child = item.lastChild; |
michael@0 | 3915 | |
michael@0 | 3916 | // "If child is an ol or ul, or new item is null and child is a Text |
michael@0 | 3917 | // node whose data consists of zero of more space characters:" |
michael@0 | 3918 | if (isHtmlElement(child, ["OL", "UL"]) |
michael@0 | 3919 | || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) { |
michael@0 | 3920 | // "Set new item to null." |
michael@0 | 3921 | newItem = null; |
michael@0 | 3922 | |
michael@0 | 3923 | // "Insert child into the parent of item immediately following |
michael@0 | 3924 | // item, preserving ranges." |
michael@0 | 3925 | movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item)); |
michael@0 | 3926 | |
michael@0 | 3927 | // "Otherwise:" |
michael@0 | 3928 | } else { |
michael@0 | 3929 | // "If new item is null, let new item be the result of calling |
michael@0 | 3930 | // createElement("li") on the ownerDocument of item, then insert |
michael@0 | 3931 | // new item into the parent of item immediately after item." |
michael@0 | 3932 | if (!newItem) { |
michael@0 | 3933 | newItem = item.ownerDocument.createElement("li"); |
michael@0 | 3934 | item.parentNode.insertBefore(newItem, item.nextSibling); |
michael@0 | 3935 | } |
michael@0 | 3936 | |
michael@0 | 3937 | // "Insert child into new item as its first child, preserving |
michael@0 | 3938 | // ranges." |
michael@0 | 3939 | movePreservingRanges(child, newItem, 0); |
michael@0 | 3940 | } |
michael@0 | 3941 | } |
michael@0 | 3942 | } |
michael@0 | 3943 | |
michael@0 | 3944 | function getSelectionListState() { |
michael@0 | 3945 | // "If the active range is null, return "none"." |
michael@0 | 3946 | if (!getActiveRange()) { |
michael@0 | 3947 | return "none"; |
michael@0 | 3948 | } |
michael@0 | 3949 | |
michael@0 | 3950 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 3951 | var newRange = blockExtend(getActiveRange()); |
michael@0 | 3952 | |
michael@0 | 3953 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 3954 | // |
michael@0 | 3955 | // "For each node contained in new range, append node to node list if the |
michael@0 | 3956 | // last member of node list (if any) is not an ancestor of node; node is |
michael@0 | 3957 | // editable; node is not an indentation element; and node is either an ol |
michael@0 | 3958 | // or ul, or the child of an ol or ul, or an allowed child of "li"." |
michael@0 | 3959 | var nodeList = getContainedNodes(newRange, function(node) { |
michael@0 | 3960 | return isEditable(node) |
michael@0 | 3961 | && !isIndentationElement(node) |
michael@0 | 3962 | && (isHtmlElement(node, ["ol", "ul"]) |
michael@0 | 3963 | || isHtmlElement(node.parentNode, ["ol", "ul"]) |
michael@0 | 3964 | || isAllowedChild(node, "li")); |
michael@0 | 3965 | }); |
michael@0 | 3966 | |
michael@0 | 3967 | // "If node list is empty, return "none"." |
michael@0 | 3968 | if (!nodeList.length) { |
michael@0 | 3969 | return "none"; |
michael@0 | 3970 | } |
michael@0 | 3971 | |
michael@0 | 3972 | // "If every member of node list is either an ol or the child of an ol or |
michael@0 | 3973 | // the child of an li child of an ol, and none is a ul or an ancestor of a |
michael@0 | 3974 | // ul, return "ol"." |
michael@0 | 3975 | if (nodeList.every(function(node) { |
michael@0 | 3976 | return isHtmlElement(node, "ol") |
michael@0 | 3977 | || isHtmlElement(node.parentNode, "ol") |
michael@0 | 3978 | || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol")); |
michael@0 | 3979 | }) |
michael@0 | 3980 | && !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) { |
michael@0 | 3981 | return "ol"; |
michael@0 | 3982 | } |
michael@0 | 3983 | |
michael@0 | 3984 | // "If every member of node list is either a ul or the child of a ul or the |
michael@0 | 3985 | // child of an li child of a ul, and none is an ol or an ancestor of an ol, |
michael@0 | 3986 | // return "ul"." |
michael@0 | 3987 | if (nodeList.every(function(node) { |
michael@0 | 3988 | return isHtmlElement(node, "ul") |
michael@0 | 3989 | || isHtmlElement(node.parentNode, "ul") |
michael@0 | 3990 | || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul")); |
michael@0 | 3991 | }) |
michael@0 | 3992 | && !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) { |
michael@0 | 3993 | return "ul"; |
michael@0 | 3994 | } |
michael@0 | 3995 | |
michael@0 | 3996 | var hasOl = nodeList.some(function(node) { |
michael@0 | 3997 | return isHtmlElement(node, "ol") |
michael@0 | 3998 | || isHtmlElement(node.parentNode, "ol") |
michael@0 | 3999 | || ("querySelector" in node && node.querySelector("ol")) |
michael@0 | 4000 | || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol")); |
michael@0 | 4001 | }); |
michael@0 | 4002 | var hasUl = nodeList.some(function(node) { |
michael@0 | 4003 | return isHtmlElement(node, "ul") |
michael@0 | 4004 | || isHtmlElement(node.parentNode, "ul") |
michael@0 | 4005 | || ("querySelector" in node && node.querySelector("ul")) |
michael@0 | 4006 | || (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul")); |
michael@0 | 4007 | }); |
michael@0 | 4008 | // "If some member of node list is either an ol or the child or ancestor of |
michael@0 | 4009 | // an ol or the child of an li child of an ol, and some member of node list |
michael@0 | 4010 | // is either a ul or the child or ancestor of a ul or the child of an li |
michael@0 | 4011 | // child of a ul, return "mixed"." |
michael@0 | 4012 | if (hasOl && hasUl) { |
michael@0 | 4013 | return "mixed"; |
michael@0 | 4014 | } |
michael@0 | 4015 | |
michael@0 | 4016 | // "If some member of node list is either an ol or the child or ancestor of |
michael@0 | 4017 | // an ol or the child of an li child of an ol, return "mixed ol"." |
michael@0 | 4018 | if (hasOl) { |
michael@0 | 4019 | return "mixed ol"; |
michael@0 | 4020 | } |
michael@0 | 4021 | |
michael@0 | 4022 | // "If some member of node list is either a ul or the child or ancestor of |
michael@0 | 4023 | // a ul or the child of an li child of a ul, return "mixed ul"." |
michael@0 | 4024 | if (hasUl) { |
michael@0 | 4025 | return "mixed ul"; |
michael@0 | 4026 | } |
michael@0 | 4027 | |
michael@0 | 4028 | // "Return "none"." |
michael@0 | 4029 | return "none"; |
michael@0 | 4030 | } |
michael@0 | 4031 | |
michael@0 | 4032 | function getAlignmentValue(node) { |
michael@0 | 4033 | // "While node is neither null nor an Element, or it is an Element but its |
michael@0 | 4034 | // "display" property has resolved value "inline" or "none", set node to |
michael@0 | 4035 | // its parent." |
michael@0 | 4036 | while ((node && node.nodeType != Node.ELEMENT_NODE) |
michael@0 | 4037 | || (node.nodeType == Node.ELEMENT_NODE |
michael@0 | 4038 | && ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) { |
michael@0 | 4039 | node = node.parentNode; |
michael@0 | 4040 | } |
michael@0 | 4041 | |
michael@0 | 4042 | // "If node is not an Element, return "left"." |
michael@0 | 4043 | if (!node || node.nodeType != Node.ELEMENT_NODE) { |
michael@0 | 4044 | return "left"; |
michael@0 | 4045 | } |
michael@0 | 4046 | |
michael@0 | 4047 | var resolvedValue = getComputedStyle(node).textAlign |
michael@0 | 4048 | // Hack around browser non-standardness |
michael@0 | 4049 | .replace(/^-(moz|webkit)-/, "") |
michael@0 | 4050 | .replace(/^auto$/, "start"); |
michael@0 | 4051 | |
michael@0 | 4052 | // "If node's "text-align" property has resolved value "start", return |
michael@0 | 4053 | // "left" if the directionality of node is "ltr", "right" if it is "rtl"." |
michael@0 | 4054 | if (resolvedValue == "start") { |
michael@0 | 4055 | return getDirectionality(node) == "ltr" ? "left" : "right"; |
michael@0 | 4056 | } |
michael@0 | 4057 | |
michael@0 | 4058 | // "If node's "text-align" property has resolved value "end", return |
michael@0 | 4059 | // "right" if the directionality of node is "ltr", "left" if it is "rtl"." |
michael@0 | 4060 | if (resolvedValue == "end") { |
michael@0 | 4061 | return getDirectionality(node) == "ltr" ? "right" : "left"; |
michael@0 | 4062 | } |
michael@0 | 4063 | |
michael@0 | 4064 | // "If node's "text-align" property has resolved value "center", "justify", |
michael@0 | 4065 | // "left", or "right", return that value." |
michael@0 | 4066 | if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) { |
michael@0 | 4067 | return resolvedValue; |
michael@0 | 4068 | } |
michael@0 | 4069 | |
michael@0 | 4070 | // "Return "left"." |
michael@0 | 4071 | return "left"; |
michael@0 | 4072 | } |
michael@0 | 4073 | |
michael@0 | 4074 | function getNextEquivalentPoint(node, offset) { |
michael@0 | 4075 | // "If node's length is zero, return null." |
michael@0 | 4076 | if (getNodeLength(node) == 0) { |
michael@0 | 4077 | return null; |
michael@0 | 4078 | } |
michael@0 | 4079 | |
michael@0 | 4080 | // "If offset is node's length, and node's parent is not null, and node is |
michael@0 | 4081 | // an inline node, return (node's parent, 1 + node's index)." |
michael@0 | 4082 | if (offset == getNodeLength(node) |
michael@0 | 4083 | && node.parentNode |
michael@0 | 4084 | && isInlineNode(node)) { |
michael@0 | 4085 | return [node.parentNode, 1 + getNodeIndex(node)]; |
michael@0 | 4086 | } |
michael@0 | 4087 | |
michael@0 | 4088 | // "If node has a child with index offset, and that child's length is not |
michael@0 | 4089 | // zero, and that child is an inline node, return (that child, 0)." |
michael@0 | 4090 | if (0 <= offset |
michael@0 | 4091 | && offset < node.childNodes.length |
michael@0 | 4092 | && getNodeLength(node.childNodes[offset]) != 0 |
michael@0 | 4093 | && isInlineNode(node.childNodes[offset])) { |
michael@0 | 4094 | return [node.childNodes[offset], 0]; |
michael@0 | 4095 | } |
michael@0 | 4096 | |
michael@0 | 4097 | // "Return null." |
michael@0 | 4098 | return null; |
michael@0 | 4099 | } |
michael@0 | 4100 | |
michael@0 | 4101 | function getPreviousEquivalentPoint(node, offset) { |
michael@0 | 4102 | // "If node's length is zero, return null." |
michael@0 | 4103 | if (getNodeLength(node) == 0) { |
michael@0 | 4104 | return null; |
michael@0 | 4105 | } |
michael@0 | 4106 | |
michael@0 | 4107 | // "If offset is 0, and node's parent is not null, and node is an inline |
michael@0 | 4108 | // node, return (node's parent, node's index)." |
michael@0 | 4109 | if (offset == 0 |
michael@0 | 4110 | && node.parentNode |
michael@0 | 4111 | && isInlineNode(node)) { |
michael@0 | 4112 | return [node.parentNode, getNodeIndex(node)]; |
michael@0 | 4113 | } |
michael@0 | 4114 | |
michael@0 | 4115 | // "If node has a child with index offset − 1, and that child's length is |
michael@0 | 4116 | // not zero, and that child is an inline node, return (that child, that |
michael@0 | 4117 | // child's length)." |
michael@0 | 4118 | if (0 <= offset - 1 |
michael@0 | 4119 | && offset - 1 < node.childNodes.length |
michael@0 | 4120 | && getNodeLength(node.childNodes[offset - 1]) != 0 |
michael@0 | 4121 | && isInlineNode(node.childNodes[offset - 1])) { |
michael@0 | 4122 | return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])]; |
michael@0 | 4123 | } |
michael@0 | 4124 | |
michael@0 | 4125 | // "Return null." |
michael@0 | 4126 | return null; |
michael@0 | 4127 | } |
michael@0 | 4128 | |
michael@0 | 4129 | function getFirstEquivalentPoint(node, offset) { |
michael@0 | 4130 | // "While (node, offset)'s previous equivalent point is not null, set |
michael@0 | 4131 | // (node, offset) to its previous equivalent point." |
michael@0 | 4132 | var prev; |
michael@0 | 4133 | while (prev = getPreviousEquivalentPoint(node, offset)) { |
michael@0 | 4134 | node = prev[0]; |
michael@0 | 4135 | offset = prev[1]; |
michael@0 | 4136 | } |
michael@0 | 4137 | |
michael@0 | 4138 | // "Return (node, offset)." |
michael@0 | 4139 | return [node, offset]; |
michael@0 | 4140 | } |
michael@0 | 4141 | |
michael@0 | 4142 | function getLastEquivalentPoint(node, offset) { |
michael@0 | 4143 | // "While (node, offset)'s next equivalent point is not null, set (node, |
michael@0 | 4144 | // offset) to its next equivalent point." |
michael@0 | 4145 | var next; |
michael@0 | 4146 | while (next = getNextEquivalentPoint(node, offset)) { |
michael@0 | 4147 | node = next[0]; |
michael@0 | 4148 | offset = next[1]; |
michael@0 | 4149 | } |
michael@0 | 4150 | |
michael@0 | 4151 | // "Return (node, offset)." |
michael@0 | 4152 | return [node, offset]; |
michael@0 | 4153 | } |
michael@0 | 4154 | |
michael@0 | 4155 | //@} |
michael@0 | 4156 | ///// Block-extending a range ///// |
michael@0 | 4157 | //@{ |
michael@0 | 4158 | |
michael@0 | 4159 | // "A boundary point (node, offset) is a block start point if either node's |
michael@0 | 4160 | // parent is null and offset is zero; or node has a child with index offset − |
michael@0 | 4161 | // 1, and that child is either a visible block node or a visible br." |
michael@0 | 4162 | function isBlockStartPoint(node, offset) { |
michael@0 | 4163 | return (!node.parentNode && offset == 0) |
michael@0 | 4164 | || (0 <= offset - 1 |
michael@0 | 4165 | && offset - 1 < node.childNodes.length |
michael@0 | 4166 | && isVisible(node.childNodes[offset - 1]) |
michael@0 | 4167 | && (isBlockNode(node.childNodes[offset - 1]) |
michael@0 | 4168 | || isHtmlElement(node.childNodes[offset - 1], "br"))); |
michael@0 | 4169 | } |
michael@0 | 4170 | |
michael@0 | 4171 | // "A boundary point (node, offset) is a block end point if either node's |
michael@0 | 4172 | // parent is null and offset is node's length; or node has a child with index |
michael@0 | 4173 | // offset, and that child is a visible block node." |
michael@0 | 4174 | function isBlockEndPoint(node, offset) { |
michael@0 | 4175 | return (!node.parentNode && offset == getNodeLength(node)) |
michael@0 | 4176 | || (offset < node.childNodes.length |
michael@0 | 4177 | && isVisible(node.childNodes[offset]) |
michael@0 | 4178 | && isBlockNode(node.childNodes[offset])); |
michael@0 | 4179 | } |
michael@0 | 4180 | |
michael@0 | 4181 | // "A boundary point is a block boundary point if it is either a block start |
michael@0 | 4182 | // point or a block end point." |
michael@0 | 4183 | function isBlockBoundaryPoint(node, offset) { |
michael@0 | 4184 | return isBlockStartPoint(node, offset) |
michael@0 | 4185 | || isBlockEndPoint(node, offset); |
michael@0 | 4186 | } |
michael@0 | 4187 | |
michael@0 | 4188 | function blockExtend(range) { |
michael@0 | 4189 | // "Let start node, start offset, end node, and end offset be the start |
michael@0 | 4190 | // and end nodes and offsets of the range." |
michael@0 | 4191 | var startNode = range.startContainer; |
michael@0 | 4192 | var startOffset = range.startOffset; |
michael@0 | 4193 | var endNode = range.endContainer; |
michael@0 | 4194 | var endOffset = range.endOffset; |
michael@0 | 4195 | |
michael@0 | 4196 | // "If some ancestor container of start node is an li, set start offset to |
michael@0 | 4197 | // the index of the last such li in tree order, and set start node to that |
michael@0 | 4198 | // li's parent." |
michael@0 | 4199 | var liAncestors = getAncestors(startNode).concat(startNode) |
michael@0 | 4200 | .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) |
michael@0 | 4201 | .slice(-1); |
michael@0 | 4202 | if (liAncestors.length) { |
michael@0 | 4203 | startOffset = getNodeIndex(liAncestors[0]); |
michael@0 | 4204 | startNode = liAncestors[0].parentNode; |
michael@0 | 4205 | } |
michael@0 | 4206 | |
michael@0 | 4207 | // "If (start node, start offset) is not a block start point, repeat the |
michael@0 | 4208 | // following steps:" |
michael@0 | 4209 | if (!isBlockStartPoint(startNode, startOffset)) do { |
michael@0 | 4210 | // "If start offset is zero, set it to start node's index, then set |
michael@0 | 4211 | // start node to its parent." |
michael@0 | 4212 | if (startOffset == 0) { |
michael@0 | 4213 | startOffset = getNodeIndex(startNode); |
michael@0 | 4214 | startNode = startNode.parentNode; |
michael@0 | 4215 | |
michael@0 | 4216 | // "Otherwise, subtract one from start offset." |
michael@0 | 4217 | } else { |
michael@0 | 4218 | startOffset--; |
michael@0 | 4219 | } |
michael@0 | 4220 | |
michael@0 | 4221 | // "If (start node, start offset) is a block boundary point, break from |
michael@0 | 4222 | // this loop." |
michael@0 | 4223 | } while (!isBlockBoundaryPoint(startNode, startOffset)); |
michael@0 | 4224 | |
michael@0 | 4225 | // "While start offset is zero and start node's parent is not null, set |
michael@0 | 4226 | // start offset to start node's index, then set start node to its parent." |
michael@0 | 4227 | while (startOffset == 0 |
michael@0 | 4228 | && startNode.parentNode) { |
michael@0 | 4229 | startOffset = getNodeIndex(startNode); |
michael@0 | 4230 | startNode = startNode.parentNode; |
michael@0 | 4231 | } |
michael@0 | 4232 | |
michael@0 | 4233 | // "If some ancestor container of end node is an li, set end offset to one |
michael@0 | 4234 | // plus the index of the last such li in tree order, and set end node to |
michael@0 | 4235 | // that li's parent." |
michael@0 | 4236 | var liAncestors = getAncestors(endNode).concat(endNode) |
michael@0 | 4237 | .filter(function(ancestor) { return isHtmlElement(ancestor, "li") }) |
michael@0 | 4238 | .slice(-1); |
michael@0 | 4239 | if (liAncestors.length) { |
michael@0 | 4240 | endOffset = 1 + getNodeIndex(liAncestors[0]); |
michael@0 | 4241 | endNode = liAncestors[0].parentNode; |
michael@0 | 4242 | } |
michael@0 | 4243 | |
michael@0 | 4244 | // "If (end node, end offset) is not a block end point, repeat the |
michael@0 | 4245 | // following steps:" |
michael@0 | 4246 | if (!isBlockEndPoint(endNode, endOffset)) do { |
michael@0 | 4247 | // "If end offset is end node's length, set it to one plus end node's |
michael@0 | 4248 | // index, then set end node to its parent." |
michael@0 | 4249 | if (endOffset == getNodeLength(endNode)) { |
michael@0 | 4250 | endOffset = 1 + getNodeIndex(endNode); |
michael@0 | 4251 | endNode = endNode.parentNode; |
michael@0 | 4252 | |
michael@0 | 4253 | // "Otherwise, add one to end offset. |
michael@0 | 4254 | } else { |
michael@0 | 4255 | endOffset++; |
michael@0 | 4256 | } |
michael@0 | 4257 | |
michael@0 | 4258 | // "If (end node, end offset) is a block boundary point, break from |
michael@0 | 4259 | // this loop." |
michael@0 | 4260 | } while (!isBlockBoundaryPoint(endNode, endOffset)); |
michael@0 | 4261 | |
michael@0 | 4262 | // "While end offset is end node's length and end node's parent is not |
michael@0 | 4263 | // null, set end offset to one plus end node's index, then set end node to |
michael@0 | 4264 | // its parent." |
michael@0 | 4265 | while (endOffset == getNodeLength(endNode) |
michael@0 | 4266 | && endNode.parentNode) { |
michael@0 | 4267 | endOffset = 1 + getNodeIndex(endNode); |
michael@0 | 4268 | endNode = endNode.parentNode; |
michael@0 | 4269 | } |
michael@0 | 4270 | |
michael@0 | 4271 | // "Let new range be a new range whose start and end nodes and offsets |
michael@0 | 4272 | // are start node, start offset, end node, and end offset." |
michael@0 | 4273 | var newRange = startNode.ownerDocument.createRange(); |
michael@0 | 4274 | newRange.setStart(startNode, startOffset); |
michael@0 | 4275 | newRange.setEnd(endNode, endOffset); |
michael@0 | 4276 | |
michael@0 | 4277 | // "Return new range." |
michael@0 | 4278 | return newRange; |
michael@0 | 4279 | } |
michael@0 | 4280 | |
michael@0 | 4281 | function followsLineBreak(node) { |
michael@0 | 4282 | // "Let offset be zero." |
michael@0 | 4283 | var offset = 0; |
michael@0 | 4284 | |
michael@0 | 4285 | // "While (node, offset) is not a block boundary point:" |
michael@0 | 4286 | while (!isBlockBoundaryPoint(node, offset)) { |
michael@0 | 4287 | // "If node has a visible child with index offset minus one, return |
michael@0 | 4288 | // false." |
michael@0 | 4289 | if (0 <= offset - 1 |
michael@0 | 4290 | && offset - 1 < node.childNodes.length |
michael@0 | 4291 | && isVisible(node.childNodes[offset - 1])) { |
michael@0 | 4292 | return false; |
michael@0 | 4293 | } |
michael@0 | 4294 | |
michael@0 | 4295 | // "If offset is zero or node has no children, set offset to node's |
michael@0 | 4296 | // index, then set node to its parent." |
michael@0 | 4297 | if (offset == 0 |
michael@0 | 4298 | || !node.hasChildNodes()) { |
michael@0 | 4299 | offset = getNodeIndex(node); |
michael@0 | 4300 | node = node.parentNode; |
michael@0 | 4301 | |
michael@0 | 4302 | // "Otherwise, set node to its child with index offset minus one, then |
michael@0 | 4303 | // set offset to node's length." |
michael@0 | 4304 | } else { |
michael@0 | 4305 | node = node.childNodes[offset - 1]; |
michael@0 | 4306 | offset = getNodeLength(node); |
michael@0 | 4307 | } |
michael@0 | 4308 | } |
michael@0 | 4309 | |
michael@0 | 4310 | // "Return true." |
michael@0 | 4311 | return true; |
michael@0 | 4312 | } |
michael@0 | 4313 | |
michael@0 | 4314 | function precedesLineBreak(node) { |
michael@0 | 4315 | // "Let offset be node's length." |
michael@0 | 4316 | var offset = getNodeLength(node); |
michael@0 | 4317 | |
michael@0 | 4318 | // "While (node, offset) is not a block boundary point:" |
michael@0 | 4319 | while (!isBlockBoundaryPoint(node, offset)) { |
michael@0 | 4320 | // "If node has a visible child with index offset, return false." |
michael@0 | 4321 | if (offset < node.childNodes.length |
michael@0 | 4322 | && isVisible(node.childNodes[offset])) { |
michael@0 | 4323 | return false; |
michael@0 | 4324 | } |
michael@0 | 4325 | |
michael@0 | 4326 | // "If offset is node's length or node has no children, set offset to |
michael@0 | 4327 | // one plus node's index, then set node to its parent." |
michael@0 | 4328 | if (offset == getNodeLength(node) |
michael@0 | 4329 | || !node.hasChildNodes()) { |
michael@0 | 4330 | offset = 1 + getNodeIndex(node); |
michael@0 | 4331 | node = node.parentNode; |
michael@0 | 4332 | |
michael@0 | 4333 | // "Otherwise, set node to its child with index offset and set offset |
michael@0 | 4334 | // to zero." |
michael@0 | 4335 | } else { |
michael@0 | 4336 | node = node.childNodes[offset]; |
michael@0 | 4337 | offset = 0; |
michael@0 | 4338 | } |
michael@0 | 4339 | } |
michael@0 | 4340 | |
michael@0 | 4341 | // "Return true." |
michael@0 | 4342 | return true; |
michael@0 | 4343 | } |
michael@0 | 4344 | |
michael@0 | 4345 | //@} |
michael@0 | 4346 | ///// Recording and restoring overrides ///// |
michael@0 | 4347 | //@{ |
michael@0 | 4348 | |
michael@0 | 4349 | function recordCurrentOverrides() { |
michael@0 | 4350 | // "Let overrides be a list of (string, string or boolean) ordered pairs, |
michael@0 | 4351 | // initially empty." |
michael@0 | 4352 | var overrides = []; |
michael@0 | 4353 | |
michael@0 | 4354 | // "If there is a value override for "createLink", add ("createLink", value |
michael@0 | 4355 | // override for "createLink") to overrides." |
michael@0 | 4356 | if (getValueOverride("createlink") !== undefined) { |
michael@0 | 4357 | overrides.push(["createlink", getValueOverride("createlink")]); |
michael@0 | 4358 | } |
michael@0 | 4359 | |
michael@0 | 4360 | // "For each command in the list "bold", "italic", "strikethrough", |
michael@0 | 4361 | // "subscript", "superscript", "underline", in order: if there is a state |
michael@0 | 4362 | // override for command, add (command, command's state override) to |
michael@0 | 4363 | // overrides." |
michael@0 | 4364 | ["bold", "italic", "strikethrough", "subscript", "superscript", |
michael@0 | 4365 | "underline"].forEach(function(command) { |
michael@0 | 4366 | if (getStateOverride(command) !== undefined) { |
michael@0 | 4367 | overrides.push([command, getStateOverride(command)]); |
michael@0 | 4368 | } |
michael@0 | 4369 | }); |
michael@0 | 4370 | |
michael@0 | 4371 | // "For each command in the list "fontName", "fontSize", "foreColor", |
michael@0 | 4372 | // "hiliteColor", in order: if there is a value override for command, add |
michael@0 | 4373 | // (command, command's value override) to overrides." |
michael@0 | 4374 | ["fontname", "fontsize", "forecolor", |
michael@0 | 4375 | "hilitecolor"].forEach(function(command) { |
michael@0 | 4376 | if (getValueOverride(command) !== undefined) { |
michael@0 | 4377 | overrides.push([command, getValueOverride(command)]); |
michael@0 | 4378 | } |
michael@0 | 4379 | }); |
michael@0 | 4380 | |
michael@0 | 4381 | // "Return overrides." |
michael@0 | 4382 | return overrides; |
michael@0 | 4383 | } |
michael@0 | 4384 | |
michael@0 | 4385 | function recordCurrentStatesAndValues() { |
michael@0 | 4386 | // "Let overrides be a list of (string, string or boolean) ordered pairs, |
michael@0 | 4387 | // initially empty." |
michael@0 | 4388 | var overrides = []; |
michael@0 | 4389 | |
michael@0 | 4390 | // "Let node be the first formattable node effectively contained in the |
michael@0 | 4391 | // active range, or null if there is none." |
michael@0 | 4392 | var node = getAllEffectivelyContainedNodes(getActiveRange()) |
michael@0 | 4393 | .filter(isFormattableNode)[0]; |
michael@0 | 4394 | |
michael@0 | 4395 | // "If node is null, return overrides." |
michael@0 | 4396 | if (!node) { |
michael@0 | 4397 | return overrides; |
michael@0 | 4398 | } |
michael@0 | 4399 | |
michael@0 | 4400 | // "Add ("createLink", node's effective command value for "createLink") to |
michael@0 | 4401 | // overrides." |
michael@0 | 4402 | overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]); |
michael@0 | 4403 | |
michael@0 | 4404 | // "For each command in the list "bold", "italic", "strikethrough", |
michael@0 | 4405 | // "subscript", "superscript", "underline", in order: if node's effective |
michael@0 | 4406 | // command value for command is one of its inline command activated values, |
michael@0 | 4407 | // add (command, true) to overrides, and otherwise add (command, false) to |
michael@0 | 4408 | // overrides." |
michael@0 | 4409 | ["bold", "italic", "strikethrough", "subscript", "superscript", |
michael@0 | 4410 | "underline"].forEach(function(command) { |
michael@0 | 4411 | if (commands[command].inlineCommandActivatedValues |
michael@0 | 4412 | .indexOf(getEffectiveCommandValue(node, command)) != -1) { |
michael@0 | 4413 | overrides.push([command, true]); |
michael@0 | 4414 | } else { |
michael@0 | 4415 | overrides.push([command, false]); |
michael@0 | 4416 | } |
michael@0 | 4417 | }); |
michael@0 | 4418 | |
michael@0 | 4419 | // "For each command in the list "fontName", "foreColor", "hiliteColor", in |
michael@0 | 4420 | // order: add (command, command's value) to overrides." |
michael@0 | 4421 | ["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) { |
michael@0 | 4422 | overrides.push([command, commands[command].value()]); |
michael@0 | 4423 | }); |
michael@0 | 4424 | |
michael@0 | 4425 | // "Add ("fontSize", node's effective command value for "fontSize") to |
michael@0 | 4426 | // overrides." |
michael@0 | 4427 | overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]); |
michael@0 | 4428 | |
michael@0 | 4429 | // "Return overrides." |
michael@0 | 4430 | return overrides; |
michael@0 | 4431 | } |
michael@0 | 4432 | |
michael@0 | 4433 | function restoreStatesAndValues(overrides) { |
michael@0 | 4434 | // "Let node be the first formattable node effectively contained in the |
michael@0 | 4435 | // active range, or null if there is none." |
michael@0 | 4436 | var node = getAllEffectivelyContainedNodes(getActiveRange()) |
michael@0 | 4437 | .filter(isFormattableNode)[0]; |
michael@0 | 4438 | |
michael@0 | 4439 | // "If node is not null, then for each (command, override) pair in |
michael@0 | 4440 | // overrides, in order:" |
michael@0 | 4441 | if (node) { |
michael@0 | 4442 | for (var i = 0; i < overrides.length; i++) { |
michael@0 | 4443 | var command = overrides[i][0]; |
michael@0 | 4444 | var override = overrides[i][1]; |
michael@0 | 4445 | |
michael@0 | 4446 | // "If override is a boolean, and queryCommandState(command) |
michael@0 | 4447 | // returns something different from override, take the action for |
michael@0 | 4448 | // command, with value equal to the empty string." |
michael@0 | 4449 | if (typeof override == "boolean" |
michael@0 | 4450 | && myQueryCommandState(command) != override) { |
michael@0 | 4451 | commands[command].action(""); |
michael@0 | 4452 | |
michael@0 | 4453 | // "Otherwise, if override is a string, and command is neither |
michael@0 | 4454 | // "createLink" nor "fontSize", and queryCommandValue(command) |
michael@0 | 4455 | // returns something not equivalent to override, take the action |
michael@0 | 4456 | // for command, with value equal to override." |
michael@0 | 4457 | } else if (typeof override == "string" |
michael@0 | 4458 | && command != "createlink" |
michael@0 | 4459 | && command != "fontsize" |
michael@0 | 4460 | && !areEquivalentValues(command, myQueryCommandValue(command), override)) { |
michael@0 | 4461 | commands[command].action(override); |
michael@0 | 4462 | |
michael@0 | 4463 | // "Otherwise, if override is a string; and command is |
michael@0 | 4464 | // "createLink"; and either there is a value override for |
michael@0 | 4465 | // "createLink" that is not equal to override, or there is no value |
michael@0 | 4466 | // override for "createLink" and node's effective command value for |
michael@0 | 4467 | // "createLink" is not equal to override: take the action for |
michael@0 | 4468 | // "createLink", with value equal to override." |
michael@0 | 4469 | } else if (typeof override == "string" |
michael@0 | 4470 | && command == "createlink" |
michael@0 | 4471 | && ( |
michael@0 | 4472 | ( |
michael@0 | 4473 | getValueOverride("createlink") !== undefined |
michael@0 | 4474 | && getValueOverride("createlink") !== override |
michael@0 | 4475 | ) || ( |
michael@0 | 4476 | getValueOverride("createlink") === undefined |
michael@0 | 4477 | && getEffectiveCommandValue(node, "createlink") !== override |
michael@0 | 4478 | ) |
michael@0 | 4479 | )) { |
michael@0 | 4480 | commands.createlink.action(override); |
michael@0 | 4481 | |
michael@0 | 4482 | // "Otherwise, if override is a string; and command is "fontSize"; |
michael@0 | 4483 | // and either there is a value override for "fontSize" that is not |
michael@0 | 4484 | // equal to override, or there is no value override for "fontSize" |
michael@0 | 4485 | // and node's effective command value for "fontSize" is not loosely |
michael@0 | 4486 | // equivalent to override:" |
michael@0 | 4487 | } else if (typeof override == "string" |
michael@0 | 4488 | && command == "fontsize" |
michael@0 | 4489 | && ( |
michael@0 | 4490 | ( |
michael@0 | 4491 | getValueOverride("fontsize") !== undefined |
michael@0 | 4492 | && getValueOverride("fontsize") !== override |
michael@0 | 4493 | ) || ( |
michael@0 | 4494 | getValueOverride("fontsize") === undefined |
michael@0 | 4495 | && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override) |
michael@0 | 4496 | ) |
michael@0 | 4497 | )) { |
michael@0 | 4498 | // "Convert override to an integer number of pixels, and set |
michael@0 | 4499 | // override to the legacy font size for the result." |
michael@0 | 4500 | override = getLegacyFontSize(override); |
michael@0 | 4501 | |
michael@0 | 4502 | // "Take the action for "fontSize", with value equal to |
michael@0 | 4503 | // override." |
michael@0 | 4504 | commands.fontsize.action(override); |
michael@0 | 4505 | |
michael@0 | 4506 | // "Otherwise, continue this loop from the beginning." |
michael@0 | 4507 | } else { |
michael@0 | 4508 | continue; |
michael@0 | 4509 | } |
michael@0 | 4510 | |
michael@0 | 4511 | // "Set node to the first formattable node effectively contained in |
michael@0 | 4512 | // the active range, if there is one." |
michael@0 | 4513 | node = getAllEffectivelyContainedNodes(getActiveRange()) |
michael@0 | 4514 | .filter(isFormattableNode)[0] |
michael@0 | 4515 | || node; |
michael@0 | 4516 | } |
michael@0 | 4517 | |
michael@0 | 4518 | // "Otherwise, for each (command, override) pair in overrides, in order:" |
michael@0 | 4519 | } else { |
michael@0 | 4520 | for (var i = 0; i < overrides.length; i++) { |
michael@0 | 4521 | var command = overrides[i][0]; |
michael@0 | 4522 | var override = overrides[i][1]; |
michael@0 | 4523 | |
michael@0 | 4524 | // "If override is a boolean, set the state override for command to |
michael@0 | 4525 | // override." |
michael@0 | 4526 | if (typeof override == "boolean") { |
michael@0 | 4527 | setStateOverride(command, override); |
michael@0 | 4528 | } |
michael@0 | 4529 | |
michael@0 | 4530 | // "If override is a string, set the value override for command to |
michael@0 | 4531 | // override." |
michael@0 | 4532 | if (typeof override == "string") { |
michael@0 | 4533 | setValueOverride(command, override); |
michael@0 | 4534 | } |
michael@0 | 4535 | } |
michael@0 | 4536 | } |
michael@0 | 4537 | } |
michael@0 | 4538 | |
michael@0 | 4539 | //@} |
michael@0 | 4540 | ///// Deleting the selection ///// |
michael@0 | 4541 | //@{ |
michael@0 | 4542 | |
michael@0 | 4543 | // The flags argument is a dictionary that can have blockMerging, |
michael@0 | 4544 | // stripWrappers, and/or direction as keys. |
michael@0 | 4545 | function deleteSelection(flags) { |
michael@0 | 4546 | if (flags === undefined) { |
michael@0 | 4547 | flags = {}; |
michael@0 | 4548 | } |
michael@0 | 4549 | |
michael@0 | 4550 | var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true; |
michael@0 | 4551 | var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true; |
michael@0 | 4552 | var direction = "direction" in flags ? flags.direction : "forward"; |
michael@0 | 4553 | |
michael@0 | 4554 | // "If the active range is null, abort these steps and do nothing." |
michael@0 | 4555 | if (!getActiveRange()) { |
michael@0 | 4556 | return; |
michael@0 | 4557 | } |
michael@0 | 4558 | |
michael@0 | 4559 | // "Canonicalize whitespace at the active range's start." |
michael@0 | 4560 | canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); |
michael@0 | 4561 | |
michael@0 | 4562 | // "Canonicalize whitespace at the active range's end." |
michael@0 | 4563 | canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset); |
michael@0 | 4564 | |
michael@0 | 4565 | // "Let (start node, start offset) be the last equivalent point for the |
michael@0 | 4566 | // active range's start." |
michael@0 | 4567 | var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset); |
michael@0 | 4568 | var startNode = start[0]; |
michael@0 | 4569 | var startOffset = start[1]; |
michael@0 | 4570 | |
michael@0 | 4571 | // "Let (end node, end offset) be the first equivalent point for the active |
michael@0 | 4572 | // range's end." |
michael@0 | 4573 | var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset); |
michael@0 | 4574 | var endNode = end[0]; |
michael@0 | 4575 | var endOffset = end[1]; |
michael@0 | 4576 | |
michael@0 | 4577 | // "If (end node, end offset) is not after (start node, start offset):" |
michael@0 | 4578 | if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") { |
michael@0 | 4579 | // "If direction is "forward", call collapseToStart() on the context |
michael@0 | 4580 | // object's Selection." |
michael@0 | 4581 | // |
michael@0 | 4582 | // Here and in a few other places, we check rangeCount to work around a |
michael@0 | 4583 | // WebKit bug: it will sometimes incorrectly remove ranges from the |
michael@0 | 4584 | // selection if nodes are removed, so collapseToStart() will throw. |
michael@0 | 4585 | // This will break everything if we're using an actual selection, but |
michael@0 | 4586 | // if getActiveRange() is really just returning globalRange and that's |
michael@0 | 4587 | // all we care about, it will work fine. I only add the extra check |
michael@0 | 4588 | // for errors I actually hit in testing. |
michael@0 | 4589 | if (direction == "forward") { |
michael@0 | 4590 | if (getSelection().rangeCount) { |
michael@0 | 4591 | getSelection().collapseToStart(); |
michael@0 | 4592 | } |
michael@0 | 4593 | getActiveRange().collapse(true); |
michael@0 | 4594 | |
michael@0 | 4595 | // "Otherwise, call collapseToEnd() on the context object's Selection." |
michael@0 | 4596 | } else { |
michael@0 | 4597 | getSelection().collapseToEnd(); |
michael@0 | 4598 | getActiveRange().collapse(false); |
michael@0 | 4599 | } |
michael@0 | 4600 | |
michael@0 | 4601 | // "Abort these steps." |
michael@0 | 4602 | return; |
michael@0 | 4603 | } |
michael@0 | 4604 | |
michael@0 | 4605 | // "If start node is a Text node and start offset is 0, set start offset to |
michael@0 | 4606 | // the index of start node, then set start node to its parent." |
michael@0 | 4607 | if (startNode.nodeType == Node.TEXT_NODE |
michael@0 | 4608 | && startOffset == 0) { |
michael@0 | 4609 | startOffset = getNodeIndex(startNode); |
michael@0 | 4610 | startNode = startNode.parentNode; |
michael@0 | 4611 | } |
michael@0 | 4612 | |
michael@0 | 4613 | // "If end node is a Text node and end offset is its length, set end offset |
michael@0 | 4614 | // to one plus the index of end node, then set end node to its parent." |
michael@0 | 4615 | if (endNode.nodeType == Node.TEXT_NODE |
michael@0 | 4616 | && endOffset == getNodeLength(endNode)) { |
michael@0 | 4617 | endOffset = 1 + getNodeIndex(endNode); |
michael@0 | 4618 | endNode = endNode.parentNode; |
michael@0 | 4619 | } |
michael@0 | 4620 | |
michael@0 | 4621 | // "Call collapse(start node, start offset) on the context object's |
michael@0 | 4622 | // Selection." |
michael@0 | 4623 | getSelection().collapse(startNode, startOffset); |
michael@0 | 4624 | getActiveRange().setStart(startNode, startOffset); |
michael@0 | 4625 | |
michael@0 | 4626 | // "Call extend(end node, end offset) on the context object's Selection." |
michael@0 | 4627 | getSelection().extend(endNode, endOffset); |
michael@0 | 4628 | getActiveRange().setEnd(endNode, endOffset); |
michael@0 | 4629 | |
michael@0 | 4630 | // "Let start block be the active range's start node." |
michael@0 | 4631 | var startBlock = getActiveRange().startContainer; |
michael@0 | 4632 | |
michael@0 | 4633 | // "While start block's parent is in the same editing host and start block |
michael@0 | 4634 | // is an inline node, set start block to its parent." |
michael@0 | 4635 | while (inSameEditingHost(startBlock, startBlock.parentNode) |
michael@0 | 4636 | && isInlineNode(startBlock)) { |
michael@0 | 4637 | startBlock = startBlock.parentNode; |
michael@0 | 4638 | } |
michael@0 | 4639 | |
michael@0 | 4640 | // "If start block is neither a block node nor an editing host, or "span" |
michael@0 | 4641 | // is not an allowed child of start block, or start block is a td or th, |
michael@0 | 4642 | // set start block to null." |
michael@0 | 4643 | if ((!isBlockNode(startBlock) && !isEditingHost(startBlock)) |
michael@0 | 4644 | || !isAllowedChild("span", startBlock) |
michael@0 | 4645 | || isHtmlElement(startBlock, ["td", "th"])) { |
michael@0 | 4646 | startBlock = null; |
michael@0 | 4647 | } |
michael@0 | 4648 | |
michael@0 | 4649 | // "Let end block be the active range's end node." |
michael@0 | 4650 | var endBlock = getActiveRange().endContainer; |
michael@0 | 4651 | |
michael@0 | 4652 | // "While end block's parent is in the same editing host and end block is |
michael@0 | 4653 | // an inline node, set end block to its parent." |
michael@0 | 4654 | while (inSameEditingHost(endBlock, endBlock.parentNode) |
michael@0 | 4655 | && isInlineNode(endBlock)) { |
michael@0 | 4656 | endBlock = endBlock.parentNode; |
michael@0 | 4657 | } |
michael@0 | 4658 | |
michael@0 | 4659 | // "If end block is neither a block node nor an editing host, or "span" is |
michael@0 | 4660 | // not an allowed child of end block, or end block is a td or th, set end |
michael@0 | 4661 | // block to null." |
michael@0 | 4662 | if ((!isBlockNode(endBlock) && !isEditingHost(endBlock)) |
michael@0 | 4663 | || !isAllowedChild("span", endBlock) |
michael@0 | 4664 | || isHtmlElement(endBlock, ["td", "th"])) { |
michael@0 | 4665 | endBlock = null; |
michael@0 | 4666 | } |
michael@0 | 4667 | |
michael@0 | 4668 | // "Record current states and values, and let overrides be the result." |
michael@0 | 4669 | var overrides = recordCurrentStatesAndValues(); |
michael@0 | 4670 | |
michael@0 | 4671 | // "If start node and end node are the same, and start node is an editable |
michael@0 | 4672 | // Text node:" |
michael@0 | 4673 | if (startNode == endNode |
michael@0 | 4674 | && isEditable(startNode) |
michael@0 | 4675 | && startNode.nodeType == Node.TEXT_NODE) { |
michael@0 | 4676 | // "Call deleteData(start offset, end offset − start offset) on start |
michael@0 | 4677 | // node." |
michael@0 | 4678 | startNode.deleteData(startOffset, endOffset - startOffset); |
michael@0 | 4679 | |
michael@0 | 4680 | // "Canonicalize whitespace at (start node, start offset), with fix |
michael@0 | 4681 | // collapsed space false." |
michael@0 | 4682 | canonicalizeWhitespace(startNode, startOffset, false); |
michael@0 | 4683 | |
michael@0 | 4684 | // "If direction is "forward", call collapseToStart() on the context |
michael@0 | 4685 | // object's Selection." |
michael@0 | 4686 | if (direction == "forward") { |
michael@0 | 4687 | if (getSelection().rangeCount) { |
michael@0 | 4688 | getSelection().collapseToStart(); |
michael@0 | 4689 | } |
michael@0 | 4690 | getActiveRange().collapse(true); |
michael@0 | 4691 | |
michael@0 | 4692 | // "Otherwise, call collapseToEnd() on the context object's Selection." |
michael@0 | 4693 | } else { |
michael@0 | 4694 | getSelection().collapseToEnd(); |
michael@0 | 4695 | getActiveRange().collapse(false); |
michael@0 | 4696 | } |
michael@0 | 4697 | |
michael@0 | 4698 | // "Restore states and values from overrides." |
michael@0 | 4699 | restoreStatesAndValues(overrides); |
michael@0 | 4700 | |
michael@0 | 4701 | // "Abort these steps." |
michael@0 | 4702 | return; |
michael@0 | 4703 | } |
michael@0 | 4704 | |
michael@0 | 4705 | // "If start node is an editable Text node, call deleteData() on it, with |
michael@0 | 4706 | // start offset as the first argument and (length of start node − start |
michael@0 | 4707 | // offset) as the second argument." |
michael@0 | 4708 | if (isEditable(startNode) |
michael@0 | 4709 | && startNode.nodeType == Node.TEXT_NODE) { |
michael@0 | 4710 | startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset); |
michael@0 | 4711 | } |
michael@0 | 4712 | |
michael@0 | 4713 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 4714 | // |
michael@0 | 4715 | // "For each node contained in the active range, append node to node list |
michael@0 | 4716 | // if the last member of node list (if any) is not an ancestor of node; |
michael@0 | 4717 | // node is editable; and node is not a thead, tbody, tfoot, tr, th, or td." |
michael@0 | 4718 | var nodeList = getContainedNodes(getActiveRange(), |
michael@0 | 4719 | function(node) { |
michael@0 | 4720 | return isEditable(node) |
michael@0 | 4721 | && !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]); |
michael@0 | 4722 | } |
michael@0 | 4723 | ); |
michael@0 | 4724 | |
michael@0 | 4725 | // "For each node in node list:" |
michael@0 | 4726 | for (var i = 0; i < nodeList.length; i++) { |
michael@0 | 4727 | var node = nodeList[i]; |
michael@0 | 4728 | |
michael@0 | 4729 | // "Let parent be the parent of node." |
michael@0 | 4730 | var parent_ = node.parentNode; |
michael@0 | 4731 | |
michael@0 | 4732 | // "Remove node from parent." |
michael@0 | 4733 | parent_.removeChild(node); |
michael@0 | 4734 | |
michael@0 | 4735 | // "If the block node of parent has no visible children, and parent is |
michael@0 | 4736 | // editable or an editing host, call createElement("br") on the context |
michael@0 | 4737 | // object and append the result as the last child of parent." |
michael@0 | 4738 | if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible) |
michael@0 | 4739 | && (isEditable(parent_) || isEditingHost(parent_))) { |
michael@0 | 4740 | parent_.appendChild(document.createElement("br")); |
michael@0 | 4741 | } |
michael@0 | 4742 | |
michael@0 | 4743 | // "If strip wrappers is true or parent is not an ancestor container of |
michael@0 | 4744 | // start node, while parent is an editable inline node with length 0, |
michael@0 | 4745 | // let grandparent be the parent of parent, then remove parent from |
michael@0 | 4746 | // grandparent, then set parent to grandparent." |
michael@0 | 4747 | if (stripWrappers |
michael@0 | 4748 | || (!isAncestor(parent_, startNode) && parent_ != startNode)) { |
michael@0 | 4749 | while (isEditable(parent_) |
michael@0 | 4750 | && isInlineNode(parent_) |
michael@0 | 4751 | && getNodeLength(parent_) == 0) { |
michael@0 | 4752 | var grandparent = parent_.parentNode; |
michael@0 | 4753 | grandparent.removeChild(parent_); |
michael@0 | 4754 | parent_ = grandparent; |
michael@0 | 4755 | } |
michael@0 | 4756 | } |
michael@0 | 4757 | } |
michael@0 | 4758 | |
michael@0 | 4759 | // "If end node is an editable Text node, call deleteData(0, end offset) on |
michael@0 | 4760 | // it." |
michael@0 | 4761 | if (isEditable(endNode) |
michael@0 | 4762 | && endNode.nodeType == Node.TEXT_NODE) { |
michael@0 | 4763 | endNode.deleteData(0, endOffset); |
michael@0 | 4764 | } |
michael@0 | 4765 | |
michael@0 | 4766 | // "Canonicalize whitespace at the active range's start, with fix collapsed |
michael@0 | 4767 | // space false." |
michael@0 | 4768 | canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false); |
michael@0 | 4769 | |
michael@0 | 4770 | // "Canonicalize whitespace at the active range's end, with fix collapsed |
michael@0 | 4771 | // space false." |
michael@0 | 4772 | canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false); |
michael@0 | 4773 | |
michael@0 | 4774 | // "If block merging is false, or start block or end block is null, or |
michael@0 | 4775 | // start block is not in the same editing host as end block, or start block |
michael@0 | 4776 | // and end block are the same:" |
michael@0 | 4777 | if (!blockMerging |
michael@0 | 4778 | || !startBlock |
michael@0 | 4779 | || !endBlock |
michael@0 | 4780 | || !inSameEditingHost(startBlock, endBlock) |
michael@0 | 4781 | || startBlock == endBlock) { |
michael@0 | 4782 | // "If direction is "forward", call collapseToStart() on the context |
michael@0 | 4783 | // object's Selection." |
michael@0 | 4784 | if (direction == "forward") { |
michael@0 | 4785 | if (getSelection().rangeCount) { |
michael@0 | 4786 | getSelection().collapseToStart(); |
michael@0 | 4787 | } |
michael@0 | 4788 | getActiveRange().collapse(true); |
michael@0 | 4789 | |
michael@0 | 4790 | // "Otherwise, call collapseToEnd() on the context object's Selection." |
michael@0 | 4791 | } else { |
michael@0 | 4792 | if (getSelection().rangeCount) { |
michael@0 | 4793 | getSelection().collapseToEnd(); |
michael@0 | 4794 | } |
michael@0 | 4795 | getActiveRange().collapse(false); |
michael@0 | 4796 | } |
michael@0 | 4797 | |
michael@0 | 4798 | // "Restore states and values from overrides." |
michael@0 | 4799 | restoreStatesAndValues(overrides); |
michael@0 | 4800 | |
michael@0 | 4801 | // "Abort these steps." |
michael@0 | 4802 | return; |
michael@0 | 4803 | } |
michael@0 | 4804 | |
michael@0 | 4805 | // "If start block has one child, which is a collapsed block prop, remove |
michael@0 | 4806 | // its child from it." |
michael@0 | 4807 | if (startBlock.children.length == 1 |
michael@0 | 4808 | && isCollapsedBlockProp(startBlock.firstChild)) { |
michael@0 | 4809 | startBlock.removeChild(startBlock.firstChild); |
michael@0 | 4810 | } |
michael@0 | 4811 | |
michael@0 | 4812 | // "If start block is an ancestor of end block:" |
michael@0 | 4813 | if (isAncestor(startBlock, endBlock)) { |
michael@0 | 4814 | // "Let reference node be end block." |
michael@0 | 4815 | var referenceNode = endBlock; |
michael@0 | 4816 | |
michael@0 | 4817 | // "While reference node is not a child of start block, set reference |
michael@0 | 4818 | // node to its parent." |
michael@0 | 4819 | while (referenceNode.parentNode != startBlock) { |
michael@0 | 4820 | referenceNode = referenceNode.parentNode; |
michael@0 | 4821 | } |
michael@0 | 4822 | |
michael@0 | 4823 | // "Call collapse() on the context object's Selection, with first |
michael@0 | 4824 | // argument start block and second argument the index of reference |
michael@0 | 4825 | // node." |
michael@0 | 4826 | getSelection().collapse(startBlock, getNodeIndex(referenceNode)); |
michael@0 | 4827 | getActiveRange().setStart(startBlock, getNodeIndex(referenceNode)); |
michael@0 | 4828 | getActiveRange().collapse(true); |
michael@0 | 4829 | |
michael@0 | 4830 | // "If end block has no children:" |
michael@0 | 4831 | if (!endBlock.hasChildNodes()) { |
michael@0 | 4832 | // "While end block is editable and is the only child of its parent |
michael@0 | 4833 | // and is not a child of start block, let parent equal end block, |
michael@0 | 4834 | // then remove end block from parent, then set end block to |
michael@0 | 4835 | // parent." |
michael@0 | 4836 | while (isEditable(endBlock) |
michael@0 | 4837 | && endBlock.parentNode.childNodes.length == 1 |
michael@0 | 4838 | && endBlock.parentNode != startBlock) { |
michael@0 | 4839 | var parent_ = endBlock; |
michael@0 | 4840 | parent_.removeChild(endBlock); |
michael@0 | 4841 | endBlock = parent_; |
michael@0 | 4842 | } |
michael@0 | 4843 | |
michael@0 | 4844 | // "If end block is editable and is not an inline node, and its |
michael@0 | 4845 | // previousSibling and nextSibling are both inline nodes, call |
michael@0 | 4846 | // createElement("br") on the context object and insert it into end |
michael@0 | 4847 | // block's parent immediately after end block." |
michael@0 | 4848 | if (isEditable(endBlock) |
michael@0 | 4849 | && !isInlineNode(endBlock) |
michael@0 | 4850 | && isInlineNode(endBlock.previousSibling) |
michael@0 | 4851 | && isInlineNode(endBlock.nextSibling)) { |
michael@0 | 4852 | endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling); |
michael@0 | 4853 | } |
michael@0 | 4854 | |
michael@0 | 4855 | // "If end block is editable, remove it from its parent." |
michael@0 | 4856 | if (isEditable(endBlock)) { |
michael@0 | 4857 | endBlock.parentNode.removeChild(endBlock); |
michael@0 | 4858 | } |
michael@0 | 4859 | |
michael@0 | 4860 | // "Restore states and values from overrides." |
michael@0 | 4861 | restoreStatesAndValues(overrides); |
michael@0 | 4862 | |
michael@0 | 4863 | // "Abort these steps." |
michael@0 | 4864 | return; |
michael@0 | 4865 | } |
michael@0 | 4866 | |
michael@0 | 4867 | // "If end block's firstChild is not an inline node, restore states and |
michael@0 | 4868 | // values from overrides, then abort these steps." |
michael@0 | 4869 | if (!isInlineNode(endBlock.firstChild)) { |
michael@0 | 4870 | restoreStatesAndValues(overrides); |
michael@0 | 4871 | return; |
michael@0 | 4872 | } |
michael@0 | 4873 | |
michael@0 | 4874 | // "Let children be a list of nodes, initially empty." |
michael@0 | 4875 | var children = []; |
michael@0 | 4876 | |
michael@0 | 4877 | // "Append the first child of end block to children." |
michael@0 | 4878 | children.push(endBlock.firstChild); |
michael@0 | 4879 | |
michael@0 | 4880 | // "While children's last member is not a br, and children's last |
michael@0 | 4881 | // member's nextSibling is an inline node, append children's last |
michael@0 | 4882 | // member's nextSibling to children." |
michael@0 | 4883 | while (!isHtmlElement(children[children.length - 1], "br") |
michael@0 | 4884 | && isInlineNode(children[children.length - 1].nextSibling)) { |
michael@0 | 4885 | children.push(children[children.length - 1].nextSibling); |
michael@0 | 4886 | } |
michael@0 | 4887 | |
michael@0 | 4888 | // "Record the values of children, and let values be the result." |
michael@0 | 4889 | var values = recordValues(children); |
michael@0 | 4890 | |
michael@0 | 4891 | // "While children's first member's parent is not start block, split |
michael@0 | 4892 | // the parent of children." |
michael@0 | 4893 | while (children[0].parentNode != startBlock) { |
michael@0 | 4894 | splitParent(children); |
michael@0 | 4895 | } |
michael@0 | 4896 | |
michael@0 | 4897 | // "If children's first member's previousSibling is an editable br, |
michael@0 | 4898 | // remove that br from its parent." |
michael@0 | 4899 | if (isEditable(children[0].previousSibling) |
michael@0 | 4900 | && isHtmlElement(children[0].previousSibling, "br")) { |
michael@0 | 4901 | children[0].parentNode.removeChild(children[0].previousSibling); |
michael@0 | 4902 | } |
michael@0 | 4903 | |
michael@0 | 4904 | // "Otherwise, if start block is a descendant of end block:" |
michael@0 | 4905 | } else if (isDescendant(startBlock, endBlock)) { |
michael@0 | 4906 | // "Call collapse() on the context object's Selection, with first |
michael@0 | 4907 | // argument start block and second argument start block's length." |
michael@0 | 4908 | getSelection().collapse(startBlock, getNodeLength(startBlock)); |
michael@0 | 4909 | getActiveRange().setStart(startBlock, getNodeLength(startBlock)); |
michael@0 | 4910 | getActiveRange().collapse(true); |
michael@0 | 4911 | |
michael@0 | 4912 | // "Let reference node be start block." |
michael@0 | 4913 | var referenceNode = startBlock; |
michael@0 | 4914 | |
michael@0 | 4915 | // "While reference node is not a child of end block, set reference |
michael@0 | 4916 | // node to its parent." |
michael@0 | 4917 | while (referenceNode.parentNode != endBlock) { |
michael@0 | 4918 | referenceNode = referenceNode.parentNode; |
michael@0 | 4919 | } |
michael@0 | 4920 | |
michael@0 | 4921 | // "If reference node's nextSibling is an inline node and start block's |
michael@0 | 4922 | // lastChild is a br, remove start block's lastChild from it." |
michael@0 | 4923 | if (isInlineNode(referenceNode.nextSibling) |
michael@0 | 4924 | && isHtmlElement(startBlock.lastChild, "br")) { |
michael@0 | 4925 | startBlock.removeChild(startBlock.lastChild); |
michael@0 | 4926 | } |
michael@0 | 4927 | |
michael@0 | 4928 | // "Let nodes to move be a list of nodes, initially empty." |
michael@0 | 4929 | var nodesToMove = []; |
michael@0 | 4930 | |
michael@0 | 4931 | // "If reference node's nextSibling is neither null nor a block node, |
michael@0 | 4932 | // append it to nodes to move." |
michael@0 | 4933 | if (referenceNode.nextSibling |
michael@0 | 4934 | && !isBlockNode(referenceNode.nextSibling)) { |
michael@0 | 4935 | nodesToMove.push(referenceNode.nextSibling); |
michael@0 | 4936 | } |
michael@0 | 4937 | |
michael@0 | 4938 | // "While nodes to move is nonempty and its last member isn't a br and |
michael@0 | 4939 | // its last member's nextSibling is neither null nor a block node, |
michael@0 | 4940 | // append its last member's nextSibling to nodes to move." |
michael@0 | 4941 | if (nodesToMove.length |
michael@0 | 4942 | && !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br") |
michael@0 | 4943 | && nodesToMove[nodesToMove.length - 1].nextSibling |
michael@0 | 4944 | && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) { |
michael@0 | 4945 | nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); |
michael@0 | 4946 | } |
michael@0 | 4947 | |
michael@0 | 4948 | // "Record the values of nodes to move, and let values be the result." |
michael@0 | 4949 | var values = recordValues(nodesToMove); |
michael@0 | 4950 | |
michael@0 | 4951 | // "For each node in nodes to move, append node as the last child of |
michael@0 | 4952 | // start block, preserving ranges." |
michael@0 | 4953 | nodesToMove.forEach(function(node) { |
michael@0 | 4954 | movePreservingRanges(node, startBlock, -1); |
michael@0 | 4955 | }); |
michael@0 | 4956 | |
michael@0 | 4957 | // "Otherwise:" |
michael@0 | 4958 | } else { |
michael@0 | 4959 | // "Call collapse() on the context object's Selection, with first |
michael@0 | 4960 | // argument start block and second argument start block's length." |
michael@0 | 4961 | getSelection().collapse(startBlock, getNodeLength(startBlock)); |
michael@0 | 4962 | getActiveRange().setStart(startBlock, getNodeLength(startBlock)); |
michael@0 | 4963 | getActiveRange().collapse(true); |
michael@0 | 4964 | |
michael@0 | 4965 | // "If end block's firstChild is an inline node and start block's |
michael@0 | 4966 | // lastChild is a br, remove start block's lastChild from it." |
michael@0 | 4967 | if (isInlineNode(endBlock.firstChild) |
michael@0 | 4968 | && isHtmlElement(startBlock.lastChild, "br")) { |
michael@0 | 4969 | startBlock.removeChild(startBlock.lastChild); |
michael@0 | 4970 | } |
michael@0 | 4971 | |
michael@0 | 4972 | // "Record the values of end block's children, and let values be the |
michael@0 | 4973 | // result." |
michael@0 | 4974 | var values = recordValues([].slice.call(endBlock.childNodes)); |
michael@0 | 4975 | |
michael@0 | 4976 | // "While end block has children, append the first child of end block |
michael@0 | 4977 | // to start block, preserving ranges." |
michael@0 | 4978 | while (endBlock.hasChildNodes()) { |
michael@0 | 4979 | movePreservingRanges(endBlock.firstChild, startBlock, -1); |
michael@0 | 4980 | } |
michael@0 | 4981 | |
michael@0 | 4982 | // "While end block has no children, let parent be the parent of end |
michael@0 | 4983 | // block, then remove end block from parent, then set end block to |
michael@0 | 4984 | // parent." |
michael@0 | 4985 | while (!endBlock.hasChildNodes()) { |
michael@0 | 4986 | var parent_ = endBlock.parentNode; |
michael@0 | 4987 | parent_.removeChild(endBlock); |
michael@0 | 4988 | endBlock = parent_; |
michael@0 | 4989 | } |
michael@0 | 4990 | } |
michael@0 | 4991 | |
michael@0 | 4992 | // "Let ancestor be start block." |
michael@0 | 4993 | var ancestor = startBlock; |
michael@0 | 4994 | |
michael@0 | 4995 | // "While ancestor has an inclusive ancestor ol in the same editing host |
michael@0 | 4996 | // whose nextSibling is also an ol in the same editing host, or an |
michael@0 | 4997 | // inclusive ancestor ul in the same editing host whose nextSibling is also |
michael@0 | 4998 | // a ul in the same editing host:" |
michael@0 | 4999 | while (getInclusiveAncestors(ancestor).some(function(node) { |
michael@0 | 5000 | return inSameEditingHost(ancestor, node) |
michael@0 | 5001 | && ( |
michael@0 | 5002 | (isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol")) |
michael@0 | 5003 | || (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul")) |
michael@0 | 5004 | ) && inSameEditingHost(ancestor, node.nextSibling); |
michael@0 | 5005 | })) { |
michael@0 | 5006 | // "While ancestor and its nextSibling are not both ols in the same |
michael@0 | 5007 | // editing host, and are also not both uls in the same editing host, |
michael@0 | 5008 | // set ancestor to its parent." |
michael@0 | 5009 | while (!( |
michael@0 | 5010 | isHtmlElement(ancestor, "ol") |
michael@0 | 5011 | && isHtmlElement(ancestor.nextSibling, "ol") |
michael@0 | 5012 | && inSameEditingHost(ancestor, ancestor.nextSibling) |
michael@0 | 5013 | ) && !( |
michael@0 | 5014 | isHtmlElement(ancestor, "ul") |
michael@0 | 5015 | && isHtmlElement(ancestor.nextSibling, "ul") |
michael@0 | 5016 | && inSameEditingHost(ancestor, ancestor.nextSibling) |
michael@0 | 5017 | )) { |
michael@0 | 5018 | ancestor = ancestor.parentNode; |
michael@0 | 5019 | } |
michael@0 | 5020 | |
michael@0 | 5021 | // "While ancestor's nextSibling has children, append ancestor's |
michael@0 | 5022 | // nextSibling's firstChild as the last child of ancestor, preserving |
michael@0 | 5023 | // ranges." |
michael@0 | 5024 | while (ancestor.nextSibling.hasChildNodes()) { |
michael@0 | 5025 | movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1); |
michael@0 | 5026 | } |
michael@0 | 5027 | |
michael@0 | 5028 | // "Remove ancestor's nextSibling from its parent." |
michael@0 | 5029 | ancestor.parentNode.removeChild(ancestor.nextSibling); |
michael@0 | 5030 | } |
michael@0 | 5031 | |
michael@0 | 5032 | // "Restore the values from values." |
michael@0 | 5033 | restoreValues(values); |
michael@0 | 5034 | |
michael@0 | 5035 | // "If start block has no children, call createElement("br") on the context |
michael@0 | 5036 | // object and append the result as the last child of start block." |
michael@0 | 5037 | if (!startBlock.hasChildNodes()) { |
michael@0 | 5038 | startBlock.appendChild(document.createElement("br")); |
michael@0 | 5039 | } |
michael@0 | 5040 | |
michael@0 | 5041 | // "Remove extraneous line breaks at the end of start block." |
michael@0 | 5042 | removeExtraneousLineBreaksAtTheEndOf(startBlock); |
michael@0 | 5043 | |
michael@0 | 5044 | // "Restore states and values from overrides." |
michael@0 | 5045 | restoreStatesAndValues(overrides); |
michael@0 | 5046 | } |
michael@0 | 5047 | |
michael@0 | 5048 | |
michael@0 | 5049 | //@} |
michael@0 | 5050 | ///// Splitting a node list's parent ///// |
michael@0 | 5051 | //@{ |
michael@0 | 5052 | |
michael@0 | 5053 | function splitParent(nodeList) { |
michael@0 | 5054 | // "Let original parent be the parent of the first member of node list." |
michael@0 | 5055 | var originalParent = nodeList[0].parentNode; |
michael@0 | 5056 | |
michael@0 | 5057 | // "If original parent is not editable or its parent is null, do nothing |
michael@0 | 5058 | // and abort these steps." |
michael@0 | 5059 | if (!isEditable(originalParent) |
michael@0 | 5060 | || !originalParent.parentNode) { |
michael@0 | 5061 | return; |
michael@0 | 5062 | } |
michael@0 | 5063 | |
michael@0 | 5064 | // "If the first child of original parent is in node list, remove |
michael@0 | 5065 | // extraneous line breaks before original parent." |
michael@0 | 5066 | if (nodeList.indexOf(originalParent.firstChild) != -1) { |
michael@0 | 5067 | removeExtraneousLineBreaksBefore(originalParent); |
michael@0 | 5068 | } |
michael@0 | 5069 | |
michael@0 | 5070 | // "If the first child of original parent is in node list, and original |
michael@0 | 5071 | // parent follows a line break, set follows line break to true. Otherwise, |
michael@0 | 5072 | // set follows line break to false." |
michael@0 | 5073 | var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1 |
michael@0 | 5074 | && followsLineBreak(originalParent); |
michael@0 | 5075 | |
michael@0 | 5076 | // "If the last child of original parent is in node list, and original |
michael@0 | 5077 | // parent precedes a line break, set precedes line break to true. |
michael@0 | 5078 | // Otherwise, set precedes line break to false." |
michael@0 | 5079 | var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1 |
michael@0 | 5080 | && precedesLineBreak(originalParent); |
michael@0 | 5081 | |
michael@0 | 5082 | // "If the first child of original parent is not in node list, but its last |
michael@0 | 5083 | // child is:" |
michael@0 | 5084 | if (nodeList.indexOf(originalParent.firstChild) == -1 |
michael@0 | 5085 | && nodeList.indexOf(originalParent.lastChild) != -1) { |
michael@0 | 5086 | // "For each node in node list, in reverse order, insert node into the |
michael@0 | 5087 | // parent of original parent immediately after original parent, |
michael@0 | 5088 | // preserving ranges." |
michael@0 | 5089 | for (var i = nodeList.length - 1; i >= 0; i--) { |
michael@0 | 5090 | movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent)); |
michael@0 | 5091 | } |
michael@0 | 5092 | |
michael@0 | 5093 | // "If precedes line break is true, and the last member of node list |
michael@0 | 5094 | // does not precede a line break, call createElement("br") on the |
michael@0 | 5095 | // context object and insert the result immediately after the last |
michael@0 | 5096 | // member of node list." |
michael@0 | 5097 | if (precedesLineBreak_ |
michael@0 | 5098 | && !precedesLineBreak(nodeList[nodeList.length - 1])) { |
michael@0 | 5099 | nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); |
michael@0 | 5100 | } |
michael@0 | 5101 | |
michael@0 | 5102 | // "Remove extraneous line breaks at the end of original parent." |
michael@0 | 5103 | removeExtraneousLineBreaksAtTheEndOf(originalParent); |
michael@0 | 5104 | |
michael@0 | 5105 | // "Abort these steps." |
michael@0 | 5106 | return; |
michael@0 | 5107 | } |
michael@0 | 5108 | |
michael@0 | 5109 | // "If the first child of original parent is not in node list:" |
michael@0 | 5110 | if (nodeList.indexOf(originalParent.firstChild) == -1) { |
michael@0 | 5111 | // "Let cloned parent be the result of calling cloneNode(false) on |
michael@0 | 5112 | // original parent." |
michael@0 | 5113 | var clonedParent = originalParent.cloneNode(false); |
michael@0 | 5114 | |
michael@0 | 5115 | // "If original parent has an id attribute, unset it." |
michael@0 | 5116 | originalParent.removeAttribute("id"); |
michael@0 | 5117 | |
michael@0 | 5118 | // "Insert cloned parent into the parent of original parent immediately |
michael@0 | 5119 | // before original parent." |
michael@0 | 5120 | originalParent.parentNode.insertBefore(clonedParent, originalParent); |
michael@0 | 5121 | |
michael@0 | 5122 | // "While the previousSibling of the first member of node list is not |
michael@0 | 5123 | // null, append the first child of original parent as the last child of |
michael@0 | 5124 | // cloned parent, preserving ranges." |
michael@0 | 5125 | while (nodeList[0].previousSibling) { |
michael@0 | 5126 | movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length); |
michael@0 | 5127 | } |
michael@0 | 5128 | } |
michael@0 | 5129 | |
michael@0 | 5130 | // "For each node in node list, insert node into the parent of original |
michael@0 | 5131 | // parent immediately before original parent, preserving ranges." |
michael@0 | 5132 | for (var i = 0; i < nodeList.length; i++) { |
michael@0 | 5133 | movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent)); |
michael@0 | 5134 | } |
michael@0 | 5135 | |
michael@0 | 5136 | // "If follows line break is true, and the first member of node list does |
michael@0 | 5137 | // not follow a line break, call createElement("br") on the context object |
michael@0 | 5138 | // and insert the result immediately before the first member of node list." |
michael@0 | 5139 | if (followsLineBreak_ |
michael@0 | 5140 | && !followsLineBreak(nodeList[0])) { |
michael@0 | 5141 | nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]); |
michael@0 | 5142 | } |
michael@0 | 5143 | |
michael@0 | 5144 | // "If the last member of node list is an inline node other than a br, and |
michael@0 | 5145 | // the first child of original parent is a br, and original parent is not |
michael@0 | 5146 | // an inline node, remove the first child of original parent from original |
michael@0 | 5147 | // parent." |
michael@0 | 5148 | if (isInlineNode(nodeList[nodeList.length - 1]) |
michael@0 | 5149 | && !isHtmlElement(nodeList[nodeList.length - 1], "br") |
michael@0 | 5150 | && isHtmlElement(originalParent.firstChild, "br") |
michael@0 | 5151 | && !isInlineNode(originalParent)) { |
michael@0 | 5152 | originalParent.removeChild(originalParent.firstChild); |
michael@0 | 5153 | } |
michael@0 | 5154 | |
michael@0 | 5155 | // "If original parent has no children:" |
michael@0 | 5156 | if (!originalParent.hasChildNodes()) { |
michael@0 | 5157 | // "Remove original parent from its parent." |
michael@0 | 5158 | originalParent.parentNode.removeChild(originalParent); |
michael@0 | 5159 | |
michael@0 | 5160 | // "If precedes line break is true, and the last member of node list |
michael@0 | 5161 | // does not precede a line break, call createElement("br") on the |
michael@0 | 5162 | // context object and insert the result immediately after the last |
michael@0 | 5163 | // member of node list." |
michael@0 | 5164 | if (precedesLineBreak_ |
michael@0 | 5165 | && !precedesLineBreak(nodeList[nodeList.length - 1])) { |
michael@0 | 5166 | nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); |
michael@0 | 5167 | } |
michael@0 | 5168 | |
michael@0 | 5169 | // "Otherwise, remove extraneous line breaks before original parent." |
michael@0 | 5170 | } else { |
michael@0 | 5171 | removeExtraneousLineBreaksBefore(originalParent); |
michael@0 | 5172 | } |
michael@0 | 5173 | |
michael@0 | 5174 | // "If node list's last member's nextSibling is null, but its parent is not |
michael@0 | 5175 | // null, remove extraneous line breaks at the end of node list's last |
michael@0 | 5176 | // member's parent." |
michael@0 | 5177 | if (!nodeList[nodeList.length - 1].nextSibling |
michael@0 | 5178 | && nodeList[nodeList.length - 1].parentNode) { |
michael@0 | 5179 | removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode); |
michael@0 | 5180 | } |
michael@0 | 5181 | } |
michael@0 | 5182 | |
michael@0 | 5183 | // "To remove a node node while preserving its descendants, split the parent of |
michael@0 | 5184 | // node's children if it has any. If it has no children, instead remove it from |
michael@0 | 5185 | // its parent." |
michael@0 | 5186 | function removePreservingDescendants(node) { |
michael@0 | 5187 | if (node.hasChildNodes()) { |
michael@0 | 5188 | splitParent([].slice.call(node.childNodes)); |
michael@0 | 5189 | } else { |
michael@0 | 5190 | node.parentNode.removeChild(node); |
michael@0 | 5191 | } |
michael@0 | 5192 | } |
michael@0 | 5193 | |
michael@0 | 5194 | |
michael@0 | 5195 | //@} |
michael@0 | 5196 | ///// Canonical space sequences ///// |
michael@0 | 5197 | //@{ |
michael@0 | 5198 | |
michael@0 | 5199 | function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) { |
michael@0 | 5200 | // "If n is zero, return the empty string." |
michael@0 | 5201 | if (n == 0) { |
michael@0 | 5202 | return ""; |
michael@0 | 5203 | } |
michael@0 | 5204 | |
michael@0 | 5205 | // "If n is one and both non-breaking start and non-breaking end are false, |
michael@0 | 5206 | // return a single space (U+0020)." |
michael@0 | 5207 | if (n == 1 && !nonBreakingStart && !nonBreakingEnd) { |
michael@0 | 5208 | return " "; |
michael@0 | 5209 | } |
michael@0 | 5210 | |
michael@0 | 5211 | // "If n is one, return a single non-breaking space (U+00A0)." |
michael@0 | 5212 | if (n == 1) { |
michael@0 | 5213 | return "\xa0"; |
michael@0 | 5214 | } |
michael@0 | 5215 | |
michael@0 | 5216 | // "Let buffer be the empty string." |
michael@0 | 5217 | var buffer = ""; |
michael@0 | 5218 | |
michael@0 | 5219 | // "If non-breaking start is true, let repeated pair be U+00A0 U+0020. |
michael@0 | 5220 | // Otherwise, let it be U+0020 U+00A0." |
michael@0 | 5221 | var repeatedPair; |
michael@0 | 5222 | if (nonBreakingStart) { |
michael@0 | 5223 | repeatedPair = "\xa0 "; |
michael@0 | 5224 | } else { |
michael@0 | 5225 | repeatedPair = " \xa0"; |
michael@0 | 5226 | } |
michael@0 | 5227 | |
michael@0 | 5228 | // "While n is greater than three, append repeated pair to buffer and |
michael@0 | 5229 | // subtract two from n." |
michael@0 | 5230 | while (n > 3) { |
michael@0 | 5231 | buffer += repeatedPair; |
michael@0 | 5232 | n -= 2; |
michael@0 | 5233 | } |
michael@0 | 5234 | |
michael@0 | 5235 | // "If n is three, append a three-element string to buffer depending on |
michael@0 | 5236 | // non-breaking start and non-breaking end:" |
michael@0 | 5237 | if (n == 3) { |
michael@0 | 5238 | buffer += |
michael@0 | 5239 | !nonBreakingStart && !nonBreakingEnd ? " \xa0 " |
michael@0 | 5240 | : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 " |
michael@0 | 5241 | : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0" |
michael@0 | 5242 | : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0" |
michael@0 | 5243 | : "impossible"; |
michael@0 | 5244 | |
michael@0 | 5245 | // "Otherwise, append a two-element string to buffer depending on |
michael@0 | 5246 | // non-breaking start and non-breaking end:" |
michael@0 | 5247 | } else { |
michael@0 | 5248 | buffer += |
michael@0 | 5249 | !nonBreakingStart && !nonBreakingEnd ? "\xa0 " |
michael@0 | 5250 | : nonBreakingStart && !nonBreakingEnd ? "\xa0 " |
michael@0 | 5251 | : !nonBreakingStart && nonBreakingEnd ? " \xa0" |
michael@0 | 5252 | : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0" |
michael@0 | 5253 | : "impossible"; |
michael@0 | 5254 | } |
michael@0 | 5255 | |
michael@0 | 5256 | // "Return buffer." |
michael@0 | 5257 | return buffer; |
michael@0 | 5258 | } |
michael@0 | 5259 | |
michael@0 | 5260 | function canonicalizeWhitespace(node, offset, fixCollapsedSpace) { |
michael@0 | 5261 | if (fixCollapsedSpace === undefined) { |
michael@0 | 5262 | // "an optional boolean argument fix collapsed space that defaults to |
michael@0 | 5263 | // true" |
michael@0 | 5264 | fixCollapsedSpace = true; |
michael@0 | 5265 | } |
michael@0 | 5266 | |
michael@0 | 5267 | // "If node is neither editable nor an editing host, abort these steps." |
michael@0 | 5268 | if (!isEditable(node) && !isEditingHost(node)) { |
michael@0 | 5269 | return; |
michael@0 | 5270 | } |
michael@0 | 5271 | |
michael@0 | 5272 | // "Let start node equal node and let start offset equal offset." |
michael@0 | 5273 | var startNode = node; |
michael@0 | 5274 | var startOffset = offset; |
michael@0 | 5275 | |
michael@0 | 5276 | // "Repeat the following steps:" |
michael@0 | 5277 | while (true) { |
michael@0 | 5278 | // "If start node has a child in the same editing host with index start |
michael@0 | 5279 | // offset minus one, set start node to that child, then set start |
michael@0 | 5280 | // offset to start node's length." |
michael@0 | 5281 | if (0 <= startOffset - 1 |
michael@0 | 5282 | && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) { |
michael@0 | 5283 | startNode = startNode.childNodes[startOffset - 1]; |
michael@0 | 5284 | startOffset = getNodeLength(startNode); |
michael@0 | 5285 | |
michael@0 | 5286 | // "Otherwise, if start offset is zero and start node does not follow a |
michael@0 | 5287 | // line break and start node's parent is in the same editing host, set |
michael@0 | 5288 | // start offset to start node's index, then set start node to its |
michael@0 | 5289 | // parent." |
michael@0 | 5290 | } else if (startOffset == 0 |
michael@0 | 5291 | && !followsLineBreak(startNode) |
michael@0 | 5292 | && inSameEditingHost(startNode, startNode.parentNode)) { |
michael@0 | 5293 | startOffset = getNodeIndex(startNode); |
michael@0 | 5294 | startNode = startNode.parentNode; |
michael@0 | 5295 | |
michael@0 | 5296 | // "Otherwise, if start node is a Text node and its parent's resolved |
michael@0 | 5297 | // value for "white-space" is neither "pre" nor "pre-wrap" and start |
michael@0 | 5298 | // offset is not zero and the (start offset − 1)st element of start |
michael@0 | 5299 | // node's data is a space (0x0020) or non-breaking space (0x00A0), |
michael@0 | 5300 | // subtract one from start offset." |
michael@0 | 5301 | } else if (startNode.nodeType == Node.TEXT_NODE |
michael@0 | 5302 | && ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1 |
michael@0 | 5303 | && startOffset != 0 |
michael@0 | 5304 | && /[ \xa0]/.test(startNode.data[startOffset - 1])) { |
michael@0 | 5305 | startOffset--; |
michael@0 | 5306 | |
michael@0 | 5307 | // "Otherwise, break from this loop." |
michael@0 | 5308 | } else { |
michael@0 | 5309 | break; |
michael@0 | 5310 | } |
michael@0 | 5311 | } |
michael@0 | 5312 | |
michael@0 | 5313 | // "Let end node equal start node and end offset equal start offset." |
michael@0 | 5314 | var endNode = startNode; |
michael@0 | 5315 | var endOffset = startOffset; |
michael@0 | 5316 | |
michael@0 | 5317 | // "Let length equal zero." |
michael@0 | 5318 | var length = 0; |
michael@0 | 5319 | |
michael@0 | 5320 | // "Let collapse spaces be true if start offset is zero and start node |
michael@0 | 5321 | // follows a line break, otherwise false." |
michael@0 | 5322 | var collapseSpaces = startOffset == 0 && followsLineBreak(startNode); |
michael@0 | 5323 | |
michael@0 | 5324 | // "Repeat the following steps:" |
michael@0 | 5325 | while (true) { |
michael@0 | 5326 | // "If end node has a child in the same editing host with index end |
michael@0 | 5327 | // offset, set end node to that child, then set end offset to zero." |
michael@0 | 5328 | if (endOffset < endNode.childNodes.length |
michael@0 | 5329 | && inSameEditingHost(endNode, endNode.childNodes[endOffset])) { |
michael@0 | 5330 | endNode = endNode.childNodes[endOffset]; |
michael@0 | 5331 | endOffset = 0; |
michael@0 | 5332 | |
michael@0 | 5333 | // "Otherwise, if end offset is end node's length and end node does not |
michael@0 | 5334 | // precede a line break and end node's parent is in the same editing |
michael@0 | 5335 | // host, set end offset to one plus end node's index, then set end node |
michael@0 | 5336 | // to its parent." |
michael@0 | 5337 | } else if (endOffset == getNodeLength(endNode) |
michael@0 | 5338 | && !precedesLineBreak(endNode) |
michael@0 | 5339 | && inSameEditingHost(endNode, endNode.parentNode)) { |
michael@0 | 5340 | endOffset = 1 + getNodeIndex(endNode); |
michael@0 | 5341 | endNode = endNode.parentNode; |
michael@0 | 5342 | |
michael@0 | 5343 | // "Otherwise, if end node is a Text node and its parent's resolved |
michael@0 | 5344 | // value for "white-space" is neither "pre" nor "pre-wrap" and end |
michael@0 | 5345 | // offset is not end node's length and the end offsetth element of |
michael@0 | 5346 | // end node's data is a space (0x0020) or non-breaking space (0x00A0):" |
michael@0 | 5347 | } else if (endNode.nodeType == Node.TEXT_NODE |
michael@0 | 5348 | && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1 |
michael@0 | 5349 | && endOffset != getNodeLength(endNode) |
michael@0 | 5350 | && /[ \xa0]/.test(endNode.data[endOffset])) { |
michael@0 | 5351 | // "If fix collapsed space is true, and collapse spaces is true, |
michael@0 | 5352 | // and the end offsetth code unit of end node's data is a space |
michael@0 | 5353 | // (0x0020): call deleteData(end offset, 1) on end node, then |
michael@0 | 5354 | // continue this loop from the beginning." |
michael@0 | 5355 | if (fixCollapsedSpace |
michael@0 | 5356 | && collapseSpaces |
michael@0 | 5357 | && " " == endNode.data[endOffset]) { |
michael@0 | 5358 | endNode.deleteData(endOffset, 1); |
michael@0 | 5359 | continue; |
michael@0 | 5360 | } |
michael@0 | 5361 | |
michael@0 | 5362 | // "Set collapse spaces to true if the end offsetth element of end |
michael@0 | 5363 | // node's data is a space (0x0020), false otherwise." |
michael@0 | 5364 | collapseSpaces = " " == endNode.data[endOffset]; |
michael@0 | 5365 | |
michael@0 | 5366 | // "Add one to end offset." |
michael@0 | 5367 | endOffset++; |
michael@0 | 5368 | |
michael@0 | 5369 | // "Add one to length." |
michael@0 | 5370 | length++; |
michael@0 | 5371 | |
michael@0 | 5372 | // "Otherwise, break from this loop." |
michael@0 | 5373 | } else { |
michael@0 | 5374 | break; |
michael@0 | 5375 | } |
michael@0 | 5376 | } |
michael@0 | 5377 | |
michael@0 | 5378 | // "If fix collapsed space is true, then while (start node, start offset) |
michael@0 | 5379 | // is before (end node, end offset):" |
michael@0 | 5380 | if (fixCollapsedSpace) { |
michael@0 | 5381 | while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { |
michael@0 | 5382 | // "If end node has a child in the same editing host with index end |
michael@0 | 5383 | // offset − 1, set end node to that child, then set end offset to end |
michael@0 | 5384 | // node's length." |
michael@0 | 5385 | if (0 <= endOffset - 1 |
michael@0 | 5386 | && endOffset - 1 < endNode.childNodes.length |
michael@0 | 5387 | && inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) { |
michael@0 | 5388 | endNode = endNode.childNodes[endOffset - 1]; |
michael@0 | 5389 | endOffset = getNodeLength(endNode); |
michael@0 | 5390 | |
michael@0 | 5391 | // "Otherwise, if end offset is zero and end node's parent is in the |
michael@0 | 5392 | // same editing host, set end offset to end node's index, then set end |
michael@0 | 5393 | // node to its parent." |
michael@0 | 5394 | } else if (endOffset == 0 |
michael@0 | 5395 | && inSameEditingHost(endNode, endNode.parentNode)) { |
michael@0 | 5396 | endOffset = getNodeIndex(endNode); |
michael@0 | 5397 | endNode = endNode.parentNode; |
michael@0 | 5398 | |
michael@0 | 5399 | // "Otherwise, if end node is a Text node and its parent's resolved |
michael@0 | 5400 | // value for "white-space" is neither "pre" nor "pre-wrap" and end |
michael@0 | 5401 | // offset is end node's length and the last code unit of end node's |
michael@0 | 5402 | // data is a space (0x0020) and end node precedes a line break:" |
michael@0 | 5403 | } else if (endNode.nodeType == Node.TEXT_NODE |
michael@0 | 5404 | && ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1 |
michael@0 | 5405 | && endOffset == getNodeLength(endNode) |
michael@0 | 5406 | && endNode.data[endNode.data.length - 1] == " " |
michael@0 | 5407 | && precedesLineBreak(endNode)) { |
michael@0 | 5408 | // "Subtract one from end offset." |
michael@0 | 5409 | endOffset--; |
michael@0 | 5410 | |
michael@0 | 5411 | // "Subtract one from length." |
michael@0 | 5412 | length--; |
michael@0 | 5413 | |
michael@0 | 5414 | // "Call deleteData(end offset, 1) on end node." |
michael@0 | 5415 | endNode.deleteData(endOffset, 1); |
michael@0 | 5416 | |
michael@0 | 5417 | // "Otherwise, break from this loop." |
michael@0 | 5418 | } else { |
michael@0 | 5419 | break; |
michael@0 | 5420 | } |
michael@0 | 5421 | } |
michael@0 | 5422 | } |
michael@0 | 5423 | |
michael@0 | 5424 | // "Let replacement whitespace be the canonical space sequence of length |
michael@0 | 5425 | // length. non-breaking start is true if start offset is zero and start |
michael@0 | 5426 | // node follows a line break, and false otherwise. non-breaking end is true |
michael@0 | 5427 | // if end offset is end node's length and end node precedes a line break, |
michael@0 | 5428 | // and false otherwise." |
michael@0 | 5429 | var replacementWhitespace = canonicalSpaceSequence(length, |
michael@0 | 5430 | startOffset == 0 && followsLineBreak(startNode), |
michael@0 | 5431 | endOffset == getNodeLength(endNode) && precedesLineBreak(endNode)); |
michael@0 | 5432 | |
michael@0 | 5433 | // "While (start node, start offset) is before (end node, end offset):" |
michael@0 | 5434 | while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { |
michael@0 | 5435 | // "If start node has a child with index start offset, set start node |
michael@0 | 5436 | // to that child, then set start offset to zero." |
michael@0 | 5437 | if (startOffset < startNode.childNodes.length) { |
michael@0 | 5438 | startNode = startNode.childNodes[startOffset]; |
michael@0 | 5439 | startOffset = 0; |
michael@0 | 5440 | |
michael@0 | 5441 | // "Otherwise, if start node is not a Text node or if start offset is |
michael@0 | 5442 | // start node's length, set start offset to one plus start node's |
michael@0 | 5443 | // index, then set start node to its parent." |
michael@0 | 5444 | } else if (startNode.nodeType != Node.TEXT_NODE |
michael@0 | 5445 | || startOffset == getNodeLength(startNode)) { |
michael@0 | 5446 | startOffset = 1 + getNodeIndex(startNode); |
michael@0 | 5447 | startNode = startNode.parentNode; |
michael@0 | 5448 | |
michael@0 | 5449 | // "Otherwise:" |
michael@0 | 5450 | } else { |
michael@0 | 5451 | // "Remove the first element from replacement whitespace, and let |
michael@0 | 5452 | // element be that element." |
michael@0 | 5453 | var element = replacementWhitespace[0]; |
michael@0 | 5454 | replacementWhitespace = replacementWhitespace.slice(1); |
michael@0 | 5455 | |
michael@0 | 5456 | // "If element is not the same as the start offsetth element of |
michael@0 | 5457 | // start node's data:" |
michael@0 | 5458 | if (element != startNode.data[startOffset]) { |
michael@0 | 5459 | // "Call insertData(start offset, element) on start node." |
michael@0 | 5460 | startNode.insertData(startOffset, element); |
michael@0 | 5461 | |
michael@0 | 5462 | // "Call deleteData(start offset + 1, 1) on start node." |
michael@0 | 5463 | startNode.deleteData(startOffset + 1, 1); |
michael@0 | 5464 | } |
michael@0 | 5465 | |
michael@0 | 5466 | // "Add one to start offset." |
michael@0 | 5467 | startOffset++; |
michael@0 | 5468 | } |
michael@0 | 5469 | } |
michael@0 | 5470 | } |
michael@0 | 5471 | |
michael@0 | 5472 | |
michael@0 | 5473 | //@} |
michael@0 | 5474 | ///// Indenting and outdenting ///// |
michael@0 | 5475 | //@{ |
michael@0 | 5476 | |
michael@0 | 5477 | function indentNodes(nodeList) { |
michael@0 | 5478 | // "If node list is empty, do nothing and abort these steps." |
michael@0 | 5479 | if (!nodeList.length) { |
michael@0 | 5480 | return; |
michael@0 | 5481 | } |
michael@0 | 5482 | |
michael@0 | 5483 | // "Let first node be the first member of node list." |
michael@0 | 5484 | var firstNode = nodeList[0]; |
michael@0 | 5485 | |
michael@0 | 5486 | // "If first node's parent is an ol or ul:" |
michael@0 | 5487 | if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) { |
michael@0 | 5488 | // "Let tag be the local name of the parent of first node." |
michael@0 | 5489 | var tag = firstNode.parentNode.tagName; |
michael@0 | 5490 | |
michael@0 | 5491 | // "Wrap node list, with sibling criteria returning true for an HTML |
michael@0 | 5492 | // element with local name tag and false otherwise, and new parent |
michael@0 | 5493 | // instructions returning the result of calling createElement(tag) on |
michael@0 | 5494 | // the ownerDocument of first node." |
michael@0 | 5495 | wrap(nodeList, |
michael@0 | 5496 | function(node) { return isHtmlElement(node, tag) }, |
michael@0 | 5497 | function() { return firstNode.ownerDocument.createElement(tag) }); |
michael@0 | 5498 | |
michael@0 | 5499 | // "Abort these steps." |
michael@0 | 5500 | return; |
michael@0 | 5501 | } |
michael@0 | 5502 | |
michael@0 | 5503 | // "Wrap node list, with sibling criteria returning true for a simple |
michael@0 | 5504 | // indentation element and false otherwise, and new parent instructions |
michael@0 | 5505 | // returning the result of calling createElement("blockquote") on the |
michael@0 | 5506 | // ownerDocument of first node. Let new parent be the result." |
michael@0 | 5507 | var newParent = wrap(nodeList, |
michael@0 | 5508 | function(node) { return isSimpleIndentationElement(node) }, |
michael@0 | 5509 | function() { return firstNode.ownerDocument.createElement("blockquote") }); |
michael@0 | 5510 | |
michael@0 | 5511 | // "Fix disallowed ancestors of new parent." |
michael@0 | 5512 | fixDisallowedAncestors(newParent); |
michael@0 | 5513 | } |
michael@0 | 5514 | |
michael@0 | 5515 | function outdentNode(node) { |
michael@0 | 5516 | // "If node is not editable, abort these steps." |
michael@0 | 5517 | if (!isEditable(node)) { |
michael@0 | 5518 | return; |
michael@0 | 5519 | } |
michael@0 | 5520 | |
michael@0 | 5521 | // "If node is a simple indentation element, remove node, preserving its |
michael@0 | 5522 | // descendants. Then abort these steps." |
michael@0 | 5523 | if (isSimpleIndentationElement(node)) { |
michael@0 | 5524 | removePreservingDescendants(node); |
michael@0 | 5525 | return; |
michael@0 | 5526 | } |
michael@0 | 5527 | |
michael@0 | 5528 | // "If node is an indentation element:" |
michael@0 | 5529 | if (isIndentationElement(node)) { |
michael@0 | 5530 | // "Unset the dir attribute of node, if any." |
michael@0 | 5531 | node.removeAttribute("dir"); |
michael@0 | 5532 | |
michael@0 | 5533 | // "Unset the margin, padding, and border CSS properties of node." |
michael@0 | 5534 | node.style.margin = ""; |
michael@0 | 5535 | node.style.padding = ""; |
michael@0 | 5536 | node.style.border = ""; |
michael@0 | 5537 | if (node.getAttribute("style") == "" |
michael@0 | 5538 | // Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551 |
michael@0 | 5539 | || node.getAttribute("style") == "border-width: initial; border-color: initial; ") { |
michael@0 | 5540 | node.removeAttribute("style"); |
michael@0 | 5541 | } |
michael@0 | 5542 | |
michael@0 | 5543 | // "Set the tag name of node to "div"." |
michael@0 | 5544 | setTagName(node, "div"); |
michael@0 | 5545 | |
michael@0 | 5546 | // "Abort these steps." |
michael@0 | 5547 | return; |
michael@0 | 5548 | } |
michael@0 | 5549 | |
michael@0 | 5550 | // "Let current ancestor be node's parent." |
michael@0 | 5551 | var currentAncestor = node.parentNode; |
michael@0 | 5552 | |
michael@0 | 5553 | // "Let ancestor list be a list of nodes, initially empty." |
michael@0 | 5554 | var ancestorList = []; |
michael@0 | 5555 | |
michael@0 | 5556 | // "While current ancestor is an editable Element that is neither a simple |
michael@0 | 5557 | // indentation element nor an ol nor a ul, append current ancestor to |
michael@0 | 5558 | // ancestor list and then set current ancestor to its parent." |
michael@0 | 5559 | while (isEditable(currentAncestor) |
michael@0 | 5560 | && currentAncestor.nodeType == Node.ELEMENT_NODE |
michael@0 | 5561 | && !isSimpleIndentationElement(currentAncestor) |
michael@0 | 5562 | && !isHtmlElement(currentAncestor, ["ol", "ul"])) { |
michael@0 | 5563 | ancestorList.push(currentAncestor); |
michael@0 | 5564 | currentAncestor = currentAncestor.parentNode; |
michael@0 | 5565 | } |
michael@0 | 5566 | |
michael@0 | 5567 | // "If current ancestor is not an editable simple indentation element:" |
michael@0 | 5568 | if (!isEditable(currentAncestor) |
michael@0 | 5569 | || !isSimpleIndentationElement(currentAncestor)) { |
michael@0 | 5570 | // "Let current ancestor be node's parent." |
michael@0 | 5571 | currentAncestor = node.parentNode; |
michael@0 | 5572 | |
michael@0 | 5573 | // "Let ancestor list be the empty list." |
michael@0 | 5574 | ancestorList = []; |
michael@0 | 5575 | |
michael@0 | 5576 | // "While current ancestor is an editable Element that is neither an |
michael@0 | 5577 | // indentation element nor an ol nor a ul, append current ancestor to |
michael@0 | 5578 | // ancestor list and then set current ancestor to its parent." |
michael@0 | 5579 | while (isEditable(currentAncestor) |
michael@0 | 5580 | && currentAncestor.nodeType == Node.ELEMENT_NODE |
michael@0 | 5581 | && !isIndentationElement(currentAncestor) |
michael@0 | 5582 | && !isHtmlElement(currentAncestor, ["ol", "ul"])) { |
michael@0 | 5583 | ancestorList.push(currentAncestor); |
michael@0 | 5584 | currentAncestor = currentAncestor.parentNode; |
michael@0 | 5585 | } |
michael@0 | 5586 | } |
michael@0 | 5587 | |
michael@0 | 5588 | // "If node is an ol or ul and current ancestor is not an editable |
michael@0 | 5589 | // indentation element:" |
michael@0 | 5590 | if (isHtmlElement(node, ["OL", "UL"]) |
michael@0 | 5591 | && (!isEditable(currentAncestor) |
michael@0 | 5592 | || !isIndentationElement(currentAncestor))) { |
michael@0 | 5593 | // "Unset the reversed, start, and type attributes of node, if any are |
michael@0 | 5594 | // set." |
michael@0 | 5595 | node.removeAttribute("reversed"); |
michael@0 | 5596 | node.removeAttribute("start"); |
michael@0 | 5597 | node.removeAttribute("type"); |
michael@0 | 5598 | |
michael@0 | 5599 | // "Let children be the children of node." |
michael@0 | 5600 | var children = [].slice.call(node.childNodes); |
michael@0 | 5601 | |
michael@0 | 5602 | // "If node has attributes, and its parent is not an ol or ul, set the |
michael@0 | 5603 | // tag name of node to "div"." |
michael@0 | 5604 | if (node.attributes.length |
michael@0 | 5605 | && !isHtmlElement(node.parentNode, ["OL", "UL"])) { |
michael@0 | 5606 | setTagName(node, "div"); |
michael@0 | 5607 | |
michael@0 | 5608 | // "Otherwise:" |
michael@0 | 5609 | } else { |
michael@0 | 5610 | // "Record the values of node's children, and let values be the |
michael@0 | 5611 | // result." |
michael@0 | 5612 | var values = recordValues([].slice.call(node.childNodes)); |
michael@0 | 5613 | |
michael@0 | 5614 | // "Remove node, preserving its descendants." |
michael@0 | 5615 | removePreservingDescendants(node); |
michael@0 | 5616 | |
michael@0 | 5617 | // "Restore the values from values." |
michael@0 | 5618 | restoreValues(values); |
michael@0 | 5619 | } |
michael@0 | 5620 | |
michael@0 | 5621 | // "Fix disallowed ancestors of each member of children." |
michael@0 | 5622 | for (var i = 0; i < children.length; i++) { |
michael@0 | 5623 | fixDisallowedAncestors(children[i]); |
michael@0 | 5624 | } |
michael@0 | 5625 | |
michael@0 | 5626 | // "Abort these steps." |
michael@0 | 5627 | return; |
michael@0 | 5628 | } |
michael@0 | 5629 | |
michael@0 | 5630 | // "If current ancestor is not an editable indentation element, abort these |
michael@0 | 5631 | // steps." |
michael@0 | 5632 | if (!isEditable(currentAncestor) |
michael@0 | 5633 | || !isIndentationElement(currentAncestor)) { |
michael@0 | 5634 | return; |
michael@0 | 5635 | } |
michael@0 | 5636 | |
michael@0 | 5637 | // "Append current ancestor to ancestor list." |
michael@0 | 5638 | ancestorList.push(currentAncestor); |
michael@0 | 5639 | |
michael@0 | 5640 | // "Let original ancestor be current ancestor." |
michael@0 | 5641 | var originalAncestor = currentAncestor; |
michael@0 | 5642 | |
michael@0 | 5643 | // "While ancestor list is not empty:" |
michael@0 | 5644 | while (ancestorList.length) { |
michael@0 | 5645 | // "Let current ancestor be the last member of ancestor list." |
michael@0 | 5646 | // |
michael@0 | 5647 | // "Remove the last member of ancestor list." |
michael@0 | 5648 | currentAncestor = ancestorList.pop(); |
michael@0 | 5649 | |
michael@0 | 5650 | // "Let target be the child of current ancestor that is equal to either |
michael@0 | 5651 | // node or the last member of ancestor list." |
michael@0 | 5652 | var target = node.parentNode == currentAncestor |
michael@0 | 5653 | ? node |
michael@0 | 5654 | : ancestorList[ancestorList.length - 1]; |
michael@0 | 5655 | |
michael@0 | 5656 | // "If target is an inline node that is not a br, and its nextSibling |
michael@0 | 5657 | // is a br, remove target's nextSibling from its parent." |
michael@0 | 5658 | if (isInlineNode(target) |
michael@0 | 5659 | && !isHtmlElement(target, "BR") |
michael@0 | 5660 | && isHtmlElement(target.nextSibling, "BR")) { |
michael@0 | 5661 | target.parentNode.removeChild(target.nextSibling); |
michael@0 | 5662 | } |
michael@0 | 5663 | |
michael@0 | 5664 | // "Let preceding siblings be the preceding siblings of target, and let |
michael@0 | 5665 | // following siblings be the following siblings of target." |
michael@0 | 5666 | var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target)); |
michael@0 | 5667 | var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target)); |
michael@0 | 5668 | |
michael@0 | 5669 | // "Indent preceding siblings." |
michael@0 | 5670 | indentNodes(precedingSiblings); |
michael@0 | 5671 | |
michael@0 | 5672 | // "Indent following siblings." |
michael@0 | 5673 | indentNodes(followingSiblings); |
michael@0 | 5674 | } |
michael@0 | 5675 | |
michael@0 | 5676 | // "Outdent original ancestor." |
michael@0 | 5677 | outdentNode(originalAncestor); |
michael@0 | 5678 | } |
michael@0 | 5679 | |
michael@0 | 5680 | |
michael@0 | 5681 | //@} |
michael@0 | 5682 | ///// Toggling lists ///// |
michael@0 | 5683 | //@{ |
michael@0 | 5684 | |
michael@0 | 5685 | function toggleLists(tagName) { |
michael@0 | 5686 | // "Let mode be "disable" if the selection's list state is tag name, and |
michael@0 | 5687 | // "enable" otherwise." |
michael@0 | 5688 | var mode = getSelectionListState() == tagName ? "disable" : "enable"; |
michael@0 | 5689 | |
michael@0 | 5690 | var range = getActiveRange(); |
michael@0 | 5691 | tagName = tagName.toUpperCase(); |
michael@0 | 5692 | |
michael@0 | 5693 | // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is |
michael@0 | 5694 | // "ol"." |
michael@0 | 5695 | var otherTagName = tagName == "OL" ? "UL" : "OL"; |
michael@0 | 5696 | |
michael@0 | 5697 | // "Let items be a list of all lis that are ancestor containers of the |
michael@0 | 5698 | // range's start and/or end node." |
michael@0 | 5699 | // |
michael@0 | 5700 | // It's annoying to get this in tree order using functional stuff without |
michael@0 | 5701 | // doing getDescendants(document), which is slow, so I do it imperatively. |
michael@0 | 5702 | var items = []; |
michael@0 | 5703 | (function(){ |
michael@0 | 5704 | for ( |
michael@0 | 5705 | var ancestorContainer = range.endContainer; |
michael@0 | 5706 | ancestorContainer != range.commonAncestorContainer; |
michael@0 | 5707 | ancestorContainer = ancestorContainer.parentNode |
michael@0 | 5708 | ) { |
michael@0 | 5709 | if (isHtmlElement(ancestorContainer, "li")) { |
michael@0 | 5710 | items.unshift(ancestorContainer); |
michael@0 | 5711 | } |
michael@0 | 5712 | } |
michael@0 | 5713 | for ( |
michael@0 | 5714 | var ancestorContainer = range.startContainer; |
michael@0 | 5715 | ancestorContainer; |
michael@0 | 5716 | ancestorContainer = ancestorContainer.parentNode |
michael@0 | 5717 | ) { |
michael@0 | 5718 | if (isHtmlElement(ancestorContainer, "li")) { |
michael@0 | 5719 | items.unshift(ancestorContainer); |
michael@0 | 5720 | } |
michael@0 | 5721 | } |
michael@0 | 5722 | })(); |
michael@0 | 5723 | |
michael@0 | 5724 | // "For each item in items, normalize sublists of item." |
michael@0 | 5725 | items.forEach(normalizeSublists); |
michael@0 | 5726 | |
michael@0 | 5727 | // "Block-extend the range, and let new range be the result." |
michael@0 | 5728 | var newRange = blockExtend(range); |
michael@0 | 5729 | |
michael@0 | 5730 | // "If mode is "enable", then let lists to convert consist of every |
michael@0 | 5731 | // editable HTML element with local name other tag name that is contained |
michael@0 | 5732 | // in new range, and for every list in lists to convert:" |
michael@0 | 5733 | if (mode == "enable") { |
michael@0 | 5734 | getAllContainedNodes(newRange, function(node) { |
michael@0 | 5735 | return isEditable(node) |
michael@0 | 5736 | && isHtmlElement(node, otherTagName); |
michael@0 | 5737 | }).forEach(function(list) { |
michael@0 | 5738 | // "If list's previousSibling or nextSibling is an editable HTML |
michael@0 | 5739 | // element with local name tag name:" |
michael@0 | 5740 | if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName)) |
michael@0 | 5741 | || (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) { |
michael@0 | 5742 | // "Let children be list's children." |
michael@0 | 5743 | var children = [].slice.call(list.childNodes); |
michael@0 | 5744 | |
michael@0 | 5745 | // "Record the values of children, and let values be the |
michael@0 | 5746 | // result." |
michael@0 | 5747 | var values = recordValues(children); |
michael@0 | 5748 | |
michael@0 | 5749 | // "Split the parent of children." |
michael@0 | 5750 | splitParent(children); |
michael@0 | 5751 | |
michael@0 | 5752 | // "Wrap children, with sibling criteria returning true for an |
michael@0 | 5753 | // HTML element with local name tag name and false otherwise." |
michael@0 | 5754 | wrap(children, function(node) { return isHtmlElement(node, tagName) }); |
michael@0 | 5755 | |
michael@0 | 5756 | // "Restore the values from values." |
michael@0 | 5757 | restoreValues(values); |
michael@0 | 5758 | |
michael@0 | 5759 | // "Otherwise, set the tag name of list to tag name." |
michael@0 | 5760 | } else { |
michael@0 | 5761 | setTagName(list, tagName); |
michael@0 | 5762 | } |
michael@0 | 5763 | }); |
michael@0 | 5764 | } |
michael@0 | 5765 | |
michael@0 | 5766 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 5767 | // |
michael@0 | 5768 | // "For each node node contained in new range, if node is editable; the |
michael@0 | 5769 | // last member of node list (if any) is not an ancestor of node; node |
michael@0 | 5770 | // is not an indentation element; and either node is an ol or ul, or its |
michael@0 | 5771 | // parent is an ol or ul, or it is an allowed child of "li"; then append |
michael@0 | 5772 | // node to node list." |
michael@0 | 5773 | var nodeList = getContainedNodes(newRange, function(node) { |
michael@0 | 5774 | return isEditable(node) |
michael@0 | 5775 | && !isIndentationElement(node) |
michael@0 | 5776 | && (isHtmlElement(node, ["OL", "UL"]) |
michael@0 | 5777 | || isHtmlElement(node.parentNode, ["OL", "UL"]) |
michael@0 | 5778 | || isAllowedChild(node, "li")); |
michael@0 | 5779 | }); |
michael@0 | 5780 | |
michael@0 | 5781 | // "If mode is "enable", remove from node list any ol or ul whose parent is |
michael@0 | 5782 | // not also an ol or ul." |
michael@0 | 5783 | if (mode == "enable") { |
michael@0 | 5784 | nodeList = nodeList.filter(function(node) { |
michael@0 | 5785 | return !isHtmlElement(node, ["ol", "ul"]) |
michael@0 | 5786 | || isHtmlElement(node.parentNode, ["ol", "ul"]); |
michael@0 | 5787 | }); |
michael@0 | 5788 | } |
michael@0 | 5789 | |
michael@0 | 5790 | // "If mode is "disable", then while node list is not empty:" |
michael@0 | 5791 | if (mode == "disable") { |
michael@0 | 5792 | while (nodeList.length) { |
michael@0 | 5793 | // "Let sublist be an empty list of nodes." |
michael@0 | 5794 | var sublist = []; |
michael@0 | 5795 | |
michael@0 | 5796 | // "Remove the first member from node list and append it to |
michael@0 | 5797 | // sublist." |
michael@0 | 5798 | sublist.push(nodeList.shift()); |
michael@0 | 5799 | |
michael@0 | 5800 | // "If the first member of sublist is an HTML element with local |
michael@0 | 5801 | // name tag name, outdent it and continue this loop from the |
michael@0 | 5802 | // beginning." |
michael@0 | 5803 | if (isHtmlElement(sublist[0], tagName)) { |
michael@0 | 5804 | outdentNode(sublist[0]); |
michael@0 | 5805 | continue; |
michael@0 | 5806 | } |
michael@0 | 5807 | |
michael@0 | 5808 | // "While node list is not empty, and the first member of node list |
michael@0 | 5809 | // is the nextSibling of the last member of sublist and is not an |
michael@0 | 5810 | // HTML element with local name tag name, remove the first member |
michael@0 | 5811 | // from node list and append it to sublist." |
michael@0 | 5812 | while (nodeList.length |
michael@0 | 5813 | && nodeList[0] == sublist[sublist.length - 1].nextSibling |
michael@0 | 5814 | && !isHtmlElement(nodeList[0], tagName)) { |
michael@0 | 5815 | sublist.push(nodeList.shift()); |
michael@0 | 5816 | } |
michael@0 | 5817 | |
michael@0 | 5818 | // "Record the values of sublist, and let values be the result." |
michael@0 | 5819 | var values = recordValues(sublist); |
michael@0 | 5820 | |
michael@0 | 5821 | // "Split the parent of sublist." |
michael@0 | 5822 | splitParent(sublist); |
michael@0 | 5823 | |
michael@0 | 5824 | // "Fix disallowed ancestors of each member of sublist." |
michael@0 | 5825 | for (var i = 0; i < sublist.length; i++) { |
michael@0 | 5826 | fixDisallowedAncestors(sublist[i]); |
michael@0 | 5827 | } |
michael@0 | 5828 | |
michael@0 | 5829 | // "Restore the values from values." |
michael@0 | 5830 | restoreValues(values); |
michael@0 | 5831 | } |
michael@0 | 5832 | |
michael@0 | 5833 | // "Otherwise, while node list is not empty:" |
michael@0 | 5834 | } else { |
michael@0 | 5835 | while (nodeList.length) { |
michael@0 | 5836 | // "Let sublist be an empty list of nodes." |
michael@0 | 5837 | var sublist = []; |
michael@0 | 5838 | |
michael@0 | 5839 | // "While either sublist is empty, or node list is not empty and |
michael@0 | 5840 | // its first member is the nextSibling of sublist's last member:" |
michael@0 | 5841 | while (!sublist.length |
michael@0 | 5842 | || (nodeList.length |
michael@0 | 5843 | && nodeList[0] == sublist[sublist.length - 1].nextSibling)) { |
michael@0 | 5844 | // "If node list's first member is a p or div, set the tag name |
michael@0 | 5845 | // of node list's first member to "li", and append the result |
michael@0 | 5846 | // to sublist. Remove the first member from node list." |
michael@0 | 5847 | if (isHtmlElement(nodeList[0], ["p", "div"])) { |
michael@0 | 5848 | sublist.push(setTagName(nodeList[0], "li")); |
michael@0 | 5849 | nodeList.shift(); |
michael@0 | 5850 | |
michael@0 | 5851 | // "Otherwise, if the first member of node list is an li or ol |
michael@0 | 5852 | // or ul, remove it from node list and append it to sublist." |
michael@0 | 5853 | } else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) { |
michael@0 | 5854 | sublist.push(nodeList.shift()); |
michael@0 | 5855 | |
michael@0 | 5856 | // "Otherwise:" |
michael@0 | 5857 | } else { |
michael@0 | 5858 | // "Let nodes to wrap be a list of nodes, initially empty." |
michael@0 | 5859 | var nodesToWrap = []; |
michael@0 | 5860 | |
michael@0 | 5861 | // "While nodes to wrap is empty, or node list is not empty |
michael@0 | 5862 | // and its first member is the nextSibling of nodes to |
michael@0 | 5863 | // wrap's last member and the first member of node list is |
michael@0 | 5864 | // an inline node and the last member of nodes to wrap is |
michael@0 | 5865 | // an inline node other than a br, remove the first member |
michael@0 | 5866 | // from node list and append it to nodes to wrap." |
michael@0 | 5867 | while (!nodesToWrap.length |
michael@0 | 5868 | || (nodeList.length |
michael@0 | 5869 | && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling |
michael@0 | 5870 | && isInlineNode(nodeList[0]) |
michael@0 | 5871 | && isInlineNode(nodesToWrap[nodesToWrap.length - 1]) |
michael@0 | 5872 | && !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) { |
michael@0 | 5873 | nodesToWrap.push(nodeList.shift()); |
michael@0 | 5874 | } |
michael@0 | 5875 | |
michael@0 | 5876 | // "Wrap nodes to wrap, with new parent instructions |
michael@0 | 5877 | // returning the result of calling createElement("li") on |
michael@0 | 5878 | // the context object. Append the result to sublist." |
michael@0 | 5879 | sublist.push(wrap(nodesToWrap, |
michael@0 | 5880 | undefined, |
michael@0 | 5881 | function() { return document.createElement("li") })); |
michael@0 | 5882 | } |
michael@0 | 5883 | } |
michael@0 | 5884 | |
michael@0 | 5885 | // "If sublist's first member's parent is an HTML element with |
michael@0 | 5886 | // local name tag name, or if every member of sublist is an ol or |
michael@0 | 5887 | // ul, continue this loop from the beginning." |
michael@0 | 5888 | if (isHtmlElement(sublist[0].parentNode, tagName) |
michael@0 | 5889 | || sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) { |
michael@0 | 5890 | continue; |
michael@0 | 5891 | } |
michael@0 | 5892 | |
michael@0 | 5893 | // "If sublist's first member's parent is an HTML element with |
michael@0 | 5894 | // local name other tag name:" |
michael@0 | 5895 | if (isHtmlElement(sublist[0].parentNode, otherTagName)) { |
michael@0 | 5896 | // "Record the values of sublist, and let values be the |
michael@0 | 5897 | // result." |
michael@0 | 5898 | var values = recordValues(sublist); |
michael@0 | 5899 | |
michael@0 | 5900 | // "Split the parent of sublist." |
michael@0 | 5901 | splitParent(sublist); |
michael@0 | 5902 | |
michael@0 | 5903 | // "Wrap sublist, with sibling criteria returning true for an |
michael@0 | 5904 | // HTML element with local name tag name and false otherwise, |
michael@0 | 5905 | // and new parent instructions returning the result of calling |
michael@0 | 5906 | // createElement(tag name) on the context object." |
michael@0 | 5907 | wrap(sublist, |
michael@0 | 5908 | function(node) { return isHtmlElement(node, tagName) }, |
michael@0 | 5909 | function() { return document.createElement(tagName) }); |
michael@0 | 5910 | |
michael@0 | 5911 | // "Restore the values from values." |
michael@0 | 5912 | restoreValues(values); |
michael@0 | 5913 | |
michael@0 | 5914 | // "Continue this loop from the beginning." |
michael@0 | 5915 | continue; |
michael@0 | 5916 | } |
michael@0 | 5917 | |
michael@0 | 5918 | // "Wrap sublist, with sibling criteria returning true for an HTML |
michael@0 | 5919 | // element with local name tag name and false otherwise, and new |
michael@0 | 5920 | // parent instructions being the following:" |
michael@0 | 5921 | // . . . |
michael@0 | 5922 | // "Fix disallowed ancestors of the previous step's result." |
michael@0 | 5923 | fixDisallowedAncestors(wrap(sublist, |
michael@0 | 5924 | function(node) { return isHtmlElement(node, tagName) }, |
michael@0 | 5925 | function() { |
michael@0 | 5926 | // "If sublist's first member's parent is not an editable |
michael@0 | 5927 | // simple indentation element, or sublist's first member's |
michael@0 | 5928 | // parent's previousSibling is not an editable HTML element |
michael@0 | 5929 | // with local name tag name, call createElement(tag name) |
michael@0 | 5930 | // on the context object and return the result." |
michael@0 | 5931 | if (!isEditable(sublist[0].parentNode) |
michael@0 | 5932 | || !isSimpleIndentationElement(sublist[0].parentNode) |
michael@0 | 5933 | || !isEditable(sublist[0].parentNode.previousSibling) |
michael@0 | 5934 | || !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) { |
michael@0 | 5935 | return document.createElement(tagName); |
michael@0 | 5936 | } |
michael@0 | 5937 | |
michael@0 | 5938 | // "Let list be sublist's first member's parent's |
michael@0 | 5939 | // previousSibling." |
michael@0 | 5940 | var list = sublist[0].parentNode.previousSibling; |
michael@0 | 5941 | |
michael@0 | 5942 | // "Normalize sublists of list's lastChild." |
michael@0 | 5943 | normalizeSublists(list.lastChild); |
michael@0 | 5944 | |
michael@0 | 5945 | // "If list's lastChild is not an editable HTML element |
michael@0 | 5946 | // with local name tag name, call createElement(tag name) |
michael@0 | 5947 | // on the context object, and append the result as the last |
michael@0 | 5948 | // child of list." |
michael@0 | 5949 | if (!isEditable(list.lastChild) |
michael@0 | 5950 | || !isHtmlElement(list.lastChild, tagName)) { |
michael@0 | 5951 | list.appendChild(document.createElement(tagName)); |
michael@0 | 5952 | } |
michael@0 | 5953 | |
michael@0 | 5954 | // "Return the last child of list." |
michael@0 | 5955 | return list.lastChild; |
michael@0 | 5956 | } |
michael@0 | 5957 | )); |
michael@0 | 5958 | } |
michael@0 | 5959 | } |
michael@0 | 5960 | } |
michael@0 | 5961 | |
michael@0 | 5962 | |
michael@0 | 5963 | //@} |
michael@0 | 5964 | ///// Justifying the selection ///// |
michael@0 | 5965 | //@{ |
michael@0 | 5966 | |
michael@0 | 5967 | function justifySelection(alignment) { |
michael@0 | 5968 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 5969 | var newRange = blockExtend(globalRange); |
michael@0 | 5970 | |
michael@0 | 5971 | // "Let element list be a list of all editable Elements contained in new |
michael@0 | 5972 | // range that either has an attribute in the HTML namespace whose local |
michael@0 | 5973 | // name is "align", or has a style attribute that sets "text-align", or is |
michael@0 | 5974 | // a center." |
michael@0 | 5975 | var elementList = getAllContainedNodes(newRange, function(node) { |
michael@0 | 5976 | return node.nodeType == Node.ELEMENT_NODE |
michael@0 | 5977 | && isEditable(node) |
michael@0 | 5978 | // Ignoring namespaces here |
michael@0 | 5979 | && ( |
michael@0 | 5980 | node.hasAttribute("align") |
michael@0 | 5981 | || node.style.textAlign != "" |
michael@0 | 5982 | || isHtmlElement(node, "center") |
michael@0 | 5983 | ); |
michael@0 | 5984 | }); |
michael@0 | 5985 | |
michael@0 | 5986 | // "For each element in element list:" |
michael@0 | 5987 | for (var i = 0; i < elementList.length; i++) { |
michael@0 | 5988 | var element = elementList[i]; |
michael@0 | 5989 | |
michael@0 | 5990 | // "If element has an attribute in the HTML namespace whose local name |
michael@0 | 5991 | // is "align", remove that attribute." |
michael@0 | 5992 | element.removeAttribute("align"); |
michael@0 | 5993 | |
michael@0 | 5994 | // "Unset the CSS property "text-align" on element, if it's set by a |
michael@0 | 5995 | // style attribute." |
michael@0 | 5996 | element.style.textAlign = ""; |
michael@0 | 5997 | if (element.getAttribute("style") == "") { |
michael@0 | 5998 | element.removeAttribute("style"); |
michael@0 | 5999 | } |
michael@0 | 6000 | |
michael@0 | 6001 | // "If element is a div or span or center with no attributes, remove |
michael@0 | 6002 | // it, preserving its descendants." |
michael@0 | 6003 | if (isHtmlElement(element, ["div", "span", "center"]) |
michael@0 | 6004 | && !element.attributes.length) { |
michael@0 | 6005 | removePreservingDescendants(element); |
michael@0 | 6006 | } |
michael@0 | 6007 | |
michael@0 | 6008 | // "If element is a center with one or more attributes, set the tag |
michael@0 | 6009 | // name of element to "div"." |
michael@0 | 6010 | if (isHtmlElement(element, "center") |
michael@0 | 6011 | && element.attributes.length) { |
michael@0 | 6012 | setTagName(element, "div"); |
michael@0 | 6013 | } |
michael@0 | 6014 | } |
michael@0 | 6015 | |
michael@0 | 6016 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 6017 | newRange = blockExtend(globalRange); |
michael@0 | 6018 | |
michael@0 | 6019 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 6020 | var nodeList = []; |
michael@0 | 6021 | |
michael@0 | 6022 | // "For each node node contained in new range, append node to node list if |
michael@0 | 6023 | // the last member of node list (if any) is not an ancestor of node; node |
michael@0 | 6024 | // is editable; node is an allowed child of "div"; and node's alignment |
michael@0 | 6025 | // value is not alignment." |
michael@0 | 6026 | nodeList = getContainedNodes(newRange, function(node) { |
michael@0 | 6027 | return isEditable(node) |
michael@0 | 6028 | && isAllowedChild(node, "div") |
michael@0 | 6029 | && getAlignmentValue(node) != alignment; |
michael@0 | 6030 | }); |
michael@0 | 6031 | |
michael@0 | 6032 | // "While node list is not empty:" |
michael@0 | 6033 | while (nodeList.length) { |
michael@0 | 6034 | // "Let sublist be a list of nodes, initially empty." |
michael@0 | 6035 | var sublist = []; |
michael@0 | 6036 | |
michael@0 | 6037 | // "Remove the first member of node list and append it to sublist." |
michael@0 | 6038 | sublist.push(nodeList.shift()); |
michael@0 | 6039 | |
michael@0 | 6040 | // "While node list is not empty, and the first member of node list is |
michael@0 | 6041 | // the nextSibling of the last member of sublist, remove the first |
michael@0 | 6042 | // member of node list and append it to sublist." |
michael@0 | 6043 | while (nodeList.length |
michael@0 | 6044 | && nodeList[0] == sublist[sublist.length - 1].nextSibling) { |
michael@0 | 6045 | sublist.push(nodeList.shift()); |
michael@0 | 6046 | } |
michael@0 | 6047 | |
michael@0 | 6048 | // "Wrap sublist. Sibling criteria returns true for any div that has |
michael@0 | 6049 | // one or both of the following two attributes and no other attributes, |
michael@0 | 6050 | // and false otherwise:" |
michael@0 | 6051 | // |
michael@0 | 6052 | // * "An align attribute whose value is an ASCII case-insensitive |
michael@0 | 6053 | // match for alignment. |
michael@0 | 6054 | // * "A style attribute which sets exactly one CSS property |
michael@0 | 6055 | // (including unrecognized or invalid attributes), which is |
michael@0 | 6056 | // "text-align", which is set to alignment. |
michael@0 | 6057 | // |
michael@0 | 6058 | // "New parent instructions are to call createElement("div") on the |
michael@0 | 6059 | // context object, then set its CSS property "text-align" to alignment |
michael@0 | 6060 | // and return the result." |
michael@0 | 6061 | wrap(sublist, |
michael@0 | 6062 | function(node) { |
michael@0 | 6063 | return isHtmlElement(node, "div") |
michael@0 | 6064 | && [].every.call(node.attributes, function(attr) { |
michael@0 | 6065 | return (attr.name == "align" && attr.value.toLowerCase() == alignment) |
michael@0 | 6066 | || (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment); |
michael@0 | 6067 | }); |
michael@0 | 6068 | }, |
michael@0 | 6069 | function() { |
michael@0 | 6070 | var newParent = document.createElement("div"); |
michael@0 | 6071 | newParent.setAttribute("style", "text-align: " + alignment); |
michael@0 | 6072 | return newParent; |
michael@0 | 6073 | } |
michael@0 | 6074 | ); |
michael@0 | 6075 | } |
michael@0 | 6076 | } |
michael@0 | 6077 | |
michael@0 | 6078 | |
michael@0 | 6079 | //@} |
michael@0 | 6080 | ///// Automatic linking ///// |
michael@0 | 6081 | //@{ |
michael@0 | 6082 | // "An autolinkable URL is a string of the following form:" |
michael@0 | 6083 | var autolinkableUrlRegexp = |
michael@0 | 6084 | // "Either a string matching the scheme pattern from RFC 3986 section 3.1 |
michael@0 | 6085 | // followed by the literal string ://, or the literal string mailto:; |
michael@0 | 6086 | // followed by" |
michael@0 | 6087 | // |
michael@0 | 6088 | // From the RFC: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) |
michael@0 | 6089 | "([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)" |
michael@0 | 6090 | // "Zero or more characters other than space characters; followed by" |
michael@0 | 6091 | + "[^ \t\n\f\r]*" |
michael@0 | 6092 | // "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}." |
michael@0 | 6093 | + "[^!\"'(),\\-.:;<>[\\]`{}]"; |
michael@0 | 6094 | |
michael@0 | 6095 | // "A valid e-mail address is a string that matches the ABNF production 1*( |
michael@0 | 6096 | // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC |
michael@0 | 6097 | // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5." |
michael@0 | 6098 | // |
michael@0 | 6099 | // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / |
michael@0 | 6100 | // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" |
michael@0 | 6101 | // |
michael@0 | 6102 | //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> |
michael@0 | 6103 | //<let-dig-hyp> ::= <let-dig> | "-" |
michael@0 | 6104 | //<let-dig> ::= <letter> | <digit> |
michael@0 | 6105 | var validEmailRegexp = |
michael@0 | 6106 | "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*"; |
michael@0 | 6107 | |
michael@0 | 6108 | function autolink(node, endOffset) { |
michael@0 | 6109 | // "While (node, end offset)'s previous equivalent point is not null, set |
michael@0 | 6110 | // it to its previous equivalent point." |
michael@0 | 6111 | while (getPreviousEquivalentPoint(node, endOffset)) { |
michael@0 | 6112 | var prev = getPreviousEquivalentPoint(node, endOffset); |
michael@0 | 6113 | node = prev[0]; |
michael@0 | 6114 | endOffset = prev[1]; |
michael@0 | 6115 | } |
michael@0 | 6116 | |
michael@0 | 6117 | // "If node is not a Text node, or has an a ancestor, do nothing and abort |
michael@0 | 6118 | // these steps." |
michael@0 | 6119 | if (node.nodeType != Node.TEXT_NODE |
michael@0 | 6120 | || getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) { |
michael@0 | 6121 | return; |
michael@0 | 6122 | } |
michael@0 | 6123 | |
michael@0 | 6124 | // "Let search be the largest substring of node's data whose end is end |
michael@0 | 6125 | // offset and that contains no space characters." |
michael@0 | 6126 | var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0]; |
michael@0 | 6127 | |
michael@0 | 6128 | // "If some substring of search is an autolinkable URL:" |
michael@0 | 6129 | if (new RegExp(autolinkableUrlRegexp).test(search)) { |
michael@0 | 6130 | // "While there is no substring of node's data ending at end offset |
michael@0 | 6131 | // that is an autolinkable URL, decrement end offset." |
michael@0 | 6132 | while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) { |
michael@0 | 6133 | endOffset--; |
michael@0 | 6134 | } |
michael@0 | 6135 | |
michael@0 | 6136 | // "Let start offset be the start index of the longest substring of |
michael@0 | 6137 | // node's data that is an autolinkable URL ending at end offset." |
michael@0 | 6138 | var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index; |
michael@0 | 6139 | |
michael@0 | 6140 | // "Let href be the substring of node's data starting at start offset |
michael@0 | 6141 | // and ending at end offset." |
michael@0 | 6142 | var href = node.substringData(startOffset, endOffset - startOffset); |
michael@0 | 6143 | |
michael@0 | 6144 | // "Otherwise, if some substring of search is a valid e-mail address:" |
michael@0 | 6145 | } else if (new RegExp(validEmailRegexp).test(search)) { |
michael@0 | 6146 | // "While there is no substring of node's data ending at end offset |
michael@0 | 6147 | // that is a valid e-mail address, decrement end offset." |
michael@0 | 6148 | while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) { |
michael@0 | 6149 | endOffset--; |
michael@0 | 6150 | } |
michael@0 | 6151 | |
michael@0 | 6152 | // "Let start offset be the start index of the longest substring of |
michael@0 | 6153 | // node's data that is a valid e-mail address ending at end offset." |
michael@0 | 6154 | var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index; |
michael@0 | 6155 | |
michael@0 | 6156 | // "Let href be "mailto:" concatenated with the substring of node's |
michael@0 | 6157 | // data starting at start offset and ending at end offset." |
michael@0 | 6158 | var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset); |
michael@0 | 6159 | |
michael@0 | 6160 | // "Otherwise, do nothing and abort these steps." |
michael@0 | 6161 | } else { |
michael@0 | 6162 | return; |
michael@0 | 6163 | } |
michael@0 | 6164 | |
michael@0 | 6165 | // "Let original range be the active range." |
michael@0 | 6166 | var originalRange = getActiveRange(); |
michael@0 | 6167 | |
michael@0 | 6168 | // "Create a new range with start (node, start offset) and end (node, end |
michael@0 | 6169 | // offset), and set the context object's selection's range to it." |
michael@0 | 6170 | var newRange = document.createRange(); |
michael@0 | 6171 | newRange.setStart(node, startOffset); |
michael@0 | 6172 | newRange.setEnd(node, endOffset); |
michael@0 | 6173 | getSelection().removeAllRanges(); |
michael@0 | 6174 | getSelection().addRange(newRange); |
michael@0 | 6175 | globalRange = newRange; |
michael@0 | 6176 | |
michael@0 | 6177 | // "Take the action for "createLink", with value equal to href." |
michael@0 | 6178 | commands.createlink.action(href); |
michael@0 | 6179 | |
michael@0 | 6180 | // "Set the context object's selection's range to original range." |
michael@0 | 6181 | getSelection().removeAllRanges(); |
michael@0 | 6182 | getSelection().addRange(originalRange); |
michael@0 | 6183 | globalRange = originalRange; |
michael@0 | 6184 | } |
michael@0 | 6185 | //@} |
michael@0 | 6186 | ///// The delete command ///// |
michael@0 | 6187 | //@{ |
michael@0 | 6188 | commands["delete"] = { |
michael@0 | 6189 | preservesOverrides: true, |
michael@0 | 6190 | action: function() { |
michael@0 | 6191 | // "If the active range is not collapsed, delete the selection and |
michael@0 | 6192 | // return true." |
michael@0 | 6193 | if (!getActiveRange().collapsed) { |
michael@0 | 6194 | deleteSelection(); |
michael@0 | 6195 | return true; |
michael@0 | 6196 | } |
michael@0 | 6197 | |
michael@0 | 6198 | // "Canonicalize whitespace at the active range's start." |
michael@0 | 6199 | canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); |
michael@0 | 6200 | |
michael@0 | 6201 | // "Let node and offset be the active range's start node and offset." |
michael@0 | 6202 | var node = getActiveRange().startContainer; |
michael@0 | 6203 | var offset = getActiveRange().startOffset; |
michael@0 | 6204 | |
michael@0 | 6205 | // "Repeat the following steps:" |
michael@0 | 6206 | while (true) { |
michael@0 | 6207 | // "If offset is zero and node's previousSibling is an editable |
michael@0 | 6208 | // invisible node, remove node's previousSibling from its parent." |
michael@0 | 6209 | if (offset == 0 |
michael@0 | 6210 | && isEditable(node.previousSibling) |
michael@0 | 6211 | && isInvisible(node.previousSibling)) { |
michael@0 | 6212 | node.parentNode.removeChild(node.previousSibling); |
michael@0 | 6213 | |
michael@0 | 6214 | // "Otherwise, if node has a child with index offset − 1 and that |
michael@0 | 6215 | // child is an editable invisible node, remove that child from |
michael@0 | 6216 | // node, then subtract one from offset." |
michael@0 | 6217 | } else if (0 <= offset - 1 |
michael@0 | 6218 | && offset - 1 < node.childNodes.length |
michael@0 | 6219 | && isEditable(node.childNodes[offset - 1]) |
michael@0 | 6220 | && isInvisible(node.childNodes[offset - 1])) { |
michael@0 | 6221 | node.removeChild(node.childNodes[offset - 1]); |
michael@0 | 6222 | offset--; |
michael@0 | 6223 | |
michael@0 | 6224 | // "Otherwise, if offset is zero and node is an inline node, or if |
michael@0 | 6225 | // node is an invisible node, set offset to the index of node, then |
michael@0 | 6226 | // set node to its parent." |
michael@0 | 6227 | } else if ((offset == 0 |
michael@0 | 6228 | && isInlineNode(node)) |
michael@0 | 6229 | || isInvisible(node)) { |
michael@0 | 6230 | offset = getNodeIndex(node); |
michael@0 | 6231 | node = node.parentNode; |
michael@0 | 6232 | |
michael@0 | 6233 | // "Otherwise, if node has a child with index offset − 1 and that |
michael@0 | 6234 | // child is an editable a, remove that child from node, preserving |
michael@0 | 6235 | // its descendants. Then return true." |
michael@0 | 6236 | } else if (0 <= offset - 1 |
michael@0 | 6237 | && offset - 1 < node.childNodes.length |
michael@0 | 6238 | && isEditable(node.childNodes[offset - 1]) |
michael@0 | 6239 | && isHtmlElement(node.childNodes[offset - 1], "a")) { |
michael@0 | 6240 | removePreservingDescendants(node.childNodes[offset - 1]); |
michael@0 | 6241 | return true; |
michael@0 | 6242 | |
michael@0 | 6243 | // "Otherwise, if node has a child with index offset − 1 and that |
michael@0 | 6244 | // child is not a block node or a br or an img, set node to that |
michael@0 | 6245 | // child, then set offset to the length of node." |
michael@0 | 6246 | } else if (0 <= offset - 1 |
michael@0 | 6247 | && offset - 1 < node.childNodes.length |
michael@0 | 6248 | && !isBlockNode(node.childNodes[offset - 1]) |
michael@0 | 6249 | && !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) { |
michael@0 | 6250 | node = node.childNodes[offset - 1]; |
michael@0 | 6251 | offset = getNodeLength(node); |
michael@0 | 6252 | |
michael@0 | 6253 | // "Otherwise, break from this loop." |
michael@0 | 6254 | } else { |
michael@0 | 6255 | break; |
michael@0 | 6256 | } |
michael@0 | 6257 | } |
michael@0 | 6258 | |
michael@0 | 6259 | // "If node is a Text node and offset is not zero, or if node is a |
michael@0 | 6260 | // block node that has a child with index offset − 1 and that child is |
michael@0 | 6261 | // a br or hr or img:" |
michael@0 | 6262 | if ((node.nodeType == Node.TEXT_NODE |
michael@0 | 6263 | && offset != 0) |
michael@0 | 6264 | || (isBlockNode(node) |
michael@0 | 6265 | && 0 <= offset - 1 |
michael@0 | 6266 | && offset - 1 < node.childNodes.length |
michael@0 | 6267 | && isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) { |
michael@0 | 6268 | // "Call collapse(node, offset) on the context object's Selection." |
michael@0 | 6269 | getSelection().collapse(node, offset); |
michael@0 | 6270 | getActiveRange().setEnd(node, offset); |
michael@0 | 6271 | |
michael@0 | 6272 | // "Call extend(node, offset − 1) on the context object's |
michael@0 | 6273 | // Selection." |
michael@0 | 6274 | getSelection().extend(node, offset - 1); |
michael@0 | 6275 | getActiveRange().setStart(node, offset - 1); |
michael@0 | 6276 | |
michael@0 | 6277 | // "Delete the selection." |
michael@0 | 6278 | deleteSelection(); |
michael@0 | 6279 | |
michael@0 | 6280 | // "Return true." |
michael@0 | 6281 | return true; |
michael@0 | 6282 | } |
michael@0 | 6283 | |
michael@0 | 6284 | // "If node is an inline node, return true." |
michael@0 | 6285 | if (isInlineNode(node)) { |
michael@0 | 6286 | return true; |
michael@0 | 6287 | } |
michael@0 | 6288 | |
michael@0 | 6289 | // "If node is an li or dt or dd and is the first child of its parent, |
michael@0 | 6290 | // and offset is zero:" |
michael@0 | 6291 | if (isHtmlElement(node, ["li", "dt", "dd"]) |
michael@0 | 6292 | && node == node.parentNode.firstChild |
michael@0 | 6293 | && offset == 0) { |
michael@0 | 6294 | // "Let items be a list of all lis that are ancestors of node." |
michael@0 | 6295 | // |
michael@0 | 6296 | // Remember, must be in tree order. |
michael@0 | 6297 | var items = []; |
michael@0 | 6298 | for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) { |
michael@0 | 6299 | if (isHtmlElement(ancestor, "li")) { |
michael@0 | 6300 | items.unshift(ancestor); |
michael@0 | 6301 | } |
michael@0 | 6302 | } |
michael@0 | 6303 | |
michael@0 | 6304 | // "Normalize sublists of each item in items." |
michael@0 | 6305 | for (var i = 0; i < items.length; i++) { |
michael@0 | 6306 | normalizeSublists(items[i]); |
michael@0 | 6307 | } |
michael@0 | 6308 | |
michael@0 | 6309 | // "Record the values of the one-node list consisting of node, and |
michael@0 | 6310 | // let values be the result." |
michael@0 | 6311 | var values = recordValues([node]); |
michael@0 | 6312 | |
michael@0 | 6313 | // "Split the parent of the one-node list consisting of node." |
michael@0 | 6314 | splitParent([node]); |
michael@0 | 6315 | |
michael@0 | 6316 | // "Restore the values from values." |
michael@0 | 6317 | restoreValues(values); |
michael@0 | 6318 | |
michael@0 | 6319 | // "If node is a dd or dt, and it is not an allowed child of any of |
michael@0 | 6320 | // its ancestors in the same editing host, set the tag name of node |
michael@0 | 6321 | // to the default single-line container name and let node be the |
michael@0 | 6322 | // result." |
michael@0 | 6323 | if (isHtmlElement(node, ["dd", "dt"]) |
michael@0 | 6324 | && getAncestors(node).every(function(ancestor) { |
michael@0 | 6325 | return !inSameEditingHost(node, ancestor) |
michael@0 | 6326 | || !isAllowedChild(node, ancestor) |
michael@0 | 6327 | })) { |
michael@0 | 6328 | node = setTagName(node, defaultSingleLineContainerName); |
michael@0 | 6329 | } |
michael@0 | 6330 | |
michael@0 | 6331 | // "Fix disallowed ancestors of node." |
michael@0 | 6332 | fixDisallowedAncestors(node); |
michael@0 | 6333 | |
michael@0 | 6334 | // "Return true." |
michael@0 | 6335 | return true; |
michael@0 | 6336 | } |
michael@0 | 6337 | |
michael@0 | 6338 | // "Let start node equal node and let start offset equal offset." |
michael@0 | 6339 | var startNode = node; |
michael@0 | 6340 | var startOffset = offset; |
michael@0 | 6341 | |
michael@0 | 6342 | // "Repeat the following steps:" |
michael@0 | 6343 | while (true) { |
michael@0 | 6344 | // "If start offset is zero, set start offset to the index of start |
michael@0 | 6345 | // node and then set start node to its parent." |
michael@0 | 6346 | if (startOffset == 0) { |
michael@0 | 6347 | startOffset = getNodeIndex(startNode); |
michael@0 | 6348 | startNode = startNode.parentNode; |
michael@0 | 6349 | |
michael@0 | 6350 | // "Otherwise, if start node has an editable invisible child with |
michael@0 | 6351 | // index start offset minus one, remove it from start node and |
michael@0 | 6352 | // subtract one from start offset." |
michael@0 | 6353 | } else if (0 <= startOffset - 1 |
michael@0 | 6354 | && startOffset - 1 < startNode.childNodes.length |
michael@0 | 6355 | && isEditable(startNode.childNodes[startOffset - 1]) |
michael@0 | 6356 | && isInvisible(startNode.childNodes[startOffset - 1])) { |
michael@0 | 6357 | startNode.removeChild(startNode.childNodes[startOffset - 1]); |
michael@0 | 6358 | startOffset--; |
michael@0 | 6359 | |
michael@0 | 6360 | // "Otherwise, break from this loop." |
michael@0 | 6361 | } else { |
michael@0 | 6362 | break; |
michael@0 | 6363 | } |
michael@0 | 6364 | } |
michael@0 | 6365 | |
michael@0 | 6366 | // "If offset is zero, and node has an editable ancestor container in |
michael@0 | 6367 | // the same editing host that's an indentation element:" |
michael@0 | 6368 | if (offset == 0 |
michael@0 | 6369 | && getAncestors(node).concat(node).filter(function(ancestor) { |
michael@0 | 6370 | return isEditable(ancestor) |
michael@0 | 6371 | && inSameEditingHost(ancestor, node) |
michael@0 | 6372 | && isIndentationElement(ancestor); |
michael@0 | 6373 | }).length) { |
michael@0 | 6374 | // "Block-extend the range whose start and end are both (node, 0), |
michael@0 | 6375 | // and let new range be the result." |
michael@0 | 6376 | var newRange = document.createRange(); |
michael@0 | 6377 | newRange.setStart(node, 0); |
michael@0 | 6378 | newRange = blockExtend(newRange); |
michael@0 | 6379 | |
michael@0 | 6380 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 6381 | // |
michael@0 | 6382 | // "For each node current node contained in new range, append |
michael@0 | 6383 | // current node to node list if the last member of node list (if |
michael@0 | 6384 | // any) is not an ancestor of current node, and current node is |
michael@0 | 6385 | // editable but has no editable descendants." |
michael@0 | 6386 | var nodeList = getContainedNodes(newRange, function(currentNode) { |
michael@0 | 6387 | return isEditable(currentNode) |
michael@0 | 6388 | && !hasEditableDescendants(currentNode); |
michael@0 | 6389 | }); |
michael@0 | 6390 | |
michael@0 | 6391 | // "Outdent each node in node list." |
michael@0 | 6392 | for (var i = 0; i < nodeList.length; i++) { |
michael@0 | 6393 | outdentNode(nodeList[i]); |
michael@0 | 6394 | } |
michael@0 | 6395 | |
michael@0 | 6396 | // "Return true." |
michael@0 | 6397 | return true; |
michael@0 | 6398 | } |
michael@0 | 6399 | |
michael@0 | 6400 | // "If the child of start node with index start offset is a table, |
michael@0 | 6401 | // return true." |
michael@0 | 6402 | if (isHtmlElement(startNode.childNodes[startOffset], "table")) { |
michael@0 | 6403 | return true; |
michael@0 | 6404 | } |
michael@0 | 6405 | |
michael@0 | 6406 | // "If start node has a child with index start offset − 1, and that |
michael@0 | 6407 | // child is a table:" |
michael@0 | 6408 | if (0 <= startOffset - 1 |
michael@0 | 6409 | && startOffset - 1 < startNode.childNodes.length |
michael@0 | 6410 | && isHtmlElement(startNode.childNodes[startOffset - 1], "table")) { |
michael@0 | 6411 | // "Call collapse(start node, start offset − 1) on the context |
michael@0 | 6412 | // object's Selection." |
michael@0 | 6413 | getSelection().collapse(startNode, startOffset - 1); |
michael@0 | 6414 | getActiveRange().setStart(startNode, startOffset - 1); |
michael@0 | 6415 | |
michael@0 | 6416 | // "Call extend(start node, start offset) on the context object's |
michael@0 | 6417 | // Selection." |
michael@0 | 6418 | getSelection().extend(startNode, startOffset); |
michael@0 | 6419 | getActiveRange().setEnd(startNode, startOffset); |
michael@0 | 6420 | |
michael@0 | 6421 | // "Return true." |
michael@0 | 6422 | return true; |
michael@0 | 6423 | } |
michael@0 | 6424 | |
michael@0 | 6425 | // "If offset is zero; and either the child of start node with index |
michael@0 | 6426 | // start offset minus one is an hr, or the child is a br whose |
michael@0 | 6427 | // previousSibling is either a br or not an inline node:" |
michael@0 | 6428 | if (offset == 0 |
michael@0 | 6429 | && (isHtmlElement(startNode.childNodes[startOffset - 1], "hr") |
michael@0 | 6430 | || ( |
michael@0 | 6431 | isHtmlElement(startNode.childNodes[startOffset - 1], "br") |
michael@0 | 6432 | && ( |
michael@0 | 6433 | isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br") |
michael@0 | 6434 | || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling) |
michael@0 | 6435 | ) |
michael@0 | 6436 | ) |
michael@0 | 6437 | )) { |
michael@0 | 6438 | // "Call collapse(start node, start offset − 1) on the context |
michael@0 | 6439 | // object's Selection." |
michael@0 | 6440 | getSelection().collapse(startNode, startOffset - 1); |
michael@0 | 6441 | getActiveRange().setStart(startNode, startOffset - 1); |
michael@0 | 6442 | |
michael@0 | 6443 | // "Call extend(start node, start offset) on the context object's |
michael@0 | 6444 | // Selection." |
michael@0 | 6445 | getSelection().extend(startNode, startOffset); |
michael@0 | 6446 | getActiveRange().setEnd(startNode, startOffset); |
michael@0 | 6447 | |
michael@0 | 6448 | // "Delete the selection." |
michael@0 | 6449 | deleteSelection(); |
michael@0 | 6450 | |
michael@0 | 6451 | // "Call collapse(node, offset) on the Selection." |
michael@0 | 6452 | getSelection().collapse(node, offset); |
michael@0 | 6453 | getActiveRange().setStart(node, offset); |
michael@0 | 6454 | getActiveRange().collapse(true); |
michael@0 | 6455 | |
michael@0 | 6456 | // "Return true." |
michael@0 | 6457 | return true; |
michael@0 | 6458 | } |
michael@0 | 6459 | |
michael@0 | 6460 | // "If the child of start node with index start offset is an li or dt |
michael@0 | 6461 | // or dd, and that child's firstChild is an inline node, and start |
michael@0 | 6462 | // offset is not zero:" |
michael@0 | 6463 | if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) |
michael@0 | 6464 | && isInlineNode(startNode.childNodes[startOffset].firstChild) |
michael@0 | 6465 | && startOffset != 0) { |
michael@0 | 6466 | // "Let previous item be the child of start node with index start |
michael@0 | 6467 | // offset minus one." |
michael@0 | 6468 | var previousItem = startNode.childNodes[startOffset - 1]; |
michael@0 | 6469 | |
michael@0 | 6470 | // "If previous item's lastChild is an inline node other than a br, |
michael@0 | 6471 | // call createElement("br") on the context object and append the |
michael@0 | 6472 | // result as the last child of previous item." |
michael@0 | 6473 | if (isInlineNode(previousItem.lastChild) |
michael@0 | 6474 | && !isHtmlElement(previousItem.lastChild, "br")) { |
michael@0 | 6475 | previousItem.appendChild(document.createElement("br")); |
michael@0 | 6476 | } |
michael@0 | 6477 | |
michael@0 | 6478 | // "If previous item's lastChild is an inline node, call |
michael@0 | 6479 | // createElement("br") on the context object and append the result |
michael@0 | 6480 | // as the last child of previous item." |
michael@0 | 6481 | if (isInlineNode(previousItem.lastChild)) { |
michael@0 | 6482 | previousItem.appendChild(document.createElement("br")); |
michael@0 | 6483 | } |
michael@0 | 6484 | } |
michael@0 | 6485 | |
michael@0 | 6486 | // "If start node's child with index start offset is an li or dt or dd, |
michael@0 | 6487 | // and that child's previousSibling is also an li or dt or dd:" |
michael@0 | 6488 | if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"]) |
michael@0 | 6489 | && isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) { |
michael@0 | 6490 | // "Call cloneRange() on the active range, and let original range |
michael@0 | 6491 | // be the result." |
michael@0 | 6492 | // |
michael@0 | 6493 | // We need to add it to extraRanges so it will actually get updated |
michael@0 | 6494 | // when moving preserving ranges. |
michael@0 | 6495 | var originalRange = getActiveRange().cloneRange(); |
michael@0 | 6496 | extraRanges.push(originalRange); |
michael@0 | 6497 | |
michael@0 | 6498 | // "Set start node to its child with index start offset − 1." |
michael@0 | 6499 | startNode = startNode.childNodes[startOffset - 1]; |
michael@0 | 6500 | |
michael@0 | 6501 | // "Set start offset to start node's length." |
michael@0 | 6502 | startOffset = getNodeLength(startNode); |
michael@0 | 6503 | |
michael@0 | 6504 | // "Set node to start node's nextSibling." |
michael@0 | 6505 | node = startNode.nextSibling; |
michael@0 | 6506 | |
michael@0 | 6507 | // "Call collapse(start node, start offset) on the context object's |
michael@0 | 6508 | // Selection." |
michael@0 | 6509 | getSelection().collapse(startNode, startOffset); |
michael@0 | 6510 | getActiveRange().setStart(startNode, startOffset); |
michael@0 | 6511 | |
michael@0 | 6512 | // "Call extend(node, 0) on the context object's Selection." |
michael@0 | 6513 | getSelection().extend(node, 0); |
michael@0 | 6514 | getActiveRange().setEnd(node, 0); |
michael@0 | 6515 | |
michael@0 | 6516 | // "Delete the selection." |
michael@0 | 6517 | deleteSelection(); |
michael@0 | 6518 | |
michael@0 | 6519 | // "Call removeAllRanges() on the context object's Selection." |
michael@0 | 6520 | getSelection().removeAllRanges(); |
michael@0 | 6521 | |
michael@0 | 6522 | // "Call addRange(original range) on the context object's |
michael@0 | 6523 | // Selection." |
michael@0 | 6524 | getSelection().addRange(originalRange); |
michael@0 | 6525 | getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset); |
michael@0 | 6526 | getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset); |
michael@0 | 6527 | |
michael@0 | 6528 | // "Return true." |
michael@0 | 6529 | extraRanges.pop(); |
michael@0 | 6530 | return true; |
michael@0 | 6531 | } |
michael@0 | 6532 | |
michael@0 | 6533 | // "While start node has a child with index start offset minus one:" |
michael@0 | 6534 | while (0 <= startOffset - 1 |
michael@0 | 6535 | && startOffset - 1 < startNode.childNodes.length) { |
michael@0 | 6536 | // "If start node's child with index start offset minus one is |
michael@0 | 6537 | // editable and invisible, remove it from start node, then subtract |
michael@0 | 6538 | // one from start offset." |
michael@0 | 6539 | if (isEditable(startNode.childNodes[startOffset - 1]) |
michael@0 | 6540 | && isInvisible(startNode.childNodes[startOffset - 1])) { |
michael@0 | 6541 | startNode.removeChild(startNode.childNodes[startOffset - 1]); |
michael@0 | 6542 | startOffset--; |
michael@0 | 6543 | |
michael@0 | 6544 | // "Otherwise, set start node to its child with index start offset |
michael@0 | 6545 | // minus one, then set start offset to the length of start node." |
michael@0 | 6546 | } else { |
michael@0 | 6547 | startNode = startNode.childNodes[startOffset - 1]; |
michael@0 | 6548 | startOffset = getNodeLength(startNode); |
michael@0 | 6549 | } |
michael@0 | 6550 | } |
michael@0 | 6551 | |
michael@0 | 6552 | // "Call collapse(start node, start offset) on the context object's |
michael@0 | 6553 | // Selection." |
michael@0 | 6554 | getSelection().collapse(startNode, startOffset); |
michael@0 | 6555 | getActiveRange().setStart(startNode, startOffset); |
michael@0 | 6556 | |
michael@0 | 6557 | // "Call extend(node, offset) on the context object's Selection." |
michael@0 | 6558 | getSelection().extend(node, offset); |
michael@0 | 6559 | getActiveRange().setEnd(node, offset); |
michael@0 | 6560 | |
michael@0 | 6561 | // "Delete the selection, with direction "backward"." |
michael@0 | 6562 | deleteSelection({direction: "backward"}); |
michael@0 | 6563 | |
michael@0 | 6564 | // "Return true." |
michael@0 | 6565 | return true; |
michael@0 | 6566 | } |
michael@0 | 6567 | }; |
michael@0 | 6568 | |
michael@0 | 6569 | //@} |
michael@0 | 6570 | ///// The formatBlock command ///// |
michael@0 | 6571 | //@{ |
michael@0 | 6572 | // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3", |
michael@0 | 6573 | // "h4", "h5", "h6", "p", or "pre"." |
michael@0 | 6574 | var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3", |
michael@0 | 6575 | "h4", "h5", "h6", "p", "pre"]; |
michael@0 | 6576 | |
michael@0 | 6577 | commands.formatblock = { |
michael@0 | 6578 | preservesOverrides: true, |
michael@0 | 6579 | action: function(value) { |
michael@0 | 6580 | // "If value begins with a "<" character and ends with a ">" character, |
michael@0 | 6581 | // remove the first and last characters from it." |
michael@0 | 6582 | if (/^<.*>$/.test(value)) { |
michael@0 | 6583 | value = value.slice(1, -1); |
michael@0 | 6584 | } |
michael@0 | 6585 | |
michael@0 | 6586 | // "Let value be converted to ASCII lowercase." |
michael@0 | 6587 | value = value.toLowerCase(); |
michael@0 | 6588 | |
michael@0 | 6589 | // "If value is not a formattable block name, return false." |
michael@0 | 6590 | if (formattableBlockNames.indexOf(value) == -1) { |
michael@0 | 6591 | return false; |
michael@0 | 6592 | } |
michael@0 | 6593 | |
michael@0 | 6594 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 6595 | var newRange = blockExtend(getActiveRange()); |
michael@0 | 6596 | |
michael@0 | 6597 | // "Let node list be an empty list of nodes." |
michael@0 | 6598 | // |
michael@0 | 6599 | // "For each node node contained in new range, append node to node list |
michael@0 | 6600 | // if it is editable, the last member of original node list (if any) is |
michael@0 | 6601 | // not an ancestor of node, node is either a non-list single-line |
michael@0 | 6602 | // container or an allowed child of "p" or a dd or dt, and node is not |
michael@0 | 6603 | // the ancestor of a prohibited paragraph child." |
michael@0 | 6604 | var nodeList = getContainedNodes(newRange, function(node) { |
michael@0 | 6605 | return isEditable(node) |
michael@0 | 6606 | && (isNonListSingleLineContainer(node) |
michael@0 | 6607 | || isAllowedChild(node, "p") |
michael@0 | 6608 | || isHtmlElement(node, ["dd", "dt"])) |
michael@0 | 6609 | && !getDescendants(node).some(isProhibitedParagraphChild); |
michael@0 | 6610 | }); |
michael@0 | 6611 | |
michael@0 | 6612 | // "Record the values of node list, and let values be the result." |
michael@0 | 6613 | var values = recordValues(nodeList); |
michael@0 | 6614 | |
michael@0 | 6615 | // "For each node in node list, while node is the descendant of an |
michael@0 | 6616 | // editable HTML element in the same editing host, whose local name is |
michael@0 | 6617 | // a formattable block name, and which is not the ancestor of a |
michael@0 | 6618 | // prohibited paragraph child, split the parent of the one-node list |
michael@0 | 6619 | // consisting of node." |
michael@0 | 6620 | for (var i = 0; i < nodeList.length; i++) { |
michael@0 | 6621 | var node = nodeList[i]; |
michael@0 | 6622 | while (getAncestors(node).some(function(ancestor) { |
michael@0 | 6623 | return isEditable(ancestor) |
michael@0 | 6624 | && inSameEditingHost(ancestor, node) |
michael@0 | 6625 | && isHtmlElement(ancestor, formattableBlockNames) |
michael@0 | 6626 | && !getDescendants(ancestor).some(isProhibitedParagraphChild); |
michael@0 | 6627 | })) { |
michael@0 | 6628 | splitParent([node]); |
michael@0 | 6629 | } |
michael@0 | 6630 | } |
michael@0 | 6631 | |
michael@0 | 6632 | // "Restore the values from values." |
michael@0 | 6633 | restoreValues(values); |
michael@0 | 6634 | |
michael@0 | 6635 | // "While node list is not empty:" |
michael@0 | 6636 | while (nodeList.length) { |
michael@0 | 6637 | var sublist; |
michael@0 | 6638 | |
michael@0 | 6639 | // "If the first member of node list is a single-line |
michael@0 | 6640 | // container:" |
michael@0 | 6641 | if (isSingleLineContainer(nodeList[0])) { |
michael@0 | 6642 | // "Let sublist be the children of the first member of node |
michael@0 | 6643 | // list." |
michael@0 | 6644 | sublist = [].slice.call(nodeList[0].childNodes); |
michael@0 | 6645 | |
michael@0 | 6646 | // "Record the values of sublist, and let values be the |
michael@0 | 6647 | // result." |
michael@0 | 6648 | var values = recordValues(sublist); |
michael@0 | 6649 | |
michael@0 | 6650 | // "Remove the first member of node list from its parent, |
michael@0 | 6651 | // preserving its descendants." |
michael@0 | 6652 | removePreservingDescendants(nodeList[0]); |
michael@0 | 6653 | |
michael@0 | 6654 | // "Restore the values from values." |
michael@0 | 6655 | restoreValues(values); |
michael@0 | 6656 | |
michael@0 | 6657 | // "Remove the first member from node list." |
michael@0 | 6658 | nodeList.shift(); |
michael@0 | 6659 | |
michael@0 | 6660 | // "Otherwise:" |
michael@0 | 6661 | } else { |
michael@0 | 6662 | // "Let sublist be an empty list of nodes." |
michael@0 | 6663 | sublist = []; |
michael@0 | 6664 | |
michael@0 | 6665 | // "Remove the first member of node list and append it to |
michael@0 | 6666 | // sublist." |
michael@0 | 6667 | sublist.push(nodeList.shift()); |
michael@0 | 6668 | |
michael@0 | 6669 | // "While node list is not empty, and the first member of |
michael@0 | 6670 | // node list is the nextSibling of the last member of |
michael@0 | 6671 | // sublist, and the first member of node list is not a |
michael@0 | 6672 | // single-line container, and the last member of sublist is |
michael@0 | 6673 | // not a br, remove the first member of node list and |
michael@0 | 6674 | // append it to sublist." |
michael@0 | 6675 | while (nodeList.length |
michael@0 | 6676 | && nodeList[0] == sublist[sublist.length - 1].nextSibling |
michael@0 | 6677 | && !isSingleLineContainer(nodeList[0]) |
michael@0 | 6678 | && !isHtmlElement(sublist[sublist.length - 1], "BR")) { |
michael@0 | 6679 | sublist.push(nodeList.shift()); |
michael@0 | 6680 | } |
michael@0 | 6681 | } |
michael@0 | 6682 | |
michael@0 | 6683 | // "Wrap sublist. If value is "div" or "p", sibling criteria |
michael@0 | 6684 | // returns false; otherwise it returns true for an HTML element |
michael@0 | 6685 | // with local name value and no attributes, and false otherwise. |
michael@0 | 6686 | // New parent instructions return the result of running |
michael@0 | 6687 | // createElement(value) on the context object. Then fix disallowed |
michael@0 | 6688 | // ancestors of the result." |
michael@0 | 6689 | fixDisallowedAncestors(wrap(sublist, |
michael@0 | 6690 | ["div", "p"].indexOf(value) == - 1 |
michael@0 | 6691 | ? function(node) { return isHtmlElement(node, value) && !node.attributes.length } |
michael@0 | 6692 | : function() { return false }, |
michael@0 | 6693 | function() { return document.createElement(value) })); |
michael@0 | 6694 | } |
michael@0 | 6695 | |
michael@0 | 6696 | // "Return true." |
michael@0 | 6697 | return true; |
michael@0 | 6698 | }, indeterm: function() { |
michael@0 | 6699 | // "If the active range is null, return false." |
michael@0 | 6700 | if (!getActiveRange()) { |
michael@0 | 6701 | return false; |
michael@0 | 6702 | } |
michael@0 | 6703 | |
michael@0 | 6704 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 6705 | var newRange = blockExtend(getActiveRange()); |
michael@0 | 6706 | |
michael@0 | 6707 | // "Let node list be all visible editable nodes that are contained in |
michael@0 | 6708 | // new range and have no children." |
michael@0 | 6709 | var nodeList = getAllContainedNodes(newRange, function(node) { |
michael@0 | 6710 | return isVisible(node) |
michael@0 | 6711 | && isEditable(node) |
michael@0 | 6712 | && !node.hasChildNodes(); |
michael@0 | 6713 | }); |
michael@0 | 6714 | |
michael@0 | 6715 | // "If node list is empty, return false." |
michael@0 | 6716 | if (!nodeList.length) { |
michael@0 | 6717 | return false; |
michael@0 | 6718 | } |
michael@0 | 6719 | |
michael@0 | 6720 | // "Let type be null." |
michael@0 | 6721 | var type = null; |
michael@0 | 6722 | |
michael@0 | 6723 | // "For each node in node list:" |
michael@0 | 6724 | for (var i = 0; i < nodeList.length; i++) { |
michael@0 | 6725 | var node = nodeList[i]; |
michael@0 | 6726 | |
michael@0 | 6727 | // "While node's parent is editable and in the same editing host as |
michael@0 | 6728 | // node, and node is not an HTML element whose local name is a |
michael@0 | 6729 | // formattable block name, set node to its parent." |
michael@0 | 6730 | while (isEditable(node.parentNode) |
michael@0 | 6731 | && inSameEditingHost(node, node.parentNode) |
michael@0 | 6732 | && !isHtmlElement(node, formattableBlockNames)) { |
michael@0 | 6733 | node = node.parentNode; |
michael@0 | 6734 | } |
michael@0 | 6735 | |
michael@0 | 6736 | // "Let current type be the empty string." |
michael@0 | 6737 | var currentType = ""; |
michael@0 | 6738 | |
michael@0 | 6739 | // "If node is an editable HTML element whose local name is a |
michael@0 | 6740 | // formattable block name, and node is not the ancestor of a |
michael@0 | 6741 | // prohibited paragraph child, set current type to node's local |
michael@0 | 6742 | // name." |
michael@0 | 6743 | if (isEditable(node) |
michael@0 | 6744 | && isHtmlElement(node, formattableBlockNames) |
michael@0 | 6745 | && !getDescendants(node).some(isProhibitedParagraphChild)) { |
michael@0 | 6746 | currentType = node.tagName; |
michael@0 | 6747 | } |
michael@0 | 6748 | |
michael@0 | 6749 | // "If type is null, set type to current type." |
michael@0 | 6750 | if (type === null) { |
michael@0 | 6751 | type = currentType; |
michael@0 | 6752 | |
michael@0 | 6753 | // "Otherwise, if type does not equal current type, return true." |
michael@0 | 6754 | } else if (type != currentType) { |
michael@0 | 6755 | return true; |
michael@0 | 6756 | } |
michael@0 | 6757 | } |
michael@0 | 6758 | |
michael@0 | 6759 | // "Return false." |
michael@0 | 6760 | return false; |
michael@0 | 6761 | }, value: function() { |
michael@0 | 6762 | // "If the active range is null, return the empty string." |
michael@0 | 6763 | if (!getActiveRange()) { |
michael@0 | 6764 | return ""; |
michael@0 | 6765 | } |
michael@0 | 6766 | |
michael@0 | 6767 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 6768 | var newRange = blockExtend(getActiveRange()); |
michael@0 | 6769 | |
michael@0 | 6770 | // "Let node be the first visible editable node that is contained in |
michael@0 | 6771 | // new range and has no children. If there is no such node, return the |
michael@0 | 6772 | // empty string." |
michael@0 | 6773 | var nodes = getAllContainedNodes(newRange, function(node) { |
michael@0 | 6774 | return isVisible(node) |
michael@0 | 6775 | && isEditable(node) |
michael@0 | 6776 | && !node.hasChildNodes(); |
michael@0 | 6777 | }); |
michael@0 | 6778 | if (!nodes.length) { |
michael@0 | 6779 | return ""; |
michael@0 | 6780 | } |
michael@0 | 6781 | var node = nodes[0]; |
michael@0 | 6782 | |
michael@0 | 6783 | // "While node's parent is editable and in the same editing host as |
michael@0 | 6784 | // node, and node is not an HTML element whose local name is a |
michael@0 | 6785 | // formattable block name, set node to its parent." |
michael@0 | 6786 | while (isEditable(node.parentNode) |
michael@0 | 6787 | && inSameEditingHost(node, node.parentNode) |
michael@0 | 6788 | && !isHtmlElement(node, formattableBlockNames)) { |
michael@0 | 6789 | node = node.parentNode; |
michael@0 | 6790 | } |
michael@0 | 6791 | |
michael@0 | 6792 | // "If node is an editable HTML element whose local name is a |
michael@0 | 6793 | // formattable block name, and node is not the ancestor of a prohibited |
michael@0 | 6794 | // paragraph child, return node's local name, converted to ASCII |
michael@0 | 6795 | // lowercase." |
michael@0 | 6796 | if (isEditable(node) |
michael@0 | 6797 | && isHtmlElement(node, formattableBlockNames) |
michael@0 | 6798 | && !getDescendants(node).some(isProhibitedParagraphChild)) { |
michael@0 | 6799 | return node.tagName.toLowerCase(); |
michael@0 | 6800 | } |
michael@0 | 6801 | |
michael@0 | 6802 | // "Return the empty string." |
michael@0 | 6803 | return ""; |
michael@0 | 6804 | } |
michael@0 | 6805 | }; |
michael@0 | 6806 | |
michael@0 | 6807 | //@} |
michael@0 | 6808 | ///// The forwardDelete command ///// |
michael@0 | 6809 | //@{ |
michael@0 | 6810 | commands.forwarddelete = { |
michael@0 | 6811 | preservesOverrides: true, |
michael@0 | 6812 | action: function() { |
michael@0 | 6813 | // "If the active range is not collapsed, delete the selection and |
michael@0 | 6814 | // return true." |
michael@0 | 6815 | if (!getActiveRange().collapsed) { |
michael@0 | 6816 | deleteSelection(); |
michael@0 | 6817 | return true; |
michael@0 | 6818 | } |
michael@0 | 6819 | |
michael@0 | 6820 | // "Canonicalize whitespace at the active range's start." |
michael@0 | 6821 | canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset); |
michael@0 | 6822 | |
michael@0 | 6823 | // "Let node and offset be the active range's start node and offset." |
michael@0 | 6824 | var node = getActiveRange().startContainer; |
michael@0 | 6825 | var offset = getActiveRange().startOffset; |
michael@0 | 6826 | |
michael@0 | 6827 | // "Repeat the following steps:" |
michael@0 | 6828 | while (true) { |
michael@0 | 6829 | // "If offset is the length of node and node's nextSibling is an |
michael@0 | 6830 | // editable invisible node, remove node's nextSibling from its |
michael@0 | 6831 | // parent." |
michael@0 | 6832 | if (offset == getNodeLength(node) |
michael@0 | 6833 | && isEditable(node.nextSibling) |
michael@0 | 6834 | && isInvisible(node.nextSibling)) { |
michael@0 | 6835 | node.parentNode.removeChild(node.nextSibling); |
michael@0 | 6836 | |
michael@0 | 6837 | // "Otherwise, if node has a child with index offset and that child |
michael@0 | 6838 | // is an editable invisible node, remove that child from node." |
michael@0 | 6839 | } else if (offset < node.childNodes.length |
michael@0 | 6840 | && isEditable(node.childNodes[offset]) |
michael@0 | 6841 | && isInvisible(node.childNodes[offset])) { |
michael@0 | 6842 | node.removeChild(node.childNodes[offset]); |
michael@0 | 6843 | |
michael@0 | 6844 | // "Otherwise, if offset is the length of node and node is an |
michael@0 | 6845 | // inline node, or if node is invisible, set offset to one plus the |
michael@0 | 6846 | // index of node, then set node to its parent." |
michael@0 | 6847 | } else if ((offset == getNodeLength(node) |
michael@0 | 6848 | && isInlineNode(node)) |
michael@0 | 6849 | || isInvisible(node)) { |
michael@0 | 6850 | offset = 1 + getNodeIndex(node); |
michael@0 | 6851 | node = node.parentNode; |
michael@0 | 6852 | |
michael@0 | 6853 | // "Otherwise, if node has a child with index offset and that child |
michael@0 | 6854 | // is neither a block node nor a br nor an img nor a collapsed |
michael@0 | 6855 | // block prop, set node to that child, then set offset to zero." |
michael@0 | 6856 | } else if (offset < node.childNodes.length |
michael@0 | 6857 | && !isBlockNode(node.childNodes[offset]) |
michael@0 | 6858 | && !isHtmlElement(node.childNodes[offset], ["br", "img"]) |
michael@0 | 6859 | && !isCollapsedBlockProp(node.childNodes[offset])) { |
michael@0 | 6860 | node = node.childNodes[offset]; |
michael@0 | 6861 | offset = 0; |
michael@0 | 6862 | |
michael@0 | 6863 | // "Otherwise, break from this loop." |
michael@0 | 6864 | } else { |
michael@0 | 6865 | break; |
michael@0 | 6866 | } |
michael@0 | 6867 | } |
michael@0 | 6868 | |
michael@0 | 6869 | // "If node is a Text node and offset is not node's length:" |
michael@0 | 6870 | if (node.nodeType == Node.TEXT_NODE |
michael@0 | 6871 | && offset != getNodeLength(node)) { |
michael@0 | 6872 | // "Let end offset be offset plus one." |
michael@0 | 6873 | var endOffset = offset + 1; |
michael@0 | 6874 | |
michael@0 | 6875 | // "While end offset is not node's length and the end offsetth |
michael@0 | 6876 | // element of node's data has general category M when interpreted |
michael@0 | 6877 | // as a Unicode code point, add one to end offset." |
michael@0 | 6878 | // |
michael@0 | 6879 | // TODO: Not even going to try handling anything beyond the most |
michael@0 | 6880 | // basic combining marks, since I couldn't find a good list. I |
michael@0 | 6881 | // special-case a few Hebrew diacritics too to test basic coverage |
michael@0 | 6882 | // of non-Latin stuff. |
michael@0 | 6883 | while (endOffset != node.length |
michael@0 | 6884 | && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) { |
michael@0 | 6885 | endOffset++; |
michael@0 | 6886 | } |
michael@0 | 6887 | |
michael@0 | 6888 | // "Call collapse(node, offset) on the context object's Selection." |
michael@0 | 6889 | getSelection().collapse(node, offset); |
michael@0 | 6890 | getActiveRange().setStart(node, offset); |
michael@0 | 6891 | |
michael@0 | 6892 | // "Call extend(node, end offset) on the context object's |
michael@0 | 6893 | // Selection." |
michael@0 | 6894 | getSelection().extend(node, endOffset); |
michael@0 | 6895 | getActiveRange().setEnd(node, endOffset); |
michael@0 | 6896 | |
michael@0 | 6897 | // "Delete the selection." |
michael@0 | 6898 | deleteSelection(); |
michael@0 | 6899 | |
michael@0 | 6900 | // "Return true." |
michael@0 | 6901 | return true; |
michael@0 | 6902 | } |
michael@0 | 6903 | |
michael@0 | 6904 | // "If node is an inline node, return true." |
michael@0 | 6905 | if (isInlineNode(node)) { |
michael@0 | 6906 | return true; |
michael@0 | 6907 | } |
michael@0 | 6908 | |
michael@0 | 6909 | // "If node has a child with index offset and that child is a br or hr |
michael@0 | 6910 | // or img, but is not a collapsed block prop:" |
michael@0 | 6911 | if (offset < node.childNodes.length |
michael@0 | 6912 | && isHtmlElement(node.childNodes[offset], ["br", "hr", "img"]) |
michael@0 | 6913 | && !isCollapsedBlockProp(node.childNodes[offset])) { |
michael@0 | 6914 | // "Call collapse(node, offset) on the context object's Selection." |
michael@0 | 6915 | getSelection().collapse(node, offset); |
michael@0 | 6916 | getActiveRange().setStart(node, offset); |
michael@0 | 6917 | |
michael@0 | 6918 | // "Call extend(node, offset + 1) on the context object's |
michael@0 | 6919 | // Selection." |
michael@0 | 6920 | getSelection().extend(node, offset + 1); |
michael@0 | 6921 | getActiveRange().setEnd(node, offset + 1); |
michael@0 | 6922 | |
michael@0 | 6923 | // "Delete the selection." |
michael@0 | 6924 | deleteSelection(); |
michael@0 | 6925 | |
michael@0 | 6926 | // "Return true." |
michael@0 | 6927 | return true; |
michael@0 | 6928 | } |
michael@0 | 6929 | |
michael@0 | 6930 | // "Let end node equal node and let end offset equal offset." |
michael@0 | 6931 | var endNode = node; |
michael@0 | 6932 | var endOffset = offset; |
michael@0 | 6933 | |
michael@0 | 6934 | // "If end node has a child with index end offset, and that child is a |
michael@0 | 6935 | // collapsed block prop, add one to end offset." |
michael@0 | 6936 | if (endOffset < endNode.childNodes.length |
michael@0 | 6937 | && isCollapsedBlockProp(endNode.childNodes[endOffset])) { |
michael@0 | 6938 | endOffset++; |
michael@0 | 6939 | } |
michael@0 | 6940 | |
michael@0 | 6941 | // "Repeat the following steps:" |
michael@0 | 6942 | while (true) { |
michael@0 | 6943 | // "If end offset is the length of end node, set end offset to one |
michael@0 | 6944 | // plus the index of end node and then set end node to its parent." |
michael@0 | 6945 | if (endOffset == getNodeLength(endNode)) { |
michael@0 | 6946 | endOffset = 1 + getNodeIndex(endNode); |
michael@0 | 6947 | endNode = endNode.parentNode; |
michael@0 | 6948 | |
michael@0 | 6949 | // "Otherwise, if end node has a an editable invisible child with |
michael@0 | 6950 | // index end offset, remove it from end node." |
michael@0 | 6951 | } else if (endOffset < endNode.childNodes.length |
michael@0 | 6952 | && isEditable(endNode.childNodes[endOffset]) |
michael@0 | 6953 | && isInvisible(endNode.childNodes[endOffset])) { |
michael@0 | 6954 | endNode.removeChild(endNode.childNodes[endOffset]); |
michael@0 | 6955 | |
michael@0 | 6956 | // "Otherwise, break from this loop." |
michael@0 | 6957 | } else { |
michael@0 | 6958 | break; |
michael@0 | 6959 | } |
michael@0 | 6960 | } |
michael@0 | 6961 | |
michael@0 | 6962 | // "If the child of end node with index end offset minus one is a |
michael@0 | 6963 | // table, return true." |
michael@0 | 6964 | if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) { |
michael@0 | 6965 | return true; |
michael@0 | 6966 | } |
michael@0 | 6967 | |
michael@0 | 6968 | // "If the child of end node with index end offset is a table:" |
michael@0 | 6969 | if (isHtmlElement(endNode.childNodes[endOffset], "table")) { |
michael@0 | 6970 | // "Call collapse(end node, end offset) on the context object's |
michael@0 | 6971 | // Selection." |
michael@0 | 6972 | getSelection().collapse(endNode, endOffset); |
michael@0 | 6973 | getActiveRange().setStart(endNode, endOffset); |
michael@0 | 6974 | |
michael@0 | 6975 | // "Call extend(end node, end offset + 1) on the context object's |
michael@0 | 6976 | // Selection." |
michael@0 | 6977 | getSelection().extend(endNode, endOffset + 1); |
michael@0 | 6978 | getActiveRange().setEnd(endNode, endOffset + 1); |
michael@0 | 6979 | |
michael@0 | 6980 | // "Return true." |
michael@0 | 6981 | return true; |
michael@0 | 6982 | } |
michael@0 | 6983 | |
michael@0 | 6984 | // "If offset is the length of node, and the child of end node with |
michael@0 | 6985 | // index end offset is an hr or br:" |
michael@0 | 6986 | if (offset == getNodeLength(node) |
michael@0 | 6987 | && isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) { |
michael@0 | 6988 | // "Call collapse(end node, end offset) on the context object's |
michael@0 | 6989 | // Selection." |
michael@0 | 6990 | getSelection().collapse(endNode, endOffset); |
michael@0 | 6991 | getActiveRange().setStart(endNode, endOffset); |
michael@0 | 6992 | |
michael@0 | 6993 | // "Call extend(end node, end offset + 1) on the context object's |
michael@0 | 6994 | // Selection." |
michael@0 | 6995 | getSelection().extend(endNode, endOffset + 1); |
michael@0 | 6996 | getActiveRange().setEnd(endNode, endOffset + 1); |
michael@0 | 6997 | |
michael@0 | 6998 | // "Delete the selection." |
michael@0 | 6999 | deleteSelection(); |
michael@0 | 7000 | |
michael@0 | 7001 | // "Call collapse(node, offset) on the Selection." |
michael@0 | 7002 | getSelection().collapse(node, offset); |
michael@0 | 7003 | getActiveRange().setStart(node, offset); |
michael@0 | 7004 | getActiveRange().collapse(true); |
michael@0 | 7005 | |
michael@0 | 7006 | // "Return true." |
michael@0 | 7007 | return true; |
michael@0 | 7008 | } |
michael@0 | 7009 | |
michael@0 | 7010 | // "While end node has a child with index end offset:" |
michael@0 | 7011 | while (endOffset < endNode.childNodes.length) { |
michael@0 | 7012 | // "If end node's child with index end offset is editable and |
michael@0 | 7013 | // invisible, remove it from end node." |
michael@0 | 7014 | if (isEditable(endNode.childNodes[endOffset]) |
michael@0 | 7015 | && isInvisible(endNode.childNodes[endOffset])) { |
michael@0 | 7016 | endNode.removeChild(endNode.childNodes[endOffset]); |
michael@0 | 7017 | |
michael@0 | 7018 | // "Otherwise, set end node to its child with index end offset and |
michael@0 | 7019 | // set end offset to zero." |
michael@0 | 7020 | } else { |
michael@0 | 7021 | endNode = endNode.childNodes[endOffset]; |
michael@0 | 7022 | endOffset = 0; |
michael@0 | 7023 | } |
michael@0 | 7024 | } |
michael@0 | 7025 | |
michael@0 | 7026 | // "Call collapse(node, offset) on the context object's Selection." |
michael@0 | 7027 | getSelection().collapse(node, offset); |
michael@0 | 7028 | getActiveRange().setStart(node, offset); |
michael@0 | 7029 | |
michael@0 | 7030 | // "Call extend(end node, end offset) on the context object's |
michael@0 | 7031 | // Selection." |
michael@0 | 7032 | getSelection().extend(endNode, endOffset); |
michael@0 | 7033 | getActiveRange().setEnd(endNode, endOffset); |
michael@0 | 7034 | |
michael@0 | 7035 | // "Delete the selection." |
michael@0 | 7036 | deleteSelection(); |
michael@0 | 7037 | |
michael@0 | 7038 | // "Return true." |
michael@0 | 7039 | return true; |
michael@0 | 7040 | } |
michael@0 | 7041 | }; |
michael@0 | 7042 | |
michael@0 | 7043 | //@} |
michael@0 | 7044 | ///// The indent command ///// |
michael@0 | 7045 | //@{ |
michael@0 | 7046 | commands.indent = { |
michael@0 | 7047 | preservesOverrides: true, |
michael@0 | 7048 | action: function() { |
michael@0 | 7049 | // "Let items be a list of all lis that are ancestor containers of the |
michael@0 | 7050 | // active range's start and/or end node." |
michael@0 | 7051 | // |
michael@0 | 7052 | // Has to be in tree order, remember! |
michael@0 | 7053 | var items = []; |
michael@0 | 7054 | for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { |
michael@0 | 7055 | if (isHtmlElement(node, "LI")) { |
michael@0 | 7056 | items.unshift(node); |
michael@0 | 7057 | } |
michael@0 | 7058 | } |
michael@0 | 7059 | for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { |
michael@0 | 7060 | if (isHtmlElement(node, "LI")) { |
michael@0 | 7061 | items.unshift(node); |
michael@0 | 7062 | } |
michael@0 | 7063 | } |
michael@0 | 7064 | for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) { |
michael@0 | 7065 | if (isHtmlElement(node, "LI")) { |
michael@0 | 7066 | items.unshift(node); |
michael@0 | 7067 | } |
michael@0 | 7068 | } |
michael@0 | 7069 | |
michael@0 | 7070 | // "For each item in items, normalize sublists of item." |
michael@0 | 7071 | for (var i = 0; i < items.length; i++) { |
michael@0 | 7072 | normalizeSublists(items[i]); |
michael@0 | 7073 | } |
michael@0 | 7074 | |
michael@0 | 7075 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 7076 | var newRange = blockExtend(getActiveRange()); |
michael@0 | 7077 | |
michael@0 | 7078 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 7079 | var nodeList = []; |
michael@0 | 7080 | |
michael@0 | 7081 | // "For each node node contained in new range, if node is editable and |
michael@0 | 7082 | // is an allowed child of "div" or "ol" and if the last member of node |
michael@0 | 7083 | // list (if any) is not an ancestor of node, append node to node list." |
michael@0 | 7084 | nodeList = getContainedNodes(newRange, function(node) { |
michael@0 | 7085 | return isEditable(node) |
michael@0 | 7086 | && (isAllowedChild(node, "div") |
michael@0 | 7087 | || isAllowedChild(node, "ol")); |
michael@0 | 7088 | }); |
michael@0 | 7089 | |
michael@0 | 7090 | // "If the first visible member of node list is an li whose parent is |
michael@0 | 7091 | // an ol or ul:" |
michael@0 | 7092 | if (isHtmlElement(nodeList.filter(isVisible)[0], "li") |
michael@0 | 7093 | && isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) { |
michael@0 | 7094 | // "Let sibling be node list's first visible member's |
michael@0 | 7095 | // previousSibling." |
michael@0 | 7096 | var sibling = nodeList.filter(isVisible)[0].previousSibling; |
michael@0 | 7097 | |
michael@0 | 7098 | // "While sibling is invisible, set sibling to its |
michael@0 | 7099 | // previousSibling." |
michael@0 | 7100 | while (isInvisible(sibling)) { |
michael@0 | 7101 | sibling = sibling.previousSibling; |
michael@0 | 7102 | } |
michael@0 | 7103 | |
michael@0 | 7104 | // "If sibling is an li, normalize sublists of sibling." |
michael@0 | 7105 | if (isHtmlElement(sibling, "li")) { |
michael@0 | 7106 | normalizeSublists(sibling); |
michael@0 | 7107 | } |
michael@0 | 7108 | } |
michael@0 | 7109 | |
michael@0 | 7110 | // "While node list is not empty:" |
michael@0 | 7111 | while (nodeList.length) { |
michael@0 | 7112 | // "Let sublist be a list of nodes, initially empty." |
michael@0 | 7113 | var sublist = []; |
michael@0 | 7114 | |
michael@0 | 7115 | // "Remove the first member of node list and append it to sublist." |
michael@0 | 7116 | sublist.push(nodeList.shift()); |
michael@0 | 7117 | |
michael@0 | 7118 | // "While the first member of node list is the nextSibling of the |
michael@0 | 7119 | // last member of sublist, remove the first member of node list and |
michael@0 | 7120 | // append it to sublist." |
michael@0 | 7121 | while (nodeList.length |
michael@0 | 7122 | && nodeList[0] == sublist[sublist.length - 1].nextSibling) { |
michael@0 | 7123 | sublist.push(nodeList.shift()); |
michael@0 | 7124 | } |
michael@0 | 7125 | |
michael@0 | 7126 | // "Indent sublist." |
michael@0 | 7127 | indentNodes(sublist); |
michael@0 | 7128 | } |
michael@0 | 7129 | |
michael@0 | 7130 | // "Return true." |
michael@0 | 7131 | return true; |
michael@0 | 7132 | } |
michael@0 | 7133 | }; |
michael@0 | 7134 | |
michael@0 | 7135 | //@} |
michael@0 | 7136 | ///// The insertHorizontalRule command ///// |
michael@0 | 7137 | //@{ |
michael@0 | 7138 | commands.inserthorizontalrule = { |
michael@0 | 7139 | preservesOverrides: true, |
michael@0 | 7140 | action: function() { |
michael@0 | 7141 | // "Let start node, start offset, end node, and end offset be the |
michael@0 | 7142 | // active range's start and end nodes and offsets." |
michael@0 | 7143 | var startNode = getActiveRange().startContainer; |
michael@0 | 7144 | var startOffset = getActiveRange().startOffset; |
michael@0 | 7145 | var endNode = getActiveRange().endContainer; |
michael@0 | 7146 | var endOffset = getActiveRange().endOffset; |
michael@0 | 7147 | |
michael@0 | 7148 | // "While start offset is 0 and start node's parent is not null, set |
michael@0 | 7149 | // start offset to start node's index, then set start node to its |
michael@0 | 7150 | // parent." |
michael@0 | 7151 | while (startOffset == 0 |
michael@0 | 7152 | && startNode.parentNode) { |
michael@0 | 7153 | startOffset = getNodeIndex(startNode); |
michael@0 | 7154 | startNode = startNode.parentNode; |
michael@0 | 7155 | } |
michael@0 | 7156 | |
michael@0 | 7157 | // "While end offset is end node's length, and end node's parent is not |
michael@0 | 7158 | // null, set end offset to one plus end node's index, then set end node |
michael@0 | 7159 | // to its parent." |
michael@0 | 7160 | while (endOffset == getNodeLength(endNode) |
michael@0 | 7161 | && endNode.parentNode) { |
michael@0 | 7162 | endOffset = 1 + getNodeIndex(endNode); |
michael@0 | 7163 | endNode = endNode.parentNode; |
michael@0 | 7164 | } |
michael@0 | 7165 | |
michael@0 | 7166 | // "Call collapse(start node, start offset) on the context object's |
michael@0 | 7167 | // Selection." |
michael@0 | 7168 | getSelection().collapse(startNode, startOffset); |
michael@0 | 7169 | getActiveRange().setStart(startNode, startOffset); |
michael@0 | 7170 | |
michael@0 | 7171 | // "Call extend(end node, end offset) on the context object's |
michael@0 | 7172 | // Selection." |
michael@0 | 7173 | getSelection().extend(endNode, endOffset); |
michael@0 | 7174 | getActiveRange().setEnd(endNode, endOffset); |
michael@0 | 7175 | |
michael@0 | 7176 | // "Delete the selection, with block merging false." |
michael@0 | 7177 | deleteSelection({blockMerging: false}); |
michael@0 | 7178 | |
michael@0 | 7179 | // "If the active range's start node is neither editable nor an editing |
michael@0 | 7180 | // host, return true." |
michael@0 | 7181 | if (!isEditable(getActiveRange().startContainer) |
michael@0 | 7182 | && !isEditingHost(getActiveRange().startContainer)) { |
michael@0 | 7183 | return true; |
michael@0 | 7184 | } |
michael@0 | 7185 | |
michael@0 | 7186 | // "If the active range's start node is a Text node and its start |
michael@0 | 7187 | // offset is zero, call collapse() on the context object's Selection, |
michael@0 | 7188 | // with first argument the active range's start node's parent and |
michael@0 | 7189 | // second argument the active range's start node's index." |
michael@0 | 7190 | if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
michael@0 | 7191 | && getActiveRange().startOffset == 0) { |
michael@0 | 7192 | var newNode = getActiveRange().startContainer.parentNode; |
michael@0 | 7193 | var newOffset = getNodeIndex(getActiveRange().startContainer); |
michael@0 | 7194 | getSelection().collapse(newNode, newOffset); |
michael@0 | 7195 | getActiveRange().setStart(newNode, newOffset); |
michael@0 | 7196 | getActiveRange().collapse(true); |
michael@0 | 7197 | } |
michael@0 | 7198 | |
michael@0 | 7199 | // "If the active range's start node is a Text node and its start |
michael@0 | 7200 | // offset is the length of its start node, call collapse() on the |
michael@0 | 7201 | // context object's Selection, with first argument the active range's |
michael@0 | 7202 | // start node's parent, and the second argument one plus the active |
michael@0 | 7203 | // range's start node's index." |
michael@0 | 7204 | if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
michael@0 | 7205 | && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { |
michael@0 | 7206 | var newNode = getActiveRange().startContainer.parentNode; |
michael@0 | 7207 | var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); |
michael@0 | 7208 | getSelection().collapse(newNode, newOffset); |
michael@0 | 7209 | getActiveRange().setStart(newNode, newOffset); |
michael@0 | 7210 | getActiveRange().collapse(true); |
michael@0 | 7211 | } |
michael@0 | 7212 | |
michael@0 | 7213 | // "Let hr be the result of calling createElement("hr") on the |
michael@0 | 7214 | // context object." |
michael@0 | 7215 | var hr = document.createElement("hr"); |
michael@0 | 7216 | |
michael@0 | 7217 | // "Run insertNode(hr) on the active range." |
michael@0 | 7218 | getActiveRange().insertNode(hr); |
michael@0 | 7219 | |
michael@0 | 7220 | // "Fix disallowed ancestors of hr." |
michael@0 | 7221 | fixDisallowedAncestors(hr); |
michael@0 | 7222 | |
michael@0 | 7223 | // "Run collapse() on the context object's Selection, with first |
michael@0 | 7224 | // argument hr's parent and the second argument equal to one plus hr's |
michael@0 | 7225 | // index." |
michael@0 | 7226 | getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr)); |
michael@0 | 7227 | getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr)); |
michael@0 | 7228 | getActiveRange().collapse(true); |
michael@0 | 7229 | |
michael@0 | 7230 | // "Return true." |
michael@0 | 7231 | return true; |
michael@0 | 7232 | } |
michael@0 | 7233 | }; |
michael@0 | 7234 | |
michael@0 | 7235 | //@} |
michael@0 | 7236 | ///// The insertHTML command ///// |
michael@0 | 7237 | //@{ |
michael@0 | 7238 | commands.inserthtml = { |
michael@0 | 7239 | preservesOverrides: true, |
michael@0 | 7240 | action: function(value) { |
michael@0 | 7241 | // "Delete the selection." |
michael@0 | 7242 | deleteSelection(); |
michael@0 | 7243 | |
michael@0 | 7244 | // "If the active range's start node is neither editable nor an editing |
michael@0 | 7245 | // host, return true." |
michael@0 | 7246 | if (!isEditable(getActiveRange().startContainer) |
michael@0 | 7247 | && !isEditingHost(getActiveRange().startContainer)) { |
michael@0 | 7248 | return true; |
michael@0 | 7249 | } |
michael@0 | 7250 | |
michael@0 | 7251 | // "Let frag be the result of calling createContextualFragment(value) |
michael@0 | 7252 | // on the active range." |
michael@0 | 7253 | var frag = getActiveRange().createContextualFragment(value); |
michael@0 | 7254 | |
michael@0 | 7255 | // "Let last child be the lastChild of frag." |
michael@0 | 7256 | var lastChild = frag.lastChild; |
michael@0 | 7257 | |
michael@0 | 7258 | // "If last child is null, return true." |
michael@0 | 7259 | if (!lastChild) { |
michael@0 | 7260 | return true; |
michael@0 | 7261 | } |
michael@0 | 7262 | |
michael@0 | 7263 | // "Let descendants be all descendants of frag." |
michael@0 | 7264 | var descendants = getDescendants(frag); |
michael@0 | 7265 | |
michael@0 | 7266 | // "If the active range's start node is a block node:" |
michael@0 | 7267 | if (isBlockNode(getActiveRange().startContainer)) { |
michael@0 | 7268 | // "Let collapsed block props be all editable collapsed block prop |
michael@0 | 7269 | // children of the active range's start node that have index |
michael@0 | 7270 | // greater than or equal to the active range's start offset." |
michael@0 | 7271 | // |
michael@0 | 7272 | // "For each node in collapsed block props, remove node from its |
michael@0 | 7273 | // parent." |
michael@0 | 7274 | [].filter.call(getActiveRange().startContainer.childNodes, function(node) { |
michael@0 | 7275 | return isEditable(node) |
michael@0 | 7276 | && isCollapsedBlockProp(node) |
michael@0 | 7277 | && getNodeIndex(node) >= getActiveRange().startOffset; |
michael@0 | 7278 | }).forEach(function(node) { |
michael@0 | 7279 | node.parentNode.removeChild(node); |
michael@0 | 7280 | }); |
michael@0 | 7281 | } |
michael@0 | 7282 | |
michael@0 | 7283 | // "Call insertNode(frag) on the active range." |
michael@0 | 7284 | getActiveRange().insertNode(frag); |
michael@0 | 7285 | |
michael@0 | 7286 | // "If the active range's start node is a block node with no visible |
michael@0 | 7287 | // children, call createElement("br") on the context object and append |
michael@0 | 7288 | // the result as the last child of the active range's start node." |
michael@0 | 7289 | if (isBlockNode(getActiveRange().startContainer) |
michael@0 | 7290 | && ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) { |
michael@0 | 7291 | getActiveRange().startContainer.appendChild(document.createElement("br")); |
michael@0 | 7292 | } |
michael@0 | 7293 | |
michael@0 | 7294 | // "Call collapse() on the context object's Selection, with last |
michael@0 | 7295 | // child's parent as the first argument and one plus its index as the |
michael@0 | 7296 | // second." |
michael@0 | 7297 | getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild)); |
michael@0 | 7298 | getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild)); |
michael@0 | 7299 | |
michael@0 | 7300 | // "Fix disallowed ancestors of each member of descendants." |
michael@0 | 7301 | for (var i = 0; i < descendants.length; i++) { |
michael@0 | 7302 | fixDisallowedAncestors(descendants[i]); |
michael@0 | 7303 | } |
michael@0 | 7304 | |
michael@0 | 7305 | // "Return true." |
michael@0 | 7306 | return true; |
michael@0 | 7307 | } |
michael@0 | 7308 | }; |
michael@0 | 7309 | |
michael@0 | 7310 | //@} |
michael@0 | 7311 | ///// The insertImage command ///// |
michael@0 | 7312 | //@{ |
michael@0 | 7313 | commands.insertimage = { |
michael@0 | 7314 | preservesOverrides: true, |
michael@0 | 7315 | action: function(value) { |
michael@0 | 7316 | // "If value is the empty string, return false." |
michael@0 | 7317 | if (value === "") { |
michael@0 | 7318 | return false; |
michael@0 | 7319 | } |
michael@0 | 7320 | |
michael@0 | 7321 | // "Delete the selection, with strip wrappers false." |
michael@0 | 7322 | deleteSelection({stripWrappers: false}); |
michael@0 | 7323 | |
michael@0 | 7324 | // "Let range be the active range." |
michael@0 | 7325 | var range = getActiveRange(); |
michael@0 | 7326 | |
michael@0 | 7327 | // "If the active range's start node is neither editable nor an editing |
michael@0 | 7328 | // host, return true." |
michael@0 | 7329 | if (!isEditable(getActiveRange().startContainer) |
michael@0 | 7330 | && !isEditingHost(getActiveRange().startContainer)) { |
michael@0 | 7331 | return true; |
michael@0 | 7332 | } |
michael@0 | 7333 | |
michael@0 | 7334 | // "If range's start node is a block node whose sole child is a br, and |
michael@0 | 7335 | // its start offset is 0, remove its start node's child from it." |
michael@0 | 7336 | if (isBlockNode(range.startContainer) |
michael@0 | 7337 | && range.startContainer.childNodes.length == 1 |
michael@0 | 7338 | && isHtmlElement(range.startContainer.firstChild, "br") |
michael@0 | 7339 | && range.startOffset == 0) { |
michael@0 | 7340 | range.startContainer.removeChild(range.startContainer.firstChild); |
michael@0 | 7341 | } |
michael@0 | 7342 | |
michael@0 | 7343 | // "Let img be the result of calling createElement("img") on the |
michael@0 | 7344 | // context object." |
michael@0 | 7345 | var img = document.createElement("img"); |
michael@0 | 7346 | |
michael@0 | 7347 | // "Run setAttribute("src", value) on img." |
michael@0 | 7348 | img.setAttribute("src", value); |
michael@0 | 7349 | |
michael@0 | 7350 | // "Run insertNode(img) on the range." |
michael@0 | 7351 | range.insertNode(img); |
michael@0 | 7352 | |
michael@0 | 7353 | // "Run collapse() on the Selection, with first argument equal to the |
michael@0 | 7354 | // parent of img and the second argument equal to one plus the index of |
michael@0 | 7355 | // img." |
michael@0 | 7356 | // |
michael@0 | 7357 | // Not everyone actually supports collapse(), so we do it manually |
michael@0 | 7358 | // instead. Also, we need to modify the actual range we're given as |
michael@0 | 7359 | // well, for the sake of autoimplementation.html's range-filling-in. |
michael@0 | 7360 | range.setStart(img.parentNode, 1 + getNodeIndex(img)); |
michael@0 | 7361 | range.setEnd(img.parentNode, 1 + getNodeIndex(img)); |
michael@0 | 7362 | getSelection().removeAllRanges(); |
michael@0 | 7363 | getSelection().addRange(range); |
michael@0 | 7364 | |
michael@0 | 7365 | // IE adds width and height attributes for some reason, so remove those |
michael@0 | 7366 | // to actually do what the spec says. |
michael@0 | 7367 | img.removeAttribute("width"); |
michael@0 | 7368 | img.removeAttribute("height"); |
michael@0 | 7369 | |
michael@0 | 7370 | // "Return true." |
michael@0 | 7371 | return true; |
michael@0 | 7372 | } |
michael@0 | 7373 | }; |
michael@0 | 7374 | |
michael@0 | 7375 | //@} |
michael@0 | 7376 | ///// The insertLineBreak command ///// |
michael@0 | 7377 | //@{ |
michael@0 | 7378 | commands.insertlinebreak = { |
michael@0 | 7379 | preservesOverrides: true, |
michael@0 | 7380 | action: function(value) { |
michael@0 | 7381 | // "Delete the selection, with strip wrappers false." |
michael@0 | 7382 | deleteSelection({stripWrappers: false}); |
michael@0 | 7383 | |
michael@0 | 7384 | // "If the active range's start node is neither editable nor an editing |
michael@0 | 7385 | // host, return true." |
michael@0 | 7386 | if (!isEditable(getActiveRange().startContainer) |
michael@0 | 7387 | && !isEditingHost(getActiveRange().startContainer)) { |
michael@0 | 7388 | return true; |
michael@0 | 7389 | } |
michael@0 | 7390 | |
michael@0 | 7391 | // "If the active range's start node is an Element, and "br" is not an |
michael@0 | 7392 | // allowed child of it, return true." |
michael@0 | 7393 | if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE |
michael@0 | 7394 | && !isAllowedChild("br", getActiveRange().startContainer)) { |
michael@0 | 7395 | return true; |
michael@0 | 7396 | } |
michael@0 | 7397 | |
michael@0 | 7398 | // "If the active range's start node is not an Element, and "br" is not |
michael@0 | 7399 | // an allowed child of the active range's start node's parent, return |
michael@0 | 7400 | // true." |
michael@0 | 7401 | if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE |
michael@0 | 7402 | && !isAllowedChild("br", getActiveRange().startContainer.parentNode)) { |
michael@0 | 7403 | return true; |
michael@0 | 7404 | } |
michael@0 | 7405 | |
michael@0 | 7406 | // "If the active range's start node is a Text node and its start |
michael@0 | 7407 | // offset is zero, call collapse() on the context object's Selection, |
michael@0 | 7408 | // with first argument equal to the active range's start node's parent |
michael@0 | 7409 | // and second argument equal to the active range's start node's index." |
michael@0 | 7410 | if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
michael@0 | 7411 | && getActiveRange().startOffset == 0) { |
michael@0 | 7412 | var newNode = getActiveRange().startContainer.parentNode; |
michael@0 | 7413 | var newOffset = getNodeIndex(getActiveRange().startContainer); |
michael@0 | 7414 | getSelection().collapse(newNode, newOffset); |
michael@0 | 7415 | getActiveRange().setStart(newNode, newOffset); |
michael@0 | 7416 | getActiveRange().setEnd(newNode, newOffset); |
michael@0 | 7417 | } |
michael@0 | 7418 | |
michael@0 | 7419 | // "If the active range's start node is a Text node and its start |
michael@0 | 7420 | // offset is the length of its start node, call collapse() on the |
michael@0 | 7421 | // context object's Selection, with first argument equal to the active |
michael@0 | 7422 | // range's start node's parent and second argument equal to one plus |
michael@0 | 7423 | // the active range's start node's index." |
michael@0 | 7424 | if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE |
michael@0 | 7425 | && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { |
michael@0 | 7426 | var newNode = getActiveRange().startContainer.parentNode; |
michael@0 | 7427 | var newOffset = 1 + getNodeIndex(getActiveRange().startContainer); |
michael@0 | 7428 | getSelection().collapse(newNode, newOffset); |
michael@0 | 7429 | getActiveRange().setStart(newNode, newOffset); |
michael@0 | 7430 | getActiveRange().setEnd(newNode, newOffset); |
michael@0 | 7431 | } |
michael@0 | 7432 | |
michael@0 | 7433 | // "Let br be the result of calling createElement("br") on the context |
michael@0 | 7434 | // object." |
michael@0 | 7435 | var br = document.createElement("br"); |
michael@0 | 7436 | |
michael@0 | 7437 | // "Call insertNode(br) on the active range." |
michael@0 | 7438 | getActiveRange().insertNode(br); |
michael@0 | 7439 | |
michael@0 | 7440 | // "Call collapse() on the context object's Selection, with br's parent |
michael@0 | 7441 | // as the first argument and one plus br's index as the second |
michael@0 | 7442 | // argument." |
michael@0 | 7443 | getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); |
michael@0 | 7444 | getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); |
michael@0 | 7445 | getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); |
michael@0 | 7446 | |
michael@0 | 7447 | // "If br is a collapsed line break, call createElement("br") on the |
michael@0 | 7448 | // context object and let extra br be the result, then call |
michael@0 | 7449 | // insertNode(extra br) on the active range." |
michael@0 | 7450 | if (isCollapsedLineBreak(br)) { |
michael@0 | 7451 | getActiveRange().insertNode(document.createElement("br")); |
michael@0 | 7452 | |
michael@0 | 7453 | // Compensate for nonstandard implementations of insertNode |
michael@0 | 7454 | getSelection().collapse(br.parentNode, 1 + getNodeIndex(br)); |
michael@0 | 7455 | getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br)); |
michael@0 | 7456 | getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br)); |
michael@0 | 7457 | } |
michael@0 | 7458 | |
michael@0 | 7459 | // "Return true." |
michael@0 | 7460 | return true; |
michael@0 | 7461 | } |
michael@0 | 7462 | }; |
michael@0 | 7463 | |
michael@0 | 7464 | //@} |
michael@0 | 7465 | ///// The insertOrderedList command ///// |
michael@0 | 7466 | //@{ |
michael@0 | 7467 | commands.insertorderedlist = { |
michael@0 | 7468 | preservesOverrides: true, |
michael@0 | 7469 | // "Toggle lists with tag name "ol", then return true." |
michael@0 | 7470 | action: function() { toggleLists("ol"); return true }, |
michael@0 | 7471 | // "True if the selection's list state is "mixed" or "mixed ol", false |
michael@0 | 7472 | // otherwise." |
michael@0 | 7473 | indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) }, |
michael@0 | 7474 | // "True if the selection's list state is "ol", false otherwise." |
michael@0 | 7475 | state: function() { return getSelectionListState() == "ol" }, |
michael@0 | 7476 | }; |
michael@0 | 7477 | |
michael@0 | 7478 | //@} |
michael@0 | 7479 | ///// The insertParagraph command ///// |
michael@0 | 7480 | //@{ |
michael@0 | 7481 | commands.insertparagraph = { |
michael@0 | 7482 | preservesOverrides: true, |
michael@0 | 7483 | action: function() { |
michael@0 | 7484 | // "Delete the selection." |
michael@0 | 7485 | deleteSelection(); |
michael@0 | 7486 | |
michael@0 | 7487 | // "If the active range's start node is neither editable nor an editing |
michael@0 | 7488 | // host, return true." |
michael@0 | 7489 | if (!isEditable(getActiveRange().startContainer) |
michael@0 | 7490 | && !isEditingHost(getActiveRange().startContainer)) { |
michael@0 | 7491 | return true; |
michael@0 | 7492 | } |
michael@0 | 7493 | |
michael@0 | 7494 | // "Let node and offset be the active range's start node and offset." |
michael@0 | 7495 | var node = getActiveRange().startContainer; |
michael@0 | 7496 | var offset = getActiveRange().startOffset; |
michael@0 | 7497 | |
michael@0 | 7498 | // "If node is a Text node, and offset is neither 0 nor the length of |
michael@0 | 7499 | // node, call splitText(offset) on node." |
michael@0 | 7500 | if (node.nodeType == Node.TEXT_NODE |
michael@0 | 7501 | && offset != 0 |
michael@0 | 7502 | && offset != getNodeLength(node)) { |
michael@0 | 7503 | node.splitText(offset); |
michael@0 | 7504 | } |
michael@0 | 7505 | |
michael@0 | 7506 | // "If node is a Text node and offset is its length, set offset to one |
michael@0 | 7507 | // plus the index of node, then set node to its parent." |
michael@0 | 7508 | if (node.nodeType == Node.TEXT_NODE |
michael@0 | 7509 | && offset == getNodeLength(node)) { |
michael@0 | 7510 | offset = 1 + getNodeIndex(node); |
michael@0 | 7511 | node = node.parentNode; |
michael@0 | 7512 | } |
michael@0 | 7513 | |
michael@0 | 7514 | // "If node is a Text or Comment node, set offset to the index of node, |
michael@0 | 7515 | // then set node to its parent." |
michael@0 | 7516 | if (node.nodeType == Node.TEXT_NODE |
michael@0 | 7517 | || node.nodeType == Node.COMMENT_NODE) { |
michael@0 | 7518 | offset = getNodeIndex(node); |
michael@0 | 7519 | node = node.parentNode; |
michael@0 | 7520 | } |
michael@0 | 7521 | |
michael@0 | 7522 | // "Call collapse(node, offset) on the context object's Selection." |
michael@0 | 7523 | getSelection().collapse(node, offset); |
michael@0 | 7524 | getActiveRange().setStart(node, offset); |
michael@0 | 7525 | getActiveRange().setEnd(node, offset); |
michael@0 | 7526 | |
michael@0 | 7527 | // "Let container equal node." |
michael@0 | 7528 | var container = node; |
michael@0 | 7529 | |
michael@0 | 7530 | // "While container is not a single-line container, and container's |
michael@0 | 7531 | // parent is editable and in the same editing host as node, set |
michael@0 | 7532 | // container to its parent." |
michael@0 | 7533 | while (!isSingleLineContainer(container) |
michael@0 | 7534 | && isEditable(container.parentNode) |
michael@0 | 7535 | && inSameEditingHost(node, container.parentNode)) { |
michael@0 | 7536 | container = container.parentNode; |
michael@0 | 7537 | } |
michael@0 | 7538 | |
michael@0 | 7539 | // "If container is an editable single-line container in the same |
michael@0 | 7540 | // editing host as node, and its local name is "p" or "div":" |
michael@0 | 7541 | if (isEditable(container) |
michael@0 | 7542 | && isSingleLineContainer(container) |
michael@0 | 7543 | && inSameEditingHost(node, container.parentNode) |
michael@0 | 7544 | && (container.tagName == "P" || container.tagName == "DIV")) { |
michael@0 | 7545 | // "Let outer container equal container." |
michael@0 | 7546 | var outerContainer = container; |
michael@0 | 7547 | |
michael@0 | 7548 | // "While outer container is not a dd or dt or li, and outer |
michael@0 | 7549 | // container's parent is editable, set outer container to its |
michael@0 | 7550 | // parent." |
michael@0 | 7551 | while (!isHtmlElement(outerContainer, ["dd", "dt", "li"]) |
michael@0 | 7552 | && isEditable(outerContainer.parentNode)) { |
michael@0 | 7553 | outerContainer = outerContainer.parentNode; |
michael@0 | 7554 | } |
michael@0 | 7555 | |
michael@0 | 7556 | // "If outer container is a dd or dt or li, set container to outer |
michael@0 | 7557 | // container." |
michael@0 | 7558 | if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) { |
michael@0 | 7559 | container = outerContainer; |
michael@0 | 7560 | } |
michael@0 | 7561 | } |
michael@0 | 7562 | |
michael@0 | 7563 | // "If container is not editable or not in the same editing host as |
michael@0 | 7564 | // node or is not a single-line container:" |
michael@0 | 7565 | if (!isEditable(container) |
michael@0 | 7566 | || !inSameEditingHost(container, node) |
michael@0 | 7567 | || !isSingleLineContainer(container)) { |
michael@0 | 7568 | // "Let tag be the default single-line container name." |
michael@0 | 7569 | var tag = defaultSingleLineContainerName; |
michael@0 | 7570 | |
michael@0 | 7571 | // "Block-extend the active range, and let new range be the |
michael@0 | 7572 | // result." |
michael@0 | 7573 | var newRange = blockExtend(getActiveRange()); |
michael@0 | 7574 | |
michael@0 | 7575 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 7576 | // |
michael@0 | 7577 | // "Append to node list the first node in tree order that is |
michael@0 | 7578 | // contained in new range and is an allowed child of "p", if any." |
michael@0 | 7579 | var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") }) |
michael@0 | 7580 | .slice(0, 1); |
michael@0 | 7581 | |
michael@0 | 7582 | // "If node list is empty:" |
michael@0 | 7583 | if (!nodeList.length) { |
michael@0 | 7584 | // "If tag is not an allowed child of the active range's start |
michael@0 | 7585 | // node, return true." |
michael@0 | 7586 | if (!isAllowedChild(tag, getActiveRange().startContainer)) { |
michael@0 | 7587 | return true; |
michael@0 | 7588 | } |
michael@0 | 7589 | |
michael@0 | 7590 | // "Set container to the result of calling createElement(tag) |
michael@0 | 7591 | // on the context object." |
michael@0 | 7592 | container = document.createElement(tag); |
michael@0 | 7593 | |
michael@0 | 7594 | // "Call insertNode(container) on the active range." |
michael@0 | 7595 | getActiveRange().insertNode(container); |
michael@0 | 7596 | |
michael@0 | 7597 | // "Call createElement("br") on the context object, and append |
michael@0 | 7598 | // the result as the last child of container." |
michael@0 | 7599 | container.appendChild(document.createElement("br")); |
michael@0 | 7600 | |
michael@0 | 7601 | // "Call collapse(container, 0) on the context object's |
michael@0 | 7602 | // Selection." |
michael@0 | 7603 | getSelection().collapse(container, 0); |
michael@0 | 7604 | getActiveRange().setStart(container, 0); |
michael@0 | 7605 | getActiveRange().setEnd(container, 0); |
michael@0 | 7606 | |
michael@0 | 7607 | // "Return true." |
michael@0 | 7608 | return true; |
michael@0 | 7609 | } |
michael@0 | 7610 | |
michael@0 | 7611 | // "While the nextSibling of the last member of node list is not |
michael@0 | 7612 | // null and is an allowed child of "p", append it to node list." |
michael@0 | 7613 | while (nodeList[nodeList.length - 1].nextSibling |
michael@0 | 7614 | && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) { |
michael@0 | 7615 | nodeList.push(nodeList[nodeList.length - 1].nextSibling); |
michael@0 | 7616 | } |
michael@0 | 7617 | |
michael@0 | 7618 | // "Wrap node list, with sibling criteria returning false and new |
michael@0 | 7619 | // parent instructions returning the result of calling |
michael@0 | 7620 | // createElement(tag) on the context object. Set container to the |
michael@0 | 7621 | // result." |
michael@0 | 7622 | container = wrap(nodeList, |
michael@0 | 7623 | function() { return false }, |
michael@0 | 7624 | function() { return document.createElement(tag) } |
michael@0 | 7625 | ); |
michael@0 | 7626 | } |
michael@0 | 7627 | |
michael@0 | 7628 | // "If container's local name is "address", "listing", or "pre":" |
michael@0 | 7629 | if (container.tagName == "ADDRESS" |
michael@0 | 7630 | || container.tagName == "LISTING" |
michael@0 | 7631 | || container.tagName == "PRE") { |
michael@0 | 7632 | // "Let br be the result of calling createElement("br") on the |
michael@0 | 7633 | // context object." |
michael@0 | 7634 | var br = document.createElement("br"); |
michael@0 | 7635 | |
michael@0 | 7636 | // "Call insertNode(br) on the active range." |
michael@0 | 7637 | getActiveRange().insertNode(br); |
michael@0 | 7638 | |
michael@0 | 7639 | // "Call collapse(node, offset + 1) on the context object's |
michael@0 | 7640 | // Selection." |
michael@0 | 7641 | getSelection().collapse(node, offset + 1); |
michael@0 | 7642 | getActiveRange().setStart(node, offset + 1); |
michael@0 | 7643 | getActiveRange().setEnd(node, offset + 1); |
michael@0 | 7644 | |
michael@0 | 7645 | // "If br is the last descendant of container, let br be the result |
michael@0 | 7646 | // of calling createElement("br") on the context object, then call |
michael@0 | 7647 | // insertNode(br) on the active range." |
michael@0 | 7648 | // |
michael@0 | 7649 | // Work around browser bugs: some browsers select the |
michael@0 | 7650 | // newly-inserted node, not per spec. |
michael@0 | 7651 | if (!isDescendant(nextNode(br), container)) { |
michael@0 | 7652 | getActiveRange().insertNode(document.createElement("br")); |
michael@0 | 7653 | getSelection().collapse(node, offset + 1); |
michael@0 | 7654 | getActiveRange().setEnd(node, offset + 1); |
michael@0 | 7655 | } |
michael@0 | 7656 | |
michael@0 | 7657 | // "Return true." |
michael@0 | 7658 | return true; |
michael@0 | 7659 | } |
michael@0 | 7660 | |
michael@0 | 7661 | // "If container's local name is "li", "dt", or "dd"; and either it has |
michael@0 | 7662 | // no children or it has a single child and that child is a br:" |
michael@0 | 7663 | if (["LI", "DT", "DD"].indexOf(container.tagName) != -1 |
michael@0 | 7664 | && (!container.hasChildNodes() |
michael@0 | 7665 | || (container.childNodes.length == 1 |
michael@0 | 7666 | && isHtmlElement(container.firstChild, "br")))) { |
michael@0 | 7667 | // "Split the parent of the one-node list consisting of container." |
michael@0 | 7668 | splitParent([container]); |
michael@0 | 7669 | |
michael@0 | 7670 | // "If container has no children, call createElement("br") on the |
michael@0 | 7671 | // context object and append the result as the last child of |
michael@0 | 7672 | // container." |
michael@0 | 7673 | if (!container.hasChildNodes()) { |
michael@0 | 7674 | container.appendChild(document.createElement("br")); |
michael@0 | 7675 | } |
michael@0 | 7676 | |
michael@0 | 7677 | // "If container is a dd or dt, and it is not an allowed child of |
michael@0 | 7678 | // any of its ancestors in the same editing host, set the tag name |
michael@0 | 7679 | // of container to the default single-line container name and let |
michael@0 | 7680 | // container be the result." |
michael@0 | 7681 | if (isHtmlElement(container, ["dd", "dt"]) |
michael@0 | 7682 | && getAncestors(container).every(function(ancestor) { |
michael@0 | 7683 | return !inSameEditingHost(container, ancestor) |
michael@0 | 7684 | || !isAllowedChild(container, ancestor) |
michael@0 | 7685 | })) { |
michael@0 | 7686 | container = setTagName(container, defaultSingleLineContainerName); |
michael@0 | 7687 | } |
michael@0 | 7688 | |
michael@0 | 7689 | // "Fix disallowed ancestors of container." |
michael@0 | 7690 | fixDisallowedAncestors(container); |
michael@0 | 7691 | |
michael@0 | 7692 | // "Return true." |
michael@0 | 7693 | return true; |
michael@0 | 7694 | } |
michael@0 | 7695 | |
michael@0 | 7696 | // "Let new line range be a new range whose start is the same as |
michael@0 | 7697 | // the active range's, and whose end is (container, length of |
michael@0 | 7698 | // container)." |
michael@0 | 7699 | var newLineRange = document.createRange(); |
michael@0 | 7700 | newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset); |
michael@0 | 7701 | newLineRange.setEnd(container, getNodeLength(container)); |
michael@0 | 7702 | |
michael@0 | 7703 | // "While new line range's start offset is zero and its start node is |
michael@0 | 7704 | // not a prohibited paragraph child, set its start to (parent of start |
michael@0 | 7705 | // node, index of start node)." |
michael@0 | 7706 | while (newLineRange.startOffset == 0 |
michael@0 | 7707 | && !isProhibitedParagraphChild(newLineRange.startContainer)) { |
michael@0 | 7708 | newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer)); |
michael@0 | 7709 | } |
michael@0 | 7710 | |
michael@0 | 7711 | // "While new line range's start offset is the length of its start node |
michael@0 | 7712 | // and its start node is not a prohibited paragraph child, set its |
michael@0 | 7713 | // start to (parent of start node, 1 + index of start node)." |
michael@0 | 7714 | while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer) |
michael@0 | 7715 | && !isProhibitedParagraphChild(newLineRange.startContainer)) { |
michael@0 | 7716 | newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer)); |
michael@0 | 7717 | } |
michael@0 | 7718 | |
michael@0 | 7719 | // "Let end of line be true if new line range contains either nothing |
michael@0 | 7720 | // or a single br, and false otherwise." |
michael@0 | 7721 | var containedInNewLineRange = getContainedNodes(newLineRange); |
michael@0 | 7722 | var endOfLine = !containedInNewLineRange.length |
michael@0 | 7723 | || (containedInNewLineRange.length == 1 |
michael@0 | 7724 | && isHtmlElement(containedInNewLineRange[0], "br")); |
michael@0 | 7725 | |
michael@0 | 7726 | // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or |
michael@0 | 7727 | // "h6", and end of line is true, let new container name be the default |
michael@0 | 7728 | // single-line container name." |
michael@0 | 7729 | var newContainerName; |
michael@0 | 7730 | if (/^H[1-6]$/.test(container.tagName) |
michael@0 | 7731 | && endOfLine) { |
michael@0 | 7732 | newContainerName = defaultSingleLineContainerName; |
michael@0 | 7733 | |
michael@0 | 7734 | // "Otherwise, if the local name of container is "dt" and end of line |
michael@0 | 7735 | // is true, let new container name be "dd"." |
michael@0 | 7736 | } else if (container.tagName == "DT" |
michael@0 | 7737 | && endOfLine) { |
michael@0 | 7738 | newContainerName = "dd"; |
michael@0 | 7739 | |
michael@0 | 7740 | // "Otherwise, if the local name of container is "dd" and end of line |
michael@0 | 7741 | // is true, let new container name be "dt"." |
michael@0 | 7742 | } else if (container.tagName == "DD" |
michael@0 | 7743 | && endOfLine) { |
michael@0 | 7744 | newContainerName = "dt"; |
michael@0 | 7745 | |
michael@0 | 7746 | // "Otherwise, let new container name be the local name of container." |
michael@0 | 7747 | } else { |
michael@0 | 7748 | newContainerName = container.tagName.toLowerCase(); |
michael@0 | 7749 | } |
michael@0 | 7750 | |
michael@0 | 7751 | // "Let new container be the result of calling createElement(new |
michael@0 | 7752 | // container name) on the context object." |
michael@0 | 7753 | var newContainer = document.createElement(newContainerName); |
michael@0 | 7754 | |
michael@0 | 7755 | // "Copy all attributes of container to new container." |
michael@0 | 7756 | for (var i = 0; i < container.attributes.length; i++) { |
michael@0 | 7757 | newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value); |
michael@0 | 7758 | } |
michael@0 | 7759 | |
michael@0 | 7760 | // "If new container has an id attribute, unset it." |
michael@0 | 7761 | newContainer.removeAttribute("id"); |
michael@0 | 7762 | |
michael@0 | 7763 | // "Insert new container into the parent of container immediately after |
michael@0 | 7764 | // container." |
michael@0 | 7765 | container.parentNode.insertBefore(newContainer, container.nextSibling); |
michael@0 | 7766 | |
michael@0 | 7767 | // "Let contained nodes be all nodes contained in new line range." |
michael@0 | 7768 | var containedNodes = getAllContainedNodes(newLineRange); |
michael@0 | 7769 | |
michael@0 | 7770 | // "Let frag be the result of calling extractContents() on new line |
michael@0 | 7771 | // range." |
michael@0 | 7772 | var frag = newLineRange.extractContents(); |
michael@0 | 7773 | |
michael@0 | 7774 | // "Unset the id attribute (if any) of each Element descendant of frag |
michael@0 | 7775 | // that is not in contained nodes." |
michael@0 | 7776 | var descendants = getDescendants(frag); |
michael@0 | 7777 | for (var i = 0; i < descendants.length; i++) { |
michael@0 | 7778 | if (descendants[i].nodeType == Node.ELEMENT_NODE |
michael@0 | 7779 | && containedNodes.indexOf(descendants[i]) == -1) { |
michael@0 | 7780 | descendants[i].removeAttribute("id"); |
michael@0 | 7781 | } |
michael@0 | 7782 | } |
michael@0 | 7783 | |
michael@0 | 7784 | // "Call appendChild(frag) on new container." |
michael@0 | 7785 | newContainer.appendChild(frag); |
michael@0 | 7786 | |
michael@0 | 7787 | // "While container's lastChild is a prohibited paragraph child, set |
michael@0 | 7788 | // container to its lastChild." |
michael@0 | 7789 | while (isProhibitedParagraphChild(container.lastChild)) { |
michael@0 | 7790 | container = container.lastChild; |
michael@0 | 7791 | } |
michael@0 | 7792 | |
michael@0 | 7793 | // "While new container's lastChild is a prohibited paragraph child, |
michael@0 | 7794 | // set new container to its lastChild." |
michael@0 | 7795 | while (isProhibitedParagraphChild(newContainer.lastChild)) { |
michael@0 | 7796 | newContainer = newContainer.lastChild; |
michael@0 | 7797 | } |
michael@0 | 7798 | |
michael@0 | 7799 | // "If container has no visible children, call createElement("br") on |
michael@0 | 7800 | // the context object, and append the result as the last child of |
michael@0 | 7801 | // container." |
michael@0 | 7802 | if (![].some.call(container.childNodes, isVisible)) { |
michael@0 | 7803 | container.appendChild(document.createElement("br")); |
michael@0 | 7804 | } |
michael@0 | 7805 | |
michael@0 | 7806 | // "If new container has no visible children, call createElement("br") |
michael@0 | 7807 | // on the context object, and append the result as the last child of |
michael@0 | 7808 | // new container." |
michael@0 | 7809 | if (![].some.call(newContainer.childNodes, isVisible)) { |
michael@0 | 7810 | newContainer.appendChild(document.createElement("br")); |
michael@0 | 7811 | } |
michael@0 | 7812 | |
michael@0 | 7813 | // "Call collapse(new container, 0) on the context object's Selection." |
michael@0 | 7814 | getSelection().collapse(newContainer, 0); |
michael@0 | 7815 | getActiveRange().setStart(newContainer, 0); |
michael@0 | 7816 | getActiveRange().setEnd(newContainer, 0); |
michael@0 | 7817 | |
michael@0 | 7818 | // "Return true." |
michael@0 | 7819 | return true; |
michael@0 | 7820 | } |
michael@0 | 7821 | }; |
michael@0 | 7822 | |
michael@0 | 7823 | //@} |
michael@0 | 7824 | ///// The insertText command ///// |
michael@0 | 7825 | //@{ |
michael@0 | 7826 | commands.inserttext = { |
michael@0 | 7827 | action: function(value) { |
michael@0 | 7828 | // "Delete the selection, with strip wrappers false." |
michael@0 | 7829 | deleteSelection({stripWrappers: false}); |
michael@0 | 7830 | |
michael@0 | 7831 | // "If the active range's start node is neither editable nor an editing |
michael@0 | 7832 | // host, return true." |
michael@0 | 7833 | if (!isEditable(getActiveRange().startContainer) |
michael@0 | 7834 | && !isEditingHost(getActiveRange().startContainer)) { |
michael@0 | 7835 | return true; |
michael@0 | 7836 | } |
michael@0 | 7837 | |
michael@0 | 7838 | // "If value's length is greater than one:" |
michael@0 | 7839 | if (value.length > 1) { |
michael@0 | 7840 | // "For each element el in value, take the action for the |
michael@0 | 7841 | // insertText command, with value equal to el." |
michael@0 | 7842 | for (var i = 0; i < value.length; i++) { |
michael@0 | 7843 | commands.inserttext.action(value[i]); |
michael@0 | 7844 | } |
michael@0 | 7845 | |
michael@0 | 7846 | // "Return true." |
michael@0 | 7847 | return true; |
michael@0 | 7848 | } |
michael@0 | 7849 | |
michael@0 | 7850 | // "If value is the empty string, return true." |
michael@0 | 7851 | if (value == "") { |
michael@0 | 7852 | return true; |
michael@0 | 7853 | } |
michael@0 | 7854 | |
michael@0 | 7855 | // "If value is a newline (U+00A0), take the action for the |
michael@0 | 7856 | // insertParagraph command and return true." |
michael@0 | 7857 | if (value == "\n") { |
michael@0 | 7858 | commands.insertparagraph.action(); |
michael@0 | 7859 | return true; |
michael@0 | 7860 | } |
michael@0 | 7861 | |
michael@0 | 7862 | // "Let node and offset be the active range's start node and offset." |
michael@0 | 7863 | var node = getActiveRange().startContainer; |
michael@0 | 7864 | var offset = getActiveRange().startOffset; |
michael@0 | 7865 | |
michael@0 | 7866 | // "If node has a child whose index is offset − 1, and that child is a |
michael@0 | 7867 | // Text node, set node to that child, then set offset to node's |
michael@0 | 7868 | // length." |
michael@0 | 7869 | if (0 <= offset - 1 |
michael@0 | 7870 | && offset - 1 < node.childNodes.length |
michael@0 | 7871 | && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) { |
michael@0 | 7872 | node = node.childNodes[offset - 1]; |
michael@0 | 7873 | offset = getNodeLength(node); |
michael@0 | 7874 | } |
michael@0 | 7875 | |
michael@0 | 7876 | // "If node has a child whose index is offset, and that child is a Text |
michael@0 | 7877 | // node, set node to that child, then set offset to zero." |
michael@0 | 7878 | if (0 <= offset |
michael@0 | 7879 | && offset < node.childNodes.length |
michael@0 | 7880 | && node.childNodes[offset].nodeType == Node.TEXT_NODE) { |
michael@0 | 7881 | node = node.childNodes[offset]; |
michael@0 | 7882 | offset = 0; |
michael@0 | 7883 | } |
michael@0 | 7884 | |
michael@0 | 7885 | // "Record current overrides, and let overrides be the result." |
michael@0 | 7886 | var overrides = recordCurrentOverrides(); |
michael@0 | 7887 | |
michael@0 | 7888 | // "Call collapse(node, offset) on the context object's Selection." |
michael@0 | 7889 | getSelection().collapse(node, offset); |
michael@0 | 7890 | getActiveRange().setStart(node, offset); |
michael@0 | 7891 | getActiveRange().setEnd(node, offset); |
michael@0 | 7892 | |
michael@0 | 7893 | // "Canonicalize whitespace at (node, offset)." |
michael@0 | 7894 | canonicalizeWhitespace(node, offset); |
michael@0 | 7895 | |
michael@0 | 7896 | // "Let (node, offset) be the active range's start." |
michael@0 | 7897 | node = getActiveRange().startContainer; |
michael@0 | 7898 | offset = getActiveRange().startOffset; |
michael@0 | 7899 | |
michael@0 | 7900 | // "If node is a Text node:" |
michael@0 | 7901 | if (node.nodeType == Node.TEXT_NODE) { |
michael@0 | 7902 | // "Call insertData(offset, value) on node." |
michael@0 | 7903 | node.insertData(offset, value); |
michael@0 | 7904 | |
michael@0 | 7905 | // "Call collapse(node, offset) on the context object's Selection." |
michael@0 | 7906 | getSelection().collapse(node, offset); |
michael@0 | 7907 | getActiveRange().setStart(node, offset); |
michael@0 | 7908 | |
michael@0 | 7909 | // "Call extend(node, offset + 1) on the context object's |
michael@0 | 7910 | // Selection." |
michael@0 | 7911 | // |
michael@0 | 7912 | // Work around WebKit bug: the extend() can throw if the text we're |
michael@0 | 7913 | // adding is trailing whitespace. |
michael@0 | 7914 | try { getSelection().extend(node, offset + 1); } catch(e) {} |
michael@0 | 7915 | getActiveRange().setEnd(node, offset + 1); |
michael@0 | 7916 | |
michael@0 | 7917 | // "Otherwise:" |
michael@0 | 7918 | } else { |
michael@0 | 7919 | // "If node has only one child, which is a collapsed line break, |
michael@0 | 7920 | // remove its child from it." |
michael@0 | 7921 | // |
michael@0 | 7922 | // FIXME: IE incorrectly returns false here instead of true |
michael@0 | 7923 | // sometimes? |
michael@0 | 7924 | if (node.childNodes.length == 1 |
michael@0 | 7925 | && isCollapsedLineBreak(node.firstChild)) { |
michael@0 | 7926 | node.removeChild(node.firstChild); |
michael@0 | 7927 | } |
michael@0 | 7928 | |
michael@0 | 7929 | // "Let text be the result of calling createTextNode(value) on the |
michael@0 | 7930 | // context object." |
michael@0 | 7931 | var text = document.createTextNode(value); |
michael@0 | 7932 | |
michael@0 | 7933 | // "Call insertNode(text) on the active range." |
michael@0 | 7934 | getActiveRange().insertNode(text); |
michael@0 | 7935 | |
michael@0 | 7936 | // "Call collapse(text, 0) on the context object's Selection." |
michael@0 | 7937 | getSelection().collapse(text, 0); |
michael@0 | 7938 | getActiveRange().setStart(text, 0); |
michael@0 | 7939 | |
michael@0 | 7940 | // "Call extend(text, 1) on the context object's Selection." |
michael@0 | 7941 | getSelection().extend(text, 1); |
michael@0 | 7942 | getActiveRange().setEnd(text, 1); |
michael@0 | 7943 | } |
michael@0 | 7944 | |
michael@0 | 7945 | // "Restore states and values from overrides." |
michael@0 | 7946 | restoreStatesAndValues(overrides); |
michael@0 | 7947 | |
michael@0 | 7948 | // "Canonicalize whitespace at the active range's start, with fix |
michael@0 | 7949 | // collapsed space false." |
michael@0 | 7950 | canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false); |
michael@0 | 7951 | |
michael@0 | 7952 | // "Canonicalize whitespace at the active range's end, with fix |
michael@0 | 7953 | // collapsed space false." |
michael@0 | 7954 | canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false); |
michael@0 | 7955 | |
michael@0 | 7956 | // "If value is a space character, autolink the active range's start." |
michael@0 | 7957 | if (/^[ \t\n\f\r]$/.test(value)) { |
michael@0 | 7958 | autolink(getActiveRange().startContainer, getActiveRange().startOffset); |
michael@0 | 7959 | } |
michael@0 | 7960 | |
michael@0 | 7961 | // "Call collapseToEnd() on the context object's Selection." |
michael@0 | 7962 | // |
michael@0 | 7963 | // Work around WebKit bug: sometimes it blows up the selection and |
michael@0 | 7964 | // throws, which we don't want. |
michael@0 | 7965 | try { getSelection().collapseToEnd(); } catch(e) {} |
michael@0 | 7966 | getActiveRange().collapse(false); |
michael@0 | 7967 | |
michael@0 | 7968 | // "Return true." |
michael@0 | 7969 | return true; |
michael@0 | 7970 | } |
michael@0 | 7971 | }; |
michael@0 | 7972 | |
michael@0 | 7973 | //@} |
michael@0 | 7974 | ///// The insertUnorderedList command ///// |
michael@0 | 7975 | //@{ |
michael@0 | 7976 | commands.insertunorderedlist = { |
michael@0 | 7977 | preservesOverrides: true, |
michael@0 | 7978 | // "Toggle lists with tag name "ul", then return true." |
michael@0 | 7979 | action: function() { toggleLists("ul"); return true }, |
michael@0 | 7980 | // "True if the selection's list state is "mixed" or "mixed ul", false |
michael@0 | 7981 | // otherwise." |
michael@0 | 7982 | indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) }, |
michael@0 | 7983 | // "True if the selection's list state is "ul", false otherwise." |
michael@0 | 7984 | state: function() { return getSelectionListState() == "ul" }, |
michael@0 | 7985 | }; |
michael@0 | 7986 | |
michael@0 | 7987 | //@} |
michael@0 | 7988 | ///// The justifyCenter command ///// |
michael@0 | 7989 | //@{ |
michael@0 | 7990 | commands.justifycenter = { |
michael@0 | 7991 | preservesOverrides: true, |
michael@0 | 7992 | // "Justify the selection with alignment "center", then return true." |
michael@0 | 7993 | action: function() { justifySelection("center"); return true }, |
michael@0 | 7994 | indeterm: function() { |
michael@0 | 7995 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 7996 | // the active range. Return true if among visible editable nodes that |
michael@0 | 7997 | // are contained in the result and have no children, at least one has |
michael@0 | 7998 | // alignment value "center" and at least one does not. Otherwise return |
michael@0 | 7999 | // false." |
michael@0 | 8000 | if (!getActiveRange()) { |
michael@0 | 8001 | return false; |
michael@0 | 8002 | } |
michael@0 | 8003 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8004 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8005 | }); |
michael@0 | 8006 | return nodes.some(function(node) { return getAlignmentValue(node) == "center" }) |
michael@0 | 8007 | && nodes.some(function(node) { return getAlignmentValue(node) != "center" }); |
michael@0 | 8008 | }, state: function() { |
michael@0 | 8009 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 8010 | // the active range. Return true if there is at least one visible |
michael@0 | 8011 | // editable node that is contained in the result and has no children, |
michael@0 | 8012 | // and all such nodes have alignment value "center". Otherwise return |
michael@0 | 8013 | // false." |
michael@0 | 8014 | if (!getActiveRange()) { |
michael@0 | 8015 | return false; |
michael@0 | 8016 | } |
michael@0 | 8017 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8018 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8019 | }); |
michael@0 | 8020 | return nodes.length |
michael@0 | 8021 | && nodes.every(function(node) { return getAlignmentValue(node) == "center" }); |
michael@0 | 8022 | }, value: function() { |
michael@0 | 8023 | // "Return the empty string if the active range is null. Otherwise, |
michael@0 | 8024 | // block-extend the active range, and return the alignment value of the |
michael@0 | 8025 | // first visible editable node that is contained in the result and has |
michael@0 | 8026 | // no children. If there is no such node, return "left"." |
michael@0 | 8027 | if (!getActiveRange()) { |
michael@0 | 8028 | return ""; |
michael@0 | 8029 | } |
michael@0 | 8030 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8031 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8032 | }); |
michael@0 | 8033 | if (nodes.length) { |
michael@0 | 8034 | return getAlignmentValue(nodes[0]); |
michael@0 | 8035 | } else { |
michael@0 | 8036 | return "left"; |
michael@0 | 8037 | } |
michael@0 | 8038 | }, |
michael@0 | 8039 | }; |
michael@0 | 8040 | |
michael@0 | 8041 | //@} |
michael@0 | 8042 | ///// The justifyFull command ///// |
michael@0 | 8043 | //@{ |
michael@0 | 8044 | commands.justifyfull = { |
michael@0 | 8045 | preservesOverrides: true, |
michael@0 | 8046 | // "Justify the selection with alignment "justify", then return true." |
michael@0 | 8047 | action: function() { justifySelection("justify"); return true }, |
michael@0 | 8048 | indeterm: function() { |
michael@0 | 8049 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 8050 | // the active range. Return true if among visible editable nodes that |
michael@0 | 8051 | // are contained in the result and have no children, at least one has |
michael@0 | 8052 | // alignment value "justify" and at least one does not. Otherwise |
michael@0 | 8053 | // return false." |
michael@0 | 8054 | if (!getActiveRange()) { |
michael@0 | 8055 | return false; |
michael@0 | 8056 | } |
michael@0 | 8057 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8058 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8059 | }); |
michael@0 | 8060 | return nodes.some(function(node) { return getAlignmentValue(node) == "justify" }) |
michael@0 | 8061 | && nodes.some(function(node) { return getAlignmentValue(node) != "justify" }); |
michael@0 | 8062 | }, state: function() { |
michael@0 | 8063 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 8064 | // the active range. Return true if there is at least one visible |
michael@0 | 8065 | // editable node that is contained in the result and has no children, |
michael@0 | 8066 | // and all such nodes have alignment value "justify". Otherwise return |
michael@0 | 8067 | // false." |
michael@0 | 8068 | if (!getActiveRange()) { |
michael@0 | 8069 | return false; |
michael@0 | 8070 | } |
michael@0 | 8071 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8072 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8073 | }); |
michael@0 | 8074 | return nodes.length |
michael@0 | 8075 | && nodes.every(function(node) { return getAlignmentValue(node) == "justify" }); |
michael@0 | 8076 | }, value: function() { |
michael@0 | 8077 | // "Return the empty string if the active range is null. Otherwise, |
michael@0 | 8078 | // block-extend the active range, and return the alignment value of the |
michael@0 | 8079 | // first visible editable node that is contained in the result and has |
michael@0 | 8080 | // no children. If there is no such node, return "left"." |
michael@0 | 8081 | if (!getActiveRange()) { |
michael@0 | 8082 | return ""; |
michael@0 | 8083 | } |
michael@0 | 8084 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8085 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8086 | }); |
michael@0 | 8087 | if (nodes.length) { |
michael@0 | 8088 | return getAlignmentValue(nodes[0]); |
michael@0 | 8089 | } else { |
michael@0 | 8090 | return "left"; |
michael@0 | 8091 | } |
michael@0 | 8092 | }, |
michael@0 | 8093 | }; |
michael@0 | 8094 | |
michael@0 | 8095 | //@} |
michael@0 | 8096 | ///// The justifyLeft command ///// |
michael@0 | 8097 | //@{ |
michael@0 | 8098 | commands.justifyleft = { |
michael@0 | 8099 | preservesOverrides: true, |
michael@0 | 8100 | // "Justify the selection with alignment "left", then return true." |
michael@0 | 8101 | action: function() { justifySelection("left"); return true }, |
michael@0 | 8102 | indeterm: function() { |
michael@0 | 8103 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 8104 | // the active range. Return true if among visible editable nodes that |
michael@0 | 8105 | // are contained in the result and have no children, at least one has |
michael@0 | 8106 | // alignment value "left" and at least one does not. Otherwise return |
michael@0 | 8107 | // false." |
michael@0 | 8108 | if (!getActiveRange()) { |
michael@0 | 8109 | return false; |
michael@0 | 8110 | } |
michael@0 | 8111 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8112 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8113 | }); |
michael@0 | 8114 | return nodes.some(function(node) { return getAlignmentValue(node) == "left" }) |
michael@0 | 8115 | && nodes.some(function(node) { return getAlignmentValue(node) != "left" }); |
michael@0 | 8116 | }, state: function() { |
michael@0 | 8117 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 8118 | // the active range. Return true if there is at least one visible |
michael@0 | 8119 | // editable node that is contained in the result and has no children, |
michael@0 | 8120 | // and all such nodes have alignment value "left". Otherwise return |
michael@0 | 8121 | // false." |
michael@0 | 8122 | if (!getActiveRange()) { |
michael@0 | 8123 | return false; |
michael@0 | 8124 | } |
michael@0 | 8125 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8126 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8127 | }); |
michael@0 | 8128 | return nodes.length |
michael@0 | 8129 | && nodes.every(function(node) { return getAlignmentValue(node) == "left" }); |
michael@0 | 8130 | }, value: function() { |
michael@0 | 8131 | // "Return the empty string if the active range is null. Otherwise, |
michael@0 | 8132 | // block-extend the active range, and return the alignment value of the |
michael@0 | 8133 | // first visible editable node that is contained in the result and has |
michael@0 | 8134 | // no children. If there is no such node, return "left"." |
michael@0 | 8135 | if (!getActiveRange()) { |
michael@0 | 8136 | return ""; |
michael@0 | 8137 | } |
michael@0 | 8138 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8139 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8140 | }); |
michael@0 | 8141 | if (nodes.length) { |
michael@0 | 8142 | return getAlignmentValue(nodes[0]); |
michael@0 | 8143 | } else { |
michael@0 | 8144 | return "left"; |
michael@0 | 8145 | } |
michael@0 | 8146 | }, |
michael@0 | 8147 | }; |
michael@0 | 8148 | |
michael@0 | 8149 | //@} |
michael@0 | 8150 | ///// The justifyRight command ///// |
michael@0 | 8151 | //@{ |
michael@0 | 8152 | commands.justifyright = { |
michael@0 | 8153 | preservesOverrides: true, |
michael@0 | 8154 | // "Justify the selection with alignment "right", then return true." |
michael@0 | 8155 | action: function() { justifySelection("right"); return true }, |
michael@0 | 8156 | indeterm: function() { |
michael@0 | 8157 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 8158 | // the active range. Return true if among visible editable nodes that |
michael@0 | 8159 | // are contained in the result and have no children, at least one has |
michael@0 | 8160 | // alignment value "right" and at least one does not. Otherwise return |
michael@0 | 8161 | // false." |
michael@0 | 8162 | if (!getActiveRange()) { |
michael@0 | 8163 | return false; |
michael@0 | 8164 | } |
michael@0 | 8165 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8166 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8167 | }); |
michael@0 | 8168 | return nodes.some(function(node) { return getAlignmentValue(node) == "right" }) |
michael@0 | 8169 | && nodes.some(function(node) { return getAlignmentValue(node) != "right" }); |
michael@0 | 8170 | }, state: function() { |
michael@0 | 8171 | // "Return false if the active range is null. Otherwise, block-extend |
michael@0 | 8172 | // the active range. Return true if there is at least one visible |
michael@0 | 8173 | // editable node that is contained in the result and has no children, |
michael@0 | 8174 | // and all such nodes have alignment value "right". Otherwise return |
michael@0 | 8175 | // false." |
michael@0 | 8176 | if (!getActiveRange()) { |
michael@0 | 8177 | return false; |
michael@0 | 8178 | } |
michael@0 | 8179 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8180 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8181 | }); |
michael@0 | 8182 | return nodes.length |
michael@0 | 8183 | && nodes.every(function(node) { return getAlignmentValue(node) == "right" }); |
michael@0 | 8184 | }, value: function() { |
michael@0 | 8185 | // "Return the empty string if the active range is null. Otherwise, |
michael@0 | 8186 | // block-extend the active range, and return the alignment value of the |
michael@0 | 8187 | // first visible editable node that is contained in the result and has |
michael@0 | 8188 | // no children. If there is no such node, return "left"." |
michael@0 | 8189 | if (!getActiveRange()) { |
michael@0 | 8190 | return ""; |
michael@0 | 8191 | } |
michael@0 | 8192 | var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) { |
michael@0 | 8193 | return isEditable(node) && isVisible(node) && !node.hasChildNodes(); |
michael@0 | 8194 | }); |
michael@0 | 8195 | if (nodes.length) { |
michael@0 | 8196 | return getAlignmentValue(nodes[0]); |
michael@0 | 8197 | } else { |
michael@0 | 8198 | return "left"; |
michael@0 | 8199 | } |
michael@0 | 8200 | }, |
michael@0 | 8201 | }; |
michael@0 | 8202 | |
michael@0 | 8203 | //@} |
michael@0 | 8204 | ///// The outdent command ///// |
michael@0 | 8205 | //@{ |
michael@0 | 8206 | commands.outdent = { |
michael@0 | 8207 | preservesOverrides: true, |
michael@0 | 8208 | action: function() { |
michael@0 | 8209 | // "Let items be a list of all lis that are ancestor containers of the |
michael@0 | 8210 | // range's start and/or end node." |
michael@0 | 8211 | // |
michael@0 | 8212 | // It's annoying to get this in tree order using functional stuff |
michael@0 | 8213 | // without doing getDescendants(document), which is slow, so I do it |
michael@0 | 8214 | // imperatively. |
michael@0 | 8215 | var items = []; |
michael@0 | 8216 | (function(){ |
michael@0 | 8217 | for ( |
michael@0 | 8218 | var ancestorContainer = getActiveRange().endContainer; |
michael@0 | 8219 | ancestorContainer != getActiveRange().commonAncestorContainer; |
michael@0 | 8220 | ancestorContainer = ancestorContainer.parentNode |
michael@0 | 8221 | ) { |
michael@0 | 8222 | if (isHtmlElement(ancestorContainer, "li")) { |
michael@0 | 8223 | items.unshift(ancestorContainer); |
michael@0 | 8224 | } |
michael@0 | 8225 | } |
michael@0 | 8226 | for ( |
michael@0 | 8227 | var ancestorContainer = getActiveRange().startContainer; |
michael@0 | 8228 | ancestorContainer; |
michael@0 | 8229 | ancestorContainer = ancestorContainer.parentNode |
michael@0 | 8230 | ) { |
michael@0 | 8231 | if (isHtmlElement(ancestorContainer, "li")) { |
michael@0 | 8232 | items.unshift(ancestorContainer); |
michael@0 | 8233 | } |
michael@0 | 8234 | } |
michael@0 | 8235 | })(); |
michael@0 | 8236 | |
michael@0 | 8237 | // "For each item in items, normalize sublists of item." |
michael@0 | 8238 | items.forEach(normalizeSublists); |
michael@0 | 8239 | |
michael@0 | 8240 | // "Block-extend the active range, and let new range be the result." |
michael@0 | 8241 | var newRange = blockExtend(getActiveRange()); |
michael@0 | 8242 | |
michael@0 | 8243 | // "Let node list be a list of nodes, initially empty." |
michael@0 | 8244 | // |
michael@0 | 8245 | // "For each node node contained in new range, append node to node list |
michael@0 | 8246 | // if the last member of node list (if any) is not an ancestor of node; |
michael@0 | 8247 | // node is editable; and either node has no editable descendants, or is |
michael@0 | 8248 | // an ol or ul, or is an li whose parent is an ol or ul." |
michael@0 | 8249 | var nodeList = getContainedNodes(newRange, function(node) { |
michael@0 | 8250 | return isEditable(node) |
michael@0 | 8251 | && (!getDescendants(node).some(isEditable) |
michael@0 | 8252 | || isHtmlElement(node, ["ol", "ul"]) |
michael@0 | 8253 | || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"]))); |
michael@0 | 8254 | }); |
michael@0 | 8255 | |
michael@0 | 8256 | // "While node list is not empty:" |
michael@0 | 8257 | while (nodeList.length) { |
michael@0 | 8258 | // "While the first member of node list is an ol or ul or is not |
michael@0 | 8259 | // the child of an ol or ul, outdent it and remove it from node |
michael@0 | 8260 | // list." |
michael@0 | 8261 | while (nodeList.length |
michael@0 | 8262 | && (isHtmlElement(nodeList[0], ["OL", "UL"]) |
michael@0 | 8263 | || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) { |
michael@0 | 8264 | outdentNode(nodeList.shift()); |
michael@0 | 8265 | } |
michael@0 | 8266 | |
michael@0 | 8267 | // "If node list is empty, break from these substeps." |
michael@0 | 8268 | if (!nodeList.length) { |
michael@0 | 8269 | break; |
michael@0 | 8270 | } |
michael@0 | 8271 | |
michael@0 | 8272 | // "Let sublist be a list of nodes, initially empty." |
michael@0 | 8273 | var sublist = []; |
michael@0 | 8274 | |
michael@0 | 8275 | // "Remove the first member of node list and append it to sublist." |
michael@0 | 8276 | sublist.push(nodeList.shift()); |
michael@0 | 8277 | |
michael@0 | 8278 | // "While the first member of node list is the nextSibling of the |
michael@0 | 8279 | // last member of sublist, and the first member of node list is not |
michael@0 | 8280 | // an ol or ul, remove the first member of node list and append it |
michael@0 | 8281 | // to sublist." |
michael@0 | 8282 | while (nodeList.length |
michael@0 | 8283 | && nodeList[0] == sublist[sublist.length - 1].nextSibling |
michael@0 | 8284 | && !isHtmlElement(nodeList[0], ["OL", "UL"])) { |
michael@0 | 8285 | sublist.push(nodeList.shift()); |
michael@0 | 8286 | } |
michael@0 | 8287 | |
michael@0 | 8288 | // "Record the values of sublist, and let values be the result." |
michael@0 | 8289 | var values = recordValues(sublist); |
michael@0 | 8290 | |
michael@0 | 8291 | // "Split the parent of sublist, with new parent null." |
michael@0 | 8292 | splitParent(sublist); |
michael@0 | 8293 | |
michael@0 | 8294 | // "Fix disallowed ancestors of each member of sublist." |
michael@0 | 8295 | sublist.forEach(fixDisallowedAncestors); |
michael@0 | 8296 | |
michael@0 | 8297 | // "Restore the values from values." |
michael@0 | 8298 | restoreValues(values); |
michael@0 | 8299 | } |
michael@0 | 8300 | |
michael@0 | 8301 | // "Return true." |
michael@0 | 8302 | return true; |
michael@0 | 8303 | } |
michael@0 | 8304 | }; |
michael@0 | 8305 | |
michael@0 | 8306 | //@} |
michael@0 | 8307 | |
michael@0 | 8308 | ////////////////////////////////// |
michael@0 | 8309 | ///// Miscellaneous commands ///// |
michael@0 | 8310 | ////////////////////////////////// |
michael@0 | 8311 | |
michael@0 | 8312 | ///// The defaultParagraphSeparator command ///// |
michael@0 | 8313 | //@{ |
michael@0 | 8314 | commands.defaultparagraphseparator = { |
michael@0 | 8315 | action: function(value) { |
michael@0 | 8316 | // "Let value be converted to ASCII lowercase. If value is then equal |
michael@0 | 8317 | // to "p" or "div", set the context object's default single-line |
michael@0 | 8318 | // container name to value and return true. Otherwise, return false." |
michael@0 | 8319 | value = value.toLowerCase(); |
michael@0 | 8320 | if (value == "p" || value == "div") { |
michael@0 | 8321 | defaultSingleLineContainerName = value; |
michael@0 | 8322 | return true; |
michael@0 | 8323 | } |
michael@0 | 8324 | return false; |
michael@0 | 8325 | }, value: function() { |
michael@0 | 8326 | // "Return the context object's default single-line container name." |
michael@0 | 8327 | return defaultSingleLineContainerName; |
michael@0 | 8328 | }, |
michael@0 | 8329 | }; |
michael@0 | 8330 | |
michael@0 | 8331 | //@} |
michael@0 | 8332 | ///// The selectAll command ///// |
michael@0 | 8333 | //@{ |
michael@0 | 8334 | commands.selectall = { |
michael@0 | 8335 | // Note, this ignores the whole globalRange/getActiveRange() thing and |
michael@0 | 8336 | // works with actual selections. Not suitable for autoimplementation.html. |
michael@0 | 8337 | action: function() { |
michael@0 | 8338 | // "Let target be the body element of the context object." |
michael@0 | 8339 | var target = document.body; |
michael@0 | 8340 | |
michael@0 | 8341 | // "If target is null, let target be the context object's |
michael@0 | 8342 | // documentElement." |
michael@0 | 8343 | if (!target) { |
michael@0 | 8344 | target = document.documentElement; |
michael@0 | 8345 | } |
michael@0 | 8346 | |
michael@0 | 8347 | // "If target is null, call getSelection() on the context object, and |
michael@0 | 8348 | // call removeAllRanges() on the result." |
michael@0 | 8349 | if (!target) { |
michael@0 | 8350 | getSelection().removeAllRanges(); |
michael@0 | 8351 | |
michael@0 | 8352 | // "Otherwise, call getSelection() on the context object, and call |
michael@0 | 8353 | // selectAllChildren(target) on the result." |
michael@0 | 8354 | } else { |
michael@0 | 8355 | getSelection().selectAllChildren(target); |
michael@0 | 8356 | } |
michael@0 | 8357 | |
michael@0 | 8358 | // "Return true." |
michael@0 | 8359 | return true; |
michael@0 | 8360 | } |
michael@0 | 8361 | }; |
michael@0 | 8362 | |
michael@0 | 8363 | //@} |
michael@0 | 8364 | ///// The styleWithCSS command ///// |
michael@0 | 8365 | //@{ |
michael@0 | 8366 | commands.stylewithcss = { |
michael@0 | 8367 | action: function(value) { |
michael@0 | 8368 | // "If value is an ASCII case-insensitive match for the string |
michael@0 | 8369 | // "false", set the CSS styling flag to false. Otherwise, set the |
michael@0 | 8370 | // CSS styling flag to true. Either way, return true." |
michael@0 | 8371 | cssStylingFlag = String(value).toLowerCase() != "false"; |
michael@0 | 8372 | return true; |
michael@0 | 8373 | }, state: function() { return cssStylingFlag } |
michael@0 | 8374 | }; |
michael@0 | 8375 | |
michael@0 | 8376 | //@} |
michael@0 | 8377 | ///// The useCSS command ///// |
michael@0 | 8378 | //@{ |
michael@0 | 8379 | commands.usecss = { |
michael@0 | 8380 | action: function(value) { |
michael@0 | 8381 | // "If value is an ASCII case-insensitive match for the string "false", |
michael@0 | 8382 | // set the CSS styling flag to true. Otherwise, set the CSS styling |
michael@0 | 8383 | // flag to false. Either way, return true." |
michael@0 | 8384 | cssStylingFlag = String(value).toLowerCase() == "false"; |
michael@0 | 8385 | return true; |
michael@0 | 8386 | } |
michael@0 | 8387 | }; |
michael@0 | 8388 | //@} |
michael@0 | 8389 | |
michael@0 | 8390 | // Some final setup |
michael@0 | 8391 | //@{ |
michael@0 | 8392 | (function() { |
michael@0 | 8393 | // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit |
michael@0 | 8394 | // temporary, which means I need an extra closure to not leak the temporaries |
michael@0 | 8395 | // into the global namespace. >:( |
michael@0 | 8396 | var commandNames = []; |
michael@0 | 8397 | for (var command in commands) { |
michael@0 | 8398 | commandNames.push(command); |
michael@0 | 8399 | } |
michael@0 | 8400 | commandNames.forEach(function(command) { |
michael@0 | 8401 | // "If a command does not have a relevant CSS property specified, it |
michael@0 | 8402 | // defaults to null." |
michael@0 | 8403 | if (!("relevantCssProperty" in commands[command])) { |
michael@0 | 8404 | commands[command].relevantCssProperty = null; |
michael@0 | 8405 | } |
michael@0 | 8406 | |
michael@0 | 8407 | // "If a command has inline command activated values defined but nothing |
michael@0 | 8408 | // else defines when it is indeterminate, it is indeterminate if among |
michael@0 | 8409 | // formattable nodes effectively contained in the active range, there is at |
michael@0 | 8410 | // least one whose effective command value is one of the given values and |
michael@0 | 8411 | // at least one whose effective command value is not one of the given |
michael@0 | 8412 | // values." |
michael@0 | 8413 | if ("inlineCommandActivatedValues" in commands[command] |
michael@0 | 8414 | && !("indeterm" in commands[command])) { |
michael@0 | 8415 | commands[command].indeterm = function() { |
michael@0 | 8416 | if (!getActiveRange()) { |
michael@0 | 8417 | return false; |
michael@0 | 8418 | } |
michael@0 | 8419 | |
michael@0 | 8420 | var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode) |
michael@0 | 8421 | .map(function(node) { return getEffectiveCommandValue(node, command) }); |
michael@0 | 8422 | |
michael@0 | 8423 | var matchingValues = values.filter(function(value) { |
michael@0 | 8424 | return commands[command].inlineCommandActivatedValues.indexOf(value) != -1; |
michael@0 | 8425 | }); |
michael@0 | 8426 | |
michael@0 | 8427 | return matchingValues.length >= 1 |
michael@0 | 8428 | && values.length - matchingValues.length >= 1; |
michael@0 | 8429 | }; |
michael@0 | 8430 | } |
michael@0 | 8431 | |
michael@0 | 8432 | // "If a command has inline command activated values defined, its state is |
michael@0 | 8433 | // true if either no formattable node is effectively contained in the |
michael@0 | 8434 | // active range, and the active range's start node's effective command |
michael@0 | 8435 | // value is one of the given values; or if there is at least one |
michael@0 | 8436 | // formattable node effectively contained in the active range, and all of |
michael@0 | 8437 | // them have an effective command value equal to one of the given values." |
michael@0 | 8438 | if ("inlineCommandActivatedValues" in commands[command]) { |
michael@0 | 8439 | commands[command].state = function() { |
michael@0 | 8440 | if (!getActiveRange()) { |
michael@0 | 8441 | return false; |
michael@0 | 8442 | } |
michael@0 | 8443 | |
michael@0 | 8444 | var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode); |
michael@0 | 8445 | |
michael@0 | 8446 | if (nodes.length == 0) { |
michael@0 | 8447 | return commands[command].inlineCommandActivatedValues |
michael@0 | 8448 | .indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1; |
michael@0 | 8449 | } else { |
michael@0 | 8450 | return nodes.every(function(node) { |
michael@0 | 8451 | return commands[command].inlineCommandActivatedValues |
michael@0 | 8452 | .indexOf(getEffectiveCommandValue(node, command)) != -1; |
michael@0 | 8453 | }); |
michael@0 | 8454 | } |
michael@0 | 8455 | }; |
michael@0 | 8456 | } |
michael@0 | 8457 | |
michael@0 | 8458 | // "If a command is a standard inline value command, it is indeterminate if |
michael@0 | 8459 | // among formattable nodes that are effectively contained in the active |
michael@0 | 8460 | // range, there are two that have distinct effective command values. Its |
michael@0 | 8461 | // value is the effective command value of the first formattable node that |
michael@0 | 8462 | // is effectively contained in the active range; or if there is no such |
michael@0 | 8463 | // node, the effective command value of the active range's start node; or |
michael@0 | 8464 | // if that is null, the empty string." |
michael@0 | 8465 | if ("standardInlineValueCommand" in commands[command]) { |
michael@0 | 8466 | commands[command].indeterm = function() { |
michael@0 | 8467 | if (!getActiveRange()) { |
michael@0 | 8468 | return false; |
michael@0 | 8469 | } |
michael@0 | 8470 | |
michael@0 | 8471 | var values = getAllEffectivelyContainedNodes(getActiveRange()) |
michael@0 | 8472 | .filter(isFormattableNode) |
michael@0 | 8473 | .map(function(node) { return getEffectiveCommandValue(node, command) }); |
michael@0 | 8474 | for (var i = 1; i < values.length; i++) { |
michael@0 | 8475 | if (values[i] != values[i - 1]) { |
michael@0 | 8476 | return true; |
michael@0 | 8477 | } |
michael@0 | 8478 | } |
michael@0 | 8479 | return false; |
michael@0 | 8480 | }; |
michael@0 | 8481 | |
michael@0 | 8482 | commands[command].value = function() { |
michael@0 | 8483 | if (!getActiveRange()) { |
michael@0 | 8484 | return ""; |
michael@0 | 8485 | } |
michael@0 | 8486 | |
michael@0 | 8487 | var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0]; |
michael@0 | 8488 | |
michael@0 | 8489 | if (typeof refNode == "undefined") { |
michael@0 | 8490 | refNode = getActiveRange().startContainer; |
michael@0 | 8491 | } |
michael@0 | 8492 | |
michael@0 | 8493 | var ret = getEffectiveCommandValue(refNode, command); |
michael@0 | 8494 | if (ret === null) { |
michael@0 | 8495 | return ""; |
michael@0 | 8496 | } |
michael@0 | 8497 | return ret; |
michael@0 | 8498 | }; |
michael@0 | 8499 | } |
michael@0 | 8500 | |
michael@0 | 8501 | // "If a command preserves overrides, then before taking its action, the |
michael@0 | 8502 | // user agent must record current overrides. After taking the action, if |
michael@0 | 8503 | // the active range is collapsed, it must restore states and values from |
michael@0 | 8504 | // the recorded list." |
michael@0 | 8505 | if ("preservesOverrides" in commands[command]) { |
michael@0 | 8506 | var oldAction = commands[command].action; |
michael@0 | 8507 | |
michael@0 | 8508 | commands[command].action = function(value) { |
michael@0 | 8509 | var overrides = recordCurrentOverrides(); |
michael@0 | 8510 | var ret = oldAction(value); |
michael@0 | 8511 | if (getActiveRange().collapsed) { |
michael@0 | 8512 | restoreStatesAndValues(overrides); |
michael@0 | 8513 | } |
michael@0 | 8514 | return ret; |
michael@0 | 8515 | }; |
michael@0 | 8516 | } |
michael@0 | 8517 | }); |
michael@0 | 8518 | })(); |
michael@0 | 8519 | //@} |
michael@0 | 8520 | |
michael@0 | 8521 | // vim: foldmarker=@{,@} foldmethod=marker |