View
@@ -0,0 +1,264 @@
Annotator = require('annotator')
$ = Annotator.$
xpathRange = Annotator.Range
DiffMatchPatch = require('diff-match-patch')
seek = require('dom-seek')
# Helper functions for throwing common errors
missingParameter = (name) ->
throw new Error('missing required parameter "' + name + '"')
notImplemented = ->
throw new Error('method not implemented')
###*
# class:: Abstract base class for anchors.
###
class Anchor
# Create an instance of the anchor from a Range.
@fromRange: notImplemented
# Create an instance of the anchor from a selector.
@fromSelector: notImplemented
# Create a Range from the anchor.
toRange: notImplemented
# Create a selector from the anchor.
toSelector: notImplemented
###*
# class:: FragmentAnchor(id)
#
# This anchor type represents a fragment identifier.
#
# :param String id: The id of the fragment for the anchor.
###
class FragmentAnchor extends Anchor
constructor: (@id) ->
unless @id? then missingParameter('id')
@fromRange: (range) ->
id = $(range.commonAncestorContainer).closest('[id]').attr('id')
return new FragmentAnchor(id)
@fromSelector: (selector) ->
return new FragmentAnchor(selector.value)
toSelector: ->
return {
type: 'FragmentSelector'
value: @id
}
toRange: ->
el = document.getElementById(@id)
range = document.createRange()
range.selectNode(el)
return range
###*
# class:: RangeAnchor(range)
#
# This anchor type represents a DOM Range.
#
# :param Range range: A range describing the anchor.
###
class RangeAnchor extends Anchor
constructor: (@range) ->
unless @range? then missingParameter('range')
@fromRange: (range) ->
return new RangeAnchor(range)
# Create and anchor using the saved Range selector.
@fromSelector: (selector, options = {}) ->
root = options.root or document.body
data = {
start: selector.startContainer
startOffset: selector.startOffset
end: selector.endContainer
endOffset: selector.endOffset
}
range = new xpathRange.SerializedRange(data).normalize(root).toRange()
return new RangeAnchor(range)
toRange: ->
return @range
toSelector: (options = {}) ->
root = options.root or document.body
ignoreSelector = options.ignoreSelector
range = new xpathRange.BrowserRange(@range).serialize(root, ignoreSelector)
return {
type: 'RangeSelector'
startContainer: range.start
startOffset: range.startOffset
endContainer: range.end
endOffset: range.endOffset
}
###*
# class:: TextPositionAnchor(start, end)
#
# This anchor type represents a piece of text described by start and end
# character offsets.
#
# :param Number start: The start offset for the anchor text.
# :param Number end: The end offset for the anchor text.
###
class TextPositionAnchor extends Anchor
constructor: (@start, @end) ->
unless @start? then missingParameter('start')
unless @end? then missingParameter('end')
@fromRange: (range, options = {}) ->
root = options.root or document.body
filter = options.filter or null
range = new xpathRange.BrowserRange(range).normalize(root)
iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, filter)
start = seek(iter, range.start)
end = seek(iter, range.end) + start + range.end.textContent.length
new TextPositionAnchor(start, end)
@fromSelector: (selector) ->
return new TextPositionAnchor(selector.start, selector.end)
toRange: (options = {}) ->
root = options.root or document.body
filter = options.filter or null
range = document.createRange()
iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, filter)
{start, end} = this
count = seek(iter, start)
remainder = start - count
if iter.pointerBeforeReferenceNode
range.setStart(iter.referenceNode, remainder)
else
range.setStart(iter.nextNode(), remainder)
iter.previousNode()
length = (end - start) + remainder
count = seek(iter, length)
remainder = length - count
if iter.pointerBeforeReferenceNode
range.setEnd(iter.referenceNode, remainder)
else
range.setEnd(iter.nextNode(), remainder)
return range
toSelector: ->
return {
type: 'TextPositionSelector'
start: @start
end: @end
}
###*
# class:: TextQuoteAnchor(quote, [prefix, [suffix, [start, [end]]]])
#
# This anchor type represents a piece of text described by a quote. The quote
# may optionally include textual context and/or a position within the text.
#
# :param String quote: The anchor text to match.
# :param String prefix: A prefix that preceeds the anchor text.
# :param String suffix: A suffix that follows the anchor text.
###
class TextQuoteAnchor extends Anchor
constructor: (@quote, @prefix='', @suffix='') ->
unless @quote? then missingParameter('quote')
@fromRange: (range, options = {}) ->
root = options.root or document.body
filter = options.filter or null
range = new xpathRange.BrowserRange(range).normalize(root)
iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, filter)
start = seek(iter, range.start)
count = seek(iter, range.end)
end = start + count + range.end.textContent.length
corpus = root.textContent
prefixStart = Math.max(start - 32, 0)
exact = corpus.substr(start, end - start)
prefix = corpus.substr(prefixStart, start - prefixStart)
suffix = corpus.substr(end, 32)
return new TextQuoteAnchor(exact, prefix, suffix)
@fromSelector: (selector) ->
{exact, prefix, suffix} = selector
return new TextQuoteAnchor(exact, prefix, suffix)
toRange: (options = {}) ->
return this.toPositionAnchor(options).toRange()
toSelector: ->
selector = {
type: 'TextQuoteSelector'
exact: @quote
}
if @prefix? then selector.prefix = @prefix
if @suffix? then selector.suffix = @suffix
return selector
toPositionAnchor: (options = {}) ->
root = options.root or document.body
dmp = new DiffMatchPatch()
foldSlices = (acc, slice) ->
result = dmp.match_main(root.textContent, slice, acc.loc)
if result is -1
throw new Error('no match found')
acc.loc = result + slice.length
acc.start = Math.min(acc.start, result)
acc.end = Math.max(acc.end, result + slice.length)
return acc
slices = @quote.match(/(.|[\r\n]){1,32}/g)
loc = options.position?.start ? root.textContent.length / 2
# TODO: use the suffix
dmp.Match_Distance = root.textContent.length * 2
if @prefix? and @quote.length < 32
loc = Math.max(0, loc - @prefix.length)
result = dmp.match_main(root.textContent, @prefix, loc)
start = result + @prefix.length
end = start
else
firstSlice = slices.shift()
result = dmp.match_main(root.textContent, firstSlice, loc)
start = result
end = start + firstSlice.length
if result is -1
throw new Error('no match found')
loc = end
dmp.Match_Distance = 64
{start, end} = slices.reduce(foldSlices, {start, end, loc})
return new TextPositionAnchor(start, end)
exports.Anchor = Anchor
exports.FragmentAnchor = FragmentAnchor
exports.RangeAnchor = RangeAnchor
exports.TextPositionAnchor = TextPositionAnchor
exports.TextQuoteAnchor = TextQuoteAnchor
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -0,0 +1,43 @@
Annotator = require('annotator')
$ = Annotator.$
# Public: Wraps the DOM Nodes within the provided range with a highlight
# element of the specified class and returns the highlight Elements.
#
# normedRange - A NormalizedRange to be highlighted.
# cssClass - A CSS class to use for the highlight (default: 'annotator-hl')
#
# Returns an array of highlight Elements.
exports.highlightRange = (normedRange, cssClass='annotator-hl') ->
white = /^\s*$/
hl = $("<span class='#{cssClass}'></span>")
# Ignore text nodes that contain only whitespace characters. This prevents
# spans being injected between elements that can only contain a restricted
# subset of nodes such as table rows and lists. This does mean that there
# may be the odd abandoned whitespace node in a paragraph that is skipped
# but better than breaking table layouts.
nodes = $(normedRange.textNodes()).filter((i) -> not white.test @nodeValue)
r = nodes.wrap(hl).parent().show().toArray()
exports.removeHighlights = (highlights) ->
for h in highlights when h.parentNode?
$(h).replaceWith(h.childNodes)
# Get the bounding client rectangle of a collection in viewport coordinates.
# Unfortunately, Chrome has issues[1] with Range.getBoundingClient rect or we
# could just use that.
# [1] https://code.google.com/p/chromium/issues/detail?id=324437
exports.getBoundingClientRect = (collection) ->
# Reduce the client rectangles of the highlights to a bounding box
rects = collection.map((n) -> n.getBoundingClientRect())
return rects.reduce (acc, r) ->
top: Math.min(acc.top, r.top)
left: Math.min(acc.left, r.left)
bottom: Math.max(acc.bottom, r.bottom)
right: Math.max(acc.right, r.right)
View
@@ -29,7 +29,7 @@ module.exports = class Host extends Guest
.addClass('annotator-frame annotator-outer annotator-collapsed')
.appendTo(element)
super element, options, dontScan: true
super
this._addCrossFrameListeners()
app.appendTo(@frame)
@@ -39,8 +39,6 @@ module.exports = class Host extends Guest
# Host frame dictates the toolbar options.
this.on 'panelReady', =>
this.anchoring._scan() # Scan the document
# Guest is designed to respond to events rather than direct method
# calls. If we call set directly the other plugins will never recieve
# these events and the UI will be out of sync.
View
@@ -1,7 +1,15 @@
var Annotator = require('annotator');
// Monkeypatch annotator!
require('./monkey');
// Scroll plugin for jQuery
// TODO: replace me
require('jquery-scrollintoview')
// Polyfills
var g = Annotator.Util.getGlobal();
if (g.wgxpath) g.wgxpath.install();
var nodeIteratorShim = require('node-iterator-shim')
nodeIteratorShim();
// Applications
Annotator.Guest = require('./guest')
@@ -13,48 +21,34 @@ Annotator.Plugin.CrossFrame.Bridge = require('../bridge')
Annotator.Plugin.CrossFrame.AnnotationSync = require('../annotation-sync')
Annotator.Plugin.CrossFrame.Discovery = require('../discovery')
// Document plugin
require('../vendor/annotator.document');
// Bucket bar
require('./plugin/bucket-bar');
// Toolbar
require('./plugin/toolbar');
// Drawing highlights
require('./plugin/texthighlights');
// Creating selections
require('./plugin/textselection');
// URL fragments
require('./plugin/fragmentselector');
// Anchoring dependencies
require('diff-match-patch')
require('dom-text-mapper')
require('dom-text-matcher')
require('page-text-mapper-core')
require('text-match-engines')
// Anchoring plugins
require('./plugin/enhancedanchoring');
require('./plugin/domtextmapper');
require('./plugin/fuzzytextanchors');
require('./plugin/pdf');
require('./plugin/textquote');
require('./plugin/textposition');
require('./plugin/textrange');
var Klass = Annotator.Host;
var docs = 'https://github.com/hypothesis/h/blob/master/README.rst#customized-embedding';
var options = {
app: jQuery('link[type="application/annotator+html"]').attr('href'),
BucketBar: {container: '.annotator-frame'},
BucketBar: {container: '.annotator-frame', scrollables: ['body']},
Toolbar: {container: '.annotator-frame'}
};
// Document metadata plugins
if (window.PDFViewerApplication) {
require('./plugin/pdf')
options['BucketBar']['scrollables'] = ['#viewerContainer']
options['PDF'] = {};
} else {
require('../vendor/annotator.document');
options['Document'] = {};
}
if (window.hasOwnProperty('hypothesisRole')) {
if (typeof window.hypothesisRole === 'function') {
Klass = window.hypothesisRole;
@@ -63,15 +57,6 @@ if (window.hasOwnProperty('hypothesisRole')) {
}
}
// Simple IE autodetect function
// See for example https://stackoverflow.com/questions/19999388/jquery-check-if-user-is-using-ie/21712356#21712356
var ua = window.navigator.userAgent;
if ((ua.indexOf("MSIE ") > 0) || // for IE <=10
(ua.indexOf('Trident/') > 0) || // for IE 11
(ua.indexOf('Edge/') > 0)) { // for IE 12
options["DomTextMapper"] = {"skip": true}
}
if (window.hasOwnProperty('hypothesisConfig')) {
if (typeof window.hypothesisConfig === 'function') {
options = jQuery.extend(options, window.hypothesisConfig());
View

This file was deleted.

Oops, something went wrong.
View
@@ -1,9 +1,41 @@
raf = require('raf')
Annotator = require('annotator')
$ = Annotator.$
highlighter = require('../highlighter')
# Scroll to the next closest anchor off screen in the given direction.
scrollToClosest = (anchors, direction) ->
dir = if direction is "up" then +1 else -1
{next} = anchors.reduce (acc, anchor) ->
unless anchor.highlights?.length
return acc
{start, next} = acc
rect = highlighter.getBoundingClientRect(anchor.highlights)
# Ignore if it's not in the right direction.
if (dir is 1 and rect.top >= 0)
return acc
else if (dir is -1 and rect.top <= window.innerHeight)
return acc
# Select the closest to carry forward
if not next?
start: rect.top
next: anchor
else if start * dir < rect.top * dir
start: rect.top
next: anchor
else
acc
, {}
$(next.highlights).scrollintoview()
class Annotator.Plugin.BucketBar extends Annotator.Plugin
# prototype constants
BUCKET_THRESHOLD_PAD: 106
@@ -22,6 +54,9 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
# then that annotation will not be merged into the bucket
gapSize: 60
# Selectors for the scrollable elements on the page
scrollables: null
# buckets of annotations that overlap
buckets: []
@@ -40,43 +75,16 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
$(element).append @element
pluginInit: ->
events = [
'annotationCreated', 'annotationUpdated', 'annotationDeleted',
'annotationsLoaded'
]
for event in events
@annotator.subscribe event, this._scheduleUpdate
$(window).on 'resize scroll', this._scheduleUpdate
$(document.body).on 'resize scroll', '*', this._scheduleUpdate
# Event handler to to update when new highlights have been created
@annotator.subscribe "highlightsCreated", (highlights) =>
# All the highlights are guaranteed to belong to one anchor,
# so we can do this:
anchor = if Array.isArray highlights # Did we got a list ?
highlights[0].anchor
else
# I see that somehow if I publish an array with a signel element,
# by the time it arrives, it's not an array any more.
# Weird, but for now, let's work around it.
highlights.anchor
if anchor.annotation.id? # Is this a finished annotation ?
this._scheduleUpdate()
$(window).on 'resize scroll', @update
# Event handler to to update when highlights have been removed
@annotator.subscribe "highlightRemoved", (highlight) =>
if highlight.annotation.id? # Is this a finished annotation ?
this._scheduleUpdate()
for scrollable in @options.scrollables ? []
$(scrollable).on 'resize scroll', @update
addEventListener "docPageScrolling", this._scheduleUpdate
destroy: ->
$(window).off 'resize scroll', @update
# Update sometime soon
_scheduleUpdate: =>
return if @_updatePending?
@_updatePending = raf =>
delete @_updatePending
@_update()
for scrollable in @options.scrollables ? []
$(scrollable).off 'resize scroll', @update
_collate: (a, b) ->
for i in [0..a.length-1]
@@ -86,104 +94,34 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
return 1
return 0
_collectVirtualAnnotations: (startPage, endPage) ->
results = []
for page in [startPage .. endPage]
anchors = @annotator.anchoring.anchors[page]
if anchors?
$.merge results, (anchor.annotation for anchor in anchors when not anchor.fullyRealized)
results
# Find the first/last annotation from the list, based on page number,
# and Y offset, if already known, and jump to it.
# If the Y offsets are not yet known, just jump the page,
# wait for the highlights to be realized, and finish the selection then.
_jumpMinMax: (annotations, direction) ->
unless direction in ["up", "down"]
throw "Direction is mandatory!"
dir = if direction is "up" then +1 else -1
{next} = annotations.reduce (acc, ann) ->
{start, next} = acc
anchor = ann.anchors[0]
hl = anchor.highlight[anchor.startPage]
# Ignore this anchor if its highlight is currently on screen.
if hl?
rect = hl.getBoundingClientRect()
switch dir
when 1
if rect.bottom >= 0
return acc
when -1
if rect.top <= window.innerHeight
return acc
if not next? or start.page*dir < anchor.startPage*dir
# This one is obviously better
start:
page: anchor.startPage
top: anchor.highlight[anchor.startPage]?.getTop()
next: [anchor]
else if start.page is anchor.startPage
# This is on the same page, might be better
if hl?
# We have a real highlight, let's compare coordinates
if start.top*dir < hl.getTop()*dir
# OK, this one is better
start:
page: start.page
top: hl.getTop()
next: [anchor]
else
# No, let's keep the old one instead
acc
else
# The page is not yet rendered, can't decide yet.
# Let's just store this one, too
start: page: start.page
next: $.merge next, [anchor]
else
# No, we have clearly seen better alternatives
acc
, {}
# Get an anchor from the page we want to go to
anchor = next[0]
anchor.scrollToView()
_update: =>
wrapper = @annotator.wrapper
highlights = @annotator.anchoring.getHighlights()
defaultView = wrapper[0].ownerDocument.defaultView
# Update sometime soon
update: =>
return if @_updatePending?
@_updatePending = raf =>
delete @_updatePending
@_update()
_update: ->
# Keep track of buckets of annotations above and below the viewport
above = []
below = []
# Get the page numbers
mapper = @annotator.anchoring.document
return unless mapper? # Maybe it's too soon to do this
firstPage = 0
currentPage = mapper.getPageIndex()
lastPage = mapper.getPageCount() - 1
# Collect the virtual anchors from above and below
$.merge above, this._collectVirtualAnnotations 0, currentPage-1
$.merge below, this._collectVirtualAnnotations currentPage+1, lastPage
# Construct indicator points
points = highlights.reduce (points, hl, i) =>
d = hl.annotation
x = hl.getTop() - defaultView.pageYOffset
h = hl.getHeight()
points = @annotator.anchors.reduce (points, anchor, i) =>
unless anchor.highlights?.length
return points
rect = highlighter.getBoundingClientRect(anchor.highlights)
x = rect.top
h = rect.bottom - rect.top
if x < 0
if d not in above then above.push d
if anchor not in above then above.push anchor
else if x + h > window.innerHeight
if d not in below then below.push d
if anchor not in below then below.push anchor
else
points.push [x, 1, d]
points.push [x + h, -1, d]
points.push [x, 1, anchor]
points.push [x + h, -1, anchor]
points
, []
@@ -209,23 +147,23 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
.sort(this._collate)
.reduce ({buckets, index, carry}, [x, d, a], i, points) =>
if d > 0 # Add annotation
if (j = carry.annotations.indexOf a) < 0
carry.annotations.unshift a
if (j = carry.anchors.indexOf a) < 0
carry.anchors.unshift a
carry.counts.unshift 1
else
carry.counts[j]++
else # Remove annotation
j = carry.annotations.indexOf a # XXX: assert(i >= 0)
j = carry.anchors.indexOf a # XXX: assert(i >= 0)
if --carry.counts[j] is 0
carry.annotations.splice j, 1
carry.anchors.splice j, 1
carry.counts.splice j, 1
if (
(index.length is 0 or i is points.length - 1) or # First or last?
carry.annotations.length is 0 or # A zero marker?
carry.anchors.length is 0 or # A zero marker?
x - index[index.length-1] > @options.gapSize # A large gap?
) # Mark a new bucket.
buckets.push carry.annotations.slice()
buckets.push carry.anchors.slice()
index.push x
else
# Merge the previous bucket, making sure its predecessor contains
@@ -238,15 +176,15 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
else
last = buckets[buckets.length-1]
toMerge = []
last.push a0 for a0 in carry.annotations when a0 not in last
last.push a0 for a0 in carry.anchors when a0 not in last
last.push a0 for a0 in toMerge when a0 not in last
{buckets, index, carry}
,
buckets: []
index: []
carry:
annotations: []
anchors: []
counts: []
latest: 0
@@ -257,9 +195,9 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
# Scroll down
@buckets.push [], below, []
@index.push $(window).height() - @BUCKET_SIZE - 12,
$(window).height() - @BUCKET_SIZE - 11,
$(window).height()
@index.push window.innerHeight - @BUCKET_SIZE - 12,
window.innerHeight - @BUCKET_SIZE - 11,
window.innerHeight
# Calculate the total count for each bucket (without replies) and the
# maximum count.
@@ -285,35 +223,33 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
div.addClass('annotator-bucket-indicator')
# Creates highlights corresponding bucket when mouse is hovered
# Focus corresponding highlights bucket when mouse is hovered
# TODO: This should use event delegation on the container.
.on 'mousemove', (event) =>
bucket = @tabs.index(event.currentTarget)
for hl in @annotator.anchoring.getHighlights()
if hl.annotation in @buckets[bucket]
hl.setFocused true
else
hl.setFocused false
for anchor in @annotator.anchors
toggle = anchor in @buckets[bucket]
$(anchor.highlights).toggleClass('annotator-hl-focused', toggle)
# Gets rid of them after
.on 'mouseout', =>
for hl in @annotator.anchoring.getHighlights()
hl.setFocused false
.on 'mouseout', (event) =>
bucket = @tabs.index(event.currentTarget)
for anchor in @buckets[bucket]
$(anchor.highlights).removeClass('annotator-hl-focused')
# Does one of a few things when a tab is clicked depending on type
.on 'click', (event) =>
bucket = @tabs.index(event.currentTarget)
event.stopPropagation()
pad = defaultView.innerHeight * .2
# If it's the upper tab, scroll to next anchor above
if (@isUpper bucket)
@_jumpMinMax @buckets[bucket], "up"
scrollToClosest(@buckets[bucket], 'up')
# If it's the lower tab, scroll to next anchor below
else if (@isLower bucket)
@_jumpMinMax @buckets[bucket], "down"
scrollToClosest(@buckets[bucket], 'down')
else
annotations = @buckets[bucket].slice()
annotations = (anchor.annotation for anchor in @buckets[bucket])
annotator.selectAnnotations annotations,
(event.ctrlKey or event.metaKey),
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View
@@ -1,302 +1,96 @@
Promise = require('es6-promise').Promise
Promise = global.Promise ? require('es6-promise').Promise
Annotator = require('annotator')
$ = Annotator.$
detectedPDFjsVersion = PDFJS?.version.split(".").map parseFloat
# Compare two versions, given as arrays of numbers
compareVersions = (v1, v2) ->
unless Array.isArray(v1) and Array.isArray(v2)
throw new Error "Expecting arrays, in the form of [1, 0, 123]"
unless v1.length is v2.length
throw new Error "Can't compare versions in different formats."
for i in [0 ... v1.length]
if v1[i] < v2[i]
return -1
else if v1[i] > v2[i]
return 1
# Finished comparing, it's the same all along
return 0
class PDF extends Annotator.Plugin
documentLoaded: null
observer: null
pdfViewer: null
# Document mapper module for PDF.js documents
class window.PDFTextMapper extends PageTextMapperCore
# Are we working with a PDF document?
@isPDFDocument: ->
PDFView? or # for PDF.js up to v1.0.712
PDFViewerApplication? # for PDF.js v1.0.907 and up
# Can we use this document access strategy?
@applicable: -> @isPDFDocument()
requiresSmartStringPadding: true
# Get the number of pages
getPageCount: -> @_viewer.pages.length
# Where are we in the document?
getPageIndex: -> @_app.page - 1
# Jump to a given page
setPageIndex: (index) -> @_app.page = index + 1
# Determine whether a given page has been rendered
_isPageRendered: (index) ->
@_viewer.pages[index]?.textLayer?.renderingDone
# Get the root DOM node of a given page
getRootNodeForPage: (index) ->
@_viewer.pages[index].textLayer.textLayerDiv
constructor: ->
# Set references to objects that moved around in different versions
# of PDF.js, and define a few methods accordingly
if PDFViewerApplication?
@_app = PDFViewerApplication
@_viewer = @_app.pdfViewer
pluginInit: ->
@pdfViewer = PDFViewerApplication.pdfViewer
@pdfViewer.viewer.classList.add('has-transparent-text-layer')
if PDFViewerApplication.loading
@documentLoaded = new Promise (resolve) ->
finish = (evt) ->
window.removeEventListener('documentload', finish)
resolve()
window.addEventListener('documentload', finish)
else
@_app = @_viewer = PDFView
@setEvents()
# Starting with PDF.js v1.0.822, the CSS rules changed.
#
# See this commit:
# https://github.com/mozilla/pdf.js/commit/a2e8a5ee7fecdbb2f42eeeb2343faa38cd553a15
# We need to know about that, and set our own CSS rules accordingly,
# so that our highlights are still visible. So we add a marker class,
# if this is the case.
if compareVersions(detectedPDFjsVersion, [1, 0, 822]) >= 0
@_viewer.container.className += " has-transparent-text-layer"
# Install watchers for various events to detect page rendering/unrendering
setEvents: ->
# Detect page rendering
addEventListener "pagerender", (evt) =>
@documentLoaded = Promise.resolve()
# If we have not yet finished the initial scanning, then we are
# not interested.
return unless @pageInfo?
@observer = new MutationObserver((mutations) => this.update())
@observer.observe(@pdfViewer.viewer, {
attributes: true
attributeFilter: ['data-loaded']
childList: true
subtree: true
})
index = evt.detail.pageNumber - 1
@_onPageRendered index
destroy: ->
@pdfViewer.viewer.classList.remove('has-transparent-text-layer')
@observer.disconnect()
# Detect page un-rendering
addEventListener "DOMNodeRemoved", (evt) =>
node = evt.target
if node.nodeType is Node.ELEMENT_NODE and node.nodeName.toLowerCase() is "div" and node.className is "textLayer"
index = parseInt node.parentNode.id.substr(13) - 1
# Forget info about the new DOM subtree
@_unmapPage @pageInfo[index]
# Do something about cross-page selections
viewer = document.getElementById "viewer"
viewer.addEventListener "domChange", (event) =>
node = event.srcElement ? event.target
data = event.data
if "viewer" is node.getAttribute? "id"
console.log "Detected cross-page change event."
# This event escaped the pages.
# Must be a cross-page selection.
if data.start? and data.end?
startPage = @getPageForNode data.start
@_updateMap @pageInfo[startPage.index]
endPage = @getPageForNode data.end
@_updateMap @pageInfo[endPage.index]
@_viewer.container.addEventListener "scroll", @_onScroll
_extractionPattern: /[ ]+/g
_parseExtractedText: (text) => text.replace @_extractionPattern, " "
# Wait for PDF.js to initialize
waitForInit: ->
# Create a utility function to poll status
tryIt = (resolve) =>
# Are we ready yet?
if @_app.documentFingerprint and @_app.documentInfo
# Now we have PDF metadata."
resolve()
else
# PDF metadata is not yet available; postponing extraction.
setTimeout ( =>
# let's try again if we have PDF metadata.
tryIt resolve
), 100
# Return a promise
new Promise (resolve, reject) =>
if PDFTextMapper.applicable()
tryIt resolve
uri: ->
@documentLoaded.then ->
PDFViewerApplication.url
getMetadata: ->
@documentLoaded.then ->
info = PDFViewerApplication.documentInfo
metadata = PDFViewerApplication.metadata
# Taken from PDFViewerApplication#load
if metadata?.has('dc:title') and metadata.get('dc:title') isnt 'Untitled'
title = metadata.get('dc:title')
else if info?['Title']
title = info['Title']
else
reject "Not a PDF.js document"
# Extract the text from the PDF
scan: ->
# Return a promise
new Promise (resolve, reject) =>
@_pendingScanResolve = resolve
@waitForInit().then =>
# Initialize our main page data array
@pageInfo = []
# Start the text extraction
@_extractPageText 0
# Manually extract the text from the PDF document.
# This workaround is here to avoid depending PDFFindController's
# own text extraction routines, which sometimes fail to add
# adequate spacing.
_extractPageText: (pageIndex) ->
# Wait for the page to load
@_app.pdfDocument.getPage(pageIndex + 1).then (page) =>
# Wait for the data to be extracted
page.getTextContent().then (data) =>
title = document.title
# First, join all the pieces from the bidiTexts
rawContent = (text.str for text in data.items).join " "
# This is an experimental URN,
# as per http://tools.ietf.org/html/rfc3406#section-3.0
urn = "urn:x-pdf:" + PDFViewerApplication.documentFingerprint
link = [{href: urn}, {href: PDFViewerApplication.url}]
# Do some post-processing
content = @_parseExtractedText rawContent
return {title, link}
# Save the extracted content to our page information registery
@pageInfo[pageIndex] =
index: pageIndex
content: content
update: ->
{annotator, pdfViewer} = this
if pageIndex is @getPageCount() - 1
@_finishScan()
else
@_extractPageText pageIndex + 1
stableAnchors = []
pendingAnchors = []
refreshAnnotations = []
# This is called when scanning is finished
_finishScan: =>
# Do some besic calculations with the content
@_onHavePageContents()
for page in pdfViewer.pages when page.textLayer?.renderingDone
div = page.div ? page.el
placeholder = div.getElementsByClassName('annotator-placeholder')[0]
# OK, we are ready to rock.
@_pendingScanResolve()
switch page.renderingState
when RenderingStates.INITIAL
page.textLayer = null
when RenderingStates.FINISHED
if placeholder?
placeholder.parentNode.removeChild(placeholder)
# Do whatever we need to do after scanning
@_onAfterScan()
for anchor in annotator.anchors when anchor.highlights?
if anchor.annotation in refreshAnnotations
continue
for hl in anchor.highlights
if not document.body.contains(hl)
delete anchor.highlights
delete anchor.range
refreshAnnotations.push(anchor.annotation)
break
# Look up the page for a given DOM node
getPageForNode: (node) ->
# Search for the root of this page
div = node
while (
(div.nodeType isnt Node.ELEMENT_NODE) or
not div.getAttribute("class")? or
(div.getAttribute("class") isnt "textLayer")
)
div = div.parentNode
for annotation in refreshAnnotations
annotator.setupAnnotation(annotation)
pendingAnchors.push(annotation.anchors)
# Fetch the page number from the id. ("pageContainerN")
index = parseInt div.parentNode.id.substr(13) - 1
# Look up the page
@pageInfo[index]
getDocumentFingerprint: -> @_app.documentFingerprint
getDocumentInfo: -> @_app.documentInfo
# Annotator plugin for annotating documents handled by PDF.js
class Annotator.Plugin.PDF extends Annotator.Plugin
pluginInit: ->
# We need dom-text-mapper
unless @annotator.plugins.DomTextMapper
console.warn "The PDF Annotator plugin requires the DomTextMapper plugin. Skipping."
return
@anchoring = @annotator.anchoring
@anchoring.documentAccessStrategies.unshift
# Strategy to handle PDF documents rendered by PDF.js
name: "PDF.js"
mapper: PDFTextMapper
# Are we looking at a PDF.js-rendered document?
_isPDF: -> PDFTextMapper.applicable()
# Extract the URL of the PDF file, maybe from the chrome-extension URL
_getDocumentURI: ->
uri = window.location.href
# We might have the URI embedded in a chrome-extension URI
matches = uri.match('chrome-extension://[a-z]{32}/(content/web/viewer.html\\?file=)?(.*)')
# Get the last match
match = matches?[matches.length - 1]
if match
decodeURIComponent match
else
uri
# Get a PDF fingerPrint-based URI
_getFingerPrintURI: ->
fingerprint = @anchoring.document.getDocumentFingerprint()
# This is an experimental URN,
# as per http://tools.ietf.org/html/rfc3406#section-3.0
"urn:x-pdf:" + fingerprint
# Public: get a canonical URI, if this is a PDF. (Null otherwise)
uri: ->
return null unless @_isPDF()
# For now, we return the fingerprint-based URI first,
# because it's probably more relevant.
# OTOH, we can't use it for clickable source links ...
# but the path is also included in the matadata,
# so anybody who _needs_ that can access it from there.
@_getFingerPrintURI()
# Try to extract the title; first from metadata, then HTML header
_getTitle: ->
title = @anchoring.document.getDocumentInfo().Title?.trim()
if title? and title isnt ""
title
else
$("head title").text().trim()
# Get metadata
_metadata: ->
metadata =
link: [{
href: @_getFingerPrintURI()
}]
title: @_getTitle()
documentURI = @_getDocumentURI()
if documentURI.toLowerCase().indexOf('file://') is 0
metadata.filename = new URL(documentURI).pathname.split('/').pop()
else
metadata.link.push {href: documentURI}
metadata
# Public: Get metadata (when the doc is loaded). Returns a promise.
getMetaData: =>
new Promise (resolve, reject) =>
if @anchoring.document.waitForInit?
@anchoring.document.waitForInit().then =>
try
resolve @_metadata()
catch error
reject "Internal error"
else
reject "Not a PDF dom mapper."
annotator.plugins.BucketBar?.update()
# We want to react to some events
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
Annotator.Plugin.PDF = PDF
# This is what we do to new annotations
beforeAnnotationCreated: (annotation) =>
return unless @_isPDF()
annotation.document = @_metadata()
module.exports = PDF
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View
@@ -21,80 +21,23 @@ class Annotator.Plugin.TextSelection extends Annotator.Plugin
})
super
# Code used to create annotations around text ranges =====================
# Gets the current selection excluding any nodes that fall outside of
# the @wrapper. Then returns and Array of NormalizedRange instances.
#
# Examples
#
# # A selection inside @wrapper
# annotation.getSelectedRanges()
# # => Returns [NormalizedRange]
#
# # A selection outside of @wrapper
# annotation.getSelectedRanges()
# # => Returns []
#
# Returns Array of NormalizedRange instances.
_getSelectedRanges: ->
selection = Annotator.Util.getGlobal().getSelection()
ranges = []
rangesToIgnore = []
unless selection.isCollapsed
ranges = for i in [0...selection.rangeCount]
r = selection.getRangeAt(i)
browserRange = new Annotator.Range.BrowserRange(r)
normedRange = browserRange.normalize().limit @annotator.wrapper[0]
# If the new range falls fully outside the wrapper, we
# should add it back to the document but not return it from
# this method
rangesToIgnore.push(r) if normedRange is null
normedRange
# BrowserRange#normalize() modifies the DOM structure and deselects the
# underlying text as a result. So here we remove the selected ranges and
# reapply the new ones.
selection.removeAllRanges()
for r in rangesToIgnore
selection.addRange(r)
# Remove any ranges that fell outside of @wrapper.
$.grep ranges, (range) ->
# Add the normed range back to the selection if it exists.
selection.addRange(range.toRange()) if range
range
# This is called when the mouse is released.
# Checks to see if a selection has been made on mouseup and if so,
# Checks to see if a selection been made on mouseup and if so,
# calls Annotator's onSuccessfulSelection method.
#
# event - The event triggered this. Usually it's a mouseup Event,
# but that's not necessary. The coordinates will be used,
# if they are present. If the event (or the coordinates)
# are missing, new coordinates will be generated, based on the
# selected ranges.
# but that's not necessary.
#
# Returns nothing.
checkForEndSelection: (event = {}) =>
# Get the currently selected ranges.
selectedRanges = @_getSelectedRanges()
for range in selectedRanges
container = range.commonAncestor
return if @annotator.isAnnotator(container)
if selectedRanges.length
event.segments = []
for r in selectedRanges
event.segments.push
type: "text range"
range: r
selection = Annotator.Util.getGlobal().getSelection()
ranges = for i in [0...selection.rangeCount]
r = selection.getRangeAt(0)
if r.collapsed then continue else r
if ranges.length
event.ranges = ranges
@annotator.onSuccessfulSelection event
else
@annotator.onFailedSelection event
View
@@ -1,12 +1,21 @@
Promise = require('es6-promise').Promise
raf = require('raf')
Promise = global.Promise ? require('es6-promise').Promise
Annotator = require('annotator')
require('../monkey')
Guest = require('../guest')
anchoring = require('../anchoring/html')
highlighter = require('../highlighter')
assert = chai.assert
sinon.assert.expose(assert, prefix: '')
waitForSync = (annotation) ->
if annotation.$anchors?
return Promise.resolve()
else
return new Promise(setTimeout).then(-> waitForSync(annotation))
describe 'Guest', ->
sandbox = null
fakeCrossFrame = null
@@ -23,23 +32,6 @@ describe 'Guest', ->
on: sandbox.stub()
sync: sandbox.stub()
# Mock out the anchoring plugin. Oh how I wish I didn't have to do crazy
# shit like this.
Annotator.Plugin.EnhancedAnchoring = -> {
pluginInit: ->
@annotator.anchoring = this
_scan: sandbox.stub()
getHighlights: sandbox.stub().returns([])
getAnchors: sandbox.stub().returns([])
createAnchor: sandbox.spy (annotation, target) ->
anchor = "anchor for " + target
annotation.anchors.push anchor
result: anchor
}
Annotator.Plugin.CrossFrame = -> fakeCrossFrame
sandbox.spy(Annotator.Plugin, 'CrossFrame')
@@ -145,14 +137,8 @@ describe 'Guest', ->
beforeEach ->
guest = createGuest()
guest.plugins.Document = {uri: -> 'http://example.com'}
options = Annotator.Plugin.CrossFrame.lastCall.args[1]
it 'applies a "uri" property to the formatted object', ->
ann = {$$tag: 'tag1'}
formatted = options.formatter(ann)
assert.equal(formatted.uri, 'http://example.com/')
it 'keeps an existing uri property', ->
ann = {$$tag: 'tag1', uri: 'http://example.com/foo'}
formatted = options.formatter(ann)
@@ -195,50 +181,60 @@ describe 'Guest', ->
describe 'on "focusAnnotations" event', ->
it 'focuses any annotations with a matching tag', ->
highlight0 = $('<span></span>')
highlight1 = $('<span></span>')
guest = createGuest()
highlights = [
{annotation: {$$tag: 'tag1'}, setFocused: sandbox.stub()}
{annotation: {$$tag: 'tag2'}, setFocused: sandbox.stub()}
guest.anchors = [
{annotation: {$$tag: 'tag1'}, highlights: highlight0.toArray()}
{annotation: {$$tag: 'tag2'}, highlights: highlight1.toArray()}
]
guest.anchoring.getHighlights.returns(highlights)
emitGuestEvent('focusAnnotations', 'ctx', ['tag1'])
assert.called(highlights[0].setFocused)
assert.calledWith(highlights[0].setFocused, true)
assert.isTrue(highlight0.hasClass('annotator-hl-focused'))
it 'unfocuses any annotations without a matching tag', ->
highlight0 = $('<span class="annotator-hl-focused"></span>')
highlight1 = $('<span class="annotator-hl-focused"></span>')
guest = createGuest()
highlights = [
{annotation: {$$tag: 'tag1'}, setFocused: sandbox.stub()}
{annotation: {$$tag: 'tag2'}, setFocused: sandbox.stub()}
guest.anchors = [
{annotation: {$$tag: 'tag1'}, highlights: highlight0.toArray()}
{annotation: {$$tag: 'tag2'}, highlights: highlight1.toArray()}
]
guest.anchoring.getHighlights.returns(highlights)
emitGuestEvent('focusAnnotations', 'ctx', ['tag1'])
assert.called(highlights[1].setFocused)
assert.calledWith(highlights[1].setFocused, false)
assert.isFalse(highlight1.hasClass('annotator-hl-focused'))
describe 'on "scrollToAnnotation" event', ->
beforeEach ->
$.fn.scrollintoview = sandbox.stub()
afterEach ->
delete $.fn.scrollintoview
it 'scrolls to the anchor with the matching tag', ->
highlight = $('<span></span>')
guest = createGuest()
anchors = [
{annotation: {$$tag: 'tag1'}, scrollToView: sandbox.stub()}
guest.anchors = [
{annotation: {$$tag: 'tag1'}, highlights: highlight.toArray()}
]
guest.anchoring.getAnchors.returns(anchors)
emitGuestEvent('scrollToAnnotation', 'ctx', 'tag1')
assert.called(anchors[0].scrollToView)
assert.calledOn($.fn.scrollintoview, sinon.match(highlight))
describe 'on "getDocumentInfo" event', ->
guest = null
beforeEach ->
sandbox.stub(document, 'title', 'hi')
guest = createGuest()
guest.plugins.PDF =
uri: sandbox.stub().returns('http://example.com')
getMetaData: sandbox.stub()
uri: sandbox.stub().returns(window.location.href)
getMetadata: sandbox.stub()
afterEach ->
sandbox.restore()
it 'calls the callback with the href and pdf metadata', (done) ->
assertComplete = (payload) ->
try
assert.equal(payload.uri, 'http://example.com/')
assert.equal(payload.uri, document.location.href)
assert.equal(payload.metadata, metadata)
done()
catch e
@@ -247,27 +243,27 @@ describe 'Guest', ->
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
metadata = {title: 'hi'}
promise = Promise.resolve(metadata)
guest.plugins.PDF.getMetaData.returns(promise)
guest.plugins.PDF.getMetadata.returns(promise)
emitGuestEvent('getDocumentInfo', ctx)
it 'calls the callback with the href and document metadata if pdf check fails', (done) ->
it 'calls the callback with the href and basic metadata if pdf fails', (done) ->
assertComplete = (payload) ->
try
assert.equal(payload.uri, 'http://example.com/')
assert.equal(payload.metadata, metadata)
assert.equal(payload.uri, window.location.href)
assert.deepEqual(payload.metadata, metadata)
done()
catch e
done(e)
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
metadata = {title: 'hi'}
guest.plugins.Document = {metadata: metadata}
metadata = {title: 'hi', link: [{href: window.location.href}]}
promise = Promise.reject(new Error('Not a PDF document'))
guest.plugins.PDF.getMetaData.returns(promise)
guest.plugins.PDF.getMetadata.returns(promise)
emitGuestEvent('getDocumentInfo', ctx)
it 'notifies the channel that the return value is async', ->
delete guest.plugins.PDF
@@ -288,34 +284,174 @@ describe 'Guest', ->
guest.onAdderMouseup(event)
assert.isTrue(event.isPropagationStopped())
describe 'annotation sync', ->
it 'calls sync for createAnnotation', ->
describe 'createAnnotation()', ->
it 'adds metadata to the annotation object', (done) ->
guest = createGuest()
guest.createAnnotation({})
assert.called(fakeCrossFrame.sync)
it 'calls sync for setupAnnotation', ->
sinon.stub(guest, 'getDocumentInfo').returns(Promise.resolve({
metadata: {title: 'hello'}
uri: 'http://example.com/'
}))
annotation = {}
guest.createAnnotation(annotation)
setTimeout ->
assert.equal(annotation.uri, 'http://example.com/')
assert.deepEqual(annotation.document, {title: 'hello'})
done()
it 'treats an argument as the annotation object', ->
guest = createGuest()
guest.setupAnnotation({ranges: []})
assert.called(fakeCrossFrame.sync)
describe 'Annotator monkey patch', ->
describe 'setupAnnotation()', ->
it "doesn't declare annotation without targets as orphans", ->
guest = createGuest()
annotation = target: []
guest.setupAnnotation(annotation)
assert.isFalse !!annotation.$orphan
annotation = {foo: 'bar'}
annotation = guest.createAnnotation(annotation)
assert.equal(annotation.foo, 'bar')
describe 'setupAnnotation()', ->
el = null
range = null
beforeEach ->
el = document.createElement('span')
txt = document.createTextNode('hello')
el.appendChild(txt)
document.body.appendChild(el)
range = document.createRange()
range.selectNode(el)
afterEach ->
document.body.removeChild(el)
it "doesn't declare annotation without targets as orphans", (done) ->
guest = createGuest()
annotation = target: []
guest.setupAnnotation(annotation)
waitForSync(annotation).then ->
assert.isFalse(annotation.$orphan)
done()
it "doesn't declare annotations with a working target as orphans", ->
guest = createGuest()
annotation = target: ["test target"]
it "doesn't declare annotations with a working target as orphans", (done) ->
guest = createGuest()
annotation = target: [{selector: "test"}]
sandbox.stub(anchoring, 'anchor').returns(range)
guest.setupAnnotation(annotation)
waitForSync(annotation).then ->
assert.isFalse(annotation.$orphan)
done()
it "declares annotations with broken targets as orphans", (done) ->
guest = createGuest()
annotation = target: [{selector: 'broken selector'}]
sandbox.stub(anchoring, 'anchor').throws()
guest.setupAnnotation(annotation)
waitForSync(annotation).then ->
assert.isTrue(annotation.$orphan)
done()
it 'updates the cross frame and bucket bar plugins', (done) ->
guest = createGuest()
guest.plugins.CrossFrame =
sync: sinon.stub()
guest.plugins.BucketBar =
update: sinon.stub()
annotation = {}
guest.setupAnnotation(annotation)
waitForSync(annotation).then ->
assert.called(guest.plugins.BucketBar.update)
assert.called(guest.plugins.CrossFrame.sync)
done()
it 'saves the anchor positions on the annotation', (done) ->
guest = createGuest()
sandbox.stub(anchoring, 'anchor').returns(range)
clientRect = {top: 100, left: 200}
window.scrollX = 50
window.scrollY = 25
sandbox.stub(highlighter, 'getBoundingClientRect').returns(clientRect)
annotation = guest.setupAnnotation({target: [{selector: []}]})
waitForSync(annotation).then ->
assert.equal(annotation.$anchors.length, 1)
pos = annotation.$anchors[0].pos
assert.equal(pos.top, 125)
assert.equal(pos.left, 250)
done()
it 'adds the anchor to the "anchors" instance property"', (done) ->
guest = createGuest()
highlights = [document.createElement('span')]
sandbox.stub(anchoring, 'anchor').returns(range)
sandbox.stub(highlighter, 'highlightRange').returns(highlights)
target = [{selector: []}]
annotation = guest.setupAnnotation({target: [target]})
waitForSync(annotation).then ->
assert.equal(guest.anchors.length, 1)
assert.strictEqual(guest.anchors[0].annotation, annotation)
assert.strictEqual(guest.anchors[0].target, target)
assert.strictEqual(guest.anchors[0].range, range)
assert.strictEqual(guest.anchors[0].highlights, highlights)
done()
it 'destroys targets that have been removed from the annotation', (done) ->
annotation = {}
target = {}
highlights = []
guest = createGuest()
guest.anchors = [{annotation, target, highlights}]
removeHighlights = sandbox.stub(highlighter, 'removeHighlights')
guest.setupAnnotation(annotation)
waitForSync(annotation).then ->
assert.equal(guest.anchors.length, 0)
assert.calledWith(removeHighlights, highlights)
done()
it 'does not reanchor targets that are already anchored', (done) ->
guest = createGuest()
annotation = target: [{selector: "test"}]
stub = sandbox.stub(anchoring, 'anchor').returns(range)
guest.setupAnnotation(annotation)
waitForSync(annotation).then ->
delete annotation.$anchors
guest.setupAnnotation(annotation)
assert.isFalse !!annotation.$orphan
waitForSync(annotation).then ->
assert.equal(guest.anchors.length, 1)
assert.calledOnce(stub)
done()
it "declares annotations with broken targets as orphans", ->
guest = createGuest()
guest.anchoring.createAnchor = -> result: null
annotation = target: ["broken target"]
guest.setupAnnotation(annotation)
assert !!annotation.$orphan
describe 'deleteAnnotation()', ->
it 'removes the anchors from the "anchors" instance variable', (done) ->
guest = createGuest()
annotation = {}
guest.anchors.push({annotation})
guest.deleteAnnotation(annotation)
new Promise(raf).then ->
assert.equal(guest.anchors.length, 0)
done()
it 'updates the bucket bar plugin', (done) ->
guest = createGuest()
guest.plugins.BucketBar = update: sinon.stub()
annotation = {}
guest.anchors.push({annotation})
guest.deleteAnnotation(annotation)
new Promise(raf).then ->
assert.calledOnce(guest.plugins.BucketBar.update)
done()
it 'publishes the "annotationDeleted" event', (done) ->
guest = createGuest()
annotation = {}
publish = sandbox.stub(guest, 'publish')
guest.deleteAnnotation(annotation)
new Promise(raf).then ->
assert.calledOnce(publish)
assert.calledWith(publish, 'annotationDeleted', [annotation])
done()
it 'removes any highlights associated with the annotation', (done) ->
guest = createGuest()
annotation = {}
highlights = [document.createElement('span')]
removeHighlights = sandbox.stub(highlighter, 'removeHighlights')
guest.anchors.push({annotation, highlights})
guest.deleteAnnotation(annotation)
new Promise(raf).then ->
assert.calledOnce(removeHighlights)
assert.calledWith(removeHighlights, highlights)
done()
View
@@ -0,0 +1,98 @@
Annotator = require('annotator')
$ = Annotator.$
highlighter = require('../highlighter')
assert = chai.assert
sinon.assert.expose(assert, prefix: '')
describe "highlightRange", ->
it 'wraps a highlight span around the given range', ->
txt = document.createTextNode('test highlight span')
el = document.createElement('span')
el.appendChild(txt)
r = new Annotator.Range.NormalizedRange({
commonAncestor: el,
start: txt,
end: txt
})
result = highlighter.highlightRange(r)
assert.equal(result.length, 1)
assert.strictEqual(el.childNodes[0], result[0])
assert.isTrue(result[0].classList.contains('annotator-hl'))
it 'skips text nodes that are only white space', ->
txt = document.createTextNode('one')
blank = document.createTextNode(' ')
txt2 = document.createTextNode('two')
el = document.createElement('span')
el.appendChild(txt)
el.appendChild(blank)
el.appendChild(txt2)
r = new Annotator.Range.NormalizedRange({
commonAncestor: el,
start: txt,
end: txt2
})
result = highlighter.highlightRange(r)
assert.equal(result.length, 2)
assert.strictEqual(el.childNodes[0], result[0])
assert.strictEqual(el.childNodes[2], result[1])
describe 'removeHighlights', ->
it 'unwraps all the elements', ->
txt = document.createTextNode('word')
el = document.createElement('span')
hl = document.createElement('span')
div = document.createElement('div')
el.appendChild(txt)
hl.appendChild(el)
div.appendChild(hl)
highlighter.removeHighlights([hl])
assert.isNull(hl.parentNode)
assert.strictEqual(el.parentNode, div)
it 'does not fail on nodes with no parent', ->
txt = document.createTextNode('no parent')
hl = document.createElement('span')
hl.appendChild(txt)
highlighter.removeHighlights([hl])
describe "getBoundingClientRect", ->
it 'returns the bounding box of all the highlight client rectangles', ->
rects = [
{
top: 20
left: 15
bottom: 30
right: 25
}
{
top: 10
left: 15
bottom: 20
right: 25
}
{
top: 15
left: 20
bottom: 25
right: 30
}
{
top: 15
left: 10
bottom: 25
right: 20
}
]
fakeHighlights = rects.map (r) ->
return getBoundingClientRect: -> r
result = highlighter.getBoundingClientRect(fakeHighlights)
assert.equal(result.left, 10)
assert.equal(result.top, 10)
assert.equal(result.right, 30)
assert.equal(result.bottom, 30)
View
@@ -69,7 +69,11 @@ module.exports = class AppController
predicate = switch name
when 'Newest' then ['-!!message', '-message.updated']
when 'Oldest' then ['-!!message', 'message.updated']
when 'Location' then ['-!!message', 'message.target[0].pos.top']
when 'Location' then [
'-!!message'
'message.$anchors[0].pos.top'
'message.$anchors[0].pos.left'
]
$scope.sort = {name, predicate}
$scope.$watch (-> auth.user), (newVal, oldVal) ->
View
@@ -20,7 +20,10 @@ module.exports = class CrossFrame
new Discovery($window, options)
createAnnotationSync = ->
whitelist = ['$highlight', '$orphan', 'target', 'document', 'uri']
whitelist = [
'$anchors', '$highlight', '$orphan',
'target', 'document', 'uri'
]
options =
formatter: (annotation) ->
formatted = {}
View
@@ -406,6 +406,26 @@ module.exports = [
scope.$on '$destroy', ->
if ctrl.editing then counter?.count 'edit', -1
elem.on('beforepaste', (e) -> e.preventDefault())
elem.on('paste', (event) ->
clipboardData = event.originalEvent.clipboardData
return unless 'text/html' in clipboardData.types
content = angular.element('<div></div>')
content.html(clipboardData.getData('text/html'))
link = content.find('link').last()
return unless link.attr('href') is 'annotator:clipTarget'
clipTargetData = localStorage.getItem('clipTarget')
return unless clipTargetData
event.preventDefault()
model = scope.annotationGet()
model.target ?= []
model.target.push(JSON.parse(clipTargetData))
)
controller: AnnotationController
controllerAs: 'vm'
link: linkFn
View
@@ -1,4 +1,4 @@
Promise = require('es6-promise').Promise
Promise = global.Promise ? require('es6-promise').Promise
{module, inject} = require('angular-mock')
assert = chai.assert
Oops, something went wrong.