@annotorious/core 3.0.0-pre-alpha-49 → 3.0.0-pre-alpha-50
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/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/lifecycle/Lifecycle.ts +13 -9
- package/src/lifecycle/LifecycleEvents.ts +2 -0
- package/src/model/Annotator.ts +101 -3
- package/src/model/{w3c/W3CAnnotation.ts → W3CAnnotation.ts} +35 -4
- package/src/model/index.ts +2 -2
- package/src/state/Selection.ts +1 -1
- package/src/state/Store.ts +31 -5
- package/src/model/w3c/index.ts +0 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,9 +12,10 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
12
12
|
selectionState: SelectionState<I>,
|
|
13
13
|
hoverState: HoverState<I>,
|
|
14
14
|
viewportState?: ViewportState,
|
|
15
|
-
adapter?: FormatAdapter<I, E
|
|
15
|
+
adapter?: FormatAdapter<I, E>,
|
|
16
|
+
autoSave?: boolean
|
|
16
17
|
) => {
|
|
17
|
-
const observers
|
|
18
|
+
const observers: Map<keyof LifecycleEvents, Function[]> = new Map();
|
|
18
19
|
|
|
19
20
|
// The currently selected annotations, in the state when they were selected
|
|
20
21
|
let initialSelection: I[] = [];
|
|
@@ -23,7 +24,7 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
23
24
|
|
|
24
25
|
let idleTimeout: ReturnType<typeof setTimeout>;
|
|
25
26
|
|
|
26
|
-
const on = <T extends keyof LifecycleEvents
|
|
27
|
+
const on = <T extends keyof LifecycleEvents>(event: T, callback: LifecycleEvents<E>[T]) => {
|
|
27
28
|
if (observers.has(event)) {
|
|
28
29
|
observers.get(event).push(callback);
|
|
29
30
|
} else {
|
|
@@ -40,7 +41,7 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const emit = (event: keyof LifecycleEvents<E>, arg0: I | I[], arg1
|
|
44
|
+
const emit = (event: keyof LifecycleEvents<E>, arg0: I | I[], arg1?: I | PointerEvent) => {
|
|
44
45
|
if (observers.has(event)) {
|
|
45
46
|
setTimeout(() => {
|
|
46
47
|
observers.get(event).forEach(callback => {
|
|
@@ -48,7 +49,8 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
48
49
|
const serialized0 = Array.isArray(arg0) ?
|
|
49
50
|
arg0.map(a => adapter.serialize(a)) : adapter.serialize(arg0);
|
|
50
51
|
|
|
51
|
-
const serialized1
|
|
52
|
+
const serialized1: E | PointerEvent | undefined =
|
|
53
|
+
arg1 ? arg1 instanceof PointerEvent ? arg1 : adapter.serialize(arg1) : undefined;
|
|
52
54
|
|
|
53
55
|
callback(serialized0 as E & E[], serialized1);
|
|
54
56
|
} else {
|
|
@@ -140,11 +142,13 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
140
142
|
emit('viewportIntersect', ids.map(store.getAnnotation)));
|
|
141
143
|
|
|
142
144
|
store.observe(event => {
|
|
143
|
-
//
|
|
144
|
-
if (
|
|
145
|
-
|
|
145
|
+
// autoSave option triggers update events on idleness
|
|
146
|
+
if (autoSave) {
|
|
147
|
+
if (idleTimeout)
|
|
148
|
+
clearTimeout(idleTimeout);
|
|
146
149
|
|
|
147
|
-
|
|
150
|
+
idleTimeout = setTimeout(onIdleUpdate, 1000);
|
|
151
|
+
}
|
|
148
152
|
|
|
149
153
|
// Local CREATE and DELETE events are applied immediately
|
|
150
154
|
const { created, deleted } = event.changes;
|
|
@@ -2,6 +2,8 @@ import type { Annotation } from '../model';
|
|
|
2
2
|
|
|
3
3
|
export interface LifecycleEvents<T extends unknown = Annotation> {
|
|
4
4
|
|
|
5
|
+
clickAnnotation: (annotation: T, originalEvent: PointerEvent) => void;
|
|
6
|
+
|
|
5
7
|
createAnnotation: (annotation: T) => void;
|
|
6
8
|
|
|
7
9
|
deleteAnnotation: (annotation: T) => void;
|
package/src/model/Annotator.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Annotation } from './Annotation';
|
|
2
2
|
import type { User } from './User';
|
|
3
3
|
import type { PresenceProvider } from '../presence';
|
|
4
|
-
import type
|
|
4
|
+
import { Origin, type HoverState, type SelectionState, type Store, type ViewportState } from '../state';
|
|
5
5
|
import type { LifecycleEvents } from '../lifecycle';
|
|
6
6
|
import type { Formatter } from './Formatter';
|
|
7
|
+
import { parseAll, type FormatAdapter } from './FormatAdapter';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Base annotator interface.
|
|
@@ -14,6 +15,8 @@ export interface Annotator<I extends Annotation = Annotation, E extends unknown
|
|
|
14
15
|
|
|
15
16
|
addAnnotation(annotation: E): void;
|
|
16
17
|
|
|
18
|
+
clearAnnotations(): void;
|
|
19
|
+
|
|
17
20
|
getAnnotationById(id: string): E | undefined;
|
|
18
21
|
|
|
19
22
|
getAnnotations(): E[];
|
|
@@ -22,14 +25,20 @@ export interface Annotator<I extends Annotation = Annotation, E extends unknown
|
|
|
22
25
|
|
|
23
26
|
loadAnnotations(url: string): Promise<E[]>;
|
|
24
27
|
|
|
28
|
+
removeAnnotation(arg: E | string): E;
|
|
29
|
+
|
|
25
30
|
setAnnotations(annotations: E[]): void;
|
|
26
31
|
|
|
27
32
|
setFormatter(formatter: Formatter): void;
|
|
28
33
|
|
|
29
|
-
setUser(user: User): void;
|
|
30
|
-
|
|
31
34
|
setPresenceProvider?(provider: PresenceProvider): void;
|
|
32
35
|
|
|
36
|
+
setSelected(arg?: string | string[]): void;
|
|
37
|
+
|
|
38
|
+
setUser(user: User): void;
|
|
39
|
+
|
|
40
|
+
updateAnnotation(annotation: E): E;
|
|
41
|
+
|
|
33
42
|
on<T extends keyof LifecycleEvents<E>>(event: T, callback: LifecycleEvents<E>[T]): void;
|
|
34
43
|
|
|
35
44
|
off<T extends keyof LifecycleEvents<E>>(event: T, callback: LifecycleEvents<E>[T]): void;
|
|
@@ -48,4 +57,93 @@ export interface AnnotatorState<A extends Annotation> {
|
|
|
48
57
|
|
|
49
58
|
viewport: ViewportState;
|
|
50
59
|
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const createBaseAnnotator = <I extends Annotation, E extends unknown>(
|
|
63
|
+
store: Store<I>,
|
|
64
|
+
adapter?: FormatAdapter<I, E>
|
|
65
|
+
) => {
|
|
66
|
+
|
|
67
|
+
const addAnnotation = (annotation: E) => {
|
|
68
|
+
if (adapter) {
|
|
69
|
+
const { parsed, error } = adapter.parse(annotation);
|
|
70
|
+
if (parsed) {
|
|
71
|
+
store.addAnnotation(parsed, Origin.REMOTE);
|
|
72
|
+
} else {
|
|
73
|
+
console.error(error);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
store.addAnnotation(annotation as unknown as I, Origin.REMOTE);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const clearAnnotations = () => store.clear();
|
|
81
|
+
|
|
82
|
+
const getAnnotationById = (id: string): E | undefined => {
|
|
83
|
+
const annotation = store.getAnnotation(id);
|
|
84
|
+
return (adapter && annotation) ?
|
|
85
|
+
adapter.serialize(annotation) as E : annotation as unknown as E;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getAnnotations = () =>
|
|
89
|
+
(adapter ? store.all().map(adapter.serialize) : store.all()) as E[];
|
|
90
|
+
|
|
91
|
+
const loadAnnotations = (url: string) =>
|
|
92
|
+
fetch(url)
|
|
93
|
+
.then((response) => response.json())
|
|
94
|
+
.then((annotations) => {
|
|
95
|
+
setAnnotations(annotations);
|
|
96
|
+
return annotations;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const removeAnnotation = (arg: E | string): E => {
|
|
100
|
+
if (typeof arg === 'string') {
|
|
101
|
+
const annotation = store.getAnnotation(arg);
|
|
102
|
+
store.deleteAnnotation(arg);
|
|
103
|
+
|
|
104
|
+
return adapter ? adapter.serialize(annotation) : annotation as unknown as E;
|
|
105
|
+
} else {
|
|
106
|
+
const annotation = adapter ? adapter.parse(arg).parsed : (arg as unknown as I);
|
|
107
|
+
store.deleteAnnotation(annotation);
|
|
108
|
+
return arg;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const setAnnotations = (annotations: E[]) => {
|
|
113
|
+
if (adapter) {
|
|
114
|
+
const { parsed, failed } = parseAll(adapter)(annotations);
|
|
115
|
+
|
|
116
|
+
if (failed.length > 0)
|
|
117
|
+
console.warn(`Discarded ${failed.length} invalid annotations`, failed);
|
|
118
|
+
|
|
119
|
+
store.bulkAddAnnotation(parsed, true, Origin.REMOTE);
|
|
120
|
+
} else {
|
|
121
|
+
store.bulkAddAnnotation(annotations as unknown as I[], true, Origin.REMOTE);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const updateAnnotation = (updated: E): E => {
|
|
126
|
+
if (adapter) {
|
|
127
|
+
const crosswalked = adapter.parse(updated).parsed;
|
|
128
|
+
const previous = adapter.serialize(store.getAnnotation(crosswalked.id));
|
|
129
|
+
store.updateAnnotation(crosswalked);
|
|
130
|
+
return previous;
|
|
131
|
+
} else {
|
|
132
|
+
const previous = store.getAnnotation((updated as unknown as I).id);
|
|
133
|
+
store.updateAnnotation(updated as unknown as I);
|
|
134
|
+
return previous as unknown as E;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
addAnnotation,
|
|
140
|
+
clearAnnotations,
|
|
141
|
+
getAnnotationById,
|
|
142
|
+
getAnnotations,
|
|
143
|
+
loadAnnotations,
|
|
144
|
+
removeAnnotation,
|
|
145
|
+
setAnnotations,
|
|
146
|
+
updateAnnotation
|
|
147
|
+
}
|
|
148
|
+
|
|
51
149
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { AnnotationBody } from '../Annotation';
|
|
1
|
+
import type { AnnotationBody } from './Annotation';
|
|
3
2
|
|
|
4
3
|
export interface W3CAnnotation {
|
|
5
4
|
|
|
@@ -19,6 +18,8 @@ export interface W3CAnnotation {
|
|
|
19
18
|
|
|
20
19
|
export interface W3CAnnotationBody {
|
|
21
20
|
|
|
21
|
+
id?: string;
|
|
22
|
+
|
|
22
23
|
type?: string;
|
|
23
24
|
|
|
24
25
|
purpose?: string;
|
|
@@ -29,6 +30,8 @@ export interface W3CAnnotationBody {
|
|
|
29
30
|
|
|
30
31
|
creator?: {
|
|
31
32
|
|
|
33
|
+
type?: string;
|
|
34
|
+
|
|
32
35
|
id: string;
|
|
33
36
|
|
|
34
37
|
name?: string;
|
|
@@ -54,14 +57,34 @@ export interface W3CSelector {
|
|
|
54
57
|
value: string;
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
// https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript
|
|
61
|
+
const hashCode = (obj: Object): string => {
|
|
62
|
+
const str = JSON.stringify(obj);
|
|
63
|
+
|
|
64
|
+
let hash = 0;
|
|
65
|
+
|
|
66
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
67
|
+
let chr = str.charCodeAt(i);
|
|
68
|
+
hash = (hash << 5) - hash + chr;
|
|
69
|
+
hash |= 0; // Convert to 32bit integer
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `${hash}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
/**
|
|
58
76
|
* Helper to crosswalk the W3C annotation body to a list of core AnnotationBody objects.
|
|
59
77
|
*/
|
|
60
|
-
export const
|
|
78
|
+
export const parseW3CBodies = (
|
|
61
79
|
body: W3CAnnotationBody | W3CAnnotationBody[],
|
|
62
80
|
annotationId: string
|
|
63
81
|
): AnnotationBody[] => (Array.isArray(body) ? body : [body]).map(b => ({
|
|
64
|
-
|
|
82
|
+
// The internal model strictly requires IDs. (Because multi-user scenarios
|
|
83
|
+
// will have problems without them.) In the W3C model, bodys *may* have IDs.
|
|
84
|
+
// We'll create ad-hoc IDs for bodies without IDs, but want to make sure that
|
|
85
|
+
// generating the ID is idempotent: the same body should always get the same ID.
|
|
86
|
+
// This will avoid unexpected results when checking for equality.
|
|
87
|
+
id: b.id || hashCode(b),
|
|
65
88
|
annotation: annotationId,
|
|
66
89
|
type: b.type,
|
|
67
90
|
purpose: b.purpose,
|
|
@@ -73,3 +96,11 @@ export const parseBodies = (
|
|
|
73
96
|
}));
|
|
74
97
|
|
|
75
98
|
|
|
99
|
+
export const serializeW3CBodies = (bodies: AnnotationBody[]): W3CAnnotationBody[] =>
|
|
100
|
+
bodies.map(b => {
|
|
101
|
+
const w3c = { ...b };
|
|
102
|
+
delete w3c.annotation;
|
|
103
|
+
delete w3c.id;
|
|
104
|
+
return w3c;
|
|
105
|
+
});
|
|
106
|
+
|
package/src/model/index.ts
CHANGED
package/src/state/Selection.ts
CHANGED
|
@@ -26,7 +26,7 @@ const EMPTY: Selection = { selected: [] };
|
|
|
26
26
|
|
|
27
27
|
export const createSelectionState = <T extends Annotation>(
|
|
28
28
|
store: Store<T>,
|
|
29
|
-
selectAction: PointerSelectAction | ((a: Annotation) => PointerSelectAction)
|
|
29
|
+
selectAction: PointerSelectAction | ((a: Annotation) => PointerSelectAction) = PointerSelectAction.EDIT
|
|
30
30
|
) => {
|
|
31
31
|
const { subscribe, set } = writable<Selection>(EMPTY);
|
|
32
32
|
|
package/src/state/Store.ts
CHANGED
|
@@ -8,6 +8,8 @@ type AnnotationBodyIdentifier = { id: string, annotation: string };
|
|
|
8
8
|
|
|
9
9
|
export type Store<T extends Annotation> = ReturnType<typeof createStore<T>>;
|
|
10
10
|
|
|
11
|
+
const isAnnotation = <T extends Annotation>(arg: any): arg is T => arg.id !== undefined;
|
|
12
|
+
|
|
11
13
|
export const createStore = <T extends Annotation>() => {
|
|
12
14
|
|
|
13
15
|
const annotationIndex = new Map<string, T>();
|
|
@@ -55,17 +57,30 @@ export const createStore = <T extends Annotation>() => {
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
const updateAnnotation = (
|
|
59
|
-
const
|
|
60
|
+
const updateAnnotation = (arg1: string | T, arg2: T | Origin = Origin.LOCAL, arg3 = Origin.LOCAL) => {
|
|
61
|
+
const origin: Origin = isAnnotation(arg2) ? arg3 : arg2;
|
|
62
|
+
|
|
63
|
+
const updated: T = typeof arg1 === 'string' ? arg2 as T : arg1;
|
|
64
|
+
|
|
65
|
+
const oldId: string = typeof arg1 === 'string' ? arg1 : arg1.id;
|
|
66
|
+
const oldValue = annotationIndex.get(oldId);
|
|
60
67
|
|
|
61
68
|
if (oldValue) {
|
|
62
|
-
const update: Update<T> = diffAnnotations(oldValue,
|
|
69
|
+
const update: Update<T> = diffAnnotations(oldValue, updated);
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
if (oldId === updated.id) {
|
|
72
|
+
annotationIndex.set(oldId, updated);
|
|
73
|
+
} else {
|
|
74
|
+
annotationIndex.delete(oldId);
|
|
75
|
+
annotationIndex.set(updated.id, updated);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
oldValue.bodies.forEach(b => bodyIndex.delete(b.id));
|
|
79
|
+
updated.bodies.forEach(b => bodyIndex.set(b.id, updated.id));
|
|
65
80
|
|
|
66
81
|
emit(origin, { updated: [update] })
|
|
67
82
|
} else {
|
|
68
|
-
throw Error(`Cannot update annotation ${
|
|
83
|
+
throw Error(`Cannot update annotation ${oldId} - does not exist`);
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
|
|
@@ -93,6 +108,11 @@ export const createStore = <T extends Annotation>() => {
|
|
|
93
108
|
|
|
94
109
|
const all = () => [...annotationIndex.values()];
|
|
95
110
|
|
|
111
|
+
const clear = () => {
|
|
112
|
+
annotationIndex.clear();
|
|
113
|
+
bodyIndex.clear();
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
const bulkAddAnnotation = (annotations: T[], replace = true, origin = Origin.LOCAL) => {
|
|
97
117
|
if (replace) {
|
|
98
118
|
// Delete existing first
|
|
@@ -218,6 +238,11 @@ export const createStore = <T extends Annotation>() => {
|
|
|
218
238
|
|
|
219
239
|
annotationIndex.set(oldAnnotation.id, newAnnotation);
|
|
220
240
|
|
|
241
|
+
if (oldBody.id !== newBody.id) {
|
|
242
|
+
bodyIndex.delete(oldBody.id);
|
|
243
|
+
bodyIndex.set(newBody.id, newAnnotation.id);
|
|
244
|
+
}
|
|
245
|
+
|
|
221
246
|
return {
|
|
222
247
|
oldValue: oldAnnotation,
|
|
223
248
|
newValue: newAnnotation,
|
|
@@ -283,6 +308,7 @@ export const createStore = <T extends Annotation>() => {
|
|
|
283
308
|
bulkDeleteAnnotation,
|
|
284
309
|
bulkUpdateBodies,
|
|
285
310
|
bulkUpdateTargets,
|
|
311
|
+
clear,
|
|
286
312
|
deleteAnnotation,
|
|
287
313
|
deleteBody,
|
|
288
314
|
getAnnotation,
|
package/src/model/w3c/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './W3CAnnotation';
|