/*
* 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 <code>useraction</code> event is emitted when a widget gets modified by user interaction.
* The event is emitted for the options <code>selected</code> and <code>value</code>.
*
* @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 <code>title</code> and <code>value</code>.
*/
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<Object>} [options.entries=[]] - The list of {@TK.SelectEntry}. Each member is an
* object with the two properties <code>title</code> and <code>value</code>, 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 <code>title</code> and <code>value</code>.
*/
this.entries = [];
this._active = null;
TK.Button.prototype.initialize.call(this, options);
/**
* @member {HTMLDivElement} TK.Select#element - The main DIV container.
* Has class <code>toolkit-select</code>.
*/
TK.add_class(this.element, "toolkit-select");
/**
* @member {HTMLListElement} TK.Select#_list - A HTML list for displaying the entry titles.
* Has class <code>toolkit-select-list</code>.
*/
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 <code>title</code> and <code>value</code>
* 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;
}
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);