@samuelbradshaw/highlight-helper-js 0.9.0 → 0.10.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 +5 -5
- package/demo.html +81 -38
- package/highlight-helper.js +119 -77
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,20 +91,20 @@ You can also install it using [npm](https://www.npmjs.com/package/@samuelbradsha
|
|
|
91
91
|
- **removeHighlights(highlightIds)** – Remove the specified highlights. If `highlightIds` isn’t provided, all highlights will be removed.
|
|
92
92
|
- **getActiveHighlightId()** – Get the ID of the currently-active highlight (if there is one).
|
|
93
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
|
-
- **
|
|
94
|
+
- **setOptions(optionsToUpdate)** – Change one or more options. `optionsToUpdate` is an object with one or more of the option keys described below.
|
|
95
95
|
- **getOptions()** – Get the initialized options, including defaults for any options that weren’t explicitly set.
|
|
96
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).
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
### <a name="options"></a>Options
|
|
100
100
|
|
|
101
|
-
Options can be provided when HighlightHelper.js is initialized. They can
|
|
101
|
+
Options can be provided when HighlightHelper.js is initialized. They can be updated after initialization using `setOptions()`.
|
|
102
102
|
|
|
103
103
|
- **containerSelector** – CSS selector for the section of the page that should be annotatable. Default: `body`.
|
|
104
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
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
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).
|
|
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). `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
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
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
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`.
|
|
@@ -129,11 +129,11 @@ HighlightHelper.js sends [custom events](https://developer.mozilla.org/en-US/doc
|
|
|
129
129
|
- **hh:highlightcreate** – Sent when a new highlight is created. Includes information about the highlight.
|
|
130
130
|
- **hh:highlightupdate** – Sent when the highlight changes. Includes information about the highlight, and a list of the attributes that changed.
|
|
131
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
|
|
132
|
+
- **hh:tap** – Sent when a user taps in the annotatable container (potentially trying to tap a highlight, wrapper, or link). This will only be sent if there isn’t currently an active highlight. Includes the location of the tap, and the highlight, wrapper, or hyperlink that was tapped, if any.
|
|
133
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
134
|
- **hh:highlightactivate** – Sent when a highlight is activated. Includes information about the activated highlight.
|
|
135
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
|
|
136
|
+
- **hh:ambiguousaction** – Sent when a user taps on overlapping highlights, wrappers, and/or links. Includes the location of the tap, and the highlight(s), wrapper(s), and hyperlink(s) that were tapped. This event will not be sent if `autoTapToActivate` is set to `false`.
|
|
137
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
138
|
|
|
139
139
|
|
package/demo.html
CHANGED
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
--gray-90: hsl(0, 0%, 30%);
|
|
20
20
|
--gray-50: hsl(0, 0%, 70%);
|
|
21
21
|
}}
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
body {
|
|
24
24
|
background-color: var(--page-bg);
|
|
25
25
|
color: var(--text-color);
|
|
26
26
|
line-height: 1.4;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
main { margin-right: 2em; }
|
|
29
29
|
a { color: var(--gray-50); }
|
|
30
30
|
button {
|
|
31
31
|
cursor: pointer;
|
|
32
32
|
user-select: none;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
#annotation-menu {
|
|
36
36
|
font-family: "Helvetica", sans-serif;
|
|
37
37
|
font-size: 0.8em;
|
|
@@ -54,7 +54,10 @@
|
|
|
54
54
|
button[name="hh-color"][value="yellow"] { background-color: hsl(50, 98%, 61%); }
|
|
55
55
|
button[name="hh-color"][value="green"] { background-color: hsl(75, 70%, 49%); }
|
|
56
56
|
button[name="hh-color"][value="blue"] { background-color: hsl(182, 86%, 47%); }
|
|
57
|
-
button[name="hh-style"], button[name="hh-wrapper"] {
|
|
57
|
+
button[name="hh-style"], button[name="hh-wrapper"] {
|
|
58
|
+
position: relative;
|
|
59
|
+
color: inherit; border: 2px solid var(--gray-50);
|
|
60
|
+
}
|
|
58
61
|
button[name="hh-remove"] { background-color: var(--gray-50); color: var(--page-bg); }
|
|
59
62
|
.highlight-placeholder {
|
|
60
63
|
background-color: var(--gray-90);
|
|
@@ -68,10 +71,25 @@
|
|
|
68
71
|
font-size: smaller;
|
|
69
72
|
line-height: 0.5em;
|
|
70
73
|
&::before {
|
|
71
|
-
content: attr(data-
|
|
74
|
+
content: attr(data-content) / attr(data-accessibility-label);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
.note-container {
|
|
78
|
+
display: inline-block;
|
|
79
|
+
visibility: visible;
|
|
80
|
+
font-style: normal;
|
|
81
|
+
height: 1em;
|
|
82
|
+
}
|
|
83
|
+
.note {
|
|
84
|
+
position: absolute;
|
|
85
|
+
right: 0.5rem;
|
|
86
|
+
font-size: 1.2rem;
|
|
87
|
+
margin-top: -0.2rem;
|
|
88
|
+
&::before {
|
|
89
|
+
content: attr(data-content) / attr(data-accessibility-label);
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
|
-
|
|
92
|
+
|
|
75
93
|
#disambiguation-panel {
|
|
76
94
|
color: inherit;
|
|
77
95
|
background-color: inherit;
|
|
@@ -106,13 +124,12 @@
|
|
|
106
124
|
<body>
|
|
107
125
|
<main>
|
|
108
126
|
<h1 id="title1">HighlightHelper.js Demo</h1>
|
|
109
|
-
<img src="https://upload.wikimedia.org/wikipedia/commons/d/db/CMYK-color_model.png">
|
|
110
127
|
<p id="p1"><b>HighlightHelper.js</b> enables highlighting or underlining text in an HTML page. Highlights are drawn via <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/SVG">SVG shapes</a>, the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">Custom Highlight API</a>, or <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/mark">HTML mark elements</a>. Highlights can be drawn across multiple paragraphs or <b>inline <i>style</i></b> elements—they can even overlap!</p>
|
|
111
128
|
<p id="p2">Select text, then choose a color and style. Tap away when you’re done. Tap to activate it again for editing. Secondary elements like images (right) and Japanese ruby/furigana (below) are highlighted around.</p>
|
|
112
129
|
<p id="p3">わたしニーファイは<ruby><rb>善</rb><rt>よ</rt></ruby>い<ruby><rb>両</rb><rt>りょう</rt></ruby><ruby><rb>親</rb><rt>しん</rt></ruby>から<ruby><rb>生</rb><rt>う</rt></ruby>まれたので,<ruby><rb>父</rb><rt>ちち</rt></ruby>が<ruby><rb>学</rb><rt>まな</rt></ruby>んだすべてのことの<ruby><rb>中</rb><rt>なか</rt></ruby>から<ruby><rb>幾</rb><rt>いく</rt></ruby>らかの<ruby><rb>教</rb><rt>おし</rt></ruby>えを<ruby><rb>受</rb><rt>う</rt></ruby>けた。</p>
|
|
113
130
|
<p id="p4" style="direction: rtl;">فِي الْبَدْءِ خَلَقَ اللهُ السَّمَاوَاتِ وَالأَرْضَ. وَكَانَتِ الأَرْضُ خَرِبَةً وَخَالِيَةً، وَعَلَى وَجْهِ الْغَمْرِ ظُلْمَةٌ، وَرُوحُ اللهِ يَرِفُّ عَلَى وَجْهِ الْمِيَاهِ. وَقَالَ اللهُ: «لِيَكُنْ نُورٌ»، فَكَانَ نُورٌ.</p>
|
|
114
131
|
</main>
|
|
115
|
-
|
|
132
|
+
|
|
116
133
|
<hr>
|
|
117
134
|
<aside id="annotation-menu">
|
|
118
135
|
<div class="buttons-area">
|
|
@@ -127,9 +144,10 @@
|
|
|
127
144
|
<button name="hh-style" value="colored-text"> Colored text</button>
|
|
128
145
|
<button name="hh-style" value="redacted"> Redacted</button>
|
|
129
146
|
<button name="hh-remove">Remove</button>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
<button name="hh-wrapper" value="
|
|
147
|
+
<button name="hh-wrapper" value="none" title="No wrapper"><span class="highlight-placeholder">…</span></button>
|
|
148
|
+
<button name="hh-wrapper" value="footnote" title="Footnote"><span class="footnote-marker" data-content="a" data-accessibility-label="Footnote [a]"></span><span class="highlight-placeholder">…</span></button>
|
|
149
|
+
<button name="hh-wrapper" value="margin-note" title="Margin note"><span class="note-container"><span class="note" data-content="🙂" data-accessibility-label="Smile emoji"></span></span><span class="highlight-placeholder">…</span></button>
|
|
150
|
+
<button name="hh-wrapper" value="margin-note" title="Margin note"><span class="note-container"><span class="note" data-content="✅" data-accessibility-label="Checkmark emoji"></span></span><span class="highlight-placeholder">…</span></button>
|
|
133
151
|
</div>
|
|
134
152
|
<p>
|
|
135
153
|
<label><input type="checkbox" id="hh-remember-style" checked> Remember most recent color and style</label><br>
|
|
@@ -167,7 +185,7 @@
|
|
|
167
185
|
<p>
|
|
168
186
|
← <a href="https://github.com/samuelbradshaw/highlight-helper-js">Back to GitHub</a>
|
|
169
187
|
</p>
|
|
170
|
-
|
|
188
|
+
|
|
171
189
|
<!-- Link to the HighlightHelper.js JavaScript file -->
|
|
172
190
|
<script src="highlight-helper.js"></script>
|
|
173
191
|
<script>
|
|
@@ -181,15 +199,15 @@
|
|
|
181
199
|
},
|
|
182
200
|
{
|
|
183
201
|
highlightId: 'hh-1729991847300',
|
|
184
|
-
color: 'blue', style: 'fill',
|
|
202
|
+
color: 'blue', style: 'fill', wrapper: 'margin-note',
|
|
203
|
+
wrapperVariables: { content: '🙂', accessibilityLabel: 'Smile emoji', },
|
|
185
204
|
startParagraphId: 'p2', startParagraphOffset: 27,
|
|
186
205
|
endParagraphId: 'p2', endParagraphOffset: 42,
|
|
187
206
|
},
|
|
188
207
|
{
|
|
189
208
|
highlightId: 'hh-1729991847468',
|
|
190
|
-
color: 'red', style: '
|
|
191
|
-
wrapperVariables: {
|
|
192
|
-
readOnly: true,
|
|
209
|
+
color: 'red', style: 'single-underline', wrapper: 'footnote',
|
|
210
|
+
wrapperVariables: { content: 'a', accessibilityLabel: 'Footnote [a]', },
|
|
193
211
|
startParagraphId: 'p2', startParagraphOffset: 119,
|
|
194
212
|
endParagraphId: 'p2', endParagraphOffset: 127,
|
|
195
213
|
},
|
|
@@ -218,17 +236,18 @@
|
|
|
218
236
|
endParagraphId: 'p4', endParagraphOffset: 207,
|
|
219
237
|
},
|
|
220
238
|
];
|
|
221
|
-
|
|
239
|
+
|
|
222
240
|
// Initialize a Highlighter instance, then load highlights
|
|
223
241
|
const highlighter = new Highlighter({
|
|
224
242
|
containerSelector: 'main',
|
|
225
243
|
paragraphSelector: 'main [id]',
|
|
226
244
|
wrappers: {
|
|
227
|
-
'footnote': { start: '<span class="footnote-marker" data-
|
|
245
|
+
'footnote': { start: '<span class="footnote-marker" data-content="{content}" data-accessibility-label="{accessibilityLabel}"></span>', end: '', },
|
|
246
|
+
'margin-note': { start: '<span class="note-container"><span class="note" data-content="{content}" data-accessibility-label="{accessibilityLabel}"></span></span>', end: '', },
|
|
228
247
|
},
|
|
229
248
|
});
|
|
230
249
|
highlighter.loadHighlights(highlights);
|
|
231
|
-
|
|
250
|
+
|
|
232
251
|
// Call HighlightHelper.js methods when color, style, wrapper, or remove buttons on this demo page are tapped
|
|
233
252
|
const colorButtons = document.getElementsByName('hh-color');
|
|
234
253
|
for (const colorButton of colorButtons) {
|
|
@@ -240,7 +259,13 @@
|
|
|
240
259
|
}
|
|
241
260
|
const wrapperButtons = document.getElementsByName('hh-wrapper');
|
|
242
261
|
for (const wrapperButton of wrapperButtons) {
|
|
243
|
-
wrapperButton.
|
|
262
|
+
const contentSpan = wrapperButton.querySelector('[data-content]');
|
|
263
|
+
wrapperButton.addEventListener('click', () => {
|
|
264
|
+
highlighter.createOrUpdateHighlight({
|
|
265
|
+
wrapper: wrapperButton.value === 'none' ? null : wrapperButton.value,
|
|
266
|
+
wrapperVariables: { content: contentSpan?.dataset?.content ?? '', accessibilityLabel: contentSpan?.dataset?.accessibilityLabel ?? '', },
|
|
267
|
+
});
|
|
268
|
+
});
|
|
244
269
|
}
|
|
245
270
|
const removeButton = document.getElementsByName('hh-remove')[0];
|
|
246
271
|
removeButton.addEventListener('click', () => { highlighter.removeHighlights([highlighter.getActiveHighlightId()]); });
|
|
@@ -248,7 +273,7 @@
|
|
|
248
273
|
function respondToKeyPress(event) {
|
|
249
274
|
if (event.key === 'Backspace') highlighter.removeHighlights([highlighter.getActiveHighlightId()]);
|
|
250
275
|
}
|
|
251
|
-
|
|
276
|
+
|
|
252
277
|
// Call HighlightHelper.js methods when options are changed
|
|
253
278
|
const rememberStyleCheckbox = document.getElementById('hh-remember-style');
|
|
254
279
|
const snapToWordCheckbox = document.getElementById('hh-snap-to-word');
|
|
@@ -278,11 +303,11 @@
|
|
|
278
303
|
selectionHandlesTextSelectionCheckbox.addEventListener('change', (event) => { highlighter.setOption('showCustomSelectionHandlesForTextSelection', event.currentTarget.checked); });
|
|
279
304
|
const selectionHandlesOnTouchCheckbox = document.getElementById('hh-selection-handles-touch');
|
|
280
305
|
selectionHandlesOnTouchCheckbox.addEventListener('change', (event) => { highlighter.setOption('showCustomSelectionHandlesOnTouch', event.currentTarget.checked); });
|
|
281
|
-
|
|
306
|
+
|
|
282
307
|
const container = document.querySelector(highlighter.getOptions().containerSelector);
|
|
283
308
|
const snippetDiv = document.getElementById('snippet');
|
|
284
309
|
const infoDiv = document.getElementById('highlight-info');
|
|
285
|
-
|
|
310
|
+
|
|
286
311
|
// Respond to highlight update and highlight activate event
|
|
287
312
|
container.addEventListener('hh:highlightupdate', updateHighlightInfo);
|
|
288
313
|
container.addEventListener('hh:highlightactivate', updateHighlightInfo);
|
|
@@ -300,6 +325,9 @@
|
|
|
300
325
|
for (const annotatedParagraph of annotatedParagraphs) {
|
|
301
326
|
const snippetParagraph = annotatedParagraph.cloneNode(true);
|
|
302
327
|
snippetParagraph.id = `${snippetParagraph.id}_${event.detail.highlight.highlightId}`;
|
|
328
|
+
for (const wrapperEl of snippetParagraph.querySelectorAll('[data-highlight-id]')) {
|
|
329
|
+
wrapperEl.dataset.highlightId = wrapperEl.dataset.highlightId + '_snippet';
|
|
330
|
+
}
|
|
303
331
|
snippetDiv.append(snippetParagraph);
|
|
304
332
|
}
|
|
305
333
|
const snippetHighlights = [
|
|
@@ -317,15 +345,23 @@
|
|
|
317
345
|
});
|
|
318
346
|
snippetHighlighter.loadHighlights(snippetHighlights);
|
|
319
347
|
}
|
|
320
|
-
|
|
348
|
+
|
|
321
349
|
// Respond to highlight deactivate event
|
|
322
350
|
container.addEventListener('hh:highlightdeactivate', clearHighlightInfo);
|
|
323
351
|
function clearHighlightInfo(event) {
|
|
324
352
|
snippetDiv.innerHTML = '';
|
|
325
353
|
infoDiv.innerHTML = 'N/A';
|
|
326
354
|
}
|
|
327
|
-
|
|
328
|
-
// Respond to
|
|
355
|
+
|
|
356
|
+
// Respond to tap
|
|
357
|
+
container.addEventListener('hh:tap', respondToTap);
|
|
358
|
+
function respondToTap(event) {
|
|
359
|
+
if (event.detail.wrappers.length === 1) {
|
|
360
|
+
highlighter.activateHighlight(event.detail.wrappers[0].highlightInfo.highlightId);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Respond to ambiguous action events (when a user taps on overlapping highlights, wrappers, and/or links)
|
|
329
365
|
const disambiguationPanel = document.getElementById('disambiguation-panel');
|
|
330
366
|
const disambiguationButtonsDiv = document.getElementById('disambiguation-buttons');
|
|
331
367
|
const disambiguationCancelButton = document.getElementById('disambiguation-cancel');
|
|
@@ -333,7 +369,11 @@
|
|
|
333
369
|
container.addEventListener('hh:ambiguousaction', handleAmbiguousAction);
|
|
334
370
|
function handleAmbiguousAction(event) {
|
|
335
371
|
disambiguationButtonsDiv.innerHTML = '';
|
|
336
|
-
|
|
372
|
+
const highlightIds = new Set();
|
|
373
|
+
for (const highlight of event.detail.highlights) highlightIds.add(highlight.highlightId);
|
|
374
|
+
for (const wrapper of event.detail.wrappers) highlightIds.add(wrapper.highlightInfo.highlightId);
|
|
375
|
+
const highlights = highlighter.getHighlightInfo(highlightIds);
|
|
376
|
+
for (const highlight of highlights) {
|
|
337
377
|
let buttonStyle = highlighter.getOptions().styles[highlight.style]['css']
|
|
338
378
|
let buttonHtml = `
|
|
339
379
|
<button data-color="${highlight.color}" onclick="disambiguationPanel.close();highlighter.activateHighlight('${highlight.highlightId}');">
|
|
@@ -354,7 +394,20 @@
|
|
|
354
394
|
// TODO: 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:ambiguousaction. 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.
|
|
355
395
|
setTimeout(function() { disambiguationPanel.showModal(); }, 10);
|
|
356
396
|
}
|
|
357
|
-
|
|
397
|
+
|
|
398
|
+
// Update margin note position when a highlight with a start wrapper is active
|
|
399
|
+
function updateMarginNotePosition(event) {
|
|
400
|
+
const activeHighlightId = highlighter.getActiveHighlightId();
|
|
401
|
+
if (!activeHighlightId) return;
|
|
402
|
+
const note = container.querySelector(`.hh-wrapper-start[data-highlight-id="${activeHighlightId}"] .note`);
|
|
403
|
+
if (!note) return;
|
|
404
|
+
const selection = getSelection();
|
|
405
|
+
const startRect = selection.type === 'Range' ? selection.getRangeAt(0).getBoundingClientRect() : null;
|
|
406
|
+
if (startRect) note.style.top = (startRect.top + window.scrollY - 1) + 'px';
|
|
407
|
+
}
|
|
408
|
+
container.addEventListener('hh:highlightactivate', updateMarginNotePosition);
|
|
409
|
+
document.addEventListener('selectionchange', updateMarginNotePosition);
|
|
410
|
+
|
|
358
411
|
// Log events for debugging
|
|
359
412
|
container.addEventListener('hh:highlightcreate', console.info)
|
|
360
413
|
container.addEventListener('hh:highlightupdate', console.info)
|
|
@@ -365,16 +418,6 @@
|
|
|
365
418
|
container.addEventListener('hh:selectionupdate', console.info)
|
|
366
419
|
container.addEventListener('hh:ambiguousaction', console.info)
|
|
367
420
|
container.addEventListener('hh:tap', console.info)
|
|
368
|
-
|
|
369
|
-
// snippetDiv.addEventListener('hh:highlightcreate', console.warn)
|
|
370
|
-
// snippetDiv.addEventListener('hh:highlightupdate', console.warn)
|
|
371
|
-
// snippetDiv.addEventListener('hh:highlightremove', console.warn)
|
|
372
|
-
// snippetDiv.addEventListener('hh:highlightsload', console.warn)
|
|
373
|
-
// snippetDiv.addEventListener('hh:highlightactivate', console.warn)
|
|
374
|
-
// snippetDiv.addEventListener('hh:highlightdeactivate', console.warn)
|
|
375
|
-
// snippetDiv.addEventListener('hh:selectionupdate', console.warn)
|
|
376
|
-
// snippetDiv.addEventListener('hh:ambiguousaction', console.warn)
|
|
377
|
-
// snippetDiv.addEventListener('hh:tap', console.warn)
|
|
378
421
|
</script>
|
|
379
422
|
</body>
|
|
380
423
|
</html>
|
package/highlight-helper.js
CHANGED
|
@@ -57,7 +57,7 @@ function Highlighter(options = _defaultOptions) {
|
|
|
57
57
|
|
|
58
58
|
this._updateAppearanceStylesheet();
|
|
59
59
|
this._updateSelectionUi('appearance');
|
|
60
|
-
this.
|
|
60
|
+
this.setOptions({ selectionHandles: this._options.selectionHandles });
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
Highlighter.prototype._initializeHighlighter = function (previousContainerSelector = null) {
|
|
@@ -112,11 +112,13 @@ Highlighter.prototype._initializeHighlighter = function (previousContainerSelect
|
|
|
112
112
|
this._hyperlinkElements = this._annotatableContainer.getElementsByTagName('a');
|
|
113
113
|
this._hyperlinksByPosition = {}
|
|
114
114
|
for (let hyp = 0; hyp < this._hyperlinkElements.length; hyp++) {
|
|
115
|
+
const hyperlink = this._hyperlinkElements[hyp];
|
|
116
|
+
hyperlink.dataset.hhPosition = hyp;
|
|
115
117
|
this._hyperlinksByPosition[hyp] = {
|
|
116
118
|
'position': hyp,
|
|
117
|
-
'text':
|
|
118
|
-
'url':
|
|
119
|
-
'hyperlinkElement':
|
|
119
|
+
'text': hyperlink.innerHTML,
|
|
120
|
+
'url': hyperlink.href,
|
|
121
|
+
'hyperlinkElement': hyperlink,
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -315,12 +317,12 @@ Highlighter.prototype._loadEventListeners = function () {
|
|
|
315
317
|
this._tapResult.isLongPress = isLongPress;
|
|
316
318
|
this._annotatableContainer.dispatchEvent(new CustomEvent('hh:tap', { detail: this._tapResult, }));
|
|
317
319
|
if (options.autoTapToActivate && this._tapResult?.targetFound && !isLongPress) {
|
|
318
|
-
if (this._tapResult.
|
|
320
|
+
if (this._tapResult.targetCount > 1) {
|
|
321
|
+
return this._annotatableContainer.dispatchEvent(new CustomEvent('hh:ambiguousaction', { detail: this._tapResult, }));
|
|
322
|
+
} else if (this._tapResult.highlights.length === 1) {
|
|
319
323
|
return this.activateHighlight(this._tapResult.highlights[0].highlightId);
|
|
320
|
-
} else if (this._tapResult.
|
|
324
|
+
} else if (this._tapResult.hyperlinks.length === 1) {
|
|
321
325
|
return this.activateHyperlink(this._tapResult.hyperlinks[0].position);
|
|
322
|
-
} else if (this._tapResult.highlights.length + this._tapResult.hyperlinks.length > 1) {
|
|
323
|
-
return this._annotatableContainer.dispatchEvent(new CustomEvent('hh:ambiguousaction', { detail: this._tapResult, }));
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
328
|
}
|
|
@@ -446,6 +448,34 @@ Highlighter.prototype.drawHighlights = function (highlightIds = Object.keys(this
|
|
|
446
448
|
// Don't redraw a read-only highlight that's already drawn
|
|
447
449
|
if (wasDrawnAsReadOnly) continue;
|
|
448
450
|
|
|
451
|
+
// Insert wrappers before drawing, so any layout shift from wrapper content is reflected in drawing positions
|
|
452
|
+
if (highlightInfo.wrapper && (options.wrappers[highlightInfo.wrapper]?.start || options.wrappers[highlightInfo.wrapper]?.end)) {
|
|
453
|
+
const addWrapper = (edge, range, htmlString) => {
|
|
454
|
+
htmlString = `<span class="hh-wrapper-${edge}" data-highlight-id="${highlightId}" data-color="${highlightInfo.color}" data-style="${highlightInfo.style}" data-hh-ignore="">${htmlString}</span>`
|
|
455
|
+
for (const key of Object.keys(highlightInfo.wrapperVariables)) {
|
|
456
|
+
htmlString = htmlString.replaceAll(`{${key}}`, highlightInfo.wrapperVariables[key]);
|
|
457
|
+
}
|
|
458
|
+
const template = document.createElement('template');
|
|
459
|
+
template.innerHTML = htmlString;
|
|
460
|
+
let htmlElement = template.content.firstChild;
|
|
461
|
+
range.insertNode(htmlElement);
|
|
462
|
+
}
|
|
463
|
+
const startRange = highlightInfo.rangeObj;
|
|
464
|
+
const endRange = document.createRange(); endRange.setStart(highlightInfo.rangeObj.endContainer, highlightInfo.rangeObj.endOffset);
|
|
465
|
+
const wrapperInfo = options.wrappers[highlightInfo.wrapper];
|
|
466
|
+
addWrapper('start', startRange, wrapperInfo.start);
|
|
467
|
+
addWrapper('end', endRange, wrapperInfo.end);
|
|
468
|
+
rangeParagraphs.forEach(p => { p.normalize(); });
|
|
469
|
+
range = this._getCorrectedRangeObj(highlightId);
|
|
470
|
+
if (!highlightInfo.readOnly && options.drawingMode === 'svg') {
|
|
471
|
+
const startWrapperEl = this._annotatableContainer.querySelector(`.hh-wrapper-start[data-highlight-id="${highlightId}"]`);
|
|
472
|
+
const endWrapperEl = this._annotatableContainer.querySelector(`.hh-wrapper-end[data-highlight-id="${highlightId}"]`);
|
|
473
|
+
range = document.createRange();
|
|
474
|
+
range.setStartAfter(startWrapperEl);
|
|
475
|
+
range.setEndBefore(endWrapperEl);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
449
479
|
// Draw highlights with mark elements
|
|
450
480
|
if (highlightInfo.readOnly || options.drawingMode === 'mark-elements') {
|
|
451
481
|
// Inject HTML <mark> elements
|
|
@@ -478,13 +508,13 @@ Highlighter.prototype.drawHighlights = function (highlightIds = Object.keys(this
|
|
|
478
508
|
styledMark.appendChild(textNode);
|
|
479
509
|
// Absorb adjacent elements without text into the mark element. This prevents unstyled gaps around CSS-rendered pseudoelement content (such as superscript footnote markers).
|
|
480
510
|
let prev = styledMark.previousSibling;
|
|
481
|
-
while (prev && prev.nodeType === Node.ELEMENT_NODE && prev.textContent === '') {
|
|
511
|
+
while (prev && prev.nodeType === Node.ELEMENT_NODE && prev.textContent === '' && !prev.hasAttribute('data-hh-ignore')) {
|
|
482
512
|
const toAbsorb = prev; prev = prev.previousSibling;
|
|
483
513
|
styledMark.prepend(toAbsorb);
|
|
484
514
|
}
|
|
485
515
|
if (tn < relevantTextNodes.length - 1) {
|
|
486
516
|
let next = styledMark.nextSibling;
|
|
487
|
-
while (next && next.nodeType === Node.ELEMENT_NODE && next.textContent === '') {
|
|
517
|
+
while (next && next.nodeType === Node.ELEMENT_NODE && next.textContent === '' && !next.hasAttribute('data-hh-ignore')) {
|
|
488
518
|
const toAbsorb = next; next = next.nextSibling;
|
|
489
519
|
styledMark.appendChild(toAbsorb);
|
|
490
520
|
}
|
|
@@ -528,28 +558,6 @@ Highlighter.prototype.drawHighlights = function (highlightIds = Object.keys(this
|
|
|
528
558
|
group.innerHTML = svgContent;
|
|
529
559
|
this._svgBackground.appendChild(group);
|
|
530
560
|
}
|
|
531
|
-
|
|
532
|
-
// Update wrapper (for read-only highlights only)
|
|
533
|
-
if (highlightInfo.readOnly) {
|
|
534
|
-
if (highlightInfo.wrapper && (options.wrappers[highlightInfo.wrapper]?.start || options.wrappers[highlightInfo.wrapper]?.end)) {
|
|
535
|
-
const addWrapper = (edge, range, htmlString) => {
|
|
536
|
-
htmlString = `<span class="hh-wrapper-${edge}" data-highlight-id="${highlightId}" data-color="${highlightInfo.color}" data-style="${highlightInfo.style}" data-hh-ignore="">${htmlString}</span>`
|
|
537
|
-
for (const key of Object.keys(highlightInfo.wrapperVariables)) {
|
|
538
|
-
htmlString = htmlString.replaceAll(`{${key}}`, highlightInfo.wrapperVariables[key]);
|
|
539
|
-
}
|
|
540
|
-
const template = document.createElement('template');
|
|
541
|
-
template.innerHTML = htmlString;
|
|
542
|
-
let htmlElement = template.content.firstChild;
|
|
543
|
-
range.insertNode(htmlElement);
|
|
544
|
-
}
|
|
545
|
-
const startRange = highlightInfo.rangeObj;
|
|
546
|
-
const endRange = document.createRange(); endRange.setStart(highlightInfo.rangeObj.endContainer, highlightInfo.rangeObj.endOffset);
|
|
547
|
-
const wrapperInfo = options.wrappers[highlightInfo.wrapper];
|
|
548
|
-
addWrapper('start', startRange, wrapperInfo.start);
|
|
549
|
-
addWrapper('end', endRange, wrapperInfo.end);
|
|
550
|
-
rangeParagraphs.forEach(p => { p.normalize(); });
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
561
|
}
|
|
554
562
|
|
|
555
563
|
// Show container
|
|
@@ -683,14 +691,23 @@ Highlighter.prototype.createOrUpdateHighlight = function (attributes = {}, trigg
|
|
|
683
691
|
changes: appearanceChanges.concat(boundsChanges),
|
|
684
692
|
}
|
|
685
693
|
|
|
694
|
+
const wrapperChanged = appearanceChanges.some(k => k.startsWith('wrapper'));
|
|
686
695
|
if (triggeredByUserAction && !this._activeHighlightId) {
|
|
687
696
|
this.activateHighlight(highlightId);
|
|
688
697
|
} else if (this._activeHighlightId && highlightId === this._activeHighlightId && appearanceChanges.length > 0) {
|
|
689
698
|
this._updateSelectionUi('appearance');
|
|
690
|
-
}
|
|
699
|
+
}
|
|
700
|
+
if (highlightId !== this._activeHighlightId || wrapperChanged) {
|
|
691
701
|
this.drawHighlights([highlightId]);
|
|
692
702
|
}
|
|
693
703
|
|
|
704
|
+
// Redraw highlights after the current highlight if needed
|
|
705
|
+
if (wrapperChanged && options.drawingMode === 'svg') {
|
|
706
|
+
const highlights = this.getHighlightInfo();
|
|
707
|
+
const changedIndex = highlights.findIndex(h => h.highlightId === highlightId);
|
|
708
|
+
this.drawHighlights(highlights.slice(changedIndex + 1).map(h => h.highlightId));
|
|
709
|
+
}
|
|
710
|
+
|
|
694
711
|
if (isNewHighlight) {
|
|
695
712
|
this._annotatableContainer.dispatchEvent(new CustomEvent('hh:highlightcreate', { detail: detail }));
|
|
696
713
|
} else {
|
|
@@ -792,33 +809,47 @@ Highlighter.prototype.getHighlightInfo = function (highlightIds = Object.keys(th
|
|
|
792
809
|
return filteredHighlights;
|
|
793
810
|
}
|
|
794
811
|
|
|
795
|
-
// Update
|
|
796
|
-
Highlighter.prototype.
|
|
812
|
+
// Update initialized options
|
|
813
|
+
Highlighter.prototype.setOptions = function (optionsToUpdate) {
|
|
797
814
|
const options = this._options;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
this.drawHighlights();
|
|
803
|
-
} else if (key === 'colors') {
|
|
804
|
-
this._updateAppearanceStylesheet();
|
|
805
|
-
} else if (key === 'containerSelector') {
|
|
815
|
+
for (const key in optionsToUpdate) {
|
|
816
|
+
options[key] = optionsToUpdate[key] ?? options[key];
|
|
817
|
+
}
|
|
818
|
+
if ('containerSelector' in optionsToUpdate) {
|
|
806
819
|
this.removeHighlighter();
|
|
807
820
|
this._initializeHighlighter();
|
|
808
821
|
this._loadStyles();
|
|
809
822
|
this._loadEventListeners();
|
|
810
823
|
this._updateAppearanceStylesheet();
|
|
811
824
|
this._updateSelectionUi('appearance');
|
|
812
|
-
this.setOption('selectionHandles', options.selectionHandles);
|
|
813
|
-
} else if (key === 'paragraphSelector') {
|
|
814
|
-
this._initializeHighlighter();
|
|
815
|
-
} else if (key === 'selectionHandles') {
|
|
816
825
|
for (const selectionHandle of this._selectionHandles) {
|
|
817
826
|
selectionHandle.children[1].innerHTML = options.selectionHandles[selectionHandle.dataset.side] ?? '';
|
|
818
827
|
}
|
|
828
|
+
} else {
|
|
829
|
+
if ('paragraphSelector' in optionsToUpdate) {
|
|
830
|
+
this._initializeHighlighter();
|
|
831
|
+
}
|
|
832
|
+
if ('drawingMode' in optionsToUpdate || 'styles' in optionsToUpdate || 'colors' in optionsToUpdate) {
|
|
833
|
+
this._updateAppearanceStylesheet();
|
|
834
|
+
}
|
|
835
|
+
if ('drawingMode' in optionsToUpdate || 'styles' in optionsToUpdate) {
|
|
836
|
+
if (supportsHighlightApi) CSS.highlights.clear();
|
|
837
|
+
this.drawHighlights();
|
|
838
|
+
}
|
|
839
|
+
if ('selectionHandles' in optionsToUpdate) {
|
|
840
|
+
for (const selectionHandle of this._selectionHandles) {
|
|
841
|
+
selectionHandle.children[1].innerHTML = options.selectionHandles[selectionHandle.dataset.side] ?? '';
|
|
842
|
+
}
|
|
843
|
+
}
|
|
819
844
|
}
|
|
820
845
|
}
|
|
821
846
|
|
|
847
|
+
/** @deprecated Use setOptions({ key: value }) instead */
|
|
848
|
+
Highlighter.prototype.setOption = function (key, value) {
|
|
849
|
+
console.warn('setOption() is deprecated. Use setOptions({ key: value }) instead.');
|
|
850
|
+
this.setOptions({ [key]: value });
|
|
851
|
+
}
|
|
852
|
+
|
|
822
853
|
// Get all of the initialized options
|
|
823
854
|
Highlighter.prototype.getOptions = function () {
|
|
824
855
|
return this._options;
|
|
@@ -839,41 +870,51 @@ Highlighter.prototype.removeHighlighter = function () {
|
|
|
839
870
|
|
|
840
871
|
/********************** Private Methods **********************/
|
|
841
872
|
|
|
842
|
-
// Check if the tap
|
|
873
|
+
// Check if the tap hits any highlights, wrappers, or links
|
|
843
874
|
Highlighter.prototype._checkForTapTargets = function (pointerEvent) {
|
|
844
875
|
if (!pointerEvent) return;
|
|
845
876
|
|
|
846
|
-
// Check for tapped highlights
|
|
847
|
-
const
|
|
848
|
-
const tappedHyperlinks = [];
|
|
877
|
+
// Check for tapped highlights
|
|
878
|
+
const tappedHighlightIds = [];
|
|
849
879
|
if (!pointerEvent.target.closest('[data-hh-ignore]')) {
|
|
850
880
|
for (const highlightId of Object.keys(this._highlightsById)) {
|
|
851
881
|
const highlightInfo = this._highlightsById[highlightId];
|
|
852
882
|
const highlightRange = highlightInfo.rangeObj;
|
|
853
883
|
for (const rangeRect of highlightRange.getClientRects()) {
|
|
854
884
|
if (this._isPointInRect(pointerEvent.clientX, pointerEvent.clientY, rangeRect, 5)) {
|
|
855
|
-
|
|
885
|
+
tappedHighlightIds.push(highlightId);
|
|
856
886
|
break;
|
|
857
887
|
}
|
|
858
888
|
}
|
|
859
889
|
}
|
|
860
|
-
for (const hyperlinkInfo of Object.values(this._hyperlinksByPosition)) {
|
|
861
|
-
if (pointerEvent.target.closest('a') === hyperlinkInfo.hyperlinkElement) {
|
|
862
|
-
tappedHyperlinks.push(hyperlinkInfo);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
890
|
}
|
|
891
|
+
const tappedHighlights = this.getHighlightInfo(tappedHighlightIds);
|
|
866
892
|
|
|
867
|
-
//
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
const
|
|
893
|
+
// Check for tapped wrappers and hyperlinks
|
|
894
|
+
const tappedWrappers = [];
|
|
895
|
+
const tappedHyperlinks = [];
|
|
896
|
+
const targetElements = document.elementsFromPoint(pointerEvent.clientX, pointerEvent.clientY);
|
|
897
|
+
for (const element of targetElements) {
|
|
898
|
+
const wrapper = element.closest('.hh-wrapper-start, .hh-wrapper-end');
|
|
899
|
+
if (wrapper) {
|
|
900
|
+
tappedWrappers.push({
|
|
901
|
+
position: wrapper.className.includes('hh-wrapper-start') ? 'start' : 'end',
|
|
902
|
+
highlightInfo: this._highlightsById[wrapper.dataset.highlightId],
|
|
903
|
+
});
|
|
904
|
+
} else if (element.matches('a') && element.dataset.hhPosition) {
|
|
905
|
+
const hyperlinkInfo = this._hyperlinksByPosition[element.dataset.hhPosition];
|
|
906
|
+
tappedHyperlinks.push(hyperlinkInfo);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
871
909
|
|
|
910
|
+
const targetCount = tappedHighlights.length + tappedWrappers.length + tappedHyperlinks.length;
|
|
872
911
|
return {
|
|
873
|
-
'
|
|
912
|
+
'targetCount': targetCount,
|
|
913
|
+
'targetFound': targetCount > 0,
|
|
874
914
|
'tapRange': this._getCaretFromCoordinates(pointerEvent.clientX, pointerEvent.clientY),
|
|
875
915
|
'pointerEvent': pointerEvent,
|
|
876
|
-
'highlights':
|
|
916
|
+
'highlights': tappedHighlights,
|
|
917
|
+
'wrappers': tappedWrappers,
|
|
877
918
|
'hyperlinks': tappedHyperlinks,
|
|
878
919
|
}
|
|
879
920
|
}
|
|
@@ -958,6 +999,7 @@ Highlighter.prototype._updateSelectionUi = function (changeType = 'appearance')
|
|
|
958
999
|
this._svgActiveOverlay.innerHTML = '';
|
|
959
1000
|
if (this._activeHighlightId && options.drawingMode === 'svg') {
|
|
960
1001
|
let range = this._getCorrectedRangeObj(this._activeHighlightId);
|
|
1002
|
+
range = this._snapRangeToBoundaries(range);
|
|
961
1003
|
const rangeParagraphs = this._annotatableContainer.querySelectorAll(`#${highlightInfo.rangeParagraphIds.join(', #')}`);
|
|
962
1004
|
const clientRects = this._getMergedClientRects(range, rangeParagraphs);
|
|
963
1005
|
this._svgActiveOverlay.dataset.color = color;
|
|
@@ -981,7 +1023,7 @@ Highlighter.prototype._updateSelectionUi = function (changeType = 'appearance')
|
|
|
981
1023
|
if (this._activeHighlightId && options.drawingMode === 'svg') {
|
|
982
1024
|
this._selectionStylesheet.replaceSync(`
|
|
983
1025
|
${options.containerSelector} g[data-highlight-id="${this._activeHighlightId}"][data-style] { display: none; }
|
|
984
|
-
${options.containerSelector} .hh-wrapper-start[data-highlight-id="${this._activeHighlightId}"], .hh-wrapper-end[data-highlight-id="${this._activeHighlightId}"] {
|
|
1026
|
+
${options.containerSelector} .hh-wrapper-start[data-highlight-id="${this._activeHighlightId}"], .hh-wrapper-end[data-highlight-id="${this._activeHighlightId}"] { visibility: hidden; }
|
|
985
1027
|
${options.containerSelector} ::selection { background-color: transparent; }
|
|
986
1028
|
`);
|
|
987
1029
|
} else if (this._activeHighlightId) {
|
|
@@ -989,7 +1031,7 @@ Highlighter.prototype._updateSelectionUi = function (changeType = 'appearance')
|
|
|
989
1031
|
this._selectionStylesheet.replaceSync(`
|
|
990
1032
|
${options.containerSelector} ::highlight(${highlightInfo.escapedHighlightId}) { all: unset; }
|
|
991
1033
|
${options.containerSelector} mark[data-highlight-id="${this._activeHighlightId}"][data-style] { all: unset; }
|
|
992
|
-
${options.containerSelector} .hh-wrapper-start[data-highlight-id="${this._activeHighlightId}"], .hh-wrapper-end[data-highlight-id="${this._activeHighlightId}"] {
|
|
1034
|
+
${options.containerSelector} .hh-wrapper-start[data-highlight-id="${this._activeHighlightId}"], .hh-wrapper-end[data-highlight-id="${this._activeHighlightId}"] { visibility: hidden; }
|
|
993
1035
|
${options.containerSelector} ::selection { --hh-color: ${colorString}; ${styleTemplate} }
|
|
994
1036
|
${options.containerSelector} rt::selection, ${options.containerSelector} img::selection { background-color: transparent; }
|
|
995
1037
|
`);
|
|
@@ -1035,7 +1077,7 @@ Highlighter.prototype._updateSelectionUi = function (changeType = 'appearance')
|
|
|
1035
1077
|
}
|
|
1036
1078
|
if (selectionHandle.dataset.side !== side) {
|
|
1037
1079
|
selectionHandle.dataset.side = side;
|
|
1038
|
-
this.
|
|
1080
|
+
this.setOptions({ selectionHandles: options.selectionHandles });
|
|
1039
1081
|
}
|
|
1040
1082
|
}
|
|
1041
1083
|
this._setCustomSelectionHandleVisibility(true);
|
|
@@ -1076,6 +1118,17 @@ Highlighter.prototype._snapRangeToBoundaries = function (range, anchorNode = nul
|
|
|
1076
1118
|
}
|
|
1077
1119
|
}
|
|
1078
1120
|
|
|
1121
|
+
// If the range starts at the end of a text node, move it to start at the beginning of the following text node. This prevents the range from jumping across the text node boundary and selecting an extra word.
|
|
1122
|
+
if (startOffset === startNode.textContent.length) {
|
|
1123
|
+
const walker = this._getTextNodeWalker(range.commonAncestorContainer);
|
|
1124
|
+
walker.currentNode = startNode;
|
|
1125
|
+
const nextTextNode = walker.nextNode();
|
|
1126
|
+
if (nextTextNode) {
|
|
1127
|
+
startNode = nextTextNode;
|
|
1128
|
+
startOffset = 0;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1079
1132
|
// Prevent the range from starting or ending in an invalid text node
|
|
1080
1133
|
if (this._annotatableContainer.contains(range.commonAncestorContainer) && (this._shouldSkipTextNode(startNode) || this._shouldSkipTextNode(endNode))) {
|
|
1081
1134
|
if (this._shouldSkipTextNode(startNode)) {
|
|
@@ -1095,17 +1148,6 @@ Highlighter.prototype._snapRangeToBoundaries = function (range, anchorNode = nul
|
|
|
1095
1148
|
|
|
1096
1149
|
// Snap to the nearest word
|
|
1097
1150
|
if (options.snapToWord) {
|
|
1098
|
-
// If the range starts at the end of a text node, move it to start at the beginning of the following text node. This prevents the range from jumping across the text node boundary and selecting an extra word.
|
|
1099
|
-
if (startOffset === startNode.textContent.length) {
|
|
1100
|
-
const walker = this._getTextNodeWalker(range.commonAncestorContainer);
|
|
1101
|
-
walker.currentNode = startNode;
|
|
1102
|
-
const nextTextNode = walker.nextNode();
|
|
1103
|
-
if (nextTextNode) {
|
|
1104
|
-
startNode = nextTextNode;
|
|
1105
|
-
startOffset = 0;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
1151
|
// Trim whitespace and dashes at range start and end
|
|
1110
1152
|
while (_reWhitespaceDash.test(startOffset < startNode.textContent.length && startNode.textContent[startOffset])) startOffset += 1;
|
|
1111
1153
|
while (endOffset > 0 && _reWhitespaceDash.test(endNode.textContent[endOffset - 1])) endOffset -= 1;
|
package/package.json
CHANGED