/* * 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 */ "use strict"; (function(w, TK) { var merge = function(dst) { var key, i, src; for (i = 1; i < arguments.length; i++) { src = arguments[i]; for (key in src) { dst[key] = src[key]; } } return dst; }; var mixin = function(dst, src) { var fun, key; for (key in src) if (!dst[key]) { if (key === "constructor" || key === "_class" || key === "Extends" || key === "Implements" || key === "options") continue; if (!src.hasOwnProperty(key)) continue; fun = src[key]; dst[key] = fun; } return dst; }; function call_handler(self, fun, args) { try { return fun.apply(self, args); } catch (e) { TK.warn("event handler", fun, "threw", e); } } function dispatch_events(self, handlers, args) { var v; if (Array.isArray(handlers)) { for (var i = 0; i < handlers.length; i++) { v = call_handler(self, handlers[i], args); if (v !== void(0)) return v; } } else return call_handler(self, handlers, args); } function add_event(to, event, fun) { var tmp = to[event]; if (!tmp) { to[event] = fun; } else if (Array.isArray(tmp)) { tmp.push(fun); } else { to[event] = [ tmp, fun ]; } } function remove_event(from, event, fun) { var tmp = from[event]; if (!tmp) return; if (Array.isArray(tmp)) { for (var i = 0; i < tmp.length; i++) { if (tmp[i] === fun) { tmp[i] = tmp[tmp.length-1]; tmp.length --; } } if (tmp.length === 1) from[event] = tmp[0]; else if (tmp.length === 0) delete from[event]; } else if (tmp === fun) { delete from[event]; } } function add_static_event(w, event, fun) { var p = w.prototype, e; if (!p.hasOwnProperty('static_events')) { if (p.static_events) { p.static_events = e = Object.assign({}, p.static_events); } else { p.static_events = e = {}; } } else e = p.static_events; add_event(e, event, fun); } function arrayify(x) { if (!Array.isArray(x)) x = [ x ]; return x; } function merge_static_events(a, b) { var event; if (!a) return b; if (!b) return Object.assign({}, a); for (event in a) { var tmp = a[event]; if (b.hasOwnProperty(event)) { b[event] = arrayify(tmp).concat(arrayify(b[event])); } else { b[event] = Array.isArray(tmp) ? tmp.slice(0) : tmp; } } return Object.assign({}, a, b); } TK.class = function(o) { var constructor; var methods; var tmp, i, c, key; if (tmp = o.Extends) { if (typeof(tmp) === "function") { tmp = tmp.prototype; } if (typeof(o.options) === "object" && typeof(tmp.options) === "object") { o.options = Object.assign(Object.create(tmp.options), o.options); } if (o.static_events) o.static_events = merge_static_events(tmp.static_events, o.static_events); methods = Object.assign(Object.create(tmp), o); } else { methods = o; } tmp = o.Implements; // mixins if (tmp !== void(0)) { tmp = arrayify(tmp); for (i = 0; i < tmp.length; i++) { if (typeof(tmp[i]) === "function") { c = tmp[i].prototype; } else c = tmp[i]; if (typeof(c.options) === "object") { if (!methods.hasOwnProperty("options")) { methods.options = Object.create(methods.options || null); } methods.options = merge({}, c.options, methods.options); } if (c.static_events) { if (methods.static_events) { methods.static_events = merge_static_events(c.static_events, Object.assign({}, methods.static_events)); } else { methods.static_events = c.static_events; } } methods = mixin(methods, c, true); } } var init = methods.initialize; var post_init = methods.initialized; if (post_init) { constructor = function() { init.apply(this, arguments); post_init.call(this); }; } else constructor = init || (function() {}); constructor.prototype = methods; methods.constructor = constructor; return constructor; }; var __native_events = { // mouse mouseenter : true, mouseleave : true, mousedown : true, mouseup : true, mousemove : true, mouseover : true, click : true, dblclick : true, startdrag : true, stopdrag : true, drag : true, dragenter : true, dragleave : true, dragover : true, drop : true, dragend : true, // touch touchstart : true, touchend : true, touchmove : true, touchenter : true, touchleave : true, touchcancel: true, keydown : true, keypress : true, keyup : true, scroll : true, focus : true, blur : true, // mousewheel mousewheel : true, DOMMouseScroll : true, wheel : true, submit : true, contextmenu: true, }; function is_native_event(type) { return __native_events[type]; } function remove_native_events(element) { var type; var s = this.static_events; var d = this.__events; var handler = this.__native_handler; for (type in s) if (is_native_event(type)) TK.remove_event_listener(element, type, handler); for (type in d) if (is_native_event(type) && (!s || !s.hasOwnProperty(type))) TK.remove_event_listener(element, type, handler); } function add_native_events(element) { var type; var s = this.static_events; var d = this.__events; var handler = this.__native_handler; for (type in s) if (is_native_event(type)) TK.add_event_listener(element, type, handler); for (type in d) if (is_native_event(type) && (!s || !s.hasOwnProperty(type))) TK.add_event_listener(element, type, handler); } function native_handler(ev) { /* FIXME: * * mouseover and error are cancelled with true * * beforeunload is cancelled with null */ if (this.fire_event(ev.type, ev) === false) return false; } function has_event_listeners(event) { var ev = this.__events; if (ev.hasOwnProperty(event)) return true; ev = this.static_events; return ev && ev.hasOwnProperty(event); } /** * This is the base class for all widgets in toolkit. * It provides an API for event handling and other basic implementations. * * @class TK.Base */ TK.Base = TK.class({ initialize : function(options) { this.__events = {}; this.__event_target = null; this.__native_handler = native_handler.bind(this); this.set_options(options); this.fire_event("initialize"); }, initialized : function() { /** * Is fired when an instance is initialized. * * @event TK.Base#initialized */ this.fire_event("initialized"); }, /** * Destroys all event handlers and the options object. * * @method TK.Base#destroy */ destroy : function() { if (this.__event_target) { remove_native_events.call(this, this.__event_target, this.__events); } this.__events = null; this.__event_target = null; this.__native_handler = null; this.options = null; }, /** * Merges a new options object into the existing one, * including deep copies of objects. If an option key begins with * the string "on" it is considered as event handler. In this case * the value should be the handler function for the event with * the corresponding name without the first "on" characters. * * @method TK.Base#set_options(options) * * @param {Object} [options={ }] - An object containing initial options. */ set_options : function(o) { var opt = this.options; var key, a, b; if (typeof(o) !== "object") { delete this.options; o = {}; } else if (typeof(opt) === "object") for (key in o) if (o.hasOwnProperty(key)) { a = o[key]; b = opt[key]; if (typeof a === "object" && a && Object.getPrototypeOf(Object.getPrototypeOf(a)) === null && typeof b === "object" && b && Object.getPrototypeOf(Object.getPrototypeOf(b)) === null ) { o[key] = merge({}, b, a); } } if (this.hasOwnProperty("options")) { this.options = merge(opt, o); } else if (opt) { this.options = Object.assign(Object.create(opt), o); } else { this.options = Object.assign({}, o); } for (key in this.options) if (key.startsWith("on")) { this.add_event(key.substr(2).toLowerCase(), this.options[key]); delete this.options[key]; } }, /** * Get the value of an option. * * @method TK.Base#get * * @param {string} key - The option name. */ get: function (key) { return this.options[key]; }, /** * Sets an option. Fires both the events set with arguments key * and value; and the event 'set_'+key with arguments value * and key. * * @method TK.Base#set * * @param {string} key - The name of the option. * @param {mixed} value - The value of the option. * * @emits TK.Base#set * @emits TK.Base#set_[option] */ set: function (key, value) { var e; this.options[key] = value; /** * Is fired when an option is set. * * @event TK.Base#set * * @param {string} name - The name of the option. * @param {mixed} value - The value of the option. */ if (this.has_event_listeners("set")) this.fire_event("set", key, value); /** * Is fired when an option is set. * * @event TK.Base#set_[option] * * @param {mixed} value - The value of the option. */ e = "set_"+key; if (this.has_event_listeners(e)) this.fire_event(e, value, key); return value; }, /** * Sets an option by user interaction. Emits the userset * event. The userset event can be cancelled (if an event handler * returns false), in which case the option is not set. * Returns true if the option was set, false * otherwise. If the option was set, it will emit a useraction event. * * @method TK.Base#userset * * @param {string} key - The name of the option. * @param {mixed} value - The value of the option. * * @emits TK.Base#userset * @emits TK.Base#useraction */ userset: function(key, value) { if (false === this.fire_event("userset", key, value)) return false; value = this.set(key, value); this.fire_event("useraction", key, value); return true; }, /** * Delegates all occuring DOM events of a specific DOM node to the widget. * This way the widget fires e.g. a click event if someone clicks on the * given DOM node. * * @method TK.Base#delegate_events * * @param {HTMLElement} element - The element all native events of the widget should be bound to. * * @returns {HTMLElement} The element * * @emits TK.Base#delegated */ delegate_events: function (element) { var old_target = this.__event_target; /** * Is fired when an element is delegated. * * @event TK.Base#delegated * * @param {HTMLElement|Array} element - The element which receives all * native DOM events. * @param {HTMLElement|Array} old_element - The element which previously * received all native DOM events. */ this.fire_event("delegated", element, old_target); if (old_target) remove_native_events.call(this, old_target); if (element) add_native_events.call(this, element); this.__event_target = element; return element; }, /** * Register an event handler. * * @method TK.Base#add_event * * @param {string} event - The event descriptor. * @param {Function} func - The function to call when the event happens. * @param {boolean} prevent - Set to true if the event should prevent the default behavior. * @param {boolean} stop - Set to true if the event should stop bubbling up the tree. */ add_event: function (event, func) { var ev, tmp; if (typeof event !== "string") throw new TypeError("Expected string."); if (typeof func !== "function") throw new TypeError("Expected function."); if (arguments.length !== 2) throw new Error("Bad number of arguments."); if (is_native_event(event) && (ev = this.__event_target) && !this.has_event_listeners(event)) TK.add_event_listener(ev, event, this.__native_handler); ev = this.__events; add_event(ev, event, func); }, /** * Removes the given function from the event queue. * If it is a native DOM event, it removes the DOM event listener * as well. * * @method TK.Base#remove_event * * @param {string} event - The event descriptor. * @param {Function} fun - The function to remove. */ remove_event: function (event, fun) { remove_event(this.__events, event, fun); // remove native DOM event listener from __event_target if (is_native_event(event) && !this.has_event_listeners(event)) { var ev = this.__event_target; if (ev) TK.remove_event_listener(ev, event, this.__native_handler); } }, /** * Fires an event. * * @method TK.Base#fire_event * * @param {string} event - The event descriptor. * @param {...*} args - Event arguments. */ fire_event: function (event) { var ev; var args; var v; ev = this.__events; if (ev !== void(0) && (event in ev)) { ev = ev[event]; args = Array.prototype.slice.call(arguments, 1); v = dispatch_events(this, ev, args); if (v !== void(0)) return v; } ev = this.static_events; if (ev !== void(0) && (event in ev)) { ev = ev[event]; if (args === void(0)) args = Array.prototype.slice.call(arguments, 1); v = dispatch_events(this, ev, args); if (v !== void(0)) return v; } }, /** * Test if the event descriptor has some handler functions in the queue. * * @method TK.Base#has_event_listeners * * @param {string} event - The event desriptor. * * @returns {boolean} True if the event has some handler functions in the queue, false if not. */ has_event_listeners: has_event_listeners, /** * Add multiple event handlers at once, either as dedicated event handlers or a list of event * descriptors with a single handler function. * * @method TK.Base#add_events * * @param {Object | Array} events - Object with event descriptors as keys and functions as * values or Array of event descriptors. The latter requires a handler function as the * second argument. * @param {Function} func - A function to add as event handler if the first argument is an * array of event desriptors. */ add_events: function (events, func) { var i; if (Array.isArray(events)) { for (i = 0; i < events.length; i++) this.add_event(events[i], func); } else { for (i in events) if (events.hasOwnProperty(i)) this.add_event(i, events[i]); } }, /** * Remove multiple event handlers at once, either as dedicated event handlers or a list of * event descriptors with a single handler function. * * @method TK.Base#remove_events * * @param {Object | Array} events - Object with event descriptors as keys and functions as * values or Array of event descriptors. The latter requires the handler function as the * second argument. * @param {Function} func - A function to remove from event handler queue if the first * argument is an array of event desriptors. */ remove_events: function (events, func) { var i; if (Array.isArray(events)) { for (i = 0; i < events.length; i++) this.remove_event(events[i], func); } else { for (i in events) if (events.hasOwnProperty(i)) this.remove_event(i, events[i]); } }, /** * Fires several events. * * @method TK.Base#fire_events * * @param {Array.} events - A list of event names to fire. */ fire_events: function (events) { for (var i in events) { if (events.hasOwnProperty(i)) this.fire_event(i, events[i]); } } }); function get_child_options(parent, name, options, config) { var ret = {}; var key, pref = name+"."; var tmp; var inherit_options = !!config.inherit_options; var blacklist_options = config.blacklist_options || []; if (tmp = config.default_options) Object.assign(ret, (typeof(tmp) === "function") ? tmp.call(parent) : tmp); for (key in options) { if (key.startsWith(pref)) { ret[key.substr(pref.length)] = options[key]; } if (inherit_options && blacklist_options.indexOf(tmp) < 0) { if (key in config.create.prototype._options && !(key in TK["Widget"].prototype._options)) { ret[key] = options[key]; } } } var map_options = config.map_options; if (map_options) for (key in map_options) { if (options[key]) { ret[map_options[key]] = options[key]; } } return ret; } function ChildWidget(widget, name, config) { /** * Defines a {@link TK.Widget} as a child for another widget. This function * is used internally to simplify widget definitions. E.g. the {@link TK.Icon} of a * {@link TK.Button} is defined as a TK.ChildWidget. TK.ChildWidgets * are created/added after the initialization of the parent widget. * If not configured explicitly, all options of the child widget can * be accessed via TK.Widget.options[config.name + "." + option] * on the parent widget. * * @param {TK.Widget} widget - The {@link TK.Widget} to add the TK.ChildWidget to. * @param {string} name - The identifier of the element inside the parent Element, TK.Widget[config.name]. * @param {object} config - The configuration of the child element. * * @property {TK.Widget} config.create - A TK.Widget class derivate to be used as child widget. * @property {boolean} [config.fixed] - A fixed child widget cannot be removed after initialization. * @property {boolean} [config.show=false] - Show/hide a non-fixed child widget on initialization. * @property {string} [config.option="show_"+config.name] - A custom option of the parent widget * to determine the visibility of the child element. If this is * null, TK.Widget.options["show_"+ config.name] * is used to toggle its visibility. The child element is visible, if * this options is !== false. * @property {function} [config.append] - A function overriding the generic * append mechanism. If not null, this function is * supposed to take care of adding the child widget to the parent * widgets DOM. Otherwise the element of the child widget is added * to the element of the parent widget. * @property {boolean} [config.inherit_options=false] - Defines if both widgets share the * same set of options. If true, Setting an option on the * parent widget also sets the same option on the child widget. If false, * the options of the child widget can be accessed via options[config.name + "." + option] * in the parent widget. * @property {array} [config.map_options=[]] - A list of options to be mapped between * parent and child. Keys are the options to be added to the parent, values the options * names in the child widget. * names in the parent to which the childrens options (defined as values) are mapped to. * If one of these options is set * on the parent widget, it also gets set on the child widget. This is * a fine-grained version of config.inherit-options. * @property {boolean} [config.userset_ignore=false] - Do not care about the userset * event of the parent widget, only keep track of set. * @property {boolean} [config.userset_delegate=false] - Delegates all user interaction from * the child to the parent element. If the user triggers an event on * the child widget, the userset function of the parent * element is called. * @property {array} [config.static_events=[]] - An array of static events to be * added to the parent widget. Each entry is a mapping between * the name of the event and the callback function. * @property {boolean} [config.toggle_class=false] - Defines if the parent widget * receives the class toolkit-has-[name] as soon as * the child element is shown. * @property {array} [config.blacklist_options] - Array containing options names * which are skipped on `inherit_options`. * * @class TK.ChildWidget * */ var p = widget.prototype; var key = config.option || "show_"+name; var tmp, m; var static_events = { }; if (!config.userset_ignore) static_events.userset = (config.inherit_options || config.userset_delegate) ? function(key, value) { this.parent.userset(key, value); return false; } : function(key, value) { this.parent.userset(name+"."+key, value); return false; }; if (m = config.static_events) Object.assign(static_events, m); if (config.create === void(0)) { TK.warn("'create' is undefined. Skipping ChildWidget ", name); return; } var child = TK.class({ Extends: config.create, static_events: static_events, }); /* trigger child widget creation after initialization */ add_static_event(widget, "initialized", function() { /* we do not want to trash the class cache */ if (!this[name]) { this[name] = null; this.set(key, this.options[key]); } }); /* clean up on destroy */ add_static_event(widget, "destroy", function() { if (this[name]) { this[name].destroy(); this[name] = null; } }); var fixed = config.fixed; var append = config.append; var toggle_class = !!config.toggle_class; if (append === void(0)) append = true; /* child widget creation */ add_static_event(widget, "set_"+key, function(val) { var C = this[name]; var show = fixed || !!val; if (show && !C) { var O = get_child_options(this, name, this.options, config); if (append === true) O.container = this.element; var w = new child(O); this.add_child(w); this[name] = w; if (typeof(append) === "function") append.call(this); } else if (!show && C) { C.destroy(); this[name] = null; } if (toggle_class) TK.toggle_class(this.element, "toolkit-has-"+name, show); this.trigger_resize(); }); var set_cb = function(val, key) { if (this[name]) this[name].set(key.substr(name.length+1), val); }; for (tmp in child.prototype._options) { add_static_event(widget, "set_"+name+"."+tmp, set_cb); p._options[name+"."+tmp] = child.prototype._options[tmp]; } /* direct option inherit */ var blacklist_options = config.blacklist_options || []; if (config.inherit_options) { set_cb = function(val, key) { if (this[name]) this[name].set(key, val); }; for (tmp in child.prototype._options) { if (tmp in TK["Widget"].prototype._options) continue; if (blacklist_options.indexOf(tmp) > -1) continue; add_static_event(widget, "set_"+tmp, set_cb); if (!p._options[tmp]) p._options[tmp] = child.prototype._options[tmp]; } } set_cb = function(key) { return function(val) { if (this[name]) this[name].set(key, val); }; }; if (m = config.map_options) { for (tmp in m) { p._options[tmp] = child.prototype._options[m[tmp]]; if (!p.options[tmp]) p.options[tmp] = child.prototype.options[m[tmp]]; add_static_event(widget, "set_"+tmp, set_cb(m[tmp])); } } if (!config.options) { if (!p._options[key]) p._options[key] = "boolean"; p.options[key] = fixed || !!config.show; } } TK.add_static_event = add_static_event; TK.ChildWidget = ChildWidget; function ChildElement(widget, name, config) { /** * Creates a HTMLElement as a child for a widget. Is used to simplify * widget definitions. E.g. the tiny marker used to display the back-end * value is a simple DIV added using TK.ChildElement. The generic element * is a DIV added to TK.Widget.element with the class * toolkit-[name]. Default creating and adding can be * overwritten with custom callback functions. * * @param {TK.Widget} widget - The {@link TK.Widget} to add the TK.ChildElement to. * @param {string} name - The identifier of the element. It will be prefixed * by an underscore TK.Widget["_" + config.name]. * @param {object} config - The configuration of the child element. * * @property {boolean} [config.show=false] - Show/hide the child element on initialization. * @property {string} [config.option="show_"+config.name] - A custom option of the parent widget * to determine the visibility of the child element. If this is * null, TK.Widget.options["show_"+ config.name] * is used to toggle its visibility. The child element is visible, if * this options is !== false. * @property {function} [config.display_check] - A function overriding the * generic show_option behavior. If set, this function * is called with the value of show_option as argument * as soon as it gets set and is supposed to return a boolean * defining the visibility of the element. * @property {function} [config.append] - A function overriding the generic * append mechanism. If not null, this function is * supposed to take care of adding the child element to the parent * widgets DOM. * @property {function} [config.create] - A function overriding the generic * creation mechanism. If not null, this function is * supposed to create and return a DOM element to be added to the * parent widget. * @property {boolean} [config.toggle_class=false] - Defines if the parent widget * receives the class toolkit-has-[name] as soon as * the child element is shown. * @property {array} [config.draw_options] - A list of options of the parent * widget which are supposed to trigger a check if the element has to * be added or removed. * @property {function} [config.draw] - A function to be called on redraw. * * @class TK.ChildElement * */ var p = widget.prototype; var show_option = config.option || ("show_" + name); var index = "_"+name; var display_check = config.display_check; /* This is done to make sure that the object property is created * inside of the constructor. Otherwise, if we add the widget later * might be turned into a generic mapping. */ add_static_event(widget, "initialize", function() { this[index] = null; }); /* trigger child element creation after initialization */ add_static_event(widget, "initialized", function() { this.set(show_option, this.options[show_option]); }); /* clean up on destroy */ add_static_event(widget, "destroy", function() { if (this[index]) { this[index].remove(); this[index] = null; } }); var append = config.append; var create = config.create; var toggle_class = !!config.toggle_class; if (create === void(0)) create = function() { return TK.element("div", "toolkit-"+name); } if (append === void(0)) append = function() { this.element.appendChild(this[index]); } add_static_event(widget, "set_"+show_option, function(value) { var C = this[index]; var show = display_check ? display_check(value) : (value !== false); if (show && !C) { C = create.call(this); this[index] = C; append.call(this, this.options); } else if (C && !show) { this[index] = null; C.remove(); } if (toggle_class) TK.toggle_class(this.element, "toolkit-has-"+name, value); this.trigger_resize(); }); if (config.draw) { var m = config.draw_options; if (!m) m = [ show_option ]; else m.push(show_option); for (var i = 0; i < m.length; i++) { add_static_event(widget, "set_"+m[i], function() { if (this.options[show_option] !== false) this.draw_once(config.draw); }); } } if (p._options[show_option] === void(0)) { p._options[show_option] = "boolean"; p.options[show_option] = !!config.show; } } TK.ChildElement = ChildElement; })(this, this.TK||{});