Commit 38e4d947 authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Migrate adder to TS

parent d883b0b0
import { render } from 'preact'; import { render } from 'preact';
import AdderToolbar from './components/AdderToolbar'; import AdderToolbar from './components/AdderToolbar';
import type { Command } from './components/AdderToolbar';
import { isTouchDevice } from '../shared/user-agent'; import { isTouchDevice } from '../shared/user-agent';
import { createShadowRoot } from './util/shadow-root'; import type { Destroyable } from '../types/annotator';
/** import { createShadowRoot } from './util/shadow-root';
* @typedef {1} ArrowPointingDown
* Show the adder above the selection with an arrow pointing down at the
* selected text.
*/
export const ARROW_POINTING_DOWN = 1;
/**
* @typedef {2} ArrowPointingUp
* Show the adder above the selection with an arrow pointing up at the
* selected text.
*/
export const ARROW_POINTING_UP = 2;
/** export enum ArrowDirection {
* @typedef {ArrowPointingDown|ArrowPointingUp} ArrowDirection DOWN = 1,
* Show the adder above the selection with an arrow pointing up at the UP = 2,
* selected text. }
*/
/** type Target = {
* @typedef Target /** Offset from left edge of viewport */
* @prop {number} left - Offset from left edge of viewport. left: number;
* @prop {number} top - Offset from top edge of viewport. /** Offset from top edge of viewport */
* @prop {ArrowDirection} arrowDirection - Direction of the adder's arrow. top: number;
*/ /** Direction of the adder's arrow */
arrowDirection: ArrowDirection;
};
/** @param {number} pixels */ function toPx(pixels: number) {
function toPx(pixels) {
return pixels.toString() + 'px'; return pixels.toString() + 'px';
} }
...@@ -44,14 +33,10 @@ const ARROW_H_MARGIN = 20; ...@@ -44,14 +33,10 @@ const ARROW_H_MARGIN = 20;
/** /**
* Return the closest ancestor of `el` which has been positioned. * Return the closest ancestor of `el` which has been positioned.
*
* If no ancestor has been positioned, returns the root element. * If no ancestor has been positioned, returns the root element.
*
* @param {Element} el
* @return {Element}
*/ */
function nearestPositionedAncestor(el) { function nearestPositionedAncestor(el: Element): Element {
let parentEl = /** @type {Element} */ (el.parentElement); let parentEl = el.parentElement!;
while (parentEl.parentElement) { while (parentEl.parentElement) {
if (getComputedStyle(parentEl).position !== 'static') { if (getComputedStyle(parentEl).position !== 'static') {
break; break;
...@@ -61,15 +46,14 @@ function nearestPositionedAncestor(el) { ...@@ -61,15 +46,14 @@ function nearestPositionedAncestor(el) {
return parentEl; return parentEl;
} }
/** type AdderOptions = {
* @typedef AdderOptions /** Callback invoked when "Annotate" button is clicked */
* @prop {() => void} onAnnotate - Callback invoked when "Annotate" button is clicked onAnnotate: () => void;
* @prop {() => void} onHighlight - Callback invoked when "Highlight" button is clicked /** Callback invoked when "Highlight" button is clicked */
* @prop {(tags: string[]) => void} onShowAnnotations - onHighlight: () => void;
* Callback invoked when "Show" button is clicked /** Callback invoked when "Show" button is clicked */
* onShowAnnotations: (tags: string[]) => void;
* @typedef {import('../types/annotator').Destroyable} Destroyable };
*/
/** /**
* Container for the 'adder' toolbar which provides controls for the user to * Container for the 'adder' toolbar which provides controls for the user to
...@@ -79,20 +63,29 @@ function nearestPositionedAncestor(el) { ...@@ -79,20 +63,29 @@ function nearestPositionedAncestor(el) {
* the container for the toolbar that positions it on the page and isolates * the container for the toolbar that positions it on the page and isolates
* it from the page's styles using shadow DOM, and the `AdderToolbar` Preact * it from the page's styles using shadow DOM, and the `AdderToolbar` Preact
* component which actually renders the toolbar. * component which actually renders the toolbar.
*
* @implements {Destroyable}
*/ */
export class Adder { export class Adder implements Destroyable {
private _outerContainer: HTMLElement;
private _shadowRoot: ShadowRoot;
private _view: Window;
private _isVisible: boolean;
private _arrowDirection: 'up' | 'down';
private _onAnnotate: () => void;
private _onHighlight: () => void;
private _onShowAnnotations: (tags: string[]) => void;
/** Annotation tags associated with the current selection. */
private _annotationsForSelection: string[];
/** /**
* Create the toolbar's container and hide it. * Create the toolbar's container and hide it.
* *
* The adder is initially hidden. * The adder is initially hidden.
* *
* @param {HTMLElement} element - The DOM element into which the adder will be created * @param element - The DOM element into which the adder will be created
* @param {AdderOptions} options - Options object specifying `onAnnotate` and `onHighlight` * @param options - Options object specifying `onAnnotate` and `onHighlight`
* event handlers. * event handlers.
*/ */
constructor(element, options) { constructor(element: HTMLElement, options: AdderOptions) {
this._outerContainer = document.createElement('hypothesis-adder'); this._outerContainer = document.createElement('hypothesis-adder');
element.appendChild(this._outerContainer); element.appendChild(this._outerContainer);
this._shadowRoot = createShadowRoot(this._outerContainer); this._shadowRoot = createShadowRoot(this._outerContainer);
...@@ -105,34 +98,15 @@ export class Adder { ...@@ -105,34 +98,15 @@ export class Adder {
left: 0, left: 0,
}); });
this._view = /** @type {Window} */ (element.ownerDocument.defaultView); this._view = element.ownerDocument.defaultView!;
this._width = () => {
const firstChild = /** @type {Element} */ (this._shadowRoot.firstChild);
return firstChild.getBoundingClientRect().width;
};
this._height = () => {
const firstChild = /** @type {Element} */ (this._shadowRoot.firstChild);
return firstChild.getBoundingClientRect().height;
};
this._isVisible = false; this._isVisible = false;
/** @type {'up'|'down'} */
this._arrowDirection = 'up'; this._arrowDirection = 'up';
this._annotationsForSelection = [];
this._onAnnotate = options.onAnnotate; this._onAnnotate = options.onAnnotate;
this._onHighlight = options.onHighlight; this._onHighlight = options.onHighlight;
this._onShowAnnotations = options.onShowAnnotations; this._onShowAnnotations = options.onShowAnnotations;
/**
* Annotation tags associated with the current selection.
*
* @type {string[]}
*/
this._annotationsForSelection = [];
this._render(); this._render();
} }
...@@ -173,13 +147,12 @@ export class Adder { ...@@ -173,13 +147,12 @@ export class Adder {
* Display the adder in the best position in order to target the * Display the adder in the best position in order to target the
* selected text in `selectionRect`. * selected text in `selectionRect`.
* *
* @param {DOMRect} selectionRect - The rect of text to target, in viewport * @param selectionRect - The rect of text to target, in viewport coordinates.
* coordinates. * @param isRTLselection - True if the selection was made right-to-left, such
* @param {boolean} isRTLselection - True if the selection was made * that the focus point is mostly likely at the top-left edge of
* rigth-to-left, such that the focus point is mosty likely at the * `targetRect`.
* top-left edge of `targetRect`.
*/ */
show(selectionRect, isRTLselection) { show(selectionRect: DOMRect, isRTLselection: boolean) {
const { left, top, arrowDirection } = this._calculateTarget( const { left, top, arrowDirection } = this._calculateTarget(
selectionRect, selectionRect,
isRTLselection isRTLselection
...@@ -187,11 +160,21 @@ export class Adder { ...@@ -187,11 +160,21 @@ export class Adder {
this._showAt(left, top); this._showAt(left, top);
this._isVisible = true; this._isVisible = true;
this._arrowDirection = arrowDirection === ARROW_POINTING_UP ? 'up' : 'down'; this._arrowDirection = arrowDirection === ArrowDirection.UP ? 'up' : 'down';
this._render(); this._render();
} }
private _width(): number {
const firstChild = this._shadowRoot.firstChild as Element;
return firstChild.getBoundingClientRect().width;
}
private _height(): number {
const firstChild = this._shadowRoot.firstChild as Element;
return firstChild.getBoundingClientRect().height;
}
/** /**
* Determine the best position for the Adder and its pointer-arrow. * Determine the best position for the Adder and its pointer-arrow.
* - Position the pointer-arrow near the end of the selection (where the user's * - Position the pointer-arrow near the end of the selection (where the user's
...@@ -200,24 +183,25 @@ export class Adder { ...@@ -200,24 +183,25 @@ export class Adder {
* - Position the Adder below the selection (arrow pointing up) for LTR selections * - Position the Adder below the selection (arrow pointing up) for LTR selections
* and above (arrow down) for RTL selections * and above (arrow down) for RTL selections
* *
* @param {DOMRect} selectionRect - The rect of text to target, in viewport * @param selectionRect - The rect of text to target, in viewport coordinates.
* coordinates. * @param isRTLselection - True if the selection was made right-to-left, such
* @param {boolean} isRTLselection - True if the selection was made * that the focus point is mostly likely at the top-left edge of
* rigth-to-left, such that the focus point is mosty likely at the * `targetRect`.
* top-left edge of `targetRect`.
* @return {Target}
*/ */
_calculateTarget(selectionRect, isRTLselection) { private _calculateTarget(
selectionRect: DOMRect,
isRTLselection: boolean
): Target {
// Set the initial arrow direction based on whether the selection was made // Set the initial arrow direction based on whether the selection was made
// forwards/upwards or downwards/backwards. // forwards/upwards or downwards/backwards.
/** @type {ArrowDirection} */ let arrowDirection; let arrowDirection: ArrowDirection;
if (isRTLselection && !isTouchDevice()) { if (isRTLselection && !isTouchDevice()) {
arrowDirection = ARROW_POINTING_DOWN; arrowDirection = ArrowDirection.DOWN;
} else { } else {
// Render the adder below the selection for touch devices due to competing // Render the adder below the selection for touch devices due to competing
// space with the native copy/paste bar that typical (not always) renders above // space with the native copy/paste bar that typical (not always) renders above
// the selection. // the selection.
arrowDirection = ARROW_POINTING_UP; arrowDirection = ArrowDirection.UP;
} }
let top; let top;
let left; let left;
...@@ -241,14 +225,14 @@ export class Adder { ...@@ -241,14 +225,14 @@ export class Adder {
// bottom of the viewport. // bottom of the viewport.
if ( if (
selectionRect.top - adderHeight < 0 && selectionRect.top - adderHeight < 0 &&
arrowDirection === ARROW_POINTING_DOWN arrowDirection === ArrowDirection.DOWN
) { ) {
arrowDirection = ARROW_POINTING_UP; arrowDirection = ArrowDirection.UP;
} else if (selectionRect.top + adderHeight > this._view.innerHeight) { } else if (selectionRect.top + adderHeight > this._view.innerHeight) {
arrowDirection = ARROW_POINTING_DOWN; arrowDirection = ArrowDirection.DOWN;
} }
if (arrowDirection === ARROW_POINTING_UP) { if (arrowDirection === ArrowDirection.UP) {
top = top =
selectionRect.top + selectionRect.top +
selectionRect.height + selectionRect.height +
...@@ -272,11 +256,11 @@ export class Adder { ...@@ -272,11 +256,11 @@ export class Adder {
* Find a Z index value that will cause the adder to appear on top of any * Find a Z index value that will cause the adder to appear on top of any
* content in the document when the adder is shown at (left, top). * content in the document when the adder is shown at (left, top).
* *
* @param {number} left - Horizontal offset from left edge of viewport. * @param left - Horizontal offset from left edge of viewport.
* @param {number} top - Vertical offset from top edge of viewport. * @param top - Vertical offset from top edge of viewport.
* @return {number} - greatest zIndex (default value of 1) * @return greatest zIndex (default value of 1)
*/ */
_findZindex(left, top) { private _findZindex(left: number, top: number): number {
if (document.elementsFromPoint === undefined) { if (document.elementsFromPoint === undefined) {
// In case of not being able to use `document.elementsFromPoint`, // In case of not being able to use `document.elementsFromPoint`,
// default to the large arbitrary number (2^15) // default to the large arbitrary number (2^15)
...@@ -317,15 +301,15 @@ export class Adder { ...@@ -317,15 +301,15 @@ export class Adder {
* Show the adder at the given position and with the arrow pointing in * Show the adder at the given position and with the arrow pointing in
* `arrowDirection`. * `arrowDirection`.
* *
* @param {number} left - Horizontal offset from left edge of viewport. * @param left - Horizontal offset from left edge of viewport.
* @param {number} top - Vertical offset from top edge of viewport. * @param top - Vertical offset from top edge of viewport.
*/ */
_showAt(left, top) { private _showAt(left: number, top: number) {
// Translate the (left, top) viewport coordinates into positions relative to // Translate the (left, top) viewport coordinates into positions relative to
// the adder's nearest positioned ancestor (NPA). // the adder's nearest positioned ancestor (NPA).
// //
// Typically the adder is a child of the `<body>` and the NPA is the root // Typically, the adder is a child of the `<body>` and the NPA is the root
// `<html>` element. However page styling may make the `<body>` positioned. // `<html>` element. However, page styling may make the `<body>` positioned.
// See https://github.com/hypothesis/client/issues/487. // See https://github.com/hypothesis/client/issues/487.
const positionedAncestor = nearestPositionedAncestor(this._outerContainer); const positionedAncestor = nearestPositionedAncestor(this._outerContainer);
const parentRect = positionedAncestor.getBoundingClientRect(); const parentRect = positionedAncestor.getBoundingClientRect();
...@@ -339,9 +323,8 @@ export class Adder { ...@@ -339,9 +323,8 @@ export class Adder {
}); });
} }
_render() { private _render() {
/** @param {import('./components/AdderToolbar').Command} command */ const handleCommand = (command: Command) => {
const handleCommand = command => {
switch (command) { switch (command) {
case 'annotate': case 'annotate':
this._onAnnotate(); this._onAnnotate();
......
import { act } from 'preact/test-utils'; import { act } from 'preact/test-utils';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { import { Adder, ArrowDirection, $imports } from '../adder';
Adder,
ARROW_POINTING_UP,
ARROW_POINTING_DOWN,
$imports,
} from '../adder';
function rect(left, top, width, height) { function rect(left, top, width, height) {
return { left, top, width, height }; return { left, top, width, height };
...@@ -199,25 +194,25 @@ describe('Adder', () => { ...@@ -199,25 +194,25 @@ describe('Adder', () => {
it('positions the adder below the selection if the selection is forwards', () => { it('positions the adder below the selection if the selection is forwards', () => {
const target = adder._calculateTarget(rect(100, 200, 100, 20), false); const target = adder._calculateTarget(rect(100, 200, 100, 20), false);
assert.isAbove(target.top, 220); assert.isAbove(target.top, 220);
assert.equal(target.arrowDirection, ARROW_POINTING_UP); assert.equal(target.arrowDirection, ArrowDirection.UP);
}); });
it('positions the adder above the selection if the selection is backwards', () => { it('positions the adder above the selection if the selection is backwards', () => {
const target = adder._calculateTarget(rect(100, 200, 100, 20), true); const target = adder._calculateTarget(rect(100, 200, 100, 20), true);
assert.isBelow(target.top, 200); assert.isBelow(target.top, 200);
assert.equal(target.arrowDirection, ARROW_POINTING_DOWN); assert.equal(target.arrowDirection, ArrowDirection.DOWN);
}); });
it('does not position the adder above the top of the viewport', () => { it('does not position the adder above the top of the viewport', () => {
const target = adder._calculateTarget(rect(100, -100, 100, 20), false); const target = adder._calculateTarget(rect(100, -100, 100, 20), false);
assert.isAtLeast(target.top, 0); assert.isAtLeast(target.top, 0);
assert.equal(target.arrowDirection, ARROW_POINTING_UP); assert.equal(target.arrowDirection, ArrowDirection.UP);
}); });
it('does not position the adder above the top of the viewport even when selection is backwards', () => { it('does not position the adder above the top of the viewport even when selection is backwards', () => {
const target = adder._calculateTarget(rect(100, -100, 100, 20), true); const target = adder._calculateTarget(rect(100, -100, 100, 20), true);
assert.isAtLeast(target.top, 0); assert.isAtLeast(target.top, 0);
assert.equal(target.arrowDirection, ARROW_POINTING_UP); assert.equal(target.arrowDirection, ArrowDirection.UP);
}); });
it('does not position the adder below the bottom of the viewport', () => { it('does not position the adder below the bottom of the viewport', () => {
...@@ -252,7 +247,7 @@ describe('Adder', () => { ...@@ -252,7 +247,7 @@ describe('Adder', () => {
}); });
const target = adder._calculateTarget(rect(100, 200, 100, 20), true); const target = adder._calculateTarget(rect(100, 200, 100, 20), true);
assert.isAbove(target.top, 220); assert.isAbove(target.top, 220);
assert.equal(target.arrowDirection, ARROW_POINTING_UP); assert.equal(target.arrowDirection, ArrowDirection.UP);
}); });
}); });
}); });
...@@ -337,7 +332,7 @@ describe('Adder', () => { ...@@ -337,7 +332,7 @@ describe('Adder', () => {
describe('#_showAt', () => { describe('#_showAt', () => {
context('when the document and body elements have no offset', () => { context('when the document and body elements have no offset', () => {
it('shows adder at target position', () => { it('shows adder at target position', () => {
adder._showAt(100, 100, ARROW_POINTING_UP); adder._showAt(100, 100, ArrowDirection.UP);
const { left, top } = adderRect(); const { left, top } = adderRect();
assert.equal(left, 100); assert.equal(left, 100);
...@@ -355,7 +350,7 @@ describe('Adder', () => { ...@@ -355,7 +350,7 @@ describe('Adder', () => {
}); });
it('shows adder at target position', () => { it('shows adder at target position', () => {
adder._showAt(100, 100, ARROW_POINTING_UP); adder._showAt(100, 100, ArrowDirection.UP);
const { left, top } = adderRect(); const { left, top } = adderRect();
assert.equal(left, 100); assert.equal(left, 100);
...@@ -373,7 +368,7 @@ describe('Adder', () => { ...@@ -373,7 +368,7 @@ describe('Adder', () => {
}); });
it('shows adder at target position when document element is offset', () => { it('shows adder at target position when document element is offset', () => {
adder._showAt(100, 100, ARROW_POINTING_UP); adder._showAt(100, 100, ArrowDirection.UP);
const { left, top } = adderRect(); const { left, top } = adderRect();
assert.equal(left, 100); assert.equal(left, 100);
...@@ -399,7 +394,7 @@ describe('Adder', () => { ...@@ -399,7 +394,7 @@ describe('Adder', () => {
describe('#hide', () => { describe('#hide', () => {
it('shows the container in the correct location', () => { it('shows the container in the correct location', () => {
adder._showAt(100, 100, ARROW_POINTING_UP); adder._showAt(100, 100, ArrowDirection.UP);
let pos = adderRect(); let pos = adderRect();
assert.equal(pos.left, 100); assert.equal(pos.left, 100);
......
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