From daf0e0dfd4e401099f8c4971e5305010a68f2349 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Thu, 23 Feb 2023 11:17:24 +0100 Subject: [PATCH] Annotation Player as rudimentary Web Component --- app/templates/index.html | 21 +--- app/www/annotations.js | 215 +++++++++++++++++++++++++++++++++------ 2 files changed, 186 insertions(+), 50 deletions(-) diff --git a/app/templates/index.html b/app/templates/index.html index 6395ed1..8914d49 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -168,29 +168,16 @@ padding: 10px; } - #annotations .svganim_player { + #annotations .svganim_player, + #annotations annotation-player { display: inline-block; position: relative; width: 300px; 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; - } + diff --git a/app/www/annotations.js b/app/www/annotations.js index 2227e66..f09b3de 100644 --- a/app/www/annotations.js +++ b/app/www/annotations.js @@ -1,3 +1,161 @@ +class AnnotationPlayer extends HTMLElement { + constructor() { + super(); + // We don't use constructor() because an element's attributes + // are unavailable until connected to the DOM. + } + + connectedCallback() { + // Create a shadow root + this.attachShadow({ mode: "open" }); + + const imgEl = document.createElement('img'); + const playerEl = document.createElement('div'); + + + imgEl.src = `/annotation/${this.annotation.id}.svg`; + imgEl.addEventListener('click', () => { + imgEl.style.display = 'none'; + new Annotator( + playerEl, + "tags.json", + this.annotation.url, + { is_player: true, crop_to_fit: true, autoplay: true } + ); + }) + + 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; */ + position: relative; + } + + .timecode { + position: absolute; + right: 100%; + width: 5%; + font-size: 8px; + } + + + .controls--playback input[type='range'] { + /* position: absolute; + z-index: 100; + bottom: 0; + left: 0; + right: 0; */ + width: 100%; + + } + + .controls button.paused, + .controls button.playing { + position: absolute; + left: 100%; + width: 30px; + height: 30px; + } + + .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; + } + + .gray { + position: absolute; + background: rgba(255, 255, 255, 0.7); + } + + + `; + + this.shadowRoot.appendChild(styleEl); + this.shadowRoot.appendChild(imgEl); + this.shadowRoot.appendChild(playerEl); + + } + + setAnnotation(annotation) { + this.annotation = annotation; + } +} + +window.customElements.define('annotation-player', AnnotationPlayer); + + class AnnotationManager { constructor(rootEl, tagsUrl) { this.rootEl = rootEl; @@ -86,7 +244,7 @@ class AnnotationManager { selectTag(tag) { this.clearSelectedTag(); - this.selectedTag = tag; + this.selectedTag = tag; tag.menuLiEl.classList.add('selected'); this.loadAnnotationsForTag(tag) } @@ -141,14 +299,12 @@ class AnnotationManager { 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('span'); - infoEl.classList.add('annotation-info'); + const playerEl = new AnnotationPlayer(); //document.createElement('annotation-player'); + playerEl.setAnnotation(annotation); const selectEl = document.createElement('input'); selectEl.type = 'checkbox'; - selectEl.id = 'select-'+annotation.id_hash; + selectEl.id = 'select-' + annotation.id_hash; selectEl.addEventListener('change', (ev) => { if (ev.target.checked) { this.addAnnotationToSelection(annotation); @@ -157,25 +313,14 @@ class AnnotationManager { } }) + const tag = this.rootTag.find_by_id(annotation.tag); console.log(tag) + const infoEl = document.createElement('span'); + infoEl.classList.add('annotation-info'); 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(selectEl); liEl.appendChild(infoEl); @@ -195,22 +340,26 @@ class AnnotationManager { } addAnnotationToSelection(annotation) { - if (this.selectedAnnotations.indexOf(annotation) === -1){ + if (this.selectedAnnotations.indexOf(annotation) === -1) { this.selectedAnnotations.push(annotation); } - this.annotationsEl.querySelector('#select-'+annotation.id_hash).checked = true; + this.annotationsEl.querySelector('#select-' + annotation.id_hash).checked = true; this.buildAnnotationActions() } removeAnnotationFromSelection(annotation) { - if (this.selectedAnnotations.indexOf(annotation) !== -1){ + if (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() } - + + /** + * Build the form items to select & move the annotations + * @returns undefined + */ buildAnnotationActions() { - if(!this.actionsEl || !this.annotations.length) return + if (!this.actionsEl || !this.annotations.length) return this.actionsEl.innerHTML = ""; const selectAllLabelEl = document.createElement('label'); @@ -220,7 +369,7 @@ class AnnotationManager { selectAllEl.checked = this.annotations.length === this.selectedAnnotations.length; // selectAllEl.innerText = `Select all ${this.annotations.length} items`; selectAllEl.addEventListener('change', (ev) => { - if(ev.target.checked) { + if (ev.target.checked) { this.annotations.forEach((a) => this.addAnnotationToSelection(a)); } else { this.resetSelectedAnnotations(); @@ -231,7 +380,7 @@ class AnnotationManager { this.actionsEl.appendChild(selectAllLabelEl) - if(!this.selectedAnnotations.length) return; + if (!this.selectedAnnotations.length) return; const moveLabelEl = document.createElement('label'); moveLabelEl.innerText = `Change tag for ${this.selectedAnnotations.length} items` @@ -239,7 +388,7 @@ class AnnotationManager { this.rootTag.descendants().forEach((tag, i) => { const tagEl = document.createElement('option'); tagEl.value = tag.id; - if(tag.id == this.selectedTag.id){ + if (tag.id == this.selectedTag.id) { tagEl.selected = true; } tagEl.innerHTML = tag.get_indented_name(); @@ -304,19 +453,19 @@ class AnnotationManager { async moveSelectedAnnotations(tag) { // TODO: add button for this // 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, { + 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) this.loadTags() //updates the counts }