add highlight annotations
This commit is contained in:
parent
7e1a282ad6
commit
4df34b28e5
9 changed files with 359 additions and 34 deletions
|
@ -10,6 +10,10 @@
|
|||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
.OMLQuote img {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.OMLAnnotation .OMLQuoteBackground {
|
||||
position: absolute;
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
oml.SELECTION = 0
|
||||
oml.HIGHLIGHT = 1
|
||||
|
||||
oml.ui.annotation = function(annotation, $iframe) {
|
||||
var value = Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>')
|
||||
if (annotation.type == oml.HIGHLIGHT) {
|
||||
let coord = annotation.coords[0].map(p => parseInt(p)).join(',')
|
||||
let image = `/${oml.user.ui.item}/2048p${parseInt(annotation.page)},${coord}.jpg`
|
||||
value = `<img src="${image}">`
|
||||
}
|
||||
var $quoteText = Ox.Element()
|
||||
.addClass('OxSelectable OMLQuote')
|
||||
.html(Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>'))
|
||||
.html(value)
|
||||
.on({
|
||||
click: function(event) {
|
||||
var id
|
||||
|
|
|
@ -151,7 +151,7 @@ oml.ui.viewer = function() {
|
|||
height: '100%',
|
||||
border: 0
|
||||
}).onMessage(function(data, event) {
|
||||
console.log('got', event, data)
|
||||
// console.log('got', event, data, data.page)
|
||||
if (event == 'addAnnotation') {
|
||||
addAnnotation(data);
|
||||
var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents)
|
||||
|
@ -217,7 +217,7 @@ oml.ui.viewer = function() {
|
|||
}
|
||||
var map = {}
|
||||
map[sortKey] = function(value) {
|
||||
return value.toString();
|
||||
return value ? value.toString() : '';
|
||||
}
|
||||
annotations = Ox.sortBy(annotations, sortKey, map)
|
||||
oml.$ui.annotationFolder.empty();
|
||||
|
|
62
static/reader/pdf.css
Normal file
62
static/reader/pdf.css
Normal file
|
@ -0,0 +1,62 @@
|
|||
|
||||
.toolbarButton.cropFile::before,
|
||||
.secondaryToolbarButton.cropFile::before {
|
||||
mask-image: url(pdf/toolbarButton-crop.png);
|
||||
}
|
||||
.toolbarButton.embedPage::before,
|
||||
.secondaryToolbarButton.embedPage::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGxpbmUgeDE9Ijg4IiB5MT0iNTYiIHgyPSIyNCIgeTI9IjEyOCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMjQiIHkxPSIxMjgiIHgyPSI4OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMTY4IiB5MT0iNTYiIHgyPSIyMzIiIHkyPSIxMjgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjQ4Ii8+PGxpbmUgeDE9IjIzMiIgeTE9IjEyOCIgeDI9IjE2OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48L3N2Zz48IS0teyJjb2xvciI6ImRlZmF1bHQiLCJuYW1lIjoic3ltYm9sRW1iZWQiLCJ0aGVtZSI6Im94bWVkaXVtIn0tLT4=);
|
||||
}
|
||||
@media screen and (min-resolution: 2dppx) {
|
||||
.toolbarButton.cropFile::before,
|
||||
.secondaryToolbarButton.cropFile::before {
|
||||
mask-image: url(pdf/toolbarButton-crop@2x.png);
|
||||
}
|
||||
.toolbarButton.embedPage::before,
|
||||
.secondaryToolbarButton.embedPage::before {
|
||||
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGxpbmUgeDE9Ijg4IiB5MT0iNTYiIHgyPSIyNCIgeTI9IjEyOCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMjQiIHkxPSIxMjgiIHgyPSI4OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMTY4IiB5MT0iNTYiIHgyPSIyMzIiIHkyPSIxMjgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjQ4Ii8+PGxpbmUgeDE9IjIzMiIgeTE9IjEyOCIgeDI9IjE2OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48L3N2Zz48IS0teyJjb2xvciI6ImRlZmF1bHQiLCJuYW1lIjoic3ltYm9sRW1iZWQiLCJ0aGVtZSI6Im94bWVkaXVtIn0tLT4=);
|
||||
}
|
||||
}
|
||||
|
||||
.verticalToolbarSeparator.hiddenMediumView,
|
||||
#print,
|
||||
#secondaryPrint,
|
||||
#openFile,
|
||||
#secondaryOpenFile,
|
||||
#editorModeButtons {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page .crop-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
//background: rgba(0,0,0,0.5);
|
||||
cursor: crosshair;
|
||||
z-index: 100;
|
||||
}
|
||||
.page .crop-overlay.inactive {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.page .crop-overlay canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.page .highlights {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
//background: rgba(0,0,0,0.5);
|
||||
z-index: 101;
|
||||
pointer-events: none;
|
||||
}
|
||||
.page .highlights canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -1,6 +1,97 @@
|
|||
var id = document.location.pathname.split('/')[1];
|
||||
var annotations = [];
|
||||
var currentPage = 1, rendered = false
|
||||
var highlightInactive = true
|
||||
var selectedAnnotation
|
||||
|
||||
const SELECTION = 0
|
||||
const HIGHLIGHT = 1
|
||||
|
||||
|
||||
var div = document.createElement("div")
|
||||
div.innerHTML = `
|
||||
<button id="cropFile" class="toolbarButton cropFile hiddenLargeView" title="Highlight" tabindex="30" data-l10n-id="crop_file">
|
||||
<span data-l10n-id="crop_file_label">Highlight</span>
|
||||
</button>
|
||||
`
|
||||
var cropFile = div.querySelector("#cropFile")
|
||||
|
||||
document.querySelector('#toolbarViewerRight').insertBefore(cropFile, document.querySelector('#toolbarViewerRight').firstChild)
|
||||
|
||||
// secondary menu
|
||||
div.innerHTML = `
|
||||
<button id="secondaryCropFile" class="secondaryToolbarButton visibleMediumView cropFile" title="Highlight" tabindex="50" data-l10n-id="crop">
|
||||
<span data-l10n-id="crop_label">Highlight</span>
|
||||
</button>
|
||||
`
|
||||
var secondaryCropFile = div.querySelector("#secondaryCropFile")
|
||||
document.querySelector('#secondaryToolbarButtonContainer').insertBefore(
|
||||
secondaryCropFile,
|
||||
document.querySelector('#secondaryToolbarButtonContainer').firstChild
|
||||
)
|
||||
|
||||
function initOverlay() {
|
||||
document.querySelectorAll('#cropFile,.secondaryToolbarButton.cropFile').forEach(btn => {
|
||||
btn.addEventListener('click', event=> {
|
||||
if (highlightInactive) {
|
||||
event.target.style.background = 'red'
|
||||
highlightInactive = false
|
||||
document.querySelectorAll('.crop-overlay.inactive').forEach(element => {
|
||||
element.classList.remove('inactive')
|
||||
})
|
||||
} else {
|
||||
event.target.style.background = ''
|
||||
highlightInactive = true
|
||||
document.querySelectorAll('.crop-overlay').forEach(element => {
|
||||
element.classList.add('inactive')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
PDFViewerApplication.initializedPromise.then(function() {
|
||||
PDFViewerApplication.pdfViewer.eventBus.on("pagesinit", function(event) {
|
||||
/*
|
||||
document.querySelector('#viewerContainer').addEventListener('scroll', event => {
|
||||
if (window.parent && window.parent.postMessage) {
|
||||
if (first) {
|
||||
first = false
|
||||
} else {
|
||||
window.parent.postMessage({event: 'scrolled', top: event.target.scrollTop})
|
||||
}
|
||||
}
|
||||
})
|
||||
*/
|
||||
})
|
||||
PDFViewerApplication.pdfViewer.eventBus.on("pagerender", function(event) {
|
||||
var page = event.pageNumber.toString()
|
||||
var div = event.source.div
|
||||
var overlay = document.createElement('div')
|
||||
overlay.classList.add('crop-overlay')
|
||||
overlay.id = 'overlay' + page
|
||||
if (highlightInactive) {
|
||||
overlay.classList.add('inactive')
|
||||
}
|
||||
div.appendChild(overlay)
|
||||
|
||||
renderHighlightSelectionOverlay(overlay, id, page, event.source)
|
||||
var highlights = document.createElement('div')
|
||||
highlights.classList.add('highlights')
|
||||
highlights.id = 'highlights' + page
|
||||
var canvas = document.createElement('canvas')
|
||||
highlights.appendChild(canvas)
|
||||
div.appendChild(highlights)
|
||||
renderHighlights(page)
|
||||
})
|
||||
PDFViewerApplication.eventBus.on('pagerendered', function(event) {
|
||||
loadAnnotations(event.pageNumber)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.PDFViewerApplication ? initOverlay() : document.addEventListener("webviewerloaded", initOverlay)
|
||||
})
|
||||
|
||||
|
||||
Ox.load({
|
||||
'UI': {
|
||||
|
@ -25,7 +116,15 @@ Ox.load({
|
|||
document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64;
|
||||
}
|
||||
}, delay)
|
||||
var oldSelection = selectedAnnotation
|
||||
selectedAnnotation = data.id
|
||||
selectAnnotation(data.id)
|
||||
if (oldSelection) {
|
||||
var old = annotations.filter(function(a) { return a.id == oldSelection })[0]
|
||||
if (old && old.type == HIGHLIGHT) {
|
||||
renderHighlights(old.page)
|
||||
}
|
||||
}
|
||||
} else if (event == 'addAnnotation') {
|
||||
createAnnotation()
|
||||
} else if (event == 'addAnnotations') {
|
||||
|
@ -37,7 +136,11 @@ Ox.load({
|
|||
}
|
||||
data.annotations.forEach(function(annotation) {
|
||||
annotations.push(annotation)
|
||||
if (annotation.type == HIGHLIGHT) {
|
||||
renderHighlights(annotation.page)
|
||||
} else {
|
||||
renderAnnotation(annotation)
|
||||
}
|
||||
})
|
||||
} else if (event == 'removeAnnotation') {
|
||||
removeAnnotation(data.id)
|
||||
|
@ -75,18 +178,6 @@ window.addEventListener('mouseup', function(event) {
|
|||
}
|
||||
})
|
||||
|
||||
function bindEvents() {
|
||||
if (!window.PDFViewerApplication || !window.PDFViewerApplication.eventBus) {
|
||||
setTimeout(bindEvents, 10)
|
||||
return
|
||||
}
|
||||
PDFViewerApplication.eventBus.on('pagerendered', function(event) {
|
||||
loadAnnotations(event.pageNumber)
|
||||
})
|
||||
}
|
||||
|
||||
bindEvents()
|
||||
|
||||
function getHighlight() {
|
||||
var pageNumber = PDFViewerApplication.pdfViewer.currentPageNumber;
|
||||
var pageIndex = pageNumber - 1;
|
||||
|
@ -102,6 +193,7 @@ function getHighlight() {
|
|||
var text = selection.toString();
|
||||
var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]);
|
||||
return {
|
||||
type: SELECTION,
|
||||
page: pageNumber,
|
||||
pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel,
|
||||
position: position,
|
||||
|
@ -134,6 +226,10 @@ function renderAnnotation(annotation) {
|
|||
}
|
||||
var pageElement = page.canvas.parentElement.parentElement;
|
||||
var viewport = page.viewport;
|
||||
if (annotation.type == HIGHLIGHT) {
|
||||
renderHighlights(annotation.page)
|
||||
} else if (annotation.coords) {
|
||||
pageElement.querySelectorAll('.oml-annotation').forEach(el => el.remove())
|
||||
annotation.coords.forEach(function (rect) {
|
||||
var bounds = viewport.convertToViewportRectangle(rect);
|
||||
var el = document.createElement('div');
|
||||
|
@ -153,6 +249,9 @@ function renderAnnotation(annotation) {
|
|||
});
|
||||
pageElement.appendChild(el);
|
||||
});
|
||||
} else {
|
||||
// console.log("annotation without position", annotation)
|
||||
}
|
||||
}
|
||||
|
||||
function addAnnotation(annotation) {
|
||||
|
@ -169,6 +268,11 @@ function selectAnnotation(id) {
|
|||
g.classList.add('selected')
|
||||
g.style.backgroundColor = 'blue'
|
||||
})
|
||||
annotations.forEach(a => {
|
||||
if (a.id == id && a.type == HIGHLIGHT) {
|
||||
renderHighlights(a.page)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function deselectAnnotation(id) {
|
||||
|
@ -207,7 +311,7 @@ function loadAnnotations(page) {
|
|||
e.remove()
|
||||
})
|
||||
annotations.filter(function(a) {
|
||||
return a.page == page
|
||||
return a.page == page && !a.type == HIGHLIGHT
|
||||
}).forEach(function(annot) {
|
||||
renderAnnotation(annot)
|
||||
})
|
||||
|
@ -220,3 +324,145 @@ function isInView(element) {
|
|||
var elementBottom = elementTop + $(element).height();
|
||||
return elementTop < docViewBottom && elementBottom > docViewTop;
|
||||
}
|
||||
|
||||
function renderHighlightSelectionOverlay(root, documentId, page, source) {
|
||||
var canvas = document.createElement('canvas')
|
||||
root.appendChild(canvas)
|
||||
canvas.width = canvas.clientWidth
|
||||
canvas.height = canvas.clientHeight
|
||||
var ctx = canvas.getContext('2d');
|
||||
var viewerContainer = document.querySelector('#viewerContainer')
|
||||
var bounds = root.getBoundingClientRect();
|
||||
var base = 2048
|
||||
var scale = Math.max(bounds.height, bounds.width) / base
|
||||
var last_mousex = last_mousey = 0;
|
||||
var mousex = mousey = 0;
|
||||
var mousedown = false;
|
||||
var p = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0
|
||||
}
|
||||
var inside = false
|
||||
|
||||
canvas.addEventListener('mousedown', function(e) {
|
||||
if (inside) {
|
||||
const coords = [
|
||||
[p.left, p.top, p.right, p.bottom]
|
||||
]
|
||||
addAnnotation({
|
||||
type: HIGHLIGHT,
|
||||
id: Ox.SHA1(pageNumber.toString() + JSON.stringify(p)),
|
||||
text: "",
|
||||
page: parseInt(page),
|
||||
pageLabel: source.pageLabel,
|
||||
coords: coords,
|
||||
})
|
||||
return
|
||||
}
|
||||
let bounds = root.getBoundingClientRect();
|
||||
last_mousex = e.clientX - bounds.left;
|
||||
last_mousey = e.clientY - bounds.top;
|
||||
p.top = parseInt(last_mousey / scale)
|
||||
p.left = parseInt(last_mousex / scale)
|
||||
mousedown = true;
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function(e) {
|
||||
if (mousedown) {
|
||||
mousedown = false;
|
||||
p.bottom = parseInt(mousey / scale)
|
||||
p.right = parseInt(mousex / scale)
|
||||
|
||||
if (p.top > p.bottom) {
|
||||
var t = p.top
|
||||
p.top = p.bottom
|
||||
p.bottom = t
|
||||
}
|
||||
if (p.left > p.right) {
|
||||
var t = p.left
|
||||
p.left = p.right
|
||||
p.right = t
|
||||
}
|
||||
/*
|
||||
var url = `${baseUrl}/documents/${documentId}/2048p${page},${p.left},${p.top},${p.right},${p.bottom}.jpg`
|
||||
info.url = `${baseUrl}/document/${documentId}/${page}`
|
||||
info.page = page
|
||||
if (p.left != p.right && p.top != p.bottom) {
|
||||
var context = formatOutput(info, url)
|
||||
copyToClipboard(context)
|
||||
addToRecent({
|
||||
document: documentId,
|
||||
page: parseInt(page),
|
||||
title: info.title,
|
||||
type: 'fragment',
|
||||
link: `${baseUrl}/documents/${documentId}/${page}`,
|
||||
src: url
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', function(e) {
|
||||
let bounds = root.getBoundingClientRect();
|
||||
mousex = e.clientX - bounds.left;
|
||||
mousey = e.clientY - bounds.top;
|
||||
|
||||
if(mousedown) {
|
||||
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)
|
||||
ctx.beginPath()
|
||||
var width = mousex - last_mousex
|
||||
var height = mousey - last_mousey
|
||||
ctx.rect(last_mousex, last_mousey, width, height)
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
let py = parseInt(mousey / scale)
|
||||
let px = parseInt(mousex / scale)
|
||||
if (py > p.top && py < p.bottom && px > p.left && px < p.right) {
|
||||
inside = true
|
||||
canvas.style.cursor = 'pointer'
|
||||
canvas.title = 'Click to add highlight'
|
||||
} else {
|
||||
inside = false
|
||||
canvas.style.cursor = ''
|
||||
canvas.title = ''
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function renderHighlights(page) {
|
||||
var pageAnnotations = annotations.filter(annotation => {
|
||||
return annotation.type == HIGHLIGHT && (!page || (annotation.page == page))
|
||||
})
|
||||
pageAnnotations.forEach(annotation => {
|
||||
let page = annotation.page
|
||||
var canvas = document.querySelector(`#highlights${page} canvas`)
|
||||
if (canvas) {
|
||||
canvas.width = canvas.clientWidth
|
||||
canvas.height = canvas.clientHeight
|
||||
var ctx = canvas.getContext('2d');
|
||||
var viewerContainer = document.querySelector('#viewerContainer')
|
||||
var bounds = canvas.parentElement.getBoundingClientRect();
|
||||
var base = 2048
|
||||
var scale = Math.max(bounds.height, bounds.width) / base
|
||||
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)
|
||||
pageAnnotations.forEach(annotation => {
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = annotation.id == selectedAnnotation ? 'blue' : 'yellow';
|
||||
ctx.lineWidth = 2;
|
||||
annotation.coords.forEach(coord => {
|
||||
const width = coord[2] - coord[0],
|
||||
height = coord[3] - coord[1];
|
||||
ctx.rect(coord[0] * scale, coord[1] * scale, width * scale, height * scale)
|
||||
})
|
||||
ctx.stroke();
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
BIN
static/reader/pdf/toolbarButton-crop.png
Normal file
BIN
static/reader/pdf/toolbarButton-crop.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
4
static/reader/pdf/toolbarButton-crop.svg
Normal file
4
static/reader/pdf/toolbarButton-crop.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="54" width="54">
|
||||
<path stroke-linejoin="round" stroke="#333" stroke-linecap="round" stroke-width="5" fill="none" d="m13.2 39 35-34.6m-45 8.1h36.6v38m-27-47v36.6h38"/>
|
||||
</svg>
|
After Width: | Height: | Size: 243 B |
BIN
static/reader/pdf/toolbarButton-crop@10.png
Normal file
BIN
static/reader/pdf/toolbarButton-crop@10.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
static/reader/pdf/toolbarButton-crop@2x.png
Normal file
BIN
static/reader/pdf/toolbarButton-crop@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
Loading…
Reference in a new issue