@plannotator/web-highlighter 0.8.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.
Files changed (88) hide show
  1. package/.cursor/environment.json +6 -0
  2. package/.eslintrc.js +250 -0
  3. package/.prettierrc +9 -0
  4. package/.travis.yml +17 -0
  5. package/CHANGELOG.md +220 -0
  6. package/LICENSE +21 -0
  7. package/README.md +371 -0
  8. package/README.zh_CN.md +367 -0
  9. package/config/base.config.js +25 -0
  10. package/config/base.example.config.js +38 -0
  11. package/config/paths.js +22 -0
  12. package/config/server.config.js +17 -0
  13. package/config/webpack.config.dev.js +18 -0
  14. package/config/webpack.config.example.js +20 -0
  15. package/config/webpack.config.prod.js +28 -0
  16. package/dist/data/cache.d.ts +13 -0
  17. package/dist/index.d.ts +58 -0
  18. package/dist/model/range/dom.d.ts +6 -0
  19. package/dist/model/range/index.d.ts +20 -0
  20. package/dist/model/range/selection.d.ts +14 -0
  21. package/dist/model/source/dom.d.ts +23 -0
  22. package/dist/model/source/index.d.ts +18 -0
  23. package/dist/painter/dom.d.ts +22 -0
  24. package/dist/painter/index.d.ts +19 -0
  25. package/dist/painter/style.d.ts +1 -0
  26. package/dist/types/index.d.ts +102 -0
  27. package/dist/util/camel.d.ts +5 -0
  28. package/dist/util/const.d.ts +41 -0
  29. package/dist/util/deferred.d.ts +9 -0
  30. package/dist/util/dom.d.ts +32 -0
  31. package/dist/util/event.emitter.d.ts +13 -0
  32. package/dist/util/hook.d.ts +15 -0
  33. package/dist/util/interaction.d.ts +6 -0
  34. package/dist/util/is.mobile.d.ts +5 -0
  35. package/dist/util/tool.d.ts +4 -0
  36. package/dist/util/uuid.d.ts +4 -0
  37. package/dist/web-highlighter.min.js +3 -0
  38. package/dist/web-highlighter.min.js.map +1 -0
  39. package/docs/ADVANCE.md +113 -0
  40. package/docs/ADVANCE.zh_CN.md +111 -0
  41. package/docs/img/create-flow.jpg +0 -0
  42. package/docs/img/create-flow.zh_CN.jpg +0 -0
  43. package/docs/img/logo.png +0 -0
  44. package/docs/img/remove-flow.jpg +0 -0
  45. package/docs/img/remove-flow.zh_CN.jpg +0 -0
  46. package/docs/img/sample.gif +0 -0
  47. package/example/index.css +2 -0
  48. package/example/index.js +214 -0
  49. package/example/local.store.js +72 -0
  50. package/example/my.css +119 -0
  51. package/example/tpl.html +59 -0
  52. package/package.json +103 -0
  53. package/script/build.js +17 -0
  54. package/script/convet-md.js +25 -0
  55. package/script/dev.js +22 -0
  56. package/src/data/cache.ts +57 -0
  57. package/src/index.ts +285 -0
  58. package/src/model/range/dom.ts +94 -0
  59. package/src/model/range/index.ts +88 -0
  60. package/src/model/range/selection.ts +55 -0
  61. package/src/model/source/dom.ts +66 -0
  62. package/src/model/source/index.ts +54 -0
  63. package/src/painter/dom.ts +345 -0
  64. package/src/painter/index.ts +199 -0
  65. package/src/painter/style.ts +21 -0
  66. package/src/types/index.ts +118 -0
  67. package/src/util/camel.ts +6 -0
  68. package/src/util/const.ts +54 -0
  69. package/src/util/deferred.ts +37 -0
  70. package/src/util/dom.ts +155 -0
  71. package/src/util/event.emitter.ts +45 -0
  72. package/src/util/hook.ts +52 -0
  73. package/src/util/interaction.ts +20 -0
  74. package/src/util/is.mobile.ts +7 -0
  75. package/src/util/tool.ts +14 -0
  76. package/src/util/uuid.ts +10 -0
  77. package/test/api.spec.ts +555 -0
  78. package/test/event.spec.ts +284 -0
  79. package/test/fixtures/broken.json +32 -0
  80. package/test/fixtures/index.html +11 -0
  81. package/test/fixtures/source.json +47 -0
  82. package/test/hook.spec.ts +244 -0
  83. package/test/integrate.spec.ts +48 -0
  84. package/test/mobile.spec.ts +87 -0
  85. package/test/option.spec.ts +212 -0
  86. package/test/util.spec.ts +244 -0
  87. package/test-newlines.html +226 -0
  88. package/tsconfig.json +23 -0
