@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/core",
3
- "version": "3.0.0-pre-alpha-49",
3
+ "version": "3.0.0-pre-alpha-50",
4
4
  "description": "Annotorious core types and functions",
5
5
  "author": "Rainer Simon",
6
6
  "license": "BSD-3-Clause",
package/src/index.ts CHANGED
@@ -2,4 +2,4 @@ export * from './lifecycle';
2
2
  export * from './model';
3
3
  export * from './presence';
4
4
  export * from './state';
5
- export * from './utils';
5
+ export * from './utils';
@@ -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 = new Map<string, LifecycleEvents<E>[keyof LifecycleEvents<E>][]>();
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<E>>(event: T, callback: LifecycleEvents<E>[T]) => {
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: I = null) => {
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 = arg1 && adapter.serialize(arg1);
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
- // Idleness update trigger
144
- if (idleTimeout)
145
- clearTimeout(idleTimeout);
145
+ // autoSave option triggers update events on idleness
146
+ if (autoSave) {
147
+ if (idleTimeout)
148
+ clearTimeout(idleTimeout);
146
149
 
147
- idleTimeout = setTimeout(onIdleUpdate, 1000);
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;
@@ -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 { HoverState, SelectionState, Store, ViewportState } from '../state';
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 { v4 as uuidv4 } from 'uuid';
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 parseBodies = (
78
+ export const parseW3CBodies = (
61
79
  body: W3CAnnotationBody | W3CAnnotationBody[],
62
80
  annotationId: string
63
81
  ): AnnotationBody[] => (Array.isArray(body) ? body : [body]).map(b => ({
64
- id: uuidv4(),
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
+
@@ -1,6 +1,6 @@
1
- export * from './w3c';
2
1
  export * from './Annotation';
3
2
  export * from './Annotator';
4
3
  export * from './FormatAdapter';
5
4
  export * from './Formatter';
6
- export * from './User';
5
+ export * from './User';
6
+ export * from './W3CAnnotation';
@@ -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
 
@@ -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 = (annotation: T, origin = Origin.LOCAL) => {
59
- const oldValue = annotationIndex.get(annotation.id);
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, annotation);
69
+ const update: Update<T> = diffAnnotations(oldValue, updated);
63
70
 
64
- annotationIndex.set(annotation.id, annotation);
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 ${annotation.id} - does not exist`);
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,
@@ -1 +0,0 @@
1
- export * from './W3CAnnotation';