/*
* 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
*/
/**
* The <code>useraction</code> event is emitted when a widget gets modified by user interaction.
* The event is emitted for the option <code>show</code>.
*
* @event Pages#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
*/
import { addClass, toggleClass, isDomNode } from '../utils/dom.js';
import { Container } from './container.js';
import { ChildWidgets } from '../utils/child_widgets.js';
import { defineRender } from '../renderer.js';
function onPageSetActive(value) {
const pages = this.parent;
if (value) {
const index = pages.getPages().indexOf(this);
pages.showChild(this);
pages.update('show', index);
/**
* The page to show has changed.
*
* @param {Page} page - The {@link Page} instance of the newly selected page.
* @param {number} id - The ID of the page.
*
* @event Pages#changed
*/
pages.emit('changed', this, index);
} else {
pages.hideChild(this);
}
}
function onPageAdded(page, position) {
const pages = this.widget;
page.addClass('aux-page');
page.on('set_active', onPageSetActive);
const current = pages.current();
if (page.get('active')) {
pages.set('show', position);
} else {
let show = pages.get('show');
// if the current active page has been moved, we have to update the
// show property
if (show >= position && show >= 0 && show < this.getList().length - 1) {
++show;
}
// update all pages active option, possibly also that of the new page
pages.set('show', show);
}
// the new page is active
if (page.get('active')) {
// we don't want any animation
if (current && current !== page) pages.hideChild(current);
// we do not want to animate pages when they are being added
if (pages.isDrawn()) page.set('visible', true);
pages.showChild(page);
} else {
pages.hideChild(page);
page.forceHide();
}
/**
* A page was added to the Pages.
*
* @event Pages#added
*
* @param {Page} page - The {@link Page} which was added as a page.
*/
pages.emit('added', page, position);
}
function onPageRemoved(page, position) {
const pages = this.widget;
page.removeClass('aux-page');
page.off('set_active', onPageSetActive);
const show = pages.get('show');
const length = this.getList().length;
if (position < show) {
pages.set('show', show - 1);
} else if (position === show) {
if (show < length) {
// show the next page
pages.set('show', show);
} else if (length) {
// show the previous page
pages.set('show', show - 1);
} else {
pages.set('show', -1);
}
}
/**
* A page was removed from the Pages
*
* @event Pages#removed
*
* @param {Page} page - The {@link Page} which was removed.
* @param {number} index - The index at which the container was.
*/
pages.emit('removed', page, position);
}
/**
* Pages contains different pages ({@link Page}s) which can
* be swiched via option.
*
* @class Pages
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {Array<Page|DOMNode|String>} [options.pages=[]] -
* An array of either an instance of {@link Page} (or derivate),
* a DOMNode or a string of HTML which gets wrapped in a new {@link Container}.
* @property {Integer} [options.show=-1] - The page to show. Set to -1 to hide all pages.
* @property {String} [options.animation="horizontal"] - The direction of the
* flip animation, either `horizontal` or `vertical`.
*
* @extends Container
*
* @example
* var pages = new Pages({
* pages: [
* {
* content: document.createElement("span"),
* },
* {
* content: "<h1>Foobar</h1><p>Lorem ipsum dolor sit amet</p>",
* }
* ]
* });
*/
export class Pages extends Container {
static get _options() {
return {
direction: 'string',
pages: 'array',
show: 'int',
animation: 'string',
};
}
static get options() {
return {
direction: 'forward',
pages: [],
show: -1,
animation: 'horizontal',
};
}
static get static_events() {
return {
set_show: function (value) {
const list = this.pages.getList();
for (let i = 0; i < list.length; i++) {
const page = list[i];
page.update('active', i === value);
}
},
};
}
static get renderers() {
return [
defineRender('direction', function (direction) {
const element = this.element;
toggleClass(element, 'aux-forward', direction === 'forward');
toggleClass(element, 'aux-backward', direction === 'backward');
}),
defineRender('animation', function (animation) {
const element = this.element;
toggleClass(element, 'aux-vertical', animation === 'vertical');
toggleClass(element, 'aux-horizontal', animation === 'horizontal');
}),
defineRender('show', function (show) {
this.getPages().forEach((page, index) => {
if (index === show) {
page.addClass('aux-active');
} else {
page.removeClass('aux-active');
}
});
}),
];
}
initialize(options) {
super.initialize(options);
/**
* The main DIV element. Has the class <code>.aux-pages</code>.
*
* @member Pages#element
*/
this.pages = new ChildWidgets(this, {
filter: Page,
});
this.pages.on('child_added', onPageAdded);
this.pages.on('child_removed', onPageRemoved);
}
initialized() {
super.initialized();
this.addPages(this.options.pages);
this.set('show', this.options.show);
}
draw(O, element) {
addClass(element, 'aux-pages');
super.draw(O, element);
}
/**
* Adds an array of pages.
*
* @method Pages#addPages
*
* @property {Array<Page|DOMNode|String>} [options.pages=[]] -
* An array of either an instance of {@link Page} (or derivate),
* a DOMNode or a string which gets wrapped in a new {@link Page}.
*
* @example
* var p = new Pages();
* p.addPages(['foobar']);
*
*/
addPages(pages) {
for (let i = 0; i < pages.length; i++) this.addPage(pages[i]);
}
createPage(content, options) {
if (typeof content === 'string' || content === void 0) {
if (!options) options = {};
const page = new Page(options);
page.element.innerHTML = content;
return page;
} else if (isDomNode(content)) {
if (content.tagName === 'TEMPLATE') {
content = content.content.cloneNode(true);
}
if (content.remove) content.remove();
if (!options) options = {};
const page = new Page(options);
page.element.appendChild(content);
return page;
} else if (content instanceof Page) {
return content;
} else {
throw new TypeError('Unexpected argument type.');
}
}
/**
* Adds a {@link Page} to the pages and a corresponding {@link Button}
* to the pages {@link Navigation}.
*
* @method Pages#addPage
*
* @param {Page|DOMNode|String} content - The content of the page.
* Either an instance of a {@link Page} (or derivate) widget,
* a DOMNode or a string of HTML which gets wrapped in a new {@link Container}
* with optional options from argument `options`.
* @param {integer|undefined} position - The position to add the new
* page to. If undefined, the page is added at the end.
* @param {Object} [options={ }] - An object containing options for
* the {@link Page} to be added as page if `content` is
* either a string or a DOMNode.
* @emits Pages#added
*/
addPage(content, position, options) {
const page = this.createPage(content, options);
const pages = this.getPages();
const element = this.element;
const length = pages.length;
if (position !== void 0 && typeof position !== 'number')
throw new TypeError('position: Argument must be a number.');
if (!(position >= 0 && position < length)) {
element.appendChild(page.element);
} else {
element.insertBefore(page.element, pages[position].element);
}
if (page.parent !== this) {
// if this page is a web component, the above appendChild would have
// already triggered a call to addChild
this.addChild(page);
}
return page;
}
/**
* Removes a page from the Pages.
*
* @method Pages#removePage
*
* @param {integer|Page} page - The container to remove. Either an
* index or the {@link Page} widget generated by <code>addPage</code>.
* @param {Boolean} destroy - destroy the {@link Page} after removal.
*
* @emits Pages#removed
*/
removePage(page, destroy) {
let position = -1;
if (page instanceof Page) {
position = this.pages.indexOf(page);
} else if (typeof page === 'number') {
position = page;
page = this.pages.at(position);
}
if (!page || position === -1) throw new Error('Unknown page.');
this.element.removeChild(page.element);
if (this.pages.at(position) === page) {
// NOTE: if we remove a child which is a web component,
// it will itself call removeChild
this.removeChild(page);
}
if (destroy) {
page.destroyAndRemove();
}
}
/**
* Removes all pages.
*
* @method Pages#empty
*/
empty() {
while (this.getPages().length) this.removePage(0);
}
current() {
/**
* Returns the currently displayed page or null.
*
* @method Pages#current
*/
return this.pages.at(this.options.show) || null;
}
/**
* Opens the first page of the pages. Returns <code>true</code> if a
* first page exists, <code>false</code> otherwise.
*
* @method Pages#first
*
* @returns {Boolean} True if successful, false otherwise.
*/
first() {
if (this.getPages().length) {
this.set('show', 0);
return true;
}
return false;
}
/**
* Opens the last page of the pages. Returns <code>true</code> if a
* last page exists, <code>false</code> otherwise.
*
* @method Pages#last
*
* @returns {Boolean} True if successful, false otherwise.
*/
last() {
const length = this.getPages().length;
if (length) {
this.set('show', length - 1);
return true;
}
return false;
}
/**
* Opens the next page of the pages. Returns <code>true</code> if a
* next page exists, <code>false</code> otherwise.
*
* @method Pages#next
*
* @returns {Boolean} True if successful, false otherwise.
*/
next() {
const show = this.options.show;
const length = this.getPages().length;
if (show + 1 < length) {
this.set('show', show + 1);
return true;
}
return false;
}
/**
* Opens the previous page of the pages. Returns <code>true</code> if a
* previous page exists, <code>false</code> otherwise.
*
* @method Pages#prev
*
* @returns {Boolean} True if successful, false otherwise.
*/
prev() {
const show = this.options.show;
const length = this.getPages().length;
if (show === 0) return false;
this.set('show', show - 1);
return show - 1 < length;
}
set(key, value) {
if (key === 'show') {
if (value !== this.options.show) {
if (value > this.options.show) {
this.set('direction', 'forward');
} else {
this.set('direction', 'backward');
}
}
} else if (key === 'pages') {
this.options.pages.forEach((page) => this.removePage(page, true));
value = this.addPages(value || []);
}
return super.set(key, value);
}
getPages() {
return this.pages.getList();
}
destroy() {
this.empty();
super.destroy();
}
}
/**
* Page is the child widget to be used in {@link Pages}.
*
* @class Page
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {String} [options.label=""] - The label of the pages corresponding button
* @property {Number} [options.hiding_duration=-1] - Default to auto-determine hiding duration from style.
* @property {Number} [options.showing_duration=-1] - Default to auto-determine showing duration from style.
*
* @extends Container
*/
export class Page extends Container {
static get _options() {
return {
label: 'string',
icon: 'string',
};
}
static get options() {
return {
label: '',
icon: '',
hiding_duration: -1,
showing_duration: -1,
role: 'tabpanel',
active: false,
};
}
}