Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 9 commits
  • 14 files changed
  • 0 comments
  • 1 contributor
Dec 06, 2012
Randall Leeds make form.pt match the one in deform
The way it was (maybe taken from deform_bootstrap) was causing
duplicate directives.
1ec52ca
Randall Leeds replace bootstrap with angular-bootstrap 133467b
Randall Leeds sensible prototype property syntax
I'm still hurting from the embarrassment of having written this.
e04c58d
Randall Leeds fix bad superconstructor call in heatmap 9718145
Randall Leeds add annotation and viewer ng templates f744272
Randall Leeds add annotation directive cf43ff0
Randall Leeds rework toolbars and annotator-outer 21f001e
Randall Leeds new CSS for the new ng templates cd5a64f
Randall Leeds start to break apart the Hypothesis controller
- Break Hypothesis down into App and Viewer controllers (Auth
was factored out earlier)
- Break the XDM pieces and the bulk of what used to be the
main subclass of Annotator into a service, since the annotator
services, inside the iframe, is only every a singleton anyway.
This distinguishes the service from the highlight providers a bit.
Eventually, multiple "annotators" on the host page could
communicate with the same service (in the iframe) or, in the case
of a browser extension, the service can be in the background page
app context.
- Use the $location provider for routes and hook it up to the viewer
so that detail is triggered by a route parameter. This will let our
back functionality be implemented by browser history and could
prove useful as well as sensible.
369f5b4
1  h/assets.py
@@ -75,7 +75,6 @@ def Handlebars(*names, **kw):
75 75 templates,
76 76 underscore,
77 77 'h:lib/jquery.mousewheel.min.js',
78   - 'deform_bootstrap:static/bootstrap.min.js',
79 78 Uglify(
80 79 *[
81 80 Coffee('h:/js/%s.coffee' % name,
57 h/css/app.scss
@@ -23,19 +23,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
23 23
24 24
25 25
26   -//HIDDEN TABS////////////////////////////////
27   -.nav a[data-target="#activate-tab"],
28   -.nav a[data-target="#forgot-tab"] {
29   - display: none;
30   -}
31   -
32   -.nav .active a[data-target="#activate-tab"],
33   -.nav .active a[data-target="#forgot-tab"] {
34   - display: initial;
35   -}
36   -
37   -
38   -
39 26 //ANNOTATOR STYLES////////////////////////////////
40 27 .annotator-hide {
41 28 display: none;
@@ -52,7 +39,25 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
52 39 width: $heatmap-width;
53 40 }
54 41
  42 +.sliding-panels > li {
  43 + @include single-transition(left, 0.4s, ease-in);
  44 + padding-left: 1.5em;
  45 + position: absolute;
  46 + height: 100%;
  47 + width: 100%;
55 48
  49 + & > * {
  50 + @include smallshadow(-2px);
  51 + }
  52 +
  53 + &:first-of-type {
  54 + padding-left: 0;
  55 + }
  56 +
  57 + &.collapsed {
  58 + left: 100%;
  59 + }
  60 +}
56 61
57 62 //SIDEBAR LAYOUT////////////////////////////////
58 63 #gutter {
@@ -60,14 +65,15 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
60 65 background-attachment: fixed;
61 66 height: 100%;
62 67 margin-left: $heatmap-width + 17px;
  68 + padding-top: 2.5em; // toolbar height + top margin
63 69 position: relative;
64 70 }
65 71
66 72 #wrapper {
  73 + background: url('../images/noise_1.png');
  74 + background-attachment: fixed;
67 75 height: 100%;
68   - overflow: auto;
69   - padding: 3.75em 1em 1em 1em;
70   - -webkit-overflow-scrolling: touch;
  76 + position: relative;
71 77 }
72 78
73 79
@@ -170,14 +176,14 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
170 176 z-index: 4;
171 177 background: url('../images/noise_1.png');
172 178 background-attachment: fixed;
173   - margin-top: 2.5em;
174   - padding-top: 1em;
  179 + padding: 1em;
175 180 position: absolute;
176 181 width: 100%;
177 182
178 183 .close {
179   - margin-right: .5em;
180   - margin-top: .25em;
  184 + position: absolute;
  185 + right: .5em;
  186 + top: .5em;
181 187 }
182 188
183 189 &.collapsed {
@@ -201,6 +207,11 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
201 207 background-color: #f2dede;
202 208 border-color: #F5A1A0;
203 209 }
  210 +
  211 + footer > ul {
  212 + margin-top: 1em;
  213 + text-align: right;
  214 + }
204 215 }
205 216
206 217
@@ -212,15 +223,15 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
212 223 border: 1px solid $grayLighter;
213 224 border-top-left-radius: 4px;
214 225 border-bottom-left-radius: 4px;
215   - left: 1px;
216   - line-height: $baseLineHeight;
  226 + height: 2em;
  227 + left: 7px;
  228 + line-height: 1.4em; // 2em (height) - .6em (padding)
