Start working again on queryCommand*()
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Thu, 23 Jun 2011 14:43:44 -0600
changeset 316 503076bbf557
parent 315 1480f4a5da74
child 317 067950bfb8f5
Start working again on queryCommand*()
editcommands.html
extramethods.html
implementation.js
source.html
--- a/editcommands.html	Thu Jun 23 14:43:28 2011 -0600
+++ b/editcommands.html	Thu Jun 23 14:43:44 2011 -0600
@@ -330,6 +330,15 @@
 and <var title="">value</var> parameters, even if specified, are ignored except where
 otherwise stated.
 
+<p>The <dfn id=querycommandenabled() title=queryCommandEnabled()><code>queryCommandEnabled(<var title="">command</var>)</code></dfn>
+method on the <code class=external data-anolis-spec=html><a href=http://www.whatwg.org/html/#htmldocument>HTMLDocument</a></code> interface allows
+scripts to ask whether calling <code><a href=#execcommand()>execCommand()</a></code> would have any
+effect.
+
+<p class=XXX>The <dfn id=querycommandindeterm() title=queryCommandIndeterm()><code>queryCommandIndeterm(<var title="">command</var>)</code></dfn>
+method on the <code class=external data-anolis-spec=html><a href=http://www.whatwg.org/html/#htmldocument>HTMLDocument</a></code> interface is a
+useless method that always returns false.  I think.
+
 <p>The <dfn id=querycommandstate() title=queryCommandState()><code>queryCommandState(<var title="">command</var>)</code></dfn>
 method on the <code class=external data-anolis-spec=html><a href=http://www.whatwg.org/html/#htmldocument>HTMLDocument</a></code> interface allows
 scripts to ask true-or-false status questions about the current selection, such
@@ -352,9 +361,11 @@
 specification.
 
 <p><a href=#command title=command>Commands</a> may have an associated
-<dfn id=action>action</dfn>, <dfn id=state>state</dfn>, <dfn id=value>value</dfn>, and/or <dfn id=relevant-css-property>relevant
-CSS property</dfn>.  If not otherwise specified, the <a href=#action>action</a> for a
-<a href=#command>command</a> is to do nothing, the <a href=#state>state</a> is false, the
+<dfn id=action>action</dfn>, <dfn id=enabled-flag>enabled flag</dfn>, <dfn id=indeterminate-flag>indeterminate flag</dfn>,
+<dfn id=state>state</dfn>, <dfn id=value>value</dfn>, and/or <dfn id=relevant-css-property>relevant CSS property</dfn>.
+If not otherwise specified, the <a href=#action>action</a> for a <a href=#command>command</a>
+is to do nothing, the <a href=#enabled-flag>enabled flag</a> is true, the
+<a href=#indeterminate-flag>indeterminate flag</a> is false, the <a href=#state>state</a> is false, the
 <a href=#value>value</a> is the empty string, and the <a href=#relevant-css-property>relevant CSS
 property</a> is null.
 <!--
@@ -379,6 +390,12 @@
 <var title="">command</var>, with <var title="">showUI</var> and <var title="">value</var> passed to the
 instructions as arguments.
 
