Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

...
  • 9 commits
  • 14 files changed
  • 0 commit comments
  • 1 contributor
Commits on 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
View
@@ -75,7 +75,6 @@ def Handlebars(*names, **kw):
templates,
underscore,
'h:lib/jquery.mousewheel.min.js',
- 'deform_bootstrap:static/bootstrap.min.js',
Uglify(
*[
Coffee('h:/js/%s.coffee' % name,
57 h/css/app.scss
View
@@ -23,19 +23,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
-//HIDDEN TABS////////////////////////////////
-.nav a[data-target="#activate-tab"],
-.nav a[data-target="#forgot-tab"] {
- display: none;
-}
-
-.nav .active a[data-target="#activate-tab"],
-.nav .active a[data-target="#forgot-tab"] {
- display: initial;
-}
-
-
-
//ANNOTATOR STYLES////////////////////////////////
.annotator-hide {
display: none;
@@ -52,7 +39,25 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
width: $heatmap-width;
}
+.sliding-panels > li {
+ @include single-transition(left, 0.4s, ease-in);
+ padding-left: 1.5em;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ & > * {
+ @include smallshadow(-2px);
+ }
+
+ &:first-of-type {
+ padding-left: 0;
+ }
+
+ &.collapsed {
+ left: 100%;
+ }
+}
//SIDEBAR LAYOUT////////////////////////////////
#gutter {
@@ -60,14 +65,15 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
background-attachment: fixed;
height: 100%;
margin-left: $heatmap-width + 17px;
+ padding-top: 2.5em; // toolbar height + top margin
position: relative;
}
#wrapper {
+ background: url('../images/noise_1.png');
+ background-attachment: fixed;
height: 100%;
- overflow: auto;
- padding: 3.75em 1em 1em 1em;
- -webkit-overflow-scrolling: touch;
+ position: relative;
}
@@ -170,14 +176,14 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
z-index: 4;
background: url('../images/noise_1.png');
background-attachment: fixed;
- margin-top: 2.5em;
- padding-top: 1em;
+ padding: 1em;
position: absolute;
width: 100%;
.close {
- margin-right: .5em;
- margin-top: .25em;
+ position: absolute;
+ right: .5em;
+ top: .5em;
}
&.collapsed {
@@ -201,6 +207,11 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
background-color: #f2dede;
border-color: #F5A1A0;
}
+
+ footer > ul {
+ margin-top: 1em;
+ text-align: right;
+ }
}
@@ -212,15 +223,15 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
border: 1px solid $grayLighter;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
- left: 1px;
- line-height: $baseLineHeight;
+ height: 2em;
+ left: 7px;
+ line-height: 1.4em; // 2em (height) - .6em (padding)
margin-top: .5em;
padding: .3em;
position: absolute;
text-align: right;
width: 100%;
z-index: 5;
- left: 7px;
& > div {
display: inline-block;
77 h/css/common.scss
View
@@ -207,6 +207,18 @@ a {
}
+//OUTER//////////////////////////////////
+.annotator-outer {
+ background: url('../images/noise_1.png');
+ background-attachment: fixed;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ padding: 1em;
+ -webkit-overflow-scrolling: touch;
+}
+
+
//EXCERPT////////////////////////////////
.excerpt {
@@ -349,10 +361,6 @@ blockquote {
&.active {
display: inherit !important;
}
-
- footer > ul {
- text-align: right;
- }
}
@@ -372,6 +380,7 @@ blockquote {
//ANNOTATION////////////////////////////////
//This is for everything that is formatted as an annotation.
.annotation {
+ position: relative;
.user {
font-weight: bold;
@@ -396,15 +405,35 @@ blockquote {
}
+//THREADING////////////////////////////////
+//Threaded discussion specific
+.thread .annotator-listing {
+ border-left: 1px dotted $grayLight;
+
+ .threadexp {
+ height: $threadexp-width;
+ width: $threadexp-width;
+ position: absolute;
+ top: .8em;
+ left: -($threadexp-width / 2);
+ outline: 1px dotted #aaa;
+ @include icon("minus_1.png");
+ }
+
+ .annotation {
+ padding-left: $thread-padding;
+ padding-top: .35em;
+ &.squished {
+ padding-left: 0;
+ }
+ }
+}
+
+
//DETAIL////////////////////////////////
//This is specific to the detail view.
.detail {
- position: relative;
- .paper > .threadexp {
- display: none;
- }
-
.annotator-controls {
@include single-transition(opacity, .25s, ease-in-out);
opacity: 0;
@@ -421,29 +450,6 @@ blockquote {
margin-bottom: .25em;
}
- //Threaded discussion specific
- .annotator-listing {
- border-left: 1px dotted $grayLight;
-
- .threadexp {
- height: $threadexp-width;
- width: $threadexp-width;
- position: absolute;
- top: .8em;
- left: -($threadexp-width / 2);
- outline: 1px dotted #aaa;
- @include icon("minus_1.png");
- }
-
- & > .detail, .writer {
- padding-left: $thread-padding;
- padding-top: .35em;
- &.squished {
- padding-left: 0;
- }
- }
- }
-
&.hover {
& > .annotator-controls {
opacity: 1;
@@ -451,7 +457,7 @@ blockquote {
}
//These are all the changes needed to collapse thread objects.
- &.collapsed {
+ .collapsed {
& > .body {
overflow: hidden;
text-overflow: ellipsis;
@@ -490,6 +496,11 @@ blockquote {
@include smallshadow(2px, 1px, .1);
bottom: 0px;
}
+
+ // Things not shown in the summary view
+ .annotator-controls, .bottombar, .excerpt, .thread {
+ display: none;
+ }
}
24 h/js/app.coffee
View
@@ -1,7 +1,19 @@
-angular.module 'h', [
- 'deform'
- 'h.controllers'
- 'h.directives'
- 'h.filters'
- 'h.services'
+imports = [
+ 'bootstrap'
+ 'deform'
+ 'h.controllers'
+ 'h.directives'
+ 'h.filters'
+ 'h.services'
]
+
+
+configure = ($routeProvider, $locationProvider) ->
+ $routeProvider.when '/app/viewer',
+ controller: 'Viewer'
+ reloadOnSearch: false
+ templateUrl: 'viewer.html'
+configure.$inject = ['$routeProvider', '$locationProvider']
+
+
+angular.module('h', imports, configure)
762 h/js/controllers.coffee
View
@@ -1,715 +1,129 @@
-class Hypothesis extends Annotator
- # Plugin configuration
- options:
- Heatmap: {}
- Permissions:
- showEditPermissionsCheckbox: false,
- showViewPermissionsCheckbox: false,
- userString: (user) -> user.replace(/^acct:(.+)@(.+)$/, '$1 on $2')
+class App
+ this.$inject = ['$compile', '$http', '$location', '$scope', 'annotator']
+ constructor: ($compile, $http, $location, $scope, annotator) ->
+ {plugins} = annotator
- # Internal state
- bucket: -1 # * The index of the bucket shown in the summary view
- detail: false # * Whether the viewer shows a summary or detail listing
- hash: -1 # * cheap UUID :cake:
- cache: {} # * object cache
- visible: false # * Whether the sidebar is visible
- unsaved_drafts: [] # * Unsaved drafts currenty open
+ $location.path '/app/viewer'
- this.$inject = ['$rootElement', '$scope', '$compile', '$http']
- constructor: (@element, @scope, @compile, @http) ->
- super @element, @options
+ $scope.$on 'reset', =>
+ angular.extend $scope,
+ personas: []
+ persona: null
+ token: null
- # Load plugins
- for own name, opts of @options
- if not @plugins[name] and name of Annotator.Plugin
- this.addPlugin(name, opts)
-
- # Export public, bound functions on the scope
- for own key of this
- if typeof this[key] == 'function' and not key.match('^_')
- @scope[key] = this[key]
-
- # Establish cross-domain communication to the widget host
- @provider = new easyXDM.Rpc
- swf: @options.swf
- onReady: this._initialize
- ,
- local:
- publish: (event, args, k, fk) =>
- if event in ['annotationCreated']
- [h] = args
- annotation = @cache[h]
- this.publish event, [annotation]
- addPlugin: => this.addPlugin arguments...
- createAnnotation: =>
- if @plugins.Permissions.user?
- @cache[h = ++@hash] = this.createAnnotation()
- h
- else
- this.showAuth true
- this.show()
- null
- showEditor: (stub) =>
- return unless this._canCloseUnsaved()
- h = stub.hash
- annotation = $.extend @cache[h], stub,
- hash:
- toJSON: => undefined
- valueOf: => h
- this.showEditor annotation
- # This guy does stuff when you "back out" of the interface.
- # (Currently triggered by a click on the source page.)
- back: =>
- # If it's in the detail view, loads the bucket back up.
- if @detail
- this.showViewer(@heatmap.buckets[@bucket])
- this.publish('hostUpdated')
- # If it's not in the detail view, the assumption is that it's in the
- # bucket view and hides the whole interface.
- else
- this.hide()
- update: => this.publish 'hostUpdated'
- remote:
- publish: {}
- setupAnnotation: {}
- onEditorHide: {}
- onEditorSubmit: {}
- showFrame: {}
- hideFrame: {}
- dragFrame: {}
- getHighlights: {}
- setActiveHighlights: {}
- getMaxBottom: {}
- scrollTop: {}
-
- # Prepare a MarkDown renderer, and add some post-processing
- # so that all created links have their target set to _blank
- @renderer = Markdown.getSanitizingConverter()
- @renderer.hooks.chain "postConversion", (text) ->
- text.replace /<a href=/, "<a target=\"_blank\" href="
-
- @scope.$watch 'personas', (newValue, oldValue) =>
+ $scope.$watch 'personas', (newValue, oldValue) =>
if newValue?.length
- @element.find('#persona')
+ annotator.element.find('#persona')
.off('change').on('change', -> $(this).submit())
.off('click')
- this.showAuth(false)
+ $scope.showAuth = false
else
- @scope.persona = null
- @scope.token = null
- @element.find('#persona').off('click').on('click', => this.showAuth())
+ $scope.persona = null
+ $scope.token = null
- @scope.$watch 'persona', (newValue, oldValue) =>
+ $scope.$watch 'persona', (newValue, oldValue) =>
if oldValue? and not newValue?
- @http.post 'logout', '',
+ $http.post 'logout', '',
withCredentials: true
- .success (data) => @scope.reset()
+ .success (data) => $scope.reset()
- @scope.$watch 'token', (newValue, oldValue) =>
- if @plugins.Auth?
- @plugins.Auth.token = newValue
- @plugins.Auth.updateHeaders()
+ $scope.$watch 'token', (newValue, oldValue) =>
+ if plugins.Auth?
+ plugins.Auth.token = newValue
+ plugins.Auth.updateHeaders()
if newValue?
- if not @plugins.Auth?
- this.addPlugin 'Auth',
- tokenUrl: @scope.tokenUrl
+ if not plugins.Auth?
+ annotator.addPlugin 'Auth',
+ tokenUrl: $scope.tokenUrl
token: newValue
else
- @plugins.Auth.setToken(newValue)
- @plugins.Auth.withToken @plugins.Permissions._setAuthFromToken
+ plugins.Auth.setToken(newValue)
+ plugins.Auth.withToken plugins.Permissions._setAuthFromToken
else
- @plugins.Permissions.setUser(null)
- delete @plugins.Auth
+ plugins.Permissions.setUser(null)
+ delete plugins.Auth
# Fetch the initial model from the server
- @http.get 'model'
+ $http.get 'model',
withCredentials: true
.success (data) =>
- angular.extend @scope, data
-
- this.reset()
- this.showAuth false
-
- this
-
- _initialize: =>
- # Set up interface elements
- this._setupHeatmap()
- @wrapper.append(@viewer.element, @editor.element)
- @heatmap.element.appendTo(document.body)
- @viewer.show()
-
- @provider.getMaxBottom (max) =>
- @element.find('#toolbar').css("top", "#{max}px")
- @element.find('#gutter').css("padding-top", "#{max}px")
- @heatmap.BUCKET_THRESHOLD_PAD = (
- max + @heatmap.constructor.prototype.BUCKET_THRESHOLD_PAD
- )
-
- this.subscribe 'beforeAnnotationCreated', (annotation) =>
- annotation.created = annotation.updated = (new Date()).toString()
- annotation.user = @plugins.Permissions.options.userId(
- @plugins.Permissions.user)
-
- this.publish 'hostUpdated'
-
- _setupWrapper: ->
- @wrapper = @element.find('#wrapper')
- .on 'mousewheel', (event, delta) ->
- # prevent overscroll from scrolling host frame
- # http://stackoverflow.com/questions/5802467
- scrollTop = $(this).scrollTop()
- scrollBottom = $(this).get(0).scrollHeight - $(this).innerHeight()
- if delta > 0 and scrollTop == 0
- event.preventDefault()
- else if delta < 0 and scrollTop == scrollBottom
- event.preventDefault()
- this
-
- _setupDocumentEvents: ->
- @element.find('#toolbar .tri').click =>
- if @visible
- this.hide()
- else
- if @viewer.isShown() and @bucket == -1
- this._fillDynamicBucket()
- this.show()
-
- el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
- el.width = el.height = 1
- @element.append el
-
- handle = @element.find('#toolbar .tri')[0]
- handle.addEventListener 'dragstart', (event) =>
- event.dataTransfer.setData 'text/plain', ''
- event.dataTransfer.setDragImage(el, 0, 0)
- @provider.dragFrame event.screenX
- handle.addEventListener 'dragend', (event) =>
- @provider.dragFrame event.screenX
- @element[0].addEventListener 'dragover', (event) =>
- @provider.dragFrame event.screenX
- @element[0].addEventListener 'dragleave', (event) =>
- @provider.dragFrame event.screenX
-
- this
-
- _setupDynamicStyle: ->
- this
-
- _setupHeatmap: () ->
- @heatmap = @plugins.Heatmap
-
- # Update the heatmap when certain events are pubished
- events = [
- 'annotationCreated'
- 'annotationDeleted'
- 'annotationsLoaded'
- 'hostUpdated'
- ]
-
- for event in events
- this.subscribe event, =>
- @provider.getHighlights ({highlights, offset}) =>
- @heatmap.updateHeatmap
- highlights: highlights.map (hl) =>
- hl.data = @cache[hl.data]
- hl
- offset: offset
- if @visible and @viewer.isShown() and @bucket == -1 and not @detail
- this._fillDynamicBucket()
-
- @heatmap.element.click =>
- @bucket = -1
- this._fillDynamicBucket()
- this.show()
-
- @heatmap.subscribe 'updated', =>
- tabs = d3.select(document.body)
- .selectAll('div.heatmap-pointer')
- .data =>
- buckets = []
- @heatmap.index.forEach (b, i) =>
- if @heatmap.buckets[i].length > 0
- buckets.push i
- else if @heatmap.isUpper(i) or @heatmap.isLower(i)
- buckets.push i
- buckets
-
- {highlights, offset} = d3.select(@heatmap.element[0]).datum()
- height = $(window).outerHeight(true)
- pad = height * .2
-
- # Enters into tabs var, and generates bucket pointers from them
- tabs.enter().append('div')
- .classed('heatmap-pointer', true)
-
- tabs.exit().remove()
-
- tabs
-
- .style 'top', (d) =>
- "#{(@heatmap.index[d] + @heatmap.index[d+1]) / 2}px"
-
- .html (d) =>
- "<div class='label'>#{@heatmap.buckets[d].length}</div><div class='svg'></div>"
-
- .classed('upper', @heatmap.isUpper)
- .classed('lower', @heatmap.isLower)
-
- .style 'display', (d) =>
- if (@heatmap.buckets[d].length is 0) then 'none' else ''
-
- # Creates highlights corresponding bucket when mouse is hovered
- .on 'mousemove', (bucket) =>
- unless @viewer.isShown() and @detail
- unless @heatmap.buckets[bucket]?.length then bucket = @bucket
- @provider.setActiveHighlights @heatmap.buckets[bucket]?.map (a) =>
- a.hash.valueOf()
-
- # Gets rid of them after
- .on 'mouseout', =>
- unless @viewer.isShown() and @detail
- @provider.setActiveHighlights @heatmap.buckets[@bucket]?.map (a) =>
- a.hash.valueOf()
-
- # Does one of a few things when a tab is clicked depending on type
- .on 'mouseup', (bucket) =>
- d3.event.preventDefault()
-
- # If it's the upper tab, scroll to next bucket above
- if @heatmap.isUpper bucket
- threshold = offset + @heatmap.index[0]
- next = highlights.reduce (next, hl) ->
- if next < hl.offset.top < threshold then hl.offset.top else next
- , threshold - height
- @provider.scrollTop next - pad
- @bucket = -1
- this._fillDynamicBucket()
-
- # If it's the lower tab, scroll to next bucket below
- else if @heatmap.isLower bucket
- threshold = offset + @heatmap.index[0] + pad
- next = highlights.reduce (next, hl) ->
- if threshold < hl.offset.top < next then hl.offset.top else next
- , offset + height
- @provider.scrollTop next - pad
- @bucket = -1
- this._fillDynamicBucket()
-
- # If it's neither of the above, load the bucket into the viewer
- else
- annotations = @heatmap.buckets[bucket]
- @bucket = bucket
- this.showViewer(annotations)
- this.show()
-
- this
-
- # Creates an instance of Annotator.Viewer and assigns it to the @viewer
- # property, appends it to the @wrapper and sets up event listeners.
- #
- # Returns itself to allow chaining.
- _setupViewer: ->
- @viewer = new Annotator.Viewer(readOnly: @options.readOnly)
- @viewer.hide()
- .on("edit", this.onEditAnnotation)
- .on("delete", this.onDeleteAnnotation)
-
- # Show newly created annotations in the viewer immediately
- this.subscribe 'annotationCreated', (annotation) =>
- this.updateViewer [annotation]
-
- this
+ angular.extend $scope, data
- # Creates an instance of the Annotator.Editor and assigns it to @editor.
- # Appends this to the @wrapper and sets up event listeners.
- #
- # Returns itself for chaining.
- _setupEditor: ->
- @editor = this._createEditor()
- .on 'hide save', =>
- if @unsaved_drafts.indexOf(@editor) > -1
- @unsaved_drafts.splice(@unsaved_drafts.indexOf(@editor), 1)
- .on 'hide', =>
- @provider.onEditorHide()
- .on 'save', =>
- @provider.onEditorSubmit()
- this
+ # Set the initial state
+ # Asynchronous so that other controllers get time to initialize
+ $scope.$evalAsync "$broadcast('reset')"
- _createEditor: ->
- editor = new Annotator.Editor()
- editor.hide()
- editor.fields = [{
- element: editor.element,
- load: (field, annotation) ->
- $(field).find('textarea').val(annotation.text || '')
- submit: (field, annotation) ->
- annotation.text = $(field).find('textarea').val()
- }]
- @unsaved_drafts.push editor
- editor
+class Auth
+ this.$inject = ['$compile', '$element', '$http', '$scope', 'deform']
+ constructor: ($compile, $element, $http, $scope, deform) ->
+ $scope.submit = ->
+ controls = $element.find('.sheet .active form').formSerialize()
+ $http.post '', controls,
+ headers:
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ withCredentials: true
+ .success (data) =>
+ # Extend the scope with updated model data
+ angular.extend($scope, data.model) if data.model?
- _fillDynamicBucket: ->
- {highlights, offset} = d3.select(@heatmap.element[0]).datum()
- bottom = offset + @heatmap.element.height()
- this.showViewer highlights.reduce (acc, hl) =>
- if hl.offset.top >= offset and hl.offset.top <= bottom
- acc.push hl.data
- acc
- , []
+ # Replace any forms which were re-rendered in this response
+ for oid of data.form
+ target = '#' + oid
- # Public: Initialises an annotation either from an object representation or
- # an annotation created with Annotator#createAnnotation(). It finds the
- # selected range and higlights the selection in the DOM.
- #
- # annotation - An annotation Object to initialise.
- # fireEvents - Will fire the 'annotationCreated' event if true.
- #
- # Examples
- #
- # # Create a brand new annotation from the currently selected text.
- # annotation = annotator.createAnnotation()
- # annotation = annotator.setupAnnotation(annotation)
- # # annotation has now been assigned the currently selected range
- # # and a highlight appended to the DOM.
- #
- # # Add an existing annotation that has been stored elsewere to the DOM.
- # annotation = getStoredAnnotationWithSerializedRanges()
- # annotation = annotator.setupAnnotation(annotation)
- #
- # Returns the initialised annotation.
- setupAnnotation: (annotation) ->
- # Delagate to Annotator implementation after we give it a valid array of
- # ranges. This is needed until Annotator stops assuming ranges need to be
- # added.
- if annotation.thread
- annotation.ranges = []
+ $form = $(data.form[oid])
+ $form.replaceAll(target)
- if not annotation.hash
- @cache[h = ++@hash] = $.extend annotation,
- hash:
- toJSON: => undefined
- valueOf: => h
- stub =
- hash: annotation.hash.valueOf()
- ranges: annotation.ranges
- @provider.setupAnnotation stub
+ link = $compile $form
+ link $scope
- showViewer: (annotations=[], detail=false) =>
- if (@visible and not detail) or @unsaved_drafts.indexOf(@editor) > -1
- if not this._canCloseUnsaved() then return
+ deform.focusFirstInput target
- # Thread the messages using JWZ
- messages = mail.messageThread().thread annotations.map (a) ->
- m = mail.message(null, a.id, a.thread?.split('/') or [])
- m.annotation = a
- m
+ $scope.$on 'reset', =>
+ angular.extend $scope,
+ auth: null
+ username: null
+ password: null
+ email: null
+ code: null
- thread = (context, selector) ->
- context.select('.annotator-listing')
- .selectAll(-> d3.selectAll(this.children).filter(selector)[0])
- .data ((m) -> m.children), ((d) -> d.message.id)
+ $scope.$on 'showAuth', => $scope.auth = 'login'
- context = d3.select(@viewer.element[0]).datum(messages)
- items = thread context, '.annotation'
- excerpts = thread context, '.excerpt'
- if not detail
- # Save the state so the bucket view can be restored when exiting
- # the detail view.
- @detail = false
+class Viewer
+ this.$inject = ['$location', '$routeParams', '$scope', 'annotator']
+ constructor: ($location, $routeParams, $scope, annotator) ->
+ thread = null
- excerpts.remove()
- excerpts.exit().remove()
+ annotator.subscribe 'annotationsLoaded', (annotations) =>
+ thread = mail.messageThread().thread annotations.map (a) =>
+ m = mail.message(null, a.id, a.thread?.split('/') or [])
+ m.annotation = a
+ m
- items.enter().append('li').classed('annotation', true)
- items.exit().remove()
- items
- .html (d) =>
- env = $.extend {}, d.message.annotation,
- text: @renderer.makeHtml d.message.annotation.text
- Handlebars.templates.summary env
- .classed('detail', false)
- .classed('summary', true)
- .classed('paper', true)
- .on 'mouseup', (d, i) =>
- a = d.message.annotation
- query =
- thread: if a.thread then [a.thread, a.id].join('/') else a.id
- @plugins.Store._apiRequest 'search', query, (data) =>
- if data?.rows then this.updateViewer(data.rows || [])
- this.showViewer([a], true)
- .on 'mouseover', =>
- d3.event.stopPropagation()
- item = d3.select(d3.event.currentTarget).datum().message.annotation
- @provider.setActiveHighlights [item.hash.valueOf()]
- .on 'mouseout', =>
- d3.event.stopPropagation()
- item = d3.select(d3.event.currentTarget).datum().message.annotation
- @provider.setActiveHighlights @heatmap.buckets[@bucket]?.map (a) =>
- a.hash.valueOf()
- else
- # Mark that the detail view is now shown, so that exiting returns to the
- # bucket view rather than the document.
- @detail = true
+ # TODO: deal with empty parents
+ $scope.$apply (scope) =>
+ scope.annotations = (t.message.annotation for t in thread.children)
- excerpts.enter()
- .insert('li', '.annotation')
- .classed('paper', true)
- .classed('excerpt', true)
- .append('blockquote')
- excerpts.exit().remove()
+ $scope.annotation = null
+ $scope.annotations = []
- excerpts
- .select('blockquote').each (d) ->
- chars = 180
- quote = d.message.annotation.quote.replace(/\u00a0/g, ' ') # replace &nbsp;
- trunc = quote.substring(0, quote.lastIndexOf(' ', 180)) + "...<a href='#' class='more'>more</a>"
- if quote.length >= chars
- d3.select(this)
- .html(trunc)
- .on 'click', ->
- d3.event.stopPropagation()
- d3.event.preventDefault()
- if d3.select(d3.event.target).classed('more')
- d3.select(this).html(quote + "<a href='#' class='less'>less</a>")
- if d3.select(d3.event.target).classed('less')
- d3.select(this).html(trunc)
- else
- d3.select(this).html(quote)
+ $scope.getThread = (id) =>
+ if thread? then (thread.getSpecificChild id) else null
- highlights = []
- excerpts.each (d) =>
- h = d.message.annotation.hash
- if h then highlights.push h.valueOf()
- @provider.setActiveHighlights highlights
+ $scope.showDetail = (annotation) =>
+ $location.search 'detail', annotation.id
- while items.length
- items.enter().append('li').classed('annotation', true)
- items.exit().remove()
- items
- .each (d) -> # XXX: SLOW! Repeats calculation alllll the time
- count = d.flattenChildren()?.length or 0
- replyCount =
- toJSON: => undefined
- valueOf: =>
- "#{count} " + (if count == 1 then 'reply' else 'replies')
- unless count == 0
- d.message.annotation.replyCount = replyCount
- .html (d) =>
- env = $.extend {}, d.message.annotation,
- text: @renderer.makeHtml d.message.annotation.text
- Handlebars.templates.detail env
- .classed('paper', (c) -> not c.parent.message?)
- .classed('detail', true)
- .classed('summary', false)
-
- .sort (d, e) =>
- n = d.message.annotation.created
- m = e.message.annotation.created
- (n < m) - (m < n)
-
- .on 'mouseover', =>
- d3.event.stopPropagation()
- d3.select(d3.event.currentTarget).classed('hover', true)
-
- .on 'mouseout', =>
- d3.event.stopPropagation()
- d3.select(d3.event.currentTarget).classed('hover', false)
-
- .on 'mouseup', =>
- event = d3.event
- target = event.target
- unless target.tagName is 'A' then return
- event.stopPropagation()
-
- animate = (parent) ->
- collapsed = parent.classed('collapsed')
- parent.select('.thread')
- .transition().duration(200)
- .style('overflow', 'hidden')
- .style 'height', ->
- if collapsed
- "0px"
- else
- "#{$(this).children().outerHeight(true)}px"
- .each 'end', ->
- unless collapsed
- d3.select(this)
- .style('height', null)
- .style('overflow', null)
-
- parent = d3.select(event.currentTarget)
- switch d3.event.target.getAttribute('href')
- when '#collapse'
- d3.event.preventDefault()
- collapsed = parent.classed('collapsed')
- animate parent.classed('collapsed', !collapsed)
- when '#reply'
- unless @plugins.Permissions?.user
- this.showAuth true
- break
- d3.event.preventDefault()
- parent = d3.select(event.currentTarget)
- animate parent.classed('collapsed', false)
- reply = this.createAnnotation()
- reply.thread = this.threadId(parent.datum().message.annotation)
-
- editor = this._createEditor()
- editor.load(reply)
- editor.element.removeClass('annotator-outer')
- editor.on 'save', (annotation) =>
- this.publish 'annotationCreated', [annotation]
-
- d3.select(editor.element[0]).select('form')
- .data([reply])
- .html(Handlebars.templates.editor)
- .on 'mouseover', => d3.event.stopPropagation()
-
- item = d3.select(d3.event.currentTarget)
- .select('.annotator-listing')
- .insert('li', '.annotation')
- .classed('annotation', true)
- .classed('writer', true)
-
- editor.element.appendTo(item.node())
- editor.on('hide', => item.remove())
-
- editor.on 'hide save', =>
- @unsaved_drafts.splice(@unsaved_drafts.indexOf(editor), 1)
-
- editor.element.find(":input:first").focus()
-
- context = items.select '.thread'
- items = thread context, '.annotation'
-
- @editor.hide()
- @viewer.show()
-
- updateViewer: (annotations) =>
- existing = d3.select(@viewer.element[0]).datum()
- if existing?
- annotations = existing.flattenChildren()?.map((c) -> c.annotation)
- .concat(annotations)
- this.showViewer(annotations or [], @detail)
-
- showEditor: (annotation) =>
- if not annotation.user?
- @plugins.Permissions.addFieldsToAnnotation(annotation)
-
- @viewer.hide()
- @editor.load(annotation)
- @editor.element.find('.annotator-controls').remove()
-
- quote = annotation.quote.replace(/\u00a0/g, ' ') # replace &nbsp;
- excerpt = $('<li class="paper excerpt">')
- excerpt.append($("<blockquote>#{quote}</blockquote>"))
-
- item = $('<li class="annotation paper writer">')
- item.append($(Handlebars.templates.editor(annotation)))
-
- @editor.element.find('.annotator-listing').empty()
- .append(excerpt)
- .append(item)
- .find(":input:first").focus()
-
- @unsaved_drafts.push @editor
-
- d3.select(@viewer.element[0]).datum(null)
- this.show()
-
- show: =>
- if @detail
- annotations = d3.select(@viewer.element[0]).datum().children.map (c) =>
- c.message.annotation.hash.valueOf()
- else
- annotations = @heatmap.buckets[@bucket]?.map (a) => a.hash.valueOf()
-
- @visible = true
- @provider.setActiveHighlights annotations
- @provider.showFrame()
- @element.find('#toolbar').addClass('shown')
- .find('.tri').attr('draggable', true)
-
- hide: =>
- @lastWidth = window.innerWidth
- @visible = false
- @provider.setActiveHighlights []
- @provider.hideFrame()
- @element.find('#toolbar').removeClass('shown')
- .find('.tri').attr('draggable', false)
-
- _canCloseUnsaved: ->
- # See if there's an unsaved/uncancelled reply
- can_close = true
- open_editors = 0
- for editor in @unsaved_drafts
- unsaved_text = editor.element.find(':input:first').attr 'value'
- if unsaved_text? and unsaved_text.toString().length > 0
- open_editors += 1
-
- if open_editors > 0
- if open_editors > 1
- ctext = "You have #{open_editors} unsaved replies."
+ $scope.$on '$routeUpdate', =>
+ if $routeParams.detail?
+ thread = thread?.getSpecificChild $routeParams.detail
+ $scope.annotation = thread?.message.annotation
else
- ctext = "You have an unsaved reply."
- ctext = ctext + " Do you really want to close the view?"
- can_close = confirm ctext
-
- if can_close then @unsaved_drafts = []
- can_close
-
- threadId: (annotation) ->
- if annotation?.thread?
- annotation.thread + '/' + annotation.id
- else
- annotation.id
-
- showAuth: (show=true) =>
- $header = @element.find('header')
- visible = !$header.hasClass('collapsed')
- if (visible != show)
- if (show)
- $header.removeClass('collapsed')
- else
- $header.addClass('collapsed')
- $header.one 'webkitTransitionEnd transitionend OTransitionEnd', =>
- $header.find('.nav-tabs > li.active > a').trigger('shown')
-
- submit: =>
- controls = @element.find('.sheet .active form').formSerialize()
- @http.post '', controls,
- headers:
- 'Content-Type': 'application/x-www-form-urlencoded'
- withCredentials: true
- .success (data) =>
- # Extend the scope with updated model data
- angular.extend(@scope, data.model) if data.model?
-
- # Replace any forms which were re-rendered in this response
- for oid of data.form
- target = '#' + oid
-
- $form = $(data.form[oid])
- $form.replaceAll(target)
-
- link = @compile $form
- link @scope
-
- deform.focusFirstInput target
-
- reset: =>
- angular.extend @scope,
- username: null
- password: null
- email: null
- code: null
- personas: []
- persona: null
- token: null
+ $scope.annotation = null
angular.module('h.controllers', [])
- .controller('Hypothesis', Hypothesis)
+ .controller('App', App)
+ .controller('Auth', Auth)
+ .controller('Viewer', Viewer)
69 h/js/directives.coffee
View
@@ -1,16 +1,63 @@
-navTabsDirective = (deform) ->
+annotation = ($filter) ->
link: (scope, iElement, iAttrs, controller) ->
- iElement.find('a')
+ annotation = scope.annotation
+ thread = scope.getThread annotation.id
+ angular.extend scope, annotation
+ angular.extend scope,
+ created: ($filter 'fuzzyTime') annotation.created
+ user: ($filter 'userName') annotation.user
+ text: ($filter 'converter') annotation.text
+ replies: (c.message.annotation for c in (thread?.children or []))
+ replyCount: thread?.flattenChildren()?.length or 0
+ restrict: 'C'
+ scope: true
+annotation.$inject = ['$filter']
- # Focus the first form element when showing a tab pane
- .on 'shown', (e) ->
- target = $(e.target).data('target')
- deform.focusFirstInput(target)
- # Always show the first pane to start
- .first().tab('show')
- restrict: 'C'
-navTabsDirective.$inject = ['deform']
+tabReveal = ($parse) ->
+ compile: (tElement, tAttrs, transclude) ->
+ panes = []
+
+ pre: (scope, iElement, iAttrs, [ngModel, tabbable] = controller) ->
+ # Hijack the tabbable controller's addPane so that the visibility of the
+ # secret ones can be managed. This avoids traversing the DOM to find
+ # the tab panes.
+ addPane = tabbable.addPane
+ tabbable.addPane = (element, attr) =>
+ removePane = addPane.call tabbable, element, attr
+ panes.push
+ element: element
+ attr: attr
+ =>
+ for i in [0..panes.length]
+ if panes[i].element is element
+ panes.splice i, 1
+ break
+ removePane()
+
+ post: (scope, iElement, iAttrs, [ngModel, tabbable] = controller) ->
+ tabs = angular.element(iElement.children()[0]).find('li')
+ hiddenPanes = ($parse iAttrs.tabReveal)()
+ unless angular.isArray hiddenPanes
+ throw (new TypeError 'tabReveal expression must evaluate to an Array')
+
+ update = =>
+ for i in [0..panes.length-1]
+ pane = panes[i]
+ value = pane.attr.value || pane.attr.title
+ if value == ngModel.$modelValue
+ deform.focusFirstInput pane.element
+ pane.element.css 'display', ''
+ angular.element(tabs[i]).css 'display', ''
+ else if value in hiddenPanes
+ pane.element.css 'display', 'none'
+ angular.element(tabs[i]).css 'display', 'none'
+
+ scope.$watch iAttrs.ngModel, => scope.$evalAsync update
+ require: ['ngModel', 'tabbable']
+tabReveal.$inject = ['$parse']
+
angular.module('h.directives', ['ngSanitize', 'deform'])
- .directive('navTabs', navTabsDirective)
+ .directive('annotation', annotation)
+ .directive('tabReveal', tabReveal)
2  h/js/inject/host.coffee
View
@@ -97,7 +97,7 @@ class Annotator.Host extends Annotator
.each ->
if $(this).data('annotation').hash in hashes
$(this).addClass('annotator-hl-active')
- else
+ else if not $(this).hasClass('annotator-hl-temporary')
$(this).removeClass('annotator-hl-active')
getMaxBottom: =>
sel = '*' + (":not(.annotator-#{x})" for x in [
7 h/js/plugin/heatmap.coffee
View
@@ -1,8 +1,7 @@
class Annotator.Plugin.Heatmap extends Annotator.Plugin
-
# prototype constants
- this::BUCKET_THRESHOLD_PAD = 40
- this::BUCKET_SIZE = 50
+ BUCKET_THRESHOLD_PAD: 40
+ BUCKET_SIZE: 50
# heatmap svg skeleton
html: """
@@ -30,7 +29,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
index: []
constructor: (element, options) ->
- super $(@html, options)
+ super $(@html), options
_collate: (a, b) =>
for i in [0..a.length-1]
431 h/js/services.coffee
View
@@ -1 +1,432 @@
+class Hypothesis extends Annotator
+ # Plugin configuration
+ options:
+ Heatmap: {}
+ Permissions:
+ showEditPermissionsCheckbox: false,
+ showViewPermissionsCheckbox: false,
+ userString: (user) -> user.replace(/^acct:(.+)@(.+)$/, '$1 on $2')
+
+ # Internal state
+ bucket: -1 # * The index of the bucket shown in the summary view
+ detail: false # * Whether the viewer shows a summary or detail listing
+ hash: -1 # * cheap UUID :cake:
+ cache: {} # * object cache
+ visible: false # * Whether the sidebar is visible
+ unsaved_drafts: [] # * Unsaved drafts currenty open
+
+ this.$inject = ['$document']
+ constructor: ($document) ->
+ super
+
+ # Load plugins
+ for own name, opts of @options
+ if not @plugins[name] and name of Annotator.Plugin
+ this.addPlugin(name, opts)
+
+ # Establish cross-domain communication to the widget host
+ @provider = new easyXDM.Rpc
+ swf: @options.swf
+ onReady: this._initialize
+ ,
+ local:
+ publish: (event, args, k, fk) =>
+ if event in ['annotationCreated']
+ [h] = args
+ annotation = @cache[h]
+ this.publish event, [annotation]
+ addPlugin: => this.addPlugin arguments...
+ createAnnotation: =>
+ if @plugins.Permissions.user?
+ @cache[h = ++@hash] = this.createAnnotation()
+ h
+ else
+ this.showAuth true
+ this.show()
+ null
+ showEditor: (stub) =>
+ return unless this._canCloseUnsaved()
+ h = stub.hash
+ annotation = $.extend @cache[h], stub,
+ hash:
+ toJSON: => undefined
+ valueOf: => h
+ this.showEditor annotation
+ # This guy does stuff when you "back out" of the interface.
+ # (Currently triggered by a click on the source page.)
+ back: =>
+ # If it's in the detail view, loads the bucket back up.
+ if @detail
+ this.showViewer(@heatmap.buckets[@bucket])
+ this.publish('hostUpdated')
+ # If it's not in the detail view, the assumption is that it's in the
+ # bucket view and hides the whole interface.
+ else
+ this.hide()
+ update: => this.publish 'hostUpdated'
+ remote:
+ publish: {}
+ setupAnnotation: {}
+ onEditorHide: {}
+ onEditorSubmit: {}
+ showFrame: {}
+ hideFrame: {}
+ dragFrame: {}
+ getHighlights: {}
+ setActiveHighlights: {}
+ getMaxBottom: {}
+ scrollTop: {}
+
+ _initialize: =>
+ # Set up interface elements
+ this._setupHeatmap()
+ @heatmap.element.appendTo(document.body)
+
+ @provider.getMaxBottom (max) =>
+ @element.find('#toolbar').css("top", "#{max}px")
+ @element.find('#gutter').css("margin-top", "#{max}px")
+ @heatmap.BUCKET_THRESHOLD_PAD = (
+ max + @heatmap.constructor.prototype.BUCKET_THRESHOLD_PAD
+ )
+
+ this.subscribe 'beforeAnnotationCreated', (annotation) =>
+ annotation.created = annotation.updated = (new Date()).toString()
+ annotation.user = @plugins.Permissions.options.userId(
+ @plugins.Permissions.user)
+
+ this.publish 'hostUpdated'
+
+ _setupWrapper: ->
+ @wrapper = @element.find('#wrapper')
+ .on 'mousewheel', (event, delta) ->
+ # prevent overscroll from scrolling host frame
+ # This is actually a bit tricky. Starting from the event target and
+ # working up the DOM tree, find an element which is scrollable
+ # and has a scrollHeight larger than its clientHeight.
+ # I've obsered that some styles, such as :before content, may increase
+ # scrollHeight of non-scrollable elements, and that there a mysterious
+ # discrepancy of 1px sometimes occurs that invalidates the equation
+ # typically cited for determining when scrolling has reached bottom:
+ # (scrollHeight - scrollTop == clientHeight)
+ $current = $(event.target)
+ while (
+ ($current.css('overflow') in ['visible', '']) or
+ ($current[0].scrollHeight == $current[0].clientHeight)
+ )
+ $current = $current.parent()
+ if not $current[0]? then return event.preventDefault()
+ scrollTop = $current[0].scrollTop
+ scrollEnd = $current[0].scrollHeight - $current[0].clientHeight
+ if delta > 0 and scrollTop == 0
+ event.preventDefault()
+ else if delta < 0 and scrollEnd - scrollTop <= 1
+ event.preventDefault()
+ this
+
+ _setupDocumentEvents: ->
+ @element.find('#toolbar .tri').click =>
+ if @visible
+ this.hide()
+ else
+ if @viewer.isShown() and @bucket == -1
+ this._fillDynamicBucket()
+ this.show()
+
+ el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
+ el.width = el.height = 1
+ @element.append el
+
+ handle = @element.find('#toolbar .tri')[0]
+ handle.addEventListener 'dragstart', (event) =>
+ event.dataTransfer.setData 'text/plain', ''
+ event.dataTransfer.setDragImage(el, 0, 0)
+ @provider.dragFrame event.screenX
+ handle.addEventListener 'dragend', (event) =>
+ @provider.dragFrame event.screenX
+ @element[0].addEventListener 'dragover', (event) =>
+ @provider.dragFrame event.screenX
+ @element[0].addEventListener 'dragleave', (event) =>
+ @provider.dragFrame event.screenX
+
+ this
+
+ _setupDynamicStyle: ->
+ this
+
+ _setupHeatmap: () ->
+ @heatmap = @plugins.Heatmap
+
+ # Update the heatmap when certain events are pubished
+ events = [
+ 'annotationCreated'
+ 'annotationDeleted'
+ 'annotationsLoaded'
+ 'hostUpdated'
+ ]
+
+ for event in events
+ this.subscribe event, =>
+ @provider.getHighlights ({highlights, offset}) =>
+ @heatmap.updateHeatmap
+ highlights: highlights.map (hl) =>
+ hl.data = @cache[hl.data]
+ hl
+ offset: offset
+ if @visible and @viewer.isShown() and @bucket == -1 and not @detail
+ this._fillDynamicBucket()
+
+ @heatmap.element.click =>
+ @bucket = -1
+ this._fillDynamicBucket()
+ this.show()
+
+ @heatmap.subscribe 'updated', =>
+ tabs = d3.select(document.body)
+ .selectAll('div.heatmap-pointer')
+ .data =>
+ buckets = []
+ @heatmap.index.forEach (b, i) =>
+ if @heatmap.buckets[i].length > 0
+ buckets.push i
+ else if @heatmap.isUpper(i) or @heatmap.isLower(i)
+ buckets.push i
+ buckets
+
+ {highlights, offset} = d3.select(@heatmap.element[0]).datum()
+ height = $(window).outerHeight(true)
+ pad = height * .2
+
+ # Enters into tabs var, and generates bucket pointers from them
+ tabs.enter().append('div')
+ .classed('heatmap-pointer', true)
+
+ tabs.exit().remove()
+
+ tabs
+
+ .style 'top', (d) =>
+ "#{(@heatmap.index[d] + @heatmap.index[d+1]) / 2}px"
+
+ .html (d) =>
+ "<div class='label'>#{@heatmap.buckets[d].length}</div><div class='svg'></div>"
+
+ .classed('upper', @heatmap.isUpper)
+ .classed('lower', @heatmap.isLower)
+
+ .style 'display', (d) =>
+ if (@heatmap.buckets[d].length is 0) then 'none' else ''
+
+ # Creates highlights corresponding bucket when mouse is hovered
+ .on 'mousemove', (bucket) =>
+ unless @viewer.isShown() and @detail
+ unless @heatmap.buckets[bucket]?.length then bucket = @bucket
+ @provider.setActiveHighlights @heatmap.buckets[bucket]?.map (a) =>
+ a.hash.valueOf()
+
+ # Gets rid of them after
+ .on 'mouseout', =>
+ unless @viewer.isShown() and @detail
+ @provider.setActiveHighlights @heatmap.buckets[@bucket]?.map (a) =>
+ a.hash.valueOf()
+
+ # Does one of a few things when a tab is clicked depending on type
+ .on 'mouseup', (bucket) =>
+ d3.event.preventDefault()
+
+ # If it's the upper tab, scroll to next bucket above
+ if @heatmap.isUpper bucket
+ threshold = offset + @heatmap.index[0]
+ next = highlights.reduce (next, hl) ->
+ if next < hl.offset.top < threshold then hl.offset.top else next
+ , threshold - height
+ @provider.scrollTop next - pad
+ @bucket = -1
+ this._fillDynamicBucket()
+
+ # If it's the lower tab, scroll to next bucket below
+ else if @heatmap.isLower bucket
+ threshold = offset + @heatmap.index[0] + pad
+ next = highlights.reduce (next, hl) ->
+ if threshold < hl.offset.top < next then hl.offset.top else next
+ , offset + height
+ @provider.scrollTop next - pad
+ @bucket = -1
+ this._fillDynamicBucket()
+
+ # If it's neither of the above, load the bucket into the viewer
+ else
+ annotations = @heatmap.buckets[bucket]
+ @bucket = bucket
+ this.showViewer(annotations)
+ this.show()
+
+ this
+
+ # Creates an instance of Annotator.Viewer and assigns it to the @viewer
+ # property, appends it to the @wrapper and sets up event listeners.
+ #
+ # Returns itself to allow chaining.
+ _setupViewer: ->
+ @viewer = new Annotator.Viewer(readOnly: @options.readOnly)
+ @viewer.hide()
+ .on("edit", this.onEditAnnotation)
+ .on("delete", this.onDeleteAnnotation)
+
+ this
+
+ # Creates an instance of the Annotator.Editor and assigns it to @editor.
+ # Appends this to the @wrapper and sets up event listeners.
+ #
+ # Returns itself for chaining.
+ _setupEditor: ->
+ @editor = this._createEditor()
+ .on 'hide save', =>
+ if @unsaved_drafts.indexOf(@editor) > -1
+ @unsaved_drafts.splice(@unsaved_drafts.indexOf(@editor), 1)
+ .on 'hide', =>
+ @provider.onEditorHide()
+ .on 'save', =>
+ @provider.onEditorSubmit()
+ this
+
+ _createEditor: ->
+ editor = new Annotator.Editor()
+ editor.hide()
+ editor.fields = [{
+ element: editor.element,
+ load: (field, annotation) ->
+ $(field).find('textarea').val(annotation.text || '')
+ submit: (field, annotation) ->
+ annotation.text = $(field).find('textarea').val()
+ }]
+
+ @unsaved_drafts.push editor
+ editor
+
+ _fillDynamicBucket: ->
+ {highlights, offset} = d3.select(@heatmap.element[0]).datum()
+ bottom = offset + @heatmap.element.height()
+ this.showViewer highlights.reduce (acc, hl) =>
+ if hl.offset.top >= offset and hl.offset.top <= bottom
+ acc.push hl.data
+ acc
+ , []
+
+ # Public: Initialises an annotation either from an object representation or
+ # an annotation created with Annotator#createAnnotation(). It finds the
+ # selected range and higlights the selection in the DOM.
+ #
+ # annotation - An annotation Object to initialise.
+ # fireEvents - Will fire the 'annotationCreated' event if true.
+ #
+ # Examples
+ #
+ # # Create a brand new annotation from the currently selected text.
+ # annotation = annotator.createAnnotation()
+ # annotation = annotator.setupAnnotation(annotation)
+ # # annotation has now been assigned the currently selected range
+ # # and a highlight appended to the DOM.
+ #
+ # # Add an existing annotation that has been stored elsewere to the DOM.
+ # annotation = getStoredAnnotationWithSerializedRanges()
+ # annotation = annotator.setupAnnotation(annotation)
+ #
+ # Returns the initialised annotation.
+ setupAnnotation: (annotation) ->
+ # Delagate to Annotator implementation after we give it a valid array of
+ # ranges. This is needed until Annotator stops assuming ranges need to be
+ # added.
+ if annotation.thread
+ annotation.ranges = []
+
+ if not annotation.hash
+ @cache[h = ++@hash] = $.extend annotation,
+ hash:
+ toJSON: => undefined
+ valueOf: => h
+ stub =
+ hash: annotation.hash.valueOf()
+ ranges: annotation.ranges
+ @provider.setupAnnotation stub
+
+ showViewer: (annotations=[], detail=false) =>
+ if (@visible and not detail) or @unsaved_drafts.indexOf(@editor) > -1
+ if not this._canCloseUnsaved() then return
+
+ # Not implemented
+
+ showEditor: (annotation) =>
+ if not annotation.user?
+ @plugins.Permissions.addFieldsToAnnotation(annotation)
+
+ @viewer.hide()
+ @editor.load(annotation)
+ @editor.element.find('.annotator-controls').remove()
+
+ quote = annotation.quote.replace(/\u00a0/g, ' ') # replace &nbsp;
+ excerpt = $('<li class="paper excerpt">')
+ excerpt.append($("<blockquote>#{quote}</blockquote>"))
+
+ item = $('<li class="annotation paper writer">')
+ item.append($(Handlebars.templates.editor(annotation)))
+
+ @editor.element.find('.annotator-listing').empty()
+ .append(excerpt)
+ .append(item)
+ .find(":input:first").focus()
+
+ @unsaved_drafts.push @editor
+
+ d3.select(@viewer.element[0]).datum(null)
+ this.show()
+
+ show: =>
+ if @detail
+ annotations = d3.select(@viewer.element[0]).datum().children.map (c) =>
+ c.message.annotation.hash.valueOf()
+ else
+ annotations = @heatmap.buckets[@bucket]?.map (a) => a.hash.valueOf()
+
+ @visible = true
+ @provider.setActiveHighlights annotations
+ @provider.showFrame()
+ @element.find('#toolbar').addClass('shown')
+ .find('.tri').attr('draggable', true)
+
+ hide: =>
+ @lastWidth = window.innerWidth
+ @visible = false
+ @provider.setActiveHighlights []
+ @provider.hideFrame()
+ @element.find('#toolbar').removeClass('shown')
+ .find('.tri').attr('draggable', false)
+
+ _canCloseUnsaved: ->
+ # See if there's an unsaved/uncancelled reply
+ can_close = true
+ open_editors = 0
+ for editor in @unsaved_drafts
+ unsaved_text = editor.element.find(':input:first').attr 'value'
+ if unsaved_text? and unsaved_text.toString().length > 0
+ open_editors += 1
+
+ if open_editors > 0
+ if open_editors > 1
+ ctext = "You have #{open_editors} unsaved replies."
+ else
+ ctext = "You have an unsaved reply."
+ ctext = ctext + " Do you really want to close the view?"
+ can_close = confirm ctext
+
+ if can_close then @unsaved_drafts = []
+ can_close
+
+ threadId: (annotation) ->
+ if annotation?.thread?
+ annotation.thread + '/' + annotation.id
+ else
+ annotation.id
+
+
angular.module('h.services', [])
+ .service('annotator', Hypothesis)
9 h/lib/angular-bootstrap.min.js
View
@@ -0,0 +1,9 @@
+/*
+ AngularJS v1.0.3
+ (c) 2010-2012 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(n,j){'use strict';j.module("bootstrap",[]).directive({dropdownToggle:["$document","$location","$window",function(h,e){var d=null,a;return{restrict:"C",link:function(g,b){g.$watch(function(){return e.path()},function(){a&&a()});b.parent().bind("click",function(){a&&a()});b.bind("click",function(i){i.preventDefault();i.stopPropagation();i=!1;d&&(i=d===b,a());i||(b.parent().addClass("open"),d=b,a=function(c){c&&c.preventDefault();c&&c.stopPropagation();h.unbind("click",a);b.parent().removeClass("open");
+d=a=null},h.bind("click",a))})}}}],tabbable:function(){return{restrict:"C",compile:function(h){var e=j.element('<ul class="nav nav-tabs"></ul>'),d=j.element('<div class="tab-content"></div>');d.append(h.contents());h.append(e).append(d)},controller:["$scope","$element",function(h,e){var d=e.contents().eq(0),a=e.controller("ngModel")||{},g=[],b;a.$render=function(){var a=this.$viewValue;if(b?b.value!=a:a)if(b&&(b.paneElement.removeClass("active"),b.tabElement.removeClass("active"),b=null),a){for(var c=
+0,d=g.length;c<d;c++)if(a==g[c].value){b=g[c];break}b&&(b.paneElement.addClass("active"),b.tabElement.addClass("active"))}};this.addPane=function(e,c){function l(){f.title=c.title;f.value=c.value||c.title;if(!a.$setViewValue&&(!a.$viewValue||f==b))a.$viewValue=f.value;a.$render()}var k=j.element("<li><a href></a></li>"),m=k.find("a"),f={paneElement:e,paneAttrs:c,tabElement:k};g.push(f);c.$observe("value",l)();c.$observe("title",function(){l();m.text(f.title)})();d.append(k);k.bind("click",function(b){b.preventDefault();
+b.stopPropagation();a.$setViewValue?h.$apply(function(){a.$setViewValue(f.value);a.$render()}):(a.$viewValue=f.value,a.$render())});return function(){f.tabElement.remove();for(var a=0,b=g.length;a<b;a++)f==g[a]&&g.splice(a,1)}}}]}},tabPane:function(){return{require:"^tabbable",restrict:"C",link:function(h,e,d,a){e.bind("$remove",a.addPane(e,d))}}}})})(window,window.angular);
18 h/templates/annotation.html
View
@@ -0,0 +1,18 @@
+<div class="annotation">
+ <div class="time" data-ng-bind="created" />
+ <div class="annotator-controls">
+ <a class="write">Reply</a>
+ </div>
+ <div class="user" data-ng-bind="user" />
+ <div class="body" data-ng-bind-html="text" />
+ <div class="bottombar">
+ <span class="replycount" data-ng-bind="replyCount" />
+ </div>
+ <div class="thread">
+ <a class="threadexp" />
+ <ul class="annotator-listing">
+ <li data-ng-include="'annotation.html'"
+ data-ng-repeat="annotation in replies" />
+ </ul>
+ </div>
+</div>
94 h/templates/app.pt
View
@@ -1,19 +1,17 @@
<!DOCTYPE html>
<html lang="en" metal:use-macro="main_template">
- <body data-ng-app="h"
- data-ng-controller="Hypothesis"
- metal:fill-slot="body">
+ <body data-ng-app="h" data-ng-controller="App" metal:fill-slot="body">
<div id="toolbar" class="form-inline">
<div class="tri"></div>
<div class="tinyman">
+ <a data-ng-hide="persona"
+ data-ng-click="$broadcast('showAuth')">Sign in</a>
<form id="persona"
action
method="POST"
enctype="multipart/form-data"
accept-charset="utf-8">
<input name="__formid__" type="hidden" value="persona" />
- <a data-ng-hide="persona"
- data-ng-click="showAuth()">Sign in</a>
<div class="dropdown"
data-ng-show="persona">
<a role="button"
@@ -36,50 +34,52 @@
</div>
</div>
<div id="gutter">
- <header class="sheet" data-ng-model="auth" data-ng-submit="submit()">
- <a class="close" data-ng-click="showAuth(false)"></a>
- <ul class="nav nav-tabs">
- <li><a data-target="#login-tab"
- data-toggle="tab">Log in</a></li>
- <li><a data-target="#register-tab"
- data-toggle="tab">Sign up</a></li>
- <li><a data-target="#forgot-tab"
- data-toggle="tab">Forgot your password?</a></li>
- <li><a data-target="#activate-tab"
- data-toggle="tab">Set your password</a></li>
- </ul>
- <div class="tab-content form-vertical">
- <div tal:repeat="name ['login', 'register']"
- tal:attributes="id '%s-tab' % name"
- class="tab-pane">
- ${structure: layout.forms[name].render()}
- <footer>
- <ul>
- <li>
- <a onclick="$('.nav a[data-target]')
- .filter('[data-target=#forgot-tab]')
- .tab('show')"
- >Password help?</a>
- </li>
- <li>
- <a onclick="$('.nav a[data-target]')
- .filter('[data-target=#activate-tab]')
- .tab('show')"
- >Have you already reserved a username with us?</a>
- </li>
- </ul>
- </footer>
- </div>
- <div id="forgot-tab" class="tab-pane">
- ${structure: layout.forms['forgot'].render()}
- </div>
- <div id="activate-tab" class="tab-pane">
- ${structure: layout.forms['activate'].render()}
- </div>
+ <header data-ng-class="auth || 'collapsed'"
+ data-ng-controller="Auth"
+ data-ng-model="auth"
+ data-ng-submit="submit()"
+ data-tab-reveal="['forgot','activate']"
+ class="form-vertical sheet tabbable">
+ <a data-ng-click="auth = null" class="close"></a>
+ <div data-title="Sign in"
+ data-value="login"
+ class="tab-pane">
+ ${structure: layout.forms['login'].render()}
+ </div>
+ <div data-title="Create an account"
+ data-value="register"
+ class="tab-pane">
+ ${structure: layout.forms['register'].render()}
</div>
+ <div data-title="Forgot your password?"
+ data-value="forgot"
+ class="tab-pane">
+ ${structure: layout.forms['forgot'].render()}
+ </div>
+ <div data-title="Set your password"
+ data-value="activate"
+ class="tab-pane">
+ ${structure: layout.forms['activate'].render()}
+ </div>
+ <footer>
+ <ul>
+ <li>
+ <a data-ng-click="auth = 'forgot'">Password help?</a>
+ </li>
+ <li>
+ <a data-ng-click="auth = 'activate'">Have you already reserved a
+ username with us?</a>
+ </li>
+ </ul>
+ </footer>
</header>
- <div id="wrapper">
- </div>
+ <div id="wrapper" data-ng-view=""></div>
</div>
+ <script type="text/ng-template" id="annotation.html">
+ <metal:main use-macro="load: annotation.html" />
+ </script>
+ <script type="text/ng-template" id="viewer.html">
+ <metal:main use-macro="load: viewer.html" />
+ </script>
</body>
</html>
5 h/templates/deform/form.pt
View
@@ -1,11 +1,12 @@
<form
id="${field.formid}"
- class="deform ${field.css_class}"
action="${field.action}"
method="${field.method}"
enctype="multipart/form-data"
accept-charset="utf-8"
- i18n:domain="deform">
+ i18n:domain="deform"
+ tal:attributes="class field.css_class">
+
<fieldset>
27 h/templates/viewer.html
View
@@ -0,0 +1,27 @@
+<ul class="sliding-panels">
+ <li>
+ <!-- Summary -->
+ <div class="annotator-outer annotator-viewer">
+ <ul class="annotator-listing">
+ <li data-ng-click="showDetail(annotation)"
+ data-ng-include="'annotation.html'"
+ data-ng-repeat="annotation in annotations"
+ class="paper summary">
+ </li>
+ </ul>
+ </div>
+ </li>
+
+ <li data-ng-class="!annotation && 'collapsed' || ''">
+ <!-- Details -->
+ <div class="annotator-outer annotator-viewer"
+ data-ng-switch="annotation != null">
+ <div data-ng-show="annotation.quote" class="paper excerpt">
+ <blockquote data-ng-bind="annotation.quote" />
+ </div>
+ <div data-ng-include="'annotation.html'"
+ data-ng-switch-when="true"
+ class="paper detail" />
+ </div>
+ </li>
+</ul>

No commit comments for this range

Something went wrong with that request. Please try again.