GeezerVison is showing w3c.github.io (truncated) — execCommand
This spec is incomplete and it is not expected that it will advance
beyond draft status. Authors should not use most of these features
directly, but instead use JavaScript editing libraries. The features
described in this document are not implemented consistently or fully by
user agents, and it is not expected that this will change in the
foreseeable future. There is currently no alternative to some
execCommand actions related to clipboard content and
contentEditable=true is often used to draw the caret and move the caret
in the block direction as well as a few minor subpoints. This spec is
to meant to help implementations in standardizing these existing
features. It is predicted that in the future both specs will be
replaced by Content Editable and
Input Events.
This document defines the behavior of the editing commands that can be
executed with execCommand.
Introduction
The APIs specified here were originally introduced in Microsoft's
Internet Explorer, but have subsequently been copied by other browsers
in a haphazard and imprecise fashion. Although the behavior specified
here does not exactly match any browser at the time of writing, it can
serve as a target to converge to in the future.
Where the reasoning behind the specification is of interest, such as
when major preexisting rendering engines are known not to match it, the
reasoning is available by clicking the "comments" button on the right
(requires JavaScript). If you have questions about why the
specification says something, check the comments first. They're
sometimes longer than the specification text itself, and commonly
record what the major browsers do and other essential information.
The principles used for writing this reference list are:
- If all browsers that implement a particular feature agree on some
detail of how it works, match them unless there's very good reason not
to. When it's not clear what behavior is best, try to follow the
implementations with the most market share. But if one browser's
behavior is clearly better than the others', go with the better
behavior.
- If a command is issued to format some text in a particular way, we
will format the text that way no matter what. If the user clicks the
"bold" button, they don't care that the text didn't become bold because
of an external CSS rule or for any other reason, they only care that it
didn't work. The only exception (beyond where it's simply impossible,
like propagated text-decorations we can't remove) is that we don't try
to override !important rules from external stylesheets, although we
also don't go out of our way to respect them.
- When we're given a presentational command like "bold", don't modify
anything other than presentational markup related to that command. If
an element has non-presentational attributes like id or class, don't
split it up or remove it or anything. At most convert it to a span, if
it's some type of presentational element. ("Presentational" here really
means "browsers produce it in response to execCommand() so we need to
treat it as presentational", so it includes things like [^strong^] and
[^em^].) Of course, in some cases we have to remove elements, like when
merging two blocks.
- Don't interfere with more markup than necessary. If the user
modifies only a small run of text, don't go around simplifying
ancestors or siblings or whatever unless it's necessary to produce
simpler markup in the place that was actually modified.
- But if we are already changing around something's style, convert
existing styles to the preferred format. For instance, we use
<b> for bold (if the CSS styling flag is
false), and convert <strong> and
<span style="font-weight: bold"> if we
happen to be modifying that node anyway.
- Try not to make the document less conforming than it originally
was. If we happen to make it more conforming, good, although we don't
have to go out of our way to do that. In some cases we do make the
document less conforming, generally because there's some clear use-case
that requires it or because it matches existing browsers behavior. (For
instance, see the styleWithCSS = false mode, and the fact that
insertImage doesn't add an alt attribute, etc.)
- Keep the markup as concise as possible. (I've received feedback
that this is very important to authors.) Ideally, the markup should
look as simple and neat as what a human would have produced by
hand-editing. We do complicated manipulation to pull styles down from
ancestors rather than having to use inline CSS, and make sure to tidy
up any styles on elements that we happen to be modifying anyway.
Previous principles take precedence over this one, however.
Tests
Tests can be found here.
Issues
This specification is mostly feature-complete. It should be considered
mostly stable and awaiting implementater review and feedback.
Significant known issues that I need feedback on, or otherwise am not
planning to fix just yet:
- Need to make CSS terminology more precise, about setting/unsetting
CSS properties. The intent is to modify the style attribute,
CSSOM-style. Suggestions appreciated on how I should spec this.
- I use resolved value
instead of computed or used or anything like that, just because that's
what my test implementation uses (via getComputedStyle). This is not
necessarily the best actual choice: if it should be something else,
please tell me.
-
I haven't paid much attention to performance. The algorithms here
aren't performance-critical in most cases, but I might have
accidentally included some algorithms that are too slow anyway on
large pages. Generally I haven't worried about throwing nodes away
and recreating them multiple times or things like that, as long as
it produces the correct result.
If it would be useful to implementers for me to spend time and spec
complexity on avoiding some of the masses of useless operations
that are currently required, please say so. All intermediate DOM
states are black-box detectable via mutation events or whatever
their replacement will be, so implementers theoretically can't
optimize most of this stuff too much themselves, but in practice I
doubt anyone will rely on the intermediate DOM states much.
- [^br^]s are a nightmare. I have tons of hacks all over the place
which are totally wrong, mostly to account for the fact that sometimes
[^br^]s do nothing and we need to treat that case magically. I don't
know what a good way is to fix this. At this point I've mostly gotten
the evil concentrated in the definitions "collapsed line break",
"extraneous line break", and "collapsed block prop", but there's lots
of other special-case handling scattered about. Feedback appreciated.
How do browsers handle this?
- The CSS styling flag is an issue. Currently authors are forced to
turn it entirely on or entirely off. If it's on, it produces stuff like
<span style=font-weight:bold> instead of
<b>, while if it's off, it produces stuff
like <font color=red> instead of
<span style=color:red>. The issue is that
authors might want a mix, like making the markup as concise as possible
while still conforming, and they can't do that. Changing the flag on a
per-command basis doesn't help because of things like the "restore the
values" algorithm, which might create several different types of style
at once and has to use the same styling flag for all of them. This was
discussed back in March in
this thread, along with a number of other things, but at that
time I hadn't written commands that change multiple styles at once,
so it seemed feasible to ask authors to switch styleWithCSS on or off
on a per-command basis.
- I haven't defined the "undo" or "redo" commands yet. They look very
complicated to define precisely, and other people are working on them
right now.
A variety of other issues are also noted in the text, formatted like
this. Feedback would be appreciated on all of them.
TODO:
- Scour browser bug trackers to try spotting issues I haven't
thought of.
- The wording I use for DOM stuff is not maximally precise. Really
I want DOM Core to define nice concepts that I can xref, like "insert
a node". I don't want to have to explicitly refer to DOM methods like
insertBefore() every time I want to move things.
- JavaScript can modify the DOM synchronously in some cases, such
as DOM mutation events and onunload when moving around iframes and
objects. This has to be dealt with somehow. (Pointed out by Ryosuke
Niwa of WebKit:
1
2)
- What happens if you do something like delete a selection or
insert text or whatnot in the middle of a surrogate pair? This could
make the content not serialize through a character encoding change.
- Some more thought needs to go into what happens to the selection
when you mutate the DOM. In some cases the results are pretty
arbitrary. It might make sense to do some kind of normalization.
- I'm sloppy about handling things like nodes that don't descend
from a Document, comments that are children of a Document, that sort
of thing. Not essential for prototyping, but needs to be cleaned up
eventually. Mostly we should be able to avoid the problems by
requiring that everything be editable, since that immediately means
it has to descend from an element or Document (and cannot be
parentless itself).
- I need to pay more attention to invisible nodes. These will have
no visual effect, but they'll make many algorithms behave
differently: decomposing a range, block-extending, etc. Also, need to
improve the definition to include things like whitespace-only nodes.
- Have to make sure that in all the places where we set a
selection, it's valid.
- Redefine things in terms of ranges, not selections.
- Allow some type of switch to affect non-editable regions too,
perhaps on a per-command basis.
- Things like delete, forwardDelete, insertText need to handle
non-BMP characters.
Also TODO: Things that are only implemented by a couple of browsers
and may or may not be useful to spec:
- decreaseFontSize, increaseFontSize: Only implemented in Gecko and
Opera.
- contentReadOnly, enableInlineTableEditing, enableObjectResizing,
heading, insertBrOnReturn: MDC docs say not implemented in IE (didn't
test).
- readOnly: MDC docs say it's a deprecated equivalent of
contentReadOnly, so presumably like useCSS but less popular.
- 2D-Position, absolutePosition, clearAuthenticationCache,
createBookmark, insertButton, insertFieldset, insertIframe,
insertInput*, insertMarquee, insertSelectDropdown,
insertSelectListbox, insertTextarea, liveResize, multipleSelection,
overwrite, print, refresh, saveAs, unbookmark: Mentioned in MSDN docs
but not MDC, so presumably IE-only. Some of these seem inappropriate
or useless, others will bear investigation.
- findString, fontSizeDelta, insertNewlineInQuotedContent,
justifyNone, print, transpose: There's code for these in WebKit,
Source/WebCore/editing/EditorCommand.cpp, but I didn't see them
mentioned elsewhere. Some might be worth adding.
- unselect: Seems to not be implemented by Gecko or Opera, and IE
behaves oddly: it seems to collapse the selection instead of removing
it. Will only implement if there seems to be demand; it's redundant
to Selection.removeAllRanges() anyway.
Things I haven't looked at that multiple browsers implement:
- redo, undo: Needs review of the Google work on this; will
probably be quite complicated.
Things that would be useful to address for the future but aren't
important to fix right now are in comments prefixed with "TODO".
Commands
Properties of commands
This specification defines a number of commands, identified by ASCII
case-insensitive strings. Each command can
have several pieces of data associated with it:
-
Action: What the command does when executed via
execCommand(). Every command defined in this specification has an
action defined for it in the relevant
section. For example, the bold command's action generally
makes the current selection bold, or removes bold if the selection
is already bold. An editing toolbar might provide buttons that
execute the action for a command if clicked, or a script might run an
action without user interaction to achieve
some particular effect. Actions return either true or false, which
can affect the return value of execCommand().
-
Indeterminate: A boolean value
returned by
queryCommandIndeterm(),
depending on the current state of the document. Generally, a
command that has a state defined will be indeterminate if the state is true for part but not all of the current
selection, and a command that has a value defined will be indeterminate if different parts of the
selection have different values.
An editing toolbar might display a button or control in a special
way if the command is indeterminate, like showing a "bold" button as
partially depressed, or leaving a font size selector blank instead
of showing the font size of the current selection. As a rule, a
command can only be indeterminate if its state is false, supposing it has a state.
-
State: A boolean value returned by
queryCommandState(), depending on
the current state of the document. The state
of a command is true if it is already in
effect, in some sense specific to the command. Most commands that have a state
defined will take opposite actions depending on whether the state is true or false, such as making the selection
bold if the state is false and removing bold
if the state is true. Others will just have no
effect if the state is true, like the justifyCenter command. Still others will have the
same effect regardless, like the styleWithCss
command. An editing toolbar might display a button or control
differently depending on the state and
indeterminacy of
the command.
-
Value: A string returned by
queryCommandValue(), depending on
the current state of the document. A command
usually has a value instead of a state if the property it modifies can take more than
two different values, like the
foreColor command. If the command is indeterminate, its value
is generally based on the start of the selection. Otherwise, in
most cases the value holds true for the entire
selection, but see the
justifyCenter command and its
three companions for an exception. An
editing toolbar might display the value of a
command as selected in a drop-down or filled
in in a text box, if the command isn't
indeterminate.
-
Relevant CSS property: This
is defined for certain inline
formatting commands, and is used in algorithms specific to
those commands. It is an implementation detail, and is not exposed
to authors. If a command does not have a
relevant CSS property
specified, it defaults to null.
Supported commands
If you try doing anything with an unrecognized command (except
queryCommandSupported), IE10 Developer Preview throws an "Invalid
argument" exception. Firefox 15.0a1 throws NS_ERROR_NOT_IMPLEMENTED
on querying indeterm/state/value, and returns false from
execCommand/queryCommandEnabled. Chrome 19 dev returns false from
everything. Opera Next 12.00 alpha throws NOT_SUPPORTED_ERR for
execCommand and returns false for enabled/state/value. Originally I
went with IE, although of course with a standard exception type. But
after discussion (WebKit bug,
Mozilla
bug), I changed to match WebKit (except that I return "" for
value instead of false). The issue is that there are a whole bunch of
IE commands that no one else supports or wants to support, and
throwing on execCommand() would make lots of pages break. WebKit was
unwilling to take the compat risk, so we took the safer option.
Some commands will be supported in a given user agent, and some will not.
All commands defined in this
specification must be supported, except
optionally the copy
command, the cut
command, and/or the paste command. Additional vendor-specific commands can also be
supported, but implementers must prefix any
vendor-specific command names
with a vendor-specific string (e.g., "ms", "moz", "webkit", "opera").
I.e., no trying to look good on lazy conformance tests by just
sticking in a stub implementation that does nothing.
A command that does absolutely nothing in a
particular user agent, such that execCommand() never has any effect and
queryCommandEnabled() and
queryCommandIndeterm() and
queryCommandState()
and queryCommandValue() each return the
same value all the time, must not be supported.
In a particular user agent, every command must
be consistently either supported or not.
Specifically, a user agent must not permit one page to see the same
command sometimes supported and sometimes not over the course of the
same browsing session, unless the user agent has been upgraded or
reconfigured in the middle of a session. However, user agents may
treat the same command as supported for some pages and not others, e.g., if
the command is only supported for certain
origins for security reasons.
Authors can tell whether a command is supported using queryCommandSupported().
Enabled commands
At any given time, a supported command can
be either enabled or not. Authors can tell
whether a command is currently enabled using queryCommandEnabled(). Commands that are not enabled do nothing, as described in the definitions of
the various methods that invoke commands.
Testing with bold:
IE10PP2 seems to return true if the active range's start node is
editable, false otherwise.
Firefox 6.0a2 seems to always return true if there's anything
editable on the page, and throw otherwise. (This is bug
676401.)
Chrome 14 dev seems to behave the same as IE10PP2.
Opera 11.11 seems to always return true if there's anything
editable on the page, and false otherwise.
Firefox and Opera behave more or less uselessly. IE doesn't make
much sense, in that whether a command is enabled seems meaningless:
it will execute it on all nodes in the selection, editable or not.
Chrome's definition makes sense in that it will only run the
command if it's enabled, but it doesn't make much sense to only
have the command run if the start is editable.
It's not clear to me what the point of this method is. There's no
way we're going to always return true if the command will do
something and false if it won't. I originally just stuck with a
really conservative definition that happens to be convenient: if
there's nothing selected, obviously nothing will work, and we want
to bail out early in that case anyway because all the algorithms
will talk about the active range. If there are use-cases for it to
be more precise, I could make it so.
Bug
16094 illustrated that we don't really want to be able to
modify multiple editing hosts at once, nor do we want to do
anything if the start and end aren't both editable, so I co-opted
this definition to fit my ends.
Among commands defined in this
specification, those listed in Miscellaneous commands are always
enabled, except for the cut command and
the paste
command. The other commands defined here are enabled if the active
range is not null, its [=range/start node=] is either editable or an [=editing host=], the editing
host of its [=range/start node=] is not an
EditContext
editing host, its [=range/end node=] is either editable or
an [=editing host=], the editing host of
its [=range/end node=] is not an
EditContext
editing host,
and there is some [=editing host=] that is an
[=tree/inclusive ancestor=] of both its [=range/start node=] and its
[=range/end node=].
Methods to query and execute commands
partial interface Document {
[CEReactions] boolean execCommand(DOMString commandId, optional boolean showUI = false, optional (TrustedHTML or DOMString) value = "");
};
TODO: Add IDL for queryCommand* functions.
TODO: Define behavior for show UI.
When the execCommand(command,
show UI, value)
method on the {{Document}} interface is invoked, the user agent must
run the following steps:
- If only one argument was provided, let show UI
be false.
- If only one or two arguments were provided, let value be the empty string.
-
For supported: see comment before Supported commands.
For enabled: I didn't research this closely, but at a first
glance, this is possibly how Chrome 14 dev and Opera 11.11
behave. Maybe also Firefox 6.0a2, except it throws if the command
isn't enabled, I think. IE9 returns true in at least some cases
even if the command is disabled. TODO: Is this right? Maybe we
should be returning false in other cases too?
If command is not supported or not enabled,
return false.
-
If command is not in the Miscellaneous commands section:
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.
- Let affected editing host be the [=editing host=]
that is an [=tree/inclusive ancestor=] of the active range's [=range/start node=] and
[=range/end node=], and is not the [=tree/ancestor=] of any
[=editing host=] that is an [=tree/inclusive ancestor=] of the
active range's [=range/start node=] and
[=range/end node=].
Such an editing host must exist, because otherwise the command
would not be enabled.
- [=Fire an event=] named "beforeinput" at affected editing
host using {{InputEvent}}, with its {{Event/bubbles}} and
{{Event/cancelable}} attributes initialized to true, and its
{{InputEvent/data}} attribute initialized to null.
- If the value returned by the previous step is false, return
false.
- If command is not enabled, return false.
We have to check again whether the command is enabled, because
the beforeinput handler might have done something annoying like
getSelection().removeAllRanges().
- Let affected editing host be the [=editing
host=] that is an [=tree/inclusive ancestor=] of the active range's [=range/start node=] and
[=range/end node=], and is not the [=tree/ancestor=] of any
[=editing host=] that is an [=tree/inclusive ancestor=] of the
active range's [=range/start node=] and
[=range/end node=].
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.
- Take the action for command, passing value to the instructions
as an argument.
- If the previous step returned false, return false.
- If the action modified DOM tree, then [=fire an event=] named
"input" at affected editing host using {{InputEvent}}, with
its {{Event/isTrusted}} and {{Event/bubbles}} attributes initialized to
true, {{InputEvent/inputType}} attribute initialized to the [=map an
edit command to input type value|mapped value=] of command,
and its {{InputEvent/data}} attribute initialized to null.
- Return true.
To map an edit command to input type value, follow this table:
| edit command | inputType
|
|---|
| backColor | formatBackColor
| | bold | formatBold
| | createLink | insertLink
| | fontName | formatFontName
| | foreColor | formatFontColor
| | strikethrough | formatStrikeThrough
| | superscript | formatSuperscript
| | delete | deleteContentBackward
| | forwardDelete | deleteContentForward
| | indent | formatIndent
| | insertHorizontalRule | insertHorizontalRule
| | insertLineBreak | insertLineBreak
| | insertOrderedList | insertOrderedList
| | insertParagraph | insertParagraph
| | insertText | insertText
| | insertUnorderedList | insertUnorderedList
| | justifyCenter | formatJustifyCenter
| | justifyFull | formatJustifyFull
| | justifyLeft | formatJustifyLeft
| | justifyRight | formatJustifyRight
| | outdent | formatOutdent
| | cut | deleteByCut
| | paste | insertFromPaste
| | redo | historyRedo
| | undo | historyUndo
|
If no mapping exists, return an empty string.
When the queryCommandEnabled(command) method on the {{Document}} interface is
invoked, the user agent must run the following steps:
-
See comment before Supported
commands.
- Return true if command is both supported and enabled, false
otherwise.
When the queryCommandIndeterm(command) method on the {{Document}} interface is
invoked, the user agent must run the following steps:
-
For supported: see comment before Supported commands.
What happens if you call queryCommand(Indeterm|State|Value)() on
a command where it makes no sense?
IE9 consistently returns false for all three. However, any
command that has a state defined also has a value defined, which
is equal to the state: it returns boolean true or false.
Firefox 6.0a2 consistently throws NS_ERROR_FAILURE for
indeterm/state if not supported, and returns an empty string for
value. Exceptions include unlink (seems to always return
indeterm/state false), and styleWithCss/useCss (throw
NS_ERROR_FAILURE even for value).
Chrome 14 dev returns false for all three, and even does this for
unrecognized commands. It also always defines value if state is
defined: it returns the state cast to a string, either "true" or
"false".
Opera 11.11 returns false for state and "" for value (it doesn't
support indeterm). Like Chrome, this is even for unrecognized
commands.
Gecko's behavior is the most useful. If the author tries querying
some aspect of a command that makes no sense, they shouldn't
receive a value that looks like it might make sense but is
actually just a constant. Originally, I went even further than
Gecko: I required exceptions even for value, since doing
otherwise makes no sense. But throwing more exceptions is less
compatible on the whole than throwing more exceptions, so based
on
discussion, I switched to a behavior more like Opera, which
is more or less IE/WebKit behavior but made slightly more sane.
If command is not supported or has no indeterminacy, return false.
- Return true if command is indeterminate, otherwise false.
When the queryCommandState(command) method on the {{Document}} interface is
invoked, the user agent must run the following steps:
-
See comment on the comparable line for queryCommandIndeterm().
If command is not supported or has no state,
return false.
- If the state override for
command is set, return it.
- Return true if command's state is true, otherwise false.
Firefox 6.0a2 always throws an exception when this is called. Opera
11.11 seems to return false if there's nothing editable on the page,
which is unhelpful. The spec follows IE9 and Chrome 14 dev. The
reason this is useful, compared to just running one of the other
methods and seeing if you get a NOT_SUPPORTED_ERR, is that other
methods might throw different exceptions for other reasons. It's
easier to check a boolean than to check exception types, especially
since as of June 2011 UAs aren't remotely consistent on what they do
with unsupported commands.
Actually, correction: Firefox < 15ish throws an exception if
nothing editable is on the page. Otherwise it behaves just like
IE/Chrome. See Mozilla bug
742240.
When the queryCommandSupported(command) method on the {{Document}} interface is
invoked, the user agent must return true if command
is supported and available within the current
script on the current site, and false otherwise.
When the queryCommandValue(command) method on the {{Document}} interface is
invoked, the user agent must run the following steps:
-
This is what Firefox 6.0a2 and Opera 11.11 seem to do when the
command isn't enabled. Chrome 14 dev seems to return the string
"false", and IE9 seems to return boolean false. For the case where
there's no value, or the command isn't supported, see the comment
on the comparable line for queryCommandIndeterm().
If command is not supported or has no value,
return the empty string.
-
Yuck. This is incredibly messy, as are lots of other
fontSize-related things, but I don't want to define a whole second
notion of value for the sake of a single command . . .
If command is "fontSize" and its value override is set, convert the value override to an integer number of pixels
and return the legacy font size
for the result.
- If the value override for
command is set, return it.
- Return command's value.
All of these methods must treat their command
argument ASCII case-insensitively.
The methods in this section have mostly been designed so that the
following invariants hold after execCommand()
is called, assuming it didn't throw an exception:
-
queryCommandIndeterm() will return false (or
throw an exception).
-
queryCommandState() will return the opposite
of what it did before execCommand() was
called (or throw an exception).
-
queryCommandValue() will return something
equivalent to the value passed to execCommand() (or throw an exception). "Equivalent" here
needs to be construed broadly in some cases, such as fontSize.
The first two points do not always hold for strikethrough or underline, because
it can be impossible to unset text-decoration in CSS. Also, by
design, the state of insertOrderedList and
insertUnorderedList might be true both before
and after calling, because they only remove one level of indentation.
unlink should set the value to null. And
finally, the state of the various justify
commands should always be true after calling, and the value should
always be the appropriate string ("center", "justify", "left", or
"right"). Any other deviations from these invariants are bugs in the
specification.
Common definitions
An HTML element is an {{Element}} whose
[=Element/namespace=] is the HTML namespace.
A prohibited paragraph child
name is "address", "article", "aside", "blockquote", "caption",
"center", "col", "colgroup", "dd", "details", "dir", "div", "dl", "dt",
"fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3",
"h4", "h5", "h6", "header", "hgroup", "hr", "li", "listing", "menu",
"nav", "ol", "p", "plaintext", "pre", "section", "summary", "table",
"tbody", "td", "tfoot", "th", "thead", "tr", "ul", or "xmp".
These are all the things that will close a <p> if found as a
descendant. I think. Plus table stuff, since that can't be a descendant
of a p either, although it won't auto-close it.
A prohibited paragraph child
is an HTML element whose [=Element/local
name=] is a prohibited
paragraph child name.
The block/inline node definitions are CSS-based. "Prohibited paragraph
child" is conceptually similar to "block node", but based on the
element name. Generally we want to use block/inline node when we're
interested in the visual effect, and prohibited paragraph children when
we're concerned about parsing or semantics. TODO: Audit all "block
node" usages to see if they need to become "visible block node", now
that block nodes can be invisible (if they descend from display: none).
A block node is either an {{Element}} whose
"display" property does not have resolved value
"inline" or "inline-block" or "inline-table" or "none", or a
[=document=], or a {{DocumentFragment}}.
An inline node is a node that is not a block
node.
Something is editable if it is a node; it is not an [=editing host=]; it does not
have a
contenteditable attribute set to the false state; its
[=tree/parent=] is an [=editing host=] or editable; and either it is an HTML element, or it is an
svg or
math element, or it is not an {{Element}} and its
[=tree/parent=] is an HTML element.
An editable node cannot be a [=document=] or
{{DocumentFragment}}, its [=tree/parent=] cannot be null, and it must
descend from either an {{Element}} or a [=document=].
The editing host of node is null if node is neither editable nor an [=editing host=]; node itself, if node is an [=editing
host=]; or the nearest [=tree/ancestor=] of node
that is an [=editing host=], if node is editable.
Two nodes are in the
same editing host if the editing host
of the first is non-null and the same as the editing host of the second.
Barring bugs, the algorithms here will not alter the attributes of a
non-editable element; will not remove a non-editable node from its
parent (except to immediately give it a new parent in the same editing
host); and will not add, remove, or reorder children of a node unless
it is either editable or an editing host. An editing host is never
editable, so authors are assured that editing commands will only modify
the editing host's contents and not the editing host itself.
A collapsed line break is a [^br^]
that begins a line box which has nothing else in it, and therefore has
zero height.
Is this a good definition at all? I mean things like
<p>foo<br></p>, or the second one in
<p>foo<br><br></p>. The way I test it is by
adding a text node after it containing a zwsp; if that changes the
offsetHeight of its nearest non-inline ancestor, I deem it collapsed.
But what if it happens to be display: none right now, for instance? Or
its ancestor has a fixed height? Would it be better to use some
DOM-based definition?
TODO: The thing about li is a not very nice hack. The issue is that an
li won't collapse even if it has no children at all, but that's not
true in all browsers (at least not in Opera 11.11), and also it breaks
assumptions elsewhere. E.g., if it gets turned into a p.
An extraneous line break is a
[^br^] that has no visual effect, in that removing it from the DOM
would not change layout, except that a [^br^] that is the sole child of
an [^li^] is not extraneous.
Also possibly a bad definition. Again, I test by just removing it and
seeing what happens. (Actually, setting display: none, so that it
doesn't mess up ranges.)
A whitespace node is either a {{Text}}
node whose {{CharacterData/data}} is the empty string; or a {{Text}}
node whose {{CharacterData/data}} consists only of one or more tabs
(0x0009), line feeds (0x000A), carriage returns (0x000D), and/or spaces
(0x0020), and whose [=tree/parent=] is an {{Element}} whose resolved value for
"white-space" is "normal" or "nowrap"; or a {{Text}} node whose
{{CharacterData/data}} consists only of one or more tabs (0x0009),
carriage returns (0x000D), and/or spaces (0x0020), and whose
[=tree/parent=] is an {{Element}} whose resolved value for
"white-space" is "pre-line".
node is a collapsed whitespace node if the
following algorithm returns true:
This definition is also bad. It's a crude attempt to emulate CSS2.1
16.6.1, but leaving out a ton of the subtleties. I actually don't want
the exact CSS definitions, because those depend on things like where
lines are broken, but I'm not sure this definition is right anyway.
E.g., what about a pre-line text node consisting of a single line break
that's at the end of a block? That collapses, same idea as an
extraneous line break. We could also worry about nodes containing only
zwsp or such if we wanted, or display: none, or . . .
- If node is not a whitespace node, return false.
- If node's {{CharacterData/data}} is the empty
string, return true.
- Let ancestor be node's
[=tree/parent=].
- If ancestor is null, return true.
- If the "display" property of some [=tree/ancestor=] of
node has resolved value
"none", return true.
- While ancestor is not a
block node and its [=tree/parent=] is not null, set ancestor to its [=tree/parent=].
-
At this point we know node consists of some
whitespace, of a sort that will collapse if it's at the start or
end of a line. We go backwards until we find the first block
boundary, and if everything until there is invisible or
whitespace, we conclude that node is
collapsed. We assume a block boundary is either when we hit a
line break or block node, or we hit the end of ancestor (which is the nearest ancestor block node). All
this is very imprecise, of course, but it's fairly simple and
will work in common cases.
We have to avoid invoking the definition of "visible" here to
avoid infinite recursion: that depends on the concept of
collapsed whitespace nodes. Instead, we repeat the parts we need,
which turns out to be "not much of it".
Let reference be node.
- While reference is a [=tree/descendant=] of
ancestor:
- Let reference be the node before it in [=tree order=].
- If reference is a block node or a [^br^], return true.
- If reference is a {{Text}} node that is not
a whitespace node, or is an [^img^],
break from this loop.
-
We found something before our text node on (probably) the same
line, so presumably it's not at the line's start. Now we need to
look forward and see if we're at the line's end. If we aren't there
either, then we assume we're not collapsed, so return false.
Let reference be node.
- While reference is a [=tree/descendant=] of
ancestor:
- Let reference be the node after it in [=tree order=], or null if
there is no such node.
- If reference is a block node or a [^br^], return true.
- If reference is a {{Text}} node that is not
a whitespace node, or is an [^img^],
break from this loop.
- Return false.
TODO: Consider whether we really want to depend on img specifically
here. It seems more likely that we want something like "any replaced
content that has nonzero height and width" or such. When fixing this,
make sure to audit for other occurrences of this assumption.
Something is visible if it is a node that either is a block
node, or a {{Text}} node that is not a collapsed whitespace node, or an
[^img^], or a [^br^] that is not an extraneous line break, or any node with a visible
[=tree/descendant=]; excluding any node with an [=tree/inclusive ancestor=]
{{Element}} whose "display" property has resolved value
"none".
Something is invisible if it is a node that is not visible.
TODO: Reconsider whether we want to lump invisible nodes in here. If we
don't and change the definition, make sure to audit all callers, since
then a block could have collapsed block prop descendants that aren't
children.
A collapsed block prop is either a
collapsed line break that is not an
extraneous line break, or an
{{Element}} that is an inline node and whose
[=tree/children=] are all either invisible or
collapsed
block props and that has at least one [=tree/child=] that is a
collapsed block prop.
A collapsed block prop is something like the <br> in <p><br></p>, or the <br> and <span> in
<p><span><br></span></p>. These
are necessary to stop the block from having zero height when it has no
other contents, but serve no purpose and should be removed once the
block has other contents that stop it from collapsing.
TODO: I say "first range" because I think that's what Gecko actually
does, and Gecko is the only one that allows multiple ranges in a
selection. This is keeping in mind that it stores ranges sorted by
start, not by the order the user added them, and silently removes or
shortens existing ranges to avoid overlap. It probably makes the most
sense in the long term to have the command affect all ranges. But I'll
leave this for later.
The active range is the [=range=] of the
selection
given by calling getSelection() on the context object. (Thus
the active range may be null.)
Each {{Document}} has a boolean CSS styling
flag associated with it, which must initially be false. (The styleWithCSS
command can be used to modify or query it, by means of the
execCommand() and
queryCommandState()
methods.)
Each {{Document}} is associated with a string known as the default single-line container
name, which must initially be "div". (The defaultParagraphSeparator command can be used to modify
or query it, by means of the execCommand() and queryCommandValue() methods.)
For some commands, each
{{Document}} must have a boolean state
override and/or a string value
override. These do not change the command's state or value, but change the way some algorithms behave, as
specified in those algorithms' definitions. Initially, both must be
unset for every command. Whenever the number of
[=ranges=] in the selection changes to something different, and
whenever a [=boundary point=] of the [=range=] at a given index in the
selection
changes to something different, the state
override and value override must be
unset for every command. The value override for the backColor
command must be the same as the value
override for the hiliteColor command, such that setting one sets the other
to the same thing and unsetting one unsets the other.
The primary purpose of state and value overrides is that if the user
runs a command like bold with a collapsed selection, then types something
without moving the cursor, they expect it to have the given style (bold
or such). Thus the commands like bold set state and value
overrides, and insertText checks for them and applies them to the
newly-inserted text. Other commands like delete also interact
with overrides.
See bug
16207.
When
document.open() is called and a [=document=]'s singleton
objects are all replaced by new instances of those objects, editing
state associated with that document (including the CSS styling flag, default single-line container
name, and any state overrides or value overrides) must be reset.
Of course, any action that replaces a [=document=] object entirely,
such as reloading the page, will also reset any editing state
associated with the document.
When this specification refers to a method or attribute that is defined
in a specification, the user agent must treat the method or attribute
as defined by that specification. In particular, if a script has
overridden a standard property with a custom one, the user agent must
only use the overridden property when a script refers to it, and must
continue to use the specification-defined behavior when this
specification refers to it.
When a list or set of nodes is assigned to a variable without specifying
the order, they must be initially in [=tree order=], if they share a
root. (If they don't share a root, the order will be specified.) When
the user agent is instructed to run particular steps for each member of
a list, it must do so sequentially in the list's order.
Common algorithms
Assorted common algorithms
To move a node to a new location, preserving ranges, remove the node from its original [=tree/parent=] (if any),
then insert it in the new location. In doing so, follow these rules
instead of those defined by the [=insert=] and [=remove=] algorithms:
Many of the algorithms in this specification move nodes around in the
DOM. The normal rules for range mutation require that any range
endpoints inside those nodes are moved to the node's parent as soon
as the node is moved, which would corrupt the selection. For
instance, if the user selects the text "foo" and then bolds it, first
we produce <b></b>foo, then
<b>foo</b>. When we move the "foo"
text node into its new parent, we have to do so "preserving ranges",
so that the text "foo" is still selected.
- Let node be the moved node, old parent and
old index be the old [=tree/parent=] (which may
be null) and [=tree/index=], and new parent and
new index be the new [=tree/parent=] and
[=tree/index=].
-
This is actually implicit, but I state it anyway for
completeness.
If a [=boundary point=]'s node is the same as or a [=tree/descendant=]
of node, leave it unchanged, so it moves to
the new location.
- If a [=boundary point=]'s node is new parent and its
[=boundary point/offset=] is greater than new
index, add one to its [=boundary point/offset=].
- If a [=boundary point=]'s node is old parent and its
[=boundary point/offset=] is old index or
old index + 1, set its node to new parent and add
new index − old index to its
[=boundary point/offset=].
- If a [=boundary point=]'s node is old parent and its
[=boundary point/offset=] is greater than old
index + 1, subtract one from its [=boundary point/offset=].
TODO: Do we want to get rid of attributes that are no longer allowed
here?
To set the tag name of an
{{Element}} element to new
name:
This is needed because the DOM doesn't allow any way of changing an
existing element's name. Sometimes we want to, e.g., convert a markup
element to a span. In that case we invoke this algorithm to create a
new element, move it to the right place, copy attributes from the old
element, move the old element's children, and remove the old element.
- If element is an HTML
element with [=Element/local name=] equal to new
name, return element.
- If element's [=tree/parent=] is null, return
element.
- Let replacement element be the result of
calling
createElement(new
name) on the {{Node/ownerDocument}} of element.
- Insert replacement element into element's [=tree/parent=] immediately before
element.
- Copy all attributes of element to
replacement element, in order.
- While element has [=tree/children=], append
the first [=tree/child=] of element as the last
[=tree/child=] of replacement element, preserving ranges.
- Remove element from its [=tree/parent=].
- Return replacement element.
To remove extraneous
line breaks before a node node:
<br> sometimes has no effect in CSS, such
as in the markup foo<br><p>bar</p>. In such cases we like
to remove the extra markup to keep things tidy.
- Let ref be the {{Node/ previousSibling}} of
node.
- If ref is null, abort these steps.
- While ref has [=tree/children=], set
ref to its {{Node/lastChild}}.
- While ref is invisible but not an extraneous line break, and ref does not equal node's
[=tree/parent=], set ref to the node before it in [=tree order=].
- If ref is an editable
extraneous line break, remove it
from its [=tree/parent=].
To remove
extraneous line breaks at the end of a node node:
- Let ref be node.
- While ref has [=tree/children=], set
ref to its {{Node/lastChild}}.
- While ref is invisible but not an extraneous line break, and ref does not equal node, set
ref to the node before it in [=tree order=].
- If ref is an editable
extraneous line break:
-
If the block ends with <span><br></span>, for instance,
we want to remove the span too.
While ref's [=tree/parent=] is editable and invisible, set ref to
its [=tree/parent=].
- Remove ref from its [=tree/parent=].
To remove extraneous
line breaks from a node, first remove extraneous line breaks
before it, then remove extraneous line
breaks at the end of it.
Wrapping a list of nodes
To wrap a list node list of
consecutive [=tree/sibling=] nodes, run the following algorithm. In addition to
node list, the algorithm accepts two inputs: an
algorithm sibling criteria that accepts a
node as input and outputs a boolean, and an
algorithm new parent instructions that accepts
nothing as input and outputs a node or null. If not provided, sibling criteria returns false and new parent
instructions returns null.
This algorithm basically does two things. First, it looks at the
previous and next siblings of the nodes in node
list. If running sibling criteria on one or
both of the siblings returns true, the nodes in node
list are moved into the sibling(s). Otherwise, new parent instructions is run, and the result is used to
wrap node list. For instance, to wrap node list in a <b>, one might
invoke this algorithm with sibling criteria
returning true only for <b> elements and
new parent instructions creating and returning a
new <b> element.
-
We need to treat [^br^]s as visible here even if they're not,
because wrapping them might be significant even if they're
invisible: it can turn an extraneous line break into a
non-extraneous one.
If every member of node list is invisible, and none is a [^br^], return null and
abort these steps.
- If node list's first member's [=tree/parent=]
is null, return null and abort these steps.
-
Trailing br's like this always need to go along with their line.
Otherwise they'll create an extra line if we wrap in a block
element, instead of vanishing as they should.
If node list's last member is an inline node that's not a [^br^], and
node list's last member's
{{Node/nextSibling}} is a [^br^], append that [^br^] to
node list.
-
See bug
13811, bug
14231. If there's a non-adjacent sibling that matches the
sibling criteria and only invisible nodes intervene, we want to
skip over the invisible nodes. For instance, bolding <b>foo</b><!--bar-->[baz] should
produce <b>foo<!--bar-->baz</b>. Similarly,
and more usefully, creating an ordered list with <ol><li>foo</ol>
<p>[bar]</p> should produce <ol><li>foo</li>
<li>[bar]</ol>, not <ol><li>foo</ol>
<ol><li>[bar]</ol>.
While node list's first member's
{{Node/previousSibling}} is invisible,
prepend it to node list.
- While node list's last member's
{{Node/nextSibling}} is invisible, append it
to node list.
- If the {{Node/previousSibling}} of the first member of
node list is editable
and running sibling criteria on it returns
true, let new parent be the
{{Node/previousSibling}} of the first member of node
list.
- Otherwise, if the {{Node/ nextSibling}} of the last member of
node list is editable and
running sibling criteria on it returns true, let
new parent be the {{Node/nextSibling}} of the
last member of node list.
- Otherwise, run new parent instructions, and
let new parent be the result.
-
This can only happen if new parent
instructions is run and it returns null. This can be used
to only merge with adjacent siblings, in case you don't want to
create a new parent if that fails.
If new parent is null, abort these steps and
return null.
-
Most callers will create a new element to return in new parent instructions, whose parent will therefore be
null. But they can also return an existing node if that makes
sense, so the nodes will be moved to an uncle or something. The
toggle lists algorithm makes use of this.
If new parent's [=tree/parent=] is null:
- Insert new parent into the
[=tree/parent=] of the first member of node
list immediately before the first member of
node list.
-
Basically, we want any boundary points around the wrapped
nodes to go inside the wrapper. Without this step, wrapping
"{}<br>" in a blockquote would go like
{}<br>
-> {}<blockquote></blockquote><br>
-> {}<blockquote><br></blockquote>.
The second line is due to range mutation rules: a boundary
point with an offset equal to the index of a newly-inserted
node stays put, so it remains before it. With this step, it
goes like
{}<br>
-> {}<blockquote></blockquote><br>
-> <blockquote></blockquote>{}<br>
-> <blockquote>{}<br></blockquote>.
The difference in the final step is because we move the
<br> "preserving ranges". This means that adjacent
boundary points get swept along with it. Previously, the
<blockquote> intervened, so a boundary point after it
would get taken along but one before it would not.
Another solution that one might be tempted to consider
would be to just put the wrapper after the wrapped
elements. Then the boundary points would stay put, before
the wrapper, so they'd still be adjacent to the nodes to be
wrapped, like:
{<p>foo</p>}
-> {<p>foo</p>}<blockquote></blockquote>
-> <blockquote>{<p>foo</p>}</blockquote>.
The problem is that this completely breaks if you're
wrapping multiple things and not all are selected. It would
go like this:
<p>foo</p>{<p>bar</p>}
-> <p>foo</p>{<p>bar</p>}<blockquote></blockquote>
-> <p>foo</p><blockquote>{<p>bar</p>}</blockquote>
-> <blockquote>{<p>foo</p><p>bar</p>}</blockquote>.
The last step is again because of the range mutation rules:
the boundary point stays put when a new node is inserted.
They're fundamentally asymmetric.
An alternative solution would be to define the concept of
moving a list of adjacent sibling nodes while preserving
ranges, and handle this explicitly at a more abstract
level.
TODO: Think about this some more. Maybe there's a better
way.
If any [=range=] has a [=boundary point=] with node equal to the [=tree/parent=] of
new parent and [=boundary point/offset=]
equal to the [=tree/index=] of new
parent, add one to that [=boundary point=]'s [=boundary
point/offset=].
- Let original parent be the [=tree/parent=] of
the first member of node list.
- If new parent is before the first member of
node list in [=tree order=]:
- If new parent is not an inline node, but the last
visible [=tree/child=] of new parent
and the first visible member of
node list are both inline nodes, and the last
[=tree/child=] of new parent is not a
[^br^], call
createElement("br")
on the {{Node/ ownerDocument}} of new
parent and append the result as the last [=tree/child=]
of new parent.
- For each node in node
list, append node as the last
[=tree/child=] of new parent, preserving ranges.
- Otherwise:
- If new parent is not an inline node, but the first
visible [=tree/child=] of new parent
and the last visible member of
node list are both inline nodes, and the last member of
node list is not a [^br^], call
createElement("br")
on the {{Node/ ownerDocument}} of new
parent and insert the result as the first [=tree/child=]
of new parent.
- For each node in node
list, in reverse order, insert node as the first [=tree/child=] of new
parent, preserving ranges.
-
This could happen if new parent instructions
returned a node whose parent wasn't null.
If original parent is editable and has no [=tree/children=], remove it
from its [=tree/parent=].
-
Probably because both the previous and next sibling met them. We
want to merge them in this case.
If new parent's {{Node/nextSibling}} is
editable and running sibling criteria on it returns true:
- If new parent is not an inline node, but new
parent's last [=tree/child=] and new
parent's {{Node/nextSibling}}'s first [=tree/child=] are
both inline nodes,
and new parent's last [=tree/child=] is not a
[^br^], call
createElement("br")
on the {{Node/ ownerDocument}} of new parent
and append the result as the last [=tree/child=] of new parent.
- While new parent's {{Node/nextSibling}}
has [=tree/children=], append its first [=tree/child=] as the
last [=tree/child=] of new parent, preserving ranges.
- Remove new parent's {{Node/nextSibling}}
from its [=tree/parent=].
-
Remove extraneous
line breaks from new parent.
- Return new parent.
Allowed children
List is mostly based on current HTML5, together with obsolete
elements. I mostly got the obsolete element list by testing what
Firefox 5.0a2 splits when you do insertHorizontalRule.
TODO: The definitions of prohibited paragraph children and elements
with inline contents should be in the HTML spec (possibly under a
different name) so they don't fall out of sync. They'll do for now.
A name of an
element with inline contents is "a", "abbr", "b", "bdi", "bdo",
"cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
"kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp",
"small", "span", "strong", "sub", "sup", "u", "var", "acronym",
"listing", "strike", "xmp", "big", "blink", "font", "marquee",
"nobr", or "tt".
An element with inline
contents is an HTML element whose
[=Element/local name=] is a name of an element with
inline contents.
TODO: This list doesn't currently match HTML's validity
requirements for a few reasons:
- We need to handle invalid elements, which have no conformance
requirements but should be treated properly. In particular, they
can interfere with serialization (e.g., center cannot descend from
p).
- Sometimes users give instructions that have to produce invalid
DOMs to get the expected effect, like indenting the first item of a
list.
- The HTML validity requirements are sometimes quite complicated.
- I just haven't had bothered to be systematic about it yet. I've
only covered what's come up in my tests.
I deliberately allow [^dt^] to contain headers and such, in
violation of HTML. If I didn't, then when the user tried to
formatBlock a [^dt^] as a header, it would break apart the whole
[^dl^], which seems worse. See bug
13201.
A node or string child is an
allowed child of a node or string parent if the
following algorithm returns true:
Often we move around nodes, and sometimes this can result in
unreasonable things like two <p>'s nested
inside one another. This algorithm checks for DOMs we never want to
have, so that other algorithms can avoid creating them or fix them if
they do happen. The fix
disallowed ancestors algorithm is one frequently-invoked caller
of this algorithm.
- If parent is "colgroup", "table", "tbody",
"tfoot", "thead", "tr", or an HTML
element with [=Element/local name=] equal to one of those, and
child is a {{Text}} node whose
{{CharacterData/data}} does not consist solely of space characters, return false.
-
Actually, no node can occur in the DOM after plaintext,
generally. But let's not get too carried away.
If parent is "script", "style", "plaintext",
or "xmp", or an HTML element with
[=Element/local name=] equal to one of those, and child is not a {{Text}} node, return false.
- If child is a [=document=],
{{DocumentFragment}}, or {{DocumentType}}, return false.
- If child is an HTML
element, set child to the [=Element/local
name=] of child.
- If child is not a string, return true.
- If parent is an HTML
element:
-
Cannot be serialized as text/html. In some cases it can, like
<a>foo<table><td><a>bar</a></td></table>baz</a>,
but it's invalid in those cases too, so no need for
complication.
If child is "a", and parent or some [=tree/ancestor=] of parent is an [^a^], return false.
-
This generally cannot be serialized either, for p. For other
elements with inline contents, this serves to prevent things
like <span><p>foo</p></span>, which
will parse fine but aren't supposed to happen anyway.
If child is a prohibited paragraph child
name and parent or some
[=tree/ancestor=] of parent is an
element with inline
contents, return false.
-
Also can't be serialized as text/html.
If child is "h1", "h2", "h3", "h4", "h5",
or "h6", and parent or some
[=tree/ancestor=] of parent is an
HTML element with [=Element/local
name=] "h1", "h2", "h3", "h4", "h5", or "h6", return false.
-
Further requirements only care about the parent itself, not
ancestors, so we don't need to know the node itself.
Let parent be the [=Element/local name=]
of parent.
- If parent is an {{Element}} or
{{DocumentFragment}}, return true.
- If parent is not a string, return false.
-
We allow children even where some intervening nodes will be
inserted, like tr as a child of table.
If parent is on the left-hand side of an
entry on the following list, then return true if child is listed on the right-hand side of that entry,
and false otherwise.
- colgroup: col
- table: caption, col, colgroup, tbody, td, tfoot, th, thead,
tr
- tbody, tfoot, thead: td, th, tr
- tr: td, th
- dl: dt, dd
- dir, ol, ul: dir, li, ol, ul
- hgroup: h1, h2, h3, h4, h5, h6
- If child is "body", "caption", "col",
"colgroup", "frame", "frameset", "head", "html", "tbody", "td",
"tfoot", "th", "thead", or "tr", return false.
-
dd/dt/li will serialize fine as the child of random stuff, but it
makes no sense at all, so we want to avoid it anyway.
If child is "dd" or "dt" and parent is not "dl", return false.
- If child is "li" and parent is not "ol" or "ul", return false.
- If parent is on the left-hand side of an
entry on the following list and child is listed
on the right-hand side of that entry, return false.
- a: a
- dd, dt: dd, dt
- h1, h2, h3, h4, h5, h6: h1, h2, h3, h4, h5, h6
- li: li
- nobr: nobr
- All names of an
element with inline contents: all prohibited paragraph child
names
- td, th: caption, col, colgroup, tbody, td, tfoot, th, thead,
tr
- Return true.
Inline formatting commands
Inline formatting command definitions
The difference between "contained" and "effectively contained" is
basically that 1) in <b>[foo]</b>, the text node and the
<b> are effectively contained but not contained; and 2) in
<b>f[o]o</b>, the text node is effectively contained but
not contained, and the <b> is neither effectively contained nor
contained.
A node node is effectively contained in a [=range=]
range if range is not
[=range/collapsed=], and at least one of the following holds:
-
node is contained in
range.
-
So like <b>f[oo]</b> or <b>f[o]o</b> or
<b>f[oo</b>}, but not <b>foo[</b>} or
<b>f[]oo</b>.
node is range's
[=range/start node=], it is a {{Text}} node, and its
[=Node/length=] is different from range's
[=range/start offset=].
-
node is range's [=range/end
node=], it is a {{Text}} node, and range's
[=range/end offset=] is not 0.
-
Basically, anything whose children are all effectively contained
should be effectively contained itself, except that in a case
like <b>f[o]o</b> we don't want <b> to be
effectively contained even though the text node is. That's
because we split the text node before we actually do anything,
and the <b> will no longer be effectively contained.
node has at least one [=tree/child=]; and all
its [=tree/children=] are effectively contained in range; and either range's
[=range/start node=] is not a [=tree/descendant=] of node or is not a {{Text}} node or range's [=range/start offset=] is zero; and either
range's [=range/end node=] is not a
[=tree/descendant=] of node or is not a
{{Text}} node or range's [=range/end offset=]
is its [=range/end node=]'s [=Node/length=].
A modifiable element is a [^b^],
[^em^], [^i^], [^s^], [^span^], [^strike^], [^strong^],
sub,
sup, or [^u^] element with no attributes except possibly
style; or a [^font^] element with no attributes except
possibly
style,
color,
face, and/or
size; or an [^a^] element with no attributes except
possibly
style and/or
href.
A simple modifiable element
is an HTML element for which at least one
of the following holds:
- It is an [^a^], [^b^], [^em^], [^font^], [^i^], [^s^], [^span^],
[^strike^], [^strong^],
sub,
sup, or [^u^] element with no attributes.
- It is an [^a^], [^b^], [^em^], [^font^], [^i^], [^s^], [^span^],
[^strike^], [^strong^],
sub,
sup, or [^u^] element with exactly one attribute, which
is
style, which sets no CSS properties (including invalid
or unrecognized properties).
- It is an [^a^] element with exactly one attribute, which is
href.
- It is a [^font^] element with exactly one attribute, which is
either
color,
face, or
size.
- It is a [^b^] or [^strong^] element with exactly one attribute,
which is
style, and the
style attribute sets exactly one CSS property (including
invalid or unrecognized properties), which is "font-weight".
- It is an [^i^] or [^em^] element with exactly one attribute,
which is
style, and the
style attribute sets exactly one CSS property (including
invalid or unrecognized properties), which is "font-style".
- It is an [^a^], [^font^], or [^span^] element with exactly one
attribute, which is
style, and the
style attribute sets exactly one CSS property (including
invalid or unrecognized properties), and that property is not
"text-decoration".
- It is an [^a^], [^font^], [^s^], [^span^], [^strike^], or [^u^]
element with exactly one attribute, which is
style, and the
style attribute sets exactly one CSS property (including
invalid or unrecognized properties), which is "text-decoration",
which is set to "line-through" or "underline" or "overline" or
"none".
Conceptually, a simple modifiable element is a modifiable element
which specifies a value for at most one command. As the names imply,
inline formatting commands will try not to modify anything other than
modifiable elements. For instance, <dfn>
normally creates italics, but it's not modifiable, so running
the italic
command will not remove it: it will nest <span
style="font-style: normal"> inside.
A formattable node is an editable visible node that is either a {{Text}} node, an [^img^],
or a [^br^].
Two quantities are equivalent
values for a command if either both are
null, or both are strings and they're equal and the command does not define any equivalent values, or both are strings and
the command defines equivalent values and they match the
definition.
Two quantities are loosely
equivalent values for a command if
either they are equivalent values
for the command, or if the command is the
fontSize command; one of the quantities is
one of "x-small", "small", "medium", "large", "x-large", "xx-large",
or "xxx-large"; and the other quantity is the resolved value of
"font-size" on a [^font^] element whose
size attribute has the corresponding value set ("1"
through "7" respectively).
Loose equivalence needs to be used when comparing effective command
values to other values, while regular equivalence is used in other
cases. The effective command value for fontSize is converted to
pixels, so comparing it to a specified value literally would produce
false negatives. But a specified value in pixels is actually
different from a specified value like "small" or "x-large",
because there is no precise mapping from such keywords to pixels.
If a command has inline command activated
values defined but nothing else defines when it is indeterminate, it is indeterminate if among formattable nodes
effectively contained in the
active range, there is at least one whose
effective command value is one
of the given values and at least one whose effective command value is not one of
the given values.
For bold and similar commands, IE 9 RC seems to consider the state
true or false depending on the first element. All other browsers
follow the same general idea as the spec, considering a range bold
only if all text in it is bold, and this seems to match at least
OpenOffice.org's bold feature. Opera 11.11 seemingly doesn't take CSS
into account, and only looks at whether something descends from a
<b>. I couldn't properly test IE9 because it threw exceptions
(Error: Unspecified error.) on most of the tests I ran. But what I
have here seems to match Firefox 6.0a2 in every case, and Chrome 14
dev in all cases with a few exceptions.
If a command has inline command activated
values defined, its state is true if either
no formattable node is effectively contained in the active range, and the active range's [=range/start node=]'s effective command value is one of the
given values; or if there is at least one formattable node effectively contained in the active range, and all of them have an effective command value equal to one
of the given values.
Testing with hiliteColor: Opera 11.11 seems to always return the
effective command value of the active range's start node. Chrome 14
dev returns boolean false consistently, bizarrely enough. Firefox
6.0a2 seems to follow the same idea as the spec, but it likes to
return "transparent", including sometimes when the answer really
clearly should not be "transparent". IE9 throws exceptions most of
the time for backColor, so I can't say for sure, but in the few
cases where it doesn't throw it returns a random-looking number, so
I'll assume it's crazy like for foreColor.
I decided on something that would guarantee the following
invariant: whenever you execute a command with a value provided
(assuming value is relevant), queryCommandValue() will always
return something equivalent to what you set.
If a command is a standard
inline value command, it is indeterminate if among formattable nodes
that are effectively contained
in the active range, there are two that
have distinct effective command values. Its value is the effective command value of the first
formattable node that is effectively contained in the active range; or if there is no such node, the
effective command value of the
active range's [=range/start node=]; or
if that is null, the empty string.
The notions of inline command activated values and standard inline
value commands are mostly just shorthand to avoid repeating the same
boilerplate half a dozen times.
Assorted inline formatting command algorithms
The effective command value
of a node node for a given
command is returned by the following algorithm,
which will return either a string or null:
This is logically somewhat like CSS computed or resolved values, and
in fact for most commands it's identical to CSS resolved values (see
the end of the algorithm). We need a separate concept for some
commands where we can't rely on CSS for some reason: createLink and
unlink aren't CSS-related at all, backColor and hiliteColor need
special treatment because background-color isn't an inherited
property, subscript and superscript rely on <sub>/<sup> instead of
CSS vertical-align, and strikethrough and underline don't map to
unique CSS properties.
- If neither node nor its [=tree/parent=] is an
{{Element}}, return null.
- If node is not an {{Element}}, return the
effective command value of its
[=tree/parent=] for command.
- If command is "createLink" or "unlink":
- While node is not null, and is not an
[^a^] element that has an
href attribute, set node to its
[=tree/parent=].
- If node is null, return null.
- Return the [=Attr/value=] of node's
href attribute.
- If command is "backColor" or "hiliteColor":
- While the resolved
value of "background-color" on node is
any fully transparent value, and node's
[=tree/parent=] is an {{Element}}, set node
to its [=tree/parent=].
-
What happens if everything's background is fully
transparent? See bug.
If you do queryCommandValue() for backColor/hiliteColor
when no backgrounds are set anywhere, so it goes up to the
root element, no two engines agree. IE10PP2 just returns a
random-looking number (16777215). Firefox 8.0a2 returns
"transparent", Chrome 15 dev returns "rgba(0, 0, 0, 0)",
and Opera 11.50 returns "rgb(255, 255, 255)".
Opera's behavior is incorrect. The current page might be an
iframe, in which case the background really will be
transparent and the (inaccessible) background color of the
parent page will show through. Also, the user might have
changed their preferences, but I'm not too worried about
that.
So instead we just return the value as-is. This means that
it will be fully transparent, which is perhaps somewhat
useless information, but it's the best we can do. More
generally, any non-opaque value is not going to tell you
what you actually want, namely "what color is the user
actually seeing?" We have no realistic way to work around
this in the general case.
Return the resolved
value of "background-color" for node.
- If command is "subscript" or "superscript":
- Let affected by subscript and
affected by superscript be two boolean
variables, both initially false.
- While node is an
inline node:
-
Firefox 6.0a2 ignores vertical-align for this purpose,
and only cares about <sub> and <sup> tags
themselves. Opera 11.11 is similar, and in fact behaves
like that even for commands like bold. The spec
originally followed Chrome 14 dev, mainly because WebKit
itself will produce spans with vertical-align sub or
super, and we want to handle them correctly. However,
Ryosuke
informs me that WebKit's behavior here is viewed as
a
bug, so I changed it to match Gecko/Opera.
If node is a
sub, set affected by
subscript to true.
- Otherwise, if node is a
sup, set affected by
superscript to true.
- Set node to its [=tree/parent=].
- If affected by subscript and
affected by superscript are both true,
return the string "mixed".
- If affected by subscript is true, return
"subscript".
- If affected by superscript is true,
return "superscript".
- Return null.
- If command is "strikethrough", and the
"text-decoration" property of node or any of its
[=tree/ancestors=] has resolved value
containing "line-through", return "line-through". Otherwise, return
null.
- If command is "underline", and the
"text-decoration" property of node or any of its
[=tree/ancestors=] has resolved value
containing "underline", return "underline". Otherwise, return null.
- Return the resolved value
for node of the
relevant CSS property for command.
The specified command value
of an {{Element}} element for a given command is returned by the following algorithm, which will
return either a string or null:
This is logically somewhat like CSS inline style. In addition to the
caveats for effective command value, we also treat elements like
<b> and <font> as having the same meaning as <span>s with inline style set, because they're
logically pretty much the same and can in fact be produced by the
same command depending on the CSS styling
flag.
- If command is "backColor" or "hiliteColor"
and the {{Element}}'s display property does not have resolved value
"inline", return null.
- If command is "createLink" or "unlink":
- If element is an [^a^] element and has an
href attribute, return the [=Attr/value=] of that
attribute.
- Return null.
- If command is "subscript" or "superscript":
- If element is a
sup, return "superscript".
- If element is a
sub, return "subscript".
- Return null.
- If command is "strikethrough", and
element has a
style attribute set, and that attribute sets
"text-decoration":
- If element's
style attribute sets "text-decoration" to a value
containing "line-through", return "line-through".
- Return null.
- If command is "strikethrough" and
element is an [^s^] or [^strike^] element,
return "line-through".
- If command is "underline", and
element has a
style attribute set, and that attribute sets
"text-decoration":
- If element's
style attribute sets "text-decoration" to a value
containing "underline", return "underline".
- Return null.
- If command is "underline" and
element is a [^u^] element, return "underline".
- Let property be the relevant CSS property for
command.
- If property is null, return null.
- If element has a
style attribute set, and that attribute has the effect
of setting property, return the value that it
sets property to.
- If element is a [^font^] element that has an
attribute whose effect is to create a presentational hint for
property, return the value that the hint sets
property to. (For a
size of 7, this will be the non-CSS value "xxx-large".)
- If element is in the following list, and
property is equal to the CSS property name listed
for it, return the string listed for it.
- [^b^], [^strong^]: font-weight: "bold"
- [^i^], [^em^]: font-style: "italic"
- Return null.
To record the values of a list of
nodes node list:
When we move nodes around, we often change their parents. If their
parents had any styles applied, this will make the nodes' styles
change too, which often isn't what we want. For instance, if
something is wrapped in <blockquote style="color:
red">, and a script runs the
outdent command on it, the blockquote will
be removed and the style will go along with it. Recording the values
of its children first, then restoring them afterward, will ensure the
nodes don't change color when outdented.
- Let values be a list of (node, command, specified command value) triples,
initially empty.
-
As with removeFormat, we put subscript first so it doesn't
interfere with fontSize, and omit superscript because it's
redundant with subscript.
For each node in node
list, for each command in the list
"subscript", "bold", "fontName", "fontSize", "foreColor",
"hiliteColor", "italic", "strikethrough", and "underline" in that
order:
- Let ancestor equal node.
- If ancestor is not an {{Element}}, set it
to its [=tree/parent=].
- While ancestor is an {{Element}} and its
specified command value
for command is null, set it to its
[=tree/parent=].
- If ancestor is an {{Element}}, add
(node, command,
ancestor's specified command value for
command) to values.
Otherwise add (node, command, null) to values.
- Return values.
To restore the values specified by
a list values returned by the record the values algorithm:
- For each (node, command,
value) triple in values:
- Let ancestor equal node.
- If ancestor is not an {{Element}}, set it
to its [=tree/parent=].
- While ancestor is an {{Element}} and its
specified command value
for command is null, set it to its
[=tree/parent=].
- If value is null and ancestor is an {{Element}},
push down values on node for
command, with new value
null.
- Otherwise, if ancestor is an {{Element}}
and its specified command
value for command is not equivalent to
value, or if ancestor is
not an {{Element}} and value is not null,
force the value of
command to value on node.
Clearing an element's value
To clear the value of an {{Element}}
element:
The idea is to remove any specified command value that the
element might have for the command. This might involve changing its
attributes, setting its tag name, or removing it entirely
while leaving its children in place. The key caller is set the selection's value, which
clears the values of everything in the selection before doing
anything else to keep the markup tidy.
- Let command be the current command.
- If element is not editable, return the empty list.
-
We want to abort early so that we don't try unsetting
background-color on a non-inline element.
If element's specified command value for
command is null, return the empty list.
- If element is a simple modifiable element:
- Let children be the [=tree/children=] of
element.
- For each child in children, insert child into
element's [=tree/parent=] immediately before
element, preserving ranges.
- Remove element from its [=tree/parent=].
- Return children.
- If command is "strikethrough", and
element has a
style attribute that sets "text-decoration" to some
value containing "line-through", delete "line-through" from the
value.
- If command is "underline", and
element has a
style attribute that sets "text-decoration" to some
value containing "underline", delete "underline" from the value.
- If the relevant CSS property
for command is not null, unset that property of
element.
- If element is a [^font^] element:
- If command is "foreColor", unset
element's
color attribute, if set.
- If command is "fontName", unset
element's
face attribute, if set.
- If command is "fontSize", unset
element's
size attribute, if set.
- If element is an [^a^] element and
command is "createLink" or "unlink", unset the
href property of element.
-
If we get past this step, we're something like <b
class=foo> where we want to keep the extra attributes, so we
stick them on a span.
If element's specified command value for
command is null, return the empty list.
-
Set the tag name of element to "span", and return the one-node list consisting of the result.
Pushing down values
This algorithm goes up to just below the nearest ancestor with the
right style, then re-applies the bad styles repeatedly going down,
omitting the things we want to have the new style. This is
basically what WebKit does, although WebKit sometimes starts higher
up and therefore makes more intrusive changes, often creating more
markup. IE follows the same general approach too.
Gecko instead seems to start breaking up elements from the bottom,
so that the range consists of a few consecutive siblings, and it
can then break up the problematic element into a maximum of two
pieces. The spec's approach seems to create fewer elements and
simpler markup (or at least markup that's no more complex) in most
cases I throw at it.
Gecko's approach does have the major advantage that it gets
underlines right in many cases for free. E.g.,
<u>foo<font color=red>[bar]baz</font></u>
-> <u>foo</u><font color=red>bar<u>baz</u></font> (spec)
-> <u>foo</u><font color=red>bar</font><u><font color=red>baz</font></u> (Gecko)
The spec's markup here is much shorter and contains fewer elements,
but is wrong: the underline under "baz" has changed color from
black to red. It might be worth trying to copy Gecko's results in
such cases, but that won't solve all underline problems, so perhaps
it's not worth it.
Opera also seems to break up the markup surrounding the range, but
even more aggressively: even if it doesn't need to pull down
styles. In some cases this does actually result in shorter markup,
specifically if the existing tags are short (like i or b) and we're
adding tags that are long (like span with a style attribute).
To push down values to a node node, given a new value
new value:
The idea here is that if an undesired value is being propagated from
an ancestor, we remove that style from the ancestor and re-apply it
to all the descendants other than node. This way
we don't have to have nested styles, which is usually more cluttered
(although not always).
- Let command be the current command.
-
E.g., a text node child of a document fragment.
If node's [=tree/parent=] is not an
{{Element}}, abort this algorithm.
- If the effective command
value of command is loosely equivalent to
new value on node, abort this algorithm.
- Let current ancestor be node's [=tree/parent=].
- Let ancestor list be a list of nodes, initially empty.
- While current ancestor is an editable {{Element}} and the effective command value of
command is not loosely equivalent to new value on it, append current
ancestor to ancestor list, then set
current ancestor to its [=tree/parent=].
- If ancestor list is empty, abort this
algorithm.
- Let propagated value be the specified command value of
command on the last member of ancestor list.
-
We can only remove specified values, so if the value isn't
specified, give up. Unless we're actually trying to push down a
null specified value, like for unlink.
If propagated value is null and is not equal
to new value, abort this algorithm.
-
If we go all the way up to the root and still don't have the
desired value, pushing down values is pointless. It will create
extra markup for no purpose. Except if the value is null, which
basically just means "try to get rid of anything affecting the
current element but don't aim for any specific value".
Nevertheless, Chrome 14 dev does seem to do this. Running bold
on <span style=font-weight:300>f[o]o</span> breaks
up the span and adds a <b> as a sibling. In IE9, Firefox
6.0a2, and Opera 11.50, it instead nests the <b> inside
the <span>. It's a tradeoff: WebKit's behavior is better
for things like
<font color=red>fo[o</font><font color=blue>b]ar</font>
-> <font color=red>fo</font><font color=green>[ob]</font><font color=blue>ar</font>
(where the spec adds two extra font tags instead of one), but
the spec is simpler for things like
<font color=red>f[o]o</font>
-> <font color=red>f<font color=green>[o]</font>o</font>
(where WebKit splits the existing tag up in addition to
creating a new tag). I'm not particularly sure which approach
is better overall, so I'll go with the majority of browsers. If
these algorithms move to use runs of consecutive siblings
instead of doing everything node-by-node, it might make sense
to break up the parent as long as it won't create an extra node
(i.e., we're styling something that includes the first or last
child).
If the effective command
value of command is not loosely equivalent to new value on the [=tree/parent=] of the last member of
ancestor list, and new
value is not null, abort this algorithm.
- While ancestor list is not empty:
- Let current ancestor be the last member
of ancestor list.
- Remove the last member from ancestor
list.
- If the specified command
value of current ancestor for
command is not null, set propagated value to that value.
- Let children be the [=tree/children=] of
current ancestor.
- If the specified command
value of current ancestor for
command is not null, clear the value of current
ancestor.
- For every child in children:
- If child is node,
continue with the next child.
-
TODO: This will be incorrect for relative font sizes. If
the font size on the parent was removed and the font size
on the child is in ems or percents or something, it will
now change value. This isn't likely to come up, so we'll
ignore it for now.
If child is an {{Element}} whose
specified command
value for command is neither null
nor equivalent to propagated value, continue with the next
child.
- If child is the last member of
ancestor list, continue with the next
child.
-
Force the value of
child, with command
as in this algorithm and new value
equal to propagated value.
Forcing the value of a node
To force the value of a node node to new
value:
This algorithm checks if the node has the desired value, and if not,
it wraps the node (or, if that's not possible, its descendants) in a
simple modifiable element.
After forcing the value, descendants might still have a different
value.
- Let command be the current command.
- If node's [=tree/parent=] is null, abort this
algorithm.
- If new value is null, abort this algorithm.
- If node is an allowed child of "span":
-
Even if the value matches, we stick it in a preceding
sibling if possible. This ensures
"a<cite>b</cite>c" ->
"<i>a<cite>b</cite>c</i>" instead
of
"<i>a</i><cite>b</cite><i>c</i>".
While we're at it, we also handle more elaborate cases like
<b>foo</b>[bar]<b>baz</b> and even
<i><b>foo</b></i>[bar]<i><b>baz</b></i>
(the latter becomes
<b><i>foo</i>bar<i>baz</i></b>).
Theoretically this algorithm could pointlessly reorganize
the DOM in the event of unreasonable style rules, but it's
not a big enough deal for us to care, since the resulting
style will still be right.
Reorder modifiable
descendants of node's {{Node/
previousSibling}}.
-
Reorder modifiable
descendants of node's
{{Node/nextSibling}}.
-
The new parent instructions we'd want are
too complicated to reasonably feed into the wrap algorithm,
so we just let them return null and do the wrapping ourselves
if sibling criteria didn't return true.
Wrap the one-node list consisting of node, with sibling criteria
returning true for a simple modifiable element
whose specified command
value is equivalent to new
value and whose effective command value is
loosely equivalent to
new value and false otherwise, and with
new parent instructions returning null.
-
Bug
13996.
If node is invisible, abort this algorithm.
- If the effective command
value of command is loosely equivalent to
new value on node, abort this algorithm.
- If node is not an allowed child of "span":
- Let children be all [=tree/children=] of
node, omitting any that are {{Element}}s
whose specified command
value for command is neither null nor
equivalent to new
value.
-
This means that if it has no children, we do nothing. IE9
inserts an empty wrapper element in that case, but I'm not
sure what the point is, and no one else does, so I don't.
WebKit seems to ignore the node if its only child consists
solely of whitespace, but I don't see any grounds for that
and no one else does, so I don't.
Force the value of each
node in children, with
command and new value
as in this invocation of the algorithm.
- Abort this algorithm.
- If the effective command
value of command is loosely equivalent to
new value on node, abort this algorithm.
- Let new parent be null.
- If the CSS styling flag is false:
- If command is "bold" and new value is "bold", let new parent
be the result of calling
createElement("b")
on the {{Node/ownerDocument}} of node.
- If command is "italic" and
new value is "italic", let new parent
be the result of calling
createElement("i")
on the {{Node/ownerDocument}} of node.
-
TODO: Actual UAs use strike, not s, but s is shorter and
HTML5 makes strike invalid. I've gone with s for now, but
maybe we want to change the spec to require strike.
If command is "strikethrough" and
new value is "line-through", let
new parent be the result of calling
createElement("s")
on the {{Node/ ownerDocument}} of node.
- If command is "underline" and
new value is "underline", let new parent be the result of calling
createElement("u")
on the {{Node/ownerDocument}} of node.
-
See comment for foreColor for discussion. TODO: Define more
carefully what happens when things are out of range or not
integers or whatever.
If command is "foreColor", and
new value is fully opaque with red,
green, and blue components in the range 0 to 255:
- Let new parent be the result of
calling
createElement("font")
on the {{Node/ ownerDocument}} of node.
- Set the
color attribute of new
parent to the result of applying the
rules for serializing simple color values to
new value (interpreted as a
simple color).
- If command is "fontName", let
new parent be the result of calling
createElement("font")
on the {{Node/ownerDocument}} of node, then
set the
face attribute of new parent to
new value.
- If command is "createLink" or "unlink":
- Let new parent be the result of calling
createElement("a")
on the {{Node/ownerDocument}} of node.
- Set the
href attribute of new parent to
new value.
-
Nested a elements are bad, because they can't be serialized
to text/html. hrefs should already have been cleared in a
previous step, but we might have <a name> or such
lurking about.
Let ancestor be node's [=tree/parent=].
- While ancestor is not null:
-
TODO: This will mean any link-specific attributes will be
transferred, which makes them both invalid and useless.
Is that okay? I don't really want to list them all,
because that sort of list is prone to bitrot.
If ancestor is an [^a^], set the tag name of ancestor to "span", and let ancestor be the result.
- Set ancestor to its [=tree/parent=].
-
WebKit is the only engine that ever outputs anything but font
tags for fontSize. For size=7, it uses font-size:
-webkit-xxx-large. We just output a font tag no matter what for
size=7.
If command is "fontSize"; and new value is one of "x-small", "small", "medium",
"large", "x-large", "xx-large", or "xxx-large"; and either the
CSS styling flag is false, or
new value is "xxx-large": let new parent be the result of calling createElement("font")
on the {{Node/ownerDocument}} of node, then
set the
size attribute of new parent to
the number from the following table based on new
value:
- x-small: 1
- small: 2
- normal: 3
- large: 4
- x-large: 5
- xx-large: 6
- xxx-large: 7
-
We always use sup/sub elements, even in CSS mode, following Gecko
and contradicting WebKit. This is because <span
value="vertical-align: sub/super">, the obvious equivalent
(and what WebKit uses), behaves quite differently: it doesn't
reduce font-size, which is ugly. WebKit's behavior is a bug anyway.
If command is "subscript" or "superscript"
and new value is "subscript", let new parent be the result of calling createElement("sub")
on the {{Node/ownerDocument}} of node.
- If command is "subscript" or "superscript"
and new value is "superscript", let new parent be the result of calling
createElement("sup")
on the {{Node/ownerDocument}} of node.
- If new parent is null, let new
parent be the result of calling
createElement("span")
on the {{Node/ownerDocument}} of node.
-
This preserves boundary points correctly, as usual.
Insert new parent in node's [=tree/parent=] before node.
- If the effective command
value of command for new
parent is not loosely equivalent to
new value, and the relevant
CSS property for command is not null, set
that CSS property of new parent to new value (if the new value would be valid).
Need to be explicit. I think "if the new value would be valid"
means "if the new value isn't xxx-large for font-size", need to
double-check.
- If command is "strikethrough", and
new value is "line-through", and the effective command value of
"strikethrough" for new parent is not
"line-through", set the "text-decoration" property of new parent to "line-through".
- If command is "underline", and
new value is "underline", and the effective command value of
"underline" for new parent is not "underline",
set the "text-decoration" property of new
parent to "underline".
- Append node to new parent
as its last [=tree/child=], preserving
ranges.
- If node is an {{Element}} and the effective command value of
command for node is not
loosely equivalent to new value:
- Insert node into the [=tree/parent=] of
new parent before new
parent, preserving ranges.
- Remove new parent from its
[=tree/parent=].
- Let children be all [=tree/children=] of
node, omitting any that are {{Element}}s
whose specified command
value for command is neither null nor
equivalent to new
value.
-
Force the value of each
node in children, with
command and new value
as in this invocation of the algorithm.
To reorder modifiable
descendants of a node node, given a command command and a value
new value:
- Let candidate equal node.
- While candidate is a modifiable element, and candidate has exactly one [=tree/child=], and that
[=tree/child=] is also a modifiable
element, and candidate is not a simple modifiable element or
candidate's specified command value for
command is not equivalent to new
value, set candidate to its
[=tree/child=].
- If candidate is node, or
is not a simple modifiable
element, or its specified
command value is not equivalent to new value,
or its effective command value
is not loosely equivalent to
new value, abort these steps.
- While candidate has [=tree/children=], insert
the first [=tree/child=] of candidate into
candidate's [=tree/parent=] immediately before
candidate, preserving ranges.
-
If candidate had no children, any boundary point inside it will
get moved to its parent here, which is okay. We don't want to
preserve ranges, because that would move boundary points that
originally were in candidate but were moved to its parent by
the last step to move to node's parent.
We move to after node so that boundary points before and after
node wind up consistently inside candidate when we move
preserving ranges. If we had
{<node>foo<candidate></candidate></node>}
it thus becomes
{<node>foo</node>}<candidate></candidate>
by the range mutation rules, and then when we move preserving
ranges, it becomes
<candidate>{<node>foo</node>}</candidate>
which is reasonable.
If we had inserted candidate before node, instead it would go
{<candidate></candidate><node>foo</node>}
{<candidate><node>foo</node>}</candidate>
because of the interaction of regular range mutation rules with
preserving-ranges rules.
Insert candidate into node's [=tree/parent=] immediately after node.
- Append the node as the last [=tree/child=] of
candidate, preserving ranges.
Setting the selection's value
To set the selection's
value to new value:
The effect of this algorithm is to ensure that all nodes effectively contained in the selection
have the value requested, producing the simplest markup possible to
achieve that effect. It's inspired by the approach WebKit takes.
The only places where the algorithm should fail are when there's an
!important CSS rule that conflicts with the requested style (which
we don't try to override because we assume it's !important for a
reason), or when it's literally impossible to succeed (such as when
a text-decoration or link URL is propagated from a non-editable
ancestor). Any other failures are bugs.
First, if a node has a specified
command value for the command, we unset it (clear its value).
This step also removes simple modifiable elements
entirely, and replaces elements like [^b^] or [^font^] with
[^span^]s if they aren't simple modifiable elements. This will be
sufficient if the desired value is inherited from an ancestor, or
if it's the default (like font-style: normal) and no conflicting
value is inherited from an ancestor. Even if clearing values
doesn't actually fix the style of the node we're dealing with, we
do it anyway to simplify the generated markup.
If clearing values didn't work, and it looks like an ancestor has a
specified command value that
we're inheriting, we push the value down from that ancestor. Thus
if we're unbolding the letter "r" in
foo bar baz,
we get
foo bar baz.
If we didn't push down values, the final step (forcing values)
would instead give us
foo bar baz,
which is much longer and uglier. We take care not to disturb the
style or semantics of anything but the node we're dealing with.
We'll only push down values if some ancestor actually has the value
we want, so we can inherit it. Otherwise, it will just create
useless markup.
Finally, if neither of the above strategies worked, we have to add
new markup to get the desired value (forcing the value). First we try just
sticking it into its previous or next sibling, if that's a simple modifiable element (so it
won't add any styles or semantics we don't want). Otherwise, we
create a new simple modifiable element and wrap it in that. It's
common that a previous sibling is the simple modifiable element we
want, because often we'll set the value of several consecutive
siblings in succession. In that case, the element created for the
first can be reused for the later ones.
This last step works a bit differently if the node isn't an
allowed child of "span". In that case,
wrapping it in a simple modifiable element would make the document
less conforming than it already was, or would cause other problems.
Instead, we recursively force the value of its children. The
recursion will terminate when we hit a node that's an allowed child
of "span", or when there are no further descendants. (In the latter
case, there are no descendants that are text nodes or such, so we
don't really need to style anything.)
After all this, the node is guaranteed to have the value we want,
barring bugs in the algorithm or the two exceptions noted earlier
(!important style rules, and impossible cases). We then re-run the
algorithm on each child recursively. Typically this means just
clearing the value of each descendant, because it should then
inherit the value we just set on its ancestor. In the unusual case
that a descendant's value is wrong even after we clear its value,
such as because of a non-inline style rule (like trying to unbold a
heading), we'll repeat the above steps to ensure that the value
really gets set as desired.
- Let command be the current command.
-
IE9 seems to wrap the whole line instead, or something like that,
although it does nothing for createLink. We follow all other
browsers' general behavior: change the state/value, and then make
sure that takes effect if the user types something before
changing the cursor position.
If there is no formattable node
effectively contained in the
active range:
- If command has inline command activated
values, set the state override
to true if new value is among them and false
if it's not.
- If command is "subscript", unset the
state override for "superscript".
- If command is "superscript", unset the
state override for "subscript".
- If new value is null, unset the
value override (if any).
- Otherwise, if command is "createLink" or
it has a value specified, set the value override to new
value.
- Abort these steps.
-
The last sentence here just prettifies the resulting range a bit.
If the active range's [=range/start
node=] is an editable {{Text}} node, and
its [=range/start offset=] is neither zero nor its [=range/start
node=]'s [=Node/length=], call splitText()
on the active range's [=range/start
node=], with argument equal to the active
range's [=range/start offset=]. Then set the active range's [=range/start node=] to the
result, and its [=range/start offset=] to zero.
- If the active range's [=range/end
node=] is an editable {{Text}} node, and its
[=range/end offset=] is neither zero nor its [=range/end node=]'s
[=Node/length=], call
splitText()
on the active range's [=range/end node=],
with argument equal to the active range's
[=range/end offset=].
- Let element list be all
editable {{Element}}s effectively contained in the active range.
- For each element in element
list, clear the value of
element.
-
We skip non-editable nodes.
-
IE9
-
Allows everything to be modified by execCommand(), regardless
of whether it's editable.
-
Firefox 4.0
-
Ignores execCommand() if the start and end of the selection
are not both editable. If the start and end are editable but
something in the middle is not, seems to relocate the
non-editable part in the middle or something like that.
-
Chrome 12 dev
-
Ignores execCommand() if the start and end of the selection
are not both editable. If the start and end are editable but
something in the middle is not, applies the given command but
skips the non-editable parts. But the state doesn't ignore
the non-editable parts, so if you bold such a selection you
can't unbold it, for instance, since the middle part will
remain bold (so it will keep on trying to bold it instead of
switching to unbold).
-
Opera 11.00
-
Ignores execCommand() if the start and end of the selection
are not both editable. If the start and end are editable but
something in the middle is not, applies the command to
everything, even the non-editable part.
I chose to go with the non-IE behavior, per
discussion. Ignoring non-editable things is convenient for
the common use-case of an editor, where you don't want the user
to bold random parts of the UI when they hit the bold button.
For cases where it's not desired, you can always turn
designMode on briefly before using execCommand(), so the non-IE
behavior is a lot easier to work around than the IE behavior.
I don't see the value in ever just ignoring execCommand(). If
the start and end are not editable, I'm going to say you should
still style any editable nodes in between. I'm also going to
ignore non-editable nodes for the purposes of determining
state, so (for instance) if all the editable nodes are bolded,
it will unbold instead of bolding.
Let node list be all editable nodes effectively contained in the
active range.
-
TODO: This is inefficient. It would be most efficient to only
push down values on the highest-level effectively contained
nodes, and to batch operations so we handle runs of adjacent
siblings at once. Should we bother fixing this?
For each node in node
list:
-
Push down values on node.
-
If the node isn't an allowed child of "span", forcing its
value will just force its children's value, which is
redundant. So don't.
If node is an allowed child of "span", force the value of node.
The backColor command
This command must not be enabled if the
editing host is in the
plaintext-only state.
For historical reasons, backColor and hiliteColor behave identically.
We have three behaviors to choose from for this one:
- Chrome 11 dev and IE 9 RC treat it the same as hiliteColor
(although IE 9 RC doesn't support hiliteColor itself).
- Firefox 4 in non-CSS mode sets the bgcolor of the nearest td or
body, or something like that. In testing, it seems to jump out of
contenteditable elements to style non-editable ancestors, which is
alarming.
- Firefox 4 in CSS mode and Opera 11 set the background of the
nearest block container, although it doesn't seem to be very
dependable (probably I just don't get what exactly it's doing).
(1) is obviously redundant, but has plurality support, so we could
spec it that way if the other ways were useless.
(3) is incoherent from a user perspective. For instance, if you try
it on paragraphs the background will have big gaps where the
margins are. If you try it on an inline element that's a child of
the editing host, it will do nothing or apply the background to
everything or such, even though such an inline element is visually
indistinguishable from one sitting inside a div. This would only
make sense if we take considerable effort to ensure that block
elements all have no margins, or if we wrap things in a div if they
have margins, or something like that.
That leaves (2). That might be useful if it actually set the
document's background color, but it seems like it sets table cell
backgrounds sometimes instead, which is really confusing.
The path of least resistance is to standardize this as meaning the
same thing as hiliteColor, and make up new commands if we want to
do things like set the document background color. See hiliteColor
for comments.
Action:
- If value is not a valid CSS color, prepend
"#" to it.
- If value is still not a valid CSS color, or
if it is currentColor, return false.
-
Set the selection's value
to value.
- Return true.
Standard inline value
command
Relevant CSS property:
"background-color"
Equivalent values: Either both
strings are valid CSS colors and have the same red, green, blue, and
alpha components, or neither string is a valid CSS color.
The bold command
This command must not be enabled if the
editing host is in the
plaintext-only state.
If the selection is collapsed (but not if it contains nothing but is
not collapsed), IE9 wraps the whole line in a <strong>. This
seems bizarre and no one else does it, so I don't do it. It's a
similar story for similar commands (fontName, italic, etc.). Except
not for strikethrough, where it just does nothing if the selection is
empty. Why strikethrough? I don't know.
Action: If queryCommandState("bold") returns
true, set the selection's
value to "normal". Otherwise set the selection's value to "bold".
Either way, return true.
The cutoff of 600 matches Chrome 14 dev. The cutoff used by IE9 and
Firefox 6.0a2 seems to be 500, and the distinction isn't relevant
for Opera 11.11 (it doesn't use CSS here at all AFAICT). On my test
systems with default fonts, Chrome 14 dev displays 700 and up as
bold, while the other three display 600 and up as bold.
Thus in Chrome on my system, the bold command will behave a bit
oddly the first time you hit it if there's anything in the range
with font-weight: 600, but it will look right in other browsers. On
the other hand, if I followed IE/Firefox, it would look wrong on
all my browsers for font-weight: 500.
700 actually makes more sense: then you'd view 100-300 as light,
400-600 as medium, 700-900 as bold. But that's not how it seems to
work in browsers, so I'll go with 600 as the cutoff.
Inline command activated
values: "bold", "600", "700", "800", or "900"
Relevant CSS property:
"font-weight"
Equivalent values: Either the two
strings are equal, or one is "bold" and the other is "700", or one is
"normal" and the other is "400".
The createLink command
This command must not be enabled if the
editing host is in the
plaintext-only state.
If the selection doesn't contain anything (meaning, e.g.,
deleteContents() doesn't change anything), then Chrome 12 dev
inserts a link at the selection start, with the text equal to the
link URL. Other browsers don't do it, so I don't either.
IE10PP2, Firefox 7.0a2, Chrome 14 dev, and Opera 11.50 all do not
support indeterminate, state, or value for createLink or unlink. I
previously defined indeterminate and value anyway because they make
sense, but then undefined
them. The nontrivial thing is what value to return if there's
no link, since any string can occur as a link href, in principle.
What are the use-cases for indeterm, state, or value for
createLink/unlink?
Action:
-
Firefox 4b11 and Chrome 11 dev both silently do nothing in this
case. IE 9 RC and Opera 11 both treat the request literally.
Gecko and WebKit probably have it right here: users who enter no
URL are very unlikely to want to link to a relative URL resolving
to the current document. If they really want to, they can always
specify "#" for the value, or the author can rewrite it, so it's
not like this makes the API less useful.
If value is the empty string, return false.
-
There are three approaches here. For instance, if you ask
browsers to create a link to "http://example.org" on the "b"
here:
<a href=http://example.com><b>Abc</b></a>
Chrome 10 dev produces:
<b><a href=http://example.com>A</a><a href=http://example.org>b</a><a href=http://example.com>c</a></b>
Firefox 4b11 produces (roughly):
<a href=http://example.com><b>A<a href=http://example.org>b</a>c</b></a>
(This doesn't round-trip through text/html serialization.) IE 9
RC and Opera 11 produce simply:
<a href=http://example.org><b>Abc</b></a>
The last behavior probably best matches user expectations. If
you happen to miss out a character when selecting the link you
want to change, do you really intend to only change the link of
part of it?
For each editable [^a^] element that has
an
href attribute and is an [=tree/ancestor=] of some
node effectively contained in the
active range, set that [^a^]
element's
href attribute to value.
-
Set the selection's value
to value.
- Return true.
The fontName command
This command must not be enabled if the
editing host is in the
plaintext-only state.
UAs differ a bit in the details here:
-
IE 9 RC
-
Empty string sets <font face="">
-
Firefox 4b11
-
Empty string does nothing
-
Chrome 11 dev
-
Empty string does nothing, '"monospace"' same as 'monospace'
(i.e., cannot escape font-family keywords because quotes are
stripped, clearly wrong)
-
Opera 11
-
Empty string sets <font face="">
Setting an empty font-family has the effect of inheriting the font
from the parent (although I don't see where the February 24, 2011
CSS 3 Fonts draft says that). Thus it makes sense that if we
special-case this, it should be to unset the font somehow.
Special-casing the empty string to do nothing doesn't make sense to
me. With createLink we'd expect the user to enter the URL
themselves, so it makes sense to special-case clicking OK without
entering anything. But here it's very likely that the font list
will be fixed by the author (how many users will understand CSS
font-family syntax?), so I don't think such usability concerns
apply.
Action: Set the selection's value to
value, then return true.
The value is complicated.
-
IE 9 RC
-
Always the empty string. Not very useful.
-
Firefox 4b11
-
Confusing. Sometimes it returns generic family names, like
"sans-serif". Sometimes it gives specific font names, like "tt"
when the font is specified as "monospace". Sometimes it gives the
literal font-family string. Not sure what it's doing here.
-
Chrome 11 dev
-
Gives the literal value of font-family, except if it's inherited
from default values (no explicit style declarations anywhere),
when it seems to return the exact font name.
-
Opera 11
-
Returns the literal value of font-family, except if it's
inherited from default values, when it returns the empty string.
I'm just going to punt on this and say it should be the resolved
value of font-family. I'll leave CSSOM to decide what that means if
there are no applicable style rules.
Standard inline value
command
Relevant CSS property:
"font-family"
The fontSize command
This command must not be enabled if the
editing host is in the
plaintext-only state.
-
IE 9
-
Parses the value as a number (allowing floating-point), rounds to
the nearest integer, then clamps to the range 1 to 7. If the
value is not a valid number, including if it has trailing
characters (like "2em"), does nothing. Normalizes relative sizes,
so "+0" is the same as "+3", etc. Treats empty string the same as
"1".
-
Firefox 4.0
-
Passes the value through literally to <font size=>, so
"2em" gets you <font size="2em">. Always uses <font>,
even with styleWithCss true. Ignores the command if the value is
the empty string.
-
Chrome 12 dev
-
Parses the value as a legacy font size, so "2em" becomes "2",
then outputs a <font> with the resulting number. If there
is no resulting number, like for a value of "xx-small", does
nothing. In styleWithCss mode, outputs a span with corresponding
CSS keywords: 1 = x-small, 2 = small, . . ., 6 = xx-large, 7 =
-webkit-xxx-large. Normalizes relative sizes, so "+0" is the same
as "3", etc. Ignores the command if the value is the empty
string.
-
Opera 11
-
Parses the value as an integer (ignoring floating-point as
trailing characters), then outputs that. This means that "+0"
becomes <font size=0> instead of <font size=+0> or
<font size=3>. Non-numeric values get interpreted as 0.
Does not clamp, and is willing to output negative numbers. Treats
empty string as "0".
What all of these have in common is that they force the author to
deal with legacy font values and don't let them use CSS. This is
undesirable, but to avoid it we'd really have to create a new
command. If nothing else, the value returned by queryCommandValue() has to be numeric, so authors can't
really use the command sanely no matter what we do. See bug
14251.
Note that 1 is the same size as x-small in browsers, not xx-small,
contrary to the CSS Fonts spec.
Action:
-
Strip leading and trailing whitespace from value.
- If value is not a
valid floating point number, and would not be a
valid floating point number if a single leading "+" character
were stripped, return false.
- If the first character of value is "+",
delete the character and let mode be
"relative-plus".
- Otherwise, if the first character of value is
"-", delete the character and let mode be
"relative-minus".
- Otherwise, let mode be "absolute".
- Apply the
rules for parsing non-negative integers to value, and let number be the result.
- If mode is "relative-plus", add three to
number.
- If mode is "relative-minus", negate
number, then add three to it.
- If number is less than one, let number equal 1.
- If number is greater than seven, let
number equal 7.
- Set value to the string here corresponding to
number:
The entry for 7 here is an issue: there's no CSS value that
corresponds to it. Even if we got one added to the drafts, it
wouldn't be backward-compatible to use it. WebKit is the only
engine that supports CSS output for fontSize, and it uses
-webkit-xxx-large in this case, which is unworkable. Instead, we
just always output a font tag for size 7. If authors want
conforming markup, they'll need to give CSS sizes above size 7,
not legacy sizes.
- 1: x-small
- 2: small
- 3: medium
- 4: large
- 5: x-large
- 6: xx-large
- 7: xxx-large
-
Set the selection's value
to value.
- Return true.
This follows Firefox 6.0a2. Chrome 14 dev always returns false. Note
that indeterminacy here keys off the effective command value, while
the value is based only on an approximation (a number from one to
seven). Thus it's possible for every subrange of the selection to
have the same value, but for the selection to still be indeterminate.
Setting the fontSize to the value will make it determinate without
changing anything's value.
Indeterminate: True if among formattable nodes
that are effectively contained
in the active range, there are two that
have distinct effective command values. Otherwise
false.
-
IE9
-
Seems to return a number based on the computed font-size, but
only if it's exactly right, otherwise it returns null. Something
like that.
-
Firefox 6.0a2
-
Seemingly goes up to the nearest ancestor that's a <font
size> and returns the literal value of that attribute, or ""
if there's no such ancestor.
-
Chrome 14 dev
-
Gets the computed font-size in pixels, and rounds to the nearest
<font size> equivalent, rounding up in the event of a tie.
Except that if it's small enough, it returns "0", which doesn't
make sense because that behaves the same as "1".
-
Opera 11.11
-
Like Firefox, except it returns "3" if there's no <font
size> ancestor, and it converts relative values to absolute
("+1" -> "4").
Chrome's behavior seems the most useful. As usual, IE returns a
variable type and all other browsers return strings, and we follow
other browsers.
If the selection isn't someplace editable, Chrome works like usual;
some other browsers behave differently. I see no reason to behave
differently.
Value:
- If the active range is null, return
the empty string.
-
See comment for standard inline value commands on how I decided
on this choice of node.
Let pixel size be the effective command value of the
first formattable node that is
effectively contained in the
active range, or if there is no such
node, the effective command
value of the active range's
[=range/start node=], in either case interpreted as a number of
pixels.
- Return the legacy font size
for pixel size.
Relevant CSS property:
"font-size"
The legacy font size for an
integer pixel size is returned by the following
algorithm:
- Let returned size be 1.
- While returned size is less than 7:
- Let lower bound be the resolved
value of "font-size" in pixels of a [^font^] element whose
size attribute is set to returned
size.
- Let upper bound be the resolved
value of "font-size" in pixels of a [^font^] element whose
size attribute is set to one plus returned size.
- Let average be the average of
upper bound and lower
bound.
- If pixel size is less than
average, return the one-code
unit string consisting of the digit returned
size.
- Add one to returned size.
- Return "7".
The foreColor command
This command must not be enabled if the
editing host is in the
plaintext-only state.
Color interpretations:
IE10PP2 Firefox 7.0a2 Chrome 14 dev Opera 11.50
blue blue blue #0000ff #0000ff
f #f - - #f00000
#f #f - - #f00000
00f #00f - #0000ff #00000f
#00f #00f rgb(0, 0, 255) #0000ff #00000f
0000ff #0000ff - #0000ff #0000ff
#0000ff #0000ff rgb(0, 0, 255) #0000ff #0000ff
000000fff #0000ff - - -
#000000fff #0000ff - - -
rgb(0, 0, 255) rgb(0,0,255) rgb(0, 0, 255) #0000ff #00b000
rgb(0%, 0%, 100%) rgb(0,0,255) rgb(0, 0, 255) #0000ff #00b000
rgb( 0 ,0 ,255) rgb(0,0,255) rgb(0, 0, 255) #0000ff #00b000
rgba(0, 0, 255, 0.0) #ba0000 rgba(0, 0, 255, 0) rgba(0, 0, 255, 0) #00ba00
rgb(15, -10, 375) rgb(15,0,255) rgb(15, 0, 255) #0f00ff #00b015
rgba(0, 0, 0, 1) #ba0010 rgb(0, 0, 0) - #00ba00
rgba(255, 255, 255, 1) #000055 rgb(255, 255, 255) #ffffff #00ba02
rgba(0, 0, 255, 0.5) #ba0000 rgba(0, 0, 255, 0.5) rgba(0, 0, 255, 0.5) #00ba00
hsl(240, 100%, 50%) #000150 rgb(0, 0, 255) #0000ff #000024
cornsilk cornsilk cornsilk #fff8dc #fff8dc
potato quiche #0000c0 - - #000a00
transparent transparent - rgba(0, 0, 0, 0) #00a000
currentColor #c0e000 currentcolor rgba(0, 0, 0, 0) #c000e0
The interpretations given for Firefox are only in styleWithCSS
mode. In non-styleWithCSS mode, it just outputs the string
literally as the <font color> attribute value, which can lead
to different results. The given output for Chrome is for
<font>; the output in styleWithCSS mode is the same, but
rgb() is used instead of hex notation, and "transparent" and
"currentcolor" are passed through under those names. IE and Opera
only support <font> to begin with.
Conclusions:
- Everyone accepts simple color keywords and #xxxxxx notation.
- Opera mangles #xxx, but everyone else handles it fine.
- The leading # is optional in all browsers but Gecko.
- rgb() is accepted by everyone but Opera.
- rgba() and hsl() are accepted by Gecko and WebKit, but rejected
by IE and Opera.
- IE and Opera mangle unrecognized stuff, Gecko and WebKit
ignore.
- Browsers will happily output stuff like "transparent" and
"rgba()" into <font color> even though it won't be uniformly
accepted there.
- Opera and WebKit normalize the output color very aggressively,
Gecko leaves keywords intact but otherwise normalizes for CSS
output (but doesn't normalize at all for <font>), and IE
normalizes inconsistently.
What I'm going to say is that it either has to be a valid CSS
color, or prefixing it with # must result in a valid CSS color. For
<font>, I'll say that the output color
should be normalized to #xxxxxx form. If the color is not a simple
color (fully opaque with all channels between 0 and 255), I'll
force style="" even if styleWithCSS mode is
off. Some of this disagrees with all browsers, but it's unlikely to
hurt and it makes sense.
Action:
-
TODO: Define "valid CSS color" (here and in other color places).
If value is not a valid CSS color, prepend
"#" to it.
-
currentColor is bad for the same reason as relative font sizes.
It will confuse the algorithm, and doesn't seem very useful
anyway.
If value is still not a valid CSS color, or
if it is currentColor, return false.
-
Set the selection's value
to value.
- Return true.
Opera 11 seems to return true for the state if there's some color
style applied, false otherwise, which seems fairly useless; authors
want to use value here, not state. So I'll match other browsers and
not define any state.
For value, the spec essentially matches Firefox 6.0a2 and Chrome 14
dev, as far as how to decide what color the node has. IE9 seems to
always return the number 0 for some bizarre reason. There are some
cases where Firefox returns the empty string for some reason, and
it seems to select the active node a little differently. Opera uses
#xxxxxx format for getComputedStyle() but rgb() here, and also
drops the transparent part of the color if there is any.
Standard inline value
command
Relevant CSS property: "color"
Equivalent values: Either both
strings are valid CSS colors and have the same red, green, blue, and
alpha components, or neither string is a valid CSS color.
The hiliteColor command
This command must not be enabled if the
editing host is in the
plaintext-only state.
For historical reasons, backColor and hiliteColor behave identically.
IE 9 RC doesn't support this. It uses backColor instead, but Gecko
and Opera treat that differently, while all non-IE browsers treat
hiliteColor the same, so I'm standardizing hiliteColor as the way
to highlight text.
This is slightly tricky, because background-color does different
things on block and inline elements. Given the name
("hiliteColor"), we really only want to apply it to inline
elements. This is how everyone but Gecko behaves, but Gecko
sometimes applies it to blocks too. WebKit doesn't set it on
non-inline elements, but does clear it and push it down from them.
The spec doesn't do any of these: background-color on non-inline
elements is not touched by hiliteColor, neither created nor
removed. If users want to remove the style, they need to use
removeFormat. Adding it usually makes no sense; see the comment for
backColor.
For color parsing, see the comment for foreColor.
See bug
13829.
Action:
- If value is not a valid CSS color, prepend
"#" to it.
-
currentColor is bad for the same reason as relative font sizes.
It will confuse the algorithm, and doesn't seem very useful
anyway. For hiliteColor you could conceive of it being useful,
but it will still confuse the algorithm, so ban it for now
anyway.
If value is still not a valid CSS color, or
if it is currentColor, return false.
-
Set the selection's value
to value.
- Return true.
For indeterminacy, this follows no one. Firefox 6.0a2 and Chrome 14
dev both always return false. However, the spec makes sense, since
it's consistent with other commands.
Standard inline value
command
Relevant CSS property:
"background-color"
Equivalent values: Either both
strings are valid CSS colors and have the same red, green, blue, and
alpha components, or neither string is a valid CSS color.
The italic command
This command must not be enabled if the
editing host is in the
plaintext-only state.
Action: If queryCommandState("italic") returns
true, set the selection's
value to "normal". Otherwise set the selection's value to
"italic". Either way, return true.
Inline command activated
values: "italic" or "oblique"
Relevant CSS property:
"font-style"
The removeFormat command
This command must not be enabled if the
editing host is in the
plaintext-only state.
See bug, and
also research by Ryosuke
for WebKit.
Tested in IE 9, Firefox 4.0, Chrome 12 dev, Opera 11.00.
- Tags stripped by everyone: b big cite code dfn em font i ins
kbd s samp small strike strong sub sup tt u var
- Tags left alone by everyone: br hr img
- Unrecognized elements: stripped by Firefox and Opera, left
alone by IE and Chrome.
- blink: stripped only by IE
- abbr: stripped only by Firefox
- a, wbr: stripped only by Opera
- nobr: left alone only by Firefox
- acronym, bdo, q: left alone only by Opera
- bdi, del, mark, span, svg: treated the same as unknown elements
All elements whose default rendering is display: block are left
untouched by all browsers (although IE seems to throw an exception
on <marquee> for some reason).
It's not clear to me why we should leave <a> alone, but
everyone but Opera does. In OpenOffice.org 3.2.1, doing "Default
Formatting (Ctrl+M)" doesn't remove links. In Microsoft Word 2007,
doing "Clear Formatting" also doesn't remove links. Verdict: don't
remove links. Apparently they don't logically qualify as
"formatting".
Conclusion: IE/WebKit is a solid majority by market share and
they're closely interoperable, since WebKit copied IE here. Also,
it makes more sense to assume that unrecognized elements don't
represent any kind of inline formatting, i.e., have a blacklist of
elements to remove instead of a whitelist to keep. Thus I remove
more or less the same things as IE/WebKit.
I remove blink because IE does it and it makes sense, although
Chrome doesn't; I remove abbr although only Firefox does, for
consistency with acronym; and I remove bdi and mark because they're
evidently left alone only because they're unrecognized. Finally, I
remove span because otherwise, something like <span style="font-variant: small-caps"> will be
left intact, which isn't expected and matches no browser except IE.
(Chrome doesn't remove spans in general, but it does remove spans
with style attributes, or something like that.)
Browsers will split up all these inline elements if the selection
is contained within them. Opera does strip unrecognized elements
with display: block if they're within the selection, but doesn't
split them up if they contain the selection.
Chrome 14 dev removes style attributes from every element in the
range, but IE10PP2, Firefox 7.0a2, and Opera 11.50 do not, so I go
with them. As noted above, this means I need to remove spans. I
could conceivably change to remove only spans with style
attributes, but it doesn't seem worth it: I'll just match Gecko.
TODO: This has to be kept in sync when new HTML elements are added.
I need to figure out some way of coordinating this.
A removeFormat candidate is an
editable HTML
element with [=Element/local name=] "abbr", "acronym", "b",
"bdi", "bdo", "big", "blink", "cite", "code", "dfn", "em", "font",
"i", "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", "span",
"strike", "strong", "sub", "sup", "tt", "u", or "var".
Action:
- Let elements to remove be a list of every
removeFormat candidate
effectively contained in the
active range.
- For each element in elements to
remove:
- While element has [=tree/children=],
insert the first [=tree/child=] of element
into the [=tree/parent=] of element
immediately before element, preserving ranges.
- Remove element from its [=tree/parent=].
-
The last sentence just prettifies the resulting range a bit.
If the active range's [=range/start
node=] is an editable {{Text}} node, and
its [=range/start offset=] is neither zero nor its [=range/start
node=]'s [=Node/length=], call splitText()
on the active range's [=range/start
node=], with argument equal to the active
range's [=range/start offset=]. Then set the active range's [=range/start node=] to the
result, and its [=range/start offset=] to zero.
- If the active range's [=range/end
node=] is an editable {{Text}} node, and its
[=range/end offset=] is neither zero nor its [=range/end node=]'s
[=Node/length=], call
splitText()
on the active range's [=range/end node=],
with argument equal to the active range's
[=range/end offset=].
- Let node list consist of all editable
nodes effectively
contained in the active range.
-
TODO: Splitting the parent is really a block algorithm. It's not
clear whether it's desirable to use for inline nodes. Perhaps
it's okay, but it makes me a little uneasy.
For each node in node
list, while node's [=tree/parent=] is a
removeFormat candidate
in the same editing host
as node, split
the parent of the one-node list consisting of node.
-
This step is for cases like <p
style=font-weight:bold>foo[bar]baz</p>, where
splitting/removing tags won't help. We don't need to run
superscript, since subscript does the same thing here. We run
subscript first so <sub>/<sup> won't upset fontSize.
For each of the entries in the following list, in the given
order, set the selection's
value to null, with command as given.
- subscript
- bold
- fontName
- fontSize
- foreColor
- hiliteColor
- italic
- strikethrough
- underline
- Return true.
The subscript command
This command must not be enabled if the
editing host is in the
plaintext-only state.
Action:
- Call
queryCommandState("subscript"), and
let state be the result.
-
Set the selection's value
to null.
- If state is false, set the selection's value to
"subscript".
- Return true.
Indeterminate: True if either among
formattable
nodes that are effectively
contained in the active range, there
is at least one with effective
command value "subscript" and at least one with some other
effective command value; or if
there is some formattable node
effectively contained in the
active range with effective command value "mixed".
Otherwise false.
For <sup><sub>foo</sub></sup>, Firefox 6.0a2
and Opera 11.11 say the state is true for both superscript and
subscript, and indeterminate is false; Chrome 14 dev says it's true
for subscript but not superscript, and indeterminate is false. We
follow neither of these behaviors: we return false for both states,
and say indeterminate is true. The reason is because we want to
return true for a state if we'll do nothing, false if we'll do
something; and if we have nesting like this, we'll always do
something, namely get rid of all those ancestors and replace them
with a single tag. This matches what happens in other indeterminate
situations, so it's fair to consider it indeterminate.
Inline command activated
values: "subscript"
The superscript command
This command must not be enabled if the
editing host is in the
plaintext-only state.
Action:
- Call
queryCommandState("superscript"),
and let state be the result.
-
Set the selection's value
to null.
- If state is false, set the selection's value to
"superscript".
- Return true.
Indeterminate: True if either among
formattable
nodes that are effectively
contained in the active range, there
is at least one with effective
command value "superscript" and at least one with some other
effective command value; or if
there is some formattable node
effectively contained in the
active range with effective command value "mixed".
Otherwise false.
Inline command activated
values: "superscript"
The underline command
This command must not be enabled if the
editing host is in the
plaintext-only state.
TODO: There are a lot of problems with underline color and
thickness, because text-decoration in CSS is horrible. These aren't
prohibitive for normal use and existing browsers don't handle them
either, so fixing these problems or working around them can be put
off for now.
- Pushing down underlines can change their color, since the color
of an underline follows the color of the element where it's
declared instead of the text it's drawn under. This could be fixed
by adding a special case for this condition and inserting extra
color rules, such as by setting a color on the underlining element
and then having another element inside it that resets the color.
Horrible, but that's text-decoration for you. Alternatively, the
new text-decoration-color property in the CSS 3 Text draft could
come in handy here, in which case we'd degrade pretty gracefully in
legacy UAs.
- Underline thickness depends on font-size in all rendering
engines but WebKit, so pushing them down creates thickness problems
as well as color problems. Working around this is a similar story
to the previous, except we have no text-decoration-width property
yet (see
www-style post).
- The preceding two points can't be avoided, because the only way
to remove underlines in CSS is to push down styles (unlike most
other things where you could override it). Recent (February 2011)
CSS 3 Text drafts have added support for a "text-decoration-line:
cancel-underline" property, but we can only use that if there's no
other possibility, since it won't work in legacy browsers.
(Although we should use it once there's no other possibility.)
- More generally, from a user's perspective, color and thickness
of underlines is going to be more or less random if they're
applying them to text with varying size or color. If they underline
a bunch of text all at once, it will all get the same
color/thickness, probably. But if they underline letter-by-letter,
it probably will vary. But sometimes when they underline a bunch of
text at once it will also vary, if the algorithm decides to create
multiple elements for whatever reason (like an intervening
unwrappable node). This is unlikely to match user expectations.
There's not much we can do about this without entirely revamping
text-decoration, so we'll have to live with it.
- Currently we don't treat non-underline text-decorations
properly, because we have no way to set (or cancel) underlines
independently of other text-decorations from within CSS. I've sent
feedback to www-style.
Action: If queryCommandState("underline")
returns true, set the
selection's value to null. Otherwise set the selection's value to
"underline". Either way, return true.
Inline command activated
values: "underline"
The unlink command
This command must not be enabled if the
editing host is in the
plaintext-only state.
IE 9 RC unlinks the whole link you're pointing at, while others
only unlink the current text. The latter behavior seems less
expected, as with createLink, although I can't articulate precisely
why. Word 2007 and OpenOffice.org 3.2.1 (Ubuntu) seem to give an
option to remove the whole link or none of it, which backs the
spec's requirement. See also #whatwg logs starting at 2011-05-13 at
16:53 EDT (UTC-0400).
See comment for the createLink command about indeterm/state/value.
Action:
- Let hyperlinks be a list of every [^a^]
element that has an
href attribute and is contained in the
active range or is an [=tree/ancestor=]
of one of its [=boundary points=].
-
Clear the value of each member of
hyperlinks.
- Return true.
Block formatting commands
Block formatting command definitions
An indentation element is either
a [^blockquote^], or a [^div^] that has a
style attribute that sets "margin" or some subproperty of
it.
We need to allow stuff that sets border/padding because WebKit
(Chrome 12 dev) sets "border: none; padding: 0px" when indenting. We
need to allow stuff that sets classes because WebKit sets
class="webkit-indent-blockquote". We need to allow stuff that sets
dir because IE9 does. The criteria could probably be tightened up a
bit to reduce false positives, but it'll do for now.
A simple indentation
element is an indentation
element that has no attributes except possibly
- a
style attribute that sets no properties other than
"margin", "border", "padding", or subproperties of those; and/or
- a
dir attribute.
The notions of indentation element and simple indentation element
parallel those of modifiable
element and simple
modifiable element.
listing and xmp are included because otherwise insertParagraph inside
them won't work, since paragraphs aren't an allowed child.
A non-list single-line
container is an HTML element with
[=Element/local name=] "address", "div", "h1", "h2", "h3", "h4",
"h5", "h6", "listing", "p", "pre", or "xmp".
A single-line container is
either a non-list
single-line container, or an HTML
element with [=Element/local name=] "li", "dt", or "dd".
The block node of a node node is either a block node or null, as returned by the following
algorithm:
- While node is an inline node, set node to its
[=tree/parent=].
- Return node.
Bug
14062. See also Mozilla bug
590640, specifically comments 48 and on.
If a command preserves overrides, then before taking
its action, the user agent must record current overrides. After
taking the action, if the active range is [=range/collapsed=], it
must restore states and
values from the recorded list.
All block commands preserve overrides except the insertText
command, which treats overrides specially.
Assorted block formatting command algorithms
TODO: When breaking a non-inline element out of an inline element,
like p in b or whatever, it would make sense to re-wrap the contents
in the inline tag.
To fix disallowed ancestors
of node:
We often run this algorithm after we move a node someplace, just in
case it wound up somewhere it's not supposed to be. This avoids
things like unserializable DOMs, blocks nested inside inlines, etc.
- If node is not editable, abort these steps.
-
This case is really intended to handle stuff like list items or
table cells that wander outside their proper place. We generally
convert them into [^p^]s.
If node is not an allowed child of any of its
[=tree/ancestors=] in the
same editing host:
- If node is a [^dd^] or [^dt^], wrap the one-node list consisting of node, with sibling criteria
returning true for any [^dl^] with no attributes and false otherwise, and
new parent instructions returning the result
of calling
createElement("dl")
on the context object.
Then abort these steps.
-
There's no reason to change the node to a paragraph if that
won't make it an allowed child anyway.
If "p" is not an allowed child
of the editing host of
node, abort these steps.
- If node is not a prohibited paragraph child,
abort these steps.
-
Set the tag name of node to the default single-line
container name, and let node be the
result.
-
Because maybe it somehow wound up as the child of a p, like
via insertHTML.
Fix disallowed
ancestors of node.
- Let children be node's [=tree/children=].
- For each child in children, if child is a prohibited paragraph child:
-
Record the values of the
one-node list consisting of child, and let values be the
result.
-
Split the parent of the
one-node list consisting of child.
-
Restore the values from
values.
- Abort these steps.
-
Record the values of the
one-node list consisting of node, and let values be the result.
- While node is not an allowed child of its [=tree/parent=], split the parent of the one-node list consisting of node.
-
Restore the values from
values.
This algorithm implies that we don't support a sublist in the
middle of an item, only at the end. For instance,
<li>foo<ol>...</ol>bar</li>
gets transformed to
<li>foo</li><ol>...</ol><li>bar</li>
which in particular creates an extra list marker for "bar". This is
okay; we don't need to expose all of HTML's markup abilities
through execCommand(). Similarly, the superscript and subscript
commands don't allow nesting. I didn't see any way to get a sublist
in the middle of an item in Word 2007 or in OpenOffice.org 3.2.1
Ubuntu package, nor in any browser using just execCommand(), so it
should be no big problem if we require that such nesting not occur.
(Existing browsers behave weirdly and inconsistently when
confronted with this kind of nesting.)
The reason we need this is that otherwise it gets very confusing to
figure out what happens in cases like trying to outdent
<ol><li>[foo<ol><li>bar]</ol>baz</ol>
If we first normalize, then the natural answer is something like
<p>[foo<ol><li>bar]<li>baz</ol>
but if we don't, we'd have to special-case in the toggle lists and
outdent algorithms. This might be worthwhile, but it's not at all
clear, and what I have works okay, so I'll stick with it for now.
TODO: Investigate fixing this.
To normalize sublists in a
node item:
- If item is not an [^li^] or it is not
editable or its [=tree/parent=] is not
editable, abort these steps.
- Let new item be null.
- While item has an [^ol^] or [^ul^]
[=tree/child=]:
- Let child be the last [=tree/child=] of
item.
- If child is an [^ol^] or [^ul^], or
new item is null and child is a {{Text}} node whose {{CharacterData/ data}}
consists of zero of more space characters:
- Set new item to null.
- Insert child into the [=tree/parent=]
of item immediately following
item, preserving ranges.
- Otherwise:
- If new item is null, let
new item be the result of calling
createElement("li")
on the {{Node/ ownerDocument}} of item,
then insert new item into the
[=tree/parent=] of item immediately
after item.
- Insert child into new
item as its first [=tree/child=], preserving ranges.
The selection's list state is
returned by the following algorithm:
This is just a helper to tell the state and indeterminacy of
the insertOrderedList command and the insertUnorderedList command:
|
ol indeterm
|
ol state
|
ul indeterm
|
ul state
|
|
ol
|
false
|
true
|
false
|
false
|
|
ul
|
false
|
false
|
false
|
true
|
|
mixed
|
true
|
false
|
true
|
false
|
|
mixed ol
|
true
|
false
|
false
|
false
|
|
mixed ul
|
false
|
false
|
true
|
false
|
|
none
|
false
|
false
|
false
|
false
|
- If the active range is null, return
"none".
-
Block-extend the active range, and let new
range be the result.
- Let node list be a list of nodes, initially empty.
- For each node
contained in new range, append node to node list if the last member
of node list (if any) is not an
[=tree/ancestor=] of node; node is editable; node is not an indentation
element; and node is either an [^ol^] or
[^ul^], or the [=tree/child=] of an [^ol^] or [^ul^], or an
allowed child of "li".
- If node list is empty, return "none".
-
The child-of-child case is necessary right now because of the
following:
<ol><li>[foo<ol><li>bar]</ol>baz</ol>
With the current (July 2011) block-extend algorithm, this will
become:
{<ol><li>foo<ol><li>bar</ol>}baz</ol>
because of the magical li handling in block-extend. We want
this to register as ol, because after normalizing sublists it
will become
{<ol><li>foo</li><ol><li>bar</ol>}<li>baz</ol>
But the text node "foo" will wind up in node list, and is not
the child of an ol. This is all very messy and has to do with
questionable decisions about how to handle nested lists.
If every member of node list is either an
[^ol^] or the [=tree/child=] of an [^ol^] or the [=tree/child=]
of an [^li^] [=tree/child=] of an [^ol^], and none is a [^ul^] or
an [=tree/ancestor=] of a [^ul^], return "ol".
-
This condition and the last are mutually exclusive, so the order
is actually irrelevant. Clearly they could only both hold if no
member of node list is an ol or ul, so if they both held, every
member would have to be either the child of an ol and of a ul, or
of an ol and an li, or a ul and an li, or of an li that's the
child of both an ol and a ul. This is impossible unless the list
is empty, in which case we already aborted.
If every member of node list is either a
[^ul^] or the [=tree/child=] of a [^ul^] or the [=tree/child=] of
an [^li^] [=tree/child=] of a [^ul^], and none is an [^ol^] or an
[=tree/ancestor=] of an [^ol^], return "ul".
- If some member of node list is either an
[^ol^] or the [=tree/child=] or [=tree/ancestor=] of an [^ol^] or the
[=tree/child=] of an [^li^] [=tree/child=] of an [^ol^], and some
member of node list is either a [^ul^] or the
[=tree/child=] or [=tree/ancestor=] of a [^ul^] or the [=tree/child=]
of an [^li^] [=tree/child=] of a [^ul^], return "mixed".
- If some member of node list is either an
[^ol^] or the [=tree/child=] or [=tree/ancestor=] of an [^ol^] or the
[=tree/child=] of an [^li^] [=tree/child=] of an [^ol^], return
"mixed ol".
- If some member of node list is either a
[^ul^] or the [=tree/child=] or [=tree/ancestor=] of a [^ul^] or the
[=tree/child=] of an [^li^] [=tree/child=] of a [^ul^], return "mixed
ul".
- Return "none".
When querying the value of justify*, IE9 seems to return boolean
false across the board when it doesn't throw exceptions, which it
usually does in my tests. Chrome 14 dev returns the string "true"
or "false" depending on state, as in other cases, which is useless.
Opera 11.11 returns "" across the board. Firefox 6.0a2 behaves like
with other command values: it returns
"center"/"justify"/"left"/"right" depending on the active range's
start node. Since this is the only behavior that's possibly useful,
it's what I specced. Firefox ties the value closely to the state,
returning true for the state if and only if the value matches the
desired value, but this seems less useful than what I've specced
for the state.
This API is based on the four-state text-align of CSS 2.1. We do
some crude mapping to make it not break too badly with CSS3 values,
but it's not going to work well given the design of the API.
The alignment value of a node node is returned by the
following algorithm:
This is basically like the resolved value of text-align, but with two
key differences. First, it only ever evaluates to
center/justify/left/right, since that's the model that the justify
commands work with. Second, it ignores inline elements, because
text-align has no effect on them and their alignment is actually
governed by their nearest block ancestor (if any).
- While node is neither null nor an
{{Element}}, or it is an {{Element}} but its "display" property has
resolved
value "inline" or "none", set node to its
[=tree/parent=].
-
This means there's no applicable style rule, so probably it will
wind up left-aligned. Of course this ignores the fact that the
alignment will really be "start", so this is wrong for RTL, but
it's a pretty marginal corner case anyway. (It will only happen
if, e.g., everything up to and including the html and body
elements have display: inline or none.)
If node is not an {{Element}}, return "left".
- If node's "text-align" property has
resolved
value "start", return "left" if the directionality of node is "ltr", "right" if it is "rtl".
- If node's "text-align" property has
resolved
value "end", return "right" if the directionality of node is "ltr", "left" if it is "rtl".
- If node's "text-align" property has
resolved
value "center", "justify", "left", or "right", return that
value.
- Return "left".
Sometimes one location corresponds to multiple distinct boundary
points. For instance, in the DOM <p>Hello</p>, a boundary point might lie at the
beginning of the text node or the beginning of the element node, but
these don't logically differ much and will appear the same to the
user, so we often want to treat them the same. The algorithms here
allow navigating through such equivalent boundary points, for when we
want to make the selection as inclusive or exclusive as possible. For
deletion, we want to delete as few nodes as possible, so we move the
start node forward and the end node backward. In other cases we might
do the reverse, expanding the selection. In still other cases we
might want to move forward or backward to try getting to a text node.
Given a [=boundary point=] (node, offset), the next equivalent
point is either a [=boundary point=] or null, as returned by
the following algorithm:
- If node's [=Node/length=] is zero, return
null.
- We don't want to move into or out of zero-length
nodes, because that would move us straight through them. For
instance, if
{}<span></span> were
equivalent to <span>{}</span>, it
would also be equivalent to <span></span>{}. This produces very unexpected
results for nodes like <br>.
- If offset is node's
[=Node/length=], and node's [=tree/parent=] is
not null, and node is an
inline node, return (node's
[=tree/parent=], 1 + node's [=tree/index=]).
- For instance,
<span>foo[]</span> is equivalent to
<span>foo{}</span>, which is
equivalent to <span>foo</span>{}.
However, <p>foo{}</p> is
not equivalent to <p>foo</p>{} – the cursor might look like
it's in a visibly different position.
- If node has a [=tree/child=] with
[=tree/index=] offset, and that [=tree/child=]'s
[=Node/length=] is not zero, and that [=tree/child=] is an inline node, return (that [=tree/child=], 0).
- For instance,
{}<span>foo</span> is equivalent to
<span>{}foo</span>, which is
equivalent to <span>[]foo</span>.
As noted before, though, we don't descend into empty nodes. And
again, {}<p>foo</p> is different
from <p>{}foo</p>.
- Return null.
Given a [=boundary point=] (node, offset), the previous
equivalent point is either a [=boundary point=] or null, as
returned by the following algorithm:
- If node's [=Node/length=] is zero, return
null.
- If offset is 0, and node's [=tree/parent=] is not null, and node is an inline node, return
(node's [=tree/parent=], node's [=tree/index=]).
- If node has a [=tree/child=] with
[=tree/index=] offset − 1, and that
[=tree/child=]'s [=Node/length=] is not zero, and that [=tree/child=]
is an inline node, return (that
[=tree/child=], that [=tree/child=]'s [=Node/length=]).
- Return null.
The first equivalent point of
a [=boundary point=] (node, offset) is returned by the following algorithm:
- While (node, offset)'s
previous equivalent point is
not null, set (node, offset)
to its previous equivalent
point.
- Return (node, offset).
The last equivalent point of a
[=boundary point=] (node, offset) is returned by the following algorithm:
- While (node, offset)'s
next equivalent point is not
null, set (node, offset) to
its next equivalent point.
- Return (node, offset).
Block-extending a range
A [=boundary point=] (node, offset) is a block start
point if either node's [=tree/parent=] is
null and offset is zero; or node has a [=tree/child=] with [=tree/index=] offset − 1, and that [=tree/child=] is either a visible block node or a
visible [^br^].
A [=boundary point=] (node, offset) is a block end point
if either node's [=tree/parent=] is null and
offset is node's
[=Node/length=]; or node has a [=tree/child=]
with [=tree/index=] offset, and that
[=tree/child=] is a visible block node.
A [=boundary point=] is a block
boundary point if it is either a block start point or a block end point.
When a user agent is to block-extend a
[=range=] range, it must run the following steps:
Generally, block commands work on any block that contains part of
the selection, even if the selection doesn't include the whole
block. This algorithm takes an input range, copies it, stretches
out the copy to contain entire blocks, and returns the result. Then
the caller will normally use it instead of the range it started
with. For instance, if the cursor is collapsed in a text node
inside a paragraph, this will generally return a range that
includes the whole paragraph.
Two bits of magic worth noting. First, <br> counts as a block delimiter here, since it
looks the same as a block boundary (assuming no margin etc.) and
this is a visual API. We include the <br> as part of the line that precedes it. Second,
if the selection is inside an <li>,
this will extend it to include the whole <li>. This latter point is weird, and I should
re-examine it sometime, but it seems to work.
- Let start node, start
offset, end node, and end
offset be the [=range/start=] and [=range/end=] nodes and [=boundary point/offsets=] of
range.
- If some [=tree/inclusive ancestor=] of start
node is an [^li^], set start offset to the
[=tree/index=] of the last such [^li^] in [=tree order=], and set
start node to that [^li^]'s [=tree/parent=].
- If (start node, start
offset) is not a block start
point, repeat the following steps:
- If start offset is zero, set it to
start node's [=tree/index=], then set
start node to its [=tree/parent=].
- Otherwise, subtract one from start
offset.
- If (start node, start
offset) is a block boundary
point, break from this loop.
-
This just changes something like <div>{<p>foo]</p></div> to
{<div><p>foo]</p></div>.
While start offset is zero and start node's [=tree/parent=] is not null, set
start offset to start
node's [=tree/index=], then set start
node to its [=tree/parent=].
- If some [=tree/inclusive ancestor=] of end
node is an [^li^], set end offset to one
plus the [=tree/index=] of the last such [^li^] in [=tree order=],
and set end node to that [^li^]'s
[=tree/parent=].
- If (end node, end offset)
is not a block end point, repeat the
following steps:
- If end offset is end
node's [=Node/length=], set it to one plus end node's [=tree/index=], then set end
node to its [=tree/parent=].
- Otherwise, add one to end offset.
- If (end node, end
offset) is a block boundary
point, break from this loop.
- While end offset is end
node's [=Node/length=] and end node's
[=tree/parent=] is not null, set end offset to
one plus end node's [=tree/index=], then set
end node to its [=tree/parent=].
- Let new range be a new [=range=] whose
[=range/start=] and [=range/end=] nodes and [=boundary point/offsets=] are
start node, start offset,
end node, and end offset.
- Return new range.
A node node follows a line break if the following
algorithm returns true:
- Let offset be zero.
- While (node, offset) is
not a block boundary point:
- If node has a visible [=tree/child=] with [=tree/index=]
offset minus one, return false.
- If offset is zero or node has no [=tree/children=], set offset to node's [=tree/index=],
then set node to its [=tree/parent=].
- Otherwise, set node to its [=tree/child=]
with [=tree/index=] offset minus one, then
set offset to node's
[=Node/length=].
- Return true.
A node node precedes a line break if the following
algorithm returns true:
- Let offset be node's
[=Node/length=].
- While (node, offset) is
not a block boundary point:
- If node has a visible [=tree/child=] with [=tree/index=]
offset, return false.
- If offset is node's
[=Node/length=] or node has no
[=tree/children=], set offset to one plus
node's [=tree/index=], then set node to its [=tree/parent=].
- Otherwise, set node to its [=tree/child=]
with [=tree/index=] offset and set
offset to zero.
- Return true.
Recording and restoring overrides
To record current overrides:
- Let overrides be a list of (string, string or
boolean) ordered pairs, initially empty.
-
When restoring, some commands can interfere with others.
Specifically, we want to restore createLink before foreColor
and underline, and subscript and superscript before fontSize.
TODO: This approach only works for default styles (although I'm
not sure offhand how we could handle non-default styles in
principle).
Firefox 7.0a2 and Opera 11.50 don't honor createLink with
collapsed selections. If you insert text, it's not linked. The
spec follows Chrome 14 dev. IE9 also ignores createLink with
collapsed selections, but its behavior in other cases for
collapsed selections is totally different from all other
browsers, so it's not a fair comparison.
If there is a value override for
"createLink", add ("createLink", value
override for "createLink") to overrides.
-
Firefox 7.0a2 and Opera 11.50 will honor repeated
subscript/superscript commands on a collapsed selection, allowing
you to nest them. The spec follows the general philosophy that we
don't allow users to nest subscript/superscript, so the last one
wins. Chrome 14 dev is similar to the spec.
For each command in the list "bold",
"italic", "strikethrough", "subscript", "superscript",
"underline", in order: if there is a state override for command, add (command, command's state override)
to overrides.
- For each command in the list "fontName",
"fontSize", "foreColor", "hiliteColor", in order: if there is a
value override for command, add (command, command's value override) to
overrides.
- Return overrides.
To record current states
and values:
- Let overrides be a list of (string, string or
boolean) ordered pairs, initially empty.
- Let node be the first formattable node effectively contained in the active range, or null if there is none.
- If node is null, return overrides.
- Add ("createLink", node's effective command value for
"createLink") to overrides.
-
Thus we will set state overrides based on the first formattable
node, to match values. This means that if you have <p>foo<b>[bar</b>baz]</p> and
hit backspace and hit A, you'll get <p>foo<b>a[]</b></p>, although
bold was previously indeterminate. This is needed to match the
behavior of hitting A straight away, since innerText doesn't
strip wrappers when it invokes "delete the contents".
For each command in the list "bold",
"italic", "strikethrough", "subscript", "superscript",
"underline", in order: if node's effective command value for
command is one of its inline command activated
values, add (command, true) to
overrides, and otherwise add (command, false) to overrides.
- For each command in the list "fontName",
"foreColor", "hiliteColor", in order: add (command, command's value) to overrides.
-
Special case for fontSize, because its values are weird.
Add ("fontSize", node's effective command value for
"fontSize") to overrides.
This is wrong: it will convert non-pixel sizes to pixel sizes.
But I don't see any way to avoid it. Hopefully it won't come up
too often. font-size is a real problem, because the mapping from
specified value to computed value is lossy and not fully defined
(e.g., how many px is "small"?).
- Return overrides.
To restore states and
values specified by a list overrides
returned by the record current
overrides or record
current states and values algorithm:
- Let node be the first formattable node effectively contained in the active range, or null if there is none.
- If node is not null, then for each
(command, override) pair in
overrides, in order:
- If override is a boolean, and
queryCommandState(command) returns something different from
override, take the action for command, with
value equal to the empty string.
- Otherwise, if override is a string, and
command is neither "createLink" nor
"fontSize", and
queryCommandValue(command) returns something not equivalent to
override, take the action for command, with
value equal to override.
-
This special case is needed because createLink
has no value.
Otherwise, if override is a string; and
command is "createLink"; and either there
is a value override for
"createLink" that is not equal to override, or there is no value override for "createLink" and
node's effective command value for
"createLink" is not equal to override:
take the action for "createLink", with
value equal to override.
-
The override will be some CSS value, so we have to convert it
to a legacy font size.
Otherwise, if override is a string; and
command is "fontSize"; and either there
is a value override for
"fontSize" that is not equal to override,
or there is no value override
for "fontSize" and node's effective command value for
"fontSize" is not loosely equivalent to
override:
- Convert override to an integer number
of pixels, and set override to the
legacy font size for the
result.
- Take the action for "fontSize",
with value equal to override.
- Otherwise, continue this loop from the beginning.
-
If we took the action for a command, we need to reset
node, because it might have changed. For
instance, if the selection was foo[bar]baz, the text node could have been split so
that the first part is now outside the active range.
Set node to the first formattable node effectively contained in the
active range, if there is one.
- Otherwise, for each (command,
override) pair in overrides, in order:
- If override is a boolean, set the
state override for command to override.
- If override is a string, set the
value override for command to override.
Deleting the selection
TODO: Consider what should happen for block merging in corner cases
like display: inline-table.
To delete the selection, given a
block merging flag that defaults to true, a
strip wrappers flag that defaults to true, and a
string direction that defaults to "forward":
The idea behind this algorithm is self-explanatory, but the details
wind up being remarkably complicated.
First, any editable nodes inside the selection will be deleted, and
the selection will be collapsed. By way of contrast, effectively contained tries to expand
the range to include as much as possible, so <p>[foo]</p> contains the <p>. What we do here is contract the range to
include as little as possible, so {<p>foo</p>} contains only foo and doesn't delete the paragraph.
After that, if the selection originally started and ended in
different blocks, and the block merging flag is
true, the end block will get merged into the start block. This is
needed so if the user selects text on several lines and deletes it,
the text immediately that was before the selection winds up on the
same line as the text immediately after it. For example,
<p>fo[o</p><div>b]ar</div>
becomes <p>fo[]ar</p>. This
procedure winds up being tricky, and takes up a large chunk of the
logic.
Tables are a notable special case. If an entire table is contained
in the range, it will be deleted. If it's anything less, only the
contents of the cells will be deleted and the table structure will
be left intact.
The strip wrappers flag controls what happens
if the deletion removes all the contents of an inline element. If
wrappers are being stripped, the empty inline element will be
removed: this is usually what you want, because the user can't
position the selection inside it. But callers like the insertText
command that intend to immediately insert new contents want to
leave the wrappers, so the new contents are wrapped by the same
thing as the old.
Even if strip wrappers is true, the algorithm
will set a state override and
value override for any styles it
winds up removing. This way, if the user deletes a wrapper that
adds a style (or link for that matter), then types something, the
new text will get the style from the old text.
- If the active range is null, abort
these steps and do nothing.
-
Canonicalize whitespace at
the active range's [=range/start=].
-
Canonicalize whitespace at
the active range's [=range/end=].
- Let (start node, start
offset) be the last equivalent
point for the active range's
[=range/start=].
- Let (end node, end
offset) be the first
equivalent point for the active
range's [=range/end=].
- If (end node, end offset)
is not [=boundary point/after=] (start node,
start offset):
This is a selection like <span>foo[</span></span>]bar</span>,
where the boundary points are equivalent but not identical. We
just collapse it and abort, since there's nothing to delete.
- If direction is "forward", call
collapseToStart() on
the context object's
selection.
- Otherwise, call
collapseToEnd() on the
context object's
selection.
- Abort these steps.
- If start node is a {{Text}} node and
start offset is 0, set start
offset to the [=tree/index=] of start node,
then set start node to its [=tree/parent=].
- If end node is a {{Text}} node and
end offset is its [=Node/length=], set
end offset to one plus the [=tree/index=] of
end node, then set end node
to its [=tree/parent=].
The previous two steps are so that we won't leave empty text
nodes anywhere.
- Call
collapse(start node,
start offset) on the context object's
selection.
- Call
extend(end node,
end offset) on the context object's
selection.
-
When we delete a selection that spans multiple blocks, we merge
the end block's contents into the start block, like
<p>fo[o</p><pre>b]ar</pre>
-> <p>fo[]ar</p>.
We figure out what the start and end blocks are before we start
deleting anything.
- Let start block be the active range's [=range/start node=].
- While start block's [=tree/parent=] is
in the same editing host and
start block is an inline
node, set start block to its [=tree/parent=].
-
We only merge to or from block nodes or editing hosts. (This is
just in case someone makes a span into an editing host and
sticks paragraphs inside it or something . . . we could
probably drop that proviso.) Firefox 7.0a2 ignores the display
property when merging, so it doesn't merge <span style=display:block> but does merge
<p style=display:inline>. This is
undesirable, because it's visually wrong. IE10PP2 and Chrome 14
dev behave more like the spec, and Opera 11.50 seems to be
unable to make up its mind.
If span isn't an allowed child, it's probably something
unpleasant like a table row or a list or such. We don't want to
merge to or from something like that, because we'd most likely
wind up with the wrong type of child somewhere. It should be
pretty hard for this to happen given the normalization we do on
the selection; I'm not actually sure how it could happen at
all, actually, unless you start out with a DOM that has
non-allowed children someplace. So it's basically a sanity
check.
We don't let either start block or end block be a td or th.
This means we'll never merge to or from a td or th. This
matches Firefox 5.0a2, and reportedly Word as well. Chrome 13
dev and Opera 11.11 allow merging from a non-table cell end
block to a table cell start block, but not vice versa. In IE9
the delete key just does nothing.
If start block is neither a block node nor an [=editing host=], or "span"
is not an allowed child of
start block, or start
block is a [^td^] or [^th^], set start
block to null.
- Let end block be the
active range's [=range/end node=].
- While end block's [=tree/parent=] is
in the same editing host
and end block is an inline node, set end block
to its [=tree/parent=].
- If end block is neither a block node nor an [=editing host=], or "span" is
not an allowed child of end block, or end block is a [^td^] or
[^th^], set end block to null.
-
Later on we'll restore overrides. This
ensures that if we delete inline formatting elements and the user
then types something, the typed text will have the same style as
before.
-
As far as I can tell, IE9 and Opera 11.50 don't do this at all.
If you delete a selection and then start typing, the new text
doesn't take on the styles of the old text.
Firefox 7.0a2 seems to do it for some styles but not others.
Strikethrough, superscript, subscript, and links seem to be
lost, at a minimum.
The spec goes with something like Chrome 14 dev, which tries to
preserve lots of stuff.
Record current states
and values, and let overrides be the
result.
-
Now we actually begin deleting things.
-
This whole piece of the algorithm is based on deleteContents() in
DOM Range, copy-pasted and then adjusted to fit.
If start node and end
node are the same, and start node is an
editable {{Text}} node:
- Call
deleteData(start
offset, end offset − start offset) on start
node.
-
Canonicalize whitespace
at (start node, start
offset), with fix collapsed space
false.
- If direction is "forward", call
collapseToStart() on
the context object's
selection.
- Otherwise, call
collapseToEnd() on the
context object's
selection.
-
This is needed to restore any overrides that would otherwise
be lost. TODO: In this and similar cases, we could optimize
by saving only overrides, not the full state/value.
Restore states and
values from overrides.
- Abort these steps.
- If start node is an editable {{Text}} node, call
deleteData() on it, with start
offset as the first argument and ([=Node/length=] of
start node − start offset)
as the second argument.
- Let node list be a list of nodes, initially empty.
-
IE9 doesn't seem to let you do any intercell deletions: the
delete key does nothing if you select across multiple cells.
Firefox 5.0a2 and Opera 11.11 behave as the spec says, not
removing any table things. Chrome 13 dev will remove entire rows
if selected. Note that IE, Firefox, Word 2007, and OpenOffice.org
3.2.1 Ubuntu all switch to a magic cell-selection mode when you
try to select between cells, at least in some cases, instead of
selecting letter-by-letter.
For each node contained in the
active range, append node to node list if the last member
of node list (if any) is not an
[=tree/ancestor=] of node; node is editable; and
node is not a [^thead^], [^tbody^],
[^tfoot^], [^tr^], [^th^], or [^td^].
- For each node in node
list:
- Let parent be the [=tree/parent=] of
node.
- Remove node from parent.
-
Do this before stripping wrappers: see bug
13831.
If the block node of parent has no visible
[=tree/children=], and parent is editable or an [=editing host=], call
createElement("br")
on the context
object and append the result as the last [=tree/child=]
of parent.
-
Taking insertText to test the case where strip wrappers is
false, with value a: <p>[foo<b>bar</b>]baz becomes
<p>a[]baz per spec, in IE9, and
in Chrome 14 dev. Firefox 7.0a2 and Opera 11.50 make it
<p>a[]<b></b>baz,
with a useless wrapper. <p>foo<b>[bar</b>baz] becomes
<p>foo<b>a[]</b> per
spec and in IE9 and Firefox 7.0a2 and Opera 11.50; in Chrome
14 dev apparently it initially becomes <p>fooa[], but then the style is recreated.
This is detectable if you do something weird like
<span style=color:#aBcDeF>
instead of <b>: it comes
<font class=Apple-style-span
color=#abcdef> or such. I follow IE9 in all cases,
because it makes the most sense.
If strip wrappers is true or parent is not an [=tree/inclusive ancestor=] of
start node, while parent is an editable
inline node with [=Node/length=]
0, let grandparent be the [=tree/parent=]
of parent, then remove parent from grandparent, then
set parent to grandparent.
Even if strip wrappers is false, we still
want to strip wrappers that aren't [=tree/inclusive
ancestors=] of start node. The idea of
not stripping wrappers is that we're going to insert new
content right afterward, like text or an image, but that new
content will be inserted at the start node. Wrappers in other
places still need to be removed, because they would otherwise
remain empty.
- If end node is an editable {{Text}} node, call
deleteData(0, end offset) on it.
-
Canonicalize whitespace at
the active range's [=range/start=],
with fix collapsed space false.
-
Canonicalize whitespace at
the active range's [=range/end=], with
fix collapsed space false.
-
Now we need to merge blocks. The simplest case is something like
<p>fo[o</p><p>bar</p><p>b]az</p>
-> <p>fo</p>{}<p>az</p>
-> <p>fo{}az</p>
where neither block descends from the other. More complicated is
something like
foo[<p>]bar</p>
-> foo[]bar
or
<p>foo[</p>]bar
-> <p>foo[]bar</p>
where one descends from the other.
- If block merging is false, or
start block or end block is null, or
start block is not in the same editing host as
end block, or start block
and end block are the same:
- If direction is "forward", call
collapseToStart() on
the context object's
selection.
- Otherwise, call
collapseToEnd() on the
context object's
selection.
-
Restore states and
values from overrides.
- Abort these steps.
-
We might have added a br to the start/end block in an earlier
step. Now we're about to merge the blocks, and we don't want the
br's to get in the way. The end block is being destroyed no
matter what. If the start block winds up empty after merging,
we'll add a new br child at the end so it doesn't collapse.
If start block has one [=tree/child=], which
is a collapsed block prop,
remove its [=tree/child=] from it.
-
Just repeatedly blow up the end block in this case.
If start block is an [=tree/ancestor=] of
end block:
- Let reference node be end
block.
- While reference node is not a
[=tree/child=] of start block, set
reference node to its [=tree/parent=].
- Call
collapse() on the
context
object's selection, with first argument
start block and second argument the
[=tree/index=] of reference node.
- If end block has no [=tree/children=]:
- While end block is editable and is the only [=tree/child=] of
its [=tree/parent=] and is not a [=tree/child=] of
start block, let parent equal end block, then
remove end block from parent, then set end block to
parent.
- If end block is
editable and is not an inline
node, and its {{Node/ previousSibling}} and {{Node/
nextSibling}} are both inline nodes, call
createElement("br")
on the context
object and insert it into end
block's [=tree/parent=] immediately after end block.
- If end block is
editable, remove it from its [=tree/parent=].
-
Restore states and
values from overrides.
- Abort these steps.
- If end block's {{Node/firstChild}} is not
an inline node, restore states and values from
record, then abort these steps.
- Let children be a list of
nodes, initially empty.
- Append the first [=tree/child=] of end
block to children.
- While children's last member is not a
[^br^], and children's last member's
{{Node/nextSibling}} is an inline
node, append children's last member's
{{Node/nextSibling}} to children.
-
Record the values of
children, and let values be the result.
- While children's first member's
[=tree/parent=] is not start block,
split the parent of children.
- If children's first member's {{Node/
previousSibling}} is an editable [^br^],
remove that [^br^] from its [=tree/parent=].
-
In this case, pull in everything that comes after start block, until we hit a br or block node.
Otherwise, if start block is a
[=tree/descendant=] of end block:
- Call
collapse() on the
context
object's selection, with first argument
start block and second argument start block's [=Node/length=].
- Let reference node be start
block.
- While reference node is not a
[=tree/child=] of end block, set
reference node to its [=tree/parent=].
- If reference node's {{Node/nextSibling}}
is an inline node and start block's {{Node/lastChild}} is a [^br^], remove
start block's {{Node/lastChild}} from it.
- Let nodes to move be a list of
nodes, initially empty.
- If reference node's {{Node/nextSibling}}
is neither null nor a block node,
append it to nodes to move.
- While nodes to move is nonempty and its
last member isn't a [^br^] and its last member's
{{Node/nextSibling}} is neither null nor a
block node, append its last member's {{Node/nextSibling}}
to nodes to move.
-
Record the values of
nodes to move, and let values be the result.
- For each node in nodes to
move, append node as the last
[=tree/child=] of start block, preserving ranges.
-
In the last case, just move all the children of the end block to
the start block, and then get rid of any elements we emptied that
way.
Otherwise:
- Call
collapse() on the
context
object's selection, with first argument
start block and second argument start block's [=Node/length=].
- If end block's {{Node/firstChild}} is an
inline node and start
block's {{Node/lastChild}} is a [^br^], remove
start block's {{Node/lastChild}} from it.
-
Record the values of
end block's [=tree/children=], and let
values be the result.
- While end block has [=tree/children=],
append the first [=tree/child=] of end block
to start block,
preserving ranges.
- While end block has no [=tree/children=],
let parent be the [=tree/parent=] of
end block, then remove end
block from parent, then set
end block to parent.
-
We might have deleted the contents between two lists, in which
case we should merge them. See bug
13976.
- Let ancestor be start
block.
- While ancestor has an [=tree/inclusive
ancestor=] [^ol^] in the same
editing host whose {{Node/ nextSibling}} is also an [^ol^]
in the same editing host, or
an [=tree/inclusive ancestor=] [^ul^] in the same editing host whose
{{Node/ nextSibling}} is also a [^ul^] in the same editing host:
- While ancestor and its
{{Node/nextSibling}} are not both [^ol^]s in the same editing host, and are
also not both [^ul^]s in the
same editing host, set ancestor to its
[=tree/parent=].
- While ancestor's {{Node/nextSibling}} has
[=tree/children=], append ancestor's
{{Node/nextSibling}}'s {{Node/firstChild}} as the last
[=tree/child=] of ancestor, preserving ranges.
- Remove ancestor's {{Node/nextSibling}}
from its [=tree/parent=].
-
Restore the values from
values.
- If start block has no [=tree/children=], call
createElement("br")
on the context object and
append the result as the last [=tree/child=] of start
block.
-
Remove
extraneous line breaks at the end of start
block.
-
Restore states and values
from overrides.
Splitting a node list's parent
To split the parent of a list
node list of consecutive [=tree/sibling=]
nodes:
This algorithm breaks up the parent of node
list. If they're the only children of their parent, the
parent is removed entirely. If there are preceding or following
siblings, the original parent is left intact as the parent of those
siblings. If there are both preceding and following siblings, the
original parent is left as the parent of the following siblings and
a clone is used for the parent of the preceding siblings.
We make sure not to disrupt the appearance any more than necessary.
Obviously margins or such on the parent will be lost, but the
children will not wind up on the same line as anything they weren't
already on the same line as. E.g., if we split the parent of "bar"
in foo<p>bar</p>, we get
foo<br>bar, not foobar. (This is amazingly complicated and error-prone.)
We don't preserve inline styles: callers that want to do that
should call record the values and
restore the values themselves.
All this is useful in a lot of situations, like for outdenting. For
inline formatting commands, we almost always rely on pushing down
values instead, since that often leads to tidier markup.
- Let original parent be the [=tree/parent=] of
the first member of node list.
- If original parent is not editable or its [=tree/parent=] is null, do nothing
and abort these steps.
- If the first [=tree/child=] of original
parent is in node list, remove extraneous line breaks
before original parent.
- If the first [=tree/child=] of original
parent is in node list, and
original parent follows a
line break, set follows line break to true.
Otherwise, set follows line break to false.
- If the last [=tree/child=] of original parent
is in node list, and original
parent precedes a line
break, set precedes line break to true.
Otherwise, set precedes line break to false.
-
TODO: We insert things after the parent. This is bad, because
it will cause them to become part of any ranges that
immediately follow. For instance, if we're hitting "bar" in
<div><p>foo<p>bar</div>{<p>baz}
it becomes
<div><p>foo</div>{<p>bar<p>baz}
instead of
<div><p>foo</div><p>bar{<p>baz}
because of how range mutation rules work. This doesn't happen
if we insert before. This may or may not be important enough to
bother working around.
If the first [=tree/child=] of original
parent is not in node list, but its
last [=tree/child=] is:
- For each node in node
list, in reverse order, insert node into the [=tree/parent=] of original
parent immediately after original
parent, preserving ranges.
- If precedes line break is true, and the
last member of node list does not precede a
line break, call
createElement("br")
on the context object
and insert the result immediately after the last member of
node list.
-
Remove
extraneous line breaks at the end of original
parent.
- Abort these steps.
- If the first [=tree/child=] of original
parent is not in node list:
- Let cloned parent be the result of
calling
cloneNode(false)
on original parent.
- If original parent has an
id attribute, unset it.
- Insert cloned parent into the
[=tree/parent=] of original parent
immediately before original parent.
- While the {{Node/ previousSibling}} of the first member of
node list is not null, append the first
[=tree/child=] of original parent as the last
[=tree/child=] of cloned parent, preserving ranges.
-
Notice that a boundary point that was immediately before the
element will now be immediately before its children, just because
of the regular range mutation rules, without needing to worry
about preserving ranges. Likewise for boundary points immediately
after the element, if we wind up removing the element in the
final step. Preserving ranges is only necessary for the sake of
boundary points in the element or its descendants.
For each node in node
list, insert node into the
[=tree/parent=] of original parent
immediately before original parent, preserving ranges.
- If follows line break is true, and the first
member of node list does not follow a line
break, call
createElement("br")
on the context object and
insert the result immediately before the first member of
node list.
- If the last member of node list is an
inline node other than a [^br^], and the
first [=tree/child=] of original parent is a
[^br^], and original parent is not an inline node, remove the first [=tree/child=] of
original parent from original
parent.
- If original parent has no [=tree/children=]:
- Remove original parent from its
[=tree/parent=].
- If precedes line break is true, and the
last member of node list does not precede a
line break, call
createElement("br")
on the context object
and insert the result immediately after the last member of
node list.
- Otherwise, remove
extraneous line breaks before original
parent.
-
The parent might be null if it's a br that we removed in the last
step, in which case this step isn't necessary.
If node list's last member's
{{Node/nextSibling}} is null, but its [=tree/parent=] is not
null, remove extraneous
line breaks at the end of node list's
last member's [=tree/parent=].
To remove a node node while preserving its descendants,
split the parent of node's [=tree/children=] if it has any. If it has no
[=tree/children=], instead remove it from its [=tree/parent=].
Canonical space sequences
Whitespace in HTML normally collapses. However, if the user hits
the space bar twice in an HTML editor, they expect to see two
spaces, not one. Even if they hit the space bar once at the
beginning or end of a line, it would collapse without special
handling. The only good solution here is for the author to set
white-space: pre-wrap on the editable area, and on everywhere the
content is reproduced. But if they don't, we have to painfully hack
around the problem.
This is a basically intractable problem because of the unfortunate
confluence of three factors. One, our characters are Unicode, and
Unicode doesn't know about whitespace collapsing, so it provides no
special characters to control it. Two, HTML itself provides no
features that control whitespace collapsing without undesired side
effects (like inhibiting line breaks or not being allowed inside
<p>). Three, we need to support user
agents that don't reliably support CSS, since that includes many
popular mail clients.
The upshot is we have no good way to control whitespace collapse,
so we rely on the least bad way available: . This doesn't collapse with adjacent
whitespace in browsers, which is good. But it also doesn't allow a
line break opportunity, which is bad. In any run of whitespace that
we don't want to collapse, any two regular spaces must be separated
by an so they don't collapse
together, but we need to carefully limit runs of consecutive
s to minimize the damage to
line-breaking behavior.
The result is an elaborate and meticulously-crafted hodgepodge of
bad compromises that frankly isn't worth the effort to explain
here. The saving grace is that it all gets disabled if white-space
is set to pre-wrap as it should be, so authors can opt out of the
insanity. Interested readers will find detailed rationale for the
exact sequences required in the comments.
See long comment before insertText.
The canonical space sequence
of length n, with boolean flags non-breaking start and non-breaking end,
is returned by the following algorithm:
- If n is zero, return the empty string.
- If n is one and both non-breaking start and non-breaking end
are false, return a single space (U+0020).
- If n is one, return a single non-breaking
space (U+00A0).
- Let buffer be the empty string.
- If non-breaking start is true, let
repeated pair be U+00A0 U+0020. Otherwise, let
it be U+0020 U+00A0.
- While n is greater than three, append
repeated pair to buffer and
subtract two from n.
- If n is three, append a three-code unit
string to buffer depending on non-breaking start and non-breaking end:
-
non-breaking start and non-breaking end false
-
U+0020 U+00A0 U+0020
-
non-breaking start true, non-breaking end false
-
U+00A0 U+00A0 U+0020
-
non-breaking start false, non-breaking end true
-
U+0020 U+00A0 U+00A0
-
non-breaking start and non-breaking end both true
-
U+00A0 U+0020 U+00A0
- Otherwise, append a two-code unit
string to buffer depending on non-breaking start and non-breaking end:
-
non-breaking start and non-breaking end false
-
non-breaking start true, non-breaking end false
-
U+00A0 U+0020
-
non-breaking start false, non-breaking end true
-
U+0020 U+00A0
-
non-breaking start and non-breaking end both true
-
U+00A0 U+00A0
- Return buffer.
To canonicalize whitespace at
(node, offset), given an
optional boolean argument fix collapsed space
that defaults to true:
- If node is neither editable nor an editing host,
abort these steps.
- Let start node equal node
and let start offset equal offset.
-
First we go to the beginning of the current whitespace run.
Repeat the following steps:
- If start node has a [=tree/child=]
in the same editing host
with [=tree/index=] start offset minus one,
set start node to that [=tree/child=], then
set start offset to start
node's [=Node/length=].
-
TODO: Following a line break is unlikely to be the right
criterion.
Otherwise, if start offset is zero and
start node does not follow a
line break and start node's
[=tree/parent=] is in the
same editing host, set start offset
to start node's [=tree/index=], then set
start node to its [=tree/parent=].
- Otherwise, if start node is a {{Text}}
node and its [=tree/parent=]'s resolved
value for "white-space" is neither "pre" nor "pre-wrap" and
start offset is not zero and the
(start offset − 1)st code
unit of start node's {{CharacterData/
data}} is a space (0x0020) or non-breaking space (0x00A0),
subtract one from start offset.
- Otherwise, break from this loop.
-
Now we collapse any consecutive spaces, if fix
collapsed space is true.
Let end node equal start
node and end offset equal start offset.
- Let length equal zero.
-
This tries to delete spaces at the beginning of a line (bug
14119).
Let collapse spaces be true if start offset is zero and start node
follows a line break,
otherwise false.
- Repeat the following steps:
- If end node has a [=tree/child=]
in the same editing
host with [=tree/index=] end offset,
set end node to that [=tree/child=], then
set end offset to zero.
-
TODO: Preceding a line break is unlikely to be the right
criterion.
Otherwise, if end offset is end node's [=Node/length=] and end
node does not precede a line break and
end node's [=tree/parent=] is in the same editing host, set
end offset to one plus end
node's [=tree/index=], then set end
node to its [=tree/parent=].
- Otherwise, if end node is a {{Text}} node
and its [=tree/parent=]'s resolved
value for "white-space" is neither "pre" nor "pre-wrap" and
end offset is not end
node's [=Node/length=] and the end
offsetth code
unit of end node's {{CharacterData/
data}} is a space (0x0020) or non-breaking space (0x00A0):
- If fix collapsed space is true, and
collapse spaces is true, and the
end offsetth code
unit of end node's {{CharacterData/
data}} is a space (0x0020): call
deleteData(end
offset, 1) on end
node, then continue this loop from the beginning.
- Set collapse spaces to true if the
end offsetth code
unit of end node's {{CharacterData/
data}} is a space (0x0020), false otherwise.
- Add one to end offset.
- Add one to length.
- Otherwise, break from this loop.
-
We've already stripped leading whitespace, and collapsed
consecutive spaces. Now we try to strip any collapsed trailing
whitespace (bug
14119 again).
If fix collapsed space is true, then while
(start node, start
offset) is [=boundary point/before=] (end
node, end offset):
- If end node has a [=tree/child=]
in the same editing
host with [=tree/index=] end offset −
1, set end node to that [=tree/child=],
then set end offset to end
node's [=Node/length=].
- Otherwise, if end offset is zero and
end node's [=tree/parent=] is in the same editing host, set
end offset to end node's
[=tree/index=], then set end node to its
[=tree/parent=].
- Otherwise, if end node is a {{Text}} node
and its [=tree/parent=]'s resolved
value for "white-space" is neither "pre" nor "pre-wrap" and
end offset is end node's
[=Node/length=] and the last code
unit of end node's {{CharacterData/
data}} is a space (0x0020) and end node
precedes a line break:
- Subtract one from end offset.
- Subtract one from length.
- Call
deleteData(end
offset, 1) on end
node.
- Otherwise, break from this loop.
-
Finally we replace with the canonical sequence.
Let replacement whitespace be the canonical space sequence of
length length. non-breaking
start is true if start offset is zero
and start node follows a line break, and false
otherwise. non-breaking end is true if
end offset is end node's
{{CharacterData/length}} and end node
precedes a line break, and
false otherwise.
- While (start node, start
offset) is [=boundary point/before=] (end
node, end offset):
- If start node has a [=tree/child=] with
[=tree/index=] start offset, set
start node to that [=tree/child=], then set
start offset to zero.
- Otherwise, if start node is not a
{{Text}} node or if start offset is
start node's [=Node/length=], set
start offset to one plus start node's [=tree/index=], then set start node to its [=tree/parent=].
- Otherwise:
- Remove the first code
unit from replacement whitespace, and
let element be that code
unit.
- If element is not the same as the
start offsetth code
unit of start node's {{CharacterData/
data}}:
-
We need to insert then delete, so that we don't
change range boundary points. TODO: switch to using
"replace data" now that DOM Core has defined that.
Call
insertData(start offset,
element) on
start node.
- Call
deleteData(start offset + 1,
1) on start node.
- Add one to start offset.
Indenting and outdenting
There are two basically different types of indent/outdent: lists,
and everything else. For lists we'll wrap the item in a nested list
to indent, and split its parent to outdent. For everything
else we'll wrap in a <blockquote> to
indent, and try breaking it out of an ancestor indentation element to outdent.
Indenting winds up being pretty simple: just add an appropriate
wrapper. There's not really anything to think about here except
which wrapper we want (<ol> or
<ul> or <blockquote>), and establishing that is not rocket
science.
Outdenting is considerably more complicated. The basic idea we
follow is to first find the nearest editable ancestor that's a list
or indentation element. If we
succeed, and the node we're trying to outdent is the only
descendant of the ancestor, of course we can just remove the
ancestor and that's that. Otherwise, what we do is remove the
ancestor and then indent all its other descendants, much like
pushing down
values.
But of course, there are complications. We don't always actually
want to remove the closest ancestor that's causing
indentation. For one thing, we prefer ancestors that we can remove
completely, i.e., simple indentation elements. When
outdenting <blockquote><blockquote
id="abc">foo</blockquote></blockquote>,
removing the inner tag would result in <blockquote><div
id="abc">foo</div></blockquote>, since we
don't want to lose the id. Thus we prefer to remove the outer tag
and wind up with <blockquote
id="abc">foo</blockquote>.
Also, if the node we're outdenting is itself a list, we prefer to
remove an ancestor indentation
element rather than the list. Otherwise, if the user selected
some text, indented it, then added a list, there would be no way to
remove the indentation without removing the list first. This way,
the user could remove the list with the appropriate list-toggling
command or remove the indentation with the outdent command.
We have to handle entire lists of siblings at once, or else we'd
wind up doing something like
<ol>
{<li>foo</li>
<ol><li>bar</li></ol>}
</ol>
->
<ol><ol>
<li>foo</li>
<li>bar</li>
</ol></ol>
->
<ol><ol><ol>
<li>foo</li>
<li>bar</li>
</ol></ol></ol>
since by the time we got to doing the <ol> that originally
contained "bar", we won't remember that we aren't supposed to
indent "foo" a second time.
To indent a list node list
of consecutive [=tree/sibling=] nodes:
- If node list is empty, do nothing and abort
these steps.
- Let first node be the first member of
node list.
- If first node's [=tree/parent=] is an [^ol^]
or [^ul^]:
- Let tag be the [=Element/local name=] of
the [=tree/parent=] of first node.
-
This matches IE9, Firefox 4.0, and Chrome 12 dev. If
there's a preceding <li>, Opera 11.10 instead adds
the new parent to the end of that <li>, so it's not
the child of another list, which is invalid. But the other
browsers' way of doing things makes things simpler. E.g.,
if we want to indent an <li> and it has
<ol>/<ul> children, we have to distinguish
between the case where we want to indent the whole
<li> or only the first part. It also allows things
like
<ol><li>
foo
<ol><li>bar</li></ol>
baz
</li></ol>
in which case it's unclear what we should do if the user
selects "foo" and indents. I've filed a
bug on HTML5.
Wrap node list, with
sibling criteria returning true for an
HTML element with [=Element/local
name=] tag and false otherwise, and
new parent instructions returning the
result of calling createElement(tag)
on the {{Node/ ownerDocument}} of first
node.
- Abort these steps.
-
Firefox 4.0 respects the CSS styling flag for indent, but
Chrome 12 dev does not. I always produce blockquotes, even if
CSS styling is on, for two reasons. One, IE9 handles inline
margin attributes badly: when outdenting, it propagates the
margin to the parent, which doesn't actually remove it. Two, in
CSS mode I'd want to use <div style="margin: 1em 40px">
to match non-CSS mode, but authors are very likely to want to
remove the top/bottom margin, which they can't do if it's not a
special tag. Authors who really want divs for indentation could
always convert the blockquotes to divs themselves. But if
people really want it, I could respect CSS styling mode here
too.
The top/bottom margins might be undesirable here, but no more
so than for <ol>/<ul>/<p>/etc. Here as there,
authors can remove them with CSS if they want.
blockquote indents on both sides, so we don't have to worry
about directionality. In theory it would be better if we
indented only on the start side, but that requires care to get
right in mixed-direction cases. Even once browsers start to
support margin-start and so on, we can't use them because a) we
have to work okay in legacy browsers and b) it doesn't help if
a descendant block has different direction (so should be
indented the other way). So let's not worry about it: most
browsers don't, and the ones that do get it wrong. Just indent
on both sides.
Wrap node list, with
sibling criteria returning true for a
simple indentation
element and false otherwise, and new parent
instructions returning the result of calling createElement("blockquote")
on the {{Node/ownerDocument}} of first node.
Let new parent be the result.
-
Fix disallowed ancestors of
new parent.
Things that are produced for indentation that we need to consider
removing:
- Plain <blockquote> (produced by spec, Firefox 4.0
non-CSS, Opera 11.00)
- <blockquote style="margin-right: 0" dir="ltr"> and
<blockquote style="margin-left: 0" dir="rtl"> (IE9)
- <blockquote class="webkit-indent-blockquote" style="margin:
0 0 0 40px; border: none; padding: 0px"> (Chrome 12 dev)
- <div style="margin-left: 40px"> and <div
style="margin-right: 40px"> (Firefox 4.0 CSS if no other element
available)
- Other random things with display: block whose left or right
margin was increased by 40px (Firefox 4.0 CSS)
For discussion on the list-related stuff, see the comment for
insertOrderedList.
Gecko in CSS mode just adds margin properties to random elements
that are lying around. We don't attempt to remove those, because 1)
the amount and position of the margin can vary (it increases the
margin if there's a preexisting one), so it's potentially
complicated, and 2) no browser removes such margins on outdent,
including Gecko, except for Gecko in CSS mode. TODO: Consider
removing it anyway.
To outdent a node node:
- If node is not editable, abort these steps.
-
The easy case is when the whole element is indented. In this case
we remove the whole thing indiscriminately. In the case of
blockquotes created by IE, this might change the direction of
some children, but then their direction was probably changed
incorrectly in the first place, so no harm.
If node is a simple indentation element,
remove node, preserving its descendants.
Then abort these steps.
-
This might be a simple indentation element that had style added
to it by Firefox in CSS mode, for instance (color, font-family,
etc.).
If node is an indentation element:
- Unset the
dir attribute of node, if any.
- Unset the margin, padding, and border CSS properties of
node.
-
Set the tag name of node to "div".
- Abort these steps.
-
Approximate algorithms when an ancestor is causing the
indentation appear to be:
-
IE9
-
Go to the innermost element causing indentation. If the stuff
to be outdented includes all the contents of that element,
get rid of it, but if it has any attributes, change it to a
<p> with those same attributes. This is an excellent
idea in general, but unfortunately it preserves
explicitly-specified margins in style attributes, which isn't
great. In other cases, it moves the stuff to be outdented
outside. Not clear on all the details, seems to be pretty
confusing. Also does a bunch of seemingly arbitrary
normalization like removing divs and some attributes from
some things . . .
-
Firefox 4.0
-
Go to the innermost element causing indentation. If the stuff
to be outdented includes all the contents of that element,
get rid of it, even if it has arbitrary attributes.
Otherwise, move the stuff to be outdented outside the
indenting element. If there are any intervening elements that
include stuff not to be outdented, wrap the outdented stuff
in copies (which can duplicate id's, etc.).
-
Chrome 12 dev
-
Go to the outermost element causing indentation (even if the
current element is itself causing indentation). Move the text
to be outdented outside that outermost element, without
regard to any intervening elements. Then recreate the
original styles on the moved text, in some fashion. Something
like that; it confuses me and doesn't seem to be reasonable.
-
Opera 11.00
-
Like Firefox, except it goes to the outermost element, not
the innermost. Also seems to special-case to avoid duplicate
id's, and has a few other quirks.
Overall, all flawed, so I'll make up my own, patterned after
pushing down styles. First we search ancestors for a simple
indentation element, which we stand a chance of completely
removing. Failing that, we look for an indentation element
that's not simple, so we can't completely remove it.
Let current ancestor be node's [=tree/parent=].
- Let ancestor list be a list of nodes, initially empty.
- While current ancestor is an editable {{Element}} that is neither a simple indentation element nor an
[^ol^] nor a [^ul^], append current ancestor to
ancestor list and then set current
ancestor to its [=tree/parent=].
- If current ancestor is not an editable simple
indentation element:
- Let current ancestor be node's [=tree/parent=].
- Let ancestor list be the empty list.
- While current ancestor is an editable {{Element}} that is neither an
indentation element nor an
[^ol^] nor a [^ul^], append current
ancestor to ancestor list and then
set current ancestor to its
[=tree/parent=].
-
When asked to outdent a list wrapped in a simple indentation
element, Chrome 12 dev removes the list instead of the simple
indentation element. Opera 11.10 seems to remove both. IE9 and
Firefox 4.0 remove the simple indentation element, as does the
spec.
If node is an [^ol^] or [^ul^] and
current ancestor is not an editable indentation element:
- Unset the
reversed,
start, and
type attributes of node, if any
are set.
- Let children be the [=tree/children=] of
node.
-
We can't turn it into a div if it's the child of an ol or ul,
because that's not allowed: there's no way to group li's (see
HTML
bug 13128).
If node has attributes, and its
[=tree/parent=] is not an [^ol^] or [^ul^], set the tag name of node to "div".
- Otherwise:
-
Record the values of
node's [=tree/children=], and let
values be the result.
- Remove node, preserving its descendants.
-
Restore the values from
values.
-
Fix disallowed
ancestors of each member of children.
- Abort these steps.
- If current ancestor is not an editable indentation
element, abort these steps.
-
If we get to this point, we have an ancestor to split up.
Append current ancestor to ancestor list.
-
We can't outdent it yet, because we need its children to remain
intact for the loop.
Let original ancestor be current ancestor.
- While ancestor list is not empty:
- Let current ancestor be the last member
of ancestor list.
- Remove the last member from ancestor
list.
- Let target be the [=tree/child=] of
current ancestor that is equal to either
node or the last member of ancestor list.
- If target is an inline node that is not a [^br^], and its
{{Node/nextSibling}} is a [^br^], remove target's {{Node/nextSibling}} from its [=tree/parent=].
- Let preceding siblings be the
[=tree/precedings=] [=tree/siblings=] of target, and let following siblings
be the [=tree/followings=] [=tree/siblings=] of target.
-
Indent preceding
siblings.
-
Indent following
siblings.
-
Outdent original
ancestor.
Toggling lists
This is the action for the
insertOrderedList command and the insertUnorderedList command, which behave identically
except for which list type they target. It does several things that
vary contextually.
If everything in the selection is contained in the target list type
already, this more or less just outdents everything one step. This
is relatively simple.
Otherwise, it's slightly more complicated:
First, any lists of the opposite list type (other tag
name) get converted to the target list type (tag name). They get merged into a sibling if appropriate,
otherwise we set the tag name.
Then we go through all the affected nodes, handling each run of
consecutive siblings separately. Any line that's not already
wrapped in an <li> gets wrapped. If the
parent at this point isn't a list at all, the run gets wrapped in a
list. If it's the wrong type of list, we split the parent and rewrap it in the right
type of list. That's basically it, except that we have to exercise
the usual care to try merging with siblings and so forth.
Research for insertOrderedList/insertUnorderedList: tested the
following command sequences in IE9, Firefox 4.0, Chrome 12 dev,
Opera 11.10, OpenOffice.org 3.2.1 Ubuntu package, Microsoft Office
Word 2007. The commands "ol", "ul", "indent", "outdent" correspond
in browsers to "insertOrderedList", "insertUnorderedList",
"indent", and "outdent"; in OO.org to "Numbering On/Off", "Bullets
On/Off", "Increase Indent", "Decrease Indent"; and in Word to
"Numbering", "Bullets", "Increase Indent", "Decrease Indent".
Note: OO has a bunch of extra options, like "Promote One Level",
"Demote One Level", "Promote One Level With Subpoints", "Demote One
Level With Subpoints", "Insert Unnumbered Entry", "Restart
Numbering". The regular "Increase/Decrease Indent" commands work
oddly, and I assume they're not really meant to be used inside
lists. Thus I also tested with "Promote One Level" and "Demote One
Level". These are denoted by OO' instead of OO.
Assume that there are style rules in effect like
ol ol { list-style-type: lower-alpha }
ol ol ol { list-style-type: lower-roman }
This is the default appearance in Word, and I set OO to something
similar with Bullets and Numbering → Outline in the list editing
toolbox. I'm ignoring bullet style throughout, for no particular
reason.
- In an existing ordered list equivalent to
<ol><li>foo<li>bar<li>baz</ol>quz:
- Select "bar", do "ol":
-
Word/OO
-
Remove indent and number "2", change "3" to "2".
-
Browsers
-
Remove indent and number "2", change "3" to "1".
-
Spec
-
Same as browsers.
- Select "bar", do "ul":
-
Word
-
Leave indent the same, change "2" to a bullet, change "3"
to "2".
-
OO
-
Increase indent, change "2" to a bullet, change "3" to
"2".
-
IE
-
Change all numbers to bullets.
-
Firefox/Chrome/Opera
-
Leave indent the same, change "2" to a bullet, change "3"
to "1".
-
Spec
-
Same as Firefox/Chrome/Opera.
- Select "bar", do "indent":
-
Word/OO'/Browsers
-
Increase indent, change "2" to "a", change "3" to "2".
-
OO
-
Increase indent, do not change any numbers.
-
Spec
-
Same as Word/OO'/Browsers.
- Select "bar", do "outdent":
-
Word
-
Do nothing.
-
OO
-
Leave indent the same, de-indent "2" so it goes past the
left margin (?!), do not change any numbers.
-
OO'
-
Option grayed out.
-
Browsers
-
Remove indent and the number "2", change "3" to "1".
-
Spec
-
Same as browsers.
- Select "quz", do "ol":
-
Word/OO/IE/Chrome
-
Add as fourth item to existing list, numbered "4".
-
Firefox/Opera
-
Create new list, number the item "1".
-
Spec
-
Same as OO/Word/IE/Chrome.
- In an existing ordered list equivalent to
<ol><li>foo<br>bar<li>baz</ol>:
- Select "foo", do "ol":
-
Word/OO/IE/Chrome/Opera
-
Remove indent from both "foo" and "bar", change "2" ->
"1".
-
Firefox
-
Increase indent for "foo" only, add additional "a" marker
after "1" and before "foo".
-
Spec
-
Same as Word/OO/IE/Chrome/Opera.
- Select "foo", do "ul":
-
Word/Opera
-
Change "1" -> bullet, "2" -> "1".
-
OO
-
Increase indent for both "foo" and "bar", change "1"
-> bullet, "2" -> "1".
-
IE
-
Change all numbers to bullets.
-
Firefox
-
Increase indent for "foo" only, add additional bullet
marker after "1" and before "foo".
-
Chrome
-
Remove indent from "bar", change "1" -> bullet, "2"
-> "1".
-
Spec
-
Same as Word/Opera.
- Select "foo", do "indent":
-
Word
-
Increase indent for whole list.
-
OO
-
Increase indent for both "foo" and "bar".
-
OO'
-
Increase indent for "foo", change "1" -> "a".
-
IE/Firefox non-CSS/Opera
-
Increase indent for both "foo" and "bar", change "1"
-> "a", "2" -> "1".
-
Firefox CSS
-
Increase indent for "foo" only (<div
style="margin-left: 40px">).
-
Chrome
-
Increase indent for "foo" only, add "a" before "foo",
move "1" to be before "bar".
-
Spec
-
Same as IE/Firefox non-CSS/Opera.
- Select "foo", do "outdent":
-
Word
-
Decrease indent for whole list, so it goes past the left
margin.
-
OO
-
Decrease indent for "bar" and "1." (so "1." goes past the
left margin), but not "foo".
-
OO'
-
Option grayed out.
-
IE/Chrome/Opera
-
Remove indent from both "foo" and "bar", remove "1",
change "2" -> "1".
-
Firefox
-
Do nothing.
-
Spec
-
Same as IE/Chrome/Opera.
- Select "bar", do "ol":
-
Word/OO/IE/Chrome/Opera
-
Remove indent from both "foo" and "bar", change "2" ->
"1".
-
Firefox
-
Increase indent for "bar" only, add "a" marker before it.
-
Spec
-
Same as Word/OO/IE/Chrome/Opera.
- Select "bar", do "ul":
-
Word/Opera
-
Change "1" -> bullet, "2" -> "1".
-
OO
-
Increase indent for both "foo" and "bar", change "1"
-> bullet, "2" -> "1".
-
IE
-
Change all numbers to bullets.
-
Firefox
-
Increase indent for "bar" only, add bullet marker before
it.
-
Chrome
-
Remove indent from "foo", change "1" -> bullet and
move it before "bar", change "2" -> "1".
-
Spec
-
Same as Word/Opera.
- Select "bar", do "indent":
-
Word
-
Increase indent for whole list.
-
OO
-
Increase indent for both "foo" and "bar".
-
OO'
-
Increase indent for "foo", change "1" -> "a".
-
IE/Firefox non-CSS/Opera
-
Increase indent for both "foo" and "bar", change "1"
-> "a", "2" -> "1".
-
Firefox CSS
-
Increase indent for "bar" only (<div
style="margin-left: 40px">).
-
Chrome
-
Increase indent for "bar" only, add "a" before "bar",
move "bar" above "foo" (?!).
-
Spec
-
Same as IE/Firefox non-CSS/Opera.
- Select "bar", do "outdent":
-
Word
-
Decrease indent for whole list, so it goes past the left
margin.
-
OO
-
Decrease indent for "bar" and "1." (so "1." goes past the
left margin), but not "foo".
-
OO'
-
Option grayed out.
-
IE/Chrome/Opera
-
Remove indent from both "foo" and "bar", remove "1",
change "2" -> "1".
-
Firefox
-
Do nothing.
-
Spec
-
Same as IE/Chrome/Opera.
- In an existing nested ordered list equivalent to
<ol><li>foo<ol><li>bar<li>baz</ol><li>quz</ol>:
- Select "bar", do "ol":
-
Word/IE/Firefox
-
Decrease indent, remove "a" ("bar" is aligned with "foo"
with no marker of its own), change "b" -> "a".
-
OO
-
Remove all indent, change "b" -> "a".
-
Chrome
-
Decrease indent, change "a" -> "2", "b" -> "a", "2"
-> "3".
-
Opera
-
Decrease indent, change "a" -> "2", "b" -> "a", "2"
-> "4", insert extra "3" list marker before new "a".
-
Spec
-
Same as Chrome.
- Select "bar", do "ul":
-
Word/Firefox/Chrome
-
Change "a" -> bullet, "b" -> "a".
-
OO
-
Increase indent, change "a" -> bullet, "b" -> "a".
-
IE
-
Change "a" and "b" to bullets.
-
Opera
-
Change "a" -> bullet, "b" -> "a", "2" -> "4",
insert extra list markers "2" and "3" before new bullet
and "a".
-
Spec
-
Same as Word/Firefox/Chrome.
- Select "bar", do "indent":
-
Word/OO'/IE
-
Increase indent, change "a" -> "i", leave "b" alone.
-
OO
-
Increase indent, do not change numbers.
-
Firefox/Chrome/Opera
-
Increase indent, change "a" -> "i", "b" -> "a".
-
Spec
-
Same as Firefox/Chrome/Opera.
- Select "bar", do "outdent":
-
Word/OO'/IE/Chrome
-
Decrease indent, change "a" -> "2", "b" -> "a", "2"
-> "3".
-
OO
-
Leave indent the same, de-indent "a" so it goes past the
left margin (?!).
-
Firefox
-
Decrease indent, remove "a" ("bar" is aligned with "foo"
with no marker of its own), change "b" -> "a".
-
Opera
-
Decrease indent, change "a" -> "2", "b" -> "a", "2"
-> "4", insert extra list marker "3" before new "a".
-
Spec
-
Same as Word/OO'/IE/Chrome.
- In existing nested lists equivalent to
<ol><li>foo<ul><li>bar<li>baz</ul><li>quz</ol>:
- Select "bar", do "ol":
-
Word
-
Change all bullets to numbers. (Not letters, even though
indented!)
-
OO
-
Decrease indent, change first bullet -> "2", "2" ->
"3".
-
IE
-
Change all bullets to letters.
-
Firefox/Chrome
-
Change first bullet to "a".
-
Opera
-
Change first bullet -> "a", "2" -> "4", insert
extra list markers "2" and "3" before new "a" and bullet.
-
Spec
-
Same as Firefox/Chrome.
- Select "bar", do "ul":
-
Word/IE/Firefox
-
Decrease indent, remove first bullet ("bar" is aligned
with "foo" with no marker of its own).
-
OO
-
Remove all indent, remove first bullet, leave all else
the same.
-
Chrome
-
Decrease indent, change first bullet -> "2", "2" ->
"3".
-
Opera
-
Decrease indent, change first bullet -> "2", "2" ->
"4", insert extra list marker "3" before old bullet.
-
Spec
-
Same as Chrome.
- Select "bar", do "indent":
-
Word
-
Increase indent, change first bullet to "i" (?!).
-
OO/OO'/Firefox/Chrome/Opera
-
Increase indent.
-
IE
-
Increase indent, change "2" -> "3" (?!?!). (I don't
see from the markup why the 2 actually changes to a 3.
The markup seems to be as other browsers.)
-
Spec
-
Same as OO/OO'/Firefox/Chrome/Opera.
- Select "bar", do "outdent":
-
Word/IE/Chrome
-
Decrease indent, change first bullet -> "2", "2" ->
"3".
-
OO
-
Usual crazy stuff, move bullet left but leave text alone.
-
OO'
-
Option grayed out. (Interesting.)
-
Firefox
-
Decrease indent, remove first bullet ("bar" is aligned
with "foo" with no marker of its own).
-
Opera
-
Decrease indent, change first bullet -> "2", "2" ->
"4", insert extra list marker "3" before old bullet.
-
Spec
-
Same as Word/IE/Chrome.
- In an existing nested ordered list equivalent to
<ol><li>foo<li>bar<ol><li>baz</ol><li>quz</ol>:
- Select "bar", do "ol":
-
Word/OO
-
Remove indent and "2", change "3" -> "2".
-
IE/Chrome/Opera
-
Remove indent and "2", decrease indent of "baz", change
"2" and "3" -> "1".
-
Firefox
-
Increase indent, add extra "a" marker between "2" and
"bar".
-
Spec
-
Different from all of them: remove indent and "2", change
"3" -> "1".
- Select "bar", do "ul":
-
Word
-
Change "2" -> bullet.
-
OO
-
Increase indent, change "2" -> bullet, "3" -> "2".
-
IE
-
Change "1", "2", "3" -> bullets (and "a" to "1").
-
Firefox
-
Increase indent, add extra bullet marker between "2" and
"bar".
-
Chrome
-
Decrease indent of "baz", change "2" -> bullet, "a"
and "3" -> "1".
-
Opera
-
Change "2" -> bullet, "a" and "3" -> "1".
-
Spec
-
Different from all of them: change "2" -> bullet, "3"
-> "1".
- Select "bar", do "indent":
-
Word/OO'
-
Increase indent, change "2" -> "a", "a" -> "b", "3"
-> "2".
-
OO
-
Increase indent (double amount, past "baz").
-
Firefox non-CSS/Opera
-
Increase indent of both "bar" and "baz", change "2" ->
"a", "a" -> "i", "3" -> "2".
-
Firefox CSS
-
Increase indent.
-
Chrome
-
Increase indent, add "a" marker before "bar", move "2"
marker to before the "a" marker of "baz".
-
Spec
-
Same as Word/OO'.
- Select "bar", do "outdent":
-
Word/Firefox
-
Do nothing.
-
OO
-
Decrease indent on "2", leave "bar" alone.
-
OO'
-
Option grayed out.
-
IE
-
Decrease indent of "baz", change "2" and "3" -> "1",
"a" -> "2".
-
Chrome/Opera
-
Decrease indent of "bar" and "baz", remove "2", change
"a" and "3" -> "1".
-
Spec
-
Different from all of them: remove indent and "2", change
"3" -> "1".
- In an existing nested ordered list equivalent to
<ol><li>foo<li>bar<ol><li>baz</ol>quz<li>qoz</ol>:
- Does not appear to be possible in Word or OO.
- Also might be impossible to actually make such a list using
execCommand() in browsers.
- Suffice it to say that there's a lot of variation.
- In an existing indented region equivalent to
foo<blockquote>bar</blockquote>baz:
- Select "bar", do "ol":
-
Word/OO/Firefox/Chrome
-
Increase indent, add "1".
-
IE
-
Increase indent, add "a".
-
Opera
-
Add "1" (but do not increase indent).
- Select "foobar", do "ol":
-
Word/IE
-
Increase indent of both, add "1" before "foo" and "a"
before "bar".
-
OO
-
Increase indent of "bar" one step, increase indent of
"foo" two steps so it's aligned with "bar", add "1"
before "foo" and "2" before "bar".
-
Firefox
-
Increase indent of both, add "1" before foo", add "2"
before "bar" aligned with the "1" of "foo" (so large gap
between "2" and "bar").
-
Chrome
-
Increase indent of "foo", add "1" before "foo" and "2"
before "bar".
-
Opera
-
Mash everything together on one line. But if you do
<p>foo</p><blockquote>bar</blockquote><p>baz</p>
instead, same as Chrome.
- Select "foo" and do "ol", then select "bar" and do "ol":
-
Word/OO/Firefox/Opera
-
Different than doing both at once (often in exciting
ways).
-
IE/Chrome
-
Same as doing both at once.
-
<p>foo</p><blockquote><p>bar</p><p>baz</p></blockquote>
- Select "foobar" and do "ol":
-
Word
-
One-item list with sublist.
-
OO/Firefox/Chrome/Opera
-
One two-item list, unindented.
-
IE9
-
Two one-item lists.
- Select "foo", do "ol", then select "bar" and do "ol":
-
Word/OO/Chrome
-
One two-item list, unindented.
-
IE9/Firefox
-
Two one-item lists.
-
Opera
-
Two one-item lists, both unindented.
- Desired behavior: One-item list with sublist in both cases.
- In an existing multi-line indented region equivalent to
<blockquote>foo<br>bar<br>baz</blockquote>:
- Select "bar", do "ol":
-
Word/OO/Firefox/Chrome
-
Increase indent, add "1".
-
IE
-
Increase indent of everything, add "a" before "foo". If
you do
<blockquote><p>foo<p>bar<p>baz</blockquote>,
same as Word/OO/Firefox/Chrome.
-
Opera
-
Don't increase indent of anything, add "1" before "bar".
- In an existing multi-line indented region equivalent to
<blockquote>foo<br>bar</blockquote>baz:
- Select "barbaz", do "ol":
-
Word
-
Indent both, add "a" before "bar" and "2" before "baz".
-
OO
-
Indent "baz", add "1" before "bar" and "2" before "baz".
-
IE
-
Indent everything, add "a" before "foo" and "1" before
"baz". If you do
<blockquote><p>foo<p>bar</blockquote><p>baz,
indent "bar" and "baz" and put "1" before each.
-
Firefox
-
Indent "bar" and put "1" before it, put "baz" after "bar"
on the same line. If you do
<blockquote><p>foo<p>bar</blockquote><p>baz,
same as Chrome.
-
Chrome
-
Indent "bar" once and "baz" twice, put "1" before "bar"
and "2" before "baz".
-
Opera
-
Put a "1" before "bar" and move "baz" to the same line.
If you do
<blockquote><p>foo<p>bar</blockquote><p>baz,
indent "baz", put a "1" before "bar" and a "2" before
"baz".
- Select "bar", do "ol", then select "baz" and do "ol":
-
Word/OO/Opera
-
Different from if you do both together.
-
IE
-
Different with <br>, same with <p>.
-
Firefox
-
Three behaviors, depending on whether you do it in one
step with <br>, one step with <p>, or two
steps with either (same behavior regardless with two
steps).
-
Chrome
-
Same behavior in all four cases.
-
<blockquote>foo<ol><li>bar</ol></blockquote>baz:
- Select "baz", do "ol":
-
Word/OO/Chrome
-
Add "baz" as a new item to existing list.
-
IE/Firefox/Opera
-
Make "baz" its own new list.
-
<ul><li>foo</li><ol><li>bar</li></ol></ul>baz:
- Select "baz", do "ol":
-
IE/Firefox/Chrome/Opera
-
Separate list.
Ignoring the conceptual model of HTML, which users won't
understand, here's the conceptual model I've developed for lists:
text is divided up into blocks. Each block has an indentation level
and a list marker type. The list marker type can be either nothing,
ordered, or unordered. A list block cannot have indentation level
less than one. Any given piece of text is part of only one block. A
block may be visually non-contiguous, such as if a single list
block is interrupted by a further-indented block.
To find the right number (or letter) for an ordered-list block,
look at the immediately preceding block, but skip over any blocks
of higher indentation level. If there is no immediately preceding
block, or it's not an ordered-list block, or it has a lower
indentation level, the number is 1 (or a, i, etc.). Otherwise, it's
the number of the preceding block plus one.
ol/ul commands change the selected block to that list marker type,
or remove the list marker type if it's already the chosen type. If
the block has indentation level zero, it increases to one.
indent/outdent commands change the selected block's indentation
level. If a list block's indentation level is reduced to zero, it's
converted to a regular block.
What this means from an HTML perspective, roughly:
- A list block is the entire contents of an <li> element,
ignoring any nested list elements or indentation elements. A
non-list block is a line box.
- Indentation level is equal to the number of ancestor elements
that are either <li>s or indentation elements (blockquotes or
indenting divs).
- To find the list marker type, go to the first ancestor that's
either an <li> or indentation element.
- Correct numbering should automatically follow from the way
<ol> works in HTML (which is one of the reasons I use this
model).
- An ol command in an ordered-list block removes the surrounding
<li>, migrating its contents into the parent of the
<ol>. This splits up the <ol> if it's not the first or
last child, and wraps the contents in a new <li> if
necessary. If there's another list or indentation element nested in
the <li> we're removing, it will get re-wrapped in a new
<ol>, outside the newly-created <li>, so that it
maintains its indentation. This might cause the new <li> to
wind up in multiple pieces, if the original block was not
contiguous, which means the non-contiguous block is split into
several blocks (with different numbers).
- An ol command in an unordered-list block breaks up the parent
<ul> and puts a new <ol> in between the two pieces,
moving the parent <li> into it. If the <li> was the
first or last child, we merge with an existing adjacent <ol>
if possible. All children stay as they are.
- An ol command in a non-list block with indentation zero wraps
it in a new <ol><li>, or merges with an adjacent
<ol> if possible.
- An ol command in a non-list block with nonzero indentation
converts the parent to an <ol><li>, breaking it up if
necessary.
- The ul command works similarly to ol.
- indent in a non-list block wraps in an indentation element. In
a list block, it wraps the <li> in an extra <ol> or
<ul> as appropriate. With merging. Whatever. Let me just
write the spec.
- outdent in a non-list block strips an indentation element, if
one is present. In a list block, it breaks apart the parent
<ol> or <ul> and makes the affected block a sibling in
between the newly-split list elements. Will create new <li>s,
etc. etc.
Sheesh, lists are complicated.
To toggle lists, given a string
tag name (either "ol" or "ul"):
- Let mode be "disable" if the selection's list state is tag name, and "enable" otherwise.
- Let other tag name be "ol" if
tag name is "ul", and "ul" if tag name is
"ol".
- Let items be a list of all [^li^]s that are
[=tree/inclusive ancestors=] of the active
range's [=range/start=] and/or [=range/end=] node.
-
TODO: This overnormalizes, but it seems like the simplest
solution for now.
For each item in items,
normalize sublists of
item.
-
Block-extend the active range, and let new
range be the result.
- If mode is "enable", then let
lists to convert consist of every editable HTML element
with [=Element/local name=] other tag name that
is contained in
new range, and for every list in lists to convert:
-
Convert it to the right name. If possible, we want to merge
with a neighboring list of the correct type. Failing that, we
set the tag name.
If list's {{Node/ previousSibling}} or
{{Node/nextSibling}} is an editable
HTML element with [=Element/local
name=] tag name:
- Let children be list's [=tree/children=].
-
Record the values of
children, and let values be the result.
-
Split the parent of
children.
-
Wrap children, with
sibling criteria returning true for an
HTML element with
[=Element/local name=] tag name and
false otherwise.
-
Restore the values from
values.
- Otherwise, set the tag name
of list to tag name.
- Let node list be a list of nodes, initially empty.
-
We exclude indentation elements so that selecting some random
text and doing indent followed by insertOrderedList will have
the same result as the reverse. Specifically,
<blockquote>[foo]</blockquote> ->
<blockquote><ol><li>[foo]</li></ol></blockquote>
per spec and Firefox 4.0 and (more or less) Chrome 12 dev.
Opera 11.10 instead does
<ol><li>foo</li></ol>, so the
indentation vanishes. IE9 does
<ol><ol><li>foo</li></ol></ol>,
but that doesn't make semantic sense and is different from how
it would work if you reversed the commands. OpenOffice.org
3.2.1 (Ubuntu) and Word 2007 both agree with the spec in this
case.
For each node node contained in
new range, if node is
editable; the last member of node list (if any) is not an [=tree/ancestor=] of
node; node is not an
indentation element; and
either node is an [^ol^] or [^ul^], or its
[=tree/parent=] is an [^ol^] or [^ul^], or it is an allowed child of "li"; then append
node to node list.
-
We don't want to touch these. E.g., assuming tag name is "ol",
[foo<ol><li>bar</ol>baz]
-> <ol><li>[foo<li>bar<li>baz]</ol>
not <ol><li>[foo</li><ol><li>bar</ol><li>baz]</ol>.
But
<ul><li>foo<li>[bar</li><ol><li>baz</ol><li>quz]</ul>
-> <ul><li>foo</ul><ol><li>[bar</li><ol><li>baz</ol><li>quz]</ol>
not <ul><li>foo</ul><ol><li>[bar</ol><ul><ol><li>baz</ol></ul><ol><li>quz]</ol>
If mode is "enable", remove from node list any [^ol^] or [^ul^] whose [=tree/parent=] is
not also an [^ol^] or [^ul^].
- If mode is "disable", then while
node list is not empty:
- Let sublist be an empty list of
nodes.
- Remove the first member from node list
and append it to sublist.
- If the first member of sublist is an
HTML element with [=Element/local
name=] tag name, outdent it and continue this loop from the
beginning.
- While node list is not empty, and the
first member of node list is the
{{Node/nextSibling}} of the last member of sublist and is not an HTML
element with [=Element/local name=] tag
name, remove the first member from node
list and append it to sublist.
-
Record the values of
sublist, and let values
be the result.
-
Split the parent of sublist.
-
Fix disallowed
ancestors of each member of sublist.
-
Restore the values from
values.
- Otherwise, while node list is not empty:
- Let sublist be an empty list of
nodes.
-
Accumulate consecutive sibling nodes in sublist, first
converting them all to li's (except if they're already
lists).
While either sublist is empty, or
node list is not empty and its first
member is the {{Node/nextSibling}} of sublist's last member:
-
Thus <p>foo</p> becomes
<ol><li>foo</ol> instead of
<ol><li><p>foo</ol>, and likewise
for div, but other things will be put inside the
<li>.
If node list's first member is a
[^p^] or [^div^], set the tag
name of node list's first member
to "li", and append the result to sublist. Remove the first member from
node list.
- Otherwise, if the first member of node
list is an [^li^] or [^ol^] or [^ul^], remove it from
node list and append it to
sublist.
- Otherwise:
- Let nodes to wrap be a list of
nodes, initially empty.
[Page truncated to keep GeezerVison snappy.] |