+<p>When <code><a href=#querycommandenabled()>queryCommandEnabled()</a></code> is invoked, the user agent must
+return the <a href=#enabled-flag>enabled flag</a> for <var title="">command</var>.
+
+<p>When <code><a href=#querycommandindeterm()>queryCommandIndeterm()</a></code> is invoked, the user agent must
+return the <a href=#indeterminate-flag>indeterminate flag</a> for <var title="">command</var>.
+
 <p>When <code><a href=#querycommandstate()>queryCommandState()</a></code> is invoked, the user agent must return
 the <a href=#state>state</a> for <var title="">command</var>.
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extramethods.html	Thu Jun 23 14:43:44 2011 -0600
@@ -0,0 +1,193 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Auto-running queryCommand*() tests</title>
+<link rel=stylesheet href=tests.css>
+<style>
+#toolbar { display: none }
+body > div > table > tbody > tr { display: table-row !important }
+.yes, .no, .maybe { font-size: 1em }
+body > div > table > tbody > tr > td,
+body > div > table > tbody > tr > th {
+	width: 15%;
+}
+body > div > table > tbody > tr > td[rowspan],
+body > div > table > tbody > tr > th:first-child {
+	width: 50%;
+}
+body > div > table > tbody > tr > td:last-child,
+body > div > table > tbody > tr > th:last-child {
+	width: 5%;
+}
+</style>
+<p>Legend: {[ are the selection anchor, }] are the selection focus, {}
+represent an element boundary point, [] represent a text node boundary point.
+Syntax and some of the tests taken from <a
+href=http://www.browserscope.org/richtext2/test>Browserscope</a>.  data-start
+and data-end attributes also represent element boundary points, with the node
+being the element with the attribute and the offset given as the attribute
+value, for cases where HTML parsing doesn't allow text nodes.  Currently we
+don't really pay attention to reversed selections at all, so they might get
+displayed as forwards or such.
+
+<h1>Table of Contents</h1>
+<ul>
+</ul>
+
+<script src=implementation.js></script>
+<script src=tests.js></script>
+<script>
+"use strict";
+// Set up all the HTML automatically so I can add new commands to test without
+// copy-pasting in five places
+(function() {
+	var toc = document.querySelector("ul");
+	for (var command in tests) {
+		var li = document.createElement("li");
+		li.innerHTML = "<a href=#" + command + ">" + command + "</a>";
+		toc.appendChild(li);
+
+		var div = document.createElement("div");
+		div.id = command;
+		div.innerHTML = "<h1>" + command + "</h1>"
+			+ "<button onclick=\"runTests('" + command + "')\">Run tests</button>"
+			+ (command in notes ? "<p>" + notes[command] : "")
+			+ "<table border=1><tr><th>Input <th>Feature <th>Spec <th>Browser <th>Same?</table>"
+			+ "<p><label>New test input: <input></label>"
+			+ "<button onclick=\"addTest('" + command + "')\">Add test</button>";
+		document.body.appendChild(div);
+	}
+})();
+
+function runTests(command) {
+    var runTestsButton = document.querySelector("#" + command + " button");
+    if (runTestsButton.textContent != "Run tests") {
+        return;
+    }
+    runTestsButton.parentNode.removeChild(runTestsButton);
+
+	var addTestButton = document.querySelector("#" + command + " button");
+	var input = document.getElementById(command).getElementsByTagName("input")[0];
+	for (var i = 0; i < tests[command].length; i++) {
+		// This code actually focuses and clicks everything because for some
+		// reason, anything else doesn't work in IE9 . . .
+		//
+		// In case the input contains something unpleasant like newlines, we
+		// smuggle in the string as a data attribute, not just the value.  This
+		// is probably unnecessarily magic.
+		if (typeof tests[command][i] == "string") {
+			input.value = tests[command][i];
+			input.setAttribute("data-input", tests[command][i]);
+		} else {
+			input.value = tests[command][i][1];
+			input.setAttribute("data-input", tests[command][i][1]);
+		}
+		input.focus();
+		addTestButton.click();
+	}
+	input.value = "";
+
+	document.querySelector("#" + command).scrollIntoView();
+}
+
+function addTest(command) {
+	var tr = doSetup("#" + command + " > table", 0);
+
+	var test;
+	var input = document.getElementById(command).getElementsByTagName("input")[0];
+
+	if (input.hasAttribute("data-input")) {
+		test = input.getAttribute("data-input");
+	} else {
+		test = input.value;
+	}
+	input.removeAttribute("data-input");
+
+	var inputCell = document.createElement("td");
+	var points = setupCell(inputCell, test);
+	inputCell.rowSpan = 4;
+	tr.appendChild(inputCell);
+	inputCell.firstChild.contentEditable = "true";
+	inputCell.firstChild.spellcheck = false;
+
+	compareMethods(tr, command, points, "Enabled", myQueryCommandEnabled, document.queryCommandEnabled);
+
+	var newTr = document.createElement("tr");
+	tr.parentNode.appendChild(newTr);
+	compareMethods(newTr, command, points, "Indeterm", myQueryCommandIndeterm, document.queryCommandIndeterm);
+
+	newTr = document.createElement("tr");
+	tr.parentNode.appendChild(newTr);
+	compareMethods(newTr, command, points, "State", myQueryCommandState, document.queryCommandState);
+
+	newTr = document.createElement("tr");
+	tr.parentNode.appendChild(newTr);
+	compareMethods(newTr, command, points, "Value", myQueryCommandValue, document.queryCommandValue);
+
+	inputCell.firstChild.contentEditable = "inherit";
+	inputCell.firstChild.removeAttribute("spellcheck");
+	inputCell.firstChild.innerHTML = test;
+	inputCell.lastChild.textContent = inputCell.firstChild.innerHTML;
+}
+
+function compareMethods(tr, command, points, label, specFn, browserFn) {
+	var labelCell = document.createElement("th");
+	labelCell.textContent = label;
+	tr.appendChild(labelCell);
+
+	var specCell = document.createElement("td");
+	tr.appendChild(specCell);
+
+	var specResult;
+	try {
+		var range = document.createRange();
+		range.setStart(points[0], points[1]);
+		range.setEnd(points[2], points[3]);
+		if (range.collapsed) {
+			range.setEnd(points[0], points[1]);
+		}
+
+		specResult = specFn(command, range);
+	} catch (e) {
+		specCell.textContent = "Exception: " + e;
+		if (typeof e == "object" && "stack" in e) {
+			specCell.textContent += " (stack: " + e.stack + ")";
+		}
+	}
+
+	if (typeof specResult != "undefined") {
+		specCell.textContent = typeof specResult + ' "' + specResult + '"';
+	}
+
+
+	var browserCell = document.createElement("td");
+	tr.appendChild(browserCell);
+
+	var browserResult;
+	try {
+		setSelection(points[0], points[1], points[2], points[3]);
+		browserResult = browserFn.call(document, command);
+	} catch (e) {
+		browserCell.textContent = "Exception: " + e;
+		if (typeof e == "object" && "stack" in e) {
+			browserCell.textContent += " (stack: " + e.stack + ")";
+		}
+	}
+
+	getSelection().removeAllRanges();
+
+	if (typeof browserResult != "undefined") {
+		browserCell.textContent = typeof browserResult + ' "' + browserResult + '"';
+	}
+
+
+	var sameCell = document.createElement("td");
+	tr.appendChild(sameCell);
+	if (specResult === browserResult) {
+		sameCell.className = "yes";
+		sameCell.textContent = "\u2713";
+	} else {
+		sameCell.className = "no";
+		sameCell.textContent = "\u2717";
+	}
+}
+</script>
--- a/implementation.js	Thu Jun 23 14:43:28 2011 -0600
+++ b/implementation.js	Thu Jun 23 14:43:44 2011 -0600
@@ -483,9 +483,7 @@
 ///// Methods of the HTMLDocument interface /////
 /////////////////////////////////////////////////
 //@{
