Commit 487d782e authored by Robert Knight and Sheetal Umesh Kumar's avatar Robert Knight and Sheetal Umesh Kumar Committed by Robert Knight

Show adder if user makes a selection and then launches the plugin.

If a user selects some text, and then realises that Hypothesis isn't
active, their first reaction will probably be to activate Hypothesis.
In this scenario, we should pop up the adder near the selection on launch.

 * If there is a selection when the 'ready' event fires, show the adder
   at the focus point of the selection.

 * For consistency, show the adder at the focus point of the selection
   after a selection whilst Hypothesis is active.

 * Add 'range-util' module which provides utility functions to get
   the coordinates at the end of the selection.

   'range-util' uses the DOM Range, Selection and NodeIterator APIs directly
   rather than Annotator's Range functions for several reasons:

   1) Given that H now requires IE >= 10, all of our target browsers
      have all the APIs that we need.

   2) Using the DOM APIs directly proved easier to debug and test.
parent 3bf56c1c
...@@ -7,7 +7,7 @@ Annotator = require('annotator') ...@@ -7,7 +7,7 @@ Annotator = require('annotator')
$ = Annotator.$ $ = Annotator.$
highlighter = require('./highlighter') highlighter = require('./highlighter')
rangeUtil = require('./range-util')
animationPromise = (fn) -> animationPromise = (fn) ->
return new Promise (resolve, reject) -> return new Promise (resolve, reject) ->
...@@ -17,7 +17,6 @@ animationPromise = (fn) -> ...@@ -17,7 +17,6 @@ animationPromise = (fn) ->
catch error catch error
reject(error) reject(error)
module.exports = class Guest extends Annotator module.exports = class Guest extends Annotator
SHOW_HIGHLIGHTS_CLASS = 'annotator-highlights-always-on' SHOW_HIGHLIGHTS_CLASS = 'annotator-highlights-always-on'
...@@ -196,6 +195,7 @@ module.exports = class Guest extends Annotator ...@@ -196,6 +195,7 @@ module.exports = class Guest extends Annotator
range = Annotator.Range.sniff(anchor.range) range = Annotator.Range.sniff(anchor.range)
normedRange = range.normalize(root) normedRange = range.normalize(root)
highlights = highlighter.highlightRange(normedRange) highlights = highlighter.highlightRange(normedRange)
$(highlights).data('annotation', anchor.annotation) $(highlights).data('annotation', anchor.annotation)
anchor.highlights = highlights anchor.highlights = highlights
return anchor return anchor
...@@ -341,6 +341,11 @@ module.exports = class Guest extends Annotator ...@@ -341,6 +341,11 @@ module.exports = class Guest extends Annotator
tags = (a.$$tag for a in annotations) tags = (a.$$tag for a in annotations)
@crossframe?.call('focusAnnotations', tags) @crossframe?.call('focusAnnotations', tags)
showAdder: (position) ->
@adder
.css(position)
.show()
onSuccessfulSelection: (event, immediate) -> onSuccessfulSelection: (event, immediate) ->
unless event? unless event?
throw "Called onSuccessfulSelection without an event!" throw "Called onSuccessfulSelection without an event!"
...@@ -360,10 +365,8 @@ module.exports = class Guest extends Annotator ...@@ -360,10 +365,8 @@ module.exports = class Guest extends Annotator
@onAdderClick event @onAdderClick event
else else
# Show the adder button # Show the adder button
@adder selection = Annotator.Util.getGlobal().getSelection()
.css(Annotator.Util.mousePosition(event, @element[0])) this.showAdder(rangeUtil.selectionEndPosition(selection))
.show()
true true
onFailedSelection: (event) -> onFailedSelection: (event) ->
......
...@@ -10,6 +10,7 @@ module.exports = class TextSelection extends Annotator.Plugin ...@@ -10,6 +10,7 @@ module.exports = class TextSelection extends Annotator.Plugin
$(document).bind({ $(document).bind({
"touchend": @checkForEndSelection "touchend": @checkForEndSelection
"mouseup": @checkForEndSelection "mouseup": @checkForEndSelection
"ready": @checkForEndSelection
}) })
null null
...@@ -18,6 +19,7 @@ module.exports = class TextSelection extends Annotator.Plugin ...@@ -18,6 +19,7 @@ module.exports = class TextSelection extends Annotator.Plugin
$(document).unbind({ $(document).unbind({
"touchend": @checkForEndSelection "touchend": @checkForEndSelection
"mouseup": @checkForEndSelection "mouseup": @checkForEndSelection
"ready": @checkForEndSelection
}) })
super super
......
'use strict';
function translate(rect, x, y) {
return {
left: rect.left + x,
top: rect.top + y,
width: rect.width,
height: rect.height,
};
}
function mapViewportRectToDocument(window, rect) {
// `pageXOffset` and `pageYOffset` are used rather than `scrollX`
// and `scrollY` for IE 10/11 compatibility.
return translate(rect, window.pageXOffset, window.pageYOffset);
}
/**
* Returns true if the start point of a selection occurs after the end point,
* in document order.
*/
function isSelectionBackwards(selection) {
if (selection.focusNode === selection.anchorNode) {
return selection.focusOffset < selection.anchorOffset;
}
var range = selection.getRangeAt(0);
return range.startContainer === selection.focusNode;
}
/**
* Returns true if `node` is between the `startContainer` and `endContainer` of
* the given `range`, inclusive.
*
* @param {Range} range
* @param {Node} node
*/
function isNodeInRange(range, node) {
if (node === range.startContainer || node === range.endContainer) {
return true;
}
/* jshint -W016 */
var isAfterStart = range.startContainer.compareDocumentPosition(node) &
Node.DOCUMENT_POSITION_FOLLOWING;
var isBeforeEnd = range.endContainer.compareDocumentPosition(node) &
Node.DOCUMENT_POSITION_PRECEDING;
return isAfterStart && isBeforeEnd;
}
/**
* Iterate over all Node(s) in `range` in document order and invoke `callback`
* for each of them.
*
* @param {Range} range
* @param {Function} callback
*/
function forEachNodeInRange(range, callback) {
var root = range.commonAncestorContainer;
// The `whatToShow`, `filter` and `expandEntityReferences` arguments are
// mandatory in IE although optional according to the spec.
var nodeIter = root.ownerDocument.createNodeIterator(root,
NodeFilter.SHOW_ALL, null /* filter */, false /* expandEntityReferences */);
/* jshint -W084 */
var currentNode;
while (currentNode = nodeIter.nextNode()) {
if (isNodeInRange(range, currentNode)) {
callback(currentNode);
}
}
}
/**
* Returns the bounding rectangles of non-whitespace text nodes in `range`.
*
* @param {Range} range
* @return {Array<Rect>} Array of bounding rects in document coordinates.
*/
function getTextBoundingBoxes(range) {
var whitespaceOnly = /^\s*$/;
var textNodes = [];
forEachNodeInRange(range, function (node) {
if (node.nodeType === Node.TEXT_NODE &&
!node.textContent.match(whitespaceOnly)) {
textNodes.push(node);
}
});
var rects = [];
textNodes.forEach(function (node) {
var nodeRange = node.ownerDocument.createRange();
nodeRange.selectNodeContents(node);
if (node === range.startContainer) {
nodeRange.setStart(node, range.startOffset);
}
if (node === range.endContainer) {
nodeRange.setEnd(node, range.endOffset);
}
if (nodeRange.collapsed) {
// If the range ends at the start of this text node or starts at the end
// of this node then do not include it.
return;
}
// Measure the range and translate from viewport to document coordinates
var viewportRects = Array.from(nodeRange.getClientRects());
nodeRange.detach();
rects = rects.concat(viewportRects.map(function (rect) {
return mapViewportRectToDocument(node.ownerDocument.defaultView, rect);
}));
});
return rects;
}
/**
* Returns the coordinates of the end (ie. at the focus point) of the text in a
* selection.
*
* @param {Selection} selection
* @return {Object?} The X and Y coordinates of the selection.
*/
function selectionEndPosition(selection) {
if (selection.isCollapsed) {
return null;
}
var textBoxes = getTextBoundingBoxes(selection.getRangeAt(0));
if (textBoxes.length === 0) {
return null;
}
if (isSelectionBackwards(selection)) {
return {top: textBoxes[0].top, left: textBoxes[0].left};
} else {
var lastBox = textBoxes[textBoxes.length - 1];
return {top: lastBox.top, left: lastBox.left + lastBox.width};
}
}
module.exports = {
selectionEndPosition: selectionEndPosition,
};
'use strict';
var rangeUtil = require('../range-util');
describe('range-util', function () {
var selection;
var testNode;
beforeEach(function () {
selection = window.getSelection();
selection.collapse();
testNode = document.createElement('span');
testNode.innerHTML = 'Some text content here';
document.body.appendChild(testNode);
});
afterEach(function () {
testNode.parentElement.removeChild(testNode);
});
function selectNode(node) {
var range = testNode.ownerDocument.createRange();
range.selectNodeContents(node);
selection.addRange(range);
}
describe('#selectionEndPosition', function () {
it('returns null if the selection is empty', function () {
assert.isNull(rangeUtil.selectionEndPosition(selection));
});
it('returns a point if the selection is not empty', function () {
selectNode(testNode);
assert.ok(rangeUtil.selectionEndPosition(selection));
});
it('returns the top-left corner if the selection is backwards', function () {
selectNode(testNode);
selection.collapseToEnd();
selection.extend(testNode, 0);
var pos = rangeUtil.selectionEndPosition(selection);
assert.equal(pos.left, testNode.offsetLeft);
});
it('returns the bottom-right corner if the selection is forwards', function () {
selectNode(testNode);
var pos = rangeUtil.selectionEndPosition(selection);
assert.equal(pos.left, testNode.offsetLeft + testNode.offsetWidth);
});
});
});
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
require('core-js/es6/promise'); require('core-js/es6/promise');
require('core-js/fn/array/find'); require('core-js/fn/array/find');
require('core-js/fn/array/find-index'); require('core-js/fn/array/find-index');
require('core-js/fn/array/from');
require('core-js/fn/object/assign'); require('core-js/fn/object/assign');
// URL constructor, required by IE 10/11, // URL constructor, required by IE 10/11,
......
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