/**
* A simple tabs component.
* <p>Part of the XUI module which, for now, has an undocumented API.
*/
export class Tabs extends HTMLElement {
constructor() {
super();
this._closeableTabs = null; // Null or contains a possibly empty string.
this._newTabButton = null;
this._selectTab = this.selectTab.bind(this);
this._closeTab = this.closeTab.bind(this);
this._head = null;
this._body = null;
this._tabPairs = null;
}
connectedCallback() {
let head = this.firstElementChild;
if (head !== null && head.classList.contains("xui-tabs-head")) {
// Already connected.
return;
}
// User code may have already added its own classes.
this.classList.add("xui-control");
// tabindex="0" makes this element focusable and tab-able.
this.setAttribute("tabindex", "0");
this._closeableTabs = this.getAttribute("closeabletabs");
this._newTabButton = this.getAttribute("newtabbutton");
let childElements = [];
let child = this.firstChild;
while (child !== null) {
const nextChild = child.nextSibling;
if (child.nodeType === Node.ELEMENT_NODE) {
childElements.push(child);
}
this.removeChild(child);
child = nextChild;
}
this._head = document.createElement("div");
this._head.setAttribute("class", "xui-control xui-tabs-head");
this.appendChild(this._head);
this._body = document.createElement("div");
this._body.setAttribute("class", "xui-tabs-body");
this.appendChild(this._body);
const childCount = 2 * Math.floor(childElements.length / 2);
for (let i = 0; i < childCount; i += 2) {
const label = childElements[i];
const component = childElements[i+1];
const [tab, host] = this.createTab(label, component);
this._head.appendChild(tab);
this._body.appendChild(host);
}
if (this._newTabButton !== null) {
const tab = document.createElement("span");
let tooltip = this._newTabButton;
if (tooltip.trim().length === 0) {
tooltip = "Open a new tab";
}
tab.setAttribute("title", tooltip);
tab.addEventListener("click", this.newTab.bind(this));
tab.setAttribute("class", "xui-tabs-new-tab");
const button = document.createElement("span");
button.setAttribute("class",
"xui-control xui-small-icon xui-tabs-new-button");
button.textContent = StockIcon["plus"];
tab.appendChild(button);
this._head.appendChild(tab);
}
if (childCount > 0) {
this.selected = 0;
}
}
createTab(label, component) {
const tab = document.createElement("span");
tab.setAttribute("class", "xui-tabs-tab");
tab.addEventListener("click", this._selectTab);
tab.appendChild(label);
if (this._closeableTabs !== null) {
const button = document.createElement("span");
button.setAttribute("class",
"xui-control xui-small-icon xui-tabs-close-button");
let tooltip = this._closeableTabs;
if (tooltip.trim().length === 0) {
tooltip = "Close tab";
}
button.setAttribute("title", tooltip);
button.textContent = StockIcon["cancel"];
button.addEventListener("click", this._closeTab);
tab.appendChild(button);
}
const host = document.createElement("div");
host.setAttribute("class", "xui-tabs-host");
host.appendChild(component);
return [tab, host];
}
selectTab(event) {
if (event.button === 0 && event.detail === 1) {
// First click on primary button.
Util.consumeEvent(event);
const tab = event.currentTarget;
const label = tab.firstElementChild;
const index = this.indexOfLabel(label);
if (index >= 0 && this.selected !== index) {
const tabPair = this.get(index);
assertOrError(tabPair !== null && Object.is(tabPair[0], label));
const component = tabPair[1];
const tabsEvent = Tabs.newEvent("selectingtab",
index, label, component);
this.dispatchEvent(tabsEvent);
if (tabsEvent.defaultPrevented) {
return;
}
this.selected = index;
this.dispatchEventLater(Tabs.newEvent("tabselected",
index, label, component));
}
}
}
static newEvent(type, index, label, component) {
return new CustomEvent(type,
{ bubbles: false, composed: false,
cancelable: type.endsWith("tab"),
detail: { index: index, label: label,
component: component }});
}
dispatchEventLater(event) {
window.setTimeout(() => {
this.dispatchEvent(event);
});
}
indexOfLabel(label) {
const tabs = this.all;
const count = tabs.length;
for (let i = 0; i < count; ++i) {
let [l, c] = tabs[i];
if (Object.is(l, label)) {
return i;
}
}
return -1;
}
newTab(event) {
if (event.button === 0 && event.detail === 1) {
// First click on primary button.
Util.consumeEvent(event);
const index = this.count;
const tabsEvent = Tabs.newEvent("addingtab", index, null, null);
this.dispatchEvent(tabsEvent);
let label = null, component = null;
if (tabsEvent.defaultPrevented ||
(label = tabsEvent.detail.label) === null ||
(component = tabsEvent.detail.component) === null) {
return;
}
this.add(label, component);
this.dispatchEventLater(Tabs.newEvent("tabadded",
index, label, component));
}
}
closeTab(event) {
if (event.button === 0 && event.detail === 1) {
Util.consumeEvent(event);
const button = event.currentTarget;
const label = button.parentElement.firstElementChild;
const index = this.indexOfLabel(label);
if (index >= 0) {
const tabPair = this.get(index);
assertOrError(tabPair !== null && Object.is(tabPair[0], label));
const component = tabPair[1];
const tabsEvent = Tabs.newEvent("closingtab",
index, label, component);
this.dispatchEvent(tabsEvent);
if (tabsEvent.defaultPrevented) {
return;
}
this.remove(index);
this.dispatchEventLater(Tabs.newEvent("tabclosed",
index, label, component));
}
}
}
get selected() {
let child = this._body.firstElementChild;
let i = 0;
while (child !== null) {
if (child.classList.contains("xui-tabs-selected")) {
return i;
}
child = child.nextElementSibling;
++i;
}
return -1;
}
set selected(index) {
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length; // Not tabs.length if closeabletabs!==null.
for (let i = 0; i < count; ++i) {
const tab = tabs[i];
const host = hosts[i];
if (i === index) {
tab.classList.add("xui-tabs-selected");
host.classList.add("xui-tabs-selected");
} else {
tab.classList.remove("xui-tabs-selected");
host.classList.remove("xui-tabs-selected");
}
}
}
get all() {
if (this._tabPairs === null) {
let tabPairs = [];
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length;
for (let i = 0; i < count; ++i) {
tabPairs.push([ tabs.item(i).firstElementChild,
hosts.item(i).firstElementChild ]);
}
this._tabPairs = tabPairs;
}
return this._tabPairs;
}
get count() {
return this.all.length;
}
get(index) {
const tabPairs = this.all;
if (index < 0 || index >= tabPairs.length) {
return null;
} else {
return tabPairs[index];
}
}
add(label, component) {
this.insert(this.count, label, component);
}
insert(index, label, component) {
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length;
if (index >= 0 && index <= count) {
let beforeTab, beforeHost;
if (index < count) {
beforeTab = tabs[index];
beforeHost = hosts[index];
} else {
// If newTabButton!==null then tabs[count] is the "plus" button.
beforeTab = (this._newTabButton !== null)? tabs[count] : null;
beforeHost = null;
}
const [tab, host] = this.createTab(label, component);
this._head.insertBefore(tab, beforeTab);
this._body.insertBefore(host, beforeHost);
this._tabPairs = null;
this.selected = index;
}
}
remove(index) {
let removed = null;
const tabs = this._head.children;
const hosts = this._body.children;
const count = hosts.length;
if (index >= 0 && index < count) {
let select = -1;
if (index + 1 < count) {
select = index; // Select following tab.
} else {
select = index - 1; // Select preceding tab, if any (select=-1).
}
removed = [ tabs[index].firstElementChild,
hosts[index].firstElementChild ];
this._head.removeChild(tabs[index]);
this._body.removeChild(hosts[index]);
this._tabPairs = null;
if (select >= 0) {
this.selected = select;
}
}
return removed;
}
}
window.customElements.define("xui-tabs", Tabs);