InputEvents L2 tests

Test browser quirks for input events

by Isaac Nygaard

HTML

<h3>Test cases:</h3>
<div contenteditable=true>
  <b>text te</b><i>ex</i>
</div>
<div contenteditable=true>
  <span class='b'>text te</span><span class='i'>ex</span>
</div>
<div contenteditable=true>
  text te<u>ex text</u>
</div>
<div contenteditable=true>
  text te<span class='u'>ex text</span>
</div>

<h3>Events:</h3>
<label>Log limit: <input type=number min=1 max=999 inc=1 value=20></label>
<label><input type=checkbox>Cancel <code>beforeinput</code></label>
<output></output>

CSS

/* Test cases */
div[contenteditable]{
  border: 1px dotted black;
  margin: 5px;
  padding: 5px;
}
.b { font-weight: bold; }
.u { text-decoration: underline; }
.i { font-style: italic; }

/* Event output log */
output{
  display: block;
  font-family: monospace;
  white-space: pre;
  font-size: 90%;
  padding: 5px;
  margin: 5px;
  border: 1px solid blue;
}
output ul{
	margin:2px 0 2px 30px;
	padding:0;
}
output pre{
	display: inline-block;
	margin:.05em;
}
label{ display: block; }

/* HTML serialzation styles */
pre{
  border: 0.1em solid #704E05;
  margin: 0.5em;
  padding: 0.2em .5em;
  background-color: #FFF3DB;
  white-space: pre-wrap;
}
pre .tag{ font-weight: bold; }
pre .tag_attr{ font-style: italic; }
pre .text{}
pre .text::before{
	content: '#';
	color: #12B46E;
	font-weight: bold;
}
pre [data-range]::before{
	color: #7C00F9;
	font-weight: 900;
}
pre [data-range=start]::before{
	content: '「';
}
pre [data-range=end]::before{
	content: '」';
	vertical-align: -20%;
}
pre [data-range=collapsed]::before{
	content: '|';
}
pre span[data-sid]::after{
	content: attr(data-sid);
	color: #12B46E;
	font-size: 70%;
	vertical-align: sub;
}

JavaScript

const output = document.querySelector("output");
const divs = document.querySelectorAll("div");
const [count, cancel] = document.querySelectorAll("input");

// Keep a log of last N events, in reverse temporal order
function log(html) {
  if (typeof html === "string") {
    const cont = document.createElement("div");
    cont.innerHTML = html;
    html = cont;
  }
  output.prepend(html);
  while (output.children.length > count.valueAsNumber)
    output.lastElementChild.remove();
}

// Print div's html with some extra coloring/labeling features
class HTMLSerialize {
  // divisions in hue, when rotating colors for next element
  static HUE_DIVS = 7;
  // attributes to serialize; {js attribute -> html attribute}
  static SHOW_ATTRS = {
    className: "class"
  };
  // singelton tags/eleemnts
  static SINGLETON = new Set(["br", "hr", "wbr", "col", "command", "img"]);
  // counter for unique ids and colors; {container -> {id/hue: int}
  static counter = new Map();

  /**
   * @param src source element to serialize
   * @param target where to render the serialization
   * @param anchors output of ranges2anchors
   */
  constructor(src, target, anchors = null) {
    this.src = src;
    if (!HTMLSerialize.counter.has(src))
      HTMLSerialize.counter.set(src, {
        id: 0,
        hue: 0
      });
    this.frag = document.createDocumentFragment();
    this.serialize_recursive(src, anchors, true);
    target.replaceChildren(this.frag);
  }

  /** Give each node a unqiue id, and each element a hue. Can be used
  	to indicate whether a node was recreated/destroyed
  */
  assign_id(el) {
    if (typeof el.serialization === "undefined") {
      let c = HTMLSerialize.counter.get(this.src);
      el.serialization = Object.assign({}, c);
      c.id++;
      if (el.nodeType == Node.ELEMENT_NODE)
        c.hue = (c.hue + 360 / (HTMLSerialize.HUE_DIVS + 0.5)) % 360;
    }
  }

  add_cursor(cursor, root = null) {
    const s = document.createElement("span");
    s.dataset.range =...