Skip to content

Commit 266026a

Browse files
committed
Merge branch 'issue-29-highlight-active-annotation' into 'main'
highlight selected annotations and annotations hovered over Closes #30 and #29 See merge request SCDH/tei-processing/seed-frontend-components!4
2 parents 8bf7acb + 755d4c1 commit 266026a

File tree

6 files changed

+127
-14
lines changed

6 files changed

+127
-14
lines changed

src/redux/colorizeText.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import log from "./logging";
1010
export const preferredColorPredicate: string = "https://intertextuality.org/annotation#preferredCssColor";
1111
export const colorPriorityPredicate: string = "https://intertextuality.org/annotation#colorPriority";
1212

13-
export const defaultColor: string = "yellow";
13+
export const visualCategory: string = "background-color";
14+
export const defaultCss: CSSDefinition = { "background-color": "whitesmoke" };
1415

1516
/*
1617
* A thunk for setting the CSS for all annotations. This will update
@@ -32,7 +33,7 @@ export const setCssAnnotationsThunk = () => {
3233
log.debug("setting CSS for annotation : " + annotId);
3334
// get the annotation by ID
3435
const annotation = annots[annotId];
35-
css[annotId] = {};
36+
css[annotId] = { 0: defaultCss };
3637
// collect attributed RDF classes into the tags variable
3738
var tags: any = {}
3839
// an annotation may have multiple predicates with multiple objects
@@ -50,16 +51,13 @@ export const setCssAnnotationsThunk = () => {
5051
tags[clasUri] = ontology[clasUri];
5152
var tag: Predications = ontology[clasUri];
5253
// get the CSS properties: background color
53-
if (!tag.hasOwnProperty(preferredColorPredicate)) {
54-
css[annotId][0] = new CSSStyleDeclaration();
55-
css[annotId][0]["background-color"] = defaultColor;
56-
} else {
54+
if (tag.hasOwnProperty(preferredColorPredicate)) {
5755
var priority: number = 0;
5856
if (tag.hasOwnProperty(colorPriorityPredicate)) {
5957
priority = +tag[colorPriorityPredicate][0].value;
6058
}
6159
css[annotId][priority] = {};
62-
css[annotId][priority]["background-color"] = tag[preferredColorPredicate][0].value;
60+
css[annotId][priority][visualCategory] = tag[preferredColorPredicate][0].value;
6361
}
6462
}
6563
}

src/redux/seed-store.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@ export type SeedDispatch = ((action: Action<"listenerMiddleware/add">) => Unsubs
4646

4747
export const seedListenerMiddleware = createListenerMiddleware();
4848
export const startAppListening = seedListenerMiddleware.startListening.withTypes<SeedState, SeedDispatch>();
49+
export type startAppListeningType = typeof startAppListening;
4950

5051
export const addAppListener = addListener.withTypes<SeedState, SeedDispatch>();

src/redux/store.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { configureStore } from "@reduxjs/toolkit";
22

3-
import { seedListenerMiddleware } from "./seed-store";
3+
import { seedListenerMiddleware, startAppListening } from "./seed-store";
44
import textsReducer from "./textsSlice";
55
import textViewsReducer from "./textViewsSlice";
66
import annotationsReducer from "./annotationsSlice";
77
import ontologyReducer from "./ontologySlice";
88
import synopsisReducer from "./synopsisSlice";
99
import { subscribeAnnotationsCssUpdater, subscribeSegmentsCssOnCssUpdater, subscribeSegmentsCssOnSegmentsUpdater } from "./colorizeText";
10+
import { addTextViewListeners } from "./textViewMiddleware";
1011

1112
/*
1213
* Listener middleware for the store. Without listener middleware,
@@ -35,3 +36,6 @@ export type AppDispatch = typeof store.dispatch;
3536
subscribeAnnotationsCssUpdater(store);
3637
subscribeSegmentsCssOnCssUpdater(store);
3738
subscribeSegmentsCssOnSegmentsUpdater(store);
39+
40+
// add listener middleware
41+
addTextViewListeners(startAppListening);

src/redux/textViewMiddleware.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { startAppListeningType } from "./seed-store";
2+
import { fetchAnnotationsPerSegment, setSegmentsPerAnnotation, TextViewsSlice, AnnotationsPerSegment, SegmentsPerAnnotation } from "./textViewsSlice";
3+
4+
import log from "./logging";
5+
6+
7+
/*
8+
* A function that adds listener middleware to a {SeedStore}.
9+
*
10+
* USAGE: `addTextViewListeners(startAppListening);`
11+
*/
12+
export const addTextViewListeners = (startListening: startAppListeningType) => {
13+
log.debug("adding listener middleware for the text view slice");
14+
15+
// add listener middleware for inverting AnnotationsPerSegment and storing it to SegmentsPerAnnotation
16+
startListening({
17+
//type: "textViews/fetchAnnotationsPerSegment",
18+
actionCreator: fetchAnnotationsPerSegment.fulfilled,
19+
effect: (_action, listenerApi): void => {
20+
log.debug("call to annotations middleware");
21+
const textViewsSlice: TextViewsSlice = listenerApi.getState().textViews;
22+
Object.keys(textViewsSlice).forEach((textViewId: string) => {
23+
const annotsPerSegment: AnnotationsPerSegment = textViewsSlice[textViewId].annotationsPerSegment;
24+
// start with empty object
25+
const segmentsPerAnnot: SegmentsPerAnnotation = {};
26+
Object.keys(annotsPerSegment).filter(segmentId => segmentId.length > 0).forEach((segmentId: string) => {
27+
const annots: Array<string> = annotsPerSegment[segmentId];
28+
annots.forEach((annotId: string) => {
29+
if (segmentsPerAnnot.hasOwnProperty(annotId)) {
30+
segmentsPerAnnot[annotId].push(segmentId);
31+
} else {
32+
segmentsPerAnnot[annotId] = new Array<string>(1).fill(segmentId);
33+
}
34+
});
35+
});
36+
// write to slice
37+
listenerApi.dispatch(setSegmentsPerAnnotation({viewId: textViewId, segmentsPerAnnot: segmentsPerAnnot}));
38+
});
39+
}
40+
});
41+
}

