Fire beforeinput/input events for execCommand()
authorAryeh Gregor <ayg@aryeh.name>
Fri, 24 Feb 2012 10:24:06 -0700
changeset 703 a089a5315642
parent 702 33d0413c3be8
child 704 4cacaa76c31a
Fire beforeinput/input events for execCommand()

Fixes: https://www.w3.org/Bugs/Public/show_bug.cgi?id=13118
Reported-By: Mathias Bynens
Reported-By: Ehsan Akhgari
Reported-By: Ryosuke Niwa
Reported-By: Annie Sullivan
Reported-By: Jonas Sicking
Reported-By: Ojan Vafai
conformancetest/event.html
editing.html
preprocess
source.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conformancetest/event.html	Fri Feb 24 10:24:06 2012 -0700
@@ -0,0 +1,270 @@
+<!doctype html>
+<title>Editing event tests</title>
+<script src=http://dvcs.w3.org/hg/html/raw-file/tip/tests/resources/testharness.js></script>
+<script src=http://dvcs.w3.org/hg/html/raw-file/tip/tests/resources/testharnessreport.js></script>
+<div id=test></div>
+<div id=log></div>
+<script>
+"use strict";
+
+var div = document.querySelector("#test");
+add_completion_callback(function() { div.parentNode.removeChild(div) });
+
+function copyEvent(e) {
+	var ret = {};
+	ret.original = e;
+	["type", "target", "currentTarget", "eventPhase", "bubbles", "cancelable",
+	"defaultPrevented", "isTrusted", "command", "value"].forEach(function(k) {
+		ret[k] = e[k];
+	});
+	return ret;
+}
+
+var tests = [
+	{
+		name: "Simple editable div",
+		html: "<div contenteditable>foo<b>bar</b>baz</div>",
+		initRange: function(range) {
+			range.setStart(div.querySelector("b").firstChild, 0);
+			range.setEnd(div.querySelector("b"), 1);
+		},
+		target: function() { return div.firstChild },
+		command: "bold",
+		value: "",
+	},
+	{
+		name: "Editable b",
+		html: "foo<b contenteditable>bar</b>baz",
+		initRange: function(range) {
+			range.setStart(div.querySelector("b").firstChild, 0);
+			range.setEnd(div.querySelector("b"), 1);
+		},
+		target: function() { return div.querySelector("b") },
+		command: "bold",
+		value: "",
+	},
+	{
+		name: "No editable content",
+		html: "foo<b>bar</b>baz",
+		initRange: function(range) {
+			range.setStart(div.querySelector("b").firstChild, 0);
+			range.setEnd(div.querySelector("b"), 1);
+		},
+		target: function() { return null },
+		command: "bold",
+		value: "",
+	},
+	{
+		name: "Partially-selected editable content",
+		html: "foo<b contenteditable>bar</b>baz",
+		initRange: function(range) {
+			range.setStart(div.querySelector("b").firstChild, 0);
+			range.setEnd(div, 3);
+		},
+		target: function() { return null },
+		command: "bold",
+		value: "",
+	},
+	{
+		name: "Selection spans two editing hosts",
+		html: "<div contenteditable>foo</div><div contenteditable>bar</div>",
+		initRange: function(range) {
+			range.setStart(div.querySelector("div").firstChild, 2);
+			range.setEnd(div.querySelector("div + div").firstChild, 1);
+		},
+		target: function() { return null },
+		command: "bold",
+		value: "",
+	},
+	{
+		name: "Selection includes two editing hosts",
+		html: "foo<div contenteditable>bar</div>baz<div contenteditable>quz</div>qoz",
+		initRange: function(range) {
+			range.setStart(div.firstChild, 2);
+			range.setEnd(div.lastChild, 1);
+		},
+		target: function() { return null },
+		command: "bold",
+		value: "",
+	},
+	{
+		name: "Changing selection from handler",
+		html: "<div contenteditable>foo</div><div contenteditable>bar</div>",
+		initRange: function(range) {
+			range.setStart(div.querySelector("div").firstChild, 0);
+			range.setEnd(div.querySelector("div").firstChild, 3);
+		},
+		target: function() { return div.firstChild },
+		finalTarget: function() { return div.lastChild },
+		beforeInputAction: function() {
+			getSelection().removeAllRanges();
+			var range = document.createRange();
+			range.setStart(div.querySelector("div + div").firstChild, 0);
+			range.setEnd(div.querySelector("div + div").firstChild, 3);
+			getSelection().addRange(range);
+		},
+		command: "bold",
+		value: "",
+	},
+];
+
+var commandTests = {
+	backColor: ["green"],
+	createLink: ["http://www.w3.org/community/editing/"],
+	fontName: ["serif", "Helvetica"],
+	fontSize: ["6", "15px"],
+	foreColor: ["green"],
+	hiliteColor: ["green"],
+	italic: [],
+	removeFormat: [],
+	strikeThrough: [],
+	subscript: [],
+	superscript: [],
+	underline: [],
+	unlink: [],
+	delete: [],
+	formatBlock: ["p"],
+	forwardDelete: [],
+	indent: [],
+	insertHorizontalRule: ["id"],
+	insertHTML: ["<b>hi</b>"],
+	insertImage: ["http://example.com/some-image"],
+	insertLineBreak: [],
+	insertOrderedList: [],
+	insertParagraph: [],
+	insertText: ["abc"],
+	insertUnorderedList: [],
+	justifyCenter: [],
+	justifyFull: [],
+	justifyLeft: [],
+	justifyRight: [],
+	outdent: [],
+	redo: [],
+	selectAll: [],
+	styleWithCSS: [],
+	undo: [],
+	useCSS: [],
+};
+
+Object.keys(commandTests).forEach(function(command) {
+	commandTests[command] = ["", "quasit"].concat(commandTests[command]);
+	commandTests[command].forEach(function(value) {
+		tests.push({
+			name: "Command " + command + ", value " + format_value(value),
+			html: "<div contenteditable>foo<b>bar</b>baz</div>",
+			initRange: function(range) {
+				range.setStart(div.querySelector("b").firstChild, 0);
+				range.setEnd(div.querySelector("b"), 1);
+			},
+			target: function() {
+				return ["redo", "selectAll", "styleWithCSS", "undo", "useCSS"]
+					.indexOf(command) == -1 ? div.firstChild : null;
+			},
+			command: command,
+			value: value,
+		});
+	});
+});
+
+tests.forEach(function(obj) {
+	[true, false].forEach(function(cancel) {
+		// Kill all event handlers first
+		var newDiv = div.cloneNode(false);
+		div.parentNode.insertBefore(newDiv, div);
+		div.parentNode.removeChild(div);
+		div = newDiv;
+
+		div.innerHTML = obj.html;
+
+		var originalContents = div.cloneNode(true);
+
+		getSelection().removeAllRanges();
+		var range = document.createRange();
+		obj.initRange(range);
+		getSelection().addRange(range);
+
+		var target = obj.target();
+		var finalTarget = "finalTarget" in obj ? obj.finalTarget() : target;
+		var command = obj.command;
+		var value = obj.value;
+
+		var beforeInputEvents = [];
+		var inputEvents = [];
+		div.addEventListener("beforeinput", function(e) {
+			var copied = copyEvent(e);
+			copied.inputEventsLength = inputEvents.length;
+			beforeInputEvents.push(copied);
+			if (cancel) {
+				e.preventDefault();
+			}
+			if ("beforeInputAction" in obj) {
+				obj.beforeInputAction();
+			}
+		});
+		div.addEventListener("input", function(e) { inputEvents.push(copyEvent(e)) });
+
+		// Uncomment this code instead of the execCommand() to make all the
+		// tests pass, as a sanity check
+		//var e = new Event("beforeinput", {bubbles: true, cancelable: true});
+		//e.command = command;
+		//e.value = value;
+		//var ret = target ? target.dispatchEvent(e) : false;
+		//if (ret) {
+		//	var e = new Event("input", {bubbles: true});
+		//	e.command = command;
+		//	e.value = value;
+		//	finalTarget.dispatchEvent(e);
+		//}
+
+		document.execCommand(command, false, value);
+
+		test(function() {
+			assert_equals(beforeInputEvents.length, target ? 1 : 0,
+				"number of beforeinput events fired");
+			if (beforeInputEvents.length == 0) {
+				assert_equals(inputEvents.length, 0, "number of input events fired");
+				return;
+			}
+			var e = beforeInputEvents[0];
+			assert_equals(e.inputEventsLength, 0, "number of input events fired");
+			assert_equals(e.type, "beforeinput", "event.type");
+			assert_equals(e.target, target, "event.target");
+			assert_equals(e.currentTarget, div, "event.currentTarget");
+			assert_equals(e.eventPhase, Event.BUBBLING_PHASE, "event.eventPhase");
+			assert_equals(e.bubbles, true, "event.bubbles");
+			assert_equals(e.cancelable, true, "event.cancelable");
+			assert_equals(e.defaultPrevented, false, "event.defaultPrevented");
+			assert_equals(e.isTrusted, true, "event.isTrusted");
+			assert_equals(e.command, command, "e.command");
+			assert_equals(e.value, value, "e.value");
+			assert_equals(Object.getPrototypeOf(e.original), EditingBeforeInputEvent,
+				"event prototype");
+			assert_true(originalContents.isEqualNode(div),
+				"div contents not yet changed");
+		}, obj.name + ": beforeinput event, " + (cancel ? "canceled" : "uncanceled"));
+
+		test(function() {
+			assert_equals(inputEvents.length, target && !cancel ? 1 : 0,
+				"number of input events fired");
+			if (!target || cancel) {
+				assert_true(originalContents.isEqualNode(div),
+					"div contents must not be changed");
+				return;
+			}
+			var e = inputEvents[0];
+			assert_equals(e.type, "input", "event.type");
+			assert_equals(e.target, finalTarget, "event.target");
+			assert_equals(e.currentTarget, div, "event.currentTarget");
+			assert_equals(e.eventPhase, Event.BUBBLING_PHASE, "event.eventPhase");
+			assert_equals(e.bubbles, true, "event.bubbles");
+			assert_equals(e.cancelable, false, "event.cancelable");
+			assert_equals(e.defaultPrevented, false, "event.defaultPrevented");
+			assert_equals(e.isTrusted, true, "event.isTrusted");
+			assert_equals(e.command, command, "e.command");
+			assert_equals(e.value, value, "e.value");
+			assert_equals(Object.getPrototypeOf(e.original), EditingInputEvent,
+				"event prototype");
+		}, obj.name + ": input event, " + (cancel ? "canceled" : "uncanceled"));
+	});
+});
+</script>
--- a/editing.html	Fri Feb 24 08:46:37 2012 -0700
+++ b/editing.html	Fri Feb 24 10:24:06 2012 -0700
@@ -1399,6 +1399,49 @@
 
 <h2 id=methods-to-query-and-execute-commands>Methods to query and execute commands</h2>
 
