Selection API


This is a small API I wrote to handle tasks related to selecting text. I see potential uses for it in a WYSIWYG style application, among other places.

Contents

Check if there is a Selection

SelectionUtils.isAnythingSelected(caretSelection = false)

The isAnythingSelected method will return true if something on the page is selected, and false if there is no selection.

The optional caretSelection argument is for dealing with caret selections (e.g. selections with a width of 0). By default it is false, which means that they do not count as selections, and thus that the method will return false even if there is a selection with a width of 0. However, if this argument is set to true, caret selections will be counted, and the method will return true if there is either a caret selection or a regular one.

In most scenarios, you will want to leave this as false.

Try it out by selecting some text on this page, and watching the following indicator change:

Check if there is a Selection within an Element

SelectionUtils.isSelectionWithin(element, caretSelection = false)

The isSelectionWithin method will return true is there is a selection fully within a particular element. As with isAnythingSelected, if the optional caretSelection argument is true zero-width selections will be counted, otherwise they will not be. This is useful, because if you wanted to check that a contenteditable div has focus, then you can simply use this method: for it to have focus, it must at least have a caret selection.

Try it out by selecting text on this page vs only text in the paragraph below, and watch the following indicator:

Select some text in this paragraph to see the indicator go green!

Remove a Selection

SelectionUtils.removeSelection()

This method simply deselects all text programmatically. For example, select text anywhere on this page, then click this button:

Make a Selection

SelectionUtils.makeSelection(element, firstPos, secondPos)

This method allows you to select text programmatically. element is the element to make the selection within. firstPos is the number of characters, from the start of the element, that you want the selection to start at. secondPos is the number of characters, from the start of the element, that you want the selection to end at.

So, for example, to select the text "for example" at the beginning of this paragraph, we would say SelectionUtils.makeSelection(paragraph, 4, 15). There are two quirks here: the first character is 0, not one, and the last character is actually not inclusive, so we need to add one at the end of secondPos count. Try it out by clicking this button:

Note: with a contenteditable element, you can set the firstPos and secondPos arguments to the same value to move the caret.

SelectionUtils.selectDOMNode(node)

This is a secondary way to make selections. It selects the entirety of a given DOM node, node.

Get the position of an existing Selection

SelectionUtils.getSelectionPosition(element)

This method gets the start and end points of a selection, in terms of number of characters from the start of the given element. It is the counterpart to the makeSelection method. It returns an array with two values, the first being the firstPos value (in terms of makeSelection), and the second being secondPos. If the array is empty, there is either no selection, or the selection is not exclusively within the given element.

Try selecting some text in this paragraph, and watch the box below to see what this method returns. Notice that it works regardless of mixed tags.

As a second demo, it is obvious that this method and makeSelection are designed to work together. Try selecting some text in this paragraph, click the "Save Selection" button, select something else, and then click the "Restore Selection" button to bring back the selection you had earlier.

Get the selected text

SelectionUtils.getSelectedText()

The getSelectedText method gets the text currently selected in any element, stripped of any HTML. For example, select some text on this page, and watch the box below:

Get the tags surrounding a Selection

SelectionUtils.getParentTags()

This returns a list of all DOM nodes which the selection is fully nested inside of. It could be useful for a WYSIWYG editor, where you can light up the bold button if the user has some bold text selected, etc.

Try selecting some text on this page, and watch the box below. Here's some text with nested tags.

Wrap HTML around a Selection

SelectionUtils.toggleHTMLWrapping(element, tagName, attributes = "", override = false)

This method is used to wrap HTML around a Selection, for example surrounding it in <b> tags to make it bold. Calling this function when the selection is already nested within the given tags will unwrap the selection from those tags, e.g. making it no longer bold.

If some of the text selected is already wrapped in the same tag, and some is not, executing this will wrap all of the text with that tag, rather than unwrap the text which is already wrapped.

The demo here behaves like a WYSIWYG editor with two buttons: one which makes the text bold (SelectionUtils.toggleHTMLWrapping(editor, "B")) and one which colours the text a random colour (SelectionUtils.toggleHTMLWrapping(editor, "SPAN", 'style="color:#' + color + '"', true)):

Try selecting some text in here and wrapping it in HTML with the buttons above...

Note: rich text editors often end up producing redundant markup, like nesting the same tag or empty tags (e.g. <b></b> and <b>some <b>tex</b>t</b>), which can cause issues when dealing with things like colour (e.g. old colours sticking around) and is just generally unpleasent.

To avoid this, this API cleans HTML to normalise these cases - but you have to first (before calling toggleHTMLWrapping) configure the tags it is allowed to clean.

This demo uses the following: SelectionUtils.cleanableTags = ["B", "SPAN"];

You may also find it useful to use custom tags (e.g. COLORED-TEXT), just so that elements have semantic differences besides just their attributes, as this API only uses the tag name when toggling / cleaning tags, and thus if we were to add highlighting to our example, nested highlighting and colouring would be cleaned to detrimental effect.

Clear Formatting

SelectionUtils.clearFormatting(element)

This removes all formatting from the selected text. Formatting is defined as any tags within the SelectionUtils.cleanableTags array (for more information about this, see the note in the section above). element is the element the selection must be in for formatting to get cleared.

The following button will clear formatting in the HTML Wrapping example. Try selecting some text, making it bold and colouring it, and then selecting a portion of it and clicking this button:

Insert HTML

SelectionUtils.insertAtSelection(element, toInsert)

This replaces the selected text / HTML with new text / HTML. In a contenteditable element without a selection, this method will add the new text / HTML at the text caret instead.

element is the element which must contain the selection. toInsert is new content to replace that at the existing selection / insert at the caret. It can either be a string containing HTML, or a DOM node (e.g. object created by document.createElement()).

Try it out - select any text in this paragraph, and then click "Replace" to replace it with the contents of the input box below:

Get the position of a Selection

SelectionUtils.getSelectionBoundingBox(element)

This method gives you the coordinates needed to find where the selection is on the page. This can then be used to, for example, show a popup above the selection with controls.

element is the element which must contain the selection. If there is no selection, or it is on the wrong element, an empty array is returned. Otherwise, this method will return an array with four values, [x1, y1, x2, y2]. (x1, y1) represents the position of the top-left corner of the selection (in terms of pixels from the top-left of the page), while (x2, y2) represents the position of the bottom-right corner of the selection (in terms of pixels from the top-left of the page).

Note: be careful using this function in conjunction with the selected event. This function causes the selection event to retrigger, meaning that if it is called by that event you will enter an endless recursive cycle and the page will crash. Either use proper event locking, or work around this by using a different method to call the function (e.g. an interval that just calls it every 100ms or so is what this demo uses).

Note: No rectangle will appear if the selection is not exclusively within element.

Note: this assumes that the selection is rectangular. If the selection is non-rectangular, they will project the rectangle to make calculations easier - for example, in the following selection (x1, y1) and (x2, y2) will respectively be the top-left and bottom-right corners of the orange rectangle:

Select any text in this paragraph to see this function in action. A rectangle should appear around your selection, and in fact this is the rectangle defined by (x1, y1, x2, y2). Watch what happens to the rectangle if you select text which wraps across multiple lines (if this text is not long enough, you can resize this window to make it smaller horizontally, and then it should span across a few).

Event Handling

element.addEventListener("selected", ()=>{ ... })

This API defines a new event: selected. Whenever the selection is changed / removed, this is event is fired on the element which contains all of the selected text, and bubbles down the tree.

Most of the demos on this page have been using this event - e.g. the getSelectedText demo uses this to know when to re-call the function.