@pie-players/pie-tool-annotation-toolbar 0.3.42 → 0.3.44
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/dist/index.d.ts +0 -1
- package/package.json +5 -9
- package/dist/index.d.ts.map +0 -1
- package/tool-annotation-toolbar.svelte +0 -654
package/dist/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-tool-annotation-toolbar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.44",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Text annotation toolbar with highlighting and underlining for PIE assessment player",
|
|
6
6
|
"repository": {
|
|
@@ -20,19 +20,15 @@
|
|
|
20
20
|
"highlighting",
|
|
21
21
|
"accessibility"
|
|
22
22
|
],
|
|
23
|
-
"svelte": "./tool-annotation-toolbar.svelte",
|
|
24
23
|
"main": "./dist/tool-annotation-toolbar.js",
|
|
25
24
|
"exports": {
|
|
26
25
|
".": {
|
|
27
26
|
"types": "./dist/index.d.ts",
|
|
28
|
-
"import": "./dist/tool-annotation-toolbar.js"
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
"./tool-annotation-toolbar.svelte": "./tool-annotation-toolbar.svelte"
|
|
27
|
+
"import": "./dist/tool-annotation-toolbar.js"
|
|
28
|
+
}
|
|
32
29
|
},
|
|
33
30
|
"files": [
|
|
34
31
|
"dist",
|
|
35
|
-
"tool-annotation-toolbar.svelte",
|
|
36
32
|
"package.json",
|
|
37
33
|
"README.md"
|
|
38
34
|
],
|
|
@@ -40,8 +36,8 @@
|
|
|
40
36
|
"unpkg": "./dist/tool-annotation-toolbar.js",
|
|
41
37
|
"jsdelivr": "./dist/tool-annotation-toolbar.js",
|
|
42
38
|
"dependencies": {
|
|
43
|
-
"@pie-players/pie-assessment-toolkit": "0.3.
|
|
44
|
-
"@pie-players/pie-players-shared": "0.3.
|
|
39
|
+
"@pie-players/pie-assessment-toolkit": "0.3.44",
|
|
40
|
+
"@pie-players/pie-players-shared": "0.3.44"
|
|
45
41
|
},
|
|
46
42
|
"types": "./dist/index.d.ts",
|
|
47
43
|
"scripts": {
|
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"}
|
|
@@ -1,654 +0,0 @@
|
|
|
1
|
-
<svelte:options
|
|
2
|
-
customElement={{
|
|
3
|
-
tag: 'pie-tool-annotation-toolbar',
|
|
4
|
-
shadow: 'open',
|
|
5
|
-
props: {
|
|
6
|
-
enabled: { type: 'Boolean', attribute: 'enabled' },
|
|
7
|
-
highlightCoordinator: { type: 'Object' },
|
|
8
|
-
ttsService: { type: 'Object' }
|
|
9
|
-
}
|
|
10
|
-
}}
|
|
11
|
-
/>
|
|
12
|
-
|
|
13
|
-
<script lang="ts">
|
|
14
|
-
import "./highlights.css";
|
|
15
|
-
import type {
|
|
16
|
-
AssessmentToolkitRegionScopeContext,
|
|
17
|
-
AssessmentToolkitShellContext,
|
|
18
|
-
HighlightCoordinator,
|
|
19
|
-
TtsServiceApi
|
|
20
|
-
} from '@pie-players/pie-assessment-toolkit';
|
|
21
|
-
import {
|
|
22
|
-
connectAssessmentToolkitRegionScopeContext,
|
|
23
|
-
connectAssessmentToolkitShellContext,
|
|
24
|
-
HighlightColor
|
|
25
|
-
} from '@pie-players/pie-assessment-toolkit';
|
|
26
|
-
|
|
27
|
-
interface Props {
|
|
28
|
-
enabled?: boolean;
|
|
29
|
-
highlightCoordinator?: HighlightCoordinator | null;
|
|
30
|
-
ttsService?: TtsServiceApi | null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let {
|
|
34
|
-
enabled = true,
|
|
35
|
-
highlightCoordinator = null,
|
|
36
|
-
ttsService = null
|
|
37
|
-
}: Props = $props();
|
|
38
|
-
|
|
39
|
-
const isBrowser = typeof window !== 'undefined';
|
|
40
|
-
|
|
41
|
-
// Storage key for sessionStorage
|
|
42
|
-
const STORAGE_KEY = 'pie-annotations';
|
|
43
|
-
|
|
44
|
-
// Available highlight colors (modern, accessible palette)
|
|
45
|
-
const HIGHLIGHT_COLORS = [
|
|
46
|
-
{ name: HighlightColor.YELLOW, hex: '#fde995', label: 'Yellow highlight' },
|
|
47
|
-
{ name: HighlightColor.PINK, hex: '#ff9fae', label: 'Pink highlight' },
|
|
48
|
-
{ name: HighlightColor.BLUE, hex: '#a7e0f6', label: 'Blue highlight' },
|
|
49
|
-
{ name: HighlightColor.GREEN, hex: '#a6e1c5', label: 'Green highlight' }
|
|
50
|
-
] as const;
|
|
51
|
-
|
|
52
|
-
// Disallowed elements - don't show toolbar when selecting these
|
|
53
|
-
const DISALLOWED_SELECTORS = [
|
|
54
|
-
'button',
|
|
55
|
-
'input',
|
|
56
|
-
'select',
|
|
57
|
-
'textarea',
|
|
58
|
-
'[contenteditable="true"]',
|
|
59
|
-
'.pie-tool-annotation-toolbar',
|
|
60
|
-
'.pie-tool-toolbar',
|
|
61
|
-
'[role="button"]',
|
|
62
|
-
'[role="textbox"]'
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
// State - using Svelte 5 $state rune for reactive state
|
|
66
|
-
let contextHostElement = $state<HTMLElement | null>(null);
|
|
67
|
-
let toolbarElement = $state<HTMLElement | null>(null);
|
|
68
|
-
let shellContext = $state<AssessmentToolkitShellContext | null>(null);
|
|
69
|
-
let regionScopeContext = $state<AssessmentToolkitRegionScopeContext | null>(null);
|
|
70
|
-
let toolbarState = $state({
|
|
71
|
-
isVisible: false,
|
|
72
|
-
selectedText: '',
|
|
73
|
-
selectedRange: null as Range | null,
|
|
74
|
-
toolbarPosition: { x: 0, y: 0 }
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// TTS state
|
|
78
|
-
let ttsSpeaking = $state(false);
|
|
79
|
-
|
|
80
|
-
// UX state
|
|
81
|
-
let justShown = $state(false); // Flag to prevent immediate hiding after showing
|
|
82
|
-
let positionAnnouncement = $state(''); // For screen readers when toolbar is repositioned
|
|
83
|
-
|
|
84
|
-
// Track annotation count for reactivity (increments on add/remove to trigger UI updates)
|
|
85
|
-
let annotationCount = $state(0);
|
|
86
|
-
|
|
87
|
-
// Track if current selection overlaps with an existing annotation
|
|
88
|
-
let overlappingAnnotationId = $state<string | null>(null);
|
|
89
|
-
|
|
90
|
-
// Derived state
|
|
91
|
-
let hasAnnotations = $derived(annotationCount > 0);
|
|
92
|
-
let hasOverlappingAnnotation = $derived(overlappingAnnotationId !== null);
|
|
93
|
-
let effectiveScopeElement = $derived(
|
|
94
|
-
regionScopeContext?.scopeElement || shellContext?.scopeElement || null
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
function getEffectiveRoot(): HTMLElement {
|
|
98
|
-
const ownerDoc = contextHostElement?.ownerDocument;
|
|
99
|
-
return effectiveScopeElement || ownerDoc?.documentElement || document.documentElement;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function getStorageKey(): string {
|
|
103
|
-
const scopeKey = shellContext?.canonicalItemId || shellContext?.itemId || 'global';
|
|
104
|
-
return `${STORAGE_KEY}:${scopeKey}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Find annotation that overlaps with the given range
|
|
109
|
-
*/
|
|
110
|
-
function findOverlappingAnnotation(range: Range): string | null {
|
|
111
|
-
if (!highlightCoordinator) return null;
|
|
112
|
-
|
|
113
|
-
const annotations = highlightCoordinator.getAnnotations();
|
|
114
|
-
for (const annotation of annotations) {
|
|
115
|
-
// Check if ranges overlap
|
|
116
|
-
// Two ranges overlap if: startA < endB && startB < endA
|
|
117
|
-
const cmp1 = range.compareBoundaryPoints(Range.START_TO_START, annotation.range);
|
|
118
|
-
const cmp2 = range.compareBoundaryPoints(Range.END_TO_END, annotation.range);
|
|
119
|
-
const cmp3 = range.compareBoundaryPoints(Range.START_TO_END, annotation.range);
|
|
120
|
-
const cmp4 = range.compareBoundaryPoints(Range.END_TO_START, annotation.range);
|
|
121
|
-
|
|
122
|
-
// Check various overlap conditions:
|
|
123
|
-
// 1. Selection is inside annotation
|
|
124
|
-
// 2. Annotation is inside selection
|
|
125
|
-
// 3. Selection partially overlaps annotation
|
|
126
|
-
if (
|
|
127
|
-
(cmp1 >= 0 && cmp2 <= 0) || // selection inside annotation
|
|
128
|
-
(cmp1 <= 0 && cmp2 >= 0) || // annotation inside selection
|
|
129
|
-
(cmp3 > 0 && cmp4 < 0) // partial overlap
|
|
130
|
-
) {
|
|
131
|
-
return annotation.id;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Check if selection is in an allowed area
|
|
139
|
-
*/
|
|
140
|
-
function isInAllowedArea(node: Node): boolean {
|
|
141
|
-
if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// For text nodes, check parent element
|
|
146
|
-
const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : (node as Element);
|
|
147
|
-
if (!element) return false;
|
|
148
|
-
|
|
149
|
-
// Check if element or any ancestor matches disallowed selectors
|
|
150
|
-
return !DISALLOWED_SELECTORS.some((sel) => {
|
|
151
|
-
try {
|
|
152
|
-
return element.closest(sel) !== null;
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function isWithinScope(range: Range): boolean {
|
|
160
|
-
if (!effectiveScopeElement) return true;
|
|
161
|
-
const ancestor = range.commonAncestorContainer;
|
|
162
|
-
const element =
|
|
163
|
-
ancestor.nodeType === Node.TEXT_NODE
|
|
164
|
-
? ancestor.parentElement
|
|
165
|
-
: (ancestor as Element);
|
|
166
|
-
return !!element && effectiveScopeElement.contains(element);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Save annotations to sessionStorage.
|
|
171
|
-
* Uses HighlightCoordinator's exportAnnotations for proper serialization.
|
|
172
|
-
*/
|
|
173
|
-
function saveAnnotations() {
|
|
174
|
-
if (!isBrowser || !highlightCoordinator) return;
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const root = getEffectiveRoot();
|
|
178
|
-
const serialized = highlightCoordinator.exportAnnotations(root);
|
|
179
|
-
sessionStorage.setItem(getStorageKey(), JSON.stringify(serialized));
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.error('[AnnotationToolbar] Failed to save annotations:', error);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Load annotations from sessionStorage.
|
|
187
|
-
* Uses HighlightCoordinator's importAnnotations for proper deserialization.
|
|
188
|
-
*/
|
|
189
|
-
function loadAnnotations() {
|
|
190
|
-
if (!isBrowser || !highlightCoordinator) return;
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const json = sessionStorage.getItem(getStorageKey());
|
|
194
|
-
if (!json) return;
|
|
195
|
-
|
|
196
|
-
const data = JSON.parse(json);
|
|
197
|
-
const root = getEffectiveRoot();
|
|
198
|
-
const restored = highlightCoordinator.importAnnotations(data, root);
|
|
199
|
-
|
|
200
|
-
console.log(`[AnnotationToolbar] Restored ${restored} annotations`);
|
|
201
|
-
annotationCount = highlightCoordinator.getAnnotations().length;
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error('[AnnotationToolbar] Failed to load annotations:', error);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Handle selection change - show toolbar if valid selection
|
|
209
|
-
*/
|
|
210
|
-
function handleSelectionChange() {
|
|
211
|
-
if (!enabled || !isBrowser) return;
|
|
212
|
-
|
|
213
|
-
const sel = window.getSelection();
|
|
214
|
-
if (!sel || sel.rangeCount === 0) return hideToolbar();
|
|
215
|
-
|
|
216
|
-
const range = sel.getRangeAt(0);
|
|
217
|
-
const text = sel.toString().trim();
|
|
218
|
-
|
|
219
|
-
// Hide if empty or in disallowed area
|
|
220
|
-
if (!text || !isWithinScope(range) || !isInAllowedArea(range.commonAncestorContainer)) {
|
|
221
|
-
return hideToolbar();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Calculate position
|
|
225
|
-
const rect = range.getBoundingClientRect();
|
|
226
|
-
const x = rect.left + rect.width / 2;
|
|
227
|
-
const y = rect.top - 8;
|
|
228
|
-
|
|
229
|
-
// Check if selection overlaps with an existing annotation
|
|
230
|
-
overlappingAnnotationId = findOverlappingAnnotation(range);
|
|
231
|
-
|
|
232
|
-
toolbarState.isVisible = true;
|
|
233
|
-
toolbarState.selectedText = text;
|
|
234
|
-
toolbarState.selectedRange = range.cloneRange();
|
|
235
|
-
toolbarState.toolbarPosition = { x, y };
|
|
236
|
-
|
|
237
|
-
// Announce to screen readers
|
|
238
|
-
const textPreview = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
|
239
|
-
positionAnnouncement = `Annotation toolbar opened for "${textPreview}"`;
|
|
240
|
-
setTimeout(() => { positionAnnouncement = ''; }, 2000);
|
|
241
|
-
|
|
242
|
-
// Set justShown flag to prevent immediate hiding
|
|
243
|
-
justShown = true;
|
|
244
|
-
setTimeout(() => {
|
|
245
|
-
justShown = false;
|
|
246
|
-
}, 100);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Hide toolbar and clean up TTS
|
|
251
|
-
*/
|
|
252
|
-
function hideToolbar() {
|
|
253
|
-
if (ttsSpeaking && ttsService) {
|
|
254
|
-
ttsService.stop();
|
|
255
|
-
ttsSpeaking = false;
|
|
256
|
-
}
|
|
257
|
-
toolbarState.isVisible = false;
|
|
258
|
-
toolbarState.selectedText = '';
|
|
259
|
-
toolbarState.selectedRange = null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Add highlight annotation
|
|
264
|
-
*/
|
|
265
|
-
function handleHighlight(color: HighlightColor) {
|
|
266
|
-
if (!toolbarState.selectedRange || !highlightCoordinator) return;
|
|
267
|
-
const text = toolbarState.selectedText;
|
|
268
|
-
highlightCoordinator.addAnnotation(toolbarState.selectedRange, color);
|
|
269
|
-
annotationCount = highlightCoordinator.getAnnotations().length;
|
|
270
|
-
saveAnnotations();
|
|
271
|
-
|
|
272
|
-
// Announce to screen readers
|
|
273
|
-
const colorName = color === HighlightColor.UNDERLINE ? 'underlined' : `highlighted in ${color}`;
|
|
274
|
-
const textPreview = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
|
275
|
-
positionAnnouncement = `"${textPreview}" ${colorName}`;
|
|
276
|
-
setTimeout(() => { positionAnnouncement = ''; }, 3000);
|
|
277
|
-
|
|
278
|
-
hideToolbar();
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Remove the annotation that overlaps with current selection
|
|
283
|
-
*/
|
|
284
|
-
function handleRemoveAnnotation() {
|
|
285
|
-
if (!overlappingAnnotationId || !highlightCoordinator) {
|
|
286
|
-
console.warn('[AnnotationToolbar] No overlapping annotation to remove');
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
console.log('[AnnotationToolbar] Removing annotation:', overlappingAnnotationId);
|
|
291
|
-
|
|
292
|
-
const annotation = highlightCoordinator.getAnnotation(overlappingAnnotationId);
|
|
293
|
-
if (!annotation) {
|
|
294
|
-
console.warn('[AnnotationToolbar] Annotation not found:', overlappingAnnotationId);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const text = annotation.range.toString();
|
|
299
|
-
highlightCoordinator.removeAnnotation(overlappingAnnotationId);
|
|
300
|
-
const newCount = highlightCoordinator.getAnnotations().length;
|
|
301
|
-
annotationCount = newCount;
|
|
302
|
-
console.log('[AnnotationToolbar] Annotations remaining:', newCount);
|
|
303
|
-
saveAnnotations();
|
|
304
|
-
|
|
305
|
-
// Announce to screen readers
|
|
306
|
-
const textPreview = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
|
307
|
-
positionAnnouncement = `Removed annotation from "${textPreview}"`;
|
|
308
|
-
setTimeout(() => { positionAnnouncement = ''; }, 3000);
|
|
309
|
-
|
|
310
|
-
hideToolbar();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Clear all annotations
|
|
315
|
-
*/
|
|
316
|
-
function handleClearAnnotations() {
|
|
317
|
-
const count = annotationCount;
|
|
318
|
-
highlightCoordinator?.clearAnnotations();
|
|
319
|
-
annotationCount = 0;
|
|
320
|
-
sessionStorage.removeItem(getStorageKey());
|
|
321
|
-
|
|
322
|
-
// Announce to screen readers
|
|
323
|
-
positionAnnouncement = `${count} annotation${count === 1 ? '' : 's'} cleared`;
|
|
324
|
-
setTimeout(() => { positionAnnouncement = ''; }, 3000);
|
|
325
|
-
|
|
326
|
-
hideToolbar();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Read aloud with TTS
|
|
331
|
-
*/
|
|
332
|
-
async function handleTTSClick() {
|
|
333
|
-
if (!toolbarState.selectedRange || !ttsService) return;
|
|
334
|
-
|
|
335
|
-
ttsSpeaking = true;
|
|
336
|
-
try {
|
|
337
|
-
console.log('[AnnotationToolbar] Speaking range:', toolbarState.selectedRange.toString().substring(0, 50));
|
|
338
|
-
|
|
339
|
-
// Use speakRange for accurate word highlighting
|
|
340
|
-
// Note: TTS service should already be initialized by ToolkitCoordinator
|
|
341
|
-
await ttsService.speakRange(toolbarState.selectedRange, {
|
|
342
|
-
contentRoot: getEffectiveRoot()
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
console.log('[AnnotationToolbar] TTS completed successfully');
|
|
346
|
-
} catch (error) {
|
|
347
|
-
console.error('[AnnotationToolbar] TTS error:', error);
|
|
348
|
-
alert(`TTS failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
349
|
-
} finally {
|
|
350
|
-
ttsSpeaking = false;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Handle keyboard shortcuts
|
|
356
|
-
*/
|
|
357
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
358
|
-
if (e.key === 'Escape' && toolbarState.isVisible) {
|
|
359
|
-
e.preventDefault();
|
|
360
|
-
hideToolbar();
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Handle click outside toolbar
|
|
366
|
-
*/
|
|
367
|
-
function handleDocumentClick(e: Event) {
|
|
368
|
-
if (!toolbarState.isVisible || justShown) return;
|
|
369
|
-
|
|
370
|
-
if (toolbarElement && !toolbarElement.contains(e.target as Node)) {
|
|
371
|
-
hideToolbar();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Effect for event listeners and initialization
|
|
376
|
-
$effect(() => {
|
|
377
|
-
if (!isBrowser) return;
|
|
378
|
-
|
|
379
|
-
// Load persisted annotations after a delay to ensure content is rendered
|
|
380
|
-
// PIE section player needs time to render items before we can restore ranges
|
|
381
|
-
// Increased from 500ms to 2000ms to ensure all content is fully loaded
|
|
382
|
-
const loadTimer = setTimeout(() => {
|
|
383
|
-
loadAnnotations();
|
|
384
|
-
}, 2000);
|
|
385
|
-
|
|
386
|
-
const pointerEventTarget: HTMLElement | Document = effectiveScopeElement || document;
|
|
387
|
-
pointerEventTarget.addEventListener('mouseup', handleSelectionChange);
|
|
388
|
-
pointerEventTarget.addEventListener('click', handleDocumentClick);
|
|
389
|
-
pointerEventTarget.addEventListener('touchend', handleSelectionChange);
|
|
390
|
-
pointerEventTarget.addEventListener('touchstart', handleDocumentClick);
|
|
391
|
-
|
|
392
|
-
// Keyboard and scroll events
|
|
393
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
394
|
-
window.addEventListener('scroll', hideToolbar, true);
|
|
395
|
-
|
|
396
|
-
return () => {
|
|
397
|
-
clearTimeout(loadTimer);
|
|
398
|
-
|
|
399
|
-
pointerEventTarget.removeEventListener('mouseup', handleSelectionChange);
|
|
400
|
-
pointerEventTarget.removeEventListener('click', handleDocumentClick);
|
|
401
|
-
pointerEventTarget.removeEventListener('touchend', handleSelectionChange);
|
|
402
|
-
pointerEventTarget.removeEventListener('touchstart', handleDocumentClick);
|
|
403
|
-
|
|
404
|
-
// Remove keyboard and scroll events
|
|
405
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
406
|
-
window.removeEventListener('scroll', hideToolbar, true);
|
|
407
|
-
};
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
$effect(() => {
|
|
411
|
-
if (!contextHostElement) return;
|
|
412
|
-
const cleanupShell = connectAssessmentToolkitShellContext(
|
|
413
|
-
contextHostElement,
|
|
414
|
-
(value: AssessmentToolkitShellContext) => {
|
|
415
|
-
shellContext = value;
|
|
416
|
-
}
|
|
417
|
-
);
|
|
418
|
-
const cleanupRegion = connectAssessmentToolkitRegionScopeContext(
|
|
419
|
-
contextHostElement,
|
|
420
|
-
(value: AssessmentToolkitRegionScopeContext) => {
|
|
421
|
-
regionScopeContext = value;
|
|
422
|
-
}
|
|
423
|
-
);
|
|
424
|
-
return () => {
|
|
425
|
-
cleanupRegion();
|
|
426
|
-
cleanupShell();
|
|
427
|
-
};
|
|
428
|
-
});
|
|
429
|
-
</script>
|
|
430
|
-
|
|
431
|
-
<div bind:this={contextHostElement} style="display: none;" aria-hidden="true"></div>
|
|
432
|
-
|
|
433
|
-
{#if toolbarState.isVisible}
|
|
434
|
-
<div
|
|
435
|
-
bind:this={toolbarElement}
|
|
436
|
-
class="pie-tool-annotation-toolbar notranslate"
|
|
437
|
-
style={`left:${toolbarState.toolbarPosition.x}px; top:${toolbarState.toolbarPosition.y}px; transform: translate(-50%, -100%);`}
|
|
438
|
-
role="toolbar"
|
|
439
|
-
aria-label="Text annotation toolbar"
|
|
440
|
-
translate="no"
|
|
441
|
-
>
|
|
442
|
-
<!-- Highlight Color Swatches -->
|
|
443
|
-
{#each HIGHLIGHT_COLORS as color}
|
|
444
|
-
<button
|
|
445
|
-
class="pie-tool-annotation-toolbar__highlight-swatch"
|
|
446
|
-
style="background-color: {color.hex};"
|
|
447
|
-
onclick={() => handleHighlight(color.name)}
|
|
448
|
-
aria-label={color.label}
|
|
449
|
-
title={color.label}
|
|
450
|
-
>
|
|
451
|
-
<span class="pie-sr-only">{color.label}</span>
|
|
452
|
-
</button>
|
|
453
|
-
{/each}
|
|
454
|
-
|
|
455
|
-
<!-- Underline Button -->
|
|
456
|
-
<button
|
|
457
|
-
class="pie-tool-annotation-toolbar__button pie-tool-annotation-toolbar__button--icon"
|
|
458
|
-
onclick={() => handleHighlight(HighlightColor.UNDERLINE)}
|
|
459
|
-
aria-label="Underline selected text"
|
|
460
|
-
title="Underline"
|
|
461
|
-
>
|
|
462
|
-
<svg
|
|
463
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
464
|
-
viewBox="0 0 24 24"
|
|
465
|
-
width="18"
|
|
466
|
-
height="18"
|
|
467
|
-
fill="currentColor"
|
|
468
|
-
aria-hidden="true"
|
|
469
|
-
>
|
|
470
|
-
<path
|
|
471
|
-
d="M5,21H19V19H5V21M12,17A6,6 0 0,0 18,11V3H15.5V11A3.5,3.5 0 0,1 12,14.5A3.5,3.5 0 0,1 8.5,11V3H6V11A6,6 0 0,0 12,17Z"
|
|
472
|
-
/>
|
|
473
|
-
</svg>
|
|
474
|
-
</button>
|
|
475
|
-
|
|
476
|
-
<!-- Text-to-Speech (only if ttsService available) -->
|
|
477
|
-
{#if ttsService}
|
|
478
|
-
<div class="divider divider-horizontal mx-0 w-px"></div>
|
|
479
|
-
<button
|
|
480
|
-
class="pie-tool-annotation-toolbar__button pie-tool-annotation-toolbar__button--icon"
|
|
481
|
-
onclick={handleTTSClick}
|
|
482
|
-
disabled={ttsSpeaking}
|
|
483
|
-
aria-label="Read selected text aloud"
|
|
484
|
-
title="Read Aloud"
|
|
485
|
-
>
|
|
486
|
-
<svg
|
|
487
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
488
|
-
viewBox="0 0 24 24"
|
|
489
|
-
width="18"
|
|
490
|
-
height="18"
|
|
491
|
-
fill="currentColor"
|
|
492
|
-
aria-hidden="true"
|
|
493
|
-
>
|
|
494
|
-
<path
|
|
495
|
-
d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z"
|
|
496
|
-
/>
|
|
497
|
-
</svg>
|
|
498
|
-
</button>
|
|
499
|
-
{/if}
|
|
500
|
-
|
|
501
|
-
<!-- Divider before Remove/Clear -->
|
|
502
|
-
{#if hasOverlappingAnnotation || hasAnnotations}
|
|
503
|
-
<div class="divider divider-horizontal mx-0 w-px"></div>
|
|
504
|
-
|
|
505
|
-
<!-- Remove This Annotation -->
|
|
506
|
-
{#if hasOverlappingAnnotation}
|
|
507
|
-
<button
|
|
508
|
-
class="pie-tool-annotation-toolbar__button pie-tool-annotation-toolbar__button--warning"
|
|
509
|
-
onclick={handleRemoveAnnotation}
|
|
510
|
-
aria-label="Remove this annotation"
|
|
511
|
-
title="Remove"
|
|
512
|
-
>
|
|
513
|
-
<svg
|
|
514
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
515
|
-
viewBox="0 0 24 24"
|
|
516
|
-
width="18"
|
|
517
|
-
height="18"
|
|
518
|
-
fill="currentColor"
|
|
519
|
-
aria-hidden="true"
|
|
520
|
-
>
|
|
521
|
-
<path
|
|
522
|
-
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
|
|
523
|
-
/>
|
|
524
|
-
</svg>
|
|
525
|
-
</button>
|
|
526
|
-
{/if}
|
|
527
|
-
|
|
528
|
-
<!-- Clear All Annotations -->
|
|
529
|
-
{#if hasAnnotations}
|
|
530
|
-
<button
|
|
531
|
-
class="pie-tool-annotation-toolbar__button pie-tool-annotation-toolbar__button--danger"
|
|
532
|
-
onclick={handleClearAnnotations}
|
|
533
|
-
aria-label="Clear all annotations from document"
|
|
534
|
-
title="Clear All"
|
|
535
|
-
>
|
|
536
|
-
Clear All
|
|
537
|
-
</button>
|
|
538
|
-
{/if}
|
|
539
|
-
{/if}
|
|
540
|
-
</div>
|
|
541
|
-
{/if}
|
|
542
|
-
|
|
543
|
-
<!-- Screen reader announcements -->
|
|
544
|
-
<div role="status" aria-live="polite" aria-atomic="true" class="pie-sr-only">
|
|
545
|
-
{positionAnnouncement}
|
|
546
|
-
</div>
|
|
547
|
-
|
|
548
|
-
<style>
|
|
549
|
-
.pie-tool-annotation-toolbar {
|
|
550
|
-
position: fixed;
|
|
551
|
-
z-index: 4200;
|
|
552
|
-
display: flex;
|
|
553
|
-
gap: 0.25rem;
|
|
554
|
-
padding: 0.5rem;
|
|
555
|
-
border-radius: 0.5rem;
|
|
556
|
-
background: var(--pie-background, #fff);
|
|
557
|
-
color: var(--pie-text, #111827);
|
|
558
|
-
border: 1px solid var(--pie-border, #d1d5db);
|
|
559
|
-
box-shadow: 0 10px 25px -8px rgb(0 0 0 / 0.3);
|
|
560
|
-
user-select: none;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
.pie-tool-annotation-toolbar__highlight-swatch {
|
|
564
|
-
width: 2.5rem;
|
|
565
|
-
height: 2rem;
|
|
566
|
-
border: 2px solid color-mix(in srgb, var(--pie-border-dark, #111827) 20%, transparent);
|
|
567
|
-
border-radius: 0.375rem;
|
|
568
|
-
cursor: pointer;
|
|
569
|
-
transition: all 0.15s ease;
|
|
570
|
-
display: flex;
|
|
571
|
-
align-items: center;
|
|
572
|
-
justify-content: center;
|
|
573
|
-
padding: 0;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
.pie-tool-annotation-toolbar__highlight-swatch:hover {
|
|
577
|
-
transform: scale(1.1);
|
|
578
|
-
border-color: color-mix(in srgb, var(--pie-border-dark, #111827) 45%, transparent);
|
|
579
|
-
box-shadow: 0 2px 8px rgb(0 0 0 / 0.15);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
.pie-tool-annotation-toolbar__highlight-swatch:focus-visible {
|
|
583
|
-
outline: 2px solid var(--pie-button-focus-outline, var(--pie-primary, #3f51b5));
|
|
584
|
-
outline-offset: 2px;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
.pie-tool-annotation-toolbar .divider-horizontal {
|
|
588
|
-
height: auto;
|
|
589
|
-
width: 1px;
|
|
590
|
-
background-color: color-mix(in srgb, var(--pie-border, #d1d5db) 70%, transparent);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/* Screen reader only content */
|
|
594
|
-
.pie-sr-only {
|
|
595
|
-
position: absolute;
|
|
596
|
-
width: 1px;
|
|
597
|
-
height: 1px;
|
|
598
|
-
padding: 0;
|
|
599
|
-
margin: -1px;
|
|
600
|
-
overflow: hidden;
|
|
601
|
-
clip: rect(0, 0, 0, 0);
|
|
602
|
-
white-space: nowrap;
|
|
603
|
-
border-width: 0;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/* Button styling */
|
|
607
|
-
.pie-tool-annotation-toolbar__button {
|
|
608
|
-
display: inline-flex;
|
|
609
|
-
align-items: center;
|
|
610
|
-
justify-content: center;
|
|
611
|
-
gap: 0.35rem;
|
|
612
|
-
padding: 0.4rem 0.55rem;
|
|
613
|
-
border: 1px solid var(--pie-button-border, #d1d5db);
|
|
614
|
-
border-radius: 0.4rem;
|
|
615
|
-
background: var(--pie-button-bg, #fff);
|
|
616
|
-
color: var(--pie-button-color, var(--pie-text, #111827));
|
|
617
|
-
cursor: pointer;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
.pie-tool-annotation-toolbar__button--icon {
|
|
621
|
-
min-width: 2rem;
|
|
622
|
-
min-height: 2rem;
|
|
623
|
-
padding: 0.45rem;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
.pie-tool-annotation-toolbar__button:hover {
|
|
627
|
-
background: var(--pie-button-hover-bg, #f9fafb);
|
|
628
|
-
color: var(--pie-button-hover-color, var(--pie-text, #111827));
|
|
629
|
-
border-color: var(--pie-button-hover-border, #9ca3af);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
.pie-tool-annotation-toolbar__button:focus-visible {
|
|
633
|
-
outline: 2px solid var(--pie-button-focus-outline, var(--pie-primary, #3f51b5));
|
|
634
|
-
outline-offset: 2px;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
.pie-tool-annotation-toolbar__button:disabled {
|
|
638
|
-
opacity: 0.6;
|
|
639
|
-
cursor: not-allowed;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
.pie-tool-annotation-toolbar__button--warning {
|
|
643
|
-
color: var(--pie-missing-icon, #92400e);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
.pie-tool-annotation-toolbar__button--danger {
|
|
647
|
-
color: var(--pie-incorrect-icon, #b91c1c);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
.pie-tool-annotation-toolbar__button svg {
|
|
651
|
-
width: 18px;
|
|
652
|
-
height: 18px;
|
|
653
|
-
}
|
|
654
|
-
</style>
|