tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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(/&nbsp;/g, " ")
    242          : gEditor.innerHTML,
    243        expectedResult
    244      );
    245    } else {
    246      assert_equals(
    247        options.ignoreWhiteSpaceDifference
    248          ? gEditor.innerHTML.replace(/&nbsp;/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 }