@samuelbradshaw/highlight-helper-js 1.0.0 → 1.0.2

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
@@ -609,6 +609,7 @@ document.getElementById('copy-html').addEventListener('click', (event) => {
609
609
  ```css
610
610
  @media print {
611
611
  [data-hh-additions] { display: none; } /* Hide SVG highlights and custom drag handles */
612
+ [data-hh-wrapper] { display: none; } /* Hide wrappers */
612
613
  mark[data-hh-highlight-id] { all: unset; } /* Hide mark-element highlights */
613
614
  }
614
615
  ```
@@ -618,5 +619,4 @@ document.getElementById('copy-html').addEventListener('click', (event) => {
618
619
  ```javascript
619
620
  // 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
621
  document.dispatchEvent(new Event('visibilitychange'));
621
- });
622
622
  ```
@@ -272,6 +272,9 @@
272
272
  this._dragAnchorOffset = this._activeHandle.dataset.hhPosition === 'start' ? selectionRange.endOffset : selectionRange.startOffset;
273
273
  this._container.addEventListener('pointermove', respondToHandleDrag, { signal: this._controller.signal });
274
274
  this._updateSelectionState();
275
+
276
+ // Check for tap targets so hh:tap can fire if the area around a handle is tapped without dragging (tap result will reset to null if the user starts dragging)
277
+ this._tapResult = this._checkForTapTargets(event);
275
278
  }
276
279
  // Prevent default drag interaction (which would show a thumbnail and drag selected text)
277
280
  return event.preventDefault();
@@ -409,7 +412,11 @@
409
412
  const height = Math.round(entry.contentBoxSize[0].blockSize);
410
413
  // Respond to width changes (resize window, device rotation) and height changes (lazy-loaded images, dynamically-inserted elements)
411
414
  if (width !== previousWidth || height !== previousHeight) {
412
- if (options.drawingMode === 'svg') this.drawHighlights();
415
+ if (options.drawingMode === 'svg') {
416
+ this.drawHighlights();
417
+ } else {
418
+ this._updateHighlightRects();
419
+ }
413
420
  if (this._previousSelectionRange) this._updateSelectionState();
414
421
  previousWidth = width;
415
422
  previousHeight = height;
@@ -531,13 +538,23 @@
531
538
  startParagraphOffset = Number.parseInt(properties.startParagraphOffset ?? oldHighlightInfo?.startParagraphOffset);
532
539
  endParagraphId = properties.endParagraphId ?? oldHighlightInfo?.endParagraphId;
533
540
  endParagraphOffset = Number.parseInt(properties.endParagraphOffset ?? oldHighlightInfo?.endParagraphOffset);
541
+ if (!this._paragraphIds.includes(startParagraphId) || !this._paragraphIds.includes(endParagraphId)) return;
534
542
  ([ startNode, startOffset ] = this._getTextNodeAndOffset(document.getElementById(startParagraphId), startParagraphOffset, 'start'));
535
543
  ([ endNode, endOffset ] = this._getTextNodeAndOffset(document.getElementById(endParagraphId), endParagraphOffset, 'end'));
536
544
  } else if (adjustedSelectionRange) {
537
545
  startNode = adjustedSelectionRange.startContainer;
538
546
  startOffset = adjustedSelectionRange.startOffset;
547
+ if (this._shouldSkipTextNode(startNode)) {
548
+ startNode = this._getNextValidTextNode(startNode);
549
+ startOffset = 0;
550
+ }
539
551
  endNode = adjustedSelectionRange.endContainer;
540
552
  endOffset = adjustedSelectionRange.endOffset;
553
+ if (this._shouldSkipTextNode(endNode)) {
554
+ endNode = this._getPreviousValidTextNode(endNode);
555
+ endOffset = endNode?.length;
556
+ }
557
+ if (!startNode || !endNode) return;
541
558
  ([ startParagraphId, startParagraphOffset ] = this._getParagraphOffset(startNode, startOffset));
542
559
  ([ endParagraphId, endParagraphOffset ] = this._getParagraphOffset(endNode, endOffset));
543
560
  }
@@ -952,9 +969,9 @@
952
969
  this._additionsRect = this._additionsDiv.getBoundingClientRect();
953
970
  const x = clientX - this._additionsRect.left;
954
971
  const y = clientY - this._additionsRect.top;
955
- for (const highlightId of Object.keys(this._highlightsById)) {
972
+ for (const [highlightId, highlightInfo] of Object.entries(this._highlightsById)) {
956
973
  if (highlightIds.has(highlightId)) continue;
957
- const rangeLineRects = this._highlightsById[highlightId].rangeLineRects;
974
+ const rangeLineRects = highlightInfo.rangeLineRects;
958
975
  if (!rangeLineRects) continue;
959
976
  for (const rect of rangeLineRects) {
960
977
  const padding = rect.height / 8;
@@ -1131,8 +1148,12 @@
1131
1148
  const styleString = this._getStyleString(highlightInfo.style, 'css', false, highlightInfo.variables, highlightInfo.rangeRect);
1132
1149
  if (resolvedMode === 'highlight-api') {
1133
1150
  const colorString = this._options.colorDefs[highlightInfo.color];
1134
- cssText += `${this._containerSelector} ::highlight(${highlightInfo.escapedHighlightId}) { --hh-color: ${colorString}; ${styleString} }\n`;
1135
- cssText += `${this._containerSelector} :is(rt, img)::highlight(${highlightInfo.escapedHighlightId}) { color: inherit; background-color: transparent; }\n`;
1151
+ cssText += `
1152
+ ${this._containerSelector} ::highlight(${highlightInfo.escapedHighlightId}) { all: unset; }
1153
+ ${this._containerSelector} :is(${this._paragraphSelector})::highlight(${highlightInfo.escapedHighlightId}),
1154
+ ${this._containerSelector} :is(${this._paragraphSelector}) *::highlight(${highlightInfo.escapedHighlightId}) { --hh-color: ${colorString}; ${styleString} }
1155
+ ${this._containerSelector} :is(rt, img)::highlight(${highlightInfo.escapedHighlightId}) { color: inherit; background-color: transparent; }
1156
+ `;
1136
1157
  } else {
1137
1158
  cssText += `mark[data-hh-highlight-id=${highlightInfo.escapedHighlightId}][data-hh-style] { ${styleString} }\n`;
1138
1159
  }
@@ -1250,8 +1271,10 @@
1250
1271
  ${this._containerSelector} ::highlight(${highlightInfo.escapedHighlightId}) { all: unset; }
1251
1272
  ${this._containerSelector} mark[data-hh-highlight-id="${activeHighlightId}"][data-hh-style] { all: unset; }
1252
1273
  ${this._containerSelector} [data-hh-wrapper][data-hh-highlight-id="${activeHighlightId}"] { visibility: hidden; }
1253
- ${this._containerSelector} ::selection { --hh-color: ${colorString}; ${styleString} }
1254
- ${this._containerSelector} rt::selection, ${this._containerSelector} img::selection { background-color: transparent; }
1274
+ ${this._containerSelector} ::selection { background-color: transparent; }
1275
+ ${this._containerSelector} :is(${this._paragraphSelector})::selection,
1276
+ ${this._containerSelector} :is(${this._paragraphSelector}) *::selection { --hh-color: ${colorString}; ${styleString} }
1277
+ ${this._containerSelector} :is(rt, img)::selection { background-color: transparent; }
1255
1278
  `);
1256
1279
 
1257
1280
  // No active highlight (show the regular selection UI)
@@ -1268,7 +1291,7 @@
1268
1291
  (options.showDragHandles.includes('touch') || pointerType === 'mouse')
1269
1292
  && (options.showDragHandles.includes('highlights') && activeHighlightId
1270
1293
  || options.showDragHandles.includes('selection') && !activeHighlightId);
1271
- if (selection.type === 'Range' && showDragHandles) {
1294
+ if (selection.type === 'Range' && showDragHandles && rangeLineRects.length > 0) {
1272
1295
  const startNodeIsRtl = globalThis.getComputedStyle(selectionRange.startContainer.parentElement).direction === 'rtl';
1273
1296
  const endNodeIsRtl = globalThis.getComputedStyle(selectionRange.endContainer.parentElement).direction === 'rtl';
1274
1297
  for (const handle of this._dragHandles) {
@@ -1356,7 +1379,7 @@
1356
1379
  }
1357
1380
  if (skipEnd) {
1358
1381
  endNode = this._getPreviousValidTextNode(endNode);
1359
- endOffset = endNode.length;
1382
+ endOffset = endNode?.length;
1360
1383
  }
1361
1384
  if (!startNode || !endNode || (startNode === endNode && startOffset === endOffset)) {
1362
1385
  return range.cloneRange().collapse(true);
@@ -1459,7 +1482,7 @@
1459
1482
  // Determine if text node should be skipped when snapping to word or calculating character offsets
1460
1483
  Highlighter.prototype._shouldSkipTextNode = function (textNode) {
1461
1484
  const parentParagraph = this._closestValidParagraph(textNode.parentNode);
1462
- const ignoreParent = textNode.parentNode.closest('[data-hh-ignore]');
1485
+ const ignoreParent = textNode.parentNode.closest(`${this._containerSelector} [data-hh-ignore]`);
1463
1486
  if (!parentParagraph || ignoreParent || textNode.textContent === '') return true;
1464
1487
  return false;
1465
1488
  }
@@ -1675,7 +1698,7 @@
1675
1698
  range = document.caretRangeFromPoint(clientX, clientY);
1676
1699
  }
1677
1700
  if (!range) return;
1678
- if (range.startContainer.parentElement?.closest('[data-hh-ignore]')) return;
1701
+ if (range.startContainer.parentElement?.closest(`${this._containerSelector} [data-hh-ignore]`)) return;
1679
1702
  if (checkXDistance || checkYDistance) {
1680
1703
  const maxDistance = 30;
1681
1704
  const caretClientRect = range.getBoundingClientRect();
@@ -1737,6 +1760,16 @@
1737
1760
  return { lines, snapTolerance, pseudoElemRects };
1738
1761
  }
1739
1762
 
1763
+ Highlighter.prototype._updateHighlightRects = function (highlightIds = Object.keys(this._highlightsById), paragraphLineRectsCache = new Map(), columnGeometryCache = new Map()) {
1764
+ for (const highlightId of highlightIds) {
1765
+ const highlightInfo = this._highlightsById[highlightId];
1766
+ const rangeParagraphs = highlightInfo.rangeParagraphIds.map(id => document.getElementById(id));
1767
+ const [rangeRect, rangeLineRects] = this._getRangeRects(highlightInfo.rangeObj, rangeParagraphs, paragraphLineRectsCache, columnGeometryCache);
1768
+ highlightInfo.rangeRect = rangeRect;
1769
+ highlightInfo.rangeLineRects = rangeLineRects;
1770
+ }
1771
+ }
1772
+
1740
1773
  // Get merged client rects from the highlight range
1741
1774
  Highlighter.prototype._getRangeRects = function (range, paragraphs, paragraphLineRectsCache = new Map(), columnGeometryCache = new Map()) {
1742
1775
  let rangeRectCoordinates = { top: Number.POSITIVE_INFINITY, bottom: Number.NEGATIVE_INFINITY, left: Number.POSITIVE_INFINITY, right: Number.NEGATIVE_INFINITY };
@@ -1752,6 +1785,7 @@
1752
1785
  }
1753
1786
  const { lines, snapTolerance, pseudoElemRects } = paragraphLineRects;
1754
1787
  // If the previous paragraph is below the current paragraph (such as in multi-column layouts), reset prevParagraphBottom
1788
+ if (lines.length === 0) continue;
1755
1789
  if (prevParagraphBottom > lines[0].bottom) prevParagraphBottom = Number.NEGATIVE_INFINITY;
1756
1790
  const lineStates = lines.map((lineRect, i) => ({
1757
1791
  topY: i === 0 ? prevParagraphBottom : lines[i - 1].bottom,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samuelbradshaw/highlight-helper-js",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",