mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 23:00:56 +01:00
1810 lines
51 KiB
TypeScript
1810 lines
51 KiB
TypeScript
/**
|
|
* EGroupware eTemplate2 - JS file which contains the complete et2 module
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link https://www.egroupware.org
|
|
* @author Andreas Stöckel
|
|
* @copyright EGroupware GmbH 2011-2021
|
|
*/
|
|
|
|
|
|
import {et2_widget} from "./et2_core_widget";
|
|
import {et2_baseWidget, et2_container} from "./et2_core_baseWidget";
|
|
import {EgwApp} from "../jsapi/egw_app";
|
|
import {et2_IInput, et2_IPrint, et2_IResizeable, et2_ISubmitListener} from "./et2_core_interfaces";
|
|
import {egw} from "../jsapi/egw_global";
|
|
import {et2_arrayMgr, et2_readonlysArrayMgr} from "./et2_core_arrayMgr";
|
|
import {et2_checkType} from "./et2_core_common";
|
|
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
|
|
import {et2_loadXMLFromURL} from "./et2_core_xml";
|
|
import {et2_nextmatch, et2_nextmatch_header_bar} from "./et2_extension_nextmatch";
|
|
import '../jsapi/egw_json.js';
|
|
import {egwIsMobile} from "../egw_action/egw_action_common";
|
|
import './Layout/Et2Box/Et2Box';
|
|
import './Layout/Et2Details/Et2Details';
|
|
import './Layout/Et2Groupbox/Et2Groupbox';
|
|
import './Layout/Et2Tabs/Et2Tab';
|
|
import './Layout/Et2Tabs/Et2Tabs';
|
|
import './Layout/Et2Tabs/Et2TabPanel';
|
|
import './Layout/Et2Tabs/Et2TabsMobile';
|
|
import './Et2Avatar/Et2Avatar';
|
|
import './Et2Avatar/Et2AvatarGroup';
|
|
import './Et2Button/Et2Button';
|
|
import './Et2Button/Et2ButtonCopy';
|
|
import './Et2Button/Et2ButtonIcon';
|
|
import './Et2Button/Et2ButtonScroll';
|
|
import './Et2Button/Et2ButtonTimestamper';
|
|
import './Et2Checkbox/Et2Checkbox';
|
|
import './Et2Checkbox/Et2CheckboxReadonly';
|
|
import './Et2Date/Et2Date';
|
|
import './Et2Date/Et2DateDuration';
|
|
import './Et2Date/Et2DateDurationReadonly';
|
|
import './Et2Date/Et2DateRange';
|
|
import './Et2Date/Et2DateReadonly';
|
|
import './Et2Date/Et2DateSince';
|
|
import './Et2Date/Et2DateTime';
|
|
import './Et2Date/Et2DateTimeOnly';
|
|
import './Et2Date/Et2DateTimeOnlyReadonly';
|
|
import './Et2Date/Et2DateTimeReadonly';
|
|
import './Et2Date/Et2DateTimeToday';
|
|
import './Et2Description/Et2Description';
|
|
import './Et2Dialog/Et2Dialog';
|
|
import './Et2Dialog/Et2MergeDialog';
|
|
import './Et2DropdownButton/Et2DropdownButton';
|
|
import './Et2Email/Et2Email';
|
|
import './Expose/Et2ImageExpose';
|
|
import './Expose/Et2DescriptionExpose';
|
|
import './Et2Favorites/Et2Favorites';
|
|
import './Et2Favorites/Et2FavoritesMenu';
|
|
import './Et2Image/Et2Image';
|
|
import './Et2Image/Et2AppIcon';
|
|
import './Et2Avatar/Et2LAvatar';
|
|
import './Et2Link/Et2Link';
|
|
import './Et2Link/Et2LinkAdd';
|
|
import './Et2Link/Et2LinkAppSelect';
|
|
import './Et2Link/Et2LinkEntry';
|
|
import './Et2Link/Et2LinkList';
|
|
import './Et2Link/Et2LinkSearch';
|
|
import './Et2Link/Et2LinkString';
|
|
import './Et2Link/Et2LinkTo';
|
|
import './Et2Nextmatch/ColumnSelection';
|
|
import './Et2Nextmatch/Headers/AccountFilterHeader';
|
|
import './Et2Nextmatch/Headers/CustomFilterHeader';
|
|
import './Et2Nextmatch/Headers/EntryHeader';
|
|
import './Et2Nextmatch/Headers/FilterHeader';
|
|
import './Et2MenuItem/Et2MenuItem';
|
|
import './Et2Select/Et2Listbox';
|
|
import './Et2Select/Et2Select';
|
|
import './Et2Select/SelectTypes';
|
|
import './Et2Select/Tag/Et2Tag';
|
|
import './Et2Select/Tag/Et2CategoryTag';
|
|
import './Et2Select/Tag/Et2EmailTag';
|
|
import './Et2Select/Tag/Et2ThumbnailTag';
|
|
import './Et2Spinner/Et2Spinner';
|
|
import './Et2Switch/Et2Switch';
|
|
import './Et2Textarea/Et2Textarea';
|
|
import './Et2Textarea/Et2TextareaReadonly';
|
|
import './Et2Textbox/Et2Textbox';
|
|
import './Et2Textbox/Et2TextboxReadonly';
|
|
import './Et2Textbox/Et2Number';
|
|
import './Et2Textbox/Et2NumberReadonly';
|
|
import './Et2Colorpicker/Et2Colorpicker';
|
|
import './Et2Url/Et2Url';
|
|
import './Et2Url/Et2UrlReadonly';
|
|
import './Et2Url/Et2UrlEmail';
|
|
import './Et2Url/Et2UrlEmailReadonly';
|
|
import './Et2Url/Et2UrlPhone';
|
|
import './Et2Url/Et2UrlPhoneReadonly';
|
|
import './Et2Url/Et2UrlFax';
|
|
import './Et2Url/Et2UrlFaxReadonly';
|
|
import "./Layout/Et2Split/Et2Split";
|
|
import "./Layout/RowLimitedMixin";
|
|
import "./Et2Vfs/Et2VfsMime";
|
|
import "./Et2Vfs/Et2VfsPath";
|
|
import "./Et2Vfs/Et2VfsSelectButton";
|
|
import "./Et2Vfs/Et2VfsSelectDialog";
|
|
import "./Et2Vfs/Et2VfsSelectRow";
|
|
import "./Et2Vfs/Et2VfsUid";
|
|
import "./Et2Vfs/Et2VfsName";
|
|
import "./Validators/EgwValidationFeedback";
|
|
import "./Et2Textbox/Et2Password";
|
|
import './Et2Textbox/Et2Searchbox';
|
|
import "./Et2Tree/Et2Tree";
|
|
import "./Et2Tree/Et2TreeDropdown";
|
|
|
|
|
|
/* Include all widget classes here, we only care about them registering, not importing anything*/
|
|
import './et2_widget_vfs'; // Vfs must be first (before et2_widget_file) due to import cycle
|
|
import './et2_widget_template';
|
|
import './et2_widget_grid';
|
|
import './et2_widget_box';
|
|
import './et2_widget_hbox';
|
|
import './et2_widget_button';
|
|
import './et2_widget_entry';
|
|
import './et2_widget_textbox';
|
|
import './et2_widget_number';
|
|
import './et2_widget_selectbox';
|
|
import './et2_widget_radiobox';
|
|
import './et2_widget_date';
|
|
import './et2_widget_dialog';
|
|
import './et2_widget_diff';
|
|
import './et2_widget_styles';
|
|
import './et2_widget_html';
|
|
import './et2_widget_htmlarea';
|
|
import './et2_widget_taglist';
|
|
import './et2_widget_toolbar';
|
|
import './et2_widget_historylog';
|
|
import './et2_widget_hrule';
|
|
import './et2_widget_iframe';
|
|
import './et2_widget_file';
|
|
import './et2_widget_placeholder';
|
|
import './et2_widget_progress';
|
|
import './et2_widget_portlet';
|
|
import './et2_widget_selectAccount';
|
|
import './et2_widget_ajaxSelect';
|
|
import './et2_widget_video';
|
|
import './et2_widget_audio';
|
|
import './et2_widget_barcode';
|
|
import './et2_widget_itempicker';
|
|
import './et2_widget_script';
|
|
import './et2_widget_countdown';
|
|
import './et2_extension_nextmatch';
|
|
import './et2_extension_customfields';
|
|
import {Et2Tabs} from "./Layout/Et2Tabs/Et2Tabs";
|
|
import {Et2Dialog} from "./Et2Dialog/Et2Dialog";
|
|
|
|
|
|
/**
|
|
* The etemplate2 class manages a certain etemplate2 instance.
|
|
*
|
|
* @param _container is the DOM-Node into which the DOM-Nodes of this instance
|
|
* should be inserted
|
|
* @param _menuaction is the URL to which the form data should be submitted.
|
|
*/
|
|
export class etemplate2
|
|
{
|
|
/**
|
|
* List of loaded templates
|
|
*/
|
|
public static templates = {};
|
|
/**
|
|
* List of etemplates by loaded template
|
|
*/
|
|
private static _byTemplate = {};
|
|
|
|
|
|
private _etemplate_exec_id : string;
|
|
private readonly menuaction : string;
|
|
name : string;
|
|
private uniqueId : void | string;
|
|
private template_base_url : string;
|
|
|
|
private _widgetContainer : et2_container;
|
|
private _DOMContainer : HTMLElement;
|
|
|
|
private resize_timeout : number | boolean;
|
|
private destroy_session : any;
|
|
private close_prompt : any;
|
|
private _skip_close_prompt : boolean;
|
|
private app_obj : EgwApp;
|
|
app : string;
|
|
|
|
/**
|
|
* Flag indicating that all loading is done, and the etemplate is ready to be used by app.js
|
|
*
|
|
* onChange handler checks this to ignore change events before the etemplate is ready
|
|
*/
|
|
private ready : boolean = false;
|
|
|
|
constructor(_container : HTMLElement, _menuaction? : string, _uniqueId? : string)
|
|
{
|
|
if(typeof _menuaction == "undefined")
|
|
{
|
|
_menuaction = "EGroupware\\Api\\Etemplate::ajax_process_content";
|
|
}
|
|
|
|
// Copy the given parameters
|
|
this._DOMContainer = _container;
|
|
this.menuaction = _menuaction;
|
|
|
|
// Unique ID to prevent DOM collisions across multiple templates
|
|
this.uniqueId = _uniqueId ? _uniqueId : (_container.getAttribute("id") ? _container.getAttribute("id").replace('.', '-') : '');
|
|
|
|
/**
|
|
* Preset the object variable
|
|
* @type {et2_container}
|
|
*/
|
|
this._widgetContainer = null;
|
|
|
|
|
|
// List of templates (XML) that are known, not always used. Indexed by id.
|
|
// We share list of templates with iframes and popups
|
|
try
|
|
{
|
|
if(opener && opener.etemplate2)
|
|
{
|
|
etemplate2.templates = opener.etemplate2.templates;
|
|
}
|
|
// @ts-ignore
|
|
else if(top.etemplate2)
|
|
{
|
|
// @ts-ignore
|
|
etemplate2.templates = top.etemplate2.templates;
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
// catch security exception if opener is from a different domain
|
|
console.log('Security exception accessing etemplate2.prototype of opener or top!');
|
|
}
|
|
if(typeof etemplate2.templates == "undefined")
|
|
{
|
|
etemplate2.templates = {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the resize event of all widgets
|
|
*
|
|
* @param {jQuery.event} e
|
|
*/
|
|
public resize(e)
|
|
{
|
|
const event = e;
|
|
const self = this;
|
|
let excess_height : number | boolean = false;
|
|
|
|
// Check if the framework has an specific excess height calculation
|
|
if(typeof window.framework != 'undefined' && typeof window.framework.get_wExcessHeight != 'undefined')
|
|
{
|
|
excess_height = window.framework.get_wExcessHeight(window);
|
|
}
|
|
|
|
//@TODO implement getaccess height for other framework and remove
|
|
if(typeof event != 'undefined' && event.type == 'resize')
|
|
{
|
|
if(this.resize_timeout)
|
|
{
|
|
clearTimeout(<number>this.resize_timeout);
|
|
}
|
|
this.resize_timeout = setTimeout(function()
|
|
{
|
|
self.resize_timeout = false;
|
|
if(self._widgetContainer)
|
|
{
|
|
const appHeader = jQuery('#divAppboxHeader');
|
|
|
|
//Calculate the excess height
|
|
excess_height = egw(window).is_popup() ? jQuery(window).height() - jQuery(self._DOMContainer).height() - appHeader.outerHeight() + 11 : 0;
|
|
// Recalculate excess height if the appheader is shown
|
|
if(appHeader.length > 0 && appHeader.is(':visible'))
|
|
{
|
|
excess_height -= appHeader.outerHeight() - 9;
|
|
}
|
|
|
|
// Do not resize if the template height is bigger than screen available height
|
|
// For templates which have sub templates and they are bigger than screenHeight
|
|
if(screen.availHeight < jQuery(self._DOMContainer).height())
|
|
{
|
|
excess_height = 0;
|
|
}
|
|
|
|
// If we're visible, call the "resize" event of all functions which implement the
|
|
// "IResizeable" interface
|
|
if(jQuery(self.DOMContainer).is(":visible"))
|
|
{
|
|
self._widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
if (typeof _widget.resize === 'function')
|
|
{
|
|
_widget.resize(excess_height);
|
|
}
|
|
}, self, et2_IResizeable);
|
|
}
|
|
}
|
|
}, 100);
|
|
}
|
|
// Initial resize needs to be resized immediately (for instance for nextmatch resize)
|
|
else if(this._widgetContainer)
|
|
{
|
|
// Call the "resize" event of all functions which implement the
|
|
// "IResizeable" interface
|
|
this._widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
_widget.resize(excess_height);
|
|
}, this, et2_IResizeable);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clears the current instance.
|
|
* @param _keep_app_object keep app object
|
|
* @param _keep_session keep server-side et2 session eg. for vfs-select
|
|
*/
|
|
public clear(_keep_app_object? : boolean, _keep_session? : boolean)
|
|
{
|
|
this.DOMContainer.dispatchEvent(new Event("clear", {bubbles: true}));
|
|
|
|
// Remove any handlers on window (resize)
|
|
if(this.uniqueId)
|
|
{
|
|
jQuery(window).off("." + this.uniqueId);
|
|
}
|
|
|
|
// call our destroy_session handler, if it is not already unbind, and unbind it after
|
|
if(this.destroy_session)
|
|
{
|
|
if(!_keep_session)
|
|
{
|
|
this.destroy_session();
|
|
}
|
|
this.unbind_unload();
|
|
}
|
|
if(this._widgetContainer != null)
|
|
{
|
|
// Un-register handler
|
|
this._widgetContainer.egw().unregisterJSONPlugin(this.handle_assign, this, 'assign');
|
|
|
|
this._widgetContainer.destroy();
|
|
this._widgetContainer = null;
|
|
}
|
|
jQuery(this._DOMContainer).empty();
|
|
|
|
// Remove self from the index
|
|
for(const name in etemplate2.templates)
|
|
{
|
|
if(typeof etemplate2._byTemplate[name] == "undefined")
|
|
{
|
|
continue;
|
|
}
|
|
for(let i = 0; i < etemplate2._byTemplate[name].length; i++)
|
|
{
|
|
if(etemplate2._byTemplate[name][i] === this)
|
|
{
|
|
etemplate2._byTemplate[name].splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If using a private app object, remove all of them
|
|
if(!_keep_app_object && this.app_obj !== window.app)
|
|
{
|
|
for(const app_name in this.app_obj)
|
|
{
|
|
if(this.app_obj[app_name] instanceof EgwApp)
|
|
{
|
|
this.app_obj[app_name].destroy(this.app_obj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get widgetContainer() : et2_container
|
|
{
|
|
return this._widgetContainer;
|
|
}
|
|
|
|
get DOMContainer() : HTMLElement
|
|
{
|
|
return this._DOMContainer;
|
|
}
|
|
|
|
get etemplate_exec_id() : string
|
|
{
|
|
return this._etemplate_exec_id;
|
|
}
|
|
|
|
get isReady() : boolean
|
|
{
|
|
return this.ready;
|
|
}
|
|
|
|
/**
|
|
* Creates an associative array containing the data array managers for each part
|
|
* of the associative data array. A part is something like "content", "readonlys"
|
|
* or "sel_options".
|
|
*
|
|
* @param {object} _data object with values for attributes content, sel_options, readonlys, modifications
|
|
*/
|
|
private _createArrayManagers(_data)
|
|
{
|
|
if(typeof _data == "undefined")
|
|
{
|
|
_data = {};
|
|
}
|
|
|
|
// Create all neccessary _data entries
|
|
const neededEntries = ["content", "sel_options", "readonlys", "modifications",
|
|
"validation_errors"];
|
|
for(let i = 0; i < neededEntries.length; i++)
|
|
{
|
|
if(typeof _data[neededEntries[i]] == "undefined" || !_data[neededEntries[i]])
|
|
{
|
|
egw.debug("log", "Created not passed entry '" + neededEntries[i] +
|
|
"' in data array.");
|
|
_data[neededEntries[i]] = {};
|
|
}
|
|
}
|
|
|
|
const result = {};
|
|
|
|
// Create an array manager object for each part of the _data array.
|
|
for(const key in _data)
|
|
{
|
|
switch(key)
|
|
{
|
|
case "etemplate_exec_id": // already processed
|
|
case "app_header":
|
|
break;
|
|
case "readonlys":
|
|
result[key] = new et2_readonlysArrayMgr(_data[key]);
|
|
result[key].perspectiveData.owner = this._widgetContainer;
|
|
break;
|
|
default:
|
|
result[key] = new et2_arrayMgr(_data[key]);
|
|
result[key].perspectiveData.owner = this._widgetContainer;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Bind our unload handler to notify server that eT session/request no longer needed
|
|
*
|
|
* We only bind, if we have an etemplate_exec_id: not the case for pure client-side
|
|
* calls, eg. via et2_dialog.
|
|
*/
|
|
bind_unload()
|
|
{
|
|
// Prompt user to save for dirty popups
|
|
if(window !== egw_topWindow() && !this.close_prompt)
|
|
{
|
|
this.close_prompt = this._close_changed_prompt.bind(this);
|
|
window.addEventListener("beforeunload", this.close_prompt);
|
|
}
|
|
else if (window == egw_topWindow())
|
|
{
|
|
window.addEventListener("beforeunload", this.destroy_session);
|
|
}
|
|
if(this._etemplate_exec_id)
|
|
{
|
|
this.destroy_session = jQuery.proxy(function(ev)
|
|
{
|
|
// need to use async === "keepalive" to run via beforeunload
|
|
egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session",
|
|
[this._etemplate_exec_id], null, null, "keepalive").sendRequest();
|
|
}, this);
|
|
|
|
window.addEventListener("unload", this.destroy_session);
|
|
}
|
|
}
|
|
|
|
private _close_changed_prompt(e : BeforeUnloadEvent)
|
|
{
|
|
if(this._skip_close_prompt || !this.isDirty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Cancel the event
|
|
e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
|
|
|
|
// Chrome requires returnValue to be set
|
|
e.returnValue = '';
|
|
return "";
|
|
}
|
|
|
|
public skip_close_prompt(skip = true)
|
|
{
|
|
this._skip_close_prompt = skip;
|
|
}
|
|
|
|
/**
|
|
* Unbind our unload handler
|
|
*/
|
|
unbind_unload()
|
|
{
|
|
window.removeEventListener("beforeunload", this.destroy_session);
|
|
window.removeEventListener("beforeunload", this.close_prompt);
|
|
if(window.onbeforeunload === this.destroy_session)
|
|
{
|
|
window.onbeforeunload = null;
|
|
}
|
|
else
|
|
{
|
|
const onbeforeunload = window.onbeforeunload;
|
|
window.onbeforeunload = null;
|
|
// bind unload handler again (can NOT do it direct, as this would be quick enough to be still triggered!)
|
|
window.setTimeout(function()
|
|
{
|
|
window.onbeforeunload = onbeforeunload;
|
|
}, 100);
|
|
}
|
|
delete this.destroy_session;
|
|
}
|
|
|
|
/**
|
|
* Download a URL not triggering our unload handler and therefore destroying our et2 request
|
|
*
|
|
* We use a new anchor element to avoid not destroying other etemplates as well, which
|
|
* is what happens if we use window.location
|
|
*
|
|
* @param {string} _url
|
|
*/
|
|
download(_url)
|
|
{
|
|
const a = document.createElement('a');
|
|
a.href = _url;
|
|
a.download = 'download';
|
|
|
|
// Programmatically trigger a click on the anchor element
|
|
a.click();
|
|
}
|
|
|
|
/**
|
|
* Loads the template from the given URL and sets the data object
|
|
*
|
|
* @param {string} _name name of template
|
|
* @param {string} _url url to load template
|
|
* @param {object} _data object with attributes content, langRequire, etemplate_exec_id, ...
|
|
* @param {function} _callback called after template is loaded
|
|
* @param {object} _app local app object
|
|
* @param {boolean} _no_et2_ready true: do not send et2_ready, used by et2_dialog to not overwrite app.js et2 object
|
|
* @param {string} _open_target flag of string to distinguish between tab target and normal app object
|
|
* @return Promise
|
|
*/
|
|
async load(_name, _url, _data, _callback?, _app?, _no_et2_ready?, _open_target?)
|
|
{
|
|
this.ready = false;
|
|
let app = _app || window.app;
|
|
this.name = _name; // store top-level template name to have it available in widgets
|
|
// store template base url, in case initial template is loaded via webdav, to use that for further loads too
|
|
// need to split off domain first, as it could contain app-name part of template eg. stylite.report.xet and https://my.stylite.de/egw/...
|
|
if(_url && _url[0] != '/')
|
|
{
|
|
this.template_base_url = _url.match(/https?:\/\/[^/]+/).shift();
|
|
_url = _url.split(this.template_base_url)[1];
|
|
}
|
|
else
|
|
{
|
|
this.template_base_url = '';
|
|
}
|
|
this.template_base_url += _url.split(_name.split('.').shift())[0];
|
|
|
|
egw().debug("info", "Loaded data", _data);
|
|
const currentapp = this.app = _data.currentapp || egw().app_name();
|
|
const appname = _name.split('.')[0];
|
|
// if no app object provided and template app is not currentapp (eg. infolog CRM view)
|
|
// create private app object / closure with just classes / prototypes
|
|
if(!_app && appname && appname != currentapp || _open_target)
|
|
{
|
|
app = {classes: window.app.classes};
|
|
}
|
|
// remember used app object, to eg. use: onchange="widget.getInstanceMgr().app_object[app].callback()"
|
|
this.app_obj = app;
|
|
|
|
// extract $content['msg'] and call egw.message() with it
|
|
const msg = _data.content.msg;
|
|
if(typeof msg != 'undefined')
|
|
{
|
|
egw(window).message(msg);
|
|
delete _data.content.msg;
|
|
}
|
|
|
|
// Register a handler for AJAX responses
|
|
egw(currentapp, window).registerJSONPlugin(this.handle_assign, this, 'assign');
|
|
|
|
if(egw.debug_level() >= 3)
|
|
{
|
|
if(console.groupCollapsed)
|
|
{
|
|
egw.window.console.groupCollapsed("Loading %s into ", _name, '#' + this._DOMContainer.id);
|
|
}
|
|
}
|
|
// Timing & profiling on debug level 'log' (4)
|
|
if(egw.debug_level() >= 4)
|
|
{
|
|
if(console.time)
|
|
{
|
|
console.time(_name);
|
|
}
|
|
if(console.profile)
|
|
{
|
|
console.profile(_name);
|
|
}
|
|
var start_time = (new Date).getTime();
|
|
}
|
|
|
|
// require necessary translations from server AND the app.js file, if not already loaded
|
|
let promisses = [window.egw_ready]; // to wait for legacy-loaded JS
|
|
if(Array.isArray(_data.langRequire))
|
|
{
|
|
promisses.push(egw(currentapp, window).langRequire(window, _data.langRequire));
|
|
}
|
|
if(appname && typeof app[appname] !== "object")
|
|
{
|
|
/*
|
|
Don't have the app.ts code - load it here and delay load until its ready
|
|
promisses.push(import(egw.webserverUrl + "/" + appname + "/js/app.min.js?" + ((new Date).valueOf() / 86400 | 0).toString())
|
|
.then(() =>
|
|
{
|
|
if(typeof app.classes[appname] === "undefined")
|
|
{
|
|
throw new Error("app.classes." + appname + " not found!");
|
|
}
|
|
}));
|
|
*/
|
|
|
|
}
|
|
return Promise.all(promisses).catch((err) =>
|
|
{
|
|
console.log("et2.load(): error loading lang-files and app.js: " + err.message);
|
|
}).then(() =>
|
|
{
|
|
this.clear();
|
|
|
|
// Initialize application js
|
|
let app_callback = null;
|
|
// Only initialize once
|
|
// new app class with constructor function in app.classes[appname]
|
|
if(typeof app[appname] !== 'object' && typeof app.classes[appname] == 'function')
|
|
{
|
|
app[appname] = new app.classes[appname]();
|
|
}
|
|
else if(appname && typeof app[appname] !== "object")
|
|
{
|
|
egw.debug("warn", "Did not load '%s' JS object", appname);
|
|
}
|
|
// If etemplate current app does not match app owning the template,
|
|
// initialize the current app too
|
|
if(typeof app[this.app] !== 'object' && typeof app.classes[this.app] == 'function')
|
|
{
|
|
app[this.app] = new app.classes[this.app]();
|
|
}
|
|
if(typeof app[appname] == "object")
|
|
{
|
|
app_callback = function(_et2, _name)
|
|
{
|
|
app[appname].et2_ready(_et2, _name);
|
|
};
|
|
}
|
|
|
|
// Create the basic widget container and attach it to the DOM
|
|
this._widgetContainer = new et2_container(null);
|
|
this._widgetContainer.setApiInstance(egw(currentapp, egw.elemWindow(this._DOMContainer)));
|
|
this._widgetContainer.setInstanceManager(this);
|
|
this._widgetContainer.setParentDOMNode(this._DOMContainer);
|
|
|
|
// store the id to submit it back to server
|
|
if(_data)
|
|
{
|
|
this._etemplate_exec_id = _data.etemplate_exec_id;
|
|
// set app_header
|
|
if(typeof _data.app_header == 'string')
|
|
{
|
|
// @ts-ignore
|
|
window.egw_app_header(_data.app_header);
|
|
}
|
|
// bind our unload handler
|
|
this.bind_unload();
|
|
}
|
|
|
|
const _load = function()
|
|
{
|
|
egw.debug("log", "Loading template...");
|
|
if(egw.debug_level() >= 4 && console.timeStamp)
|
|
{
|
|
console.timeStamp("Begin rendering template");
|
|
console.time("Template load");
|
|
console.time("loadFromXML");
|
|
}
|
|
|
|
// Add into indexed list - do this before, so anything looking can find it,
|
|
// even if it's not loaded
|
|
if(typeof etemplate2._byTemplate[_name] == "undefined")
|
|
{
|
|
etemplate2._byTemplate[_name] = [];
|
|
}
|
|
etemplate2._byTemplate[_name].push(this);
|
|
|
|
// Read the XML structure of the requested template
|
|
if(etemplate2.templates[this.name].hasAttribute("slot"))
|
|
{
|
|
this.DOMContainer.setAttribute("slot", etemplate2.templates[this.name].getAttribute("slot"));
|
|
}
|
|
|
|
this._widgetContainer.loadFromXML(etemplate2.templates[this.name]);
|
|
console.timeEnd("loadFromXML");
|
|
console.time("deferred");
|
|
|
|
// List of Promises from widgets that are not quite fully loaded
|
|
const deferred = [];
|
|
|
|
// Inform the widget tree that it has been successfully loaded.
|
|
this._widgetContainer.loadingFinished(deferred);
|
|
|
|
// Connect to the window resize event
|
|
jQuery(window).on("resize." + this.uniqueId, this, function(e)
|
|
{
|
|
e.data.resize(e);
|
|
});
|
|
|
|
if(egw.debug_level() >= 3 && console.groupEnd)
|
|
{
|
|
if(console.timeStamp)
|
|
{
|
|
console.timeStamp("loading finished, waiting for deferred");
|
|
}
|
|
egw.window.console.groupEnd();
|
|
}
|
|
|
|
// Wait for everything to be loaded, then finish it up. Use timeout to give anything else a chance
|
|
// to run.
|
|
setTimeout(() =>
|
|
{
|
|
Promise.race([Promise.all(deferred),
|
|
// If loading takes too long, give some feedback so we can try to track down why
|
|
new Promise((resolve) =>
|
|
{
|
|
setTimeout(() =>
|
|
{
|
|
if(this.ready)
|
|
{
|
|
return;
|
|
}
|
|
egw.debug("error", "Loading timeout");
|
|
console.debug("Deferred widget list, look for widgets still pending.", deferred);
|
|
resolve()
|
|
}, 10000
|
|
);
|
|
})
|
|
]).then(() =>
|
|
{
|
|
|
|
console.timeEnd("deferred");
|
|
console.timeStamp("Deferred done");
|
|
// Clear dirty now that it's all loaded
|
|
this.widgetContainer.iterateOver((_widget) =>
|
|
{
|
|
_widget.resetDirty();
|
|
}, this, et2_IInput);
|
|
egw.debug("log", "Finished loading %s, triggering load event", _name);
|
|
|
|
if(typeof window.framework != 'undefined' && typeof window.framework.et2_loadingFinished != 'undefined')
|
|
{
|
|
//Call loading finished method of the framework with local window
|
|
window.framework.et2_loadingFinished(egw(window).window);
|
|
}
|
|
// Trigger the "resize" event
|
|
this.resize();
|
|
|
|
// Automatically set focus to first visible input for popups
|
|
if(this._widgetContainer._egw.is_popup() && jQuery('[autofocus]', this._DOMContainer).focus().length == 0)
|
|
{
|
|
this.focusOnFirstInput();
|
|
}
|
|
|
|
// Now etemplate is ready for others to interact with (eg: app.js)
|
|
this.ready = true;
|
|
|
|
// Tell others about it
|
|
if(typeof _callback == "function")
|
|
{
|
|
_callback.call(window, this, _name);
|
|
}
|
|
if(app_callback && _callback != app_callback && !_no_et2_ready)
|
|
{
|
|
app_callback.call(window, this, _name);
|
|
}
|
|
if(appname && appname != this.app && typeof app[this.app] == "object" && !_no_et2_ready)
|
|
{
|
|
// Loaded a template from a different application?
|
|
// Let the application that loaded it know too
|
|
app[this.app].et2_ready(this, this.name);
|
|
}
|
|
|
|
// Dispatch an event that will bubble through shadow DOM boundary (pass through custom elements)
|
|
this._DOMContainer.dispatchEvent(new CustomEvent('load', {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: this
|
|
}));
|
|
|
|
if(etemplate2.templates[this.name].attributes.onload)
|
|
{
|
|
let onload = et2_checkType(etemplate2.templates[this.name].attributes.onload.value, 'js', 'onload', {});
|
|
if(typeof onload === 'string')
|
|
{
|
|
onload = et2_compileLegacyJS(onload, this, this._widgetContainer);
|
|
}
|
|
onload.call(this._widgetContainer);
|
|
}
|
|
|
|
// Profiling
|
|
if(egw.debug_level() >= 4)
|
|
{
|
|
if(console.timeEnd)
|
|
{
|
|
console.timeEnd(_name);
|
|
}
|
|
if(console.profileEnd)
|
|
{
|
|
console.profileEnd(_name);
|
|
}
|
|
const end_time = (new Date).getTime();
|
|
let gen_time_div = jQuery('#divGenTime_' + appname);
|
|
if(!gen_time_div.length)
|
|
{
|
|
gen_time_div = jQuery('.pageGenTime');
|
|
}
|
|
gen_time_div.find('.et2RenderTime').remove();
|
|
gen_time_div.append('<span class="et2RenderTime">' + egw.lang('eT2 rendering took %1s', '' + ((end_time - start_time) / 1000)) + '</span>');
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
// Load & process
|
|
try
|
|
{
|
|
if(etemplate2.templates[_name])
|
|
{
|
|
// Set array managers first, or errors will happen
|
|
this._widgetContainer.setArrayMgrs(this._createArrayManagers(_data));
|
|
|
|
// Already have it
|
|
_load.apply(this, []);
|
|
return;
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
// weird security exception in IE denying access to template cache in opener
|
|
if(e.message == 'Permission denied')
|
|
{
|
|
etemplate2.templates = {};
|
|
}
|
|
// other error eg. in app.js et2_ready or event handlers --> rethrow it
|
|
else
|
|
{
|
|
throw e;
|
|
}
|
|
}
|
|
// Split the given data into array manager objects and pass those to the
|
|
// widget container - do this here because file is loaded async
|
|
this._widgetContainer.setArrayMgrs(this._createArrayManagers(_data));
|
|
|
|
// Asynchronously load the XET file
|
|
return et2_loadXMLFromURL(_url, function(_xmldoc)
|
|
{
|
|
|
|
// Scan for templates and store them
|
|
for(let i = 0; i < _xmldoc.childNodes.length; i++)
|
|
{
|
|
const template = _xmldoc.childNodes[i];
|
|
if(template.nodeName.toLowerCase() != "template")
|
|
{
|
|
continue;
|
|
}
|
|
etemplate2.templates[template.getAttribute("id")] = template;
|
|
if(!_name)
|
|
{
|
|
this.name = template.getAttribute("id");
|
|
}
|
|
}
|
|
_load.apply(this, []);
|
|
}, this);
|
|
});
|
|
}
|
|
|
|
public focusOnFirstInput()
|
|
{
|
|
const $input = jQuery('input:visible,et2-textbox:visible,et2-select-email:visible', this.DOMContainer)
|
|
// Date fields open the calendar popup on focus
|
|
.not('.et2_date')
|
|
.filter(function()
|
|
{
|
|
// Skip inputs that are out of tab ordering
|
|
const $this = jQuery(this);
|
|
return !$this.attr('tabindex') || parseInt($this.attr('tabIndex')) >= 0;
|
|
}).first();
|
|
|
|
// mobile device, focus only if the field is empty (usually means new entry)
|
|
// should focus always for non-mobile one
|
|
if(egwIsMobile() && $input.val() == "" || !egwIsMobile())
|
|
{
|
|
$input.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if template contains any dirty (unsaved) content
|
|
*
|
|
* @returns {Boolean}
|
|
*/
|
|
public isDirty()
|
|
{
|
|
let dirty = false;
|
|
this._widgetContainer?.iterateOver(function(_widget)
|
|
{
|
|
if(_widget.isDirty && _widget.isDirty())
|
|
{
|
|
console.info(_widget.id + " is dirty", _widget);
|
|
dirty = true;
|
|
}
|
|
}, this);
|
|
|
|
return dirty;
|
|
}
|
|
|
|
/**
|
|
* Submit the et2_container form to a blank iframe in order to activate browser autocomplete
|
|
*/
|
|
autocomplete_fixer()
|
|
{
|
|
const self = this;
|
|
const form = self._DOMContainer;
|
|
|
|
// Safari always do the autofill for password field regardless of autocomplete = off
|
|
// and since there's no other way to switch the autocomplete of, we should switch the
|
|
// form autocomplete off (e.g. compose dialog, attachment password field)
|
|
if(navigator.userAgent.match(/safari/i) && !navigator.userAgent.match(/chrome/i)
|
|
&& jQuery('input[type="password"]').length > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(form)
|
|
{
|
|
// Stop submit propagation in order to not fire other possible submit events
|
|
form.onsubmit = function(e)
|
|
{
|
|
e.stopPropagation();
|
|
};
|
|
|
|
// Firefox give a security warning when transmitting to "about:blank" from a https site
|
|
// we work around that by giving existing etemplate/empty.html url
|
|
// Safari shows same warning, thought Chrome userAgent also includes Safari
|
|
if(navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i))
|
|
{
|
|
jQuery(form).attr({action: egw.webserverUrl + '/api/templates/default/empty.html', method: 'post'});
|
|
}
|
|
// need to trigger submit because submit() would not trigger onsubmit event
|
|
// since the submit does not get fired directly via user interaction.
|
|
jQuery(form).trigger('submit');
|
|
}
|
|
}
|
|
|
|
private _set_button(button, values)
|
|
{
|
|
if(typeof button == 'string')
|
|
{
|
|
button = this._widgetContainer.getWidgetById(button);
|
|
}
|
|
// Button parameter used for submit buttons in datagrid
|
|
// TODO: This should probably go in nextmatch's getValues(), along with selected rows somehow.
|
|
// I'm just not sure how.
|
|
if(button && !values.button)
|
|
{
|
|
let i;
|
|
values.button = {};
|
|
const path = button.getPath();
|
|
let target = values;
|
|
for(i = 0; i < path.length; i++)
|
|
{
|
|
if(!values[path[i]])
|
|
{
|
|
values[path[i]] = {};
|
|
}
|
|
target = values[path[i]];
|
|
}
|
|
if(target != values || button.id.indexOf('[') != -1 && path.length == 0)
|
|
{
|
|
let indexes = button.id.split('[');
|
|
if(indexes.length > 1)
|
|
{
|
|
indexes = [indexes.shift(), indexes.join('[')];
|
|
indexes[1] = indexes[1].substring(0, indexes[1].length - 1);
|
|
const children = indexes[1].split('][');
|
|
if(children.length)
|
|
{
|
|
indexes = jQuery.merge([indexes[0]], children);
|
|
}
|
|
}
|
|
let idx = '';
|
|
for(i = 0; i < indexes.length; i++)
|
|
{
|
|
idx = indexes[i];
|
|
if(!target[idx] || target[idx]['$row_cont'])
|
|
{
|
|
target[idx] = i < indexes.length - 1 ? {} : true;
|
|
}
|
|
target = target[idx];
|
|
}
|
|
}
|
|
else if(typeof values.button == 'undefined' || jQuery.isEmptyObject(values.button))
|
|
{
|
|
delete values.button;
|
|
values[button.id] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if there is an invalid widget / all widgets are valid
|
|
*
|
|
* @param container
|
|
* @param values
|
|
* @return Promise<et2_widget>|Promise<Et2Widget>|null
|
|
*/
|
|
async isInvalid(container : et2_container | undefined, values : object | undefined) : Promise<et2_ISubmitListener> | null
|
|
{
|
|
if(typeof container === 'undefined')
|
|
{
|
|
container = this._widgetContainer;
|
|
}
|
|
if(typeof values === 'undefined')
|
|
{
|
|
values = this.getValues(container);
|
|
}
|
|
let invalid = [];
|
|
container.iterateOver(function(_widget)
|
|
{
|
|
let submit = _widget.submit(values);
|
|
if(submit === false)
|
|
{
|
|
invalid.push(_widget);
|
|
}
|
|
else if(submit instanceof Promise)
|
|
{
|
|
invalid.push(submit.then(function(ok)
|
|
{
|
|
return ok ? false : this;
|
|
}.bind(_widget)));
|
|
}
|
|
}, this, et2_ISubmitListener);
|
|
|
|
return Promise.all(invalid);
|
|
}
|
|
|
|
/**
|
|
* Submit form via ajax
|
|
*
|
|
* @param {(et2_button|string)} button button widget or string with id
|
|
* @param {boolean|string} async true: do an asynchronious submit, string: spinner message (please wait...)
|
|
* default is asynchronoush with message
|
|
* @param {boolean} no_validation - Do not do individual widget validation, just submit their current values
|
|
* @param {et2_widget|undefined} _container container to submit, default whole template
|
|
* @return {boolean} true if submit was send, false if eg. validation stoped submit
|
|
*/
|
|
submit(button, async, no_validation, _container)
|
|
{
|
|
const api = this._widgetContainer.egw();
|
|
|
|
if(typeof no_validation == 'undefined')
|
|
{
|
|
no_validation = false;
|
|
}
|
|
const container = _container || this._widgetContainer;
|
|
|
|
// Get the form values
|
|
const values = this.getValues(container);
|
|
|
|
const doSubmit = () =>
|
|
{
|
|
if(typeof async == 'undefined' || typeof async == 'string')
|
|
{
|
|
api.loading_prompt('et2_submit_spinner', true, api.lang(typeof async == 'string' ? async : 'Please wait...'));
|
|
async = true;
|
|
}
|
|
if(button)
|
|
{
|
|
this._set_button(button, values);
|
|
}
|
|
|
|
// Create the request object
|
|
if(this.menuaction)
|
|
{
|
|
|
|
//Autocomplete
|
|
this.autocomplete_fixer();
|
|
|
|
// unbind our session-destroy handler, as we are submitting
|
|
this.unbind_unload();
|
|
|
|
|
|
const request = api.json(this.menuaction, [this._etemplate_exec_id, values, no_validation], function()
|
|
{
|
|
api.loading_prompt('et2_submit_spinner', false);
|
|
}, this, async);
|
|
const request_promise = request.sendRequest();
|
|
|
|
// Set up timeout for 30 seconds
|
|
let warning_message = null;
|
|
const timeout_id = window.setTimeout(() =>
|
|
{
|
|
// Do not abort request, it might still come
|
|
api.debug("warn", "Request '" + this.menuaction + "' timeout")
|
|
warning_message = api.message(api.lang("No response from server: your data is probably NOT saved"), "warning");
|
|
api.loading_prompt('et2_submit_spinner', false);
|
|
}, 30000);
|
|
|
|
// Cancel timeout
|
|
request_promise.then(() =>
|
|
{
|
|
// Responded * and response processed *
|
|
clearTimeout(timeout_id);
|
|
if(warning_message)
|
|
{
|
|
warning_message.close();
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
this._widgetContainer.egw().debug("warn", "Missing menuaction for submit. Values: ", values);
|
|
}
|
|
}
|
|
|
|
// Trigger the submit event
|
|
let canSubmit = true;
|
|
let invalid = null;
|
|
if(!no_validation)
|
|
{
|
|
canSubmit = !(invalid = this.isInvalid(container, values));
|
|
|
|
invalid?.then((widgets) =>
|
|
{
|
|
let invalid_widgets = widgets.filter((widget) => widget);
|
|
|
|
if(invalid_widgets.length && !(invalid_widgets[0] instanceof et2_widget))
|
|
{
|
|
// Handle validation_error (messages coming back from server as a response) if widget is children of a tabbox
|
|
let tmpWidget = invalid_widgets[0];
|
|
while(tmpWidget.getParent() && tmpWidget.getType() !== 'ET2-TABBOX')
|
|
{
|
|
tmpWidget = tmpWidget.getParent();
|
|
}
|
|
//Activate the tab where the widget with validation error is located
|
|
if(tmpWidget.getType() === 'ET2-TABBOX')
|
|
{
|
|
(<Et2Tabs><unknown>tmpWidget).activateTab(invalid_widgets[0]);
|
|
}
|
|
// scroll the widget into view
|
|
if(typeof tmpWidget.scrollIntoView === 'function')
|
|
{
|
|
tmpWidget.scrollIntoView();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
doSubmit();
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
doSubmit();
|
|
}
|
|
return canSubmit;
|
|
}
|
|
|
|
/**
|
|
* Does a full form post submit necessary for downloads
|
|
*
|
|
* Only use this one if you need it, use the ajax submit() instead.
|
|
* It ensures eT2 session continues to exist on server by unbinding unload handler and rebinding it.
|
|
*
|
|
* @param {(et2_button|string)} button button widget or string with id
|
|
*/
|
|
postSubmit(button)
|
|
{
|
|
// Get the form values
|
|
const values = this.getValues(this._widgetContainer);
|
|
|
|
// Trigger the submit event
|
|
let canSubmit = true;
|
|
this._widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
if(_widget.submit(values) === false)
|
|
{
|
|
canSubmit = false;
|
|
}
|
|
}, this, et2_ISubmitListener);
|
|
|
|
if(canSubmit)
|
|
{
|
|
if(button)
|
|
{
|
|
this._set_button(button, values);
|
|
}
|
|
|
|
// unbind our session-destroy handler, as we are submitting
|
|
this.unbind_unload();
|
|
|
|
const form = jQuery("<form id='form' action='" + egw().webserverUrl +
|
|
"/index.php?menuaction=" + this._widgetContainer.egw().getAppName() + ".EGroupware\\Api\\Etemplate.process_exec&ajax=true' method='POST'>");
|
|
|
|
const etemplate_id = jQuery(document.createElement("input"))
|
|
.attr("name", 'etemplate_exec_id')
|
|
.attr("type", 'hidden')
|
|
.val(this._etemplate_exec_id)
|
|
.appendTo(form);
|
|
|
|
const input = document.createElement("input");
|
|
input.type = "hidden";
|
|
input.name = 'value';
|
|
input.value = egw().jsonEncode(values);
|
|
form.append(input);
|
|
form.appendTo(jQuery('body')).submit();
|
|
|
|
// bind unload handler again (can NOT do it direct, as this would be quick enough to be still triggered!)
|
|
window.setTimeout(jQuery.proxy(this.bind_unload, this), 100);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches all input element values and returns them in an associative
|
|
* array. Widgets which introduce namespacing can use the internal _target
|
|
* parameter to add another layer.
|
|
*
|
|
* @param {et2_widget} _root widget to start iterating
|
|
* @param {boolean} skip_reset_dirty true: do NOT reset dirty status
|
|
*/
|
|
getValues(_root : et2_widget, skip_reset_dirty : boolean)
|
|
{
|
|
const result = {};
|
|
|
|
// Iterate over the widget tree
|
|
_root.iterateOver(function(_widget)
|
|
{
|
|
// The widget must have an id to be included in the values array
|
|
if(_widget.id === undefined || _widget.id === "")
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the path to the node we have to store the value at
|
|
let path = _widget.getPath();
|
|
|
|
// check if id contains a hierachical name, eg. "button[save]"
|
|
let id = _widget.id || "";
|
|
let indexes = id?.split('[');
|
|
if(indexes?.length > 1)
|
|
{
|
|
indexes = [indexes.shift(), indexes.join('[')];
|
|
indexes[1] = indexes[1].substring(0, indexes[1].length - 1);
|
|
const children = indexes[1].split('][');
|
|
if(children.length)
|
|
{
|
|
indexes = jQuery.merge([indexes[0]], children);
|
|
}
|
|
path = path.concat(indexes);
|
|
// Take the last one as the ID
|
|
id = path.pop();
|
|
}
|
|
|
|
// Set the _target variable to that node
|
|
let _target = result;
|
|
for(var i = 0; i < path.length; i++)
|
|
{
|
|
// Create a new object for not-existing path nodes
|
|
if(typeof _target[path[i]] === 'undefined')
|
|
{
|
|
_target[path[i]] = {};
|
|
}
|
|
|
|
// Check whether the path node is really an object
|
|
if(typeof _target[path[i]] === 'object')
|
|
{
|
|
_target = _target[path[i]];
|
|
}
|
|
else
|
|
{
|
|
egw.debug("error", "ID collision while writing at path " +
|
|
"node '" + path[i] + "'");
|
|
}
|
|
}
|
|
|
|
// Handle arrays, eg radio[]
|
|
if(id === "")
|
|
{
|
|
id = typeof _target == "undefined" ? 0 : Object.keys(_target).length;
|
|
}
|
|
|
|
const value = _widget.getValue(true); // true: let widget know getValue() / submit is calling it
|
|
|
|
// Check whether the entry is really undefined
|
|
if(typeof _target[id] != "undefined" && (typeof _target[id] != 'object' || typeof value != 'object'))
|
|
{
|
|
// Don't warn about children of nextmatch header - they're part of nm value
|
|
if(!_widget.getParent().instanceOf(et2_nextmatch_header_bar))
|
|
{
|
|
egw.debug("warn", _widget, "Overwriting value of '" + _widget.id +
|
|
"', id exists twice!");
|
|
}
|
|
}
|
|
|
|
// Store the value of the widget and reset its dirty flag
|
|
if(value !== null)
|
|
{
|
|
// Merge, if possible (link widget)
|
|
if(typeof _target[id] == 'object' && typeof value == 'object')
|
|
{
|
|
_target[id] = jQuery.extend({}, _target[id], value);
|
|
}
|
|
else
|
|
{
|
|
_target[id] = value;
|
|
}
|
|
}
|
|
else if(jQuery.isEmptyObject(_target))
|
|
{
|
|
// Avoid sending back empty sub-arrays
|
|
_target = result;
|
|
for(var i = 0; i < path.length - 1; i++)
|
|
{
|
|
_target = _target[path[i]];
|
|
}
|
|
delete _target[path[path.length - 1]];
|
|
}
|
|
if (!skip_reset_dirty)
|
|
{
|
|
_widget.resetDirty();
|
|
}
|
|
|
|
}, this, et2_IInput);
|
|
|
|
egw().debug("info", "Value", result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* "Intelligently" refresh the template based on the given ID
|
|
*
|
|
* Rather than blindly re-load the entire template, we try to be a little smarter about it.
|
|
* If there's a message provided, we try to find where it goes and set it directly. Then
|
|
* we look for a nextmatch widget, and tell it to refresh its data based on that ID.
|
|
*
|
|
* @see egw_message.refresh()
|
|
*
|
|
* @param {string} msg message to try to display. eg: "Entry added" (not used anymore, handeled by egw_refresh and egw_message)
|
|
* @param {string} app app-name
|
|
* @param {(string|null)} id application specific entry ID to try to refresh
|
|
* @param {(string|null)} type type of change. One of 'update','edit', 'delete', 'add' or null
|
|
* @return {boolean} true if nextmatch found and refreshed, false if not
|
|
*/
|
|
refresh(msg, app, id, type)
|
|
{
|
|
// msg, app; // unused but required by function signature
|
|
let refresh_done = false;
|
|
|
|
// Refresh nextmatches
|
|
this._widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
// Trigger refresh
|
|
_widget.refresh(id, type);
|
|
refresh_done = true;
|
|
}, this, et2_nextmatch);
|
|
|
|
return refresh_done;
|
|
}
|
|
|
|
/**
|
|
* "Intelligently" refresh a given app
|
|
*
|
|
* @see egw_message.refresh()
|
|
*
|
|
* @param {string} _msg message to try to display. eg: "Entry added" (not used anymore, handeled by egw_refresh and egw_message)
|
|
* @param {string} _app app-name
|
|
* @param {(string|null)} _id application specific entry ID to try to refresh
|
|
* @param {(string|null)} _type type of change. One of 'update','edit', 'delete', 'add' or null
|
|
* @return {boolean} true if nextmatch found and refreshed, false if not
|
|
*/
|
|
static app_refresh(_msg, _app, _id, _type)
|
|
{
|
|
let refresh_done = false;
|
|
let app = _app.split('-');
|
|
const et2 = etemplate2.getByApplication(app[0]);
|
|
for(let i = 0; i < et2.length; i++)
|
|
{
|
|
if(app[1])
|
|
{
|
|
if(et2[i]['uniqueId'].match(_app))
|
|
{
|
|
refresh_done = et2[i].refresh(_msg, app[0], _id, _type) || refresh_done;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
refresh_done = et2[i].refresh(_msg, app[0], _id, _type) || refresh_done;
|
|
}
|
|
}
|
|
return refresh_done;
|
|
}
|
|
|
|
/**
|
|
* "Intelligently" print a given etemplate
|
|
*
|
|
* Mostly, we let the nextmatch change how many rows it's showing, so you don't
|
|
* get just one printed page.
|
|
*
|
|
* @return {Deferred[]} A list of Deferred objects that must complete before
|
|
* actual printing can begin.
|
|
*/
|
|
public print()
|
|
{
|
|
// Sometimes changes take time
|
|
const deferred = [];
|
|
|
|
// Skip hidden etemplates
|
|
if(jQuery(this._DOMContainer).filter(':visible').length === 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
// Allow any widget to change for printing
|
|
this._widgetContainer.iterateOver(function(_widget)
|
|
{
|
|
// Skip widgets from a different etemplate (home)
|
|
if(_widget.getInstanceManager() != this)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Skip hidden widgets
|
|
if(jQuery(_widget.getDOMNode()).filter(':visible').length === 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const result = _widget.beforePrint();
|
|
if(typeof result == "object")
|
|
{
|
|
deferred.push(result);
|
|
}
|
|
}, this, et2_IPrint);
|
|
|
|
return deferred;
|
|
}
|
|
|
|
|
|
// Some static things to make getting into widget context a little easier //
|
|
|
|
|
|
/**
|
|
* Get a list of etemplate2 objects that loaded the given template name
|
|
*
|
|
* @param template String Name of the template that was loaded
|
|
*
|
|
* @return Array list of etemplate2 that have that template
|
|
*/
|
|
|
|
public static getByTemplate(template) : etemplate2[]
|
|
{
|
|
if(typeof etemplate2._byTemplate[template] != "undefined")
|
|
{
|
|
return etemplate2._byTemplate[template];
|
|
}
|
|
else
|
|
{
|
|
// Return empty array so you can always iterate over results
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a list of etemplate2 objects that are associated with the given application
|
|
*
|
|
* "Associated" is determined by the first part of the template
|
|
*
|
|
* @param {string} app app-name
|
|
* @return {etemplate2[]} list of etemplate2 that have that app as the first part of their loaded template
|
|
*/
|
|
public static getByApplication(app) : etemplate2[]
|
|
{
|
|
let list = [];
|
|
for(let name in etemplate2._byTemplate)
|
|
{
|
|
if(name.indexOf(app + ".") == 0)
|
|
{
|
|
list = list.concat(etemplate2._byTemplate[name]);
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
* Get a etemplate2 object from the given DOM ID
|
|
*
|
|
* @param {string} id DOM ID of the container node
|
|
* @returns {etemplate2|null}
|
|
*/
|
|
public static getById(id) : etemplate2|null
|
|
{
|
|
for(let name in etemplate2._byTemplate)
|
|
{
|
|
for(let i = 0; i < etemplate2._byTemplate[name].length; i++)
|
|
{
|
|
const et = etemplate2._byTemplate[name][i];
|
|
|
|
if(et._DOMContainer.getAttribute("id") == id)
|
|
{
|
|
return et;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Plugin for egw.json type "et2_load"
|
|
*
|
|
* @param _type
|
|
* @param _response
|
|
* @returns Promise
|
|
*/
|
|
public static async handle_load(_type, _response)
|
|
{
|
|
// Check the parameters
|
|
const data = _response.data;
|
|
// window-close does NOT send data.DOMNodeID!
|
|
const dialog = <any>document.querySelector('et2-dialog > form' + (data.DOMNodeID ? '#' + data.DOMNodeID : '.dialog_content'))?.parentNode ??
|
|
// Reloaded into same container
|
|
(this?.DOMContainer?.parentNode instanceof Et2Dialog ? this.DOMContainer.parentNode : undefined);
|
|
|
|
if (dialog)
|
|
{
|
|
// stop dialogs from being closed on button click
|
|
dialog.callback = () => false;
|
|
}
|
|
|
|
// handle Api\Framework::refresh_opener()
|
|
if(Array.isArray(data['refresh-opener']))
|
|
{
|
|
if(window.opener || dialog)// && typeof window.opener.egw_refresh == 'function')
|
|
{
|
|
const egw = window.egw(dialog ? window : opener);
|
|
egw.refresh.apply(egw, data['refresh-opener']);
|
|
}
|
|
}
|
|
const egw = window.egw(window);
|
|
|
|
// need to set app_header before message, as message temp. replaces app_header
|
|
if(typeof data.data == 'object' && typeof data.data.app_header == 'string')
|
|
{
|
|
if (dialog)
|
|
{
|
|
dialog.title = data.data.app_header;
|
|
}
|
|
else
|
|
{
|
|
egw.app_header(data.data.app_header, data.data.currentapp || null);
|
|
}
|
|
delete data.data.app_header;
|
|
}
|
|
|
|
// handle Api\Framework::message()
|
|
if(Array.isArray(data.message))
|
|
{
|
|
egw.message.apply(egw, data.message);
|
|
}
|
|
|
|
// handle Api\Framework::window_close(), this will terminate execution
|
|
if(data['window-close'])
|
|
{
|
|
if(typeof data['window-close'] == 'string' && data['window-close'] !== 'true')
|
|
{
|
|
alert(data['window-close']);
|
|
}
|
|
if (dialog)
|
|
{
|
|
return dialog.close();
|
|
}
|
|
egw.close();
|
|
return true;
|
|
}
|
|
|
|
// handle Api\Framework::window_focus()
|
|
if(data['window-focus'])
|
|
{
|
|
window.focus();
|
|
}
|
|
|
|
// handle framework.setSidebox calls
|
|
if(!dialog && window.framework && Array.isArray(data.setSidebox))
|
|
{
|
|
if(data['fw-target'])
|
|
{
|
|
data.setSidebox[0] = data['fw-target'];
|
|
}
|
|
|
|
window.framework.setSidebox.apply(window.framework, data.setSidebox);
|
|
}
|
|
|
|
// regular et2 re-load
|
|
if(typeof data.url == "string" && typeof data.data === 'object')
|
|
{
|
|
let load : Promise<any>;
|
|
// @ts-ignore
|
|
if(this && typeof this.load == 'function')
|
|
{
|
|
// Called from etemplate
|
|
// set id in case serverside returned a different template
|
|
this._DOMContainer.id = this.uniqueId = data.DOMNodeID;
|
|
|
|
// @ts-ignore
|
|
load = this.load(data.name, data.url, data.data);
|
|
}
|
|
else
|
|
{
|
|
// Not etemplate
|
|
const node = document.getElementById(data.DOMNodeID);
|
|
let uniqueId = data.DOMNodeID;
|
|
if(node)
|
|
{
|
|
if(node.children.length)
|
|
{
|
|
// Node has children already? Check for loading over an
|
|
// existing etemplate
|
|
const old = etemplate2.getById(node.id);
|
|
if(old)
|
|
{
|
|
old.clear();
|
|
}
|
|
}
|
|
if(data['open_target'] && !uniqueId.match(data['open_target']))
|
|
{
|
|
uniqueId = data.DOMNodeID.replace('.', '-') + '-' + data['open_target'];
|
|
}
|
|
const et2 = new etemplate2(node, data.data.menuaction, uniqueId);
|
|
load = et2.load(data.name, data.url, data.data, null, null, null, data['fw-target']);
|
|
}
|
|
else
|
|
{
|
|
egw.debug("error", "Could not find target node %s", data.DOMNodeID);
|
|
}
|
|
}
|
|
|
|
// Extra handling for being loaded into a Et2Dialog
|
|
if(dialog)
|
|
{
|
|
load.then(() =>
|
|
{
|
|
// Move footer type buttons into dialog footer
|
|
const buttons = dialog._adoptTemplateButtons();
|
|
|
|
// Make sure adopted buttons are removed on clear
|
|
dialog.addEventListener("clear", () =>
|
|
{
|
|
buttons.forEach(n => n.remove());
|
|
});
|
|
});
|
|
}
|
|
return load;
|
|
}
|
|
|
|
throw("Error while parsing et2_load response");
|
|
}
|
|
|
|
/**
|
|
* Plugin for egw.json type "et2_validation_error"
|
|
*
|
|
* @param _type
|
|
* @param _response
|
|
*/
|
|
public static handle_validation_error(_type, _response)
|
|
{
|
|
// Display validation errors
|
|
for(let id in _response.data)
|
|
{
|
|
// @ts-ignore
|
|
const widget = this._widgetContainer.getWidgetById(id);
|
|
if(widget && widget.instanceOf(et2_baseWidget))
|
|
{
|
|
(<et2_baseWidget>widget).showMessage(_response.data[id], 'validation_error');
|
|
|
|
}
|
|
else if(typeof widget.set_validation_error == "function")
|
|
{
|
|
widget.set_validation_error(_response.data[id]);
|
|
}
|
|
// Handle validation_error (messages coming back from server as a response) if widget is children of a tabbox
|
|
let tmpWidget = widget;
|
|
while(tmpWidget.getParent() && tmpWidget.getType() !== 'ET2-TABBOX')
|
|
{
|
|
tmpWidget = tmpWidget.getParent();
|
|
}
|
|
//Activate the tab where the widget with validation error is located
|
|
if(tmpWidget.getType() === 'ET2-TABBOX')
|
|
{
|
|
(<Et2Tabs><unknown>tmpWidget).activateTab(widget);
|
|
}
|
|
// scroll the widget into view
|
|
if (typeof widget.getDOMNode().scrollIntoView === 'function')
|
|
{
|
|
widget.scrollIntoView();
|
|
}
|
|
}
|
|
egw().debug("warn", "Validation errors", _response.data);
|
|
}
|
|
|
|
/**
|
|
* Handle assign for attributes on etemplate2 widgets
|
|
*
|
|
* @param {string} type "assign"
|
|
* @param {object} res Response
|
|
* res.data.id {String} Widget ID
|
|
* res.data.key {String} Attribute name
|
|
* res.data.value New value for widget
|
|
* res.data.etemplate_exec_id
|
|
* @param {object} req
|
|
* @returns {Boolean} Handled by this plugin
|
|
* @throws Invalid parameters if the required res.data parameters are missing
|
|
*/
|
|
public handle_assign(type, res, req)
|
|
{
|
|
//type, req; // unused, but required by plugin signature
|
|
|
|
//Check whether all needed parameters have been passed and call the alertHandler function
|
|
if((typeof res.data.id != 'undefined') &&
|
|
(typeof res.data.key != 'undefined') &&
|
|
(typeof res.data.value != 'undefined')
|
|
)
|
|
{
|
|
if(typeof res.data.etemplate_exec_id == 'undefined' ||
|
|
res.data.etemplate_exec_id != this._etemplate_exec_id)
|
|
{
|
|
// Not for this etemplate, but not an error
|
|
return false;
|
|
}
|
|
if(res.data.key == 'etemplate_exec_id')
|
|
{
|
|
this._etemplate_exec_id = res.data.value;
|
|
return true;
|
|
}
|
|
if(this._widgetContainer == null)
|
|
{
|
|
// Right etemplate, but it's already been cleared.
|
|
egw.debug('warn', "Tried to call assign on an un-loaded etemplate", res.data);
|
|
return false;
|
|
}
|
|
const widget = this._widgetContainer.getWidgetById(res.data.id);
|
|
if(widget)
|
|
{
|
|
if(typeof widget['set_' + res.data.key] != 'function')
|
|
{
|
|
egw.debug('warn', "Cannot set %s attribute %s via JSON assign, no set_%s()", res.data.id, res.data.key, res.data.key);
|
|
return false;
|
|
}
|
|
try
|
|
{
|
|
widget['set_' + res.data.key].call(widget, res.data.value);
|
|
return true;
|
|
}
|
|
catch(e)
|
|
{
|
|
egw.debug("error", "When assigning %s on %s via AJAX, \n" + (e.message || e + ""), res.data.key, res.data.id, widget);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
throw 'Invalid parameters';
|
|
}
|
|
}
|
|
|
|
// make etemplate2 global, as we need it to check an app uses it and then call methods on it
|
|
if(typeof window.etemplate2 === 'undefined')
|
|
{
|
|
window['etemplate2'] = etemplate2;
|
|
}
|
|
|
|
// Calls etemplate2_handle_response in the context of the object which
|
|
// requested the response from the server
|
|
egw(window).registerJSONPlugin(etemplate2.handle_load, null, 'et2_load');
|
|
egw(window).registerJSONPlugin(etemplate2.handle_validation_error, null, 'et2_validation_error'); |