activeadmin_annotations 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +14 -19
- data/CONTRIBUTING.md +9 -15
- data/GOVERNANCE.md +10 -14
- data/README.md +24 -7
- data/SECURITY.md +14 -16
- data/activeadmin_annotations.gemspec +2 -2
- data/admin/annotation_reviews.rb +14 -6
- data/admin/annotations.rb +33 -13
- data/app/assets/controllers/activeadmin_annotations/annotation_client.js +44 -0
- data/app/assets/controllers/activeadmin_annotations/annotation_list.js +29 -0
- data/app/assets/controllers/activeadmin_annotations/annotator_controller.js +45 -370
- data/app/assets/controllers/activeadmin_annotations/composer.js +179 -0
- data/app/assets/controllers/activeadmin_annotations/highlights.js +76 -0
- data/app/assets/controllers/activeadmin_annotations/selection.js +73 -0
- data/app/assets/javascripts/activeadmin_annotations/annotator_logic.mjs +73 -0
- data/app/assets/stylesheets/activeadmin_annotations.css +15 -0
- data/app/helpers/active_admin/annotations/panel_helper.rb +15 -8
- data/app/models/active_admin/annotations/review.rb +7 -2
- data/app/services/active_admin/annotations/review_service.rb +34 -27
- data/app/views/active_admin/annotations/_panel.html.erb +15 -2
- data/db/migrate/20260617120000_create_activeadmin_annotations_tables.rb +27 -10
- data/db/migrate/20260706150000_add_metadata_json_gin_index_to_annotation_reviews.rb +1 -1
- data/db/migrate/20260710120000_add_panel_query_index_to_annotation_spans.rb +1 -1
- data/lib/activeadmin/annotations/copy_text.rb +0 -1
- data/lib/activeadmin/annotations/engine.rb +16 -1
- data/lib/activeadmin/annotations/exporter.rb +1 -1
- data/lib/activeadmin/annotations/panel_html.rb +21 -0
- data/lib/activeadmin/annotations/review_access.rb +13 -1
- data/lib/activeadmin/annotations/review_panel.rb +9 -6
- data/lib/activeadmin/annotations/span_create.rb +70 -0
- data/lib/activeadmin/annotations/version.rb +1 -1
- data/lib/activeadmin_annotations.rb +2 -0
- metadata +12 -4
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { currentSelection } from "activeadmin_annotations/selection";
|
|
3
|
+
import { savedHighlightOffsetPairs } from "activeadmin_annotations/annotator_logic";
|
|
4
|
+
import {
|
|
5
|
+
HighlightLayer,
|
|
6
|
+
PENDING_HIGHLIGHT_NAME,
|
|
7
|
+
SAVED_HIGHLIGHT_NAME,
|
|
8
|
+
} from "activeadmin_annotations/highlights";
|
|
9
|
+
import { emptyAnnotationListItem, annotationListItem } from "activeadmin_annotations/annotation_list";
|
|
10
|
+
import { AnnotationClient, csrfToken } from "activeadmin_annotations/annotation_client";
|
|
11
|
+
import { Composer } from "activeadmin_annotations/composer";
|
|
5
12
|
|
|
6
13
|
export default class extends Controller {
|
|
7
14
|
static targets = [
|
|
@@ -19,6 +26,11 @@ export default class extends Controller {
|
|
|
19
26
|
static values = {
|
|
20
27
|
reviewId: String,
|
|
21
28
|
field: String,
|
|
29
|
+
subjectType: String,
|
|
30
|
+
subjectId: String,
|
|
31
|
+
context: { type: Object, default: {} },
|
|
32
|
+
contextDigest: String,
|
|
33
|
+
contentRevisionVersion: Number,
|
|
22
34
|
createUrl: String,
|
|
23
35
|
updateUrlTemplate: String,
|
|
24
36
|
categoryLabel: { type: String, default: "Category" },
|
|
@@ -30,7 +42,9 @@ export default class extends Controller {
|
|
|
30
42
|
connect() {
|
|
31
43
|
this.pendingSelection = null;
|
|
32
44
|
this.editingAnnotationId = null;
|
|
33
|
-
this.
|
|
45
|
+
this.highlightLayer = new HighlightLayer(this.annotatableTarget);
|
|
46
|
+
this.annotationClient = new AnnotationClient({ csrfToken: csrfToken() });
|
|
47
|
+
this.composer = new Composer(this);
|
|
34
48
|
this.renderAnnotations();
|
|
35
49
|
this.annotatableTarget.addEventListener("mouseup", this.commitAnnotatableSelection);
|
|
36
50
|
this.annotatableTarget.addEventListener("keyup", this.commitAnnotatableSelection);
|
|
@@ -43,8 +57,7 @@ export default class extends Controller {
|
|
|
43
57
|
this.annotatableTarget.removeEventListener("keyup", this.commitAnnotatableSelection);
|
|
44
58
|
document.removeEventListener("selectionchange", this.handleSelectionChange);
|
|
45
59
|
document.removeEventListener("keydown", this.handleDocumentKeydown);
|
|
46
|
-
this.
|
|
47
|
-
this.clearSavedHighlights();
|
|
60
|
+
this.highlightLayer.disconnect();
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
commitAnnotatableSelection = () => {
|
|
@@ -52,39 +65,15 @@ export default class extends Controller {
|
|
|
52
65
|
|
|
53
66
|
requestAnimationFrame(() => {
|
|
54
67
|
const selection = this.currentSelection();
|
|
55
|
-
if (selection) this.
|
|
68
|
+
if (selection) this.composer.activate(selection);
|
|
56
69
|
});
|
|
57
70
|
};
|
|
58
71
|
|
|
59
72
|
handleSelectionChange = () => {
|
|
60
73
|
if (this.readonlyValue) return;
|
|
61
|
-
if (this.
|
|
74
|
+
if (this.composer.isOpen()) this.composer.ensurePendingHighlight();
|
|
62
75
|
};
|
|
63
76
|
|
|
64
|
-
activateComposer(selection) {
|
|
65
|
-
this.editingAnnotationId = null;
|
|
66
|
-
|
|
67
|
-
const selectionChanged =
|
|
68
|
-
!this.pendingSelection ||
|
|
69
|
-
this.pendingSelection.startOffset !== selection.startOffset ||
|
|
70
|
-
this.pendingSelection.endOffset !== selection.endOffset ||
|
|
71
|
-
this.pendingSelection.selectedText !== selection.selectedText;
|
|
72
|
-
|
|
73
|
-
this.pendingSelection = selection;
|
|
74
|
-
this.selectedPreviewTarget.textContent = selection.selectedText;
|
|
75
|
-
|
|
76
|
-
if (selectionChanged) {
|
|
77
|
-
this.commentInputTarget.value = "";
|
|
78
|
-
this.categoryInputTarget.value = "";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
this.hideError();
|
|
82
|
-
this.setComposerHeading("New annotation");
|
|
83
|
-
this.composerTarget.classList.remove("aa-annotations-composer-hidden");
|
|
84
|
-
this.applyPendingHighlight(selection);
|
|
85
|
-
window.getSelection()?.removeAllRanges();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
77
|
editAnnotation(event) {
|
|
89
78
|
if (this.readonlyValue) return;
|
|
90
79
|
|
|
@@ -92,21 +81,7 @@ export default class extends Controller {
|
|
|
92
81
|
const annotation = this.annotationsValue.find((entry) => String(entry.id) === String(annotationId));
|
|
93
82
|
if (!annotation) return;
|
|
94
83
|
|
|
95
|
-
this.
|
|
96
|
-
this.pendingSelection = {
|
|
97
|
-
selectedText: annotation.selected_text,
|
|
98
|
-
startOffset: Number(annotation.start_offset),
|
|
99
|
-
endOffset: Number(annotation.end_offset),
|
|
100
|
-
};
|
|
101
|
-
this.selectedPreviewTarget.textContent = annotation.selected_text;
|
|
102
|
-
this.commentInputTarget.value = annotation.comment;
|
|
103
|
-
this.categoryInputTarget.value = annotation.category || "";
|
|
104
|
-
this.hideError();
|
|
105
|
-
this.setComposerHeading("Edit annotation");
|
|
106
|
-
this.composerTarget.classList.remove("aa-annotations-composer-hidden");
|
|
107
|
-
this.applyPendingHighlight(this.pendingSelection);
|
|
108
|
-
window.getSelection()?.removeAllRanges();
|
|
109
|
-
this.commentInputTarget.focus();
|
|
84
|
+
this.composer.edit(annotation);
|
|
110
85
|
}
|
|
111
86
|
|
|
112
87
|
async deleteAnnotation(event) {
|
|
@@ -115,369 +90,69 @@ export default class extends Controller {
|
|
|
115
90
|
const annotationId = event.currentTarget.dataset.annotationId;
|
|
116
91
|
const annotation = this.annotationsValue.find((entry) => String(entry.id) === String(annotationId));
|
|
117
92
|
if (!annotation) return;
|
|
118
|
-
|
|
119
93
|
if (!window.confirm("Delete this annotation?")) return;
|
|
120
94
|
|
|
121
|
-
|
|
122
|
-
const response = await fetch(this.annotationUrl(annotationId), {
|
|
123
|
-
method: "DELETE",
|
|
124
|
-
credentials: "same-origin",
|
|
125
|
-
headers: {
|
|
126
|
-
Accept: "application/json",
|
|
127
|
-
"X-CSRF-Token": this.csrfToken(),
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (!response.ok) {
|
|
132
|
-
window.alert("Could not delete annotation.");
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (String(this.editingAnnotationId) === String(annotationId)) this.resetComposer();
|
|
137
|
-
|
|
138
|
-
this.annotationsValue = this.annotationsValue.filter(
|
|
139
|
-
(entry) => String(entry.id) !== String(annotationId)
|
|
140
|
-
);
|
|
141
|
-
this.renderAnnotations();
|
|
142
|
-
} catch (_error) {
|
|
143
|
-
window.alert("Could not delete annotation.");
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
resetComposer() {
|
|
148
|
-
this.pendingSelection = null;
|
|
149
|
-
this.editingAnnotationId = null;
|
|
150
|
-
this.clearPendingHighlight();
|
|
151
|
-
this.selectedPreviewTarget.textContent = "";
|
|
152
|
-
this.commentInputTarget.value = "";
|
|
153
|
-
this.categoryInputTarget.value = "";
|
|
154
|
-
this.hideError();
|
|
155
|
-
this.setComposerHeading("New annotation");
|
|
156
|
-
this.composerTarget.classList.add("aa-annotations-composer-hidden");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
composerIsOpen() {
|
|
160
|
-
return !this.composerTarget.classList.contains("aa-annotations-composer-hidden");
|
|
95
|
+
await this.composer.destroy(annotationId);
|
|
161
96
|
}
|
|
162
97
|
|
|
163
98
|
cancelComposer() {
|
|
164
|
-
this.
|
|
165
|
-
window.getSelection()?.removeAllRanges();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
ensurePendingHighlight() {
|
|
169
|
-
if (!this.pendingSelection) return;
|
|
170
|
-
|
|
171
|
-
this.applyPendingHighlight(this.pendingSelection);
|
|
99
|
+
this.composer.cancel();
|
|
172
100
|
}
|
|
173
101
|
|
|
174
102
|
async submitComposer() {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const comment = this.commentInputTarget.value.trim();
|
|
178
|
-
if (!comment) {
|
|
179
|
-
this.showError("Comment is required.");
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (this.editingAnnotationId) {
|
|
184
|
-
await this.updateAnnotation(comment);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
await this.createAnnotation(comment);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async createAnnotation(comment) {
|
|
192
|
-
const payload = {
|
|
193
|
-
annotation: {
|
|
194
|
-
annotation_review_id: this.reviewIdValue,
|
|
195
|
-
field_name: this.fieldValue,
|
|
196
|
-
selected_text: this.pendingSelection.selectedText,
|
|
197
|
-
start_offset: this.pendingSelection.startOffset,
|
|
198
|
-
end_offset: this.pendingSelection.endOffset,
|
|
199
|
-
comment: comment,
|
|
200
|
-
category: this.categoryInputTarget.value || null,
|
|
201
|
-
context_paths_json: [],
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const response = await fetch(this.createUrlValue, {
|
|
207
|
-
method: "POST",
|
|
208
|
-
credentials: "same-origin",
|
|
209
|
-
headers: {
|
|
210
|
-
Accept: "application/json",
|
|
211
|
-
"Content-Type": "application/json",
|
|
212
|
-
"X-CSRF-Token": this.csrfToken(),
|
|
213
|
-
},
|
|
214
|
-
body: JSON.stringify(payload),
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (!response.ok) {
|
|
218
|
-
this.showError("Could not save annotation.");
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const annotation = await response.json();
|
|
223
|
-
this.resetComposer();
|
|
224
|
-
this.annotationsValue = [...this.annotationsValue, this.normalizeAnnotation(annotation)];
|
|
225
|
-
this.renderAnnotations();
|
|
226
|
-
window.getSelection()?.removeAllRanges();
|
|
227
|
-
} catch (_error) {
|
|
228
|
-
this.showError("Could not save annotation.");
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async updateAnnotation(comment) {
|
|
233
|
-
const payload = {
|
|
234
|
-
annotation: {
|
|
235
|
-
comment: comment,
|
|
236
|
-
category: this.categoryInputTarget.value || null,
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const response = await fetch(this.annotationUrl(this.editingAnnotationId), {
|
|
242
|
-
method: "PATCH",
|
|
243
|
-
credentials: "same-origin",
|
|
244
|
-
headers: {
|
|
245
|
-
Accept: "application/json",
|
|
246
|
-
"Content-Type": "application/json",
|
|
247
|
-
"X-CSRF-Token": this.csrfToken(),
|
|
248
|
-
},
|
|
249
|
-
body: JSON.stringify(payload),
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
if (!response.ok) {
|
|
253
|
-
this.showError("Could not update annotation.");
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const annotation = await response.json();
|
|
258
|
-
this.resetComposer();
|
|
259
|
-
this.annotationsValue = this.annotationsValue.map((entry) =>
|
|
260
|
-
String(entry.id) === String(annotation.id) ? this.normalizeAnnotation(annotation) : entry
|
|
261
|
-
);
|
|
262
|
-
this.renderAnnotations();
|
|
263
|
-
} catch (_error) {
|
|
264
|
-
this.showError("Could not update annotation.");
|
|
265
|
-
}
|
|
103
|
+
await this.composer.submit();
|
|
266
104
|
}
|
|
267
105
|
|
|
268
106
|
handleDocumentKeydown = (event) => {
|
|
269
107
|
if (event.key !== "Escape") return;
|
|
270
|
-
if (!this.
|
|
108
|
+
if (!this.composer.isOpen()) return;
|
|
271
109
|
|
|
272
|
-
this.
|
|
110
|
+
this.composer.cancel();
|
|
273
111
|
};
|
|
274
112
|
|
|
275
113
|
currentSelection() {
|
|
276
|
-
|
|
277
|
-
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;
|
|
278
|
-
|
|
279
|
-
const range = selection.getRangeAt(0);
|
|
280
|
-
if (!this.selectionInsideAnnotatable(range)) return null;
|
|
281
|
-
|
|
282
|
-
const selectedText = selection.toString().trim();
|
|
283
|
-
if (!selectedText) return null;
|
|
284
|
-
|
|
285
|
-
const startOffset = this.offsetWithinAnnotatable(range.startContainer, range.startOffset);
|
|
286
|
-
const endOffset = this.offsetWithinAnnotatable(range.endContainer, range.endOffset);
|
|
287
|
-
if (startOffset === null || endOffset === null || endOffset <= startOffset) return null;
|
|
288
|
-
|
|
289
|
-
return { selectedText, startOffset, endOffset };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
selectionInsideAnnotatable(range) {
|
|
293
|
-
const startElement = this.elementForNode(range.startContainer);
|
|
294
|
-
const endElement = this.elementForNode(range.endContainer);
|
|
295
|
-
return this.annotatableTarget.contains(startElement) && this.annotatableTarget.contains(endElement);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
elementForNode(node) {
|
|
299
|
-
return node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
offsetWithinAnnotatable(node, offset) {
|
|
303
|
-
const walker = document.createTreeWalker(this.annotatableTarget, NodeFilter.SHOW_TEXT);
|
|
304
|
-
let position = 0;
|
|
305
|
-
|
|
306
|
-
while (walker.nextNode()) {
|
|
307
|
-
const current = walker.currentNode;
|
|
308
|
-
if (current === node) return position + offset;
|
|
309
|
-
position += current.textContent.length;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return null;
|
|
114
|
+
return currentSelection(window.getSelection(), this.annotatableTarget);
|
|
313
115
|
}
|
|
314
116
|
|
|
315
117
|
renderAnnotations() {
|
|
316
|
-
if (!this.
|
|
118
|
+
if (!this.composer.isOpen()) this.clearPendingHighlight();
|
|
317
119
|
this.renderSavedHighlights();
|
|
318
120
|
this.annotationListTarget.innerHTML = "";
|
|
319
121
|
|
|
320
122
|
if (this.annotationsValue.length === 0) {
|
|
321
|
-
this.annotationListTarget.appendChild(
|
|
123
|
+
this.annotationListTarget.appendChild(emptyAnnotationListItem());
|
|
322
124
|
return;
|
|
323
125
|
}
|
|
324
126
|
|
|
325
127
|
this.annotationsValue.forEach((annotation) => {
|
|
326
|
-
this.annotationListTarget.appendChild(
|
|
128
|
+
this.annotationListTarget.appendChild(
|
|
129
|
+
annotationListItem(annotation, {
|
|
130
|
+
readonly: this.readonlyValue,
|
|
131
|
+
categoryLabel: this.categoryLabelValue,
|
|
132
|
+
categories: this.categoriesValue,
|
|
133
|
+
})
|
|
134
|
+
);
|
|
327
135
|
});
|
|
328
136
|
}
|
|
329
137
|
|
|
330
138
|
applyPendingHighlight(selection) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.setHighlight(PENDING_HIGHLIGHT_NAME, [range]);
|
|
139
|
+
this.highlightLayer.set(PENDING_HIGHLIGHT_NAME, [
|
|
140
|
+
[selection.startOffset, selection.endOffset],
|
|
141
|
+
]);
|
|
335
142
|
}
|
|
336
143
|
|
|
337
144
|
renderSavedHighlights() {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const startOffset = Number(annotation.start_offset);
|
|
343
|
-
const endOffset = Number(annotation.end_offset);
|
|
344
|
-
if (!Number.isFinite(startOffset) || !Number.isFinite(endOffset) || endOffset <= startOffset) return null;
|
|
345
|
-
|
|
346
|
-
return this.rangeForOffsets(startOffset, endOffset);
|
|
347
|
-
})
|
|
348
|
-
.filter((range) => range !== null);
|
|
349
|
-
|
|
350
|
-
this.setHighlight(SAVED_HIGHLIGHT_NAME, ranges);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
setHighlight(name, ranges) {
|
|
354
|
-
if (!this.highlightsSupported) return;
|
|
355
|
-
|
|
356
|
-
this.clearHighlight(name);
|
|
357
|
-
if (ranges.length === 0) return;
|
|
358
|
-
|
|
359
|
-
const highlight = new Highlight();
|
|
360
|
-
ranges.forEach((range) => highlight.add(range));
|
|
361
|
-
CSS.highlights.set(name, highlight);
|
|
145
|
+
this.highlightLayer.set(
|
|
146
|
+
SAVED_HIGHLIGHT_NAME,
|
|
147
|
+
savedHighlightOffsetPairs(this.annotationsValue, this.editingAnnotationId)
|
|
148
|
+
);
|
|
362
149
|
}
|
|
363
150
|
|
|
364
151
|
clearPendingHighlight() {
|
|
365
|
-
this.
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
clearSavedHighlights() {
|
|
369
|
-
this.clearHighlight(SAVED_HIGHLIGHT_NAME);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
clearHighlight(name) {
|
|
373
|
-
if (!this.highlightsSupported) return;
|
|
374
|
-
|
|
375
|
-
CSS.highlights.delete(name);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
rangeForOffsets(startOffset, endOffset) {
|
|
379
|
-
const walker = document.createTreeWalker(this.annotatableTarget, NodeFilter.SHOW_TEXT);
|
|
380
|
-
let position = 0;
|
|
381
|
-
let startNode = null;
|
|
382
|
-
let startPosition = 0;
|
|
383
|
-
let endNode = null;
|
|
384
|
-
let endPosition = 0;
|
|
385
|
-
|
|
386
|
-
while (walker.nextNode()) {
|
|
387
|
-
const node = walker.currentNode;
|
|
388
|
-
const length = node.textContent.length;
|
|
389
|
-
const nextPosition = position + length;
|
|
390
|
-
|
|
391
|
-
if (!startNode && startOffset <= nextPosition) {
|
|
392
|
-
startNode = node;
|
|
393
|
-
startPosition = startOffset - position;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (!endNode && endOffset <= nextPosition) {
|
|
397
|
-
endNode = node;
|
|
398
|
-
endPosition = endOffset - position;
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
position = nextPosition;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (!startNode || !endNode) return null;
|
|
406
|
-
|
|
407
|
-
const range = document.createRange();
|
|
408
|
-
range.setStart(startNode, startPosition);
|
|
409
|
-
range.setEnd(endNode, endPosition);
|
|
410
|
-
return range;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
emptyAnnotationListItem() {
|
|
414
|
-
const item = document.createElement("p");
|
|
415
|
-
item.className = "aa-annotations-empty";
|
|
416
|
-
item.textContent = "No annotations yet.";
|
|
417
|
-
return item;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
annotationListItem(annotation) {
|
|
421
|
-
const item = document.createElement("article");
|
|
422
|
-
item.className = "aa-annotations-annotation-item";
|
|
423
|
-
const categoryLabel = this.categoryLabelFor(annotation.category);
|
|
424
|
-
item.innerHTML = `
|
|
425
|
-
<blockquote>${this.escapeHtml(annotation.selected_text)}</blockquote>
|
|
426
|
-
<p>${this.escapeHtml(annotation.comment)}</p>
|
|
427
|
-
${annotation.category ? `<p><strong>${this.escapeHtml(this.categoryLabelValue)}:</strong> ${this.escapeHtml(categoryLabel)}</p>` : ""}
|
|
428
|
-
<div class="aa-annotations-annotation-actions">
|
|
429
|
-
<button type="button" class="action-item-button" data-annotation-id="${this.escapeHtml(String(annotation.id))}" data-action="activeadmin-annotations--annotator#editAnnotation">Edit</button>
|
|
430
|
-
<button type="button" class="action-item-button aa-annotations-delete-button" data-annotation-id="${this.escapeHtml(String(annotation.id))}" data-action="activeadmin-annotations--annotator#deleteAnnotation">Delete</button>
|
|
431
|
-
</div>
|
|
432
|
-
`;
|
|
433
|
-
return item;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
setComposerHeading(title) {
|
|
437
|
-
if (this.hasComposerHeadingTarget) this.composerHeadingTarget.textContent = title;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
categoryLabelFor(value) {
|
|
441
|
-
if (!value) return "";
|
|
442
|
-
return this.categoriesValue[value] || value;
|
|
152
|
+
this.highlightLayer.clear(PENDING_HIGHLIGHT_NAME);
|
|
443
153
|
}
|
|
444
154
|
|
|
445
155
|
annotationUrl(annotationId) {
|
|
446
156
|
return this.updateUrlTemplateValue.replace(":id", annotationId);
|
|
447
157
|
}
|
|
448
|
-
|
|
449
|
-
normalizeAnnotation(annotation) {
|
|
450
|
-
return {
|
|
451
|
-
id: annotation.id,
|
|
452
|
-
field_name: annotation.field_name,
|
|
453
|
-
selected_text: annotation.selected_text,
|
|
454
|
-
start_offset: annotation.start_offset,
|
|
455
|
-
end_offset: annotation.end_offset,
|
|
456
|
-
comment: annotation.comment,
|
|
457
|
-
category: annotation.category,
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
showError(message) {
|
|
462
|
-
this.errorMessageTarget.textContent = message;
|
|
463
|
-
this.errorMessageTarget.classList.remove("aa-annotations-hidden");
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
hideError() {
|
|
467
|
-
this.errorMessageTarget.textContent = "";
|
|
468
|
-
this.errorMessageTarget.classList.add("aa-annotations-hidden");
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
csrfToken() {
|
|
472
|
-
const meta = document.querySelector("meta[name='csrf-token']");
|
|
473
|
-
return meta ? meta.getAttribute("content") : "";
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
escapeHtml(value) {
|
|
477
|
-
return value
|
|
478
|
-
.replaceAll("&", "&")
|
|
479
|
-
.replaceAll("<", "<")
|
|
480
|
-
.replaceAll(">", ">")
|
|
481
|
-
.replaceAll('"', """);
|
|
482
|
-
}
|
|
483
158
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createAnnotationPayload, normalizeAnnotation } from "activeadmin_annotations/annotator_logic";
|
|
2
|
+
|
|
3
|
+
export class Composer {
|
|
4
|
+
constructor(controller) {
|
|
5
|
+
this.controller = controller;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
activate(selection) {
|
|
9
|
+
const panel = this.controller;
|
|
10
|
+
panel.editingAnnotationId = null;
|
|
11
|
+
|
|
12
|
+
const selectionChanged =
|
|
13
|
+
!panel.pendingSelection ||
|
|
14
|
+
panel.pendingSelection.startOffset !== selection.startOffset ||
|
|
15
|
+
panel.pendingSelection.endOffset !== selection.endOffset ||
|
|
16
|
+
panel.pendingSelection.selectedText !== selection.selectedText;
|
|
17
|
+
|
|
18
|
+
panel.pendingSelection = selection;
|
|
19
|
+
panel.selectedPreviewTarget.textContent = selection.selectedText;
|
|
20
|
+
|
|
21
|
+
if (selectionChanged) {
|
|
22
|
+
panel.commentInputTarget.value = "";
|
|
23
|
+
panel.categoryInputTarget.value = "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.hideError();
|
|
27
|
+
this.setHeading("New annotation");
|
|
28
|
+
panel.composerTarget.classList.remove("aa-annotations-composer-hidden");
|
|
29
|
+
panel.applyPendingHighlight(selection);
|
|
30
|
+
window.getSelection()?.removeAllRanges();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
edit(annotation) {
|
|
34
|
+
const panel = this.controller;
|
|
35
|
+
panel.editingAnnotationId = annotation.id;
|
|
36
|
+
panel.pendingSelection = {
|
|
37
|
+
selectedText: annotation.selected_text,
|
|
38
|
+
startOffset: Number(annotation.start_offset),
|
|
39
|
+
endOffset: Number(annotation.end_offset),
|
|
40
|
+
};
|
|
41
|
+
panel.selectedPreviewTarget.textContent = annotation.selected_text;
|
|
42
|
+
panel.commentInputTarget.value = annotation.comment;
|
|
43
|
+
panel.categoryInputTarget.value = annotation.category || "";
|
|
44
|
+
this.hideError();
|
|
45
|
+
this.setHeading("Edit annotation");
|
|
46
|
+
panel.composerTarget.classList.remove("aa-annotations-composer-hidden");
|
|
47
|
+
panel.applyPendingHighlight(panel.pendingSelection);
|
|
48
|
+
window.getSelection()?.removeAllRanges();
|
|
49
|
+
panel.commentInputTarget.focus();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
reset() {
|
|
53
|
+
const panel = this.controller;
|
|
54
|
+
panel.pendingSelection = null;
|
|
55
|
+
panel.editingAnnotationId = null;
|
|
56
|
+
panel.clearPendingHighlight();
|
|
57
|
+
panel.selectedPreviewTarget.textContent = "";
|
|
58
|
+
panel.commentInputTarget.value = "";
|
|
59
|
+
panel.categoryInputTarget.value = "";
|
|
60
|
+
this.hideError();
|
|
61
|
+
this.setHeading("New annotation");
|
|
62
|
+
panel.composerTarget.classList.add("aa-annotations-composer-hidden");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
cancel() {
|
|
66
|
+
this.reset();
|
|
67
|
+
window.getSelection()?.removeAllRanges();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isOpen() {
|
|
71
|
+
return !this.controller.composerTarget.classList.contains("aa-annotations-composer-hidden");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ensurePendingHighlight() {
|
|
75
|
+
if (!this.controller.pendingSelection) return;
|
|
76
|
+
this.controller.applyPendingHighlight(this.controller.pendingSelection);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async submit() {
|
|
80
|
+
const panel = this.controller;
|
|
81
|
+
if (panel.readonlyValue) return;
|
|
82
|
+
if (!panel.pendingSelection) return;
|
|
83
|
+
|
|
84
|
+
const comment = panel.commentInputTarget.value.trim();
|
|
85
|
+
if (!comment) {
|
|
86
|
+
this.showError("Comment is required.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (panel.editingAnnotationId) {
|
|
91
|
+
await this.update(comment);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await this.create(comment);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async create(comment) {
|
|
99
|
+
const panel = this.controller;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const annotation = await panel.annotationClient.create(
|
|
103
|
+
panel.createUrlValue,
|
|
104
|
+
this.createPayload(comment)
|
|
105
|
+
);
|
|
106
|
+
if (annotation.annotation_review_id) panel.reviewIdValue = annotation.annotation_review_id;
|
|
107
|
+
this.reset();
|
|
108
|
+
panel.annotationsValue = [...panel.annotationsValue, this.normalize(annotation)];
|
|
109
|
+
panel.renderAnnotations();
|
|
110
|
+
window.getSelection()?.removeAllRanges();
|
|
111
|
+
} catch (_error) {
|
|
112
|
+
this.showError("Could not save annotation.");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async update(comment) {
|
|
117
|
+
const panel = this.controller;
|
|
118
|
+
const payload = {
|
|
119
|
+
annotation: {
|
|
120
|
+
comment: comment,
|
|
121
|
+
category: panel.categoryInputTarget.value || null,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const annotation = await panel.annotationClient.update(
|
|
127
|
+
panel.annotationUrl(panel.editingAnnotationId),
|
|
128
|
+
payload
|
|
129
|
+
);
|
|
130
|
+
this.reset();
|
|
131
|
+
panel.annotationsValue = panel.annotationsValue.map((entry) =>
|
|
132
|
+
String(entry.id) === String(annotation.id) ? this.normalize(annotation) : entry
|
|
133
|
+
);
|
|
134
|
+
panel.renderAnnotations();
|
|
135
|
+
} catch (_error) {
|
|
136
|
+
this.showError("Could not update annotation.");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async destroy(annotationId) {
|
|
141
|
+
const panel = this.controller;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await panel.annotationClient.destroy(panel.annotationUrl(annotationId));
|
|
145
|
+
if (String(panel.editingAnnotationId) === String(annotationId)) this.reset();
|
|
146
|
+
panel.annotationsValue = panel.annotationsValue.filter(
|
|
147
|
+
(entry) => String(entry.id) !== String(annotationId)
|
|
148
|
+
);
|
|
149
|
+
panel.renderAnnotations();
|
|
150
|
+
} catch (_error) {
|
|
151
|
+
window.alert("Could not delete annotation.");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
createPayload(comment) {
|
|
156
|
+
return createAnnotationPayload(this.controller, comment);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
normalize(annotation) {
|
|
160
|
+
return normalizeAnnotation(annotation);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setHeading(title) {
|
|
164
|
+
const panel = this.controller;
|
|
165
|
+
if (panel.hasComposerHeadingTarget) panel.composerHeadingTarget.textContent = title;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
showError(message) {
|
|
169
|
+
const panel = this.controller;
|
|
170
|
+
panel.errorMessageTarget.textContent = message;
|
|
171
|
+
panel.errorMessageTarget.classList.remove("aa-annotations-hidden");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
hideError() {
|
|
175
|
+
const panel = this.controller;
|
|
176
|
+
panel.errorMessageTarget.textContent = "";
|
|
177
|
+
panel.errorMessageTarget.classList.add("aa-annotations-hidden");
|
|
178
|
+
}
|
|
179
|
+
}
|