Commit d2561e81 authored by Ujvari Gergely's avatar Ujvari Gergely

Merge branch 'develop' of https://github.com/hypothesis/h into develop

parents 114b9748 2e32dcb2
......@@ -38,6 +38,7 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
padding: 1em;
padding-bottom: 3.5em; // 1em + 2.5em of wrapper top padding
position: absolute;
height: 100%;
left: 0;
......@@ -225,7 +226,7 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
position: relative;
text-align: right;
top: -3.5em;
a { @include tertiarytext; }
a { @extend .small; }
}
input:not([type="submit"]) { width: 100%; }
......
......@@ -187,26 +187,8 @@ $em: 14 / 1em !default;
}
//ANNOTATOR TEXT STYLES////////////////////////////////
@mixin primarytext {
font-weight: bold;
font-size: 1.3em;
}
@mixin secondarytext {
font-size: 1.1em;
font-weight: bold;
}
@mixin tertiarytext {
font-size: .8em;
}
//FONTICON////////////////////////////////
@mixin fonticon($char, $iconside, $offset: 0) {
@mixin fonticon($char, $iconside, $offset: .2em) {
text-decoration: none;
cursor: pointer;
color: $gray;
......@@ -217,7 +199,6 @@ $em: 14 / 1em !default;
@if $iconside == left {
&:before {
content: $char;
vertical-align: -.1em;
font-family: 'icomoon';
font-style: normal;
margin-right: $offset;
......@@ -234,7 +215,6 @@ $em: 14 / 1em !default;
}
&:after {
content: $char;
vertical-align: -.1em;
font-family: 'icomoon';
font-style: normal;
margin-left: $offset;
......
......@@ -53,12 +53,11 @@ h2 {
}
h3 {
font-size: 1em;
margin: 0;
font-size: 1.4em;
}
h4 {
font-size: 1.25em;
font-size: 1.15em;
margin: 0 0 .1em;
}
......@@ -101,8 +100,8 @@ button, input[type=submit], .btn {
display: inline-block
}
.tertiarytext {
@include tertiarytext;
.small {
font-size: .8em;
}
.icon-hidden {
......@@ -495,31 +494,27 @@ blockquote {
.annotation {
position: relative;
div.body {
@include force-wrap;
clear: both;
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
.upper-left {
font-weight: bold;
text-decoration: underline;
margin-bottom: .25em;
float: left;
.user {
font-weight: 800;
}
textarea.body {
min-height: 8em;
width: 100%;
}
.body {
div {
@include force-wrap;
clear: both;
margin: .25em 0 .5em;
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
.privacy {
float: right;
textarea {
min-height: 8em;
width: 100%;
}
}
.buttonbar {
......@@ -532,7 +527,7 @@ blockquote {
}
.tip {
@include tertiarytext;
@extend .small;
float: right;
}
}
......@@ -541,6 +536,7 @@ blockquote {
//THREADING////////////////////////////////
//Threaded discussion specific
.thread {
margin-top: 1em;
position: relative;
& > ul {
......@@ -566,18 +562,13 @@ blockquote {
height: $threadexp-width;
width: $threadexp-width;
position: absolute;
top: .8em;
top: $threadexp-width / 2;
left: -($threadexp-width / 2);
outline: 1px dotted #aaa;
@include icon("minus_1.png");
}
.reply-count {
@include tertiarytext;
}
.annotation {
padding-top: .35em;
&.squished {
padding-left: 0;
}
......@@ -596,6 +587,10 @@ blockquote {
margin-bottom: 0;
}
.reply-icon {
display: none;
}
.user {
display: run-in;
margin-right: .25em;
......@@ -617,45 +612,26 @@ blockquote {
//MAGICONTROLS////////////////////////////////
.magicontrols {
@include pie-clearfix;
background-color: $bodyBackground;
float: right;
& > * {
@include transition(
opacity 0.1s ease-in-out .25s,
font-size .1s ease-in-out .25s,
margin .1s ease-in-out .25s,
opacity .1s ease-in-out .25s
);
@include tertiarytext;
float: left;
& > .pull-left {
margin-right: 1em;
}
& > .pull-right {
margin-left: 1em;
}
.show {
font-size: 0;
@include transition(
opacity 0 ease-in-out .15s
);
opacity: 0;
}
.detail:hover & {
& > * {
font-size: 1em;
}
.show {
:hover > & {
opacity: 1;
}
.time {
opacity: 0;
text-size: 0;
}
}
}
.reply-count {
.detail .magicontrols & {
display: none;
}
}
......@@ -678,7 +654,9 @@ blockquote {
}
// Things not shown in the summary view
.annotator-controls, .magicontrols .show, .buttonbar {
display: none;
.magicontrols {
.reply-icon, .pull-right {
display: none;
}
}
}
......@@ -99,6 +99,7 @@
//IFRAME////////////////////////////////
.annotator-frame {
@include reset-box-model;
height: 100%;
position: fixed;
top: 0;
......
This diff is collapsed.
annotation = ['$filter', ($filter) ->
compile: (tElement, tAttrs, transclude) ->
# Adjust the ngModel directive to use the isolate scope binding.
# The expression will be bound in the isolate as '$modelValue'.
if tAttrs.ngModel
tAttrs.$set '$modelValue', tAttrs.ngModel, false
tAttrs.$set 'ngModel', '$modelValue', false
post: (scope, iElement, iAttrs, controller) ->
return unless controller
# Bind shift+enter to save
iElement.find('textarea').bind
keydown: (e) ->
if e.keyCode == 13 && e.shiftKey
e.preventDefault()
scope.save()
# Format the annotation for display
controller.$formatters.push (value) ->
return unless angular.isObject value
created: value.created
body: ($filter 'converter') (value.text or '')
text: value.text
user: value.user
privacy: scope.getPrivacyLevel value.permissions
controller.$parsers.push (value) ->
return unless angular.isObject value
if controller.$pristine
controller.$modelValue
else
angular.extend controller.$modelValue,
text: value.text
permissions: value.privacy.permissions
# Publish the controller
scope.model = controller
link: (scope, elem, attrs, controller) ->
return unless controller?
# Bind shift+enter to save
elem.bind
keydown: (e) ->
if e.keyCode == 13 && e.shiftKey
e.preventDefault()
scope.save()
# Publish the controller
scope.model = controller
controller: 'AnnotationController'
priority: 100 # Must run before ngModel
require: '?ngModel'
restrict: 'C'
scope:
$modelValue: '='
scope: {}
templateUrl: 'annotation.html'
]
markdown = ['$filter', '$timeout', ($filter, $timeout) ->
link: (scope, elem, attrs, controller) ->
return unless controller?
# Format the annotation for display
controller.$formatters.push (value) ->
if scope.readonly
value
else if value
($filter 'converter') value
else
''
# Publish the controller
scope.model = controller
# Auto-focus the input box
scope.$watch 'readonly', (newValue) ->
unless newValue then $timeout -> elem.find('textarea').focus()
require: '?ngModel'
restrict: 'E'
scope:
readonly: '@'
required: '@'
templateUrl: 'markdown.html'
]
privacy = ->
levels = [
{name: 'Public', permissions: { 'read': ['group:__world__'] } },
{name: 'Private', permissions: { 'read': [] } }
]
getLevel = (permissions) ->
return unless permissions?
for level in levels
roleSet = {}
# Construct a set (using a key->exist? mapping) of roles for each verb
for verb of permissions
roleSet[verb] = {}
for role in permissions[verb]
roleSet[verb][role] = true
# Check that no (verb, role) is missing from the role set
mismatch = false
for verb of level.permissions
for role in level.permissions[verb]
if roleSet[verb]?[role]?
delete roleSet[verb][role]
else
mismatch = true
break
# Check that no extra (verb, role) is missing from the privacy level
mismatch ||= Object.keys(roleSet[verb]).length
if mismatch then break else return level
# Unrecognized privacy level
name: 'Custom'
value: permissions
link: (scope, elem, attrs, controller) ->
return unless controller?
controller.$formatters.push getLevel
controller.$parsers.push (privacy) -> privacy?.permissions
scope.model = controller
scope.levels = levels
require: '?ngModel'
restrict: 'E'
scope: true
templateUrl: 'privacy.html'
recursive = ['$compile', '$timeout', ($compile, $timeout) ->
compile: (tElement, tAttrs, transclude) ->
placeholder = angular.element '<!-- recursive -->'
......@@ -144,6 +194,8 @@ thread = ->
angular.module('h.directives', ['ngSanitize'])
.directive('annotation', annotation)
.directive('markdown', markdown)
.directive('privacy', privacy)
.directive('recursive', recursive)
.directive('resettable', resettable)
.directive('tabReveal', tabReveal)
......
$ = Annotator.$
util =
debounce: (fn, delay=200) =>
timer = null
(args...) =>
if timer then clearTimeout(timer)
setTimeout =>
timer = null
fn args...
, delay
class Annotator.Host extends Annotator
# Events to be bound on Annotator#element.
events:
......@@ -19,9 +9,6 @@ class Annotator.Host extends Annotator
# Plugin configuration
options: {}
# timer used to throttle event frequency
updateTimer: null
# Drag state variables
drag:
delta: 0
......@@ -34,63 +21,72 @@ class Annotator.Host extends Annotator
@app = @options.app
delete @options.app
# Create the iframe
if document.baseURI and window.PDFView?
# XXX: Hack around PDF.js resource: origin. Bug in jschannel?
hostOrigin = '*'
else
hostOrigin = window.location.origin
@frame = $('<iframe></iframe>')
.css(visibility: 'hidden')
.attr('src', "#{@app}#/?xdm=#{encodeURIComponent(hostOrigin)}")
.appendTo(@wrapper)
.addClass('annotator-frame annotator-outer annotator-collapsed')
.bind 'load', => this._setupXDM()
# Load plugins
for own name, opts of @options
if not @plugins[name]
this.addPlugin(name, opts)
# Grab this for exporting the iframe from easyXDM
annotator = this
# Establish cross-domain communication
@consumer = new easyXDM.Rpc
channel: 'annotator'
container: @wrapper[0]
local: options.local
onReady: () ->
# easyXDM updates this configuration object which provides access
# to the `props` attribute to find the value of the iframe's src
# attribute to find the iframe created by easyXDM.
frame = $(this.container).find('[src^="'+@props.src+'"]')
.css('visibility', 'visible')
# Export the iframe element via the private `annotator` variable,
# which references the `Annotator.Host` object.
annotator.frame = frame
swf: options.swf
props:
className: 'annotator-frame annotator-outer annotator-collapsed'
style:
visibility: 'hidden'
remote: @app
,
local:
publish: (args..., k, fk) => this.publish args...
setupAnnotation: => this.setupAnnotation arguments...
deleteAnnotation: (annotation) =>
toDelete = []
@wrapper.find('.annotator-hl')
.each ->
data = $(this).data('annotation')
if data.id == annotation.id and data not in toDelete
toDelete.push data
this.deleteAnnotation d for d in toDelete
loadAnnotations: => this.loadAnnotations arguments...
onEditorHide: this.onEditorHide
onEditorSubmit: this.onEditorSubmit
showFrame: =>
# Scan the document text with the DOM Text libraries
this.scanDocument "Annotator initialized"
_setupXDM: ->
# Set up the bridge plugin, which bridges the main annotation methods
# between the host page and the panel widget.
whitelist = ['diffHTML', 'quote', 'ranges', 'target']
this.addPlugin 'Bridge',
origin: '*'
window: @frame[0].contentWindow
formatter: (annotation) =>
formatted = {}
for k, v of annotation when k in whitelist
formatted[k] = v
formatted
parser: (annotation) =>
parsed = {}
for k, v of annotation when k in whitelist
parsed[k] = v
parsed
# Build a channel for the panel UI
@panel = Channel.build
origin: '*'
scope: 'annotator:panel'
window: @frame[0].contentWindow
onReady: =>
@frame.css('visibility', 'visible')
@panel
.bind('onEditorHide', this.onEditorHide)
.bind('onEditorSubmit', this.onEditorSubmit)
.bind('showFrame', =>
@frame.css 'margin-left': "#{-1 * @frame.width()}px"
@frame.removeClass 'annotator-no-transition'
@frame.removeClass 'annotator-collapsed'
)
hideFrame: =>
.bind('hideFrame', =>
@frame.css 'margin-left': ''
@frame.removeClass 'annotator-no-transition'
@frame.addClass 'annotator-collapsed'
)
dragFrame: (screenX) =>
.bind('dragFrame', (ctx, screenX) =>
if screenX > 0
if @drag.last?
@drag.delta += screenX - @drag.last
......@@ -98,34 +94,29 @@ class Annotator.Host extends Annotator
unless @drag.tick
@drag.tick = true
window.requestAnimationFrame this._dragRefresh
)
getHighlights: =>
.bind('getHighlights', =>
highlights: $(@wrapper).find('.annotator-hl').map ->
offset: $(this).offset()
height: $(this).outerHeight(true)
data: $(this).data('annotation')
data: $(this).data('annotation').$$tag
.get()
offset: $(window).scrollTop()
)
setActiveHighlights: (ids=[]) =>
.bind('setActiveHighlights', (ctx, tags=[]) =>
@wrapper.find('.annotator-hl')
.each ->
if $(this).data('annotation').id in ids
if $(this).data('annotation').$$tag in tags
$(this).addClass('annotator-hl-active')
else if not $(this).hasClass('annotator-hl-temporary')
$(this).removeClass('annotator-hl-active')
)
getHref: =>
uri = decodeURIComponent document.location.href
if document.location.hash
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
.bind('getHref', => this.getHref())
getMaxBottom: =>
.bind('getMaxBottom', =>
sel = '*' + (":not(.annotator-#{x})" for x in [
'adder', 'outer', 'notice', 'filter', 'frame'
]).join('')
......@@ -144,25 +135,30 @@ class Annotator.Host extends Annotator
else
0
Math.max.apply(Math, all)
)
scrollTop: (y) =>
.bind('scrollTop', (ctx, y) =>
$('html, body').stop().animate {scrollTop: y}, 600
remote:
publish: {}
addPlugin: {}
createAnnotation: {}
showEditor: {}
showViewer: {}
back: {}
update: {}
)
scanDocument: (reason = "something happened") =>
try
console.log "Analyzing host frame, because " + reason + "..."
r = @domMatcher.scan()
scanTime = r.time
console.log "Traversal+scan took " + scanTime + " ms."
catch e
console.log e.message
console.log e.stack
_setupWrapper: ->
@wrapper = @element
.on 'mouseup', =>
if not @ignoreMouseup
setTimeout =>
@consumer.back() unless @selectedRanges?.length
unless @selectedRanges?.length then @panel?.notify method: 'back'
this._setupMatching()
@domMatcher.setRootNode @wrapper[0]
this
_setupDocumentEvents: ->
......@@ -191,7 +187,7 @@ class Annotator.Host extends Annotator
height: $(window).height()
position: 'absolute'
top: $(window).scrollTop()
@consumer.update()
@panel?.notify method: 'publish', params: 'hostUpdated'
document.addEventListener 'touchmove', update
document.addEventListener 'touchstart', =>
touch = true
......@@ -235,32 +231,13 @@ class Annotator.Host extends Annotator
'margin-left': "#{m}px"
width: "#{w}px"
setupAnnotation: (annotation) ->
annotation = super
# Highlights are jQuery collections which have a circular references to the
# annotation via data stored with `.data()`. Therefore, reconfigure the
# property to hide them from serialization.
Object.defineProperty annotation, 'highlights',
enumerable: false
annotation
showEditor: (annotation) =>
if not annotation.id?
@consumer.createAnnotation (id) =>
if id?
annotation.id = id
@consumer.showEditor annotation
else
this.deleteAnnotation annotation
@ignoreMouseup = false
else
@consumer.showEditor annotation
@plugins.Bridge.showEditor annotation
this
checkForStartSelection: (event) =>
# Override to prevent Annotator choking when this ties to access the
# viewer but preserve the manipulation of the attribute `mouseIsDown` which
# is needed for preventing the sidebar from closing while annotating.
# is needed for preventing the panel from closing while annotating.
unless event and this.isAnnotator(event.target)
@mouseIsDown = true
class Annotator.Plugin.Bridge extends Annotator.Plugin
# These events maintain the awareness of annotations between the two
# communicating annotators.
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded'
# Plugin configuration
options:
# Origins allowed to communicate on the channel
origin: '*'
# Scope identifier to distinguish this channel from any others
scope: 'annotator:bridge'
# Formats an annotation for sending across the bridge
formatter: (annotation) -> annotation
# Parses an annotation received from the bridge
parser: (annotation) -> annotation
# Merge function. If specified, it will be called with the local copy of
# an annotation and a parsed copy received as an argument to an RPC call
# to reconcile any differences. The default behavior is to merge all
# keys of the remote object into the local copy
merge: (local, remote) ->
for k, v of remote
local[k] = v
local
# Cache of annotations which have crossed the bridge for fast, encapsulated
# association of annotations received in arguments to window-local copies.
cache: {}
constructor: (elem, options) ->
if options.window?
# Pull the option out and restore it after the super constructor is
# called. Unfortunately, Delegator uses a jQuery function which
# inspects this too closely and causes security errors.
window = options.window
delete options.window
super elem, options
@options.window = window
else
super
pluginInit: ->
@options.onReady = this.onReady
@channel = Channel.build @options
# Assign a non-enumerable tag to objects which cross the bridge.
# This tag is used to identify the objects between message.
_tag: (msg, tag) ->
return msg if msg.$$tag
tag = tag or (window.btoa Math.random())
Object.defineProperty msg, '$$tag', value: tag
@cache[tag] = msg
msg
# Call the configured parser and tag the result
_parse: ({tag, msg}) ->
local = @cache[tag]
remote = @options.parser msg
if local?
merged = @options.merge local, remote
else
merged = remote
this._tag merged, tag
# Tag the object and format it with the configured formatter
_format: (annotation) ->
this._tag annotation
msg = @options.formatter annotation
tag: annotation.$$tag
msg: msg
beforeAnnotationCreated: (annotation) =>
unless annotation.$$tag?
tag = this.createAnnotation()
this._tag annotation, tag
annotationDeleted: (annotation) =>
return unless @cache[annotation.$$tag?]
delete @cache[annotation.$$tag]
this.deleteAnnotation annotation
annotationsLoaded: (annotations) =>
this.setupAnnotation a for a in annotations
onReady: =>
@channel
## Remote method call bindings
.bind('createAnnotation', (txn, tag) =>
annotation = this._tag {}, tag
@annotator.publish 'beforeAnnotationCreated', annotation
this._format annotation
)
.bind('setupAnnotation', (txn, annotation) =>
this._format (@annotator.setupAnnotation (this._parse annotation))
)
.bind('updateAnnotation', (txn, annotation) =>
this._format (@annotator.updateAnnotation (this._parse annotation))
)
.bind('deleteAnnotation', (txn, annotation) =>
delete @cache[annotation.tag]
this._format (@annotator.deleteAnnotation (this._parse annotation))
)
## Notifications
.bind('showEditor', (ctx, annotation) =>
@annotator.showEditor (this._parse annotation)
)
.bind('showViewer', (ctx, annotations) =>
@annotator.showEditor (this._parse a for a in annotations)
)
createAnnotation: (cb) ->
tag = window.btoa Math.random()
@channel.call
method: 'createAnnotation'
params: tag
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
tag
setupAnnotation: (annotation, cb) ->
@channel.call
method: 'setupAnnotation'
params: this._format annotation
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
updateAnnotation: (annotation, cb) ->
@channel.call
method: 'updateAnnotation'
params: this._format annotation
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
deleteAnnotation: (annotation, cb) ->
@channel.notify
method: 'deleteAnnotation'
params: this._format annotation
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
showEditor: (annotation) ->
@channel.notify
method: 'showEditor'
params: this._format annotation
showViewer: (annotations) ->
@channel.notify
method: 'showViewer'
params: (this._format a for a in annotations)
class Annotator.Plugin.Threading extends Annotator.Plugin
# These events maintain the awareness of annotations between the two
# communicating annotators.
events:
'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded'
'beforeAnnotationCreated': 'beforeAnnotationCreated'
# Cache of annotations which have crossed the bridge for fast, encapsulated
# association of annotations received in arguments to window-local copies.
cache: {}
pluginInit: ->
@annotator.threading = mail.messageThread()
thread: (annotation) ->
# Assign a temporary id if necessary. Threading relies on the id.
unless annotation.id?
Object.defineProperty annotation, 'id',
configurable: true
enumerable: false
writable: true
value: window.btoa Math.random()
# Get or create a thread to contain the annotation
thread = (@annotator.threading.getContainer annotation.id)
thread.message =
annotation: annotation
id: annotation.id
references: annotation.thread?.split('/')
# Attach the thread to its parent, if any.
references = thread.message?.references
if references?.length
prev = references[references.length-1]
@annotator.threading.getContainer(prev).addChild thread
# Update the id table
@annotator.threading.idTable[annotation.id] = thread
thread
annotationDeleted: (annotation) =>
thread = (@annotator.threading.getContainer annotation.id)
delete @annotator.threading.idTable[annotation.id]
thread.message = null
if thread.parent? then @annotator.threading.pruneEmpties thread.parent
annotationsLoaded: (annotations) =>
@annotator.threading.thread annotations.map (a) ->
annotation: a
id: a.id
references: a.thread?.split '/'
beforeAnnotationCreated: (annotation) =>
this.thread annotation
This diff is collapsed.
/**
* @license AngularJS v1.0.4
* @license AngularJS v1.0.5
* (c) 2010-2012 Google, Inc. http://angularjs.org
* License: MIT
*/
......
/**
* @license AngularJS v1.0.4
* @license AngularJS v1.0.5
* (c) 2010-2012 Google, Inc. http://angularjs.org
* License: MIT
*/
......
This diff is collapsed.
/*
** Annotator 1.2.6-dev-c4fcdfe
** Annotator 1.2.5-dev-5a5e03a
** 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: 2013-01-29 11:20:24Z
** Built at: 2013-02-25 17:23:26Z
*/
(function() {
......
This diff is collapsed.
/*
** Annotator 1.2.6-dev-c4fcdfe
** Annotator 1.2.5-dev-5a5e03a
** 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: 2013-01-29 11:20:26Z
** Built at: 2013-02-25 17:23:22Z
*/
(function() {
......
/*
** Annotator 1.2.6-dev-c4fcdfe
** Annotator 1.2.5-dev-859d4e3
** 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: 2013-01-29 11:20:27Z
** Built at: 2013-03-13 14:59:29Z
*/
(function() {
......@@ -65,10 +65,17 @@
};
Store.prototype.annotationCreated = function(annotation) {
var _this = this;
var quote, quoteHTML,
_this = this;
if (__indexOf.call(this.annotations, annotation) < 0) {
this.registerAnnotation(annotation);
quote = annotation.quote;
quoteHTML = annotation.quoteHTML;
delete annotation.quote;
delete annotation.quoteHTML;
return this._apiRequest('create', annotation, function(data) {
annotation.quote = quote;
annotation.quoteHTML = quoteHTML;
if (!(data.id != null)) {
console.warn(Annotator._t("Warning: No ID returned from server for annotation "), annotation);
}
......@@ -80,9 +87,16 @@
};
Store.prototype.annotationUpdated = function(annotation) {
var _this = this;
var quote, quoteHTML,
_this = this;
if (__indexOf.call(this.annotations, annotation) >= 0) {
quote = annotation.quote;
quoteHTML = annotation.quoteHTML;
delete annotation.quote;
delete annotation.quoteHTML;
return this._apiRequest('update', annotation, (function(data) {
annotation.quote = quote;
annotation.quoteHTML = quoteHTML;
return _this.updateAnnotation(annotation, data);
}));
}
......@@ -180,7 +194,7 @@
Store.prototype._urlFor = function(action, id) {
var replaceWith, url;
replaceWith = id != null ? '/' + id : '';
url = this.options.prefix != null ? this.options.prefix : '';
url = this.options.prefix || '/';
url += this.options.urls[action];
url = url.replace(/\/:id/, replaceWith);
return url;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -269,7 +269,7 @@
pruneEmpties: pruneEmpties,
groupBySubject: groupBySubject,
thread: thread,
idTable: idTable
get idTable() { return idTable }
}
}();
}
......
# Naive text matcher
class window.DTM_ExactMatcher
constructor: ->
@distinct = true
@caseSensitive = false
setDistinct: (value) -> @distinct = value
setCaseSensitive: (value) -> @caseSensitive = value
search: (text, pattern) ->
# console.log "Searching for '" + pattern + "' in '" + text + "'."
pLen = pattern.length
results = []
index = 0
unless @caseSensitive
text = text.toLowerCase()
pattern = pattern.toLowerCase()
while (i = text.indexOf pattern) > -1
do =>
# console.log "Found '" + pattern + "' @ " + i +
# " (=" + (index + i) + ")"
results.push
start: index + i
end: index + i + pLen
if @distinct
text = text.substr i + pLen
index += i + pLen
else
text = text.substr i + 1
index += i + 1
results
class window.DTM_RegexMatcher
constructor: ->
@caseSensitive = false
setCaseSensitive: (value) -> @caseSensitive = value
search: (text, pattern) ->
re = new RegExp pattern, if @caseSensitive then "g" else "gi"
{ start: m.index, end: m.index + m[0].length } while m = re.exec text
# diff-match-patch - based text matcher
class window.DTM_DMPMatcher
constructor: ->
@dmp = new diff_match_patch
@dmp.Diff_Timeout = 0
@caseSensitive = false
_reverse: (text) -> text.split("").reverse().join ""
# Use this to get the max allowed pattern length.
# Trying to use a longer pattern will give an error.
getMaxPatternLength: -> @dmp.Match_MaxBits
# The following example is a classic dilemma.
# There are two potential matches, one is close to the expected location
# but contains a one character error, the other is far from the expected
# location but is exactly the pattern sought after:
#
# match_main("abc12345678901234567890abbc", "abc", 26)
#
# Which result is returned (0 or 24) is determined by the
# MatchDistance property.
#
# An exact letter match which is 'distance' characters away
# from the fuzzy location would score as a complete mismatch.
# For example, a distance of '0' requires the match be at the exact
# location specified, whereas a threshold of '1000' would require
# a perfect match to be within 800 characters of the expected location
# to be found using a 0.8 threshold (see below).
#
# The larger MatchDistance is, the slower search may take to compute.
#
# This variable defaults to 1000.
setMatchDistance: (distance) -> @dmp.Match_Distance = distance
getMatchDistance: -> @dmp.Match_Distance
# MatchThreshold determines the cut-off value for a valid match.
#
# If Match_Threshold is closer to 0, the requirements for accuracy
# increase. If Match_Threshold is closer to 1 then it is more likely
# that a match will be found. The larger Match_Threshold is, the slower
# search may take to compute.
#
# This variable defaults to 0.5.
setMatchThreshold: (threshold) -> @dmp.Match_Threshold = threshold
getMatchThreshold: -> @dmp.Match_Threshold
getCaseSensitive: -> caseSensitive
setCaseSensitive: (value) -> @caseSensitive = value
# Given a text to search, a pattern to search for and an
# expected location in the text near which to find the pattern,
# return the location which matches closest.
#
# The function will search for the best match based on both the number
# of character errors between the pattern and the potential match,
# as well as the distance between the expected location and the
# potential match.
#
# If no match is found, the function returns null.
search: (text, pattern, expectedStartLoc = 0, options = {}) ->
# console.log "In dtm search. text: '" + text + "', pattern: '" + pattern +
# "', expectedStartLoc: " + expectedStartLoc + ", options:"
# console.log options
if expectedStartLoc < 0 then throw new Error "Can't search at negative indices!"
unless @caseSensitive
text = text.toLowerCase()
pattern = pattern.toLowerCase()
pLen = pattern.length
maxLen = @getMaxPatternLength()
if pLen <= maxLen
result = @searchForSlice text, pattern, expectedStartLoc
else
startSlice = pattern.substr 0, maxLen
startPos = @searchForSlice text, startSlice, expectedStartLoc
if startPos?
startLen = startPos.end - startPos.start
endSlice = pattern.substr pLen - maxLen, maxLen
endLoc = startPos.start + pLen - maxLen
endPos = @searchForSlice text, endSlice, endLoc
if endPos?
endLen = endPos.end - endPos.start
matchLen = endPos.end - startPos.start
startIndex = startPos.start
endIndex = endPos.end
if pLen*0.5 <= matchLen <= pLen*1.5
result =
start: startIndex
end: endPos.end
# data:
# startError: startPos.data.error
# endError: endPos.data.error
# uncheckedMidSection: Math.max 0, matchLen - startLen - endLen
# lengthError: matchLen - pLen
# else
# console.log "Sorry, matchLen (" + matchLen + ") is not between " +
# 0.5*pLen + " and " + 1.5*pLen
# else
# console.log "endSlice ('" + endSlice + "') not found"
# else
# console.log "startSlice ('" + startSlice + "') not found"
unless result? then return []
if options.withLevenhstein or options.withDiff
found = text.substr result.start, result.end - result.start
result.diff = @dmp.diff_main pattern, found
if options.withLevenshstein
result.lev = @dmp.diff_levenshtein result.diff
if options.withDiff
@dmp.diff_cleanupSemantic result.diff
result.diffHTML = @dmp.diff_prettyHtml result.diff
[result]
# Compare two string slices, get Levenhstein and visual diff
compare: (text1, text2) ->
unless (text1? and text2?)
throw new Error "Can not compare non-existing strings!"
result = {}
result.diff = @dmp.diff_main text1, text2
result.lev = @dmp.diff_levenshtein result.diff
result.errorLevel = result.lev / text1.length
@dmp.diff_cleanupSemantic result.diff
result.diffHTML = @dmp.diff_prettyHtml result.diff
result
# ============= Private part ==========================================
# You don't need to call the functions below this point manually
searchForSlice: (text, slice, expectedStartLoc) ->
# console.log "searchForSlice: '" + text + "', '" + slice + "', " +
# expectedStartLoc
r1 = @dmp.match_main text, slice, expectedStartLoc
startIndex = r1.index
if startIndex is -1 then return null
txet = @_reverse text
nrettap = @_reverse slice
expectedEndLoc = startIndex + slice.length
expectedDneLoc = text.length - expectedEndLoc
r2 = @dmp.match_main txet, nrettap, expectedDneLoc
dneIndex = r2.index
endIndex = text.length - dneIndex
result =
start: startIndex
end: endIndex
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