Source RapidContext_Widget_TabContainer.js

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2023 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15// Namespace initialization
16if (typeof(RapidContext) == "undefined") {
17    RapidContext = {};
18}
19RapidContext.Widget = RapidContext.Widget || { Classes: {} };
20
21/**
22 * Creates a new tab container widget.
23 *
24 * @constructor
25 * @param {Object} attrs the widget and node attributes
26 * @param {boolean} [attrs.hidden] the hidden widget flag, defaults to false
27 * @param {...Pane} [child] the child Pane widgets
28 *
29 * @return {Widget} the widget DOM node
30 *
31 * @class The tab container widget class. Used to provide a set of tabbed
32 *     pages, where the user can switch page freely. Internally it uses a
33 *     `<div>` HTML element containing `Pane` widgets that are hidden and shown
34 *     according to the page transitions. If a child `Pane` widget is
35 *     `pageCloseable`, a close button will be available on the tab label.
36 * @extends RapidContext.Widget
37 *
38 * @example <caption>JavaScript</caption>
39 * let page1 = RapidContext.Widget.Pane({ pageTitle: "One" });
40 * ...
41 * let page2 = RapidContext.Widget.Pane({ pageTitle: "Two", pageCloseable: true });
42 * ...
43 * let attrs = { style: { width: "100%", height: "100%" } };
44 * let tabs = RapidContext.Widget.TabContainer(attrs, page1, page2);
45 *
46 * @example <caption>User Interface XML</caption>
47 * <TabContainer style="width: 100%; height: 100%;">
48 *   <Pane pageTitle="One">
49 *     ...
50 *   </Pane>
51 *   <Pane pageTitle="Two" pageCloseable="true">
52 *     ...
53 *   </Pane>
54 * <TabContainer>
55 */
56RapidContext.Widget.TabContainer = function (attrs/*, ... */) {
57    let labels = MochiKit.DOM.DIV({ "class": "widgetTabContainerLabels" });
58    let container = MochiKit.DOM.DIV({ "class": "widgetTabContainerContent" });
59    let o = MochiKit.DOM.DIV(attrs, labels, container);
60    RapidContext.Widget._widgetMixin(o, RapidContext.Widget.TabContainer);
61    o.classList.add("widgetTabContainer");
62    RapidContext.Util.registerSizeConstraints(container, "100% - 20", "100% - 40");
63    o.resizeContent = o._resizeContent;
64    container.resizeContent = () => {};
65    o._selectedIndex = -1;
66    o.setAttrs(attrs);
67    o.addAll(Array.from(arguments).slice(1));
68    o.on("click", ".widgetTabContainerLabel", o._handleLabelClick);
69    return o;
70};
71
72// Register widget class
73RapidContext.Widget.Classes.TabContainer = RapidContext.Widget.TabContainer;
74
75/**
76 * Handles tab label click events.
77 *
78 * @param {Event} evt the DOM Event object
79 */
80RapidContext.Widget.TabContainer.prototype._handleLabelClick = function (evt) {
81    let label = evt.delegateTarget;
82    let pos = label ? Array.from(label.parentNode.children).indexOf(label) : -1;
83    let child = this.getChildNodes()[pos];
84    if (child) {
85        evt.preventDefault();
86        evt.stopImmediatePropagation();
87        if (evt.target.dataset.close) {
88            this.removeChildNode(child);
89        } else {
90            this.selectChild(child);
91        }
92    }
93};
94
95/**
96 * Returns an array with all child pane widgets. Note that the array
97 * is a real JavaScript array, not a dynamic `NodeList`.
98 *
99 * @return {Array} the array of child DOM nodes
100 */
101RapidContext.Widget.TabContainer.prototype.getChildNodes = function () {
102    return Array.from(this.lastChild.childNodes);
103};
104
105/**
106 * Adds a single child page widget to this widget. The child widget
107 * should be a `RapidContext.Widget.Pane` widget, or it will be added to a
108 * new one.
109 *
110 * @param {Widget} child the page widget to add
111 *
112 * @see RapidContext.Widget.Pane
113 */
114RapidContext.Widget.TabContainer.prototype.addChildNode = function (child) {
115    if (!RapidContext.Widget.isWidget(child, "Pane")) {
116        child = RapidContext.Widget.Pane(null, child);
117    }
118    RapidContext.Util.registerSizeConstraints(child, "100%", "100%");
119    child.hide();
120    let text = MochiKit.DOM.SPAN(null, child.pageTitle);
121    let icon = null;
122    if (child.pageCloseable) {
123        icon = RapidContext.Widget.Icon({ "class": "fa fa-close", tooltip: "Close" });
124        icon.dataset.close = true;
125    }
126    let labelAttrs = { "class": "widgetTabContainerLabel" };
127    let label = MochiKit.DOM.DIV(labelAttrs, MochiKit.DOM.DIV({}, text, icon));
128    this.firstChild.append(label);
129    this.lastChild.append(child);
130    if (this._selectedIndex < 0) {
131        this.selectChild(0);
132    }
133};
134
135/**
136 * Removes a single child DOM node from this widget. This method is
137 * sometimes overridden by child widgets in order to hide or control
138 * intermediate DOM nodes required by the widget.
139 *
140 * Note that this method will NOT destroy the removed child widget,
141 * so care must be taken to ensure proper child widget destruction.
142 *
143 * @param {Widget|Node} child the DOM node to remove
144 */
145RapidContext.Widget.TabContainer.prototype.removeChildNode = function (child) {
146    let index = this.getChildNodes().indexOf(child);
147    if (index < 0) {
148        throw new Error("Cannot remove DOM node that is not a TabContainer child");
149    }
150    if (this._selectedIndex == index) {
151        child._handleExit();
152        this._selectedIndex = -1;
153    }
154    RapidContext.Widget.destroyWidget(this.firstChild.childNodes[index]);
155    child.remove();
156    child.emit && child.emit("close");
157    if (this._selectedIndex > index) {
158        this._selectedIndex--;
159    }
160    if (this._selectedIndex < 0 && this.getChildNodes().length > 0) {
161        this.selectChild((index == 0) ? 0 : index - 1);
162    }
163};
164
165// TODO: add support for status updates in child pane widget
166
167/**
168 * Returns the index of the currently selected child in the tab
169 * container.
170 *
171 * @return {number} the index of the selected child, or
172 *         -1 if no child is selected
173 */
174RapidContext.Widget.TabContainer.prototype.selectedIndex = function () {
175    return this._selectedIndex;
176};
177
178/**
179 * Returns the child widget currently selected in the tab container.
180 *
181 * @return {Node} the child widget selected, or
182 *         null if no child is selected
183 */
184RapidContext.Widget.TabContainer.prototype.selectedChild = function () {
185    let children = this.getChildNodes();
186    return (this._selectedIndex < 0) ? null : children[this._selectedIndex];
187};
188
189/**
190 * Selects a specified child in the tab container. This method can be
191 * called without arguments to re-select the currently selected tab.
192 *
193 * @param {number|Node} [indexOrChild] the child index or node
194 */
195RapidContext.Widget.TabContainer.prototype.selectChild = function (indexOrChild) {
196    let children = this.getChildNodes();
197    let label;
198    if (this._selectedIndex >= 0) {
199        label = this.firstChild.childNodes[this._selectedIndex];
200        label.classList.remove("selected");
201        children[this._selectedIndex]._handleExit();
202    }
203    let index = -1;
204    if (indexOrChild == null) {
205        index = this._selectedIndex;
206    } else if (typeof(indexOrChild) == "number") {
207        index = indexOrChild;
208    } else {
209        index = children.indexOf(indexOrChild);
210    }
211    this._selectedIndex = (index < 0 || index >= children.length) ? -1 : index;
212    if (this._selectedIndex >= 0) {
213        label = this.firstChild.childNodes[this._selectedIndex];
214        label.classList.add("selected");
215        children[this._selectedIndex]._handleEnter();
216    }
217};
218
219/**
220 * Resizes the currently selected child. This method need not be called
221 * directly, but is automatically called whenever a parent node is
222 * resized. It optimizes the resize chain by only resizing those DOM
223 * child nodes that are visible, i.e. the currently selected tab
224 * container child.
225 */
226RapidContext.Widget.TabContainer.prototype._resizeContent = function () {
227    RapidContext.Util.resizeElements(this.lastChild);
228    let child = this.selectedChild();
229    if (child != null) {
230        RapidContext.Util.resizeElements(child);
231    }
232};
233