src/redux/textViewsSlice.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export interface TextViewState {
5656
*/
5757
annotationsPerSegment: AnnotationsPerSegment;
5858

59+
/*
60+
* The reverse of `annotationsPerSegment`.
61+
*/
62+
segmentsPerAnnotation: SegmentsPerAnnotation;
63+
5964
/*
6065
* The CSS attributed to an segment on the base of annotations on
6166
* it.
@@ -80,6 +85,15 @@ export interface AnnotationsPerSegment {
8085

8186
}
8287

88+
/*
89+
* The reverse of {AnnotationsPerSegment}.
90+
*/
91+
export interface SegmentsPerAnnotation {
92+
93+
[annotId: string]: Array<string>;
94+
95+
}
96+
8397
/*
8498
* The {TextViewsSlice} is a slice of the redux state for keeping
8599
* the state of a view on a text. The type of
@@ -145,6 +159,7 @@ const textViewsSlice = createSlice({
145159
scrollPosition: null,
146160
annotations: [],
147161
annotationsPerSegment: {},
162+
segmentsPerAnnotation: {},
148163
cssPerSegment: {}
149164
};
150165
},
@@ -167,7 +182,13 @@ const textViewsSlice = createSlice({
167182
setCssForAllSegments: (state, action: PayloadAction<{viewId: string, cssPerSegment: { [segmentId: string]: CSSDefinition }}>) => {
168183
state[action.payload.viewId].cssPerSegment = action.payload.cssPerSegment;
169184
},
170-
185+
/*
186+
* Update the {TextViewState.segmentsPerAnnotation} property
187+
* of a text view.
188+
*/
189+
setSegmentsPerAnnotation: (state, action: PayloadAction<{viewId: string, segmentsPerAnnot: SegmentsPerAnnotation}>) => {
190+
state[action.payload.viewId].segmentsPerAnnotation = action.payload.segmentsPerAnnot;
191+
}
171192
},
172193
extraReducers: (builder) => {
173194
builder.addCase(
@@ -178,6 +199,6 @@ const textViewsSlice = createSlice({
178199
},
179200
});
180201

181-
export const { initTextView, setText, scrolledTo, setCssForAllSegments } = textViewsSlice.actions;
202+
export const { initTextView, setText, scrolledTo, setCssForAllSegments, setSegmentsPerAnnotation } = textViewsSlice.actions;
182203

183204
export default textViewsSlice.reducer;

src/seed-text-view.ts

+52-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { storeConsumerMixin } from './store-consumer-mixin';
77
import { windowMixin, windowStyles } from './window-mixin';
88

99
import { seedTextViewContext } from "./seed-context";
10-
import { addAppListener } from "./redux/seed-store";
10+
import { addAppListener, SeedState } from "./redux/seed-store";
1111
import { initText, setText, TextState } from "./redux/textsSlice";
1212
import { initTextView, setText as setTextViewText, scrolledTo, fetchAnnotationsPerSegment } from "./redux/textViewsSlice";
13+
import { annotationSelected, annotationsPassedBy } from './redux/annotationsSlice';
1314
import { selectAnnotationsAtSegmentThunk, passByAnnotationsAtSegmentThunk } from "./redux/selectAnnotations";
1415
import { CSSDefinition } from './redux/cssTypes';
1516
import { scrolled, syncOthers } from './redux/synopsisSlice';
@@ -18,8 +19,6 @@ import { setScrollTarget } from './redux/synopsisMiddleware';
1819

1920
import log from "./logging";
2021

21-
import { SeedState } from './redux/seed-store';
22-
2322

2423

2524
// define the web component
@@ -47,6 +46,8 @@ export class SeedTextView extends windowMixin(storeConsumerMixin(LitElement)) {
4746
@state()
4847
doc: string | undefined;
4948

49+
protected annotationSelected: string | null = null;
50+
5051
protected docLoaded: boolean = false;
5152

5253
protected msgQueue: Array<any> = [];
@@ -117,6 +118,37 @@ export class SeedTextView extends windowMixin(storeConsumerMixin(LitElement)) {
117118
}
118119
}));
119120

