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