Commit e8128aee authored by Nick Stenning's avatar Nick Stenning

Convert Annotator customisations into monkeypatches

This commit moves most customisations of the upstream Annotator v1.2.x
into annotator_monkey.coffee, which monkeypatches the Annotator object.
Customisations required by the anchoring subsystem are moved into the
"enhanced anchoring" plugin.

With this change, we should be able to replace our inlined Annotator
source with the built JavaScript for upstream Annotator!
parent 8034072c
...@@ -12,6 +12,8 @@ class Annotator extends Delegator ...@@ -12,6 +12,8 @@ class Annotator extends Delegator
events: events:
".annotator-adder button click": "onAdderClick" ".annotator-adder button click": "onAdderClick"
".annotator-adder button mousedown": "onAdderMousedown" ".annotator-adder button mousedown": "onAdderMousedown"
".annotator-hl mouseover": "onHighlightMouseover"
".annotator-hl mouseout": "startViewerHideTimer"
html: html:
adder: '<div class="annotator-adder"><button>' + _t('Annotate') + '</button></div>' adder: '<div class="annotator-adder"><button>' + _t('Annotate') + '</button></div>'
...@@ -26,11 +28,11 @@ class Annotator extends Delegator ...@@ -26,11 +28,11 @@ class Annotator extends Delegator
viewer: null viewer: null
selectedTargets: null selectedRanges: null
mouseIsDown: false mouseIsDown: false
inAdderClick: false ignoreMouseup: false
viewerHideTimer: null viewerHideTimer: null
...@@ -138,6 +140,7 @@ class Annotator extends Delegator ...@@ -138,6 +140,7 @@ class Annotator extends Delegator
# Returns itself for chaining. # Returns itself for chaining.
_setupDocumentEvents: -> _setupDocumentEvents: ->
$(document).bind({ $(document).bind({
"mouseup": this.checkForEndSelection
"mousedown": this.checkForStartSelection "mousedown": this.checkForStartSelection
}) })
this this
...@@ -205,14 +208,51 @@ class Annotator extends Delegator ...@@ -205,14 +208,51 @@ class Annotator extends Delegator
if idx != -1 if idx != -1
Annotator._instances.splice(idx, 1) Annotator._instances.splice(idx, 1)
# Public: 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 = Util.getGlobal().getSelection()
ranges = []
rangesToIgnore = []
unless selection.isCollapsed
ranges = for i in [0...selection.rangeCount]
r = selection.getRangeAt(i)
browserRange = new Range.BrowserRange(r)
normedRange = browserRange.normalize().limit(@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
# Utility function to get the decoded form of the document URI normedRange
getHref: =>
uri = decodeURIComponent document.location.href # BrowserRange#normalize() modifies the DOM structure and deselects the
if document.location.hash then uri = uri.slice 0, (-1 * location.hash.length) # underlying text as a result. So here we remove the selected ranges and
$('meta[property^="og:url"]').each -> uri = decodeURIComponent this.content # reapply the new ones.
$('link[rel^="canonical"]').each -> uri = decodeURIComponent this.href selection.removeAllRanges()
return uri
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
# Public: Creates and returns a new annotation object. Publishes the # Public: Creates and returns a new annotation object. Publishes the
# 'beforeAnnotationCreated' event to allow the new annotation to be modified. # 'beforeAnnotationCreated' event to allow the new annotation to be modified.
...@@ -233,8 +273,7 @@ class Annotator extends Delegator ...@@ -233,8 +273,7 @@ class Annotator extends Delegator
# Public: Initialises an annotation either from an object representation or # Public: Initialises an annotation either from an object representation or
# an annotation created with Annotator#createAnnotation(). It finds the # an annotation created with Annotator#createAnnotation(). It finds the
# selected range and higlights the selection in the DOM, extracts the # selected range and higlights the selection in the DOM.
# quoted text and serializes the range.
# #
# annotation - An annotation Object to initialise. # annotation - An annotation Object to initialise.
# #
...@@ -252,29 +291,35 @@ class Annotator extends Delegator ...@@ -252,29 +291,35 @@ class Annotator extends Delegator
# #
# Returns the initialised annotation. # Returns the initialised annotation.
setupAnnotation: (annotation) -> setupAnnotation: (annotation) ->
# If this is a new annotation, we might have to add the targets root = @wrapper[0]
annotation.target ?= @selectedTargets annotation.ranges or= @selectedRanges
@selectedTargets = []
annotation.anchors = []
for t in annotation.target ? [] normedRanges = []
for r in annotation.ranges
try try
# Create an anchor for this target normedRanges.push(Range.sniff(r).normalize(root))
result = this.anchoring.createAnchor annotation, t catch e
anchor = result.result if e instanceof Range.RangeError
if result.error? instanceof Range.RangeError this.publish('rangeNormalizeFail', [annotation, r, e])
this.publish 'rangeNormalizeFail', [annotation, result.error.range, result.error] else
if anchor? # Oh Javascript, why you so crap? This will lose the traceback.
t.diffHTML = anchor.diffHTML throw e
t.diffCaseOnly = anchor.diffCaseOnly
annotation.quote = []
# Store this anchor for the annotation annotation.ranges = []
annotation.anchors.push anchor annotation.highlights = []
catch exception for normed in normedRanges
console.log "Error in setupAnnotation for", annotation.id, annotation.quote.push $.trim(normed.text())
":", exception.stack ? exception annotation.ranges.push normed.serialize(@wrapper[0], '.annotator-hl')
$.merge annotation.highlights, this.highlightRange(normed)
# Join all the quotes into one string.
annotation.quote = annotation.quote.join(' / ')
# Save the annotation data on each highlighter element.
$(annotation.highlights).data('annotation', annotation)
$(annotation.highlights).attr('data-annotation-id', annotation.id)
annotation annotation
...@@ -308,9 +353,10 @@ class Annotator extends Delegator ...@@ -308,9 +353,10 @@ class Annotator extends Delegator
# #
# Returns deleted annotation. # Returns deleted annotation.
deleteAnnotation: (annotation) -> deleteAnnotation: (annotation) ->
if annotation.anchors? if annotation.highlights?
for a in annotation.anchors for h in annotation.highlights when h.parentNode?
a.remove() child = h.childNodes[0]
$(h).replaceWith(h.childNodes)
this.publish('annotationDeleted', [annotation]) this.publish('annotationDeleted', [annotation])
annotation annotation
...@@ -341,16 +387,8 @@ class Annotator extends Delegator ...@@ -341,16 +387,8 @@ class Annotator extends Delegator
this.publish 'annotationsLoaded', [clone] this.publish 'annotationsLoaded', [clone]
clone = annotations.slice() 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 loader annotations
this this
# Public: Calls the Store#dumpAnnotations() method. # Public: Calls the Store#dumpAnnotations() method.
...@@ -363,6 +401,38 @@ class Annotator extends Delegator ...@@ -363,6 +401,38 @@ class Annotator extends Delegator
console.warn(_t("Can't dump annotations without Store plugin.")) console.warn(_t("Can't dump annotations without Store plugin."))
return false return false
# 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.
for node in normedRange.textNodes() when not white.test(node.nodeValue)
$(node).wrapAll(hl).parent().show()[0]
# Public: highlight a list of ranges
#
# normedRanges - An array of NormalizedRanges to be highlighted.
# cssClass - A CSS class to use for the highlight (default: 'annotator-hl')
#
# Returns an array of highlight Elements.
highlightRanges: (normedRanges, cssClass='annotator-hl') ->
highlights = []
for r in normedRanges
$.merge highlights, this.highlightRange(r, cssClass)
highlights
# Public: Registers a plugin with the Annotator. A plugin can only be # Public: Registers a plugin with the Annotator. A plugin can only be
# registered once. The plugin will be instantiated in the following order. # registered once. The plugin will be instantiated in the following order.
# #
...@@ -417,11 +487,13 @@ class Annotator extends Delegator ...@@ -417,11 +487,13 @@ class Annotator extends Delegator
this this
# Callback method called when the @editor fires the "hide" event. Itself # Callback method called when the @editor fires the "hide" event. Itself
# publishes the 'annotationEditorHidden' event # publishes the 'annotationEditorHidden' event and resets the @ignoreMouseup
# property to allow listening to mouse events.
# #
# Returns nothing. # Returns nothing.
onEditorHide: => onEditorHide: =>
this.publish('annotationEditorHidden', [@editor]) this.publish('annotationEditorHidden', [@editor])
@ignoreMouseup = false
# Callback method called when the @editor fires the "save" event. Itself # Callback method called when the @editor fires the "save" event. Itself
# publishes the 'annotationEditorSubmit' event and creates/updates the # publishes the 'annotationEditorSubmit' event and creates/updates the
...@@ -481,52 +553,36 @@ class Annotator extends Delegator ...@@ -481,52 +553,36 @@ class Annotator extends Delegator
this.startViewerHideTimer() this.startViewerHideTimer()
@mouseIsDown = true @mouseIsDown = true
# This method is to be called by the mechanisms responsible for # Annotator#element callback. Checks to see if a selection has been made
# triggering annotation (and highlight) creation. # on mouseup and if so displays the Annotator#adder. If @ignoreMouseup is
# # set will do nothing. Also resets the @mouseIsDown property.
# event - any event which has triggered this. #
# The following fields are used: # event - A mouseup Event object.
# targets: an array of targets, which should be used to anchor the #
# newly created annotation # Returns nothing.
# pageX and pageY: if the adder button is shown, use there coordinates checkForEndSelection: (event) =>
# @mouseIsDown = false
# 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 # This prevents the note image from jumping away on the mouseup
# of a click on icon.
if @ignoreMouseup
return
# This is called to create a target from a raw selection, # Get the currently selected ranges.
# using selectors created by the registered selector creators @selectedRanges = this.getSelectedRanges()
_getTargetFromSelection: (selection) ->
source: @getHref()
selector: @anchoring.getSelectorsFromSelection(selection)
onFailedSelection: (event) -> for range in @selectedRanges
@adder.hide() container = range.commonAncestor
@selectedTargets = [] if $(container).hasClass('annotator-hl')
container = $(container).parents('[class!=annotator-hl]')[0]
return if this.isAnnotator(container)
if event and @selectedRanges.length
@adder
.css(Util.mousePosition(event, @wrapper[0]))
.show()
else
@adder.hide()
# Public: Determines if the provided element is part of the annotator plugin. # Public: Determines if the provided element is part of the annotator plugin.
# Useful for ignoring mouse actions on the annotator elements. # Useful for ignoring mouse actions on the annotator elements.
...@@ -545,14 +601,40 @@ class Annotator extends Delegator ...@@ -545,14 +601,40 @@ class Annotator extends Delegator
isAnnotator: (element) -> isAnnotator: (element) ->
!!$(element).parents().addBack().filter('[class^=annotator-]').not(@wrapper).length !!$(element).parents().addBack().filter('[class^=annotator-]').not(@wrapper).length
# Annotator#element callback. # Annotator#element callback. Displays viewer with all annotations
# associated with highlight Elements under the cursor.
#
# event - A mouseover Event object.
#
# Returns nothing.
onHighlightMouseover: (event) =>
# Cancel any pending hiding of the viewer.
this.clearViewerHideTimer()
# Don't do anything if we're making a selection
return false if @mouseIsDown
# If the viewer is already shown, hide it first
@viewer.hide() if @viewer.isShown()
annotations = $(event.target)
.parents('.annotator-hl')
.addBack()
.map( -> return $(this).data("annotation"))
.toArray()
# Now show the viewer with the wanted annotations
this.showViewer(annotations, Util.mousePosition(event, @wrapper[0]))
# Annotator#element callback. Sets @ignoreMouseup to true to prevent
# the annotation selection events firing when the adder is clicked.
# #
# event - A mousedown Event object # event - A mousedown Event object
# #
# Returns nothing. # Returns nothing.
onAdderMousedown: (event) => onAdderMousedown: (event) =>
event?.preventDefault() event?.preventDefault()
@inAdderClick = true @ignoreMouseup = true
# Annotator#element callback. Displays the @editor in place of the @adder and # 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 # loads in a newly created annotation Object. The click event is used as well
...@@ -567,25 +649,18 @@ class Annotator extends Delegator ...@@ -567,25 +649,18 @@ class Annotator extends Delegator
# Hide the adder # Hide the adder
position = @adder.position() position = @adder.position()
@adder.hide() @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 # Show a temporary highlight so the user can see what they selected
for anchor in annotation.anchors # Also extract the quotation and serialize the ranges
for page, hl of anchor.highlight annotation = this.setupAnnotation(this.createAnnotation())
hl.setTemporary true $(annotation.highlights).addClass('annotator-hl-temporary')
# Subscribe to the editor events
# Make the highlights permanent if the annotation is saved # Make the highlights permanent if the annotation is saved
save = => save = =>
do cleanup do cleanup
for anchor in annotation.anchors $(annotation.highlights).removeClass('annotator-hl-temporary')
for page, hl of anchor.highlight
hl.setTemporary false
# Fire annotationCreated events so that plugins can react to them # Fire annotationCreated events so that plugins can react to them
this.publish('annotationCreated', [annotation]) this.publish('annotationCreated', [annotation])
...@@ -646,24 +721,6 @@ class Annotator extends Delegator ...@@ -646,24 +721,6 @@ class Annotator extends Delegator
# Delete highlight elements. # Delete highlight elements.
this.deleteAnnotation annotation this.deleteAnnotation annotation
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 # Create namespace for Annotator plugins
class Annotator.Plugin extends Delegator class Annotator.Plugin extends Delegator
constructor: (element, options) -> constructor: (element, options) ->
...@@ -674,9 +731,8 @@ class Annotator.Plugin extends Delegator ...@@ -674,9 +731,8 @@ class Annotator.Plugin extends Delegator
# Sniff the browser environment and attempt to add missing functionality. # Sniff the browser environment and attempt to add missing functionality.
g = Util.getGlobal() g = Util.getGlobal()
# Checks for the presence of wicked-good-xpath if not g.document?.evaluate?
# It is always safe to install it, it'll not overwrite existing functions $.getScript('http://assets.annotateit.org/vendor/xpath.min.js')
if g.wgxpath? then g.wgxpath.install()
if not g.getSelection? if not g.getSelection?
$.getScript('http://assets.annotateit.org/vendor/ierange.min.js') $.getScript('http://assets.annotateit.org/vendor/ierange.min.js')
......
# Disable Annotator's default highlight events
delete Annotator.prototype.events[".annotator-hl mouseover"]
delete Annotator.prototype.events[".annotator-hl mouseout"]
# Disable Annotator's default selection detection
Annotator.prototype._setupDocumentEvents = ->
$(document).bind({
# omit the "mouseup" check
"mousedown": this.checkForStartSelection
})
this
# Utility function to get the decoded form of the document URI
Annotator.prototype.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
# Override setupAnnotation
Annotator.prototype.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.anchoring.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
catch exception
console.log "Error in setupAnnotation for", annotation.id,
":", exception.stack ? exception
annotation
# Override deleteAnnotation to deal with anchors, not highlights.
Annotator.prototype.deleteAnnotation = (annotation) ->
if annotation.anchors?
for a in annotation.anchors
a.remove()
this.publish('annotationDeleted', [annotation])
annotation
# 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.
Annotator.prototype.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
# This is called to create a target from a raw selection,
# using selectors created by the registered selector creators
Annotator.prototype._getTargetFromSelection = (selection) ->
source: @getHref()
selector: @anchoring.getSelectorsFromSelection(selection)
Annotator.prototype.onFailedSelection = (event) ->
@adder.hide()
@selectedTargets = []
# Override the onAdderClick event handler.
#
# N.B. (Convenient) CoffeeScript horror. The original handler is bound to the
# instance using =>, which means that despite the fact this has a single arrow,
# it will end up bound to the instance regardless.
Annotator.prototype.onAdderClick = (event) ->
event?.preventDefault()
# Hide the adder
position = @adder.position()
@adder.hide()
@ignoreMouseup = 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)
this.subscribe('annotationEditorHidden', cancel)
this.subscribe('annotationEditorSubmit', save)
# Display the editor.
this.showEditor(annotation, position)
# Provide a bunch of event handlers for anchors. N.B. These aren't explicitly
# bound to the instances, so can't actually be used as event handlers. They must
# be bound as closures:
#
# elem.on('mouseover', (e) => annotator.onAnchorMouseover(e))
#
Annotator.prototype.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])
Annotator.prototype.onAnchorMouseout = (event) ->
this.startViewerHideTimer()
Annotator.prototype.onAnchorMousedown = ->
Annotator.prototype.onAnchorClick = ->
# Checks for the presence of wicked-good-xpath
# It is always safe to install it, it'll not overwrite existing functions
g = Annotator.Util.getGlobal()
if g.wgxpath? then g.wgxpath.install()
...@@ -163,8 +163,20 @@ class Annotator.Plugin.EnhancedAnchoring extends Annotator.Plugin ...@@ -163,8 +163,20 @@ class Annotator.Plugin.EnhancedAnchoring extends Annotator.Plugin
@_setupDocumentAccessStrategies() @_setupDocumentAccessStrategies()
this._setupAnchorEvents() this._setupAnchorEvents()
self = this
@annotator.anchoring = this @annotator.anchoring = this
# Override loadAnnotations to account for the possibility that the anchoring
# plugin is currently scanning the page.
_loadAnnotations = Annotator.prototype.loadAnnotations
Annotator.prototype.loadAnnotations = (annotations=[]) ->
if self.pendingScan?
# Schedule annotation load for when scan has finished
self.pendingScan.then =>
_loadAnnotations.call(this, annotations)
else
_loadAnnotations.call(this, annotations)
# PUBLIC Try to find the right anchoring point for a given target # PUBLIC Try to find the right anchoring point for a given target
# #
# Returns an Anchor object if succeeded, null otherwise # Returns an Anchor object if succeeded, null otherwise
......
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