Commit 9aecc0c3 authored by Robert Knight's avatar Robert Knight

Switch the direction of the adder arrow to point up if it would disappear off the top of the screen

If the last line of the selection is very close to the top of the screen
then show the adder pointing up at the text rather than down at it.

Also constrain the adder's position so that it never appears outside the
viewport.
parent ceb8e238
......@@ -8,6 +8,12 @@ var classnames = require('classnames');
*/
var ARROW_POINTING_DOWN = 1;
/**
* Show the adder above the selection with an arrow pointing down at the
* selected text.
*/
var ARROW_POINTING_UP = 2;
/**
* Returns the HTML template for the adder.
*/
......@@ -15,6 +21,10 @@ function template() {
return require('./adder.html');
}
function toPx(pixels) {
return pixels.toString() + 'px';
}
/**
* Controller for the 'adder' toolbar which appears next to the selection
* and provides controls for the user to create new annotations.
......@@ -22,23 +32,103 @@ function template() {
* @param {JQuery} element - jQuery element for the adder.
*/
function Adder(element) {
this.element = element;
var ARROW_HEIGHT = 10;
// The preferred gap between the end of the text selection and the adder's
// arrow position.
var ARROW_H_MARGIN = 20;
var view = element.ownerDocument.defaultView;
// Set initial style. The adder is hidden using the `visibility`
// property rather than `display` so that we can compute its size in order to
// position it before display.
element.style.display = 'block';
element.style.visibility = 'hidden';
function width() {
return element.getBoundingClientRect().width;
}
function height() {
return element.getBoundingClientRect().height;
}
/** Hide the adder */
this.hide = function () {
element.hide();
element.style.visibility = 'hidden';
};
this.showAt = function (position, arrowDirection) {
element[0].className = classnames({
/**
* Return the best position to show the adder in order to target the
* selected text in `targetRect`.
*
* @param {Rect} targetRect - The rect of text to target, in document
* coordinates.
* @param {boolean} isSelectionBackwards - True if the selection was made
* backwards, such that the focus point is at the left edge of
* `targetRect`.
*/
this.target = function (targetRect, isSelectionBackwards) {
// Set position and arrow direction depending on available space
var arrowDirection = ARROW_POINTING_DOWN;
var top;
var left;
// Position the adder such that the arrow it is above or below the selection
// and close to the end.
var hMargin = Math.min(ARROW_H_MARGIN, targetRect.width);
if (isSelectionBackwards) {
left = targetRect.left - width() / 2 + hMargin;
} else {
left = targetRect.left + targetRect.width - width() / 2 - hMargin;
}
// Note: `pageYOffset` is used instead of `scrollY` here for
// IE compatibility
if (targetRect.top - height() < view.pageYOffset &&
arrowDirection === ARROW_POINTING_DOWN) {
arrowDirection = ARROW_POINTING_UP;
}
if (arrowDirection === ARROW_POINTING_UP) {
top = targetRect.top + targetRect.height + ARROW_HEIGHT;
} else {
top = targetRect.top - height() - ARROW_HEIGHT;
}
// Constrain the adder to the viewport
left = Math.max(left, view.pageXOffset);
left = Math.min(left, view.pageXOffset + view.innerWidth - width());
top = Math.max(top, view.pageYOffset);
top = Math.min(top, view.pageYOffset + view.innerHeight - height());
return {top: top, left: left, arrowDirection: arrowDirection};
};
/**
* Show the adder at the given position and with the arrow pointing in
* `arrowDirection`.
*/
this.showAt = function (left, top, arrowDirection) {
element.className = classnames({
'annotator-adder': true,
'annotator-adder--arrow-down': arrowDirection === ARROW_POINTING_DOWN,
'annotator-adder--arrow-up': arrowDirection === ARROW_POINTING_UP,
});
element[0].style.top = position.top;
element[0].style.left = position.left;
element.show();
element.style.top = toPx(top);
element.style.left = toPx(left);
element.style.visibility = 'visible';
};
}
module.exports = {
ARROW_POINTING_DOWN: ARROW_POINTING_DOWN,
ARROW_POINTING_UP: ARROW_POINTING_UP,
template: template,
Adder: Adder,
};
......@@ -48,7 +48,7 @@ module.exports = class Guest extends Annotator
constructor: (element, options) ->
super
this.adderCtrl = new adder.Adder(@adder)
this.adderCtrl = new adder.Adder(@adder[0])
this.anchors = []
cfOptions =
......@@ -353,8 +353,10 @@ module.exports = class Guest extends Annotator
else
# Show the adder button
selection = Annotator.Util.getGlobal().getSelection()
this.adderCtrl.showAt(rangeUtil.selectionEndPosition(selection),
adder.ARROW_POINTING_DOWN)
isBackwards = rangeUtil.isSelectionBackwards(selection)
focusRect = rangeUtil.selectionFocusRect(selection)
{left, top, arrowDirection} = this.adderCtrl.target(focusRect, isBackwards)
this.adderCtrl.showAt(left, top, arrowDirection)
true
onFailedSelection: (event) ->
......
......@@ -116,13 +116,14 @@ function getTextBoundingBoxes(range) {
}
/**
* Returns the coordinates of the end (ie. at the focus point) of the text in a
* selection.
* Returns the rectangle for the line of text containing the focus point
* of a Selection.
*
* @param {Selection} selection
* @return {Object?} The X and Y coordinates of the selection.
* @return {Object?} A rect containing the coordinates in the document of the
* line of text containing the focus point of the selection.
*/
function selectionEndPosition(selection) {
function selectionFocusRect(selection) {
if (selection.isCollapsed) {
return null;
}
......@@ -130,14 +131,15 @@ function selectionEndPosition(selection) {
if (textBoxes.length === 0) {
return null;
}
if (isSelectionBackwards(selection)) {
return {top: textBoxes[0].top, left: textBoxes[0].left};
return textBoxes[0];
} else {
var lastBox = textBoxes[textBoxes.length - 1];
return {top: lastBox.top, left: lastBox.left + lastBox.width};
return textBoxes[textBoxes.length - 1];
}
}
module.exports = {
selectionEndPosition: selectionEndPosition,
isSelectionBackwards: isSelectionBackwards,
selectionFocusRect: selectionFocusRect,
};
'use strict';
var adder = require('../adder');
function rect(left, top, width, height) {
return {left: left, top: top, width: width, height: height};
}
describe('adder', function () {
var adderCtrl;
beforeEach(function () {
var adderEl = document.createElement('div');
adderEl.innerHTML = adder.template();
adderEl = adderEl.firstChild;
document.body.appendChild(adderEl.firstChild);
adderCtrl = new adder.Adder(adderEl.firstChild);
});
afterEach(function () {
adderCtrl.element.parentNode.removeChild(adderCtrl.element);
});
function windowSize() {
var window = adderCtrl.element.ownerDocument.defaultView;
return {width: window.innerWidth, height: window.innerHeight};
}
function adderSize() {
var rect = adderCtrl.element.getBoundingClientRect();
return {width: rect.width, height: rect.height};
}
describe('#target', function () {
it('positions the adder above the selection', function () {
var target = adderCtrl.target(rect(100,200,100,20), false);
assert.isAbove(target.top, 100);
assert.isAbove(target.left, 100);
assert.isBelow(target.left, 200);
assert.equal(target.arrowDirection, adder.ARROW_POINTING_DOWN);
});
it('does not position the adder above the top of the viewport', function () {
var target = adderCtrl.target(rect(100,-100,100,20), false);
assert.isAtLeast(target.top, 0);
assert.equal(target.arrowDirection, adder.ARROW_POINTING_UP);
});
it('does not position the adder below the bottom of the viewport', function () {
var viewSize = windowSize();
var target = adderCtrl.target(rect(0,viewSize.height + 100,10,20), false);
assert.isAtMost(target.top, viewSize.height - adderSize().height);
});
it('does not position the adder beyond the right edge of the viewport', function () {
var viewSize = windowSize();
var target = adderCtrl.target(rect(viewSize.width + 100,100,10,20), false);
assert.isAtMost(target.left, viewSize.width);
});
it('does not positon the adder beyond the left edge of the viewport', function () {
var target = adderCtrl.target(rect(-100,100,10,10), false);
assert.isAtLeast(target.left, 0);
});
});
});
......@@ -11,7 +11,7 @@ describe('range-util', function () {
selection.collapse();
testNode = document.createElement('span');
testNode.innerHTML = 'Some text content here';
testNode.innerHTML = 'Some text <br>content here';
document.body.appendChild(testNode);
});
......@@ -25,28 +25,30 @@ describe('range-util', function () {
selection.addRange(range);
}
describe('#selectionEndPosition', function () {
describe('#selectionFocusRect', function () {
it('returns null if the selection is empty', function () {
assert.isNull(rangeUtil.selectionEndPosition(selection));
assert.isNull(rangeUtil.selectionFocusRect(selection));
});
it('returns a point if the selection is not empty', function () {
selectNode(testNode);
assert.ok(rangeUtil.selectionEndPosition(selection));
assert.ok(rangeUtil.selectionFocusRect(selection));
});
it('returns the top-left corner if the selection is backwards', function () {
it('returns the first line\'s rect 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);
var rect = rangeUtil.selectionFocusRect(selection);
assert.equal(rect.left, testNode.offsetLeft);
assert.equal(rect.top, testNode.offsetTop);
});
it('returns the bottom-right corner if the selection is forwards', function () {
it('returns the last line\'s rect if the selection is forwards', function () {
selectNode(testNode);
var pos = rangeUtil.selectionEndPosition(selection);
assert.equal(pos.left, testNode.offsetLeft + testNode.offsetWidth);
var rect = rangeUtil.selectionFocusRect(selection);
assert.equal(rect.left, testNode.offsetLeft);
assert.equal(rect.top + rect.height, testNode.offsetTop + testNode.offsetHeight);
});
});
});
.annotator-adder {
box-sizing: border-box;
direction: ltr;
margin-top: -60px;
margin-left: -65px;
position: absolute;
background: $white;
border: 1px solid rgba(0,0,0,0.20);
......@@ -32,6 +30,11 @@
bottom: -5px;
}
.annotator-adder--arrow-up:before {
@include adder-arrow(225deg);
top: -5px;
}
.annotator-adder-actions {
display: flex;
flex-direction: row;
......
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