Commit a4ce9479 authored by gergely-ujvari's avatar gergely-ujvari

Merge pull request #1866 from hypothesis/vanilla-annotator

Replace Annotator with dist build
parents 16f4d24a a14c60a1
This diff is collapsed.
# 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: Destroy the instance, unbinding all events.
#
# Returns nothing.
destroy: ->
this.removeEvents()
# 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) ->
closure = => this[functionName].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(', ')}")
This diff is collapsed.
# Save references to Range and Util (because we call Annotator.noConflict() when
# bootstrapping)
Range = Annotator.Range
Util = Annotator.Util
# Disable Annotator's default highlight events
delete Annotator.prototype.events[".annotator-hl mouseover"]
delete Annotator.prototype.events[".annotator-hl mouseout"]
......@@ -188,3 +194,104 @@ Annotator.prototype.onAnchorClick = ->
# It is always safe to install it, it'll not overwrite existing functions
g = Annotator.Util.getGlobal()
if g.wgxpath? then g.wgxpath.install()
Range.BrowserRange.prototype.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 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
This diff is collapsed.
Range.BrowserRange.prototype.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)
# 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: decides whether node A is an ancestor of node B.
#
# This function purposefully ignores the native browser function for this,
# because it acts weird in PhantomJS.
# Issue: https://github.com/ariya/phantomjs/issues/11479
Util.contains = (parent, child) ->
node = child
while node?
if node is parent then return true
node = node.parentNode
return false
# 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
# Public: read out the text value of a range using the selection API
#
# This method selects the specified range, and asks for the string
# value of the selection. What this returns is very close to what the user
# actually sees.
Util.readRangeViaSelection = (range) ->
sel = Util.getGlobal().getSelection() # Get the browser selection object
sel.removeAllRanges() # clear the selection
sel.addRange range.toRange() # Select the range
sel.toString() # Read out the selection
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;')
Util.uuid = (-> counter = 0; -> counter++)()
Util.getGlobal = -> (-> this)()
# Return the maximum z-index of any element in $elements (a jQuery collection).
Util.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)
Util.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.
Util.preventEventDefault = (event) ->
event?.preventDefault?()
# 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) =>
Annotator.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) =>
Annotator.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 = $(Annotator.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
/*
** Annotator v1.2.9-dev-b091a74
** https://github.com/okfn/annotator/
**
** Copyright 2015, the Annotator project contributors.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2015-01-19 11:35:26Z
*/
//
// Generated by CoffeeScript 1.6.3
(function() {
var base64Decode, base64UrlDecode, createDateFromISO8601, parseToken,
__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; };
createDateFromISO8601 = function(string) {
var d, date, offset, regexp, time, _ref;
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);
if (d[3]) {
date.setMonth(d[3] - 1);
}
if (d[5]) {
date.setDate(d[5]);
}
if (d[7]) {
date.setHours(d[7]);
}
if (d[8]) {
date.setMinutes(d[8]);
}
if (d[10]) {
date.setSeconds(d[10]);
}
if (d[12]) {
date.setMilliseconds(Number("0." + d[12]) * 1000);
}
if (d[14]) {
offset = (Number(d[16]) * 60) + Number(d[17]);
offset *= (_ref = d[15] === '-') != null ? _ref : {
1: -1
};
}
offset -= date.getTimezoneOffset();
time = Number(date) + (offset * 60 * 1000);
date.setTime(Number(time));
return date;
};
base64Decode = function(data) {
var ac, b64, bits, dec, h1, h2, h3, h4, i, o1, o2, o3, tmp_arr;
if (typeof atob !== "undefined" && atob !== null) {
return atob(data);
} else {
b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
i = 0;
ac = 0;
dec = "";
tmp_arr = [];
if (!data) {
return data;
}
data += '';
while (i < data.length) {
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);
}
}
return tmp_arr.join('');
}
};
base64UrlDecode = function(data) {
var i, m, _i, _ref;
m = data.length % 4;
if (m !== 0) {
for (i = _i = 0, _ref = 4 - m; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
data += '=';
}
}
data = data.replace(/-/g, '+');
data = data.replace(/_/g, '/');
return base64Decode(data);
};
parseToken = function(token) {
var head, payload, sig, _ref;
_ref = token.split('.'), head = _ref[0], payload = _ref[1], sig = _ref[2];
return JSON.parse(base64UrlDecode(payload));
};
Annotator.Plugin.Auth = (function(_super) {
__extends(Auth, _super);
Auth.prototype.options = {
token: null,
tokenUrl: '/auth/token',
autoFetch: true
};
function Auth(element, options) {
Auth.__super__.constructor.apply(this, arguments);
this.waitingForToken = [];
if (this.options.token) {
this.setToken(this.options.token);
} else {
this.requestToken();
}
}
Auth.prototype.requestToken = function() {
var _this = this;
this.requestInProgress = true;
return $.ajax({
url: this.options.tokenUrl,
dataType: 'text',
xhrFields: {
withCredentials: true
}
}).done(function(data, status, xhr) {
return _this.setToken(data);
}).fail(function(xhr, status, err) {
var msg;
msg = Annotator._t("Couldn't get auth token:");
console.error("" + msg + " " + err, xhr);
return Annotator.showNotification("" + msg + " " + xhr.responseText, Annotator.Notification.ERROR);
}).always(function() {
return _this.requestInProgress = false;
});
};
Auth.prototype.setToken = function(token) {
var _results,
_this = this;
this.token = token;
this._unsafeToken = parseToken(token);
if (this.haveValidToken()) {
if (this.options.autoFetch) {
this.refreshTimeout = setTimeout((function() {
return _this.requestToken();
}), (this.timeToExpiry() - 2) * 1000);
}
this.updateHeaders();
_results = [];
while (this.waitingForToken.length > 0) {
_results.push(this.waitingForToken.pop()(this._unsafeToken));
}
return _results;
} else {
console.warn(Annotator._t("Didn't get a valid token."));
if (this.options.autoFetch) {
console.warn(Annotator._t("Getting a new token in 10s."));
return setTimeout((function() {
return _this.requestToken();
}), 10 * 1000);
}
}
};
Auth.prototype.haveValidToken = function() {
var allFields;
allFields = this._unsafeToken && this._unsafeToken.issuedAt && this._unsafeToken.ttl && this._unsafeToken.consumerKey;
if (allFields && this.timeToExpiry() > 0) {
return true;
} else {
return false;
}
};
Auth.prototype.timeToExpiry = function() {
var expiry, issue, now, timeToExpiry;
now = new Date().getTime() / 1000;
issue = createDateFromISO8601(this._unsafeToken.issuedAt).getTime() / 1000;
expiry = issue + this._unsafeToken.ttl;
timeToExpiry = expiry - now;
if (timeToExpiry > 0) {
return timeToExpiry;
} else {
return 0;
}
};
Auth.prototype.updateHeaders = function() {
var current;
current = this.element.data('annotator:headers');
return this.element.data('annotator:headers', $.extend(current, {
'x-annotator-auth-token': this.token
}));
};
Auth.prototype.withToken = function(callback) {
if (callback == null) {
return;
}
if (this.haveValidToken()) {
return callback(this._unsafeToken);
} else {
this.waitingForToken.push(callback);
if (!this.requestInProgress) {
return this.requestToken();
}
}
};
return Auth;
})(Annotator.Plugin);
}).call(this);
//
//# sourceMappingURL=annotator.auth.map
\ No newline at end of file
/*
** Annotator v1.2.9-dev-b091a74
** https://github.com/okfn/annotator/
**
** Copyright 2015, the Annotator project contributors.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2015-01-19 11:35:26Z
*/
//
// Generated by CoffeeScript 1.6.3
(function() {
var _ref,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__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; };
Annotator.Plugin.Document = (function(_super) {
var $;
__extends(Document, _super);
function Document() {
this._getFavicon = __bind(this._getFavicon, this);
this._getLinks = __bind(this._getLinks, this);
this._getTitle = __bind(this._getTitle, this);
this._getMetaTags = __bind(this._getMetaTags, this);
this._getEprints = __bind(this._getEprints, this);
this._getPrism = __bind(this._getPrism, this);
this._getDublinCore = __bind(this._getDublinCore, this);
this._getTwitter = __bind(this._getTwitter, this);
this._getFacebook = __bind(this._getFacebook, this);
this._getHighwire = __bind(this._getHighwire, this);
this.getDocumentMetadata = __bind(this.getDocumentMetadata, this);
this.beforeAnnotationCreated = __bind(this.beforeAnnotationCreated, this);
this.uris = __bind(this.uris, this);
this.uri = __bind(this.uri, this);
_ref = Document.__super__.constructor.apply(this, arguments);
return _ref;
}
$ = Annotator.$;
Document.prototype.events = {
'beforeAnnotationCreated': 'beforeAnnotationCreated'
};
Document.prototype.pluginInit = function() {
return this.getDocumentMetadata();
};
Document.prototype.uri = function() {
var link, uri, _i, _len, _ref1;
uri = decodeURIComponent(document.location.href);
_ref1 = this.metadata;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
link = _ref1[_i];
if (link.rel === "canonical") {
uri = link.href;
}
}
return uri;
};
Document.prototype.uris = function() {
var href, link, uniqueUrls, _i, _len, _ref1;
uniqueUrls = {};
_ref1 = this.metadata.link;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
link = _ref1[_i];
if (link.href) {
uniqueUrls[link.href] = true;
}
}
return (function() {
var _results;
_results = [];
for (href in uniqueUrls) {
_results.push(href);
}
return _results;
})();
};
Document.prototype.beforeAnnotationCreated = function(annotation) {
return annotation.document = this.metadata;
};
Document.prototype.getDocumentMetadata = function() {
this.metadata = {};
this._getHighwire();
this._getDublinCore();
this._getFacebook();
this._getEprints();
this._getPrism();
this._getTwitter();
this._getFavicon();
this._getTitle();
this._getLinks();
return this.metadata;
};
Document.prototype._getHighwire = function() {
return this.metadata.highwire = this._getMetaTags("citation", "name", "_");
};
Document.prototype._getFacebook = function() {
return this.metadata.facebook = this._getMetaTags("og", "property", ":");
};
Document.prototype._getTwitter = function() {
return this.metadata.twitter = this._getMetaTags("twitter", "name", ":");
};
Document.prototype._getDublinCore = function() {
return this.metadata.dc = this._getMetaTags("dc", "name", ".");
};
Document.prototype._getPrism = function() {
return this.metadata.prism = this._getMetaTags("prism", "name", ".");
};
Document.prototype._getEprints = function() {
return this.metadata.eprints = this._getMetaTags("eprints", "name", ".");
};
Document.prototype._getMetaTags = function(prefix, attribute, delimiter) {
var content, match, meta, n, name, tags, _i, _len, _ref1;
tags = {};
_ref1 = $("meta");
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
meta = _ref1[_i];
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;
};
Document.prototype._getTitle = function() {
if (this.metadata.highwire.title) {
return this.metadata.title = this.metadata.highwire.title[0];
} else if (this.metadata.eprints.title) {
return this.metadata.title = this.metadata.eprints.title;
} else if (this.metadata.prism.title) {
return this.metadata.title = this.metadata.prism.title;
} else if (this.metadata.facebook.title) {
return this.metadata.title = this.metadata.facebook.title;
} else if (this.metadata.twitter.title) {
return this.metadata.title = this.metadata.twitter.title;
} else if (this.metadata.dc.title) {
return this.metadata.title = this.metadata.dc.title;
} else {
return this.metadata.title = $("head title").text();
}
};
Document.prototype._getLinks = function() {
var doi, href, id, l, lang, link, name, rel, type, url, values, _i, _j, _k, _len, _len1, _len2, _ref1, _ref2, _ref3, _results;
this.metadata.link = [
{
href: document.location.href
}
];
_ref1 = $("link");
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
link = _ref1[_i];
l = $(link);
href = this._absoluteUrl(l.prop('href'));
rel = l.prop('rel');
type = l.prop('type');
lang = l.prop('hreflang');
if (rel !== "alternate" && rel !== "canonical" && rel !== "bookmark") {
continue;
}
if (rel === 'alternate') {
if (type && type.match(/^application\/(rss|atom)\+xml/)) {
continue;
}
if (lang) {
continue;
}
}
this.metadata.link.push({
href: href,
rel: rel,
type: type
});
}
_ref2 = this.metadata.highwire;
for (name in _ref2) {
values = _ref2[name];
if (name === "pdf_url") {
for (_j = 0, _len1 = values.length; _j < _len1; _j++) {
url = values[_j];
this.metadata.link.push({
href: this._absoluteUrl(url),
type: "application/pdf"
});
}
}
if (name === "doi") {
for (_k = 0, _len2 = values.length; _k < _len2; _k++) {
doi = values[_k];
if (doi.slice(0, 4) !== "doi:") {
doi = "doi:" + doi;
}
this.metadata.link.push({
href: doi
});
}
}
}
_ref3 = this.metadata.dc;
_results = [];
for (name in _ref3) {
values = _ref3[name];
if (name === "identifier") {
_results.push((function() {
var _l, _len3, _results1;
_results1 = [];
for (_l = 0, _len3 = values.length; _l < _len3; _l++) {
id = values[_l];
if (id.slice(0, 4) === "doi:") {
_results1.push(this.metadata.link.push({
href: id
}));
} else {
_results1.push(void 0);
}
}
return _results1;
}).call(this));
} else {
_results.push(void 0);
}
}
return _results;
};
Document.prototype._getFavicon = function() {
var link, _i, _len, _ref1, _ref2, _results;
_ref1 = $("link");
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
link = _ref1[_i];
if ((_ref2 = $(link).prop("rel")) === "shortcut icon" || _ref2 === "icon") {
_results.push(this.metadata["favicon"] = this._absoluteUrl(link.href));
} else {
_results.push(void 0);
}
}
return _results;
};
Document.prototype._absoluteUrl = function(url) {
var d;
d = document.createElement('a');
d.href = url;
return d.href;
};
return Document;
})(Annotator.Plugin);
}).call(this);
//
//# sourceMappingURL=annotator.document.map
\ No newline at end of file
This diff is collapsed.
......@@ -33,8 +33,9 @@ module.exports = function(config) {
'h/static/scripts/vendor/Markdown.Converter.js',
'h/static/scripts/vendor/unorm.js',
'h/static/scripts/vendor/uuid.js',
'h/static/scripts/annotator/annotator.min.js',
'h/static/scripts/annotator/plugin/auth.js',
'h/static/scripts/vendor/annotator.js',
'h/static/scripts/annotator/monkey.js',
'h/static/scripts/vendor/annotator.auth.js',
'h/static/scripts/annotator/plugin/bridge.js',
'h/static/scripts/annotator/plugin/bucket-bar.js',
'h/static/scripts/annotator/plugin/threading.js',
......
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