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
RapidContext
Access · Discovery · Insight
www.rapidcontext.com