@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 +65 -0
- package/lib/index.js +234 -0
- package/lib/utils.d.ts +3 -0
- package/lib/utils.js +8 -0
- package/package.json +40 -0
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
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
|
+
}
|