121+
this.store?.dispatch(addAppListener({
122+
actionCreator: annotationSelected,
123+
effect: (_action, listenerApi): void => {
124+
// reset highlighting of previously selected annotation
125+
if (this.annotationSelected) {
126+
this.colorizeAnnotation(listenerApi.getState(), this.annotationSelected, { "border": "none"});
127+
}
128+
// highlight currently selected annotation
129+
this.annotationSelected = listenerApi.getState().annotations.annotationSelected ?? "unknown";
130+
// TODO: make CSS configurable
131+
this.colorizeAnnotation(listenerApi.getState(), this.annotationSelected, { "border": "1px solid red"});
132+
}
133+
}));
134+
135+
this.store?.dispatch(addAppListener({
136+
actionCreator: annotationsPassedBy,
137+
effect: (_action, listenerApi): void => {
138+
// reset highlighting of previously transient annotations:
139+
// general annotation highlighting and transient
140+
// highlighting are bound to the same visual category,
141+
// so we can use the general highlighting for a reset
142+
this.colorizeText(listenerApi.getState().textViews[this.id].cssPerSegment);
143+
// highlight transient annotations
144+
const annotIds: Array<string> = listenerApi.getState().annotations.annotationsTransient;
145+
annotIds.forEach(annotId => {
146+
// TODO: make CSS configurable
147+
this.colorizeAnnotation(listenerApi.getState(), annotId, { "background-color": "silver"});
148+
});
149+
}
150+
}));
151+
120152
// this.storeUnsubscribeListeners.push(subsc);
121153
}
122154

@@ -147,7 +179,7 @@ export class SeedTextView extends windowMixin(storeConsumerMixin(LitElement)) {
147179
}
148180

149181
/*
150-
* A callback for scrolling the text to `scrollTarget` position.
182+
* A callback for scrolling the text to `scrollTarget` position.
151183
*/
152184
scrollTextTo(scrollTarget: string): void {
153185
if (this.iframe) {
@@ -308,6 +340,22 @@ export class SeedTextView extends windowMixin(storeConsumerMixin(LitElement)) {
308340
}
309341
}
310342

343+
/*
344+
* Get the text segements included in an annotation given by
345+
* `annotId` and colorize them with the given CSS.
346+
*/
347+
protected colorizeAnnotation(state: SeedState, annotId: string, css: CSSDefinition): void {
348+
if (state.textViews[this.id].segmentsPerAnnotation.hasOwnProperty(annotId)) {
349+
log.debug("colorizing selected annotation in " + this.id + ": " + annotId);
350+
const segments: Array<string> = state.textViews[this.id].segmentsPerAnnotation[annotId];
351+
const cssPerSegments: { [segmentId: string]: CSSDefinition } = {};
352+
segments.forEach(seg => {
353+
cssPerSegments[seg] = css;
354+
});
355+
this.colorizeText(cssPerSegments);
356+
}
357+
}
358+
311359
static styles: CSSResultGroup = [
312360
windowStyles,
313361
css`

0 commit comments

Comments
 (0)