@annotorious/core 3.0.0-rc.3 → 3.0.0-rc.4
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/lifecycle/Lifecycle.ts +25 -11
- package/src/model/Annotator.ts +40 -2
- package/src/state/UndoStack.ts +34 -3
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { dequal } from 'dequal/lite';
|
|
2
|
-
import type { Annotation, FormatAdapter } from '../model';
|
|
3
|
-
import { Origin } from '../state';
|
|
4
|
-
import type {
|
|
2
|
+
import type { Annotation, AnnotatorState, FormatAdapter } from '../model';
|
|
3
|
+
import { Origin, type ChangeSet, type UndoStack } from '../state';
|
|
4
|
+
import type { ViewportState } from '../state';
|
|
5
5
|
import type { LifecycleEvents } from './LifecycleEvents';
|
|
6
6
|
|
|
7
7
|
export type Lifecycle<I extends Annotation, E extends unknown> =
|
|
8
8
|
ReturnType<typeof createLifecyleObserver<I, E>>;
|
|
9
9
|
|
|
10
10
|
export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
hoverState: HoverState<I>,
|
|
14
|
-
viewportState?: ViewportState,
|
|
11
|
+
state: AnnotatorState<I>,
|
|
12
|
+
undoStack: UndoStack<I>,
|
|
15
13
|
adapter?: FormatAdapter<I, E>,
|
|
16
14
|
autoSave?: boolean
|
|
17
15
|
) => {
|
|
16
|
+
const { store, selection, hover, viewport } = state;
|
|
17
|
+
|
|
18
18
|
const observers: Map<keyof LifecycleEvents, Function[]> = new Map();
|
|
19
19
|
|
|
20
20
|
// The currently selected annotations, in the state when they were selected
|
|
@@ -62,7 +62,7 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const onIdleUpdate = () => {
|
|
65
|
-
const { selected } =
|
|
65
|
+
const { selected } = selection;
|
|
66
66
|
|
|
67
67
|
// User idle after activity - fire update events for selected
|
|
68
68
|
// annotations that changed
|
|
@@ -81,7 +81,7 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
selection.subscribe(({ selected })=> {
|
|
85
85
|
if (initialSelection.length === 0 && selected.length === 0)
|
|
86
86
|
return;
|
|
87
87
|
|
|
@@ -125,7 +125,7 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
125
125
|
emit('selectionChanged', initialSelection);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
hover.subscribe(id => {
|
|
129
129
|
if (!currentHover && id) {
|
|
130
130
|
emit('mouseEnterAnnotation', store.getAnnotation(id));
|
|
131
131
|
} else if (currentHover && !id) {
|
|
@@ -138,7 +138,7 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
138
138
|
currentHover = id;
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
viewport?.subscribe(ids =>
|
|
142
142
|
emit('viewportIntersect', ids.map(store.getAnnotation)));
|
|
143
143
|
|
|
144
144
|
store.observe(event => {
|
|
@@ -192,6 +192,20 @@ export const createLifecyleObserver = <I extends Annotation, E extends unknown>(
|
|
|
192
192
|
}
|
|
193
193
|
}, { origin: Origin.REMOTE });
|
|
194
194
|
|
|
195
|
+
const onUndoOrRedo = (undo: boolean) => (changes: ChangeSet<I>) => {
|
|
196
|
+
const { created, deleted, updated } = changes;
|
|
197
|
+
created.forEach(a => emit('createAnnotation', a));
|
|
198
|
+
deleted.forEach(a => emit('deleteAnnotation', a));
|
|
199
|
+
|
|
200
|
+
if (undo)
|
|
201
|
+
updated.forEach(t => emit('updateAnnotation', t.oldValue, t.newValue));
|
|
202
|
+
else
|
|
203
|
+
updated.forEach(t => emit('updateAnnotation', t.newValue, t.oldValue));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
undoStack.on('undo', onUndoOrRedo(true));
|
|
207
|
+
undoStack.on('redo', onUndoOrRedo(false));
|
|
208
|
+
|
|
195
209
|
return { on, off, emit }
|
|
196
210
|
|
|
197
211
|
}
|
package/src/model/Annotator.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface Annotator<I extends Annotation = Annotation, E extends unknown
|
|
|
16
16
|
|
|
17
17
|
addAnnotation(annotation: E): void;
|
|
18
18
|
|
|
19
|
+
cancelSelected(): void;
|
|
20
|
+
|
|
21
|
+
canRedo(): boolean;
|
|
22
|
+
|
|
23
|
+
canUndo(): boolean;
|
|
24
|
+
|
|
19
25
|
clearAnnotations(): void;
|
|
20
26
|
|
|
21
27
|
destroy(): void;
|
|
@@ -24,6 +30,8 @@ export interface Annotator<I extends Annotation = Annotation, E extends unknown
|
|
|
24
30
|
|
|
25
31
|
getAnnotations(): E[];
|
|
26
32
|
|
|
33
|
+
getSelected(): E[];
|
|
34
|
+
|
|
27
35
|
getUser(): User;
|
|
28
36
|
|
|
29
37
|
loadAnnotations(url: string): Promise<E[]>;
|
|
@@ -69,11 +77,13 @@ export interface AnnotatorState<A extends Annotation> {
|
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export const createBaseAnnotator = <I extends Annotation, E extends unknown>(
|
|
72
|
-
|
|
73
|
-
undoStack: UndoStack
|
|
80
|
+
state: AnnotatorState<I>,
|
|
81
|
+
undoStack: UndoStack<I>,
|
|
74
82
|
adapter?: FormatAdapter<I, E>
|
|
75
83
|
) => {
|
|
76
84
|
|
|
85
|
+
const { store, selection } = state;
|
|
86
|
+
|
|
77
87
|
const addAnnotation = (annotation: E) => {
|
|
78
88
|
if (adapter) {
|
|
79
89
|
const { parsed, error } = adapter.parse(annotation);
|
|
@@ -87,6 +97,8 @@ export const createBaseAnnotator = <I extends Annotation, E extends unknown>(
|
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
const cancelSelected = () => selection.clear();
|
|
101
|
+
|
|
90
102
|
const clearAnnotations = () => store.clear();
|
|
91
103
|
|
|
92
104
|
const getAnnotationById = (id: string): E | undefined => {
|
|
@@ -98,6 +110,16 @@ export const createBaseAnnotator = <I extends Annotation, E extends unknown>(
|
|
|
98
110
|
const getAnnotations = () =>
|
|
99
111
|
(adapter ? store.all().map(adapter.serialize) : store.all()) as E[];
|
|
100
112
|
|
|
113
|
+
const getSelected = () => {
|
|
114
|
+
const selectedIds = selection.selected?.map(s => s.id) || [];
|
|
115
|
+
|
|
116
|
+
const selected = selectedIds.map(id => store.getAnnotation(id));
|
|
117
|
+
|
|
118
|
+
return adapter
|
|
119
|
+
? selected.map(adapter.serialize)
|
|
120
|
+
: selected as unknown as E[];
|
|
121
|
+
}
|
|
122
|
+
|
|
101
123
|
const loadAnnotations = (url: string) =>
|
|
102
124
|
fetch(url)
|
|
103
125
|
.then((response) => response.json())
|
|
@@ -132,6 +154,14 @@ export const createBaseAnnotator = <I extends Annotation, E extends unknown>(
|
|
|
132
154
|
}
|
|
133
155
|
}
|
|
134
156
|
|
|
157
|
+
const setSelected = (arg?: string | string[]) => {
|
|
158
|
+
if (arg) {
|
|
159
|
+
selection.setSelected(arg);
|
|
160
|
+
} else {
|
|
161
|
+
selection.clear();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
135
165
|
const updateAnnotation = (updated: E): E => {
|
|
136
166
|
if (adapter) {
|
|
137
167
|
const crosswalked = adapter.parse(updated).parsed;
|
|
@@ -145,15 +175,23 @@ export const createBaseAnnotator = <I extends Annotation, E extends unknown>(
|
|
|
145
175
|
}
|
|
146
176
|
}
|
|
147
177
|
|
|
178
|
+
// Note that we don't spread the undoStack - it has a .destroy()
|
|
179
|
+
// method that would likely get overwritten by other Annotator implementations
|
|
180
|
+
// if people are not careful.
|
|
148
181
|
return {
|
|
149
182
|
addAnnotation,
|
|
183
|
+
cancelSelected,
|
|
184
|
+
canRedo: undoStack.canRedo,
|
|
185
|
+
canUndo: undoStack.canUndo,
|
|
150
186
|
clearAnnotations,
|
|
151
187
|
getAnnotationById,
|
|
152
188
|
getAnnotations,
|
|
189
|
+
getSelected,
|
|
153
190
|
loadAnnotations,
|
|
154
191
|
redo: undoStack.redo,
|
|
155
192
|
removeAnnotation,
|
|
156
193
|
setAnnotations,
|
|
194
|
+
setSelected,
|
|
157
195
|
undo: undoStack.undo,
|
|
158
196
|
updateAnnotation
|
|
159
197
|
}
|
package/src/state/UndoStack.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createNanoEvents, type Unsubscribe } from 'nanoevents';
|
|
1
2
|
import type { Annotation } from '../model';
|
|
2
3
|
import type { Store } from './Store';
|
|
3
4
|
import { Origin } from './StoreObserver';
|
|
@@ -8,17 +9,33 @@ import { mergeChanges, type ChangeSet, type StoreChangeEvent, type Update } from
|
|
|
8
9
|
// as a new undo/redo step.
|
|
9
10
|
const DEBOUNCE = 250;
|
|
10
11
|
|
|
11
|
-
export interface UndoStack {
|
|
12
|
+
export interface UndoStack <T extends Annotation> {
|
|
13
|
+
|
|
14
|
+
canRedo(): boolean;
|
|
15
|
+
|
|
16
|
+
canUndo(): boolean;
|
|
12
17
|
|
|
13
18
|
destroy(): void;
|
|
14
19
|
|
|
20
|
+
on<E extends keyof UndoStackEvents<T>>(event: E, callback: UndoStackEvents<T>[E]): Unsubscribe;
|
|
21
|
+
|
|
15
22
|
undo(): void;
|
|
16
23
|
|
|
17
24
|
redo(): void;
|
|
18
25
|
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
export
|
|
28
|
+
export interface UndoStackEvents <T extends Annotation> {
|
|
29
|
+
|
|
30
|
+
redo(change: ChangeSet<T>): void;
|
|
31
|
+
|
|
32
|
+
undo(change: ChangeSet<T>): void;
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const createUndoStack = <T extends Annotation>(store: Store<T>): UndoStack<T> => {
|
|
37
|
+
|
|
38
|
+
const emitter = createNanoEvents<UndoStackEvents<T>>();
|
|
22
39
|
|
|
23
40
|
const changeStack: ChangeSet<T>[] = [];
|
|
24
41
|
|
|
@@ -77,16 +94,20 @@ export const createUndoStack = <T extends Annotation>(store: Store<T>): UndoStac
|
|
|
77
94
|
if (pointer > -1) {
|
|
78
95
|
muteEvents = true;
|
|
79
96
|
|
|
80
|
-
const { created, updated, deleted} = changeStack[pointer];
|
|
97
|
+
const { created, updated, deleted } = changeStack[pointer];
|
|
81
98
|
|
|
82
99
|
undoCreated(created);
|
|
83
100
|
undoUpdated(updated);
|
|
84
101
|
undoDeleted(deleted);
|
|
85
102
|
|
|
103
|
+
emitter.emit('undo', changeStack[pointer]);
|
|
104
|
+
|
|
86
105
|
pointer -= 1;
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
109
|
+
const canUndo = () => pointer > -1;
|
|
110
|
+
|
|
90
111
|
const redo = () => {
|
|
91
112
|
if (changeStack.length - 1 > pointer) {
|
|
92
113
|
muteEvents = true;
|
|
@@ -97,14 +118,24 @@ export const createUndoStack = <T extends Annotation>(store: Store<T>): UndoStac
|
|
|
97
118
|
redoUpdated(updated);
|
|
98
119
|
redoDeleted(deleted);
|
|
99
120
|
|
|
121
|
+
emitter.emit('redo', changeStack[pointer + 1]);
|
|
122
|
+
|
|
100
123
|
pointer += 1;
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
|
|
127
|
+
const canRedo = () => changeStack.length - 1 > pointer;
|
|
128
|
+
|
|
104
129
|
const destroy = () => store.unobserve(onChange);
|
|
105
130
|
|
|
131
|
+
const on = <E extends keyof UndoStackEvents<T>>(event: E, callback: UndoStackEvents<T>[E]) =>
|
|
132
|
+
emitter.on(event, callback);
|
|
133
|
+
|
|
106
134
|
return {
|
|
135
|
+
canRedo,
|
|
136
|
+
canUndo,
|
|
107
137
|
destroy,
|
|
138
|
+
on,
|
|
108
139
|
redo,
|
|
109
140
|
undo
|
|
110
141
|
}
|