1// Copyright (c) 2021-2025 GrayBits LLC 2 3import PopInInterface from 'gray_matter/utility/pop_in_interface' 4import * as Builder from 'gray_matter/utility/builder' 5import axios from 'axios' 6 7let current_annotation; 8let container; 9let image_container; 10let annotation_label; 11let annotation_details; 12let details_rich_text; 13let dragged_annotation; 14let dragged; 15let buffered_label; 16let buffered_details; 17let response; 18let popin; 19 20export async function openInterface(upload_id, options = {}){ 21 let existing = document.getElementById('annotations_interface'); 22 if (existing) existing.remove(); 23 24 let params = {} 25 if (options.annotations) params['annotations'] = options.annotations; 26 popin = new PopInInterface( 27 document.body, 28 { 29 url: `/gray_matter/uploads/${upload_id}/annotations_interface`, 30 method: 'put', 31 params: params, 32 setup: setup, 33 preserve_popins: options.preserve_popins 34 } 35 ); 36 return () => { 37 return new Promise((resolve, reject) => { 38 response = resolve; 39 }); 40 } 41} 42 43function setup(popin){ 44 container = document.getElementById('upload_annotations'); 45 image_container = document.getElementById('annotated_image_container'); 46 annotation_label = document.querySelector('input[name="annotation_label"]'); 47 annotation_details = document.querySelector('.rich_text_input'); 48 details_rich_text = annotation_details.closest('.rich_text_editor').interface; 49 popin.element.querySelector('button.save_annotation').addEventListener('click', saveAnnotation); 50 popin.element.querySelector('button.save_annotations').addEventListener('click', saveAnnotations); 51 popin.element.querySelector('button.delete').addEventListener('click', deleteAnnotation); 52 popin.element.querySelector('#annotated_image_container').addEventListener('click', selectAnnotation); 53 annotation_label.addEventListener('input', syncLabel); 54 image_container.addEventListener('mousedown', startDrag); 55 document.body.addEventListener('cancelpopin', cancel); 56 image_container.querySelectorAll('.annotation').forEach(setupAnnotation); 57} 58 59function startDrag(e){ 60 dragged_annotation = e.target.closest('.annotation'); 61 if (!dragged_annotation) return; 62 63 dragged = false; 64 image_container.addEventListener('mousemove', drag); 65 image_container.addEventListener('mouseup', endDrag); 66} 67 68function drag(e){ 69 if (!dragged_annotation) return; 70 71 dragged = true; 72 setAnnotationPosition(e, dragged_annotation); 73} 74 75function endDrag(e){ 76 dragged_annotation = undefined; 77 image_container.removeEventListener('mousemove', drag); 78 image_container.removeEventListener('mouseup', endDrag); 79} 80 81function setAnnotationPosition(e, annotation){ 82 let rect = image_container.getBoundingClientRect(); 83 let x = (e.pageX - rect.x) / rect.width; 84 let y = (e.pageY - rect.y) / rect.height; 85 86 if (x < 0) x = 0; 87 if (y < 0) y = 0; 88 if (x > 1) x = 1; 89 if (y > 1) y = 1; 90 91 annotation.annotation.x = x; 92 annotation.annotation.y = y; 93 annotation.style.left = `${100 * x}%`; 94 annotation.style.top = `${100 * y}%`; 95} 96 97function selectAnnotation(e){ 98 if (dragged){ 99 dragged = false; 100 return; 101 } 102 if (current_annotation){ 103 clearAnnotation(); 104 current_annotation = undefined; 105 } 106 107 let annotation = e.target.closest('.annotation'); 108 109 if (annotation && !e.target.classList.contains('annotation_label_positioner')){ 110 current_annotation = annotation; 111 } else { 112 current_annotation = Builder.createFromHTML('<div class="annotation"><div class="annotation_label_positioner"><div class="annotation_label">New</div></div></div>'); 113 current_annotation.annotation = { x: 0, y: 0, label: 'New', details: ''}; 114 current_annotation.label = current_annotation.querySelector('.annotation_label'); 115 setAnnotationPosition(e, current_annotation); 116 image_container.append(current_annotation); 117 current_annotation.offsetHeight; 118 current_annotation.classList.add('added'); 119 } 120 container.classList.add('selected'); 121 current_annotation.classList.add('active'); 122 setInputValues(current_annotation.annotation.label, current_annotation.annotation.details); 123 buffered_label = current_annotation.annotation.label; 124 buffered_details = current_annotation.annotation.details; 125} 126 127function setupAnnotation(annotation){ 128 annotation.annotation = { 129 x: parseFloat(annotation.dataset.x), 130 y: parseFloat(annotation.dataset.y), 131 label: annotation.dataset.label, 132 details: annotation.dataset.details 133 }; 134 annotation.style.left = `${100 * parseFloat(annotation.dataset.x)}%`; 135 annotation.style.top = `${100 * parseFloat(annotation.dataset.y)}%`; 136 annotation.label = annotation.querySelector('.annotation_label'); 137 annotation.classList.add('added'); 138} 139 140function saveAnnotation(e){ 141 if (current_annotation){ 142 details_rich_text.syncInput(); 143 current_annotation.annotation.label = annotation_label.value; 144 current_annotation.annotation.details = annotation_details.value; 145 clearAnnotation(); 146 } 147} 148 149function saveAnnotations(){ 150 let annotations = [...image_container.querySelectorAll('.annotation')].map((a) => a.annotation); 151 document.body.removeEventListener('cancelpopin', cancel); 152 popin.cancel(); 153 response(annotations); 154} 155 156function cancel(){ 157 response(false); 158 document.body.removeEventListener('cancelpopin', cancel); 159} 160 161function clearAnnotation(){ 162 container.classList.remove('selected'); 163 current_annotation.classList.remove('active'); 164 current_annotation = undefined; 165} 166 167function deleteAnnotation(e){ 168 current_annotation.remove(); 169 clearAnnotation(); 170} 171 172function setInputValues(label, details){ 173 annotation_label.value = label; 174 annotation_details.value = details; 175 details_rich_text.syncFromInput(); 176} 177 178function syncLabel(){ 179 if (!current_annotation) return; 180 181 current_annotation.label.innerHTML = annotation_label.value; 182};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.