@pie-players/pie-tool-magnifier 0.1.8 → 1.0.0-beta.2
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 +101 -124
- package/custom-elements.json +83 -0
- package/dist/index.d.ts +1 -8
- package/dist/tool-magnifier.js +3102 -2869
- package/package.json +34 -30
- package/Magnifier.svelte +0 -340
- package/dist/index.d.ts.map +0 -1
- package/dist/tool-magnifier.js.map +0 -1
- package/index.ts +0 -8
- package/tool-magnifier.svelte +0 -140
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-tool-magnifier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Magnifying lens tool for PIE assessment player",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/pie-framework/pie-players.git"
|
|
8
|
+
"url": "git+https://github.com/pie-framework/pie-players.git",
|
|
9
|
+
"directory": "packages/tool-magnifier"
|
|
9
10
|
},
|
|
10
11
|
"publishConfig": {
|
|
11
12
|
"access": "public"
|
|
@@ -16,51 +17,54 @@
|
|
|
16
17
|
"tool",
|
|
17
18
|
"magnifier",
|
|
18
19
|
"magnification",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
20
|
+
"low-vision",
|
|
21
|
+
"accessibility"
|
|
21
22
|
],
|
|
22
|
-
"svelte": "./tool-magnifier.svelte",
|
|
23
23
|
"main": "./dist/tool-magnifier.js",
|
|
24
|
-
"types": "./dist/index.d.ts",
|
|
25
24
|
"exports": {
|
|
26
25
|
".": {
|
|
27
26
|
"types": "./dist/index.d.ts",
|
|
28
|
-
"import": "./dist/tool-magnifier.js"
|
|
29
|
-
"svelte": "./tool-magnifier.svelte"
|
|
27
|
+
"import": "./dist/tool-magnifier.js"
|
|
30
28
|
}
|
|
31
29
|
},
|
|
30
|
+
"customElements": "custom-elements.json",
|
|
32
31
|
"files": [
|
|
33
32
|
"dist",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"Magnifier.svelte",
|
|
37
|
-
"package.json",
|
|
38
|
-
"README.md"
|
|
33
|
+
"custom-elements.json",
|
|
34
|
+
"package.json"
|
|
39
35
|
],
|
|
40
|
-
"peerDependencies": {
|
|
41
|
-
"svelte": "^5.51.0"
|
|
42
|
-
},
|
|
43
36
|
"license": "MIT",
|
|
44
37
|
"unpkg": "./dist/tool-magnifier.js",
|
|
45
38
|
"jsdelivr": "./dist/tool-magnifier.js",
|
|
46
39
|
"dependencies": {
|
|
47
|
-
"@pie-players/pie-assessment-toolkit": "0.2
|
|
48
|
-
"@pie-players/pie-players-shared": "0.2
|
|
49
|
-
"@sveltejs/kit": "^2.52.0",
|
|
50
|
-
"daisyui": "^5.5.18"
|
|
40
|
+
"@pie-players/pie-assessment-toolkit": "1.0.0-beta.2",
|
|
41
|
+
"@pie-players/pie-players-shared": "1.0.0-beta.2"
|
|
51
42
|
},
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
52
44
|
"scripts": {
|
|
53
45
|
"build": "vite build",
|
|
54
46
|
"dev": "vite build --watch",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
47
|
+
"test": "bun test tests",
|
|
48
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
49
|
+
"check": "svelte-check --tsconfig ./tsconfig.json --compiler-warnings options_missing_custom_element:ignore",
|
|
50
|
+
"lint": "biome lint ."
|
|
57
51
|
},
|
|
58
52
|
"devDependencies": {
|
|
59
|
-
"@biomejs/biome": "^2.
|
|
60
|
-
"@
|
|
61
|
-
"svelte": "^
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
53
|
+
"@biomejs/biome": "^2.5.15",
|
|
54
|
+
"@happy-dom/global-registrator": "^20.14.5",
|
|
55
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
56
|
+
"svelte": "^5.57.2",
|
|
57
|
+
"svelte-check": "^4.7.6",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
59
|
+
"vite": "^8.3.3",
|
|
60
|
+
"vite-plugin-dts": "^5.1.2"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/tool-magnifier#readme",
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/pie-framework/pie-players/issues"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20.0.0"
|
|
68
|
+
},
|
|
69
|
+
"sideEffects": true
|
|
66
70
|
}
|
package/Magnifier.svelte
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
<!-- Magnifier.svelte - Modern Svelte 5 implementation of HTML magnifier -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
import { onMount, untrack } from 'svelte';
|
|
4
|
-
|
|
5
|
-
interface MagnifierProps {
|
|
6
|
-
zoom?: number;
|
|
7
|
-
width?: number;
|
|
8
|
-
height?: number;
|
|
9
|
-
shape?: 'square' | 'circle';
|
|
10
|
-
visible?: boolean;
|
|
11
|
-
onPrepareContent?: (clonedContent: HTMLElement) => void;
|
|
12
|
-
onContentUpdated?: (bodyElement: HTMLElement) => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let {
|
|
16
|
-
zoom = 2,
|
|
17
|
-
width = 420,
|
|
18
|
-
height = 280,
|
|
19
|
-
shape = 'square',
|
|
20
|
-
visible = $bindable(false),
|
|
21
|
-
onPrepareContent,
|
|
22
|
-
onContentUpdated
|
|
23
|
-
}: MagnifierProps = $props();
|
|
24
|
-
|
|
25
|
-
// Reactive state
|
|
26
|
-
let magnifierEl = $state<HTMLDivElement | null>(null);
|
|
27
|
-
let contentEl = $state<HTMLDivElement | null>(null);
|
|
28
|
-
let clonedBody = $state<HTMLElement | null>(null);
|
|
29
|
-
let position = $state({ x: 200, y: 200 });
|
|
30
|
-
let isDragging = $state(false);
|
|
31
|
-
let dragOffset = $state({ x: 0, y: 0 });
|
|
32
|
-
|
|
33
|
-
// MutationObserver to sync content changes
|
|
34
|
-
let observer: MutationObserver | null = null;
|
|
35
|
-
let syncTimeoutHandle: number | null = null;
|
|
36
|
-
|
|
37
|
-
// Derived styles
|
|
38
|
-
let magnifierStyle = $derived(`
|
|
39
|
-
left: ${position.x}px;
|
|
40
|
-
top: ${position.y}px;
|
|
41
|
-
width: ${width}px;
|
|
42
|
-
height: ${height}px;
|
|
43
|
-
${shape === 'circle' ? 'border-radius: 50%;' : ''}
|
|
44
|
-
display: ${visible ? 'block' : 'none'};
|
|
45
|
-
`);
|
|
46
|
-
|
|
47
|
-
let contentStyle = $derived(`
|
|
48
|
-
transform: scale(${zoom});
|
|
49
|
-
left: ${-position.x * zoom - window.scrollX * zoom}px;
|
|
50
|
-
top: ${-position.y * zoom - window.scrollY * zoom}px;
|
|
51
|
-
width: ${document.body.scrollWidth}px;
|
|
52
|
-
height: ${document.body.scrollHeight}px;
|
|
53
|
-
`);
|
|
54
|
-
|
|
55
|
-
function cloneDocumentBody() {
|
|
56
|
-
const bodyOriginal = document.body;
|
|
57
|
-
const bodyCopy = bodyOriginal.cloneNode(true) as HTMLElement;
|
|
58
|
-
|
|
59
|
-
// Copy background color
|
|
60
|
-
const bgColor = getComputedStyle(bodyOriginal).backgroundColor;
|
|
61
|
-
if (bgColor && magnifierEl) {
|
|
62
|
-
magnifierEl.style.backgroundColor = bgColor;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Reset styles
|
|
66
|
-
bodyCopy.style.cursor = 'auto';
|
|
67
|
-
bodyCopy.style.paddingTop = '0px';
|
|
68
|
-
bodyCopy.setAttribute('unselectable', 'on');
|
|
69
|
-
|
|
70
|
-
// Copy canvas contents
|
|
71
|
-
const canvasOriginal = bodyOriginal.querySelectorAll('canvas');
|
|
72
|
-
const canvasCopy = bodyCopy.querySelectorAll('canvas');
|
|
73
|
-
if (canvasOriginal.length > 0 && canvasOriginal.length === canvasCopy.length) {
|
|
74
|
-
for (let i = 0; i < canvasOriginal.length; i++) {
|
|
75
|
-
const ctx = canvasCopy[i].getContext('2d');
|
|
76
|
-
if (ctx) {
|
|
77
|
-
try {
|
|
78
|
-
ctx.drawImage(canvasOriginal[i], 0, 0);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
// Silent fail - likely CORS issue
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Remove elements that shouldn't be magnified
|
|
87
|
-
const elementsToRemove = [
|
|
88
|
-
...bodyCopy.querySelectorAll('script'),
|
|
89
|
-
...bodyCopy.querySelectorAll('audio'),
|
|
90
|
-
...bodyCopy.querySelectorAll('video'),
|
|
91
|
-
...bodyCopy.querySelectorAll('.magnifier'),
|
|
92
|
-
...bodyCopy.querySelectorAll('.magnifier-clone-ignore'),
|
|
93
|
-
...bodyCopy.querySelectorAll('[data-magnifier-ignore]')
|
|
94
|
-
];
|
|
95
|
-
elementsToRemove.forEach(el => el.remove());
|
|
96
|
-
|
|
97
|
-
// Custom content preparation
|
|
98
|
-
if (onPrepareContent) {
|
|
99
|
-
onPrepareContent(bodyCopy);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return bodyCopy;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function updateContent() {
|
|
106
|
-
if (!visible || !contentEl) return;
|
|
107
|
-
|
|
108
|
-
// Clear existing content
|
|
109
|
-
contentEl.innerHTML = '';
|
|
110
|
-
|
|
111
|
-
// Clone and prepare content
|
|
112
|
-
clonedBody = cloneDocumentBody();
|
|
113
|
-
contentEl.appendChild(clonedBody);
|
|
114
|
-
|
|
115
|
-
// Sync scroll positions
|
|
116
|
-
syncScrollPositions();
|
|
117
|
-
|
|
118
|
-
// Notify content update
|
|
119
|
-
if (onContentUpdated && clonedBody) {
|
|
120
|
-
const bodyInClone = clonedBody.querySelector('body');
|
|
121
|
-
if (bodyInClone) {
|
|
122
|
-
onContentUpdated(bodyInClone as HTMLElement);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function syncScrollPositions() {
|
|
128
|
-
if (!clonedBody) return;
|
|
129
|
-
|
|
130
|
-
// Find all scrolled divs in the original document
|
|
131
|
-
const scrolledDivs = Array.from(document.querySelectorAll('div')).filter(
|
|
132
|
-
div => div.scrollTop > 0 && !magnifierEl?.contains(div)
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Sync scroll positions to cloned elements
|
|
136
|
-
scrolledDivs.forEach(div => {
|
|
137
|
-
const selectors: string[] = [];
|
|
138
|
-
|
|
139
|
-
if (div.id) {
|
|
140
|
-
selectors.push('#' + div.id);
|
|
141
|
-
}
|
|
142
|
-
if (div.className) {
|
|
143
|
-
selectors.push('.' + div.className.split(' ').join('.'));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
selectors.forEach(selector => {
|
|
147
|
-
const targets = clonedBody!.querySelectorAll(selector);
|
|
148
|
-
if (targets.length === 1) {
|
|
149
|
-
const target = targets[0] as HTMLElement;
|
|
150
|
-
target.scrollTop = div.scrollTop;
|
|
151
|
-
target.scrollLeft = div.scrollLeft;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function queueContentUpdate() {
|
|
158
|
-
if (syncTimeoutHandle !== null) {
|
|
159
|
-
clearTimeout(syncTimeoutHandle);
|
|
160
|
-
}
|
|
161
|
-
syncTimeoutHandle = window.setTimeout(() => {
|
|
162
|
-
updateContent();
|
|
163
|
-
}, 100);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function startObserving() {
|
|
167
|
-
if (observer) return;
|
|
168
|
-
|
|
169
|
-
observer = new MutationObserver((mutations) => {
|
|
170
|
-
for (const mutation of mutations) {
|
|
171
|
-
// Skip mutations within the magnifier itself
|
|
172
|
-
if (magnifierEl && magnifierEl.contains(mutation.target as Node)) {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
queueContentUpdate();
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
observer.observe(document.body, {
|
|
181
|
-
childList: true,
|
|
182
|
-
subtree: true,
|
|
183
|
-
attributes: true,
|
|
184
|
-
attributeFilter: ['class', 'width', 'height', 'style'],
|
|
185
|
-
attributeOldValue: true
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function stopObserving() {
|
|
190
|
-
if (observer) {
|
|
191
|
-
observer.disconnect();
|
|
192
|
-
observer = null;
|
|
193
|
-
}
|
|
194
|
-
if (syncTimeoutHandle !== null) {
|
|
195
|
-
clearTimeout(syncTimeoutHandle);
|
|
196
|
-
syncTimeoutHandle = null;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Drag handlers
|
|
201
|
-
function handlePointerDown(e: PointerEvent) {
|
|
202
|
-
// Don't start drag on interactive elements or their children
|
|
203
|
-
const target = e.target as HTMLElement;
|
|
204
|
-
|
|
205
|
-
// Check if the target or any parent is an interactive element
|
|
206
|
-
const interactiveTags = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
|
|
207
|
-
let element: HTMLElement | null = target;
|
|
208
|
-
while (element && element !== magnifierEl) {
|
|
209
|
-
if (interactiveTags.includes(element.tagName.toUpperCase())) {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
element = element.parentElement;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
isDragging = true;
|
|
216
|
-
dragOffset = {
|
|
217
|
-
x: e.clientX - position.x,
|
|
218
|
-
y: e.clientY - position.y
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
if (magnifierEl) {
|
|
222
|
-
magnifierEl.setPointerCapture(e.pointerId);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function handlePointerMove(e: PointerEvent) {
|
|
227
|
-
if (!isDragging) return;
|
|
228
|
-
|
|
229
|
-
position = {
|
|
230
|
-
x: e.clientX - dragOffset.x,
|
|
231
|
-
y: e.clientY - dragOffset.y
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function handlePointerUp(e: PointerEvent) {
|
|
236
|
-
if (isDragging && magnifierEl) {
|
|
237
|
-
magnifierEl.releasePointerCapture(e.pointerId);
|
|
238
|
-
}
|
|
239
|
-
isDragging = false;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Scroll sync handler
|
|
243
|
-
function handleScroll(e: Event) {
|
|
244
|
-
if (!visible) return;
|
|
245
|
-
|
|
246
|
-
const target = e.target as HTMLElement;
|
|
247
|
-
if (magnifierEl?.contains(target)) return;
|
|
248
|
-
|
|
249
|
-
// Queue a content update to sync scroll positions
|
|
250
|
-
queueContentUpdate();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Update content when becoming visible
|
|
254
|
-
$effect(() => {
|
|
255
|
-
if (visible) {
|
|
256
|
-
untrack(() => {
|
|
257
|
-
updateContent();
|
|
258
|
-
startObserving();
|
|
259
|
-
});
|
|
260
|
-
} else {
|
|
261
|
-
untrack(() => {
|
|
262
|
-
stopObserving();
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Update content when zoom changes
|
|
268
|
-
$effect(() => {
|
|
269
|
-
// Track zoom dependency
|
|
270
|
-
zoom;
|
|
271
|
-
if (visible) {
|
|
272
|
-
untrack(() => {
|
|
273
|
-
updateContent();
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// Lifecycle
|
|
279
|
-
onMount(() => {
|
|
280
|
-
// Listen for scroll events
|
|
281
|
-
window.addEventListener('scroll', handleScroll, true);
|
|
282
|
-
window.addEventListener('resize', queueContentUpdate);
|
|
283
|
-
|
|
284
|
-
return () => {
|
|
285
|
-
stopObserving();
|
|
286
|
-
window.removeEventListener('scroll', handleScroll, true);
|
|
287
|
-
window.removeEventListener('resize', queueContentUpdate);
|
|
288
|
-
};
|
|
289
|
-
});
|
|
290
|
-
</script>
|
|
291
|
-
|
|
292
|
-
<div
|
|
293
|
-
bind:this={magnifierEl}
|
|
294
|
-
class="magnifier"
|
|
295
|
-
style={magnifierStyle}
|
|
296
|
-
onpointerdown={handlePointerDown}
|
|
297
|
-
onpointermove={handlePointerMove}
|
|
298
|
-
onpointerup={handlePointerUp}
|
|
299
|
-
role="region"
|
|
300
|
-
aria-label="Content magnifier"
|
|
301
|
-
>
|
|
302
|
-
<div bind:this={contentEl} class="magnifier-content" style={contentStyle}></div>
|
|
303
|
-
<div class="magnifier-glass"></div>
|
|
304
|
-
<slot />
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
<style>
|
|
308
|
-
.magnifier {
|
|
309
|
-
position: fixed;
|
|
310
|
-
overflow: hidden;
|
|
311
|
-
background-color: white;
|
|
312
|
-
border: 1px solid #555;
|
|
313
|
-
border-radius: 4px;
|
|
314
|
-
z-index: 10000;
|
|
315
|
-
cursor: move;
|
|
316
|
-
touch-action: none;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
.magnifier-content {
|
|
320
|
-
position: absolute;
|
|
321
|
-
top: 0;
|
|
322
|
-
left: 0;
|
|
323
|
-
overflow: visible;
|
|
324
|
-
display: block;
|
|
325
|
-
transform-origin: left top;
|
|
326
|
-
user-select: none;
|
|
327
|
-
pointer-events: none;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.magnifier-glass {
|
|
331
|
-
position: absolute;
|
|
332
|
-
top: 0;
|
|
333
|
-
left: 0;
|
|
334
|
-
width: 100%;
|
|
335
|
-
height: 100%;
|
|
336
|
-
opacity: 0;
|
|
337
|
-
background-color: transparent;
|
|
338
|
-
pointer-events: all;
|
|
339
|
-
}
|
|
340
|
-
</style>
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|