Compare commits

..

4 commits

Author SHA1 Message Date
Ruben van de Ven
6fbe49473d Annotation export to self-contained zip 2023-02-23 18:52:21 +01:00
Ruben van de Ven
8be08ce9d6 More player controls 2023-02-23 18:28:16 +01:00
Ruben van de Ven
f7e9fd99fc Style Player component and move to annotate.js 2023-02-23 13:25:03 +01:00
Ruben van de Ven
daf0e0dfd4 Annotation Player as rudimentary Web Component 2023-02-23 11:17:24 +01:00
4 changed files with 434 additions and 82 deletions

View file

@ -168,29 +168,16 @@
padding: 10px; padding: 10px;
} }
#annotations .svganim_player { #annotations .svganim_player,
#annotations annotation-player {
display: inline-block; display: inline-block;
position: relative; position: relative;
width: 300px; width: 300px;
height: 200px; height: 200px;
overflow: hidden;
padding: 10px;
background: white;
} }
#annotations .svganim_player svg {
width: 100%;
height: 100%;
}
#annotations .svganim_player.play:not(.loading) .controls {
visibility: hidden;
}
#annotations .svganim_player:hover .controls {
visibility: visible !important;
}
</style> </style>
<script src="assets/nouislider-15.5.0.js"></script> <script src="assets/nouislider-15.5.0.js"></script>
<script src="assets/wNumb-1.2.0.min.js"></script> <script src="assets/wNumb-1.2.0.min.js"></script>

View file

