1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 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 = RapidContext.UI.DIV({ "class": "widgetTabContainerLabels" });
58 let container = RapidContext.UI.DIV({ "class": "widgetTabContainerContent" });
59 let o = RapidContext.UI.DIV(attrs, labels, container);
60 RapidContext.Widget._widgetMixin(o, RapidContext.Widget.TabContainer);
61 o.classList.add("widgetTabContainer");
62 o._selectedIndex = -1;
63 o.setAttrs(attrs);
64 o.addAll(Array.from(arguments).slice(1));
65 o.on("click", ".widgetTabContainerLabel", o._handleLabelClick);
66 return o;
67};
68
69// Register widget class
70RapidContext.Widget.Classes.TabContainer = RapidContext.Widget.TabContainer;
71
72/**
73 * Handles tab label click events.
74 *
75 * @param {Event} evt the DOM Event object
76 */
77RapidContext.Widget.TabContainer.prototype._handleLabelClick = function (evt) {
78 let label = evt.delegateTarget;
79 let pos = label ? Array.from(label.parentNode.children).indexOf(label) : -1;
80 let child = this.getChildNodes()[pos];
81 if (child) {
82 evt.preventDefault();
83 evt.stopImmediatePropagation();
84 if (evt.target.dataset.close) {
85 this.removeChildNode(child);
86 } else {
87 this.selectChild(child);
88 }
89 }
90};
91
92/**
93 * Returns an array with all child pane widgets. Note that the array
94 * is a real JavaScript array, not a dynamic `NodeList`.
95 *
96 * @return {Array} the array of child DOM nodes
97 */
98RapidContext.Widget.TabContainer.prototype.getChildNodes = function () {
99 return Array.from(this.lastChild.childNodes);
100};
101
102/**
103 * Adds a single child page widget to this widget. The child widget
104 * should be a `RapidContext.Widget.Pane` widget, or it will be added to a
105 * new one.
106 *
107 * @param {Widget} child the page widget to add
108 *
109 * @see RapidContext.Widget.Pane
110 */
111RapidContext.Widget.TabContainer.prototype.addChildNode = function (child) {
112 if (!RapidContext.Widget.isWidget(child, "Pane")) {
113 child = RapidContext.Widget.Pane(null, child);
114 }
115 child.style.width = child.style.height = "100%";
116 child.hide();
117 let text = RapidContext.UI.SPAN({}, child.pageTitle);
118 let icon = null;
119 if (child.pageCloseable) {
120 icon = RapidContext.Widget.Icon({ "class": "fa fa-close", tooltip: "Close" });
121 icon.dataset.close = true;
122 }
123 let labelAttrs = { "class": "widgetTabContainerLabel" };
124 let label = RapidContext.UI.DIV(labelAttrs, RapidContext.UI.DIV({}, text, icon));
125 this.firstChild.append(label);
126 this.lastChild.append(child);
127 if (this._selectedIndex < 0) {
128 this.selectChild(0);
129 }
130};
131
132/**
133 * Removes a single child DOM node from this widget. This method is
134 * sometimes overridden by child widgets in order to hide or control
135 * intermediate DOM nodes required by the widget.
136 *
137 * Note that this method will NOT destroy the removed child widget,
138 * so care must be taken to ensure proper child widget destruction.
139 *
140 * @param {Widget|Node} child the DOM node to remove
141 */
142RapidContext.Widget.TabContainer.prototype.removeChildNode = function (child) {
143 let index = this.getChildNodes().indexOf(child);
144 if (index < 0) {
145 throw new Error("Cannot remove DOM node that is not a TabContainer child");
146 }
147 if (this._selectedIndex == index) {
148 child._handleExit();
149 this._selectedIndex = -1;
150 }
151 RapidContext.Widget.destroyWidget(this.firstChild.childNodes[index]);
152 child.remove();
153 child.emit && child.emit("close");
154 if (this._selectedIndex > index) {
155 this._selectedIndex--;
156 }
157 if (this._selectedIndex < 0 && this.getChildNodes().length > 0) {
158 this.selectChild((index == 0) ? 0 : index - 1);
159 }
160};
161
162// TODO: add support for status updates in child pane widget
163
164/**
165 * Returns the index of the currently selected child in the tab
166 * container.
167 *
168 * @return {number} the index of the selected child, or
169 * -1 if no child is selected
170 */
171RapidContext.Widget.TabContainer.prototype.selectedIndex = function () {
172 return this._selectedIndex;
173};
174
175/**
176 * Returns the child widget currently selected in the tab container.
177 *
178 * @return {Node} the child widget selected, or
179 * null if no child is selected
180 */
181RapidContext.Widget.TabContainer.prototype.selectedChild = function () {
182 let children = this.getChildNodes();
183 return (this._selectedIndex < 0) ? null : children[this._selectedIndex];
184};
185
186/**
187 * Selects a specified child in the tab container. This method can be
188 * called without arguments to re-select the currently selected tab.
189 *
190 * @param {number|Node} [indexOrChild] the child index or node
191 */
192RapidContext.Widget.TabContainer.prototype.selectChild = function (indexOrChild) {
193 let children = this.getChildNodes();
194 let label;
195 if (this._selectedIndex >= 0) {
196 label = this.firstChild.childNodes[this._selectedIndex];
197 label.classList.remove("selected");
198 children[this._selectedIndex]._handleExit();
199 }
200 let index = -1;
201 if (indexOrChild == null) {
202 index = this._selectedIndex;
203 } else if (typeof(indexOrChild) == "number") {
204 index = indexOrChild;
205 } else {
206 index = children.indexOf(indexOrChild);
207 }
208 this._selectedIndex = (index < 0 || index >= children.length) ? -1 : index;
209 if (this._selectedIndex >= 0) {
210 label = this.firstChild.childNodes[this._selectedIndex];
211 label.classList.add("selected");
212 children[this._selectedIndex]._handleEnter();
213 }
214};
215
RapidContext
Access · Discovery · Insight
www.rapidcontext.com