Commit 4ebdeebd authored by Nick Stenning's avatar Nick Stenning

Import Annotator sources into h

Rather than using a prebuilt vendored Annotator from our fork, this
commit imports the entire forked Annotator source into the h repository.
Our intent is not to maintain this fork within h, but to remove the fork
repository and incrementally move back to dependence on upstream
Annotator.
parent 5279e095
# Abstract anchor class.
class Anchor
constructor: (@annotator, @annotation, @target
@startPage, @endPage,
@quote, @diffHTML, @diffCaseOnly) ->
unless @annotator? then throw "annotator is required!"
unless @annotation? then throw "annotation is required!"
unless @target? then throw "target is required!"
unless @startPage? then "startPage is required!"
unless @endPage? then throw "endPage is required!"
unless @quote? then throw "quote is required!"
@highlight = {}
# Return highlights for the given page
_createHighlight: (page) ->
throw "Function not implemented"
# Create the missing highlights for this anchor
realize: () =>
return if @fullyRealized # If we have everything, go home
# Collect the pages that are already rendered
renderedPages = [@startPage .. @endPage].filter (index) =>
@annotator.domMapper.isPageMapped index
# Collect the pages that are already rendered, but not yet anchored
pagesTodo = renderedPages.filter (index) => not @highlight[index]?
return unless pagesTodo.length # Return if nothing to do
# Create the new highlights
created = for page in pagesTodo
@highlight[page] = @_createHighlight page
# Check if everything is rendered now
@fullyRealized = renderedPages.length is @endPage - @startPage + 1
# Announce the creation of the highlights
@annotator.publish 'highlightsCreated', created
# Remove the highlights for the given set of pages
virtualize: (pageIndex) =>
highlight = @highlight[pageIndex]
return unless highlight? # No highlight for this page
highlight.removeFromDocument()
delete @highlight[pageIndex]
# Mark this anchor as not fully rendered
@fullyRealized = false
# Announce the removal of the highlight
@annotator.publish 'highlightRemoved', highlight
# Virtualize and remove an anchor from all involved pages
remove: ->
# Go over all the pages
for index in [@startPage .. @endPage]
@virtualize index
anchors = @annotator.anchors[index]
# Remove the anchor from the list
i = anchors.indexOf this
anchors[i..i] = []
# Kill the list if it's empty
delete @annotator.anchors[index] unless anchors.length
# This is called when the underlying Annotator has been udpated
annotationUpdated: ->
# Notify the highlights
for index in [@startPage .. @endPage]
@highlight[index]?.annotationUpdated()
# Selection and range creation reference for the following code:
# http://www.quirksmode.org/dom/range_intro.html
#
# I've removed any support for IE TextRange (see commit d7085bf2 for code)
# for the moment, having no means of testing it.
util =
uuid: (-> counter = 0; -> counter++)()
getGlobal: -> (-> this)()
# Return the maximum z-index of any element in $elements (a jQuery collection).
maxZIndex: ($elements) ->
all = for el in $elements
if $(el).css('position') == 'static'
-1
else
# Use parseFloat since we may get scientific notation for large
# values.
parseFloat($(el).css('z-index')) or -1
Math.max.apply(Math, all)
mousePosition: (e, offsetEl) ->
# If the offset element is not a positioning root use its offset parent
unless $(offsetEl).css('position') in ['absolute', 'fixed', 'relative']
offsetEl = $(offsetEl).offsetParent()[0]
offset = $(offsetEl).offset()
{
top: e.pageY - offset.top,
left: e.pageX - offset.left
}
# Checks to see if an event parameter is provided and contains the prevent
# default method. If it does it calls it.
#
# This is useful for methods that can be optionally used as callbacks
# where the existance of the parameter must be checked before calling.
preventEventDefault: (event) ->
event?.preventDefault?()
# Store a reference to the current Annotator object.
_Annotator = this.Annotator
# Fake two-phase / pagination support, used for HTML documents
class DummyDocumentAccess
@applicable: -> true
getPageIndex: -> 0
getPageCount: -> 1
getPageIndexForPos: -> 0
isPageMapped: -> true
scan: ->
class Annotator extends Delegator
# Events to be bound on Annotator#element.
events:
".annotator-adder button click": "onAdderClick"
".annotator-adder button mousedown": "onAdderMousedown"
html:
adder: '<div class="annotator-adder"><button>' + _t('Annotate') + '</button></div>'
wrapper: '<div class="annotator-wrapper"></div>'
options: # Configuration options
readOnly: false # Start Annotator in read-only mode. No controls will be shown.
plugins: {}
editor: null
viewer: null
selectedTargets: null
mouseIsDown: false
inAdderClick: false
viewerHideTimer: null
# Public: Creates an instance of the Annotator. Requires a DOM Element in
# which to watch for annotations as well as any options.
#
# NOTE: If the Annotator is not supported by the current browser it will not
# perform any setup and simply return a basic object. This allows plugins
# to still be loaded but will not function as expected. It is reccomended
# to call Annotator.supported() before creating the instance or using the
# Unsupported plugin which will notify users that the Annotator will not work.
#
# element - A DOM Element in which to annotate.
# options - An options Object. NOTE: There are currently no user options.
#
# Examples
#
# annotator = new Annotator(document.body)
#
# # Example of checking for support.
# if Annotator.supported()
# annotator = new Annotator(document.body)
# else
# # Fallback for unsupported browsers.
#
# Returns a new instance of the Annotator.
constructor: (element, options) ->
super
@plugins = {}
@selectorCreators = []
@anchoringStrategies = []
# Return early if the annotator is not supported.
return this unless Annotator.supported()
this._setupDocumentEvents() unless @options.readOnly
this._setupAnchorEvents()
this._setupWrapper()
this._setupDocumentAccessStrategies()
this._setupViewer()._setupEditor()
this._setupDynamicStyle()
# Perform initial DOM scan, unless told not to.
this._scan() unless @options.noScan
# Create adder
this.adder = $(this.html.adder).appendTo(@wrapper).hide()
# Initializes the available document access strategies
_setupDocumentAccessStrategies: ->
@documentAccessStrategies = [
# Default dummy strategy for simple HTML documents.
# The generic fallback.
name: "Dummy"
mapper: DummyDocumentAccess
]
this
# Initializes the components used for analyzing the document
_chooseAccessPolicy: ->
if @domMapper? then return
# Go over the available strategies
for s in @documentAccessStrategies
# Can we use this strategy for this document?
if s.mapper.applicable()
@documentAccessStrategy = s
@domMapper = new s.mapper()
@anchors = {}
addEventListener "docPageMapped", (evt) =>
@_realizePage evt.pageIndex
addEventListener "docPageUnmapped", (evt) =>
@_virtualizePage evt.pageIndex
s.init?()
return this
# Remove the current document access policy
_removeCurrentAccessPolicy: ->
return unless @domMapper?
list = @documentAccessStrategies
index = list.indexOf @documentAccessStrategy
list.splice(index, 1) unless index is -1
@domMapper.destroy?()
delete @domMapper
# Perform a scan of the DOM. Required for finding anchors.
_scan: ->
# Ensure that we have a document access strategy
this._chooseAccessPolicy()
try
@pendingScan = @domMapper.scan()
catch
@_removeCurrentAccessPolicy()
@_scan()
return
# Wraps the children of @element in a @wrapper div. NOTE: This method will also
# remove any script elements inside @element to prevent them re-executing.
#
# Returns itself to allow chaining.
_setupWrapper: ->
@wrapper = $(@html.wrapper)
# We need to remove all scripts within the element before wrapping the
# contents within a div. Otherwise when scripts are reappended to the DOM
# they will re-execute. This is an issue for scripts that call
# document.write() - such as ads - as they will clear the page.
@element.find('script').remove()
@element.wrapInner(@wrapper)
@wrapper = @element.find('.annotator-wrapper')
this
# Creates an instance of Annotator.Viewer and assigns it to the @viewer
# property, appends it to the @wrapper and sets up event listeners.
#
# Returns itself to allow chaining.
_setupViewer: ->
@viewer = new Annotator.Viewer(readOnly: @options.readOnly)
@viewer.hide()
.on("edit", this.onEditAnnotation)
.on("delete", this.onDeleteAnnotation)
.addField({
load: (field, annotation) =>
if annotation.text
$(field).html(Util.escape(annotation.text))
else
$(field).html("<i>#{_t 'No Comment'}</i>")
this.publish('annotationViewerTextField', [field, annotation])
})
.element.appendTo(@wrapper).bind({
"mouseover": this.clearViewerHideTimer
"mouseout": this.startViewerHideTimer
})
this
# Creates an instance of the Annotator.Editor and assigns it to @editor.
# Appends this to the @wrapper and sets up event listeners.
#
# Returns itself for chaining.
_setupEditor: ->
@editor = new Annotator.Editor()
@editor.hide()
.on('hide', this.onEditorHide)
.on('save', this.onEditorSubmit)
.addField({
type: 'textarea',
label: _t('Comments') + '\u2026'
load: (field, annotation) ->
$(field).find('textarea').val(annotation.text || '')
submit: (field, annotation) ->
annotation.text = $(field).find('textarea').val()
})
@editor.element.appendTo(@wrapper)
this
# Sets up the selection event listeners to watch mouse actions on the document.
#
# Returns itself for chaining.
_setupDocumentEvents: ->
$(document).bind({
"mousedown": this.checkForStartSelection
})
this
# Sets up handlers to anchor-related events
_setupAnchorEvents: ->
# When annotations are updated
@on 'annotationUpdated', (annotation) =>
# Notify the anchors
for anchor in annotation.anchors or []
anchor.annotationUpdated()
# Sets up any dynamically calculated CSS for the Annotator.
#
# Returns itself for chaining.
_setupDynamicStyle: ->
style = $('#annotator-dynamic-style')
if (!style.length)
style = $('<style id="annotator-dynamic-style"></style>').appendTo(document.head)
sel = '*' + (":not(.annotator-#{x})" for x in ['adder', 'outer', 'notice', 'filter']).join('')
# use the maximum z-index in the page
max = util.maxZIndex($(document.body).find(sel))
# but don't go smaller than 1010, because this isn't bulletproof --
# dynamic elements in the page (notifications, dialogs, etc.) may well
# have high z-indices that we can't catch using the above method.
max = Math.max(max, 1000)
style.text [
".annotator-adder, .annotator-outer, .annotator-notice {"
" z-index: #{max + 20};"
"}"
".annotator-filter {"
" z-index: #{max + 10};"
"}"
].join("\n")
this
# Public: Destroy the current Annotator instance, unbinding all events and
# disposing of all relevant elements.
#
# Returns nothing.
destroy: ->
$(document).unbind({
"mouseup": this.checkForEndSelection
"mousedown": this.checkForStartSelection
})
$('#annotator-dynamic-style').remove()
@adder.remove()
@viewer.destroy()
@editor.destroy()
@wrapper.find('.annotator-hl').each ->
$(this).contents().insertBefore(this)
$(this).remove()
@wrapper.contents().insertBefore(@wrapper)
@wrapper.remove()
@element.data('annotator', null)
for name, plugin of @plugins
@plugins[name].destroy()
this.removeEvents()
# Utility function to get the decoded form of the document URI
getHref: =>
uri = decodeURIComponent document.location.href
if document.location.hash then uri = uri.slice 0, (-1 * location.hash.length)
$('meta[property^="og:url"]').each -> uri = decodeURIComponent this.content
$('link[rel^="canonical"]').each -> uri = decodeURIComponent this.href
return uri
# Public: Creates and returns a new annotation object. Publishes the
# 'beforeAnnotationCreated' event to allow the new annotation to be modified.
#
# Examples
#
# annotator.createAnnotation() # Returns {}
#
# annotator.on 'beforeAnnotationCreated', (annotation) ->
# annotation.myProperty = 'This is a custom property'
# annotator.createAnnotation() # Returns {myProperty: "This is a…"}
#
# Returns a newly created annotation Object.
createAnnotation: () ->
annotation = {}
this.publish('beforeAnnotationCreated', [annotation])
annotation
# Do some normalization to get a "canonical" form of a string.
# Used to even out some browser differences.
normalizeString: (string) -> string.replace /\s{2,}/g, " "
# Find the given type of selector from an array of selectors, if it exists.
# If it does not exist, null is returned.
findSelector: (selectors, type) ->
for selector in selectors
if selector.type is type then return selector
null
# Try to find the right anchoring point for a given target
#
# Returns an Anchor object if succeeded, null otherwise
createAnchor: (annotation, target) ->
unless target?
throw new Error "Trying to find anchor for null target!"
error = null
anchor = null
for s in @anchoringStrategies
try
a = s.code.call this, annotation, target
if a
return result: a
catch error
console.log "Strategy '" + s.name + "' has thrown an error.",
error.stack ? error
return error: "No strategies worked."
# Public: Initialises an annotation either from an object representation or
# an annotation created with Annotator#createAnnotation(). It finds the
# selected range and higlights the selection in the DOM, extracts the
# quoted text and serializes the range.
#
# annotation - An annotation Object to initialise.
#
# Examples
#
# # Create a brand new annotation from the currently selected text.
# annotation = annotator.createAnnotation()
# annotation = annotator.setupAnnotation(annotation)
# # annotation has now been assigned the currently selected range
# # and a highlight appended to the DOM.
#
# # Add an existing annotation that has been stored elsewere to the DOM.
# annotation = getStoredAnnotationWithSerializedRanges()
# annotation = annotator.setupAnnotation(annotation)
#
# Returns the initialised annotation.
setupAnnotation: (annotation) ->
# If this is a new annotation, we might have to add the targets
annotation.target ?= @selectedTargets
@selectedTargets = []
annotation.anchors = []
for t in annotation.target ? []
try
# Create an anchor for this target
result = this.createAnchor annotation, t
anchor = result.result
if result.error? instanceof Range.RangeError
this.publish 'rangeNormalizeFail', [annotation, result.error.range, result.error]
if anchor?
t.diffHTML = anchor.diffHTML
t.diffCaseOnly = anchor.diffCaseOnly
# Store this anchor for the annotation
annotation.anchors.push anchor
# Store the anchor for all involved pages
for pageIndex in [anchor.startPage .. anchor.endPage]
@anchors[pageIndex] ?= []
@anchors[pageIndex].push anchor
# Realizing the anchor
anchor.realize()
catch exception
console.log "Error in setupAnnotation for", annotation.id,
":", exception.stack ? exception
annotation
# Public: Publishes the 'beforeAnnotationUpdated' and 'annotationUpdated'
# events. Listeners wishing to modify an updated annotation should subscribe
# to 'beforeAnnotationUpdated' while listeners storing annotations should
# subscribe to 'annotationUpdated'.
#
# annotation - An annotation Object to update.
#
# Examples
#
# annotation = {tags: 'apples oranges pears'}
# annotator.on 'beforeAnnotationUpdated', (annotation) ->
# # validate or modify a property.
# annotation.tags = annotation.tags.split(' ')
# annotator.updateAnnotation(annotation)
# # => Returns ["apples", "oranges", "pears"]
#
# Returns annotation Object.
updateAnnotation: (annotation) ->
this.publish('beforeAnnotationUpdated', [annotation])
this.publish('annotationUpdated', [annotation])
annotation
# Public: Deletes the annotation by removing the highlight from the DOM.
# Publishes the 'annotationDeleted' event on completion.
#
# annotation - An annotation Object to delete.
#
# Returns deleted annotation.
deleteAnnotation: (annotation) ->
if annotation.anchors?
for a in annotation.anchors
a.remove()
this.publish('annotationDeleted', [annotation])
annotation
# Public: Loads an Array of annotations into the @element. Breaks the task
# into chunks of 10 annotations.
#
# annotations - An Array of annotation Objects.
#
# Examples
#
# loadAnnotationsFromStore (annotations) ->
# annotator.loadAnnotations(annotations)
#
# Returns itself for chaining.
loadAnnotations: (annotations=[]) ->
loader = (annList=[]) =>
now = annList.splice(0,10)
for n in now
this.setupAnnotation(n)
# If there are more to do, do them after a 10ms break (for browser
# responsiveness).
if annList.length > 0
setTimeout((-> loader(annList)), 10)
else
this.publish 'annotationsLoaded', [clone]
clone = annotations.slice()
if annotations.length # Do we have to do something?
if @pendingScan? # Is there a pending scan?
# Schedule the parsing the annotations for
# when scan has finished
@pendingScan.then =>
setTimeout => loader annotations
else # no pending scan
# We can start parsing them right away
loader annotations
this
# Public: Calls the Store#dumpAnnotations() method.
#
# Returns dumped annotations Array or false if Store is not loaded.
dumpAnnotations: () ->
if @plugins['Store']
@plugins['Store'].dumpAnnotations()
else
console.warn(_t("Can't dump annotations without Store plugin."))
return false
# Public: Registers a plugin with the Annotator. A plugin can only be
# registered once. The plugin will be instantiated in the following order.
#
# 1. A new instance of the plugin will be created (providing the @element and
# options as params) then assigned to the @plugins registry.
# 2. The current Annotator instance will be attached to the plugin.
# 3. The Plugin#pluginInit() method will be called if it exists.
#
# name - Plugin to instantiate. Must be in the Annotator.Plugins namespace.
# options - Any options to be provided to the plugin constructor.
#
# Examples
#
# annotator
# .addPlugin('Tags')
# .addPlugin('Store', {
# prefix: '/store'
# })
# .addPlugin('Permissions', {
# user: 'Bill'
# })
#
# Returns itself to allow chaining.
addPlugin: (name, options) ->
if @plugins[name]
console.error _t("You cannot have more than one instance of any plugin.")
else
klass = Annotator.Plugin[name]
if typeof klass is 'function'
@plugins[name] = new klass(@element[0], options)
@plugins[name].annotator = this
@plugins[name].pluginInit?()
else
console.error _t("Could not load ") + name + _t(" plugin. Have you included the appropriate <script> tag?")
this # allow chaining
# Public: Loads the @editor with the provided annotation and updates its
# position in the window.
#
# annotation - An annotation to load into the editor.
# location - Position to set the Editor in the form {top: y, left: x}
#
# Examples
#
# annotator.showEditor({text: "my comment"}, {top: 34, left: 234})
#
# Returns itself to allow chaining.
showEditor: (annotation, location) =>
@editor.element.css(location)
@editor.load(annotation)
this.publish('annotationEditorShown', [@editor, annotation])
this
# Callback method called when the @editor fires the "hide" event. Itself
# publishes the 'annotationEditorHidden' event
#
# Returns nothing.
onEditorHide: =>
this.publish('annotationEditorHidden', [@editor])
# Callback method called when the @editor fires the "save" event. Itself
# publishes the 'annotationEditorSubmit' event and creates/updates the
# edited annotation.
#
# Returns nothing.
onEditorSubmit: (annotation) =>
this.publish('annotationEditorSubmit', [@editor, annotation])
# Public: Loads the @viewer with an Array of annotations and positions it
# at the location provided. Calls the 'annotationViewerShown' event.
#
# annotation - An Array of annotations to load into the viewer.
# location - Position to set the Viewer in the form {top: y, left: x}
#
# Examples
#
# annotator.showViewer(
# [{text: "my comment"}, {text: "my other comment"}],
# {top: 34, left: 234})
# )
#
# Returns itself to allow chaining.
showViewer: (annotations, location) =>
@viewer.element.css(location)
@viewer.load(annotations)
this.publish('annotationViewerShown', [@viewer, annotations])
# Annotator#element event callback. Allows 250ms for mouse pointer to get from
# annotation highlight to @viewer to manipulate annotations. If timer expires
# the @viewer is hidden.
#
# Returns nothing.
startViewerHideTimer: =>
# Don't do this if timer has already been set by another annotation.
if not @viewerHideTimer
@viewerHideTimer = setTimeout @viewer.hide, 250
# Viewer#element event callback. Clears the timer set by
# Annotator#startViewerHideTimer() when the @viewer is moused over.
#
# Returns nothing.
clearViewerHideTimer: () =>
clearTimeout(@viewerHideTimer)
@viewerHideTimer = false
# Annotator#element callback. Sets the @mouseIsDown property used to
# determine if a selection may have started to true. Also calls
# Annotator#startViewerHideTimer() to hide the Annotator#viewer.
#
# event - A mousedown Event object.
#
# Returns nothing.
checkForStartSelection: (event) =>
unless event and this.isAnnotator(event.target)
this.startViewerHideTimer()
@mouseIsDown = true
# This is called to create a target from a raw selection,
# using selectors created by the registered selector creators
_getTargetFromSelection: (selection) =>
selectors = []
for c in @selectorCreators
description = c.describe selection
for selector in description
selectors.push selector
# Create the target
source: @getHref()
selector: selectors
# This method is to be called by the mechanisms responsible for
# triggering annotation (and highlight) creation.
#
# event - any event which has triggered this.
# The following fields are used:
# targets: an array of targets, which should be used to anchor the
# newly created annotation
# pageX and pageY: if the adder button is shown, use there coordinates
#
# immadiate - should we show the adder button, or should be proceed
# to create the annotation/highlight immediately ?
#
# returns false if the creation of annotations is forbidden at the moment,
# true otherwise.
onSuccessfulSelection: (event, immediate = false) ->
# Check whether we got a proper event
unless event?
throw "Called onSuccessfulSelection without an event!"
unless event.segments?
throw "Called onSuccessulSelection with an event with missing segments!"
# Describe the selection with targets
@selectedTargets = (@_getTargetFromSelection s for s in event.segments)
# Do we want immediate annotation?
if immediate
# Create an annotation
@onAdderClick event
else
# Show the adder button
@adder
.css(util.mousePosition(event, @wrapper[0]))
.show()
true
onFailedSelection: (event) ->
@adder.hide()
@selectedTargets = []
# Public: Determines if the provided element is part of the annotator plugin.
# Useful for ignoring mouse actions on the annotator elements.
# NOTE: The @wrapper is not included in this check.
#
# element - An Element or TextNode to check.
#
# Examples
#
# span = document.createElement('span')
# annotator.isAnnotator(span) # => Returns false
#
# annotator.isAnnotator(annotator.viewer.element) # => Returns true
#
# Returns true if the element is a child of an annotator element.
isAnnotator: (element) ->
!!$(element).parents().andSelf().filter('[class^=annotator-]').not(@wrapper).length
# Annotator#element callback.
#
# event - A mousedown Event object
#
# Returns nothing.
onAdderMousedown: (event) =>
event?.preventDefault()
@inAdderClick = true
# Annotator#element callback. Displays the @editor in place of the @adder and
# loads in a newly created annotation Object. The click event is used as well
# as the mousedown so that we get the :active state on the @adder when clicked
#
# event - A mousedown Event object
#
# Returns nothing.
onAdderClick: (event) =>
event?.preventDefault?()
# Hide the adder
position = @adder.position()
@adder.hide()
@inAdderClick = false
# Create a new annotation.
annotation = this.createAnnotation()
# Extract the quotation and serialize the ranges
annotation = this.setupAnnotation(annotation)
# Show a temporary highlight so the user can see what they selected
for anchor in annotation.anchors
for page, hl of anchor.highlight
hl.setTemporary true
# Make the highlights permanent if the annotation is saved
save = =>
do cleanup
for anchor in annotation.anchors
for page, hl of anchor.highlight
hl.setTemporary false
# Fire annotationCreated events so that plugins can react to them
this.publish('annotationCreated', [annotation])
# Remove the highlights if the edit is cancelled
cancel = =>
do cleanup
this.deleteAnnotation(annotation)
# Don't leak handlers at the end
cleanup = =>
this.unsubscribe('annotationEditorHidden', cancel)
this.unsubscribe('annotationEditorSubmit', save)
# Subscribe to the editor events
this.subscribe('annotationEditorHidden', cancel)
this.subscribe('annotationEditorSubmit', save)
# Display the editor.
this.showEditor(annotation, position)
# Annotator#viewer callback function. Displays the Annotator#editor in the
# positions of the Annotator#viewer and loads the passed annotation for
# editing.
#
# annotation - An annotation Object for editing.
#
# Returns nothing.
onEditAnnotation: (annotation) =>
offset = @viewer.element.position()
# Update the annotation when the editor is saved
update = =>
do cleanup
this.updateAnnotation(annotation)
# Remove handlers when finished
cleanup = =>
this.unsubscribe('annotationEditorHidden', cleanup)
this.unsubscribe('annotationEditorSubmit', update)
# Subscribe to the editor events
this.subscribe('annotationEditorHidden', cleanup)
this.subscribe('annotationEditorSubmit', update)
# Replace the viewer with the editor
@viewer.hide()
this.showEditor(annotation, offset)
# Annotator#viewer callback function. Deletes the annotation provided to the
# callback.
#
# annotation - An annotation Object for deletion.
#
# Returns nothing.
onDeleteAnnotation: (annotation) =>
@viewer.hide()
# Delete highlight elements.
this.deleteAnnotation annotation
# Collect all the highlights (optionally for a given set of annotations)
getHighlights: (annotations) ->
results = []
if annotations?
# Collect only the given set of annotations
for annotation in annotations
for anchor in annotation.anchors
for page, hl of anchor.highlight
results.push hl
else
# Collect from everywhere
for page, anchors of @anchors
$.merge results, (anchor.highlight[page] for anchor in anchors when anchor.highlight[page]?)
results
# Realize anchors on a given pages
_realizePage: (index) ->
# If the page is not mapped, give up
return unless @domMapper.isPageMapped index
# Go over all anchors related to this page
for anchor in @anchors[index] ? []
anchor.realize()
# Virtualize anchors on a given page
_virtualizePage: (index) ->
# Go over all anchors related to this page
for anchor in @anchors[index] ? []
anchor.virtualize index
onAnchorMouseover: (event) ->
# Cancel any pending hiding of the viewer.
this.clearViewerHideTimer()
# Don't do anything if we're making a selection or
# already displaying the viewer
return false if @mouseIsDown or @viewer.isShown()
this.showViewer event.data.getAnnotations(event),
util.mousePosition(event, @wrapper[0])
onAnchorMouseout: (event) ->
this.startViewerHideTimer()
onAnchorMousedown: (event) ->
onAnchorClick: (event) ->
# Create namespace for Annotator plugins
class Annotator.Plugin extends Delegator
constructor: (element, options) ->
super
pluginInit: ->
destroy: ->
this.removeEvents()
# Sniff the browser environment and attempt to add missing functionality.
g = util.getGlobal()
# Checks for the presence of wicked-good-xpath
# It is always safe to install it, it'll not overwrite existing functions
if g.wgxpath? then g.wgxpath.install()
if not g.getSelection?
$.getScript('http://assets.annotateit.org/vendor/ierange.min.js')
if not g.JSON?
$.getScript('http://assets.annotateit.org/vendor/json2.min.js')
# Ensure the Node constants are defined
if not g.Node?
g.Node =
ELEMENT_NODE : 1
ATTRIBUTE_NODE : 2
TEXT_NODE : 3
CDATA_SECTION_NODE : 4
ENTITY_REFERENCE_NODE : 5
ENTITY_NODE : 6
PROCESSING_INSTRUCTION_NODE : 7
COMMENT_NODE : 8
DOCUMENT_NODE : 9
DOCUMENT_TYPE_NODE : 10
DOCUMENT_FRAGMENT_NODE : 11
NOTATION_NODE : 12
# Bind our local copy of jQuery so plugins can use the extensions.
Annotator.$ = $
# Export other modules for use in plugins.
Annotator.Delegator = Delegator
Annotator.Range = Range
Annotator.util = util
Annotator.Util = Util
# Expose a global instance registry
Annotator._instances = []
Annotator.Highlight = Highlight
Annotator.Anchor = Anchor
# Bind gettext helper so plugins can use localisation.
Annotator._t = _t
# Returns true if the Annotator can be used in the current browser.
Annotator.supported = -> (-> !!this.getSelection)()
# Restores the Annotator property on the global object to it's
# previous value and returns the Annotator.
Annotator.noConflict = ->
util.getGlobal().Annotator = _Annotator
this
# Create global access for Annotator
$.fn.annotator = (options) ->
args = Array::slice.call(arguments, 1)
this.each ->
# check the data() cache, if it's there we'll call the method requested
instance = $.data(this, 'annotator')
if instance
options && instance[options].apply(instance, args)
else
instance = new Annotator(this, options)
$.data(this, 'annotator', instance)
# Export Annotator object.
this.Annotator = Annotator;
# Public: Delegator is the base class that all of Annotators objects inherit
# from. It provides basic functionality such as instance options, event
# delegation and pub/sub methods.
class Delegator
# Public: Events object. This contains a key/pair hash of events/methods that
# should be bound. See Delegator#addEvents() for usage.
events: {}
# Public: Options object. Extended on initialisation.
options: {}
# A jQuery object wrapping the DOM Element provided on initialisation.
element: null
# Public: Constructor function that sets up the instance. Binds the @events
# hash and extends the @options object.
#
# element - The DOM element that this intance represents.
# options - An Object literal of options.
#
# Examples
#
# element = document.getElementById('my-element')
# instance = new Delegator(element, {
# option: 'my-option'
# })
#
# Returns a new instance of Delegator.
constructor: (element, options) ->
@options = $.extend(true, {}, @options, options)
@element = $(element)
# Delegator creates closures for each event it binds. This is a private
# registry of created closures, used to enable event unbinding.
@_closures = {}
this.on = this.subscribe
this.addEvents()
# Public: binds the function names in the @events Object to their events.
#
# The @events Object should be a set of key/value pairs where the key is the
# event name with optional CSS selector. The value should be a String method
# name on the current class.
#
# This is called by the default Delegator constructor and so shouldn't usually
# need to be called by the user.
#
# Examples
#
# # This will bind the clickedElement() method to the click event on @element.
# @options = {"click": "clickedElement"}
#
# # This will delegate the submitForm() method to the submit event on the
# # form within the @element.
# @options = {"form submit": "submitForm"}
#
# # This will bind the updateAnnotationStore() method to the custom
# # annotation:save event. NOTE: Because this is a custom event the
# # Delegator#subscribe() method will be used and updateAnnotationStore()
# # will not recieve an event parameter like the previous two examples.
# @options = {"annotation:save": "updateAnnotationStore"}
#
# Returns nothing.
addEvents: ->
for event in Delegator._parseEvents(@events)
this._addEvent event.selector, event.event, event.functionName
# Public: unbinds functions previously bound to events by addEvents().
#
# The @events Object should be a set of key/value pairs where the key is the
# event name with optional CSS selector. The value should be a String method
# name on the current class.
#
# Returns nothing.
removeEvents: ->
for event in Delegator._parseEvents(@events)
this._removeEvent event.selector, event.event, event.functionName
# Binds an event to a callback function represented by a String. A selector
# can be provided in order to watch for events on a child element.
#
# The event can be any standard event supported by jQuery or a custom String.
# If a custom string is used the callback function will not recieve an
# event object as it's first parameter.
#
# selector - Selector String matching child elements. (default: '')
# event - The event to listen for.
# functionName - A String function name to bind to the event.
#
# Examples
#
# # Listens for all click events on instance.element.
# instance._addEvent('', 'click', 'onClick')
#
# # Delegates the instance.onInputFocus() method to focus events on all
# # form inputs within instance.element.
# instance._addEvent('form :input', 'focus', 'onInputFocus')
#
# Returns itself.
_addEvent: (selector, event, functionName) ->
f = if typeof functionName is 'string'
this[functionName]
else
functionName
closure = => f.apply(this, arguments)
if selector == '' and Delegator._isCustomEvent(event)
this.subscribe(event, closure)
else
@element.delegate(selector, event, closure)
@_closures["#{selector}/#{event}/#{functionName}"] = closure
this
# Unbinds a function previously bound to an event by the _addEvent method.
#
# Takes the same arguments as _addEvent(), and an event will only be
# successfully unbound if the arguments to removeEvent() are exactly the same
# as the original arguments to _addEvent(). This would usually be called by
# _removeEvents().
#
# selector - Selector String matching child elements. (default: '')
# event - The event to listen for.
# functionName - A String function name to bind to the event.
#
# Returns itself.
_removeEvent: (selector, event, functionName) ->
closure = @_closures["#{selector}/#{event}/#{functionName}"]
if selector == '' and Delegator._isCustomEvent(event)
this.unsubscribe(event, closure)
else
@element.undelegate(selector, event, closure)
delete @_closures["#{selector}/#{event}/#{functionName}"]
this
# Public: Fires an event and calls all subscribed callbacks with any parameters
# provided. This is essentially an alias of @element.triggerHandler() but
# should be used to fire custom events.
#
# NOTE: Events fired using .publish() will not bubble up the DOM.
#
# event - A String event name.
# params - An Array of parameters to provide to callbacks.
#
# Examples
#
# instance.subscribe('annotation:save', (msg) -> console.log(msg))
# instance.publish('annotation:save', ['Hello World'])
# # => Outputs "Hello World"
#
# Returns itself.
publish: () ->
@element.triggerHandler.apply @element, arguments
this
# Public: Listens for custom event which when published will call the provided
# callback. This is essentially a wrapper around @element.bind() but removes
# the event parameter that jQuery event callbacks always recieve. These
# parameters are unnessecary for custom events.
#
# event - A String event name.
# callback - A callback function called when the event is published.
#
# Examples
#
# instance.subscribe('annotation:save', (msg) -> console.log(msg))
# instance.publish('annotation:save', ['Hello World'])
# # => Outputs "Hello World"
#
# Returns itself.
subscribe: (event, callback) ->
closure = -> callback.apply(this, [].slice.call(arguments, 1))
# Ensure both functions have the same unique id so that jQuery will accept
# callback when unbinding closure.
closure.guid = callback.guid = ($.guid += 1)
@element.bind event, closure
this
# Public: Unsubscribes a callback from an event. The callback will no longer
# be called when the event is published.
#
# event - A String event name.
# callback - A callback function to be removed.
#
# Examples
#
# callback = (msg) -> console.log(msg)
# instance.subscribe('annotation:save', callback)
# instance.publish('annotation:save', ['Hello World'])
# # => Outputs "Hello World"
#
# instance.unsubscribe('annotation:save', callback)
# instance.publish('annotation:save', ['Hello Again'])
# # => No output.
#
# Returns itself.
unsubscribe: ->
@element.unbind.apply @element, arguments
this
# Parse the @events object of a Delegator into an array of objects containing
# string-valued "selector", "event", and "func" keys.
Delegator._parseEvents = (eventsObj) ->
events = []
for sel, functionName of eventsObj
[selector..., event] = sel.split ' '
events.push({
selector: selector.join(' '),
event: event,
functionName: functionName
})
return events
# Native jQuery events that should recieve an event object. Plugins can
# add their own methods to this if required.
Delegator.natives = do ->
specials = (key for own key, val of jQuery.event.special)
"""
blur focus focusin focusout load resize scroll unload click dblclick
mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave
change select submit keydown keypress keyup error
""".split(/[^a-z]+/).concat(specials)
# Checks to see if the provided event is a DOM event supported by jQuery or
# a custom user event.
#
# event - String event name.
#
# Examples
#
# Delegator._isCustomEvent('click') # => false
# Delegator._isCustomEvent('mousedown') # => false
# Delegator._isCustomEvent('annotation:created') # => true
#
# Returns true if event is a custom user event.
Delegator._isCustomEvent = (event) ->
[event] = event.split('.')
$.inArray(event, Delegator.natives) == -1
# Stub the console when not available so that everything still works.
functions = [
"log", "debug", "info", "warn", "exception", "assert", "dir", "dirxml",
"trace", "group", "groupEnd", "groupCollapsed", "time", "timeEnd", "profile",
"profileEnd", "count", "clear", "table", "error", "notifyFirebug", "firebug",
"userObjects"
]
if console?
# Opera's console doesn't have a group function as of 2010-07-01
if not console.group?
console.group = (name) -> console.log "GROUP: ", name
# Webkit's developer console has yet to implement groupCollapsed as of 2010-07-01
if not console.groupCollapsed?
console.groupCollapsed = console.group
# Stub out any remaining functions
for fn in functions
if not console[fn]?
console[fn] = -> console.log _t("Not implemented:") + " console.#{name}"
else
this.console = {}
for fn in functions
this.console[fn] = ->
this.console['error'] = (args...) ->
alert("ERROR: #{args.join(', ')}")
this.console['warn'] = (args...) ->
alert("WARNING: #{args.join(', ')}")
# Public: Creates an element for editing annotations.
class Annotator.Editor extends Annotator.Widget
# Events to be bound to @element.
events:
"form submit": "submit"
".annotator-save click": "submit"
".annotator-cancel click": "hide"
".annotator-cancel mouseover": "onCancelButtonMouseover"
"textarea keydown": "processKeypress"
# Classes to toggle state.
classes:
hide: 'annotator-hide'
focus: 'annotator-focus'
# HTML template for @element.
html: """
<div class="annotator-outer annotator-editor">
<form class="annotator-widget">
<ul class="annotator-listing"></ul>
<div class="annotator-controls">
<a href="#cancel" class="annotator-cancel">""" + _t('Cancel') + """</a>
<a href="#save" class="annotator-save annotator-focus">""" + _t('Save') + """</a>
</div>
</form>
</div>
"""
options: {} # Configuration options
# Public: Creates an instance of the Editor object. This will create the
# @element from the @html string and set up all events.
#
# options - An Object literal containing options. There are currently no
# options implemented.
#
# Examples
#
# # Creates a new editor, adds a custom field and
# # loads an annotation for editing.
# editor = new Annotator.Editor
# editor.addField({
# label: 'My custom input field',
# type: 'textarea'
# load: someLoadCallback
# save: someSaveCallback
# })
# editor.load(annotation)
#
# Returns a new Editor instance.
constructor: (options) ->
super $(@html)[0], options
@fields = []
@annotation = {}
# Public: Displays the Editor and fires a "show" event.
# Can be used as an event callback and will call Event#preventDefault()
# on the supplied event.
#
# event - Event object provided if method is called by event
# listener (default:undefined)
#
# Examples
#
# # Displays the editor.
# editor.show()
#
# # Displays the editor on click (prevents default action).
# $('a.show-editor').bind('click', editor.show)
#
# Returns itself.
show: (event) =>
util.preventEventDefault event
@element.removeClass(@classes.hide)
@element.find('.annotator-save').addClass(@classes.focus)
# invert if necessary
this.checkOrientation()
# give main textarea focus
@element.find(":input:first").focus()
this.setupDraggables()
this.publish('show')
# Public: Hides the Editor and fires a "hide" event. Can be used as an event
# callback and will call Event#preventDefault() on the supplied event.
#
# event - Event object provided if method is called by event
# listener (default:undefined)
#
# Examples
#
# # Hides the editor.
# editor.hide()
#
# # Hide the editor on click (prevents default action).
# $('a.hide-editor').bind('click', editor.hide)
#
# Returns itself.
hide: (event) =>
util.preventEventDefault event
@element.addClass(@classes.hide)
this.publish('hide')
# Public: Loads an annotation into the Editor and displays it setting
# Editor#annotation to the provided annotation. It fires the "load" event
# providing the current annotation subscribers can modify the annotation
# before it updates the editor fields.
#
# annotation - An annotation Object to display for editing.
#
# Examples
#
# # Diplays the editor with the annotation loaded.
# editor.load({text: 'My Annotation'})
#
# editor.on('load', (annotation) ->
# console.log annotation.text
# ).load({text: 'My Annotation'})
# # => Outputs "My Annotation"
#
# Returns itself.
load: (annotation) =>
@annotation = annotation
this.publish('load', [@annotation])
for field in @fields
field.load(field.element, @annotation)
this.show()
# Public: Hides the Editor and passes the annotation to all registered fields
# so they can update its state. It then fires the "save" event so that other
# parties can further modify the annotation.
# Can be used as an event callback and will call Event#preventDefault() on the
# supplied event.
#
# event - Event object provided if method is called by event
# listener (default:undefined)
#
# Examples
#
# # Submits the editor.
# editor.submit()
#
# # Submits the editor on click (prevents default action).
# $('button.submit-editor').bind('click', editor.submit)
#
# # Appends "Comment: " to the annotation comment text.
# editor.on('save', (annotation) ->
# annotation.text = "Comment: " + annotation.text
# ).submit()
#
# Returns itself.
submit: (event) =>
util.preventEventDefault event
for field in @fields
field.submit(field.element, @annotation)
this.publish('save', [@annotation])
this.hide()
# Public: Adds an addional form field to the editor. Callbacks can be provided
# to update the view and anotations on load and submission.
#
# options - An options Object. Options are as follows:
# id - A unique id for the form element will also be set as the
# "for" attrubute of a label if there is one. Defaults to
# a timestamp. (default: "annotator-field-{timestamp}")
# type - Input type String. One of "input", "textarea",
# "checkbox", "select" (default: "input")
# label - Label to display either in a label Element or as place-
# holder text depending on the type. (default: "")
# load - Callback Function called when the editor is loaded with a
# new annotation. Recieves the field <li> element and the
# annotation to be loaded.
# submit - Callback Function called when the editor is submitted.
# Recieves the field <li> element and the annotation to be
# updated.
#
# Examples
#
# # Add a new input element.
# editor.addField({
# label: "Tags",
#
# # This is called when the editor is loaded use it to update your input.
# load: (field, annotation) ->
# # Do something with the annotation.
# value = getTagString(annotation.tags)
# $(field).find('input').val(value)
#
# # This is called when the editor is submitted use it to retrieve data
# # from your input and save it to the annotation.
# submit: (field, annotation) ->
# value = $(field).find('input').val()
# annotation.tags = getTagsFromString(value)
# })
#
# # Add a new checkbox element.
# editor.addField({
# type: 'checkbox',
# id: 'annotator-field-my-checkbox',
# label: 'Allow anyone to see this annotation',
# load: (field, annotation) ->
# # Check what state of input should be.
# if checked
# $(field).find('input').attr('checked', 'checked')
# else
# $(field).find('input').removeAttr('checked')
# submit: (field, annotation) ->
# checked = $(field).find('input').is(':checked')
# # Do something.
# })
#
# Returns the created <li> Element.
addField: (options) ->
field = $.extend({
id: 'annotator-field-' + util.uuid()
type: 'input'
label: ''
load: ->
submit: ->
}, options)
input = null
element = $('<li class="annotator-item" />')
field.element = element[0]
switch (field.type)
when 'textarea' then input = $('<textarea />')
when 'input', 'checkbox' then input = $('<input />')
when 'select' then input = $('<select />')
element.append(input);
input.attr({
id: field.id
placeholder: field.label
})
if field.type == 'checkbox'
input[0].type = 'checkbox'
element.addClass('annotator-checkbox')
element.append($('<label />', {for: field.id, html: field.label}))
@element.find('ul:first').append(element)
@fields.push field
field.element
checkOrientation: ->
super
list = @element.find('ul')
controls = @element.find('.annotator-controls')
if @element.hasClass(@classes.invert.y)
controls.insertBefore(list)
else if controls.is(':first-child')
controls.insertAfter(list)
this
# Event callback. Listens for the following special keypresses.
# - escape: Hides the editor
# - enter: Submits the editor
#
# event - A keydown Event object.
#
# Returns nothing
processKeypress: (event) =>
if event.keyCode is 27 # "Escape" key => abort.
this.hide()
else if event.keyCode is 13 and !event.shiftKey
# If "return" was pressed without the shift key, we're done.
this.submit()
# Event callback. Removes the focus class from the submit button when the
# cancel button is hovered.
#
# Returns nothing
onCancelButtonMouseover: =>
@element.find('.' + @classes.focus).removeClass(@classes.focus);
# Sets up mouse events for resizing and dragging the editor window.
# window events are bound only when needed and throttled to only update
# the positions at most 60 times a second.
#
# Returns nothing.
setupDraggables: () ->
@element.find('.annotator-resize').remove()
# Find the first/last item element depending on orientation
if @element.hasClass(@classes.invert.y)
cornerItem = @element.find('.annotator-item:last')
else
cornerItem = @element.find('.annotator-item:first')
if cornerItem
$('<span class="annotator-resize"></span>').appendTo(cornerItem)
mousedown = null
classes = @classes
editor = @element
textarea = null
resize = editor.find('.annotator-resize')
controls = editor.find('.annotator-controls')
throttle = false
onMousedown = (event) ->
if event.target == this
mousedown = {
element: this
top: event.pageY
left: event.pageX
}
# Find the first text area if there is one.
textarea = editor.find('textarea:first')
$(window).bind({
'mouseup.annotator-editor-resize': onMouseup
'mousemove.annotator-editor-resize': onMousemove
})
event.preventDefault();
onMouseup = ->
mousedown = null;
$(window).unbind '.annotator-editor-resize'
onMousemove = (event) =>
if mousedown and throttle == false
diff = {
top: event.pageY - mousedown.top
left: event.pageX - mousedown.left
}
if mousedown.element == resize[0]
height = textarea.outerHeight()
width = textarea.outerWidth()
directionX = if editor.hasClass(classes.invert.x) then -1 else 1
directionY = if editor.hasClass(classes.invert.y) then 1 else -1
textarea.height height + (diff.top * directionY)
textarea.width width + (diff.left * directionX)
# Only update the mousedown object if the dimensions
# have changed, otherwise they have reached their minimum
# values.
mousedown.top = event.pageY unless textarea.outerHeight() == height
mousedown.left = event.pageX unless textarea.outerWidth() == width
else if mousedown.element == controls[0]
editor.css({
top: parseInt(editor.css('top'), 10) + diff.top
left: parseInt(editor.css('left'), 10) + diff.left
})
mousedown.top = event.pageY
mousedown.left = event.pageX
throttle = true;
setTimeout(->
throttle = false
, 1000/60);
resize.bind 'mousedown', onMousedown
controls.bind 'mousedown', onMousedown
# I18N
gettext = null
if Gettext?
_gettext = new Gettext(domain: "annotator")
gettext = (msgid) -> _gettext.gettext(msgid)
else
gettext = (msgid) -> msgid
_t = (msgid) -> gettext(msgid)
unless jQuery?.fn?.jquery
console.error(_t("Annotator requires jQuery: have you included lib/vendor/jquery.js?"))
unless JSON and JSON.parse and JSON.stringify
console.error(_t("Annotator requires a JSON implementation: have you included lib/vendor/json2.js?"))
$ = jQuery
Util = {}
# Public: Flatten a nested array structure
#
# Returns an array
Util.flatten = (array) ->
flatten = (ary) ->
flat = []
for el in ary
flat = flat.concat(if el and $.isArray(el) then flatten(el) else el)
return flat
flatten(array)
# Public: Finds all text nodes within the elements in the current collection.
#
# Returns a new jQuery collection of text nodes.
Util.getTextNodes = (jq) ->
getTextNodes = (node) ->
if node and node.nodeType != Node.TEXT_NODE
nodes = []
# If not a comment then traverse children collecting text nodes.
# We traverse the child nodes manually rather than using the .childNodes
# property because IE9 does not update the .childNodes property after
# .splitText() is called on a child text node.
if node.nodeType != Node.COMMENT_NODE
# Start at the last child and walk backwards through siblings.
node = node.lastChild
while node
nodes.push getTextNodes(node)
node = node.previousSibling
# Finally reverse the array so that nodes are in the correct order.
return nodes.reverse()
else
return node
jq.map -> Util.flatten(getTextNodes(this))
# Public: determine the last text node inside or before the given node
Util.getLastTextNodeUpTo = (n) ->
switch n.nodeType
when Node.TEXT_NODE
return n # We have found our text node.
when Node.ELEMENT_NODE
# This is an element, we need to dig in
if n.lastChild? # Does it have children at all?
result = Util.getLastTextNodeUpTo n.lastChild
if result? then return result
else
# Not a text node, and not an element node.
# Could not find a text node in current node, go backwards
n = n.previousSibling
if n?
Util.getLastTextNodeUpTo n
else
null
# Public: determine the first text node in or after the given jQuery node.
Util.getFirstTextNodeNotBefore = (n) ->
switch n.nodeType
when Node.TEXT_NODE
return n # We have found our text node.
when Node.ELEMENT_NODE
# This is an element, we need to dig in
if n.firstChild? # Does it have children at all?
result = Util.getFirstTextNodeNotBefore n.firstChild
if result? then return result
else
# Not a text or an element node.
# Could not find a text node in current node, go forward
n = n.nextSibling
if n?
Util.getFirstTextNodeNotBefore n
else
null
Util.xpathFromNode = (el, relativeRoot) ->
try
result = simpleXPathJQuery.call el, relativeRoot
catch exception
console.log "jQuery-based XPath construction failed! Falling back to manual."
result = simpleXPathPure.call el, relativeRoot
result
Util.nodeFromXPath = (xp, root) ->
steps = xp.substring(1).split("/")
node = root
for step in steps
[name, idx] = step.split "["
idx = if idx? then parseInt (idx?.split "]")[0] else 1
node = findChild node, name.toLowerCase(), idx
node
Util.escape = (html) ->
html
.replace(/&(?!\w+;)/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
# Abstract highlight class
class Highlight
constructor: (@anchor, @pageIndex) ->
@annotator = @anchor.annotator
@annotation = @anchor.annotation
# Mark/unmark this hl as temporary (while creating an annotation)
setTemporary: (value) ->
throw "Operation not implemented."
# Is this a temporary hl?
isTemporary: ->
throw "Operation not implemented."
# TODO: review the usage of the batch parameters.
# Mark/unmark this hl as focused
#
# Value specifies whether it should be focused or not
#
# The 'batch' field specifies whether this call is only one of
# many subsequent calls, which should be executed together.
#
# In this case, a "finalizeHighlights" event will be published
# when all the flags have been set, and the changes should be
# executed.
setFocused: (value, batch = false) ->
throw "Operation not implemented."
# React to changes in the underlying annotation
annotationUpdated: ->
#console.log "In HL", this, "annotation has been updated."
# Remove all traces of this hl from the document
removeFromDocument: ->
throw "Operation not implemented."
# Get the HTML elements making up the highlight
# If you implement this, you get automatic implementation for the functions
# below. However, if you need a more sophisticated control mechanism,
# you are free to leave this unimplemented, and manually implement the
# rest.
_getDOMElements: ->
throw "Operation not implemented."
# Get the Y offset of the highlight. Override for more control
getTop: -> $(@_getDOMElements()).offset().top
# Get the height of the highlight. Override for more control
getHeight: -> $(@_getDOMElements()).outerHeight true
# Get the bottom Y offset of the highlight. Override for more control.
getBottom: -> @getTop() + @getBottom()
# Scroll the highlight into view. Override for more control
scrollTo: -> $(@_getDOMElements()).scrollintoview()
# Scroll the highlight into view, with a comfortable margin.
# up should be true if we need to scroll up; false otherwise
paddedScrollTo: (direction) ->
unless direction? then throw "Direction is required"
dir = if direction is "up" then -1 else +1
where = $(@_getDOMElements())
wrapper = @annotator.wrapper
defaultView = wrapper[0].ownerDocument.defaultView
pad = defaultView.innerHeight * .2
where.scrollintoview
complete: ->
scrollable = if this.parentNode is this.ownerDocument
$(this.ownerDocument.body)
else
$(this)
top = scrollable.scrollTop()
correction = pad * dir
scrollable.stop().animate {scrollTop: top + correction}, 300
# Scroll up to the highlight, with a comfortable margin.
paddedScrollUpTo: -> @paddedScrollTo "up"
# Scroll down to the highlight, with a comfortable margin.
paddedScrollDownTo: -> @paddedScrollTo "down"
Annotator = Annotator || {}
# Public: A simple notification system that can be used to display information,
# warnings and errors to the user. Display of notifications are controlled
# cmpletely by CSS by adding/removing the @options.classes.show class. This
# allows styling/animation using CSS rather than hardcoding styles.
class Annotator.Notification extends Delegator
# Sets events to be bound to the @element.
events:
"click": "hide"
# Default options.
options:
html: "<div class='annotator-notice'></div>"
classes:
show: "annotator-notice-show"
info: "annotator-notice-info"
success: "annotator-notice-success"
error: "annotator-notice-error"
# Public: Creates an instance of Notification and appends it to the
# document body.
#
# options - The following options can be provided.
# classes - A Object literal of classes used to determine state.
# html - An HTML string used to create the notification.
#
# Examples
#
# # Displays a notification with the text "Hello World"
# notification = new Annotator.Notification
# notification.show("Hello World")
#
# Returns
constructor: (options) ->
super $(@options.html).appendTo(document.body)[0], options
# Public: Displays the annotation with message and optional status. The
# message will hide itself after 5 seconds or if the user clicks on it.
#
# message - A message String to display (HTML will be escaped).
# status - A status constant. This will apply a class to the element for
# styling. (default: Annotator.Notification.INFO)
#
# Examples
#
# # Displays a notification with the text "Hello World"
# notification.show("Hello World")
#
# # Displays a notification with the text "An error has occurred"
# notification.show("An error has occurred", Annotator.Notification.ERROR)
#
# Returns itself.
show: (message, status=Annotator.Notification.INFO) =>
@currentStatus = status
$(@element)
.addClass(@options.classes.show)
.addClass(@options.classes[@currentStatus])
.html(Util.escape(message || ""))
setTimeout this.hide, 5000
this
# Public: Hides the notification.
#
# Examples
#
# # Hides the notification.
# notification.hide()
#
# Returns itself.
hide: =>
@currentStatus ?= Annotator.Notification.INFO
$(@element)
.removeClass(@options.classes.show)
.removeClass(@options.classes[@currentStatus])
this
# Constants for controlling the display of the notification. Each constant
# adds a different class to the Notification#element.
Annotator.Notification.INFO = 'show'
Annotator.Notification.SUCCESS = 'success'
Annotator.Notification.ERROR = 'error'
# Attach notification methods to the Annotation object on document ready.
$(->
notification = new Annotator.Notification
Annotator.showNotification = notification.show
Annotator.hideNotification = notification.hide
)
# Public: Creates a Date object from an ISO8601 formatted date String.
#
# string - ISO8601 formatted date String.
#
# Returns Date instance.
createDateFromISO8601 = (string) ->
regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
"(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" +
"(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"
d = string.match(new RegExp(regexp))
offset = 0
date = new Date(d[1], 0, 1)
date.setMonth(d[3] - 1) if d[3]
date.setDate(d[5]) if d[5]
date.setHours(d[7]) if d[7]
date.setMinutes(d[8]) if d[8]
date.setSeconds(d[10]) if d[10]
date.setMilliseconds(Number("0." + d[12]) * 1000) if d[12]
if d[14]
offset = (Number(d[16]) * 60) + Number(d[17])
offset *= ((d[15] == '-') ? 1 : -1)
offset -= date.getTimezoneOffset()
time = (Number(date) + (offset * 60 * 1000))
date.setTime(Number(time))
date
base64Decode = (data) ->
if atob?
# Gecko and Webkit provide native code for this
atob(data)
else
# Adapted from MIT/BSD licensed code at http://phpjs.org/functions/base64_decode
# version 1109.2015
b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
i = 0
ac = 0
dec = ""
tmp_arr = []
if not data
return data
data += ''
while i < data.length
# unpack four hexets into three octets using index points in b64
h1 = b64.indexOf(data.charAt(i++))
h2 = b64.indexOf(data.charAt(i++))
h3 = b64.indexOf(data.charAt(i++))
h4 = b64.indexOf(data.charAt(i++))
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4
o1 = bits >> 16 & 0xff
o2 = bits >> 8 & 0xff
o3 = bits & 0xff
if h3 == 64
tmp_arr[ac++] = String.fromCharCode(o1)
else if h4 == 64
tmp_arr[ac++] = String.fromCharCode(o1, o2)
else
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3)
tmp_arr.join('')
base64UrlDecode = (data) ->
m = data.length % 4
if m != 0
for i in [0...4 - m]
data += '='
data = data.replace(/-/g, '+')
data = data.replace(/_/g, '/')
base64Decode(data)
parseToken = (token) ->
[head, payload, sig] = token.split('.')
JSON.parse(base64UrlDecode(payload))
# Public: Supports the Store plugin by providing Authentication headers.
class Annotator.Plugin.Auth extends Annotator.Plugin
# User options that can be provided.
options:
# An authentication token. Used to skip the request to the server for a
# a token.
token: null
# The URL on the local server to request an authentication token.
tokenUrl: '/auth/token'
# If true will try and fetch a token when the plugin is initialised.
autoFetch: true
# Public: Create a new instance of the Auth plugin.
#
# element - The element to bind all events to. Usually the Annotator#element.
# options - An Object literal containing user options.
#
# Examples
#
# plugin = new Annotator.Plugin.Auth(annotator.element, {
# tokenUrl: '/my/custom/path'
# })
#
# Returns instance of Auth.
constructor: (element, options) ->
super
# List of functions to be executed when we have a valid token.
@waitingForToken = []
if @options.token
this.setToken(@options.token)
else
this.requestToken()
# Public: Makes a request to the local server for an authentication token.
#
# Examples
#
# auth.requestToken()
#
# Returns jqXHR object.
requestToken: ->
@requestInProgress = true
$.ajax
url: @options.tokenUrl
dataType: 'text'
xhrFields:
     withCredentials: true # Send any auth cookies to the backend
