1/** 2 * EGroupware eTemplate2 - JS Portlet object - used by Home 3 * 4 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 5 * @package home 6 * @package etemplate 7 * @subpackage api 8 * @link http://www.egroupware.org 9 * @author Nathan Gray 10 * @version $Id$ 11 */ 12 13/*egw:uses 14 /vendor/bower-asset/jquery/dist/jquery.js; 15 et2_core_baseWidget; 16*/ 17 18import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; 19import {et2_valueWidget} from "./et2_core_valueWidget"; 20import {ClassWithAttributes} from "./et2_core_inheritance"; 21import {et2_action_object_impl, et2_DOMWidget} from "./et2_core_DOMWidget"; 22 23/** 24 * Class which implements the UI of a Portlet 25 * 26 * This manages the frame and decoration, but also provides the UI for properties. 27 * 28 * Portlets are only internal to EGroupware. 29 * 30 * Home does not fully implement WSRP, but tries not to conflict, ether. 31 * @link http://docs.oasis-open.org/wsrp/v2/wsrp-2.0-spec-os-01.html 32 * @augments et2_baseWidget 33 */ 34class et2_portlet extends et2_valueWidget 35{ 36 static readonly _attributes : any = { 37 "title": { 38 "name": "Title", 39 "description": "Goes in the little bit at the top with the icons", 40 "type": "string", 41 "default": "" 42 }, 43 "edit_template": { 44 "name": "Edit template", 45 "description": "Custom eTemplate used to customize / set up the portlet", 46 "type": "string", 47 "default": egw.webserverUrl+"/home/templates/default/edit.xet" 48 }, 49 "color": { 50 "name": "Color", 51 "description": "Set the portlet color", 52 "type": "string", 53 "default": '' 54 }, 55 "settings": { 56 "name": "Customization settings", 57 "description": "Array of customization settings, similar in structure to preference settings", 58 "type": "any", 59 "default": et2_no_init 60 }, 61 "actions": { 62 default: {} 63 }, 64 "width": { "default": 2, "ignore": true}, 65 "height": { "default": 1, "type": "integer"}, 66 "rows": {"ignore": true, default: et2_no_init}, 67 "cols": {"ignore": true, default: et2_no_init}, 68 "resize_ratio": {"ignore": true, default: et2_no_init}, // Portlets are explicitly sized 69 "row": { 70 "name": "Row", 71 "description": "Home page location (row) - handled by home app", 72 "default": 1 73 }, 74 "col": { 75 "name": "Column", 76 "description": "Home page location(column) - handled by home app", 77 "default": 1 78 } 79 }; 80 81 private GRID : number = 55; 82 private settings: any; 83 /** 84 * These are the "normal" actions that every portlet is expected to have. 85 * The widget provides default actions for all of these, but they can 86 * be added to or overridden if needed by setting the action attribute. 87 */ 88 protected default_actions : any = { 89 edit_settings: { 90 icon: "edit", 91 caption: "Configure", 92 "default": true, 93 hideOnDisabled: true, 94 group: "portlet" 95 }, 96 remove_portlet: { 97 icon: "delete", 98 caption: "Remove", 99 group: "portlet" 100 } 101 }; 102 protected div: JQuery; 103 protected header: JQuery; 104 protected content: JQuery; 105 106 /** 107 * Constructor 108 * 109 * @memberOf et2_portlet 110 */ 111 constructor(_parent, _attrs? : WidgetConfig, _child? : object) 112 { 113 // Call the inherited constructor 114 super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_portlet._attributes, _child || {})); 115 116 let self = this; 117 118 // Create DOM nodes 119 this.div = jQuery(document.createElement("div")) 120 .addClass(this.options.class) 121 .addClass("ui-widget ui-widget-content ui-corner-all") 122 .addClass("et2_portlet") 123 /* Gridster */ 124 .attr("data-sizex", this.options.width) 125 .attr("data-sizey", this.options.height) 126 .attr("data-row", this.options.row) 127 .attr("data-col", this.options.col) 128 129 .resizable( { 130 autoHide: true, 131 grid: this.GRID, 132 //containment: this.getParent().getDOMNode(), 133 stop: function(event, ui) { 134 self.set_width(Math.round(ui.size.width / self.GRID)); 135 self.set_height(Math.round(ui.size.height / self.GRID)); 136 self.egw().jsonq("home.home_ui.ajax_set_properties",[self.id, {},{ 137 width: self.options.width, 138 height: self.options.height 139 }], 140 null, 141 self 142 ); 143 // Tell children 144 self.iterateOver(function(widget) {widget.resize();},null,et2_IResizeable); 145 } 146 }); 147 this.header = jQuery(document.createElement("div")) 148 .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_header') 149 .addClass("ui-widget-header ui-corner-all") 150 .appendTo(this.div) 151 .html(this.options.title); 152 this.content = jQuery(document.createElement("div")) 153 .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_content') 154 .appendTo(this.div); 155 156 this.setDOMNode(this.div[0]); 157 } 158 159 destroy() 160 { 161 for(let i = 0; i < this._children.length; i++) 162 { 163 // Check for child is a different template and clear it, 164 // since it won't be cleared by destroy() 165 if(this._children[i].getInstanceManager() != this.getInstanceManager()) 166 { 167 this._children[i].getInstanceManager().clear(); 168 } 169 } 170 super.destroy(); 171 } 172 173 doLoadingFinished() 174 { 175 this.set_color(this.options.color); 176 return true; 177 } 178 179 /** 180 * If anyone asks, return the content node, so content goes inside 181 */ 182 getDOMNode(_sender) 183 { 184 if(typeof _sender != 'undefined' && _sender != this) 185 { 186 return this.content[0]; 187 } 188 return super.getDOMNode(_sender); 189 } 190 191 /** 192 * Overriden from parent to add in default actions 193 */ 194 set_actions(actions) 195 { 196 // Set targets for actions 197 let defaults: any = {}; 198 for(let action_name in this.default_actions) 199 { 200 defaults[action_name] = this.default_actions[action_name]; 201 // Translate caption here, as translations aren't available earlier 202 defaults[action_name].caption = this.egw().lang(this.default_actions[action_name].caption); 203 if(typeof this[action_name] == "function") 204 { 205 defaults[action_name].onExecute = jQuery.proxy(this[action_name],this); 206 } 207 } 208 209 // Add in defaults, but let provided actions override them 210 this.options.actions = jQuery.extend(true,{},defaults,actions); 211 super.set_actions(this.options.actions); 212 } 213 214 /** 215 * Override _link_actions to remove edit action, if there is no settings 216 * 217 * @param actions 218 */ 219 _link_actions(actions) 220 { 221 // Get the top level element 222 let objectManager = <egwActionObject><unknown>egw_getAppObjectManager(true); 223 let widget_object =objectManager.getObjectById(this.id); 224 if (widget_object == null) 225 { 226 // Add a new container to the object manager which will hold the widget 227 // objects 228 widget_object = objectManager.insertObject(false, new egwActionObject( 229 this.id, objectManager, <egwActionObjectInterface><unknown>new et2_action_object_impl(<et2_DOMWidget><unknown>this).getAOI(), 230 this._actionManager || (<egwAction><unknown>objectManager.manager).getActionById(this.id) || objectManager.manager 231 )); 232 } 233 234 // Delete all old objects 235 widget_object.clear(); 236 237 // Go over the widget & add links - this is where we decide which actions are 238 // 'allowed' for this widget at this time 239 let action_links = []; 240 for(let i in actions) 241 { 242 let id = typeof actions[i].id != 'undefined' ? actions[i].id : i; 243 let action = { 244 actionId: id, 245 enabled: true 246 }; 247 248 // If there are no settings, there can be no customization, so remove the edit action 249 if(id == 'edit_settings' && (!this.options.settings || jQuery.isEmptyObject(this.options.settings))) 250 { 251 this.egw().debug("log", "No settings for portlet %o, edit_settings action removed", this); 252 action.enabled = false; 253 } 254 action_links.push(action); 255 } 256 257 widget_object.updateActionLinks(action_links); 258 } 259 260 /** 261 * Create & show a dialog for customizing this portlet 262 * 263 * Properties for customization are sent in the 'settings' attribute 264 */ 265 edit_settings() 266 { 267 let dialog = et2_createWidget("dialog", { 268 callback: jQuery.proxy(this._process_edit, this), 269 template: this.options.edit_template, 270 value: { 271 content: this.options.settings 272 }, 273 buttons: et2_dialog.BUTTONS_OK_CANCEL 274 },this); 275 // Set seperately to avoid translation 276 dialog.set_title(this.egw().lang("Edit") + " " + (this.options.title || '')); 277 } 278 279 _process_edit(button_id, value) 280 { 281 if(button_id != et2_dialog.OK_BUTTON) return; 282 283 284 // Save settings - server might reply with new content if the portlet needs an update, 285 // but ideally it doesn't 286 this.div.addClass("loading"); 287 288 // Pass updated settings, unless we're removing 289 let settings = (typeof value == 'string') ? {} : this.options.settings || {}; 290 this.egw().jsonq("home.home_ui.ajax_set_properties",[this.id, settings, value, this.settings ? this.settings.group : false], 291 function(data) { 292 // This section not for us 293 if(!data || typeof data.attributes == 'undefined') return false; 294 295 this.div.removeClass("loading"); 296 this.set_value(data.content); 297 for(let key in data.attributes) 298 { 299 if(typeof this["set_"+key] == "function") 300 { 301 this["set_"+key].call(this, data.attributes[key]); 302 } 303 else if (this.attributes[key]) 304 { 305 this.options[key] = data.attributes[key]; 306 } 307 } 308 309 // Flagged as needing to edit settings? Open dialog 310 if(typeof data.edit_settings != 'undefined' && data.edit_settings) 311 { 312 this.edit_settings(); 313 } 314 315 // Only resize once, and only if needed 316 if(data.attributes.width || data.attributes.height) 317 { 318 // Tell children 319 try { 320 this.iterateOver(function(widget) {widget.resize();},null,et2_IResizeable); 321 } catch (e) { 322 // Something went wrong, but do not stop 323 egw.debug('warn',e,this); 324 } 325 } 326 }, 327 this); 328 329 // Extend, not replace, because settings has types while value has just value 330 if(typeof value == 'object') 331 { 332 jQuery.extend(this.options.settings, value); 333 } 334 } 335 336 /** 337 * Remove this portlet from the home page 338 */ 339 remove_portlet() 340 { 341 let self = this; 342 et2_dialog.show_dialog(function(button_id) { 343 if(button_id != et2_dialog.OK_BUTTON) return; 344 self._process_edit(button_id, '~remove~'); 345 self.getParent().removeChild(self); 346 self.destroy(); 347 },this.egw().lang("Remove"), this.options.title,{}, 348 et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.QUESTION_MESSAGE 349 ); 350 } 351 352 /** 353 * Set the HTML content of the portlet 354 * 355 * @param value String HTML fragment 356 */ 357 set_value(value) 358 { 359 this.content.html(value); 360 } 361 362 /** 363 * Set the content of the header 364 * 365 * @param value String HTML fragment 366 */ 367 set_title(value) 368 { 369 this.header.contents() 370 .filter(function() { 371 return this.nodeType === 3; 372 }) 373 .remove(); 374 this.options.title = value; 375 this.header.append(value); 376 } 377 378 /** 379 * Let this portlet stand out a little by allowing a custom color 380 */ 381 set_color(color) 382 { 383 this.options.color = color; 384 this.header.css("backgroundColor", color); 385 this.header.css('color', jQuery.Color(this.header.css("backgroundColor")).lightness() > 0.5 ? 'black':'white'); 386 this.content.css("backgroundColor", color); 387 } 388 389 /** 390 * Set the number of grid cells this widget spans 391 * 392 * @param value int Number of horizontal grid cells 393 */ 394 set_width(value) 395 { 396 this.options.width = value; 397 this.div.attr("data-sizex", value); 398 // Clear what's there from jQuery, we get width from CSS according to sizex 399 this.div.css('width',''); 400 } 401 402 /** 403 * Set the number of vertical grid cells this widget spans 404 * 405 * @param value int Number of vertical grid cells 406 */ 407 set_height(value) 408 { 409 this.options.height = value; 410 this.div.attr("data-sizey", value); 411 // Clear what's there from jQuery, we get width from CSS according to sizey 412 this.div.css('height',''); 413 } 414} 415et2_register_widget(et2_portlet, ["portlet"]); 416 417