UNPKG

annotorious-tahqiq

Version:
291 lines (253 loc) 10.7 kB
import { Annotation, TextGranularity } from "../types/Annotation"; import { CancelButton } from "./CancelButton"; import { DeleteButton } from "./DeleteButton"; import { SaveButton } from "./SaveButton"; import "../styles/AnnotationBlock.scss"; /** * HTML div element associated with an annotation, which can be made editable to udpate * or delete its associated annotation. */ class AnnotationBlock extends HTMLElement { annotation: Annotation; bodyElement: HTMLDivElement; editorId?: string; labelElement: HTMLHeadingElement; clickable: boolean; onCancel: () => void; onClick: (annotationBlock: AnnotationBlock) => void; onDelete: (annotationBlock: AnnotationBlock) => void; onDrag: (start: boolean) => void; onReorder: (evt: DragEvent) => void; onSave: (annotationBlock: AnnotationBlock) => Promise<void>; updateAnnotorious: (annotation: Annotation) => void; /** * Instantiate an annotation block. * * @param {object} props Properties passed to this annotation block. * @param {Annotation} props.annotation Annotation to associate with this annotation block. * @param {boolean} props.editable True if this annotation block should be editable, * otherwise false. * @param {Function} props.onCancel Cancel annotation handler function. * @param {Function} props.onClick Click handler function. * @param {Function} props.onDelete Delete annotation handler function. * @param {Function} props.onDrag Drag start/end handler function. * @param {Function} props.onSave Save annotation handler function. * @param {Function} props.onReorder Drop event handler function. * @param {Function} props.updateAnnotorious Function that updates this annotation in the * Annotorious display. */ constructor(props: { annotation: Annotation; editable: boolean; onCancel: () => void; onClick: (annotationBlock: AnnotationBlock) => void; onDelete: (annotationBlock: AnnotationBlock) => void; onDrag: (start: boolean) => void; onReorder: (evt: DragEvent) => void; onSave: (annotationBlock: AnnotationBlock) => Promise<void>; updateAnnotorious: (annotation: Annotation) => void; }) { super(); this.annotation = props.annotation; this.onCancel = props.onCancel; this.onClick = props.onClick; this.onDelete = props.onDelete; this.onDrag = props.onDrag; this.onReorder = props.onReorder; this.onSave = props.onSave; this.updateAnnotorious = props.updateAnnotorious; // Set class this.setAttribute("class", "tahqiq-block-display"); // Create and append label element (div with text, contenteditable in edit mode) this.labelElement = document.createElement("h3"); this.labelElement.setAttribute("class", "tahqiq-label-display"); this.labelElement.setAttribute("data-placeholder", "Optional label"); // Create and append body element (div with text in read-only, TinyMCE in edit mode) this.bodyElement = document.createElement("div"); this.bodyElement.setAttribute("class", "tahqiq-body-display"); if ( Array.isArray(this.annotation.body) && this.annotation.body.length > 0 ) { this.labelElement.innerHTML = this.annotation.body[0].label || ""; this.bodyElement.innerHTML = this.annotation.body[0].value; } if (this.annotation.textGranularity !== TextGranularity.LINE) { // line-level annotations do not have labels this.append(this.labelElement); } this.append(this.bodyElement); if (this.annotation.id) { this.dataset.annotationId = this.annotation.id; } // Set click event listener this.clickable = true; this.addEventListener("click", () => { // bail out if clicking disabled if (! this.clickable) { return; } // if you click on this block, and it is in read-only mode, make editable if (!this.classList.contains("tahqiq-block-editor")) { this.onClick(this); // selection event not fired in this case, so make editable this.makeEditable(); } }); // Set drag event listeners this.draggable = true; this.addEventListener("dragstart", this.startDrag.bind(this)); this.addEventListener("dragover", (evt) => { evt.preventDefault(); // required to allow drop }); this.addEventListener("dragend", () => { this.onDrag(false); // Hide drop zones }); this.addEventListener("dragenter", (evt) => { if (evt.currentTarget instanceof AnnotationBlock) this.classList.add("tahqiq-drag-target"); }); this.addEventListener("dragleave", (evt) => { if (evt.currentTarget instanceof AnnotationBlock) this.classList.remove("tahqiq-drag-target"); }); this.addEventListener("drop", this.onReorder); // Set editable if needed if (props.editable) { this.makeEditable(); } } /** * Utility function to encode HTML into entities for use in TinyMCE. * * @param {string} content The HTML content to be encoded. * @returns {string} Content with encoded HTML entities. */ encodeHTML(content: string): string { return content.replace(/[&<>'"]/g, (tag: string) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;", }[tag] || tag)); } /** * Makes an existing annotation block editable by adding TinyMCE * and adding Save, Cancel, and Delete buttons. */ makeEditable(): void { if (this.getAttribute("class") == "tahqiq-block-editor") { return; } this.setAttribute("class", "tahqiq-block-editor"); this.draggable = false; // make label editable this.labelElement.setAttribute("contenteditable", "true"); this.labelElement.setAttribute("class", "tahqiq-label-editor"); // add TinyMCE window.tinyConfig.init_instance_callback = this.setEditorId.bind(this); const editor = document.createElement("tinymce-editor"); editor.setAttribute("config", "tinyConfig"); editor.setAttribute("api-key", window.tinyApiKey); editor.innerHTML = this.encodeHTML(this.bodyElement.innerHTML); this.bodyElement.setAttribute("class", "tahqiq-body-editor"); this.bodyElement.innerHTML = ""; this.bodyElement.append(editor); this.bodyElement.focus(); // add save and cancel buttons this.append(new SaveButton(this)); this.append(new CancelButton(this)); // if this is a saved annotation, add delete button if (this.annotation.id) { this.append(new DeleteButton(this)); } } /** * Converts an annotation block that has been made editable back to display format. * * @param {boolean} updateAnnotation True if this annotation block's annotation should be * updated in Annotorious, otherwise false. */ makeReadOnly(updateAnnotation?: boolean): void { this.setAttribute("class", "tahqiq-block-display"); this.draggable = true; this.labelElement.setAttribute("contenteditable", "false"); this.labelElement.setAttribute("class", "tahqiq-label-display"); this.bodyElement.setAttribute("class", "tahqiq-body-display"); // restore the original content if (updateAnnotation) { if (Array.isArray(this.annotation.body) && this.annotation.body.length > 0) { this.bodyElement.innerHTML = this.annotation.body[0].value; this.labelElement.innerHTML = this.annotation.body[0].label || ""; } // add the annotation again to update the image selection region, // in case the user has modified it and wants to cancel this.updateAnnotorious(this.annotation); } else { // otherwise, set the content to TinyMCE editor's content const editorContent = window.tinymce.get(this.editorId).getContent(); this.bodyElement.innerHTML = editorContent; } // remove buttons (or should we just hide them?) this.querySelectorAll("button").forEach((el) => el.remove()); } /** * Update the annotation on this annotation block. * * @param {Annotation} annotation Updated annotation. */ setAnnotation(annotation: Annotation) { this.annotation = annotation; } /** * Set the TinyMCE editor id on this annotation block. * * @param {any} editor The TinyMCE editor instance */ // eslint-disable-next-line @typescript-eslint/no-explicit-any setEditorId(editor:any) { this.editorId = editor.id; } /** * On drag start, set the drag data to the dragged item's ID, and show drop zones. * * @param {DragEvent} evt The "dragstart" DragEvent */ startDrag(evt: DragEvent) { const target = evt.target as AnnotationBlock; evt.dataTransfer?.setData("text", target?.dataset?.annotationId || ""); this.onDrag(true); } /** * When a block is dragged over, give it "tahqiq-drag-target" style; else, remove that style. * * @param {boolean} draggedOver Boolean indicating whether this block is being dragged over. */ setDraggedOver(draggedOver: boolean): void { if (draggedOver) { this.classList.add("tahqiq-drag-target"); } else { this.classList.remove("tahqiq-drag-target"); } } /** * Set whether or not this annotation block can be drag and dropped. * Should not be draggable during editing or loading. * * @param {boolean} draggable Boolean indicating if this block is draggable. */ setDraggable(draggable: boolean): void { this.draggable = draggable; } /** * Set whether or not this annotation block can be clicked * to select. Should not be clickable when another block is * being edited. * * @param {boolean} clickable Boolean indicating if this block is clickable */ setClickable(clickable: boolean): void { this.clickable = clickable; } } export { AnnotationBlock };