widgets/pages.js

  1. /*
  2. * This file is part of AUX.
  3. *
  4. * AUX is free software; you can redistribute it and/or
  5. * modify it under the terms of the GNU General Public
  6. * License as published by the Free Software Foundation; either
  7. * version 3 of the License, or (at your option) any later version.
  8. *
  9. * AUX is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General
  15. * Public License along with this program; if not, write to the
  16. * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  17. * Boston, MA 02110-1301 USA
  18. */
  19. /**
  20. * The <code>useraction</code> event is emitted when a widget gets modified by user interaction.
  21. * The event is emitted for the option <code>show</code>.
  22. *
  23. * @event Pages#useraction
  24. *
  25. * @param {string} name - The name of the option which was changed due to the users action
  26. * @param {mixed} value - The new value of the option
  27. */
  28. import { addClass, toggleClass, isDomNode } from '../utils/dom.js';
  29. import { Container } from './container.js';
  30. import { ChildWidgets } from '../utils/child_widgets.js';
  31. import { defineRender } from '../renderer.js';
  32. function onPageSetActive(value) {
  33. const pages = this.parent;
  34. if (value) {
  35. const index = pages.getPages().indexOf(this);
  36. pages.showChild(this);
  37. pages.update('show', index);
  38. /**
  39. * The page to show has changed.
  40. *
  41. * @param {Page} page - The {@link Page} instance of the newly selected page.
  42. * @param {number} id - The ID of the page.
  43. *
  44. * @event Pages#changed
  45. */
  46. pages.emit('changed', this, index);
  47. } else {
  48. pages.hideChild(this);
  49. }
  50. }
  51. function onPageAdded(page, position) {
  52. const pages = this.widget;
  53. page.addClass('aux-page');
  54. page.on('set_active', onPageSetActive);
  55. const current = pages.current();
  56. if (page.get('active')) {
  57. pages.set('show', position);
  58. } else {
  59. let show = pages.get('show');
  60. // if the current active page has been moved, we have to update the
  61. // show property
  62. if (show >= position && show >= 0 && show < this.getList().length - 1) {
  63. ++show;
  64. }
  65. // update all pages active option, possibly also that of the new page
  66. pages.set('show', show);
  67. }
  68. // the new page is active
  69. if (page.get('active')) {
  70. // we don't want any animation
  71. if (current && current !== page) pages.hideChild(current);
  72. // we do not want to animate pages when they are being added
  73. if (pages.isDrawn()) page.set('visible', true);
  74. pages.showChild(page);
  75. } else {
  76. pages.hideChild(page);
  77. page.forceHide();
  78. }
  79. /**
  80. * A page was added to the Pages.
  81. *
  82. * @event Pages#added
  83. *
  84. * @param {Page} page - The {@link Page} which was added as a page.
  85. */
  86. pages.emit('added', page, position);
  87. }
  88. function onPageRemoved(page, position) {
  89. const pages = this.widget;
  90. page.removeClass('aux-page');
  91. page.off('set_active', onPageSetActive);
  92. const show = pages.get('show');
  93. const length = this.getList().length;
  94. if (position < show) {
  95. pages.set('show', show - 1);
  96. } else if (position === show) {
  97. if (show < length) {
  98. // show the next page
  99. pages.set('show', show);
  100. } else if (length) {
  101. // show the previous page
  102. pages.set('show', show - 1);
  103. } else {
  104. pages.set('show', -1);
  105. }
  106. }
  107. /**
  108. * A page was removed from the Pages
  109. *
  110. * @event Pages#removed
  111. *
  112. * @param {Page} page - The {@link Page} which was removed.
  113. * @param {number} index - The index at which the container was.
  114. */
  115. pages.emit('removed', page, position);
  116. }
  117. /**
  118. * Pages contains different pages ({@link Page}s) which can
  119. * be swiched via option.
  120. *
  121. * @class Pages
  122. *
  123. * @param {Object} [options={ }] - An object containing initial options.
  124. *
  125. * @property {Array<Page|DOMNode|String>} [options.pages=[]] -
  126. * An array of either an instance of {@link Page} (or derivate),
  127. * a DOMNode or a string of HTML which gets wrapped in a new {@link Container}.
  128. * @property {Integer} [options.show=-1] - The page to show. Set to -1 to hide all pages.
  129. * @property {String} [options.animation="horizontal"] - The direction of the
  130. * flip animation, either `horizontal` or `vertical`.
  131. *
  132. * @extends Container
  133. *
  134. * @example
  135. * var pages = new Pages({
  136. * pages: [
  137. * {
  138. * content: document.createElement("span"),
  139. * },
  140. * {
  141. * content: "<h1>Foobar</h1><p>Lorem ipsum dolor sit amet</p>",
  142. * }
  143. * ]
  144. * });
  145. */
  146. export class Pages extends Container {
  147. static get _options() {
  148. return {
  149. direction: 'string',
  150. pages: 'array',
  151. show: 'int',
  152. animation: 'string',
  153. };
  154. }
  155. static get options() {
  156. return {
  157. direction: 'forward',
  158. pages: [],
  159. show: -1,
  160. animation: 'horizontal',
  161. };
  162. }
  163. static get static_events() {
  164. return {
  165. set_show: function (value) {
  166. const list = this.pages.getList();
  167. for (let i = 0; i < list.length; i++) {
  168. const page = list[i];
  169. page.update('active', i === value);
  170. }
  171. },
  172. };
  173. }
  174. static get renderers() {
  175. return [
  176. defineRender('direction', function (direction) {
  177. const element = this.element;
  178. toggleClass(element, 'aux-forward', direction === 'forward');
  179. toggleClass(element, 'aux-backward', direction === 'backward');
  180. }),
  181. defineRender('animation', function (animation) {
  182. const element = this.element;
  183. toggleClass(element, 'aux-vertical', animation === 'vertical');
  184. toggleClass(element, 'aux-horizontal', animation === 'horizontal');
  185. }),
  186. defineRender('show', function (show) {
  187. this.getPages().forEach((page, index) => {
  188. if (index === show) {
  189. page.addClass('aux-active');
  190. } else {
  191. page.removeClass('aux-active');
  192. }
  193. });
  194. }),
  195. ];
  196. }
  197. initialize(options) {
  198. super.initialize(options);
  199. /**
  200. * The main DIV element. Has the class <code>.aux-pages</code>.
  201. *
  202. * @member Pages#element
  203. */
  204. this.pages = new ChildWidgets(this, {
  205. filter: Page,
  206. });
  207. this.pages.on('child_added', onPageAdded);
  208. this.pages.on('child_removed', onPageRemoved);
  209. }
  210. initialized() {
  211. super.initialized();
  212. this.addPages(this.options.pages);
  213. this.set('show', this.options.show);
  214. }
  215. draw(O, element) {
  216. addClass(element, 'aux-pages');
  217. super.draw(O, element);
  218. }
  219. /**
  220. * Adds an array of pages.
  221. *
  222. * @method Pages#addPages
  223. *
  224. * @property {Array<Page|DOMNode|String>} [options.pages=[]] -
  225. * An array of either an instance of {@link Page} (or derivate),
  226. * a DOMNode or a string which gets wrapped in a new {@link Page}.
  227. *
  228. * @example
  229. * var p = new Pages();
  230. * p.addPages(['foobar']);
  231. *
  232. */
  233. addPages(pages) {
  234. for (let i = 0; i < pages.length; i++) this.addPage(pages[i]);
  235. }
  236. createPage(content, options) {
  237. if (typeof content === 'string' || content === void 0) {
  238. if (!options) options = {};
  239. const page = new Page(options);
  240. page.element.innerHTML = content;
  241. return page;
  242. } else if (isDomNode(content)) {
  243. if (content.tagName === 'TEMPLATE') {
  244. content = content.content.cloneNode(true);
  245. }
  246. if (content.remove) content.remove();
  247. if (!options) options = {};
  248. const page = new Page(options);
  249. page.element.appendChild(content);
  250. return page;
  251. } else if (content instanceof Page) {
  252. return content;
  253. } else {
  254. throw new TypeError('Unexpected argument type.');
  255. }
  256. }
  257. /**
  258. * Adds a {@link Page} to the pages and a corresponding {@link Button}
  259. * to the pages {@link Navigation}.
  260. *
  261. * @method Pages#addPage
  262. *
  263. * @param {Page|DOMNode|String} content - The content of the page.
  264. * Either an instance of a {@link Page} (or derivate) widget,
  265. * a DOMNode or a string of HTML which gets wrapped in a new {@link Container}
  266. * with optional options from argument `options`.
  267. * @param {integer|undefined} position - The position to add the new
  268. * page to. If undefined, the page is added at the end.
  269. * @param {Object} [options={ }] - An object containing options for
  270. * the {@link Page} to be added as page if `content` is
  271. * either a string or a DOMNode.
  272. * @emits Pages#added
  273. */
  274. addPage(content, position, options) {
  275. const page = this.createPage(content, options);
  276. const pages = this.getPages();
  277. const element = this.element;
  278. const length = pages.length;
  279. if (position !== void 0 && typeof position !== 'number')
  280. throw new TypeError('position: Argument must be a number.');
  281. if (!(position >= 0 && position < length)) {
  282. element.appendChild(page.element);
  283. } else {
  284. element.insertBefore(page.element, pages[position].element);
  285. }
  286. if (page.parent !== this) {
  287. // if this page is a web component, the above appendChild would have
  288. // already triggered a call to addChild
  289. this.addChild(page);
  290. }
  291. return page;
  292. }
  293. /**
  294. * Removes a page from the Pages.
  295. *
  296. * @method Pages#removePage
  297. *
  298. * @param {integer|Page} page - The container to remove. Either an
  299. * index or the {@link Page} widget generated by <code>addPage</code>.
  300. * @param {Boolean} destroy - destroy the {@link Page} after removal.
  301. *
  302. * @emits Pages#removed
  303. */
  304. removePage(page, destroy) {
  305. let position = -1;
  306. if (page instanceof Page) {
  307. position = this.pages.indexOf(page);
  308. } else if (typeof page === 'number') {
  309. position = page;
  310. page = this.pages.at(position);
  311. }
  312. if (!page || position === -1) throw new Error('Unknown page.');
  313. this.element.removeChild(page.element);
  314. if (this.pages.at(position) === page) {
  315. // NOTE: if we remove a child which is a web component,
  316. // it will itself call removeChild
  317. this.removeChild(page);
  318. }
  319. if (destroy) {
  320. page.destroyAndRemove();
  321. }
  322. }
  323. /**
  324. * Removes all pages.
  325. *
  326. * @method Pages#empty
  327. */
  328. empty() {
  329. while (this.getPages().length) this.removePage(0);
  330. }
  331. current() {
  332. /**
  333. * Returns the currently displayed page or null.
  334. *
  335. * @method Pages#current
  336. */
  337. return this.pages.at(this.options.show) || null;
  338. }
  339. /**
  340. * Opens the first page of the pages. Returns <code>true</code> if a
  341. * first page exists, <code>false</code> otherwise.
  342. *
  343. * @method Pages#first
  344. *
  345. * @returns {Boolean} True if successful, false otherwise.
  346. */
  347. first() {
  348. if (this.getPages().length) {
  349. this.set('show', 0);
  350. return true;
  351. }
  352. return false;
  353. }
  354. /**
  355. * Opens the last page of the pages. Returns <code>true</code> if a
  356. * last page exists, <code>false</code> otherwise.
  357. *
  358. * @method Pages#last
  359. *
  360. * @returns {Boolean} True if successful, false otherwise.
  361. */
  362. last() {
  363. const length = this.getPages().length;
  364. if (length) {
  365. this.set('show', length - 1);
  366. return true;
  367. }
  368. return false;
  369. }
  370. /**
  371. * Opens the next page of the pages. Returns <code>true</code> if a
  372. * next page exists, <code>false</code> otherwise.
  373. *
  374. * @method Pages#next
  375. *
  376. * @returns {Boolean} True if successful, false otherwise.
  377. */
  378. next() {
  379. const show = this.options.show;
  380. const length = this.getPages().length;
  381. if (show + 1 < length) {
  382. this.set('show', show + 1);
  383. return true;
  384. }
  385. return false;
  386. }
  387. /**
  388. * Opens the previous page of the pages. Returns <code>true</code> if a
  389. * previous page exists, <code>false</code> otherwise.
  390. *
  391. * @method Pages#prev
  392. *
  393. * @returns {Boolean} True if successful, false otherwise.
  394. */
  395. prev() {
  396. const show = this.options.show;
  397. const length = this.getPages().length;
  398. if (show === 0) return false;
  399. this.set('show', show - 1);
  400. return show - 1 < length;
  401. }
  402. set(key, value) {
  403. if (key === 'show') {
  404. if (value !== this.options.show) {
  405. if (value > this.options.show) {
  406. this.set('direction', 'forward');
  407. } else {
  408. this.set('direction', 'backward');
  409. }
  410. }
  411. } else if (key === 'pages') {
  412. this.options.pages.forEach((page) => this.removePage(page, true));
  413. value = this.addPages(value || []);
  414. }
  415. return super.set(key, value);
  416. }
  417. getPages() {
  418. return this.pages.getList();
  419. }
  420. destroy() {
  421. this.empty();
  422. super.destroy();
  423. }
  424. }
  425. /**
  426. * Page is the child widget to be used in {@link Pages}.
  427. *
  428. * @class Page
  429. *
  430. * @param {Object} [options={ }] - An object containing initial options.
  431. *
  432. * @property {String} [options.label=""] - The label of the pages corresponding button
  433. * @property {Number} [options.hiding_duration=-1] - Default to auto-determine hiding duration from style.
  434. * @property {Number} [options.showing_duration=-1] - Default to auto-determine showing duration from style.
  435. *
  436. * @extends Container
  437. */
  438. export class Page extends Container {
  439. static get _options() {
  440. return {
  441. label: 'string',
  442. icon: 'string',
  443. };
  444. }
  445. static get options() {
  446. return {
  447. label: '',
  448. icon: '',
  449. hiding_duration: -1,
  450. showing_duration: -1,
  451. role: 'tabpanel',
  452. active: false,
  453. };
  454. }
  455. }