@vdegenne/highlight-manager 0.1.1

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/lib/index.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ interface Info {
2
+ elements: HTMLElement[];
3
+ highlightIndexStart: number;
4
+ highlightIndexEnd: number;
5
+ highlightElements: HTMLElement[];
6
+ /**
7
+ * First element of highlightElements if there is one
8
+ */
9
+ highlightElement: HTMLElement | undefined;
10
+ highlightContent: string | undefined;
11
+ }
12
+ interface Options {
13
+ css: string;
14
+ highlightTextColor: string;
15
+ /**
16
+ * @default true
17
+ */
18
+ loop: boolean;
19
+ /**
20
+ * A function for extra selection if selector is not enough
21
+ * and need a way to filter elements based on properties.
22
+ * Return false if you want to keep an element out of the bag.
23
+ */
24
+ atomicSelection: (element: HTMLElement, i: number) => boolean;
25
+ beforeHighlight: (() => void) | undefined;
26
+ onSelectionChange: ((info: Info) => void) | undefined;
27
+ /**
28
+ * By default the stylesheet for selection is applied to the main document.
29
+ * Which means won't highlight elements in shadow doms.
30
+ * You can target the element to give the stylesheet to.
31
+ * If the given element has no shadow dom, it will fail silently.
32
+ */
33
+ applyStyleSheetTo: Document | HTMLElement | ShadowRoot;
34
+ /**
35
+ * @default false
36
+ */
37
+ scrollWhenOffscreen: boolean;
38
+ }
39
+ export declare class HighLightManager {
40
+ #private;
41
+ protected selector: string;
42
+ constructor(selector: string, options?: Partial<Options>);
43
+ replaceCSS(css: string): void;
44
+ highlightWhenAvailable(index?: number, { checkSpeedMs, timeout, }?: {
45
+ checkSpeedMs?: number;
46
+ timeout?: number;
47
+ }): Promise<HTMLElement>;
48
+ cancelHighlightWhenAvailable(reason?: unknown): void;
49
+ getInfo(cache?: boolean): Info;
50
+ unhighlightAll(elements?: HTMLElement[], cache?: boolean): void;
51
+ highlightAll(cache?: boolean): void;
52
+ selectAll: (cache?: boolean) => void;
53
+ /**
54
+ * @returns {boolean} true if the highlight succeeded, false otherwise.
55
+ */
56
+ highlight(start: number, end?: number, unhighlightAll?: boolean, cache?: boolean): boolean;
57
+ previous(step?: number, cache?: boolean): void;
58
+ next(step?: number, cache?: boolean): void;
59
+ extendLeftHighlight(step?: number, cache?: boolean): void;
60
+ reduceLeftHighlight(step?: number, cache?: boolean): void;
61
+ extendRightHighlight(step?: number, cache?: boolean): void;
62
+ reduceRightHighlight(step?: number, cache?: boolean): void;
63
+ }
64
+ export {};
65
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,234 @@
1
+ import { querySelectorAll } from 'html-vision';
2
+ import { isInViewport, sleep } from './utils.js';
3
+ const defaults = {
4
+ atomicSelection(_element) {
5
+ return true;
6
+ },
7
+ // css: 'background-color: #cddc39a1 !important; color: black !important',
8
+ // css: 'background-color: var(--md-sys-color-surface-container-highest) !important; color: var(--md-sys-color-on-surface) !important',
9
+ css: 'background-color: var(--md-sys-color-primary-container) !important; color: var(--md-sys-color-on-primary-container) !important',
10
+ // css: 'background-color: var(--md-sys-color-primary) !important; color: var(--md-sys-color-on-primary) !important',
11
+ // css: 'background-color: var(--md-sys-color-outline-variant) !important; color: var(--md-sys-color-on-surface) !important',
12
+ highlightTextColor: 'var(--md-sys-color-on-primary-container)',
13
+ loop: true,
14
+ beforeHighlight: undefined,
15
+ onSelectionChange: undefined,
16
+ applyStyleSheetTo: document,
17
+ scrollWhenOffscreen: false,
18
+ };
19
+ // Local array of all declared highlighters for id control.
20
+ const highlighters = [];
21
+ export class HighLightManager {
22
+ #cache;
23
+ #options;
24
+ #ss;
25
+ #id;
26
+ constructor(selector, options) {
27
+ this.selector = selector;
28
+ this.#cache = {
29
+ elements: [],
30
+ // highlightIndex: -1,
31
+ highlightIndexStart: -1,
32
+ highlightIndexEnd: -1,
33
+ // highlightElement: undefined,
34
+ highlightElements: [],
35
+ highlightElement: undefined,
36
+ highlightContent: undefined,
37
+ };
38
+ // alias
39
+ this.selectAll = this.highlightAll.bind(this);
40
+ this.#id = highlighters.push(this);
41
+ this.#options = { ...defaults, ...options };
42
+ /* stylesheet */
43
+ this.#ss = new CSSStyleSheet();
44
+ let applyTo; // element to apply stylesheet to
45
+ if (this.#options.applyStyleSheetTo === document.documentElement ||
46
+ !(this.#options.applyStyleSheetTo instanceof HTMLElement) ||
47
+ this.#options.applyStyleSheetTo.shadowRoot === null) {
48
+ applyTo = document;
49
+ }
50
+ else {
51
+ applyTo = this.#options.applyStyleSheetTo.shadowRoot;
52
+ }
53
+ applyTo.adoptedStyleSheets.push(this.#ss);
54
+ // this.#ss.replaceSync(`[highlight] {${css}}`);
55
+ this.replaceCSS(this.#options.css);
56
+ }
57
+ replaceCSS(css) {
58
+ this.#options.css = css;
59
+ this.#ss.replaceSync(`[highlight${this.#id}] {${css}} [highlight${this.#id}]:hover {${css}} [highlight${this.#id}] * {color: ${this.#options.highlightTextColor} !important;}`);
60
+ }
61
+ #highlightWhenAvailablePromiseWR;
62
+ highlightWhenAvailable(index = 0, { checkSpeedMs = 1000, timeout = 5000, } = {}) {
63
+ // cancel any existing run
64
+ this.cancelHighlightWhenAvailable('restarted');
65
+ const wr = Promise.withResolvers();
66
+ this.#highlightWhenAvailablePromiseWR = wr;
67
+ (async () => {
68
+ const start = Date.now();
69
+ while (this.#highlightWhenAvailablePromiseWR === wr) {
70
+ const els = querySelectorAll(this.selector);
71
+ const el = els[index];
72
+ if (el) {
73
+ this.highlight(index, index, true, false);
74
+ wr.resolve(el);
75
+ this.#highlightWhenAvailablePromiseWR = undefined;
76
+ return;
77
+ }
78
+ if (timeout > 0 && Date.now() - start >= timeout) {
79
+ this.cancelHighlightWhenAvailable('timeout');
80
+ return;
81
+ }
82
+ await sleep(checkSpeedMs);
83
+ }
84
+ })();
85
+ return wr.promise;
86
+ }
87
+ cancelHighlightWhenAvailable(reason = 'canceled') {
88
+ if (this.#highlightWhenAvailablePromiseWR) {
89
+ this.#highlightWhenAvailablePromiseWR.reject(reason);
90
+ this.#highlightWhenAvailablePromiseWR = undefined;
91
+ }
92
+ }
93
+ getInfo(cache = false) {
94
+ if (cache) {
95
+ return this.#cache;
96
+ }
97
+ // console.log(this.selector)
98
+ const elements = querySelectorAll(this.selector).filter((el, i) => this.#options.atomicSelection(el, i));
99
+ const highlightElements = elements.filter((el) => el.hasAttribute(`highlight${this.#id}`));
100
+ // const highlightIndexStart = elements.findIndex((el) =>
101
+ // el.hasAttribute('highlight'),
102
+ // );
103
+ if (!highlightElements || highlightElements.length === 0) {
104
+ throw Error("The highlighted element couldn't be found");
105
+ }
106
+ const highlightIndexStart = elements.indexOf(highlightElements[0]);
107
+ const highlightIndexEnd = elements.indexOf(highlightElements[highlightElements.length - 1]);
108
+ if (highlightElements.length === 1) {
109
+ // const highlightElement = elements[highlightIndex];
110
+ }
111
+ const highlightContent = highlightElements
112
+ // TODO: should prob change that ariaLabel (for lens into a customizable content getter)
113
+ .map((el) => el.ariaLabel || el.innerText?.trim() || '')
114
+ .join('');
115
+ // highlightElement?.innerText.trim();
116
+ return (this.#cache = {
117
+ elements,
118
+ // highlightIndex,
119
+ highlightIndexStart,
120
+ highlightIndexEnd,
121
+ highlightElements,
122
+ highlightElement: highlightElements[0],
123
+ highlightContent,
124
+ });
125
+ }
126
+ unhighlightAll(elements, cache = true) {
127
+ if (!elements) {
128
+ elements = this.getInfo(cache).elements;
129
+ }
130
+ elements.forEach((el) => el.removeAttribute(`highlight${this.#id}`));
131
+ }
132
+ highlightAll(cache = false) {
133
+ const { elements } = this.getInfo(cache);
134
+ this.highlight(0, elements.length - 1, false, cache);
135
+ }
136
+ /**
137
+ * @returns {boolean} true if the highlight succeeded, false otherwise.
138
+ */
139
+ highlight(start, end, unhighlightAll = true, cache = false) {
140
+ if (end === undefined) {
141
+ end = start;
142
+ }
143
+ if (start > end) {
144
+ return false;
145
+ // const tmp = start
146
+ // start = end
147
+ // end = tmp
148
+ }
149
+ const { elements, highlightIndexStart, highlightIndexEnd } = this.getInfo(cache);
150
+ // console.log(elements)
151
+ if (highlightIndexStart === start && highlightIndexEnd === end) {
152
+ return false;
153
+ }
154
+ // console.log(highlightIndexStart, highlightIndexEnd, start, end)
155
+ this.#options.beforeHighlight?.();
156
+ // playClick()
157
+ if (unhighlightAll) {
158
+ this.unhighlightAll(elements, cache);
159
+ }
160
+ const elementsToHighlight = elements.slice(start, end + 1);
161
+ if (elementsToHighlight.length === 0) {
162
+ return false;
163
+ }
164
+ if (this.#options.scrollWhenOffscreen ||
165
+ !isInViewport(elementsToHighlight[0])) {
166
+ elementsToHighlight[0]?.scrollIntoView({
167
+ behavior: 'smooth',
168
+ block: 'center',
169
+ inline: 'center',
170
+ });
171
+ }
172
+ elementsToHighlight.forEach((el) => el.setAttribute(`highlight${this.#id}`, ''));
173
+ // elements[index]?.setAttribute('highlight', '');
174
+ if (this.#options.onSelectionChange) {
175
+ this.#options.onSelectionChange(this.getInfo(false));
176
+ }
177
+ return true;
178
+ }
179
+ previous(step = 1, cache = false) {
180
+ const { elements, highlightIndexStart, highlightIndexEnd } = this.getInfo(cache);
181
+ const len = elements.length;
182
+ if (len === 0) {
183
+ this.highlight(-1, -1, true, cache);
184
+ return;
185
+ }
186
+ let previousIndex = highlightIndexStart !== highlightIndexEnd
187
+ ? highlightIndexStart
188
+ : this.#options.loop
189
+ ? (highlightIndexStart - step + len) % len
190
+ : Math.max(0, highlightIndexStart - step);
191
+ this.highlight(previousIndex, previousIndex, true, cache);
192
+ }
193
+ next(step = 1, cache = false) {
194
+ const { elements, highlightIndexStart, highlightIndexEnd } = this.getInfo(cache);
195
+ const len = elements.length;
196
+ if (len === 0) {
197
+ this.highlight(-1, -1, true, cache);
198
+ return;
199
+ }
200
+ let nextIndex = highlightIndexStart !== highlightIndexEnd
201
+ ? highlightIndexEnd
202
+ : this.#options.loop
203
+ ? (highlightIndexEnd + step) % len
204
+ : Math.min(len - 1, highlightIndexEnd + step);
205
+ this.highlight(nextIndex, nextIndex, true, cache);
206
+ }
207
+ extendLeftHighlight(step = 1, cache = false) {
208
+ // playClick();
209
+ const { highlightIndexStart, highlightIndexEnd } = this.getInfo(cache);
210
+ const newStart = Math.max(0, highlightIndexStart - step);
211
+ this.highlight(newStart, highlightIndexEnd, false, cache);
212
+ }
213
+ reduceLeftHighlight(step = 1, cache = false) {
214
+ // playClick();
215
+ const { elements, highlightIndexStart, highlightIndexEnd } = this.getInfo(cache);
216
+ // TODO: should prob change the min to end index
217
+ const newStart = Math.min(elements.length - 1, highlightIndexStart + step);
218
+ this.highlight(newStart, highlightIndexEnd, true, cache);
219
+ }
220
+ extendRightHighlight(step = 1, cache = false) {
221
+ // playClick();
222
+ const { elements, highlightIndexStart, highlightIndexEnd } = this.getInfo(cache);
223
+ const newEnd = Math.min(elements.length - 1, highlightIndexEnd + step);
224
+ this.highlight(highlightIndexStart, newEnd, false, cache);
225
+ }
226
+ reduceRightHighlight(step = 1, cache = false) {
227
+ // playClick();
228
+ const { highlightIndexStart, highlightIndexEnd } = this.getInfo(cache);
229
+ // TODO: should prob change the max to end index
230
+ const newEnd = Math.max(0, highlightIndexEnd - step);
231
+ this.highlight(highlightIndexStart, newEnd, true, cache);
232
+ }
233
+ }
234
+ //# sourceMappingURL=index.js.map
package/lib/utils.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function sleep(milli?: number): Promise<unknown>;
2
+ export declare function isInViewport(el: Element): boolean;
3
+ //# sourceMappingURL=utils.d.ts.map
package/lib/utils.js ADDED
@@ -0,0 +1,8 @@
1
+ export function sleep(milli = 1000) {
2
+ return new Promise((r) => setTimeout(r, milli));
3
+ }
4
+ export function isInViewport(el) {
5
+ return (el.getBoundingClientRect().top >= 0 &&
6
+ el.getBoundingClientRect().bottom <= window.innerHeight);
7
+ }
8
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@vdegenne/highlight-manager",
3
+ "version": "0.1.1",
4
+ "description": "helper to navigate/highlight elements in a page based on a css selector",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./lib/index.js",
9
+ "types": "./lib/index.d.ts"
10
+ }
11
+ },
12
+ "files": [
13
+ "lib/**",
14
+ "!lib/**/*.js.map",
15
+ "!lib/**/*.d.ts.map"
16
+ ],
17
+ "scripts": {
18
+ "build": "wireit"
19
+ },
20
+ "wireit": {
21
+ "build": {
22
+ "command": "tsc --pretty",
23
+ "clean": "if-file-deleted",
24
+ "files": [
25
+ "./tsconfig.json",
26
+ "./src"
27
+ ],
28
+ "output": [
29
+ "./lib"
30
+ ]
31
+ }
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^6.0.3",
35
+ "wireit": "^0.14.12"
36
+ },
37
+ "dependencies": {
38
+ "html-vision": "^0.3.10"
39
+ }
40
+ }