Open SetTrend opened 5 years ago
Character count should mimic the behaviour of the
Element.innerText
property, i.e. considering<br/>
elements,<p>
elements and block elements following inline elements as new-line characters (=\n
).
Does this mean that the interpretation of the offset could change based on the computed styles of some of the elements in the range (e.g. if elements are inline or block)?
Exactly.
The number of new-line characters may be based on the distance between the last character of line block (A) and the first character of the following line block (C) in em, divided by line-hight:
em1 := Font size of the last character of line block (A) in device pixels
em2 := Font size of the first character of line block (C) in device pixels
lh1 := Line height of the last character of line block (A) in percent / 100
lh2 := Line height of the first character of line block (C) in percent / 100
distance := Distance between base line of the last character of line block (A)
and base line of first character of line block (C) in device pixels
Number of new-line characters := round(distance / ((em1 * lh1 + em2 * lh2) / 2)))
That would give a relative true representation of what's displayed by HTML. That's what's supposed to be returned by Selection::toString()
This is supposed to handle the issue that in a <pre>
element (or any element styled with white-space: pre;
), \n
and <br/>
are handled the same. Some JavaScript tools convert between these two formats.
Creating a rich-text editor for an editing host element may add <span>
, <u>
, <b>
and <i>
elements around parts of text. Yet, the selection must be retained after such change.
This suggestion is about to solve the issue that after any change, the selection (or just the cursor position) should be at a place with most vicinity to the (logical) position it was before any of these changes were applied.
So this is not really anchorOffset / focusOffset but rather the number of characters between the beginning of a given element to whatever position in DOM. That seems like a useful feature but I don't think we should tie that to selection.
It probably needs to be proposed as a generic DOM API instead.
I have some related work [1] that I have used for something similar [2], but across page views rather than edits. I did not incorporate any special handling of inline and block element rules, though, which is why I asked for clarification there.
[1] https://github.com/tilgovi/dom-seek [2] https://github.com/tilgovi/dom-anchor-text-position
@rniwa:
The logical meaning is basically the same: Both handle a cursor position/selection. Thus, I'd suggest to keep it with the Selection API. The sole difference is: The current logic is based on nodes, the suggested logic is based on characters.
Perhaps Microsoft Word Selection object may give some valuable hint. It offers different types of selections and cursor movement units, one of them being characters.
@tilgovi :
Looks great!
I have similar code done for my project (not published on GitHub yet as it's still a work in progress). My present solution deals with <br/>
/\n
conversion but doesn't deal with paragraph/block distances any further:
/**
* Gets current browser cursor position as number of characters from root element.
* @param rootElement DOM element to search for current browser cursor caret.
* @abstract This function iterates through DOM elements and DOM text nodes and adds their text length until it reaches the currently selected text node.
*/
function getFlatSelectionStartOffset(rootElement: HTMLElement): number
{
const sel = window.getSelection();
let selOffset = -1;
if (sel)
{
// console.log(`Source anchor Node: ${(<HTMLElement>sel.anchorNode).nodeName} (${sel.anchorOffset})`);
let selNode = sel.anchorNode;
if (selNode && rootElement.contains(selNode))
// This for initialization deals with Google Chrome bug/peculiarity, positioning cursor behind <br/> element but setting anchorOffset to <br/> element. Node::innerText property converts <br/> element to '\n'.
for (selOffset = sel.anchorOffset || (selNode.nodeType === Node.ELEMENT_NODE ? (<HTMLElement>selNode).innerText.length : 0);selNode !== rootElement;selNode = <Node>selNode!.parentNode)
{
for (let sibling = selNode!.previousSibling;sibling;sibling = sibling!.previousSibling)
{
switch (sibling.nodeType)
{
case Node.ELEMENT_NODE:
{
const sibElem = <HTMLElement>sibling;
selOffset += (sibElem.innerText || (sibElem.tagName.toLowerCase() === "br" ? "\n" : "")).length;
}
break;
case Node.TEXT_NODE:
selOffset += (sibling!.nodeValue || "").length;
break;
}
}
}
}
return selOffset;
}
/**
* Sets browser cursor position from number of characters within root element.
* @param rootElement DOM element to position browser cursor selection within.
* @param selOffset Number of characters to offset from beginning of root element text.
* @abstract This function iterates through DOM elements and DOM text nodes until the given offset is reached in text. Finally, the browser cursor selection is set to the text node found.
*/
function setFlatSelectionStartOffset(rootElement: HTMLElement, selOffset: number): void
{
if (selOffset >= 0)
{
let sibling: Node | null = rootElement.childNodes[0];
loop:
while (sibling)
{
switch (sibling.nodeType)
{
case Node.ELEMENT_NODE:
{
let len: number;
const sibElem = <HTMLElement>sibling;
if ((len = (sibElem.innerText || (sibElem.tagName.toLowerCase() === "br" ? "\n" : "")).length) < selOffset) selOffset -= len;
else
{
sibling = sibling!.childNodes[0];
continue;
}
break;
}
case Node.TEXT_NODE:
{
let len: number;
if ((len = (sibling!.nodeValue || "").length) < selOffset) selOffset -= len;
else break loop;
break;
}
}
sibling = sibling!.nextSibling;
}
if (sibling) window.getSelection().collapse(sibling, selOffset);
}
}
});
The logical meaning is basically the same: Both handle a cursor position/selection. Thus, I'd suggest to keep it with the Selection API. The sole difference is: The current logic is based on nodes, the suggested logic is based on characters.
No, my point was that this is an API that is useful without the pretense of a selection. It's useful to be able to say, point to the character X from the beginning of an element E. So this should really be a DOM API.
Hmm ... I see your point.
Yet, isn't "point to the character X from the beginning of an element E" logically equal to Selection.collapse(node, offset)
?
Yet, isn't "point to the character X from the beginning of an element E" logically equal to Selection.collapse(node, offset)
No, because then you'd be modifying the selection of a document. That would trigger a bunch of unnecessary work in the browser including but not limited to moving the focus, etc...
What do you want to point with if not cursor/focus?
What do you want to point with if not cursor/focus?
I think the best way to answer this is to imagine a world where querySelector or getElementById would not just give you a reference to a node, but also give it input focus.
Creating variable references to caret locations, or selection-like objects with one or maybe more ranges between caret locations, would be super useful. Using them to also get or set input selection is of course one common use case, a bit like invoking .focus() on an element.
Having character / code point / grapheme iterators you could place or move back and forth at/to indexed positions in one or more of of those units, compare with each other for equality, before/after, and query for what their next and previous character / code point / grapheme is would be even more useful, like a NodeIterator or TreeWalker, but for text, and hopefully with a friendlier api.
With primitives like these, it could probably get really nice and low-friction to slice and dice cut/copy/paste type operations into DocumentFragment objects at given locations in your document, for instance.
Being able to use this type of api not just with selection objects, but also the targetRanges from beforeinput events, would make them very practical tools.
Ah, I comprehend.
Good thought!
Thanks for clarifying.
Add function prototypes for getting/setting
anchorNode
/anchorOffset
andfocusNode
/focusOffset
as character offsets based on a given anchestor node.The suggested functionality is necessary to track and regain a selection in an editing host when DOM manipulation takes place.
Suggested IDL Interfaces
Details
1.
Should yield:
2.
Should yield:
Both suggested functions are supposed to either throw an
ReferenceError
or yieldvoid 0
whencontextElement
is not an anchestor of both,selObj.anchorNode
/selObj.focusNode
andrngObj.startOffset
/rngObj.endOffset
, resp.selObj.AddRange()
should take aboveRange
object, recognize that it is based on a context element and put the cursor selection according to the character offsets given in aRange
object; based oncontextElement
.Character count should mimic the behaviour of the
Element.innerText
property, i.e. considering<br/>
elements,<p>
elements and block elements following inline elements as new-line characters (=\n
).