460 lines
17 KiB
JavaScript
460 lines
17 KiB
JavaScript
|
/*
|
||
|
* 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 option <code>show</code>.
|
||
|
*
|
||
|
* @event TK.Knob#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_arrows() {
|
||
|
if (!this._prev.parentNode) return;
|
||
|
if (this._prev.parentNode) this._prev.remove();
|
||
|
if (this._next.parentNode) this._next.remove();
|
||
|
var E = this.element;
|
||
|
TK.remove_class(E, "toolkit-over");
|
||
|
this.trigger_resize();
|
||
|
}
|
||
|
function show_arrows() {
|
||
|
if (this._prev.parentNode) return;
|
||
|
var E = this.element;
|
||
|
E.insertBefore(this._prev, this._clip);
|
||
|
E.appendChild(this._next);
|
||
|
TK.add_class(E, "toolkit-over");
|
||
|
this.trigger_resize();
|
||
|
}
|
||
|
function prev_clicked(e) {
|
||
|
this.userset("show", Math.max(0, this.options.show - 1));
|
||
|
}
|
||
|
function prev_dblclicked(e) {
|
||
|
this.userset("show", 0);
|
||
|
}
|
||
|
|
||
|
function next_clicked(e) {
|
||
|
this.userset("show", Math.min(this.buttons.length-1, this.options.show + 1));
|
||
|
}
|
||
|
function next_dblclicked(e) {
|
||
|
this.userset("show", this.buttons.length-1);
|
||
|
}
|
||
|
|
||
|
function button_clicked(button) {
|
||
|
this.userset("show", this.buttons.indexOf(button));
|
||
|
}
|
||
|
function easeInOut (t, b, c, d) {
|
||
|
t /= d/2;
|
||
|
if (t < 1) return c/2*t*t + b;
|
||
|
t--;
|
||
|
return -c/2 * (t*(t-2) - 1) + b;
|
||
|
}
|
||
|
|
||
|
var zero = { width: 0, height: 0};
|
||
|
|
||
|
TK.ButtonArray = TK.class({
|
||
|
/**
|
||
|
* TK.ButtonArray is a list of ({@link TK.Button})s, arranged
|
||
|
* either vertically or horizontally. TK.ButtonArray is able to
|
||
|
* add arrow buttons automatically if the overal size is less
|
||
|
* than the width/height of the buttons list.
|
||
|
*
|
||
|
* @param {Object} [options={ }] - An object containing initial options.
|
||
|
*
|
||
|
* @property {Array<Object|String>} [options.buttons=[]] - A list of
|
||
|
* button options objects or label strings which is converted to
|
||
|
* button instances on init. If `get` is called, a converted list
|
||
|
* of button instances is returned.
|
||
|
* @property {Boolean} [options.auto_arrows=true] - Set to `false`
|
||
|
* to disable auto-generated arrow buttons on overflow.
|
||
|
* @property {String} [options.direction="horizontal"] - The layout
|
||
|
* of the button list, either "horizontal" or "vertical".
|
||
|
* @property {Integer|TK.Button} [options.show=-1] - The {@link TK.Button}
|
||
|
* to scroll to and highlight, expects either the button index starting
|
||
|
* from zero or the {@link TK.Button} instance itself. Set to `-1` to
|
||
|
* de-select any selected button.
|
||
|
* @property {Integer} [options.scroll=0] - Offer scrollbars for generic
|
||
|
* scrolling. This reduces performance because movement is done in JS
|
||
|
* instead of (pesumably accelerated) CSS transitions. 0 for standard
|
||
|
* behavior, n > 0 is handled as milliseconds for transitions.
|
||
|
* @property {Object} [options.button_class=TK.Button] - A class to
|
||
|
* be used for instantiating the buttons.
|
||
|
*
|
||
|
* @class TK.ButtonArray
|
||
|
*
|
||
|
* @extends TK.Container
|
||
|
*/
|
||
|
_class: "ButtonArray",
|
||
|
Extends: TK.Container,
|
||
|
_options: Object.assign(Object.create(TK.Container.prototype._options), {
|
||
|
buttons: "array",
|
||
|
auto_arrows: "boolean",
|
||
|
direction: "string",
|
||
|
show: "int",
|
||
|
resized: "boolean",
|
||
|
scroll: "int",
|
||
|
button_class: "TK.Button",
|
||
|
}),
|
||
|
options: {
|
||
|
buttons: [],
|
||
|
auto_arrows: true,
|
||
|
direction: "horizontal",
|
||
|
show: -1,
|
||
|
resized: false,
|
||
|
scroll: 0,
|
||
|
button_class: TK.Button,
|
||
|
},
|
||
|
static_events: {
|
||
|
set_buttons: function(value) {
|
||
|
for (var i = 0; i < this.buttons.length; i++)
|
||
|
this.buttons[i].destroy();
|
||
|
this.buttons = [];
|
||
|
this.add_buttons(value);
|
||
|
},
|
||
|
set_direction: function(value) {
|
||
|
this.prev.set("label", value === "vertical" ? "\u25B2" : "\u25C0");
|
||
|
this.next.set("label", value === "vertical" ? "\u25BC" : "\u25B6");
|
||
|
},
|
||
|
set_show: function(value) {
|
||
|
var button = this.current();
|
||
|
if (button) {
|
||
|
button.set("state", true);
|
||
|
/**
|
||
|
* Is fired when a button is activated.
|
||
|
*
|
||
|
* @event TK.ButtonArray#changed
|
||
|
*
|
||
|
* @param {TK.Button} button - The {@link TK.Button} which was clicked.
|
||
|
* @param {int} id - the ID of the clicked {@link TK.Button}.
|
||
|
*/
|
||
|
this.fire_event("changed", button, value);
|
||
|
}
|
||
|
},
|
||
|
},
|
||
|
initialize: function (options) {
|
||
|
/**
|
||
|
* @member {Array} TK.ButtonArray#buttons - An array holding all {@link TK.Button}s.
|
||
|
*/
|
||
|
this.buttons = [];
|
||
|
TK.Container.prototype.initialize.call(this, options);
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.ButtonArray#element - The main DIV container.
|
||
|
* Has class <code>toolkit-buttonarray</code>.
|
||
|
*/
|
||
|
TK.add_class(this.element, "toolkit-buttonarray");
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.ButtonArray#_clip - A clipping area containing the list of {@link TK.Button}s.
|
||
|
* Has class <code>toolkit-clip</code>.
|
||
|
*/
|
||
|
this._clip = TK.element("div", "toolkit-clip");
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.ButtonArray#_container - A container for all the {@link TK.Button}s.
|
||
|
* Has class <code>toolkit-container</code>.
|
||
|
*/
|
||
|
this._container = TK.element("div", "toolkit-container");
|
||
|
this.element.appendChild(this._clip);
|
||
|
this._clip.appendChild(this._container);
|
||
|
|
||
|
var vert = this.get("direction") === "vertical";
|
||
|
|
||
|
/**
|
||
|
* @member {TK.Button} TK.ButtonArray#prev - The previous arrow {@link TK.Button} instance.
|
||
|
*/
|
||
|
this.prev = new TK.Button({class: "toolkit-previous", dblclick:400});
|
||
|
/**
|
||
|
* @member {TK.Button} TK.ButtonArray#next - The next arrow {@link TK.Button} instance.
|
||
|
*/
|
||
|
this.next = new TK.Button({class: "toolkit-next", dblclick:400});
|
||
|
|
||
|
this.prev.add_event("click", prev_clicked.bind(this));
|
||
|
this.prev.add_event("doubleclick", prev_dblclicked.bind(this));
|
||
|
this.next.add_event("click", next_clicked.bind(this));
|
||
|
this.next.add_event("doubleclick", next_dblclicked.bind(this));
|
||
|
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.ButtonArray#_prev - The HTMLDivElement of the previous {@link TK.Button}.
|
||
|
*/
|
||
|
this._prev = this.prev.element;
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.ButtonArray#_next - The HTMLDivElement of the next {@link TK.Button}.
|
||
|
*/
|
||
|
this._next = this.next.element;
|
||
|
|
||
|
this.set("direction", this.options.direction);
|
||
|
this.set("scroll", this.options.scroll);
|
||
|
this.add_children([this.prev, this.next]);
|
||
|
this.add_buttons(this.options.buttons);
|
||
|
this._sizes = null;
|
||
|
},
|
||
|
|
||
|
resize: function () {
|
||
|
var tmp, e;
|
||
|
|
||
|
var os = this._sizes;
|
||
|
var s = {
|
||
|
container: this._container.getBoundingClientRect(),
|
||
|
clip: {
|
||
|
height: TK.inner_height(this._clip),
|
||
|
width: TK.inner_width(this._clip),
|
||
|
},
|
||
|
buttons: [],
|
||
|
buttons_pos: [],
|
||
|
prev: this._prev.parentNode ? this._prev.getBoundingClientRect() : os ? os.prev : zero,
|
||
|
next: this._next.parentNode ? this._next.getBoundingClientRect() : os ? os.next : zero,
|
||
|
element: this.element.getBoundingClientRect(),
|
||
|
};
|
||
|
|
||
|
this._sizes = s;
|
||
|
|
||
|
for (var i = 0; i < this.buttons.length; i++) {
|
||
|
e = this.buttons[i].element;
|
||
|
s.buttons[i] = e.getBoundingClientRect();
|
||
|
s.buttons_pos[i] = { left: e.offsetLeft, top: e.offsetTop };
|
||
|
}
|
||
|
|
||
|
TK.Container.prototype.resize.call(this);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Adds an array of buttons to the end of the list.
|
||
|
*
|
||
|
* @method TK.ButtonArray#add_buttons
|
||
|
*
|
||
|
* @param {Array.<string|object>} options - An Array containing objects
|
||
|
* with options for the buttons (see {@link TK.Button} for more
|
||
|
* information) or strings for the buttons labels.
|
||
|
*/
|
||
|
add_buttons: function (options) {
|
||
|
for (var i = 0; i < options.length; i++)
|
||
|
this.add_button(options[i]);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Adds a {@link TK.Button} to the TK.ButtonArray.
|
||
|
*
|
||
|
* @method TK.ButtonArray#add_button
|
||
|
*
|
||
|
* @param {Object|string} options - An object containing options for the
|
||
|
* {@link TK.Button} to add or a string for the label.
|
||
|
* @param {integer} [position] - The position to add the {@link TK.Button}
|
||
|
* to. If `undefined`, the {@link TK.Button} is added to the end of the list.
|
||
|
*
|
||
|
* @returns {TK.Button} The {@link TK.Button} instance.
|
||
|
*/
|
||
|
add_button: function (options, position) {
|
||
|
if (typeof options === "string")
|
||
|
options = {label: options}
|
||
|
var b = new this.options.button_class(options);
|
||
|
var len = this.buttons.length;
|
||
|
var vert = this.options.direction === "vertical";
|
||
|
if (position === void(0))
|
||
|
position = this.buttons.length;
|
||
|
if (position === len) {
|
||
|
this.buttons.push(b);
|
||
|
this._container.appendChild(b.element);
|
||
|
} else {
|
||
|
this.buttons.splice(position, 0, b);
|
||
|
this._container.insertBefore(b.element,
|
||
|
this._container.childNodes[position]);
|
||
|
}
|
||
|
|
||
|
this.add_child(b);
|
||
|
|
||
|
this.trigger_resize();
|
||
|
b.add_event("click", button_clicked.bind(this, b));
|
||
|
/**
|
||
|
* A {@link TK.Button} was added to the TK.ButtonArray.
|
||
|
*
|
||
|
* @event TK.ButtonArray#added
|
||
|
*
|
||
|
* @param {TK.Button} button - The {@link TK.Button} which was added to TK.ButtonArray.
|
||
|
*/
|
||
|
if (b === this.current())
|
||
|
b.set("state", true);
|
||
|
this.fire_event("added", b);
|
||
|
|
||
|
return b;
|
||
|
},
|
||
|
/**
|
||
|
* Removes a {@link TK.Button} from the TK.ButtonArray.
|
||
|
*
|
||
|
* @method TK.ButtonArray#remove_button
|
||
|
*
|
||
|
* @param {integer|TK.Button} button - button index or the {@link TK.Button}
|
||
|
* instance to be removed.
|
||
|
*/
|
||
|
remove_button: function (button) {
|
||
|
if (typeof button === "object")
|
||
|
button = this.buttons.indexOf(button);
|
||
|
if (button < 0 || button >= this.buttons.length)
|
||
|
return;
|
||
|
/**
|
||
|
* A {@link TK.Button} was removed from the TK.ButtonArray.
|
||
|
*
|
||
|
* @event TK.ButtonArray#removed
|
||
|
*
|
||
|
* @param {TK.Button} button - The {@link TK.Button} instance which was removed.
|
||
|
*/
|
||
|
this.fire_event("removed", this.buttons[button]);
|
||
|
if (this.current() && button <= this.options.show) {
|
||
|
this.options.show --;
|
||
|
this.invalid.show = true;
|
||
|
this.trigger_draw();
|
||
|
}
|
||
|
this.buttons[button].destroy();
|
||
|
this.buttons.splice(button, 1);
|
||
|
this.trigger_resize();
|
||
|
},
|
||
|
|
||
|
destroy: function () {
|
||
|
for (var i = 0; i < this.buttons.length; i++)
|
||
|
this.buttons[i].destroy();
|
||
|
this.prev.destroy();
|
||
|
this.next.destroy();
|
||
|
this._container.remove();
|
||
|
this._clip.remove();
|
||
|
TK.Container.prototype.destroy.call(this);
|
||
|
},
|
||
|
|
||
|
redraw: function() {
|
||
|
TK.Container.prototype.redraw.call(this);
|
||
|
var I = this.invalid;
|
||
|
var O = this.options;
|
||
|
var S = this._sizes;
|
||
|
|
||
|
if (I.direction) {
|
||
|
var E = this.element;
|
||
|
TK.remove_class(E, "toolkit-vertical", "toolkit-horizontal");
|
||
|
TK.add_class(E, "toolkit-"+O.direction);
|
||
|
}
|
||
|
|
||
|
if (I.validate("direction", "auto_arrows") || I.resized) {
|
||
|
if (O.auto_arrows && O.resized && !O.needs_resize) {
|
||
|
var dir = O.direction === "vertical";
|
||
|
var subd = dir ? 'top' : 'left';
|
||
|
var subs = dir ? 'height' : 'width';
|
||
|
|
||
|
var clipsize = S.clip[subs];
|
||
|
var listsize = 0;
|
||
|
|
||
|
if (this.buttons.length)
|
||
|
listsize = S.buttons_pos[this.buttons.length-1][subd] +
|
||
|
S.buttons[this.buttons.length-1][subs];
|
||
|
if (Math.round(listsize) > Math.round(clipsize)) {
|
||
|
show_arrows.call(this);
|
||
|
} else if (Math.round(listsize) <= Math.round(clipsize)) {
|
||
|
hide_arrows.call(this);
|
||
|
}
|
||
|
} else if (!O.auto_arrows) {
|
||
|
hide_arrows.call(this);
|
||
|
}
|
||
|
}
|
||
|
if (I.validate("show", "direction", "resized")) {
|
||
|
if (O.resized && !O.needs_resize) {
|
||
|
var show = O.show
|
||
|
if (show >= 0 && show < this.buttons.length) {
|
||
|
/* move the container so that the requested button is shown */
|
||
|
var dir = O.direction === "vertical";
|
||
|
var subd = dir ? 'top' : 'left';
|
||
|
var subt = dir ? 'scrollTop' : 'scrollLeft';
|
||
|
var subs = dir ? 'height' : 'width';
|
||
|
|
||
|
var btnrect = S.buttons[show];
|
||
|
var clipsize = S.clip[subs];
|
||
|
var listsize = 0;
|
||
|
var btnsize = 0;
|
||
|
var btnpos = 0;
|
||
|
if (S.buttons.length) {
|
||
|
listsize = S.buttons_pos[this.buttons.length-1][subd] +
|
||
|
S.buttons[this.buttons.length-1][subs];
|
||
|
btnsize = S.buttons[show][subs];
|
||
|
btnpos = S.buttons_pos[show][subd];
|
||
|
}
|
||
|
|
||
|
var p = (Math.max(0, Math.min(listsize - clipsize, btnpos - (clipsize / 2 - btnsize / 2))));
|
||
|
if (this.options.scroll) {
|
||
|
var s = this._clip[subt];
|
||
|
this._scroll = {to: ~~p, from: s, dir: p > s ? 1 : -1, diff: ~~p - s, time: Date.now()};
|
||
|
this.invalid.scroll = true;
|
||
|
if (this._container.style[subd])
|
||
|
this._container.style[subd] = null;
|
||
|
} else {
|
||
|
this._container.style[subd] = -p + "px";
|
||
|
if (s)
|
||
|
this._clip[subt] = 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (this.invalid.scroll && this._scroll) {
|
||
|
var subt = O.direction === "vertical" ? 'scrollTop' : 'scrollLeft';
|
||
|
var s = ~~this._clip[subt];
|
||
|
var _s = this._scroll;
|
||
|
var now = Date.now();
|
||
|
if ((s >= _s.to && _s.dir > 0)
|
||
|
|| (s <= _s.to && _s.dir < 0)
|
||
|
|| now > (_s.time + O.scroll)) {
|
||
|
this.invalid.scroll = false;
|
||
|
this._clip[subt] = _s.to;
|
||
|
} else {
|
||
|
this._clip[subt] = easeInOut(Date.now() - _s.time, _s.from, _s.diff, O.scroll);
|
||
|
this.trigger_draw_next();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The currently active button.
|
||
|
*
|
||
|
* @method TK.ButtonArray#current
|
||
|
*
|
||
|
* @returns {TK.Button} The active {@link TK.Button} or null, if none is selected.
|
||
|
*/
|
||
|
current: function() {
|
||
|
var n = this.options.show;
|
||
|
if (n >= 0 && n < this.buttons.length) {
|
||
|
return this.buttons[n];
|
||
|
}
|
||
|
return null;
|
||
|
},
|
||
|
|
||
|
set: function (key, value) {
|
||
|
var button;
|
||
|
if (key === "show") {
|
||
|
//if (value < 0) value = 0;
|
||
|
//if (value >= this.buttons.length) value = this.buttons.length - 1;
|
||
|
if (value === this.options.show) return value;
|
||
|
|
||
|
button = this.current();
|
||
|
if (button) button.set("state", false);
|
||
|
}
|
||
|
if (key == "scroll") {
|
||
|
TK[value>0?"add_class":"remove_class"](this.element, "toolkit-scroll");
|
||
|
this.trigger_resize();
|
||
|
}
|
||
|
return TK.Container.prototype.set.call(this, key, value);
|
||
|
},
|
||
|
get: function (key) {
|
||
|
if (key === "buttons") return this.buttons;
|
||
|
return TK.Container.prototype.get.call(this, key);
|
||
|
}
|
||
|
});
|
||
|
})(this, this.TK);
|