input-events-get-target-ranges.js (16671B)
1 "use strict"; 2 3 // TODO: extend `EditorTestUtils` in editing/include/edit-test-utils.mjs 4 5 const kBackspaceKey = "\uE003"; 6 const kDeleteKey = "\uE017"; 7 const kArrowRight = "\uE014"; 8 const kArrowLeft = "\uE012"; 9 const kShift = "\uE008"; 10 const kMeta = "\uE03d"; 11 const kControl = "\uE009"; 12 const kAlt = "\uE00A"; 13 const kKeyA = "a"; 14 15 const kImgSrc = 16 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg=="; 17 18 let gSelection, gEditor, gBeforeinput, gInput; 19 20 function initializeTest(aInnerHTML) { 21 function onBeforeinput(event) { 22 // NOTE: Blink makes `getTargetRanges()` return empty range after 23 // propagation, but this test wants to check the result during 24 // propagation. Therefore, we need to cache the result, but will 25 // assert if `getTargetRanges()` returns different ranges after 26 // checking the cached ranges. 27 event.cachedRanges = event.getTargetRanges(); 28 gBeforeinput.push(event); 29 } 30 function onInput(event) { 31 event.cachedRanges = event.getTargetRanges(); 32 gInput.push(event); 33 } 34 if (gEditor !== document.querySelector("div[contenteditable]")) { 35 if (gEditor) { 36 gEditor.isListeningToInputEvents = false; 37 gEditor.removeEventListener("beforeinput", onBeforeinput); 38 gEditor.removeEventListener("input", onInput); 39 } 40 gEditor = document.querySelector("div[contenteditable]"); 41 } 42 gSelection = getSelection(); 43 gBeforeinput = []; 44 gInput = []; 45 if (!gEditor.isListeningToInputEvents) { 46 gEditor.isListeningToInputEvents = true; 47 gEditor.addEventListener("beforeinput", onBeforeinput); 48 gEditor.addEventListener("input", onInput); 49 } 50 51 setupEditor(aInnerHTML); 52 gBeforeinput = []; 53 gInput = []; 54 } 55 56 function getArrayOfRangesDescription(arrayOfRanges) { 57 if (arrayOfRanges === null) { 58 return "null"; 59 } 60 if (arrayOfRanges === undefined) { 61 return "undefined"; 62 } 63 if (!Array.isArray(arrayOfRanges)) { 64 return "Unknown Object"; 65 } 66 if (arrayOfRanges.length === 0) { 67 return "[]"; 68 } 69 let result = "["; 70 for (let range of arrayOfRanges) { 71 result += `{${getRangeDescription(range)}},`; 72 } 73 result += "]"; 74 return result; 75 } 76 77 function getRangeDescription(range) { 78 function getNodeDescription(node) { 79 if (!node) { 80 return "null"; 81 } 82 switch (node.nodeType) { 83 case Node.TEXT_NODE: 84 case Node.COMMENT_NODE: 85 case Node.CDATA_SECTION_NODE: 86 return `${node.nodeName} "${node.data}"`; 87 case Node.ELEMENT_NODE: 88 return `<${node.nodeName.toLowerCase()}>`; 89 default: 90 return `${node.nodeName}`; 91 } 92 } 93 if (range === null) { 94 return "null"; 95 } 96 if (range === undefined) { 97 return "undefined"; 98 } 99 return range.startContainer == range.endContainer && 100 range.startOffset == range.endOffset 101 ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})` 102 : `(${getNodeDescription(range.startContainer)}, ${ 103 range.startOffset 104 }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`; 105 } 106 107 function sendDeleteKey(modifier) { 108 if (!modifier) { 109 return new test_driver.Actions() 110 .keyDown(kDeleteKey) 111 .keyUp(kDeleteKey) 112 .send(); 113 } 114 return new test_driver.Actions() 115 .keyDown(modifier) 116 .keyDown(kDeleteKey) 117 .keyUp(kDeleteKey) 118 .keyUp(modifier) 119 .send(); 120 } 121 122 function sendBackspaceKey(modifier) { 123 if (!modifier) { 124 return new test_driver.Actions() 125 .keyDown(kBackspaceKey) 126 .keyUp(kBackspaceKey) 127 .send(); 128 } 129 return new test_driver.Actions() 130 .keyDown(modifier) 131 .keyDown(kBackspaceKey) 132 .keyUp(kBackspaceKey) 133 .keyUp(modifier) 134 .send(); 135 } 136 137 function sendKeyA() { 138 return new test_driver.Actions() 139 .keyDown(kKeyA) 140 .keyUp(kKeyA) 141 .send(); 142 } 143 144 function sendArrowLeftKey() { 145 return new test_driver.Actions() 146 .keyDown(kArrowLeft) 147 .keyUp(kArrowLeft) 148 .send(); 149 } 150 151 function sendArrowRightKey() { 152 return new test_driver.Actions() 153 .keyDown(kArrowRight) 154 .keyUp(kArrowRight) 155 .send(); 156 } 157 158 function checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedRanges) { 159 assert_equals( 160 gBeforeinput.length, 161 1, 162 "One beforeinput event should be fired if the key operation tries to delete something" 163 ); 164 assert_true( 165 Array.isArray(gBeforeinput[0].cachedRanges), 166 "gBeforeinput[0].getTargetRanges() should return an array of StaticRange instances at least during propagation" 167 ); 168 let arrayOfExpectedRanges = Array.isArray(expectedRanges) 169 ? expectedRanges 170 : [expectedRanges]; 171 // Before checking the length of array of ranges, we should check the given 172 // range first because the ranges are more important than whether there are 173 // redundant additional unexpected ranges. 174 for ( 175 let i = 0; 176 i < 177 Math.max(arrayOfExpectedRanges.length, gBeforeinput[0].cachedRanges.length); 178 i++ 179 ) { 180 assert_equals( 181 getRangeDescription(gBeforeinput[0].cachedRanges[i]), 182 getRangeDescription(arrayOfExpectedRanges[i]), 183 `gBeforeinput[0].getTargetRanges()[${i}] should return expected range (inputType is "${gBeforeinput[0].inputType}")` 184 ); 185 } 186 assert_equals( 187 gBeforeinput[0].cachedRanges.length, 188 arrayOfExpectedRanges.length, 189 `getTargetRanges() of beforeinput event should return ${arrayOfExpectedRanges.length} ranges` 190 ); 191 } 192 193 function checkGetTargetRangesOfInputOnDeleteSomething() { 194 assert_equals( 195 gInput.length, 196 1, 197 "One input event should be fired if the key operation deletes something" 198 ); 199 // https://github.com/w3c/input-events/issues/113 200 assert_true( 201 Array.isArray(gInput[0].cachedRanges), 202 "gInput[0].getTargetRanges() should return an array of StaticRange instances at least during propagation" 203 ); 204 assert_equals( 205 gInput[0].cachedRanges.length, 206 0, 207 "gInput[0].getTargetRanges() should return empty array during propagation" 208 ); 209 } 210 211 function checkGetTargetRangesOfInputOnDoNothing() { 212 assert_equals( 213 gInput.length, 214 0, 215 "input event shouldn't be fired when the key operation does not cause modifying the DOM tree" 216 ); 217 } 218 219 function checkBeforeinputAndInputEventsOnNOOP() { 220 assert_equals( 221 gBeforeinput.length, 222 0, 223 "beforeinput event shouldn't be fired when the key operation does not cause modifying the DOM tree" 224 ); 225 assert_equals( 226 gInput.length, 227 0, 228 "input event shouldn't be fired when the key operation does not cause modifying the DOM tree" 229 ); 230 } 231 232 function checkEditorContentResultAsSubTest( 233 expectedResult, 234 description, 235 options = {} 236 ) { 237 test(() => { 238 if (Array.isArray(expectedResult)) { 239 assert_in_array( 240 options.ignoreWhiteSpaceDifference 241 ? gEditor.innerHTML.replace(/ /g, " ") 242 : gEditor.innerHTML, 243 expectedResult 244 ); 245 } else { 246 assert_equals( 247 options.ignoreWhiteSpaceDifference 248 ? gEditor.innerHTML.replace(/ /g, " ") 249 : gEditor.innerHTML, 250 expectedResult 251 ); 252 } 253 }, `${description} - comparing innerHTML`); 254 } 255 256 // Similar to `setupDiv` in editing/include/tests.js, this method sets 257 // innerHTML value of gEditor, and sets multiple selection ranges specified 258 // with the markers. 259 // - `[` specifies start boundary in a text node 260 // - `{` specifies start boundary before a node 261 // - `]` specifies end boundary in a text node 262 // - `}` specifies end boundary after a node 263 function setupEditor(innerHTMLWithRangeMarkers) { 264 const startBoundaries = innerHTMLWithRangeMarkers.match(/\{|\[/g) || []; 265 const endBoundaries = innerHTMLWithRangeMarkers.match(/\}|\]/g) || []; 266 if (startBoundaries.length !== endBoundaries.length) { 267 throw "Should match number of open/close markers"; 268 } 269 270 gEditor.innerHTML = innerHTMLWithRangeMarkers; 271 gEditor.focus(); 272 273 if (startBoundaries.length === 0) { 274 // Don't remove the range for now since some tests may assume that 275 // setting innerHTML does not remove all selection ranges. 276 return; 277 } 278 279 function getNextRangeAndDeleteMarker(startNode) { 280 function getNextLeafNode(node) { 281 function inclusiveDeepestFirstChildNode(container) { 282 while (container.firstChild) { 283 container = container.firstChild; 284 } 285 return container; 286 } 287 if (node.hasChildNodes()) { 288 return inclusiveDeepestFirstChildNode(node); 289 } 290 if (node.nextSibling) { 291 return inclusiveDeepestFirstChildNode(node.nextSibling); 292 } 293 let nextSibling = (function nextSiblingOfAncestorElement(child) { 294 for ( 295 let parent = child.parentElement; 296 parent && parent != gEditor; 297 parent = parent.parentElement 298 ) { 299 if (parent.nextSibling) { 300 return parent.nextSibling; 301 } 302 } 303 return null; 304 })(node); 305 if (!nextSibling) { 306 return null; 307 } 308 return inclusiveDeepestFirstChildNode(nextSibling); 309 } 310 function scanMarkerInTextNode(textNode, offset) { 311 return /[\{\[\]\}]/.exec(textNode.data.substr(offset)); 312 } 313 let startMarker = (function scanNextStartMaker( 314 startContainer, 315 startOffset 316 ) { 317 function scanStartMakerInTextNode(textNode, offset) { 318 let scanResult = scanMarkerInTextNode(textNode, offset); 319 if (scanResult === null) { 320 return null; 321 } 322 if (scanResult[0] === "}" || scanResult[0] === "]") { 323 throw "An end marker is found before a start marker"; 324 } 325 return { 326 marker: scanResult[0], 327 container: textNode, 328 offset: scanResult.index + offset 329 }; 330 } 331 if (startContainer.nodeType === Node.TEXT_NODE) { 332 let scanResult = scanStartMakerInTextNode(startContainer, startOffset); 333 if (scanResult !== null) { 334 return scanResult; 335 } 336 } 337 let nextNode = startContainer; 338 while ((nextNode = getNextLeafNode(nextNode))) { 339 if (nextNode.nodeType === Node.TEXT_NODE) { 340 let scanResult = scanStartMakerInTextNode(nextNode, 0); 341 if (scanResult !== null) { 342 return scanResult; 343 } 344 continue; 345 } 346 } 347 return null; 348 })(startNode, 0); 349 if (startMarker === null) { 350 return null; 351 } 352 let endMarker = (function scanNextEndMarker(startContainer, startOffset) { 353 function scanEndMarkerInTextNode(textNode, offset) { 354 let scanResult = scanMarkerInTextNode(textNode, offset); 355 if (scanResult === null) { 356 return null; 357 } 358 if (scanResult[0] === "{" || scanResult[0] === "[") { 359 throw "A start marker is found before an end marker"; 360 } 361 return { 362 marker: scanResult[0], 363 container: textNode, 364 offset: scanResult.index + offset 365 }; 366 } 367 if (startContainer.nodeType === Node.TEXT_NODE) { 368 let scanResult = scanEndMarkerInTextNode(startContainer, startOffset); 369 if (scanResult !== null) { 370 return scanResult; 371 } 372 } 373 let nextNode = startContainer; 374 while ((nextNode = getNextLeafNode(nextNode))) { 375 if (nextNode.nodeType === Node.TEXT_NODE) { 376 let scanResult = scanEndMarkerInTextNode(nextNode, 0); 377 if (scanResult !== null) { 378 return scanResult; 379 } 380 continue; 381 } 382 } 383 return null; 384 })(startMarker.container, startMarker.offset + 1); 385 if (endMarker === null) { 386 throw "Found an open marker, but not found corresponding close marker"; 387 } 388 function indexOfContainer(container, child) { 389 let offset = 0; 390 for (let node = container.firstChild; node; node = node.nextSibling) { 391 if (node == child) { 392 return offset; 393 } 394 offset++; 395 } 396 throw "child must be a child node of container"; 397 } 398 (function deleteFoundMarkers() { 399 function removeNode(node) { 400 let container = node.parentElement; 401 let offset = indexOfContainer(container, node); 402 node.remove(); 403 return { container, offset }; 404 } 405 if (startMarker.container == endMarker.container) { 406 // If the text node becomes empty, remove it and set collapsed range 407 // to the position where there is the text node. 408 if (startMarker.container.length === 2) { 409 if (!/[\[\{][\]\}]/.test(startMarker.container.data)) { 410 throw `Unexpected text node (data: "${startMarker.container.data}")`; 411 } 412 let { container, offset } = removeNode(startMarker.container); 413 startMarker.container = endMarker.container = container; 414 startMarker.offset = endMarker.offset = offset; 415 startMarker.marker = endMarker.marker = ""; 416 return; 417 } 418 startMarker.container.data = `${startMarker.container.data.substring( 419 0, 420 startMarker.offset 421 )}${startMarker.container.data.substring( 422 startMarker.offset + 1, 423 endMarker.offset 424 )}${startMarker.container.data.substring(endMarker.offset + 1)}`; 425 if (startMarker.offset >= startMarker.container.length) { 426 startMarker.offset = endMarker.offset = startMarker.container.length; 427 return; 428 } 429 endMarker.offset--; // remove the start marker's length 430 if (endMarker.offset > endMarker.container.length) { 431 endMarker.offset = endMarker.container.length; 432 } 433 return; 434 } 435 if (startMarker.container.length === 1) { 436 let { container, offset } = removeNode(startMarker.container); 437 startMarker.container = container; 438 startMarker.offset = offset; 439 startMarker.marker = ""; 440 } else { 441 startMarker.container.data = `${startMarker.container.data.substring( 442 0, 443 startMarker.offset 444 )}${startMarker.container.data.substring(startMarker.offset + 1)}`; 445 } 446 if (endMarker.container.length === 1) { 447 let { container, offset } = removeNode(endMarker.container); 448 endMarker.container = container; 449 endMarker.offset = offset; 450 endMarker.marker = ""; 451 } else { 452 endMarker.container.data = `${endMarker.container.data.substring( 453 0, 454 endMarker.offset 455 )}${endMarker.container.data.substring(endMarker.offset + 1)}`; 456 } 457 })(); 458 (function handleNodeSelectMarker() { 459 if (startMarker.marker === "{") { 460 if (startMarker.offset === 0) { 461 // The range start with the text node. 462 let container = startMarker.container.parentElement; 463 startMarker.offset = indexOfContainer( 464 container, 465 startMarker.container 466 ); 467 startMarker.container = container; 468 } else if (startMarker.offset === startMarker.container.data.length) { 469 // The range start after the text node. 470 let container = startMarker.container.parentElement; 471 startMarker.offset = 472 indexOfContainer(container, startMarker.container) + 1; 473 startMarker.container = container; 474 } else { 475 throw 'Start marker "{" is allowed start or end of a text node'; 476 } 477 } 478 if (endMarker.marker === "}") { 479 if (endMarker.offset === 0) { 480 // The range ends before the text node. 481 let container = endMarker.container.parentElement; 482 endMarker.offset = indexOfContainer(container, endMarker.container); 483 endMarker.container = container; 484 } else if (endMarker.offset === endMarker.container.data.length) { 485 // The range ends with the text node. 486 let container = endMarker.container.parentElement; 487 endMarker.offset = 488 indexOfContainer(container, endMarker.container) + 1; 489 endMarker.container = container; 490 } else { 491 throw 'End marker "}" is allowed start or end of a text node'; 492 } 493 } 494 })(); 495 let range = document.createRange(); 496 range.setStart(startMarker.container, startMarker.offset); 497 range.setEnd(endMarker.container, endMarker.offset); 498 return range; 499 } 500 501 let ranges = []; 502 for ( 503 let range = getNextRangeAndDeleteMarker(gEditor.firstChild); 504 range; 505 range = getNextRangeAndDeleteMarker(range.endContainer) 506 ) { 507 ranges.push(range); 508 } 509 510 gSelection.removeAllRanges(); 511 for (let range of ranges) { 512 gSelection.addRange(range); 513 } 514 515 if (gSelection.rangeCount != ranges.length) { 516 throw `Failed to set selection to the given ranges whose length is ${ranges.length}, but only ${gSelection.rangeCount} ranges are added`; 517 } 518 }