@samuelbradshaw/highlight-helper-js 1.0.3 → 1.1.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
@@ -122,7 +122,7 @@ You can also install it using [npm](https://www.npmjs.com/package/@samuelbradsha
122
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
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
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.
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. New highlights and the active highlight use the text selection for their range, unless a range is provided. Other highlights keep their range.
126
126
  - **draw** – Boolean. Indicates if the highlight should be drawn after loading. Default: `true`.
127
127
  - **activate** – Boolean. Indicates if the highlight should be activated after loading. Default: `true`.
128
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:
@@ -165,6 +165,7 @@ HighlightHelper.js sends [custom events](https://developer.mozilla.org/en-US/doc
165
165
  - **hh:highlightactivate** – Sent when a highlight is activated.
166
166
  - **hh:highlightdeactivate** – Sent when a highlight is deactivated.
167
167
  - **hh:highlightremove** – Sent when a highlight is removed.
168
+ - **hh:layoutchange** – Sent when highlights redraw because of container size changes.
168
169
 
169
170
  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
 
@@ -221,9 +222,9 @@ The following options can be set using the `setOptions()` method. Options that a
221
222
  - **right** – HTML string for the right selection handle.
222
223
  - Template variables (optional): `var(--hh-color)` can be used to reference the highlight color.
223
224
  - **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`.
225
+ - **highlights** – Show custom handles for active highlights.
226
+ - **selection** – Show custom handles for text selection.
227
+ - **touch** – Show custom handles on touch devices (may conflict with built-in selection handles).
227
228
  - **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
229
  - **styleDefs** – Object. Defines highlight style templates. Keys are style names. Default keys: `fill`, `underline`. Each style has four available properties:
229
230
  - **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.
@@ -279,19 +280,21 @@ Other:
279
280
  ### <a name="known-issues"></a>Known issues
280
281
 
281
282
  - **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)).
283
+ - Workaround: Tap the highlight again to show the handles, or enable custom drag handles.
284
+ - **Safari on iOS and macOS** doesn't allow full control of the underline style on a `::highlight()` pseudo-element, so underline highlights drawn by the CSS Custom Highlight API may look incorrect (see [StackOverflow](https://stackoverflow.com/q/79060854/1349044) and [WebKit Bugzilla](https://bugs.webkit.org/show_bug.cgi?id=282027)).
284
285
  - Workaround: Use a different drawing mode.
286
+ - **Safari on iOS and macOS** ignores text decoration styles on `::selection`, so active underline highlights can't be styled with CSS alone.
287
+ - Workaround: Set `svg` and `svgActive` for underline styles in styleDefs. On Safari, if a style has an `svg` template, HighlightHelper.js draws the active highlight with `svgActive` (or `svg` if `svgActive` isn't set), in any drawing mode.
288
+ - **Safari on iOS** ignores all `::selection` styles, so active highlights show the native blue selection overlay instead of the highlight style.
289
+ - Workaround: Set `svg` and `svgActive` for all styles in styleDefs. Alternatively, 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).
285
290
  - **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
291
  - 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
292
  - **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
293
  - Workaround: Double-tap then drag the stylus.
291
294
  - **Safari on macOS** – SVG highlights are slightly shifted when zoomed to the maximum zoom size.
292
295
  - 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.
296
+ - **Amazon Fire webview** doesn’t show selection handles, making it difficult to resize an existing highlight.
297
+ - Workaround: Long-press and drag to select everything you need before lifting your finger, or enable custom drag handles.
295
298
 
296
299
 
297
300
  ### <a name="choosing-a-drawing-mode"></a>Choosing a drawing mode
@@ -449,7 +452,7 @@ mark[data-hh-highlight-id][data-hh-style="fill"][data-hh-position~="end"] {
449
452
  border-bottom-right-radius: 0.25em;
450
453
  margin-right: -0.13em; padding-right: 0.13em;
451
454
  }
452
- mark[data-hh-highlight-id][data-hh-style="fill"] + mark[hh-highlight-id][data-hh-style="fill"] {
455
+ mark[data-hh-highlight-id][data-hh-style="fill"] + mark[data-hh-highlight-id][data-hh-style="fill"] {
453
456
  margin-left: 0; padding-left: 0;
454
457
  }
455
458
  ```
@@ -610,7 +613,7 @@ document.getElementById('copy-html').addEventListener('click', (event) => {
610
613
  @media print {
611
614
  [data-hh-additions] { display: none; } /* Hide SVG highlights and custom drag handles */
612
615
  [data-hh-wrapper] { display: none; } /* Hide wrappers */
613
- mark[data-hh-highlight-id] { all: unset; } /* Hide mark-element highlights */
616
+ mark[data-hh-highlight-id][data-hh-style] { all: unset; } /* Hide mark-element highlights */
614
617
  }
615
618
  ```
616
619
 
@@ -124,6 +124,7 @@
124
124
  position: absolute;
125
125
  width: 0px;
126
126
  visibility: hidden;
127
+ touch-action: none;
127
128
  }
128
129
  [data-hh-pointer-down="true"] [data-hh-handle] {
129
130
  /* Hiding selection handle reduces jumpiness when dragging (and makes it easier to see the selection) */
@@ -136,13 +137,13 @@
136
137
  [data-hh-handle] [draggable] {
137
138
  position: absolute;
138
139
  top: -0.3em;
139
- width: 3.2em;
140
- height: calc(100% + 1.3em);
140
+ width: 2.2em;
141
+ height: calc(100% + 1.8em);
141
142
  background-color: transparent;
142
143
  z-index: 1;
143
144
  }
144
- [data-hh-handle][data-hh-side="left"] [draggable] { right: -2em; }
145
- [data-hh-handle][data-hh-side="right"] [draggable] { left: -2em; }
145
+ [data-hh-handle][data-hh-side="left"] [draggable] { right: -0.4em; }
146
+ [data-hh-handle][data-hh-side="right"] [draggable] { left: -0.4em; }
146
147
  [data-hh-default-handle] {
147
148
  position: absolute;
148
149
  width: 0.8em;
@@ -177,7 +178,8 @@
177
178
  fill: transparent;
178
179
  stroke: none;
179
180
  }
180
- mark[data-hh-highlight-id] {
181
+ mark[data-hh-highlight-id]:where(${this._containerSelector} *) {
182
+ /* Scoped to the container, so another Highlighter instance's base rule can't override this instance's highlight styles */
181
183
  print-color-adjust: exact;
182
184
  background-color: transparent;
183
185
  color: inherit;
@@ -397,6 +399,7 @@
397
399
  // Hyperlink click (for each hyperlink in container)
398
400
  for (const hyperlinkElement of this._hyperlinkElements) {
399
401
  hyperlinkElement.addEventListener('click', (event) => {
402
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
400
403
  this.deactivateHighlights();
401
404
  if (!this._allowHyperlinkClick) event.preventDefault();
402
405
  }, { signal: this._controller.signal });
@@ -420,6 +423,7 @@
420
423
  if (this._previousSelectionRange) this._updateSelectionState();
421
424
  previousWidth = width;
422
425
  previousHeight = height;
426
+ this._container.dispatchEvent(new CustomEvent('hh:layoutchange', { detail: { width, height } }));
423
427
  }
424
428
  }
425
429
  }, Math.floor(Object.keys(this._highlightsById).length / 20)));
@@ -428,6 +432,7 @@
428
432
  // Set drawing mode to mark elements temporarily when printing (highlights get offset in SVG drawing mode)
429
433
  let previousDrawingMode = null;
430
434
  globalThis.addEventListener('beforeprint', () => {
435
+ if (this._activeHighlightId) this.deactivateHighlights();
431
436
  previousDrawingMode = this._options.drawingMode;
432
437
  if (previousDrawingMode === 'mark-elements') return;
433
438
  this.setOptions({ drawingMode: 'mark-elements' });
@@ -533,9 +538,15 @@
533
538
  let startParagraphId, startParagraphOffset, endParagraphId, endParagraphOffset;
534
539
  const selection = this._getRestoredSelectionOrCaret(globalThis.getSelection());
535
540
  if (selection.type === 'Range') adjustedSelectionRange = this._snapRangeToBoundaries(selection.getRangeAt(0));
536
- if ((properties.startParagraphId ?? properties.startParagraphOffset ?? properties.endParagraphId ?? properties.endParagraphOffset != null) || (adjustedSelectionRange && !adjustedSelectionRange.collapsed)) {
541
+ const hasBoundsProperties = ['startParagraphId', 'startParagraphOffset', 'endParagraphId', 'endParagraphOffset'].some(key => properties[key] != null);
542
+ // New highlights and the active highlight take their bounds from the text selection. Other highlights keep their saved bounds, unless new bounds are provided (otherwise, updating an inactive highlight would move it to the text selection, or do nothing if there isn't one).
543
+ const useSelectionBounds = !hasBoundsProperties && adjustedSelectionRange && !adjustedSelectionRange.collapsed && (isNewHighlight || highlightId === this._activeHighlightId);
544
+ if (!isNewHighlight && !hasBoundsProperties && !useSelectionBounds) {
545
+ // Only appearance properties changed, so reuse the saved range instead of recalculating it
546
+ highlightRange = this._getCorrectedRangeObj(highlightId);
547
+ } else if (hasBoundsProperties || useSelectionBounds) {
537
548
  let startNode, startOffset, endNode, endOffset;
538
- if (properties.startParagraphId ?? properties.startParagraphOffset ?? properties.endParagraphId ?? properties.endParagraphOffset != null) {
549
+ if (!useSelectionBounds) {
539
550
  startParagraphId = properties.startParagraphId ?? oldHighlightInfo?.startParagraphId;
540
551
  startParagraphOffset = Number.parseInt(properties.startParagraphOffset ?? oldHighlightInfo?.startParagraphOffset);
541
552
  endParagraphId = properties.endParagraphId ?? oldHighlightInfo?.endParagraphId;
@@ -543,7 +554,7 @@
543
554
  if (!this._paragraphIds.includes(startParagraphId) || !this._paragraphIds.includes(endParagraphId)) return;
544
555
  ([ startNode, startOffset ] = this._getTextNodeAndOffset(document.getElementById(startParagraphId), startParagraphOffset, 'start'));
545
556
  ([ endNode, endOffset ] = this._getTextNodeAndOffset(document.getElementById(endParagraphId), endParagraphOffset, 'end'));
546
- } else if (adjustedSelectionRange) {
557
+ } else {
547
558
  startNode = adjustedSelectionRange.startContainer;
548
559
  startOffset = adjustedSelectionRange.startOffset;
549
560
  if (this._shouldSkipTextNode(startNode)) {
@@ -883,9 +894,19 @@
883
894
  return;
884
895
  }
885
896
  const selection = globalThis.getSelection();
886
- const highlightRange = this._getCorrectedRangeObj(highlightId);
897
+
898
+ // Switch the active highlight before redrawing the previously active one, so there's no in-between state with no active highlight (an hh:selectionchange event in that state could lead an app to create a new highlight over the previous one)
899
+ const previousHighlight = this._highlightsById[this._activeHighlightId];
887
900
  this._activeHighlightId = highlightId;
888
901
 
902
+ // Redraw the previously active highlight. In mark-elements mode, this changes the DOM, so get the new highlight's range afterward.
903
+ if (previousHighlight) this.drawHighlights([previousHighlight.highlightId]);
904
+ const highlightRange = this._getCorrectedRangeObj(highlightId);
905
+
906
+ // Save the highlight's range as the previous selection range. Otherwise, a pending selectionchange event (such as one from the tap that led to this highlight being activated) is compared against a stale range, which can deactivate the highlight or restore a different selection range.
907
+ this._previousSelectionRange = highlightRange.cloneRange();
908
+ this._previousSelectionDirection = 'forward';
909
+
889
910
  // Update the selection range if needed
890
911
  // In Android Chrome, sometimes selection handles don't show when a selection is updated programmatically, so it's best to only update the selection if needed.
891
912
  let selectionRange = selection.type === 'Range' ? selection.getRangeAt(0) : null;
@@ -895,6 +916,7 @@
895
916
  }
896
917
 
897
918
  this._updateSelectionState();
919
+ if (previousHighlight) this._container.dispatchEvent(new CustomEvent('hh:highlightdeactivate', { detail: { highlight: previousHighlight } }));
898
920
  this._container.dispatchEvent(new CustomEvent('hh:highlightactivate', { detail: { highlight: highlightToActivate } }));
899
921
  }
900
922
 
@@ -1151,13 +1173,14 @@
1151
1173
  if (resolvedMode === 'highlight-api') {
1152
1174
  const colorString = this._options.colorDefs[highlightInfo.color];
1153
1175
  cssText += `
1154
- ${this._containerSelector} ::highlight(${highlightInfo.escapedHighlightId}) { all: unset; }
1155
- ${this._containerSelector} :is(${this._paragraphSelector})::highlight(${highlightInfo.escapedHighlightId}),
1156
- ${this._containerSelector} :is(${this._paragraphSelector}) *::highlight(${highlightInfo.escapedHighlightId}) { --hh-color: ${colorString}; ${styleString} }
1157
- ${this._containerSelector} :is(rt, img)::highlight(${highlightInfo.escapedHighlightId}) { color: inherit; background-color: transparent; }
1176
+ :where(${this._containerSelector} *)::highlight(${highlightInfo.escapedHighlightId}) { all: unset; }
1177
+ :where(${this._containerSelector} :is(${this._paragraphSelector}))::highlight(${highlightInfo.escapedHighlightId}),
1178
+ :where(${this._containerSelector} :is(${this._paragraphSelector}) *)::highlight(${highlightInfo.escapedHighlightId}) { --hh-color: ${colorString}; ${styleString} }
1179
+ :where(${this._containerSelector} :is(rt, img))::highlight(${highlightInfo.escapedHighlightId}),
1180
+ :where(${this._containerSelector} :is(${this._paragraphSelector}) :is(rt, img))::highlight(${highlightInfo.escapedHighlightId}) { color: inherit; background-color: transparent; }
1158
1181
  `;
1159
1182
  } else {
1160
- cssText += `mark[data-hh-highlight-id=${highlightInfo.escapedHighlightId}][data-hh-style] { ${styleString} }\n`;
1183
+ cssText += `mark[data-hh-style]:where([data-hh-highlight-id=${highlightInfo.escapedHighlightId}]) { ${styleString} }\n`;
1161
1184
  }
1162
1185
  }
1163
1186
  }
@@ -1190,7 +1213,6 @@
1190
1213
  // Get active highlight properties
1191
1214
  const activeHighlightId = this._activeHighlightId;
1192
1215
  const highlightInfo = this._highlightsById[activeHighlightId] ?? null;
1193
- const isSvgActive = activeHighlightId && highlightInfo?.resolvedDrawingMode === 'svg';
1194
1216
  const color = highlightInfo?.color ?? null;
1195
1217
  const style = highlightInfo?.style ?? null;
1196
1218
  const variables = highlightInfo?.variables ?? {};
@@ -1209,6 +1231,9 @@
1209
1231
  if (boundsHash != this._previousBoundsHash) changes.push('selection');
1210
1232
  if (changes.length === 0) return;
1211
1233
 
1234
+ // WebKit doesn't draw text decorations on ::selection, and iOS ignores ::selection styles entirely. If the style has an SVG template, use it for the active state.
1235
+ const useSvgSelection = activeHighlightId && (highlightInfo?.resolvedDrawingMode === 'svg' || isWebKit && this._getStyleDef(style)?.svg);
1236
+
1212
1237
  // Get range metadata
1213
1238
  let rangeText = null, rangeHtml = null, rangeParagraphIds = null, rangeObj = null;
1214
1239
  let rangeRect = null, rangeParagraphs = [], rangeLineRects = [];
@@ -1234,9 +1259,9 @@
1234
1259
  this._selectionState = { activeHighlightId, color, style, variables, isResizing, pointerType, selection, changes, rangeText, rangeHtml, rangeRect: rangeRect, rangeLineRects, rangeParagraphIds, rangeObj };
1235
1260
  this._previousBoundsHash = boundsHash;
1236
1261
 
1237
- // Draw SVG selection rects (SVG drawing mode only)
1262
+ // Draw SVG selection rects (SVG drawing mode, or WebKit selection style fallback)
1238
1263
  this._svgActiveOverlay.innerHTML = '';
1239
- if (isSvgActive) {
1264
+ if (useSvgSelection) {
1240
1265
  this._svgActiveOverlay.dataset.hhColor = color;
1241
1266
  this._svgActiveOverlay.dataset.hhStyle = style;
1242
1267
  let svgContent = '';
@@ -1256,33 +1281,33 @@
1256
1281
  const colorString = options.colorDefs[color] ?? (supportsAccentColor ? 'AccentColor' : 'dodgerblue');
1257
1282
  this._container.style.setProperty('--hh-color', colorString);
1258
1283
 
1259
- // Hide the active highlight (and wrappers), and set a selection style that mimics the highlight. This avoids the need to redraw the highlight while actively editing it (especially important for <mark> highlights, because DOM manipulation around the selection can make the selection UI unstable).
1260
- if (isSvgActive) {
1261
- this._selectionStylesheet.replaceSync(`
1262
- ${this._containerSelector} g[data-hh-highlight-id="${activeHighlightId}"][data-hh-style] { display: none; }
1263
- ${this._containerSelector} [data-hh-wrapper][data-hh-highlight-id="${activeHighlightId}"] { visibility: hidden; }
1264
- ${this._containerSelector} ::selection { background-color: transparent; }
1265
- `);
1266
- } else if (activeHighlightId) {
1267
- // Remove stale SVG highlight if a highlight switches from SVG to a different resolvedDrawingMode
1268
- for (const svgHighlight of this._svgBackground.querySelectorAll(`g[data-hh-highlight-id="${activeHighlightId}"]`)) {
1269
- svgHighlight.remove();
1270
- }
1271
- const styleString = this._getStyleString(style, 'css', true, highlightInfo.variables, highlightInfo.rangeRect);
1272
- this._selectionStylesheet.replaceSync(`
1273
- ${this._containerSelector} ::highlight(${highlightInfo.escapedHighlightId}) { all: unset; }
1284
+ // Hide the active highlight (and wrappers), and either draw it with the SVG overlay or set a selection style that mimics it. This avoids the need to redraw the highlight while actively editing it (especially important for <mark> highlights, because DOM manipulation around the selection can make the selection UI unstable).
1285
+ if (activeHighlightId) {
1286
+ let cssText = `
1287
+ ${this._containerSelector} ::highlight(${highlightInfo.escapedHighlightId}),
1288
+ ${this._containerSelector} :is(${this._paragraphSelector})::highlight(${highlightInfo.escapedHighlightId}),
1289
+ ${this._containerSelector} :is(${this._paragraphSelector}) *::highlight(${highlightInfo.escapedHighlightId}) { all: unset; }
1274
1290
  ${this._containerSelector} mark[data-hh-highlight-id="${activeHighlightId}"][data-hh-style] { all: unset; }
1275
1291
  ${this._containerSelector} [data-hh-wrapper][data-hh-highlight-id="${activeHighlightId}"] { visibility: hidden; }
1292
+ ${this._containerSelector} g[data-hh-highlight-id="${activeHighlightId}"][data-hh-style] { display: none; }
1276
1293
  ${this._containerSelector} ::selection { background-color: transparent; }
1277
- ${this._containerSelector} :is(${this._paragraphSelector})::selection,
1278
- ${this._containerSelector} :is(${this._paragraphSelector}) *::selection { --hh-color: ${colorString}; ${styleString} }
1279
- ${this._containerSelector} :is(rt, img)::selection { background-color: transparent; }
1280
- `);
1294
+ `;
1295
+ // If not drawn by the SVG overlay, style the selection to mimic the highlight
1296
+ if (!useSvgSelection) {
1297
+ const styleString = this._getStyleString(style, 'css', true, highlightInfo.variables, highlightInfo.rangeRect);
1298
+ cssText += `
1299
+ ${this._containerSelector} :is(${this._paragraphSelector})::selection,
1300
+ ${this._containerSelector} :is(${this._paragraphSelector}) *::selection { --hh-color: ${colorString}; ${styleString} }
1301
+ ${this._containerSelector} :is(rt, img)::selection,
1302
+ ${this._containerSelector} :is(${this._paragraphSelector}) :is(rt, img)::selection { background-color: transparent; }
1303
+ `;
1304
+ }
1305
+ this._selectionStylesheet.replaceSync(cssText);
1281
1306
 
1282
1307
  // No active highlight (show the regular selection UI)
1283
1308
  } else {
1284
1309
  this._selectionStylesheet.replaceSync(`
1285
- ${this._containerSelector} ::selection { background-color: Highlight; color: inherit; }
1310
+ :where(${this._containerSelector} *)::selection { background-color: Highlight; color: inherit; }
1286
1311
  ${this._containerSelector} rt::selection, ${this._containerSelector} img::selection { background-color: transparent; }
1287
1312
  `);
1288
1313
  }
@@ -1506,7 +1531,7 @@
1506
1531
  const options = this._options;
1507
1532
  let css = '';
1508
1533
  for (const color of Object.keys(options.colorDefs)) {
1509
- css += `${this._containerSelector} [data-hh-color="${color}"] { --hh-color: ${options.colorDefs[color]}; }\n`;
1534
+ css += `:where(${this._containerSelector} [data-hh-color="${color}"]) { --hh-color: ${options.colorDefs[color]}; }\n`;
1510
1535
  }
1511
1536
  this._colorStylesheet.replaceSync(css);
1512
1537
  }
@@ -1515,8 +1540,7 @@
1515
1540
  Highlighter.prototype._getResolvedDrawingMode = function (highlightInfo) {
1516
1541
  const options = this._options;
1517
1542
  if (options.drawingMode === 'mark-elements') return 'mark-elements';
1518
- const resolvedStyle = Object.hasOwn(options.styleDefs, highlightInfo.style) ? highlightInfo.style : _defaultStyle;
1519
- if ((options.drawingMode === 'svg' && !options.styleDefs[resolvedStyle]?.svg)
1543
+ if ((options.drawingMode === 'svg' && !this._getStyleDef(highlightInfo.style)?.svg)
1520
1544
  || (options.drawingMode === 'highlight-api' && !supportsHighlightApi)) {
1521
1545
  return 'mark-elements';
1522
1546
  }
@@ -1589,13 +1613,18 @@
1589
1613
  return str.replace(/\{[^}]+\}/g, match => map[match] ?? match);
1590
1614
  }
1591
1615
 
1616
+ // Get the style definition for a highlight style, falling back to the default style
1617
+ Highlighter.prototype._getStyleDef = function (style) {
1618
+ const styleDefs = this._options.styleDefs;
1619
+ return styleDefs[Object.hasOwn(styleDefs, style) ? style : _defaultStyle];
1620
+ }
1621
+
1592
1622
  // Get style string for a given highlight style
1593
1623
  Highlighter.prototype._getStyleString = function (style, type, active = false, variables = {}, rangeRect = null, lineRect = null) {
1594
- const options = this._options;
1595
- style = Object.hasOwn(options.styleDefs, style) ? style : _defaultStyle;
1596
- let styleString = options.styleDefs[style]?.[type] ?? '';
1624
+ const styleDef = this._getStyleDef(style);
1625
+ let styleString = styleDef?.[type] ?? '';
1597
1626
  if (active) {
1598
- styleString = options.styleDefs[style]?.[`${type}Active`] ?? styleString;
1627
+ styleString = styleDef?.[`${type}Active`] ?? styleString;
1599
1628
  }
1600
1629
  if (styleString) {
1601
1630
  styleString = this._applySubstitutions(styleString, variables, rangeRect, lineRect);
@@ -1777,6 +1806,8 @@
1777
1806
  }
1778
1807
 
1779
1808
  Highlighter.prototype._updateHighlightRects = function (highlightIds = Object.keys(this._highlightsById), paragraphLineRectsCache = new Map(), columnGeometryCache = new Map()) {
1809
+ // Refresh _additionsRect in case it has stale data
1810
+ this._additionsRect = this._additionsDiv.getBoundingClientRect();
1780
1811
  for (const highlightId of highlightIds) {
1781
1812
  const highlightInfo = this._highlightsById[highlightId];
1782
1813
  const rangeParagraphs = highlightInfo.rangeParagraphIds.map(id => document.getElementById(id));
@@ -2064,7 +2095,7 @@
2064
2095
 
2065
2096
  // Check browser type
2066
2097
  const isTouchDevice = navigator.maxTouchPoints > 0;
2067
- const isWebKit = /^((?!Chrome|Firefox|Android|Samsung).)*AppleWebKit/i.test(navigator.userAgent);
2098
+ const isWebKit = /^(?!.*(Chrome|Firefox|Android|Samsung)).*AppleWebKit/i.test(navigator.userAgent);
2068
2099
  const isWKWebView = isWebKit && globalThis.webkit?.messageHandlers;
2069
2100
  const supportsCaretPositionFromPoint = document.caretPositionFromPoint;
2070
2101
  const supportsCaretRangeFromPoint = document.caretRangeFromPoint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samuelbradshaw/highlight-helper-js",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "JavaScript tool that enables highlighting or underlining text in an HTML page.",
5
5
  "type": "module",
6
6
  "main": "highlight-helper.mjs",