class AnnotationManager { constructor(rootEl, tagsUrl) { this.rootEl = rootEl; this.tagsEl = this.rootEl.querySelector('#tags'); this.annotationsEl = this.rootEl.querySelector('#annotations'); this.loadTags(tagsUrl); } loadTags(tagFile) { // tags config const request = new Request(tagFile); return fetch(request) .then(response => response.json()) .then(rootTag => { this.rootTag = Tag.from(rootTag); this.buildTagList() }); } buildTagList() { // build, and rebuild this.tagsEl.innerHTML = ""; const addTag = (tag, parentEl) => { const tagLiEl = document.createElement('li'); const tagSubEl = document.createElement('ul'); const tagEl = document.createElement('div'); tagEl.innerText = tag.get_name(); tagEl.addEventListener('click', (ev) => { this.selectTag(tag); }) const colorEl = document.createElement('input'); colorEl.type = 'color'; colorEl.value = tag.get_color(); colorEl.addEventListener('change', (ev) => { this.setTagColor(tag, ev.target.value); }); tagEl.prepend(colorEl) tagLiEl.classList.add('tag-id-' + tag.id); tagLiEl.appendChild(tagEl); tag.menuLiEl = tagLiEl; tagLiEl.appendChild(tagSubEl); tag.children.forEach((tag) => addTag(tag, tagSubEl)); const tagAddSubEl = document.createElement('li'); tagAddSubEl.classList.add('add-tag') tagAddSubEl.innerText = 'add tag'; tagAddSubEl.addEventListener('click', (ev) => { const name = prompt(`Add a tag under '${tag.get_name()}':`); if (name === null || name.length < 1) return //cancel this.addTag(name, tag); }); tagSubEl.appendChild(tagAddSubEl); parentEl.appendChild(tagLiEl); }; addTag(this.rootTag, this.tagsEl); } selectTag(tag) { this.clearSelectedTag(); tag.menuLiEl.classList.add('selected'); this.loadAnnotationsForTag(tag) } clearSelectedTag() { const selected = this.tagsEl.querySelectorAll('.selected'); selected.forEach((s) => s.classList.remove('selected')); //TODO empty annotationEl this.annotationsEl.innerHTML = ""; } selectAnnotation() { throw new Error("Not implemented"); } addTag(name, parentTag) { const tag = new Tag(); tag.id = crypto.randomUUID(); tag.name = name; tag.parent = parentTag; parentTag.children.push(tag); this.buildTagList(); this.saveTags(); } loadAnnotationsForTag(tag) { this.annotationsEl.innerHTML = ""; const request = new Request("/tags/" + tag.id); return fetch(request) .then(response => response.json()) .then(annotations => { this.annotations = annotations; this.buildAnnotationList() }); } buildAnnotationList() { this.annotationsEl.innerHTML = ""; const ulEl = document.createElement('ul'); this.annotations.forEach((annotation, idx) => { const liEl = document.createElement('li'); const imgEl = document.createElement('img'); const playerEl = document.createElement('div'); const infoEl = document.createElement('div'); infoEl.classList.add('annotation-info'); const tag = this.rootTag.find_by_id(annotation.tag); console.log(tag) infoEl.innerText = `[${tag.get_name()}] ${annotation.comment}`; imgEl.src = `/annotation/${annotation.id}.svg`; imgEl.addEventListener('click', () => { imgEl.style.display = 'none'; new Annotator( playerEl, "tags.json", annotation.url, { is_player: true, crop_to_fit: true, autoplay: true } ); }) playerEl.classList.add('play'); liEl.appendChild(imgEl); liEl.appendChild(playerEl); liEl.appendChild(infoEl); ulEl.appendChild(liEl); }); this.annotationsEl.appendChild(ulEl); } renameTag(tag) { // TODO: add button for this const name = prompt(`Rename tag '${tag.get_name()}':`); if (name === null || name.length < 1) return //cancel tag.name = name; this.saveTags(); } removeTag(tag) { // TODO: add button for this const request = new Request("/tags/" + tag.id); return fetch(request) .then(response => response.json()) .then(annotations => { if(annotations.length) { alert(`Cannot remove '${tag.get_name()}', as it is used for ${annotations.length} annotations.`) } else { // TODO: remove tag tag.parent.children.splice(tag.parent.children.indexOf(tag), 1); tag.parent = null; this.saveTags(); } }); } setTagColor(tag, color) { tag.color = color; this.buildTagList(); this.saveTags(); } moveSelectedAnnotations(newTag) { // TODO: add button for this throw new Error("Not implemented"); } saveTags() { const json = this.rootTag.export(); // TODO: save to remote console.log('save', json) } } class Tag { parent = null; children = []; static from(json_obj) { if (json_obj.hasOwnProperty('children')) { json_obj.children = json_obj.children.map(Tag.from) } else { json_obj.children = []; } json_obj.parent = null; const tag = Object.assign(new Tag(), json_obj); tag.children.map((child) => child.parent = tag); return tag; } is_root() { return this.parent === null; } descendants(inc_self = false) { let tags = this.children.flatMap((t) => t.descendants(true)) if (inc_self) tags.unshift(this) return tags; } find_by_id(tag_id) { const desc = this.descendants().filter((tag) => tag.id == tag_id); if (desc.length) return desc[0]; return null; } depth() { if (this.parent === null) { return 0; } return this.parent.depth() + 1; } root() { if (this.parent !== null) { return this.parent.root(); } return this; } get_name() { if (this.hasOwnProperty('name')) { return this.name; } return this.id; } get_color() { if (this.hasOwnProperty('color')) { return this.color; } if (this.parent !== null) { return this.parent.get_color() } return 'black'; } _json_replacer(key, value) { if (key === 'children' && !value.length) { return undefined; } if (key === 'parent' || key === 'menuLiEl') { return undefined; } if (key === 'color' && this.parent !== null && this.parent.get_color() === value) { return undefined; } return value; } export() { return JSON.stringify(this, this._json_replacer) } }