@@ -0,0 +1,345 @@
1
+ import type HighlightRange from '@src/model/range';
2
+ import type { SelectedNode, DomNode } from '@src/types';
3
+ import { SplitType, SelectedNodeType } from '@src/types';
4
+ import { hasClass, addClass as addElementClass, isHighlightWrapNode, removeAllClass } from '@src/util/dom';
5
+ import {
6
+ ID_DIVISION,
7
+ getDefaultOptions,
8
+ CAMEL_DATASET_IDENTIFIER,
9
+ CAMEL_DATASET_IDENTIFIER_EXTRA,
10
+ DATASET_IDENTIFIER,
11
+ DATASET_SPLIT_TYPE,
12
+ DATASET_IDENTIFIER_EXTRA,
13
+ } from '../util/const';
14
+ import { unique } from '../util/tool';
15
+
16
+ /**
17
+ * 支持的选择器类型
18
+ * - class: .title, .main-nav
19
+ * - id: #nav, #js-toggle-btn
20
+ * - tag: div, p, span
21
+ */
22
+ const isMatchSelector = ($node: HTMLElement, selector: string): boolean => {
23
+ if (!$node) {
24
+ return false;
25
+ }
26
+
27
+ if (/^\./.test(selector)) {
28
+ const className = selector.replace(/^\./, '');
29
+
30
+ return $node && hasClass($node, className);
31
+ } else if (/^#/.test(selector)) {
32
+ const id = selector.replace(/^#/, '');
33
+
34
+ return $node && $node.id === id;
35
+ }
36
+
37
+ const tagName = selector.toUpperCase();
38
+
39
+ return $node && $node.tagName === tagName;
40
+ };
41
+
42
+ /**
43
+ * If start node and end node is the same, don't need to tranvers the dom tree.
44
+ */
45
+ const getNodesIfSameStartEnd = (
46
+ $startNode: Text,
47
+ startOffset: number,
48
+ endOffset: number,
49
+ exceptSelectors?: string[],
50
+ ) => {
51
+ let $element = $startNode as Node;
52
+
53
+ const isExcepted = ($e: HTMLElement) => exceptSelectors?.some(s => isMatchSelector($e, s));
54
+
55
+ while ($element) {
56
+ if ($element.nodeType === 1 && isExcepted($element as HTMLElement)) {
57
+ return [];
58
+ }
59
+
60
+ $element = $element.parentNode;
61
+ }
62
+
63
+ $startNode.splitText(startOffset);
64
+
65
+ const passedNode = $startNode.nextSibling as Text;
66
+
67
+ passedNode.splitText(endOffset - startOffset);
68
+
69
+ return [
70
+ {
71
+ $node: passedNode,
72
+ type: SelectedNodeType.text,
73
+ splitType: SplitType.both,
74
+ },
75
+ ];
76
+ };
77
+
78
+ /**
79
+ * get all the dom nodes between the start and end node
80
+ */
81
+ export const getSelectedNodes = (
82
+ $root: Document | HTMLElement,
83
+ start: DomNode,
84
+ end: DomNode,
85
+ exceptSelectors: string[],
86
+ ): SelectedNode[] => {
87
+ const $startNode = start.$node;
88
+ const $endNode = end.$node;
89
+ const startOffset = start.offset;
90
+ const endOffset = end.offset;
91
+
92
+ // split current node when the start-node and end-node is the same
93
+ if ($startNode === $endNode && $startNode instanceof Text) {
94
+ return getNodesIfSameStartEnd($startNode, startOffset, endOffset, exceptSelectors);
95
+ }
96
+
97
+ const nodeStack: (ChildNode | Document | HTMLElement | Text)[] = [$root];
98
+ const selectedNodes: SelectedNode[] = [];
99
+
100
+ const isExcepted = ($e: HTMLElement) => exceptSelectors?.some(s => isMatchSelector($e, s));
101
+
102
+ let withinSelectedRange = false;
103
+ let curNode: Node = null;
104
+
105
+ while ((curNode = nodeStack.pop())) {
106
+ // do not traverse the excepted node
107
+ if (curNode.nodeType === 1 && isExcepted(curNode as HTMLElement)) {
108
+ continue;
109
+ }
110
+
111
+ const children = curNode.childNodes;
112
+
113
+ for (let i = children.length - 1; i >= 0; i--) {
114
+ nodeStack.push(children[i]);
115
+ }
116
+
117
+ // only collect text nodes
118
+ if (curNode === $startNode) {
119
+ if (curNode.nodeType === 3) {
120
+ (curNode as Text).splitText(startOffset);
121
+
122
+ const node = curNode.nextSibling as Text;
123
+
124
+ selectedNodes.push({
125
+ $node: node,
126
+ type: SelectedNodeType.text,
127
+ splitType: SplitType.head,
128
+ });
129
+ }
130
+
131
+ // meet the start-node (begin to traverse)
132
+ withinSelectedRange = true;
133
+ } else if (curNode === $endNode) {
134
+ if (curNode.nodeType === 3) {
135
+ const node = curNode as Text;
136
+
137
+ node.splitText(endOffset);
138
+ selectedNodes.push({
139
+ $node: node,
140
+ type: SelectedNodeType.text,
141
+ splitType: SplitType.tail,
142
+ });
143
+ }
144
+
145
+ // meet the end-node
146
+ break;
147
+ }
148
+ // handle text nodes between the range
149
+ else if (withinSelectedRange && curNode.nodeType === 3) {
150
+ selectedNodes.push({
151
+ $node: curNode as Text,
152
+ type: SelectedNodeType.text,
153
+ splitType: SplitType.none,
154
+ });
155
+ }
156
+ }
157
+
158
+ return selectedNodes;
159
+ };
160
+
161
+ const addClass = ($el: HTMLElement, className?: string[] | string): HTMLElement => {
162
+ let classNames = Array.isArray(className) ? className : [className];
163
+
164
+ classNames = classNames.length === 0 ? [getDefaultOptions().style.className] : classNames;
165
+ classNames.forEach(c => {
166
+ addElementClass($el, c);
167
+ });
168
+
169
+ return $el;
170
+ };
171
+
172
+ const isNodeEmpty = ($n: Node): boolean => !$n || !$n.textContent;
173
+
174
+ /**
175
+ * Wrap a common wrapper.
176
+ */
177
+ const wrapNewNode = (
178
+ selected: SelectedNode,
179
+ range: HighlightRange,
180
+ className: string[] | string,
181
+ wrapTag: string,
182
+ ): HTMLElement => {
183
+ const $wrap = document.createElement(wrapTag);
184
+
185
+ addClass($wrap, className);
186
+
187
+ $wrap.appendChild(selected.$node.cloneNode(false));
188
+ selected.$node.parentNode.replaceChild($wrap, selected.$node);
189
+
190
+ $wrap.setAttribute(`data-${DATASET_IDENTIFIER}`, range.id);
191
+ $wrap.setAttribute(`data-${DATASET_SPLIT_TYPE}`, selected.splitType);
192
+ $wrap.setAttribute(`data-${DATASET_IDENTIFIER_EXTRA}`, '');
193
+
194
+ return $wrap;
195
+ };
196
+
197
+ /**
198
+ * Split and wrapper each one.
199
+ */
200
+ const wrapPartialNode = (
201
+ selected: SelectedNode,
202
+ range: HighlightRange,
203
+ className: string[] | string,
204
+ wrapTag: string,
205
+ ): HTMLElement => {
206
+ const $wrap: HTMLElement = document.createElement(wrapTag);
207
+
208
+ const $parent = selected.$node.parentNode as HTMLElement;
209
+ const $prev = selected.$node.previousSibling;
210
+ const $next = selected.$node.nextSibling;
211
+ const $fr = document.createDocumentFragment();
212
+ const parentId = $parent.dataset[CAMEL_DATASET_IDENTIFIER];
213
+ const parentExtraId = $parent.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA];
214
+ const extraInfo = parentExtraId ? parentId + ID_DIVISION + parentExtraId : parentId;
215
+
216
+ $wrap.setAttribute(`data-${DATASET_IDENTIFIER}`, range.id);
217
+ $wrap.setAttribute(`data-${DATASET_IDENTIFIER_EXTRA}`, extraInfo);
218
+ $wrap.appendChild(selected.$node.cloneNode(false));
219
+
220
+ let headSplit = false;
221
+ let tailSplit = false;
222
+ let splitType: SplitType;
223
+
224
+ if ($prev) {
225
+ const $span = $parent.cloneNode(false);
226
+
227
+ $span.textContent = $prev.textContent;
228
+ $fr.appendChild($span);
229
+ headSplit = true;
230
+ }
231
+
232
+ const classNameList: string[] = [];
233
+
234
+ if (Array.isArray(className)) {
235
+ classNameList.push(...className);
236
+ } else {
237
+ classNameList.push(className);
238
+ }
239
+
240
+ addClass($wrap, unique(classNameList));
241
+ $fr.appendChild($wrap);
242
+
243
+ if ($next) {
244
+ const $span = $parent.cloneNode(false);
245
+
246
+ $span.textContent = $next.textContent;
247
+ $fr.appendChild($span);
248
+ tailSplit = true;
249
+ }
250
+
251
+ if (headSplit && tailSplit) {
252
+ splitType = SplitType.both;
253
+ } else if (headSplit) {
254
+ splitType = SplitType.head;
255
+ } else if (tailSplit) {
256
+ splitType = SplitType.tail;
257
+ } else {
258
+ splitType = SplitType.none;
259
+ }
260
+
261
+ $wrap.setAttribute(`data-${DATASET_SPLIT_TYPE}`, splitType);
262
+ $parent.parentNode.replaceChild($fr, $parent);
263
+
264
+ return $wrap;
265
+ };
266
+
267
+ /**
268
+ * Just update id info (no wrapper updated).
269
+ */
270
+ const wrapOverlapNode = (selected: SelectedNode, range: HighlightRange, className: string[] | string): HTMLElement => {
271
+ const $parent = selected.$node.parentNode as HTMLElement;
272
+ const $wrap: HTMLElement = $parent;
273
+
274
+ removeAllClass($wrap);
275
+ addClass($wrap, className);
276
+
277
+ const dataset = $parent.dataset;
278
+ const formerId = dataset[CAMEL_DATASET_IDENTIFIER];
279
+
280
+ dataset[CAMEL_DATASET_IDENTIFIER] = range.id;
281
+ dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] = dataset[CAMEL_DATASET_IDENTIFIER_EXTRA]
282
+ ? formerId + ID_DIVISION + dataset[CAMEL_DATASET_IDENTIFIER_EXTRA]
283
+ : formerId;
284
+
285
+ return $wrap;
286
+ };
287
+
288
+ /**
289
+ * wrap a dom node with highlight wrapper
290
+ *
291
+ * Because of supporting the highlight-overlapping,
292
+ * Highlighter can't just wrap all nodes in a simple way.
293
+ * There are three types:
294
+ * - wrapping a whole new node (without any wrapper)
295
+ * - wrapping part of the node
296
+ * - wrapping the whole wrapped node
297
+ */
298
+ export const wrapHighlight = (
299
+ selected: SelectedNode,
300
+ range: HighlightRange,
301
+ className: string[] | string,
302
+ wrapTag: string,
303
+ ): HTMLElement => {
304
+ const $parent = selected.$node.parentNode as HTMLElement;
305
+ const $prev = selected.$node.previousSibling;
306
+ const $next = selected.$node.nextSibling;
307
+
308
+ let $wrap: HTMLElement;
309
+
310
+ // text node, not in a highlight wrapper -> should be wrapped in a highlight wrapper
311
+ if (!isHighlightWrapNode($parent)) {
312
+ $wrap = wrapNewNode(selected, range, className, wrapTag);
313
+ }
314
+ // text node, in a highlight wrap -> should split the existing highlight wrapper
315
+ else if (isHighlightWrapNode($parent) && (!isNodeEmpty($prev) || !isNodeEmpty($next))) {
316
+ $wrap = wrapPartialNode(selected, range, className, wrapTag);
317
+ }
318
+ // completely overlap (with a highlight wrap) -> only add extra id info
319
+ else {
320
+ $wrap = wrapOverlapNode(selected, range, className);
321
+ }
322
+
323
+ return $wrap;
324
+ };
325
+
326
+ /**
327
+ * merge the adjacent text nodes
328
+ * .normalize() API has some bugs in IE11
329
+ */
330
+ export const normalizeSiblingText = ($s: Node, isNext = true) => {
331
+ if (!$s || $s.nodeType !== 3) {
332
+ return;
333
+ }
334
+
335
+ const $sibling = isNext ? $s.nextSibling : $s.previousSibling;
336
+
337
+ if ($sibling.nodeType !== 3) {
338
+ return;
339
+ }
340
+
341
+ const text = $sibling.nodeValue;
342
+
343
+ $s.nodeValue = isNext ? $s.nodeValue + text : text + $s.nodeValue;
344
+ $sibling.parentNode.removeChild($sibling);
345
+ };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Painter object is designed for some painting work about higlighting,
3
+ * including rendering, cleaning...
4
+ * No need to instantiate repeatly. A Highlighter instance will bind a Painter instance.
5
+ */
6
+
7
+ import type HighlightRange from '@src/model/range';
8
+ import type { PainterOptions, HookMap } from '@src/types';
9
+ import HighlightSource from '@src/model/source';
10
+ import { wrapHighlight, getSelectedNodes, normalizeSiblingText } from '@src/painter/dom';
11
+ import { getHighlightsByRoot, forEach, addClass, removeAllClass } from '@src/util/dom';
12
+ import { ERROR } from '@src/types';
13
+ import { initDefaultStylesheet } from '@src/painter/style';
14
+ import {
15
+ ID_DIVISION,
16
+ eventEmitter,
17
+ DATASET_IDENTIFIER,
18
+ INTERNAL_ERROR_EVENT,
19
+ CAMEL_DATASET_IDENTIFIER,
20
+ CAMEL_DATASET_IDENTIFIER_EXTRA,
21
+ } from '@src/util/const';
22
+
23
+ export default class Painter {
24
+ options: PainterOptions;
25
+
26
+ $style: HTMLStyleElement;
27
+
28
+ styleId: string;
29
+
30
+ hooks: HookMap;
31
+
32
+ constructor(options: PainterOptions, hooks: HookMap) {
33
+ this.options = {
34
+ $root: options.$root,
35
+ wrapTag: options.wrapTag,
36
+ exceptSelectors: options.exceptSelectors,
37
+ className: options.className,
38
+ };
39
+ this.hooks = hooks;
40
+
41
+ initDefaultStylesheet();
42
+ }
43
+
44
+ /* =========================== render =========================== */
45
+ highlightRange(range: HighlightRange): HTMLElement[] {
46
+ if (!range.frozen) {
47
+ throw ERROR.HIGHLIGHT_RANGE_FROZEN;
48
+ }
49
+
50
+ const { $root, className, exceptSelectors } = this.options;
51
+ const hooks = this.hooks;
52
+
53
+ let $selectedNodes = getSelectedNodes($root, range.start, range.end, exceptSelectors);
54
+
55
+ if (!hooks.Render.SelectedNodes.isEmpty()) {
56
+ $selectedNodes = hooks.Render.SelectedNodes.call(range.id, $selectedNodes) || [];
57
+ }
58
+
59
+ return $selectedNodes.map(n => {
60
+ let $node = wrapHighlight(n, range, className, this.options.wrapTag);
61
+
62
+ if (!hooks.Render.WrapNode.isEmpty()) {
63
+ $node = hooks.Render.WrapNode.call(range.id, $node);
64
+ }
65
+
66
+ return $node;
67
+ });
68
+ }
69
+
70
+ highlightSource(sources: HighlightSource | HighlightSource[]): HighlightSource[] {
71
+ const list = Array.isArray(sources) ? sources : [sources];
72
+
73
+ const renderedSources: HighlightSource[] = [];
74
+
75
+ list.forEach(s => {
76
+ if (!(s instanceof HighlightSource)) {
77
+ eventEmitter.emit(INTERNAL_ERROR_EVENT, {
78
+ type: ERROR.SOURCE_TYPE_ERROR,
79
+ });
80
+
81
+ return;
82
+ }
83
+
84
+ const range = s.deSerialize(this.options.$root, this.hooks);
85
+ const $nodes = this.highlightRange(range);
86
+
87
+ if ($nodes.length > 0) {
88
+ renderedSources.push(s);
89
+ } else {
90
+ eventEmitter.emit(INTERNAL_ERROR_EVENT, {
91
+ type: ERROR.HIGHLIGHT_SOURCE_NONE_RENDER,
92
+ detail: s,
93
+ });
94
+ }
95
+ });
96
+
97
+ return renderedSources;
98
+ }
99
+ /* ============================================================== */
100
+
101
+ /* =========================== clean =========================== */
102
+ // id: target id - highlight with this id should be clean
103
+ // if there is no highlight for this id, it will return false, vice versa
104
+ removeHighlight(id: string): boolean {
105
+ // whether extra ids contains the target id
106
+ const reg = new RegExp(`(${id}\\${ID_DIVISION}|\\${ID_DIVISION}?${id}$)`);
107
+
108
+ const hooks = this.hooks;
109
+ const wrapTag = this.options.wrapTag;
110
+ const $spans = document.querySelectorAll<HTMLElement>(`${wrapTag}[data-${DATASET_IDENTIFIER}]`);
111
+
112
+ // nodes to remove
113
+ const $toRemove: HTMLElement[] = [];
114
+ // nodes to update main id
115
+ const $idToUpdate: HTMLElement[] = [];
116
+ // nodes to update extra id
117
+ const $extraToUpdate: HTMLElement[] = [];
118
+
119
+ for (const $s of $spans) {
120
+ const spanId = $s.dataset[CAMEL_DATASET_IDENTIFIER];
121
+ const spanExtraIds = $s.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA];
122
+
123
+ // main id is the target id and no extra ids --> to remove
124
+ if (spanId === id && !spanExtraIds) {
125
+ $toRemove.push($s);
126
+ }
127
+ // main id is the target id but there is some extra ids -> update main id & extra id
128
+ else if (spanId === id) {
129
+ $idToUpdate.push($s);
130
+ }
131
+ // main id isn't the target id but extra ids contains it -> just remove it from extra id
132
+ else if (spanId !== id && reg.test(spanExtraIds)) {
133
+ $extraToUpdate.push($s);
134
+ }
135
+ }
136
+
137
+ $toRemove.forEach($s => {
138
+ const $parent = $s.parentNode;
139
+ const $fr = document.createDocumentFragment();
140
+
141
+ forEach($s.childNodes, ($c: Node) => $fr.appendChild($c.cloneNode(false)));
142
+
143
+ const $prev = $s.previousSibling;
144
+ const $next = $s.nextSibling;
145
+
146
+ $parent.replaceChild($fr, $s);
147
+ // there are bugs in IE11, so use a more reliable function
148
+ normalizeSiblingText($prev, true);
149
+ normalizeSiblingText($next, false);
150
+ hooks.Remove.UpdateNodes.call(id, $s, 'remove');
151
+ });
152
+
153
+ $idToUpdate.forEach($s => {
154
+ const dataset = $s.dataset;
155
+ const ids = dataset[CAMEL_DATASET_IDENTIFIER_EXTRA].split(ID_DIVISION);
156
+ const newId = ids.shift();
157
+
158
+ // find overlapped wrapper associated with "extra id"
159
+ const $overlapSpan = document.querySelector<HTMLElement>(
160
+ `${wrapTag}[data-${DATASET_IDENTIFIER}="${newId}"]`,
161
+ );
162
+
163
+ if ($overlapSpan) {
164
+ // empty the current class list
165
+ removeAllClass($s);
166
+ // retain the class list of the overlapped wrapper which associated with "extra id"
167
+ addClass($s, [...$overlapSpan.classList]);
168
+ }
169
+
170
+ dataset[CAMEL_DATASET_IDENTIFIER] = newId;
171
+ dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] = ids.join(ID_DIVISION);
172
+
173
+ hooks.Remove.UpdateNodes.call(id, $s, 'id-update');
174
+ });
175
+
176
+ $extraToUpdate.forEach($s => {
177
+ const extraIds = $s.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA];
178
+
179
+ $s.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] = extraIds.replace(reg, '');
180
+ hooks.Remove.UpdateNodes.call(id, $s, 'extra-update');
181
+ });
182
+
183
+ return $toRemove.length + $idToUpdate.length + $extraToUpdate.length !== 0;
184
+ }
185
+
186
+ removeAllHighlight() {
187
+ const { wrapTag, $root } = this.options;
188
+ const $spans = getHighlightsByRoot($root, wrapTag);
189
+
190
+ $spans.forEach($s => {
191
+ const $parent = $s.parentNode;
192
+ const $fr = document.createDocumentFragment();
193
+
194
+ forEach($s.childNodes, ($c: Node) => $fr.appendChild($c.cloneNode(false)));
195
+ $parent.replaceChild($fr, $s);
196
+ });
197
+ }
198
+ /* ============================================================== */
199
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * inject styles
3
+ */
4
+ import { STYLESHEET_ID, getStylesheet } from '@src/util/const';
5
+
6
+ export const initDefaultStylesheet = () => {
7
+ const styleId = STYLESHEET_ID;
8
+
9
+ let $style: HTMLStyleElement = document.getElementById(styleId) as HTMLStyleElement;
10
+
11
+ if (!$style) {
12
+ const $cssNode = document.createTextNode(getStylesheet());
13
+
14
+ $style = document.createElement('style');
15
+ $style.id = styleId;
16
+ $style.appendChild($cssNode);
17
+ document.head.appendChild($style);
18
+ }
19
+
20
+ return $style;
21
+ };
@@ -0,0 +1,118 @@
1
+ import type Hook from '@src/util/hook';
2
+
3
+ export type RootElement = Document | HTMLElement;
4
+
5
+ export interface HighlighterOptions {
6
+ $root?: RootElement;
7
+ exceptSelectors?: string[];
8
+ wrapTag?: string;
9
+ verbose?: boolean;
10
+ style?: {
11
+ className?: string[] | string;
12
+ };
13
+ }
14
+
15
+ export interface PainterOptions {
16
+ $root: RootElement;
17
+ wrapTag: string;
18
+ className: string[] | string;
19
+ exceptSelectors: string[];
20
+ }
21
+
22
+ export enum SplitType {
23
+ none = 'none',
24
+ head = 'head',
25
+ tail = 'tail',
26
+ both = 'both',
27
+ }
28
+
29
+ export enum ERROR {
30
+ DOM_TYPE_ERROR = '[DOM] Receive wrong node type.',
31
+ DOM_SELECTION_EMPTY = '[DOM] The selection contains no dom node, may be you except them.',
32
+ RANGE_INVALID = "[RANGE] Got invalid dom range, can't convert to a valid highlight range.",
33
+ RANGE_NODE_INVALID = "[RANGE] Start or end node isn't a text node, it may occur an error.",
34
+ DB_ID_DUPLICATE_ERROR = '[STORE] Unique id conflict.',
35
+ CACHE_SET_ERROR = "[CACHE] Cache.data can't be set manually, please use .save().",
36
+ SOURCE_TYPE_ERROR = "[SOURCE] Object isn't a highlight source instance.",
37
+ HIGHLIGHT_RANGE_FROZEN = '[HIGHLIGHT_RANGE] A highlight range must be frozen before render.',
38
+ HIGHLIGHT_SOURCE_RECREATE = '[HIGHLIGHT_SOURCE] Recreate highlights from sources error.',
39
+ // eslint-disable-next-line max-len
40
+ HIGHLIGHT_SOURCE_NONE_RENDER = "[HIGHLIGHT_SOURCE] This highlight source isn't rendered. May be the exception skips it or the dom structure has changed.",
41
+ }
42
+
43
+ export enum EventType {
44
+ CREATE = 'selection:create',
45
+ REMOVE = 'selection:remove',
46
+ MODIFY = 'selection:modify',
47
+ HOVER = 'selection:hover',
48
+ HOVER_OUT = 'selection:hover-out',
49
+ CLICK = 'selection:click',
50
+ }
51
+
52
+ export enum CreateFrom {
53
+ STORE = 'from-store',
54
+ INPUT = 'from-input',
55
+ }
56
+
57
+ export enum SelectedNodeType {
58
+ text = 'text',
59
+ span = 'span',
60
+ }
61
+
62
+ export interface SelectedNode {
63
+ $node: Node | Text;
64
+ type: SelectedNodeType;
65
+ splitType: SplitType;
66
+ }
67
+
68
+ export interface DomMeta {
69
+ parentTagName: string;
70
+ parentIndex: number;
71
+ textOffset: number;
72
+ extra?: unknown;
73
+ }
74
+
75
+ export interface DomNode {
76
+ $node: Node;
77
+ offset: number;
78
+ }
79
+
80
+ export interface HighlightPosition {
81
+ start: {
82
+ top: number;
83
+ left: number;
84
+ };
85
+ end: {
86
+ top: number;
87
+ left: number;
88
+ };
89
+ }
90
+
91
+ export interface HookMap {
92
+ Render: {
93
+ UUID: Hook<string>;
94
+ SelectedNodes: Hook<SelectedNode[]>;
95
+ WrapNode: Hook<HTMLElement>;
96
+ };
97
+ Serialize: {
98
+ Restore: Hook<DomNode[]>;
99
+ RecordInfo: Hook<string>;
100
+ };
101
+ Remove: {
102
+ UpdateNodes: Hook;
103
+ };
104
+ }
105
+
106
+ export enum UserInputEvent {
107
+ touchend = 'touchend',
108
+ mouseup = 'mouseup',
109
+ touchstart = 'touchstart',
110
+ click = 'click',
111
+ mouseover = 'mouseover',
112
+ }
113
+
114
+ export interface IInteraction {
115
+ PointerEnd: UserInputEvent;
116
+ PointerTap: UserInputEvent;
117
+ PointerOver: UserInputEvent;
118
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * convert dash-joined string to camel case
3
+ */
4
+
5
+ export default (a: string): string =>
6
+ a.split('-').reduce((str, s, idx) => str + (idx === 0 ? s : s[0].toUpperCase() + s.slice(1)), '');