@annotorious/core 3.0.0-pre-alpha-43
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 +3 -0
- package/package.json +34 -0
- package/src/index.ts +5 -0
- package/src/lifecycle/Lifecycle.ts +192 -0
- package/src/lifecycle/LifecycleEvents.ts +19 -0
- package/src/lifecycle/index.ts +2 -0
- package/src/model/FormatAdapter.ts +36 -0
- package/src/model/core/Annotation.ts +57 -0
- package/src/model/core/Annotator.ts +47 -0
- package/src/model/core/Formatter.ts +19 -0
- package/src/model/core/User.ts +19 -0
- package/src/model/core/index.ts +4 -0
- package/src/model/index.ts +3 -0
- package/src/model/w3c/W3CAnnotation.ts +73 -0
- package/src/model/w3c/index.ts +1 -0
- package/src/presence/Appearance.ts +9 -0
- package/src/presence/AppearanceProvider.ts +53 -0
- package/src/presence/ColorPalette.ts +14 -0
- package/src/presence/PresenceEvents.ts +9 -0
- package/src/presence/PresenceProvider.ts +7 -0
- package/src/presence/PresenceState.ts +145 -0
- package/src/presence/PresentUser.ts +10 -0
- package/src/presence/index.ts +6 -0
- package/src/state/Hover.ts +34 -0
- package/src/state/Selection.ts +113 -0
- package/src/state/Store.ts +297 -0
- package/src/state/StoreObserver.ts +149 -0
- package/src/state/SvelteStore.ts +38 -0
- package/src/state/Viewport.ts +19 -0
- package/src/state/index.ts +6 -0
- package/src/utils/bodyUtils.ts +23 -0
- package/src/utils/collaboratorUtils.ts +19 -0
- package/src/utils/diffAnnotations.ts +33 -0
- package/src/utils/index.ts +4 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { createNanoEvents, type Unsubscribe } from 'nanoevents';
|
|
3
|
+
import type { User } from '../model';
|
|
4
|
+
import type { PresentUser } from './PresentUser';
|
|
5
|
+
import type { PresenceEvents } from './PresenceEvents';
|
|
6
|
+
import { createDefaultAppearenceProvider } from './AppearanceProvider';
|
|
7
|
+
import type { AppearanceProvider } from './AppearanceProvider';
|
|
8
|
+
|
|
9
|
+
export interface PresenceState {
|
|
10
|
+
|
|
11
|
+
// Get users currently present to this room
|
|
12
|
+
getPresentUsers(): PresentUser[];
|
|
13
|
+
|
|
14
|
+
// Notify of a given present user's activity on the given annotations
|
|
15
|
+
notifyActivity(presenceKey: string, annotationIds: string[]): void;
|
|
16
|
+
|
|
17
|
+
// Add a listener for the given presence event
|
|
18
|
+
on<E extends keyof PresenceEvents>(event: E, callback: PresenceEvents[E]): Unsubscribe;
|
|
19
|
+
|
|
20
|
+
// Initial sync - which users are present under which keys
|
|
21
|
+
syncUsers(state: { presenceKey: string, user: User }[]): void;
|
|
22
|
+
|
|
23
|
+
// Update the selection state for the given prresent user
|
|
24
|
+
updateSelection(presenceKey: string, selection: string[] | null): void;
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isListEqual = (listA: any[], listB: any[]) =>
|
|
29
|
+
listA.every(a => listA.includes(a)) && listB.every(b => listA.includes(b));
|
|
30
|
+
|
|
31
|
+
// This client's presence key
|
|
32
|
+
export const PRESENCE_KEY = nanoid();
|
|
33
|
+
|
|
34
|
+
export const createPresenceState = (
|
|
35
|
+
appearanceProvider: AppearanceProvider = createDefaultAppearenceProvider()
|
|
36
|
+
): PresenceState => {
|
|
37
|
+
|
|
38
|
+
const emitter = createNanoEvents<PresenceEvents>();
|
|
39
|
+
|
|
40
|
+
const presentUsers = new Map<string, PresentUser>();
|
|
41
|
+
|
|
42
|
+
const selectionStates = new Map<string, string[]>();
|
|
43
|
+
|
|
44
|
+
const addUser = (presenceKey: string, user: User) => {
|
|
45
|
+
if (presentUsers.has(presenceKey)) {
|
|
46
|
+
console.warn('Attempt to add user that is already present', presenceKey, user);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const appearance = appearanceProvider.addUser(presenceKey, user);
|
|
51
|
+
|
|
52
|
+
presentUsers.set(presenceKey, {
|
|
53
|
+
...user,
|
|
54
|
+
presenceKey,
|
|
55
|
+
appearance
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const removeUser = (presenceKey: string) => {
|
|
60
|
+
const user = presentUsers.get(presenceKey);
|
|
61
|
+
if (!user) {
|
|
62
|
+
console.warn('Attempt to remove user that is not present', presenceKey);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
appearanceProvider.removeUser(user);
|
|
67
|
+
|
|
68
|
+
presentUsers.delete(presenceKey);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const syncUsers = (state: { presenceKey: string, user: User }[]) => {
|
|
72
|
+
// const keys = new Set(others.map(s => s.presenceKey));
|
|
73
|
+
const keys = new Set(state.map(s => s.presenceKey));
|
|
74
|
+
|
|
75
|
+
// These users need to be added to the presentUsers list
|
|
76
|
+
// const toAdd = others.filter(({ presenceKey }) => !presentUsers.has(presenceKey));
|
|
77
|
+
const toAdd = state.filter(({ presenceKey }) => !presentUsers.has(presenceKey));
|
|
78
|
+
|
|
79
|
+
// These users need to be dropped from the list
|
|
80
|
+
const toRemove = Array.from(presentUsers.values()).filter(presentUser =>
|
|
81
|
+
!keys.has(presentUser.presenceKey));
|
|
82
|
+
|
|
83
|
+
toAdd.forEach(({ presenceKey, user }) => addUser(presenceKey, user));
|
|
84
|
+
|
|
85
|
+
toRemove.forEach(user => {
|
|
86
|
+
const { presenceKey } = user;
|
|
87
|
+
|
|
88
|
+
// If this user has a selection, fire deselect event
|
|
89
|
+
if (selectionStates.has(presenceKey))
|
|
90
|
+
emitter.emit('selectionChange', user, null);
|
|
91
|
+
|
|
92
|
+
removeUser(presenceKey)
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (toAdd.length > 0 || toRemove.length > 0)
|
|
96
|
+
emitter.emit('presence', getPresentUsers());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const notifyActivity = (presenceKey: string, annotationIds: string[]) => {
|
|
100
|
+
const user = presentUsers.get(presenceKey);
|
|
101
|
+
|
|
102
|
+
if (!user) {
|
|
103
|
+
console.warn('Activity notification from user that is not present');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const currentSelection = selectionStates.get(presenceKey);
|
|
108
|
+
|
|
109
|
+
// Was there a selection change we might have missed?
|
|
110
|
+
if (!currentSelection || !isListEqual(currentSelection, annotationIds)) {
|
|
111
|
+
selectionStates.set(presenceKey, annotationIds);
|
|
112
|
+
emitter.emit('selectionChange', user, annotationIds);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const updateSelection = (presenceKey: string, selection: string[] | null) => {
|
|
117
|
+
const from = presentUsers.get(presenceKey);
|
|
118
|
+
if (!from) {
|
|
119
|
+
console.warn('Selection change for user that is not present', presenceKey);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (selection)
|
|
124
|
+
selectionStates.set(presenceKey, selection);
|
|
125
|
+
else
|
|
126
|
+
selectionStates.delete(presenceKey);
|
|
127
|
+
|
|
128
|
+
emitter.emit('selectionChange', from, selection);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const getPresentUsers = () =>
|
|
132
|
+
[...Array.from(presentUsers.values())];
|
|
133
|
+
|
|
134
|
+
const on = <E extends keyof PresenceEvents>(event: E, callback: PresenceEvents[E]) =>
|
|
135
|
+
emitter.on(event, callback);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
getPresentUsers,
|
|
139
|
+
notifyActivity,
|
|
140
|
+
on,
|
|
141
|
+
syncUsers,
|
|
142
|
+
updateSelection
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
import type { Annotation } from '../model';
|
|
3
|
+
import type { Store } from './Store';
|
|
4
|
+
|
|
5
|
+
export type HoverState<T extends Annotation> = ReturnType<typeof createHoverState<T>>;
|
|
6
|
+
|
|
7
|
+
export const createHoverState = <T extends Annotation>(store: Store<T>) => {
|
|
8
|
+
|
|
9
|
+
const { subscribe, set } = writable<string>(null);
|
|
10
|
+
|
|
11
|
+
let currentHover: string = null;
|
|
12
|
+
|
|
13
|
+
subscribe(updated => currentHover = updated);
|
|
14
|
+
|
|
15
|
+
// Track store delete and update events
|
|
16
|
+
store.observe(( { changes }) => {
|
|
17
|
+
if (currentHover) {
|
|
18
|
+
const isDeleted = changes.deleted.some(a => a.id === currentHover);
|
|
19
|
+
if (isDeleted)
|
|
20
|
+
set(null);
|
|
21
|
+
|
|
22
|
+
const updated = changes.updated.find(({ oldValue }) => oldValue.id === currentHover);
|
|
23
|
+
if (updated)
|
|
24
|
+
set(updated.newValue.id);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
get current() { return currentHover },
|
|
30
|
+
subscribe,
|
|
31
|
+
set
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
import type { Annotation } from '../model';
|
|
3
|
+
import type { Store } from './Store';
|
|
4
|
+
|
|
5
|
+
export type Selection = {
|
|
6
|
+
|
|
7
|
+
selected: { id: string, editable?: boolean }[],
|
|
8
|
+
|
|
9
|
+
pointerEvent?: PointerEvent;
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SelectionState<T extends Annotation> = ReturnType<typeof createSelectionState<T>>;
|
|
14
|
+
|
|
15
|
+
export enum PointerSelectAction {
|
|
16
|
+
|
|
17
|
+
EDIT = 'EDIT', // Make annotation target(s) editable on pointer select
|
|
18
|
+
|
|
19
|
+
HIGHLIGHT = 'HIGHLIGHT', // Just hightlight on select, but don't make editable
|
|
20
|
+
|
|
21
|
+
NONE = 'NONE' // Click won't select - annotation is completely inert
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EMPTY: Selection = { selected: [] };
|
|
26
|
+
|
|
27
|
+
export const createSelectionState = <T extends Annotation>(
|
|
28
|
+
store: Store<T>,
|
|
29
|
+
selectAction: PointerSelectAction | ((a: Annotation) => PointerSelectAction)
|
|
30
|
+
) => {
|
|
31
|
+
const { subscribe, set } = writable<Selection>(EMPTY);
|
|
32
|
+
|
|
33
|
+
let currentSelection: Selection = EMPTY;
|
|
34
|
+
|
|
35
|
+
subscribe(updated => currentSelection = updated);
|
|
36
|
+
|
|
37
|
+
const clear = () => set(EMPTY);
|
|
38
|
+
|
|
39
|
+
const isEmpty = () => currentSelection.selected?.length === 0;
|
|
40
|
+
|
|
41
|
+
const isSelected = (annotationOrId: T | string) => {
|
|
42
|
+
if (currentSelection.selected.length === 0)
|
|
43
|
+
return false;
|
|
44
|
+
|
|
45
|
+
const id = typeof annotationOrId === 'string' ? annotationOrId : annotationOrId.id;
|
|
46
|
+
return currentSelection.selected.some(i => i.id === id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// TODO enable CTRL select
|
|
50
|
+
const clickSelect = (id: string, pointerEvent: PointerEvent) => {
|
|
51
|
+
const annotation = store.getAnnotation(id);
|
|
52
|
+
if (annotation) {
|
|
53
|
+
const action = onPointerSelect(annotation, selectAction);
|
|
54
|
+
if (action === PointerSelectAction.EDIT)
|
|
55
|
+
set({ selected: [{ id, editable: true }], pointerEvent });
|
|
56
|
+
else if (action === PointerSelectAction.HIGHLIGHT)
|
|
57
|
+
set({ selected: [{ id }], pointerEvent });
|
|
58
|
+
else
|
|
59
|
+
set({ selected: [], pointerEvent });
|
|
60
|
+
} else {
|
|
61
|
+
console.warn('Invalid selection: ' + id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const setSelected = (idOrIds: string | string[], editable: boolean = true) => {
|
|
66
|
+
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
67
|
+
|
|
68
|
+
// Remove invalid
|
|
69
|
+
const annotations =
|
|
70
|
+
ids.map(id => store.getAnnotation(id)).filter(a => a);
|
|
71
|
+
|
|
72
|
+
set({ selected: annotations.map(({ id }) => ({ id, editable })) });
|
|
73
|
+
|
|
74
|
+
if (annotations.length !== ids.length)
|
|
75
|
+
console.warn('Invalid selection', idOrIds);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const removeFromSelection = (ids: string[]) => {
|
|
79
|
+
if (currentSelection.selected.length === 0)
|
|
80
|
+
return false;
|
|
81
|
+
|
|
82
|
+
const { selected } = currentSelection;
|
|
83
|
+
|
|
84
|
+
// Checks which of the given annotations are actually in the selection
|
|
85
|
+
const toRemove = selected.filter(({ id }) => ids.includes(id))
|
|
86
|
+
|
|
87
|
+
if (toRemove.length > 0)
|
|
88
|
+
set({ selected: selected.filter(({ id }) => !ids.includes(id)) });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Track store delete and update events
|
|
92
|
+
store.observe(({ changes }) =>
|
|
93
|
+
removeFromSelection(changes.deleted.map(a => a.id)));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
clear,
|
|
97
|
+
clickSelect,
|
|
98
|
+
get selected() { return currentSelection ? [...currentSelection.selected ] : null},
|
|
99
|
+
get pointerEvent() { return currentSelection ? currentSelection.pointerEvent : null },
|
|
100
|
+
isEmpty,
|
|
101
|
+
isSelected,
|
|
102
|
+
setSelected,
|
|
103
|
+
subscribe
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const onPointerSelect = (
|
|
109
|
+
annotation: Annotation,
|
|
110
|
+
action?: PointerSelectAction | ((a: Annotation) => PointerSelectAction)
|
|
111
|
+
): PointerSelectAction => (typeof action === 'function') ?
|
|
112
|
+
(action(annotation) || PointerSelectAction.EDIT) :
|
|
113
|
+
(action || PointerSelectAction.EDIT);
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { Annotation, AnnotationBody, AnnotationTarget } from '../model';
|
|
2
|
+
import { diffAnnotations } from '../utils';
|
|
3
|
+
import { Origin, shouldNotify, type Update, type ChangeSet } from './StoreObserver';
|
|
4
|
+
import type { StoreObserver, StoreChangeEvent, StoreObserveOptions } from './StoreObserver';
|
|
5
|
+
|
|
6
|
+
// Shorthand
|
|
7
|
+
type AnnotationBodyIdentifier = { id: string, annotation: string };
|
|
8
|
+
|
|
9
|
+
export type Store<T extends Annotation> = ReturnType<typeof createStore<T>>;
|
|
10
|
+
|
|
11
|
+
export const createStore = <T extends Annotation>() => {
|
|
12
|
+
|
|
13
|
+
const annotationIndex = new Map<string, T>();
|
|
14
|
+
|
|
15
|
+
const bodyIndex = new Map<string, string>();
|
|
16
|
+
|
|
17
|
+
const observers: StoreObserver<T>[] = [];
|
|
18
|
+
|
|
19
|
+
const observe = (onChange: { (event: StoreChangeEvent<T>): void }, options: StoreObserveOptions = {}) =>
|
|
20
|
+
observers.push({ onChange, options });
|
|
21
|
+
|
|
22
|
+
const unobserve = (onChange: { (event: StoreChangeEvent<T>): void }) => {
|
|
23
|
+
const idx = observers.findIndex(observer => observer.onChange == onChange);
|
|
24
|
+
if (idx > -1)
|
|
25
|
+
observers.splice(idx, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const emit = (origin: Origin, changes: ChangeSet<T>) => {
|
|
29
|
+
const event: StoreChangeEvent<T> = {
|
|
30
|
+
origin,
|
|
31
|
+
changes: {
|
|
32
|
+
created: changes.created || [],
|
|
33
|
+
updated: changes.updated || [],
|
|
34
|
+
deleted: changes.deleted || []
|
|
35
|
+
},
|
|
36
|
+
state: [...annotationIndex.values()]
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
observers.forEach(observer => {
|
|
40
|
+
if (shouldNotify(observer, event))
|
|
41
|
+
observer.onChange(event);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const addAnnotation = (annotation: T, origin = Origin.LOCAL) => {
|
|
46
|
+
const existing = annotationIndex.get(annotation.id);
|
|
47
|
+
|
|
48
|
+
if (existing) {
|
|
49
|
+
throw Error(`Cannot add annotation ${annotation.id} - exists already`);
|
|
50
|
+
} else {
|
|
51
|
+
annotationIndex.set(annotation.id, annotation);
|
|
52
|
+
|
|
53
|
+
annotation.bodies.forEach(b => bodyIndex.set(b.id, annotation.id));
|
|
54
|
+
emit(origin, { created: [annotation] });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const updateAnnotation = (annotation: T, origin = Origin.LOCAL) => {
|
|
59
|
+
const oldValue = annotationIndex.get(annotation.id);
|
|
60
|
+
|
|
61
|
+
if (oldValue) {
|
|
62
|
+
const update: Update<T> = diffAnnotations(oldValue, annotation);
|
|
63
|
+
|
|
64
|
+
annotationIndex.set(annotation.id, annotation);
|
|
65
|
+
|
|
66
|
+
emit(origin, { updated: [update] })
|
|
67
|
+
} else {
|
|
68
|
+
throw Error(`Cannot update annotation ${annotation.id} - does not exist`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const addBody = (body: AnnotationBody, origin = Origin.LOCAL) => {
|
|
73
|
+
const oldValue = annotationIndex.get(body.annotation);
|
|
74
|
+
if (oldValue) {
|
|
75
|
+
const newValue = {
|
|
76
|
+
...oldValue,
|
|
77
|
+
bodies: [ ...oldValue.bodies, body ]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
annotationIndex.set(oldValue.id, newValue);
|
|
81
|
+
|
|
82
|
+
bodyIndex.set(body.id, newValue.id);
|
|
83
|
+
|
|
84
|
+
const update: Update<T> = {
|
|
85
|
+
oldValue, newValue, bodiesCreated: [ body ]
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
emit(origin, { updated: [update] });
|
|
89
|
+
} else {
|
|
90
|
+
console.warn(`Attempt to add body to missing annotation: ${body.annotation}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const all = () => [...annotationIndex.values()];
|
|
95
|
+
|
|
96
|
+
const bulkAddAnnotation = (annotations: T[], replace = true, origin = Origin.LOCAL) => {
|
|
97
|
+
if (replace) {
|
|
98
|
+
// Delete existing first
|
|
99
|
+
const deleted = [...annotationIndex.values()];
|
|
100
|
+
annotationIndex.clear();
|
|
101
|
+
bodyIndex.clear();
|
|
102
|
+
|
|
103
|
+
annotations.forEach(annotation => {
|
|
104
|
+
annotationIndex.set(annotation.id, annotation);
|
|
105
|
+
annotation.bodies.forEach(b => bodyIndex.set(b.id, annotation.id));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
emit(origin, { created: annotations, deleted });
|
|
109
|
+
} else {
|
|
110
|
+
// Don't allow overwriting of existing annotations
|
|
111
|
+
const existing = annotations.reduce((all, next) => {
|
|
112
|
+
const existing = annotationIndex.get(next.id);
|
|
113
|
+
return existing ? [...all, existing ] : all;
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
if (existing.length > 0)
|
|
117
|
+
throw Error(`Bulk insert would overwrite the following annotations: ${existing.map(a => a.id).join(', ')}`);
|
|
118
|
+
|
|
119
|
+
annotations.forEach(annotation => {
|
|
120
|
+
annotationIndex.set(annotation.id, annotation);
|
|
121
|
+
annotation.bodies.forEach(b => bodyIndex.set(b.id, annotation.id));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
emit(origin, { created: annotations });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const deleteOneAnnotation = (annotationOrId: T | string) => {
|
|
129
|
+
const id = typeof annotationOrId === 'string' ? annotationOrId : annotationOrId.id;
|
|
130
|
+
|
|
131
|
+
const existing = annotationIndex.get(id);
|
|
132
|
+
if (existing) {
|
|
133
|
+
annotationIndex.delete(id);
|
|
134
|
+
existing.bodies.forEach(b => bodyIndex.delete(b.id));
|
|
135
|
+
return existing;
|
|
136
|
+
} else {
|
|
137
|
+
console.warn(`Attempt to delete missing annotation: ${id}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const deleteAnnotation = (annotationOrId: T | string, origin = Origin.LOCAL) => {
|
|
142
|
+
const deleted = deleteOneAnnotation(annotationOrId);
|
|
143
|
+
if (deleted)
|
|
144
|
+
emit(origin, { deleted: [ deleted ]});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const bulkDeleteAnnotation = (annotationsOrIds: (T | string)[], origin = Origin.LOCAL) => {
|
|
148
|
+
const deleted = annotationsOrIds.reduce((deleted, arg) => {
|
|
149
|
+
const existing = deleteOneAnnotation(arg);
|
|
150
|
+
return existing ? [...deleted, existing] : deleted;
|
|
151
|
+
}, [] as T[]);
|
|
152
|
+
|
|
153
|
+
if (deleted.length > 0)
|
|
154
|
+
emit(origin, { deleted });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const deleteBody = (body: AnnotationBodyIdentifier, origin = Origin.LOCAL) => {
|
|
158
|
+
const oldAnnotation = annotationIndex.get(body.annotation);
|
|
159
|
+
|
|
160
|
+
if (oldAnnotation) {
|
|
161
|
+
const oldBody = oldAnnotation.bodies.find(b => b.id === body.id);
|
|
162
|
+
|
|
163
|
+
if (oldBody) {
|
|
164
|
+
bodyIndex.delete(oldBody.id);
|
|
165
|
+
|
|
166
|
+
const newAnnotation = {
|
|
167
|
+
...oldAnnotation,
|
|
168
|
+
bodies: oldAnnotation.bodies.filter(b => b.id !== body.id)
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
annotationIndex.set(oldAnnotation.id, newAnnotation);
|
|
172
|
+
|
|
173
|
+
const update: Update<T> = {
|
|
174
|
+
oldValue: oldAnnotation, newValue: newAnnotation, bodiesDeleted: [oldBody]
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
emit(origin, { updated: [update] });
|
|
178
|
+
} else {
|
|
179
|
+
console.warn(`Attempt to delete missing body ${body.id} from annotation ${body.annotation}`);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
console.warn(`Attempt to delete body from missing annotation ${body.annotation}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const getAnnotation = (id: string): T | undefined => {
|
|
187
|
+
const a = annotationIndex.get(id);
|
|
188
|
+
return a ? {...a} : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const getBody = (id: string): AnnotationBody | undefined => {
|
|
192
|
+
const annotationId = bodyIndex.get(id);
|
|
193
|
+
if (annotationId) {
|
|
194
|
+
const annotation = getAnnotation(annotationId);
|
|
195
|
+
const body = annotation.bodies.find(b => b.id === id);
|
|
196
|
+
if (body) {
|
|
197
|
+
return body;
|
|
198
|
+
} else {
|
|
199
|
+
console.error(`Store integrity error: body ${id} in index, but not in annotation`);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
console.warn(`Attempt to retrieve missing body: ${id}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const updateOneBody = (oldBodyId: AnnotationBodyIdentifier, newBody: AnnotationBody) => {
|
|
207
|
+
if (oldBodyId.annotation !== newBody.annotation)
|
|
208
|
+
throw 'Annotation integrity violation: annotation ID must be the same when updating bodies';
|
|
209
|
+
|
|
210
|
+
const oldAnnotation = annotationIndex.get(oldBodyId.annotation);
|
|
211
|
+
if (oldAnnotation) {
|
|
212
|
+
const oldBody = oldAnnotation.bodies.find(b => b.id === oldBodyId.id);
|
|
213
|
+
|
|
214
|
+
const newAnnotation = {
|
|
215
|
+
...oldAnnotation,
|
|
216
|
+
bodies: oldAnnotation.bodies.map(b => b.id === oldBody.id ? newBody : b)
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
annotationIndex.set(oldAnnotation.id, newAnnotation);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
oldValue: oldAnnotation,
|
|
223
|
+
newValue: newAnnotation,
|
|
224
|
+
bodiesUpdated: [{ oldBody, newBody }]
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
console.warn(`Attempt to add body to missing annotation ${oldBodyId.annotation}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const updateBody = (oldBodyId: AnnotationBodyIdentifier, newBody: AnnotationBody, origin = Origin.LOCAL) => {
|
|
232
|
+
const update = updateOneBody(oldBodyId, newBody);
|
|
233
|
+
emit(origin, { updated: [ update ]} );
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const bulkUpdateBodies = (bodies: AnnotationBody[], origin = Origin.LOCAL) => {
|
|
237
|
+
const updated = bodies.map(b => updateOneBody({ id: b.id, annotation: b.annotation }, b));
|
|
238
|
+
emit(origin, { updated });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const updateOneTarget = (target: AnnotationTarget): Update<T> => {
|
|
242
|
+
const oldValue = annotationIndex.get(target.annotation);
|
|
243
|
+
|
|
244
|
+
if (oldValue) {
|
|
245
|
+
const newValue = {
|
|
246
|
+
...oldValue,
|
|
247
|
+
target: {
|
|
248
|
+
...oldValue.target,
|
|
249
|
+
...target
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
annotationIndex.set(oldValue.id, newValue);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
oldValue, newValue, targetUpdated: {
|
|
257
|
+
oldTarget: oldValue.target,
|
|
258
|
+
newTarget: target
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
} else {
|
|
262
|
+
console.warn(`Attempt to update target on missing annotation: ${target.annotation}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const updateTarget = (target: AnnotationTarget, origin = Origin.LOCAL) => {
|
|
267
|
+
const update = updateOneTarget(target);
|
|
268
|
+
if (update)
|
|
269
|
+
emit(origin, { updated: [ update ]} );
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const bulkUpdateTargets = (targets: AnnotationTarget[], origin = Origin.LOCAL) => {
|
|
273
|
+
const updated = targets.map(updateOneTarget).filter(val => val);
|
|
274
|
+
if (updated.length > 0)
|
|
275
|
+
emit(origin, { updated });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
addAnnotation,
|
|
280
|
+
addBody,
|
|
281
|
+
all,
|
|
282
|
+
bulkAddAnnotation,
|
|
283
|
+
bulkDeleteAnnotation,
|
|
284
|
+
bulkUpdateBodies,
|
|
285
|
+
bulkUpdateTargets,
|
|
286
|
+
deleteAnnotation,
|
|
287
|
+
deleteBody,
|
|
288
|
+
getAnnotation,
|
|
289
|
+
getBody,
|
|
290
|
+
observe,
|
|
291
|
+
unobserve,
|
|
292
|
+
updateAnnotation,
|
|
293
|
+
updateBody,
|
|
294
|
+
updateTarget
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
}
|