@ -2,7 +2,9 @@ import json
import logging import logging
import os import os
import shutil import shutil
import tempfile
from urllib.error import HTTPError from urllib.error import HTTPError
from zipfile import ZipFile
import tornado.ioloop import tornado.ioloop
import tornado.web import tornado.web
import tornado.websocket import tornado.websocket
@ -240,6 +242,73 @@ class AudioListingHandler(tornado.web.RequestHandler):
print(names) print(names)
self.write(json.dumps(names)) self.write(json.dumps(names))
class ExportHandler(tornado.web.RequestHandler):
"""
Export a player to a zip file
"""
def initialize(self, config, index: svganim.strokes.AnnotationIndex):
self.config = config
self.index = index
async def get(self, filename):
logger.info(f"file {filename=}")
if filename not in self.index.drawings:
raise tornado.web.HTTPError(404)
t_in = self.get_argument('t_in', None)
t_out = self.get_argument('t_out', None)
animation = self.index.drawings[filename].get_animation()
if t_in is not None and t_out is not None:
animation = animation.getSlice(float(t_in), float(t_out))
with tempfile.TemporaryDirectory() as tdir:
with ZipFile(tdir + '/annotation.zip', 'w') as archive:
logger.info('write svg')
svgstring = animation.get_as_svg()
archive.writestr('drawing.svg', svgstring)
logger.info('write png')
archive.writestr('drawing.png', cairosvg.svg2png(bytestring=svgstring))
logger.info('write mp3')
audio = await animation.audio.export(format="mp3")
archive.writestr('drawing.mp3', audio.read())
logger.info('write json')
data = animation.asDict()
data['audio']['file'] = 'drawing.mp3';
archive.writestr('annotation.json', json.dumps(data))
logger.info('write js')
with open('www/annotate.js', 'r') as fp:
archive.writestr('annotate.js', fp.read())
with open('www/assets/wNumb-1.2.0.min.js', 'r') as fp:
archive.writestr('wNumb-1.2.0.min.js', fp.read())
logger.info('write html')
html = """
<html>
<head>
<script src="wNumb-1.2.0.min.js"></script>
<script src="annotate.js"></script>
</head>
<body>
<annotation-player data-poster-url="drawing.svg" data-annotation-url="annotation.json">
</body>
</html>
"""
archive.writestr('drawing.html', html)
with open(tdir + '/annotation.zip', 'rb') as fp:
self.set_header("Content-Type", "application/zip")
self.write(fp.read())
logger.info('done')
class AnimationHandler(tornado.web.RequestHandler): class AnimationHandler(tornado.web.RequestHandler):
def initialize(self, config, index: svganim.strokes.AnnotationIndex): def initialize(self, config, index: svganim.strokes.AnnotationIndex):
@ -669,6 +738,7 @@ class Server:
}, },
), ),
(r"/files/(.*)", AnimationHandler, {"config": self.config, "index": self.index}), (r"/files/(.*)", AnimationHandler, {"config": self.config, "index": self.index}),
(r"/export/(.*)", ExportHandler, {"config": self.config, "index": self.index}),
( (
r"/audio/(.+)", r"/audio/(.+)",
tornado.web.StaticFileHandler, tornado.web.StaticFileHandler,

View file

@ -130,10 +130,11 @@ class Annotator extends EventTarget {
time *= -1; time *= -1;
} }
const s = Math.floor(time / 1000); const s = Math.floor(time / 1000);
const minutes = Math.floor(s / 60); const minutes = String(Math.floor(s / 60)).padStart(2, '0');
const seconds = s - minutes * 60; const seconds = String(s - minutes * 60).padStart(2, '0');
const ms = Math.floor((time / 1000 - s) * 1000); // show miliseconds only in annotator
return `${neg}${minutes}:${seconds}.${ms}`; const ms = !this.config.is_player ? "." + String(Math.floor((time / 1000 - s) * 1000)).padStart(3, '0') : "";
return `${neg}${minutes}:${seconds}${ms}`;
}, },
undo: (tc) => { undo: (tc) => {
let [rest, ms] = tc.split(/[\.\,]/); let [rest, ms] = tc.split(/[\.\,]/);
@ -143,7 +144,7 @@ class Annotator extends EventTarget {
ms += v * factor; ms += v * factor;
factor *= 60; factor *= 60;
}); });
return `${ms}`; return `${ms} `;
} }
}); });
@ -152,6 +153,7 @@ class Annotator extends EventTarget {
this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.wrapperEl.appendChild(this.svgEl); this.wrapperEl.appendChild(this.svgEl);
this.wrapperEl.classList.add(this.config.is_player ? "svganim_player" : "svganim_annotator"); this.wrapperEl.classList.add(this.config.is_player ? "svganim_player" : "svganim_annotator");
this.wrapperEl.classList.add(this.config.crop_to_fit ? "cropped-to-selection" : "follow-drawing");
this.controlsEl = document.createElement('div'); this.controlsEl = document.createElement('div');
@ -192,15 +194,40 @@ class Annotator extends EventTarget {
ev.preventDefault(); // we don't want to spacebar, as this is captured in the overall keydown event ev.preventDefault(); // we don't want to spacebar, as this is captured in the overall keydown event
}) })
if (!this.config.is_player) {
this.scrubberEl = document.createElement('div'); this.scrubberEl = document.createElement('div');
this.scrubberEl.classList.add('scrubber') this.scrubberEl.classList.add('scrubber')
this.controlsEl.appendChild(this.scrubberEl); this.controlsEl.appendChild(this.scrubberEl);
if(!this.config.is_player){
this.annotationsEl = document.createElement('div'); this.annotationsEl = document.createElement('div');
this.annotationsEl.classList.add('annotations') this.annotationsEl.classList.add('annotations')
this.controlsEl.appendChild(this.annotationsEl); this.controlsEl.appendChild(this.annotationsEl);
} else {
const extraEl = document.createElement('details');
extraEl.classList.add('controls--extra');
const summaryEl = document.createElement('summary');
summaryEl.innerHTML = "&mldr;";
extraEl.appendChild(summaryEl);
const extraControlsEl = document.createElement('ul');
// TODO: add handlers
const toggleFutureEl = document.createElement('li');
toggleFutureEl.innerText = "Show preview"
toggleFutureEl.addEventListener('click', () => this.wrapperEl.classList.toggle('hide-drawing-preview'));
extraControlsEl.appendChild(toggleFutureEl);
const toggleCropPlayerEl = document.createElement('li');
toggleCropPlayerEl.innerText = "Crop to selection";
toggleCropPlayerEl.addEventListener('click', () => this.toggleCrop());
extraControlsEl.appendChild(toggleCropPlayerEl);
extraEl.appendChild(extraControlsEl);
this.playbackControlsEl.appendChild(extraEl);
} }
@ -339,7 +366,7 @@ class Annotator extends EventTarget {
if (this.selectedAnnotationI == annotation_i) { if (this.selectedAnnotationI == annotation_i) {
this.annotationEl.classList.add('selected'); this.annotationEl.classList.add('selected');
} }
this.annotationEl.title = `[${annotation.tag}] ${annotation.comment}`; this.annotationEl.title = `[${annotation.tag}] ${annotation.comment} `;
this.annotationEl.addEventListener('mouseover', (e) => { this.annotationEl.addEventListener('mouseover', (e) => {
@ -425,7 +452,7 @@ class Annotator extends EventTarget {
// draw full stroke of annotation // draw full stroke of annotation
console.debug('setInOut'); console.debug('setInOut');
this.drawStrokePosition(this.inPointPosition, this.outPointPosition); this.drawStrokePosition(this.inPointPosition, this.outPointPosition);
console.debug([`${this.inPointTimeMs}`, `${this.outPointTimeMs}`]) console.debug([`${this.inPointTimeMs} `, `${this.outPointTimeMs} `])
this.slider.set([this.inPointTimeMs, this.outPointTimeMs]); this.slider.set([this.inPointTimeMs, this.outPointTimeMs]);
// console.debug(this.selectedAnnotation); // console.debug(this.selectedAnnotation);
@ -460,7 +487,7 @@ class Annotator extends EventTarget {
.then(data => { .then(data => {
if (!this.config.is_player) { if (!this.config.is_player) {
const metadata_req = new Request(`/annotations/${data.file}`, { const metadata_req = new Request(`/ annotations / ${data.file} `, {
method: 'GET', method: 'GET',
}); });
return fetch(metadata_req) return fetch(metadata_req)
@ -478,7 +505,7 @@ class Annotator extends EventTarget {
}) })
.then(() => { .then(() => {
// play on click for player // play on click for player
if(this.config.is_player) { if (this.config.is_player) {
this.svgEl.addEventListener('click', (ev) => { this.svgEl.addEventListener('click', (ev) => {
console.debug('clicked for play/pause'); console.debug('clicked for play/pause');
this.playPause(); this.playPause();
@ -486,9 +513,9 @@ class Annotator extends EventTarget {
} }
// autoplay if necessary // autoplay if necessary
if(this.config.autoplay){ if (this.config.autoplay) {
this.play(); // play should remove loading this.play(); // play should remove loading
} else{ } else {
this.wrapperEl.classList.remove('loading'); this.wrapperEl.classList.remove('loading');
} }
}) })
@ -845,7 +872,7 @@ class Annotator extends EventTarget {
titleEl.title = this.title ?? "[click to add title for this diagram]" titleEl.title = this.title ?? "[click to add title for this diagram]"
titleEl.addEventListener('click', (ev) => { titleEl.addEventListener('click', (ev) => {
const title = prompt("Change the title for the drawing", this.title ?? ""); const title = prompt("Change the title for the drawing", this.title ?? "");
if(title === null) return; //cancel if (title === null) return; //cancel
titleEl.innerText = title.length ? title : "[add title]"; titleEl.innerText = title.length ? title : "[add title]";
this.title = title.length ? title : null; this.title = title.length ? title : null;
this.updateState(); this.updateState();
@ -986,7 +1013,7 @@ class Annotator extends EventTarget {
} }
getFinalFrameTime() { getFinalFrameTime() {
if(this.strokes.length == 0) return null; // when no strokes are loaded (eg. for annotation) if (this.strokes.length == 0) return null; // when no strokes are loaded (eg. for annotation)
const points = this.strokes[this.strokes.length - 1].points; const points = this.strokes[this.strokes.length - 1].points;
return points[points.length - 1][3]; return points[points.length - 1][3];
} }
@ -1032,7 +1059,7 @@ class Annotator extends EventTarget {
// } // }
// for (let index = 0; index < inPath_i; index++) { // for (let index = 0; index < inPath_i; index++) {
// const pathEl = this.svgEl.querySelector(`.path${index}`); // const pathEl = this.svgEl.querySelector(`.path${ index } `);
// if (pathEl) { // if (pathEl) {
// pathEl.classList.add('before_in'); // pathEl.classList.add('before_in');
// } // }
@ -1044,7 +1071,7 @@ class Annotator extends EventTarget {
// const path = this.strokes[path_i]; // const path = this.strokes[path_i];
// // console.log(path); // // console.log(path);
// let pathEl = this.svgEl.querySelector(`.path${path_i}`); // let pathEl = this.svgEl.querySelector(`.path${ path_i } `);
// if (!pathEl) { // if (!pathEl) {
// pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
// pathEl.style.stroke = path.color; // pathEl.style.stroke = path.color;
@ -1072,10 +1099,10 @@ class Annotator extends EventTarget {
updateViewbox() { updateViewbox() {
if (this.config.crop_to_fit) { if (this.config.crop_to_fit) {
this.svgEl.setAttribute('viewBox', `${this.bounding_box.x} ${this.bounding_box.y} ${this.bounding_box.width} ${this.bounding_box.height}`); this.svgEl.setAttribute('viewBox', `${this.bounding_box.x} ${this.bounding_box.y} ${this.bounding_box.width} ${this.bounding_box.height} `);
} else { } else {
let x,y,w,h; let x, y, w, h;
if(this.currentViewboxI !== null) { if (this.currentViewboxI !== null) {
x = this.viewboxes[this.currentViewboxI].x, x = this.viewboxes[this.currentViewboxI].x,
y = this.viewboxes[this.currentViewboxI].y, y = this.viewboxes[this.currentViewboxI].y,
w = this.dimensions[0], w = this.dimensions[0],
@ -1086,15 +1113,24 @@ class Annotator extends EventTarget {
w = this.dimensions[0], w = this.dimensions[0],
h = this.dimensions[1]; h = this.dimensions[1];
} }
this.svgEl.setAttribute('viewBox', `${x} ${y} ${w} ${h}`); this.svgEl.setAttribute('viewBox', `${x} ${y} ${w} ${h} `);
} }
} }
toggleCrop(){ setCrop(crop_to_fit) {
this.config.crop_to_fit = !this.config.crop_to_fit; this.config.crop_to_fit = Boolean(crop_to_fit);
if (this.config.crop_to_fit) {
this.wrapperEl.classList.add('cropped-to-selection');
} else {
this.wrapperEl.classList.remove('cropped-to-selection');
}
this.updateViewbox(); this.updateViewbox();
} }
toggleCrop() {
this.setCrop(!this.config.crop_to_fit);
}
getNextPosition(path_i, point_i) { getNextPosition(path_i, point_i) {
const path = this.strokes[path_i]; const path = this.strokes[path_i];
let next_path, next_point; let next_path, next_point;
@ -1446,3 +1482,265 @@ class Annotator extends EventTarget {
} }
class AnnotationPlayer extends HTMLElement {
constructor() {
super();
// We don't use constructor() because an element's attributes
// are unavailable until connected to the DOM.
// attributes:
// - data-no-crop
// - autoplay
// - preload
// - data-poster-src
// - data-annotation-url
}
connectedCallback() {
// Create a shadow root
this.attachShadow({ mode: "open" });
const imgEl = document.createElement('img');
const playerEl = document.createElement('div');
const config = {
is_player: true,
crop_to_fit: this.hasAttribute('data-no-crop') ? false : true,
autoplay: true,
}
imgEl.src = this.getAttribute('data-poster-url');
imgEl.addEventListener('click', () => {
imgEl.style.display = 'none';
this.annotator = new Annotator(
playerEl,
null, //"tags.json",
this.getAttribute('data-annotation-url'),
config
);
})
playerEl.classList.add('play');
const styleEl = document.createElement('style');
styleEl.textContent = `
:host{
overflow: hidden;
padding: 10px;
background: white;
}
svg, img {
width: 100%;
height: 100%;
}
.play:not(.loading) .controls {
visibility: hidden;
}
:host(:hover) .controls {
visibility: visible !important;
}
.controls--playback {
display:flex;
background: rgba(0,0,0,.5);
border-radius: 3px;
}
.timecode {
width: 30px;
font-size: 8px;
background: none;
border: none;
color: white;
}
.controls--playback input[type='range'] {
flex-grow: 1;
-webkit-appearance: none;
background: none;
}
input[type="range"]::-webkit-slider-runnable-track,
input[type="range"]::-moz-range-track {
background: lightgray;
height: 5px;
border-radius: 3px;
}
input[type="range"]::-moz-range-progress {
background-color: white;
height: 5px;
border-radius: 3px 0 0 3px;
}
input[type="range"]::-webkit-slider-thumb,
input[type="range"]::-moz-range-thumb {
-webkit-appearance: none;
height: 15px;
width: 15px;
background: white;
margin-top: -5px;
border-radius: 50%;
border:none;
}
.controls button.paused,
.controls button.playing {
order: -1;
width: 30px;
height: 30px;
border: none;
background: none;
color: white;
line-height: 1;
}
.controls button.paused::before {
content: '⏵';
}
.controls button.playing::before {
content: '⏸';
}
.loading .controls button:is(.playing, .paused)::before {
content: '↺';
display: inline-block;
animation: rotate 1s infinite;
}
@keyframes rotate {
0% {
transform: rotate(359deg)
}
100% {
transform: rotate(0deg)
}
}
.controls {
position: absolute !important;
z-index: 100;
bottom: 10px;
left: 5%;
right: 0;
width: 90%;
}
svg .background {
fill: white
}
path {
fill: none;
stroke: gray;
stroke-width: 1mm;
stroke-linecap: round;
}
g.before path {
opacity: 0.5;
stroke: gray !important;
}
g.after path,
path.before_in {
opacity: .1;
stroke: gray !important;
}
.hide-drawing-preview g.after path, .hide-drawing-preview path.before_in{
opacity:0;
}
.gray {
position: absolute;
background: rgba(255, 255, 255, 0.7);
}
details{
color: white;
}
summary{
list-style: none;
cursor: pointer;
padding: 0 5px;
}
details > ul{
position: absolute;
bottom: 30px;
background: rgba(0,0,0, .5);
border-radius: 3px;
right: 0;
padding: 5px;
margin: 0;
list-style: none;
font-size: 10pt;
}
details > ul li:hover{
cursor: pointer;
text-decoration: underline;
}
.play:not(.hide-drawing-preview) details > ul li:first-child{
/*text-decoration: line-through;*/
font-weight:bold;
}
.play.cropped-to-selection details > ul li:nth-child(2){
/*text-decoration: line-through;*/
font-weight:bold;
}
`;
this.shadowRoot.appendChild(styleEl);
this.shadowRoot.appendChild(imgEl);
this.shadowRoot.appendChild(playerEl);
}
setAnnotation(annotation) {
// this.annotation = annotation;
this.setAttribute('data-annotation-url', annotation.url)
this.setAttribute('data-poster-url', `/annotation/${annotation.id}.svg`)
}
toggleCrop() {
if (this.hasAttribute('data-no-crop')) {
this.removeAttribute('data-no-crop');
} else {
this.setAttribute('data-no-crop', true);
}
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name, oldValue, newValue);
if (name == 'data-no-crop') {
if (!this.annotator) {
return;
}
this.annotator.setCrop(!this.hasAttribute('data-no-crop'));
}
}
// required for attributeChangedCallback()
static get observedAttributes() { return ['data-no-crop']; }
}
window.customElements.define('annotation-player', AnnotationPlayer);

View file

@ -141,14 +141,12 @@ class AnnotationManager {
const ulEl = document.createElement('ul'); const ulEl = document.createElement('ul');
this.annotations.forEach((annotation, idx) => { this.annotations.forEach((annotation, idx) => {
const liEl = document.createElement('li'); const liEl = document.createElement('li');
const imgEl = document.createElement('img'); const playerEl = new AnnotationPlayer(); //document.createElement('annotation-player');
const playerEl = document.createElement('div'); playerEl.setAnnotation(annotation);
const infoEl = document.createElement('span');
infoEl.classList.add('annotation-info');
const selectEl = document.createElement('input'); const selectEl = document.createElement('input');
selectEl.type = 'checkbox'; selectEl.type = 'checkbox';
selectEl.id = 'select-'+annotation.id_hash; selectEl.id = 'select-' + annotation.id_hash;
selectEl.addEventListener('change', (ev) => { selectEl.addEventListener('change', (ev) => {
if (ev.target.checked) { if (ev.target.checked) {
this.addAnnotationToSelection(annotation); this.addAnnotationToSelection(annotation);
@ -157,25 +155,20 @@ class AnnotationManager {
} }
}) })
const tag = this.rootTag.find_by_id(annotation.tag); const tag = this.rootTag.find_by_id(annotation.tag);
console.log(tag) console.log(tag)
const infoEl = document.createElement('span');
infoEl.classList.add('annotation-info');
infoEl.innerText = `[${tag.get_name()}] ${annotation.comment}`; infoEl.innerText = `[${tag.get_name()}] ${annotation.comment}`;
imgEl.src = `/annotation/${annotation.id}.svg`; const downloadEl = document.createElement('a');
imgEl.addEventListener('click', () => { downloadEl.href = annotation.url.replace('files', 'export');
imgEl.style.display = 'none'; downloadEl.innerHTML = '&darr;';
new Annotator(
playerEl,
"tags.json",
annotation.url,
{ is_player: true, crop_to_fit: true, autoplay: true }
);
})
playerEl.classList.add('play'); infoEl.appendChild(downloadEl);
liEl.appendChild(imgEl);
liEl.appendChild(playerEl); liEl.appendChild(playerEl);
liEl.appendChild(selectEl); liEl.appendChild(selectEl);
liEl.appendChild(infoEl); liEl.appendChild(infoEl);
@ -195,22 +188,26 @@ class AnnotationManager {
} }
addAnnotationToSelection(annotation) { addAnnotationToSelection(annotation) {
if (this.selectedAnnotations.indexOf(annotation) === -1){ if (this.selectedAnnotations.indexOf(annotation) === -1) {
this.selectedAnnotations.push(annotation); this.selectedAnnotations.push(annotation);
} }
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true; this.annotationsEl.querySelector('#select-' + annotation.id_hash).checked = true;
this.buildAnnotationActions() this.buildAnnotationActions()
} }
removeAnnotationFromSelection(annotation) { removeAnnotationFromSelection(annotation) {
if (this.selectedAnnotations.indexOf(annotation) !== -1){ if (this.selectedAnnotations.indexOf(annotation) !== -1) {
this.selectedAnnotations.splice(this.selectedAnnotations.indexOf(annotation), 1) this.selectedAnnotations.splice(this.selectedAnnotations.indexOf(annotation), 1)
} }
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = false; this.annotationsEl.querySelector('#select-' + annotation.id_hash).checked = false;
this.buildAnnotationActions() this.buildAnnotationActions()
} }
/**
* Build the form items to select & move the annotations
* @returns undefined
*/
buildAnnotationActions() { buildAnnotationActions() {
if(!this.actionsEl || !this.annotations.length) return if (!this.actionsEl || !this.annotations.length) return
this.actionsEl.innerHTML = ""; this.actionsEl.innerHTML = "";
const selectAllLabelEl = document.createElement('label'); const selectAllLabelEl = document.createElement('label');
@ -220,7 +217,7 @@ class AnnotationManager {
selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length; selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length;
// selectAllEl.innerText = `Select all ${this.annotations.length} items`; // selectAllEl.innerText = `Select all ${this.annotations.length} items`;
selectAllEl.addEventListener('change', (ev) => { selectAllEl.addEventListener('change', (ev) => {
if(ev.target.checked) { if (ev.target.checked) {
this.annotations.forEach((a) => this.addAnnotationToSelection(a)); this.annotations.forEach((a) => this.addAnnotationToSelection(a));
} else { } else {
this.resetSelectedAnnotations(); this.resetSelectedAnnotations();
@ -231,7 +228,7 @@ class AnnotationManager {
this.actionsEl.appendChild(selectAllLabelEl) this.actionsEl.appendChild(selectAllLabelEl)
if(!this.selectedAnnotations.length) return; if (!this.selectedAnnotations.length) return;
const moveLabelEl = document.createElement('label'); const moveLabelEl = document.createElement('label');
moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items` moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items`
@ -239,7 +236,7 @@ class AnnotationManager {
this.rootTag.descendants().forEach((tag, i) => { this.rootTag.descendants().forEach((tag, i) => {
const tagEl = document.createElement('option'); const tagEl = document.createElement('option');
tagEl.value = tag.id; tagEl.value = tag.id;
if(tag.id == this.selectedTag.id){ if (tag.id == this.selectedTag.id) {
tagEl.selected = true; tagEl.selected = true;
} }
tagEl.innerHTML = tag.get_indented_name(); tagEl.innerHTML = tag.get_indented_name();
@ -308,7 +305,7 @@ class AnnotationManager {
await Promise.all(this.selectedAnnotations.map(async (annotation) => { await Promise.all(this.selectedAnnotations.map(async (annotation) => {
const formData = new FormData(); const formData = new FormData();
formData.append('tag_id', tag.id) formData.append('tag_id', tag.id)
return await fetch('/annotation/'+annotation.id, { return await fetch('/annotation/' + annotation.id, {
method: 'POST', method: 'POST',
// headers: { // headers: {
// 'Content-Type': 'application/json' // 'Content-Type': 'application/json'