@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.
- package/.cursor/environment.json +6 -0
- package/.eslintrc.js +250 -0
- package/.prettierrc +9 -0
- package/.travis.yml +17 -0
- package/CHANGELOG.md +220 -0
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/README.zh_CN.md +367 -0
- package/config/base.config.js +25 -0
- package/config/base.example.config.js +38 -0
- package/config/paths.js +22 -0
- package/config/server.config.js +17 -0
- package/config/webpack.config.dev.js +18 -0
- package/config/webpack.config.example.js +20 -0
- package/config/webpack.config.prod.js +28 -0
- package/dist/data/cache.d.ts +13 -0
- package/dist/index.d.ts +58 -0
- package/dist/model/range/dom.d.ts +6 -0
- package/dist/model/range/index.d.ts +20 -0
- package/dist/model/range/selection.d.ts +14 -0
- package/dist/model/source/dom.d.ts +23 -0
- package/dist/model/source/index.d.ts +18 -0
- package/dist/painter/dom.d.ts +22 -0
- package/dist/painter/index.d.ts +19 -0
- package/dist/painter/style.d.ts +1 -0
- package/dist/types/index.d.ts +102 -0
- package/dist/util/camel.d.ts +5 -0
- package/dist/util/const.d.ts +41 -0
- package/dist/util/deferred.d.ts +9 -0
- package/dist/util/dom.d.ts +32 -0
- package/dist/util/event.emitter.d.ts +13 -0
- package/dist/util/hook.d.ts +15 -0
- package/dist/util/interaction.d.ts +6 -0
- package/dist/util/is.mobile.d.ts +5 -0
- package/dist/util/tool.d.ts +4 -0
- package/dist/util/uuid.d.ts +4 -0
- package/dist/web-highlighter.min.js +3 -0
- package/dist/web-highlighter.min.js.map +1 -0
- package/docs/ADVANCE.md +113 -0
- package/docs/ADVANCE.zh_CN.md +111 -0
- package/docs/img/create-flow.jpg +0 -0
- package/docs/img/create-flow.zh_CN.jpg +0 -0
- package/docs/img/logo.png +0 -0
- package/docs/img/remove-flow.jpg +0 -0
- package/docs/img/remove-flow.zh_CN.jpg +0 -0
- package/docs/img/sample.gif +0 -0
- package/example/index.css +2 -0
- package/example/index.js +214 -0
- package/example/local.store.js +72 -0
- package/example/my.css +119 -0
- package/example/tpl.html +59 -0
- package/package.json +103 -0
- package/script/build.js +17 -0
- package/script/convet-md.js +25 -0
- package/script/dev.js +22 -0
- package/src/data/cache.ts +57 -0
- package/src/index.ts +285 -0
- package/src/model/range/dom.ts +94 -0
- package/src/model/range/index.ts +88 -0
- package/src/model/range/selection.ts +55 -0
- package/src/model/source/dom.ts +66 -0
- package/src/model/source/index.ts +54 -0
- package/src/painter/dom.ts +345 -0
- package/src/painter/index.ts +199 -0
- package/src/painter/style.ts +21 -0
- package/src/types/index.ts +118 -0
- package/src/util/camel.ts +6 -0
- package/src/util/const.ts +54 -0
- package/src/util/deferred.ts +37 -0
- package/src/util/dom.ts +155 -0
- package/src/util/event.emitter.ts +45 -0
- package/src/util/hook.ts +52 -0
- package/src/util/interaction.ts +20 -0
- package/src/util/is.mobile.ts +7 -0
- package/src/util/tool.ts +14 -0
- package/src/util/uuid.ts +10 -0
- package/test/api.spec.ts +555 -0
- package/test/event.spec.ts +284 -0
- package/test/fixtures/broken.json +32 -0
- package/test/fixtures/index.html +11 -0
- package/test/fixtures/source.json +47 -0
- package/test/hook.spec.ts +244 -0
- package/test/integrate.spec.ts +48 -0
- package/test/mobile.spec.ts +87 -0
- package/test/option.spec.ts +212 -0
- package/test/util.spec.ts +244 -0
- package/test-newlines.html +226 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* all constants
|
|
3
|
+
* cSpell:ignore mengshou
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type HighlightSource from '@src/model/source';
|
|
7
|
+
import type { ERROR } from '@src/types';
|
|
8
|
+
import camel from '@src/util/camel';
|
|
9
|
+
import EventEmitter from '@src/util/event.emitter';
|
|
10
|
+
|
|
11
|
+
export const ID_DIVISION = ';';
|
|
12
|
+
export const LOCAL_STORE_KEY = 'highlight-mengshou';
|
|
13
|
+
export const STYLESHEET_ID = 'highlight-mengshou-style';
|
|
14
|
+
|
|
15
|
+
export const DATASET_IDENTIFIER = 'highlight-id';
|
|
16
|
+
export const DATASET_IDENTIFIER_EXTRA = 'highlight-id-extra';
|
|
17
|
+
export const DATASET_SPLIT_TYPE = 'highlight-split-type';
|
|
18
|
+
export const CAMEL_DATASET_IDENTIFIER = camel(DATASET_IDENTIFIER);
|
|
19
|
+
export const CAMEL_DATASET_IDENTIFIER_EXTRA = camel(DATASET_IDENTIFIER_EXTRA);
|
|
20
|
+
export const CAMEL_DATASET_SPLIT_TYPE = camel(DATASET_SPLIT_TYPE);
|
|
21
|
+
|
|
22
|
+
const DEFAULT_WRAP_TAG = 'span';
|
|
23
|
+
|
|
24
|
+
export const getDefaultOptions = () => ({
|
|
25
|
+
$root: document || document.documentElement,
|
|
26
|
+
exceptSelectors: null,
|
|
27
|
+
wrapTag: DEFAULT_WRAP_TAG,
|
|
28
|
+
verbose: false,
|
|
29
|
+
style: {
|
|
30
|
+
className: 'highlight-mengshou-wrap',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const getStylesheet = () => `
|
|
35
|
+
.${getDefaultOptions().style.className} {
|
|
36
|
+
background: #ff9;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
.${getDefaultOptions().style.className}.active {
|
|
40
|
+
background: #ffb;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
export const ROOT_IDX = -2;
|
|
45
|
+
export const UNKNOWN_IDX = -1;
|
|
46
|
+
export const INTERNAL_ERROR_EVENT = 'error';
|
|
47
|
+
|
|
48
|
+
interface EventHandlerMap {
|
|
49
|
+
[key: string]: (...args: any[]) => void;
|
|
50
|
+
error: (data: { type: ERROR; detail?: HighlightSource; error?: any }) => void;
|
|
51
|
+
}
|
|
52
|
+
class ErrorEventEmitter extends EventEmitter<EventHandlerMap> {}
|
|
53
|
+
|
|
54
|
+
export const eventEmitter = new ErrorEventEmitter();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
interface Deferred<T> {
|
|
2
|
+
promise: Promise<T>;
|
|
3
|
+
resolve: (args: T) => unknown;
|
|
4
|
+
reject: (e?: unknown) => unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function getDeferred<T>(): Deferred<T> {
|
|
8
|
+
let resolve: (args: T) => unknown;
|
|
9
|
+
let reject: (e?: unknown) => unknown;
|
|
10
|
+
|
|
11
|
+
const promise = new Promise<T>((r, j) => {
|
|
12
|
+
resolve = r;
|
|
13
|
+
reject = j;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
promise,
|
|
18
|
+
resolve,
|
|
19
|
+
reject,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const resolve = <T>(data: T) => {
|
|
24
|
+
const defer = getDeferred<T>();
|
|
25
|
+
|
|
26
|
+
defer.resolve(data);
|
|
27
|
+
|
|
28
|
+
return defer.promise;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const reject = <T>(data: T) => {
|
|
32
|
+
const defer = getDeferred<T>();
|
|
33
|
+
|
|
34
|
+
defer.reject(data);
|
|
35
|
+
|
|
36
|
+
return defer.promise;
|
|
37
|
+
};
|
package/src/util/dom.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { RootElement } from '@src/types';
|
|
2
|
+
import {
|
|
3
|
+
ID_DIVISION,
|
|
4
|
+
DATASET_IDENTIFIER,
|
|
5
|
+
CAMEL_DATASET_IDENTIFIER,
|
|
6
|
+
CAMEL_DATASET_IDENTIFIER_EXTRA,
|
|
7
|
+
} from '@src/util/const';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* whether a wrapper node
|
|
11
|
+
*/
|
|
12
|
+
export const isHighlightWrapNode = ($node: HTMLElement): boolean =>
|
|
13
|
+
!!$node.dataset && !!$node.dataset[CAMEL_DATASET_IDENTIFIER];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ===================================================================================
|
|
17
|
+
* below methods (getHighlightId/getExtraHighlightId)
|
|
18
|
+
* will check whether the node is inside a wrapper iteratively util reach the root node
|
|
19
|
+
* if the node is not inside the root, the id must be empty
|
|
20
|
+
* ====================================================================================
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const findAncestorWrapperInRoot = ($node: HTMLElement, $root: RootElement): HTMLElement => {
|
|
24
|
+
let isInsideRoot = false;
|
|
25
|
+
let $wrapper: HTMLElement = null;
|
|
26
|
+
|
|
27
|
+
while ($node) {
|
|
28
|
+
if (isHighlightWrapNode($node)) {
|
|
29
|
+
$wrapper = $node;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if ($node === $root) {
|
|
33
|
+
isInsideRoot = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
$node = $node.parentNode as HTMLElement;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return isInsideRoot ? $wrapper : null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* get highlight id by a node
|
|
45
|
+
*/
|
|
46
|
+
export const getHighlightId = ($node: HTMLElement, $root: RootElement): string => {
|
|
47
|
+
$node = findAncestorWrapperInRoot($node, $root);
|
|
48
|
+
|
|
49
|
+
if (!$node) {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return $node.dataset[CAMEL_DATASET_IDENTIFIER];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* get extra highlight id by a node
|
|
58
|
+
*/
|
|
59
|
+
export const getExtraHighlightId = ($node: HTMLElement, $root: RootElement): string[] => {
|
|
60
|
+
$node = findAncestorWrapperInRoot($node, $root);
|
|
61
|
+
|
|
62
|
+
if (!$node) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return $node.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA].split(ID_DIVISION).filter(i => i);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* get all highlight wrapping nodes nodes from a root node
|
|
71
|
+
*/
|
|
72
|
+
export const getHighlightsByRoot = ($roots: RootElement | RootElement[], wrapTag: string): HTMLElement[] => {
|
|
73
|
+
if (!Array.isArray($roots)) {
|
|
74
|
+
$roots = [$roots];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const $wraps: HTMLElement[] = [];
|
|
78
|
+
|
|
79
|
+
for (const $r of $roots) {
|
|
80
|
+
const $list = $r.querySelectorAll<HTMLElement>(`${wrapTag}[data-${DATASET_IDENTIFIER}]`);
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line prefer-spread
|
|
83
|
+
$wraps.push.apply($wraps, $list);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return $wraps;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* get all highlight wrapping nodes by highlight id from a root node
|
|
91
|
+
*/
|
|
92
|
+
export const getHighlightById = ($root: RootElement, id: string, wrapTag: string): HTMLElement[] => {
|
|
93
|
+
const $highlights: HTMLElement[] = [];
|
|
94
|
+
const reg = new RegExp(`(${id}\\${ID_DIVISION}|\\${ID_DIVISION}?${id}$)`);
|
|
95
|
+
const $list = $root.querySelectorAll<HTMLElement>(`${wrapTag}[data-${DATASET_IDENTIFIER}]`);
|
|
96
|
+
|
|
97
|
+
for (const $l of $list) {
|
|
98
|
+
const $n = $l;
|
|
99
|
+
const nid = $n.dataset[CAMEL_DATASET_IDENTIFIER];
|
|
100
|
+
|
|
101
|
+
if (nid === id) {
|
|
102
|
+
$highlights.push($n);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const extraId = $n.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA];
|
|
107
|
+
|
|
108
|
+
if (reg.test(extraId)) {
|
|
109
|
+
$highlights.push($n);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return $highlights;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const forEach = ($nodes: NodeList, cb: (n: Node, idx: number, s: NodeList) => void): void => {
|
|
118
|
+
for (let i = 0; i < $nodes.length; i++) {
|
|
119
|
+
cb($nodes[i], i, $nodes);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const removeEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject) => {
|
|
124
|
+
$el.removeEventListener(evt, fn);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* maybe be need some polyfill methods later
|
|
129
|
+
* provide unified dom methods for compatibility
|
|
130
|
+
*/
|
|
131
|
+
export const addEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject) => {
|
|
132
|
+
$el.addEventListener(evt, fn);
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
removeEventListener($el, evt, fn);
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const addClass = ($el: HTMLElement, className: string[] | string) => {
|
|
140
|
+
if (!Array.isArray(className)) {
|
|
141
|
+
className = [className];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$el.classList.add(...className);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const removeClass = ($el: HTMLElement, className: string): void => {
|
|
148
|
+
$el.classList.remove(className);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const removeAllClass = ($el: HTMLElement): void => {
|
|
152
|
+
$el.className = '';
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const hasClass = ($el: HTMLElement, className: string): boolean => $el.classList.contains(className);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tiny event emitter
|
|
3
|
+
* modify from mitt
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
type EventHandler = (...data: unknown[]) => void;
|
|
7
|
+
|
|
8
|
+
type EventMap = Record<string, EventHandler>;
|
|
9
|
+
type HandlersMap<T extends EventMap> = {
|
|
10
|
+
[K in keyof T]: T[K][];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class EventEmitter<U extends EventMap = EventMap> {
|
|
14
|
+
private handlersMap: HandlersMap<U> = Object.create(null);
|
|
15
|
+
|
|
16
|
+
on<T extends keyof U>(type: T, handler: U[T]) {
|
|
17
|
+
if (!this.handlersMap[type]) {
|
|
18
|
+
this.handlersMap[type] = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.handlersMap[type].push(handler);
|
|
22
|
+
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
off<T extends keyof U>(type: T, handler: U[T]) {
|
|
27
|
+
if (this.handlersMap[type]) {
|
|
28
|
+
this.handlersMap[type].splice(this.handlersMap[type].indexOf(handler) >>> 0, 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
emit<T extends keyof U>(type: T, ...data: Parameters<U[T]>) {
|
|
35
|
+
if (this.handlersMap[type]) {
|
|
36
|
+
this.handlersMap[type].slice().forEach(handler => {
|
|
37
|
+
handler(...data);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default EventEmitter;
|
package/src/util/hook.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* simple hook
|
|
3
|
+
* webpack-plugin-liked api
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
type HookCallback<T> = (...args: unknown[]) => T;
|
|
7
|
+
|
|
8
|
+
class Hook<T = unknown> {
|
|
9
|
+
name = '';
|
|
10
|
+
|
|
11
|
+
private readonly ops: HookCallback<T>[] = [];
|
|
12
|
+
|
|
13
|
+
constructor(name?) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
tap(cb: HookCallback<T>) {
|
|
18
|
+
if (this.ops.indexOf(cb) === -1) {
|
|
19
|
+
this.ops.push(cb);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
this.remove(cb);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
remove(cb: HookCallback<T>) {
|
|
28
|
+
const idx = this.ops.indexOf(cb);
|
|
29
|
+
|
|
30
|
+
if (idx < 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.ops.splice(idx, 1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
isEmpty() {
|
|
38
|
+
return this.ops.length === 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
call(...args: unknown[]) {
|
|
42
|
+
let ret: T;
|
|
43
|
+
|
|
44
|
+
this.ops.forEach(op => {
|
|
45
|
+
ret = op(...args);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return ret;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default Hook;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter for mobile and desktop events
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IInteraction } from '@src/types';
|
|
6
|
+
import { UserInputEvent } from '@src/types';
|
|
7
|
+
import detectMobile from '@src/util/is.mobile';
|
|
8
|
+
|
|
9
|
+
export default (): IInteraction => {
|
|
10
|
+
const isMobile = detectMobile(window.navigator.userAgent);
|
|
11
|
+
|
|
12
|
+
const interaction: IInteraction = {
|
|
13
|
+
PointerEnd: isMobile ? UserInputEvent.touchend : UserInputEvent.mouseup,
|
|
14
|
+
PointerTap: isMobile ? UserInputEvent.touchstart : UserInputEvent.click,
|
|
15
|
+
// hover and click will be the same event in mobile
|
|
16
|
+
PointerOver: isMobile ? UserInputEvent.touchstart : UserInputEvent.mouseover,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return interaction;
|
|
20
|
+
};
|
package/src/util/tool.ts
ADDED
package/src/util/uuid.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate UUID
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
|
6
|
+
export default function createUUID(a?): string {
|
|
7
|
+
return a
|
|
8
|
+
? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
|
|
9
|
+
: (([1e7] as unknown as string) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, createUUID);
|
|
10
|
+
}
|