# on success, set the auth token
.done (data, status, xhr) =>
this.setToken(data)
# on failure, relay any message given by the server to the user with a notification
.fail (xhr, status, err) =>
msg = Annotator._t("Couldn't get auth token:")
console.error "#{msg} #{err}", xhr
Annotator.showNotification("#{msg} #{xhr.responseText}", Annotator.Notification.ERROR)
# always reset the requestInProgress indicator
.always =>
@requestInProgress = false
# Public: Sets the @token and checks it's validity. If the token is invalid
# requests a new one from the server.
#
# token - A token string.
#
# Examples
#
# auth.setToken('eyJh...9jQ3I')
#
# Returns nothing.
setToken: (token) ->
@token = token
# Parse the token without verifying its authenticity:
@_unsafeToken = parseToken(token)
if this.haveValidToken()
if @options.autoFetch
# Set timeout to fetch new token 2 seconds before current token expiry
@refreshTimeout = setTimeout (() => this.requestToken()), (this.timeToExpiry() - 2) * 1000
# Set headers field on this.element
this.updateHeaders()
# Run callbacks waiting for token
while @waitingForToken.length > 0
@waitingForToken.pop()(@_unsafeToken)
else
console.warn Annotator._t("Didn't get a valid token.")
if @options.autoFetch
console.warn Annotator._t("Getting a new token in 10s.")
setTimeout (() => this.requestToken()), 10 * 1000
# Public: Checks the validity of the current token. Note that this *does
# not* check the authenticity of the token.
#
# Examples
#
# auth.haveValidToken() # => Returns true if valid.
#
# Returns true if the token is valid.
haveValidToken: () ->
allFields = @_unsafeToken &&
@_unsafeToken.issuedAt &&
@_unsafeToken.ttl &&
@_unsafeToken.consumerKey
if allFields && this.timeToExpiry() > 0
return true
else
return false
# Public: Calculates the time in seconds until the current token expires.
#
# Returns Number of seconds until token expires.
timeToExpiry: ->
now = new Date().getTime() / 1000
issue = createDateFromISO8601(@_unsafeToken.issuedAt).getTime() / 1000
expiry = issue + @_unsafeToken.ttl
timeToExpiry = expiry - now
if (timeToExpiry > 0) then timeToExpiry else 0
# Public: Updates the headers to be sent with the Store requests. This is
# achieved by updating the 'annotator:headers' key in the @element.data()
# store.
#
# Returns nothing.
updateHeaders: ->
current = @element.data('annotator:headers')
@element.data('annotator:headers', $.extend(current, {
'x-annotator-auth-token': @token,
}))
# Runs the provided callback if a valid token is available. Otherwise requests
# a token until it recieves a valid one.
#
# callback - A callback function to call once a valid token is obtained.
#
# Examples
#
# auth.withToken ->
# store.loadAnnotations()
#
# Returns nothing.
withToken: (callback) ->
if not callback?
return
if this.haveValidToken()
callback(@_unsafeToken)
else
this.waitingForToken.push(callback)
if not @requestInProgress
this.requestToken()
class Annotator.Plugin.Document extends Annotator.Plugin
$ = Annotator.$
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
pluginInit: ->
this.getDocumentMetadata()
# returns the primary URI for the document being annotated
uri: =>
uri = decodeURIComponent document.location.href
for link in @metadata.link
if link.rel == "canonical"
uri = link.href
return uri
# returns all uris for the document being annotated
uris: =>
uniqueUrls = {}
for link in @metadata.link
uniqueUrls[link.href] = true if link.href
return (href for href of uniqueUrls)
beforeAnnotationCreated: (annotation) =>
annotation.document = @metadata
getDocumentMetadata: =>
@metadata = {}
# first look for some common metadata types
# TODO: look for microdata/rdfa?
this._getHighwire()
this._getDublinCore()
this._getFacebook()
this._getEprints()
this._getPrism()
this._getTwitter()
this._getFavicon()
this._getDocOwners()
# extract out/normalize some things
this._getTitle()
this._getLinks()
return @metadata
_getHighwire: =>
return @metadata.highwire = this._getMetaTags("citation", "name", "_")
_getFacebook: =>
return @metadata.facebook = this._getMetaTags("og", "property", ":")
_getTwitter: =>
return @metadata.twitter = this._getMetaTags("twitter", "name", ":")
_getDublinCore: =>
return @metadata.dc = this._getMetaTags("dc", "name", ".")
_getPrism: =>
return @metadata.prism = this._getMetaTags("prism", "name", ".")
_getEprints: =>
return @metadata.eprints = this._getMetaTags("eprints", "name", ".")
_getMetaTags: (prefix, attribute, delimiter) =>
tags = {}
for meta in $("meta")
name = $(meta).attr(attribute)
content = $(meta).prop("content")
if name
match = name.match(RegExp("^#{prefix}#{delimiter}(.+)$", "i"))
if match
n = match[1]
if tags[n]
tags[n].push(content)
else
tags[n] = [content]
return tags
_getTitle: =>
if @metadata.highwire.title
@metadata.title = @metadata.highwire.title[0]
else if @metadata.eprints.title
@metadata.title = @metadata.eprints.title
else if @metadata.prism.title
@metadata.title = @metadata.prism.title
else if @metadata.facebook.title
@metadata.title = @metadata.facebook.title
else if @metadata.twitter.title
@metadata.title = @metadata.twitter.title
else if @metadata.dc.title
@metadata.title = @metadata.dc.title
else
@metadata.title = $("head title").text()
_getLinks: =>
# we know our current location is a link for the document
@metadata.link = [href: document.location.href]
# look for some relevant link relations
for link in $("link")
l = $(link)
href = this._absoluteUrl(l.prop('href')) # get absolute url
rel = l.prop('rel')
type = l.prop('type')
relTypes = ["alternate", "canonical", "bookmark", "shortlink"]
dropTypes = ["application/rss+xml", "application/atom+xml"]
if rel in relTypes and type not in dropTypes
@metadata.link.push(href: href, rel: rel, type: type)
# look for links in scholar metadata
for name, values of @metadata.highwire
if name == "pdf_url"
for url in values
@metadata.link.push
href: this._absoluteUrl(url)
type: "application/pdf"
# kind of a hack to express DOI identifiers as links but it's a
# convenient place to look them up later, and somewhat sane since
# they don't have a type
if name == "doi"
for doi in values
if doi[0..3] != "doi:"
doi = "doi:" + doi
@metadata.link.push(href: doi)
# look for links in dublincore data
for name, values of @metadata.dc
if name == "identifier"
for id in values
if id[0..3] == "doi:"
@metadata.link.push(href: id)
_getFavicon: =>
for link in $("link")
if $(link).prop("rel") in ["shortcut icon", "icon"]
@metadata["favicon"] = this._absoluteUrl(link.href)
_getDocOwners: =>
@metadata.reply_to = []
for a in $("a")
if a.rel is 'reply-to'
if a.href.toLowerCase().slice(0,7) is "mailto:"
@metadata.reply_to.push a.href[7..]
else
@metadata.reply_to.push a.href
# hack to get a absolute url from a possibly relative one
_absoluteUrl: (url) ->
d = document.createElement('a')
d.href = url
d.href
# Annotator plugin providing dom-text-mapper
class Annotator.Plugin.DomTextMapper extends Annotator.Plugin
pluginInit: ->
if @options.skip
console.log "Not registering DOM-Text-Mapper."
return
@annotator.documentAccessStrategies.unshift
# Document access strategy for simple HTML documents,
# with enhanced text extraction and mapping features.
name: "DOM-Text-Mapper"
mapper: window.DomTextMapper
init: => @annotator.domMapper.setRootNode @annotator.wrapper[0]
# Annotator plugin for fuzzy text matching
class Annotator.Plugin.FuzzyTextAnchors extends Annotator.Plugin
pluginInit: ->
# Do we have the basic text anchors plugin loaded?
unless @annotator.plugins.TextAnchors
console.warn "The FuzzyTextAnchors Annotator plugin requires the TextAnchors plugin. Skipping."
return
@Annotator = Annotator
# Initialize the text matcher library
@textFinder = new DomTextMatcher => @annotator.domMapper.getCorpus()
# Register our fuzzy strategies
@annotator.anchoringStrategies.push
# Two-phased fuzzy text matching strategy. (Using context and quote.)
# This can handle document structure changes,
# and also content changes.
name: "two-phase fuzzy"
code: this.twoPhaseFuzzyMatching
@annotator.anchoringStrategies.push
# Naive fuzzy text matching strategy. (Using only the quote.)
# This can handle document structure changes,
# and also content changes.
name: "one-phase fuzzy"
code: this.fuzzyMatching
twoPhaseFuzzyMatching: (annotation, target) =>
# This won't work without DTM
return unless @annotator.domMapper.getInfoForNode?
# Fetch the quote and the context
quoteSelector = @annotator.findSelector target.selector, "TextQuoteSelector"
prefix = quoteSelector?.prefix
suffix = quoteSelector?.suffix
quote = quoteSelector?.exact
# No context, to joy
unless (prefix? and suffix?) then return null
# Fetch the expected start and end positions
posSelector = @annotator.findSelector target.selector, "TextPositionSelector"
expectedStart = posSelector?.start
expectedEnd = posSelector?.end
options =
contextMatchDistance: @annotator.domMapper.getCorpus().length * 2
contextMatchThreshold: 0.5
patternMatchThreshold: 0.5
flexContext: true
result = @textFinder.searchFuzzyWithContext prefix, suffix, quote,
expectedStart, expectedEnd, false, options
# If we did not got a result, give up
unless result.matches.length
# console.log "Fuzzy matching did not return any results. Giving up on two-phase strategy."
return null
# here is our result
match = result.matches[0]
# console.log "2-phase fuzzy found match at: [" + match.start + ":" +
# match.end + "]: '" + match.found + "' (exact: " + match.exact + ")"
# OK, we have everything
# Create a TextPositionAnchor from this data
new @Annotator.TextPositionAnchor @annotator, annotation, target,
match.start, match.end,
(@annotator.domMapper.getPageIndexForPos match.start),
(@annotator.domMapper.getPageIndexForPos match.end),
match.found,
unless match.exact then match.comparison.diffHTML,
unless match.exact then match.exactExceptCase
fuzzyMatching: (annotation, target) =>
# This won't work without DTM
return unless @annotator.domMapper.getInfoForNode?
# Fetch the quote
quoteSelector = @annotator.findSelector target.selector, "TextQuoteSelector"
quote = quoteSelector?.exact
# No quote, no joy
unless quote? then return null
# For too short quotes, this strategy is bound to return false positives.
# See https://github.com/hypothesis/h/issues/853 for details.
return unless quote.length >= 32
# Get a starting position for the search
posSelector = @annotator.findSelector target.selector, "TextPositionSelector"
expectedStart = posSelector?.start
# Get full document length
len = @annotator.domMapper.getCorpus().length
# If we don't have the position saved, start at the middle of the doc
expectedStart ?= Math.floor(len / 2)
# Do the fuzzy search
options =
matchDistance: len * 2
withFuzzyComparison: true
result = @textFinder.searchFuzzy quote, expectedStart, false, options
# If we did not got a result, give up
unless result.matches.length
# console.log "Fuzzy matching did not return any results. Giving up on one-phase strategy."
return null
# here is our result
match = result.matches[0]
# console.log "1-phase fuzzy found match at: [" + match.start + ":" +
# match.end + "]: '" + match.found + "' (exact: " + match.exact + ")"
# OK, we have everything
# Create a TextPosutionAnchor from this data
new @Annotator.TextPositionAnchor @annotator, annotation, target,
match.start, match.end,
(@annotator.domMapper.getPageIndexForPos match.start),
(@annotator.domMapper.getPageIndexForPos match.end),
match.found,
unless match.exact then match.comparison.diffHTML,
unless match.exact then match.exactExceptCase
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
# 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
@_tryExtractPage = (index) => @_viewer.getPageTextContent(index)
else
@_app = @_viewer = PDFView
@_finder = @_app.findController ? # PDF.js v1.0.712
PDFFindController # up to PDF.js v1.0.437
@_tryExtractPage = (index) =>
new Promise (resolve, reject) =>
tryIt = =>
page = @_finder.pdfPageSource.pages[index]
if page?.pdfPage?
page.getTextContent().then(resolve)
else
setTimeout tryIt, 100
tryIt()
@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) =>
# If we have not yet finished the initial scanning, then we are
# not interested.
return unless @pageInfo?
index = evt.detail.pageNumber - 1
@_onPageRendered index
# 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
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 =>
# Wait for the document to load
@_app.pdfDocument.getPage(1).then =>
@pageInfo = []
@_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) ->
@_tryExtractPage(pageIndex).then (data) =>
# There is some variation about what I might find here,
# depending on PDF.js version, so we need to do some guesswork.
textData = data.bidiTexts ? data.items ? data
# First, join all the pieces from the bidiTexts
rawContent = (text.str for text in textData).join " "
# Do some post-processing
content = @_parseExtractedText rawContent
# Save the extracted content to our page information registery
@pageInfo[pageIndex] =
index: pageIndex
content: content
if pageIndex is @getPageCount() - 1
@_finishScan()
else
@_extractPageText pageIndex + 1
# This is called when scanning is finished
_finishScan: =>
# Do some besic calculations with the content
@_onHavePageContents()
# OK, we are ready to rock.
@_pendingScanResolve()
# Do whatever we need to do after scanning
@_onAfterScan()
# 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
# 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
$ = Annotator.$
pluginInit: ->
# We need dom-text-mapper
unless @annotator.plugins.DomTextMapper
console.warn "The PDF Annotator plugin requires the DomTextMapper plugin. Skipping."
return
@annotator.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 = @annotator.domMapper.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 = @annotator.domMapper.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 @annotator.domMapper.waitForInit?
@annotator.domMapper.waitForInit().then =>
try
resolve @_metadata()
catch error
reject "Internal error"
else
reject "Not a PDF dom mapper."
# We want to react to some events
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
# This is what we do to new annotations
beforeAnnotationCreated: (annotation) =>
return unless @_isPDF()
annotation.document = @_metadata()
# This plugin implements the UI code for creating text annotations
class Annotator.Plugin.TextAnchors extends Annotator.Plugin
# Plugin initialization
pluginInit: ->
# We need text highlights
unless @annotator.plugins.TextHighlights
throw new Error "The TextAnchors Annotator plugin requires the TextHighlights plugin."
@Annotator = Annotator
@$ = Annotator.$
# Register the event handlers required for creating a selection
$(document).bind({
"mouseup": @checkForEndSelection
})
null
# 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 then the mouse is released.
# Checks to see if a selection has been made on mouseup and if so,
# calls Annotator's onSuccessfulSelection method.
# Also resets the @mouseIsDown property.
#
# 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.
#
# Returns nothing.
checkForEndSelection: (event = {}) =>
@annotator.mouseIsDown = false
# We don't care about the adder button click
return if @annotator.inAdderClick
# Get the currently selected ranges.
selectedRanges = @_getSelectedRanges()
for range in selectedRanges
container = range.commonAncestor
# TODO: what is selection ends inside a different type of highlight?
if @Annotator.TextHighlight.isInstance container
container = @Annotator.TextHighlight.getIndependentParent container
return if @annotator.isAnnotator(container)
if selectedRanges.length
event.segments = []
for r in selectedRanges
event.segments.push
type: "text range"
range: r
# Do we have valid page coordinates inside the event
# which has triggered this function?
unless event.pageX
# No, we don't. Adding fake coordinates
pos = selectedRanges[0].getEndCoords()
event.pageX = pos.x
event.pageY = pos.y #- window.scrollY
@annotator.onSuccessfulSelection event
else
@annotator.onFailedSelection event
# Strategies used for creating anchors from saved data
# This plugin containts the text highlight implementation,
# required for annotating text.
class TextHighlight extends Annotator.Highlight
# XXX: This is a temporay workaround until the Highlighter extension
# PR will be merged which will restore separation properly
@highlightClass = 'annotator-hl'
# Save the Annotator class reference, while we have access to it.
# TODO: Is this really the way to go? How do other plugins do it?
@Annotator = Annotator
@$ = Annotator.$
@highlightType = 'TextHighlight'
# Is this element a text highlight physical anchor ?
@isInstance: (element) -> @$(element).hasClass 'annotator-hl'
# Find the first parent outside this physical anchor
@getIndependentParent: (element) ->
@$(element).parents(':not([class^=annotator-hl])')[0]
# List of annotators we have already set up events for
@_inited: []
# Collect the annotations impacted by an event
@getAnnotations: (event) ->
TextHighlight.$(event.target)
.parents('.annotator-hl')
.andSelf()
.map( -> TextHighlight.$(this).data("annotation"))
.toArray()
# Set up events for this annotator
@_init: (annotator) ->
return if annotator in @_inited
annotator.element.delegate ".annotator-hl", "mouseover", this,
(event) => annotator.onAnchorMouseover event
annotator.element.delegate ".annotator-hl", "mouseout", this,
(event) => annotator.onAnchorMouseout event
annotator.element.delegate ".annotator-hl", "mousedown", this,
(event) => annotator.onAnchorMousedown event
annotator.element.delegate ".annotator-hl", "click", this,
(event) => annotator.onAnchorClick event
@_inited.push 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.
_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()
for node in nodes
event = document.createEvent "UIEvents"
event.initUIEvent "domChange", true, false, window, 0
event.reason = "created hilite"
node.dispatchEvent event
r
constructor: (anchor, pageIndex, normedRange) ->
super anchor, pageIndex
TextHighlight._init @annotator
@$ = TextHighlight.$
@Annotator = TextHighlight.Annotator
# Create a highlights, and link them with the annotation
@_highlights = @_highlightRange normedRange
@$(@_highlights).data "annotation", @annotation
# Implementing the required APIs
# Is this a temporary hl?
isTemporary: -> @_temporary
# Mark/unmark this hl as active
setTemporary: (value) ->
@_temporary = value
if value
@$(@_highlights).addClass('annotator-hl-temporary')
else
@$(@_highlights).removeClass('annotator-hl-temporary')
# Mark/unmark this hl as focused
setFocused: (value) ->
if value
@$(@_highlights).addClass('annotator-hl-focused')
else
@$(@_highlights).removeClass('annotator-hl-focused')
# Remove all traces of this hl from the document
removeFromDocument: ->
for hl in @_highlights
# Is this highlight actually the part of the document?
if hl.parentNode? and @annotator.domMapper.isPageMapped @pageIndex
# We should restore original state
child = hl.childNodes[0]
@$(hl).replaceWith hl.childNodes
event = document.createEvent "UIEvents"
event.initUIEvent "domChange", true, false, window, 0
event.reason = "removed hilite (annotation deleted)"
child.parentNode.dispatchEvent event
# Get the HTML elements making up the highlight
_getDOMElements: -> @_highlights
class Annotator.Plugin.TextHighlights extends Annotator.Plugin
# Plugin initialization
pluginInit: ->
# Export the text highlight class for other plugins
Annotator.TextHighlight = TextHighlight
\ No newline at end of file
# This anchor type stores information about a piece of text,
# described using start and end character offsets
class TextPositionAnchor extends Annotator.Anchor
@Annotator = Annotator
constructor: (annotator, annotation, target,
@start, @end, startPage, endPage,
quote, diffHTML, diffCaseOnly) ->
super annotator, annotation, target,
startPage, endPage,
quote, diffHTML, diffCaseOnly
# This pair of offsets is the key information,
# upon which this anchor is based upon.
unless @start? then throw new Error "start is required!"
unless @end? then throw new Error "end is required!"
@Annotator = TextPositionAnchor.Annotator
# This is how we create a highlight out of this kind of anchor
_createHighlight: (page) ->
# First we create the range from the stored stard and end offsets
mappings = @annotator.domMapper.getMappingsForCharRange @start, @end, [page]
# Get the wanted range out of the response of DTM
realRange = mappings.sections[page].realRange
# Get a BrowserRange
browserRange = new @Annotator.Range.BrowserRange realRange
# Get a NormalizedRange
normedRange = browserRange.normalize @annotator.wrapper[0]
# Create the highligh
new @Annotator.TextHighlight this, page, normedRange
# Annotator plugin for text position-based anchoring
class Annotator.Plugin.TextPosition extends Annotator.Plugin
pluginInit: ->
@Annotator = Annotator
# Register the creator for text quote selectors
@annotator.selectorCreators.push
name: "TextPositionSelector"
describe: @_getTextPositionSelector
@annotator.anchoringStrategies.push
# Position-based strategy. (The quote is verified.)
# This can handle document structure changes,
# but not the content changes.
name: "position"
code: @createFromPositionSelector
# Export the anchor type
@Annotator.TextPositionAnchor = TextPositionAnchor
# Create a TextPositionSelector around a range
_getTextPositionSelector: (selection) =>
# We only care about "text range" selections.
return [] unless selection.type is "text range"
# We need dom-text-mapper - style functionality
return [] unless @annotator.domMapper.getStartPosForNode?
startOffset = @annotator.domMapper.getStartPosForNode selection.range.start
endOffset = @annotator.domMapper.getEndPosForNode selection.range.end
if startOffset? and endOffset?
[
type: "TextPositionSelector"
start: startOffset
end: endOffset
]
else
# It looks like we can't determine the start and end offsets.
# That means no valid TextPosition selector can be generated from this.
unless startOffset?
console.log "Warning: can't generate TextPosition selector, because",
selection.range.start,
"does not have a valid start position."
unless endOffset?
console.log "Warning: can't generate TextPosition selector, because",
selection.range.end,
"does not have a valid end position."
[ ]
# Create an anchor using the saved TextPositionSelector.
# The quote is verified.
createFromPositionSelector: (annotation, target) =>
# We need the TextPositionSelector
selector = @annotator.findSelector target.selector, "TextPositionSelector"
return unless selector?
unless selector.start?
console.log "Warning: 'start' field is missing from TextPositionSelector. Skipping."
return null
unless selector.end?
console.log "Warning: 'end' field is missing from TextPositionSelector. Skipping."
return null
corpus = @annotator.domMapper.getCorpus?()
# This won't work without d-t-m
return null unless corpus
content = corpus[selector.start ... selector.end].trim()
currentQuote = @annotator.normalizeString content
savedQuote = @annotator.getQuoteForTarget? target
if savedQuote? and currentQuote isnt savedQuote
# We have a saved quote, let's compare it to current content
#console.log "Could not apply position selector" +
# " [#{selector.start}:#{selector.end}] to current document," +
# " because the quote has changed. " +
# "(Saved quote is '#{savedQuote}'." +
# " Current quote is '#{currentQuote}'.)"
return null
# Create a TextPositionAnchor from this data
new TextPositionAnchor @annotator, annotation, target,
selector.start, selector.end,
(@annotator.domMapper.getPageIndexForPos selector.start),
(@annotator.domMapper.getPageIndexForPos selector.end),
currentQuote
# This plugin defines the TextQuote selector
class Annotator.Plugin.TextQuote extends Annotator.Plugin
@Annotator = Annotator
@$ = Annotator.$
# Plugin initialization
pluginInit: ->
# Register the creator for text quote selectors
@annotator.selectorCreators.push
name: "TextQuoteSelector"
describe: @_getTextQuoteSelector
# Register function to get quote from this selector
@annotator.getQuoteForTarget = (target) =>
selector = @annotator.findSelector target.selector, "TextQuoteSelector"
if selector?
@annotator.normalizeString selector.exact
else
null
# Create a TextQuoteSelector around a range
_getTextQuoteSelector: (selection) =>
return [] unless selection.type is "text range"
unless selection.range?
throw new Error "Called getTextQuoteSelector() with null range!"
rangeStart = selection.range.start
unless rangeStart?
throw new Error "Called getTextQuoteSelector() on a range with no valid start."
rangeEnd = selection.range.end
unless rangeEnd?
throw new Error "Called getTextQuoteSelector() on a range with no valid end."
if @annotator.domMapper.getStartPosForNode?
# Calculate the quote and context using DTM
startOffset = @annotator.domMapper.getStartPosForNode rangeStart
endOffset = @annotator.domMapper.getEndPosForNode rangeEnd
if startOffset? and endOffset?
quote = @annotator.domMapper.getCorpus()[startOffset .. endOffset-1].trim()
[prefix, suffix] = @annotator.domMapper.getContextForCharRange startOffset, endOffset
[
type: "TextQuoteSelector"
exact: quote
prefix: prefix
suffix: suffix
]
else
# It looks like we can't determine the start and end offsets.
# That means no valid TextQuote selector can be generated from this.
console.log "Warning: can't generate TextQuote selector.", startOffset, endOffset
[ ]
else
# Get the quote directly from the range
[
type: "TextQuoteSelector"
exact: selection.range.text().trim()
]
# This anhor type stores information about a piece of text,
# described using the actual reference to the range in the DOM.
#
# When creating this kind of anchor, you are supposed to pass
# in a NormalizedRange object, which should cover exactly
# the wanted piece of text; no character offset correction is supported.
#
# Also, please note that these anchors can not really be virtualized,
# because they don't have any truly DOM-independent information;
# the core information stored is the reference to an object which
# lives in the DOM. Therefore, no lazy loading is possible with
# this kind of anchor. For that, use TextPositionAnchor instead.
#
# This plugin also adds a strategy to reanchor based on range selectors.
# If the TextQuote plugin is also loaded, then it will also check
# the saved quote against what is available now.
#
# If the TextPosition plugin is loaded, it will create a TextPosition
# anchor; otherwise it will record a TextRangeAnchor.
class TextRangeAnchor extends Annotator.Anchor
@Annotator = Annotator
constructor: (annotator, annotation, target, @range, quote) ->
super annotator, annotation, target, 0, 0, quote
unless @range? then throw new Error "range is required!"
@Annotator = TextRangeAnchor.Annotator
# This is how we create a highlight out of this kind of anchor
_createHighlight: ->
# Create the highligh
new @Annotator.TextHighlight this, 0, @range
# Annotator plugin for creating, and anchoring based on text range
# selectors
class Annotator.Plugin.TextRange extends Annotator.Plugin
pluginInit: ->
@Annotator = Annotator
# Register the creator for range selectors
@annotator.selectorCreators.push
name: "RangeSelector"
describe: @_getRangeSelector
# Register our anchoring strategies
@annotator.anchoringStrategies.push
# Simple strategy based on DOM Range
name: "range"
code: @createFromRangeSelector
# Export these anchor types
@annotator.TextRangeAnchor = TextRangeAnchor
# Create a RangeSelector around a range
_getRangeSelector: (selection) =>
return [] unless selection.type is "text range"
sr = selection.range.serialize @annotator.wrapper[0], '.' + @Annotator.TextHighlight.highlightClass
[
type: "RangeSelector"
startContainer: sr.startContainer
startOffset: sr.startOffset
endContainer: sr.endContainer
endOffset: sr.endOffset
]
# Create and anchor using the saved Range selector.
# The quote is verified.
createFromRangeSelector: (annotation, target) =>
selector = @annotator.findSelector target.selector, "RangeSelector"
unless selector? then return null
# Try to apply the saved XPath
try
range = @Annotator.Range.sniff selector
normedRange = range.normalize @annotator.wrapper[0]
catch error
return null
# Get the text of this range
if @annotator.domMapper.getInfoForNode?
# Determine the current content of the given range using DTM
startInfo = @annotator.domMapper.getInfoForNode normedRange.start
return null unless startInfo # Don't fret if page is not mapped
startOffset = startInfo.start
endInfo = @annotator.domMapper.getInfoForNode normedRange.end
return null unless endInfo # Don't fret if page is not mapped
endOffset = endInfo.end
rawQuote = @annotator.domMapper.getCorpus()[startOffset .. endOffset-1].trim()
else
# Determine the current content of the given range directly
rawQuote = normedRange.text().trim()
currentQuote = @annotator.normalizeString rawQuote
# Look up the saved quote
savedQuote = @annotator.getQuoteForTarget? target
if savedQuote? and currentQuote isnt savedQuote
#console.log "Could not apply XPath selector to current document, " +
# "because the quote has changed. (Saved quote is '#{savedQuote}'." +
# " Current quote is '#{currentQuote}'.)"
return null
if startInfo?.start? and endInfo?.end?
# Create a TextPositionAnchor from the start and end offsets
# of this range
# (to be used with dom-text-mapper)
new @Annotator.TextPositionAnchor @annotator, annotation, target,
startInfo.start, endInfo.end,
(startInfo.pageIndex ? 0), (endInfo.pageIndex ? 0),
currentQuote
else
# Create a TextRangeAnchor from this range
# (to be used whithout dom-text-mapper)
new TextRangeAnchor @annotator, annotation, target,
normedRange, currentQuote
Range = {}
# Public: Determines the type of Range of the provided object and returns
# a suitable Range instance.
#
# r - A range Object.
#
# Examples
#
# selection = window.getSelection()
# Range.sniff(selection.getRangeAt(0))
# # => Returns a BrowserRange instance.
#
# Returns a Range object or false.
Range.sniff = (r) ->
if r.commonAncestorContainer?
new Range.BrowserRange(r)
else if typeof r.start is "string"
# Annotator <= 1.2.6 upgrade code
new Range.SerializedRange
startContainer: r.start
startOffset: r.startOffset
endContainer: r.end
endOffset: r.endOffset
else if typeof r.startContainer is "string"
new Range.SerializedRange(r)
else if r.start and typeof r.start is "object"
new Range.NormalizedRange(r)
else
console.error(_t("Could not sniff range type"))
false
# Public: Finds an Element Node using an XPath relative to the document root.
#
# If the document is served as application/xhtml+xml it will try and resolve
# any namespaces within the XPath.
#
# xpath - An XPath String to query.
#
# Examples
#
# node = Range.nodeFromXPath('/html/body/div/p[2]')
# if node
# # Do something with the node.
#
# Returns the Node if found otherwise null.
Range.nodeFromXPath = (xpath, root=document) ->
evaluateXPath = (xp, nsResolver=null) ->
try
document.evaluate('.' + xp, root, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
catch exception
# There are cases when the evaluation fails, because the
# HTML documents contains nodes with invalid names,
# for example tags with equal signs in them, or something like that.
# In these cases, the XPath expressions will have these abominations,
# too, and then they can not be evaluated.
# In these cases, we get an XPathException, with error code 52.
# See http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathException
# This does not necessarily make any sense, but this what we see
# happening.
console.log "XPath evaluation failed."
console.log "Trying fallback..."
# We have a an 'evaluator' for the really simple expressions that
# should work for the simple expressions we generate.
Util.nodeFromXPath(xp, root)
if not $.isXMLDoc document.documentElement
evaluateXPath xpath
else
# We're in an XML document, create a namespace resolver function to try
# and resolve any namespaces in the current document.
# https://developer.mozilla.org/en/DOM/document.createNSResolver
customResolver = document.createNSResolver(
if document.ownerDocument == null
document.documentElement
else
document.ownerDocument.documentElement
)
node = evaluateXPath xpath, customResolver
unless node
# If the previous search failed to find a node then we must try to
# provide a custom namespace resolver to take into account the default
# namespace. We also prefix all node names with a custom xhtml namespace
# eg. 'div' => 'xhtml:div'.
xpath = (for segment in xpath.split '/'
if segment and segment.indexOf(':') == -1
segment.replace(/^([a-z]+)/, 'xhtml:$1')
else segment
).join('/')
# Find the default document namespace.
namespace = document.lookupNamespaceURI null
# Try and resolve the namespace, first seeing if it is an xhtml node
# otherwise check the head attributes.
customResolver = (ns) ->
if ns == 'xhtml' then namespace
else document.documentElement.getAttribute('xmlns:' + ns)
node = evaluateXPath xpath, customResolver
node
class Range.RangeError extends Error
constructor: (@type, @message, @parent=null) ->
super(@message)
# Public: Creates a wrapper around a range object obtained from a DOMSelection.
class Range.BrowserRange
# Public: Creates an instance of BrowserRange.
#
# object - A range object obtained via DOMSelection#getRangeAt().
#
# Examples
#
# selection = window.getSelection()
# range = new Range.BrowserRange(selection.getRangeAt(0))
#
# Returns an instance of BrowserRange.
constructor: (obj) ->
@commonAncestorContainer = obj.commonAncestorContainer
@startContainer = obj.startContainer
@startOffset = obj.startOffset
@endContainer = obj.endContainer
@endOffset = obj.endOffset
# Public: normalize works around the fact that browsers don't generate
# ranges/selections in a consistent manner. Some (Safari) will create
# ranges that have (say) a textNode startContainer and elementNode
# endContainer. Others (Firefox) seem to only ever generate
# textNode/textNode or elementNode/elementNode pairs.
#
# Returns an instance of Range.NormalizedRange
normalize: (root) ->
if @tainted
console.error(_t("You may only call normalize() once on a BrowserRange!"))
return false
else
@tainted = true
r = {}
# Look at the start
if @startContainer.nodeType is Node.ELEMENT_NODE
# We are dealing with element nodes
r.start = Util.getFirstTextNodeNotBefore @startContainer.childNodes[@startOffset]
r.startOffset = 0
else
# We are dealing with simple text nodes
r.start = @startContainer
r.startOffset = @startOffset
# Look at the end
if @endContainer.nodeType is Node.ELEMENT_NODE
# Get specified node.
node = @endContainer.childNodes[@endOffset]
if node? # Does that node exist?
# Look for a text node either at the immediate beginning of node
n = node
while n? and (n.nodeType isnt Node.TEXT_NODE)
n = n.firstChild
if n? # Did we find a text node at the start of this element?
# Check the previous sibling
prev = n.previousSibling
if prev? and (prev.nodeType is Node.TEXT_NODE)
# We have another text righ before us. Use that instead.
r.end = prev
r.endOffset = prev.nodeValue.length
else
# No, we need to stick to this node.
r.end = n
r.endOffset = 0
unless r.end?
# We need to find a text node in the previous sibling of the node at the
# given offset, if one exists, or in the previous sibling of its container.
if @endOffset
node = @endContainer.childNodes[@endOffset - 1]
else
node = @endContainer.previousSibling
r.end = Util.getLastTextNodeUpTo node
r.endOffset = r.end.nodeValue.length
else # We are dealing with simple text nodes
r.end = @endContainer
r.endOffset = @endOffset
# We have collected the initial data.
# Now let's start to slice & dice the text elements!
nr = {}
changed = false
if r.startOffset > 0
# Do we really have to cut?
if r.start.nodeValue.length > r.startOffset
# Yes. Cut.
nr.start = r.start.splitText(r.startOffset)
changed = true
else
# Avoid splitting off zero-length pieces.
nr.start = r.start.nextSibling
else
nr.start = r.start
# is the whole selection inside one text element ?
if r.start is r.end
if nr.start.nodeValue.length > (r.endOffset - r.startOffset)
nr.start.splitText(r.endOffset - r.startOffset)
changed = true
nr.end = nr.start
else # no, the end of the selection is in a separate text element
# does the end need to be cut?
if r.end.nodeValue.length > r.endOffset
r.end.splitText(r.endOffset)
changed = true
nr.end = r.end
# Make sure the common ancestor is an element node.
nr.commonAncestor = @commonAncestorContainer
while nr.commonAncestor.nodeType isnt Node.ELEMENT_NODE
nr.commonAncestor = nr.commonAncestor.parentNode
if changed
event = document.createEvent "UIEvents"
event.initUIEvent "domChange", true, false, window, 0
event.reason = "range normalization"
event.data = nr
nr.commonAncestor.dispatchEvent event
new Range.NormalizedRange(nr)
# Public: Creates a range suitable for storage.
#
# root - A root Element from which to anchor the serialisation.
# ignoreSelector - A selector String of elements to ignore. For example
# elements injected by the annotator.
#
# Returns an instance of SerializedRange.
serialize: (root, ignoreSelector) ->
this.normalize(root).serialize(root, ignoreSelector)
# Public: A normalised range is most commonly used throughout the annotator.
# its the result of a deserialised SerializedRange or a BrowserRange with
# out browser inconsistencies.
class Range.NormalizedRange
# Public: Creates an instance of a NormalizedRange.
#
# This is usually created by calling the .normalize() method on one of the
# other Range classes rather than manually.
#
# obj - An Object literal. Should have the following properties.
# commonAncestor: A Element that encompasses both the start and end nodes
# start: The first TextNode in the range.
# end The last TextNode in the range.
#
# Returns an instance of NormalizedRange.
constructor: (obj) ->
@commonAncestor = obj.commonAncestor
@start = obj.start
@end = obj.end
# Public: For API consistency.
#
# Returns itself.
normalize: (root) ->
this
# Public: Limits the nodes within the NormalizedRange to those contained
# withing the bounds parameter. It returns an updated range with all
# properties updated. NOTE: Method returns null if all nodes fall outside
# of the bounds.
#
# bounds - An Element to limit the range to.
#
# Returns updated self or null.
limit: (bounds) ->
nodes = $.grep this.textNodes(), (node) ->
node.parentNode == bounds or $.contains(bounds, node.parentNode)
return null unless nodes.length
@start = nodes[0]
@end = nodes[nodes.length - 1]
startParents = $(@start).parents()
for parent in $(@end).parents()
if startParents.index(parent) != -1
@commonAncestor = parent
break
this
# Convert this range into an object consisting of two pairs of (xpath,
# character offset), which can be easily stored in a database.
#
# root - The root Element relative to which XPaths should be calculated
# ignoreSelector - A selector String of elements to ignore. For example
# elements injected by the annotator.
#
# Returns an instance of SerializedRange.
serialize: (root, ignoreSelector) ->
serialization = (node, isEnd) ->
if ignoreSelector
origParent = $(node).parents(":not(#{ignoreSelector})").eq(0)
else
origParent = $(node).parent()
xpath = Util.xpathFromNode(origParent, root)[0]
textNodes = Util.getTextNodes(origParent)
# Calculate real offset as the combined length of all the
# preceding textNode siblings. We include the length of the
# node if it's the end node.
nodes = textNodes.slice(0, textNodes.index(node))
offset = 0
for n in nodes
offset += n.nodeValue.length
if isEnd then [xpath, offset + node.nodeValue.length] else [xpath, offset]
start = serialization(@start)
end = serialization(@end, true)
new Range.SerializedRange({
# XPath strings
startContainer: start[0]
endContainer: end[0]
# Character offsets (integer)
startOffset: start[1]
endOffset: end[1]
})
# Public: Creates a concatenated String of the contents of all the text nodes
# within the range.
#
# Returns a String.
text: ->
(for node in this.textNodes()
node.nodeValue
).join ''
# Public: Fetches only the text nodes within th range.
#
# Returns an Array of TextNode instances.
textNodes: ->
textNodes = Util.getTextNodes($(this.commonAncestor))
[start, end] = [textNodes.index(this.start), textNodes.index(this.end)]
# Return the textNodes that fall between the start and end indexes.
$.makeArray textNodes[start..end]
# Public: Converts the Normalized range to a native browser range.
#
# See: https://developer.mozilla.org/en/DOM/range
#
# Examples
#
# selection = window.getSelection()
# selection.removeAllRanges()
# selection.addRange(normedRange.toRange())
#
# Returns a Range object.
toRange: ->
range = document.createRange()
range.setStartBefore(@start)
range.setEndAfter(@end)
range
# Utility function to bottom-right the coordinates of this range,
# by inserting a test element before it, and taking it's pos.
getEndCoords: ->
me = $ this.end # Get the start element
probe = $ "<span></span>" # Prepare an element for probing
probe.insertAfter me # insert the probe element before the start
pos = probe.offset() # get the position
probe.remove() # remove the probe, restoring the original state
# return the wanted data
x: pos.left
y: pos.top
# Public: A range suitable for storing in local storage or serializing to JSON.
class Range.SerializedRange
# Public: Creates a SerializedRange
#
# obj - The stored object. It should have the following properties.
# startContainer: An xpath to the Element containing the first TextNode
# relative to the root Element.
# startOffset: The offset to the start of the selection from obj.start.
# endContainer: An xpath to the Element containing the last TextNode
# relative to the root Element.
# startOffset: The offset to the end of the selection from obj.end.
#
# Returns an instance of SerializedRange
constructor: (obj) ->
@startContainer = obj.startContainer
@startOffset = obj.startOffset
@endContainer = obj.endContainer
@endOffset = obj.endOffset
# Public: Creates a NormalizedRange.
#
# root - The root Element from which the XPaths were generated.
#
# Returns a NormalizedRange instance.
normalize: (root) ->
range = {}
for p in ['start', 'end']
xpath = this[p + 'Container']
try
node = Range.nodeFromXPath(xpath, root)
catch e
throw new Range.RangeError(p, "Error while finding #{p} node: #{xpath}: #{e}", e)
if not node
throw new Range.RangeError(p, "Couldn't find #{p} node: #{xpath}")
# Unfortunately, we *can't* guarantee only one textNode per
# elementNode, so we have to walk along the element's textNodes until
# the combined length of the textNodes to that point exceeds or
# matches the value of the offset.
length = 0
targetOffset = this[p + 'Offset']
# Range excludes its endpoint because it describes the boundary position.
# Target the string index of the last character inside the range.
if p is 'end' then targetOffset--
for tn in Util.getTextNodes($(node))
if (length + tn.nodeValue.length > targetOffset)
range[p + 'Container'] = tn
range[p + 'Offset'] = this[p + 'Offset'] - length
break
else
length += tn.nodeValue.length
# If we fall off the end of the for loop without having set
# 'startOffset'/'endOffset', the element has shorter content than when
# we annotated, so throw an error:
if not range[p + 'Offset']?
throw new Range.RangeError(p, "#{p}offset", "Couldn't find offset #{this[p + 'Offset']} in element #{this[p]}")
# Here's an elegant next step...
#
# range.commonAncestorContainer = $(range.startContainer).parents().has(range.endContainer)[0]
#
# ...but unfortunately Node.contains() is broken in Safari 5.1.5 (7534.55.3)
# and presumably other earlier versions of WebKit. In particular, in a
# document like
#
# <p>Hello</p>
#
# the code
#
# p = document.getElementsByTagName('p')[0]
# p.contains(p.firstChild)
#
# returns `false`. Yay.
#
# So instead, we step through the parents from the bottom up and use
# Node.compareDocumentPosition() to decide when to set the
# commonAncestorContainer and bail out.
contains = if not document.compareDocumentPosition?
# IE
(a, b) -> a.contains(b)
else
# Everyone else
(a, b) -> a.compareDocumentPosition(b) & 16
$(range.startContainer).parents().each ->
if contains(this, range.endContainer)
range.commonAncestorContainer = this
return false
new Range.BrowserRange(range).normalize(root)
# Public: Creates a range suitable for storage.
#
# root - A root Element from which to anchor the serialisation.
# ignoreSelector - A selector String of elements to ignore. For example
# elements injected by the annotator.
#
# Returns an instance of SerializedRange.
serialize: (root, ignoreSelector) ->
this.normalize(root).serialize(root, ignoreSelector)
# Public: Returns the range as an Object literal.
toObject: ->
{
startContainer: @startContainer
startOffset: @startOffset
endContainer: @endContainer
endOffset: @endOffset
}
# Public: Creates an element for viewing annotations.
class Annotator.Viewer extends Annotator.Widget
# Events to be bound to the @element.
events:
".annotator-edit click": "onEditClick"
".annotator-delete click": "onDeleteClick"
# Classes for toggling annotator state.
classes:
hide: 'annotator-hide'
showControls: 'annotator-visible'
# HTML templates for @element and @item properties.
html:
element:"""
<div class="annotator-outer annotator-viewer">
<ul class="annotator-widget annotator-listing"></ul>
</div>
"""
item: """
<li class="annotator-annotation annotator-item">
<span class="annotator-controls">
<a href="#" title="View as webpage" class="annotator-link">View as webpage</a>
<button title="Edit" class="annotator-edit">Edit</button>
<button title="Delete" class="annotator-delete">Delete</button>
</span>
</li>
"""
# Configuration options
options:
readOnly: false # Start the viewer in read-only mode. No controls will be shown.
# Public: Creates an instance of the Viewer object. This will create the
# @element from the @html.element string and set up all events.
#
# options - An Object literal containing options.
#
# Examples
#
# # Creates a new viewer, adds a custom field and displays an annotation.
# viewer = new Annotator.Viewer()
# viewer.addField({
# load: someLoadCallback
# })
# viewer.load(annotation)
#
# Returns a new Viewer instance.
constructor: (options) ->
super $(@html.element)[0], options
@item = $(@html.item)[0]
@fields = []
@annotations = []
# Public: Displays the Viewer and first the "show" event. Can be used as an
# event callback and will call Event#preventDefault() on the supplied event.
#
# event - Event object provided if method is called by event
# listener (default:undefined)
#
# Examples
#
# # Displays the editor.
# viewer.show()
#
# # Displays the viewer on click (prevents default action).
# $('a.show-viewer').bind('click', viewer.show)
#
# Returns itself.
show: (event) =>
util.preventEventDefault event
controls = @element
.find('.annotator-controls')
.addClass(@classes.showControls)
setTimeout((=> controls.removeClass(@classes.showControls)), 500)
@element.removeClass(@classes.hide)
this.checkOrientation().publish('show')
# Public: Checks to see if the Viewer is currently displayed.
#
# Examples
#
# viewer.show()
# viewer.isShown() # => Returns true
#
# viewer.hide()
# viewer.isShown() # => Returns false
#
# Returns true if the Viewer is visible.
isShown: ->
not @element.hasClass(@classes.hide)
# Public: Hides the Editor and fires the "hide" event. Can be used as an event
# callback and will call Event#preventDefault() on the supplied event.
#
# event - Event object provided if method is called by event
# listener (default:undefined)
#
# Examples
#
# # Hides the editor.
# viewer.hide()
#
# # Hide the viewer on click (prevents default action).
# $('a.hide-viewer').bind('click', viewer.hide)
#
# Returns itself.
hide: (event) =>
util.preventEventDefault event
@element.addClass(@classes.hide)
this.publish('hide')
# Public: Loads annotations into the viewer and shows it. Fires the "load"
# event once the viewer is loaded passing the annotations into the callback.
#
# annotation - An Array of annotation elements.
#
# Examples
#
# viewer.load([annotation1, annotation2, annotation3])
#
# Returns itslef.
load: (annotations) =>
@annotations = annotations || []
list = @element.find('ul:first').empty()
for annotation in @annotations
item = $(@item).clone().appendTo(list).data('annotation', annotation)
controls = item.find('.annotator-controls')
link = controls.find('.annotator-link')
edit = controls.find('.annotator-edit')
del = controls.find('.annotator-delete')
links = new LinkParser(annotation.links or []).get('alternate', {'type': 'text/html'})
if links.length is 0 or not links[0].href?
link.remove()
else
link.attr('href', links[0].href)
if @options.readOnly
edit.remove()
del.remove()
else
controller = {
showEdit: -> edit.removeAttr('disabled')
hideEdit: -> edit.attr('disabled', 'disabled')
showDelete: -> del.removeAttr('disabled')
hideDelete: -> del.attr('disabled', 'disabled')
}
for field in @fields
element = $(field.element).clone().appendTo(item)[0]
field.load(element, annotation, controller)
this.publish('load', [@annotations])
this.show()
# Public: Adds an addional field to an annotation view. A callback can be
# provided to update the view on load.
#
# options - An options Object. Options are as follows:
# load - Callback Function called when the view is loaded with an
# annotation. Recieves a newly created clone of @item and
# the annotation to be displayed (it will be called once
# for each annotation being loaded).
#
# Examples
#
# # Display a user name.
# viewer.addField({
# # This is called when the viewer is loaded.
# load: (field, annotation) ->
# field = $(field)
#
# if annotation.user
# field.text(annotation.user) # Display the user
# else
# field.remove() # Do not display the field.
# })
#
# Returns itself.
addField: (options) ->
field = $.extend({
load: ->
}, options)
field.element = $('<div />')[0]
@fields.push field
field.element
this
# Callback function: called when the edit button is clicked.
#
# event - An Event object.
#
# Returns nothing.
onEditClick: (event) =>
this.onButtonClick(event, 'edit')
# Callback function: called when the delete button is clicked.
#
# event - An Event object.
#
# Returns nothing.
onDeleteClick: (event) =>
this.onButtonClick(event, 'delete')
# Fires an event of type and passes in the associated annotation.
#
# event - An Event object.
# type - The type of event to fire. Either "edit" or "delete".
#
# Returns nothing.
onButtonClick: (event, type) ->
item = $(event.target).parents('.annotator-annotation')
this.publish(type, [item.data('annotation')])
# Private: simple parser for hypermedia link structure
#
# Examples:
#
# links = [
# { rel: 'alternate', href: 'http://example.com/pages/14.json', type: 'application/json' },
# { rel: 'prev': href: 'http://example.com/pages/13' }
# ]
#
# lp = LinkParser(links)
# lp.get('alternate') # => [ { rel: 'alternate', href: 'http://...', ... } ]
# lp.get('alternate', {type: 'text/html'}) # => []
#
class LinkParser
constructor: (@data) ->
get: (rel, cond={}) ->
cond = $.extend({}, cond, {rel: rel})
keys = (k for own k, v of cond)
for d in @data
match = keys.reduce ((m, k) -> m and (d[k] is cond[k])), true
if match
d
else
continue
# Public: Base class for the Editor and Viewer elements. Contains methods that
# are shared between the two.
class Annotator.Widget extends Delegator
# Classes used to alter the widgets state.
classes:
hide: 'annotator-hide'
invert:
x: 'annotator-invert-x'
y: 'annotator-invert-y'
# Public: Creates a new Widget instance.
#
# element - The Element that represents the widget in the DOM.
# options - An Object literal of options.
#
# Examples
#
# element = document.createElement('div')
# widget = new Annotator.Widget(element)
#
# Returns a new Widget instance.
constructor: (element, options) ->
super
@classes = $.extend {}, Annotator.Widget.prototype.classes, @classes
# Public: Unbind the widget's events and remove its element from the DOM.
#
# Returns nothing.
destroy: ->
this.removeEvents()
@element.remove()
checkOrientation: ->
this.resetOrientation()
window = $(util.getGlobal())
widget = @element.children(":first")
offset = widget.offset()
viewport = {
top: window.scrollTop(),
right: window.width() + window.scrollLeft()
}
current = {
top: offset.top
right: offset.left + widget.width()
}
if (current.top - viewport.top) < 0
this.invertY()
if (current.right - viewport.right) > 0
this.invertX()
this
# Public: Resets orientation of widget on the X & Y axis.
#
# Examples
#
# widget.resetOrientation() # Widget is original way up.
#
# Returns itself for chaining.
resetOrientation: ->
@element.removeClass(@classes.invert.x).removeClass(@classes.invert.y)
this
# Public: Inverts the widget on the X axis.
#
# Examples
#
# widget.invertX() # Widget is now right aligned.
#
# Returns itself for chaining.
invertX: ->
@element.addClass @classes.invert.x
this
# Public: Inverts the widget on the Y axis.
#
# Examples
#
# widget.invertY() # Widget is now upside down.
#
# Returns itself for chaining.
invertY: ->
@element.addClass @classes.invert.y
this
# Public: Find out whether or not the widget is currently upside down
#
# Returns a boolean: true if the widget is upside down
isInvertedY: ->
@element.hasClass @classes.invert.y
# Public: Find out whether or not the widget is currently right aligned
#
# Returns a boolean: true if the widget is right aligned
isInvertedX: ->
@element.hasClass @classes.invert.x
# A simple XPath evaluator using jQuery which can evaluate queries of
simpleXPathJQuery = (relativeRoot) ->
jq = this.map ->
path = ''
elem = this
while elem?.nodeType == Node.ELEMENT_NODE and elem isnt relativeRoot
tagName = elem.tagName.replace(":", "\\:")
idx = $(elem.parentNode).children(tagName).index(elem) + 1
idx = "[#{idx}]"
path = "/" + elem.tagName.toLowerCase() + idx + path
elem = elem.parentNode
path
jq.get()
# A simple XPath evaluator using only standard DOM methods which can
# evaluate queries of the form /tag[index]/tag[index].
simpleXPathPure = (relativeRoot) ->
getPathSegment = (node) ->
name = getNodeName node
pos = getNodePosition node
"#{name}[#{pos}]"
rootNode = relativeRoot
getPathTo = (node) ->
xpath = '';
while node != rootNode
unless node?
throw new Error "Called getPathTo on a node which was not a descendant of @rootNode. " + rootNode
xpath = (getPathSegment node) + '/' + xpath
node = node.parentNode
xpath = '/' + xpath
xpath = xpath.replace /\/$/, ''
xpath
jq = this.map ->
path = getPathTo this
path
jq.get()
findChild = (node, type, index) ->
unless node.hasChildNodes()
throw new Error "XPath error: node has no children!"
children = node.childNodes
found = 0
for child in children
name = getNodeName child
if name is type
found += 1
if found is index
return child
throw new Error "XPath error: wanted child not found."
# Get the node name for use in generating an xpath expression.
getNodeName = (node) ->
nodeName = node.nodeName.toLowerCase()
switch nodeName
when "#text" then return "text()"
when "#comment" then return "comment()"
when "#cdata-section" then return "cdata-section()"
else return nodeName
# Get the index of the node as it appears in its parent's child list
getNodePosition = (node) ->
pos = 0
tmp = node
while tmp
if tmp.nodeName is node.nodeName
pos++
tmp = tmp.previousSibling
pos
\ No newline at end of file
// Generated by CoffeeScript 1.6.3
/*
** Annotator 1.2.6-dev-b8c7514
** https://github.com/okfn/annotator/
**
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2014-12-15 12:17:59Z
*/
/*
//
*/
// Generated by CoffeeScript 1.6.3
(function() {
var $, Anchor, Annotator, Delegator, DummyDocumentAccess, Highlight, LinkParser, Range, Util, findChild, fn, functions, g, getNodeName, getNodePosition, gettext, simpleXPathJQuery, simpleXPathPure, util, _Annotator, _gettext, _i, _j, _len, _len1, _ref, _t,
__slice = [].slice,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
simpleXPathJQuery = function(relativeRoot) {
var jq;
jq = this.map(function() {
var elem, idx, path, tagName;
path = '';
elem = this;
while ((elem != null ? elem.nodeType : void 0) === Node.ELEMENT_NODE && elem !== relativeRoot) {
tagName = elem.tagName.replace(":", "\\:");
idx = $(elem.parentNode).children(tagName).index(elem) + 1;
idx = "[" + idx + "]";
path = "/" + elem.tagName.toLowerCase() + idx + path;
elem = elem.parentNode;
}
return path;
});
return jq.get();
};
simpleXPathPure = function(relativeRoot) {
var getPathSegment, getPathTo, jq, rootNode;
getPathSegment = function(node) {
var name, pos;
name = getNodeName(node);
pos = getNodePosition(node);
return "" + name + "[" + pos + "]";
};
rootNode = relativeRoot;
getPathTo = function(node) {
var xpath;
xpath = '';
while (node !== rootNode) {
if (node == null) {
throw new Error("Called getPathTo on a node which was not a descendant of @rootNode. " + rootNode);
}
xpath = (getPathSegment(node)) + '/' + xpath;
node = node.parentNode;
}
xpath = '/' + xpath;
xpath = xpath.replace(/\/$/, '');
return xpath;
};
jq = this.map(function() {
var path;
path = getPathTo(this);
return path;
});
return jq.get();
};
findChild = function(node, type, index) {
var child, children, found, name, _i, _len;
if (!node.hasChildNodes()) {
throw new Error("XPath error: node has no children!");
}
children = node.childNodes;
found = 0;
for (_i = 0, _len = children.length; _i < _len; _i++) {
child = children[_i];
name = getNodeName(child);
if (name === type) {
found += 1;
if (found === index) {
return child;
}
}
}
throw new Error("XPath error: wanted child not found.");
};
getNodeName = function(node) {
var nodeName;
nodeName = node.nodeName.toLowerCase();
switch (nodeName) {
case "#text":
return "text()";
case "#comment":
return "comment()";
case "#cdata-section":
return "cdata-section()";
default:
return nodeName;
}
};
getNodePosition = function(node) {
var pos, tmp;
pos = 0;
tmp = node;
while (tmp) {
if (tmp.nodeName === node.nodeName) {
pos++;
}
tmp = tmp.previousSibling;
}
return pos;
};
gettext = null;
if (typeof Gettext !== "undefined" && Gettext !== null) {
_gettext = new Gettext({
domain: "annotator"
});
gettext = function(msgid) {
return _gettext.gettext(msgid);
};
} else {
gettext = function(msgid) {
return msgid;
};
}
_t = function(msgid) {
return gettext(msgid);
};
if (!(typeof jQuery !== "undefined" && jQuery !== null ? (_ref = jQuery.fn) != null ? _ref.jquery : void 0 : void 0)) {
console.error(_t("Annotator requires jQuery: have you included lib/vendor/jquery.js?"));
}
if (!(JSON && JSON.parse && JSON.stringify)) {
console.error(_t("Annotator requires a JSON implementation: have you included lib/vendor/json2.js?"));
}
$ = jQuery;
Util = {};
Util.flatten = function(array) {
var flatten;
flatten = function(ary) {
var el, flat, _i, _len;
flat = [];
for (_i = 0, _len = ary.length; _i < _len; _i++) {
el = ary[_i];
flat = flat.concat(el && $.isArray(el) ? flatten(el) : el);
}
return flat;
};
return flatten(array);
};
Util.getTextNodes = function(jq) {
var getTextNodes;
getTextNodes = function(node) {
var nodes;
if (node && node.nodeType !== Node.TEXT_NODE) {
nodes = [];
if (node.nodeType !== Node.COMMENT_NODE) {
node = node.lastChild;
while (node) {
nodes.push(getTextNodes(node));
node = node.previousSibling;
}
}
return nodes.reverse();
} else {
return node;
}
};
return jq.map(function() {
return Util.flatten(getTextNodes(this));
});
};
Util.getLastTextNodeUpTo = function(n) {
var result;
switch (n.nodeType) {
case Node.TEXT_NODE:
return n;
case Node.ELEMENT_NODE:
if (n.lastChild != null) {
result = Util.getLastTextNodeUpTo(n.lastChild);
if (result != null) {
return result;
}
}
break;
}
n = n.previousSibling;
if (n != null) {
return Util.getLastTextNodeUpTo(n);
} else {
return null;
}
};
Util.getFirstTextNodeNotBefore = function(n) {
var result;
switch (n.nodeType) {
case Node.TEXT_NODE:
return n;
case Node.ELEMENT_NODE:
if (n.firstChild != null) {
result = Util.getFirstTextNodeNotBefore(n.firstChild);
if (result != null) {
return result;
}
}
break;
}
n = n.nextSibling;
if (n != null) {
return Util.getFirstTextNodeNotBefore(n);
} else {
return null;
}
};
Util.xpathFromNode = function(el, relativeRoot) {
var exception, result;
try {
result = simpleXPathJQuery.call(el, relativeRoot);
} catch (_error) {
exception = _error;
console.log("jQuery-based XPath construction failed! Falling back to manual.");
result = simpleXPathPure.call(el, relativeRoot);
}
return result;
};
Util.nodeFromXPath = function(xp, root) {
var idx, name, node, step, steps, _i, _len, _ref1;
steps = xp.substring(1).split("/");
node = root;
for (_i = 0, _len = steps.length; _i < _len; _i++) {
step = steps[_i];
_ref1 = step.split("["), name = _ref1[0], idx = _ref1[1];
idx = idx != null ? parseInt((idx != null ? idx.split("]") : void 0)[0]) : 1;
node = findChild(node, name.toLowerCase(), idx);
}
return node;
};
Util.escape = function(html) {
return html.replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};
functions = ["log", "debug", "info", "warn", "exception", "assert", "dir", "dirxml", "trace", "group", "groupEnd", "groupCollapsed", "time", "timeEnd", "profile", "profileEnd", "count", "clear", "table", "error", "notifyFirebug", "firebug", "userObjects"];
if (typeof console !== "undefined" && console !== null) {
if (console.group == null) {
console.group = function(name) {
return console.log("GROUP: ", name);
};
}
if (console.groupCollapsed == null) {
console.groupCollapsed = console.group;
}
for (_i = 0, _len = functions.length; _i < _len; _i++) {
fn = functions[_i];
if (console[fn] == null) {
console[fn] = function() {
return console.log(_t("Not implemented:") + (" console." + name));
};
}
}
} else {
this.console = {};
for (_j = 0, _len1 = functions.length; _j < _len1; _j++) {
fn = functions[_j];
this.console[fn] = function() {};
}
this.console['error'] = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return alert("ERROR: " + (args.join(', ')));
};
this.console['warn'] = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return alert("WARNING: " + (args.join(', ')));
};
}
Delegator = (function() {
Delegator.prototype.events = {};
Delegator.prototype.options = {};
Delegator.prototype.element = null;
function Delegator(element, options) {
this.options = $.extend(true, {}, this.options, options);
this.element = $(element);
this._closures = {};
this.on = this.subscribe;
this.addEvents();
}
Delegator.prototype.addEvents = function() {
var event, _k, _len2, _ref1, _results;
_ref1 = Delegator._parseEvents(this.events);
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
event = _ref1[_k];
_results.push(this._addEvent(event.selector, event.event, event.functionName));
}
return _results;
};
Delegator.prototype.removeEvents = function() {
var event, _k, _len2, _ref1, _results;
_ref1 = Delegator._parseEvents(this.events);
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
event = _ref1[_k];
_results.push(this._removeEvent(event.selector, event.event, event.functionName));
}
return _results;
};
Delegator.prototype._addEvent = function(selector, event, functionName) {
var closure, f,
_this = this;
f = typeof functionName === 'string' ? this[functionName] : functionName;
closure = function() {
return f.apply(_this, arguments);
};
if (selector === '' && Delegator._isCustomEvent(event)) {
this.subscribe(event, closure);
} else {
this.element.delegate(selector, event, closure);
}
this._closures["" + selector + "/" + event + "/" + functionName] = closure;
return this;
};
Delegator.prototype._removeEvent = function(selector, event, functionName) {
var closure;
closure = this._closures["" + selector + "/" + event + "/" + functionName];
if (selector === '' && Delegator._isCustomEvent(event)) {
this.unsubscribe(event, closure);
} else {
this.element.undelegate(selector, event, closure);
}
delete this._closures["" + selector + "/" + event + "/" + functionName];
return this;
};
Delegator.prototype.publish = function() {
this.element.triggerHandler.apply(this.element, arguments);
return this;
};
Delegator.prototype.subscribe = function(event, callback) {
var closure;
closure = function() {
return callback.apply(this, [].slice.call(arguments, 1));
};
closure.guid = callback.guid = ($.guid += 1);
this.element.bind(event, closure);
return this;
};
Delegator.prototype.unsubscribe = function() {
this.element.unbind.apply(this.element, arguments);
return this;
};
return Delegator;
})();
Delegator._parseEvents = function(eventsObj) {
var event, events, functionName, sel, selector, _k, _ref1;
events = [];
for (sel in eventsObj) {
functionName = eventsObj[sel];
_ref1 = sel.split(' '), selector = 2 <= _ref1.length ? __slice.call(_ref1, 0, _k = _ref1.length - 1) : (_k = 0, []), event = _ref1[_k++];
events.push({
selector: selector.join(' '),
event: event,
functionName: functionName
});
}
return events;
};
Delegator.natives = (function() {
var key, specials, val;
specials = (function() {
var _ref1, _results;
_ref1 = jQuery.event.special;
_results = [];
for (key in _ref1) {
if (!__hasProp.call(_ref1, key)) continue;
val = _ref1[key];
_results.push(key);
}
return _results;
})();
return "blur focus focusin focusout load resize scroll unload click dblclick\nmousedown mouseup mousemove mouseover mouseout mouseenter mouseleave\nchange select submit keydown keypress keyup error".split(/[^a-z]+/).concat(specials);
})();
Delegator._isCustomEvent = function(event) {
event = event.split('.')[0];
return $.inArray(event, Delegator.natives) === -1;
};
Range = {};
Range.sniff = function(r) {
if (r.commonAncestorContainer != null) {
return new Range.BrowserRange(r);
} else if (typeof r.start === "string") {
return new Range.SerializedRange({
startContainer: r.start,
startOffset: r.startOffset,
endContainer: r.end,
endOffset: r.endOffset
});
} else if (typeof r.startContainer === "string") {
return new Range.SerializedRange(r);
} else if (r.start && typeof r.start === "object") {
return new Range.NormalizedRange(r);
} else {
console.error(_t("Could not sniff range type"));
return false;
}
};
Range.nodeFromXPath = function(xpath, root) {
var customResolver, evaluateXPath, namespace, node, segment;
if (root == null) {
root = document;
}
evaluateXPath = function(xp, nsResolver) {
var exception;
if (nsResolver == null) {
nsResolver = null;
}
try {
return document.evaluate('.' + xp, root, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
} catch (_error) {
exception = _error;
console.log("XPath evaluation failed.");
console.log("Trying fallback...");
return Util.nodeFromXPath(xp, root);
}
};
if (!$.isXMLDoc(document.documentElement)) {
return evaluateXPath(xpath);
} else {
customResolver = document.createNSResolver(document.ownerDocument === null ? document.documentElement : document.ownerDocument.documentElement);
node = evaluateXPath(xpath, customResolver);
if (!node) {
xpath = ((function() {
var _k, _len2, _ref1, _results;
_ref1 = xpath.split('/');
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
segment = _ref1[_k];
if (segment && segment.indexOf(':') === -1) {
_results.push(segment.replace(/^([a-z]+)/, 'xhtml:$1'));
} else {
_results.push(segment);
}
}
return _results;
})()).join('/');
namespace = document.lookupNamespaceURI(null);
customResolver = function(ns) {
if (ns === 'xhtml') {
return namespace;
} else {
return document.documentElement.getAttribute('xmlns:' + ns);
}
};
node = evaluateXPath(xpath, customResolver);
}
return node;
}
};
Range.RangeError = (function(_super) {
__extends(RangeError, _super);
function RangeError(type, message, parent) {
this.type = type;
this.message = message;
this.parent = parent != null ? parent : null;
RangeError.__super__.constructor.call(this, this.message);
}
return RangeError;
})(Error);
Range.BrowserRange = (function() {
function BrowserRange(obj) {
this.commonAncestorContainer = obj.commonAncestorContainer;
this.startContainer = obj.startContainer;
this.startOffset = obj.startOffset;
this.endContainer = obj.endContainer;
this.endOffset = obj.endOffset;
}
BrowserRange.prototype.normalize = function(root) {
var changed, event, n, node, nr, prev, r;
if (this.tainted) {
console.error(_t("You may only call normalize() once on a BrowserRange!"));
return false;
} else {
this.tainted = true;
}
r = {};
if (this.startContainer.nodeType === Node.ELEMENT_NODE) {
r.start = Util.getFirstTextNodeNotBefore(this.startContainer.childNodes[this.startOffset]);
r.startOffset = 0;
} else {
r.start = this.startContainer;
r.startOffset = this.startOffset;
}
if (this.endContainer.nodeType === Node.ELEMENT_NODE) {
node = this.endContainer.childNodes[this.endOffset];
if (node != null) {
n = node;
while ((n != null) && (n.nodeType !== Node.TEXT_NODE)) {
n = n.firstChild;
}
if (n != null) {
prev = n.previousSibling;
if ((prev != null) && (prev.nodeType === Node.TEXT_NODE)) {
r.end = prev;
r.endOffset = prev.nodeValue.length;
} else {
r.end = n;
r.endOffset = 0;
}
}
}
if (r.end == null) {
if (this.endOffset) {
node = this.endContainer.childNodes[this.endOffset - 1];
} else {
node = this.endContainer.previousSibling;
}
r.end = Util.getLastTextNodeUpTo(node);
r.endOffset = r.end.nodeValue.length;
}
} else {
r.end = this.endContainer;
r.endOffset = this.endOffset;
}
nr = {};
changed = false;
if (r.startOffset > 0) {
if (r.start.nodeValue.length > r.startOffset) {
nr.start = r.start.splitText(r.startOffset);
changed = true;
} else {
nr.start = r.start.nextSibling;
}
} else {
nr.start = r.start;
}
if (r.start === r.end) {
if (nr.start.nodeValue.length > (r.endOffset - r.startOffset)) {
nr.start.splitText(r.endOffset - r.startOffset);
changed = true;
}
nr.end = nr.start;
} else {
if (r.end.nodeValue.length > r.endOffset) {
r.end.splitText(r.endOffset);
changed = true;
}
nr.end = r.end;
}
nr.commonAncestor = this.commonAncestorContainer;
while (nr.commonAncestor.nodeType !== Node.ELEMENT_NODE) {
nr.commonAncestor = nr.commonAncestor.parentNode;
}
if (changed) {
event = document.createEvent("UIEvents");
event.initUIEvent("domChange", true, false, window, 0);
event.reason = "range normalization";
event.data = nr;
nr.commonAncestor.dispatchEvent(event);
}
return new Range.NormalizedRange(nr);
};
BrowserRange.prototype.serialize = function(root, ignoreSelector) {
return this.normalize(root).serialize(root, ignoreSelector);
};
return BrowserRange;
})();
Range.NormalizedRange = (function() {
function NormalizedRange(obj) {
this.commonAncestor = obj.commonAncestor;
this.start = obj.start;
this.end = obj.end;
}
NormalizedRange.prototype.normalize = function(root) {
return this;
};
NormalizedRange.prototype.limit = function(bounds) {
var nodes, parent, startParents, _k, _len2, _ref1;
nodes = $.grep(this.textNodes(), function(node) {
return node.parentNode === bounds || $.contains(bounds, node.parentNode);
});
if (!nodes.length) {
return null;
}
this.start = nodes[0];
this.end = nodes[nodes.length - 1];
startParents = $(this.start).parents();
_ref1 = $(this.end).parents();
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
parent = _ref1[_k];
if (startParents.index(parent) !== -1) {
this.commonAncestor = parent;
break;
}
}
return this;
};
NormalizedRange.prototype.serialize = function(root, ignoreSelector) {
var end, serialization, start;
serialization = function(node, isEnd) {
var n, nodes, offset, origParent, textNodes, xpath, _k, _len2;
if (ignoreSelector) {
origParent = $(node).parents(":not(" + ignoreSelector + ")").eq(0);
} else {
origParent = $(node).parent();
}
xpath = Util.xpathFromNode(origParent, root)[0];
textNodes = Util.getTextNodes(origParent);
nodes = textNodes.slice(0, textNodes.index(node));
offset = 0;
for (_k = 0, _len2 = nodes.length; _k < _len2; _k++) {
n = nodes[_k];
offset += n.nodeValue.length;
}
if (isEnd) {
return [xpath, offset + node.nodeValue.length];
} else {
return [xpath, offset];
}
};
start = serialization(this.start);
end = serialization(this.end, true);
return new Range.SerializedRange({
startContainer: start[0],
endContainer: end[0],
startOffset: start[1],
endOffset: end[1]
});
};
NormalizedRange.prototype.text = function() {
var node;
return ((function() {
var _k, _len2, _ref1, _results;
_ref1 = this.textNodes();
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
node = _ref1[_k];
_results.push(node.nodeValue);
}
return _results;
}).call(this)).join('');
};
NormalizedRange.prototype.textNodes = function() {
var end, start, textNodes, _ref1;
textNodes = Util.getTextNodes($(this.commonAncestor));
_ref1 = [textNodes.index(this.start), textNodes.index(this.end)], start = _ref1[0], end = _ref1[1];
return $.makeArray(textNodes.slice(start, +end + 1 || 9e9));
};
NormalizedRange.prototype.toRange = function() {
var range;
range = document.createRange();
range.setStartBefore(this.start);
range.setEndAfter(this.end);
return range;
};
NormalizedRange.prototype.getEndCoords = function() {
var me, pos, probe;
me = $(this.end);
probe = $("<span></span>");
probe.insertAfter(me);
pos = probe.offset();
probe.remove();
return {
x: pos.left,
y: pos.top
};
};
return NormalizedRange;
})();
Range.SerializedRange = (function() {
function SerializedRange(obj) {
this.startContainer = obj.startContainer;
this.startOffset = obj.startOffset;
this.endContainer = obj.endContainer;
this.endOffset = obj.endOffset;
}
SerializedRange.prototype.normalize = function(root) {
var contains, e, length, node, p, range, targetOffset, tn, xpath, _k, _l, _len2, _len3, _ref1, _ref2;
range = {};
_ref1 = ['start', 'end'];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
p = _ref1[_k];
xpath = this[p + 'Container'];
try {
node = Range.nodeFromXPath(xpath, root);
} catch (_error) {
e = _error;
throw new Range.RangeError(p, "Error while finding " + p + " node: " + xpath + ": " + e, e);
}
if (!node) {
throw new Range.RangeError(p, "Couldn't find " + p + " node: " + xpath);
}
length = 0;
targetOffset = this[p + 'Offset'];
if (p === 'end') {
targetOffset--;
}
_ref2 = Util.getTextNodes($(node));
for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) {
tn = _ref2[_l];
if (length + tn.nodeValue.length > targetOffset) {
range[p + 'Container'] = tn;
range[p + 'Offset'] = this[p + 'Offset'] - length;
break;
} else {
length += tn.nodeValue.length;
}
}
if (range[p + 'Offset'] == null) {
throw new Range.RangeError(p, "" + p + "offset", "Couldn't find offset " + this[p + 'Offset'] + " in element " + this[p]);
}
}
contains = document.compareDocumentPosition == null ? function(a, b) {
return a.contains(b);
} : function(a, b) {
return a.compareDocumentPosition(b) & 16;
};
$(range.startContainer).parents().each(function() {
if (contains(this, range.endContainer)) {
range.commonAncestorContainer = this;
return false;
}
});
return new Range.BrowserRange(range).normalize(root);
};
SerializedRange.prototype.serialize = function(root, ignoreSelector) {
return this.normalize(root).serialize(root, ignoreSelector);
};
SerializedRange.prototype.toObject = function() {
return {
startContainer: this.startContainer,
startOffset: this.startOffset,
endContainer: this.endContainer,
endOffset: this.endOffset
};
};
return SerializedRange;
})();
Anchor = (function() {
function Anchor(annotator, annotation, target, startPage, endPage, quote, diffHTML, diffCaseOnly) {
this.annotator = annotator;
this.annotation = annotation;
this.target = target;
this.startPage = startPage;
this.endPage = endPage;
this.quote = quote;
this.diffHTML = diffHTML;
this.diffCaseOnly = diffCaseOnly;
this.virtualize = __bind(this.virtualize, this);
this.realize = __bind(this.realize, this);
if (this.annotator == null) {
throw "annotator is required!";
}
if (this.annotation == null) {
throw "annotation is required!";
}
if (this.target == null) {
throw "target is required!";
}
if (this.startPage == null) {
"startPage is required!";
}
if (this.endPage == null) {
throw "endPage is required!";
}
if (this.quote == null) {
throw "quote is required!";
}
this.highlight = {};
}
Anchor.prototype._createHighlight = function(page) {
throw "Function not implemented";
};
Anchor.prototype.realize = function() {
var created, page, pagesTodo, renderedPages, _k, _ref1, _ref2, _results,
_this = this;
if (this.fullyRealized) {
return;
}
renderedPages = (function() {
_results = [];
for (var _k = _ref1 = this.startPage, _ref2 = this.endPage; _ref1 <= _ref2 ? _k <= _ref2 : _k >= _ref2; _ref1 <= _ref2 ? _k++ : _k--){ _results.push(_k); }
return _results;
}).apply(this).filter(function(index) {
return _this.annotator.domMapper.isPageMapped(index);
});
pagesTodo = renderedPages.filter(function(index) {
return _this.highlight[index] == null;
});
if (!pagesTodo.length) {
return;
}
created = (function() {
var _l, _len2, _results1;
_results1 = [];
for (_l = 0, _len2 = pagesTodo.length; _l < _len2; _l++) {
page = pagesTodo[_l];
_results1.push(this.highlight[page] = this._createHighlight(page));
}
return _results1;
}).call(this);
this.fullyRealized = renderedPages.length === this.endPage - this.startPage + 1;
return this.annotator.publish('highlightsCreated', created);
};
Anchor.prototype.virtualize = function(pageIndex) {
var highlight;
highlight = this.highlight[pageIndex];
if (highlight == null) {
return;
}
highlight.removeFromDocument();
delete this.highlight[pageIndex];
this.fullyRealized = false;
return this.annotator.publish('highlightRemoved', highlight);
};
Anchor.prototype.remove = function() {
var anchors, i, index, _k, _ref1, _ref2, _ref3, _results;
_results = [];
for (index = _k = _ref1 = this.startPage, _ref2 = this.endPage; _ref1 <= _ref2 ? _k <= _ref2 : _k >= _ref2; index = _ref1 <= _ref2 ? ++_k : --_k) {
this.virtualize(index);
anchors = this.annotator.anchors[index];
i = anchors.indexOf(this);
[].splice.apply(anchors, [i, i - i + 1].concat(_ref3 = [])), _ref3;
if (!anchors.length) {
_results.push(delete this.annotator.anchors[index]);
} else {
_results.push(void 0);
}
}
return _results;
};
Anchor.prototype.annotationUpdated = function() {
var index, _k, _ref1, _ref2, _ref3, _results;
_results = [];
for (index = _k = _ref1 = this.startPage, _ref2 = this.endPage; _ref1 <= _ref2 ? _k <= _ref2 : _k >= _ref2; index = _ref1 <= _ref2 ? ++_k : --_k) {
_results.push((_ref3 = this.highlight[index]) != null ? _ref3.annotationUpdated() : void 0);
}
return _results;
};
return Anchor;
})();
Highlight = (function() {
function Highlight(anchor, pageIndex) {
this.anchor = anchor;
this.pageIndex = pageIndex;
this.annotator = this.anchor.annotator;
this.annotation = this.anchor.annotation;
}
Highlight.prototype.setTemporary = function(value) {
throw "Operation not implemented.";
};
Highlight.prototype.isTemporary = function() {
throw "Operation not implemented.";
};
Highlight.prototype.setFocused = function(value, batch) {
if (batch == null) {
batch = false;
}
throw "Operation not implemented.";
};
Highlight.prototype.annotationUpdated = function() {};
Highlight.prototype.removeFromDocument = function() {
throw "Operation not implemented.";
};
Highlight.prototype._getDOMElements = function() {
throw "Operation not implemented.";
};
Highlight.prototype.getTop = function() {
return $(this._getDOMElements()).offset().top;
};
Highlight.prototype.getHeight = function() {
return $(this._getDOMElements()).outerHeight(true);
};
Highlight.prototype.getBottom = function() {
return this.getTop() + this.getBottom();
};
Highlight.prototype.scrollTo = function() {
return $(this._getDOMElements()).scrollintoview();
};
Highlight.prototype.paddedScrollTo = function(direction) {
var defaultView, dir, pad, where, wrapper;
if (direction == null) {
throw "Direction is required";
}
dir = direction === "up" ? -1 : +1;
where = $(this._getDOMElements());
wrapper = this.annotator.wrapper;
defaultView = wrapper[0].ownerDocument.defaultView;
pad = defaultView.innerHeight * .2;
return where.scrollintoview({
complete: function() {
var correction, scrollable, top;
scrollable = this.parentNode === this.ownerDocument ? $(this.ownerDocument.body) : $(this);
top = scrollable.scrollTop();
correction = pad * dir;
return scrollable.stop().animate({
scrollTop: top + correction
}, 300);
}
});
};
Highlight.prototype.paddedScrollUpTo = function() {
return this.paddedScrollTo("up");
};
Highlight.prototype.paddedScrollDownTo = function() {
return this.paddedScrollTo("down");
};
return Highlight;
})();
util = {
uuid: (function() {
var counter;
counter = 0;
return function() {
return counter++;
};
})(),
getGlobal: function() {
return (function() {
return this;
})();
},
maxZIndex: function($elements) {
var all, el;
all = (function() {
var _k, _len2, _results;
_results = [];
for (_k = 0, _len2 = $elements.length; _k < _len2; _k++) {
el = $elements[_k];
if ($(el).css('position') === 'static') {
_results.push(-1);
} else {
_results.push(parseFloat($(el).css('z-index')) || -1);
}
}
return _results;
})();
return Math.max.apply(Math, all);
},
mousePosition: function(e, offsetEl) {
var offset, _ref1;
if ((_ref1 = $(offsetEl).css('position')) !== 'absolute' && _ref1 !== 'fixed' && _ref1 !== 'relative') {
offsetEl = $(offsetEl).offsetParent()[0];
}
offset = $(offsetEl).offset();
return {
top: e.pageY - offset.top,
left: e.pageX - offset.left
};
},
preventEventDefault: function(event) {
return event != null ? typeof event.preventDefault === "function" ? event.preventDefault() : void 0 : void 0;
}
};
_Annotator = this.Annotator;
DummyDocumentAccess = (function() {
function DummyDocumentAccess() {}
DummyDocumentAccess.applicable = function() {
return true;
};
DummyDocumentAccess.prototype.getPageIndex = function() {
return 0;
};
DummyDocumentAccess.prototype.getPageCount = function() {
return 1;
};
DummyDocumentAccess.prototype.getPageIndexForPos = function() {
return 0;
};
DummyDocumentAccess.prototype.isPageMapped = function() {
return true;
};
DummyDocumentAccess.prototype.scan = function() {};
return DummyDocumentAccess;
})();
Annotator = (function(_super) {
__extends(Annotator, _super);
Annotator.prototype.events = {
".annotator-adder button click": "onAdderClick",
".annotator-adder button mousedown": "onAdderMousedown"
};
Annotator.prototype.html = {
adder: '<div class="annotator-adder"><button>' + _t('Annotate') + '</button></div>',
wrapper: '<div class="annotator-wrapper"></div>'
};
Annotator.prototype.options = {
readOnly: false
};
Annotator.prototype.plugins = {};
Annotator.prototype.editor = null;
Annotator.prototype.viewer = null;
Annotator.prototype.selectedTargets = null;
Annotator.prototype.mouseIsDown = false;
Annotator.prototype.inAdderClick = false;
Annotator.prototype.canAnnotate = false;
Annotator.prototype.viewerHideTimer = null;
function Annotator(element, options) {
this.onDeleteAnnotation = __bind(this.onDeleteAnnotation, this);
this.onEditAnnotation = __bind(this.onEditAnnotation, this);
this.onAdderClick = __bind(this.onAdderClick, this);
this.onAdderMousedown = __bind(this.onAdderMousedown, this);
this._getTargetFromSelection = __bind(this._getTargetFromSelection, this);
this.checkForStartSelection = __bind(this.checkForStartSelection, this);
this.clearViewerHideTimer = __bind(this.clearViewerHideTimer, this);
this.startViewerHideTimer = __bind(this.startViewerHideTimer, this);
this.showViewer = __bind(this.showViewer, this);
this.onEditorSubmit = __bind(this.onEditorSubmit, this);
this.onEditorHide = __bind(this.onEditorHide, this);
this.showEditor = __bind(this.showEditor, this);
this.getHref = __bind(this.getHref, this);
Annotator.__super__.constructor.apply(this, arguments);
this.plugins = {};
this.selectorCreators = [];
this.anchoringStrategies = [];
if (!Annotator.supported()) {
return this;
}
if (!this.options.readOnly) {
this._setupDocumentEvents();
}
this._setupAnchorEvents();
this._setupWrapper();
this._setupDocumentAccessStrategies();
this._setupViewer()._setupEditor();
this._setupDynamicStyle();
if (!this.options.noScan) {
this._scan();
}
this.adder = $(this.html.adder).appendTo(this.wrapper).hide();
}
Annotator.prototype._setupDocumentAccessStrategies = function() {
this.documentAccessStrategies = [
{
name: "Dummy",
mapper: DummyDocumentAccess
}
];
return this;
};
Annotator.prototype._chooseAccessPolicy = function() {
var s, _k, _len2, _ref1,
_this = this;
if (this.domMapper != null) {
return;
}
_ref1 = this.documentAccessStrategies;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
s = _ref1[_k];
if (s.mapper.applicable()) {
this.documentAccessStrategy = s;
this.domMapper = new s.mapper();
this.anchors = {};
addEventListener("docPageMapped", function(evt) {
return _this._realizePage(evt.pageIndex);
});
addEventListener("docPageUnmapped", function(evt) {
return _this._virtualizePage(evt.pageIndex);
});
if (typeof s.init === "function") {
s.init();
}
return this;
}
}
};
Annotator.prototype._removeCurrentAccessPolicy = function() {
var index, list, _base;
if (this.domMapper == null) {
return;
}
list = this.documentAccessStrategies;
index = list.indexOf(this.documentAccessStrategy);
if (index !== -1) {
list.splice(index, 1);
}
if (typeof (_base = this.domMapper).destroy === "function") {
_base.destroy();
}
return delete this.domMapper;
};
Annotator.prototype._scan = function() {
var _this = this;
this._chooseAccessPolicy();
try {
this.pendingScan = this.domMapper.scan();
} catch (_error) {
this._removeCurrentAccessPolicy();
this._scan();
return;
}
if (this.pendingScan != null) {
return this.pendingScan.then(function() {
return _this.enableAnnotating();
});
} else {
return this.enableAnnotating();
}
};
Annotator.prototype._setupWrapper = function() {
this.wrapper = $(this.html.wrapper);
this.element.find('script').remove();
this.element.wrapInner(this.wrapper);
this.wrapper = this.element.find('.annotator-wrapper');
return this;
};
Annotator.prototype._setupViewer = function() {
var _this = this;
this.viewer = new Annotator.Viewer({
readOnly: this.options.readOnly
});
this.viewer.hide().on("edit", this.onEditAnnotation).on("delete", this.onDeleteAnnotation).addField({
load: function(field, annotation) {
if (annotation.text) {
$(field).html(Util.escape(annotation.text));
} else {
$(field).html("<i>" + (_t('No Comment')) + "</i>");
}
return _this.publish('annotationViewerTextField', [field, annotation]);
}
}).element.appendTo(this.wrapper).bind({
"mouseover": this.clearViewerHideTimer,
"mouseout": this.startViewerHideTimer
});
return this;
};
Annotator.prototype._setupEditor = function() {
this.editor = new Annotator.Editor();
this.editor.hide().on('hide', this.onEditorHide).on('save', this.onEditorSubmit).addField({
type: 'textarea',
label: _t('Comments') + '\u2026',
load: function(field, annotation) {
return $(field).find('textarea').val(annotation.text || '');
},
submit: function(field, annotation) {
return annotation.text = $(field).find('textarea').val();
}
});
this.editor.element.appendTo(this.wrapper);
return this;
};
Annotator.prototype._setupDocumentEvents = function() {
$(document).bind({
"mousedown": this.checkForStartSelection
});
return this;
};
Annotator.prototype._setupAnchorEvents = function() {
var _this = this;
return this.on('annotationUpdated', function(annotation) {
var anchor, _k, _len2, _ref1, _results;
_ref1 = annotation.anchors || [];
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
anchor = _ref1[_k];
_results.push(anchor.annotationUpdated());
}
return _results;
});
};
Annotator.prototype._setupDynamicStyle = function() {
var max, sel, style, x;
style = $('#annotator-dynamic-style');
if (!style.length) {
style = $('<style id="annotator-dynamic-style"></style>').appendTo(document.head);
}
sel = '*' + ((function() {
var _k, _len2, _ref1, _results;
_ref1 = ['adder', 'outer', 'notice', 'filter'];
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
x = _ref1[_k];
_results.push(":not(.annotator-" + x + ")");
}
return _results;
})()).join('');
max = util.maxZIndex($(document.body).find(sel));
max = Math.max(max, 1000);
style.text([".annotator-adder, .annotator-outer, .annotator-notice {", " z-index: " + (max + 20) + ";", "}", ".annotator-filter {", " z-index: " + (max + 10) + ";", "}"].join("\n"));
return this;
};
Annotator.prototype.destroy = function() {
var name, plugin, _ref1;
$(document).unbind({
"mouseup": this.checkForEndSelection,
"mousedown": this.checkForStartSelection
});
$('#annotator-dynamic-style').remove();
this.adder.remove();
this.viewer.destroy();
this.editor.destroy();
this.wrapper.find('.annotator-hl').each(function() {
$(this).contents().insertBefore(this);
return $(this).remove();
});
this.wrapper.contents().insertBefore(this.wrapper);
this.wrapper.remove();
this.element.data('annotator', null);
_ref1 = this.plugins;
for (name in _ref1) {
plugin = _ref1[name];
this.plugins[name].destroy();
}
return this.removeEvents();
};
Annotator.prototype.enableAnnotating = function(value, local) {
if (value == null) {
value = true;
}
if (local == null) {
local = true;
}
if (value === this.canAnnotate) {
return;
}
this.canAnnotate = value;
this.publish("enableAnnotating", value);
if (!(value || local)) {
return this.adder.hide();
}
};
Annotator.prototype.disableAnnotating = function(local) {
if (local == null) {
local = true;
}
return this.enableAnnotating(false, local);
};
Annotator.prototype.getHref = function() {
var uri;
uri = decodeURIComponent(document.location.href);
if (document.location.hash) {
uri = uri.slice(0, -1 * location.hash.length);
}
$('meta[property^="og:url"]').each(function() {
return uri = decodeURIComponent(this.content);
});
$('link[rel^="canonical"]').each(function() {
return uri = decodeURIComponent(this.href);
});
return uri;
};
Annotator.prototype.createAnnotation = function() {
var annotation;
annotation = {};
this.publish('beforeAnnotationCreated', [annotation]);
return annotation;
};
Annotator.prototype.normalizeString = function(string) {
return string.replace(/\s{2,}/g, " ");
};
Annotator.prototype.findSelector = function(selectors, type) {
var selector, _k, _len2;
for (_k = 0, _len2 = selectors.length; _k < _len2; _k++) {
selector = selectors[_k];
if (selector.type === type) {
return selector;
}
}
return null;
};
Annotator.prototype.createAnchor = function(annotation, target) {
var a, anchor, error, s, _k, _len2, _ref1, _ref2;
if (target == null) {
throw new Error("Trying to find anchor for null target!");
}
error = null;
anchor = null;
_ref1 = this.anchoringStrategies;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
s = _ref1[_k];
try {
a = s.code.call(this, annotation, target);
if (a) {
return {
result: a
};
}
} catch (_error) {
error = _error;
console.log("Strategy '" + s.name + "' has thrown an error.", (_ref2 = error.stack) != null ? _ref2 : error);
}
}
return {
error: "No strategies worked."
};
};
Annotator.prototype.setupAnnotation = function(annotation) {
var anchor, exception, pageIndex, result, t, _base, _k, _l, _len2, _ref1, _ref2, _ref3, _ref4, _ref5;
if (annotation.target == null) {
annotation.target = this.selectedTargets;
}
this.selectedTargets = [];
annotation.anchors = [];
_ref2 = (_ref1 = annotation.target) != null ? _ref1 : [];
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
t = _ref2[_k];
try {
result = this.createAnchor(annotation, t);
anchor = result.result;
if ((result.error != null) instanceof Range.RangeError) {
this.publish('rangeNormalizeFail', [annotation, result.error.range, result.error]);
}
if (anchor != null) {
t.diffHTML = anchor.diffHTML;
t.diffCaseOnly = anchor.diffCaseOnly;
annotation.anchors.push(anchor);
for (pageIndex = _l = _ref3 = anchor.startPage, _ref4 = anchor.endPage; _ref3 <= _ref4 ? _l <= _ref4 : _l >= _ref4; pageIndex = _ref3 <= _ref4 ? ++_l : --_l) {
if ((_base = this.anchors)[pageIndex] == null) {
_base[pageIndex] = [];
}
this.anchors[pageIndex].push(anchor);
}
anchor.realize();
}
} catch (_error) {
exception = _error;
console.log("Error in setupAnnotation for", annotation.id, ":", (_ref5 = exception.stack) != null ? _ref5 : exception);
}
}
return annotation;
};
Annotator.prototype.updateAnnotation = function(annotation) {
this.publish('beforeAnnotationUpdated', [annotation]);
this.publish('annotationUpdated', [annotation]);
return annotation;
};
Annotator.prototype.deleteAnnotation = function(annotation) {
var a, _k, _len2, _ref1;
if (annotation.anchors != null) {
_ref1 = annotation.anchors;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
a = _ref1[_k];
a.remove();
}
}
this.publish('annotationDeleted', [annotation]);
return annotation;
};
Annotator.prototype.loadAnnotations = function(annotations) {
var clone, loader,
_this = this;
if (annotations == null) {
annotations = [];
}
loader = function(annList) {
var n, now, _k, _len2;
if (annList == null) {
annList = [];
}
now = annList.splice(0, 10);
for (_k = 0, _len2 = now.length; _k < _len2; _k++) {
n = now[_k];
_this.setupAnnotation(n);
}
if (annList.length > 0) {
return setTimeout((function() {
return loader(annList);
}), 10);
} else {
return _this.publish('annotationsLoaded', [clone]);
}
};
clone = annotations.slice();
if (annotations.length) {
if (this.pendingScan != null) {
this.pendingScan.then(function() {
return setTimeout(function() {
return loader(annotations);
});
});
} else {
loader(annotations);
}
}
return this;
};
Annotator.prototype.dumpAnnotations = function() {
if (this.plugins['Store']) {
return this.plugins['Store'].dumpAnnotations();
} else {
console.warn(_t("Can't dump annotations without Store plugin."));
return false;
}
};
Annotator.prototype.addPlugin = function(name, options) {
var klass, _base;
if (this.plugins[name]) {
console.error(_t("You cannot have more than one instance of any plugin."));
} else {
klass = Annotator.Plugin[name];
if (typeof klass === 'function') {
this.plugins[name] = new klass(this.element[0], options);
this.plugins[name].annotator = this;
if (typeof (_base = this.plugins[name]).pluginInit === "function") {
_base.pluginInit();
}
} else {
console.error(_t("Could not load ") + name + _t(" plugin. Have you included the appropriate <script> tag?"));
}
}
return this;
};
Annotator.prototype.showEditor = function(annotation, location) {
this.editor.element.css(location);
this.editor.load(annotation);
this.publish('annotationEditorShown', [this.editor, annotation]);
return this;
};
Annotator.prototype.onEditorHide = function() {
this.publish('annotationEditorHidden', [this.editor]);
return this.enableAnnotating();
};
Annotator.prototype.onEditorSubmit = function(annotation) {
return this.publish('annotationEditorSubmit', [this.editor, annotation]);
};
Annotator.prototype.showViewer = function(annotations, location) {
this.viewer.element.css(location);
this.viewer.load(annotations);
return this.publish('annotationViewerShown', [this.viewer, annotations]);
};
Annotator.prototype.startViewerHideTimer = function() {
if (!this.viewerHideTimer) {
return this.viewerHideTimer = setTimeout(this.viewer.hide, 250);
}
};
Annotator.prototype.clearViewerHideTimer = function() {
clearTimeout(this.viewerHideTimer);
return this.viewerHideTimer = false;
};
Annotator.prototype.checkForStartSelection = function(event) {
if (!(event && this.isAnnotator(event.target))) {
this.startViewerHideTimer();
}
return this.mouseIsDown = true;
};
Annotator.prototype._getTargetFromSelection = function(selection) {
var c, description, selector, selectors, _k, _l, _len2, _len3, _ref1;
selectors = [];
_ref1 = this.selectorCreators;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
c = _ref1[_k];
description = c.describe(selection);
for (_l = 0, _len3 = description.length; _l < _len3; _l++) {
selector = description[_l];
selectors.push(selector);
}
}
return {
source: this.getHref(),
selector: selectors
};
};
Annotator.prototype.onSuccessfulSelection = function(event, immediate) {
var s;
if (immediate == null) {
immediate = false;
}
if (event == null) {
throw "Called onSuccessfulSelection without an event!";
}
if (event.segments == null) {
throw "Called onSuccessulSelection with an event with missing segments!";
}
if (!this.canAnnotate) {
return false;
}
this.selectedTargets = (function() {
var _k, _len2, _ref1, _results;
_ref1 = event.segments;
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
s = _ref1[_k];
_results.push(this._getTargetFromSelection(s));
}
return _results;
}).call(this);
if (immediate) {
this.onAdderClick(event);
} else {
this.adder.css(util.mousePosition(event, this.wrapper[0])).show();
}
return true;
};
Annotator.prototype.onFailedSelection = function(event) {
this.adder.hide();
return this.selectedTargets = [];
};
Annotator.prototype.isAnnotator = function(element) {
return !!$(element).parents().andSelf().filter('[class^=annotator-]').not(this.wrapper).length;
};
Annotator.prototype.onAdderMousedown = function(event) {
if (event != null) {
event.preventDefault();
}
this.disableAnnotating();
return this.inAdderClick = true;
};
Annotator.prototype.onAdderClick = function(event) {
var anchor, annotation, cancel, cleanup, hl, page, position, save, _k, _len2, _ref1, _ref2,
_this = this;
if (event != null) {
if (typeof event.preventDefault === "function") {
event.preventDefault();
}
}
position = this.adder.position();
this.adder.hide();
this.inAdderClick = false;
annotation = this.createAnnotation();
annotation = this.setupAnnotation(annotation);
_ref1 = annotation.anchors;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
anchor = _ref1[_k];
_ref2 = anchor.highlight;
for (page in _ref2) {
hl = _ref2[page];
hl.setTemporary(true);
}
}
save = function() {
var _l, _len3, _ref3, _ref4;
cleanup();
_ref3 = annotation.anchors;
for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
anchor = _ref3[_l];
_ref4 = anchor.highlight;
for (page in _ref4) {
hl = _ref4[page];
hl.setTemporary(false);
}
}
return _this.publish('annotationCreated', [annotation]);
};
cancel = function() {
cleanup();
return _this.deleteAnnotation(annotation);
};
cleanup = function() {
_this.unsubscribe('annotationEditorHidden', cancel);
return _this.unsubscribe('annotationEditorSubmit', save);
};
this.subscribe('annotationEditorHidden', cancel);
this.subscribe('annotationEditorSubmit', save);
return this.showEditor(annotation, position);
};
Annotator.prototype.onEditAnnotation = function(annotation) {
var cleanup, offset, update,
_this = this;
offset = this.viewer.element.position();
update = function() {
cleanup();
return _this.updateAnnotation(annotation);
};
cleanup = function() {
_this.unsubscribe('annotationEditorHidden', cleanup);
return _this.unsubscribe('annotationEditorSubmit', update);
};
this.subscribe('annotationEditorHidden', cleanup);
this.subscribe('annotationEditorSubmit', update);
this.viewer.hide();
return this.showEditor(annotation, offset);
};
Annotator.prototype.onDeleteAnnotation = function(annotation) {
this.viewer.hide();
return this.deleteAnnotation(annotation);
};
Annotator.prototype.getHighlights = function(annotations) {
var anchor, anchors, annotation, hl, page, results, _k, _l, _len2, _len3, _ref1, _ref2, _ref3;
results = [];
if (annotations != null) {
for (_k = 0, _len2 = annotations.length; _k < _len2; _k++) {
annotation = annotations[_k];
_ref1 = annotation.anchors;
for (_l = 0, _len3 = _ref1.length; _l < _len3; _l++) {
anchor = _ref1[_l];
_ref2 = anchor.highlight;
for (page in _ref2) {
hl = _ref2[page];
results.push(hl);
}
}
}
} else {
_ref3 = this.anchors;
for (page in _ref3) {
anchors = _ref3[page];
$.merge(results, (function() {
var _len4, _m, _results;
_results = [];
for (_m = 0, _len4 = anchors.length; _m < _len4; _m++) {
anchor = anchors[_m];
if (anchor.highlight[page] != null) {
_results.push(anchor.highlight[page]);
}
}
return _results;
})());
}
}
return results;
};
Annotator.prototype._realizePage = function(index) {
var anchor, _k, _len2, _ref1, _ref2, _results;
if (!this.domMapper.isPageMapped(index)) {
return;
}
_ref2 = (_ref1 = this.anchors[index]) != null ? _ref1 : [];
_results = [];
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
anchor = _ref2[_k];
_results.push(anchor.realize());
}
return _results;
};
Annotator.prototype._virtualizePage = function(index) {
var anchor, _k, _len2, _ref1, _ref2, _results;
_ref2 = (_ref1 = this.anchors[index]) != null ? _ref1 : [];
_results = [];
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
anchor = _ref2[_k];
_results.push(anchor.virtualize(index));
}
return _results;
};
Annotator.prototype.onAnchorMouseover = function(event) {
this.clearViewerHideTimer();
if (this.mouseIsDown || this.viewer.isShown()) {
return false;
}
return this.showViewer(event.data.getAnnotations(event), util.mousePosition(event, this.wrapper[0]));
};
Annotator.prototype.onAnchorMouseout = function(event) {
return this.startViewerHideTimer();
};
Annotator.prototype.onAnchorMousedown = function(event) {};
Annotator.prototype.onAnchorClick = function(event) {};
return Annotator;
})(Delegator);
Annotator.Plugin = (function(_super) {
__extends(Plugin, _super);
function Plugin(element, options) {
Plugin.__super__.constructor.apply(this, arguments);
}
Plugin.prototype.pluginInit = function() {};
Plugin.prototype.destroy = function() {
return this.removeEvents();
};
return Plugin;
})(Delegator);
g = util.getGlobal();
if (g.wgxpath != null) {
g.wgxpath.install();
}
if (g.getSelection == null) {
$.getScript('http://assets.annotateit.org/vendor/ierange.min.js');
}
if (g.JSON == null) {
$.getScript('http://assets.annotateit.org/vendor/json2.min.js');
}
if (g.Node == null) {
g.Node = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
ENTITY_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
COMMENT_NODE: 8,
DOCUMENT_NODE: 9,
DOCUMENT_TYPE_NODE: 10,
DOCUMENT_FRAGMENT_NODE: 11,
NOTATION_NODE: 12
};
}
Annotator.$ = $;
Annotator.Delegator = Delegator;
Annotator.Range = Range;
Annotator.util = util;
Annotator.Util = Util;
Annotator._instances = [];
Annotator.Highlight = Highlight;
Annotator.Anchor = Anchor;
Annotator._t = _t;
Annotator.supported = function() {
return (function() {
return !!this.getSelection;
})();
};
Annotator.noConflict = function() {
util.getGlobal().Annotator = _Annotator;
return this;
};
$.fn.annotator = function(options) {
var args;
args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var instance;
instance = $.data(this, 'annotator');
if (instance) {
return options && instance[options].apply(instance, args);
} else {
instance = new Annotator(this, options);
return $.data(this, 'annotator', instance);
}
});
};
this.Annotator = Annotator;
Annotator.Widget = (function(_super) {
__extends(Widget, _super);
Widget.prototype.classes = {
hide: 'annotator-hide',
invert: {
x: 'annotator-invert-x',
y: 'annotator-invert-y'
}
};
function Widget(element, options) {
Widget.__super__.constructor.apply(this, arguments);
this.classes = $.extend({}, Annotator.Widget.prototype.classes, this.classes);
}
Widget.prototype.destroy = function() {
this.removeEvents();
return this.element.remove();
};
Widget.prototype.checkOrientation = function() {
var current, offset, viewport, widget, window;
this.resetOrientation();
window = $(util.getGlobal());
widget = this.element.children(":first");
offset = widget.offset();
viewport = {
top: window.scrollTop(),
right: window.width() + window.scrollLeft()
};
current = {
top: offset.top,
right: offset.left + widget.width()
};
if ((current.top - viewport.top) < 0) {
this.invertY();
}
if ((current.right - viewport.right) > 0) {
this.invertX();
}
return this;
};
Widget.prototype.resetOrientation = function() {
this.element.removeClass(this.classes.invert.x).removeClass(this.classes.invert.y);
return this;
};
Widget.prototype.invertX = function() {
this.element.addClass(this.classes.invert.x);
return this;
};
Widget.prototype.invertY = function() {
this.element.addClass(this.classes.invert.y);
return this;
};
Widget.prototype.isInvertedY = function() {
return this.element.hasClass(this.classes.invert.y);
};
Widget.prototype.isInvertedX = function() {
return this.element.hasClass(this.classes.invert.x);
};
return Widget;
})(Delegator);
Annotator.Editor = (function(_super) {
__extends(Editor, _super);
Editor.prototype.events = {
"form submit": "submit",
".annotator-save click": "submit",
".annotator-cancel click": "hide",
".annotator-cancel mouseover": "onCancelButtonMouseover",
"textarea keydown": "processKeypress"
};
Editor.prototype.classes = {
hide: 'annotator-hide',
focus: 'annotator-focus'
};
Editor.prototype.html = "<div class=\"annotator-outer annotator-editor\">\n <form class=\"annotator-widget\">\n <ul class=\"annotator-listing\"></ul>\n <div class=\"annotator-controls\">\n <a href=\"#cancel\" class=\"annotator-cancel\">" + _t('Cancel') + "</a>\n<a href=\"#save\" class=\"annotator-save annotator-focus\">" + _t('Save') + "</a>\n </div>\n </form>\n</div>";
Editor.prototype.options = {};
function Editor(options) {
this.onCancelButtonMouseover = __bind(this.onCancelButtonMouseover, this);
this.processKeypress = __bind(this.processKeypress, this);
this.submit = __bind(this.submit, this);
this.load = __bind(this.load, this);
this.hide = __bind(this.hide, this);
this.show = __bind(this.show, this);
Editor.__super__.constructor.call(this, $(this.html)[0], options);
this.fields = [];
this.annotation = {};
}
Editor.prototype.show = function(event) {
util.preventEventDefault(event);
this.element.removeClass(this.classes.hide);
this.element.find('.annotator-save').addClass(this.classes.focus);
this.checkOrientation();
this.element.find(":input:first").focus();
this.setupDraggables();
return this.publish('show');
};
Editor.prototype.hide = function(event) {
util.preventEventDefault(event);
this.element.addClass(this.classes.hide);
return this.publish('hide');
};
Editor.prototype.load = function(annotation) {
var field, _k, _len2, _ref1;
this.annotation = annotation;
this.publish('load', [this.annotation]);
_ref1 = this.fields;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
field = _ref1[_k];
field.load(field.element, this.annotation);
}
return this.show();
};
Editor.prototype.submit = function(event) {
var field, _k, _len2, _ref1;
util.preventEventDefault(event);
_ref1 = this.fields;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
field = _ref1[_k];
field.submit(field.element, this.annotation);
}
this.publish('save', [this.annotation]);
return this.hide();
};
Editor.prototype.addField = function(options) {
var element, field, input;
field = $.extend({
id: 'annotator-field-' + util.uuid(),
type: 'input',
label: '',
load: function() {},
submit: function() {}
}, options);
input = null;
element = $('<li class="annotator-item" />');
field.element = element[0];
switch (field.type) {
case 'textarea':
input = $('<textarea />');
break;
case 'input':
case 'checkbox':
input = $('<input />');
break;
case 'select':
input = $('<select />');
}
element.append(input);
input.attr({
id: field.id,
placeholder: field.label
});
if (field.type === 'checkbox') {
input[0].type = 'checkbox';
element.addClass('annotator-checkbox');
element.append($('<label />', {
"for": field.id,
html: field.label
}));
}
this.element.find('ul:first').append(element);
this.fields.push(field);
return field.element;
};
Editor.prototype.checkOrientation = function() {
var controls, list;
Editor.__super__.checkOrientation.apply(this, arguments);
list = this.element.find('ul');
controls = this.element.find('.annotator-controls');
if (this.element.hasClass(this.classes.invert.y)) {
controls.insertBefore(list);
} else if (controls.is(':first-child')) {
controls.insertAfter(list);
}
return this;
};
Editor.prototype.processKeypress = function(event) {
if (event.keyCode === 27) {
return this.hide();
} else if (event.keyCode === 13 && !event.shiftKey) {
return this.submit();
}
};
Editor.prototype.onCancelButtonMouseover = function() {
return this.element.find('.' + this.classes.focus).removeClass(this.classes.focus);
};
Editor.prototype.setupDraggables = function() {
var classes, controls, cornerItem, editor, mousedown, onMousedown, onMousemove, onMouseup, resize, textarea, throttle,
_this = this;
this.element.find('.annotator-resize').remove();
if (this.element.hasClass(this.classes.invert.y)) {
cornerItem = this.element.find('.annotator-item:last');
} else {
cornerItem = this.element.find('.annotator-item:first');
}
if (cornerItem) {
$('<span class="annotator-resize"></span>').appendTo(cornerItem);
}
mousedown = null;
classes = this.classes;
editor = this.element;
textarea = null;
resize = editor.find('.annotator-resize');
controls = editor.find('.annotator-controls');
throttle = false;
onMousedown = function(event) {
if (event.target === this) {
mousedown = {
element: this,
top: event.pageY,
left: event.pageX
};
textarea = editor.find('textarea:first');
$(window).bind({
'mouseup.annotator-editor-resize': onMouseup,
'mousemove.annotator-editor-resize': onMousemove
});
return event.preventDefault();
}
};
onMouseup = function() {
mousedown = null;
return $(window).unbind('.annotator-editor-resize');
};
onMousemove = function(event) {
var diff, directionX, directionY, height, width;
if (mousedown && throttle === false) {
diff = {
top: event.pageY - mousedown.top,
left: event.pageX - mousedown.left
};
if (mousedown.element === resize[0]) {
height = textarea.outerHeight();
width = textarea.outerWidth();
directionX = editor.hasClass(classes.invert.x) ? -1 : 1;
directionY = editor.hasClass(classes.invert.y) ? 1 : -1;
textarea.height(height + (diff.top * directionY));
textarea.width(width + (diff.left * directionX));
if (textarea.outerHeight() !== height) {
mousedown.top = event.pageY;
}
if (textarea.outerWidth() !== width) {
mousedown.left = event.pageX;
}
} else if (mousedown.element === controls[0]) {
editor.css({
top: parseInt(editor.css('top'), 10) + diff.top,
left: parseInt(editor.css('left'), 10) + diff.left
});
mousedown.top = event.pageY;
mousedown.left = event.pageX;
}
throttle = true;
return setTimeout(function() {
return throttle = false;
}, 1000 / 60);
}
};
resize.bind('mousedown', onMousedown);
return controls.bind('mousedown', onMousedown);
};
return Editor;
})(Annotator.Widget);
Annotator.Viewer = (function(_super) {
__extends(Viewer, _super);
Viewer.prototype.events = {
".annotator-edit click": "onEditClick",
".annotator-delete click": "onDeleteClick"
};
Viewer.prototype.classes = {
hide: 'annotator-hide',
showControls: 'annotator-visible'
};
Viewer.prototype.html = {
element: "<div class=\"annotator-outer annotator-viewer\">\n <ul class=\"annotator-widget annotator-listing\"></ul>\n</div>",
item: "<li class=\"annotator-annotation annotator-item\">\n <span class=\"annotator-controls\">\n <a href=\"#\" title=\"View as webpage\" class=\"annotator-link\">View as webpage</a>\n <button title=\"Edit\" class=\"annotator-edit\">Edit</button>\n <button title=\"Delete\" class=\"annotator-delete\">Delete</button>\n </span>\n</li>"
};
Viewer.prototype.options = {
readOnly: false
};
function Viewer(options) {
this.onDeleteClick = __bind(this.onDeleteClick, this);
this.onEditClick = __bind(this.onEditClick, this);
this.load = __bind(this.load, this);
this.hide = __bind(this.hide, this);
this.show = __bind(this.show, this);
Viewer.__super__.constructor.call(this, $(this.html.element)[0], options);
this.item = $(this.html.item)[0];
this.fields = [];
this.annotations = [];
}
Viewer.prototype.show = function(event) {
var controls,
_this = this;
util.preventEventDefault(event);
controls = this.element.find('.annotator-controls').addClass(this.classes.showControls);
setTimeout((function() {
return controls.removeClass(_this.classes.showControls);
}), 500);
this.element.removeClass(this.classes.hide);
return this.checkOrientation().publish('show');
};
Viewer.prototype.isShown = function() {
return !this.element.hasClass(this.classes.hide);
};
Viewer.prototype.hide = function(event) {
util.preventEventDefault(event);
this.element.addClass(this.classes.hide);
return this.publish('hide');
};
Viewer.prototype.load = function(annotations) {
var annotation, controller, controls, del, edit, element, field, item, link, links, list, _k, _l, _len2, _len3, _ref1, _ref2;
this.annotations = annotations || [];
list = this.element.find('ul:first').empty();
_ref1 = this.annotations;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
annotation = _ref1[_k];
item = $(this.item).clone().appendTo(list).data('annotation', annotation);
controls = item.find('.annotator-controls');
link = controls.find('.annotator-link');
edit = controls.find('.annotator-edit');
del = controls.find('.annotator-delete');
links = new LinkParser(annotation.links || []).get('alternate', {
'type': 'text/html'
});
if (links.length === 0 || (links[0].href == null)) {
link.remove();
} else {
link.attr('href', links[0].href);
}
if (this.options.readOnly) {
edit.remove();
del.remove();
} else {
controller = {
showEdit: function() {
return edit.removeAttr('disabled');
},
hideEdit: function() {
return edit.attr('disabled', 'disabled');
},
showDelete: function() {
return del.removeAttr('disabled');
},
hideDelete: function() {
return del.attr('disabled', 'disabled');
}
};
}
_ref2 = this.fields;
for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) {
field = _ref2[_l];
element = $(field.element).clone().appendTo(item)[0];
field.load(element, annotation, controller);
}
}
this.publish('load', [this.annotations]);
return this.show();
};
Viewer.prototype.addField = function(options) {
var field;
field = $.extend({
load: function() {}
}, options);
field.element = $('<div />')[0];
this.fields.push(field);
field.element;
return this;
};
Viewer.prototype.onEditClick = function(event) {
return this.onButtonClick(event, 'edit');
};
Viewer.prototype.onDeleteClick = function(event) {
return this.onButtonClick(event, 'delete');
};
Viewer.prototype.onButtonClick = function(event, type) {
var item;
item = $(event.target).parents('.annotator-annotation');
return this.publish(type, [item.data('annotation')]);
};
return Viewer;
})(Annotator.Widget);
LinkParser = (function() {
function LinkParser(data) {
this.data = data;
}
LinkParser.prototype.get = function(rel, cond) {
var d, k, keys, match, v, _k, _len2, _ref1, _results;
if (cond == null) {
cond = {};
}
cond = $.extend({}, cond, {
rel: rel
});
keys = (function() {
var _results;
_results = [];
for (k in cond) {
if (!__hasProp.call(cond, k)) continue;
v = cond[k];
_results.push(k);
}
return _results;
})();
_ref1 = this.data;
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
d = _ref1[_k];
match = keys.reduce((function(m, k) {
return m && (d[k] === cond[k]);
}), true);
if (match) {
_results.push(d);
} else {
continue;
}
}
return _results;
};
return LinkParser;
})();
Annotator = Annotator || {};
Annotator.Notification = (function(_super) {
__extends(Notification, _super);
Notification.prototype.events = {
"click": "hide"
};
Notification.prototype.options = {
html: "<div class='annotator-notice'></div>",
classes: {
show: "annotator-notice-show",
info: "annotator-notice-info",
success: "annotator-notice-success",
error: "annotator-notice-error"
}
};
function Notification(options) {
this.hide = __bind(this.hide, this);
this.show = __bind(this.show, this);
Notification.__super__.constructor.call(this, $(this.options.html).appendTo(document.body)[0], options);
}
Notification.prototype.show = function(message, status) {
if (status == null) {
status = Annotator.Notification.INFO;
}
this.currentStatus = status;
$(this.element).addClass(this.options.classes.show).addClass(this.options.classes[this.currentStatus]).html(Util.escape(message || ""));
setTimeout(this.hide, 5000);
return this;
};
Notification.prototype.hide = function() {
if (this.currentStatus == null) {
this.currentStatus = Annotator.Notification.INFO;
}
$(this.element).removeClass(this.options.classes.show).removeClass(this.options.classes[this.currentStatus]);
return this;
};
return Notification;
})(Delegator);
Annotator.Notification.INFO = 'show';
Annotator.Notification.SUCCESS = 'success';
Annotator.Notification.ERROR = 'error';
$(function() {
var notification;
notification = new Annotator.Notification;
Annotator.showNotification = notification.show;
return Annotator.hideNotification = notification.hide;
});
}).call(this);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment