From 55ae9c1c7b76da83b5d8984ff9822fd5a23822e5 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 15 Jan 2020 08:47:33 +0100 Subject: [PATCH 01/61] first try with TypeScript: timesheet app.js incl. new egw_app base-class --- api/js/etemplate/et2_types.d.ts | 148 +++ api/js/jsapi/egw.js | 7 + api/js/jsapi/egw_app.js | 1634 +++++++++++++++++++++++++ api/js/jsapi/egw_app.ts | 2029 +++++++++++++++++++++++++++++++ api/js/jsapi/egw_global.d.ts | 16 + package.json | 2 + timesheet/js/app.js | 358 +++--- timesheet/js/app.ts | 196 +++ tsconfig.json | 23 + 9 files changed, 4216 insertions(+), 197 deletions(-) create mode 100644 api/js/etemplate/et2_types.d.ts create mode 100644 api/js/jsapi/egw_app.js create mode 100644 api/js/jsapi/egw_app.ts create mode 100644 api/js/jsapi/egw_global.d.ts create mode 100644 timesheet/js/app.ts create mode 100644 tsconfig.json diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts new file mode 100644 index 0000000000..c6616da7c3 --- /dev/null +++ b/api/js/etemplate/et2_types.d.ts @@ -0,0 +1,148 @@ +declare module eT2 +{ + +} +declare var etemplate2 : any; +declare var et2_DOMWidget : any; +declare var et2_surroundingsMgr : any; +declare var et2_arrayMgr : any; +declare var et2_readonlysArrayMgr : any; +declare var et2_baseWidget : any; +declare var et2_container : any; +declare var et2_placeholder : any; +declare var et2_validTypes : any; +declare var et2_typeDefaults : any; +declare var et2_no_init : any; +declare var et2_editableWidget : any; +declare var et2_inputWidget : any; +declare var et2_IDOMNode : any; +declare var et2_IInput : any; +declare var et2_IResizeable : any; +declare var et2_IAligned : any; +declare var et2_ISubmitListener : any; +declare var et2_IDetachedDOM : any; +declare var et2_IPrint : any; +declare var et2_valueWidget : any; +declare var et2_registry : any; +declare var et2_widget : any; +declare var et2_dataview : any; +declare var et2_dataview_controller : any; +declare var et2_dataview_selectionManager : any; +declare var et2_dataview_IInvalidatable : any; +declare var et2_dataview_IViewRange : any; +declare var et2_IDataProvider : any; +declare var et2_dataview_column : any; +declare var et2_dataview_columns : any; +declare var et2_dataview_container : any; +declare var et2_dataview_grid : any; +declare var et2_dataview_row : any; +declare var et2_dataview_rowProvider : any; +declare var et2_dataview_spacer : any; +declare var et2_dataview_tile : any; +declare var et2_customfields_list : any; +declare var et2_INextmatchHeader : any; +declare var et2_INextmatchSortable : any; +declare var et2_nextmatch : any; +declare var et2_nextmatch_header_bar : any; +declare var et2_nextmatch_header : any; +declare var et2_nextmatch_customfields : any; +declare var et2_nextmatch_sortheader : any; +declare var et2_nextmatch_filterheader : any; +declare var et2_nextmatch_accountfilterheader : any; +declare var et2_nextmatch_taglistheader : any; +declare var et2_nextmatch_entryheader : any; +declare var et2_nextmatch_customfilter : any; +declare var et2_nextmatch_controller : any; +declare var et2_dynheight : any; +declare var et2_nextmatch_rowProvider : any; +declare var et2_nextmatch_rowWidget : any; +declare var et2_nextmatch_rowTemplateWidget : any; +declare var et2_ajaxSelect : any; +declare var et2_ajaxSelect_ro : any; +declare var et2_barcode : any; +declare var et2_box : any; +declare var et2_details : any; +declare var et2_button : any; +declare var et2_checkbox : any; +declare var et2_checkbox_ro : any; +declare var et2_color : any; +declare var et2_color_ro : any; +declare var et2_date : any; +declare var et2_date_duration : any; +declare var et2_date_duration_ro : any; +declare var et2_date_ro : any; +declare var et2_date_range : any; +declare var et2_description : any; +declare var et2_dialog : any; +declare var et2_diff : any; +declare var et2_dropdown_button : any; +declare var et2_entry : any; +declare var et2_favorites : any; +declare var et2_file : any; +declare var et2_grid : any; +declare var et2_groupbox : any; +declare var et2_groupbox_legend : any; +declare var et2_hbox : any; +declare var et2_historylog : any; +declare var et2_hrule : any; +declare var et2_html : any; +declare var et2_htmlarea : any; +declare var et2_iframe : any; +declare var et2_image : any; +declare var et2_appicon : any; +declare var et2_avatar : any; +declare var et2_avatar_ro : any; +declare var et2_lavatar : any; +declare var et2_itempicker : any; +declare var et2_link_to : any; +declare var et2_link_apps : any; +declare var et2_link_entry : any; +declare var et2_link_string : any; +declare var et2_link_list : any; +declare var et2_link_add : any; +declare var et2_number : any; +declare var et2_number_ro : any; +declare var et2_portlet : any; +declare var et2_progress : any; +declare var et2_radiobox : any; +declare var et2_radiobox_ro : any; +declare var et2_radioGroup : any; +declare var et2_script : any; +declare var et2_selectAccount : any; +declare var et2_selectAccount_ro : any; +declare var et2_selectbox : any; +declare var et2_selectbox_ro : any; +declare var et2_menulist : any; +declare var et2_split : any; +declare var et2_styles : any; +declare var et2_tabbox : any; +declare var et2_taglist : any; +declare var et2_taglist_account : any; +declare var et2_taglist_email : any; +declare var et2_taglist_category : any; +declare var et2_taglist_thumbnail : any; +declare var et2_taglist_state : any; +declare var et2_taglist_ro : any; +declare var et2_template : any; +declare var et2_textbox : any; +declare var et2_textbox_ro : any; +declare var et2_searchbox : any; +declare var et2_timestamper : any; +declare var et2_toolbar : any; +declare var et2_tree : any; +declare var et2_url : any; +declare var et2_url_ro : any; +declare var et2_vfs : any; +declare var et2_vfsName : any; +declare var et2_vfsPath : any; +declare var et2_vfsName_ro : any; +declare var et2_vfsMime : any; +declare var et2_vfsSize : any; +declare var et2_vfsMode : any; +declare var et2_vfsUid : any; +declare var et2_vfsUpload : any; +declare var et2_vfsSelect : any; +declare var et2_video : any; +declare var et2_IExposable : any; +declare function et2_createWidget(type : string, params : {}, parent? : any) : any; +declare function nm_action(_action : {}, _senders : [], _target : any, _ids? : any) : void; \ No newline at end of file diff --git a/api/js/jsapi/egw.js b/api/js/jsapi/egw.js index ff8dcfb944..f8bb84c042 100644 --- a/api/js/jsapi/egw.js +++ b/api/js/jsapi/egw.js @@ -437,6 +437,13 @@ }; })(); +// get TypeScript modules working with our loader +function require(_file) +{ + return { EgwApp: window.EgwApp}; +} +var exports = {}; + /** * Call a function specified by it's name (possibly dot separated, eg. "app.myapp.myfunc") * diff --git a/api/js/jsapi/egw_app.js b/api/js/jsapi/egw_app.js new file mode 100644 index 0000000000..08a036a7fd --- /dev/null +++ b/api/js/jsapi/egw_app.js @@ -0,0 +1,1634 @@ +"use strict"; +/** + * EGroupware clientside Application javascript base object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Ralf Becker + * @author Hadi Nategh + * @author Nathan Gray + */ +Object.defineProperty(exports, "__esModule", { value: true }); +require("jquery"); +require("jqueryui"); +require("../jsapi/egw_global"); +require("../etemplate/et2_types"); +/** + * Common base class for application javascript + * Each app should extend as needed. + * + * All application javascript should be inside. Intitialization goes in init(), + * clean-up code goes in destroy(). Initialization is done once all js is loaded. + * + * var app.appname = AppJS.extend({ + * // Actually set this one, the rest is example + * appname: appname, + * + * internal_var: 1000, + * + * init: function() + * { + * // Call the super + * this._super.apply(this, arguments); + * + * // Init the stuff + * if ( egw.preference('dateformat', 'common') ) + * { + * // etc + * } + * }, + * _private: function() + * { + * // Underscore private by convention + * } + * }); + * + * @class AppJS + * @augments Class + */ +var EgwApp = /** @class */ (function () { + /** + * Initialization and setup goes here, but the etemplate2 object + * is not yet ready. + */ + function EgwApp() { + /** + * Mailvelope "egroupware" Keyring + */ + this.mailvelope_keyring = undefined; + this.egw = egw(this.appname, window); + // Initialize sidebox for non-popups. + // ID set server side + if (!this.egw.is_popup()) { + var sidebox = jQuery('#favorite_sidebox_' + this.appname); + if (sidebox.length == 0 && egw_getFramework() != null) { + var egw_fw = egw_getFramework(); + sidebox = jQuery('#favorite_sidebox_' + this.appname, egw_fw.sidemenuDiv); + } + // Make sure we're running in the top window when we init sidebox + //@ts-ignore + if (window.app[this.appname] === this && window.top.app[this.appname] !== this && window.top.app[this.appname]) { + //@ts-ignore + window.top.app[this.appname]._init_sidebox(sidebox); + } + else { + this._init_sidebox(sidebox); + } + } + this.mailvelopeSyncHandlerObj = this.mailvelopeSyncHandler(); + } + /** + * Clean up any created objects & references + * @param {object} _app local app object + */ + EgwApp.prototype.destroy = function (_app) { + delete this.et2; + if (this.sidebox) + this.sidebox.off(); + delete this.sidebox; + if (!_app) + delete app[this.appname]; + }; + /** + * This function is called when the etemplate2 object is loaded + * and ready. If you must store a reference to the et2 object, + * make sure to clean it up in destroy(). Note that this can be called + * several times, with different et2 objects, as templates are loaded. + * + * @param {etemplate2} et2 + * @param {string} name template name + */ + EgwApp.prototype.et2_ready = function (et2, name) { + if (this.et2 !== null) { + egw.debug('log', "Changed et2 object"); + } + this.et2 = et2.widgetContainer; + this._fix_iFrameScrolling(); + if (this.egw && this.egw.is_popup()) + this._set_Window_title(); + // Highlights the favorite based on initial list state + this.highlight_favorite(); + }; + /** + * Observer method receives update notifications from all applications + * + * App is responsible for only reacting to "messages" it is interested in! + * + * @param {string} _msg message (already translated) to show, eg. 'Entry deleted' + * @param {string} _app application name + * @param {(string|number)} _id id of entry to refresh or null + * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {string} _msg_type 'error', 'warning' or 'success' (default) + * @param {object|null} _links app => array of ids of linked entries + * or null, if not triggered on server-side, which adds that info + * @return {false|*} false to stop regular refresh, thought all observers are run + */ + EgwApp.prototype.observer = function (_msg, _app, _id, _type, _msg_type, _links) { + }; + /** + * Push method receives push notification about updates to entries from the application + * + * It can use the extra _data parameter to determine if the client has read access to + * the entry - if an update of the list is necessary. + * + * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {string} _app application name + * @param {(string|number)} _id id of entry to refresh or null + * @param {mixed} _data eg. owner or responsible to decide if update is necessary + * @returns {undefined} + */ + EgwApp.prototype.push = function (_type, _app, _id, _data) { + }; + /** + * Open an entry. + * + * Designed to be used with the action system as a callback + * eg: onExecute => app..open + * + * @param _action + * @param _senders + */ + EgwApp.prototype.open = function (_action, _senders) { + var id_app = _senders[0].id.split('::'); + egw.open(id_app[1], this.appname); + }; + EgwApp.prototype._do_action = function (action_id, selected) { + }; + /** + * A generic method to action to server asynchronously + * + * Designed to be used with the action system as a callback. + * In the PHP side, set the action + * 'onExecute' => 'javaScript:app..action', and + * implement _do_action(action_id, selected) + * + * @param {egwAction} _action + * @param {egwActionObject[]} _elems + */ + EgwApp.prototype.action = function (_action, _elems) { + // let user confirm select-all + var select_all = _action.getManager().getActionById("select_all"); + var confirm_msg = (_elems.length > 1 || select_all && select_all.checked) && + typeof _action.data.confirm_multiple != 'undefined' ? + _action.data.confirm_multiple : _action.data.confirm; + if (typeof confirm_msg != 'undefined') { + var that = this; + var action_id = _action.id; + et2_dialog.show_dialog(function (button_id, value) { + if (button_id != et2_dialog.NO_BUTTON) { + that._do_action(action_id, _elems); + } + }, confirm_msg, egw.lang('Confirmation required'), et2_dialog.BUTTONS_YES_NO, et2_dialog.QUESTION_MESSAGE); + } + else if (typeof this._do_action == 'function') { + this._do_action(_action.id, _elems); + } + else { + // If this is a nextmatch action, do an ajax submit setting the action + var nm = null; + var action = _action; + while (nm == null && action.parent != null) { + if (action.data.nextmatch) + nm = action.data.nextmatch; + action = action.parent; + } + if (nm != null) { + var value = {}; + value[nm.options.settings.action_var] = _action.id; + nm.set_value(value); + nm.getInstanceManager().submit(); + } + } + }; + /** + * Set the application's state to the given state. + * + * While not pretending to implement the history API, it is patterned similarly + * @link http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html + * + * The default implementation works with the favorites to apply filters to a nextmatch. + * + * + * @param {{name: string, state: object}|string} state Object (or JSON string) for a state. + * Only state is required, and its contents are application specific. + * @param {string} template template name to check, instead of trying all templates of current app + * @return {boolean} false - Returns false to stop event propagation + */ + EgwApp.prototype.setState = function (state, template) { + // State should be an object, not a string, but we'll parse + if (typeof state == "string") { + if (state.indexOf('{') != -1 || state == 'null') { + state = JSON.parse(state); + } + } + if (typeof state != "object") { + egw.debug('error', 'Unable to set state to %o, needs to be an object', state); + return; + } + if (state == null) { + state = {}; + } + // Check for egw.open() parameters + if (state.state && state.state.id && state.state.app) { + return egw.open(state.state, undefined, undefined, {}, '_self'); + } + // Try and find a nextmatch widget, and set its filters + var nextmatched = false; + var et2 = template ? etemplate2.getByTemplate(template) : etemplate2.getByApplication(this.appname); + for (var i = 0; i < et2.length; i++) { + et2[i].widgetContainer.iterateOver(function (_widget) { + // Firefox has trouble with spaces in search + if (state.state && state.state.search) + state.state.search = unescape(state.state.search); + // Apply + if (state.state && state.state.sort && state.state.sort.id) { + _widget.sortBy(state.state.sort.id, state.state.sort.asc, false); + } + if (state.state && state.state.selectcols) { + // Make sure it's a real array, not an object, then set cols + _widget.set_columns(jQuery.extend([], state.state.selectcols)); + } + _widget.applyFilters(state.state || state.filter || {}); + nextmatched = true; + }, this, et2_nextmatch); + if (nextmatched) + return false; + } + // 'blank' is the special name for no filters, send that instead of the nice translated name + var safe_name = jQuery.isEmptyObject(state) || jQuery.isEmptyObject(state.state || state.filter) ? 'blank' : state.name.replace(/[^A-Za-z0-9-_]/g, '_'); + var url = '/' + this.appname + '/index.php'; + // Try a redirect to list, if app defines a "list" value in registry + if (egw.link_get_registry(this.appname, 'list')) { + url = egw.link('/index.php', jQuery.extend({ 'favorite': safe_name }, egw.link_get_registry(this.appname, 'list'))); + } + // if no list try index value from application + else if (egw.app(this.appname).index) { + url = egw.link('/index.php', 'menuaction=' + egw.app(this.appname).index + '&favorite=' + safe_name); + } + egw.open_link(url, undefined, undefined, this.appname); + return false; + }; + /** + * Retrieve the current state of the application for future restoration + * + * The state can be anything, as long as it's an object. The contents are + * application specific. The default implementation finds a nextmatch and + * returns its value. + * The return value of this function cannot be passed directly to setState(), + * since setState is expecting an additional wrapper, eg: + * {name: 'something', state: getState()} + * + * @return {object} Application specific map representing the current state + */ + EgwApp.prototype.getState = function () { + var state = {}; + // Try and find a nextmatch widget, and set its filters + var et2 = etemplate2.getByApplication(this.appname); + for (var i = 0; i < et2.length; i++) { + et2[i].widgetContainer.iterateOver(function (_widget) { + state = _widget.getValue(); + }, this, et2_nextmatch); + } + return state; + }; + /** + * Function to load selected row from nm into a template view + * + * @param {object} _action + * @param {object} _senders + * @param {boolean} _noEdit defines whether to set edit button or not default is false + * @param {function} et2_callback function to run after et2 is loaded + */ + EgwApp.prototype.viewEntry = function (_action, _senders, _noEdit, et2_callback) { + //full id in nm + var id = _senders[0].id; + // flag for edit button + var noEdit = _noEdit || false; + // nm row id + var rowID = ''; + // content to feed to etemplate2 + var content = {}; + var self = this; + if (id) { + var parts = id.split('::'); + rowID = parts[1]; + content = egw.dataGetUIDdata(id); + if (content.data) + content = content.data; + } + // create a new app object with just constructors for our new etemplate2 object + var app = { classes: window.app.classes }; + /* destroy generated etemplate for view mode in DOM*/ + var destroy = function () { + self.viewContainer.remove(); + delete self.viewTemplate; + delete self.viewContainer; + delete self.et2_view; + // we need to reference back into parent context this + for (var v in self) { + this[v] = self[v]; + } + app = null; + }; + // view container + this.viewContainer = jQuery(document.createElement('div')) + .addClass('et2_mobile_view') + .css({ + "z-index": 102, + width: "100%", + height: "100%", + background: "white", + display: 'block', + position: 'absolute', + left: 0, + bottom: 0, + right: 0, + overflow: 'auto' + }) + .attr('id', 'popupMainDiv') + .appendTo('body'); + // close button + var close = jQuery(document.createElement('span')) + .addClass('egw_fw_mobile_popup_close loaded') + .click(function () { + destroy.call(app[self.appname]); + //disable selected actions after close + egw_globalObjectManager.setAllSelected(false); + }) + .appendTo(this.viewContainer); + if (!noEdit) { + // edit button + var edit = jQuery(document.createElement('span')) + .addClass('mobile-view-editBtn') + .click(function () { + egw.open(rowID, self.appname); + }) + .appendTo(this.viewContainer); + } + // view template main container (content) + this.viewTemplate = jQuery(document.createElement('div')) + .attr('id', this.appname + '-view') + .addClass('et2_mobile-view-container popupMainDiv') + .appendTo(this.viewContainer); + var mobileViewTemplate = (_action.data.mobileViewTemplate || 'edit').split('?'); + var templateName = mobileViewTemplate[0]; + var templateTimestamp = mobileViewTemplate[1]; + var templateURL = egw.webserverUrl + '/' + this.appname + '/templates/mobile/' + templateName + '.xet' + '?' + templateTimestamp; + var data = { + 'content': content, + 'readonlys': { '__ALL__': true, 'link_to': false }, + 'currentapp': this.appname, + 'langRequire': this.et2.getArrayMgr('langRequire').data, + 'sel_options': this.et2.getArrayMgr('sel_options').data, + 'modifications': this.et2.getArrayMgr('modifications').data, + 'validation_errors': this.et2.getArrayMgr('validation_errors').data + }; + // etemplate2 object for view + this.et2_view = new etemplate2(this.viewTemplate[0], false); + framework.pushState('view'); + if (templateName) { + this.et2_view.load(this.appname + '.' + templateName, templateURL, data, typeof et2_callback == 'function' ? et2_callback : function () { }, app); + } + // define a global close function for view template + // in order to be able to destroy view on action + this.et2_view.close = destroy; + }; + /** + * Initializes actions and handlers on sidebox (delete) + * + * @param {jQuery} sidebox jQuery of DOM node + */ + EgwApp.prototype._init_sidebox = function (sidebox) { + // Initialize egw tutorial sidebox, but only for non-popups, as calendar edit app.js has this.et2 set to tutorial et2 object + if (!this.egw.is_popup()) { + var egw_fw = egw_getFramework(); + var tutorial = jQuery('#egw_tutorial_' + this.appname + '_sidebox', egw_fw ? egw_fw.sidemenuDiv : document); + // _init_sidebox gets currently called multiple times, which needs to be fixed + if (tutorial.length && !this.tutorial_initialised) { + this.egwTutorial_init(tutorial[0]); + this.tutorial_initialised = true; + } + } + if (sidebox.length) { + var self = this; + if (this.sidebox) + this.sidebox.off(); + this.sidebox = sidebox; + sidebox + .off() + // removed .on("mouse(enter|leave)" (wrapping trash icon), as it stalls delete in IE11 + .on("click.sidebox", "div.ui-icon-trash", this, this.delete_favorite) + // need to install a favorite handler, as we switch original one off with .off() + .on('click.sidebox', 'li[data-id]', this, function (event) { + var li = jQuery(this); + li.siblings().removeClass('ui-state-highlight'); + var state = {}; + var pref = egw.preference('favorite_' + this.dataset.id, self.appname); + if (pref) { + // Extend, to prevent changing the preference by reference + jQuery.extend(true, state, pref); + } + if (this.dataset.id != 'add') { + event.stopImmediatePropagation(); + self.setState.call(self, state); + return false; + } + }) + .addClass("ui-helper-clearfix"); + //Add Sortable handler to sideBox fav. menu + jQuery('ul', '#favorite_sidebox_' + this.appname).sortable({ + items: 'li:not([data-id$="add"])', + placeholder: 'ui-fav-sortable-placeholder', + delay: 250, + helper: function (event, item) { + // We'll need to know which app this is for + item.attr('data-appname', self.appname); + // Create custom helper so it can be dragged to Home + var h_parent = item.parent().parent().clone(); + h_parent.find('li').not('[data-id="' + item.attr('data-id') + '"]').remove(); + h_parent.appendTo('body'); + return h_parent; + }, + // @ts-ignore + refreshPositions: true, + update: function (event, ui) { + // @ts-ignore + var favSortedList = jQuery(this).sortable('toArray', { attribute: 'data-id' }); + self.egw.set_preference(self.appname, 'fav_sort_pref', favSortedList); + self._refresh_fav_nm(); + } + }); + // Bind favorite de-select + var egw_fw = egw_getFramework(); + if (egw_fw && egw_fw.applications[this.appname] && egw_fw.applications[this.appname].browser + && egw_fw.applications[this.appname].browser.baseDiv) { + jQuery(egw_fw.applications[this.appname].browser.baseDiv) + .off('.sidebox') + .on('change.sidebox', function () { + self.highlight_favorite(); + }); + } + return true; + } + return false; + }; + /** + * Add a new favorite + * + * Fetches the current state from the application, then opens a dialog to get the + * name and other settings. If user proceeds, the favorite is saved, and if possible + * the sidebox is directly updated to include the new favorite + * + * @param {object} [state] State settings to be merged into the application state + */ + EgwApp.prototype.add_favorite = function (state) { + if (typeof this.favorite_popup == "undefined" || // Create popup if it's not defined yet + (this.favorite_popup && typeof this.favorite_popup.group != "undefined" + && !this.favorite_popup.group.isAttached())) // recreate the favorite popup if the group selectbox is not attached (eg. after et2 submit) + { + this._create_favorite_popup(); + } + // Get current state + this.favorite_popup.state = jQuery.extend({}, this.getState(), state || {}); + /* + // Add in extras + for(var extra in this.options.filters) + { + // Don't overwrite what nm has, chances are nm has more up-to-date value + if(typeof this.popup.current_filters == 'undefined') + { + this.popup.current_filters[extra] = this.nextmatch.options.settings[extra]; + } + } + + // Add in application's settings + if(this.filters != true) + { + for(var i = 0; i < this.filters.length; i++) + { + this.popup.current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]]; + } + } + */ + // Make sure it's an object - deep copy to prevent references in sub-objects (col_filters) + this.favorite_popup.state = jQuery.extend(true, {}, this.favorite_popup.state); + // Update popup with current set filters (more for debug than user) + var filter_list = []; + var add_to_popup = function (arr) { + filter_list.push("
    "); + jQuery.each(arr, function (index, filter) { + filter_list.push("
  • " + index + "" + + (typeof filter != "object" ? "" + filter + "" : "")); + if (typeof filter == "object" && filter != null) + add_to_popup(filter); + filter_list.push("
  • "); + }); + filter_list.push("
"); + }; + add_to_popup(this.favorite_popup.state); + jQuery("#" + this.appname + "_favorites_popup_state", this.favorite_popup) + .replaceWith(jQuery(filter_list.join("")).attr("id", this.appname + "_favorites_popup_state")); + jQuery("#" + this.appname + "_favorites_popup_state", this.favorite_popup) + .hide() + .siblings(".ui-icon-circle-plus") + .removeClass("ui-icon-circle-minus"); + // Popup + this.favorite_popup.dialog("open"); + console.log(this); + // Stop the normal bubbling if this is called on click + return false; + }; + /** + * Update favorite items in nm fav. menu + * + */ + EgwApp.prototype._refresh_fav_nm = function () { + var self = this; + if (etemplate2 && etemplate2.getByApplication) { + var et2 = etemplate2.getByApplication(self.appname); + for (var i = 0; i < et2.length; i++) { + et2[i].widgetContainer.iterateOver(function (_widget) { + _widget.stored_filters = _widget.load_favorites(self.appname); + _widget.init_filters(_widget); + }, self, et2_favorites); + } + } + else { + throw new Error("_refresh_fav_nm():Either et2 is not ready/ not there yet. Make sure that etemplate2 is ready before call this method."); + } + }; + /** + * Create the "Add new" popup dialog + */ + EgwApp.prototype._create_favorite_popup = function () { + var self = this; + var favorite_prefix = 'favorite_'; + // Clear old, if existing + if (this.favorite_popup && this.favorite_popup.group) { + this.favorite_popup.group.free(); + delete this.favorite_popup; + } + // Create popup + this.favorite_popup = jQuery('
\ +
\ + ' + + '\ +
\ + ' + this.egw.lang("Details") + '\ +
    \ + \ +
').appendTo(this.et2 ? this.et2.getDOMNode() : jQuery('body')); + // @ts-ignore + jQuery(".ui-icon-circle-plus", this.favorite_popup).prev().andSelf().click(function () { + var details = jQuery("#" + self.appname + "_favorites_popup_state", self.favorite_popup) + .slideToggle() + .siblings(".ui-icon-circle-plus") + .toggleClass("ui-icon-circle-minus"); + }); + // Add some controls if user is an admin + var apps = egw().user('apps'); + var is_admin = (typeof apps['admin'] != "undefined"); + if (is_admin) { + this.favorite_popup.group = et2_createWidget("select-account", { + id: "favorite[group]", + account_type: "groups", + empty_label: "Groups", + no_lang: true, + parent_node: this.appname + '_favorites_popup_admin' + }, (this.et2 || null)); + this.favorite_popup.group.loadingFinished(); + } + var buttons = {}; + buttons['save'] = { + text: this.egw.lang('save'), + default: true, + style: 'background-image: url(' + this.egw.image('save') + ')', + click: function () { + // Add a new favorite + var name = jQuery("#name", this); + if (name.val()) { + // Add to the list + name.val(name.val().replace(/(<([^>]+)>)/ig, "")); + var safe_name = name.val().replace(/[^A-Za-z0-9-_]/g, "_"); + var favorite = { + name: name.val(), + group: (typeof self.favorite_popup.group != "undefined" && + self.favorite_popup.group.get_value() ? self.favorite_popup.group.get_value() : false), + state: self.favorite_popup.state + }; + var favorite_pref = favorite_prefix + safe_name; + // Save to preferences + if (typeof self.favorite_popup.group != "undefined" && self.favorite_popup.group.getValue() != '') { + // Admin stuff - save preference server side + self.egw.jsonq('EGroupware\\Api\\Framework::ajax_set_favorite', [ + self.appname, + name.val(), + "add", + self.favorite_popup.group.get_value(), + self.favorite_popup.state + ]); + self.favorite_popup.group.set_value(''); + } + else { + // Normal user - just save to preferences client side + self.egw.set_preference(self.appname, favorite_pref, favorite); + } + // Add to list immediately + if (self.sidebox) { + // Remove any existing with that name + jQuery('[data-id="' + safe_name + '"]', self.sidebox).remove(); + // Create new item + var html = "\n"; + jQuery(html).insertBefore(jQuery('li', self.sidebox).last()); + self._init_sidebox(self.sidebox); + } + // Try to update nextmatch favorites too + self._refresh_fav_nm(); + } + // Reset form + delete self.favorite_popup.state; + name.val(""); + jQuery("#filters", self.favorite_popup).empty(); + jQuery(this).dialog("close"); + } + }; + buttons['cancel'] = { + text: this.egw.lang("cancel"), + style: 'background-image: url(' + this.egw.image('cancel') + ')', + click: function () { + if (typeof self.favorite_popup.group !== 'undefined' && self.favorite_popup.group.set_value) { + self.favorite_popup.group.set_value(null); + } + jQuery(this).dialog("close"); + } + }; + this.favorite_popup.dialog({ + autoOpen: false, + modal: true, + buttons: buttons, + close: function () { + } + }); + // Bind handler for enter keypress + this.favorite_popup.off('keydown').on('keydown', jQuery.proxy(function (e) { + var tagName = e.target.tagName.toLowerCase(); + tagName = (tagName === 'input' && e.target.type === 'button') ? 'button' : tagName; + if (e.keyCode == jQuery.ui.keyCode.ENTER && tagName !== 'textarea' && tagName !== 'select' && tagName !== 'button') { + e.preventDefault(); + jQuery('button[default]', this.favorite_popup.parent()).trigger('click'); + return false; + } + }, this)); + return false; + }; + /** + * Delete a favorite from the list and update preferences + * Registered as a handler on the delete icons + * + * @param {jQuery.event} event event object + */ + EgwApp.prototype.delete_favorite = function (event) { + // Don't do the menu + event.stopImmediatePropagation(); + var app = event.data; + var id = jQuery(this).parentsUntil('li').parent().attr("data-id"); + var group = jQuery(this).parentsUntil('li').parent().attr("data-group") || ''; + var line = jQuery('li[data-id="' + id + '"]', app.sidebox); + var name = line.first().text(); + var trash = this; + line.addClass('loading'); + // Make sure first + var do_delete = function (button_id) { + if (button_id != et2_dialog.YES_BUTTON) { + line.removeClass('loading'); + return; + } + // Hide the trash + jQuery(trash).hide(); + // Delete preference server side + var request = egw.json("EGroupware\\Api\\Framework::ajax_set_favorite", [app.appname, id, "delete", group, ''], function (result) { + // Got the full response from callback, which we don't want + if (result.type) + return; + if (result && typeof result == 'boolean') { + // Remove line from list + line.slideUp("slow", function () { }); + app._refresh_fav_nm(); + } + else { + // Something went wrong server side + line.removeClass('loading').addClass('ui-state-error'); + } + }, jQuery(trash).parentsUntil("li").parent(), true, jQuery(trash).parentsUntil("li").parent()); + request.sendRequest(true); + }; + et2_dialog.show_dialog(do_delete, (egw.lang("Delete") + " " + name + "?"), egw.lang("Delete"), et2_dialog.YES_NO, et2_dialog.QUESTION_MESSAGE); + return false; + }; + /** + * Mark the favorite closest matching the current state + * + * Closest matching takes into account not set values, so we pick the favorite + * with the most matching values without a value that differs. + */ + EgwApp.prototype.highlight_favorite = function () { + if (!this.sidebox) + return; + var state = this.getState(); + var best_match = false; + var best_count = 0; + var self = this; + jQuery('li[data-id]', this.sidebox).removeClass('ui-state-highlight'); + jQuery('li[data-id]', this.sidebox).each(function (i, href) { + var favorite = {}; + if (this.dataset.id && egw.preference('favorite_' + this.dataset.id, self.appname)) { + favorite = egw.preference('favorite_' + this.dataset.id, self.appname); + } + if (!favorite || jQuery.isEmptyObject(favorite)) + return; + // Handle old style by making it like new style + if (favorite.filter && !favorite.state) { + favorite.state = favorite.filter; + } + var match_count = 0; + var extra_keys = Object.keys(favorite.state); + for (var state_key in state) { + extra_keys.splice(extra_keys.indexOf(state_key), 1); + if (typeof favorite.state != 'undefined' && typeof state[state_key] != 'undefined' && typeof favorite.state[state_key] != 'undefined' && (state[state_key] == favorite.state[state_key] || !state[state_key] && !favorite.state[state_key])) { + match_count++; + } + else if (state_key == 'selectcols') { + // Skip, might be set, might not + } + else if (typeof state[state_key] != 'undefined' && state[state_key] && typeof state[state_key] === 'object' + && typeof favorite.state != 'undefined' && typeof favorite.state[state_key] != 'undefined' && favorite.state[state_key] && typeof favorite.state[state_key] === 'object') { + if ((typeof state[state_key].length !== 'undefined' || typeof state[state_key].length !== 'undefined') + && (state[state_key].length || Object.keys(state[state_key]).length) != (favorite.state[state_key].length || Object.keys(favorite.state[state_key]).length)) { + // State or favorite has a length, but the other does not + if ((state[state_key].length === 0 || Object.keys(state[state_key]).length === 0) && + (favorite.state[state_key].length == 0 || Object.keys(favorite.state[state_key]).length === 0)) { + // Just missing, or one is an array and the other is an object + continue; + } + // One has a value and the other doesn't, no match + return; + } + else if (state[state_key].length !== 'undefined' && typeof favorite.state[state_key].length !== 'undefined' && + state[state_key].length === 0 && favorite.state[state_key].length === 0) { + // Both set, but both empty + match_count++; + continue; + } + // Consider sub-objects (column filters) individually + for (var sub_key in state[state_key]) { + if (state[state_key][sub_key] == favorite.state[state_key][sub_key] || !state[state_key][sub_key] && !favorite.state[state_key][sub_key]) { + match_count++; + } + else if (state[state_key][sub_key] && favorite.state[state_key][sub_key] && + typeof state[state_key][sub_key] === 'object' && typeof favorite.state[state_key][sub_key] === 'object') { + // Too deep to keep going, just string compare for perfect match + if (JSON.stringify(state[state_key][sub_key]) === JSON.stringify(favorite.state[state_key][sub_key])) { + match_count++; + } + } + else if (typeof state[state_key][sub_key] !== 'undefined' && state[state_key][sub_key] != favorite.state[state_key][sub_key]) { + // Different values, do not match + return; + } + } + } + else if (typeof state[state_key] !== 'undefined' + && typeof favorite.state != 'undefined' && typeof favorite.state[state_key] !== 'undefined' + && state[state_key] != favorite.state[state_key]) { + // Different values, do not match + return; + } + } + // Check for anything set that the current one does not have + for (var i = 0; i < extra_keys.length; i++) { + if (favorite.state[extra_keys[i]]) + return; + } + if (match_count > best_count) { + best_match = this.dataset.id; + best_count = match_count; + } + }); + if (best_match) { + jQuery('li[data-id="' + best_match + '"]', this.sidebox).addClass('ui-state-highlight'); + } + }; + /** + * Fix scrolling iframe browsed by iPhone/iPod/iPad touch devices + */ + EgwApp.prototype._fix_iFrameScrolling = function () { + if (/iPhone|iPod|iPad/.test(navigator.userAgent)) { + jQuery("iframe").on({ + load: function () { + var body = this.contentWindow.document.body; + var div = jQuery(document.createElement("div")) + .css({ + 'height': jQuery(this.parentNode).height(), + 'width': jQuery(this.parentNode).width(), + 'overflow': 'scroll' + }); + while (body.firstChild) { + div.append(body.firstChild); + } + jQuery(body).append(div); + } + }); + } + }; + /** + * Set document title, uses getWindowTitle to get the correct title, + * otherwise set it with uniqueID as default title + */ + EgwApp.prototype._set_Window_title = function () { + var title = this.getWindowTitle(); + if (title) { + document.title = this.et2._inst.uniqueId + ": " + title; + } + }; + /** + * Window title getter function in order to set the window title + * this can be overridden on each application app.js file to customize the title value + * + * @returns {string} window title + */ + EgwApp.prototype.getWindowTitle = function () { + var titleWidget = this.et2.getWidgetById('title'); + if (titleWidget) { + return titleWidget.options.value; + } + else { + return this.et2._inst.uniqueId; + } + }; + /** + * Handler for drag and drop when dragging nextmatch rows from mail app + * and dropped on a row in the current application. We copy the mail into + * the filemanager to link it since we can't link directly. + * + * This doesn't happen automatically. Each application must indicate that + * it will accept dropped mail by it's nextmatch actions: + * + * $actions['info_drop_mail'] = array( + * 'type' => 'drop', + * 'acceptedTypes' => 'mail', + * 'onExecute' => 'javaScript:app.infolog.handle_dropped_mail', + * 'hideOnDisabled' => true + * ); + * + * This action, when defined, will not affect the automatic linking between + * normal applications. + * + * @param {egwAction} _action + * @param {egwActionObject[]} _selected Dragged mail rows + * @param {egwActionObject} _target Current application's nextmatch row the mail was dropped on + */ + EgwApp.prototype.handle_dropped_mail = function (_action, _selected, _target) { + /** + * Mail doesn't support link system, so we copy it to VFS + */ + var ids = _target.id.split("::"); + if (ids.length != 2 || ids[0] == 'mail') + return; + var vfs_path = "/apps/" + ids[0] + "/" + ids[1]; + var mail_ids = []; + for (var i = 0; i < _selected.length; i++) { + mail_ids.push(_selected[i].id); + } + if (mail_ids.length) { + egw.message(egw.lang("Please wait...")); + this.egw.json('filemanager.filemanager_ui.ajax_action', ['mail', mail_ids, vfs_path], function (data) { + // Trigger an update (minimal, no sorting changes) to display the new link + egw.refresh(data.msg || '', ids[0], ids[1], 'update'); + }).sendRequest(true); + } + }; + /** + * Get json data for videos from the given url + * + * @return {Promise, object} return Promise, json object as resolved result and error message in case of failure + */ + EgwApp.prototype.egwTutorialGetData = function () { + var self = this; + return new Promise(function (_resolve, _reject) { + var resolve = _resolve; + var reject = _reject; + // delay the execution and let the rendering catches up. Seems only FF problem + window.setTimeout(function () { + self.egw.json('EGroupware\\Api\\Framework\\Tutorial::ajax_data', [self.egw.app_name()], function (_data) { + resolve(_data); + }).sendRequest(); + }, 0); + }); + }; + /** + * Create and Render etemplate2 for egroupware tutorial + * sidebox option. The .xet file is stored in api/templates/default/egw_tutorials + * + * @description tutorials json object should have the following structure: + * object: + * { + * [app name]:{ + * [language tag]:[ + * {src:"",thumbnail:"",title:"",desc:""} + * ] + * } + * } + * + * *Note: "desc" and "title" are optional attributes, which "desc" would appears as tooltip for the video. + * + * example: + * { + * "mail":{ + * "en":[ + * {src:"https://www.youtube.com/embed/mCDJndpjO40", thumbnail:"http://img.youtube.com/vi/mCDJndpjO40/0.jpg", "title":"PGP Encryption", "desc":""}, + * {src:"https://www.youtube.com/embed/mCDJndpjO", thumbnail:"http://img.youtube.com/vi/mCDJndpjO/0.jpg", "title":"Subscription", "desc":""}, + * ], + * "de":[ + * {src:"https://www.youtube.com/embed/m40", thumbnail:"http://img.youtube.com/vi/m40/0.jpg", "title":"PGP Verschlüsselung", "desc":""}, + * {src:"https://www.youtube.com/embed/mpjO", thumbnail:"http://img.youtube.com/vi/mpjO/0.jpg", "title":"Ordner Abonnieren", "desc":""}, + * ] + * } + * } + * + * @param {DOMNode} div + */ + EgwApp.prototype.egwTutorial_init = function (div) { + // et2 object + var etemplate = new etemplate2(div, false); + var template = egw.webserverUrl + '/api/templates/default/egw_tutorial.xet?1'; + this.egwTutorialGetData().then(function (_data) { + var lang = egw.preference('lang'); + var content = { content: { list: [] } }; + if (_data && _data[egw.app_name()]) { + if (!_data[egw.app_name()][lang]) + lang = 'en'; + if (typeof _data[egw.app_name()][lang] != 'undefined' + && _data[egw.app_name()][lang].length > 0) { + for (var i = 0; i < _data[egw.app_name()][lang].length; i++) { + var tuid = egw.app_name() + '-' + lang + '-' + i; + _data[egw.app_name()][lang][i]['onclick'] = 'app.' + egw.app_name() + '.egwTutorialPopup("' + tuid + '")'; + } + content.content.list = _data[egw.app_name()][lang]; + if (template.indexOf('.xet') > 0) { + etemplate.load('', template, content, function () { }); + } + else { + etemplate.load(template, '', content); + } + } + } + }, function (_err) { + console.log(_err); + }); + }; + /** + * Open popup to show given tutorial id + * @param {string} _tuid tutorial object id + * - tuid: appname-lang-index + */ + EgwApp.prototype.egwTutorialPopup = function (_tuid) { + var url = egw.link('/index.php', 'menuaction=api.EGroupware\\Api\\Framework\\Tutorial.popup&tuid=' + _tuid); + egw.open_link(url, '_blank', '960x580'); + }; + /** + * Function to set video iframe base on selected tutorial from tutorials box + * + * @param {string} _url + */ + EgwApp.prototype.tutorial_videoOnClick = function (_url) { + var frame = etemplate2.getByApplication('api')[0].widgetContainer.getWidgetById('src'); + if (frame) { + frame.set_value(_url); + } + }; + /** + * Function calls on discard checkbox and will set + * the egw_tutorial_noautoload preference + * + * @param {type} egw + * @param {type} widget + */ + EgwApp.prototype.tutorial_autoloadDiscard = function (egw, widget) { + if (widget) { + this.egw.set_preference('common', 'egw_tutorial_noautoload', widget.get_value()); + } + }; + /** + * Check if Mailvelope is available, open (or create) "egroupware" keyring and call callback with it + * + * @param {function} _callback called if and only if mailvelope is available (context is this!) + */ + EgwApp.prototype.mailvelopeAvailable = function (_callback) { + var self = this; + var callback = jQuery.proxy(_callback, this); + if (typeof mailvelope !== 'undefined') { + this.mailvelopeOpenKeyring().then(callback); + } + else { + jQuery(window).on('mailvelope', function () { + self.mailvelopeOpenKeyring().then(callback); + }); + } + }; + /** + * mailvelope object contains SyncHandlers + * + * @property {function} descriptionuploadSync function called by Mailvelope to upload encrypted private key backup + * @property {function} downloadSync function called by Mailvelope to download encrypted private key backup + * @property {function} backup function called by Mailvelope to upload a public keyring backup + * @property {function} restore function called by Mailvelope to restore a public keyring backup + */ + EgwApp.prototype.mailvelopeSyncHandler = function () { + return { + /** + * function called by Mailvelope to upload a public keyring + * @param {UploadSyncHandler} _uploadObj + * @property {string} etag entity tag for the uploaded encrypted keyring, or null if initial upload + * @property {AsciiArmored} keyringMsg encrypted keyring as PGP armored message + * @returns {Promise.} + */ + uploadSync: function (_uploadObj) { + return new Promise(function (_resolve, _reject) { }); + }, + /** + * function called by Mailvelope to download a public keyring + * + * @param {object} _downloadObj + * @property {string} etag entity tag for the current local keyring, or null if no local eTag + * @returns {Promise.} + */ + downloadSync: function (_downloadObj) { + return new Promise(function (_resolve, _reject) { }); + }, + /** + * function called by Mailvelope to upload an encrypted private key backup + * + * @param {BackupSyncPacket} _backup + * @property {AsciiArmored} backup an encrypted private key as PGP armored message + * @returns {Promise.} + */ + backup: function (_backup) { + return new Promise(function (_resolve, _reject) { + // Store backup sync packet into .PGP-Key-Backup file in user directory + jQuery.ajax({ + method: 'PUT', + url: egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PGP-Key-Backup', + contentType: 'application/json', + data: JSON.stringify(_backup), + success: function () { + _resolve(_backup); + }, + error: function (_err) { + _reject(_err); + } + }); + }); + }, + /** + * function called by Mailvelope to restore an encrypted private key backup + * + * @returns {Promise.} + * @todo + */ + restore: function () { + return new Promise(function (_resolve, _reject) { + var resolve = _resolve; + var reject = _reject; + jQuery.ajax({ + url: egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PGP-Key-Backup', + method: 'GET', + success: function (_backup) { + resolve(JSON.parse(_backup)); + egw.message('Your key has been restored successfully.'); + }, + error: function (_err) { + //Try with old back file name + if (_err.status == 404) { + jQuery.ajax({ + method: 'GET', + url: egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PK_PGP', + success: function (_backup) { + resolve(JSON.parse(_backup)); + egw.message('Your key has been restored successfully.'); + }, + error: function (_err) { + _reject(_err); + } + }); + } + else { + _reject(_err); + } + } + }); + }); + } + }; + }; + /** + * Function for backup file operations + * + * @param {type} _url Url of the backup file + * @param {type} _cmd command to operate + * - PUT: to store backup file + * - GET: to read backup file + * - DELETE: to delete backup file + * + * @param {type} _successCallback function called when the operation is successful + * @param {type} _errorCallback function called when the operation fails + * @param {type} _data data which needs to be stored in file via PUT command + */ + EgwApp.prototype._mailvelopeBackupFileOperator = function (_url, _cmd, _successCallback, _errorCallback, _data) { + var ajaxObj = { + url: _url || egw.webserverUrl + '/webdav.php/home/' + egw.user('account_lid') + '/.PGP-Key-Backup', + method: _cmd, + success: _successCallback, + error: _errorCallback + }; + switch (_cmd) { + case 'PUT': + jQuery.extend({}, ajaxObj, { + data: JSON.stringify(_data), + contentType: 'application/json' + }); + break; + case 'GET': + jQuery.extend({}, ajaxObj, { + dataType: 'json' + }); + break; + case 'DELETE': + break; + } + jQuery.ajax(ajaxObj); + }; + /** + * Create backup dialog + * @param {string} _selector DOM selector to attach backupDialog + * @param {boolean} _initSetup determine wheter it's an initialization backup or restore backup + * + * @returns {Promise.} + */ + EgwApp.prototype.mailvelopeCreateBackupDialog = function (_selector, _initSetup) { + var self = this; + var selector = _selector || 'body'; + var initSetup = _initSetup; + jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove(); + return new Promise(function (_resolve, _reject) { + var resolve = _resolve; + var reject = _reject; + mailvelope.getKeyring('egroupware').then(function (_keyring) { + _keyring.addSyncHandler(self.mailvelopeSyncHandlerObj); + var options = { + initialSetup: initSetup + }; + _keyring.createKeyBackupContainer(selector, options).then(function (_popupId) { + var $backup_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'); + $backup_selector.css({ position: 'absolute', "z-index": 1 }); + _popupId.isReady().then(function (result) { + egw.message('Your key has been backedup into .PGP-Key-Backup successfully.'); + jQuery(selector).empty(); + }); + resolve(_popupId); + }, function (_err) { + reject(_err); + }); + }, function (_err) { + reject(_err); + }); + }); + }; + /** + * Delete backup key from filesystem + */ + EgwApp.prototype.mailvelopeDeleteBackup = function () { + var self = this; + et2_dialog.show_dialog(function (_button_id) { + if (_button_id == et2_dialog.YES_BUTTON) { + self._mailvelopeBackupFileOperator(undefined, 'DELETE', function () { + self.egw.message(self.egw.lang('The backup key has been deleted.')); + }, function (_err) { + self.egw.message(self.egw.lang('Was not able to delete the backup key because %1', _err)); + }); + } + }, self.egw.lang('Are you sure, you would like to delete the backup key?'), self.egw.lang('Delete backup key'), {}, et2_dialog.BUTTONS_YES_CANCEL, et2_dialog.QUESTION_MESSAGE, undefined, self.egw); + }; + /** + * Create mailvelope restore dialog + * @param {string} _selector DOM selector to attach restorDialog + * @param {boolean} _restorePassword if true, will restore key password too + * + * @returns {Promise} + */ + EgwApp.prototype.mailvelopeCreateRestoreDialog = function (_selector, _restorePassword) { + var self = this; + var restorePassword = _restorePassword; + var selector = _selector || 'body'; + //Clear the + jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove(); + return new Promise(function (_resolve, _reject) { + var resolve = _resolve; + var reject = _reject; + mailvelope.getKeyring('egroupware').then(function (_keyring) { + _keyring.addSyncHandler(self.mailvelopeSyncHandlerObj); + var options = { + restorePassword: restorePassword + }; + _keyring.restoreBackupContainer(selector, options).then(function (_restoreId) { + var $restore_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'); + $restore_selector.css({ position: 'absolute', "z-index": 1 }); + resolve(_restoreId); + }, function (_err) { + reject(_err); + }); + }, function (_err) { + reject(_err); + }); + }); + }; + /** + * Create a dialog to show all backup/restore options + * + * @returns {undefined} + */ + EgwApp.prototype.mailvelopeCreateBackupRestoreDialog = function () { + var self = this; + var appname = egw.app_name(); + var menu = [ + // Header row should be empty item 0 + {}, + // Restore Keyring item 1 + { label: "Restore key", image: "lock", onclick: "app." + appname + ".mailvelopeCreateRestoreDialog('#_mvelo')" }, + // Restore pass phrase item 2 + { label: "Restore password", image: "password", onclick: "app." + appname + ".mailvelopeCreateRestoreDialog('#_mvelo', true)" }, + // Delete backup Key item 3 + { label: "Delete backup", image: "delete", onclick: "app." + appname + ".mailvelopeDeleteBackup" }, + // Backup Key item 4 + { label: "Backup Key", image: "save", onclick: "app." + appname + ".mailvelopeCreateBackupDialog('#_mvelo', false)" } + ]; + var dialog = function (_content, _callback) { + return et2_createWidget("dialog", { + callback: function (_button_id, _value) { + if (typeof _callback == "function") { + _callback.call(this, _button_id, _value.value); + } + }, + title: egw.lang('Backup/Restore'), + buttons: [{ "button_id": 'close', "text": egw.lang('Close'), id: 'dialog[close]', image: 'cancelled', "default": true }], + value: { + content: { + menu: _content + } + }, + template: egw.webserverUrl + '/api/templates/default/pgp_backup_restore.xet', + class: "pgp_backup_restore", + modal: true + }); + }; + if (typeof mailvelope != 'undefined') { + mailvelope.getKeyring('egroupware').then(function (_keyring) { + self._mailvelopeBackupFileOperator(undefined, 'GET', function (_data) { + dialog(menu); + }, function () { + // Remove delete item + menu.splice(3, 1); + menu[3]['onclick'] = "app." + appname + ".mailvelopeCreateBackupDialog('#_mvelo', true)"; + dialog(menu); + }); + }, function () { + mailvelope.createKeyring('egroupware').then(function () { dialog(menu); }); + }); + } + else { + this.mailvelopeInstallationOffer(); + } + }; + /** + * Create a dialog and offers installation option for installing mailvelope plugin + * plus it offers a video tutorials to get the user morte familiar with mailvelope + */ + EgwApp.prototype.mailvelopeInstallationOffer = function () { + var buttons = [ + { "text": egw.lang('Install'), id: 'install', image: 'check', "default": true }, + { "text": egw.lang('Close'), id: 'close', image: 'cancelled' } + ]; + var dialog = function (_content, _callback) { + return et2_createWidget("dialog", { + callback: function (_button_id, _value) { + if (typeof _callback == "function") { + _callback.call(this, _button_id, _value.value); + } + }, + title: egw.lang('PGP Encryption Installation'), + buttons: buttons, + dialog_type: 'info', + value: { + content: _content + }, + template: egw.webserverUrl + '/api/templates/default/pgp_installation.xet', + class: "pgp_installation", + modal: true + //resizable:false, + }); + }; + var content = [ + // Header row should be empty item 0 + {}, + { domain: this.egw.lang('Add your domain as "%1" in options to list of email providers and enable API.', '*.' + this._mailvelopeDomain()), video: "test", control: "true" } + ]; + dialog(content, function (_button) { + if (_button == 'install') { + if (typeof chrome != 'undefined') { + // ATM we are not able to trigger mailvelope installation directly + // since the installation should be triggered from the extension + // owner validate website (mailvelope.com), therefore, we just redirect + // user to chrome webstore to install mailvelope from there. + window.open('https://chrome.google.com/webstore/detail/mailvelope/kajibbejlbohfaggdiogboambcijhkke'); + } + else if (typeof InstallTrigger != 'undefined' && InstallTrigger.enabled()) { + InstallTrigger.install({ mailvelope: "https://download.mailvelope.com/releases/latest/mailvelope.firefox.xpi" }, function (_url, _status) { + if (_status == 0) { + et2_dialog.alert(egw.lang('Mailvelope addon installation succeded. Now you may configure the options.')); + return; + } + else { + et2_dialog.alert(egw.lang('Mailvelope addon installation failed! Please try again.')); + } + }); + } + } + }); + }; + /** + * Open (or create) "egroupware" keyring and call callback with it + * + * @returns {Promise.} Keyring or Error with message + */ + EgwApp.prototype.mailvelopeOpenKeyring = function () { + var self = this; + return new Promise(function (_resolve, _reject) { + if (self.mailvelope_keyring) + _resolve(self.mailvelope_keyring); + var resolve = _resolve; + var reject = _reject; + mailvelope.getKeyring('egroupware').then(function (_keyring) { + self.mailvelope_keyring = _keyring; + resolve(_keyring); + }, function (_err) { + mailvelope.createKeyring('egroupware').then(function (_keyring) { + self.mailvelope_keyring = _keyring; + var mvelo_settings_selector = self.mailvelope_iframe_selector + .split(',').map(function (_val) { return 'body>' + _val; }).join(','); + mailvelope.createSettingsContainer('body', _keyring, { + email: self.egw.user('account_email'), + fullName: self.egw.user('account_fullname') + }).then(function () { + // make only Mailvelope settings dialog visible + jQuery(mvelo_settings_selector).css({ position: 'absolute', top: 0 }); + // add a close button, so we know when to offer storing public key to AB + jQuery('') + .css({ position: 'absolute', top: 8, right: 8, "z-index": 2 }) + .click(function () { + // try fetching public key, to check user created onw + self.mailvelope_keyring.exportOwnPublicKey(self.egw.user('account_email')).then(function (_pubKey) { + // CreateBackupDialog + self.mailvelopeCreateBackupDialog().then(function (_popupId) { + jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').css({ position: 'absolute', "z-index": 1 }); + }, function (_err) { + egw.message(_err); + }); + // if yes, hide settings dialog + jQuery(mvelo_settings_selector).each(function (index, item) { + if (!item.src.match(/keyBackupDialog.html/, 'ig')) + item.remove(); + }); + jQuery('button#mailvelope_close_settings').remove(); + // offer user to store his public key to AB for other users to find + var buttons = [ + { button_id: 2, text: 'Yes', id: 'dialog[yes]', image: 'check', default: true }, + { button_id: 3, text: 'No', id: 'dialog[no]', image: 'cancelled' } + ]; + if (egw.user('apps').admin) { + buttons.unshift({ + button_id: 5, text: 'Yes and allow non-admin users to do that too (recommended)', + id: 'dialog[yes_allow]', image: 'check', default: true + }); + delete buttons[1].default; + } + et2_dialog.show_dialog(function (_button_id) { + if (_button_id != et2_dialog.NO_BUTTON) { + var keys = {}; + keys[self.egw.user('account_id')] = _pubKey; + self.egw.json('addressbook.addressbook_bo.ajax_set_pgp_keys', [keys, _button_id != et2_dialog.YES_BUTTON ? true : undefined]).sendRequest() + .then(function (_data) { + self.egw.message(_data.response['0'].data); + }); + } + }, self.egw.lang('It is recommended to store your public key in addressbook, so other users can write you encrypted mails.'), self.egw.lang('Store your public key in Addressbook?'), {}, buttons, et2_dialog.QUESTION_MESSAGE, undefined, self.egw); + }, function (_err) { + self.egw.message(_err.message + "\n\n" + + self.egw.lang("You will NOT be able to send or receive encrypted mails before completing that step!"), 'error'); + }); + }) + .appendTo('body'); + }); + resolve(_keyring); + }, function (_err) { + reject(_err); + }); + }); + }); + }; + /** + * Mailvelope uses Domain without first part: eg. "stylite.de" for "egw.stylite.de" + * + * @returns {string} + */ + EgwApp.prototype._mailvelopeDomain = function () { + var parts = document.location.hostname.split('.'); + if (parts.length > 1) + parts.shift(); + return parts.join('.'); + }; + /** + * Check if we have a key for all recipients + * + * @param {Array} _recipients + * @returns {Promise.} Array of recipients or Error with recipients without key + */ + EgwApp.prototype.mailvelopeGetCheckRecipients = function (_recipients) { + // replace rfc822 addresses with raw email, as Mailvelop does not like them and lowercase all email + var rfc822_preg = /<([^'" <>]+)>$/; + var recipients = _recipients.map(function (_recipient) { + var matches = _recipient.match(rfc822_preg); + return matches ? matches[1].toLowerCase() : _recipient.toLowerCase(); + }); + // check if we have keys for all recipients + var self = this; + return new Promise(function (_resolve, _reject) { + var resolve = _resolve; + var reject = _reject; + self.mailvelopeOpenKeyring().then(function (_keyring) { + var keyring = _keyring; + _keyring.validKeyForAddress(recipients).then(function (_status) { + var no_key = []; + for (var email in _status) { + if (!_status[email]) + no_key.push(email); + } + if (no_key.length) { + // server addressbook on server for missing public keys + self.egw.json('addressbook.addressbook_bo.ajax_get_pgp_keys', [no_key]).sendRequest().then(function (_data) { + var data = _data.response['0'].data; + var promises = []; + for (var email in data) { + promises.push(keyring.importPublicKey(data[email]).then(function (_result) { + if (_result == 'IMPORTED' || _result == 'UPDATED') { + no_key.splice(no_key.indexOf(email), 1); + } + })); + } + Promise.all(promises).then(function () { + if (no_key.length) { + reject(new Error(self.egw.lang('No key for recipient:') + ' ' + no_key.join(', '))); + } + else { + resolve(recipients); + } + }); + }); + } + else { + resolve(recipients); + } + }); + }, function (_err) { + reject(_err); + }); + }); + }; + /** + * Check if the share action is enabled for this entry + * + * @param {egwAction} _action + * @param {egwActionObject[]} _entries + * @param {egwActionObject} _target + * @returns {boolean} if action is enabled + */ + EgwApp.prototype.is_share_enabled = function (_action, _entries, _target) { + return true; + }; + /** + * create a share-link for the given entry + * + * @param {egwAction} _action egw actions + * @param {egwActionObject[]} _senders selected nm row + * @param {egwActionObject} _target Drag source. Not used here. + * @param {Boolean} _writable Allow edit access from the share. + * @param {Boolean} _files Allow access to files from the share. + * @param {Function} _callback Callback with results + * @returns {Boolean} returns false if not successful + */ + EgwApp.prototype.share_link = function (_action, _senders, _target, _writable, _files, _callback) { + var path = _senders[0].id; + if (!path) { + return this.egw.message(this.egw.lang('Missing share path. Unable to create share.'), 'error'); + } + switch (_action.id) { + case 'shareFilemanager': + // Sharing a link to just files in filemanager + var id = path.split('::'); + path = '/apps/' + id[0] + '/' + id[1]; + } + if (typeof _writable === 'undefined' && _action.parent && _action.parent.getActionById('shareWritable')) { + _writable = _action.parent.getActionById('shareWritable').checked || false; + } + if (typeof _files === 'undefined' && _action.parent && _action.parent.getActionById('shareFiles')) { + _files = _action.parent.getActionById('shareFiles').checked || false; + } + return egw.json('EGroupware\\Api\\Sharing::ajax_create', [_action.id, path, _writable, _files], _callback ? _callback : this._share_link_callback, this, true, this).sendRequest(); + }; + EgwApp.prototype.share_merge = function (_action, _senders, _target) { + var parent = _action.parent.parent; + var _writable = false; + var _files = false; + if (parent && parent.getActionById('shareWritable')) { + _writable = parent.getActionById('shareWritable').checked || false; + } + if (parent && parent.getActionById('shareFiles')) { + _files = parent.getActionById('shareFiles').checked || false; + } + // Share only works on one at a time + var promises = []; + for (var i = 0; i < _senders.length; i++) { + promises.push(new Promise(function (resolve, reject) { + this.share_link(_action, [_senders[i]], _target, _writable, _files, resolve); + }.bind(this))); + } + // But merge into email can handle several + Promise.all(promises.map(function (p) { p.catch(function (e) { console.log(e); }); })) + .then(function (values) { + // Process document after all shares created + return nm_action(_action, _senders, _target); + }); + }; + /** + * Share-link callback + * @param {object} _data + */ + EgwApp.prototype._share_link_callback = function (_data) { + if (_data.msg || _data.share_link) + window.egw_refresh(_data.msg, this.appname); + var copy_link_to_clipboard = function (evt) { + var $target = jQuery(evt.target); + $target.select(); + try { + var successful = document.execCommand('copy'); + if (successful) { + egw.message('Share link copied into clipboard'); + return true; + } + } + catch (e) { } + egw.message('Failed to copy the link!'); + }; + jQuery("body").on("click", "[name=share_link]", copy_link_to_clipboard); + et2_createWidget("dialog", { + callback: function (button_id, value) { + jQuery("body").off("click", "[name=share_link]", copy_link_to_clipboard); + return true; + }, + title: _data.title ? _data.title : egw.lang("%1 Share Link", _data.writable ? egw.lang("Writable") : egw.lang("Readonly")), + template: _data.template, + width: 450, + value: { content: { "share_link": _data.share_link } } + }); + }; + return EgwApp; +}()); +exports.EgwApp = EgwApp; diff --git a/api/js/jsapi/egw_app.ts b/api/js/jsapi/egw_app.ts new file mode 100644 index 0000000000..428b2691be --- /dev/null +++ b/api/js/jsapi/egw_app.ts @@ -0,0 +1,2029 @@ +/** + * EGroupware clientside Application javascript base object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Ralf Becker + * @author Hadi Nategh + * @author Nathan Gray + */ + +import 'jquery'; +import 'jqueryui'; +import '../jsapi/egw_global'; +import '../etemplate/et2_types'; + +/** + * Common base class for application javascript + * Each app should extend as needed. + * + * All application javascript should be inside. Intitialization goes in init(), + * clean-up code goes in destroy(). Initialization is done once all js is loaded. + * + * var app.appname = AppJS.extend({ + * // Actually set this one, the rest is example + * appname: appname, + * + * internal_var: 1000, + * + * init: function() + * { + * // Call the super + * this._super.apply(this, arguments); + * + * // Init the stuff + * if ( egw.preference('dateformat', 'common') ) + * { + * // etc + * } + * }, + * _private: function() + * { + * // Underscore private by convention + * } + * }); + * + * @class AppJS + * @augments Class + */ +export abstract class EgwApp +{ + /** + * Internal application name - override this + */ + readonly appname: string; + + /** + * Internal reference to the most recently loaded etemplate2 widget tree + * + * NOTE: This variable can change which etemplate it points to as the user + * works. For example, loading the home or admin apps can cause + * et2_ready() to be called again with a different template. this.et2 will + * then point to a different template. If the user then closes that tab, + * this.et2 will point to a destroyed object, and trying to use it will fail. + * + * If you need a reference to a certain template you can either store a local + * reference or access it through etemplate2. + * + * @example Store a local reference + * // in et2_ready() + * if(name == 'index') this.index_et2 = et2.widgetContainer; + * + * // Remember to clean up in destroy() + * delete this.index_et2; + * + * // Instead of this.et2, using a local reference + * this.index_et2 ... + * + * + * @example Access via etemplate2 object + * // Instead of this.et2, using it's unique ID + * var et2 = etemplate2.getById('myapp-index) + * if(et2) + * { + * et2.widgetContainer. ... + * } + * + * @var {et2_container} + */ + et2: any; + + /** + * Internal reference to egw client-side api object for current app and window + * + * @var {egw} + */ + egw: any; + + sidebox: JQuery; + + viewContainer: JQuery; + viewTemplate: JQuery; + et2_view: any; + favorite_popup : JQuery | any; + + tutorial_initialised: boolean; + + dom_id : string; + + mailvelopeSyncHandlerObj : any; + + /** + * Initialization and setup goes here, but the etemplate2 object + * is not yet ready. + */ + constructor() + { + this.egw = egw(this.appname, window); + + // Initialize sidebox for non-popups. + // ID set server side + if(!this.egw.is_popup()) + { + var sidebox = jQuery('#favorite_sidebox_'+this.appname); + if(sidebox.length == 0 && egw_getFramework() != null) + { + var egw_fw = egw_getFramework(); + sidebox= jQuery('#favorite_sidebox_'+this.appname,egw_fw.sidemenuDiv); + } + // Make sure we're running in the top window when we init sidebox + //@ts-ignore + if(window.app[this.appname] === this && window.top.app[this.appname] !== this && window.top.app[this.appname]) + { + //@ts-ignore + window.top.app[this.appname]._init_sidebox(sidebox); + } + else + { + this._init_sidebox(sidebox); + } + } + this.mailvelopeSyncHandlerObj = this.mailvelopeSyncHandler(); + } + + /** + * Clean up any created objects & references + * @param {object} _app local app object + */ + destroy(_app) + { + delete this.et2; + if (this.sidebox) + this.sidebox.off(); + delete this.sidebox; + if (!_app) delete app[this.appname]; + } + + /** + * This function is called when the etemplate2 object is loaded + * and ready. If you must store a reference to the et2 object, + * make sure to clean it up in destroy(). Note that this can be called + * several times, with different et2 objects, as templates are loaded. + * + * @param {etemplate2} et2 + * @param {string} name template name + */ + et2_ready(et2, name : string) + { + if(this.et2 !== null) + { + egw.debug('log', "Changed et2 object"); + } + this.et2 = et2.widgetContainer; + this._fix_iFrameScrolling(); + if (this.egw && this.egw.is_popup()) this._set_Window_title(); + + // Highlights the favorite based on initial list state + this.highlight_favorite(); + } + + /** + * Observer method receives update notifications from all applications + * + * App is responsible for only reacting to "messages" it is interested in! + * + * @param {string} _msg message (already translated) to show, eg. 'Entry deleted' + * @param {string} _app application name + * @param {(string|number)} _id id of entry to refresh or null + * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {string} _msg_type 'error', 'warning' or 'success' (default) + * @param {object|null} _links app => array of ids of linked entries + * or null, if not triggered on server-side, which adds that info + * @return {false|*} false to stop regular refresh, thought all observers are run + */ + observer(_msg, _app, _id, _type, _msg_type, _links) + { + + } + + /** + * Push method receives push notification about updates to entries from the application + * + * It can use the extra _data parameter to determine if the client has read access to + * the entry - if an update of the list is necessary. + * + * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {string} _app application name + * @param {(string|number)} _id id of entry to refresh or null + * @param {mixed} _data eg. owner or responsible to decide if update is necessary + * @returns {undefined} + */ + push(_type, _app, _id, _data) + { + + } + + /** + * Open an entry. + * + * Designed to be used with the action system as a callback + * eg: onExecute => app..open + * + * @param _action + * @param _senders + */ + open(_action, _senders) { + var id_app = _senders[0].id.split('::'); + egw.open(id_app[1], this.appname); + } + + _do_action(action_id : string, selected : []) + { + } + + /** + * A generic method to action to server asynchronously + * + * Designed to be used with the action system as a callback. + * In the PHP side, set the action + * 'onExecute' => 'javaScript:app..action', and + * implement _do_action(action_id, selected) + * + * @param {egwAction} _action + * @param {egwActionObject[]} _elems + */ + action(_action, _elems) + { + // let user confirm select-all + var select_all = _action.getManager().getActionById("select_all"); + var confirm_msg = (_elems.length > 1 || select_all && select_all.checked) && + typeof _action.data.confirm_multiple != 'undefined' ? + _action.data.confirm_multiple : _action.data.confirm; + + if (typeof confirm_msg != 'undefined') + { + var that = this; + var action_id = _action.id; + et2_dialog.show_dialog(function(button_id,value) + { + if (button_id != et2_dialog.NO_BUTTON) + { + that._do_action(action_id, _elems); + } + }, confirm_msg, egw.lang('Confirmation required'), et2_dialog.BUTTONS_YES_NO, et2_dialog.QUESTION_MESSAGE); + } + else if (typeof this._do_action == 'function') + { + this._do_action(_action.id, _elems); + } + else + { + // If this is a nextmatch action, do an ajax submit setting the action + var nm = null; + var action = _action; + while(nm == null && action.parent != null) + { + if(action.data.nextmatch) nm = action.data.nextmatch; + action = action.parent; + } + if(nm != null) + { + var value = {}; + value[nm.options.settings.action_var] = _action.id; + nm.set_value(value); + nm.getInstanceManager().submit(); + } + } + } + + /** + * Set the application's state to the given state. + * + * While not pretending to implement the history API, it is patterned similarly + * @link http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html + * + * The default implementation works with the favorites to apply filters to a nextmatch. + * + * + * @param {{name: string, state: object}|string} state Object (or JSON string) for a state. + * Only state is required, and its contents are application specific. + * @param {string} template template name to check, instead of trying all templates of current app + * @return {boolean} false - Returns false to stop event propagation + */ + setState(state, template) + { + // State should be an object, not a string, but we'll parse + if(typeof state == "string") + { + if(state.indexOf('{') != -1 || state =='null') + { + state = JSON.parse(state); + } + } + if(typeof state != "object") + { + egw.debug('error', 'Unable to set state to %o, needs to be an object',state); + return; + } + if(state == null) + { + state = {}; + } + + // Check for egw.open() parameters + if(state.state && state.state.id && state.state.app) + { + return egw.open(state.state,undefined,undefined,{},'_self'); + } + + // Try and find a nextmatch widget, and set its filters + var nextmatched = false; + var et2 = template ? etemplate2.getByTemplate(template) : etemplate2.getByApplication(this.appname); + for(var i = 0; i < et2.length; i++) + { + et2[i].widgetContainer.iterateOver(function(_widget) { + // Firefox has trouble with spaces in search + if(state.state && state.state.search) state.state.search = unescape(state.state.search); + + // Apply + if(state.state && state.state.sort && state.state.sort.id) + { + _widget.sortBy(state.state.sort.id, state.state.sort.asc,false); + } + if(state.state && state.state.selectcols) + { + // Make sure it's a real array, not an object, then set cols + _widget.set_columns(jQuery.extend([],state.state.selectcols)); + } + _widget.applyFilters(state.state || state.filter || {}); + nextmatched = true; + }, this, et2_nextmatch); + if(nextmatched) return false; + } + + // 'blank' is the special name for no filters, send that instead of the nice translated name + var safe_name = jQuery.isEmptyObject(state) || jQuery.isEmptyObject(state.state||state.filter) ? 'blank' : state.name.replace(/[^A-Za-z0-9-_]/g, '_'); + var url = '/'+this.appname+'/index.php'; + + // Try a redirect to list, if app defines a "list" value in registry + if (egw.link_get_registry(this.appname, 'list')) + { + url = egw.link('/index.php', jQuery.extend({'favorite': safe_name}, egw.link_get_registry(this.appname, 'list'))); + } + // if no list try index value from application + else if (egw.app(this.appname).index) + { + url = egw.link('/index.php', 'menuaction='+egw.app(this.appname).index+'&favorite='+safe_name); + } + egw.open_link(url, undefined, undefined, this.appname); + return false; + } + + /** + * Retrieve the current state of the application for future restoration + * + * The state can be anything, as long as it's an object. The contents are + * application specific. The default implementation finds a nextmatch and + * returns its value. + * The return value of this function cannot be passed directly to setState(), + * since setState is expecting an additional wrapper, eg: + * {name: 'something', state: getState()} + * + * @return {object} Application specific map representing the current state + */ + getState() + { + var state = {}; + + // Try and find a nextmatch widget, and set its filters + var et2 = etemplate2.getByApplication(this.appname); + for(var i = 0; i < et2.length; i++) + { + et2[i].widgetContainer.iterateOver(function(_widget) { + state = _widget.getValue(); + }, this, et2_nextmatch); + } + + return state; + } + + /** + * Function to load selected row from nm into a template view + * + * @param {object} _action + * @param {object} _senders + * @param {boolean} _noEdit defines whether to set edit button or not default is false + * @param {function} et2_callback function to run after et2 is loaded + */ + viewEntry(_action, _senders, _noEdit, et2_callback) + { + //full id in nm + var id = _senders[0].id; + // flag for edit button + var noEdit = _noEdit || false; + // nm row id + var rowID = ''; + // content to feed to etemplate2 + var content:any = {}; + + var self = this; + + if (id){ + var parts = id.split('::'); + rowID = parts[1]; + content = egw.dataGetUIDdata(id); + if (content.data) content = content.data; + } + + // create a new app object with just constructors for our new etemplate2 object + var app = { classes: window.app.classes }; + + /* destroy generated etemplate for view mode in DOM*/ + var destroy = function(){ + self.viewContainer.remove(); + delete self.viewTemplate; + delete self.viewContainer; + delete self.et2_view; + // we need to reference back into parent context this + for (var v in self) + { + this[v] = self[v]; + } + app = null; + }; + + // view container + this.viewContainer = jQuery(document.createElement('div')) + .addClass('et2_mobile_view') + .css({ + "z-index":102, + width:"100%", + height:"100%", + background:"white", + display:'block', + position: 'absolute', + left:0, + bottom:0, + right:0, + overflow:'auto' + }) + .attr('id','popupMainDiv') + .appendTo('body'); + + // close button + var close = jQuery(document.createElement('span')) + .addClass('egw_fw_mobile_popup_close loaded') + .click(function(){ + destroy.call(app[self.appname]); + //disable selected actions after close + egw_globalObjectManager.setAllSelected(false); + }) + .appendTo(this.viewContainer); + if (!noEdit) + { + // edit button + var edit = jQuery(document.createElement('span')) + .addClass('mobile-view-editBtn') + .click(function(){ + egw.open(rowID, self.appname); + }) + .appendTo(this.viewContainer); + } + // view template main container (content) + this.viewTemplate = jQuery(document.createElement('div')) + .attr('id', this.appname+'-view') + .addClass('et2_mobile-view-container popupMainDiv') + .appendTo(this.viewContainer); + + var mobileViewTemplate = (_action.data.mobileViewTemplate ||'edit').split('?'); + var templateName = mobileViewTemplate[0]; + var templateTimestamp = mobileViewTemplate[1]; + var templateURL = egw.webserverUrl+ '/' + this.appname + '/templates/mobile/'+templateName+'.xet'+'?'+templateTimestamp; + + var data = { + 'content': content, + 'readonlys': {'__ALL__':true,'link_to':false}, + 'currentapp': this.appname, + 'langRequire': this.et2.getArrayMgr('langRequire').data, + 'sel_options': this.et2.getArrayMgr('sel_options').data, + 'modifications': this.et2.getArrayMgr('modifications').data, + 'validation_errors': this.et2.getArrayMgr('validation_errors').data + }; + + // etemplate2 object for view + this.et2_view = new etemplate2 (this.viewTemplate[0], false); + framework.pushState('view'); + if(templateName) + { + this.et2_view.load(this.appname+'.'+templateName,templateURL, data, typeof et2_callback == 'function'?et2_callback:function(){}, app); + } + + // define a global close function for view template + // in order to be able to destroy view on action + this.et2_view.close = destroy; + } + + /** + * Initializes actions and handlers on sidebox (delete) + * + * @param {jQuery} sidebox jQuery of DOM node + */ + _init_sidebox(sidebox) + { + // Initialize egw tutorial sidebox, but only for non-popups, as calendar edit app.js has this.et2 set to tutorial et2 object + if (!this.egw.is_popup()) + { + var egw_fw = egw_getFramework(); + var tutorial = jQuery('#egw_tutorial_'+this.appname+'_sidebox', egw_fw ? egw_fw.sidemenuDiv : document); + // _init_sidebox gets currently called multiple times, which needs to be fixed + if (tutorial.length && !this.tutorial_initialised) + { + this.egwTutorial_init(tutorial[0]); + this.tutorial_initialised = true; + } + } + if(sidebox.length) + { + var self = this; + if(this.sidebox) this.sidebox.off(); + this.sidebox = sidebox; + sidebox + .off() + // removed .on("mouse(enter|leave)" (wrapping trash icon), as it stalls delete in IE11 + .on("click.sidebox","div.ui-icon-trash", this, this.delete_favorite) + // need to install a favorite handler, as we switch original one off with .off() + .on('click.sidebox','li[data-id]', this, function(event) { + var li = jQuery(this); + li.siblings().removeClass('ui-state-highlight'); + + var state = {}; + var pref = egw.preference('favorite_' + this.dataset.id, self.appname); + if(pref) + { + // Extend, to prevent changing the preference by reference + jQuery.extend(true, state, pref); + } + if(this.dataset.id != 'add') + { + event.stopImmediatePropagation(); + self.setState.call(self, state); + return false; + } + }) + .addClass("ui-helper-clearfix"); + + //Add Sortable handler to sideBox fav. menu + jQuery('ul','#favorite_sidebox_'+this.appname).sortable({ + items:'li:not([data-id$="add"])', + placeholder:'ui-fav-sortable-placeholder', + delay:250, //(millisecond) delay before the sorting should start + helper: function(event, item : any) { + // We'll need to know which app this is for + item.attr('data-appname',self.appname); + // Create custom helper so it can be dragged to Home + var h_parent = item.parent().parent().clone(); + h_parent.find('li').not('[data-id="'+item.attr('data-id')+'"]').remove(); + h_parent.appendTo('body'); + return h_parent; + }, + // @ts-ignore + refreshPositions: true, + update: function (event, ui) + { + // @ts-ignore + var favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'}); + + self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList); + + self._refresh_fav_nm(); + } + }); + + // Bind favorite de-select + var egw_fw = egw_getFramework(); + if(egw_fw && egw_fw.applications[this.appname] && egw_fw.applications[this.appname].browser + && egw_fw.applications[this.appname].browser.baseDiv) + { + jQuery(egw_fw.applications[this.appname].browser.baseDiv) + .off('.sidebox') + .on('change.sidebox', function() { + self.highlight_favorite(); + }); + } + return true; + } + return false; + } + + /** + * Add a new favorite + * + * Fetches the current state from the application, then opens a dialog to get the + * name and other settings. If user proceeds, the favorite is saved, and if possible + * the sidebox is directly updated to include the new favorite + * + * @param {object} [state] State settings to be merged into the application state + */ + add_favorite(state) + { + if(typeof this.favorite_popup == "undefined" || // Create popup if it's not defined yet + (this.favorite_popup && typeof this.favorite_popup.group != "undefined" + && !this.favorite_popup.group.isAttached())) // recreate the favorite popup if the group selectbox is not attached (eg. after et2 submit) + { + this._create_favorite_popup(); + } + // Get current state + this.favorite_popup.state = jQuery.extend({}, this.getState(), state||{}); +/* + // Add in extras + for(var extra in this.options.filters) + { + // Don't overwrite what nm has, chances are nm has more up-to-date value + if(typeof this.popup.current_filters == 'undefined') + { + this.popup.current_filters[extra] = this.nextmatch.options.settings[extra]; + } + } + + // Add in application's settings + if(this.filters != true) + { + for(var i = 0; i < this.filters.length; i++) + { + this.popup.current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]]; + } + } +*/ + // Make sure it's an object - deep copy to prevent references in sub-objects (col_filters) + this.favorite_popup.state = jQuery.extend(true,{},this.favorite_popup.state); + + // Update popup with current set filters (more for debug than user) + var filter_list = []; + var add_to_popup = function(arr) { + filter_list.push("
    "); + jQuery.each(arr, function(index, filter) { + filter_list.push("
  • "+index+"" + + (typeof filter != "object" ? ""+filter+"": "") + ); + if(typeof filter == "object" && filter != null) add_to_popup(filter); + filter_list.push("
  • "); + }); + filter_list.push("
"); + }; + add_to_popup(this.favorite_popup.state); + jQuery("#"+this.appname+"_favorites_popup_state",this.favorite_popup) + .replaceWith( + jQuery(filter_list.join("")).attr("id",this.appname+"_favorites_popup_state") + ); + jQuery("#"+this.appname+"_favorites_popup_state",this.favorite_popup) + .hide() + .siblings(".ui-icon-circle-plus") + .removeClass("ui-icon-circle-minus"); + + // Popup + this.favorite_popup.dialog("open"); + console.log(this); + + // Stop the normal bubbling if this is called on click + return false; + } + + /** + * Update favorite items in nm fav. menu + * + */ + _refresh_fav_nm () + { + var self = this; + + if(etemplate2 && etemplate2.getByApplication) + { + var et2 = etemplate2.getByApplication(self.appname); + for(var i = 0; i < et2.length; i++) + { + et2[i].widgetContainer.iterateOver(function(_widget) { + _widget.stored_filters = _widget.load_favorites(self.appname); + _widget.init_filters(_widget); + }, self, et2_favorites); + } + } + else + { + throw new Error ("_refresh_fav_nm():Either et2 is not ready/ not there yet. Make sure that etemplate2 is ready before call this method."); + } + } + + /** + * Create the "Add new" popup dialog + */ + _create_favorite_popup() + { + var self = this; + var favorite_prefix = 'favorite_'; + + // Clear old, if existing + if(this.favorite_popup && this.favorite_popup.group) + { + this.favorite_popup.group.free(); + delete this.favorite_popup; + } + + // Create popup + this.favorite_popup = jQuery('
\ +
\ + ' + + + '\ +
\ + '+ this.egw.lang("Details") + '\ +
    \ + \ +
' + ).appendTo(this.et2 ? this.et2.getDOMNode() : jQuery('body')); + + // @ts-ignore + jQuery(".ui-icon-circle-plus",this.favorite_popup).prev().andSelf().click(function() { + var details = jQuery("#"+self.appname+"_favorites_popup_state",self.favorite_popup) + .slideToggle() + .siblings(".ui-icon-circle-plus") + .toggleClass("ui-icon-circle-minus"); + }); + + // Add some controls if user is an admin + var apps = egw().user('apps'); + var is_admin = (typeof apps['admin'] != "undefined"); + if(is_admin) + { + this.favorite_popup.group = et2_createWidget("select-account",{ + id: "favorite[group]", + account_type: "groups", + empty_label: "Groups", + no_lang: true, + parent_node: this.appname+'_favorites_popup_admin' + },(this.et2 || null)); + this.favorite_popup.group.loadingFinished(); + } + + var buttons = {}; + buttons['save'] = { + text: this.egw.lang('save'), + default: true, + style: 'background-image: url('+this.egw.image('save')+')', + click: function() { + // Add a new favorite + var name = jQuery("#name",this); + + if(name.val()) + { + // Add to the list + name.val(name.val().replace(/(<([^>]+)>)/ig,"")); + var safe_name = name.val().replace(/[^A-Za-z0-9-_]/g,"_"); + var favorite = { + name: name.val(), + group: (typeof self.favorite_popup.group != "undefined" && + self.favorite_popup.group.get_value() ? self.favorite_popup.group.get_value() : false), + state: self.favorite_popup.state + }; + + var favorite_pref = favorite_prefix+safe_name; + + // Save to preferences + if(typeof self.favorite_popup.group != "undefined" && self.favorite_popup.group.getValue() != '') + { + // Admin stuff - save preference server side + self.egw.jsonq('EGroupware\\Api\\Framework::ajax_set_favorite', + [ + self.appname, + name.val(), + "add", + self.favorite_popup.group.get_value(), + self.favorite_popup.state + ] + ); + self.favorite_popup.group.set_value(''); + } + else + { + // Normal user - just save to preferences client side + self.egw.set_preference(self.appname,favorite_pref,favorite); + } + + // Add to list immediately + if(self.sidebox) + { + // Remove any existing with that name + jQuery('[data-id="'+safe_name+'"]',self.sidebox).remove(); + + // Create new item + var html = "\n"; + jQuery(html).insertBefore(jQuery('li',self.sidebox).last()); + self._init_sidebox(self.sidebox); + } + + // Try to update nextmatch favorites too + self._refresh_fav_nm(); + } + // Reset form + delete self.favorite_popup.state; + name.val(""); + jQuery("#filters",self.favorite_popup).empty(); + + jQuery(this).dialog("close"); + } + }; + buttons['cancel'] = { + text: this.egw.lang("cancel"), + style: 'background-image: url('+this.egw.image('cancel')+')', + click: function() { + if(typeof self.favorite_popup.group !== 'undefined' && self.favorite_popup.group.set_value) + { + self.favorite_popup.group.set_value(null); + } + jQuery(this).dialog("close"); + } + }; + + this.favorite_popup.dialog({ + autoOpen: false, + modal: true, + buttons: buttons, + close: function() { + } + }); + + // Bind handler for enter keypress + this.favorite_popup.off('keydown').on('keydown', jQuery.proxy(function(e) { + var tagName = e.target.tagName.toLowerCase(); + tagName = (tagName === 'input' && e.target.type === 'button') ? 'button' : tagName; + + if(e.keyCode == jQuery.ui.keyCode.ENTER && tagName !== 'textarea' && tagName !== 'select' && tagName !=='button') + { + e.preventDefault(); + jQuery('button[default]',this.favorite_popup.parent()).trigger('click'); + return false; + } + },this)); + + return false; + } + + /** + * Delete a favorite from the list and update preferences + * Registered as a handler on the delete icons + * + * @param {jQuery.event} event event object + */ + delete_favorite(event) + { + // Don't do the menu + event.stopImmediatePropagation(); + + var app = event.data; + var id = jQuery(this).parentsUntil('li').parent().attr("data-id"); + var group = jQuery(this).parentsUntil('li').parent().attr("data-group") || ''; + var line = jQuery('li[data-id="'+id+'"]',app.sidebox); + var name = line.first().text(); + var trash = this; + line.addClass('loading'); + + // Make sure first + var do_delete = function(button_id) + { + if(button_id != et2_dialog.YES_BUTTON) + { + line.removeClass('loading'); + return; + } + + // Hide the trash + jQuery(trash).hide(); + + // Delete preference server side + var request = egw.json("EGroupware\\Api\\Framework::ajax_set_favorite", + [app.appname, id, "delete", group, ''], + function(result) { + // Got the full response from callback, which we don't want + if(result.type) return; + + if(result && typeof result == 'boolean') + { + // Remove line from list + line.slideUp("slow", function() { }); + + app._refresh_fav_nm(); + } + else + { + // Something went wrong server side + line.removeClass('loading').addClass('ui-state-error'); + } + }, + jQuery(trash).parentsUntil("li").parent(), + true, + jQuery(trash).parentsUntil("li").parent() + ); + request.sendRequest(true); + }; + et2_dialog.show_dialog(do_delete, (egw.lang("Delete") + " " +name +"?"), + egw.lang("Delete"), et2_dialog.YES_NO, et2_dialog.QUESTION_MESSAGE); + + return false; + } + + /** + * Mark the favorite closest matching the current state + * + * Closest matching takes into account not set values, so we pick the favorite + * with the most matching values without a value that differs. + */ + highlight_favorite() { + if(!this.sidebox) return; + + var state = this.getState(); + var best_match = false; + var best_count = 0; + var self = this; + + jQuery('li[data-id]',this.sidebox).removeClass('ui-state-highlight'); + + jQuery('li[data-id]',this.sidebox).each(function(i,href) { + var favorite : any = {}; + if(this.dataset.id && egw.preference('favorite_'+this.dataset.id,self.appname)) + { + favorite = egw.preference('favorite_'+this.dataset.id,self.appname); + } + if(!favorite || jQuery.isEmptyObject(favorite)) return; + + // Handle old style by making it like new style + if(favorite.filter && !favorite.state) + { + favorite.state = favorite.filter; + } + + var match_count = 0; + var extra_keys = Object.keys(favorite.state); + for(var state_key in state) + { + extra_keys.splice(extra_keys.indexOf(state_key),1); + if(typeof favorite.state != 'undefined' && typeof state[state_key] != 'undefined'&&typeof favorite.state[state_key] != 'undefined' && ( state[state_key] == favorite.state[state_key] || !state[state_key] && !favorite.state[state_key])) + { + match_count++; + } + else if (state_key == 'selectcols') + { + // Skip, might be set, might not + } + else if (typeof state[state_key] != 'undefined' && state[state_key] && typeof state[state_key] === 'object' + && typeof favorite.state != 'undefined' && typeof favorite.state[state_key] != 'undefined' && favorite.state[state_key] && typeof favorite.state[state_key] === 'object') + { + if((typeof state[state_key].length !== 'undefined' || typeof state[state_key].length !== 'undefined') + && (state[state_key].length || Object.keys(state[state_key]).length) != (favorite.state[state_key].length || Object.keys(favorite.state[state_key]).length )) + { + // State or favorite has a length, but the other does not + if((state[state_key].length === 0 || Object.keys(state[state_key]).length === 0) && + (favorite.state[state_key].length == 0 || Object.keys(favorite.state[state_key]).length === 0)) + { + // Just missing, or one is an array and the other is an object + continue; + } + // One has a value and the other doesn't, no match + return; + } + else if (state[state_key].length !== 'undefined' && typeof favorite.state[state_key].length !== 'undefined' && + state[state_key].length === 0 && favorite.state[state_key].length === 0) + { + // Both set, but both empty + match_count++; + continue; + } + // Consider sub-objects (column filters) individually + for(var sub_key in state[state_key]) + { + if(state[state_key][sub_key] == favorite.state[state_key][sub_key] || !state[state_key][sub_key] && !favorite.state[state_key][sub_key]) + { + match_count++; + } + else if (state[state_key][sub_key] && favorite.state[state_key][sub_key] && + typeof state[state_key][sub_key] === 'object' && typeof favorite.state[state_key][sub_key] === 'object') + { + // Too deep to keep going, just string compare for perfect match + if(JSON.stringify(state[state_key][sub_key]) === JSON.stringify(favorite.state[state_key][sub_key])) + { + match_count++; + } + } + else if(typeof state[state_key][sub_key] !== 'undefined' && state[state_key][sub_key] != favorite.state[state_key][sub_key]) + { + // Different values, do not match + return; + } + } + } + else if (typeof state[state_key] !== 'undefined' + && typeof favorite.state != 'undefined'&&typeof favorite.state[state_key] !== 'undefined' + && state[state_key] != favorite.state[state_key]) + { + // Different values, do not match + return; + } + } + // Check for anything set that the current one does not have + for(var i = 0; i < extra_keys.length; i++) + { + if(favorite.state[extra_keys[i]]) return; + } + if(match_count > best_count) + { + best_match = this.dataset.id; + best_count = match_count; + } + }); + if(best_match) + { + jQuery('li[data-id="'+best_match+'"]',this.sidebox).addClass('ui-state-highlight'); + } + } + + /** + * Fix scrolling iframe browsed by iPhone/iPod/iPad touch devices + */ + _fix_iFrameScrolling() + { + if (/iPhone|iPod|iPad/.test(navigator.userAgent)) + { + jQuery("iframe").on({ + load: function() + { + var body = this.contentWindow.document.body; + + var div = jQuery(document.createElement("div")) + .css ({ + 'height' : jQuery(this.parentNode).height(), + 'width' : jQuery(this.parentNode).width(), + 'overflow' : 'scroll'}); + while (body.firstChild) + { + div.append(body.firstChild); + } + jQuery(body).append(div); + } + }); + } + } + + /** + * Set document title, uses getWindowTitle to get the correct title, + * otherwise set it with uniqueID as default title + */ + _set_Window_title () + { + var title = this.getWindowTitle(); + if (title) + { + document.title = this.et2._inst.uniqueId + ": " + title; + } + } + + /** + * Window title getter function in order to set the window title + * this can be overridden on each application app.js file to customize the title value + * + * @returns {string} window title + */ + getWindowTitle () + { + var titleWidget = this.et2.getWidgetById('title'); + if (titleWidget) + { + return titleWidget.options.value; + } + else + { + return this.et2._inst.uniqueId; + } + } + + /** + * Handler for drag and drop when dragging nextmatch rows from mail app + * and dropped on a row in the current application. We copy the mail into + * the filemanager to link it since we can't link directly. + * + * This doesn't happen automatically. Each application must indicate that + * it will accept dropped mail by it's nextmatch actions: + * + * $actions['info_drop_mail'] = array( + * 'type' => 'drop', + * 'acceptedTypes' => 'mail', + * 'onExecute' => 'javaScript:app.infolog.handle_dropped_mail', + * 'hideOnDisabled' => true + * ); + * + * This action, when defined, will not affect the automatic linking between + * normal applications. + * + * @param {egwAction} _action + * @param {egwActionObject[]} _selected Dragged mail rows + * @param {egwActionObject} _target Current application's nextmatch row the mail was dropped on + */ + handle_dropped_mail(_action, _selected, _target) + { + /** + * Mail doesn't support link system, so we copy it to VFS + */ + var ids = _target.id.split("::"); + if(ids.length != 2 || ids[0] == 'mail') return; + + var vfs_path = "/apps/"+ids[0]+"/"+ids[1]; + var mail_ids = []; + + for(var i = 0; i < _selected.length; i++) + { + mail_ids.push(_selected[i].id); + } + if(mail_ids.length) + { + egw.message(egw.lang("Please wait...")); + this.egw.json('filemanager.filemanager_ui.ajax_action',['mail',mail_ids, vfs_path],function(data){ + // Trigger an update (minimal, no sorting changes) to display the new link + egw.refresh(data.msg||'',ids[0],ids[1],'update'); + }).sendRequest(true); + } + } + + /** + * Get json data for videos from the given url + * + * @return {Promise, object} return Promise, json object as resolved result and error message in case of failure + */ + egwTutorialGetData(){ + var self = this; + return new Promise (function(_resolve, _reject) + { + var resolve = _resolve; + var reject = _reject; + // delay the execution and let the rendering catches up. Seems only FF problem + window.setTimeout(function(){ + self.egw.json('EGroupware\\Api\\Framework\\Tutorial::ajax_data', [self.egw.app_name()], function(_data){ + resolve(_data); + }).sendRequest(); + },0); + + }); + } + + /** + * Create and Render etemplate2 for egroupware tutorial + * sidebox option. The .xet file is stored in api/templates/default/egw_tutorials + * + * @description tutorials json object should have the following structure: + * object: + * { + * [app name]:{ + * [language tag]:[ + * {src:"",thumbnail:"",title:"",desc:""} + * ] + * } + * } + * + * *Note: "desc" and "title" are optional attributes, which "desc" would appears as tooltip for the video. + * + * example: + * { + * "mail":{ + * "en":[ + * {src:"https://www.youtube.com/embed/mCDJndpjO40", thumbnail:"http://img.youtube.com/vi/mCDJndpjO40/0.jpg", "title":"PGP Encryption", "desc":""}, + * {src:"https://www.youtube.com/embed/mCDJndpjO", thumbnail:"http://img.youtube.com/vi/mCDJndpjO/0.jpg", "title":"Subscription", "desc":""}, + * ], + * "de":[ + * {src:"https://www.youtube.com/embed/m40", thumbnail:"http://img.youtube.com/vi/m40/0.jpg", "title":"PGP Verschlüsselung", "desc":""}, + * {src:"https://www.youtube.com/embed/mpjO", thumbnail:"http://img.youtube.com/vi/mpjO/0.jpg", "title":"Ordner Abonnieren", "desc":""}, + * ] + * } + * } + * + * @param {DOMNode} div + */ + egwTutorial_init(div) + { + // et2 object + var etemplate = new etemplate2 (div, false); + var template = egw.webserverUrl+'/api/templates/default/egw_tutorial.xet?1'; + + this.egwTutorialGetData().then(function(_data){ + var lang = egw.preference('lang'); + var content = {content:{list:[]}}; + if (_data && _data[egw.app_name()]) + { + if (!_data[egw.app_name()][lang]) lang = 'en'; + if (typeof _data[egw.app_name()][lang] !='undefined' + && _data[egw.app_name()][lang].length > 0) + { + for (var i=0;i < _data[egw.app_name()][lang].length;i++) + { + var tuid = egw.app_name() + '-' +lang + '-' + i; + _data[egw.app_name()][lang][i]['onclick'] = 'app.'+egw.app_name()+'.egwTutorialPopup("'+tuid+'")'; + } + content.content.list = _data[egw.app_name()][lang]; + + if (template.indexOf('.xet') >0) + { + etemplate.load ('',template , content, function(){}); + } + else + { + etemplate.load (template, '', content); + } + } + } + }, + function(_err){ + console.log(_err); + }); + } + + /** + * Open popup to show given tutorial id + * @param {string} _tuid tutorial object id + * - tuid: appname-lang-index + */ + egwTutorialPopup (_tuid) + { + var url = egw.link('/index.php', 'menuaction=api.EGroupware\\Api\\Framework\\Tutorial.popup&tuid='+_tuid); + egw.open_link(url,'_blank','960x580'); + } + + /** + * Function to set video iframe base on selected tutorial from tutorials box + * + * @param {string} _url + */ + tutorial_videoOnClick (_url) + { + var frame = etemplate2.getByApplication('api')[0].widgetContainer.getWidgetById('src'); + if (frame) + { + frame.set_value(_url); + } + } + + /** + * Function calls on discard checkbox and will set + * the egw_tutorial_noautoload preference + * + * @param {type} egw + * @param {type} widget + */ + tutorial_autoloadDiscard (egw, widget) + { + if (widget) + { + this.egw.set_preference('common', 'egw_tutorial_noautoload', widget.get_value()); + } + } + + /** + * Check if Mailvelope is available, open (or create) "egroupware" keyring and call callback with it + * + * @param {function} _callback called if and only if mailvelope is available (context is this!) + */ + mailvelopeAvailable(_callback) + { + var self = this; + var callback = jQuery.proxy(_callback, this); + + if (typeof mailvelope !== 'undefined') + { + this.mailvelopeOpenKeyring().then(callback); + } + else + { + jQuery(window).on('mailvelope', function() + { + self.mailvelopeOpenKeyring().then(callback); + }); + } + } + + /** + * mailvelope object contains SyncHandlers + * + * @property {function} descriptionuploadSync function called by Mailvelope to upload encrypted private key backup + * @property {function} downloadSync function called by Mailvelope to download encrypted private key backup + * @property {function} backup function called by Mailvelope to upload a public keyring backup + * @property {function} restore function called by Mailvelope to restore a public keyring backup + */ + private mailvelopeSyncHandler() + { + return { + /** + * function called by Mailvelope to upload a public keyring + * @param {UploadSyncHandler} _uploadObj + * @property {string} etag entity tag for the uploaded encrypted keyring, or null if initial upload + * @property {AsciiArmored} keyringMsg encrypted keyring as PGP armored message + * @returns {Promise.} + */ + uploadSync: function(_uploadObj) + { + return new Promise(function(_resolve,_reject){}); + }, + + /** + * function called by Mailvelope to download a public keyring + * + * @param {object} _downloadObj + * @property {string} etag entity tag for the current local keyring, or null if no local eTag + * @returns {Promise.} + */ + downloadSync: function(_downloadObj) + { + return new Promise(function(_resolve,_reject){}); + }, + + /** + * function called by Mailvelope to upload an encrypted private key backup + * + * @param {BackupSyncPacket} _backup + * @property {AsciiArmored} backup an encrypted private key as PGP armored message + * @returns {Promise.} + */ + backup: function(_backup) + { + return new Promise(function(_resolve,_reject){ + // Store backup sync packet into .PGP-Key-Backup file in user directory + jQuery.ajax({ + method:'PUT', + url: egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup', + contentType: 'application/json', + data: JSON.stringify(_backup), + success:function(){ + _resolve(_backup); + }, + error: function(_err){ + _reject(_err); + } + }); + }); + }, + + /** + * function called by Mailvelope to restore an encrypted private key backup + * + * @returns {Promise.} + * @todo + */ + restore: function() + { + return new Promise(function(_resolve,_reject){ + var resolve = _resolve; + var reject = _reject; + jQuery.ajax({ + url:egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup', + method: 'GET', + success: function(_backup){ + resolve(JSON.parse(_backup)); + egw.message('Your key has been restored successfully.'); + }, + error: function(_err){ + //Try with old back file name + if (_err.status == 404) + { + jQuery.ajax({ + method:'GET', + url: egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PK_PGP', + success: function(_backup){ + resolve(JSON.parse(_backup)); + egw.message('Your key has been restored successfully.'); + }, + error: function(_err){ + _reject(_err); + } + }); + } + else + { + _reject(_err); + } + } + }); + }); + } + }; + } + + /** + * Function for backup file operations + * + * @param {type} _url Url of the backup file + * @param {type} _cmd command to operate + * - PUT: to store backup file + * - GET: to read backup file + * - DELETE: to delete backup file + * + * @param {type} _successCallback function called when the operation is successful + * @param {type} _errorCallback function called when the operation fails + * @param {type} _data data which needs to be stored in file via PUT command + */ + _mailvelopeBackupFileOperator(_url, _cmd, _successCallback, _errorCallback, _data?) + { + var ajaxObj = { + url: _url || egw.webserverUrl+'/webdav.php/home/'+egw.user('account_lid')+'/.PGP-Key-Backup', + method: _cmd, + success: _successCallback, + error: _errorCallback + }; + switch (_cmd) + { + case 'PUT': + jQuery.extend({},ajaxObj, { + data: JSON.stringify(_data), + contentType: 'application/json' + }); + break; + case 'GET': + jQuery.extend({},ajaxObj, { + dataType: 'json' + }); + break; + case 'DELETE': + break; + } + jQuery.ajax(ajaxObj); + } + + /** + * Create backup dialog + * @param {string} _selector DOM selector to attach backupDialog + * @param {boolean} _initSetup determine wheter it's an initialization backup or restore backup + * + * @returns {Promise.} + */ + mailvelopeCreateBackupDialog(_selector?, _initSetup?) + { + var self = this; + var selector = _selector || 'body'; + var initSetup = _initSetup; + jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove(); + return new Promise(function(_resolve, _reject) + { + var resolve = _resolve; + var reject = _reject; + + mailvelope.getKeyring('egroupware').then(function(_keyring : any) + { + _keyring.addSyncHandler(self.mailvelopeSyncHandlerObj); + + var options = { + initialSetup:initSetup + }; + _keyring.createKeyBackupContainer(selector, options).then(function(_popupId){ + var $backup_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'); + $backup_selector.css({position:'absolute', "z-index":1}); + _popupId.isReady().then(function(result){ + egw.message('Your key has been backedup into .PGP-Key-Backup successfully.'); + jQuery(selector).empty(); + }); + resolve(_popupId); + }, + function(_err){ + reject(_err); + }); + }, + function(_err) + { + reject(_err); + }); + }); + } + + /** + * Delete backup key from filesystem + */ + mailvelopeDeleteBackup() + { + var self = this; + et2_dialog.show_dialog(function (_button_id) + { + if (_button_id == et2_dialog.YES_BUTTON ) + { + self._mailvelopeBackupFileOperator(undefined, 'DELETE', function(){ + self.egw.message(self.egw.lang('The backup key has been deleted.')); + }, function(_err){ + self.egw.message(self.egw.lang('Was not able to delete the backup key because %1',_err)); + }); + } + }, + self.egw.lang('Are you sure, you would like to delete the backup key?'), + self.egw.lang('Delete backup key'), + {}, et2_dialog.BUTTONS_YES_CANCEL, et2_dialog.QUESTION_MESSAGE, undefined, self.egw); + } + + /** + * Create mailvelope restore dialog + * @param {string} _selector DOM selector to attach restorDialog + * @param {boolean} _restorePassword if true, will restore key password too + * + * @returns {Promise} + */ + mailvelopeCreateRestoreDialog(_selector, _restorePassword) + { + var self = this; + var restorePassword = _restorePassword; + var selector = _selector || 'body'; + //Clear the + jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').remove(); + return new Promise(function(_resolve, _reject){ + var resolve = _resolve; + var reject = _reject; + + mailvelope.getKeyring('egroupware').then(function(_keyring) + { + _keyring.addSyncHandler(self.mailvelopeSyncHandlerObj); + + var options = { + restorePassword:restorePassword + }; + _keyring.restoreBackupContainer(selector, options).then(function(_restoreId){ + var $restore_selector = jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'); + $restore_selector.css({position:'absolute', "z-index":1}); + resolve(_restoreId); + }, + function(_err){ + reject(_err); + }); + }, + function(_err) + { + reject(_err); + }); + }); + } + + /** + * Create a dialog to show all backup/restore options + * + * @returns {undefined} + */ + mailvelopeCreateBackupRestoreDialog() + { + var self = this; + var appname = egw.app_name(); + var menu = [ + // Header row should be empty item 0 + {}, + // Restore Keyring item 1 + {label:"Restore key" ,image:"lock", onclick:"app."+appname+".mailvelopeCreateRestoreDialog('#_mvelo')"}, + // Restore pass phrase item 2 + {label:"Restore password",image:"password", onclick:"app."+appname+".mailvelopeCreateRestoreDialog('#_mvelo', true)"}, + // Delete backup Key item 3 + {label:"Delete backup", image:"delete", onclick:"app."+appname+".mailvelopeDeleteBackup"}, + // Backup Key item 4 + {label:"Backup Key", image:"save", onclick:"app."+appname+".mailvelopeCreateBackupDialog('#_mvelo', false)"} + ]; + + var dialog = function(_content, _callback?) + { + return et2_createWidget("dialog", { + callback: function(_button_id, _value) { + if (typeof _callback == "function") + { + _callback.call(this, _button_id, _value.value); + } + }, + title: egw.lang('Backup/Restore'), + buttons:[{"button_id": 'close',"text": egw.lang('Close'), id: 'dialog[close]', image: 'cancelled', "default":true}], + value: { + content: { + menu:_content + } + }, + template: egw.webserverUrl+'/api/templates/default/pgp_backup_restore.xet', + class: "pgp_backup_restore", + modal:true + }); + }; + if (typeof mailvelope != 'undefined') + { + mailvelope.getKeyring('egroupware').then(function(_keyring) + { + self._mailvelopeBackupFileOperator(undefined, 'GET', function(_data){ + dialog(menu); + }, + function(){ + // Remove delete item + menu.splice(3,1); + menu[3]['onclick'] = "app."+appname+".mailvelopeCreateBackupDialog('#_mvelo', true)"; + dialog(menu); + }); + }, + function(){ + mailvelope.createKeyring('egroupware').then(function(){dialog(menu);}); + }); + } + else + { + this.mailvelopeInstallationOffer(); + } + } + + /** + * Create a dialog and offers installation option for installing mailvelope plugin + * plus it offers a video tutorials to get the user morte familiar with mailvelope + */ + mailvelopeInstallationOffer () + { + var buttons = [ + {"text": egw.lang('Install'), id: 'install', image: 'check', "default":true}, + {"text": egw.lang('Close'), id:'close', image: 'cancelled'} + ]; + var dialog = function(_content, _callback) + { + return et2_createWidget("dialog", { + callback: function(_button_id, _value) { + if (typeof _callback == "function") + { + _callback.call(this, _button_id, _value.value); + } + }, + title: egw.lang('PGP Encryption Installation'), + buttons: buttons, + dialog_type: 'info', + value: { + content: _content + }, + template: egw.webserverUrl+'/api/templates/default/pgp_installation.xet', + class: "pgp_installation", + modal: true + //resizable:false, + }); + }; + var content = [ + // Header row should be empty item 0 + {}, + {domain:this.egw.lang('Add your domain as "%1" in options to list of email providers and enable API.', + '*.'+this._mailvelopeDomain()), video:"test", control:"true"} + ]; + + dialog(content, function(_button){ + if (_button == 'install') + { + if (typeof chrome != 'undefined') + { + // ATM we are not able to trigger mailvelope installation directly + // since the installation should be triggered from the extension + // owner validate website (mailvelope.com), therefore, we just redirect + // user to chrome webstore to install mailvelope from there. + window.open('https://chrome.google.com/webstore/detail/mailvelope/kajibbejlbohfaggdiogboambcijhkke'); + } + else if (typeof InstallTrigger != 'undefined' && InstallTrigger.enabled()) + { + InstallTrigger.install({mailvelope:"https://download.mailvelope.com/releases/latest/mailvelope.firefox.xpi"}, + function(_url, _status){ + if (_status == 0) + { + et2_dialog.alert(egw.lang('Mailvelope addon installation succeded. Now you may configure the options.')); + return; + } + else + { + et2_dialog.alert(egw.lang('Mailvelope addon installation failed! Please try again.')); + } + }); + } + } + }); + } + + /** + * PGP begin and end tags + */ + readonly begin_pgp_message: '-----BEGIN PGP MESSAGE-----'; + readonly end_pgp_message: '-----END PGP MESSAGE-----'; + + /** + * Mailvelope "egroupware" Keyring + */ + mailvelope_keyring : any = undefined; + + /** + * jQuery selector for Mailvelope iframes in all browsers + */ + readonly mailvelope_iframe_selector: 'iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]'; + + /** + * Open (or create) "egroupware" keyring and call callback with it + * + * @returns {Promise.} Keyring or Error with message + */ + mailvelopeOpenKeyring() + { + var self = this; + + return new Promise(function(_resolve, _reject) + { + if (self.mailvelope_keyring) _resolve(self.mailvelope_keyring); + + var resolve = _resolve; + var reject = _reject; + + mailvelope.getKeyring('egroupware').then(function(_keyring) + { + self.mailvelope_keyring = _keyring; + + resolve(_keyring); + }, + function(_err) + { + mailvelope.createKeyring('egroupware').then(function(_keyring) + { + self.mailvelope_keyring = _keyring; + var mvelo_settings_selector = self.mailvelope_iframe_selector + .split(',').map(function(_val){return 'body>'+_val;}).join(','); + + mailvelope.createSettingsContainer('body', _keyring, { + email: self.egw.user('account_email'), + fullName: self.egw.user('account_fullname') + }).then(function() + { + // make only Mailvelope settings dialog visible + jQuery(mvelo_settings_selector).css({position: 'absolute', top: 0}); + // add a close button, so we know when to offer storing public key to AB + jQuery('') + .css({position: 'absolute', top: 8, right: 8, "z-index":2}) + .click(function() + { + // try fetching public key, to check user created onw + self.mailvelope_keyring.exportOwnPublicKey(self.egw.user('account_email')).then(function(_pubKey) + { + // CreateBackupDialog + self.mailvelopeCreateBackupDialog().then(function(_popupId){ + jQuery('iframe[src^="chrome-extension"],iframe[src^="about:blank?mvelo"]').css({position:'absolute', "z-index":1}); + }, + function(_err){ + egw.message(_err); + }); + + // if yes, hide settings dialog + jQuery(mvelo_settings_selector).each(function(index,item : any){ + if (!item.src.match(/keyBackupDialog.html/,'ig')) item.remove(); + }); + jQuery('button#mailvelope_close_settings').remove(); + + // offer user to store his public key to AB for other users to find + var buttons = [ + {button_id: 2, text: 'Yes', id: 'dialog[yes]', image: 'check', default: true}, + {button_id: 3, text : 'No', id: 'dialog[no]', image: 'cancelled'} + ]; + if (egw.user('apps').admin) + { + buttons.unshift({ + button_id: 5, text: 'Yes and allow non-admin users to do that too (recommended)', + id: 'dialog[yes_allow]', image: 'check', default: true + }); + delete buttons[1].default; + } + et2_dialog.show_dialog(function (_button_id) + { + if (_button_id != et2_dialog.NO_BUTTON ) + { + var keys = {}; + keys[self.egw.user('account_id')] = _pubKey; + self.egw.json('addressbook.addressbook_bo.ajax_set_pgp_keys', + [keys, _button_id != et2_dialog.YES_BUTTON ? true : undefined]).sendRequest() + .then(function(_data) + { + self.egw.message(_data.response['0'].data); + }); + } + }, + self.egw.lang('It is recommended to store your public key in addressbook, so other users can write you encrypted mails.'), + self.egw.lang('Store your public key in Addressbook?'), + {}, buttons, et2_dialog.QUESTION_MESSAGE, undefined, self.egw); + }, + function(_err){ + self.egw.message(_err.message+"\n\n"+ + self.egw.lang("You will NOT be able to send or receive encrypted mails before completing that step!"), 'error'); + }); + }) + .appendTo('body'); + }); + resolve(_keyring); + }, + function(_err) + { + reject(_err); + }); + }); + }); + } + + /** + * Mailvelope uses Domain without first part: eg. "stylite.de" for "egw.stylite.de" + * + * @returns {string} + */ + _mailvelopeDomain() + { + var parts = document.location.hostname.split('.'); + if (parts.length > 1) parts.shift(); + return parts.join('.'); + } + + /** + * Check if we have a key for all recipients + * + * @param {Array} _recipients + * @returns {Promise.} Array of recipients or Error with recipients without key + */ + mailvelopeGetCheckRecipients(_recipients) + { + // replace rfc822 addresses with raw email, as Mailvelop does not like them and lowercase all email + var rfc822_preg = /<([^'" <>]+)>$/; + var recipients = _recipients.map(function(_recipient) + { + var matches = _recipient.match(rfc822_preg); + return matches ? matches[1].toLowerCase() : _recipient.toLowerCase(); + }); + + // check if we have keys for all recipients + var self = this; + return new Promise(function(_resolve, _reject) + { + var resolve = _resolve; + var reject = _reject; + self.mailvelopeOpenKeyring().then(function(_keyring : any) + { + var keyring = _keyring; + _keyring.validKeyForAddress(recipients).then(function(_status) + { + var no_key = []; + for(var email in _status) + { + if (!_status[email]) no_key.push(email); + } + if (no_key.length) + { + // server addressbook on server for missing public keys + self.egw.json('addressbook.addressbook_bo.ajax_get_pgp_keys', [no_key]).sendRequest().then(function(_data) + { + var data = _data.response['0'].data; + var promises = []; + for(var email in data) + { + promises.push(keyring.importPublicKey(data[email]).then(function(_result) + { + if (_result == 'IMPORTED' || _result == 'UPDATED') + { + no_key.splice(no_key.indexOf(email),1); + } + })); + } + Promise.all(promises).then(function() + { + if (no_key.length) + { + reject(new Error(self.egw.lang('No key for recipient:')+' '+no_key.join(', '))); + } + else + { + resolve(recipients); + } + }); + }); + } + else + { + resolve(recipients); + } + }); + }, + function(_err) + { + reject(_err); + }); + }); + } + + /** + * Check if the share action is enabled for this entry + * + * @param {egwAction} _action + * @param {egwActionObject[]} _entries + * @param {egwActionObject} _target + * @returns {boolean} if action is enabled + */ + is_share_enabled(_action, _entries, _target) + { + return true; + } + /** + * create a share-link for the given entry + * + * @param {egwAction} _action egw actions + * @param {egwActionObject[]} _senders selected nm row + * @param {egwActionObject} _target Drag source. Not used here. + * @param {Boolean} _writable Allow edit access from the share. + * @param {Boolean} _files Allow access to files from the share. + * @param {Function} _callback Callback with results + * @returns {Boolean} returns false if not successful + */ + share_link(_action, _senders, _target, _writable, _files, _callback){ + var path = _senders[0].id; + if(!path) + { + return this.egw.message(this.egw.lang('Missing share path. Unable to create share.'), 'error'); + } + switch(_action.id) + { + case 'shareFilemanager': + // Sharing a link to just files in filemanager + var id = path.split('::'); + path = '/apps/'+ id[0] + '/' + id[1]; + } + if(typeof _writable === 'undefined' && _action.parent && _action.parent.getActionById('shareWritable')) + { + _writable = _action.parent.getActionById('shareWritable').checked || false; + } + if(typeof _files === 'undefined' && _action.parent && _action.parent.getActionById('shareFiles')) + { + _files = _action.parent.getActionById('shareFiles').checked || false; + } + + return egw.json('EGroupware\\Api\\Sharing::ajax_create', [_action.id, path, _writable, _files], + _callback ? _callback : this._share_link_callback, this, true, this).sendRequest(); + } + + share_merge(_action, _senders, _target) + { + var parent = _action.parent.parent; + var _writable = false; + var _files = false; + if(parent && parent.getActionById('shareWritable')) + { + _writable = parent.getActionById('shareWritable').checked || false; + } + if(parent && parent.getActionById('shareFiles')) + { + _files = parent.getActionById('shareFiles').checked || false; + } + + // Share only works on one at a time + var promises = []; + for(var i = 0; i < _senders.length; i++) + { + promises.push(new Promise(function(resolve, reject) { + this.share_link(_action, [_senders[i]], _target, _writable, _files, resolve); + }.bind(this))); + } + + // But merge into email can handle several + Promise.all(promises.map(function(p){p.catch(function(e){console.log(e)})})) + .then(function(values) { + // Process document after all shares created + return nm_action(_action, _senders, _target); + }); + } + + /** + * Share-link callback + * @param {object} _data + */ + _share_link_callback(_data) { + if (_data.msg || _data.share_link) window.egw_refresh(_data.msg, this.appname); + + var copy_link_to_clipboard = function(evt){ + var $target = jQuery(evt.target); + $target.select(); + try { + var successful = document.execCommand('copy'); + if (successful) + { + egw.message('Share link copied into clipboard'); + return true; + } + } + catch (e) {} + egw.message('Failed to copy the link!'); + }; + jQuery("body").on("click", "[name=share_link]", copy_link_to_clipboard); + et2_createWidget("dialog", { + callback: function( button_id, value) { + jQuery("body").off("click", "[name=share_link]", copy_link_to_clipboard); + return true; + }, + title: _data.title ? _data.title : egw.lang("%1 Share Link", _data.writable ? egw.lang("Writable"): egw.lang("Readonly")), + template: _data.template, + width: 450, + value: {content:{ "share_link": _data.share_link }} + }); + } +} diff --git a/api/js/jsapi/egw_global.d.ts b/api/js/jsapi/egw_global.d.ts new file mode 100644 index 0000000000..3e26f06d7f --- /dev/null +++ b/api/js/jsapi/egw_global.d.ts @@ -0,0 +1,16 @@ +/** + * for now use "somehow" created global egw + */ +declare function egw_getFramework() : any; +//declare var window : Window & typeof globalThis; +declare var chrome : any; +declare var InstallTrigger : any; +//declare function egw(string, object) : object; +declare var egw : any; +declare var app : {classes: any}; +declare var egw_globalObjectManager : any; +declare var framework : any; + +declare var mailvelope : any; + +declare function egw_refresh(_msg : string, app : string, id? : string|number, _type?, targetapp?, replace?, _with?, msgtype?); \ No newline at end of file diff --git a/package.json b/package.json index 4b7d9f5c03..3705f1fa20 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "dependencies": {}, "repository": {}, "devDependencies": { + "@types/jquery": "^1.10.35", + "@types/jqueryui": "^1.11.37", "grunt": "^1.0.3", "grunt-contrib-cssmin": "^2.2.1", "grunt-contrib-uglify-es": "^3.3.0", diff --git a/timesheet/js/app.js b/timesheet/js/app.js index 4cd8018280..63bf2661ce 100644 --- a/timesheet/js/app.js +++ b/timesheet/js/app.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware - Timesheet - Javascript UI * @@ -6,206 +7,169 @@ * @author Hadi Nategh * @copyright (c) 2008-16 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +/*egw:uses + /api/js/jsapi/egw_app.js + */ +require("jquery"); +require("jqueryui"); +require("../jsapi/egw_global"); +require("../etemplate/et2_types"); +var egw_app_1 = require("../../api/js/jsapi/egw_app"); /** * UI for timesheet * * @augments AppJS */ -app.classes.timesheet = AppJS.extend( -{ - appname: 'timesheet', - /** - * et2 widget container - */ - et2: null, - /** - * path widget - */ - - /** - * Constructor - * - * @memberOf app.timesheet - */ - init: function() - { - // call parent - this._super.apply(this, arguments); - }, - - /** - * Destructor - */ - destroy: function() - { - delete this.et2; - // call parent - this._super.apply(this, arguments); - }, - - /** - * This function is called when the etemplate2 object is loaded - * and ready. If you must store a reference to the et2 object, - * make sure to clean it up in destroy(). - * - * @param et2 etemplate2 Newly ready object - */ - et2_ready: function(et2) - { - // call parent - this._super.apply(this, arguments); - - if (typeof et2.templates['timesheet.index'] != "undefined") - { - this.filter_change(); - this.filter2_change(); - } - }, - - /** - * - */ - filter_change: function() - { - var filter = this.et2.getWidgetById('filter'); - var dates = this.et2.getWidgetById('timesheet.index.dates'); - - if (filter && dates) - { - dates.set_disabled(filter.get_value() !== "custom"); - if (filter.value == "custom") - { - jQuery(this.et2.getWidgetById('startdate').getDOMNode()).find('input').focus(); - } - } - return true; - }, - - /** - * show or hide the details of rows by selecting the filter2 option - * either 'all' for details or 'no_description' for no details - * - */ - filter2_change: function() - { - var nm = this.et2.getWidgetById('nm'); - var filter2 = this.et2.getWidgetById('filter2'); - - if (nm && filter2) - { - egw.css("#timesheet-index span.timesheet_titleDetails","font-weight:" + (filter2.getValue() == '1' ? "bold;" : "normal;")); - // Show / hide descriptions - egw.css(".et2_label.ts_description","display:" + (filter2.getValue() == '1' ? "block;" : "none;")); - } - }, - - /** - * Wrapper so add action in the context menu can pass current - * filter values into new edit dialog - * - * @see add_with_extras - * - * @param {egwAction} action - * @param {egwActionObject[]} selected - */ - add_action_handler: function(action, selected) - { - var nm = action.getManager().data.nextmatch || false; - if(nm) - { - this.add_with_extras(nm); - } - }, - - /** - * Opens a new edit dialog with some extra url parameters pulled from - * nextmatch filters. - * - * @param {et2_widget} widget Originating/calling widget - */ - add_with_extras: function(widget) - { - var nm = widget.getRoot().getWidgetById('nm'); - var nm_value = nm.getValue() || {}; - - var extras = {}; - if(nm_value.cat_id) - { - extras.cat_id = nm_value.cat_id; - } - - if(nm_value.col_filter && nm_value.col_filter.linked) - { - var split = nm_value.col_filter.linked.split(':') || ''; - extras.link_app = split[0] || ''; - extras.link_id = split[1] || ''; - } - if(nm_value.col_filter && nm_value.col_filter.pm_id) - { - extras.link_app = 'projectmanager'; - extras.link_id = nm_value.col_filter.pm_id; - } - else if (nm_value.col_filter && nm_value.col_filter.ts_project) - { - extras.ts_project = nm_value.col_filter.ts_project; - } - - egw.open('','timesheet','add',extras); - }, - - /** - * Change handler for project selection to set empty ts_project string, if project get deleted - * - * @param {type} _egw - * @param {et2_widget_link_entry} _widget - * @returns {undefined} - */ - pm_id_changed: function(_egw, _widget) - { - // Update price list - var ts_pricelist = _widget.getRoot().getWidgetById('pl_id'); - egw.json('projectmanager_widget::ajax_get_pricelist',[_widget.getValue()],function(value) { - ts_pricelist.set_select_options(value||{}) - }).sendRequest(true); - - var ts_project = this.et2.getWidgetById('ts_project'); - if (ts_project) - { - ts_project.set_blur(_widget.getValue() ? _widget.search.val() : ''); - } - }, - - /** - * Update custom filter timespan, without triggering a change - */ - update_timespan: function(start, end) - { - if(this && this.et2) - { - var nm = this.et2.getWidgetById('nm'); - if(nm) - { - // Toggle update_in_progress to avoid another request - nm.update_in_progress = true; - this.et2.getWidgetById('startdate').set_value(start); - this.et2.getWidgetById('enddate').set_value(end); - nm.activeFilters.startdate = start; - nm.activeFilters.enddate = end; - nm.update_in_progress = false; - } - } - }, - - /** - * Get title in order to set it as document title - * @returns {string} - */ - getWindowTitle: function() - { - var widget = this.et2.getWidgetById('ts_title'); - if(widget) return widget.options.value; - } -}); +var TimesheetApp = /** @class */ (function (_super) { + __extends(TimesheetApp, _super); + function TimesheetApp() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * This function is called when the etemplate2 object is loaded + * and ready. If you must store a reference to the et2 object, + * make sure to clean it up in destroy(). + * + * @param et2 etemplate2 Newly ready object + * @param string name + */ + TimesheetApp.prototype.et2_ready = function (et2, name) { + // call parent + _super.prototype.et2_ready.call(this, et2, name); + if (typeof et2.templates['timesheet.index'] != "undefined") { + this.filter_change(); + this.filter2_change(); + } + }; + /** + * + */ + TimesheetApp.prototype.filter_change = function () { + var filter = this.et2.getWidgetById('filter'); + var dates = this.et2.getWidgetById('timesheet.index.dates'); + if (filter && dates) { + dates.set_disabled(filter.get_value() !== "custom"); + if (filter.value == "custom") { + jQuery(this.et2.getWidgetById('startdate').getDOMNode()).find('input').focus(); + } + } + return true; + }; + /** + * show or hide the details of rows by selecting the filter2 option + * either 'all' for details or 'no_description' for no details + * + */ + TimesheetApp.prototype.filter2_change = function () { + var nm = this.et2.getWidgetById('nm'); + var filter2 = this.et2.getWidgetById('filter2'); + if (nm && filter2) { + egw.css("#timesheet-index span.timesheet_titleDetails", "font-weight:" + (filter2.getValue() == '1' ? "bold;" : "normal;")); + // Show / hide descriptions + egw.css(".et2_label.ts_description", "display:" + (filter2.getValue() == '1' ? "block;" : "none;")); + } + }; + /** + * Wrapper so add action in the context menu can pass current + * filter values into new edit dialog + * + * @see add_with_extras + * + * @param {egwAction} action + * @param {egwActionObject[]} selected + */ + TimesheetApp.prototype.add_action_handler = function (action, selected) { + var nm = action.getManager().data.nextmatch || false; + if (nm) { + this.add_with_extras(nm); + } + }; + /** + * Opens a new edit dialog with some extra url parameters pulled from + * nextmatch filters. + * + * @param {et2_widget} widget Originating/calling widget + */ + TimesheetApp.prototype.add_with_extras = function (widget) { + var nm = widget.getRoot().getWidgetById('nm'); + var nm_value = nm.getValue() || {}; + var extras = {}; + if (nm_value.cat_id) { + extras.cat_id = nm_value.cat_id; + } + if (nm_value.col_filter && nm_value.col_filter.linked) { + var split = nm_value.col_filter.linked.split(':') || ''; + extras.link_app = split[0] || ''; + extras.link_id = split[1] || ''; + } + if (nm_value.col_filter && nm_value.col_filter.pm_id) { + extras.link_app = 'projectmanager'; + extras.link_id = nm_value.col_filter.pm_id; + } + else if (nm_value.col_filter && nm_value.col_filter.ts_project) { + extras.ts_project = nm_value.col_filter.ts_project; + } + egw.open('', 'timesheet', 'add', extras); + }; + /** + * Change handler for project selection to set empty ts_project string, if project get deleted + * + * @param {type} _egw + * @param {et2_widget_link_entry} _widget + * @returns {undefined} + */ + TimesheetApp.prototype.pm_id_changed = function (_egw, _widget) { + // Update price list + var ts_pricelist = _widget.getRoot().getWidgetById('pl_id'); + egw.json('projectmanager_widget::ajax_get_pricelist', [_widget.getValue()], function (value) { + ts_pricelist.set_select_options(value || {}); + }).sendRequest(true); + var ts_project = this.et2.getWidgetById('ts_project'); + if (ts_project) { + ts_project.set_blur(_widget.getValue() ? _widget.search.val() : ''); + } + }; + /** + * Update custom filter timespan, without triggering a change + */ + TimesheetApp.prototype.update_timespan = function (start, end) { + if (this && this.et2) { + var nm = this.et2.getWidgetById('nm'); + if (nm) { + // Toggle update_in_progress to avoid another request + nm.update_in_progress = true; + this.et2.getWidgetById('startdate').set_value(start); + this.et2.getWidgetById('enddate').set_value(end); + nm.activeFilters.startdate = start; + nm.activeFilters.enddate = end; + nm.update_in_progress = false; + } + } + }; + /** + * Get title in order to set it as document title + * @returns {string} + */ + TimesheetApp.prototype.getWindowTitle = function () { + var widget = this.et2.getWidgetById('ts_title'); + if (widget) + return widget.options.value; + }; + return TimesheetApp; +}(egw_app_1.EgwApp)); +app.classes.timesheet = TimesheetApp; diff --git a/timesheet/js/app.ts b/timesheet/js/app.ts new file mode 100644 index 0000000000..17da1918c0 --- /dev/null +++ b/timesheet/js/app.ts @@ -0,0 +1,196 @@ +/** + * EGroupware - Timesheet - Javascript UI + * + * @link http://www.egroupware.org + * @package timesheet + * @author Hadi Nategh + * @copyright (c) 2008-16 by Ralf Becker + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + */ + +/*egw:uses + /api/js/jsapi/egw_app.js + */ + +import 'jquery'; +import 'jqueryui'; +import '../jsapi/egw_global'; +import '../etemplate/et2_types'; + +import { EgwApp } from '../../api/js/jsapi/egw_app'; + +/** + * UI for timesheet + * + * @augments AppJS + */ +class TimesheetApp extends EgwApp +{ + readonly appname: 'timesheet'; + + /** + * This function is called when the etemplate2 object is loaded + * and ready. If you must store a reference to the et2 object, + * make sure to clean it up in destroy(). + * + * @param et2 etemplate2 Newly ready object + * @param string name + */ + et2_ready(et2, name : string) + { + // call parent + super.et2_ready(et2, name); + + if (typeof et2.templates['timesheet.index'] != "undefined") + { + this.filter_change(); + this.filter2_change(); + } + } + + /** + * + */ + filter_change() + { + var filter = this.et2.getWidgetById('filter'); + var dates = this.et2.getWidgetById('timesheet.index.dates'); + + if (filter && dates) + { + dates.set_disabled(filter.get_value() !== "custom"); + if (filter.value == "custom") + { + jQuery(this.et2.getWidgetById('startdate').getDOMNode()).find('input').focus(); + } + } + return true; + } + + /** + * show or hide the details of rows by selecting the filter2 option + * either 'all' for details or 'no_description' for no details + * + */ + filter2_change() + { + var nm = this.et2.getWidgetById('nm'); + var filter2 = this.et2.getWidgetById('filter2'); + + if (nm && filter2) + { + egw.css("#timesheet-index span.timesheet_titleDetails","font-weight:" + (filter2.getValue() == '1' ? "bold;" : "normal;")); + // Show / hide descriptions + egw.css(".et2_label.ts_description","display:" + (filter2.getValue() == '1' ? "block;" : "none;")); + } + } + + /** + * Wrapper so add action in the context menu can pass current + * filter values into new edit dialog + * + * @see add_with_extras + * + * @param {egwAction} action + * @param {egwActionObject[]} selected + */ + add_action_handler(action, selected) + { + var nm = action.getManager().data.nextmatch || false; + if(nm) + { + this.add_with_extras(nm); + } + } + + /** + * Opens a new edit dialog with some extra url parameters pulled from + * nextmatch filters. + * + * @param {et2_widget} widget Originating/calling widget + */ + add_with_extras(widget) + { + var nm = widget.getRoot().getWidgetById('nm'); + var nm_value = nm.getValue() || {}; + + var extras : any = {}; + if (nm_value.cat_id) + { + extras.cat_id = nm_value.cat_id; + } + + if (nm_value.col_filter && nm_value.col_filter.linked) + { + var split = nm_value.col_filter.linked.split(':') || ''; + extras.link_app = split[0] || ''; + extras.link_id = split[1] || ''; + } + if (nm_value.col_filter && nm_value.col_filter.pm_id) + { + extras.link_app = 'projectmanager'; + extras.link_id = nm_value.col_filter.pm_id; + } + else if (nm_value.col_filter && nm_value.col_filter.ts_project) + { + extras.ts_project = nm_value.col_filter.ts_project; + } + + egw.open('','timesheet','add',extras); + } + + /** + * Change handler for project selection to set empty ts_project string, if project get deleted + * + * @param {type} _egw + * @param {et2_widget_link_entry} _widget + * @returns {undefined} + */ + pm_id_changed(_egw, _widget) + { + // Update price list + var ts_pricelist = _widget.getRoot().getWidgetById('pl_id'); + egw.json('projectmanager_widget::ajax_get_pricelist',[_widget.getValue()],function(value) { + ts_pricelist.set_select_options(value||{}) + }).sendRequest(true); + + var ts_project = this.et2.getWidgetById('ts_project'); + if (ts_project) + { + ts_project.set_blur(_widget.getValue() ? _widget.search.val() : ''); + } + } + + /** + * Update custom filter timespan, without triggering a change + */ + update_timespan(start, end) + { + if(this && this.et2) + { + var nm = this.et2.getWidgetById('nm'); + if(nm) + { + // Toggle update_in_progress to avoid another request + nm.update_in_progress = true; + this.et2.getWidgetById('startdate').set_value(start); + this.et2.getWidgetById('enddate').set_value(end); + nm.activeFilters.startdate = start; + nm.activeFilters.enddate = end; + nm.update_in_progress = false; + } + } + } + + /** + * Get title in order to set it as document title + * @returns {string} + */ + getWindowTitle() + { + var widget = this.et2.getWidgetById('ts_title'); + if(widget) return widget.options.value; + } +} + +app.classes.timesheet = TimesheetApp; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..289d350068 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es5", + "lib": ["es2015","dom"], + "outDir": "", + "rootDir": "./", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "removeComments": false, + "moduleResolution": "node", + "noImplicitAny": false + }, + "include" : [ + "**/js/*", + "api/js/**/*", + "node_modules/@types/**/*", + "*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file From 0a66978fcda2458e85351cfae5cdac3ebb94be42 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sun, 19 Jan 2020 15:36:00 +0100 Subject: [PATCH 02/61] script to convert app.js to TypeScript --- doc/js2ts.php | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100755 doc/js2ts.php diff --git a/doc/js2ts.php b/doc/js2ts.php new file mode 100755 index 0000000000..41c75730bf --- /dev/null +++ b/doc/js2ts.php @@ -0,0 +1,193 @@ +#!/usr/bin/env php + + * @copyright 2020 by Ralf Becker + */ + +if (PHP_SAPI !== 'cli') // security precaution: forbit calling as web-page +{ + die('

fix_api.php must NOT be called as web-page --> exiting !!!

'); +} + +// raw replacements +$replace = array( + '/^app.classes.([a-z0-9_]+)(\s*=\s*AppJS.extend)\(.*^}\);/misU' => + function($matches) { + return strtr($matches[0], [ + 'app.classes.'.$matches[1].$matches[2].'(' => 'class '.ucfirst($matches[1]).'App extends EgwApp', + "\n});" => "\n}\n\napp.classes.$matches[1] = ".ucfirst($matches[1])."App;" + ]); + }, + "/^\tappname:\s*'([^']+)',/m" => "\treadonly appname: '$1';", + "/^\t([^: ,;(]+):\s*([^()]+),/m" => "\t\$1: $2;", + "/^\t([^:\n]+):\s*function\s*\(.*this._super.(apply|call)\(/msU" => + function($matches) { + return str_replace('this._super', + $matches[1] === 'init' ? 'super' : 'super.'.$matches[1], $matches[0]); + }, + "/^\t([^:\n]+):\s*function\s*\(/m" => function($matches) { + return "\t".($matches[1] === 'init' ? 'constructor' : $matches[1]).'('; + }, + "/^\t},$/m" => "\t}", + '/^ \* @version \$Id\$\n/m' => '', + '#^ \* @link http://www.egroupware.org#m' => ' * @link: https://www.egroupware.org', +); + +/** + * Add boilerplate for app.js files after header + * + * @param $app + * @param $content + * @return string + */ +function app_js_add($app, $content) +{ + return preg_replace('#^(/\*\*.*\n\ \*/)#Us', << Date: Tue, 21 Jan 2020 10:12:39 +0100 Subject: [PATCH 03/61] WIP with TypeScript --- api/js/etemplate/et2_core_DOMWidget.js | 1553 ++++++++---------- api/js/etemplate/et2_core_DOMWidget.ts | 899 ++++++++++ api/js/etemplate/et2_core_common.js | 1075 +++++------- api/js/etemplate/et2_core_common.ts | 780 +++++++++ api/js/etemplate/et2_core_inheritance.js | 301 ++-- api/js/etemplate/et2_core_inheritance.ts | 217 +++ api/js/etemplate/et2_core_interfaces.js | 183 +-- api/js/etemplate/et2_core_interfaces.ts | 208 +++ api/js/etemplate/et2_core_widget.js | 1896 ++++++++++------------ api/js/etemplate/et2_core_widget.ts | 1066 ++++++++++++ api/js/etemplate/et2_types.d.ts | 19 +- api/js/jsapi/app_base.js | 22 + api/js/jsapi/egw_global.d.ts | 1 + 13 files changed, 5388 insertions(+), 2832 deletions(-) create mode 100644 api/js/etemplate/et2_core_DOMWidget.ts create mode 100644 api/js/etemplate/et2_core_common.ts create mode 100644 api/js/etemplate/et2_core_inheritance.ts create mode 100644 api/js/etemplate/et2_core_interfaces.ts create mode 100644 api/js/etemplate/et2_core_widget.ts diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js index c4720bbbd6..41cb7f5fec 100644 --- a/api/js/etemplate/et2_core_DOMWidget.js +++ b/api/js/etemplate/et2_core_DOMWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS DOM Widget class * @@ -6,16 +7,31 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_core_interfaces; - et2_core_widget; - /api/js/egw_action/egw_action.js; + et2_core_interfaces; + et2_core_widget; + /api/js/egw_action/egw_action.js; */ - +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +require("./et2_core_interfaces"); +require("./et2_core_common"); +var et2_core_widget_1 = require("./et2_core_widget"); +require("../egw_action/egw_action.js"); /** * Abstract widget class which can be inserted into the DOM. All widget classes * deriving from this class have to care about implementing the "getDOMNode" @@ -23,820 +39,660 @@ * * @augments et2_widget */ -var et2_DOMWidget = (function(){ "use strict"; return et2_widget.extend(et2_IDOMNode, -{ - attributes: { - "disabled": { - "name": "Disabled", - "type": "boolean", - "description": "Defines whether this widget is visible. Not to be confused with an input widget's HTML attribute 'disabled'.", - "default": false - }, - "width": { - "name": "Width", - "type": "dimension", - "default": et2_no_init, - "description": "Width of the element in pixels, percentage or 'auto'" - }, - "height": { - "name": "Height", - "type": "dimension", - "default": et2_no_init, - "description": "Height of the element in pixels, percentage or 'auto'" - }, - "class": { - "name": "CSS Class", - "type": "string", - "default": et2_no_init, - "description": "CSS Class which is applied to the dom element of this node" - }, - "overflow": { - "name": "Overflow", - "type": "string", - "default": et2_no_init, - "description": "If set, the css-overflow attribute is set to that value" - }, - "parent_node": { - "name": "DOM parent", - "type": "string", - "default": et2_no_init, - "description": "Insert into the target DOM node instead of the normal location" - }, - "actions": { - "name": "Actions list", - "type": "any", - "default": et2_no_init, - "description": "List of egw actions that can be done on the widget. This includes context menu, drag and drop. TODO: Link to action documentation" - }, - default_execute: { - name: "Default onExecute for actions", - type: "js", - default: et2_no_init, - description: "Set default onExecute javascript method for action not specifying their own" - }, - resize_ratio: { - name: "Resize height of the widget on callback resize", - type:"string", - default: '', - description: "Allow Resize height of the widget based on exess height and given ratio" - }, - data: { - name: "comma-separated name:value pairs set as data attributes on DOM node", - type: "string", - default: '', - description: 'data="mime:${row}[mime]" would generate data-mime="..." in DOM, eg. to use it in CSS on a parent' - }, - background: { - name: "Add background image", - type: "string", - default:'', - description: "Sets background image, left, right and scale on DOM", - } - }, - - /** - * When the DOMWidget is initialized, it grabs the DOM-Node of the parent - * object (if available) and passes it to its own "createDOMNode" function - * - * @memberOf et2_DOMWidget - */ - init: function() { - // Call the inherited constructor - this._super.apply(this, arguments); - - this.parentNode = null; - - this._attachSet = { - "node": null, - "parent": null - }; - - this.disabled = false; - this._surroundingsMgr = null; - }, - - /** - * Detatches the node from the DOM and clears all references to the parent - * node or the dom node of this widget. - */ - destroy: function() { - - this.detachFromDOM(); - this.parentNode = null; - this._attachSet = {}; - - if(this._actionManager) - { - var app_om = egw_getObjectManager(this.egw().getAppName(), false,1); - if(app_om) - { - var om = app_om.getObjectById(this.id); - if(om) om.remove(); - } - this._actionManager.remove(); - this._actionManager = null; - } - - if (this._surroundingsMgr) - { - this._surroundingsMgr.free(); - this._surroundingsMgr = null; - } - - this._super(); - }, - - /** - * Attaches the container node of this widget to the DOM-Tree - */ - doLoadingFinished: function() { - // Check whether the parent implements the et2_IDOMNode interface. If - // yes, grab the DOM node and create our own. - if (this._parent && this._parent.implements(et2_IDOMNode)) { - if(this.options.parent_node) - { - this.set_parent_node(this.options.parent_node); - } - else - { - this.setParentDOMNode(this._parent.getDOMNode(this)); - } - } - - return true; - }, - - /** - * Detaches the widget from the DOM tree, if it had been attached to the - * DOM-Tree using the attachToDOM method. - */ - detachFromDOM: function() { - - if (this._attachSet.node && this._attachSet.parent) - { - // Remove the current node from the parent node - try { - this._attachSet.parent.removeChild(this._attachSet.node); - } catch (e) { - // Don't throw a DOM error if the node wasn't in the parent - } - - // Reset the "attachSet" - this._attachSet = { - "node": null, - "parent": null - }; - - return true; - } - - return false; - }, - - /** - * Attaches the widget to the DOM tree. Fails if the widget is already - * attached to the tree or no parent node or no node for this widget is - * defined. - */ - attachToDOM: function() { - // Attach the DOM node of this widget (if existing) to the new parent - var node = this.getDOMNode(this); - if (node && this.parentNode && - (node != this._attachSet.node || - this.parentNode != this._attachSet.parent)) - { - // If the surroundings manager exists, surround the DOM-Node of this - // widget with the DOM-Nodes inside the surroundings manager. - if (this._surroundingsMgr) - { - node = this._surroundingsMgr.getDOMNode(node); - } - - // Append this node at its index - var idx = this.getDOMIndex(); - if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) - { - this.parentNode.appendChild(node); - } - else - { - this.parentNode.insertBefore(node, this.parentNode.childNodes[idx]); - } - - // Store the currently attached nodes - this._attachSet = { - "node": node, - "parent": this.parentNode - }; - - return true; - } - - return false; - }, - - isAttached: function() { - return this.parentNode != null; - }, - - getSurroundings: function() { - if (!this._surroundingsMgr) - { - this._surroundingsMgr = new et2_surroundingsMgr(this); - } - - return this._surroundingsMgr; - }, - - /** - * Get data for the tab this widget is on. - * - * Will return null if the widget is not on a tab or tab data containing - * - id - * - label - * - widget (top level widget) - * - contentDiv (jQuery object for the div the tab content is in) - * - * @returns {Object|null} Data for tab the widget is on - */ - get_tab_info: function() { - var parent = this; - do { - parent = parent._parent; - } while (parent !== this.getRoot() && parent._type !== 'tabbox'); - - // No tab - if(parent === this.getRoot()) - { - return null; - } - - // Find the tab index - for(var i = 0; i < parent.tabData.length; i++) - { - // Find the tab by DOM heritage - if(parent.tabData[i].contentDiv.has(this.div).length) - { - return parent.tabData[i]; - } - } - // On a tab, but we couldn't find it by DOM nodes Maybe tab template is - // not loaded yet. Try checking IDs. - var template = this; - do { - template = template._parent; - } while (template !== parent && template._type !== 'template'); - for(var i = parent.tabData.length - 1; i >= 0; i--) - { - if(template && template.id && template.id === parent.tabData[i].id) - { - return parent.tabData[i]; - } - } - // Fallback - return this._parent.get_tab_info(); - }, - - /** - * Set the parent DOM node of this element. Takes a wider variety of types - * than setParentDOMNode(), and matches the set_ naming convention. - * - * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. - */ - set_parent_node: function(_node) { - if(typeof _node == "string") - { - var parent = jQuery('#'+_node); - if(parent.length === 0 && window.parent) - { - // Could not find it, try again with wider context - // (in case there's an iframe in admin, for example) - parent = jQuery('#'+_node, window.parent.document); - } - if(parent.length === 0) - { - this.egw().debug('warn','Unable to find DOM parent node with ID "%s" for widget %o.',_node,this); - } - else - { - this.setParentDOMNode(parent.get(0)); - } - } - else - { - this.setParentDOMNode(_node); - } - }, - - /** - * Set the parent DOM node of this element. If another parent node is already - * set, this widget removes itself from the DOM tree - * - * @param _node - */ - setParentDOMNode: function(_node) { - if (_node != this.parentNode) - { - // Detatch this element from the DOM tree - this.detachFromDOM(); - - this.parentNode = _node; - - // And attatch the element to the DOM tree - this.attachToDOM(); - } - }, - - /** - * Returns the parent node. - */ - getParentDOMNode: function() { - return this.parentNode; - }, - - /** - * Returns the index of this element in the DOM tree - */ - getDOMIndex: function() { - if (this._parent) - { - var idx = 0; - var children = this._parent.getChildren(); - - if(children && children.indexOf) return children.indexOf(this); - - egw.debug('warn', 'No Array.indexOf(), falling back to looping. '); - for (var i = 0; i < children.length; i++) - { - if (children[i] == this) - { - return idx; - } - else if (children[i].isInTree()) - { - idx++; - } - } - } - - return -1; - }, - - /** - * Sets the id of the DOM-Node. - * - * DOM id's have dots "." replaced with dashes "-" - * - * @param {string} _value id to set - */ - set_id: function(_value) { - - this.id = _value; - this.dom_id = _value ? this.getInstanceManager().uniqueId+'_'+_value.replace(/\./g, '-') : _value; - - var node = this.getDOMNode(this); - if (node) - { - if (_value != "") - { - node.setAttribute("id", this.dom_id); - } - else - { - node.removeAttribute("id"); - } - } - }, - - set_disabled: function(_value) { - var node = this._surroundingsMgr != null ? this._surroundingsMgr.getDOMNode(this.getDOMNode(this)) : this.getDOMNode(this); - if (node && this.disabled != _value) - { - this.disabled = _value; - - if (_value) - { - jQuery(node).hide(); - } - else - { - jQuery(node).show(); - } - } - }, - - set_width: function(_value) { - this.width = _value; - - var node = this.getDOMNode(this); - if (node) - { - jQuery(node).css("width", _value); - } - }, - - set_height: function(_value) { - this.height = _value; - - var node = this.getDOMNode(this); - if (node) - { - jQuery(node).css("height", _value); - } - }, - - set_class: function(_value) { - var node = this.getDOMNode(this); - if (node) - { - if (this["class"]) - { - jQuery(node).removeClass(this["class"]); - } - jQuery(node).addClass(_value); - } - - this["class"] = _value; - }, - - set_overflow: function(_value) { - this.overflow = _value; - - var node = this.getDOMNode(this); - if (node) - { - jQuery(node).css("overflow", _value); - } - }, - - set_data: function(_value) - { - var node = this.getDOMNode(this); - if (node && _value) - { - var pairs = _value.split(/,/g); - for(var i=0; i < pairs.length; ++i) - { - var name_value = pairs[i].split(':'); - jQuery(node).attr('data-'+name_value[0], name_value[1]); - } - } - }, - - set_background: function(_value) - { - var node = this.getDOMNode(this); - var values = ''; - if (_value && node) - { - values = _value.split(','); - jQuery(node).css({ - "background-image":'url("'+values[0]+'")', - "background-position-x":values[1], - "background-position-y":values[2], - "background-scale":values[3] - }); - } - }, - - /** - * Set Actions on the widget - * - * Each action is defined as an object: - * - * move: { - * type: "drop", - * acceptedTypes: "mail", - * icon: "move", - * caption: "Move to" - * onExecute: javascript:mail_move" - * } - * - * This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped, - * the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the - * dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The - * etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler - * can operate in the widget context easily. The location varies depending on your action though. It - * might be action.parent.parent.data.widget - * - * To customise how the actions are handled for a particular widget, override _link_actions(). It handles - * the more widget-specific parts. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - * @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method - */ - set_actions: function(actions) - { - if(this.id == "" || typeof this.id == "undefined") - { - this.egw().debug("warn", "Widget should have an ID if you want actions",this); - return; - } - - // Initialize the action manager and add some actions to it - // Only look 1 level deep - var gam = egw_getActionManager(this.egw().appName,true,1); - if(typeof this._actionManager != "object") - { - if(gam.getActionById(this.getInstanceManager().uniqueId,1) !== null) - { - gam = gam.getActionById(this.getInstanceManager().uniqueId,1); - } - if(gam.getActionById(this.id,1) != null) - { - this._actionManager = gam.getActionById(this.id,1); - } - else - { - this._actionManager = gam.addAction("actionManager", this.id); - } - } - this._actionManager.updateActions(actions, this.egw().appName); - if (this.options.default_execute) this._actionManager.setDefaultExecute(this.options.default_execute); - - // Put a reference to the widget into the action stuff, so we can - // easily get back to widget context from the action handler - this._actionManager.data = {widget: this}; - - // Link the actions to the DOM - this._link_actions(actions); - }, - - set_default_execute: function(_default_execute) - { - this.options.default_execute = _default_execute; - - if (this._actionManager) this._actionManager.setDefaultExecute(null, _default_execute); - }, - - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * This can be overwritten to not allow all actions, by not returning them here. - * - * @param actions - * @returns {Array} - */ - _get_action_links: function(actions) - { - var action_links = []; - for(var i in actions) - { - var action = actions[i]; - action_links.push(typeof action.id != 'undefined' ? action.id : i); - } - return action_links; - }, - - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions: function(actions) - { - // Get the top level element for the tree - var objectManager = egw_getAppObjectManager(true); - var widget_object = objectManager.getObjectById(this.id); - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = objectManager.insertObject(false, new egwActionObject( - this.id, objectManager, new et2_action_object_impl(this), - this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager - )); - } - else - { - widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); - } - - // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); - - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - widget_object.updateActionLinks(action_links); - } - -});}).call(this); - +var et2_DOMWidget = /** @class */ (function (_super_1) { + __extends(et2_DOMWidget, _super_1); + /** + * When the DOMWidget is initialized, it grabs the DOM-Node of the parent + * object (if available) and passes it to its own "createDOMNode" function + * + * @memberOf et2_DOMWidget + */ + function et2_DOMWidget(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})) || this; + _this.parentNode = null; + _this._attachSet = { + "node": null, + "parent": null + }; + _this.disabled = false; + _this._surroundingsMgr = null; + return _this; + } + /** + * Detatches the node from the DOM and clears all references to the parent + * node or the dom node of this widget. + */ + et2_DOMWidget.prototype.destroy = function () { + this.detachFromDOM(); + this.parentNode = null; + this._attachSet = {}; + if (this._actionManager) { + var app_om = egw_getObjectManager(this.egw().getAppName(), false, 1); + if (app_om) { + var om = app_om.getObjectById(this.id); + if (om) + om.remove(); + } + this._actionManager.remove(); + this._actionManager = null; + } + if (this._surroundingsMgr) { + this._surroundingsMgr.free(); + this._surroundingsMgr = null; + } + this._super(); + }; + /** + * Attaches the container node of this widget to the DOM-Tree + */ + et2_DOMWidget.prototype.doLoadingFinished = function () { + // Check whether the parent implements the et2_IDOMNode interface. If + // yes, grab the DOM node and create our own. + if (this.getParent() && this.getParent().implements(et2_IDOMNode)) { + if (this.options.parent_node) { + this.set_parent_node(this.options.parent_node); + } + else { + this.setParentDOMNode(this.getParent().getDOMNode(this)); + } + } + return true; + }; + /** + * Detaches the widget from the DOM tree, if it had been attached to the + * DOM-Tree using the attachToDOM method. + */ + et2_DOMWidget.prototype.detachFromDOM = function () { + if (this._attachSet.node && this._attachSet.parent) { + // Remove the current node from the parent node + try { + this._attachSet.parent.removeChild(this._attachSet.node); + } + catch (e) { + // Don't throw a DOM error if the node wasn't in the parent + } + // Reset the "attachSet" + this._attachSet = { + "node": null, + "parent": null + }; + return true; + } + return false; + }; + /** + * Attaches the widget to the DOM tree. Fails if the widget is already + * attached to the tree or no parent node or no node for this widget is + * defined. + */ + et2_DOMWidget.prototype.attachToDOM = function () { + // Attach the DOM node of this widget (if existing) to the new parent + var node = this.getDOMNode(this); + if (node && this.parentNode && + (node != this._attachSet.node || + this.parentNode != this._attachSet.parent)) { + // If the surroundings manager exists, surround the DOM-Node of this + // widget with the DOM-Nodes inside the surroundings manager. + if (this._surroundingsMgr) { + node = this._surroundingsMgr.getDOMNode(node); + } + // Append this node at its index + var idx = this.getDOMIndex(); + if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) { + this.parentNode.appendChild(node); + } + else { + this.parentNode.insertBefore(node, this.parentNode.childNodes[idx]); + } + // Store the currently attached nodes + this._attachSet = { + "node": node, + "parent": this.parentNode + }; + return true; + } + return false; + }; + et2_DOMWidget.prototype.isAttached = function () { + return this.parentNode != null; + }; + et2_DOMWidget.prototype.getSurroundings = function () { + if (!this._surroundingsMgr) { + this._surroundingsMgr = new et2_surroundingsMgr(this); + } + return this._surroundingsMgr; + }; + /** + * Get data for the tab this widget is on. + * + * Will return null if the widget is not on a tab or tab data containing + * - id + * - label + * - widget (top level widget) + * - contentDiv (jQuery object for the div the tab content is in) + * + * @returns {Object|null} Data for tab the widget is on + */ + et2_DOMWidget.prototype.get_tab_info = function () { + var parent = this; + do { + parent = parent._parent; + } while (parent !== this.getRoot() && parent._type !== 'tabbox'); + // No tab + if (parent === this.getRoot()) { + return null; + } + // Find the tab index + for (var i = 0; i < parent.tabData.length; i++) { + // Find the tab by DOM heritage + if (parent.tabData[i].contentDiv.has(this.div).length) { + return parent.tabData[i]; + } + } + // On a tab, but we couldn't find it by DOM nodes Maybe tab template is + // not loaded yet. Try checking IDs. + var template = this; + do { + template = template._parent; + } while (template !== parent && template._type !== 'template'); + for (var i = parent.tabData.length - 1; i >= 0; i--) { + if (template && template.id && template.id === parent.tabData[i].id) { + return parent.tabData[i]; + } + } + // Fallback + return this.getParent().get_tab_info(); + }; + /** + * Set the parent DOM node of this element. Takes a wider variety of types + * than setParentDOMNode(), and matches the set_ naming convention. + * + * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. + */ + et2_DOMWidget.prototype.set_parent_node = function (_node) { + if (typeof _node == "string") { + var parent = jQuery('#' + _node); + if (parent.length === 0 && window.parent) { + // Could not find it, try again with wider context + // (in case there's an iframe in admin, for example) + parent = jQuery('#' + _node, window.parent.document); + } + if (parent.length === 0) { + this.egw().debug('warn', 'Unable to find DOM parent node with ID "%s" for widget %o.', _node, this); + } + else { + this.setParentDOMNode(parent.get(0)); + } + } + else { + this.setParentDOMNode(_node); + } + }; + /** + * Set the parent DOM node of this element. If another parent node is already + * set, this widget removes itself from the DOM tree + * + * @param _node + */ + et2_DOMWidget.prototype.setParentDOMNode = function (_node) { + if (_node != this.parentNode) { + // Detatch this element from the DOM tree + this.detachFromDOM(); + this.parentNode = _node; + // And attatch the element to the DOM tree + this.attachToDOM(); + } + }; + /** + * Returns the parent node. + */ + et2_DOMWidget.prototype.getParentDOMNode = function () { + return this.parentNode; + }; + /** + * Returns the index of this element in the DOM tree + */ + et2_DOMWidget.prototype.getDOMIndex = function () { + if (this.getParent()) { + var idx = 0; + var children = this.getParent().getChildren(); + if (children && children.indexOf) + return children.indexOf(this); + egw.debug('warn', 'No Array.indexOf(), falling back to looping. '); + for (var i = 0; i < children.length; i++) { + if (children[i] == this) { + return idx; + } + else if (children[i].isInTree()) { + idx++; + } + } + } + return -1; + }; + /** + * Sets the id of the DOM-Node. + * + * DOM id's have dots "." replaced with dashes "-" + * + * @param {string} _value id to set + */ + et2_DOMWidget.prototype.set_id = function (_value) { + this.id = _value; + this.dom_id = _value ? this.getInstanceManager().uniqueId + '_' + _value.replace(/\./g, '-') : _value; + var node = this.getDOMNode(this); + if (node) { + if (_value != "") { + node.setAttribute("id", this.dom_id); + } + else { + node.removeAttribute("id"); + } + } + }; + et2_DOMWidget.prototype.set_disabled = function (_value) { + var node = this._surroundingsMgr != null ? this._surroundingsMgr.getDOMNode(this.getDOMNode(this)) : this.getDOMNode(this); + if (node && this.disabled != _value) { + this.disabled = _value; + if (_value) { + jQuery(node).hide(); + } + else { + jQuery(node).show(); + } + } + }; + et2_DOMWidget.prototype.set_width = function (_value) { + this.width = _value; + var node = this.getDOMNode(this); + if (node) { + jQuery(node).css("width", _value); + } + }; + et2_DOMWidget.prototype.set_height = function (_value) { + this.height = _value; + var node = this.getDOMNode(this); + if (node) { + jQuery(node).css("height", _value); + } + }; + et2_DOMWidget.prototype.set_class = function (_value) { + var node = this.getDOMNode(this); + if (node) { + if (this["class"]) { + jQuery(node).removeClass(this["class"]); + } + jQuery(node).addClass(_value); + } + this["class"] = _value; + }; + et2_DOMWidget.prototype.set_overflow = function (_value) { + this.overflow = _value; + var node = this.getDOMNode(this); + if (node) { + jQuery(node).css("overflow", _value); + } + }; + et2_DOMWidget.prototype.set_data = function (_value) { + var node = this.getDOMNode(this); + if (node && _value) { + var pairs = _value.split(/,/g); + for (var i = 0; i < pairs.length; ++i) { + var name_value = pairs[i].split(':'); + jQuery(node).attr('data-' + name_value[0], name_value[1]); + } + } + }; + et2_DOMWidget.prototype.set_background = function (_value) { + var node = this.getDOMNode(this); + var values = ''; + if (_value && node) { + values = _value.split(','); + jQuery(node).css({ + "background-image": 'url("' + values[0] + '")', + "background-position-x": values[1], + "background-position-y": values[2], + "background-scale": values[3] + }); + } + }; + /** + * Set Actions on the widget + * + * Each action is defined as an object: + * + * move: { + * type: "drop", + * acceptedTypes: "mail", + * icon: "move", + * caption: "Move to" + * onExecute: javascript:mail_move" + * } + * + * This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped, + * the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the + * dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The + * etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler + * can operate in the widget context easily. The location varies depending on your action though. It + * might be action.parent.parent.data.widget + * + * To customise how the actions are handled for a particular widget, override _link_actions(). It handles + * the more widget-specific parts. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + * @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method + */ + et2_DOMWidget.prototype.set_actions = function (actions) { + if (this.id == "" || typeof this.id == "undefined") { + this.egw().debug("warn", "Widget should have an ID if you want actions", this); + return; + } + // Initialize the action manager and add some actions to it + // Only look 1 level deep + var gam = egw_getActionManager(this.egw().appName, true, 1); + if (typeof this._actionManager != "object") { + if (gam.getActionById(this.getInstanceManager().uniqueId, 1) !== null) { + gam = gam.getActionById(this.getInstanceManager().uniqueId, 1); + } + if (gam.getActionById(this.id, 1) != null) { + this._actionManager = gam.getActionById(this.id, 1); + } + else { + this._actionManager = gam.addAction("actionManager", this.id); + } + } + this._actionManager.updateActions(actions, this.egw().appName); + if (this.options.default_execute) + this._actionManager.setDefaultExecute(this.options.default_execute); + // Put a reference to the widget into the action stuff, so we can + // easily get back to widget context from the action handler + this._actionManager.data = { widget: this }; + // Link the actions to the DOM + this._link_actions(actions); + }; + et2_DOMWidget.prototype.set_default_execute = function (_default_execute) { + this.options.default_execute = _default_execute; + if (this._actionManager) + this._actionManager.setDefaultExecute(null, _default_execute); + }; + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * This can be overwritten to not allow all actions, by not returning them here. + * + * @param actions + * @returns {Array} + */ + et2_DOMWidget.prototype._get_action_links = function (actions) { + var action_links = []; + for (var i in actions) { + var action = actions[i]; + action_links.push(typeof action.id != 'undefined' ? action.id : i); + } + return action_links; + }; + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + et2_DOMWidget.prototype._link_actions = function (actions) { + // Get the top level element for the tree + var objectManager = egw_getAppObjectManager(true); + var widget_object = objectManager.getObjectById(this.id); + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = objectManager.insertObject(false, new egwActionObject(this.id, objectManager, new et2_action_object_impl(this), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); + } + else { + widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); + } + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + widget_object.updateActionLinks(action_links); + }; + et2_DOMWidget._attributes = { + "disabled": { + "name": "Disabled", + "type": "boolean", + "description": "Defines whether this widget is visible. Not to be confused with an input widget's HTML attribute 'disabled'.", + "default": false + }, + "width": { + "name": "Width", + "type": "dimension", + "default": et2_no_init, + "description": "Width of the element in pixels, percentage or 'auto'" + }, + "height": { + "name": "Height", + "type": "dimension", + "default": et2_no_init, + "description": "Height of the element in pixels, percentage or 'auto'" + }, + "class": { + "name": "CSS Class", + "type": "string", + "default": et2_no_init, + "description": "CSS Class which is applied to the dom element of this node" + }, + "overflow": { + "name": "Overflow", + "type": "string", + "default": et2_no_init, + "description": "If set, the css-overflow attribute is set to that value" + }, + "parent_node": { + "name": "DOM parent", + "type": "string", + "default": et2_no_init, + "description": "Insert into the target DOM node instead of the normal location" + }, + "actions": { + "name": "Actions list", + "type": "any", + "default": et2_no_init, + "description": "List of egw actions that can be done on the widget. This includes context menu, drag and drop. TODO: Link to action documentation" + }, + default_execute: { + name: "Default onExecute for actions", + type: "js", + default: et2_no_init, + description: "Set default onExecute javascript method for action not specifying their own" + }, + resize_ratio: { + name: "Resize height of the widget on callback resize", + type: "string", + default: '', + description: "Allow Resize height of the widget based on exess height and given ratio" + }, + data: { + name: "comma-separated name:value pairs set as data attributes on DOM node", + type: "string", + default: '', + description: 'data="mime:${row}[mime]" would generate data-mime="..." in DOM, eg. to use it in CSS on a parent' + }, + background: { + name: "Add background image", + type: "string", + default: '', + description: "Sets background image, left, right and scale on DOM", + } + }; + return et2_DOMWidget; +}(et2_core_widget_1.et2_widget)); /** * The surroundings manager class allows to append or prepend elements around * an widget node. * * @augments Class */ -var et2_surroundingsMgr = (function(){ "use strict"; return ClassWithAttributes.extend( -{ - /** - * Constructor - * - * @memberOf et2_surroundingsMgr - * @param _widget - */ - init: function(_widget) { - this.widget = _widget; - - this._widgetContainer = null; - this._widgetSurroundings = []; - this._widgetPlaceholder = null; - this._widgetNode = null; - this._ownPlaceholder = true; - }, - - destroy: function() { - this._widgetContainer = null; - this._widgetSurroundings = null; - this._widgetPlaceholder = null; - this._widgetNode = null; - }, - - prependDOMNode: function(_node) { - this._widgetSurroundings.unshift(_node); - this._surroundingsUpdated = true; - }, - - appendDOMNode: function(_node) { - // Append an placeholder first if none is existing yet - if (this._ownPlaceholder && this._widgetPlaceholder == null) - { - this._widgetPlaceholder = document.createElement("span"); - this._widgetSurroundings.push(this._widgetPlaceholder); - } - - // Append the given node - this._widgetSurroundings.push(_node); - this._surroundingsUpdated = true; - }, - - insertDOMNode: function(_node) { - if (!this._ownPlaceholder || this._widgetPlaceholder == null) - { - this.appendDOMNode(_node); - return; - } - - // Get the index of the widget placeholder and delete it, insert the - // given node instead - var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); - this._widgetSurroundings.splice(idx, 1, _node); - - // Delete the reference to the own placeholder - this._widgetPlaceholder = null; - this._ownPlaceholder = false; - }, - - removeDOMNode: function(_node) { - for (var i = 0; this._widgetSurroundings && i < this._widgetSurroundings.length; i++) - { - if (this._widgetSurroundings[i] == _node) - { - this._widgetSurroundings.splice(i, 1); - this._surroundingsUpdated = true; - break; - } - } - }, - - setWidgetPlaceholder: function(_node) { - if (_node != this._widgetPlaceholder) - { - if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null) - { - // Delete the current placeholder which was created by the - // widget itself - var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); - this._widgetSurroundings.splice(idx, 1); - - // Delete any reference to the own placeholder and set the - // _ownPlaceholder flag to false - this._widgetPlaceholder = null; - this._ownPlaceholder = false; - } - - this._ownPlaceholder = (_node == null); - this._widgetPlaceholder = _node; - this._surroundingsUpdated = true; - } - }, - - _rebuildContainer: function() { - // Return if there has been no change in the "surroundings-data" - if (!this._surroundingsUpdated) - { - return false; - } - - // Build the widget container - if (this._widgetSurroundings.length > 0) - { - // Check whether the widgetPlaceholder is really inside the DOM-Tree - var hasPlaceholder = et2_hasChild(this._widgetSurroundings, - this._widgetPlaceholder); - - // If not, append another widget placeholder - if (!hasPlaceholder) - { - this._widgetPlaceholder = document.createElement("span"); - this._widgetSurroundings.push(this._widgetPlaceholder); - - this._ownPlaceholder = true; - } - - // If the surroundings array only contains one element, set this one - // as the widget container - if (this._widgetSurroundings.length == 1) - { - if (this._widgetSurroundings[0] == this._widgetPlaceholder) - { - this._widgetContainer = null; - } - else - { - this._widgetContainer = this._widgetSurroundings[0]; - } - } - else - { - // Create an outer "span" as widgetContainer - this._widgetContainer = document.createElement("span"); - - // Append the children inside the widgetSurroundings array to - // the widget container - for (var i = 0; i < this._widgetSurroundings.length; i++) - { - this._widgetContainer.appendChild(this._widgetSurroundings[i]); - } - } - } - else - { - this._widgetContainer = null; - this._widgetPlaceholder = null; - } - - this._surroundingsUpdated = false; - - return true; - }, - - update: function() { - if (this._surroundingsUpdated) - { - var attached = this.widget ? this.widget.isAttached() : false; - - // Reattach the widget - this will call the "getDOMNode" function - // and trigger the _rebuildContainer function. - if (attached && this.widget) - { - this.widget.detachFromDOM(); - this.widget.attachToDOM(); - } - } - }, - - getDOMNode: function(_widgetNode) { - // Update the whole widgetContainer if this is not the first time this - // function has been called but the widget node has changed. - if (this._widgetNode != null && this._widgetNode != _widgetNode) - { - this._surroundingsUpdated = true; - } - - // Copy a reference to the given node - this._widgetNode = _widgetNode; - - // Build the container if it didn't exist yet. - var updated = this._rebuildContainer(); - - // Return the widget node itself if there are no surroundings arround - // it - if (this._widgetContainer == null) - { - return _widgetNode; - } - - // Replace the widgetPlaceholder with the given widget node if the - // widgetContainer has been updated - if (updated) - { - this._widgetPlaceholder.parentNode.replaceChild(_widgetNode, - this._widgetPlaceholder); - if (!this._ownPlaceholder) - { - this._widgetPlaceholder = _widgetNode; - } - } - - // Return the widget container - return this._widgetContainer; - } - -});}).call(this); - +var et2_surroundingsMgr = /** @class */ (function (_super_1) { + __extends(et2_surroundingsMgr, _super_1); + function et2_surroundingsMgr() { + return _super_1 !== null && _super_1.apply(this, arguments) || this; + } + /** + * Constructor + * + * @memberOf et2_surroundingsMgr + * @param _widget + */ + et2_surroundingsMgr.prototype.init = function (_widget) { + this.widget = _widget; + this._widgetContainer = null; + this._widgetSurroundings = []; + this._widgetPlaceholder = null; + this._widgetNode = null; + this._ownPlaceholder = true; + }; + et2_surroundingsMgr.prototype.destroy = function () { + this._widgetContainer = null; + this._widgetSurroundings = null; + this._widgetPlaceholder = null; + this._widgetNode = null; + }; + et2_surroundingsMgr.prototype.prependDOMNode = function (_node) { + this._widgetSurroundings.unshift(_node); + this._surroundingsUpdated = true; + }; + et2_surroundingsMgr.prototype.appendDOMNode = function (_node) { + // Append an placeholder first if none is existing yet + if (this._ownPlaceholder && this._widgetPlaceholder == null) { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + } + // Append the given node + this._widgetSurroundings.push(_node); + this._surroundingsUpdated = true; + }; + et2_surroundingsMgr.prototype.insertDOMNode = function (_node) { + if (!this._ownPlaceholder || this._widgetPlaceholder == null) { + this.appendDOMNode(_node); + return; + } + // Get the index of the widget placeholder and delete it, insert the + // given node instead + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1, _node); + // Delete the reference to the own placeholder + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + }; + et2_surroundingsMgr.prototype.removeDOMNode = function (_node) { + for (var i = 0; this._widgetSurroundings && i < this._widgetSurroundings.length; i++) { + if (this._widgetSurroundings[i] == _node) { + this._widgetSurroundings.splice(i, 1); + this._surroundingsUpdated = true; + break; + } + } + }; + et2_surroundingsMgr.prototype.setWidgetPlaceholder = function (_node) { + if (_node != this._widgetPlaceholder) { + if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null) { + // Delete the current placeholder which was created by the + // widget itself + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1); + // Delete any reference to the own placeholder and set the + // _ownPlaceholder flag to false + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + } + this._ownPlaceholder = (_node == null); + this._widgetPlaceholder = _node; + this._surroundingsUpdated = true; + } + }; + et2_surroundingsMgr.prototype._rebuildContainer = function () { + // Return if there has been no change in the "surroundings-data" + if (!this._surroundingsUpdated) { + return false; + } + // Build the widget container + if (this._widgetSurroundings.length > 0) { + // Check whether the widgetPlaceholder is really inside the DOM-Tree + var hasPlaceholder = et2_hasChild(this._widgetSurroundings, this._widgetPlaceholder); + // If not, append another widget placeholder + if (!hasPlaceholder) { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + this._ownPlaceholder = true; + } + // If the surroundings array only contains one element, set this one + // as the widget container + if (this._widgetSurroundings.length == 1) { + if (this._widgetSurroundings[0] == this._widgetPlaceholder) { + this._widgetContainer = null; + } + else { + this._widgetContainer = this._widgetSurroundings[0]; + } + } + else { + // Create an outer "span" as widgetContainer + this._widgetContainer = document.createElement("span"); + // Append the children inside the widgetSurroundings array to + // the widget container + for (var i = 0; i < this._widgetSurroundings.length; i++) { + this._widgetContainer.appendChild(this._widgetSurroundings[i]); + } + } + } + else { + this._widgetContainer = null; + this._widgetPlaceholder = null; + } + this._surroundingsUpdated = false; + return true; + }; + et2_surroundingsMgr.prototype.update = function () { + if (this._surroundingsUpdated) { + var attached = this.widget ? this.widget.isAttached() : false; + // Reattach the widget - this will call the "getDOMNode" function + // and trigger the _rebuildContainer function. + if (attached && this.widget) { + this.widget.detachFromDOM(); + this.widget.attachToDOM(); + } + } + }; + et2_surroundingsMgr.prototype.getDOMNode = function (_widgetNode) { + // Update the whole widgetContainer if this is not the first time this + // function has been called but the widget node has changed. + if (this._widgetNode != null && this._widgetNode != _widgetNode) { + this._surroundingsUpdated = true; + } + // Copy a reference to the given node + this._widgetNode = _widgetNode; + // Build the container if it didn't exist yet. + var updated = this._rebuildContainer(); + // Return the widget node itself if there are no surroundings arround + // it + if (this._widgetContainer == null) { + return _widgetNode; + } + // Replace the widgetPlaceholder with the given widget node if the + // widgetContainer has been updated + if (updated) { + this._widgetPlaceholder.parentNode.replaceChild(_widgetNode, this._widgetPlaceholder); + if (!this._ownPlaceholder) { + this._widgetPlaceholder = _widgetNode; + } + } + // Return the widget container + return this._widgetContainer; + }; + return et2_surroundingsMgr; +}(et2_core_inheritance_1.ClassWithAttributes)); /** * The egw_action system requires an egwActionObjectInterface Interface implementation * to tie actions to DOM nodes. This one can be used by any widget. @@ -847,40 +703,33 @@ var et2_surroundingsMgr = (function(){ "use strict"; return ClassWithAttributes. * @param {Object} node * */ -function et2_action_object_impl(widget, node) -{ - var aoi = new egwActionObjectInterface(); - var objectNode = node; - - aoi.getWidget = function() { - return widget; - }; - - aoi.doGetDOMNode = function() { - return objectNode?objectNode:widget.getDOMNode(); - }; - -// _outerCall may be used to determine, whether the state change has been -// evoked from the outside and the stateChangeCallback has to be called -// or not. - aoi.doSetState = function(_state, _outerCall) { - }; - -// The doTiggerEvent function may be overritten by the aoi if it wants to -// support certain action implementation specific events like EGW_AI_DRAG_OVER -// or EGW_AI_DRAG_OUT - aoi.doTriggerEvent = function(_event, _data) { - switch(_event) - { - case EGW_AI_DRAG_OVER: - jQuery(this.node).addClass("ui-state-active"); - break; - case EGW_AI_DRAG_OUT: - jQuery(this.node).removeClass("ui-state-active"); - break; - } - }; - - - return aoi; -}; +function et2_action_object_impl(widget, node) { + var aoi = new egwActionObjectInterface(); + var objectNode = node; + aoi.getWidget = function () { + return widget; + }; + aoi.doGetDOMNode = function () { + return objectNode ? objectNode : widget.getDOMNode(); + }; + // _outerCall may be used to determine, whether the state change has been + // evoked from the outside and the stateChangeCallback has to be called + // or not. + aoi.doSetState = function (_state, _outerCall) { + }; + // The doTiggerEvent function may be overritten by the aoi if it wants to + // support certain action implementation specific events like EGW_AI_DRAG_OVER + // or EGW_AI_DRAG_OUT + aoi.doTriggerEvent = function (_event, _data) { + switch (_event) { + case EGW_AI_DRAG_OVER: + jQuery(this.node).addClass("ui-state-active"); + break; + case EGW_AI_DRAG_OUT: + jQuery(this.node).removeClass("ui-state-active"); + break; + } + }; + return aoi; +} +; diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts new file mode 100644 index 0000000000..2197a2feff --- /dev/null +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -0,0 +1,899 @@ +/** + * EGroupware eTemplate2 - JS DOM Widget class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/*egw:uses + et2_core_interfaces; + et2_core_widget; + /api/js/egw_action/egw_action.js; +*/ + +import { ClassWithAttributes } from './et2_core_inheritance'; +import './et2_core_interfaces'; +import './et2_core_common'; +import {et2_widget, et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import '../egw_action/egw_action.js'; + +/** + * Abstract widget class which can be inserted into the DOM. All widget classes + * deriving from this class have to care about implementing the "getDOMNode" + * function which has to return the DOM-Node. + * + * @augments et2_widget + */ +class et2_DOMWidget extends et2_widget implements et2_IDOMNode +{ + static readonly _attributes : any = { + "disabled": { + "name": "Disabled", + "type": "boolean", + "description": "Defines whether this widget is visible. Not to be confused with an input widget's HTML attribute 'disabled'.", + "default": false + }, + "width": { + "name": "Width", + "type": "dimension", + "default": et2_no_init, + "description": "Width of the element in pixels, percentage or 'auto'" + }, + "height": { + "name": "Height", + "type": "dimension", + "default": et2_no_init, + "description": "Height of the element in pixels, percentage or 'auto'" + }, + "class": { + "name": "CSS Class", + "type": "string", + "default": et2_no_init, + "description": "CSS Class which is applied to the dom element of this node" + }, + "overflow": { + "name": "Overflow", + "type": "string", + "default": et2_no_init, + "description": "If set, the css-overflow attribute is set to that value" + }, + "parent_node": { + "name": "DOM parent", + "type": "string", + "default": et2_no_init, + "description": "Insert into the target DOM node instead of the normal location" + }, + "actions": { + "name": "Actions list", + "type": "any", + "default": et2_no_init, + "description": "List of egw actions that can be done on the widget. This includes context menu, drag and drop. TODO: Link to action documentation" + }, + default_execute: { + name: "Default onExecute for actions", + type: "js", + default: et2_no_init, + description: "Set default onExecute javascript method for action not specifying their own" + }, + resize_ratio: { + name: "Resize height of the widget on callback resize", + type:"string", + default: '', + description: "Allow Resize height of the widget based on exess height and given ratio" + }, + data: { + name: "comma-separated name:value pairs set as data attributes on DOM node", + type: "string", + default: '', + description: 'data="mime:${row}[mime]" would generate data-mime="..." in DOM, eg. to use it in CSS on a parent' + }, + background: { + name: "Add background image", + type: "string", + default:'', + description: "Sets background image, left, right and scale on DOM", + } + } + + parentNode : HTMLElement; + disabled : boolean; + private _attachSet: object; + private _actionManager: any; + + /** + * When the DOMWidget is initialized, it grabs the DOM-Node of the parent + * object (if available) and passes it to its own "createDOMNode" function + * + * @memberOf et2_DOMWidget + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + this.parentNode = null; + + this._attachSet = { + "node": null, + "parent": null + }; + + this.disabled = false; + this._surroundingsMgr = null; + } + + /** + * Detatches the node from the DOM and clears all references to the parent + * node or the dom node of this widget. + */ + destroy() + { + this.detachFromDOM(); + this.parentNode = null; + this._attachSet = {}; + + if(this._actionManager) + { + var app_om = egw_getObjectManager(this.egw().getAppName(), false,1); + if(app_om) + { + var om = app_om.getObjectById(this.id); + if(om) om.remove(); + } + this._actionManager.remove(); + this._actionManager = null; + } + + if (this._surroundingsMgr) + { + this._surroundingsMgr.free(); + this._surroundingsMgr = null; + } + + this._super(); + } + + /** + * Attaches the container node of this widget to the DOM-Tree + */ + doLoadingFinished() + { + // Check whether the parent implements the et2_IDOMNode interface. If + // yes, grab the DOM node and create our own. + if (this.getParent() && this.getParent().implements(et2_IDOMNode)) { + if(this.options.parent_node) + { + this.set_parent_node(this.options.parent_node); + } + else + { + this.setParentDOMNode(this.getParent().getDOMNode(this)); + } + } + + return true; + } + + /** + * Detaches the widget from the DOM tree, if it had been attached to the + * DOM-Tree using the attachToDOM method. + */ + detachFromDOM() { + + if (this._attachSet.node && this._attachSet.parent) + { + // Remove the current node from the parent node + try { + this._attachSet.parent.removeChild(this._attachSet.node); + } catch (e) { + // Don't throw a DOM error if the node wasn't in the parent + } + + // Reset the "attachSet" + this._attachSet = { + "node": null, + "parent": null + }; + + return true; + } + + return false; + } + + /** + * Attaches the widget to the DOM tree. Fails if the widget is already + * attached to the tree or no parent node or no node for this widget is + * defined. + */ + attachToDOM() { + // Attach the DOM node of this widget (if existing) to the new parent + var node = this.getDOMNode(this); + if (node && this.parentNode && + (node != this._attachSet.node || + this.parentNode != this._attachSet.parent)) + { + // If the surroundings manager exists, surround the DOM-Node of this + // widget with the DOM-Nodes inside the surroundings manager. + if (this._surroundingsMgr) + { + node = this._surroundingsMgr.getDOMNode(node); + } + + // Append this node at its index + var idx = this.getDOMIndex(); + if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) + { + this.parentNode.appendChild(node); + } + else + { + this.parentNode.insertBefore(node, this.parentNode.childNodes[idx]); + } + + // Store the currently attached nodes + this._attachSet = { + "node": node, + "parent": this.parentNode + }; + + return true; + } + + return false; + } + + isAttached() { + return this.parentNode != null; + } + + private _surroundingsMgr : et2_surroundingsMgr; + + getSurroundings() { + if (!this._surroundingsMgr) + { + this._surroundingsMgr = new et2_surroundingsMgr(this); + } + + return this._surroundingsMgr; + } + + /** + * Get data for the tab this widget is on. + * + * Will return null if the widget is not on a tab or tab data containing + * - id + * - label + * - widget (top level widget) + * - contentDiv (jQuery object for the div the tab content is in) + * + * @returns {Object|null} Data for tab the widget is on + */ + get_tab_info() { + var parent = this; + do { + parent = parent._parent; + } while (parent !== this.getRoot() && parent._type !== 'tabbox'); + + // No tab + if(parent === this.getRoot()) + { + return null; + } + + // Find the tab index + for(var i = 0; i < parent.tabData.length; i++) + { + // Find the tab by DOM heritage + if(parent.tabData[i].contentDiv.has(this.div).length) + { + return parent.tabData[i]; + } + } + // On a tab, but we couldn't find it by DOM nodes Maybe tab template is + // not loaded yet. Try checking IDs. + var template = this; + do { + template = template._parent; + } while (template !== parent && template._type !== 'template'); + for(var i = parent.tabData.length - 1; i >= 0; i--) + { + if(template && template.id && template.id === parent.tabData[i].id) + { + return parent.tabData[i]; + } + } + // Fallback + return this.getParent().get_tab_info(); + } + + /** + * Set the parent DOM node of this element. Takes a wider variety of types + * than setParentDOMNode(), and matches the set_ naming convention. + * + * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. + */ + set_parent_node(_node) { + if(typeof _node == "string") + { + var parent = jQuery('#'+_node); + if(parent.length === 0 && window.parent) + { + // Could not find it, try again with wider context + // (in case there's an iframe in admin, for example) + parent = jQuery('#'+_node, window.parent.document); + } + if(parent.length === 0) + { + this.egw().debug('warn','Unable to find DOM parent node with ID "%s" for widget %o.',_node,this); + } + else + { + this.setParentDOMNode(parent.get(0)); + } + } + else + { + this.setParentDOMNode(_node); + } + } + + /** + * Set the parent DOM node of this element. If another parent node is already + * set, this widget removes itself from the DOM tree + * + * @param _node + */ + setParentDOMNode(_node) { + if (_node != this.parentNode) + { + // Detatch this element from the DOM tree + this.detachFromDOM(); + + this.parentNode = _node; + + // And attatch the element to the DOM tree + this.attachToDOM(); + } + } + + /** + * Returns the parent node. + */ + getParentDOMNode() { + return this.parentNode; + } + + /** + * Returns the index of this element in the DOM tree + */ + getDOMIndex() { + if (this.getParent()) + { + var idx = 0; + var children = this.getParent().getChildren(); + + if(children && children.indexOf) return children.indexOf(this); + + egw.debug('warn', 'No Array.indexOf(), falling back to looping. '); + for (var i = 0; i < children.length; i++) + { + if (children[i] == this) + { + return idx; + } + else if (children[i].isInTree()) + { + idx++; + } + } + } + + return -1; + } + + /** + * Sets the id of the DOM-Node. + * + * DOM id's have dots "." replaced with dashes "-" + * + * @param {string} _value id to set + */ + set_id(_value) { + + this.id = _value; + this.dom_id = _value ? this.getInstanceManager().uniqueId+'_'+_value.replace(/\./g, '-') : _value; + + var node = this.getDOMNode(this); + if (node) + { + if (_value != "") + { + node.setAttribute("id", this.dom_id); + } + else + { + node.removeAttribute("id"); + } + } + } + + set_disabled(_value) { + var node = this._surroundingsMgr != null ? this._surroundingsMgr.getDOMNode(this.getDOMNode(this)) : this.getDOMNode(this); + if (node && this.disabled != _value) + { + this.disabled = _value; + + if (_value) + { + jQuery(node).hide(); + } + else + { + jQuery(node).show(); + } + } + } + + set_width(_value) { + this.width = _value; + + var node = this.getDOMNode(this); + if (node) + { + jQuery(node).css("width", _value); + } + } + + set_height(_value) { + this.height = _value; + + var node = this.getDOMNode(this); + if (node) + { + jQuery(node).css("height", _value); + } + } + + set_class(_value) { + var node = this.getDOMNode(this); + if (node) + { + if (this["class"]) + { + jQuery(node).removeClass(this["class"]); + } + jQuery(node).addClass(_value); + } + + this["class"] = _value; + } + + set_overflow(_value) { + this.overflow = _value; + + var node = this.getDOMNode(this); + if (node) + { + jQuery(node).css("overflow", _value); + } + } + + set_data(_value) + { + var node = this.getDOMNode(this); + if (node && _value) + { + var pairs = _value.split(/,/g); + for(var i=0; i < pairs.length; ++i) + { + var name_value = pairs[i].split(':'); + jQuery(node).attr('data-'+name_value[0], name_value[1]); + } + } + } + + set_background(_value) + { + var node = this.getDOMNode(this); + var values = ''; + if (_value && node) + { + values = _value.split(','); + jQuery(node).css({ + "background-image":'url("'+values[0]+'")', + "background-position-x":values[1], + "background-position-y":values[2], + "background-scale":values[3] + }); + } + } + + /** + * Set Actions on the widget + * + * Each action is defined as an object: + * + * move: { + * type: "drop", + * acceptedTypes: "mail", + * icon: "move", + * caption: "Move to" + * onExecute: javascript:mail_move" + * } + * + * This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped, + * the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the + * dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The + * etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler + * can operate in the widget context easily. The location varies depending on your action though. It + * might be action.parent.parent.data.widget + * + * To customise how the actions are handled for a particular widget, override _link_actions(). It handles + * the more widget-specific parts. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + * @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method + */ + set_actions(actions) + { + if(this.id == "" || typeof this.id == "undefined") + { + this.egw().debug("warn", "Widget should have an ID if you want actions",this); + return; + } + + // Initialize the action manager and add some actions to it + // Only look 1 level deep + var gam = egw_getActionManager(this.egw().appName,true,1); + if(typeof this._actionManager != "object") + { + if(gam.getActionById(this.getInstanceManager().uniqueId,1) !== null) + { + gam = gam.getActionById(this.getInstanceManager().uniqueId,1); + } + if(gam.getActionById(this.id,1) != null) + { + this._actionManager = gam.getActionById(this.id,1); + } + else + { + this._actionManager = gam.addAction("actionManager", this.id); + } + } + this._actionManager.updateActions(actions, this.egw().appName); + if (this.options.default_execute) this._actionManager.setDefaultExecute(this.options.default_execute); + + // Put a reference to the widget into the action stuff, so we can + // easily get back to widget context from the action handler + this._actionManager.data = {widget: this}; + + // Link the actions to the DOM + this._link_actions(actions); + } + + set_default_execute(_default_execute) + { + this.options.default_execute = _default_execute; + + if (this._actionManager) this._actionManager.setDefaultExecute(null, _default_execute); + } + + /** + * Get all action-links / id's of 1.-level actions from a given action object + * + * This can be overwritten to not allow all actions, by not returning them here. + * + * @param actions + * @returns {Array} + */ + _get_action_links(actions) + { + var action_links = []; + for(var i in actions) + { + var action = actions[i]; + action_links.push(typeof action.id != 'undefined' ? action.id : i); + } + return action_links; + } + + /** + * Link the actions to the DOM nodes / widget bits. + * + * @param {object} actions {ID: {attributes..}+} map of egw action information + */ + _link_actions(actions) + { + // Get the top level element for the tree + var objectManager = egw_getAppObjectManager(true); + var widget_object = objectManager.getObjectById(this.id); + if (widget_object == null) { + // Add a new container to the object manager which will hold the widget + // objects + widget_object = objectManager.insertObject(false, new egwActionObject( + this.id, objectManager, new et2_action_object_impl(this), + this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager + )); + } + else + { + widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); + } + + // Delete all old objects + widget_object.clear(); + widget_object.unregisterActions(); + + // Go over the widget & add links - this is where we decide which actions are + // 'allowed' for this widget at this time + var action_links = this._get_action_links(actions); + widget_object.updateActionLinks(action_links); + } + +} + +/** + * The surroundings manager class allows to append or prepend elements around + * an widget node. + * + * @augments Class + */ +class et2_surroundingsMgr extends ClassWithAttributes +{ + /** + * Constructor + * + * @memberOf et2_surroundingsMgr + * @param _widget + */ + init(_widget) { + this.widget = _widget; + + this._widgetContainer = null; + this._widgetSurroundings = []; + this._widgetPlaceholder = null; + this._widgetNode = null; + this._ownPlaceholder = true; + } + + destroy() { + this._widgetContainer = null; + this._widgetSurroundings = null; + this._widgetPlaceholder = null; + this._widgetNode = null; + } + + prependDOMNode(_node) { + this._widgetSurroundings.unshift(_node); + this._surroundingsUpdated = true; + } + + appendDOMNode(_node) { + // Append an placeholder first if none is existing yet + if (this._ownPlaceholder && this._widgetPlaceholder == null) + { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + } + + // Append the given node + this._widgetSurroundings.push(_node); + this._surroundingsUpdated = true; + } + + insertDOMNode(_node) { + if (!this._ownPlaceholder || this._widgetPlaceholder == null) + { + this.appendDOMNode(_node); + return; + } + + // Get the index of the widget placeholder and delete it, insert the + // given node instead + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1, _node); + + // Delete the reference to the own placeholder + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + } + + removeDOMNode(_node) { + for (var i = 0; this._widgetSurroundings && i < this._widgetSurroundings.length; i++) + { + if (this._widgetSurroundings[i] == _node) + { + this._widgetSurroundings.splice(i, 1); + this._surroundingsUpdated = true; + break; + } + } + } + + setWidgetPlaceholder(_node) { + if (_node != this._widgetPlaceholder) + { + if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null) + { + // Delete the current placeholder which was created by the + // widget itself + var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); + this._widgetSurroundings.splice(idx, 1); + + // Delete any reference to the own placeholder and set the + // _ownPlaceholder flag to false + this._widgetPlaceholder = null; + this._ownPlaceholder = false; + } + + this._ownPlaceholder = (_node == null); + this._widgetPlaceholder = _node; + this._surroundingsUpdated = true; + } + } + + _rebuildContainer() { + // Return if there has been no change in the "surroundings-data" + if (!this._surroundingsUpdated) + { + return false; + } + + // Build the widget container + if (this._widgetSurroundings.length > 0) + { + // Check whether the widgetPlaceholder is really inside the DOM-Tree + var hasPlaceholder = et2_hasChild(this._widgetSurroundings, + this._widgetPlaceholder); + + // If not, append another widget placeholder + if (!hasPlaceholder) + { + this._widgetPlaceholder = document.createElement("span"); + this._widgetSurroundings.push(this._widgetPlaceholder); + + this._ownPlaceholder = true; + } + + // If the surroundings array only contains one element, set this one + // as the widget container + if (this._widgetSurroundings.length == 1) + { + if (this._widgetSurroundings[0] == this._widgetPlaceholder) + { + this._widgetContainer = null; + } + else + { + this._widgetContainer = this._widgetSurroundings[0]; + } + } + else + { + // Create an outer "span" as widgetContainer + this._widgetContainer = document.createElement("span"); + + // Append the children inside the widgetSurroundings array to + // the widget container + for (var i = 0; i < this._widgetSurroundings.length; i++) + { + this._widgetContainer.appendChild(this._widgetSurroundings[i]); + } + } + } + else + { + this._widgetContainer = null; + this._widgetPlaceholder = null; + } + + this._surroundingsUpdated = false; + + return true; + } + + update() { + if (this._surroundingsUpdated) + { + var attached = this.widget ? this.widget.isAttached() : false; + + // Reattach the widget - this will call the "getDOMNode" function + // and trigger the _rebuildContainer function. + if (attached && this.widget) + { + this.widget.detachFromDOM(); + this.widget.attachToDOM(); + } + } + } + + getDOMNode(_widgetNode) { + // Update the whole widgetContainer if this is not the first time this + // function has been called but the widget node has changed. + if (this._widgetNode != null && this._widgetNode != _widgetNode) + { + this._surroundingsUpdated = true; + } + + // Copy a reference to the given node + this._widgetNode = _widgetNode; + + // Build the container if it didn't exist yet. + var updated = this._rebuildContainer(); + + // Return the widget node itself if there are no surroundings arround + // it + if (this._widgetContainer == null) + { + return _widgetNode; + } + + // Replace the widgetPlaceholder with the given widget node if the + // widgetContainer has been updated + if (updated) + { + this._widgetPlaceholder.parentNode.replaceChild(_widgetNode, + this._widgetPlaceholder); + if (!this._ownPlaceholder) + { + this._widgetPlaceholder = _widgetNode; + } + } + + // Return the widget container + return this._widgetContainer; + } + +} + +/** + * The egw_action system requires an egwActionObjectInterface Interface implementation + * to tie actions to DOM nodes. This one can be used by any widget. + * + * The class extension is different than the widgets + * + * @param {et2_DOMWidget} widget + * @param {Object} node + * + */ +function et2_action_object_impl(widget, node) +{ + var aoi = new egwActionObjectInterface(); + var objectNode = node; + + aoi.getWidget = function() { + return widget; + }; + + aoi.doGetDOMNode = function() { + return objectNode?objectNode:widget.getDOMNode(); + }; + +// _outerCall may be used to determine, whether the state change has been +// evoked from the outside and the stateChangeCallback has to be called +// or not. + aoi.doSetState = function(_state, _outerCall) { + }; + +// The doTiggerEvent function may be overritten by the aoi if it wants to +// support certain action implementation specific events like EGW_AI_DRAG_OVER +// or EGW_AI_DRAG_OUT + aoi.doTriggerEvent = function(_event, _data) { + switch(_event) + { + case EGW_AI_DRAG_OVER: + jQuery(this.node).addClass("ui-state-active"); + break; + case EGW_AI_DRAG_OUT: + jQuery(this.node).removeClass("ui-state-active"); + break; + } + }; + + + return aoi; +}; diff --git a/api/js/etemplate/et2_core_common.js b/api/js/etemplate/et2_core_common.js index 878b913ada..cf751451fb 100644 --- a/api/js/etemplate/et2_core_common.js +++ b/api/js/etemplate/et2_core_common.js @@ -7,79 +7,64 @@ * @link http://www.egroupware.org * @author Andreas Stöckel * @copyright Stylite 2011 - * @version $Id$ */ - /** * IE Fix for array.indexOf */ -if (typeof Array.prototype.indexOf == "undefined") -{ - Array.prototype.indexOf = function(_elem) { - for (var i = 0; i < this.length; i++) - { - if (this[i] === _elem) - return i; - } - return -1; - }; +if (typeof Array.prototype.indexOf == "undefined") { + Array.prototype.indexOf = function (_elem) { + for (var i = 0; i < this.length; i++) { + if (this[i] === _elem) + return i; + } + return -1; + }; } - /** * Array with all types supported by the et2_checkType function. */ var et2_validTypes = ["boolean", "string", "rawstring", "html", "float", "integer", "any", "js", "dimension"]; - /** * Object whith default values for the above types. Do not specify array or * objects inside the et2_typeDefaults object, as this instance will be shared * between all users of it. */ var et2_typeDefaults = { - "boolean": false, - "string": "", - "rawstring": "", // no html-entity decoding - "html": "", - "js": null, - "float": 0.0, - "integer": 0, - "any": null, - "dimension": "auto" + "boolean": false, + "string": "", + "rawstring": "", + "html": "", + "js": null, + "float": 0.0, + "integer": 0, + "any": null, + "dimension": "auto" }; - -function et2_evalBool(_val) -{ - if (typeof _val == "string") - { - if (_val == "false" || _val == "0") - { - return false; - } - } - - return _val ? true : false; +function et2_evalBool(_val) { + if (typeof _val == "string") { + if (_val == "false" || _val == "0") { + return false; + } + } + return _val ? true : false; } - /** * Concat et2 name together, eg. et2_concat("namespace","test[something]") == "namespace[test][something]" * @param variable number of arguments to contact * @returns string */ -function et2_form_name(_cname,_name) -{ - var parts = []; - for(var i=0; i < arguments.length; ++i) - { - var name = arguments[i]; - if (typeof name == 'string' && name.length > 0) // et2_namespace("","test") === "test" === et2_namespace(null,"test") - { - parts = parts.concat(name.replace(/]/g,'').split('[')); - } - } - var name = parts.shift(); - return parts.length ? name + '['+parts.join('][')+']' : name; +function et2_form_name(_cname, _name) { + var parts = []; + for (var i = 0; i < arguments.length; ++i) { + var name = arguments[i]; + if (typeof name == 'string' && name.length > 0) // et2_namespace("","test") === "test" === et2_namespace(null,"test") + { + parts = parts.concat(name.replace(/]/g, '').split('[')); + } + } + var name = parts.shift(); + return parts.length ? name + '[' + parts.join('][') + ']' : name; } - /** * Checks whether the given value is of the given type. Strings are converted * into the corresponding type. The (converted) value is returned. All supported @@ -90,192 +75,138 @@ function et2_form_name(_cname,_name) * @param string _attr attribute name * @param object _widget */ -function et2_checkType(_val, _type, _attr, _widget) -{ - if (typeof _attr == "undefined") - { - _attr = null; - } - - function _err() { - var res = et2_typeDefaults[_type]; - - if(typeof _val != "undefined" && _val) - { - egw.debug("warn", "Widget %o: '" + _val + "' was not of specified _type '" + - _type + (_attr != null ? "' for attribute '" + _attr + "' " : "") + - "and is now '" + res + "'",_widget); - } - return res; - } - - // If the type is "any" simply return the value again - if (_type == "any") - { - return _val; - } - - // we dont check default-value any further, that also fixes type="js" does NOT accept null, - // which happens on expanded values - if (_val === et2_typeDefaults[_type]) - { - return _val; - } - - // If the type is boolean, check whether the given value is exactly true or - // false. Otherwise check whether the value is the string "true" or "false". - if (_type == "boolean") - { - if (_val === true || _val === false) - { - return _val; - } - - if (typeof _val == "string") - { - var lcv = _val.toLowerCase(); - if (lcv === "true" || lcv === "false" || lcv === "") - { - return _val === "true"; - } - if(lcv === "0" || lcv === "1") - { - return _val === "1"; - } - } - else if (typeof _val == "number") - { - return _val != 0; - } - - return _err(); - } - - // Check whether the given value is of the type "string" - if (_type == "string" || _type == "html" || _type == "rawstring") - { - if (typeof _val == "number") // as php is a bit vague here, silently convert to a string - { - return _val.toString(); - } - - if (typeof _val == "string") - { - return _type == "string" ? html_entity_decode(_val) : _val; - } - - // Handle some less common possibilities - // Maybe a split on an empty string - if(typeof _val == "object" && jQuery.isEmptyObject(_val)) return ""; - - return _err(); - } - - // Check whether the value is already a number, otherwise try to convert it - // to one. - if (_type == "float") - { - if (typeof _val == "number") - { - return _val; - } - - if (!isNaN(_val)) - { - return parseFloat(_val); - } - - return _err(); - } - - // Check whether the value is an integer by comparing the result of - // parseInt(_val) to the value itself. - if (_type == "integer") - { - if (parseInt(_val) == _val) - { - return parseInt(_val); - } - - return _err(); - } - - // Parse the given dimension value - if (_type == "dimension") - { - // Case 1: The value is "auto" - if (_val == "auto") - { - return _val; - } - - // Case 2: The value is simply a number, attach "px" - if (!isNaN(_val)) - { - return parseFloat(_val) + "px"; - } - - // Case 3: The value is already a valid css pixel value or a percentage - if (typeof _val == "string" && - ((_val.indexOf("px") == _val.length - 2 && !isNaN(_val.split("px")[0])) || - (_val.indexOf("%") == _val.length - 1 && !isNaN(_val.split("%")[0])))) - { - return _val; - } - - return _err(); - } - - // Javascript - if (_type == "js") - { - if (typeof _val == "function" || typeof _val == "undefined") - { - return _val; - } - if (_val) _val = _val.replace(/window\.close\(\)/g, 'egw(window).close()'); - - // Check to see if it's a string in app.appname.function format, and wrap it in - // a closure to make sure context is preserved - if(typeof _val == "string" && _val.substr(0,4) == "app." && app) - { - var parts = _val.split('.'); - var func = parts.pop(); - var parent = window; - for(var i=0; i < parts.length && typeof parent[parts[i]] != 'undefined'; ++i) - { - parent = parent[parts[i]]; - } - if (typeof parent[func] == 'function') - { - try - { - return jQuery.proxy(parent[func],parent); - } - catch (e) - { - req.egw.debug('error', 'Function', _val); - return _err(); - } - } - } - - if (!_val || typeof _val == "string") - { - return _val; // get compiled later in widgets own initAttributes, as widget is not yet initialised - } - } - - // We should never come here - throw("Invalid type identifier '" + _attr + "': '" + _type+"'"); +function et2_checkType(_val, _type, _attr, _widget) { + if (typeof _attr == "undefined") { + _attr = null; + } + function _err() { + var res = et2_typeDefaults[_type]; + if (typeof _val != "undefined" && _val) { + egw.debug("warn", "Widget %o: '" + _val + "' was not of specified _type '" + + _type + (_attr != null ? "' for attribute '" + _attr + "' " : "") + + "and is now '" + res + "'", _widget); + } + return res; + } + // If the type is "any" simply return the value again + if (_type == "any") { + return _val; + } + // we dont check default-value any further, that also fixes type="js" does NOT accept null, + // which happens on expanded values + if (_val === et2_typeDefaults[_type]) { + return _val; + } + // If the type is boolean, check whether the given value is exactly true or + // false. Otherwise check whether the value is the string "true" or "false". + if (_type == "boolean") { + if (_val === true || _val === false) { + return _val; + } + if (typeof _val == "string") { + var lcv = _val.toLowerCase(); + if (lcv === "true" || lcv === "false" || lcv === "") { + return _val === "true"; + } + if (lcv === "0" || lcv === "1") { + return _val === "1"; + } + } + else if (typeof _val == "number") { + return _val != 0; + } + return _err(); + } + // Check whether the given value is of the type "string" + if (_type == "string" || _type == "html" || _type == "rawstring") { + if (typeof _val == "number") // as php is a bit vague here, silently convert to a string + { + return _val.toString(); + } + if (typeof _val == "string") { + return _type == "string" ? html_entity_decode(_val) : _val; + } + // Handle some less common possibilities + // Maybe a split on an empty string + if (typeof _val == "object" && jQuery.isEmptyObject(_val)) + return ""; + return _err(); + } + // Check whether the value is already a number, otherwise try to convert it + // to one. + if (_type == "float") { + if (typeof _val == "number") { + return _val; + } + if (!isNaN(_val)) { + return parseFloat(_val); + } + return _err(); + } + // Check whether the value is an integer by comparing the result of + // parseInt(_val) to the value itself. + if (_type == "integer") { + if (parseInt(_val) == _val) { + return parseInt(_val); + } + return _err(); + } + // Parse the given dimension value + if (_type == "dimension") { + // Case 1: The value is "auto" + if (_val == "auto") { + return _val; + } + // Case 2: The value is simply a number, attach "px" + if (!isNaN(_val)) { + return parseFloat(_val) + "px"; + } + // Case 3: The value is already a valid css pixel value or a percentage + if (typeof _val == "string" && + ((_val.indexOf("px") == _val.length - 2 && !isNaN(_val.split("px")[0])) || + (_val.indexOf("%") == _val.length - 1 && !isNaN(_val.split("%")[0])))) { + return _val; + } + return _err(); + } + // Javascript + if (_type == "js") { + if (typeof _val == "function" || typeof _val == "undefined") { + return _val; + } + if (_val) + _val = _val.replace(/window\.close\(\)/g, 'egw(window).close()'); + // Check to see if it's a string in app.appname.function format, and wrap it in + // a closure to make sure context is preserved + if (typeof _val == "string" && _val.substr(0, 4) == "app." && app) { + var parts = _val.split('.'); + var func = parts.pop(); + var parent = window; + for (var i = 0; i < parts.length && typeof parent[parts[i]] != 'undefined'; ++i) { + parent = parent[parts[i]]; + } + if (typeof parent[func] == 'function') { + try { + return jQuery.proxy(parent[func], parent); + } + catch (e) { + egw.debug('error', 'Function', _val); + return _err(); + } + } + } + if (!_val || typeof _val == "string") { + return _val; // get compiled later in widgets own initAttributes, as widget is not yet initialised + } + } + // We should never come here + throw ("Invalid type identifier '" + _attr + "': '" + _type + "'"); } - /** * If et2_no_init is set as default value, the initAttributes function will not * try to initialize the attribute with the default value. */ var et2_no_init = new Object(); - /** * Validates the given attribute with the given id. The validation checks for * the existance of a human name, a description, a type and a default value. @@ -283,113 +214,81 @@ var et2_no_init = new Object(); * empty string, the type defaults to any and the default to the corresponding * type default. */ -function et2_validateAttrib(_id, _attrib) -{ - // Default ignore to false. - if (typeof _attrib["ignore"] == "undefined") - { - _attrib["ignore"] = false; - } - - // Break if "ignore" is set to true. - if (_attrib.ignore) - { - return; - } - - if (typeof _attrib["name"] == "undefined") - { - _attrib["name"] = _id; - egw.debug("log", "Human name ('name'-Field) for attribute '" + - _id + "' has not been supplied, set to '" + _id + "'"); - } - - if (typeof _attrib["description"] == "undefined") - { - _attrib["description"] = ""; - egw.debug("log", "Description for attribute '" + - _id + "' has not been supplied"); - } - - if (typeof _attrib["type"] == "undefined") - { - _attrib["type"] = "any"; - } - else - { - if (et2_validTypes.indexOf(_attrib["type"]) < 0) - { - egw.debug("error", "Invalid type '" + _attrib["type"] + "' for attribute '" + _id + - "' supplied. Valid types are ", et2_validTypes); - } - } - - // Set the defaults - if (typeof _attrib["default"] == "undefined") - { - _attrib["default"] = et2_typeDefaults[_attrib["type"]]; - } +function et2_validateAttrib(_id, _attrib) { + // Default ignore to false. + if (typeof _attrib["ignore"] == "undefined") { + _attrib["ignore"] = false; + } + // Break if "ignore" is set to true. + if (_attrib.ignore) { + return; + } + if (typeof _attrib["name"] == "undefined") { + _attrib["name"] = _id; + egw.debug("log", "Human name ('name'-Field) for attribute '" + + _id + "' has not been supplied, set to '" + _id + "'"); + } + if (typeof _attrib["description"] == "undefined") { + _attrib["description"] = ""; + egw.debug("log", "Description for attribute '" + + _id + "' has not been supplied"); + } + if (typeof _attrib["type"] == "undefined") { + _attrib["type"] = "any"; + } + else { + if (et2_validTypes.indexOf(_attrib["type"]) < 0) { + egw.debug("error", "Invalid type '" + _attrib["type"] + "' for attribute '" + _id + + "' supplied. Valid types are ", et2_validTypes); + } + } + // Set the defaults + if (typeof _attrib["default"] == "undefined") { + _attrib["default"] = et2_typeDefaults[_attrib["type"]]; + } } - /** * Equivalent to the PHP array_values function */ -function et2_arrayValues(_arr) -{ - var result = []; - for (var key in _arr) - { - if (parseInt(key) == key) - { - result.push(_arr[key]); - } - } - - return result; +function et2_arrayValues(_arr) { + var result = []; + for (var key in _arr) { + // @ts-ignore we check key is an integer + if (parseInt(key) == key) { + result.push(_arr[key]); + } + } + return result; } - /** * Equivalent to the PHP array_keys function */ -function et2_arrayKeys(_arr) -{ - var result = []; - for (var key in _arr) - { - result.push(key); - } - - return result; +function et2_arrayKeys(_arr) { + var result = []; + for (var key in _arr) { + result.push(key); + } + return result; } - -function et2_arrayIntKeys(_arr) -{ - var result = []; - for (var key in _arr) - { - result.push(parseInt(key)); - } - - return result; +function et2_arrayIntKeys(_arr) { + var result = []; + for (var key in _arr) { + result.push(parseInt(key)); + } + return result; } - - /** * Equivalent to the PHP substr function, partly take from phpjs, licensed under * the GPL. */ -function et2_substr (str, start, len) { - var end = str.length; - - if (start < 0) - { - start += end; - } - end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); - - return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); +function et2_substr(str, start, len) { + var end = str.length; + if (start < 0) { + start += end; + } + end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); + return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); } - /** * Split a $delimiter-separated options string, which can contain parts with * delimiters enclosed in $enclosure. Ported from class.boetemplate.inc.php @@ -407,374 +306,276 @@ function et2_substr (str, start, len) { * @param string _enclosure='"' * @return array */ -function et2_csvSplit(_str, _num, _delimiter, _enclosure) -{ - // Default the parameters - if (typeof _str == "undefined" || _str == null) - { - _str = ""; - } - if (typeof _num == "undefined") - { - _num = null; - } - - if (typeof _delimiter == "undefined") - { - _delimiter = ","; - } - - if (typeof _enclosure == "undefined") - { - _enclosure = '"'; - } - - // If the _enclosure string does not occur in the string, simply use the - // split function - if (_str.indexOf(_enclosure) == -1) - { - return _num === null ? _str.split(_delimiter) : - _str.split(_delimiter, _num); - } - - // Split the string at the delimiter and join it again, when a enclosure is - // found at the beginning/end of a part - var parts = _str.split(_delimiter); - for (var n = 0; typeof parts[n] != "undefined"; n++) - { - var part = parts[n]; - - if (part.charAt(0) === _enclosure) - { - var m = n; - while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) - { - parts[n] += _delimiter + parts[++m]; - delete(parts[m]); - } - parts[n] = et2_substr(parts[n].replace( - new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1 , -1); - n = m; - } - } - - // Rebuild the array index - parts = et2_arrayValues(parts); - - // Limit the parts to the given number - if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) - { - parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); - parts = parts.slice(0, _num); - } - - return parts; +function et2_csvSplit(_str, _num, _delimiter, _enclosure) { + // Default the parameters + if (typeof _str == "undefined" || _str == null) { + _str = ""; + } + if (typeof _num == "undefined") { + _num = null; + } + if (typeof _delimiter == "undefined") { + _delimiter = ","; + } + if (typeof _enclosure == "undefined") { + _enclosure = '"'; + } + // If the _enclosure string does not occur in the string, simply use the + // split function + if (_str.indexOf(_enclosure) == -1) { + return _num === null ? _str.split(_delimiter) : + _str.split(_delimiter, _num); + } + // Split the string at the delimiter and join it again, when a enclosure is + // found at the beginning/end of a part + var parts = _str.split(_delimiter); + for (var n = 0; typeof parts[n] != "undefined"; n++) { + var part = parts[n]; + if (part.charAt(0) === _enclosure) { + var m = n; + while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) { + parts[n] += _delimiter + parts[++m]; + delete (parts[m]); + } + parts[n] = et2_substr(parts[n].replace(new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1, -1); + n = m; + } + } + // Rebuild the array index + parts = et2_arrayValues(parts); + // Limit the parts to the given number + if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) { + parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); + parts = parts.slice(0, _num); + } + return parts; } - /** * Parses the given string and returns an array marking parts which are URLs */ -function et2_activateLinks(_content) -{ - var _match = false; - var arr = []; - - function _splitPush(_matches, _proc) - { - if (_matches) - { - // We had a match - _match = true; - - // Replace "undefined" with "" - for (var i = 1; i < _matches.length; i++) - { - if (typeof _matches[i] == "undefined") - { - _matches[i] = ""; - } - } - - // Split the content string at the given position(s) - // but we only handle the first occurence - var splitted = _content.split(_matches[0]); - - // Push the not-matched part - var left = splitted.shift(); - if (left) - { - // activate the links of the left string - arr = arr.concat(et2_activateLinks(left)); - } - - // Call the callback function which converts the matches into an object - // and appends it to the string - _proc(_matches); - - // Set the new working string to the right part - _content = splitted.join(_matches[0]); - } - } - - var mail_regExp = /(mailto:)?([a-z0-9._-]+)@([a-z0-9_-]+)\.([a-z0-9._-]+)/i; - - // First match things beginning with http:// (or other protocols) - var protocol = '(http:\\/\\/|(ftp:\\/\\/|https:\\/\\/))'; // only http:// gets removed, other protocolls are shown - var domain = '([\\w-]+\\.[\\w-.]+)'; - var subdir = '([\\w\\-\\.,@?^=%&;:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; - var http_regExp = new RegExp(protocol + domain + subdir, 'i'); - - // Now match things beginning with www. - var domain = 'www(\\.[\\w-.]+)'; - var subdir = '([\\w\\-\\.,@?^=%&:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; - var www_regExp = new RegExp(domain + subdir, 'i'); - - do { - _match = false; - - // Abort if the remaining length of _content is smaller than 20 for - // performance reasons - if (!_content) - { - break; - } - - // No need make emailaddress spam-save, as it gets dynamically created - _splitPush(_content.match(mail_regExp), function(_matches) { - arr.push({ - "href": (_matches[1]?'':'mailto:')+_matches[0], - "text": _matches[2] + "@" + _matches[3] + "." + _matches[4] - }); - }); - - // Create hrefs for links starting with "http://" - _splitPush(_content.match(http_regExp), function(_matches) { - arr.push({ - "href": _matches[0], - "text": _matches[2] + _matches[3] + _matches[4] - }); - }); - - // Create hrefs for links starting with "www." - _splitPush(_content.match(www_regExp), function(_matches) { - arr.push({ - "href": "http://" + _matches[0], - "text": _matches[0] - }); - }); - } while (_match) - - arr.push(_content); - - return arr; +function et2_activateLinks(_content) { + var _match = false; + var arr = []; + function _splitPush(_matches, _proc) { + if (_matches) { + // We had a match + _match = true; + // Replace "undefined" with "" + for (var i = 1; i < _matches.length; i++) { + if (typeof _matches[i] == "undefined") { + _matches[i] = ""; + } + } + // Split the content string at the given position(s) + // but we only handle the first occurence + var splitted = _content.split(_matches[0]); + // Push the not-matched part + var left = splitted.shift(); + if (left) { + // activate the links of the left string + arr = arr.concat(et2_activateLinks(left)); + } + // Call the callback function which converts the matches into an object + // and appends it to the string + _proc(_matches); + // Set the new working string to the right part + _content = splitted.join(_matches[0]); + } + } + var mail_regExp = /(mailto:)?([a-z0-9._-]+)@([a-z0-9_-]+)\.([a-z0-9._-]+)/i; + // First match things beginning with http:// (or other protocols) + var protocol = '(http:\\/\\/|(ftp:\\/\\/|https:\\/\\/))'; // only http:// gets removed, other protocolls are shown + var domain = '([\\w-]+\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&;:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var http_regExp = new RegExp(protocol + domain + subdir, 'i'); + // Now match things beginning with www. + var domain = 'www(\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var www_regExp = new RegExp(domain + subdir, 'i'); + do { + _match = false; + // Abort if the remaining length of _content is smaller than 20 for + // performance reasons + if (!_content) { + break; + } + // No need make emailaddress spam-save, as it gets dynamically created + _splitPush(_content.match(mail_regExp), function (_matches) { + arr.push({ + "href": (_matches[1] ? '' : 'mailto:') + _matches[0], + "text": _matches[2] + "@" + _matches[3] + "." + _matches[4] + }); + }); + // Create hrefs for links starting with "http://" + _splitPush(_content.match(http_regExp), function (_matches) { + arr.push({ + "href": _matches[0], + "text": _matches[2] + _matches[3] + _matches[4] + }); + }); + // Create hrefs for links starting with "www." + _splitPush(_content.match(www_regExp), function (_matches) { + arr.push({ + "href": "http://" + _matches[0], + "text": _matches[0] + }); + }); + } while (_match); + arr.push(_content); + return arr; } - /** * Inserts the structure generated by et2_activateLinks into the given DOM-Node */ -function et2_insertLinkText(_text, _node, _target) -{ - if(!_node) - { - egw.debug("warn", "et2_insertLinkText called without node", _text, _node, _target); - return; - } - - // Clear the node - for (var i = _node.childNodes.length - 1; i >= 0; i--) - { - _node.removeChild(_node.childNodes[i]); - } - - for (var i = 0; i < _text.length; i++) - { - var s = _text[i]; - - if (typeof s == "string" || typeof s == "number") - { - // Include line breaks - var lines = s.split ? s.split('\n') : [s]; - - // Insert the lines - for (var j = 0; j < lines.length; j++) - { - _node.appendChild(document.createTextNode(lines[j])); - - if (j < lines.length - 1) - { - _node.appendChild(document.createElement("br")); - } - } - } - else if(s.text) // no need to generate a link, if there is no content in it - { - if(!s.href) - { - egw.debug("warn", "et2_activateLinks gave bad data", s, _node, _target); - s.href = ""; - } - var a = jQuery(document.createElement("a")) - .attr("href", s.href) - .text(s.text); - - if (typeof _target != "undefined" && _target && _target != "_self" && s.href.substr(0, 7) != "mailto:") - { - a.attr("target", _target); - } - // open mailto links depending on preferences in mail app - if (s.href.substr(0, 7) == "mailto:" && - (egw.user('apps').mail || egw.user('apps').felamimail) && - egw.preference('force_mailto','addressbook') != '1') - { - a.click(function(event){ - egw.open_link(this.href); - return false; - }); - } - - a.appendTo(_node); - } - } +function et2_insertLinkText(_text, _node, _target) { + if (!_node) { + egw.debug("warn", "et2_insertLinkText called without node", _text, _node, _target); + return; + } + // Clear the node + for (var i = _node.childNodes.length - 1; i >= 0; i--) { + _node.removeChild(_node.childNodes[i]); + } + for (var i = 0; i < _text.length; i++) { + var s = _text[i]; + if (typeof s == "string" || typeof s == "number") { + // Include line breaks + var lines = typeof s !== "number" && s.split ? s.split('\n') : [s + ""]; + // Insert the lines + for (var j = 0; j < lines.length; j++) { + _node.appendChild(document.createTextNode(lines[j])); + if (j < lines.length - 1) { + _node.appendChild(document.createElement("br")); + } + } + } + else if (s.text) // no need to generate a link, if there is no content in it + { + if (!s.href) { + egw.debug("warn", "et2_activateLinks gave bad data", s, _node, _target); + s.href = ""; + } + var a = jQuery(document.createElement("a")) + .attr("href", s.href) + .text(s.text); + if (typeof _target != "undefined" && _target && _target != "_self" && s.href.substr(0, 7) != "mailto:") { + a.attr("target", _target); + } + // open mailto links depending on preferences in mail app + if (s.href.substr(0, 7) == "mailto:" && + (egw.user('apps').mail || egw.user('apps').felamimail) && + egw.preference('force_mailto', 'addressbook') != '1') { + a.click(function (event) { + egw.open_link(this.href); + return false; + }); + } + a.appendTo(_node); + } + } } - /** * Creates a copy of the given object (non recursive) */ -function et2_cloneObject(_obj) -{ - var result = {}; - - for (var key in _obj) - { - result[key] = _obj[key]; - } - - return result; +function et2_cloneObject(_obj) { + var result = {}; + for (var key in _obj) { + result[key] = _obj[key]; + } + return result; } - /** * Returns true if the given array of nodes or their children contains the given * child node. */ -function et2_hasChild(_nodes, _child) -{ - for (var i = 0; i < _nodes.length; i++) - { - if (_nodes[i] == _child) - { - return true; - } - else if (_nodes[i].childNodes) - { - var res = et2_hasChild(_nodes[i].childNodes, _child); - - if (res) - { - return true; - } - } - } - - return false; +function et2_hasChild(_nodes, _child) { + for (var i = 0; i < _nodes.length; i++) { + if (_nodes[i] == _child) { + return true; + } + else if (_nodes[i].childNodes) { + var res = et2_hasChild(_nodes[i].childNodes, _child); + if (res) { + return true; + } + } + } + return false; } - /** * Functions to work with ranges and range intersection (used in the dataview) */ - /** * Common functions used in most view classes */ - /** * Returns an "range" object with the given top position and height */ -function et2_range(_top, _height) -{ - return { - "top": _top, - "bottom": _top + _height - }; +function et2_range(_top, _height) { + return { + "top": _top, + "bottom": _top + _height + }; } - /** * Returns an "area" object with the given top- and bottom position */ -function et2_bounds(_top, _bottom) -{ - return { - "top": _top, - "bottom": _bottom - }; +function et2_bounds(_top, _bottom) { + return { + "top": _top, + "bottom": _bottom + }; } - /** * Returns whether two range objects intersect each other */ -function et2_rangeIntersect(_ar1, _ar2) -{ - return ! (_ar1.bottom < _ar2.top || _ar1.top > _ar2.bottom); +function et2_rangeIntersect(_ar1, _ar2) { + return !(_ar1.bottom < _ar2.top || _ar1.top > _ar2.bottom); } - /** * Returns whether two ranges intersect (result = 0) or their relative position * to each other (used to do a binary search inside a list of sorted range objects). */ -function et2_rangeIntersectDir(_ar1, _ar2) -{ - if (_ar1.bottom < _ar2.top) - { - return -1; - } - if (_ar1.top > _ar2.bottom) - { - return 1; - } - return 0; +function et2_rangeIntersectDir(_ar1, _ar2) { + if (_ar1.bottom < _ar2.top) { + return -1; + } + if (_ar1.top > _ar2.bottom) { + return 1; + } + return 0; } - /** * Returns whether two ranges are equal. */ -function et2_rangeEqual(_ar1, _ar2) -{ - return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; +function et2_rangeEqual(_ar1, _ar2) { + return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; } - /** * Substracts _ar2 from _ar1, returns an array of new ranges. */ -function et2_rangeSubstract(_ar1, _ar2) -{ - // Per default return the complete _ar1 range - var res = [_ar1]; - - // Check whether there is an intersection between the given ranges - if (et2_rangeIntersect(_ar1, _ar2)) - { - res = [et2_bounds(_ar1.top, _ar2.top), - et2_bounds(_ar2.bottom, _ar1.bottom)]; - } - - // Remove all zero-length ranges from the result - for (var i = res.length - 1; i >= 0; i--) - { - if (res[i].bottom - res[i].top <= 0) - { - res.splice(i, 1); - } - } - - return res; +function et2_rangeSubstract(_ar1, _ar2) { + // Per default return the complete _ar1 range + var res = [_ar1]; + // Check whether there is an intersection between the given ranges + if (et2_rangeIntersect(_ar1, _ar2)) { + res = [et2_bounds(_ar1.top, _ar2.top), + et2_bounds(_ar2.bottom, _ar1.bottom)]; + } + // Remove all zero-length ranges from the result + for (var i = res.length - 1; i >= 0; i--) { + if (res[i].bottom - res[i].top <= 0) { + res.splice(i, 1); + } + } + return res; } - /** * Decode html entities so they can be added via .text(_str), eg. html_entity_decode('&') === '&' * * @param {string} _str * @returns {string} */ -function html_entity_decode(_str) -{ - return _str && _str.indexOf('&') != -1 ? jQuery(''+_str+'').text() : _str; +function html_entity_decode(_str) { + return _str && _str.indexOf('&') != -1 ? jQuery('' + _str + '').text() : _str; } diff --git a/api/js/etemplate/et2_core_common.ts b/api/js/etemplate/et2_core_common.ts new file mode 100644 index 0000000000..f428e43306 --- /dev/null +++ b/api/js/etemplate/et2_core_common.ts @@ -0,0 +1,780 @@ +/** + * EGroupware eTemplate2 - JS Widget base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + */ + +/** + * IE Fix for array.indexOf + */ +if (typeof Array.prototype.indexOf == "undefined") +{ + Array.prototype.indexOf = function(_elem) { + for (var i = 0; i < this.length; i++) + { + if (this[i] === _elem) + return i; + } + return -1; + }; +} + +/** + * Array with all types supported by the et2_checkType function. + */ +var et2_validTypes = ["boolean", "string", "rawstring", "html", "float", "integer", "any", "js", "dimension"]; + +/** + * Object whith default values for the above types. Do not specify array or + * objects inside the et2_typeDefaults object, as this instance will be shared + * between all users of it. + */ +var et2_typeDefaults : object = { + "boolean": false, + "string": "", + "rawstring": "", // no html-entity decoding + "html": "", + "js": null, + "float": 0.0, + "integer": 0, + "any": null, + "dimension": "auto" +}; + +function et2_evalBool(_val) +{ + if (typeof _val == "string") + { + if (_val == "false" || _val == "0") + { + return false; + } + } + + return _val ? true : false; +} + +/** + * Concat et2 name together, eg. et2_concat("namespace","test[something]") == "namespace[test][something]" + * @param variable number of arguments to contact + * @returns string + */ +function et2_form_name(_cname,_name) +{ + var parts = []; + for(var i=0; i < arguments.length; ++i) + { + var name = arguments[i]; + if (typeof name == 'string' && name.length > 0) // et2_namespace("","test") === "test" === et2_namespace(null,"test") + { + parts = parts.concat(name.replace(/]/g,'').split('[')); + } + } + var name = parts.shift(); + return parts.length ? name + '['+parts.join('][')+']' : name; +} + +/** + * Checks whether the given value is of the given type. Strings are converted + * into the corresponding type. The (converted) value is returned. All supported + * types are listed in the et2_validTypes array. + * + * @param mixed _val value + * @param string _type a valid type eg. "string" or "js" + * @param string _attr attribute name + * @param object _widget + */ +function et2_checkType(_val, _type, _attr, _widget) +{ + if (typeof _attr == "undefined") + { + _attr = null; + } + + function _err() { + var res = et2_typeDefaults[_type]; + + if(typeof _val != "undefined" && _val) + { + egw.debug("warn", "Widget %o: '" + _val + "' was not of specified _type '" + + _type + (_attr != null ? "' for attribute '" + _attr + "' " : "") + + "and is now '" + res + "'",_widget); + } + return res; + } + + // If the type is "any" simply return the value again + if (_type == "any") + { + return _val; + } + + // we dont check default-value any further, that also fixes type="js" does NOT accept null, + // which happens on expanded values + if (_val === et2_typeDefaults[_type]) + { + return _val; + } + + // If the type is boolean, check whether the given value is exactly true or + // false. Otherwise check whether the value is the string "true" or "false". + if (_type == "boolean") + { + if (_val === true || _val === false) + { + return _val; + } + + if (typeof _val == "string") + { + var lcv = _val.toLowerCase(); + if (lcv === "true" || lcv === "false" || lcv === "") + { + return _val === "true"; + } + if(lcv === "0" || lcv === "1") + { + return _val === "1"; + } + } + else if (typeof _val == "number") + { + return _val != 0; + } + + return _err(); + } + + // Check whether the given value is of the type "string" + if (_type == "string" || _type == "html" || _type == "rawstring") + { + if (typeof _val == "number") // as php is a bit vague here, silently convert to a string + { + return _val.toString(); + } + + if (typeof _val == "string") + { + return _type == "string" ? html_entity_decode(_val) : _val; + } + + // Handle some less common possibilities + // Maybe a split on an empty string + if(typeof _val == "object" && jQuery.isEmptyObject(_val)) return ""; + + return _err(); + } + + // Check whether the value is already a number, otherwise try to convert it + // to one. + if (_type == "float") + { + if (typeof _val == "number") + { + return _val; + } + + if (!isNaN(_val)) + { + return parseFloat(_val); + } + + return _err(); + } + + // Check whether the value is an integer by comparing the result of + // parseInt(_val) to the value itself. + if (_type == "integer") + { + if (parseInt(_val) == _val) + { + return parseInt(_val); + } + + return _err(); + } + + // Parse the given dimension value + if (_type == "dimension") + { + // Case 1: The value is "auto" + if (_val == "auto") + { + return _val; + } + + // Case 2: The value is simply a number, attach "px" + if (!isNaN(_val)) + { + return parseFloat(_val) + "px"; + } + + // Case 3: The value is already a valid css pixel value or a percentage + if (typeof _val == "string" && + ((_val.indexOf("px") == _val.length - 2 && !isNaN(_val.split("px")[0] as any)) || + (_val.indexOf("%") == _val.length - 1 && !isNaN(_val.split("%")[0] as any)))) + { + return _val; + } + + return _err(); + } + + // Javascript + if (_type == "js") + { + if (typeof _val == "function" || typeof _val == "undefined") + { + return _val; + } + if (_val) _val = _val.replace(/window\.close\(\)/g, 'egw(window).close()'); + + // Check to see if it's a string in app.appname.function format, and wrap it in + // a closure to make sure context is preserved + if(typeof _val == "string" && _val.substr(0,4) == "app." && app) + { + var parts = _val.split('.'); + var func = parts.pop(); + var parent = window; + for(var i=0; i < parts.length && typeof parent[parts[i]] != 'undefined'; ++i) + { + parent = parent[parts[i]]; + } + if (typeof parent[func] == 'function') + { + try + { + return jQuery.proxy(parent[func],parent); + } + catch (e) + { + egw.debug('error', 'Function', _val); + return _err(); + } + } + } + + if (!_val || typeof _val == "string") + { + return _val; // get compiled later in widgets own initAttributes, as widget is not yet initialised + } + } + + // We should never come here + throw("Invalid type identifier '" + _attr + "': '" + _type+"'"); +} + +/** + * If et2_no_init is set as default value, the initAttributes function will not + * try to initialize the attribute with the default value. + */ +const et2_no_init = new Object(); + +/** + * Validates the given attribute with the given id. The validation checks for + * the existance of a human name, a description, a type and a default value. + * If the human name defaults to the given id, the description defaults to an + * empty string, the type defaults to any and the default to the corresponding + * type default. + */ +function et2_validateAttrib(_id, _attrib) +{ + // Default ignore to false. + if (typeof _attrib["ignore"] == "undefined") + { + _attrib["ignore"] = false; + } + + // Break if "ignore" is set to true. + if (_attrib.ignore) + { + return; + } + + if (typeof _attrib["name"] == "undefined") + { + _attrib["name"] = _id; + egw.debug("log", "Human name ('name'-Field) for attribute '" + + _id + "' has not been supplied, set to '" + _id + "'"); + } + + if (typeof _attrib["description"] == "undefined") + { + _attrib["description"] = ""; + egw.debug("log", "Description for attribute '" + + _id + "' has not been supplied"); + } + + if (typeof _attrib["type"] == "undefined") + { + _attrib["type"] = "any"; + } + else + { + if (et2_validTypes.indexOf(_attrib["type"]) < 0) + { + egw.debug("error", "Invalid type '" + _attrib["type"] + "' for attribute '" + _id + + "' supplied. Valid types are ", et2_validTypes); + } + } + + // Set the defaults + if (typeof _attrib["default"] == "undefined") + { + _attrib["default"] = et2_typeDefaults[_attrib["type"]]; + } +} + +/** + * Equivalent to the PHP array_values function + */ +function et2_arrayValues(_arr) +{ + var result = []; + for (var key in _arr) + { + // @ts-ignore we check key is an integer + if (parseInt(key) == key) + { + result.push(_arr[key]); + } + } + + return result; +} + +/** + * Equivalent to the PHP array_keys function + */ +function et2_arrayKeys(_arr) +{ + var result = []; + for (var key in _arr) + { + result.push(key); + } + + return result; +} + +function et2_arrayIntKeys(_arr) +{ + var result = []; + for (var key in _arr) + { + result.push(parseInt(key)); + } + + return result; +} + + +/** + * Equivalent to the PHP substr function, partly take from phpjs, licensed under + * the GPL. + */ +function et2_substr (str, start, len) { + var end = str.length; + + if (start < 0) + { + start += end; + } + end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); + + return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); +} + +/** + * Split a $delimiter-separated options string, which can contain parts with + * delimiters enclosed in $enclosure. Ported from class.boetemplate.inc.php + * + * Examples: + * - et2_csvSplit('"1,2,3",2,3') === array('1,2,3','2','3') + * - et2_csvSplit('1,2,3',2) === array('1','2,3') + * - et2_csvSplit('"1,2,3",2,3',2) === array('1,2,3','2,3') + * - et2_csvSplit('"a""b,c",d') === array('a"b,c','d') // to escape enclosures double them! + * + * @param string _str + * @param int _num=null in how many parts to split maximal, parts over this + * number end up (unseparated) in the last part + * @param string _delimiter=',' + * @param string _enclosure='"' + * @return array + */ +function et2_csvSplit(_str : string, _num? : number, _delimiter? : string, _enclosure? : string) +{ + // Default the parameters + if (typeof _str == "undefined" || _str == null) + { + _str = ""; + } + if (typeof _num == "undefined") + { + _num = null; + } + + if (typeof _delimiter == "undefined") + { + _delimiter = ","; + } + + if (typeof _enclosure == "undefined") + { + _enclosure = '"'; + } + + // If the _enclosure string does not occur in the string, simply use the + // split function + if (_str.indexOf(_enclosure) == -1) + { + return _num === null ? _str.split(_delimiter) : + _str.split(_delimiter, _num); + } + + // Split the string at the delimiter and join it again, when a enclosure is + // found at the beginning/end of a part + var parts = _str.split(_delimiter); + for (var n = 0; typeof parts[n] != "undefined"; n++) + { + var part = parts[n]; + + if (part.charAt(0) === _enclosure) + { + var m = n; + while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) + { + parts[n] += _delimiter + parts[++m]; + delete(parts[m]); + } + parts[n] = et2_substr(parts[n].replace( + new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1 , -1); + n = m; + } + } + + // Rebuild the array index + parts = et2_arrayValues(parts); + + // Limit the parts to the given number + if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) + { + parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); + parts = parts.slice(0, _num); + } + + return parts; +} + +/** + * Parses the given string and returns an array marking parts which are URLs + */ +function et2_activateLinks(_content) +{ + var _match = false; + var arr = []; + + function _splitPush(_matches, _proc) + { + if (_matches) + { + // We had a match + _match = true; + + // Replace "undefined" with "" + for (var i = 1; i < _matches.length; i++) + { + if (typeof _matches[i] == "undefined") + { + _matches[i] = ""; + } + } + + // Split the content string at the given position(s) + // but we only handle the first occurence + var splitted = _content.split(_matches[0]); + + // Push the not-matched part + var left = splitted.shift(); + if (left) + { + // activate the links of the left string + arr = arr.concat(et2_activateLinks(left)); + } + + // Call the callback function which converts the matches into an object + // and appends it to the string + _proc(_matches); + + // Set the new working string to the right part + _content = splitted.join(_matches[0]); + } + } + + var mail_regExp = /(mailto:)?([a-z0-9._-]+)@([a-z0-9_-]+)\.([a-z0-9._-]+)/i; + + // First match things beginning with http:// (or other protocols) + var protocol = '(http:\\/\\/|(ftp:\\/\\/|https:\\/\\/))'; // only http:// gets removed, other protocolls are shown + var domain = '([\\w-]+\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&;:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var http_regExp = new RegExp(protocol + domain + subdir, 'i'); + + // Now match things beginning with www. + var domain = 'www(\\.[\\w-.]+)'; + var subdir = '([\\w\\-\\.,@?^=%&:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; + var www_regExp = new RegExp(domain + subdir, 'i'); + + do { + _match = false; + + // Abort if the remaining length of _content is smaller than 20 for + // performance reasons + if (!_content) + { + break; + } + + // No need make emailaddress spam-save, as it gets dynamically created + _splitPush(_content.match(mail_regExp), function(_matches) { + arr.push({ + "href": (_matches[1]?'':'mailto:')+_matches[0], + "text": _matches[2] + "@" + _matches[3] + "." + _matches[4] + }); + }); + + // Create hrefs for links starting with "http://" + _splitPush(_content.match(http_regExp), function(_matches) { + arr.push({ + "href": _matches[0], + "text": _matches[2] + _matches[3] + _matches[4] + }); + }); + + // Create hrefs for links starting with "www." + _splitPush(_content.match(www_regExp), function(_matches) { + arr.push({ + "href": "http://" + _matches[0], + "text": _matches[0] + }); + }); + } while (_match) + + arr.push(_content); + + return arr; +} + +/** + * Inserts the structure generated by et2_activateLinks into the given DOM-Node + */ +function et2_insertLinkText(_text, _node, _target) +{ + if(!_node) + { + egw.debug("warn", "et2_insertLinkText called without node", _text, _node, _target); + return; + } + + // Clear the node + for (var i = _node.childNodes.length - 1; i >= 0; i--) + { + _node.removeChild(_node.childNodes[i]); + } + + for (var i = 0; i < _text.length; i++) + { + var s = _text[i]; + + if (typeof s == "string" || typeof s == "number") + { + // Include line breaks + var lines = typeof s !== "number" && s.split ? s.split('\n') : [s+""]; + + // Insert the lines + for (var j = 0; j < lines.length; j++) + { + _node.appendChild(document.createTextNode(lines[j])); + + if (j < lines.length - 1) + { + _node.appendChild(document.createElement("br")); + } + } + } + else if(s.text) // no need to generate a link, if there is no content in it + { + if(!s.href) + { + egw.debug("warn", "et2_activateLinks gave bad data", s, _node, _target); + s.href = ""; + } + var a = jQuery(document.createElement("a")) + .attr("href", s.href) + .text(s.text); + + if (typeof _target != "undefined" && _target && _target != "_self" && s.href.substr(0, 7) != "mailto:") + { + a.attr("target", _target); + } + // open mailto links depending on preferences in mail app + if (s.href.substr(0, 7) == "mailto:" && + (egw.user('apps').mail || egw.user('apps').felamimail) && + egw.preference('force_mailto','addressbook') != '1') + { + a.click(function(event){ + egw.open_link(this.href); + return false; + }); + } + + a.appendTo(_node); + } + } +} + +/** + * Creates a copy of the given object (non recursive) + */ +function et2_cloneObject(_obj) +{ + var result = {}; + + for (var key in _obj) + { + result[key] = _obj[key]; + } + + return result; +} + +/** + * Returns true if the given array of nodes or their children contains the given + * child node. + */ +function et2_hasChild(_nodes, _child) +{ + for (var i = 0; i < _nodes.length; i++) + { + if (_nodes[i] == _child) + { + return true; + } + else if (_nodes[i].childNodes) + { + var res = et2_hasChild(_nodes[i].childNodes, _child); + + if (res) + { + return true; + } + } + } + + return false; +} + +/** + * Functions to work with ranges and range intersection (used in the dataview) + */ + +/** + * Common functions used in most view classes + */ + +/** + * Returns an "range" object with the given top position and height + */ +function et2_range(_top, _height) +{ + return { + "top": _top, + "bottom": _top + _height + }; +} + +/** + * Returns an "area" object with the given top- and bottom position + */ +function et2_bounds(_top, _bottom) +{ + return { + "top": _top, + "bottom": _bottom + }; +} + +/** + * Returns whether two range objects intersect each other + */ +function et2_rangeIntersect(_ar1, _ar2) +{ + return ! (_ar1.bottom < _ar2.top || _ar1.top > _ar2.bottom); +} + +/** + * Returns whether two ranges intersect (result = 0) or their relative position + * to each other (used to do a binary search inside a list of sorted range objects). + */ +function et2_rangeIntersectDir(_ar1, _ar2) +{ + if (_ar1.bottom < _ar2.top) + { + return -1; + } + if (_ar1.top > _ar2.bottom) + { + return 1; + } + return 0; +} + +/** + * Returns whether two ranges are equal. + */ +function et2_rangeEqual(_ar1, _ar2) +{ + return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; +} + +/** + * Substracts _ar2 from _ar1, returns an array of new ranges. + */ +function et2_rangeSubstract(_ar1, _ar2) +{ + // Per default return the complete _ar1 range + var res = [_ar1]; + + // Check whether there is an intersection between the given ranges + if (et2_rangeIntersect(_ar1, _ar2)) + { + res = [et2_bounds(_ar1.top, _ar2.top), + et2_bounds(_ar2.bottom, _ar1.bottom)]; + } + + // Remove all zero-length ranges from the result + for (var i = res.length - 1; i >= 0; i--) + { + if (res[i].bottom - res[i].top <= 0) + { + res.splice(i, 1); + } + } + + return res; +} + +/** + * Decode html entities so they can be added via .text(_str), eg. html_entity_decode('&') === '&' + * + * @param {string} _str + * @returns {string} + */ +function html_entity_decode(_str) +{ + return _str && _str.indexOf('&') != -1 ? jQuery(''+_str+'').text() : _str; +} diff --git a/api/js/etemplate/et2_core_inheritance.js b/api/js/etemplate/et2_core_inheritance.js index 37f00022ae..360bb03454 100644 --- a/api/js/etemplate/et2_core_inheritance.js +++ b/api/js/etemplate/et2_core_inheritance.js @@ -1,158 +1,163 @@ +"use strict"; /** * EGroupware eTemplate2 - JS code for implementing inheritance with attributes * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api - * @link http://www.egroupware.org + * @link: https://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_core_common; - egw_inheritance; + et2_core_common; */ - -var ClassWithAttributes = (function(){ "use strict"; return Class.extend( -{ - /** - * Returns the value of the given attribute. If the property does not - * exist, an error message is issued. - * - * @param {string} _name - * @return {*} - */ - getAttribute: function(_name) { - if (typeof this.attributes[_name] != "undefined" && - !this.attributes[_name].ignore) - { - if (typeof this["get_" + _name] == "function") - { - return this["get_" + _name](); - } - else - { - return this[_name]; - } - } - else - { - egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); - } - }, - - /** - * The setAttribute function sets the attribute with the given name to - * the given value. _override defines, whether this[_name] will be set, - * if this key already exists. _override defaults to true. A warning - * is issued if the attribute does not exist. - * - * @param {string} _name - * @param {*} _value - * @param {boolean} _override - */ - setAttribute: function(_name, _value, _override) { - if (typeof this.attributes[_name] != "undefined") - { - if (!this.attributes[_name].ignore) - { - if (typeof _override == "undefined") - { - _override = true; - } - - var val = et2_checkType(_value, this.attributes[_name].type, - _name, this); - - if (typeof this["set_" + _name] == "function") - { - this["set_" + _name](val); - } - else if (_override || typeof this[_name] == "undefined") - { - this[_name] = val; - } - } - } - else - { - egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); - } - }, - - /** - * generateAttributeSet sanitizes the given associative array of attributes - * (by passing each entry to "et2_checkType" and checking for existance of - * the attribute) and adds the default values to the associative array. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - generateAttributeSet: function(_attrs) { - - // Sanity check and validation - for (var key in _attrs) - { - if (typeof this.attributes[key] != "undefined") - { - if (!this.attributes[key].ignore) - { - _attrs[key] = et2_checkType(_attrs[key], this.attributes[key].type, - key, this); - } - } - else - { - // Key does not exist - delete it and issue a warning - delete(_attrs[key]); - egw.debug("warn", this, "Attribute '" + key + - "' does not exist in " + _attrs.type+"!"); - } - } - - // Include default values or already set values for this attribute - for (var key in this.attributes) - { - if (typeof _attrs[key] == "undefined") - { - var _default = this.attributes[key]["default"]; - if (_default == et2_no_init) - { - _default = undefined; - } - - _attrs[key] = _default; - } - } - - return _attrs; - }, - - /** - * The initAttributes function sets the attributes to their default - * values. The attributes are not overwritten, which means, that the - * default is only set, if either a setter exists or this[propName] does - * not exist yet. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - initAttributes: function(_attrs) { - for (var key in _attrs) - { - if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) - { - this.setAttribute(key, _attrs[key], false); - } - } - }, - - _validate_attributes: function(attributes) - { - // Validate the attributes - for (var key in attributes) - { - et2_validateAttrib(key, attributes[key]); - } - } -});}).call(this); \ No newline at end of file +require("../jsapi/egw_global"); +require("et2_core_common"); +var ClassWithAttributes = /** @class */ (function () { + function ClassWithAttributes() { + } + /** + * Returns the value of the given attribute. If the property does not + * exist, an error message is issued. + * + * @param {string} _name + * @return {*} + */ + ClassWithAttributes.prototype.getAttribute = function (_name) { + if (typeof this.attributes[_name] != "undefined" && + !this.attributes[_name].ignore) { + if (typeof this["get_" + _name] == "function") { + return this["get_" + _name](); + } + else { + return this[_name]; + } + } + else { + egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); + } + }; + /** + * The setAttribute function sets the attribute with the given name to + * the given value. _override defines, whether this[_name] will be set, + * if this key already exists. _override defaults to true. A warning + * is issued if the attribute does not exist. + * + * @param {string} _name + * @param {*} _value + * @param {boolean} _override + */ + ClassWithAttributes.prototype.setAttribute = function (_name, _value, _override) { + if (typeof this.attributes[_name] != "undefined") { + if (!this.attributes[_name].ignore) { + if (typeof _override == "undefined") { + _override = true; + } + var val = et2_checkType(_value, this.attributes[_name].type, _name, this); + if (typeof this["set_" + _name] == "function") { + this["set_" + _name](val); + } + else if (_override || typeof this[_name] == "undefined") { + this[_name] = val; + } + } + } + else { + egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); + } + }; + /** + * generateAttributeSet sanitizes the given associative array of attributes + * (by passing each entry to "et2_checkType" and checking for existance of + * the attribute) and adds the default values to the associative array. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + ClassWithAttributes.prototype.generateAttributeSet = function (_attrs) { + // Sanity check and validation + for (var key in _attrs) { + if (typeof this.attributes[key] != "undefined") { + if (!this.attributes[key].ignore) { + _attrs[key] = et2_checkType(_attrs[key], this.attributes[key].type, key, this); + } + } + else { + // Key does not exist - delete it and issue a warning + delete (_attrs[key]); + egw.debug("warn", this, "Attribute '" + key + + "' does not exist in " + _attrs.type + "!"); + } + } + // Include default values or already set values for this attribute + for (var key in this.attributes) { + if (typeof _attrs[key] == "undefined") { + var _default = this.attributes[key]["default"]; + if (_default == et2_no_init) { + _default = undefined; + } + _attrs[key] = _default; + } + } + return _attrs; + }; + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + ClassWithAttributes.prototype.initAttributes = function (_attrs) { + for (var key in _attrs) { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { + this.setAttribute(key, _attrs[key], false); + } + } + }; + /** + * Extend current _attributes with the one from the parent class + * + * This gives inheritance from the parent plus the ability to override in the current class. + * + * @param _attributes + * @param _parent + */ + ClassWithAttributes.extendAttributes = function (_attributes, _parent) { + function _copyMerge(_new, _old) { + var result = {}; + // Copy the new object + if (typeof _new != "undefined") { + for (var key in _new) { + result[key] = _new[key]; + } + } + // Merge the old object + for (var key in _old) { + if (typeof result[key] == "undefined") { + result[key] = _old[key]; + } + } + return result; + } + var attributes = {}; + // Copy the old attributes + for (var key in _attributes) { + attributes[key] = _copyMerge({}, _attributes[key]); + } + // Add the old attributes to the new ones. If the attributes already + // exist, they are merged. + for (var key in _parent) { + var _old = _parent[key]; + attributes[key] = _copyMerge(attributes[key], _old); + } + // Validate the attributes + for (var key in attributes) { + et2_validateAttrib(key, attributes[key]); + } + return attributes; + }; + return ClassWithAttributes; +}()); +exports.ClassWithAttributes = ClassWithAttributes; diff --git a/api/js/etemplate/et2_core_inheritance.ts b/api/js/etemplate/et2_core_inheritance.ts new file mode 100644 index 0000000000..34c891ce30 --- /dev/null +++ b/api/js/etemplate/et2_core_inheritance.ts @@ -0,0 +1,217 @@ +/** + * EGroupware eTemplate2 - JS code for implementing inheritance with attributes + * + * @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 + */ + +/*egw:uses + et2_core_common; +*/ + +import '../jsapi/egw_global'; +import 'et2_core_common'; + +export class ClassWithAttributes +{ + /** + * Object to collect the attributes we operate on + */ + attributes: object; + + /** + * Returns the value of the given attribute. If the property does not + * exist, an error message is issued. + * + * @param {string} _name + * @return {*} + */ + getAttribute(_name) { + if (typeof this.attributes[_name] != "undefined" && + !this.attributes[_name].ignore) + { + if (typeof this["get_" + _name] == "function") + { + return this["get_" + _name](); + } + else + { + return this[_name]; + } + } + else + { + egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); + } + } + + /** + * The setAttribute function sets the attribute with the given name to + * the given value. _override defines, whether this[_name] will be set, + * if this key already exists. _override defaults to true. A warning + * is issued if the attribute does not exist. + * + * @param {string} _name + * @param {*} _value + * @param {boolean} _override + */ + setAttribute(_name, _value, _override) + { + if (typeof this.attributes[_name] != "undefined") + { + if (!this.attributes[_name].ignore) + { + if (typeof _override == "undefined") + { + _override = true; + } + + var val = et2_checkType(_value, this.attributes[_name].type, + _name, this); + + if (typeof this["set_" + _name] == "function") + { + this["set_" + _name](val); + } + else if (_override || typeof this[_name] == "undefined") + { + this[_name] = val; + } + } + } + else + { + egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); + } + } + + /** + * generateAttributeSet sanitizes the given associative array of attributes + * (by passing each entry to "et2_checkType" and checking for existance of + * the attribute) and adds the default values to the associative array. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + generateAttributeSet(_attrs) + { + // Sanity check and validation + for (var key in _attrs) + { + if (typeof this.attributes[key] != "undefined") + { + if (!this.attributes[key].ignore) + { + _attrs[key] = et2_checkType(_attrs[key], this.attributes[key].type, + key, this); + } + } + else + { + // Key does not exist - delete it and issue a warning + delete(_attrs[key]); + egw.debug("warn", this, "Attribute '" + key + + "' does not exist in " + _attrs.type+"!"); + } + } + + // Include default values or already set values for this attribute + for (var key in this.attributes) + { + if (typeof _attrs[key] == "undefined") + { + var _default = this.attributes[key]["default"]; + if (_default == et2_no_init) + { + _default = undefined; + } + + _attrs[key] = _default; + } + } + + return _attrs; + } + + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * @param {object} _attrs is the associative array containing the attributes. + */ + initAttributes(_attrs) + { + for (var key in _attrs) + { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) + { + this.setAttribute(key, _attrs[key], false); + } + } + } + + /** + * Extend current _attributes with the one from the parent class + * + * This gives inheritance from the parent plus the ability to override in the current class. + * + * @param _attributes + * @param _parent + */ + static extendAttributes(_attributes : object, _parent : object) : object + { + function _copyMerge(_new, _old) + { + var result = {}; + + // Copy the new object + if (typeof _new != "undefined") + { + for (var key in _new) + { + result[key] = _new[key]; + } + } + + // Merge the old object + for (var key in _old) + { + if (typeof result[key] == "undefined") + { + result[key] = _old[key]; + } + } + + return result; + } + + var attributes = {}; + + // Copy the old attributes + for (var key in _attributes) + { + attributes[key] = _copyMerge({}, _attributes[key]); + } + + // Add the old attributes to the new ones. If the attributes already + // exist, they are merged. + for (var key in _parent) + { + var _old = _parent[key]; + + attributes[key] = _copyMerge(attributes[key], _old); + } + + // Validate the attributes + for (var key in attributes) + { + et2_validateAttrib(key, attributes[key]); + } + + return attributes; + } +} \ No newline at end of file diff --git a/api/js/etemplate/et2_core_interfaces.js b/api/js/etemplate/et2_core_interfaces.js index dc87241236..86bf331979 100644 --- a/api/js/etemplate/et2_core_interfaces.js +++ b/api/js/etemplate/et2_core_interfaces.js @@ -6,156 +6,39 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - -/*egw:uses - et2_core_inheritance; -*/ - /** - * Interface for all widget classes, which are based on a DOM node. + * Checks if an object / et2_widget implements given methods + * + * @param obj + * @param methods */ -var et2_IDOMNode = new Interface({ - /** - * Returns the DOM-Node of the current widget. The return value has to be - * a plain DOM node. If you want to return an jQuery object as you receive - * it with - * - * obj = jQuery(node); - * - * simply return obj[0]; - * - * @param _sender The _sender parameter defines which widget is asking for - * the DOMNode. Depending on that, the widget may return different nodes. - * This is used in the grid. Normally the _sender parameter can be omitted - * in most implementations of the getDOMNode function. - * However, you should always define the _sender parameter when calling - * getDOMNode! - */ - getDOMNode: function(_sender) {} -}); - -/** - * Interface for all widgets which support returning a value - */ -var et2_IInput = new Interface({ - /** - * getValue has to return the value of the input widget - */ - getValue: function() {}, - - /** - * Is dirty returns true if the value of the widget has changed since it - * was loaded. - */ - isDirty: function() {}, - - /** - * Causes the dirty flag to be reseted. - */ - resetDirty: function() {}, - - /** - * Checks the data to see if it is valid, as far as the client side can tell. - * Return true if it's not possible to tell on the client side, because the server - * will have the chance to validate also. - * - * The messages array is to be populated with everything wrong with the data, - * so don't stop checking after the first problem unless it really makes sense - * to ignore other problems. - * - * @param {String[]} messages List of messages explaining the failure(s). - * messages should be fairly short, and already translated. - * - * @return {boolean} True if the value is valid (enough), false to fail - */ - isValid: function(messages) {} -}); - -/** - * Interface for widgets which should be automatically resized - */ -var et2_IResizeable = new Interface({ - /** - * Called whenever the window is resized - */ - resize: function() {} -}); - -/** - * Interface for widgets which have the align attribute - */ -var et2_IAligned = new Interface({ - get_align: function() {} -}); - -/** - * Interface for widgets which want to e.g. perform clientside validation before - * the form is submitted. - */ -var et2_ISubmitListener = new Interface({ - /** - * Called whenever the template gets submitted. Return false if you want to - * stop submission. - * - * @param _values contains the values which will be sent to the server. - * Listeners may change these values before they get submitted. - */ - submit: function(_values) {} -}); - -/** - * Interface all widgets must support which can operate on given DOM-Nodes. This - * is used in grid lists, when only a single row gets really stored in the widget - * tree and this instance has to work on multiple copies of the DOM-Tree elements. - */ -var et2_IDetachedDOM = new Interface({ - - /** - * Creates a list of attributes which can be set when working in the - * "detached" mode. The result is stored in the _attrs array which is provided - * by the calling code. - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) {}, - - /** - * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - */ - getDetachedNodes: function() {}, - - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which have to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes: function(_nodes, _values) {} - -}); - -/** - * Interface for widgets that need to do something special before printing - */ -var et2_IPrint = new Interface({ - /** - * Set up for printing - * - * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up - * (waiting for data) - */ - beforePrint: function() {}, - - /** - * Reset after printing - */ - afterPrint: function() {} -}); \ No newline at end of file +function implements_methods(obj, methods) { + for (var i = 0; i < methods.length; ++i) { + if (typeof obj[methods[i]] !== 'function') { + return false; + } + } + return true; +} +function implements_et2_IDOMNode(obj) { + return implements_methods(obj, ["getDOMNode"]); +} +function implements_et2_IInput(obj) { + return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); +} +function implements_et2_IResizeable(obj) { + return implements_methods(obj, ["resize"]); +} +function implements_et2_IAligned(obj) { + return implements_methods(obj, ["get_align"]); +} +function implements_et2_ISubmitListener(obj) { + return implements_methods(obj, ["submit"]); +} +function implements_et2_IDetachedDOM(obj) { + return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); +} +function implements_et2_IPrint(obj) { + return implements_methods(obj, ["beforePrint", "afterPrint"]); +} diff --git a/api/js/etemplate/et2_core_interfaces.ts b/api/js/etemplate/et2_core_interfaces.ts new file mode 100644 index 0000000000..ddc1e853bd --- /dev/null +++ b/api/js/etemplate/et2_core_interfaces.ts @@ -0,0 +1,208 @@ +/** + * EGroupware eTemplate2 - File which contains all interfaces + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/** + * Checks if an object / et2_widget implements given methods + * + * @param obj + * @param methods + */ +function implements_methods(obj : et2_widget, methods : string[]) : boolean +{ + for(let i=0; i < methods.length; ++i) + { + if (typeof obj[methods[i]] !== 'function') + { + return false; + } + } + return true; +} + +/** + * Interface for all widget classes, which are based on a DOM node. + */ +interface et2_IDOMNode +{ + /** + * Returns the DOM-Node of the current widget. The return value has to be + * a plain DOM node. If you want to return an jQuery object as you receive + * it with + * + * obj = jQuery(node); + * + * simply return obj[0]; + * + * @param _sender The _sender parameter defines which widget is asking for + * the DOMNode. Depending on that, the widget may return different nodes. + * This is used in the grid. Normally the _sender parameter can be omitted + * in most implementations of the getDOMNode function. + * However, you should always define the _sender parameter when calling + * getDOMNode! + */ + getDOMNode(_sender? : et2_widget) : HTMLElement +} +function implements_et2_IDOMNode(obj : et2_widget) +{ + return implements_methods(obj, ["getDOMNode"]); +} + +/** + * Interface for all widgets which support returning a value + */ +interface et2_IInput +{ + /** + * getValue has to return the value of the input widget + */ + getValue() : any + + /** + * Is dirty returns true if the value of the widget has changed since it + * was loaded. + */ + isDirty() : boolean + + /** + * Causes the dirty flag to be reseted. + */ + resetDirty() : void + + /** + * Checks the data to see if it is valid, as far as the client side can tell. + * Return true if it's not possible to tell on the client side, because the server + * will have the chance to validate also. + * + * The messages array is to be populated with everything wrong with the data, + * so don't stop checking after the first problem unless it really makes sense + * to ignore other problems. + * + * @param {String[]} messages List of messages explaining the failure(s). + * messages should be fairly short, and already translated. + * + * @return {boolean} True if the value is valid (enough), false to fail + */ + isValid(messages) : boolean +} +function implements_et2_IInput(obj : et2_widget) +{ + return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); +} + +/** + * Interface for widgets which should be automatically resized + */ +interface et2_IResizeable +{ + /** + * Called whenever the window is resized + */ + resize() : void +} +function implements_et2_IResizeable(obj : et2_widget) +{ + return implements_methods(obj, ["resize"]); +} + +/** + * Interface for widgets which have the align attribute + */ +interface et2_IAligned +{ + get_align() : string +} +function implements_et2_IAligned(obj : et2_widget) +{ + return implements_methods(obj, ["get_align"]); +} + +/** + * Interface for widgets which want to e.g. perform clientside validation before + * the form is submitted. + */ +interface et2_ISubmitListener +{ + /** + * Called whenever the template gets submitted. Return false if you want to + * stop submission. + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + submit(_values) : void +} +function implements_et2_ISubmitListener(obj : et2_widget) +{ + return implements_methods(obj, ["submit"]); +} + +/** + * Interface all widgets must support which can operate on given DOM-Nodes. This + * is used in grid lists, when only a single row gets really stored in the widget + * tree and this instance has to work on multiple copies of the DOM-Tree elements. + */ +interface et2_IDetachedDOM +{ + + /** + * Creates a list of attributes which can be set when working in the + * "detached" mode. The result is stored in the _attrs array which is provided + * by the calling code. + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs : string[]) : void + + /** + * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be + * passed to the "setDetachedAttributes" function in the same order. + */ + getDetachedNodes() : HTMLElement[] + + /** + * Sets the given associative attribute->value array and applies the + * attributes to the given DOM-Node. + * + * @param _nodes is an array of nodes which have to be in the same order as + * the nodes returned by "getDetachedNodes" + * @param _values is an associative array which contains a subset of attributes + * returned by the "getDetachedAttributes" function and sets them to the + * given values. + */ + setDetachedAttributes(_nodes : HTMLElement[], _values : object) : void + +} +function implements_et2_IDetachedDOM(obj : et2_widget) +{ + return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); +} + +/** + * Interface for widgets that need to do something special before printing + */ +interface et2_IPrint +{ + /** + * Set up for printing + * + * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up + * (waiting for data) + */ + beforePrint() : JQueryPromise | void + + /** + * Reset after printing + */ + afterPrint() : void +} +function implements_et2_IPrint(obj : et2_widget) +{ + return implements_methods(obj, ["beforePrint", "afterPrint"]); +} diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js index fc3c0623ff..b07590740b 100644 --- a/api/js/etemplate/et2_core_widget.js +++ b/api/js/etemplate/et2_core_widget.js @@ -1,29 +1,40 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Widget base class * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api - * @link http://www.egroupware.org + * @link https://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - jsapi.egw; - et2_core_xml; - et2_core_common; - et2_core_inheritance; - et2_core_arrayMgr; + jsapi.egw; + et2_core_xml; + et2_core_common; + et2_core_inheritance; + et2_core_arrayMgr; */ - +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * The registry contains all XML tag names and the corresponding widget * constructor. */ var et2_registry = {}; - /** * Registers the widget class defined by the given constructor and associates it * with the types in the _types array. @@ -31,27 +42,21 @@ var et2_registry = {}; * @param {function} _constructor constructor * @param {array} _types widget types _constructor wants to register for */ -function et2_register_widget(_constructor, _types) -{ - "use strict"; - - // Iterate over all given types and register those - for (var i = 0; i < _types.length; i++) - { - var type = _types[i].toLowerCase(); - - // Check whether a widget has already been registered for one of the - // types. - if (et2_registry[type]) - { - egw.debug("warn", "Widget class registered for " + type + - " will be overwritten."); - } - - et2_registry[type] = _constructor; - } +function et2_register_widget(_constructor, _types) { + "use strict"; + // Iterate over all given types and register those + for (var i = 0; i < _types.length; i++) { + var type = _types[i].toLowerCase(); + // Check whether a widget has already been registered for one of the + // types. + if (et2_registry[type]) { + egw.debug("warn", "Widget class registered for " + type + + " will be overwritten."); + } + et2_registry[type] = _constructor; + } } - +exports.et2_register_widget = et2_register_widget; /** * Creates a widget registered for the given tag-name. If "readonly" is listed * inside the attributes, et2_createWidget will try to use the "_ro" type of the @@ -65,1015 +70,832 @@ function et2_register_widget(_constructor, _types) * is not passed, it will default to null. Then you have to attach the element * to a parent using the addChild or insertChild method. */ -function et2_createWidget(_name, _attrs, _parent) -{ - "use strict"; - - if (typeof _attrs == "undefined") - { - _attrs = {}; - } - - if (typeof _attrs != "object") - { - _attrs = {}; - } - - if (typeof _parent == "undefined") - { - _parent = null; - } - - // Parse the "readonly" and "type" flag for this element here, as they - // determine which constructor is used - var nodeName = _attrs["type"] = _name; - var readonly = _attrs["readonly"] = - typeof _attrs["readonly"] == "undefined" ? false : _attrs["readonly"]; - - // Get the constructor - if the widget is readonly, use the special "_ro" - // constructor if it is available - var constructor = typeof et2_registry[nodeName] == "undefined" ? - et2_placeholder : et2_registry[nodeName]; - if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined") - { - constructor = et2_registry[nodeName + "_ro"]; - } - - // Do an sanity check for the attributes - constructor.prototype.generateAttributeSet(_attrs); - - // Create the new widget and return it - return new constructor(_parent, _attrs); +function et2_createWidget(_name, _attrs, _parent) { + "use strict"; + if (typeof _attrs == "undefined") { + _attrs = {}; + } + if (typeof _attrs != "object") { + _attrs = {}; + } + if (typeof _parent == "undefined") { + _parent = null; + } + // Parse the "readonly" and "type" flag for this element here, as they + // determine which constructor is used + var nodeName = _attrs["type"] = _name; + var readonly = _attrs["readonly"] = + typeof _attrs["readonly"] == "undefined" ? false : _attrs["readonly"]; + // Get the constructor - if the widget is readonly, use the special "_ro" + // constructor if it is available + var constructor = typeof et2_registry[nodeName] == "undefined" ? + et2_placeholder : et2_registry[nodeName]; + if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined") { + constructor = et2_registry[nodeName + "_ro"]; + } + // Do an sanity check for the attributes + constructor.prototype.generateAttributeSet(_attrs); + // Create the new widget and return it + return new constructor(_parent, _attrs); } - +exports.et2_createWidget = et2_createWidget; /** * The et2 widget base class. * * @augments ClassWithAttributes */ -var et2_widget = (function(){ "use strict"; return ClassWithAttributes.extend( -{ - attributes: { - "id": { - "name": "ID", - "type": "string", - "description": "Unique identifier of the widget" - }, - - "no_lang": { - "name": "No translation", - "type": "boolean", - "default": false, - "description": "If true, no translations are made for this widget" - }, - - /** - * Ignore the "span" property by default - it is read by the grid and - * other widgets. - */ - "span": { - "ignore": true - }, - - /** - * Ignore the "type" tag - it is read by the "createElementFromNode" - * function and passed as second parameter of the widget constructor - */ - "type": { - "name": "Widget type", - "type": "string", - "ignore": true, - "description": "What kind of widget this is" - }, - - /** - * Ignore the readonly tag by default - its also read by the - * "createElementFromNode" function. - */ - "readonly": { - "ignore": true - }, - - /** - * Widget's attributes - */ - attributes: { - "name": "Widget attributes", - "type": "any", - "ignore": true, - "description": "Object of widget attributes" - } - }, - - // Set the legacyOptions array to the names of the properties the "options" - // attribute defines. - legacyOptions: [], - - /** - * Set this variable to true if this widget can have namespaces - */ - createNamespace: false, - - /** - * The init function is the constructor of the widget. When deriving new - * classes from the widget base class, always call this constructor unless - * you know what you're doing. - * - * @param _parent is the parent object from the XML tree which contains this - * object. The default constructor always adds the new instance to the - * children list of the given parent object. _parent may be NULL. - * @param _attrs is an associative array of attributes. - * @memberOf et2_widget - */ - init: function(_parent, _attrs) { - - // Check whether all attributes are available - if (typeof _parent == "undefined") - { - _parent = null; - } - - if (typeof _attrs == "undefined") - { - _attrs = {}; - } - - if (_attrs.attributes) - { - jQuery.extend(_attrs, _attrs.attributes); - } - // Initialize all important parameters - this._mgrs = {}; - this._inst = null; - this._children = []; - this._type = _attrs["type"]; - this.id = _attrs["id"]; - - // Add this widget to the given parent widget - if (_parent != null) - { - _parent.addChild(this); - } - - // The supported widget classes array defines a whitelist for all widget - // classes or interfaces child widgets have to support. - this.supportedWidgetClasses = [et2_widget]; - - if (_attrs["id"]) - { - // Create a namespace for this object - if (this.createNamespace) - { - this.checkCreateNamespace(); - } - } - - if(this.id) - { - //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']'); - } - - // Add all attributes hidden in the content arrays to the attributes - // parameter - this.transformAttributes(_attrs); - - // Create a local copy of the options object - this.options = et2_cloneObject(_attrs); - }, - - /** - * The destroy function destroys all children of the widget, removes itself - * from the parents children list. - * In all classes derrived from et2_widget ALWAYS override the destroy - * function and remove ALL references to other objects. Also remember to - * unbind ANY event this widget created and to remove all DOM-Nodes it - * created. - */ - destroy: function() { - - // Call the destructor of all children - for (var i = this._children.length - 1; i >= 0; i--) - { - this._children[i].free(); - } - - // Remove this element from the parent, if it exists - if (typeof this._parent != "undefined" && this._parent !== null) - { - this._parent.removeChild(this); - } - - // Free the array managers if they belong to this widget - for (var key in this._mgrs) - { - if (this._mgrs[key] && this._mgrs[key].owner == this) - { - this._mgrs[key].free(); - } - } - }, - - /** - * Creates a copy of this widget. The parameters given are passed to the - * constructor of the copied object. If the parameters are omitted, _parent - * is defaulted to null - * - * @param {et2_widget} _parent parent to set for clone, default null - */ - clone: function(_parent) { - - // Default _parent to null - if (typeof _parent == "undefined") - { - _parent = null; - } - - // Create the copy - var copy = new (this.constructor)(_parent, this.options); - - // Assign this element to the copy - copy.assign(this); - - return copy; - }, - - assign: function(_obj) { - if (typeof _obj._children == "undefined") - { - this.egw().debug("log", "Foo!"); - } - - // Create a clone of all child elements of the given object - for (var i = 0; i < _obj._children.length; i++) - { - _obj._children[i].clone(this); - } - - // Copy a reference to the content array manager - this.setArrayMgrs(_obj.mgrs); - }, - - /** - * Returns the parent widget of this widget - */ - getParent: function() { - return this._parent; - }, - - /** - * Returns the list of children of this widget. - */ - getChildren: function() { - return this._children; - }, - - /** - * Returns the base widget - */ - getRoot: function() { - if (this._parent != null) - { - return this._parent.getRoot(); - } - else - { - return this; - } - }, - - /** - * Inserts an child at the end of the list. - * - * @param _node is the node which should be added. It has to be an instance - * of et2_widget - */ - addChild: function(_node) { - this.insertChild(_node, this._children.length); - }, - - /** - * Inserts a child at the given index. - * - * @param _node is the node which should be added. It has to be an instance - * of et2_widget - * @param _idx is the position at which the element should be added. - */ - insertChild: function(_node, _idx) { - // Check whether the node is one of the supported widget classes. - if (this.isOfSupportedWidgetClass(_node)) - { - // Remove the node from its original parent - if (_node._parent) - { - _node._parent.removeChild(_node); - } - - _node._parent = this; - this._children.splice(_idx, 0, _node); - - if(_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && _node.parentNode) - { - _node.detachFromDOM(); - _node.parentNode = this.getDOMNode(_node); - _node.attachToDOM(); - } - - } - else - { - this.egw().debug("error", "Widget " + _node._type +" is not supported by this widget class", this); -// throw("Widget is not supported by this widget class!"); - } - }, - - /** - * Removes the child but does not destroy it. - * - * @param {et2_widget} _node child to remove - */ - removeChild: function(_node) { - // Retrieve the child from the child list - var idx = this._children.indexOf(_node); - - if (idx >= 0) - { - // This element is no longer parent of the child - _node._parent = null; - - this._children.splice(idx, 1); - } - }, - - /** - * Searches an element by id in the tree, descending into the child levels. - * - * @param _id is the id you're searching for - */ - getWidgetById: function(_id) { - if (this.id == _id) - { - return this; - } - if(!this._children) return null; - - for (var i = 0; i < this._children.length; i++) - { - var elem = this._children[i].getWidgetById(_id); - - if (elem != null) - { - return elem; - } - } - if(this.id && _id.indexOf('[') > -1 && this._children.length) - { - var ids = (new et2_arrayMgr()).explodeKey(_id); - var widget = this; - for(var i = 0; i < ids.length && widget !== null; i++) - { - widget = widget.getWidgetById(ids[i]); - } - return widget; - } - - return null; - }, - - /** - * Function which allows iterating over the complete widget tree. - * - * @param _callback is the function which should be called for each widget - * @param _context is the context in which the function should be executed - * @param _type is an optional parameter which specifies a class/interface - * the elements have to be instanceOf. - */ - iterateOver: function(_callback, _context, _type) { - if (typeof _type == "undefined") - { - _type = et2_widget; - } - - if (this.isInTree() && this.instanceOf(_type)) - { - _callback.call(_context, this); - } - - for (var i = 0; i < this._children.length; i++) - { - this._children[i].iterateOver(_callback, _context, _type); - } - }, - - /** - * Returns true if the widget currently resides in the visible part of the - * widget tree. E.g. Templates which have been cloned are not in the visible - * part of the widget tree. - * - * @param _sender - * @param {boolean} _vis can be used by widgets overwriting this function - simply - * write - * return this._super(inTree); - * when calling this function the _vis parameter does not have to be supplied. - */ - isInTree: function(_sender, _vis) { - if (typeof _vis == "undefined") - { - _vis = true; - } - - if (this._parent) - { - return _vis && this._parent.isInTree(this); - } - - return _vis; - }, - - isOfSupportedWidgetClass: function(_obj) - { - for (var i = 0; i < this.supportedWidgetClasses.length; i++) - { - if (_obj.instanceOf(this.supportedWidgetClasses[i])) - { - return true; - } - } - return false; - }, - - /** - * The parseXMLAttrs function takes an XML DOM attributes object - * and adds the given attributes to the _target associative array. This - * function also parses the legacyOptions. - * - * @param _attrsObj is the XML DOM attributes object - * @param {object} _target is the object to which the attributes should be written. - * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute - */ - parseXMLAttrs: function(_attrsObj, _target, _proto) { - - // Check whether the attributes object is really existing, if not abort - if (typeof _attrsObj == "undefined") - { - return; - } - - // Iterate over the given attributes and parse them - var mgr = this.getArrayMgr("content"); - for (var i = 0; i < _attrsObj.length; i++) - { - var attrName = _attrsObj[i].name; - var attrValue = _attrsObj[i].value; - - // Special handling for the legacy options - if (attrName == "options" && _proto.legacyOptions.length > 0) - { - // Check for modifications on legacy options here. Normal modifications - // are handled in widget constructor, but it's too late for legacy options then - if(_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) - { - var mod = this.getArrayMgr("modifications").getEntry(_target.id); - if(typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options; - } - // expand legacyOptions with content - if(attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) - { - attrValue = mgr.expandName(attrValue); - } - - // Parse the legacy options (as a string, other types not allowed) - var splitted = et2_csvSplit(attrValue+""); - - for (var j = 0; j < splitted.length && j < _proto.legacyOptions.length; j++) - { - // Blank = not set, unless there's more legacy options provided after - if(splitted[j].trim().length === 0 && _proto.legacyOptions.length >= splitted.length) continue; - - // Check to make sure we don't overwrite a current option with a legacy option - if(typeof _target[_proto.legacyOptions[j]] === "undefined") - { - attrValue = splitted[j]; - - /** - If more legacy options than expected, stuff them all in the last legacy option - Some legacy options take a comma separated list. - */ - if(j == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) - { - attrValue = splitted.slice(j); - } - - var attr = _proto.attributes[_proto.legacyOptions[j]]; - - // If the attribute is marked as boolean, parse the - // expression as bool expression. - if (attr.type == "boolean") - { - attrValue = mgr.parseBoolExpression(attrValue); - } - else if (typeof attrValue != "object") - { - attrValue = mgr.expandName(attrValue); - } - _target[_proto.legacyOptions[j]] = attrValue; - } - } - } - else if (attrName == "readonly" && typeof _target[attrName] != "undefined") - { - // do NOT overwrite already evaluated readonly attribute - } - else - { - if (mgr != null && typeof _proto.attributes[attrName] != "undefined") - { - var attr = _proto.attributes[attrName]; - - // If the attribute is marked as boolean, parse the - // expression as bool expression. - if (attr.type == "boolean") - { - attrValue = mgr.parseBoolExpression(attrValue); - } - else - { - attrValue = mgr.expandName(attrValue); - } - } - - // Set the attribute - _target[attrName] = attrValue; - } - } - }, - - /** - * Apply the "modifications" to the element and translate attributes marked - * with "translate: true" - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) { - - // Apply the content of the modifications array - if (this.id) - { - if (typeof this.id != "string") - { - console.log(this.id); - } - - if(this.getArrayMgr("modifications")) - { - var data = this.getArrayMgr("modifications").getEntry(this.id); - - // Check for already inside namespace - if(this.createNamespace && this.getArrayMgr("modifications").perspectiveData.owner == this) - { - data = this.getArrayMgr("modifications").data; - } - if (typeof data === 'object') - { - for (var key in data) - { - _attrs[key] = data[key]; - } - } - } - } - - // Translate the attributes - for (var key in _attrs) - { - if (_attrs[key] && typeof this.attributes[key] != "undefined") - { - if (this.attributes[key].translate === true || - (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"])) - { - _attrs[key] = this.egw().lang(_attrs[key]); - } - } - } - }, - - /** - * Create a et2_widget from an XML node. - * - * First the type and attributes are read from the node. Then the readonly & modifications - * arrays are checked for changes specific to the loaded data. Then the appropriate - * constructor is called. After the constructor returns, the widget has a chance to - * further initialize itself from the XML node when the widget's loadFromXML() method - * is called with the node. - * - * @param _node XML node to read - * - * @return et2_widget - */ - createElementFromNode: function(_node) { - var attributes = {}; - - // Parse the "readonly" and "type" flag for this element here, as they - // determine which constructor is used - var _nodeName = attributes["type"] = _node.getAttribute("type") ? - _node.getAttribute("type") : _node.nodeName.toLowerCase(); - var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ? - this.getArrayMgr("readonlys").isReadOnly( - _node.getAttribute("id"), _node.getAttribute("readonly"), - typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly ) : false; - - // Check to see if modifications change type - var modifications = this.getArrayMgr("modifications"); - if(modifications && _node.getAttribute("id")) { - var entry = modifications.getEntry(_node.getAttribute("id")); - if(entry == null) - { - // Try again, but skip the fancy stuff - // TODO: Figure out why the getEntry() call doesn't always work - var entry = modifications.data[_node.getAttribute("id")]; - if(entry) - { - this.egw().debug("warn", "getEntry("+_node.getAttribute("id")+") failed, but the data is there.", modifications, entry); - } - else - { - // Try the root, in case a namespace got missed - var entry = modifications.getRoot().getEntry(_node.getAttribute("id")); - } - } - if(entry && entry.type && typeof entry.type === 'string') - { - _nodeName = attributes["type"] = entry.type; - } - entry = null; - } - - // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"), - // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs! - if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0) - { - _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName); - } - - // Get the constructor - if the widget is readonly, use the special "_ro" - // constructor if it is available - var constructor = typeof et2_registry[_nodeName] == "undefined" ? - et2_placeholder : et2_registry[_nodeName]; - if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined") - { - constructor = et2_registry[_nodeName + "_ro"]; - } - - // Parse the attributes from the given XML attributes object - this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); - - // Do an sanity check for the attributes - constructor.prototype.generateAttributeSet(attributes); - - // Creates the new widget, passes this widget as an instance and - // passes the widgetType. Then it goes on loading the XML for it. - var widget = new constructor(this, attributes); - - // Load the widget itself from XML - widget.loadFromXML(_node); - - return widget; - }, - - /** - * Loads the widget tree from an XML node - * - * @param _node xml node - */ - loadFromXML: function(_node) { - // Load the child nodes. - for (var i = 0; i < _node.childNodes.length; i++) - { - var node = _node.childNodes[i]; - var widgetType = node.nodeName.toLowerCase(); - - if (widgetType == "#comment") - { - continue; - } - - if (widgetType == "#text") - { - if (node.data.replace(/^\s+|\s+$/g, '')) - { - this.loadContent(node.data); - } - continue; - } - - // Create the new element - this.createElementFromNode(node); - } - }, - - /** - * Called whenever textNodes are loaded from the XML tree - * - * @param _content - */ - loadContent: function(_content) { - }, - - /** - * Called when loading the widget (sub-tree) is finished. First when this - * function is called, the DOM-Tree is created. loadingFinished is - * recursively called for all child elements. Do not directly override this - * function but the doLoadingFinished function which is executed before - * descending deeper into the DOM-Tree. - * - * Some widgets (template) do not load immediately because they request - * additional resources via AJAX. They will return a Deferred Promise object. - * If you call loadingFinished(promises) after creating such a widget - * programmatically, you might need to wait for it to fully complete its - * loading before proceeding. In that case use: - * - * var promises = []; - * widget.loadingFinished(promises); - * jQuery.when.apply(null, promises).done( doneCallback ); - * - * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred} - * - * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed. - */ - loadingFinished: function(promises) { - // Call all availble setters - this.initAttributes(this.options); - - // Make sure promises is defined to avoid errors. - // We'll warn (below) if programmer should have passed it. - if(typeof promises == "undefined") - { - promises = []; - var warn_if_deferred = true; - } - - var loadChildren = function() - { - // Descend recursively into the tree - for (var i = 0; i < this._children.length; i++) - { - try - { - this._children[i].loadingFinished(promises); - } - catch (e) - { - egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o",e.valueOf(),this._children[i],e.stack); - } - } - }; - - var result = this.doLoadingFinished(); - if(typeof result == "boolean" && result) - { - // Simple widget finishes nicely - loadChildren.apply(this, arguments); - } - else if (typeof result == "object" && result.done) - { - // Warn if list was not provided - if(warn_if_deferred) - { - // Might not be a problem, but if you need the widget to be really loaded, it could be - egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); - } - // Widget is waiting. Add to the list - promises.push(result); - // Fihish loading when it's finished - result.done(jQuery.proxy(loadChildren, this)); - } - }, - - /** - * The initAttributes function sets the attributes to their default - * values. The attributes are not overwritten, which means, that the - * default is only set, if either a setter exists or this[propName] does - * not exist yet. - * - * Overwritten here to compile legacy JS code in attributes of type "js" - * - * @param {object} _attrs - */ - initAttributes: function(_attrs) { - for (var key in _attrs) - { - if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) - { - var val = _attrs[key]; - // compile string values of attribute type "js" to functions - if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string') - { - val = et2_compileLegacyJS(val, this, - this.instanceOf(et2_inputWidget) ? this.getInputNode() : - (this.implements(et2_IDOMNode) ? this.getDOMNode() : null)); - } - this.setAttribute(key, val, false); - } - } - }, - - /** - * Does specific post-processing after the widget is loaded. Most widgets should not - * need to do anything here, it should all be done before. - * - * @return {boolean|Promise} True if the widget is fully loaded, false to avoid procesing children, - * or a Promise if loading is not actually finished (eg. waiting for AJAX) - * - * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise} - */ - doLoadingFinished: function() { - return true; - }, - - /** - * The egw function returns the instance of the client side api belonging - * to this widget tree. The api instance can be set in the "container" - * widget using the setApiInstance function. - */ - egw: function() { - // The _egw property is not set - if (typeof this._egw === 'undefined') - { - if (this._parent != null) - { - return this._parent.egw(); - } - - // Get the window this object belongs to - var wnd = null; - if (this.implements(et2_IDOMNode)) - { - var node = this.getDOMNode(); - if(node && node.ownerDocument) - { - wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; - } - } - - // If we're the root object, return the phpgwapi API instance - return egw('phpgwapi', wnd); - } - - return this._egw; - }, - - /** - * Sets the client side api instance. It can be retrieved by the widget tree - * by using the "egw()" function. - * - * @param {egw} _egw egw object to set - */ - setApiInstance: function(_egw) { - this._egw = _egw; - }, - - /** - * Sets all array manager objects - this function can be used to set the - * root array managers of the container object. - * - * @param {object} _mgrs - */ - setArrayMgrs: function(_mgrs) { - this._mgrs = et2_cloneObject(_mgrs); - }, - - /** - * Returns an associative array containing the top-most array managers. - * - * @param _mgrs is used internally and should not be supplied. - */ - getArrayMgrs: function(_mgrs) { - if (typeof _mgrs == "undefined") - { - _mgrs = {}; - } - - // Add all managers of this object to the result, if they have not already - // been set in the result - for (var key in this._mgrs) - { - if (typeof _mgrs[key] == "undefined") - { - _mgrs[key] = this._mgrs[key]; - } - } - - // Recursively applies this function to the parent widget - if (this._parent) - { - this._parent.getArrayMgrs(_mgrs); - } - - return _mgrs; - }, - - /** - * Sets the array manager for the given part - * - * @param {string} _part which array mgr to set - * @param {object} _mgr - */ - setArrayMgr: function(_part, _mgr) { - this._mgrs[_part] = _mgr; - }, - - /** - * Returns the array manager object for the given part - * - * @param {string} _part name of array mgr to return - */ - getArrayMgr: function(_part) { - if (this._mgrs && typeof this._mgrs[_part] != "undefined") - { - return this._mgrs[_part]; - } - else if (this._parent) - { - return this._parent.getArrayMgr(_part); - } - - return null; - }, - - /** - * Checks whether a namespace exists for this element in the content array. - * If yes, an own perspective of the content array is created. If not, the - * parent content manager is used. - */ - checkCreateNamespace: function() { - // Get the content manager - var mgrs = this.getArrayMgrs(); - - for (var key in mgrs) - { - var mgr = mgrs[key]; - - // Get the original content manager if we have already created a - // perspective for this node - if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) - { - mgr = mgr.parentMgr; - } - - // Check whether the manager has a namespace for the id of this object - var entry = mgr.getEntry(this.id); - if (typeof entry === 'object' && entry !== null||this.id ) - { - // The content manager has an own node for this object, so - // create an own perspective. - this._mgrs[key] = mgr.openPerspective(this, this.id); - } - else - { - // The current content manager does not have an own namespace for - // this element, so use the content manager of the parent. - delete(this._mgrs[key]); - } - } - }, - - /** - * Sets the instance manager object (of type etemplate2, see etemplate2.js) - * - * @param {etemplate2} _inst - */ - setInstanceManager: function(_inst) { - this._inst = _inst; - }, - - /** - * Returns the instance manager - * - * @return {etemplate2} - */ - getInstanceManager: function() { - if (this._inst != null) - { - return this._inst; - } - else if (this._parent) - { - return this._parent.getInstanceManager(); - } - - return null; - }, - - /** - * Returns the path into the data array. By default, array manager takes care of - * this, but some extensions need to override this - */ - getPath: function() { - var path = this.getArrayMgr("content").getPath(); - - // Prevent namespaced widgets with value from going an extra layer deep - if(this.id && this.createNamespace && path[path.length -1] == this.id) path.pop(); - - return path; - } -});}).call(this); - +var et2_widget = /** @class */ (function (_super) { + __extends(et2_widget, _super); + /** + * Widget constructor + * + * To implement the attributes inheritance and overriding each extending class/widget needs to call: + * + * super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + * + * @param _parent is the parent object from the XML tree which contains this + * object. The default constructor always adds the new instance to the + * children list of the given parent object. _parent may be NULL. + * @param _attrs is an associative array of attributes. + * @param _child attributes object from the child + */ + function et2_widget(_parent, _attrs, _child) { + var _this = _super.call(this) || this; + _this._children = []; + _this._mgrs = {}; + /** + * This is used and therefore it we can not (yet) make it private + * + * @deprecated use this.getInstanceMgr() + */ + _this._inst = null; + _this.attributes = et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {}); + // Check whether all attributes are available + if (typeof _parent == "undefined") { + _parent = null; + } + if (typeof _attrs == "undefined") { + _attrs = {}; + } + if (_attrs.attributes) { + jQuery.extend(_attrs, _attrs.attributes); + } + // Initialize all important parameters + _this._mgrs = {}; + _this._inst = null; + _this._children = []; + _this._type = _attrs["type"]; + _this.id = _attrs["id"]; + // Add this widget to the given parent widget + if (_parent != null) { + _parent.addChild(_this); + } + // The supported widget classes array defines a whitelist for all widget + // classes or interfaces child widgets have to support. + _this.supportedWidgetClasses = [et2_widget]; + if (_attrs["id"]) { + // Create a namespace for this object + if (_this.createNamespace) { + _this.checkCreateNamespace(); + } + } + if (_this.id) { + //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']'); + } + // Add all attributes hidden in the content arrays to the attributes + // parameter + _this.transformAttributes(_attrs); + // Create a local copy of the options object + _this.options = et2_cloneObject(_attrs); + return _this; + } + /** + * The destroy function destroys all children of the widget, removes itself + * from the parents children list. + * In all classes derrived from et2_widget ALWAYS override the destroy + * function and remove ALL references to other objects. Also remember to + * unbind ANY event this widget created and to remove all DOM-Nodes it + * created. + */ + et2_widget.prototype.destroy = function () { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].free(); + } + // Remove this element from the parent, if it exists + if (typeof this._parent != "undefined" && this._parent !== null) { + this._parent.removeChild(this); + } + // Free the array managers if they belong to this widget + for (var key in this._mgrs) { + if (this._mgrs[key] && this._mgrs[key].owner == this) { + this._mgrs[key].free(); + } + } + }; + /** + * Creates a copy of this widget. The parameters given are passed to the + * constructor of the copied object. If the parameters are omitted, _parent + * is defaulted to null + * + * @param {et2_widget} _parent parent to set for clone, default null + */ + et2_widget.prototype.clone = function (_parent) { + // Default _parent to null + if (typeof _parent == "undefined") { + _parent = null; + } + // Create the copy + var copy = new this.constructor(_parent, this.options); + // Assign this element to the copy + copy.assign(this); + return copy; + }; + et2_widget.prototype.assign = function (_obj) { + if (typeof _obj._children == "undefined") { + this.egw().debug("log", "Foo!"); + } + // Create a clone of all child elements of the given object + for (var i = 0; i < _obj._children.length; i++) { + _obj._children[i].clone(this); + } + // Copy a reference to the content array manager + this.setArrayMgrs(_obj.mgrs); + }; + /** + * Returns the parent widget of this widget + */ + et2_widget.prototype.getParent = function () { + return this._parent; + }; + /** + * Returns the list of children of this widget. + */ + et2_widget.prototype.getChildren = function () { + return this._children; + }; + /** + * Returns the base widget + */ + et2_widget.prototype.getRoot = function () { + if (this._parent != null) { + return this._parent.getRoot(); + } + else { + return this; + } + }; + /** + * Inserts an child at the end of the list. + * + * @param _node is the node which should be added. It has to be an instance + * of et2_widget + */ + et2_widget.prototype.addChild = function (_node) { + this.insertChild(_node, this._children.length); + }; + /** + * Inserts a child at the given index. + * + * @param _node is the node which should be added. It has to be an instance + * of et2_widget + * @param _idx is the position at which the element should be added. + */ + et2_widget.prototype.insertChild = function (_node, _idx) { + // Check whether the node is one of the supported widget classes. + if (this.isOfSupportedWidgetClass(_node)) { + // Remove the node from its original parent + if (_node._parent) { + _node._parent.removeChild(_node); + } + _node._parent = this; + this._children.splice(_idx, 0, _node); + if (_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && _node.parentNode) { + _node.detachFromDOM(); + _node.parentNode = this.getDOMNode(_node); + _node.attachToDOM(); + } + } + else { + this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this); + // throw("Widget is not supported by this widget class!"); + } + }; + /** + * Removes the child but does not destroy it. + * + * @param {et2_widget} _node child to remove + */ + et2_widget.prototype.removeChild = function (_node) { + // Retrieve the child from the child list + var idx = this._children.indexOf(_node); + if (idx >= 0) { + // This element is no longer parent of the child + _node._parent = null; + this._children.splice(idx, 1); + } + }; + /** + * Searches an element by id in the tree, descending into the child levels. + * + * @param _id is the id you're searching for + */ + et2_widget.prototype.getWidgetById = function (_id) { + if (this.id == _id) { + return this; + } + if (!this._children) + return null; + for (var i = 0; i < this._children.length; i++) { + var elem = this._children[i].getWidgetById(_id); + if (elem != null) { + return elem; + } + } + if (this.id && _id.indexOf('[') > -1 && this._children.length) { + var ids = (new et2_arrayMgr()).explodeKey(_id); + var widget = this; + for (var i = 0; i < ids.length && widget !== null; i++) { + widget = widget.getWidgetById(ids[i]); + } + return widget; + } + return null; + }; + /** + * Function which allows iterating over the complete widget tree. + * + * @param _callback is the function which should be called for each widget + * @param _context is the context in which the function should be executed + * @param _type is an optional parameter which specifies a class/interface + * the elements have to be instanceOf. + */ + et2_widget.prototype.iterateOver = function (_callback, _context, _type) { + if (typeof _type == "undefined") { + _type = et2_widget; + } + if (this.isInTree() && this.instanceOf(_type)) { + _callback.call(_context, this); + } + for (var i = 0; i < this._children.length; i++) { + this._children[i].iterateOver(_callback, _context, _type); + } + }; + /** + * Returns true if the widget currently resides in the visible part of the + * widget tree. E.g. Templates which have been cloned are not in the visible + * part of the widget tree. + * + * @param _sender + * @param {boolean} _vis can be used by widgets overwriting this function - simply + * write + * return this._super(inTree); + * when calling this function the _vis parameter does not have to be supplied. + */ + et2_widget.prototype.isInTree = function (_sender, _vis) { + if (typeof _vis == "undefined") { + _vis = true; + } + if (this._parent) { + return _vis && this._parent.isInTree(this); + } + return _vis; + }; + et2_widget.prototype.isOfSupportedWidgetClass = function (_obj) { + for (var i = 0; i < this.supportedWidgetClasses.length; i++) { + if (_obj instanceof this.supportedWidgetClasses[i]) { + return true; + } + } + return false; + }; + /** + * The parseXMLAttrs function takes an XML DOM attributes object + * and adds the given attributes to the _target associative array. This + * function also parses the legacyOptions. + * + * @param _attrsObj is the XML DOM attributes object + * @param {object} _target is the object to which the attributes should be written. + * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute + */ + et2_widget.prototype.parseXMLAttrs = function (_attrsObj, _target, _proto) { + // Check whether the attributes object is really existing, if not abort + if (typeof _attrsObj == "undefined") { + return; + } + // Iterate over the given attributes and parse them + var mgr = this.getArrayMgr("content"); + for (var i = 0; i < _attrsObj.length; i++) { + var attrName = _attrsObj[i].name; + var attrValue = _attrsObj[i].value; + // Special handling for the legacy options + if (attrName == "options" && _proto.legacyOptions.length > 0) { + // Check for modifications on legacy options here. Normal modifications + // are handled in widget constructor, but it's too late for legacy options then + if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) { + var mod = this.getArrayMgr("modifications").getEntry(_target.id); + if (typeof mod.options != "undefined") + attrValue = _attrsObj[i].value = mod.options; + } + // expand legacyOptions with content + if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { + attrValue = mgr.expandName(attrValue); + } + // Parse the legacy options (as a string, other types not allowed) + var splitted = et2_csvSplit(attrValue + ""); + for (var j = 0; j < splitted.length && j < _proto.legacyOptions.length; j++) { + // Blank = not set, unless there's more legacy options provided after + if (splitted[j].trim().length === 0 && _proto.legacyOptions.length >= splitted.length) + continue; + // Check to make sure we don't overwrite a current option with a legacy option + if (typeof _target[_proto.legacyOptions[j]] === "undefined") { + attrValue = splitted[j]; + /** + If more legacy options than expected, stuff them all in the last legacy option + Some legacy options take a comma separated list. + */ + if (j == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) { + attrValue = splitted.slice(j); + } + var attr = _proto.attributes[_proto.legacyOptions[j]]; + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } + else if (typeof attrValue != "object") { + attrValue = mgr.expandName(attrValue); + } + _target[_proto.legacyOptions[j]] = attrValue; + } + } + } + else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { + // do NOT overwrite already evaluated readonly attribute + } + else { + if (mgr != null && typeof _proto.attributes[attrName] != "undefined") { + var attr = _proto.attributes[attrName]; + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } + else { + attrValue = mgr.expandName(attrValue); + } + } + // Set the attribute + _target[attrName] = attrValue; + } + } + }; + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * @param {object} _attrs + */ + et2_widget.prototype.transformAttributes = function (_attrs) { + // Apply the content of the modifications array + if (this.id) { + if (typeof this.id != "string") { + console.log(this.id); + } + if (this.getArrayMgr("modifications")) { + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for already inside namespace + if (this.createNamespace && this.getArrayMgr("modifications").perspectiveData.owner == this) { + data = this.getArrayMgr("modifications").data; + } + if (typeof data === 'object') { + for (var key in data) { + _attrs[key] = data[key]; + } + } + } + } + // Translate the attributes + for (var key in _attrs) { + if (_attrs[key] && typeof this.attributes[key] != "undefined") { + if (this.attributes[key].translate === true || + (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"])) { + _attrs[key] = this.egw().lang(_attrs[key]); + } + } + } + }; + /** + * Create a et2_widget from an XML node. + * + * First the type and attributes are read from the node. Then the readonly & modifications + * arrays are checked for changes specific to the loaded data. Then the appropriate + * constructor is called. After the constructor returns, the widget has a chance to + * further initialize itself from the XML node when the widget's loadFromXML() method + * is called with the node. + * + * @param _node XML node to read + * + * @return et2_widget + */ + et2_widget.prototype.createElementFromNode = function (_node) { + var attributes = {}; + // Parse the "readonly" and "type" flag for this element here, as they + // determine which constructor is used + var _nodeName = attributes["type"] = _node.getAttribute("type") ? + _node.getAttribute("type") : _node.nodeName.toLowerCase(); + var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ? + this.getArrayMgr("readonlys").isReadOnly(_node.getAttribute("id"), _node.getAttribute("readonly"), typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false; + // Check to see if modifications change type + var modifications = this.getArrayMgr("modifications"); + if (modifications && _node.getAttribute("id")) { + var entry = modifications.getEntry(_node.getAttribute("id")); + if (entry == null) { + // Try again, but skip the fancy stuff + // TODO: Figure out why the getEntry() call doesn't always work + var entry = modifications.data[_node.getAttribute("id")]; + if (entry) { + this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry); + } + else { + // Try the root, in case a namespace got missed + var entry = modifications.getRoot().getEntry(_node.getAttribute("id")); + } + } + if (entry && entry.type && typeof entry.type === 'string') { + _nodeName = attributes["type"] = entry.type; + } + entry = null; + } + // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"), + // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs! + if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0) { + _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName); + } + // Get the constructor - if the widget is readonly, use the special "_ro" + // constructor if it is available + var constructor = typeof et2_registry[_nodeName] == "undefined" ? + et2_placeholder : et2_registry[_nodeName]; + if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined") { + constructor = et2_registry[_nodeName + "_ro"]; + } + // Parse the attributes from the given XML attributes object + this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); + // Do an sanity check for the attributes + constructor.prototype.generateAttributeSet(attributes); + // Creates the new widget, passes this widget as an instance and + // passes the widgetType. Then it goes on loading the XML for it. + var widget = new constructor(this, attributes); + // Load the widget itself from XML + widget.loadFromXML(_node); + return widget; + }; + /** + * Loads the widget tree from an XML node + * + * @param _node xml node + */ + et2_widget.prototype.loadFromXML = function (_node) { + // Load the child nodes. + for (var i = 0; i < _node.childNodes.length; i++) { + var node = _node.childNodes[i]; + var widgetType = node.nodeName.toLowerCase(); + if (widgetType == "#comment") { + continue; + } + if (widgetType == "#text") { + if (node.data.replace(/^\s+|\s+$/g, '')) { + this.loadContent(node.data); + } + continue; + } + // Create the new element + this.createElementFromNode(node); + } + }; + /** + * Called whenever textNodes are loaded from the XML tree + * + * @param _content + */ + et2_widget.prototype.loadContent = function (_content) { + }; + /** + * Called when loading the widget (sub-tree) is finished. First when this + * function is called, the DOM-Tree is created. loadingFinished is + * recursively called for all child elements. Do not directly override this + * function but the doLoadingFinished function which is executed before + * descending deeper into the DOM-Tree. + * + * Some widgets (template) do not load immediately because they request + * additional resources via AJAX. They will return a Deferred Promise object. + * If you call loadingFinished(promises) after creating such a widget + * programmatically, you might need to wait for it to fully complete its + * loading before proceeding. In that case use: + * + * var promises = []; + * widget.loadingFinished(promises); + * jQuery.when.apply(null, promises).done( doneCallback ); + * + * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred} + * + * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed. + */ + et2_widget.prototype.loadingFinished = function (promises) { + // Call all availble setters + this.initAttributes(this.options); + // Make sure promises is defined to avoid errors. + // We'll warn (below) if programmer should have passed it. + if (typeof promises == "undefined") { + promises = []; + var warn_if_deferred = true; + } + var loadChildren = function () { + // Descend recursively into the tree + for (var i = 0; i < this._children.length; i++) { + try { + this._children[i].loadingFinished(promises); + } + catch (e) { + egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack); + } + } + }; + var result = this.doLoadingFinished(); + if (typeof result == "boolean" && result) { + // Simple widget finishes nicely + loadChildren.apply(this, arguments); + } + else if (typeof result == "object" && result.done) { + // Warn if list was not provided + if (warn_if_deferred) { + // Might not be a problem, but if you need the widget to be really loaded, it could be + egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); + } + // Widget is waiting. Add to the list + promises.push(result); + // Fihish loading when it's finished + result.done(jQuery.proxy(loadChildren, this)); + } + }; + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * Overwritten here to compile legacy JS code in attributes of type "js" + * + * @param {object} _attrs + */ + et2_widget.prototype.initAttributes = function (_attrs) { + for (var key in _attrs) { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { + var val = _attrs[key]; + // compile string values of attribute type "js" to functions + if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string') { + val = et2_compileLegacyJS(val, this, this.instanceOf(et2_inputWidget) ? this.getInputNode() : + (this.implements(et2_IDOMNode) ? this.getDOMNode() : null)); + } + this.setAttribute(key, val, false); + } + } + }; + /** + * Does specific post-processing after the widget is loaded. Most widgets should not + * need to do anything here, it should all be done before. + * + * @return {boolean|Promise} True if the widget is fully loaded, false to avoid procesing children, + * or a Promise if loading is not actually finished (eg. waiting for AJAX) + * + * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise} + */ + et2_widget.prototype.doLoadingFinished = function () { + return true; + }; + /** + * The egw function returns the instance of the client side api belonging + * to this widget tree. The api instance can be set in the "container" + * widget using the setApiInstance function. + */ + et2_widget.prototype.egw = function () { + // The _egw property is not set + if (typeof this._egw === 'undefined') { + if (this._parent != null) { + return this._parent.egw(); + } + // Get the window this object belongs to + var wnd = null; + if (this.implements(et2_IDOMNode)) { + var node = this.getDOMNode(); + if (node && node.ownerDocument) { + wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; + } + } + // If we're the root object, return the phpgwapi API instance + return egw('phpgwapi', wnd); + } + return this._egw; + }; + /** + * Sets the client side api instance. It can be retrieved by the widget tree + * by using the "egw()" function. + * + * @param {egw} _egw egw object to set + */ + et2_widget.prototype.setApiInstance = function (_egw) { + this._egw = _egw; + }; + /** + * Sets all array manager objects - this function can be used to set the + * root array managers of the container object. + * + * @param {object} _mgrs + */ + et2_widget.prototype.setArrayMgrs = function (_mgrs) { + this._mgrs = et2_cloneObject(_mgrs); + }; + /** + * Returns an associative array containing the top-most array managers. + * + * @param _mgrs is used internally and should not be supplied. + */ + et2_widget.prototype.getArrayMgrs = function (_mgrs) { + if (typeof _mgrs == "undefined") { + _mgrs = {}; + } + // Add all managers of this object to the result, if they have not already + // been set in the result + for (var key in this._mgrs) { + if (typeof _mgrs[key] == "undefined") { + _mgrs[key] = this._mgrs[key]; + } + } + // Recursively applies this function to the parent widget + if (this._parent) { + this._parent.getArrayMgrs(_mgrs); + } + return _mgrs; + }; + /** + * Sets the array manager for the given part + * + * @param {string} _part which array mgr to set + * @param {object} _mgr + */ + et2_widget.prototype.setArrayMgr = function (_part, _mgr) { + this._mgrs[_part] = _mgr; + }; + /** + * Returns the array manager object for the given part + * + * @param {string} _part name of array mgr to return + */ + et2_widget.prototype.getArrayMgr = function (_part) { + if (this._mgrs && typeof this._mgrs[_part] != "undefined") { + return this._mgrs[_part]; + } + else if (this._parent) { + return this._parent.getArrayMgr(_part); + } + return null; + }; + /** + * Checks whether a namespace exists for this element in the content array. + * If yes, an own perspective of the content array is created. If not, the + * parent content manager is used. + */ + et2_widget.prototype.checkCreateNamespace = function () { + // Get the content manager + var mgrs = this.getArrayMgrs(); + for (var key in mgrs) { + var mgr = mgrs[key]; + // Get the original content manager if we have already created a + // perspective for this node + if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) { + mgr = mgr.parentMgr; + } + // Check whether the manager has a namespace for the id of this object + var entry = mgr.getEntry(this.id); + if (typeof entry === 'object' && entry !== null || this.id) { + // The content manager has an own node for this object, so + // create an own perspective. + this._mgrs[key] = mgr.openPerspective(this, this.id); + } + else { + // The current content manager does not have an own namespace for + // this element, so use the content manager of the parent. + delete (this._mgrs[key]); + } + } + }; + /** + * Sets the instance manager object (of type etemplate2, see etemplate2.js) + * + * @param {etemplate2} _inst + */ + et2_widget.prototype.setInstanceManager = function (_inst) { + this._inst = _inst; + }; + /** + * Returns the instance manager + * + * @return {etemplate2} + */ + et2_widget.prototype.getInstanceManager = function () { + if (this._inst != null) { + return this._inst; + } + else if (this._parent) { + return this._parent.getInstanceManager(); + } + return null; + }; + /** + * Returns the path into the data array. By default, array manager takes care of + * this, but some extensions need to override this + */ + et2_widget.prototype.getPath = function () { + var path = this.getArrayMgr("content").getPath(); + // Prevent namespaced widgets with value from going an extra layer deep + if (this.id && this.createNamespace && path[path.length - 1] == this.id) + path.pop(); + return path; + }; + /** + * The implements function can be used to check whether the object + * implements the given interface. + * + * As TypeScript can not (yet) check if an objects implements an interface on runtime, + * we currently implements with each interface a function called 'implements_'+interfacename + * to be able to check here. + * + * @param _iface name of interface to check + */ + et2_widget.prototype.implements = function (_iface_name) { + if (typeof window['implements_' + _iface_name] === 'function' && + window['implements_' + _iface_name](this)) { + return true; + } + return false; + }; + /** + * Check if object is an instance of a class or implements an interface (specified by the interfaces name) + * + * @param _class_or_interfacename class(-name) or string with name of interface + */ + et2_widget.prototype.instanceOf = function (_class_or_interfacename) { + if (typeof _class_or_interfacename === 'string') { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; + }; + et2_widget._attributes = { + "id": { + "name": "ID", + "type": "string", + "description": "Unique identifier of the widget" + }, + "no_lang": { + "name": "No translation", + "type": "boolean", + "default": false, + "description": "If true, no translations are made for this widget" + }, + /** + * Ignore the "span" property by default - it is read by the grid and + * other widgets. + */ + "span": { + "ignore": true + }, + /** + * Ignore the "type" tag - it is read by the "createElementFromNode" + * function and passed as second parameter of the widget constructor + */ + "type": { + "name": "Widget type", + "type": "string", + "ignore": true, + "description": "What kind of widget this is" + }, + /** + * Ignore the readonly tag by default - its also read by the + * "createElementFromNode" function. + */ + "readonly": { + "ignore": true + }, + /** + * Widget's attributes + */ + attributes: { + "name": "Widget attributes", + "type": "any", + "ignore": true, + "description": "Object of widget attributes" + } + }; + return et2_widget; +}(et2_core_inheritance_1.ClassWithAttributes)); +exports.et2_widget = et2_widget; diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts new file mode 100644 index 0000000000..bfbf62716f --- /dev/null +++ b/api/js/etemplate/et2_core_widget.ts @@ -0,0 +1,1066 @@ +/** + * EGroupware eTemplate2 - JS Widget base class + * + * @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 + */ + +/*egw:uses + jsapi.egw; + et2_core_xml; + et2_core_common; + et2_core_inheritance; + et2_core_arrayMgr; +*/ + +import { ClassWithAttributes } from './et2_core_inheritance'; + +/** + * The registry contains all XML tag names and the corresponding widget + * constructor. + */ +var et2_registry = {}; + +/** + * Registers the widget class defined by the given constructor and associates it + * with the types in the _types array. + * + * @param {function} _constructor constructor + * @param {array} _types widget types _constructor wants to register for + */ +export function et2_register_widget(_constructor, _types) +{ + "use strict"; + + // Iterate over all given types and register those + for (var i = 0; i < _types.length; i++) + { + var type = _types[i].toLowerCase(); + + // Check whether a widget has already been registered for one of the + // types. + if (et2_registry[type]) + { + egw.debug("warn", "Widget class registered for " + type + + " will be overwritten."); + } + + et2_registry[type] = _constructor; + } +} + +/** + * Creates a widget registered for the given tag-name. If "readonly" is listed + * inside the attributes, et2_createWidget will try to use the "_ro" type of the + * widget. + * + * @param _name is the name of the widget with which it is registered. If the + * widget is not found, an et2_placeholder will be created. + * @param _attrs is an associative array with attributes. If not passed, it will + * default to an empty object. + * @param _parent is the parent to which the element will be attached. If _parent + * is not passed, it will default to null. Then you have to attach the element + * to a parent using the addChild or insertChild method. + */ +export function et2_createWidget(_name : string, _attrs : object, _parent? : any) : et2_widget +{ + "use strict"; + + if (typeof _attrs == "undefined") + { + _attrs = {}; + } + + if (typeof _attrs != "object") + { + _attrs = {}; + } + + if (typeof _parent == "undefined") + { + _parent = null; + } + + // Parse the "readonly" and "type" flag for this element here, as they + // determine which constructor is used + var nodeName = _attrs["type"] = _name; + var readonly = _attrs["readonly"] = + typeof _attrs["readonly"] == "undefined" ? false : _attrs["readonly"]; + + // Get the constructor - if the widget is readonly, use the special "_ro" + // constructor if it is available + var constructor = typeof et2_registry[nodeName] == "undefined" ? + et2_placeholder : et2_registry[nodeName]; + if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined") + { + constructor = et2_registry[nodeName + "_ro"]; + } + + // Do an sanity check for the attributes + constructor.prototype.generateAttributeSet(_attrs); + + // Create the new widget and return it + return new constructor(_parent, _attrs); +} + +export interface WidgetConfig { + type?: string; + readonly?: boolean; + width?: number; + [propName: string]: any; +} + +/** + * The et2 widget base class. + * + * @augments ClassWithAttributes + */ +export class et2_widget extends ClassWithAttributes +{ + static readonly _attributes: any = { + "id": { + "name": "ID", + "type": "string", + "description": "Unique identifier of the widget" + }, + + "no_lang": { + "name": "No translation", + "type": "boolean", + "default": false, + "description": "If true, no translations are made for this widget" + }, + + /** + * Ignore the "span" property by default - it is read by the grid and + * other widgets. + */ + "span": { + "ignore": true + }, + + /** + * Ignore the "type" tag - it is read by the "createElementFromNode" + * function and passed as second parameter of the widget constructor + */ + "type": { + "name": "Widget type", + "type": "string", + "ignore": true, + "description": "What kind of widget this is" + }, + + /** + * Ignore the readonly tag by default - its also read by the + * "createElementFromNode" function. + */ + "readonly": { + "ignore": true + }, + + /** + * Widget's attributes + */ + attributes: { + "name": "Widget attributes", + "type": "any", + "ignore": true, + "description": "Object of widget attributes" + } + } + + // Set the legacyOptions array to the names of the properties the "options" + // attribute defines. + legacyOptions: []; + + private _type: string; + id: string; + supportedWidgetClasses : any[]; + options: WidgetConfig; + readonly: boolean; + + /** + * Set this variable to true if this widget can have namespaces + */ + createNamespace: false; + + /** + * Widget constructor + * + * To implement the attributes inheritance and overriding each extending class/widget needs to call: + * + * super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + * + * @param _parent is the parent object from the XML tree which contains this + * object. The default constructor always adds the new instance to the + * children list of the given parent object. _parent may be NULL. + * @param _attrs is an associative array of attributes. + * @param _child attributes object from the child + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { + super(); // because we in the top of the widget hierarchy + this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {}); + + // Check whether all attributes are available + if (typeof _parent == "undefined") { + _parent = null; + } + + if (typeof _attrs == "undefined") { + _attrs = {}; + } + + if (_attrs.attributes) { + jQuery.extend(_attrs, _attrs.attributes); + } + // Initialize all important parameters + this._mgrs = {}; + this._inst = null; + this._children = []; + this._type = _attrs["type"]; + this.id = _attrs["id"]; + + // Add this widget to the given parent widget + if (_parent != null) { + _parent.addChild(this); + } + + // The supported widget classes array defines a whitelist for all widget + // classes or interfaces child widgets have to support. + this.supportedWidgetClasses = [et2_widget]; + + if (_attrs["id"]) { + // Create a namespace for this object + if (this.createNamespace) { + this.checkCreateNamespace(); + } + } + + if (this.id) { + //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']'); + } + + // Add all attributes hidden in the content arrays to the attributes + // parameter + this.transformAttributes(_attrs); + + // Create a local copy of the options object + this.options = et2_cloneObject(_attrs); + } + + /** + * The destroy function destroys all children of the widget, removes itself + * from the parents children list. + * In all classes derrived from et2_widget ALWAYS override the destroy + * function and remove ALL references to other objects. Also remember to + * unbind ANY event this widget created and to remove all DOM-Nodes it + * created. + */ + destroy() + { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].free(); + } + + // Remove this element from the parent, if it exists + if (typeof this._parent != "undefined" && this._parent !== null) { + this._parent.removeChild(this); + } + + // Free the array managers if they belong to this widget + for (var key in this._mgrs) { + if (this._mgrs[key] && this._mgrs[key].owner == this) { + this._mgrs[key].free(); + } + } + } + + /** + * Creates a copy of this widget. The parameters given are passed to the + * constructor of the copied object. If the parameters are omitted, _parent + * is defaulted to null + * + * @param {et2_widget} _parent parent to set for clone, default null + */ + clone(_parent) + { + // Default _parent to null + if (typeof _parent == "undefined") { + _parent = null; + } + + // Create the copy + var copy = new (this.constructor)(_parent, this.options); + + // Assign this element to the copy + copy.assign(this); + + return copy; + } + + assign(_obj) + { + if (typeof _obj._children == "undefined") { + this.egw().debug("log", "Foo!"); + } + + // Create a clone of all child elements of the given object + for (var i = 0; i < _obj._children.length; i++) { + _obj._children[i].clone(this); + } + + // Copy a reference to the content array manager + this.setArrayMgrs(_obj.mgrs); + } + + private _parent: et2_widget; + + /** + * Returns the parent widget of this widget + */ + getParent() + { + return this._parent; + } + + private _children = []; + + /** + * Returns the list of children of this widget. + */ + getChildren() + { + return this._children; + } + + /** + * Returns the base widget + */ + getRoot() + { + if (this._parent != null) { + return this._parent.getRoot(); + } else { + return this; + } + } + + /** + * Inserts an child at the end of the list. + * + * @param _node is the node which should be added. It has to be an instance + * of et2_widget + */ + addChild(_node) + { + this.insertChild(_node, this._children.length); + } + + /** + * Inserts a child at the given index. + * + * @param _node is the node which should be added. It has to be an instance + * of et2_widget + * @param _idx is the position at which the element should be added. + */ + insertChild(_node, _idx) + { + // Check whether the node is one of the supported widget classes. + if (this.isOfSupportedWidgetClass(_node)) { + // Remove the node from its original parent + if (_node._parent) { + _node._parent.removeChild(_node); + } + + _node._parent = this; + this._children.splice(_idx, 0, _node); + + if (_node.implements(et2_IDOMNode) && this.implements(et2_IDOMNode) && _node.parentNode) { + _node.detachFromDOM(); + _node.parentNode = (this).getDOMNode(_node); + _node.attachToDOM(); + } + + } else { + this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this); +// throw("Widget is not supported by this widget class!"); + } + } + + /** + * Removes the child but does not destroy it. + * + * @param {et2_widget} _node child to remove + */ + removeChild(_node) + { + // Retrieve the child from the child list + var idx = this._children.indexOf(_node); + + if (idx >= 0) { + // This element is no longer parent of the child + _node._parent = null; + + this._children.splice(idx, 1); + } + } + + /** + * Searches an element by id in the tree, descending into the child levels. + * + * @param _id is the id you're searching for + */ + getWidgetById(_id) + { + if (this.id == _id) { + return this; + } + if (!this._children) return null; + + for (var i = 0; i < this._children.length; i++) { + var elem = this._children[i].getWidgetById(_id); + + if (elem != null) { + return elem; + } + } + if (this.id && _id.indexOf('[') > -1 && this._children.length) { + var ids = (new et2_arrayMgr()).explodeKey(_id); + var widget = this; + for (var i = 0; i < ids.length && widget !== null; i++) { + widget = widget.getWidgetById(ids[i]); + } + return widget; + } + + return null; + } + + /** + * Function which allows iterating over the complete widget tree. + * + * @param _callback is the function which should be called for each widget + * @param _context is the context in which the function should be executed + * @param _type is an optional parameter which specifies a class/interface + * the elements have to be instanceOf. + */ + iterateOver(_callback, _context, _type) + { + if (typeof _type == "undefined") { + _type = et2_widget; + } + + if (this.isInTree() && this.instanceOf(_type)) { + _callback.call(_context, this); + } + + for (var i = 0; i < this._children.length; i++) { + this._children[i].iterateOver(_callback, _context, _type); + } + } + + /** + * Returns true if the widget currently resides in the visible part of the + * widget tree. E.g. Templates which have been cloned are not in the visible + * part of the widget tree. + * + * @param _sender + * @param {boolean} _vis can be used by widgets overwriting this function - simply + * write + * return this._super(inTree); + * when calling this function the _vis parameter does not have to be supplied. + */ + isInTree(_sender?, _vis? : boolean) + { + if (typeof _vis == "undefined") { + _vis = true; + } + + if (this._parent) { + return _vis && this._parent.isInTree(this); + } + + return _vis; + } + + isOfSupportedWidgetClass(_obj) + { + for (var i = 0; i < this.supportedWidgetClasses.length; i++) { + if (_obj instanceof this.supportedWidgetClasses[i]) { + return true; + } + } + return false; + } + + /** + * The parseXMLAttrs function takes an XML DOM attributes object + * and adds the given attributes to the _target associative array. This + * function also parses the legacyOptions. + * + * @param _attrsObj is the XML DOM attributes object + * @param {object} _target is the object to which the attributes should be written. + * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute + */ + parseXMLAttrs(_attrsObj, _target, _proto) + { + // Check whether the attributes object is really existing, if not abort + if (typeof _attrsObj == "undefined") { + return; + } + + // Iterate over the given attributes and parse them + var mgr = this.getArrayMgr("content"); + for (var i = 0; i < _attrsObj.length; i++) { + var attrName = _attrsObj[i].name; + var attrValue = _attrsObj[i].value; + + // Special handling for the legacy options + if (attrName == "options" && _proto.legacyOptions.length > 0) { + // Check for modifications on legacy options here. Normal modifications + // are handled in widget constructor, but it's too late for legacy options then + if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) { + var mod = this.getArrayMgr("modifications").getEntry(_target.id); + if (typeof mod.options != "undefined") attrValue = _attrsObj[i].value = mod.options; + } + // expand legacyOptions with content + if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { + attrValue = mgr.expandName(attrValue); + } + + // Parse the legacy options (as a string, other types not allowed) + var splitted = et2_csvSplit(attrValue + ""); + + for (var j = 0; j < splitted.length && j < _proto.legacyOptions.length; j++) { + // Blank = not set, unless there's more legacy options provided after + if (splitted[j].trim().length === 0 && _proto.legacyOptions.length >= splitted.length) continue; + + // Check to make sure we don't overwrite a current option with a legacy option + if (typeof _target[_proto.legacyOptions[j]] === "undefined") { + attrValue = splitted[j]; + + /** + If more legacy options than expected, stuff them all in the last legacy option + Some legacy options take a comma separated list. + */ + if (j == _proto.legacyOptions.length - 1 && splitted.length > _proto.legacyOptions.length) { + attrValue = splitted.slice(j); + } + + var attr = _proto.attributes[_proto.legacyOptions[j]]; + + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } else if (typeof attrValue != "object") { + attrValue = mgr.expandName(attrValue); + } + _target[_proto.legacyOptions[j]] = attrValue; + } + } + } else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { + // do NOT overwrite already evaluated readonly attribute + } else { + if (mgr != null && typeof _proto.attributes[attrName] != "undefined") { + var attr = _proto.attributes[attrName]; + + // If the attribute is marked as boolean, parse the + // expression as bool expression. + if (attr.type == "boolean") { + attrValue = mgr.parseBoolExpression(attrValue); + } else { + attrValue = mgr.expandName(attrValue); + } + } + + // Set the attribute + _target[attrName] = attrValue; + } + } + } + + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + + // Apply the content of the modifications array + if (this.id) { + if (typeof this.id != "string") { + console.log(this.id); + } + + if (this.getArrayMgr("modifications")) { + var data = this.getArrayMgr("modifications").getEntry(this.id); + + // Check for already inside namespace + if (this.createNamespace && this.getArrayMgr("modifications").perspectiveData.owner == this) { + data = this.getArrayMgr("modifications").data; + } + if (typeof data === 'object') { + for (var key in data) { + _attrs[key] = data[key]; + } + } + } + } + + // Translate the attributes + for (var key in _attrs) { + if (_attrs[key] && typeof this.attributes[key] != "undefined") { + if (this.attributes[key].translate === true || + (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"])) { + _attrs[key] = this.egw().lang(_attrs[key]); + } + } + } + } + + /** + * Create a et2_widget from an XML node. + * + * First the type and attributes are read from the node. Then the readonly & modifications + * arrays are checked for changes specific to the loaded data. Then the appropriate + * constructor is called. After the constructor returns, the widget has a chance to + * further initialize itself from the XML node when the widget's loadFromXML() method + * is called with the node. + * + * @param _node XML node to read + * + * @return et2_widget + */ + createElementFromNode(_node) + { + var attributes = {}; + + // Parse the "readonly" and "type" flag for this element here, as they + // determine which constructor is used + var _nodeName = attributes["type"] = _node.getAttribute("type") ? + _node.getAttribute("type") : _node.nodeName.toLowerCase(); + var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ? + this.getArrayMgr("readonlys").isReadOnly( + _node.getAttribute("id"), _node.getAttribute("readonly"), + typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false; + + // Check to see if modifications change type + var modifications = this.getArrayMgr("modifications"); + if (modifications && _node.getAttribute("id")) { + var entry = modifications.getEntry(_node.getAttribute("id")); + if (entry == null) { + // Try again, but skip the fancy stuff + // TODO: Figure out why the getEntry() call doesn't always work + var entry = modifications.data[_node.getAttribute("id")]; + if (entry) { + this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry); + } else { + // Try the root, in case a namespace got missed + var entry = modifications.getRoot().getEntry(_node.getAttribute("id")); + } + } + if (entry && entry.type && typeof entry.type === 'string') { + _nodeName = attributes["type"] = entry.type; + } + entry = null; + } + + // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"), + // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs! + if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0) { + _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName); + } + + // Get the constructor - if the widget is readonly, use the special "_ro" + // constructor if it is available + var constructor = typeof et2_registry[_nodeName] == "undefined" ? + et2_placeholder : et2_registry[_nodeName]; + if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined") { + constructor = et2_registry[_nodeName + "_ro"]; + } + + // Parse the attributes from the given XML attributes object + this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); + + // Do an sanity check for the attributes + constructor.prototype.generateAttributeSet(attributes); + + // Creates the new widget, passes this widget as an instance and + // passes the widgetType. Then it goes on loading the XML for it. + var widget = new constructor(this, attributes); + + // Load the widget itself from XML + widget.loadFromXML(_node); + + return widget; + } + + /** + * Loads the widget tree from an XML node + * + * @param _node xml node + */ + loadFromXML(_node) + { + // Load the child nodes. + for (var i = 0; i < _node.childNodes.length; i++) { + var node = _node.childNodes[i]; + var widgetType = node.nodeName.toLowerCase(); + + if (widgetType == "#comment") { + continue; + } + + if (widgetType == "#text") { + if (node.data.replace(/^\s+|\s+$/g, '')) { + this.loadContent(node.data); + } + continue; + } + + // Create the new element + this.createElementFromNode(node); + } + } + + /** + * Called whenever textNodes are loaded from the XML tree + * + * @param _content + */ + loadContent(_content) + { + } + + /** + * Called when loading the widget (sub-tree) is finished. First when this + * function is called, the DOM-Tree is created. loadingFinished is + * recursively called for all child elements. Do not directly override this + * function but the doLoadingFinished function which is executed before + * descending deeper into the DOM-Tree. + * + * Some widgets (template) do not load immediately because they request + * additional resources via AJAX. They will return a Deferred Promise object. + * If you call loadingFinished(promises) after creating such a widget + * programmatically, you might need to wait for it to fully complete its + * loading before proceeding. In that case use: + * + * var promises = []; + * widget.loadingFinished(promises); + * jQuery.when.apply(null, promises).done( doneCallback ); + * + * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred} + * + * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed. + */ + loadingFinished(promises) + { + // Call all availble setters + this.initAttributes(this.options); + + // Make sure promises is defined to avoid errors. + // We'll warn (below) if programmer should have passed it. + if (typeof promises == "undefined") { + promises = []; + var warn_if_deferred = true; + } + + var loadChildren = function () { + // Descend recursively into the tree + for (var i = 0; i < this._children.length; i++) { + try { + this._children[i].loadingFinished(promises); + } catch (e) { + egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack); + } + } + }; + + var result = this.doLoadingFinished(); + if (typeof result == "boolean" && result) { + // Simple widget finishes nicely + loadChildren.apply(this, arguments); + } else if (typeof result == "object" && result.done) { + // Warn if list was not provided + if (warn_if_deferred) { + // Might not be a problem, but if you need the widget to be really loaded, it could be + egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); + } + // Widget is waiting. Add to the list + promises.push(result); + // Fihish loading when it's finished + result.done(jQuery.proxy(loadChildren, this)); + } + } + + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + * + * Overwritten here to compile legacy JS code in attributes of type "js" + * + * @param {object} _attrs + */ + initAttributes(_attrs) + { + for (var key in _attrs) { + if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { + var val = _attrs[key]; + // compile string values of attribute type "js" to functions + if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string') { + val = et2_compileLegacyJS(val, this, + this.instanceOf(et2_inputWidget) ? (this).getInputNode() : + (this.implements(et2_IDOMNode) ? (this).getDOMNode() : null)); + } + this.setAttribute(key, val, false); + } + } + } + + /** + * Does specific post-processing after the widget is loaded. Most widgets should not + * need to do anything here, it should all be done before. + * + * @return {boolean|Promise} True if the widget is fully loaded, false to avoid procesing children, + * or a Promise if loading is not actually finished (eg. waiting for AJAX) + * + * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise} + */ + doLoadingFinished() : JQueryPromise | boolean + { + return true; + } + + private _egw: object; + + /** + * The egw function returns the instance of the client side api belonging + * to this widget tree. The api instance can be set in the "container" + * widget using the setApiInstance function. + */ + egw() + { + // The _egw property is not set + if (typeof this._egw === 'undefined') { + if (this._parent != null) { + return this._parent.egw(); + } + + // Get the window this object belongs to + var wnd = null; + if (this.implements(et2_IDOMNode)) { + var node = (this).getDOMNode(); + if (node && node.ownerDocument) { + wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; + } + } + + // If we're the root object, return the phpgwapi API instance + return egw('phpgwapi', wnd); + } + + return this._egw; + } + + /** + * Sets the client side api instance. It can be retrieved by the widget tree + * by using the "egw()" function. + * + * @param {egw} _egw egw object to set + */ + setApiInstance(_egw) + { + this._egw = _egw; + } + + private _mgrs = {}; + + /** + * Sets all array manager objects - this function can be used to set the + * root array managers of the container object. + * + * @param {object} _mgrs + */ + setArrayMgrs(_mgrs) + { + this._mgrs = et2_cloneObject(_mgrs); + } + + /** + * Returns an associative array containing the top-most array managers. + * + * @param _mgrs is used internally and should not be supplied. + */ + getArrayMgrs(_mgrs? : object) + { + if (typeof _mgrs == "undefined") { + _mgrs = {}; + } + + // Add all managers of this object to the result, if they have not already + // been set in the result + for (var key in this._mgrs) { + if (typeof _mgrs[key] == "undefined") { + _mgrs[key] = this._mgrs[key]; + } + } + + // Recursively applies this function to the parent widget + if (this._parent) { + this._parent.getArrayMgrs(_mgrs); + } + + return _mgrs; + } + + /** + * Sets the array manager for the given part + * + * @param {string} _part which array mgr to set + * @param {object} _mgr + */ + setArrayMgr(_part : string, _mgr) + { + this._mgrs[_part] = _mgr; + } + + /** + * Returns the array manager object for the given part + * + * @param {string} _part name of array mgr to return + */ + getArrayMgr(_part : string) + { + if (this._mgrs && typeof this._mgrs[_part] != "undefined") { + return this._mgrs[_part]; + } else if (this._parent) { + return this._parent.getArrayMgr(_part); + } + + return null; + } + + /** + * Checks whether a namespace exists for this element in the content array. + * If yes, an own perspective of the content array is created. If not, the + * parent content manager is used. + */ + checkCreateNamespace() + { + // Get the content manager + var mgrs = this.getArrayMgrs(); + + for (var key in mgrs) { + var mgr = mgrs[key]; + + // Get the original content manager if we have already created a + // perspective for this node + if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) { + mgr = mgr.parentMgr; + } + + // Check whether the manager has a namespace for the id of this object + var entry = mgr.getEntry(this.id); + if (typeof entry === 'object' && entry !== null || this.id) { + // The content manager has an own node for this object, so + // create an own perspective. + this._mgrs[key] = mgr.openPerspective(this, this.id); + } else { + // The current content manager does not have an own namespace for + // this element, so use the content manager of the parent. + delete (this._mgrs[key]); + } + } + } + + /** + * This is used and therefore it we can not (yet) make it private + * + * @deprecated use this.getInstanceMgr() + */ + _inst = null; + + /** + * Sets the instance manager object (of type etemplate2, see etemplate2.js) + * + * @param {etemplate2} _inst + */ + setInstanceManager(_inst) + { + this._inst = _inst; + } + + /** + * Returns the instance manager + * + * @return {etemplate2} + */ + getInstanceManager() + { + if (this._inst != null) { + return this._inst; + } else if (this._parent) { + return this._parent.getInstanceManager(); + } + + return null; + } + + /** + * Returns the path into the data array. By default, array manager takes care of + * this, but some extensions need to override this + */ + getPath() + { + var path = this.getArrayMgr("content").getPath(); + + // Prevent namespaced widgets with value from going an extra layer deep + if (this.id && this.createNamespace && path[path.length - 1] == this.id) path.pop(); + + return path; + } + + /** + * The implements function can be used to check whether the object + * implements the given interface. + * + * As TypeScript can not (yet) check if an objects implements an interface on runtime, + * we currently implements with each interface a function called 'implements_'+interfacename + * to be able to check here. + * + * @param _iface name of interface to check + */ + implements (_iface_name : string) + { + if (typeof window['implements_'+_iface_name] === 'function' && + window['implements_'+_iface_name](this)) + { + return true + } + return false; + } + + /** + * Check if object is an instance of a class or implements an interface (specified by the interfaces name) + * + * @param _class_or_interfacename class(-name) or string with name of interface + */ + instanceOf(_class_or_interfacename: any) : boolean + { + if (typeof _class_or_interfacename === 'string') + { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; + } +} diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index c6616da7c3..f94c8e206a 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -3,18 +3,21 @@ declare module eT2 } declare var etemplate2 : any; -declare var et2_DOMWidget : any; +declare class et2_widget{} +declare class et2_DOMWidget{} +declare class et2_inputWidget{ + getInputNode() : HTMLElement +} declare var et2_surroundingsMgr : any; declare var et2_arrayMgr : any; declare var et2_readonlysArrayMgr : any; declare var et2_baseWidget : any; declare var et2_container : any; declare var et2_placeholder : any; -declare var et2_validTypes : any; -declare var et2_typeDefaults : any; -declare var et2_no_init : any; +declare var et2_validTypes : string[]; +declare var et2_typeDefaults : object; +//declare const et2_no_init : object; declare var et2_editableWidget : any; -declare var et2_inputWidget : any; declare var et2_IDOMNode : any; declare var et2_IInput : any; declare var et2_IResizeable : any; @@ -23,8 +26,7 @@ declare var et2_ISubmitListener : any; declare var et2_IDetachedDOM : any; declare var et2_IPrint : any; declare var et2_valueWidget : any; -declare var et2_registry : any; -declare var et2_widget : any; +declare var et2_registry : {}; declare var et2_dataview : any; declare var et2_dataview_controller : any; declare var et2_dataview_selectionManager : any; @@ -145,4 +147,5 @@ declare var et2_vfsSelect : any; declare var et2_video : any; declare var et2_IExposable : any; declare function et2_createWidget(type : string, params : {}, parent? : any) : any; -declare function nm_action(_action : {}, _senders : [], _target : any, _ids? : any) : void; \ No newline at end of file +declare function nm_action(_action : {}, _senders : [], _target : any, _ids? : any) : void; +declare function et2_compileLegacyJS(_code : string, _widget : et2_widget, _context? : HTMLElement) : Function; \ No newline at end of file diff --git a/api/js/jsapi/app_base.js b/api/js/jsapi/app_base.js index 1e9085c17b..488bfcbaa2 100644 --- a/api/js/jsapi/app_base.js +++ b/api/js/jsapi/app_base.js @@ -201,6 +201,28 @@ var AppJS = (function(){ "use strict"; return Class.extend( }, + /** + * Push method receives push notification about updates to entries from the application + * + * It can use the extra _data parameter to determine if the client has read access to + * the entry - if an update of the list is necessary. + * + * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {string} _app application name + * @param {(string|number)} _id id of entry to refresh or null + * @param {mixed} _data eg. owner or responsible to decide if update is necessary + * @returns {undefined} + */ + push: function(_type, _app, _id, _data) + { + + }, + /** * Open an entry. * diff --git a/api/js/jsapi/egw_global.d.ts b/api/js/jsapi/egw_global.d.ts index 3e26f06d7f..0e8acbf0fd 100644 --- a/api/js/jsapi/egw_global.d.ts +++ b/api/js/jsapi/egw_global.d.ts @@ -10,6 +10,7 @@ declare var egw : any; declare var app : {classes: any}; declare var egw_globalObjectManager : any; declare var framework : any; +declare var egw_LAB : any; declare var mailvelope : any; From 7141ac3fd6c8ecd2dc6fc6fad4bb6b535f4c3445 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 10:36:02 +0100 Subject: [PATCH 04/61] move implements and instanceOf methods to inheritance --- api/js/etemplate/et2_core_inheritance.js | 28 +++++++++++++++++++ api/js/etemplate/et2_core_inheritance.ts | 34 ++++++++++++++++++++++++ api/js/etemplate/et2_core_widget.js | 28 ------------------- api/js/etemplate/et2_core_widget.ts | 34 ------------------------ 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/api/js/etemplate/et2_core_inheritance.js b/api/js/etemplate/et2_core_inheritance.js index 360bb03454..ae478d5a64 100644 --- a/api/js/etemplate/et2_core_inheritance.js +++ b/api/js/etemplate/et2_core_inheritance.js @@ -158,6 +158,34 @@ var ClassWithAttributes = /** @class */ (function () { } return attributes; }; + /** + * The implements function can be used to check whether the object + * implements the given interface. + * + * As TypeScript can not (yet) check if an objects implements an interface on runtime, + * we currently implements with each interface a function called 'implements_'+interfacename + * to be able to check here. + * + * @param _iface name of interface to check + */ + ClassWithAttributes.prototype.implements = function (_iface_name) { + if (typeof window['implements_' + _iface_name] === 'function' && + window['implements_' + _iface_name](this)) { + return true; + } + return false; + }; + /** + * Check if object is an instance of a class or implements an interface (specified by the interfaces name) + * + * @param _class_or_interfacename class(-name) or string with name of interface + */ + ClassWithAttributes.prototype.instanceOf = function (_class_or_interfacename) { + if (typeof _class_or_interfacename === 'string') { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; + }; return ClassWithAttributes; }()); exports.ClassWithAttributes = ClassWithAttributes; diff --git a/api/js/etemplate/et2_core_inheritance.ts b/api/js/etemplate/et2_core_inheritance.ts index 34c891ce30..33acaf6e2e 100644 --- a/api/js/etemplate/et2_core_inheritance.ts +++ b/api/js/etemplate/et2_core_inheritance.ts @@ -214,4 +214,38 @@ export class ClassWithAttributes return attributes; } + + /** + * The implements function can be used to check whether the object + * implements the given interface. + * + * As TypeScript can not (yet) check if an objects implements an interface on runtime, + * we currently implements with each interface a function called 'implements_'+interfacename + * to be able to check here. + * + * @param _iface name of interface to check + */ + implements (_iface_name : string) + { + if (typeof window['implements_'+_iface_name] === 'function' && + window['implements_'+_iface_name](this)) + { + return true + } + return false; + } + + /** + * Check if object is an instance of a class or implements an interface (specified by the interfaces name) + * + * @param _class_or_interfacename class(-name) or string with name of interface + */ + instanceOf(_class_or_interfacename: any) : boolean + { + if (typeof _class_or_interfacename === 'string') + { + return this.implements(_class_or_interfacename); + } + return this instanceof _class_or_interfacename; + } } \ No newline at end of file diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js index b07590740b..58939b7cc7 100644 --- a/api/js/etemplate/et2_core_widget.js +++ b/api/js/etemplate/et2_core_widget.js @@ -822,34 +822,6 @@ var et2_widget = /** @class */ (function (_super) { path.pop(); return path; }; - /** - * The implements function can be used to check whether the object - * implements the given interface. - * - * As TypeScript can not (yet) check if an objects implements an interface on runtime, - * we currently implements with each interface a function called 'implements_'+interfacename - * to be able to check here. - * - * @param _iface name of interface to check - */ - et2_widget.prototype.implements = function (_iface_name) { - if (typeof window['implements_' + _iface_name] === 'function' && - window['implements_' + _iface_name](this)) { - return true; - } - return false; - }; - /** - * Check if object is an instance of a class or implements an interface (specified by the interfaces name) - * - * @param _class_or_interfacename class(-name) or string with name of interface - */ - et2_widget.prototype.instanceOf = function (_class_or_interfacename) { - if (typeof _class_or_interfacename === 'string') { - return this.implements(_class_or_interfacename); - } - return this instanceof _class_or_interfacename; - }; et2_widget._attributes = { "id": { "name": "ID", diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index bfbf62716f..3cd04294e2 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -1029,38 +1029,4 @@ export class et2_widget extends ClassWithAttributes return path; } - - /** - * The implements function can be used to check whether the object - * implements the given interface. - * - * As TypeScript can not (yet) check if an objects implements an interface on runtime, - * we currently implements with each interface a function called 'implements_'+interfacename - * to be able to check here. - * - * @param _iface name of interface to check - */ - implements (_iface_name : string) - { - if (typeof window['implements_'+_iface_name] === 'function' && - window['implements_'+_iface_name](this)) - { - return true - } - return false; - } - - /** - * Check if object is an instance of a class or implements an interface (specified by the interfaces name) - * - * @param _class_or_interfacename class(-name) or string with name of interface - */ - instanceOf(_class_or_interfacename: any) : boolean - { - if (typeof _class_or_interfacename === 'string') - { - return this.implements(_class_or_interfacename); - } - return this instanceof _class_or_interfacename; - } } From e8d6f41e052bfae069bf11028ab3c7f0072b3e4b Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 11:47:49 +0100 Subject: [PATCH 05/61] get et2_DOMWidget to TypeScript --- api/js/etemplate/et2_core_DOMWidget.js | 144 +++++++++++---------- api/js/etemplate/et2_core_DOMWidget.ts | 168 ++++++++++++++----------- api/js/etemplate/et2_core_widget.js | 10 +- api/js/etemplate/et2_core_widget.ts | 14 ++- api/js/etemplate/et2_types.d.ts | 6 +- 5 files changed, 198 insertions(+), 144 deletions(-) diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js index 41cb7f5fec..5bd1f816de 100644 --- a/api/js/etemplate/et2_core_DOMWidget.js +++ b/api/js/etemplate/et2_core_DOMWidget.js @@ -31,7 +31,7 @@ var et2_core_inheritance_1 = require("./et2_core_inheritance"); require("./et2_core_interfaces"); require("./et2_core_common"); var et2_core_widget_1 = require("./et2_core_widget"); -require("../egw_action/egw_action.js"); +var egw_action_js_1 = require("../egw_action/egw_action.js"); /** * Abstract widget class which can be inserted into the DOM. All widget classes * deriving from this class have to care about implementing the "getDOMNode" @@ -39,8 +39,8 @@ require("../egw_action/egw_action.js"); * * @augments et2_widget */ -var et2_DOMWidget = /** @class */ (function (_super_1) { - __extends(et2_DOMWidget, _super_1); +var et2_DOMWidget = /** @class */ (function (_super) { + __extends(et2_DOMWidget, _super); /** * When the DOMWidget is initialized, it grabs the DOM-Node of the parent * object (if available) and passes it to its own "createDOMNode" function @@ -50,13 +50,13 @@ var et2_DOMWidget = /** @class */ (function (_super_1) { function et2_DOMWidget(_parent, _attrs, _child) { var _this = // Call the inherited constructor - _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})) || this; + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})) || this; _this.parentNode = null; + _this.disabled = false; _this._attachSet = { "node": null, "parent": null }; - _this.disabled = false; _this._surroundingsMgr = null; return _this; } @@ -69,7 +69,7 @@ var et2_DOMWidget = /** @class */ (function (_super_1) { this.parentNode = null; this._attachSet = {}; if (this._actionManager) { - var app_om = egw_getObjectManager(this.egw().getAppName(), false, 1); + var app_om = egw_action_js_1.egw_getObjectManager(this.egw().getAppName(), false, 1); if (app_om) { var om = app_om.getObjectById(this.id); if (om) @@ -79,10 +79,10 @@ var et2_DOMWidget = /** @class */ (function (_super_1) { this._actionManager = null; } if (this._surroundingsMgr) { - this._surroundingsMgr.free(); + this._surroundingsMgr.destroy(); this._surroundingsMgr = null; } - this._super(); + _super.prototype.destroy.call(this); }; /** * Attaches the container node of this widget to the DOM-Tree @@ -178,32 +178,39 @@ var et2_DOMWidget = /** @class */ (function (_super_1) { et2_DOMWidget.prototype.get_tab_info = function () { var parent = this; do { - parent = parent._parent; - } while (parent !== this.getRoot() && parent._type !== 'tabbox'); + parent = parent.getParent(); + } while (parent !== this.getRoot() && parent.getType() !== 'tabbox'); // No tab if (parent === this.getRoot()) { return null; } + var tabbox = parent; // Find the tab index - for (var i = 0; i < parent.tabData.length; i++) { + for (var i = 0; i < tabbox.tabData.length; i++) { // Find the tab by DOM heritage - if (parent.tabData[i].contentDiv.has(this.div).length) { - return parent.tabData[i]; + // @ts-ignore + if (tabbox.tabData[i].contentDiv.has(this.div).length) { + return tabbox.tabData[i]; } } // On a tab, but we couldn't find it by DOM nodes Maybe tab template is // not loaded yet. Try checking IDs. var template = this; do { - template = template._parent; - } while (template !== parent && template._type !== 'template'); - for (var i = parent.tabData.length - 1; i >= 0; i--) { - if (template && template.id && template.id === parent.tabData[i].id) { - return parent.tabData[i]; + template = template.getParent(); + // @ts-ignore + } while (template !== tabbox && template.getType() !== 'template'); + for (var i = tabbox.tabData.length - 1; i >= 0; i--) { + if (template && template.id && template.id === tabbox.tabData[i].id) { + return tabbox.tabData[i]; } } // Fallback - return this.getParent().get_tab_info(); + var fallback = this.getParent(); + if (typeof fallback.get_tab_info === 'function') { + return fallback.get_tab_info(); + } + return null; }; /** * Set the parent DOM node of this element. Takes a wider variety of types @@ -391,7 +398,7 @@ var et2_DOMWidget = /** @class */ (function (_super_1) { } // Initialize the action manager and add some actions to it // Only look 1 level deep - var gam = egw_getActionManager(this.egw().appName, true, 1); + var gam = egw_action_js_1.egw_getActionManager(this.egw().appName, true, 1); if (typeof this._actionManager != "object") { if (gam.getActionById(this.getInstanceManager().uniqueId, 1) !== null) { gam = gam.getActionById(this.getInstanceManager().uniqueId, 1); @@ -440,15 +447,15 @@ var et2_DOMWidget = /** @class */ (function (_super_1) { */ et2_DOMWidget.prototype._link_actions = function (actions) { // Get the top level element for the tree - var objectManager = egw_getAppObjectManager(true); + var objectManager = egw_action_js_1.egw_getAppObjectManager(true); var widget_object = objectManager.getObjectById(this.id); if (widget_object == null) { // Add a new container to the object manager which will hold the widget // objects - widget_object = objectManager.insertObject(false, new egwActionObject(this.id, objectManager, new et2_action_object_impl(this), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); + widget_object = objectManager.insertObject(false, new egw_action_js_1.egwActionObject(this.id, objectManager, (new et2_action_object_impl(this)).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); } else { - widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); + widget_object.setAOI((new et2_action_object_impl(this, this.getDOMNode())).getAOI()); } // Delete all old objects widget_object.clear(); @@ -531,28 +538,26 @@ var et2_DOMWidget = /** @class */ (function (_super_1) { /** * The surroundings manager class allows to append or prepend elements around * an widget node. - * - * @augments Class */ -var et2_surroundingsMgr = /** @class */ (function (_super_1) { - __extends(et2_surroundingsMgr, _super_1); - function et2_surroundingsMgr() { - return _super_1 !== null && _super_1.apply(this, arguments) || this; - } +var et2_surroundingsMgr = /** @class */ (function (_super) { + __extends(et2_surroundingsMgr, _super); /** * Constructor * * @memberOf et2_surroundingsMgr * @param _widget */ - et2_surroundingsMgr.prototype.init = function (_widget) { - this.widget = _widget; - this._widgetContainer = null; - this._widgetSurroundings = []; - this._widgetPlaceholder = null; - this._widgetNode = null; - this._ownPlaceholder = true; - }; + function et2_surroundingsMgr(_widget) { + var _this = _super.call(this) || this; + _this._widgetContainer = null; + _this._widgetSurroundings = []; + _this._widgetPlaceholder = null; + _this._widgetNode = null; + _this._ownPlaceholder = true; + _this._surroundingsUpdated = false; + _this.widget = _widget; + return _this; + } et2_surroundingsMgr.prototype.destroy = function () { this._widgetContainer = null; this._widgetSurroundings = null; @@ -703,33 +708,38 @@ var et2_surroundingsMgr = /** @class */ (function (_super_1) { * @param {Object} node * */ -function et2_action_object_impl(widget, node) { - var aoi = new egwActionObjectInterface(); - var objectNode = node; - aoi.getWidget = function () { - return widget; +var et2_action_object_impl = /** @class */ (function () { + function et2_action_object_impl(_widget, _node) { + var widget = _widget; + var objectNode = _node; + this.aoi = new egw_action_js_1.egwActionObjectInterface(); + this.aoi.getWidget = function () { + return widget; + }; + this.aoi.doGetDOMNode = function () { + return objectNode ? objectNode : widget.getDOMNode(); + }; + // _outerCall may be used to determine, whether the state change has been + // evoked from the outside and the stateChangeCallback has to be called + // or not. + this.aoi.doSetState = function (_state, _outerCall) { + }; + // The doTiggerEvent function may be overritten by the aoi if it wants to + // support certain action implementation specific events like EGW_AI_DRAG_OVER + // or EGW_AI_DRAG_OUT + this.aoi.doTriggerEvent = function (_event, _data) { + switch (_event) { + case egw_action_js_1.EGW_AI_DRAG_OVER: + jQuery(this.node).addClass("ui-state-active"); + break; + case egw_action_js_1.EGW_AI_DRAG_OUT: + jQuery(this.node).removeClass("ui-state-active"); + break; + } + }; + } + et2_action_object_impl.prototype.getAOI = function () { + return this.aoi; }; - aoi.doGetDOMNode = function () { - return objectNode ? objectNode : widget.getDOMNode(); - }; - // _outerCall may be used to determine, whether the state change has been - // evoked from the outside and the stateChangeCallback has to be called - // or not. - aoi.doSetState = function (_state, _outerCall) { - }; - // The doTiggerEvent function may be overritten by the aoi if it wants to - // support certain action implementation specific events like EGW_AI_DRAG_OVER - // or EGW_AI_DRAG_OUT - aoi.doTriggerEvent = function (_event, _data) { - switch (_event) { - case EGW_AI_DRAG_OVER: - jQuery(this.node).addClass("ui-state-active"); - break; - case EGW_AI_DRAG_OUT: - jQuery(this.node).removeClass("ui-state-active"); - break; - } - }; - return aoi; -} -; + return et2_action_object_impl; +}()); diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index 2197a2feff..cbdf69d55d 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -18,7 +18,11 @@ import { ClassWithAttributes } from './et2_core_inheritance'; import './et2_core_interfaces'; import './et2_core_common'; import {et2_widget, et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget"; -import '../egw_action/egw_action.js'; +import { + egw_getObjectManager, egwActionObjectInterface, + egw_getActionManager, egw_getAppObjectManager, + egwActionObject, egwAction, EGW_AI_DRAG_OVER, EGW_AI_DRAG_OUT +} from '../egw_action/egw_action.js'; /** * Abstract widget class which can be inserted into the DOM. All widget classes @@ -27,7 +31,7 @@ import '../egw_action/egw_action.js'; * * @augments et2_widget */ -class et2_DOMWidget extends et2_widget implements et2_IDOMNode +abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode { static readonly _attributes : any = { "disabled": { @@ -98,10 +102,17 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode } } - parentNode : HTMLElement; - disabled : boolean; - private _attachSet: object; + parentNode : HTMLElement = null; + disabled : boolean = false; + private _attachSet: any = { + "node": null, + "parent": null + }; private _actionManager: any; + width: number; + height: number; + dom_id: string; + overflow: string; /** * When the DOMWidget is initialized, it grabs the DOM-Node of the parent @@ -114,17 +125,11 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); - this.parentNode = null; - - this._attachSet = { - "node": null, - "parent": null - }; - - this.disabled = false; this._surroundingsMgr = null; } + abstract getDOMNode(_sender?: et2_widget): HTMLElement + /** * Detatches the node from the DOM and clears all references to the parent * node or the dom node of this widget. @@ -149,11 +154,11 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode if (this._surroundingsMgr) { - this._surroundingsMgr.free(); + this._surroundingsMgr.destroy(); this._surroundingsMgr = null; } - this._super(); + super.destroy(); } /** @@ -163,14 +168,15 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode { // Check whether the parent implements the et2_IDOMNode interface. If // yes, grab the DOM node and create our own. - if (this.getParent() && this.getParent().implements(et2_IDOMNode)) { + if (this.getParent() && this.getParent().implements(et2_IDOMNode)) + { if(this.options.parent_node) { this.set_parent_node(this.options.parent_node); } else { - this.setParentDOMNode(this.getParent().getDOMNode(this)); + this.setParentDOMNode((this.getParent()).getDOMNode(this)); } } @@ -272,11 +278,12 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode * * @returns {Object|null} Data for tab the widget is on */ - get_tab_info() { - var parent = this; + get_tab_info() : object | null + { + var parent : et2_widget = this; do { - parent = parent._parent; - } while (parent !== this.getRoot() && parent._type !== 'tabbox'); + parent = parent.getParent(); + } while (parent !== this.getRoot() && parent.getType() !== 'tabbox'); // No tab if(parent === this.getRoot()) @@ -284,30 +291,39 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode return null; } + let tabbox : et2_tabbox = parent; + // Find the tab index - for(var i = 0; i < parent.tabData.length; i++) + for(var i = 0; i < tabbox.tabData.length; i++) { // Find the tab by DOM heritage - if(parent.tabData[i].contentDiv.has(this.div).length) + // @ts-ignore + if(tabbox.tabData[i].contentDiv.has(this.div).length) { - return parent.tabData[i]; + return tabbox.tabData[i]; } } // On a tab, but we couldn't find it by DOM nodes Maybe tab template is // not loaded yet. Try checking IDs. - var template = this; + var template : et2_widget = this; do { - template = template._parent; - } while (template !== parent && template._type !== 'template'); - for(var i = parent.tabData.length - 1; i >= 0; i--) + template = template.getParent(); + // @ts-ignore + } while (template !== tabbox && template.getType() !== 'template'); + for (var i = tabbox.tabData.length - 1; i >= 0; i--) { - if(template && template.id && template.id === parent.tabData[i].id) + if (template && template.id && template.id === tabbox.tabData[i].id) { - return parent.tabData[i]; + return tabbox.tabData[i]; } } // Fallback - return this.getParent().get_tab_info(); + let fallback = this.getParent(); + if (typeof fallback.get_tab_info === 'function') + { + return fallback.get_tab_info(); + } + return null; } /** @@ -316,7 +332,8 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode * * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. */ - set_parent_node(_node) { + set_parent_node(_node) + { if(typeof _node == "string") { var parent = jQuery('#'+_node); @@ -347,7 +364,8 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode * * @param _node */ - setParentDOMNode(_node) { + setParentDOMNode(_node : HTMLElement) + { if (_node != this.parentNode) { // Detatch this element from the DOM tree @@ -615,13 +633,13 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode // Add a new container to the object manager which will hold the widget // objects widget_object = objectManager.insertObject(false, new egwActionObject( - this.id, objectManager, new et2_action_object_impl(this), + this.id, objectManager, (new et2_action_object_impl(this)).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager )); } else { - widget_object.setAOI(new et2_action_object_impl(this, this.getDOMNode())); + widget_object.setAOI((new et2_action_object_impl(this, this.getDOMNode())).getAOI()); } // Delete all old objects @@ -639,25 +657,27 @@ class et2_DOMWidget extends et2_widget implements et2_IDOMNode /** * The surroundings manager class allows to append or prepend elements around * an widget node. - * - * @augments Class */ class et2_surroundingsMgr extends ClassWithAttributes { + widget: et2_DOMWidget; + private _widgetContainer: any = null; + private _widgetSurroundings: any[] = []; + private _widgetPlaceholder: any = null; + private _widgetNode: HTMLElement = null; + private _ownPlaceholder: boolean = true; + private _surroundingsUpdated: boolean = false; + /** * Constructor * * @memberOf et2_surroundingsMgr * @param _widget */ - init(_widget) { + constructor(_widget : et2_DOMWidget) + { + super(); this.widget = _widget; - - this._widgetContainer = null; - this._widgetSurroundings = []; - this._widgetPlaceholder = null; - this._widgetNode = null; - this._ownPlaceholder = true; } destroy() { @@ -736,7 +756,7 @@ class et2_surroundingsMgr extends ClassWithAttributes } } - _rebuildContainer() { + private _rebuildContainer() { // Return if there has been no change in the "surroundings-data" if (!this._surroundingsUpdated) { @@ -847,7 +867,6 @@ class et2_surroundingsMgr extends ClassWithAttributes // Return the widget container return this._widgetContainer; } - } /** @@ -860,40 +879,47 @@ class et2_surroundingsMgr extends ClassWithAttributes * @param {Object} node * */ -function et2_action_object_impl(widget, node) +class et2_action_object_impl { - var aoi = new egwActionObjectInterface(); - var objectNode = node; + aoi : egwActionObjectInterface; - aoi.getWidget = function() { - return widget; - }; - - aoi.doGetDOMNode = function() { - return objectNode?objectNode:widget.getDOMNode(); - }; + constructor(_widget : et2_DOMWidget, _node? : HTMLElement) + { + var widget = _widget; + var objectNode = _node; + this.aoi = new egwActionObjectInterface(); + this.aoi.getWidget = function () { + return widget; + }; + this.aoi.doGetDOMNode = function () { + return objectNode ? objectNode : widget.getDOMNode(); + }; // _outerCall may be used to determine, whether the state change has been // evoked from the outside and the stateChangeCallback has to be called // or not. - aoi.doSetState = function(_state, _outerCall) { - }; + this.aoi.doSetState = function (_state, _outerCall) + { + }; // The doTiggerEvent function may be overritten by the aoi if it wants to // support certain action implementation specific events like EGW_AI_DRAG_OVER // or EGW_AI_DRAG_OUT - aoi.doTriggerEvent = function(_event, _data) { - switch(_event) + this.aoi.doTriggerEvent = function (_event, _data) { - case EGW_AI_DRAG_OVER: - jQuery(this.node).addClass("ui-state-active"); - break; - case EGW_AI_DRAG_OUT: - jQuery(this.node).removeClass("ui-state-active"); - break; - } - }; + switch (_event) { + case EGW_AI_DRAG_OVER: + jQuery(this.node).addClass("ui-state-active"); + break; + case EGW_AI_DRAG_OUT: + jQuery(this.node).removeClass("ui-state-active"); + break; + } + }; + } - - return aoi; -}; + getAOI() + { + return this.aoi; + } +} diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js index 58939b7cc7..d751d68d65 100644 --- a/api/js/etemplate/et2_core_widget.js +++ b/api/js/etemplate/et2_core_widget.js @@ -180,7 +180,7 @@ var et2_widget = /** @class */ (function (_super) { et2_widget.prototype.destroy = function () { // Call the destructor of all children for (var i = this._children.length - 1; i >= 0; i--) { - this._children[i].free(); + this._children[i].destroy(); } // Remove this element from the parent, if it exists if (typeof this._parent != "undefined" && this._parent !== null) { @@ -189,10 +189,16 @@ var et2_widget = /** @class */ (function (_super) { // Free the array managers if they belong to this widget for (var key in this._mgrs) { if (this._mgrs[key] && this._mgrs[key].owner == this) { - this._mgrs[key].free(); + this._mgrs[key].destroy(); } } }; + et2_widget.prototype.getType = function () { + return this._type; + }; + et2_widget.prototype.setType = function (_type) { + this._type = _type; + }; /** * Creates a copy of this widget. The parameters given are passed to the * constructor of the copied object. If the parameters are omitted, _parent diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index 3cd04294e2..3127c647b2 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -264,7 +264,7 @@ export class et2_widget extends ClassWithAttributes { // Call the destructor of all children for (var i = this._children.length - 1; i >= 0; i--) { - this._children[i].free(); + this._children[i].destroy(); } // Remove this element from the parent, if it exists @@ -275,11 +275,21 @@ export class et2_widget extends ClassWithAttributes // Free the array managers if they belong to this widget for (var key in this._mgrs) { if (this._mgrs[key] && this._mgrs[key].owner == this) { - this._mgrs[key].free(); + this._mgrs[key].destroy(); } } } + getType() : string + { + return this._type; + } + + setType(_type : string) + { + this._type = _type; + } + /** * Creates a copy of this widget. The parameters given are passed to the * constructor of the copied object. If the parameters are omitted, _parent diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index f94c8e206a..1b8134c7c8 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -4,7 +4,7 @@ declare module eT2 } declare var etemplate2 : any; declare class et2_widget{} -declare class et2_DOMWidget{} +declare class et2_DOMWidget extends et2_widget{} declare class et2_inputWidget{ getInputNode() : HTMLElement } @@ -117,7 +117,9 @@ declare var et2_selectbox_ro : any; declare var et2_menulist : any; declare var et2_split : any; declare var et2_styles : any; -declare var et2_tabbox : any; +declare class et2_tabbox extends et2_widget { + tabData : any; +} declare var et2_taglist : any; declare var et2_taglist_account : any; declare var et2_taglist_email : any; From a28dffd5cca3fca52a633f7a4ea7a7acde7c3952 Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 21 Jan 2020 04:15:46 -0700 Subject: [PATCH 06/61] First run at TS for valueWidget --- api/js/etemplate/et2_core_valueWidget.js | 219 +++++++++++------------ api/js/etemplate/et2_core_valueWidget.ts | 137 ++++++++++++++ 2 files changed, 244 insertions(+), 112 deletions(-) create mode 100644 api/js/etemplate/et2_core_valueWidget.ts diff --git a/api/js/etemplate/et2_core_valueWidget.js b/api/js/etemplate/et2_core_valueWidget.js index 404986cd9a..39ac9f6ad2 100644 --- a/api/js/etemplate/et2_core_valueWidget.js +++ b/api/js/etemplate/et2_core_valueWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS widget class with value attribute and auto loading * @@ -9,12 +10,26 @@ * @copyright Stylite 2011 * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; */ - +require("./et2_core_baseWidget"); +require("./et2_core_common"); /** * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces * the "value" attribute and automatically loads it from the "content" array @@ -22,111 +37,91 @@ * * @augments et2_baseWidget */ -var et2_valueWidget = (function(){ "use strict"; return et2_baseWidget.extend( -{ - attributes: { - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - }, - "value": { - "name": "Value", - "description": "The value of the widget", - "type": "rawstring", // no html-entity decoding - "default": et2_no_init - } - }, - - /** - * - * - * @memberOf et2_valueWidget - * @param _attrs - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - if (this.id) - { - // Set the value for this element - var contentMgr = this.getArrayMgr("content"); - if (contentMgr != null) { - var val = contentMgr.getEntry(this.id,false,true); - if (val !== null) - { - _attrs["value"] = val; - } - } - // Check for already inside namespace - if(this.createNamespace && this.getArrayMgr("content").perspectiveData.owner == this) - { - _attrs["value"] = this.getArrayMgr("content").data; - } - - } - }, - - set_label: function(_value) { - // Abort if ther was no change in the label - if (_value == this.label) - { - return; - } - - if (_value) - { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) - { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - - // Clear the label container. - this._labelContainer.empty(); - - // Create the placeholder element and set it - var ph = document.createElement("span"); - this.getSurroundings().setWidgetPlaceholder(ph); - - // Split the label at the "%s" - var parts = et2_csvSplit(_value, 2, "%s"); - - // Update the content of the label container - for (var i = 0; i < parts.length; i++) - { - if (parts[i]) - { - this._labelContainer.append(document.createTextNode(parts[i])); - } - if (i == 0) - { - this._labelContainer.append(ph); - } - } - - // add class if label is empty - this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); - } - else - { - // Delete the labelContainer from the surroundings object - if (this._labelContainer) - { - this.getSurroundings().removeDOMNode(this._labelContainer[0]); - } - this._labelContainer = null; - } - - // Update the surroundings in order to reflect the change in the label - this.getSurroundings().update(); - - // Copy the given value - this.label = _value; - } -});}).call(this); - +var et2_valueWidget = /** @class */ (function (_super) { + __extends(et2_valueWidget, _super); + function et2_valueWidget() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * + * + * @memberOf et2_valueWidget + * @param _attrs + */ + et2_valueWidget.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + if (this.id) { + // Set the value for this element + var contentMgr = this.getArrayMgr("content"); + if (contentMgr != null) { + var val = contentMgr.getEntry(this.id, false, true); + if (val !== null) { + _attrs["value"] = val; + } + } + // Check for already inside namespace + if (this.createNamespace && this.getArrayMgr("content").perspectiveData.owner == this) { + _attrs["value"] = this.getArrayMgr("content").data; + } + } + }; + et2_valueWidget.prototype.set_label = function (_value) { + // Abort if there was no change in the label + if (_value == this.label) { + return; + } + if (_value) { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + // Clear the label container. + this._labelContainer.empty(); + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + // Split the label at the "%s" + var parts = et2_csvSplit(_value, 2, "%s"); + // Update the content of the label container + for (var i = 0; i < parts.length; i++) { + if (parts[i]) { + this._labelContainer.append(document.createTextNode(parts[i])); + } + if (i == 0) { + this._labelContainer.append(ph); + } + } + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); + } + else { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + // Update the surroundings in order to reflect the change in the label + this.getSurroundings().update(); + // Copy the given value + this.label = _value; + }; + et2_valueWidget._attributes = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "description": "The value of the widget", + "type": "rawstring", + "default": et2_no_init + } + }; + return et2_valueWidget; +}(et2_baseWidget)); diff --git a/api/js/etemplate/et2_core_valueWidget.ts b/api/js/etemplate/et2_core_valueWidget.ts new file mode 100644 index 0000000000..842fc01f24 --- /dev/null +++ b/api/js/etemplate/et2_core_valueWidget.ts @@ -0,0 +1,137 @@ +/** + * EGroupware eTemplate2 - JS widget class with value attribute and auto loading + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; +*/ + +import './et2_core_baseWidget' +import './et2_core_common'; + +/** + * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces + * the "value" attribute and automatically loads it from the "content" array + * after loading from XML. + * + * @augments et2_baseWidget + */ +class et2_valueWidget extends et2_baseWidget +{ + static readonly _attributes : any = { + "label": { + "name": "Label", + "default": "", + "type": "string", + "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", + "translate": true + }, + "value": { + "name": "Value", + "description": "The value of the widget", + "type": "rawstring", // no html-entity decoding + "default": et2_no_init + } + }; + + /** + * + * + * @memberOf et2_valueWidget + * @param _attrs + */ + transformAttributes(_attrs : object) + { + super.transformAttributes(_attrs); + + if (this.id) + { + // Set the value for this element + var contentMgr = this.getArrayMgr("content"); + if (contentMgr != null) { + var val = contentMgr.getEntry(this.id,false,true); + if (val !== null) + { + _attrs["value"] = val; + } + } + // Check for already inside namespace + if(this.createNamespace && this.getArrayMgr("content").perspectiveData.owner == this) + { + _attrs["value"] = this.getArrayMgr("content").data; + } + + } + } + + set_label(_value) + { + // Abort if there was no change in the label + if (_value == this.label) + { + return; + } + + if (_value) + { + // Create the label container if it didn't exist yet + if (this._labelContainer == null) + { + this._labelContainer = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.getSurroundings().insertDOMNode(this._labelContainer[0]); + } + + // Clear the label container. + this._labelContainer.empty(); + + // Create the placeholder element and set it + var ph = document.createElement("span"); + this.getSurroundings().setWidgetPlaceholder(ph); + + // Split the label at the "%s" + var parts = et2_csvSplit(_value, 2, "%s"); + + // Update the content of the label container + for (var i = 0; i < parts.length; i++) + { + if (parts[i]) + { + this._labelContainer.append(document.createTextNode(parts[i])); + } + if (i == 0) + { + this._labelContainer.append(ph); + } + } + + // add class if label is empty + this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); + } + else + { + // Delete the labelContainer from the surroundings object + if (this._labelContainer) + { + this.getSurroundings().removeDOMNode(this._labelContainer[0]); + } + this._labelContainer = null; + } + + // Update the surroundings in order to reflect the change in the label + this.getSurroundings().update(); + + // Copy the given value + this.label = _value; + } +} + From 630797f12707a067e8bd03c020822c934128a98c Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 12:22:22 +0100 Subject: [PATCH 07/61] baseWidget in TS --- api/js/etemplate/et2_core_DOMWidget.js | 1 + api/js/etemplate/et2_core_DOMWidget.ts | 6 +- api/js/etemplate/et2_core_baseWidget.js | 2 - api/js/etemplate/et2_core_baseWidget.ts | 448 ++++++++++++++++++++++++ api/js/etemplate/et2_core_widget.ts | 4 +- api/js/jsapi/egw_global.d.ts | 1 + 6 files changed, 455 insertions(+), 7 deletions(-) create mode 100644 api/js/etemplate/et2_core_baseWidget.ts diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js index 5bd1f816de..40cf9e3f2f 100644 --- a/api/js/etemplate/et2_core_DOMWidget.js +++ b/api/js/etemplate/et2_core_DOMWidget.js @@ -535,6 +535,7 @@ var et2_DOMWidget = /** @class */ (function (_super) { }; return et2_DOMWidget; }(et2_core_widget_1.et2_widget)); +exports.et2_DOMWidget = et2_DOMWidget; /** * The surroundings manager class allows to append or prepend elements around * an widget node. diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index cbdf69d55d..21da82fac4 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -31,7 +31,7 @@ import { * * @augments et2_widget */ -abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode +export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode { static readonly _attributes : any = { "disabled": { @@ -187,8 +187,8 @@ abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode * Detaches the widget from the DOM tree, if it had been attached to the * DOM-Tree using the attachToDOM method. */ - detachFromDOM() { - + detachFromDOM() + { if (this._attachSet.node && this._attachSet.parent) { // Remove the current node from the parent node diff --git a/api/js/etemplate/et2_core_baseWidget.js b/api/js/etemplate/et2_core_baseWidget.js index 017a3e13ba..f453fa1076 100644 --- a/api/js/etemplate/et2_core_baseWidget.js +++ b/api/js/etemplate/et2_core_baseWidget.js @@ -6,8 +6,6 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ /*egw:uses diff --git a/api/js/etemplate/et2_core_baseWidget.ts b/api/js/etemplate/et2_core_baseWidget.ts new file mode 100644 index 0000000000..f10cb4a1e8 --- /dev/null +++ b/api/js/etemplate/et2_core_baseWidget.ts @@ -0,0 +1,448 @@ +/** + * EGroupware eTemplate2 - JS Widget base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + lib/tooltip; + et2_core_DOMWidget; +*/ + +//import { ClassWithAttributes } from './et2_core_inheritance'; +import './et2_core_interfaces'; +import './et2_core_common'; +import { et2_DOMWidget } from './et2_core_DOMWidget'; +import { ClassWithAttributes } from "./et2_core_inheritance"; +import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from "./et2_core_widget"; + +/** + * Class which manages the DOM node itself. The simpleWidget class is derrived + * from et2_DOMWidget and implements the getDOMNode function. A setDOMNode + * function is provided, which attatches the given node to the DOM if possible. + * + * @augments et2_DOMWidget + */ +class et2_baseWidget extends et2_DOMWidget implements et2_IAligned +{ + static readonly _attributes: any = { + "statustext": { + "name": "Tooltip", + "type": "string", + "description": "Tooltip which is shown for this element", + "translate": true + }, + "statustext_html": { + "name": "Tooltip is html", + "type": "boolean", + "description": "Flag to allow html content in tooltip", + "default": false + }, + "align": { + "name": "Align", + "type": "string", + "default": "left", + "description": "Position of this element in the parent hbox" + }, + "onclick": { + "name": "onclick", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the element is clicked." + } + } + + align: string = 'left'; + node: HTMLElement = null; + statustext: string = ''; + private _messageDiv: JQuery = null; + private _tooltipElem: JQuery = null; + onclick: any; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + } + + destroy() + { + super.destroy(); + + this.node = null; + this._messageDiv = null; + } + + /** + * The setMessage function can be used to attach a small message box to the + * widget. This is e.g. used to display validation errors or success messages + * + * @param _text is the text which should be displayed as a message + * @param _type is an css class which is attached to the message box. + * Currently available are "hint", "success" and "validation_error", defaults + * to "hint" + * @param _floating if true, the object will be in one row with the element, + * defaults to true + * @param _prepend if set, the message is displayed behind the widget node + * instead of before. Defaults to false. + */ + showMessage(_text, _type, _floating, _prepend) + { + // Preset the parameters + if (typeof _type == "undefined") + { + _type = "hint"; + } + + if (typeof _floating == "undefined") + { + _floating = true; + } + + if (typeof _prepend == "undefined") + { + _prepend = false; + } + + var surr = this.getSurroundings(); + + // Remove the message div from the surroundings before creating a new + // one + this.hideMessage(false, true); + + // Create the message div and add it to the "surroundings" manager + this._messageDiv = jQuery(document.createElement("div")) + .addClass("message") + .addClass(_type) + .addClass(_floating ? "floating" : "") + .text(_text.valueOf() + ""); + + // Decide whether to prepend or append the div + if (_prepend) + { + surr.prependDOMNode(this._messageDiv[0]); + } + else + { + surr.appendDOMNode(this._messageDiv[0]); + } + + surr.update(); + } + + /** + * The hideMessage function can be used to hide a previously shown message. + * + * @param _fade if true, the message div will fade out, otherwise the message + * div is removed immediately. Defaults to true. + * @param _noUpdate is used internally to prevent an update of the surroundings + * manager. + */ + hideMessage(_fade, _noUpdate) + { + if (typeof _fade == "undefined") + { + _fade = true; + } + + if (typeof _noUpdate == "undefined") + { + _noUpdate = false; + } + + // Remove the message from the surroundings manager and remove the + // reference to it + if (this._messageDiv != null) + { + var surr = this.getSurroundings(); + var self = this; + var messageDiv = this._messageDiv; + self._messageDiv = null; + + var _done = function() { + surr.removeDOMNode(messageDiv[0]); + + // Update the surroundings manager + if (!_noUpdate) + { + surr.update(); + } + }; + + // Either fade out or directly call the function which removes the div + if (_fade) + { + messageDiv.fadeOut("fast", _done); + } + else + { + _done(); + } + } + } + + detachFromDOM() + { + // Detach this node from the tooltip node + if (this._tooltipElem) + { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + + // Remove the binding to the click handler + if (this.node) + { + jQuery(this.node).unbind("click.et2_baseWidget"); + } + + return super.detachFromDOM(); + } + + attachToDOM() + { + let ret = super.attachToDOM(); + + // Add the binding for the click handler + if (this.node) + { + jQuery(this.node).bind("click.et2_baseWidget", this, function(e) { + return e.data.click.call(e.data, e, this); + }); + if (typeof this.onclick == 'function') jQuery(this.node).addClass('et2_clickable'); + } + + // Update the statustext + this.set_statustext(this.statustext); + + return ret; + } + + setDOMNode(_node) + { + if (_node != this.node) + { + // Deatch the old node from the DOM + this.detachFromDOM(); + + // Set the new DOM-Node + this.node = _node; + + // Attatch the DOM-Node to the tree + return this.attachToDOM(); + } + + return false; + } + + getDOMNode(_sender?: et2_widget) + { + return this.node; + } + + getTooltipElement() + { + return this.getDOMNode(this); + } + + /** + * Click handler calling custom handler set via onclick attribute to this.onclick + * + * @param _ev + * @returns + */ + click(_ev) { + if(typeof this.onclick == 'function') + { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.splice(1, 0, this); + + return this.onclick.apply(this, args); + } + + return true; + } + + set_statustext(_value) { + // Tooltip should not be shown in mobile view + if (egwIsMobile()) return; + // Don't execute the code below, if no tooltip will be attached/detached + if (_value == "" && !this._tooltipElem) + { + return; + } + + // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} + if (_value.indexOf('{') !== -1) + { + var egw = this.egw(); + _value = _value.replace(/{([^}]+)}/g, function(str,p1) + { + return egw.lang(p1); + }); + } + + this.statustext = _value; + + //Get the domnode the tooltip should be attached to + var elem = jQuery(this.getTooltipElement()); + + if (elem) + { + //If a tooltip is already attached to the element, remove it first + if (this._tooltipElem) + { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + + if (_value && _value != '') + { + this.egw().tooltipBind(elem, _value, this.options.statustext_html); + this._tooltipElem = elem; + } + } + } + + set_align(_value) + { + this.align = _value; + } + + get_align() + { + return this.align; + } +} + +/** + * Simple container object + */ +class et2_container extends et2_baseWidget +{ + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + } + + /** + * The destroy function destroys all children of the widget, removes itself + * from the parents children list. + * Overriden to not try to remove self from parent, as that's not possible. + */ + destroy() + { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) + { + this._children[i].destroy(); + } + + // Free the array managers if they belong to this widget + for (var key in this._mgrs) + { + if (this._mgrs[key] && this._mgrs[key].owner == this) + { + this._mgrs[key].destroy(); + } + } + } +} + +/** + * Container object for not-yet supported widgets + * + * @augments et2_baseWidget + */ +class et2_placeholder extends et2_baseWidget implements et2_IDetachedDOM +{ + /** + * he attrNodes object will hold the DOM nodes which represent the + * values of this object + */ + attrNodes: {}; + visible: boolean = false; + placeDiv: JQuery; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + this.attrNodes = {}; + + // Create the placeholder div + this.placeDiv = jQuery(document.createElement("span")) + .addClass("et2_placeholder"); + + var headerNode = jQuery(document.createElement("span")) + .text(this.getType() || "") + .addClass("et2_caption") + .appendTo(this.placeDiv); + + var attrsCntr = jQuery(document.createElement("span")) + .appendTo(this.placeDiv) + .hide(); + + headerNode.click(this, function(e) { + e.data.visible = !e.data.visible; + if (e.data.visible) + { + attrsCntr.show(); + } + else + { + attrsCntr.hide(); + } + }); + + for (var key in this.options) + { + if (typeof this.options[key] != "undefined") + { + if (typeof this.attrNodes[key] == "undefined") + { + this.attrNodes[key] = jQuery(document.createElement("span")) + .addClass("et2_attr"); + attrsCntr.append(this.attrNodes[key]); + } + + this.attrNodes[key].text(key + "=" + this.options[key]); + } + } + + this.setDOMNode(this.placeDiv[0]); + } + + getDetachedAttributes(_attrs) + { + _attrs.push("value"); + } + + getDetachedNodes() + { + return [this.placeDiv[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.placeDiv = jQuery(_nodes[0]); + } +} + diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index 3127c647b2..d7d3f94475 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -338,7 +338,7 @@ export class et2_widget extends ClassWithAttributes return this._parent; } - private _children = []; + protected _children = []; /** * Returns the list of children of this widget. @@ -893,7 +893,7 @@ export class et2_widget extends ClassWithAttributes this._egw = _egw; } - private _mgrs = {}; + protected _mgrs = {}; /** * Sets all array manager objects - this function can be used to set the diff --git a/api/js/jsapi/egw_global.d.ts b/api/js/jsapi/egw_global.d.ts index 0e8acbf0fd..67862ce828 100644 --- a/api/js/jsapi/egw_global.d.ts +++ b/api/js/jsapi/egw_global.d.ts @@ -11,6 +11,7 @@ declare var app : {classes: any}; declare var egw_globalObjectManager : any; declare var framework : any; declare var egw_LAB : any; +declare function egwIsMobile() : boolean; declare var mailvelope : any; From 372d439087cbef2c65e40b15dd74ee0a48f7bfd6 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 12:48:48 +0100 Subject: [PATCH 08/61] some fixes for valueWidget --- api/js/etemplate/et2_core_baseWidget.js | 732 ++++++++++------------- api/js/etemplate/et2_core_baseWidget.ts | 2 +- api/js/etemplate/et2_core_valueWidget.js | 10 +- api/js/etemplate/et2_core_valueWidget.ts | 9 +- api/js/etemplate/et2_types.d.ts | 4 +- 5 files changed, 347 insertions(+), 410 deletions(-) diff --git a/api/js/etemplate/et2_core_baseWidget.js b/api/js/etemplate/et2_core_baseWidget.js index f453fa1076..690264cadf 100644 --- a/api/js/etemplate/et2_core_baseWidget.js +++ b/api/js/etemplate/et2_core_baseWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Widget base class * @@ -7,13 +8,30 @@ * @link http://www.egroupware.org * @author Andreas Stöckel */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - lib/tooltip; - et2_core_DOMWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + lib/tooltip; + et2_core_DOMWidget; */ - +//import { ClassWithAttributes } from './et2_core_inheritance'; +require("./et2_core_interfaces"); +require("./et2_core_common"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which manages the DOM node itself. The simpleWidget class is derrived * from et2_DOMWidget and implements the getDOMNode function. A setDOMNode @@ -21,407 +39,319 @@ * * @augments et2_DOMWidget */ -var et2_baseWidget = (function(){ "use strict"; return et2_DOMWidget.extend(et2_IAligned, -{ - attributes: { - "statustext": { - "name": "Tooltip", - "type": "string", - "description": "Tooltip which is shown for this element", - "translate": true - }, - "statustext_html": { - "name": "Tooltip is html", - "type": "boolean", - "description": "Flag to allow html content in tooltip", - "default": false - }, - "align": { - "name": "Align", - "type": "string", - "default": "left", - "description": "Position of this element in the parent hbox" - }, - "onclick": { - "name": "onclick", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the element is clicked." - } - }, - - /** - * Constructor - * - * @memberOf et2BaseWidget - */ - init: function() { - this.align = "left"; - - this._super.apply(this, arguments); - - this.node = null; - this.statustext = ""; - this._messageDiv = null; - this._tooltipElem = null; - }, - - destroy: function() { - this._super.apply(this, arguments); - - this.node = null; - this._messageDiv = null; - }, - - /** - * The setMessage function can be used to attach a small message box to the - * widget. This is e.g. used to display validation errors or success messages - * - * @param _text is the text which should be displayed as a message - * @param _type is an css class which is attached to the message box. - * Currently available are "hint", "success" and "validation_error", defaults - * to "hint" - * @param _floating if true, the object will be in one row with the element, - * defaults to true - * @param _prepend if set, the message is displayed behind the widget node - * instead of before. Defaults to false. - */ - showMessage: function(_text, _type, _floating, _prepend) { - - // Preset the parameters - if (typeof _type == "undefined") - { - _type = "hint"; - } - - if (typeof _floating == "undefined") - { - _floating = true; - } - - if (typeof _prepend == "undefined") - { - _prepend = false; - } - - var surr = this.getSurroundings(); - - // Remove the message div from the surroundings before creating a new - // one - this.hideMessage(false, true); - - // Create the message div and add it to the "surroundings" manager - this._messageDiv = jQuery(document.createElement("div")) - .addClass("message") - .addClass(_type) - .addClass(_floating ? "floating" : "") - .text(_text.valueOf() + ""); - - // Decide whether to prepend or append the div - if (_prepend) - { - surr.prependDOMNode(this._messageDiv[0]); - } - else - { - surr.appendDOMNode(this._messageDiv[0]); - } - - surr.update(); - }, - - /** - * The hideMessage function can be used to hide a previously shown message. - * - * @param _fade if true, the message div will fade out, otherwise the message - * div is removed immediately. Defaults to true. - * @param _noUpdate is used internally to prevent an update of the surroundings - * manager. - */ - hideMessage: function(_fade, _noUpdate) { - if (typeof _fade == "undefined") - { - _fade = true; - } - - if (typeof _noUpdate == "undefined") - { - _noUpdate = false; - } - - // Remove the message from the surroundings manager and remove the - // reference to it - if (this._messageDiv != null) - { - var surr = this.getSurroundings(); - var self = this; - var messageDiv = this._messageDiv; - self._messageDiv = null; - - var _done = function() { - surr.removeDOMNode(messageDiv[0]); - - // Update the surroundings manager - if (!_noUpdate) - { - surr.update(); - } - }; - - // Either fade out or directly call the function which removes the div - if (_fade) - { - messageDiv.fadeOut("fast", _done); - } - else - { - _done(); - } - } - }, - - detachFromDOM: function() { - // Detach this node from the tooltip node - if (this._tooltipElem) - { - this.egw().tooltipUnbind(this._tooltipElem); - this._tooltipElem = null; - } - - // Remove the binding to the click handler - if (this.node) - { - jQuery(this.node).unbind("click.et2_baseWidget"); - } - - this._super.apply(this, arguments); - }, - - attachToDOM: function() { - this._super.apply(this, arguments); - - // Add the binding for the click handler - if (this.node) - { - jQuery(this.node).bind("click.et2_baseWidget", this, function(e) { - return e.data.click.call(e.data, e, this); - }); - if (typeof this.onclick == 'function') jQuery(this.node).addClass('et2_clickable'); - } - - // Update the statustext - this.set_statustext(this.statustext); - }, - - setDOMNode: function(_node) { - if (_node != this.node) - { - // Deatch the old node from the DOM - this.detachFromDOM(); - - // Set the new DOM-Node - this.node = _node; - - // Attatch the DOM-Node to the tree - return this.attachToDOM(); - } - - return false; - }, - - getDOMNode: function() { - return this.node; - }, - - getTooltipElement: function() { - return this.getDOMNode(this); - }, - - /** - * Click handler calling custom handler set via onclick attribute to this.onclick - * - * @param _ev - * @returns - */ - click: function(_ev) { - if(typeof this.onclick == 'function') - { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.splice(1, 0, this); - - return this.onclick.apply(this, args); - } - - return true; - }, - - set_statustext: function(_value) { - // Tooltip should not be shown in mobile view - if (egwIsMobile()) return; - // Don't execute the code below, if no tooltip will be attached/detached - if (_value == "" && !this._tooltipElem) - { - return; - } - - // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} - if (_value.indexOf('{') !== -1) - { - var egw = this.egw(); - _value = _value.replace(/{([^}]+)}/g, function(str,p1) - { - return egw.lang(p1); - }); - } - - this.statustext = _value; - - //Get the domnode the tooltip should be attached to - var elem = jQuery(this.getTooltipElement()); - - if (elem) - { - //If a tooltip is already attached to the element, remove it first - if (this._tooltipElem) - { - this.egw().tooltipUnbind(this._tooltipElem); - this._tooltipElem = null; - } - - if (_value && _value != '') - { - this.egw().tooltipBind(elem, _value, this.options.statustext_html); - this._tooltipElem = elem; - } - } - }, - - set_align: function(_value) { - this.align = _value; - }, - - get_align: function(_value) { - return this.align; - } - -});}).call(this); - +var et2_baseWidget = /** @class */ (function (_super) { + __extends(et2_baseWidget, _super); + /** + * Constructor + */ + function et2_baseWidget(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.align = 'left'; + _this.node = null; + _this.statustext = ''; + _this._messageDiv = null; + _this._tooltipElem = null; + return _this; + } + et2_baseWidget.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.node = null; + this._messageDiv = null; + }; + /** + * The setMessage function can be used to attach a small message box to the + * widget. This is e.g. used to display validation errors or success messages + * + * @param _text is the text which should be displayed as a message + * @param _type is an css class which is attached to the message box. + * Currently available are "hint", "success" and "validation_error", defaults + * to "hint" + * @param _floating if true, the object will be in one row with the element, + * defaults to true + * @param _prepend if set, the message is displayed behind the widget node + * instead of before. Defaults to false. + */ + et2_baseWidget.prototype.showMessage = function (_text, _type, _floating, _prepend) { + // Preset the parameters + if (typeof _type == "undefined") { + _type = "hint"; + } + if (typeof _floating == "undefined") { + _floating = true; + } + if (typeof _prepend == "undefined") { + _prepend = false; + } + var surr = this.getSurroundings(); + // Remove the message div from the surroundings before creating a new + // one + this.hideMessage(false, true); + // Create the message div and add it to the "surroundings" manager + this._messageDiv = jQuery(document.createElement("div")) + .addClass("message") + .addClass(_type) + .addClass(_floating ? "floating" : "") + .text(_text.valueOf() + ""); + // Decide whether to prepend or append the div + if (_prepend) { + surr.prependDOMNode(this._messageDiv[0]); + } + else { + surr.appendDOMNode(this._messageDiv[0]); + } + surr.update(); + }; + /** + * The hideMessage function can be used to hide a previously shown message. + * + * @param _fade if true, the message div will fade out, otherwise the message + * div is removed immediately. Defaults to true. + * @param _noUpdate is used internally to prevent an update of the surroundings + * manager. + */ + et2_baseWidget.prototype.hideMessage = function (_fade, _noUpdate) { + if (typeof _fade == "undefined") { + _fade = true; + } + if (typeof _noUpdate == "undefined") { + _noUpdate = false; + } + // Remove the message from the surroundings manager and remove the + // reference to it + if (this._messageDiv != null) { + var surr = this.getSurroundings(); + var self = this; + var messageDiv = this._messageDiv; + self._messageDiv = null; + var _done = function () { + surr.removeDOMNode(messageDiv[0]); + // Update the surroundings manager + if (!_noUpdate) { + surr.update(); + } + }; + // Either fade out or directly call the function which removes the div + if (_fade) { + messageDiv.fadeOut("fast", _done); + } + else { + _done(); + } + } + }; + et2_baseWidget.prototype.detachFromDOM = function () { + // Detach this node from the tooltip node + if (this._tooltipElem) { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + // Remove the binding to the click handler + if (this.node) { + jQuery(this.node).unbind("click.et2_baseWidget"); + } + return _super.prototype.detachFromDOM.call(this); + }; + et2_baseWidget.prototype.attachToDOM = function () { + var ret = _super.prototype.attachToDOM.call(this); + // Add the binding for the click handler + if (this.node) { + jQuery(this.node).bind("click.et2_baseWidget", this, function (e) { + return e.data.click.call(e.data, e, this); + }); + if (typeof this.onclick == 'function') + jQuery(this.node).addClass('et2_clickable'); + } + // Update the statustext + this.set_statustext(this.statustext); + return ret; + }; + et2_baseWidget.prototype.setDOMNode = function (_node) { + if (_node != this.node) { + // Deatch the old node from the DOM + this.detachFromDOM(); + // Set the new DOM-Node + this.node = _node; + // Attatch the DOM-Node to the tree + return this.attachToDOM(); + } + return false; + }; + et2_baseWidget.prototype.getDOMNode = function (_sender) { + return this.node; + }; + et2_baseWidget.prototype.getTooltipElement = function () { + return this.getDOMNode(this); + }; + /** + * Click handler calling custom handler set via onclick attribute to this.onclick + * + * @param _ev + * @returns + */ + et2_baseWidget.prototype.click = function (_ev) { + if (typeof this.onclick == 'function') { + // Make sure function gets a reference to the widget, splice it in as 2. argument if not + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.splice(1, 0, this); + return this.onclick.apply(this, args); + } + return true; + }; + et2_baseWidget.prototype.set_statustext = function (_value) { + // Tooltip should not be shown in mobile view + if (egwIsMobile()) + return; + // Don't execute the code below, if no tooltip will be attached/detached + if (_value == "" && !this._tooltipElem) { + return; + } + // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} + if (_value.indexOf('{') !== -1) { + var egw = this.egw(); + _value = _value.replace(/{([^}]+)}/g, function (str, p1) { + return egw.lang(p1); + }); + } + this.statustext = _value; + //Get the domnode the tooltip should be attached to + var elem = jQuery(this.getTooltipElement()); + if (elem) { + //If a tooltip is already attached to the element, remove it first + if (this._tooltipElem) { + this.egw().tooltipUnbind(this._tooltipElem); + this._tooltipElem = null; + } + if (_value && _value != '') { + this.egw().tooltipBind(elem, _value, this.options.statustext_html); + this._tooltipElem = elem; + } + } + }; + et2_baseWidget.prototype.set_align = function (_value) { + this.align = _value; + }; + et2_baseWidget.prototype.get_align = function () { + return this.align; + }; + et2_baseWidget._attributes = { + "statustext": { + "name": "Tooltip", + "type": "string", + "description": "Tooltip which is shown for this element", + "translate": true + }, + "statustext_html": { + "name": "Tooltip is html", + "type": "boolean", + "description": "Flag to allow html content in tooltip", + "default": false + }, + "align": { + "name": "Align", + "type": "string", + "default": "left", + "description": "Position of this element in the parent hbox" + }, + "onclick": { + "name": "onclick", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the element is clicked." + } + }; + return et2_baseWidget; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +exports.et2_baseWidget = et2_baseWidget; /** * Simple container object - * - * @augments et2_baseWidget */ -var et2_container = (function(){ "use strict"; return et2_baseWidget.extend( -{ - /** - * Constructor - * - * @memberOf et2_container - */ - init: function() { - this._super.apply(this, arguments); - - this.setDOMNode(document.createElement("div")); - }, - - /** - * The destroy function destroys all children of the widget, removes itself - * from the parents children list. - * Overriden to not try to remove self from parent, as that's not possible. - */ - destroy: function() { - // Call the destructor of all children - for (var i = this._children.length - 1; i >= 0; i--) - { - this._children[i].free(); - } - - // Free the array managers if they belong to this widget - for (var key in this._mgrs) - { - if (this._mgrs[key] && this._mgrs[key].owner == this) - { - this._mgrs[key].free(); - } - } - } -});}).call(this); - +var et2_container = /** @class */ (function (_super) { + __extends(et2_container, _super); + /** + * Constructor + */ + function et2_container(_parent, _attrs, _child) { + // Call the inherited constructor + return _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + } + /** + * The destroy function destroys all children of the widget, removes itself + * from the parents children list. + * Overriden to not try to remove self from parent, as that's not possible. + */ + et2_container.prototype.destroy = function () { + // Call the destructor of all children + for (var i = this._children.length - 1; i >= 0; i--) { + this._children[i].destroy(); + } + // Free the array managers if they belong to this widget + for (var key in this._mgrs) { + if (this._mgrs[key] && this._mgrs[key].owner == this) { + this._mgrs[key].destroy(); + } + } + }; + return et2_container; +}(et2_baseWidget)); /** * Container object for not-yet supported widgets * * @augments et2_baseWidget */ -var et2_placeholder = (function(){ "use strict"; return et2_baseWidget.extend([et2_IDetachedDOM], -{ - /** - * Constructor - * - * @memberOf et2_placeholder - */ - init: function() { - this._super.apply(this, arguments); - - // The attrNodes object will hold the DOM nodes which represent the - // values of this object - this.attrNodes = {}; - - this.visible = false; - - // Create the placeholder div - this.placeDiv = jQuery(document.createElement("span")) - .addClass("et2_placeholder"); - - var headerNode = jQuery(document.createElement("span")) - .text(this._type || "") - .addClass("et2_caption") - .appendTo(this.placeDiv); - - var attrsCntr = jQuery(document.createElement("span")) - .appendTo(this.placeDiv) - .hide(); - - headerNode.click(this, function(e) { - e.data.visible = !e.data.visible; - if (e.data.visible) - { - attrsCntr.show(); - } - else - { - attrsCntr.hide(); - } - }); - - for (var key in this.options) - { - if (typeof this.options[key] != "undefined") - { - if (typeof this.attrNodes[key] == "undefined") - { - this.attrNodes[key] = jQuery(document.createElement("span")) - .addClass("et2_attr"); - attrsCntr.append(this.attrNodes[key]); - } - - this.attrNodes[key].text(key + "=" + this.options[key]); - } - } - - this.setDOMNode(this.placeDiv[0]); - }, - - getDetachedAttributes: function(_attrs) { - _attrs.push("value"); - }, - - getDetachedNodes: function() { - return [this.placeDiv[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) { - this.placeDiv = jQuery(_nodes[0]); - } -});}).call(this); - +var et2_placeholder = /** @class */ (function (_super) { + __extends(et2_placeholder, _super); + /** + * Constructor + */ + function et2_placeholder(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.visible = false; + _this.attrNodes = {}; + // Create the placeholder div + _this.placeDiv = jQuery(document.createElement("span")) + .addClass("et2_placeholder"); + var headerNode = jQuery(document.createElement("span")) + .text(_this.getType() || "") + .addClass("et2_caption") + .appendTo(_this.placeDiv); + var attrsCntr = jQuery(document.createElement("span")) + .appendTo(_this.placeDiv) + .hide(); + headerNode.click(_this, function (e) { + e.data.visible = !e.data.visible; + if (e.data.visible) { + attrsCntr.show(); + } + else { + attrsCntr.hide(); + } + }); + for (var key in _this.options) { + if (typeof _this.options[key] != "undefined") { + if (typeof _this.attrNodes[key] == "undefined") { + _this.attrNodes[key] = jQuery(document.createElement("span")) + .addClass("et2_attr"); + attrsCntr.append(_this.attrNodes[key]); + } + _this.attrNodes[key].text(key + "=" + _this.options[key]); + } + } + _this.setDOMNode(_this.placeDiv[0]); + return _this; + } + et2_placeholder.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value"); + }; + et2_placeholder.prototype.getDetachedNodes = function () { + return [this.placeDiv[0]]; + }; + et2_placeholder.prototype.setDetachedAttributes = function (_nodes, _values) { + this.placeDiv = jQuery(_nodes[0]); + }; + return et2_placeholder; +}(et2_baseWidget)); diff --git a/api/js/etemplate/et2_core_baseWidget.ts b/api/js/etemplate/et2_core_baseWidget.ts index f10cb4a1e8..6999728606 100644 --- a/api/js/etemplate/et2_core_baseWidget.ts +++ b/api/js/etemplate/et2_core_baseWidget.ts @@ -28,7 +28,7 @@ import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from * * @augments et2_DOMWidget */ -class et2_baseWidget extends et2_DOMWidget implements et2_IAligned +export class et2_baseWidget extends et2_DOMWidget implements et2_IAligned { static readonly _attributes: any = { "statustext": { diff --git a/api/js/etemplate/et2_core_valueWidget.js b/api/js/etemplate/et2_core_valueWidget.js index 39ac9f6ad2..6e176b4996 100644 --- a/api/js/etemplate/et2_core_valueWidget.js +++ b/api/js/etemplate/et2_core_valueWidget.js @@ -28,7 +28,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); /vendor/bower-asset/jquery/dist/jquery.js; et2_core_baseWidget; */ -require("./et2_core_baseWidget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); require("./et2_core_common"); /** * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces @@ -40,7 +40,10 @@ require("./et2_core_common"); var et2_valueWidget = /** @class */ (function (_super) { __extends(et2_valueWidget, _super); function et2_valueWidget() { - return _super !== null && _super.apply(this, arguments) || this; + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.label = ''; + _this._labelContainer = null; + return _this; } /** * @@ -124,4 +127,5 @@ var et2_valueWidget = /** @class */ (function (_super) { } }; return et2_valueWidget; -}(et2_baseWidget)); +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_valueWidget = et2_valueWidget; diff --git a/api/js/etemplate/et2_core_valueWidget.ts b/api/js/etemplate/et2_core_valueWidget.ts index 842fc01f24..f562550e48 100644 --- a/api/js/etemplate/et2_core_valueWidget.ts +++ b/api/js/etemplate/et2_core_valueWidget.ts @@ -15,7 +15,7 @@ et2_core_baseWidget; */ -import './et2_core_baseWidget' +import { et2_baseWidget } from './et2_core_baseWidget' import './et2_core_common'; /** @@ -25,7 +25,7 @@ import './et2_core_common'; * * @augments et2_baseWidget */ -class et2_valueWidget extends et2_baseWidget +export class et2_valueWidget extends et2_baseWidget { static readonly _attributes : any = { "label": { @@ -43,6 +43,9 @@ class et2_valueWidget extends et2_baseWidget } }; + label: string = ''; + private _labelContainer: JQuery = null; + /** * * @@ -73,7 +76,7 @@ class et2_valueWidget extends et2_baseWidget } } - set_label(_value) + set_label(_value : string) { // Abort if there was no change in the label if (_value == this.label) diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index 1b8134c7c8..25c966a0a5 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -5,13 +5,14 @@ declare module eT2 declare var etemplate2 : any; declare class et2_widget{} declare class et2_DOMWidget extends et2_widget{} +declare class et2_baseWidget extends et2_DOMWidget{} +declare class et2_valueWidget extends et2_baseWidget{} declare class et2_inputWidget{ getInputNode() : HTMLElement } declare var et2_surroundingsMgr : any; declare var et2_arrayMgr : any; declare var et2_readonlysArrayMgr : any; -declare var et2_baseWidget : any; declare var et2_container : any; declare var et2_placeholder : any; declare var et2_validTypes : string[]; @@ -25,7 +26,6 @@ declare var et2_IAligned : any; declare var et2_ISubmitListener : any; declare var et2_IDetachedDOM : any; declare var et2_IPrint : any; -declare var et2_valueWidget : any; declare var et2_registry : {}; declare var et2_dataview : any; declare var et2_dataview_controller : any; From 87270d97edf87ad3f1ccd7cffd500736f2761e85 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 14:18:15 +0100 Subject: [PATCH 09/61] inputWidget with TS --- api/js/etemplate/et2_core_DOMWidget.ts | 3 +- api/js/etemplate/et2_core_baseWidget.js | 1 - api/js/etemplate/et2_core_baseWidget.ts | 11 +- api/js/etemplate/et2_core_inputWidget.ts | 343 +++++++++++++++++++++++ api/js/etemplate/et2_core_valueWidget.js | 2 - api/js/etemplate/et2_core_valueWidget.ts | 4 +- api/js/etemplate/et2_types.d.ts | 7 +- 7 files changed, 356 insertions(+), 15 deletions(-) create mode 100644 api/js/etemplate/et2_core_inputWidget.ts diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index 21da82fac4..f0b5115ff0 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -215,7 +215,8 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode * attached to the tree or no parent node or no node for this widget is * defined. */ - attachToDOM() { + attachToDOM() + { // Attach the DOM node of this widget (if existing) to the new parent var node = this.getDOMNode(this); if (node && this.parentNode && diff --git a/api/js/etemplate/et2_core_baseWidget.js b/api/js/etemplate/et2_core_baseWidget.js index 690264cadf..ee23d8a281 100644 --- a/api/js/etemplate/et2_core_baseWidget.js +++ b/api/js/etemplate/et2_core_baseWidget.js @@ -27,7 +27,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); lib/tooltip; et2_core_DOMWidget; */ -//import { ClassWithAttributes } from './et2_core_inheritance'; require("./et2_core_interfaces"); require("./et2_core_common"); var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); diff --git a/api/js/etemplate/et2_core_baseWidget.ts b/api/js/etemplate/et2_core_baseWidget.ts index 6999728606..f9d020c9b7 100644 --- a/api/js/etemplate/et2_core_baseWidget.ts +++ b/api/js/etemplate/et2_core_baseWidget.ts @@ -14,7 +14,6 @@ et2_core_DOMWidget; */ -//import { ClassWithAttributes } from './et2_core_inheritance'; import './et2_core_interfaces'; import './et2_core_common'; import { et2_DOMWidget } from './et2_core_DOMWidget'; @@ -94,7 +93,7 @@ export class et2_baseWidget extends et2_DOMWidget implements et2_IAligned * @param _prepend if set, the message is displayed behind the widget node * instead of before. Defaults to false. */ - showMessage(_text, _type, _floating, _prepend) + showMessage(_text, _type?, _floating?, _prepend?) { // Preset the parameters if (typeof _type == "undefined") @@ -146,7 +145,7 @@ export class et2_baseWidget extends et2_DOMWidget implements et2_IAligned * @param _noUpdate is used internally to prevent an update of the surroundings * manager. */ - hideMessage(_fade, _noUpdate) + hideMessage(_fade? : boolean, _noUpdate? : boolean) { if (typeof _fade == "undefined") { @@ -259,7 +258,8 @@ export class et2_baseWidget extends et2_DOMWidget implements et2_IAligned * @param _ev * @returns */ - click(_ev) { + click(_ev) + { if(typeof this.onclick == 'function') { // Make sure function gets a reference to the widget, splice it in as 2. argument if not @@ -272,7 +272,8 @@ export class et2_baseWidget extends et2_DOMWidget implements et2_IAligned return true; } - set_statustext(_value) { + set_statustext(_value) + { // Tooltip should not be shown in mobile view if (egwIsMobile()) return; // Don't execute the code below, if no tooltip will be attached/detached diff --git a/api/js/etemplate/et2_core_inputWidget.ts b/api/js/etemplate/et2_core_inputWidget.ts new file mode 100644 index 0000000000..819cdbc9e0 --- /dev/null +++ b/api/js/etemplate/et2_core_inputWidget.ts @@ -0,0 +1,343 @@ +/** + * EGroupware eTemplate2 - JS Widget base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_valueWidget; +*/ + +import './et2_core_common'; +import { ClassWithAttributes } from "./et2_core_inheritance"; +import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from "./et2_core_widget"; +import { et2_DOMWidget } from './et2_core_DOMWidget' +import { et2_valueWidget } from './et2_core_valueWidget' +import './et2_types'; + +/** + * et2_inputWidget derrives from et2_simpleWidget and implements the IInput + * interface. When derriving from this class, call setDOMNode with an input + * DOMNode. + */ +export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ISubmitListener +{ + static readonly _attributes : any = { + "needed": { + "name": "Required", + "default": false, + "type": "boolean", + "description": "If required, the user must enter a value before the form can be submitted" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the value changes." + }, + "onfocus": { + "name": "onfocus", + "type": "js", + "default": et2_no_init, + "description": "JS code which get executed when wiget receives focus." + }, + "validation_error": { + "name": "Validation Error", + "type": "string", + "default": et2_no_init, + "description": "Used internally to store the validation error that came from the server." + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + readonly: { + name: "readonly", + type: "boolean", + "default": false, + description: "Does NOT allow user to enter data, just displays existing data" + } + } + + private _oldValue: any; + onchange: Function; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + // mark value as not initialised, so set_value can determine if it is necessary to trigger change event + this._oldValue = et2_no_init; + this._labelContainer = null; + } + + destroy() + { + var node = this.getInputNode(); + if (node) + { + jQuery(node).unbind("change.et2_inputWidget"); + jQuery(node).unbind("focus"); + } + + super.destroy(); + + this._labelContainer = null; + } + + /** + * Load the validation errors from the server + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + + // Check whether an validation error entry exists + if (this.id && this.getArrayMgr("validation_errors")) + { + var val = this.getArrayMgr("validation_errors").getEntry(this.id); + if (val) + { + _attrs["validation_error"] = val; + } + } + } + + attachToDOM() + { + var node = this.getInputNode(); + if (node) + { + jQuery(node) + .off('.et2_inputWidget') + .bind("change.et2_inputWidget", this, function(e) { + e.data.change.call(e.data, this); + }) + .bind("focus.et2_inputWidget", this, function(e) { + e.data.focus.call(e.data, this); + }); + } + + return super.attachToDOM(); + +// jQuery(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved +// jQuery(this.getInputNode()).validator(); + } + + detatchFromDOM() + { +// if(this.getInputNode()) { +// jQuery(this.getInputNode()).data("validator").destroy(); +// } + super.detachFromDOM(); + } + + change(_node) + { + var messages = []; + var valid = this.isValid(messages); + + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + + if (valid && this.onchange) + { + if(typeof this.onchange == 'function') + { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.push(this); + + return this.onchange.apply(this, args); + } else { + return (et2_compileLegacyJS(this.options.onchange, this, _node))(); + } + } + return valid; + } + + focus(_node) + { + if(typeof this.options.onfocus == 'function') + { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.push(this); + + return this.options.onfocus.apply(this, args); + } + } + + /** + * Set value of widget and trigger for real changes a change event + * + * First initialisation (_oldValue === et2_no_init) is NOT considered a change! + * + * @param {string} _value value to set + */ + set_value(_value) + { + var node = this.getInputNode(); + if (node) + { + jQuery(node).val(_value); + if(this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) + { + jQuery(node).change(); + } + } + this._oldValue = _value; + } + + set_id(_value) + { + this.id = _value; + this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId+'_'+this.id : _value; + + // Set the id of the _input_ node (in contrast to the default + // implementation, which sets the base node) + var node = this.getInputNode(); + if (node) + { + // Unique ID to prevent DOM collisions across multiple templates + if (_value != "") + { + node.setAttribute("id", this.dom_id); + node.setAttribute("name", _value); + } + else + { + node.removeAttribute("id"); + node.removeAttribute("name"); + } + } + } + + set_needed(_value) + { + var node = this.getInputNode(); + if (node) + { + if(_value && !this.options.readonly) { + jQuery(node).attr("required", "required"); + } else { + node.removeAttribute("required"); + } + } + } + + set_validation_error(_value) + { + var node = this.getInputNode(); + if (node) + { + if (_value === false) + { + this.hideMessage(); + jQuery(node).removeClass("invalid"); + } + else + { + this.showMessage(_value, "validation_error"); + jQuery(node).addClass("invalid"); + + // If on a tab, switch to that tab so user can see it + let widget : et2_widget = this; + while(widget.getParent() && widget.getType() != 'tabbox') + { + widget = widget.getParent(); + } + if (widget.getType() == 'tabbox') (widget).activateTab(this); + } + } + } + + /** + * Set tab index + * + * @param {number} index + */ + set_tabindex(index) + { + jQuery(this.getInputNode()).attr("tabindex", index); + } + + getInputNode() + { + return this.node; + } + + get_value() + { + return this.getValue(); + } + + getValue() + { + var node = this.getInputNode(); + if (node) + { + var val = jQuery(node).val(); + + return val; + } + + return this._oldValue; + } + + isDirty() + { + return this._oldValue != this.getValue(); + } + + resetDirty() + { + this._oldValue = this.getValue(); + } + + isValid(messages) + { + var ok = true; + + // Check for required + if (this.options && this.options.needed && !this.options.readonly && !this.disabled && + (this.getValue() == null || this.getValue().valueOf() == '')) + { + messages.push(this.egw().lang('Field must not be empty !!!')); + ok = false; + } + return ok; + } + + /** + * Called whenever the template gets submitted. We return false if the widget + * is not valid, which cancels the submission. + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + submit(_values) + { + var messages = []; + var valid = this.isValid(messages); + + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + return valid; + } +} + diff --git a/api/js/etemplate/et2_core_valueWidget.js b/api/js/etemplate/et2_core_valueWidget.js index 6e176b4996..e9d484dc4b 100644 --- a/api/js/etemplate/et2_core_valueWidget.js +++ b/api/js/etemplate/et2_core_valueWidget.js @@ -34,8 +34,6 @@ require("./et2_core_common"); * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces * the "value" attribute and automatically loads it from the "content" array * after loading from XML. - * - * @augments et2_baseWidget */ var et2_valueWidget = /** @class */ (function (_super) { __extends(et2_valueWidget, _super); diff --git a/api/js/etemplate/et2_core_valueWidget.ts b/api/js/etemplate/et2_core_valueWidget.ts index f562550e48..221a19c16c 100644 --- a/api/js/etemplate/et2_core_valueWidget.ts +++ b/api/js/etemplate/et2_core_valueWidget.ts @@ -22,8 +22,6 @@ import './et2_core_common'; * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces * the "value" attribute and automatically loads it from the "content" array * after loading from XML. - * - * @augments et2_baseWidget */ export class et2_valueWidget extends et2_baseWidget { @@ -44,7 +42,7 @@ export class et2_valueWidget extends et2_baseWidget }; label: string = ''; - private _labelContainer: JQuery = null; + protected _labelContainer: JQuery = null; /** * diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index 25c966a0a5..6383250267 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -10,6 +10,10 @@ declare class et2_valueWidget extends et2_baseWidget{} declare class et2_inputWidget{ getInputNode() : HTMLElement } +declare class et2_tabbox extends et2_valueWidget { + tabData : any; + activateTab(et2_widget); +} declare var et2_surroundingsMgr : any; declare var et2_arrayMgr : any; declare var et2_readonlysArrayMgr : any; @@ -117,9 +121,6 @@ declare var et2_selectbox_ro : any; declare var et2_menulist : any; declare var et2_split : any; declare var et2_styles : any; -declare class et2_tabbox extends et2_widget { - tabData : any; -} declare var et2_taglist : any; declare var et2_taglist_account : any; declare var et2_taglist_email : any; From af1e62b178489e38e261af3eeb51144fa13f5fcd Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 21 Jan 2020 06:54:24 -0700 Subject: [PATCH 10/61] Some return types --- api/js/etemplate/et2_core_DOMWidget.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index f0b5115ff0..1d9dd8d0a8 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -100,7 +100,7 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode default:'', description: "Sets background image, left, right and scale on DOM", } - } + }; parentNode : HTMLElement = null; disabled : boolean = false; @@ -259,7 +259,8 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode private _surroundingsMgr : et2_surroundingsMgr; - getSurroundings() { + getSurroundings() : et2_surroundingsMgr + { if (!this._surroundingsMgr) { this._surroundingsMgr = new et2_surroundingsMgr(this); @@ -382,14 +383,16 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode /** * Returns the parent node. */ - getParentDOMNode() { + getParentDOMNode() : HTMLElement + { return this.parentNode; } /** * Returns the index of this element in the DOM tree */ - getDOMIndex() { + getDOMIndex() : number + { if (this.getParent()) { var idx = 0; From 29809e2395cbd9550e7f8970bfa3c8eb3505cfc8 Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 21 Jan 2020 07:01:43 -0700 Subject: [PATCH 11/61] First run at TS for valueWidget --- api/js/etemplate/et2_core_valueWidget.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/api/js/etemplate/et2_core_valueWidget.ts b/api/js/etemplate/et2_core_valueWidget.ts index 221a19c16c..a56a65bc7d 100644 --- a/api/js/etemplate/et2_core_valueWidget.ts +++ b/api/js/etemplate/et2_core_valueWidget.ts @@ -41,9 +41,6 @@ export class et2_valueWidget extends et2_baseWidget } }; - label: string = ''; - protected _labelContainer: JQuery = null; - /** * * @@ -59,7 +56,7 @@ export class et2_valueWidget extends et2_baseWidget // Set the value for this element var contentMgr = this.getArrayMgr("content"); if (contentMgr != null) { - var val = contentMgr.getEntry(this.id,false,true); + let val = contentMgr.getEntry(this.id, false, true); if (val !== null) { _attrs["value"] = val; @@ -74,7 +71,7 @@ export class et2_valueWidget extends et2_baseWidget } } - set_label(_value : string) + set_label(_value) { // Abort if there was no change in the label if (_value == this.label) From 9f4cd98787f18c1a23df322c8f5d4c0efe3af2d1 Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 21 Jan 2020 07:06:34 -0700 Subject: [PATCH 12/61] Second run at TS for valueWidget, restoring what was lost --- api/js/etemplate/et2_core_inputWidget.js | 576 +++++++++++------------ api/js/etemplate/et2_core_valueWidget.ts | 5 +- 2 files changed, 274 insertions(+), 307 deletions(-) diff --git a/api/js/etemplate/et2_core_inputWidget.js b/api/js/etemplate/et2_core_inputWidget.js index 6c9cfdab1b..75dc6bebd9 100644 --- a/api/js/etemplate/et2_core_inputWidget.js +++ b/api/js/etemplate/et2_core_inputWidget.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Widget base class * @@ -6,317 +7,280 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_valueWidget; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +require("./et2_types"); /** * et2_inputWidget derrives from et2_simpleWidget and implements the IInput * interface. When derriving from this class, call setDOMNode with an input * DOMNode. - * - * @augments et2_valueWidget */ -var et2_inputWidget = (function(){ "use strict"; return et2_valueWidget.extend([et2_IInput,et2_ISubmitListener], -{ - attributes: { - "needed": { - "name": "Required", - "default": false, - "type": "boolean", - "description": "If required, the user must enter a value before the form can be submitted" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the value changes." - }, - "onfocus": { - "name": "onfocus", - "type": "js", - "default": et2_no_init, - "description": "JS code which get executed when wiget receives focus." - }, - "validation_error": { - "name": "Validation Error", - "type": "string", - "default": et2_no_init, - "description": "Used internally to store the validation error that came from the server." - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - readonly: { - name: "readonly", - type: "boolean", - "default": false, - description: "Does NOT allow user to enter data, just displays existing data" - } - }, - - /** - * Constructor - * - * @memberOf et2_inputWidget - */ - init: function() { - this._super.apply(this, arguments); - - // mark value as not initialised, so set_value can determine if it is necessary to trigger change event - this._oldValue = et2_no_init; - this._labelContainer = null; - }, - - destroy: function() { - var node = this.getInputNode(); - if (node) - { - jQuery(node).unbind("change.et2_inputWidget"); - jQuery(node).unbind("focus"); - } - - this._super.apply(this, arguments); - - this._labelContainer = null; - }, - - /** - * Load the validation errors from the server - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - // Check whether an validation error entry exists - if (this.id && this.getArrayMgr("validation_errors")) - { - var val = this.getArrayMgr("validation_errors").getEntry(this.id); - if (val) - { - _attrs["validation_error"] = val; - } - } - }, - - attachToDOM: function() { - var node = this.getInputNode(); - if (node) - { - jQuery(node) - .off('.et2_inputWidget') - .bind("change.et2_inputWidget", this, function(e) { - e.data.change.call(e.data, this); - }) - .bind("focus.et2_inputWidget", this, function(e) { - e.data.focus.call(e.data, this); - }); - } - - this._super.apply(this,arguments); - -// jQuery(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved -// jQuery(this.getInputNode()).validator(); - }, - - detatchFromDOM: function() { -// if(this.getInputNode()) { -// jQuery(this.getInputNode()).data("validator").destroy(); -// } - this._super.apply(this,arguments); - }, - - change: function(_node) { - var messages = []; - var valid = this.isValid(messages); - - // Passing false will clear any set messages - this.set_validation_error(valid ? false : messages); - - if (valid && this.onchange) - { - if(typeof this.onchange == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return this.onchange.apply(this, args); - } else { - return (et2_compileLegacyJS(this.options.onchange, this, _node))(); - } - } - return valid; - }, - - focus: function(_node) - { - if(typeof this.options.onfocus == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return this.options.onfocus.apply(this, args); - } - }, - - /** - * Set value of widget and trigger for real changes a change event - * - * First initialisation (_oldValue === et2_no_init) is NOT considered a change! - * - * @param {string} _value value to set - */ - set_value: function(_value) - { - var node = this.getInputNode(); - if (node) - { - jQuery(node).val(_value); - if(this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) - { - jQuery(node).change(); - } - } - this._oldValue = _value; - }, - - set_id: function(_value) { - this.id = _value; - this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId+'_'+this.id : _value; - - // Set the id of the _input_ node (in contrast to the default - // implementation, which sets the base node) - var node = this.getInputNode(); - if (node) - { - // Unique ID to prevent DOM collisions across multiple templates - if (_value != "") - { - node.setAttribute("id", this.dom_id); - node.setAttribute("name", _value); - } - else - { - node.removeAttribute("id"); - node.removeAttribute("name"); - } - } - }, - - set_needed: function(_value) { - var node = this.getInputNode(); - if (node) - { - if(_value && !this.options.readonly) { - jQuery(node).attr("required", "required"); - } else { - node.removeAttribute("required"); - } - } - - }, - - set_validation_error: function(_value) { - var node = this.getInputNode(); - if (node) - { - if (_value === false) - { - this.hideMessage(); - jQuery(node).removeClass("invalid"); - } - else - { - this.showMessage(_value, "validation_error"); - jQuery(node).addClass("invalid"); - - // If on a tab, switch to that tab so user can see it - var widget = this; - while(widget._parent && widget._type != 'tabbox') - { - widget = widget._parent; - } - if (widget._type == 'tabbox') widget.activateTab(this); - } - } - }, - - /** - * Set tab index - * - * @param {number} index - */ - set_tabindex: function(index) { - jQuery(this.getInputNode()).attr("tabindex", index); - }, - - getInputNode: function() { - return this.node; - }, - - get_value: function() { - return this.getValue(); - }, - - getValue: function() { - var node = this.getInputNode(); - if (node) - { - var val = jQuery(node).val(); - - return val; - } - - return this._oldValue; - }, - - isDirty: function() { - return this._oldValue != this.getValue(); - }, - - resetDirty: function() { - this._oldValue = this.getValue(); - }, - - isValid: function(messages) { - var ok = true; - - // Check for required - if (this.options && this.options.needed && !this.options.readonly && !this.disabled && - (this.getValue() == null || this.getValue().valueOf() == '')) - { - messages.push(this.egw().lang('Field must not be empty !!!')); - ok = false; - } - return ok; - }, - - /** - * Called whenever the template gets submitted. We return false if the widget - * is not valid, which cancels the submission. - * - * @param _values contains the values which will be sent to the server. - * Listeners may change these values before they get submitted. - */ - submit: function(_values) { - var messages = []; - var valid = this.isValid(messages); - - // Passing false will clear any set messages - this.set_validation_error(valid ? false : messages); - return valid; - } -});}).call(this); - +var et2_inputWidget = /** @class */ (function (_super) { + __extends(et2_inputWidget, _super); + /** + * Constructor + */ + function et2_inputWidget(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + // mark value as not initialised, so set_value can determine if it is necessary to trigger change event + _this._oldValue = et2_no_init; + _this._labelContainer = null; + return _this; + } + et2_inputWidget.prototype.destroy = function () { + var node = this.getInputNode(); + if (node) { + jQuery(node).unbind("change.et2_inputWidget"); + jQuery(node).unbind("focus"); + } + _super.prototype.destroy.call(this); + this._labelContainer = null; + }; + /** + * Load the validation errors from the server + * + * @param {object} _attrs + */ + et2_inputWidget.prototype.transformAttributes = function (_attrs) { + _super.prototype.transformAttributes.call(this, _attrs); + // Check whether an validation error entry exists + if (this.id && this.getArrayMgr("validation_errors")) { + var val = this.getArrayMgr("validation_errors").getEntry(this.id); + if (val) { + _attrs["validation_error"] = val; + } + } + }; + et2_inputWidget.prototype.attachToDOM = function () { + var node = this.getInputNode(); + if (node) { + jQuery(node) + .off('.et2_inputWidget') + .bind("change.et2_inputWidget", this, function (e) { + e.data.change.call(e.data, this); + }) + .bind("focus.et2_inputWidget", this, function (e) { + e.data.focus.call(e.data, this); + }); + } + return _super.prototype.attachToDOM.call(this); + // jQuery(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved + // jQuery(this.getInputNode()).validator(); + }; + et2_inputWidget.prototype.detatchFromDOM = function () { + // if(this.getInputNode()) { + // jQuery(this.getInputNode()).data("validator").destroy(); + // } + _super.prototype.detachFromDOM.call(this); + }; + et2_inputWidget.prototype.change = function (_node) { + var messages = []; + var valid = this.isValid(messages); + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + if (valid && this.onchange) { + if (typeof this.onchange == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return this.onchange.apply(this, args); + } + else { + return (et2_compileLegacyJS(this.options.onchange, this, _node))(); + } + } + return valid; + }; + et2_inputWidget.prototype.focus = function (_node) { + if (typeof this.options.onfocus == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return this.options.onfocus.apply(this, args); + } + }; + /** + * Set value of widget and trigger for real changes a change event + * + * First initialisation (_oldValue === et2_no_init) is NOT considered a change! + * + * @param {string} _value value to set + */ + et2_inputWidget.prototype.set_value = function (_value) { + var node = this.getInputNode(); + if (node) { + jQuery(node).val(_value); + if (this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) { + jQuery(node).change(); + } + } + this._oldValue = _value; + }; + et2_inputWidget.prototype.set_id = function (_value) { + this.id = _value; + this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId + '_' + this.id : _value; + // Set the id of the _input_ node (in contrast to the default + // implementation, which sets the base node) + var node = this.getInputNode(); + if (node) { + // Unique ID to prevent DOM collisions across multiple templates + if (_value != "") { + node.setAttribute("id", this.dom_id); + node.setAttribute("name", _value); + } + else { + node.removeAttribute("id"); + node.removeAttribute("name"); + } + } + }; + et2_inputWidget.prototype.set_needed = function (_value) { + var node = this.getInputNode(); + if (node) { + if (_value && !this.options.readonly) { + jQuery(node).attr("required", "required"); + } + else { + node.removeAttribute("required"); + } + } + }; + et2_inputWidget.prototype.set_validation_error = function (_value) { + var node = this.getInputNode(); + if (node) { + if (_value === false) { + this.hideMessage(); + jQuery(node).removeClass("invalid"); + } + else { + this.showMessage(_value, "validation_error"); + jQuery(node).addClass("invalid"); + // If on a tab, switch to that tab so user can see it + var widget = this; + while (widget.getParent() && widget.getType() != 'tabbox') { + widget = widget.getParent(); + } + if (widget.getType() == 'tabbox') + widget.activateTab(this); + } + } + }; + /** + * Set tab index + * + * @param {number} index + */ + et2_inputWidget.prototype.set_tabindex = function (index) { + jQuery(this.getInputNode()).attr("tabindex", index); + }; + et2_inputWidget.prototype.getInputNode = function () { + return this.node; + }; + et2_inputWidget.prototype.get_value = function () { + return this.getValue(); + }; + et2_inputWidget.prototype.getValue = function () { + var node = this.getInputNode(); + if (node) { + var val = jQuery(node).val(); + return val; + } + return this._oldValue; + }; + et2_inputWidget.prototype.isDirty = function () { + return this._oldValue != this.getValue(); + }; + et2_inputWidget.prototype.resetDirty = function () { + this._oldValue = this.getValue(); + }; + et2_inputWidget.prototype.isValid = function (messages) { + var ok = true; + // Check for required + if (this.options && this.options.needed && !this.options.readonly && !this.disabled && + (this.getValue() == null || this.getValue().valueOf() == '')) { + messages.push(this.egw().lang('Field must not be empty !!!')); + ok = false; + } + return ok; + }; + /** + * Called whenever the template gets submitted. We return false if the widget + * is not valid, which cancels the submission. + * + * @param _values contains the values which will be sent to the server. + * Listeners may change these values before they get submitted. + */ + et2_inputWidget.prototype.submit = function (_values) { + var messages = []; + var valid = this.isValid(messages); + // Passing false will clear any set messages + this.set_validation_error(valid ? false : messages); + return valid; + }; + et2_inputWidget._attributes = { + "needed": { + "name": "Required", + "default": false, + "type": "boolean", + "description": "If required, the user must enter a value before the form can be submitted" + }, + "onchange": { + "name": "onchange", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when the value changes." + }, + "onfocus": { + "name": "onfocus", + "type": "js", + "default": et2_no_init, + "description": "JS code which get executed when wiget receives focus." + }, + "validation_error": { + "name": "Validation Error", + "type": "string", + "default": et2_no_init, + "description": "Used internally to store the validation error that came from the server." + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + readonly: { + name: "readonly", + type: "boolean", + "default": false, + description: "Does NOT allow user to enter data, just displays existing data" + } + }; + return et2_inputWidget; +}(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_inputWidget = et2_inputWidget; diff --git a/api/js/etemplate/et2_core_valueWidget.ts b/api/js/etemplate/et2_core_valueWidget.ts index a56a65bc7d..4fc8a263c3 100644 --- a/api/js/etemplate/et2_core_valueWidget.ts +++ b/api/js/etemplate/et2_core_valueWidget.ts @@ -41,6 +41,9 @@ export class et2_valueWidget extends et2_baseWidget } }; + label: string = ''; + private _labelContainer: JQuery = null; + /** * * @@ -71,7 +74,7 @@ export class et2_valueWidget extends et2_baseWidget } } - set_label(_value) + set_label(_value : string) { // Abort if there was no change in the label if (_value == this.label) From 96a9ab721108908add58e817471dff9fece719bc Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 15:12:29 +0100 Subject: [PATCH 13/61] WIP on et2_widget_textbox --- api/js/etemplate/et2_core_DOMWidget.js | 1 + api/js/etemplate/et2_core_DOMWidget.ts | 1 + api/js/etemplate/et2_core_interfaces.js | 7 + api/js/etemplate/et2_core_interfaces.ts | 7 + api/js/etemplate/et2_core_widget.js | 3 + api/js/etemplate/et2_core_widget.ts | 2 +- api/js/etemplate/et2_types.d.ts | 4 +- api/js/etemplate/et2_widget_textbox.js | 1143 +++++++++++------------ api/js/etemplate/et2_widget_textbox.ts | 647 +++++++++++++ 9 files changed, 1217 insertions(+), 598 deletions(-) create mode 100644 api/js/etemplate/et2_widget_textbox.ts diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js index 40cf9e3f2f..b6589e99f0 100644 --- a/api/js/etemplate/et2_core_DOMWidget.js +++ b/api/js/etemplate/et2_core_DOMWidget.js @@ -32,6 +32,7 @@ require("./et2_core_interfaces"); require("./et2_core_common"); var et2_core_widget_1 = require("./et2_core_widget"); var egw_action_js_1 = require("../egw_action/egw_action.js"); +require("./et2_types"); /** * Abstract widget class which can be inserted into the DOM. All widget classes * deriving from this class have to care about implementing the "getDOMNode" diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index 1d9dd8d0a8..dce01e8d8e 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -23,6 +23,7 @@ import { egw_getActionManager, egw_getAppObjectManager, egwActionObject, egwAction, EGW_AI_DRAG_OVER, EGW_AI_DRAG_OUT } from '../egw_action/egw_action.js'; +import './et2_types'; /** * Abstract widget class which can be inserted into the DOM. All widget classes diff --git a/api/js/etemplate/et2_core_interfaces.js b/api/js/etemplate/et2_core_interfaces.js index 86bf331979..0ae9f1ec9c 100644 --- a/api/js/etemplate/et2_core_interfaces.js +++ b/api/js/etemplate/et2_core_interfaces.js @@ -21,24 +21,31 @@ function implements_methods(obj, methods) { } return true; } +var et2_IDOMNode = "et2_IDOMNode"; function implements_et2_IDOMNode(obj) { return implements_methods(obj, ["getDOMNode"]); } +var et2_IInput = "et2_IInput"; function implements_et2_IInput(obj) { return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); } +var et2_IResizeable = "et2_IResizeable"; function implements_et2_IResizeable(obj) { return implements_methods(obj, ["resize"]); } +var et2_IAligned = "et2_IAligned"; function implements_et2_IAligned(obj) { return implements_methods(obj, ["get_align"]); } +var et2_ISubmitListener = "et2_ISubmitListener"; function implements_et2_ISubmitListener(obj) { return implements_methods(obj, ["submit"]); } +var et2_IDetachedDOM = "et2_IDetachedDOM"; function implements_et2_IDetachedDOM(obj) { return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); } +var et2_IPrint = "et2_IPrint"; function implements_et2_IPrint(obj) { return implements_methods(obj, ["beforePrint", "afterPrint"]); } diff --git a/api/js/etemplate/et2_core_interfaces.ts b/api/js/etemplate/et2_core_interfaces.ts index ddc1e853bd..bb8a31182f 100644 --- a/api/js/etemplate/et2_core_interfaces.ts +++ b/api/js/etemplate/et2_core_interfaces.ts @@ -49,6 +49,7 @@ interface et2_IDOMNode */ getDOMNode(_sender? : et2_widget) : HTMLElement } +var et2_IDOMNode = "et2_IDOMNode"; function implements_et2_IDOMNode(obj : et2_widget) { return implements_methods(obj, ["getDOMNode"]); @@ -91,6 +92,7 @@ interface et2_IInput */ isValid(messages) : boolean } +var et2_IInput = "et2_IInput"; function implements_et2_IInput(obj : et2_widget) { return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); @@ -106,6 +108,7 @@ interface et2_IResizeable */ resize() : void } +var et2_IResizeable = "et2_IResizeable"; function implements_et2_IResizeable(obj : et2_widget) { return implements_methods(obj, ["resize"]); @@ -118,6 +121,7 @@ interface et2_IAligned { get_align() : string } +var et2_IAligned = "et2_IAligned"; function implements_et2_IAligned(obj : et2_widget) { return implements_methods(obj, ["get_align"]); @@ -138,6 +142,7 @@ interface et2_ISubmitListener */ submit(_values) : void } +var et2_ISubmitListener = "et2_ISubmitListener"; function implements_et2_ISubmitListener(obj : et2_widget) { return implements_methods(obj, ["submit"]); @@ -179,6 +184,7 @@ interface et2_IDetachedDOM setDetachedAttributes(_nodes : HTMLElement[], _values : object) : void } +var et2_IDetachedDOM = "et2_IDetachedDOM"; function implements_et2_IDetachedDOM(obj : et2_widget) { return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); @@ -202,6 +208,7 @@ interface et2_IPrint */ afterPrint() : void } +var et2_IPrint = "et2_IPrint"; function implements_et2_IPrint(obj : et2_widget) { return implements_methods(obj, ["beforePrint", "afterPrint"]); diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js index d751d68d65..32d0395de7 100644 --- a/api/js/etemplate/et2_core_widget.js +++ b/api/js/etemplate/et2_core_widget.js @@ -121,6 +121,9 @@ var et2_widget = /** @class */ (function (_super) { */ function et2_widget(_parent, _attrs, _child) { var _this = _super.call(this) || this; + // Set the legacyOptions array to the names of the properties the "options" + // attribute defines. + _this.legacyOptions = []; _this._children = []; _this._mgrs = {}; /** diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index d7d3f94475..9c276afa2f 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -174,7 +174,7 @@ export class et2_widget extends ClassWithAttributes // Set the legacyOptions array to the names of the properties the "options" // attribute defines. - legacyOptions: []; + legacyOptions: string[] = []; private _type: string; id: string; diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index 6383250267..d74a881a53 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -23,13 +23,13 @@ declare var et2_validTypes : string[]; declare var et2_typeDefaults : object; //declare const et2_no_init : object; declare var et2_editableWidget : any; -declare var et2_IDOMNode : any; +/*declare var et2_IDOMNode : any; declare var et2_IInput : any; declare var et2_IResizeable : any; declare var et2_IAligned : any; declare var et2_ISubmitListener : any; declare var et2_IDetachedDOM : any; -declare var et2_IPrint : any; +declare var et2_IPrint : any;*/ declare var et2_registry : {}; declare var et2_dataview : any; declare var et2_dataview_controller : any; diff --git a/api/js/etemplate/et2_widget_textbox.js b/api/js/etemplate/et2_widget_textbox.js index ec8da5c351..67d59932b3 100644 --- a/api/js/etemplate/et2_widget_textbox.js +++ b/api/js/etemplate/et2_widget_textbox.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Textbox object * @@ -6,615 +7,567 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +require("./et2_types"); /** * Class which implements the "textbox" XET-Tag * * @augments et2_inputWidget */ -var et2_textbox = (function(){ "use strict"; return et2_inputWidget.extend([et2_IResizeable], -{ - attributes: { - "multiline": { - "name": "multiline", - "type": "boolean", - "default": false, - "description": "If true, the textbox is a multiline edit field." - }, - "size": { - "name": "Size", - "type": "integer", - "default": et2_no_init, - "description": "Field width" - }, - "maxlength": { - "name": "Maximum length", - "type": "integer", - "default": et2_no_init, - "description": "Maximum number of characters allowed" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - // These for multi-line - "rows": { - "name": "Rows", - "type": "integer", - "default": -1, - "description": "Multiline field height - better to use CSS" - }, - "cols": { - "name": "Size", - "type": "integer", - "default": -1, - "description": "Multiline field width - better to use CSS" - }, - "validator": { - "name": "Validator", - "type": "string", - "default": et2_no_init, - "description": "Perl regular expression eg. '/^[0-9][a-f]{4}$/i'" - }, - "autocomplete": { - "name": "Autocomplete", - "type": "string", - "default": "", - "description": "Weither or not browser should autocomplete that field: 'on', 'off', 'default' (use attribute from form). Default value for type password is set to off." - }, - onkeypress: { - name: "onKeypress", - type: "js", - default: et2_no_init, - description: "JS code or app.$app.$method called when key is pressed, return false cancels it." - } - }, - - legacyOptions: ["size", "maxlength", "validator"], - - /** - * Constructor - * - * @memberOf et2_textbox - */ - init: function() { - this._super.apply(this, arguments); - - this.input = null; - - this.createInputWidget(); - }, - - createInputWidget: function() { - if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) - { - this.input = jQuery(document.createElement("textarea")); - - if (this.options.rows > 0) - { - this.input.attr("rows", this.options.rows); - } - - if (this.options.cols > 0) - { - this.input.attr("cols", this.options.cols); - } - } - else - { - this.input = jQuery(document.createElement("input")); - switch(this.options.type) - { - case "passwd": - this.input.attr("type", "password"); - // Make autocomplete default value off for password field - // seems browsers not respecting 'off' anymore and started to - // impelement a new key called "new-password" considered as switching - // autocomplete off. - // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion - if (this.options.autocomplete === "" || this.options.autocomplete == "off") this.options.autocomplete = "new-password"; - break; - case "hidden": - this.input.attr("type", "hidden"); - break; - } - if (this.options.autocomplete) this.input.attr("autocomplete", this.options.autocomplete); - } - - if(this.options.size) { - this.set_size(this.options.size); - } - if(this.options.blur) { - this.set_blur(this.options.blur); - } - if(this.options.readonly) { - this.set_readonly(true); - } - this.input.addClass("et2_textbox"); - this.setDOMNode(this.input[0]); - if(this.options.value) - { - this.set_value(this.options.value); - } - if (this.options.onkeypress && typeof this.options.onkeypress == 'function') - { - var self = this; - this.input.keypress(function(_ev) - { - return self.options.onkeypress.call(this, _ev, self); - }); - } - }, - - /** - * Override the parent set_id method to manuipulate the input DOM node - * - * @param {type} _value - * @returns {undefined} - */ - set_id: function(_value) - { - this._super.apply(this,arguments); - // Remove the name attribute inorder to affect autocomplete="off" - // for no password save. ATM seems all browsers ignore autocomplete for - // input field inside the form - if (this.options.type === "passwd" - && this.options.autocomplete === "off") this.input.removeAttr('name'); - }, - - destroy: function() { - var node = this.getInputNode(); - if (node) jQuery(node).unbind("keypress"); - - this._super.apply(this, arguments); - }, - - getValue: function() - { - if(this.options && this.options.blur && this.input.val() == this.options.blur) return ""; - return this._super.apply(this, arguments); - }, - - /** - * Clientside validation using regular expression in "validator" attribute - * - * @param {array} _messages - */ - isValid: function(_messages) - { - var ok = true; - // Check input is valid - if(this.options && this.options.validator && !this.options.readonly && !this.disabled) - { - if (typeof this.options.validator == 'string') - { - var parts = this.options.validator.split('/'); - var flags = parts.pop(); - if (parts.length < 2 || parts[0] !== '') - { - _messages.push(this.egw().lang("'%1' has an invalid format !!!", this.options.validator)); - return false; // show invalid expression - } - parts.shift(); - this.options.validator = new RegExp(parts.join('/'), flags); - } - var value = this.getValue(); - if (!(ok = this.options.validator.test(value))) - { - _messages.push(this.egw().lang("'%1' has an invalid format !!!", value)); - } - } - return this._super.apply(this, arguments) && ok; - }, - - /** - * Set input widget size - * @param _size Rather arbitrary size units, approximately characters - */ - set_size: function(_size) { - if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) - { - this.input.css('width', _size + "em"); - } - else if (typeof _size != 'undefined' && _size != this.input.attr("size")) - { - this.size = _size; - this.input.attr("size", this.size); - } - }, - - /** - * Set maximum characters allowed - * @param _size Max characters allowed - */ - set_maxlength: function(_size) { - if (typeof _size != 'undefined' && _size != this.input.attr("maxlength")) - { - this.maxLength = _size; - this.input.attr("maxLength", this.maxLength); - } - }, - - /** - * Set HTML readonly attribute. - * Do not confuse this with etemplate readonly, which would use et_textbox_ro instead - * @param _readonly Boolean - */ - set_readonly: function(_readonly) { - this.input.attr("readonly", _readonly); - this.input.toggleClass('et2_textbox_ro', _readonly); - }, - - set_blur: function(_value) { - if(_value) { - this.input.attr("placeholder", this.egw().lang(_value) + ""); // HTML5 - if(!this.input[0].placeholder) { - // Not HTML5 - if(this.input.val() == "") this.input.val(this.egw().lang(this.options.blur)); - this.input.focus(this,function(e) { - if(e.data.input.val() == e.data.egw().lang(e.data.options.blur)) e.data.input.val(""); - }).blur(this, function(e) { - if(e.data.input.val() == "") e.data.input.val(e.data.egw().lang(e.data.options.blur)); - }); - } - } else { - if (!this.getValue()) this.input.val(''); - this.input.removeAttr("placeholder"); - } - this.options.blur = _value; - }, - - set_autocomplete: function(_value) { - this.options.autocomplete = _value; - this.input.attr('autocomplete', _value); - }, - - resize: function (_height) - { - if (_height && this.options.multiline) - { - // apply the ratio - _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; - if (_height != 0) - { - this.input.height(this.input.height() + _height); - // resize parent too, so mailvelope injected into parent inherits its height - this.input.parent().height(this.input.parent().height()+_height); - } - } - } -});}).call(this); -et2_register_widget(et2_textbox, ["textbox", "passwd", "hidden"]); - +var et2_textbox = /** @class */ (function (_super_1) { + __extends(et2_textbox, _super_1); + /** + * Constructor + */ + function et2_textbox(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.legacyOptions = ["size", "maxlength", "validator"]; + _this.input = null; + _this.input = null; + _this.createInputWidget(); + return _this; + } + et2_textbox.prototype.createInputWidget = function () { + if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) { + this.input = jQuery(document.createElement("textarea")); + if (this.options.rows > 0) { + this.input.attr("rows", this.options.rows); + } + if (this.options.cols > 0) { + this.input.attr("cols", this.options.cols); + } + } + else { + this.input = jQuery(document.createElement("input")); + switch (this.options.type) { + case "passwd": + this.input.attr("type", "password"); + // Make autocomplete default value off for password field + // seems browsers not respecting 'off' anymore and started to + // impelement a new key called "new-password" considered as switching + // autocomplete off. + // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion + if (this.options.autocomplete === "" || this.options.autocomplete == "off") + this.options.autocomplete = "new-password"; + break; + case "hidden": + this.input.attr("type", "hidden"); + break; + } + if (this.options.autocomplete) + this.input.attr("autocomplete", this.options.autocomplete); + } + if (this.options.size) { + this.set_size(this.options.size); + } + if (this.options.blur) { + this.set_blur(this.options.blur); + } + if (this.options.readonly) { + this.set_readonly(true); + } + this.input.addClass("et2_textbox"); + this.setDOMNode(this.input[0]); + if (this.options.value) { + this.set_value(this.options.value); + } + if (this.options.onkeypress && typeof this.options.onkeypress == 'function') { + var self = this; + this.input.keypress(function (_ev) { + return self.options.onkeypress.call(this, _ev, self); + }); + } + }; + /** + * Override the parent set_id method to manuipulate the input DOM node + * + * @param {type} _value + * @returns {undefined} + */ + et2_textbox.prototype.set_id = function (_value) { + _super_1.prototype.set_id.call(this, _value); + // Remove the name attribute inorder to affect autocomplete="off" + // for no password save. ATM seems all browsers ignore autocomplete for + // input field inside the form + if (this.options.type === "passwd" + && this.options.autocomplete === "off") + this.input.removeAttr('name'); + }; + et2_textbox.prototype.destroy = function () { + var node = this.getInputNode(); + if (node) + jQuery(node).unbind("keypress"); + _super_1.prototype.destroy.call(this); + }; + et2_textbox.prototype.getValue = function () { + if (this.options && this.options.blur && this.input.val() == this.options.blur) + return ""; + return _super_1.prototype.getValue.call(this); + }; + /** + * Clientside validation using regular expression in "validator" attribute + * + * @param {array} _messages + */ + et2_textbox.prototype.isValid = function (_messages) { + var ok = true; + // Check input is valid + if (this.options && this.options.validator && !this.options.readonly && !this.disabled) { + if (typeof this.options.validator == 'string') { + var parts = this.options.validator.split('/'); + var flags = parts.pop(); + if (parts.length < 2 || parts[0] !== '') { + _messages.push(this.egw().lang("'%1' has an invalid format !!!", this.options.validator)); + return false; // show invalid expression + } + parts.shift(); + this.options.validator = new RegExp(parts.join('/'), flags); + } + var value = this.getValue(); + if (!(ok = this.options.validator.test(value))) { + _messages.push(this.egw().lang("'%1' has an invalid format !!!", value)); + } + } + return _super_1.prototype.isValid.call(this, _messages) && ok; + }; + /** + * Set input widget size + * @param _size Rather arbitrary size units, approximately characters + */ + et2_textbox.prototype.set_size = function (_size) { + if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) { + this.input.css('width', _size + "em"); + } + else if (typeof _size != 'undefined' && _size != this.input.attr("size")) { + this.size = _size; + this.input.attr("size", this.size); + } + }; + /** + * Set maximum characters allowed + * @param _size Max characters allowed + */ + et2_textbox.prototype.set_maxlength = function (_size) { + if (typeof _size != 'undefined' && _size != this.input.attr("maxlength")) { + this.maxLength = _size; + this.input.attr("maxLength", this.maxLength); + } + }; + /** + * Set HTML readonly attribute. + * Do not confuse this with etemplate readonly, which would use et_textbox_ro instead + * @param _readonly Boolean + */ + et2_textbox.prototype.set_readonly = function (_readonly) { + this.input.attr("readonly", _readonly); + this.input.toggleClass('et2_textbox_ro', _readonly); + }; + et2_textbox.prototype.set_blur = function (_value) { + if (_value) { + this.input.attr("placeholder", this.egw().lang(_value) + ""); // HTML5 + if (!this.input[0].placeholder) { + // Not HTML5 + if (this.input.val() == "") + this.input.val(this.egw().lang(this.options.blur)); + this.input.focus(this, function (e) { + if (e.data.input.val() == e.data.egw().lang(e.data.options.blur)) + e.data.input.val(""); + }).blur(this, function (e) { + if (e.data.input.val() == "") + e.data.input.val(e.data.egw().lang(e.data.options.blur)); + }); + } + } + else { + if (!this.getValue()) + this.input.val(''); + this.input.removeAttr("placeholder"); + } + this.options.blur = _value; + }; + et2_textbox.prototype.set_autocomplete = function (_value) { + this.options.autocomplete = _value; + this.input.attr('autocomplete', _value); + }; + et2_textbox.prototype.resize = function (_height) { + if (_height && this.options.multiline) { + // apply the ratio + _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height; + if (_height != 0) { + this.input.height(this.input.height() + _height); + // resize parent too, so mailvelope injected into parent inherits its height + this.input.parent().height(this.input.parent().height() + _height); + } + } + }; + et2_textbox._attributes = { + "multiline": { + "name": "multiline", + "type": "boolean", + "default": false, + "description": "If true, the textbox is a multiline edit field." + }, + "size": { + "name": "Size", + "type": "integer", + "default": et2_no_init, + "description": "Field width" + }, + "maxlength": { + "name": "Maximum length", + "type": "integer", + "default": et2_no_init, + "description": "Maximum number of characters allowed" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + // These for multi-line + "rows": { + "name": "Rows", + "type": "integer", + "default": -1, + "description": "Multiline field height - better to use CSS" + }, + "cols": { + "name": "Size", + "type": "integer", + "default": -1, + "description": "Multiline field width - better to use CSS" + }, + "validator": { + "name": "Validator", + "type": "string", + "default": et2_no_init, + "description": "Perl regular expression eg. '/^[0-9][a-f]{4}$/i'" + }, + "autocomplete": { + "name": "Autocomplete", + "type": "string", + "default": "", + "description": "Weither or not browser should autocomplete that field: 'on', 'off', 'default' (use attribute from form). Default value for type password is set to off." + }, + onkeypress: { + name: "onKeypress", + type: "js", + default: et2_no_init, + description: "JS code or app.$app.$method called when key is pressed, return false cancels it." + } + }; + return et2_textbox; +}(et2_core_inputWidget_1.et2_inputWidget)); +et2_core_widget_1.et2_register_widget(et2_textbox, ["textbox", "passwd", "hidden"]); /** * et2_textbox_ro is the dummy readonly implementation of the textbox. * * @augments et2_valueWidget */ -var et2_textbox_ro = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - /** - * Ignore all more advanced attributes. - */ - attributes: { - "multiline": { - "ignore": true - }, - "maxlength": { - "ignore": true - }, - "onchange": { - "ignore": true - }, - "rows": { - "ignore": true - }, - "cols": { - "ignore": true - }, - "size": { - "ignore": true - }, - "needed": { - "ignore": true - } - }, - - /** - * Constructor - * - * @memberOf et2_textbox_ro - */ - init: function() { - this._super.apply(this, arguments); - - this.value = ""; - this.span = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.value_span = jQuery(document.createElement("span")) - .addClass("et2_textbox_ro") - .appendTo(this.span); - - this.setDOMNode(this.span[0]); - }, - - set_label: function(label) - { - // Remove current label - this.span.contents() - .filter(function(){ return this.nodeType == 3; }).remove(); - - var parts = et2_csvSplit(label, 2, "%s"); - this.span.prepend(parts[0]); - this.span.append(parts[1]); - this.label = label; - - // add class if label is empty - this.span.toggleClass('et2_label_empty', !label || !parts[0]); - }, - set_value: function(_value) - { - this.value = _value; - - if(!_value) - { - _value = ""; - } - if (this.label !="") - { - this.span.removeClass('et2_label_empty'); - } - else - { - this.span.addClass('et2_label_empty'); - } - this.value_span.text(_value); - }, - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value", "label"); - }, - - getDetachedNodes: function() - { - return [this.span[0], this.value_span[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - this.span = jQuery(_nodes[0]); - this.value_span = jQuery(_nodes[1]); - if(typeof _values["label"] != 'undefined') - { - this.set_label(_values["label"]); - } - if(typeof _values["value"] != 'undefined') - { - this.set_value(_values["value"]); - } - } -});}).call(this); -et2_register_widget(et2_textbox_ro, ["textbox_ro"]); - +var et2_textbox_ro = /** @class */ (function (_super_1) { + __extends(et2_textbox_ro, _super_1); + /** + * Constructor + */ + function et2_textbox_ro(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.value = ""; + _this.span = jQuery(document.createElement("label")) + .addClass("et2_label"); + _this.value_span = jQuery(document.createElement("span")) + .addClass("et2_textbox_ro") + .appendTo(_this.span); + _this.setDOMNode(_this.span[0]); + return _this; + } + et2_textbox_ro.prototype.set_label = function (label) { + // Remove current label + this.span.contents() + .filter(function () { return this.nodeType == 3; }).remove(); + var parts = et2_csvSplit(label, 2, "%s"); + this.span.prepend(parts[0]); + this.span.append(parts[1]); + this.label = label; + // add class if label is empty + this.span.toggleClass('et2_label_empty', !label || !parts[0]); + }; + et2_textbox_ro.prototype.set_value = function (_value) { + this.value = _value; + if (!_value) { + _value = ""; + } + if (this.label != "") { + this.span.removeClass('et2_label_empty'); + } + else { + this.span.addClass('et2_label_empty'); + } + this.value_span.text(_value); + }; + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + et2_textbox_ro.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "label"); + }; + et2_textbox_ro.prototype.getDetachedNodes = function () { + return [this.span[0], this.value_span[0]]; + }; + et2_textbox_ro.prototype.setDetachedAttributes = function (_nodes, _values) { + this.span = jQuery(_nodes[0]); + this.value_span = jQuery(_nodes[1]); + if (typeof _values["label"] != 'undefined') { + this.set_label(_values["label"]); + } + if (typeof _values["value"] != 'undefined') { + this.set_value(_values["value"]); + } + }; + /** + * Ignore all more advanced attributes. + */ + et2_textbox_ro._attributes = { + "multiline": { + "ignore": true + }, + "maxlength": { + "ignore": true + }, + "onchange": { + "ignore": true + }, + "rows": { + "ignore": true + }, + "cols": { + "ignore": true + }, + "size": { + "ignore": true + }, + "needed": { + "ignore": true + } + }; + return et2_textbox_ro; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_textbox_ro, ["textbox_ro"]); /** * et2_searchbox is a widget which provides a collapsable input search * with on searching indicator and clear handler regardless of any browser limitation. - * - * @type type */ -var et2_searchbox = (function(){ "use strict"; return et2_textbox.extend( -{ - /** - * Advanced attributes - */ - attributes: { - overlay:{ - name:"Overlay searchbox", - type:"boolean", - default:false, - description:"Define wheter the searchbox overlays while it's open (true) or stay as solid box infront of the search button (false). Default is false." - }, - fix: { - name:"Fix searchbox", - type:"boolean", - default:true, - description:"Define wheter the searchbox should be a fix input field or flexible search button. Default is true (fix)." - } - }, - - /** - * Constructor - * - * @memberOf et2_searchbox - */ - init: function() { - this.value = ""; - this.div = jQuery(document.createElement('div')) - .addClass('et2_searchbox'); - this.flex = jQuery(document.createElement('div')) - .addClass('flex') - .appendTo(this.div); - this._super.apply(this, arguments); - this.setDOMNode(this.div[0]); - this._createWidget(); - }, - - _createWidget:function() - { - var self = this; - if (this.options.overlay) this.flex.addClass('overlay'); - // search button indicator - // no need to create search button if it's a fix search field - if (!this.options.fix) - { - this.button = et2_createWidget('button',{image:"search","background_image":"1"},this); - this.button.onclick= function(){ - self._show_hide(jQuery(self.flex).hasClass('hide')); - self.search.input.focus(); - }; - this.div.prepend(this.button.getDOMNode()); - } - // input field - this.search = et2_createWidget('textbox',{"blur":egw.lang("search"), - onkeypress:function(event) { - if(event.which == 13) - { - event.preventDefault(); - self.getInstanceManager().autocomplete_fixer(); - // Use a timeout to make sure we get the autocomplete value, - // if one was chosen, instead of what was actually typed. - // Chrome doesn't need this, but FF does. - window.setTimeout(function() { - self.set_value(self.search.input.val()); - self.change(); - },0); - } - }},this); - // Autocomplete needs name - this.search.input.attr('name', this.id||'searchbox'); - this.search.input.on({ - keyup:function(event) - { - self.clear.toggle(self.get_value() !='' || !self.options.fix); - if(event.which == 27) // Escape - { - // Excape clears search - self.set_value(''); - } - }, - - blur: function(event){ - if (egwIsMobile()) return; - if (!event.relatedTarget || !jQuery(event.relatedTarget.parentNode).hasClass('et2_searchbox')) - { - self._show_hide((!self.options.overlay && self.get_value())); - } - if (typeof self.oldValue !='undefined' && self._oldValue != self.get_value()) { - self.change(); - } - }, - mousedown:function(event){ - if (event.target.type == 'span') event.stopImmidatePropagation(); - } - }); - this.flex.append(this.search.getDOMNode()); - - // clear button implementation - this.clear = jQuery(document.createElement('span')) - .addClass('ui-icon clear') - .toggle(!this.options.fix || (this._oldValue != '' && !jQuery.isEmptyObject(this._oldValue))) - .on('mousedown',function(event){ - event.preventDefault(); - }) - .on('click',function(event) { - if (self.get_value()){ - self.search.input.val(''); - self.search.input.focus(); - self._show_hide(true); - if (self._oldValue) self.change(); - } - else - { - self._show_hide(false); - } - if (self.options.fix) self.clear.hide(); - }) - .appendTo(this.flex); - }, - - /** - * Show/hide search field - * @param {boolean} _stat true means show and false means hide - */ - _show_hide: function(_stat) - { - // Not applied for fix option - if (this.options.fix) return; - - jQuery(this.flex).toggleClass('hide',!_stat); - jQuery(this.getDOMNode()).toggleClass('expanded', _stat); - }, - - /** - * toggle search button status based on value - */ - _searchToggleState:function() - { - if (this.options.fix || egwIsMobile()) return; - - if (!this.get_value()) - { - jQuery(this.button.getDOMNode()).removeClass('toolbar_toggled'); - this.button.set_statustext(''); - } - else - { - jQuery(this.button.getDOMNode()).addClass('toolbar_toggled'); - this.button.set_statustext(egw.lang("search for '%1'", this.get_value())); - } - }, - - /** - * override change function in order to preset the toggle state - */ - change:function() - { - this._searchToggleState(); - - this._super.apply(this,arguments); - }, - - - get_value:function(){ - return this.search.input.val(); - }, - - set_value: function (_value){ - this._super.apply(this,arguments); - if (this.search) this.search.input.val(_value); - }, - - /** - * override doLoadingFinished in order to set initial state - */ - doLoadingFinished: function() - { - this._super.apply(this,arguments); - if (!this.get_value()) { - this._show_hide(false); - } - else{ - this._show_hide(!this.options.overlay); - this._searchToggleState(); - } - }, - - /** - * Overrride attachToDOM in order to unbind change handler - */ - attachToDOM: function() { - this._super.apply(this,arguments); - var node = this.getInputNode(); - if (node) - { - jQuery(node).off('.et2_inputWidget'); - } - }, -});}).call(this); -et2_register_widget(et2_searchbox, ["searchbox"]); +var et2_searchbox = /** @class */ (function (_super_1) { + __extends(et2_searchbox, _super_1); + /** + * Constructor + */ + function et2_searchbox(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.value = ""; + _this.div = jQuery(document.createElement('div')) + .addClass('et2_searchbox'); + _this.flex = jQuery(document.createElement('div')) + .addClass('flex') + .appendTo(_this.div); + //this._super.apply(this, arguments); + _this.setDOMNode(_this.div[0]); + _this._createWidget(); + return _this; + } + et2_searchbox.prototype._createWidget = function () { + var self = this; + if (this.options.overlay) + this.flex.addClass('overlay'); + // search button indicator + // no need to create search button if it's a fix search field + if (!this.options.fix) { + this.button = et2_core_widget_1.et2_createWidget('button', { image: "search", "background_image": "1" }, this); + this.button.onclick = function () { + self._show_hide(jQuery(self.flex).hasClass('hide')); + self.search.input.focus(); + }; + this.div.prepend(this.button.getDOMNode()); + } + // input field + this.search = et2_core_widget_1.et2_createWidget('textbox', { "blur": egw.lang("search"), + onkeypress: function (event) { + if (event.which == 13) { + event.preventDefault(); + self.getInstanceManager().autocomplete_fixer(); + // Use a timeout to make sure we get the autocomplete value, + // if one was chosen, instead of what was actually typed. + // Chrome doesn't need this, but FF does. + window.setTimeout(function () { + self.set_value(self.search.input.val()); + self.change(); + }, 0); + } + } }, this); + // Autocomplete needs name + this.search.input.attr('name', this.id || 'searchbox'); + this.search.input.on({ + keyup: function (event) { + self.clear.toggle(self.get_value() != '' || !self.options.fix); + if (event.which == 27) // Escape + { + // Excape clears search + self.set_value(''); + } + }, + blur: function (event) { + if (egwIsMobile()) + return; + if (!event.relatedTarget || !jQuery(event.relatedTarget.parentNode).hasClass('et2_searchbox')) { + self._show_hide((!self.options.overlay && self.get_value())); + } + if (typeof self.oldValue != 'undefined' && self._oldValue != self.get_value()) { + self.change(); + } + }, + mousedown: function (event) { + if (event.target.type == 'span') + event.stopImmidatePropagation(); + } + }); + this.flex.append(this.search.getDOMNode()); + // clear button implementation + this.clear = jQuery(document.createElement('span')) + .addClass('ui-icon clear') + .toggle(!this.options.fix || (this._oldValue != '' && !jQuery.isEmptyObject(this._oldValue))) + .on('mousedown', function (event) { + event.preventDefault(); + }) + .on('click', function (event) { + if (self.get_value()) { + self.search.input.val(''); + self.search.input.focus(); + self._show_hide(true); + if (self._oldValue) + self.change(); + } + else { + self._show_hide(false); + } + if (self.options.fix) + self.clear.hide(); + }) + .appendTo(this.flex); + }; + /** + * Show/hide search field + * @param {boolean} _stat true means show and false means hide + */ + et2_searchbox.prototype._show_hide = function (_stat) { + // Not applied for fix option + if (this.options.fix) + return; + jQuery(this.flex).toggleClass('hide', !_stat); + jQuery(this.getDOMNode()).toggleClass('expanded', _stat); + }; + /** + * toggle search button status based on value + */ + et2_searchbox.prototype._searchToggleState = function () { + if (this.options.fix || egwIsMobile()) + return; + if (!this.get_value()) { + jQuery(this.button.getDOMNode()).removeClass('toolbar_toggled'); + this.button.set_statustext(''); + } + else { + jQuery(this.button.getDOMNode()).addClass('toolbar_toggled'); + this.button.set_statustext(egw.lang("search for '%1'", this.get_value())); + } + }; + /** + * override change function in order to preset the toggle state + */ + et2_searchbox.prototype.change = function () { + this._searchToggleState(); + this._super.apply(this, arguments); + }; + et2_searchbox.prototype.get_value = function () { + return this.search.input.val(); + }; + et2_searchbox.prototype.set_value = function (_value) { + _super_1.prototype.set_value.call(this, _value); + if (this.search) + this.search.input.val(_value); + }; + /** + * override doLoadingFinished in order to set initial state + */ + et2_searchbox.prototype.doLoadingFinished = function () { + _super_1.prototype.doLoadingFinished.call(this); + if (!this.get_value()) { + this._show_hide(false); + } + else { + this._show_hide(!this.options.overlay); + this._searchToggleState(); + } + }; + /** + * Overrride attachToDOM in order to unbind change handler + */ + et2_searchbox.prototype.attachToDOM = function () { + _super_1.prototype.attachToDOM.call(this); + var node = this.getInputNode(); + if (node) { + jQuery(node).off('.et2_inputWidget'); + } + }; + /** + * Advanced attributes + */ + et2_searchbox._attributes = { + overlay: { + name: "Overlay searchbox", + type: "boolean", + default: false, + description: "Define wheter the searchbox overlays while it's open (true) or stay as solid box infront of the search button (false). Default is false." + }, + fix: { + name: "Fix searchbox", + type: "boolean", + default: true, + description: "Define wheter the searchbox should be a fix input field or flexible search button. Default is true (fix)." + } + }; + return et2_searchbox; +}(et2_textbox)); +et2_core_widget_1.et2_register_widget(et2_searchbox, ["searchbox"]); diff --git a/api/js/etemplate/et2_widget_textbox.ts b/api/js/etemplate/et2_widget_textbox.ts new file mode 100644 index 0000000000..f8e96b8a8d --- /dev/null +++ b/api/js/etemplate/et2_widget_textbox.ts @@ -0,0 +1,647 @@ +/** + * EGroupware eTemplate2 - JS Textbox object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_inputWidget; + et2_core_valueWidget; +*/ + +import './et2_core_common'; +import { ClassWithAttributes } from "./et2_core_inheritance"; +import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from "./et2_core_widget"; +import { et2_DOMWidget } from './et2_core_DOMWidget' +import { et2_valueWidget } from './et2_core_valueWidget' +import { et2_inputWidget } from './et2_core_inputWidget' +import './et2_types'; + +/** + * Class which implements the "textbox" XET-Tag + * + * @augments et2_inputWidget + */ +class et2_textbox extends et2_inputWidget implements et2_IResizeable +{ + static readonly _attributes : any = { + "multiline": { + "name": "multiline", + "type": "boolean", + "default": false, + "description": "If true, the textbox is a multiline edit field." + }, + "size": { + "name": "Size", + "type": "integer", + "default": et2_no_init, + "description": "Field width" + }, + "maxlength": { + "name": "Maximum length", + "type": "integer", + "default": et2_no_init, + "description": "Maximum number of characters allowed" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + // These for multi-line + "rows": { + "name": "Rows", + "type": "integer", + "default": -1, + "description": "Multiline field height - better to use CSS" + }, + "cols": { + "name": "Size", + "type": "integer", + "default": -1, + "description": "Multiline field width - better to use CSS" + }, + "validator": { + "name": "Validator", + "type": "string", + "default": et2_no_init, + "description": "Perl regular expression eg. '/^[0-9][a-f]{4}$/i'" + }, + "autocomplete": { + "name": "Autocomplete", + "type": "string", + "default": "", + "description": "Weither or not browser should autocomplete that field: 'on', 'off', 'default' (use attribute from form). Default value for type password is set to off." + }, + onkeypress: { + name: "onKeypress", + type: "js", + default: et2_no_init, + description: "JS code or app.$app.$method called when key is pressed, return false cancels it." + } + }; + + legacyOptions: string[] = ["size", "maxlength", "validator"]; + input: JQuery = null; + size: number|string; + maxLength: number|string; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + + this.input = null; + + this.createInputWidget(); + } + + createInputWidget() + { + if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) + { + this.input = jQuery(document.createElement("textarea")); + + if (this.options.rows > 0) + { + this.input.attr("rows", this.options.rows); + } + + if (this.options.cols > 0) + { + this.input.attr("cols", this.options.cols); + } + } + else + { + this.input = jQuery(document.createElement("input")); + switch(this.options.type) + { + case "passwd": + this.input.attr("type", "password"); + // Make autocomplete default value off for password field + // seems browsers not respecting 'off' anymore and started to + // impelement a new key called "new-password" considered as switching + // autocomplete off. + // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion + if (this.options.autocomplete === "" || this.options.autocomplete == "off") this.options.autocomplete = "new-password"; + break; + case "hidden": + this.input.attr("type", "hidden"); + break; + } + if (this.options.autocomplete) this.input.attr("autocomplete", this.options.autocomplete); + } + + if(this.options.size) { + this.set_size(this.options.size); + } + if(this.options.blur) { + this.set_blur(this.options.blur); + } + if(this.options.readonly) { + this.set_readonly(true); + } + this.input.addClass("et2_textbox"); + this.setDOMNode(this.input[0]); + if(this.options.value) + { + this.set_value(this.options.value); + } + if (this.options.onkeypress && typeof this.options.onkeypress == 'function') + { + var self = this; + this.input.keypress(function(_ev) + { + return self.options.onkeypress.call(this, _ev, self); + }); + } + } + + /** + * Override the parent set_id method to manuipulate the input DOM node + * + * @param {type} _value + * @returns {undefined} + */ + set_id(_value) + { + super.set_id(_value); + + // Remove the name attribute inorder to affect autocomplete="off" + // for no password save. ATM seems all browsers ignore autocomplete for + // input field inside the form + if (this.options.type === "passwd" + && this.options.autocomplete === "off") this.input.removeAttr('name'); + } + + destroy() + { + var node = this.getInputNode(); + if (node) jQuery(node).unbind("keypress"); + + super.destroy(); + } + + getValue() + { + if(this.options && this.options.blur && this.input.val() == this.options.blur) return ""; + + return super.getValue(); + } + + /** + * Clientside validation using regular expression in "validator" attribute + * + * @param {array} _messages + */ + isValid(_messages) + { + var ok = true; + // Check input is valid + if(this.options && this.options.validator && !this.options.readonly && !this.disabled) + { + if (typeof this.options.validator == 'string') + { + var parts = this.options.validator.split('/'); + var flags = parts.pop(); + if (parts.length < 2 || parts[0] !== '') + { + _messages.push(this.egw().lang("'%1' has an invalid format !!!", this.options.validator)); + return false; // show invalid expression + } + parts.shift(); + this.options.validator = new RegExp(parts.join('/'), flags); + } + var value = this.getValue(); + if (!(ok = this.options.validator.test(value))) + { + _messages.push(this.egw().lang("'%1' has an invalid format !!!", value)); + } + } + return super.isValid(_messages) && ok; + } + + /** + * Set input widget size + * @param _size Rather arbitrary size units, approximately characters + */ + set_size(_size : number|string) + { + if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) + { + this.input.css('width', _size + "em"); + } + else if (typeof _size != 'undefined' && _size != this.input.attr("size")) + { + this.size = _size; + this.input.attr("size", this.size); + } + } + + /** + * Set maximum characters allowed + * @param _size Max characters allowed + */ + set_maxlength(_size : number|string) + { + if (typeof _size != 'undefined' && _size != this.input.attr("maxlength")) + { + this.maxLength = _size; + this.input.attr("maxLength", this.maxLength); + } + } + + /** + * Set HTML readonly attribute. + * Do not confuse this with etemplate readonly, which would use et_textbox_ro instead + * @param _readonly Boolean + */ + set_readonly(_readonly) + { + this.input.attr("readonly", _readonly); + this.input.toggleClass('et2_textbox_ro', _readonly); + } + + set_blur(_value) + { + if(_value) { + this.input.attr("placeholder", this.egw().lang(_value) + ""); // HTML5 + if(!this.input[0].placeholder) { + // Not HTML5 + if(this.input.val() == "") this.input.val(this.egw().lang(this.options.blur)); + this.input.focus(this,function(e) { + if(e.data.input.val() == e.data.egw().lang(e.data.options.blur)) e.data.input.val(""); + }).blur(this, function(e) { + if(e.data.input.val() == "") e.data.input.val(e.data.egw().lang(e.data.options.blur)); + }); + } + } else { + if (!this.getValue()) this.input.val(''); + this.input.removeAttr("placeholder"); + } + this.options.blur = _value; + } + + set_autocomplete(_value) + { + this.options.autocomplete = _value; + this.input.attr('autocomplete', _value); + } + + resize(_height) + { + if (_height && this.options.multiline) + { + // apply the ratio + _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; + if (_height != 0) + { + this.input.height(this.input.height() + _height); + // resize parent too, so mailvelope injected into parent inherits its height + this.input.parent().height(this.input.parent().height()+_height); + } + } + } +} +et2_register_widget(et2_textbox, ["textbox", "passwd", "hidden"]); + +/** + * et2_textbox_ro is the dummy readonly implementation of the textbox. + * + * @augments et2_valueWidget + */ +class et2_textbox_ro extends et2_valueWidget implements et2_IDetachedDOM +{ + /** + * Ignore all more advanced attributes. + */ + static readonly _attributes : any = { + "multiline": { + "ignore": true + }, + "maxlength": { + "ignore": true + }, + "onchange": { + "ignore": true + }, + "rows": { + "ignore": true + }, + "cols": { + "ignore": true + }, + "size": { + "ignore": true + }, + "needed": { + "ignore": true + } + }; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + this.value = ""; + this.span = jQuery(document.createElement("label")) + .addClass("et2_label"); + this.value_span = jQuery(document.createElement("span")) + .addClass("et2_textbox_ro") + .appendTo(this.span); + + this.setDOMNode(this.span[0]); + } + + set_label(label) + { + // Remove current label + this.span.contents() + .filter(function(){ return this.nodeType == 3; }).remove(); + + var parts = et2_csvSplit(label, 2, "%s"); + this.span.prepend(parts[0]); + this.span.append(parts[1]); + this.label = label; + + // add class if label is empty + this.span.toggleClass('et2_label_empty', !label || !parts[0]); + } + + set_value(_value) + { + this.value = _value; + + if(!_value) + { + _value = ""; + } + if (this.label !="") + { + this.span.removeClass('et2_label_empty'); + } + else + { + this.span.addClass('et2_label_empty'); + } + this.value_span.text(_value); + } + + /** + * Code for implementing et2_IDetachedDOM + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "label"); + } + + getDetachedNodes() + { + return [this.span[0], this.value_span[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.span = jQuery(_nodes[0]); + this.value_span = jQuery(_nodes[1]); + if(typeof _values["label"] != 'undefined') + { + this.set_label(_values["label"]); + } + if(typeof _values["value"] != 'undefined') + { + this.set_value(_values["value"]); + } + } +} +et2_register_widget(et2_textbox_ro, ["textbox_ro"]); + +/** + * et2_searchbox is a widget which provides a collapsable input search + * with on searching indicator and clear handler regardless of any browser limitation. + */ +class et2_searchbox extends et2_textbox +{ + /** + * Advanced attributes + */ + static readonly _attributes : any = { + overlay:{ + name:"Overlay searchbox", + type:"boolean", + default:false, + description:"Define wheter the searchbox overlays while it's open (true) or stay as solid box infront of the search button (false). Default is false." + }, + fix: { + name:"Fix searchbox", + type:"boolean", + default:true, + description:"Define wheter the searchbox should be a fix input field or flexible search button. Default is true (fix)." + } + } + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + this.value = ""; + this.div = jQuery(document.createElement('div')) + .addClass('et2_searchbox'); + this.flex = jQuery(document.createElement('div')) + .addClass('flex') + .appendTo(this.div); + + //this._super.apply(this, arguments); + + this.setDOMNode(this.div[0]); + this._createWidget(); + } + + _createWidget() + { + var self = this; + if (this.options.overlay) this.flex.addClass('overlay'); + // search button indicator + // no need to create search button if it's a fix search field + if (!this.options.fix) + { + this.button = et2_createWidget('button',{image:"search","background_image":"1"},this); + this.button.onclick= function(){ + self._show_hide(jQuery(self.flex).hasClass('hide')); + self.search.input.focus(); + }; + this.div.prepend(this.button.getDOMNode()); + } + // input field + this.search = et2_createWidget('textbox',{"blur":egw.lang("search"), + onkeypress:function(event) { + if(event.which == 13) + { + event.preventDefault(); + self.getInstanceManager().autocomplete_fixer(); + // Use a timeout to make sure we get the autocomplete value, + // if one was chosen, instead of what was actually typed. + // Chrome doesn't need this, but FF does. + window.setTimeout(function() { + self.set_value(self.search.input.val()); + self.change(); + },0); + } + }},this); + // Autocomplete needs name + this.search.input.attr('name', this.id||'searchbox'); + this.search.input.on({ + keyup:function(event) + { + self.clear.toggle(self.get_value() !='' || !self.options.fix); + if(event.which == 27) // Escape + { + // Excape clears search + self.set_value(''); + } + }, + + blur(event){ + if (egwIsMobile()) return; + if (!event.relatedTarget || !jQuery(event.relatedTarget.parentNode).hasClass('et2_searchbox')) + { + self._show_hide((!self.options.overlay && self.get_value())); + } + if (typeof self.oldValue !='undefined' && self._oldValue != self.get_value()) { + self.change(); + } + }, + mousedown:function(event){ + if (event.target.type == 'span') event.stopImmidatePropagation(); + } + }); + this.flex.append(this.search.getDOMNode()); + + // clear button implementation + this.clear = jQuery(document.createElement('span')) + .addClass('ui-icon clear') + .toggle(!this.options.fix || (this._oldValue != '' && !jQuery.isEmptyObject(this._oldValue))) + .on('mousedown',function(event){ + event.preventDefault(); + }) + .on('click',function(event) { + if (self.get_value()){ + self.search.input.val(''); + self.search.input.focus(); + self._show_hide(true); + if (self._oldValue) self.change(); + } + else + { + self._show_hide(false); + } + if (self.options.fix) self.clear.hide(); + }) + .appendTo(this.flex); + } + + /** + * Show/hide search field + * @param {boolean} _stat true means show and false means hide + */ + _show_hide(_stat) + { + // Not applied for fix option + if (this.options.fix) return; + + jQuery(this.flex).toggleClass('hide',!_stat); + jQuery(this.getDOMNode()).toggleClass('expanded', _stat); + } + + /** + * toggle search button status based on value + */ + _searchToggleState() + { + if (this.options.fix || egwIsMobile()) return; + + if (!this.get_value()) + { + jQuery(this.button.getDOMNode()).removeClass('toolbar_toggled'); + this.button.set_statustext(''); + } + else + { + jQuery(this.button.getDOMNode()).addClass('toolbar_toggled'); + this.button.set_statustext(egw.lang("search for '%1'", this.get_value())); + } + } + + /** + * override change function in order to preset the toggle state + */ + change() + { + this._searchToggleState(); + + this._super.apply(this,arguments); + } + + + get_value() + { + return this.search.input.val(); + } + + set_value(_value) + { + super.set_value(_value); + if (this.search) this.search.input.val(_value); + } + + /** + * override doLoadingFinished in order to set initial state + */ + doLoadingFinished() + { + super.doLoadingFinished(); + if (!this.get_value()) { + this._show_hide(false); + } + else{ + this._show_hide(!this.options.overlay); + this._searchToggleState(); + } + } + + /** + * Overrride attachToDOM in order to unbind change handler + */ + attachToDOM() + { + super.attachToDOM(); + + var node = this.getInputNode(); + if (node) + { + jQuery(node).off('.et2_inputWidget'); + } + } +} +et2_register_widget(et2_searchbox, ["searchbox"]); From 13313cae6c7cd7bb21b6ffbd72aad50df0f7ba9a Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 21 Jan 2020 07:19:39 -0700 Subject: [PATCH 14/61] Make labelContainer protected for subclasses --- api/js/etemplate/et2_core_valueWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/js/etemplate/et2_core_valueWidget.ts b/api/js/etemplate/et2_core_valueWidget.ts index 4fc8a263c3..7e28d39339 100644 --- a/api/js/etemplate/et2_core_valueWidget.ts +++ b/api/js/etemplate/et2_core_valueWidget.ts @@ -42,7 +42,7 @@ export class et2_valueWidget extends et2_baseWidget }; label: string = ''; - private _labelContainer: JQuery = null; + protected _labelContainer: JQuery = null; /** * From f7bdd798d4e4f845d25bedf9601871fe98a1fa61 Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 21 Jan 2020 07:36:45 -0700 Subject: [PATCH 15/61] TS for Box widget --- api/js/etemplate/et2_core_widget.js | 4 + api/js/etemplate/et2_core_widget.ts | 4 +- api/js/etemplate/et2_widget_box.js | 403 +++++++++++++--------------- api/js/etemplate/et2_widget_box.ts | 262 ++++++++++++++++++ 4 files changed, 459 insertions(+), 214 deletions(-) create mode 100644 api/js/etemplate/et2_widget_box.ts diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js index 32d0395de7..36cdcc1ca9 100644 --- a/api/js/etemplate/et2_core_widget.js +++ b/api/js/etemplate/et2_core_widget.js @@ -124,6 +124,10 @@ var et2_widget = /** @class */ (function (_super) { // Set the legacyOptions array to the names of the properties the "options" // attribute defines. _this.legacyOptions = []; + /** + * Set this variable to true if this widget can have namespaces + */ + _this.createNamespace = false; _this._children = []; _this._mgrs = {}; /** diff --git a/api/js/etemplate/et2_core_widget.ts b/api/js/etemplate/et2_core_widget.ts index 9c276afa2f..177b7ff1d6 100644 --- a/api/js/etemplate/et2_core_widget.ts +++ b/api/js/etemplate/et2_core_widget.ts @@ -170,7 +170,7 @@ export class et2_widget extends ClassWithAttributes "ignore": true, "description": "Object of widget attributes" } - } + }; // Set the legacyOptions array to the names of the properties the "options" // attribute defines. @@ -185,7 +185,7 @@ export class et2_widget extends ClassWithAttributes /** * Set this variable to true if this widget can have namespaces */ - createNamespace: false; + createNamespace: boolean = false; /** * Widget constructor diff --git a/api/js/etemplate/et2_widget_box.js b/api/js/etemplate/et2_widget_box.js index 8fd5f98a13..99ebf4e84b 100644 --- a/api/js/etemplate/et2_widget_box.js +++ b/api/js/etemplate/et2_widget_box.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Box object * @@ -9,12 +10,28 @@ * @copyright Stylite 2011 * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; */ - +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +var jQuery = require("jquery"); +var et2_core_xml_1 = require("./et2_core_xml"); /** * Class which implements box and vbox tag * @@ -23,137 +40,108 @@ * * @augments et2_baseWidget */ -var et2_box = (function(){ "use strict"; return et2_baseWidget.extend([et2_IDetachedDOM], -{ - attributes: { - // Not needed - "rows": {"ignore": true}, - "cols": {"ignore": true} - }, - - createNamespace: true, - - /** - * Constructor - * - * @memberOf et2_box - */ - init: function() { - this._super.apply(this, arguments); - - this.div = jQuery(document.createElement("div")) - .addClass("et2_" + this._type) - .addClass("et2_box_widget"); - - this.setDOMNode(this.div[0]); - }, - - /** - * Overriden so we can check for autorepeating children. We only check for - * $ in the immediate children & grandchildren of this node. - * - * @param {object} _node - */ - loadFromXML: function(_node) { - if(this._type != "box") - { - return this._super.apply(this, arguments); - } - // Load the child nodes. - var childIndex = 0; - var repeatNode = null; - for (var i=0; i < _node.childNodes.length; i++) - { - var node = _node.childNodes[i]; - var widgetType = node.nodeName.toLowerCase(); - - if (widgetType == "#comment") - { - continue; - } - - if (widgetType == "#text") - { - if (node.data.replace(/^\s+|\s+$/g, '')) - { - this.loadContent(node.data); - } - continue; - } - - // Create the new element, if no expansion needed - var id = et2_readAttrWithDefault(node, "id", ""); - if(id.indexOf('$') < 0 || widgetType != 'box') - { - this.createElementFromNode(node); - childIndex++; - } - else - { - repeatNode = node; - } - } - - // Only the last child repeats(?) - if(repeatNode != null) - { - var currentPerspective = this.getArrayMgr("content").perspectiveData; - // Extra content - for(childIndex; typeof this.getArrayMgr("content").data[childIndex] != "undefined" && this.getArrayMgr("content").data[childIndex]; childIndex++) { - // Adjust for the row - var mgrs = this.getArrayMgrs(); - for(var name in mgrs) - { - if(this.getArrayMgr(name).getEntry(childIndex)) - { - this.getArrayMgr(name).perspectiveData.row = childIndex; - } - } - - this.createElementFromNode(repeatNode); - } - - // Reset - for(var name in this.getArrayMgrs()) - { - this.getArrayMgr(name).perspectiveData = currentPerspective; - } - } - }, - - /** - * Code for implementing et2_IDetachedDOM - * This doesn't need to be implemented. - * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push('data'); - }, - - getDetachedNodes: function() - { - return [this.getDOMNode()]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - if (_values.data) - { - var pairs = _values.data.split(/,/g); - for(var i=0; i < pairs.length; ++i) - { - var name_value = pairs[i].split(':'); - jQuery(_nodes[0]).attr('data-'+name_value[0], name_value[1]); - } - } - } - -});}).call(this); -et2_register_widget(et2_box, ["vbox", "box"]); - +var et2_box = /** @class */ (function (_super) { + __extends(et2_box, _super); + /** + * Constructor + * + * @memberOf et2_box + */ + function et2_box(_parent, _attrs, _child) { + var _this = _super.call(this, arguments) || this; + _this.createNamespace = true; + _this.div = jQuery(document.createElement("div")) + .addClass("et2_" + _this.getType()) + .addClass("et2_box_widget"); + _this.setDOMNode(_this.div[0]); + return _this; + } + /** + * Overriden so we can check for autorepeating children. We only check for + * $ in the immediate children & grandchildren of this node. + * + * @param {object} _node + */ + et2_box.prototype.loadFromXML = function (_node) { + if (this.getType() != "box") { + return _super.prototype.loadFromXML.call(this, _node); + } + // Load the child nodes. + var childIndex = 0; + var repeatNode = null; + for (var i = 0; i < _node.childNodes.length; i++) { + var node = _node.childNodes[i]; + var widgetType = node.nodeName.toLowerCase(); + if (widgetType == "#comment") { + continue; + } + if (widgetType == "#text") { + if (node.data.replace(/^\s+|\s+$/g, '')) { + this.loadContent(node.data); + } + continue; + } + // Create the new element, if no expansion needed + var id = et2_core_xml_1.et2_readAttrWithDefault(node, "id", ""); + if (id.indexOf('$') < 0 || widgetType != 'box') { + this.createElementFromNode(node); + childIndex++; + } + else { + repeatNode = node; + } + } + // Only the last child repeats(?) + if (repeatNode != null) { + var currentPerspective = this.getArrayMgr("content").perspectiveData; + // Extra content + for (childIndex; typeof this.getArrayMgr("content").data[childIndex] != "undefined" && this.getArrayMgr("content").data[childIndex]; childIndex++) { + // Adjust for the row + var mgrs = this.getArrayMgrs(); + for (var name in mgrs) { + if (this.getArrayMgr(name).getEntry(childIndex)) { + this.getArrayMgr(name).perspectiveData.row = childIndex; + } + } + this.createElementFromNode(repeatNode); + } + // Reset + for (var name in this.getArrayMgrs()) { + this.getArrayMgr(name).perspectiveData = currentPerspective; + } + } + }; + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {array} _attrs array to add further attributes to + */ + et2_box.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push('data'); + }; + et2_box.prototype.getDetachedNodes = function () { + return [this.getDOMNode()]; + }; + et2_box.prototype.setDetachedAttributes = function (_nodes, _values) { + if (_values.data) { + var pairs = _values.data.split(/,/g); + for (var i = 0; i < pairs.length; ++i) { + var name_value = pairs[i].split(':'); + jQuery(_nodes[0]).attr('data-' + name_value[0], name_value[1]); + } + } + }; + et2_box._attributes = { + // Not needed + "rows": { "ignore": true }, + "cols": { "ignore": true } + }; + return et2_box; +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_box = et2_box; +et2_core_widget_1.et2_register_widget(et2_box, ["vbox", "box"]); /** * Details widget implementation * widget name is "details" and can be use as a wrapping container @@ -167,80 +155,71 @@ et2_register_widget(et2_box, ["vbox", "box"]); *
* */ -var et2_details = (function(){ "use strict"; return et2_box.extend( -{ - attributes:{ - "toggle_align": { - name: "Toggle button alignment", - description:" Defines where to align the toggle button, default is right alignment", - type:"string", - default: "right" - }, - title: { - name: "title", - description:"Set a header title for box and shows it next to toggle button, default is no title", - type:"string", - default: "", - translate: true - } - }, - - init: function() { - this._super.apply(this, arguments); - - this.div = jQuery(document.createElement('div')).addClass('et2_details'); - this.title = jQuery(document.createElement('span')) - .addClass('et2_label et2_details_title') - .appendTo(this.div); - this.span = jQuery(document.createElement('span')) - .addClass('et2_details_toggle') - .appendTo(this.div); - this.wrapper = jQuery(document.createElement('div')) - .addClass('et2_details_wrapper') - .appendTo(this.div); - - - this._createWidget(); - }, - - /** - * Function happens on toggle action - */ - _toggle: function (){ - this.div.toggleClass('et2_details_expanded'); - }, - - /** - * Create widget, set contents, and binds handlers - */ - _createWidget: function () { - var self = this; - - this.span.on('click', function (e){ - self._toggle(); - }); - - //Set header title - if (this.options.title) - { - this.title - .click (function(){self._toggle();}) - .text(this.options.title); - } - - // Align toggle button left/right - if (this.options.toggle_align === "left") this.span.css({float:'left'}); - }, - - getDOMNode: function(_sender) { - if (!_sender || _sender === this) - { - return this.div[0]; - } - else - { - return this.wrapper[0]; - } - } -});}).call(this); -et2_register_widget(et2_details, ["details"]); \ No newline at end of file +var et2_details = /** @class */ (function (_super) { + __extends(et2_details, _super); + function et2_details(_parent, _attrs, _child) { + var _this = _super.call(this, _parent, _attrs, _child) || this; + _this.div = jQuery(document.createElement('div')).addClass('et2_details'); + _this.title = jQuery(document.createElement('span')) + .addClass('et2_label et2_details_title') + .appendTo(_this.div); + _this.span = jQuery(document.createElement('span')) + .addClass('et2_details_toggle') + .appendTo(_this.div); + _this.wrapper = jQuery(document.createElement('div')) + .addClass('et2_details_wrapper') + .appendTo(_this.div); + _this._createWidget(); + return _this; + } + /** + * Function happens on toggle action + */ + et2_details.prototype._toggle = function () { + this.div.toggleClass('et2_details_expanded'); + }; + /** + * Create widget, set contents, and binds handlers + */ + et2_details.prototype._createWidget = function () { + var self = this; + this.span.on('click', function (e) { + self._toggle(); + }); + //Set header title + if (this.options.title) { + this.title + .click(function () { self._toggle(); }) + .text(this.options.title); + } + // Align toggle button left/right + if (this.options.toggle_align === "left") + this.span.css({ float: 'left' }); + }; + et2_details.prototype.getDOMNode = function (_sender) { + if (!_sender || _sender === this) { + return this.div[0]; + } + else { + return this.wrapper[0]; + } + }; + et2_details._attributes = { + "toggle_align": { + name: "Toggle button alignment", + description: " Defines where to align the toggle button, default is right alignment", + type: "string", + default: "right" + }, + title: { + name: "title", + description: "Set a header title for box and shows it next to toggle button, default is no title", + type: "string", + default: "", + translate: true + } + }; + return et2_details; +}(et2_box)); +exports.et2_details = et2_details; +et2_core_widget_1.et2_register_widget(et2_details, ["details"]); diff --git a/api/js/etemplate/et2_widget_box.ts b/api/js/etemplate/et2_widget_box.ts new file mode 100644 index 0000000000..6076c24284 --- /dev/null +++ b/api/js/etemplate/et2_widget_box.ts @@ -0,0 +1,262 @@ +/** + * EGroupware eTemplate2 - JS Box object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_baseWidget; +*/ + + +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_baseWidget} from "./et2_core_baseWidget"; +import * as jQuery from "jquery"; +import {et2_readAttrWithDefault} from './et2_core_xml' + +/** + * Class which implements box and vbox tag + * + * Auto-repeat: In order to get box auto repeat to work we need to have another + * box as a wrapper with an id set. + * + * @augments et2_baseWidget + */ +export class et2_box extends et2_baseWidget implements et2_IDetachedDOM +{ + static readonly _attributes: any = { + // Not needed + "rows": {"ignore": true}, + "cols": {"ignore": true} + }; + + createNamespace: boolean = true; + div: JQuery; + + /** + * Constructor + * + * @memberOf et2_box + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + super(arguments); + + this.div = jQuery(document.createElement("div")) + .addClass("et2_" + this.getType()) + .addClass("et2_box_widget"); + + this.setDOMNode(this.div[0]); + } + + /** + * Overriden so we can check for autorepeating children. We only check for + * $ in the immediate children & grandchildren of this node. + * + * @param {object} _node + */ + loadFromXML(_node) { + if(this.getType() != "box") + { + return super.loadFromXML(_node); + } + // Load the child nodes. + var childIndex = 0; + var repeatNode = null; + for (var i=0; i < _node.childNodes.length; i++) + { + var node = _node.childNodes[i]; + var widgetType = node.nodeName.toLowerCase(); + + if (widgetType == "#comment") + { + continue; + } + + if (widgetType == "#text") + { + if (node.data.replace(/^\s+|\s+$/g, '')) + { + this.loadContent(node.data); + } + continue; + } + + // Create the new element, if no expansion needed + var id = et2_readAttrWithDefault(node, "id", ""); + if(id.indexOf('$') < 0 || widgetType != 'box') + { + this.createElementFromNode(node); + childIndex++; + } + else + { + repeatNode = node; + } + } + + // Only the last child repeats(?) + if(repeatNode != null) + { + var currentPerspective = this.getArrayMgr("content").perspectiveData; + // Extra content + for(childIndex; typeof this.getArrayMgr("content").data[childIndex] != "undefined" && this.getArrayMgr("content").data[childIndex]; childIndex++) { + // Adjust for the row + var mgrs = this.getArrayMgrs(); + for(var name in mgrs) + { + if(this.getArrayMgr(name).getEntry(childIndex)) + { + this.getArrayMgr(name).perspectiveData.row = childIndex; + } + } + + this.createElementFromNode(repeatNode); + } + + // Reset + for(var name in this.getArrayMgrs()) + { + this.getArrayMgr(name).perspectiveData = currentPerspective; + } + } + } + + /** + * Code for implementing et2_IDetachedDOM + * This doesn't need to be implemented. + * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen + * + * @param {array} _attrs array to add further attributes to + */ + getDetachedAttributes(_attrs) + { + _attrs.push('data'); + } + + getDetachedNodes() + { + return [this.getDOMNode()]; + } + + setDetachedAttributes(_nodes, _values) + { + if (_values.data) + { + var pairs = _values.data.split(/,/g); + for(var i=0; i < pairs.length; ++i) + { + var name_value = pairs[i].split(':'); + jQuery(_nodes[0]).attr('data-'+name_value[0], name_value[1]); + } + } + } + +} +et2_register_widget(et2_box, ["vbox", "box"]); + +/** + * Details widget implementation + * widget name is "details" and can be use as a wrapping container + * in order to make its children collapsible. + * + * Note: details widget does not represent html5 "details" tag in DOM + * + *
+ * + * .... + *
+ * + */ +export class et2_details extends et2_box +{ + static readonly _attributes: any = { + "toggle_align": { + name: "Toggle button alignment", + description:" Defines where to align the toggle button, default is right alignment", + type:"string", + default: "right" + }, + title: { + name: "title", + description:"Set a header title for box and shows it next to toggle button, default is no title", + type:"string", + default: "", + translate: true + } + }; + + private title : JQuery; + private span : JQuery; + private wrapper : JQuery; + + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, _child); + + this.div = jQuery(document.createElement('div')).addClass('et2_details'); + this.title = jQuery(document.createElement('span')) + .addClass('et2_label et2_details_title') + .appendTo(this.div); + this.span = jQuery(document.createElement('span')) + .addClass('et2_details_toggle') + .appendTo(this.div); + this.wrapper = jQuery(document.createElement('div')) + .addClass('et2_details_wrapper') + .appendTo(this.div); + + + this._createWidget(); + } + + /** + * Function happens on toggle action + */ + _toggle () + { + this.div.toggleClass('et2_details_expanded'); + } + + /** + * Create widget, set contents, and binds handlers + */ + _createWidget() + { + var self = this; + + this.span.on('click', function (e){ + self._toggle(); + }); + + //Set header title + if (this.options.title) + { + this.title + .click (function(){self._toggle();}) + .text(this.options.title); + } + + // Align toggle button left/right + if (this.options.toggle_align === "left") this.span.css({float:'left'}); + } + + getDOMNode(_sender) + { + if (!_sender || _sender === this) + { + return this.div[0]; + } + else + { + return this.wrapper[0]; + } + } +} +et2_register_widget(et2_details, ["details"]); \ No newline at end of file From 487cebc56d0fb66959f719b728f3a5e4f33d2424 Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 21 Jan 2020 07:43:04 -0700 Subject: [PATCH 16/61] Pass individual parameters --- api/js/etemplate/et2_widget_box.js | 2 +- api/js/etemplate/et2_widget_box.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/js/etemplate/et2_widget_box.js b/api/js/etemplate/et2_widget_box.js index 99ebf4e84b..f065b634d2 100644 --- a/api/js/etemplate/et2_widget_box.js +++ b/api/js/etemplate/et2_widget_box.js @@ -48,7 +48,7 @@ var et2_box = /** @class */ (function (_super) { * @memberOf et2_box */ function et2_box(_parent, _attrs, _child) { - var _this = _super.call(this, arguments) || this; + var _this = _super.call(this, _parent, _attrs, _child) || this; _this.createNamespace = true; _this.div = jQuery(document.createElement("div")) .addClass("et2_" + _this.getType()) diff --git a/api/js/etemplate/et2_widget_box.ts b/api/js/etemplate/et2_widget_box.ts index 6076c24284..7fdc9bd70c 100644 --- a/api/js/etemplate/et2_widget_box.ts +++ b/api/js/etemplate/et2_widget_box.ts @@ -47,7 +47,7 @@ export class et2_box extends et2_baseWidget implements et2_IDetachedDOM */ constructor(_parent, _attrs? : WidgetConfig, _child? : object) { - super(arguments); + super(_parent, _attrs, _child); this.div = jQuery(document.createElement("div")) .addClass("et2_" + this.getType()) From 5c6f73a26ee17cf4504f6f13510926bf920eba11 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 15:54:26 +0100 Subject: [PATCH 17/61] textbox and button widget --- api/js/etemplate/et2_core_inputWidget.ts | 2 +- api/js/etemplate/et2_core_interfaces.ts | 2 +- api/js/etemplate/et2_types.d.ts | 5 +- api/js/etemplate/et2_widget_button.js | 809 +++++++++++------------ api/js/etemplate/et2_widget_button.ts | 459 +++++++++++++ api/js/etemplate/et2_widget_textbox.js | 46 +- api/js/etemplate/et2_widget_textbox.ts | 48 +- 7 files changed, 898 insertions(+), 473 deletions(-) create mode 100644 api/js/etemplate/et2_widget_button.ts diff --git a/api/js/etemplate/et2_core_inputWidget.ts b/api/js/etemplate/et2_core_inputWidget.ts index 819cdbc9e0..f3ea3d778a 100644 --- a/api/js/etemplate/et2_core_inputWidget.ts +++ b/api/js/etemplate/et2_core_inputWidget.ts @@ -67,7 +67,7 @@ export class et2_inputWidget extends et2_valueWidget implements et2_IInput, et2_ } } - private _oldValue: any; + protected _oldValue: any; onchange: Function; /** diff --git a/api/js/etemplate/et2_core_interfaces.ts b/api/js/etemplate/et2_core_interfaces.ts index bb8a31182f..3f8ebf5928 100644 --- a/api/js/etemplate/et2_core_interfaces.ts +++ b/api/js/etemplate/et2_core_interfaces.ts @@ -106,7 +106,7 @@ interface et2_IResizeable /** * Called whenever the window is resized */ - resize() : void + resize(number) : void } var et2_IResizeable = "et2_IResizeable"; function implements_et2_IResizeable(obj : et2_widget) diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index d74a881a53..4953e5712b 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -14,6 +14,10 @@ declare class et2_tabbox extends et2_valueWidget { tabData : any; activateTab(et2_widget); } +declare class et2_button extends et2_DOMWidget { + click() : boolean + onclick: Function +} declare var et2_surroundingsMgr : any; declare var et2_arrayMgr : any; declare var et2_readonlysArrayMgr : any; @@ -68,7 +72,6 @@ declare var et2_ajaxSelect_ro : any; declare var et2_barcode : any; declare var et2_box : any; declare var et2_details : any; -declare var et2_button : any; declare var et2_checkbox : any; declare var et2_checkbox_ro : any; declare var et2_color : any; diff --git a/api/js/etemplate/et2_widget_button.js b/api/js/etemplate/et2_widget_button.js index 73d6590523..6961dd20ef 100644 --- a/api/js/etemplate/et2_widget_button.js +++ b/api/js/etemplate/et2_widget_button.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Button object * @@ -6,440 +7,386 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_baseWidget; */ - +require("./et2_core_common"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +require("./et2_types"); /** * Class which implements the "button" XET-Tag - * @augments et2_baseWidget */ -var et2_button = (function(){ "use strict"; return et2_baseWidget.extend([et2_IInput, et2_IDetachedDOM], -{ - attributes: { - "label": { - "name": "caption", - "type": "string", - "description": "Label of the button", - "translate": true - }, - "image": { - "name": "Icon", - "type": "string", - "description": "Use an icon instead of label (when available)" - }, - "ro_image": { - "name": "Read-only Icon", - "type": "string", - "description": "Use this icon instead of hiding for read-only" - }, - "onclick": { - "description": "JS code which gets executed when the button is clicked" - }, - "accesskey": { - "name": "Access Key", - "type": "string", - "default": et2_no_init, - "description": "Alt + activates widget" - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - background_image: { - name: "Add image in front of text", - type: "boolean", - description: "Adds image in front of text instead of just using an image with text as tooltip", - default: et2_no_init // to leave it undefined, if not defined, so background-image is assigned by default - }, - novalidate: { - name: "Do NOT validate form", - type: "boolean", - description: "Do NOT validate form before submitting it", - default: false - }, - // No such thing as a required button - "needed": { - "ignore": true - } - }, - - legacyOptions: ["image", "ro_image"], - - /** - * Constructor - * - * @memberOf et2_button - */ - init: function() { - this._super.apply(this, arguments); - - this.label = ""; - this.clicked = false; - this.btn = null; - this.image = null; - - if (!this.options.background_image && (this.options.image || this.options.ro_image)) - { - this.image = jQuery(document.createElement("img")) - .addClass("et2_button et2_button_icon"); - if (!this.options.readonly) this.image.addClass("et2_clickable"); - this.setDOMNode(this.image[0]); - return; - } - if (!this.options.readonly || this.options.ro_image) - { - this.btn = jQuery(document.createElement("button")) - .addClass("et2_button") - .attr({type:"button"}); - this.setDOMNode(this.btn[0]); - } - if (this.options.image) this.set_image(this.options.image); - }, - - /** - * Apply the "modifications" to the element and translate attributes marked - * with "translate: true" - * - * Reimplemented here to assign default background-images to buttons - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) - { - if (this.id && typeof _attrs.background_image == 'undefined' && !_attrs.image) - { - for(var image in et2_button.default_background_images) - { - if (this.id.match(et2_button.default_background_images[image])) - { - _attrs.image = image; - _attrs.background_image = true; - break; - } - } - } - for(var name in et2_button.default_classes) - { - if (this.id.match(et2_button.default_classes[name])) - { - _attrs.class = (typeof _attrs.class == 'undefined' ? '' : _attrs.class+' ')+name; - break; - } - } - this._super.apply(this, arguments); - }, - - set_accesskey: function(key) { - jQuery(this.node).attr("accesskey", key); - }, - /** - * Set image and update current image - * - * @param _image - */ - set_image: function(_image) { - this.options.image = _image; - this.update_image(); - }, - /** - * Set readonly image and update current image - * - * @param _image - */ - set_ro_image: function(_image) { - this.options.ro_image = _image; - this.update_image(); - }, - /** - * Set current image (dont update options.image) - * - * @param _image - */ - update_image: function(_image) { - if(!this.isInTree() || !this.options.background_image && this.image == null) return; - - if (typeof _image == 'undefined') - _image = this.options.readonly ? (this.options.ro_image || this.options.image) : this.options.image; - - // Silently blank for percentages instead of warning about missing image - use a progress widget - if(_image.match(/^[0-9]+\%$/)) - { - _image = ""; - //this.egw().debug("warn", "Use a progress widget instead of percentage images", this); - } - - var found_image = false; - if(_image != "") - { - var src = this.egw().image(_image); - if(src) - { - found_image = true; - } - else if (_image[0] == '/' || _image.substr(0,4) == 'http') - { - src= image; - found_image = true; - } - if(found_image) - { - if(this.image != null) - { - this.image.attr("src", src); - } - else if (this.options.background_image && this.btn) - { - this.btn.css("background-image","url("+src+")"); - this.btn.addClass('et2_button_with_image'); - } - } - } - if(!found_image) - { - this.set_label(this.label); - if(this.btn) - { - this.btn.css("background-image",""); - this.btn.removeClass('et2_button_with_image'); - } - } - }, - - /** - * Set options.readonly and update image - * - * @param {boolean} _ro - */ - set_readonly: function(_ro) - { - if (_ro != this.options.readonly) - { - this.options.readonly = _ro; - - if (this.options.image || this.options.ro_image) - { - this.update_image(); - } - // dont show readonly buttons as clickable - if (this.btn || this.image) - { - (this.btn || this.image) - .toggleClass('et2_clickable', !_ro) - .toggleClass('et2_button_ro', _ro) - .css('cursor', _ro ? 'default' : 'pointer'); // temp. 'til it is removed from et2_button - } - } - }, - - attachToDOM: function() { - this._super.apply(this, arguments); - - if (this.options.readonly && (this.btn || this.image)) - { - (this.btn || this.image) - .removeClass('et2_clickable') - .addClass('et2_button_ro') - .css('cursor', 'default'); // temp. 'til it is removed from et2_button - } - }, - - getDOMNode: function() { - return this.btn ? this.btn[0] : (this.image ? this.image[0] : null); - }, - - /** - * Overwritten to maintain an internal clicked attribute - * - * @param _ev - * @returns {Boolean} - */ - click: function(_ev) { - // ignore click on readonly button - if (this.options.readonly) return false; - - this.clicked = true; - - if (!this._super.apply(this, arguments)) - { - this.clicked = false; - return false; - } - - // Submit the form - if (this._type != "buttononly") - { - this.getInstanceManager().submit(this, false, this.options.novalidate); //TODO: this only needs to be passed if it's in a datagrid - } - this.clicked = false; - return true; - }, - - set_label: function(_value) { - if (this.btn) - { - this.label = _value; - - this.btn.text(_value); - - if (_value && !this.image) - this.btn.addClass('et2_button_text'); - else - this.btn.removeClass('et2_button_text'); - } - if(this.image) - { - this.image.attr("alt", _value); - // Don't set title if there's a tooltip, browser may show both - if(!this.options.statustext) - { - this.image.attr("title",_value); - } - } - }, - - /** - * Set tab index - * - * @param {number} index - */ - set_tabindex: function(index) { - jQuery(this.btn).attr("tabindex", index); - }, - - /** - * Implementation of the et2_IInput interface - */ - - /** - * Always return false as a button is never dirty - */ - isDirty: function() { - return false; - }, - - resetDirty: function() { - }, - - getValue: function() { - if (this.clicked) - { - return true; - } - - // If "null" is returned, the result is not added to the submitted - // array. - return null; - }, - isValid: function() { - return true; - }, - - /** - * et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("label", "value", "class", "image", "ro_image", "onclick", "background_image" ); - }, - - getDetachedNodes: function() - { - return [ - this.btn != null ? this.btn[0] : null, - this.image != null ? this.image[0] : null - ]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - // Datagrid puts in the row for null - this.btn = _nodes[0].nodeName[0] != '#' ? jQuery(_nodes[0]) : null; - this.image = jQuery(_nodes[1]); - - if (typeof _values["id"] != "undefined") - { - this.set_id(_values["id"]); - } - if (typeof _values["label"] != "undefined") - { - this.set_label(_values["label"]); - } - if (typeof _values["value"] != "undefined") - { - } - if (typeof _values["image"] != "undefined") - { - this.set_image(_values["image"]); - } - if (typeof _values["ro_image"] != "undefined") - { - this.set_ro_image(_values["ro_image"]); - } - if (typeof _values["class"] != "undefined") - { - this.set_class(_values["class"]); - } - - if (typeof _values["onclick"] != "undefined") - { - this.options.onclick = _values["onclick"]; - } - var type = this._type; - var attrs = jQuery.extend(_values, this.options); - var parent = this._parent; - jQuery(this.getDOMNode()).bind("click.et2_baseWidget", this, function(e) { - var widget = et2_createWidget(type,attrs,parent); - e.data = widget; - e.data.set_id(_values["id"]); - return e.data.click.call(e.data,e); - }); - } -});}).call(this); -et2_register_widget(et2_button, ["button", "buttononly"]); - -// Static class stuff -jQuery.extend(et2_button, -/** @lends et2_button */ -{ - /** - * images to be used as background-image, if none is explicitly applied and id matches given regular expression - */ - default_background_images: { - save: /save(&|\]|$)/, - apply: /apply(&|\]|$)/, - cancel: /cancel(&|\]|$)/, - delete: /delete(&|\]|$)/, - discard: /discard(&|\]|$)/, - edit: /edit(&|\[\]|$)/, - next: /(next|continue)(&|\]|$)/, - finish: /finish(&|\]|$)/, - back: /(back|previous)(&|\]|$)/, - copy: /copy(&|\]|$)/, - more: /more(&|\]|$)/, - check: /(yes|check)(&|\]|$)/, - cancelled: /no(&|\]|$)/, - ok: /ok(&|\]|$)/, - close: /close(&|\]|$)/, - add: /(add(&|\]|$)|create)/ // customfields use create* - }, - - /** - * Classnames added automatic to buttons to set certain hover background colors - */ - default_classes: { - et2_button_cancel: /cancel(&|\]|$)/, // yellow - et2_button_question: /(yes|no)(&|\]|$)/, // yellow - et2_button_delete: /delete(&|\]|$)/ // red - } -}); +var et2_button = /** @class */ (function (_super) { + __extends(et2_button, _super); + /** + * Constructor + */ + function et2_button(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.legacyOptions = ["image", "ro_image"]; + _this.label = ""; + _this.clicked = false; + _this.btn = null; + _this.image = null; + if (!_this.options.background_image && (_this.options.image || _this.options.ro_image)) { + _this.image = jQuery(document.createElement("img")) + .addClass("et2_button et2_button_icon"); + if (!_this.options.readonly) + _this.image.addClass("et2_clickable"); + _this.setDOMNode(_this.image[0]); + return _this; + } + if (!_this.options.readonly || _this.options.ro_image) { + _this.btn = jQuery(document.createElement("button")) + .addClass("et2_button") + .attr({ type: "button" }); + _this.setDOMNode(_this.btn[0]); + } + if (_this.options.image) + _this.set_image(_this.options.image); + return _this; + } + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * Reimplemented here to assign default background-images to buttons + * + * @param {object} _attrs + */ + et2_button.prototype.transformAttributes = function (_attrs) { + if (this.id && typeof _attrs.background_image == 'undefined' && !_attrs.image) { + for (var image in et2_button.default_background_images) { + if (this.id.match(et2_button.default_background_images[image])) { + _attrs.image = image; + _attrs.background_image = true; + break; + } + } + } + for (var name in et2_button.default_classes) { + if (this.id.match(et2_button.default_classes[name])) { + _attrs.class = (typeof _attrs.class == 'undefined' ? '' : _attrs.class + ' ') + name; + break; + } + } + _super.prototype.transformAttributes.call(this, _attrs); + }; + et2_button.prototype.set_accesskey = function (key) { + jQuery(this.node).attr("accesskey", key); + }; + /** + * Set image and update current image + * + * @param _image + */ + et2_button.prototype.set_image = function (_image) { + this.options.image = _image; + this.update_image(); + }; + /** + * Set readonly image and update current image + * + * @param _image + */ + et2_button.prototype.set_ro_image = function (_image) { + this.options.ro_image = _image; + this.update_image(); + }; + /** + * Set current image (dont update options.image) + * + * @param _image + */ + et2_button.prototype.update_image = function (_image) { + if (!this.isInTree() || !this.options.background_image && this.image == null) + return; + if (typeof _image == 'undefined') + _image = this.options.readonly ? (this.options.ro_image || this.options.image) : this.options.image; + // Silently blank for percentages instead of warning about missing image - use a progress widget + if (_image.match(/^[0-9]+\%$/)) { + _image = ""; + //this.egw().debug("warn", "Use a progress widget instead of percentage images", this); + } + var found_image = false; + if (_image != "") { + var src = this.egw().image(_image); + if (src) { + found_image = true; + } + else if (_image[0] == '/' || _image.substr(0, 4) == 'http') { + src = _image; + found_image = true; + } + if (found_image) { + if (this.image != null) { + this.image.attr("src", src); + } + else if (this.options.background_image && this.btn) { + this.btn.css("background-image", "url(" + src + ")"); + this.btn.addClass('et2_button_with_image'); + } + } + } + if (!found_image) { + this.set_label(this.label); + if (this.btn) { + this.btn.css("background-image", ""); + this.btn.removeClass('et2_button_with_image'); + } + } + }; + /** + * Set options.readonly and update image + * + * @param {boolean} _ro + */ + et2_button.prototype.set_readonly = function (_ro) { + if (_ro != this.options.readonly) { + this.options.readonly = _ro; + if (this.options.image || this.options.ro_image) { + this.update_image(); + } + // dont show readonly buttons as clickable + if (this.btn || this.image) { + (this.btn || this.image) + .toggleClass('et2_clickable', !_ro) + .toggleClass('et2_button_ro', _ro) + .css('cursor', _ro ? 'default' : 'pointer'); // temp. 'til it is removed from et2_button + } + } + }; + et2_button.prototype.attachToDOM = function () { + var ret = _super.prototype.attachToDOM.call(this); + if (this.options.readonly && (this.btn || this.image)) { + (this.btn || this.image) + .removeClass('et2_clickable') + .addClass('et2_button_ro') + .css('cursor', 'default'); // temp. 'til it is removed from et2_button + } + return ret; + }; + et2_button.prototype.getDOMNode = function () { + return this.btn ? this.btn[0] : (this.image ? this.image[0] : null); + }; + /** + * Overwritten to maintain an internal clicked attribute + * + * @param _ev + * @returns {Boolean} + */ + et2_button.prototype.click = function (_ev) { + // ignore click on readonly button + if (this.options.readonly) + return false; + this.clicked = true; + if (!_super.prototype.click.apply(this, arguments)) { + this.clicked = false; + return false; + } + // Submit the form + if (this.getType() != "buttononly") { + this.getInstanceManager().submit(this, false, this.options.novalidate); //TODO: this only needs to be passed if it's in a datagrid + } + this.clicked = false; + return true; + }; + et2_button.prototype.set_label = function (_value) { + if (this.btn) { + this.label = _value; + this.btn.text(_value); + if (_value && !this.image) + this.btn.addClass('et2_button_text'); + else + this.btn.removeClass('et2_button_text'); + } + if (this.image) { + this.image.attr("alt", _value); + // Don't set title if there's a tooltip, browser may show both + if (!this.options.statustext) { + this.image.attr("title", _value); + } + } + }; + /** + * Set tab index + * + * @param {number} index + */ + et2_button.prototype.set_tabindex = function (index) { + jQuery(this.btn).attr("tabindex", index); + }; + /** + * Implementation of the et2_IInput interface + */ + /** + * Always return false as a button is never dirty + */ + et2_button.prototype.isDirty = function () { + return false; + }; + et2_button.prototype.resetDirty = function () { + }; + et2_button.prototype.getValue = function () { + if (this.clicked) { + return true; + } + // If "null" is returned, the result is not added to the submitted + // array. + return null; + }; + et2_button.prototype.isValid = function () { + return true; + }; + /** + * et2_IDetachedDOM + * + * @param {array} _attrs + */ + et2_button.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("label", "value", "class", "image", "ro_image", "onclick", "background_image"); + }; + et2_button.prototype.getDetachedNodes = function () { + return [ + this.btn != null ? this.btn[0] : null, + this.image != null ? this.image[0] : null + ]; + }; + et2_button.prototype.setDetachedAttributes = function (_nodes, _values) { + // Datagrid puts in the row for null + this.btn = _nodes[0].nodeName[0] != '#' ? jQuery(_nodes[0]) : null; + this.image = jQuery(_nodes[1]); + if (typeof _values["id"] != "undefined") { + this.set_id(_values["id"]); + } + if (typeof _values["label"] != "undefined") { + this.set_label(_values["label"]); + } + if (typeof _values["value"] != "undefined") { + } + if (typeof _values["image"] != "undefined") { + this.set_image(_values["image"]); + } + if (typeof _values["ro_image"] != "undefined") { + this.set_ro_image(_values["ro_image"]); + } + if (typeof _values["class"] != "undefined") { + this.set_class(_values["class"]); + } + if (typeof _values["onclick"] != "undefined") { + this.options.onclick = _values["onclick"]; + } + var type = this.getType(); + var attrs = jQuery.extend(_values, this.options); + var parent = this.getParent(); + jQuery(this.getDOMNode()).bind("click.et2_baseWidget", this, function (e) { + var widget = et2_core_widget_1.et2_createWidget(type, attrs, parent); + e.data = widget; + e.data.set_id(_values["id"]); + return e.data.click.call(e.data, e); + }); + }; + et2_button._attributes = { + "label": { + "name": "caption", + "type": "string", + "description": "Label of the button", + "translate": true + }, + "image": { + "name": "Icon", + "type": "string", + "description": "Use an icon instead of label (when available)" + }, + "ro_image": { + "name": "Read-only Icon", + "type": "string", + "description": "Use this icon instead of hiding for read-only" + }, + "onclick": { + "description": "JS code which gets executed when the button is clicked" + }, + "accesskey": { + "name": "Access Key", + "type": "string", + "default": et2_no_init, + "description": "Alt + activates widget" + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + background_image: { + name: "Add image in front of text", + type: "boolean", + description: "Adds image in front of text instead of just using an image with text as tooltip", + default: et2_no_init // to leave it undefined, if not defined, so background-image is assigned by default + }, + novalidate: { + name: "Do NOT validate form", + type: "boolean", + description: "Do NOT validate form before submitting it", + default: false + }, + // No such thing as a required button + "needed": { + "ignore": true + } + }; + /** + * images to be used as background-image, if none is explicitly applied and id matches given regular expression + */ + et2_button.default_background_images = { + save: /save(&|\]|$)/, + apply: /apply(&|\]|$)/, + cancel: /cancel(&|\]|$)/, + delete: /delete(&|\]|$)/, + discard: /discard(&|\]|$)/, + edit: /edit(&|\[\]|$)/, + next: /(next|continue)(&|\]|$)/, + finish: /finish(&|\]|$)/, + back: /(back|previous)(&|\]|$)/, + copy: /copy(&|\]|$)/, + more: /more(&|\]|$)/, + check: /(yes|check)(&|\]|$)/, + cancelled: /no(&|\]|$)/, + ok: /ok(&|\]|$)/, + close: /close(&|\]|$)/, + add: /(add(&|\]|$)|create)/ // customfields use create* + }; + /** + * Classnames added automatic to buttons to set certain hover background colors + */ + et2_button.default_classes = { + et2_button_cancel: /cancel(&|\]|$)/, + et2_button_question: /(yes|no)(&|\]|$)/, + et2_button_delete: /delete(&|\]|$)/ // red + }; + return et2_button; +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_button = et2_button; +et2_core_widget_1.et2_register_widget(et2_button, ["button", "buttononly"]); diff --git a/api/js/etemplate/et2_widget_button.ts b/api/js/etemplate/et2_widget_button.ts new file mode 100644 index 0000000000..31bf8952e4 --- /dev/null +++ b/api/js/etemplate/et2_widget_button.ts @@ -0,0 +1,459 @@ +/** + * EGroupware eTemplate2 - JS Button object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_baseWidget; +*/ + +import './et2_core_common'; +import { ClassWithAttributes } from "./et2_core_inheritance"; +import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from "./et2_core_widget"; +import { et2_DOMWidget } from './et2_core_DOMWidget' +import { et2_baseWidget } from './et2_core_baseWidget' +import './et2_types'; + +/** + * Class which implements the "button" XET-Tag + */ +export class et2_button extends et2_baseWidget implements et2_IInput, et2_IDetachedDOM +{ + static readonly _attributes : any = { + "label": { + "name": "caption", + "type": "string", + "description": "Label of the button", + "translate": true + }, + "image": { + "name": "Icon", + "type": "string", + "description": "Use an icon instead of label (when available)" + }, + "ro_image": { + "name": "Read-only Icon", + "type": "string", + "description": "Use this icon instead of hiding for read-only" + }, + "onclick": { + "description": "JS code which gets executed when the button is clicked" + }, + "accesskey": { + "name": "Access Key", + "type": "string", + "default": et2_no_init, + "description": "Alt + activates widget" + }, + "tabindex": { + "name": "Tab index", + "type": "integer", + "default": et2_no_init, + "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." + }, + background_image: { + name: "Add image in front of text", + type: "boolean", + description: "Adds image in front of text instead of just using an image with text as tooltip", + default: et2_no_init // to leave it undefined, if not defined, so background-image is assigned by default + }, + novalidate: { + name: "Do NOT validate form", + type: "boolean", + description: "Do NOT validate form before submitting it", + default: false + }, + // No such thing as a required button + "needed": { + "ignore": true + } + }; + + legacyOptions: string[] = ["image", "ro_image"]; + + /** + * images to be used as background-image, if none is explicitly applied and id matches given regular expression + */ + static readonly default_background_images: object = { + save: /save(&|\]|$)/, + apply: /apply(&|\]|$)/, + cancel: /cancel(&|\]|$)/, + delete: /delete(&|\]|$)/, + discard: /discard(&|\]|$)/, + edit: /edit(&|\[\]|$)/, + next: /(next|continue)(&|\]|$)/, + finish: /finish(&|\]|$)/, + back: /(back|previous)(&|\]|$)/, + copy: /copy(&|\]|$)/, + more: /more(&|\]|$)/, + check: /(yes|check)(&|\]|$)/, + cancelled: /no(&|\]|$)/, + ok: /ok(&|\]|$)/, + close: /close(&|\]|$)/, + add: /(add(&|\]|$)|create)/ // customfields use create* + }; + + /** + * Classnames added automatic to buttons to set certain hover background colors + */ + static readonly default_classes: object = { + et2_button_cancel: /cancel(&|\]|$)/, // yellow + et2_button_question: /(yes|no)(&|\]|$)/, // yellow + et2_button_delete: /delete(&|\]|$)/ // red + }; + + label: string = ""; + clicked: boolean = false; + btn: JQuery = null; + image: JQuery = null; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + if (!this.options.background_image && (this.options.image || this.options.ro_image)) + { + this.image = jQuery(document.createElement("img")) + .addClass("et2_button et2_button_icon"); + if (!this.options.readonly) this.image.addClass("et2_clickable"); + this.setDOMNode(this.image[0]); + return; + } + if (!this.options.readonly || this.options.ro_image) + { + this.btn = jQuery(document.createElement("button")) + .addClass("et2_button") + .attr({type:"button"}); + this.setDOMNode(this.btn[0]); + } + if (this.options.image) this.set_image(this.options.image); + } + + /** + * Apply the "modifications" to the element and translate attributes marked + * with "translate: true" + * + * Reimplemented here to assign default background-images to buttons + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + if (this.id && typeof _attrs.background_image == 'undefined' && !_attrs.image) + { + for(var image in et2_button.default_background_images) + { + if (this.id.match(et2_button.default_background_images[image])) + { + _attrs.image = image; + _attrs.background_image = true; + break; + } + } + } + for(var name in et2_button.default_classes) + { + if (this.id.match(et2_button.default_classes[name])) + { + _attrs.class = (typeof _attrs.class == 'undefined' ? '' : _attrs.class+' ')+name; + break; + } + } + super.transformAttributes(_attrs); + } + + set_accesskey(key) + { + jQuery(this.node).attr("accesskey", key); + } + /** + * Set image and update current image + * + * @param _image + */ + set_image(_image) + { + this.options.image = _image; + this.update_image(); + } + /** + * Set readonly image and update current image + * + * @param _image + */ + set_ro_image(_image) + { + this.options.ro_image = _image; + this.update_image(); + } + /** + * Set current image (dont update options.image) + * + * @param _image + */ + update_image(_image?) + { + if(!this.isInTree() || !this.options.background_image && this.image == null) return; + + if (typeof _image == 'undefined') + _image = this.options.readonly ? (this.options.ro_image || this.options.image) : this.options.image; + + // Silently blank for percentages instead of warning about missing image - use a progress widget + if(_image.match(/^[0-9]+\%$/)) + { + _image = ""; + //this.egw().debug("warn", "Use a progress widget instead of percentage images", this); + } + + var found_image = false; + if(_image != "") + { + var src = this.egw().image(_image); + if(src) + { + found_image = true; + } + else if (_image[0] == '/' || _image.substr(0,4) == 'http') + { + src = _image; + found_image = true; + } + if(found_image) + { + if(this.image != null) + { + this.image.attr("src", src); + } + else if (this.options.background_image && this.btn) + { + this.btn.css("background-image","url("+src+")"); + this.btn.addClass('et2_button_with_image'); + } + } + } + if(!found_image) + { + this.set_label(this.label); + if(this.btn) + { + this.btn.css("background-image",""); + this.btn.removeClass('et2_button_with_image'); + } + } + } + + /** + * Set options.readonly and update image + * + * @param {boolean} _ro + */ + set_readonly(_ro) + { + if (_ro != this.options.readonly) + { + this.options.readonly = _ro; + + if (this.options.image || this.options.ro_image) + { + this.update_image(); + } + // dont show readonly buttons as clickable + if (this.btn || this.image) + { + (this.btn || this.image) + .toggleClass('et2_clickable', !_ro) + .toggleClass('et2_button_ro', _ro) + .css('cursor', _ro ? 'default' : 'pointer'); // temp. 'til it is removed from et2_button + } + } + } + + attachToDOM() + { + let ret = super.attachToDOM(); + + if (this.options.readonly && (this.btn || this.image)) + { + (this.btn || this.image) + .removeClass('et2_clickable') + .addClass('et2_button_ro') + .css('cursor', 'default'); // temp. 'til it is removed from et2_button + } + return ret; + } + + getDOMNode() + { + return this.btn ? this.btn[0] : (this.image ? this.image[0] : null); + } + + /** + * Overwritten to maintain an internal clicked attribute + * + * @param _ev + * @returns {Boolean} + */ + click(_ev) + { + // ignore click on readonly button + if (this.options.readonly) return false; + + this.clicked = true; + + if (!super.click.apply(this, arguments)) + { + this.clicked = false; + return false; + } + + // Submit the form + if (this.getType() != "buttononly") + { + this.getInstanceManager().submit(this, false, this.options.novalidate); //TODO: this only needs to be passed if it's in a datagrid + } + this.clicked = false; + return true; + } + + set_label(_value) + { + if (this.btn) + { + this.label = _value; + + this.btn.text(_value); + + if (_value && !this.image) + this.btn.addClass('et2_button_text'); + else + this.btn.removeClass('et2_button_text'); + } + if(this.image) + { + this.image.attr("alt", _value); + // Don't set title if there's a tooltip, browser may show both + if(!this.options.statustext) + { + this.image.attr("title",_value); + } + } + } + + /** + * Set tab index + * + * @param {number} index + */ + set_tabindex(index) + { + jQuery(this.btn).attr("tabindex", index); + } + + /** + * Implementation of the et2_IInput interface + */ + + /** + * Always return false as a button is never dirty + */ + isDirty() + { + return false; + } + + resetDirty() + { + } + + getValue() + { + if (this.clicked) + { + return true; + } + + // If "null" is returned, the result is not added to the submitted + // array. + return null; + } + + isValid() + { + return true; + } + + /** + * et2_IDetachedDOM + * + * @param {array} _attrs + */ + getDetachedAttributes(_attrs) + { + _attrs.push("label", "value", "class", "image", "ro_image", "onclick", "background_image" ); + } + + getDetachedNodes() + { + return [ + this.btn != null ? this.btn[0] : null, + this.image != null ? this.image[0] : null + ]; + } + + setDetachedAttributes(_nodes, _values) + { + // Datagrid puts in the row for null + this.btn = _nodes[0].nodeName[0] != '#' ? jQuery(_nodes[0]) : null; + this.image = jQuery(_nodes[1]); + + if (typeof _values["id"] != "undefined") + { + this.set_id(_values["id"]); + } + if (typeof _values["label"] != "undefined") + { + this.set_label(_values["label"]); + } + if (typeof _values["value"] != "undefined") + { + } + if (typeof _values["image"] != "undefined") + { + this.set_image(_values["image"]); + } + if (typeof _values["ro_image"] != "undefined") + { + this.set_ro_image(_values["ro_image"]); + } + if (typeof _values["class"] != "undefined") + { + this.set_class(_values["class"]); + } + + if (typeof _values["onclick"] != "undefined") + { + this.options.onclick = _values["onclick"]; + } + var type = this.getType(); + var attrs = jQuery.extend(_values, this.options); + var parent = this.getParent(); + jQuery(this.getDOMNode()).bind("click.et2_baseWidget", this, function(e) { + var widget = et2_createWidget(type,attrs,parent); + e.data = widget; + e.data.set_id(_values["id"]); + return e.data.click.call(e.data,e); + }); + } +} +et2_register_widget(et2_button, ["button", "buttononly"]); diff --git a/api/js/etemplate/et2_widget_textbox.js b/api/js/etemplate/et2_widget_textbox.js index 67d59932b3..f24bbda4b1 100644 --- a/api/js/etemplate/et2_widget_textbox.js +++ b/api/js/etemplate/et2_widget_textbox.js @@ -39,15 +39,15 @@ require("./et2_types"); * * @augments et2_inputWidget */ -var et2_textbox = /** @class */ (function (_super_1) { - __extends(et2_textbox, _super_1); +var et2_textbox = /** @class */ (function (_super) { + __extends(et2_textbox, _super); /** * Constructor */ function et2_textbox(_parent, _attrs, _child) { var _this = // Call the inherited constructor - _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; _this.legacyOptions = ["size", "maxlength", "validator"]; _this.input = null; _this.input = null; @@ -112,7 +112,7 @@ var et2_textbox = /** @class */ (function (_super_1) { * @returns {undefined} */ et2_textbox.prototype.set_id = function (_value) { - _super_1.prototype.set_id.call(this, _value); + _super.prototype.set_id.call(this, _value); // Remove the name attribute inorder to affect autocomplete="off" // for no password save. ATM seems all browsers ignore autocomplete for // input field inside the form @@ -124,12 +124,12 @@ var et2_textbox = /** @class */ (function (_super_1) { var node = this.getInputNode(); if (node) jQuery(node).unbind("keypress"); - _super_1.prototype.destroy.call(this); + _super.prototype.destroy.call(this); }; et2_textbox.prototype.getValue = function () { if (this.options && this.options.blur && this.input.val() == this.options.blur) return ""; - return _super_1.prototype.getValue.call(this); + return _super.prototype.getValue.call(this); }; /** * Clientside validation using regular expression in "validator" attribute @@ -155,7 +155,7 @@ var et2_textbox = /** @class */ (function (_super_1) { _messages.push(this.egw().lang("'%1' has an invalid format !!!", value)); } } - return _super_1.prototype.isValid.call(this, _messages) && ok; + return _super.prototype.isValid.call(this, _messages) && ok; }; /** * Set input widget size @@ -165,7 +165,7 @@ var et2_textbox = /** @class */ (function (_super_1) { if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) { this.input.css('width', _size + "em"); } - else if (typeof _size != 'undefined' && _size != this.input.attr("size")) { + else if (typeof _size != 'undefined' && _size != parseInt(this.input.attr("size"))) { this.size = _size; this.input.attr("size", this.size); } @@ -175,7 +175,7 @@ var et2_textbox = /** @class */ (function (_super_1) { * @param _size Max characters allowed */ et2_textbox.prototype.set_maxlength = function (_size) { - if (typeof _size != 'undefined' && _size != this.input.attr("maxlength")) { + if (typeof _size != 'undefined' && _size != parseInt(this.input.attr("maxlength"))) { this.maxLength = _size; this.input.attr("maxLength", this.maxLength); } @@ -286,21 +286,22 @@ var et2_textbox = /** @class */ (function (_super_1) { }; return et2_textbox; }(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_textbox = et2_textbox; et2_core_widget_1.et2_register_widget(et2_textbox, ["textbox", "passwd", "hidden"]); /** * et2_textbox_ro is the dummy readonly implementation of the textbox. * * @augments et2_valueWidget */ -var et2_textbox_ro = /** @class */ (function (_super_1) { - __extends(et2_textbox_ro, _super_1); +var et2_textbox_ro = /** @class */ (function (_super) { + __extends(et2_textbox_ro, _super); /** * Constructor */ function et2_textbox_ro(_parent, _attrs, _child) { var _this = // Call the inherited constructor - _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; _this.value = ""; _this.span = jQuery(document.createElement("label")) .addClass("et2_label"); @@ -388,15 +389,16 @@ et2_core_widget_1.et2_register_widget(et2_textbox_ro, ["textbox_ro"]); * et2_searchbox is a widget which provides a collapsable input search * with on searching indicator and clear handler regardless of any browser limitation. */ -var et2_searchbox = /** @class */ (function (_super_1) { - __extends(et2_searchbox, _super_1); +var et2_searchbox = /** @class */ (function (_super) { + __extends(et2_searchbox, _super); /** * Constructor */ function et2_searchbox(_parent, _attrs, _child) { var _this = // Call the inherited constructor - _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.value = ""; _this.value = ""; _this.div = jQuery(document.createElement('div')) .addClass('et2_searchbox'); @@ -459,8 +461,8 @@ var et2_searchbox = /** @class */ (function (_super_1) { } }, mousedown: function (event) { - if (event.target.type == 'span') - event.stopImmidatePropagation(); + if (event.target.tagName == 'span') + event.stopImmediatePropagation(); } }); this.flex.append(this.search.getDOMNode()); @@ -518,13 +520,13 @@ var et2_searchbox = /** @class */ (function (_super_1) { */ et2_searchbox.prototype.change = function () { this._searchToggleState(); - this._super.apply(this, arguments); + _super.prototype.change.apply(this, arguments); }; et2_searchbox.prototype.get_value = function () { return this.search.input.val(); }; et2_searchbox.prototype.set_value = function (_value) { - _super_1.prototype.set_value.call(this, _value); + _super.prototype.set_value.call(this, _value); if (this.search) this.search.input.val(_value); }; @@ -532,7 +534,7 @@ var et2_searchbox = /** @class */ (function (_super_1) { * override doLoadingFinished in order to set initial state */ et2_searchbox.prototype.doLoadingFinished = function () { - _super_1.prototype.doLoadingFinished.call(this); + var ret = _super.prototype.doLoadingFinished.call(this); if (!this.get_value()) { this._show_hide(false); } @@ -540,16 +542,18 @@ var et2_searchbox = /** @class */ (function (_super_1) { this._show_hide(!this.options.overlay); this._searchToggleState(); } + return ret; }; /** * Overrride attachToDOM in order to unbind change handler */ et2_searchbox.prototype.attachToDOM = function () { - _super_1.prototype.attachToDOM.call(this); + var ret = _super.prototype.attachToDOM.call(this); var node = this.getInputNode(); if (node) { jQuery(node).off('.et2_inputWidget'); } + return ret; }; /** * Advanced attributes diff --git a/api/js/etemplate/et2_widget_textbox.ts b/api/js/etemplate/et2_widget_textbox.ts index f8e96b8a8d..e11ecc7bdf 100644 --- a/api/js/etemplate/et2_widget_textbox.ts +++ b/api/js/etemplate/et2_widget_textbox.ts @@ -20,6 +20,7 @@ import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from import { et2_DOMWidget } from './et2_core_DOMWidget' import { et2_valueWidget } from './et2_core_valueWidget' import { et2_inputWidget } from './et2_core_inputWidget' +import { et2_button } from './et2_widget_button' import './et2_types'; /** @@ -27,7 +28,7 @@ import './et2_types'; * * @augments et2_inputWidget */ -class et2_textbox extends et2_inputWidget implements et2_IResizeable +export class et2_textbox extends et2_inputWidget implements et2_IResizeable { static readonly _attributes : any = { "multiline": { @@ -89,8 +90,8 @@ class et2_textbox extends et2_inputWidget implements et2_IResizeable legacyOptions: string[] = ["size", "maxlength", "validator"]; input: JQuery = null; - size: number|string; - maxLength: number|string; + size: number; + maxLength: number; /** * Constructor @@ -100,7 +101,6 @@ class et2_textbox extends et2_inputWidget implements et2_IResizeable // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); - this.input = null; this.createInputWidget(); @@ -236,13 +236,13 @@ class et2_textbox extends et2_inputWidget implements et2_IResizeable * Set input widget size * @param _size Rather arbitrary size units, approximately characters */ - set_size(_size : number|string) + set_size(_size : number) { if (this.options.multiline || this.options.rows > 1 || this.options.cols > 1) { this.input.css('width', _size + "em"); } - else if (typeof _size != 'undefined' && _size != this.input.attr("size")) + else if (typeof _size != 'undefined' && _size != parseInt(this.input.attr("size"))) { this.size = _size; this.input.attr("size", this.size); @@ -253,9 +253,9 @@ class et2_textbox extends et2_inputWidget implements et2_IResizeable * Set maximum characters allowed * @param _size Max characters allowed */ - set_maxlength(_size : number|string) + set_maxlength(_size : number) { - if (typeof _size != 'undefined' && _size != this.input.attr("maxlength")) + if (typeof _size != 'undefined' && _size != parseInt(this.input.attr("maxlength"))) { this.maxLength = _size; this.input.attr("maxLength", this.maxLength); @@ -277,7 +277,7 @@ class et2_textbox extends et2_inputWidget implements et2_IResizeable { if(_value) { this.input.attr("placeholder", this.egw().lang(_value) + ""); // HTML5 - if(!this.input[0].placeholder) { + if (!(this.input[0]).placeholder) { // Not HTML5 if(this.input.val() == "") this.input.val(this.egw().lang(this.options.blur)); this.input.focus(this,function(e) { @@ -299,7 +299,7 @@ class et2_textbox extends et2_inputWidget implements et2_IResizeable this.input.attr('autocomplete', _value); } - resize(_height) + resize(_height : number) { if (_height && this.options.multiline) { @@ -349,6 +349,9 @@ class et2_textbox_ro extends et2_valueWidget implements et2_IDetachedDOM "ignore": true } }; + value: string = ""; + span: JQuery; + value_span: JQuery; /** * Constructor @@ -358,7 +361,6 @@ class et2_textbox_ro extends et2_valueWidget implements et2_IDetachedDOM // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); - this.value = ""; this.span = jQuery(document.createElement("label")) .addClass("et2_label"); this.value_span = jQuery(document.createElement("span")) @@ -456,6 +458,13 @@ class et2_searchbox extends et2_textbox description:"Define wheter the searchbox should be a fix input field or flexible search button. Default is true (fix)." } } + value: string = ""; + div: JQuery; + flex: JQuery; + button: et2_button; + search: et2_textbox; + oldValue: any; + clear: JQuery; /** * Constructor @@ -486,7 +495,7 @@ class et2_searchbox extends et2_textbox // no need to create search button if it's a fix search field if (!this.options.fix) { - this.button = et2_createWidget('button',{image:"search","background_image":"1"},this); + this.button = et2_createWidget('button',{image:"search","background_image":"1"},this); this.button.onclick= function(){ self._show_hide(jQuery(self.flex).hasClass('hide')); self.search.input.focus(); @@ -494,7 +503,7 @@ class et2_searchbox extends et2_textbox this.div.prepend(this.button.getDOMNode()); } // input field - this.search = et2_createWidget('textbox',{"blur":egw.lang("search"), + this.search = et2_createWidget('textbox',{"blur":egw.lang("search"), onkeypress:function(event) { if(event.which == 13) { @@ -533,7 +542,7 @@ class et2_searchbox extends et2_textbox } }, mousedown:function(event){ - if (event.target.type == 'span') event.stopImmidatePropagation(); + if (event.target.tagName == 'span') event.stopImmediatePropagation(); } }); this.flex.append(this.search.getDOMNode()); @@ -600,7 +609,7 @@ class et2_searchbox extends et2_textbox { this._searchToggleState(); - this._super.apply(this,arguments); + super.change.apply(this,arguments); } @@ -620,14 +629,16 @@ class et2_searchbox extends et2_textbox */ doLoadingFinished() { - super.doLoadingFinished(); + let ret = super.doLoadingFinished(); + if (!this.get_value()) { this._show_hide(false); } - else{ + else { this._show_hide(!this.options.overlay); this._searchToggleState(); } + return ret; } /** @@ -635,13 +646,14 @@ class et2_searchbox extends et2_textbox */ attachToDOM() { - super.attachToDOM(); + let ret = super.attachToDOM(); var node = this.getInputNode(); if (node) { jQuery(node).off('.et2_inputWidget'); } + return ret; } } et2_register_widget(et2_searchbox, ["searchbox"]); From af6afdcffe679873b7691bdc113a86e25c20c573 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jan 2020 16:11:08 +0100 Subject: [PATCH 18/61] template widget with TS --- api/js/etemplate/et2_core_DOMWidget.ts | 2 +- api/js/etemplate/et2_types.d.ts | 3 +- api/js/etemplate/et2_widget_template.js | 442 ++++++++++++------------ api/js/etemplate/et2_widget_template.ts | 256 ++++++++++++++ 4 files changed, 472 insertions(+), 231 deletions(-) create mode 100644 api/js/etemplate/et2_widget_template.ts diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index dce01e8d8e..28d86ecd64 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -165,7 +165,7 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode /** * Attaches the container node of this widget to the DOM-Tree */ - doLoadingFinished() + doLoadingFinished() : boolean | JQueryPromise { // Check whether the parent implements the et2_IDOMNode interface. If // yes, grab the DOM node and create our own. diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index 4953e5712b..94e230a3d0 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -154,4 +154,5 @@ declare var et2_video : any; declare var et2_IExposable : any; declare function et2_createWidget(type : string, params : {}, parent? : any) : any; declare function nm_action(_action : {}, _senders : [], _target : any, _ids? : any) : void; -declare function et2_compileLegacyJS(_code : string, _widget : et2_widget, _context? : HTMLElement) : Function; \ No newline at end of file +declare function et2_compileLegacyJS(_code : string, _widget : et2_widget, _context? : HTMLElement) : Function; +declare function et2_loadXMLFromURL(_url : string, _callback : Function, _context? : object, _fail_callback? : Function) : void; diff --git a/api/js/etemplate/et2_widget_template.js b/api/js/etemplate/et2_widget_template.js index 2de6cc2de3..30bb559bf2 100644 --- a/api/js/etemplate/et2_widget_template.js +++ b/api/js/etemplate/et2_widget_template.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Template base class * @@ -6,241 +7,224 @@ * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel - * @copyright Stylite 2011 - * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_core_xml; - et2_core_DOMWidget; + et2_core_xml; + et2_core_DOMWidget; */ - +require("./et2_core_interfaces"); +require("./et2_core_common"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_widget_1 = require("./et2_core_widget"); +require("./et2_types"); /** * Class which implements the "template" XET-Tag. When the id parameter is set, * the template class checks whether another template with this id already * exists. If yes, this template is removed from the DOM tree, copied and * inserted in place of this template. - * - * @augments et2_DOMWidget */ -var et2_template = (function(){ "use strict"; return et2_DOMWidget.extend( -{ - attributes: { - "template": { - "name": "Template", - "type": "string", - "description": "Name / ID of template with optional cache-buster ('?'+filemtime of template on server)", - "default": et2_no_init - }, - "group": { - // TODO: Not implemented - "name": "Group", - "description":"Not implemented", - //"default": 0 - "default": et2_no_init - }, - "version": { - "name": "Version", - "type": "string", - "description": "Version of the template" - }, - "lang": { - "name": "Language", - "type": "string", - "description": "Language the template is written in" - }, - "content": { - "name": "Content index", - "default": et2_no_init, - "description": "Used for passing in specific content to the template other than what it would get by ID." - }, - url: { - name: "URL of template", - type: "string", - description: "full URL to load template incl. cache-buster" - }, - "onload": { - "name": "onload", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed after the template is loaded." - } - }, - - createNamespace: true, - - /** - * Initializes this template widget as a simple container. - * - * @memberOf et2_template - * @param {et2_widget} _parent - * @param {object} _attrs - */ - init: function(_parent, _attrs) { - // Set this early, so it's available for creating namespace - if(_attrs.content) - { - this.content = _attrs.content; - } - this._super.apply(this, arguments); - - this.div = document.createElement("div"); - - // Deferred object so we can load via AJAX - this.loading = jQuery.Deferred(); - - // run transformAttributes now, to get server-side modifications (url!) - if (_attrs.template) - { - this.id = _attrs.template; - this.transformAttributes(_attrs); - this.options = et2_cloneObject(_attrs); - _attrs = {}; - } - if (this.id != "" || this.options.template) - { - var parts = (this.options.template || this.id).split('?'); - var cache_buster = parts.length > 1 ? parts.pop() : null; - var template_name = parts.pop(); - - // Check to see if XML is known - var xml = null; - var templates = etemplate2.prototype.templates; // use global eTemplate cache - if(!(xml = templates[template_name])) - { - // Check to see if ID is short form --> prepend parent/top-level name - if(template_name.indexOf('.') < 0) - { - var root = _parent ? _parent.getRoot() : null; - var top_name = root && root._inst ? root._inst.name : null; - if (top_name && template_name.indexOf('.') < 0) template_name = top_name+'.'+template_name; - } - xml = templates[template_name]; - if(!xml) - { - // Ask server - var url = this.options.url; - if (!this.options.url) - { - var splitted = template_name.split('.'); - var app = splitted.shift(); - // use template base url from initial template, to continue using webdav, if that was loaded via webdav - url = this.getRoot()._inst.template_base_url + app + "/templates/default/" + - splitted.join('.')+ ".xet" + (cache_buster ? '?download='+cache_buster : ''); - } - // if server did not give a cache-buster, fall back to current time - if (url.indexOf('?') == -1) url += '?download='+(new Date).valueOf(); - - if(this.options.url || splitted.length) - { - var fetch_url_callback = function(_xmldoc) { - // Scan for templates and store them - for(var i = 0; i < _xmldoc.childNodes.length; i++) { - var template = _xmldoc.childNodes[i]; - if(template.nodeName.toLowerCase() != "template") continue; - templates[template.getAttribute("id")] = template; - } - - // Read the XML structure of the requested template - if (typeof templates[template_name] != 'undefined') this.loadFromXML(templates[template_name]); - - // Update flag - this.loading.resolve(); - - } - - et2_loadXMLFromURL(url, fetch_url_callback, this, function( error) { - url = egw.link('/'+ app + "/templates/default/" + - splitted.join('.')+ ".xet", {download:cache_buster? cache_buster :(new Date).valueOf()}); - - et2_loadXMLFromURL(url, fetch_url_callback, this); - }); - } - return; - } - } - if(xml !== null && typeof xml !== "undefined") - { - this.egw().debug("log", "Loading template from XML: ", template_name); - this.loadFromXML(xml); - // Don't call this here - done by caller, or on whole widget tree - //this.loadingFinished(); - - // But resolve the promise - this.loading.resolve(); - } - else - { - this.egw().debug("warn", "Unable to find XML for ", template_name); - this.loading.reject(); - } - } - else - { - // No actual template - this.loading.resolve(); - } - }, - - /** - * Override parent to support content attribute - * Templates always have ID set, but seldom do we want them to - * create a namespace based on their ID. - */ - checkCreateNamespace: function() { - if(this.content) - { - var old_id = this.id; - this.id = this.content; - this._super.apply(this, arguments); - this.id = old_id; - } - }, - - getDOMNode: function() { - return this.div; - }, - - attachToDOM: function() { - if (this.div) - { - jQuery(this.div) - .off('.et2_template') - .bind("load.et2_template", this, function(e) { - e.data.load.call(e.data, this); - }); - } - - this._super.apply(this,arguments); - }, - - /** - * Called after the template is fully loaded to handle any onload handlers - */ - load: function() { - if(typeof this.options.onload == 'function') - { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if(args.indexOf(this) == -1) args.push(this); - - return this.options.onload.apply(this, args); - } - }, - - /** - * Override to return the promise for deferred loading - */ - doLoadingFinished: function() { - // Apply parent now, which actually puts into the DOM - this._super.apply(this, arguments); - - // Fire load event when done loading - this.loading.done(jQuery.proxy(function() {jQuery(this).trigger("load");},this.div)); - - // Not done yet, but widget will let you know - return this.loading.promise(); - } -});}).call(this); -et2_register_widget(et2_template, ["template"]); - +var et2_template = /** @class */ (function (_super) { + __extends(et2_template, _super); + /** + * Constructor + */ + function et2_template(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_core_DOMWidget_1.et2_DOMWidget._attributes, _child || {})) || this; + _this.createNamespace = true; + // Set this early, so it's available for creating namespace + if (_attrs.content) { + _this.content = _attrs.content; + } + // constructor was called here before! + _this.div = document.createElement("div"); + // Deferred object so we can load via AJAX + _this.loading = jQuery.Deferred(); + // run transformAttributes now, to get server-side modifications (url!) + if (_attrs.template) { + _this.id = _attrs.template; + _this.transformAttributes(_attrs); + _this.options = et2_cloneObject(_attrs); + _attrs = {}; + } + if (_this.id != "" || _this.options.template) { + var parts = (_this.options.template || _this.id).split('?'); + var cache_buster = parts.length > 1 ? parts.pop() : null; + var template_name = parts.pop(); + // Check to see if XML is known + var xml = null; + var templates = etemplate2.prototype.templates; // use global eTemplate cache + if (!(xml = templates[template_name])) { + // Check to see if ID is short form --> prepend parent/top-level name + if (template_name.indexOf('.') < 0) { + var root = _parent ? _parent.getRoot() : null; + var top_name = root && root._inst ? root._inst.name : null; + if (top_name && template_name.indexOf('.') < 0) + template_name = top_name + '.' + template_name; + } + xml = templates[template_name]; + if (!xml) { + // Ask server + var url = _this.options.url; + if (!_this.options.url) { + var splitted = template_name.split('.'); + var app = splitted.shift(); + // use template base url from initial template, to continue using webdav, if that was loaded via webdav + url = _this.getRoot()._inst.template_base_url + app + "/templates/default/" + + splitted.join('.') + ".xet" + (cache_buster ? '?download=' + cache_buster : ''); + } + // if server did not give a cache-buster, fall back to current time + if (url.indexOf('?') == -1) + url += '?download=' + (new Date).valueOf(); + if (_this.options.url || splitted.length) { + var fetch_url_callback = function (_xmldoc) { + // Scan for templates and store them + for (var i = 0; i < _xmldoc.childNodes.length; i++) { + var template = _xmldoc.childNodes[i]; + if (template.nodeName.toLowerCase() != "template") + continue; + templates[template.getAttribute("id")] = template; + } + // Read the XML structure of the requested template + if (typeof templates[template_name] != 'undefined') + this.loadFromXML(templates[template_name]); + // Update flag + this.loading.resolve(); + }; + et2_loadXMLFromURL(url, fetch_url_callback, _this, function (error) { + url = egw.link('/' + app + "/templates/default/" + + splitted.join('.') + ".xet", { download: cache_buster ? cache_buster : (new Date).valueOf() }); + et2_loadXMLFromURL(url, fetch_url_callback, this); + }); + } + return _this; + } + } + if (xml !== null && typeof xml !== "undefined") { + _this.egw().debug("log", "Loading template from XML: ", template_name); + _this.loadFromXML(xml); + // Don't call this here - done by caller, or on whole widget tree + //this.loadingFinished(); + // But resolve the promise + _this.loading.resolve(); + } + else { + _this.egw().debug("warn", "Unable to find XML for ", template_name); + _this.loading.reject(); + } + } + else { + // No actual template + _this.loading.resolve(); + } + return _this; + } + /** + * Override parent to support content attribute + * Templates always have ID set, but seldom do we want them to + * create a namespace based on their ID. + */ + et2_template.prototype.checkCreateNamespace = function () { + if (this.content) { + var old_id = this.id; + this.id = this.content; + _super.prototype.checkCreateNamespace.apply(this, arguments); + this.id = old_id; + } + }; + et2_template.prototype.getDOMNode = function () { + return this.div; + }; + et2_template.prototype.attachToDOM = function () { + if (this.div) { + jQuery(this.div) + .off('.et2_template') + .bind("load.et2_template", this, function (e) { + e.data.load.call(e.data, this); + }); + } + return _super.prototype.attachToDOM.call(this); + }; + /** + * Called after the template is fully loaded to handle any onload handlers + */ + et2_template.prototype.load = function () { + if (typeof this.options.onload == 'function') { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if (args.indexOf(this) == -1) + args.push(this); + return this.options.onload.apply(this, args); + } + }; + /** + * Override to return the promise for deferred loading + */ + et2_template.prototype.doLoadingFinished = function () { + // Apply parent now, which actually puts into the DOM + _super.prototype.doLoadingFinished.call(this); + // Fire load event when done loading + this.loading.done(jQuery.proxy(function () { jQuery(this).trigger("load"); }, this.div)); + // Not done yet, but widget will let you know + return this.loading.promise(); + }; + et2_template._attributes = { + "template": { + "name": "Template", + "type": "string", + "description": "Name / ID of template with optional cache-buster ('?'+filemtime of template on server)", + "default": et2_no_init + }, + "group": { + // TODO: Not implemented + "name": "Group", + "description": "Not implemented", + //"default": 0 + "default": et2_no_init + }, + "version": { + "name": "Version", + "type": "string", + "description": "Version of the template" + }, + "lang": { + "name": "Language", + "type": "string", + "description": "Language the template is written in" + }, + "content": { + "name": "Content index", + "default": et2_no_init, + "description": "Used for passing in specific content to the template other than what it would get by ID." + }, + url: { + name: "URL of template", + type: "string", + description: "full URL to load template incl. cache-buster" + }, + "onload": { + "name": "onload", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed after the template is loaded." + } + }; + return et2_template; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +et2_core_widget_1.et2_register_widget(et2_template, ["template"]); diff --git a/api/js/etemplate/et2_widget_template.ts b/api/js/etemplate/et2_widget_template.ts new file mode 100644 index 0000000000..c5e643c948 --- /dev/null +++ b/api/js/etemplate/et2_widget_template.ts @@ -0,0 +1,256 @@ +/** + * EGroupware eTemplate2 - JS Template base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + */ + +/*egw:uses + et2_core_xml; + et2_core_DOMWidget; +*/ + +import './et2_core_interfaces'; +import './et2_core_common'; +import { et2_DOMWidget } from './et2_core_DOMWidget'; +import { ClassWithAttributes } from "./et2_core_inheritance"; +import { et2_widget, et2_createWidget, et2_register_widget, WidgetConfig } from "./et2_core_widget"; +import './et2_types'; + +/** + * Class which implements the "template" XET-Tag. When the id parameter is set, + * the template class checks whether another template with this id already + * exists. If yes, this template is removed from the DOM tree, copied and + * inserted in place of this template. + */ +class et2_template extends et2_DOMWidget +{ + static readonly _attributes : any = { + "template": { + "name": "Template", + "type": "string", + "description": "Name / ID of template with optional cache-buster ('?'+filemtime of template on server)", + "default": et2_no_init + }, + "group": { + // TODO: Not implemented + "name": "Group", + "description":"Not implemented", + //"default": 0 + "default": et2_no_init + }, + "version": { + "name": "Version", + "type": "string", + "description": "Version of the template" + }, + "lang": { + "name": "Language", + "type": "string", + "description": "Language the template is written in" + }, + "content": { + "name": "Content index", + "default": et2_no_init, + "description": "Used for passing in specific content to the template other than what it would get by ID." + }, + url: { + name: "URL of template", + type: "string", + description: "full URL to load template incl. cache-buster" + }, + "onload": { + "name": "onload", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed after the template is loaded." + } + }; + + createNamespace: boolean = true; + content: string; + div: HTMLDivElement; + loading: JQueryDeferred; + + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); + + // Set this early, so it's available for creating namespace + if(_attrs.content) + { + this.content = _attrs.content; + } + // constructor was called here before! + + this.div = document.createElement("div"); + + // Deferred object so we can load via AJAX + this.loading = jQuery.Deferred(); + + // run transformAttributes now, to get server-side modifications (url!) + if (_attrs.template) + { + this.id = _attrs.template; + this.transformAttributes(_attrs); + this.options = et2_cloneObject(_attrs); + _attrs = {}; + } + if (this.id != "" || this.options.template) + { + var parts = (this.options.template || this.id).split('?'); + var cache_buster = parts.length > 1 ? parts.pop() : null; + var template_name = parts.pop(); + + // Check to see if XML is known + var xml = null; + var templates = etemplate2.prototype.templates; // use global eTemplate cache + if(!(xml = templates[template_name])) + { + // Check to see if ID is short form --> prepend parent/top-level name + if(template_name.indexOf('.') < 0) + { + var root = _parent ? _parent.getRoot() : null; + var top_name = root && root._inst ? root._inst.name : null; + if (top_name && template_name.indexOf('.') < 0) template_name = top_name+'.'+template_name; + } + xml = templates[template_name]; + if(!xml) + { + // Ask server + var url = this.options.url; + if (!this.options.url) + { + var splitted = template_name.split('.'); + var app = splitted.shift(); + // use template base url from initial template, to continue using webdav, if that was loaded via webdav + url = this.getRoot()._inst.template_base_url + app + "/templates/default/" + + splitted.join('.')+ ".xet" + (cache_buster ? '?download='+cache_buster : ''); + } + // if server did not give a cache-buster, fall back to current time + if (url.indexOf('?') == -1) url += '?download='+(new Date).valueOf(); + + if(this.options.url || splitted.length) + { + var fetch_url_callback = function(_xmldoc) { + // Scan for templates and store them + for(var i = 0; i < _xmldoc.childNodes.length; i++) { + var template = _xmldoc.childNodes[i]; + if(template.nodeName.toLowerCase() != "template") continue; + templates[template.getAttribute("id")] = template; + } + + // Read the XML structure of the requested template + if (typeof templates[template_name] != 'undefined') this.loadFromXML(templates[template_name]); + + // Update flag + this.loading.resolve(); + }; + + et2_loadXMLFromURL(url, fetch_url_callback, this, function( error) { + url = egw.link('/'+ app + "/templates/default/" + + splitted.join('.')+ ".xet", {download:cache_buster? cache_buster :(new Date).valueOf()}); + + et2_loadXMLFromURL(url, fetch_url_callback, this); + }); + } + return; + } + } + if(xml !== null && typeof xml !== "undefined") + { + this.egw().debug("log", "Loading template from XML: ", template_name); + this.loadFromXML(xml); + // Don't call this here - done by caller, or on whole widget tree + //this.loadingFinished(); + + // But resolve the promise + this.loading.resolve(); + } + else + { + this.egw().debug("warn", "Unable to find XML for ", template_name); + this.loading.reject(); + } + } + else + { + // No actual template + this.loading.resolve(); + } + } + + /** + * Override parent to support content attribute + * Templates always have ID set, but seldom do we want them to + * create a namespace based on their ID. + */ + checkCreateNamespace() + { + if(this.content) + { + var old_id = this.id; + this.id = this.content; + super.checkCreateNamespace.apply(this, arguments); + this.id = old_id; + } + } + + getDOMNode() + { + return this.div; + } + + attachToDOM() + { + if (this.div) + { + jQuery(this.div) + .off('.et2_template') + .bind("load.et2_template", this, function(e) { + e.data.load.call(e.data, this); + }); + } + + return super.attachToDOM(); + } + + /** + * Called after the template is fully loaded to handle any onload handlers + */ + load() + { + if(typeof this.options.onload == 'function') + { + // Make sure function gets a reference to the widget + var args = Array.prototype.slice.call(arguments); + if(args.indexOf(this) == -1) args.push(this); + + return this.options.onload.apply(this, args); + } + } + + /** + * Override to return the promise for deferred loading + */ + doLoadingFinished() + { + // Apply parent now, which actually puts into the DOM + super.doLoadingFinished(); + + // Fire load event when done loading + this.loading.done(jQuery.proxy(function() {jQuery(this).trigger("load");},this.div)); + + // Not done yet, but widget will let you know + return this.loading.promise(); + } +} +et2_register_widget(et2_template, ["template"]); + From 6758895ae24a661423a10c88fe185070e41b78d9 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 21 Jan 2020 16:14:45 +0100 Subject: [PATCH 19/61] Convert et2_video widget to TS --- api/js/etemplate/et2_widget_video.js | 286 +++++++++++++-------------- api/js/etemplate/et2_widget_video.ts | 188 ++++++++++++++++++ 2 files changed, 325 insertions(+), 149 deletions(-) create mode 100644 api/js/etemplate/et2_widget_video.ts diff --git a/api/js/etemplate/et2_widget_video.js b/api/js/etemplate/et2_widget_video.js index 439051cbf1..e142496353 100644 --- a/api/js/etemplate/et2_widget_video.js +++ b/api/js/etemplate/et2_widget_video.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Description object * @@ -9,13 +10,28 @@ * @copyright Stylite AG * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_baseWidget; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_interfaces; + et2_core_baseWidget; */ - +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); /** * This widget represents the HTML5 video tag with all its optional attributes * @@ -38,152 +54,124 @@ *