+<p class=XXX>We fire events as requested in
+<a href="https://www.w3.org/Bugs/Public/show_bug.cgi?id=13118">bug 13118</a>.
+This is a new feature does not currently match any browser.  <strong>If you are
+implementing this, please make sure to file any feedback as bugs.  The spec is
+not finalized yet and can still be easily changed.</strong>
+
+<pre class=idl>[Constructor(DOMString <var title="">type</var>, optional <a href=#editingbeforeinputeventinit>EditingBeforeInputEventInit</a> <var title="">eventInitDict</var>)]
+interface <dfn id=editingbeforeinputevent>EditingBeforeInputEvent</dfn> : <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#event>Event</a> {
+  readonly attribute DOMString <a href=#dom-editingbeforeinputevent-command title=dom-EditingBeforeInputEvent-command>command</a>;
+  readonly attribute DOMString <a href=#dom-editingbeforeinputevent-value title=dom-EditingBeforeInputEvent-value>value</a>;
+};
+
+dictionary <dfn id=editingbeforeinputeventinit>EditingBeforeInputEventInit</dfn> : <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#eventinit>EventInit</a> {
+  DOMString command;
+  DOMString value;
+};
+
+[Constructor(DOMString <var title="">type</var>, optional <a href=#editinginputeventinit>EditingInputEventInit</a> <var title="">eventInitDict</var>)]
+interface <dfn id=editinginputevent>EditingInputEvent</dfn> : <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#event>Event</a> {
+  readonly attribute DOMString <a href=#dom-editinginputevent-command title=dom-EditingInputEvent-command>command</a>;
+  readonly attribute DOMString <a href=#dom-editinginputevent-value title=dom-EditingInputEvent-value>value</a>;
+};
+
+dictionary <dfn id=editinginputeventinit>EditingInputEventInit</dfn> : <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#eventinit>EventInit</a> {
+  DOMString command;
+  DOMString value;
+};</pre>
+
+<p class=XXX>We have two different interfaces because we might want to add
+additional members to the input event but not the beforeinput event, such as a
+list of nodes that were affected.
+
+<p>When an <code><a href=#editingbeforeinputevent>EditingBeforeInputEvent</a></code> object is created, the
+<dfn id=dom-editingbeforeinputevent-command title=dom-EditingBeforeInputEvent-command><code>command</code></dfn> and
+<dfn id=dom-editingbeforeinputevent-value title=dom-EditingBeforeInputEvent-value><code>value</code></dfn>
+attributes must both be initialized to the empty string, unless otherwise
+specified.
+
+<p>When an <code><a href=#editinginputevent>EditingInputEvent</a></code> object is created, the
+<dfn id=dom-editinginputevent-command title=dom-EditingInputEvent-command><code>command</code></dfn> and
+<dfn id=dom-editinginputevent-value title=dom-EditingInputEvent-value><code>value</code></dfn> attributes must
+both be initialized to the empty string, unless otherwise specified.
+
 <p class=comments>TODO: Define behavior for <var title="">show UI</var>.
 
 <p>When the <dfn id=execcommand() title=execCommand()><code>execCommand(<var title="">command</var>,
@@ -1427,9 +1470,80 @@
 
   <p>If <var title="">command</var> is not <a href=#enabled>enabled</a>, return false.
 
+  <li>
+  <p>If <var title="">command</var> is not in the
+  <a href=#miscellaneous-commands>Miscellaneous commands</a> section:
+
+  <p class=XXX>We don't fire events for copy/cut/paste/undo/redo/selectAll
+  because they should all have their own events.  We don't fire events for
+  styleWithCSS/useCSS because it's not obvious where to fire them, or why
+  anyone would want them.  We don't fire events for unsupported commands,
+  because then if they became supported and were classified with the
+  miscellaneous events, we'd have to stop firing events for consistency's sake.
+
+  <ol>
+    <li>Let <var title="">affected editing host</var> be the <a href=#editing-host>editing host</a>
+    that is an <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-inclusive-ancestor title=concept-tree-inclusive-ancestor>inclusive ancestor</a> of the <a href=#active-range>active range</a>'s
+    <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-start-node title=concept-range-start-node>start node</a> and <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-end-node title=concept-range-end-node>end node</a>, and is not the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of any
+    <a href=#editing-host>editing host</a> that is an <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-inclusive-ancestor title=concept-tree-inclusive-ancestor>inclusive ancestor</a> of the
+    <a href=#active-range>active range</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-start-node title=concept-range-start-node>start node</a> and <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-end-node title=concept-range-end-node>end node</a>.
+
+    <p class=note>Such an editing host must exist, because otherwise the
+    command would not be <a href=#enabled>enabled</a>.
+
+    <li><a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-event-dispatch title=concept-event-dispatch>Dispatch</a> an <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-event title=concept-event>event</a> at <var title="">affected editing host</var> that uses
+    the <code><a href=#editingbeforeinputevent>EditingBeforeInputEvent</a></code> interface.  The event's
+    <code class=external data-anolis-spec=dom title=dom-Event-type><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-event-type>type</a></code> attribute must
+    be initialized to "beforeinput"; its
+    <code class=external data-anolis-spec=dom title=dom-Event-isTrusted><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-event-istrusted>isTrusted</a></code>,
+    <code class=external data-anolis-spec=dom title=dom-Event-bubbles><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-event-bubbles>bubbles</a></code>, and
+    <code class=external data-anolis-spec=dom title=dom-Event-cancelable><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-event-cancelable>cancelable</a></code>
+    attributes must be initialized to true; its
+    <code title=dom-EditingBeforeInputEvent-command><a href=#dom-editingbeforeinputevent-command>command</a></code> attribute
+    must be initialized to <var title="">command</var>; and its
+    <code title=dom-EditingBeforeInputEvent-value><a href=#dom-editingbeforeinputevent-value>value</a></code> attribute must
+    be initialized to <var title="">value</var>.
+
+    <li>If the value returned by the previous step is false, return false.
+
+    <li>If <var title="">command</var> is not <a href=#enabled>enabled</a>, return false.
+
+    <p class=XXX>We have to check again whether the command is enabled, because
+    the beforeinput handler might have done something annoying like
+    getSelection().removeAllRanges().
+
+    <li>Let <var title="">affected editing host</var> be the <a href=#editing-host>editing host</a>
+    that is an <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-inclusive-ancestor title=concept-tree-inclusive-ancestor>inclusive ancestor</a> of the <a href=#active-range>active range</a>'s
+    <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-start-node title=concept-range-start-node>start node</a> and <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-end-node title=concept-range-end-node>end node</a>, and is not the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of any
+    <a href=#editing-host>editing host</a> that is an <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-inclusive-ancestor title=concept-tree-inclusive-ancestor>inclusive ancestor</a> of the
+    <a href=#active-range>active range</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-start-node title=concept-range-start-node>start node</a> and <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-end-node title=concept-range-end-node>end node</a>.
+
+    <p class=XXX>This new affected editing host is what we'll fire the input
+    event at in a couple of lines.  We want to compute it beforehand just to be
+    safe: bugs in the command action might remove the selection or something
+    bad like that, and we don't want to have to handle it later.  We recompute
+    it after the beforeinput event is handled so that if the handler moves the
+    selection to some other editing host, the input event will be fired at the
+    editing host that was actually affected.
+  </ol>
+
   <li>Take the <a href=#action>action</a> for <var title="">command</var>, passing
   <var title="">value</var> to the instructions as an argument.
 
+  <li>If <var title="">command</var> is not in the
+  <a href=#miscellaneous-commands>Miscellaneous commands</a> section, then
+  <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-event-dispatch title=concept-event-dispatch>dispatch</a> an <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-event title=concept-event>event</a> at <var title="">affected editing host</var> that uses the
+  <code><a href=#editinginputevent>EditingInputEvent</a></code> interface.  The event's
+  <code class=external data-anolis-spec=dom title=dom-Event-type><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-event-type>type</a></code> attribute must be
+  initialized to "input"; its
+  <code class=external data-anolis-spec=dom title=dom-Event-isTrusted><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-event-istrusted>isTrusted</a></code> and
+  <code class=external data-anolis-spec=dom title=dom-Event-bubbles><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-event-bubbles>bubbles</a></code> attributes
+  must be initialized to true; its
+  <code title=dom-EditingInputEvent-command><a href=#dom-editinginputevent-command>command</a></code> attribute must be
+  initialized to <var title="">command</var>; and its
+  <code title=dom-EditingInputEvent-value><a href=#dom-editinginputevent-value>value</a></code> attribute must be
+  initialized to <var title="">value</var>.
+
   <li>Return true.
 </ol>
 
--- a/preprocess	Fri Feb 24 08:46:37 2012 -0700
+++ b/preprocess	Fri Feb 24 10:24:06 2012 -0700
@@ -38,6 +38,7 @@
     'descendant': '<span data-anolis-spec=dom title=concept-tree-descendant>descendant</span>',
     'directionality': '<span data-anolis-spec=html title="the directionality">directionality</span>',
     'div': '<code data-anolis-spec=html title="the div element">div</code>',
+    'dispatch': '<span data-anolis-spec=dom title=concept-event-dispatch>dispatch</span>',
     'dd': '<code data-anolis-spec=html title="the dd element">dd</code>',
     'dl': '<code data-anolis-spec=html title="the dl element">dl</code>',
     'dt': '<code data-anolis-spec=html title="the dt element">dt</code>',
@@ -51,6 +52,7 @@
     'endoffset': '<span data-anolis-spec=dom title=concept-range-end-offset>end offset</span>',
     'equivalent': '<span title="equivalent values">equivalent</span>',
     'extractcontents': '<code data-anolis-spec=dom title=dom-Range-extractContents>extractContents()</code>',
+    'event': '<span data-anolis-spec=dom title=concept-event>event</span>',
     'firstchild': '<code data-anolis-spec=dom title=dom-Node-firstChild>firstChild</code>',
     'followingsibling': '<span data-anolis-spec=dom title="concept-tree-following">following</span> <span data-anolis-spec=dom title=concept-tree-sibling>sibling</span>',
     'font': '<code data-anolis-spec=html title=font>font</code>',
--- a/source.html	Fri Feb 24 08:46:37 2012 -0700
+++ b/source.html	Fri Feb 24 10:24:06 2012 -0700
@@ -1365,6 +1365,50 @@
 
 <h2>Methods to query and execute commands</h2>
 <!-- @{ -->
+<p class=XXX>We fire events as requested in
+<a href=https://www.w3.org/Bugs/Public/show_bug.cgi?id=13118>bug 13118</a>.
+This is a new feature does not currently match any browser.  <strong>If you are
+implementing this, please make sure to file any feedback as bugs.  The spec is
+not finalized yet and can still be easily changed.</strong>
+
+<pre class=idl>
+[Constructor(DOMString <var>type</var>, optional <span>EditingBeforeInputEventInit</span> <var>eventInitDict</var>)]
+interface <dfn>EditingBeforeInputEvent</dfn> : <span data-anolis-spec=dom>Event</span> {
+  readonly attribute DOMString <span title=dom-EditingBeforeInputEvent-command>command</span>;
+  readonly attribute DOMString <span title=dom-EditingBeforeInputEvent-value>value</span>;
+};
+
+dictionary <dfn>EditingBeforeInputEventInit</dfn> : <span data-anolis-spec=dom>EventInit</span> {
+  DOMString command;
+  DOMString value;
+};
+
+[Constructor(DOMString <var>type</var>, optional <span>EditingInputEventInit</span> <var>eventInitDict</var>)]
+interface <dfn>EditingInputEvent</dfn> : <span data-anolis-spec=dom>Event</span> {
+  readonly attribute DOMString <span title=dom-EditingInputEvent-command>command</span>;
+  readonly attribute DOMString <span title=dom-EditingInputEvent-value>value</span>;
+};
+
+dictionary <dfn>EditingInputEventInit</dfn> : <span data-anolis-spec=dom>EventInit</span> {
+  DOMString command;
+  DOMString value;
+};</pre>
+
+<p class=XXX>We have two different interfaces because we might want to add
+additional members to the input event but not the beforeinput event, such as a
+list of nodes that were affected.
+
+<p>When an <code>EditingBeforeInputEvent</code> object is created, the
+<dfn title=dom-EditingBeforeInputEvent-command><code>command</code></dfn> and
+<dfn title=dom-EditingBeforeInputEvent-value><code>value</code></dfn>
+attributes must both be initialized to the empty string, unless otherwise
+specified.
+
+<p>When an <code>EditingInputEvent</code> object is created, the
+<dfn title=dom-EditingInputEvent-command><code>command</code></dfn> and
+<dfn title=dom-EditingInputEvent-value><code>value</code></dfn> attributes must
+both be initialized to the empty string, unless otherwise specified.
+
 <p class=comments>TODO: Define behavior for <var>show UI</var>.
 
 <p>When the <dfn title=execCommand()><code>execCommand(<var>command</var>,
@@ -1394,9 +1438,80 @@
 
   <p>If <var>command</var> is not <span>enabled</span>, return false.
 
+  <li>
+  <p>If <var>command</var> is not in the
+  <a href=#miscellaneous-commands>Miscellaneous commands</a> section:
+
+  <p class=XXX>We don't fire events for copy/cut/paste/undo/redo/selectAll
+  because they should all have their own events.  We don't fire events for
+  styleWithCSS/useCSS because it's not obvious where to fire them, or why
+  anyone would want them.  We don't fire events for unsupported commands,
+  because then if they became supported and were classified with the
+  miscellaneous events, we'd have to stop firing events for consistency's sake.
+
+  <ol>
+    <li>Let <var>affected editing host</var> be the <span>editing host</span>
+    that is an [[inclusiveancestor]] of the <span>active range</span>'s
+    [[startnode]] and [[endnode]], and is not the [[ancestor]] of any
+    <span>editing host</span> that is an [[inclusiveancestor]] of the
+    <span>active range</span>'s [[startnode]] and [[endnode]].
+
+    <p class=note>Such an editing host must exist, because otherwise the
+    command would not be <span>enabled</span>.
+
+    <li>[[Dispatch]] an [[event]] at <var>affected editing host</var> that uses
+    the <code>EditingBeforeInputEvent</code> interface.  The event's
+    <code data-anolis-spec=dom title=dom-Event-type>type</code> attribute must
+    be initialized to "beforeinput"; its
+    <code data-anolis-spec=dom title=dom-Event-isTrusted>isTrusted</code>,
+    <code data-anolis-spec=dom title=dom-Event-bubbles>bubbles</code>, and
+    <code data-anolis-spec=dom title=dom-Event-cancelable>cancelable</code>
+    attributes must be initialized to true; its
+    <code title=dom-EditingBeforeInputEvent-command>command</code> attribute
+    must be initialized to <var>command</var>; and its
+    <code title=dom-EditingBeforeInputEvent-value>value</code> attribute must
+    be initialized to <var>value</var>.
+
+    <li>If the value returned by the previous step is false, return false.
+
+    <li>If <var>command</var> is not <span>enabled</span>, return false.
+
+    <p class=XXX>We have to check again whether the command is enabled, because
+    the beforeinput handler might have done something annoying like
+    getSelection().removeAllRanges().
+
+    <li>Let <var>affected editing host</var> be the <span>editing host</span>
+    that is an [[inclusiveancestor]] of the <span>active range</span>'s
+    [[startnode]] and [[endnode]], and is not the [[ancestor]] of any
+    <span>editing host</span> that is an [[inclusiveancestor]] of the
+    <span>active range</span>'s [[startnode]] and [[endnode]].
+
+    <p class=XXX>This new affected editing host is what we'll fire the input
+    event at in a couple of lines.  We want to compute it beforehand just to be
+    safe: bugs in the command action might remove the selection or something
+    bad like that, and we don't want to have to handle it later.  We recompute
+    it after the beforeinput event is handled so that if the handler moves the
+    selection to some other editing host, the input event will be fired at the
+    editing host that was actually affected.
+  </ol>
+
   <li>Take the <span>action</span> for <var>command</var>, passing
   <var>value</var> to the instructions as an argument.
 
+  <li>If <var>command</var> is not in the
+  <a href=#miscellaneous-commands>Miscellaneous commands</a> section, then
+  [[dispatch]] an [[event]] at <var>affected editing host</var> that uses the
+  <code>EditingInputEvent</code> interface.  The event's
+  <code data-anolis-spec=dom title=dom-Event-type>type</code> attribute must be
+  initialized to "input"; its
+  <code data-anolis-spec=dom title=dom-Event-isTrusted>isTrusted</code> and
+  <code data-anolis-spec=dom title=dom-Event-bubbles>bubbles</code> attributes
+  must be initialized to true; its
+  <code title=dom-EditingInputEvent-command>command</code> attribute must be
+  initialized to <var>command</var>; and its
+  <code title=dom-EditingInputEvent-value>value</code> attribute must be
+  initialized to <var>value</var>.
+
   <li>Return true.
 </ol>