217 229 margin-top: .5em;
218 230 padding: .3em;
219 231 position: absolute;
220 232 text-align: right;
221 233 width: 100%;
222 234 z-index: 5;
223   - left: 7px;
224 235
225 236 & > div {
226 237 display: inline-block;
77 h/css/common.scss
@@ -207,6 +207,18 @@ a {
207 207 }
208 208
209 209
  210 +//OUTER//////////////////////////////////
  211 +.annotator-outer {
  212 + background: url('../images/noise_1.png');
  213 + background-attachment: fixed;
  214 + width: 100%;
  215 + height: 100%;
  216 + overflow: auto;
  217 + padding: 1em;
  218 + -webkit-overflow-scrolling: touch;
  219 +}
  220 +
  221 +
210 222
211 223 //EXCERPT////////////////////////////////
212 224 .excerpt {
@@ -349,10 +361,6 @@ blockquote {
349 361 &.active {
350 362 display: inherit !important;
351 363 }
352   -
353   - footer > ul {
354   - text-align: right;
355   - }
356 364 }
357 365
358 366
@@ -372,6 +380,7 @@ blockquote {
372 380 //ANNOTATION////////////////////////////////
373 381 //This is for everything that is formatted as an annotation.
374 382 .annotation {
  383 + position: relative;
375 384
376 385 .user {
377 386 font-weight: bold;
@@ -396,15 +405,35 @@ blockquote {
396 405 }
397 406
398 407
  408 +//THREADING////////////////////////////////
  409 +//Threaded discussion specific
  410 +.thread .annotator-listing {
  411 + border-left: 1px dotted $grayLight;
  412 +
  413 + .threadexp {
  414 + height: $threadexp-width;
  415 + width: $threadexp-width;
  416 + position: absolute;
  417 + top: .8em;
  418 + left: -($threadexp-width / 2);
  419 + outline: 1px dotted #aaa;
  420 + @include icon("minus_1.png");
  421 + }
  422 +
  423 + .annotation {
  424 + padding-left: $thread-padding;
  425 + padding-top: .35em;
  426 + &.squished {
  427 + padding-left: 0;
  428 + }
  429 + }
  430 +}
  431 +
  432 +
399 433
400 434 //DETAIL////////////////////////////////
401 435 //This is specific to the detail view.
402 436 .detail {
403   - position: relative;
404   - .paper > .threadexp {
405   - display: none;
406   - }
407   -
408 437 .annotator-controls {
409 438 @include single-transition(opacity, .25s, ease-in-out);
410 439 opacity: 0;
@@ -421,29 +450,6 @@ blockquote {
421 450 margin-bottom: .25em;
422 451 }
423 452
424   - //Threaded discussion specific
425   - .annotator-listing {
426   - border-left: 1px dotted $grayLight;
427   -
428   - .threadexp {
429   - height: $threadexp-width;
430   - width: $threadexp-width;
431   - position: absolute;
432   - top: .8em;
433   - left: -($threadexp-width / 2);
434   - outline: 1px dotted #aaa;
435   - @include icon("minus_1.png");
436   - }
437   -
438   - & > .detail, .writer {
439   - padding-left: $thread-padding;
440   - padding-top: .35em;
441   - &.squished {
442   - padding-left: 0;
443   - }
444   - }
445   - }
446   -
447 453 &.hover {
448 454 & > .annotator-controls {
449 455 opacity: 1;
@@ -451,7 +457,7 @@ blockquote {
451 457 }
452 458
453 459 //These are all the changes needed to collapse thread objects.
454   - &.collapsed {
  460 + .collapsed {
455 461 & > .body {
456 462 overflow: hidden;
457 463 text-overflow: ellipsis;
@@ -490,6 +496,11 @@ blockquote {
490 496 @include smallshadow(2px, 1px, .1);
491 497 bottom: 0px;
492 498 }
  499 +
  500 + // Things not shown in the summary view
  501 + .annotator-controls, .bottombar, .excerpt, .thread {
  502 + display: none;
  503 + }
493 504 }
494 505
495 506
24 h/js/app.coffee
... ... @@ -1,7 +1,19 @@
1   -angular.module 'h', [
2   - 'deform'
3   - 'h.controllers'
4   - 'h.directives'
5   - 'h.filters'
6   - 'h.services'
  1 +imports = [
  2 + 'bootstrap'
  3 + 'deform'
  4 + 'h.controllers'
  5 + 'h.directives'
  6 + 'h.filters'
  7 + 'h.services'
7 8 ]
  9 +
  10 +
  11 +configure = ($routeProvider, $locationProvider) ->
  12 + $routeProvider.when '/app/viewer',
  13 + controller: 'Viewer'
  14 + reloadOnSearch: false
  15 + templateUrl: 'viewer.html'
  16 +configure.$inject = ['$routeProvider', '$locationProvider']
  17 +
  18 +
  19 +angular.module('h', imports, configure)
762 h/js/controllers.coffee
... ... @@ -1,715 +1,129 @@
1   -class Hypothesis extends Annotator
2   - # Plugin configuration
3   - options:
4   - Heatmap: {}
5   - Permissions:
6   - showEditPermissionsCheckbox: false,
7   - showViewPermissionsCheckbox: false,
8   - userString: (user) -> user.replace(/^acct:(.+)@(.+)$/, '$1 on $2')
  1 +class App
  2 + this.$inject = ['$compile', '$http', '$location', '$scope', 'annotator']
  3 + constructor: ($compile, $http, $location, $scope, annotator) ->
  4 + {plugins} = annotator
9 5
10   - # Internal state
11   - bucket: -1 # * The index of the bucket shown in the summary view
12   - detail: false # * Whether the viewer shows a summary or detail listing
13   - hash: -1 # * cheap UUID :cake:
14   - cache: {} # * object cache
15   - visible: false # * Whether the sidebar is visible
16   - unsaved_drafts: [] # * Unsaved drafts currenty open
  6 + $location.path '/app/viewer'
17 7
18   - this.$inject = ['$rootElement', '$scope', '$compile', '$http']
19   - constructor: (@element, @scope, @compile, @http) ->
20   - super @element, @options
  8 + $scope.$on 'reset', =>
  9 + angular.extend $scope,
  10 + personas: []
  11 + persona: null
  12 + token: null
21 13
22   - # Load plugins
23   - for own name, opts of @options
24   - if not @plugins[name] and name of Annotator.Plugin
25   - this.addPlugin(name, opts)
26   -
27   - # Export public, bound functions on the scope
28   - for own key of this
29   - if typeof this[key] == 'function' and not key.match('^_')
30   - @scope[key] = this[key]
31   -
32   - # Establish cross-domain communication to the widget host
33   - @provider = new easyXDM.Rpc
34   - swf: @options.swf
35   - onReady: this._initialize
36   - ,
37   - local:
38   - publish: (event, args, k, fk) =>
39   - if event in ['annotationCreated']
40   - [h] = args
41   - annotation = @cache[h]
42   - this.publish event, [annotation]
43   - addPlugin: => this.addPlugin arguments...
44   - createAnnotation: =>
45   - if @plugins.Permissions.user?
46   - @cache[h = ++@hash] = this.createAnnotation()
47   - h
48   - else
49   - this.showAuth true
50   - this.show()
51   - null
52   - showEditor: (stub) =>
53   - return unless this._canCloseUnsaved()
54   - h = stub.hash
55   - annotation = $.extend @cache[h], stub,
56   - hash:
57   - toJSON: => undefined
58   - valueOf: => h
59   - this.showEditor annotation
60   - # This guy does stuff when you "back out" of the interface.
61   - # (Currently triggered by a click on the source page.)
62   - back: =>
63   - # If it's in the detail view, loads the bucket back up.
64   - if @detail
65   - this.showViewer(@heatmap.buckets[@bucket])
66   - this.publish('hostUpdated')
67   - # If it's not in the detail view, the assumption is that it's in the
68   - # bucket view and hides the whole interface.
69   - else
70   - this.hide()
71   - update: => this.publish 'hostUpdated'
72   - remote:
73   - publish: {}
74   - setupAnnotation: {}
75   - onEditorHide: {}
76   - onEditorSubmit: {}
77   - showFrame: {}
78   - hideFrame: {}
79   - dragFrame: {}
80   - getHighlights: {}
81   - setActiveHighlights: {}
82   - getMaxBottom: {}
83   - scrollTop: {}
84   -
85   - # Prepare a MarkDown renderer, and add some post-processing
86   - # so that all created links have their target set to _blank
87   - @renderer = Markdown.getSanitizingConverter()
88   - @renderer.hooks.chain "postConversion", (text) ->
89   - text.replace /<a href=/, "<a target=\"_blank\" href="
90   -
91   - @scope.$watch 'personas', (newValue, oldValue) =>
  14 + $scope.$watch 'personas', (newValue, oldValue) =>
92 15 if newValue?.length
93   - @element.find('#persona')
  16 + annotator.element.find('#persona')
94 17 .off('change').on('change', -> $(this).submit())
95 18 .off('click')
96   - this.showAuth(false)
  19 + $scope.showAuth = false
97 20 else
98   - @scope.persona = null
99   - @scope.token = null
100   - @element.find('#persona').off('click').on('click', => this.showAuth())
  21 + $scope.persona = null
  22 + $scope.token = null
101 23
102   - @scope.$watch 'persona', (newValue, oldValue) =>
  24 + $scope.$watch 'persona', (newValue, oldValue) =>
103 25 if oldValue? and not newValue?
104   - @http.post 'logout', '',
  26 + $http.post 'logout', '',
105 27 withCredentials: true
106   - .success (data) => @scope.reset()
  28 + .success (data) => $scope.reset()
107 29
108   - @scope.$watch 'token', (newValue, oldValue) =>
109   - if @plugins.Auth?
110   - @plugins.Auth.token = newValue
111   - @plugins.Auth.updateHeaders()
  30 + $scope.$watch 'token', (newValue, oldValue) =>
  31 + if plugins.Auth?
  32 + plugins.Auth.token = newValue
  33 + plugins.Auth.updateHeaders()
112 34
113 35 if newValue?
114   - if not @plugins.Auth?
115   - this.addPlugin 'Auth',
116   - tokenUrl: @scope.tokenUrl
  36 + if not plugins.Auth?
  37 + annotator.addPlugin 'Auth',
  38 + tokenUrl: $scope.tokenUrl
117 39 token: newValue
118 40 else
119   - @plugins.Auth.setToken(newValue)
120   - @plugins.Auth.withToken @plugins.Permissions._setAuthFromToken
  41 + plugins.Auth.setToken(newValue)
  42 + plugins.Auth.withToken plugins.Permissions._setAuthFromToken
121 43 else
122   - @plugins.Permissions.setUser(null)
123   - delete @plugins.Auth
  44 + plugins.Permissions.setUser(null)
  45 + delete plugins.Auth
124 46
125 47 # Fetch the initial model from the server
126   - @http.get 'model'
  48 + $http.get 'model',
127 49 withCredentials: true
128 50 .success (data) =>
129   - angular.extend @scope, data
130   -
131   - this.reset()
132   - this.showAuth false
133   -
134   - this
135   -
136   - _initialize: =>
137   - # Set up interface elements
138   - this._setupHeatmap()
139   - @wrapper.append(@viewer.element, @editor.element)
140   - @heatmap.element.appendTo(document.body)
141   - @viewer.show()
142   -
143   - @provider.getMaxBottom (max) =>
144   - @element.find('#toolbar').css("top", "#{max}px")
145   - @element.find('#gutter').css("padding-top", "#{max}px")
146   - @heatmap.BUCKET_THRESHOLD_PAD = (
147   - max + @heatmap.constructor.prototype.BUCKET_THRESHOLD_PAD
148   - )
149   -
150   - this.subscribe 'beforeAnnotationCreated', (annotation) =>
151   - annotation.created = annotation.updated = (new Date()).toString()
152   - annotation.user = @plugins.Permissions.options.userId(
153   - @plugins.Permissions.user)
154   -
155   - this.publish 'hostUpdated'
156   -
157   - _setupWrapper: ->
158   - @wrapper = @element.find('#wrapper')
159   - .on 'mousewheel', (event, delta) ->
160   - # prevent overscroll from scrolling host frame
161   - # http://stackoverflow.com/questions/5802467
162   - scrollTop = $(this).scrollTop()
163   - scrollBottom = $(this).get(0).scrollHeight - $(this).innerHeight()
164   - if delta > 0 and scrollTop == 0
165   - event.preventDefault()
166   - else if delta < 0 and scrollTop == scrollBottom
167   - event.preventDefault()
168   - this
169   -
170   - _setupDocumentEvents: ->
171   - @element.find('#toolbar .tri').click =>
172   - if @visible
173   - this.hide()
174   - else
175   - if @viewer.isShown() and @bucket == -1
176   - this._fillDynamicBucket()
177   - this.show()
178   -
179   - el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
180   - el.width = el.height = 1
181   - @element.append el
182   -
183   - handle = @element.find('#toolbar .tri')[0]
184   - handle.addEventListener 'dragstart', (event) =>
185   - event.dataTransfer.setData 'text/plain', ''
186   - event.dataTransfer.setDragImage(el, 0, 0)
187   - @provider.dragFrame event.screenX
188   - handle.addEventListener 'dragend', (event) =>
189   - @provider.dragFrame event.screenX
190   - @element[0].addEventListener 'dragover', (event) =>
191   - @provider.dragFrame event.screenX
192   - @element[0].addEventListener 'dragleave', (event) =>
193   - @provider.dragFrame event.screenX
194   -
195   - this
196   -
197   - _setupDynamicStyle: ->
198   - this
199   -
200   - _setupHeatmap: () ->
201   - @heatmap = @plugins.Heatmap
202   -
203   - # Update the heatmap when certain events are pubished
204   - events = [
205   - 'annotationCreated'
206   - 'annotationDeleted'
207   - 'annotationsLoaded'
208   - 'hostUpdated'
209   - ]
210   -
211   - for event in events
212   - this.subscribe event, =>
213   - @provider.getHighlights ({highlights, offset}) =>
214   - @heatmap.updateHeatmap
215   - highlights: highlights.map (hl) =>
216   - hl.data = @cache[hl.data]
217   - hl
218   - offset: offset
219   - if @visible and @viewer.isShown() and @bucket == -1 and not @detail
220   - this._fillDynamicBucket()
221   -
222   - @heatmap.element.click =>
223   - @bucket = -1
224   - this._fillDynamicBucket()
225   - this.show()
226   -
227   - @heatmap.subscribe 'updated', =>
228   - tabs = d3.select(document.body)
229   - .selectAll('div.heatmap-pointer')
230   - .data =>
231   - buckets = []
232   - @heatmap.index.forEach (b, i) =>
233   - if @heatmap.buckets[i].length > 0
234   - buckets.push i
235   - else if @heatmap.isUpper(i) or @heatmap.isLower(i)
236   - buckets.push i
237   - buckets
238   -
239   - {highlights, offset} = d3.select(@heatmap.element[0]).datum()
240   - height = $(window).outerHeight(true)
241   - pad = height * .2
242   -
243   - # Enters into tabs var, and generates bucket pointers from them
244   - tabs.enter().append('div')
245   - .classed('heatmap-pointer', true)
246   -
247   - tabs.exit().remove()
248   -
249   - tabs
250   -
251   - .style 'top', (d) =>
252   - "#{(@heatmap.index[d] + @heatmap.index[d+1]) / 2}px"
253   -
254   - .html (d) =>
255   - "<div class='label'>#{@heatmap.buckets[d].length}</div><div class='svg'></div>"
256   -
257   - .classed('upper', @heatmap.isUpper)
258   - .classed('lower', @heatmap.isLower)
259   -
260   - .style 'display', (d) =>
261   - if (@heatmap.buckets[d].length is 0) then 'none' else ''
262   -
263   - # Creates highlights corresponding bucket when mouse is hovered
264   - .on 'mousemove', (bucket) =>
265   - unless @viewer.isShown() and @detail
266   - unless @heatmap.buckets[bucket]?.length then bucket = @bucket
267   - @provider.setActiveHighlights @heatmap.buckets[bucket]?.map (a) =>
268   - a.hash.valueOf()
269   -
270   - # Gets rid of them after
271   - .on 'mouseout', =>
272   - unless @viewer.isShown() and @detail
273   - @provider.setActiveHighlights @heatmap.buckets[@bucket]?.map (a) =>
274   - a.hash.valueOf()
275   -
276   - # Does one of a few things when a tab is clicked depending on type
277   - .on 'mouseup', (bucket) =>
278   - d3.event.preventDefault()
279   -
280   - # If it's the upper tab, scroll to next bucket above
281   - if @heatmap.isUpper bucket
282   - threshold = offset + @heatmap.index[0]
283   - next = highlights.reduce (next, hl) ->
284   - if next < hl.offset.top < threshold then hl.offset.top else next
285   - , threshold - height
286   - @provider.scrollTop next - pad
287   - @bucket = -1
288   - this._fillDynamicBucket()
289   -
290   - # If it's the lower tab, scroll to next bucket below
291   - else if @heatmap.isLower bucket
292   - threshold = offset + @heatmap.index[0] + pad
293   - next = highlights.reduce (next, hl) ->
294   - if threshold < hl.offset.top < next then hl.offset.top else next
295   - , offset + height
296   - @provider.scrollTop next - pad
297   - @bucket = -1
298   - this._fillDynamicBucket()
299   -
300   - # If it's neither of the above, load the bucket into the viewer
301   - else
302   - annotations = @heatmap.buckets[bucket]
303   - @bucket = bucket
304   - this.showViewer(annotations)
305   - this.show()
306   -
307   - this
308   -
309   - # Creates an instance of Annotator.Viewer and assigns it to the @viewer
310   - # property, appends it to the @wrapper and sets up event listeners.
311   - #
312   - # Returns itself to allow chaining.
313   - _setupViewer: ->
314   - @viewer = new Annotator.Viewer(readOnly: @options.readOnly)
315   - @viewer.hide()
316   - .on("edit", this.onEditAnnotation)
317   - .on("delete", this.onDeleteAnnotation)
318   -
319   - # Show newly created annotations in the viewer immediately
320   - this.subscribe 'annotationCreated', (annotation) =>
321   - this.updateViewer [annotation]
322   -
323   - this
  51 + angular.extend $scope, data
324 52
325   - # Creates an instance of the Annotator.Editor and assigns it to @editor.
326   - # Appends this to the @wrapper and sets up event listeners.
327   - #
328   - # Returns itself for chaining.
329   - _setupEditor: ->
330   - @editor = this._createEditor()
331   - .on 'hide save', =>
332   - if @unsaved_drafts.indexOf(@editor) > -1
333   - @unsaved_drafts.splice(@unsaved_drafts.indexOf(@editor), 1)
334   - .on 'hide', =>
335   - @provider.onEditorHide()
336   - .on 'save', =>
337   - @provider.onEditorSubmit()
338   - this
  53 + # Set the initial state
  54 + # Asynchronous so that other controllers get time to initialize
  55 + $scope.$evalAsync "$broadcast('reset')"
339 56
340   - _createEditor: ->
341   - editor = new Annotator.Editor()
342   - editor.hide()
343   - editor.fields = [{
344   - element: editor.element,
345   - load: (field, annotation) ->
346   - $(field).find('textarea').val(annotation.text || '')
347   - submit: (field, annotation) ->
348   - annotation.text = $(field).find('textarea').val()
349   - }]
350 57
351   - @unsaved_drafts.push editor
352   - editor
  58 +class Auth
  59 + this.$inject = ['$compile', '$element', '$http', '$scope', 'deform']
  60 + constructor: ($compile, $element, $http, $scope, deform) ->
  61 + $scope.submit = ->
  62 + controls = $element.find('.sheet .active form').formSerialize()
  63 + $http.post '', controls,
  64 + headers:
  65 + 'Content-Type': 'application/x-www-form-urlencoded'
  66 + withCredentials: true
  67 + .success (data) =>
  68 + # Extend the scope with updated model data
  69 + angular.extend($scope, data.model) if data.model?
353 70
354   - _fillDynamicBucket: ->
355   - {highlights, offset} = d3.select(@heatmap.element[0]).datum()
356   - bottom = offset + @heatmap.element.height()
357   - this.showViewer highlights.reduce (acc, hl) =>
358   - if hl.offset.top >= offset and hl.offset.top <= bottom
359   - acc.push hl.data
360   - acc
361   - , []
  71 + # Replace any forms which were re-rendered in this response
  72 + for oid of data.form
  73 + target = '#' + oid
362 74
363   - # Public: Initialises an annotation either from an object representation or
364   - # an annotation created with Annotator#createAnnotation(). It finds the
365   - # selected range and higlights the selection in the DOM.
366   - #
367   - # annotation - An annotation Object to initialise.
368   - # fireEvents - Will fire the 'annotationCreated' event if true.
369   - #
370   - # Examples
371   - #
372   - # # Create a brand new annotation from the currently selected text.
373   - # annotation = annotator.createAnnotation()
374   - # annotation = annotator.setupAnnotation(annotation)
375   - # # annotation has now been assigned the currently selected range
376   - # # and a highlight appended to the DOM.
377   - #
378   - # # Add an existing annotation that has been stored elsewere to the DOM.
379   - # annotation = getStoredAnnotationWithSerializedRanges()
380   - # annotation = annotator.setupAnnotation(annotation)
381   - #
382   - # Returns the initialised annotation.
383   - setupAnnotation: (annotation) ->
384   - # Delagate to Annotator implementation after we give it a valid array of
385   - # ranges. This is needed until Annotator stops assuming ranges need to be
386   - # added.
387   - if annotation.thread
388   - annotation.ranges = []
  75 + $form = $(data.form[oid])
  76 + $form.replaceAll(target)
389 77
390   - if not annotation.hash
391   - @cache[h = ++@hash] = $.extend annotation,
392   - hash:
393   - toJSON: => undefined
394   - valueOf: => h
395   - stub =
396   - hash: annotation.hash.valueOf()
397   - ranges: annotation.ranges
398   - @provider.setupAnnotation stub
  78 + link = $compile $form
  79 + link $scope
399 80
400   - showViewer: (annotations=[], detail=false) =>
401   - if (@visible and not detail) or @unsaved_drafts.indexOf(@editor) > -1
402   - if not this._canCloseUnsaved() then return
  81 + deform.focusFirstInput target
403 82
404   - # Thread the messages using JWZ
405   - messages = mail.messageThread().thread annotations.map (a) ->
406   - m = mail.message(null, a.id, a.thread?.split('/') or [])
407   - m.annotation = a
408   - m
  83 + $scope.$on 'reset', =>
  84 + angular.extend $scope,
  85 + auth: null
  86 + username: null
  87 + password: null
  88 + email: null
  89 + code: null
409 90
410   - thread = (context, selector) ->
411   - context.select('.annotator-listing')
412   - .selectAll(-> d3.selectAll(this.children).filter(selector)[0])
413   - .data ((m) -> m.children), ((d) -> d.message.id)
  91 + $scope.$on 'showAuth', => $scope.auth = 'login'
414 92
415   - context = d3.select(@viewer.element[0]).datum(messages)
416   - items = thread context, '.annotation'
417   - excerpts = thread context, '.excerpt'
418 93
419   - if not detail
420   - # Save the state so the bucket view can be restored when exiting
421   - # the detail view.
422   - @detail = false
  94 +class Viewer
  95 + this.$inject = ['$location', '$routeParams', '$scope', 'annotator']
  96 + constructor: ($location, $routeParams, $scope, annotator) ->
  97 + thread = null
423 98
424   - excerpts.remove()
425   - excerpts.exit().remove()
  99 + annotator.subscribe 'annotationsLoaded', (annotations) =>
  100 + thread = mail.messageThread().thread annotations.map (a) =>
  101 + m = mail.message(null, a.id, a.thread?.split('/') or [])
  102 + m.annotation = a
  103 + m
426 104
427   - items.enter().append('li').classed('annotation', true)
428   - items.exit().remove()
429   - items
430   - .html (d) =>
431   - env = $.extend {}, d.message.annotation,
432   - text: @renderer.makeHtml d.message.annotation.text
433   - Handlebars.templates.summary env
434   - .classed('detail', false)
435   - .classed('summary', true)
436   - .classed('paper', true)
437   - .on 'mouseup', (d, i) =>
438   - a = d.message.annotation
439   - query =
440   - thread: if a.thread then [a.thread, a.id].join('/') else a.id
441   - @plugins.Store._apiRequest 'search', query, (data) =>
442   - if data?.rows then this.updateViewer(data.rows || [])
443   - this.showViewer([a], true)
444   - .on 'mouseover', =>
445   - d3.event.stopPropagation()
446   - item = d3.select(d3.event.currentTarget).datum().message.annotation
447   - @provider.setActiveHighlights [item.hash.valueOf()]
448   - .on 'mouseout', =>
449   - d3.event.stopPropagation()
450   - item = d3.select(d3.event.currentTarget).datum().message.annotation
451   - @provider.setActiveHighlights @heatmap.buckets[@bucket]?.map (a) =>
452   - a.hash.valueOf()
453   - else
454   - # Mark that the detail view is now shown, so that exiting returns to the
455   - # bucket view rather than the document.
456   - @detail = true
  105 + # TODO: deal with empty parents
  106 + $scope.$apply (scope) =>
  107 + scope.annotations = (t.message.annotation for t in thread.children)
457 108
458   - excerpts.enter()
459   - .insert('li', '.annotation')
460   - .classed('paper', true)
461   - .classed('excerpt', true)
462   - .append('blockquote')
463   - excerpts.exit().remove()
  109 + $scope.annotation = null
  110 + $scope.annotations = []
464 111
465   - excerpts
466   - .select('blockquote').each (d) ->
467   - chars = 180
468   - quote = d.message.annotation.quote.replace(/\u00a0/g, ' ') # replace &nbsp;
469   - trunc = quote.substring(0, quote.lastIndexOf(' ', 180)) + "...<a href='#' class='more'>more</a>"
470   - if quote.length >= chars
471   - d3.select(this)
472   - .html(trunc)
473   - .on 'click', ->
474   - d3.event.stopPropagation()
475   - d3.event.preventDefault()
476   - if d3.select(d3.event.target).classed('more')
477   - d3.select(this).html(quote + "<a href='#' class='less'>less</a>")
478   - if d3.select(d3.event.target).classed('less')
479   - d3.select(this).html(trunc)
480   - else
481   - d3.select(this).html(quote)
  112 + $scope.getThread = (id) =>
  113 + if thread? then (thread.getSpecificChild id) else null
482 114
483   - highlights = []
484   - excerpts.each (d) =>
485   - h = d.message.annotation.hash
486   - if h then highlights.push h.valueOf()
487   - @provider.setActiveHighlights highlights
  115 + $scope.showDetail = (annotation) =>
  116 + $location.search 'detail', annotation.id
488 117
489   - while items.length
490   - items.enter().append('li').classed('annotation', true)
491   - items.exit().remove()
492   - items
493   - .each (d) -> # XXX: SLOW! Repeats calculation alllll the time
494   - count = d.flattenChildren()?.length or 0
495   - replyCount =
496   - toJSON: => undefined
497   - valueOf: =>
498   - "#{count} " + (if count == 1 then 'reply' else 'replies')
499   - unless count == 0
500   - d.message.annotation.replyCount = replyCount
501   - .html (d) =>
502   - env = $.extend {}, d.message.annotation,
503   - text: @renderer.makeHtml d.message.annotation.text
504   - Handlebars.templates.detail env
505   - .classed('paper', (c) -> not c.parent.message?)
506   - .classed('detail', true)
507   - .classed('summary', false)
508   -
509   - .sort (d, e) =>
510   - n = d.message.annotation.created
511   - m = e.message.annotation.created
512   - (n < m) - (m < n)
513   -
514   - .on 'mouseover', =>
515   - d3.event.stopPropagation()
516   - d3.select(d3.event.currentTarget).classed('hover', true)
517   -
518   - .on 'mouseout', =>
519   - d3.event.stopPropagation()
520   - d3.select(d3.event.currentTarget).classed('hover', false)
521   -
522   - .on 'mouseup', =>
523   - event = d3.event
524   - target = event.target
525   - unless target.tagName is 'A' then return
526   - event.stopPropagation()
527   -
528   - animate = (parent) ->
529   - collapsed = parent.classed('collapsed')
530   - parent.select('.thread')
531   - .transition().duration(200)
532   - .style('overflow', 'hidden')
533   - .style 'height', ->
534   - if collapsed
535   - "0px"
536   - else
537   - "#{$(this).children().outerHeight(true)}px"
538   - .each 'end', ->
539   - unless collapsed
540   - d3.select(this)
541   - .style('height', null)
542   - .style('overflow', null)
543   -
544   - parent = d3.select(event.currentTarget)
545   - switch d3.event.target.getAttribute('href')
546   - when '#collapse'
547   - d3.event.preventDefault()
548   - collapsed = parent.classed('collapsed')
549   - animate parent.classed('collapsed', !collapsed)
550   - when '#reply'
551   - unless @plugins.Permissions?.user
552   - this.showAuth true
553   - break
554   - d3.event.preventDefault()
555   - parent = d3.select(event.currentTarget)
556   - animate parent.classed('collapsed', false)
557   - reply = this.createAnnotation()
558   - reply.thread = this.threadId(parent.datum().message.annotation)
559   -
560   - editor = this._createEditor()
561   - editor.load(reply)
562   - editor.element.removeClass('annotator-outer')
563   - editor.on 'save', (annotation) =>
564   - this.publish 'annotationCreated', [annotation]
565   -
566   - d3.select(editor.element[0]).select('form')
567   - .data([reply])
568   - .html(Handlebars.templates.editor)
569   - .on 'mouseover', => d3.event.stopPropagation()
570   -
571   - item = d3.select(d3.event.currentTarget)
572   - .select('.annotator-listing')
573   - .insert('li', '.annotation')
574   - .classed('annotation', true)
575   - .classed('writer', true)
576   -
577   - editor.element.appendTo(item.node())
578   - editor.on('hide', => item.remove())
579   -
580   - editor.on 'hide save', =>
581   - @unsaved_drafts.splice(@unsaved_drafts.indexOf(editor), 1)
582   -
583   - editor.element.find(":input:first").focus()
584   -
585   - context = items.select '.thread'
586   - items = thread context, '.annotation'
587   -
588   - @editor.hide()
589   - @viewer.show()
590   -
591   - updateViewer: (annotations) =>
592   - existing = d3.select(@viewer.element[0]).datum()
593   - if existing?
594   - annotations = existing.flattenChildren()?.map((c) -> c.annotation)
595   - .concat(annotations)
596   - this.showViewer(annotations or [], @detail)
597   -
598   - showEditor: (annotation) =>
599   - if not annotation.user?
600   - @plugins.Permissions.addFieldsToAnnotation(annotation)
601   -
602   - @viewer.hide()
603   - @editor.load(annotation)
604   - @editor.element.find('.annotator-controls').remove()
605   -
606   - quote = annotation.quote.replace(/\u00a0/g, ' ') # replace &nbsp;
607   - excerpt = $('<li class="paper excerpt">')
608   - excerpt.append($("<blockquote>#{quote}</blockquote>"))
609   -
610   - item = $('<li class="annotation paper writer">')
611   - item.append($(Handlebars.templates.editor(annotation)))
612   -
613   - @editor.element.find('.annotator-listing').empty()
614   - .append(excerpt)
615   - .append(item)
616   - .find(":input:first").focus()
617   -
618   - @unsaved_drafts.push @editor
619   -
620   - d3.select(@viewer.element[0]).datum(null)
621   - this.show()
622   -
623   - show: =>
624   - if @detail
625   - annotations = d3.select(@viewer.element[0]).datum().children.map (c) =>
626   - c.message.annotation.hash.valueOf()
627   - else
628   - annotations = @heatmap.buckets[@bucket]?.map (a) => a.hash.valueOf()
629   -
630   - @visible = true
631   - @provider.setActiveHighlights annotations
632   - @provider.showFrame()
633   - @element.find('#toolbar').addClass('shown')
634   - .find('.tri').attr('draggable', true)
635   -
636   - hide: =>
637   - @lastWidth = window.innerWidth
638   - @visible = false
639   - @provider.setActiveHighlights []
640   - @provider.hideFrame()
641   - @element.find('#toolbar').removeClass('shown')
642   - .find('.tri').attr('draggable', false)
643   -
644   - _canCloseUnsaved: ->
645   - # See if there's an unsaved/uncancelled reply
646   - can_close = true
647   - open_editors = 0
648   - for editor in @unsaved_drafts
649   - unsaved_text = editor.element.find(':input:first').attr 'value'
650   - if unsaved_text? and unsaved_text.toString().length > 0
651   - open_editors += 1
652   -
653   - if open_editors > 0
654   - if open_editors > 1
655   - ctext = "You have #{open_editors} unsaved replies."
  118 + $scope.$on '$routeUpdate', =>
  119 + if $routeParams.detail?
  120 + thread = thread?.getSpecificChild $routeParams.detail
  121 + $scope.annotation = thread?.message.annotation
656 122 else
657   - ctext = "You have an unsaved reply."
658   - ctext = ctext + " Do you really want to close the view?"
659   - can_close = confirm ctext
660   -
661   - if can_close then @unsaved_drafts = []
662   - can_close
663   -
664   - threadId: (annotation) ->
665   - if annotation?.thread?
666   - annotation.thread + '/' + annotation.id
667   - else
668   - annotation.id
669   -
670   - showAuth: (show=true) =>
671   - $header = @element.find('header')
672   - visible = !$header.hasClass('collapsed')
673   - if (visible != show)
674   - if (show)
675   - $header.removeClass('collapsed')
676   - else
677   - $header.addClass('collapsed')
678   - $header.one 'webkitTransitionEnd transitionend OTransitionEnd', =>
679   - $header.find('.nav-tabs > li.active > a').trigger('shown')
680   -
681   - submit: =>
682   - controls = @element.find('.sheet .active form').formSerialize()
683   - @http.post '', controls,
684   - headers:
685   - 'Content-Type': 'application/x-www-form-urlencoded'
686   - withCredentials: true
687   - .success (data) =>
688   - # Extend the scope with updated model data
689   - angular.extend(@scope, data.model) if data.model?
690   -
691   - # Replace any forms which were re-rendered in this response
692   - for oid of data.form
693   - target = '#' + oid
694   -
695   - $form = $(data.form[oid])
696   - $form.replaceAll(target)
697   -
698   - link = @compile $form
699   - link @scope
700   -
701   - deform.focusFirstInput target
702   -
703   - reset: =>
704   - angular.extend @scope,
705   - username: null
706   - password: null
707   - email: null
708   - code: null
709   - personas: []
710   - persona: null
711   - token: null
  123 + $scope.annotation = null
712 124
713 125
714 126 angular.module('h.controllers', [])
715   - .controller('Hypothesis', Hypothesis)
  127 + .controller('App', App)
  128 + .controller('Auth', Auth)
  129 + .controller('Viewer', Viewer)
69 h/js/directives.coffee
... ... @@ -1,16 +1,63 @@
1   -navTabsDirective = (deform) ->
  1 +annotation = ($filter) ->
2 2 link: (scope, iElement, iAttrs, controller) ->
3   - iElement.find('a')
  3 + annotation = scope.annotation
  4 + thread = scope.getThread annotation.id
  5 + angular.extend scope, annotation
  6 + angular.extend scope,
  7 + created: ($filter 'fuzzyTime') annotation.created
  8 + user: ($filter 'userName') annotation.user
  9 + text: ($filter 'converter') annotation.text
  10 + replies: (c.message.annotation for c in (thread?.children or []))
  11 + replyCount: thread?.flattenChildren()?.length or 0
  12 + restrict: 'C'
  13 + scope: true
  14 +annotation.$inject = ['$filter']
4 15
5   - # Focus the first form element when showing a tab pane
6   - .on 'shown', (e) ->
7   - target = $(e.target).data('target')
8   - deform.focusFirstInput(target)
9 16
10   - # Always show the first pane to start
11   - .first().tab('show')
12   - restrict: 'C'
13   -navTabsDirective.$inject = ['deform']
  17 +tabReveal = ($parse) ->
  18 + compile: (tElement, tAttrs, transclude) ->
  19 + panes = []
  20 +
  21 + pre: (scope, iElement, iAttrs, [ngModel, tabbable] = controller) ->
  22 + # Hijack the tabbable controller's addPane so that the visibility of the
  23 + # secret ones can be managed. This avoids traversing the DOM to find
  24 + # the tab panes.
  25 + addPane = tabbable.addPane
  26 + tabbable.addPane = (element, attr) =>
  27 + removePane = addPane.call tabbable, element, attr
  28 + panes.push
  29 + element: element
  30 + attr: attr
  31 + =>
  32 + for i in [0..panes.length]
  33 + if panes[i].element is element
  34 + panes.splice i, 1
  35 + break
  36 + removePane()
  37 +
  38 + post: (scope, iElement, iAttrs, [ngModel, tabbable] = controller) ->
  39 + tabs = angular.element(iElement.children()[0]).find('li')
  40 + hiddenPanes = ($parse iAttrs.tabReveal)()
  41 + unless angular.isArray hiddenPanes
  42 + throw (new TypeError 'tabReveal expression must evaluate to an Array')
  43 +
  44 + update = =>
  45 + for i in [0..panes.length-1]
  46 + pane = panes[i]
  47 + value = pane.attr.value || pane.attr.title
  48 + if value == ngModel.$modelValue
  49 + deform.focusFirstInput pane.element
  50 + pane.element.css 'display', ''
  51 + angular.element(tabs[i]).css 'display', ''
  52 + else if value in hiddenPanes
  53 + pane.element.css 'display', 'none'
  54 + angular.element(tabs[i]).css 'display', 'none'
  55 +
  56 + scope.$watch iAttrs.ngModel, => scope.$evalAsync update
  57 + require: ['ngModel', 'tabbable']
  58 +tabReveal.$inject = ['$parse']
  59 +
14 60
15 61 angular.module('h.directives', ['ngSanitize', 'deform'])
16   - .directive('navTabs', navTabsDirective)
  62 + .directive('annotation', annotation)
  63 + .directive('tabReveal', tabReveal)
2  h/js/inject/host.coffee
@@ -97,7 +97,7 @@ class Annotator.Host extends Annotator
97 97 .each ->
98 98 if $(this).data('annotation').hash in hashes
99 99 $(this).addClass('annotator-hl-active')
100   - else
  100 + else if not $(this).hasClass('annotator-hl-temporary')
101 101 $(this).removeClass('annotator-hl-active')
102 102 getMaxBottom: =>
103 103 sel = '*' + (":not(.annotator-#{x})" for x in [
7 h/js/plugin/heatmap.coffee
... ... @@ -1,8 +1,7 @@
1 1 class Annotator.Plugin.Heatmap extends Annotator.Plugin
2   -
3 2 # prototype constants
4   - this::BUCKET_THRESHOLD_PAD = 40
5   - this::BUCKET_SIZE = 50
  3 + BUCKET_THRESHOLD_PAD: 40
  4 + BUCKET_SIZE: 50
6 5
7 6 # heatmap svg skeleton
8 7 html: """
@@ -30,7 +29,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
30 29 index: []
31 30
32 31 constructor: (element, options) ->
33   - super $(@html, options)
  32 + super $(@html), options
34 33
35 34 _collate: (a, b) =>
36 35 for i in [0..a.length-1]
431 h/js/services.coffee
... ... @@ -1 +1,432 @@
  1 +class Hypothesis extends Annotator
  2 + # Plugin configuration
  3 + options:
  4 + Heatmap: {}
  5 + Permissions:
  6 + showEditPermissionsCheckbox: false,
  7 + showViewPermissionsCheckbox: false,
  8 + userString: (user) -> user.replace(/^acct:(.+)@(.+)$/, '$1 on $2')
  9 +
  10 + # Internal state
  11 + bucket: -1 # * The index of the bucket shown in the summary view
  12 + detail: false # * Whether the viewer shows a summary or detail listing
  13 + hash: -1 # * cheap UUID :cake:
  14 + cache: {} # * object cache
  15 + visible: false # * Whether the sidebar is visible
  16 + unsaved_drafts: [] # * Unsaved drafts currenty open
  17 +
  18 + this.$inject = ['$document']
  19 + constructor: ($document) ->
  20 + super
  21 +
  22 + # Load plugins
  23 + for own name, opts of @options
  24 + if not @plugins[name] and name of Annotator.Plugin
  25 + this.addPlugin(name, opts)
  26 +
  27 + # Establish cross-domain communication to the widget host
  28 + @provider = new easyXDM.Rpc
  29 + swf: @options.swf
  30 + onReady: this._initialize
  31 + ,
  32 + local:
  33 + publish: (event, args, k, fk) =>
  34 + if event in ['annotationCreated']
  35 + [h] = args
  36 + annotation = @cache[h]
  37 + this.publish event, [annotation]
  38 + addPlugin: => this.addPlugin arguments...
  39 + createAnnotation: =>
  40 + if @plugins.Permissions.user?
  41 + @cache[h = ++@hash] = this.createAnnotation()
  42 + h
  43 + else
  44 + this.showAuth true
  45 + this.show()
  46 + null
  47 + showEditor: (stub) =>
  48 + return unless this._canCloseUnsaved()
  49 + h = stub.hash
  50 + annotation = $.extend @cache[h], stub,
  51 + hash:
  52 + toJSON: => undefined
  53 + valueOf: => h
  54 + this.showEditor annotation
  55 + # This guy does stuff when you "back out" of the interface.
  56 + # (Currently triggered by a click on the source page.)
  57 + back: =>
  58 + # If it's in the detail view, loads the bucket back up.
  59 + if @detail
  60 + this.showViewer(@heatmap.buckets[@bucket])
  61 + this.publish('hostUpdated')
  62 + # If it's not in the detail view, the assumption is that it's in the
  63 + # bucket view and hides the whole interface.
  64 + else
  65 + this.hide()
  66 + update: => this.publish 'hostUpdated'
  67 + remote:
  68 + publish: {}
  69 + setupAnnotation: {}
  70 + onEditorHide: {}
  71 + onEditorSubmit: {}
  72 + showFrame: {}
  73 + hideFrame: {}
  74 + dragFrame: {}
  75 + getHighlights: {}
  76 + setActiveHighlights: {}
  77 + getMaxBottom: {}
  78 + scrollTop: {}
  79 +
  80 + _initialize: =>
  81 + # Set up interface elements
  82 + this._setupHeatmap()
  83 + @heatmap.element.appendTo(document.body)
  84 +
  85 + @provider.getMaxBottom (max) =>
  86 + @element.find('#toolbar').css("top", "#{max}px")
  87 + @element.find('#gutter').css("margin-top", "#{max}px")
  88 + @heatmap.BUCKET_THRESHOLD_PAD = (
  89 + max + @heatmap.constructor.prototype.BUCKET_THRESHOLD_PAD
  90 + )
  91 +
  92 + this.subscribe 'beforeAnnotationCreated', (annotation) =>
  93 + annotation.created = annotation.updated = (new Date()).toString()
  94 + annotation.user = @plugins.Permissions.options.userId(
  95 + @plugins.Permissions.user)
  96 +
  97 + this.publish 'hostUpdated'
  98 +
  99 + _setupWrapper: ->
  100 + @wrapper = @element.find('#wrapper')
  101 + .on 'mousewheel', (event, delta) ->
  102 + # prevent overscroll from scrolling host frame
  103 + # This is actually a bit tricky. Starting from the event target and
  104 + # working up the DOM tree, find an element which is scrollable
  105 + # and has a scrollHeight larger than its clientHeight.
  106 + # I've obsered that some styles, such as :before content, may increase
  107 + # scrollHeight of non-scrollable elements, and that there a mysterious
  108 + # discrepancy of 1px sometimes occurs that invalidates the equation
  109 + # typically cited for determining when scrolling has reached bottom:
  110 + # (scrollHeight - scrollTop == clientHeight)
  111 + $current = $(event.target)
  112 + while (
  113 + ($current.css('overflow') in ['visible', '']) or
  114 + ($current[0].scrollHeight == $current[0].clientHeight)
  115 + )
  116 + $current = $current.parent()
  117 + if not $current[0]? then return event.preventDefault()
  118 + scrollTop = $current[0].scrollTop
  119 + scrollEnd = $current[0].scrollHeight - $current[0].clientHeight
  120 + if delta > 0 and scrollTop == 0
  121 + event.preventDefault()
  122 + else if delta < 0 and scrollEnd - scrollTop <= 1
  123 + event.preventDefault()
  124 + this
  125 +
  126 + _setupDocumentEvents: ->
  127 + @element.find('#toolbar .tri').click =>
  128 + if @visible
  129 + this.hide()
  130 + else
  131 + if @viewer.isShown() and @bucket == -1
  132 + this._fillDynamicBucket()
  133 + this.show()
  134 +
  135 + el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
  136 + el.width = el.height = 1
  137 + @element.append el
  138 +
  139 + handle = @element.find('#toolbar .tri')[0]
  140 + handle.addEventListener 'dragstart', (event) =>
  141 + event.dataTransfer.setData 'text/plain', ''
  142 + event.dataTransfer.setDragImage(el, 0, 0)
  143 + @provider.dragFrame event.screenX
  144 + handle.addEventListener 'dragend', (event) =>
  145 + @provider.dragFrame event.screenX
  146 + @element[0].addEventListener 'dragover', (event) =>
  147 + @provider.dragFrame event.screenX
  148 + @element[0].addEventListener 'dragleave', (event) =>
  149 + @provider.dragFrame event.screenX
  150 +
  151 + this
  152 +
  153 + _setupDynamicStyle: ->
  154 + this
  155 +
  156 + _setupHeatmap: () ->
  157 + @heatmap = @plugins.Heatmap
  158 +
  159 + # Update the heatmap when certain events are pubished
  160 + events = [
  161 + 'annotationCreated'
  162 + 'annotationDeleted'
  163 + 'annotationsLoaded'
  164 + 'hostUpdated'
  165 + ]
  166 +
  167 + for event in events
  168 + this.subscribe event, =>
  169 + @provider.getHighlights ({highlights, offset}) =>
  170 + @heatmap.updateHeatmap
  171 + highlights: highlights.map (hl) =>
  172 + hl.data = @cache[hl.data]
  173 + hl
  174 + offset: offset
  175 + if @visible and @viewer.isShown() and @bucket == -1 and not @detail
  176 + this._fillDynamicBucket()
  177 +
  178 + @heatmap.element.click =>
  179 + @bucket = -1
  180 + this._fillDynamicBucket()
  181 + this.show()
  182 +
  183 + @heatmap.subscribe 'updated', =>
  184 + tabs = d3.select(document.body)
  185 + .selectAll('div.heatmap-pointer')
  186 + .data =>
  187 + buckets = []
  188 + @heatmap.index.forEach (b, i) =>
  189 + if @heatmap.buckets[i].length > 0
  190 + buckets.push i
  191 + else if @heatmap.isUpper(i) or @heatmap.isLower(i)
  192 + buckets.push i
  193 + buckets
  194 +
  195 + {highlights, offset} = d3.select(@heatmap.element[0]).datum()
  196 + height = $(window).outerHeight(true)
  197 + pad = height * .2
  198 +
  199 + # Enters into tabs var, and generates bucket pointers from them
  200 + tabs.enter().append('div')
  201 + .classed('heatmap-pointer', true)
  202 +
  203 + tabs.exit().remove()
  204 +
  205 + tabs
  206 +
  207 + .style 'top', (d) =>
  208 + "#{(@heatmap.index[d] + @heatmap.index[d+1]) / 2}px"
  209 +
  210 + .html (d) =>
  211 + "<div class='label'>#{@heatmap.buckets[d].length}</div><div class='svg'></div>"
  212 +
  213 + .classed('upper', @heatmap.isUpper)
  214 + .classed('lower', @heatmap.isLower)
  215 +
  216 + .style 'display', (d) =>
  217 + if (@heatmap.buckets[d].length is 0) then 'none' else ''
  218 +
  219 + # Creates highlights corresponding bucket when mouse is hovered
  220 + .on 'mousemove', (bucket) =>
  221 + unless @viewer.isShown() and @detail
  222 + unless @heatmap.buckets[bucket]?.length then bucket = @bucket
  223 + @provider.setActiveHighlights @heatmap.buckets[bucket]?.map (a) =>
  224 + a.hash.valueOf()
  225 +
  226 + # Gets rid of them after
  227 + .on 'mouseout', =>
  228 + unless @viewer.isShown() and @detail
  229 + @provider.setActiveHighlights @heatmap.buckets[@bucket]?.map (a) =>
  230 + a.hash.valueOf()
  231 +
  232 + # Does one of a few things when a tab is clicked depending on type
  233 + .on 'mouseup', (bucket) =>
  234 + d3.event.preventDefault()
  235 +
  236 + # If it's the upper tab, scroll to next bucket above
  237 + if @heatmap.isUpper bucket
  238 + threshold = offset + @heatmap.index[0]
  239 + next = highlights.reduce (next, hl) ->
  240 + if next < hl.offset.top < threshold then hl.offset.top else next
  241 + , threshold - height
  242 + @provider.scrollTop next - pad
  243 + @bucket = -1
  244 + this._fillDynamicBucket()
  245 +
  246 + # If it's the lower tab, scroll to next bucket below
  247 + else if @heatmap.isLower bucket
  248 + threshold = offset + @heatmap.index[0] + pad
  249 + next = highlights.reduce (next, hl) ->
  250 + if threshold < hl.offset.top < next then hl.offset.top else next
  251 + , offset + height
  252 + @provider.scrollTop next - pad
  253 + @bucket = -1
  254 + this._fillDynamicBucket()
  255 +
  256 + # If it's neither of the above, load the bucket into the viewer
  257 + else
  258 + annotations = @heatmap.buckets[bucket]
  259 + @bucket = bucket
  260 + this.showViewer(annotations)
  261 + this.show()
  262 +
  263 + this
  264 +
  265 + # Creates an instance of Annotator.Viewer and assigns it to the @viewer
  266 + # property, appends it to the @wrapper and sets up event listeners.
  267 + #
  268 + # Returns itself to allow chaining.
  269 + _setupViewer: ->
  270 + @viewer = new Annotator.Viewer(readOnly: @options.readOnly)
  271 + @viewer.hide()
  272 + .on("edit", this.onEditAnnotation)
  273 + .on("delete", this.onDeleteAnnotation)
  274 +
  275 + this
  276 +
  277 + # Creates an instance of the Annotator.Editor and assigns it to @editor.
  278 + # Appends this to the @wrapper and sets up event listeners.
  279 + #
  280 + # Returns itself for chaining.
  281 + _setupEditor: ->
  282 + @editor = this._createEditor()
  283 + .on 'hide save', =>
  284 + if @unsaved_drafts.indexOf(@editor) > -1
  285 + @unsaved_drafts.splice(@unsaved_drafts.indexOf(@editor), 1)
  286 + .on 'hide', =>
  287 + @provider.onEditorHide()
  288 + .on 'save', =>
  289 + @provider.onEditorSubmit()
  290 + this
  291 +
  292 + _createEditor: ->
  293 + editor = new Annotator.Editor()
  294 + editor.hide()
  295 + editor.fields = [{
  296 + element: editor.element,
  297 + load: (field, annotation) ->
  298 + $(field).find('textarea').val(annotation.text || '')
  299 + submit: (field, annotation) ->
  300 + annotation.text = $(field).find('textarea').val()
  301 + }]
  302 +
  303 + @unsaved_drafts.push editor
  304 + editor
  305 +
  306 + _fillDynamicBucket: ->