w3c / selection-api

Selection API
http://w3c.github.io/selection-api/
Other
47 stars 29 forks source link

Add Selection/Range methods based on anchestor element #102

Open SetTrend opened 5 years ago

SetTrend commented 5 years ago

Add function prototypes for getting/setting anchorNode/anchorOffset and focusNode/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

partial interface Selection {
  [Throws]
  Range? getRangeAt(unsigned long index, Element contextElement);
};

partial interface Window {
  [Throws] Selection? getSelection(Element contextElement);
};

Details

1.

const selObj = window.getSelection(contextElement: HTMLElement);

Should yield:

selObj.anchorNode === selObj.focusNode === contextElement;

selObj.anchorOffset = AnchorCharacterOffsetFromContextElement(contextElement);
selObj.focusOffset = FocusCharacterOffsetFromContextElement(contextElement);


2.

const rngObj = window.getSelection().getRangeAt(index: number, contextElement: HTMLElement);

Should yield:

rngObj.commonAncestorContainer === rngObj.startContainer === rngObj.endContainer === contextElement;

rngObj.startOffset = AnchorCharacterOffsetFromContextElement(contextElement);
rngObj.endOffset = FocusCharacterOffsetFromContextElement(contextElement);

Both suggested functions are supposed to either throw an ReferenceError or yield void 0 when contextElement is not an anchestor of both, selObj.anchorNode/selObj.focusNode and rngObj.startOffset/rngObj.endOffset, resp.

selObj.AddRange() should take above Range object, recognize that it is based on a context element and put the cursor selection according to the character offsets given in a Range object; based on contextElement.

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).

tilgovi commented 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)?

SetTrend commented 5 years ago

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)))

distance

That would give a relative true representation of what's displayed by HTML. That's what's supposed to be returned by Selection::toString()

SetTrend commented 5 years ago

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.

rniwa commented 5 years ago

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.

tilgovi commented 5 years ago

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

SetTrend commented 5 years ago

@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);
  }
}
});
rniwa commented 5 years ago

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.

SetTrend commented 5 years ago

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)?

rniwa commented 5 years ago

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...

SetTrend commented 5 years ago

What do you want to point with if not cursor/focus?

johan commented 5 years ago

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.

johan commented 5 years ago

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.

SetTrend commented 5 years ago

Ah, I comprehend.

Good thought!

Thanks for clarifying.