@samuelbradshaw/highlight-helper-js 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,48 +1,49 @@
1
1
  # HighlightHelper.js
2
2
 
3
- **HighlightHelper.js** is a JavaScript tool that enables highlighting or underlining text in an HTML page (such as a digital book or article). Highlighting text in an HTML document can be difficult, especially in cases where a highlight starts in one element but ends in another.
3
+ **HighlightHelper.js** is a JavaScript library for drawing highlights in HTML content. It handles on-page rendering and provides events for user interaction, leaving other business logic such as data persistence to the consumer.
4
4
 
5
- Behind the scenes, HighlightHelper.js supports three different mechanisms for drawing highlights:
6
-
7
- 1. [SVG shapes](https://developer.mozilla.org/en-US/docs/Web/SVG) drawn behind text (default).
8
- 2. The [CSS Custom Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API) (experimental*).
9
- 3. Inserted [HTML mark elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/mark).
10
-
11
- An HTML demo page that shows basic functionality can be found here: [HighlightHelper.js Demo](https://samuelbradshaw.github.io/highlight-helper-js/demo.html). Source code for the demo is in **demo.html**. HighlightHelper.js itself is in **highlight-helper.js**.
12
-
13
- *The Custom Highlight API requires Safari 17.2+ or Chrome 105+, and may hang when rendering a large number of highlights. Additionally, only a few CSS styles are supported (see [Styling Highlights](https://www.w3.org/TR/css-pseudo-4/#highlight-styling)). Browser support and loading speed will hopefully improve in the future. See also the [load test page](https://samuelbradshaw.github.io/highlight-helper-js/test-load.html) for HighlightHelper.js.
5
+ To see it in action, open the [HighlightHelper.js Demo](https://samuelbradshaw.github.io/highlight-helper-js/demo.html).
14
6
 
15
7
  ### Documentation:
16
- - [Known issues](#known-issues)
8
+ - [Features](#features)
17
9
  - [Getting started](#getting-started)
18
10
  - [Installation options](#installation-options)
19
- - [Methods, options, and custom events](#methods-options-and-custom-events)
20
- - [Methods](#methods)
21
- - [Options](#options)
11
+ - [Basic usage](#basic-usage)
12
+ - [Constructor](#constructor)
13
+ - [Public methods](#public-methods)
14
+ - [Advanced usage](#advanced-usage)
22
15
  - [Custom events](#custom-events)
23
- - [Highlight attributes](#highlight-attributes)
24
-
25
- ## <a name="known-issues"></a>Known issues
16
+ - [Highlight objects](#highlight-objects)
17
+ - [Options](#options)
18
+ - [Element attributes](#element-attributes)
19
+ - [Appendix](#appendix)
20
+ - [Known issues](#known-issues)
21
+ - [Choosing a drawing mode](#choosing-a-drawing-mode)
22
+ - [Code snippets](#code-snippets)
26
23
 
27
- There are a few known issues:
24
+ ## <a name="features"></a>Features
28
25
 
29
- - **Chrome on Android** doesn’t set focus (and therefore doesn’t show text selection handles) when text is programmatically selected (such as from a disambiguation panel), unless the selection is initiated directly by a user’s tap. However, if you’re working in a native app with a webview, you may be able to focus the webview from Android (see [StackOverflow](https://stackoverflow.com/a/6372903/1349044) and [GitHub](https://github.com/lawlsausage/save_gfy/pull/1)).
30
- - **Safari on iOS** doesn’t respect the text selection color set via CSS, so when a highlight is active for editing it has a blue overlay. However, if you’re working in a native app with a webview, you may be able to set colors natively with [tintColor](https://stackoverflow.com/a/60510743/1349044), [highlightView](https://developer.apple.com/documentation/uikit/uitextselectiondisplayinteraction/4195471-highlightview), and/or [handleViews](https://developer.apple.com/documentation/uikit/uitextselectiondisplayinteraction/4195470-handleviews).
31
- - **Safari on iOS** loses focus and doesn’t send a `visibilitychange` event if the device screen locks after a period of inactivity. This can cause selection handles to not show when unlocking your device and tapping on an existing highlight. Going to another app and back or manually triggering a `visibilitychange` event will allow HighlightHelper.js to set focus again. Focus will also be set again if the user long-presses to select text.
32
- - **Safari on iOS and macOS** doesn’t allow setting underline thickness on a ::highlight() pseudo-element, so underline highlights drawn by the Custom Highlight API are thin and hard to see (see [StackOverflow](https://stackoverflow.com/q/79060854/1349044) and [WebKit Bugzilla](https://bugs.webkit.org/show_bug.cgi?id=282027)). This will hopefully be fixed in a future version of Safari.
26
+ - Draw highlights across element boundaries.
27
+ - Draw highlights as read-only, or enable editing with custom drag handles.
28
+ - Respond to tap, hover, and other events.
29
+ - Flexible “wrappers” API for attaching content to a highlight.
30
+ - Define your own colors and styles in multiple drawing modes.
31
+ - Compatibility with standard text selection APIs.
33
32
 
33
+ ### <a name="use-cases"></a>Use cases
34
34
 
35
- ## <a name="getting-started"></a>Getting started
35
+ HighlightHelper.js can support many use cases where styles are applied dynamically to text. For example:
36
36
 
37
- The easiest way to get started is to download demo.html and highlight-helper.js, open demo.html in a text editor and browser, and make changes to adapt it for your needs.
37
+ - Study tools (highlight textbooks, articles, Bible/scripture content).
38
+ - Commentaries (add indicators for footnotes or related content).
39
+ - Drafting (insert comments, underline spelling errors, show collaborative editing).
40
+ - Synced audio (show emphasis on the current word).
41
+ - Accessibility (embed labels for screen readers).
42
+ - Searching (highlight search matches, find on page).
43
+ - Code editors (syntax highlighting).
38
44
 
39
- In the source of demo.html, you’ll see CSS styles, followed by the HTML body, followed by JavaScript code. The JavaScript code does the following:
40
- 1. Links to highlight-helper.js,
41
- 2. Initializes a “Highlighter” instance with options,
42
- 3. Pre-loads and draws a few existing highlights,
43
- 3. Sets up logic to call HighlightHelper.js methods when buttons on the demo page are tapped, and
44
- 4. Sets up listeners to respond to custom event messages that come back from HighlightHelper.js.
45
45
 
46
+ ## <a name="getting-started"></a>Getting started
46
47
 
47
48
  ### <a name="installation-options"></a>Installation options
48
49
 
@@ -78,83 +79,544 @@ You can also install it using [npm](https://www.npmjs.com/package/@samuelbradsha
78
79
  % npm i @samuelbradshaw/highlight-helper-js
79
80
  ```
80
81
 
81
- ## <a name="methods-options-and-custom-events"></a>Methods, options, and custom events
82
+ ### <a name="basic-usage"></a>Basic usage
83
+
84
+ ```html
85
+ <!-- Import HighlightHelper.js (classic JavaScript) -->
86
+ <script src="https://cdn.jsdelivr.net/gh/samuelbradshaw/highlight-helper-js@main/highlight-helper.min.js"></script>
87
+
88
+ <script>
89
+ // Prepare highlights
90
+ const highlights = [
91
+ {
92
+ highlightId: 'hh-1729991847207',
93
+ color: 'green', style: 'fill',
94
+ startParagraphId: 'p1', startParagraphOffset: 27,
95
+ endParagraphId: 'p1', endParagraphOffset: 59,
96
+ },
97
+ ];
98
+
99
+ // Create a Highlighter instance
100
+ const containerSelector = 'main';
101
+ const paragraphSelector = 'main [id]';
102
+ const highlighter = new Highlighter(containerSelector, paragraphSelector);
103
+
104
+ // Set options
105
+ const options = { drawingMode: 'svg' };
106
+ highlighter.setOptions(options);
107
+
108
+ // Load highlights
109
+ highlighter.loadHighlights(highlights);
110
+ </script>
111
+ ```
112
+
113
+ ### <a name="constructor"></a>Constructor
114
+
115
+ - **Highlighter(containerSelector, paragraphSelector)** – Create a Highlighter instance. Parameters:
116
+ - **containerSelector** – CSS selector for the “annotatable container” – the section of the page with content to be highlighted. You can have multiple highlighters on a page, but they must be in different, non-overlapping containers. Default: `body`.
117
+ - **paragraphSelector** – CSS selector for the paragraphs (or other block elements) that can be highlighted within the container. Each paragraph is expected to have an ID attribute for anchoring highlights. Default: `:is(h1, h2, h3, h4, h5, h6, p, ol, ul, dl)[id]`.
118
+
119
+
120
+ ### <a name="public-methods"></a>Public methods
121
+
122
+ - **loadHighlights(highlights)** – Load a set of highlights. If highlights are already loaded, this method will diff for changes and replace the loaded highlights. Parameters:
123
+ - **highlights** – Array of [highlight objects](#highlight-objects). Each highlight should have a `highlightId`, and one or more other attributes. Omitted or invalid properties will fall back to previous or default values.
124
+ - **createOrUpdateHighlight(properties, draw = true, activate = true)** – Create and load a new highlight, or update an existing highlight. Return value: updated highlight object. Parameters:
125
+ - **properties** – Object with one or more [highlight object](#highlight-objects) properties. When updating an existing highlight, only the `highlightId` and properties that changed need to be provided. If a highlight ID isn't provided, HighlightHelper.js will update the currently-active highlight or create a new highlight with a default ID.
126
+ - **draw** – Boolean. Indicates if the highlight should be drawn after loading. Default: `true`.
127
+ - **activate** – Boolean. Indicates if the highlight should be activated after loading. Default: `true`.
128
+ - **drawHighlights(highlightIds = all)** – Draw or redraw highlights. In most cases, HighlightHelper.js will draw highlights automatically, but a manual redraw can be triggered if needed. Parameters:
129
+ - **highlightIds** – Array of highlight IDs. Default: all highlights.
130
+ - **activateHighlight(highlightId)** – Activate the specified highlight (i.e. enable it for editing). Only one highlight can be active at a time (because it's tied to text selection, and there can only be one text selection on a page). This is usually called in response to an `hh:tap` event. Parameters:
131
+ - **highlightId** – The `highlightId` of the highlight to activate.
132
+ - **activateHyperlink(index)** – Activate the specified hyperlink (i.e. open the link). A link without overlapping highlights will open automatically, but if there are overlaps it will need to be handled manually in response to the `hh:tap` event. Parameters:
133
+ - **index** – The 0-based index of the link, relative to other links in the container.
134
+ - **deactivateHighlights(removeSelectionRanges = true, redraw = true)** – Deactivate the active highlight, if there is one. Highlights are deactivated automatically if the user taps away. Parameters:
135
+ - **removeSelectionRanges** – Boolean. Indicates whether text selection should be cleared. Default: `true`.
136
+ - **redraw** – Boolean. Indicates whether the highlight should be redrawn. Default: `true`.
137
+ - **getActiveHighlightId()** – Get the highlight ID of the active highlight (if there is one).
138
+ - **getHighlightInfo(highlightIds = all, paragraphId = null)** – Get an array of [highlight objects](#highlight-objects) for specified highlight IDs. Highlights will be sorted based on their position on the page. Parameters:
139
+ - **highlightIds** – Array of highlight IDs. Default: all highlights.
140
+ - **paragraphId** – Paragraph ID. Filters highlights based on their start paragraph. Default: `null`.
141
+ - **getTargetsAtPoint(clientX, clientY)** – Gets any highlights, wrappers, and hyperlinks at the given coordinates.
142
+ - **clientX** – Horizontal (x) coordinate relative to the viewport.
143
+ - **clientY** – Vertical (y) coordinate relative to the viewport.
144
+ - **setOptions(optionsToUpdate)** – Change one or more options. Parameters:
145
+ - **optionsToUpdate** – Object with one or more [option keys and values](#options).
146
+ - **getOptions()** – Get the current option values.
147
+ - **getSelectionState()** – Get information about the current text selection and active highlight.
148
+ - **removeHighlights(highlightIds = all)** – Remove the specified highlights. Parameters:
149
+ - **highlightIds** – Array of highlight IDs. Default: all highlights.
150
+ - **removeHighlighter()** – Removes the current Highlighter instance and its highlights, resetting to a clean state. Called automatically if a new Highlighter instance is created on the same container.
151
+
152
+
153
+ ## <a name="advanced-usage"></a>Advanced usage
154
+
155
+ ### <a name="custom-events"></a>Custom events
156
+
157
+ HighlightHelper.js sends [custom events](https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events) to the annotatable container. These can be used to trigger actions.
158
+
159
+ - **hh:tap** – Sent when a user taps in the container (potentially trying to tap a highlight, wrapper, or link).
160
+ - **hh:hover** – Sent when a user hovers over a highlight, wrapper, or link. Requires `enableHover` to be set in [options](#options).
161
+ - **hh:selectionchange** – Sent when the selection state changes.
162
+ - **hh:highlightsload** – Sent when an array of highlights loads.
163
+ - **hh:highlightcreate** – Sent when a new highlight is created.
164
+ - **hh:highlightupdate** – Sent when a highlight is updated.
165
+ - **hh:highlightactivate** – Sent when a highlight is activated.
166
+ - **hh:highlightdeactivate** – Sent when a highlight is deactivated.
167
+ - **hh:highlightremove** – Sent when a highlight is removed.
168
+
169
+ Each event has a `detail` property that provides additional information. For example, the `hh:tap` event could be used to activate a highlight:
170
+
171
+ ```javascript
172
+ const container = document.getElementById('annotatable-container');
173
+ container.addEventListener('hh:tap', (event) => {
174
+ console.log(event.detail);
175
+ if (event.detail.highlights.length >= 1) {
176
+ highlighter.activateHighlight(event.detail.highlights[0].highlightId);
177
+ }
178
+ });
179
+ ```
180
+
82
181
 
83
- ### <a name="methods"></a>Methods
182
+ ## <a name="highlight-objects"></a>Highlight objects
84
183
 
85
- - **loadHighlights(highlights)** – Load an array of existing highlights into HighlightHelper.js. Each highlight should have one or more of the properties described under “Highlight attributes” below. Omitted or invalid properties will fall back to previous or default values. If highlights are already loaded, this method will replace the loaded highlights by diffing for changes, then adding, removing, or updating individual highlights as needed.
86
- - **createOrUpdateHighlight(attributes, triggeredByUserAction)** – Create or update an individual highlight. `attributes` is an object with one or more of the properties described under “Highlight attributes” below. If a highlight ID isn’t passed in, the currently-active highlight will be updated, if there is one, otherwise a new highlight will be created. If it’s a new highlight and start and end bounds aren’t passed in, the highlight will be created based on selected text. `triggeredByUserAction` is an optional boolean (default: `true`). If this is set to false, the highlight won’t be activated after editing, and the color and style won’t be saved as the default for the next highlight.
87
- - **drawHighlights(highlightIds)** – Draw or redraw highlights on the page, given an array of highlight IDs. If no highlight IDs are passed, all highlights will be drawn. This shouldn’t need to be called frequently (HighlightHelper.js will usually draw or redraw highlights automatically).
88
- - **activateHighlight(highlightId)** – Activate a highlight for editing, given its `highlightId`. Only one highlight can be active at a time (because it’s tied to text selection, and there can only be one text selection on a page). Normally, a highlight activates automatically when the user taps it; however, if there are overlapping highlights or if `autoTapToActivate` is set to `false`, this will need to be called manually.
89
- - **activateHyperlink(position)** – Activate the specified hyperlink (i.e. open the link). `position` is the 0-based position of the link in the annotatable area of the page, relative to other links. Normally, a link opens automatically when the user taps it; however, if a highlight overlaps the hyperlink or if `autoTapToActivate` is set to `false`, this will need to be called manually.
90
- - **deactivateHighlights()** – Clear the text selection and deactivate any active highlights. Normally, highlights are deactivated automatically when the user taps away.
91
- - **removeHighlights(highlightIds)** – Remove the specified highlights. If `highlightIds` isn’t provided, all highlights will be removed.
92
- - **getActiveHighlightId()** – Get the ID of the currently-active highlight (if there is one).
93
- - **getHighlightInfo(highlightIds, paragraphId)** – Get highlight information for an array of highlight IDs. If no highlight IDs are passed, information for all relevant highlights will be returned. If `paragraphId` is provided, only highlights that start in the specified paragraph will be returned. Highlights will be sorted based on their position on the page.
94
- - **setOption(key, value)** – Change one of the initialized options. Available options are described below.
95
- - **getOptions()** – Get the initialized options, including defaults for any options that weren’t explicitly set.
96
- - **removeHighlighter()** – Removes the current Highlighter instance and all of its highlights from the page. If a new Highlighter instance is created with the same container (as defined by the `containerSelector` option), the previous instance for that container will be removed automatically (there can be multiple Highlighter instances on a page, but each one needs to have a different container).
184
+ Highlight objects have the following properties that can be edited using the createOrUpdateHighlight() method:
185
+
186
+ - **highlightId** – String. Unique identifier for the highlight. Example: `dQw4w9WgXcQ`. Default: `hh-[timestamp]`.
187
+ - **color** – String. Highlight color (key from the `colorDefs` option). Default: `yellow`.
188
+ - **style** – String. Highlight style (key from the `styleDefs` option). Default: `fill`.
189
+ - **wrapper** – String. Highlight wrapper (key from the `wrapperDefs` option). Example: `{ startLabel: 'Start', }`. Default: `null`.
190
+ - **variables** – Object. Style and wrapper variables. Values are substituted into `{key}` placeholders in style and wrapper templates. Default: `null`.
191
+ - **readOnly** – Boolean. Indicates whether the highlight should be read-only. Default: `false`.
192
+ - **startParagraphId** – String. Paragraph ID where the highlight starts. Example: `p1`. Default based on selected text.
193
+ - **startParagraphOffset** – Integer. Character offset* where the highlight starts, relative to the beginning of the paragraph. Example: `0`. Default based on selected text.
194
+ - **endParagraphId** – String. Paragraph ID where the highlight ends. Example: `p1`. Default based on selected text.
195
+ - **endParagraphOffset** – Integer. Character offset* where the highlight ends (exclusive), relative to the beginning of the paragraph. Example: `27`. Default based on selected text.
196
+
197
+ These additional properties are added/updated automatically:
198
+
199
+ - **rangeText** – String. Plain text of the highlighted range.
200
+ - **rangeHtml** – String. HTML content of the highlighted range.
201
+ - **rangeRect** – DOMRect. Location of rendered highlight, relative to the top of the page (bounding rectangle).
202
+ - **rangeLineRects** – Array of DOMRects. Location of rendered highlight lines, relative to the top of the page (one rectangle for each line of text).
203
+ - **rangeParagraphIds** – Array of paragraph IDs. Paragraphs in the highlighted range.
204
+ - **rangeObj** – [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) object that represents where the highlight is drawn.
205
+ - **resolvedDrawingMode** – String. Drawing mode used to render the highlight. Should be either the current drawing mode, or `mark-elements` (fallback when a highlight can't be rendered in the current drawing mode).
206
+ - **escapedHighlightId** – String. Escaped highlight ID used internally in case the provided ID isn't a valid CSS identifier.
207
+
208
+ *Elements with the attribute `data-hh-ignore` are skipped when calculating character offsets. See [element attributes](#element-attributes).
97
209
 
98
210
 
99
211
  ### <a name="options"></a>Options
100
212
 
101
- Options can be provided when HighlightHelper.js is initialized. They can also be set on demand using `setOption(key, value)` (described above). All of these settings are optional. If not specified, the default value for each option will be used.
102
-
103
- - **containerSelector** – CSS selector for the section of the page that should be annotatable. Default: `body`.
104
- - **paragraphSelector** – CSS selector for the paragraphs (or other block elements) on the page that should be annotatable. Each paragraph is expected to have an ID attribute in the HTML, which is used to keep track of where a highlight starts and ends. Default: `h1[id], h2[id], h3[id], h4[id], h5[id], h6[id], p[id], ol[id], ul[id], dl[id], tr[id]`.
105
- - **colors** – Object that describes available highlight colors. Keys are color names, and values are [CSS color values](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default: red, orange, yellow, green, blue (see full default values at the bottom of highlight-helper.js).
106
- - **styles** – Object that describes available highlight styles. Keys are style names, and there are four properties for each style: `css`, `svg`, `css-active` (optional), and `svg-active` (optional). `css` is a CSS string used for styling highlights in `highlight-api` and `mark-elements` drawing modes, as well as for read-only highlights. `svg` is an SVG string (one or more SVG shapes) to represent the highlight in `svg` drawing mode. `css-active` and `svg-active` are alternative styles to be used when a highlight is active. The CSS custom property `var(--hh-color)` can be used to reference the highlight color value. For SVG highlights, the following variables (if present) will be replaced with relevant values from the highlight’s DOMRect: `{x}`, `{y}`, `{width}`, `{height}`, `{top}`, `{bottom}`, `{left}`, `{right}`. Default: fill, single-underline, double-underline, colored-text, redacted (see full default values at the bottom of highlight-helper.js).
107
- - **wrappers** – Object that describes available highlight wrappers. Keys are wrapper names, and values are objects with two optional properties: `start` (HTML string to be inserted at the beginning of the highlight), and `end` (HTML string to be inserted at the end of the highlight). Only read-only highlights support wrappers. `var(--hh-color)` can be used to reference the highlight color value. Variables surrounded by curly brackets will be replaced with variables stored in the `wrapperVariables` attribute of the highlight, if applicable. See default values at the bottom of highlight-helper.js.
108
- - **selectionHandles** – Object that describes custom selection handles that a user can drag to resize a selection or highlight. Because most touch devices have built-in selection handles, custom selection handles will only show when using a mouse or trackpad. There are two properties: `left` (HTML string to be used for the left selection handle) and `right` (HTML string to be used for the right selection handle). `var(--hh-color)` can be used to reference the highlight color value. See default values at the bottom of highlight-helper.js.
109
- - **rememberStyle** – Whether the most recent color, style, and wrapper should be remembered and used by default the next time the user creates a highlight. Boolean. Default: `true`.
110
- - **snapToWord** – Whether highlights should snap to the nearest word boundary. Spaces and dashes are considered word boundaries (this option may not work correctly in all languages). Boolean. Default: `false`.
111
- - **autoTapToActivate** – Whether HighlightHelper.js should automatically activate highlights and hyperlinks when they’re tapped. If set to false, you’ll need to listen for the `hh:tap` event and call `activateHighlight()` or `activateHyperlink()` manually when needed. Boolean. Default: `true`.
112
- - **longPressTimeout** – Duration in milliseconds before a tap turns into a long-press. On long-press, HighlightHelper.js will send an `hh:tap` event with `isLongPress: true`. If this option is set to 0, HighlightHelper.js will treat long-presses the same as regular taps, sending the `hh:tap` event after the user lifts their finger. If you have access to system APIs (such as in a mobile app), you may be able to get the system long-press duration to use for this value (which may vary based on accessibility settings). For example, Android has a [getLongPressTimeout()](https://developer.android.com/reference/kotlin/android/view/ViewConfiguration#getlongpresstimeout) method and iOS has a [minimumPressDuration](https://developer.apple.com/documentation/uikit/uilongpressgesturerecognizer/minimumpressduration) property. Default: `500`.
113
- - **pointerMode** – Mode for responding to pointer events. Options are `simple` (create highlights by selecting text, then tapping a color or style); `live` (create highlights immediately when selecting text, without needing to tap a button); and `auto` (`simple` for touch and mouse input, but `live` when a stylus or Apple Pencil is detected). Default: `auto`.
114
- - **drawingMode** – Mode for drawing highlights on the page. Options are `svg` (SVG shapes), `highlight-api` (Custom Highlight API), and `mark-elements` (inserted HTML mark elements). For faster performance, read-only highlights will always be drawn as mark elements, even if a different drawing mode is set. Default: `svg`.
115
- - **defaultColor** – Key of the default highlight color. Default: `yellow`.
116
- - **defaultStyle** – Key of the default highlight style. Default: `fill`.
117
- - **defaultWrapper** – Key of the default highlight wrapper. Default: `none`.
118
- - **highlightIdFunction** – Identifier of a function that provides unique IDs for new highlights. Default: `_getNewHighlightId`.
119
- - **showCustomSelectionHandlesForActiveHighlights** – Whether custom selection handles should show for active highlights. Default: `true`.
120
- - **showCustomSelectionHandlesForTextSelection** – Whether custom selection handles should show for text selection. Default: `false`.
121
- - **showCustomSelectionHandlesOnTouch** – Whether custom selection handles should show on touch devices. Default: `false`.
213
+ The following options can be set using the `setOptions()` method. Options that aren't defined will fall back to default values. For complex options, it may help to reference the default values at the bottom of highlight-helper.js.
214
+
215
+ - **drawingMode** – String. Default method for drawing highlights. See [Choosing a drawing mode](#choosing-a-drawing-mode). Possible values: `svg`, `highlight-api`, or `mark-elements`. Highlight styles not supported in the current drawing mode will fall back to `mark-elements`. Default: `svg`.
216
+ - **snapToWord** – Boolean. Indicates whether highlights should snap to the nearest word boundary, delimited by spaces and dashes (may not work correctly in all languages). Default: `false`.
217
+ - **longPressTimeout** – Integer. Minimum duration in milliseconds to be considered a long-press (`isLongPress` property on `hh:tap` event). Some operating systems may provide an API to get the system long-press duration ([Android](https://developer.android.com/reference/kotlin/android/view/ViewConfiguration#getlongpresstimeout); [iOS](https://developer.apple.com/documentation/uikit/uilongpressgesturerecognizer/minimumpressduration)), which may vary based on accessibility settings. Default: `500`.
218
+ - **enableHover** – Boolean. Indicates whether the `hh:hover` event is enabled. Disabled by default to reduce processing. Default: `false`.
219
+ - **dragHandles** – Object with properties `left` and `right`. Template used for custom handles that a user can drag to resize a selection or highlight. Properties:
220
+ - **left** – HTML string for the left selection handle.
221
+ - **right** – HTML string for the right selection handle.
222
+ - Template variables (optional): `var(--hh-color)` can be used to reference the highlight color.
223
+ - **showDragHandles** – Array. Indicates when custom drag handles should be shown. Default: `['highlights']`. Possible values:
224
+ - **highlights** – Show custom handles for active highlights. Default: `true`.
225
+ - **selection** – Show custom handles for text selection. Default: `false`.
226
+ - **touch** – Show custom handles on touch devices (may be unstable due to interaction with built-in selection handles). Default: `false`.
227
+ - **colorDefs** – Object. Defines highlight colors. Keys are color names, and values are [CSS color values](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). Default keys: `red`, `orange`, `yellow`, `green`, `blue`.
228
+ - **styleDefs** – Object. Defines highlight style templates. Keys are style names. Default keys: `fill`, `underline`. Each style has four available properties:
229
+ - **css** (required) – CSS string used in `highlight-api` and `mark-elements` drawing modes, for active text selection, and as a fallback in `svg` drawing mode.
230
+ - **cssActive** (optional) – alternate CSS string for active highlights. Falls back to `css`.
231
+ - **svg** (optional) – SVG string used in `svg` drawing mode. Falls back to `css`.
232
+ - **svgActive** (optional) – alternate SVG string for active highlights. Falls back to `svg`, `cssActive`, or `css`.
233
+ - Template variables (optional): `var(--hh-color)` can be used to reference the highlight color. The following variable substitutions are also supported (applied in this precedence order):
234
+ - `{key}` – replaced with provided values from the highlight's `variables` property.
235
+ - `{line.x}`, `{line.y}`, `{line.width}`, `{line.height}`, `{line.top}`, `{line.bottom}`, `{line.left}`, `{line.right}` – replaced with values from the bounding rect of the current line of the highlight.
236
+ - `{range.x}`, `{range.y}`, `{range.width}`, `{range.height}`, `{range.top}`, `{range.bottom}`, `{range.left}`, `{range.right}` – replaced with values from the bounding rect of the highlight range.
237
+ - `{column.x}`, `{column.y}`, `{column.width}`, `{column.height}`, `{column.top}`, `{column.bottom}`, `{column.left}`, `{column.right}` – replaced with values from the bounding rect of the nearest column or block element ancestor of the paragraph.
238
+ - `{container.x}`, `{container.y}`, `{container.width}`, `{container.height}`, `{container.top}`, `{container.bottom}`, `{container.left}`, `{container.right}` – replaced with values from bounding rect of the annotatable container.
239
+ - **wrapperDefs** – Object. Defines highlight wrapper templates. Keys are wrapper names. Default keys: `screen-reader-label`. Each wrapper has two properties:
240
+ - **start** – HTML template for the start wrapper. Default: `null`.
241
+ - **end** – HTML template for the end wrapper. Default: `null`.
242
+ - Template variables (optional): `var(--hh-color)` can be used to reference the highlight color. All of the substitutions described under `styleDefs` are also supported.
243
+
244
+
245
+ ### <a name="element-attributes"></a>Element attributes
246
+
247
+ Elements used or created by HighlightHelper.js have the following properties, which can be used for CSS styling:
248
+
249
+ Identifying attributes:
250
+ - **@data-hh-container** – Annotatable container.
251
+ - **@data-hh-additions** – Element that contains drag handles and the SVG background.
252
+ - **@data-hh-handle** – Drag handle parent element.
253
+ - **@data-hh-handle-content** – Drag handle child element.
254
+ - **@data-hh-default-handle** – Default drag handle (can be overwritten in options).
255
+ - **@data-hh-svg-background** – SVG background where SVG highlights are drawn.
256
+ - **@data-hh-svg-active-overlay** – SVG group with shapes for the active highlight.
257
+ - **@data-hh-wrapper** – Wrapper element.
258
+
259
+ Differentiating attributes:
260
+ - **@data-hh-highlight-id** (on highlight and wrapper elements) – highlight ID.
261
+ - **@data-hh-color** (on highlight and wrapper elements) – color name.
262
+ - **@data-hh-style** (on highlight and wrapper elements) – style name.
263
+ - **@data-hh-wrapper** (on highlight and wrapper elements) – wrapper name.
264
+ - **@data-hh-position** (on mark highlight and wrapper elements) – indicates if the element is at the `start` or `end` of the highlight, or both (`start end`).
265
+ - **@data-hh-index** (on hyperlink) – 0-based index of the hyperlink.
266
+ - **@data-hh-side** (on drag handle) – indicates if the handle is `left` or `right`.
267
+ - **@data-hh-wrapper-hash** (on wrapper elements) – used internally to check if a wrapper needs to be replaced with a different wrapper.
268
+
269
+ Other:
270
+ - **@class="sr-only"** – Elements with this attribute are only visible to screen readers.
271
+ - **@data-hh-ignore** – Elements with this attribute are skipped when calculating character offsets and highlight rectangle positions.
272
+ - **@highlighter** (on container) – Highlighter instance associated with the container.
273
+ - **@style="--hh-color: [color string];"** (on container) – CSS custom property with the current highlight's color string.
274
+ - **@data-pointer-down** (on container) – Indicates whether the pointer (mouse, finger, etc.) is down on the page.
275
+
276
+
277
+ ## <a name="appendix"></a>Appendix
278
+
279
+ ### <a name="known-issues"></a>Known issues
280
+
281
+ - **Chrome on Android** doesn't show text selection handles when text is selected programmatically (such as from a disambiguation panel, or with snap-to-word). Text selection UI must be initiated by a user gesture.
282
+ - Workaround: Tap the highlight again to show the handles.
283
+ - **Safari on iOS and macOS** doesn't allow setting underline thickness on a ::highlight() pseudo-element, so underline highlights drawn by the CSS Custom Highlight API are thin and hard to see (see [StackOverflow](https://stackoverflow.com/q/79060854/1349044) and [WebKit Bugzilla](https://bugs.webkit.org/show_bug.cgi?id=282027)).
284
+ - Workaround: Use a different drawing mode.
285
+ - **Safari on iOS and macOS** expand text selection to include the previous word when selecting a word in an inline element at the beginning of the line of text. Code to reproduce: `<p style="font-family: Times; font-size: 16px; width: 250px">This is an example of <i>text selection</i> <b>jumping</b> to include the previous word when text wraps at the beginning of an inline element. Double-click (macOS) or long-press (iOS) the word "jumping."</p>`
286
+ - Workaround: Manually adjust highlight after it's created.
287
+ - **Safari on iOS** doesn't respect text selection colors set via CSS, so when a highlight is active for editing it has a blue overlay.
288
+ - Workaround: In a native app with a webview, you may be able to set colors natively. See [tintColor](https://stackoverflow.com/a/60510743/1349044), [highlightView](https://developer.apple.com/documentation/uikit/uitextselectiondisplayinteraction/4195471-highlightview), and/or [handleViews](https://developer.apple.com/documentation/uikit/uitextselectiondisplayinteraction/4195470-handleviews).
289
+ - **Safari on iOS 26** doesn't create a text selection when using an Apple Pencil (or other compatible stylus), until the stylus is lifted.
290
+ - Workaround: Double-tap then drag the stylus.
291
+ - **Safari on macOS** – SVG highlights are slightly shifted when zoomed to the maximum zoom size.
292
+ - Workaround: Reduce text size.
293
+ - **Amazon Fire webview** doesn’t show selection handles, making it difficult to resize an existing highlight (and custom drag handles don't work reliably on touch devices).
294
+ - Workaround: Long-press and drag to select everything you need before lifting your finger.
295
+
296
+
297
+ ### <a name="choosing-a-drawing-mode"></a>Choosing a drawing mode
298
+
299
+ | | SVG | Highlight API | Mark Elements |
300
+ | :--- | :--- | :--- | :--- |
301
+ | **Loading speed** | Very fast | Slow | Fast |
302
+ | **Browser support** | Full support | Requires iOS 17.2+ | Full support |
303
+ | **Background shapes** | Good | None | Limited |
304
+ | **Background colors** | Good | Good | Good |
305
+ | **Text styles** | None, but falls back to mark elements | Limited | Good |
306
+ | **Printing** | Poor by default, but falls back to mark elements | Good | Good |
307
+ | **Screen reader accessibility** | None by default; good with wrappers | None by default; good with wrappers | Good by default; poor with overlapping highlights |
308
+
309
+ See the [load test page](https://samuelbradshaw.github.io/highlight-helper-js/test-load.html) for informal benchmarking.
310
+
311
+
312
+ ### <a name="code-snippets"></a>Code snippets
313
+
314
+ These code snippets demonstrate some of the ways that HighlightHelper.js can be used. The [HighlightHelper.js Demo](https://samuelbradshaw.github.io/highlight-helper-js/demo.html) may also be helpful.
315
+
316
+ #### Floating menu
317
+
318
+ ```css
319
+ body {
320
+ -webkit-touch-callout: none; /* Hide default text selction menu */
321
+ }
322
+ ```
122
323
 
324
+ ```javascript
325
+ // Hide default text selection menu
326
+ window.addEventListener('contextmenu', event => { event.preventDefault() });
327
+
328
+ function updateFloatingMenu() {
329
+ const selection = window.getSelection();
330
+ if (selection.type === 'Range' && !isResizing && !isScrolling) {
331
+ showMenu(selection.getBoundingClientRect());
332
+ } else {
333
+ hideMenu();
334
+ }
335
+ }
336
+
337
+ // Respond to selection resize
338
+ let isResizing;
339
+ container.addEventListener('hh:selectionchange', (event) => {
340
+ isResizing = event.detail.isResizing;
341
+ updateFloatingMenu();
342
+ });
343
+
344
+ // Respond to page scroll
345
+ let scrollTimeout, isScrolling;
346
+ window.addEventListener('scroll', (event) => {
347
+ isScrolling = true;
348
+ updateFloatingMenu();
349
+ clearTimeout(scrollTimeout);
350
+ scrollTimeout = setTimeout(() => {
351
+ isScrolling = false;
352
+ updateFloatingMenu();
353
+ }, 200);
354
+ });
355
+ ```
123
356
 
124
- ### <a name="custom-events"></a>Custom events
357
+ #### Live highlighting with a stylus or Apple Pencil
358
+
359
+ ```javascript
360
+ let previousColor = 'yellow';
361
+ let previousStyle = 'fill';
362
+ function createLiveHighlight(event) {
363
+ const selectionState = event.detail;
364
+ if (selectionState.pointerType === 'pen' && selectionState.changes.includes('selection') && selectionState.selection.type === 'Range' && !highlighter.getActiveHighlightId()) {
365
+ highlighter.createOrUpdateHighlight({
366
+ highlightId: crypto.randomUUID(),
367
+ color: previousColor,
368
+ style: previousStyle,
369
+ });
370
+ }
371
+ }
372
+ container.addEventListener('hh:selectionchange', createLiveHighlight);
373
+ ```
374
+
375
+ #### Responding to selection color change
376
+
377
+ ```javascript
378
+ // In a native app, this could be used to update the color of the system selection UI
379
+ function updateColors(event) {
380
+ if (event.detail.changes.includes('color')) {
381
+ setNativeAccentColor(event.detail.color);
382
+ }
383
+ }
384
+ container.addEventListener('hh:selectionchange', updateColors);
385
+ ```
386
+
387
+ #### Light and dark mode colors
388
+
389
+ ```css
390
+ html { color-scheme: light dark; }
391
+ html[data-theme="light"] { color-scheme: light; }
392
+ html[data-theme="dark"] { color-scheme: dark; }
393
+ ```
394
+
395
+ ```javascript
396
+ const options = {
397
+ colorDefs: {
398
+ 'red': 'light-dark(#ff8080, #cc0000)',
399
+ },
400
+ }
401
+ highlighter.setOptions(options);
402
+ ```
403
+
404
+ #### Changing color vibrance or opacity
125
405
 
126
- HighlightHelper.js sends [custom events](https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events) to the container element (defined in the `containerSelector` option) that can be listened for and responded to:
127
-
128
- - **hh:highlightsload** – Sent when an array of highlights loads. Includes the number of highlights added, removed, or updated, the total number of loaded highlights, and the time they took to load (in milliseconds).
129
- - **hh:highlightcreate** – Sent when a new highlight is created. Includes information about the highlight.
130
- - **hh:highlightupdate** – Sent when the highlight changes. Includes information about the highlight, and a list of the attributes that changed.
131
- - **hh:highlightremove** – Sent when a highlight is removed. Includes the ID of the removed highlight.
132
- - **hh:tap** – Sent when a user taps in the annotatable container (potentially trying to activate a highlight or link). This will only be sent if there isn’t currently an active highlight. Includes the location of the tap, the highlight(s) that were tapped, and the hyperlink(s) that were tapped, if any.
133
- - **hh:selectionupdate** – Sent when the selection color or style changes. Can be used to update custom selection UI, such as the color of selection handles. Includes the key of the color and style.
134
- - **hh:highlightactivate** – Sent when a highlight is activated. Includes information about the activated highlight.
135
- - **hh:highlightdeactivate** – Sent when a highlight is deactivated. Includes the ID of the deactivated highlight.
136
- - **hh:ambiguousaction** – Sent when a user taps on overlapping highlights or an overlapping highlight and link. Includes the location of the tap, the highlight(s) that were tapped, and the hyperlink(s) that were tapped. This event will not be sent if `autoTapToActivate` is set to `false`.
137
- - **selectionchange** – This is a [standard JavaScript event](https://developer.mozilla.org/en-US/docs/Web/API/Document/selectionchange_event) (not specific to HighlightHelper.js), but it’s worth mentioning here because it can be useful for adding custom selection UI around the selected text (such as a floating annotation menu). When this sends, you can call `window.getSelection()` to get the current [Selection object](https://developer.mozilla.org/en-US/docs/Web/API/Selection).
138
-
139
-
140
- ## <a name="highlight-attributes"></a>Highlight attributes
141
-
142
- These are the attributes that HighlightHelper.js stores for each highlight. You can update a highlight by calling `createOrUpdateHighlight(attributes)`, where `attributes` is an object that includes the keys to be updated.
143
-
144
- - **highlightId** (read-only after creation) – The ID of the highlight.
145
- - **color** – The color of the highlight (key from the `colors` object in the initialized options). Example: `red`.
146
- - **style** – The style of the highlight (key from the `styles` object in the initialized options). Example: `single-underline`.
147
- - **wrapper** – The wrapper of the highlight (key from the `wrappers` object in the initialized options). More information about wrappers can be found above. Example: `none`.
148
- - **wrapperVariables** – Variables used in wrappers. Example: `{ marker: 'a', }`.
149
- - **readOnly** – Whether the highlight should be read-only (prevents a user from changing its color, bounds, or other attributes). Boolean. Example: `true`.
150
- - **startParagraphId** – ID of the paragraph where the highlight starts. Example: `p1`.
151
- - **startParagraphOffset** – Character offset where the highlight starts, relative to the beginning of the paragraph.* Example: 12.
152
- - **endParagraphId** – ID of the paragraph where the highlight ends. Example: `p1`.
153
- - **endParagraphOffset** – Character offset where the highlight ends, relative to the beginning of the paragraph.* Example: 14.
154
- - **escapedHighlightId** (read-only) – Escaped highlight ID used as a CSS identifier (this is used internally by HighlightHelper.js).
155
- - **rangeText** (read-only) – Text content in the highlighted range.
156
- - **rangeHtml** (read-only) – HTML content in the highlighted range.
157
- - **rangeParagraphIds** (read-only) – IDs of paragraphs in the highlighted range.
158
- - **rangeObj** (read-only) – The [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) object that represents where the highlight is drawn.
159
-
160
- *If you have dynamic elements that need to be inserted into an annotatable paragraph temporarily, you can mark them with the `data-hh-ignore` attribute. HighlightHelper.js will skip over elements with this attribute when calculating character offsets and drawing highlights.
406
+ ```javascript
407
+ const options = {
408
+ styleDefs: {
409
+ 'fill': {
410
+ css: 'background-color: hsl(from var(--hh-color) h s l / var(--highlight-opacity, 50%));',
411
+ cssActive: 'background-color: hsl(from var(--hh-color) h s l / var(--highlight-opacity-active, 80%));',
412
+ svg: '<rect x="{line.x}" y="{line.y}" style="fill: hsl(from var(--hh-color) h s l / var(--highlight-opacity, 50%)); width: {line.width}px; height: calc({line.height}px * 0.85); transform: translateY(calc({line.height}px * 0.15));" />',
413
+ svgActive: `
414
+ <rect x="{line.x}" y="{line.y}" style="fill: hsl(from var(--hh-color) h s l / var(--highlight-opacity-active, 80%)); width: {line.width}px; height: calc({line.height}px * 0.85); transform: translateY(calc({line.height}px * 0.15));" />
415
+ `,
416
+ },
417
+ },
418
+ }
419
+ highlighter.setOptions(options);
420
+
421
+ function setColorOpacity(highContrast = false) {
422
+ if (highContrast) {
423
+ container.style.setProperty('--highlight-opacity', '80%');
424
+ container.style.setProperty('--highlight-opacity-active', '100%');
425
+ } else {
426
+ container.style.setProperty('--highlight-opacity', '50%');
427
+ container.style.setProperty('--highlight-opacity-active', '80%');
428
+ }
429
+ }
430
+ const highContrastToggle = document.querySelector('[name="highContrast"]');
431
+ highContrastToggle.addEventListener('change', (event) => { setColorOpacity(highContrastToggle.checked) });
432
+ setColorOpacity();
433
+ ```
434
+
435
+ #### Styling start and end mark elements
436
+
437
+ ```css
438
+ mark[data-hh-highlight-id][data-hh-style="fill"] {
439
+ -webkit-box-decoration-break: clone;
440
+ box-decoration-break: clone;
441
+ }
442
+ mark[data-hh-highlight-id][data-hh-style="fill"][data-hh-position~="start"] {
443
+ border-top-left-radius: 0.25em;
444
+ border-bottom-left-radius: 0.25em;
445
+ margin-left: -0.13em; padding-left: 0.13em;
446
+ }
447
+ mark[data-hh-highlight-id][data-hh-style="fill"][data-hh-position~="end"] {
448
+ border-top-right-radius: 0.25em;
449
+ border-bottom-right-radius: 0.25em;
450
+ margin-right: -0.13em; padding-right: 0.13em;
451
+ }
452
+ mark[data-hh-highlight-id][data-hh-style="fill"] + mark[hh-highlight-id][data-hh-style="fill"] {
453
+ margin-left: 0; padding-left: 0;
454
+ }
455
+ ```
456
+
457
+ #### Keeping wrapper on same line as highlight
458
+
459
+ ```css
460
+ [data-hh-wrapper="smile"] {
461
+ white-space: nowrap;
462
+ }
463
+ ```
464
+
465
+ ```javascript
466
+ const options = {
467
+ wrapperDefs: {
468
+ 'smile': {
469
+ start: '<span>[:)]</span>&NoBreak;',
470
+ end: '&NoBreak;<span>[(:]</span>',
471
+ },
472
+ },
473
+ };
474
+ highlighter.setOptions(options);
475
+ ```
476
+
477
+ #### Inserting dynamic elements
478
+
479
+ ```html
480
+ <!-- data-hh-ignore prevents text content from being included in character counts -->
481
+ <a href="#footnote" class="footnote-marker" data-hh-ignore>**</a>
482
+
483
+ <!-- class="sr-only" makes text content only visible to screen readers -->
484
+ <span class="sr-only" data-hh-ignore>(begin highlight)</span>
485
+ ```
486
+
487
+ #### Responding to tap events
488
+
489
+ ```javascript
490
+ // Add event listener for tap
491
+ container.addEventListener('hh:tap', respondToTap);
492
+ function respondToTap(event) {
493
+ if (event.detail.targetCount === 0 || event.detail.isLongPress || event.detail.selectionType === 'Range') return;
494
+ if (event.detail.targetCount > 1) {
495
+ handleAmbiguousTap(event);
496
+ } else if (event.detail.targetCount === 1) {
497
+ if (event.detail.highlights.length === 1) {
498
+ highlighter.activateHighlight(event.detail.highlights[0].highlightId);
499
+ } else if (event.detail.wrappers.length === 1) {
500
+ highlighter.activateHighlight(event.detail.wrappers[0].highlight.highlightId);
501
+ } else if (event.detail.hyperlinks.length === 1) {
502
+ highlighter.activateHyperlink(event.detail.hyperlinks[0].index);
503
+ }
504
+ }
505
+ }
506
+
507
+ // Load disambiguation panel for ambiguous taps
508
+ const disambiguationPanel = document.getElementById('disambiguation-panel');
509
+ const disambiguationButtons = document.getElementById('disambiguation-buttons');
510
+ const disambiguationCancelButton = document.getElementById('disambiguation-cancel');
511
+ disambiguationCancelButton.addEventListener('click', (event) => disambiguationPanel.close());
512
+ const handleAmbiguousTap = (event) => {
513
+ // Build buttons
514
+ let buttonsHtml = '';
515
+ const highlightIds = new Set();
516
+ for (const highlight of event.detail.highlights) highlightIds.add(highlight.highlightId);
517
+ for (const wrapper of event.detail.wrappers) highlightIds.add(wrapper.highlight.highlightId);
518
+ for (const highlight of highlighter.getHighlightInfo(highlightIds)) {
519
+ let colorString = highlighter.getOptions().colorDefs[highlight.color];
520
+ let styleString = highlighter.getOptions().styleDefs[highlight.style]['css'];
521
+ buttonsHtml += `
522
+ <button data-highlight-id="${highlight.highlightId}" style="--hh-color: ${colorString};">
523
+ <span style="${styleString}">${highlight.rangeText}</span>
524
+ </button>
525
+ `;
526
+ }
527
+ for (const hyperlink of event.detail.hyperlinks) {
528
+ buttonsHtml += `
529
+ <button data-hyperlink-index="${hyperlink.index}">
530
+ <span style="color: var(--dark-gray); text-decoration: underline;">${hyperlink.text}</span>
531
+ </button>
532
+ `;
533
+ }
534
+ disambiguationButtons.innerHTML = buttonsHtml;
535
+
536
+ // Add event listeners to buttons
537
+ for (const button of disambiguationButtons.children) {
538
+ button.addEventListener('click', () => {
539
+ disambiguationPanel.close();
540
+ if (button.dataset.highlightId) {
541
+ highlighter.activateHighlight(button.dataset.highlightId);
542
+ } else if (button.dataset.hyperlinkIndex) {
543
+ highlighter.activateHyperlink(button.dataset.hyperlinkIndex);
544
+ }
545
+ });
546
+ }
547
+
548
+ // Waiting 10 milliseconds before showing the disambiguation panel is a workaround for an issue in Android Chrome where a click event fires right after the pointerup event that triggered hh:tap. If the click event happens to be at the same position as a button in the panel, the button will receive the click, dismissing the panel before the user sees it.
549
+ setTimeout(function() { disambiguationPanel.showModal(); }, 10);
550
+ }
551
+ ```
552
+
553
+ #### Responding to hover events
554
+
555
+ ```javascript
556
+ container.addEventListener('hh:hover', (event) => {
557
+ container.style.cursor = (event.detail.targetCount > 0) ? 'pointer' : '';
558
+ });
559
+ ```
560
+
561
+ #### Moving wrappers during highlight resize
562
+
563
+ ```javascript
564
+ // Wrappers redraw when the highlight deactivates, but they can be manually adjusted while the highlight is active
565
+ function moveMarginIndicator(event) {
566
+ const activeHighlightId = highlighter.getActiveHighlightId();
567
+ if (!event.detail.activeHighlightId) return;
568
+ const marginIndicator = container.querySelector(`[data-hh-wrapper][data-hh-position="start"][data-hh-highlight-id="${activeHighlightId}"] .note`);
569
+ if (marginIndicator) {
570
+ marginIndicator.style.top = `${event.detail.rangeRect.top}px`;
571
+ }
572
+ }
573
+ container.addEventListener('hh:selectionchange', moveMarginIndicator);
574
+ ```
575
+
576
+ #### Keyboard shortcut for deleting a highlight
577
+
578
+ ```javascript
579
+ function removeHighlight(event) {
580
+ const activeHighlightId = highlighter.getActiveHighlightId();
581
+ highlighter.removeHighlights([activeHighlightId]);
582
+ }
583
+ document.addEventListener('keydown', (event) => {
584
+ if (event.key === 'Backspace') removeHighlight(event);
585
+ });
586
+ ```
587
+
588
+ #### Copying selected text or HTML
589
+
590
+ ```html
591
+ <button id="copy-text">Copy Text</button>
592
+ <button id="copy-html">Copy HTML</button>
593
+ ```
594
+
595
+ ```javascript
596
+ function copyToClipboard(plainText = true) {
597
+ const selectionState = highlighter.getSelectionState();
598
+ const content = plainText ? selectionState.rangeText : selectionState.rangeHtml;
599
+ navigator.clipboard.writeText(content);
600
+ }
601
+ document.getElementById('copy-text').addEventListener('click', copyToClipboard);
602
+ document.getElementById('copy-html').addEventListener('click', (event) => {
603
+ copyToClipboard(false);
604
+ });
605
+ ```
606
+
607
+ #### Hide highlights when printing
608
+
609
+ ```css
610
+ @media print {
611
+ [data-hh-additions] { display: none; } /* Hide SVG highlights and custom drag handles */
612
+ mark[data-hh-highlight-id] { all: unset; } /* Hide mark-element highlights */
613
+ }
614
+ ```
615
+
616
+ #### Reinitializing selection UI in iOS Safari
617
+
618
+ ```javascript
619
+ // After a period of inactivity with a locked screen, certain versions of iOS Safari may get into a state where system selection handles don't show when the app tries to select text programmatically (such as when a user taps an existing highlight). Sending a visibilitychange event triggers HighlightHelper.js to run code that enables programmatic text selection again.
620
+ document.dispatchEvent(new Event('visibilitychange'));
621
+ });
622
+ ```