-function myExecCommand(command, showUI, value, range) {
-	command = command.toLowerCase();
-
+function setupEditCommandMethod(command, range) {
 	if (typeof range != "undefined") {
 		globalRange = range;
 	} else {
@@ -495,58 +493,76 @@
 	// "If the active range is null, all commands must behave as though they
 	// were not defined except those in the miscellaneous commands section."
 	if (!globalRange && command != "selectall" && command != "stylewithcss" && command != "usecss") {
-		return;
-	}
-
-	if (!(command in commands)) {
-		return;
-	}
-
-	commands[command].action(value);
-
-	globalRange = null;
-}
-
-function myQueryCommandState(command) {
-	command = command.toLowerCase();
-
-	if (typeof range != "undefined") {
-		globalRange = range;
-	} else {
-		globalRange = getActiveRange();
-	}
-
-	if (!globalRange && command != "selectall" && command != "stylewithcss" && command != "usecss") {
-		return;
+		return false;
 	}
 
 	if (!(command in commands)) {
-		return;
-	}
-
-	return commands[command].state();
+		return false;
+	}
+
+	return true;
+}
+
+function myExecCommand(command, showUI, value, range) {
+	command = command.toLowerCase();
+
+	if (setupEditCommandMethod(command, range)) {
+		commands[command].action(value);
+	}
 
 	globalRange = null;
 }
 
-function myQueryCommandValue(command) {
+function myQueryCommandEnabled(command, range) {
 	command = command.toLowerCase();
 
-	if (typeof range != "undefined") {
-		globalRange = range;
+	if (setupEditCommandMethod(command, range)) {
+		return commands[command].enabled();
 	} else {
-		globalRange = getActiveRange();
-	}
-
-	if (!globalRange && command != "selectall" && command != "stylewithcss" && command != "usecss") {
-		return;
-	}
-
-	if (!(command in commands)) {
-		return;
-	}
-
-	return commands[command].value();
+		return false;
+	}
+
+	globalRange = null;
+}
+
+function myQueryCommandIndeterm(command, range) {
+	command = command.toLowerCase();
+
+	if (setupEditCommandMethod(command, range)) {
+		return commands[command].indeterm();
+	} else {
+		return false;
+	}
+
+	globalRange = null;
+}
+
+function myQueryCommandState(command, range) {
+	command = command.toLowerCase();
+
+	if (setupEditCommandMethod(command, range)) {
+		return commands[command].state();
+	} else {
+		// "If not otherwise specified, the action for a command is to do
+		// nothing, the state is false, the value is the empty string, and the
+		// relevant CSS property is null."
+		return false;
+	}
+
+	globalRange = null;
+}
+
+function myQueryCommandValue(command, range) {
+	command = command.toLowerCase();
+
+	if (setupEditCommandMethod(command, range)) {
+		return commands[command].value();
+	} else {
+		// "If not otherwise specified, the action for a command is to do
+		// nothing, the state is false, the value is the empty string, and the
+		// relevant CSS property is null."
+		return "";
+	}
 
 	globalRange = null;
 }
@@ -6304,6 +6320,12 @@
 		if (!("action" in commands[command])) {
 			commands[command].action = function() {};
 		}
+		if (!("enabled" in commands[command])) {
+			commands[command].enabled = function() { return true };
+		}
+		if (!("indeterm" in commands[command])) {
+			commands[command].indeterm = function() { return false };
+		}
 		if (!("state" in commands[command])) {
 			commands[command].state = function() { return false };
 		}
--- a/source.html	Thu Jun 23 14:43:28 2011 -0600
+++ b/source.html	Thu Jun 23 14:43:44 2011 -0600
@@ -265,6 +265,17 @@
 otherwise stated.
 
 <p>The <dfn
+title=queryCommandEnabled()><code>queryCommandEnabled(<var>command</var>)</code></dfn>
+method on the <code data-anolis-spec=html>HTMLDocument</code> interface allows
+scripts to ask whether calling <code>execCommand()</code> would have any
+effect.
+
+<p class=XXX>The <dfn
+title=queryCommandIndeterm()><code>queryCommandIndeterm(<var>command</var>)</code></dfn>
+method on the <code data-anolis-spec=html>HTMLDocument</code> interface is a
+useless method that always returns false.  I think.
+
+<p>The <dfn
 title=queryCommandState()><code>queryCommandState(<var>command</var>)</code></dfn>
 method on the <code data-anolis-spec=html>HTMLDocument</code> interface allows
 scripts to ask true-or-false status questions about the current selection, such
@@ -290,9 +301,11 @@
 specification.
 
 <p><span title=command>Commands</span> may have an associated
-<dfn>action</dfn>, <dfn>state</dfn>, <dfn>value</dfn>, and/or <dfn>relevant
-CSS property</dfn>.  If not otherwise specified, the <span>action</span> for a
-<span>command</span> is to do nothing, the <span>state</span> is false, the
+<dfn>action</dfn>, <dfn>enabled flag</dfn>, <dfn>indeterminate flag</dfn>,
+<dfn>state</dfn>, <dfn>value</dfn>, and/or <dfn>relevant CSS property</dfn>.
+If not otherwise specified, the <span>action</span> for a <span>command</span>
+is to do nothing, the <span>enabled flag</span> is true, the
+<span>indeterminate flag</span> is false, the <span>state</span> is false, the
 <span>value</span> is the empty string, and the <span>relevant CSS
 property</span> is null.
 <!--
@@ -317,6 +330,12 @@
 <var>command</var>, with <var>showUI</var> and <var>value</var> passed to the
 instructions as arguments.
 
+<p>When <code>queryCommandEnabled()</code> is invoked, the user agent must
+return the <span>enabled flag</span> for <var>command</var>.
+
+<p>When <code>queryCommandIndeterm()</code> is invoked, the user agent must
+return the <span>indeterminate flag</span> for <var>command</var>.
+
 <p>When <code>queryCommandState()</code> is invoked, the user agent must return
 the <span>state</span> for <var>command</var>.