From 901825bcfaf9556b03f5e82647098ea896f79100 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 15 Jan 2020 08:47:33 +0100 Subject: [PATCH] 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