Commit 5ff971ca authored by Jake Hartnell's avatar Jake Hartnell

Merge pull request #2149 from hypothesis/305-hammerjs

Rework sidebar drag resize
parents 51737ead f46f6315
Annotator = require('annotator')
$ = Annotator.$
Hammer = require('hammerjs')
Guest = require('./guest')
# Minimum width to which the frame can be resized.
MIN_RESIZE = 280
module.exports = class Host extends Guest
# Drag state variables
drag:
delta: 0
enabled: false
last: null
tick: false
gestureState: null
constructor: (element, options) ->
# Create the iframe
......@@ -53,7 +54,7 @@ module.exports = class Host extends Guest
this.publish('setVisibleHighlights', !!options.showHighlights)
if @plugins.BucketBar?
this._setupDragEvents()
this._setupGestures()
@plugins.BucketBar.element.on 'click', (event) =>
if @frame.hasClass 'annotator-collapsed'
this.showFrame()
......@@ -63,12 +64,11 @@ module.exports = class Host extends Guest
super
showFrame: (options={transition: true}) ->
unless @drag.enabled
@frame.css 'margin-left': "#{-1 * @frame.width()}px"
if options.transition
@frame.removeClass 'annotator-no-transition'
else
@frame.addClass 'annotator-no-transition'
@frame.css 'margin-left': "#{-1 * @frame.width()}px"
@frame.removeClass 'annotator-collapsed'
if @toolbar?
......@@ -86,61 +86,89 @@ module.exports = class Host extends Guest
.removeClass('h-icon-chevron-right')
.addClass('h-icon-chevron-left')
_addCrossFrameListeners: ->
@crossframe.on('showFrame', this.showFrame.bind(this, null))
@crossframe.on('hideFrame', this.hideFrame.bind(this, null))
_setupDragEvents: ->
el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
el.width = el.height = 1
@element.append el
dragStart = (event) =>
event.dataTransfer.dropEffect = 'none'
event.dataTransfer.effectAllowed = 'none'
event.dataTransfer.setData 'text/plain', ''
event.dataTransfer.setDragImage el, 0, 0
@drag.enabled = true
@drag.last = event.screenX
m = parseInt (getComputedStyle @frame[0]).marginLeft
@frame.css
'margin-left': "#{m}px"
this.showFrame()
dragEnd = (event) =>
@drag.enabled = false
@drag.last = null
for handle in [@plugins.BucketBar.element[0], @plugins.Toolbar.buttons[0]]
handle.draggable = true
handle.addEventListener 'dragstart', dragStart
handle.addEventListener 'dragend', dragEnd
document.addEventListener 'dragover', (event) =>
this._dragUpdate event.screenX
_dragUpdate: (screenX) =>
unless @drag.enabled then return
if @drag.last?
@drag.delta += screenX - @drag.last
@drag.last = screenX
unless @drag.tick
@drag.tick = true
window.requestAnimationFrame this._dragRefresh
_dragRefresh: =>
d = @drag.delta
@drag.delta = 0
@drag.tick = false
m = parseInt (getComputedStyle @frame[0]).marginLeft
w = -1 * m
m += d
w -= d
@frame.addClass 'annotator-no-transition'
@frame.css
'margin-left': "#{m}px"
width: "#{w}px"
_initializeGestureState: ->
@gestureState =
acc: null
initial: null
renderFrame: null
onPan: (event) =>
# Smooth updates
_updateLayout = =>
# Only schedule one frame at a time
return if @gestureState.renderFrame
# Schedule update
@gestureState.renderFrame = window.requestAnimationFrame =>
# Clear the frame
@gestureState.renderFrame = null
# Stop if finished
return unless @gestureState.acc?
# Set style
m = @gestureState.acc
w = -m
@frame.css('margin-left', "#{m}px")
if w >= MIN_RESIZE then @frame.css('width', "#{-m}px")
switch event.type
when 'panstart'
# Initialize the gesture state
this._initializeGestureState()
# Immadiate response
@frame.addClass 'annotator-no-transition'
# Escape iframe capture
@frame.css('pointer-events', 'none')
# Set origin margin
@gestureState.initial = parseInt(getComputedStyle(@frame[0]).marginLeft)
when 'panend'
# Re-enable transitions
@frame.removeClass 'annotator-no-transition'
# Re-enable iframe events
@frame.css('pointer-events', '')
# Consider the frame open if it open to at least a minimum width
if @gestureState.acc <= -MIN_RESIZE then this.showFrame()
# Reset the gesture state
this._initializeGestureState()
when 'panleft', 'panright'
return unless @gestureState.initial?
# Compute new margin from delta and initial conditions
m = @gestureState.initial
d = event.deltaX
acc = Math.min(Math.round(m + d), 0)
@gestureState.acc = acc
# Start updating
_updateLayout()
onSwipe: (event) =>
switch event.type
when 'swipeleft'
this.showFrame()
when 'swiperight'
this.hideFrame()
_setupGestures: ->
$toggle = @toolbar.find('[name=sidebar-toggle]')
# Prevent any default gestures on the handle
$toggle.on('touchmove', (event) -> event.preventDefault())
# Set up the Hammer instance and handlers
mgr = new Hammer.Manager($toggle[0])
.on('panstart panend panleft panright', this.onPan)
.on('swipeleft swiperight', this.onSwipe)
# Set up the gesture recognition
pan = mgr.add(new Hammer.Pan({direction: Hammer.DIRECTION_HORIZONTAL}))
swipe = mgr.add(new Hammer.Swipe({direction: Hammer.DIRECTION_HORIZONTAL}))
swipe.recognizeWith(pan)
# Set up the initial state
this._initializeGestureState()
# Return this for chaining
this
......@@ -58,3 +58,82 @@ describe 'Host', ->
host = createHost()
emitHostEvent('hideFrame')
assert.called(target)
describe 'pan gestures', ->
host = null
beforeEach ->
host = createHost({})
describe 'panstart event', ->
beforeEach ->
sandbox.stub(window, 'getComputedStyle').returns({marginLeft: '100px'})
host.onPan({type: 'panstart'})
it 'disables pointer events and transitions on the widget', ->
assert.isTrue(host.frame.hasClass('annotator-no-transition'))
assert.equal(host.frame.css('pointer-events'), 'none')
it 'captures the left margin as the gesture initial state', ->
assert.equal(host.gestureState.initial, '100')
describe 'panend event', ->
it 'enables pointer events and transitions on the widget', ->
host.gestureState = {acc: 0}
host.onPan({type: 'panend'})
assert.isFalse(host.frame.hasClass('annotator-no-transition'))
assert.equal(host.frame.css('pointer-events'), '')
it 'calls `showFrame` if the widget is fully visible', ->
host.gestureState = {acc: -500}
showFrame = sandbox.stub(host, 'showFrame')
host.onPan({type: 'panend'})
assert.calledOnce(showFrame)
it 'does not call `showFrame` if the widget is not fully visible', ->
host.gestureState = {acc: -100}
showFrame = sandbox.stub(host, 'showFrame')
host.onPan({type: 'panend'})
assert.notCalled(showFrame)
describe 'panleft and panright events', ->
raf = null
# PhantomJS may or may not have rAF so the normal sandbox approach
# doesn't quite work. We assign and delete it ourselves instead when
# it isn't already present.
beforeEach ->
if window.requestAnimationFrame?
sandbox.stub(window, 'requestAnimationFrame')
else
raf = window.requestAnimationFrame = sandbox.stub()
afterEach ->
if raf?
raf = null
delete window.requestAnimationFrame
it 'shrinks or grows the widget to match the delta', ->
host.gestureState = {initial: -100}
host.onPan({type: 'panleft', deltaX: -50})
assert.equal(host.gestureState.acc, -150)
host.onPan({type: 'panright', deltaX: 100})
assert.equal(host.gestureState.acc, 0)
describe 'swipe gestures', ->
host = null
beforeEach ->
host = createHost({})
it 'opens the sidebar on swipeleft', ->
showFrame = sandbox.stub(host, 'showFrame')
host.onSwipe({type: 'swipeleft'})
assert.calledOnce(showFrame)
it 'closes the sidebar on swiperight', ->
hideFrame = sandbox.stub(host, 'hideFrame')
host.onSwipe({type: 'swiperight'})
assert.calledOnce(hideFrame)
......@@ -235,7 +235,7 @@ $base-font-size: 14px;
@media screen and (min-width: 37.5em) {
.annotator-frame {
@include single-transition(margin-left, .25s);
@include single-transition(margin-left, .15s);
@include transition-timing-function(cubic-bezier(.55, 0, .2, .8));
width: 428px;
margin-left: -428px;
......
......@@ -9,6 +9,7 @@
"clean-css": "2.2.2",
"coffee-script": "1.7.1",
"coffeeify": "^1.0.0",
"hammerjs": "^2.0.4",
"node-uuid": "^1.4.3",
"uglify-js": "2.4.14"
},
......@@ -80,6 +81,7 @@
"diff-match-patch": "diff_match_patch",
"dom-text-mapper": "DomTextMapper",
"dom-text-matcher": "DomTextMatcher",
"hammerjs": "Hammer",
"jquery": "$",
"jquery-scrollintoview": {
"depends": [
......
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