@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.
@@ -0,0 +1,149 @@
1
+ import type { Annotation, AnnotationBody, AnnotationTarget } from '../model';
2
+
3
+ /** Interface for listening to changes in the annotation store **/
4
+ export interface StoreObserver<T extends Annotation> {
5
+
6
+ onChange: { (event: StoreChangeEvent<T>): void };
7
+
8
+ options: StoreObserveOptions;
9
+
10
+ }
11
+
12
+ /** A change event fired when the store state changes **/
13
+ export interface StoreChangeEvent<T extends Annotation> {
14
+
15
+ origin: Origin;
16
+
17
+ changes: ChangeSet<T>;
18
+
19
+ state: T[];
20
+
21
+ }
22
+
23
+ export interface ChangeSet<T extends Annotation> {
24
+
25
+ created?: T[];
26
+
27
+ deleted?: T[];
28
+
29
+ updated?: Update<T>[];
30
+
31
+ }
32
+
33
+ export interface Update<T extends Annotation> {
34
+
35
+ oldValue: T;
36
+
37
+ newValue: T;
38
+
39
+ bodiesCreated?: AnnotationBody[];
40
+
41
+ bodiesDeleted?: AnnotationBody[];
42
+
43
+ bodiesUpdated?: Array<{ oldBody: AnnotationBody, newBody: AnnotationBody }>;
44
+
45
+ targetUpdated?: { oldTarget: AnnotationTarget, newTarget: AnnotationTarget};
46
+
47
+ }
48
+
49
+ /** Options to control which events the observer wants to get notified about **/
50
+ export interface StoreObserveOptions {
51
+
52
+ // Observe changes on targets, bodies or both?
53
+ ignore?: Ignore;
54
+
55
+ // Observe changes on one more specific annotations
56
+ annotations?: string | string[];
57
+
58
+ // Observer changes only for a specific origin
59
+ origin?: Origin
60
+
61
+ }
62
+
63
+ /** Allows the observer to ignore certain event types **/
64
+ export enum Ignore {
65
+
66
+ // Don't notify this observer for changes that involve bodies only
67
+ BODY_ONLY = 'BODY_ONLY',
68
+
69
+ // Don't notify for changes on targets only
70
+ TARGET_ONLY = 'TARGET_ONLY'
71
+
72
+ }
73
+
74
+ /** Allows the observer to listen only for events that originated locally or from a remote source **/
75
+ export enum Origin {
76
+
77
+ LOCAL = 'LOCAL',
78
+
79
+ REMOTE = 'REMOTE'
80
+
81
+ }
82
+
83
+ /** Tests if this observer should be notified about this event **/
84
+ export const shouldNotify = <T extends Annotation>(observer: StoreObserver<T>, event: StoreChangeEvent<T>) => {
85
+ const { changes, origin } = event;
86
+
87
+ const isRelevantOrigin =
88
+ !observer.options.origin || observer.options.origin === origin;
89
+
90
+ if (!isRelevantOrigin)
91
+ return false;
92
+
93
+ if (observer.options.ignore) {
94
+ const { ignore } = observer.options;
95
+
96
+ // Shorthand
97
+ const has = (arg: any[]) => arg?.length > 0;
98
+
99
+ const hasAnnotationChanges =
100
+ has(changes.created) || has(changes.deleted);
101
+
102
+ if (!hasAnnotationChanges) {
103
+ const hasBodyChanges =
104
+ changes.updated?.some(u => has(u.bodiesCreated) || has(u.bodiesDeleted) || has(u.bodiesUpdated));
105
+
106
+ const hasTargetChanges =
107
+ changes.updated?.some(u => u.targetUpdated);
108
+
109
+ if (ignore === Ignore.BODY_ONLY && hasBodyChanges && !hasTargetChanges)
110
+ return false;
111
+
112
+ if (ignore === Ignore.TARGET_ONLY && hasTargetChanges && !hasBodyChanges)
113
+ return false;
114
+ }
115
+ }
116
+
117
+ if (observer.options.annotations) {
118
+ // This observer has a filter set on specific annotations - check affected
119
+ const affectedAnnotations = new Set([
120
+ ...changes.created.map(a => a.id),
121
+ ...changes.deleted.map(a => a.id),
122
+ ...changes.updated.map(({ oldValue }) => oldValue.id)
123
+ ]);
124
+
125
+ const observed = Array.isArray(observer.options.annotations) ?
126
+ observer.options.annotations : [ observer.options.annotations ];
127
+
128
+ return Boolean(observed.find(id => affectedAnnotations.has(id)));
129
+ } else {
130
+ return true;
131
+ }
132
+
133
+ }
134
+
135
+ export const mergeChanges = <T extends Annotation>(event: StoreChangeEvent<T>, toMerge: StoreChangeEvent<T>) => {
136
+ if (event.origin !== toMerge.origin)
137
+ throw 'Cannot merge events from different origins';
138
+
139
+ return {
140
+ origin: toMerge.origin,
141
+ changes: {
142
+ // TODO filter created that were deleted in the same go
143
+ created: [...(event.changes.created || []), ...(toMerge.changes.created || []) ],
144
+ deleted: [...(event.changes.deleted || []), ...(toMerge.changes.deleted || []) ]
145
+ // TODO merge updates
146
+ },
147
+ state: toMerge.state
148
+ }
149
+ }
@@ -0,0 +1,38 @@
1
+ import type { Annotation } from '../model';
2
+ import { createStore } from './Store';
3
+ import type { StoreChangeEvent } from './StoreObserver';
4
+
5
+ type Subscriber = <T extends Annotation>(annotation: T[]) => void;
6
+
7
+ /**
8
+ * A simple wrapper around the event-based store implementation
9
+ * that adds a Svelte shim, for use with the reactive '$' notation.
10
+ * Other frameworks might not actually need this. But it's pretty
11
+ * convenient for everyone using Svelte, as well as for the
12
+ * basic (Svelte-based) Annotorious standard implementation.
13
+ */
14
+ export const createSvelteStore = <T extends Annotation>() => {
15
+
16
+ const store = createStore<T>();
17
+
18
+ const subscribe = (onChange: Subscriber) => {
19
+
20
+ // Register a store observer on behalf of the subscriber
21
+ const shim = (event: StoreChangeEvent<T>) => onChange(event.state);
22
+ store.observe(shim);
23
+
24
+ // Immediately call the subscriber function with the
25
+ // current store value, according to the Svelte contract.
26
+ // https://stackoverflow.com/questions/68220955/how-does-svelte-unsubscribe-actually-work
27
+ onChange(store.all());
28
+
29
+ // Return the unsubscribe function
30
+ return () => store.unobserve(shim);
31
+ }
32
+
33
+ return {
34
+ ...store,
35
+ subscribe
36
+ }
37
+
38
+ }
@@ -0,0 +1,19 @@
1
+ import { writable } from 'svelte/store';
2
+
3
+ export type ViewportState = ReturnType<typeof createViewportState>;
4
+
5
+ export const createViewportState = () => {
6
+
7
+ const { subscribe, set } = writable<string[]>([]);
8
+
9
+ let inViewport: string[] = [];
10
+
11
+ subscribe(updated => inViewport = updated);
12
+
13
+ return {
14
+ get inViewport() { return [...inViewport] },
15
+ subscribe,
16
+ set
17
+ };
18
+
19
+ }
@@ -0,0 +1,6 @@
1
+ export * from './Hover';
2
+ export * from './Selection';
3
+ export * from './Store';
4
+ export * from './StoreObserver';
5
+ export * from './SvelteStore';
6
+ export * from './Viewport';
@@ -0,0 +1,23 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import type { Annotation, AnnotationBody, User } from '../model';
3
+
4
+ export interface HasValue {
5
+
6
+ value: string;
7
+
8
+ [key:string]: any;
9
+
10
+ }
11
+
12
+ export const createBody = (
13
+ annotation: Annotation,
14
+ payload: HasValue,
15
+ created?: Date,
16
+ creator?: User
17
+ ): AnnotationBody => ({
18
+ id: uuidv4(),
19
+ annotation: annotation.id,
20
+ created: created || new Date(),
21
+ creator,
22
+ ...payload
23
+ });
@@ -0,0 +1,19 @@
1
+ import type { Annotation, User } from '../model';
2
+
3
+ /**
4
+ * Returns all users listed as creators or updaters in any parts of this
5
+ * annotation.
6
+ */
7
+ export const getCollaborators = (annotation: Annotation): User[] => {
8
+ const { creator, updatedBy } = annotation.target;
9
+
10
+ const bodyCollaborators = annotation.bodies.reduce((users, body) => (
11
+ [...users, body.creator, body.updatedBy]
12
+ ), [] as User[]);
13
+
14
+ return [
15
+ creator,
16
+ updatedBy,
17
+ ...bodyCollaborators
18
+ ].filter(u => u); // Remove undefined
19
+ }
@@ -0,0 +1,33 @@
1
+ import { dequal } from 'dequal/lite';
2
+ import type { Update } from '../state';
3
+ import type { Annotation } from '../model';
4
+
5
+ const getAddedBodies = (oldValue: Annotation, newValue: Annotation) => {
6
+ const oldBodyIds = new Set(oldValue.bodies.map(b => b.id));
7
+ return newValue.bodies.filter(b => !oldBodyIds.has(b.id));
8
+ }
9
+
10
+ const getRemovedBodies = (oldValue: Annotation, newValue: Annotation) => {
11
+ const newBodyIds = new Set(newValue.bodies.map(b => b.id));
12
+ return oldValue.bodies.filter(b => !newBodyIds.has(b.id));
13
+ }
14
+
15
+ const getChangedBodies = (oldValue: Annotation, newValue: Annotation) =>
16
+ newValue.bodies
17
+ .map(newBody => {
18
+ const oldBody = oldValue.bodies.find(b => b.id === newBody.id);
19
+ return { newBody, oldBody: oldBody && !dequal(oldBody, newBody) ? oldBody : undefined }
20
+ })
21
+ .filter(({ oldBody }) => oldBody);
22
+
23
+ const hasTargetChanged = (oldValue: Annotation, newValue: Annotation) =>
24
+ !dequal(oldValue.target, newValue.target);
25
+
26
+ export const diffAnnotations = <T extends Annotation = Annotation>(oldValue: T, newValue: T): Update<T> => ({
27
+ oldValue,
28
+ newValue,
29
+ bodiesCreated: getAddedBodies(oldValue, newValue),
30
+ bodiesDeleted: getRemovedBodies(oldValue, newValue),
31
+ bodiesUpdated: getChangedBodies(oldValue, newValue),
32
+ targetUpdated: hasTargetChanged(oldValue, newValue) ? { oldTarget: oldValue.target, newTarget: newValue.target } : undefined
33
+ });
@@ -0,0 +1,4 @@
1
+ export * from './bodyUtils';
2
+ export * from './collaboratorUtils';
3
+ export * from './diffAnnotations';
4
+
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />