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