UNPKG

lichen-annotate-pdf

Version:

Annotation layer for pdf.js in vue fork of Aaron Leong pdf-annotate-vue

402 lines (353 loc) 11.9 kB
import PDFJSAnnotate from '../PDFJSAnnotate'; import config from '../config'; import { addEventListener, removeEventListener } from './event'; import { BORDER_COLOR, disableUserSelect, enableUserSelect, findSVGContainer, findSVGAtPoint, getOffsetAnnotationRect, getMetadata, convertToSvgPoint } from './utils'; let _enabled = false; let isDragging = false; let overlay; let dragOffsetX, dragOffsetY, dragStartX, dragStartY; const OVERLAY_BORDER_SIZE = 3; /** * Create an overlay for editing an annotation. * * @param {Element} target The annotation element to apply overlay for */ function createEditOverlay (target) { destroyEditOverlay(); overlay = document.createElement('div'); let anchor = document.createElement('a'); let parentNode = findSVGContainer(target).parentNode; let id = target.getAttribute('data-pdf-annotate-id'); let rect = getOffsetAnnotationRect(target); let styleLeft = rect.left - OVERLAY_BORDER_SIZE; let styleTop = rect.top - OVERLAY_BORDER_SIZE; overlay.setAttribute('id', 'pdf-annotate-edit-overlay'); overlay.setAttribute('data-target-id', id); overlay.style.boxSizing = 'content-box'; overlay.style.position = 'absolute'; overlay.style.top = `${styleTop}px`; overlay.style.left = `${styleLeft}px`; overlay.style.width = `${rect.width}px`; overlay.style.height = `${rect.height}px`; overlay.style.border = `${OVERLAY_BORDER_SIZE}px solid ${BORDER_COLOR}`; overlay.style.borderRadius = `${OVERLAY_BORDER_SIZE}px`; overlay.style.zIndex = 20100; anchor.innerHTML = '×'; anchor.setAttribute('href', 'javascript://'); anchor.style.background = '#fff'; anchor.style.borderRadius = '20px'; anchor.style.border = '1px solid #bbb'; anchor.style.color = '#bbb'; anchor.style.fontSize = '16px'; anchor.style.padding = '2px'; anchor.style.textAlign = 'center'; anchor.style.textDecoration = 'none'; anchor.style.position = 'absolute'; anchor.style.top = '-13px'; anchor.style.right = '-13px'; anchor.style.width = '25px'; anchor.style.height = '25px'; overlay.appendChild(anchor); parentNode.appendChild(overlay); document.addEventListener('click', handleDocumentClick); document.addEventListener('keyup', handleDocumentKeyup); document.addEventListener('mousedown', handleDocumentMousedown); anchor.addEventListener('click', deleteAnnotation); anchor.addEventListener('mouseover', () => { anchor.style.color = '#35A4DC'; anchor.style.borderColor = '#999'; anchor.style.boxShadow = '0 1px 1px #ccc'; }); anchor.addEventListener('mouseout', () => { anchor.style.color = '#bbb'; anchor.style.borderColor = '#bbb'; anchor.style.boxShadow = ''; }); overlay.addEventListener('mouseover', () => { if (!isDragging) { anchor.style.display = ''; } }); overlay.addEventListener('mouseout', () => { anchor.style.display = 'none'; }); } /** * Destroy the edit overlay if it exists. */ function destroyEditOverlay () { if (overlay) { overlay.parentNode.removeChild(overlay); overlay = null; } document.removeEventListener('click', handleDocumentClick); document.removeEventListener('keyup', handleDocumentKeyup); document.removeEventListener('mousedown', handleDocumentMousedown); document.removeEventListener('mousemove', handleDocumentMousemove); document.removeEventListener('mouseup', handleDocumentMouseup); enableUserSelect(); } /** * Delete currently selected annotation */ function deleteAnnotation () { if (!overlay) { return; } let annotationId = overlay.getAttribute('data-target-id'); let svg = overlay.parentNode.querySelector(config.annotationSvgQuery()); let { documentId } = getMetadata(svg); PDFJSAnnotate.getStoreAdapter().deleteAnnotation(documentId, annotationId).then(() => { let nodes = document.querySelectorAll(`[data-pdf-annotate-id="${annotationId}"]`); [...nodes].forEach((n) => { n.parentNode.removeChild(n); }); }); destroyEditOverlay(); } /** * Handle document.click event * * @param {Event} e The DOM event that needs to be handled */ function handleDocumentClick (e) { if (!findSVGAtPoint(e.clientX, e.clientY)) { return; } // Remove current overlay let overlay = document.getElementById('pdf-annotate-edit-overlay'); if (overlay) { if (isDragging || e.target === overlay) { return; } destroyEditOverlay(); } } /** * Handle document.keyup event * * @param {KeyboardEvent} e The DOM event that needs to be handled */ function handleDocumentKeyup (e) { // keyCode is deprecated, so prefer the newer "key" method if possible let keyTest; if (e.key) { keyTest = e.key.toLowerCase() === 'delete' || e.key.toLowerCase() === 'backspace'; } else { keyTest = e.keyCode === 8 || e.keyCode === 46; } if (overlay && keyTest && e.target.nodeName.toLowerCase() !== 'textarea' && e.target.nodeName.toLowerCase() !== 'input') { e.preventDefault(); deleteAnnotation(); } } /** * Handle document.mousedown event * * @param {Event} e The DOM event that needs to be handled */ function handleDocumentMousedown (e) { if (e.target !== overlay) { return; } // Highlight and strikeout annotations are bound to text within the document. // It doesn't make sense to allow repositioning these types of annotations. let annotationId = overlay.getAttribute('data-target-id'); let target = document.querySelector(`[data-pdf-annotate-id="${annotationId}"]`); let type = target.getAttribute('data-pdf-annotate-type'); if (type === 'highlight' || type === 'strikeout') { return; } isDragging = true; dragOffsetX = e.clientX; dragOffsetY = e.clientY; dragStartX = overlay.offsetLeft; dragStartY = overlay.offsetTop; overlay.style.background = 'rgba(255, 255, 255, 0.7)'; overlay.style.cursor = 'move'; overlay.querySelector('a').style.display = 'none'; document.addEventListener('mousemove', handleDocumentMousemove); document.addEventListener('mouseup', handleDocumentMouseup); disableUserSelect(); } /** * Handle document.mousemove event * * @param {Event} e The DOM event that needs to be handled */ function handleDocumentMousemove (e) { let parentNode = overlay.parentNode; let rect = parentNode.getBoundingClientRect(); let y = (dragStartY + (e.clientY - dragOffsetY)); let x = (dragStartX + (e.clientX - dragOffsetX)); let minY = 0; let maxY = rect.height; let minX = 0; let maxX = rect.width; if (y > minY && y + overlay.offsetHeight < maxY) { overlay.style.top = `${y}px`; } if (x > minX && x + overlay.offsetWidth < maxX) { overlay.style.left = `${x}px`; } } /** * Handle document.mouseup event * * @param {Event} e The DOM event that needs to be handled */ function handleDocumentMouseup (e) { let annotationId = overlay.getAttribute('data-target-id'); let target = document.querySelectorAll(`[data-pdf-annotate-id="${annotationId}"]`); let type = target[0].getAttribute('data-pdf-annotate-type'); let svg = overlay.parentNode.querySelector(config.annotationSvgQuery()); let { documentId } = getMetadata(svg); overlay.querySelector('a').style.display = ''; PDFJSAnnotate.getStoreAdapter().getAnnotation(documentId, annotationId).then((annotation) => { let attribX = 'x'; let attribY = 'y'; if (['circle', 'fillcircle', 'emptycircle'].indexOf(type) > -1) { attribX = 'cx'; attribY = 'cy'; } if (type === 'point') { // Broken /* [...target].forEach((t, i) => { let moveTo = { x: overlay.offsetLeft + 3, y: overlay.offsetTop + 3 }; t.setAttribute(attribX, moveTo.x); t.setAttribute(attribY, moveTo.y); annotation[attribX] = moveTo.x; annotation[attribY] = moveTo.y; }); */ return; } else if (['area', 'highlight', 'textbox', 'circle', 'fillcircle', 'emptycircle'].indexOf(type) > -1) { let modelStart = convertToSvgPoint([dragStartX, dragStartY], svg); let modelEnd = convertToSvgPoint([overlay.offsetLeft, overlay.offsetTop], svg); let modelDelta = { x: modelEnd[0] - modelStart[0], y: modelEnd[1] - modelStart[1] }; if (type === 'textbox') { target = [target[0].firstChild]; } [...target].forEach((t, i) => { let modelX = parseInt(t.getAttribute(attribX), 10); let modelY = parseInt(t.getAttribute(attribY), 10); if (modelDelta.y !== 0) { modelY = modelY + modelDelta.y; t.setAttribute(attribY, modelY); if (annotation && annotation.rectangles && i < annotation.rectangles.length) { annotation.rectangles[i].y = modelY; } else if (annotation && annotation[attribY]) { annotation[attribY] = modelY; } } if (modelDelta.x !== 0) { modelX = modelX + modelDelta.x; t.setAttribute(attribX, modelX); if (annotation && annotation.rectangles && i < annotation.rectangles.length) { annotation.rectangles[i].x = modelX; } else if (annotation && annotation[attribX]) { annotation[attribX] = modelX; } } }); } else if (type === 'strikeout') { return; // let { deltaX, deltaY } = getDelta('x1', 'y1'); // [...target].forEach(target, (t, i) => { // if (deltaY !== 0) { // t.setAttribute('y1', parseInt(t.getAttribute('y1'), 10) + deltaY); // t.setAttribute('y2', parseInt(t.getAttribute('y2'), 10) + deltaY); // annotation.rectangles[i].y = parseInt(t.getAttribute('y1'), 10); // } // if (deltaX !== 0) { // t.setAttribute('x1', parseInt(t.getAttribute('x1'), 10) + deltaX); // t.setAttribute('x2', parseInt(t.getAttribute('x2'), 10) + deltaX); // annotation.rectangles[i].x = parseInt(t.getAttribute('x1'), 10); // } // }); } else if (type === 'drawing' || type === 'arrow') { // Do nothing as currently broken /* let modelStart = convertToSvgPoint([dragStartX, dragStartY], svg); let modelEnd = convertToSvgPoint([overlay.offsetLeft, overlay.offsetTop], svg); let modelDelta = { x: modelEnd[0] - modelStart[0], y: modelEnd[1] - modelStart[1] }; annotation.lines.forEach((line, i) => { let [x, y] = annotation.lines[i]; annotation.lines[i][0] = x + modelDelta.x; annotation.lines[i][1] = y + modelDelta.y; }); target[0].parentNode.removeChild(target[0]); appendChild(svg, annotation); */ return; } PDFJSAnnotate.getStoreAdapter().editAnnotation(documentId, annotationId, annotation); }); setTimeout(() => { isDragging = false; }, 0); overlay.style.background = ''; overlay.style.cursor = ''; document.removeEventListener('mousemove', handleDocumentMousemove); document.removeEventListener('mouseup', handleDocumentMouseup); enableUserSelect(); } /** * Handle annotation.click event * * @param {Element} e The annotation element that was clicked */ function handleAnnotationClick (target) { createEditOverlay(target); } /** * Enable edit mode behavior. */ export function enableEdit () { if (_enabled) { return; } _enabled = true; addEventListener('annotation:click', handleAnnotationClick); }; /** * Disable edit mode behavior. */ export function disableEdit (val) { if (val) { destroyEditOverlay(); _enabled = false; removeEventListener('annotation:click', handleAnnotationClick); } else { destroyEditOverlay(); if (!_enabled) { return; } _enabled = false; removeEventListener('annotation:click', handleAnnotationClick); } };