/* * This file is part of Toolkit. * * Toolkit is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * Toolkit is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU General * Public License along with this program; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA */ /** * The useraction event is emitted when a widget gets modified by user interaction. * The event is emitted for the options selected and value. * * @event TK.Select#useraction * * @param {string} name - The name of the option which was changed due to the users action * @param {mixed} value - The new value of the option */ "use strict"; (function(w, TK){ function hide_list() { this.__transition = false; this.__timeout = false; if (!this.__open) { this._list.remove(); } else { document.addEventListener("touchstart", this._global_touch_start); document.addEventListener("mousedown", this._global_touch_start); } } function show_list(show) { if (show) { var ew = TK.outer_width(this.element, true); document.body.appendChild(this._list); var cw = TK.width(); var ch = TK.height(); var sx = TK.scroll_left(); var sy = TK.scroll_top(); TK.set_styles(this._list, { "opacity": "0", "maxHeight": ch+"px", "maxWidth": cw+"px", "minWidth": ew+"px" }); var lw = TK.outer_width(this._list, true); var lh = TK.outer_height(this._list, true); TK.set_styles(this._list, { "top": Math.min(TK.position_top(this.element) + TK.outer_height(this.element, true), ch + sy - lh) + "px", "left": Math.min(TK.position_left(this.element), cw + sx - lw) + "px", }); } else { document.removeEventListener("touchstart", this._global_touch_start); document.removeEventListener("mousedown", this._global_touch_start); } TK.set_style(this._list, "opacity", show ? "1" : "0"); this.__transition = true; this.__open = show; if (this.__timeout !== false) window.clearTimeout(this.__timeout); var dur = TK.get_duration(this._list); this.__timeout = window.setTimeout(hide_list.bind(this), dur); } function low_remove_entry(entry) { var li = entry.element; var entries = this.entries; var id = entries.indexOf(entry); if (id === -1) throw new Error("Entry removed twice."); // remove from DOM if (li.parentElement == this._list) this._list.removeChild(li); // remove from list entries.splice(id, 1); // selection var sel = this.options.selected; if (sel !== false) { if (sel > id) { this.options.selected --; } else if (sel === id) { this.options.selected = false; this.set("label", ""); } } this.invalid.entries = true; this.select(this.options.selected); /** * Is fired when a new entry is added to the list. * * @event TK.Select.entryremoved * * @param {Object} entry - An object containing the members title and value. */ this.fire_event("entryremoved", entry); } TK.Select = TK.class({ /** * TK.Select provides a {@link TK.Button} with a select list to choose from * a list of {@TK.SelectEntry}. * * @class TK.Select * * @extends TK.Button * * @param {Object} [options={ }] - An object containing initial options. * * @property {Integer|Boolean} [options.selected=false] - The index of the selected {@TK.SelectEntry}. * Set to `false` to unselect any already selected entries. * @property {mixed} [options.value] - The value of the selected entry. * @property {Boolean} [options.auto_size=true] - If `true`, the TK.Select is * auto-sized to be as wide as the widest {@TK.SelectEntry}. * @property {Array} [options.entries=[]] - The list of {@TK.SelectEntry}. Each member is an * object with the two properties title and value, a string used * as label for constructing a {@TK.SelectEntry} or an instance of {@TK.SelectEntry}. * */ _class: "Select", Extends: TK.Button, _options: Object.assign(Object.create(TK.Button.prototype._options), { entries: "array", selected: "int", value: "mixed", auto_size: "boolean", show_list: "boolean", sort: "function", resized: "boolean", }), options: { entries: [], // A list of strings or objects {title: "Title", value: 1} or SelectEntry instance selected: false, value: false, auto_size: true, show_list: false, icon: "arrowdown", }, static_events: { click: function(e) { this.set("show_list", !this.options.show_list); }, "set_show_list": function (v) {this.set("icon", (v ? "arrowup" : "arrowdown"));}, }, initialize: function (options) { this.__open = false; this.__timeout = -1; /** * @member {Array} TK.Select#entries - An array containing all entry objects with members title and value. */ this.entries = []; this._active = null; TK.Button.prototype.initialize.call(this, options); /** * @member {HTMLDivElement} TK.Select#element - The main DIV container. * Has class toolkit-select. */ TK.add_class(this.element, "toolkit-select"); /** * @member {HTMLListElement} TK.Select#_list - A HTML list for displaying the entry titles. * Has class toolkit-select-list. */ this._list = TK.element("ul", "toolkit-select-list"); this._global_touch_start = function (e) { if (this.__open && !this.__transition && !this._list.contains(e.target) && !this.element.contains(e.target)) { this.show_list(false); } }.bind(this); var sel = this.options.selected; var val = this.options.value; this.set("entries", this.options.entries); if (sel === false && val !== false) { this.set("value", val); } else { this.set("selected", sel); } }, destroy: function () { this.clear(); this._list.remove(); TK.Button.prototype.destroy.call(this); }, /** * Show or hide the select list * * @method TK.Select#show_list * * @param {boolean} show - `true` to show and `false` to hide the list * of {@link TK.SelectEntry}. */ show_list: function (s) { this.set("show_list", !!s); }, /** * Select a {@link TK.SelectEntry} by its index. * * @method TK.Select#select * * @param {Integer} index - The index of the {@link TK.SelectEntry} to select. */ select: function (id) { this.set("selected", id); }, /** * Select a {@link TK.SelectEntry} by its value. * * @method TK.Select#select_value * * @param {mixed} value - The value of the {@link TK.SelectEntry} to select. */ select_value: function (value) { var id = this.index_by_value.call(this, value); this.set("selected", id); }, /** * Select a {@link TK.SelectEntry} by its title. * * @method TK.Select#select_title * * @param {mixed} title - The title of the {@link TK.SelectEntry} to select. */ select_title: function (title) { var id = this.index_by_title.call(this, title); this.set("selected", id); }, /** * Replaces the list of {@link TK.SelectEntry} to select from with an entirely new one. * * @method TK.Select#set_entries * * @param {Array} entries - An array of {@link TK.SelectEntry} to set as the new list to select from. * Please refer to {@link TK.Select#add_entry} for more details. */ set_entries: function (entries) { // Replace all entries with a new options list this.clear(); this.add_entries(entries); this.select(this.index_by_value.call(this, this.options.value)); }, /** * Adds new {@link TK.SelectEntry} to the end of the list to select from. * * @method TK.Select#add_entries * * @param {Array} entries - An array of {@link TK.SelectEntry} to add to the end of the list * of {@link TK.SelectEntry} to select from. Please refer to {@link TK.Select#add_entry} * for more details. */ add_entries: function (entries) { for (var i = 0; i < entries.length; i++) this.add_entry(entries[i]); }, /** * Adds a single {@link TK.SelectEntry} to the end of the list. * * @method TK.Select#add_entry * * @param {mixed} entry - A string to be displayed and used as the value, * an object with members title and value * or an instance of {@link TK.SelectEntry}. * * @emits TK.Select.entryadded */ add_entry: function (ent) { var O = this.options; var entry; var entries = this.entries; if (TK.SelectEntry.prototype.isPrototypeOf(ent)) { entry = ent; } else { entry = new TK.SelectEntry({ value: (typeof ent === "string") ? ent : ent.value, title: (typeof ent === "string") ? ent : (ent.title !== void(0)) ? ent.title : ent.value.toString() }); } this.add_child(entry); entries.push(entry); entry.set("container", this._list) var id; if (O.sort) { entries.sort(O.sort); id = entries.indexOf(entry); if (id !== entries.length - 1) this._list.insertBefore(entry.element, entries[id+1].element); } else { id = entries.length - 1; } this.invalid.entries = true; if (this.options.selected === id) { this.invalid.selected = true; this.trigger_draw(); } else if (this.options.selected > id) { this.set("selected", this.options.selected+1); } else { this.trigger_draw(); } /** * Is fired when a new {@link TK.SelectEntry} is added to the list. * * @event TK.Select#entryadded * * @param {TK.SelectEntry} entry - A new {@link TK.SelectEntry}. */ this.fire_event("entryadded", entry); }, /** * Remove a {@link TK.SelectEntry} from the list by its index. * * @method TK.Select#remove_id * * @param {Integer} index - The index of the {@link TK.SelectEntry} to be removed from the list. * * @emits TK.Select#entryremoved */ remove_index: function (index) { var entry = this.entries[index]; this.remove_child(entry); }, /** * Remove a {@link TK.SelectEntry} from the list by its value. * * @method TK.Select#remove_value * * @param {mixed} value - The value of the {@link TK.SelectEntry} to be removed from the list. * * @emits TK.Select#entryremoved */ remove_value: function (val) { this.remove_id(this.index_by_value.call(this, val)); }, /** * Remove an entry from the list by its title. * * @method TK.Select#remove_title * * @param {string} title - The title of the entry to be removed from the list. * * @emits TK.Select#entryremoved */ remove_title: function (title) { this.remove_id(this.index_by_title.call(this, title)); }, /** * Remove an entry from the list. * * @method TK.Select#remove_entry * * @param {TK.SelectEntry} entry - The {@link TK.SelectEntry} to be removed from the list. * * @emits TK.Select#entryremoved */ remove_entry: function (entry) { this.remove_child(entry); }, remove_entries: function (a) { for (var i = 0; i < a.length; i++) this.remove_entry(a[i]); }, remove_child: function(child) { TK.Button.prototype.remove_child.call(this, child); if (TK.SelectEntry.prototype.isPrototypeOf(child)) { low_remove_entry.call(this, child); } }, /** * Get the index of a {@link TK.SelectEntry} by its value. * * @method TK.Select#index_by_value * * @param {Mixed} value - The value of the {@link TK.SelectEntry}. * * @returns {Integer|Boolean} The index of the entry or `false`. */ index_by_value: function (val) { var entries = this.entries; for (var i = 0; i < entries.length; i++) { if (entries[i].options.value === val) return i; } return false; }, /** * Get the index of a {@link TK.SelectEntry} by its title/label. * * @method TK.Select#index_by_title * * @param {String} title - The title/label of the {@link TK.SelectEntry}. * * @returns {Integer|Boolean} The index of the entry or `false`. */ index_by_title: function (title) { var entries = this.entries; for (var i = 0; i < entries.length; i++) { if (entries[i].options.title === title) return i; } return false; }, /** * Get the index of a {@link TK.SelectEntry} by the {@link TK.SelectEntry} itself. * * @method TK.Select#index_by_entry * * @param {TK.SelectEntry} entry - The {@link TK.SelectEntry}. * * @returns {Integer|Boolean} The index of the entry or `false`. */ index_by_entry: function (entry) { var pos = this.entries.indexOf(entry); return pos === -1 ? false : pos; }, /** * Get a {@link TK.SelectEntry} by its value. * * @method TK.Select#entry_by_value * * @param {Mixed} value - The value of the {@link TK.SelectEntry}. * * @returns {TK.SelectEntry|False} The {@link TK.SelectEntry} or `false`. */ entry_by_value: function (val) { var entries = this.entries; for (var i = 0; i < entries.length; i++) { if (entries[i].options.value === val) return entries[i]; } return false; }, /** * Get a {@link TK.SelectEntry} by its title/label. * * @method TK.Select#entry_by_title * * @param {String} title - The title of the {@link TK.SelectEntry}. * * @returns {TK.SelectEntry|Boolean} The {@link TK.SelectEntry} or `false`. */ entry_by_title: function (title) { var entries = this.entries; for (var i = 0; i < entries.length; i++) { if (entries[i].options.title === title) return entries[i]; } return false; }, /** * Get a {@link TK.SelectEntry} by its index. * * @method TK.Select#entry_by_index * * @param {Integer} index - The index of the {@link TK.SelectEntry}. * * @returns {TK.SelectEntry|Boolean} The {@link TK.SelectEntry} or `false`. */ entry_by_index: function (index) { if (index >= 0 && index < entries.length && entries[index]) return entries[i]; return false; }, /** * Get a value by its {@link TK.SelectEntry} index. * * @method TK.Select#value_by_index * * @param {Integer} index - The index of the {@link TK.SelectEntry}. * * @returns {Mixed|Boolean} The value of the {@link TK.SelectEntry} or `false`. */ value_by_index: function(index) { var entries = this.entries; if (index >= 0 && index < entries.length && entries[index]) { return entries[index].options.value; } return false; }, /** * Get the value of a {@link TK.SelectEntry}. * * @method TK.Select#value_by_entry * * @param {TK.SelectEntry} entry - The {@link TK.SelectEntry}. * * @returns {mixed} The value of the {@link TK.SelectEntry}. */ value_by_entry: function(entry) { return entry.options.value; }, /** * Get the value of a {@link TK.SelectEntry} by its title/label. * * @method TK.Select#value_by_title * * @param {String} title - The title of the {@link TK.SelectEntry}. * * @returns {Mixed|Boolean} The value of the {@link TK.SelectEntry} or `false`. */ value_by_title: function (title) { var entries = this.entries; for (var i = 0; i < entries.length; i++) { if (entries[i].options.title === title) return entries[i].options.value; } return false; }, /** * Remove all {@link TK.SelectEntry} from the list. * * @method TK.Select#clear * * @emits TK.Select#cleared */ clear: function () { TK.empty(this._list); this.select(false); var entries = this.entries.slice(0); for (var i = 0; i < entries.length; i++) { this.remove_child(entries[i]); } /** * Is fired when the list is cleared. * * @event TK.Select.cleared */ this.fire_event("cleared"); }, redraw: function() { TK.Button.prototype.redraw.call(this); var I = this.invalid; var O = this.options; var E = this.element; if (I.selected || I.value) { I.selected = I.value = false; if (this._active) { TK.remove_class(this._active, "toolkit-active"); } var entry = this.entries[O.selected]; if (entry) { this._active = entry.element; TK.add_class(entry.element, "toolkit-active"); } else { this._active = null; } } if (I.validate("entries", "auto_size")) { I.show_list = true; var L; if (O.auto_size && (L = this._label)) { var width = 0; E.style.width = "auto"; var orig_content = document.createDocumentFragment(); while (L.firstChild) orig_content.appendChild(L.firstChild); var entries = this.entries; for (var i = 0; i < entries.length; i++) { L.appendChild(document.createTextNode(entries[i].options.title)); L.appendChild(document.createElement("BR")); } TK.S.add(function() { width = TK.outer_width(E, true); TK.S.add(function() { while (L.firstChild) L.removeChild(L.firstChild); L.appendChild(orig_content); TK.outer_width(E, true, width); }, 1); }); } } if (I.validate("show_list", "resized")) { show_list.call(this, O.show_list); } }, /** * Get the currently selected {@link TK.SelectEntry}. * * @method TK.Select#current * * @returns {TK.SelectEntry|Boolean} The currently selected {@link TK.SelectEntry} or `false`. */ current: function() { if (this.options.selected !== false) return this.entries[this.options.selected]; return false; }, /** * Get the currently selected {@link TK.SelectEntry}'s index. Just for the sake of completeness, this * function abstracts `options.selected`. * * @method TK.Select#current_index * * @returns {Integer|Boolean} The index of the currently selected {@link TK.SelectEntry} or `false`. */ current_index: function() { return this.options.selected; }, /** * Get the currently selected {@link TK.SelectEntry}'s value. * * @method TK.Select#current_value * * @returns {Mixed|Boolean} The value of the currently selected {@link TK.SelectEntry} or `false`. */ current_value: function() { var w = this.current(); if (w) return w.get("value"); return false; }, set: function (key, value) { if (key === "value") { this.set("selected", this.index_by_value.call(this, value)); return this.current_value(); } value = TK.Button.prototype.set.call(this, key, value); switch (key) { case "selected": var entry = this.current(); if (entry !== false) { TK.Button.prototype.set.call(this, "value", entry.options.value); this.set("label", entry.options.label); } else { TK.Button.prototype.set.call(this, "value", void 0); this.set("label", false); } break; case "entries": this.set_entries(value); break; } return value; } }); function on_select(e) { var w = this.parent; var id = w.index_by_entry(this); var entry = this; e.stopPropagation(); e.preventDefault(); if (w.userset("selected", id) === false) return false; w.userset("value", this.options.value); /** * Is fired when a selection was made by the user. The arguments * are the value of the currently selected {@link TK.SelectEntry}, its index, its title and the {@link TK.SelectEntry} instance. * * @event TK.Select#select * * @param {mixed} value - The value of the selected entry. * @param {number} value - The ID of the selected entry. * @param {string} value - The title of the selected entry. * @param {string} value - The title of the selected entry. */ w.fire_event("select", entry.options.value, id, entry.options.title); w.show_list(false); return false; } TK.SelectEntry = TK.class({ /** * TK.SelectEntry provides a {@link TK.Label} as an entry for {@link TK.Select}. * * @class TK.SelectEntry * * @extends TK.Label * * @param {Object} [options={ }] - An object containing initial options. * * @property {String} [options.title=""] - The title of the entry. Kept for backward compatibility, deprecated, use label instead. * @property {mixed} [options.value] - The value of the selected entry. * */ _class: "SelectEntry", Extends: TK.Label, _options: Object.assign(Object.create(TK.Label.prototype._options), { value: "Mixed", title: "String", }), options: { title: "", value: null }, initialize: function (options) { var E = this.element = TK.element("li", "toolkit-option"); TK.Label.prototype.initialize.call(this, options); this.set("title", this.options.title); }, static_events: { touchstart: on_select, mousedown: on_select, }, set: function (key, value) { switch (key) { case "title": this.set("label", value); break; case "label": this.options.title = value; break; } return TK.Label.prototype.set.call(this, key, value); } }); })(this, this.TK);