1/**
2 * EGroupware eTemplate2 - JS Dropdown Button object
3 *
4 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5 * @package etemplate
6 * @subpackage api
7 * @link http://www.egroupware.org
8 * @author Nathan Gray
9 * @copyright Nathan Gray 2013
10 * @version $Id$
11 */
12
13/*egw:uses
14	/vendor/bower-asset/jquery/dist/jquery.js;
15	/vendor/bower-asset/jquery-ui/jquery-ui.js;
16	et2_baseWidget;
17*/
18
19import {et2_inputWidget} from './et2_core_inputWidget';
20import {WidgetConfig, et2_register_widget} from "./et2_core_widget";
21import {ClassWithAttributes} from "./et2_core_inheritance";
22
23/**
24 * A split button - a button with a dropdown list
25 *
26 * There are several parts to the button UI:
27 * - Container: This is what is percieved as the dropdown button, the whole package together
28 *   - Button: The part on the left that can be clicked
29 *   - Arrow: The button to display the choices
30 *   - Menu: The list of choices
31 *
32 * Menu options are passed via the select_options.  They are normally ID => Title pairs,
33 * as for a select box, but the title can also be full HTML if needed.
34 *
35 * @augments et2_inputWidget
36 */
37export class et2_dropdown_button extends et2_inputWidget
38{
39	static readonly attributes : any = {
40		"label": {
41			"name": "caption",
42			"type": "string",
43			"description": "Label of the button",
44			"translate": true,
45			"default": "Select..."
46		},
47		"label_updates": {
48			"name": "Label updates",
49			"type": "boolean",
50			"description": "Button label updates when an option is selected from the menu",
51			"default": true
52		},
53		"image": {
54			"name": "Icon",
55			"type": "string",
56			"description": "Add an icon"
57		},
58		"ro_image": {
59			"name": "Read-only Icon",
60			"type": "string",
61			"description": "Use this icon instead of hiding for read-only"
62		},
63		"onclick": {
64			"description": "JS code which gets executed when the button is clicked"
65		},
66		"select_options": {
67			"type": "any",
68			"name": "Select options",
69			"default": {},
70			"description": "Select options for dropdown.  Can be a simple key => value list, or value can be full HTML",
71			// Skip normal initialization for this one
72			"ignore": true
73		},
74		"accesskey": {
75			"name": "Access Key",
76			"type": "string",
77			"default": et2_no_init,
78			"description": "Alt + <key> activates widget"
79		},
80		"tabindex": {
81			"name": "Tab index",
82			"type": "integer",
83			"default": et2_no_init,
84			"description": "Specifies the tab order of a widget when the 'tab' button is used for navigating."
85		},
86		// No such thing as a required button
87		"required": {
88			"ignore": true
89		}
90	};
91
92	internal_ids : any = {
93		div:	"",
94		button:	"",
95		menu:	""
96	};
97
98	div : JQuery =  null;
99	buttons : JQuery = null;
100	button : JQuery = null;
101	arrow : JQuery = null;
102	menu : JQuery = null;
103	image : JQuery = null;
104	clicked : boolean = false;
105	label_updates : boolean = true;
106	value : any = null;
107	/**
108	 * Default menu, so there is something for the widget browser / editor to show
109	 */
110	readonly default_menu : string = '<ul> \
111<li data-id="opt_1.1"><a href="#">Option-1.1</a></li>\
112<li data-id="opt_1.2"><a href="#">Option-1.2</a></li>\
113<li data-id="opt_1.3"><a href="#">Option-1.3</a></li>\
114<li data-id="opt_1.4"><a href="#">Option-1.4<br>\
115	<small>with second line</small>\
116</a></li>\
117<li data-id="opt_1.5"><a href="#">Option-1.5</a></li>\
118</ul>';
119
120	/**
121	 * Constructor
122	 *
123	 * @memberOf et2_dropdown_button
124	 */
125	constructor(_parent?, _attrs? : WidgetConfig, _child? : object) {
126		super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_dropdown_button._attributes, _child || {}));
127
128		this.clicked = false;
129
130		let self = this;
131
132		// Create the individual UI elements
133
134		// Menu is a UL
135		this.menu = jQuery(this.default_menu).attr("id",this.internal_ids.menu)
136			.hide()
137			.menu({
138				select: function(event,ui) {
139					self.onselect.call(self,event,ui.item);
140				}
141			});
142
143		this.buttons = jQuery(document.createElement("div"))
144			.addClass("et2_dropdown");
145
146		// Main "wrapper" div
147		this.div = jQuery(document.createElement("div"))
148			.attr("id", this.internal_ids.div)
149			.append(this.buttons)
150			.append(this.menu);
151
152		// Left side - activates click action
153		this.button = jQuery(document.createElement("button"))
154			.attr("id", this.internal_ids.button)
155			.attr("type", "button")
156			.addClass("ui-widget ui-corner-left").removeClass("ui-corner-all")
157			.appendTo(this.buttons);
158
159		// Right side - shows dropdown
160		this.arrow = jQuery(document.createElement("button"))
161			.addClass("ui-widget ui-corner-right").removeClass("ui-corner-all")
162			.attr("type", "button")
163			.click(function() {
164				// ignore click on readonly button
165				if (self.options.readonly) return false;
166				// Clicking it again hides menu
167				if(self.menu.is(":visible"))
168				{
169					self.menu.hide();
170					return false;
171				}
172				// Show menu dropdown
173				var menu = self.menu.show().position({
174					my: "left top",
175					at: "left bottom",
176					of: self.buttons
177				});
178				// Hide menu if clicked elsewhere
179				jQuery( document ).one( "click", function() {
180					menu.hide();
181				});
182				return false;
183			})
184			// This is the actual down arrow icon
185			.append("<div class='ui-icon ui-icon-triangle-1-s'/>")
186			.appendTo(this.buttons);
187
188		// Common button UI
189		this.buttons.children("button")
190			.addClass("ui-state-default")
191			.hover(
192				function() {jQuery(this).addClass("ui-state-hover");},
193				function() {jQuery(this).removeClass("ui-state-hover");}
194			);
195
196		// Icon
197		this.image = jQuery(document.createElement("img"));
198
199		this.setDOMNode(this.div[0]);
200	}
201
202	destroy() {
203		// Destroy widget
204		if(this.menu && this.menu.data('ui-menu')) this.menu.menu("destroy");
205
206		// Null children
207		this.image = null;
208		this.button = null;
209		this.arrow = null;
210		this.buttons = null;
211		this.menu = null;
212
213		// Remove
214		this.div.empty().remove();
215	}
216
217	set_id(_id) {
218		super.set_id(_id);
219
220		// Update internal IDs - not really needed since we refer by internal
221		// javascript reference, but good to keep up to date
222		this.internal_ids = {
223			div:	this.dom_id + "_wrapper",
224			button:	this.dom_id,
225			menu:	this.dom_id + "_menu"
226		};
227		for(let key in this.internal_ids)
228		{
229			if(this[key] == null) continue;
230			this[key].attr("id", this.internal_ids[key]);
231		}
232	}
233
234	/**
235	 * Set if the button label changes to match the selected option
236	 *
237	 * @param updates boolean Turn updating on or off
238	 */
239	set_label_updates(updates)
240	{
241		this.label_updates = updates;
242	}
243
244	set_accesskey(key)
245	{
246		jQuery(this.node).attr("accesskey", key);
247	}
248
249	set_ro_image(_image)
250	{
251		if(this.options.readonly)
252		{
253			this.set_image(_image);
254		}
255	}
256
257	set_image(_image)
258	{
259		if(!this.isInTree() || this.image == null) return;
260		if(!_image.trim())
261		{
262			this.image.hide();
263		}
264		else
265		{
266			this.image.show();
267		}
268
269		let src = this.egw().image(_image);
270		if(src)
271		{
272			this.image.attr("src", src);
273		}
274		// allow url's too
275		else if (_image[0] == '/' || _image.substr(0,4) == 'http')
276		{
277			this.image.attr('src', _image);
278		}
279		else
280		{
281			this.image.hide();
282		}
283	}
284
285	/**
286	 * Overwritten to maintain an internal clicked attribute
287	 *
288	 * @param _ev
289	 * @returns {Boolean}
290	 */
291	click(_ev)
292	{
293		// ignore click on readonly button
294		if (this.options.readonly) return false;
295
296		this.clicked = true;
297
298		if (!super.click(_ev))
299		{
300			this.clicked = false;
301			return false;
302		}
303		this.clicked = false;
304		return true;
305	}
306
307	onselect(event, selected_node)
308	{
309		this.set_value(selected_node.attr("data-id"));
310		this.change(selected_node);
311	}
312
313	attachToDOM()
314	{
315		let res = super.attachToDOM();
316
317		// Move the parent's handler to the button, or we can't tell the difference between the clicks
318		jQuery(this.node).unbind("click.et2_baseWidget");
319		this.button.off().bind("click.et2_baseWidget", this, function(e) {
320			return e.data.click.call(e.data, this);
321		});
322		return res;
323	}
324
325	set_label(_value)
326	{
327		if (this.button)
328		{
329			this.label = _value;
330
331			this.button.text(_value)
332				.prepend(this.image);
333		}
334	}
335
336	/**
337	 * Set the options for the dropdown
338	 *
339	 * @param options Object ID => Label pairs
340	 */
341	set_select_options(options) {
342		this.menu.first().empty();
343
344		// Allow more complicated content, if passed
345		if(typeof options == "string")
346		{
347			this.menu.append(options);
348		}
349		else
350		{
351			let add_complex = function(node, options)
352			{
353				for(let key in options)
354				{
355					let item;
356					if(typeof options[key] == "string")
357					{
358						item = jQuery("<li data-id='"+key+"'><a href='#'>"+options[key]+"</a></li>");
359					}
360					else if (options[key]["label"])
361					{
362						item =jQuery("<li data-id='"+key+"'><a href='#'>"+options[key]["label"]+"</a></li>");
363					}
364					// Optgroup
365					else
366					{
367						item = jQuery("<li><a href='#'>"+key+"</a></li>");
368						add_complex(node.append("<ul>"), options[key]);
369					}
370					node.append(item);
371					if(item && options[key].icon)
372					{
373						// we supply a applicable class for item images
374						jQuery('a',item).prepend('<img class="et2_button_icon" src="' + (options[key].icon.match(/^(http|https|\/)/) ? options[key].icon : egw.image(options[key].icon)) +'"/>');
375					}
376				}
377			}
378			add_complex(this.menu.first(), options);
379		}
380		this.menu.menu("refresh");
381	}
382
383	/**
384	 * Set tab index
385	 */
386	set_tabindex(index)
387	{
388		jQuery(this.button).attr("tabindex", index);
389	}
390
391	set_value(new_value)
392	{
393		let menu_item = jQuery("[data-id='"+new_value+"']",this.menu);
394		if(menu_item.length)
395		{
396			this.value = new_value;
397			if(this.label_updates)
398			{
399				this.set_label(menu_item.text());
400			}
401		}
402		else
403		{
404			this.value = null;
405			if(this.label_updates)
406			{
407				this.set_label(this.options.label);
408			}
409		}
410	}
411
412	getValue()
413	{
414		return this.value;
415	}
416
417	/**
418	 * Set options.readonly
419	 *
420	 * @param {boolean} _ro
421	 */
422	set_readonly(_ro : boolean)
423	{
424		if (_ro != this.options.readonly)
425		{
426			this.options.readonly = _ro;
427
428			// don't make readonly dropdown buttons clickable
429			if (this.buttons)
430			{
431				this.buttons.find('button')
432					.toggleClass('et2_clickable', !_ro)
433					.toggleClass('et2_button_ro', _ro)
434					.css('cursor', _ro ? 'default' : 'pointer');
435			}
436		}
437	}
438}
439et2_register_widget(et2_dropdown_button, ["dropdown_button"]);
440
441
442