UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

827 lines (715 loc) 25.8 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2013-2014 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Martin Wittemann (wittemann) * Daniel Wagner (danielwagner) ************************************************************************ */ /** * A row of buttons used to switch between connected pages. The buttons can be * right- or left-aligned, or they can be justified, i.e. they will be stretched * to fill the available width. * * <h2>Markup</h2> * Each Tabs widget contains an unordered list element (<code>ul</code>), which * will be created if not already present. * The tabs are list items (<code>li</code>). Each tab can contain * a button with a <code>tabsPage</code> data attribute where the value is a * CSS selector string identifying the corresponding page. Headers and pages * will not be created automatically. They can be predefined in the DOM before * the <code>q().tabs()</code> factory method is called, or added programmatically. * * <h2>CSS Classes</h2> * <table> * <thead> * <tr> * <td>Class Name</td> * <td>Applied to</td> * <td>Description</td> * </tr> * </thead> * <tbody> * <tr> * <td><code>qx-tabs</code></td> * <td>Container element</td> * <td>Identifies the Tabs widget</td> * </tr> * <tr> * <td><code>qx-tabs-horizontal</code></td> * <td>Container element</td> * <td>Styles the widget in horizontal orientation</td> * </tr> * <tr> * <td><code>qx-tabs-vertical</code></td> * <td>Container element</td> * <td>Styles the widget in vertical orientation</td> * </tr> * <tr> * <td><code>qx-tabs-container</code></td> * <td>Tab page container (<code>div</code>)</td> * <td>Styles the tab pages' container (horizontal orientation only)</td> * </tr> * <tr> * <td><code>qx-flex-justify-end</code></td> * <td>Tab container (<code>ul</code>)</td> * <td>Browsers with flexbox support only: Styles the tab buttons when they are right-aligned</td> * </tr> * <tr> * <td><code>qx-tabs-left</code></td> * <td>Container element</td> * <td>Internet Explorer < 10 only: Styles the tab buttons when they are left-aligned</td> * </tr> * <tr> * <td><code>qx-tabs-justify</code></td> * <td>Container element</td> * <td>Internet Explorer < 10 only: Styles the tab buttons when they are stretched to fill out the available width</td> * </tr> * <tr> * <td><code>qx-tabs-right</code></td> * <td>Container element</td> * <td>Internet Explorer < 10 only: Styles the tab buttons when they are right-aligned</td> * </tr> * <tr> * <td><code>qx-tabs-button</code></td> * <td>Tab button (<code>li</code>)</td> * <td>Identifies and styles the tabs</td> * </tr> * <tr> * <td><code>qx-tabs-button-active</code></td> * <td>Tab button (<code>li</code>)</td> * <td>Identifies and styles the currently selected tab. Applied in addition to <code>qx-tabs-button</code></td> * </tr> * <tr> * <td><code>qx-flex-1</code></td> * <td>Tab button (<code>li</code>)</td> * <td>Browsers with flexbox support only: Styles the tab buttons when they are stretched to fill out the available width</td> * </tr> * <tr> * <td><code>qx-tabs-page</code></td> * <td>Tab page (<code>div</code> in horizontal mode, <code>li</code>)</td> * <td>Styles the tab pages.</td> * </tr> * </tbody> * </table> * * <h2 class="widget-markup">Generated DOM Structure</h2> * * @require(qx.module.Template) * * @group (Widget) */ qx.Bootstrap.define("qx.ui.website.Tabs", { extend: qx.ui.website.Widget, statics: { /** * Factory method which converts the current collection into a collection of * tabs widgets. * * @attach{qxWeb} * @param align {String?} Tab button alignment. Default: <code>left</code> * @param preselected {Integer?} The (zero-based) index of the tab that * should initially be selected. Default: <code>0</code> * @param orientation {String?} <code>horizontal</code> (default) or <code>vertical</code> * @return {qx.ui.website.Tabs} */ tabs: function(align, preselected, orientation) { var tabs = new qx.ui.website.Tabs(this); if (typeof preselected !== "undefined") { tabs.setConfig("preselected", preselected); } tabs.init(); if (align) { tabs.setConfig("align", align); } if (orientation) { tabs.setConfig("orientation", orientation); } if (align || orientation) { tabs.render(); } return tabs; }, /** * *button* * * Template used by {@link #addButton} to create a new button. * * Default value: <pre><li><button>{{{content}}}</button></li></pre> */ _templates: { button: "<li><button>{{{content}}}</button></li>" }, /** * *preselected* * The index of the page that should be opened after initial * rendering, or <code>null</code> if no page should be opened. * * Default value: <pre>0</pre> * * *align* * Configuration for the alignment of the tab buttons in horizontal * mode. This possible values are <code>left</code>, * <code>justify</code> and * <code>right</code> * * Default value: <pre>left</pre> * * *orientation* * Controls the layout of the widget. "horizontal" renders it as a * tab bar appropriate for wide screens. "vertical" renders it as a * stack of collapsible panes (sometimes called an accordion) that * is better suited for narrow screens. * * *mediaQuery* * A CSS media query string that will be used with a * media query listener to dynamically set the widget's * orientation. The widget will be rendered in vertical mode unless * the query matches. */ _config: { preselected: 0, align: "left", orientation: "horizontal", mediaQuery: null } }, construct: function(selector, context) { this.base(arguments, selector, context); }, events: { /** * Fired when the selected page has changed. The value is * the newly selected page's index */ "changeSelected": "Number" }, members: { __mediaQueryListener: null, init: function() { if (!this.base(arguments)) { return false; } var mediaQuery = this.getConfig("mediaQuery"); if (mediaQuery) { this.setConfig("orientation", this._initMediaQueryListener(mediaQuery)); } var orientation = this.getConfig("orientation"); this.addClasses([this.getCssPrefix(), this.getCssPrefix() + "-" + orientation, "qx-flex-ready"]); if (this.getChildren("ul").length === 0) { var list = qxWeb.create("<ul/>"); var content = this.getChildren(); if (content.length > 0) { list.insertBefore(content.eq(0)); } else { this.append(list); } } var container = this.find("> ." + this.getCssPrefix() + "-container"); var buttons = this.getChildren("ul").getFirst() .getChildren("li").not("." + this.getCssPrefix() + "-page"); buttons._forEachElementWrapped(function(button) { button.addClass(this.getCssPrefix() + "-button"); var pageSelector = button.getData(this.getCssPrefix() + "-page"); if (!pageSelector) { return; } button.addClass(this.getCssPrefix() + "-button") .on("tap", this._onTap, this); var page = this._getPage(button); if (page.length > 0) { page.addClass(this.getCssPrefix() + "-page"); if (orientation == "vertical") { this.__deactivateTransition(page); if (q.getNodeName(page[0]) == "div") { var li = q.create("<li>") .addClass(this.getCssPrefix() + "-page") .setAttribute("id", page.getAttribute("id")) .insertAfter(button[0]); page.remove() .getChildren().appendTo(li); page = li; } this._storePageHeight(page); } else if (orientation == "horizontal") { if (q.getNodeName(page[0]) == "li") { var div = q.create("<div>") .addClass(this.getCssPrefix() + "-page") .setAttribute("id", page.getAttribute("id")); page.remove() .getChildren().appendTo(div); page = div; } } if (orientation == "horizontal") { if (container.length === 0) { container = qxWeb.create("<div class='" + this.getCssPrefix() + "-container'>") .insertAfter(this.find("> ul")[0]); } page.appendTo(container[0]); } } this._showPage(null, button); this.__activateTransition(page); }.bind(this)); if (orientation == "vertical" && container.length == 1 && container.getChildren().length === 0) { container.remove(); } if (orientation == "horizontal" && this.getConfig("align") == "right" && q.env.get("engine.name") == "mshtml" && q.env.get("browser.documentmode") < 10) { buttons.remove(); for (var i = buttons.length - 1; i >= 0; i--) { this.find("> ul").append(buttons[i]); } } var active = buttons.filter("." + this.getCssPrefix() + "-button-active"); var preselected = this.getConfig("preselected"); if (active.length === 0 && typeof preselected == "number") { active = buttons.eq(preselected).addClass(this.getCssPrefix() + "-button-active"); } if (active.length > 0) { var activePage = this._getPage(active); this.__deactivateTransition(activePage); this._showPage(active, null); this.__activateTransition(activePage); } this.getChildren("ul").getFirst().on("keydown", this._onKeyDown, this); if (orientation === "horizontal") { this._applyAlignment(this); } qxWeb(window).on("resize", this._onResize, this); return true; }, render: function() { var mediaQuery = this.getConfig("mediaQuery"); if (mediaQuery) { this.setConfig("orientation", this._initMediaQueryListener(mediaQuery)); } var orientation = this.getConfig("orientation"); if (orientation === "horizontal") { return this._renderHorizontal(); } else if (orientation === "vertical") { return this._renderVertical(); } }, /** * Initiates a media query listener for dynamic orientation switching * @param mediaQuery {String} CSS media query string * @return {String} The appropriate orientation: "horizontal" if the * media query matches, "vertical" if it doesn't */ _initMediaQueryListener: function(mediaQuery) { var mql = this.__mediaQueryListener; if (!mql) { mql = q.matchMedia(mediaQuery); this.__mediaQueryListener = mql; mql.on("change", function(query) { this.render(); }.bind(this)); } if (mql.matches) { return "horizontal"; } else { return "vertical"; } }, /** * Render the widget in horizontal mode * @return {qx.ui.website.Tabs} The collection for chaining */ _renderHorizontal: function() { this.removeClass(this.getCssPrefix() + "-vertical") .addClasses([this.getCssPrefix() + "", this.getCssPrefix() + "-horizontal"]) .find("> ul").addClass("qx-hbox"); var container = this.find("> ." + this.getCssPrefix() + "-container"); if (container.length == 0) { container = qxWeb.create("<div class='" + this.getCssPrefix() + "-container'>") .insertAfter(this.find("> ul")[0]); } var selectedPage; this.find("> ul > ." + this.getCssPrefix() + "-button")._forEachElementWrapped(function(li) { var page = this.find(li.getData(this.getCssPrefix() + "-page")); if (q.getNodeName(page[0]) == "li") { var div = q.create("<div>") .addClass(this.getCssPrefix() + "-page") .setAttribute("id", page.getAttribute("id")); page.remove() .getChildren().appendTo(div); page = div; } page.appendTo(container[0]); this._switchPages(page, null); if (li.hasClass(this.getCssPrefix() + "-button-active")) { selectedPage = page; } }.bind(this)); if (!selectedPage) { var firstButton = this.find("> ul > ." + this.getCssPrefix() + "-button").eq(0) .addClass(this.getCssPrefix() + "-button-active"); selectedPage = this._getPage(firstButton); } this._switchPages(null, selectedPage); this._applyAlignment(this); this.setEnabled(this.getEnabled()); return this; }, /** * Render the widget in vertical mode * @return {qx.ui.website.Tabs} The collection for chaining */ _renderVertical: function() { this.find("> ul.qx-hbox").removeClass("qx-hbox"); this.removeClasses([this.getCssPrefix() + "-horizontal"]) .addClasses([this.getCssPrefix() + "", this.getCssPrefix() + "-vertical"]) .getChildren("ul").getFirst() .getChildren("li").not("." + this.getCssPrefix() + "-page") ._forEachElementWrapped(function(button) { button.addClass(this.getCssPrefix() + "-button"); var page = this._getPage(button); if (page.length === 0) { return; } this.__deactivateTransition(page); if (q.getNodeName(page[0]) == "div") { var li = q.create("<li>") .addClass(this.getCssPrefix() + "-page") .setAttribute("id", page.getAttribute("id")); page.getChildren().appendTo(li); li.insertAfter(button[0]); page.remove(); page = li; } this._storePageHeight(page); if (button.hasClass(this.getCssPrefix() + "-button-active")) { this._switchPages(null, page); } else { this._switchPages(page, null); } this.__activateTransition(page); }.bind(this)); this.setEnabled(this.getEnabled()); return this; }, /** * Re-render on browser window resize (page heights must be re- * calculated) */ _onResize: function() { // make sure this runs after a MediaQueryListener callback // which might have changed the orientation setTimeout(function() { if (this.getConfig("orientation") == "vertical") { this._renderVertical(); } }.bind(this), 100); }, /** * Adds a new tab button * * @param label {String} The button's content. Can include markup. * @param pageSelector {String} CSS Selector that identifies the associated page * @return {qx.ui.website.Tabs} The collection for chaining */ addButton: function(label, pageSelector) { var link = qxWeb.create( qxWeb.template.render( this.getTemplate("button"), { content: label } ) ).addClass(this.getCssPrefix() + "-button"); var list = this.find("> ul"); var links = list.getChildren("li"); if (list.hasClass(this.getCssPrefix() + "-right") && links.length > 0) { link.insertBefore(links.getFirst()); } else { link.appendTo(list); } link.on("tap", this._onTap, this) .addClass(this.getCssPrefix() + "-button"); if (this.find("> ul ." + this.getCssPrefix() + "-button").length === 1) { link.addClass(this.getCssPrefix() + "-button-active"); } if (pageSelector) { link.setData(this.getCssPrefix() + "-page", pageSelector); var page = this._getPage(link); page.addClass(this.getCssPrefix() + "-page"); if (link.hasClass(this.getCssPrefix() + "-button-active")) { this._switchPages(null, page); } else { this._switchPages(page, null); } } return this; }, /** * Selects a tab button * * @param index {Integer} index of the button to select * @return {qx.ui.website.Tabs} The collection for chaining */ select: function(index) { var buttons = this.find("> ul > ." + this.getCssPrefix() + "-button"); var oldButton = this.find("> ul > ." + this.getCssPrefix() + "-button-active") .removeClass(this.getCssPrefix() + "-button-active"); if (this.getConfig("align") == "right") { index = buttons.length - 1 - index; } var newButton = buttons.eq(index).addClass(this.getCssPrefix() + "-button-active"); this._showPage(newButton, oldButton); this.emit("changeSelected", index); return this; }, /** * Initiates the page switch when a button was clicked/tapped * * @param e {Event} Tap event */ _onTap: function(e) { if (!this.getEnabled()) { return; } var orientation = this.getConfig("orientation"); var tappedButton = e.getCurrentTarget(); var oldButton = this.find("> ul > ." + this.getCssPrefix() + "-button-active"); if (oldButton[0] == tappedButton && orientation == "horizontal") { return; } oldButton.removeClass(this.getCssPrefix() + "-button-active"); if (orientation == "vertical") { this._showPage(null, oldButton); if (oldButton[0] == tappedButton && orientation == "vertical") { return; } } var newButton; var buttons = this.find("> ul > ." + this.getCssPrefix() + "-button") ._forEachElementWrapped(function(button) { if (tappedButton === button[0]) { newButton = button; } }); this._showPage(newButton, oldButton); newButton.addClass(this.getCssPrefix() + "-button-active"); var index = buttons.indexOf(newButton[0]); if (this.getConfig("align") == "right") { index = buttons.length - 1 - index; } this.emit("changeSelected", index); }, /** * Allows tab selection using the left and right arrow keys * * @param e {Event} keydown event */ _onKeyDown: function(e) { var key = e.getKeyIdentifier(); if (!(key == "Left" || key == "Right")) { return; } var rightAligned = this.getConfig("align") == "right"; var buttons = this.find("> ul > ." + this.getCssPrefix() + "-button"); if (rightAligned) { buttons.reverse(); } var active = this.find("> ul > ." + this.getCssPrefix() + "-button-active"); var next; if (key == "Right") { if (!rightAligned) { next = active.getNext("." + this.getCssPrefix() + "-button"); } else { next = active.getPrev("." + this.getCssPrefix() + "-button"); } } else { if (!rightAligned) { next = active.getPrev("." + this.getCssPrefix() + "-button"); } else { next = active.getNext("." + this.getCssPrefix() + "-button"); } } if (next.length > 0) { var idx = buttons.indexOf(next); this.select(idx); next.getChildren("button").focus(); } }, /** * Initiates the page switch if a tab button is selected * * @param newButton {qxWeb} selected button * @param oldButton {qxWeb} previously active button */ _showPage: function(newButton, oldButton) { var oldPage = this._getPage(oldButton); var newPage = this._getPage(newButton); if (this.getConfig("orientation") === "horizontal" && (oldPage[0] == newPage[0])) { return; } this._switchPages(oldPage, newPage); }, /** * Executes a page switch * * @param oldPage {qxWeb} the previously selected page * @param newPage {qxWeb} the newly selected page */ _switchPages: function(oldPage, newPage) { var orientation = this.getConfig("orientation"); if (orientation === "horizontal") { if (oldPage) { oldPage.hide(); } if (newPage) { newPage.show(); } } else if (orientation === "vertical") { if (oldPage && oldPage.length > 0) { oldPage.setStyle("height", oldPage.getHeight() + "px"); oldPage[0].offsetHeight; oldPage.setStyles({ "height": "0px", "paddingTop": "0px", "paddingBottom": "0px" }); oldPage.addClass(this.getCssPrefix() + "-page-closed"); } if (newPage && newPage.length > 0) { newPage.removeClass(this.getCssPrefix() + "-page-closed"); if (!newPage.getStyle("transition") || newPage.getStyle("transition").indexOf("none") === 0) { newPage.setStyle("height", ""); } else { var openedHeight = newPage.getProperty("openedHeight"); if (qxWeb.type.get(openedHeight) == "String") { newPage.setStyle("height", openedHeight); } } } } }, /** * Returns the tab page associated with the given button * * @param button {qxWeb} Tab button * @return {qxWeb} Tab page */ _getPage: function(button) { var pageSelector; if (button) { pageSelector = button.getData(this.getCssPrefix() + "-page"); } return this.find(pageSelector); }, /** * Apply the CSS classes for the alignment * * @param tabs {qx.ui.website.Tabs[]} tabs collection */ _applyAlignment: function(tabs) { var align = tabs.getConfig("align"); var buttons = tabs.find("> ul > li"); if (q.env.get("engine.name") == "mshtml" && q.env.get("browser.documentmode") < 10) { if (align == "left") { tabs.addClass(this.getCssPrefix() + "-left"); } else { tabs.removeClass(this.getCssPrefix() + "-left"); } if (align == "justify") { tabs.addClass(this.getCssPrefix() + "-justify"); } else { tabs.removeClass(this.getCssPrefix() + "-justify"); } if (align == "right") { tabs.addClass(this.getCssPrefix() + "-right"); } else { tabs.removeClass(this.getCssPrefix() + "-right"); } } else { tabs.find("> ul").addClass("qx-hbox"); if (align == "justify") { buttons.addClass("qx-flex1"); } else { buttons.removeClass("qx-flex1"); } if (align == "right") { tabs.find("> ul").addClass("qx-flex-justify-end"); } else { tabs.find("> ul").removeClass("qx-flex-justify-end"); } } }, /** * Stores the page's natural height for the page opening transition * @param page {qxWeb} page */ _storePageHeight: function(page) { var closedClass = this.getCssPrefix() + "-page-closed"; var isClosed = page.hasClass(closedClass); if (isClosed) { page.removeClass(this.getCssPrefix() + "-page-closed"); } var prevDisplay = page[0].style.display; var prevHeight = page[0].style.height; page[0].style.height = ""; page[0].style.display = "block"; page.setProperty("openedHeight", page.getHeight() + "px"); if (isClosed) { page.addClass(this.getCssPrefix() + "-page-closed"); } page[0].style.height = prevHeight; page[0].style.display = prevDisplay; }, /** * Stores an element's CSS transition styles in a property * and removes them from the style declaration * * @param elem {qxWeb} Element */ __deactivateTransition: function(elem) { var transition = elem.getStyles([ "transitionDelay", "transitionDuration", "transitionProperty", "transitionTimingFunction" ]); if (transition.transitionProperty.indexOf("none") == -1) { elem.setProperty("__qxtransition", transition); elem.setStyle("transition", "none"); } }, /** * Restores an element's transition styles * * @param elem {qxWeb} Element */ __activateTransition: function(elem) { var transition = elem.getProperty("__qxtransition"); var style = elem.getStyle("transitionProperty"); if (transition && style.indexOf("none") != -1) { elem.setStyles(transition); elem.setProperty("__qxtransition", ""); } }, dispose: function() { this.__mediaQueryListener = undefined; var cssPrefix = this.getCssPrefix(); qxWeb(window).off("resize", this._onResize, this); this.find("> ul > ." + this.getCssPrefix() + "-button").off("tap", this._onTap, this); this.getChildren("ul").getFirst().off("keydown", this._onKeyDown, this) .setHtml(""); this.setHtml("").removeClasses([cssPrefix, "qx-flex-ready"]); return this.base(arguments); } }, defer: function(statics) { qxWeb.$attach({ tabs: statics.tabs }); } });