/*
* This file is part of AUX.
*
* AUX is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* AUX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General
* Public License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*/
import { S } from '../dom_scheduler.js';
import { defineChildWidget } from '../child_widget.js';
import {
addClass,
removeClass,
toggleClass,
innerHeight,
innerWidth,
} from '../utils/dom.js';
import { Button } from './button.js';
import { Buttons } from './buttons.js';
import { Container } from './container.js';
function easeLinear(t) {
return t;
}
function easeInOut(t) {
if (t < 0.5) {
return 2 * t * t;
} else {
return 1 - Math.pow(-2 * t + 2, 2) / 2;
}
}
class ScrollAnimation {
constructor(options) {
this.element = options.element;
this.duration = options.duration;
if (this.duration < 0) {
this.duration = 0;
}
this.from = options.from;
this.to = options.to;
this.vertical = options.vertical;
this.easing = options.easing || easeLinear;
this.startTime = performance.now();
this._draw = () => {
let t =
this.duration > 0
? (performance.now() - this.startTime) / this.duration
: 1;
// catch NaN
if (t < 0) t = 0;
else if (t > 1) t = 1;
const pos = this.from + this.easing(t) * (this.to - this.from);
if (this.vertical) {
this.element.scrollTop = pos;
} else {
this.element.scrollLeft = pos;
}
if (t < 1) {
S.addNext(this._draw);
}
};
this.start();
}
stop() {
S.remove(this._draw);
}
pause() {
S.remove(this._draw);
}
start() {
S.add(this._draw);
}
}
/**
* The <code>useraction</code> event is emitted when a widget gets modified by user interaction.
* The event is emitted for the option <code>select</code>.
*
* @event Navigation#useraction
* @param {string} name - The name of the option which was changed due to the users action
* @param {mixed} value - The new value of the option
*/
function autoArrows() {
const O = this.options;
if (!O.auto_arrows) return;
const B = this.buttons.getButtons();
const vert = O.direction === 'vertical';
const cons = vert
? innerHeight(this.buttons.element)
: innerWidth(this.buttons.element);
let list;
if (B.length) {
const lastb = B[B.length - 1].element;
const rect = lastb.getBoundingClientRect();
list =
lastb[vert ? 'offsetTop' : 'offsetLeft'] +
rect[vert ? 'height' : 'width'];
} else {
list = 0;
}
this.update('arrows', list > cons);
}
function prevClicked() {
this.userset('select', Math.max(0, this.options.select - 1));
}
function prevDblClicked() {
this.userset('select', 0);
}
function nextClicked() {
this.userset(
'select',
Math.min(this.buttons.getButtons().length - 1, this.options.select + 1)
);
}
function nextDblClicked() {
this.userset('select', this.buttons.getButtons().length - 1);
}
/**
* Navigation is a {@link Container} including a {@Buttons} widget for e.g. navigating between
* pages inside a {@link Pager}. It keeps the currently highlighted {@link Button}
* inside the visible area and adds previous and next {@link Button}s
* if needed.
*
* @extends Container
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {String} [options.direction="horizontal"] - The layout of
* the Navigation, either `horizontal` or `vertical`.
* @property {Boolean} [options.arrows=false] - Show or hide previous and next {@link Button}s.
* @property {Boolean} [options.auto_arrows=true] - Set to false to disable
* automatic creation of the previous/next buttons.
* @property {Integer} [options.scroll=500] - Duration of the scrolling animation.
*
* @class Navigation
*/
export class Navigation extends Container {
static get _options() {
return Object.assign({}, Container.getOptionTypes(), {
_clip_width: 'number',
_clip_height: 'number',
_list_width: 'number',
_list_height: 'number',
_button_positions: 'object',
direction: 'string',
arrows: 'boolean',
auto_arrows: 'boolean',
resized: 'boolean',
scroll: 'int',
});
}
static get options() {
return {
direction: 'horizontal',
arrows: false,
auto_arrows: true,
resized: false,
scroll: 500,
};
}
static get static_events() {
return {
set_direction: function (value) {
this.prev.set('icon', value === 'vertical' ? 'arrowup' : 'arrowleft'); //"\u25B2" : "\u25C0");
this.next.set(
'icon',
value === 'vertical' ? 'arrowdown' : 'arrowright'
); //"\u25BC" : "\u25B6");
},
hide: function () {
if (this._scroll_animation) {
this._scroll_animation.pause();
}
},
show: function () {
if (this._scroll_animation) {
this._scroll_animation.start();
}
},
set_select: function (val) {
this.prev.set('disabled', val <= 0);
this.next.set('disabled', val == this.buttons.getButtons().length - 1);
},
};
}
_getButtonScrollPosition() {
const O = this.options;
const show = O.select;
const button_list = this.buttons.getButtons();
if (show < 0 || show >= button_list.length) return 0;
const button_position = O._button_positions.get(button_list[show]);
if (!button_position) return 0;
const is_vertical = O.direction === 'vertical';
const clip_size = is_vertical ? O._clip_height : O._clip_width;
const list_size = is_vertical ? O._list_height : O._list_width;
const offset = is_vertical ? button_position.top : button_position.left;
const button_size = is_vertical
? button_position.height
: button_position.width;
/*
console.log(O, 'clip_size', clip_size, 'list_size', list_size, 'offset', offset,
'button_size', button_size);
console.log('innerHeight', innerHeight(this.buttons.element));
*/
let pos = Math.min(
offset + button_size / 2 - clip_size / 2,
list_size - clip_size
);
if (pos < 0) pos = 0;
return pos;
}
initialize(options) {
super.initialize(options);
/**
* @member {HTMLDivElement} Navigation#element - The main DIV container.
* Has class <code>.aux-navigation</code>.
*/
/**
* @member {Button} Navigation#prev - The previous arrow {@link Button} instance.
*/
this.prev = new Button({ class: 'aux-previous', dblclick: 400, tabindex: false });
/**
* @member {Button} Navigation#next - The next arrow {@link Button} instance.
*/
this.next = new Button({ class: 'aux-next', dblclick: 400, tabindex: false });
this.prev.on('click', prevClicked.bind(this));
this.prev.on('doubleclick', prevDblClicked.bind(this));
this.next.on('click', nextClicked.bind(this));
this.next.on('doubleclick', nextDblClicked.bind(this));
// these properties contain the scroll position of the buttons child
// element
this._scroll_left = 0;
this._scroll_top = 0;
this.set('_button_positions', new Map());
this.set('auto_arrows', this.options.auto_arrows);
this.set('direction', this.options.direction);
}
initialized() {
super.initialized();
const measure_clip = () => {
const buttons = this.buttons;
this.update('_clip_height', innerHeight(buttons.element));
this.update('_clip_width', innerWidth(buttons.element));
};
this.addSubscriptions(
this.buttons.buttons.forEachAsync((button) => {
let measured = false;
const positions = this.get('_button_positions');
const updateLength = () => {
let list_width = 0;
let list_height = 0;
positions.forEach((info) => {
list_width = Math.max(list_width, info.width + info.left);
list_height = Math.max(list_height, info.height + info.top);
});
this.update('_list_width', list_width);
this.update('_list_height', list_height);
};
const info = {
width: 0,
height: 0,
left: 0,
top: 0,
};
const measure = (_button) => {
const element = _button.element;
const bounding_box = element.getBoundingClientRect();
info.width = bounding_box.width;
info.height = bounding_box.height;
info.left = element.offsetLeft;
info.top = element.offsetTop;
if (!measured) {
measured = true;
positions.set(_button, info);
}
updateLength();
this.invalidate('_button_positions');
};
let sub = button.observeResize(measure);
return () => {
if (!sub) return;
sub();
sub = null;
if (measured) {
positions.delete(button);
updateLength();
this.invalidate('_button_positions');
}
};
}),
this.buttons.observeResize(measure_clip),
this.next.observeResize(measure_clip),
this.prev.observeResize(measure_clip),
this.buttons.subscribe('scroll', () => {
this._scroll_top = this.buttons.element.scrollTop;
this._scroll_left = this.buttons.element.scrollLeft;
})
);
}
resize() {
autoArrows.call(this);
super.resize();
}
draw(O, element) {
addClass(element, 'aux-navigation');
super.draw(O, element);
}
redraw() {
const O = this.options;
const I = this.invalid;
const E = this.element;
if (I.direction) {
removeClass(E, 'aux-vertical', 'aux-horizontal');
addClass(E, 'aux-' + O.direction);
}
if (I.validate('arrows')) {
if (O.arrows) {
if (!this.prev.element.parentElement) {
E.appendChild(this.prev.element);
E.appendChild(this.next.element);
this.addChild(this.prev);
this.addChild(this.next);
}
} else {
if (this.prev.element.parentElement) {
E.removeChild(this.prev.element);
E.removeChild(this.next.element);
this.removeChild(this.prev);
this.removeChild(this.next);
}
}
toggleClass(E, 'aux-over', O.arrows);
}
if (
I.validate(
'select',
'direction',
'_list_width',
'_list_height',
'_clip_width',
'_clip_height',
'_button_positions'
)
) {
if (this._scroll_animation) {
this._scroll_animation.stop();
this._scroll_animation = null;
}
const position = this._getButtonScrollPosition();
const is_vertical = O.direction === 'vertical';
const from = is_vertical ? this._scroll_top : this._scroll_left;
if (position !== from) {
this._scroll_animation = new ScrollAnimation({
element: this.buttons.element,
duration: O.scroll,
from: from,
to: position,
easing: easeInOut,
vertical: is_vertical,
});
}
}
super.redraw();
}
addButton(...arg) {
return this.buttons.addButton(...arg);
}
addButtons(...arg) {
return this.buttons.addButtons(...arg);
}
removeButton(...arg) {
return this.buttons.removeButton(...arg);
}
empty(...arg) {
return this.buttons.empty(...arg);
}
destroy() {
super.destroy();
if (this._scroll_animation) {
this._scroll_animation.stop();
this._scroll_animation = null;
}
}
}
/**
* @member {Buttons} Navigation#buttons - The {@link Buttons} of the Navigation.
*/
defineChildWidget(Navigation, 'buttons', {
create: Buttons,
show: true,
inherit_options: true,
userset_delegate: true,
});