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

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

Support for annotating multiple frames
parents 3e324c61 9008013c
......@@ -206,4 +206,15 @@ $input-border-radius: 2px;
}
}
}
}
\ No newline at end of file
}
//NOISE///////////
//Provides the noise background
.noise {
background: url("../images/noise_1.png");
}
.dark-noise {
background: url("../images/dark_noise_1.png");
}
......@@ -510,34 +510,6 @@ blockquote {
}
}
//Controlbar Icons
.alwaysonhighlights-icon {
@include fonticon("\e01b", left);
color: rgb(211, 211, 211);
font-size: 18px;
bottom: 1px;
left: 3px;
position: absolute;
}
.highlighter-icon {
@include fonticon("\e01a", left);
color: rgb(211, 211, 211);
font-size: 18px;
bottom: 1px;
left: 3px;
position: absolute;
}
.commenter-icon {
@include fonticon("\e01e", left);
color: rgb(211, 211, 211);
font-size: 18px;
bottom: 1px;
left: 3px;
position: absolute;
}
//VISIBILITY
.visibility {
.dropdown-toggle {
......@@ -619,17 +591,6 @@ blockquote {
}
//NOISE///////////
//Provides the noise background
.noise {
background: url("../images/noise_1.png");
}
.dark-noise {
background: url("../images/dark_noise_1.png");
}
//KNOCKOUT///////////
//Provides a knockout background
.knockout {
......@@ -653,13 +614,14 @@ blockquote {
//TOOL BAR////////////////////////////////
.topbar {
@include smallshadow;
@include smallshadow(0);
background: $white;
border: solid 1px $grayLighter;
border-style: solid none;
height: 2em;
position: fixed;
left: -1px;
right: -1px;
left: 0;
right: 0;
top: .5em;
z-index: 5;
......@@ -1277,19 +1239,11 @@ h3.stream {
}
}
.visual-container {
margin-left: 3em;
}
.magnify-glass {
width: 2.25em;
min-height: 22px;
}
.magnify-glass .VS-icon {
margin-left: 3.5em;
}
.magnify-glass .VS-icon-search:hover {
width:12px;
height:12px;
......
@import 'base';
@import 'compass/css3/user-interface';
@import 'compass/layout/stretching';
@import 'compass/reset/utilities';
$baseFontSize: 14px;
//ADDER////////////////////////////////
.annotator-adder {
......@@ -64,6 +66,127 @@
}
//HEATMAP STUFF////////////////////////////////
.annotator-heatmap {
cursor: ew-resize;
position: absolute;
overflow: hidden;
height: 100%;
width: $heatmap-width + 17px;
left: -($heatmap-width + 17px);
svg {
@include stretch-y;
background: hsla(0, 0, 0, .1);
border-left: solid 1px $grayLighter;
height: 100%;
left: 17px;
width: $heatmap-width;
}
}
.heatmap-pointer {
@include transition-property(left);
@include transition-duration(.2s);
margin-top: -1em; // TODO: Less janking v-align
// color: rgb(238, 238, 238);
color: #666;
left: 0;
position: absolute;
height: 20.1px;
width: 40.1px;
vertical-align: middle;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-align: center;
cursor: pointer;
.svg {
width: 100%;
height: 100%;
background-image: url("../images/side_tab.svg");
background-position: center center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.label {
font-weight: bold;
font-family: $sansFontFamily;
font-size: 13.1px;
line-height: 17.5px;
left: 5px;
right: 2px;
position: absolute;
bottom: 1px;
}
&:hover {
left: 2px;
}
&.lower, &.upper {
@include single-transition(margin-top, .2s);
left: 7px;
width: 33.1px;
height: 26.1px;
.label {
left: 0;
right: 0;
}
&:hover {
left: 7px;
}
}
&.upper {
.svg {
background-image: url("../images/up_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
&.lower {
.svg {
background-image: url("../images/down_tab.svg");
}
.label {
top: 1px;
}
&:hover {
margin-top: -.8em;
}
}
&.flip {
@include rotateY(180deg);
.label {
@include rotateY(180deg);
}
}
&.commenter {
@include single-transition(margin-top, .2s);
left: 3px;
width: 37px;
.svg {
background-image: url("../images/comment_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
}
//HIGHLIGHTS////////////////////////////////
.annotator-highlights-always-on .annotator-hl,
......@@ -96,9 +219,14 @@
display: none;
}
//IFRAME////////////////////////////////
// Sidebar
.annotator-frame {
@include reset-box-model;
@include user-select(none);
@extend .noise;
font-size: $baseFontSize / 16px * 1em;
line-height: $baseLineHeight / 16px * 1em;
height: 100%;
position: fixed;
top: 0;
......@@ -107,8 +235,21 @@
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
&.annotator-collapsed {
margin-left: -$heatmap-width - 17px;
margin-left: 0;
}
* {
font-size: 100%;
}
& > iframe {
@include reset-box-model;
height: 100%;
width: 100%;
z-index: 3;
position: relative;
}
}
.annotator-no-transition {
......@@ -116,6 +257,118 @@
}
//CONTROLBAR STUFF////////////////////////////////
.annotator-toolbar {
@include single-transition(height, .25s, ease, .25s);
font-size: $baseFontSize;
line-height: $baseLineHeight;
position: absolute;
overflow: hidden;
height: 200px;
left: -($heatmap-width + 17px - 7px);
top: .5em;
width: 2.5em;
z-index: 2;
a, a:hover, a:active, a:visited {
color: $grayLighter;
text-decoration: none;
&.tri-icon {
color: rgba(200, 200, 200, .3);
text-shadow:
1px .8px 1.5px $white,
0 0 0 #000;
&:hover {
color: rgba(235, 235, 230, .1);
}
}
}
ul, li {
@include box-sizing(border-box);
@include reset-box-model;
@include reset-list-style;
}
}
.annotator-toolbar.annotator-hide {
height: 2.2em;
&:hover {
height: 200px;
}
}
.annotator-toolbar li {
@include border-radius(4px);
@include smallshadow(0);
background: $white;
border: solid 1px $grayLighter;
position: relative;
left: 2px;
height: 2em;
width: 2em;
&:first-child {
@include border-radius(4px 0 0 4px);
border-right-style: none;
left: 0;
width: 2.3em;
}
& + li {
margin-top: 5px;
}
a {
font-size: $baseFontSize * 1.3;
line-height: $baseLineHeight * 1.3;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
position: absolute;
left: .2em;
}
a.pushed {
color: $hypothered;
}
}
// Toolbar Icons
.alwaysonhighlights-icon {
@include fonticon("\e01b", left);
}
.highlighter-icon {
@include fonticon("\e01a", left);
}
.commenter-icon {
@include fonticon("\e01e", left);
}
.tri-icon {
@include fonticon("\e00b", left);
}
// Toolbar notification counter
.annotator-notification-counter {
font-family: $sansFontFamily;
pointer-events: none;
position: absolute;
margin-left: .7em;
margin-top: .25em;
z-index: 2;
font-size: $baseFontSize * .86;
}
/*
Mobile layout
240-479 px
......@@ -124,8 +377,8 @@
@media screen and (min-width: 15em) {
.annotator-frame {
width: 99%;
margin-left: -99%;
width: 90%;
margin-left: -90%;
}
}
......@@ -151,8 +404,8 @@
@media screen and (min-width: 37.5em) {
.annotator-frame {
@include single-transition(margin-left, .4s);
width: 428px + $heatmap-width + 17px;
margin-left: -428px - $heatmap-width - 17px;
width: 428px;// + $heatmap-width + 17px;
margin-left: -428px;// - $heatmap-width - 17px;
}
}
......
@import 'compass';
@import 'compass/css3/transform';
@import 'compass/css3/user-interface';
@import 'compass/layout/stretching';
@import 'compass/reset/utilities';
......@@ -36,7 +35,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
.sliding-panels > li {
@extend .content;
@extend .noise;
@include smallshadow(-2px);
@include stretch-y;
@include transition(transform .4s);
......@@ -64,76 +62,27 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
//SIDEBAR LAYOUT////////////////////////////////
#wrapper {
@extend .noise;
height: 100%;
margin-left: $heatmap-width + 17px;
position: relative;
}
.topbar {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 7px;
top: .5em;
.inner {
margin: 0 .5em 0 3.1em;
}
.tri {
@include fonticon("\e00b", left);
color: rgba( 200, 200, 200, .3);
text-shadow:
1px .8px 1.5px $white,
0 0 0 #000;
position: absolute;
z-index: 1;
top: .3em;
left: .3em;
line-height: $baseLineHeight * .9;
font-size: $baseLineHeight * .9;
cursor: w-resize;
&:before {
vertical-align: 0;
}
&:hover {
color: rgba( 235, 235, 230, .1);
}
}
.notification-counter {
font-family: $sansFontFamily;
position: absolute;
margin-left: .75em;
float: left;
margin-top: 0.37em;
z-index: 2;
font-size: 12px;
cursor: w-resize;
}
&.shown {
.tri {
@include fonticon("\e010", left);
cursor: ew-resize;
}
.notification-counter {
cursor: ew-resize;
}
margin: 0 .5em 0 .5em;
}
}
.bottombar {
position: fixed;
bottom: 0px;
left: 39px; //17 + $heatmap_width
left: 0;
height: 3.7em;
width: 100%;
.notif-list {
margin-right: 3.85em;
margin-right: 1em;
box-shadow: 1px 1px 9px #b5d4ff;
}
......@@ -151,169 +100,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
}
}
//CONTROLBAR STUFF////////////////////////////////
.controlbar {
position: absolute;
overflow: hidden;
width: 40px;
height: 100px;
z-index: 1;
left: 1px;
top: 100%;
-moz-transition:height .25s, -moz-transform .25s;
-webkit-transition:height .25s, -webkit-transform .25s;
-o-transition:height .25s, -o-transform .25s;
transition:height .25s, transform .25s;
}
.hiddencontrolbar {
@extend .controlbar;
height: 0px;
}
.controlbarbutton {
position: relative;
left: 1px;
width: 25px;
height: 25px;
padding: 5px;
margin-top: 5px;
box-shadow: 1px 1px 1px 1px;
color: gray;
background-color: white;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
&:active {
top:1px;
box-shadow: 1px 1px 1px 0px;
}
a.pushed {
color: $hypothered;
}
}
//HEATMAP STUFF////////////////////////////////
.annotator-heatmap {
@include stretch-y;
height: 100%;
svg {
@include stretch-y;
background: hsla(0, 0, 0, .1);
border-left: solid 1px $grayLighter;
height: 100%;
left: 17px;
width: $heatmap-width;
}
}
.heatmap-pointer {
@include user-select(none);
@include transition-property(left);
@include transition-duration(.2s);
margin-top: -1em; // TODO: Less janking v-align
// color: rgb(238, 238, 238);
color: #666;
left: 0;
position: absolute;
height: 20.1px;
width: 40.1px;
vertical-align: middle;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-align: center;
cursor: pointer;
.svg {
width: 100%;
height: 100%;
background-image: url("../images/side_tab.svg");
background-position: center center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.label {
font-weight: bold;
font-family: $sansFontFamily;
font-size: 13.1px;
left: 5px;
right: 2px;
position: absolute;
bottom: 1px;
}
&:hover {
left: 2px;
}
&.lower, &.upper {
@include single-transition(margin-top, .2s);
left: 7px;
width: 33.1px;
height: 26.1px;
.label {
left: 0;
right: 0;
}
&:hover {
left: 7px;
}
}
&.upper {
.svg {
background-image: url("../images/up_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
&.lower {
.svg {
background-image: url("../images/down_tab.svg");
}
.label {
top: 1px;
}
&:hover {
margin-top: -.8em;
}
}
&.flip {
@include rotateY(180deg);
.label {
@include rotateY(180deg);
}
}
&.commenter {
@include single-transition(margin-top, .2s);
left: 3px;
width: 37px;
.svg {
background-image: url("../images/comment_tab.svg");
}
.label {
bottom: 1px;
}
&:hover {
margin-top: -1.2em;
}
}
}
//SHEET////////////////////////////////
//This comes down from under the toolbar
......@@ -323,10 +109,7 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
max-height: 30em;
overflow: hidden;
position: absolute;
top: 100%;
width: 100%;
left: 4px;
left: 0;
right: 0;
top: 2px;
......@@ -336,10 +119,6 @@ svg { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); }
top: .25em;
}
.nav-tabs {
padding-left: 1.3em;
}
&.collapsed {
max-height: 0;
}
......
This diff is collapsed.
$ = Annotator.$
class Annotator.Guest extends Annotator
# Events to be bound on Annotator#element.
events:
".annotator-adder button click": "onAdderClick"
".annotator-adder button mousedown": "onAdderMousedown"
".annotator-hl mousedown": "onHighlightMousedown"
".annotator-hl click": "onHighlightClick"
# Plugin configuration
options:
Document: {}
# Internal state
tool: 'comment'
visibleHighlights: false
constructor: (element, options) ->
Gettext.prototype.parse_locale_data annotator_locale_data
super
@frame = $('<div></div>')
.appendTo(@wrapper)
.addClass('annotator-frame annotator-outer annotator-collapsed')
delete @options.app
this.addPlugin 'Bridge',
formatter: (annotation) =>
formatted = {}
if annotation.document?
formatted['uri'] = @plugins.Document.uri()
for k, v of annotation when k isnt 'highlights'
formatted[k] = v
formatted
onConnect: (source, origin, scope) =>
@panel = this._setupXDM
window: source
origin: origin
scope: "#{scope}:provider"
onReady: =>
console.log "Guest functions are ready for #{origin}"
# Load plugins
for own name, opts of @options
if not @plugins[name]
this.addPlugin(name, opts)
# Scan the document text with the DOM Text libraries
this.scanDocument "Annotator initialized"
_setupXDM: (options) ->
channel = Channel.build options
channel
.bind('onEditorHide', this.onEditorHide)
.bind('onEditorSubmit', this.onEditorSubmit)
.bind('setActiveHighlights', (ctx, tags=[]) =>
@wrapper.find('.annotator-hl')
.each ->
if $(this).data('annotation').$$tag in tags
$(this).addClass('annotator-hl-active')
else if not $(this).hasClass('annotator-hl-temporary')
$(this).removeClass('annotator-hl-active')
)
.bind('adderClick', =>
@onAdderClick @event
)
.bind('getDocumentInfo', =>
return {
uri: @plugins.Document.uri()
metadata: @plugins.Document.metadata
}
)
.bind('setTool', (ctx, name) => this.setTool name)
.bind('setVisibleHighlights', (ctx, state) =>
this.setVisibleHighlights state
)
scanDocument: (reason = "something happened") =>
try
console.log "Analyzing host frame, because " + reason + "..."
r = @domMatcher.scan()
scanTime = r.time
console.log "Traversal+scan took " + scanTime + " ms."
catch e
console.log e.message
console.log e.stack
_setupWrapper: ->
@wrapper = @element
.on 'click', =>
unless @ignoreMouseup or @noBack
setTimeout =>
unless @selectedRanges?.length
@panel?.notify method: 'back'
this._setupMatching()
@domMatcher.setRootNode @wrapper[0]
this
# These methods aren't used in the iframe-hosted configuration of Annotator.
_setupViewer: -> this
_setupEditor: -> this
showViewer: (annotations) =>
@panel?.notify method: "showViewer", params: (a.id for a in annotations)
updateViewer: (annotations) =>
@panel?.notify method: "updateViewer", params: (a.id for a in annotations)
showEditor: (annotation) => @plugins.Bridge.showEditor annotation
checkForStartSelection: (event) =>
# Override to prevent Annotator choking when this ties to access the
# viewer but preserve the manipulation of the attribute `mouseIsDown` which
# is needed for preventing the panel from closing while annotating.
unless event and this.isAnnotator(event.target)
@mouseIsDown = true
confirmSelection: ->
return true unless @selectedRanges.length is 1
target = this.getTargetFromRange @selectedRanges[0]
selector = this.findSelector target.selector, "TextQuoteSelector"
length = selector.exact.length
if length > 2 then return true
return confirm "You have selected a very short piece of text: only " + length + " chars. Are you sure you want to highlight this?"
onSuccessfulSelection: (event) ->
if @tool is 'highlight'
# Do we really want to make this selection?
return unless this.confirmSelection()
# Create the annotation right away
# Don't use the default method to create an annotation,
# because we don't want to publish the beforeAnnotationCreated event
# just yet.
#
# annotation = this.createAnnotation()
#
# Create an empty annotation manually instead
annotation = {inject: true}
annotation = this.setupAnnotation annotation
$(annotation.highlights).addClass 'annotator-hl'
# Notify listeners
this.publish 'beforeAnnotationCreated', annotation
this.publish 'annotationCreated', annotation
else
super event
# When clicking on a highlight in highlighting mode,
# set @noBack to true to prevent the sidebar from closing
onHighlightMousedown: (event) =>
if (@tool is 'highlight') or @visibleHighlights then @noBack = true
# When clicking on a highlight in highlighting mode,
# tell the sidebar to bring up the viewer for the relevant annotations
onHighlightClick: (event) =>
return unless (@tool is 'highlight') or @visibleHighlights and @noBack
# Collect relevant annotations
annotations = $(event.target)
.parents('.annotator-hl')
.addBack()
.map -> return $(this).data("annotation")
# Tell sidebar to show the viewer for these annotations
this.showViewer annotations
# We have already prevented closing the sidebar, now reset this flag
@noBack = false
setTool: (name) ->
@tool = name
@panel?.notify
method: 'setTool'
params: name
switch name
when 'comment'
this.setVisibleHighlights this.visibleHighlights, true
when 'highlight'
this.setVisibleHighlights true, true
setVisibleHighlights: (state=true, temporary=false) ->
unless temporary
@visibleHighlights = state
@panel?.notify
method: 'setVisibleHighlights'
params: state
markerClass = 'annotator-highlights-always-on'
if state or (@tool is 'highlight')
@element.addClass markerClass
else
@element.removeClass markerClass
addComment: ->
sel = @selectedRanges # Save the selection
# Nuke the selection, since we won't be using that.
# We will attach this to the end of the document.
# Our override for setupAnnotation will add that highlight.
@selectedRanges = []
this.onAdderClick() # Open editor (with 0 targets)
@selectedRanges = sel # restore the selection
createFakeCommentRange: ->
posSelector =
type: "TextPositionSelector"
start: @domMapper.corpus.length - 1
end: @domMapper.corpus.length
anchor = this.findAnchorFromPositionSelector selector: [posSelector]
anchor.range
# Override for setupAnnotation
setupAnnotation: (annotation) ->
# Set up annotation as usual
annotation = super(annotation)
# Does it have proper highlights?
unless annotation.highlights?.length or annotation.references?.length or annotation.target?.length
# No highlights and no references means that this is a comment,
# or re-attachment has failed, but we'll skip orphaned annotations.
# Get a fake range at the end of the document, and highlight it
range = this.createFakeCommentRange()
hl = this.highlightRange range
# Register this highlight for the annotation, and vica versa
$.merge annotation.highlights, hl
$(hl).data('annotation', annotation)
annotation
# Open the sidebar
showFrame: ->
@panel?.notify method: 'open'
# Close the sidebar
hideFrame: ->
@panel?.notify method: 'back'
addToken: (token) =>
@api.notify
method: 'addToken'
params: token
onAdderClick: (event) =>
"""
Differs from upstream in a few ways:
- Don't fire annotationCreated events: that's the job of the sidebar
- Save the event for retriggering if login interrupts the flow
"""
event?.preventDefault()
# Save the event for restarting edit
@event = event
# Hide the adder
@adder.hide()
position = @adder.position()
# Show a temporary highlight so the user can see what they selected
# Also extract the quotation and serialize the ranges
annotation = this.setupAnnotation(this.createAnnotation())
$(annotation.highlights).addClass('annotator-hl-temporary')
# Subscribe to the editor events
# Make the highlights permanent if the annotation is saved
save = =>
do cleanup
$(annotation.highlights).removeClass('annotator-hl-temporary')
# Remove the highlights if the edit is cancelled
cancel = =>
do cleanup
this.deleteAnnotation(annotation)
# Don't leak handlers at the end
cleanup = =>
this.unsubscribe('annotationEditorHidden', cancel)
this.unsubscribe('annotationEditorSubmit', save)
this.subscribe('annotationEditorHidden', cancel)
this.subscribe('annotationEditorSubmit', save)
# Display the editor.
this.showEditor(annotation, position)
This diff is collapsed.
This diff is collapsed.
$ = Annotator.$
class Annotator.Plugin.Heatmap extends Annotator.Plugin
# prototype constants
BUCKET_THRESHOLD_PAD: 40
BUCKET_THRESHOLD_PAD: 25
BUCKET_SIZE: 50
BOTTOM_CORRECTION: 14
......@@ -38,10 +40,44 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
# index for fast hit detection in the buckets
index: []
# whether to update the viewer as the window is scrolled
dynamicBucket: true
constructor: (element, options) ->
super $(@html), options
this._rebaseUrls()
@element.appendTo element
# this._rebaseUrls() -- not clear this is a great idea
if @options.container?
$(@options.container).append @element
else
$(element).append @element
pluginInit: ->
return unless d3?
events = [
'annotationCreated', 'annotationUpdated', 'annotationDeleted',
'annotationsLoaded'
]
for event in events
if event is 'annotationCreated'
@annotator.subscribe event, =>
@dynamicBucket = false
this._update()
else
@annotator.subscribe event, this._update
@element.on 'click', (event) =>
event.stopPropagation()
this._fillDynamicBucket()
@dynamicBucket = true
$(window).on 'resize scroll', this._update
$(document.body).on 'resize scroll', '*', this._update
if window.PDFView?
# XXX: PDF.js hack
$(PDFView.container).on 'scroll', this._update
_rebaseUrls: ->
# We can't rely on browsers to implement the xml:base property correctly.
......@@ -62,7 +98,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
rect.attr('fill', fill)
rect.attr('filter', filter)
_collate: (a, b) =>
_collate: (a, b) ->
for i in [0..a.length-1]
if a[i] < b[i]
return -1
......@@ -77,11 +113,10 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
.interpolate(d3.interpolateHcl)
c(v).toString()
updateHeatmap: (data) =>
return unless d3?
wrapper = this.element.offsetParent()
{highlights, offset} = data
_update: =>
wrapper = @annotator.wrapper
highlights = wrapper.find('.annotator-hl')
defaultView = wrapper[0].ownerDocument.defaultView
# Keep track of buckets of annotations above and below the viewport
above = []
......@@ -89,10 +124,10 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
comments = []
# Construct control points for the heatmap highlights
points = highlights.reduce (points, hl, i) =>
x = hl.offset.top - wrapper.offset().top - offset
h = hl.height
d = hl.data
points = highlights.toArray().reduce (points, hl, i) =>
d = $(hl).data('annotation')
x = $(hl).offset().top - wrapper.offset().top - defaultView.pageYOffset
h = $(hl).outerHeight(true)
# XXX: Hacky stuff before unattached annotations V2
# Detect comments and push them into a separate bucket
......@@ -198,7 +233,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
max = 0
for b in @buckets
info = b.reduce (info, a) ->
subtotal = (a.thread?.flattenChildren()?.length or 0)
subtotal = a.reply_count or 0
return {
top: info.top + 1
replies: (info.replies or 0) + subtotal
......@@ -240,7 +275,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
[offsets[1], i, 1, 1e-6] ]
# Update the data bindings
element = d3.select(@element[0]).datum(data)
element = d3.select(@element[0]).datum(highlights)
# Update gradient stops
opacity = d3.scale.pow().domain([0, max]).range([.1, .6]).exponent(2)
......@@ -273,6 +308,72 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
tabs.enter().append('div')
.classed('heatmap-pointer', true)
# Creates highlights corresponding bucket when mouse is hovered
.on 'mousemove', (bucket) =>
highlights = wrapper.find('.annotator-hl')
highlights.toArray().forEach (hl) =>
if $(hl).data('annotation') in @buckets[bucket]
$(hl).addClass('annotator-hl-active')
else if not $(hl).hasClass('annotator-hl-temporary')
$(hl).removeClass('annotator-hl-active')
# Gets rid of them after
.on 'mouseout', =>
highlights = wrapper.find('.annotator-hl')
highlights.removeClass('annotator-hl-active')
# Does one of a few things when a tab is clicked depending on type
.on 'click', (bucket) =>
d3.event.stopPropagation()
highlights = wrapper.find('.annotator-hl')
pad = defaultView.innerHeight * .2
# If it's the upper tab, scroll to next bucket above
if @isUpper bucket
threshold = defaultView.pageYOffset
{next} = highlights.toArray().reduce (acc, hl) ->
{pos, next} = acc
if pos < $(hl).offset().top < threshold
pos: $(hl).offset().top
next: $(hl)
else
acc
, {pos: 0, next: null}
next?.scrollintoview
complete: ->
if this.parentNode is this.ownerDocument
scrollable = $(this.ownerDocument.body)
else
scrollable = $(this)
top = scrollable.scrollTop()
scrollable.stop().animate {scrollTop: top - pad}, 300
# If it's the lower tab, scroll to next bucket below
else if @isLower bucket
threshold = defaultView.pageYOffset + defaultView.innerHeight - pad
{next} = highlights.toArray().reduce (acc, hl) ->
{pos, next} = acc
if threshold < $(hl).offset().top < pos
pos: $(hl).offset().top
next: $(hl)
else
acc
, {pos: Number.MAX_VALUE, next: null}
next?.scrollintoview
complete: ->
if this.parentNode is this.ownerDocument
scrollable = $(this.ownerDocument.body)
else
scrollable = $(this)
top = scrollable.scrollTop()
scrollable.stop().animate {scrollTop: top + pad}, 300
# If it's neither of the above, load the bucket into the viewer
else
d3.event.stopPropagation()
@dynamicBucket = false
annotator.showViewer @buckets[bucket]
tabs.exit().remove()
tabs
......@@ -289,7 +390,24 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
.style 'display', (d) =>
if (@buckets[d].length is 0) then 'none' else ''
this.publish('updated')
if @dynamicBucket
this._fillDynamicBucket()
_fillDynamicBucket: =>
top = window.pageYOffset
bottom = top + $(window).innerHeight()
highlights = @annotator.wrapper.find('.annotator-hl')
visible = highlights.toArray().reduce (acc, hl) =>
if $(hl).offset().top >= top and $(hl).offset().top <= bottom
if $(hl).data('annotation') not in acc
acc.push $(hl).data('annotation')
else
annotation = $(hl).data('annotation')
if not (annotation.target?.length or annotation.references?.length)
acc.push annotation
acc
, []
@annotator.updateViewer visible
isUpper: (i) => i == 1
isLower: (i) => i == @index.length - 3
......
$ = Annotator.$
class Annotator.Plugin.Toolbar extends Annotator.Plugin
events:
'updateNotificationCounter': 'onUpdateNotificationCounter'
html:
element: '<div class="annotator-toolbar"></div>'
notification: '<div class="annotator-notification-counter"></div>"'
options:
items: [
"title": "Toggle Sidebar"
"class": "tri-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
collapsed = window.annotator.frame.hasClass('annotator-collapsed')
if collapsed
window.annotator.showFrame()
else
window.annotator.hideFrame()
,
"title": "Show Annotations"
"class": "alwaysonhighlights-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
state = not window.annotator.visibleHighlights
window.annotator.setVisibleHighlights state
if state
$(event.target).addClass('pushed')
else
$(event.target).removeClass('pushed')
,
"title": "Highlighting Mode"
"class": "highlighter-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
state = not (window.annotator.tool is 'highlight')
tool = if state then 'highlight' else 'comment'
window.annotator.setTool tool
if state
$(event.target).addClass('pushed')
else
$(event.target).removeClass('pushed')
,
"title": "New Comment"
"class": "commenter-icon"
"click": (event) ->
event.preventDefault()
event.stopPropagation()
window.annotator.addComment()
]
pluginInit: ->
@annotator.toolbar = @toolbar = $(@html.element)
if @options.container?
$(@options.container).append @toolbar
else
$(@element).append @toolbar
@notificationCounter = $(@html.notification)
@toolbar.append(@notificationCounter)
@buttons = @options.items.reduce (buttons, item) =>
button = $('<a></a>')
.attr('href', '')
.attr('title', item.title)
.on('click', item.click)
.addClass(item.class)
.data('state', false)
buttons.add button
, $()
list = $('<ul></ul>')
@buttons.appendTo(list)
@buttons.wrap('<li></li>')
@toolbar.append(list)
onUpdateNotificationCounter: (count) ->
element = $(@buttons[0])
element.toggle('fg_highlight', {color: 'lightblue'})
setTimeout ->
element.toggle('fg_highlight', {color: 'lightblue'})
, 500
switch
when count > 9
@notificationCounter.text('>9')
when 0 < count < 9
@notificationCounter.text("+#{count}")
else
@notificationCounter.text('')
This diff is collapsed.
......@@ -732,8 +732,11 @@
return Math.max.apply(Math, all);
},
mousePosition: function(e, offsetEl) {
var offset;
offset = $(offsetEl).position();
var offset, _ref1;
if ((_ref1 = $(offsetEl).css('position')) !== 'absolute' && _ref1 !== 'fixed' && _ref1 !== 'relative') {
offsetEl = $(offsetEl).offsetParent()[0];
}
offset = $(offsetEl).offset();
return {
top: e.pageY - offset.top,
left: e.pageX - offset.left
......
/*!
* jQuery scrollintoview() plugin and :scrollable selector filter
*
* Version 1.8 (14 Jul 2011)
* Requires jQuery 1.4 or newer
*
* Copyright (c) 2011 Robert Koritnik
* Licensed under the terms of the MIT license
* http://www.opensource.org/licenses/mit-license.php
*/
(function ($) {
var converter = {
vertical: { x: false, y: true },
horizontal: { x: true, y: false },
both: { x: true, y: true },
x: { x: true, y: false },
y: { x: false, y: true }
};
var settings = {
duration: "fast",
direction: "both"
};
var rootrx = /^(?:html)$/i;
// gets border dimensions
var borders = function (domElement, styles) {
styles = styles || (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(domElement, null) : domElement.currentStyle);
var px = document.defaultView && document.defaultView.getComputedStyle ? true : false;
var b = {
top: (parseFloat(px ? styles.borderTopWidth : $.css(domElement, "borderTopWidth")) || 0),
left: (parseFloat(px ? styles.borderLeftWidth : $.css(domElement, "borderLeftWidth")) || 0),
bottom: (parseFloat(px ? styles.borderBottomWidth : $.css(domElement, "borderBottomWidth")) || 0),
right: (parseFloat(px ? styles.borderRightWidth : $.css(domElement, "borderRightWidth")) || 0)
};
return {
top: b.top,
left: b.left,
bottom: b.bottom,
right: b.right,
vertical: b.top + b.bottom,
horizontal: b.left + b.right
};
};
var dimensions = function ($element) {
var win = $(window);
var isRoot = rootrx.test($element[0].nodeName);
return {
border: isRoot ? { top: 0, left: 0, bottom: 0, right: 0} : borders($element[0]),
scroll: {
top: (isRoot ? win : $element).scrollTop(),
left: (isRoot ? win : $element).scrollLeft()
},
scrollbar: {
right: isRoot ? 0 : $element.innerWidth() - $element[0].clientWidth,
bottom: isRoot ? 0 : $element.innerHeight() - $element[0].clientHeight
},
rect: (function () {
var r = $element[0].getBoundingClientRect();
return {
top: isRoot ? 0 : r.top,
left: isRoot ? 0 : r.left,
bottom: isRoot ? $element[0].clientHeight : r.bottom,
right: isRoot ? $element[0].clientWidth : r.right
};
})()
};
};
$.fn.extend({
scrollintoview: function (options) {
/// <summary>Scrolls the first element in the set into view by scrolling its closest scrollable parent.</summary>
/// <param name="options" type="Object">Additional options that can configure scrolling:
/// duration (default: "fast") - jQuery animation speed (can be a duration string or number of milliseconds)
/// direction (default: "both") - select possible scrollings ("vertical" or "y", "horizontal" or "x", "both")
/// complete (default: none) - a function to call when scrolling completes (called in context of the DOM element being scrolled)
/// </param>
/// <return type="jQuery">Returns the same jQuery set that this function was run on.</return>
options = $.extend({}, settings, options);
options.direction = converter[typeof (options.direction) === "string" && options.direction.toLowerCase()] || converter.both;
var dirStr = "";
if (options.direction.x === true) dirStr = "horizontal";
if (options.direction.y === true) dirStr = dirStr ? "both" : "vertical";
var el = this.eq(0);
var scroller = el.closest(":scrollable(" + dirStr + ")");
// check if there's anything to scroll in the first place
if (scroller.length > 0)
{
scroller = scroller.eq(0);
var dim = {
e: dimensions(el),
s: dimensions(scroller)
};
var rel = {
top: dim.e.rect.top - (dim.s.rect.top + dim.s.border.top),
bottom: dim.s.rect.bottom - dim.s.border.bottom - dim.s.scrollbar.bottom - dim.e.rect.bottom,
left: dim.e.rect.left - (dim.s.rect.left + dim.s.border.left),
right: dim.s.rect.right - dim.s.border.right - dim.s.scrollbar.right - dim.e.rect.right
};
var animOptions = {};
// vertical scroll
if (options.direction.y === true)
{
if (rel.top < 0)
{
animOptions.scrollTop = dim.s.scroll.top + rel.top;
}
else if (rel.top > 0 && rel.bottom < 0)
{
animOptions.scrollTop = dim.s.scroll.top + Math.min(rel.top, -rel.bottom);
}
}
// horizontal scroll
if (options.direction.x === true)
{
if (rel.left < 0)
{
animOptions.scrollLeft = dim.s.scroll.left + rel.left;
}
else if (rel.left > 0 && rel.right < 0)
{
animOptions.scrollLeft = dim.s.scroll.left + Math.min(rel.left, -rel.right);
}
}
// scroll if needed
if (!$.isEmptyObject(animOptions))
{
if (rootrx.test(scroller[0].nodeName))
{
scroller = $("html,body");
}
scroller
.animate(animOptions, options.duration)
.eq(0) // we want function to be called just once (ref. "html,body")
.queue(function (next) {
$.isFunction(options.complete) && options.complete.call(scroller[0]);
next();
});
}
else
{
// when there's nothing to scroll, just call the "complete" function
$.isFunction(options.complete) && options.complete.call(scroller[0]);
}
}
// return set back
return this;
}
});
var scrollValue = {
auto: true,
scroll: true,
visible: false,
hidden: false
};
$.extend($.expr[":"], {
scrollable: function (element, index, meta, stack) {
var direction = converter[typeof (meta[3]) === "string" && meta[3].toLowerCase()] || converter.both;
var styles = (document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(element, null) : element.currentStyle);
var overflow = {
x: scrollValue[styles.overflowX.toLowerCase()] || false,
y: scrollValue[styles.overflowY.toLowerCase()] || false,
isRoot: rootrx.test(element.nodeName)
};
// check if completely unscrollable (exclude HTML element because it's special)
if (!overflow.x && !overflow.y && !overflow.isRoot)
{
return false;
}
var size = {
height: {
scroll: element.scrollHeight,
client: element.clientHeight
},
width: {
scroll: element.scrollWidth,
client: element.clientWidth
},
// check overflow.x/y because iPad (and possibly other tablets) don't dislay scrollbars
scrollableX: function () {
return (overflow.x || overflow.isRoot) && this.width.scroll > this.width.client;
},
scrollableY: function () {
return (overflow.y || overflow.isRoot) && this.height.scroll > this.height.client;
}
};
return direction.y && size.scrollableY() || direction.x && size.scrollableX();
}
});
})(jQuery);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment