2022-06-08 13:28:36 +02:00
|
|
|
class AnnotationManager {
|
|
|
|
constructor(rootEl, tagsUrl) {
|
|
|
|
this.rootEl = rootEl;
|
|
|
|
|
|
|
|
|
|
|
|
this.tagsEl = this.rootEl.querySelector('#tags');
|
|
|
|
this.annotationsEl = this.rootEl.querySelector('#annotations');
|
|
|
|
|
2022-06-08 17:26:30 +02:00
|
|
|
this.selectedAnnotations = [];
|
|
|
|
this.selectedTag = null;
|
2022-06-08 18:51:22 +02:00
|
|
|
this.tagsUrl = tagsUrl;
|
|
|
|
this.loadTags();
|
2022-06-08 13:28:36 +02:00
|
|
|
}
|
|
|
|
|
2022-06-08 18:51:22 +02:00
|
|
|
loadTags() {
|
2022-06-08 13:28:36 +02:00
|
|
|
// tags config
|
2022-06-08 18:51:22 +02:00
|
|
|
const request = new Request(this.tagsUrl);
|
2022-06-08 13:28:36 +02:00
|
|
|
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');
|
|
|
|
|
2022-06-08 18:51:22 +02:00
|
|
|
tagEl.innerText = `${tag.get_name()} (${tag.annotation_count ?? 0})`;
|
2022-06-08 13:28:36 +02:00
|
|
|
tagEl.addEventListener('click', (ev) => {
|
|
|
|
this.selectTag(tag);
|
|
|
|
})
|
2022-06-08 17:26:30 +02:00
|
|
|
tagEl.addEventListener('dblclick', (ev) => {
|
|
|
|
this.renameTag(tag);
|
|
|
|
})
|
2022-06-08 13:28:36 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2022-06-08 17:26:30 +02:00
|
|
|
const rmEl = document.createElement('input');
|
|
|
|
rmEl.type = 'button';
|
|
|
|
rmEl.classList.add('rm-tag');
|
|
|
|
rmEl.value = '🗑';
|
|
|
|
rmEl.addEventListener('click', (ev) => {
|
|
|
|
ev.stopPropagation();
|
|
|
|
this.removeTag(tag);
|
|
|
|
});
|
|
|
|
tagEl.appendChild(rmEl)
|
|
|
|
|
2022-06-08 13:28:36 +02:00
|
|
|
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();
|
2022-06-08 17:26:30 +02:00
|
|
|
this.selectedTag = tag;
|
2022-06-08 13:28:36 +02:00
|
|
|
tag.menuLiEl.classList.add('selected');
|
|
|
|
this.loadAnnotationsForTag(tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
clearSelectedTag() {
|
2022-06-08 17:26:30 +02:00
|
|
|
this.selectedTag = null;
|
2022-06-08 13:28:36 +02:00
|
|
|
const selected = this.tagsEl.querySelectorAll('.selected');
|
|
|
|
selected.forEach((s) => s.classList.remove('selected'));
|
2022-06-08 17:26:30 +02:00
|
|
|
this.resetSelectedAnnotations()
|
2022-06-08 13:28:36 +02:00
|
|
|
//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 = "";
|
|
|
|
|
2022-06-08 17:26:30 +02:00
|
|
|
this.actionsEl = document.createElement('div');
|
|
|
|
this.actionsEl.classList.add('annotations-actions')
|
|
|
|
|
|
|
|
this.buildAnnotationActions();
|
|
|
|
|
|
|
|
this.annotationsEl.appendChild(this.actionsEl);
|
|
|
|
|
|
|
|
|
2022-06-08 13:28:36 +02:00
|
|
|
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');
|
2022-06-08 17:26:30 +02:00
|
|
|
const infoEl = document.createElement('span');
|
2022-06-08 13:28:36 +02:00
|
|
|
infoEl.classList.add('annotation-info');
|
|
|
|
|
2022-06-08 17:26:30 +02:00
|
|
|
const selectEl = document.createElement('input');
|
|
|
|
selectEl.type = 'checkbox';
|
|
|
|
selectEl.id = 'select-'+annotation.id_hash;
|
|
|
|
selectEl.addEventListener('change', (ev) => {
|
|
|
|
if (ev.target.checked) {
|
|
|
|
this.addAnnotationToSelection(annotation);
|
|
|
|
} else {
|
|
|
|
this.removeAnnotationFromSelection(annotation);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2022-06-08 13:28:36 +02:00
|
|
|
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);
|
2022-06-08 17:26:30 +02:00
|
|
|
liEl.appendChild(selectEl);
|
2022-06-08 13:28:36 +02:00
|
|
|
liEl.appendChild(infoEl);
|
|
|
|
ulEl.appendChild(liEl);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
this.annotationsEl.appendChild(ulEl);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-06-08 17:26:30 +02:00
|
|
|
resetSelectedAnnotations() {
|
|
|
|
this.selectedAnnotations = [];
|
|
|
|
|
|
|
|
this.annotationsEl.querySelectorAll("li input[type='checkbox']").forEach((box) => box.checked = false);
|
|
|
|
this.buildAnnotationActions()
|
|
|
|
}
|
|
|
|
|
|
|
|
addAnnotationToSelection(annotation) {
|
|
|
|
if (this.selectedAnnotations.indexOf(annotation) === -1){
|
|
|
|
this.selectedAnnotations.push(annotation);
|
|
|
|
}
|
|
|
|
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true;
|
|
|
|
this.buildAnnotationActions()
|
|
|
|
}
|
|
|
|
removeAnnotationFromSelection(annotation) {
|
|
|
|
if (this.selectedAnnotations.indexOf(annotation) !== -1){
|
|
|
|
this.selectedAnnotations.splice(this.selectedAnnotations.indexOf(annotation), 1)
|
|
|
|
}
|
2022-06-14 17:31:49 +02:00
|
|
|
this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = false;
|
2022-06-08 17:26:30 +02:00
|
|
|
this.buildAnnotationActions()
|
|
|
|
}
|
|
|
|
|
|
|
|
buildAnnotationActions() {
|
|
|
|
if(!this.actionsEl || !this.annotations.length) return
|
|
|
|
this.actionsEl.innerHTML = "";
|
|
|
|
|
|
|
|
const selectAllLabelEl = document.createElement('label');
|
|
|
|
selectAllLabelEl.innerText = `Select all`
|
|
|
|
const selectAllEl = document.createElement('input');
|
|
|
|
selectAllEl.type = 'checkbox';
|
|
|
|
selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length;
|
|
|
|
// selectAllEl.innerText = `Select all ${this.annotations.length} items`;
|
|
|
|
selectAllEl.addEventListener('change', (ev) => {
|
|
|
|
if(ev.target.checked) {
|
|
|
|
this.annotations.forEach((a) => this.addAnnotationToSelection(a));
|
|
|
|
} else {
|
|
|
|
this.resetSelectedAnnotations();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
selectAllLabelEl.appendChild(selectAllEl)
|
|
|
|
|
|
|
|
this.actionsEl.appendChild(selectAllLabelEl)
|
|
|
|
|
|
|
|
if(!this.selectedAnnotations.length) return;
|
|
|
|
|
|
|
|
const moveLabelEl = document.createElement('label');
|
|
|
|
moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items`
|
|
|
|
const moveSelectEl = document.createElement('select');
|
|
|
|
this.rootTag.descendants().forEach((tag, i) => {
|
|
|
|
const tagEl = document.createElement('option');
|
|
|
|
tagEl.value = tag.id;
|
|
|
|
if(tag.id == this.selectedTag.id){
|
|
|
|
tagEl.selected = true;
|
|
|
|
}
|
|
|
|
tagEl.innerHTML = tag.get_indented_name();
|
|
|
|
moveSelectEl.appendChild(tagEl);
|
|
|
|
});
|
|
|
|
moveSelectEl.addEventListener('change', (ev) => {
|
|
|
|
const tag = this.rootTag.find_by_id(ev.target.value);
|
|
|
|
console.log(tag);
|
|
|
|
this.moveSelectedAnnotations(tag);
|
|
|
|
})
|
|
|
|
moveLabelEl.appendChild(moveSelectEl)
|
|
|
|
|
|
|
|
|
|
|
|
this.actionsEl.appendChild(moveLabelEl)
|
|
|
|
}
|
|
|
|
|
2022-06-08 13:28:36 +02:00
|
|
|
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();
|
2022-06-08 17:26:30 +02:00
|
|
|
this.buildTagList();
|
2022-06-08 13:28:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
removeTag(tag) {
|
2022-06-08 17:26:30 +02:00
|
|
|
if (!confirm(`Do you want to delete ${tag.get_name()}`)) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-06-08 13:28:36 +02:00
|
|
|
// TODO: add button for this
|
|
|
|
const request = new Request("/tags/" + tag.id);
|
|
|
|
return fetch(request)
|
2022-06-08 17:26:30 +02:00
|
|
|
.then(response => {
|
|
|
|
if (response.status == 404) {
|
|
|
|
return [] // not existing tag surely has no annotations
|
|
|
|
} else {
|
|
|
|
return response.json()
|
|
|
|
}
|
|
|
|
})
|
2022-06-08 13:28:36 +02:00
|
|
|
.then(annotations => {
|
2022-06-08 17:26:30 +02:00
|
|
|
if (annotations.length) {
|
2022-06-08 13:28:36 +02:00
|
|
|
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();
|
2022-06-08 17:26:30 +02:00
|
|
|
this.buildTagList();
|
2022-06-08 13:28:36 +02:00
|
|
|
}
|
2022-06-08 17:26:30 +02:00
|
|
|
})
|
2022-06-08 13:28:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setTagColor(tag, color) {
|
|
|
|
tag.color = color;
|
|
|
|
this.buildTagList();
|
|
|
|
|
|
|
|
this.saveTags();
|
|
|
|
}
|
|
|
|
|
2022-06-08 18:22:04 +02:00
|
|
|
async moveSelectedAnnotations(tag) {
|
2022-06-08 13:28:36 +02:00
|
|
|
// TODO: add button for this
|
2022-06-08 18:22:04 +02:00
|
|
|
// alert(`This doesn't work yet! (move to tag ${tag.get_name()})`)
|
|
|
|
|
|
|
|
await Promise.all(this.selectedAnnotations.map(async (annotation) => {
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append('tag_id', tag.id)
|
|
|
|
return await fetch('/annotation/'+annotation.id, {
|
|
|
|
method: 'POST',
|
|
|
|
// headers: {
|
|
|
|
// 'Content-Type': 'application/json'
|
|
|
|
// },
|
|
|
|
body: formData //JSON.stringify({'tag_id': tag.id})
|
|
|
|
}).catch((e) => alert('Something went wrong saving the tags'));
|
|
|
|
}));
|
|
|
|
|
|
|
|
this.loadAnnotationsForTag(this.selectedTag)
|
2022-06-08 18:51:22 +02:00
|
|
|
this.loadTags() //updates the counts
|
2022-06-08 13:28:36 +02:00
|
|
|
}
|
|
|
|
|
2022-06-08 17:26:30 +02:00
|
|
|
async saveTags() {
|
2022-06-08 13:28:36 +02:00
|
|
|
const json = this.rootTag.export();
|
|
|
|
console.log('save', json)
|
2022-06-08 17:26:30 +02:00
|
|
|
const response = await fetch('/tags.json', {
|
|
|
|
method: 'PUT',
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
body: json
|
|
|
|
}).catch((e) => alert('Something went wrong saving the tags'));
|
|
|
|
|
2022-06-08 13:28:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
2022-06-08 17:26:30 +02:00
|
|
|
if (this.hasOwnProperty('name') && this.name !== null) {
|
2022-06-08 13:28:36 +02:00
|
|
|
return this.name;
|
|
|
|
}
|
|
|
|
return this.id;
|
|
|
|
}
|
|
|
|
|
2022-06-08 17:26:30 +02:00
|
|
|
get_indented_name() {
|
|
|
|
const name = this.get_name();
|
|
|
|
return ' '.repeat((this.depth() - 1) * 2) + '- ' + name
|
|
|
|
}
|
|
|
|
|
2022-06-08 13:28:36 +02:00
|
|
|
get_color() {
|
2022-06-08 17:26:30 +02:00
|
|
|
if (this.hasOwnProperty('color') && this.color !== null) {
|
2022-06-08 13:28:36 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|