Commit 85c5dc44 authored by Kristof Csillag's avatar Kristof Csillag

Merge pull request #778 from hypothesis/multi-frame-8

Support for annotating multiple frames
parents 3e324c61 9008013c
......@@ -206,4 +206,15 @@ $input-border-radius: 2px;
}
}
}
}
\ No newline at end of file
}
//NOISE///////////
//Provides the noise background
.noise {
background: url("../images/noise_1.png");
}
.dark-noise {
background: url("../images/dark_noise_1.png");
}
......@@ -510,34 +510,6 @@ blockquote {
}
}
//Controlbar Icons
.alwaysonhighlights-icon {
@include fonticon("\e01b", left);
color: rgb(211, 211, 211);
font-size: 18px;
bottom: 1px;
left: 3px;
position: absolute;
}
.highlighter-icon {
@include fonticon("\e01a", left);
color: rgb(211, 211, 211);
font-size: 18px;
bottom: 1px;
left: 3px;
position: absolute;
}
.commenter-icon {
@include fonticon("\e01e", left);
color: rgb(211, 211, 211);
font-size: 18px;
bottom: 1px;
left: 3px;
position: absolute;
}
//VISIBILITY
.visibility {
.dropdown-toggle {
......@@ -619,17 +591,6 @@ blockquote {
}
//NOISE///////////
//Provides the noise background
.noise {
background: url("../images/noise_1.png");
}
.dark-noise {
background: url("../images/dark_noise_1.png");
}
//KNOCKOUT///////////
//Provides a knockout background
.knockout {
......@@ -653,13 +614,14 @@ blockquote {
//TOOL BAR////////////////////////////////
.topbar {
@include smallshadow;
@include smallshadow(0);
background: $white;
border: solid 1px $grayLighter;
border-style: solid none;
height: 2em;
position: fixed;
left: -1px;
right: -1px;
left: 0;
right: 0;
top: .5em;
z-index: 5;
......@@ -1277,19 +1239,11 @@ h3.stream {
}
}
.visual-container {
margin-left: 3em;
}
.magnify-glass {
width: 2.25em;
min-height: 22px;
}
.magnify-glass .VS-icon {
margin-left: 3.5em;
}
.magnify-glass .VS-icon-search:hover {
width:12px;
height:12px;
......
@import 'base';
@import 'compass/css3/user-interface';
@import 'compass/layout/stretching';
@import 'compass/reset/utilities';
$baseFontSize: 14px;
//ADDER////////////////////////////////
.annotator-adder {
......@@ -64,6 +66,127 @@
}
//HEATMAP STUFF////////////////////////////////
.annotator-heatmap {
cursor: ew-resize;
position: absolute;
overflow: hidden;
height: 100%;
width: $heatmap-width + 17px;
left: -($heatmap-width + 17px);
svg {
@include stretch-y;
background: hsla(0, 0, 0, .1);
border-left: solid 1px $grayLighter;
height: 100%;
left: 17px;
width: $heatmap-width;
}
}
.heatmap-pointer {
@include transition-property(left);
@include transition-duration(.2s);
margin-top: -1em; // TODO: Less janking v-align
// color: rgb(238, 238, 238);
color: #666;
left: 0;
position: absolute;
height: 20.1px;
width: 40.1px;
vertical-align: middle;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-align: center;
cursor: pointer;
.svg {
width: 100%;
height: 100%;
background-image: url("../images/side_tab.svg");
background-position: center center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.label {
font-weight: bold;
font-family: $sansFontFamily;
font-size: 13.1px;
line-height: 17.5px;
left: 5px;
right: 2px;
position: absolute;
bottom: 1px;
}
&:hover {
left: 2px;
}
&.lower, &.upper {
@include single-transition(margin-top, .2s);
left: 7px;
width: 33.1px;
height: 26.1px;
.label {
left: 0;
right: 0;
}
&:hover {
left: 7px;
}
}
&.upper {
.svg {
background-image: url("../images/up_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
&.lower {
.svg {
background-image: url("../images/down_tab.svg");
}
.label {
top: 1px;
}
&:hover {
margin-top: -.8em;
}
}
&.flip {
@include rotateY(180deg);
.label {
@include rotateY(180deg);
}
}
&.commenter {
@include single-transition(margin-top, .2s);
left: 3px;
width: 37px;
.svg {
background-image: url("../images/comment_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
}
//HIGHLIGHTS////////////////////////////////
.annotator-highlights-always-on .annotator-hl,
......@@ -96,9 +219,14 @@
display: none;
}
//IFRAME////////////////////////////////
// Sidebar
.annotator-frame {
@include reset-box-model;
@include user-select(none);
@extend .noise;
font-size: $baseFontSize / 16px * 1em;
line-height: $baseLineHeight / 16px * 1em;
height: 100%;
position: fixed;
top: 0;
......@@ -107,8 +235,21 @@
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
&.annotator-collapsed {
margin-left: -$heatmap-width - 17px;
margin-left: 0;
}
* {
font-size: 100%;
}
& > iframe {
@include reset-box-model;
height: 100%;
width: 100%;
z-index: 3;
position: relative;
}
}
.annotator-no-transition {
......@@ -116,6 +257,118 @@
}
//CONTROLBAR STUFF////////////////////////////////
.annotator-toolbar {
@include single-transition(height, .25s, ease, .25s);
font-size: $baseFontSize;
line-height: $baseLineHeight;
position: absolute;
overflow: hidden;
height: 200px;
left: -($heatmap-width + 17px - 7px);
top: .5em;
width: 2.5em;
z-index: 2;
a, a:hover, a:active, a:visited {
color: $grayLighter;
text-decoration: none;
&.tri-icon {
color: rgba(200, 200, 200, .3);
text-shadow:
1px .8px 1.5px $white,
0 0 0 #000;
&:hover {
color: rgba(235, 235, 230, .1);
}
}
}
ul, li {
@include box-sizing(border-box);
@include reset-box-model;
@include reset-list-style;
}
}
.annotator-toolbar.annotator-hide {
height: 2.2em;
&:hover {
height: 200px;
}
}
.annotator-toolbar li {
@include border-radius(4px);
@include smallshadow(0);
background: $white;
border: solid 1px $grayLighter;
position: relative;
left: 2px;
height: 2em;
width: 2em;
&:first-child {
@include border-radius(4px 0 0 4px);
border-right-style: none;
left: 0;
width: 2.3em;
}
& + li {
margin-top: 5px;
}
a {
font-size: $baseFontSize * 1.3;
line-height: $baseLineHeight * 1.3;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
position: absolute;
left: .2em;
}
a.pushed {
color: $hypothered;
}
}
// Toolbar Icons
.alwaysonhighlights-icon {
@include fonticon("\e01b", left);
}
.highlighter-icon {
@include fonticon("\e01a", left);
}
.commenter-icon {
@include fonticon("\e01e", left);
}
.tri-icon {
@include fonticon("\e00b", left);
}
// Toolbar notification counter
.annotator-notification-counter {
font-family: $sansFontFamily;
pointer-events: none;
position: absolute;
margin-left: .7em;
margin-top: .25em;
z-index: 2;
font-size: $baseFontSize * .86;
}
/*
Mobile layout
240-479 px
......@@ -124,8 +377,8 @@
@media screen and (min-width: 15em) {
.annotator-frame {
width: 99%;
margin-left: -99%;
width: 90%;
margin-left: -90%;
}
}
......@@ -151,8 +404,8 @@
@media screen and (min-width: 37.5em) {
.annotator-frame {
@include single-transition(margin-left, .4s);
width: 428px + $heatmap-width + 17px;
margin-left: -428px - $heatmap-width - 17px;
width: 428px;// + $heatmap-width + 17px;
margin-left: -428px;// - $heatmap-width - 17px;
}
}
......
@import 'compass';
@import 'compass/css3/transform';
@import 'compass/css3/user-interface';
@import 'compass/layout/stretching';
@import 'compass/reset/utilities';
......@@ -36,7 +35,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
.sliding-panels > li {
@extend .content;
@extend .noise;
@include smallshadow(-2px);
@include stretch-y;
@include transition(transform .4s);
......@@ -64,76 +62,27 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
//SIDEBAR LAYOUT////////////////////////////////
#wrapper {
@extend .noise;
height: 100%;
margin-left: $heatmap-width + 17px;
position: relative;
}
.topbar {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 7px;
top: .5em;
.inner {
margin: 0 .5em 0 3.1em;
}
.tri {
@include fonticon("\e00b", left);
color: rgba( 200, 200, 200, .3);
text-shadow:
1px .8px 1.5px $white,
0 0 0 #000;
position: absolute;
z-index: 1;
top: .3em;
left: .3em;
line-height: $baseLineHeight * .9;
font-size: $baseLineHeight * .9;
cursor: w-resize;
&:before {
vertical-align: 0;
}
&:hover {
color: rgba( 235, 235, 230, .1);
}
}
.notification-counter {
font-family: $sansFontFamily;
position: absolute;
margin-left: .75em;
float: left;
margin-top: 0.37em;
z-index: 2;
font-size: 12px;
cursor: w-resize;
}
&.shown {
.tri {
@include fonticon("\e010", left);
cursor: ew-resize;
}
.notification-counter {
cursor: ew-resize;
}
margin: 0 .5em 0 .5em;
}
}
.bottombar {
position: fixed;
bottom: 0px;
left: 39px; //17 + $heatmap_width
left: 0;
height: 3.7em;
width: 100%;
.notif-list {
margin-right: 3.85em;
margin-right: 1em;
box-shadow: 1px 1px 9px #b5d4ff;
}
......@@ -151,169 +100,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
}
}
//CONTROLBAR STUFF////////////////////////////////
.controlbar {
position: absolute;
overflow: hidden;
width: 40px;
height: 100px;
z-index: 1;
left: 1px;
top: 100%;
-moz-transition:height .25s, -moz-transform .25s;
-webkit-transition:height .25s, -webkit-transform .25s;
-o-transition:height .25s, -o-transform .25s;
transition:height .25s, transform .25s;
}
.hiddencontrolbar {
@extend .controlbar;
height: 0px;
}
.controlbarbutton {
position: relative;
left: 1px;
width: 25px;
height: 25px;
padding: 5px;
margin-top: 5px;
box-shadow: 1px 1px 1px 1px;
color: gray;
background-color: white;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
&:active {
top:1px;
box-shadow: 1px 1px 1px 0px;
}
a.pushed {
color: $hypothered;
}
}
//HEATMAP STUFF////////////////////////////////
.annotator-heatmap {
@include stretch-y;
height: 100%;
svg {
@include stretch-y;
background: hsla(0, 0, 0, .1);
border-left: solid 1px $grayLighter;
height: 100%;
left: 17px;
width: $heatmap-width;
}
}
.heatmap-pointer {
@include user-select(none);
@include transition-property(left);
@include transition-duration(.2s);
margin-top: -1em; // TODO: Less janking v-align
// color: rgb(238, 238, 238);
color: #666;
left: 0;
position: absolute;
height: 20.1px;
width: 40.1px;
vertical-align: middle;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-align: center;
cursor: pointer;
.svg {
width: 100%;
height: 100%;
background-image: url("../images/side_tab.svg");
background-position: center center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.label {
font-weight: bold;
font-family: $sansFontFamily;
font-size: 13.1px;
left: 5px;
right: 2px;
position: absolute;
bottom: 1px;
}
&:hover {
left: 2px;
}
&.lower, &.upper {
@include single-transition(margin-top, .2s);
left: 7px;
width: 33.1px;
height: 26.1px;
.label {
left: 0;
right: 0;
}
&:hover {
left: 7px;
}
}
&.upper {
.svg {
background-image: url("../images/up_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
&.lower {
.svg {
background-image: url("../images/down_tab.svg");
}
.label {
top: 1px;
}
&:hover {
margin-top: -.8em;
}
}
&.flip {
@include rotateY(180deg);
.label {
@include rotateY(180deg);
}
}
&.commenter {
@include single-transition(margin-top, .2s);
left: 3px;
width: 37px;
.svg {
background-image: url("../images/comment_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
}
//SHEET////////////////////////////////
//This comes down from under the toolbar
......@@ -323,10 +109,7 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
max-height: 30em;
overflow: hidden;
position: absolute;
top: 100%;
width: 100%;
left: 4px;
left: 0;
right: 0;
top: 2px;
......@@ -336,10 +119,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
top: .25em;
}
.nav-tabs {
padding-left: 1.3em;
}
&.collapsed {
max-height: 0;
}
......
......@@ -23,82 +23,7 @@ class App
baseUrl = baseUrl.replace /\/*$/, '/'
$scope.baseUrl = baseUrl
{plugins, provider} = annotator
heatmap = annotator.plugins.Heatmap
$scope.dynamicBucket = true
heatmap.element.bind 'click', =>
return unless drafts.discard()
$location.search('id', null).replace()
$scope.dynamicBucket = true
annotator.showViewer()
heatmap.publish 'updated'
$scope.$digest()
heatmap.subscribe 'updated', =>
elem = d3.select(heatmap.element[0])
return unless elem?.datum()
data = {highlights, offset} = elem.datum()
tabs = elem.selectAll('div').data data
height = $(window).outerHeight(true)
pad = height * .2
{highlights, offset} = elem.datum()
visible = $scope.frame.visible
if $scope.dynamicBucket and visible and $location.path() == '/viewer'
bottom = offset + heatmap.element.height()
annotations = highlights.reduce (acc, hl) =>
if hl.offset.top >= offset and hl.offset.top <= bottom
if hl.data not in acc
acc.push hl.data
acc
, []
annotator.updateViewer annotations
elem.selectAll('.heatmap-pointer')
# Creates highlights corresponding bucket when mouse is hovered
.on 'mouseover', (bucket) =>
unless $location.path() == '/viewer' and $location.search()?.id?
provider.notify
method: 'setActiveHighlights'
params: heatmap.buckets[bucket]?.map (a) => a.$$tag
# Gets rid of them after
.on 'mouseout', =>
if $location.path() == '/viewer' and not $location.search()?.id?
provider.notify method: 'setActiveHighlights'
# Does one of a few things when a tab is clicked depending on type
.on 'click', (bucket) =>
d3.event.stopPropagation()
# If it's the upper tab, switch to dynamic bucket mode,
# and scroll to next bucket above
if heatmap.isUpper bucket
$scope.dynamicBucket = true
threshold = offset + heatmap.index[0]
next = highlights.reduce (next, hl) ->
if next < hl.offset.top < threshold then hl.offset.top else next
, 0
provider.notify method: 'scrollTop', params: next - pad
# If it's the lower tab, switch do dynamic bucket mode,
# and scroll to next bucket below
else if heatmap.isLower bucket
$scope.dynamicBucket = true
threshold = offset + heatmap.index[0] + height - pad
next = highlights.reduce (next, hl) ->
if threshold < hl.offset.top < next then hl.offset.top else next
, Number.MAX_VALUE
provider.notify method: 'scrollTop', params: next - pad
# If it's neither of the above, load the bucket into the viewer
else
return unless drafts.discard()
$scope.dynamicBucket = false
$location.search({'id' : null })
annotator.showViewer heatmap.buckets[bucket]
{plugins, host, providers} = annotator
$scope.$watch 'sheet.collapsed', (newValue) ->
$scope.sheet.tab = if newValue then null else 'login'
......@@ -162,24 +87,27 @@ class App
plugins.Permissions.setUser(null)
delete plugins.Auth
$scope.reloadAnnotations() unless $scope.skipAuthChangeReload
delete $scope.skipAuthChangeReload
if newValue isnt oldValue
$scope.reloadAnnotations() unless $scope.skipAuthChangeReload
delete $scope.skipAuthChangeReload
$scope.$watch 'socialView.name', (newValue, oldValue) ->
return if newValue is oldValue
console.log "Social View changed to '" + newValue + "'. Reloading annotations."
$scope.reloadAnnotations()
$scope.$watch 'frame.visible', (newValue) ->
$scope.$watch 'frame.visible', (newValue, oldValue) ->
routeName = $location.path().replace /^\//, ''
if newValue
annotator.show()
annotator.provider?.notify method: 'showFrame'
annotator.host.notify method: 'showFrame', params: routeName
$element.find('.topbar').find('.tri').attr('draggable', true)
else
else if oldValue
$scope.sheet.collapsed = true
annotator.hide()
annotator.provider.notify method: 'setActiveHighlights'
annotator.provider.notify method: 'hideFrame'
annotator.host.notify method: 'hideFrame', params: routeName
for p in annotator.providers
p.channel.notify method: 'setActiveHighlights'
$element.find('.topbar').find('.tri').attr('draggable', false)
$scope.$watch 'sheet.collapsed', (newValue) ->
......@@ -252,9 +180,10 @@ class App
$scope.toggleAlwaysOnHighlights = ->
$scope.alwaysOnMode = not $scope.alwaysOnMode
provider.notify
method: 'setAlwaysOnMode'
params: $scope.alwaysOnMode
for p in providers
p.channel.notify
method: 'setAlwaysOnMode'
params: $scope.alwaysOnMode
$scope.highlightingMode = false
......@@ -278,9 +207,10 @@ class App
$scope.highlightingMode = not $scope.highlightingMode
annotator.socialView.name =
if $scope.highlightingMode then "single-player" else "none"
provider.notify
method: 'setHighlightingMode'
params: $scope.highlightingMode
for p in providers
p.channel.notify
method: 'setHighlightingMode'
params: $scope.highlightingMode
$scope.createUnattachedAnnotation = ->
return unless drafts.discard() # Invoke draft support
......@@ -446,41 +376,31 @@ class App
, 1500
$scope.reloadAnnotations = ->
if annotator.plugins.Store?
$scope.$root.annotations = []
annotator.threading.thread []
Store = annotator.plugins.Store
annotations = Store.annotations
annotator.plugins.Store.annotations = []
annotator.deleteAnnotation a for a in annotations
# XXX: Hacky hacky stuff to ensure that any search requests in-flight
# at this time have no effect when they resolve and that future events
# have no effect on this Store. Unfortunately, it's not possible to
# unregister all the events or properly unload the Store because the
# registration loses the closure. The approach here is perhaps
# cleaner than fishing them out of the jQuery private data.
# * Overwrite the Store's handle to the annotator, giving it one
# with a noop `loadAnnotations` method.
Store.annotator = loadAnnotations: angular.noop
# * Make all api requests into a noop.
Store._apiRequest = angular.noop
# * Make the update function into a noop.
Store.updateAnnotation = angular.noop
# * Remove the plugin and re-add it to the annotator.
delete annotator.plugins.Store
annotator.considerSocialView Store.options
annotator.addStore Store.options
href = annotator.plugins.Store.options.loadFromSearch.uri
for uri in annotator.plugins.Document.uris()
unless uri is href # Do not load the href again
console.log "Also loading annotations for: " + uri
annotator.plugins.Store.loadAnnotationsFromSearch uri: uri
$scope.new_updates = 0
$scope.new_updates = 0
$scope.$root.annotations = []
annotator.threading.thread []
Store = annotator.plugins.Store
annotations = Store.annotations
annotator.plugins.Store.annotations = []
annotator.deleteAnnotation a for a in annotations
# XXX: Hacky hacky stuff to ensure that any search requests in-flight
# at this time have no effect when they resolve and that future events
# have no effect on this Store. Unfortunately, it's not possible to
# unregister all the events or properly unload the Store because the
# registration loses the closure. The approach here is perhaps
# cleaner than fishing them out of the jQuery private data.
# * Overwrite the Store's handle to the annotator, giving it one
# with a noop `loadAnnotations` method.
Store.annotator = loadAnnotations: angular.noop
# * Make all api requests into a noop.
Store._apiRequest = angular.noop
# * Make the update function into a noop.
Store.updateAnnotation = angular.noop
# * Remove the plugin and re-add it to the annotator.
delete annotator.plugins.Store
annotator.addPlugin 'Store', annotator.options.Store
# Notifications
$scope.notifications = []
......@@ -516,11 +436,10 @@ class App
else text = 'changes.'
notif.text = 'Click to load ' + updates + ' ' + text
return unless updates
$element.find('.tri').toggle('fg_highlight',{color:'lightblue'})
$timeout ->
$element.find('.tri').toggle('fg_highlight',{color:'lightblue'})
, 500
for p in annotator.providers
p.channel.notify
method: 'updateNotificationCounter'
params: updates
$scope.$watch 'show_search', (value, old) ->
if value and not old
......@@ -539,11 +458,7 @@ class App
path = "#{path}__streamer__"
# Collect all uris we should watch
href = annotator.plugins.Store.options.loadFromSearch.uri
uris = href + '' # For copy
for uri in annotator.plugins.Document.uris()
unless uri is href
uris += "," + uri
uris = (e for e of annotator.plugins.Store.entities).join ','
filter =
streamfilter
......@@ -662,6 +577,7 @@ class Annotation
reply =
references: references
uri: $scope.thread.message.uri
annotator.publish 'beforeAnnotationCreated', [reply]
......@@ -756,14 +672,17 @@ class Annotation
class Editor
this.$inject = ['$location', '$routeParams', '$scope', 'annotator']
constructor: ($location, $routeParams, $scope, annotator) ->
{providers} = annotator
save = ->
$location.path('/viewer').search('id', $scope.annotation.id).replace()
annotator.provider.notify method: 'onEditorSubmit'
annotator.provider.notify method: 'onEditorHide'
for p in providers
p.channel.notify method: 'onEditorSubmit'
p.channel.notify method: 'onEditorHide'
cancel = ->
$location.path('/viewer').search('id', null).replace()
annotator.provider.notify method: 'onEditorHide'
for p in providers
p.channel.notify method: 'onEditorHide'
$scope.action = if $routeParams.action? then $routeParams.action else 'create'
if $scope.action is 'create'
......@@ -791,7 +710,7 @@ class Viewer
$location, $rootScope, $routeParams, $scope,
annotator
) ->
{provider, threading} = annotator
{providers, threading} = annotator
$scope.focus = (annotation) ->
if angular.isArray annotation
......@@ -800,13 +719,16 @@ class Viewer
highlights = [annotation.$$tag]
else
highlights = []
provider.notify method: 'setActiveHighlights', params: highlights
for p in providers
p.channel.notify
method: 'setActiveHighlights'
params: highlights
class Search
this.$inject = ['$filter', '$location', '$routeParams', '$scope', 'annotator']
constructor: ($filter, $location, $routeParams, $scope, annotator) ->
{provider, threading} = annotator
{providers, threading} = annotator
$scope.highlighter = '<span class="search-hl-active">$&</span>'
$scope.filter_orderBy = $filter('orderBy')
......@@ -863,11 +785,13 @@ class Search
highlights = [annotation.$$tag]
else
highlights = []
provider.notify method: 'setActiveHighlights', params: highlights
for p in providers
p.channel.notify
method: 'setActiveHighlights'
params: highlights
refresh = =>
$scope.search_filter = $routeParams.matched
heatmap = annotator.plugins.Heatmap
# Create the regexps for highlighting the matches inside the annotations' bodies
$scope.text_tokens = $routeParams.in_body_text.split ' '
......@@ -884,30 +808,29 @@ class Search
threads = []
$scope.render_order = {}
# Choose the root annotations to work with
for bucket in heatmap.buckets
for annotation in bucket
# The annotation itself is a hit.
thread = annotation.thread
if annotation.id in $scope.search_filter
threads.push thread
$scope.render_order[annotation.id] = []
buildRenderOrder(annotation.id, [thread])
continue
# Maybe it has a child we were looking for
children = thread.flattenChildren()
has_search_result = false
if children?
for child in children
if child.id in $scope.search_filter
has_search_result = true
break
if has_search_result
threads.push thread
$scope.render_order[annotation.id] = []
buildRenderOrder(annotation.id, [thread])
for annotation in $scope.annotations
# The annotation itself is a hit.
thread = annotation.thread
if annotation.id in $scope.search_filter
threads.push thread
$scope.render_order[annotation.id] = []
buildRenderOrder(annotation.id, [thread])
continue
# Maybe it has a child we were looking for
children = thread.flattenChildren()
has_search_result = false
if children?
for child in children
if child.id in $scope.search_filter
has_search_result = true
break
if has_search_result
threads.push thread
$scope.render_order[annotation.id] = []
buildRenderOrder(annotation.id, [thread])
# Re-construct exact order the annotation threads will be shown
......
$ = Annotator.$
class Annotator.Guest extends Annotator
# Events to be bound on Annotator#element.
events:
".annotator-adder button click": "onAdderClick"
".annotator-adder button mousedown": "onAdderMousedown"
".annotator-hl mousedown": "onHighlightMousedown"
".annotator-hl click": "onHighlightClick"
# Plugin configuration
options:
Document: {}
# Internal state
tool: 'comment'
visibleHighlights: false
constructor: (element, options) ->
Gettext.prototype.parse_locale_data annotator_locale_data
super
@frame = $('<div></div>')
.appendTo(@wrapper)
.addClass('annotator-frame annotator-outer annotator-collapsed')
delete @options.app
this.addPlugin 'Bridge',
formatter: (annotation) =>
formatted = {}
if annotation.document?
formatted['uri'] = @plugins.Document.uri()
for k, v of annotation when k isnt 'highlights'
formatted[k] = v
formatted
onConnect: (source, origin, scope) =>
@panel = this._setupXDM
window: source
origin: origin
scope: "#{scope}:provider"
onReady: =>
console.log "Guest functions are ready for #{origin}"
# Load plugins
for own name, opts of @options
if not @plugins[name]
this.addPlugin(name, opts)
# Scan the document text with the DOM Text libraries
this.scanDocument "Annotator initialized"
_setupXDM: (options) ->
channel = Channel.build options
channel
.bind('onEditorHide', this.onEditorHide)
.bind('onEditorSubmit', this.onEditorSubmit)
.bind('setActiveHighlights', (ctx, tags=[]) =>
@wrapper.find('.annotator-hl')
.each ->
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')
)
.bind('adderClick', =>
@onAdderClick @event
)
.bind('getDocumentInfo', =>
return {
uri: @plugins.Document.uri()
metadata: @plugins.Document.metadata
}
)
.bind('setTool', (ctx, name) => this.setTool name)
.bind('setVisibleHighlights', (ctx, state) =>
this.setVisibleHighlights state
)
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 'click', =>
unless @ignoreMouseup or @noBack
setTimeout =>
unless @selectedRanges?.length
@panel?.notify method: 'back'
this._setupMatching()
@domMatcher.setRootNode @wrapper[0]
this
# These methods aren't used in the iframe-hosted configuration of Annotator.
_setupViewer: -> this
_setupEditor: -> this
showViewer: (annotations) =>
@panel?.notify method: "showViewer", params: (a.id for a in annotations)
updateViewer: (annotations) =>
@panel?.notify method: "updateViewer", params: (a.id for a in annotations)
showEditor: (annotation) => @plugins.Bridge.showEditor annotation
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 panel from closing while annotating.
unless event and this.isAnnotator(event.target)
@mouseIsDown = true
confirmSelection: ->
return true unless @selectedRanges.length is 1
target = this.getTargetFromRange @selectedRanges[0]
selector = this.findSelector target.selector, "TextQuoteSelector"
length = selector.exact.length
if length > 2 then return true
return confirm "You have selected a very short piece of text: only " + length + " chars. Are you sure you want to highlight this?"
onSuccessfulSelection: (event) ->
if @tool is 'highlight'
# Do we really want to make this selection?
return unless this.confirmSelection()
# Create the annotation right away
# Don't use the default method to create an annotation,
# because we don't want to publish the beforeAnnotationCreated event
# just yet.
#
# annotation = this.createAnnotation()
#
# Create an empty annotation manually instead
annotation = {inject: true}
annotation = this.setupAnnotation annotation
$(annotation.highlights).addClass 'annotator-hl'
# Notify listeners
this.publish 'beforeAnnotationCreated', annotation
this.publish 'annotationCreated', annotation
else
super event
# When clicking on a highlight in highlighting mode,
# set @noBack to true to prevent the sidebar from closing
onHighlightMousedown: (event) =>
if (@tool is 'highlight') or @visibleHighlights then @noBack = true
# When clicking on a highlight in highlighting mode,
# tell the sidebar to bring up the viewer for the relevant annotations
onHighlightClick: (event) =>
return unless (@tool is 'highlight') or @visibleHighlights and @noBack
# Collect relevant annotations
annotations = $(event.target)
.parents('.annotator-hl')
.addBack()
.map -> return $(this).data("annotation")
# Tell sidebar to show the viewer for these annotations
this.showViewer annotations
# We have already prevented closing the sidebar, now reset this flag
@noBack = false
setTool: (name) ->
@tool = name
@panel?.notify
method: 'setTool'
params: name
switch name
when 'comment'
this.setVisibleHighlights this.visibleHighlights, true
when 'highlight'
this.setVisibleHighlights true, true
setVisibleHighlights: (state=true, temporary=false) ->
unless temporary
@visibleHighlights = state
@panel?.notify
method: 'setVisibleHighlights'
params: state
markerClass = 'annotator-highlights-always-on'
if state or (@tool is 'highlight')
@element.addClass markerClass
else
@element.removeClass markerClass
addComment: ->
sel = @selectedRanges # Save the selection
# Nuke the selection, since we won't be using that.
# We will attach this to the end of the document.
# Our override for setupAnnotation will add that highlight.
@selectedRanges = []
this.onAdderClick() # Open editor (with 0 targets)
@selectedRanges = sel # restore the selection
createFakeCommentRange: ->
posSelector =
type: "TextPositionSelector"
start: @domMapper.corpus.length - 1
end: @domMapper.corpus.length
anchor = this.findAnchorFromPositionSelector selector: [posSelector]
anchor.range
# Override for setupAnnotation
setupAnnotation: (annotation) ->
# Set up annotation as usual
annotation = super(annotation)
# Does it have proper highlights?
unless annotation.highlights?.length or annotation.references?.length or annotation.target?.length
# No highlights and no references means that this is a comment,
# or re-attachment has failed, but we'll skip orphaned annotations.
# Get a fake range at the end of the document, and highlight it
range = this.createFakeCommentRange()
hl = this.highlightRange range
# Register this highlight for the annotation, and vica versa
$.merge annotation.highlights, hl
$(hl).data('annotation', annotation)
annotation
# Open the sidebar
showFrame: ->
@panel?.notify method: 'open'
# Close the sidebar
hideFrame: ->
@panel?.notify method: 'back'
addToken: (token) =>
@api.notify
method: 'addToken'
params: token
onAdderClick: (event) =>
"""
Differs from upstream in a few ways:
- Don't fire annotationCreated events: that's the job of the sidebar
- Save the event for retriggering if login interrupts the flow
"""
event?.preventDefault()
# Save the event for restarting edit
@event = event
# Hide the adder
@adder.hide()
position = @adder.position()
# Show a temporary highlight so the user can see what they selected
# Also extract the quotation and serialize the ranges
annotation = this.setupAnnotation(this.createAnnotation())
$(annotation.highlights).addClass('annotator-hl-temporary')
# Subscribe to the editor events
# Make the highlights permanent if the annotation is saved
save = =>
do cleanup
$(annotation.highlights).removeClass('annotator-hl-temporary')
# 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)
$ = Annotator.$
class Annotator.Host extends Annotator
# Events to be bound on Annotator#element.
events:
".annotator-adder button click": "onAdderClick"
".annotator-adder button mousedown": "onAdderMousedown"
".annotator-hl mousedown": "onHighlightMousedown"
".annotator-hl click": "onHighlightClick"
# Plugin configuration
options: {}
class Annotator.Host extends Annotator.Guest
# Drag state variables
drag:
delta: 0
......@@ -19,12 +9,6 @@ class Annotator.Host extends Annotator
tick: false
constructor: (element, options) ->
Gettext.prototype.parse_locale_data annotator_locale_data
super
@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?
......@@ -34,269 +18,121 @@ class Annotator.Host extends Annotator
# XXX: Hack for missing window.location.origin in FF
hostOrigin ?= window.location.protocol + "//" + window.location.host
@frame = $('<iframe></iframe>')
.css(display: 'none')
.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)
# Scan the document text with the DOM Text libraries
this.scanDocument "Annotator initialized"
setPersistentHighlights: ->
body = $('body')
markerClass = 'annotator-highlights-always-on'
if @alwaysOnMode or @highlightingMode
body.addClass markerClass
else
body.removeClass markerClass
# Open the sidebar
showFrame: ->
@panel?.notify method: 'open'
# Close the sidebar
hideFrame: ->
@panel?.notify method: 'back'
# Set dynamic bucket mode on the sidebar
setDynamicBucketMode: (value) ->
@panel?.notify method: 'setDynamicBucketMode', params: value
_setupXDM: ->
# Set up the bridge plugin, which bridges the main annotation methods
# between the host page and the panel widget.
whitelist = ['diffHTML', 'diffCaseOnly', 'quote', 'ranges', 'target', 'references']
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
this.addPlugin 'Document'
# Build a channel for the publish API
@api = Channel.build
origin: '*'
scope: 'annotator:api'
window: @frame[0].contentWindow
# Build a channel for the panel UI
@panel = Channel.build
origin: '*'
scope: 'annotator:panel'
window: @frame[0].contentWindow
onReady: =>
@frame.css('display', '')
@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'
)
.bind('hideFrame', =>
@frame.css 'margin-left': ''
@frame.removeClass 'annotator-no-transition'
@frame.addClass 'annotator-collapsed'
)
.bind('dragFrame', (ctx, screenX) =>
if screenX > 0
if @drag.last?
@drag.delta += screenX - @drag.last
@drag.last = screenX
unless @drag.tick
@drag.tick = true
window.requestAnimationFrame this._dragRefresh
)
app = $('<iframe></iframe>')
.attr('seamless', '')
.attr('src', "#{options.app}#/?xdm=#{encodeURIComponent(hostOrigin)}")
.bind('getHighlights', =>
highlights: $(@wrapper).find('.annotator-hl')
.filter ->
this.offsetWidth > 0 || this.offsetHeight > 0
.map ->
offset: $(this).offset()
height: $(this).outerHeight(true)
data: $(this).data('annotation').$$tag
.get()
offset: $(window).scrollTop()
)
.bind('setActiveHighlights', (ctx, tags=[]) =>
@wrapper.find('.annotator-hl')
.each ->
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')
)
.bind('setAlwaysOnMode', (ctx, value) =>
@alwaysOnMode = value
this.setPersistentHighlights()
)
.bind('setHighlightingMode', (ctx, value) =>
@highlightingMode = value
if @highlightingMode then @adder.hide()
this.setPersistentHighlights()
)
.bind('addComment', (ctx) =>
sel = @selectedRanges # Save the selection
adderShown = @adder.is ":visible" # Save the state of adder icon
# Nuke the selection, since we won't be using that.
# We will attach this to the end of the document.
# Our override for setupAnnotation will add that highlight.
@selectedRanges = []
this.onAdderClick() # Open editor (with 0 targets)
setTimeout (=> # At some point, later
@selectedRanges = sel # restore the selection
if adderShown then @adder.show() # restore the state of addder icon
), 200
)
.bind('getHref', => this.getHref())
.bind('getMaxBottom', =>
sel = '*' + (":not(.annotator-#{x})" for x in [
'adder', 'outer', 'notice', 'filter', 'frame'
]).join('')
# use the maximum bottom position in the page
all = for el in $(document.body).find(sel)
p = $(el).css('position')
t = $(el).offset().top
z = $(el).css('z-index')
if (y = /\d+/.exec($(el).css('top'))?[0])
t = Math.min(Number y, t)
if (p == 'absolute' or p == 'fixed') and t == 0 and z != 'auto'
bottom = $(el).outerHeight(false)
# but don't go larger than 80, because this isn't bulletproof
if bottom > 80 then 0 else bottom
else
0
Math.max.apply(Math, all)
)
.bind('scrollTop', (ctx, y) =>
$('html, body').stop().animate {scrollTop: y}, 600
)
.bind('setDrag', (ctx, drag) =>
@drag.enabled = drag
@drag.last = null
)
.bind('adderClick', =>
@onAdderClick @event
)
.bind('getDocumentInfo', =>
return {
uri: @plugins.Document.uri()
metadata: @plugins.Document.metadata
}
)
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', =>
unless @ignoreMouseup or @noBack
setTimeout =>
unless @selectedRanges?.length then this.hideFrame()
this._setupMatching()
@domMatcher.setRootNode @wrapper[0]
this
_setupDocumentEvents: ->
tick = false
timeout = null
touch = false
update = =>
if touch
# Defer updates on mobile until after touch events are over
if timeout then cancelTimeout timeout
timeout = setTimeout =>
timeout = null
do updateFrame
, 400
else
do updateFrame
updateFrame = =>
unless tick
tick = true
requestAnimationFrame =>
tick = false
if touch
# CSS "position: fixed" is hell of broken on most mobile devices
@frame?.css
display: ''
height: $(window).height()
position: 'absolute'
top: $(window).scrollTop()
@panel?.notify method: 'publish', params: 'hostUpdated'
document.addEventListener 'touchmove', update
document.addEventListener 'touchstart', =>
touch = true
@frame?.css
display: 'none'
do update
super
document.addEventListener 'dragover', (event) =>
unless @drag.enabled then return
if @drag.last?
@drag.delta += event.screenX - @drag.last
app.appendTo(@frame)
if @toolbar?
@toolbar.addClass 'annotator-hide'
app
.on('mouseenter', => @toolbar.removeClass 'annotator-hide')
.on('mouseleave', => @toolbar.addClass 'annotator-hide')
if @plugins.Heatmap?
this._setupDragEvents()
@plugins.Heatmap.element.on 'click', (event) =>
if @frame.hasClass 'annotator-collapsed'
this.showFrame()
else
this.hideFrame()
_setupXDM: (options) ->
channel = super
channel
.bind('showFrame', (ctx, routeName) =>
unless @drag.enabled
@frame.css 'margin-left': "#{-1 * @frame.width()}px"
@frame.removeClass 'annotator-no-transition'
@frame.removeClass 'annotator-collapsed'
switch routeName
when 'editor'
this.publish 'annotationEditorShown'
when 'viewer'
this.publish 'annotationViewerShown'
)
.bind('hideFrame', (ctx, routeName) =>
@frame.css 'margin-left': ''
@frame.removeClass 'annotator-no-transition'
@frame.addClass 'annotator-collapsed'
switch routeName
when 'editor'
this.publish 'annotationEditorHidden'
when 'viewer'
this.publish 'annotationViewerHidden'
)
.bind('dragFrame', (ctx, screenX) => this._dragUpdate screenX)
.bind('getMaxBottom', =>
sel = '*' + (":not(.annotator-#{x})" for x in [
'adder', 'outer', 'notice', 'filter', 'frame'
]).join('')
# use the maximum bottom position in the page
all = for el in $(document.body).find(sel)
p = $(el).css('position')
t = $(el).offset().top
z = $(el).css('z-index')
if (y = /\d+/.exec($(el).css('top'))?[0])
t = Math.min(Number y, t)
if (p == 'absolute' or p == 'fixed') and t == 0 and z != 'auto'
bottom = $(el).outerHeight(false)
# but don't go larger than 80, because this isn't bulletproof
if bottom > 80 then 0 else bottom
else
0
Math.max.apply(Math, all)
)
.bind('updateNotificationCounter', (ctx, count) =>
this.publish 'updateNotificationCounter', count
)
_setupDragEvents: ->
el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
el.width = el.height = 1
@element.append el
handle = @plugins.Heatmap.element[0]
handle.draggable = true
handle.addEventListener 'dragstart', (event) =>
event.dataTransfer.dropEffect = 'none'
event.dataTransfer.effectAllowed = 'none'
event.dataTransfer.setData 'text/plain', ''
event.dataTransfer.setDragImage el, 0, 0
@drag.enabled = true
@drag.last = event.screenX
unless @drag.tick
@drag.tick = true
window.requestAnimationFrame this._dragRefresh
$(window).on 'resize scroll', update
$(document.body).on 'resize scroll', '*', update
m = parseInt (getComputedStyle @frame[0]).marginLeft
@frame.css
'margin-left': "#{m}px"
this.showFrame()
if window.PDFView?
# XXX: PDF.js hack
$(PDFView.container).on 'scroll', update
handle.addEventListener 'dragend', (event) =>
@drag.enabled = false
@drag.last = null
super
document.addEventListener 'dragover', (event) =>
this._dragUpdate event.screenX
# These methods aren't used in the iframe-hosted configuration of Annotator.
_setupViewer: -> this
_setupEditor: -> this
_dragUpdate: (screenX) =>
unless @drag.enabled then return
if @drag.last?
@drag.delta += screenX - @drag.last
@drag.last = screenX
unless @drag.tick
@drag.tick = true
window.requestAnimationFrame this._dragRefresh
_dragRefresh: =>
d = @drag.delta
......@@ -312,159 +148,3 @@ class Annotator.Host extends Annotator
@frame.css
'margin-left': "#{m}px"
width: "#{w}px"
showViewer: (annotations) =>
@panel?.notify method: "showViewer", params: (a.$$tag for a in annotations)
updateViewer: (annotations) =>
@panel?.notify method: "updateViewer", params: (a.$$tag for a in annotations)
showEditor: (annotation) => @plugins.Bridge.showEditor annotation
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 panel from closing while annotating.
unless event and this.isAnnotator(event.target)
@mouseIsDown = true
confirmSelection: ->
return true unless @selectedRanges.length is 1
target = this.getTargetFromRange @selectedRanges[0]
selector = this.findSelector target.selector, "TextQuoteSelector"
length = selector.exact.length
if length > 2 then return true
return confirm "You have selected a very short piece of text: only " + length + " chars. Are you sure you want to highlight this?"
onSuccessfulSelection: (event) ->
if @highlightingMode
# Do we really want to make this selection?
return unless this.confirmSelection()
# Create the annotation right away
# Don't use the default method to create an annotation,
# because we don't want to publish the beforeAnnotationCreated event
# just yet.
#
# annotation = this.createAnnotation()
#
# Create an empty annotation manually instead
annotation = {}
annotation = this.setupAnnotation annotation
$(annotation.highlights).addClass 'annotator-hl'
# Tell the sidebar about the new annotation
@plugins.Bridge.injectAnnotation annotation
# Switch view to show the new annotation
this.updateViewer [ annotation ]
else
super event
createFakeCommentRange: ->
posSelector =
type: "TextPositionSelector"
start: @domMapper.corpus.length - 1
end: @domMapper.corpus.length
anchor = this.findAnchorFromPositionSelector selector: [posSelector]
anchor.range
# Override for setupAnnotation
setupAnnotation: (annotation) ->
# Set up annotation as usual
annotation = super(annotation)
# Does it have proper highlights?
unless annotation.highlights?.length or annotation.references?.length or annotation.target?.length
# No highlights and no references means that this is a comment,
# or re-attachment has failed, but we'll skip orphaned annotations.
# Get a fake range at the end of the document, and highlight it
range = this.createFakeCommentRange()
hl = this.highlightRange range
# Register this highlight for the annotation, and vica versa
$.merge annotation.highlights, hl
$(hl).data('annotation', annotation)
annotation
# When clicking on a highlight in highlighting mode,
# set @noBack to true to prevent the sidebar from closing
onHighlightMousedown: (event) =>
if @highlightingMode or @alwaysOnMode then @noBack = true
# When clicking on a highlight in highlighting mode,
# tell the sidebar to bring up the viewer for the relevant annotations
onHighlightClick: (event) =>
return unless @highlightingMode or @alwaysOnMode and @noBack
# We have already prevented closing the sidebar, now reset this flag
@noBack = false
# Collect relevant annotations
annotations = $(event.target)
.parents('.annotator-hl')
.addBack()
.map -> return $(this).data("annotation")
# Tell sidebar to show the viewer for these annotations
this.showViewer annotations
# Switch off dynamic bucket mode
this.setDynamicBucketMode false
addToken: (token) =>
@api.notify
method: 'addToken'
params: token
onAdderClick: (event) =>
"""
Differs from upstream in a few ways:
- Don't fire annotationCreated events: that's the job of the sidebar
- Save the event for retriggering if login interrupts the flow
"""
event?.preventDefault()
# Save the event for restarting edit
@event = event
# Hide the adder
@adder.hide()
position = @adder.position()
# Show a temporary highlight so the user can see what they selected
# Also extract the quotation and serialize the ranges
annotation = this.setupAnnotation(this.createAnnotation())
$(annotation.highlights).addClass('annotator-hl-temporary')
# Subscribe to the editor events
# Make the highlights permanent if the annotation is saved
save = =>
do cleanup
$(annotation.highlights).removeClass('annotator-hl-temporary')
# 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)
$ = Annotator.$
class Annotator.Plugin.Bridge extends Annotator.Plugin
# These events maintain the awareness of annotations between the two
# communicating annotators.
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationCreated': 'annotationCreated'
'annotationUpdated': 'annotationUpdated'
'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded'
......@@ -15,6 +19,14 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
# Scope identifier to distinguish this channel from any others
scope: 'annotator:bridge'
# When this is true, this bridge will act as a gateway and, similar to DHCP,
# offer to connect to bridges in other frames it discovers.
gateway: false
# A callback to invoke when a connection is established. The function is
# passed two arguments, the source window and origin of the other frame.
onConnect: -> true
# Formats an annotation for sending across the bridge
formatter: (annotation) -> annotation
......@@ -32,7 +44,13 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
# Cache of annotations which have crossed the bridge for fast, encapsulated
# association of annotations received in arguments to window-local copies.
cache: {}
cache: null
# Connected bridge links
links: null
# Annotations currently being updated -- used to avoid event callback loops
updating: null
constructor: (elem, options) ->
if options.window?
......@@ -46,10 +64,13 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
else
super
@cache = {}
@links = []
@updating = {}
pluginInit: ->
console.log "Initializing bridge plugin. Connecting to #{@options.origin}"
@options.onReady = this.onReady
@channel = Channel.build @options
$(window).on 'message', this._onMessage
this._beacon()
# Assign a non-enumerable tag to objects which cross the bridge.
# This tag is used to identify the objects between message.
......@@ -79,110 +100,211 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
tag: annotation.$$tag
msg: msg
beforeAnnotationCreated: (annotation) =>
return if annotation.$$tag?
this.beforeCreateAnnotation annotation
annotationDeleted: (annotation) =>
return unless annotation.$$tag? and @cache[annotation.$$tag]
this.deleteAnnotation annotation, (err) =>
if err then @annotator.setupAnnotation annotation
else delete @cache[annotation.$$tag]
annotationsLoaded: (annotations) =>
this.setupAnnotation a for a in annotations
onReady: =>
@channel
# Construct a channel to another frame
_build: (options) ->
console.log "Bridge plugin connecting to #{options.origin}"
channel = Channel.build(options)
## Remote method call bindings
.bind('setupAnnotation', (txn, annotation) =>
this._format (@annotator.setupAnnotation (this._parse annotation))
)
.bind('beforeCreateAnnotation', (txn, annotation) =>
annotation = this._parse annotation
delete @cache[annotation.$$tag]
@annotator.publish 'beforeAnnotationCreated', annotation
@cache[annotation.$$tag] = annotation
this._format annotation
)
.bind('setupAnnotation', (txn, annotation) =>
this._format (@annotator.setupAnnotation (this._parse annotation))
.bind('createAnnotation', (txn, annotation) =>
annotation = this._parse annotation
delete @cache[annotation.$$tag]
@annotator.publish 'annotationCreated', annotation
@cache[annotation.$$tag] = annotation
this._format annotation
)
.bind('updateAnnotation', (txn, annotation) =>
this._format (@annotator.updateAnnotation (this._parse annotation))
annotation = this._parse annotation
delete @cache[annotation.$$tag]
annotation = @annotator.updateAnnotation annotation
@cache[annotation.$$tag] = annotation
this._format annotation
)
.bind('deleteAnnotation', (txn, annotation) =>
annotation = this._parse annotation
delete @cache[annotation.$$tag]
annotation = @annotator.deleteAnnotation annotation
result = this._format annotation
res = this._format annotation
delete @cache[annotation.$$tag]
result
res
)
## Notifications
.bind('loadAnnotations', (txn, annotations) =>
annotations = (this._parse a for a in annotations when not @cache[a.tag])
@annotator.loadAnnotations annotations
)
.bind('showEditor', (ctx, annotation) =>
@annotator.showEditor (this._parse annotation)
)
.bind('injectAnnotation', (ctx, annotation) =>
a = this._parse annotation
# Send out a beacon to let other frames know to connect to us
_beacon: ->
queue = [window.top]
while queue.length
parent = queue.shift()
if parent isnt window
console.log window.location.toString(), 'sending beacon...'
parent.postMessage '__annotator_dhcp_discovery', @options.origin
for child in parent.frames
queue.push child
# This is a special marker flag, which indicated that
# this annotation is not new, and therefore should not be pushed
# among the drafts
a.inject = true
@annotator.publish 'beforeAnnotationCreated', a
@annotator.publish 'annotationCreated', a
)
# Make a method call on all links
_call: (options) ->
_makeDestroyFn = (c) =>
(error, reason) =>
c.destroy()
@links = (l for l in @links when l.channel isnt c)
deferreds = @links.map (l) ->
d = $.Deferred().fail (_makeDestroyFn l.channel)
options = $.extend {}, options,
success: (result) -> d.resolve result
error: (error, reason) ->
if error isnt 'timeout_error'
d.reject error, reason
else
d.resolve null
timeout: 1000
l.channel.call options
d.promise()
$.when(deferreds...)
.then (results...) =>
annotation = {}
for r in results when r isnt null
$.extend annotation, (this._parse r)
options.callback? null, annotation
.fail (failure) =>
options.callback? failure
# Publish a notification to all links
_notify: (options) ->
for l in @links
l.channel.notify options
_onMessage: (e) =>
{source, origin, data} = e.originalEvent
match = data.match /^__annotator_dhcp_(discovery|ack|offer)(:\d+)?$/
return unless match
if match[1] is 'discovery'
if @options.gateway
scope = ':' + ('' + Math.random()).replace(/\D/g, '')
source.postMessage '__annotator_dhcp_offer' + scope, origin
else
source.postMessage '__annotator_dhcp_ack', origin
return
else if match[1] is 'ack'
if @options.gateway
scope = ':' + ('' + Math.random()).replace(/\D/g, '')
source.postMessage '__annotator_dhcp_offer' + scope, origin
else
return
else if match[1] is 'offer'
if @options.gateway
return
else
scope = match[2]
scope = @options.scope + scope
options = $.extend {}, @options,
window: source
origin: origin
scope: scope
onReady: =>
options.onConnect.call @annotator, source, origin, scope
channel.notify
method: 'loadAnnotations'
params: (this._format a for t, a of @cache)
channel = this._build options
@links.push
channel: channel
window: source
beforeAnnotationCreated: (annotation) =>
return if annotation.$$tag?
this.beforeCreateAnnotation annotation
this
annotationCreated: (annotation) =>
return unless annotation.$$tag? and @cache[annotation.$$tag]
this.createAnnotation annotation
this
annotationUpdated: (annotation) =>
return unless annotation.$$tag? and @cache[annotation.$$tag]
this.updateAnnotation annotation
this
annotationDeleted: (annotation) =>
return unless annotation.$$tag? and @cache[annotation.$$tag]
this.deleteAnnotation annotation, (err) =>
if err then @annotator.setupAnnotation annotation
else delete @cache[annotation.$$tag]
this
annotationsLoaded: (annotations) =>
this._notify
method: 'loadAnnotations'
params: (this._format a for a in annotations when not a.$$tag?)
this
beforeCreateAnnotation: (annotation, cb) ->
@channel.call
this._call
method: 'beforeCreateAnnotation'
params: this._format annotation
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
callback: cb
annotation
setupAnnotation: (annotation, cb) ->
@channel.call
this._call
method: 'setupAnnotation'
params: this._format annotation
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
callback: cb
annotation
createAnnotation: (annotation, cb) ->
this._call
method: 'createAnnotation'
params: this._format annotation
callback: cb
annotation
updateAnnotation: (annotation, cb) ->
@channel.call
this._call
method: 'updateAnnotation'
params: this._format annotation
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
callback: cb
annotation
deleteAnnotation: (annotation, cb) ->
@channel.call
this._call
method: 'deleteAnnotation'
params: this._format annotation
success: (annotation) =>
annotation = this._parse annotation
cb? null, annotation
error: (error, reason) => cb? {error, reason}
callback: cb
annotation
showEditor: (annotation) ->
@channel.notify
this._notify
method: 'showEditor'
params: this._format annotation
this
injectAnnotation: (annotation) ->
@channel.notify
method: 'injectAnnotation'
params: this._format annotation
this
$ = Annotator.$
class Annotator.Plugin.Heatmap extends Annotator.Plugin
# prototype constants
BUCKET_THRESHOLD_PAD: 40
BUCKET_THRESHOLD_PAD: 25
BUCKET_SIZE: 50
BOTTOM_CORRECTION: 14
......@@ -38,10 +40,44 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
# index for fast hit detection in the buckets
index: []
# whether to update the viewer as the window is scrolled
dynamicBucket: true
constructor: (element, options) ->
super $(@html), options
this._rebaseUrls()
@element.appendTo element
# this._rebaseUrls() -- not clear this is a great idea
if @options.container?
$(@options.container).append @element
else
$(element).append @element
pluginInit: ->
return unless d3?
events = [
'annotationCreated', 'annotationUpdated', 'annotationDeleted',
'annotationsLoaded'
]
for event in events
if event is 'annotationCreated'
@annotator.subscribe event, =>
@dynamicBucket = false
this._update()
else
@annotator.subscribe event, this._update
@element.on 'click', (event) =>
event.stopPropagation()
this._fillDynamicBucket()
@dynamicBucket = true
$(window).on 'resize scroll', this._update
$(document.body).on 'resize scroll', '*', this._update
if window.PDFView?
# XXX: PDF.js hack
$(PDFView.container).on 'scroll', this._update
_rebaseUrls: ->
# We can't rely on browsers to implement the xml:base property correctly.
......@@ -62,7 +98,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
rect.attr('fill', fill)
rect.attr('filter', filter)
_collate: (a, b) =>
_collate: (a, b) ->
for i in [0..a.length-1]
if a[i] < b[i]
return -1
......@@ -77,11 +113,10 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
.interpolate(d3.interpolateHcl)
c(v).toString()
updateHeatmap: (data) =>
return unless d3?
wrapper = this.element.offsetParent()
{highlights, offset} = data
_update: =>
wrapper = @annotator.wrapper
highlights = wrapper.find('.annotator-hl')
defaultView = wrapper[0].ownerDocument.defaultView
# Keep track of buckets of annotations above and below the viewport
above = []
......@@ -89,10 +124,10 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
comments = []
# Construct control points for the heatmap highlights
points = highlights.reduce (points, hl, i) =>
x = hl.offset.top - wrapper.offset().top - offset
h = hl.height
d = hl.data
points = highlights.toArray().reduce (points, hl, i) =>
d = $(hl).data('annotation')
x = $(hl).offset().top - wrapper.offset().top - defaultView.pageYOffset
h = $(hl).outerHeight(true)
# XXX: Hacky stuff before unattached annotations V2
# Detect comments and push them into a separate bucket
......@@ -198,7 +233,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
max = 0
for b in @buckets
info = b.reduce (info, a) ->
subtotal = (a.thread?.flattenChildren()?.length or 0)
subtotal = a.reply_count or 0
return {
top: info.top + 1
replies: (info.replies or 0) + subtotal
......@@ -240,7 +275,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
[offsets[1], i, 1, 1e-6] ]
# Update the data bindings
element = d3.select(@element[0]).datum(data)
element = d3.select(@element[0]).datum(highlights)
# Update gradient stops
opacity = d3.scale.pow().domain([0, max]).range([.1, .6]).exponent(2)
......@@ -273,6 +308,72 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
tabs.enter().append('div')
.classed('heatmap-pointer', true)
# Creates highlights corresponding bucket when mouse is hovered
.on 'mousemove', (bucket) =>
highlights = wrapper.find('.annotator-hl')
highlights.toArray().forEach (hl) =>
if $(hl).data('annotation') in @buckets[bucket]
$(hl).addClass('annotator-hl-active')
else if not $(hl).hasClass('annotator-hl-temporary')
$(hl).removeClass('annotator-hl-active')
# Gets rid of them after
.on 'mouseout', =>
highlights = wrapper.find('.annotator-hl')
highlights.removeClass('annotator-hl-active')
# Does one of a few things when a tab is clicked depending on type
.on 'click', (bucket) =>
d3.event.stopPropagation()
highlights = wrapper.find('.annotator-hl')
pad = defaultView.innerHeight * .2
# If it's the upper tab, scroll to next bucket above
if @isUpper bucket
threshold = defaultView.pageYOffset
{next} = highlights.toArray().reduce (acc, hl) ->
{pos, next} = acc
if pos < $(hl).offset().top < threshold
pos: $(hl).offset().top
next: $(hl)
else
acc
, {pos: 0, next: null}
next?.scrollintoview
complete: ->
if this.parentNode is this.ownerDocument
scrollable = $(this.ownerDocument.body)
else
scrollable = $(this)
top = scrollable.scrollTop()
scrollable.stop().animate {scrollTop: top - pad}, 300
# If it's the lower tab, scroll to next bucket below
else if @isLower bucket
threshold = defaultView.pageYOffset + defaultView.innerHeight - pad
{next} = highlights.toArray().reduce (acc, hl) ->
{pos, next} = acc
if threshold < $(hl).offset().top < pos
pos: $(hl).offset().top
next: $(hl)
else
acc
, {pos: Number.MAX_VALUE, next: null}
next?.scrollintoview
complete: ->
if this.parentNode is this.ownerDocument
scrollable = $(this.ownerDocument.body)
else
scrollable = $(this)
top = scrollable.scrollTop()
scrollable.stop().animate {scrollTop: top + pad}, 300
# If it's neither of the above, load the bucket into the viewer
else
d3.event.stopPropagation()
@dynamicBucket = false
annotator.showViewer @buckets[bucket]
tabs.exit().remove()
tabs
......@@ -289,7 +390,24 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
.style 'display', (d) =>
if (@buckets[d].length is 0) then 'none' else ''
this.publish('updated')
if @dynamicBucket
this._fillDynamicBucket()
_fillDynamicBucket: =>
top = window.pageYOffset
bottom = top + $(window).innerHeight()
highlights = @annotator.wrapper.find('.annotator-hl')
visible = highlights.toArray().reduce (acc, hl) =>
if $(hl).offset().top >= top and $(hl).offset().top <= bottom
if $(hl).data('annotation') not in acc
acc.push $(hl).data('annotation')
else
annotation = $(hl).data('annotation')
if not (annotation.target?.length or annotation.references?.length)
acc.push annotation
acc
, []
@annotator.updateViewer visible
isUpper: (i) => i == 1
isLower: (i) => i == @index.length - 3
......
$ = Annotator.$
class Annotator.Plugin.Toolbar extends Annotator.Plugin
events:
'updateNotificationCounter': 'onUpdateNotificationCounter'
html:
element: '<div class="annotator-toolbar"></div>'
notification: '<div class="annotator-notification-counter"></div>"'
options:
items: [
"title": "Toggle Sidebar"
"class": "tri-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
collapsed = window.annotator.frame.hasClass('annotator-collapsed')
if collapsed
window.annotator.showFrame()
else
window.annotator.hideFrame()
,
"title": "Show Annotations"
"class": "alwaysonhighlights-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
state = not window.annotator.visibleHighlights
window.annotator.setVisibleHighlights state
if state
$(event.target).addClass('pushed')
else
$(event.target).removeClass('pushed')
,
"title": "Highlighting Mode"
"class": "highlighter-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
state = not (window.annotator.tool is 'highlight')
tool = if state then 'highlight' else 'comment'
window.annotator.setTool tool
if state
$(event.target).addClass('pushed')
else
$(event.target).removeClass('pushed')
,
"title": "New Comment"
"class": "commenter-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
window.annotator.addComment()
]
pluginInit: ->
@annotator.toolbar = @toolbar = $(@html.element)
if @options.container?
$(@options.container).append @toolbar
else
$(@element).append @toolbar
@notificationCounter = $(@html.notification)
@toolbar.append(@notificationCounter)
@buttons = @options.items.reduce (buttons, item) =>
button = $('<a></a>')
.attr('href', '')
.attr('title', item.title)
.on('click', item.click)
.addClass(item.class)
.data('state', false)
buttons.add button
, $()
list = $('<ul></ul>')
@buttons.appendTo(list)
@buttons.wrap('<li></li>')
@toolbar.append(list)
onUpdateNotificationCounter: (count) ->
element = $(@buttons[0])
element.toggle('fg_highlight', {color: 'lightblue'})
setTimeout ->
element.toggle('fg_highlight', {color: 'lightblue'})
, 500
switch
when count > 9
@notificationCounter.text('>9')
when 0 < count < 9
@notificationCounter.text("+#{count}")
else
@notificationCounter.text('')
class Hypothesis extends Annotator
events:
serviceDiscovery: 'serviceDiscovery'
'annotationCreated': 'updateAncestors'
'annotationUpdated': 'updateAncestors'
'annotationDeleted': 'updateAncestors'
'serviceDiscovery': 'serviceDiscovery'
# Plugin configuration
options:
noMatching: true
Discovery: {}
Heatmap: {}
Permissions:
permissions:
read: ['group:__world__']
......@@ -39,19 +41,29 @@ class Hypothesis extends Annotator
showViewPermissionsCheckbox: false,
userString: (user) -> user.replace(/^acct:(.+)@(.+)$/, '$1 on $2')
Threading: {}
Document: {}
# Internal state
dragging: false # * To enable dragging only when we really want to
ongoing_edit: false # * Is there an interrupted edit by login
providers: null
host: null
tool: 'comment'
visibleHighlights: false
# Here as a noop just to make the Permissions plugin happy
# XXX: Change me when Annotator stops assuming things about viewers
viewer:
addField: (-> )
this.$inject = ['$document', '$location', '$rootScope', '$route', 'authentication', 'drafts']
constructor: ($document, $location, $rootScope, $route, authentication, drafts) ->
this.$inject = [
'$document', '$location', '$rootScope', '$route', '$window',
'authentication', 'drafts'
]
constructor: (
$document, $location, $rootScope, $route, $window,
authentication, drafts
) ->
Gettext.prototype.parse_locale_data annotator_locale_data
super ($document.find 'body')
......@@ -61,13 +73,74 @@ class Hypothesis extends Annotator
@clientID = uuid.unparse buffer
$.ajaxSetup headers: "x-client-id": @clientID
@auth = authentication
@providers = []
@socialView =
name: "none" # "single-player"
this.patch_store()
# Load plugins
for own name, opts of @options
if not @plugins[name] and name of Annotator.Plugin
this.addPlugin(name, opts)
# Set up XDM connection
this._setupXDM()
# Set up the bridge plugin, which bridges the main annotation methods
# between the host page and the panel widget.
whitelist = [
'diffHTML', 'inject', 'quote', 'ranges', 'target', 'id', 'references',
'uri', 'diffCaseOnly'
]
this.addPlugin 'Bridge',
gateway: true
formatter: (annotation) =>
formatted = {}
for k, v of annotation when k in whitelist
formatted[k] = v
if annotation.thread? and annotation.thread?.children.length
formatted.reply_count = annotation.thread.flattenChildren().length
else
formatted.reply_count = 0
formatted
parser: (annotation) =>
parsed = {}
for k, v of annotation when k in whitelist
parsed[k] = v
parsed
onConnect: (source, origin, scope) =>
options =
window: source
origin: origin
scope: "#{scope}:provider"
onReady: =>
console.log "Provider functions are ready for #{origin}"
if source is @element.injector().get('$window').parent
@host = channel
entities = []
channel = this._setupXDM options
channel.call
method: 'getDocumentInfo'
success: (info) =>
entityUris = {}
entityUris[info.uri] = true
for link in info.metadata.link
entityUris[link.href] = true if link.href
for href of entityUris
entities.push href
this.plugins.Store?.loadAnnotations()
channel.notify
method: 'setTool'
params: this.tool
channel.notify
method: 'setVisibleHighlights'
params: this.visibleHighlights
@providers.push
channel: channel
entities: entities
# Add some info to new annotations
this.subscribe 'beforeAnnotationCreated', (annotation) =>
......@@ -114,74 +187,14 @@ class Hypothesis extends Annotator
this.subscribe 'annotationDeleted', (a) =>
$rootScope.annotations = $rootScope.annotations.filter (b) -> b isnt a
# Update the heatmap when the host is updated or annotations are loaded
bridge = @plugins.Bridge
heatmap = @plugins.Heatmap
threading = @threading
updateOn = [
'hostUpdated'
'annotationsLoaded'
'annotationCreated'
'annotationDeleted'
]
for event in updateOn
this.subscribe event, =>
@provider.call
method: 'getHighlights'
success: ({highlights, offset}) ->
heatmap.updateHeatmap
highlights:
for hl in highlights when hl.data
annotation = bridge.cache[hl.data]
angular.extend hl, data: annotation
offset: offset
# Reload the route after annotations are loaded
this.subscribe 'annotationsLoaded', -> $route.reload()
@auth = authentication
@socialView =
name: "none" # "single-player"
_setupXDM: ->
$location = @element.injector().get '$location'
_setupXDM: (options) ->
$rootScope = @element.injector().get '$rootScope'
$window = @element.injector().get '$window'
drafts = @element.injector().get 'drafts'
# Set up the bridge plugin, which bridges the main annotation methods
# between the host page and the panel widget.
whitelist = ['diffHTML', 'diffCaseOnly', 'quote', 'ranges', 'target', 'references']
this.addPlugin 'Bridge',
origin: $location.search().xdm
window: $window.parent
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
@api = Channel.build
origin: $location.search().xdm
scope: 'annotator:api'
window: $window.parent
.bind('addToken', (ctx, token) =>
@element.scope().token = token
@element.scope().$digest()
)
@provider = Channel.build
origin: $location.search().xdm
scope: 'annotator:panel'
window: $window.parent
onReady: => console.log "Sidepanel: channel is ready"
provider = Channel.build options
# Dodge toolbars [DISABLE]
#@provider.getMaxBottom (max) =>
# @element.css('margin-top', "#{max}px")
......@@ -189,31 +202,35 @@ class Hypothesis extends Annotator
# @element.find('#gutter').css("margin-top", "#{max}px")
# @plugins.Heatmap.BUCKET_THRESHOLD_PAD += max
@provider
provider
.bind('publish', (ctx, args...) => this.publish args...)
.bind('back', =>
# This guy does stuff when you "back out" of the interface.
# (Currently triggered by a click on the source page.)
return unless drafts.discard()
$rootScope.$apply => this.hide()
)
.bind('setDynamicBucketMode', (ctx, value) => $rootScope.$apply =>
this.setDynamicBucketMode value
$rootScope.$apply =>
return unless drafts.discard()
this.hide()
)
.bind('open', =>
# Pop out the sidebar
$rootScope.$apply => this.show())
$rootScope.$apply => this.show()
)
.bind('showViewer', (ctx, ids=[]) =>
this.showViewer ((@threading.getContainer id).message for id in ids)
)
.bind('showViewer', (ctx, tags=[]) =>
this.showViewer (@plugins.Bridge.cache[tag] for tag in tags)
.bind('updateViewer', (ctx, ids=[]) =>
this.updateViewer ((@threading.getContainer id).message for id in ids)
)
.bind('updateViewer', (ctx, tags=[]) =>
this.updateViewer (@plugins.Bridge.cache[tag] for tag in tags)
.bind('setTool', (ctx, name) => this.setTool name)
.bind('setVisibleHighlights', (ctx, state) =>
this.setVisibleHighlights state
)
_setupWrapper: ->
......@@ -245,27 +262,11 @@ class Hypothesis extends Annotator
this
_setupDocumentEvents: ->
el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
el.width = el.height = 1
@element.append el
handle = @element.find('.topbar .tri')[0]
handle.addEventListener 'dragstart', (event) =>
event.dataTransfer.setData 'text/plain', ''
event.dataTransfer.setDragImage el, 0, 0
@dragging = true
@provider.notify method: 'setDrag', params: true
@provider.notify method: 'dragFrame', params: event.screenX
handle.addEventListener 'dragend', (event) =>
@dragging = false
@provider.notify method: 'setDrag', params: false
@provider.notify method: 'dragFrame', params: event.screenX
@element[0].addEventListener 'dragover', (event) =>
if @dragging then @provider.notify method: 'dragFrame', params: event.screenX
@element[0].addEventListener 'dragleave', (event) =>
if @dragging then @provider.notify method: 'dragFrame', params: event.screenX
this
document.addEventListener 'dragover', (event) =>
for p in @providers
p.channel.notify
method: 'dragFrame'
params: event.screenX
# Override things not used in the angular version.
_setupDynamicStyle: -> this
......@@ -276,7 +277,9 @@ class Hypothesis extends Annotator
getHtmlQuote: (quote) -> quote
# Do nothing in the app frame, let the host handle it.
setupAnnotation: (annotation) -> annotation
setupAnnotation: (annotation) ->
annotation.highlights = []
annotation
sortAnnotations: (a, b) ->
a_upd = if a.updated? then new Date(a.updated) else new Date()
......@@ -307,8 +310,9 @@ class Hypothesis extends Annotator
this.updateViewer annotations
clickAdder: =>
@provider.notify
method: 'adderClick'
for p in @providers
p.channel.notify
method: 'adderClick'
showEditor: (annotation) =>
this.show()
......@@ -318,7 +322,8 @@ class Hypothesis extends Annotator
unless this.plugins.Auth? and this.plugins.Auth.haveValidToken()
$route.current.locals.$scope.$apply ->
$route.current.locals.$scope.$emit 'showAuth', true
@provider.notify method: 'onEditorHide'
for p in @providers
p.channel.notify method: 'onEditorHide'
@ongoing_edit = true
return
......@@ -345,18 +350,35 @@ class Hypothesis extends Annotator
hide: =>
@element.scope().frame.visible = false
setDynamicBucketMode: (value) =>
@element.scope().dynamicBucket = value
patch_store: (store) =>
patch_store: ->
$location = @element.injector().get '$location'
$rootScope = @element.injector().get '$rootScope'
Store = Annotator.Plugin.Store
# When the Store plugin is first instantiated, don't load annotations.
# They will be loaded manually as entities are registered by participating
# frames.
Store.prototype.loadAnnotations = ->
query = {}
@annotator.considerSocialView.call @annotator, query
this.entities ?= {}
for p in @annotator.providers
for uri in p.entities
unless this.entities[uri]?
console.log "Loading annotations for: " + uri
this.entities[uri] = true
this.loadAnnotationsFromSearch (angular.extend query, uri: uri)
# When the store plugin finishes a request, update the annotation
# using a monkey-patched update function which updates the threading
# if the annotation has a newly-assigned id and ensures that the id
# is enumerable.
store.updateAnnotation = (annotation, data) =>
Store.prototype.updateAnnotation = (annotation, data) =>
unless Object.keys(data).length
return
if annotation.id? and annotation.id != data.id
# Update the id table for the threading
thread = @threading.getContainer annotation.id
......@@ -380,62 +402,58 @@ class Hypothesis extends Annotator
# Update the annotation with the new data
annotation = angular.extend annotation, data
@plugins.Bridge?.updateAnnotation annotation
# Give angular a chance to react
$rootScope.$digest()
considerSocialView: (options) ->
considerSocialView: (query) ->
switch @socialView.name
when "none"
# Sweet, nothing to do, just clean up previous filters
console.log "Not applying any Social View filters."
delete options.loadFromSearch.user
delete query.user
when "single-player"
if (p = @auth.persona)?
console.log "Social View filter: single player mode."
options.loadFromSearch.user = "acct:" + p.username + "@" + p.provider
query.user = "acct:" + p.username + "@" + p.provider
else
console.log "Social View: single-player mode, but ignoring it, since not logged in."
delete options.loadFromSearch.user
delete query.user
else
console.warn "Unsupported Social View: '" + @socialView.name + "'!"
serviceDiscovery: (options) =>
$location = @element.injector().get '$location'
$rootScope = @element.injector().get '$rootScope'
angular.extend @options, Store: options
# Get the location of the annotated document
@provider.call
method: 'getDocumentInfo'
success: (info) =>
href = info.uri
@plugins.Document.metadata = info.metadata
options = angular.extend {}, (@options.Store or {}),
annotationData:
uri: href
loadFromSearch:
limit: 1000
uri: href
this.considerSocialView options
this.addStore(options)
addStore: (options) ->
this.addPlugin 'Store', options
this.patch_store this.plugins.Store
href = options.loadFromSearch?.uri
return unless href?
console.log "Loaded annotions for '" + href + "'."
for uri in @plugins.Document.uris()
# Do not load annotations from the href twice
unless uri is href
console.log "Also loading annotations for: " + uri
this.plugins.Store.loadAnnotationsFromSearch uri: uri
# Bubbles updates through the thread so that guests see accurate
# reply counts.
updateAncestors: (annotation) =>
for ref in (annotation.references?.slice().reverse() or [])
rel = (@threading.getContainer ref).message
if rel?
$timeout = @element.injector().get('$timeout')
$timeout (=> @plugins.Bridge.updateAnnotation rel), 10
this.updateAncestors(rel)
break # Only the nearest existing ancestor, the rest is by induction.
serviceDiscovery: (options) =>
@options.Store ?= {}
angular.extend @options.Store, options
this.addPlugin 'Store', @options.Store
setTool: (name) =>
return if name is @tool
@tool = name
for p in @providers
p.channel.notify
method: 'setTool'
params: name
setVisibleHighlights: (state) =>
return if state is @visibleHighlights
@visibleHighlights = state
for p in @providers
p.channel.notify
method: 'setVisibleHighlights'
params: state
class AuthenticationProvider
constructor: ->
......
......@@ -732,8 +732,11 @@
return Math.max.apply(Math, all);
},
mousePosition: function(e, offsetEl) {
var offset;
offset = $(offsetEl).position();
var offset, _ref1;
if ((_ref1 = $(offsetEl).css('position')) !== 'absolute' && _ref1 !== 'fixed' && _ref1 !== 'relative') {
offsetEl = $(offsetEl).offsetParent()[0];
}
offset = $(offsetEl).offset();
return {
top: e.pageY - offset.top,
left: e.pageX - offset.left
......
/*!
* jQuery scrollintoview() plugin and :scrollable selector filter
*
* Version 1.8 (14 Jul 2011)
* Requires jQuery 1.4 or newer
*
* Copyright (c) 2011 Robert Koritnik
* Licensed under the terms of the MIT license
* http://www.opensource.org/licenses/mit-license.php
*/
(function ($) {
var converter = {
vertical: { x: false, y: true },
horizontal: { x: true, y: false },
both: { x: true, y: true },
x: { x: true, y: false },
y: { x: false, y: true }
};
var settings = {
duration: "fast",
direction: "both"
};
var rootrx = /^(?:html)$/i;
// gets border dimensions
var borders = function (domElement, styles) {
styles = styles || (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(domElement, null) : domElement.currentStyle);
var px = document.defaultView && document.defaultView.getComputedStyle ? true : false;
var b = {
top: (parseFloat(px ? styles.borderTopWidth : $.css(domElement, "borderTopWidth")) || 0),
left: (parseFloat(px ? styles.borderLeftWidth : $.css(domElement, "borderLeftWidth")) || 0),
bottom: (parseFloat(px ? styles.borderBottomWidth : $.css(domElement, "borderBottomWidth")) || 0),
right: (parseFloat(px ? styles.borderRightWidth : $.css(domElement, "borderRightWidth")) || 0)
};
return {
top: b.top,
left: b.left,
bottom: b.bottom,
right: b.right,
vertical: b.top + b.bottom,
horizontal: b.left + b.right
};
};
var dimensions = function ($element) {
var win = $(window);
var isRoot = rootrx.test($element[0].nodeName);
return {
border: isRoot ? { top: 0, left: 0, bottom: 0, right: 0} : borders($element[0]),
scroll: {
top: (isRoot ? win : $element).scrollTop(),
left: (isRoot ? win : $element).scrollLeft()
},
scrollbar: {
right: isRoot ? 0 : $element.innerWidth() - $element[0].clientWidth,
bottom: isRoot ? 0 : $element.innerHeight() - $element[0].clientHeight
},
rect: (function () {
var r = $element[0].getBoundingClientRect();
return {
top: isRoot ? 0 : r.top,
left: isRoot ? 0 : r.left,
bottom: isRoot ? $element[0].clientHeight : r.bottom,
right: isRoot ? $element[0].clientWidth : r.right
};
})()
};
};
$.fn.extend({
scrollintoview: function (options) {
/// <summary>Scrolls the first element in the set into view by scrolling its closest scrollable parent.</summary>
/// <param name="options" type="Object">Additional options that can configure scrolling:
/// duration (default: "fast") - jQuery animation speed (can be a duration string or number of milliseconds)
/// direction (default: "both") - select possible scrollings ("vertical" or "y", "horizontal" or "x", "both")
/// complete (default: none) - a function to call when scrolling completes (called in context of the DOM element being scrolled)
/// </param>
/// <return type="jQuery">Returns the same jQuery set that this function was run on.</return>
options = $.extend({}, settings, options);
options.direction = converter[typeof (options.direction) === "string" && options.direction.toLowerCase()] || converter.both;
var dirStr = "";
if (options.direction.x === true) dirStr = "horizontal";
if (options.direction.y === true) dirStr = dirStr ? "both" : "vertical";
var el = this.eq(0);
var scroller = el.closest(":scrollable(" + dirStr + ")");
// check if there's anything to scroll in the first place
if (scroller.length > 0)
{
scroller = scroller.eq(0);
var dim = {
e: dimensions(el),
s: dimensions(scroller)
};
var rel = {
top: dim.e.rect.top - (dim.s.rect.top + dim.s.border.top),
bottom: dim.s.rect.bottom - dim.s.border.bottom - dim.s.scrollbar.bottom - dim.e.rect.bottom,
left: dim.e.rect.left - (dim.s.rect.left + dim.s.border.left),
right: dim.s.rect.right - dim.s.border.right - dim.s.scrollbar.right - dim.e.rect.right
};
var animOptions = {};
// vertical scroll
if (options.direction.y === true)
{
if (rel.top < 0)
{
animOptions.scrollTop = dim.s.scroll.top + rel.top;
}
else if (rel.top > 0 && rel.bottom < 0)
{
animOptions.scrollTop = dim.s.scroll.top + Math.min(rel.top, -rel.bottom);
}
}
// horizontal scroll
if (options.direction.x === true)
{
if (rel.left < 0)
{
animOptions.scrollLeft = dim.s.scroll.left + rel.left;
}
else if (rel.left > 0 && rel.right < 0)
{
animOptions.scrollLeft = dim.s.scroll.left + Math.min(rel.left, -rel.right);
}
}
// scroll if needed
if (!$.isEmptyObject(animOptions))
{
if (rootrx.test(scroller[0].nodeName))
{
scroller = $("html,body");
}
scroller
.animate(animOptions, options.duration)
.eq(0) // we want function to be called just once (ref. "html,body")
.queue(function (next) {
$.isFunction(options.complete) && options.complete.call(scroller[0]);
next();
});
}
else
{
// when there's nothing to scroll, just call the "complete" function
$.isFunction(options.complete) && options.complete.call(scroller[0]);
}
}
// return set back
return this;
}
});
var scrollValue = {
auto: true,
scroll: true,
visible: false,
hidden: false
};
$.extend($.expr[":"], {
scrollable: function (element, index, meta, stack) {
var direction = converter[typeof (meta[3]) === "string" && meta[3].toLowerCase()] || converter.both;
var styles = (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(element, null) : element.currentStyle);
var overflow = {
x: scrollValue[styles.overflowX.toLowerCase()] || false,
y: scrollValue[styles.overflowY.toLowerCase()] || false,
isRoot: rootrx.test(element.nodeName)
};
// check if completely unscrollable (exclude HTML element because it's special)
if (!overflow.x && !overflow.y && !overflow.isRoot)
{
return false;
}
var size = {
height: {
scroll: element.scrollHeight,
client: element.clientHeight
},
width: {
scroll: element.scrollWidth,
client: element.clientWidth
},
// check overflow.x/y because iPad (and possibly other tablets) don't dislay scrollbars
scrollableX: function () {
return (overflow.x || overflow.isRoot) && this.width.scroll > this.width.client;
},
scrollableY: function () {
return (overflow.y || overflow.isRoot) && this.height.scroll > this.height.client;
}
};
return direction.y && size.scrollableY() || direction.x && size.scrollableX();
}
});
})(jQuery);
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