diff --git a/addressbook/js/CRM.js b/addressbook/js/CRM.js deleted file mode 100644 index 99766c9d61..0000000000 --- a/addressbook/js/CRM.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * EGroupware - Addressbook - Javascript UI - * - * @link: https://www.egroupware.org - * @package addressbook - * @author Hadi Nategh - * @author Ralf Becker - * @copyright (c) 2008-21 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 { EgwApp } from '../../api/js/jsapi/egw_app'; -import { et2_nextmatch } from "../../api/js/etemplate/et2_extension_nextmatch"; -import { egw } from "../../api/js/jsapi/egw_global.js"; -/** - * UI for Addressbook CRM view - * - */ -export class CRMView extends EgwApp { - /** - * Constructor - * - * CRM is part of addressbook - */ - constructor() { - // call parent - super('addressbook'); - // List ID - this.list_id = ""; - // Reference to the list - this.nm = null; - // Which addressbook contact id(s) we are showing entries for - this.contact_ids = []; - // Private js for the list - this.app_obj = null; - // Push data key(s) to check for our contact ID in the entry's ACL data - this.push_contact_ids = ["contact_id"]; - } - /** - * Destructor - */ - destroy(_app) { - this.nm = null; - if (this.app_obj != null) { - this.app_obj.destroy(_app); - } - // call parent - super.destroy(_app); - } - /** - * A template from an app is ready, looks like it might be a CRM view. - * Check it, get CRM ready, and bind accordingly - * - * @param et2 - * @param appname - */ - static view_ready(et2, app_obj) { - // Check to see if the template is for a CRM view - if (et2.app == app_obj.appname) { - return CRMView.reconnect(app_obj); - } - // Make sure object is there, etemplate2 will pick it up and call our et2_ready - let crm = undefined; - // @ts-ignore - if (typeof et2.app_obj.crm == "undefined" && app.classes.crm) { - // @ts-ignore - crm = et2.app_obj.crm = new app.classes.crm(); - } - if (typeof crm == "undefined") { - egw.debug("error", "CRMView object is missing"); - return false; - } - // We can set this now - crm.set_view_obj(app_obj); - } - /** - * This function is called when the etemplate2 object is loaded - * and ready. The associated app [is supposed to have] already called its own et2_ready(), - * so any changes done here will override the app. - * - * @param {etemplate2} et2 newly ready object - * @param {string} name Template name - */ - et2_ready(et2, name) { - // call parent - super.et2_ready(et2, name); - } - /** - * Our CRM has become disconnected from its list, probably because something submitted. - * Find it, and get things working again. - * - * @param app_obj - */ - static reconnect(app_obj) { - var _a; - // Check - let contact_ids = app_obj.et2.getArrayMgr("content").getEntry("action_id") || ""; - if (!contact_ids) - return; - for (let existing_app of EgwApp._instances) { - if (existing_app instanceof CRMView && existing_app.list_id == app_obj.et2.getInstanceManager().uniqueId) { - // List was reloaded. Rebind. - existing_app.app_obj.destroy(existing_app.app_obj.appname); - if (!((_a = existing_app.nm) === null || _a === void 0 ? void 0 : _a.getParent())) { - try { - // This will probably not die cleanly, we had a reference when it was destroyed - existing_app.nm.destroy(); - } - catch (e) { } - } - return existing_app.set_view_obj(app_obj); - } - } - } - /** - * Set the associated private app JS - * We try and pull the needed info here - */ - set_view_obj(app_obj) { - this.app_obj = app_obj; - // Make sure object is there, etemplate2 will pick it up and call our et2_ready - app_obj.et2.getInstanceManager().app_obj.crm = this; - // Make _sure_ we get notified if the list is removed (actions, refresh) - this is not always a full - // destruction - jQuery(app_obj.et2.getDOMNode()).on('clear', function () { - this.nm = null; - }.bind(this)); - // For easy reference later - this.list_id = app_obj.et2.getInstanceManager().uniqueId; - this.nm = app_obj.et2.getDOMWidgetById('nm'); - let contact_ids = app_obj.et2.getArrayMgr("content").getEntry("action_id") || ""; - if (typeof contact_ids == "string") { - contact_ids = contact_ids.split(","); - } - this.set_contact_ids(contact_ids); - // Override the push handler - this._override_push(app_obj); - } - /** - * Set or change which contact IDs we are showing entries for - */ - set_contact_ids(ids) { - this.contact_ids = ids; - let filter = { action_id: this.contact_ids }; - if (this.nm !== null) { - this.nm.applyFilters(filter); - } - } - /** - * Handle a push notification about entry changes from the websocket - * - * @param pushData - * @param {string} pushData.app application name - * @param {(string|number)} pushData.id id of entry to refresh or null - * @param {string} pushData.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: ask server for data, add in intelligently - * @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary - * @param {number} pushData.account_id User that caused the notification - */ - push(pushData) { - if (pushData.app !== this.app_obj.appname || !this.nm) - return; - // If we know about it and it's an update, just update. - // This must be before all ACL checks, as contact might have changed and entry needs to be removed - // (server responds then with null / no entry causing the entry to disappear) - if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData))) { - // Check to see if it's in OUR nextmatch - let uid = this.uid(pushData); - let known = Object.values(this.nm.controller._indexMap).filter(function (row) { return row.uid == uid; }); - let type = pushData.type; - if (known && known.length > 0) { - if (!this.id_check(pushData.acl)) { - // Was ours, not anymore, and we know this now - no server needed. Just remove from nm. - type = et2_nextmatch.DELETE; - } - return this.nm.refresh(pushData.id, type); - } - } - if (this.id_check(pushData.acl)) { - return this._app_obj_push(pushData); - } - } - /** - * Check to see if the given entry is "ours" - * - * @param entry - */ - id_check(entry) { - // Check if it's for one of our contacts - for (let field of this.push_contact_ids) { - if (entry && entry[field]) { - let val = typeof entry[field] == "string" ? [entry[field]] : entry[field]; - if (val.filter(v => this.contact_ids.indexOf(v) >= 0).length > 0) { - return true; - } - } - } - return false; - } - /** - * Override the list's push handler to do nothing, we'll call it if we want it. - * - * @param app_obj - * @private - */ - _override_push(app_obj) { - this._app_obj_push = app_obj.push.bind(app_obj); - app_obj.push = function (pushData) { return false; }; - } -} -app.classes.crm = CRMView; -//# sourceMappingURL=CRM.js.map \ No newline at end of file diff --git a/addressbook/js/app.js b/addressbook/js/app.js deleted file mode 100644 index aef410c799..0000000000 --- a/addressbook/js/app.js +++ /dev/null @@ -1,1554 +0,0 @@ -import { E as EgwApp, e as egw, a as etemplate2, b as et2_dialog, n as nm_action, f as fetchAll } from '../../chunks/etemplate2-0eb045cf.js'; -import '../../chunks/CRM-49d7b139.js'; -import '../../chunks/egw_dragdrop_dhtmlx_tree-31643465.js'; -import '../../chunks/egw-5f30b5ae.js'; -import '../../vendor/bower-asset/jquery/dist/jquery.min.js'; -import '../../vendor/bower-asset/jquery-ui/jquery-ui.js'; -import '../../chunks/egw_json-98998d7e.js'; -import '../../chunks/egw_core-0ec5dc11.js'; -import '../../vendor/tinymce/tinymce/tinymce.min.js'; - -/** - * EGroupware - Addressbook - Javascript UI - * - * @link: https://www.egroupware.org - * @package addressbook - * @author Hadi Nategh - * @author Ralf Becker - * @copyright (c) 2008-21 by Ralf Becker - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - */ -/** - * Object to call app.addressbook.openCRMview with - */ - -/** - * UI for Addressbook - * - * @augments AppJS - */ -class AddressbookApp extends EgwApp { - // These fields help with push - push_grant_fields = ["owner", "shared_with"]; - push_filter_fields = ["tid", "owner", "cat_id"]; - /** - * Constructor - * - * @memberOf app.addressbook - */ - - constructor() { - // call parent - super('addressbook'); - } - /** - * Destructor - */ - - - destroy(_app) { - // call parent - super.destroy(_app); - } - /** - * 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 {etemplate2} et2 newly ready object - * @param {string} name - */ - - - et2_ready(et2, name) { - // r49769 let's CRM view run under currentapp == "addressbook", which causes - // app.addressbook.et2_ready called before app.infolog.et2_ready and therefore - // app.addressbook.et2 would point to infolog template, if we not stop here - if (name.match(/^infolog|tracker\./)) return; // call parent - - super.et2_ready(et2, name); - - switch (name) { - case 'addressbook.edit': - var content = this.et2.getArrayMgr('content').data; - - if (typeof content.showsearchbuttons == 'undefined' || !content.showsearchbuttons) { - this.show_custom_country(jQuery('select[id*="adr_one_countrycode"]').get(0)); - this.show_custom_country(jQuery('select[id*="adr_two_countrycode"]').get(0)); // Instanciate infolog JS too - wrong app, so it won't be done automatically - - if (typeof window.app.infolog != 'object' && typeof window.app.classes['infolog'] == 'function') { - window.app.infolog = new window.app.classes.infolog(); - } - } // Call check value if the AB got opened with presets - - - if (window.location.href.match(/&presets\[email\]/g) && content.presets_fields) { - for (var i = 0; i < content.presets_fields.length; i++) { - this.check_value(this.et2.getWidgetById(content.presets_fields), 0); - } - } - - break; - } - - jQuery('select[id*="adr_one_countrycode"]').each(function () { - if (app.addressbook) app.addressbook.show_custom_country(this); - }); - jQuery('select[id*="adr_two_countrycode"]').each(function () { - if (app.addressbook) app.addressbook.show_custom_country(this); - }); - } - /** - * Observer method receives update notifications from all applications - * - * App is responsible for only reacting to "messages" it is interested in! - * - * Addressbook checks for CRM view to update the displayed data if you edit - * that contact - * - * @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) { - // Edit to the current entry - var state = this.getState(); - - if (_app === 'addressbook' && state && state.type && state.type === 'view' && state.id === _id) { - var content = egw.dataGetUIDdata('addressbook::' + _id); - - if (content.data) { - var view = etemplate2.getById('addressbook-view'); - - if (view) { - view.widgetContainer._children[0].set_value({ - content: content.data - }); - } - } - - return false; - } else if (_app === 'calendar') { - // Event changed, update any [known] contacts participating - var content = egw.dataGetUIDdata(_app + '::' + _id); - - if (content && content.data && content.data.participant_types && content.data.participant_types.c) { - for (var contact in content.data.participant_types.c) { - // Refresh handles checking to see if the contact is known, - // and updating it directly - egw.dataRefreshUID('addressbook::' + contact); - } - - return true; - } else if (!content) { - // No data on the event, we'll have to reload if calendar column is visible - // to get the updated information - var nm = etemplate2.getById('addressbook-index').widgetContainer.getWidgetById('nm'); - var pref = nm ? nm._getPreferences() : false; - - if (pref && pref.visible.indexOf('calendar_calendar') > -1) { - nm.refresh(null, 'update'); - } - } - } - - return true; - } - /** - * Handle a push notification about entry changes from the websocket - * - * Get's called for data of all apps, but should only handle data of apps it displays, - * which is by default only it's own, but can be for multiple apps eg. for calendar. - * - * @param pushData - * @param {string} pushData.app application name - * @param {(string|number)} pushData.id id of entry to refresh or null - * @param {string} pushData.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 {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary - * @param {number} pushData.account_id User that caused the notification - */ - - - push(pushData) { - // show missed calls on their CRM view - let et2_id = this.et2?.getInstanceManager().uniqueId; - - if (pushData.app === 'stylite' && pushData.acl.missed && et2_id && et2_id.substr(0, 17) === 'addressbook-view-' && pushData.acl.account_id == this.egw.user('account_id') && pushData.acl.contact_id == this.et2.getArrayMgr("content")?.getEntry("id")) { - egw_getFramework()?.notifyAppTab(et2_id.substr(17)); - } // don't care about other apps data - - - if (pushData.app !== this.appname) return; // Update the contact list - - if (this.et2 && this.et2.getInstanceManager().name == "addressbook.index") { - return super.push(pushData); - } // Update CRM view (sidebox part), if open - - - let contact_id = this.et2?.getArrayMgr("content")?.getEntry("id") || 0; - - if (this.et2 && contact_id && contact_id == pushData.id) { - this.et2.getInstanceManager().submit(); - } - } - /** - * Change handler for contact / org selectbox - * - * @param node - * @param {et2_extension_nextmatch} nm - * @param {et2_selectbox} widget - */ - - - change_grouped_view(node, nm, widget) { - let template = "addressbook.index.rows"; - let value = {}; - - if (nm.activeFilters.sitemgr_display) { - template = nm.activeFilters.sitemgr_display + '.rows'; - } else if (widget.getValue().indexOf("org_name") == 0) { - template = "addressbook.index.org_rows"; - } else if (widget.getValue().indexOf('duplicate') === 0) { - template = 'addressbook.index.duplicate_rows'; - } - - if (nm.activeFilters.col_filter.parent_id) { - template = widget.getValue().indexOf('duplicate') === 0 ? 'addressbook.index.duplicate_rows' : 'addressbook.index.org_rows'; - } - - let promise = nm.set_template(template); - value[widget.id] = widget.getValue(); - - if (promise) { - jQuery.when.apply(null, promise).done(function () { - nm.applyFilters(value); - }); - } - - return !promise; - } - /** - * Open CRM view from addressbook index itself - * - * @param _action - * @param _senders - */ - - - view(_action, _senders) { - let extras = { - contact_id: _senders[0].id.split('::').pop(), - index: _senders[0]._index - }; - let data = egw.dataGetUIDdata(_senders[0].id)['data']; // CRM list - - if (_action.id != 'view') { - extras.crm_list = _action.id.replace('view-', ''); - } - - if (!extras.crm_list) extras.crm_list = egw.preference('crm_list', 'addressbook'); - extras.title = _action.id.match(/\-organisation/) && data.org_name != "" ? data.org_name : data.n_fn + " (" + egw.lang(extras.crm_list) + ")"; - extras.icon = data.photo; - return this.openCRMview(extras); - } - /** - * Open a CRM view for a contact: callback for link-registry / egw.open / other apps - * - * @param {CrmParams} _params object with attribute "contact_id" and optional "title", "crm_list", "icon" - * @param {object} _senders use egw.dataGetUIDdata to get contact_id - */ - - - openCRMview(_params, _senders) { - let contact_id = typeof _params === 'object' ? _params.contact_id : _params; - - if (typeof _senders === 'object') { - let data = egw.dataGetUIDdata(_senders[0].id); - contact_id = data.data.contact_id; - } - - if (typeof contact_id !== 'undefined') { - let crm_list = _params.crm_list || egw.preference('crm_list', 'addressbook'); - if (!crm_list || crm_list === '~edit~') crm_list = 'infolog'; - let url = this.egw.link('/index.php', { - menuaction: 'addressbook.addressbook_ui.view', - ajax: 'true', - contact_id: contact_id, - crm_list: crm_list - }); // no framework, just open the url - - if (typeof this.egw.window.framework === 'undefined') { - return this.egw.open_link(url); - } - - let open = function (_title) { - let title = _title || this.egw.link_title('addressbook', contact_id, open); - - if (title) { - this.egw.window.framework.tabLinkHandler(url, { - displayName: title, - icon: _params.icon || this.egw.link('/api/avatar.php', { - contact_id: contact_id, - etag: new Date().valueOf() / 86400 | 0 // cache for a day, better then no invalidation - - }), - refreshCallback: function () { - etemplate2.getById("addressbook-view-" + this.appName)?.app_obj.addressbook.view_set_list(); - }, - id: contact_id + '-' + crm_list - }); - } - }.bind(this); - - open(_params.title); - } - } - /** - * Set link filter for the already open & rendered list - * - * @param {Object} filter Object with key / value pairs of filters to set - */ - - - view_set_list(filter) { - // Find the infolog list - var list = etemplate2.getById(jQuery(this.et2.getInstanceManager().DOMContainer).nextAll('.et2_container').attr('id')); - var nm = list ? list.widgetContainer.getWidgetById('nm') : null; - - if (nm) { - nm.applyFilters(filter); - } - } - /** - * Run an action from CRM view toolbar - * - * @param {object} _action - */ - - - view_actions(_action, _widget) { - var app_id = _widget.dom_id.split('_'); - - var et2 = etemplate2.getById(app_id[0]); - var id = et2.widgetContainer.getArrayMgr('content').data.id; - - switch (_widget.id) { - case 'button[edit]': - this.egw.open(id, 'addressbook', 'edit'); - break; - - case 'button[copy]': - this.egw.open(id, 'addressbook', 'edit', { - makecp: 1 - }); - break; - - case 'button[delete]': - et2_dialog.confirm(_widget, egw.lang('Delete this contact?'), egw.lang('Delete')); - break; - - case 'button[close]': - framework.activeApp.tab.closeButton.click(); - break; - - default: - // submit all other buttons back to server - et2.widgetContainer._inst.submit(); - - break; - } - } - /** - * Open the calender to view the selected contacts - * @param {egwAction} _action - * @param {egwActionObject[]} _senders - */ - - - view_calendar(_action, _senders) { - var extras = { - filter: 'all', - cat_id: '', - owner: [] - }; - var orgs = []; - - for (var i = 0; i < _senders.length; i++) { - // Remove UID prefix for just contact_id - var ids = _senders[i].id.split('::'); - - ids.shift(); - ids = ids.join('::'); // Orgs need to get all the contact IDs first - - if (ids.substr(0, 9) == 'org_name:') { - orgs.push(ids); - } else { - // Check to see if this is a user account, we prefer to use - // account ID in calendar - var data = this.egw.dataGetUIDdata(_senders[i].id); - - if (data && data.data && data.data.account_id) { - extras.owner.push(data.data.account_id); - } else { - extras.owner.push('c' + ids); - } - } - } - - if (orgs.length > 0) { - // Get organisation contacts, then show infolog list - this.egw.json('addressbook.addressbook_ui.ajax_organisation_contacts', [orgs], function (contacts) { - for (var i = 0; i < contacts.length; i++) { - extras.owner.push('c' + contacts[i]); - } - - extras.owner = extras.owner.join(','); - this.egw.open('', 'calendar', 'list', extras, 'calendar'); - }, this, true, this).sendRequest(); - } else { - extras.owner = extras.owner.join(','); - egw.open('', 'calendar', 'list', extras, 'calendar'); - } - } - /** - * Add appointment or show calendar for selected contacts, call default nm_action after some checks - * - * @param _action - * @param _senders - */ - - - add_cal(_action, _senders) { - if (!_senders[0].id.match(/^(?:addressbook::)?[0-9]+$/)) { - // send org-view requests to server - _action.data.nm_action = "submit"; - nm_action(_action, _senders); - } else { - var ids = egw.user('account_id') + ','; - - for (var i = 0; i < _senders.length; i++) { - // Remove UID prefix for just contact_id - var id = _senders[i].id.split('::'); - - ids += "c" + id[1] + (i < _senders.length - 1 ? "," : ""); - } - - var extra = {}; - extra[_action.data && _action.data.url && _action.data.url.indexOf('owner') > 0 ? 'owner' : 'participants'] = ids; - if (_action.id === 'schedule_call') extra['videoconference'] = 1; // Use framework to add calendar entry - - egw.open('', 'calendar', 'add', extra); - } - } - /** - * View infolog entries linked to selected contact - * @param {egwAction} _action Select action - * @param {egwActionObject[]} _senders Selected contact(s) - */ - - - view_infolog(_action, _senders) { - var extras = { - action: 'addressbook', - action_id: [], - action_title: _senders.length > 1 ? this.egw.lang('selected contacts') : '' - }; - var orgs = []; - - for (var i = 0; i < _senders.length; i++) { - // Remove UID prefix for just contact_id - var ids = _senders[i].id.split('::'); - - ids.shift(); - ids = ids.join('::'); // Orgs need to get all the contact IDs first - - if (ids.substr(0, 9) == 'org_name:') { - orgs.push(ids); - } else { - extras.action_id.push(ids); - } - } - - if (orgs.length > 0) { - // Get organisation contacts, then show infolog list - this.egw.json('addressbook.addressbook_ui.ajax_organisation_contacts', [orgs], function (contacts) { - extras.action_id = extras.action_id.concat(contacts); - this.egw.open('', 'infolog', 'list', extras, 'infolog'); - }, this, true, this).sendRequest(); - } else { - egw.open('', 'infolog', 'list', extras, 'infolog'); - } - } - /** - * Add task for selected contacts, call default nm_action after some checks - * - * @param _action - * @param _senders - */ - - - add_task(_action, _senders) { - if (!_senders[0].id.match(/^(addressbook::)?[0-9]+$/)) { - // send org-view requests to server - _action.data.nm_action = "submit"; - } else { - // call nm_action's popup - _action.data.nm_action = "popup"; - } - - nm_action(_action, _senders); - } - /** - * Actions via ajax - * - * @param {egwAction} _action - * @param {egwActionObject[]} _selected - */ - - - action(_action, _selected) { - let all = _action.parent.data.nextmatch?.getSelection().all; - let no_notifications = _action.parent.getActionById("no_notifications")?.checked || false; - let ids = []; // Loop so we get just the app's ID - - for (var i = 0; i < _selected.length; i++) { - var id = _selected[i].id; - ids.push(id.split("::").pop()); - } - - switch (_action.id) { - case 'delete': - egw.json("addressbook.addressbook_ui.ajax_action", [_action.id, ids, all, no_notifications]).sendRequest(true); - break; - } - } - /** - * [More...] in phones clicked: copy allways shown phone numbers to phone popup - * - * @param {jQuery.event} _event - * @param {et2_widget} _widget - */ - - - showphones(_event, _widget) { - this._copyvalues({ - tel_home: 'tel_home2', - tel_work: 'tel_work2', - tel_cell: 'tel_cell2', - tel_fax: 'tel_fax2' - }); - - jQuery('table.editphones').css('display', 'inline'); - - _event.stopPropagation(); - - return false; - } - /** - * [OK] in phone popup clicked: copy phone numbers back to always shown ones - * - * @param {jQuery.event} _event - * @param {et2_widget} _widget - */ - - - hidephones(_event, _widget) { - this._copyvalues({ - tel_home2: 'tel_home', - tel_work2: 'tel_work', - tel_cell2: 'tel_cell', - tel_fax2: 'tel_fax' - }); - - jQuery('table.editphones').css('display', 'none'); - - _event.stopPropagation(); - - return false; - } - /** - * Copy content of multiple fields - * - * @param {object} what object with src: dst pairs - */ - - - _copyvalues(what) { - for (var name in what) { - var src = this.et2.getWidgetById(name); - var dst = this.et2.getWidgetById(what[name]); - if (src && dst) dst.set_value(src.get_value ? src.get_value() : src.value); - } // change tel_prefer according to what - - - var tel_prefer = this.et2.getWidgetById('tel_prefer'); - - if (tel_prefer) { - var val = tel_prefer.get_value ? tel_prefer.get_value() : tel_prefer.value; - if (typeof what[val] != 'undefined') tel_prefer.set_value(what[val]); - } - } - /** - * Callback function to create confirm dialog for duplicates contacts - * - * @param {object} _data includes duplicates contacts information - * - */ - - - _confirmdialog_callback(_data) { - var confirmdialog = function (_title, _value, _buttons, _egw_or_appname) { - return et2_createWidget("dialog", { - callback(_buttons, _value) { - if (_buttons == et2_dialog.OK_BUTTON) { - var id = ''; - var content = this.template.widgetContainer.getArrayMgr('content').data; - - for (var row in _value.grid) { - if (_value.grid[row].confirm == "true" && typeof content.grid != 'undefined') { - id = this.options.value.content.grid[row].confirm; - egw.open(id, 'addressbook'); - } - } - } - }, - - title: _title || egw.lang('Input required'), - buttons: _buttons || et2_dialog.BUTTONS_OK_CANCEL, - value: { - content: { - grid: _value - } - }, - template: egw.webserverUrl + '/addressbook/templates/default/dupconfirmdialog.xet' - }, et2_dialog._create_parent(_egw_or_appname)); - }; - - if (_data.msg && _data.doublicates) { - var content = []; - - for (var id in _data.doublicates) { - content.push({ - "confirm": id, - "name": _data.doublicates[id] - }); - } - - confirmdialog(this.egw.lang('Duplicate warning'), content, et2_dialog.BUTTONS_OK_CANCEL); - } - - if (typeof _data.fileas_options == 'object' && this.et2) { - var selbox = this.et2.getWidgetById('fileas_type'); - - if (selbox) { - selbox.set_select_options(_data.fileas_sel_options); - } - } - } - /** - * Callback if certain fields get changed - * - * @param {widget} widget widget - * @param {string} own_id Current AB id - */ - - - check_value(widget, own_id) { - // if we edit an account, call account_change to let it do it's stuff too - if (this.et2.getWidgetById('account_lid')) { - this.account_change(null, widget); - } - - var values = this.et2._inst.getValues(this.et2); - - if (widget.id.match(/n_/)) { - var value = ''; - if (values.n_prefix) value += values.n_prefix + " "; - if (values.n_given) value += values.n_given + " "; - if (values.n_middle) value += values.n_middle + " "; - if (values.n_family) value += values.n_family + " "; - if (values.n_suffix) value += values.n_suffix; - var name = this.et2.getWidgetById("n_fn"); - if (typeof name != 'undefined') name.set_value(value); - } - - egw.json('addressbook.addressbook_ui.ajax_check_values', [values, widget.id, own_id], this._confirmdialog_callback, this, true, this).sendRequest(); - } - - show_custom_country(selectbox) { - if (!selectbox) return; - var custom_field_name = selectbox.id.replace("countrycode", "countryname"); - var custom_field = document.getElementById(custom_field_name); - - if (custom_field && selectbox.value == "-custom-") { - custom_field.style.display = "inline"; - } else if (custom_field) { - if ((selectbox.value == "" || selectbox.value == null) && custom_field.value != "") { - selectbox.value = "-custom-"; // Chosen needs this to update - - jQuery(selectbox).trigger("liszt:updated"); - custom_field.style.display = "inline"; - } else { - custom_field.style.display = "none"; - } - } - - var region = this.et2.getWidgetById(selectbox.name.replace('countrycode', 'region')); - - if (region) { - region.set_country_code(selectbox.value); - } - } - /** - * Add a new mailing list. If any contacts are selected, they will be added. - * - * @param {egwAction} owner - * @param {egwActionObject[]} selected - */ - - - add_new_list(owner, selected) { - if (!owner || typeof owner == 'object') { - var filter = this.et2.getWidgetById('filter'); - owner = filter.getValue() || egw.preference('add_default', 'addressbook'); - } - - var contacts = []; - - if (selected && selected[0] && selected[0].getAllSelected()) { - // Action says all contacts selected, better ask the server for _all_ the IDs - var fetching = fetchAll(selected, this.et2.getWidgetById('nm'), jQuery.proxy(function (contacts) { - this._add_new_list_prompt(owner, contacts); - }, this)); - if (fetching) return; - } - - if (selected && selected.length) { - for (var i = 0; i < selected.length; i++) { - // Remove UID prefix for just contact_id - var ids = selected[i].id.split('::'); - ids.shift(); - ids = ids.join('::'); - contacts.push(ids); - } - } - - this._add_new_list_prompt(owner, contacts); - } - /** - * Ask the user for a name, then create a new list with the provided contacts - * in it. - * - * @param {int} owner - * @param {String[]} contacts - */ - - - _add_new_list_prompt(owner, contacts) { - var lists = this.et2.getWidgetById('filter2'); - let owner_options = this.et2.getArrayMgr('sel_options').getEntry('filter') || {}; - - let callback = function (button, values) { - if (button == et2_dialog.OK_BUTTON) { - egw.json('addressbook.addressbook_ui.ajax_set_list', [0, values.name, values.owner, contacts], function (result) { - if (typeof result == 'object') return; // This response not for us - // Update list - - if (result) { - lists.options.select_options.unshift({ - value: result, - label: values.name - }); - lists.set_select_options(lists.options.select_options); // Set to new list so they can see it easily - - lists.set_value(result); // Call change event manually after setting the value - // Not sure why our selectbox does not trigger change event - - jQuery(lists.node).change(); - } // Add to actions - - - var addressbook_actions = egw_getActionManager('addressbook', false); - var dist_lists = null; - - if (addressbook_actions && (dist_lists = addressbook_actions.getActionById('to_list'))) { - var id = 'to_list_' + result; - var action = dist_lists.addAction('popup', id, values.name); - action.setDefaultExecute(action.parent.onExecute.fnct); - action.updateAction({ - group: 1 - }); - } - }).sendRequest(true); - } - }; - - let dialog = et2_createWidget("dialog", { - callback: callback, - title: this.egw.lang('Add a new list'), - buttons: et2_dialog.BUTTONS_OK_CANCEL, - value: { - content: { - owner: owner - }, - sel_options: { - owner: owner_options - } - }, - template: egw.webserverUrl + '/addressbook/templates/default/add_list_dialog.xet', - class: "et2_prompt", - minWidth: 400 - }, this.et2); - } - /** - * Rename the current distribution list selected in the nextmatch filter2 - * - * Differences from add_new_list are in the dialog, parameters sent, and how the - * response is dealt with - * - * @param {egwAction} action Action selected in context menu (rename) - * @param {egwActionObject[]} selected The selected row(s). Not used for this. - */ - - - rename_list(action, selected) { - var lists = this.et2.getWidgetById('filter2'); - var list = lists.getValue() || 0; - var value = null; - - for (var i = 0; i < lists.options.select_options.length; i++) { - if (lists.options.select_options[i].value == list) { - value = lists.options.select_options[i]; - } - } - - et2_dialog.show_prompt(function (button, name) { - if (button == et2_dialog.OK_BUTTON) { - egw.json('addressbook.addressbook_ui.ajax_set_list', [list, name], function (result) { - if (typeof result == 'object') return; // This response not for us - // Update list - - if (result) { - value.label = name; - lists.set_select_options(lists.options.select_options); - } - }).sendRequest(true); - } - }, this.egw.lang('Name for the distribution list'), this.egw.lang('Rename list'), value.label); - } - /** - * OnChange for distribution list selectbox - */ - - - filter2_onchange() { - var filter = this.et2.getWidgetById('filter'); - var filter2 = this.et2.getWidgetById('filter2'); - var widget = this.et2.getWidgetById('nm'); - var filter2_val = filter2.get_value(); - - if (filter2_val == 'add') { - this.add_new_list(typeof widget == 'undefined' ? this.et2.getWidgetById('filter').value : widget.header.filter.get_value()); - filter2.set_value(''); - } // automatic switch to accounts addressbook or all addressbooks depending on distribution list is a group - else if (filter2_val && filter2_val < 0 !== (filter.get_value() === '0')) { - // Change filter & filter2 at the same time - widget.applyFilters({ - filter: filter2_val < 0 ? '0' : '', - filter2: filter2_val - }); // Don't get rows here, let applyFilters() do it - - return false; - } - - return true; - } - /** - * Method to enable actions by comparing a field with given value - */ - - - nm_compare_field() { - var field = this.et2.getWidgetById('filter2'); - if (field) var val = field.get_value(); - - if (val) { - return nm_compare_field; - } else { - return false; - } - } - /** - * Apply advanced search filters to index nextmatch - * - * @param {object} filters - */ - - - adv_search(filters) { - var index = window.opener.etemplate2.getById('addressbook-index'); - - if (!index) { - alert('Could not find index'); - egw(window).close(); - return false; - } - - var nm = index.widgetContainer.getWidgetById('nm'); - - if (!index) { - window.opener.egw.message('Could not find list', 'error'); - egw(window).close(); - return false; - } // Reset filters first - - - nm.activeFilters = {}; - nm.applyFilters(filters); - return false; - } - /** - * Mail vCard - * - * @param {object} _action - * @param {array} _elems - */ - - - adb_mail_vcard(_action, _elems) { - var link = { - 'preset[type]': [], - 'preset[file]': [] - }; - var content = { - data: { - files: { - file: [], - type: [] - } - } - }; - var nm = this.et2.getWidgetById('nm'); - - if (fetchAll(_elems, nm, jQuery.proxy(function (ids) { - this.adb_mail_vcard(_action, ids.map(function (num) { - return { - id: 'addressbook::' + num - }; - })); - }, this))) { - return; - } - - for (var i = 0; i < _elems.length; i++) { - var idToUse = _elems[i].id; - var idToUseArray = idToUse.split('::'); - idToUse = idToUseArray[1]; - link['preset[type]'].push("text/vcard; charset=" + (egw.preference('vcard_charset', 'addressbook') || 'utf-8')); - link['preset[file]'].push("vfs://default/apps/addressbook/" + idToUse + "/.entry"); - content.data.files.file.push("vfs://default/apps/addressbook/" + idToUse + "/.entry"); - content.data.files.type.push("text/vcard; charset=" + (egw.preference('vcard_charset', 'addressbook') || 'utf-8')); - } - - egw.openWithinWindow("mail", "setCompose", content, link, /mail.mail_compose.compose/); - - for (var index in content) { - if (content[index].file.length > 0) { - egw.message(egw.lang('%1 contact(s) added as %2', content[index].file.length, egw.lang(index))); - return; - } - } - } - /** - * Action function to set business or private mail checkboxes to user preferences - * - * @param {egwAction} action Action user selected. - */ - - - mailCheckbox(action) { - var preferences = { - business: action.getManager().getActionById('email_business').checked ? true : false, - private: action.getManager().getActionById('email_home').checked ? true : false - }; - this.egw.set_preference('addressbook', 'preferredMail', preferences); - } - /** - * Action function to add the email address (business or home) of the selected - * contacts to a compose email popup window. - * - * Uses the egw API to handle the opening of the popup. - * - * @param {egwAction} action Action user selected. Should have ID of either - * 'email_business' or 'email_home', from server side definition of actions. - * @param {egwActionObject[]} selected Selected rows - */ - - - addEmail(action, selected) { - // Check for all selected. - var nm = this.et2.getWidgetById('nm'); - - if (fetchAll(selected, nm, jQuery.proxy(function (ids) { - // fetchAll() returns just the ID, no prefix, so map it to match normal selected - this.addEmail(action, ids.map(function (num) { - return { - id: 'addressbook::' + num - }; - })); - }, this))) { - // Need more IDs, will use the above callback when they're ready. - return; - } // Go through selected & pull email addresses from data - - - var emails = []; - - for (var i = 0; i < selected.length; i++) { - // Pull data from global cache - var data = egw.dataGetUIDdata(selected[i].id) || { - data: {} - }; - var email_business = data.data[action.getManager().getActionById('email_business').checked ? 'email' : '']; - var email = data.data[action.getManager().getActionById('email_home').checked ? 'email_home' : '']; // prefix email with full name - - var personal = data.data.n_fn || ''; - if (personal.match(/[^a-z0-9. -]/i)) personal = '"' + personal.replace(/"/, '\\"') + '"'; //remove comma in personal as it will confilict with mail content comma seperator in the process - - personal = personal.replace(/,/g, ''); - - if (email_business) { - emails.push((personal ? personal + ' <' : '') + email_business + (personal ? '>' : '')); - } - - if (email) { - emails.push((personal ? personal + ' <' : '') + email + (personal ? '>' : '')); - } - } - - switch (action.id) { - case "add_to_to": - egw.open_link('mailto:' + emails.join(',').replace(/&/g, '__AMPERSAND__')); - break; - - case "add_to_cc": - egw.open_link('mailto:' + '?cc=' + emails.join(',').replace(/&/g, '__AMPERSAND__')); //egw.mailto('mailto:'); - - break; - - case "add_to_bcc": - egw.open_link('mailto:' + '?bcc=' + emails.join(',').replace(/&/g, '__AMPERSAND__')); - break; - } - - return false; - } - /** - * Merge the selected contacts into the target document. - * - * Normally we let the framework handle this, but in addressbook we want to - * interfere and customize things a little to ask about saving to infolog. - * - * @param {egwAction} action - The document they clicked - * @param {egwActionObject[]} selected - Rows selected - */ - - - merge_mail(action, selected, target) { - // Special processing for email documents - ask about infolog - if (action && action.data && selected.length > 1) { - var callback = function (button, value) { - if (button == et2_dialog.OK_BUTTON) { - var _action = jQuery.extend(true, {}, action); - - if (value.infolog) { - _action.data.menuaction += '&to_app=infolog&info_type=' + value.info_type; - } - - nm_action(_action, selected, target); - } - }; - - et2_createWidget("dialog", { - callback: callback, - title: action.caption, - buttons: et2_dialog.BUTTONS_OK_CANCEL, - type: et2_dialog.QUESTION_MESSAGE, - template: egw.webserverUrl + '/addressbook/templates/default/mail_merge_dialog.xet', - value: { - content: { - info_type: 'email' - }, - sel_options: this.et2.getArrayMgr('sel_options').data - } - }); - } else { - // Normal processing for only one contact selected - return nm_action(action, selected, target); - } - } - /** - * Retrieve the current state of the application for future restoration - * - * Overridden from parent to handle viewing a contact. In this case state - * will be {contact_id: #} - * - * @return {object} Application specific map representing the current state - */ - - - getState() { - // Most likely we're in the list view - var state = super.getState(); - - if (jQuery.isEmptyObject(state)) { - // Not in a list view. Try to find contact ID - var etemplates = etemplate2.getByApplication('addressbook'); - - for (var i = 0; i < etemplates.length; i++) { - var content = etemplates[i].widgetContainer.getArrayMgr("content"); - - if (content && content.getEntry('id')) { - state = { - app: 'addressbook', - id: content.getEntry('id'), - type: 'view' - }; - break; - } - } - } - - return state; - } - /** - * Set the application's state to the given state. - * - * Overridden from parent to stop the contact view's infolog nextmatch from - * being changed. - * - * @param {{name: string, state: object}|string} state Object (or JSON string) for a state. - * Only state is required, and its contents are application specific. - * - * @return {boolean} false - Returns false to stop event propagation - */ - - - setState(state, template) { - var current_state = this.getState(); // 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); - } - } // Redirect from view to list - parent would do this, but infolog nextmatch stops it - - - if (current_state.app && current_state.id && (typeof state.state == 'undefined' || typeof state.state.app == 'undefined')) { - // Redirect to list - // '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, '_'); - egw.open('', this.appname, 'list', { - 'favorite': safe_name - }, this.appname); - return false; - } else if (jQuery.isEmptyObject(state)) { - // Regular handling first to clear everything but advanced search - super.setState(state); // Clear advanced search, which is in session and etemplate - - egw.json('addressbook.addressbook_ui.ajax_clear_advanced_search', [], function () { - framework.setWebsiteTitle('addressbook', ''); - var index = etemplate2.getById('addressbook-index'); - - if (index && index.widgetContainer) { - var nm = index.widgetContainer.getWidgetById('nm'); - - if (nm) { - nm.applyFilters({ - advanced_search: false - }); - } - } - }, this).sendRequest(true); - return false; - } else if (state.state.grouped_view) { - // Deal with grouped views that are not valid (not in list of options) - // by faking viewing that organisation - var index = etemplate2.getById('addressbook-index'); - - if (index && index.widgetContainer) { - var grouped = index.widgetContainer.getWidgetById('grouped_view'); - var options; - - if (grouped && grouped.options && grouped.options.select_options) { - options = grouped.options.select_options; - } // Check to see if it's not there - - - if (options && (options.find && !options.find(function (e) { - console.log(e); - return e.value === state.state.grouped_view; - }) || typeof options.find === 'undefined' && !options[state.state.grouped_view])) { - window.setTimeout(function () { - app.addressbook.setState(state); - }, 500); - var nm = index.widgetContainer.getWidgetById('nm'); - - var action = nm.controller._actionManager.getActionById('view_org'); - - var senders = [{ - _context: { - _widget: nm - } - }]; - return nm_action(action, senders, {}, { - ids: [state.state.grouped_view] - }); - } - } - } // Make sure advanced search is false if not set, this clears any - // currently set advanced search - - - if (typeof state.state.advanced_search === 'undefined') { - state.state.advanced_search = false; - } - - return super.setState(state); - } - /** - * Field changed, call server validation - * - * @param {jQuery.Event} _ev - * @param {et2_button} _widget - */ - - - account_change(_ev, _widget) { - switch (_widget.id) { - case 'account_passwd': - case 'account_lid': - case 'n_family': - case 'n_given': - case 'account_passwd_2': - var values = this.et2._inst.getValues(this.et2); - - var data = { - account_id: this.et2.getArrayMgr('content').data.account_id, - account_lid: values.account_lid, - account_firstname: values.n_given, - account_lastname: values.n_family, - account_email: values.email, - account_passwd: values.account_passwd, - account_passwd_2: values.account_passwd_2 - }; - this.egw.message(''); - this.egw.json('admin_account::ajax_check', [data, _widget.id], function (_msg) { - if (_msg && typeof _msg == 'string') { - egw(window).message(_msg, 'error'); // context get's lost :( - - _widget.getDOMNode().focus(); - } - }, this).sendRequest(); - break; - } - } - /** - * Get title in order to set it as document title - * @returns {string} - */ - - - getWindowTitle() { - var widget = this.et2.getWidgetById('n_fn'); - if (widget) return widget.options.value; - } - /** - * Enable/Disable geolocation action items in contextmenu base on address availabilty - * - * @param {egwAction} _action - * @param {egwActionObject[]} _selected selected rows - * @returns {boolean} return false if no address found - */ - - - geoLocation_enabled(_action, _selected) { - // multiple selection is not supported - if (_selected.length > 1) return false; - var url = this.getGeolocationConfig(); // exit if no url or invalide url given - - if (!url || typeof url === 'undefined' || typeof url !== 'string') { - egw.debug('warn', 'no url or invalid url given as geoLocationUrl'); - return false; - } - - var content = egw.dataGetUIDdata(_selected[0].id); // Selected, but data not found - - if (!content || typeof content.data === 'undefined') return false; - var type = _action.id === 'business' ? 'one' : 'two'; - var addrs = [content.data['adr_' + type + '_street'], content.data['adr_' + type + '_locality'], content.data['adr_' + type + '_postalcode']]; - var fields = ''; // Replcae placeholders with acctual values - - for (var i = 0; i < addrs.length; i++) { - fields += addrs[i] ? addrs[i] : ''; - } - - return url !== '' && fields !== '' ? true : false; - } - /** - * Generate a geo location URL based on geolocation_url in - * site configuration - * - * @param {object} _dest_data - * @param {string} _dest_type type of destination address ('one'| 'two') - * @param {object} _src_data address data to be used as source contact data|coordination object - * @param {string} _src_type type of source address ('browser'|'one'|'two') - * @returns {Boolean|string} return url and return false if no address - */ - - - geoLocationUrl(_dest_data, _dest_type, _src_data, _src_type) { - var dest_type = _dest_type || 'one'; - var url = this.getGeolocationConfig(); // exit if no url or invalide url given - - if (!url || typeof url === 'undefined' || typeof url !== 'string') { - egw.debug('warn', 'no url or invalid url given as geoLocationUrl'); - return false; - } // array of placeholders with their representing values - - - var addrs = [[// source address - { - id: 'r0', - val: _src_type === 'browser' ? _src_data.latitude : _src_data['adr_' + _src_type + '_street'] - }, { - id: 't0', - val: _src_type === 'browser' ? _src_data.longitude : _src_data['adr_' + _src_type + '_locality'] - }, { - id: 'c0', - val: _src_type === 'browser' ? '' : _src_data['adr_' + _src_type + '_countrycode'] - }, { - id: 'z0', - val: _src_type === 'browser' ? '' : _src_data['adr_' + _src_type + '_postalcode'] - }], [// destination address - { - id: 'r1', - val: _dest_data['adr_' + dest_type + '_street'] - }, { - id: 't1', - val: _dest_data['adr_' + dest_type + '_locality'] - }, { - id: 'c1', - val: _dest_data['adr_' + dest_type + '_countrycode'] - }, { - id: 'z1', - val: _dest_data['adr_' + dest_type + '_postalcode'] - }]]; - var src_param = url.match(/{{%rs=.*%rs}}/ig); - - if (src_param[0]) { - src_param = src_param[0].replace(/{{%rs=/, ''); - src_param = src_param.replace(/%rs}}/, ''); - url = url.replace(/{{%rs=.*%rs}}/, src_param); - } - - var d_param = url.match(/{{%d=.*%d}}/ig); - - if (d_param[0]) { - d_param = d_param[0].replace(/{{%d=/, ''); - d_param = d_param.replace(/%d}}/, ''); - url = url.replace(/{{%d=.*%d}}/, d_param); - } // Replcae placeholders with acctual values - - - for (var j = 0; j < addrs.length; j++) { - for (var i = 0; i < addrs[j].length; i++) { - url = url.replace('%' + addrs[j][i]['id'], addrs[j][i]['val'] ? addrs[j][i]['val'] : ""); - } - } - - return url !== '' ? url : false; - } - /** - * Open a popup base on selected address in provided map - * - * @param {object} _action - * @param {object} _selected - */ - - - geoLocationExec(_action, _selected) { - var content = egw.dataGetUIDdata(_selected[0].id); - var geolocation_src = egw.preference('geolocation_src', 'addressbook'); - var self = this; - - if (geolocation_src === 'browser' && navigator.geolocation) { - navigator.geolocation.getCurrentPosition(function (position) { - if (position && position.coords) { - var url = self.geoLocationUrl(content.data, _action.id === 'business' ? 'one' : 'two', position.coords, 'browser'); - window.open(url, '_blank'); - } - }); - } else { - egw.json('addressbook.addressbook_ui.ajax_get_contact', [egw.user('account_id')], function (_data) { - var url = self.geoLocationUrl(content.data, _action.id === 'business' ? 'one' : 'two', _data, geolocation_src === 'browser' ? 'one' : geolocation_src); - window.open(url, '_blank'); - }).sendRequest(); - } - } - /** - * Get geolocation_url stored in config|default url - * - * @returns {String} - */ - - - getGeolocationConfig() { - // This default url should be identical to the first value of geolocation_url array - // defined in addressbook_hooks::config - var default_url = 'https://maps.here.com/directions/drive{{%rs=/%rs}}%r0,%t0,%z0,%c0{{%d=/%d}}%r1,%t1,%z1+%c1'; - var geo_url = egw.config('geolocation_url'); - if (geo_url) geo_url = geo_url[0]; - return geo_url || default_url; - } - /** - * Check to see if the selection contains at most one account - * - * @param {egwAction} action - * @param {egwActionObject[]} selected Selected rows - */ - - - can_merge(action, selected) { - return selected.filter(function (row) { - var data = egw.dataGetUIDdata(row.id); - return data && data.data.account_id; - }).length <= 1; - } - /** - * Check if the share action is enabled for this entry - * This only works for single contacts - * - * @param {egwAction} _action - * @param {egwActionObject[]} _entries - * @param {egwActionObject} _target - * @returns {boolean} if action is enabled - */ - - - is_share_enabled(_action, _entries, _target) { - var enabled = true; - - for (var i = 0; i < _entries.length; i++) { - let id = _entries[i].id.split('::'); - - if (isNaN(id[1])) { - return false; - } - } - - return enabled; - } - /** - * Check if selected user(s) is online then enable action - * @param _action - * @param _selected - */ - - - videoconference_isUserOnline(_action, _selected) { - let list = app.status ? app.status.getEntireList() : {}; - - for (let sel in _selected) { - if (sel == '0' && _selected[sel]['id'] == 'nm') continue; - let row = egw.dataGetUIDdata(_selected[sel]['id']); - let enabled = false; - - for (let entry in list) { - if (row.data && row.data.account_id && row.data.account_id == list[entry]['account_id']) { - enabled = list[entry]['data']['status']['active']; - } - } - - if (!enabled) return false; - } - - return true; - } - - videoconference_isThereAnyCall(_action, _selected) { - return this.videoconference_isUserOnline(_action, _selected) && egw.getSessionItem('status', 'videoconference-session'); - } - /** - * Call action - * @param _action - * @param _selected - */ - - - videoconference_actionCall(_action, _selected) { - let data = []; - - for (let sel in _selected) { - let row = egw.dataGetUIDdata(_selected[sel]['id']); - data.push({ - id: row.data.account_id, - name: row.data.n_fn, - avatar: "account:" + row.data.account_id, - audioonly: _action.id == 'audiocall' ? true : false - }); - } - - if (_action.id == 'invite') { - app.status.inviteToCall(data, egw.getSessionItem('status', 'videoconference-session')); - } else { - app.status.makeCall(data); - } - } - /** - * Check if new shared_with value is allowed / user has rights to share into that AB - * - * Remove the entry again, if user is not allowed - */ - - - shared_changed() { - let shared = this.et2.getInputWidgetById('shared_values'); - let value = shared?.get_value(); - - if (value) { - this.egw.json('addressbook.addressbook_ui.ajax_check_shared', [{ - contact: this.et2.getInstanceManager().getValues(this.et2), - // for sharing policy - shared_values: value, - shared_writable: this.et2.getInputWidgetById('shared_writable').get_value() - }], _data => { - if (Array.isArray(_data) && _data.length) { - // remove not allowed entries - shared.set_value(value.filter(val => _data.indexOf(val) === -1)); - } - }).sendRequest(); - } - } - -} - -app.classes.addressbook = AddressbookApp; -//# sourceMappingURL=app.js.map diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js deleted file mode 100644 index bcab4e052d..0000000000 --- a/api/js/etemplate/et2_core_DOMWidget.js +++ /dev/null @@ -1,751 +0,0 @@ -/** - * EGroupware eTemplate2 - JS DOM Widget class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - et2_core_interfaces; - et2_core_widget; - /api/js/egw_action/egw_action.js; -*/ -import { ClassWithAttributes } from './et2_core_inheritance'; -import { et2_IDOMNode } from "./et2_core_interfaces"; -import { et2_hasChild, et2_no_init } from "./et2_core_common"; -import { et2_widget } from "./et2_core_widget"; -import { egw_getActionManager, egwActionObject, egwActionObjectInterface, egw_getAppObjectManager, egw_getObjectManager } from '../egw_action/egw_action.js'; -import { EGW_AI_DRAG_OVER, EGW_AI_DRAG_OUT } from '../egw_action/egw_action_constants.js'; -import { egw } from "../jsapi/egw_global"; -/** - * Abstract widget class which can be inserted into the DOM. All widget classes - * deriving from this class have to care about implementing the "getDOMNode" - * function which has to return the DOM-Node. - * - * @augments et2_widget - */ -export class et2_DOMWidget extends et2_widget { - /** - * When the DOMWidget is initialized, it grabs the DOM-Node of the parent - * object (if available) and passes it to its own "createDOMNode" function - * - * @memberOf et2_DOMWidget - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); - this.parentNode = null; - this.disabled = false; - this._attachSet = { - "node": null, - "parent": null - }; - this._surroundingsMgr = null; - } - /** - * Detatches the node from the DOM and clears all references to the parent - * node or the dom node of this widget. - */ - destroy() { - this.detachFromDOM(); - this.parentNode = null; - this._attachSet = {}; - if (this._actionManager) { - var app_om = egw_getObjectManager(this.egw().getAppName(), false, 1); - if (app_om) { - var om = app_om.getObjectById(this.id); - if (om) - om.remove(); - } - this._actionManager.remove(); - this._actionManager = null; - } - if (this._surroundingsMgr) { - this._surroundingsMgr.destroy(); - this._surroundingsMgr = null; - } - super.destroy(); - } - /** - * Attaches the container node of this widget to the DOM-Tree - */ - doLoadingFinished() { - // Check whether the parent implements the et2_IDOMNode interface. If - // yes, grab the DOM node and create our own. - if (this.getParent() && this.getParent().implements(et2_IDOMNode)) { - if (this.options.parent_node) { - this.set_parent_node(this.options.parent_node); - } - else { - this.setParentDOMNode(this.getParent().getDOMNode(this)); - } - } - return true; - } - /** - * Detaches the widget from the DOM tree, if it had been attached to the - * DOM-Tree using the attachToDOM method. - */ - detachFromDOM() { - if (this._attachSet && this._attachSet.node && this._attachSet.parent) { - // Remove the current node from the parent node - try { - this._attachSet.parent.removeChild(this._attachSet.node); - } - catch (e) { - // Don't throw a DOM error if the node wasn't in the parent - } - // Reset the "attachSet" - this._attachSet = { - "node": null, - "parent": null - }; - return true; - } - return false; - } - /** - * Attaches the widget to the DOM tree. Fails if the widget is already - * attached to the tree or no parent node or no node for this widget is - * defined. - */ - attachToDOM() { - // Attach the DOM node of this widget (if existing) to the new parent - var node = this.getDOMNode(this); - if (node && this.parentNode && - (!this._attachSet || this._attachSet && node != this._attachSet.node || - this.parentNode != this._attachSet.parent)) { - // If the surroundings manager exists, surround the DOM-Node of this - // widget with the DOM-Nodes inside the surroundings manager. - if (this._surroundingsMgr) { - node = this._surroundingsMgr.getDOMNode(node); - } - // Append this node at its index - var idx = this.getDOMIndex(); - if (idx < 0 || idx >= this.parentNode.childNodes.length - 1) { - this.parentNode.appendChild(node); - } - else { - this.parentNode.insertBefore(node, this.parentNode.childNodes[idx]); - } - // Store the currently attached nodes - this._attachSet = { - "node": node, - "parent": this.parentNode - }; - return true; - } - return false; - } - /** - * Inserts a child at the given index. - * - * @param _node is the node which should be added. It has to be an instance - * of et2_widget - * @param _idx is the position at which the element should be added. - */ - insertChild(_node, _idx) { - super.insertChild(_node, _idx); - if (_node.instanceOf && _node.instanceOf(et2_DOMWidget) && typeof _node.hasOwnProperty('parentNode') && this.getDOMNode(this)) { - try { - _node.setParentDOMNode(this.getDOMNode(_node)); - } - catch (_a) { - // Not ready to be added, usually due to construction order, - // will probably try again in doLoadingFinished() - } - } - // _node is actually a Web Component - else if (_node instanceof Element) { - this.getDOMNode().append(_node); - } - } - isAttached() { - return this.parentNode != null; - } - getSurroundings() { - if (!this._surroundingsMgr) { - this._surroundingsMgr = new et2_surroundingsMgr(this); - } - return this._surroundingsMgr; - } - /** - * Get data for the tab this widget is on. - * - * Will return null if the widget is not on a tab or tab data containing - * - id - * - label - * - widget (top level widget) - * - contentDiv (jQuery object for the div the tab content is in) - * - * @returns {Object|null} Data for tab the widget is on - */ - get_tab_info() { - var parent = this; - do { - parent = parent.getParent(); - } while (parent !== this.getRoot() && parent.getType() !== 'tabbox'); - // No tab - if (parent === this.getRoot()) { - return null; - } - let tabbox = parent; - // Find the tab index - for (var i = 0; i < tabbox.tabData.length; i++) { - // Find the tab by DOM heritage - // @ts-ignore - if (tabbox.tabData[i].contentDiv.has(this.div).length) { - return tabbox.tabData[i]; - } - } - // On a tab, but we couldn't find it by DOM nodes Maybe tab template is - // not loaded yet. Try checking IDs. - var template = this; - do { - template = template.getParent(); - // @ts-ignore - } while (template !== tabbox && template.getType() !== 'template'); - for (var i = tabbox.tabData.length - 1; i >= 0; i--) { - if (template && template.id && template.id === tabbox.tabData[i].id) { - return tabbox.tabData[i]; - } - } - // Fallback - let fallback = this.getParent(); - if (typeof fallback.get_tab_info === 'function') { - return fallback.get_tab_info(); - } - return null; - } - /** - * Set the parent DOM node of this element. Takes a wider variety of types - * than setParentDOMNode(), and matches the set_ naming convention. - * - * @param _node String|DOMNode DOM node to contain the widget, or the ID of the DOM node. - */ - set_parent_node(_node) { - if (typeof _node == "string") { - var parent = jQuery('#' + _node); - if (parent.length === 0 && window.parent) { - // Could not find it, try again with wider context - // (in case there's an iframe in admin, for example) - parent = jQuery('#' + _node, window.parent.document); - } - if (parent.length === 0) { - this.egw().debug('warn', 'Unable to find DOM parent node with ID "%s" for widget %o.', _node, this); - } - else { - this.setParentDOMNode(parent.get(0)); - } - } - else { - this.setParentDOMNode(_node); - } - } - /** - * Set the parent DOM node of this element. If another parent node is already - * set, this widget removes itself from the DOM tree - * - * @param _node - */ - setParentDOMNode(_node) { - if (_node != this.parentNode) { - // Detatch this element from the DOM tree - this.detachFromDOM(); - this.parentNode = _node; - // And attatch the element to the DOM tree - this.attachToDOM(); - } - } - /** - * Returns the parent node. - */ - getParentDOMNode() { - return this.parentNode; - } - /** - * Returns the index of this element in the DOM tree - */ - getDOMIndex() { - if (this.getParent()) { - var idx = 0; - var children = this.getParent().getChildren(); - if (children && children.indexOf) - return children.indexOf(this); - egw.debug('warn', 'No Array.indexOf(), falling back to looping. '); - for (var i = 0; i < children.length; i++) { - if (children[i] == this) { - return idx; - } - else if (children[i].isInTree()) { - idx++; - } - } - } - return -1; - } - /** - * Sets the id of the DOM-Node. - * - * DOM id's have dots "." replaced with dashes "-" - * - * @param {string} _value id to set - */ - set_id(_value) { - this.id = _value; - this.dom_id = _value ? this.getInstanceManager().uniqueId + '_' + _value.replace(/\./g, '-') : _value; - var node = this.getDOMNode(this); - if (node) { - if (_value != "") { - node.setAttribute("id", this.dom_id); - } - else { - node.removeAttribute("id"); - } - } - } - set_disabled(_value) { - var node = this._surroundingsMgr != null ? this._surroundingsMgr.getDOMNode(this.getDOMNode(this)) : this.getDOMNode(this); - if (node && this.disabled != _value) { - this.disabled = _value; - if (_value) { - jQuery(node).hide(); - } - else { - jQuery(node).show(); - } - } - } - set_width(_value) { - this.width = _value; - var node = this.getDOMNode(this); - if (node) { - jQuery(node).css("width", _value); - } - } - set_height(_value) { - this.height = _value; - var node = this.getDOMNode(this); - if (node) { - jQuery(node).css("height", _value); - } - } - set_class(_value) { - var node = this.getDOMNode(this); - if (node) { - if (this["class"]) { - jQuery(node).removeClass(this["class"]); - } - jQuery(node).addClass(_value); - } - this["class"] = _value; - } - set_overflow(_value) { - this.overflow = _value; - var node = this.getDOMNode(this); - if (node) { - jQuery(node).css("overflow", _value); - } - } - set_data(_value) { - var node = this.getDOMNode(this); - if (node && _value) { - var pairs = _value.split(/,/g); - for (var i = 0; i < pairs.length; ++i) { - var name_value = pairs[i].split(':'); - jQuery(node).attr('data-' + name_value[0], name_value[1]); - } - } - } - set_background(_value) { - var node = this.getDOMNode(this); - var values = ''; - if (_value && node) { - values = _value.split(','); - jQuery(node).css({ - "background-image": 'url("' + values[0] + '")', - "background-position-x": values[1], - "background-position-y": values[2], - "background-scale": values[3] - }); - } - } - /** - * Set Actions on the widget - * - * Each action is defined as an object: - * - * move: { - * type: "drop", - * acceptedTypes: "mail", - * icon: "move", - * caption: "Move to" - * onExecute: javascript:mail_move" - * } - * - * This will turn the widget into a drop target for "mail" drag types. When "mail" drag types are dropped, - * the global function mail_move(egwAction action, egwActionObject sender) will be called. The ID of the - * dragged "mail" will be in sender.id, some information about the sender will be in sender.context. The - * etemplate2 widget involved can typically be found in action.parent.data.widget, so your handler - * can operate in the widget context easily. The location varies depending on your action though. It - * might be action.parent.parent.data.widget - * - * To customise how the actions are handled for a particular widget, override _link_actions(). It handles - * the more widget-specific parts. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - * @see api/src/Etemplate/Widget/Nextmatch.php egw_actions() method - */ - set_actions(actions) { - if (this.id == "" || typeof this.id == "undefined") { - this.egw().debug("warn", "Widget should have an ID if you want actions", this); - return; - } - // Initialize the action manager and add some actions to it - // Only look 1 level deep - var gam = egw_getActionManager(this.egw().appName, true, 1); - if (typeof this._actionManager != "object") { - if (gam.getActionById(this.getInstanceManager().uniqueId, 1) !== null) { - gam = gam.getActionById(this.getInstanceManager().uniqueId, 1); - } - if (gam.getActionById(this.id, 1) != null) { - this._actionManager = gam.getActionById(this.id, 1); - } - else { - this._actionManager = gam.addAction("actionManager", this.id); - } - } - this._actionManager.updateActions(actions, this.egw().appName); - if (this.options.default_execute) - this._actionManager.setDefaultExecute(this.options.default_execute); - // Put a reference to the widget into the action stuff, so we can - // easily get back to widget context from the action handler - this._actionManager.data = { widget: this }; - // Link the actions to the DOM - this._link_actions(actions); - } - set_default_execute(_default_execute) { - this.options.default_execute = _default_execute; - if (this._actionManager) - this._actionManager.setDefaultExecute(null, _default_execute); - } - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * This can be overwritten to not allow all actions, by not returning them here. - * - * @param actions - * @returns {Array} - */ - _get_action_links(actions) { - var action_links = []; - for (var i in actions) { - var action = actions[i]; - action_links.push(typeof action.id != 'undefined' ? action.id : i); - } - return action_links; - } - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions(actions) { - // Get the top level element for the tree - var objectManager = egw_getAppObjectManager(true); - var widget_object = objectManager.getObjectById(this.id); - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = objectManager.insertObject(false, new egwActionObject(this.id, objectManager, (new et2_action_object_impl(this)).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); - } - else { - widget_object.setAOI((new et2_action_object_impl(this, this.getDOMNode())).getAOI()); - } - // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - widget_object.updateActionLinks(action_links); - } -} -et2_DOMWidget._attributes = { - "disabled": { - "name": "Disabled", - "type": "boolean", - "description": "Defines whether this widget is visible. Not to be confused with an input widget's HTML attribute 'disabled'.", - "default": false - }, - "width": { - "name": "Width", - "type": "dimension", - "default": et2_no_init, - "description": "Width of the element in pixels, percentage or 'auto'" - }, - "height": { - "name": "Height", - "type": "dimension", - "default": et2_no_init, - "description": "Height of the element in pixels, percentage or 'auto'" - }, - "class": { - "name": "CSS Class", - "type": "string", - "default": et2_no_init, - "description": "CSS Class which is applied to the dom element of this node" - }, - "overflow": { - "name": "Overflow", - "type": "string", - "default": et2_no_init, - "description": "If set, the css-overflow attribute is set to that value" - }, - "parent_node": { - "name": "DOM parent", - "type": "string", - "default": et2_no_init, - "description": "Insert into the target DOM node instead of the normal location" - }, - "actions": { - "name": "Actions list", - "type": "any", - "default": et2_no_init, - "description": "List of egw actions that can be done on the widget. This includes context menu, drag and drop. TODO: Link to action documentation" - }, - default_execute: { - name: "Default onExecute for actions", - type: "js", - default: et2_no_init, - description: "Set default onExecute javascript method for action not specifying their own" - }, - resize_ratio: { - name: "Resize height of the widget on callback resize", - type: "string", - default: '', - description: "Allow Resize height of the widget based on exess height and given ratio" - }, - data: { - name: "comma-separated name:value pairs set as data attributes on DOM node", - type: "string", - default: '', - description: 'data="mime:${row}[mime]" would generate data-mime="..." in DOM, eg. to use it in CSS on a parent' - }, - background: { - name: "Add background image", - type: "string", - default: '', - description: "Sets background image, left, right and scale on DOM", - } -}; -/** - * The surroundings manager class allows to append or prepend elements around - * an widget node. - */ -export class et2_surroundingsMgr extends ClassWithAttributes { - /** - * Constructor - * - * @memberOf et2_surroundingsMgr - * @param _widget - */ - constructor(_widget) { - super(); - this._widgetContainer = null; - this._widgetSurroundings = []; - this._widgetPlaceholder = null; - this._widgetNode = null; - this._ownPlaceholder = true; - this._surroundingsUpdated = false; - this.widget = _widget; - } - destroy() { - this._widgetContainer = null; - this._widgetSurroundings = null; - this._widgetPlaceholder = null; - this._widgetNode = null; - } - prependDOMNode(_node) { - this._widgetSurroundings.unshift(_node); - this._surroundingsUpdated = true; - } - appendDOMNode(_node) { - // Append an placeholder first if none is existing yet - if (this._ownPlaceholder && this._widgetPlaceholder == null) { - this._widgetPlaceholder = document.createElement("span"); - this._widgetSurroundings.push(this._widgetPlaceholder); - } - // Append the given node - this._widgetSurroundings.push(_node); - this._surroundingsUpdated = true; - } - insertDOMNode(_node) { - if (!this._ownPlaceholder || this._widgetPlaceholder == null) { - this.appendDOMNode(_node); - return; - } - // Get the index of the widget placeholder and delete it, insert the - // given node instead - var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); - this._widgetSurroundings.splice(idx, 1, _node); - // Delete the reference to the own placeholder - this._widgetPlaceholder = null; - this._ownPlaceholder = false; - } - removeDOMNode(_node) { - for (var i = 0; this._widgetSurroundings && i < this._widgetSurroundings.length; i++) { - if (this._widgetSurroundings[i] == _node) { - this._widgetSurroundings.splice(i, 1); - this._surroundingsUpdated = true; - break; - } - } - } - setWidgetPlaceholder(_node) { - if (_node != this._widgetPlaceholder) { - if (_node != null && this._ownPlaceholder && this._widgetPlaceholder != null) { - // Delete the current placeholder which was created by the - // widget itself - var idx = this._widgetSurroundings.indexOf(this._widgetPlaceholder); - this._widgetSurroundings.splice(idx, 1); - // Delete any reference to the own placeholder and set the - // _ownPlaceholder flag to false - this._widgetPlaceholder = null; - this._ownPlaceholder = false; - } - this._ownPlaceholder = (_node == null); - this._widgetPlaceholder = _node; - this._surroundingsUpdated = true; - } - } - _rebuildContainer() { - // Return if there has been no change in the "surroundings-data" - if (!this._surroundingsUpdated) { - return false; - } - // Build the widget container - if (this._widgetSurroundings.length > 0) { - // Check whether the widgetPlaceholder is really inside the DOM-Tree - var hasPlaceholder = et2_hasChild(this._widgetSurroundings, this._widgetPlaceholder); - // If not, append another widget placeholder - if (!hasPlaceholder) { - this._widgetPlaceholder = document.createElement("span"); - this._widgetSurroundings.push(this._widgetPlaceholder); - this._ownPlaceholder = true; - } - // If the surroundings array only contains one element, set this one - // as the widget container - if (this._widgetSurroundings.length == 1) { - if (this._widgetSurroundings[0] == this._widgetPlaceholder) { - this._widgetContainer = null; - } - else { - this._widgetContainer = this._widgetSurroundings[0]; - } - } - else { - // Create an outer "span" as widgetContainer - this._widgetContainer = document.createElement("span"); - // Append the children inside the widgetSurroundings array to - // the widget container - for (var i = 0; i < this._widgetSurroundings.length; i++) { - this._widgetContainer.appendChild(this._widgetSurroundings[i]); - } - } - } - else { - this._widgetContainer = null; - this._widgetPlaceholder = null; - } - this._surroundingsUpdated = false; - return true; - } - update() { - if (this._surroundingsUpdated) { - var attached = this.widget ? this.widget.isAttached() : false; - // Reattach the widget - this will call the "getDOMNode" function - // and trigger the _rebuildContainer function. - if (attached && this.widget) { - this.widget.detachFromDOM(); - this.widget.attachToDOM(); - } - } - } - getDOMNode(_widgetNode) { - // Update the whole widgetContainer if this is not the first time this - // function has been called but the widget node has changed. - if (this._widgetNode != null && this._widgetNode != _widgetNode) { - this._surroundingsUpdated = true; - } - // Copy a reference to the given node - this._widgetNode = _widgetNode; - // Build the container if it didn't exist yet. - var updated = this._rebuildContainer(); - // Return the widget node itself if there are no surroundings arround - // it - if (this._widgetContainer == null) { - return _widgetNode; - } - // Replace the widgetPlaceholder with the given widget node if the - // widgetContainer has been updated - if (updated) { - this._widgetPlaceholder.parentNode.replaceChild(_widgetNode, this._widgetPlaceholder); - if (!this._ownPlaceholder) { - this._widgetPlaceholder = _widgetNode; - } - } - // Return the widget container - return this._widgetContainer; - } - getWidgetSurroundings() { - return this._widgetSurroundings; - } -} -/** - * The egw_action system requires an egwActionObjectInterface Interface implementation - * to tie actions to DOM nodes. This one can be used by any widget. - * - * The class extension is different than the widgets - * - * @param {et2_DOMWidget} widget - * @param {Object} node - * - */ -export class et2_action_object_impl { - constructor(_widget, _node) { - var widget = _widget; - var objectNode = _node; - this.aoi = new egwActionObjectInterface(); - this.aoi.getWidget = function () { - return widget; - }; - this.aoi.doGetDOMNode = function () { - return objectNode ? objectNode : widget.getDOMNode(); - }; - // _outerCall may be used to determine, whether the state change has been - // evoked from the outside and the stateChangeCallback has to be called - // or not. - this.aoi.doSetState = function (_state, _outerCall) { - }; - // The doTiggerEvent function may be overritten by the aoi if it wants to - // support certain action implementation specific events like EGW_AI_DRAG_OVER - // or EGW_AI_DRAG_OUT - this.aoi.doTriggerEvent = function (_event, _data) { - switch (_event) { - case EGW_AI_DRAG_OVER: - jQuery(this.node).addClass("ui-state-active"); - break; - case EGW_AI_DRAG_OUT: - jQuery(this.node).removeClass("ui-state-active"); - break; - } - }; - } - getAOI() { - return this.aoi; - } -} -//# sourceMappingURL=et2_core_DOMWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_arrayMgr.js b/api/js/etemplate/et2_core_arrayMgr.js deleted file mode 100644 index 3e74237b2f..0000000000 --- a/api/js/etemplate/et2_core_arrayMgr.js +++ /dev/null @@ -1,406 +0,0 @@ -/** - * EGroupware eTemplate2 - JS content array manager - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - et2_core_common; - egw_inheritance; - et2_core_phpExpressionCompiler; -*/ -import { et2_evalBool } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -import { et2_compilePHPExpression } from "./et2_core_phpExpressionCompiler"; -/** - * Manage access to various template customisation arrays passed to etemplate->exec(). - * - * This manages access to content, modifications and readonlys arrays - */ -export class et2_arrayMgr { - /** - * Constructor - * - * @memberOf et2_arrayMgr - * @param _data - * @param _parentMgr - */ - constructor(_data = {}, _parentMgr) { - this.splitIds = true; - // Holds information about the current perspective - this.perspectiveData = { - "owner": null, - "key": null, - "row": null - }; - this.readOnly = false; - if (typeof _parentMgr == "undefined") { - _parentMgr = null; - } - // Copy the parent manager which is needed to access relative data when - // being in a relative perspective of the manager - this._parentMgr = _parentMgr; - // Hold a reference to the data - if (typeof _data == "undefined" || !_data) { - egw.debug("log", "No data passed to content array manager. Probably a mismatch between template namespaces and data."); - _data = {}; - } - // Expand sub-arrays that have been shmushed together, so further perspectives work - // Shmushed keys look like: ${row}[info_cat] - // Expanded: ${row}: Object{info_cat: ..value} - if (this.splitIds) { - // For each index, we need a key: {..} sub array - for (let key in _data) { - // Split up indexes - const indexes = key.replace(/[/g, "[").split('['); - // Put data in the proper place - if (indexes.length > 1) { - const value = _data[key]; - let target = _data; - for (let i = 0; i < indexes.length; i++) { - indexes[i] = indexes[i].replace(/]/g, '').replace(']', ''); - if (typeof target[indexes[i]] == "undefined" || target[indexes[i]] === null) { - target[indexes[i]] = i == indexes.length - 1 ? value : {}; - } - target = target[indexes[i]]; - } - delete _data[key]; - } - } - } - this.data = _data; - } - /** - * Returns the root content array manager object - */ - getRoot() { - if (this._parentMgr != null) { - return this._parentMgr.getRoot(); - } - return this; - } - getParentMgr() { - return this._parentMgr; - } - getPerspectiveData() { - return this.perspectiveData; - } - setPerspectiveData(new_perspective) { - this.perspectiveData = new_perspective; - } - setRow(new_row) { - this.perspectiveData.row = new_row; - } - /** - * Explodes compound keys (eg IDs) into a list of namespaces - * This uses no internal values, just expands - * - * eg: - * a[b][c] => [a,b,c] - * col_filter[tr_tracker] => [col_filter, tr_tracker] - * - * @param {string} _key - * - * @return {string[]} - */ - explodeKey(_key) { - if (!_key || typeof _key == 'string' && _key.trim() === "") - return []; - // Parse the given key by removing the "]"-chars and splitting at "[" - let indexes = [_key]; - if (typeof _key === "string") { - _key = _key.replace(/[/g, "[").replace(/]/g, "]"); - indexes = _key.split('['); - } - if (indexes.length > 1) { - indexes = [indexes.shift(), indexes.join('[')]; - indexes[1] = indexes[1].substring(0, indexes[1].length - 1); - const children = indexes[1].split(']['); - if (children.length) { - indexes = jQuery.merge([indexes[0]], children); - } - } - return indexes; - } - /** - * Returns the path to this content array manager perspective as an array - * containing the key values - * - * @param _path is used internally, do not supply it manually. - */ - getPath(_path) { - if (typeof _path == "undefined") { - _path = []; - } - if (this.perspectiveData.key != null) { - // prepend components of this.perspectiveData.key to path, can be more then one eg. "nm[rows]" - _path = this.perspectiveData.key.replace(/]/g, '').split('[').concat(_path); - } - if (this._parentMgr != null) { - _path = this._parentMgr.getPath(_path); - } - return _path; - } - /** - * Get array entry is the equivalent to the boetemplate get_array function. - * It returns a reference to the (sub) array with the given key. This also works - * for keys using the ETemplate referencing scheme like a[b][c] - * - * @param _key is the string index, may contain sub-indices like a[b] - * @param _referenceInto if true none-existing sub-arrays/-indices get created - * to be returned as reference, else false is returned. Defaults to false - * @param _skipEmpty returns null if _key is not present in this content array. - * Defaults to false. - */ - getEntry(_key, _referenceInto, _skipEmpty) { - if (typeof _referenceInto == "undefined") { - _referenceInto = false; - } - if (typeof _skipEmpty == "undefined") { - _skipEmpty = false; - } - // Parse the given key by removing the "]"-chars and splitting at "[" - const indexes = this.explodeKey(_key); - if (indexes.length == 0 && _skipEmpty) - return null; - let entry = this.data; - for (let i = 0; i < indexes.length; i++) { - // Abort if the current entry is not an object (associative array) and - // we should descend further into it. - const isObject = typeof entry === 'object'; - if (!isObject && !_referenceInto || entry == null || jQuery.isEmptyObject(entry)) { - return null; - } - // Check whether the entry actually exists - const idx = indexes[i]; - if (_skipEmpty && (!isObject || typeof entry[idx] == "undefined")) { - return null; - } - entry = entry[idx]; - } - return entry; - } - /** - * Equivalent to the boetemplate::expand_name function. - * - * Expands variables inside the given identifier to their values inside the - * content array. - * - * @param {string} _ident Key used to reference into managed array - * @return {*} - */ - expandName(_ident) { - // Check whether the identifier refers to an index in the content array - const is_index_in_content = _ident.charAt(0) == '@'; - // Check whether "$" occurs in the given identifier - const pos_var = _ident.indexOf('$'); - if (pos_var >= 0 && (this.perspectiveData.row != null || !_ident.match(/\$\{?row\}?/)) - // Avoid messing with regex in validators - && pos_var !== _ident.indexOf("$/")) { - // Get the content array for the current row - const row = typeof this.perspectiveData.row == 'number' ? this.perspectiveData.row : ''; - const row_cont = this.data[row] || {}; - // $cont is NOT root but current name-space in old eTemplate - const cont = this.data; //getRoot().data; - const _cont = this.data; // according to a grep only used in ImportExport just twice - // Check whether the expression has already been compiled - if not, - // try to compile it first. If an error occurs, the identifier - // function is set to null - if (typeof et2_arrayMgr.compiledExpressions[_ident] == "undefined") { - try { - if (this.perspectiveData.row == null) { - // No row, compile for only top level content - // @ts-ignore - et2_arrayMgr.compiledExpressions[_ident] = et2_compilePHPExpression(_ident, ["cont", "_cont"]); - } - else { - // @ts-ignore - et2_arrayMgr.compiledExpressions[_ident] = et2_compilePHPExpression(_ident, ["row", "cont", "row_cont", "_cont"]); - } - } - catch (e) { - et2_arrayMgr.compiledExpressions[_ident] = null; - egw.debug("error", "Error while compiling PHP->JS ", e); - } - } - // Execute the previously compiled expression, if it is not "null" - // because compilation failed. The parameters have to be in the same - // order as defined during compilation. - if (et2_arrayMgr.compiledExpressions[_ident]) { - try { - if (this.perspectiveData.row == null) { - // No row, exec with only top level content - _ident = et2_arrayMgr.compiledExpressions[_ident](cont, _cont); - } - else { - _ident = et2_arrayMgr.compiledExpressions[_ident](row, cont, row_cont, _cont); - } - } - catch (e) { - // only log error, as they are no real errors but missing data - egw.debug("log", typeof e == 'object' ? e.message : e); - _ident = null; - } - } - } - if (is_index_in_content && _ident) { - // If an additional "@" is specified, this means that we have to return - // the entry from the root element - if (_ident.charAt(1) == '@') { - return this.getRoot().getEntry(_ident.substr(2)); - } - else { - return this.getEntry(_ident.substr(1)); - } - } - return _ident; - } - parseBoolExpression(_expression) { - // If the first char of the expression is a '!' this means, that the value - // is to be negated. - if (_expression.charAt(0) == '!') { - return !this.parseBoolExpression(_expression.substr(1)); - } - // Split the expression at a possible "=" - const parts = _expression.split('='); - // Expand the first value - let val = this.expandName(parts[0]); - val = (typeof val == "undefined" || val === null) ? '' : '' + val; - // If a second expression existed, test that one - if (typeof parts[1] != "undefined") { - // Expand the second value - const checkVal = '' + this.expandName(parts[1]); - // Values starting with / are treated as regular expression. It is - // checked whether the first value matches the regular expression - if (checkVal.charAt(0) == '/') { - return (new RegExp(checkVal.substr(1, checkVal.length - 2))) - .test(val); - } - // Otherwise check for simple equality - return val == checkVal; - } - return et2_evalBool(val); - } - /** - * ? - * - * @param {object} _owner owner object - * @param {(string|null|object)} _root string with key, null for whole data or object with data - * @param {number?} _row key for into the _root for the desired row - */ - openPerspective(_owner, _root, _row) { - // Get the root node - let root = typeof _root == "string" ? this.data[_root] : - (_root == null ? this.data : _root); - if (typeof root == "undefined" && typeof _root == "string") - root = this.getEntry(_root); - // Create a new content array manager with the given root - const constructor = this.readOnly ? et2_readonlysArrayMgr : et2_arrayMgr; - const mgr = new constructor(root, this); - // Set the owner - mgr.perspectiveData.owner = _owner; - // Set the root key - if (typeof _root == "string") { - mgr.perspectiveData.key = _root; - } - // Set _row parameter - if (typeof _row != "undefined") { - mgr.perspectiveData.row = _row; - } - return mgr; - } -} -et2_arrayMgr.compiledExpressions = {}; -/** - * @augments et2_arrayMgr - */ -export class et2_readonlysArrayMgr extends et2_arrayMgr { - constructor() { - super(...arguments); - this.readOnly = true; - } - /** - * Find out if the given ID is readonly, according to the array data - * - * @memberOf et2_readonlysArrayMgr - * @param _id - * @param _attr - * @param _parent - * @returns - */ - isReadOnly(_id, _attr, _parent) { - let entry = null; - if (_id != null) { - if (_id.indexOf('$') >= 0 || _id.indexOf('@') >= 0) { - _id = this.expandName(_id); - } - // readonlys was not namespaced in old eTemplate, therefore if we dont find data - // under current namespace, we look into parent - // (if there is anything namespaced, we will NOT look for parent!) - let mgr = this; - while (mgr.getParentMgr() && jQuery.isEmptyObject(mgr.data)) { - mgr = mgr.getParentMgr(); - } - entry = mgr.getEntry(_id); - } - // Let the array entry override the read only attribute entry - if (typeof entry != "undefined" && !(typeof entry === 'object')) { - return entry; - } - // If the attribute is set, return that - if (typeof _attr != "undefined" && _attr !== null) { - // Accept 'editable', but otherwise boolean - return this.expandName(_attr) === 'editable' ? 'editable' : et2_evalBool(_attr); - } - // Otherwise take into accounf whether the parent is readonly - if (typeof _parent != "undefined" && _parent) { - return true; - } - // Otherwise return the default value - entry = this.getEntry("__ALL__"); - return entry !== null && (typeof entry != "undefined"); - } - /** - * Override parent to handle cont and row_cont. - * - * Normally these should refer to the readonlys data, but that's not - * useful, so we use the owner inside perspective data to expand using content. - * - * @param {string} ident Key for searching into the array. - * @returns {*} - */ - expandName(ident) { - return this.perspectiveData.owner.getArrayMgr('content').expandName(ident); - } -} -/** - * Creates a new set of array managers - * - * @param _owner is the owner object of the array managers - this object (a widget) - * will free the array manager - * @param _mgrs is the original set of array managers, the array managers are - * inside an associative array as recived from et2_widget::getArrayMgrs() - * @param _data is an associative array of new data which will be merged into the - * existing array managers. - * @param _row is the row for which the array managers will be opened. - */ -export function et2_arrayMgrs_expand(_owner, _mgrs, _data, _row) { - // Create a copy of the given _mgrs associative array - let result = {}; - // Merge the given data associative array into the existing array managers - for (let key in _mgrs) { - result[key] = _mgrs[key]; - if (typeof _data[key] != "undefined") { - // Open a perspective for the given data row - let rowData = {}; - rowData[_row] = _data[key]; - result[key] = _mgrs[key].openPerspective(_owner, rowData, _row); - } - } - // Return the resulting managers object - return result; -} -//# sourceMappingURL=et2_core_arrayMgr.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_baseWidget.js b/api/js/etemplate/et2_core_baseWidget.js deleted file mode 100644 index 9d78fe43da..0000000000 --- a/api/js/etemplate/et2_core_baseWidget.js +++ /dev/null @@ -1,416 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Widget base class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -import { et2_DOMWidget } from './et2_core_DOMWidget'; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_register_widget } from "./et2_core_widget"; -import { et2_no_init } from "./et2_core_common"; -import { egwIsMobile } from "../egw_action/egw_action_common.js"; -/** - * Class which manages the DOM node itself. The simpleWidget class is derrived - * from et2_DOMWidget and implements the getDOMNode function. A setDOMNode - * function is provided, which attatches the given node to the DOM if possible. - * - * @augments et2_DOMWidget - */ -export class et2_baseWidget extends et2_DOMWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_baseWidget._attributes, _child || {})); - this.align = 'left'; - this.node = null; - this.statustext = ''; - this._messageDiv = null; - this._tooltipElem = null; - } - destroy() { - super.destroy(); - this.node = null; - this._messageDiv = null; - } - /** - * The setMessage function can be used to attach a small message box to the - * widget. This is e.g. used to display validation errors or success messages - * - * @param _text is the text which should be displayed as a message - * @param _type is an css class which is attached to the message box. - * Currently available are "hint", "success" and "validation_error", defaults - * to "hint" - * @param _floating if true, the object will be in one row with the element, - * defaults to true - * @param _prepend if set, the message is displayed behind the widget node - * instead of before. Defaults to false. - */ - showMessage(_text, _type, _floating, _prepend) { - // Preset the parameters - if (typeof _type == "undefined") { - _type = "hint"; - } - if (typeof _floating == "undefined") { - _floating = true; - } - if (typeof _prepend == "undefined") { - _prepend = false; - } - var surr = this.getSurroundings(); - // Remove the message div from the surroundings before creating a new - // one - this.hideMessage(false, true); - // Create the message div and add it to the "surroundings" manager - this._messageDiv = jQuery(document.createElement("div")) - .addClass("message") - .addClass(_type) - .addClass(_floating ? "floating" : "") - .text(_text.valueOf() + ""); - // Decide whether to prepend or append the div - if (_prepend) { - surr.prependDOMNode(this._messageDiv[0]); - } - else { - surr.appendDOMNode(this._messageDiv[0]); - } - surr.update(); - } - /** - * The hideMessage function can be used to hide a previously shown message. - * - * @param _fade if true, the message div will fade out, otherwise the message - * div is removed immediately. Defaults to true. - * @param _noUpdate is used internally to prevent an update of the surroundings - * manager. - */ - hideMessage(_fade, _noUpdate) { - if (typeof _fade == "undefined") { - _fade = true; - } - if (typeof _noUpdate == "undefined") { - _noUpdate = false; - } - // Remove the message from the surroundings manager and remove the - // reference to it - if (this._messageDiv != null) { - var surr = this.getSurroundings(); - var self = this; - var messageDiv = this._messageDiv; - self._messageDiv = null; - var _done = function () { - surr.removeDOMNode(messageDiv[0]); - // Update the surroundings manager - if (!_noUpdate) { - surr.update(); - } - }; - // Either fade out or directly call the function which removes the div - if (_fade) { - messageDiv.fadeOut("fast", _done); - } - else { - _done(); - } - } - } - detachFromDOM() { - // Detach this node from the tooltip node - if (this._tooltipElem) { - this.egw().tooltipUnbind(this._tooltipElem); - this._tooltipElem = null; - } - // Remove the binding to the click handler - if (this.node) { - jQuery(this.node).unbind("click.et2_baseWidget"); - } - return super.detachFromDOM(); - } - attachToDOM() { - let ret = super.attachToDOM(); - // Add the binding for the click handler - if (this.node) { - jQuery(this.node).bind("click.et2_baseWidget", this, function (e) { - return e.data.click.call(e.data, e, this); - }); - if (typeof this.onclick == 'function') - jQuery(this.node).addClass('et2_clickable'); - } - // Update the statustext - this.set_statustext(this.statustext); - return ret; - } - setDOMNode(_node) { - if (_node != this.node) { - // Deatch the old node from the DOM - this.detachFromDOM(); - // Set the new DOM-Node - this.node = _node; - // Attatch the DOM-Node to the tree - return this.attachToDOM(); - } - return false; - } - getDOMNode(_sender) { - return this.node; - } - getTooltipElement() { - return this.getDOMNode(this); - } - /** - * Click handler calling custom handler set via onclick attribute to this.onclick - * - * @param _ev - * @returns - */ - click(_ev) { - if (typeof this.onclick == 'function') { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.splice(1, 0, this); - return this.onclick.apply(this, args); - } - return true; - } - set_statustext(_value) { - // Tooltip should not be shown in mobile view - if (egwIsMobile()) - return; - // Don't execute the code below, if no tooltip will be attached/detached - if (_value == "" && !this._tooltipElem) { - return; - } - this.statustext = _value; - //Get the domnode the tooltip should be attached to - var elem = jQuery(this.getTooltipElement()); - if (elem) { - // Make readable by screenreader - elem.attr("aria-description", this.statustext); - //If a tooltip is already attached to the element, remove it first - if (this._tooltipElem) { - this.egw().tooltipUnbind(this._tooltipElem); - this._tooltipElem = null; - } - if (_value && _value != '') { - this.egw().tooltipBind(elem, _value, this.options.statustext_html); - this._tooltipElem = elem; - } - } - } - set_align(_value) { - this.align = _value; - } - get_align() { - return this.align; - } -} -et2_baseWidget._attributes = { - "statustext": { - "name": "Tooltip", - "type": "string", - "description": "Tooltip which is shown for this element", - "translate": true - }, - "statustext_html": { - "name": "Tooltip is html", - "type": "boolean", - "description": "Flag to allow html content in tooltip", - "default": false - }, - "align": { - "name": "Align", - "type": "string", - "default": "left", - "description": "Position of this element in the parent hbox" - }, - "onclick": { - "name": "onclick", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the element is clicked." - } -}; -/** - * Simple container object - * - * There is no tag to put this in a template. By convention we only make one of these per etemplate, - * and it's the top level object. - */ -export class et2_container extends et2_baseWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_container._attributes, _child || {})); - this.setDOMNode(document.createElement("div")); - } - /** - * The destroy function destroys all children of the widget, removes itself - * from the parents children list. - * Overriden to not try to remove self from parent, as that's not possible. - */ - destroy() { - // Call the destructor of all children - for (var i = this._children.length - 1; i >= 0; i--) { - this._children[i].destroy(); - } - // Free the array managers if they belong to this widget - for (var key in this._mgrs) { - if (this._mgrs[key] && this._mgrs[key].owner == this) { - this._mgrs[key].destroy(); - } - } - } - /** - * Searches for a DOM widget by id in the tree, descending into the child levels. - * - * @param _id is the id you're searching for - */ - getDOMWidgetById(_id) { - let widget = this.getWidgetById(_id); - if (widget && widget.instanceOf(et2_DOMWidget)) { - return widget; - } - return null; - } - /** - * Searches for a Value widget by id in the tree, descending into the child levels. - * - * @param _id is the id you're searching for - */ - getInputWidgetById(_id) { - let widget = this.getWidgetById(_id); - // instead of checking widget to be instance of valueWidget (which would create a circular dependency) - // we check for the interface/methods of valueWidget - if (widget && typeof widget.get_value === 'function' && typeof widget.set_value === 'function') { - return widget; - } - return null; - } - /** - * Set the value for a child widget, specified by the given ID - * - * @param id string The ID you're searching for - * @param value Value for the widget - * - * @return Returns the result of widget's set_value(), though this is usually undefined - * - * @throws Error If the widget cannot be found or it does not have a set_value() function - */ - setValueById(id, value) { - let widget = this.getWidgetById(id); - if (!widget) - throw 'Could not find widget ' + id; - // Don't care about what class it is, just that it has the function - // @ts-ignore - if (typeof widget.set_value !== 'function') { - throw 'Widget ' + id + ' does not have a set_value() function'; - } - // @ts-ignore - return widget.set_value(value); - } - /** - * Get the current value of a child widget, specified by the given ID - * - * This is the current value of the widget, which may be different from the original value given in content - * - * @param id string The ID you're searching for - * @throws Error If the widget cannot be found or it does not have a set_value() function - */ - getValueById(id) { - let widget = this.getWidgetById(id); - if (!widget) - throw 'Could not find widget ' + id; - // Don't care about what class it is, just that it has the function - // @ts-ignore - if (typeof widget.get_value !== 'function') { - throw 'Widget ' + id + ' does not have a get_value() function'; - } - // @ts-ignore - return widget.get_value(); - } - /** - * Set the value for a child widget, specified by the given ID - * - * @param id string The ID you're searching for - * @throws Error If the widget cannot be found or it does not have a set_value() function - */ - setDisabledById(id, value) { - let widget = this.getWidgetById(id); - if (!widget) - throw 'Could not find widget ' + id; - // Don't care about what class it is, just that it has the function - // @ts-ignore - if (typeof widget.set_disabled !== 'function') { - throw 'Widget ' + id + ' does not have a set_disabled() function'; - } - // @ts-ignore - return widget.set_disabled(value); - } -} -// Register widget for attributes, but not for any xml tags -et2_register_widget(et2_container, []); -/** - * Container object for not-yet supported widgets - * - * @augments et2_baseWidget - */ -export class et2_placeholder extends et2_baseWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_placeholder._attributes, _child || {})); - this.visible = false; - this.attrNodes = {}; - // Create the placeholder div - this.placeDiv = jQuery(document.createElement("span")) - .addClass("et2_placeholder"); - var headerNode = jQuery(document.createElement("span")) - .text(this.getType() || "") - .addClass("et2_caption") - .appendTo(this.placeDiv); - var attrsCntr = jQuery(document.createElement("span")) - .appendTo(this.placeDiv) - .hide(); - headerNode.click(this, function (e) { - e.data.visible = !e.data.visible; - if (e.data.visible) { - attrsCntr.show(); - } - else { - attrsCntr.hide(); - } - }); - for (var key in this.options) { - if (typeof this.options[key] != "undefined") { - if (typeof this.attrNodes[key] == "undefined") { - this.attrNodes[key] = jQuery(document.createElement("span")) - .addClass("et2_attr"); - attrsCntr.append(this.attrNodes[key]); - } - this.attrNodes[key].text(key + "=" + this.options[key]); - } - } - this.setDOMNode(this.placeDiv[0]); - } - getDetachedAttributes(_attrs) { - _attrs.push("value"); - } - getDetachedNodes() { - return [this.placeDiv[0]]; - } - setDetachedAttributes(_nodes, _values) { - this.placeDiv = jQuery(_nodes[0]); - } -} -// Register widget, but no tags -et2_register_widget(et2_placeholder, ['placeholder', 'placeholder_ro']); -//# sourceMappingURL=et2_core_baseWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_common.js b/api/js/etemplate/et2_core_common.js deleted file mode 100644 index a94f363b7a..0000000000 --- a/api/js/etemplate/et2_core_common.js +++ /dev/null @@ -1,582 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Widget base class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -import { egw } from "../jsapi/egw_global"; -/** - * IE Fix for array.indexOf - */ -if (typeof Array.prototype.indexOf == "undefined") { - Array.prototype.indexOf = function (_elem) { - for (var i = 0; i < this.length; i++) { - if (this[i] === _elem) - return i; - } - return -1; - }; -} -/** - * Array with all types supported by the et2_checkType function. - */ -export var et2_validTypes = ["boolean", "string", "rawstring", "html", "float", "integer", "any", "js", "dimension"]; -/** - * Object whith default values for the above types. Do not specify array or - * objects inside the et2_typeDefaults object, as this instance will be shared - * between all users of it. - */ -export var et2_typeDefaults = { - "boolean": false, - "string": "", - "rawstring": "", - "html": "", - "js": null, - "float": 0.0, - "integer": 0, - "any": null, - "dimension": "auto" -}; -export function et2_evalBool(_val) { - if (typeof _val == "string") { - if (_val == "false" || _val == "0") { - return false; - } - } - return _val ? true : false; -} -/** - * Concat et2 name together, eg. et2_concat("namespace","test[something]") == "namespace[test][something]" - * @param variable number of arguments to contact - * @returns string - */ -export function et2_form_name(_cname, _name) { - var parts = []; - for (var i = 0; i < arguments.length; ++i) { - var name = arguments[i]; - if (typeof name == 'string' && name.length > 0) // et2_namespace("","test") === "test" === et2_namespace(null,"test") - { - parts = parts.concat(name.replace(/]/g, '').split('[')); - } - } - var name = parts.shift(); - return parts.length ? name + '[' + parts.join('][') + ']' : name; -} -/** - * Checks whether the given value is of the given type. Strings are converted - * into the corresponding type. The (converted) value is returned. All supported - * types are listed in the et2_validTypes array. - * - * @param mixed _val value - * @param string _type a valid type eg. "string" or "js" - * @param string _attr attribute name - * @param object _widget - */ -export function et2_checkType(_val, _type, _attr, _widget) { - if (typeof _attr == "undefined") { - _attr = null; - } - function _err() { - var res = et2_typeDefaults[_type]; - if (typeof _val != "undefined" && _val) { - egw.debug("warn", "Widget %o: '" + _val + "' was not of specified _type '" + - _type + (_attr != null ? "' for attribute '" + _attr + "' " : "") + - "and is now '" + res + "'", _widget); - } - return res; - } - // If the type is "any" simply return the value again - if (_type == "any") { - return _val; - } - // we dont check default-value any further, that also fixes type="js" does NOT accept null, - // which happens on expanded values - if (_val === et2_typeDefaults[_type]) { - return _val; - } - // If the type is boolean, check whether the given value is exactly true or - // false. Otherwise check whether the value is the string "true" or "false". - if (_type == "boolean") { - if (_val === true || _val === false) { - return _val; - } - if (typeof _val == "string") { - var lcv = _val.toLowerCase(); - if (lcv === "true" || lcv === "false" || lcv === "") { - return _val === "true"; - } - if (lcv === "0" || lcv === "1") { - return _val === "1"; - } - } - else if (typeof _val == "number") { - return _val != 0; - } - return _err(); - } - // Check whether the given value is of the type "string" - if (_type == "string" || _type == "html" || _type == "rawstring") { - if (typeof _val == "number") // as php is a bit vague here, silently convert to a string - { - return _val.toString(); - } - if (typeof _val == "string") { - return _type == "string" ? html_entity_decode(_val) : _val; - } - // Handle some less common possibilities - // Maybe a split on an empty string - if (typeof _val == "object" && jQuery.isEmptyObject(_val)) - return ""; - return _err(); - } - // Check whether the value is already a number, otherwise try to convert it - // to one. - if (_type == "float") { - if (typeof _val == "number") { - return _val; - } - if (!isNaN(_val)) { - return parseFloat(_val); - } - return _err(); - } - // Check whether the value is an integer by comparing the result of - // parseInt(_val) to the value itself. - if (_type == "integer") { - if (parseInt(_val) == _val) { - return parseInt(_val); - } - return _err(); - } - // Parse the given dimension value - if (_type == "dimension") { - // Case 1: The value is "auto" - if (_val == "auto") { - return _val; - } - // Case 2: The value is simply a number, attach "px" - if (!isNaN(_val)) { - return parseFloat(_val) + "px"; - } - // Case 3: The value is already a valid css pixel value or a percentage - if (typeof _val == "string" && - ((_val.indexOf("px") == _val.length - 2 && !isNaN(_val.split("px")[0])) || - (_val.indexOf("%") == _val.length - 1 && !isNaN(_val.split("%")[0])))) { - return _val; - } - return _err(); - } - // Javascript - if (_type == "js") { - if (typeof _val == "function" || typeof _val == "undefined") { - return _val; - } - if (_val) - _val = _val.replace(/window\.close\(\)/g, 'egw(window).close()'); - // Check to see if it's a string in app.appname.function format, and wrap it in - // a closure to make sure context is preserved - if (typeof _val == "string" && _val.substr(0, 4) == "app." && app) { - var parts = _val.split('.'); - var func = parts.pop(); - var parent = window; - for (var i = 0; i < parts.length && typeof parent[parts[i]] != 'undefined'; ++i) { - parent = parent[parts[i]]; - } - if (typeof parent[func] == 'function') { - try { - return jQuery.proxy(parent[func], parent); - } - catch (e) { - egw.debug('error', 'Function', _val); - return _err(); - } - } - } - if (!_val || typeof _val == "string") { - return _val; // get compiled later in widgets own initAttributes, as widget is not yet initialised - } - } - // We should never come here - throw ("Invalid type identifier '" + _attr + "': '" + _type + "'"); -} -/** - * If et2_no_init is set as default value, the initAttributes function will not - * try to initialize the attribute with the default value. - */ -export const et2_no_init = new Object(); -/** - * Validates the given attribute with the given id. The validation checks for - * the existance of a human name, a description, a type and a default value. - * If the human name defaults to the given id, the description defaults to an - * empty string, the type defaults to any and the default to the corresponding - * type default. - */ -export function et2_validateAttrib(_id, _attrib) { - // Default ignore to false. - if (typeof _attrib["ignore"] == "undefined") { - _attrib["ignore"] = false; - } - // Break if "ignore" is set to true. - if (_attrib.ignore) { - return; - } - if (typeof _attrib["name"] == "undefined") { - _attrib["name"] = _id; - egw.debug("log", "Human name ('name'-Field) for attribute '" + - _id + "' has not been supplied, set to '" + _id + "'"); - } - if (typeof _attrib["description"] == "undefined") { - _attrib["description"] = ""; - egw.debug("log", "Description for attribute '" + - _id + "' has not been supplied"); - } - if (typeof _attrib["type"] == "undefined") { - _attrib["type"] = "any"; - } - else { - if (et2_validTypes.indexOf(_attrib["type"]) < 0) { - egw.debug("error", "Invalid type '" + _attrib["type"] + "' for attribute '" + _id + - "' supplied. Valid types are ", et2_validTypes); - } - } - // Set the defaults - if (typeof _attrib["default"] == "undefined") { - _attrib["default"] = et2_typeDefaults[_attrib["type"]]; - } -} -/** - * Equivalent to the PHP array_values function - */ -export function et2_arrayValues(_arr) { - var result = []; - for (var key in _arr) { - // @ts-ignore we check key is an integer - if (parseInt(key) == key) { - result.push(_arr[key]); - } - } - return result; -} -/** - * Equivalent to the PHP array_keys function - */ -export function et2_arrayKeys(_arr) { - var result = []; - for (var key in _arr) { - result.push(key); - } - return result; -} -export function et2_arrayIntKeys(_arr) { - var result = []; - for (var key in _arr) { - result.push(parseInt(key)); - } - return result; -} -/** - * Equivalent to the PHP substr function, partly take from phpjs, licensed under - * the GPL. - */ -export function et2_substr(str, start, len) { - var end = str.length; - if (start < 0) { - start += end; - } - end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); - return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); -} -/** - * Split a $delimiter-separated options string, which can contain parts with - * delimiters enclosed in $enclosure. Ported from class.boetemplate.inc.php - * - * Examples: - * - et2_csvSplit('"1,2,3",2,3') === array('1,2,3','2','3') - * - et2_csvSplit('1,2,3',2) === array('1','2,3') - * - et2_csvSplit('"1,2,3",2,3',2) === array('1,2,3','2,3') - * - et2_csvSplit('"a""b,c",d') === array('a"b,c','d') // to escape enclosures double them! - * - * @param string _str - * @param int _num=null in how many parts to split maximal, parts over this - * number end up (unseparated) in the last part - * @param string _delimiter=',' - * @param string _enclosure='"' - * @return array - */ -export function et2_csvSplit(_str, _num, _delimiter, _enclosure) { - // Default the parameters - if (typeof _str == "undefined" || _str == null) { - _str = ""; - } - if (typeof _num == "undefined") { - _num = null; - } - if (typeof _delimiter == "undefined") { - _delimiter = ","; - } - if (typeof _enclosure == "undefined") { - _enclosure = '"'; - } - // If the _enclosure string does not occur in the string, simply use the - // split function - if (_str.indexOf(_enclosure) == -1) { - return _num === null ? _str.split(_delimiter) : - _str.split(_delimiter, _num); - } - // Split the string at the delimiter and join it again, when a enclosure is - // found at the beginning/end of a part - var parts = _str.split(_delimiter); - for (var n = 0; typeof parts[n] != "undefined"; n++) { - var part = parts[n]; - if (part.charAt(0) === _enclosure) { - var m = n; - while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) { - parts[n] += _delimiter + parts[++m]; - delete (parts[m]); - } - parts[n] = et2_substr(parts[n].replace(new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1, -1); - n = m; - } - } - // Rebuild the array index - parts = et2_arrayValues(parts); - // Limit the parts to the given number - if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) { - parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); - parts = parts.slice(0, _num); - } - return parts; -} -/** - * Parses the given string and returns an array marking parts which are URLs - */ -export function et2_activateLinks(_content) { - var _match = false; - var arr = []; - function _splitPush(_matches, _proc) { - if (_matches) { - // We had a match - _match = true; - // Replace "undefined" with "" - for (var i = 1; i < _matches.length; i++) { - if (typeof _matches[i] == "undefined") { - _matches[i] = ""; - } - } - // Split the content string at the given position(s) - // but we only handle the first occurence - var splitted = _content.split(_matches[0]); - // Push the not-matched part - var left = splitted.shift(); - if (left) { - // activate the links of the left string - arr = arr.concat(et2_activateLinks(left)); - } - // Call the callback function which converts the matches into an object - // and appends it to the string - _proc(_matches); - // Set the new working string to the right part - _content = splitted.join(_matches[0]); - } - } - var mail_regExp = /(mailto:)?([a-z0-9._-]+)@([a-z0-9_-]+)\.([a-z0-9._-]+)/i; - // First match things beginning with http:// (or other protocols) - var protocol = '(http:\\/\\/|(ftp:\\/\\/|https:\\/\\/))'; // only http:// gets removed, other protocolls are shown - var domain = '([\\w-]+\\.[\\w-.]+)'; - var subdir = '([\\w\\-\\.,@?^=%&;:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; - var http_regExp = new RegExp(protocol + domain + subdir, 'i'); - // Now match things beginning with www. - var domain = 'www(\\.[\\w-.]+)'; - var subdir = '([\\w\\-\\.,@?^=%&:\\/~\\+#]*[\\w\\-\\@?^=%&\\/~\\+#])?'; - var www_regExp = new RegExp(domain + subdir, 'i'); - do { - _match = false; - // Abort if the remaining length of _content is smaller than 20 for - // performance reasons - if (!_content) { - break; - } - // No need make emailaddress spam-save, as it gets dynamically created - _splitPush(_content.match(mail_regExp), function (_matches) { - arr.push({ - "href": (_matches[1] ? '' : 'mailto:') + _matches[0], - "text": _matches[2] + "@" + _matches[3] + "." + _matches[4] - }); - }); - // Create hrefs for links starting with "http://" - _splitPush(_content.match(http_regExp), function (_matches) { - arr.push({ - "href": _matches[0], - "text": _matches[2] + _matches[3] + _matches[4] - }); - }); - // Create hrefs for links starting with "www." - _splitPush(_content.match(www_regExp), function (_matches) { - arr.push({ - "href": "http://" + _matches[0], - "text": _matches[0] - }); - }); - } while (_match); - arr.push(_content); - return arr; -} -/** - * Inserts the structure generated by et2_activateLinks into the given DOM-Node - */ -export function et2_insertLinkText(_text, _node, _target) { - if (!_node) { - egw.debug("warn", "et2_insertLinkText called without node", _text, _node, _target); - return; - } - // Clear the node - for (var i = _node.childNodes.length - 1; i >= 0; i--) { - _node.removeChild(_node.childNodes[i]); - } - for (var i = 0; i < _text.length; i++) { - var s = _text[i]; - if (typeof s == "string" || typeof s == "number") { - // Include line breaks - var lines = typeof s !== "number" && s.split ? s.split('\n') : [s + ""]; - // Insert the lines - for (var j = 0; j < lines.length; j++) { - _node.appendChild(document.createTextNode(lines[j])); - if (j < lines.length - 1) { - _node.appendChild(document.createElement("br")); - } - } - } - else if (s === null || s === void 0 ? void 0 : s.text) { - if (!s.href) { - egw.debug("warn", "et2_activateLinks gave bad data", s, _node, _target); - s.href = ""; - } - var a = jQuery(document.createElement("a")) - .attr("href", s.href) - .text(s.text); - if (typeof _target != "undefined" && _target && _target != "_self" && s.href.substr(0, 7) != "mailto:") { - a.attr("target", _target); - } - // open mailto links depending on preferences in mail app - if (s.href.substr(0, 7) == "mailto:" && - (egw.user('apps').mail || egw.user('apps').felamimail) && - egw.preference('force_mailto', 'addressbook') != '1') { - a.click(function (event) { - egw.open_link(this.href); - return false; - }); - } - a.appendTo(_node); - } - } -} -/** - * Creates a copy of the given object (non recursive) - */ -export function et2_cloneObject(_obj) { - var result = {}; - for (var key in _obj) { - result[key] = _obj[key]; - } - return result; -} -/** - * Returns true if the given array of nodes or their children contains the given - * child node. - */ -export function et2_hasChild(_nodes, _child) { - for (var i = 0; i < _nodes.length; i++) { - if (_nodes[i] == _child) { - return true; - } - else if (_nodes[i].childNodes) { - var res = et2_hasChild(_nodes[i].childNodes, _child); - if (res) { - return true; - } - } - } - return false; -} -/** - * Functions to work with ranges and range intersection (used in the dataview) - */ -/** - * Common functions used in most view classes - */ -/** - * Returns an "range" object with the given top position and height - */ -export function et2_range(_top, _height) { - return { - "top": _top, - "bottom": _top + _height - }; -} -/** - * Returns an "area" object with the given top- and bottom position - */ -export function et2_bounds(_top, _bottom) { - return { - "top": _top, - "bottom": _bottom - }; -} -/** - * Returns whether two range objects intersect each other - */ -export function et2_rangeIntersect(_ar1, _ar2) { - return !(_ar1.bottom < _ar2.top || _ar1.top > _ar2.bottom); -} -/** - * Returns whether two ranges intersect (result = 0) or their relative position - * to each other (used to do a binary search inside a list of sorted range objects). - */ -export function et2_rangeIntersectDir(_ar1, _ar2) { - if (_ar1.bottom < _ar2.top) { - return -1; - } - if (_ar1.top > _ar2.bottom) { - return 1; - } - return 0; -} -/** - * Returns whether two ranges are equal. - */ -export function et2_rangeEqual(_ar1, _ar2) { - return _ar1.top === _ar2.top && _ar1.bottom === _ar2.bottom; -} -/** - * Substracts _ar2 from _ar1, returns an array of new ranges. - */ -export function et2_rangeSubstract(_ar1, _ar2) { - // Per default return the complete _ar1 range - var res = [_ar1]; - // Check whether there is an intersection between the given ranges - if (et2_rangeIntersect(_ar1, _ar2)) { - res = [et2_bounds(_ar1.top, _ar2.top), - et2_bounds(_ar2.bottom, _ar1.bottom)]; - } - // Remove all zero-length ranges from the result - for (var i = res.length - 1; i >= 0; i--) { - if (res[i].bottom - res[i].top <= 0) { - res.splice(i, 1); - } - } - return res; -} -/** - * Decode html entities so they can be added via .text(_str), eg. html_entity_decode('&') === '&' - * - * @param {string} _str - * @returns {string} - */ -export function html_entity_decode(_str) { - return _str && _str.indexOf('&') != -1 ? jQuery('' + _str + '').text() : _str; -} -//# sourceMappingURL=et2_core_common.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_editableWidget.js b/api/js/etemplate/et2_core_editableWidget.js deleted file mode 100644 index 246550bb67..0000000000 --- a/api/js/etemplate/et2_core_editableWidget.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Widget base class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - et2_core_inputWidget; -*/ -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_no_init } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -/** - * et2_editableWidget derives from et2_inputWidget and adds the ability to start - * readonly, then turn editable on double-click. If we decide to do this with - * more widgets, it should just be merged with et2_inputWidget. - * - * @augments et2_inputWidget - */ -export class et2_editableWidget extends et2_inputWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // 'Editable' really should be boolean for everything else to work - if (_attrs.readonly && typeof _attrs.readonly === 'string') { - _attrs.readonly = true; - var toggle_readonly = _attrs.toggle_readonly; - } - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_editableWidget._attributes, _child || {})); - if (typeof toggle_readonly != 'undefined') - this._toggle_readonly = toggle_readonly; - } - destroy() { - let node = this.getInputNode(); - if (node) { - jQuery(node).off('.et2_editableWidget'); - } - super.destroy(); - } - /** - * Load the validation errors from the server - * - * @param {object} _attrs - */ - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - } - attachToDOM() { - let res = super.attachToDOM(); - let node = this.getDOMNode(); - if (node && this._toggle_readonly) { - jQuery(node) - .off('.et2_editableWidget') - .on("dblclick.et2_editableWidget", this, function (e) { - e.data.dblclick.call(e.data, this); - }) - .addClass('et2_clickable et2_editable'); - } - else { - jQuery(node).addClass('et2_editable_readonly'); - } - return res; - } - detatchFromDOM() { - super.detatchFromDOM(); - } - /** - * Handle double click - * - * Turn widget editable - * - * @param {DOMNode} _node - */ - dblclick(_node) { - // Turn off readonly - this.set_readonly(false); - jQuery('body').on("click.et2_editableWidget", this, function (e) { - // Make sure click comes from body, not a popup - if (jQuery.contains(this, e.target) && e.target.type != 'textarea') { - jQuery(this).off("click.et2_editableWidget"); - e.data.focusout.call(e.data, this); - } - }); - } - /** - * User clicked somewhere else, save and turn back to readonly - * - * @param {DOMNode} _node Body node - * @returns {et2_core_editableWidgetet2_editableWidget.et2_core_editableWidgetAnonym$0@call;getInstanceManager@call;submit} - */ - focusout(_node) { - var value = this.get_value(); - var oldValue = this._oldValue; - // Change back to readonly - this.set_readonly(true); - // No change, do nothing - if (value == oldValue) - return; - // Submit - if (this.options.save_callback) { - var params = [value]; - if (this.options.save_callback_params) { - params = params.concat(this.options.save_callback_params.split(',')); - } - egw.json(this.options.save_callback, params, function () { - }, this, true, this).sendRequest(); - } - else { - this.set_value(value); - return this.getInstanceManager().submit(); - } - } - /** - * Called whenever the template gets submitted. - * If we have a save_callback, we call that before the submit (no check on - * the result) - * - * @param _values contains the values which will be sent to the server. - * Listeners may change these values before they get submitted. - */ - submit(_values) { - if (this.options.readonly) { - // Not currently editing, just continue on - return true; - } - // Change back to readonly - this.set_readonly(true); - var params = [this.get_value()]; - if (this.options.save_callback_params) { - params = params.concat(this.options.save_callback_params.split(',')); - } - if (this.options.save_callback) { - egw.json(this.options.save_callback, params, function () { - }, this, true, this).sendRequest(); - } - return true; - } -} -et2_editableWidget._attributes = { - readonly: { - name: "readonly", - type: "string", - default: false, - description: "If set to 'editable' will start readonly, double clicking will make it editable and clicking out will save", - ignore: true // todo: not sure why this used to be ignored before migration by default but not anymore - }, - toggle_readonly: { - name: "toggle_readonly", - type: "boolean", - default: true, - description: "Double clicking makes widget editable. If off, must be made editable in some other way." - }, - save_callback: { - name: "save_callback", - type: "string", - default: et2_no_init, - description: "Ajax callback to save changed value when readonly is 'editable'. If not provided, a regular submit is done." - }, - save_callback_params: { - name: "readonly", - type: "string", - default: et2_no_init, - description: "Additional parameters passed to save_callback" - }, - editable_height: { - name: "Editable height", - description: "Set height for widget while in edit mode", - type: "string" - } -}; -//# sourceMappingURL=et2_core_editableWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_inheritance.js b/api/js/etemplate/et2_core_inheritance.js deleted file mode 100644 index 461492dfad..0000000000 --- a/api/js/etemplate/et2_core_inheritance.js +++ /dev/null @@ -1,252 +0,0 @@ -/** - * EGroupware eTemplate2 - JS code for implementing inheritance with attributes - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link: https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - et2_core_common; -*/ -import { egw } from "../jsapi/egw_global"; -import { et2_checkType, et2_no_init, et2_validateAttrib } from "./et2_core_common"; -import { et2_implements_registry } from "./et2_core_interfaces"; -// Needed for mixin -export function mix(superclass) { - return new MixinBuilder(superclass); -} -export class MixinBuilder { - constructor(superclass) { - this.superclass = superclass; - } - with(...mixins) { - return mixins.reduce(this.applyMixins, this.superclass); - } - applyMixins(derivedConstructor, baseConstructor) { - Object.getOwnPropertyNames(baseConstructor.prototype) - .forEach(name => { - Object.defineProperty(derivedConstructor.prototype, name, Object. - getOwnPropertyDescriptor(baseConstructor.prototype, name)); - }); - } - copyProperties(target, source) { - for (let key of Reflect.ownKeys(source)) { - if (key !== "constructor" && key !== "prototype" && key !== "name") { - let desc = Object.getOwnPropertyDescriptor(source, key); - Object.defineProperty(target, key, desc); - } - } - } -} -// This one from Typescript docs -export function applyMixins(derivedCtor, constructors) { - constructors.forEach((baseCtor) => { - Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { - Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || - Object.create(null)); - }); - }); -} -/* -Experiments in using mixins to combine et2_widget & LitElement -Note that this "works", in that it mixes the code properly. -It does not work in that the resulting class does not work with et2's inheritance & class checking stuff - -// This one to make TypeScript happy? -interface et2_textbox extends et2_textbox, LitElement {} -// This one to make the inheritance magic happen -applyMixins(et2_textbox, [et2_textbox,LitElement]); -// Make it a real WebComponent -customElements.define("et2-textbox",et2_textbox); - - */ -export class ClassWithInterfaces { - /** - * The implements function can be used to check whether the object - * implements the given interface. - * - * As TypeScript can not (yet) check if an objects implements an interface on runtime, - * we currently implements with each interface a function called 'implements_'+interfacename - * to be able to check here. - * - * @param _iface name of interface to check - */ - implements(_iface_name) { - if (typeof et2_implements_registry[_iface_name] === 'function' && - et2_implements_registry[_iface_name](this)) { - return true; - } - return false; - } - /** - * Check if object is an instance of a class or implements an interface (specified by the interfaces name) - * - * @param _class_or_interfacename class(-name) or string with name of interface - */ - instanceOf(_class_or_interfacename) { - if (typeof _class_or_interfacename === 'string') { - return this.implements(_class_or_interfacename); - } - return this instanceof _class_or_interfacename; - } -} -export class ClassWithAttributes extends ClassWithInterfaces { - /** - * Returns the value of the given attribute. If the property does not - * exist, an error message is issued. - * - * @param {string} _name - * @return {*} - */ - getAttribute(_name) { - if (typeof this.attributes[_name] != "undefined" && - !this.attributes[_name].ignore) { - if (typeof this["get_" + _name] == "function") { - return this["get_" + _name](); - } - else { - return this[_name]; - } - } - else { - egw.debug("error", this, "Attribute '" + _name + "' does not exist!"); - } - } - /** - * The setAttribute function sets the attribute with the given name to - * the given value. _override defines, whether this[_name] will be set, - * if this key already exists. _override defaults to true. A warning - * is issued if the attribute does not exist. - * - * @param {string} _name - * @param {*} _value - * @param {boolean} _override - */ - setAttribute(_name, _value, _override) { - if (typeof this.attributes[_name] != "undefined") { - if (!this.attributes[_name].ignore) { - if (typeof _override == "undefined") { - _override = true; - } - var val = et2_checkType(_value, this.attributes[_name].type, _name, this); - if (typeof this["set_" + _name] == "function") { - this["set_" + _name](val); - } - else if (_override || typeof this[_name] == "undefined") { - this[_name] = val; - } - } - } - else { - egw.debug("warn", this, "Attribute '" + _name + "' does not exist!"); - } - } - /** - * generateAttributeSet sanitizes the given associative array of attributes - * (by passing each entry to "et2_checkType" and checking for existance of - * the attribute) and adds the default values to the associative array. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - static generateAttributeSet(widget, _attrs) { - // Sanity check and validation - for (var key in _attrs) { - if (typeof widget[key] != "undefined") { - if (!widget[key].ignore) { - _attrs[key] = et2_checkType(_attrs[key], widget[key].type, key, this); - } - } - else { - // Key does not exist - delete it and issue a warning - delete (_attrs[key]); - egw.debug("warn", this, "Attribute '" + key + - "' does not exist in " + _attrs.type + "!"); - } - } - // Include default values or already set values for this attribute - for (var key in widget) { - if (typeof _attrs[key] == "undefined") { - var _default = widget[key]["default"]; - if (_default == et2_no_init) { - _default = undefined; - } - _attrs[key] = _default; - } - } - return _attrs; - } - /** - * The initAttributes function sets the attributes to their default - * values. The attributes are not overwritten, which means, that the - * default is only set, if either a setter exists or this[propName] does - * not exist yet. - * - * @param {object} _attrs is the associative array containing the attributes. - */ - initAttributes(_attrs) { - for (var key in _attrs) { - if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { - this.setAttribute(key, _attrs[key], false); - } - } - } - static buildAttributes(class_prototype) { - let class_tree = []; - let attributes = {}; - let n = 0; - do { - n++; - class_tree.push(class_prototype); - class_prototype = Object.getPrototypeOf(class_prototype); - } while (class_prototype !== ClassWithAttributes && n < 50); - for (let i = class_tree.length - 1; i >= 0; i--) { - attributes = ClassWithAttributes.extendAttributes(attributes, class_tree[i]._attributes); - } - return attributes; - } - /** - * Extend current _attributes with the one from the parent class - * - * This gives inheritance from the parent plus the ability to override in the current class. - * - * @param _attributes - * @param _parent - */ - static extendAttributes(_parent, _attributes) { - function _copyMerge(_new, _old) { - var result = {}; - // Copy the new object - if (typeof _new != "undefined") { - for (var key in _new) { - result[key] = _new[key]; - } - } - // Merge the old object - for (var key in _old) { - if (typeof result[key] == "undefined") { - result[key] = _old[key]; - } - } - return result; - } - var attributes = {}; - // Copy the old attributes - for (var key in _attributes) { - attributes[key] = _copyMerge({}, _attributes[key]); - } - // Add the old attributes to the new ones. If the attributes already - // exist, they are merged. - for (var key in _parent) { - var _old = _parent[key]; - attributes[key] = _copyMerge(attributes[key], _old); - } - // Validate the attributes - for (var key in attributes) { - et2_validateAttrib(key, attributes[key]); - } - return attributes; - } -} -//# sourceMappingURL=et2_core_inheritance.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_inputWidget.js b/api/js/etemplate/et2_core_inputWidget.js deleted file mode 100644 index 0b639dd439..0000000000 --- a/api/js/etemplate/et2_core_inputWidget.js +++ /dev/null @@ -1,294 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Widget base class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_valueWidget; -*/ -import { et2_no_init } from "./et2_core_common"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_valueWidget } from './et2_core_valueWidget'; -import { et2_compileLegacyJS } from "./et2_core_legacyJSFunctions"; -/** - * et2_inputWidget derrives from et2_simpleWidget and implements the IInput - * interface. When derriving from this class, call setDOMNode with an input - * DOMNode. - */ -export class et2_inputWidget extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_inputWidget._attributes, _child || {})); - // mark value as not initialised, so set_value can determine if it is necessary to trigger change event - this._oldValue = et2_no_init; - this._labelContainer = null; - } - destroy() { - var node = this.getInputNode(); - if (node) { - jQuery(node).unbind("change.et2_inputWidget"); - jQuery(node).unbind("focus"); - } - super.destroy(); - this._labelContainer = null; - } - /** - * Make sure dirty flag is properly set - */ - doLoadingFinished() { - let result = super.doLoadingFinished(); - this.resetDirty(); - return result; - } - /** - * Load the validation errors from the server - * - * @param {object} _attrs - */ - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - // Check whether an validation error entry exists - if (this.id && this.getArrayMgr("validation_errors")) { - var val = this.getArrayMgr("validation_errors").getEntry(this.id); - if (val) { - _attrs["validation_error"] = val; - } - } - } - attachToDOM() { - var node = this.getInputNode(); - if (node) { - jQuery(node) - .off('.et2_inputWidget') - .bind("change.et2_inputWidget", this, function (e) { - e.data.change.call(e.data, this); - }) - .bind("focus.et2_inputWidget", this, function (e) { - e.data.focus.call(e.data, this); - }); - } - return super.attachToDOM(); - // jQuery(this.getInputNode()).attr("novalidate","novalidate"); // Stop browser from getting involved - // jQuery(this.getInputNode()).validator(); - } - detatchFromDOM() { - // if(this.getInputNode()) { - // jQuery(this.getInputNode()).data("validator").destroy(); - // } - super.detachFromDOM(); - } - change(_node, _widget, _value) { - var messages = []; - var valid = this.isValid(messages); - // Passing false will clear any set messages - this.set_validation_error(valid ? false : messages); - if (valid && this.onchange) { - if (typeof this.onchange == 'function') { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.push(this); - return this.onchange.apply(this, args); - } - else { - return (et2_compileLegacyJS(this.options.onchange, this, _node))(); - } - } - return valid; - } - focus(_node) { - if (typeof this.options.onfocus == 'function') { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.push(this); - return this.options.onfocus.apply(this, args); - } - } - /** - * Set value of widget and trigger for real changes a change event - * - * First initialisation (_oldValue === et2_no_init) is NOT considered a change! - * - * @param {string} _value value to set - */ - set_value(_value) { - var node = this.getInputNode(); - if (node) { - jQuery(node).val(_value); - if (this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) { - jQuery(node).change(); - } - } - this._oldValue = _value; - } - set_id(_value) { - this.id = _value; - this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId + '_' + this.id : _value; - // Set the id of the _input_ node (in contrast to the default - // implementation, which sets the base node) - var node = this.getInputNode(); - if (node) { - // Unique ID to prevent DOM collisions across multiple templates - if (_value != "") { - node.setAttribute("id", this.dom_id); - node.setAttribute("name", _value); - } - else { - node.removeAttribute("id"); - node.removeAttribute("name"); - } - } - } - set_needed(_value) { - var node = this.getInputNode(); - if (node) { - if (_value && !this.options.readonly) { - jQuery(node).attr("required", "required"); - } - else { - node.removeAttribute("required"); - } - } - } - set_validation_error(_value) { - var node = this.getInputNode(); - if (node) { - if (_value === false) { - this.hideMessage(); - jQuery(node).removeClass("invalid"); - } - else { - this.showMessage(_value, "validation_error"); - jQuery(node).addClass("invalid"); - // If on a tab, switch to that tab so user can see it - let widget = this; - while (widget.getParent() && widget.getType() != 'tabbox') { - widget = widget.getParent(); - } - if (widget.getType() == 'tabbox') - widget.activateTab(this); - } - } - } - /** - * Set tab index - * - * @param {number} index - */ - set_tabindex(index) { - jQuery(this.getInputNode()).attr("tabindex", index); - } - getInputNode() { - return this.node; - } - get_value() { - return this.getValue(); - } - getValue() { - var node = this.getInputNode(); - if (node) { - var val = jQuery(node).val(); - return val; - } - return this._oldValue; - } - isDirty() { - let value = this.getValue(); - if (typeof value !== typeof this._oldValue) { - return true; - } - if (this._oldValue === value) { - return false; - } - switch (typeof this._oldValue) { - case "object": - if (typeof this._oldValue.length !== "undefined" && - this._oldValue.length !== value.length) { - return true; - } - for (let key in this._oldValue) { - if (this._oldValue[key] !== value[key]) - return true; - } - return false; - default: - return this._oldValue != value; - } - } - resetDirty() { - this._oldValue = this.getValue(); - } - isValid(messages) { - var ok = true; - // Check for required - if (this.options && this.options.needed && !this.options.readonly && !this.disabled && - (this.getValue() == null || this.getValue().valueOf() == '')) { - messages.push(this.egw().lang('Field must not be empty !!!')); - ok = false; - } - return ok; - } - /** - * Called whenever the template gets submitted. We return false if the widget - * is not valid, which cancels the submission. - * - * @param _values contains the values which will be sent to the server. - * Listeners may change these values before they get submitted. - */ - submit(_values) { - var messages = []; - var valid = this.isValid(messages); - // Passing false will clear any set messages - this.set_validation_error(valid ? false : messages); - return valid; - } -} -et2_inputWidget._attributes = { - "needed": { - "name": "Required", - "default": false, - "type": "boolean", - "description": "If required, the user must enter a value before the form can be submitted" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the value changes." - }, - "onfocus": { - "name": "onfocus", - "type": "js", - "default": et2_no_init, - "description": "JS code which get executed when wiget receives focus." - }, - "validation_error": { - "name": "Validation Error", - "type": "string", - "default": et2_no_init, - "description": "Used internally to store the validation error that came from the server." - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - readonly: { - name: "readonly", - type: "boolean", - "default": false, - description: "Does NOT allow user to enter data, just displays existing data" - } -}; -//# sourceMappingURL=et2_core_inputWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_interfaces.js b/api/js/etemplate/et2_core_interfaces.js deleted file mode 100644 index 3eb84635c5..0000000000 --- a/api/js/etemplate/et2_core_interfaces.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * EGroupware eTemplate2 - File which contains all interfaces - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -export var et2_implements_registry = {}; -/** - * Checks if an object / et2_widget implements given methods - * - * @param obj - * @param methods - */ -export function implements_methods(obj, methods) { - for (let i = 0; i < methods.length; ++i) { - if (typeof obj[methods[i]] !== 'function') { - return false; - } - } - return true; -} -export const et2_IDOMNode = "et2_IDOMNode"; -et2_implements_registry.et2_IDOMNode = function (obj) { - return implements_methods(obj, ["getDOMNode"]); -}; -export const et2_IInputNode = "et2_IInputNode"; -et2_implements_registry.et2_IInputNode = function (obj) { - return implements_methods(obj, ["getInputNode"]); -}; -export const et2_IInput = "et2_IInput"; -et2_implements_registry.et2_IInput = function (obj) { - return implements_methods(obj, ["getValue", "isDirty", "resetDirty", "isValid"]); -}; -export const et2_IResizeable = "et2_IResizeable"; -et2_implements_registry.et2_IResizeable = function (obj) { - return implements_methods(obj, ["resize"]); -}; -export const et2_IAligned = "et2_IAligned"; -et2_implements_registry.et2_IAligned = function (obj) { - return implements_methods(obj, ["get_align"]); -}; -export const et2_ISubmitListener = "et2_ISubmitListener"; -et2_implements_registry.et2_ISubmitListener = function (obj) { - return implements_methods(obj, ["submit"]); -}; -export const et2_IDetachedDOM = "et2_IDetachedDOM"; -et2_implements_registry.et2_IDetachedDOM = function (obj) { - return implements_methods(obj, ["getDetachedAttributes", "getDetachedNodes", "setDetachedAttributes"]); -}; -export const et2_IPrint = "et2_IPrint"; -et2_implements_registry.et2_IPrint = function (obj) { - return implements_methods(obj, ["beforePrint", "afterPrint"]); -}; -export const et2_IExposable = "et2_IExposable"; -et2_implements_registry.et2_IExposable = function (obj) { - return implements_methods(obj, ["getMedia"]); -}; -//# sourceMappingURL=et2_core_interfaces.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_legacyJSFunctions.js b/api/js/etemplate/et2_core_legacyJSFunctions.js deleted file mode 100644 index 7da64ace0d..0000000000 --- a/api/js/etemplate/et2_core_legacyJSFunctions.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * EGroupware eTemplate2 - Execution layer for legacy event code - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-21 - */ -/*egw:uses - et2_interfaces; - et2_core_common; -*/ -import { egw } from "../jsapi/egw_global"; -import { et2_IDOMNode } from "./et2_core_interfaces"; -export function et2_compileLegacyJS(_code, _widget, _context) { - // Replace the javascript pseudo-functions - _code = js_pseudo_funcs(_code, _widget); - // Check whether _code is simply "1" -- if yes replace it accordingly - if (_code === '1') { - _code = 'widget.getInstanceManager().submit(); return false;'; - } - // Check whether some pseudo-variables still reside inside of the code, - // if yes, replace them. - if (_code.indexOf("$") >= 0 || _code.indexOf("@") >= 0) { - // Get the content array manager for the widget - var mgr = _widget.getArrayMgr("content"); - if (mgr) { - _code = mgr.expandName(_code); - } - } - // Context is the context in which the function will run. Set context to - // null as a default, so that it's possible to find bugs where "this" is - // accessed in the code, but not properly set. - var context = _context ? _context : null; - // Check whether the given widget implements the "et2_IDOMNode" - // interface - if (!context && _widget.implements(et2_IDOMNode)) { - context = _widget.getDOMNode(); - } - // Check to see if it's referring to an existing function with no arguments specified. - // If so, bind context & use it directly - if (_code.indexOf("(") === -1) { - var parts = _code.split("."); - var existing_func = parts.pop(); - var parent = _widget.egw().window; - for (var i = 0; i < parts.length; ++i) { - if (typeof parent[parts[i]] !== "undefined") { - parent = parent[parts[i]]; - } - // Nope - else { - break; - } - } - if (typeof parent[existing_func] === "function") { - return parent[existing_func]; - } - } - // Generate the function itself, if it fails, log the error message and - // return a function which always returns false - try { - // Code is app.appname.function, add the arguments so it can be executed - if (typeof _code == 'string' && _code.indexOf('app') == 0 && _code.split('.').length >= 3 && _code.indexOf('(') == -1) { - const parts = _code.split('.'); - const app = _widget.getInstanceManager().app_obj; - // check if we need to load the object - if (parts.length === 3 && typeof app[parts[1]] === 'undefined') { - return function (ev, widget) { - return egw.applyFunc(_code, [ev, widget]); - }; - } - // Code is app.appname.function, add the arguments so it can be executed - _code += '(ev,widget)'; - } - // use app object from etemplate2, which might be private and not just window.app - _code = _code.replace(/(window\.)?app\./, 'widget.getInstanceManager().app_obj.'); - var func = new Function('ev', 'widget', _code); - } - catch (e) { - _widget.egw().debug('error', 'Error while compiling JS code ', _code); - return (function () { return false; }); - } - // Execute the code and return its results, pass the egw instance and - // the widget - return function (ev) { - // Dump the executed code for debugging - egw.debug('log', 'Executing legacy JS code: ', _code); - if (arguments && arguments.length > 2) { - egw.debug('warn', 'Legacy JS code only supports 2 arguments (event and widget)', _code, arguments); - } - // Return the result of the called function - return func.call(context, ev, _widget); - }; -} -/** - * Resolve javascript pseudo functions in onclick or onchange: - * - egw::link('$l','$p') calls egw.link($l,$p) - * - form::name('name') returns expanded name/id taking into account the name at that point of the template hierarchy - * - egw::lang('Message ...') translate the message, calls egw.lang() - * - confirm('message') translates 'message' and adds a '?' if not present - * - window.open() replaces it with egw_openWindowCentered2() - * - xajax_doXMLHTTP('etemplate. replace ajax calls in widgets with special handler not requiring etemplate run rights - * - * @param {string} _val onclick, onchange, ... action - * @param {et2_widget} widget - * @ToDo replace xajax_doXMLHTTP with egw.json() - * @ToDo replace (common) cases of confirm with new dialog, idea: calling function supplys function to call after confirm - * @ToDo template::styles(name) inserts the styles of a named template - * @return string - */ -function js_pseudo_funcs(_val, widget) { - if (_val.indexOf('egw::link(') != -1) { - _val = _val.replace(/egw::link\(/g, 'egw.link('); - } - if (_val.indexOf('form::name(') != -1) { - // et2_form_name doesn't care about ][, just [ - var _cname = widget.getPath() ? widget.getPath().join("[") : false; - _val = _val.replace(/form::name\(/g, "'" + widget.getRoot()._inst.uniqueId + "_'+" + (_cname ? "et2_form_name('" + _cname + "'," : '(')); - } - if (_val.indexOf('egw::lang(') != -1) { - _val = _val.replace(/egw::lang\(/g, 'egw.lang('); - } - // ToDo: inserts the styles of a named template - /*if (preg_match('/template::styles\(["\']{1}(.*)["\']{1}\)/U',$on,$matches)) - { - $tpl = $matches[1] == $this->name ? $this : new etemplate($matches[1]); - $on = str_replace($matches[0],"''",$on); - }*/ - // translate messages in confirm() - if (_val.indexOf('confirm(') != -1) { - _val = _val.replace(/confirm\((['"])(.*?)(\?)?['"]\)/, "confirm(egw.lang($1$2$1)+'$3')"); // add ? if not there, saves extra phrase - } - // replace window.open() with EGw's egw_openWindowCentered2() - if (_val.indexOf('window.open(') != -1) { - _val = _val.replace(/window.open\('(.*)','(.*)','dependent=yes,width=([^,]*),height=([^,]*),scrollbars=yes,status=(.*)'\)/, "egw_openWindowCentered2('$1', '$2', $3, $4, '$5')"); - } - // replace xajax calls to code in widgets, with the "etemplate" handler, - // this allows to call widgets with the current app, otherwise everyone would need etemplate run rights - if (_val.indexOf("xajax_doXMLHTTP('etemplate.") != -1) { - _val = _val.replace(/^xajax_doXMLHTTP\('etemplate\.([a-z]+_widget\.[a-zA-Z0-9_]+)\'/, "xajax_doXMLHTTP('" + egw.getAppName() + ".$1.etemplate'"); - } - if (_val.indexOf('this.form.submit()') != -1) { - _val = _val.replace('this.form.submit()', 'widget.getInstanceManager().submit()'); - } - return _val; -} -//# sourceMappingURL=et2_core_legacyJSFunctions.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_phpExpressionCompiler.js b/api/js/etemplate/et2_core_phpExpressionCompiler.js deleted file mode 100644 index cc46d10b16..0000000000 --- a/api/js/etemplate/et2_core_phpExpressionCompiler.js +++ /dev/null @@ -1,353 +0,0 @@ -/** - * EGroupware eTemplate2 - A simple PHP expression parser written in JS - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - et2_core_common; -*/ -import { egw } from "../jsapi/egw_global"; -/** - * Function which compiles the given PHP string to a JS function which can be - * easily executed. - * - * @param _expr is the PHP string expression - * @param _vars is an array with variable names (without the PHP $). - * The parameters have to be passed to the resulting JS function in the same - * order. - */ -export function et2_compilePHPExpression(_expr, _vars) { - if (typeof _vars == "undefined") { - _vars = []; - } - try { - // Initialize the parser object and create the syntax tree for the given - // expression - var parser = _php_parser(_expr); - var syntaxTree = []; - // Parse the given expression as if it was a double quoted string - _php_parseDoubleQuoteString(parser, syntaxTree); - // Transform the generated syntaxTree into a JS string - var js = _php_compileJSCode(_vars, syntaxTree); - // Log the successfull compiling - egw.debug("log", "Compiled PHP " + _expr + " --> " + js); - } - catch (e) { - // if expression does NOT compile use it literally and log a warning, but not stop execution - egw.debug("warn", "Error compiling PHP " + _expr + " --> using it literally (" + - (typeof e == 'string' ? e : e.message) + ")!"); - return function () { return _expr; }; - } - // Prepate the attributes for the function constuctor - var attrs = []; - for (var i = 0; i < _vars.length; i++) { - attrs.push("_" + _vars[i]); - } - attrs.push(js); - // Create the function and return it - return (Function.apply(Function, attrs)); -} -const STATE_DEFAULT = 0; -const STATE_ESCAPED = 1; -const STATE_CURLY_BRACE_OPEN = 2; -const STATE_EXPECT_CURLY_BRACE_CLOSE = 3; -const STATE_EXPECT_RECT_BRACE_CLOSE = 4; -const STATE_EXPR_BEGIN = 5; -const STATE_EXPR_END = 6; -function _throwParserErr(_p, _err) { - throw ("Syntax error while parsing '" + _p.expr + "' at " + - _p.pos + ", " + _err); -} -function _php_parseDoubleQuoteString(_p, _tree) { - // Extract all PHP variables from the string - var state = STATE_DEFAULT; - var str = ""; - while (_p.pos < _p.expr.length) { - // Read the current char and then increment the parser position by - // one - var c = _p.expr.charAt(_p.pos++); - switch (state) { - case STATE_DEFAULT: - case STATE_CURLY_BRACE_OPEN: - switch (c) { - case '\\': - state = STATE_ESCAPED; - break; - case '$': - // check for '$$' as used in placeholder syntax, it is NOT expanded and returned as is - if (_p.expr.charAt(_p.pos) == "$" && state == STATE_DEFAULT) { - _p.pos++; - str += '$$'; - break; - } - // check for '$' as last char, as in PHP "test$" === 'test$', $ as last char is NOT expanded - if (_p.pos == _p.expr.length) { - str += '$'; - break; - } - // check for regular expression "/ $/" - if (_p.expr.charAt(_p.pos) == '/' && _p.expr.charAt(0) == '/') { - str += '$'; - break; - } - if (str) { - _tree.push(str); - str = ""; - } - // Support for the ${[expr] sytax - if (_p.expr.charAt(_p.pos) == "{" && state != STATE_CURLY_BRACE_OPEN) { - state = STATE_CURLY_BRACE_OPEN; - _p.pos++; - } - if (state == STATE_CURLY_BRACE_OPEN) { - _tree.push(_php_parseVariable(_p)); - state = STATE_EXPECT_CURLY_BRACE_CLOSE; - } - else { - _tree.push(_php_parseVariable(_p)); - } - break; - case '{': - state = STATE_CURLY_BRACE_OPEN; - break; - default: - if (state == STATE_CURLY_BRACE_OPEN) { - str += '{'; - state = STATE_DEFAULT; - } - str += c; - } - break; - case STATE_ESCAPED: - str += c; - break; - case STATE_EXPECT_CURLY_BRACE_CLOSE: - // When returning from the variableEx parser, - // the current char must be a "}" - if (c != "}") { - _throwParserErr(_p, "expected '}', but got " + c); - } - state = STATE_DEFAULT; - break; - } - } - // Throw an error when reaching the end of the string but expecting - // "}" - if (state == STATE_EXPECT_CURLY_BRACE_CLOSE) { - _throwParserErr(_p, "unexpected end of string, expected '}'"); - } - // Push the last part of the string onto the syntax tree - if (state == STATE_CURLY_BRACE_OPEN) { - str += "{"; - } - if (str) { - _tree.push(str); - } -} -// Regular expression which matches on PHP variable identifiers (without the $) -var PHP_VAR_PREG = /^([A-Za-z0-9_]+)/; -function _php_parseVariableName(_p) { - // Extract the variable name form the expression - var vname = PHP_VAR_PREG.exec(_p.expr.substr(_p.pos)); - if (vname) { - // Increment the parser position by the length of vname - _p.pos += vname[0].length; - return { "variable": vname[0], "accessExpressions": [] }; - } - _throwParserErr(_p, "expected variable identifier."); -} -function _php_parseVariable(_p) { - // Parse the first variable - var variable = _php_parseVariableName(_p); - // Parse all following variable access identifiers - var state = STATE_DEFAULT; - while (_p.pos < _p.expr.length) { - var c = _p.expr.charAt(_p.pos++); - switch (state) { - case STATE_DEFAULT: - switch (c) { - case "[": - // Parse the expression inside the rect brace - variable.accessExpressions.push(_php_parseExpression(_p)); - state = STATE_EXPECT_RECT_BRACE_CLOSE; - break; - default: - _p.pos--; - return variable; - } - break; - case STATE_EXPECT_RECT_BRACE_CLOSE: - if (c != "]") { - _throwParserErr(_p, " expected ']', but got " + c); - } - state = STATE_DEFAULT; - break; - } - } - return variable; -} -/** - * Reads a string delimited by the char _delim or the regExp _delim from the - * current parser context and returns it. - * - * @param {object} _p parser contect - * @param {string} _delim delimiter - * @return {string} string read (or throws an exception) - */ -function _php_readString(_p, _delim) { - var state = STATE_DEFAULT; - var str = ""; - while (_p.pos < _p.expr.length) { - var c = _p.expr.charAt(_p.pos++); - switch (state) { - case STATE_DEFAULT: - if (c == "\\") { - state = STATE_ESCAPED; - } - else if (c === _delim || (typeof _delim != "string" && _delim.test(c))) { - return str; - } - else { - str += c; - } - break; - case STATE_ESCAPED: - str += c; - state = STATE_DEFAULT; - break; - } - } - _throwParserErr(_p, "unexpected end of string while parsing string!"); -} -function _php_parseExpression(_p) { - var state = STATE_EXPR_BEGIN; - var result = null; - while (_p.pos < _p.expr.length) { - var c = _p.expr.charAt(_p.pos++); - switch (state) { - case STATE_EXPR_BEGIN: - switch (c) { - // Skip whitespace - case " ": - case "\n": - case "\r": - case "\t": - break; - case "\"": - result = []; - var p = _php_parser(_php_readString(_p, "\"")); - _php_parseDoubleQuoteString(p, result); - state = STATE_EXPR_END; - break; - case "\'": - result = _php_readString(_p, "'"); - state = STATE_EXPR_END; - break; - case "$": - result = _php_parseVariable(_p); - state = STATE_EXPR_END; - break; - default: - _p.pos--; - result = _php_readString(_p, /[^A-Za-z0-9_#]/); - if (!result) { - _throwParserErr(_p, "unexpected char " + c); - } - _p.pos--; - state = STATE_EXPR_END; - break; - } - break; - case STATE_EXPR_END: - switch (c) { - // Skip whitespace - case " ": - case "\n": - case "\r": - case "\t": - break; - default: - _p.pos--; - return result; - } - } - } - _throwParserErr(_p, "unexpected end of string while parsing access expressions!"); -} -function _php_parser(_expr) { - return { - expr: _expr, - pos: 0 - }; -} -function _throwCompilerErr(_err) { - throw ("PHP to JS compiler error, " + _err); -} -function _php_compileVariable(_vars, _variable) { - if (_vars.indexOf(_variable.variable) >= 0) { - // Attach a "_" to the variable name as PHP variable names may start - // with numeric values - var result = "_" + _variable.variable; - // Create the access functions - for (var i = 0; i < _variable.accessExpressions.length; i++) { - result += "[" + - _php_compileString(_vars, _variable.accessExpressions[i]) + - "]"; - } - return '(typeof _' + _variable.variable + ' != "undefined" && typeof ' + result + '!="undefined" && ' + result + ' != null ? ' + result + ':"")'; - } - _throwCompilerErr("Variable $" + _variable.variable + " is not defined."); -} -function _php_compileString(_vars, _string) { - if (!(_string instanceof Array)) { - _string = [_string]; - } - var parts = []; - var hasString = false; - for (var i = 0; i < _string.length; i++) { - var part = _string[i]; - if (typeof part == "string") { - hasString = true; - // Escape all "'" and "\" chars and add the string to the parts array - parts.push("'" + part.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'"); - } - else { - parts.push(_php_compileVariable(_vars, part)); - } - } - if (!hasString) // Force the result to be of the type string - { - parts.push('""'); - } - return parts.join(" + "); -} -function _php_compileJSCode(_vars, _tree) { - // Each tree starts with a "string" - return "return " + _php_compileString(_vars, _tree) + ";"; -} -// Include this code in in order to test the above code -/*(function () { - var row = 10; - var row_cont = {"title": "Hello World!"}; - var cont = {10: row_cont}; - - function test(_php, _res) - { - console.log( - et2_compilePHPExpression(_php, ["row", "row_cont", "cont"]) - (row, row_cont, cont) === _res); - } - - test("${row}[title]", "10[title]"); - test("{$row_cont[title]}", "Hello World!"); - test('{$cont["$row"][\'title\']}', "Hello World!"); - test("$row_cont[${row}[title]]"); - test("\\\\", "\\"); - test("", ""); -})();*/ -//# sourceMappingURL=et2_core_phpExpressionCompiler.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_valueWidget.js b/api/js/etemplate/et2_core_valueWidget.js deleted file mode 100644 index 361f32306a..0000000000 --- a/api/js/etemplate/et2_core_valueWidget.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * EGroupware eTemplate2 - JS widget class with value attribute and auto loading - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; -*/ -import { et2_baseWidget } from './et2_core_baseWidget'; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_csvSplit, et2_no_init } from "./et2_core_common"; -/** - * et2_valueWidget is the base class for et2_inputWidget - valueWidget introduces - * the "value" attribute and automatically loads it from the "content" array - * after loading from XML. - */ -export class et2_valueWidget extends et2_baseWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_valueWidget._attributes, _child || {})); - this.label = ''; - this._labelContainer = null; - } - /** - * - * @param _attrs - */ - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - if (this.id) { - // Set the value for this element - var contentMgr = this.getArrayMgr("content"); - if (contentMgr != null) { - let val = contentMgr.getEntry(this.id, false, true); - if (val !== null) { - _attrs["value"] = val; - } - } - // Check for already inside namespace - if (this._createNamespace() && this.getArrayMgr("content").perspectiveData.owner == this) { - _attrs["value"] = this.getArrayMgr("content").data; - } - } - } - set_label(_value) { - // Abort if there was no change in the label - if (_value == this.label) { - return; - } - if (_value) { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - // Clear the label container. - this._labelContainer.empty(); - // Create the placeholder element and set it - var ph = document.createElement("span"); - this.getSurroundings().setWidgetPlaceholder(ph); - // Split the label at the "%s" - var parts = et2_csvSplit(_value, 2, "%s"); - // Update the content of the label container - for (var i = 0; i < parts.length; i++) { - if (parts[i]) { - this._labelContainer.append(document.createTextNode(parts[i])); - } - if (i == 0) { - this._labelContainer.append(ph); - } - } - // add class if label is empty - this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); - } - else { - // Delete the labelContainer from the surroundings object - if (this._labelContainer) { - this.getSurroundings().removeDOMNode(this._labelContainer[0]); - } - this._labelContainer = null; - } - // Update the surroundings in order to reflect the change in the label - this.getSurroundings().update(); - // Copy the given value - this.label = _value; - } - get_value() { - return this.value; - } - /** - * Set value of widget - * - * @param {string} _value value to set - */ - set_value(_value) { - this.value = _value; - } -} -et2_valueWidget._attributes = { - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - }, - "value": { - "name": "Value", - "description": "The value of the widget", - "type": "rawstring", - "default": et2_no_init - } -}; -//# sourceMappingURL=et2_core_valueWidget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_widget.js b/api/js/etemplate/et2_core_widget.js deleted file mode 100644 index b82b66a534..0000000000 --- a/api/js/etemplate/et2_core_widget.js +++ /dev/null @@ -1,910 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Widget base class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - jsapi.egw; - et2_core_xml; - et2_core_common; - et2_core_inheritance; - et2_core_arrayMgr; -*/ -import { ClassWithAttributes } from './et2_core_inheritance'; -import { et2_arrayMgr } from "./et2_core_arrayMgr"; -import { egw } from "../jsapi/egw_global"; -import { et2_cloneObject, et2_csvSplit } from "./et2_core_common"; -import { et2_compileLegacyJS } from "./et2_core_legacyJSFunctions"; -import { et2_IDOMNode, et2_IInputNode } from "./et2_core_interfaces"; -/** - * The registry contains all XML tag names and the corresponding widget - * constructor. - */ -export var et2_registry = {}; -export var et2_attribute_registry = {}; -/** - * Registers the widget class defined by the given constructor, registers all its class attributes, and associates it - * with the types in the _types array. - * - * @param {function} _constructor constructor - * @param {array} _types widget types _constructor wants to register for - */ -export function et2_register_widget(_constructor, _types) { - "use strict"; - et2_attribute_registry[_constructor.name] = ClassWithAttributes.buildAttributes(_constructor); - // Iterate over all given types and register those - for (var i = 0; i < _types.length; i++) { - var type = _types[i].toLowerCase(); - // Check whether a widget has already been registered for one of the - // types. - if (et2_registry[type]) { - egw.debug("warn", "Widget class registered for " + type + - " will be overwritten."); - } - et2_registry[type] = _constructor; - } -} -/** - * Creates a widget registered for the given tag-name. If "readonly" is listed - * inside the attributes, et2_createWidget will try to use the "_ro" type of the - * widget. - * - * @param _name is the name of the widget with which it is registered. If the - * widget is not found, an et2_placeholder will be created. - * @param _attrs is an associative array with attributes. If not passed, it will - * default to an empty object. - * @param _parent is the parent to which the element will be attached. If _parent - * is not passed, it will default to null. Then you have to attach the element - * to a parent using the addChild or insertChild method. - */ -export function et2_createWidget(_name, _attrs, _parent) { - "use strict"; - if (typeof _attrs == "undefined") { - _attrs = {}; - } - if (typeof _attrs != "object") { - _attrs = {}; - } - if (typeof _parent == "undefined") { - _parent = null; - } - // Parse the "readonly" and "type" flag for this element here, as they - // determine which constructor is used - var nodeName = _attrs["type"] = _name; - var readonly = _attrs["readonly"] = - typeof _attrs["readonly"] == "undefined" ? false : _attrs["readonly"]; - // Get the constructor - if the widget is readonly, use the special "_ro" - // constructor if it is available - let constructor = et2_registry[typeof et2_registry[nodeName] == "undefined" ? 'placeholder' : nodeName]; - if (readonly && typeof et2_registry[nodeName + "_ro"] != "undefined") { - constructor = et2_registry[nodeName + "_ro"]; - } - // Do an sanity check for the attributes - ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], _attrs); - // Create the new widget and return it - return new constructor(_parent, _attrs); -} -/** - * The et2 widget base class. - * - * @augments ClassWithAttributes - */ -export class et2_widget extends ClassWithAttributes { - /** - * Widget constructor - * - * To implement the attributes inheritance and overriding each extending class/widget needs to call: - * - * super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_DOMWidget._attributes, _child || {})); - * - * @param _parent is the parent object from the XML tree which contains this - * object. The default constructor always adds the new instance to the - * children list of the given parent object. _parent may be NULL. - * @param _attrs is an associative array of attributes. - * @param _child attributes object from the child - */ - constructor(_parent, _attrs, _child) { - super(); // because we in the top of the widget hierarchy - this._children = []; - this._mgrs = {}; - /** - * This is used and therefore it we can not (yet) make it private - * - * @deprecated use this.getInstanceMgr() - */ - this._inst = null; - this.attributes = ClassWithAttributes.extendAttributes(et2_widget._attributes, _child || {}); - // Check whether all attributes are available - if (typeof _parent == "undefined") { - _parent = null; - } - if (typeof _attrs == "undefined") { - _attrs = {}; - } - if (_attrs.attributes) { - jQuery.extend(_attrs, _attrs.attributes); - } - // Initialize all important parameters - this._mgrs = {}; - this._inst = null; - this._children = []; - this._type = _attrs["type"]; - this.id = _attrs["id"]; - // Add this widget to the given parent widget - if (_parent != null) { - _parent.addChild(this); - } - // The supported widget classes array defines a whitelist for all widget - // classes or interfaces child widgets have to support. - this.supportedWidgetClasses = [et2_widget]; - if (_attrs["id"]) { - // Create a namespace for this object - if (this._createNamespace()) { - this.checkCreateNamespace(_attrs); - } - } - if (this.id) { - //this.id = this.id.replace(/\[/g,'[').replace(/]/g,']'); - } - // Add all attributes hidden in the content arrays to the attributes - // parameter - this.transformAttributes(_attrs); - // Create a local copy of the options object - this.options = et2_cloneObject(_attrs); - } - /** - * The destroy function destroys all children of the widget, removes itself - * from the parents children list. - * In all classes derrived from et2_widget ALWAYS override the destroy - * function and remove ALL references to other objects. Also remember to - * unbind ANY event this widget created and to remove all DOM-Nodes it - * created. - */ - destroy() { - // Call the destructor of all children - for (var i = this._children.length - 1; i >= 0; i--) { - this._children[i].destroy(); - } - // Remove this element from the parent, if it exists - if (typeof this._parent != "undefined" && this._parent !== null) { - this._parent.removeChild(this); - } - // Free the array managers if they belong to this widget - for (var key in this._mgrs) { - if (this._mgrs[key] && this._mgrs[key].owner == this) { - this._mgrs[key].destroy(); - } - } - } - getType() { - return this._type; - } - setType(_type) { - this._type = _type; - } - /** - * Creates a copy of this widget. The parameters given are passed to the - * constructor of the copied object. If the parameters are omitted, _parent - * is defaulted to null - * - * @param {et2_widget} _parent parent to set for clone, default null - */ - clone(_parent) { - // Default _parent to null - if (typeof _parent == "undefined") { - _parent = null; - } - // Create the copy - var copy = new this.constructor(_parent, this.options); - // Assign this element to the copy - copy.assign(this); - return copy; - } - assign(_obj) { - if (typeof _obj._children == "undefined") { - this.egw().debug("log", "Foo!"); - } - // Create a clone of all child elements of the given object - for (var i = 0; i < _obj._children.length; i++) { - _obj._children[i].clone(this); - } - // Copy a reference to the content array manager - this.setArrayMgrs(_obj.mgrs); - } - /** - * Returns the parent widget of this widget - */ - getParent() { - return this._parent; - } - /** - * Returns the list of children of this widget. - */ - getChildren() { - return this._children; - } - /** - * Returns the base widget - */ - getRoot() { - if (this._parent != null) { - return this._parent.getRoot(); - } - else { - return this; - } - } - /** - * Inserts an child at the end of the list. - * - * @param _node is the node which should be added. It has to be an instance - * of et2_widget - */ - addChild(_node) { - this.insertChild(_node, this._children.length); - } - /** - * Inserts a child at the given index. - * - * @param _node is the node which should be added. It has to be an instance - * of et2_widget - * @param _idx is the position at which the element should be added. - */ - insertChild(_node, _idx) { - // Check whether the node is one of the supported widget classes. - if (this.isOfSupportedWidgetClass(_node)) { - // Remove the node from its original parent - if (_node._parent) { - _node._parent.removeChild(_node); - } - _node._parent = this; - this._children.splice(_idx, 0, _node); - } - else { - this.egw().debug("error", "Widget " + _node._type + " is not supported by this widget class", this); - // throw("Widget is not supported by this widget class!"); - } - } - /** - * Removes the child but does not destroy it. - * - * @param {et2_widget} _node child to remove - */ - removeChild(_node) { - // Retrieve the child from the child list - var idx = this._children.indexOf(_node); - if (idx >= 0) { - // This element is no longer parent of the child - _node._parent = null; - this._children.splice(idx, 1); - } - } - /** - * Searches an element by id in the tree, descending into the child levels. - * - * @param _id is the id you're searching for - */ - getWidgetById(_id) { - if (this.id == _id) { - return this; - } - if (!this._children) - return null; - for (var i = 0; i < this._children.length; i++) { - var elem = this._children[i].getWidgetById(_id); - if (elem != null) { - return elem; - } - } - if (this.id && _id.indexOf('[') > -1 && this._children.length) { - var ids = (new et2_arrayMgr()).explodeKey(_id); - var widget = this; - for (var i = 0; i < ids.length && widget !== null; i++) { - widget = widget.getWidgetById(ids[i]); - } - return widget; - } - return null; - } - /** - * Function which allows iterating over the complete widget tree. - * - * @param _callback is the function which should be called for each widget - * @param _context is the context in which the function should be executed - * @param _type is an optional parameter which specifies a class/interface - * the elements have to be instanceOf. - */ - iterateOver(_callback, _context, _type) { - if (typeof _type == "undefined") { - _type = et2_widget; - } - if (this.isInTree() && this.instanceOf(_type)) { - _callback.call(_context, this); - } - for (var i = 0; i < this._children.length; i++) { - this._children[i].iterateOver(_callback, _context, _type); - } - } - /** - * Returns true if the widget currently resides in the visible part of the - * widget tree. E.g. Templates which have been cloned are not in the visible - * part of the widget tree. - * - * @param _sender - * @param {boolean} _vis can be used by widgets overwriting this function - simply - * write - * return this._super(inTree); - * when calling this function the _vis parameter does not have to be supplied. - */ - isInTree(_sender, _vis) { - if (typeof _vis == "undefined") { - _vis = true; - } - if (this._parent) { - return _vis && this._parent.isInTree(this); - } - return _vis; - } - isOfSupportedWidgetClass(_obj) { - for (var i = 0; i < this.supportedWidgetClasses.length; i++) { - if (_obj instanceof this.supportedWidgetClasses[i]) { - return true; - } - } - return false; - } - /** - * The parseXMLAttrs function takes an XML DOM attributes object - * and adds the given attributes to the _target associative array. This - * function also parses the legacyOptions. - * - * @param _attrsObj is the XML DOM attributes object - * @param {object} _target is the object to which the attributes should be written. - * @param {et2_widget} _proto prototype with attributes and legacyOptions attribute - */ - parseXMLAttrs(_attrsObj, _target, _proto) { - // Check whether the attributes object is really existing, if not abort - if (typeof _attrsObj == "undefined") { - return; - } - // Iterate over the given attributes and parse them - var mgr = this.getArrayMgr("content"); - for (var i = 0; i < _attrsObj.length; i++) { - var attrName = _attrsObj[i].name; - var attrValue = _attrsObj[i].value; - // Special handling for the legacy options - if (attrName == "options" && _proto.constructor.legacyOptions && _proto.constructor.legacyOptions.length > 0) { - let legacy = _proto.constructor.legacyOptions || []; - let attrs = et2_attribute_registry[Object.getPrototypeOf(_proto).constructor.name] || {}; - // Check for modifications on legacy options here. Normal modifications - // are handled in widget constructor, but it's too late for legacy options then - if (_target.id && this.getArrayMgr("modifications").getEntry(_target.id)) { - var mod = this.getArrayMgr("modifications").getEntry(_target.id); - if (typeof mod.options != "undefined") - attrValue = _attrsObj[i].value = mod.options; - } - // expand legacyOptions with content - if (attrValue.charAt(0) == '@' || attrValue.indexOf('$') != -1) { - attrValue = mgr.expandName(attrValue); - } - // Parse the legacy options (as a string, other types not allowed) - var splitted = et2_csvSplit(attrValue + ""); - for (var j = 0; j < splitted.length && j < legacy.length; j++) { - // Blank = not set, unless there's more legacy options provided after - if (splitted[j].trim().length === 0 && legacy.length >= splitted.length) - continue; - // Check to make sure we don't overwrite a current option with a legacy option - if (typeof _target[legacy[j]] === "undefined") { - attrValue = splitted[j]; - /** - If more legacy options than expected, stuff them all in the last legacy option - Some legacy options take a comma separated list. - */ - if (j == legacy.length - 1 && splitted.length > legacy.length) { - attrValue = splitted.slice(j); - } - var attr = et2_attribute_registry[_proto.constructor.name][legacy[j]] || {}; - // If the attribute is marked as boolean, parse the - // expression as bool expression. - if (attr.type == "boolean") { - attrValue = mgr.parseBoolExpression(attrValue); - } - else if (typeof attrValue != "object") { - attrValue = mgr.expandName(attrValue); - } - _target[legacy[j]] = attrValue; - } - } - } - else if (attrName == "readonly" && typeof _target[attrName] != "undefined") { - // do NOT overwrite already evaluated readonly attribute - } - else { - let attrs = et2_attribute_registry[_proto.constructor.name] || {}; - if (mgr != null && typeof attrs[attrName] != "undefined") { - var attr = attrs[attrName]; - // If the attribute is marked as boolean, parse the - // expression as bool expression. - if (attr.type == "boolean") { - attrValue = mgr.parseBoolExpression(attrValue); - } - else { - attrValue = mgr.expandName(attrValue); - } - } - // Set the attribute - _target[attrName] = attrValue; - } - } - } - /** - * Apply the "modifications" to the element and translate attributes marked - * with "translate: true" - * - * @param {object} _attrs - */ - transformAttributes(_attrs) { - // Apply the content of the modifications array - if (this.id) { - if (typeof this.id != "string") { - console.log(this.id); - } - if (this.getArrayMgr("modifications")) { - var data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for already inside namespace - if (this._createNamespace() && this.getArrayMgr("modifications").perspectiveData.owner == this) { - data = this.getArrayMgr("modifications").data; - } - if (typeof data === 'object') { - for (var key in data) { - _attrs[key] = data[key]; - } - } - } - } - // Translate the attributes - for (var key in _attrs) { - if (_attrs[key] && typeof this.attributes[key] != "undefined") { - if (this.attributes[key].translate === true || - (this.attributes[key].translate === "!no_lang" && !_attrs["no_lang"])) { - let value = _attrs[key]; - // allow statustext to contain multiple translated sub-strings eg: {Firstname}.{Lastname} - if (value.indexOf('{') !== -1) { - const egw = this.egw(); - _attrs[key] = value.replace(/{([^}]+)}/g, function (str, p1) { - return egw.lang(p1); - }); - } - else { - _attrs[key] = this.egw().lang(value); - } - } - } - } - } - /** - * Create a et2_widget from an XML node. - * - * First the type and attributes are read from the node. Then the readonly & modifications - * arrays are checked for changes specific to the loaded data. Then the appropriate - * constructor is called. After the constructor returns, the widget has a chance to - * further initialize itself from the XML node when the widget's loadFromXML() method - * is called with the node. - * - * @param _node XML node to read - * @param _name XML node name - * - * @return et2_widget - */ - createElementFromNode(_node, _name) { - var attributes = {}; - // Parse the "readonly" and "type" flag for this element here, as they - // determine which constructor is used - var _nodeName = attributes["type"] = _node.getAttribute("type") ? - _node.getAttribute("type") : _node.nodeName.toLowerCase(); - var readonly = attributes["readonly"] = this.getArrayMgr("readonlys") ? - this.getArrayMgr("readonlys").isReadOnly(_node.getAttribute("id"), _node.getAttribute("readonly"), typeof this.readonly !== 'undefined' ? this.readonly : this.options.readonly) : false; - // Check to see if modifications change type - var modifications = this.getArrayMgr("modifications"); - if (modifications && _node.getAttribute("id")) { - var entry = modifications.getEntry(_node.getAttribute("id")); - if (entry == null) { - // Try again, but skip the fancy stuff - // TODO: Figure out why the getEntry() call doesn't always work - var entry = modifications.data[_node.getAttribute("id")]; - if (entry) { - this.egw().debug("warn", "getEntry(" + _node.getAttribute("id") + ") failed, but the data is there.", modifications, entry); - } - else { - // Try the root, in case a namespace got missed - entry = modifications.getRoot().getEntry(_node.getAttribute("id")); - } - } - if (entry && entry.type && typeof entry.type === 'string') { - _nodeName = attributes["type"] = entry.type; - } - entry = null; - } - // if _nodeName / type-attribute contains something to expand (eg. type="@${row}[type]"), - // we need to expand it now as it defines the constructor and by that attributes parsed via parseXMLAttrs! - if (_nodeName.charAt(0) == '@' || _nodeName.indexOf('$') >= 0) { - _nodeName = attributes["type"] = this.getArrayMgr('content').expandName(_nodeName); - } - // Get the constructor - if the widget is readonly, use the special "_ro" - // constructor if it is available - var constructor = et2_registry[typeof et2_registry[_nodeName] == "undefined" ? 'placeholder' : _nodeName]; - if (readonly === true && typeof et2_registry[_nodeName + "_ro"] != "undefined") { - constructor = et2_registry[_nodeName + "_ro"]; - } - // Parse the attributes from the given XML attributes object - this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); - // Do an sanity check for the attributes - ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes); - if (undefined == window.customElements.get(_nodeName)) { - // Creates the new widget, passes this widget as an instance and - // passes the widgetType. Then it goes on loading the XML for it. - var widget = new constructor(this, attributes); - // Load the widget itself from XML - widget.loadFromXML(_node); - } - else { - widget = this.loadWebComponent(_nodeName, _node, attributes); - if (this.addChild) { - // webcomponent going into old et2_widget - this.addChild(widget); - } - } - return widget; - } - /** - * Load a Web Component - * @param _nodeName - * @param _node - */ - loadWebComponent(_nodeName, _node, attributes) { - let widget = document.createElement(_nodeName); - widget.textContent = _node.textContent; - // Apply any set attributes - _node.getAttributeNames().forEach(attribute => widget.setAttribute(attribute, attributes[attribute])); - return widget; - } - /** - * Loads the widget tree from an XML node - * - * @param _node xml node - */ - loadFromXML(_node) { - // Load the child nodes. - for (var i = 0; i < _node.childNodes.length; i++) { - var node = _node.childNodes[i]; - var widgetType = node.nodeName.toLowerCase(); - if (widgetType == "#comment") { - continue; - } - if (widgetType == "#text") { - if (node.data.replace(/^\s+|\s+$/g, '')) { - this.loadContent(node.data); - } - continue; - } - // Create the new element - this.createElementFromNode(node); - } - } - /** - * Called whenever textNodes are loaded from the XML tree - * - * @param _content - */ - loadContent(_content) { - } - /** - * Called when loading the widget (sub-tree) is finished. First when this - * function is called, the DOM-Tree is created. loadingFinished is - * recursively called for all child elements. Do not directly override this - * function but the doLoadingFinished function which is executed before - * descending deeper into the DOM-Tree. - * - * Some widgets (template) do not load immediately because they request - * additional resources via AJAX. They will return a Deferred Promise object. - * If you call loadingFinished(promises) after creating such a widget - * programmatically, you might need to wait for it to fully complete its - * loading before proceeding. In that case use: - * - * var promises = []; - * widget.loadingFinished(promises); - * jQuery.when.apply(null, promises).done( doneCallback ); - * - * @see {@link http://api.jquery.com/category/deferred-object/|jQuery Deferred} - * - * @param {Promise[]} promises List of promises from widgets that are not done. Pass an empty array, it will be filled if needed. - */ - loadingFinished(promises) { - // Call all availble setters - this.initAttributes(this.options); - // Make sure promises is defined to avoid errors. - // We'll warn (below) if programmer should have passed it. - if (typeof promises == "undefined") { - promises = []; - var warn_if_deferred = true; - } - var loadChildren = function () { - // Descend recursively into the tree - for (var i = 0; i < this._children.length; i++) { - try { - this._children[i].loadingFinished(promises); - } - catch (e) { - egw.debug("error", "There was an error with a widget:\nError:%o\nProblem widget:%o", e.valueOf(), this._children[i], e.stack); - } - } - }; - var result = this.doLoadingFinished(); - if (typeof result == "boolean" && result) { - // Simple widget finishes nicely - loadChildren.apply(this, arguments); - } - else if (typeof result == "object" && result.done) { - // Warn if list was not provided - if (warn_if_deferred) { - // Might not be a problem, but if you need the widget to be really loaded, it could be - egw.debug("warn", "Loading was deferred for widget %o, but creator is not checking. Pass a list to loadingFinished().", this); - } - // Widget is waiting. Add to the list - promises.push(result); - // Fihish loading when it's finished - result.done(jQuery.proxy(loadChildren, this)); - } - } - /** - * The initAttributes function sets the attributes to their default - * values. The attributes are not overwritten, which means, that the - * default is only set, if either a setter exists or this[propName] does - * not exist yet. - * - * Overwritten here to compile legacy JS code in attributes of type "js" - * - * @param {object} _attrs - */ - initAttributes(_attrs) { - for (var key in _attrs) { - if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { - var val = _attrs[key]; - // compile string values of attribute type "js" to functions - if (this.attributes[key].type == 'js' && typeof _attrs[key] == 'string') { - val = et2_compileLegacyJS(val, this, this.implements(et2_IInputNode) ? this.getInputNode() : - (this.implements(et2_IDOMNode) ? this.getDOMNode() : null)); - } - this.setAttribute(key, val, false); - } - } - } - /** - * Does specific post-processing after the widget is loaded. Most widgets should not - * need to do anything here, it should all be done before. - * - * @return {boolean|Promise} True if the widget is fully loaded, false to avoid procesing children, - * or a Promise if loading is not actually finished (eg. waiting for AJAX) - * - * @see {@link http://api.jquery.com/deferred.promise/|jQuery Promise} - */ - doLoadingFinished() { - return true; - } - /** - * The egw function returns the instance of the client side api belonging - * to this widget tree. The api instance can be set in the "container" - * widget using the setApiInstance function. - */ - egw() { - // The _egw property is not set - if (typeof this._egw === 'undefined') { - if (this._parent != null) { - return this._parent.egw(); - } - // Get the window this object belongs to - var wnd = null; - if (this.implements(et2_IDOMNode)) { - var node = this.getDOMNode(); - if (node && node.ownerDocument) { - wnd = node.ownerDocument.parentNode || node.ownerDocument.defaultView; - } - } - // If we're the root object, return the phpgwapi API instance - return egw('phpgwapi', wnd); - } - return this._egw; - } - /** - * Sets the client side api instance. It can be retrieved by the widget tree - * by using the "egw()" function. - * - * @param {IegwAppLocal} _egw egw object to set - */ - setApiInstance(_egw) { - this._egw = _egw; - } - /** - * Sets all array manager objects - this function can be used to set the - * root array managers of the container object. - * - * @param {object} _mgrs - */ - setArrayMgrs(_mgrs) { - this._mgrs = et2_cloneObject(_mgrs); - } - /** - * Returns an associative array containing the top-most array managers. - * - * @param _mgrs is used internally and should not be supplied. - */ - getArrayMgrs(_mgrs) { - if (typeof _mgrs == "undefined") { - _mgrs = {}; - } - // Add all managers of this object to the result, if they have not already - // been set in the result - for (var key in this._mgrs) { - if (typeof _mgrs[key] == "undefined") { - _mgrs[key] = this._mgrs[key]; - } - } - // Recursively applies this function to the parent widget - if (this._parent) { - this._parent.getArrayMgrs(_mgrs); - } - return _mgrs; - } - /** - * Sets the array manager for the given part - * - * @param {string} _part which array mgr to set - * @param {object} _mgr - */ - setArrayMgr(_part, _mgr) { - this._mgrs[_part] = _mgr; - } - /** - * Returns the array manager object for the given part - * - * @param {string} managed_array_type name of array mgr to return - */ - getArrayMgr(managed_array_type) { - if (this._mgrs && typeof this._mgrs[managed_array_type] != "undefined") { - return this._mgrs[managed_array_type]; - } - else if (this._parent) { - return this._parent.getArrayMgr(managed_array_type); - } - return null; - } - /** - * Checks whether a namespace exists for this element in the content array. - * If yes, an own perspective of the content array is created. If not, the - * parent content manager is used. - * - * Constructor attributes are passed in case a child needs to make decisions - */ - checkCreateNamespace(_attrs) { - // Get the content manager - var mgrs = this.getArrayMgrs(); - for (var key in mgrs) { - var mgr = mgrs[key]; - // Get the original content manager if we have already created a - // perspective for this node - if (typeof this._mgrs[key] != "undefined" && mgr.perspectiveData.owner == this) { - mgr = mgr.parentMgr; - } - // Check whether the manager has a namespace for the id of this object - var entry = mgr.getEntry(this.id); - if (typeof entry === 'object' && entry !== null || this.id) { - // The content manager has an own node for this object, so - // create an own perspective. - this._mgrs[key] = mgr.openPerspective(this, this.id); - } - else { - // The current content manager does not have an own namespace for - // this element, so use the content manager of the parent. - delete (this._mgrs[key]); - } - } - } - /** - * Widgets that do support a namespace should override and return true. - * - * Since a private attribute doesn't get instanciated properly before it's needed, - * we use a method so we can get what we need while still in the constructor. - * - * @private - */ - _createNamespace() { - return false; - } - /** - * Sets the instance manager object (of type etemplate2, see etemplate2.js) - * - * @param {etemplate2} _inst - */ - setInstanceManager(_inst) { - this._inst = _inst; - } - /** - * Returns the instance manager - * - * @return {etemplate2} - */ - getInstanceManager() { - if (this._inst != null) { - return this._inst; - } - else if (this._parent) { - return this._parent.getInstanceManager(); - } - return null; - } - /** - * Returns the path into the data array. By default, array manager takes care of - * this, but some extensions need to override this - */ - getPath() { - var path = this.getArrayMgr("content").getPath(); - // Prevent namespaced widgets with value from going an extra layer deep - if (this.id && this._createNamespace() && path[path.length - 1] == this.id) - path.pop(); - return path; - } -} -et2_widget._attributes = { - "id": { - "name": "ID", - "type": "string", - "description": "Unique identifier of the widget" - }, - "no_lang": { - "name": "No translation", - "type": "boolean", - "default": false, - "description": "If true, no translations are made for this widget" - }, - /** - * Ignore the "span" property by default - it is read by the grid and - * other widgets. - */ - "span": { - "ignore": true - }, - /** - * Ignore the "type" tag - it is read by the "createElementFromNode" - * function and passed as second parameter of the widget constructor - */ - "type": { - "name": "Widget type", - "type": "string", - "ignore": true, - "description": "What kind of widget this is" - }, - /** - * Ignore the readonly tag by default - its also read by the - * "createElementFromNode" function. - */ - "readonly": { - "ignore": true - }, - /** - * Widget's attributes - */ - attributes: { - "name": "Widget attributes", - "type": "any", - "ignore": true, - "description": "Object of widget attributes" - } -}; -// Set the legacyOptions array to the names of the properties the "options" -// attribute defines. -et2_widget.legacyOptions = []; -//# sourceMappingURL=et2_core_widget.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_core_xml.js b/api/js/etemplate/et2_core_xml.js deleted file mode 100644 index a198fc0d5f..0000000000 --- a/api/js/etemplate/et2_core_xml.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * EGroupware eTemplate2 - JS XML Code - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -import "../../../vendor/bower-asset/jquery/dist/jquery.min.js"; -import "../../../vendor/bower-asset/jquery-ui/jquery-ui.js"; -import "../jquery/jquery.noconflict.js"; -import { egw } from "../jsapi/egw_global.js"; -/** - * Loads the given URL asynchronously from the server - * - * We make the Ajax call through main-windows jQuery object, to ensure cached copy - * in main-windows etemplate2 prototype works in IE too! - * - * @param {string} _url - * @param {function} _callback function(_xml) - * @param {object} _context for _callback - * @param {function} _fail_callback function(_xml) - * @return Promise - */ -export function et2_loadXMLFromURL(_url, _callback, _context, _fail_callback) { - if (typeof _context == "undefined") { - _context = null; - } - // use window object from main window with same algorithm as for the template cache - let win; - try { - if (opener && opener.etemplate2) { - win = opener; - } - } - catch (e) { - // catch security exception if opener is from a different domain - } - if (typeof win == "undefined") { - win = egw.top; - } - return win.jQuery.ajax({ - // we add the full url (protocol and domain) as sometimes just the path - // gives a CSP error interpreting it as file:///path - // (if there are a enough 404 errors in html content ...) - url: (_url[0] == '/' ? location.protocol + '//' + location.host : '') + _url, - context: _context, - type: 'GET', - dataType: 'xml', - success: function (_data, _status, _xmlhttp) { - if (typeof _callback === 'function') { - _callback.call(_context, _data.documentElement); - } - }, - error: function (_xmlhttp, _err) { - egw().debug('error', 'Loading eTemplate from ' + _url + ' failed! ' + _xmlhttp.status + ' ' + _xmlhttp.statusText); - if (typeof _fail_callback === 'function') { - _fail_callback.call(_context, _err); - } - } - }); -} -export function et2_directChildrenByTagName(_node, _tagName) { - // Normalize the tag name - _tagName = _tagName.toLowerCase(); - let result = []; - for (let i = 0; i < _node.childNodes.length; i++) { - if (_tagName == _node.childNodes[i].nodeName.toLowerCase()) { - result.push(_node.childNodes[i]); - } - } - return result; -} -export function et2_filteredNodeIterator(_node, _callback, _context) { - for (let i = 0; i < _node.childNodes.length; i++) { - let node = _node.childNodes[i]; - let nodeName = node.nodeName.toLowerCase(); - if (nodeName.charAt(0) != "#") { - _callback.call(_context, node, nodeName); - } - } -} -export function et2_readAttrWithDefault(_node, _name, _default) { - let val = _node.getAttribute(_name); - return (val === null) ? _default : val; -} -//# sourceMappingURL=et2_core_xml.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview.js b/api/js/etemplate/et2_dataview.js deleted file mode 100644 index 76601ef212..0000000000 --- a/api/js/etemplate/et2_dataview.js +++ /dev/null @@ -1,479 +0,0 @@ -/** - * EGroupware eTemplate2 - dataview code - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_common; - - et2_dataview_model_columns; - et2_dataview_view_grid; - et2_dataview_view_rowProvider; - et2_dataview_view_resizeable; -*/ -import { et2_dataview_column, et2_dataview_columns } from './et2_dataview_model_columns'; -import { et2_dataview_view_resizable } from "./et2_dataview_view_resizeable"; -import { et2_dataview_grid } from "./et2_dataview_view_grid"; -import { et2_dataview_rowProvider } from "./et2_dataview_view_rowProvider"; -import { egw } from "../jsapi/egw_global"; -/** - * The et2_dataview class is the main class for displaying a dataview. The - * dataview class manages the creation of the outer html nodes (like the table, - * header, etc.) and contains the root container: an instance of - * et2_dataview_view_grid, which can be accessed using the "grid" property of - * this object. - * - * @augments Class - */ -export class et2_dataview { - /** - * Constructor for the grid container - * - * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted - * @param {egw} _egw - * @memberOf et2_dataview - */ - constructor(_parentNode, _egw) { - // Copy the arguments - this.parentNode = jQuery(_parentNode); - this.egw = _egw; - // Initialize some variables - this.columnNodes = []; // Array with the header containers - this.columns = []; - this.columnMgr = null; - this.rowProvider = null; - this.width = 0; - this.height = 0; - this.uniqueId = "gridCont_" + this.egw.uid(); - // Build the base nodes - this._createElements(); - // Read the browser dependant variables - this._getDepVars(); - } - /** - * Destroys the object, removes all dom nodes and clears all references. - */ - destroy() { - // Clear the columns - this._clearHeader(); - // Free the grid - if (this.grid) { - this.grid.destroy(); - } - // Free the row provider - if (this.rowProvider) { - this.rowProvider.destroy(); - } - // Detatch the outer element - this.table.remove(); - } - /** - * Clears all data rows and reloads them - */ - clear() { - if (this.grid) { - this.grid.clear(); - } - } - /** - * Returns the column container node for the given column index - * - * @param _columnIdx the integer column index - */ - getHeaderContainerNode(_columnIdx) { - if (typeof this.columnNodes[_columnIdx] != "undefined") { - return this.columnNodes[_columnIdx].container[0]; - } - return null; - } - /** - * Sets the column descriptors and creates the column header according to it. - * The inner grid will be emptied if it has already been built. - */ - setColumns(_columnData) { - // Free all column objects which have been created till this moment - this._clearHeader(); - // Copy the given column data - this.columnMgr = new et2_dataview_columns(_columnData); - // Create the stylesheets - this.updateColumns(); - // Build the header row - this._buildHeader(); - // Build the grid - this._buildGrid(); - } - /** - * Resizes the grid - */ - resize(_w, _h) { - // Not fully initialized yet... - if (!this.columnMgr) - return; - if (this.width != _w) { - this.width = _w; - // Take grid border width into account - _w -= (this.table.outerWidth(true) - this.table.innerWidth()); - // Take grid header border's width into account. eg. category colors may add extra pixel into width - _w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth()); - // Rebuild the column stylesheets - this.columnMgr.setTotalWidth(_w - this.scrollbarWidth); - this._updateColumns(); - } - if (this.height != _h) { - this.height = _h; - // Set the height of the grid. - if (this.grid) { - this.grid.setScrollHeight(this.height - - this.headTr.outerHeight(true)); - } - } - } - /** - * Returns the column manager object. You can use it to set the visibility - * of columns etc. Call "updateHeader" if you did any changes. - */ - getColumnMgr() { - return this.columnMgr; - } - /** - * Recalculates the stylesheets which determine the column visibility and - * width. - * - * @param setDefault boolean Allow admins to save current settings as default for all users - */ - updateColumns(setDefault = false) { - if (this.columnMgr) { - this._updateColumns(); - } - // Ability to notify parent / someone else - if (this.onUpdateColumns) { - this.onUpdateColumns(setDefault); - } - } - /* --- PRIVATE FUNCTIONS --- */ - /* --- Code for building the grid container DOM-Tree elements ---- */ - /** - * Builds the base DOM-Tree elements - */ - _createElements() { - /* - Structure: - - - [HEAD] - - - [GRID CONTAINER] - -
- */ - this.containerTr = jQuery(document.createElement("tr")); - this.headTr = jQuery(document.createElement("tr")); - this.thead = jQuery(document.createElement("thead")) - .append(this.headTr); - this.tbody = jQuery(document.createElement("tbody")) - .append(this.containerTr); - this.table = jQuery(document.createElement("table")) - .addClass("egwGridView_outer") - .append(this.thead, this.tbody) - .appendTo(this.parentNode); - } - /* --- Code for building the header row --- */ - /** - * Clears the header row - */ - _clearHeader() { - if (this.columnMgr) { - this.columnMgr.destroy(); - this.columnMgr = null; - } - // Remove dynamic CSS, - for (var i = 0; i < this.columns.length; i++) { - if (this.columns[i].tdClass) { - this.egw.css('.' + this.columns[i].tdClass); - } - if (this.columns[i].divClass) { - this.egw.css('.' + this.columns[i].divClass); - this.egw.css(".egwGridView_outer ." + this.columns[i].divClass); - this.egw.css(".egwGridView_grid ." + this.columns[i].divClass); - } - } - this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow"); - // Reset the headerColumns array and empty the table row - this.columnNodes = []; - this.columns = []; - this.headTr.empty(); - } - /** - * Sets the column data which is retrieved by calling egwGridColumns.getColumnData. - * The columns will be updated. - */ - _updateColumns() { - // Copy the columns data - this.columns = this.columnMgr.getColumnData(); - // Count the visible rows - var total_cnt = 0; - for (var i = 0; i < this.columns.length; i++) { - if (this.columns[i].visible) { - total_cnt++; - } - } - // Set the grid column styles - var first = true; - var vis_col = this.visibleColumnCount = 0; - var totalWidth = 0; - for (var i = 0; i < this.columns.length; i++) { - var col = this.columns[i]; - col.tdClass = this.uniqueId + "_td_" + col.id; - col.divClass = this.uniqueId + "_div_" + col.id; - if (col.visible) { - vis_col++; - this.visibleColumnCount++; - // Update the visibility of the column - this.egw.css("." + col.tdClass, "display: table-cell; " + - "!important;"); - // Ugly browser dependant code - each browser seems to treat the - // right (collapsed) border of the row differently - var subBorder = 0; - var subHBorder = 0; - /* - if (jQuery.browser.mozilla) - { - var maj = jQuery.browser.version.split(".")[0]; - if (maj < 2) { - subBorder = 1; // Versions <= FF 3.6 - } - } - if (jQuery.browser.webkit) - { - if (!first) - { - subBorder = 1; - } - subHBorder = 1; - } - if ((jQuery.browser.msie || jQuery.browser.opera) && first) - { - subBorder = -1; - } - */ - // Make the last columns one pixel smaller, to prevent a horizontal - // scrollbar from showing up - if (vis_col == total_cnt) { - subBorder += 1; - } - // Write the width of the header columns - var headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder)); - this.egw.css(".egwGridView_outer ." + col.divClass, "width: " + headerWidth + "px;"); - // Write the width of the body-columns - var columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder)); - this.egw.css(".egwGridView_grid ." + col.divClass, "width: " + columnWidth + "px;"); - totalWidth += col.width; - first = false; - } - else { - this.egw.css("." + col.tdClass, "display: none;"); - } - } - // Add the full row and spacer class - this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", "border-right-width: 0 !important;"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); - } - /** - * Builds the containers for the header row - */ - _buildHeader() { - var self = this; - var handler = function (event) { - }; - for (var i = 0; i < this.columns.length; i++) { - var col = this.columns[i]; - // Create the column header and the container element - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer") - .addClass(col.divClass); - var column = jQuery(document.createElement("th")) - .addClass(col.tdClass) - .attr("align", "left") - .append(cont) - .appendTo(this.headTr); - if (this.columnMgr && this.columnMgr.getColumnById(i)) { - column.addClass(this.columnMgr.getColumnById(i).fixedWidth ? 'fixedWidth' : 'relativeWidth'); - if (this.columnMgr.getColumnById(i).visibility === et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { - column.addClass('noResize'); - } - } - // make column resizable - var enc_column = self.columnMgr.getColumnById(col.id); - if (enc_column.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { - et2_dataview_view_resizable.makeResizeable(column, function (_w) { - // User wants the column to stay where they put it, even for relative - // width columns, so set it explicitly first and adjust other relative - // columns to match. - if (this.relativeWidth) { - // Set to selected width - this.set_width(_w + "px"); - self.columnMgr.updated(); - // Just triggers recalculation - self.columnMgr.getColumnWidth(0); - // Set relative widths to match - var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w; - this.set_width(_w / relative); - for (var i = 0; i < self.columnMgr.columnCount(); i++) { - var col = self.columnMgr.getColumnById('col_' + i); - if (!col || col == this || col.fixedWidth) - continue; - col.set_width(self.columnMgr.getColumnWidth(i) / relative); - } - // Triggers column change callback, which saves - self.updateColumns(); - } - else { - this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px"); - self.columnMgr.updated(); - self.updateColumns(); - } - }, enc_column); - } - // Store both nodes in the columnNodes array - this.columnNodes.push({ - "column": column, - "container": cont - }); - } - this._buildSelectCol(); - } - /** - * Builds the select cols column - */ - _buildSelectCol() { - // Build the "select columns" icon - this.selectColIcon = jQuery(document.createElement("span")) - .addClass("selectcols") - .css('display', 'inline-block'); // otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height - // Build the option column - this.selectCol = jQuery(document.createElement("th")) - .addClass("optcol") - .append(this.selectColIcon) - // Toggle display of option popup - .click(this, function (e) { if (e.data.selectColumnsClick) - e.data.selectColumnsClick(e); }) - .appendTo(this.headTr); - this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth() - + this.selectCol.width() + 1); - } - /** - * Builds the inner grid class - */ - _buildGrid() { - // Create the collection of column ids - var colIds = []; - for (var i = 0; i < this.columns.length; i++) { - if (this.columns[i].visible) { - colIds[i] = this.columns[i].id; - } - } - // Create the row provider - if (this.rowProvider) { - this.rowProvider.destroy(); - } - this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds); - // Create the grid class and pass "19" as the starting average row height - this.grid = new et2_dataview_grid(null, null, this.egw, this.rowProvider, 19); - // Insert the grid into the DOM-Tree - var tr = jQuery(this.grid.getFirstNode()); - this.containerTr.replaceWith(tr); - this.containerTr = tr; - } - /* --- Code for calculating the browser/css depending widths --- */ - /** - * Reads the browser dependant variables - */ - _getDepVars() { - if (typeof this.scrollbarWidth === 'undefined') { - // Clone the table and attach it to the outer body tag - var clone = this.table.clone(); - jQuery(egw.top.document.getElementsByTagName("body")[0]) - .append(clone); - // Read the scrollbar width - this.scrollbarWidth = this.constructor.prototype.scrollbarWidth = - this._getScrollbarWidth(clone); - // Read the header border width - this.headerBorderWidth = this.constructor.prototype.headerBorderWidth = - this._getHeaderBorderWidth(clone); - // Read the column border width - this.columnBorderWidth = this.constructor.prototype.columnBorderWidth = - this._getColumnBorderWidth(clone); - // Remove the cloned DOM-Node again from the outer body - clone.remove(); - } - } - /** - * Reads the scrollbar width - */ - _getScrollbarWidth(_table) { - // Create a temporary td and two divs, which are inserted into the - // DOM-Tree. The outer div has a fixed size and "overflow" set to auto. - // When the second div is inserted, it will be forced to display a scrollbar. - var div_inner = jQuery(document.createElement("div")) - .css("height", "1000px"); - var div_outer = jQuery(document.createElement("div")) - .css("height", "100px") - .css("width", "100px") - .css("overflow", "auto") - .append(div_inner); - var td = jQuery(document.createElement("td")) - .append(div_outer); - // Store the scrollbar width statically. - jQuery("tbody tr", _table).append(td); - var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth()); - // Remove the elements again - div_outer.remove(); - return width; - } - /** - * Calculates the total width of the header column border - */ - _getHeaderBorderWidth(_table) { - // Create a temporary th which is appended to the outer thead row - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer"); - var th = jQuery(document.createElement("th")) - .append(cont); - // Insert the th into the document tree - jQuery("thead tr", _table).append(th); - // Calculate the total border width - var width = th.outerWidth(true) - cont.width(); - // Remove the appended element again - th.remove(); - return width; - } - /** - * Calculates the total width of the column border - */ - _getColumnBorderWidth(_table) { - // Create a temporary th which is appended to the outer thead row - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer"); - var td = jQuery(document.createElement("td")) - .append(cont); - // Insert the th into the document tree - jQuery("tbody tr", _table).append(td); - // Calculate the total border width - _table.addClass("egwGridView_grid"); - var width = td.outerWidth(true) - cont.width(); - // Remove the appended element again - td.remove(); - return width; - } -} -//# sourceMappingURL=et2_dataview.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_controller.js b/api/js/etemplate/et2_dataview_controller.js deleted file mode 100644 index b44d14621c..0000000000 --- a/api/js/etemplate/et2_dataview_controller.js +++ /dev/null @@ -1,834 +0,0 @@ -/** - * EGroupware eTemplate2 - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -import { et2_dataview_selectionManager } from "./et2_dataview_controller_selection"; -import { et2_dataview_row } from "./et2_dataview_view_row"; -import { et2_arrayIntKeys, et2_bounds } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -import { egwBitIsSet } from "../egw_action/egw_action_common.js"; -import { EGW_AO_STATE_NORMAL, EGW_AO_STATE_SELECTED } from "../egw_action/egw_action_constants.js"; -/** - * The fetch timeout specifies the time during which the controller tries to - * consolidate requests for rows. - */ -export const ET2_DATAVIEW_FETCH_TIMEOUT = 50; -export const ET2_DATAVIEW_STEPSIZE = 50; -/** - * The et2_dataview_controller class is the intermediate layer between a grid - * instance and the corresponding data source. It manages updating the grid, - * as well as inserting and deleting rows. - */ -export class et2_dataview_controller { - /** - * Constructor of the et2_dataview_controller, connects to the grid - * callback. - * - * @param _grid is the grid the controller should controll. - * @param _rowCallback is the callback function that gets called when a row - * is requested. - * @param _linkCallback is the callback function that gets called for - * requesting action links for a row. The row data, the index of the row and - * the uid are passed as parameters to the function. - * uid is passed to the function. - * @param _actionObjectManager is the object that manages the action - * objects. - */ - constructor(_parentController, _grid) { - this._indexMap = {}; - // Copy the given arguments - this._parentController = _parentController; - this._grid = _grid; - // Initialize list of child controllers - this._children = []; - // Initialize the "index map" which contains all currently displayed - // containers hashed by the "index" - this._indexMap = {}; - // Timer used for queing fetch requests - this._queueTimer = null; - // Array which contains all currently queued row indices in the form of - // an associative array - this._queue = {}; - // Current concurrent requests we have - this._request_queue = []; - // Register the dataFetch callback - this._grid.setDataCallback(this._gridCallback, this); - // Record the child - if (this._parentController != null) { - this._parentController._children.push(this); - } - } - destroy() { - // Destroy the selection manager - this._selectionMgr.destroy(); - // Clear the selection timeout - this._clearTimer(); - // Remove the child from the child list - if (this._parentController != null) { - var idx = this._parentController._children.indexOf(this); - if (idx >= 0) { - // This element is no longer parent of the child - this._parentController._children.splice(idx, 1); - this._parentController = null; - } - } - this._grid = null; - } - /** - * @param value is an object implementing the et2_IDataProvider - * interface - */ - setDataProvider(value) { - this._dataProvider = value; - } - setRowCallback(value) { - this._rowCallback = value; - } - setLinkCallback(value) { - this._linkCallback = value; - } - /** - * @param value is the context in which the _rowCallback and the - * _linkCallback are called. - */ - setContext(value) { - this._context = value; - } - setActionObjectManager(_actionObjectManager) { - if (this._selectionMgr) { - this._selectionMgr.destroy(); - } - // Create the selection manager - this._selectionMgr = new et2_dataview_selectionManager(this._parentController ? this._parentController._selectionMgr : null, this._indexMap, _actionObjectManager, this._selectionFetchRange, this._makeIndexVisible, this); - } - /** - * The update function queries the server for changes in the currently - * managed index range -- those changes are then merged into the current - * view without a complete rebuild of every row. - * - * @param {boolean} clear Skip the fancy stuff, dump everything and start again. - * Completely clears the grid and selection. - */ - update(clear) { - // Avoid update after destroy - // Happens sometimes if AJAX response comes after etemplate unload - if (!this._grid) - return; - // --------- - // TODO: Actually stuff here should be done if the server responds that - // there at all were some changes (needs implementation of "refresh") - // Tell the grid not to try and update itself while we do this - this._grid.doInvalidate = false; - if (clear) { - // Scroll to top - this._grid.makeIndexVisible(0); - this._grid.clear(); - // Free selection manager - this._selectionMgr.clear(); - // Clear object manager - this._objectManager.clear(); - // Clear the map - this._indexMap = {}; - // Update selection manager, it uses this by reference - this._selectionMgr.setIndexMap(this._indexMap); - // Clear the queue - this._queue = {}; - // Invalidate the change detection, re-fetches any known rows - this._lastModification = 0; - } - // Remove all rows which are outside the view range - this._grid.cleanup(); - // Get the currently visible range from the grid - var range = this._grid.getIndexRange(); - // Force range.top and range.bottom to contain an integer - if (range.top === false) { - range.top = range.bottom = 0; - } - this._request_queue = []; - // Require that range from the server - this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true); - } - /** - * Rebuilds the complete grid. - */ - reset() { - // Throw away all internal mappings and reset the timestamp - this._indexMap = {}; - // Update selection manager, it uses this by reference - this._selectionMgr.setIndexMap(this._indexMap); - // Clear the grid - this._grid.clear(); - // Clear the row queue - this._queue = {}; - // Reset the request queue - this._request_queue = []; - // Update the data - this.update(); - } - /** - * Loads the initial order. Do not call multiple times. - */ - loadInitialOrder(order) { - for (var i = 0; i < order.length; i++) { - this._getIndexEntry(i).uid = order[i]; - } - } - /** - * Load initial data - * - * @param {string} uid_key Name of the unique row identifier field - * @param {Object} data Key / Value mapping of initial data. - */ - loadInitialData(uid_prefix, uid_key, data) { - var idx = 0; - for (var key in data) { - // Skip any extra keys - if (typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") - continue; - // Add to row / uid map - var entry = this._getIndexEntry(idx++); - entry.uid = data[key][uid_key] + ""; - if (entry.uid.indexOf(uid_prefix) < 0) { - entry.uid = uid_prefix + "::" + entry.uid; - } - // Add to data cache so grid will find it - egw.dataStoreUID(entry.uid, data[key]); - // Don't try to insert the rows, grid will do that automatically - } - if (idx == 0) { - // No rows, start with an empty - this._selectionMgr.clear(); - this._emptyRow(this._grid._total == 0); - } - } - /** - * Returns the depth of the controller instance. - */ - getDepth() { - if (this._parentController) { - return this._parentController.getDepth() + 1; - } - return 0; - } - /** - * Set the data cache prefix - * The default is to use appname, but if you need to set it explicitly to - * something else to avoid conflicts. Use the same prefix everywhere for - * each type of data. eg. infolog for infolog entries, even if accessed via addressbook - */ - setPrefix(prefix) { - this.dataStorePrefix = prefix; - } - /** - * Returns the row information of the passed node, or null if not available - * - * @param {DOMNode} node - * @return {string|false} UID, or false if not found - */ - getRowByNode(node) { - // Whatever the node, find a TR - var row_node = jQuery(node).closest('tr'); - var row = null; - // Check index map - simple case - var indexed = this._getIndexEntry(row_node.index()); - if (indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) { - row = indexed; - } - else { - // Check whole index map - for (var index in this._indexMap) { - indexed = this._indexMap[index]; - if (indexed && indexed.row && indexed.row.getDOMNode() == row_node[0]) { - row = indexed; - break; - } - } - } - // Check children - for (var i = 0; !row && i < this._children.length; i++) { - var child_row = this._children[i].getRowByNode(node); - if (child_row !== false) - row = child_row; - } - if (row && !row.controller) { - row.controller = this; - } - return row; - } - /** - * Returns the current "total" count. - */ - getTotalCount() { - return this._grid.getTotalCount(); - } - /* -- PRIVATE FUNCTIONS -- */ - _getIndexEntry(_idx) { - // Create an entry in the index map if it does not exist yet - if (typeof this._indexMap[_idx] === "undefined") { - this._indexMap[_idx] = { - "row": null, - "uid": null - }; - } - // Always update the index of the entries before returning them. This is - // neccessary, as when we remove the uid from an entry without row, its - // index does not get updated any further - this._indexMap[_idx]["idx"] = _idx; - return this._indexMap[_idx]; - } - /** - * Inserts a new data row into the grid. index and uid are derived from the - * given management entry. If the data for the given uid does not exist yet, - * a "loading" placeholder will be shown instead. The function will do - * nothing if there already is a row associated to the entry. This function - * will not re-insert a row if the entry already had a row. - * - * @param _entry is the management entry for the index the row will be - * displayed at. - * @param _update specifies whether the row should be updated if _entry.row - * already exists. - * @return true, if all data for the row has been available, false - * otherwise. - */ - _insertDataRow(_entry, _update) { - // Abort if the entry already has a row but the _insert flag is not set - if (_entry.row && !_update) { - return true; - } - // Context used for the callback functions - var ctx = { "self": this, "entry": _entry }; - // Create a new row instance, if it does not exist yet - var createdRow = false; - if (!_entry.row) { - createdRow = true; - _entry.row = this._createRow(ctx); - _entry.row.setDestroyCallback(this._destroyCallback, ctx); - } - // Load the row data if we have a uid for the entry - this.hasData = false; // Gets updated by the _dataCallback - if (_entry.uid) { - // Register the callback / immediately load the data - this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback, ctx); - } - // Display the loading "row prototype" if we don't have data for the row - if (!this.hasData) { - // Get the average height, the "-5" derives from the td padding - var avg = Math.round(this._grid.getAverageHeight() - 5) + "px"; - var prototype = this._grid.getRowProvider().getPrototype("loading"); - jQuery("div", prototype).css("height", avg); - var node = _entry.row.getJNode(); - node.empty(); - node.append(prototype.children()); - } - // Insert the row into the table -- the same row must never be inserted - // twice into the grid, so this function only executes the following - // code only if it is a newly created row. - if (createdRow && _entry.row) { - this._grid.insertRow(_entry.idx, _entry.row); - } - // Remove 'No matches found' row - var row = jQuery(".egwGridView_empty", this._grid.innerTbody).remove(); - if (row.length) { - this._selectionMgr.unregisterRow("", 0); - } - // Update index map only for push (autorefresh disabled) - if (this._indexMap[_entry.idx] && this._indexMap[_entry.idx].uid !== _entry.uid) { - let max = parseInt(Object.keys(this._indexMap).reduce((a, b) => this._indexMap[a] > this._indexMap[b] ? a : b)); - for (let idx = max; idx >= _entry.idx; idx--) { - let entry = this._indexMap[idx]; - this._indexMap[idx].idx = idx + 1; - this._indexMap[this._indexMap[idx].idx] = this._indexMap[idx]; - if (this._selectionMgr && this._selectionMgr._registeredRows[entry.uid]) { - this._selectionMgr._registeredRows[entry.uid].idx = entry.idx; - } - } - } - this._indexMap[_entry.idx] = _entry; - return this.hasData; - } - /** - * Create a new row. - * - * @param {type} ctx - * @returns {et2_dataview_container} - */ - _createRow(ctx) { - return new et2_dataview_row(this._grid); - } - /** - * Function which gets called by the grid when data is requested. - * - * @param _idxStart is the index of the first row for which data is - * requested. - * @param _idxEnd is the index of the last requested row. - */ - _gridCallback(_idxStart, _idxEnd) { - var needsData = false; - // Iterate over all elements the dataview requested and create a row - // which indicates that we are currently loading data - for (var i = _idxStart; i <= _idxEnd; i++) { - var entry = this._getIndexEntry(i); - // Insert the row for the entry -- do not update rows which are - // already existing, as we do not have new data for those. - if (!this._insertDataRow(entry, false) && needsData === false) { - needsData = i; - } - } - // Queue fetching that data range - if (needsData !== false) { - this._queueFetch(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false); - } - } - /** - * The _queueFetch function is used to queue a fetch request. - * TODO: Refresh is currently not used - */ - _queueFetch(_range, _direction, _isUpdate) { - // Force immediate to be false - _isUpdate = _isUpdate ? _isUpdate : false; - // Push the requests onto the request queue - var start = Math.max(0, _range.top); - var end = Math.min(this._grid.getTotalCount(), _range.bottom); - for (var i = start; i < end; i++) { - if (typeof this._queue[i] === "undefined") { - this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current - } - } - // Start the queue timer, if this has not already been done - if (this._queueTimer === null && !_isUpdate) { - var self = this; - egw.debug('log', 'Dataview queue: ', _range); - this._queueTimer = window.setTimeout(function () { - self._flushQueue(false); - }, ET2_DATAVIEW_FETCH_TIMEOUT); - } - if (_isUpdate) { - this._flushQueue(true); - } - } - /** - * Flushes the queue. - */ - _flushQueue(_isUpdate) { - // Clear any still existing timer - this._clearTimer(); - // Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE - var marked = {}; - var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2); - var total = this._grid.getTotalCount(); - for (var key in this._queue) { - if (this._queue[key] > 1) - continue; - key = parseInt(key); - var b = Math.max(0, key - r + (r * this._queue[key])); - var t = Math.min(key + r + (r * this._queue[key]), total - 1); - var c = 0; - for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i++) { - if (typeof this._queue[i] == "undefined" - || this._queue[i] <= 1) { - this._queue[i] = 2; // Stage 2 -- pending or available - marked[i] = true; - c++; - } - } - } - // Create a list with start indices and counts - var fetchList = []; - var entry = null; - var last = 0; - // Get the int keys and sort the array numeric - var arr = et2_arrayIntKeys(marked).sort(function (a, b) { return a > b ? 1 : (a == b ? 0 : -1); }); - for (var i = 0; i < arr.length; i++) { - if (i == 0 || arr[i] - last > 1) { - if (entry) { - fetchList.push(entry); - } - entry = { - "start": arr[i], - "count": 1 - }; - } - else { - entry.count++; - } - last = arr[i]; - } - if (entry) { - fetchList.push(entry); - } - // Special case: If there are no entries in the fetch list and this is - // an update, create an dummy entry, so that we'll get the current count - if (fetchList.length === 0 && _isUpdate) { - fetchList.push({ - "start": 0, "count": 0 - }); - // Disable grid invalidate, or it might request again before we're done - this._grid.doInvalidate = false; - } - egw.debug("log", "Dataview flush", fetchList); - // Execute all queries - for (var i = 0; i < fetchList.length; i++) { - // Build the query - var query = { - "start": fetchList[i].start, - "num_rows": fetchList[i].count, - "refresh": false - }; - // Context used in the callback function - var ctx = { - "self": this, - "start": query.start, - "count": query.num_rows, - "lastModification": this._lastModification, - prefix: undefined - }; - if (this.dataStorePrefix) { - ctx.prefix = this.dataStorePrefix; - } - this._queueRequest(query, ctx); - } - } - /** - * Queue a request for data - * @param {Object} query - * @param {Object} ctx - */ - _queueRequest(query, ctx) { - this._request_queue.push({ - query: query, - context: ctx, - // Start pending, set to 1 when request sent - status: 0 - }); - this._fetchQueuedRequest(); - } - /** - * Fetch data for a queued request, subject to rate limit - */ - _fetchQueuedRequest() { - // Check to see if there's room - var count = 0; - for (var i = 0; i < this._request_queue.length; i++) { - if (this._request_queue[i].status > 0) - count++; - } - // Too many requests, will try again after response is received - if (count >= et2_dataview_controller.CONCURRENT_REQUESTS || this._request_queue.length === 0) { - return; - } - // Keep at least 1 previous pending - var keep = 1; - // The most recent is the one the user's most interested in - var request = null; - for (var i = this._request_queue.length - 1; i >= 0; i--) { - // Only interested in pending requests (status 0) - if (this._request_queue[i].status != 0) { - continue; - } - if (request == null) { - request = this._request_queue[i]; - } - else if (keep > 0) { - keep--; - } - else if (keep <= 0) { - // Cancel pending, they've probably scrolled past. - this._request_queue.splice(i, 1); - } - } - if (request == null) - return; - // Request being sent - request.status = 1; - // Call the callback - this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context); - } - _clearTimer() { - // Reset the queue timer upon destruction - if (this._queueTimer) { - window.clearTimeout(this._queueTimer); - this._queueTimer = null; - } - } - /** - * Called by the data source when the data changes - * - * @param _data Object|null New data, or null. Null will remove the row. - */ - _dataCallback(_data) { - // Set the "hasData" flag - this.self.hasData = true; - // Call the row callback with the new data -- the row callback then - // generates the row DOM nodes that will be inserted into the grid - if (this.self._rowCallback) { - // Remove everything from the current row - this.entry.row.clear(); - // If there's no data, stop - if (typeof _data == "undefined" || _data == null) { - this.self._destroyCallback.call(this, this.entry.row); - return; - } - // Fill the row DOM Node with data - this.self._rowCallback.call(this.self._context, _data, this.entry.row, this.entry.idx, this.entry); - // Attach the "subgrid" tag to the row, if the depth of this - // controller is larger than zero - var tr = this.entry.row.getDOMNode(); - var d = this.self.getDepth(); - if (d > 0) { - jQuery(tr).addClass("subentry"); - jQuery("td:first", tr).children("div").last().addClass("level_" + d + " indentation"); - if (this.entry.idx == 0) { - // Set the CSS for the level - required so columns line up - var indent = jQuery("").appendTo('body'); - egw.css(".subentry td div.innerContainer.level_" + d, "margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px"); - indent.remove(); - } - } - var links = null; - // Look for a flag in the row to avoid actions. Use for sums or extra header rows. - if (!_data.no_actions) { - // Get the action links if the links callback is set - if (this.self._linkCallback) { - links = this.self._linkCallback.call(this.self._context, _data, this.entry.idx, this.entry.uid); - } - // Register the row in the selection manager - this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx, tr, links); - } - else { - // Remember that - this.entry.no_actions = true; - } - // Invalidate the current row entry - this.entry.row.invalidate(); - } - } - /** - * - */ - _destroyCallback(_row) { - // Unregister the row from the selection manager, if not selected - // If it is selected, leave it there - allows selecting rows and scrolling - var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid); - if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED)) { - var tr = this.entry.row.getDOMNode(); - this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL); - this.self._selectionMgr.unregisterRow(this.entry.uid, tr); - } - // There is no further row connected to the entry - this.entry.row = null; - // Unregister the data callback - this.self._dataProvider.dataUnregisterUID(this.entry.uid, this.self._dataCallback, this); - } - /** - * Returns an array containing "_count" index mapping entries starting from - * the index given in "_start". - */ - _getIndexMapping(_start, _count) { - var result = []; - for (var i = _start; i < _start + _count; i++) { - result.push(this._getIndexEntry(i)); - } - return result; - } - /** - * Updates the grid according to the new order. The function simply does the - * following: It iterates along the new order (given in _order) and the old - * order given in _idxMap. Iteration variables used are - * a) i -- points to the current entry in _order - * b) idx -- points to the current grid row that will be effected by - * this operation. - * c) mapIdx -- points to the current entry in _indexMap - * The following cases may occur: - * a) The current entry in the old order has no uid or no row -- in that - * case the row at the current position is simply updated, - * the old pointer will be incremented. - * b) The two uids differ -- insert a new row with the new uid, do not - * increment the old pointer. - * c) The two uids are the same -- increment the old pointer. - * In a last step all rows that are left in the old order are deleted. All - * newly created index entries are returned. This function does not update - * the internal mapping in _idxMap. - */ - _updateOrder(_start, _count, _idxMap, _order) { - // The result contains the newly created index map entries which have to - // be merged with the result - var result = []; - // Iterate over the new order - var mapIdx = 0; - var idx = _start; - for (var i = 0; i < _order.length; i++, idx++) { - var current = _idxMap[mapIdx]; - if (!current.row || !current.uid) { - // If there is no row yet at the current position or the uid - // of that entry is unknown, simply update the entry. - current.uid = _order[i]; - current.idx = idx; - // Only update the row, if it is displayed (e.g. has a "loading" - // row displayed) -- this is needed for prefetching - if (current.row) { - this._insertDataRow(current, true); - } - mapIdx++; - } - else if (current.uid !== _order[i]) { - // Insert a new row at the new position - var entry = { - "idx": idx, - "uid": _order[i], - "row": null - }; - this._insertDataRow(entry, true); - // Remember the new entry - result.push(entry); - } - else { - // Do nothing, the uids do not differ, just update the index of - // the element - current.idx = idx; - mapIdx++; - } - } - // Delete as many rows as we have left, invalidate the corresponding - // index entry - for (var i = mapIdx; i < _idxMap.length; i++) { - if (typeof _idxMap[i] != 'undefined') { - _idxMap[i].uid = null; - } - } - return result; - } - _mergeResult(_newEntries, _invalidStartIdx, _diff, _total) { - if (_newEntries.length > 0 || _diff > 0) { - // Create a new index map - var newMap = {}; - // Insert all new entries into the new index map - for (var i = 0; i < _newEntries.length; i++) { - newMap[_newEntries[i].idx] = _newEntries[i]; - } - // Merge the old map with all old entries - for (var key in this._indexMap) { - // Get the corresponding index entry - var entry = this._indexMap[key]; - // Calculate the new index -- if rows were deleted, we'll - // have to adjust the index - var newIdx = entry.idx >= _invalidStartIdx - ? entry.idx - _diff : entry.idx; - if (newIdx >= 0 && newIdx < _total - && typeof newMap[newIdx] === "undefined") { - entry.idx = newIdx; - newMap[newIdx] = entry; - } - else if (newMap[newIdx] !== this._indexMap[key]) { - // Make sure the old entry gets invalidated - entry.idx = null; - entry.row = null; - } - } - // Make the new index map the current index map - this._indexMap = newMap; - this._selectionMgr.setIndexMap(newMap); - } - } - _fetchCallback(_response) { - // Remove answered request from queue - var request = null; - for (var i = 0; i < this.self._request_queue.length; i++) { - if (this.self._request_queue[i].context == this) { - request = this.self._request_queue[i]; - this.self._request_queue.splice(i, 1); - break; - } - } - this.self._lastModification = _response.lastModification; - // Do nothing if _response.order evaluates to false - if (!_response.order) { - return; - } - // Make sure _response.order.length is not longer than the requested - // count, if a specific count was requested - var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order; - // Remove from queue, or it will not be fetched again - if (_response.total < this.count) { - // Less rows than we expected - // Clear the queue, or the remnants will never be loaded again - this.self._queue = {}; - } - else { - for (var i = this.start; i < this.start + order.length; i++) - delete this.self._queue[i]; - } - // Get the current index map for the updated region - var idxMap = this.self._getIndexMapping(this.start, order.length); - // Update the grid using the new order. The _updateOrder function does - // not update the internal mapping while inserting and deleting rows, as - // this would move us to another asymptotic runtime level. - var res = this.self._updateOrder(this.start, this.count, idxMap, order); - // Merge the new indices, update all indices with rows that were not - // affected and invalidate all indices if there were changes - this.self._mergeResult(res, this.start + order.length, idxMap.length - order.length, _response.total); - if (_response.total == 0) { - this.self._emptyRow(true); - } - else { - var row = jQuery(".egwGridView_empty", this.self._grid.innerTbody).remove(); - this.self._selectionMgr.unregisterRow("", 0, row.get(0)); - } - // Now it's OK to invalidate, if it wasn't before - this.self._grid.doInvalidate = true; - // Update the total element count in the grid - this.self._grid.setTotalCount(_response.total); - this.self._selectionMgr.setTotalCount(_response.total); - // Schedule an invalidate, in case total is the same - this.self._grid.invalidate(); - // Check if requests are waiting - this.self._fetchQueuedRequest(); - } - /** - * Insert an empty / placeholder row when there is no data to display - */ - _emptyRow(_noRows) { - var noRows = !_noRows ? false : true; - jQuery(".egwGridView_empty", this._grid.innerTbody).remove(); - if (typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty")) { - var placeholder = this._grid._rowProvider.getPrototype("empty"); - if (jQuery("td", placeholder).length == 1) { - jQuery("td", placeholder).css("width", this._grid.outerCell.width() + "px"); - } - placeholder.appendTo(this._grid.innerTbody); - // Register placeholder action only if no rows - if (noRows) { - // Get the action links if the links callback is set - var links = null; - if (this._linkCallback) { - links = this._linkCallback.call(this._context, {}, 0, ""); - } - this._selectionMgr.registerRow("", 0, placeholder.get(0), links); - } - } - } - /** - * Callback function used by the selection manager to translate the selected - * range to uids. - */ - _selectionFetchRange(_range, _callback, _context) { - this._dataProvider.dataFetch({ "start": _range.top, "num_rows": _range.bottom - _range.top + 1, - "no_data": true }, function (_response) { - _callback.call(_context, _response.order); - }, _context); - } - /** - * Tells the grid to make the given index visible. - */ - _makeIndexVisible(_idx) { - this._grid.makeIndexVisible(_idx); - } -} -// Maximum concurrent data requests. Additional ones are held in the queue. -et2_dataview_controller.CONCURRENT_REQUESTS = 5; -//# sourceMappingURL=et2_dataview_controller.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_controller_selection.js b/api/js/etemplate/et2_dataview_controller_selection.js deleted file mode 100644 index 3e8d0cf740..0000000000 --- a/api/js/etemplate/et2_dataview_controller_selection.js +++ /dev/null @@ -1,525 +0,0 @@ -/** - * EGroupware eTemplate2 - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - et2_dataview_view_aoi; - - egw_action.egw_keymanager; -*/ -import { egw } from "../jsapi/egw_global"; -import { et2_bounds } from "./et2_core_common"; -import { et2_dialog } from "./et2_widget_dialog"; -import { et2_createWidget } from "./et2_core_widget"; -import { et2_dataview_rowAOI } from "./et2_dataview_view_aoi"; -import { egwActionObjectInterface } from "../egw_action/egw_action.js"; -import { EGW_AO_SHIFT_STATE_BLOCK, EGW_AO_SHIFT_STATE_MULTI, EGW_AO_STATE_FOCUSED, EGW_AO_STATE_NORMAL, EGW_AO_STATE_SELECTED } from "../egw_action/egw_action_constants.js"; -import { egwBitIsSet, egwSetBit } from "../egw_action/egw_action_common.js"; -/** - * The selectioManager is internally used by the et2_dataview_controller class - * to manage the row selection. - * As the action system does not allow selection of entries which are currently - * not in the dom tree, we have to manage this in this class. The idea is to - * manage an external action object interface for each visible row and proxy all - * state changes between an dummy action object, that does no selection handling, - * and the external action object interface. - * - * @augments Class - */ -export class et2_dataview_selectionManager { - /** - * Constructor - * - * @param _parent - * @param _indexMap - * @param _actionObjectManager - * @param _queryRangeCallback - * @param _makeVisibleCallback - * @param _context - * @memberOf et2_dataview_selectionManager - */ - constructor(_parent, _indexMap, _actionObjectManager, _queryRangeCallback, _makeVisibleCallback, _context) { - // Copy the arguments - this._parent = _parent; - this._indexMap = _indexMap; - this._actionObjectManager = _actionObjectManager; - this._queryRangeCallback = _queryRangeCallback; - this._makeVisibleCallback = _makeVisibleCallback; - this._context = _context; - // Attach this manager to the parent manager if one is given - if (this._parent) { - this._parent._children.push(this); - } - // Use our selection instead of object manager's to handle not-loaded rows - if (_actionObjectManager) { - this._actionObjectManager.getAllSelected = jQuery.proxy(this.getAllSelected, this); - } - // Internal map which contains all curently selected uids and their - // state - this._registeredRows = {}; - this._focusedEntry = null; - this._invertSelection = false; - this._selectAll = false; - this._inUpdate = false; - this._total = 0; - this._children = []; - // Callback for when the selection changes - this.select_callback = null; - } - destroy() { - // If we have a parent, unregister from that - if (this._parent) { - var idx = this._parent._children.indexOf(this); - this._parent._children.splice(idx, 1); - } - // Destroy all children - for (var i = this._children.length - 1; i >= 0; i--) { - this._children[i].destroy(); - } - // Delete all still registered rows - for (var key in this._registeredRows) { - this.unregisterRow(key, this._registeredRows[key].tr); - } - this.select_callback = null; - } - clear() { - for (var key in this._registeredRows) { - this.unregisterRow(key, this._registeredRows[key].tr); - delete this._registeredRows[key]; - } - if (this._actionObjectManager) { - this._actionObjectManager.clear(); - } - for (key in this._indexMap) { - delete this._indexMap[key]; - } - this._total = 0; - this._focusedEntry = null; - this._invertSelection = false; - this._selectAll = false; - this._inUpdate = false; - } - setIndexMap(_indexMap) { - this._indexMap = _indexMap; - } - setTotalCount(_total) { - this._total = _total; - } - registerRow(_uid, _idx, _tr, _links) { - // Get the corresponding entry from the registered rows array - var entry = this._getRegisteredRowsEntry(_uid); - // If the row has changed unregister the old one and do not delete - // entry from the entry map - if (entry.tr && entry.tr !== _tr) { - this.unregisterRow(_uid, entry.tr, true); - } - // Create the AOI for the tr - if (!entry.tr && _links) { - this._attachActionObjectInterface(entry, _tr, _uid); - this._attachActionObject(entry, _tr, _uid, _links, _idx); - } - // Update the entry - if (entry.ao) - entry.ao._index; - entry.idx = _idx; - entry.tr = _tr; - // Update the visible state of the _tr - this._updateEntryState(entry, entry.state); - } - unregisterRow(_uid, _tr, _noDelete) { - // _noDelete defaults to false - _noDelete = _noDelete ? true : false; - if (typeof this._registeredRows[_uid] !== "undefined" - && this._registeredRows[_uid].tr === _tr) { - this._inUpdate = true; - // Don't leave focusedEntry - // @ts-ignore - if (this._focusedEntry !== null && this._focusedEntry.uid == _uid) { - this.setFocused(_uid, false); - } - this._registeredRows[_uid].tr = null; - this._registeredRows[_uid].aoi = null; - // Remove the action object from its container - if (this._registeredRows[_uid].ao) { - this._registeredRows[_uid].ao.remove(); - this._registeredRows[_uid].ao = null; - } - if (!_noDelete - && this._registeredRows[_uid].state === EGW_AO_STATE_NORMAL) { - delete this._registeredRows[_uid]; - } - this._inUpdate = false; - } - } - resetSelection() { - this._invertSelection = false; - this._selectAll = false; - this._actionObjectManager.setAllSelected(false); - for (var key in this._registeredRows) { - this.setSelected(key, false); - } - for (var i = 0; i < this._children.length; i++) { - this._children[i].resetSelection(); - } - } - setSelected(_uid, _selected) { - this._selectAll = false; - var entry = this._getRegisteredRowsEntry(_uid); - this._updateEntryState(entry, egwSetBit(entry.state, EGW_AO_STATE_SELECTED, _selected)); - } - getAllSelected() { - var selected = this.getSelected(); - return selected.all || (selected.ids.length === this._total); - } - setFocused(_uid, _focused) { - // Reset the state of the currently focused entry - if (this._focusedEntry) { - this._updateEntryState(this._focusedEntry, egwSetBit(this._focusedEntry.state, EGW_AO_STATE_FOCUSED, false)); - this._focusedEntry = null; - } - // Mark the new given uid as focused - if (_focused) { - //console.log('et2_dataview_controller_selection::setFocused -> UID:'+_uid+' is focused by:'+this._actionObjectManager.name); - var entry = this._focusedEntry = this._getRegisteredRowsEntry(_uid); - this._updateEntryState(entry, egwSetBit(entry.state, EGW_AO_STATE_FOCUSED, true)); - } - } - selectAll() { - // Reset the selection - this.resetSelection(); - this._selectAll = true; - // Run as a range if there's less then the max - if (egw.dataKnownUIDs(this._context._dataProvider.dataStorePrefix).length !== this._total && - this._total <= et2_dataview_selectionManager.MAX_SELECTION) { - this._selectRange(0, this._total); - } - // Tell action manager to do all - this._actionObjectManager.setAllSelected(true); - // Update the selection - for (var key in this._registeredRows) { - var entry = this._registeredRows[key]; - this._updateEntryState(entry, entry.state); - } - this._selectAll = true; - } - getSelected() { - // Collect all currently selected ids - var ids = []; - for (var key in this._registeredRows) { - if (egwBitIsSet(this._registeredRows[key].state, EGW_AO_STATE_SELECTED)) { - ids.push(key); - } - } - // Push all events of the child managers onto the list - for (var i = 0; i < this._children.length; i++) { - ids = ids.concat(this._children[i].getSelected().ids); - } - // Return an array containing those ids - // RB: we are currently NOT using "inverted" - return { - //"inverted": this._invertSelection, - "all": this._selectAll, - "ids": ids - }; - } - /** -- PRIVATE FUNCTIONS -- **/ - _attachActionObjectInterface(_entry, _tr, _uid) { - // Create the AOI which is used internally in the selection manager - // this AOI is not connected to the AO, as the selection manager - // cares about selection etc. - _entry.aoi = new et2_dataview_rowAOI(_tr); - _entry.aoi.setStateChangeCallback(function (_newState, _changedBit, _shiftState) { - if (_changedBit === EGW_AO_STATE_SELECTED) { - // Call the select handler - this._handleSelect(_uid, _entry, egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_BLOCK), egwBitIsSet(_shiftState, EGW_AO_SHIFT_STATE_MULTI)); - } - }, this); - } - _getDummyAOI(_entry, _tr, _uid, _idx) { - // Create AOI - var dummyAOI = new egwActionObjectInterface(); - var self = this; - // Handling for Action Implementations updating the state - dummyAOI.doSetState = function (_state) { - if (!self._inUpdate) { - // Update the "focused" flag - self.setFocused(_uid, egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); - // Generally update the state - self._updateState(_uid, _state); - } - }; - // Handle the "make visible" event, pass the request to the parent - // controller - dummyAOI.doMakeVisible = function () { - self._makeVisibleCallback.call(self._context, _idx); - }; - // Connect the the two AOIs - dummyAOI.doTriggerEvent = _entry.aoi.doTriggerEvent; - // Implementation of the getDOMNode function, so that the event - // handlers can be properly bound - dummyAOI.getDOMNode = function () { return _tr; }; - return dummyAOI; - } - _attachActionObject(_entry, _tr, _uid, _links, _idx) { - // Get the dummyAOI which connects the action object to the tr but - // does no selection handling - var dummyAOI = this._getDummyAOI(_entry, _tr, _uid, _idx); - // Create an action object for the tr and connect it to a dummy AOI - if (this._actionObjectManager) { - if (this._actionObjectManager.getObjectById(_uid)) { - var state = _entry.state; - this._actionObjectManager.getObjectById(_uid).remove(); - _entry.state = state; - } - _entry.ao = this._actionObjectManager.addObject(_uid, dummyAOI); - } - // Force context (actual widget) in here, it's the last place it's available - _entry.ao._context = this._context; - _entry.ao.updateActionLinks(_links); - _entry.ao._index = _idx; - // Overwrite some functions like "traversePath", "getNext" and - // "getPrevious" - var self = this; - function getIndexAO(_idx) { - // Check whether the index is in the index map - if (typeof self._indexMap[_idx] !== "undefined" - && self._indexMap[_idx].uid) { - return self._getRegisteredRowsEntry(self._indexMap[_idx].uid).ao; - } - return null; - } - function getElementRelatively(_step) { - var total = self._total || Object.keys(self._indexMap).length; - var max_index = Math.max.apply(Math, Object.keys(self._indexMap)); - // Get a reasonable number of iterations - not all - var count = Math.max(1, Math.min(self._total, 50)); - var element = null; - var idx = _entry.idx; - while (element == null && count > 0 && max_index > 0) { - count--; - element = getIndexAO(Math.max(0, Math.min(max_index, idx += _step))); - } - return element; - } - _entry.ao.getPrevious = function (_step) { - return getElementRelatively(-_step); - }; - _entry.ao.getNext = function (_step) { - return getElementRelatively(_step); - }; - _entry.ao.traversePath = function (_obj) { - // Get the start and the stop index - var s = Math.min(this._index, _obj._index); - var e = Math.max(this._index, _obj._index); - var result = []; - for (var i = s; i < e; i++) { - var ao = getIndexAO(i); - if (ao) { - result.push(ao); - } - } - return result; - }; - } - _updateState(_uid, _state) { - var entry = this._getRegisteredRowsEntry(_uid); - this._updateEntryState(entry, _state); - return entry; - } - _updateEntryState(_entry, _state) { - if (this._selectAll) { - _state |= EGW_AO_STATE_SELECTED; - } - else if (this._invertSelection) { - _state ^= EGW_AO_STATE_SELECTED; - } - // Attach ao if not there, happens for rows loaded for selection, but - // not displayed yet - if (!_entry.ao && _entry.uid && this._actionObjectManager) { - var _links = []; - for (var key in this._registeredRows) { - if (this._registeredRows[key].ao && this._registeredRows[key].ao.actionLinks) { - _links = this._registeredRows[key].ao.actionLinks; - break; - } - } - if (_links.length) { - this._attachActionObjectInterface(_entry, null, _entry.uid); - this._attachActionObject(_entry, null, _entry.uid, _links, _entry.idx); - } - } - // Update the state if it has changed - if ((_entry.aoi && _entry.aoi.getState() !== _state) || _entry.state != _state) { - this._inUpdate = true; // Recursion prevention - // Update the state of the action object - if (_entry.ao) { - _entry.ao.setSelected(egwBitIsSet(_state, EGW_AO_STATE_SELECTED)); - _entry.ao.setFocused(egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); - } - this._inUpdate = false; - // Delete the element if state was set to "NORMAL" and there is - // no tr - if (_state === EGW_AO_STATE_NORMAL && !_entry.tr) { - delete this._registeredRows[_entry.uid]; - } - } - // Update the visual state - if (_entry.aoi && _entry.aoi.doSetState) { - _entry.aoi.doSetState(_state); - } - // Update the state of the entry - _entry.state = _state; - } - _getRegisteredRowsEntry(_uid) { - if (typeof this._registeredRows[_uid] === "undefined") { - this._registeredRows[_uid] = { - "uid": _uid, - "idx": null, - "state": EGW_AO_STATE_NORMAL, - "tr": null, - "aoi": null, - "ao": null - }; - } - return this._registeredRows[_uid]; - } - _handleSelect(_uid, _entry, _shift, _ctrl) { - // If not "_ctrl" is set, reset the selection - if (!_ctrl) { - var top = this; - while (top._parent !== null) { - top = top._parent; - } - top.resetSelection(); - this._actionObjectManager.setAllSelected(false); // needed for hirachical stuff - } - // Mark the element that was clicked as selected - var entry = this._getRegisteredRowsEntry(_uid); - this.setSelected(_uid, !_ctrl || !egwBitIsSet(entry.state, EGW_AO_STATE_SELECTED)); - // Focus the element if shift is not pressed - if (!_shift) { - this.setFocused(_uid, true); - } - else if (this._focusedEntry) { - this._selectRange(this._focusedEntry.idx, _entry.idx); - } - if (this.select_callback && typeof this.select_callback == "function") { - this.select_callback.apply(this._context, arguments); - } - } - _selectRange(_start, _stop) { - // Contains ranges that are not currently in the index map and that have - // to be queried - var queryRanges = []; - // Iterate over the given range and select the elements in the range - // from _start to _stop - var naStart = false; - var s = Math.min(_start, _stop); - var e = Math.max(_stop, _start); - var RANGE_MAX = 50; - var range_break = s + RANGE_MAX; - for (var i = s; i <= e; i++) { - if (typeof this._indexMap[i] !== "undefined" && - this._indexMap[i].uid && egw.dataGetUIDdata(this._indexMap[i].uid)) { - // Add the range to the "queryRanges" - if (naStart !== false) { - queryRanges.push(et2_bounds(naStart, i - 1)); - naStart = false; - range_break += RANGE_MAX; - } - // Select the element, unless flagged for exclusion - // Check for no_actions flag via data - var data = egw.dataGetUIDdata(this._indexMap[i].uid); - if (data && data.data && !data.data.no_actions) { - this.setSelected(this._indexMap[i].uid, true); - } - } - else if (naStart === false) { - naStart = i; - range_break = naStart + RANGE_MAX; - } - else if (i >= range_break) { - queryRanges.push(et2_bounds(naStart ? naStart : s, i - 1)); - naStart = i; - range_break += RANGE_MAX; - } - } - // Add the last range to the "queryRanges" - if (naStart !== false) { - queryRanges.push(et2_bounds(naStart, i - 1)); - naStart = false; - } - // Query all unknown ranges from the server - if (queryRanges.length > 0) { - this._query_ranges(queryRanges); - } - } - _query_ranges(queryRanges) { - var that = this; - var record_count = 0; - var range_index = 0; - var range_count = queryRanges.length; - var cont = true; - var fetchPromise = new Promise(function (resolve) { - resolve(); - }); - // Found after dialog loads - var progressbar; - var parent = et2_dialog._create_parent(); - var dialog = et2_createWidget("dialog", { - callback: - // Abort the long task if they canceled the data load - function () { cont = false; }, - template: egw.webserverUrl + '/api/templates/default/long_task.xet', - message: egw.lang('Loading'), - title: egw.lang('please wait...'), - buttons: [{ "button_id": et2_dialog.CANCEL_BUTTON, "text": egw.lang('cancel'), id: 'dialog[cancel]', image: 'cancel' }], - width: 300 - }, parent); - jQuery(dialog.template.DOMContainer).on('load', function () { - // Get access to template widgets - progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); - }); - for (var i = 0; i < queryRanges.length; i++) { - if (record_count + (queryRanges[i].bottom - queryRanges[i].top + 1) > that.MAX_SELECTION) { - egw.message(egw.lang('Too many rows selected.
Select all, or less than %1 rows', that.MAX_SELECTION)); - break; - } - else { - record_count += (queryRanges[i].bottom - queryRanges[i].top + 1); - fetchPromise = fetchPromise.then((function () { - // Check for abort - if (!cont) - return; - return new Promise(function (resolve) { - that._queryRangeCallback.call(that._context, this, function (_order) { - for (var j = 0; j < _order.length; j++) { - // Check for no_actions flag via data since entry isn't there/available - var data = egw.dataGetUIDdata(_order[j]); - if (!data || data && data.data && !data.data.no_actions) { - var entry = this._getRegisteredRowsEntry(_order[j]); - this._updateEntryState(entry, egwSetBit(entry.state, EGW_AO_STATE_SELECTED, true)); - } - } - progressbar.set_value(100 * (++range_index / range_count)); - resolve(); - }, that); - }.bind(this)); - }).bind(queryRanges[i])); - } - } - fetchPromise.finally(function () { - dialog.destroy(); - }); - } -} -// Maximum number of rows we can safely fetch for selection -// Actual selection may have more rows if we already have some -et2_dataview_selectionManager.MAX_SELECTION = 1000; -//# sourceMappingURL=et2_dataview_controller_selection.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_interfaces.js b/api/js/etemplate/et2_dataview_interfaces.js deleted file mode 100644 index 42caa9d19f..0000000000 --- a/api/js/etemplate/et2_dataview_interfaces.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * EGroupware eTemplate2 - Contains interfaces used inside the dataview - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - et2_core_inheritance; -*/ -import { implements_methods, et2_implements_registry } from "./et2_core_interfaces"; -export const et2_dataviewIInvalidatable = "et2_dataview_IInvalidatable"; -et2_implements_registry.et2_dataview_IInvalidatable = function (obj) { - return implements_methods(obj, ["invalidate"]); -}; -export const et2_dataview_IViewRange = "et2_dataview_IViewRange"; -et2_implements_registry.et2_dataview_IViewRange = function (obj) { - return implements_methods(obj, ["setViewRange"]); -}; -export const et2_IDataProvider = "et2_IDataProvider"; -et2_implements_registry.et2_IDataProvider = function (obj) { - return implements_methods(obj, ["dataFetch", "dataRegisterUID", "dataUnregisterUID"]); -}; -//# sourceMappingURL=et2_dataview_interfaces.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_model_columns.js b/api/js/etemplate/et2_dataview_model_columns.js deleted file mode 100644 index 2d4e7f8e5c..0000000000 --- a/api/js/etemplate/et2_dataview_model_columns.js +++ /dev/null @@ -1,416 +0,0 @@ -/** - * EGroupware eTemplate2 - Class which contains a the columns model - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - et2_core_inheritance; - et2_inheritance; -*/ -import { egw } from "../jsapi/egw_global"; -/** - * Class which stores the data of a single column. - * - * @augments Class - */ -export class et2_dataview_column { - /** - * Constructor - */ - constructor(_attrs) { - /** - * Defines the visibility state of this column. - */ - this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE; - this.caption = ''; - /** - * Column type - Type of the column - * - * One of ET2_COL_TYPE_DEFAULT or ET2_COL_TYPE_NAME_ICON_FIXED - */ - this.type = et2_dataview_column.ET2_COL_TYPE_DEFAULT; - /** - * Width of the column - */ - this.width = 80; - /** - * Maximum width of the column - */ - this.maxWidth = 0; - /** - * Minimum width of the column, in pixels. Values below this are rejected. - */ - this.minWidth = 20; - this.id = _attrs.id; - if (typeof _attrs.visibility !== "undefined") { - this.visibility = _attrs.visibility; - } - this.caption = _attrs.caption; - if (typeof _attrs.type !== "undefined") { - this.type = _attrs.type; - } - if (typeof _attrs.width !== "undefined") { - this.set_width(_attrs.width); - } - if (typeof _attrs.maxWidth !== "undefined") { - this.maxWidth = _attrs.maxWidth; - } - if (typeof _attrs.minWidth !== "undefined") { - this.minWidth = _attrs.minWidth; - } - } - /** - * Set the column width - * - * Posible value types are: - * 1. "100" => fixedWidth 100px - * 2. "100px" => fixedWidth 100px - * 3. "50%" => relativeWidth 50% - * 4. 0.5 => relativeWidth 50% - * - * @param {float|string} _value - */ - set_width(_value) { - // Parse the width parameter. - this.relativeWidth = false; - this.fixedWidth = false; - var w = _value; - if (typeof w == 'number') { - this.relativeWidth = parseFloat(w.toFixed(3)); - } - else if (w.charAt(w.length - 1) == "%" && !isNaN(w.substr(0, w.length - 1))) { - this.relativeWidth = parseInt(w.substr(0, w.length - 1)) / 100; - // Relative widths with more than 100% are not allowed! - if (this.relativeWidth > 1) { - this.relativeWidth = false; - } - } - else if (w.substr(w.length - 2, 2) == "px" && !isNaN(w.substr(0, w.length - 2))) { - this.fixedWidth = parseInt(w.substr(0, w.length - 2)); - } - else if (typeof w == 'string' && !isNaN(parseFloat(w))) { - this.fixedWidth = parseInt(w); - } - } - set_visibility(_value) { - // If visibility is always, don't turn it off - if (this.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS || this.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) - return; - if (_value === true) { - this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE; - } - else if (_value === false) { - this.visibility = et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; - } - else if (typeof _value == "number") { - this.visibility = _value; - } - else { - egw().debug("warn", "Invalid visibility option for column: ", _value); - } - } -} -et2_dataview_column.ET2_COL_TYPE_DEFAULT = 0; -et2_dataview_column.ET2_COL_TYPE_NAME_ICON_FIXED = 1; -et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS = 0; -et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE = 1; -et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE = 2; -et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT = 3; -et2_dataview_column.ET2_COL_VISIBILITY_DISABLED = 4; -et2_dataview_column._attributes = { - "id": { - "name": "ID", - "type": "string", - "description": "Unique identifier for this column. It is used to " + - "store changed column widths or visibilities." - }, - "visibility": { - "name": "Visibility", - "type": "integer", - "default": et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE, - "description": "Defines the visibility state of this column." - }, - "caption": { - "name": "Caption", - "type": "string", - "description": "Caption of the column as it is displayed in the " + - "select columns popup." - }, - "type": { - "name": "Column type", - "type": "integer", - "default": et2_dataview_column.ET2_COL_TYPE_DEFAULT, - "description": "Type of the column" - }, - "width": { - "name": "Width", - "type": "dimension", - "default": "80px", - "description": "Width of the column." - }, - "minWidth": { - "name": "Minimum width", - "type": "integer", - "default": 20, - "description": "Minimum width of the column, in pixels. Values below this are rejected." - }, - "maxWidth": { - "name": "Maximum width", - "type": "integer", - "default": 0, - "description": "Maximum width of the column" - } -}; -/** - * Contains logic for the columns class. The columns class represents the unique set - * of columns a grid view owns. The parameters of the columns (except for visibility) - * do normaly not change. - */ -export class et2_dataview_columns { - constructor(_columnData) { - // Initialize some variables - this._totalWidth = 0; - this._totalFixed = 0; - this.columnWidths = []; - // Create the columns object - this.columns = new Array(_columnData.length); - for (var i = 0; i < _columnData.length; i++) { - this.columns[i] = new et2_dataview_column(_columnData[i]); - } - this._updated = true; - } - destroy() { - // Free all column objects - for (var i = 0; i < this.columns.length; i++) { - this.columns[i] = null; - } - } - updated() { - this._updated = true; - } - columnCount() { - return this.columns.length; - } - get totalWidth() { - return this._totalWidth; - } - get totalFixed() { - return this._totalFixed ? this._totalFixed : 0; - } - /** - * Set the total width of the header row - * - * @param {(string|number)} _width - */ - setTotalWidth(_width) { - if (_width != this._totalWidth && _width > 0) { - this._totalWidth = _width; - this._updated = true; - } - } - /** - * Returns the index of the colum with the given id - * - * @param {string} _id - */ - getColumnIndexById(_id) { - for (var i = 0; i < this.columns.length; i++) { - if (this.columns[i].id == _id) { - return i; - } - } - return -1; - } - /** - * Returns the column with the given id - * - * @param {string} _id - */ - getColumnById(_id) { - var idx = this.getColumnIndexById(_id); - return (idx == -1) ? null : this.columns[idx]; - } - /** - * Returns the width of the column with the given index - * - * @param {number} _idx - */ - getColumnWidth(_idx) { - if (this._totalWidth > 0 && _idx >= 0 && _idx < this.columns.length) { - // Recalculate the column widths if something has changed. - if (this._updated) { - this._calculateWidths(); - this._updated = false; - } - // Return the calculated width for the column with the given index. - return this.columnWidths[_idx]; - } - return 0; - } - /** - * Returns an array containing the width of the column and its visibility - * state. - */ - getColumnData() { - var result = []; - for (var i = 0; i < this.columns.length; i++) { - result.push({ - "id": this.columns[i].id, - "width": this.getColumnWidth(i), - "visible": this.columns[i].visibility !== et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && - this.columns[i].visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED - }); - } - return result; - } - /** - * Returns an associative array which contains data about the visibility - * state of the columns. - */ - getColumnVisibilitySet() { - var result = {}; - for (var i = 0; i < this.columns.length; i++) { - if (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { - result[this.columns[i].id] = { - "caption": this.columns[i].caption, - "enabled": (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS) && - (this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) && - (this.columns[i].type != et2_dataview_column.ET2_COL_TYPE_NAME_ICON_FIXED), - "visible": this.columns[i].visibility != et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE - }; - } - } - return result; - } - /** - * Sets a column visiblity set - * - * @param {object} _set - */ - setColumnVisibilitySet(_set) { - for (var k in _set) { - var col = this.getColumnById(k); - if (col) { - col.set_visibility(_set[k].visible ? et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : - et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE); - } - } - this._updated = true; - } - /* ---- PRIVATE FUNCTIONS ---- */ - /** - * Calculates the absolute column width depending on the previously set - * "totalWidth" value. The calculated values are stored in the columnWidths - * array. - */ - _calculateWidths() { - // Reset some values which are used during the calculation - let _larger = Array(this.columns.length); - for (var i = 0; i < this.columns.length; i++) { - _larger[i] = false; - } - // Remove the spacing between the columns from the total width - var tw = this._totalWidth; - // Calculate how many space is - relatively - not occupied with columns with - // relative or fixed width - var totalRelative = 0; - var fixedCount = 0; - this._totalFixed = 0; - for (var i = 0; i < this.columns.length; i++) { - var col = this.columns[i]; - if (col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && - col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { - // Some bounds sanity checking - if (col.fixedWidth > tw || col.fixedWidth < 0) { - col.fixedWidth = false; - } - else if (col.relativeWidth > 1 || col.relativeWidth < 0) { - col.relativeWidth = false; - } - if (col.relativeWidth) { - totalRelative += col.relativeWidth; - } - else if (col.fixedWidth) { - this._totalFixed += col.fixedWidth; - fixedCount++; - } - } - } - // Now calculate the absolute width of the columns in pixels - var usedTotal = 0; - this.columnWidths = []; - for (var i = 0; i < this.columns.length; i++) { - var w = 0; - var col = this.columns[i]; - if (col.visibility != et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE && - col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { - if (_larger[i]) { - w = col.maxWidth; - } - else if (col.fixedWidth) { - w = col.fixedWidth; - } - else if (col.relativeWidth) { - // Reset relative to an actual percentage (of 1.00) or - // resizing eventually sends them to 0 - col.relativeWidth = col.relativeWidth / totalRelative; - w = Math.round((tw - this._totalFixed) * col.relativeWidth); - } - if (w > tw || (col.maxWidth && w > col.maxWidth)) { - w = Math.min(tw - usedTotal, col.maxWidth); - } - if (w < 0 || w < col.minWidth) { - w = Math.max(0, col.minWidth); - } - } - this.columnWidths.push(w); - usedTotal += w; - } - // Deal with any accumulated rounding errors - if (usedTotal != tw) { - var column, columnIndex; - var remaining_width = (usedTotal - tw); - // Pick the first relative column and use it - for (columnIndex = 0; columnIndex < this.columns.length; columnIndex++) { - if (this.columns[columnIndex].visibility === et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE || - this.columns[columnIndex].visibility === et2_dataview_column.ET2_COL_VISIBILITY_DISABLED || - this.columnWidths[columnIndex] <= 0 || - remaining_width > 0 && this.columnWidths[columnIndex] <= this.columns[columnIndex].minWidth) { - continue; - } - var col = this.columns[columnIndex]; - if (col.relativeWidth || !col.fixedWidth) { - column = col; - break; - } - else if (!col.fixedWidth) { - column = col; - } - } - if (!column) { - // Distribute shortage over all fixed width columns - var diff = Math.round(remaining_width / fixedCount); - for (var i = 0; i < this.columns.length; i++) { - var col = this.columns[i]; - var col_diff = (diff < 0 ? - Math.max(remaining_width, diff) : - Math.min(remaining_width, diff)); - if (!col.fixedWidth) - continue; - var new_width = this.columnWidths[i] - col_diff; - remaining_width -= col_diff; - this.columnWidths[i] = Math.max(0, Math.min(new_width, tw)); - } - } - else { - this.columnWidths[columnIndex] = Math.max(column.minWidth, this.columnWidths[columnIndex] - remaining_width); - } - } - } -} -//# sourceMappingURL=et2_dataview_model_columns.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_aoi.js b/api/js/etemplate/et2_dataview_view_aoi.js deleted file mode 100644 index b1fd55e426..0000000000 --- a/api/js/etemplate/et2_dataview_view_aoi.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * EGroupware eTemplate2 - Contains interfaces used inside the dataview - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - egw_action.egw_action_common; - egw_action.egw_action; - /vendor/bower-asset/jquery-touchswipe/jquery.touchSwipe.js; -*/ -import { egwActionObjectInterface } from "../egw_action/egw_action.js"; -import { EGW_AO_SHIFT_STATE_MULTI, EGW_AO_STATE_FOCUSED, EGW_AO_STATE_SELECTED } from '../egw_action/egw_action_constants.js'; -import { egwBitIsSet, egwGetShiftState, egwPreventSelect, egwSetBit, egwUnfocus, egwIsMobile } from "../egw_action/egw_action_common.js"; -import { _egw_active_menu } from "../egw_action/egw_menu.js"; -/** - * Contains the action object interface implementation for the nextmatch widget - * row. - */ -export const EGW_SELECTMODE_DEFAULT = 0; -export const EGW_SELECTMODE_TOGGLE = 1; -/** - * An action object interface for each nextmatch widget row - "inherits" from - * egwActionObjectInterface - * - * @param {DOMNode} _node - */ -export function et2_dataview_rowAOI(_node) { - "use strict"; - var aoi = new egwActionObjectInterface(); - aoi.node = _node; - aoi.selectMode = EGW_SELECTMODE_DEFAULT; - aoi.checkBox = null; //(jQuery(":checkbox", aoi.node))[0]; - // Rows without a checkbox OR an id set are unselectable - aoi.doGetDOMNode = function () { - return aoi.node; - }; - // Prevent the browser from selecting the content of the element, when - // a special key is pressed. - jQuery(_node).mousedown(egwPreventSelect); - /** - * Now append some action code to the node - * - * @memberOf et2_dataview_rowAOI - * @param {DOMEvent} e - * @param {object} _params - */ - var selectHandler = function (e, _params) { - // Reset the focus so that keyboard navigation will work properly - // after the element has been clicked - egwUnfocus(); - // Reset the prevent selection code (in order to allow wanted - // selection of text) - _node.onselectstart = null; - if (e.target != aoi.checkBox) { - var selected = egwBitIsSet(aoi.getState(), EGW_AO_STATE_SELECTED); - var state = egwGetShiftState(e); - if (_params) { - if (egwIsMobile()) { - switch (_params.swip) { - case "left": - case "right": - state = 1; - // Hide context menu on swip actions - if (_egw_active_menu) - _egw_active_menu.hide(); - break; - case "up": - case "down": - return; - } - } - } - switch (aoi.selectMode) { - case EGW_SELECTMODE_DEFAULT: - aoi.updateState(EGW_AO_STATE_SELECTED, !egwBitIsSet(state, EGW_AO_SHIFT_STATE_MULTI) || !selected, state); - break; - case EGW_SELECTMODE_TOGGLE: - aoi.updateState(EGW_AO_STATE_SELECTED, !selected, egwSetBit(state, EGW_AO_SHIFT_STATE_MULTI, true)); - break; - } - } - }; - if (egwIsMobile()) { - jQuery(_node).swipe({ - allowPageScroll: "vertical", - longTapThreshold: 10, - swipe: function (event, direction, distance) { - if (distance > 100) - selectHandler(event, { swip: direction }); - }, - tap: function (event, duration) { - selectHandler(event); - }, - // stop scrolling touch being confused from tap - longTap: function (event) { - return; - } - }); - } - else { - jQuery(_node).click(selectHandler); - } - jQuery(aoi.checkBox).change(function () { - aoi.updateState(EGW_AO_STATE_SELECTED, this.checked, EGW_AO_SHIFT_STATE_MULTI); - }); - aoi.doSetState = function (_state) { - var selected = egwBitIsSet(_state, EGW_AO_STATE_SELECTED); - if (this.checkBox) { - this.checkBox.checked = selected; - } - jQuery(this.node).toggleClass('focused', egwBitIsSet(_state, EGW_AO_STATE_FOCUSED)); - jQuery(this.node).toggleClass('selected', selected); - }; - return aoi; -} -//# sourceMappingURL=et2_dataview_view_aoi.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_container.js b/api/js/etemplate/et2_dataview_view_container.js deleted file mode 100644 index e58bf87c57..0000000000 --- a/api/js/etemplate/et2_dataview_view_container.js +++ /dev/null @@ -1,312 +0,0 @@ -/** - * EGroupware eTemplate2 - dataview code - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - * @version $Id$ - */ -import { et2_bounds } from "./et2_core_common"; -import { ClassWithInterfaces } from "./et2_core_inheritance"; -/** - * The et2_dataview_container class is the main object each dataview consits of. - * Each row, spacer as well as the grid itself are containers. A container is - * described by its parent element and a certain height. On the DOM-Level a - * container may consist of multiple "tr" nodes, which are treated as a unit. - * Some containers (like grid containers) are capable of managing a set of child - * containers. Each container can indicate, that it thinks that it's height - * might have changed. In that case it informs its parent element about that. - * The only requirement for the parent element is, that it implements the - * et2_dataview_IInvalidatable interface. - * A container does not know where it resides inside the grid, or whether it is - * currently visible or not -- this information is efficiently managed by the - * et2_dataview_grid container. - * - * @augments Class - */ -export class et2_dataview_container extends ClassWithInterfaces { - /** - * Initializes the container object. - * - * @param _parent is an object which implements the IInvalidatable - * interface. _parent may not be null. - * @memberOf et2_dataview_container - */ - constructor(_parent) { - super(); - // Copy the given invalidation element - this._parent = _parent; - this._nodes = []; - this._inTree = false; - this._attachData = { "node": null, "prepend": false }; - this._destroyCallback = null; - this._destroyContext = null; - this._height = -1; - this._index = 0; - this._top = 0; - } - /** - * Destroys this container. Classes deriving from et2_dataview_container - * should override this method and take care of unregistering all event - * handlers etc. - */ - destroy() { - // Remove the nodes from the tree - this.removeFromTree(); - // Call the callback function (if one is registered) - if (this._destroyCallback) { - this._destroyCallback.call(this._destroyContext, this); - } - } - /** - * Sets the "destroyCallback" -- the given function gets called whenever - * the container is destroyed. This instance is passed as an parameter to - * the callback. - * - * @param {function} _callback - * @param {object} _context - */ - setDestroyCallback(_callback, _context) { - this._destroyCallback = _callback; - this._destroyContext = _context; - } - /** - * Inserts all container nodes into the DOM tree after or before the given - * element. - * - * @param _node is the node after/before which the container "tr"s should - * get inserted. _node should be a simple DOM node, not a jQuery object. - * @param _prepend specifies whether the container should be inserted before - * or after the given node. Inserting before is needed for inserting the - * first element in front of an spacer. - */ - insertIntoTree(_node, _prepend) { - if (!this._inTree && _node != null && this._nodes.length > 0) { - // Store the parent node and indicate that this element is now in - // the tree. - this._attachData = { node: _node, prepend: _prepend }; - this._inTree = true; - for (let i = 0; i < this._nodes.length; i++) { - if (i == 0) { - if (_prepend) { - _node.before(this._nodes[0]); - } - else { - _node.after(this._nodes[0]); - } - } - else { - // Insert all following nodes after the previous node - this._nodes[i - 1].after(this._nodes[i]); - } - } - // Invalidate this element in order to update the height of the - // parent - this.invalidate(); - } - } - /** - * Removes all container nodes from the tree. - */ - removeFromTree() { - if (this._inTree) { - // Call the jQuery remove function to remove all nodes from the tree - // again. - for (let i = 0; i < this._nodes.length; i++) { - this._nodes[i].remove(); - } - // Reset the "attachData" - this._inTree = false; - this._attachData = { "node": null, "prepend": false }; - } - } - /** - * Appends a node to the container. - * - * @param _node is the DOM-Node which should be appended. - */ - appendNode(_node) { - // Add the given node to the "nodes" array - this._nodes.push(_node); - // If the container is already in the tree, attach the given node to the - // tree. - if (this._inTree) { - if (this._nodes.length === 1) { - if (this._attachData.prepend) { - this._attachData.node.before(_node); - } - else { - this._attachData.node.after(_node); - } - } - else { - this._nodes[this._nodes.length - 2].after(_node); - } - this.invalidate(); - } - } - /** - * Removes a certain node from the container - * - * @param {HTMLElement} _node - */ - removeNode(_node) { - // Get the index of the node in the nodes array - const idx = this._nodes.indexOf(_node); - if (idx >= 0) { - // Remove the node if the container is currently attached - if (this._inTree) { - _node.parentNode.removeChild(_node); - } - // Remove the node from the nodes array - this._nodes.splice(idx, 1); - } - } - /** - * Returns the last node of the container - new nodes have to be appended - * after it. - */ - getLastNode() { - if (this._nodes.length > 0) { - return this._nodes[this._nodes.length - 1]; - } - return null; - } - /** - * Returns the first node of the container. - */ - getFirstNode() { - return this._nodes.length > 0 ? this._nodes[0] : null; - } - /** - * Returns the accumulated height of all container nodes. Only visible nodes - * (without "display: none" etc.) are taken into account. - */ - getHeight() { - if (this._height === -1 && this._inTree) { - this._height = 0; - // Setting this before measuring height helps with issues getting the - // wrong height due to margins & collapsed borders - this.tr.css('display', 'block'); - // Increment the height value for each visible container node - for (let i = 0; i < this._nodes.length; i++) { - if (et2_dataview_container._isVisible(this._nodes[i][0])) { - this._height += et2_dataview_container._nodeHeight(this._nodes[i][0]); - } - } - this.tr.css('display', ''); - } - return (this._height === -1) ? 0 : this._height; - } - /** - * Returns a datastructure containing information used for calculating the - * average row height of a grid. - * The datastructure has the - * { - * avgHeight: , - * avgCount: - * } - */ - getAvgHeightData() { - return { - "avgHeight": this.getHeight(), - "avgCount": 1 - }; - } - /** - * Returns the previously set "pixel top" of the container. - */ - getTop() { - return this._top; - } - /** - * Returns the "pixel bottom" of the container. - */ - getBottom() { - return this._top + this.getHeight(); - } - /** - * Returns the range of the element. - */ - getRange() { - return et2_bounds(this.getTop(), this.getBottom()); - } - /** - * Returns the index of the element. - */ - getIndex() { - return this._index; - } - /** - * Returns how many elements this container represents. - */ - getCount() { - return 1; - } - /** - * Sets the top of the element. - * - * @param {number} _value - */ - setTop(_value) { - this._top = _value; - } - /** - * Sets the index of the element. - * - * @param {number} _value - */ - setIndex(_value) { - this._index = _value; - } - /* -- et2_dataview_IInvalidatable -- */ - /** - * Broadcasts an invalidation through the container tree. Marks the own - * height as invalid. - */ - invalidate() { - // Abort if this element is already marked as invalid. - if (this._height !== -1) { - // Delete the own, probably computed height - this._height = -1; - // Broadcast the invalidation to the parent element - this._parent.invalidate(); - } - } - /* -- PRIVATE FUNCTIONS -- */ - /** - * Used to check whether an element is visible or not (non recursive). - * - * @param _obj is the element which should be checked for visibility, it is - * only checked whether some stylesheet makes the element invisible, not if - * the given object is actually inside the DOM. - */ - static _isVisible(_obj) { - // Check whether the element is localy invisible - if (_obj.style && (_obj.style.display === "none" - || _obj.style.visibility === "none")) { - return false; - } - // Get the computed style of the element - const style = window.getComputedStyle ? window.getComputedStyle(_obj, null) - // @ts-ignore - : _obj.currentStyle; - if (style.display === "none" || style.visibility === "none") { - return false; - } - return true; - } - /** - * Returns the height of a node in pixels and zero if the element is not - * visible. The height is clamped to positive values. - * - * @param {HTMLElement} _node - */ - static _nodeHeight(_node) { - return _node.offsetHeight; - } -} -//# sourceMappingURL=et2_dataview_view_container.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_grid.js b/api/js/etemplate/et2_dataview_view_grid.js deleted file mode 100644 index e4db6c56ab..0000000000 --- a/api/js/etemplate/et2_dataview_view_grid.js +++ /dev/null @@ -1,1074 +0,0 @@ -/** - * EGroupware eTemplate2 - Class which contains the "grid" base class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_common; - - et2_dataview_interfaces; - et2_dataview_view_container; - et2_dataview_view_spacer; -*/ -import { et2_implements_registry } from "./et2_core_interfaces"; -import { et2_dataview_IViewRange } from "./et2_dataview_interfaces"; -import { et2_dataview_container } from "./et2_dataview_view_container"; -import { et2_dataview_spacer } from "./et2_dataview_view_spacer"; -import { et2_bounds, et2_range, et2_rangeEqual, et2_rangeIntersect } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -export class et2_dataview_grid extends et2_dataview_container { - /** - * Creates the grid. - * - * @param _parent is the parent grid class - if null, this means that this - * is the outer grid which manages the scrollarea. If not null, all other - * parameters are ignored and copied from the given grid instance. - * @param _parentGrid - * @param _egw - * @param _rowProvider - * @param _avgHeight is the starting average height of the column rows. - * @memberOf et2_dataview_grid - */ - constructor(_parent, _parentGrid, _egw, _rowProvider, _avgHeight) { - // Call the inherited constructor - super(_parent); - // If the parent is given, copy all other parameters from it - if (_parentGrid != null) { - this.egw = _parent.egw; - this._orgAvgHeight = false; - this._rowProvider = _parentGrid._rowProvider; - } - else { - // Otherwise copy the given parameters - this.egw = _egw; - this._orgAvgHeight = _avgHeight; - this._rowProvider = _rowProvider; - // As this grid instance has no parent, we need a scroll container - this._scrollHeight = 0; - this._scrollTimeout = null; - } - this._parentGrid = _parentGrid; - this._scrollTimeout = null; - this._invalidateTimeout = null; - this._invalidateCallback = null; - this._invalidateContext = null; - // Flag for stopping invalidate while working - this.doInvalidate = true; - // _map contains a mapping between the grid indices and the elements - // associated to it. The first element in the array always refers to the - // element starting at index zero (being a spacer if the grid currently - // displays another range). - this._map = []; - // _viewRange contains the current pixel-range of the grid which is - // visible. - this._viewRange = et2_range(0, 0); - // Holds the maximum count of elements - this._total = 0; - // Holds data used for storing the current average height data - this._avgHeight = false; - this._avgCount = -1; - // Build the outer grid nodes - this._createNodes(); - } - destroy() { - // Destroy all containers - this.setTotalCount(0); - // Stop the scroll timeout - if (this._scrollTimeout) { - window.clearTimeout(this._scrollTimeout); - } - // Stop the invalidate timeout - if (this._invalidateTimeout) { - window.clearTimeout(this._invalidateTimeout); - } - super.destroy(); - } - clear() { - // Store the old total count and rescue the current average height in - // form of the "original average height" - const oldTotalCount = this._total; - this._orgAvgHeight = this.getAverageHeight(); - // Set the total count to zero - this.setTotalCount(0); - // Reset the total count value - this.setTotalCount(oldTotalCount); - } - /** - * Throws all elements away which are outside the current view range - */ - cleanup() { - // Update the pixel positions - this._recalculateElementPosition(); - // Get the visible mapping indices and recalculate index and pixel - // position of the containers. - const mapVis = this._calculateVisibleMappingIndices(); - // Delete all invisible elements -- if anything changed, we have to - // recalculate the pixel positions again - this._cleanupOutOfRangeElements(mapVis, 0); - } - /** - * The insertRow function can be called to insert the given container(s) at - * the given row index. If there currently is another container at that - * given position, the new container(s) will be inserted above the old - * container. Yet the "total count" of the grid will be preserved by - * removing the correct count of elements from the next possible spacer. If - * no spacer is found, the last containers will be removed. This causes - * inserting new containers at the end of a grid to be immediately removed - * again. - * - * @param _index is the row index at which the given container(s) should be - * inserted. - * @param _container is eiter a single et2_dataview_container instance - * which should be inserted at the given position. Or an array of - * et2_dataview_container instances. If you want to remove the container - * don't do that manually by calling its "destroy" function but use the - * deleteRow function. - */ - insertRow(_index, _container) { - // Calculate the map element the given index refers to - const idx = this._calculateMapIndex(_index); - if (idx !== false) { - // Wrap the container inside an array - if (_container instanceof et2_dataview_container) { - _container = [_container]; - } - // Fetch the average height - const avg = this.getAverageHeight(); - // Call the internal _doInsertContainer function - for (let i = 0; i < _container.length; i++) { - this._doInsertContainer(_index, idx, _container[i], avg); - } - // Schedule an "invalidate" event - this.invalidate(); - } - } - /** - * The deleteRow function can be used to remove the element at the given - * index. - * - * @param _index is the index from which should be deleted. If the given - * index is outside the so called "managedRange" nothing will happen, as the - * container has already been destroyed by the grid instance. - */ - deleteRow(_index) { - // Calculate the map element the given index refers to - const idx = this._calculateMapIndex(_index); - if (idx !== false) { - this._doDeleteContainer(idx, false); - // Schedule an "invalidate" event - this.invalidate(); - } - } - /** - * The given callback gets called whenever the scroll position changed or - * the visible element range changed. The element indices are passed to the - * function as et2_range. - */ - setInvalidateCallback(_callback, _context) { - this._invalidateCallback = _callback; - this._invalidateContext = _context; - } - /** - * The setDataCallback function is used to set the callback that will be - * called when the grid requires new data. - * - * @param _callback is the callback function which gets called when the grid - * needs some new rows. - * @param _context is the context in which the callback function gets - * called. - */ - setDataCallback(_callback, _context) { - this._callback = _callback; - this._context = _context; - } - /** - * The updateTotalCount function can be used to update the total count of - * rows that are displayed inside the grid. Changing the count always causes - * the spacer at the bottom (if it exists) to be - * - * @param _count specifies how many entries the grid can show. - */ - setTotalCount(_count) { - // Abort if the total count has not changed - if (_count === this._total) - return; - // Calculate how many elements have to be removed/added - const delta = Math.max(0, _count) - this._total; - if (delta > 0) { - this._appendEmptyRows(delta); - } - else { - this._decreaseTotal(-delta); - } - this._total = Math.max(0, _count); - // Schedule an invalidate - this.invalidate(); - } - /** - * Returns the current "total" count. - */ - getTotalCount() { - return this._total; - } - /** - * The setViewRange function updates the range in which rows are shown. - */ - setViewRange(_range) { - // Set the new view range - this._viewRange = _range; - // Immediately call the "invalidate" function - this._doInvalidate(); - } - /** - * Return the indices of the currently visible rows. - */ - getVisibleIndexRange(_viewRange) { - function getElemIdx(_elem, _px) { - if (_elem instanceof et2_dataview_spacer) { - return _elem.getIndex() - + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); - } - return _elem.getIndex(); - } - let idxTop = 0; - let idxBottom = 0; - let vr; - if (_viewRange) { - vr = _viewRange; - } - else { - // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT - vr = et2_bounds(this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); - } - // Get the elements at the top and the bottom of the view - let topElem = null; - let botElem = null; - for (let i = 0; i < this._map.length; i++) { - if (!topElem && this._map[i].getBottom() > vr.top) { - topElem = this._map[i]; - } - if (this._map[i].getTop() > vr.bottom) { - botElem = this._map[i]; - break; - } - } - if (!botElem) { - botElem = this._map[this._map.length - 1]; - } - if (topElem) { - idxTop = getElemIdx.call(this, topElem, vr.top); - idxBottom = getElemIdx.call(this, botElem, vr.bottom); - } - // Return the calculated index top and bottom - return et2_bounds(idxTop, idxBottom); - } - /** - * Returns index range of all currently managed rows. - */ - getIndexRange() { - let idxTop = false; - let idxBottom = false; - for (let i = 0; i < this._map.length; i++) { - if (!(this._map[i] instanceof et2_dataview_spacer)) { - const idx = this._map[i].getIndex(); - if (idxTop === false) { - idxTop = idx; - } - idxBottom = idx; - } - } - return et2_bounds(idxTop, idxBottom); - } - /** - * Updates the scrollheight - */ - setScrollHeight(_height) { - this._scrollHeight = _height; - // Update the height of the outer container - if (this.scrollarea) { - this.scrollarea.height(_height); - } - // Update the viewing range - this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); - } - /** - * Returns the average row height data, overrides the corresponding function - * of the et2_dataview_container. - */ - getAvgHeightData() { - if (this._avgHeight === false) { - let avgCount = 0; - let avgSum = 0; - for (let i = 0; i < this._map.length; i++) { - const data = this._map[i].getAvgHeightData(); - if (data !== null) { - avgSum += data.avgHeight * data.avgCount; - avgCount += data.avgCount; - } - } - // Calculate the average height, but only if we have a height - if (avgCount > 0 && avgSum > 0) { - this._avgHeight = avgSum / avgCount; - this._avgCount = avgCount; - } - } - // Return the calculated average height if it is available - if (this._avgHeight !== false) { - return { - "avgCount": this._avgCount, - "avgHeight": this._avgHeight - }; - } - // Otherwise return the parent average height - if (this._parent) { - return this._parent.getAvgHeightData(); - } - // Otherwise return the original average height given in the constructor - if (this._orgAvgHeight !== false) { - return { - "avgCount": 1, - "avgHeight": this._orgAvgHeight - }; - } - return null; - } - /** - * Returns the average row height in pixels. - */ - getAverageHeight() { - const data = this.getAvgHeightData(); - return data ? data.avgHeight : 19; - } - /** - * Returns the row provider. - */ - getRowProvider() { - return this._rowProvider; - } - /** - * Called whenever the size of this or another element in the container tree - * changes. - */ - invalidate() { - // Clear any existing "invalidate" timeout - if (this._invalidateTimeout) { - window.clearTimeout(this._invalidateTimeout); - } - if (!this.doInvalidate) { - return; - } - const self = this; - const _super = super.invalidate(); - this._invalidateTimeout = window.setTimeout(function () { - this.egw.debug("log", "Dataview grid timed invalidate"); - // Clear the "_avgHeight" - self._avgHeight = false; - self._avgCount = -1; - self._invalidateTimeout = null; - self._doInvalidate(_super); - }, et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); - } - /** - * Makes the given index visible: TODO: Propagate this to the parent grid. - */ - makeIndexVisible(_idx) { - // Get the element range - const elemRange = this._getElementRange(_idx); - // Abort if the index was out of range - if (!elemRange) { - return false; - } - // Calculate the current visible range - const visibleRange = et2_bounds(this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); - // Check whether the element is currently completely visible -- if yes, - // do nothing - if (visibleRange.top < elemRange.top - && visibleRange.bottom > elemRange.bottom) { - return true; - } - if (elemRange.top < visibleRange.top) { - this.scrollarea.scrollTop(elemRange.top); - } - else { - const h = elemRange.bottom - elemRange.top; - this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h); - } - } - /* ---- PRIVATE FUNCTIONS ---- */ - /* _inspectStructuralIntegrity: function() { - var idx = 0; - for (var i = 0; i < this._map.length; i++) - { - if (this._map[i].getIndex() != idx) - { - throw "Index missmatch!"; - } - idx += this._map[i].getCount(); - } - - if (idx !== this._total) - { - throw "Total count missmatch!"; - } - },*/ - /** - * Translates the given index to a range, returns false if the index is - * out of range. - */ - _getElementRange(_idx) { - // Recalculate the element positions - this._recalculateElementPosition(); - // Translate the given index to the map index - const mapIdx = this._calculateMapIndex(_idx); - // Do nothing if the given index is out of range - if (mapIdx === false) { - return false; - } - // Get the map element - const elem = this._map[mapIdx]; - // Get the element range - if (elem instanceof et2_dataview_spacer) { - const avg = this.getAverageHeight(); - return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx), avg); - } - return elem.getRange(); - } - /** - * Recalculates the position of the currently managed containers. This - * routine only updates the pixel position of the elements -- the index of - * the elements is guaranteed to be maintained correctly by all high level - * functions of the grid, as the index position is needed to be correct for - * the "deleteRow" and "insertRow" functions, and we cannot effort to call - * this calculation method after every change in the grid mapping. - */ - _recalculateElementPosition() { - for (let i = 0; i < this._map.length; i++) { - if (i == 0) { - this._map[i].setTop(0); - } - else { - this._map[i].setTop(this._map[i - 1].getBottom()); - } - } - } - /** - * The "_calculateVisibleMappingIndices" function calculates the indices of - * the _map array, which refer to containers that are currently (partially) - * visible. This function is used internally by "_doInvalidate". - */ - _calculateVisibleMappingIndices() { - // First update the "top" and "bottom", and "index" values of all - // managed elements, and at the same time calculate the mapping indices - // of the elements which are inside the current view range. - const mapVis = { "top": -1, "bottom": -1 }; - for (let i = 0; i < this._map.length; i++) { - // Update the top of the "map visible index" -- set it to the first - // element index, where the bottom line is beneath the top line - // of the view range. - if (mapVis.top === -1 - && this._map[i].getBottom() > this._viewRange.top) { - mapVis.top = i; - } - // Update the bottom of the "map visible index" -- set it to the - // first element index, where the top line is beneath the bottom - // line of the view range. - if (mapVis.bottom === -1 - && this._map[i].getTop() > this._viewRange.bottom) { - mapVis.bottom = i; - break; - } - } - return mapVis; - } - /** - * Deletes all elements which are "out of the view range". This function is - * internally used by "_doInvalidate". How many elements that are out of the - * view range get preserved fully depends on the _holdCount parameter - * variable. - * - * @param _mapVis contains the _map indices of the just visible containers. - * @param _holdCount contains the number of elements that should be kept, - * if not given, this parameter defaults to ET2_GRID_HOLD_COUNT - */ - _cleanupOutOfRangeElements(_mapVis, _holdCount) { - // Iterates over the map from and to the given indices and pushes all - // elements onto the given array, which are more than _holdCount - // elements remote from the start. - function searchElements(_arr, _start, _stop, _dir) { - let dist = 0; - for (let i = _start; _dir > 0 ? i <= _stop : i >= _stop; i += _dir) { - if (dist > _holdCount) { - _arr.push(i); - } - else { - dist += this._map[i].getCount(); - } - } - } - // Set _holdCount to ET2_GRID_HOLD_COUNT if the parameters is not given - _holdCount = typeof _holdCount === "undefined" ? et2_dataview_grid.ET2_GRID_HOLD_COUNT : - _holdCount; - // Collect all elements that will be deleted at the top and at the - // bottom of the grid - const deleteTop = []; - const deleteBottom = []; - if (_mapVis.top !== -1) { - searchElements.call(this, deleteTop, _mapVis.top, 0, -1); - } - if (_mapVis.bottom !== -1) { - searchElements.call(this, deleteBottom, _mapVis.bottom, this._map.length - 1, 1); - } - // The offset variable specifies how many elements have been deleted - // from the map -- this variable is needed as deleting elements from the - // map shifts the map indices. We iterate in oposite direction over the - // elements, as this allows the _doDeleteContainer/ container function - // to extend the (possibly) existing spacer at the top of the grid - let offs = 0; - for (var i = deleteTop.length - 1; i >= 0; i--) { - // Delete the container and calculate the new offset - const mapLength = this._map.length; - this._doDeleteContainer(deleteTop[i] - offs, true); - offs += mapLength - this._map.length; - } - for (var i = deleteBottom.length - 1; i >= 0; i--) { - this._doDeleteContainer(deleteBottom[i] - offs, true); - } - return deleteBottom.length + deleteTop.length > 0; - } - /** - * The _updateContainers function is used internally by "_doInvalidate" in - * order to call the "setViewRange" function of all containers the implement - * that interfaces (needed for nested grids), and to request new elements - * for all currently visible spacers. - */ - _updateContainers() { - for (let i = 0; i < this._map.length; i++) { - const container = this._map[i]; - // Check which type the container object has - const isSpacer = container instanceof et2_dataview_spacer; - const hasIViewRange = !isSpacer && et2_implements_registry.et2_dataview_IViewRange(container, et2_dataview_IViewRange); - // If the container has one of those special types, calculate the - // view range and use that to update the view range of the element - // or to request new elements for the spacer - if (isSpacer || hasIViewRange) { - // Calculate the relative view range and check whether - // the element is really visible - const elemRange = container.getRange(); - // Abort if the element is not inside the visible range - if (!et2_rangeIntersect(this._viewRange, elemRange)) { - continue; - } - if (hasIViewRange) { - // Update the view range of the container - container.setViewRange(et2_bounds(this._viewRange.top - elemRange.top, this._viewRange.bottom - elemRange.top)); - } - else // This container is a spacer - { - // Obtain the average element height - const avg = container._rowHeight; - // Get the visible container range (vcr) - const vcr_top = Math.max(this._viewRange.top, elemRange.top); - const vcr_bot = Math.min(this._viewRange.bottom, elemRange.bottom); - // Calculate the indices of the elements which will be - // requested - const cidx = container.getIndex(); - const ccnt = container.getCount(); - // Calculate the start index -- prevent vtop from getting - // negative (and so idxStart being smaller than cidx) and - // ensure that idxStart is not larger than the maximum - // container index. - const vtop = Math.max(0, vcr_top); - let idxStart = Math.floor(Math.min(cidx + ccnt - 1, cidx + (vtop - elemRange.top) / avg, this._total)); - // Calculate the end index -- prevent vtop from getting - // negative (and so idxEnd being smaller than cidx) and - // ensure that idxEnd is not larger than the maximum - // container index. - const vbot = Math.max(0, vcr_bot); - let idxEnd = Math.ceil(Math.min(cidx + ccnt - 1, cidx + (vbot - elemRange.top) / avg, this._total)); - // Initial resize while the grid is hidden will give NaN - // This is an important optimisation, as it is involved in not - // loading all rows, so we override in that case so - // there are more than the 2-3 that fit in the min height. - if (isNaN(idxStart) && isSpacer) - idxStart = cidx - 1; - if (isNaN(idxEnd) && isSpacer && this._scrollHeight > 0 && elemRange.bottom == 0) { - idxEnd = Math.min(ccnt, cidx + Math.ceil((this._viewRange.bottom - container._top) / (this._orgAvgHeight || 0))); - } - // Call the data callback - if (this._callback) { - const self = this; - egw.debug("log", "Dataview grid flag for update: ", { start: idxStart, end: idxEnd }); - window.setTimeout(function () { - // If row template changes, self._callback might disappear - if (typeof self._callback != "undefined") { - self._callback.call(self._context, idxStart, idxEnd); - } - }, 0); - } - } - } - } - } - /** - * Invalidate iterates over the "mapping" array. It calculates which - * containers have to be removed and where new containers should be added. - */ - _doInvalidate(_super) { - if (!this.doInvalidate) - return; - // Not visible? - if (jQuery(":visible", this.outerCell).length == 0) { - return; - } - // Update the pixel positions - this._recalculateElementPosition(); - // Call the callback - if (this._invalidateCallback) { - const range = this.getVisibleIndexRange(et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); - this._invalidateCallback.call(this._invalidateContext, range); - } - // Get the visible mapping indices and recalculate index and pixel - // position of the containers. - const mapVis = this._calculateVisibleMappingIndices(); - // Delete all invisible elements -- if anything changed, we have to - // recalculate the pixel positions again - if (this._cleanupOutOfRangeElements(mapVis)) { - this._recalculateElementPosition(); - } - // Update the view range of all visible elements that implement the - // corresponding interface and request elements for all visible spacers - this._updateContainers(); - // Call the inherited invalidate function, broadcast the invalidation - // through the container tree. - if (this._parent && _super) { - _super._doInvalidate(); - } - } - /** - * Translates the given grid index into the element index of the map. If the - * given index is completely out of the range, "false" is returned. - */ - _calculateMapIndex(_index) { - let top = 0; - let bot = this._map.length - 1; - while (top <= bot) { - const idx = Math.floor((top + bot) / 2); - const elem = this._map[idx]; - const realIdx = elem.getIndex(); - const realCnt = elem.getCount(); - if (_index >= realIdx && _index < realIdx + realCnt) { - return idx; - } - else if (_index < realIdx) { - bot = idx - 1; - } - else { - top = idx + 1; - } - } - return false; - } - _insertContainerAtSpacer(_index, _mapIndex, _mapElem, _container, _avg) { - // Set the index of the new container - _container.setIndex(_index); - // Calculate at which position the spacer has to be splitted - const splitIdx = _index - _mapElem.getIndex(); - // Get the count of elements that remain at the top of the splitter - const cntTop = splitIdx; - // Get the count of elements that remain at the bottom of the splitter - // -- it has to be one element less than before - const cntBottom = _mapElem.getCount() - splitIdx - 1; - // Split the containers if cntTop and cntBottom are larger than zero - if (cntTop > 0 && cntBottom > 0) { - // Set the new count of the currently existing container, preserving - // its height as it was - _mapElem.setCount(cntTop); - // Add the new element after the old container - _container.insertIntoTree(_mapElem.getLastNode()); - // Create a new spacer and add it after the newly inserted container - const newSpacer = new et2_dataview_spacer(this, this._rowProvider); - newSpacer.setCount(cntBottom, _avg); - newSpacer.setIndex(_index + 1); - newSpacer.insertIntoTree(_container.getLastNode()); - // Insert the container and the new spacer into the map - this._map.splice(_mapIndex + 1, 0, _container, newSpacer); - } - else if (cntTop === 0 && cntBottom > 0) { - // Simply adjust the size of the old spacer and insert the new - // container in front of it - _container.insertIntoTree(_mapElem.getFirstNode(), true); - _mapElem.setIndex(_index + 1); - _mapElem.setCount(cntBottom, _avg); - this._map.splice(_mapIndex, 0, _container); - } - else if (cntTop > 0 && cntBottom === 0) { - // Simply add the new container to the end of the old container and - // adjust the count of the old spacer to the remaining count. - _container.insertIntoTree(_mapElem.getLastNode()); - _mapElem.setCount(cntTop); - this._map.splice(_mapIndex + 1, 0, _container); - } - else // if (cntTop === 0 && cntBottom === 0) - { - // Append the new container to the current container and then - // destroy the old container - _container.insertIntoTree(_mapElem.getLastNode()); - _mapElem.destroy(); - this._map.splice(_mapIndex, 1, _container); - } - } - _insertContainerAtElement(_index, _mapIndex, _mapElem, _container, _avg) { - // In a first step, simply insert the element at the specified position, - // in front of the element _mapElem. - _container.setIndex(_index); - _container.insertIntoTree(_mapElem.getFirstNode(), true); - this._map.splice(_mapIndex, 0, _container); - // Search for the next spacer and increment the indices of all other - // elements until there - let _newIndex = _index + 1; - for (let i = _mapIndex + 1; i < this._map.length; i++) { - // Update the index of the element - this._map[i].setIndex(_newIndex++); - // We've found a spacer -- decrement its element count and abort - if (this._map[i] instanceof et2_dataview_spacer) { - this._decrementSpacerCount(i, _avg); - return; - } - } - // We've found no spacer so far, remove the last element from the map in - // order to obtain the "totalCount" (especially the last element is no - // spacer, so the following code cannot remove a spacer) - this._map.pop().destroy(); - } - /** - * Inserts the given container at the given index. - */ - _doInsertContainer(_index, _mapIndex, _container, _avg) { - // Check whether the given element at the map index is a spacer. If - // yes, we have to split the spacer at that position. - const mapElem = this._map[_mapIndex]; - if (mapElem instanceof et2_dataview_spacer) { - this._insertContainerAtSpacer(_index, _mapIndex, mapElem, _container, _avg); - } - else { - this._insertContainerAtElement(_index, _mapIndex, mapElem, _container, _avg); - } - } - /** - * Replaces the container at the given index with a spacer. The function - * tries to extend any spacer lying above or below the given _mapIndex. - * This code does not destroy the given container, but maintains its map - * index. - * - * @param _mapIndex is the index of _mapElem in the _map array. - * @param _mapElem is the container which should be replaced. - */ - _replaceContainerWithSpacer(_mapIndex, _mapElem) { - let newAvg; - let spacer; - let totalHeight; - let totalCount; - // Check whether a spacer can be extended above or below the given - // _mapIndex - let spacerAbove = null; - let spacerBelow = null; - if (_mapIndex > 0 - && this._map[_mapIndex - 1] instanceof et2_dataview_spacer) { - spacerAbove = this._map[_mapIndex - 1]; - } - if (_mapIndex < this._map.length - 1 - && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) { - spacerBelow = this._map[_mapIndex + 1]; - } - if (!spacerAbove && !spacerBelow) { - // No spacer can be extended -- simply create a new one - spacer = new et2_dataview_spacer(this, this._rowProvider); - spacer.setIndex(_mapElem.getIndex()); - spacer.addAvgHeight(_mapElem.getHeight()); - spacer.setCount(1, _mapElem.getHeight()); - // Insert the new spacer at the correct place into the DOM tree and - // the mapping - spacer.insertIntoTree(_mapElem.getLastNode()); - this._map.splice(_mapIndex + 1, 0, spacer); - } - else if (spacerAbove && spacerBelow) { - // We're going to consolidate the upper and the lower spacer. To do - // that we'll calculate a new count of elements and a new average - // height, so that the upper container can get the height of all - // three elements together - totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() - + _mapElem.getHeight(); - totalCount = spacerAbove.getCount() + spacerBelow.getCount() - + 1; - newAvg = totalHeight / totalCount; - // Update the upper spacer - spacerAbove.addAvgHeight(_mapElem.getHeight()); - spacerAbove.setCount(totalCount, newAvg); - // Delete the lower spacer and remove it from the mapping - spacerBelow.destroy(); - this._map.splice(_mapIndex + 1, 1); - } - else { - // One of the two spacers is available - spacer = spacerAbove || spacerBelow; - // Calculate the new count and the new average height of that spacer - totalCount = spacer.getCount() + 1; - totalHeight = spacer.getHeight() + _mapElem.getHeight(); - newAvg = totalHeight / totalCount; - // Set the new container height - spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); - spacer.addAvgHeight(_mapElem.getHeight()); - spacer.setCount(totalCount, newAvg); - } - } - /** - * Checks whether there is another spacer below the given map index and if - * yes, consolidates the two. - */ - _consolidateSpacers(_mapIndex) { - if (_mapIndex < this._map.length - 1 - && this._map[_mapIndex] instanceof et2_dataview_spacer - && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) { - const spacerAbove = this._map[_mapIndex]; - const spacerBelow = this._map[_mapIndex + 1]; - // Calculate the new height/count of both containers - const totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); - const totalCount = spacerAbove.getCount() + spacerBelow.getCount(); - const newAvg = totalCount / totalHeight; - // Extend the new spacer - spacerAbove.setCount(totalCount, newAvg); - // Delete the old spacer - spacerBelow.destroy(); - this._map.splice(_mapIndex + 1, 1); - } - } - /** - * Decrements the count of the spacer at the given _mapIndex by one. If the - * given spacer has no more elements, it will be removed from the mapping. - * Note that this function does not update the indices of the following - * elements, this function is only used internally by the - * _insertContainerAtElement function and the _doDeleteContainer function - * where appropriate adjustments to the map data structure are done. - * - * @param _mapIndex is the index of the spacer in the "map" data structure. - * @param _avg is the new average height of the container, may be - * "undefined" in which case the height of the spacer rows is kept as it - * was. - */ - _decrementSpacerCount(_mapIndex, _avg) { - const cnt = this._map[_mapIndex].getCount() - 1; - if (cnt > 0) { - this._map[_mapIndex].setCount(cnt, _avg); - } - else { - this._map[_mapIndex].destroy(); - this._map.splice(_mapIndex, 1); - } - } - /** - * Deletes the container at the given index. - */ - _doDeleteContainer(_mapIndex, _replaceWithSpacer) { - // _replaceWithSpacer defaults to false - _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; - // Fetch the element at the given map index - const mapElem = this._map[_mapIndex]; - // Indicates whether an element has really been removed -- if yes, the - // bottom spacer will be extended - let removedElement = false; - // Check whether the map element is a spacer -- if yes, we have to do - // some special treatment - if (mapElem instanceof et2_dataview_spacer) { - // Do nothing if the "_replaceWithSpacer" flag is true as the - // element already is a spacer - if (!_replaceWithSpacer) { - this._decrementSpacerCount(_mapIndex); - removedElement = true; - } - } - else { - if (_replaceWithSpacer) { - this._replaceContainerWithSpacer(_mapIndex, mapElem); - } - else { - removedElement = true; - } - // Remove the complete (current) container, decrement the _mapIndex - this._map[_mapIndex].destroy(); - this._map.splice(_mapIndex, 1); - _mapIndex--; - // The delete operation may have created two joining spacers -- this - // is highly suboptimal, so we'll consolidate those two spacers - this._consolidateSpacers(_mapIndex); - } - // Update the indices of all elements after the current one, if we've - // really removed an element - if (removedElement) { - for (let i = _mapIndex + 1; i < this._map.length; i++) { - this._map[i].setIndex(this._map[i].getIndex() - 1); - } - // Extend the last spacer as we have to maintain the spacer count - this._appendEmptyRows(1); - } - } - /** - * The appendEmptyRows function is used internally to append empty rows to - * the end of the table. This functionality is needed in order to maintain - * the "total count" in the _doDeleteContainer function and to increase the - * "total count" in the "setCount" function. - * - * @param _count specifies the count of empty rows that will be added to the - * end of the table. - */ - _appendEmptyRows(_count) { - // Special case -- the last element in the "_map" is no spacer -- this - // means, that the "managedRange" is currently at the bottom of the list - // -- so we have to insert a new spacer - let spacer = null; - const lastIndex = this._map.length - 1; - if (this._map.length === 0 || - !(this._map[lastIndex] instanceof et2_dataview_spacer)) { - // Create a new spacer - spacer = new et2_dataview_spacer(this, this._rowProvider); - // Insert the spacer -- we have a special case if there currently is - // no element inside the mapping - if (this._map.length === 0) { - // Add a dummy element to the grid - const dummy = jQuery(document.createElement("tr")); - this.innerTbody.append(dummy); - // Append the spacer to the grid - spacer.setIndex(0); - spacer.insertIntoTree(dummy, false); - // Remove the dummy element - dummy.remove(); - } - else { - // Insert the new spacer after the last element - spacer.setIndex(this._map[lastIndex].getIndex() + 1); - spacer.insertIntoTree(this._map[lastIndex].getLastNode()); - } - // Add the new spacer to the mapping - this._map.push(spacer); - } - else { - // Get the spacer at the bottom of the mapping - spacer = this._map[lastIndex]; - } - // Update the spacer count - spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); - } - /** - * The _decreaseTotal function is used to decrease the total row count in - * the grid. It tries to remove the given count of rows from the spacer - * located at the bottom of the grid, if this is not possible, it starts - * removing complete rows. - * - * @param _delta specifies how many rows should be removed. - */ - _decreaseTotal(_delta) { - // Iterate over the current mapping, starting at the bottom and delete - // rows. _delta is decreased for each removed row. Abort when delta is - // zero or the map is empty - while (_delta > 0 && this._map.length > 0) { - const cont = this._map[this._map.length - 1]; - // Remove as many containers as possible from spacers - if (cont instanceof et2_dataview_spacer) { - const diff = cont.getCount() - _delta; - if (diff > 0) { - // We're done as the spacer still has entries left - _delta = 0; - cont.setCount(diff, this.getAverageHeight()); - break; - } - else { - // Decrease _delta by the count of rows the spacer had - _delta -= diff + _delta; - } - } - else { - // We're going to remove a single row: remove it - _delta -= 1; - } - // Destroy the container if there are no rows left - cont.destroy(); - this._map.pop(); - } - // Check whether _delta is really zero - if (_delta > 0) { - this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); - } - } - /** - * Creates the grid DOM-Nodes - */ - _createNodes() { - this.tr = jQuery(document.createElement("tr")); - this.outerCell = jQuery(document.createElement("td")) - .addClass("frame") - .attr("colspan", this._rowProvider.getColumnCount() - + (this._parentGrid ? 0 : 1)) - .appendTo(this.tr); - // Create the scrollarea div if this is the outer grid - this.scrollarea = null; - if (this._parentGrid == null) { - this.scrollarea = jQuery(document.createElement("div")) - .addClass("egwGridView_scrollarea") - .scroll(this, function (e) { - // Clear any older scroll timeout - if (e.data._scrollTimeout) { - window.clearTimeout(e.data._scrollTimeout); - } - // Clear any existing "invalidate" timeout (as the - // "setViewRange" function triggered by the scroll event - // forces an "invalidate"). - if (e.data._invalidateTimeout) { - window.clearTimeout(e.data._invalidateTimeout); - e.data._invalidateTimeout = null; - } - // Set a new timeout which calls the setViewArea - // function - e.data._scrollTimeout = window.setTimeout(jQuery.proxy(function () { - const newRange = et2_range(this.scrollarea.scrollTop() - et2_dataview_grid.ET2_GRID_VIEW_EXT, this._scrollHeight + et2_dataview_grid.ET2_GRID_VIEW_EXT * 2); - if (!et2_rangeEqual(newRange, this._viewRange)) { - this.setViewRange(newRange); - } - }, e.data), et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT); - }) - .height(this._scrollHeight) - .appendTo(this.outerCell); - } - // Create the inner table - const table = jQuery(document.createElement("table")) - .addClass("egwGridView_grid") - .appendTo(this.scrollarea ? this.scrollarea : this.outerCell); - this.innerTbody = jQuery(document.createElement("tbody")) - .appendTo(table); - // Set the tr as container element - this.appendNode(jQuery(this.tr[0])); - } -} -/** - * Determines how many pixels the view range of the gridview is extended inside - * the scroll callback. - */ -et2_dataview_grid.ET2_GRID_VIEW_EXT = 50; -/** - * Determines the timeout after which the scroll-event is processed. - */ -et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT = 50; -/** - * Determines the timeout after which the invalidate-request gets processed. - */ -et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT = 25; -/** - * Determines how many elements are kept displayed outside of the current view - * range until they get removed. - */ -et2_dataview_grid.ET2_GRID_HOLD_COUNT = 50; -//# sourceMappingURL=et2_dataview_view_grid.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_resizeable.js b/api/js/etemplate/et2_dataview_view_resizeable.js deleted file mode 100644 index cdb61ee6e9..0000000000 --- a/api/js/etemplate/et2_dataview_view_resizeable.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * EGroupware eTemplate2 - Functions which allow resizing of table headers - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -import { egw } from "../jsapi/egw_global"; -/** - * This set of functions is currently only supporting resizing in ew-direction - */ -export class et2_dataview_view_resizable { - // In resize region returns whether the mouse is currently in the - // "resizeRegion" - static inResizeRegion(_x, _elem) { - var ol = _x - _elem.offset().left; - return (ol > (_elem.outerWidth(true) - et2_dataview_view_resizable.RESIZE_BORDER)); - } - static startResize(_outerElem, _elem, _callback, _column) { - if (this.overlay == null || this.helper == null) { - // Prevent text selection - // FireFox handles highlight prevention (text selection) different than other browsers - if (typeof _elem[0].style.MozUserSelect != "undefined") { - _elem[0].style.MozUserSelect = "none"; - } - else { - _elem[0].onselectstart = function () { - return false; - }; - } - // Indicate resizing is in progress - jQuery(_outerElem).addClass('egwResizing'); - // Reset the "didResize" flag - this.didResize = false; - // Create the resize helper - var left = _elem.offset().left; - this.helper = jQuery(document.createElement("div")) - .addClass("egwResizeHelper") - .appendTo("body") - .css("top", _elem.offset().top + "px") - .css("left", left + "px") - .css("height", _outerElem.outerHeight(true) + "px"); - // Create the overlay which will be catching the mouse movements - this.overlay = jQuery(document.createElement("div")) - .addClass("egwResizeOverlay") - .bind("mousemove", function (e) { - this.didResize = true; - this.resizeWidth = Math.max(e.pageX - left + et2_dataview_view_resizable.RESIZE_ADD, _column && _column.minWidth ? _column.minWidth : et2_dataview_view_resizable.RESIZE_MIN_WIDTH); - this.helper.css("width", this.resizeWidth + "px"); - }.bind(this)) - .bind("mouseup", function () { - this.stopResize(_outerElem); - // Reset text selection - _elem[0].onselectstart = null; - // Call the callback if the user actually performed a resize - if (this.didResize) { - _callback(this.resizeWidth); - } - }.bind(this)) - .appendTo("body"); - } - } - static stopResize(_outerElem) { - jQuery(_outerElem).removeClass('egwResizing'); - if (this.helper != null) { - this.helper.remove(); - this.helper = null; - } - if (this.overlay != null) { - this.overlay.remove(); - this.overlay = null; - } - } -} -// Define some constants -et2_dataview_view_resizable.RESIZE_BORDER = 12; -et2_dataview_view_resizable.RESIZE_MIN_WIDTH = 25; -et2_dataview_view_resizable.RESIZE_ADD = 2; // Used to ensure mouse is under the resize element after resizing has finished -et2_dataview_view_resizable.helper = null; -et2_dataview_view_resizable.overlay = null; -et2_dataview_view_resizable.didResize = false; -et2_dataview_view_resizable.resizeWidth = 0; -et2_dataview_view_resizable.makeResizeable = function (_elem, _callback, _context) { - // Get the table surrounding the given element - this element is used to - // align the helper properly - var outerTable = _elem.closest("table"); - // Bind the "mousemove" event in the "resize" namespace - _elem.bind("mousemove.resize", function (e) { - var stopResize = false; - // Stop switch to resize cursor if the mouse position - // is more intended for scrollbar not the resize edge - // 8pixel is an arbitary number for scrolbar area - if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) { - stopResize = true; - } - _elem.css("cursor", et2_dataview_view_resizable.inResizeRegion(e.pageX, _elem) && !stopResize ? "ew-resize" : "auto"); - }); - // Bind the "mousedown" event in the "resize" namespace - _elem.bind("mousedown.resize", function (e) { - var stopResize = false; - // Stop resize if the mouse position is more intended - // for scrollbar not the resize edge - // 8pixel is an arbitary number for scrolbar area - if (e.target.clientHeight < e.target.scrollHeight && e.target.offsetWidth - e.offsetX <= 8) { - stopResize = true; - } - // Do not triger startResize if clicked element is select-tag, as it may causes conflict in some browsers - if (et2_dataview_view_resizable.inResizeRegion(e.pageX, _elem) && e.target.tagName != 'SELECT' && !stopResize) { - // Start the resizing - et2_dataview_view_resizable.startResize(outerTable, _elem, function (_w) { - _callback.call(_context, _w); - }, _context); - } - }); - // Bind double click for auto-size - _elem.dblclick(function (e) { - // Just show message for relative width columns - if (_context && _context.relativeWidth) { - return egw.message(egw.lang('You tried to automatically size a flex column, which always takes the rest of the space', 'info')); - } - // Find column class - it's usually the first one - var col_class = ''; - for (var i = 0; i < this.classList.length; i++) { - if (this.classList[i].indexOf('gridCont') === 0) { - col_class = this.classList[i]; - break; - } - } - // Find widest part, including header - var column = jQuery(this); - column.children().css('width', 'auto'); - var max_width = column.children().children().innerWidth(); - var padding = column.outerWidth(true) - max_width; - var resize = jQuery(this).closest('.egwGridView_outer') - .find('tbody td.' + col_class + '> div:first-child') - .add(column.children()) - // Set column width to auto to allow space for everything to flow - .css('width', 'auto'); - resize.children() - .css({ 'white-space': 'nowrap' }) - .each(function () { - var col = jQuery(this); - // Find visible (text) children and force them to not wrap - var children = col.find('span:visible, time:visible, label:visible') - .css({ 'white-space': 'nowrap' }); - this.offsetWidth; - children.each(function () { - var child = jQuery(this); - this.offsetWidth; - if (child.outerWidth() > max_width) { - max_width = child.outerWidth(); - } - window.getComputedStyle(this).width; - }); - this.offsetWidth; - if (col.innerWidth() > max_width) { - max_width = col.innerWidth(); - } - // Reset children - children.css('white-space', ''); - children.css('display', ''); - }) - .css({ 'white-space': '' }); - // Reset column - column.children().css('width', ''); - resize.css('width', ''); - _callback.call(_context, max_width + padding); - }); -}; -et2_dataview_view_resizable.et2_dataview_resetResizeable = function (_elem) { - // Remove all events in the ".resize" namespace from the element - _elem.unbind(".resize"); -}; -//# sourceMappingURL=et2_dataview_view_resizeable.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_row.js b/api/js/etemplate/et2_dataview_view_row.js deleted file mode 100644 index 7d53df0eb1..0000000000 --- a/api/js/etemplate/et2_dataview_view_row.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * EGroupware eTemplate2 - dataview - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - egw_action.egw_action; - - et2_dataview_view_container; -*/ -import { et2_dataview_IViewRange } from "./et2_dataview_interfaces"; -import { et2_dataview_container } from "./et2_dataview_view_container"; -export class et2_dataview_row extends et2_dataview_container { - /** - * Creates the row container. Use the "setRow" function to load the actual - * row content. - * - * @param _parent is the row parent container. - */ - constructor(_parent) { - // Call the inherited constructor - super(_parent); - // Create the outer "tr" tag and append it to the container - this.tr = jQuery(document.createElement("tr")); - this.appendNode(this.tr); - // Grid row which gets expanded when clicking on the corresponding - // button - this.expansionContainer = null; - this.expansionVisible = false; - // Toggle button which is used to show and hide the expansionContainer - this.expansionButton = null; - } - destroy() { - if (this.expansionContainer != null) { - this.expansionContainer.destroy(); - } - super.destroy(); - } - clear() { - this.tr.empty(); - } - makeExpandable(_expandable, _callback, _context) { - if (_expandable) { - // Create the tr and the button if this has not been done yet - if (!this.expansionButton) { - this.expansionButton = jQuery(document.createElement("span")); - this.expansionButton.addClass("arrow closed"); - } - // Update context - var self = this; - this.expansionButton.off("click").on("click", function (e) { - self._handleExpansionButtonClick(_callback, _context); - e.stopImmediatePropagation(); - }); - jQuery("td:first", this.tr).prepend(this.expansionButton); - } - else { - // If the row is made non-expandable, remove the created DOM-Nodes - if (this.expansionButton) { - this.expansionButton.remove(); - } - if (this.expansionContainer) { - this.expansionContainer.destroy(); - } - this.expansionButton = null; - this.expansionContainer = null; - } - } - removeFromTree() { - if (this.expansionContainer) { - this.expansionContainer.removeFromTree(); - } - this.expansionContainer = null; - this.expansionButton = null; - super.removeFromTree(); - } - getDOMNode() { - return this.tr[0]; - } - getJNode() { - return this.tr; - } - getHeight() { - var h = super.getHeight(); - if (this.expansionContainer && this.expansionVisible) { - h += this.expansionContainer.getHeight(); - } - return h; - } - getAvgHeightData() { - // Only take the height of the own tr into account - //var oldVisible = this.expansionVisible; - this.expansionVisible = false; - var res = { - "avgHeight": this.getHeight(), - "avgCount": 1 - }; - this.expansionVisible = true; - return res; - } - /** -- PRIVATE FUNCTIONS -- **/ - _handleExpansionButtonClick(_callback, _context) { - // Create the "expansionContainer" if it does not exist yet - if (!this.expansionContainer) { - this.expansionContainer = _callback.call(_context); - this.expansionContainer.insertIntoTree(this.tr); - this.expansionVisible = false; - } - // Toggle the visibility of the expansion tr - this.expansionVisible = !this.expansionVisible; - jQuery(this.expansionContainer._nodes[0]).toggle(this.expansionVisible); - // Set the class of the arrow - if (this.expansionVisible) { - this.expansionButton.addClass("opened"); - this.expansionButton.removeClass("closed"); - } - else { - this.expansionButton.addClass("closed"); - this.expansionButton.removeClass("opened"); - } - this.invalidate(); - } - /** -- Implementation of et2_dataview_IViewRange -- **/ - setViewRange(_range) { - if (this.expansionContainer && this.expansionVisible - && this.expansionContainer.implements(et2_dataview_IViewRange)) { - // Substract the height of the own row from the container - var oh = jQuery(this._nodes[0]).height(); - _range.top -= oh; - // Proxy the setViewRange call to the expansion container - this.expansionContainer.setViewRange(_range); - } - } -} -//# sourceMappingURL=et2_dataview_view_row.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_rowProvider.js b/api/js/etemplate/et2_dataview_view_rowProvider.js deleted file mode 100644 index 4966619fde..0000000000 --- a/api/js/etemplate/et2_dataview_view_rowProvider.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * EGroupware eTemplate2 - Class which contains a factory method for rows - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inheritance; - et2_core_interfaces; - et2_core_arrayMgr; - et2_core_widget; -*/ -/** - * The row provider contains prototypes (full clonable dom-trees) - * for all registered row types. - */ -export class et2_dataview_rowProvider { - /** - * - * @param _outerId - * @param _columnIds - */ - constructor(_outerId, _columnIds) { - // Copy the given parameters - this._outerId = _outerId; - this._columnIds = _columnIds; - this._prototypes = {}; - this._template = null; - this._mgrs = null; - this._rootWidget = null; - // Create the default row "prototypes" - this._createFullRowPrototype(); - this._createDefaultPrototype(); - this._createEmptyPrototype(); - this._createLoadingPrototype(); - } - destroy() { - this._template = null; - this._mgrs = null; - this._rootWidget = null; - this._prototypes = {}; - this._columnIds = []; - } - getColumnCount() { - return this._columnIds.length; - } - /** - * Returns a clone of the prototype with the given name. If the generator - * callback function is given, this function is called if the prototype - * does not yet registered. - * - * @param {string} _name - * @param {function} _generator - * @param {object} _context - */ - getPrototype(_name, _generator, _context) { - if (typeof this._prototypes[_name] == "undefined") { - if (typeof _generator != "undefined") { - this._prototypes[_name] = _generator.call(_context, this._outerId, this._columnIds); - } - else { - return null; - } - } - return this._prototypes[_name].clone(); - } - /* ---- PRIVATE FUNCTIONS ---- */ - _createFullRowPrototype() { - var tr = jQuery(document.createElement("tr")); - var td = jQuery(document.createElement("td")) - .addClass(this._outerId + "_td_fullRow") - .attr("colspan", this._columnIds.length) - .appendTo(tr); - var div = jQuery(document.createElement("div")) - .addClass(this._outerId + "_div_fullRow") - .appendTo(td); - this._prototypes["fullRow"] = tr; - } - _createDefaultPrototype() { - var tr = jQuery(document.createElement("tr")); - // Append a td for each column - for (var column of this._columnIds) { - if (!column) - continue; - var td = jQuery(document.createElement("td")) - .addClass(this._outerId + "_td_" + column) - .appendTo(tr); - var div = jQuery(document.createElement("div")) - .addClass(this._outerId + "_div_" + column) - .addClass("innerContainer") - .appendTo(td); - } - this._prototypes["default"] = tr; - } - _createEmptyPrototype() { - this._prototypes["empty"] = jQuery(document.createElement("tr")); - } - _createLoadingPrototype() { - var fullRow = this.getPrototype("fullRow"); - jQuery("div", fullRow).addClass("loading"); - this._prototypes["loading"] = fullRow; - } -} -//# sourceMappingURL=et2_dataview_view_rowProvider.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_spacer.js b/api/js/etemplate/et2_dataview_view_spacer.js deleted file mode 100644 index 4496f3d518..0000000000 --- a/api/js/etemplate/et2_dataview_view_spacer.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * EGroupware eTemplate2 - Class which contains the spacer container - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_dataview_view_container; -*/ -import { et2_dataview_container } from "./et2_dataview_view_container"; -/** - * @augments et2_dataview_container - */ -export class et2_dataview_spacer extends et2_dataview_container { - /** - * Constructor - * - * @param _parent - * @param _rowProvider - * @memberOf et2_dataview_spacer - */ - constructor(_parent, _rowProvider) { - // Call the inherited container constructor - super(_parent); - // Initialize the row count and the row height - this._count = 0; - this._rowHeight = 19; - this._avgSum = 0; - this._avgCount = 0; - // Get the spacer row and append it to the container - this.spacerNode = _rowProvider.getPrototype("spacer", this._createSpacerPrototype, this); - this._phDiv = jQuery("td", this.spacerNode); - this.appendNode(this.spacerNode); - } - setCount(_count, _rowHeight) { - // Set the new count and _rowHeight if given - this._count = _count; - if (typeof _rowHeight !== "undefined") { - this._rowHeight = _rowHeight; - } - // Update the element height - this._phDiv.height(this._count * this._rowHeight); - // Call the invalidate function - this.invalidate(); - } - getCount() { - return this._count; - } - getHeight() { - // Set the calculated height, so that "invalidate" will work correctly - this._height = this._count * this._rowHeight; - return this._height; - } - getAvgHeightData() { - if (this._avgCount > 0) { - return { - "avgHeight": this._avgSum / this._avgCount, - "avgCount": this._avgCount - }; - } - return null; - } - addAvgHeight(_height) { - this._avgSum += _height; - this._avgCount++; - } - /* ---- PRIVATE FUNCTIONS ---- */ - _createSpacerPrototype(_outerId, _columnIds) { - var tr = jQuery(document.createElement("tr")); - var td = jQuery(document.createElement("td")) - .addClass("egwGridView_spacer") - .addClass(_outerId + "_spacer_fullRow") - .attr("colspan", _columnIds.length) - .appendTo(tr); - return tr; - } -} -//# sourceMappingURL=et2_dataview_view_spacer.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_tile.js b/api/js/etemplate/et2_dataview_view_tile.js deleted file mode 100644 index 1388e73615..0000000000 --- a/api/js/etemplate/et2_dataview_view_tile.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * EGroupware eTemplate2 - dataview code - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage dataview - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2014 - * @version $Id: et2_dataview_view_container_1.js 46338 2014-03-20 09:40:37Z ralfbecker $ - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_dataview_interfaces; -*/ -import { et2_dataview_row } from "./et2_dataview_view_row"; -/** - * Displays tiles or thumbnails (squares) instead of full rows. - * - * It's important that the template specifies a fixed width and height (via CSS) - * so that the rows and columns work out properly. - * - */ -export class et2_dataview_tile extends et2_dataview_row { - /** - * Creates the row container. Use the "setRow" function to load the actual - * row content. - * - * @param _parent is the row parent container. - * @memberOf et2_dataview_row - */ - constructor(_parent) { - // Call the inherited constructor - super(_parent); - this.columns = 4; - // Make sure the needed class is there to get the CSS - this.tr.addClass('tile'); - } - makeExpandable(_expandable, _callback, _context) { - // Nope. It mostly works, it's just weird. - } - getAvgHeightData() { - var res = { - "avgHeight": this.getHeight() / this.columns, - "avgCount": this.columns - }; - return res; - } - /** - * Returns the height for the tile. - * - * This is where we do the magic. If a new row should start, we return the proper - * height. If this should be another tile in the same row, we say it has 0 height. - * @returns {Number} - */ - getHeight() { - if (this._index % this.columns == 0) { - return super.getHeight(); - } - else { - return 0; - } - } - /** - * Broadcasts an invalidation through the container tree. Marks the own - * height as invalid. - */ - invalidate() { - if (this._inTree && this.tr) { - var template_width = jQuery('.innerContainer', this.tr).children().outerWidth(true); - if (template_width) { - this.tr.css('width', template_width + (this.tr.outerWidth(true) - this.tr.width())); - } - } - this._recalculate_columns(); - super.invalidate(); - } - /** - * Recalculate how many columns we can fit in a row. - * While browser takes care of the actual layout, we need this for proper - * pagination. - */ - _recalculate_columns() { - if (this._inTree && this.tr && this.tr.parent()) { - this.columns = Math.max(1, parseInt(this.tr.parent().innerWidth() / this.tr.outerWidth(true))); - } - } -} -//# sourceMappingURL=et2_dataview_view_tile.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_customfields.js b/api/js/etemplate/et2_extension_customfields.js deleted file mode 100644 index adceca6a30..0000000000 --- a/api/js/etemplate/et2_extension_customfields.js +++ /dev/null @@ -1,635 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Custom fields object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2011 - */ -/*egw:uses - lib/tooltip; - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_xml; - et2_core_DOMWidget; - et2_core_inputWidget; -*/ -import { et2_createWidget, et2_register_widget, et2_registry } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { et2_cloneObject, et2_no_init } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -export class et2_customfields_list extends et2_valueWidget { - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_customfields_list._attributes, _child || {})); - this.rows = {}; - this.widgets = {}; - this.detachedNodes = []; - // Some apps (infolog edit) don't give ID, so assign one to get settings - if (!this.id) { - this.id = _attrs.id = et2_customfields_list.DEFAULT_ID; - // Add all attributes hidden in the content arrays to the attributes - // parameter - this.transformAttributes(_attrs); - // Create a local copy of the options object - this.options = et2_cloneObject(_attrs); - } - // Create the table body and the table - this.tbody = jQuery(document.createElement("tbody")); - this.table = jQuery(document.createElement("table")) - .addClass("et2_grid et2_customfield_list"); - this.table.append(this.tbody); - if (!this.options.fields) - this.options.fields = {}; - if (typeof this.options.fields === 'string') { - const fields = this.options.fields.split(','); - this.options.fields = {}; - for (var i = 0; i < fields.length; i++) { - this.options.fields[fields[i]] = true; - } - } - if (this.options.type_filter && typeof this.options.type_filter == "string") { - this.options.type_filter = this.options.type_filter.split(","); - } - if (this.options.type_filter) { - const already_filtered = !jQuery.isEmptyObject(this.options.fields); - for (let field_name in this.options.customfields) { - // Already excluded? - if (already_filtered && !this.options.fields[field_name]) - continue; - if (!this.options.customfields[field_name].type2 || this.options.customfields[field_name].type2.length == 0 || - this.options.customfields[field_name].type2 == '0') { - // No restrictions - this.options.fields[field_name] = true; - continue; - } - const types = typeof this.options.customfields[field_name].type2 == 'string' ? this.options.customfields[field_name].type2.split(",") : this.options.customfields[field_name].type2; - this.options.fields[field_name] = false; - for (var i = 0; i < types.length; i++) { - if (jQuery.inArray(types[i], this.options.type_filter) > -1) { - this.options.fields[field_name] = true; - } - } - } - } - this.setDOMNode(this.table[0]); - } - destroy() { - super.destroy(); - this.rows = {}; - this.widgets = {}; - this.detachedNodes = []; - this.tbody = null; - } - /** - * What does this do? I don't know, but when everything is done the second - * time, this makes it work. Otherwise, all custom fields are lost. - */ - assign(_obj) { - this.loadFields(); - } - getDOMNode(_sender) { - // Check whether the _sender object exists inside the management array - if (this.rows && _sender.id && this.rows[_sender.id]) { - return this.rows[_sender.id]; - } - if (this.rows && _sender.id && _sender.id.indexOf("_label") && this.rows[_sender.id.replace("_label", "")]) { - return jQuery(this.rows[_sender.id.replace("_label", "")]).prev("td")[0] || null; - } - return super.getDOMNode(_sender); - } - /** - * Initialize widgets for custom fields - */ - loadFields() { - if (!this.options || !this.options.customfields) - return; - // Already set up - avoid duplicates in nextmatch - if (this.getType() == 'customfields-list' && !this.isInTree() && Object.keys(this.widgets).length > 0) - return; - if (!jQuery.isEmptyObject(this.widgets)) - return; - // Check for global setting changes (visibility) - const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if (global_data && global_data.fields && !this.options.fields) - this.options.fields = global_data.fields; - // For checking app entries - const apps = this.egw().link_app_list(); - // Create the table rows - for (let field_name in this.options.customfields) { - // Skip fields if we're filtering - if (this.getType() != 'customfields-list' && !jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) - continue; - const field = this.options.customfields[field_name]; - let id = this.options.prefix + field_name; - // Need curlies around ID for nm row expansion - if (this.id == '$row') { - id = "{" + this.id + "}" + "[" + this.options.prefix + field_name + "]"; - } - else if (this.id != et2_customfields_list.DEFAULT_ID) { - // Prefix this ID to avoid potential ID collisions - id = this.id + "[" + id + "]"; - } - // Avoid creating field twice - if (!this.rows[id]) { - const row = jQuery(document.createElement("tr")) - .appendTo(this.tbody) - .addClass(this.id + '_' + id); - let cf = jQuery(document.createElement("td")) - .appendTo(row); - if (!field.type) - field.type = 'text";'; - const setup_function = '_setup_' + (apps[field.type] ? 'link_entry' : field.type.replace("-", "_")); - const attrs = jQuery.extend({}, this.options[field_name] ? this.options[field_name] : {}, { - 'id': id, - 'statustext': field.help, - 'needed': field.needed, - 'readonly': this.getArrayMgr("readonlys").isReadOnly(id, "" + this.options.readonly), - 'value': this.options.value[this.options.prefix + field_name] - }); - // Can't have a required readonly, it will warn & be removed later, so avoid the warning - if (attrs.readonly === true) - delete attrs.needed; - if (this.options.onchange) { - attrs.onchange = this.options.onchange; - } - if (this[setup_function]) { - const no_skip = this[setup_function].call(this, field_name, field, attrs); - if (!no_skip) - continue; - } - this.rows[id] = cf[0]; - if (this.getType() == 'customfields-list') { - // No label, cust widget - attrs.readonly = true; - // Widget tooltips don't work in nextmatch because of the creation / binding separation - // Set title to field label so browser will show something - // Add field label & help as data attribute to row, so it can be stylied with CSS (title should be disabled) - row.attr('title', field.label); - row.attr('data-label', field.label); - row.attr('data-field', field_name); - row.attr('data-help', field.help); - this.detachedNodes.push(row[0]); - } - else { - // Label in first column, widget in 2nd - jQuery(document.createElement("td")) - .prependTo(row); - et2_createWidget("label", { id: id + "_label", value: field.label, for: id }, this); - } - // Set any additional attributes set in options, but not for widgets that pass actual options - if (['select', 'radio', 'radiogroup', 'checkbox', 'button'].indexOf(field.type) == -1 && !jQuery.isEmptyObject(field.values)) { - const w = et2_registry[attrs.type ? attrs.type : field.type]; - for (let attr_name in field.values) { - if (typeof w._attributes[attr_name] != "undefined") { - attrs[attr_name] = field.values[attr_name]; - } - } - } - // Create widget - const widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); - } - // Field is not to be shown - if (!this.options.fields || jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) { - jQuery(this.rows[field_name]).show(); - } - else { - jQuery(this.rows[field_name]).hide(); - } - } - } - /** - * Read needed info on available custom fields from various places it's stored. - */ - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - // Add in settings that are objects - // Customized settings for this widget (unlikely) - const data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); - if (global_data) { - for (let key in data) { - // Don't overwrite fields with global values - if (global_data[key] && key !== 'fields') { - data[key] = jQuery.extend(true, {}, data[key], global_data[key]); - } - } - } - for (var key in data) { - _attrs[key] = data[key]; - } - for (let key in global_data) { - if (typeof global_data[key] != 'undefined' && !_attrs[key]) - _attrs[key] = global_data[key]; - } - if (this.id) { - // Set the value for this element - const contentMgr = this.getArrayMgr("content"); - if (contentMgr != null) { - const val = contentMgr.getEntry(this.id); - _attrs["value"] = {}; - let prefix = _attrs["prefix"] || et2_customfields_list.PREFIX; - if (val !== null) { - if (this.id.indexOf(prefix) === 0 && typeof data.fields != 'undefined' && data.fields[this.id.replace(prefix, '')] === true) { - _attrs['value'][this.id] = val; - } - else { - // Only set the values that match desired custom fields - for (let key in val) { - if (key.indexOf(prefix) === 0) { - _attrs["value"][key] = val[key]; - } - } - } - //_attrs["value"] = val; - } - else { - // Check for custom fields directly in record - for (var key in _attrs.customfields) { - _attrs["value"][prefix + key] = contentMgr.getEntry(prefix + key); - } - } - } - } - } - loadFromXML(_node) { - this.loadFields(); - // Load the nodes as usual - super.loadFromXML(_node); - } - set_value(_value) { - if (!this.options.customfields) - return; - for (let field_name in this.options.customfields) { - // Skip fields if we're filtering - if (!jQuery.isEmptyObject(this.options.fields) && !this.options.fields[field_name]) - continue; - // Make sure widget is created, and has the needed function - if (!this.widgets[field_name] || !this.widgets[field_name].set_value) - continue; - let value = _value[this.options.prefix + field_name] ? _value[this.options.prefix + field_name] : null; - // Check if ID was missing - if (value == null && this.id == et2_customfields_list.DEFAULT_ID && this.getArrayMgr("content").getEntry(this.options.prefix + field_name)) { - value = this.getArrayMgr("content").getEntry(this.options.prefix + field_name); - } - switch (this.options.customfields[field_name].type) { - case 'date': - // Date custom fields are always in Y-m-d, which seldom matches user's preference - // which fails when sent to date widget. This is only used for nm rows, when possible - // this is fixed server side - if (value && isNaN(value)) { - value = jQuery.datepicker.parseDate("yy-mm-dd", value); - } - break; - } - this.widgets[field_name].set_value(value); - } - } - /** - * et2_IInput so the custom field can be it's own widget. - */ - getValue() { - // Not using an ID means we have to grab all the widget values, and put them where server knows to look - if (this.id != et2_customfields_list.DEFAULT_ID) { - return null; - } - const value = {}; - for (let field_name in this.widgets) { - if (this.widgets[field_name].getValue && !this.widgets[field_name].options.readonly) { - value[this.options.prefix + field_name] = this.widgets[field_name].getValue(); - } - } - return value; - } - isDirty() { - let dirty = false; - for (let field_name in this.widgets) { - if (this.widgets[field_name].isDirty) { - dirty = dirty || this.widgets[field_name].isDirty(); - } - } - return dirty; - } - resetDirty() { - for (let field_name in this.widgets) { - if (this.widgets[field_name].resetDirty) { - this.widgets[field_name].resetDirty(); - } - } - } - isValid() { - // Individual customfields will handle themselves - return true; - } - /** - * Adapt provided attributes to match options for widget - * - * rows > 1 --> textarea, with rows=rows and cols=len - * !rows --> input, with size=len - * rows = 1 --> input, with size=len, maxlength=len - */ - _setup_text(field_name, field, attrs) { - // No label on the widget itself - delete (attrs.label); - field.type = 'textbox'; - attrs.rows = field.rows > 1 ? field.rows : null; - if (field.len) { - attrs.size = field.len; - if (field.rows == 1) - attrs.maxlength = field.len; - } - return true; - } - _setup_passwd(field_name, field, attrs) { - // No label on the widget itself - delete (attrs.label); - let defaults = { - viewable: true, - plaintext: false, - suggest: 16 - }; - for (let key of Object.keys(defaults)) { - attrs[key] = (field.values && typeof field.values[key] !== "undefined") ? field.values[key] : defaults[key]; - } - return true; - } - _setup_ajax_select(field_name, field, attrs) { - const attributes = ['get_rows', 'get_title', 'id_field', 'template']; - if (field.values) { - for (let i = 0; i < attributes.length; i++) { - if (typeof field.values[attributes[i]] !== 'undefined') { - attrs[attributes[i]] = field.values[attributes[i]]; - } - } - } - return true; - } - _setup_float(field_name, field, attrs) { - // No label on the widget itself - delete (attrs.label); - field.type = 'float'; - if (field.len) { - attrs.size = field.len; - } - return true; - } - _setup_select(field_name, field, attrs) { - // No label on the widget itself - delete (attrs.label); - attrs.rows = field.rows; - // select_options are now send from server-side incl. ones defined via a file in EGroupware root - attrs.tags = field.tags; - return true; - } - _setup_select_account(field_name, field, attrs) { - attrs.empty_label = egw.lang('Select'); - if (field.account_type) { - attrs.account_type = field.account_type; - } - return this._setup_select(field_name, field, attrs); - } - _setup_date(field_name, field, attrs) { - attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d'; - return true; - } - _setup_date_time(field_name, field, attrs) { - attrs.data_format = field.values && field.values.format ? field.values.format : 'Y-m-d H:i:s'; - return true; - } - _setup_htmlarea(field_name, field, attrs) { - attrs.config = field.config ? field.config : {}; - attrs.config.toolbarStartupExpanded = false; - if (field.len) { - attrs.config.width = field.len + 'px'; - } - attrs.config.height = (((field.rows > 0 && field.rows != 'undefined') ? field.rows : 5) * 16) + 'px'; - // We have to push the config modifications into the modifications array, or they'll - // be overwritten by the site config from the server - const data = this.getArrayMgr("modifications").getEntry(this.options.prefix + field_name); - if (data) - jQuery.extend(data.config, attrs.config); - return true; - } - _setup_radio(field_name, field, attrs) { - // 'Empty' label will be first - delete (attrs.label); - if (field.values && field.values['']) { - attrs.label = field.values['']; - delete field.values['']; - } - field.type = 'radiogroup'; - attrs.options = field.values; - return true; - } - _setup_checkbox(field_name, field, attrs) { - // Read-only checkbox is just text - if (attrs.readonly && this.getType() !== "customfields") { - attrs.ro_true = field.label; - } - else if (field.hasOwnProperty('ro_true')) { - attrs.ro_true = field.ro_true; - } - if (field.hasOwnProperty('ro_false')) { - attrs.ro_false = field.ro_false; - } - return true; - } - /** - * People set button attributes as - * label: javascript - */ - _setup_button(field_name, field, attrs) { - // No label on the widget itself - delete (attrs.label); - attrs.label = field.label; - if (this.getType() == 'customfields-list') { - // No buttons in a list, it causes problems with detached nodes - return false; - } - // Simple case, one widget for a custom field - if (!field.values || typeof field.values != 'object' || Object.keys(field.values).length == 1) { - for (let key in field.values) { - attrs.label = key; - attrs.onclick = field.values[key]; - } - if (!attrs.label) { - attrs.label = 'No "label=onclick" in values!'; - attrs.onclick = function () { return false; }; - } - return !attrs.readonly; - } - else { - // Complicated case, a single custom field you get multiple widgets - // Handle it all here, since this is the exception - const row = jQuery('tr', this.tbody).last(); - let cf = jQuery('td', row); - // Label in first column, widget in 2nd - cf.text(field.label + ""); - cf = jQuery(document.createElement("td")) - .appendTo(row); - for (var key in field.values) { - const button_attrs = jQuery.extend({}, attrs); - button_attrs.label = key; - button_attrs.onclick = field.values[key]; - button_attrs.id = attrs.id + '_' + key; - // This controls where the button is placed in the DOM - this.rows[button_attrs.id] = cf[0]; - // Do not store in the widgets list, one name for multiple widgets would cause problems - /*this.widgets[field_name] = */ et2_createWidget(attrs.type ? attrs.type : field.type, button_attrs, this); - } - return false; - } - } - _setup_link_entry(field_name, field, attrs) { - if (field.type === 'filemanager') { - return this._setup_filemanager(field_name, field, attrs); - } - // No label on the widget itself - delete (attrs.label); - attrs.type = "link-entry"; - attrs.only_app = typeof field.only_app == "undefined" ? field.type : field.only_app; - return true; - } - _setup_filemanager(field_name, field, attrs) { - attrs.type = 'vfs-upload'; - delete (attrs.label); - if (this.getType() == 'customfields-list') { - // No special UI needed? - return true; - } - else { - // Complicated case, a single custom field you get multiple widgets - // Handle it all here, since this is the exception - const row = jQuery('tr', this.tbody).last(); - let cf = jQuery('td', row); - // Label in first column, widget in 2nd - cf.text(field.label + ""); - cf = jQuery(document.createElement("td")) - .appendTo(row); - // Create upload widget - let widget = this.widgets[field_name] = et2_createWidget(attrs.type ? attrs.type : field.type, attrs, this); - // This controls where the widget is placed in the DOM - this.rows[attrs.id] = cf[0]; - jQuery(widget.getDOMNode(widget)).css('vertical-align', 'top'); - // Add a link to existing VFS file - const select_attrs = jQuery.extend({}, attrs, - // Filemanager select - { - label: '', - mode: widget.options.multiple ? 'open-multiple' : 'open', - method: 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing', - method_id: attrs.path, - button_label: egw.lang('Link') - }, { type: 'vfs-select' }); - select_attrs.id = attrs.id + '_vfs_select'; - // This controls where the button is placed in the DOM - this.rows[select_attrs.id] = cf[0]; - // Do not store in the widgets list, one name for multiple widgets would cause problems - widget = et2_createWidget(select_attrs.type, select_attrs, this); - jQuery(widget.getDOMNode(widget)).css('vertical-align', 'top').prependTo(cf); - } - return false; - } - /** - * Display links in list as CF name - * @param field_name - * @param field - * @param attrs - */ - _setup_url(field_name, field, attrs) { - if (this.getType() == 'customfields-list') { - attrs.label = field.label; - } - return true; - } - /** - * Set which fields are visible, by name - * - * Note: no # prefix on the name - * - */ - set_visible(_fields) { - for (let name in _fields) { - if (this.rows[this.options.prefix + name]) { - if (_fields[name]) { - jQuery(this.rows[this.options.prefix + name]).show(); - } - else { - jQuery(this.rows[this.options.prefix + name]).hide(); - } - } - this.options.fields[name] = _fields[name]; - } - } - /** - * Code for implementing et2_IDetachedDOM - */ - getDetachedAttributes(_attrs) { - _attrs.push("value", "class"); - } - getDetachedNodes() { - return this.detachedNodes ? this.detachedNodes : []; - } - setDetachedAttributes(_nodes, _values) { - // Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - // Show the row if there's a value, hide it if there is no value - for (let i = 0; i < _nodes.length; i++) { - // toggle() needs a boolean to do what we want - const key = _nodes[i].getAttribute('data-field'); - jQuery(_nodes[i]).toggle(_values.fields[key] && _values.value[this.options.prefix + key] ? true : false); - } - } -} -et2_customfields_list._attributes = { - 'customfields': { - 'name': 'Custom fields', - 'description': 'Auto filled', - 'type': 'any' - }, - 'fields': { - 'name': 'Custom fields', - 'description': 'Auto filled', - 'type': 'any' - }, - 'value': { - 'name': 'Custom fields', - 'description': 'Auto filled', - 'type': "any" - }, - 'type_filter': { - 'name': 'Field filter', - "default": "", - "type": "any", - "description": "Filter displayed custom fields by their 'type2' attribute" - }, - 'private': { - ignore: true, - type: 'boolean' - }, - 'sub_app': { - 'name': 'sub app name', - 'type': "string", - 'description': "Name of sub application" - }, - // Allow onchange so you can put handlers on the sub-widgets - 'onchange': { - "name": "onchange", - "type": "string", - "default": et2_no_init, - "description": "JS code which is executed when the value changes." - }, - // Allow changing the field prefix. Normally it's the constant but importexport filter changes it. - "prefix": { - name: "prefix", - type: "string", - default: "#", - description: "Custom prefix for custom fields. Default #" - } -}; -et2_customfields_list.legacyOptions = ["type_filter", "private", "fields"]; // Field restriction & private done server-side -et2_customfields_list.PREFIX = '#'; -et2_customfields_list.DEFAULT_ID = "custom_fields"; -et2_register_widget(et2_customfields_list, ["customfields", "customfields-list"]); -//# sourceMappingURL=et2_extension_customfields.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_itempicker_actions.js b/api/js/etemplate/et2_extension_itempicker_actions.js deleted file mode 100644 index 4f079ea238..0000000000 --- a/api/js/etemplate/et2_extension_itempicker_actions.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Itempicker object - * derived from et2_link_entry widget @copyright 2011 Nathan Gray - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Christian Binder - * @author Nathan Gray - * @copyright 2012 Christian Binder - * @copyright 2011 Nathan Gray - */ -function itempickerDocumentAction(context, data) { - "use strict"; - let formid = "itempicker_action_form"; - let form = "
" - + "" - + "" - + "" - + "
"; - jQuery("body").append(form); - jQuery("#" + formid).submit().remove(); -} -//# sourceMappingURL=et2_extension_itempicker_actions.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch.js b/api/js/etemplate/et2_extension_nextmatch.js deleted file mode 100644 index 2abb156b95..0000000000 --- a/api/js/etemplate/et2_extension_nextmatch.js +++ /dev/null @@ -1,3554 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Nextmatch object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - - // Include the action system - egw_action.egw_action; - egw_action.egw_action_popup; - egw_action.egw_action_dragdrop; - egw_action.egw_menu_dhtmlx; - - // Include some core classes - et2_core_widget; - et2_core_interfaces; - et2_core_DOMWidget; - - // Include all widgets the nextmatch extension will create - et2_widget_template; - et2_widget_grid; - et2_widget_selectbox; - et2_widget_selectAccount; - et2_widget_taglist; - et2_extension_customfields; - - // Include all nextmatch subclasses - et2_extension_nextmatch_rowProvider; - et2_extension_nextmatch_controller; - et2_widget_dynheight; - - // Include the grid classes - et2_dataview; - -*/ -import { et2_csvSplit, et2_no_init } from "./et2_core_common"; -import { et2_IResizeable, implements_methods, et2_implements_registry } from "./et2_core_interfaces"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_DOMWidget } from "./et2_core_DOMWidget"; -import { et2_baseWidget } from "./et2_core_baseWidget"; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { et2_selectbox } from "./et2_widget_selectbox"; -import { et2_nextmatch_rowProvider } from "./et2_extension_nextmatch_rowProvider"; -import { et2_nextmatch_controller } from "./et2_extension_nextmatch_controller"; -import { et2_dataview } from "./et2_dataview"; -import { et2_dataview_column } from "./et2_dataview_model_columns"; -import { et2_customfields_list } from "./et2_extension_customfields"; -import { et2_link_entry } from "./et2_widget_link"; -import { et2_dialog } from "./et2_widget_dialog"; -import { et2_grid } from "./et2_widget_grid"; -import { et2_dataview_grid } from "./et2_dataview_view_grid"; -import { et2_taglist } from "./et2_widget_taglist"; -import { et2_selectAccount } from "./et2_widget_selectAccount"; -import { et2_dynheight } from "./et2_widget_dynheight"; -import { et2_arrayMgr } from "./et2_core_arrayMgr"; -import { egw } from "../jsapi/egw_global"; -import { et2_compileLegacyJS } from "./et2_core_legacyJSFunctions"; -import { egwIsMobile } from "../egw_action/egw_action_common.js"; -export const et2_INextmatchHeader = "et2_INextmatchHeader"; -et2_implements_registry.et2_INextmatchHeader = function (obj) { - return implements_methods(obj, ["setNextmatch"]); -}; -export const et2_INextmatchSortable = "et2_INextmatchSortable"; -et2_implements_registry.et2_INextmatchSortable = function (obj) { - return implements_methods(obj, ["setSortmode"]); -}; -/** - * Class which implements the "nextmatch" XET-Tag - * - * NM header is build like this in DOM - * - * +- nextmatch_header -----+------------+----------+--------+---------+--------------+-----------+-------+ - * + header_left | search.. | header_row | category | filter | filter2 | header_right | favorites | count | - * +-------------+----------+------------+----------+--------+---------+--------------+-----------+-------+ - * - * everything left incl. standard filters is floated left: - * +- nextmatch_header -----+------------+----------+--------+---------+ - * + header_left | search.. | header_row | category | filter | filter2 | - * +-------------+----------+------------+----------+--------+---------+ - * everything from header_right on is floated right: - * +--------------+-----------+-------+ - * | header_right | favorites | count | - * +--------------+-----------+-------+ - * @augments et2_DOMWidget - */ -export class et2_nextmatch extends et2_DOMWidget { - /** - * Constructor - * - * @memberOf et2_nextmatch - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch._attributes, _child || {})); - // Nextmatch can't render while hidden, we store refresh requests for later - this._queued_refreshes = []; - // When printing, we change the layout around. Keep some values so it can be restored after - this.print = { - old_height: 0, - row_selector: '', - orientation_style: null - }; - this.activeFilters = { col_filter: {} }; - this.columns = []; - // keeps sorted columns - this.sortedColumnsList = []; - // Directly set current col_filters from settings - jQuery.extend(this.activeFilters.col_filter, this.options.settings.col_filter); - /* - Process selected custom fields here, so that the settings are correctly - set before the row template is parsed - */ - const prefs = this._getPreferences(); - const cfs = {}; - for (let i = 0; i < prefs.visible.length; i++) { - if (prefs.visible[i].indexOf(et2_nextmatch_customfields.PREFIX) == 0) { - cfs[prefs.visible[i].substr(1)] = !prefs.negated; - } - } - const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if (typeof global_data == 'object' && global_data != null) { - global_data.fields = cfs; - } - this.div = jQuery(document.createElement("div")) - .addClass("et2_nextmatch"); - this.header = et2_createWidget("nextmatch_header_bar", {}, this); - this.innerDiv = jQuery(document.createElement("div")) - .appendTo(this.div); - // Create the dynheight component which dynamically scales the inner - // container. - this.dynheight = this._getDynheight(); - // Create the outer grid container - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - // Blank placeholder - this.blank = jQuery(document.createElement("div")) - .appendTo(this.dataview.table); - // We cannot create the grid controller now, as this depends on the grid - // instance, which can first be created once we have the columns - this.controller = null; - this.rowProvider = null; - } - /** - * Destroys all - */ - destroy() { - // Stop auto-refresh - if (this._autorefresh_timer) { - window.clearInterval(this._autorefresh_timer); - this._autorefresh_timer = null; - } - // Unbind handler used for toggling autorefresh - jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch'); - jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch'); - // Free the grid components - this.dataview.destroy(); - if (this.rowProvider) { - this.rowProvider.destroy(); - } - if (this.controller) { - this.controller.destroy(); - } - this.dynheight.destroy(); - super.destroy(); - } - getController() { - return this.controller; - } - /** - * Loads the nextmatch settings - * - * @param {object} _attrs - */ - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - if (this.id) { - const entry = this.getArrayMgr("content").data; - _attrs["settings"] = {}; - if (entry) { - _attrs["settings"] = entry; - // Make sure there's an action var parameter - if (_attrs["settings"]["actions"] && !_attrs.settings["action_var"]) { - _attrs.settings.action_var = "action"; - } - // Merge settings mess into attributes - for (let attr in this.attributes) { - if (_attrs.settings[attr]) { - _attrs[attr] = _attrs.settings[attr]; - delete _attrs.settings[attr]; - } - } - } - } - } - doLoadingFinished() { - super.doLoadingFinished(); - if (!this.dynheight) { - this.dynheight = this._getDynheight(); - } - // Register handler for dropped files, if possible - if (this.options.settings.row_id) { - // Appname should be first part of the template name - const split = this.options.template.split('.'); - const appname = split[0]; - // Check link registry - if (this.egw().link_get_registry(appname)) { - const self = this; - // Register a handler - // @ts-ignore - jQuery(this.div) - .on('dragenter', '.egwGridView_grid tr', function (e) { - // Figure out _which_ row - const row = self.controller.getRowByNode(this); - if (!row || !row.uid) { - return false; - } - e.stopPropagation(); - e.preventDefault(); - // Indicate acceptance - if (row.controller && row.controller._selectionMgr) { - row.controller._selectionMgr.setFocused(row.uid, true); - } - return false; - }) - .on('dragexit', '.egwGridView_grid tr', function () { - self.controller._selectionMgr.setFocused(); - }) - .on('dragover', '.egwGridView_grid tr', false).attr("dropzone", "copy") - .on('drop', '.egwGridView_grid tr', function (e) { - self.handle_drop(e, this); - return false; - }); - } - } - // stop invalidation in no visible tabs - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function () { - if (this.controller && this.controller._grid) { - this.controller._grid.doInvalidate = false; - } - }, this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function () { - if (this.controller && this.controller._grid) { - this.controller._grid.doInvalidate = true; - } - }, this)); - return true; - } - /** - * Implements the et2_IResizeable interface - lets the dynheight manager - * update the width and height and then update the dataview container. - */ - resize() { - if (this.dynheight) { - this.dynheight.update(function (_w, _h) { - this.dataview.resize(_w, _h); - }, this); - } - } - /** - * Sorts the nextmatch widget by the given ID. - * - * @param {string} _id is the id of the data entry which should be sorted. - * @param {boolean} _asc if true, the elements are sorted ascending, otherwise - * descending. If not set, the sort direction will be determined - * automatically. - * @param {boolean} _update true/undefined: call applyFilters, false: only set sort - */ - sortBy(_id, _asc, _update) { - if (typeof _update == "undefined") { - _update = true; - } - // Create the "sort" entry in the active filters if it did not exist - // yet. - if (typeof this.activeFilters["sort"] == "undefined") { - this.activeFilters["sort"] = { - "id": null, - "asc": true - }; - } - // Determine the sort direction automatically if it is not set - if (typeof _asc == "undefined") { - _asc = true; - if (this.activeFilters["sort"].id == _id) { - _asc = !this.activeFilters["sort"].asc; - } - } - // Set the sortmode display - this.iterateOver(function (_widget) { - _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc" : "desc") : "none"); - }, this, et2_INextmatchSortable); - if (_update) { - this.applyFilters({ sort: { id: _id, asc: _asc } }); - } - else { - // Update the entry in the activeFilters object - this.activeFilters["sort"] = { - "id": _id, - "asc": _asc - }; - } - } - /** - * Removes the sort entry from the active filters object and thus returns to - * the natural sort order. - */ - resetSort() { - // Check whether the nextmatch widget is currently sorted - if (typeof this.activeFilters["sort"] != "undefined") { - // Reset the sort mode - this.iterateOver(function (_widget) { - _widget.setSortmode("none"); - }, this, et2_INextmatchSortable); - // Delete the "sort" filter entry - this.applyFilters({ sort: undefined }); - } - } - /** - * Apply current or modified filters on NM widget (updating rows accordingly) - * - * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header - */ - applyFilters(_set) { - let changed = false; - let keep_selection = false; - // Avoid loops cause by change events - if (this.update_in_progress) - return; - this.update_in_progress = true; - // Cleared explicitly - if (typeof _set != 'undefined' && jQuery.isEmptyObject(_set)) { - changed = true; - this.activeFilters = { col_filter: {} }; - } - if (typeof this.activeFilters == "undefined") { - this.activeFilters = { col_filter: {} }; - } - if (typeof this.activeFilters.col_filter == "undefined") { - this.activeFilters.col_filter = {}; - } - if (typeof _set == 'object') { - for (let s in _set) { - if (s == 'col_filter') { - // allow apps setState() to reset all col_filter by using undefined or null for it - // they can not pass {} for _set / state.state, if they need to set something - if (_set.col_filter === undefined || _set.col_filter === null) { - this.activeFilters.col_filter = {}; - changed = true; - } - else { - for (let c in _set.col_filter) { - if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) { - if (_set.col_filter[c]) { - this.activeFilters.col_filter[c] = _set.col_filter[c]; - } - else { - delete this.activeFilters.col_filter[c]; - } - changed = true; - } - } - } - } - else if (s === 'selected') { - changed = true; - keep_selection = true; - this.controller._selectionMgr.resetSelection(); - this.controller._objectManager.clear(); - for (let i in _set.selected) { - this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::' + _set.selected[i], true); - } - delete _set.selected; - } - else if (this.activeFilters[s] !== _set[s]) { - this.activeFilters[s] = _set[s]; - changed = true; - } - } - } - this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); - // Keep the selection after applying filters, but only if unchanged - if (!changed || keep_selection) { - this.controller.keepSelection(); - } - else { - // Do not keep selection - this.controller._selectionMgr.resetSelection(); - this.controller._objectManager.clear(); - this.controller.keepSelection(); - } - // Update the filters in the grid controller - this.controller.setFilters(this.activeFilters); - // Update the header - this.header.setFilters(this.activeFilters); - // Update any column filters - this.iterateOver(function (column) { - // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter - if (typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) - return; - if (typeof column.set_value != "undefined" && column.id) { - column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); - } - if (column.id && typeof column.get_value == "function") { - this[column.id] = column.get_value(); - } - }, this.activeFilters.col_filter, et2_INextmatchHeader); - // Trigger an update - this.controller.update(true); - if (changed) { - // Highlight matching favorite in sidebox - if (this.getInstanceManager().app) { - const appname = this.getInstanceManager().app; - if (app[appname] && app[appname].highlight_favorite) { - app[appname].highlight_favorite(); - } - } - } - this.update_in_progress = false; - } - /** - * Refresh given rows for specified change - * - * Change type parameters allows for quicker refresh then complete server side reload: - * - update: request modified data from given rows. May be moved. - * - update-in-place: update row, but do NOT move it, or refresh if uid does not exist - * - edit: rows changed, but sorting may be affected. Full reload. - * - delete: just delete the given rows clientside (no server interaction neccessary) - * - add: put the new row in at the top, unless app says otherwise - * - * What actually happens also depends on a general preference "lazy-update": - * default/lazy: - * - add always on top - * - updates on top, if sorted by last modified, otherwise update-in-place - * - update-in-place is always in place! - * - * exact: - * - add and update on top if sorted by last modified, otherwise full refresh - * - update-in-place is always in place! - * - * Nextmatch checks the application callback nm_refresh_index, which has a default implementation - * in egw_app.nm_refresh_index(). - * - * @param {string[]|string} _row_ids rows to refresh - * @param {?string} _type "update-in-place", "update", "edit", "delete" or "add" - * - * @see jsapi.egw_refresh() - * @see egw_app.nm_refresh_index() - * @fires refresh from the widget itself - */ - refresh(_row_ids, _type) { - // Framework trying to refresh, but nextmatch not fully initialized - if (this.controller === null || !this.div) { - return; - } - // Make sure we're dealing with arrays - if (typeof _row_ids == 'string' || typeof _row_ids == 'number') - _row_ids = [_row_ids]; - // Make some changes in what we're doing based on preference - let update_pref = egw.preference("lazy-update") || 'lazy'; - if (_type == et2_nextmatch.UPDATE && !this.is_sorted_by_modified()) { - _type = update_pref == "lazy" ? et2_nextmatch.UPDATE_IN_PLACE : et2_nextmatch.EDIT; - } - else if (update_pref == "exact" && _type == et2_nextmatch.ADD && !this.is_sorted_by_modified()) { - _type = et2_nextmatch.EDIT; - } - if (_type == et2_nextmatch.ADD && !(update_pref == "lazy" || update_pref == "exact" && this.is_sorted_by_modified())) { - _type = et2_nextmatch.EDIT; - } - if (typeof _type == 'undefined') - _type = et2_nextmatch.EDIT; - if (!this.div.is(':visible')) // run refresh, once we become visible again - { - return this._queue_refresh(_row_ids, _type); - } - if (typeof _row_ids == "undefined" || _row_ids === null) { - this.applyFilters(); - // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh", [this]); - return; - } - // Clean IDs in case they're UIDs with app prefixed - _row_ids = _row_ids.map(function (id) { - if (id.toString().indexOf(this.controller.dataStorePrefix) == -1) { - return id; - } - let parts = id.split("::"); - parts.shift(); - return parts.join("::"); - }.bind(this)); - if (_type == et2_nextmatch.DELETE) { - // Record current & next index - var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; - const entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); - if (entry && entry.idx !== null) { - let next = (entry.ao ? entry.ao.getNext(_row_ids.length) : null); - if (next == null || !next.id || next.id == uid) { - // No next, select previous - next = (entry.ao ? entry.ao.getPrevious(1) : null); - } - // Stop automatic updating - this.dataview.grid.doInvalidate = false; - for (var i = 0; i < _row_ids.length; i++) { - uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; - // Delete from internal references - this.controller.deleteRow(uid); - } - // Select & focus next row - if (next && next.id && !this.options.disable_selection_advance) { - this.controller._selectionMgr.setSelected(next.id, true); - this.controller._selectionMgr.setFocused(next.id, true); - } - // Update the count - const total = this.dataview.grid._total - _row_ids.length; - // This will remove the last row! - // That's OK, because grid adds one in this.controller.deleteRow() - this.dataview.grid.setTotalCount(total); - this.controller._selectionMgr.setTotalCount(total); - // Re-enable automatic updating - this.dataview.grid.doInvalidate = true; - this.dataview.grid.invalidate(); - } - } - id_loop: for (var i = 0; i < _row_ids.length; i++) { - let uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; - // Check for update on a row we don't have - let known = Object.values(this.controller._indexMap).filter(function (row) { return row.uid == uid; }); - if ((_type == et2_nextmatch.UPDATE || _type == et2_nextmatch.UPDATE_IN_PLACE) && (!known || known.length == 0)) { - _type = et2_nextmatch.ADD; - if (update_pref == "exact" && !this.is_sorted_by_modified()) { - _type = et2_nextmatch.EDIT; - } - } - if ([et2_nextmatch.ADD, et2_nextmatch.UPDATE].indexOf(_type) !== -1) { - // Pre-ask for the row data, and only proceed if we actually get it - // need to send nextmatch filters too, as server-side will merge old version from request otherwise - this.egw().dataFetch(this.getInstanceManager().etemplate_exec_id, { refresh: _row_ids }, this.controller._filters, this.id, function (data) { - // In the event that the etemplate got removed before the data came back (Usually an action caused - // a full submit) just stop here. - if (!this.nm.getParent()) - return; - if (data.total >= 1) { - this.type == et2_nextmatch.ADD ? this.nm.refresh_add(this.uid, this.type) - : this.nm.refresh_update(this.uid); - } - else if (this.type == et2_nextmatch.UPDATE) { - // Remove row from controller - this.nm.controller.deleteRow(this.uid); - // Adjust total rows, clean grid - this.nm.controller._grid.setTotalCount(this.nm.controller._grid._total - _row_ids.length); - this.nm.controller._selectionMgr.setTotalCount(this.nm.controller._grid._total); - } - }, { type: _type, nm: this, uid: uid, prefix: this.controller.dataStorePrefix }, [_row_ids]); - return; - } - switch (_type) { - // update-in-place = update, but always only in place - case et2_nextmatch.UPDATE_IN_PLACE: - this.egw().dataRefreshUID(uid); - break; - // These ones handled above in dataFetch() callback - case et2_nextmatch.UPDATE: - // update [existing] row, maybe we'll put it on top - break; - case et2_nextmatch.DELETE: - // Handled above, more code to execute after loop so don't exit early - break; - case et2_nextmatch.ADD: - break; - // No more smart things we can do, refresh the whole thing - case et2_nextmatch.EDIT: - default: - // Trigger refresh - this.applyFilters(); - break id_loop; - } - } - // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh", [this, _row_ids, _type]); - } - /** - * An entry has been updated. Request new data, and ask app about where the row - * goes now. - * - * @param uid - */ - refresh_update(uid) { - // Row data update has been sent, let's move it where app wants it - let entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); - // Need to delete first as there's a good chance indexes will change in an unknown way - // and we can't always find it by UID after due to duplication - this.controller.deleteRow(uid); - // Pretend it's a new row, let app tell us where it goes and we'll mark it as new - if (!this.refresh_add(uid, et2_nextmatch.UPDATE)) { - // App did not want the row, or doesn't know where it goes but we've already removed it... - // Put it back before anyone notices. New data coming from server anyway. - let callback = function (data) { - data.class += " new_entry"; - this.egw().dataUnregisterUID(uid, callback, this); - }; - this.egw().dataRegisterUID(uid, callback, this, this.getInstanceManager().etemplate_exec_id, this.id); - this.controller._insertDataRow(entry, true); - } - // Update does not need to increase row count, but refresh_add() adds it in - this.controller._grid.setTotalCount(this.controller._grid.getTotalCount() - 1); - this.controller._selectionMgr.setTotalCount(this.controller._grid.getTotalCount()); - return true; - } - /** - * An entry has been added. Put it in the list. - * - * @param uid - * @return boolean false: not added, true: added - */ - refresh_add(uid, type = et2_nextmatch.ADD) { - let index = egw.preference("lazy-update") !== "exact" ? 0 : - (this.is_sorted_by_modified() ? 0 : false); - // No add, do a full refresh - if (index === false) { - return false; - } - let time = new Date().valueOf(); - this.egw().dataRegisterUID(uid, this._push_add_callback, { nm: this, uid: uid, index: index }, this.getInstanceManager().etemplate_exec_id, this.id); - return true; - } - /** - * Callback for adding a new row via push - * - * Expected context: {nm: this, uid: string, index: number} - */ - _push_add_callback(data) { - if (data && this.nm && this.nm.getParent()) { - if (data.class) { - data.class += " new_entry"; - } - // Don't remove if new data has not arrived - let stored = egw.dataGetUIDdata(this.uid); - //if(stored?.timestamp >= time) return; - // Increase displayed row count or we lose the last row when we add and the total is wrong - this.nm.controller._grid.setTotalCount(this.nm.controller._grid.getTotalCount() + 1); - this.nm.controller._selectionMgr.setTotalCount(this.nm.controller._grid.getTotalCount()); - // Insert at the top of the list, or where app said - var entry = this.nm.controller._selectionMgr._getRegisteredRowsEntry(this.uid); - entry.idx = typeof this.index == "number" ? this.index : 0; - this.nm.controller._insertDataRow(entry, true); - } - else if (this.nm && this.nm.getParent()) { - // Server didn't give us our row data - // Delete from internal references - this.nm.controller.deleteRow(this.uid); - this.nm.controller._grid.setTotalCount(this.nm.controller._grid.getTotalCount() - 1); - this.nm.controller._selectionMgr.setTotalCount(this.nm.controller._grid.getTotalCount()); - } - this.nm.egw().dataUnregisterUID(this.uid, this.nm._push_add_callback, this); - } - /** - * Queue a refresh request until later, when nextmatch is visible - * - * Nextmatch can't re-draw anything while it's hidden (it messes up the sizing when it renders) so we can't actually - * do a refresh right now. Queue it up and when visible again we'll update then. If we get too many changes - * queued, we'll throw them all away and do a full refresh. - * - * @param _row_ids - * @param _type - * @private - */ - _queue_refresh(_row_ids, _type) { - // Maximum number of requests to queue. 50 chosen arbitrarily just to limit things - const max_queued = 50; - if (this._queued_refreshes === null) { - // Already too many or an EDIT came, we'll refresh everything later - return; - } - // Cancel any existing listener - let tab = jQuery(this.getInstanceManager().DOMContainer.parentNode) - .off('show.et2_nextmatch') - .one('show.et2_nextmatch', this._queue_refresh_callback.bind(this)); - // Edit means refresh everything, so no need to keep queueing - // Too many? Forget it, we'll refresh everything. - if (this._queued_refreshes.length >= max_queued || _type == et2_nextmatch.EDIT || !_type) { - this._queued_refreshes = null; - return; - } - // Skip if already in array - if (this._queued_refreshes.some(queue => queue.ids.length === _row_ids.length && queue.ids.every((value, index) => value === _row_ids[index]))) { - return; - } - this._queued_refreshes.push({ ids: _row_ids, type: _type }); - } - _queue_refresh_callback() { - if (this._queued_refreshes === null) { - // Still bound, but length is 0 - full refresh time - this._queued_refreshes = []; - return this.applyFilters(); - } - let types = {}; - types[et2_nextmatch.ADD] = []; - types[et2_nextmatch.UPDATE] = []; - types[et2_nextmatch.UPDATE_IN_PLACE] = []; - types[et2_nextmatch.DELETE] = []; - for (let refresh of this._queued_refreshes) { - types[refresh.type] = types[refresh.type].concat(refresh.ids); - } - this._queued_refreshes = []; - for (let type in types) { - if (types[type].length > 0) { - // Fire each change type once will all changed IDs - this.refresh(types[type].filter((v, i, a) => a.indexOf(v) === i), type); - } - } - } - /** - * Is this nextmatch currently sorted by "modified" date - * - * This is decided by the row_modified options passed from the server and the current sort order - */ - is_sorted_by_modified() { - var _a; - let sort = ((_a = this.getValue()) === null || _a === void 0 ? void 0 : _a.sort) || {}; - return sort && sort.id && sort.id == this.settings.add_on_top_sort_field && sort.asc == false; - } - _get_appname() { - let app = ''; - let list = []; - list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); - if (this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { - app = list[0].substring('nextmatch'.length + 1); - } - else { - app = list[0]; - } - return app; - } - /** - * Gets the selection - * - * @return Object { ids: [UIDs], inverted: boolean} - */ - getSelection() { - const selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; - if (typeof selected == "object" && selected != null) { - return selected; - } - return { ids: [], all: false }; - } - /** - * Log some debug information about internal values - */ - spillYourGuts() { - let guts = function (controller) { - console.log("Controller:", controller); - console.log("Controller indexMap:", controller._indexMap); - console.log("Grid:", controller._grid); - console.log("Selection Manager:", controller._selectionMgr); - console.log("Selection registered rows:", controller._selectionMgr._registeredRows); - if (controller && controller._children.length > 0) { - console.groupCollapsed("Sub-grids"); - let child_index = 0; - for (let child of controller._children) { - console.groupCollapsed("Child " + (++child_index)); - guts(child); - console.groupEnd(); - } - console.groupEnd(); - } - }; - console.group("Nextmatch internals"); - guts(this.controller); - console.groupEnd(); - } - /** - * Event handler for when the selection changes - * - * If the onselect attribute was set to a string with javascript code, it will - * be executed "legacy style". You can get the selected values with getSelection(). - * If the onselect attribute is in app.appname.function style, it will be called - * with the nextmatch and an array of selected row IDs. - * - * The array can be empty, if user cleared the selection. - * - * @param action ActionObject From action system. Ignored. - * @param senders ActionObjectImplemetation From action system. Ignored. - */ - onselect(action, senders) { - // Execute the JS code connected to the event handler - if (typeof this.options.onselect == 'function') { - return this.options.onselect.call(this, this.getSelection().ids, this); - } - } - /** - * Nextmatch needs a namespace - */ - _createNamespace() { - return true; - } - /** - * Create the dynamic height so nm fills all available space - * - * @returns {undefined} - */ - _getDynheight() { - // Find the parent container, either a tab or the main container - const tab = this.get_tab_info(); - if (!tab) { - return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); - } - else if (tab && tab.contentDiv) { - return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); - } - return false; - } - /** - * Generates the column caption for the given column widget - * - * @param {et2_widget} _widget - */ - _genColumnCaption(_widget) { - let result = null; - if (typeof _widget._genColumnCaption == "function") - return _widget._genColumnCaption(); - const self = this; - _widget.iterateOver(function (_widget) { - const label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); - if (!label) - return; // skip empty, undefined or null labels - if (!result) { - result = label; - } - else { - result += ", " + label; - } - }, this, et2_INextmatchHeader); - return result; - } - /** - * Generates the column name (internal) for the given column widget - * Used in preferences to refer to the columns by name instead of position - * - * See _getColumnCaption() for human fiendly captions - * - * @param {et2_widget} _widget - */ - _getColumnName(_widget) { - if (typeof _widget._getColumnName == 'function') - return _widget._getColumnName(); - const name = _widget.id; - const child_names = []; - const children = _widget.getChildren(); - for (let i = 0; i < children.length; i++) { - if (children[i].id) - child_names.push(children[i].id); - } - const colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); - if (colName == "") { - this.egw().debug("info", "Unable to generate nm column name for ", _widget); - } - return colName; - } - /** - * Retrieve the user's preferences for this nextmatch merged with defaults - * Column display, column size, etc. - */ - _getPreferences() { - // Read preference or default for column visibility - let negated = false; - let columnPreference = ""; - if (this.options.settings.default_cols) { - negated = this.options.settings.default_cols[0] == "!"; - columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols; - } - if (this.options.settings.selectcols && this.options.settings.selectcols.length) { - columnPreference = this.options.settings.selectcols; - negated = false; - } - if (!this.options.settings.columnselection_pref) { - // Set preference name so changes are saved - this.options.settings.columnselection_pref = this.options.template; - } - let app = ''; - let list = []; - if (this.options.settings.columnselection_pref) { - let pref = {}; - list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); - if (this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { - app = list[0].substring('nextmatch'.length + 1); - pref = egw.preference(this.options.settings.columnselection_pref, app); - } - else { - app = list[0]; - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); - } - if (pref) { - negated = (pref[0] == "!"); - columnPreference = negated ? pref.substring(1) : pref; - } - } - let columnDisplay = []; - // If no column preference or default set, use all columns - if (typeof columnPreference == "string" && columnPreference.length == 0) { - columnDisplay = []; - negated = true; - } - columnDisplay = typeof columnPreference === "string" - ? et2_csvSplit(columnPreference, null, ",") : columnPreference; - // Adjusted column sizes - let size = {}; - if (this.options.settings.columnselection_pref && app) { - let size_pref = this.options.settings.columnselection_pref + "-size"; - // If columnselection pref is missing prefix, add it in - if (size_pref.indexOf('nextmatch') == -1) { - size_pref = 'nextmatch-' + size_pref; - } - size = this.egw().preference(size_pref, app); - } - if (!size) - size = {}; - // Column order - const order = {}; - for (let i = 0; i < columnDisplay.length; i++) { - order[columnDisplay[i]] = i; - } - return { - visible: columnDisplay, - visible_negated: negated, - negated: negated, - size: size, - order: order - }; - } - /** - * Apply stored user preferences to discovered columns - * - * @param {array} _row - * @param {array} _colData - */ - _applyUserPreferences(_row, _colData) { - const prefs = this._getPreferences(); - const columnDisplay = prefs.visible; - const size = prefs.size; - const negated = prefs.visible_negated; - const order = prefs.order; - let colName = ''; - // Add in display preferences - if (columnDisplay && columnDisplay.length > 0) { - RowLoop: for (let i = 0; i < _row.length; i++) { - colName = ''; - if (_row[i].disabled === true) { - _colData[i].visible = false; - continue; - } - // Customfields needs special processing - if (_row[i].widget.instanceOf(et2_nextmatch_customfields)) { - // Find cf field - for (var j = 0; j < columnDisplay.length; j++) { - if (columnDisplay[j].indexOf(_row[i].widget.id) == 0) { - _row[i].widget.options.fields = {}; - for (let k = j; k < columnDisplay.length; k++) { - if (columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) { - _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; - } - } - // Resets field visibility too - _row[i].widget._getColumnName(); - _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); - break; - } - } - // Disable if there are no custom fields - if (jQuery.isEmptyObject(_row[i].widget.customfields)) { - _colData[i].visible = false; - continue; - } - colName = _row[i].widget.id; - } - else { - colName = this._getColumnName(_row[i].widget); - } - if (!negated) { - _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; - } - if (!colName) - continue; - _colData[i].visible = negated; - let stop = false; - for (var j = 0; j < columnDisplay.length && !stop; j++) { - if (columnDisplay[j] == colName) { - _colData[i].visible = !negated; - stop = true; - } - } - if (size[colName]) { - // Make sure percentages stay percentages, and forget any preference otherwise - if (_colData[i].width.charAt(_colData[i].width.length - 1) == "%") { - _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; - } - else { - _colData[i].width = parseInt(size[colName]) + 'px'; - } - } - } - } - _colData.sort(function (a, b) { - return a.order - b.order; - }); - _row.sort(function (a, b) { - if (typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') { - return a.colData.order - b.colData.order; - } - else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') { - return a.order - b.order; - } - }); - } - /** - * Take current column display settings and store them in this.egw().preferences - * for next time - */ - _updateUserPreferences() { - const colMgr = this.dataview.getColumnMgr(); - let app = ""; - if (!this.options.settings.columnselection_pref) { - this.options.settings.columnselection_pref = this.options.template; - } - const visibility = colMgr.getColumnVisibilitySet(); - const colDisplay = []; - const colSize = {}; - const custom_fields = []; - // visibility is indexed by internal ID, widget is referenced by position, preference needs name - for (var i = 0; i < colMgr.columns.length; i++) { - // @ts-ignore - const widget = this.columns[i].widget; - let colName = this._getColumnName(widget); - if (colName) { - // Server side wants each cf listed as a seperate column - if (widget.instanceOf(et2_nextmatch_customfields)) { - // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields - colName = widget.id; - for (let name in widget.options.fields) { - if (widget.options.fields[name]) - custom_fields.push(et2_nextmatch_customfields.PREFIX + name); - } - } - if (visibility[colMgr.columns[i].id].visible) - colDisplay.push(colName); - // When saving sizes, only save columns with explicit values, preserving relative vs fixed - // Others will be left to flex if width changes or more columns are added - if (colMgr.columns[i].relativeWidth) { - colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; - } - else if (colMgr.columns[i].fixedWidth) { - colSize[colName] = colMgr.columns[i].fixedWidth; - } - } - else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { - this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); - } - } - const list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); - let pref = this.options.settings.columnselection_pref; - if (pref.indexOf('nextmatch') == 0) { - app = list[0].substring('nextmatch'.length + 1); - } - else { - app = list[0]; - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = "nextmatch-" + this.options.settings.columnselection_pref; - } - // Server side wants each cf listed as a seperate column - jQuery.merge(colDisplay, custom_fields); - // Update query value, so data source can use visible columns to exclude expensive sub-queries - const oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : []; - this.activeFilters.selectcols = this.sortedColumnsList.length > 0 ? this.sortedColumnsList : colDisplay; - // We don't need to re-query if they've removed a column - const changed = []; - ColLoop: for (var i = 0; i < colDisplay.length; i++) { - for (let j = 0; j < oldCols.length; j++) { - if (colDisplay[i] == oldCols[j]) - continue ColLoop; - } - changed.push(colDisplay[i]); - } - // If a custom field column was added, throw away cache to deal with - // efficient apps that didn't send all custom fields in the first request - const cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0; - // Save visible columns and sizes if selectcols is not emtpy (an empty selectcols actually deletes the prefrence) - if (!jQuery.isEmptyObject(this.activeFilters.selectcols)) { - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), - // Use callback after the preference gets set to trigger refresh, in case app - // isn't looking at selectcols and just uses preference - cf_added ? jQuery.proxy(function () { if (this.controller) - this.controller.update(true); }, this) : null); - // Save adjusted column sizes and inform user about it - this.egw().set_preference(app, pref + "-size", colSize); - this.egw().message(this.egw().lang("Saved column sizes to preferences.")); - } - this.egw().set_preference(app, pref + "-size", colSize); - // No significant change (just normal columns shown) and no need to wait, - // but the grid still needs to be redrawn if a custom field was removed because - // the cell content changed. This is a cheaper refresh than the callback, - // this.controller.update(true) - if ((changed.length || custom_fields.length) && !cf_added) - this.applyFilters(); - } - _parseHeaderRow(_row, _colData) { - // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget - for (var x = 0; x < _row.length; x++) { - if (!_row[x].widget) { - _row[x].widget = et2_createWidget("label", {}); - } - } - // Get column display preference - this._applyUserPreferences(_row, _colData); - // Go over the header row and create the column entries - this.columns = new Array(_row.length); - const columnData = new Array(_row.length); - // No action columns in et2 - let remove_action_index = null; - for (var x = 0; x < _row.length; x++) { - this.columns[x] = jQuery.extend({ - "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, - "widget": _row[x].widget - }, _colData[x]); - let visibility = (!_colData[x] || _colData[x].visible) ? - et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : - et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; - if (_colData[x].disabled && _colData[x].disabled !== '' && - this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) { - visibility = et2_dataview_column.ET2_COL_VISIBILITY_DISABLED; - this.columns[x].visible = false; - } - columnData[x] = { - "id": "col_" + x, - // @ts-ignore - "order": this.columns[x].order, - "caption": this._genColumnCaption(_row[x].widget), - "visibility": visibility, - "width": _colData[x] ? _colData[x].width : 0 - }; - if (_colData[x].width === 'auto') { - // Column manager does not understand 'auto', which grid widget - // uses if width is not set - columnData[x].width = '100%'; - } - if (_colData[x].minWidth) { - columnData[x].minWidth = _colData[x].minWidth; - } - if (_colData[x].maxWidth) { - columnData[x].maxWidth = _colData[x].maxWidth; - } - // No action columns in et2 - const colName = this._getColumnName(_row[x].widget); - if (colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all') { - remove_action_index = x; - } - else if (!colName) { - // Unnamed column cannot be toggled or saved - columnData[x].visibility = et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT; - this.columns[x].visible = true; - } - } - // Remove action column - if (remove_action_index != null) { - this.columns.splice(remove_action_index, remove_action_index); - columnData.splice(remove_action_index, remove_action_index); - _colData.splice(remove_action_index, remove_action_index); - } - // Create the column manager and update the grid container - this.dataview.setColumns(columnData); - for (var x = 0; x < _row.length; x++) { - // Append the widget to this container - this.addChild(_row[x].widget); - } - // Create the nextmatch row provider - this.rowProvider = new et2_nextmatch_rowProvider(this.dataview.rowProvider, this._getSubgrid, this); - // Register handler to update preferences when column properties are changed - const self = this; - this.dataview.onUpdateColumns = function () { - // Use apply to make sure context is there - self._updateUserPreferences.apply(self); - // Allow column widgets a chance to resize - self.iterateOver(function (widget) { widget.resize(); }, self, et2_IResizeable); - }; - // Register handler for column selection popup, or disable - if (this.selectPopup) { - this.selectPopup.remove(); - this.selectPopup = null; - } - if (this.options.settings.no_columnselection) { - this.dataview.selectColumnsClick = function () { return false; }; - jQuery('span.selectcols', this.dataview.headTr).hide(); - } - else { - jQuery('span.selectcols', this.dataview.headTr).show(); - this.dataview.selectColumnsClick = function (event) { - self._selectColumnsClick(event); - }; - } - } - _parseDataRow(_row, _rowData, _colData) { - const columnWidgets = []; - _row.sort(function (a, b) { - return a.colData.order - b.colData.order; - }); - for (let x = 0; x < this.columns.length; x++) { - if (!this.columns[x].visible) { - continue; - } - if (typeof _row[x] != "undefined" && _row[x].widget) { - columnWidgets[x] = _row[x].widget; - // Append the widget to this container - this.addChild(_row[x].widget); - } - else { - columnWidgets[x] = _row[x].widget; - } - // Pass along column alignment - if (_row[x].align && columnWidgets[x]) { - columnWidgets[x].align = _row[x].align; - } - } - this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this); - // Create the grid controller - this.controller = new et2_nextmatch_controller(null, this.egw(), this.getInstanceManager().etemplate_exec_id, this, null, this.dataview.grid, this.rowProvider, this.options.settings.action_links, null, this.options.actions); - this.controller.setFilters(this.activeFilters); - // Need to trigger empty row the first time - if (total == 0) - this.controller._emptyRow(); - // Set data cache prefix to either provided custom or auto - if (!this.options.settings.dataStorePrefix && this.options.settings.get_rows) { - // Use jsapi data module to update - let list = this.options.settings.get_rows.split('.', 2); - if (list.length < 2) - list = this.options.settings.get_rows.split('_'); // support "app_something::method" - this.options.settings.dataStorePrefix = list[0]; - } - this.controller.setPrefix(this.options.settings.dataStorePrefix); - // Set the view - this.controller._view = this.view; - // Load the initial order - /*this.controller.loadInitialOrder(this._getInitialOrder( - this.options.settings.rows, this.options.settings.row_id - ));*/ - // Set the initial row count - var total = typeof this.options.settings.total != "undefined" ? - this.options.settings.total : 0; - // This triggers an invalidate, which updates the grid - this.dataview.grid.setTotalCount(total); - // Insert any data sent from server, so invalidate finds data already - if (this.options.settings.rows && this.options.settings.num_rows) { - this.controller.loadInitialData(this.options.settings.dataStorePrefix, this.options.settings.row_id, this.options.settings.rows); - // Remove, to prevent duplication - delete this.options.settings.rows; - } - } - _parseGrid(_grid) { - // Search the rows for a header-row - if one is found, parse it - for (let y = 0; y < _grid.rowData.length; y++) { - // Parse the first row as a header, need header to parse the data rows - if (_grid.rowData[y]["class"] == "th" || y == 0) { - this._parseHeaderRow(_grid.cells[y], _grid.colData); - } - else { - this._parseDataRow(_grid.cells[y], _grid.rowData[y], _grid.colData); - } - } - this.dataview.table.resize(); - } - _getSubgrid(_row, _data, _controller) { - // Fetch the id of the element described by _data, this will be the - // parent_id of the elements in the subgrid - const rowId = _data.content[this.options.settings.row_id]; - // Create a new grid with the row as parent and the dataview grid as - // parent grid - const grid = new et2_dataview_grid(_row, this.dataview.grid); - // Create a new controller for the grid - const controller = new et2_nextmatch_controller(_controller, this.egw(), this.getInstanceManager().etemplate_exec_id, this, rowId, grid, this.rowProvider, this.options.settings.action_links, _controller.getObjectManager()); - controller.update(); - // Register inside the destruction callback of the grid - grid.setDestroyCallback(function () { - controller.destroy(); - }); - return grid; - } - _getInitialOrder(_rows, _rowId) { - const _order = []; - // Get the length of the non-numerical rows arra - let len = 0; - for (let key in _rows) { - if (!isNaN(parseInt(key)) && parseInt(key) > len) - len = parseInt(key); - } - // Iterate over the rows - for (let i = 0; i < len; i++) { - // Get the uid from the data - const uid = this.egw().app_name() + '::' + _rows[i][_rowId]; - // Store the data for that uid - this.egw().dataStoreUID(uid, _rows[i]); - // Push the uid onto the order array - _order.push(uid); - } - return _order; - } - _selectColumnsClick(e) { - const self = this; - const columnMgr = this.dataview.getColumnMgr(); - // ID for faking letter selection in column selection - const LETTERS = '~search_letter~'; - const columns = {}; - const columns_selected = []; - for (var i = 0; i < columnMgr.columns.length; i++) { - var col = columnMgr.columns[i]; - const widget = this.columns[i].widget; - if (col.visibility == et2_dataview_column.ET2_COL_VISIBILITY_DISABLED || - col.visibility == et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { - continue; - } - if (col.caption) { - columns[col.id] = col.caption; - if (col.visibility == et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE) - columns_selected.push(col.id); - } - // Custom fields get listed separately - if (widget.instanceOf(et2_nextmatch_customfields)) { - if (jQuery.isEmptyObject(widget.customfields)) { - // No customfields defined, don't show column - delete (columns[col.id]); - continue; - } - for (var field_name in widget.customfields) { - columns[et2_nextmatch_customfields.PREFIX + field_name] = " - " + - widget.customfields[field_name].label; - if (widget.options.fields[field_name]) - columns_selected.push(et2_customfields_list.PREFIX + field_name); - } - } - } - // Letter search - if (this.options.settings.lettersearch) { - columns[LETTERS] = egw.lang('Search letter'); - if (this.header.lettersearch.is(':visible')) - columns_selected.push(LETTERS); - } - // Build the popup - if (!this.selectPopup) { - const select = et2_createWidget("select", { - multiple: true, - rows: 8, - empty_label: this.egw().lang("select columns"), - selected_first: false, - value_class: "selcolumn_sortable_" - }, this); - select.set_select_options(columns); - select.set_value(columns_selected); - let autoRefresh; - if (!this.options.disable_autorefresh) { - autoRefresh = et2_createWidget("select", { - "empty_label": "Refresh" - }, this); - autoRefresh.set_id("nm_autorefresh"); - autoRefresh.set_select_options({ - // Cause [unknown] problems with mail - 30: "30 seconds", - //60: "1 Minute", - 180: "3 Minutes", - 300: "5 Minutes", - 900: "15 Minutes", - 1800: "30 Minutes" - }); - autoRefresh.set_value(this._get_autorefresh()); - autoRefresh.set_statustext(egw.lang("Automatically refresh list")); - } - const defaultCheck = et2_createWidget("select", { "empty_label": "Preference" }, this); - defaultCheck.set_id('nm_col_preference'); - defaultCheck.set_select_options({ - 'default': { label: 'Default', title: 'Set these columns as the default' }, - 'reset': { label: 'Reset', title: "Reset all user's column preferences" }, - 'force': { label: 'Force', title: 'Force column preference so users cannot change it' } - }); - defaultCheck.set_value(this.options.settings.columns_forced ? 'force' : ''); - const okButton = et2_createWidget("buttononly", { "background_image": true, image: "check" }, this); - okButton.set_label(this.egw().lang("ok")); - okButton.onclick = function () { - // Update visibility - const visibility = {}; - for (var i = 0; i < columnMgr.columns.length; i++) { - const col = columnMgr.columns[i]; - if (col.caption && col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { - visibility[col.id] = { visible: false }; - } - } - const value = select.getValue(); - // Update & remove letter filter - if (self.header.lettersearch) { - var show_letters = true; - if (value.indexOf(LETTERS) >= 0) { - value.splice(value.indexOf(LETTERS), 1); - } - else { - show_letters = false; - } - self._set_lettersearch(show_letters); - } - let column = 0; - for (var i = 0; i < value.length; i++) { - // Handle skipped columns - while (value[i] != "col_" + column && column < columnMgr.columns.length) { - column++; - } - if (visibility[value[i]]) { - visibility[value[i]].visible = true; - } - // Custom fields are listed seperately in column list, but are only 1 column - if (self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { - const cf = self.columns[column].widget.options.customfields; - const visible = self.columns[column].widget.options.fields; - // Turn off all custom fields - for (var field_name in cf) { - visible[field_name] = false; - } - // Turn on selected custom fields - start from 0 in case they're not in order - for (let j = 0; j < value.length; j++) { - if (value[j].indexOf(et2_customfields_list.PREFIX) != 0) - continue; - visible[value[j].substring(1)] = true; - i++; - } - self.columns[column].widget.set_visible(visible); - } - } - columnMgr.setColumnVisibilitySet(visibility); - this.sortedColumnsList = []; - jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function (i, v) { - const data_id = v.getAttribute('data-value'); - const value = select.getValue(); - if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) { - const col_id = data_id.replace('col_', ''); - const col_widget = self.columns[col_id].widget; - if (col_widget.customfields) { - self.sortedColumnsList.push(col_widget.id); - for (let field_name in col_widget.customfields) { - if (jQuery.isEmptyObject(col_widget.options.fields) || col_widget.options.fields[field_name] == true) { - self.sortedColumnsList.push(et2_customfields_list.PREFIX + field_name); - } - } - } - else { - self.sortedColumnsList.push(self._getColumnName(col_widget)); - } - } - }); - // Hide popup - self.selectPopup.toggle(); - self.dataview.updateColumns(); - // Auto refresh - self._set_autorefresh(autoRefresh ? autoRefresh.get_value() : 0); - // Set default or clear forced - if (show_letters) { - self.activeFilters.selectcols.push('lettersearch'); - } - self.getInstanceManager().submit(); - self.selectPopup = null; - }; - const cancelButton = et2_createWidget("buttononly", { "background_image": true, image: "cancel" }, this); - cancelButton.set_label(this.egw().lang("cancel")); - cancelButton.onclick = function () { - self.selectPopup.toggle(); - self.selectPopup = null; - }; - const $select = jQuery(select.getDOMNode()); - $select.find('.ui-multiselect-checkboxes').sortable({ - placeholder: 'ui-fav-sortable-placeholder', - items: 'li[class^="selcolumn_sortable_col"]', - cancel: 'li[class^="selcolumn_sortable_#"]', - cursor: "move", - tolerance: "pointer", - axis: 'y', - containment: "parent", - delay: 250, - beforeStop: function (event, ui) { - jQuery('li[class^="selcolumn_sortable_#"]', this).css({ - opacity: 1 - }); - }, - start: function (event, ui) { - jQuery('li[class^="selcolumn_sortable_#"]', this).css({ - opacity: 0.5 - }); - }, - sort: function (event, ui) { - jQuery(this).sortable("refreshPositions"); - } - }); - $select.disableSelection(); - $select.find('li[class^="selcolumn_sortable_"]').each(function (i, v) { - // @ts-ignore - jQuery(v).attr('data-value', (jQuery(v).find('input')[0].value)); - }); - const $footerWrap = jQuery(document.createElement("div")) - .addClass('dialogFooterToolbar') - .append(okButton.getDOMNode()) - .append(cancelButton.getDOMNode()); - this.selectPopup = jQuery(document.createElement("div")) - .addClass("colselection ui-dialog ui-widget-content") - .append(select.getDOMNode()) - .append($footerWrap) - .appendTo(this.innerDiv); - // Add autorefresh - if (autoRefresh) { - $footerWrap.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode())); - } - // Add default checkbox for admins - const apps = this.egw().user('apps'); - if (apps['admin']) { - $footerWrap.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode())); - } - } - else { - this.selectPopup.toggle(); - } - const t_position = jQuery(e.target).position(); - const s_position = this.div.position(); - const max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - - (2 * this.selectPopup.find('.dialogFooterToolbar').height()); - this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height', max_height); - this.selectPopup.css("top", t_position.top) - .css("left", s_position.left + this.div.width() - this.selectPopup.width()); - } - /** - * Get the currently displayed columns - * Each customfield is listed separately - */ - get_columns() { - const colMgr = this.dataview.getColumnMgr(); - const visibility = colMgr.getColumnVisibilitySet(); - const colDisplay = []; - const custom_fields = []; - // visibility is indexed by internal ID, widget is referenced by position, preference needs name - for (var i = 0; i < colMgr.columns.length; i++) { - // @ts-ignore - const widget = this.columns[i].widget; - let colName = this._getColumnName(widget); - if (colName) { - // Server side wants each cf listed as a seperate column - if (widget.instanceOf(et2_nextmatch_customfields)) { - // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields - colName = widget.id; - for (let name in widget.options.fields) { - if (widget.options.fields[name]) - custom_fields.push(et2_nextmatch_customfields.PREFIX + name); - } - } - if (visibility[colMgr.columns[i].id].visible) { - colDisplay.push(colName); - } - } - } - // List each customfield as a seperate column - jQuery.merge(colDisplay, custom_fields); - return this.sortedColumnsList.length > 0 ? this.sortedColumnsList : colDisplay; - } - /** - * Set the currently displayed columns, without updating user's preference - * - * @param {string[]} column_list List of column names - * @param {boolean} trigger_update =false - explicitly trigger an update - */ - set_columns(column_list, trigger_update = false) { - const columnMgr = this.dataview.getColumnMgr(); - const visibility = {}; - // Initialize to false - for (var i = 0; i < columnMgr.columns.length; i++) { - const col = columnMgr.columns[i]; - if (col.caption && col.visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { - visibility[col.id] = { visible: false }; - } - } - for (var i = 0; i < this.columns.length; i++) { - let widget = this.columns[i].widget; - let colName = this._getColumnName(widget); - if (column_list.indexOf(colName) !== -1 && - typeof visibility[columnMgr.columns[i].id] !== 'undefined') { - visibility[columnMgr.columns[i].id].visible = true; - } - // Custom fields are listed seperately in column list, but are only 1 column - if (widget && widget.instanceOf(et2_nextmatch_customfields)) { - // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields - colName = widget.id; - if (column_list.indexOf(colName) !== -1) { - visibility[columnMgr.columns[i].id].visible = true; - } - const cf = this.columns[i].widget.options.customfields; - const visible = this.columns[i].widget.options.fields; - // Turn off all custom fields - for (let field_name in cf) { - visible[field_name] = false; - } - // Turn on selected custom fields - start from 0 in case they're not in order - for (let j = 0; j < column_list.length; j++) { - if (column_list[j].indexOf(et2_customfields_list.PREFIX) != 0) - continue; - visible[column_list[j].substring(1)] = true; - } - widget.set_visible(visible); - } - } - columnMgr.setColumnVisibilitySet(visibility); - // We don't want to update user's preference, so directly update - this.dataview._updateColumns(); - // Allow column widgets a chance to resize - this.iterateOver(function (widget) { widget.resize(); }, this, et2_IResizeable); - } - /** - * Set the letter search preference, and update the UI - * - * @param {boolean} letters_on - */ - _set_lettersearch(letters_on) { - if (letters_on) { - this.header.lettersearch.show(); - } - else { - this.header.lettersearch.hide(); - } - const lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; - this.egw().set_preference(this.egw().app_name(), lettersearch_preference, letters_on); - } - /** - * Set the auto-refresh time period, and starts the timer if not started - * - * @param time int Refresh period, in seconds - */ - _set_autorefresh(time) { - // Start / update timer - if (this._autorefresh_timer) { - window.clearInterval(this._autorefresh_timer); - delete this._autorefresh_timer; - } - // Store preference - const refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; - const app = this._get_appname(); - if (this._get_autorefresh() != time) { - this.egw().set_preference(app, refresh_preference, time); - } - if (time > 0) { - this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); - // Bind to tab show/hide events, so that we don't bother refreshing in the background - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function (e) { - // Stop - window.clearInterval(this._autorefresh_timer); - jQuery(e.target).off(e); - // If the autorefresh time is up, bind once to trigger a refresh - // (if needed) when tab is activated again - this._autorefresh_timer = setTimeout(jQuery.proxy(function () { - // Check in case it was stopped / destroyed since - if (!this._autorefresh_timer || !this.getInstanceManager()) - return; - jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', - // Important to use anonymous function instead of just 'this.refresh' because - // of the parameters passed - jQuery.proxy(function () { this.refresh(null, 'edit'); }, this)); - }, this), time * 1000); - }, this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function (e) { - // Start normal autorefresh timer again - this._set_autorefresh(this._get_autorefresh()); - jQuery(e.target).off(e); - }, this)); - } - } - /** - * Get the auto-refresh timer - * - * @return int Refresh period, in secods - */ - _get_autorefresh() { - if (this.options.disable_autorefresh) { - return 0; - } - const refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; - return this.egw().preference(refresh_preference, this._get_appname()); - } - /** - * Enable or disable autorefresh - * - * If false, autorefresh will be shown in column selection. If the user already has an autorefresh preference - * for this nextmatch, the timer will be started. - * - * If true, the timer will be stopped and autorefresh will not be shown in column selection - * - * @param disabled - */ - set_disable_autorefresh(disabled) { - this.options.disable_autorefresh = disabled; - this._set_autorefresh(this._get_autorefresh()); - } - /** - * When the template attribute is set, the nextmatch widget tries to load - * that template and to fetch the grid which is inside of it. It then calls - * - * @param {string} template_name Full template name in the form app.template[.template] - */ - set_template(template_name) { - const template = et2_createWidget("template", { "id": template_name }, this); - if (this.template) { - // Stop early to prevent unneeded processing, and prevent infinite - // loops if the server changes the template in get_rows - if (this.template == template_name) { - return; - } - // Free the grid components - they'll be re-created as the template is processed - this.dataview.destroy(); - this.rowProvider.destroy(); - this.controller.destroy(); - this.controller = null; - // Free any children from previous template - // They may get left behind because of how detached nodes are processed - // We don't use iterateOver because it checks sub-children - for (let i = this._children.length - 1; i >= 0; i--) { - const _node = this._children[i]; - if (_node != this.header && _node !== template) { - this.removeChild(_node); - _node.destroy(); - } - } - // Clear this setting if it's the same as the template, or - // the columns will not be loaded - if (this.template == this.options.settings.columnselection_pref) { - this.options.settings.columnselection_pref = template_name; - } - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - } - if (!template) { - this.egw().debug("error", "Error while loading definition template for " + - "nextmatch widget.", template_name); - return; - } - if (this.options.disabled) { - return; - } - // Deferred parse function - template might not be fully loaded - const parse = function (template) { - // Keep the name of the template, as we'll free up the widget after parsing - this.template = template_name; - // Fetch the grid element and parse it - const definitionGrid = template.getChildren()[0]; - if (definitionGrid && definitionGrid instanceof et2_grid) { - this._parseGrid(definitionGrid); - } - else { - this.egw().debug("error", "Nextmatch widget expects a grid to be the " + - "first child of the defined template."); - return; - } - // Free the template again, but don't remove it - setTimeout(function () { - template.destroy(); - }, 1); - // Call the "setNextmatch" function of all registered - // INextmatchHeader widgets. This updates this.activeFilters.col_filters according - // to what's in the template. - this.iterateOver(function (_node) { - _node.setNextmatch(this); - }, this, et2_INextmatchHeader); - // Set filters to current values - // TODO this.controller.setFilters(this.activeFilters); - // If no data was sent from the server, and num_rows is 0, the nm will be empty. - // This triggers a cache check. - if (!this.options.settings.num_rows && this.controller) { - this.controller.update(); - } - // Load the default sort order - if (this.options.settings.order && this.options.settings.sort) { - this.sortBy(this.options.settings.order, this.options.settings.sort == "ASC", false); - } - // Start auto-refresh - this._set_autorefresh(this._get_autorefresh()); - }; - // Template might not be loaded yet, defer parsing - const promise = []; - template.loadingFinished(promise); - // Wait until template (& children) are done - jQuery.when.apply(null, promise).done(jQuery.proxy(function () { - parse.call(this, template); - if (!this.dynheight) { - this.dynheight = this._getDynheight(); - } - this.dynheight.initialized = false; - this.resize(); - }, this)); - return promise; - } - // Some accessors to match conventions - set_hide_header(hide) { - (hide ? this.header.div.hide() : this.header.div.show()); - } - set_header_left(template) { - this.header._build_header("left", template); - } - set_header_right(template) { - this.header._build_header("right", template); - } - set_header_row(template) { - this.header._build_header("row", template); - } - set_no_filter(bool, filter_name) { - if (typeof filter_name == 'undefined') { - filter_name = 'filter'; - } - this.options['no_' + filter_name] = bool; - let filter = this.header[filter_name]; - if (filter) { - filter.set_disabled(bool); - } - else if (bool) { - filter = this.header._build_select(filter_name, 'select', this.settings[filter_name], this.settings[filter_name + '_no_lang']); - } - } - set_no_filter2(bool) { - this.set_no_filter(bool, 'filter2'); - } - /** - * Directly change filter value, with no server query. - * - * This allows the server app code to change filter value, and have it - * updated in the client UI. - * - * @param {String|number} value - */ - set_filter(value) { - const update = this.update_in_progress; - this.update_in_progress = true; - this.activeFilters.filter = value; - // Update the header - this.header.setFilters(this.activeFilters); - this.update_in_progress = update; - } - /** - * Directly change filter2 value, with no server query. - * - * This allows the server app code to change filter2 value, and have it - * updated in the client UI. - * - * @param {String|number} value - */ - set_filter2(value) { - const update = this.update_in_progress; - this.update_in_progress = true; - this.activeFilters.filter2 = value; - // Update the header - this.header.setFilters(this.activeFilters); - this.update_in_progress = update; - } - /** - * If nextmatch starts disabled, it will need a resize after being shown - * to get all the sizing correct. Override the parent to add the resize - * when enabling. - * - * @param {boolean} _value - */ - set_disabled(_value) { - const previous = this.disabled; - super.set_disabled(_value); - if (previous && !_value) { - this.resize(); - } - } - /** - * Actions are handled by the controller, so ignore these during init. - * - * @param {object} actions - */ - set_actions(actions) { - if (actions != this.options.actions && this.controller != null && this.controller._actionManager) { - for (let i = this.controller._actionManager.children.length - 1; i >= 0; i--) { - this.controller._actionManager.children[i].remove(); - } - this.options.actions = actions; - this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions); - this.controller._initActions(actions); - } - } - /** - * Switch view between row and tile. - * This should be followed by a call to change the template to match, which - * will cause a reload of the grid using the new settings. - * - * @param {string} view Either 'tile' or 'row' - */ - set_view(view) { - // Restrict to the only 2 accepted values - if (view == 'tile') { - this.view = 'tile'; - } - else { - this.view = 'row'; - } - } - /** - * Set a different / additional handler for dropped files. - * - * File dropping doesn't work with the action system, so we handle it in the - * nextmatch by linking automatically to the target row. This allows an additional handler. - * It should accept a row UID and a File[], and return a boolean Execute the default (link) action - * - * @param {String|Function} handler - */ - set_onfiledrop(handler) { - this.options.onfiledrop = handler; - } - /** - * Handle drops of files by linking to the row, if possible. - * - * HTML5 / native file drops conflict with jQueryUI draggable, which handles - * all our drop actions. So we side-step the issue by registering an additional - * drop handler on the rows parent. If the row/actions itself doesn't handle - * the drop, it should bubble and get handled here. - * - * @param {object} event - * @param {object} target - */ - handle_drop(event, target) { - // Check to see if we can handle the link - // First, find the UID - const row = this.controller.getRowByNode(target); - const uid = (row === null || row === void 0 ? void 0 : row.uid) || null; - // Get the file information - let files = []; - if (event.originalEvent && event.originalEvent.dataTransfer && - event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0) { - files = event.originalEvent.dataTransfer.files; - } - else { - return false; - } - // Exectute the custom handler code - if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) { - return false; - } - event.stopPropagation(); - event.preventDefault(); - if (!row || !row.uid) - return false; - // Link the file to the row - // just use a link widget, it's all already done - const split = uid.split('::'); - const link_value = { - to_app: split.shift(), - to_id: split.join('::') - }; - // Create widget and mangle to our needs - const link = et2_createWidget("link-to", { value: link_value }, this); - link.loadingFinished(); - link.file_upload.set_drop_target(false); - if (row.row.tr) { - // Ignore most of the UI, just use the status indicators - const status = jQuery(document.createElement("div")) - .addClass('et2_link_to') - .width(row.row.tr.width()) - .position({ my: "left top", at: "left top", of: row.row.tr }) - .append(link.status_span) - .append(link.file_upload.progress) - .appendTo(row.row.tr); - // Bind to link event so we can remove when done - link.div.on('link.et2_link_to', function (e, linked) { - if (!linked) { - jQuery("li.success", link.file_upload.progress) - .removeClass('success').addClass('validation_error'); - } - else { - // Update row - link._parent.refresh(uid, 'edit'); - } - // Fade out nicely - status.delay(linked ? 1 : 2000) - .fadeOut(500, function () { - link.destroy(); - status.remove(); - }); - }); - } - // Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh - link.file_upload.set_value(files); - } - getDOMNode(_sender) { - if (_sender == this || typeof _sender === 'undefined') { - return this.div[0]; - } - if (_sender == this.header) { - return this.header.div[0]; - } - for (let i = 0; i < this.columns.length; i++) { - if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) { - return this.dataview.getHeaderContainerNode(i); - } - } - // Let header have a chance - if (_sender && _sender._parent && _sender._parent == this) { - return this.header.getDOMNode(_sender); - } - return null; - } - // Input widget - /** - * Get the current 'value' for the nextmatch - */ - getValue() { - const _ids = this.getSelection(); - // Translate the internal uids back to server uids - const idsArr = _ids.ids; - for (let i = 0; i < idsArr.length; i++) { - idsArr[i] = idsArr[i].split("::").pop(); - } - const value = { - selected: idsArr, - col_filter: {} - }; - jQuery.extend(value, this.activeFilters, this.value); - if (typeof value.selectcols == "undefined" || value.selectcols.length === 0) { - value.selectcols = this.get_columns(); - } - return value; - } - resetDirty() { } - isDirty() { return false; } - isValid() { return true; } - set_value(_value) { - this.value = _value; - } - // Printing - /** - * Prepare for printing - * - * We check for un-loaded rows, and ask the user what they want to do about them. - * If they want to print them all, we ask the server and print when they're loaded. - */ - beforePrint() { - // Add the class, if needed - this.div.addClass('print'); - // Trigger resize, so we can fit on a page - this.dynheight.outerNode.css('max-width', this.div.css('max-width')); - this.resize(); - // Reset height to auto (after width resize) so there's no restrictions - this.dynheight.innerNode.css('height', 'auto'); - // Check for rows that aren't loaded yet, or lots of rows - const range = this.controller._grid.getIndexRange(); - this.print.old_height = this.controller._grid._scrollHeight; - const loaded_count = range.bottom - range.top + 1; - const total = this.controller._grid.getTotalCount(); - // Defer the printing to ask about columns & rows - const defer = jQuery.Deferred(); - let pref = this.options.settings.columnselection_pref; - if (pref.indexOf('nextmatch') == 0) { - pref = 'nextmatch-' + pref; - } - const app = this.getInstanceManager().app; - const columns = {}; - const columnMgr = this.dataview.getColumnMgr(); - pref += '_print'; - const columns_selected = []; - // Get column names - for (let i = 0; i < columnMgr.columns.length; i++) { - const col = columnMgr.columns[i]; - const widget = this.columns[i].widget; - let colName = this._getColumnName(widget); - if (col.caption && col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { - columns[colName] = col.caption; - if (col.visibility === et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE) - columns_selected.push(colName); - } - // Custom fields get listed separately - if (widget.instanceOf(et2_nextmatch_customfields)) { - delete (columns[colName]); - colName = widget.id; - if (col.visibility === et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE && !jQuery.isEmptyObject(widget.customfields)) { - columns[colName] = col.caption; - for (let field_name in widget.customfields) { - columns[et2_nextmatch_customfields.PREFIX + field_name] = " - " + widget.customfields[field_name].label; - if (widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) { - columns_selected.push(et2_nextmatch_customfields.PREFIX + field_name); - } - } - } - } - } - // Preference exists? Set it now - if (this.egw().preference(pref, app)) { - this.set_columns(jQuery.extend([], this.egw().preference(pref, app))); - } - const callback = jQuery.proxy(function (button, value) { - if (button === et2_dialog.CANCEL_BUTTON) { - // Give dialog a chance to close, or it will be in the print - window.setTimeout(function () { - defer.reject(); - }, 0); - return; - } - // Set CSS for orientation - this.div.addClass(value.orientation); - this.egw().set_preference(app, pref + '_orientation', value.orientation); - // Try to tell browser about orientation - const css = '@page { size: ' + value.orientation + '; }', head = document.head || document.getElementsByTagName('head')[0], style = document.createElement('style'); - style.type = 'text/css'; - style.media = 'print'; - // @ts-ignore - if (style.styleSheet) { - // @ts-ignore - style.styleSheet.cssText = css; - } - else { - style.appendChild(document.createTextNode(css)); - } - head.appendChild(style); - this.print.orientation_style = style; - // Trigger resize, so we can fit on a page - this.dynheight.outerNode.css('max-width', this.div.css('max-width')); - // Handle columns - this.set_columns(value.columns); - this.egw().set_preference(app, pref, value.columns); - let rows = parseInt(value.row_count); - if (rows > total) { - rows = total; - } - // If they want the whole thing, style it as all - if (button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) { - // Add the class, gives more reliable sizing - this.div.addClass('print'); - // Show it all - jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); - } - // We need more rows - if (button === 'dialog[all]' || rows > loaded_count) { - let count = 0; - let fetchedCount = 0; - let cancel = false; - const nm = this; - const dialog = et2_dialog.show_dialog( - // Abort the long task if they canceled the data load - function () { - count = total; - cancel = true; - window.setTimeout(function () { - defer.reject(); - }, 0); - }, egw.lang('Loading'), egw.lang('please wait...'), {}, [ - { "button_id": et2_dialog.CANCEL_BUTTON, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel' } - ]); - // dataFetch() is asynchronous, so all these requests just get fired off... - // 200 rows chosen arbitrarily to reduce requests. - do { - const ctx = { - "self": this.controller, - "start": count, - "count": Math.min(rows, 200), - "lastModification": this.controller._lastModification - }; - if (nm.controller.dataStorePrefix) { - // @ts-ignore - ctx.prefix = nm.controller.dataStorePrefix; - } - nm.controller.dataFetch({ start: count, num_rows: Math.min(rows, 200) }, function (data) { - // Keep track - if (data && data.order) { - fetchedCount += data.order.length; - } - nm.controller._fetchCallback.apply(this, arguments); - if (fetchedCount >= rows) { - if (cancel) { - dialog.destroy(); - defer.reject(); - return; - } - // Use CSS to hide all but the requested rows - // Prevents us from showing more than requested, if actual height was less than average - nm.print.row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; - egw.css(nm.print.row_selector, 'display: none'); - // No scrollbar in print view - jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); - // Show it all - jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); - // Grid needs to redraw before it can be printed, so wait - window.setTimeout(jQuery.proxy(function () { - dialog.destroy(); - // Should be OK to print now - defer.resolve(); - }, nm), et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); - } - }, ctx); - count += 200; - } while (count < rows); - nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows + 1)); - } - else { - // Don't need more rows, limit to requested and finish - // Show it all - jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); - // Use CSS to hide all but the requested rows - // Prevents us from showing more than requested, if actual height was less than average - this.print.row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; - egw.css(this.print.row_selector, 'display: none'); - // No scrollbar in print view - jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); - // Give dialog a chance to close, or it will be in the print - window.setTimeout(function () { - defer.resolve(); - }, 0); - } - }, this); - var value = { - content: { - row_count: Math.min(100, total), - columns: this.egw().preference(pref, app) || columns_selected, - orientation: this.egw().preference(pref + '_orientation', app) - }, - sel_options: { - columns: columns - } - }; - this._create_print_dialog.call(this, value, callback); - return defer; - } - /** - * Create and show the print dialog, which calls the provided callback when - * done. Broken out for overriding if needed. - * - * @param {Object} value Current settings and preferences, passed to the dialog for - * the template - * @param {Object} value.content - * @param {Object} value.sel_options - * - * @param {function(int, Object)} callback - Process the dialog response, - * format things according to the specified orientation and fetch any needed - * rows. - * - */ - _create_print_dialog(value, callback) { - let base_url = this.getInstanceManager().template_base_url; - if (base_url.substr(base_url.length - 1) == '/') - base_url = base_url.slice(0, -1); // otherwise we generate a url //api/templates, which is wrong - const tab = this.get_tab_info(); - // Get title for print dialog from settings or tab, if available - const title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); - const dialog = et2_createWidget("dialog", { - // If you use a template, the second parameter will be the value of the template, as if it were submitted. - callback: callback, - buttons: et2_dialog.BUTTONS_OK_CANCEL, - title: this.egw().lang('Print') + ' ' + this.egw().lang(title), - template: this.egw().link(base_url + '/api/templates/default/nm_print_dialog.xet'), - value: value - }); - } - /** - * Try to clean up the mess we made getting ready for printing - * in beforePrint() - */ - afterPrint() { - if (!this.div.hasClass('print')) { - return; - } - this.div.removeClass('print landscape portrait'); - jQuery(this.print.orientation_style).remove(); - delete this.print.orientation_style; - // Put scrollbar back - jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', ''); - // Correct size of grid, and trigger resize to fix it - this.controller._grid.setScrollHeight(this.print.old_height); - delete this.print.old_height; - // Remove CSS rule hiding extra rows - if (this.print.row_selector) { - egw.css(this.print.row_selector, ''); - delete this.print.row_selector; - } - // Restore columns - let pref = []; - const app = this.getInstanceManager().app; - if (this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { - pref = egw.preference(this.options.settings.columnselection_pref, app); - } - else { - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); - } - if (pref) { - if (typeof pref === 'string') - pref = pref.split(','); - // @ts-ignore - this.set_columns(pref, app); - } - this.dynheight.outerNode.css('max-width', 'inherit'); - this.resize(); - } -} -et2_nextmatch._attributes = { - // These normally set in settings, but broken out into attributes to allow run-time changes - "template": { - "name": "Template", - "type": "string", - "description": "The id of the template which contains the grid layout." - }, - "hide_header": { - "name": "Hide header", - "type": "boolean", - "description": "Hide the header", - "default": false - }, - "header_left": { - "name": "Left custom template", - "type": "string", - "description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "header_right": { - "name": "Right custom template", - "type": "string", - "description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "header_row": { - "name": "Inline custom template", - "type": "string", - "description": "Customise the nextmatch - inline, after row count. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "no_filter": { - "name": "No filter", - "type": "boolean", - "description": "Hide the first filter", - "default": et2_no_init - }, - "no_filter2": { - "name": "No filter2", - "type": "boolean", - "description": "Hide the second filter", - "default": et2_no_init - }, - "disable_autorefresh": { - "name": "Disable autorefresh", - "type": "boolean", - "description": "Disable the ability to autorefresh the nextmatch on a regular interval. ", - "default": false - }, - "disable_selection_advance": { - "name": "Disable selection advance", - "type": "boolean", - "description": "If a refresh deletes the currently selected row, we normally advance the selection to the next row. Set to true to stop this.", - "default": false - }, - "view": { - "name": "View", - "type": "string", - "description": "Display entries as either 'row' or 'tile'. A matching template must also be set after changing this.", - "default": et2_no_init - }, - "onselect": { - "name": "onselect", - "type": "js", - "default": et2_no_init, - "description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method" - }, - "onfiledrop": { - "name": "onFileDrop", - "type": "js", - "default": et2_no_init, - "description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action." - }, - "onadd": { - "name": "onAdd", - "type": "js", - "default": et2_no_init, - "description": "JS code that gets executed when a new entry is added via refresh(). Allows apps to override the default handling. Return false to cancel the add." - }, - "settings": { - "name": "Settings", - "type": "any", - "description": "The nextmatch settings", - "default": {} - } -}; -/** - * Update types - * @see et2_nextmatch.refresh() for more information - */ -et2_nextmatch.ADD = 'add'; -et2_nextmatch.UPDATE_IN_PLACE = 'update-in-place'; -et2_nextmatch.UPDATE = 'update'; -et2_nextmatch.EDIT = 'edit'; -et2_nextmatch.DELETE = 'delete'; -et2_nextmatch.legacyOptions = ["template", "hide_header", "header_left", "header_right"]; -et2_register_widget(et2_nextmatch, ["nextmatch"]); -/** - * Standard nextmatch header bar, containing filters, search, record count, letter filters, etc. - * - * Unable to use an existing template for this because parent (nm) doesn't, and template widget doesn't - * actually load templates from the server. - * @augments et2_DOMWidget - */ -export class et2_nextmatch_header_bar extends et2_DOMWidget { - /** - * Constructor - * - * @param _parent - * @param _attrs - * @param _child - */ - constructor(_parent, _attrs, _child) { - super(_parent, [_parent, _parent.options.settings], ClassWithAttributes.extendAttributes(et2_nextmatch_header_bar._attributes, _child || {})); - this.nextmatch = _parent; - this.div = jQuery(document.createElement("div")) - .addClass("nextmatch_header"); - this._createHeader(); - // Flag to avoid loops while updating filters - this.update_in_progress = false; - } - destroy() { - this.nextmatch = null; - super.destroy(); - this.div = null; - } - setNextmatch(nextmatch) { - const create_once = (this.nextmatch == null); - this.nextmatch = nextmatch; - if (create_once) { - this._createHeader(); - } - // Bind row count - this.nextmatch.dataview.grid.setInvalidateCallback(function () { - this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); - }, this); - } - /** - * Actions are handled by the controller, so ignore these - * - * @param {object} actions - */ - set_actions(actions) { } - _createHeader() { - let button; - const self = this; - const nm_div = this.nextmatch.getDOMNode(); - const settings = this.nextmatch.options.settings; - this.div.prependTo(nm_div); - // Left & Right (& row) headers - this.headers = [ - { id: this.nextmatch.options.header_left }, - { id: this.nextmatch.options.header_right }, - { id: this.nextmatch.options.header_row } - ]; - // The rest of the header - this.header_div = this.row_div = jQuery(document.createElement("div")) - .addClass("nextmatch_header_row") - .appendTo(this.div); - this.filter_div = jQuery(document.createElement("div")) - .addClass('filtersContainer') - .appendTo(this.row_div); - // Search - this.search_box = jQuery(document.createElement("div")) - .addClass('search') - .prependTo(egwIsMobile() ? this.nextmatch.getDOMNode() : this.row_div); - // searchbox widget options - const searchbox_options = { - id: "search", - overlay: (typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined') ? settings.searchbox.overlay : false, - onchange: function () { - self.nextmatch.applyFilters({ search: this.get_value() }); - }, - value: settings.search, - fix: !egwIsMobile() - }; - // searchbox widget - this.et2_searchbox = et2_createWidget('searchbox', searchbox_options, this); - // Set activeFilters to current value - this.nextmatch.activeFilters.search = settings.search; - this.et2_searchbox.set_value(settings.search); - jQuery(this.et2_searchbox.getInputNode()).attr("aria-label", egw.lang("search")); - /** - * Mobile theme specific part for nm header - * nm header has very different behaivior for mobile theme and basically - * it has its own markup separately from nm header in normal templates. - */ - if (egwIsMobile()) { - this.search_box.addClass('nm-mob-header'); - jQuery(this.div).css({ display: 'inline-block' }).addClass('nm_header_hide'); - //indicates appname in header - jQuery(document.createElement('div')) - .addClass('nm_appname_header') - .text(egw.lang(egw.app_name())) - .appendTo(this.search_box); - this.delete_action = jQuery(document.createElement('div')) - .addClass('nm_delete_action') - .prependTo(this.search_box); - // toggle header - // add new button - this.fav_span = jQuery(document.createElement('div')) - .addClass('nm_favorites_div') - .prependTo(this.search_box); - // toggle header menu - this.toggle_header = jQuery(document.createElement('button')) - .addClass('nm_toggle_header') - .click(function () { - jQuery(self.div).toggleClass('nm_header_hide'); - jQuery(this).toggleClass('nm_toggle_header_on'); - window.setTimeout(function () { self.nextmatch.resize(); }, 800); - }) - .prependTo(this.search_box); - // Context menu - this.action_header = jQuery(document.createElement('button')) - .addClass('nm_action_header') - .hide() - .click(function (e) { - // @ts-ignore - jQuery('tr.selected', self.nextmatch.getDOMNode()).trigger({ type: 'contextmenu', which: 3, originalEvent: e }); - }) - .prependTo(this.search_box); - } - // Add category - if (!settings.no_cat) { - if (typeof settings.cat_id_label == 'undefined') - settings.cat_id_label = ''; - this.category = this._build_select('cat_id', settings.cat_is_select ? - 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { - multiple: false, - tags: true, - class: "select-cat", - value_class: settings.cat_id_class - }); - } - // Filter 1 - if (!settings.no_filter) { - this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); - } - // Filter 2 - if (!settings.no_filter2) { - this.filter2 = this._build_select('filter2', 'select', settings.filter2, settings.filter2_no_lang, { - multiple: false, - tags: settings.filter2_tags, - class: "select-cat", - value_class: settings.filter2_class - }); - } - // Other stuff - this.right_div = jQuery(document.createElement("div")) - .addClass('header_row_right').appendTo(this.row_div); - // Record count - this.count = jQuery(document.createElement("span")) - .addClass("header_count ui-corner-all"); - // Need to figure out how to update this as grid scrolls - // this.count.append("? - ? ").append(egw.lang("of")).append(" "); - this.count_total = jQuery(document.createElement("span")) - .appendTo(this.count) - .text(settings.total + ""); - this.count.prependTo(this.right_div); - // Favorites - this._setup_favorites(settings['favorites']); - // Export - if (typeof settings.csv_fields != "undefined" && settings.csv_fields != false) { - let definition = settings.csv_fields; - if (settings.csv_fields === true) { - definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().app_name()); - } - let button = et2_createWidget("buttononly", { id: "export", "statustext": "Export", image: "download", "background_image": true }, this); - jQuery(button.getDOMNode()) - .click(this.nextmatch, function (event) { - // @ts-ignore - egw_openWindowCentered2(egw.link('/index.php', { - 'menuaction': 'importexport.importexport_export_ui.export_dialog', - 'appname': event.data.egw().getAppName(), - 'definition': definition - }), '_blank', 850, 440, 'yes'); - }); - } - // Another place to customize nextmatch - this.header_row = jQuery(document.createElement("div")) - .addClass('header_row').appendTo(this.right_div); - // Letter search - const current_letter = this.nextmatch.options.settings.searchletter ? - this.nextmatch.options.settings.searchletter : - (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); - if (this.nextmatch.options.settings.lettersearch || current_letter) { - this.lettersearch = jQuery(document.createElement("table")) - .addClass('nextmatch_lettersearch') - .css("width", "100%") - .appendTo(this.div); - const tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch); - const row = jQuery(document.createElement("tr")).appendTo(tbody); - // Capitals, A-Z - const letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); - for (let i in letters) { - button = jQuery(document.createElement("td")) - .addClass("lettersearch") - .appendTo(row) - .attr("id", letters[i]) - .text(letters[i]); - if (letters[i] == current_letter) - button.addClass("lettersearch_active"); - } - button = jQuery(document.createElement("td")) - .addClass("lettersearch") - .appendTo(row) - .attr("id", "") - .text(egw.lang("all")); - if (!current_letter) - button.addClass("lettersearch_active"); - this.lettersearch.click(this.nextmatch, function (event) { - // this is the lettersearch table - jQuery("td", this).removeClass("lettersearch_active"); - jQuery(event.target).addClass("lettersearch_active"); - event.data.applyFilters({ searchletter: event.target.id || false }); - }); - // Set activeFilters to current value - this.nextmatch.activeFilters.searchletter = current_letter; - } - // Apply letter search preference - const lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; - if (this.lettersearch && !egw.preference(lettersearch_preference, this.nextmatch.egw().app_name())) { - this.lettersearch.hide(); - } - } - /** - * Build & bind to a sub-template into the header - * - * @param {string} location One of left, right, or row - * @param {string} template_name Name of the template to load into the location - */ - _build_header(location, template_name) { - const id = location == "left" ? 0 : (location == "right" ? 1 : 2); - const existing = this.headers[id]; - // @ts-ignore - if (existing && existing._type) { - if (existing.id == template_name) - return; - existing.destroy(); - this.headers[id] = null; - } - if (!template_name) - return; - // Load the template - const self = this; - const header = et2_createWidget("template", { "id": template_name }, this); - this.headers[id] = header; - const deferred = []; - header.loadingFinished(deferred); - // Wait until all child widgets are loaded, then bind - jQuery.when.apply(jQuery, deferred).then(function () { - // fix order in DOM by reattaching templates in correct position - switch (id) { - case 0: // header_left: prepend - jQuery(header.getDOMNode()).prependTo(self.header_div); - break; - case 1: // header_right: before favorites and count - jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); - break; - case 2: // header_row: after search - window.setTimeout(function () { - jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); - }, 1); - break; - } - self._bindHeaderInput(header); - }); - } - /** - * Build the selectbox filters in the header bar - * Sets value, options, labels, and change handlers - * - * @param {string} name - * @param {string} type - * @param {string} value - * @param {string} lang - * @param {object} extra - */ - _build_select(name, type, value, lang, extra) { - var _a; - const widget_options = jQuery.extend({ - "id": name, - "label": this.nextmatch.options.settings[name + "_label"], - "no_lang": lang, - "disabled": this.nextmatch.options['no_' + name] - }, extra); - // Set select options - // Check in content for options- - const mgr = this.nextmatch.getArrayMgr("content"); - let options = mgr.getEntry("options-" + name); - // Look in sel_options - if (!options) - options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); - // Check parent sel_options, because those are usually global and don't get passed down - if (!options) - options = (_a = this.nextmatch.getArrayMgr("sel_options").getParentMgr()) === null || _a === void 0 ? void 0 : _a.getEntry(name); - // Sometimes legacy stuff puts it in here - if (!options) - options = mgr.getEntry('rows[sel_options][' + name + ']'); - // Maybe in a row, and options got stuck in ${row} instead of top level - const row_stuck = ['${row}', '{$row}']; - for (let i = 0; !options && i < row_stuck.length; i++) { - let row_id = ''; - if ((!options || options.length == 0) && ( - // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid - this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) { - row_id = name.replace(/[0-9]+/, row_stuck[i]); - options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); - if (!options) { - row_id = row_stuck[i] + "[" + name + "]"; - options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); - } - } - if (options) { - this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].', row_id, name); - } - } - // Legacy: Add in 'All' option for cat_id, if not provided. - if (name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != '')) { - widget_options.empty_label = this.egw().lang('All categories'); - } - // Create widget - const select = et2_createWidget(type, widget_options, this); - if (options) - select.set_select_options(options); - // Set value - select.set_value(value); - // Set activeFilters to current value - this.nextmatch.activeFilters[select.id] = select.get_value(); - // Set onChange - const input = select.input; - // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - select.attributes.select_options.ignore = true; - if (this.nextmatch.options.settings[name + "_onchange"]) { - // Get the onchange function string - let onchange = this.nextmatch.options.settings[name + "_onchange"]; - // Real submits cause all sorts of problems - if (onchange.match(/this\.form\.submit/)) { - this.egw().debug("warn", "%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.", name); - onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/, 'return true;'); - } - // Connect it to the onchange event of the input element - may submit - select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); - this._bindHeaderInput(select); - } - else // default request changed rows with new filters, previous this.form.submit() - { - input.change(this.nextmatch, function (event) { - const set = {}; - set[name] = select.getValue(); - event.data.applyFilters(set); - }); - } - return select; - } - /** - * Set up the favorites UI control - * - * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of - * additional fields/settings to add in to the favorite. - */ - _setup_favorites(filters) { - if (typeof filters == "undefined" || filters === false) { - // No favorites configured - return; - } - const widget_options = { - default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite", - app: this.getInstanceManager().app, - filters: filters, - sidebox_target: 'favorite_sidebox_' + this.getInstanceManager().app - }; - this.favorites = et2_createWidget('favorites', widget_options, this); - // Add into header - jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile() ? this.search_box.find('.nm_favorites_div').show() : this.right_div); - } - /** - * Updates all the filter elements in the header - * - * Does not actually refresh the data, just sets values to match those given. - * Called by et2_nextmatch.applyFilters(). - * - * @param filters Array Key => Value pairs of current filters - */ - setFilters(filters) { - // Avoid loops cause by change events - if (this.update_in_progress) - return; - this.update_in_progress = true; - // Use an array mgr to hande non-simple IDs - const mgr = new et2_arrayMgr(filters); - this.iterateOver(function (child) { - // Skip favorites, don't want them in the filter - if (typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) - return; - let value = ''; - if (typeof child.set_value != "undefined" && child.id) { - value = mgr.getEntry(child.id); - if (value == null) - value = ''; - /** - * Sometimes a filter value is not in current options. This can - * happen in a saved favorite, for example, or if server changes - * some filter options, and the order doesn't work out. The normal behaviour - * is to warn & not set it, but for nextmatch we'll just add it - * in, and let the server either set it properly, or ignore. - */ - if (value && typeof value != 'object' && child.instanceOf(et2_selectbox)) { - let found = typeof child.options.select_options[value] != 'undefined'; - // options is array of objects with attribute value&label - if (jQuery.isArray(child.options.select_options)) { - for (let o = 0; o < child.options.select_options.length; ++o) { - if (child.options.select_options[o].value == value) { - found = true; - break; - } - } - } - if (!found) { - const old_options = child.options.select_options; - // Actual label is not available, obviously, or it would be there - old_options[value] = child.egw().lang("Loading"); - child.set_select_options(old_options); - } - } - child.set_value(value); - } - if (typeof child.get_value == "function" && child.id) { - // Put data in the proper place - let target = this; - value = child.get_value(); - // Split up indexes - const indexes = child.id.replace(/[/g, '[').split('['); - for (let i = 0; i < indexes.length; i++) { - indexes[i] = indexes[i].replace(/]/g, '').replace(']', ''); - if (i < indexes.length - 1) { - if (typeof target[indexes[i]] == "undefined") - target[indexes[i]] = {}; - target = target[indexes[i]]; - } - else { - target[indexes[i]] = value; - } - } - } - }, filters); - // Letter search - if (this.nextmatch.options.settings.lettersearch) { - jQuery("td", this.lettersearch).removeClass("lettersearch_active"); - jQuery(filters.searchletter ? "td#" + filters.searchletter : "td.lettersearch[id='']", this.lettersearch).addClass("lettersearch_active"); - // Set activeFilters to current value - filters.searchletter = jQuery("td.lettersearch_active", this.lettersearch).attr("id") || false; - } - // Reset flag - this.update_in_progress = false; - } - /** - * Help out nextmatch / widget stuff by checking to see if sender is part of header - * - * @param {et2_widget} _sender - */ - getDOMNode(_sender) { - const filters = [this.category, this.filter, this.filter2]; - for (let i = 0; i < filters.length; i++) { - if (_sender == filters[i]) { - // Give them the filter div - return this.filter_div[0]; - } - } - if (_sender == this.et2_searchbox) - return this.search_box[0]; - if (_sender.id == 'export') - return this.right_div[0]; - if (_sender && _sender._type == "template") { - for (let i = 0; i < this.headers.length; i++) { - if (_sender.id == this.headers[i].id && _sender._parent == this) - return i == 2 ? this.header_row[0] : this.header_div[0]; - } - } - return null; - } - /** - * Bind all the inputs in the header sub-templates to update the filters - * on change, and update current filter with the inputs' current values - * - * @param {et2_template} sub_header - */ - _bindHeaderInput(sub_header) { - const header = this; - const bind_change = function (_widget) { - // Previously set change function - const widget_change = _widget.change; - let change = function (_node) { - // Call previously set change function - const result = widget_change.call(_widget, _node, header.nextmatch); - // Find current value in activeFilters - let entry = header.nextmatch.activeFilters; - const path = _widget.getArrayMgr('content').explodeKey(_widget.id); - let i = 0; - if (path.length > 0) { - for (; i < path.length; i++) { - entry = entry[path[i]]; - } - } - // Update filters, if the value is different and we're not already doing so - if ((result || typeof result === 'undefined') && entry != _widget.getValue() && !header.update_in_progress) { - // Widget will not have an entry in getValues() because nulls - // are not returned, we remove it from activeFilters - if (_widget._oldValue == null) { - const path = _widget.getArrayMgr('content').explodeKey(_widget.id); - if (path.length > 0) { - let entry = header.nextmatch.activeFilters; - let i = 0; - for (; i < path.length - 1; i++) { - entry = entry[path[i]]; - } - delete entry[path[i]]; - } - header.nextmatch.applyFilters(header.nextmatch.activeFilters); - } - else { - // Not null is easy, just get values - const value = this.getInstanceManager().getValues(sub_header); - header.nextmatch.applyFilters(value[header.nextmatch.id]); - } - } - // In case this gets bound twice, it's important to return - return true; - }; - _widget.change = change; - // Set activeFilters to current value - // Use an array mgr to hande non-simple IDs - var value = {}; - value[_widget.id] = _widget._oldValue = _widget.getValue(); - const mgr = new et2_arrayMgr(value); - jQuery.extend(true, this.nextmatch.activeFilters, mgr.data); - }; - if (sub_header.instanceOf(et2_inputWidget)) { - bind_change.call(this, sub_header); - } - else { - sub_header.iterateOver(bind_change, this, et2_inputWidget); - } - } -} -et2_nextmatch_header_bar._attributes = { - "filter_label": { - "name": "Filter label", - "type": "string", - "description": "Label for filter", - "default": "", - "translate": true - }, - "filter_help": { - "name": "Filter help", - "type": "string", - "description": "Help message for filter", - "default": "", - "translate": true - }, - "filter": { - "name": "Filter value", - "type": "any", - "description": "Current value for filter", - "default": "" - }, - "no_filter": { - "name": "No filter", - "type": "boolean", - "description": "Remove filter", - "default": false - } -}; -et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); -/** - * Classes for the nextmatch sortheaders etc. - * - * @augments et2_baseWidget - */ -export class et2_nextmatch_header extends et2_baseWidget { - /** - * Constructor - * - * @memberOf et2_nextmatch_header - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_header._attributes, _child || {})); - this.labelNode = jQuery(document.createElement("span")); - this.nextmatch = null; - this.setDOMNode(this.labelNode[0]); - } - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) { - this.nextmatch = _nextmatch; - } - set_label(_value) { - this.label = _value; - this.labelNode.text(_value); - // add class if label is empty - this.labelNode.toggleClass('et2_label_empty', !_value); - } -} -et2_nextmatch_header._attributes = { - "label": { - "name": "Caption", - "type": "string", - "description": "Caption for the nextmatch header", - "translate": true - } -}; -et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); -/** - * Extend header to process customfields - * - * @augments et2_customfields_list - * - * TODO This should extend customfield widget when it's ready, put the whole column in constructor() back too - */ -export class et2_nextmatch_customfields extends et2_customfields_list { - /** - * Constructor - * - * @memberOf et2_nextmatch_customfields - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfields._attributes, _child || {})); - // Specifically take the whole column - this.table.css("width", "100%"); - } - destroy() { - this.nextmatch = null; - super.destroy(); - } - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - // Add in settings that are objects - if (!_attrs.customfields) { - // Check for custom stuff (unlikely) - let data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - if (!data) - data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); - for (let key in data) { - if (typeof data[key] === 'object' && !_attrs[key]) - _attrs[key] = data[key]; - } - } - } - setNextmatch(_nextmatch) { - this.nextmatch = _nextmatch; - this.loadFields(); - } - /** - * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio - */ - loadFields() { - if (this.nextmatch == null) { - // not ready yet - return; - } - let columnMgr = this.nextmatch.dataview.getColumnMgr(); - let nm_column = null; - const set_fields = {}; - for (let i = 0; i < this.nextmatch.columns.length; i++) { - // @ts-ignore - if (this.nextmatch.columns[i].widget == this) { - nm_column = columnMgr.columns[i]; - break; - } - } - if (!nm_column) - return; - // Check for global setting changes (visibility) - const global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if (global_data != null && global_data.fields) - this.options.fields = global_data.fields; - const apps = egw.link_app_list(); - for (let field_name in this.options.customfields) { - const field = this.options.customfields[field_name]; - const cf_id = et2_customfields_list.PREFIX + field_name; - if (this.rows[field_name]) - continue; - // Table row - const row = jQuery(document.createElement("tr")) - .appendTo(this.tbody); - const cf = jQuery(document.createElement("td")) - .appendTo(row); - this.rows[cf_id] = cf[0]; - // Create widget by type - let widget = null; - if (field.type == 'select' || field.type == 'select-account') { - if (field.values && typeof field.values[''] !== 'undefined') { - delete (field.values['']); - } - widget = et2_createWidget(field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", { - id: cf_id, - empty_label: field.label, - select_options: field.values - }, this); - } - else if (apps[field.type]) { - widget = et2_createWidget("nextmatch-entryheader", { - id: cf_id, - only_app: field.type, - blur: field.label - }, this); - } - else { - widget = et2_createWidget("nextmatch-sortheader", { - id: cf_id, - label: field.label - }, this); - } - // If this is already attached, widget needs to be finished explicitly - if (this.isAttached() && !widget.isAttached()) { - widget.loadingFinished(); - } - // Check for column filter - if (!jQuery.isEmptyObject(this.options.fields) && (this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined')) { - cf.hide(); - } - else if (jQuery.isEmptyObject(this.options.fields)) { - // If we're showing it make sure it's set, but only after - set_fields[field_name] = true; - } - } - jQuery.extend(this.options.fields, set_fields); - } - /** - * Override parent so we can update the nextmatch row too - * - * @param {array} _fields - */ - set_visible(_fields) { - super.set_visible(_fields); - // Find data row, and do it too - const self = this; - if (this.nextmatch) { - this.nextmatch.iterateOver(function (widget) { - if (widget == self) - return; - widget.set_visible(_fields); - }, this, et2_customfields_list); - } - } - /** - * Provide own column caption (column selection) - * - * If only one custom field, just use that, otherwise use "custom fields" - */ - _genColumnCaption() { - return egw.lang("Custom fields"); - } - /** - * Provide own column naming, including only selected columns - only useful - * to nextmatch itself, not for sending server-side - */ - _getColumnName() { - let name = this.id; - const visible = []; - for (var field_name in this.options.customfields) { - if (jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) { - visible.push(et2_customfields_list.PREFIX + field_name); - jQuery(this.rows[field_name]).show(); - } - else if (typeof this.rows[field_name] != "undefined") { - jQuery(this.rows[field_name]).hide(); - } - } - if (visible.length) { - name += "_" + visible.join("_"); - } - else if (this.rows) { - // None hidden means all visible - jQuery(this.rows[field_name]).parent().parent().children().show(); - } - // Update global custom fields column(s) - widgets will check on their own - // Check for custom stuff (unlikely) - let data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - if (!data) - data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {}; - if (!data.fields) - data.fields = {}; - for (let field in this.options.customfields) { - data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]); - } - return name; - } -} -et2_nextmatch_customfields._attributes = { - 'customfields': { - 'name': 'Custom fields', - 'description': 'Auto filled' - }, - 'fields': { - 'name': "Visible fields", - "description": "Auto filled" - } -}; -et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); -/** - * @augments et2_nextmatch_header - */ -// @ts-ignore -export class et2_nextmatch_sortheader extends et2_nextmatch_header { - /** - * Constructor - * - * @memberOf et2_nextmatch_sortheader - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_sortheader._attributes, _child || {})); - this.sortmode = "none"; - this.labelNode.addClass("nextmatch_sortheader none"); - } - click(_event) { - if (this.nextmatch && super.click(_event)) { - // Send default sort mode if not sorted, otherwise send undefined to calculate - this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); - return true; - } - return false; - } - /** - * Wrapper to join up interface * framework - * - * @param {string} _mode - */ - set_sortmode(_mode) { - // Set via nextmatch after setup - if (this.nextmatch) - return; - this.setSortmode(_mode); - } - /** - * Function which implements the et2_INextmatchSortable function. - * - * @param {string} _mode - */ - setSortmode(_mode) { - // Remove the last sortmode class and add the new one - this.labelNode.removeClass(this.sortmode) - .addClass(_mode); - this.sortmode = _mode; - } -} -et2_nextmatch_sortheader._attributes = { - "sortmode": { - "name": "Sort order", - "type": "string", - "description": "Default sort order", - "translate": false - } -}; -et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); -/** - * Filter from a provided list of options - */ -export class et2_nextmatch_filterheader extends et2_selectbox { - /** - * Override to add change handler - */ - createInputWidget() { - // Make sure there's an option for all - if (!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - super.createInputWidget(); - jQuery(this.getInputNode()).change(this, function (event) { - if (typeof event.data.nextmatch == 'undefined') { - // Not fully set up yet - return; - } - const col_filter = {}; - col_filter[event.data.id] = event.data.input.val(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - event.data.set_value(event.data.input.val()); - event.data.nextmatch.applyFilters({ col_filter: col_filter }); - }); - } - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) { - this.nextmatch = _nextmatch; - // Set current filter value from nextmatch settings - if (this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - } - // Make sure selectbox is not longer than the column - resize() { - this.input.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); - } -} -et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); -/** - * Filter by account - */ -export class et2_nextmatch_accountfilterheader extends et2_selectAccount { - /** - * Override to add change handler - * - */ - createInputWidget() { - // Make sure there's an option for all - if (!this.options.empty_label && !this.options.select_options[""]) { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - super.createInputWidget(); - this.input.change(this, function (event) { - if (typeof event.data.nextmatch == 'undefined') { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[event.data.id] = event.data.getValue(); - event.data.nextmatch.applyFilters({ col_filter: col_filter }); - }); - } - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) { - this.nextmatch = _nextmatch; - // Set current filter value from nextmatch settings - if (this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - } - } - // Make sure selectbox is not longer than the column - resize() { - var max = jQuery(this.parentNode).innerWidth() - 4; - var surroundings = this.getSurroundings()._widgetSurroundings; - for (var i = 0; i < surroundings.length; i++) { - max -= jQuery(surroundings[i]).outerWidth(); - } - this.input.css("max-width", max + "px"); - } -} -et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); -/** - * Filter allowing multiple values to be selected, base on a taglist instead - * of a regular selectbox - * - * @augments et2_taglist - */ -export class et2_nextmatch_taglistheader extends et2_taglist { - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_filterheader - */ - createInputWidget() { - // Make sure there's an option for all - if (!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - super.createInputWidget(); - } - /** - * Disable toggle if there are 2 or less options - * @param {Object[]} options - */ - set_select_options(options) { - if (options && options.length <= 2 && this.options.multiple == 'toggle') { - this.set_multiple(false); - } - super.set_select_options(options); - } - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) { - this.nextmatch = _nextmatch; - // Set current filter value from nextmatch settings - if (this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - } - // Make sure selectbox is not longer than the column - resize() { - this.div.css("height", ''); - this.div.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); - super.resize(); - } -} -et2_nextmatch_taglistheader._attributes = { - autocomplete_url: { default: '' }, - multiple: { default: 'toggle' }, - onchange: { - // @ts-ignore - default: function (event) { - if (typeof this.nextmatch === 'undefined') { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[this.id] = this.getValue(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - //event.data.set_value(event.data.input.val()); - this.nextmatch.applyFilters({ col_filter: col_filter }); - } - }, - rows: { default: 2 }, - class: { default: 'nm_filterheader_taglist' } -}; -et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); -/** - * Nextmatch filter that can filter for a selected entry - */ -export class et2_nextmatch_entryheader extends et2_link_entry { - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_entryheader - * @param {object} event - * @param {object} selected - */ - onchange(event, selected) { - const col_filter = {}; - col_filter[this.id] = this.get_value(); - this.nextmatch.applyFilters.call(this.nextmatch, { col_filter: col_filter }); - } - /** - * Override to always return a string appname:id (or just id) for simple (one real selection) - * cases, parent returns an object. If multiple are selected, or anything other than app and - * id, the original parent value is returned. - */ - getValue() { - let value = super.getValue(); - if (typeof value == "object" && value != null) { - if (!value.app || !value.id) - return null; - // If array with just one value, use a string instead for legacy server handling - if (typeof value.id == 'object' && value.id.shift && value.id.length == 1) { - value.id = value.id.shift(); - } - // If simple value, format it legacy string style, otherwise - // we return full value - if (typeof value.id == 'string') { - value = value.app + ":" + value.id; - } - } - return value; - } - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) { - this.nextmatch = _nextmatch; - // Set current filter value from nextmatch settings - if (this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) { - this.set_value(this.nextmatch.options.settings.col_filter[this.id]); - if (this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) { - this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - this.attributes.value.ignore = true; - //this.attributes.select_options.ignore = true; - } - // Fire on lost focus, clear filter if user emptied box - } -} -et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); -/** - * @augments et2_nextmatch_filterheader - */ -export class et2_nextmatch_customfilter extends et2_nextmatch_filterheader { - /** - * Constructor - * - * @param _parent - * @param _attrs - * @param _child - * @memberOf et2_nextmatch_customfilter - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); - switch (_attrs.widget_type) { - case "link-entry": - _attrs.type = 'nextmatch-entryheader'; - break; - default: - if (_attrs.widget_type.indexOf('select') === 0) { - _attrs.type = 'nextmatch-filterheader'; - } - else { - _attrs.type = _attrs.widget_type; - } - } - jQuery.extend(_attrs.widget_options, { id: this.id }); - _attrs.id = ''; - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); - this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this.getParent()); - const select_options = []; - const correct_type = _attrs.type; - this.real_node['type'] = _attrs.widget_type; - et2_selectbox.find_select_options(this.real_node, select_options, _attrs); - this.real_node["_type"] = correct_type; - if (typeof this.real_node.set_select_options === 'function') { - this.real_node.set_select_options(select_options); - } - } - // Just pass the real DOM node through, in case anybody asks - getDOMNode(_sender) { - return this.real_node ? this.real_node.getDOMNode(_sender) : null; - } - // Also need to pass through real children - getChildren() { - return this.real_node.getChildren() || []; - } - setNextmatch(_nextmatch) { - if (this.real_node && this.real_node.instanceOf(et2_INextmatchHeader)) { - return this.real_node.setNextmatch(_nextmatch); - } - } -} -et2_nextmatch_customfilter._attributes = { - "widget_type": { - "name": "Actual type", - "type": "string", - "description": "The actual type of widget you should use", - "no_lang": 1 - }, - "widget_options": { - "name": "Actual options", - "type": "any", - "description": "The options for the actual widget", - "no_lang": 1, - "default": {} - } -}; -et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); -//# sourceMappingURL=et2_extension_nextmatch.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch_controller.js b/api/js/etemplate/et2_extension_nextmatch_controller.js deleted file mode 100644 index 489cf24f11..0000000000 --- a/api/js/etemplate/et2_extension_nextmatch_controller.js +++ /dev/null @@ -1,601 +0,0 @@ -/** - * EGroupware eTemplate2 - Class which contains a the data model for nextmatch widgets - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -import { et2_dataview_row } from "./et2_dataview_view_row"; -import { et2_dataview_tile } from "./et2_dataview_view_tile"; -import { et2_dataview_controller } from "./et2_dataview_controller"; -import { et2_dataview_column } from "./et2_dataview_model_columns"; -import { framework, egw } from "../jsapi/egw_global"; -import { egw_getActionManager, egw_getObjectManager, egwActionObjectManager, egwActionObject } from "../egw_action/egw_action.js"; -import { EGW_AO_FLAG_DEFAULT_FOCUS, EGW_AO_EXEC_SELECTED, EGW_AO_FLAG_IS_CONTAINER } from "../egw_action/egw_action_constants.js"; -import { nm_action } from "./et2_extension_nextmatch_actions.js"; -import { egwIsMobile } from "../egw_action/egw_action_common.js"; -/** - * @augments et2_dataview_controller - */ -export class et2_nextmatch_controller extends et2_dataview_controller { - /** - * Initializes the nextmatch controller. - * - * @param _parentController is the parent nextmatch controller instance - * @param _egw is the api instance - * @param _execId is the execId of the etemplate - * @param _widget is the nextmatch-widget we are fetching data for. - * @param _grid is the grid the grid controller will be controlling - * @param _rowProvider is the nextmatch row provider instance. - * @param _objectManager is the parent object manager (if null, the object - * manager) will be created using - * @param _actionLinks contains the action links - * @param _actions contains the actions, may be null if an object manager - * is given. - * @memberOf et2_nextmatch_controller - */ - constructor(_parentController, _egw, _execId, _widget, _parentId, _grid, _rowProvider, _actionLinks, _objectManager, _actions) { - // Call the parent et2_dataview_controller constructor - super(_parentController, _grid); - this.setDataProvider(this); - this.setRowCallback(this._rowCallback); - this.setLinkCallback(this._linkCallback); - this.setContext(this); - // Copy the egw reference - this.egw = _egw; - // Keep a reference to the widget - this._widget = _widget; - // Copy the given parameters - this._actionLinks = _actionLinks; - this._execId = _execId; - // Get full widget ID, including path - var id = _widget.getArrayMgr('content').getPath(); - if (typeof id == 'string') { - this._widgetId = id; - } - else if (id.length === 1) { - this._widgetId = id[0]; - } - else { - this._widgetId = id.shift() + '[' + id.join('][') + ']'; - } - this._parentId = _parentId; - this._rowProvider = _rowProvider; - // Initialize the action and the object manager - // _initActions calls _init_link_dnd, which uses this._actionLinks, - // so this must happen after the above parameter copying - if (!_objectManager) { - this._initActions(_actions); - } - else { - this._actionManager = null; - this._objectManager = _objectManager; - } - this.setActionObjectManager(this._objectManager); - // Add our selection callback to selection manager - var self = this; - this._objectManager.setSelectedCallback = function () { self._selectCallback.apply(self, [this, arguments]); }; - // We start with no filters - this._filters = {}; - // Keep selection across filter changes - this.kept_selection = null; - this.kept_focus = null; - this.kept_expansion = []; - // Directly use the API-Implementation of dataRegisterUID and - // dataUnregisterUID - this.dataUnregisterUID = _egw.dataUnregisterUID; - // Default to rows - this._view = et2_nextmatch_controller.VIEW_ROW; - } - destroy() { - // If the actionManager variable is set, the object- and actionManager - // were created by this instance -- clear them - if (this._actionManager) { - this._objectManager.remove(); - this._actionManager.remove(); - } - this._widget = null; - super.destroy(); - } - /** - * Updates the filter instance. - */ - setFilters(_filters) { - // Update the filters - this._filters = _filters; - } - /** - * Keep the selection, if possible, across a data fetch and restore it - * after - */ - keepSelection() { - this.kept_selection = this._selectionMgr ? this._selectionMgr.getSelected() : null; - this.kept_focus = this._selectionMgr && this._selectionMgr._focusedEntry ? - this._selectionMgr._focusedEntry.uid || null : null; - // Find expanded rows - var nm = this._widget; - var controller = this; - jQuery('.arrow.opened', this._widget.getDOMNode(this._widget)).each(function () { - var entry = controller.getRowByNode(this); - if (entry && entry.uid) { - controller.kept_expansion.push(entry.uid); - } - }); - } - getObjectManager() { - return this._objectManager; - } - /** - * Deletes a row from the grid - * - * @param {string} uid - */ - deleteRow(uid) { - var entry = Object.values(this._indexMap).find(entry => entry.uid == uid); - // Unselect - this._selectionMgr.setSelected(uid, false); - if (entry && entry.idx !== null) { - this._selectionMgr.unregisterRow(uid, entry.tr); - // This will remove the row, but add an empty to the end. - // That's OK, because it will be removed when we update the row count - this._grid.deleteRow(entry.idx); - // Remove from internal map - delete this._indexMap[entry.idx]; - // Update the indices of all elements after the current one - for (var mapIndex = entry.idx + 1; typeof this._indexMap[mapIndex] !== 'undefined'; mapIndex++) { - var entry = this._indexMap[mapIndex]; - entry.idx = mapIndex - 1; - this._indexMap[mapIndex - 1] = entry; - // Update selection mgr too - if (entry.uid && typeof this._selectionMgr._registeredRows[entry.uid] !== 'undefined') { - var reg = this._selectionMgr._getRegisteredRowsEntry(entry.uid); - reg.idx = entry.idx; - if (reg.ao && reg.ao._index) - reg.ao._index = entry.idx; - this._selectionMgr._registeredRows[entry.uid].idx = reg.idx; - } - } - // Remove last one, it was moved to mapIndex-1 before increment - delete this._indexMap[mapIndex - 1]; - // Not needed, they share by reference - // this._selectionMgr.setIndexMap(this._indexMap); - } - for (let child of this._children) { - child.deleteRow(uid); - } - } - /** -- PRIVATE FUNCTIONS -- **/ - /** - * Create a new row, either normal or tiled - * - * @param {type} ctx - * @returns {et2_dataview_container} - */ - _createRow(ctx) { - switch (this._view) { - case et2_nextmatch_controller.VIEW_TILE: - var row = new et2_dataview_tile(this._grid); - // Try to overcome chrome rendering issue where float is not - // applied properly, leading to incomplete rows - window.setTimeout(function () { - if (!row.tr) - return; - row.tr.css('float', 'none'); - window.setTimeout(function () { - if (!row.tr) - return; - row.tr.css('float', 'left'); - }, 50); - }, 100); - return row; - case et2_nextmatch_controller.VIEW_ROW: - default: - return new et2_dataview_row(this._grid); - } - } - /** - * Initializes the action and the object manager. - */ - _initActions(_actions) { - // Generate a uid for the action and object manager - var uid = this._widget.id || this.egw.uid(); - if (_actions == null) - _actions = []; - // Initialize the action manager and add some actions to it - // Only look 1 level deep - var gam = egw_getActionManager(this.egw.appName, true, 1); - if (this._actionManager == null) { - this._actionManager = gam.addAction("actionManager", uid); - } - var data = this._actionManager.data; - if (data == 'undefined' || !data) { - data = {}; - } - data.nextmatch = this._widget; - data.context = this._widget.getInstanceManager().app_obj; - this._actionManager.set_data(data); - this._actionManager.updateActions(_actions, this.egw.appName); - // Set the default execute handler - var self = this; - this._actionManager.setDefaultExecute(function (_action, _senders, _target) { - // Get the selected ids descriptor object - var ids = self._selectionMgr.getSelected(); - // Pass a reference to the actual widget - if (typeof _action.data == 'undefined' || !_action.data) - _action.data = {}; - _action.data.nextmatch = self._widget; - // Call the nm_action function with the ids - nm_action(_action, _senders, _target, ids); - }); - // Set the 'Select All' handler - var select_all = this._actionManager.getActionById('select_all'); - if (select_all) { - select_all.set_onExecute(jQuery.proxy(function (action, selected) { - this._selectionMgr.selectAll(); - }, this)); - } - // Initialize the object manager - look for application - // object manager 1 level deep - var gom = egw_getObjectManager(this.egw.appName, true, 1); - if (this._objectManager == null) { - this._objectManager = gom.addObject(new egwActionObjectManager(uid, this._actionManager)); - this._objectManager.handleKeyPress = function (_keyCode, _shift, _ctrl, _alt) { - for (var i = 0; i < self._actionManager.children.length; i++) { - if (typeof self._actionManager.children[i].shortcut === 'object' && - self._actionManager.children[i].shortcut && - _keyCode == self._actionManager.children[i].shortcut.keyCode) { - return this.executeActionImplementation({ - "keyEvent": { - "keyCode": _keyCode, - "shift": _shift, - "ctrl": _ctrl, - "alt": _alt - } - }, "popup", EGW_AO_EXEC_SELECTED); - } - } - return egwActionObject.prototype.handleKeyPress.call(this, _keyCode, _shift, _ctrl, _alt); - }; - } - this._objectManager.flags = this._objectManager.flags - | EGW_AO_FLAG_DEFAULT_FOCUS | EGW_AO_FLAG_IS_CONTAINER; - this._init_links_dnd(); - if (this._selectionMgr) { - // Need to update the action links for every registered row too - for (var uid in this._selectionMgr._registeredRows) { - // Get the corresponding entry from the registered rows array - var entry = this._selectionMgr._getRegisteredRowsEntry(uid); - if (entry.ao) { - entry.ao.updateActionLinks(this._actionLinks); - } - } - } - } - /** - * Automatically add dnd support for linking - */ - _init_links_dnd() { - var mgr = this._actionManager; - var self = this; - var drop_action = mgr.getActionById('egw_link_drop'); - var drag_action = mgr.getActionById('egw_link_drag'); - var drop_cancel = mgr.getActionById('egw_cancel_drop'); - if (!this._actionLinks) { - this._actionLinks = []; - } - if (!drop_cancel) { - // Create a generic cancel action in order to cancel drop action - // applied for all apps plus file and link action. - drop_cancel = mgr.addAction('drop', 'egw_cancel_drop', this.egw.lang('Cancel'), egw.image('cancel'), function () { }, true); - drop_cancel.set_group('99'); - drop_cancel.acceptedTypes = drop_cancel.acceptedTypes.concat(Object.keys(egw.user('apps')).concat(['link', 'file'])); - this._actionLinks.push(drop_cancel.id); - } - // Check if this app supports linking - if (!egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'query') || - egw.link_get_registry(this.dataStorePrefix || this.egw.appName, 'title')) { - if (drop_action) { - drop_action.remove(); - if (this._actionLinks.indexOf(drop_action.id) >= 0) { - this._actionLinks.splice(this._actionLinks.indexOf(drop_action.id), 1); - } - } - if (drag_action) { - drag_action.remove(); - if (this._actionLinks.indexOf(drag_action.id) >= 0) { - this._actionLinks.splice(this._actionLinks.indexOf(drag_action.id), 1); - } - } - return; - } - // Don't re-add - if (drop_action == null) { - // Create the drop action that links entries - drop_action = mgr.addAction('drop', 'egw_link_drop', this.egw.lang('Create link'), egw.image('link'), function (action, source, dropped) { - // Extract link IDs - var links = []; - var id = ''; - for (var i = 0; i < source.length; i++) { - if (!source[i].id) - continue; - id = source[i].id.split('::'); - links.push({ app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1] }); - } - if (!links.length) { - return; - } - // Link the entries - self.egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", dropped.id.split('::').concat([links]), function (result) { - if (result) { - for (var i = 0; i < this._objectManager.selectedChildren.length; i++) { - this._widget.refresh(this._objectManager.selectedChildren[i].id, 'update'); - } - this._widget.egw().message('Linked'); - // Update the target to show the liks - this._widget.refresh(dropped.id, 'update'); - } - }, self, true, self).sendRequest(); - }, true); - } - if (this._actionLinks.indexOf(drop_action.id) < 0) { - this._actionLinks.push(drop_action.id); - } - // Accept other links, and files dragged from the filemanager - // This does not handle files dragged from the desktop. They are - // handled by et2_nextmatch, since it needs DOM stuff - if (drop_action.acceptedTypes.indexOf('link') == -1) { - drop_action.acceptedTypes.push('link'); - } - // Don't re-add - if (drag_action == null) { - // Create drag action that allows linking - drag_action = mgr.addAction('drag', 'egw_link_drag', this.egw.lang('link'), 'link', function (action, selected) { - // Drag helper - list titles. Arbitrarily limited to 10. - var helper = jQuery(document.createElement("div")); - for (var i = 0; i < selected.length && i < 10; i++) { - var id = selected[i].id.split('::'); - var span = jQuery(document.createElement('span')).appendTo(helper); - self.egw.link_title(id[0], id[1], function (title) { - this.append(title); - this.append('
'); - }, span); - } - // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links - // TODO: Need to decide if we need to create a customized helper interface for links anyway - //return helper; - return null; - }, true); - } - if (this._actionLinks.indexOf(drag_action.id) < 0) { - this._actionLinks.push(drag_action.id); - } - drag_action.set_dragType('link'); - } - /** - * Set the data cache prefix - * Overridden from the parent to re-check automatically the added link dnd - * since the prefix is used in support detection. - */ - setPrefix(prefix) { - super.setPrefix(prefix); - this._init_links_dnd(); - } - /** - * Overwrites the inherited _destroyCallback function in order to be able - * to free the "rowWidget". - */ - _destroyCallback(_row) { - // Destroy any widget associated to the row - if (this.entry.widget) { - this.entry.widget.destroy(); - this.entry.widget = null; - } - // Call the inherited function - super._destroyCallback(_row); - } - /** - * Creates the actual data row. - * - * @param _data is an array containing the row data - * @param _tr is the tr into which the data will be inserted - * @param _idx is the index of the row - * @param _entry is the internal row datastructure of the controller, in - * this special case used to store the rowWidget reference, so that it can - * be properly freed. - */ - _rowCallback(_data, _tr, _idx, _entry) { - // Let the row provider fill in the data row -- store the returned - // rowWidget inside the _entry - _entry.widget = this._rowProvider.getDataRow({ "content": _data }, _tr, _idx, this); - } - /** - * Returns the names of action links for a given data row -- currently these are - * always the same links, as we controll enabled/disabled over the row - * classes, unless the row UID is "", then it's an 'empty' row. - * - * The empty row placeholder can still have actions, but nothing that requires - * an actual UID. - * - * @TODO: Currently empty row is just add, need to actually filter somehow. Here - * might not be the right place. - * - * @param _data Object The data for the row - * @param _idx int The row index - * @param _uid String The row's ID - * - * @return Array List of action names that valid for the row - */ - _linkCallback(_data, _idx, _uid) { - if (_uid.trim() != "") { - return this._actionLinks; - } - // No UID, so return a filtered list of actions that doesn't need a UID - var links = []; - try { - links = typeof this._widget.options.settings.placeholder_actions != 'undefined' ? - this._widget.options.settings.placeholder_actions : (this._widget.options.add ? ["add"] : []); - // Make sure that placeholder actions are defined and existed in client-side, - // otherwise do not set them as placeholder. for instance actions with - // attribute hideOnMobile do not get sent to client-side. - var action_search = function (current) { - if (typeof this._widget.options.actions[current] !== 'undefined') - return true; - // Check children - for (var action in this._widget.options.actions) { - action = this._widget.options.actions[action]; - if (action.children && action.children[current]) - return true; - } - return false; - }; - links = links.filter(action_search, this); - } - catch (e) { - } - return links; - } - /** - * Overridden from the parent to also process any additional data that - * the data source adds, such as readonlys and additonal content. - * For example, non-numeric IDs in rows are added to the content manager - */ - _fetchCallback(_response) { - var nm = this.self._widget; - if (!nm) { - // Nextmatch either not connected, or it tried to destroy this - // but the server returned something - return; - } - // Readonlys - // Other stuff - for (var i in _response.rows) { - if (jQuery.isNumeric(i)) - continue; - if (i == 'sel_options') { - var mgr = nm.getArrayMgr(i); - for (var id in _response.rows.sel_options) { - mgr.data[id] = _response.rows.sel_options[id]; - var select = nm.getWidgetById(id); - if (select && select.set_select_options) { - select.set_select_options(_response.rows.sel_options[id]); - } - // Clear rowProvider internal cache so it uses new values - if (id == 'cat_id') { - this.self._rowProvider.categories = null; - } - // update array mgr so select widgets in row also get refreshed options - nm.getParent().getArrayMgr('sel_options').data[id] = _response.rows.sel_options[id]; - } - } - else if (i === "order" && _response.rows[i] !== nm.activeFilters.order) { - nm.sortBy(_response.rows[i], undefined, false); - } - else { - var mgr = nm.getArrayMgr('content'); - mgr.data[i] = _response.rows[i]; - // It's not enough to just update the data, the widgets need to - // be updated too, if there are matching widgets. - var widget = nm.getWidgetById(i); - if (widget && widget.set_value) { - widget.set_value(mgr.getEntry(i)); - } - } - } - // Might be trying to disable a column - var col_refresh = false; - for (var column_index = 0; column_index < nm.columns.length; column_index++) { - if (typeof nm.columns[column_index].disabled === 'string' && - nm.columns[column_index].disabled[0] === '@') { - col_refresh = true; - nm.dataview.columnMgr.getColumnById('col_' + column_index) - .set_visibility(nm.getArrayMgr('content').parseBoolExpression(nm.columns[column_index].disabled) ? - et2_dataview_column.ET2_COL_VISIBILITY_DISABLED : - nm.columns[column_index].visible); - } - } - if (col_refresh) { - nm.dataview.columnMgr.updated(); - nm.dataview._updateColumns(); - } - // If we're doing an autorefresh and the count decreases, preserve the - // selection or it will be lost when the grid rows are shuffled. Increases - // are fine though. - if (this.self && this.self.kept_selection == null && - !this.refresh && this.self._grid.getTotalCount() > _response.total) { - this.self.keepSelection(); - } - // Call the inherited function - super._fetchCallback.apply(this, arguments); - // Restore selection, if passed - if (this.self && this.self.kept_selection && this.self._selectionMgr) { - if (this.self.kept_selection.all) { - this.self._selectionMgr.selectAll(); - } - for (var i = (this.self.kept_selection.ids.length || 1) - 1; i >= 0; i--) { - // Only keep the selected if they came back in the fetch - if (_response.order.indexOf(this.self.kept_selection.ids[i]) >= 0) { - this.self._selectionMgr.setSelected(this.self.kept_selection.ids[i], true); - this.self.kept_selection.ids.splice(i, 1); - } - else { - this.self.kept_selection.ids.splice(i, 1); - } - } - if (this.self.kept_focus && _response.order.indexOf(this.self.kept_focus) >= 0) { - this.self._selectionMgr.setFocused(this.self.kept_focus, true); - } - // Re-expanding rows handled in et2_extension_nextmatch_rowProvider - // Expansions might still be valid, so we don't clear them - if (this.self.kept_selection != null && typeof this.self.kept_selection.ids != 'undefined' && this.self.kept_selection.ids.length == 0) { - this.self.kept_selection = null; - } - this.self.kept_focus = null; - } - } - /** - * Execute the select callback when the row selection changes - */ - _selectCallback(action, senders) { - if (typeof senders == "undefined") { - senders = []; - } - if (!this._widget) - return; - // inform mobile framework about nm selections, need to update status of header objects on selection - if (egwIsMobile()) - framework.nm_onselect_ctrl(this._widget, action, senders); - this._widget.onselect.call(this._widget, action, senders); - } - /** -- Implementation of et2_IDataProvider -- **/ - dataFetch(_queriedRange, _callback, _context) { - // Merge the parent id into the _queriedRange if it is set - if (this._parentId !== null) { - _queriedRange["parent_id"] = this._parentId; - } - // sub-levels dont have there own _filters object, need to use the one from parent (or it's parents parent) - var obj = this; - while ((typeof obj._filters == 'undefined' || jQuery.isEmptyObject(obj._filters)) && obj._parentController) { - obj = obj._parentController; - } - // Pass the fetch call to the API, multiplex the data about the - // nextmatch instance into the call. - this.egw.dataFetch(this._widget.getInstanceManager().etemplate_exec_id || this._execId, _queriedRange, obj._filters, this._widgetId, _callback, _context); - } - dataRegisterUID(_uid, _callback, _context) { - // Make sure we use correct prefix when data comes back - if (this._widget && this._widget._get_appname() != this.egw.getAppName()) { - _context.prefix = _uid.split('::')[0]; - } - this.egw.dataRegisterUID(_uid, _callback, _context, this._widget.getInstanceManager().etemplate_exec_id || this._execId, this._widgetId); - } - dataUnregisterUID() { - // Overwritten in the constructor - } -} -// Display constants -et2_nextmatch_controller.VIEW_ROW = 'row'; -et2_nextmatch_controller.VIEW_TILE = 'tile'; -//# sourceMappingURL=et2_extension_nextmatch_controller.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch_rowProvider.js b/api/js/etemplate/et2_extension_nextmatch_rowProvider.js deleted file mode 100644 index bbeb0faae6..0000000000 --- a/api/js/etemplate/et2_extension_nextmatch_rowProvider.js +++ /dev/null @@ -1,554 +0,0 @@ -/** - * EGroupware eTemplate2 - Class which contains a factory method for rows - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inheritance; - et2_core_interfaces; - et2_core_arrayMgr; - et2_core_widget; - et2_dataview_view_rowProvider; -*/ -import { et2_widget } from "./et2_core_widget"; -import { et2_arrayMgrs_expand } from "./et2_core_arrayMgr"; -import { et2_dataview_grid } from "./et2_dataview_view_grid"; -import { egw } from "../jsapi/egw_global"; -import { et2_IDetachedDOM, et2_IDOMNode } from "./et2_core_interfaces"; -/** - * The row provider contains prototypes (full clonable dom-trees) - * for all registered row types. - * - */ -export class et2_nextmatch_rowProvider { - /** - * Creates the nextmatch row provider. - * - * @param {et2_nextmatch_rowProvider} _rowProvider - * @param {function} _subgridCallback - * @param {object} _context - * @memberOf et2_nextmatch_rowProvider - */ - constructor(_rowProvider, _subgridCallback, _context) { - /** - * Match category-ids from class attribute eg. "cat_15" or "123,456,789 " - * - * Make sure to not match numbers inside other class-names. - * - * We can NOT use something like /(^| |,|cat_)([0-9]+)( |,|$)/g as it wont find all cats in "123,456,789 "! - */ - this.cat_regexp = /(^| |,|cat_)([0-9]+)/g; - /** - * Regular expression used to filter out non-nummerical chars from above matches - */ - this.cat_cleanup = /[^0-9]/g; - // Copy the arguments - this._rowProvider = _rowProvider; - this._subgridCallback = _subgridCallback; - this._context = _context; - this._createEmptyPrototype(); - } - destroy() { - this._rowProvider.destroy(); - this._subgridCallback = null; - this._context = null; - this._dataRow = null; - } - /** - * Creates the data row prototype. - * - * @param _widgets is an array containing the root widget for each column. - * @param _rowData contains the properties of the root "tr" (like its class) - * @param _rootWidget is the parent widget of the data rows (i.e. - * the nextmatch) - */ - setDataRowTemplate(_widgets, _rowData, _rootWidget) { - // Copy the root widget - this._rootWidget = _rootWidget; - // Create the base row - var row = this._rowProvider.getPrototype("default"); - // Copy the row template - var rowTemplate = { - "row": row[0], - "rowData": _rowData, - "widgets": _widgets, - "root": _rootWidget, - "seperated": null, - "mgrs": _rootWidget.getArrayMgrs() - }; - // Create the row widget and insert the given widgets into the row - var rowWidget = new et2_nextmatch_rowWidget(rowTemplate.mgrs, row[0]); - rowWidget._parent = _rootWidget; - rowWidget.createWidgets(_widgets); - // Get the set containing all variable attributes - var variableAttributes = this._getVariableAttributeSet(rowWidget); - // Filter out all widgets which do not implement the et2_IDetachedDOM - // interface or do not support all attributes listed in the et2_IDetachedDOM - // interface. A warning is issued for all those widgets as they heavily - // degrade the performance of the dataview - var seperated = rowTemplate.seperated = - this._seperateWidgets(variableAttributes); - // Remove all DOM-Nodes of all widgets inside the "remaining" slot from - // the row-template, then build the access functions for the detachable - // widgets - this._stripTemplateRow(rowTemplate); - this._buildNodeAccessFuncs(rowTemplate); - // Create the DOM row template - var tmpl = document.createDocumentFragment(); - row.children().each(function () { tmpl.appendChild(this); }); - this._dataRow = tmpl; - this._template = rowTemplate; - } - getDataRow(_data, _row, _idx, _controller) { - // Clone the row template - var row = this._dataRow.cloneNode(true); - // Create array managers with the given data merged in - var mgrs = et2_arrayMgrs_expand(rowWidget, this._template.mgrs, _data, _idx); - // Insert the widgets into the row which do not provide the functions - // to set the _data directly - var rowWidget = null; - if (this._template.seperated.remaining.length > 0) { - // Transform the variable attributes - for (var i = 0; i < this._template.seperated.remaining.length; i++) { - var entry = this._template.seperated.remaining[i]; - for (var j = 0; j < entry.data.length; j++) { - var set = entry.data[j]; - entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression); - } - } - // Create the row widget - var rowWidget = new et2_nextmatch_rowTemplateWidget(this._rootWidget, row); - // Let the row widget create the widgets - rowWidget.createWidgets(mgrs, this._template.placeholders); - } - // Update the content of all other widgets - for (var i = 0; i < this._template.seperated.detachable.length; i++) { - var entry = this._template.seperated.detachable[i]; - // Parse the attribute expressions - var data = {}; - for (var j = 0; j < entry.data.length; j++) { - var set = entry.data[j]; - data[set.attribute] = mgrs["content"].expandName(set.expression); - } - // Retrieve all DOM-Nodes - var nodes = new Array(entry.nodeFuncs.length); - for (var j = 0; j < nodes.length; j++) { - // Use the previously compiled node function to get the node - // from the entry - try { - nodes[j] = entry.nodeFuncs[j](row); - } - catch (e) { - debugger; - continue; - } - } - // Set the array managers first - entry.widget._mgrs = mgrs; - if (typeof data.id != "undefined") { - entry.widget.id = data.id; - } - // Adjust data for that row - entry.widget.transformAttributes.call(entry.widget, data); - // Call the setDetachedAttributes function - entry.widget.setDetachedAttributes(nodes, data, _data); - } - // Insert the row into the tr - var tr = _row.getDOMNode(); - tr.appendChild(row); - // Make the row expandable - if (typeof _data.content["is_parent"] !== "undefined" - && _data.content["is_parent"]) { - _row.makeExpandable(true, function () { - return this._subgridCallback.call(this._context, _row, _data, _controller); - }, this); - // Check for kept expansion, and set the row up to be re-expanded - // Only the top controller tracks expanded, including sub-grids - var top_controller = _controller; - while (top_controller._parentController != null) { - top_controller = top_controller._parentController; - } - var expansion_index = top_controller.kept_expansion.indexOf(top_controller.dataStorePrefix + '::' + _data.content[this._context.settings.row_id]); - if (top_controller.kept_expansion && expansion_index >= 0) { - top_controller.kept_expansion.splice(expansion_index, 1); - // Use a timeout since the DOM nodes might not be finished yet - window.setTimeout(function () { - _row.expansionButton.trigger('click'); - }, et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); - } - } - // Set the row data - this._setRowData(this._template.rowData, tr, mgrs); - return rowWidget; - } - /** - * Placeholder for empty row - * - * The empty row placeholder is used when there are no results to display. - * This allows the user to still have a drop target, or use actions that - * do not require a row ID, such as 'Add new'. - */ - _createEmptyPrototype() { - var label = this._context && this._context.options && this._context.options.settings.placeholder; - var placeholder = jQuery(document.createElement("td")) - .attr("colspan", this._rowProvider.getColumnCount()) - .css("height", "19px") - .text(typeof label != "undefined" && label ? label : egw().lang("No matches found")); - this._rowProvider._prototypes["empty"] = jQuery(document.createElement("tr")) - .addClass("egwGridView_empty") - .append(placeholder); - } - /** -- PRIVATE FUNCTIONS -- **/ - /** - * Returns an array containing objects which have variable attributes - * - * @param {et2_widget} _widget - */ - _getVariableAttributeSet(_widget) { - let variableAttributes = []; - const process = function (_widget) { - // Create the attribtues - var hasAttr = false; - var widgetData = { - "widget": _widget, - "data": [] - }; - // Get all attribute values - for (const key in _widget.attributes) { - if (!_widget.attributes[key].ignore && - typeof _widget.options[key] != "undefined") { - const val = _widget.options[key]; - // TODO: Improve detection - if (typeof val == "string" && val.indexOf("$") >= 0) { - hasAttr = true; - widgetData.data.push({ - "attribute": key, - "expression": val - }); - } - } - } - // Add the entry if there is any data in it - if (hasAttr) { - variableAttributes.push(widgetData); - } - }; - // Check each column - const columns = _widget._widgets; - for (var i = 0; i < columns.length; i++) { - // If column is hidden, don't process it - if (typeof columns[i] === 'undefined' || this._context && this._context.columns && this._context.columns[i] && !this._context.columns[i].visible) { - continue; - } - columns[i].iterateOver(process, this); - } - return variableAttributes; - } - _seperateWidgets(_varAttrs) { - // The detachable array contains all widgets which implement the - // et2_IDetachedDOM interface for all needed attributes - var detachable = []; - // The remaining array creates all widgets which have to be completely - // cloned when the widget tree is created - var remaining = []; - // Iterate over the widgets - for (var i = 0; i < _varAttrs.length; i++) { - var widget = _varAttrs[i].widget; - // Check whether the widget parents are not allready in the "remaining" - // slot - if this is the case do not include the widget at all. - var insertWidget = true; - var checkWidget = function (_widget) { - if (_widget.parent != null) { - for (var i = 0; i < remaining.length; i++) { - if (remaining[i].widget == _widget.parent) { - insertWidget = false; - return; - } - } - checkWidget(_widget.parent); - } - }; - checkWidget(widget); - // Handle the next widget if this one should not be included. - if (!insertWidget) { - continue; - } - // Check whether the widget implements the et2_IDetachedDOM interface - var isDetachable = false; - if (widget.implements(et2_IDetachedDOM)) { - // Get all attributes the widgets supports to be set in the - // "detached" mode - var supportedAttrs = []; - widget.getDetachedAttributes(supportedAttrs); - supportedAttrs.push("id"); - isDetachable = true; - for (var j = 0; j < _varAttrs[i].data.length /* && isDetachable*/; j++) { - var data = _varAttrs[i].data[j]; - var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1; - if (!supportsAttr) { - egw.debug("warn", "et2_IDetachedDOM widget " + - widget._type + " does not support " + data.attribute); - } - isDetachable = isDetachable && supportsAttr; - } - } - // Insert the widget into the correct slot - if (isDetachable) { - detachable.push(_varAttrs[i]); - } - else { - remaining.push(_varAttrs[i]); - } - } - return { - "detachable": detachable, - "remaining": remaining - }; - } - /** - * Removes to DOM code for all widgets in the "remaining" slot - * - * @param {object} _rowTemplate - */ - _stripTemplateRow(_rowTemplate) { - _rowTemplate.placeholders = []; - for (var i = 0; i < _rowTemplate.seperated.remaining.length; i++) { - var entry = _rowTemplate.seperated.remaining[i]; - // Issue a warning - widgets which do not implement et2_IDOMNode - // are very slow - egw.debug("warn", "Non-clonable widget '" + entry.widget._type + "' in dataview row - this " + - "might be slow", entry); - // Set the placeholder for the entry to null - entry.placeholder = null; - // Get the outer DOM-Node of the widget - if (entry.widget.implements(et2_IDOMNode)) { - var node = entry.widget.getDOMNode(entry.widget); - if (node && node.parentNode) { - // Get the parent node and replace the node with a placeholder - entry.placeholder = document.createElement("span"); - node.parentNode.replaceChild(entry.placeholder, node); - _rowTemplate.placeholders.push({ - "widget": entry.widget, - "func": this._compileDOMAccessFunc(_rowTemplate.row, entry.placeholder) - }); - } - } - } - } - _nodeIndex(_node) { - if (_node.parentNode == null) { - return 0; - } - for (var i = 0; i < _node.parentNode.childNodes.length; i++) { - if (_node.parentNode.childNodes[i] == _node) { - return i; - } - } - return -1; - } - /** - * Returns a function which does a relative access on the given DOM-Node - * - * @param {DOMElement} _root - * @param {DOMElement} _target - */ - _compileDOMAccessFunc(_root, _target) { - function recordPath(_root, _target, _path) { - if (typeof _path == "undefined") { - _path = []; - } - if (_root != _target && _target) { - // Get the index of _target in its parent node - var idx = this._nodeIndex(_target); - if (idx >= 0) { - // Add the access selector - _path.unshift("childNodes[" + idx + "]"); - // Record the remaining path - return recordPath.call(this, _root, _target.parentNode, _path); - } - throw ("Internal error while compiling DOM access function."); - } - else { - _path.unshift("_node"); - return "return " + _path.join(".") + ";"; - } - } - return new Function("_node", recordPath.call(this, _root, _target)); - } - /** - * Builds relative paths to the DOM-Nodes and compiles fast-access functions - * - * @param {object} _rowTemplate - */ - _buildNodeAccessFuncs(_rowTemplate) { - for (var i = 0; i < _rowTemplate.seperated.detachable.length; i++) { - var entry = _rowTemplate.seperated.detachable[i]; - // Get all needed nodes from the widget - var nodes = entry.widget.getDetachedNodes(); - var nodeFuncs = entry.nodeFuncs = new Array(nodes.length); - // Record the path to each DOM-Node - for (var j = 0; j < nodes.length; j++) { - nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row, nodes[j]); - } - } - } - /** - * Applies additional row data (like the class) to the tr - * - * @param {object} _data - * @param {DOMElement} _tr - * @param {object} _mgrs - */ - _setRowData(_data, _tr, _mgrs) { - // TODO: Implement other fields than "class" - if (_data["class"]) { - var classes = _mgrs["content"].expandName(_data["class"]); - // Get fancy with categories - var cats = []; - // Assume any numeric class is a category - if (_data["class"].indexOf("cat") !== -1 || classes.match(/[0-9]+/)) { - // Accept either cat, cat_id or category as ID, and look there for category settings - var category_location = _data["class"].match(/(cat(_id|egory)?)/); - if (category_location) - category_location = category_location[0]; - cats = classes.match(this.cat_regexp) || []; - classes = classes.replace(this.cat_regexp, ''); - // Set category class - for (var i = 0; i < cats.length; i++) { - // Need cat_, classes can't start with a number - var cat_id = cats[i].replace(this.cat_cleanup, ''); - var cat_class = 'cat_' + cat_id; - classes += ' ' + cat_class; - } - classes += " row_category"; - } - classes += " row"; - _tr.setAttribute("class", classes); - } - if (_data['valign']) { - var align = _mgrs["content"].expandName(_data["valign"]); - _tr.setAttribute("valign", align); - } - } -} -/** - * @augments et2_widget - */ -export class et2_nextmatch_rowWidget extends et2_widget { - /** - * Constructor - * - * @param _mgrs - * @param _row - * @memberOf et2_nextmatch_rowWidget - */ - constructor(_mgrs, _row) { - // Call the parent constructor with some dummy attributes - super(null, { "id": "", "type": "rowWidget" }); - // Initialize some variables - this._widgets = []; - // Copy the given DOM node and the content arrays - this._mgrs = _mgrs; - this._row = _row; - } - /** - * Copies the given array manager and clones the given widgets and inserts - * them into the row which has been passed in the constructor. - * - * @param {array} _widgets - */ - createWidgets(_widgets) { - // Clone the given the widgets with this element as parent - this._widgets = []; - let row_id = 0; - for (var i = 0; i < _widgets.length; i++) { - // Disabled columns might be missing widget - skip it - if (!_widgets[i]) - continue; - this._widgets[i] = _widgets[i].clone(this); - this._widgets[i].loadingFinished(); - // Set column alignment from widget - if (this._widgets[i].align && this._row.childNodes[row_id]) { - this._row.childNodes[row_id].align = this._widgets[i].align; - } - row_id++; - } - } - /** - * Returns the column node for the given sender - * - * @param {et2_widget} _sender - * @return {DOMElement} - */ - getDOMNode(_sender) { - var row_id = 0; - for (var i = 0; i < this._widgets.length; i++) { - // Disabled columns might be missing widget - skip it - if (!this._widgets[i]) - continue; - if (this._widgets[i] == _sender && this._row.childNodes[row_id]) { - return this._row.childNodes[row_id].childNodes[0]; // Return the i-th td tag - } - row_id++; - } - return null; - } -} -/** - * @augments et2_widget - */ -export class et2_nextmatch_rowTemplateWidget extends et2_widget { - /** - * Constructor - * - * @param _root - * @param _row - * @memberOf et2_nextmatch_rowTemplateWidget - */ - constructor(_root, _row) { - // Call the parent constructor with some dummy attributes - super(null, { "id": "", "type": "rowTemplateWidget" }); - this._root = _root; - this._mgrs = {}; - this._row = _row; - // Set parent to root widget, so sub-widget calls still work - this._parent = _root; - // Clone the widgets inside the placeholders array - this._widgets = []; - } - createWidgets(_mgrs, _widgets) { - // Set the array managers - don't use setArrayMgrs here as this creates - // an unnecessary copy of the object - this._mgrs = _mgrs; - this._widgets = new Array(_widgets.length); - for (var i = 0; i < _widgets.length; i++) { - this._row.childNodes[0].childNodes[0]; - this._widgets[i] = { - "widget": _widgets[i].widget.clone(this), - "node": _widgets[i].func(this._row) - }; - this._widgets[i].widget.loadingFinished(); - } - } - /** - * Returns the column node for the given sender - * - * @param {et2_widget} _sender - * @return {DOMElement} - */ - getDOMNode(_sender) { - for (var i = 0; i < this._widgets.length; i++) { - if (this._widgets[i].widget == _sender) { - return this._widgets[i].node; - } - } - return null; - } -} -//# sourceMappingURL=et2_extension_nextmatch_rowProvider.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_ajaxSelect.js b/api/js/etemplate/et2_widget_ajaxSelect.js deleted file mode 100644 index 769ccf2ce4..0000000000 --- a/api/js/etemplate/et2_widget_ajaxSelect.js +++ /dev/null @@ -1,225 +0,0 @@ -/** - - * EGroupware eTemplate2 - JS Ajax select / auto complete object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2012 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - et2_core_inputWidget; - et2_core_valueWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { et2_selectbox } from "./et2_widget_selectbox"; -/** - * Using AJAX, this widget allows a type-ahead find similar to a ComboBox, where as the user enters information, - * a drop-down box is populated with the n closest matches. If the user clicks on an item in the drop-down, that - * value is selected. - * n is the maximum number of results set in the user's preferences. - * The user is restricted to selecting values in the list. - * This widget can get data from any function that can provide data to a nextmatch widget. - * @augments et2_inputWidget - */ -export class et2_ajaxSelect extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_ajaxSelect - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_ajaxSelect._attributes, _child || {})); - this.input = null; - if (typeof _attrs.get_rows == 'string') { - _attrs.get_rows = this.egw().link('/index.php', { - menuaction: this.options.get_rows - }); - } - this.createInputWidget(); - this.input = null; - this.createInputWidget(); - } - createInputWidget() { - this.input = jQuery(document.createElement("input")); - this.input.addClass("et2_textbox"); - this.setDOMNode(this.input[0]); - let widget = this; - this.input.autocomplete({ - delay: 100, - source: this.options.get_rows ? - this.options.get_rows : - et2_selectbox.find_select_options(this, this.options.values), - select: function (event, ui) { - widget.value = ui.item[widget.options.id_field]; - if (widget.options.get_title) { - if (typeof widget.options.get_title == 'function') { - widget.input.val(widget.options.get_title.call(widget.value)); - } - else if (typeof widget.options.get_title == 'string') { - // TODO: Server side callback - } - } - else { - widget.input.val(ui.item.label); - } - // Prevent default action of setting field to the value - return false; - } - }); - } - getValue() { - if (this.options.blur && this.input.val() == this.options.blur) - return ""; - return this.value; - } - set_value(_value) { - this.value = _value; - if (this.input.autocomplete('instance')) { - let source = this.input.autocomplete('option', 'source'); - if (typeof source == 'object') { - for (let i in source) { - if (typeof source[i].value != 'undefined' && typeof source[i].label != 'undefined' && source[i].value === _value) { - this.input.val(source[i].label); - } - else if (typeof source[i] == 'string') { - this.input.val(source[_value]); - break; - } - } - } - else if (typeof source == 'function') { - // TODO - } - } - } - set_blur(_value) { - if (_value) { - this.input.attr("placeholder", _value + ""); // HTML5 - if (!this.input[0]["placeholder"]) { - // Not HTML5 - if (this.input.val() == "") - this.input.val(this.options.blur); - this.input.focus(this, function (e) { - if (e.data.input.val() == e.data.options.blur) - e.data.input.val(""); - }).blur(this, function (e) { - if (e.data.input.val() == "") - e.data.input.val(e.data.options.blur); - }); - } - } - else { - this.input.removeAttr("placeholder"); - } - } -} -et2_ajaxSelect._attributes = { - 'get_rows': { - "name": "Data source", - "type": "any", - "default": "", - "description": "Function to get search results, either a javascript function or server-side." - }, - 'get_title': { - "name": "Title function", - "type": "any", - "default": "", - "description": "Function to get title for selected entry. Used when closed, and if no template is given." - }, - 'id_field': { - "name": "Result ID field", - "type": "string", - "default": "value", - "description": "Which key in result sub-array to look for row ID. If omitted, the key for the row will be used." - }, - 'template': { - "name": "Row template", - "type": "string", - "default": "", - "description": "ID of the template to use to display rows. If omitted, title will be shown for each result." - }, - 'filter': { - "name": "Filter", - "type": "string", - "default": "", - "description": "Apply filter to search results. Same as nextmatch." - }, - 'filter2': { - "name": "Filter 2", - "type": "string", - "default": "", - "description": "Apply filter to search results. Same as nextmatch." - }, - 'link': { - "name": "Read only link", - "type": "boolean", - "default": "true", - "description": "If readonly, widget will be text. If link is set, widget will be a link." - }, - // Pass by code only - 'values': { - "name": "Values", - "type": "any", - "default": {}, - "description": "Specify the available options. Use this, or Data source." - } -}; -et2_register_widget(et2_ajaxSelect, ["ajax_select"]); -/** -* et2_textbox_ro is the dummy readonly implementation of the textbox. -* @augments et2_valueWidget -*/ -export class et2_ajaxSelect_ro extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_ajaxSelect_ro - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_ajaxSelect_ro._attributes, _child || {})); - this.value = ""; - this.span = jQuery(document.createElement("span")); - this.setDOMNode(this.span[0]); - } - set_value(_value) { - this.value = _value; - if (!_value) - _value = ""; - this.span.text(_value); - } - /** - * Code for implementing et2_IDetachedDOM - */ - getDetachedAttributes(_attrs) { - _attrs.push("value"); - } - getDetachedNodes() { - return [this.span[0]]; - } - setDetachedAttributes(_nodes, _values) { - this.span = jQuery(_nodes[0]); - if (typeof _values["value"] != 'undefined') { - this.set_value(_values["value"]); - } - } -} -/** - * Ignore all more advanced attributes. - */ -et2_ajaxSelect_ro._attributes = { - "multiline": { - "ignore": true - } -}; -et2_register_widget(et2_ajaxSelect_ro, ["ajax_select_ro"]); -//# sourceMappingURL=et2_widget_ajaxSelect.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_audio.js b/api/js/etemplate/et2_widget_audio.js deleted file mode 100644 index 8132d473ac..0000000000 --- a/api/js/etemplate/et2_widget_audio.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Audio tag - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Hadi Nategh - * @copyright EGroupware GmbH - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_baseWidget; -*/ -import { et2_baseWidget } from './et2_core_baseWidget'; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_register_widget } from "./et2_core_widget"; -/** - * This widget represents the HTML5 Audio tag with all its optional attributes - * - * The widget can be created in the following ways: - * - * var audioTag = et2_createWidget("audio", { - * audio_src: "../../test.mp3", - * src_type: "audio/mpeg", - * muted: true, - * autoplay: true, - * controls: true, - * loop: true, - * height: 100, - * width: 200, - * }); - * - * Or by adding XET-tag in your template (.xet) file: - * - * - */ -/** - * Class which implements the "audio" XET-Tag - * - * @augments et2_baseWidget - */ -export class et2_audio extends et2_baseWidget { - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_audio._attributes, _child || {})); - this.audio = null; - this.container = null; - //Create Audio tag - this.audio = new Audio(); - // Container - this.container = document.createElement('div'); - this.container.append(this.audio); - this.container.classList.add('et2_audio'); - if (this.options.autohide) - this.container.classList.add('et2_audio_autohide'); - if (this.options.controls) - this.audio.setAttribute("controls", '1'); - if (this.options.autoplay) - this.audio.setAttribute("autoplay", '1'); - if (this.options.muted) - this.audio.setAttribute("muted", '1'); - if (this.options.loop) - this.audio.setAttribute("loop", '1'); - if (this.options.preload) - this.audio.setAttribute('preload', this.options.preload); - this.setDOMNode(this.container); - } - /** - * Set audio source - * - * @param {string} _value url - */ - set_src(_value) { - if (_value) { - this.audio.setAttribute('src', _value); - if (this.options.src_type) { - this.audio.setAttribute('type', this.options.src_type); - } - //preload the audio after changing the source/ only if preload is allowed - if (this.options.preload != "none") - this.audio.load(); - } - } - /** - * @return Promise - */ - play() { - return this.audio.play(); - } - pause() { - this.audio.pause(); - } - currentTime() { - return this.audio.currentTime; - } - seek(_time) { - this.audio.currentTime = _time; - } -} -et2_audio._attributes = { - "src": { - "name": "Audio", - "type": "string", - "description": "Source of audio to play" - }, - "src_type": { - "name": "Source type", - "type": "string", - "description": "Defines the type the stream source provided" - }, - "muted": { - "name": "Audio control", - "type": "boolean", - "default": false, - "description": "Defines that the audio output should be muted" - }, - "autoplay": { - "name": "Autoplay", - "type": "boolean", - "default": false, - "description": "Defines if audio will start playing as soon as it is ready" - }, - "controls": { - "name": "Control buttons", - "type": "boolean", - "default": true, - "description": "Defines if audio controls, play/pause buttons should be displayed" - }, - "loop": { - "name": "Audio loop", - "type": "boolean", - "default": false, - "description": "Defines if the audio should be played repeatedly" - }, - "autohide": { - "name": "Auto hide", - "type": "boolean", - "default": false, - "description": "Auto hides audio control bars and only shows a play button, hovering for few seconds will show the whole controlbar." - }, - "preload": { - "name": "preload", - "type": "string", - "default": 'auto', - "description": "preloads audio source based on given option. none(do not preload), auto(preload), metadata(preload metadata only)." - } -}; -et2_register_widget(et2_audio, ["audio"]); -//# sourceMappingURL=et2_widget_audio.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_barcode.js b/api/js/etemplate/et2_widget_barcode.js deleted file mode 100644 index 94c5e23ab1..0000000000 --- a/api/js/etemplate/et2_widget_barcode.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * EGroupware eTemplate2 - JS barcode widget - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Hadi Nategh - * @copyright EGroupware GmbH - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /api/js/jquery/barcode/jquery-barcode.min.js; - et2_core_interfaces; - et2_core_baseWidget; -*/ -/** - * This widget creates barcode out of a given text - * - * The widget can be created in the following ways: - * - * var barcodeTag = et2_createWidget("barcode", { - * code_type:et2_barcode.TYPE_CSS, - * bgColor:"#FFFFFF", - * barColor:"#000000", - * format:et2_barcode.FORMAT_SVG, - * barWidth:"1", - * barHeight:"50" - * }); - * - * Or by adding XET-tag in your template (.xet) file: - * - * - * - * - * Further information about types and formats are defined in static part of the class at the end - */ -import { et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -/** - * Class which implements the "barcode" XET-Tag - * - */ -export class et2_barcode extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_barcode._attributes, _child || {})); - this.div = jQuery(document.createElement('div')).attr({ class: 'et2_barcode' }); - // Set domid - this.set_id(this.id); - this.setDOMNode(this.div[0]); - this.createWidget(); - } - createWidget() { - this.settings = { - output: this.options.format, - bgColor: this.options.bgColor, - color: this.options.barColor, - barWidth: this.options.barWidth, - barHeight: this.options.barHeight, - }; - if (this.get_value()) { - // @ts-ignore - this.div.barcode(this.get_value(), this.options.code_type, this.settings); - } - } - set_value(_val) { - if (typeof _val !== 'undefined') { - this.value = _val; - this.createWidget(); - } - } -} -// Class Constants -/* - * type const - */ -et2_barcode.TYPE_CODEBAR = "codebar"; -et2_barcode.TYPE_CODE11 = "code11"; //(code 11) -et2_barcode.TYPE_CODE39 = "code39"; //(code 39) -et2_barcode.TYPE_CODE128 = "code128"; //(code 128) -et2_barcode.TYPE_EAN8 = "ean8"; //(ean 8) - http://barcode-coder.com/en/ean-8-specification-101.html -et2_barcode.TYPE_EAN13 = "ean13"; //(ean 13) - http://barcode-coder.com/en/ean-13-specification-102.html -et2_barcode.TYPE_STD25 = "std25"; //(standard 2 of 5 - industrial 2 of 5) - http://barcode-coder.com/en/standard-2-of-5-specification-103.html -et2_barcode.TYPE_INT25 = "int25"; //(interleaved 2 of 5) -et2_barcode.TYPE_MSI = "msi"; -et2_barcode.TYPE_DATAMATRIX = "datamatrix"; //(ASCII + extended) - http://barcode-coder.com/en/datamatrix-specification-104.html -/** - * Formats consts - */ -et2_barcode.FORMAT_CSS = "css"; -et2_barcode.FORMAT_SVG = "svg"; -et2_barcode.FORMAT_bmp = "bmp"; -et2_barcode.FORMAT_CANVAS = "canvas"; -et2_barcode._attributes = { - "code_type": { - "name": "code type", - "type": "string", - "default": et2_barcode.TYPE_DATAMATRIX, - "description": "Barcode type to be generated, default is QR barcode" - }, - bgColor: { - "name": "bgColor", - "type": "string", - "default": '#FFFFFF', - "description": "Defines backgorund color of barcode container" - }, - barColor: { - "name": "barColor", - "type": "string", - "default": '#000000', - "description": "Defines color of the bars in barcode." - }, - format: { - "name": "format", - "type": "string", - "default": 'css', - "description": "Defines in which format the barcode should be rendered. Default is SVG." - }, - barWidth: { - "name": "bar width", - "type": "string", - "default": '1', - "description": "Defines width of each bar in the barcode." - }, - barHeight: { - "name": "bar height", - "type": "string", - "default": '50', - "description": "Defines heigh of each bar in the barcode." - }, -}; -et2_register_widget(et2_barcode, ["barcode"]); -//# sourceMappingURL=et2_widget_barcode.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_box.js b/api/js/etemplate/et2_widget_box.js deleted file mode 100644 index 278fdd5380..0000000000 --- a/api/js/etemplate/et2_widget_box.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Box object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_baseWidget } from "./et2_core_baseWidget"; -import { et2_readAttrWithDefault } from "./et2_core_xml"; -/** - * Class which implements box and vbox tag - * - * Auto-repeat: In order to get box auto repeat to work we need to have another - * box as a wrapper with an id set. - * - * @augments et2_baseWidget - */ -export class et2_box extends et2_baseWidget { - /** - * Constructor - * - * @memberOf et2_box - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, _child); - this.div = jQuery(document.createElement("div")) - .addClass("et2_" + this.getType()) - .addClass("et2_box_widget"); - this.setDOMNode(this.div[0]); - } - _createNamespace() { - return true; - } - /** - * Overriden so we can check for autorepeating children. We only check for - * $ in the immediate children & grandchildren of this node. - * - * @param {object} _node - */ - loadFromXML(_node) { - if (this.getType() != "box") { - return super.loadFromXML(_node); - } - // Load the child nodes. - var childIndex = 0; - var repeatNode = null; - for (var i = 0; i < _node.childNodes.length; i++) { - var node = _node.childNodes[i]; - var widgetType = node.nodeName.toLowerCase(); - if (widgetType == "#comment") { - continue; - } - if (widgetType == "#text") { - if (node.data.replace(/^\s+|\s+$/g, '')) { - this.loadContent(node.data); - } - continue; - } - // Create the new element, if no expansion needed - var id = et2_readAttrWithDefault(node, "id", ""); - if (id.indexOf('$') < 0 || ['box', 'grid'].indexOf(widgetType) == -1) { - this.createElementFromNode(node); - childIndex++; - } - else { - repeatNode = node; - } - } - // Only the last child repeats(?) - if (repeatNode != null) { - var currentPerspective = this.getArrayMgr("content").perspectiveData; - // Extra content - for (childIndex; typeof this.getArrayMgr("content").data[childIndex] != "undefined" && this.getArrayMgr("content").data[childIndex]; childIndex++) { - // Adjust for the row - var mgrs = this.getArrayMgrs(); - for (var name in mgrs) { - if (this.getArrayMgr(name).getEntry(childIndex)) { - this.getArrayMgr(name).setRow(childIndex); - } - } - this.createElementFromNode(repeatNode); - } - // Reset - for (var name in this.getArrayMgrs()) { - this.getArrayMgr(name).setPerspectiveData(currentPerspective); - } - } - } - /** - * Code for implementing et2_IDetachedDOM - * This doesn't need to be implemented. - * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - _attrs.push('data'); - } - getDetachedNodes() { - return [this.getDOMNode()]; - } - setDetachedAttributes(_nodes, _values) { - if (_values.data) { - var pairs = _values.data.split(/,/g); - for (var i = 0; i < pairs.length; ++i) { - var name_value = pairs[i].split(':'); - jQuery(_nodes[0]).attr('data-' + name_value[0], name_value[1]); - } - } - } -} -et2_box._attributes = { - // Not needed - "rows": { "ignore": true }, - "cols": { "ignore": true } -}; -et2_register_widget(et2_box, ["vbox", "box"]); -/** - * Details widget implementation - * widget name is "details" and can be use as a wrapping container - * in order to make its children collapsible. - * - * Note: details widget does not represent html5 "details" tag in DOM - * - *
- * - * .... - *
- * - */ -export class et2_details extends et2_box { - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, _child); - this.div = jQuery(document.createElement('div')).addClass('et2_details'); - this.title = jQuery(document.createElement('span')) - .addClass('et2_label et2_details_title') - .appendTo(this.div); - this.span = jQuery(document.createElement('span')) - .addClass('et2_details_toggle') - .appendTo(this.div); - this.wrapper = jQuery(document.createElement('div')) - .addClass('et2_details_wrapper') - .appendTo(this.div); - this._createWidget(); - } - /** - * Function happens on toggle action - */ - _toggle() { - this.div.toggleClass('et2_details_expanded'); - } - /** - * Create widget, set contents, and binds handlers - */ - _createWidget() { - const self = this; - this.span.on('click', function (e) { - self._toggle(); - }); - //Set header title - if (this.options.title) { - this.title - .click(function () { - self._toggle(); - }) - .text(this.options.title); - } - // Align toggle button left/right - if (this.options.toggle_align === "left") - this.span.css({ float: 'left' }); - } - getDOMNode(_sender) { - if (!_sender || _sender === this) { - return this.div[0]; - } - else { - return this.wrapper[0]; - } - } -} -et2_details._attributes = { - "toggle_align": { - name: "Toggle button alignment", - description: " Defines where to align the toggle button, default is right alignment", - type: "string", - default: "right" - }, - title: { - name: "title", - description: "Set a header title for box and shows it next to toggle button, default is no title", - type: "string", - default: "", - translate: true - } -}; -et2_register_widget(et2_details, ["details"]); -//# sourceMappingURL=et2_widget_box.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_button.js b/api/js/etemplate/et2_widget_button.js deleted file mode 100644 index f5c6d62b0b..0000000000 --- a/api/js/etemplate/et2_widget_button.js +++ /dev/null @@ -1,378 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Button object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_baseWidget; -*/ -import { et2_no_init } from "./et2_core_common"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_baseWidget } from './et2_core_baseWidget'; -/** - * Class which implements the "button" XET-Tag - */ -export class et2_button extends et2_baseWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_button._attributes, _child || {})); - this.label = ""; - this.clicked = false; - this.btn = null; - this.image = null; - if (!this.options.background_image && (this.options.image || this.options.ro_image)) { - this.image = jQuery(document.createElement("img")) - .addClass("et2_button et2_button_icon"); - if (!this.options.readonly) - this.image.addClass("et2_clickable"); - this.setDOMNode(this.image[0]); - return; - } - if (!this.options.readonly || this.options.ro_image) { - this.btn = jQuery(document.createElement("button")) - .addClass("et2_button") - .attr({ type: "button" }); - this.setDOMNode(this.btn[0]); - } - if (this.options.image) - this.set_image(this.options.image); - } - /** - * Apply the "modifications" to the element and translate attributes marked - * with "translate: true" - * - * Reimplemented here to assign default background-images to buttons - * - * @param {object} _attrs - */ - transformAttributes(_attrs) { - if (this.id && typeof _attrs.background_image == 'undefined' && !_attrs.image) { - for (var image in et2_button.default_background_images) { - if (this.id.match(et2_button.default_background_images[image])) { - _attrs.image = image; - _attrs.background_image = true; - break; - } - } - } - for (var name in et2_button.default_classes) { - if (this.id.match(et2_button.default_classes[name])) { - _attrs.class = (typeof _attrs.class == 'undefined' ? '' : _attrs.class + ' ') + name; - break; - } - } - super.transformAttributes(_attrs); - } - set_accesskey(key) { - jQuery(this.node).attr("accesskey", key); - } - /** - * Set image and update current image - * - * @param _image - */ - set_image(_image) { - this.options.image = _image; - this.update_image(); - } - /** - * Set readonly image and update current image - * - * @param _image - */ - set_ro_image(_image) { - this.options.ro_image = _image; - this.update_image(); - } - /** - * Set current image (dont update options.image) - * - * @param _image - */ - update_image(_image) { - if (!this.isInTree() || !this.options.background_image && this.image == null) - return; - if (typeof _image == 'undefined') - _image = this.options.readonly ? (this.options.ro_image || this.options.image) : this.options.image; - // Silently blank for percentages instead of warning about missing image - use a progress widget - if (_image.match(/^[0-9]+\%$/)) { - _image = ""; - //this.egw().debug("warn", "Use a progress widget instead of percentage images", this); - } - var found_image = false; - if (_image != "") { - var src = this.egw().image(_image); - if (src) { - found_image = true; - } - else if (_image[0] == '/' || _image.substr(0, 4) == 'http') { - src = _image; - found_image = true; - } - if (found_image) { - if (this.image != null) { - this.image.attr("src", src); - } - else if (this.options.background_image && this.btn) { - this.btn.css("background-image", "url(" + src + ")"); - this.btn.addClass('et2_button_with_image'); - } - } - } - if (!found_image) { - this.set_label(this.label); - if (this.btn) { - this.btn.css("background-image", ""); - this.btn.removeClass('et2_button_with_image'); - } - } - } - /** - * Set options.readonly and update image - * - * @param {boolean} _ro - */ - set_readonly(_ro) { - if (_ro != this.options.readonly) { - this.options.readonly = _ro; - if (this.options.image || this.options.ro_image) { - this.update_image(); - } - // dont show readonly buttons as clickable - if (this.btn || this.image) { - (this.btn || this.image) - .toggleClass('et2_clickable', !_ro) - .toggleClass('et2_button_ro', _ro) - .css('cursor', _ro ? 'default' : 'pointer'); // temp. 'til it is removed from et2_button - } - } - } - attachToDOM() { - let ret = super.attachToDOM(); - if (this.options.readonly && (this.btn || this.image)) { - (this.btn || this.image) - .removeClass('et2_clickable') - .addClass('et2_button_ro') - .css('cursor', 'default'); // temp. 'til it is removed from et2_button - } - return ret; - } - getDOMNode() { - return this.btn ? this.btn[0] : (this.image ? this.image[0] : null); - } - /** - * Overwritten to maintain an internal clicked attribute - * - * @param _ev - * @returns {Boolean} - */ - click(_ev) { - var _a, _b; - // ignore click on readonly button - if (this.options.readonly) - return false; - this.clicked = true; - // Cancel buttons don't trigger the close confirmation prompt - if ((_a = this.btn) === null || _a === void 0 ? void 0 : _a.hasClass("et2_button_cancel")) { - this.getInstanceManager().skip_close_prompt(); - } - if (!super.click.apply(this, arguments)) { - this.clicked = false; - return false; - } - // Submit the form - if (this.getType() != "buttononly") { - this.getInstanceManager().submit(this, false, this.options.novalidate); //TODO: this only needs to be passed if it's in a datagrid - } - this.clicked = false; - (_b = this.getInstanceManager()) === null || _b === void 0 ? void 0 : _b.skip_close_prompt(false); - return true; - } - set_label(_value) { - if (this.btn) { - this.label = _value; - this.btn.text(_value); - if (_value && !this.image) - this.btn.addClass('et2_button_text'); - else - this.btn.removeClass('et2_button_text'); - } - if (this.image) { - this.image.attr("alt", _value); - // Don't set title if there's a tooltip, browser may show both - if (!this.options.statustext) { - this.image.attr("title", _value); - } - } - } - /** - * Set tab index - * - * @param {number} index - */ - set_tabindex(index) { - jQuery(this.btn).attr("tabindex", index); - } - /** - * Implementation of the et2_IInput interface - */ - /** - * Always return false as a button is never dirty - */ - isDirty() { - return false; - } - resetDirty() { - } - getValue() { - if (this.clicked) { - return true; - } - // If "null" is returned, the result is not added to the submitted - // array. - return null; - } - isValid() { - return true; - } - /** - * et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes(_attrs) { - _attrs.push("label", "value", "class", "image", "ro_image", "onclick", "background_image"); - } - getDetachedNodes() { - return [ - this.btn != null ? this.btn[0] : null, - this.image != null ? this.image[0] : null - ]; - } - setDetachedAttributes(_nodes, _values) { - // Datagrid puts in the row for null - this.btn = _nodes[0].nodeName[0] != '#' ? jQuery(_nodes[0]) : null; - this.image = jQuery(_nodes[1]); - if (typeof _values["id"] != "undefined") { - this.set_id(_values["id"]); - } - if (typeof _values["label"] != "undefined") { - this.set_label(_values["label"]); - } - if (typeof _values["value"] != "undefined") { - } - if (typeof _values["image"] != "undefined") { - this.set_image(_values["image"]); - } - if (typeof _values["ro_image"] != "undefined") { - this.set_ro_image(_values["ro_image"]); - } - if (typeof _values["class"] != "undefined") { - this.set_class(_values["class"]); - } - if (typeof _values["onclick"] != "undefined") { - this.options.onclick = _values["onclick"]; - } - var type = this.getType(); - var attrs = jQuery.extend(_values, this.options); - var parent = this.getParent(); - jQuery(this.getDOMNode()).bind("click.et2_baseWidget", this, function (e) { - var widget = et2_createWidget(type, attrs, parent); - e.data = widget; - e.data.set_id(_values["id"]); - return e.data.click.call(e.data, e); - }); - } -} -et2_button._attributes = { - "label": { - "name": "caption", - "type": "string", - "description": "Label of the button", - "translate": true - }, - "image": { - "name": "Icon", - "type": "string", - "description": "Use an icon instead of label (when available)" - }, - "ro_image": { - "name": "Read-only Icon", - "type": "string", - "description": "Use this icon instead of hiding for read-only" - }, - "onclick": { - "description": "JS code which gets executed when the button is clicked", - "type": "js" - }, - "accesskey": { - "name": "Access Key", - "type": "string", - "default": et2_no_init, - "description": "Alt + activates widget" - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - background_image: { - name: "Add image in front of text", - type: "boolean", - description: "Adds image in front of text instead of just using an image with text as tooltip", - default: et2_no_init // to leave it undefined, if not defined, so background-image is assigned by default - }, - novalidate: { - name: "Do NOT validate form", - type: "boolean", - description: "Do NOT validate form before submitting it", - default: false - }, - // No such thing as a required button - "needed": { - "ignore": true - } -}; -et2_button.legacyOptions = ["image", "ro_image"]; -/** - * images to be used as background-image, if none is explicitly applied and id matches given regular expression - */ -et2_button.default_background_images = { - save: /save(&|\]|$)/, - apply: /apply(&|\]|$)/, - cancel: /cancel(&|\]|$)/, - delete: /delete(&|\]|$)/, - discard: /discard(&|\]|$)/, - edit: /edit(&|\[\]|$)/, - next: /(next|continue)(&|\]|$)/, - finish: /finish(&|\]|$)/, - back: /(back|previous)(&|\]|$)/, - copy: /copy(&|\]|$)/, - more: /more(&|\]|$)/, - check: /(yes|check)(&|\]|$)/, - cancelled: /no(&|\]|$)/, - ok: /ok(&|\]|$)/, - close: /close(&|\]|$)/, - add: /(add(&|\]|$)|create)/ // customfields use create* -}; -/** - * Classnames added automatic to buttons to set certain hover background colors - */ -et2_button.default_classes = { - et2_button_cancel: /cancel(&|\]|$)/, - et2_button_question: /(yes|no)(&|\]|$)/, - et2_button_delete: /delete(&|\]|$)/ // red -}; -et2_register_widget(et2_button, ["button", "buttononly"]); -//# sourceMappingURL=et2_widget_button.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_checkbox.js b/api/js/etemplate/et2_widget_checkbox.js deleted file mode 100644 index 9cb285da8d..0000000000 --- a/api/js/etemplate/et2_widget_checkbox.js +++ /dev/null @@ -1,254 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Checkbox object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2011 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -/** - * Class which implements the "checkbox" XET-Tag - * - * @augments et2_inputWidget - */ -export class et2_checkbox extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_checkbox - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_checkbox._attributes, _child || {})); - this.input = null; - this.toggle = null; - this.input = null; - this.createInputWidget(); - } - createInputWidget() { - this.input = jQuery(document.createElement("input")).attr("type", "checkbox"); - this.input.addClass("et2_checkbox"); - if (this.options.toggle_on || this.options.toggle_off) { - let self = this; - // checkbox container - this.toggle = jQuery(document.createElement('span')) - .addClass('et2_checkbox_slideSwitch') - .append(this.input); - // update switch status on change - this.input.change(function () { - self.getValue(); - return true; - }); - // switch container - let area = jQuery(document.createElement('span')).addClass('slideSwitch_container').appendTo(this.toggle); - // on span tag - let on = jQuery(document.createElement('span')).addClass('on').appendTo(area); - // off span tag - let off = jQuery(document.createElement('span')).addClass('off').appendTo(area); - on.text(this.options.toggle_on); - off.text(this.options.toggle_off); - // handle a tag - jQuery(document.createElement('a')).appendTo(area); - this.setDOMNode(this.toggle[0]); - } - else { - this.setDOMNode(this.input[0]); - } - } - /** - * Override default to place checkbox before label, if there is no %s in the label - * - * @param {string} label - */ - set_label(label) { - if (label.length && label.indexOf('%s') < 0) { - label = '%s' + label; - } - super.set_label(label); - jQuery(this.getSurroundings().getWidgetSurroundings()).addClass('et2_checkbox_label'); - } - /** - * Override default to match against set/unset value - * - * @param {string|boolean} _value - */ - set_value(_value) { - // in php, our database storage and et2_checkType(): "0" == false - if (_value === "0" && this.options.selected_value != "0") { - _value = false; - } - if (_value != this.value) { - if (_value == this.options.selected_value || - _value && this.options.selected_value == this.attributes["selected_value"]["default"] && - _value != this.options.unselected_value) { - if (this.options.toggle_on || this.options.toggle_off) - this.toggle.addClass('switchOn'); - this.input.prop("checked", true); - } - else { - this.input.prop("checked", false); - if (this.options.toggle_on || this.options.toggle_off) - this.toggle.removeClass('switchOn'); - } - } - } - /** - * Disable checkbox on runtime - * - * @param {boolean} _ro - */ - set_readonly(_ro) { - jQuery(this.getDOMNode()).attr('disabled', _ro); - this.input.prop('disabled', _ro); - } - /** - * Override default to return unchecked value - */ - getValue() { - if (this.input.prop("checked")) { - if (this.options.toggle_on || this.options.toggle_off) - this.toggle.addClass('switchOn'); - return this.options.selected_value; - } - else { - if (this.options.toggle_on || this.options.toggle_off) - this.toggle.removeClass('switchOn'); - return this.options.unselected_value; - } - } - set_disabled(_value) { - let parentNode = jQuery(this.getDOMNode()).parent(); - if (parentNode[0] && parentNode[0].nodeName == "label" && parentNode.hasClass('.et2_checkbox_label')) { - if (_value) { - parentNode.hide(); - } - else { - parentNode.show(); - } - } - super.set_disabled(_value); - } -} -et2_checkbox._attributes = { - "selected_value": { - "name": "Set value", - "type": "string", - "default": "true", - "description": "Value when checked" - }, - "unselected_value": { - "name": "Unset value", - "type": "string", - "default": "", - "description": "Value when not checked" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "X ", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - }, - "value": { - // Stop framework from messing with value - "type": "any" - }, - "toggle_on": { - "name": "Toggle on caption", - "type": "string", - "default": "", - "description": "String caption to show for ON status", - "translate": true - }, - "toggle_off": { - "name": "Toggle off caption", - "type": "string", - "default": "", - "description": "String caption to show OFF status", - "translate": true - } -}; -et2_checkbox.legacyOptions = ["selected_value", "unselected_value", "ro_true", "ro_false"]; -et2_register_widget(et2_checkbox, ["checkbox"]); -/** -* et2_checkbox_ro is the dummy readonly implementation of the checkbox -* @augments et2_checkbox -*/ -export class et2_checkbox_ro extends et2_checkbox { - /** - * Constructor - * - * @memberOf et2_checkbox_ro - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_checkbox_ro._attributes, _child || {})); - this.span = null; - this.value = ""; - this.span = jQuery(document.createElement("span")) - .addClass("et2_checkbox_ro"); - this.setDOMNode(this.span[0]); - } - /** - * note: checkbox is checked if even there is a value but not only if the _value is only "true" - * it's an exceptional validation for cases that we pass non boolean values as checkbox _value - * - * @param {string|boolean} _value - */ - set_value(_value) { - if (_value == this.options.selected_value || _value && this.options.selected_value == this.attributes["selected_value"]["default"] && - _value != this.options.unselected_value) { - this.span.text(this.options.ro_true); - this.value = _value; - } - else { - this.span.text(this.options.ro_false); - } - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes(_attrs) { - _attrs.push("value", "class"); - } - getDetachedNodes() { - return [this.span[0]]; - } - setDetachedAttributes(_nodes, _values) { - // Update the properties - if (typeof _values["value"] != "undefined") { - this.span = jQuery(_nodes[0]); - this.set_value(_values["value"]); - } - if (typeof _values["class"] != "undefined") { - _nodes[0].setAttribute("class", _values["class"]); - } - } -} -/** - * Ignore unset value - */ -et2_checkbox_ro._attributes = { - "unselected_value": { - "ignore": true - } -}; -et2_register_widget(et2_checkbox_ro, ["checkbox_ro"]); -//# sourceMappingURL=et2_widget_checkbox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_color.js b/api/js/etemplate/et2_widget_color.js deleted file mode 100644 index a8e352bdbe..0000000000 --- a/api/js/etemplate/et2_widget_color.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Color picker object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2012 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -/** - * Class which implements the "colorpicker" XET-Tag - * - */ -export class et2_color extends et2_inputWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_color._attributes, _child || {})); - this.cleared = true; - // included via etemplate2.css - //this.egw().includeCSS("phpgwapi/js/jquery/jpicker/css/jPicker-1.1.6.min.css"); - this.span = jQuery(""); - this.image = jQuery("") - .appendTo(this.span) - .on("click", function () { - this.input.trigger('click'); - }.bind(this)); - this.input = jQuery("").appendTo(this.span) - .on('change', function () { - this.cleared = false; - this.image.hide(); - }.bind(this)); - if (!this.options.readonly && !this.options.needed) { - this.clear = jQuery("") - .appendTo(this.span) - .on("click", function () { - this.set_value(''); - return false; - }.bind(this)); - } - this.setDOMNode(this.span[0]); - } - getValue() { - var value = this.input.val(); - if (this.cleared || value === '#FFFFFF' || value === '#ffffff') { - return ''; - } - return value; - } - set_value(color) { - if (!color) { - color = ''; - } - this.cleared = !color; - this.image.toggle(!color); - this.input.val(color); - } -} -et2_register_widget(et2_color, ["colorpicker"]); -/** - * et2_textbox_ro is the dummy readonly implementation of the textbox. - * @augments et2_valueWidget - */ -export class et2_color_ro extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_color_ro - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, _child || {}); - this.value = ""; - this.$node = jQuery(document.createElement("div")) - .addClass("et2_color"); - this.setDOMNode(this.$node[0]); - } - set_value(_value) { - this.value = _value; - if (!_value) - _value = "inherit"; - this.$node.css("background-color", _value); - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - _attrs.push("value"); - } - getDetachedNodes() { - return [this.node]; - } - setDetachedAttributes(_nodes, _values) { - this.$node = jQuery(_nodes[0]); - if (typeof _values["value"] != 'undefined') { - this.set_value(_values["value"]); - } - } -} -et2_register_widget(et2_color_ro, ["colorpicker_ro"]); -//# sourceMappingURL=et2_widget_color.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_countdown.js b/api/js/etemplate/et2_widget_countdown.js deleted file mode 100644 index 2948fa06be..0000000000 --- a/api/js/etemplate/et2_widget_countdown.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * EGroupware eTemplate2 - Countdown timer widget - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Hadi Nategh - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; -*/ -import { et2_no_init } from "./et2_core_common"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_register_widget } from "./et2_core_widget"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { egw } from "../jsapi/egw_global"; -/** - * Class which implements the "countdown" XET-Tag - * - * Value for countdown is an integer duration in seconds or a server-side to a duration converted expiry datetime. - * - * The duration has the benefit, that it does not depend on the correct set time and timezone of the browser / computer of the user. - */ -export class et2_countdown extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_countdown._attributes, _child || {})); - this.timer = null; - this.container = null; - this.days = null; - this.hours = null; - this.minutes = null; - this.seconds = null; - // Build countdown dom container - this.container = jQuery(document.createElement("div")) - .addClass("et2_countdown"); - this.days = jQuery(document.createElement("span")) - .addClass("et2_countdown_days").appendTo(this.container); - this.hours = jQuery(document.createElement("span")) - .addClass("et2_countdown_hours").appendTo(this.container); - this.minutes = jQuery(document.createElement("span")) - .addClass("et2_countdown_minutes").appendTo(this.container); - this.seconds = jQuery(document.createElement("span")) - .addClass("et2_countdown_seconds").appendTo(this.container); - this.setDOMNode(this.container[0]); - } - set_value(_time) { - if (isNaN(_time)) - return; - super.set_value(_time); - this.time = new Date(); - this.time.setSeconds(this.time.getSeconds() + parseInt(_time)); - let self = this; - this.timer = setInterval(function () { - if (self._updateTimer() <= 0) { - clearInterval(self.timer); - if (typeof self.onFinish == "function") - self.onFinish(); - } - }, 1000); - } - _updateTimer() { - let now = new Date(); - let distance = this.time.getTime() - now.getTime(); - if (distance < 0) - return 0; - if (this.options.alarm > 0 && this.options.alarm == distance / 1000 && typeof this.onAlarm == 'function') { - this.onAlarm(); - } - let values = { - days: Math.floor(distance / (1000 * 60 * 60 * 24)), - hours: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), - minutes: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)), - secounds: Math.floor((distance % (1000 * 60)) / 1000) - }; - this.days.text(values.days + this._getIndicator("days")); - this.hours.text(values.hours + this._getIndicator("hours")); - this.minutes.text(values.minutes + this._getIndicator("minutes")); - this.seconds.text(values.secounds + this._getIndicator("seconds")); - if (this.options.hideEmpties) { - if (values.days == 0) { - this.days.hide(); - if (values.hours == 0) { - this.hours.hide(); - if (values.minutes == 0) { - this.minutes.hide(); - if (values.secounds == 0) - this.seconds.hide(); - } - } - } - } - if (this.options.precision) { - const units = ['days', 'hours', 'minutes', 'seconds']; - for (let u = 0; u < 4; ++u) { - if (values[units[u]]) { - for (let n = u + this.options.precision; n < 4; n++) { - this[units[n]].hide(); - } - break; - } - else { - this[units[u]].hide(); - } - } - } - return distance; - } - _getIndicator(_v) { - return this.options.format == 's' ? egw.lang(_v).substr(0, 1) : egw.lang(_v); - } -} -et2_countdown._attributes = { - format: { - name: "display format", - type: "string", - default: "s", - description: "Defines display format; s (Initial letter) or l (Complete word) display, default is s." - }, - onFinish: { - name: "on finish countdown", - type: "js", - default: et2_no_init, - description: "Callback function to call when the countdown is finished." - }, - hideEmpties: { - name: "hide empties", - type: "string", - default: true, - description: "Only displays none empty values." - }, - precision: { - name: "how many counters to show", - type: "integer", - default: 0, - description: "Limit number of counters, eg. 2 does not show minutes and seconds, if days are displayed" - }, - alarm: { - name: "alarm", - type: "any", - default: "", - description: "Defines an alarm set before the countdown is finished, it should be in seconds" - }, - onAlarm: { - name: "alarm callback", - type: "js", - default: "", - description: "Defines a callback to gets called at alarm - timer. This only will work if there's an alarm set." - } -}; -et2_register_widget(et2_countdown, ["countdown"]); -//# sourceMappingURL=et2_widget_countdown.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_date.js b/api/js/etemplate/et2_widget_date.js deleted file mode 100644 index f4e00ef590..0000000000 --- a/api/js/etemplate/et2_widget_date.js +++ /dev/null @@ -1,1487 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Date object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2011 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - lib/date; - et2_core_inputWidget; - et2_core_valueWidget; -*/ -import "../../../vendor/bower-asset/jquery-ui/jquery-ui.js"; -import { et2_csvSplit, et2_no_init } from "./et2_core_common"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_createWidget, et2_register_widget, et2_widget } from "./et2_core_widget"; -import { et2_valueWidget } from './et2_core_valueWidget'; -import { et2_inputWidget } from './et2_core_inputWidget'; -import { et2_DOMWidget } from "./et2_core_DOMWidget"; -import { egw } from "../jsapi/egw_global"; -import { date } from "./lib/date.js"; -import { egwIsMobile } from "../egw_action/egw_action_common.js"; -// all calls to jQueryUI.datetimepicker as jQuery.datepicker give errors which are currently suppressed with @ts-ignore -// adding npm package @types/jquery.ui.datetimepicker did NOT help :( -/** - * Class which implements the "date" XET-Tag - * - * Dates are passed to the server in ISO8601 format ("Y-m-d\TH:i:sP"), and data_format is - * handled server-side. - * - * Widgets uses jQuery date- and time-picker for desktop browsers and - * HTML5 input fields for mobile devices to get their native UI for date/time entry. - */ -export class et2_date extends et2_inputWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date._attributes, _child || {})); - this.input_date = null; - this.is_mobile = false; - this.date = new Date(); - this.date.setUTCHours(0); - this.date.setMinutes(0); - this.date.setSeconds(0); - this.createInputWidget(); - } - createInputWidget() { - this.span = jQuery(document.createElement(this.options.inline ? 'div' : "span")).addClass("et2_date"); - this.input_date = jQuery(document.createElement(this.options.inline ? "div" : "input")); - if (this.options.blur) - this.input_date.attr('placeholder', this.egw().lang(this.options.blur)); - this.input_date.addClass("et2_date").attr("type", "text") - .attr("size", 7) // strlen("10:00pm")=7 - .appendTo(this.span); - this.setDOMNode(this.span[0]); - // inline calendar is not existing in html5, so allways use datepicker instead - this.is_mobile = egwIsMobile() && !this.options.inline; - if (this.is_mobile) { - this.dateFormat = 'yy-mm-dd'; - this.timeFormat = 'HH:mm'; - switch (this.getType()) { - case 'date': - this.input_date.attr('type', 'date'); - break; - case 'date-time': - this.input_date.attr('type', 'datetime-local'); - break; - case 'date-timeonly': - this.input_date.addClass("et2_time"); - this.input_date.attr('type', 'time'); - break; - } - } - else { - this.dateFormat = this.egw().dateTimeFormat(this.egw().preference("dateformat")); - this.timeFormat = this.egw().preference("timeformat") == 12 ? "h:mmtt" : "HH:mm"; - // jQuery-UI date picker - if (this.getType() != 'date-timeonly') { - this.egw().calendar(this.input_date, this.getType() == "date-time"); - } - else { - this.input_date.addClass("et2_time"); - this.egw().time(this.input_date); - } - // Avoid collision of datepicker dialog with input field - var widget = this; - this.input_date.datepicker('option', 'beforeShow', function (input, inst) { - var cal = inst.dpDiv; - setTimeout(function () { - var $input = jQuery(input); - var inputOffset = $input.offset(); - // position the datepicker in freespace zone - // avoid datepicker calendar collision with input field - if (cal.height() + inputOffset.top > window.innerHeight) { - cal.position({ - my: "left center", - at: 'right bottom', - collision: 'flip fit', - of: input - }); - } - // Add tooltip to Today/Now button - jQuery('[data-handler="today"]', cal).attr('title', widget.getType() == 'date' ? egw.lang('Today') : egw.lang('Now')); - }, 0); - }) - .datepicker('option', 'onClose', function (dateText, inst) { - // Lose focus, avoids an issue with focus - // not allowing datepicker to re-open - inst.input.blur(); - }); - } - // Update internal value when changed - var self = this; - this.input_date.bind('change', function (e) { - self.set_value(this.value); - return false; - }); - // Framewok skips nulls, but null needs to be processed here - if (this.options.value == null) { - this.set_value(null); - } - } - /** - * Calendar popup sets the ID of the input, we can't change that like other inputWidgets can - * - * @param _value - */ - set_id(_value) { - this.id = _value; - this.dom_id = _value && this.getInstanceManager() ? this.getInstanceManager().uniqueId + '_' + this.id : _value; - var node = this.getDOMNode(this); - if (node) { - if (_value != "") { - node.setAttribute("id", this.dom_id); - } - else { - node.removeAttribute("id"); - } - } - } - getInputNode() { - return this.options.inline ? super.getInputNode() : this.input_date[0]; - } - set_type(_type) { - if (_type != this.getType()) { - super.setType(_type); - this.createInputWidget(); - } - } - /** - * Dynamic disable or enable datepicker - * - * @param {boolean} _ro - */ - set_readonly(_ro) { - if (this.input_date && !this.input_date.attr('disabled') != !_ro) { - this.input_date.prop('disabled', !!_ro) - .datepicker('option', 'disabled', !!_ro); - } - } - /** - * Set (full) year of current date - * - * @param {number} _value 4-digit year - */ - set_year(_value) { - this.date.setUTCFullYear(_value); - this.set_value(this.date); - } - /** - * Set month (1..12) of current date - * - * @param {number} _value 1..12 - */ - set_month(_value) { - this.date.setUTCMonth(_value - 1); - this.set_value(this.date); - } - /** - * Set day of current date - * - * @param {number} _value 1..31 - */ - set_date(_value) { - this.date.setUTCDate(_value); - this.set_value(this.date); - } - /** - * Set hour (0..23) of current date - * - * @param {number} _value 0..23 - */ - set_hours(_value) { - this.date.setUTCHours(_value); - this.set_value(this.date); - } - /** - * Set minute (0..59) of current date - * - * @param {number} _value 0..59 - */ - set_minutes(_value) { - this.date.setUTCMinutes(_value); - this.set_value(this.date); - } - /** - * Get (full) year of current date - * - * @return {number|null} 4-digit year or null for empty - */ - get_year() { - return this.input_date.val() == "" ? null : this.date.getUTCFullYear(); - } - /** - * Get month (1..12) of current date - * - * @return {number|null} 1..12 or null for empty - */ - get_month() { - return this.input_date.val() == "" ? null : this.date.getUTCMonth() + 1; - } - /** - * Get day of current date - * - * @return {number|null} 1..31 or null for empty - */ - get_date() { - return this.input_date.val() == "" ? null : this.date.getUTCDate(); - } - /** - * Get hour (0..23) of current date - * - * @return {number|null} 0..23 or null for empty - */ - get_hours() { - return this.input_date.val() == "" ? null : this.date.getUTCHours(); - } - /** - * Get minute (0..59) of current date - * - * @return {number|null} 0..59 or null for empty - */ - get_minutes() { - return this.input_date.val() == "" ? null : this.date.getUTCMinutes(); - } - /** - * Get timestamp - * - * You can use set_value to set a timestamp. - * - * @return {number|null} timestamp (seconds since 1970-01-01) - */ - get_time() { - return this.input_date.val() == "" ? null : this.date.getTime(); - } - /** - * The range of years displayed in the year drop-down: either relative - * to today's year ("-nn:+nn"), relative to the currently selected year - * ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats - * ("nnnn:-nn"). Note that this option only affects what appears in the - * drop-down, to restrict which dates may be selected use the min_date - * and/or max_date options. - * @param {string} _value - */ - set_year_range(_value) { - if (this.input_date && this.getType() == 'date' && !this.is_mobile) { - this.input_date.datepicker('option', 'yearRange', _value); - } - this.options.year_range = _value; - } - /** - * Set the minimum allowed date - * - * The minimum selectable date. When set to null, there is no minimum. - * Multiple types supported: - * Date: A date object containing the minimum date. - * Number: A number of days from today. For example 2 represents two days - * from today and -1 represents yesterday. - * String: A string in the format defined by the dateFormat option, or a - * relative date. Relative dates must contain value and period pairs; - * valid periods are "y" for years, "m" for months, "w" for weeks, and - * "d" for days. For example, "+1m +7d" represents one month and seven - * days from today. - * @param {Date|Number|String} _value - */ - set_min(_value) { - if (this.input_date) { - if (this.is_mobile) { - this.input_date.attr('min', this._relativeDate(_value)); - } - else { - // Check for full timestamp - if (typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) { - _value = new Date(_value); - // Add timezone offset back in, or formatDate will lose those hours - var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); - if (this.getType() == 'date') { - _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); - } - } - this.input_date.datepicker('option', 'minDate', _value); - } - } - this.options.min = _value; - } - /** - * Convert non html5 min or max attributes described above to timestamps - * - * @param {string|Date} _value - */ - _relativeDate(_value) { - if (typeof _value == 'string' && _value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)) - return _value; - // @ts-ignore - return jQuery.datepicker._determineDate(jQuery.datepicker, _value, this.date).toJSON(); - } - /** - * Set the maximum allowed date - * - * The maximum selectable date. When set to null, there is no maximum. - * Multiple types supported: - * Date: A date object containing the maximum date. - * Number: A number of days from today. For example 2 represents two days - * from today and -1 represents yesterday. - * String: A string in the format defined by the dateFormat option, or a - * relative date. Relative dates must contain value and period pairs; - * valid periods are "y" for years, "m" for months, "w" for weeks, and - * "d" for days. For example, "+1m +7d" represents one month and seven - * days from today. - * @param {Date|Number|String} _value - */ - set_max(_value) { - if (this.input_date) { - if (this.is_mobile) { - this.input_date.attr('max', this._relativeDate(_value)); - } - else { - // Check for full timestamp - if (typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) { - _value = new Date(_value); - // Add timezone offset back in, or formatDate will lose those hours - var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); - if (this.getType() == 'date') { - _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); - } - } - this.input_date.datepicker('option', 'maxDate', _value); - } - } - this.options.max = _value; - } - /** - * Setting date - * - * @param {string|number|Date} _value supported are the following formats: - * - Date object with usertime as UTC value - * - string like Date.toJSON() - * - string or number with timestamp in usertime like server-side uses it - * - string starting with + or - to add/substract given number of seconds from current value, "+600" to add 10 minutes - */ - set_value(_value) { - var old_value = this._oldValue; - if (_value === null || _value === "" || _value === undefined || - // allow 0 as empty-value for date and date-time widgets, as that is used a lot eg. in InfoLog - _value == 0 && (this.getType() == 'date-time' || this.getType() == 'date')) { - if (this.input_date) { - this.input_date.val(""); - } - if (this._oldValue !== et2_no_init && old_value !== _value) { - this.change(this.input_date); - } - this._oldValue = _value; - return; - } - // timestamp in usertime, convert to 'Y-m-d\\TH:i:s\\Z', as we do on server-side with equivalent of PHP date() - if (typeof _value == 'number' || typeof _value == 'string' && !isNaN(_value) && _value[0] != '+' && _value[0] != '-') { - _value = date('Y-m-d\\TH:i:s\\Z', _value); - } - // Check for full timestamp - if (typeof _value == 'string' && _value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2})|)$/)) { - _value = new Date(_value); - } - // Handle just time as a string in the form H:i - if (typeof _value == 'string' && isNaN(_value)) { - try { - // silently fix skiped minutes or times with just one digit, as parser is quite pedantic ;-) - var fix_reg = new RegExp((this.getType() == "date-timeonly" ? '^' : ' ') + '([0-9]+)(:[0-9]*)?( ?(a|p)m?)?$', 'i'); - var matches = _value.match(fix_reg); - if (matches && (matches[1].length < 2 || matches[2] === undefined || matches[2].length < 3 || - matches[3] && matches[3] != 'am' && matches[3] != 'pm')) { - if (matches[1].length < 2 && !matches[3]) - matches[1] = '0' + matches[1]; - if (matches[2] === undefined) - matches[2] = ':00'; - while (matches[2].length < 3) - matches[2] = ':0' + matches[2].substr(1); - _value = _value.replace(fix_reg, (this.getType() == "date-timeonly" ? '' : ' ') + matches[1] + matches[2] + matches[3]); - if (matches[4] !== undefined) - matches[3] = matches[4].toLowerCase() == 'a' ? 'am' : 'pm'; - } - switch (this.getType()) { - case "date-timeonly": - // @ts-ignore - var parsed = jQuery.datepicker.parseTime(this.timeFormat, _value); - if (!parsed) // parseTime returns false - { - this.set_validation_error(this.egw().lang("'%1' has an invalid format !!!", _value)); - return; - } - this.set_validation_error(false); - // this.date is on current date, changing it in get_value() to 1970-01-01, gives a time-difference, if we are currently on DST - this.date.setDate(1); - this.date.setMonth(0); - this.date.setFullYear(1970); - // Avoid javascript timezone offset, hour is in 'user time' - this.date.setUTCHours(parsed.hour); - this.date.setMinutes(parsed.minute); - if (this.input_date.val() != _value) { - this.input_date.val(_value); - // @ts-ignore - this.input_date.timepicker('setTime', _value); - if (this._oldValue !== et2_no_init) { - this.change(this.input_date); - } - } - this._oldValue = this.date.toJSON(); - return; - default: - // Parse customfields's date with storage data_format to date object - // Or generally any date widgets with fixed date/time format - if (this.id.match(/^#/g) && this.options.value == _value || (this.options.data_format && this.options.value == _value)) { - switch (this.getType()) { - case 'date': - var parsed = jQuery.datepicker.parseDate(this.egw().dateTimeFormat(this.options.data_format), _value); - break; - case 'date-time': - var DTformat = this.options.data_format.split(' '); - // @ts-ignore - var parsed = jQuery.datepicker.parseDateTime(this.egw().dateTimeFormat(DTformat[0]), this.egw().dateTimeFormat(DTformat[1]), _value); - } - } - else // Parse other date widgets date with timepicker date/time format to date onject - { - // @ts-ignore - var parsed = jQuery.datepicker.parseDateTime(this.dateFormat, this.timeFormat, _value.replace('T', ' ')); - if (!parsed) { - this.set_validation_error(this.egw().lang("%1' han an invalid format !!!", _value)); - return; - } - } - // Update local variable, but remove the timezone offset that - // javascript adds when we parse - if (parsed) { - this.date = new Date(parsed.valueOf() - parsed.getTimezoneOffset() * 60000); - } - this.set_validation_error(false); - } - } - // catch exception from unparsable date and display it empty instead - catch (e) { - return this.set_value(null); - } - } - else if (typeof _value == 'object' && _value.date) { - this.date = _value.date; - } - else if (typeof _value == 'object' && _value.valueOf) { - this.date = _value; - } - else - // string starting with + or - --> add/substract number of seconds from current value - { - this.date.setTime(this.date.getTime() + 1000 * parseInt(_value)); - } - // Update input - popups do, but framework doesn't - _value = ''; - // Add timezone offset back in, or formatDate will lose those hours - var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); - if (this.getType() != 'date-timeonly') { - _value = jQuery.datepicker.formatDate(this.dateFormat, formatDate); - } - if (this.getType() != 'date') { - if (this.getType() != 'date-timeonly') - _value += this.is_mobile ? 'T' : ' '; - // @ts-ignore - _value += jQuery.datepicker.formatTime(this.timeFormat, { - hour: formatDate.getHours(), - minute: formatDate.getMinutes(), - seconds: 0, - timezone: 0 - }); - } - if (this.options.inline) { - this.input_date.datepicker("setDate", formatDate); - } - else { - this.input_date.val(_value); - } - if (this._oldValue !== et2_no_init && old_value != this.getValue()) { - this.change(this.input_date); - } - this._oldValue = _value; - } - getValue() { - if (this.input_date.val() == "") { - // User blanked the box - return null; - } - // date-timeonly returns just the seconds, without any date! - if (this.getType() == 'date-timeonly') { - this.date.setUTCDate(1); - this.date.setUTCMonth(0); - this.date.setUTCFullYear(1970); - } - else if (this.getType() == 'date') { - this.date.setUTCHours(0); - this.date.setUTCMinutes(0); - } - // Convert to timestamp - no seconds - this.date.setSeconds(0, 0); - return (this.date && typeof this.date.toJSON != 'undefined' && this.date.toJSON()) ? this.date.toJSON().replace(/\.\d{3}Z$/, 'Z') : this.date; - } -} -et2_date._attributes = { - "value": { - "type": "any" - }, - "type": { - "ignore": false - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - "data_format": { - "ignore": true, - "description": "Date/Time format. Can be set as an options to date widget", - "default": '' - }, - year_range: { - name: "Year range", - type: "string", - default: "c-10:c+10", - description: "The range of years displayed in the year drop-down: either relative to today's year (\"-nn:+nn\"), relative to the currently selected year (\"c-nn:c+nn\"), absolute (\"nnnn:nnnn\"), or combinations of these formats (\"nnnn:-nn\"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the min and/or max options." - }, - min: { - "name": "Minimum", - "type": "any", - "default": et2_no_init, - "description": 'Minimum allowed date. Multiple types supported:\ -Date: A date object containing the minimum date.\ -Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\ -String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.' - }, - max: { - "name": "Maximum", - "type": "any", - "default": et2_no_init, - "description": 'Maximum allowed date. Multiple types supported:\ -Date: A date object containing the maximum date.\ -Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\ -String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.' - }, - inline: { - "name": "Inline", - "type": "boolean", - "default": false, - "description": "Instead of an input field with a popup calendar, the calendar is displayed inline, with no input field" - } -}; -et2_date.legacyOptions = ["data_format"]; -et2_register_widget(et2_date, ["date", "date-time", "date-timeonly"]); -/** - * Class which implements the "date-duration" XET-Tag - */ -export class et2_date_duration extends et2_date { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date_duration._attributes, _child || {})); - this.format = null; - // Legacy option put percent in with display format - if (this.options.display_format.indexOf("%") != -1) { - this.options.percent_allowed = true; - this.options.display_format = this.options.display_format.replace("%", ""); - } - // Clean formats - this.options.display_format = this.options.display_format.replace(/[^dhms]/, ''); - if (!this.options.display_format) { - // @ts-ignore - this.options.display_format = this.attributes.display_format["default"]; - } - // Get translations - this.time_formats = { - d: this.options.short_labels ? this.egw().lang("d") : this.egw().lang("Days"), - h: this.options.short_labels ? this.egw().lang("h") : this.egw().lang("Hours"), - m: this.options.short_labels ? this.egw().lang("m") : this.egw().lang("Minutes"), - s: this.options.short_labels ? this.egw().lang("s") : this.egw().lang("Seconds") - }, - this.createInputWidget(); - } - createInputWidget() { - // Create nodes - this.node = jQuery(document.createElement("span")) - .addClass('et2_date_duration'); - let inputs = []; - for (let i = this.options.select_unit ? 1 : this.options.display_format.length; i > 0; --i) { - let input = document.createElement("input"); - inputs.push(input); - if (!this.options.select_unit) { - let attr = { min: 0 }; - switch (this.options.display_format[this.options.display_format.length - i]) { - case 's': - attr.max = 60; - attr.title = this.egw().lang('Seconds'); - break; - case 'm': - attr.max = 60; - attr.title = this.egw().lang('Minutes'); - break; - case 'h': - attr.max = 24; - attr.title = this.egw().lang('Hours'); - break; - case 'd': - attr.title = this.egw().lang('Days'); - break; - } - jQuery(input).attr(attr); - } - } - this.duration = jQuery(inputs) - .addClass('et2_date_duration') - .attr({ - type: 'number', - size: 3, - step: this.options.step, - lang: this.egw().preference('number_format')[0] === "," ? "en-150" : "en-001" - }); - this.node.append(this.duration); - var self = this; - // seems the 'invalid' event doesn't work in all browsers, eg. FF therefore - // we use focusout event to check the valifdity of input right after user - // enters the value. - this.duration.on('focusout', function () { - if (!self.duration[0].checkValidity()) - return self.duration.change(); - }); - } - /** - * Clientside validation - * - * @param {array} _messages - */ - isValid(_messages) { - var ok = true; - // if we have a html5 validation error, show it, as this.input.val() will be empty! - for (let i = 0; this.duration && i < this.duration.length; ++i) { - if (this.duration[i] && - this.duration[i].validationMessage && - !this.duration[i].validity.stepMismatch) { - _messages.push(this.duration[i].validationMessage); - ok = false; - } - } - return super.isValid(_messages) && ok; - } - attachToDOM() { - if (this.duration) { - for (let i = 0; i < this.duration.length; ++i) { - let node = this.duration[i]; - jQuery(node).bind("change.et2_inputWidget", this, function (e) { - e.data.change(this); - }); - } - } - return et2_DOMWidget.prototype.attachToDOM.apply(this, arguments); - } - getDOMNode() { - return this.node[0]; - } - getInputNode() { - return this.duration[0]; - } - /** - * Use id on node, same as DOMWidget - * - * @param {string} _value id to set - */ - set_id(_value) { - this.id = _value; - var node = this.getDOMNode(); - if (node) { - if (_value != "") { - node.setAttribute("id", this.getInstanceManager().uniqueId + '_' + this.id); - } - else { - node.removeAttribute("id"); - } - } - } - _unit2seconds(_unit) { - switch (_unit) { - case 's': - return 1; - case 'm': - return 60; - case 'h': - return 3600; - case 'd': - return 3600 * this.options.hours_per_day; - } - } - _unit_from_value(_value, _unit, _highest) { - _value *= this._unit2seconds(this.data_format); - // get value for given _unit - switch (_unit) { - case 's': - return _highest ? _value : _value % 60; - case 'm': - _value = Math.floor(_value / 60); - return _highest ? _value : _value % 60; - case 'h': - _value = Math.floor(_value / 3600); - return _highest ? _value : _value % this.options.hours_per_day; - case 'd': - return Math.floor(_value / 3600 * this.options.hours_per_day); - } - } - set_value(_value) { - this.options.value = _value; - if (!this.options.select_unit && this.options.display_format.length > 1) { - for (let i = this.options.display_format.length; --i >= 0;) { - jQuery(this.duration[i]).val(this._unit_from_value(_value, this.options.display_format[i], !i)); - } - return; - } - var display = this._convert_to_display(parseFloat(_value)); - // Set display - if (this.duration[0].nodeName == "INPUT") { - this.duration.val(display.value); - } - else { - this.duration.text(display.value + " "); - } - // Set unit as figured for display - if (display.unit && display.unit != this.options.display_format) { - if (this.format && this.format.children().length > 1) { - jQuery("option[value='" + display.unit + "']", this.format).attr('selected', 'selected'); - } - else { - this.format.text(display.unit ? this.time_formats[display.unit] : ''); - } - } - } - set_display_format(format) { - if (format.length < 1) { - this.node.remove('select.et2_date_duration'); - this.format.remove(); - this.format = null; - } - this.options.display_format = format; - if ((this.format == null || this.format.is('select')) && - (this.options.display_format.length <= 1 || this.options.readonly || !this.options.select_unit)) { - if (this.format) { - this.format.remove(); - } - this.format = jQuery(document.createElement('span')).appendTo(this.node); - } - if (!this.options.select_unit && this.options.display_format.length > 1) { - // no unit selection or display - this.format.hide(); - } - else if (this.options.display_format.length > 1 && !this.options.readonly) { - if (this.format && !this.format.is('select')) { - this.format.remove(); - this.format = null; - } - if (!this.format) { - this.format = jQuery(document.createElement("select")) - .addClass('et2_date_duration'); - this.node.append(this.format); - } - this.format.empty(); - for (var i = 0; i < this.options.display_format.length; i++) { - this.format.append(""); - } - } - else if (this.time_formats[this.options.display_format]) { - this.format.text(this.time_formats[this.options.display_format]); - } - else { - this.format.text(this.time_formats["m"]); - } - } - /** - * Converts the value in data format into value in display format. - * - * @param _value int/float Data in data format - * - * @return Object {value: Value in display format, unit: unit for display} - */ - _convert_to_display(_value) { - if (!this.options.select_unit) { - let vals = []; - for (let i = 0; i < this.options.display_format.length; ++i) { - let unit = this.options.display_format[i]; - let val = this._unit_from_value(_value, unit, i === 0); - if (unit === 's' || unit === 'm' || unit === 'h' && this.options.display_format[0] === 'd') { - vals.push(sprintf('%02d', val)); - } - else { - vals.push(val); - } - } - return { value: vals.join(':'), unit: '' }; - } - if (_value) { - // Put value into minutes for further processing - switch (this.options.data_format) { - case 'd': - _value *= this.options.hours_per_day; - // fall-through - case 'h': - _value *= 60; - break; - case 's': - _value /= 60.0; - break; - } - } - // Figure out best unit for display - var _unit = this.options.display_format == "d" ? "d" : "h"; - if (this.options.display_format.indexOf('m') > -1 && _value && _value < 60) { - _unit = 'm'; - } - else if (this.options.display_format.indexOf('d') > -1 && _value >= 60 * this.options.hours_per_day) { - _unit = 'd'; - } - _value = this.options.empty_not_0 && _value === '' || !this.options.empty_not_0 && !_value ? '' : - (_unit == 'm' ? parseInt(_value) : (Math.round((_value / 60.0 / (_unit == 'd' ? this.options.hours_per_day : 1)) * 100) / 100)); - if (_value === '') - _unit = ''; - // use decimal separator from user prefs - var format = this.egw().preference('number_format'); - var sep = format ? format[0] : '.'; - if (typeof _value == 'string' && format && sep && sep != '.') { - _value = _value.replace('.', sep); - } - return { value: _value, unit: _unit }; - } - /** - * Change displayed value into storage value and return - */ - getValue() { - if (!this.options.select_unit && this.options.display_format.length > 1) { - let value = 0; - for (let i = this.options.display_format.length; --i >= 0;) { - value += parseInt(jQuery(this.duration[i]).val()) * this._unit2seconds(this.options.display_format[i]); - } - if (this.options.data_format !== 's') { - value /= this._unit2seconds(this.options.data_format); - } - return this.options.data_format === 'm' ? Math.round(value) : value; - } - var value = this.duration.val().replace(',', '.'); - if (value === '') { - return this.options.empty_not_0 ? '' : 0; - } - // Put value into minutes for further processing - switch (this.format && this.format.val() ? this.format.val() : this.options.display_format) { - case 'd': - value *= this.options.hours_per_day; - // fall-through - case 'h': - value *= 60; - break; - } - // Minutes should be an integer. Floating point math. - if (this.options.data_format !== 's') { - value = Math.round(value); - } - switch (this.options.data_format) { - case 'd': - value /= this.options.hours_per_day; - // fall-through - case 'h': - value /= 60.0; - break; - case 's': - value = Math.round(value * 60.0); - break; - } - return value; - } -} -et2_date_duration._attributes = { - "data_format": { - "name": "Data format", - "default": "m", - "type": "string", - "description": "Units to read/store the data. 'd' = days (float), 'h' = hours (float), 'm' = minutes (int), 's' = seconds (int)." - }, - "display_format": { - "name": "Display format", - "default": "dhm", - "type": "string", - "description": "Permitted units for displaying the data. 'd' = days, 'h' = hours, 'm' = minutes, 's' = seconds. Use combinations to give a choice. Default is 'dh' = days or hours with selectbox." - }, - "select_unit": { - "name": "Select unit or input per unit", - "default": true, - "type": "boolean", - "description": "Display a unit-selection for multiple units, or an input field per unit." - }, - "percent_allowed": { - "name": "Percent allowed", - "default": false, - "type": "boolean", - "description": "Allows to enter a percentage." - }, - "hours_per_day": { - "name": "Hours per day", - "default": 8, - "type": "integer", - "description": "Number of hours in a day, for converting between hours and (working) days." - }, - "empty_not_0": { - "name": "0 or empty", - "default": false, - "type": "boolean", - "description": "Should the widget differ between 0 and empty, which get then returned as NULL" - }, - "short_labels": { - "name": "Short labels", - "default": false, - "type": "boolean", - "description": "use d/h/m instead of day/hour/minute" - }, - "step": { - "name": "Step limit", - "default": 'any', - "type": "string", - "description": "Works with the min and max attributes to limit the increments at which a numeric or date-time value can be set." - } -}; -et2_date_duration.legacyOptions = ["data_format", "display_format", "hours_per_day", "empty_not_0", "short_labels"]; -et2_register_widget(et2_date_duration, ["date-duration"]); -/** - * r/o date-duration - */ -export class et2_date_duration_ro extends et2_date_duration { - createInputWidget() { - this.node = jQuery(document.createElement("span")); - this.duration = jQuery(document.createElement("span")).appendTo(this.node); - this.format = jQuery(document.createElement("span")).appendTo(this.node); - } - /** - * Code for implementing et2_IDetachedDOM - * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree - */ - /** - * Build a list of attributes which can be set when working in the - * "detached" mode in the _attrs array which is provided - * by the calling code. - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - _attrs.push("value"); - } - /** - * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - * - * @return {array} - */ - getDetachedNodes() { - return [this.duration[0], this.format[0]]; - } - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which has to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes(_nodes, _values) { - for (var i = 0; i < _nodes.length; i++) { - // Clear the node - for (var j = _nodes[i].childNodes.length - 1; j >= 0; j--) { - _nodes[i].removeChild(_nodes[i].childNodes[j]); - } - } - if (typeof _values.value !== 'undefined') { - _values.value = parseFloat(_values.value); - } - if (_values.value) { - var display = this._convert_to_display(_values.value); - _nodes[0].appendChild(document.createTextNode(display.value)); - _nodes[1].appendChild(document.createTextNode(display.unit)); - } - } -} -et2_register_widget(et2_date_duration_ro, ["date-duration_ro"]); -/** - * et2_date_ro is the readonly implementation of some date widget. - */ -export class et2_date_ro extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date_ro._attributes, _child || {})); - /** - * Internal container for working easily with dates - */ - this.date = new Date(); - this.value = ""; - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.span = jQuery(document.createElement(this.getType() == "date-since" || this.getType() == "date-time_today" ? "span" : "time")) - .addClass("et2_date_ro et2_label") - .appendTo(this._labelContainer); - this.setDOMNode(this._labelContainer[0]); - } - set_value(_value) { - if (typeof _value == 'undefined') - _value = 0; - this.value = _value; - if (_value == 0 || _value == null) { - this.span.attr("datetime", "").text(""); - return; - } - if (typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/)) { - this.date = new Date(_value); - this.date = new Date(this.date.valueOf() + (this.date.getTimezoneOffset() * 60 * 1000)); - } - else if (typeof _value == 'string' && (isNaN(_value) || - this.options.data_format && this.options.data_format.substr(0, 3) === 'Ymd')) { - try { - // data_format is not handled server-side for custom-fields in nextmatch - // as parseDateTime requires a separate between date and time, we fix the value here - switch (this.options.data_format) { - case 'Ymd': - case 'YmdHi': - case 'YmdHis': - _value = _value.substr(0, 4) + '-' + _value.substr(4, 2) + '-' + _value.substr(6, 2) + ' ' + - (_value.substr(8, 2) || '00') + ':' + (_value.substr(10, 2) || '00') + ':' + (_value.substr(12, 2) || '00'); - break; - } - // parseDateTime to handle string PHP: DateTime local date/time format - // @ts-ignore - var parsed = (typeof jQuery.datepicker.parseDateTime("yy-mm-dd", "hh:mm:ss", _value) != 'undefined') ? - // @ts-ignore - jQuery.datepicker.parseDateTime("yy-mm-dd", "hh:mm:ss", _value) : - // @ts-ignore - jQuery.datepicker.parseDateTime(this.egw().preference('dateformat'), this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', _value); - } - // display unparsable dates as empty - catch (e) { - this.span.attr("datetime", "").text(""); - return; - } - var text = new Date(parsed); - // Update local variable, but remove the timezone offset that javascript adds - if (parsed) { - this.date = new Date(text.valueOf() - (text.getTimezoneOffset() * 60 * 1000)); - } - // JS dates use milliseconds - this.date.setTime(text.valueOf()); - } - else { - // _value is timestamp in usertime, ready to be used with date() function identical to PHP date() - this.date = _value; - } - var display = this.date.toString(); - switch (this.getType()) { - case "time_or_date": - case "date-time_today": - // Today - just the time - if (date('Y-m-d', this.date) == date('Y-m-d')) { - display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); - } - else if (this.getType() === "time_or_date") { - display = date(this.egw().preference('dateformat'), this.date); - } - // Before today - date and time - else { - display = date(this.egw().preference('dateformat') + " " + - (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); - } - break; - case "date": - display = date(this.egw().preference('dateformat'), this.date); - break; - case "date-timeonly": - display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date); - break; - case "date-time": - display = date(this.egw().preference('dateformat') + " " + - (this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date); - break; - case "date-since": - var unit2label = { - 'Y': 'years', - 'm': 'month', - 'd': 'days', - 'H': 'hours', - 'i': 'minutes', - 's': 'seconds' - }; - var unit2s = { - 'Y': 31536000, - 'm': 2628000, - 'd': 86400, - 'H': 3600, - 'i': 60, - 's': 1 - }; - var d = new Date(); - var diff = Math.round(d.valueOf() / 1000) - Math.round(this.date.valueOf() / 1000); - display = ''; - for (var unit in unit2s) { - var unit_s = unit2s[unit]; - if (diff >= unit_s || unit == 's') { - display = Math.round(diff / unit_s) + ' ' + this.egw().lang(unit2label[unit]); - break; - } - } - break; - } - this.span.attr("datetime", date("Y-m-d H:i:s", this.date)).text(display); - } - set_label(label) { - // Remove current label - this._labelContainer.contents() - .filter(function () { return this.nodeType == 3; }).remove(); - var parts = et2_csvSplit(label, 2, "%s"); - this._labelContainer.prepend(parts[0]); - this._labelContainer.append(parts[1]); - this.label = label; - // add class if label is empty - this._labelContainer.toggleClass('et2_label_empty', !label || !parts[0]); - } - /** - * Creates a list of attributes which can be set when working in the - * "detached" mode. The result is stored in the _attrs array which is provided - * by the calling code. - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - _attrs.push("label", "value", "class"); - } - /** - * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - * - * @return {array} - */ - getDetachedNodes() { - return [this._labelContainer[0], this.span[0]]; - } - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which have to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes(_nodes, _values) { - this._labelContainer = jQuery(_nodes[0]); - this.span = jQuery(_nodes[1]); - this.set_value(_values["value"]); - if (_values["label"]) { - this.set_label(_values["label"]); - } - if (_values["class"]) { - this.span.addClass(_values["class"]); - } - } -} -/** - * Ignore all more advanced attributes. - */ -et2_date_ro._attributes = { - "value": { - "type": "string" - }, - "type": { - "ignore": false - }, - "data_format": { - "ignore": true, - "description": "Format data is in. This is not used client-side because it's always a timestamp client side." - }, - min: { ignore: true }, - max: { ignore: true }, - year_range: { ignore: true } -}; -et2_register_widget(et2_date_ro, ["date_ro", "date-time_ro", "date-since", "date-time_today", "time_or_date", "date-timeonly_ro"]); -/** - * Widget for selecting a date range - */ -export class et2_date_range extends et2_inputWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_date_range._attributes, _child || {})); - this.div = jQuery(document.createElement('div')) - .attr({ class: 'et2_date_range' }); - this.from = null; - this.to = null; - this.select = null; - // Set domid - this.set_id(this.id); - this.setDOMNode(this.div[0]); - this._createWidget(); - this.set_relative(this.options.relative || false); - } - _createWidget() { - var widget = this; - this.from = et2_createWidget('date', { - id: this.id + '[from]', - blur: egw.lang('From'), - onchange() { widget.to.set_min(widget.from.getValue()); } - }, this); - this.to = et2_createWidget('date', { - id: this.id + '[to]', - blur: egw.lang('To'), - onchange() { widget.from.set_max(widget.to.getValue()); } - }, this); - this.select = et2_createWidget('select', { - id: this.id + '[relative]', - select_options: et2_date_range.relative_dates, - empty_label: this.options.blur || 'All' - }, this); - this.select.loadingFinished(); - } - /** - * Function which allows iterating over the complete widget tree. - * Overridden here to avoid problems with children when getting value - * - * @param _callback is the function which should be called for each widget - * @param _context is the context in which the function should be executed - * @param _type is an optional parameter which specifies a class/interface - * the elements have to be instanceOf. - */ - iterateOver(_callback, _context, _type) { - if (typeof _type == "undefined") { - _type = et2_widget; - } - if (this.isInTree() && this.instanceOf(_type)) { - _callback.call(_context, this); - } - } - /** - * Toggles relative or absolute dates - * - * @param {boolean} _value - */ - set_relative(_value) { - this.options.relative = _value; - if (this.options.relative) { - jQuery(this.from.getDOMNode()).hide(); - jQuery(this.to.getDOMNode()).hide(); - } - else { - jQuery(this.select.getDOMNode()).hide(); - } - } - set_value(value) { - // @ts-ignore - if (!value || typeof value == 'null') { - this.select.set_value(''); - this.from.set_value(null); - this.to.set_value(null); - } - // Relative - if (value && typeof value === 'string') { - this._set_relative_value(value); - } - else if (value && typeof value.from === 'undefined' && value[0]) { - value = { - from: value[0], - to: value[1] || new Date().valueOf() / 1000 - }; - } - else if (value && value.from && value.to) { - this.from.set_value(value.from); - this.to.set_value(value.to); - } - } - getValue() { - return this.options.relative ? - this.select.getValue() : - { from: this.from.getValue(), to: this.to.getValue() }; - } - _set_relative_value(_value) { - if (this.options.relative) { - jQuery(this.select.getDOMNode()).show(); - } - // Show description - this.select.set_value(_value); - var tempDate = new Date(); - var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), 0, -tempDate.getTimezoneOffset(), 0); - // Use strings to avoid references - this.from.set_value(today.toJSON()); - this.to.set_value(today.toJSON()); - var relative = null; - for (var index in et2_date_range.relative_dates) { - if (et2_date_range.relative_dates[index].value === _value) { - relative = et2_date_range.relative_dates[index]; - break; - } - } - if (relative) { - var dates = ["from", "to"]; - var value = today.toJSON(); - for (var i = 0; i < dates.length; i++) { - var date = dates[i]; - if (typeof relative[date] == "function") { - value = relative[date](new Date(value)); - } - else { - value = this[date]._relativeDate(relative[date]); - } - this[date].set_value(value); - } - } - } -} -et2_date_range._attributes = { - value: { - "type": "any", - "description": "An object with keys 'from' and 'to' for absolute ranges, or a relative range string" - }, - relative: { - name: 'Relative', - type: 'boolean', - description: 'Is the date range relative (this week) or absolute (2016-02-15 - 2016-02-21). This will affect the value returned.' - } -}; -// Class Constants -et2_date_range.relative_dates = [ - // Start and end are relative offsets, see et2_date.set_min() - // or Date objects - { - value: 'Today', - label: egw.lang('Today'), - from(date) { return date; }, - to(date) { return date; } - }, - { - label: egw.lang('Yesterday'), - value: 'Yesterday', - from(date) { - date.setUTCDate(date.getUTCDate() - 1); - return date; - }, - to: '' - }, - { - label: egw.lang('This week'), - value: 'This week', - from(date) { return egw.week_start(date); }, - to(date) { - date.setUTCDate(date.getUTCDate() + 6); - return date; - } - }, - { - label: egw.lang('Last week'), - value: 'Last week', - from(date) { - var d = egw.week_start(date); - d.setUTCDate(d.getUTCDate() - 7); - return d; - }, - to(date) { - date.setUTCDate(date.getUTCDate() + 6); - return date; - } - }, - { - label: egw.lang('This month'), - value: 'This month', - from(date) { - date.setUTCDate(1); - return date; - }, - to(date) { - date.setUTCMonth(date.getUTCMonth() + 1); - date.setUTCDate(0); - return date; - } - }, - { - label: egw.lang('Last month'), - value: 'Last month', - from(date) { - date.setUTCMonth(date.getUTCMonth() - 1); - date.setUTCDate(1); - return date; - }, - to(date) { - date.setUTCMonth(date.getUTCMonth() + 1); - date.setUTCDate(0); - return date; - } - }, - { - label: egw.lang('Last 3 months'), - value: 'Last 3 months', - from(date) { - date.setUTCMonth(date.getUTCMonth() - 2); - date.setUTCDate(1); - return date; - }, - to(date) { - date.setUTCMonth(date.getUTCMonth() + 3); - date.setUTCDate(0); - return date; - } - }, - /* - 'This quarter'=> array(0,0,0,0, 0,0,0,0), // Just a marker, needs special handling - 'Last quarter'=> array(0,-4,0,0, 0,-4,0,0), // Just a marker - */ - { - label: egw.lang('This year'), - value: 'This year', - from(d) { - d.setUTCMonth(0); - d.setUTCDate(1); - return d; - }, - to(d) { - d.setUTCMonth(11); - d.setUTCDate(31); - return d; - } - }, - { - label: egw.lang('Last year'), - value: 'Last year', - from(d) { - d.setUTCMonth(0); - d.setUTCDate(1); - d.setUTCYear(d.getUTCYear() - 1); - return d; - }, - to(d) { - d.setUTCMonth(11); - d.setUTCDate(31); - d.setUTCYear(d.getUTCYear() - 1); - return d; - } - } - /* Still needed? - '2 years ago' => array(-2,0,0,0, -1,0,0,0), - '3 years ago' => array(-3,0,0,0, -2,0,0,0), - */ -]; -et2_register_widget(et2_date_range, ["date-range"]); -//# sourceMappingURL=et2_widget_date.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_description.js b/api/js/etemplate/et2_widget_description.js deleted file mode 100644 index e80d4dc6d0..0000000000 --- a/api/js/etemplate/et2_widget_description.js +++ /dev/null @@ -1,353 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Description object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -var _a; -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; - expose; -*/ -import { et2_activateLinks, et2_csvSplit, et2_insertLinkText, et2_no_init } from "./et2_core_common"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_register_widget } from "./et2_core_widget"; -import { et2_baseWidget } from './et2_core_baseWidget'; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { expose } from "./expose"; -import { egw } from "../jsapi/egw_global"; -/** - * Class which implements the "description" XET-Tag - */ -export class et2_description extends expose((_a = class et2_description extends et2_baseWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_description._attributes, _child || {})); - this._labelContainer = null; - // Create the span/label tag which contains the label text - this.span = jQuery(document.createElement(this.options["for"] ? "label" : "span")) - .addClass("et2_label"); - et2_insertLinkText(this._parseText(this.options.value), this.span[0], this.options.href ? this.options.extra_link_target : '_blank'); - this.setDOMNode(this.span[0]); - } - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - if (this.id) { - var val = this.getArrayMgr("content").getEntry(this.id); - if (val) { - _attrs["value"] = val; - } - } - } - doLoadingFinished() { - super.doLoadingFinished(); - // Get the real id of the 'for' widget - var for_widget = null; - let for_id = ""; - if (this.options["for"] && ((for_widget = this.getParent().getWidgetById(this.options.for)) || - (for_widget = this.getRoot().getWidgetById(this.options.for))) && for_widget && for_widget.id) { - if (for_widget.dom_id) { - for_id = for_widget.dom_id; - if (for_widget.instanceOf(et2_inputWidget) && for_widget.getInputNode() && for_widget.dom_id !== for_widget.getInputNode().id) { - for_id = for_widget.getInputNode().id; - } - this.span.attr("for", for_id); - } - else { - // Target widget is not done yet, need to wait - var tab_deferred = jQuery.Deferred(); - window.setTimeout(function () { - var _a; - for_id = for_widget.dom_id; - if (for_widget.instanceOf(et2_inputWidget) && for_widget.getInputNode() && for_widget.dom_id !== ((_a = for_widget.getInputNode()) === null || _a === void 0 ? void 0 : _a.id)) { - for_id = for_widget.getInputNode().id; - } - this.span.attr("for", for_id); - tab_deferred.resolve(); - }.bind(this), 0); - return tab_deferred.promise(); - } - } - return true; - } - set_label(_value) { - // Abort if ther was no change in the label - if (_value == this.label) { - return; - } - if (_value) { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - // Clear the label container. - this._labelContainer.empty(); - // Create the placeholder element and set it - var ph = document.createElement("span"); - this.getSurroundings().setWidgetPlaceholder(ph); - // Split the label at the "%s" - var parts = et2_csvSplit(_value, 2, "%s"); - // Update the content of the label container - for (var i = 0; i < parts.length; i++) { - if (parts[i]) { - this._labelContainer.append(document.createTextNode(parts[i])); - } - if (i == 0) { - this._labelContainer.append(ph); - } - } - // add class if label is empty - this._labelContainer.toggleClass('et2_label_empty', !_value || !parts[0]); - } - else { - // Delete the labelContainer from the surroundings object - if (this._labelContainer) { - this.getSurroundings().removeDOMNode(this._labelContainer[0]); - } - this._labelContainer = null; - } - // Update the surroundings in order to reflect the change in the label - this.getSurroundings().update(); - // Copy the given value - this.label = _value; - } - /** - * Function to get media content to feed the expose - * @param {type} _value - * @returns {Array|Array.getMedia.mediaContent} - */ - getMedia(_value) { - let base_url = egw.webserverUrl.match(new RegExp(/^\//, 'ig')) ? egw(window).window.location.origin : ''; - let mediaContent = []; - if (_value) { - mediaContent = [{ - title: this.options.label, - href: base_url + _value, - type: this.options.type + "/*", - thumbnail: base_url + _value - }]; - if (_value.match(/\/webdav.php/, 'ig')) - mediaContent[0]["download_href"] = base_url + _value + '?download'; - } - return mediaContent; - } - set_value(_value) { - if (!_value) - _value = ""; - if (!this.options.no_lang) - _value = this.egw().lang(_value); - if (this.options.value && (this.options.value + "").indexOf('%s') != -1) { - _value = this.options.value.replace(/%s/g, _value); - } - et2_insertLinkText(this._parseText(_value), this.span[0], this.options.href ? this.options.extra_link_target : '_blank'); - // Add hover action button (Edit) - if (this.options.hover_action) { - this._build_hover_action(); - } - if (this.options.extra_link_popup || this.options.mime) { - var href = this.options.href; - var mime_data = this.options.mime_data; - var self = this; - var $span = this.options.mime_data ? jQuery(this.span) : jQuery('a', this.span); - $span.click(function (e) { - if (self.options.expose_view && typeof self.options.mime != 'undefined' && self.options.mime.match(self.mime_regexp, 'ig')) { - self._init_blueimp_gallery(e, href); - } - else { - egw(window).open_link(mime_data || href, self.options.extra_link_target, self.options.extra_link_popup, null, null, self.options.mime); - } - e.preventDefault(); - return false; - }); - } - } - _parseText(_value) { - if (this.options.href) { - var href = this.options.href; - if (href.indexOf('/') == -1 && href.split('.').length >= 3 && - !(href.indexOf('mailto:') != -1 || href.indexOf('://') != -1 || href.indexOf('javascript:') != -1)) { - href = "/index.php?menuaction=" + href; - } - if (href.charAt(0) == '/') // link relative to eGW - { - href = egw.link(href); - } - return [{ - "href": href, - "text": _value - }]; - } - else if (this.options.activate_links) { - return et2_activateLinks(_value); - } - else { - return [_value]; - } - } - set_font_style(_value) { - this.font_style = _value; - this.span.toggleClass("et2_bold", _value.indexOf("b") >= 0); - this.span.toggleClass("et2_italic", _value.indexOf("i") >= 0); - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes(_attrs) { - _attrs.push("value", "class", "href"); - } - getDetachedNodes() { - return [this.span[0]]; - } - setDetachedAttributes(_nodes, _values, _data) { - // Update the properties - var updateLink = false; - if (typeof _values["href"] != "undefined") { - updateLink = true; - this.options.href = _values["href"]; - } - if (typeof _values["value"] != "undefined" || (updateLink && (_values["value"] || this.options.value))) { - this.span = jQuery(_nodes[0]); - this.set_value(_values["value"]); - } - if (typeof _values["class"] != "undefined") { - _nodes[0].setAttribute("class", _values["class"]); - } - // Add hover action button (Edit), _data is nm's row data - if (this.options.hover_action) { - this._build_hover_action(_data); - } - } - /** - * Builds button for hover action - * @param {object} _data - */ - _build_hover_action(_data) { - var content = _data && _data.content ? _data.content : undefined; - var widget = this; - this.span.off().on('mouseenter', jQuery.proxy(function (event) { - event.stopImmediatePropagation(); - var self = this; - this.span.tooltip({ - items: 'span.et2_label', - position: { my: "right top", at: "left top", collision: "flipfit" }, - tooltipClass: "et2_email_popup", - content() { - return jQuery('') - .on('click', function () { - widget.options.hover_action.call(self, self.widget, content); - }); - }, - close(event, ui) { - ui.tooltip.hover(function () { - jQuery(this).stop(true).fadeTo(400, 1); - }, function () { - jQuery(this).fadeOut("400", function () { jQuery(this).remove(); }); - }); - } - }) - .tooltip("open"); - }, { widget: this, span: this.span })); - } - }, - _a._attributes = { - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - }, - "value": { - "name": "Value", - "type": "string", - "description": "Displayed text", - "translate": "!no_lang", - "default": "" - }, - /** - * Options converted from the "options"-attribute. - */ - "font_style": { - "name": "Font Style", - "type": "string", - "description": "Style may be a compositum of \"b\" and \"i\" which " + - " renders the text bold and/or italic." - }, - "href": { - "name": "Link URL", - "type": "string", - "description": "Link URL, empty if you don't wan't to display a link." - }, - "activate_links": { - "name": "Replace URLs", - "type": "boolean", - "default": false, - "description": "If set, URLs in the text are automatically replaced " + - "by links" - }, - "for": { - "name": "Label for widget", - "type": "string", - "description": "Marks the text as label for the given widget." - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": "_browser", - "description": "Link target for href attribute" - }, - "extra_link_popup": { - "name": "Popup", - "type": "string", - "description": "widthxheight, if popup should be used, eg. 640x480" - }, - "expose_view": { - name: "Expose view", - type: "boolean", - default: false, - description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." - }, - mime: { - name: "Mime type", - type: "string", - default: '', - description: "Mime type of the registered link" - }, - mime_data: { - name: "Mime data", - type: "string", - default: '', - description: "hash for data stored on service-side with egw_link::(get|set)_data()" - }, - hover_action: { - "name": "hover action", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when clicking on action button. This action is explicitly for attached nodes, like in nm." - }, - hover_action_title: { - "name": "hover action title", - "type": "string", - "default": "Edit", - "description": "Text to show as tooltip of defined action" - } - }, - _a.legacyOptions = ["font_style", "href", "activate_links", "for", - "extra_link_target", "extra_link_popup", "statustext"], - _a)) { -} -; -et2_register_widget(et2_description, ["description", "label"]); -//# sourceMappingURL=et2_widget_description.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_dialog.js b/api/js/etemplate/et2_widget_dialog.js deleted file mode 100644 index bf489507a9..0000000000 --- a/api/js/etemplate/et2_widget_dialog.js +++ /dev/null @@ -1,846 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Dialog Widget class - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2013 - */ -/*egw:uses - et2_core_widget; - /vendor/bower-asset/jquery-ui/jquery-ui.js; -*/ -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_widget } from "./et2_core_widget"; -import { et2_button } from "./et2_widget_button"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { etemplate2 } from "./etemplate2"; -import { egw } from "../jsapi/egw_global"; -import { et2_no_init } from "./et2_core_common"; -/** - * A common dialog widget that makes it easy to imform users or prompt for information. - * - * It is possible to have a custom dialog by using a template, but you can also use - * the static method et2_dialog.show_dialog(). At its simplest, you can just use: - * - * et2_dialog.show_dialog(false, "Operation completed"); - * - * Or a more complete example: - * - * var callback = function (button_id) - * { - * if(button_id == et2_dialog.YES_BUTTON) - * { - * // Do stuff - * } - * else if (button_id == et2_dialog.NO_BUTTON) - * { - * // Other stuff - * } - * else if (button_id == et2_dialog.CANCEL_BUTTON) - * { - * // Abort - * } - * }. - * var dialog = et2_dialog.show_dialog( - * callback, "Erase the entire database?","Break things", {} // value - * et2_dialog.BUTTONS_YES_NO_CANCEL, et2_dialog.WARNING_MESSAGE - * ); - * - * - * - * The parameters for the above are all optional, except callback and message: - * callback - function called when the dialog closes, or false/null. - * The ID of the button will be passed. Button ID will be one of the et2_dialog.*_BUTTON constants. - * The callback is _not_ called if the user closes the dialog with the X in the corner, or presses ESC. - * message - (plain) text to display - * title - Dialog title - * value (for prompt) - * buttons - et2_dialog BUTTONS_* constant, or an array of button settings - * dialog_type - et2_dialog *_MESSAGE constant - * icon - URL of icon - * - * Note that these methods will _not_ block program flow while waiting for user input. - * The user's input will be provided to the callback. - * - * You can also use the standard et2_createWidget() to create a custom dialog using an etemplate, even setting all - * the buttons yourself. - * - * var dialog = et2_createWidget("dialog",{ - * // If you use a template, the second parameter will be the value of the template, as if it were submitted. - * callback: function(button_id, value) {...}, // return false to prevent dialog closing - * buttons: [ - * // These ones will use the callback, just like normal - * {text: egw.lang("OK"),id:"OK", class="ui-priority-primary", default: true}, - * {text: egw.lang("Yes"),id:"Yes"}, - * {text: egw.lang("Sure"),id:"Sure"}, - * {text: egw.lang("Maybe"),click: function() { - * // If you override, 'this' will be the dialog DOMNode. - * // Things get more complicated. - * // Do what you like, but don't forget this line: - * jQuery(this).dialog("close") - * }, class="ui-state-error"}, - * - * ], - * title: 'Why would you want to do this?', - * template:"/egroupware/addressbook/templates/default/edit.xet", - * value: { content: {...default values}, sel_options: {...}...} - * }); - * - * @augments et2_widget - * @see http://api.jqueryui.com/dialog/ - */ -export class et2_dialog extends et2_widget { - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_dialog._attributes, _child || {})); - /** - * Details for dialog type options - */ - this._dialog_types = [ - //PLAIN_MESSAGE: 0 - "", - //INFORMATION_MESSAGE: 1, - "dialog_info", - //QUESTION_MESSAGE: 2, - "dialog_help", - //WARNING_MESSAGE: 3, - "dialog_warning", - //ERROR_MESSAGE: 4, - "dialog_error" - ]; - this._buttons = [ - /* - Pre-defined Button combos - - button ids copied from et2_dialog static, since the constants are not defined yet - - image get replaced by 'style="background-image: url('+egw.image(image)+')' for an image prefixing text - */ - //BUTTONS_OK: 0, - [{ "button_id": 1, "text": 'ok', id: 'dialog[ok]', image: 'check', "default": true }], - //BUTTONS_OK_CANCEL: 1, - [ - { "button_id": 1, "text": 'ok', id: 'dialog[ok]', image: 'check', "default": true }, - { "button_id": 0, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel' } - ], - //BUTTONS_YES_NO: 2, - [ - { "button_id": 2, "text": 'yes', id: 'dialog[yes]', image: 'check', "default": true }, - { "button_id": 3, "text": 'no', id: 'dialog[no]', image: 'cancelled' } - ], - //BUTTONS_YES_NO_CANCEL: 3, - [ - { "button_id": 2, "text": 'yes', id: 'dialog[yes]', image: 'check', "default": true }, - { "button_id": 3, "text": 'no', id: 'dialog[no]', image: 'cancelled' }, - { "button_id": 0, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel' } - ] - ]; - this.div = null; - this.template = null; - // Define this as null to avoid breaking any hierarchies (eg: destroy()) - if (this.getParent() != null) - this.getParent().removeChild(this); - // Button callbacks need a reference to this - let self = this; - for (let i = 0; i < this._buttons.length; i++) { - for (let j = 0; j < this._buttons[i].length; j++) { - this._buttons[i][j].click = (function (id) { - return function (event) { - self.click(event.target, id); - }; - })(this._buttons[i][j].button_id); - // translate button texts, as translations are not available before - this._buttons[i][j].text = egw.lang(this._buttons[i][j].text); - } - } - this.div = jQuery(document.createElement("div")); - this._createDialog(); - } - /** - * Clean up dialog - */ - destroy() { - if (this.div != null) { - // Un-dialog the dialog - this.div.dialog("destroy"); - if (this.template) { - this.template.clear(true); - this.template = null; - } - this.div = null; - } - // Call the inherited constructor - super.destroy(); - } - /** - * Internal callback registered on all standard buttons. - * The provided callback is called after the dialog is closed. - * - * @param target DOMNode The clicked button - * @param button_id integer The ID of the clicked button - */ - click(target, button_id) { - if (this.options.callback) { - if (this.options.callback.call(this, button_id, this.get_value()) === false) - return; - } - // Triggers destroy too - this.div.dialog("close"); - } - /** - * Returns the values of any widgets in the dialog. This does not include - * the buttons, which are only supplied for the callback. - */ - get_value() { - var value = this.options.value; - if (this.template) { - value = this.template.getValues(this.template.widgetContainer); - } - return value; - } - /** - * Set the displayed prompt message - * - * @param {string} message New message for the dialog - */ - set_message(message) { - this.options.message = message; - this.div.empty() - .append("") - .append(jQuery('
').text(message)); - } - /** - * Set the dialog type to a pre-defined type - * - * @param {integer} type constant from et2_dialog - */ - set_dialog_type(type) { - if (this.options.dialog_type != type && typeof this._dialog_types[type] == "string") { - this.options.dialog_type = type; - } - this.set_icon(this._dialog_types[type] ? egw.image(this._dialog_types[type]) : ""); - } - /** - * Set the icon for the dialog - * - * @param {string} icon_url - */ - set_icon(icon_url) { - if (icon_url == "") { - jQuery("img.dialog_icon", this.div).hide(); - } - else { - jQuery("img.dialog_icon", this.div).show().attr("src", icon_url); - } - } - /** - * Set the dialog buttons - * - * Use either the pre-defined options in et2_dialog, or an array - * @see http://api.jqueryui.com/dialog/#option-buttons - * @param {array} buttons - */ - set_buttons(buttons) { - this.options.buttons = buttons; - if (buttons instanceof Array) { - for (var i = 0; i < buttons.length; i++) { - var button = buttons[i]; - if (!button.click) { - button.click = jQuery.proxy(this.click, this, null, button.id); - } - // set a default background image and css class based on buttons id - if (button.id && typeof button.class == 'undefined') { - for (var name in et2_button.default_classes) { - if (button.id.match(et2_button.default_classes[name])) { - button.class = (typeof button.class == 'undefined' ? '' : button.class + ' ') + name; - break; - } - } - } - if (button.id && typeof button.image == 'undefined' && typeof button.style == 'undefined') { - for (var name in et2_button.default_background_images) { - if (button.id.match(et2_button.default_background_images[name])) { - button.image = name; - break; - } - } - } - if (button.image) { - button.style = 'background-image: url(' + - (button.image.match('^http|\/') ? - button.image : this.egw().image(button.image, 'api')) - + ')'; - delete button.image; - } - } - } - // If dialog already created, update buttons - if (this.div.data('ui-dialog')) { - this.div.dialog("option", "buttons", buttons); - // Focus default button so enter works - jQuery('.ui-dialog-buttonpane button[default]', this.div.parent()).focus(); - } - } - /** - * Set the dialog title - * - * @param {string} title New title for the dialog - */ - set_title(title) { - this.options.title = title; - this.div.dialog("option", "title", title); - } - /** - * Block interaction with the page behind the dialog - * - * @param {boolean} modal Block page behind dialog - */ - set_modal(modal) { - this.options.modal = modal; - this.div.dialog("option", "modal", modal); - } - /** - * Load an etemplate into the dialog - * - * @param template String etemplate file name - */ - set_template(template) { - if (this.template && this.options.template != template) { - this.template.clear(); - } - this.template = new etemplate2(this.div[0]); - if (template.indexOf('.xet') > 0) { - // File name provided, fetch from server - this.template.load("", template, this.options.value || { content: {} }, jQuery.proxy(function () { - // Set focus to the first input - jQuery('input', this.div).first().focus(); - }, this)); - } - else { - // Just template name, it better be loaded already - this.template.load(template, '', this.options.value || {}, - // true: do NOT call et2_ready, as it would overwrite this.et2 in app.js - undefined, undefined, true); - } - // Don't let dialog closing destroy the parent session - if (this.template.etemplate_exec_id && this.template.app) { - for (let et of etemplate2.getByApplication(this.template.app)) { - if (et !== this.template && et.etemplate_exec_id === this.template.etemplate_exec_id) { - // Found another template using that exec_id, don't destroy when dialog closes. - this.template.unbind_unload(); - break; - } - } - } - // set template-name as id, to allow to style dialogs - this.div.children().attr('id', template.replace(/^(.*\/)?([^/]+)(\.xet)?$/, '$2').replace(/\./g, '-')); - } - /** - * Actually create and display the dialog - */ - _createDialog() { - if (this.options.template) { - this.set_template(this.options.template); - } - else { - this.set_message(this.options.message); - this.set_dialog_type(this.options.dialog_type); - } - this.set_buttons(typeof this.options.buttons == "number" ? this._buttons[this.options.buttons] : this.options.buttons); - let position_my, position_at = ''; - if (this.options.position) { - let positions = this.options.position.split(','); - position_my = positions[0] ? positions[0].trim() : 'center'; - position_at = positions[1] ? positions[1].trim() : position_my; - } - let options = { - // Pass the internal object, not the option - buttons: this.options.buttons, - modal: this.options.modal, - resizable: this.options.resizable, - minWidth: this.options.minWidth, - minHeight: this.options.minHeight, - maxWidth: 640, - height: this.options.height, - title: this.options.title, - open: function () { - // Focus default button so enter works - jQuery(this).parents('.ui-dialog-buttonpane button[default]').focus(); - window.setTimeout(function () { - jQuery(this).dialog('option', 'position', { - my: position_my, - at: position_at, - of: window - }); - }.bind(this), 0); - }, - close: jQuery.proxy(function () { - this.destroy(); - }, this), - beforeClose: this.options.beforeClose, - closeText: this.egw().lang('close'), - position: { my: "center", at: "center", of: window }, - appendTo: this.options.appendTo, - draggable: this.options.draggable, - closeOnEscape: this.options.closeOnEscape, - dialogClass: this.options.dialogClass, - }; - // Leaving width unset lets it size itself according to contents - if (this.options.width) { - options['width'] = this.options.width; - } - this.div.dialog(options); - // Make sure dialog is wide enough for the title - // Arbitrary numbers that seem to work nicely. - let title_width = 20 + 10 * this.options.title.length; - if (this.div.width() < title_width && this.options.title.trim()) { - // Auto-sizing chopped the title - this.div.dialog('option', 'width', title_width); - } - } - /** - * Create a parent to inject application specific egw object with loaded translations into et2_dialog - * - * @param {string|egw} _egw_or_appname egw object with already loaded translations or application name to load translations for - */ - static _create_parent(_egw_or_appname) { - if (typeof _egw_or_appname == 'undefined') { - // @ts-ignore - _egw_or_appname = egw_appName; - } - // create a dummy parent with a correct reference to an application specific egw object - let parent = new et2_widget(); - // if egw object is passed in because called from et2, just use it - if (typeof _egw_or_appname != 'string') { - parent.setApiInstance(_egw_or_appname); - } - // otherwise use given appname to create app-specific egw instance and load default translations - else { - parent.setApiInstance(egw(_egw_or_appname)); - parent.egw().langRequireApp(parent.egw().window, _egw_or_appname); - } - return parent; - } - /** - * Show a confirmation dialog - * - * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. - * @param {string} _message Message to be place in the dialog. - * @param {string} _title Text in the top bar of the dialog. - * @param _value passed unchanged to callback as 2. parameter - * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box - * @param {integer} _type One of the message constants. This defines the style of the message. - * @param {string} _icon URL of an icon to display. If not provided, a type-specific icon will be used. - * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for - */ - static show_dialog(_callback, _message, _title, _value, _buttons, _type, _icon, _egw_or_appname) { - let parent = et2_dialog._create_parent(_egw_or_appname); - // Just pass them along, widget handles defaults & missing - return et2_createWidget("dialog", { - callback: _callback || function () { - }, - message: _message, - title: _title || parent.egw().lang('Confirmation required'), - buttons: typeof _buttons != 'undefined' ? _buttons : et2_dialog.BUTTONS_YES_NO, - dialog_type: typeof _type != 'undefined' ? _type : et2_dialog.QUESTION_MESSAGE, - icon: _icon, - value: _value, - width: 'auto' - }, parent); - } - ; - /** - * Show an alert message with OK button - * - * @param {string} _message Message to be place in the dialog. - * @param {string} _title Text in the top bar of the dialog. - * @param {integer} _type One of the message constants. This defines the style of the message. - */ - static alert(_message, _title, _type) { - let parent = et2_dialog._create_parent(et2_dialog._create_parent().egw()); - et2_createWidget("dialog", { - callback: function () { - }, - message: _message, - title: _title, - buttons: et2_dialog.BUTTONS_OK, - dialog_type: _type || et2_dialog.INFORMATION_MESSAGE - }, parent); - } - /** - * Show a prompt dialog - * - * @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in. - * @param {string} _message Message to be place in the dialog. - * @param {string} _title Text in the top bar of the dialog. - * @param {string} _value for prompt, passed to callback as 2. parameter - * @param {integer|array} _buttons One of the BUTTONS_ constants defining the set of buttons at the bottom of the box - * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for - */ - static show_prompt(_callback, _message, _title, _value, _buttons, _egw_or_appname) { - var callback = _callback; - // Just pass them along, widget handles defaults & missing - return et2_createWidget("dialog", { - callback: function (_button_id, _value) { - if (typeof callback == "function") { - callback.call(this, _button_id, _value.value); - } - }, - title: _title || egw.lang('Input required'), - buttons: _buttons || et2_dialog.BUTTONS_OK_CANCEL, - value: { - content: { - value: _value, - message: _message - } - }, - template: egw.webserverUrl + '/api/templates/default/prompt.xet', - class: "et2_prompt" - }, et2_dialog._create_parent(_egw_or_appname)); - } - /** - * Method to build a confirmation dialog only with - * YES OR NO buttons and submit content back to server - * - * @param {widget} _senders widget that has been clicked - * @param {String} _dialogMsg message shows in dialog box - * @param {String} _titleMsg message shows as a title of the dialog box - * @param {Bool} _postSubmit true: use postSubmit instead of submit - * - * @description submit the form contents including the button that has been pressed - */ - static confirm(_senders, _dialogMsg, _titleMsg, _postSubmit) { - var senders = _senders; - var buttonId = _senders.id; - var dialogMsg = (typeof _dialogMsg != "undefined") ? _dialogMsg : ''; - var titleMsg = (typeof _titleMsg != "undefined") ? _titleMsg : ''; - var egw = _senders instanceof et2_widget ? _senders.egw() : et2_dialog._create_parent().egw(); - var callbackDialog = function (button_id) { - if (button_id == et2_dialog.YES_BUTTON) { - if (_postSubmit) { - senders.getRoot().getInstanceManager().postSubmit(buttonId); - } - else if (senders.instanceOf(et2_button) && senders.getType() !== "buttononly") { - senders.clicked = true; - senders.getInstanceManager().submit(senders, false, senders.options.novalidate); - senders.clicked = false; - } - else { - senders.getRoot().getInstanceManager().submit(buttonId); - } - } - }; - et2_dialog.show_dialog(callbackDialog, egw.lang(dialogMsg), egw.lang(titleMsg), {}, et2_dialog.BUTTONS_YES_NO, et2_dialog.WARNING_MESSAGE, undefined, egw); - } - ; - /** - * Show a dialog for a long-running, multi-part task - * - * Given a server url and a list of parameters, this will open a dialog with - * a progress bar, asynchronously call the url with each parameter, and update - * the progress bar. - * Any output from the server will be displayed in a box. - * - * When all tasks are done, the callback will be called with boolean true. It will - * also be called if the user clicks a button (OK or CANCEL), so be sure to - * check to avoid executing more than intended. - * - * @param {function} _callback Function called when the user clicks a button, - * or when the list is done processing. The context will be the et2_dialog - * widget, and the button constant is passed in. - * @param {string} _message Message to be place in the dialog. Usually just - * text, but DOM nodes will work too. - * @param {string} _title Text in the top bar of the dialog. - * @param {string} _menuaction the menuaction function which should be called and - * which handles the actual request. If the menuaction is a full featured - * url, this one will be used instead. - * @param {Array[]} _list - List of parameters, one for each call to the - * address. Multiple parameters are allowed, in an array. - * @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for - * - * @return {et2_dialog} - */ - static long_task(_callback, _message, _title, _menuaction, _list, _egw_or_appname) { - let parent = et2_dialog._create_parent(_egw_or_appname); - let egw = parent.egw(); - // Special action for cancel - let buttons = [ - { "button_id": et2_dialog.OK_BUTTON, "text": egw.lang('ok'), "default": true, "disabled": true }, - { - "button_id": et2_dialog.CANCEL_BUTTON, "text": egw.lang('cancel'), click: function () { - // Cancel run - cancel = true; - jQuery("button[button_id=" + et2_dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable"); - update.call(_list.length, ''); - } - } - ]; - let dialog = et2_createWidget("dialog", { - template: egw.webserverUrl + '/api/templates/default/long_task.xet', - value: { - content: { - message: _message - } - }, - callback: function (_button_id, _value) { - if (_button_id == et2_dialog.CANCEL_BUTTON) { - cancel = true; - } - if (typeof _callback == "function") { - _callback.call(this, _button_id, _value.value); - } - }, - title: _title || egw.lang('please wait...'), - buttons: buttons - }, parent); - // OK starts disabled - jQuery("button[button_id=" + et2_dialog.OK_BUTTON + "]", dialog.div.parent()).button("disable"); - let log = null; - let progressbar = null; - let cancel = false; - let totals = { - success: 0, - skipped: 0, - failed: 0, - widget: null - }; - // Updates progressbar & log, calls next step - let update = function (response) { - // context is index - let index = this || 0; - progressbar.set_value(100 * (index / _list.length)); - progressbar.set_label(index + ' / ' + _list.length); - // Display response information - switch (response.type) { - case 'error': - jQuery("
") - .text(response.data) - .appendTo(log); - totals.failed++; - // Ask to retry / ignore / abort - et2_createWidget("dialog", { - callback: function (button) { - switch (button) { - case 'dialog[cancel]': - cancel = true; - return update.call(index, ''); - case 'dialog[skip]': - // Continue with next index - totals.skipped++; - return update.call(index, ''); - default: - // Try again with previous index - return update.call(index - 1, ''); - } - }, - message: response.data, - title: '', - buttons: [ - // These ones will use the callback, just like normal - { text: egw.lang("Abort"), id: 'dialog[cancel]' }, - { text: egw.lang("Retry"), id: 'dialog[retry]' }, - { text: egw.lang("Skip"), id: 'dialog[skip]', class: "ui-priority-primary", default: true } - ], - dialog_type: et2_dialog.ERROR_MESSAGE - }, parent); - // Early exit - return; - default: - if (response && typeof response === "string") { - totals.success++; - jQuery("
") - .text(response) - .appendTo(log); - } - else { - jQuery("
") - .text(JSON.stringify(response)) - .appendTo(log); - } - } - // Scroll to bottom - let height = log[0].scrollHeight; - log.scrollTop(height); - // Update totals - totals.widget.set_value(egw.lang("Total: %1 Successful: %2 Failed: %3 Skipped: %4", _list.length, totals.success, totals.failed, totals.skipped)); - // Fire next step - if (!cancel && index < _list.length) { - var parameters = _list[index]; - if (typeof parameters != 'object') - parameters = [parameters]; - // Async request, we'll take the next step in the callback - // We can't pass index = 0, it looks like false and causes issues - egw.json(_menuaction, parameters, update, index + 1, true, index + 1).sendRequest(); - } - else { - // All done - if (!cancel) - progressbar.set_value(100); - jQuery("button[button_id=" + et2_dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable"); - jQuery("button[button_id=" + et2_dialog.OK_BUTTON + "]", dialog.div.parent()).button("enable"); - if (!cancel && typeof _callback == "function") { - _callback.call(dialog, true, response); - } - } - }; - jQuery(dialog.template.DOMContainer).on('load', function () { - // Get access to template widgets - log = jQuery(dialog.template.widgetContainer.getWidgetById('log').getDOMNode()); - progressbar = dialog.template.widgetContainer.getWidgetById('progressbar'); - progressbar.set_label('0 / ' + _list.length); - totals.widget = dialog.template.widgetContainer.getWidgetById('totals'); - // Start - window.setTimeout(function () { - update.call(0, ''); - }, 0); - }); - return dialog; - } -} -et2_dialog._attributes = { - callback: { - name: "Callback", - type: "js", - description: "Callback function is called with the value when the dialog is closed", - "default": function (button_id) { - egw.debug("log", "Button ID: %d", button_id); - } - }, - beforeClose: { - name: "before close callback", - type: "js", - description: "Callback function before dialog is closed, return false to prevent that", - "default": function () { - } - }, - message: { - name: "Message", - type: "string", - description: "Dialog message (plain text, no html)", - "default": "Somebody forgot to set this..." - }, - dialog_type: { - name: "Dialog type", - type: "integer", - description: "To use a pre-defined dialog style, use et2_dialog.ERROR_MESSAGE, INFORMATION_MESSAGE,WARNING_MESSAGE,QUESTION_MESSAGE,PLAIN_MESSAGE constants. Default is et2_dialog.PLAIN_MESSAGE", - "default": 0 //this.PLAIN_MESSAGE - }, - buttons: { - name: "Buttons", - type: "any", - "default": 0, - description: "Buttons that appear at the bottom of the dialog. You can use the constants et2_dialog.BUTTONS_OK, BUTTONS_YES_NO, BUTTONS_YES_NO_CANCEL, BUTTONS_OK_CANCEL, or pass in an array for full control" - }, - icon: { - name: "Icon", - type: "string", - description: "URL of an icon for the dialog. If omitted, an icon based on dialog_type will be used.", - "default": "" - }, - title: { - name: "Title", - type: "string", - description: "Title for the dialog box (plain text, no html)", - "default": "" - }, - modal: { - name: "Modal", - type: "boolean", - description: "Prevent the user from interacting with the page", - "default": true - }, - resizable: { - name: "Resizable", - type: "boolean", - description: "Allow the user to resize the dialog", - "default": true - }, - value: { - "name": "Value", - "description": "The (default) value of the dialog. Use with template.", - "type": "any", - "default": et2_no_init - }, - template: { - "name": "Template", - "description": "Instead of displaying a simple message, a full template can be loaded instead. Set defaults with value.", - "type": "string", - "default": et2_no_init - }, - minWidth: { - name: "minimum width", - type: "integer", - description: "Define minimum width of dialog", - "default": 0 - }, - minHeight: { - name: "minimum height", - type: "integer", - description: "Define minimum height of dialog", - "default": 0 - }, - width: { - name: "width", - type: "string", - description: "Define width of dialog, the default is auto", - "default": et2_no_init - }, - height: { - name: "height", - type: "string", - description: "Define width of dialog, the default is auto", - "default": 'auto' - }, - position: { - name: "position", - type: "string", - description: "Define position of dialog in the main window", - default: "center" - }, - appendTo: { - name: "appendTo", - type: "string", - description: "Defines the dialog parent context", - default: '' - }, - draggable: { - name: "Draggable", - type: "boolean", - description: "Allow the user to drag the dialog", - default: true - }, - closeOnEscape: { - name: "close on escape", - type: "boolean", - description: "Allow the user to close the dialog by hiting escape", - default: true - }, - dialogClass: { - name: "dialog class", - type: "string", - description: "Add css classed into dialog container", - default: '' - } -}; -/** - * Types - * @constant - */ -et2_dialog.PLAIN_MESSAGE = 0; -et2_dialog.INFORMATION_MESSAGE = 1; -et2_dialog.QUESTION_MESSAGE = 2; -et2_dialog.WARNING_MESSAGE = 3; -et2_dialog.ERROR_MESSAGE = 4; -/* Pre-defined Button combos */ -et2_dialog.BUTTONS_OK = 0; -et2_dialog.BUTTONS_OK_CANCEL = 1; -et2_dialog.BUTTONS_YES_NO = 2; -et2_dialog.BUTTONS_YES_NO_CANCEL = 3; -/* Button constants */ -et2_dialog.CANCEL_BUTTON = 0; -et2_dialog.OK_BUTTON = 1; -et2_dialog.YES_BUTTON = 2; -et2_dialog.NO_BUTTON = 3; -// make et2_dialog publicly available as we need to call it from templates -window['et2_dialog'] = et2_dialog; -et2_register_widget(et2_dialog, ["dialog"]); -//# sourceMappingURL=et2_widget_dialog.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_diff.js b/api/js/etemplate/et2_widget_diff.js deleted file mode 100644 index e5cf3f819b..0000000000 --- a/api/js/etemplate/et2_widget_diff.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Diff object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2012 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - /vendor/bower-asset/diff2html/dist/diff2html.min.js; - et2_core_valueWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -/** - * Class that displays the diff between two [text] values - * - * @augments et2_valueWidget - */ -export class et2_diff extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_diff._attributes, _child || {})); - this.mini = true; - // included via etemplate2.css - //this.egw().includeCSS('../../../vendor/bower-asset/dist/dist2html.css'); - this.div = document.createElement("div"); - jQuery(this.div).addClass('et2_diff'); - } - set_value(value) { - jQuery(this.div).empty(); - if (typeof value == 'string') { - // Diff2Html likes to have files, we don't have them - if (value.indexOf('---') !== 0) { - value = "--- diff\n+++ diff\n" + value; - } - // @ts-ignore - var diff = Diff2Html.getPrettyHtml(value, this.diff_options); - // var ui = new Diff2HtmlUI({diff: diff}); - // ui.draw(jQuery(this.div), this.diff_options); - jQuery(this.div).append(diff); - } - else if (typeof value != 'object') { - jQuery(this.div).append(value); - } - this.check_mini(); - } - check_mini() { - if (!this.mini) { - return false; - } - var view = jQuery(this.div).children(); - this.minify(view); - var self = this; - jQuery(' ') - .appendTo(self.div) - .css("cursor", "pointer") - .click({ diff: view, div: self.div, label: self.options.label }, function (e) { - var diff = e.data.diff; - var div = e.data.div; - self.un_minify(diff); - var dialog_div = jQuery('
') - .append(diff); - dialog_div.dialog({ - title: e.data.label, - width: 'auto', - modal: true, - buttons: [{ text: self.egw().lang('ok'), click: function () { jQuery(this).dialog("close"); } }], - open() { - if (jQuery(this).parent().height() > jQuery(window).height()) { - jQuery(this).height(jQuery(window).height() * 0.7); - } - jQuery(this).addClass('et2_diff').dialog({ position: "center" }); - }, - close(event, ui) { - // Need to destroy the dialog, etemplate widget needs divs back where they were - dialog_div.dialog("destroy"); - self.minify(this); - // Put it back where it came from, or et2 will error when clear() is called - diff.prependTo(div); - } - }); - }); - } - set_label(_label) { - this.options.label = _label; - } - /** - * Make the diff into a mini-diff - * - * @param {DOMNode|String} view - */ - minify(view) { - view = jQuery(view) - .addClass('mini') - // Dialog changes these, if resized - .width('100%').css('height', 'inherit') - .show(); - jQuery('th', view).hide(); - jQuery('td.equal', view).hide() - .prevAll().hide(); - } - /** - * Expand mini-diff - * - * @param {DOMNode|String} view - */ - un_minify(view) { - jQuery(view).removeClass('mini').show(); - jQuery('th', view).show(); - jQuery('td.equal', view).show(); - } - /** - * Code for implementing et2_IDetachedDOM - * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree - */ - /** - * Build a list of attributes which can be set when working in the - * "detached" mode in the _attrs array which is provided - * by the calling code. - * - * @param {object} _attrs - */ - getDetachedAttributes(_attrs) { - _attrs.push("value", "label"); - } - /** - * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - */ - getDetachedNodes() { - return [this.div]; - } - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which has to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes(_nodes, _values) { - this.div = _nodes[0]; - if (typeof _values['label'] != 'undefined') { - this.set_label(_values['label']); - } - if (typeof _values['value'] != 'undefined') { - this.set_value(_values['value']); - } - } -} -et2_diff._attributes = { - "value": { - "type": "any" - } -}; -et2_register_widget(et2_diff, ["diff"]); -//# sourceMappingURL=et2_widget_diff.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_dropdown_button.js b/api/js/etemplate/et2_widget_dropdown_button.js deleted file mode 100644 index 934c9b7d8c..0000000000 --- a/api/js/etemplate/et2_widget_dropdown_button.js +++ /dev/null @@ -1,364 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Dropdown Button object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2013 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - et2_baseWidget; -*/ -import { et2_inputWidget } from './et2_core_inputWidget'; -import { et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_no_init } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -/** - * A split button - a button with a dropdown list - * - * There are several parts to the button UI: - * - Container: This is what is percieved as the dropdown button, the whole package together - * - Button: The part on the left that can be clicked - * - Arrow: The button to display the choices - * - Menu: The list of choices - * - * Menu options are passed via the select_options. They are normally ID => Title pairs, - * as for a select box, but the title can also be full HTML if needed. - * - * @augments et2_inputWidget - */ -export class et2_dropdown_button extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_dropdown_button - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_dropdown_button._attributes, _child || {})); - this.internal_ids = { - div: "", - button: "", - menu: "" - }; - this.div = null; - this.buttons = null; - this.button = null; - this.arrow = null; - this.menu = null; - this.image = null; - this.clicked = false; - this.label_updates = true; - this.value = null; - /** - * Default menu, so there is something for the widget browser / editor to show - */ - this.default_menu = ''; - this.clicked = false; - let self = this; - // Create the individual UI elements - // Menu is a UL - this.menu = jQuery(this.default_menu).attr("id", this.internal_ids.menu) - .hide() - .menu({ - select: function (event, ui) { - self.onselect.call(self, event, ui.item); - } - }); - this.buttons = jQuery(document.createElement("div")) - .addClass("et2_dropdown"); - // Main "wrapper" div - this.div = jQuery(document.createElement("div")) - .attr("id", this.internal_ids.div) - .append(this.buttons) - .append(this.menu); - // Left side - activates click action - this.button = jQuery(document.createElement("button")) - .attr("id", this.internal_ids.button) - .attr("type", "button") - .addClass("ui-widget ui-corner-left").removeClass("ui-corner-all") - .appendTo(this.buttons); - // Right side - shows dropdown - this.arrow = jQuery(document.createElement("button")) - .addClass("ui-widget ui-corner-right").removeClass("ui-corner-all") - .attr("type", "button") - .click(function () { - // ignore click on readonly button - if (self.options.readonly) - return false; - // Clicking it again hides menu - if (self.menu.is(":visible")) { - self.menu.hide(); - return false; - } - // Show menu dropdown - var menu = self.menu.show().position({ - my: "left top", - at: "left bottom", - of: self.buttons - }); - // Hide menu if clicked elsewhere - jQuery(document).one("click", function () { - menu.hide(); - }); - return false; - }) - // This is the actual down arrow icon - .append("
") - .appendTo(this.buttons); - // Common button UI - this.buttons.children("button") - .addClass("ui-state-default") - .hover(function () { jQuery(this).addClass("ui-state-hover"); }, function () { jQuery(this).removeClass("ui-state-hover"); }); - // Icon - this.image = jQuery(document.createElement("img")); - this.setDOMNode(this.div[0]); - } - destroy() { - // Destroy widget - if (this.menu && this.menu.data('ui-menu')) - this.menu.menu("destroy"); - // Null children - this.image = null; - this.button = null; - this.arrow = null; - this.buttons = null; - this.menu = null; - // Remove - this.div.empty().remove(); - } - set_id(_id) { - super.set_id(_id); - // Update internal IDs - not really needed since we refer by internal - // javascript reference, but good to keep up to date - this.internal_ids = { - div: this.dom_id + "_wrapper", - button: this.dom_id, - menu: this.dom_id + "_menu" - }; - for (let key in this.internal_ids) { - if (this[key] == null) - continue; - this[key].attr("id", this.internal_ids[key]); - } - } - /** - * Set if the button label changes to match the selected option - * - * @param updates boolean Turn updating on or off - */ - set_label_updates(updates) { - this.label_updates = updates; - } - set_accesskey(key) { - jQuery(this.node).attr("accesskey", key); - } - set_ro_image(_image) { - if (this.options.readonly) { - this.set_image(_image); - } - } - set_image(_image) { - if (!this.isInTree() || this.image == null) - return; - if (!_image.trim()) { - this.image.hide(); - } - else { - this.image.show(); - } - let src = this.egw().image(_image); - if (src) { - this.image.attr("src", src); - } - // allow url's too - else if (_image[0] == '/' || _image.substr(0, 4) == 'http') { - this.image.attr('src', _image); - } - else { - this.image.hide(); - } - } - /** - * Overwritten to maintain an internal clicked attribute - * - * @param _ev - * @returns {Boolean} - */ - click(_ev) { - // ignore click on readonly button - if (this.options.readonly) - return false; - this.clicked = true; - if (!super.click(_ev)) { - this.clicked = false; - return false; - } - this.clicked = false; - return true; - } - onselect(event, selected_node) { - this.set_value(selected_node.attr("data-id")); - this.change(selected_node); - } - attachToDOM() { - let res = super.attachToDOM(); - // Move the parent's handler to the button, or we can't tell the difference between the clicks - jQuery(this.node).unbind("click.et2_baseWidget"); - this.button.off().bind("click.et2_baseWidget", this, function (e) { - return e.data.click.call(e.data, this); - }); - return res; - } - set_label(_value) { - if (this.button) { - this.label = _value; - this.button.text(_value) - .prepend(this.image); - } - } - /** - * Set the options for the dropdown - * - * @param options Object ID => Label pairs - */ - set_select_options(options) { - this.menu.first().empty(); - // Allow more complicated content, if passed - if (typeof options == "string") { - this.menu.append(options); - } - else { - let add_complex = function (node, options) { - for (let key in options) { - let item; - if (typeof options[key] == "string") { - item = jQuery("
  • " + options[key] + "
  • "); - } - else if (options[key]["label"]) { - item = jQuery("
  • " + options[key]["label"] + "
  • "); - } - // Optgroup - else { - item = jQuery("
  • " + key + "
  • "); - add_complex(node.append("
      "), options[key]); - } - node.append(item); - if (item && options[key].icon) { - // we supply a applicable class for item images - jQuery('a', item).prepend(''); - } - } - }; - add_complex(this.menu.first(), options); - } - this.menu.menu("refresh"); - } - /** - * Set tab index - */ - set_tabindex(index) { - jQuery(this.button).attr("tabindex", index); - } - set_value(new_value) { - let menu_item = jQuery("[data-id='" + new_value + "']", this.menu); - if (menu_item.length) { - this.value = new_value; - if (this.label_updates) { - this.set_label(menu_item.text()); - } - } - else { - this.value = null; - if (this.label_updates) { - this.set_label(this.options.label); - } - } - } - getValue() { - return this.value; - } - /** - * Set options.readonly - * - * @param {boolean} _ro - */ - set_readonly(_ro) { - if (_ro != this.options.readonly) { - this.options.readonly = _ro; - // don't make readonly dropdown buttons clickable - if (this.buttons) { - this.buttons.find('button') - .toggleClass('et2_clickable', !_ro) - .toggleClass('et2_button_ro', _ro) - .css('cursor', _ro ? 'default' : 'pointer'); - } - } - } -} -et2_dropdown_button.attributes = { - "label": { - "name": "caption", - "type": "string", - "description": "Label of the button", - "translate": true, - "default": "Select..." - }, - "label_updates": { - "name": "Label updates", - "type": "boolean", - "description": "Button label updates when an option is selected from the menu", - "default": true - }, - "image": { - "name": "Icon", - "type": "string", - "description": "Add an icon" - }, - "ro_image": { - "name": "Read-only Icon", - "type": "string", - "description": "Use this icon instead of hiding for read-only" - }, - "onclick": { - "description": "JS code which gets executed when the button is clicked" - }, - "select_options": { - "type": "any", - "name": "Select options", - "default": {}, - "description": "Select options for dropdown. Can be a simple key => value list, or value can be full HTML", - // Skip normal initialization for this one - "ignore": true - }, - "accesskey": { - "name": "Access Key", - "type": "string", - "default": et2_no_init, - "description": "Alt + activates widget" - }, - "tabindex": { - "name": "Tab index", - "type": "integer", - "default": et2_no_init, - "description": "Specifies the tab order of a widget when the 'tab' button is used for navigating." - }, - // No such thing as a required button - "required": { - "ignore": true - } -}; -et2_register_widget(et2_dropdown_button, ["dropdown_button"]); -//# sourceMappingURL=et2_widget_dropdown_button.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_dynheight.js b/api/js/etemplate/et2_widget_dynheight.js deleted file mode 100644 index 4d353f9bc5..0000000000 --- a/api/js/etemplate/et2_widget_dynheight.js +++ /dev/null @@ -1,153 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Dynheight object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:use - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inheritance; -*/ -import { egw } from "../jsapi/egw_global"; -/** - * Object which resizes an inner node to the maximum extend of an outer node - * (without creating a scrollbar) - it achieves that by performing some very - * nasty and time consuming calculations. - */ -export class et2_dynheight { - constructor(_outerNode, _innerNode, _minHeight) { - this.initialized = false; - this.minHeight = 0; - this.bottomNodes = []; - this.innerMargin = 0; - this.outerMargin = 0; - this.outerNode = jQuery(_outerNode); - this.innerNode = jQuery(_innerNode); - this.minHeight = _minHeight; - } - destroy() { - this.outerNode = null; - this.innerNode = null; - this.bottomNodes = []; - } - /** - * Resizes the inner node. When this is done, the callback function is - * called. - * - * @param {function} _callback - * @param {object} _context - */ - update(_callback, _context) { - // Check whether the inner node is actually visible - if not, don't - // trigger the callback function - if (this.innerNode.is(":visible")) { - // Initialize the height calculation - this._initialize(); - // Get the outer container height and offset, if available - const oh = this.outerNode.height(); - const ot = this.outerNode.offset() ? this.outerNode.offset().top : 0; - // Get top and height of the inner node - const it = this.innerNode.offset().top; - // Calculate the height of the "bottomNodes" - let bminTop = this.bottomNodes.length ? Infinity : 0; - let bmaxBot = 0; - for (let i = 0; i < this.bottomNodes.length; i++) { - // Ignore hidden popups - if (this.bottomNodes[i].find('.action_popup').length) { - egw.debug('warn', "Had to skip a hidden popup - it should be removed", this.bottomNodes[i].find('.action_popup')); - continue; - } - // Ignore other hidden nodes - if (!this.bottomNodes[i].is(':visible')) - continue; - // Get height, top and bottom and calculate the maximum/minimum - let bh = this.bottomNodes[i].outerHeight(true); - let bt = this.bottomNodes[i].offset().top; - const bb = bh + bt; - if (i == 0 || bminTop > bt) { - bminTop = bt; - } - if (i == 0 || bmaxBot < bb) { - bmaxBot = bb; - } - } - // Get the height of the bottom container - const bh = Math.max(0, bmaxBot - bminTop); - // Calculate the new height of the inner container - const h = Math.max(this.minHeight, oh + ot - it - bh - - this.innerMargin - this.outerMargin); - this.innerNode.height(h); - // Update the width - // Some checking to make sure it doesn't overflow the width when user - // resizes the window - let w = this.outerNode.width(); - if (w > jQuery(window).width()) { - // 50px border, totally arbitrary, but we just need to make sure it's inside - w = jQuery(window).width() - 50; - } - if (w != this.innerNode.outerWidth()) { - this.innerNode.width(w); - } - // Call the callback function - if (typeof _callback != "undefined") { - _callback.call(_context, w, h); - } - } - } - /** - * Function used internally which collects all DOM-Nodes which are located - * below this element. - * - * @param {HTMLElement} _node - * @param {number} _bottom - */ - _collectBottomNodes(_node, _bottom) { - // Calculate the bottom position of the inner node - if (typeof _bottom == "undefined") { - _bottom = this.innerNode.offset().top + this.innerNode.height(); - } - if (_node) { - // Accumulate the outer margin of the parent elements - const node = jQuery(_node); - const ooh = node.outerHeight(true); - const oh = node.height(); - this.outerMargin += (ooh - oh) / 2; // Divide by 2 as the value contains margin-top and -bottom - // Iterate over the children of the given node and do the same - // recursively to the parent nodes until the _outerNode or body is - // reached. - const self = this; - jQuery(_node).children().each(function () { - const $this = jQuery(this); - const top = $this.offset().top; - if (this != self.innerNode[0] && top >= _bottom) { - self.bottomNodes.push($this); - } - }); - if (_node != this.outerNode[0] && _node != jQuery("body")[0]) { - this._collectBottomNodes(_node.parentNode, _bottom); - } - } - } - /** - * Used internally to calculate some information which will not change over - * the time. - */ - _initialize() { - if (!this.initialized) { - // Collect all bottomNodes and calculates the outer margin - this.bottomNodes = []; - this.outerMargin = 0; - this._collectBottomNodes(this.innerNode[0].parentNode); - // Calculate the inner margin - const ioh = this.innerNode.outerHeight(true); - const ih = this.innerNode.height(); - this.innerMargin = ioh - ih; - this.initialized = true; - } - } -} -//# sourceMappingURL=et2_widget_dynheight.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_entry.js b/api/js/etemplate/et2_widget_entry.js deleted file mode 100644 index f92773f2f1..0000000000 --- a/api/js/etemplate/et2_widget_entry.js +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Egroupware etemplate2 JS Entry widget - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - et2_core_valueWidget; -*/ -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_no_init } from "./et2_core_common"; -/** - * A widget to display a value from an entry - * - * Since we have Etemplate\Widget\Transformer, this client side widget exists - * mostly to resolve the problem where the ID for the entry widget is the same - * as the widget where you actually set the value, which prevents transformer - * from working. - * - * Server side will find the associated entry, and load it into ~ to - * avoid overwriting the widget with id="entry_id". This widget will reverse - * that, and the modifications from transformer will be applied. - * - * @augments et2_valueWidget - */ -export class et2_entry extends et2_valueWidget { - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_entry._attributes, _child || {})); - this.widget = null; - // Often the ID conflicts, so check prefix - if (_attrs.id && _attrs.id.indexOf(et2_entry.prefix) < 0) { - _attrs.id = et2_entry.prefix + _attrs.id; - } - let value = _attrs.value; - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_entry._attributes, _child || {})); - // Save value from parsing, but only if set - if (value) { - this.options.value = value; - } - this.widget = null; - this.setDOMNode(document.createElement('span')); - } - loadFromXML(_node) { - // Load the nodes as usual - super.loadFromXML(_node); - // Do the magic - this.loadField(); - } - /** - * Initialize widget for entry field - */ - loadField() { - // Create widget of correct type - let attrs = { - id: this.id + (this.options.field ? '[' + this.options.field + ']' : ''), - type: 'label', - readonly: this.options.readonly - }; - let modifications = this.getArrayMgr("modifications"); - if (modifications && this.options.field) { - jQuery.extend(attrs, modifications.getEntry(attrs.id)); - } - // Supress labels on templates - if (attrs.type == 'template' && this.options.label) { - this.egw().debug('log', "Surpressed label on <" + this.getType() + ' label="' + this.options.label + '" id="' + this.id + '"...>'); - this.options.label = ''; - } - let widget = et2_createWidget(attrs.type, attrs, this); - // If value is not set, etemplate takes care of everything - // If value was set, find the record explicitly. - if (typeof this.options.value == 'string') { - widget.options.value = this.getArrayMgr('content').getEntry(this.id + '[' + this.options.field + ']') || - this.getRoot().getArrayMgr('content').getEntry(et2_entry.prefix + this.options.value + '[' + this.options.field + ']'); - } - else if (this.options.field && this.options.value && this.options.value[this.options.field]) { - widget.options.value = this.options.value[this.options.field]; - } - if (this.options.compare) { - widget.options.value = widget.options.value == this.options.compare ? 'X' : ''; - } - if (this.options.alternate_fields) { - let sum = 0; - let fields = this.options.alternate_fields.split(':'); - for (let i = 0; i < fields.length; i++) { - let negate = (fields[i][0] == "-"); - let value = this.getArrayMgr('content').getEntry(fields[i].replace('-', '')); - sum += typeof value === 'undefined' ? 0 : (parseFloat(value) * (negate ? -1 : 1)); - if (value && this.options.field !== 'sum') { - widget.options.value = value; - break; - } - } - if (this.options.field == 'sum') { - if (this.options.precision && jQuery.isNumeric(sum)) - sum = parseFloat(sum).toFixed(this.options.precision); - widget.options.value = sum; - } - } - } -} -et2_entry._attributes = { - field: { - 'name': 'Fields', - 'description': 'Which entry field to display, or "sum" to add up the alternate_fields', - 'type': 'string' - }, - compare: { - name: 'Compare', - description: 'if given, the selected field is compared with its value and an X is printed on equality, nothing otherwise', - default: et2_no_init, - type: 'string' - }, - alternate_fields: { - name: 'Alternate fields', - description: 'colon (:) separated list of alternative fields. The first non-empty one is used if the selected field is empty, (-) used for subtraction', - type: 'string', - default: et2_no_init - }, - precision: { - name: 'Decimals to be shown', - description: 'Specifies the number of decimals for sum of alternates, the default is 2', - type: 'string', - default: '2' - }, - regex: { - name: 'Regular expression pattern', - description: 'Only used server-side in a preg_replace with regex_replace to modify the value', - default: et2_no_init, - type: 'string' - }, - regex_replace: { - name: 'Regular expression replacement pattern', - description: 'Only used server-side in a preg_replace with regex to modify the value', - default: et2_no_init, - type: 'string' - }, - value: { - type: 'any' - }, - readonly: { - default: true - } -}; -et2_entry.legacyOptions = ["field", "compare", "alternate_fields"]; -et2_entry.prefix = '~'; -et2_register_widget(et2_entry, ["entry", 'contact-value', 'contact-account', 'contact-template', 'infolog-value', 'tracker-value', 'records-value']); -//# sourceMappingURL=et2_widget_entry.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_favorites.js b/api/js/etemplate/et2_widget_favorites.js deleted file mode 100644 index 8b954da460..0000000000 --- a/api/js/etemplate/et2_widget_favorites.js +++ /dev/null @@ -1,331 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Favorite widget - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2013 - */ -/*egw:uses - et2_dropdown_button; - et2_extension_nextmatch; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_dropdown_button } from "./et2_widget_dropdown_button"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { egw, egw_getFramework } from "../jsapi/egw_global"; -/** - * Favorites widget, designed for use with a nextmatch widget - * - * The primary control is a split/dropdown button. Clicking on the left side of the button filters the - * nextmatch list by the user's default filter. The right side of the button gives a list of - * saved filters, pulled from preferences. Clicking a filter from the dropdown list sets the - * filters as saved. - * - * Favorites can also automatically be shown in the sidebox, using the special ID favorite_sidebox. - * Use the following code to generate the sidebox section: - * display_sidebox($appname,lang('Favorites'),array( - * array( - * 'no_lang' => true, - * 'text'=>'', - * 'link'=>false, - * 'icon' => false - * ) - * )); - * This sidebox list will be automatically generated and kept up to date. - * - * - * Favorites are implemented by saving the values for [column] filters. Filters are stored - * in preferences, with the name favorite_. The favorite favorite used for clicking on - * the filter button is stored in nextmatch--favorite. - * - * @augments et2_dropdown_button - */ -export class et2_favorites extends et2_dropdown_button { - /** - * Constructor - * - * @memberOf et2_favorites - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_favorites._attributes, _child || {})); - // Some convenient variables, used in closures / event handlers - this.header = null; - this.nextmatch = null; - this.favSortedList = null; - this.sidebox_target = null; - // If filter was set server side, we need to remember it until nm is created - this.nm_filter = false; - this.sidebox_target = jQuery("#" + this.options.sidebox_target); - if (this.sidebox_target.length == 0 && egw_getFramework() != null) { - let egw_fw = egw_getFramework(); - this.sidebox_target = jQuery("#" + this.options.sidebox_target, egw_fw.sidemenuDiv); - } - // Store array of sorted items - this.favSortedList = ['blank']; - let apps = egw().user('apps'); - et2_favorites.is_admin = (typeof apps['admin'] != "undefined"); - // Make sure we have an app - if (!this.options.app) { - this.options.app = this.getInstanceManager().app; - } - this.stored_filters = this.load_favorites(this.options.app); - this.preferred = egw.preference(this.options.default_pref, this.options.app); - if (!this.preferred || typeof this.stored_filters[this.preferred] == "undefined") { - this.preferred = "blank"; - } - // It helps to have the ID properly set before we get too far - this.set_id(this.id); - this.init_filters(this); - this.menu.addClass("favorites"); - // Set the default (button) value - this.set_value(this.preferred, true); - let self = this; - // Add a listener on the radio buttons to set default filter - jQuery(this.menu).on("click", "input:radio", function (event) { - // Don't do the menu - event.stopImmediatePropagation(); - // Save as default favorite - used when you click the button - self.egw().set_preference(self.options.app, self.options.default_pref, jQuery(this).val()); - self.preferred = jQuery(this).val(); - // Update sidebox, if there - if (self.sidebox_target.length) { - jQuery("div.ui-icon-heart", self.sidebox_target) - .replaceWith("
      "); - jQuery("li[data-id='" + self.preferred + "'] div.sideboxstar", self.sidebox_target) - .replaceWith("
      "); - } - // Close the menu - self.menu.hide(); - // Some user feedback - self.button.addClass("ui-state-active", 500, "swing", function () { - self.button.removeClass("ui-state-active", 2000); - }); - }); - //Sort DomNodes of sidebox fav. menu - let sideBoxDOMNodeSort = function (_favSList) { - let favS = jQuery.isArray(_favSList) ? _favSList.slice(0).reverse() : []; - for (let i = 0; i < favS.length; i++) { - self.sidebox_target.children().find('[data-id$="' + favS[i] + '"]').prependTo(self.sidebox_target.children()); - } - }; - //Add Sortable handler to nm fav. menu - jQuery(this.menu).sortable({ - items: 'li:not([data-id$="add"])', - placeholder: 'ui-fav-sortable-placeholder', - delay: 250, - update: function () { - self.favSortedList = jQuery(this).sortable('toArray', { attribute: 'data-id' }); - self.egw().set_preference(self.options.app, 'fav_sort_pref', self.favSortedList); - sideBoxDOMNodeSort(self.favSortedList); - } - }); - // Add a listener on the delete to remove - this.menu.on("click", "div.ui-icon-trash", app[self.options.app], function () { - // App instance might not be ready yet, so don't bind directly - app[self.options.app].delete_favorite.apply(this, arguments); - }) - // Wrap and unwrap because jQueryUI styles use a parent, and we don't want to change the state of the menu item - // Wrap in a span instead of a div because div gets a border - .on("mouseenter", "div.ui-icon-trash", function () { jQuery(this).wrap(""); }) - .on("mouseleave", "div.ui-icon-trash", function () { jQuery(this).unwrap(); }); - // Trigger refresh of menu options now that events are registered - // to update sidebox - if (this.sidebox_target.length > 0) { - this.init_filters(this); - } - } - /** - * Load favorites from preferences - * - * @param app String Load favorites from this application - */ - load_favorites(app) { - // Default blank filter - let stored_filters = { - 'blank': { - name: this.egw().lang("No filters"), - state: {} - } - }; - // Load saved favorites - let preferences = egw.preference("*", app); - for (let pref_name in preferences) { - if (pref_name.indexOf(et2_favorites.PREFIX) == 0 && typeof preferences[pref_name] == 'object') { - let name = pref_name.substr(et2_favorites.PREFIX.length); - stored_filters[name] = preferences[pref_name]; - // Keep older favorites working - they used to store nm filters in 'filters',not state - if (preferences[pref_name]["filters"]) { - stored_filters[pref_name]["state"] = preferences[pref_name]["filters"]; - } - } - if (pref_name == 'fav_sort_pref') { - this.favSortedList = preferences[pref_name]; - //Make sure sorted list is always an array, seems some old fav are not array - if (!jQuery.isArray(this.favSortedList)) - this.favSortedList = this.favSortedList.split(','); - } - } - if (typeof stored_filters == "undefined" || !stored_filters) { - stored_filters = {}; - } - else { - for (let name in stored_filters) { - if (this.favSortedList.indexOf(name) < 0) { - this.favSortedList.push(name); - } - } - this.egw().set_preference(this.options.app, 'fav_sort_pref', this.favSortedList); - if (this.favSortedList.length > 0) { - let sortedListObj = {}; - for (let i = 0; i < this.favSortedList.length; i++) { - if (typeof stored_filters[this.favSortedList[i]] != 'undefined') { - sortedListObj[this.favSortedList[i]] = stored_filters[this.favSortedList[i]]; - } - else { - this.favSortedList.splice(i, 1); - this.egw().set_preference(this.options.app, 'fav_sort_pref', this.favSortedList); - } - } - stored_filters = jQuery.extend(sortedListObj, stored_filters); - } - } - return stored_filters; - } - // Create & set filter options for dropdown menu - init_filters(widget, filters) { - if (typeof filters == "undefined") { - filters = this.stored_filters; - } - let options = {}; - for (let name in filters) { - options[name] = "" + - (filters[name].name != undefined ? filters[name].name : name) + - (filters[name].group != false && !et2_favorites.is_admin || name == 'blank' ? "" : - "
      "); - } - // Only add 'Add current' if we have a nextmatch - if (this.nextmatch) { - options["add"] = "" + this.egw().lang('Add current'); - } - widget.set_select_options.call(widget, options); - // Set radio to current value - jQuery("input[value='" + this.preferred + "']:radio", this.menu).attr("checked", 1); - } - set_nm_filters(filters) { - if (this.nextmatch) { - this.nextmatch.applyFilters(filters); - } - else { - console.log(filters); - } - } - onclick(node) { - // Apply preferred filter - make sure it's an object, and not a reference - if (this.preferred && this.stored_filters[this.preferred]) { - // use app[appname].setState if available to allow app to overwrite it (eg. change to non-listview in calendar) - if (typeof app[this.options.app] != 'undefined') { - app[this.options.app].setState(this.stored_filters[this.preferred]); - } - else { - this.set_nm_filters(jQuery.extend({}, this.stored_filters[this.preferred].state)); - } - } - else { - alert(this.egw().lang("No default set")); - } - } - // Apply the favorite when you pick from the list - change(selected_node) { - this.value = jQuery(selected_node).attr("data-id"); - if (this.value == "add" && this.nextmatch) { - // Get current filters - let current_filters = jQuery.extend({}, this.nextmatch.activeFilters); - // Add in extras - for (let extra in this.options.filters) { - // Don't overwrite what nm has, chances are nm has more up-to-date value - if (typeof current_filters == 'undefined') { - current_filters[extra] = this.nextmatch.options.settings[extra]; - } - } - // Skip columns for now - delete current_filters.selcolumns; - // Add in application's settings - if (this.filters != true) { - for (let i = 0; i < this.filters.length; i++) { - current_filters[this.options.filters[i]] = this.nextmatch.options.settings[this.options.filters[i]]; - } - } - // Call framework - app[this.options.app].add_favorite(current_filters); - // Reset value - this.set_value(this.preferred, true); - } - else if (this.value == 'blank') { - // Reset filters when select no filters - this.set_nm_filters({}); - } - } - set_value(filter_name, parent) { - if (parent) { - return super.set_value(filter_name); - } - if (filter_name == 'add') - return false; - app[this.options.app].setState(this.stored_filters[filter_name]); - return false; - } - getValue() { - return null; - } - /** - * Set the nextmatch to filter - * From et2_INextmatchHeader interface - * - * @param {et2_nextmatch} nextmatch - */ - setNextmatch(nextmatch) { - this.nextmatch = nextmatch; - if (this.nm_filter) { - this.set_value(this.nm_filter); - this.nm_filter = false; - } - // Re-generate filter list so we can add 'Add current' - this.init_filters(this); - } -} -et2_favorites._attributes = { - "default_pref": { - "name": "Default preference key", - "type": "string", - "description": "The preference key where default favorite is stored (not the value)" - }, - "sidebox_target": { - "name": "Sidebox target", - "type": "string", - "description": "ID of element to insert favorite list into", - "default": "favorite_sidebox" - }, - "app": { - "name": "Application", - "type": "string", - "description": "Application to show favorites for" - }, - "filters": { - "name": "Extra filters", - "type": "any", - "description": "Array of extra filters to include in the saved favorite" - }, - // These are particular to favorites - id: { "default": "favorite" }, - label: { "default": "" }, - label_updates: { "default": false }, - image: { "default": egw().image('fav_filter') }, - statustext: { "default": "Favorite queries", "type": "string" } -}; -et2_favorites.PREFIX = "favorite_"; -et2_register_widget(et2_favorites, ["favorites"]); -//# sourceMappingURL=et2_widget_favorites.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_file.js b/api/js/etemplate/et2_widget_file.js deleted file mode 100644 index 58a261b9eb..0000000000 --- a/api/js/etemplate/et2_widget_file.js +++ /dev/null @@ -1,621 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Number object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2011 - */ -/*egw:uses - et2_core_inputWidget; - api.Resumable.resumable; -*/ -import "../Resumable/resumable.js"; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_no_init } from "./et2_core_common"; -import { et2_vfsSize } from "./et2_widget_vfs"; -/** - * Class which implements file upload - * - * @augments et2_inputWidget - */ -export class et2_file extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_file - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_file._attributes, _child || {})); - this.asyncOptions = {}; - this.input = null; - this.progress = null; - this.span = null; - this.node = null; - this.input = null; - this.progress = null; - this.span = null; - // Contains all submit buttons need to be disabled during upload process - this.disabled_buttons = jQuery("input[type='submit'], button"); - // Make sure it's an object, not an array, or values get lost when sent to server - this.options.value = jQuery.extend({}, this.options.value); - if (!this.options.id) { - console.warn("File widget needs an ID. Used 'file_widget'."); - this.options.id = "file_widget"; - } - // Legacy - id ending in [] means multiple - if (this.options.id.substr(-2) == "[]") { - this.options.multiple = true; - } - // If ID ends in /, it's a directory - allow multiple - else if (this.options.id.substr(-1) === "/") { - this.options.multiple = true; - _attrs.multiple = true; - } - // Set up the URL to have the request ID & the widget ID - var instance = this.getInstanceManager(); - let self = this; - this.asyncOptions = jQuery.extend({}, this.getAsyncOptions(this)); - this.asyncOptions.fieldName = this.options.id; - this.createInputWidget(); - this.set_readonly(this.options.readonly); - } - destroy() { - super.destroy(); - this.set_drop_target(null); - this.node = null; - this.input = null; - this.span = null; - this.progress = null; - } - createInputWidget() { - this.node = jQuery(document.createElement("div")).addClass("et2_file"); - this.span = jQuery(document.createElement("span")) - .addClass('et2_file_span et2_button') - .appendTo(this.node); - if (this.options.label != '') - this.span.addClass('et2_button_text'); - let span = this.span; - this.input = jQuery(document.createElement("input")) - .attr("type", "file").attr("placeholder", this.options.blur) - .addClass("et2_file_upload") - .appendTo(this.node) - .hover(function () { - jQuery(span) - .toggleClass('et2_file_spanHover'); - }) - .on({ - mousedown: function () { - jQuery(span).addClass('et2_file_spanActive'); - }, - mouseup: function () { - jQuery(span).removeClass('et2_file_spanActive'); - } - }); - if (this.options.accept) - this.input.attr('accept', this.options.accept); - let self = this; - // trigger native input upload file - if (!this.options.readonly) - this.span.click(function () { self.input.click(); }); - // Check for File interface, should fall back to normal form submit if missing - if (typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") { - this.resumable = new Resumable(this.asyncOptions); - this.resumable.assignBrowse(this.input); - this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this)); - this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this)); - this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this)); - this.resumable.on('complete', jQuery.proxy(this.onFinish, this)); - } - else { - // This may be a problem submitting via ajax - } - if (this.options.progress) { - let widget = this.getRoot().getWidgetById(this.options.progress); - if (widget) { - //may be not available at createInputWidget time - this.progress = jQuery(widget.getDOMNode()); - } - } - if (!this.progress) { - this.progress = jQuery(document.createElement("div")).appendTo(this.node); - } - this.progress.addClass("progress"); - if (this.options.multiple) { - this.input.attr("multiple", "multiple"); - } - this.setDOMNode(this.node[0]); - // set drop target to widget dom node if no target option is specified - if (!this.options.drop_target) - this.resumable.assignDrop([this.getDOMNode()]); - } - /** - * Get any specific async upload options - */ - getAsyncOptions(self) { - return { - // Callbacks - onStart: function (event, file_count) { - return self.onStart(event, file_count); - }, - onFinish: function (event, file_count) { - self.onFinish.apply(self, [event, file_count]); - }, - onStartOne: function (event, file_name, index, file_count) { - }, - onFinishOne: function (event, response, name, number, total) { return self.finishUpload(event, response, name, number, total); }, - onProgress: function (event, progress, name, number, total) { return self.onProgress(event, progress, name, number, total); }, - onError: function (event, name, error) { return self.onError(event, name, error); }, - beforeSend: function (form) { return self.beforeSend(form); }, - chunkSize: this.options.chunk_size || 1024 * 1024, - target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"), - query: function (file) { return self.beforeSend(file); }, - // Disable checking for already uploaded chunks - testChunks: false - }; - } - /** - * Set a widget or DOM node as a HTML5 file drop target - * - * @param {string} new_target widget ID or DOM node ID to be used as a new target - */ - set_drop_target(new_target) { - // Cancel old drop target - if (this.options.drop_target) { - let widget = this.getRoot().getWidgetById(this.options.drop_target); - let drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); - if (drop_target) { - this.resumable.unAssignDrop(drop_target); - } - } - this.options.drop_target = new_target; - if (!this.options.drop_target) - return; - // Set up new drop target - let widget = this.getRoot().getWidgetById(this.options.drop_target); - let drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); - if (drop_target) { - this.resumable.assignDrop([drop_target]); - } - else { - this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); - } - } - attachToDOM() { - let res = super.attachToDOM(); - // Override parent's change, file widget will fire change when finished uploading - this.input.unbind("change.et2_inputWidget"); - return res; - } - getValue() { - return this.options.value ? this.options.value : this.input.val(); - } - /** - * Set the value of the file widget. - * - * If you pass a FileList or list of files, it will trigger the async upload - * - * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. - * @param {Event} event Most browsers require the user to initiate file transfers in some way. - * Pass the event in, if you have it. - */ - set_value(value, event) { - if (!value || typeof value == "undefined") { - value = {}; - } - if (jQuery.isEmptyObject(value)) { - this.options.value = {}; - if (this.resumable.progress() == 1) - this.progress.empty(); - // Reset the HTML element - this.input.wrap('
      ').closest('form').get(0).reset(); - this.input.unwrap(); - return; - } - let addFile = jQuery.proxy(function (i, file) { - this.resumable.addFile(file, event); - }, this); - if (typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) { - try { - this.input[0].files = value; - jQuery.each(value, addFile); - } - catch (e) { - var self = this; - var args = arguments; - jQuery.each(value, addFile); - } - } - } - /** - * Set the value for label - * The label is used as caption for span tag which customize the HTML file upload styling - * - * @param {string} value text value of label - */ - set_label(value) { - if (this.span != null && value != null) { - this.span.text(value); - } - } - getInputNode() { - if (typeof this.input == 'undefined') - return false; - return this.input[0]; - } - set_mime(mime) { - if (!mime) { - this.options.mime = null; - } - if (mime.indexOf("/") != 0) { - // Lower case it now, if it's not a regex - this.options.mime = mime.toLowerCase(); - } - else { - this.options.mime = mime; - } - } - set_multiple(_multiple) { - this.options.multiple = _multiple; - if (_multiple) { - return this.input.attr("multiple", "multiple"); - } - return this.input.removeAttr("multiple"); - } - /** - * Check to see if the provided file's mimetype matches - * - * @param f File object - * @return boolean - */ - checkMime(f) { - if (!this.options.mime) - return true; - let mime = ''; - if (this.options.mime.indexOf("/") != 0) { - // Lower case it now, if it's not a regex - mime = this.options.mime.toLowerCase(); - } - else { - // Convert into a js regex - var parts = this.options.mime.substr(1).match(/(.*)\/([igm]?)$/); - mime = new RegExp(parts[1], parts.length > 2 ? parts[2] : ""); - } - // If missing, let the server handle it - if (!mime || !f.type) - return true; - var is_preg = (typeof mime == "object"); - if (!is_preg && f.type.toLowerCase() == mime || is_preg && mime.test(f.type)) { - return true; - } - // Not right mime - return false; - } - _fileAdded(file, event) { - // Manual additions have no event - if (typeof event == 'undefined') { - event = {}; - } - // Trigger start of uploading, calls callback - if (!this.resumable.isUploading()) { - if (!(this.onStart(event, this.resumable.files.length))) - return; - } - // Here 'this' is the input - if (this.checkMime(file.file)) { - if (this.createStatus(event, file)) { - // Disable buttons - this.disabled_buttons - .not("[disabled]") - .attr("disabled", true) - .addClass('et2_button_ro') - .removeClass('et2_clickable') - .css('cursor', 'default'); - // Actually start uploading - this.resumable.upload(); - } - } - else { - // Wrong mime type - show in the list of files - return this.createStatus(this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime), file); - } - } - /** - * Add in the request id - */ - beforeSend(form) { - var instance = this.getInstanceManager(); - return { - request_id: instance.etemplate_exec_id, - widget_id: this.id - }; - } - /** - * Disables submit buttons while uploading - */ - onStart(event, file_count) { - // Hide any previous errors - this.hideMessage(); - event.data = this; - //Add dropdown_progress - if (this.options.progress_dropdownlist) { - this._build_progressDropDownList(); - } - // Callback - if (this.options.onStart) - return et2_call(this.options.onStart, event, file_count); - return true; - } - /** - * Re-enables submit buttons when done - */ - onFinish() { - this.disabled_buttons.removeAttr("disabled").css('cursor', 'pointer').removeClass('et2_button_ro'); - var file_count = this.resumable.files.length; - // Remove files from list - while (this.resumable.files.length > 0) { - this.resumable.removeFile(this.resumable.files[this.resumable.files.length - 1]); - } - var event = jQuery.Event('upload'); - event.data = this; - var result = false; - //Remove progress_dropDown_fileList class and unbind the click handler from body - if (this.options.progress_dropdownlist) { - this.progress.removeClass("progress_dropDown_fileList"); - jQuery(this.node).find('span').removeClass('totalProgress_loader'); - jQuery('body').off('click'); - } - if (this.options.onFinish && !jQuery.isEmptyObject(this.getValue())) { - result = et2_call(this.options.onFinish, event, file_count); - } - else { - result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue())); - } - if (result) { - // Fire legacy change action when done - this.change(this.input); - } - } - /** - * Build up dropdown progress with total count indicator - * - * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed - */ - _build_progressDropDownList() { - this.progress.addClass("progress_dropDown_fileList"); - //Add uploading indicator and bind hover handler on it - jQuery(this.node).find('span').addClass('totalProgress_loader'); - jQuery(this.node).find('span.et2_file_span').hover(function () { - jQuery('.progress_dropDown_fileList').show(); - }); - //Bind click handler to dismiss the dropdown while uploading - jQuery('body').on('click', function (event) { - if (event.target.className != 'remove') { - jQuery('.progress_dropDown_fileList').hide(); - } - }); - } - /** - * Creates the elements used for displaying the file, and it's upload status, and - * attaches them to the DOM - * - * @param _event Either the event, or an error message - */ - createStatus(_event, file) { - var error = (typeof _event == "object" ? "" : _event); - if (this.options.max_file_size && file.size > this.options.max_file_size) { - error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); - } - if (this.options.progress) { - var widget = this.getRoot().getWidgetById(this.options.progress); - if (widget) { - this.progress = jQuery(widget.getDOMNode()); - this.progress.addClass("progress"); - } - } - if (this.progress) { - var fileName = file.fileName || 'file'; - var status = jQuery("
    • " + fileName - + "

    • ") - .appendTo(this.progress); - jQuery("div.remove", status).on('click', file, jQuery.proxy(this.cancel, this)); - if (error != "") { - status.addClass("message ui-state-error"); - status.append("
      " + error + ""); - jQuery(".progressBar", status).css("display", "none"); - } - } - return error == ""; - } - _fileProgress(file) { - if (this.progress) { - jQuery("li[data-file='" + file.fileName.replace(/'/g, '"') + "'] > span.progressBar > p").css("width", Math.ceil(file.progress() * 100) + "%"); - } - return true; - } - onError(event, name, error) { - console.warn(event, name, error); - } - /** - * A file upload is finished, update the UI - */ - finishUpload(file, response) { - var name = file.fileName || 'file'; - if (typeof response == 'string') - response = jQuery.parseJSON(response); - if (response.response[0] && typeof response.response[0].data.length == 'undefined') { - if (typeof this.options.value !== 'object' || !this.options.multiple) { - this.set_value({}); - } - for (var key in response.response[0].data) { - if (typeof response.response[0].data[key] == "string") { - // Message from server - probably error - jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress) - .addClass("error") - .css("display", "block") - .text(response.response[0].data[key]); - } - else { - this.options.value[key] = response.response[0].data[key]; - // If not multiple, we already destroyed the status, so re-create it - if (!this.options.multiple) { - this.createStatus({}, file); - } - if (this.progress) { - jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress).addClass("message success"); - } - } - } - } - else if (this.progress) { - jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress) - .addClass("ui-state-error") - .css("display", "block") - .text(this.egw().lang("Server error")); - } - var event = jQuery.Event('upload'); - event.data = this; - // Callback - if (typeof this.onFinishOne == 'function') { - this.onFinishOne(event, response, name); - } - return true; - } - /** - * Remove a file from the list of values - * - * @param {File|string} File object, or file name, to remove - */ - remove_file(file) { - //console.info(filename); - if (typeof file == 'string') { - file = { fileName: file }; - } - for (var key in this.options.value) { - if (this.options.value[key].name == file.fileName) { - delete this.options.value[key]; - jQuery('[data-file="' + file.fileName.replace(/'/g, '"') + '"]', this.node).remove(); - return; - } - } - if (file.isComplete && !file.isComplete() && file.cancel) - file.cancel(); - } - /** - * Cancel a file - event callback - */ - cancel(e) { - e.preventDefault(); - // Look for file name in list - var target = jQuery(e.target).parents("li"); - this.remove_file(e.data); - // In case it didn't make it to the list (error) - target.remove(); - jQuery(e.target).remove(); - } - /** - * Set readonly - * - * @param {boolean} _ro boolean readonly state, true means readonly - */ - set_readonly(_ro) { - if (typeof _ro != "undefined") { - this.options.readonly = _ro; - this.span.toggleClass('et2_file_ro', _ro); - if (this.options.readonly) { - this.span.unbind('click'); - } - else { - var self = this; - this.span.off().bind('click', function () { self.input.click(); }); - } - } - } -} -et2_file._attributes = { - "multiple": { - "name": "Multiple files", - "type": "boolean", - "default": false, - "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." - }, - "max_file_size": { - "name": "Maximum file size", - "type": "integer", - "default": 0, - "description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608" - }, - "mime": { - "name": "Allowed file types", - "type": "string", - "default": et2_no_init, - "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - "progress": { - "name": "Progress node", - "type": "string", - "default": et2_no_init, - "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" - }, - "onStart": { - "name": "Start event handler", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." - }, - "onFinish": { - "name": "Finish event handler", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when all files to be uploaded are finished." - }, - drop_target: { - "name": "Optional, additional drop target for HTML5 uploads", - "type": "string", - "default": et2_no_init, - "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" - }, - label: { - "name": "Label of file upload", - "type": "string", - "default": "Choose file...", - "description": "String caption to be displayed on file upload span" - }, - progress_dropdownlist: { - "name": "List on files in progress like dropdown", - "type": "boolean", - "default": false, - "description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator" - }, - onFinishOne: { - "name": "Finish event handler for each one", - "type": "js", - "default": et2_no_init, - "description": "A (js) function called when a file to be uploaded is finished." - }, - accept: { - "name": "Acceptable extensions", - "type": "string", - "default": '', - "description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything." - }, - chunk_size: { - "name": "Chunk size", - "type": "integer", - "default": 1024 * 1024, - "description": "Max chunk size, gets set from server-side PHP (max_upload_size-1M)/2" // last chunk can be up to 2*chunk_size! - } -}; -et2_register_widget(et2_file, ["file"]); -//# sourceMappingURL=et2_widget_file.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_grid.js b/api/js/etemplate/et2_widget_grid.js deleted file mode 100644 index 784231508e..0000000000 --- a/api/js/etemplate/et2_widget_grid.js +++ /dev/null @@ -1,910 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Grid object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - * @copyright EGroupware GmbH 2011-2021 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_DOMWidget; - et2_core_xml; -*/ -import { et2_no_init } from "./et2_core_common"; -import { et2_register_widget, et2_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_action_object_impl, et2_DOMWidget } from "./et2_core_DOMWidget"; -import { egw_getAppObjectManager, egwActionObject } from '../egw_action/egw_action.js'; -import { et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault } from "./et2_core_xml"; -import { egw } from "../jsapi/egw_global"; -/** - * Class which implements the "grid" XET-Tag - * - * This also includes repeating the last row in the grid and filling - * it with content data - * - * @augments et2_DOMWidget - */ -export class et2_grid extends et2_DOMWidget { - /** - * Constructor - * - * @memberOf et2_grid - */ - constructor(_parent, _attrs, _child) { - // Call the parent constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_grid._attributes, _child || {})); - // Counters for rows and columns - this.rowCount = 0; - this.columnCount = 0; - // 2D-Array which holds references to the DOM td tags - this.cells = []; - this.rowData = []; - this.colData = []; - this.managementArray = []; - // Keep the template node for later regeneration - this.template_node = null; - // Wrapper div for height & overflow, if needed - this.wrapper = null; - // Create the table body and the table - this.table = jQuery(document.createElement("table")) - .addClass("et2_grid"); - this.thead = jQuery(document.createElement("thead")) - .appendTo(this.table); - this.tfoot = jQuery(document.createElement("tfoot")) - .appendTo(this.table); - this.tbody = jQuery(document.createElement("tbody")) - .appendTo(this.table); - } - _initCells(_colData, _rowData) { - // Copy the width and height - const w = _colData.length; - const h = _rowData.length; - // Create the 2D-Cells array - const cells = new Array(h); - for (let y = 0; y < h; y++) { - cells[y] = new Array(w); - // Initialize the cell description objects - for (let x = 0; x < w; x++) { - // Some columns (nm) we do not parse into a boolean - const col_disabled = _colData[x].disabled; - cells[y][x] = { - "td": null, - "widget": null, - "colData": _colData[x], - "rowData": _rowData[y], - "disabled": col_disabled || _rowData[y].disabled, - "class": _colData[x]["class"], - "colSpan": 1, - "autoColSpan": false, - "rowSpan": 1, - "autoRowSpan": false, - "width": _colData[x].width, - "x": x, - "y": y - }; - } - } - return cells; - } - _getColDataEntry() { - return { - width: "auto", - class: "", - align: "", - span: "1", - disabled: false - }; - } - _getRowDataEntry() { - return { - height: "auto", - class: "", - valign: "top", - span: "1", - disabled: false - }; - } - _getCell(_cells, _x, _y) { - if ((0 <= _y) && (_y < _cells.length)) { - const row = _cells[_y]; - if ((0 <= _x) && (_x < row.length)) { - return row[_x]; - } - } - throw ("Error while accessing grid cells, invalid element count or span value!"); - } - _forceNumber(_val) { - if (isNaN(_val)) { - throw (_val + " is not a number!"); - } - return parseInt(_val); - } - _fetchRowColData(columns, rows, colData, rowData) { - // Some things cannot be done inside a nextmatch - nm will do the expansion later - var nm = false; - let widget = this; - while (!nm && widget != this.getRoot()) { - nm = (widget.getType() == 'nextmatch'); - widget = widget.getParent(); - } - // Parse the columns tag - et2_filteredNodeIterator(columns, function (node, nodeName) { - const colDataEntry = this._getColDataEntry(); - // This cannot be done inside a nm, it will expand it later - colDataEntry["disabled"] = nm ? - et2_readAttrWithDefault(node, "disabled", "") : - this.getArrayMgr("content") - .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); - if (nodeName == "column") { - colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto"); - colDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); - colDataEntry["align"] = et2_readAttrWithDefault(node, "align", ""); - colDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); - // Keep any others attributes set, there's no 'column' widget - for (let i in node.attributes) { - const attr = node.attributes[i]; - if (attr.nodeType == 2 && typeof colDataEntry[attr.nodeName] == 'undefined') { - colDataEntry[attr.nodeName] = attr.value; - } - } - } - else { - colDataEntry["span"] = "all"; - } - colData.push(colDataEntry); - }, this); - // Parse the rows tag - et2_filteredNodeIterator(rows, function (node, nodeName) { - const rowDataEntry = this._getRowDataEntry(); - rowDataEntry["disabled"] = this.getArrayMgr("content") - .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); - if (nodeName == "row") { - // Remember this row for auto-repeat - it'll eventually be the last one - this.lastRowNode = node; - rowDataEntry["height"] = et2_readAttrWithDefault(node, "height", "auto"); - rowDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); - rowDataEntry["valign"] = et2_readAttrWithDefault(node, "valign", ""); - rowDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); - rowDataEntry["part"] = et2_readAttrWithDefault(node, "part", "body"); - const id = et2_readAttrWithDefault(node, "id", ""); - if (id) { - rowDataEntry["id"] = id; - } - } - else { - rowDataEntry["span"] = "all"; - } - rowData.push(rowDataEntry); - }, this); - // Add in repeated rows - // TODO: It would be nice if we could skip header (thead) & footer (tfoot) or treat them separately - let rowIndex = Infinity; - if (this.getArrayMgr("content")) { - const content = this.getArrayMgr("content"); - var rowDataEntry = rowData[rowData.length - 1]; - rowIndex = rowData.length - 1; - // Find out if we have any content rows, and how many - let cont = true; - while (cont) { - if (content.data[rowIndex]) { - rowData[rowIndex] = jQuery.extend({}, rowDataEntry); - rowIndex++; - } - else if (this.lastRowNode != null) { - // Have to look through actual widgets to support const[$row] - // style names - should be avoided so we can remove this extra check - // Old etemplate checked first two widgets, or first two box children - // This cannot be done inside a nextmatch - nm will do the expansion later - var nm = false; - if (nm) { - // No further checks for repeated rows - break; - } - // Not in a nextmatch, so we can expand with abandon - const currentPerspective = jQuery.extend({}, content.perspectiveData); - const check = function (node, nodeName) { - if (nodeName == 'box' || nodeName == 'hbox' || nodeName == 'vbox') { - return et2_filteredNodeIterator(node, check, this); - } - content.perspectiveData.row = rowIndex; - for (let attr in node.attributes) { - const value = et2_readAttrWithDefault(node, node.attributes[attr].name, ""); - // Don't include first char, those should be handled by normal means - // and it would break nextmatch - if (value.indexOf('@') > 0 || value.indexOf('$') > 0) { - // Ok, we found something. How many? Check for values. - let ident = content.expandName(value); - // expandName() handles index into content (@), but we have to look up - // regular values - if (value[0] != '@') { - // Returns null if there isn't an actual value - ident = content.getEntry(ident, false, true); - } - while (ident != null && rowIndex < 1000) { - rowData[rowIndex] = jQuery.extend({}, rowDataEntry); - content.perspectiveData.row = ++rowIndex; - ident = content.expandName(value); - if (value[0] != '@') { - // Returns null if there isn't an actual value - ident = content.getEntry(ident, false, true); - } - } - if (rowIndex >= 1000) { - egw.debug("error", "Problem in autorepeat fallback: too many rows for '%s'. Use a nextmatch, or start debugging.", value); - } - return; - } - } - }; - et2_filteredNodeIterator(this.lastRowNode, check, this); - cont = false; - content.perspectiveData = currentPerspective; - } - else { - // No more rows, stop - break; - } - } - } - if (rowIndex <= rowData.length - 1) { - // No auto-repeat - this.lastRowNode = null; - } - } - _fillCells(cells, columns, rows) { - const h = cells.length; - const w = (h > 0) ? cells[0].length : 0; - const currentPerspective = jQuery.extend({}, this.getArrayMgr("content").perspectiveData); - // Read the elements inside the columns - let x = 0; - et2_filteredNodeIterator(columns, function (node, nodeName) { - function _readColNode(node, nodeName) { - if (y >= h) { - this.egw().debug("warn", "Skipped grid cell in column, '" + - nodeName + "'"); - return; - } - const cell = this._getCell(cells, x, y); - // Read the span value of the element - if (node.getAttribute("span")) { - cell.rowSpan = node.getAttribute("span"); - } - else { - cell.rowSpan = cell.colData["span"]; - cell.autoRowSpan = true; - } - if (cell.rowSpan == "all") { - cell.rowSpan = cells.length; - } - const span = cell.rowSpan = this._forceNumber(cell.rowSpan); - // Create the widget - const widget = this.createElementFromNode(node, nodeName); - // Fill all cells the widget is spanning - for (let i = 0; i < span && y < cells.length; i++, y++) { - this._getCell(cells, x, y).widget = widget; - } - } - // If the node is a column, create the widgets which belong into - // the column - var y = 0; - if (nodeName == "column") { - et2_filteredNodeIterator(node, _readColNode, this); - } - else { - _readColNode.call(this, node, nodeName); - } - x++; - }, this); - // Read the elements inside the rows - var y = 0; - x = 0; - let readRowNode; - let nm = false; - var widget = this; - while (!nm && widget != this.getRoot()) { - nm = (widget.getType() == 'nextmatch'); - widget = widget.getParent(); - } - et2_filteredNodeIterator(rows, function (node, nodeName) { - readRowNode = function _readRowNode(node, nodeName) { - if (x >= w) { - if (nodeName != "description") { - // Only notify it skipping other than description, - // description used to pad - this.egw().debug("warn", "Skipped grid cell in row, '" + - nodeName + "'"); - } - return; - } - let cell = this._getCell(cells, x, y); - // Read the span value of the element - if (node.getAttribute("span")) { - cell.colSpan = node.getAttribute("span"); - } - else { - cell.colSpan = cell.rowData["span"]; - cell.autoColSpan = true; - } - if (cell.colSpan == "all") { - cell.colSpan = cells[y].length; - } - const span = cell.colSpan = this._forceNumber(cell.colSpan); - // Read the align value of the element - if (node.getAttribute("align")) { - cell.align = node.getAttribute("align"); - } - // store id of nextmatch-*headers, so it is available for disabled widgets, which get not instanciated - if (nodeName.substr(0, 10) == 'nextmatch-') { - cell.nm_id = node.getAttribute('id'); - } - // Apply widget's class to td, for backward compatability - if (node.getAttribute("class")) { - cell.class += (cell.class ? " " : "") + node.getAttribute("class"); - } - // Create the element - if (!cell.disabled || cell.disabled && typeof cell.disabled === 'string') { - //Skip if it is a nextmatch while the nextmatch handles row adjustment by itself - if (!nm) { - // Adjust for the row - const mgrs = this.getArrayMgrs(); - for (let name in mgrs) { - this.getArrayMgr(name).perspectiveData.row = y; - } - if (this._getCell(cells, x, y).rowData.id) { - this._getCell(cells, x, y).rowData.id = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.id); - } - if (this._getCell(cells, x, y).rowData.class) { - this._getCell(cells, x, y).rowData.class = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.class); - } - } - if (!nm && typeof cell.disabled === 'string') { - cell.disabled = this.getArrayMgr("content").parseBoolExpression(cell.disabled); - } - if (nm || !cell.disabled) { - var widget = this.createElementFromNode(node, nodeName); - } - } - // Fill all cells the widget is spanning - for (let i = 0; i < span && x < cells[y].length; i++, x++) { - cell = this._getCell(cells, x, y); - if (cell.widget == null) { - cell.widget = widget; - } - else { - throw ("Grid cell collision, two elements " + - "defined for cell (" + x + "," + y + ")!"); - } - } - }; - // If the node is a row, create the widgets which belong into - // the row - x = 0; - if (this.lastRowNode && node == this.lastRowNode) { - return; - } - if (nodeName == "row") { - // Adjust for the row - for (var name in this.getArrayMgrs()) { - //this.getArrayMgr(name).perspectiveData.row = y; - } - let cell = this._getCell(cells, x, y); - if (cell.rowData.id) { - this.getArrayMgr("content").expandName(cell.rowData.id); - } - // If row disabled, just skip it - let disabled = false; - if (node.getAttribute("disabled") == "1") { - disabled = true; - } - if (!disabled) { - et2_filteredNodeIterator(node, readRowNode, this); - } - } - else { - readRowNode.call(this, node, nodeName); - } - y++; - }, this); - // Extra content rows - for (y; y < h; y++) { - x = 0; - et2_filteredNodeIterator(this.lastRowNode, readRowNode, this); - } - // Reset - for (var name in this.getArrayMgrs()) { - this.getArrayMgr(name).perspectiveData = currentPerspective; - } - } - _expandLastCells(_cells) { - const h = _cells.length; - const w = (h > 0) ? _cells[0].length : 0; - // Determine the last cell in each row and expand its span value if - // the span has not been explicitly set. - for (var y = 0; y < h; y++) { - for (var x = w - 1; x >= 0; x--) { - var cell = _cells[y][x]; - if (cell.widget != null) { - if (cell.autoColSpan) { - cell.colSpan = w - x; - } - break; - } - } - } - // Determine the last cell in each column and expand its span value if - // the span has not been explicitly set. - for (var x = 0; x < w; x++) { - for (var y = h - 1; y >= 0; y--) { - var cell = _cells[y][x]; - if (cell.widget != null) { - if (cell.autoRowSpan) { - cell.rowSpan = h - y; - } - break; - } - } - } - } - _createNamespace() { - return true; - } - /** - * As the does not fit very well into the default widget structure, we're - * overwriting the loadFromXML function and doing a two-pass reading - - * in the first step the - * - * @param {object} _node xml node to process - */ - loadFromXML(_node) { - // Keep the node for later changing / reloading - this.template_node = _node; - // Get the columns and rows tag - const rowsElems = et2_directChildrenByTagName(_node, "rows"); - const columnsElems = et2_directChildrenByTagName(_node, "columns"); - if (rowsElems.length == 1 && columnsElems.length == 1) { - const columns = columnsElems[0]; - const rows = rowsElems[0]; - const colData = []; - const rowData = []; - // Fetch the column and row data - this._fetchRowColData(columns, rows, colData, rowData); - // Initialize the cells - const cells = this._initCells(colData, rowData); - // Create the widgets inside the cells and read the span values - this._fillCells(cells, columns, rows); - // Expand the span values of the last cells - this._expandLastCells(cells); - // Create the table rows - this.createTableFromCells(cells, colData, rowData); - } - else { - throw ("Error while parsing grid, none or multiple rows or columns tags!"); - } - } - createTableFromCells(_cells, _colData, _rowData) { - this.managementArray = []; - this.cells = _cells; - this.colData = _colData; - this.rowData = _rowData; - // Set the rowCount and columnCount variables - const h = this.rowCount = _cells.length; - const w = this.columnCount = (h > 0) ? _cells[0].length : 0; - // Create the table rows. - for (let y = 0; y < h; y++) { - let parent = this.tbody; - switch (this.rowData[y]["part"]) { - case 'header': - if (!this.tbody.children().length && !this.tfoot.children().length) { - parent = this.thead; - } - break; - case 'footer': - if (!this.tbody.children().length) { - parent = this.tfoot; - } - break; - } - const tr = jQuery(document.createElement("tr")).appendTo(parent) - .addClass(this.rowData[y]["class"]); - if (this.rowData[y].disabled) { - tr.hide(); - } - if (this.rowData[y].height != "auto") { - tr.height(this.rowData[y].height); - } - if (this.rowData[y].valign) { - tr.attr("valign", this.rowData[y].valign); - } - if (this.rowData[y].id) { - tr.attr("id", this.rowData[y].id); - } - // Create the cells. x is incremented by the colSpan value of the - // cell. - for (let x = 0; x < w;) { - // Fetch a cell from the cells - const cell = this._getCell(_cells, x, y); - if (cell.td == null && cell.widget != null) { - // Create the cell - const td = jQuery(document.createElement("td")).appendTo(tr) - .addClass(cell["class"]); - if (cell.disabled) { - td.hide(); - cell.widget.options.disabled = cell.disabled; - } - if (cell.width != "auto") { - td.width(cell.width); - } - if (cell.align) { - td.attr("align", cell.align); - } - // Add the entry for the widget to the management array - this.managementArray.push({ - "cell": td[0], - "widget": cell.widget, - "disabled": cell.disabled - }); - // Set the span values of the cell - const cs = (x == w - 1) ? w - x : Math.min(w - x, cell.colSpan); - const rs = (y == h - 1) ? h - y : Math.min(h - y, cell.rowSpan); - // Set the col and row span values - if (cs > 1) { - td.attr("colspan", cs); - } - if (rs > 1) { - td.attr("rowspan", rs); - } - // Assign the td to the cell - for (let sx = x; sx < x + cs; sx++) { - for (let sy = y; sy < y + rs; sy++) { - this._getCell(_cells, sx, sy).td = td; - } - } - x += cell.colSpan; - } - else { - x++; - } - } - } - } - getDOMNode(_sender) { - // If the parent class functions are asking for the DOM-Node, return the - // outer table. - if (_sender == this || typeof _sender == 'undefined') { - return this.wrapper != null ? this.wrapper[0] : this.table[0]; - } - // Check whether the _sender object exists inside the management array - for (let i = 0; i < this.managementArray.length; i++) { - if (this.managementArray[i].widget == _sender) { - return this.managementArray[i].cell; - } - } - return null; - } - isInTree(_sender) { - let vis = true; - if (typeof _sender != "undefined" && _sender != this) { - vis = false; - // Check whether the _sender object exists inside the management array - for (let i = 0; i < this.managementArray.length; i++) { - if (this.managementArray[i].widget == _sender) { - vis = !(typeof this.managementArray[i].disabled === 'boolean' ? - this.managementArray[i].disabled : - this.getArrayMgr("content").parseBoolExpression(this.managementArray[i].disabled)); - break; - } - } - } - return super.isInTree(this, vis); - } - /** - * Set the overflow attribute - * - * Grid needs special handling because HTML tables don't do overflow. We - * create a wrapper DIV to handle it. - * No value or default visible needs no wrapper, as table is always overflow visible. - * - * @param {string} _value Overflow value, must be a valid CSS overflow value, default 'visible' - */ - set_overflow(_value) { - let wrapper = this.wrapper || this.table.parent('[id$="_grid_wrapper"]'); - this.overflow = _value; - if (wrapper.length == 0 && _value && _value !== 'visible') { - this.wrapper = wrapper = this.table.wrap('
      ').parent(); - if (this.height) { - wrapper.css('height', this.height); - } - } - wrapper.css('overflow', _value); - if (wrapper.length && (!_value || _value === 'visible')) { - this.table.unwrap(); - } - } - /** - * Change the content for the grid, and re-generate its contents. - * - * Changing the content does not allow changing the structure of the grid, - * as that is loaded from the template file. The rows and widgets inside - * will be re-created (including auto-repeat). - * - * @param {Object} _value New data for the grid - * @param {Object} [_value.content] New content - * @param {Object} [_value.sel_options] New select options - * @param {Object} [_value.readonlys] New read-only values - */ - set_value(_value) { - // Destroy children, empty grid - for (let i = 0; i < this.managementArray.length; i++) { - const cell = this.managementArray[i]; - if (cell.widget) { - cell.widget.destroy(); - } - } - this.managementArray = []; - this.thead.empty(); - this.tfoot.empty(); - this.tbody.empty(); - // Update array managers - for (let key in _value) { - this.getArrayMgr(key).data = _value[key]; - } - // Rebuild grid - this.loadFromXML(this.template_node); - // New widgets need to finish - let promises = []; - this.loadingFinished(promises); - } - /** - * Sortable allows you to reorder grid rows using the mouse. - * The new order is returned as part of the value of the - * grid, in 'sort_order'. - * - * @param {boolean|function} sortable Callback or false to disable - */ - set_sortable(sortable) { - const $node = jQuery(this.getDOMNode()); - if (!sortable) { - $node.sortable("destroy"); - return; - } - // Make sure rows have IDs, so sortable has something to return - jQuery('tr', this.tbody).each(function (index) { - const $this = jQuery(this); - // Header does not participate in sorting - if ($this.hasClass('th')) - return; - // If row doesn't have an ID, assign the index as ID - if (!$this.attr("id")) - $this.attr("id", index); - }); - const self = this; - // Set up sortable - $node.sortable({ - // Header does not participate in sorting - items: "> tbody > tr:not(.th)", - distance: 15, - cancel: this.options.sortable_cancel, - placeholder: this.options.sortable_placeholder, - containment: this.options.sortable_containment, - connectWith: this.options.sortable_connectWith, - update: function (event, ui) { - self.egw().json(sortable, [ - self.getInstanceManager().etemplate_exec_id, - $node.sortable("toArray"), - self.id - ], null, self, true).sendRequest(); - }, - receive: function (event, ui) { - if (typeof self.sortable_recieveCallback == 'function') { - self.sortable_recieveCallback.call(self, event, ui, self.id); - } - }, - start: function (event, ui) { - if (typeof self.options.sortable_startCallback == 'function') { - self.options.sortable_startCallback.call(self, event, ui, self.id); - } - } - }); - } - /** - * Override parent to apply actions on each row - * - * @param {array} actions [ {ID: attributes..}+] as for set_actions - */ - _link_actions(actions) { - // Get the top level element for the tree - // get appObjectManager for the actual app, it might not always be the current app(e.g. running app content under admin tab) - // @ts-ignore - let objectManager = egw_getAppObjectManager(true, this.getInstanceManager().app); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; - let widget_object = objectManager.getObjectById(this.id); - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = objectManager.insertObject(false, new egwActionObject(this.id, objectManager, new et2_action_object_impl(this).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); - } - // Delete all old objects - widget_object.clear(); - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - const action_links = this._get_action_links(actions); - // Deal with each row in tbody, ignore action-wise rows in thead or tfooter for now - let i = 0, r = 0; - for (; i < this.rowData.length; i++) { - if (this.rowData[i].part != 'body') - continue; - const content = this.getArrayMgr('content').getEntry(i); - if (content) { - // Add a new action object to the object manager - const row = jQuery('tr', this.tbody)[r]; - const aoi = new et2_action_object_impl(this, row).getAOI(); - const obj = widget_object.addObject(content.id || "row_" + r, aoi); - // Set the data to the content so it's available for the action - obj.data = content; - obj.updateActionLinks(action_links); - } - r++; - } - } - /** - * Code for implementing et2_IDetachedDOM - * This doesn't need to be implemented. - * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - } - getDetachedNodes() { - return [this.getDOMNode()]; - } - setDetachedAttributes(_nodes, _values) { - } - /** - * Generates nextmatch column name for headers in a grid - * - * Implemented here as default implementation in et2_externsion_nextmatch - * only considers children, but grid does NOT instanciate disabled rows as children. - * - * @return {string} - */ - _getColumnName() { - const ids = []; - for (let r = 0; r < this.cells.length; ++r) { - const cols = this.cells[r]; - for (let c = 0; c < cols.length; ++c) { - if (cols[c].nm_id) - ids.push(cols[c].nm_id); - } - } - return ids.join('_'); - } - resize(_height) { - if (typeof this.options != 'undefined' && _height - && typeof this.options.resize_ratio != 'undefined' && this.options.resize_ratio) { - // apply the ratio - _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height; - if (_height != 0) { - if (this.wrapper) { - this.wrapper.height(this.wrapper.height() + _height); - } - else { - this.table.height(this.table.height() + _height); - } - } - } - } - /** - * Get a dummy row object containing all widget of a row - * - * This is only a temp. solution until rows are implemented as eT2 containers and - * _sender.getParent() will return a real row container. - * - * @deprecated Not used? Remove this if you find something using it - * - * @param {et2_widget} _sender - * @returns {Array|undefined} - */ - getRow(_sender) { - if (!_sender || !this.cells) - return; - for (let r = 0; r < this.cells.length; ++r) { - const row = this.cells[r]; - for (var c = 0; c < row.length; ++c) { - if (!row[c].widget) - continue; - let found = row[c].widget === _sender; - if (!found) - row[c].widget.iterateOver(function (_widget) { if (_widget === _sender) - found = true; }); - if (found) { - // return a fake row object allowing to iterate over it's children - const row_obj = new et2_widget(this, {}); - for (var c = 0; c < row.length; ++c) { - if (row[c].widget) - row_obj.addChild(row[c].widget); - } - row_obj.isInTree = jQuery.proxy(this.isInTree, this); - // we must not free the children! - row_obj.destroy = function () { - // @ts-ignore - delete row_obj._children; - }; - return row_obj; - } - } - } - } - /** - * Needed for the align interface, but we're not doing anything with it... - */ - get_align() { - return ""; - } -} -et2_grid._attributes = { - // Better to use CSS, no need to warn about it - "border": { - "ignore": true - }, - "align": { - "name": "Align", - "type": "string", - "default": "left", - "description": "Position of this element in the parent hbox" - }, - "spacing": { - "ignore": true - }, - "padding": { - "ignore": true - }, - "sortable": { - "name": "Sortable callback", - "type": "string", - "default": et2_no_init, - "description": "PHP function called when user sorts the grid. Setting this enables sorting the grid rows. The callback will be passed the ID of the grid and the new order of the rows." - }, - sortable_containment: { - name: "Sortable bounding area", - type: "string", - default: "", - description: "Defines bounding area for sortable items" - }, - sortable_connectWith: { - name: "Sortable connectWith element", - type: "string", - default: "", - description: "Defines other sortable areas that should be connected to sort list" - }, - sortable_placeholder: { - name: "Sortable placeholder", - type: "string", - default: "", - description: "Defines sortable placeholder" - }, - sortable_cancel: { - name: "Sortable cancel class", - type: "string", - default: "", - description: "Defines sortable cancel which prevents sorting the matching element" - }, - sortable_recieveCallback: { - name: "Sortable receive callback", - type: "js", - default: et2_no_init, - description: "Defines sortable receive callback function" - }, - sortable_startCallback: { - name: "Sortable start callback", - type: "js", - default: et2_no_init, - description: "Defines sortable start callback function" - } -}; -et2_register_widget(et2_grid, ["grid"]); -//# sourceMappingURL=et2_widget_grid.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_groupbox.js b/api/js/etemplate/et2_widget_groupbox.js deleted file mode 100644 index 721bb51148..0000000000 --- a/api/js/etemplate/et2_widget_groupbox.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Groupbox object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2012 - */ -/*egw:uses - et2_core_baseWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_baseWidget } from "./et2_core_baseWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -/** - * Class which implements the groupbox tag - * - * @augments et2_baseWidget - */ -export class et2_groupbox extends et2_baseWidget { - /** - * Constructor - * - * @memberOf et2_groupbox - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_groupbox._attributes, _child || {})); - this.setDOMNode(document.createElement("fieldset")); - } -} -et2_register_widget(et2_groupbox, ["groupbox"]); -/** - * @augments et2_baseWidget - */ -export class et2_groupbox_legend extends et2_baseWidget { - /** - * Constructor - * - * @memberOf et2_groupbox_legend - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_groupbox_legend._attributes, _child || {})); - let legend = jQuery(document.createElement("legend")).text(this.options.label); - this.setDOMNode(legend[0]); - } -} -et2_groupbox_legend._attributes = { - "label": { - "name": "Label", - "type": "string", - "default": "", - "description": "Label for group box", - "translate": true - } -}; -et2_register_widget(et2_groupbox_legend, ["caption"]); -//# sourceMappingURL=et2_widget_groupbox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_hbox.js b/api/js/etemplate/et2_widget_hbox.js deleted file mode 100644 index 578b4f387c..0000000000 --- a/api/js/etemplate/et2_widget_hbox.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * EGroupware eTemplate2 - JS HBox object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; -*/ -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_register_widget } from "./et2_core_widget"; -import { et2_baseWidget } from "./et2_core_baseWidget"; -import { et2_grid } from "./et2_widget_grid"; -import { et2_filteredNodeIterator } from "./et2_core_xml"; -import { et2_cloneObject } from "./et2_core_common"; -import { et2_IAligned } from "./et2_core_interfaces"; -/** - * Class which implements hbox tag - * - * @augments et2_baseWidget - */ -export class et2_hbox extends et2_baseWidget { - /** - * Constructor - * - * @memberOf et2_hbox - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_hbox._attributes, _child || {})); - this.alignData = { - "hasAlign": false, - "hasLeft": false, - "hasCenter": false, - "hasRight": false, - "lastAlign": "left" - }; - this.leftDiv = null; - this.rightDiv = null; - this.div = null; - this.leftDiv = null; - this.rightDiv = null; - this.div = jQuery(document.createElement("div")) - .addClass("et2_" + super.getType()) - .addClass("et2_box_widget"); - super.setDOMNode(this.div[0]); - } - _createNamespace() { - return true; - } - _buildAlignCells() { - if (this.alignData.hasAlign) { - // Check whether we have more than one type of align - let mto = (this.alignData.hasLeft && this.alignData.hasRight) || - (this.alignData.hasLeft && this.alignData.hasCenter) || - (this.alignData.hasCenter && this.alignData.hasRight); - if (!mto) { - // If there is only one type of align, we simply have to set - // the align of the top container - if (this.alignData.lastAlign != "left") { - this.div.addClass("et2_hbox_al_" + this.alignData.lastAlign); - } - } - else { - // Create an additional container for elements with align type - // "right" - if (this.alignData.hasRight) { - this.rightDiv = jQuery(document.createElement("div")) - .addClass("et2_hbox_right") - .appendTo(this.div); - } - // Create an additional container for elements with align type - // left, as the top container is used for the centered elements - if (this.alignData.hasCenter) { - // Create the left div if an element is centered - this.leftDiv = jQuery(document.createElement("div")) - .addClass("et2_hbox_left") - .appendTo(this.div); - this.div.addClass("et2_hbox_al_center"); - } - } - } - } - /** - * The overwritten loadFromXML function checks whether any child element has - * a special align value. - * - * @param {object} _node - */ - loadFromXML(_node) { - // Check whether any child node has an alignment tag - et2_filteredNodeIterator(_node, function (_node) { - let align = _node.getAttribute("align"); - if (!align) { - align = "left"; - } - if (align != "left") { - this.alignData.hasAlign = true; - } - this.alignData.lastAlign = align; - switch (align) { - case "left": - this.alignData.hasLeft = true; - break; - case "right": - this.alignData.hasRight = true; - break; - case "center": - this.alignData.hasCenter = true; - break; - } - }, this); - // Build the align cells - this._buildAlignCells(); - // Load the nodes as usual - super.loadFromXML(_node); - } - assign(_obj) { - // Copy the align data and the cells from the object which should be - // assigned - this.alignData = et2_cloneObject(_obj.alignData); - this._buildAlignCells(); - // Call the inherited assign function - super.assign(_obj); - } - getDOMNode(_sender) { - // Return a special align container if this hbox needs it - if (_sender != this && this.alignData.hasAlign) { - // Check whether we've create a special container for the widget - let align = (_sender.implements(et2_IAligned) ? - _sender.get_align() : "left"); - if (align == "left" && this.leftDiv != null) { - return this.leftDiv[0]; - } - if (align == "right" && this.rightDiv != null) { - return this.rightDiv[0]; - } - } - // Normally simply return the hbox-div - return super.getDOMNode(_sender); - } - /** - * Tables added to the root node need to be inline instead of blocks - * - * @param {et2_widget} child child-widget to add - */ - addChild(child) { - super.addChild(child); - if (child.instanceOf && child.instanceOf(et2_grid) && this.isAttached() || child._type == 'et2_grid' && this.isAttached()) { - jQuery(child.getDOMNode(child)).css("display", "inline-table"); - } - } -} -et2_register_widget(et2_hbox, ["hbox"]); -//# sourceMappingURL=et2_widget_hbox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_historylog.js b/api/js/etemplate/et2_widget_historylog.js deleted file mode 100644 index 5944da21d3..0000000000 --- a/api/js/etemplate/et2_widget_historylog.js +++ /dev/null @@ -1,572 +0,0 @@ -/** - * EGroupware eTemplate2 - JS History log - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright 2012 Nathan Gray - */ -import { et2_createWidget, et2_register_widget, et2_registry } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { et2_dataview } from "./et2_dataview"; -import { et2_dataview_column } from "./et2_dataview_model_columns"; -import { et2_dataview_controller } from "./et2_dataview_controller"; -import { et2_diff } from "./et2_widget_diff"; -import { et2_IDetachedDOM } from "./et2_core_interfaces"; -import { et2_dynheight } from "./et2_widget_dynheight"; -import { et2_selectbox } from "./et2_widget_selectbox"; -/** - * eTemplate history log widget displays a list of changes to the current record. - * The widget is encapsulated, and only needs the record's ID, and a map of - * fields:widgets for display. - * - * It defers its initialization until the tab that it's on is selected, to avoid - * wasting time if the user never looks at it. - * - * @augments et2_valueWidget - */ -export class et2_historylog extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_historylog - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_historylog._attributes, _child || {})); - this.div = jQuery(document.createElement("div")) - .addClass("et2_historylog"); - this.innerDiv = jQuery(document.createElement("div")) - .appendTo(this.div); - } - set_status_id(_new_id) { - this.options.status_id = _new_id; - } - doLoadingFinished() { - super.doLoadingFinished(); - // Find the tab - let tab = this.get_tab_info(); - if (tab) { - // Bind the action to when the tab is selected - const handler = function (e) { - e.data.div.unbind("click.history"); - // Bind on click tap, because we need to update history size - // after a rezise happend and history log was not the active tab - e.data.div.bind("click.history", { "history": e.data.history, div: tab.flagDiv }, function (e) { - if (e.data.history && e.data.history.dynheight) { - e.data.history.dynheight.update(function (_w, _h) { - e.data.history.dataview.resize(_w, _h); - }); - } - }); - if (typeof e.data.history.dataview == "undefined") { - e.data.history.finishInit(); - if (e.data.history.dynheight) { - e.data.history.dynheight.update(function (_w, _h) { - e.data.history.dataview.resize(_w, _h); - }); - } - } - }; - tab.flagDiv.bind("click.history", { "history": this, div: tab.flagDiv }, handler); - // Display if history tab is selected - if (tab.contentDiv.is(':visible') && typeof this.dataview == 'undefined') { - tab.flagDiv.trigger("click.history"); - } - } - else { - this.finishInit(); - } - return true; - } - _createNamespace() { - return true; - } - /** - * Finish initialization which was skipped until tab was selected - */ - finishInit() { - // No point with no ID - if (!this.options.value || !this.options.value.id) { - return; - } - this._filters = { - record_id: this.options.value.id, - appname: this.options.value.app, - get_rows: this.options.get_rows - }; - // Warn if status_id is the same as history id, that causes overlap and missing labels - if (this.options.status_id === this.id) { - this.egw().debug("warn", "status_id attribute should not be the same as historylog ID"); - } - // Create the dynheight component which dynamically scales the inner - // container. - this.div.parentsUntil('.et2_tabs').height('100%'); - const parent = this.get_tab_info(); - this.dynheight = new et2_dynheight(parent ? parent.contentDiv : this.div.parent(), this.innerDiv, 250); - // Create the outer grid container - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - const dataview_columns = []; - let _columns = typeof this.options.columns === "string" ? - this.options.columns.split(',') : this.options.columns; - for (var i = 0; i < et2_historylog.columns.length; i++) { - dataview_columns[i] = { - "id": et2_historylog.columns[i].id, - "caption": et2_historylog.columns[i].caption, - "width": et2_historylog.columns[i].width, - "visibility": _columns.indexOf(et2_historylog.columns[i].id) < 0 ? - et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE : et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE - }; - } - this.dataview.setColumns(dataview_columns); - // Create widgets for columns that stay the same, and set up varying widgets - this.createWidgets(); - // Create the gridview controller - const linkCallback = function () { - }; - this.controller = new et2_dataview_controller(null, this.dataview.grid); - this.controller.setContext(this); - this.controller.setDataProvider(this); - this.controller.setLinkCallback(linkCallback); - this.controller.setRowCallback(this.rowCallback); - this.controller.setActionObjectManager(null); - const total = typeof this.options.value.total !== "undefined" ? - this.options.value.total : 0; - // This triggers an invalidate, which updates the grid - this.dataview.grid.setTotalCount(total); - // Insert any data sent from server, so invalidate finds data already - if (this.options.value.rows && this.options.value.num_rows) { - this.controller.loadInitialData(this.options.value.dataStorePrefix, this.options.value.row_id, this.options.value.rows); - // Remove, to prevent duplication - delete this.options.value.rows; - // This triggers an invalidate, which updates the grid - this.dataview.grid.setTotalCount(total); - } - else { - // Trigger the initial update - this.controller.update(); - } - // Write something inside the column headers - for (var i = 0; i < et2_historylog.columns.length; i++) { - jQuery(this.dataview.getHeaderContainerNode(i)).text(et2_historylog.columns[i].caption); - } - // Register a resize callback - jQuery(window).on('resize.' + this.options.value.app + this.options.value.id, function () { - if (this && typeof this.dynheight != 'undefined') - this.dynheight.update(function (_w, _h) { - this.dataview.resize(_w, _h); - }.bind(this)); - }.bind(this)); - } - /** - * Destroys all - */ - destroy() { - // Unbind, if bound - if (this.options.value && !this.options.value.id) { - jQuery(window).off('.' + this.options.value.app + this.options.value.id); - } - // Free the widgets - for (let i = 0; i < et2_historylog.columns.length; i++) { - if (et2_historylog.columns[i].widget) - et2_historylog.columns[i].widget.destroy(); - } - for (let key in this.fields) { - this.fields[key].widget.destroy(); - } - // Free the grid components - if (this.dataview) - this.dataview.destroy(); - if (this.controller) - this.controller.destroy(); - if (this.dynheight) - this.dynheight.destroy(); - super.destroy(); - } - /** - * Create all needed widgets for new / old values - */ - createWidgets() { - // Constant widgets - first 3 columns - for (let i = 0; i < et2_historylog.columns.length; i++) { - if (et2_historylog.columns[i].widget_type) { - // Status ID is allowed to be remapped to something else. Only affects the widget ID though - var attrs = { 'readonly': true, 'id': (i == et2_historylog.FIELD ? this.options.status_id : et2_historylog.columns[i].id) }; - et2_historylog.columns[i].widget = et2_createWidget(et2_historylog.columns[i].widget_type, attrs, this); - et2_historylog.columns[i].widget.transformAttributes(attrs); - et2_historylog.columns[i].nodes = jQuery(et2_historylog.columns[i].widget.getDetachedNodes()); - } - } - // Add in handling for links - if (typeof this.options.value['status-widgets']['~link~'] == 'undefined') { - et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['~link~'] = this.egw().lang('link'); - this.options.value['status-widgets']['~link~'] = 'link'; - } - // Add in handling for files - if (typeof this.options.value['status-widgets']['~file~'] == 'undefined') { - et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['~file~'] = this.egw().lang('File'); - this.options.value['status-widgets']['~file~'] = 'vfs'; - } - // Add in handling for user-agent & action - if (typeof this.options.value['status-widgets']['user_agent_action'] == 'undefined') { - et2_historylog.columns[et2_historylog.FIELD].widget.optionValues['user_agent_action'] = this.egw().lang('User-agent & action'); - } - // Per-field widgets - new value & old value - this.fields = {}; - let labels = et2_historylog.columns[et2_historylog.FIELD].widget.optionValues; - // Custom fields - Need to create one that's all read-only for proper display - let cf_widget = et2_createWidget('customfields', { 'readonly': true }, this); - cf_widget.loadFields(); - // Override this or it may damage the real values - cf_widget.getValue = function () { return null; }; - for (let key in cf_widget.widgets) { - // Add label - labels[cf_widget.prefix + key] = cf_widget.options.customfields[key].label; - // If it doesn't support detached nodes, just treat it as text - if (cf_widget.widgets[key].getDetachedNodes) { - var nodes = cf_widget.widgets[key].getDetachedNodes(); - for (var i = 0; i < nodes.length; i++) { - if (nodes[i] == null) - nodes.splice(i, 1); - } - // Save to use for each row - this.fields[cf_widget.prefix + key] = { - attrs: cf_widget.widgets[key].options, - widget: cf_widget.widgets[key], - nodes: jQuery(nodes) - }; - } - } - // Add all cf labels - et2_historylog.columns[et2_historylog.FIELD].widget.set_select_options(labels); - // From app - for (var key in this.options.value['status-widgets']) { - let attrs = jQuery.extend({ 'readonly': true, 'id': key }, this.getArrayMgr('modifications').getEntry(key)); - const field = attrs.type || this.options.value['status-widgets'][key]; - const options = null; - const widget = this._create_widget(key, field, attrs, options); - if (widget === null) { - continue; - } - if (widget.instanceOf(et2_selectbox)) - widget.options.multiple = true; - widget.transformAttributes(attrs); - // Save to use for each row - let nodes = widget._children.length ? [] : jQuery(widget.getDetachedNodes()); - for (let i = 0; i < widget._children.length; i++) { - // @ts-ignore - nodes.push(jQuery(widget._children[i].getDetachedNodes())); - } - this.fields[key] = { - attrs: attrs, - widget: widget, - nodes: nodes - }; - } - // Widget for text diffs - const diff = et2_createWidget('diff', {}, this); - this.diff = { - // @ts-ignore - widget: diff, - nodes: jQuery(diff.getDetachedNodes()) - }; - } - _create_widget(key, field, attrs, options) { - let widget = null; - // If field has multiple parts (is object) and isn't an obvious select box - if (typeof field === 'object') { - // Check for multi-part statuses needing multiple widgets - let need_box = false; //!this.getArrayMgr('sel_options').getEntry(key); - for (let j in field) { - // Require widget to be a widget, to avoid invalid widgets - // (and template, which is a widget and an infolog todo status) - if (et2_registry[field[j]] && ['template'].indexOf(field[j]) < 0) // && (et2_registry[field[j]].prototype.instanceOf(et2_valueWidget)) - { - need_box = true; - break; - } - } - if (need_box) { - // Multi-part value needs multiple widgets - widget = et2_createWidget('vbox', attrs, this); - for (var i in field) { - let type = field[i]; - const child_attrs = jQuery.extend({}, attrs); - if (typeof type === 'object') { - child_attrs['select_options'] = field[i]; - type = 'select'; - } - else { - delete child_attrs['select_options']; - } - child_attrs.id = i; - const child = this._create_widget(i, type, child_attrs, options); - widget.addChild(child); - child.transformAttributes(child_attrs); - } - } - else { - attrs['select_options'] = field; - } - } - // Check for options after the type, ex: link-entry:infolog - else if (field.indexOf(':') > 0) { - var options = field.split(':'); - field = options.shift(); - } - if (widget === null) { - widget = et2_createWidget(typeof field === 'string' ? field : 'select', attrs, this); - } - if (!widget.instanceOf(et2_IDetachedDOM)) { - this.egw().debug("warn", this, "Invalid widget " + field + " for " + key + ". Status widgets must implement et2_IDetachedDOM."); - return null; - } - // Parse / set legacy options - if (options) { - const mgr = this.getArrayMgr("content"); - let legacy = widget.constructor.legacyOptions || []; - for (let i = 0; i < options.length && i < legacy.length; i++) { - // Not set - if (options[i] === "") - continue; - const attr = widget.attributes[legacy[i]]; - let attrValue = options[i]; - // If the attribute is marked as boolean, parse the - // expression as bool expression. - if (attr.type === "boolean") { - attrValue = mgr.parseBoolExpression(attrValue); - } - else { - attrValue = mgr.expandName(attrValue); - } - attrs[legacy[i]] = attrValue; - if (typeof widget['set_' + legacy[i]] === 'function') { - widget['set_' + legacy[i]].call(widget, attrValue); - } - else { - widget.options[legacy[i]] = attrValue; - } - } - } - return widget; - } - getDOMNode(_sender) { - if (_sender == this) { - return this.div[0]; - } - for (let i = 0; i < et2_historylog.columns.length; i++) { - if (_sender == et2_historylog.columns[i].widget) { - return this.dataview.getHeaderContainerNode(i); - } - } - return null; - } - dataFetch(_queriedRange, _callback, _context) { - // Skip getting data if there's no ID - if (!this.value.id) - return; - // Set num_rows to fetch via nextmatch - if (this.options.value['num_rows']) - _queriedRange['num_rows'] = this.options.value['num_rows']; - const historylog = this; - // Pass the fetch call to the API - this.egw().dataFetch(this.getInstanceManager().etemplate_exec_id, _queriedRange, this._filters, this.id, function (_response) { - _callback.call(this, _response); - }, _context, []); - } - // Needed by interface - dataRegisterUID(_uid, _callback, _context) { - this.egw().dataRegisterUID(_uid, _callback, _context, this.getInstanceManager().etemplate_exec_id, this.id); - } - dataUnregisterUID(_uid, _callback, _context) { - // Needed by interface - } - /** - * The row callback gets called by the gridview controller whenever - * the actual DOM-Nodes for a node with the given data have to be - * created. - * - * @param {type} _data - * @param {type} _row - * @param {type} _idx - * @param {type} _entry - */ - rowCallback(_data, _row, _idx, _entry) { - let tr = _row.getDOMNode(); - jQuery(tr).attr("valign", "top"); - let row = this.dataview.rowProvider.getPrototype("default"); - let self = this; - jQuery("div", row).each(function (i) { - let nodes = []; - let widget = et2_historylog.columns[i].widget; - let value = _data[et2_historylog.columns[i].id]; - if (et2_historylog.OWNER === i && _data['share_email']) { - // Show share email instead of owner - widget = undefined; - value = _data['share_email']; - } - // Get widget from list, unless it needs a diff widget - if ((typeof widget == 'undefined' || widget == null) && typeof self.fields[_data.status] != 'undefined' && (i < et2_historylog.NEW_VALUE || - i >= et2_historylog.NEW_VALUE && (self.fields[_data.status].nodes || !self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.OLD_VALUE].id])))) { - widget = self.fields[_data.status].widget; - if (!widget._children.length) { - nodes = self.fields[_data.status].nodes.clone(); - } - for (var j = 0; j < widget._children.length; j++) { - // @ts-ignore - nodes.push(self.fields[_data.status].nodes[j].clone()); - if (widget._children[j].instanceOf(et2_diff)) { - self._spanValueColumns(jQuery(this)); - } - } - } - else if (widget) { - nodes = et2_historylog.columns[i].nodes.clone(); - } - else if (( - // Already parsed & cached - typeof _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] == "object" && - typeof _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] != "undefined" && - _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id] !== null) || // typeof null === 'object' - // Large old value - self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.OLD_VALUE].id]) || - // Large new value - self._needsDiffWidget(_data['status'], _data[et2_historylog.columns[et2_historylog.NEW_VALUE].id])) { - // Large text value - span both columns, and show a nice diff - let jthis = jQuery(this); - if (i === et2_historylog.NEW_VALUE) { - // Diff widget - widget = self.diff.widget; - nodes = self.diff.nodes.clone(); - if (widget) - widget.setDetachedAttributes(nodes, { - value: value, - label: jthis.parents("td").prev().text() - }); - self._spanValueColumns(jthis); - } - } - else { - // No widget fallback - display actual value - nodes = jQuery('').text(value === null ? '' : value); - } - if (widget) { - if (widget._children.length) { - // Multi-part values - const box = jQuery(widget.getDOMNode()).clone(); - for (var j = 0; j < widget._children.length; j++) { - const id = widget._children[j].id; - const widget_value = value ? value[id] || "" : ""; - widget._children[j].setDetachedAttributes(nodes[j], { value: widget_value }); - box.append(nodes[j]); - } - nodes = box; - } - else { - widget.setDetachedAttributes(nodes, { value: value }); - } - } - jQuery(this).append(nodes); - }); - jQuery(tr).append(row.children()); - return tr; - } - /** - * How to tell if the row needs a diff widget or not - * - * @param {string} columnName - * @param {string} value - * @returns {Boolean} - */ - _needsDiffWidget(columnName, value) { - if (typeof value !== "string" && value) { - this.egw().debug("warn", "Crazy diff value", value); - return false; - } - return value === '***diff***'; - } - /** - * Make a single row's new value cell span across both new value and old value - * columns. Used for diff widget. - * - * @param {jQuery} row jQuery wrapped row node - */ - _spanValueColumns(row) { - // Stretch column 4 - row.parents("td").attr("colspan", 2) - .css("border-right", "none"); - row.css("width", (this.dataview.getColumnMgr().getColumnWidth(et2_historylog.NEW_VALUE) + - this.dataview.getColumnMgr().getColumnWidth(et2_historylog.OLD_VALUE) - 10) + 'px'); - // Skip column 5 - row.parents("td").next().remove(); - } - resize(_height) { - if (typeof this.options != 'undefined' && _height - && typeof this.options.resize_ratio != 'undefined') { - // apply the ratio - _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height; - if (_height != 0) { - // 250px is the default value for history widget - // if it's not loaded yet and window is resized - // then add the default height with excess_height - if (this.div.height() == 0) - _height += 250; - this.div.height(this.div.height() + _height); - // trigger the history registered resize - // in order to update the height with new value - this.div.trigger('resize.' + this.options.value.app + this.options.value.id); - } - } - if (this.dynheight) { - this.dynheight.update(); - } - // Resize diff widgets to match new space - if (this.dataview) { - const columns = this.dataview.getColumnMgr(); - jQuery('.et2_diff', this.div).closest('.innerContainer') - .width(columns.getColumnWidth(et2_historylog.NEW_VALUE) + columns.getColumnWidth(et2_historylog.OLD_VALUE)); - } - } -} -et2_historylog._attributes = { - "value": { - "name": "Value", - "type": "any", - "description": "Object {app: ..., id: ..., status-widgets: {}} where status-widgets is a map of fields to widgets used to display those fields" - }, - "status_id": { - "name": "status_id", - "type": "string", - "default": "status", - "description": "The history widget is traditionally named 'status'. If you name another widget in the same template 'status', you can use this attribute to re-name the history widget. " - }, - "columns": { - "name": "columns", - "type": "string", - "default": "user_ts,owner,status,new_value,old_value", - "description": "Columns to display. Default is user_ts,owner,status,new_value,old_value" - }, - "get_rows": { - "name": "get_rows", - "type": "string", - "default": "EGroupware\\Api\\Storage\\History::get_rows", - "description": "Method to get rows" - } -}; -et2_historylog.legacyOptions = ["status_id"]; -et2_historylog.columns = [ - { 'id': 'user_ts', caption: 'Date', 'width': '120px', widget_type: 'date-time', widget: null, nodes: null }, - { 'id': 'owner', caption: 'User', 'width': '150px', widget_type: 'select-account', widget: null, nodes: null }, - { 'id': 'status', caption: 'Changed', 'width': '120px', widget_type: 'select', widget: null, nodes: null }, - { 'id': 'new_value', caption: 'New Value', 'width': '50%', widget: null, nodes: null }, - { 'id': 'old_value', caption: 'Old Value', 'width': '50%', widget: null, nodes: null } -]; -et2_historylog.TIMESTAMP = 0; -et2_historylog.OWNER = 1; -et2_historylog.FIELD = 2; -et2_historylog.NEW_VALUE = 3; -et2_historylog.OLD_VALUE = 4; -et2_register_widget(et2_historylog, ['historylog']); -//# sourceMappingURL=et2_widget_historylog.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_hrule.js b/api/js/etemplate/et2_widget_hrule.js deleted file mode 100644 index aa923cf40e..0000000000 --- a/api/js/etemplate/et2_widget_hrule.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * EGroupware eTemplate2 - JS HRule object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - et2_core_baseWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_baseWidget } from "./et2_core_baseWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -/** - * Class which implements the hrule tag - * - * @augments et2_baseWidget - */ -export class et2_hrule extends et2_baseWidget { - /** - * Constructor - * - * @memberOf et2_hrule - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_hrule._attributes, _child || {})); - this.setDOMNode(document.createElement("hr")); - } -} -et2_register_widget(et2_hrule, ["hrule"]); -//# sourceMappingURL=et2_widget_hrule.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_html.js b/api/js/etemplate/et2_widget_html.js deleted file mode 100644 index 3df3a8f426..0000000000 --- a/api/js/etemplate/et2_widget_html.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * EGroupware eTemplate2 - JS widget class containing raw HTML - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - jsapi.jsapi; // Needed for egw_seperateJavaScript - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; -*/ -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_no_init } from "./et2_core_common"; -/** - * @augments et2_valueWidget - */ -export class et2_html extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_html - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_html._attributes, _child || {})); - this.htmlNode = null; - // Allow no child widgets - this.supportedWidgetClasses = []; - this.htmlNode = jQuery(document.createElement("span")); - if (this.getType() == 'htmlarea') { - this.htmlNode.addClass('et2_textbox_ro'); - } - if (this.options.label) { - this.htmlNode.append('' + this.options.label + ''); - } - this.setDOMNode(this.htmlNode[0]); - } - loadContent(_data) { - // Create an object containg the given value and an empty js string - let html = { html: _data ? _data : '', js: '' }; - // Separate the javascript from the given html. The js code will be - // written to the previously created empty js string - egw_seperateJavaScript(html); - // Append the html to the parent element - if (this.options.label) { - this.htmlNode.append('' + this.options.label + ''); - } - this.htmlNode.append(html.html); - this.htmlNode.append(html.js); - } - set_value(_value) { - this.htmlNode.empty(); - this.loadContent(_value); - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes(_attrs) { - _attrs.push("value", "class"); - } - getDetachedNodes() { - return [this.htmlNode[0]]; - } - setDetachedAttributes(_nodes, _values) { - this.htmlNode = jQuery(_nodes[0]); - if (typeof _values['value'] !== 'undefined') { - this.set_value(_values['value']); - } - } -} -et2_html._attributes = { - 'label': { - 'default': "", - description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - ignore: false, - name: "Label", - translate: true, - type: "string" - }, - "needed": { - "ignore": true - }, - value: { - name: "Value", - description: "The value of the widget", - type: "html", - default: et2_no_init - } -}; -et2_register_widget(et2_html, ["html", "htmlarea_ro"]); -//# sourceMappingURL=et2_widget_html.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_htmlarea.js b/api/js/etemplate/et2_widget_htmlarea.js deleted file mode 100644 index 9d797c24af..0000000000 --- a/api/js/etemplate/et2_widget_htmlarea.js +++ /dev/null @@ -1,483 +0,0 @@ -/** - * EGroupware eTemplate2 - JS widget for HTML editing - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Hadi Nategh - * @copyright Hadi Nategh - * @version $Id$ - */ -/*egw:uses - jsapi.jsapi; // Needed for egw_seperateJavaScript - /vendor/tinymce/tinymce/tinymce.min.js; - et2_core_editableWidget; -*/ -import { et2_editableWidget } from "./et2_core_editableWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_register_widget, et2_createWidget } from "./et2_core_widget"; -import { et2_no_init } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -import "../../../vendor/tinymce/tinymce/tinymce.min.js"; -import { etemplate2 } from "./etemplate2"; -/** - * @augments et2_inputWidget - */ -export class et2_htmlarea extends et2_editableWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_htmlarea._attributes, _child || {})); - this.editor = null; - this.htmlNode = null; - this.editor = null; // TinyMce editor instance - this.supportedWidgetClasses = []; // Allow no child widgets - this.htmlNode = jQuery(document.createElement(this.options.readonly ? "div" : "textarea")) - .addClass('et2_textbox_ro'); - if (this.options.height) { - this.htmlNode.css('height', this.options.height); - } - this.setDOMNode(this.htmlNode[0]); - } - /** - * - * @returns {undefined} - */ - doLoadingFinished() { - super.doLoadingFinished(); - this.init_editor(); - return true; - } - init_editor() { - if (this.mode == 'ascii' || this.editor != null || this.options.readonly) - return; - let imageUpload; - let self = this; - if (this.options.imageUpload && this.options.imageUpload[0] !== '/' && this.options.imageUpload.substr(0, 4) != 'http') { - imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload") + - '&request_id=' + this.getInstanceManager().etemplate_exec_id + '&widget_id=' + this.options.imageUpload + '&type=htmlarea'; - imageUpload = imageUpload.substr(egw.webserverUrl.length + 1); - } - else if (imageUpload) { - imageUpload = this.options.imageUpload.substr(egw.webserverUrl.length + 1); - } - else { - imageUpload = egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_htmlarea_upload") + - '&request_id=' + this.getInstanceManager().etemplate_exec_id + '&type=htmlarea'; - } - // default settings for initialization - let settings = { - base_url: egw.webserverUrl + '/vendor/tinymce/tinymce', - target: this.htmlNode[0], - body_id: this.dom_id + '_htmlarea', - menubar: false, - statusbar: this.options.statusbar, - toolbar_mode: this.options.toolbar_mode, - branding: false, - resize: false, - height: this.options.height, - width: this.options.width, - end_container_on_empty_block: true, - mobile: { - theme: 'silver' - }, - formats: { - customparagraph: { block: 'p', styles: { "margin-block-start": "0px", "margin-block-end": "0px" } } - }, - min_height: 100, - convert_urls: false, - language: et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')], - language_url: egw.webserverUrl + '/api/js/tinymce/langs/' + et2_htmlarea.LANGUAGE_CODE[egw.preference('lang', 'common')] + '.js', - paste_data_images: true, - paste_filter_drop: true, - browser_spellcheck: true, - contextmenu: false, - images_upload_url: imageUpload, - file_picker_callback: jQuery.proxy(this._file_picker_callback, this), - images_upload_handler: this.options.images_upload_handler, - init_instance_callback: jQuery.proxy(this._instanceIsReady, this), - auto_focus: false, - valid_children: this.options.valid_children, - plugins: [ - "print searchreplace autolink directionality ", - "visualblocks visualchars image link media template fullscreen", - "codesample table charmap hr pagebreak nonbreaking anchor toc ", - "insertdatetime advlist lists textcolor wordcount imagetools ", - "colorpicker textpattern help paste code searchreplace tabfocus" - ], - toolbar: et2_htmlarea.TOOLBAR_SIMPLE, - block_formats: "Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;" + - "Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Custom Paragraph=customparagraph", - font_formats: "Andale Mono=andale mono,times;Arial=arial,helvetica," + - "sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book " + - "antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;" + - "Courier New=courier new,courier;Georgia=georgia,palatino;" + - "Helvetica=helvetica;Impact=impact,chicago;Segoe=segoe,segoe ui;Symbol=symbol;" + - "Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal," + - "monaco;Times New Roman=times new roman,times;Trebuchet " + - "MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;" + - "Wingdings=wingdings,zapf dingbats", - fontsize_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt', - setup: function (ed) { - ed.on('init', function () { - this.getDoc().body.style.fontSize = egw.preference('rte_font_size', 'common') - + egw.preference('rte_font_unit', 'common'); - this.getDoc().body.style.fontFamily = egw.preference('rte_font', 'common'); - }); - } - }; - // extend default settings with configured options and preferences - jQuery.extend(settings, this._extendedSettings()); - this.tinymce = tinymce.init(settings); - // make sure value gets set in case of widget gets loaded by delay like - // inside an inactive tabs - this.tinymce.then(function () { - self.set_value(self.htmlNode.val()); - self.resetDirty(); - if (self.editor && self.editor.editorContainer) { - self.editor.formatter.toggle(egw.preference('rte_formatblock', 'common')); - jQuery(self.editor.editorContainer).height(self.options.height); - jQuery(self.editor.iframeElement.contentWindow.document).on('dragenter', function () { - if (jQuery('#dragover-tinymce').length < 1) - jQuery("").appendTo('head'); - }); - } - }); - } - /** - * set disabled - * - * @param {type} _value - * @returns {undefined} - */ - set_disabled(_value) { - super.set_disabled(_value); - if (_value) { - jQuery(this.tinymce_container).css('display', 'none'); - } - else { - jQuery(this.tinymce_container).css('display', 'flex'); - } - } - set_readonly(_value) { - if (this.options.readonly === _value) - return; - let value = this.get_value(); - this.options.readonly = _value; - if (this.options.readonly) { - if (this.editor) - this.editor.remove(); - this.htmlNode = jQuery(document.createElement(this.options.readonly ? "div" : "textarea")) - .addClass('et2_textbox_ro'); - if (this.options.height) { - this.htmlNode.css('height', this.options.height); - } - this.editor = null; - this.setDOMNode(this.htmlNode[0]); - this.set_value(value); - } - else { - if (!this.editor) { - this.htmlNode = jQuery(document.createElement("textarea")) - .val(value); - if (this.options.height || this.options.editable_height) { - this.htmlNode.css('height', (this.options.editable_height ? this.options.editable_height : this.options.height)); - } - this.setDOMNode(this.htmlNode[0]); - this.init_editor(); - } - } - } - /** - * Callback function runs when the filepicker in image dialog is clicked - * - * @param {type} _callback - * @param {type} _value - * @param {type} _meta - */ - _file_picker_callback(_callback, _value, _meta) { - if (typeof this.file_picker_callback == 'function') - return this.file_picker_callback.call(arguments, this); - let callback = _callback; - // Don't rely only on app_name to fetch et2 object as app_name may not - // always represent current app of the window, e.g.: mail admin account. - // Try to fetch et2 from its template name. - let etemplate = jQuery('form').data('etemplate'); - let et2; - if (etemplate && etemplate.name && !app[egw(window).app_name()]) { - et2 = etemplate2.getByTemplate(etemplate.name)[0]['widgetContainer']; - } - else { - et2 = app[egw(window).app_name()].et2; - } - let vfsSelect = et2_createWidget('vfs-select', { - id: 'upload', - mode: 'open', - name: '', - button_caption: "Link", - button_label: "Link", - dialog_title: "Link file", - method: "download" - }, et2); - jQuery(vfsSelect.getDOMNode()).on('change', function () { - callback(vfsSelect.get_value(), { alt: vfsSelect.get_value() }); - }); - // start the file selector dialog - vfsSelect.click(); - } - /** - * Callback when instance is ready - * - * @param {type} _editor - */ - _instanceIsReady(_editor) { - console.log("Editor: " + _editor.id + " is now initialized."); - // try to reserve focus state as running command on editor may steal the - // current focus. - let focusedEl = jQuery(':focus'); - this.editor = _editor; - this.editor.on('drop', function (e) { - e.preventDefault(); - }); - if (!this.disabled) - jQuery(this.editor.editorContainer).css('display', 'flex'); - this.tinymce_container = this.editor.editorContainer; - // go back to reserved focused element - focusedEl.focus(); - } - /** - * Takes all relevant preferences into account and set settings accordingly - * - * @returns {object} returns a object including all settings - */ - _extendedSettings() { - let rte_menubar = egw.preference('rte_menubar', 'common'); - let rte_toolbar = egw.preference('rte_toolbar', 'common'); - // we need to have rte_toolbar values as an array - if (rte_toolbar && typeof rte_toolbar == "object" && this.toolbar == '') { - rte_toolbar = Object.keys(rte_toolbar).map(function (key) { return rte_toolbar[key]; }); - } - else if (this.toolbar != '') { - rte_toolbar = this.toolbar.split(','); - } - let settings = { - fontsize_formats: et2_htmlarea.FONT_SIZE_FORMATS[egw.preference('rte_font_unit', 'common')], - menubar: parseInt(rte_menubar) && this.menubar ? true : typeof rte_menubar != 'undefined' ? false : this.menubar - }; - switch (this.mode) { - case 'simple': - settings['toolbar'] = et2_htmlarea.TOOLBAR_SIMPLE; - break; - case 'extended': - settings['toolbar'] = et2_htmlarea.TOOLBAR_EXTENDED; - break; - case 'advanced': - settings['toolbar'] = et2_htmlarea.TOOLBAR_ADVANCED; - break; - default: - this.mode = ''; - } - // take rte_toolbar into account if no mode restrictly set from template - if (rte_toolbar && !this.mode) { - let toolbar_diff = et2_htmlarea.TOOLBAR_LIST.filter(function (i) { return !(rte_toolbar.indexOf(i) > -1); }); - settings['toolbar'] = et2_htmlarea.TOOLBAR_ADVANCED; - toolbar_diff.forEach(function (a) { - let r = new RegExp(a); - settings['toolbar'] = settings['toolbar'].replace(r, ''); - }); - } - return settings; - } - destroy() { - if (this.editor) { - try { - this.editor.destroy(); - } - catch (e) { - egw().debug("Error destroying editor", e); - } - } - this.editor = null; - this.tinymce = null; - this.tinymce_container = null; - this.htmlNode.remove(); - this.htmlNode = null; - super.destroy(); - } - set_value(_value) { - this._oldValue = _value; - if (this.editor) { - this.editor.setContent(_value); - } - else { - if (this.options.readonly) { - this.htmlNode.empty().append(_value); - } - else { - this.htmlNode.val(_value); - } - } - this.value = _value; - } - getValue() { - return this.editor ? this.editor.getContent() : (this.options.readonly ? this.value : this.htmlNode.val()); - } - /** - * Resize htmlNode tag according to window size - * @param {type} _height excess height which comes from window resize - */ - resize(_height) { - var _a, _b; - if (_height && this.options.resize_ratio !== '0') { - // apply the ratio - _height = (this.options.resize_ratio != '') ? _height * this.options.resize_ratio : _height; - if (_height != 0) { - if (this.editor) // TinyMCE HTML - { - let h; - if (typeof this.editor.iframeElement != 'undefined' && this.editor.editorContainer.clientHeight > 0) { - h = (this.editor.editorContainer.clientHeight + _height) > 0 ? - (this.editor.editorContainer.clientHeight) + _height : this.editor.settings.min_height; - } - else // fallback height size - { - h = this.editor.settings.min_height + _height; - } - jQuery(this.editor.editorContainer).height(h); - jQuery(this.editor.iframeElement).height(h - (((_a = this.editor.editorContainer.getElementsByClassName('tox-editor-header')[0]) === null || _a === void 0 ? void 0 : _a.clientHeight) + ((_b = this.editor.editorContainer.getElementsByClassName('tox-statusbar')[0]) === null || _b === void 0 ? void 0 : _b.clientHeight))); - } - else // No TinyMCE - { - this.htmlNode.height(this.htmlNode.height() + _height); - } - } - } - } -} -et2_htmlarea._attributes = { - mode: { - 'name': 'Mode', - 'description': 'One of {ascii|simple|extended|advanced}', - 'default': '', - 'type': 'string' - }, - height: { - 'name': 'Height', - 'default': et2_no_init, - 'type': 'string' - }, - width: { - 'name': 'Width', - 'default': et2_no_init, - 'type': 'string' - }, - value: { - name: "Value", - description: "The value of the widget", - type: "html", - default: et2_no_init - }, - imageUpload: { - name: "imageUpload", - description: "Url to upload images dragged in or id of link_to widget to it's vfs upload. Can also be just a name for which content array contains a path to upload the picture.", - type: "string", - default: null - }, - file_picker_callback: { - name: "File picker callback", - description: "Callback function to get called when file picker is clicked", - type: 'js', - default: et2_no_init - }, - images_upload_handler: { - name: "Images upload handler", - description: "Callback function for handling image upload", - type: 'js', - default: et2_no_init - }, - menubar: { - name: "Menubar", - description: "Display menubar at the top of the editor", - type: "boolean", - default: true - }, - statusbar: { - name: "Status bar", - description: "Enable/disable status bar on the bottom of editor", - type: "boolean", - default: true - }, - valid_children: { - name: "Valid children", - description: "Enables to control what child tag is allowed or not allowed of the present tag. For instance: +body[style], makes style tag allowed inside body", - type: "string", - default: "+body[style]" - }, - toolbar: { - 'name': 'Toolbar', - 'description': 'Comma separated string of toolbar actions. It will only be considered if no Mode is restricted.', - 'default': '', - 'type': 'string' - }, - toolbar_mode: { - 'name': 'toolbar mode', - 'type': 'string', - 'default': 'floating', - 'description': 'It allows to extend the toolbar to accommodate the overflowing toolbar buttons. {floating, sliding, scrolling, wrap}' - } -}; -/** - * Array of toolbars - * @constant - */ -et2_htmlarea.TOOLBAR_LIST = ['undo', 'redo', 'formatselect', 'fontselect', 'fontsizeselect', - 'bold', 'italic', 'strikethrough', 'forecolor', 'backcolor', 'link', - 'alignleft', 'aligncenter', 'alignright', 'alignjustify', 'numlist', - 'bullist', 'outdent', 'indent', 'ltr', 'rtl', 'removeformat', 'code', 'image', 'searchreplace', 'fullscreen', 'table' -]; -/** - * arranged toolbars as simple mode - * @constant - */ -et2_htmlarea.TOOLBAR_SIMPLE = "undo redo|formatselect fontselect fontsizeselect | bold italic removeformat forecolor backcolor | " + - "alignleft aligncenter alignright alignjustify | bullist " + - "numlist outdent indent| link image pastetext | table"; -/** - * arranged toolbars as extended mode - * @constant - */ -et2_htmlarea.TOOLBAR_EXTENDED = "fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | " + - "link | alignleft aligncenter alignright alignjustify | numlist " + - "bullist outdent indent | removeformat | image | fullscreen | table"; -/** - * arranged toolbars as advanced mode - * @constant - */ -et2_htmlarea.TOOLBAR_ADVANCED = "undo redo| formatselect | fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | " + - "alignleft aligncenter alignright alignjustify | bullist " + - "numlist outdent indent ltr rtl | removeformat code| link image pastetext | searchreplace | fullscreen | table"; -/** - * font size formats - * @constant - */ -et2_htmlarea.FONT_SIZE_FORMATS = { - pt: "8pt 9pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 36pt 48pt 72pt", - px: "8px 9px 10px 11px 12px 14px 16px 18px 20px 22px 24px 26px 28px 36px 48px 72px" -}; -/** - * language code represention for TinyMCE lang code - */ -et2_htmlarea.LANGUAGE_CODE = { - bg: "bg_BG", ca: "ca", cs: "cs", da: "da", de: "de", en: "en_CA", - el: "el", "es-es": "es", et: "et", eu: "eu", fa: "fa_IR", fi: "fi", - fr: "fr_FR", hi: "", hr: "hr", hu: "hu_HU", id: "id", it: "it", iw: "", - ja: "ja", ko: "ko_KR", lo: "", lt: "lt", lv: "lv", nl: "nl", no: "nb_NO", - pl: "pl", pt: "pt_PT", "pt-br": "pt_BR", ru: "ru", sk: "sk", sl: "sl_SI", - sv: "sv_SE", th: "th_TH", tr: "tr_TR", uk: "en_GB", vi: "vi_VN", zh: "zh_CN", - "zh-tw": "zh_TW" -}; -et2_register_widget(et2_htmlarea, ["htmlarea"]); -//# sourceMappingURL=et2_widget_htmlarea.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_iframe.js b/api/js/etemplate/et2_widget_iframe.js deleted file mode 100644 index 70d84e9197..0000000000 --- a/api/js/etemplate/et2_widget_iframe.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * EGroupware eTemplate2 - JS widget class for an iframe - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2013 - */ -/*egw:uses - et2_core_valueWidget; -*/ -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -/** - * @augments et2_valueWidget - */ -export class et2_iframe extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_iframe - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_iframe._attributes, _child || {})); - this.htmlNode = null; - // Allow no child widgets - this.supportedWidgetClasses = []; - this.htmlNode = jQuery(document.createElement("iframe")); - if (this.options.label) { - this.htmlNode.append('' + this.options.label + ''); - } - if (this.options.fullscreen) { - this.htmlNode.attr('allowfullscreen', 1); - } - this.setDOMNode(this.htmlNode[0]); - } - /** - * Set name of iframe (to be used as target for links) - * - * @param _name - */ - set_name(_name) { - this.options.name = _name; - this.htmlNode.attr('name', _name); - } - set_allow(_allow) { - this.options.allow = _allow; - this.htmlNode.attr('allow', _allow); - } - /** - * Make it look like part of the containing document - * - * @param _seamless boolean - */ - set_seamless(_seamless) { - this.options.seamless = _seamless; - this.htmlNode.attr("seamless", _seamless); - } - set_value(_value) { - if (typeof _value == "undefined") - _value = ""; - if (_value.trim().indexOf("http") == 0 || _value.indexOf('about:') == 0 || _value[0] == '/') { - // Value is a URL - this.set_src(_value); - } - else { - // Value is content - this.set_srcdoc(_value); - } - } - /** - * Set the URL for the iframe - * - * Sets the src attribute to the given value - * - * @param _value String URL - */ - set_src(_value) { - if (_value.trim() != "") { - if (_value.trim() == 'about:blank') { - this.htmlNode.attr("src", _value); - } - else { - // Load the new page, but display a loader - let loader = jQuery('
      '); - this.htmlNode - .before(loader); - window.setTimeout(jQuery.proxy(function () { - this.htmlNode.attr("src", _value) - .one('load', function () { - loader.remove(); - }); - }, this), 0); - } - } - } - /** - * Sets the content of the iframe - * - * Sets the srcdoc attribute to the given value - * - * @param _value String Content of a document - */ - set_srcdoc(_value) { - this.htmlNode.attr("srcdoc", _value); - } -} -et2_iframe._attributes = { - 'label': { - 'default': "", - description: "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - ignore: false, - name: "Label", - translate: true, - type: "string" - }, - "needed": { - "ignore": true - }, - "seamless": { - name: "Seamless", - 'default': true, - description: "Specifies that the iframe should be rendered in a manner that makes it appear to be part of the containing document", - translate: false, - type: "boolean" - }, - "name": { - name: "Name", - "default": "", - description: "Specifies name of frame, to be used as target for links", - type: "string" - }, - fullscreen: { - name: "Fullscreen", - "default": false, - description: "Make the iframe compatible to be a fullscreen video player mode", - type: "boolean" - }, - src: { - name: "Source", - "default": "", - description: "Specifies URL for the iframe", - type: "string" - }, - allow: { - name: "Allow", - "default": "", - description: "Specifies list of allow features, e.g. camera", - type: "string" - } -}; -et2_register_widget(et2_iframe, ["iframe"]); -//# sourceMappingURL=et2_widget_iframe.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_image.js b/api/js/etemplate/et2_widget_image.js deleted file mode 100644 index acce26a4d8..0000000000 --- a/api/js/etemplate/et2_widget_image.js +++ /dev/null @@ -1,586 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Description object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2011 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_baseWidget; - expose; - /vendor/bower-asset/cropper/dist/cropper.min.js; -*/ -import { et2_baseWidget } from './et2_core_baseWidget'; -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_no_init } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -import { et2_dialog } from "./et2_widget_dialog"; -/** - * Class which implements the "image" XET-Tag - * - * @augments et2_baseWidget - */ -export class et2_image extends et2_baseWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_image._attributes, _child || {})); - this.image = null; - // Create the image or a/image tag - this.image = jQuery(document.createElement("img")); - if (this.options.label) { - this.image.attr("alt", this.options.label).attr("title", this.options.label); - } - if (this.options.href) { - this.image.addClass('et2_clickable'); - } - if (this.options["class"]) { - this.image.addClass(this.options["class"]); - } - this.setDOMNode(this.image[0]); - } - click(_ev) { - if (this.options.href) { - this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); - } - else { - super.click(_ev); - } - } - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - // Check to expand name - if (typeof _attrs["src"] != "undefined") { - let manager = this.getArrayMgr("content"); - if (manager && _attrs["src"]) { - let src = manager.getEntry(_attrs["src"], false, true); - if (typeof src != "undefined" && src !== null) { - if (typeof src == "object") { - src = egw().link('/index.php', src); - } - _attrs["src"] = src; - } - } - } - } - set_label(_value) { - this.options.label = _value; - _value = this.egw().lang(_value); - // label is NOT the alt attribute in eTemplate, but the title/tooltip - this.image.attr("alt", _value).attr("title", _value); - } - setValue(_value) { - // Value is src, images don't get IDs - this.set_src(_value); - } - set_href(_value) { - if (!this.isInTree()) { - return false; - } - this.options.href = _value; - this.image.wrapAll('"'); - let href = this.options.href; - let popup = this.options.extra_link_popup; - let target = this.options.extra_link_target; - let self = this; - this.image.click(function (e) { - if (self.options.expose_view) { - /* - TODO: Fix after implementing EXPOSE mixin class - */ - //self._init_blueimp_gallery(e,_value); - e.stopImmediatePropagation(); - } - else { - egw.open_link(href, target, popup); - } - e.preventDefault(); - return false; - }); - return true; - } - /** - * Set image src - * - * @param {string} _value image, app/image or url - * @return {boolean} true if image was found, false if not (image is either not displayed or default_src is used) - */ - set_src(_value) { - if (!this.isInTree()) { - return false; - } - this.options.src = _value; - // allow url's too - if (_value[0] == '/' || _value.substr(0, 4) == 'http' || _value.substr(0, 5) == 'data:') { - this.image.attr('src', _value).show(); - return true; - } - let src = this.egw().image(_value); - if (src) { - this.image.attr("src", src).show(); - return true; - } - src = null; - if (this.options.default_src) { - src = this.egw().image(this.options.default_src); - } - if (src) { - this.image.attr("src", src).show(); - } - else { - this.image.css("display", "none"); - } - return false; - } - /** - * Function to get media content to feed the expose - * @param {type} _value - */ - getMedia(_value) { - let base_url = egw.webserverUrl.match(/^\/ig/) ? egw(window).window.location.origin + egw.webserverUrl + '/' : egw.webserverUrl + '/'; - let mediaContent = []; - if (_value) { - mediaContent = [{ - title: this.options.label, - href: base_url + _value, - type: this.options.type + "/*", - thumbnail: base_url + _value - }]; - } - return mediaContent; - } - /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview - * - * @param {array} _attrs - */ - getDetachedAttributes(_attrs) { - _attrs.push("src", "label", "href"); - } - getDetachedNodes() { - return [this.image[0]]; - } - setDetachedAttributes(_nodes, _values) { - // Set the given DOM-Nodes - this.image = jQuery(_nodes[0]); - // Set the attributes - if (_values["src"]) { - this.set_src(_values["src"]); - } - // Not valid, but we'll deal - if (_values["value"]) { - this.setValue(_values["value"]); - } - if (_values["label"]) { - this.set_label(_values["label"]); - } - if (_values["href"]) { - this.image.addClass('et2_clickable'); - this.set_href(_values["href"]); - } - } -} -et2_image._attributes = { - "src": { - "name": "Image", - "type": "string", - "description": "Displayed image" - }, - default_src: { - name: "Default image", - type: "string", - description: "Image to use if src is not found" - }, - "href": { - "name": "Link Target", - "type": "string", - "description": "Link URL, empty if you don't wan't to display a link.", - "default": et2_no_init - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": "_self", - "description": "Link target descriptor" - }, - "extra_link_popup": { - "name": "Popup", - "type": "string", - "description": "widthxheight, if popup should be used, eg. 640x480" - }, - "imagemap": { - // TODO: Do something with this - "name": "Image map", - "description": "Currently not implemented" - }, - "label": { - "name": "Label", - "type": "string", - "description": "Label for image" - }, - "expose_view": { - name: "Expose view", - type: "boolean", - default: false, - description: "Clicking on an image with href value would popup an expose view, and will show image referenced by href." - } -}; -et2_image.legacyOptions = ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"]; -et2_register_widget(et2_image, ["image"]); -/** -* Widget displaying an application icon -*/ -export class et2_appicon extends et2_image { - set_src(_app) { - if (!_app) - _app = this.egw().app_name(); - this.image.addClass('et2_appicon'); - return super.set_src(_app == 'sitemgr-link' ? 'sitemgr/sitemgr-link' : // got removed from jdots - (this.egw().app(_app, 'icon_app') || _app) + '/' + (this.egw().app(_app, 'icon') || 'navbar')); - } -} -et2_appicon._attributes = { - default_src: { - name: "Default image", - type: "string", - default: "nonav", - description: "Image to use if there is no application icon" - } -}; -et2_register_widget(et2_appicon, ["appicon"]); -/** -* Avatar widget to display user profile picture or -* user letter avatar based on user's firstname lastname. -* -* @augments et2_baseWidget -*/ -export class et2_avatar extends et2_image { - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_avatar._attributes, _child || {})); - if (this.options.frame == 'circle') { - this.image.attr('style', 'border-radius:50%'); - } - if (this.options.contact_id) - this.setValue(this.options.contact_id); - } - /** - * Generate letter avatar with given data - * @param {type} _fname - * @param {type} _lname - * @param {type} _id - * @returns {string} return data url - */ - static lavatar(_fname, _lname, _id) { - let str = _fname + _lname + _id; - let getBgColor = function (_str) { - let hash = 0; - for (let i = 0; i < _str.length; i++) { - hash = _str[i].charCodeAt(0) + hash; - } - return et2_avatar.LAVATAR_BG_COLORS[hash % et2_avatar.LAVATAR_BG_COLORS.length]; - }; - let bg = getBgColor(str); - let size = et2_avatar.LAVATAR_SIZE * (window.devicePixelRatio ? window.devicePixelRatio : 1); - let text = (_fname ? _fname[0].toUpperCase() : "") + (_lname ? _lname[0].toUpperCase() : ""); - let canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - let context = canvas.getContext("2d"); - context.fillStyle = bg; - context.fillRect(0, 0, canvas.width, canvas.height); - context.font = Math.round(canvas.width / 2) + "px Arial"; - context.textAlign = "center"; - context.fillStyle = et2_avatar.LAVATAR_TEXT_COLOR; - context.fillText(text, size / 2, size / 1.5); - let dataURL = canvas.toDataURL(); - canvas.remove(); - return dataURL; - } - /** - * Function runs after uplaod in avatar dialog is finished and it tries to - * update image and cropper container. - * @param {type} e - */ - static uploadAvatar_onFinish(e) { - let file = e.data.resumable.files[0].file; - let reader = new FileReader(); - reader.onload = function (e) { - jQuery('#_cropper_image').attr('src', e.target.result); - jQuery('#_cropper_image').cropper('replace', e.target.result); - }; - reader.readAsDataURL(file); - } - /** - * Function to set contact id - * contact id could be in one of these formats: - * 'number', will be consider as contact_id - * 'contact:number', similar to above - * 'account:number', will be consider as account id - * @example: contact_id = "account:4" - * - * @param {string} _contact_id contact id could be as above mentioned formats - */ - set_contact_id(_contact_id) { - let params = {}; - let id = 'contact_id'; - this.image.addClass('et2_avatar'); - if (!_contact_id) { - _contact_id = this.egw().user('account_id'); - } - else if (_contact_id.match(/account:/)) { - id = 'account_id'; - _contact_id = _contact_id.replace('account:', ''); - } - else { - id = 'contact_id'; - _contact_id = _contact_id.replace('contact:', ''); - } - // if our src (incl. cache-buster) already includes the correct id, use that one - if (this.options.src && this.options.src.match("(&|\\?)contact_id=" + _contact_id + "(&|\\$)")) { - return; - } - params[id] = _contact_id; - this.set_src(egw.link('/api/avatar.php', params)); - } - /** - * Function to set value - */ - setValue(_value) { - this.set_contact_id(_value); - } - /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview - */ - getDetachedAttributes(_attrs) { - _attrs.push("contact_id", "label", "href"); - } - setDetachedAttributes(_nodes, _values) { - // Set the given DOM-Nodes - this.image = jQuery(_nodes[0]); - if (_values["contact_id"]) { - this.set_contact_id(_values["contact_id"]); - } - if (_values["label"]) { - this.set_label(_values["label"]); - } - if (_values["href"]) { - this.image.addClass('et2_clickable'); - this.set_href(_values["href"]); - } - } - /** - * Build Editable Mask Layer (EML) in order to show edit/delete actions - * on top of profile picture. - * @param {boolean} _noDelete disable delete button in initialization - */ - _buildEditableLayer(_noDelete) { - let self = this; - // editable mask layer (eml) - let wrapper = jQuery(document.createElement('div')).addClass('avatar').insertAfter(this.image); - this.image.appendTo(wrapper); - let eml = jQuery(document.createElement('div')) - .addClass('eml') - .insertAfter(this.image); - // edit button - jQuery(document.createElement('div')) - .addClass('emlEdit') - .click(function () { - let buttons = [ - { "button_id": 1, "text": self.egw().lang('save'), id: 'save', image: 'check', "default": true }, - { "button_id": 0, "text": self.egw().lang('cancel'), id: 'cancel', image: 'cancelled' } - ]; - let dialog = function (_title, _value, _buttons, _egw_or_appname) { - return et2_createWidget("dialog", { - callback: function (_buttons, _value) { - if (_buttons == 'save') { - let canvas = jQuery('#_cropper_image').cropper('getCroppedCanvas'); - self.image.attr('src', canvas.toDataURL("image/jpeg", 1.0)); - self.egw().json('addressbook.addressbook_ui.ajax_update_photo', [self.getInstanceManager().etemplate_exec_id, canvas.toDataURL('image/jpeg', 1.0)], function (res) { - if (res) { - del.show(); - } - }).sendRequest(); - } - }, - title: _title || egw.lang('Input required'), - buttons: _buttons || et2_dialog.BUTTONS_OK_CANCEL, - value: { - content: _value - }, - width: "90%", - height: "450", - resizable: false, - position: "top+10", - template: egw.webserverUrl + '/api/templates/default/avatar_edit.xet?2' - }, et2_dialog._create_parent(_egw_or_appname)); - }; - dialog(egw.lang('Edit avatar'), self.options, buttons, null); - }) - .appendTo(eml); - // delete button - var del = jQuery(document.createElement('div')) - .addClass('emlDelete') - .click(function () { - et2_dialog.show_dialog(function (_btn) { - if (_btn == et2_dialog.YES_BUTTON) { - self.egw().json('addressbook.addressbook_ui.ajax_update_photo', [self.getInstanceManager().etemplate_exec_id, null], function (res) { - if (res) { - self.image.attr('src', ''); - del.hide(); - egw.refresh('Avatar Deleted.', egw.app_name()); - } - }).sendRequest(); - } - }, egw.lang('Delete this photo?'), egw.lang('Delete'), null, et2_dialog.BUTTONS_YES_NO); - }) - .appendTo(eml); - if (_noDelete) - del.hide(); - // invisible the mask - eml.css('opacity', '0'); - eml.parent().css('position', "relative"); - // bind handler for activating actions on editable mask - eml.on({ - mouseover: function () { eml.css('opacity', '0.9'); }, - mouseout: function () { eml.css('opacity', '0'); } - }); - } - /** - * We need to build the Editable Mask Layer after widget gets loaded - */ - doLoadingFinished() { - super.doLoadingFinished(); - let self = this; - if (this.options.contact_id && this.options.editable) { - egw(window).json('addressbook.addressbook_ui.ajax_noPhotoExists', [this.options.contact_id], function (noPhotoExists) { - if (noPhotoExists) - self.image.attr('src', ''); - self._buildEditableLayer(noPhotoExists); - }).sendRequest(true); - } - if (this.options.crop) { - jQuery(this.image).cropper({ - aspectRatio: 1 / 1, - crop: function (e) { - console.log(e); - } - }); - } - return true; - } -} -et2_avatar._attributes = { - "contact_id": { - name: "Contact id", - type: "string", - default: "", - description: "Contact id should be either user account_id {account:number} or contact_id {contact:number or number}" - }, - "default_src": { - "ignore": true - }, - "frame": { - name: "Avatar frame", - type: "string", - default: "circle", - description: "Define the shape of frame that avatar will be shown inside it. it can get {circle,rectangle} values which default value is cicle." - }, - editable: { - name: "Edit avatar", - type: "boolean", - default: false, - description: "Make avatar widget editable to be able to crop profile picture or upload a new photo" - }, - crop: { - name: "Crop avatar", - type: "boolean", - default: false, - description: "Create crop container and cropping feature" - } -}; -/** - * background oolor codes - */ -et2_avatar.LAVATAR_BG_COLORS = [ - '#5a8770', '#b2b7bb', '#6fa9ab', '#f5af29', - '#0088b9', '#f18636', '#d93a37', '#a6b12e', - '#0088b9', '#f18636', '#d93a37', '#a6b12e', - '#5c9bbc', '#f5888d', '#9a89b5', '#407887', - '#9a89b5', '#5a8770', '#d33f33', '#a2b01f', - '#f0b126', '#0087bf', '#f18636', '#0087bf', - '#b2b7bb', '#72acae', '#9c8ab4', '#5a8770', - '#eeb424', '#407887' -]; -/** - * Text color - */ -et2_avatar.LAVATAR_TEXT_COLOR = '#ffffff'; -et2_avatar.LAVATAR_SIZE = 128; -et2_register_widget(et2_avatar, ["avatar"]); -/** -* Avatar readonly widget to only display user profile picture or -* user letter avatar based on user's firstname lastname. -*/ -export class et2_avatar_ro extends et2_avatar { - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_avatar_ro._attributes, _child || {})); - this.options.editable = false; - } -} -et2_register_widget(et2_avatar_ro, ["avatar_ro"]); -/** -* Letter Avatar widget to display user profile picture (given url) or -* user letter avatar based on user's firstname lastname. -* -* It will use client-side lavatar if all the following conditions are met: -* - contact_id, lname and fname are all set. -* - the given src url includes flag of lavatar=1 which means there's -* no personal avatar set for the contact yet. -* -* @augments et2_baseWidget -*/ -export class et2_lavatar extends et2_image { - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_lavatar._attributes, _child || {})); - } - set_src(_url) { - if (_url && decodeURIComponent(_url).match("lavatar=1") && (this.options.fname || this.options.lname) && this.options.contact_id) { - this.set_src(et2_avatar.lavatar(this.options.fname, this.options.lname, this.options.contact_id)); - return false; - } - super.set_src(_url); - } -} -et2_lavatar._attributes = { - lname: { - name: "last name", - type: "string", - default: "", - description: "" - }, - fname: { - name: "first name", - type: "string", - default: "", - description: "" - }, - contact_id: { - name: "contact id", - type: "string", - default: "", - description: "" - } -}; -et2_register_widget(et2_lavatar, ["lavatar"]); -//# sourceMappingURL=et2_widget_image.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_itempicker.js b/api/js/etemplate/et2_widget_itempicker.js deleted file mode 100644 index f78d19a590..0000000000 --- a/api/js/etemplate/et2_widget_itempicker.js +++ /dev/null @@ -1,335 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Itempicker object - * derived from et2_link_entry widget @copyright 2011 Nathan Gray - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Christian Binder - * @author Nathan Gray - * @copyright 2012 Christian Binder - * @copyright 2011 Nathan Gray - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; - et2_extension_itempicker_actions; - egw_action.egw_action_common; -*/ -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_csvSplit, et2_no_init } from "./et2_core_common"; -import { egw } from "../jsapi/egw_global"; -/** - * Class which implements the "itempicker" XET-Tag - * - * @augments et2_inputWidget - */ -export class et2_itempicker extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_itempicker - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_itempicker._attributes, _child || {})); - this.last_search = ""; // Remember last search value - this.action = null; // Action function for button - this.current_app = ""; // Remember currently chosen application - this.div = null; - this.left = null; - this.right = null; - this.right_container = null; - this.app_select = null; - this.search = null; - this.button_action = null; - this.itemlist = null; - this.div = null; - this.left = null; - this.right = null; - this.right_container = null; - this.app_select = null; - this.search = null; - this.button_action = null; - this.itemlist = null; - if (this.options.action !== null && typeof this.options.action == "string") { - this.action = new egwFnct(this, "javaScript:" + this.options.action); - } - else { - console.log("itempicker widget: no action provided for button"); - } - this.createInputWidget(); - } - clearSearchResults() { - this.search.val(""); - this.itemlist.html(""); - this.search.focus(); - this.clear.hide(); - } - createInputWidget() { - let _self = this; - this.div = jQuery(document.createElement("div")); - this.left = jQuery(document.createElement("div")); - this.right = jQuery(document.createElement("div")); - this.right_container = jQuery(document.createElement("div")); - this.app_select = jQuery(document.createElement("ul")); - this.search = jQuery(document.createElement("input")); - this.clear = jQuery(document.createElement("span")); - this.itemlist = jQuery(document.createElement("div")); - // Container elements - this.div.addClass("et2_itempicker"); - this.left.addClass("et2_itempicker_left"); - this.right.addClass("et2_itempicker_right"); - this.right_container.addClass("et2_itempicker_right_container"); - // Application select - this.app_select.addClass("et2_itempicker_app_select"); - let item_count = 0; - for (let key in this.options.select_options) { - let img_icon = this.egw().image(key + "/navbar"); - if (img_icon === null) { - continue; - } - let img = jQuery(document.createElement("img")); - img.attr("src", img_icon); - let item = jQuery(document.createElement("li")); - item.attr("id", key) - .click(function () { - _self.selectApplication(jQuery(this)); - }) - .append(img); - if (item_count == 0) { - this.selectApplication(item); // select first item by default - } - this.app_select.append(item); - item_count++; - } - // Search input field - this.search.addClass("et2_itempicker_search"); - this.search.keyup(function () { - let request = {}; - request.term = jQuery(this).val(); - _self.query(request); - }); - this.set_blur(this.options.blur, this.search); - // Clear button for search - this.clear - .addClass("et2_itempicker_clear ui-icon ui-icon-close") - .click(function () { - _self.clearSearchResults(); - }) - .hide(); - // Action button - this.button_action = et2_createWidget("button", {}); - jQuery(this.button_action.getDOMNode()).addClass("et2_itempicker_button_action"); - this.button_action.set_label(this.egw().lang(this.options.action_label)); - this.button_action.click = function () { _self.doAction(); }; - // Itemlist - this.itemlist.attr("id", "itempicker_itemlist"); - this.itemlist.addClass("et2_itempicker_itemlist"); - // Put everything together - this.left.append(this.app_select); - this.right_container.append(this.search); - this.right_container.append(this.clear); - this.right_container.append(this.button_action.getDOMNode()); - this.right_container.append(this.itemlist); - this.right.append(this.right_container); - this.div.append(this.right); // right before left to have a natural - this.div.append(this.left); // z-index for left div over right div - this.setDOMNode(this.div[0]); - } - doAction() { - if (this.action !== null) { - let data = {}; - data.app = this.current_app; - data.value = this.options.value; - data.checked = this.getSelectedItems(); - return this.action.exec(this, data); - } - return false; - } - getSelectedItems() { - let items = []; - jQuery(this.itemlist).children("ul").children("li.selected").each(function (index) { - items[index] = jQuery(this).attr("id"); - }); - return items; - } - /** - * Ask server for entries matching selected app/type and filtered by search string - */ - query(request) { - if (request.term.length < 3) { - return true; - } - // Remember last search - this.last_search = request.term; - // Allow hook / tie in - if (this.options.query && typeof this.options.query == 'function') { - if (!this.options.query(request, response)) - return false; - } - //if(request.term in this.cache) { - // return response(this.cache[request.term]); - //} - this.itemlist.addClass("loading"); - this.clear.css("display", "inline-block"); - egw.json("EGroupware\\Api\\Etemplate\\Widget\\ItemPicker::ajax_item_search", [this.current_app, '', request.term, request.options], this.queryResults, this, true, this).sendRequest(); - } - /** - * Server found some results for query - */ - queryResults(data) { - this.itemlist.removeClass("loading"); - this.updateItemList(data); - } - selectApplication(app) { - this.clearSearchResults(); - jQuery(".et2_itempicker_app_select li").removeClass("selected"); - app.addClass("selected"); - this.current_app = app.attr("id"); - return true; - } - set_blur(_value, input) { - if (typeof input == 'undefined') - input = this.search; - if (_value) { - input.attr("placeholder", _value); // HTML5 - if (!input[0].placeholder) { - // Not HTML5 - if (input.val() == "") - input.val(_value); - input.focus(input, function (e) { - let placeholder = _value; - if (e.data.val() == placeholder) - e.data.val(""); - }).blur(input, function (e) { - let placeholder = _value; - if (e.data.val() == "") - e.data.val(placeholder); - }); - if (input.val() == "") - input.val(_value); - } - } - else { - this.search.removeAttr("placeholder"); - } - } - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - _attrs["select_options"] = {}; - if (_attrs["application"]) { - let apps = et2_csvSplit(_attrs["application"], null, ","); - for (let i = 0; i < apps.length; i++) { - _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); - } - } - else { - _attrs["select_options"] = this.egw().link_app_list('query'); - } - // Check whether the options entry was found, if not read it from the - // content array. - if (_attrs["select_options"] == null) { - _attrs["select_options"] = this.getArrayMgr('content') - .getEntry("options-" + this.id); - } - // Default to an empty object - if (_attrs["select_options"] == null) { - _attrs["select_options"] = {}; - } - } - updateItemList(data) { - let list = jQuery(document.createElement("ul")); - let item_count = 0; - for (let id in data) { - let item = jQuery(document.createElement("li")); - if (item_count % 2 == 0) { - item.addClass("row_on"); - } - else { - item.addClass("row_off"); - } - item.attr("id", id) - .html(data[id]) - .click(function (e) { - if (e.ctrlKey || e.metaKey) { - // add to selection - jQuery(this).addClass("selected"); - } - else if (e.shiftKey) { - // select range - let start = jQuery(this).siblings(".selected").first(); - if ((start === null || start === void 0 ? void 0 : start.length) == 0) { - // no start item - cannot select range - select single item - jQuery(this).addClass("selected"); - return true; - } - let end = jQuery(this); - // swap start and end if start appears after end in dom hierarchy - if (start.index() > end.index()) { - let startOld = start; - start = end; - end = startOld; - } - // select start to end - start.addClass("selected"); - start.nextUntil(end).addClass("selected"); - end.addClass("selected"); - } - else { - // select single item - jQuery(this).siblings(".selected").removeClass("selected"); - jQuery(this).addClass("selected"); - } - }); - list.append(item); - item_count++; - } - this.itemlist.html(list); - } -} -et2_itempicker._attributes = { - "action": { - "name": "Action callback", - "type": "string", - "default": false, - "description": "Callback for action. Must be a function(context, data)" - }, - "action_label": { - "name": "Action label", - "type": "string", - "default": "Action", - "description": "Label for action button" - }, - "application": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to the listed application or applications (comma separated)" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": et2_no_init, - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - "value": { - "name": "value", - "type": "any", - "default": "", - "description": "Optional itempicker value(s) - can be used for e.g. environmental information" - }, - "query": { - "name": "Query callback", - "type": "any", - "default": false, - "description": "Callback before query to server. Must return true, or false to abort query." - } -}; -et2_itempicker.legacyOptions = ["application"]; -et2_register_widget(et2_itempicker, ["itempicker"]); -//# sourceMappingURL=et2_widget_itempicker.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_link.js b/api/js/etemplate/et2_widget_link.js deleted file mode 100644 index 265b49f814..0000000000 --- a/api/js/etemplate/et2_widget_link.js +++ /dev/null @@ -1,1964 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Link object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright 2011 Nathan Gray - */ -var _a; -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/jquery-ui/jquery-ui.js; - et2_core_inputWidget; - et2_core_valueWidget; - et2_widget_selectbox; - expose; - - // Include menu system for list context menu - egw_action.egw_menu_dhtmlx; -*/ -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { et2_selectbox } from "./et2_widget_selectbox"; -import { et2_dialog } from "./et2_widget_dialog"; -import { egw, egw_get_file_editor_prefered_mimes } from "../jsapi/egw_global"; -import { et2_csvSplit, et2_no_init } from "./et2_core_common"; -import { expose } from "./expose"; -import { egwMenu } from "../egw_action/egw_menu.js"; -/** - * UI widgets for Egroupware linking system - */ -export class et2_link_to extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_link_to - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_to._attributes, _child || {})); - this.div = jQuery(document.createElement("div")).addClass("et2_link_to et2_toolbar"); - this.link_button = null; - this.status_span = null; - this.link_entry = null; - this.file_upload = null; - this.createInputWidget(); - } - destroy() { - this.link_button = null; - this.status_span = null; - if (this.link_entry) { - this.link_entry.destroy(); - this.link_entry = null; - } - if (this.file_upload) { - this.file_upload.destroy(); - this.file_upload = null; - } - this.div = null; - super.destroy.apply(this, arguments); - } - /** - * Override to provide proper node for sub widgets to go in - * - * @param {Object} _sender - */ - getDOMNode(_sender) { - if (_sender == this) { - return this.div[0]; - } - else if (_sender._type == 'link-entry') { - return this.link_div[0]; - } - else if (_sender._type == 'file') { - return this.file_div[0]; - } - else if (_sender._type == 'vfs-select') { - return this.filemanager_button[0]; - } - } - createInputWidget() { - // Need a div for file upload widget - this.file_div = jQuery(document.createElement("div")).css({ display: 'inline-block' }).appendTo(this.div); - // Filemanager link popup - this.filemanager_button = jQuery(document.createElement("div")).css({ display: 'inline-block' }).appendTo(this.div); - // Need a div for link-to widget - this.link_div = jQuery(document.createElement("div")) - .addClass('div_link') - // Leave room for link button - .appendTo(this.div); - if (!this.options.readonly) { - // One common link button - this.link_button = jQuery(document.createElement("button")) - .text(this.egw().lang(this.options.link_label)) - .appendTo(this.div).hide() - .addClass('link') - .click(this, this.createLink); - // Span for indicating status - this.status_span = jQuery(document.createElement("span")) - .appendTo(this.div).addClass("status").hide(); - } - this.setDOMNode(this.div[0]); - } - doLoadingFinished() { - super.doLoadingFinished.apply(this, arguments); - var self = this; - if (this.link_entry && this.vfs_select && this.file_upload) { - // Already done - return false; - } - // Link-to - var link_entry_attrs = { - id: this.id + '_link_entry', - only_app: this.options.only_app, - application_list: this.options.application_list, - blur: this.options.search_label ? this.options.search_label : this.egw().lang('Search...'), - query: function () { self.link_button.hide(); return true; }, - select: function () { self.link_button.show(); return true; }, - readonly: this.options.readonly - }; - this.link_entry = et2_createWidget("link-entry", link_entry_attrs, this); - // Filemanager select - var select_attrs = { - button_label: egw.lang('Link'), - button_caption: '', - button_icon: 'link', - readonly: this.options.readonly, - dialog_title: egw.lang('Link'), - extra_buttons: [{ text: egw.lang("copy"), id: "copy", image: "copy" }, - { text: egw.lang("move"), id: "move", image: "move" }], - onchange: function () { - var values = true; - // If entry not yet saved, store for linking on server - if (!self.options.value.to_id || typeof self.options.value.to_id == 'object') { - values = self.options.value.to_id || {}; - var files = self.vfs_select.getValue(); - if (typeof files !== 'undefined') { - for (var i = 0; i < files.length; i++) { - values['link:' + files[i]] = { - app: 'link', - id: files[i], - type: 'unknown', - icon: 'link', - remark: '', - title: files[i] - }; - } - } - } - self._link_result(values); - } - }; - // only set server-side callback, if we have a real application-id (not null or array) - // otherwise it only gives an error on server-side - if (self.options.value && self.options.value.to_id && typeof self.options.value.to_id != 'object') { - select_attrs.method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing'; - select_attrs.method_id = self.options.value.to_app + ':' + self.options.value.to_id; - } - this.vfs_select = et2_createWidget("vfs-select", select_attrs, this); - this.vfs_select.set_readonly(this.options.readonly); - // File upload - var file_attrs = { - multiple: true, - id: this.id + '_file', - label: '', - // Make the whole template a drop target - drop_target: this.getInstanceManager().DOMContainer.getAttribute("id"), - readonly: this.options.readonly, - // Change to this tab when they drop - onStart: function (event, file_count) { - // Find the tab widget, if there is one - var tabs = self; - do { - tabs = tabs.getParent(); - } while (tabs != self.getRoot() && tabs.getType() != 'tabbox'); - if (tabs != self.getRoot()) { - tabs.activateTab(self); - } - return true; - }, - onFinish: function (event, file_count) { - event.data = self; - self.filesUploaded(event); - // Auto-link uploaded files - self.createLink(event); - } - }; - this.file_upload = et2_createWidget("file", file_attrs, this); - this.file_upload.set_readonly(this.options.readonly); - return true; - } - getValue() { - return this.options.value; - } - filesUploaded(event) { - var self = this; - this.link_button.show(); - } - /** - * Create a link using the current internal values - * - * @param {Object} event - */ - createLink(event) { - // Disable link button - event.data.link_button.attr("disabled", true); - var values = event.data.options.value; - var self = event.data; - var links = []; - // Links to other entries - event.data = self.link_entry; - self.link_entry.createLink(event, links); - // Files - if (!self.options.no_files) { - for (var file in self.file_upload.options.value) { - links.push({ - app: 'file', - id: file, - name: self.file_upload.options.value[file].name, - type: self.file_upload.options.value[file].type, - remark: jQuery("li[file='" + self.file_upload.options.value[file].name.replace(/'/g, '"') + "'] > input", self.file_upload.progress) - .filter(function () { return jQuery(this).attr("placeholder") != jQuery(this).val(); }).val() - }); - } - } - if (links.length == 0) { - return; - } - var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", [values.to_app, values.to_id, links], self._link_result, self, true, self); - request.sendRequest(); - } - /** - * Sent some links, server has a result - * - * @param {Object} success - */ - _link_result(success) { - if (success) { - this.link_button.hide().attr("disabled", false); - this.status_span.removeClass("error").addClass("success"); - this.status_span.fadeIn().delay(1000).fadeOut(); - delete this.options.value.app; - delete this.options.value.id; - for (var file in this.file_upload.options.value) { - delete this.file_upload.options.value[file]; - } - this.file_upload.progress.empty(); - // Server says it's OK, but didn't store - we'll send this again on submit - // This happens if you link to something before it's saved to the DB - if (typeof success == "object") { - // Save as appropriate in value - if (typeof this.options.value != "object") { - this.options.value = {}; - } - this.options.value.to_id = success; - for (let link in success) { - // Icon should be in registry - if (typeof success[link].icon == 'undefined') { - success[link].icon = egw.link_get_registry(success[link].app, 'icon'); - // No icon, try by mime type - different place for un-saved entries - if (success[link].icon == false && success[link].id.type) { - // Triggers icon by mime type, not thumbnail or app - success[link].type = success[link].id.type; - success[link].icon = true; - } - } - // Special handling for file - if not existing, we can't ask for title - if (success[link].app == 'file' && typeof success[link].title == 'undefined') { - success[link].title = success[link].id.name || ''; - } - } - } - // Look for a link-list with the same ID, refresh it - var self = this; - var list_widget = null; - this.getRoot().iterateOver(function (widget) { - if (widget.id == self.id) { - list_widget = widget; - if (success === true) { - widget._get_links(); - } - } - }, this, et2_link_list); - // If there's an array of data (entry is not yet saved), updating the list will - // not work, so add them in explicitly. - if (list_widget && success) { - // Clear list - list_widget.set_value(null); - // Add temp links in - for (var link_id in success) { - let link = success[link_id]; - if (typeof link.title == 'undefined') { - // Callback to server for title - egw.link_title(link.app, link.id, function (title) { - link.title = title; - list_widget._add_link(link); - }); - } - else { - // Add direct - list_widget._add_link(link); - } - } - } - } - else { - this.status_span.removeClass("success").addClass("error") - .fadeIn(); - } - this.div.trigger('link.et2_link_to', success); - } - set_no_files(no_files) { - if (this.options.readonly) - return; - if (no_files) { - this.file_div.hide(); - this.filemanager_button.hide(); - } - else { - this.file_div.show(); - this.filemanager_button.show(); - } - this.options.no_files = no_files; - } -} -et2_link_to._attributes = { - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to just this one application - hides app selection" - }, - "application_list": { - "name": "Application list", - "type": "any", - "default": "", - "description": "Limit to the listed application or applications (comma seperated)" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", - translate: true - }, - "no_files": { - "name": "No files", - "type": "boolean", - "default": false, - "description": "Suppress attach-files" - }, - "search_label": { - "name": "Search label", - "type": "string", - "default": "", - "description": "Label to use for search" - }, - "link_label": { - "name": "Link label", - "type": "string", - "default": "Link", - "description": "Label for the link button" - }, - "value": { - // Could be string or int if application is provided, or an Object - "type": "any" - } -}; -et2_register_widget(et2_link_to, ["link-to"]); -/** - * List of applications that support link - */ -export class et2_link_apps extends et2_selectbox { - /** - * Constructor - * - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_apps._attributes, _child || {})); - if (this.options.select_options != null) { - // Preset to last application - if (!this.options.value) { - this.set_value(egw.preference('link_app', this.egw().getAppName())); - } - // Register to update preference - var self = this; - this.input.bind("click", function () { - if (typeof self.options.value != 'undefined') - var appname = self.options.value.to_app; - egw.set_preference(appname || self.egw().getAppName(), 'link_app', self.getValue()); - }); - } - } - /** - * We get some minor speedups by overriding parent searching and directly setting select options - * - * @param {Array} _attrs an array of attributes - */ - transformAttributes(_attrs) { - var select_options = {}; - // Limit to one app - if (_attrs.only_app) { - select_options[_attrs.only_app] = this.egw().lang(_attrs.only_app); - } - else if (_attrs.application_list) { - select_options = _attrs.application_list; - } - else { - select_options = egw.link_app_list('query'); - if (typeof select_options['addressbook-email'] !== 'undefined') { - delete select_options['addressbook-email']; - } - } - _attrs.select_options = select_options; - super.transformAttributes(_attrs); - } -} -et2_link_apps._attributes = { - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to just this one application - hides app selection" - }, - "application_list": { - "name": "Application list", - "type": "any", - "default": "", - "description": "Limit to the listed application or applications (comma seperated)" - } -}; -et2_register_widget(et2_link_apps, ["link-apps"]); -/** - * Search and select an entry for linking - */ -export class et2_link_entry extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_link_entry - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_entry._attributes, _child || {})); - this.cache = {}; - this.processing = false; - this.search = null; - this.clear = null; - this.app_select = null; - this._oldValue = { - id: null, - app: this.options.value && this.options.value.app ? this.options.value.app : this.options.only_app - }; - if (typeof this.options.value == 'undefined' || this.options.value == null) { - this.options.value = {}; - } - this.cache = {}; - this.request = null; - this.createInputWidget(); - var self = this; - jQuery(this.getInstanceManager().DOMContainer).on('clear', function () { - // We need to unbind events to prevent a second triggerd event handler - // (eg. setting a project in infolog edit dialog) when the widget gets cleared. - jQuery(self.getDOMNode()).off(); - }); - } - destroy() { - super.destroy.apply(this, arguments); - this.div = null; - if (this.search.data("ui-autocomplete")) { - this.search.autocomplete("destroy"); - } - this.search = null; - this.clear = null; - this.app_select = null; - this.request = null; - } - createInputWidget() { - var self = this; - this.div = jQuery(document.createElement("div")).addClass("et2_link_entry"); - // Application selection - jQuery.widget("custom.iconselectmenu", jQuery.ui.selectmenu, { - _setText: function (element, value) { - if (element === this.buttonText) { - this._setButtonText(value); - } - else { - this._superApply(element, value); - } - }, - _setButtonText: function (value) { - var _value = this.focusIndex; - if (typeof this.focusIndex === 'undefined') { - _value = this.element.find("option:selected").val(); - } - else { - var selected = this.items[_value] || {}; - _value = selected.value; - } - var url = self.egw().image('navbar', _value); - var buttonItem = jQuery("", { - "class": "ui-selectmenu-text", - title: value - }); - jQuery('.ui-selectmenu-text', this.button).replaceWith(buttonItem); - buttonItem.css('background-image', 'url(' + url + ')'); - }, - _renderItem: function (ul, item) { - var li = jQuery("
    • ", { class: "et2_link_entry_app_option" }), wrapper = jQuery("
      ", { text: item.label }); - if (item.disabled) { - li.addClass("ui-state-disabled"); - } - ul.addClass(self.div.attr("class")); - var url = self.egw().image('navbar', item.value); - jQuery("", { - style: 'background-image: url("' + url + '");', - "class": "ui-icon " + item.element.attr("data-class"), - title: item.label - }) - .appendTo(wrapper); - return li.append(wrapper).appendTo(ul); - } - }); - this.app_select = jQuery(document.createElement("select")).appendTo(this.div) - .change(function (e) { - // Clear cache when app changes - self.cache = {}; - // Update preference with new value - egw.set_preference(self.options.value.to_app || self.egw().getAppName(), 'link_app', self.app_select.val()); - if (typeof self.options.value != 'object') - self.options.value = {}; - self.options.value.app = self.app_select.val(); - }) - .attr("aria-label", egw.lang("linkapps")); - var opt_count = 0; - for (var key in this.options.select_options) { - opt_count++; - var option = jQuery(document.createElement("option")) - .attr("value", key) - .text(this.options.select_options[key]); - option.appendTo(this.app_select); - } - if (this.options.only_app) { - this.app_select.val(this.options.only_app); - this.app_select.hide(); - if (this.options.app_icons && this.app_select.iconselectmenu('instance')) { - this.app_select.iconselectmenu('widget').hide(); - } - this.div.addClass("no_app"); - } - else { - // Now that options are in, set to last used app - this.app_select.val(this.options.value.app || ''); - if (this.app_select.iconselectmenu('instance')) { - this.app_select.iconselectmenu('update'); - } - } - // Search input - this.search = jQuery(document.createElement("input")) - // .attr("type", "search") // Fake it for all browsers below - .focus(function () { - if (!self.options.only_app) { - // Adjust width, leave room for app select & link button - self.div.removeClass("no_app"); - if (self.options.app_icons) { - self.app_select.iconselectmenu('widget').show(); - } - else { - self.app_select.show(); - } - } - }) - .blur(function (e) { - if (self.div.has(e.relatedTarget).length) - return; - if (self.options.app_icons) { - // Adjust width, leave room for app select & link button - self.div.addClass("no_app"); - self.app_select.iconselectmenu('widget').hide(); - } - else if (self.search.val()) { - if (self.options.only_app) { - // Adjust width, leave room for app select & link button - self.div.addClass("no_app"); - } - } - }) - .attr("aria-label", egw.lang("Link search")) - .attr("role", "searchbox") - .appendTo(this.div); - this.set_blur(this.options.blur ? this.options.blur : this.egw().lang("search"), this.search); - // Autocomplete - this.search.autocomplete({ - source: function (request, response) { - return self.query(request, response); - }, - select: function (event, item) { - event.data = self; - // Correct changed value from server - if (item.item.value) { - item.item.value = ("" + item.item.value).trim(); - } - self.select(event, item); - return false; - }, - focus: function (event, item) { - event.stopPropagation(); - self.search.val(item.item.label); - return false; - }, - minLength: et2_link_entry.minimum_characters, - delay: et2_link_entry.search_timeout, - disabled: self.options.disabled, - appendTo: self.div - }); - // Custom display (colors) - this.search.data("uiAutocomplete")._renderItem = function (ul, item) { - var li = jQuery(document.createElement('li')) - .data("item.autocomplete", item); - var extra = {}; - // Extra stuff - if (typeof item.label == 'object') { - extra = item.label; - item.label = extra.label ? extra.label : extra; - if (extra['style.backgroundColor'] || extra.color) { - li.css({ 'border-left': '5px solid ' + (extra.color ? extra.color : extra['style.backgroundColor']) }); - } - // Careful with this, some browsers may have trouble loading all at once, which can slow display - if (extra.icon) { - var img = self.egw().image(extra.icon); - if (img) { - jQuery(document.createElement("img")) - .attr("src", img) - .css("float", "right") - .appendTo(li); - } - } - } - // Normal stuff - li.append(jQuery("").text(item.label)) - .appendTo(ul); - window.setTimeout(function () { ul.css('max-width', jQuery('.et2_container').width() - ul.offset().left); }, 300); - return li; - }; - // Bind to enter key to start search early - this.search.keydown(function (e) { - var keycode = (e.keyCode ? e.keyCode : e.which); - if (keycode == 13 && !self.processing) { - self.search.autocomplete("option", "minLength", 0); - self.search.autocomplete("search"); - self.search.autocomplete("option", "minLength", self.minimum_characters); - return false; - } - }); - // Clear / last button - this.clear = jQuery(document.createElement("span")) - .addClass("ui-icon ui-icon-close") - .click(function (e) { - if (!self.search) - return; // only gives an error, we should never get into that situation - // No way to tell if the results is open, so if they click the button while open, it clears - if (self.last_search && self.last_search != self.search.val()) { - // Repeat last search (should be cached) - self.search.val(self.last_search); - self.last_search = ""; - self.search.autocomplete("search"); - } - else { - // Clear - self.search.autocomplete("close"); - self.set_value(null); - self.search.val(""); - // call trigger, after finishing this handler, not in the middle of it - window.setTimeout(function () { - self.search.trigger("change"); - }, 0); - } - self.search.focus(); - }) - .appendTo(this.div) - .hide(); - this.setDOMNode(this.div[0]); - } - getDOMNode() { - return this.div ? this.div[0] : null; - } - transformAttributes(_attrs) { - super.transformAttributes.apply(this, arguments); - _attrs["select_options"] = {}; - if (_attrs["application_list"]) { - var apps = (typeof _attrs["application_list"] == "string") ? et2_csvSplit(_attrs["application_list"], null, ",") : _attrs["application_list"]; - for (var i = 0; i < apps.length; i++) { - _attrs["select_options"][apps[i]] = this.egw().lang(apps[i]); - } - } - else { - _attrs["select_options"] = this.egw().link_app_list('query'); - if (typeof _attrs["select_options"]["addressbook-email"] != 'undefined') - delete _attrs["select_options"]["addressbook-email"]; - } - // Check whether the options entry was found, if not read it from the - // content array. - if (_attrs["select_options"] == null) { - _attrs["select_options"] = this.getArrayMgr('content') - .getEntry("options-" + this.id); - } - // Default to an empty object - if (_attrs["select_options"] == null) { - _attrs["select_options"] = {}; - } - } - doLoadingFinished() { - if (typeof this.options.value == 'object' && !this.options.value.app) { - this.options.value.app = egw.preference('link_app', this.options.value.to_app || this.egw().getAppName()); - // If there's no value set for app, then take the first one from the selectbox - if (typeof this.options.value.app == 'undefined' || !this.options.value.app) { - this.options.value.app = Object.keys(this.options.select_options)[0]; - } - this.app_select.val(this.options.value.app); - } - if (this.options.app_icons) { - var self = this; - this.div.addClass('app_icons'); - this.app_select.iconselectmenu({ - width: 50, - change: function () { - window.setTimeout(function () { - self.app_select.trigger("change"); - }, 0); - } - }) - .iconselectmenu("menuWidget"); - this.app_select.iconselectmenu('widget').hide(); - } - return super.doLoadingFinished.apply(this, arguments); - } - getValue() { - var value = this.options && this.options.only_app ? this.options.value.id : this.options ? this.options.value : null; - if (this.options && !this.options.only_app && this.search) { - value.search = this.search.val(); - } - return value; - } - set_value(_value) { - if (typeof _value == 'string' || typeof _value == 'number') { - if (typeof _value == 'string' && _value.indexOf(",") > 0) - _value = _value.replace(",", ":"); - if (typeof _value == 'string' && _value.indexOf(":") >= 0) { - var split = _value.split(":"); - _value = { - app: split.shift(), - id: split.length == 1 ? split[0] : split - }; - } - else if (_value && this.options.only_app) { - _value = { - app: this.options.only_app, - id: _value - }; - } - } - this._oldValue = this.options.value; - if (!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value)) { - this.search.val(""); - this.clear.hide(); - this.options.value = _value = { 'id': null }; - } - if (!_value.app) - _value.app = this.options.only_app || this.app_select.val(); - if (_value.id) { - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display', ''); - } - else { - this.clear.hide(); - return; - } - if (typeof _value != 'object' || (!_value.app && !_value.id)) { - console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); - return; - } - if (!_value.title) { - var title = this.egw().link_title(_value.app, _value.id); - if (title != null) { - _value.title = title; - } - else { - // Title will be fetched from server and then set - var title = this.egw().link_title(_value.app, _value.id, function (title) { - this.search.removeClass("loading").val(title + ""); - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display', ''); - }, this); - this.search.addClass("loading"); - } - } - if (_value.title) { - this.search.val(_value.title + ""); - } - this.options.value = _value; - jQuery("option[value='" + _value.app + "']", this.app_select).prop("selected", true); - this.app_select.hide(); - if (this.options.app_icons && this.app_select.iconselectmenu('instance')) { - this.app_select.iconselectmenu('widget').hide(); - } - this.div.addClass("no_app"); - } - set_blur(_value, input) { - if (typeof input == 'undefined') - input = this.search; - if (_value) { - input.attr("placeholder", _value); // HTML5 - if (!input[0].placeholder) { - // Not HTML5 - if (input.val() == "") - input.val(_value); - input.focus(input, function (e) { - var placeholder = _value; - if (e.data.val() == placeholder) - e.data.val(""); - }).blur(input, function (e) { - var placeholder = _value; - if (e.data.val() == "") - e.data.val(placeholder); - }); - if (input.val() == "") { - input.val(_value); - } - } - } - else { - this.search.removeAttr("placeholder"); - } - } - /** - * Set the query callback - * - * @param {function} f - */ - set_query(f) { - this.options.query = f; - } - /** - * Set the select callback - * - * @param {function} f - */ - set_select(f) { - this.options.select = f; - } - /** - * Ask server for entries matching selected app/type and filtered by search string - * - * @param {Object} request - * @param {Object} response - */ - query(request, response) { - // If there is a pending request, abort it - if (this.request) { - this.request.abort(); - this.request = null; - } - // Remember last search - this.last_search = this.search.val(); - // Allow hook / tie in - if (this.options.query && typeof this.options.query == 'function') { - if (!this.options.query(request, this)) - return false; - } - if ((typeof request.no_cache == 'undefined' && !request.no_cache) && request.term in this.cache) { - return response(this.cache[request.term]); - } - // Remember callback - this.response = response; - this.search.addClass("loading"); - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display', ''); - this.request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search", [this.app_select.val(), '', request.term, request.options], this._results, this, true, this).sendRequest(); - } - /** - * User selected a value - * - * @param {Object} event - * @param {Object} selected - * - */ - select(event, selected) { - if (selected.item.value !== null && typeof selected.item.value == "string") { - // Correct changed value from server - selected.item.value = selected.item.value.trim(); - } - if (this.options.select && typeof this.options.select == 'function') { - if (!this.options.select(event, selected)) - return false; - } - if (typeof event.data.options.value != 'object' || event.data.options.value == null) { - event.data.options.value = {}; - } - event.data.options.value.id = selected.item.value; - // Set a processing flag to filter some events - event.data.processing = true; - // Remove specific display and revert to CSS file - // show() would use inline, should be inline-block - this.clear.css('display', ''); - event.data.search.val(selected.item.label); - // Fire change event - this.search.change(); - // Turn off processing flag when done - window.setTimeout(jQuery.proxy(function () { delete this.processing; }, event.data)); - } - /** - * Server found some results - * - * @param {Array} data - */ - _results(data) { - if (this.request) { - this.request = null; - } - this.search.removeClass("loading"); - var result = []; - for (var id in data) { - result.push({ "value": id, "label": data[id] }); - } - this.cache[this.search.val()] = result; - this.response(result); - } - /** - * Create a link using the current internal values - * - * @param {Object} event - * @param {Object} _links - */ - createLink(event, _links) { - var values = event.data.options.value; - var self = event.data; - var links = []; - if (typeof _links == 'undefined') { - links = []; - } - else { - links = _links; - } - // Links to other entries - if (values.id) { - links.push({ - app: values.app, - id: values.id - }); - self.search.val(""); - } - // If a link array was passed in, don't make the ajax call - if (typeof _links == 'undefined') { - var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link", [values.to_app, values.to_id, links], self._link_result, this, true); - request.sendRequest(); - } - } - /** - * Sent some links, server has a result - * - * @param {Object} success - * - */ - _link_result(success) { - if (success) { - this.status_span.fadeIn().delay(1000).fadeOut(); - delete this.options.value.app; - delete this.options.value.id; - } - } -} -et2_link_entry._attributes = { - "value": { - "type": "any", - "default": {} - }, - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to just this one application - hides app selection" - }, - "application_list": { - "name": "Application list", - "type": "any", - "default": "", - "description": "Limit to the listed applications (comma seperated)" - }, - "app_icons": { - "name": "Application icons", - "type": "boolean", - "default": false, - "description": "Show application icons instead of names" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": et2_no_init, - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text.", - translate: true - }, - "query": { - "name": "Query callback", - "type": "js", - "default": et2_no_init, - "description": "Callback before query to server. It will be passed the request & et2_link_entry objects. Must return true, or false to abort query." - }, - "select": { - "name": "Select callback", - "type": "js", - "default": et2_no_init, - "description": "Callback when user selects an option. Must return true, or false to abort normal action." - } -}; -et2_link_entry.legacyOptions = ["only_app", "application_list"]; -et2_link_entry.search_timeout = 500; //ms after change to send query -et2_link_entry.minimum_characters = 4; // Don't send query unless there's at least this many chars -et2_register_widget(et2_link_entry, ["link-entry"]); -/** - * UI widget for a single (read-only) link - * - */ -export class et2_link extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_link - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link._attributes, _child || {})); - this.label_span = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.link = jQuery(document.createElement("span")) - .addClass("et2_link") - .appendTo(this.label_span); - if (this.options['class']) - this.label_span.addClass(this.options['class']); - this.setDOMNode(this.label_span[0]); - } - destroy() { - if (this.link) - this.link.unbind(); - this.link = null; - super.destroy.apply(this, arguments); - } - set_label(label) { - // Remove current label - this.label_span.contents() - .filter(function () { return this.nodeType == 3; }).remove(); - var parts = et2_csvSplit(label, 2, "%s"); - this.label_span.prepend(parts[0]); - this.label_span.append(parts[1]); - this.label = label; - // add class if label is empty - this.label_span.toggleClass('et2_label_empty', !label || !parts[0]); - } - set_value(_value) { - if (typeof _value != 'object' && _value && !this.options.only_app) { - if (_value.indexOf(':') >= 0) { - var app = _value.split(':', 1); - var id = _value.substr(app[0].length + 1); - _value = { 'app': app[0], 'id': id }; - } - else { - console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); - return; - } - } - // Application set, just passed ID - else if (typeof _value != "object") { - _value = { - app: this.options.only_app, - id: _value - }; - } - if (!_value || jQuery.isEmptyObject(_value)) { - this.link.text("").unbind(); - return; - } - var self = this; - this.link.unbind(); - if (_value.id && _value.app) { - this.link.addClass("et2_link"); - this.link.click(function (e) { - // try to fetch value.title if it wasn't fetched during initiation. - if (!_value.title) - _value.title = self.egw().link_title(_value.app, _value.id); - if (!self.options.target_app) { - self.options.target_app = _value.app; - } - const target = self.options.extra_link_target || _value.app; - self.egw().open(_value, "", self.options.link_hook, _value.extra_args, target, self.options.target_app); - e.stopImmediatePropagation(); - }); - } - else { - this.link.removeClass("et2_link"); - } - if (!_value.title) { - var self = this; - var node = this.link[0]; - if (_value.app && _value.id) { - var title = this.egw().link_title(_value.app, _value.id, function (title) { self.set_title(node, title); }, this); - if (title != null) { - _value.title = title; - } - else { - // Title will be fetched from server and then set - return; - } - } - else { - _value.title = ""; - } - } - this.set_title(this.link, _value.title); - } - /** - * Sets the text to be displayed. - * Used as a callback, so node is provided to make sure we get the right one - * - * @param {Object} node - * @param {String} _value description - */ - set_title(node, _value) { - if (_value === false || _value === null) - _value = ""; - if (this.options.break_title) { - // Set up title to optionally break on the provided character - replace all space with nbsp, add a - // zero-width space after the break string - _value = _value - .replace(this.options.break_title, this.options.break_title.trimEnd() + "\u200B") - .replace(/ /g, '\u00a0'); - } - jQuery(node).text(_value + ""); - } - /** - * Creates a list of attributes which can be set when working in the - * "detached" mode. The result is stored in the _attrs array which is provided - * by the calling code. - * - * @param {Array} _attrs an array of attributes - */ - getDetachedAttributes(_attrs) { - _attrs.push("label", "value"); - } - /** - * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - */ - getDetachedNodes() { - return [this.node, this.link[0]]; - } - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which have to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes(_nodes, _values) { - this.node = _nodes[0]; - this.label_span = jQuery(_nodes[0]); - this.link = jQuery(_nodes[1]); - if (typeof _values["id"] !== "undefined") - this.set_id(_values['id']); - if (typeof _values["label"] !== "undefined") - this.set_label(_values['label']); - if (typeof _values["value"] !== "undefined") - this.set_value(_values["value"]); - } -} -et2_link._attributes = { - "only_app": { - "name": "Application", - "type": "string", - "default": "", - "description": "Use the given application, so you can pass just the ID for value" - }, - "value": { - description: "Array with keys app, id, and optionally title", - type: "any" - }, - "needed": { - "ignore": true - }, - "link_hook": { - "name": "Type", - "type": "string", - "default": "view", - "description": "Hook used for displaying link (view/edit/add)" - }, - "target_app": { - "name": "Target application", - "type": "string", - "default": "", - "description": "Optional parameter to be passed to egw().open in order to open links in specified application" - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": null, - "description": "Optional parameter to be passed to egw().open in order to open links in specified target eg. _blank" - }, - "break_title": { - "name": "break title", - "type": "string", - "default": null, - "description": "Breaks title into multiple lines based on selected delimiter by replacing it with '\r\n'" - } -}; -et2_link.legacyOptions = ["only_app"]; -et2_register_widget(et2_link, ["link", "link-entry_ro"]); -/** - * UI widget for one or more links, comma separated - * - * TODO: This one used to have expose - */ -export class et2_link_string extends expose((_a = class et2_link_string extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_link_string - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_string._attributes, _child || {})); - this.list = jQuery(document.createElement("ul")) - .addClass("et2_link_string"); - if (this.options['class']) - this.list.addClass(this.options['class']); - this.setDOMNode(this.list[0]); - } - destroy() { - super.destroy.apply(this, arguments); - if (this.node != null) { - jQuery(this.node).children().unbind(); - } - } - set_value(_value) { - // Get data - if (!_value || _value == null || !this.list) { - // List can be missing if the AJAX call returns after the form is destroyed - if (this.list) { - this.list.empty(); - } - return; - } - if (typeof _value == "string" && _value.indexOf(',') > 0) { - _value = _value.split(','); - } - if (!_value.to_app && typeof _value == "object" && this.options.application) { - _value.to_app = this.options.application; - } - if (typeof _value == 'object' && _value.to_app && _value.to_id) { - this.value = _value; - this._get_links(); - return; - } - this.list.empty(); - if (typeof _value == 'object' && _value.length > 0) { - // Have full info - // Don't store new value, just update display - // Make new links - for (var i = 0; i < _value.length; i++) { - if (!this.options.only_app || this.options.only_app && _value[i].app == this.options.only_app) { - this._add_link(_value[i].id ? _value[i] : { id: _value[i], app: _value.to_app }); - } - } - } - else if (this.options.application) { - this._add_link({ id: _value, app: this.options.application }); - } - } - _get_links() { - var _value = this.value; - // Just IDs - get from server - if (this.options.only_app) { - _value.only_app = this.options.only_app; - } - this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value], this.set_value, this); - return; - } - /** - * Function to get media content to feed the expose - * @param {type} _value - * @returns {Array|Array.getMedia.mediaContent} - */ - getMedia(_value) { - let base_url = egw.webserverUrl.match(/^\//, 'ig') ? egw(window).window.location.origin + egw.webserverUrl : egw.webserverUrl; - let mediaContent = []; - if (_value && typeof _value.type != 'undefined' && _value.type.match(/video\/|audio\//, 'ig')) { - mediaContent = [{ - title: _value.id, - type: _value.type, - poster: '', - href: base_url + egw().mime_open(_value), - download_href: base_url + egw().mime_open(_value) + '?download' - }]; - } - else if (_value) { - mediaContent = [{ - title: _value.id, - href: base_url + egw().mime_open(_value).url, - download_href: base_url + egw().mime_open(_value).url + '?download', - type: _value.type - }]; - } - if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/, 'ig')) - mediaContent[0]["download_href"] = mediaContent[0].href + '?download'; - return mediaContent; - } - _add_link(_link_data) { - var self = this; - var link = jQuery(document.createElement("li")) - .appendTo(this.list) - .addClass("et2_link loading") - .click(function (e) { - var fe = egw_get_file_editor_prefered_mimes(_link_data.type); - if (self.options.expose_view && typeof _link_data.type != 'undefined' - && _link_data.type.match(self.mime_regexp, 'ig') && !_link_data.type.match(self.mime_audio_regexp, 'ig')) { - self._init_blueimp_gallery(e, _link_data); - } - else if (_link_data.type && _link_data.type.match(self.mime_audio_regexp, 'ig')) { - self._audio_player(_link_data); - } - else if (typeof _link_data.type != 'undefined' && fe && fe.mime && fe.mime[_link_data.type]) { - egw.open_link(egw.link('/index.php', { - menuaction: fe.edit.menuaction, - path: egw().mime_open(_link_data).url.replace('/webdav.php', '') - }), '', fe.edit_popup); - } - else { - self.egw().open(_link_data, "", "view", null, _link_data.app, _link_data.app); - } - e.stopImmediatePropagation(); - }); - if (_link_data.title) { - link.text(_link_data.title) - .removeClass("loading"); - } - // Now that link is created, get title from server & update - else { - this.egw().link_title(_link_data.app, _link_data.id, function (title) { - if (title) - this.removeClass("loading").text(title); - else - this.remove(); // no rights or not found - }, link); - } - } - /** - * Creates a list of attributes which can be set when working in the - * "detached" mode. The result is stored in the _attrs array which is provided - * by the calling code. - * - * @param {Array} _attrs an array of attributes - */ - getDetachedAttributes(_attrs) { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - this.getSurroundings().update(); - } - _attrs.push("value", "label"); - } - /** - * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - */ - getDetachedNodes() { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) { - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label"); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - return [this.list[0], this._labelContainer[0]]; - } - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which have to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes(_nodes, _values) { - this.list = jQuery(_nodes[0]); - this.set_value(_values["value"]); - // Special detached, to prevent DOM node modification of the normal method - this._labelContainer = _nodes.length > 1 ? jQuery(_nodes[1]) : null; - if (_values['label']) { - this.set_label(_values['label']); - } - else if (this._labelContainer) { - this._labelContainer.contents().not(this.list).remove(); - } - } - }, - _a._attributes = { - "application": { - "name": "Application", - "type": "string", - "default": "", - "description": "Use the given application, so you can pass just the ID for value" - }, - "value": { - "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", - "type": "any" - }, - "only_app": { - "name": "Application filter", - "type": "string", - "default": "", - "description": "Appname, eg. 'projectmananager' to list only linked projects" - }, - "link_type": { - "name": "Type filter", - "type": "string", - "default": "", - "description": "Sub-type key to list only entries of that type" - }, - "expose_view": { - name: "Expose view", - type: "boolean", - default: true, - description: "Clicking on description with href value would popup an expose view, and will show content referenced by href." - } - }, - _a)) { -} -; -et2_register_widget(et2_link_string, ["link-string"]); -/** - * UI widget for one or more links in a list (table) - */ -export class et2_link_list extends et2_link_string { - /** - * Constructor - * - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_list._attributes, _child || {})); - this.list = jQuery(document.createElement("table")) - .addClass("et2_link_list"); - if (this.options['class']) - this.list.addClass(this.options['class']); - this.setDOMNode(this.list[0]); - // Set up context menu - var self = this; - this.context = new egwMenu(); - this.context.addItem("comment", this.egw().lang("Comment"), "", function () { - var link_id = typeof self.context.data.link_id == 'number' ? self.context.data.link_id : self.context.data.link_id.replace(/[:\.]/g, '_'); - et2_dialog.show_prompt(function (button, comment) { - if (button != et2_dialog.OK_BUTTON) - return; - var remark = jQuery('#link_' + (self.context.data.dom_id ? self.context.data.dom_id : link_id), self.list).children('.remark'); - if (isNaN(self.context.data.link_id)) // new entry, not yet stored - { - remark.text(comment); - // Look for a link-to with the same ID, refresh it - if (self.context.data.link_id) { - var _widget = link_id.widget || null; - self.getRoot().iterateOver(function (widget) { - if (widget.id == self.id) { - _widget = widget; - } - }, self, et2_link_to); - var value = _widget != null ? _widget.getValue() : false; - if (_widget && value && value.to_id) { - value.to_id[self.context.data.link_id].remark = comment; - } - } - return; - } - remark.addClass("loading"); - var request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_comment", [link_id, comment], function () { - if (remark) { - // Append "" to make sure it's a string, not undefined - remark.removeClass("loading").text(comment + ""); - // Update internal data - self.context.data.remark = comment + ""; - } - }, this, true).sendRequest(); - }, '', self.egw().lang("Comment"), self.context.data.remark || ''); - }); - this.context.addItem("file_info", this.egw().lang("File information"), this.egw().image("edit"), function (menu_item) { - var link_data = self.context.data; - if (link_data.app == 'file') { - // File info is always the same - var url = '/apps/' + link_data.app2 + '/' + link_data.id2 + '/' + decodeURIComponent(link_data.id); - if (typeof url == 'string' && url.indexOf('webdav.php')) { - // URL is url to file in webdav, so get rid of that part - url = url.replace('/webdav.php', ''); - } - self.egw().open(url, "filemanager", "edit"); - } - }); - this.context.addItem("-", "-"); - this.context.addItem("save", this.egw().lang("Save as"), this.egw().image('save'), function (menu_item) { - var link_data = self.context.data; - // Download file - if (link_data.download_url) { - var url = link_data.download_url; - if (url[0] == '/') - url = egw.link(url); - let a = document.createElement('a'); - if (typeof a.download == "undefined") { - window.location.href = url + "?download"; - return false; - } - // Multiple file download for those that support it - a = jQuery(a) - .prop('href', url) - .prop('download', link_data.title || "") - .appendTo(self.getInstanceManager().DOMContainer); - var evt = document.createEvent('MouseEvent'); - evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - a[0].dispatchEvent(evt); - a.remove(); - return false; - } - self.egw().open(link_data, "", "view", 'download', link_data.target ? link_data.target : link_data.app, link_data.app); - }); - this.context.addItem("zip", this.egw().lang("Save as Zip"), this.egw().image('save_zip'), function (menu_item) { - // Highlight files for nice UI indicating what will be in the zip. - // Files have negative IDs. - jQuery('[id^="link_-"]', this.list).effect('highlight', {}, 2000); - // Download ZIP - window.location = self.egw().link('/index.php', { - menuaction: 'api.EGroupware\\Api\\Etemplate\\Widget\\Link.download_zip', - app: self.value.to_app, - id: self.value.to_id - }); - }); - // Only allow this option if the entry has been saved, and has a real ID - if (self.options.value && self.options.value.to_id && typeof self.options.value.to_id != 'object') { - this.context.addItem("copy_to", this.egw().lang("Copy to"), this.egw().image('copy'), function (menu_item) { - // Highlight files for nice UI indicating what will be copied - jQuery('[id="link_' + self.context.data.link_id + ']', this.list).effect('highlight', {}, 2000); - // Get target - var select_attrs = { - mode: "select-dir", - button_caption: '', - button_icon: 'copy', - button_label: egw.lang("copy"), - //extra_buttons: [{text: egw.lang("link"), id:"link", image: "link"}], - dialog_title: egw.lang('Copy to'), - method: "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_copy_to", - method_id: self.context.data - }; - let vfs_select = et2_createWidget("vfs-select", select_attrs, self); - // No button, just open it - vfs_select.button.hide(); - vfs_select.click(null); - }); - } - this.context.addItem("-", "-"); - this.context.addItem("delete", this.egw().lang("Delete link"), this.egw().image("delete"), function (menu_item) { - var link_id = isNaN(self.context.data.link_id) ? self.context.data : self.context.data.link_id; - var row = jQuery('#link_' + (self.context.data.dom_id ? self.context.data.dom_id : self.context.data.link_id), self.list); - et2_dialog.show_dialog(function (button) { if (button == et2_dialog.YES_BUTTON) - self._delete_link(link_id, row); }, egw.lang('Delete link?')); - }); - // Native DnD - Doesn't play nice with jQueryUI Sortable - // Tell jQuery to include this property - jQuery.event.props.push('dataTransfer'); - } - destroy() { - super.destroy.apply(this, arguments); - if (this.context) { - this.context.clear(); - delete this.context; - } - } - set_value(_value) { - this.list.empty(); - // Handle server passed a list of links that aren't ready yet - if (_value && typeof _value == "object") { - var list = []; - if (_value.to_id && typeof _value.to_id == "object") { - list = _value.to_id; - } - else if (_value.length) { - list = _value; - } - if (list.length > 0) { - for (var id in list) { - var link = list[id]; - if (link.app) { - // Temp IDs can cause problems since the ID includes the file name or : - if (link.link_id && typeof link.link_id != 'number') { - link.dom_id = 'temp_' + egw.uid(); - } - // Icon should be in registry - if (!link.icon) { - link.icon = egw.link_get_registry(link.app, 'icon'); - // No icon, try by mime type - different place for un-saved entries - if (link.icon == false && link.id.type) { - // Triggers icon by mime type, not thumbnail or app - link.type = link.id.type; - link.icon = true; - } - } - // Special handling for file - if not existing, we can't ask for title - if (typeof link.id == 'object' && !link.title) { - link.title = link.id.name || ''; - } - this._add_link(link); - } - } - } - else { - super.set_value(_value); - } - } - } - _add_link(_link_data) { - var row = jQuery(document.createElement("tr")) - .attr("id", "link_" + (_link_data.dom_id ? _link_data.dom_id : (typeof _link_data.link_id == "string" ? _link_data.link_id.replace(/[:\.]/g, '_') : _link_data.link_id || _link_data.id))) - .attr("draggable", _link_data.app == 'file' ? "true" : "") - .appendTo(this.list); - if (!_link_data.link_id) { - for (var k in _link_data) { - row[0].dataset[k] = _link_data[k]; - } - } - // Icon - var icon = jQuery(document.createElement("td")) - .appendTo(row) - .addClass("icon"); - if (_link_data.icon) { - var icon_widget = et2_createWidget("image", {}); - var src = ''; - // Creat a mime widget if the link has type - if (_link_data.type) { - // VFS - file - var vfs_widget = et2_createWidget('vfs-mime', {}); - vfs_widget.set_value({ - download_url: _link_data.download_url, - name: _link_data.title, - mime: _link_data.type, - path: _link_data.icon - }); - icon.append(vfs_widget.getDOMNode()); - } - else { - src = this.egw().image(_link_data.icon); - if (src) - icon_widget.set_src(src); - icon.append(icon_widget.getDOMNode()); - } - } - var columns = ['title', 'remark']; - var self = this; - for (var i = 0; i < columns.length; i++) { - var $td = jQuery(document.createElement("td")) - .appendTo(row) - .addClass(columns[i]) - .text(_link_data[columns[i]] ? _link_data[columns[i]] + "" : ""); - var dirs = _link_data[columns[i]] ? _link_data[columns[i]].split('/') : []; - if (columns[i] == 'title' && _link_data.type && dirs.length > 1) { - this._format_vfs($td, dirs, _link_data); - } - //Bind the click handler if there is download_url - if (_link_data && (typeof _link_data.download_url != 'undefined' || _link_data.app != 'egw-data')) { - $td.click(function () { - var fe_mime = egw_get_file_editor_prefered_mimes(_link_data.type); - // Check if the link entry is mime with media type, in order to open it in expose view - if (typeof _link_data.type != 'undefined' && - (_link_data.type.match(self.mime_regexp, 'ig') || (fe_mime && fe_mime.mime[_link_data.type]))) { - var $vfs_img_node = jQuery(this).parent().find('.vfsMimeIcon'); - if ($vfs_img_node.length > 0) - $vfs_img_node.click(); - } - else { - if (!self.options.target_app) { - self.options.target_app = _link_data.app; - } - self.egw().open(_link_data, "", "view", null, _link_data.target ? _link_data.target : _link_data.app, self.options.target_app); - } - }); - } - } - if (typeof _link_data.title == 'undefined') { - // Title will be fetched from server and then set - jQuery('td.title', row).addClass("loading"); - var title = this.egw().link_title(_link_data.app, _link_data.id, function (title) { - jQuery('td.title', this).removeClass("loading").text(title + ""); - }, row); - } - // Date - /* - var date_row = jQuery(document.createElement("td")) - .appendTo(row); - if(_link_data.lastmod) - { - var date_widget = et2_createWidget("date-since"); - date_widget.set_value(_link_data.lastmod); - date_row.append(date_widget.getDOMNode()); - } - */ - // Delete - // build delete button if the link is not readonly - if (!this.options.readonly) { - var delete_button = jQuery(document.createElement("td")) - .appendTo(row); - jQuery("
      ") - .appendTo(delete_button) - // We don't use ui-icon because it assigns a bg image - .addClass("delete icon") - .bind('click', function () { - et2_dialog.show_dialog(function (button) { - if (button == et2_dialog.YES_BUTTON) { - self._delete_link(self.value && typeof self.value.to_id != 'object' && _link_data.link_id ? _link_data.link_id : _link_data, row); - } - }, egw.lang('Delete link?')); - }); - } - // Context menu - row.bind("contextmenu", function (e) { - // Comment only available if link_id is there and not readonly - self.context.getItem("comment").set_enabled(typeof _link_data.link_id != 'undefined' && !self.options.readonly); - // File info only available for existing files - self.context.getItem("file_info").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); - self.context.getItem("save").set_enabled(typeof _link_data.id != 'object' && _link_data.app == 'file'); - // Zip download only offered if there are at least 2 files - self.context.getItem("zip").set_enabled(jQuery('[id^="link_-"]', this.list).length >= 2); - // Show delete item only if the widget is not readonly - self.context.getItem("delete").set_enabled(!self.options.readonly); - self.context.data = _link_data; - self.context.showAt(e.pageX, e.pageY, true); - e.preventDefault(); - }); - // Drag - adapted from egw_action_dragdrop, sidestepping action system - // so all linked files get it - // // Unfortunately, dragging files is currently only supported by Chrome - if (navigator && navigator.userAgent.indexOf('Chrome') >= 0) { - row.on("dragstart", _link_data, function (event) { - if (event.dataTransfer == null) { - return; - } - var data = event.data || {}; - if (data && data.type && data.download_url) { - event.dataTransfer.dropEffect = "copy"; - event.dataTransfer.effectAllowed = "copy"; - var url = data.download_url; - // NEED an absolute URL - if (url[0] == '/') - url = egw.link(url); - // egw.link adds the webserver, but that might not be an absolute URL - try again - if (url[0] == '/') - url = window.location.origin + url; - // Unfortunately, dragging files is currently only supported by Chrome - if (navigator && navigator.userAgent.indexOf('Chrome')) { - event.dataTransfer.setData("DownloadURL", data.type + ':' + data.title + ':' + url); - } - // Include URL as a fallback - event.dataTransfer.setData("text/uri-list", url); - } - if (event.dataTransfer.types.length == 0) { - // No file data? Abort: drag does nothing - event.preventDefault(); - return; - } - //event.dataTransfer.setDragImage(event.delegate.target,0,0); - var div = jQuery(document.createElement("div")) - .attr('id', 'drag_helper') - .css({ - position: 'absolute', - top: '0px', - left: '0px', - width: '300px' - }); - div.append(event.target.cloneNode(true)); - self.list.append(div); - event.dataTransfer.setDragImage(div.get(0), 0, 0); - }) - .on('drag', function () { - jQuery('#drag_helper', self.list).remove(); - }); - } - } - _delete_link(link_id, row) { - if (row) { - var delete_button = jQuery('.delete', row); - delete_button.removeClass("delete").addClass("loading"); - row.off(); - } - if (this.onchange) { - this.onchange(this, link_id, row); - } - if (typeof link_id != "object") { - egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_delete", [link_id], function (data) { if (data) { - row.slideUp(row.remove); - } }).sendRequest(); - } - else if (row) { - // No link ID means a link on an unsaved entry. - // Just remove the row, but need to adjust the link_to value also - row.slideUp(row.remove); - // Look for a link-to with the same ID, refresh it - if (link_id.link_id) { - var self = this; - var _widget = link_id.widget || null; - this.getRoot().iterateOver(function (widget) { - if (widget.id == self.id) { - _widget = widget; - } - }, this, et2_link_to); - var value = _widget != null ? _widget.getValue() : false; - if (_widget && value && value.to_id) { - delete value.to_id[link_id.link_id]; - _widget.set_value(value); - } - } - } - } - /** - * When the link is to a VFS file, we do some special formatting. - * - * Instead of listing the full path, we use - * Path: - filename - * When multiple files from the same directory are linked, we exclude - * the directory name from all but the first link to that directory - * - * @param {JQuery} $td Current table data cell for the title - * @param {String[]} dirs List of directories in the linked file's path - * @param {String[]} _link_data Data for the egw_link - * @returns {undefined} - */ - _format_vfs($td, dirs, _link_data) { - // Keep it here for matching next row - $td.attr('data-title', _link_data['title']); - // VFS link - check for same dir as above, and hide dir - var reformat = false; - var span_size = 1; - var prev = jQuery('td.title', $td.parent().prev('tr')); - if (prev.length === 1) { - var prev_dirs = (prev.attr('data-title') || '').split('/'); - if (prev_dirs.length > 1 && prev_dirs.length == dirs.length) { - for (var i = 0; i < dirs.length; i++) { - // Current is same as prev, blank it - if (dirs[i] === prev_dirs[i]) { - reformat = true; - span_size += dirs[i].length + 1; - dirs[i] = ''; - } - else { - break; - } - } - } - } - var filename = dirs.pop(); - if (reformat && (dirs.length - i) === 0) { - $td.html(' - ' + filename); - } - else { - // Different format for directory - span_size += dirs.join('/').length + 1; - $td.html('' + dirs.join('/') + ': - ' + filename); - } - } -} -et2_link_list._attributes = { - "show_deleted": { - "name": "Show deleted", - "type": "boolean", - "default": false, - "description": "Show links that are marked as deleted, being held for purge" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the links change." - }, - readonly: { - name: "readonly", - type: "boolean", - "default": false, - description: "Does NOT allow user to enter data, just displays existing data" - }, - "target_app": { - "name": "Target application", - "type": "string", - "default": "", - "description": "Optional parameter to be passed to egw().open in order to open links in specified application " - } -}; -et2_register_widget(et2_link_list, ["link-list"]); -/** - * - * - */ -export class et2_link_add extends et2_inputWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_link_add._attributes, _child || {})); - this.span = jQuery(document.createElement("span")) - .text(this.egw().lang("Add new")) - .addClass('et2_link_add_span'); - this.div = jQuery(document.createElement("div")).append(this.span); - this.setDOMNode(this.div[0]); - } - doLoadingFinished() { - super.doLoadingFinished.apply(this, arguments); - if (this.app_select && this.button) { - // Already done - return false; - } - this.app_select = et2_createWidget("link-apps", jQuery.extend({}, this.options, { - 'id': this.options.id + 'app', - value: this.options.application ? this.options.application : this.options.value && this.options.value.add_app ? this.options.value.add_app : null, - application_list: this.options.application ? this.options.application : null - }), this); - this.div.append(this.app_select.getDOMNode()); - this.button = et2_createWidget("button", { id: this.options.id + "_add", label: this.egw().lang("add") }, this); - this.button.set_label(this.egw().lang("add")); - var self = this; - this.button.click = function () { - self.egw().open(self.options.value.to_app + ":" + self.options.value.to_id, self.app_select.get_value(), 'add'); - return false; - }; - this.div.append(this.button.getDOMNode()); - return true; - } - /** - * Should be handled client side. - * Return null to avoid overwriting other link values, in case designer used the same ID for multiple widgets - */ - getValue() { - return null; - } -} -et2_link_add._attributes = { - "value": { - "description": "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id", - "type": "any" - }, - "application": { - "name": "Application", - "type": "string", - "default": "", - "description": "Limit to the listed application or applications (comma seperated)" - } -}; -et2_register_widget(et2_link_add, ["link-add"]); -//# sourceMappingURL=et2_widget_link.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_number.js b/api/js/etemplate/et2_widget_number.js deleted file mode 100644 index 28530c477d..0000000000 --- a/api/js/etemplate/et2_widget_number.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Number object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - et2_widget_textbox; -*/ -import { et2_textbox, et2_textbox_ro } from "./et2_widget_textbox"; -import { et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_no_init } from "./et2_core_common"; -/** - * Class which implements the "int" and textbox type=float XET-Tags - * - * @augments et2_textbox - */ -export class et2_number extends et2_textbox { - /** - * Constructor - * - * @memberOf et2_number - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_number._attributes, _child || {})); - this.min = null; - this.max = null; - this.step = null; - } - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - if (typeof _attrs.validator == 'undefined') { - _attrs.validator = _attrs.type == 'float' ? '/^-?[0-9]*[,.]?[0-9]*$/' : '/^-?[0-9]*$/'; - } - } - /** - * Clientside validation using regular expression in "validator" attribute - * - * @param {array} _messages - */ - isValid(_messages) { - let ok = true; - // if we have a html5 validation error, show it, as this.input.val() will be empty! - if (this.input && this.input[0] && this.input[0].validationMessage && !this.input[0].validity.stepMismatch) { - _messages.push(this.input[0].validationMessage); - ok = false; - } - return super.isValid(_messages) && ok; - } - createInputWidget() { - this.input = jQuery(document.createElement("input")); - this.input.attr("type", "number"); - this.input.addClass("et2_textbox"); - // bind invalid event to change, to trigger our validation - this.input.on('invalid', jQuery.proxy(this.change, this)); - if (this.options.onkeypress && typeof this.options.onkeypress == 'function') { - var self = this; - this.input.keypress(function (_ev) { - return self.options.onkeypress.call(this, _ev, self); - }); - } - this.setDOMNode(this.input[0]); - } - /** - * Set input widget size - * - * Overwritten from et2_textbox as input type=number seems to ignore size, - * therefore we set width in em instead, if not et2_fullWidth given. - * - * @param _size Rather arbitrary size units, approximately characters - */ - set_size(_size) { - if (typeof _size != 'undefined' && _size != this.input.attr("size")) { - this.size = _size; - this.input.attr("size", this.size); - if (typeof this.options.class == 'undefined' || this.options.class.search('et2_fullWidth') == -1) { - this.input.css('width', _size + 'em'); - } - } - } - set_min(_value) { - this.min = _value; - if (this.min == null) { - this.input.removeAttr("min"); - } - else { - this.input.attr("min", this.min); - } - } - set_max(_value) { - this.max = _value; - if (this.max == null) { - this.input.removeAttr("max"); - } - else { - this.input.attr("max", this.max); - } - } - set_step(_value) { - this.step = _value; - if (this.step == null) { - this.input.removeAttr("step"); - } - else { - this.input.attr("step", this.step); - } - } -} -et2_number._attributes = { - "value": { - "type": "float" - }, - // Override default width, numbers are usually shorter - "size": { - "default": 5 - }, - "min": { - "name": "Minimum", - "type": "any", - "default": et2_no_init, - "description": "Minimum allowed value" - }, - "max": { - "name": "Maximum", - "type": "any", - "default": et2_no_init, - "description": "Maximum allowed value" - }, - "step": { - "name": "step value", - "type": "integer", - "default": et2_no_init, - "description": "Step attribute specifies the interval between legal numbers" - }, - "precision": { - // TODO: Implement this in some nice way other than HTML5's step attribute - "name": "Precision", - "type": "integer", - "default": et2_no_init, - "description": "Allowed precision - # of decimal places", - "ignore": true - } -}; -et2_register_widget(et2_number, ["int", "integer", "float"]); -/** - * Extend read-only to tell it to ignore special attributes, which - * would cause warnings otherwise - * @augments et2_textbox_ro - * @class - */ -export class et2_number_ro extends et2_textbox_ro { - set_value(_value) { - if (typeof this.options.precision != 'undefined' && "" + _value != "") { - _value = parseFloat(_value).toFixed(this.options.precision); - } - super.set_value(_value); - } -} -et2_number_ro._attributes = { - min: { ignore: true }, - max: { ignore: true }, - precision: { - name: "Precision", - type: "integer", - default: et2_no_init, - description: "Allowed precision - # of decimal places", - ignore: true - }, - value: { type: "float" } -}; -et2_register_widget(et2_number_ro, ["int_ro", "integer_ro", "float_ro"]); -//# sourceMappingURL=et2_widget_number.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_password.js b/api/js/etemplate/et2_widget_password.js deleted file mode 100644 index b58cb55181..0000000000 --- a/api/js/etemplate/et2_widget_password.js +++ /dev/null @@ -1,261 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Textbox object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Andreas Stöckel - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; - et2_core_valueWidget; -*/ -import './et2_core_common'; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_textbox, et2_textbox_ro } from "./et2_widget_textbox"; -import { et2_dialog } from "./et2_widget_dialog"; -import { egw } from "../jsapi/egw_global"; -/** - * Class which implements the "textbox" XET-Tag - * - * @augments et2_inputWidget - */ -export class et2_password extends et2_textbox { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_password._attributes, _child || {})); - // The password is stored encrypted server side, and passed encrypted. - // This flag is for if we've decrypted the password to show it already - this.encrypted = true; - if (this.options.plaintext) { - this.encrypted = false; - } - } - createInputWidget() { - this.wrapper = jQuery(document.createElement("div")) - .addClass("et2_password"); - this.input = jQuery(document.createElement("input")); - this.input.attr("type", "password"); - // Make autocomplete default value off for password field - // seems browsers not respecting 'off' anymore and started to - // implement a new key called "new-password" considered as switching - // autocomplete off. - // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion - if (this.options.autocomplete === "" || this.options.autocomplete == "off") - this.options.autocomplete = "new-password"; - if (this.options.size) { - this.set_size(this.options.size); - } - if (this.options.blur) { - this.set_blur(this.options.blur); - } - if (this.options.readonly) { - this.set_readonly(true); - } - this.input.addClass("et2_textbox") - .appendTo(this.wrapper); - this.setDOMNode(this.wrapper[0]); - if (this.options.value) { - this.set_value(this.options.value); - } - if (this.options.onkeypress && typeof this.options.onkeypress == 'function') { - var self = this; - this.input.on('keypress', function (_ev) { - return self.options.onkeypress.call(this, _ev, self); - }); - } - this.input.on('change', function () { - this.encrypted = false; - }.bind(this)); - // Show button is needed from start as you can't turn viewable on via JS - let attrs = { - class: "show_hide", - image: "visibility", - onclick: this.toggle_visibility.bind(this), - statustext: this.egw().lang("Show password") - }; - if (this.options.viewable) { - this.show_button = et2_createWidget("button", attrs, this); - } - } - getInputNode() { - return this.input[0]; - } - /** - * Override the parent set_id method to manuipulate the input DOM node - * - * @param {type} _value - * @returns {undefined} - */ - set_id(_value) { - super.set_id(_value); - // Remove the name attribute inorder to affect autocomplete="off" - // for no password save. ATM seems all browsers ignore autocomplete for - // input field inside the form - if (this.options.autocomplete === "off") - this.input.removeAttr('name'); - } - /** - * Set whether or not the password is allowed to be shown in clear text. - * - * @param viewable - */ - set_viewable(viewable) { - this.options.viewable = viewable; - if (viewable) { - jQuery('.show_hide', this.wrapper).show(); - } - else { - jQuery('.show_hide', this.wrapper).hide(); - } - } - /** - * Turn on or off the suggest password button. - * - * When clicked, a password of the set length will be generated. - * - * @param length Length of password to generate. 0 to disable. - */ - set_suggest(length) { - if (typeof length !== "number") { - length = typeof length === "string" ? parseInt(length) : (length ? et2_password.DEFAULT_LENGTH : 0); - } - this.options.suggest = length; - if (length && !this.suggest_button) { - let attrs = { - class: "generate_password", - image: "generate_password", - onclick: this.suggest_password.bind(this), - statustext: this.egw().lang("Suggest password") - }; - this.suggest_button = et2_createWidget("button", attrs, this); - if (this.parentNode) { - // Turned on after initial load, need to run loadingFinished() - this.suggest_button.loadingFinished(); - } - } - if (length) { - jQuery('.generate_password', this.wrapper).show(); - } - else { - jQuery('.generate_password', this.wrapper).hide(); - } - } - /** - * If the password is viewable, toggle the visibility. - * If the password is still encrypted, we'll ask for the user's password then have the server decrypt it. - * - * @param on - */ - toggle_visibility(on) { - if (typeof on !== "boolean") { - on = this.input.attr("type") == "password"; - } - if (!this.options.viewable) { - this.input.attr("type", "password"); - return; - } - if (this.show_button) { - this.show_button.set_image(this.egw().image(on ? 'visibility_off' : 'visibility')); - } - // If we are not encrypted or not showing it, we're done - if (!this.encrypted || !on) { - this.input.attr("type", on ? "textbox" : "password"); - return; - } - // Need username & password to decrypt - let callback = function (button, user_password) { - if (button == et2_dialog.CANCEL_BUTTON) { - return this.toggle_visibility(false); - } - let request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Password::ajax_decrypt", [user_password, this.options.value], function (decrypted) { - if (decrypted) { - this.encrypted = false; - this.input.val(decrypted); - this.input.attr("type", "textbox"); - } - else { - this.set_validation_error(this.egw().lang("invalid password")); - window.setTimeout(function () { - this.set_validation_error(false); - }.bind(this), 2000); - } - }, this, true, this).sendRequest(); - }.bind(this); - let prompt = et2_dialog.show_prompt(callback, this.egw().lang("Enter your password"), this.egw().lang("Authenticate")); - // Make the password prompt a password field - prompt.div.on("load", function () { - jQuery(prompt.template.widgetContainer.getWidgetById('value').getInputNode()) - .attr("type", "password"); - }); - } - /** - * Ask the server for a password suggestion - */ - suggest_password() { - // They need to see the suggestion - this.encrypted = false; - this.options.viewable = true; - this.toggle_visibility(true); - let suggestion = "Suggestion"; - let request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Password::ajax_suggest", [this.options.suggest], function (suggestion) { - this.encrypted = false; - this.input.val(suggestion); - this.input.trigger('change'); - // Check for second password, update it too - let two = this.getParent().getWidgetById(this.id + '_2'); - if (two && two.getType() == this.getType()) { - two.options.viewable = true; - two.toggle_visibility(true); - two.set_value(suggestion); - } - }, this, true, this).sendRequest(); - } - destroy() { - super.destroy(); - } - getValue() { - return this.input.val(); - } -} -et2_password._attributes = { - "autocomplete": { - "name": "Autocomplete", - "type": "string", - "default": "off", - "description": "Whether or not browser should autocomplete that field: 'on', 'off', 'default' (use attribute from form). Default value is set to off." - }, - "viewable": { - "name": "Viewable", - "type": "boolean", - "default": false, - "description": "Allow password to be shown" - }, - "plaintext": { - name: "Plaintext", - type: "boolean", - default: true, - description: "Password is plaintext" - }, - "suggest": { - name: "Suggest password", - type: "integer", - default: 0, - description: "Suggest password length (0 for off)" - } -}; -et2_password.DEFAULT_LENGTH = 16; -et2_register_widget(et2_password, ["passwd"]); -export class et2_password_ro extends et2_textbox_ro { - set_value(value) { - this.value_span.text(value ? "********" : ""); - } -} -et2_register_widget(et2_password_ro, ["passwd_ro"]); -//# sourceMappingURL=et2_widget_password.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_portlet.js b/api/js/etemplate/et2_widget_portlet.js deleted file mode 100644 index 0c04aa4524..0000000000 --- a/api/js/etemplate/et2_widget_portlet.js +++ /dev/null @@ -1,347 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Portlet object - used by Home - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package home - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_baseWidget; -*/ -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_action_object_impl } from "./et2_core_DOMWidget"; -import { egw } from "../jsapi/egw_global"; -import { et2_no_init } from "./et2_core_common"; -import { et2_IResizeable } from "./et2_core_interfaces"; -import { et2_dialog } from "./et2_widget_dialog"; -import { egw_getAppObjectManager, egwActionObject } from "../egw_action/egw_action.js"; -/** - * Class which implements the UI of a Portlet - * - * This manages the frame and decoration, but also provides the UI for properties. - * - * Portlets are only internal to EGroupware. - * - * Home does not fully implement WSRP, but tries not to conflict, ether. - * @link http://docs.oasis-open.org/wsrp/v2/wsrp-2.0-spec-os-01.html - * @augments et2_baseWidget - */ -export class et2_portlet extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_portlet - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_portlet._attributes, _child || {})); - this.GRID = 55; - /** - * These are the "normal" actions that every portlet is expected to have. - * The widget provides default actions for all of these, but they can - * be added to or overridden if needed by setting the action attribute. - */ - this.default_actions = { - edit_settings: { - icon: "edit", - caption: "Configure", - "default": true, - hideOnDisabled: true, - group: "portlet" - }, - remove_portlet: { - icon: "delete", - caption: "Remove", - group: "portlet" - } - }; - let self = this; - // Create DOM nodes - this.div = jQuery(document.createElement("div")) - .addClass(this.options.class) - .addClass("ui-widget ui-widget-content ui-corner-all") - .addClass("et2_portlet") - /* Gridster */ - .attr("data-sizex", this.options.width) - .attr("data-sizey", this.options.height) - .attr("data-row", this.options.row) - .attr("data-col", this.options.col) - .resizable({ - autoHide: true, - grid: this.GRID, - //containment: this.getParent().getDOMNode(), - stop: function (event, ui) { - self.set_width(Math.round(ui.size.width / self.GRID)); - self.set_height(Math.round(ui.size.height / self.GRID)); - self.egw().jsonq("home.home_ui.ajax_set_properties", [self.id, {}, { - width: self.options.width, - height: self.options.height - }], null, self); - // Tell children - self.iterateOver(function (widget) { widget.resize(); }, null, et2_IResizeable); - } - }); - this.header = jQuery(document.createElement("div")) - .attr('id', this.getInstanceManager().uniqueId + '_' + this.id.replace(/\./g, '-') + '_header') - .addClass("ui-widget-header ui-corner-all") - .appendTo(this.div) - .html(this.options.title); - this.content = jQuery(document.createElement("div")) - .attr('id', this.getInstanceManager().uniqueId + '_' + this.id.replace(/\./g, '-') + '_content') - .appendTo(this.div); - this.setDOMNode(this.div[0]); - } - destroy() { - for (let i = 0; i < this._children.length; i++) { - // Check for child is a different template and clear it, - // since it won't be cleared by destroy() - if (this._children[i].getInstanceManager() != this.getInstanceManager()) { - this._children[i].getInstanceManager().clear(); - } - } - super.destroy(); - } - doLoadingFinished() { - this.set_color(this.options.color); - return true; - } - /** - * If anyone asks, return the content node, so content goes inside - */ - getDOMNode(_sender) { - if (typeof _sender != 'undefined' && _sender != this) { - return this.content[0]; - } - return super.getDOMNode(_sender); - } - /** - * Overriden from parent to add in default actions - */ - set_actions(actions) { - // Set targets for actions - let defaults = {}; - for (let action_name in this.default_actions) { - defaults[action_name] = this.default_actions[action_name]; - // Translate caption here, as translations aren't available earlier - defaults[action_name].caption = this.egw().lang(this.default_actions[action_name].caption); - if (typeof this[action_name] == "function") { - defaults[action_name].onExecute = jQuery.proxy(this[action_name], this); - } - } - // Add in defaults, but let provided actions override them - this.options.actions = jQuery.extend(true, {}, defaults, actions); - super.set_actions(this.options.actions); - } - /** - * Override _link_actions to remove edit action, if there is no settings - * - * @param actions - */ - _link_actions(actions) { - // Get the top level element - let objectManager = egw_getAppObjectManager(true); - let widget_object = objectManager.getObjectById(this.id); - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = objectManager.insertObject(false, new egwActionObject(this.id, objectManager, new et2_action_object_impl(this).getAOI(), this._actionManager || objectManager.manager.getActionById(this.id) || objectManager.manager)); - } - // Delete all old objects - widget_object.clear(); - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - let action_links = []; - for (let i in actions) { - let id = typeof actions[i].id != 'undefined' ? actions[i].id : i; - let action = { - actionId: id, - enabled: true - }; - // If there are no settings, there can be no customization, so remove the edit action - if (id == 'edit_settings' && (!this.options.settings || jQuery.isEmptyObject(this.options.settings))) { - this.egw().debug("log", "No settings for portlet %o, edit_settings action removed", this); - action.enabled = false; - } - action_links.push(action); - } - widget_object.updateActionLinks(action_links); - } - /** - * Create & show a dialog for customizing this portlet - * - * Properties for customization are sent in the 'settings' attribute - */ - edit_settings() { - let dialog = et2_createWidget("dialog", { - callback: jQuery.proxy(this._process_edit, this), - template: this.options.edit_template, - value: { - content: this.options.settings - }, - buttons: et2_dialog.BUTTONS_OK_CANCEL - }, this); - // Set seperately to avoid translation - dialog.set_title(this.egw().lang("Edit") + " " + (this.options.title || '')); - } - _process_edit(button_id, value) { - if (button_id != et2_dialog.OK_BUTTON) - return; - // Save settings - server might reply with new content if the portlet needs an update, - // but ideally it doesn't - this.div.addClass("loading"); - // Pass updated settings, unless we're removing - let settings = (typeof value == 'string') ? {} : this.options.settings || {}; - this.egw().jsonq("home.home_ui.ajax_set_properties", [this.id, settings, value, this.settings ? this.settings.group : false], function (data) { - // This section not for us - if (!data || typeof data.attributes == 'undefined') - return false; - this.div.removeClass("loading"); - this.set_value(data.content); - for (let key in data.attributes) { - if (typeof this["set_" + key] == "function") { - this["set_" + key].call(this, data.attributes[key]); - } - else if (this.attributes[key]) { - this.options[key] = data.attributes[key]; - } - } - // Flagged as needing to edit settings? Open dialog - if (typeof data.edit_settings != 'undefined' && data.edit_settings) { - this.edit_settings(); - } - // Only resize once, and only if needed - if (data.attributes.width || data.attributes.height) { - // Tell children - try { - this.iterateOver(function (widget) { widget.resize(); }, null, et2_IResizeable); - } - catch (e) { - // Something went wrong, but do not stop - egw.debug('warn', e, this); - } - } - }, this); - // Extend, not replace, because settings has types while value has just value - if (typeof value == 'object') { - jQuery.extend(this.options.settings, value); - } - } - /** - * Remove this portlet from the home page - */ - remove_portlet() { - let self = this; - et2_dialog.show_dialog(function (button_id) { - if (button_id != et2_dialog.OK_BUTTON) - return; - self._process_edit(button_id, '~remove~'); - self.getParent().removeChild(self); - self.destroy(); - }, this.egw().lang("Remove"), this.options.title, {}, et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.QUESTION_MESSAGE); - } - /** - * Set the HTML content of the portlet - * - * @param value String HTML fragment - */ - set_value(value) { - this.content.html(value); - } - /** - * Set the content of the header - * - * @param value String HTML fragment - */ - set_title(value) { - this.header.contents() - .filter(function () { - return this.nodeType === 3; - }) - .remove(); - this.options.title = value; - this.header.append(value); - } - /** - * Let this portlet stand out a little by allowing a custom color - */ - set_color(color) { - this.options.color = color; - this.header.css("backgroundColor", color); - this.header.css('color', jQuery.Color(this.header.css("backgroundColor")).lightness() > 0.5 ? 'black' : 'white'); - this.content.css("backgroundColor", color); - } - /** - * Set the number of grid cells this widget spans - * - * @param value int Number of horizontal grid cells - */ - set_width(value) { - this.options.width = value; - this.div.attr("data-sizex", value); - // Clear what's there from jQuery, we get width from CSS according to sizex - this.div.css('width', ''); - } - /** - * Set the number of vertical grid cells this widget spans - * - * @param value int Number of vertical grid cells - */ - set_height(value) { - this.options.height = value; - this.div.attr("data-sizey", value); - // Clear what's there from jQuery, we get width from CSS according to sizey - this.div.css('height', ''); - } -} -et2_portlet._attributes = { - "title": { - "name": "Title", - "description": "Goes in the little bit at the top with the icons", - "type": "string", - "default": "" - }, - "edit_template": { - "name": "Edit template", - "description": "Custom eTemplate used to customize / set up the portlet", - "type": "string", - "default": egw.webserverUrl + "/home/templates/default/edit.xet" - }, - "color": { - "name": "Color", - "description": "Set the portlet color", - "type": "string", - "default": '' - }, - "settings": { - "name": "Customization settings", - "description": "Array of customization settings, similar in structure to preference settings", - "type": "any", - "default": et2_no_init - }, - "actions": { - default: {} - }, - "width": { "default": 2, "ignore": true }, - "height": { "default": 1, "type": "integer" }, - "rows": { "ignore": true, default: et2_no_init }, - "cols": { "ignore": true, default: et2_no_init }, - "resize_ratio": { "ignore": true, default: et2_no_init }, - "row": { - "name": "Row", - "description": "Home page location (row) - handled by home app", - "default": 1 - }, - "col": { - "name": "Column", - "description": "Home page location(column) - handled by home app", - "default": 1 - } -}; -et2_register_widget(et2_portlet, ["portlet"]); -//# sourceMappingURL=et2_widget_portlet.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_progress.js b/api/js/etemplate/et2_widget_progress.js deleted file mode 100644 index 47b2bc7d2a..0000000000 --- a/api/js/etemplate/et2_widget_progress.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Progrss object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Ralf Becker - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_interfaces; - et2_core_valueWidget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_valueWidget } from "./et2_core_valueWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { egw } from "../jsapi/egw_global"; -/** - * Class which implements the "progress" XET-Tag - * - * @augments et2_valueWidget - */ -export class et2_progress extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_progress - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_progress._attributes, _child || {})); - this.progress = null; - let outer = document.createElement("div"); - outer.className = "et2_progress"; - this.progress = document.createElement("div"); - this.progress.style.width = "0"; - outer.appendChild(this.progress); - if (this.options.href) { - outer.className += ' et2_clickable'; - } - if (this.options["class"]) { - outer.className += ' ' + this.options["class"]; - } - this.setDOMNode(outer); // set's this.node = outer - } - click(e) { - super.click(e); - if (this.options.href) { - this.egw().open_link(this.options.href, this.options.extra_link_target, this.options.extra_link_popup); - } - } - // setting the value as width of the progress-bar - set_value(_value) { - super.set_value(_value); - _value = parseInt(_value) + "%"; // make sure we have percent attached - this.progress.style.width = _value; - if (!this.options.label) - this.set_label(_value); - } - // set's label as title of this.node - set_label(_value) { - this.node.title = _value; - } - // set's class of this.node; preserve baseclasses et2_progress and if this.options.href is set et2_clickable - set_class(_value) { - let baseClass = "et2_progress"; - if (this.options.href) { - baseClass += ' et2_clickable'; - } - this.node.setAttribute('class', baseClass + ' ' + _value); - } - set_href(_value) { - if (!this.isInTree()) { - return false; - } - this.options.href = _value; - if (_value) { - jQuery(this.node).addClass('et2_clickable') - .wrapAll('"'); - let href = this.options.href; - let popup = this.options.extra_link_popup; - let target = this.options.extra_link_target; - jQuery(this.node).parent().click(function (e) { - egw.open_link(href, target, popup); - e.preventDefault(); - return false; - }); - } - else if (jQuery(this.node).parent('a').length) { - jQuery(this.node).removeClass('et2_clickable') - .unwrap(); - } - return true; - } - /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview - * - * * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - _attrs.push("value", "label", "href"); - } - getDetachedNodes() { - return [this.node, this.progress]; - } - setDetachedAttributes(_nodes, _values) { - // Set the given DOM-Nodes - this.node = _nodes[0]; - this.progress = _nodes[1]; - // Set the attributes - if (_values["label"]) { - this.set_label(_values["label"]); - } - if (_values["value"]) { - this.set_value(_values["value"]); - } - else if (_values["label"]) { - this.set_value(_values["label"]); - } - if (_values["href"]) { - jQuery(this.node).addClass('et2_clickable'); - this.set_href(_values["href"]); - } - } -} -et2_progress._attributes = { - "href": { - "name": "Link Target", - "type": "string", - "description": "Link URL, empty if you don't wan't to display a link." - }, - "extra_link_target": { - "name": "Link target", - "type": "string", - "default": "_self", - "description": "Link target descriptor" - }, - "extra_link_popup": { - "name": "Popup", - "type": "string", - "description": "widthxheight, if popup should be used, eg. 640x480" - }, - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed as the title. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - } -}; -et2_progress.legacyOptions = ["href", "extra_link_target", "imagemap", "extra_link_popup", "id"]; -et2_register_widget(et2_progress, ["progress"]); -//# sourceMappingURL=et2_widget_progress.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_radiobox.js b/api/js/etemplate/et2_widget_radiobox.js deleted file mode 100644 index bfe28741f6..0000000000 --- a/api/js/etemplate/et2_widget_radiobox.js +++ /dev/null @@ -1,406 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Radiobox object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2011 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_inputWidget; -*/ -import { et2_inputWidget } from "./et2_core_inputWidget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { et2_valueWidget } from './et2_core_valueWidget'; -/** - * Class which implements the "radiobox" XET-Tag - * - * A radio button belongs to same group by giving all buttons of a group same id! - * - * set_value iterates over all of them and (un)checks them depending on given value. - * - * @augments et2_inputWidget - */ -export class et2_radiobox extends et2_inputWidget { - /** - * Constructor - * - * @memberOf et2_radiobox - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_radiobox._attributes, _child || {})); - this.input = null; - this.id = ""; - this.createInputWidget(); - } - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - let readonly = this.getArrayMgr('readonlys').getEntry(this.id); - if (readonly && readonly.hasOwnProperty(_attrs.set_value)) { - _attrs.readonly = readonly[_attrs.set_value]; - } - } - createInputWidget() { - this.input = jQuery(document.createElement("input")) - .val(this.options.set_value) - .attr("type", "radio") - .attr("disabled", this.options.readonly); - this.input.addClass("et2_radiobox"); - this.setDOMNode(this.input[0]); - } - /** - * Overwritten to set different DOM level ids by appending set_value - * - * @param _id - */ - set_id(_id) { - super.set_id(_id); - this.dom_id = this.dom_id.replace('[]', '') + '-' + this.options.set_value; - if (this.input) - this.input.attr('id', this.dom_id); - } - /** - * Default for radio buttons is label after button - * - * @param _label String New label for radio button. Use %s to locate the radio button somewhere else in the label - */ - set_label(_label) { - if (_label.length > 0 && _label.indexOf('%s') == -1) { - _label = '%s' + _label; - } - super.set_label(_label); - } - /** - * Override default to match against set/unset value AND iterate over all siblings with same id - * - * @param {string} _value - */ - set_value(_value) { - this.getRoot().iterateOver(function (radio) { - if (radio.id == this.id) { - radio.input.prop('checked', _value == radio.options.set_value); - } - }, this, et2_radiobox); - } - /** - * Override default to iterate over all siblings with same id - * - * @return {string} - */ - getValue() { - let val = this.options.value; // initial value, when form is loaded - let values = []; - this.getRoot().iterateOver(function (radio) { - values.push(radio.options.set_value); - if (radio.id == this.id && radio.input && radio.input.prop('checked')) { - val = radio.options.set_value; - } - }, this, et2_radiobox); - return val && val.indexOf(values) ? val : null; - } - /** - * Overridden from parent so if it's required, only 1 in a group needs a value - * - * @param {array} messages - * @returns {Boolean} - */ - isValid(messages) { - let ok = true; - // Check for required - if (this.options && this.options.needed && !this.options.readonly && !this.disabled && - (this.getValue() == null || this.getValue().valueOf() == '')) { - if (jQuery.isEmptyObject(this.getInstanceManager().getValues(this.getInstanceManager().widgetContainer)[this.id.replace('[]', '')])) { - messages.push(this.egw().lang('Field must not be empty !!!')); - ok = false; - } - } - return ok; - } - /** - * Set radio readonly attribute. - * - * @param _readonly Boolean - */ - set_readonly(_readonly) { - this.options.readonly = _readonly; - this.getRoot().iterateOver(function (radio) { - if (radio.id == this.id) { - radio.input.prop('disabled', _readonly); - } - }, this, et2_radiobox); - } -} -et2_radiobox._attributes = { - "set_value": { - "name": "Set value", - "type": "string", - "default": "true", - "description": "Value when selected" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "x", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - } -}; -et2_radiobox.legacyOptions = ["set_value", "ro_true", "ro_false"]; -et2_register_widget(et2_radiobox, ["radio"]); -/** - * @augments et2_valueWidget - */ -export class et2_radiobox_ro extends et2_valueWidget { - /** - * Constructor - * - * @memberOf et2_radiobox_ro - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_radiobox_ro._attributes, _child || {})); - this.value = ""; - this.span = null; - this.span = jQuery(document.createElement("span")) - .addClass("et2_radiobox"); - this.setDOMNode(this.span[0]); - } - /** - * Override default to match against set/unset value - * - * @param {string} _value - */ - set_value(_value) { - this.value = _value; - if (_value == this.options.set_value) { - this.span.text(this.options.ro_true); - } - else { - this.span.text(this.options.ro_false); - } - } - set_label(_label) { - // no label for ro radio, we show label of checked option as content, unless it's x - // then we need the label for things to make sense - if (this.options.ro_true == "x") { - return super.set_label(_label); - } - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs - */ - getDetachedAttributes(_attrs) { - // Show label in nextmatch instead of just x - this.options.ro_true = this.options.label; - _attrs.push("value"); - } - getDetachedNodes() { - return [this.span[0]]; - } - setDetachedAttributes(_nodes, _values) { - this.span = jQuery(_nodes[0]); - this.set_value(_values["value"]); - } -} -et2_radiobox_ro._attributes = { - "set_value": { - "name": "Set value", - "type": "string", - "default": "true", - "description": "Value when selected" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "x", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - }, - "label": { - "name": "Label", - "default": "", - "type": "string" - } -}; -et2_radiobox_ro.legacyOptions = ["set_value", "ro_true", "ro_false"]; -et2_register_widget(et2_radiobox_ro, ["radio_ro"]); -/** - * A group of radio buttons - * - * @augments et2_valueWidget - */ -export class et2_radioGroup extends et2_valueWidget { - /** - * Constructor - * - * @param parent - * @param attrs - * @memberOf et2_radioGroup - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_radioGroup._attributes, _child || {})); - this.node = null; - this.value = null; - this.node = jQuery(document.createElement("div")) - .addClass("et2_vbox") - .addClass("et2_box_widget"); - if (this.options.needed) { - // This isn't strictly allowed, but it works - this.node.attr("required", "required"); - } - this.setDOMNode(this.node[0]); - // The supported widget classes array defines a whitelist for all widget - // classes or interfaces child widgets have to support. - this.supportedWidgetClasses = [et2_radiobox, et2_radiobox_ro]; - } - set_value(_value) { - this.value = _value; - for (let i = 0; i < this._children.length; i++) { - let radio = this._children[i]; - radio.set_value(_value); - } - } - getValue() { - return jQuery("input:checked", this.getDOMNode()).val(); - } - /** - * Set a bunch of radio buttons - * - * @param {object} _options object with value: label pairs - */ - set_options(_options) { - // Call the destructor of all children - for (let i = this._children.length - 1; i >= 0; i--) { - this._children[i].destroy(); - } - this._children = []; - // create radio buttons for each option - for (let key in _options) { - let attrs = { - // Add index so radios work properly - "id": (this.options.readonly ? this.id : this.id + "[" + "]"), - set_value: key, - label: _options[key], - ro_true: this.options.ro_true, - ro_false: this.options.ro_false, - readonly: this.options.readonly - }; - if (typeof _options[key] === 'object' && _options[key].label) { - attrs.set_value = _options[key].value; - attrs.label = _options[key].label; - } - // Can't have a required readonly, it will warn & be removed later, so avoid the warning - if (attrs.readonly === false) { - attrs['needed'] = this.options.needed; - } - et2_createWidget("radio", attrs, this); - } - this.set_value(this.value); - } - /** - * Set a label on the group of radio buttons - * - * @param {string} _value - */ - set_label(_value) { - // Abort if ther was no change in the label - if (_value == this.label) { - return; - } - if (_value) { - // Create the label container if it didn't exist yet - if (this._labelContainer == null) { - this._labelContainer = jQuery(document.createElement("label")); - this.getSurroundings().insertDOMNode(this._labelContainer[0]); - } - // Clear the label container. - this._labelContainer.empty(); - // Create the placeholder element and set it - var ph = document.createElement("span"); - this.getSurroundings().setWidgetPlaceholder(ph); - this._labelContainer - .append(document.createTextNode(_value)) - .append(ph); - } - else { - // Delete the labelContainer from the surroundings object - if (this._labelContainer) { - this.getSurroundings().removeDOMNode(this._labelContainer[0]); - } - this._labelContainer = null; - } - } - /** - * Code for implementing et2_IDetachedDOM - * This doesn't need to be implemented. - * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen - * - * @param {object} _attrs - */ - getDetachedAttributes(_attrs) { - } - getDetachedNodes() { - return [this.getDOMNode()]; - } - setDetachedAttributes(_nodes, _values) { - } -} -et2_radioGroup._attributes = { - "label": { - "name": "Label", - "default": "", - "type": "string", - "description": "The label is displayed above the list of radio buttons. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).", - "translate": true - }, - "value": { - "name": "Value", - "type": "string", - "default": "true", - "description": "Value for each radio button" - }, - "ro_true": { - "name": "Read only selected", - "type": "string", - "default": "x", - "description": "What should be displayed when readonly and selected" - }, - "ro_false": { - "name": "Read only unselected", - "type": "string", - "default": "", - "description": "What should be displayed when readonly and not selected" - }, - "options": { - "name": "Radio options", - "type": "any", - "default": {}, - "description": "Options for radio buttons. Should be {value: label, ...}" - }, - "needed": { - "name": "Required", - "default": false, - "type": "boolean", - "description": "If required, the user must select one of the options before the form can be submitted" - } -}; -// No such tag as 'radiogroup', but it needs something -et2_register_widget(et2_radioGroup, ["radiogroup"]); -//# sourceMappingURL=et2_widget_radiobox.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_script.js b/api/js/etemplate/et2_widget_script.js deleted file mode 100644 index d1db2c93ff..0000000000 --- a/api/js/etemplate/et2_widget_script.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * EGroupware eTemplate2 - JS widget class containing javascript - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Ralf Becker - */ -/*egw:uses - et2_core_widget; -*/ -import { et2_register_widget } from "./et2_core_widget"; -import { et2_widget } from "./et2_core_widget"; -/** - * Function which executes the encapsulated script data. - * - * This should only be used for customization and NOT for regular EGroupware code! - * - * We can NOT create a script tag containing the content, as this violates our CSP policy! - * - * We use new Function(_content) instead. Therefore you have to use window to address global context: - * - * window.some_func = function() {...} - * - * instead of not working - * - * function some_funct() {...} - * - * @augments et2_widget - */ -export class et2_script extends et2_widget { - constructor(_parent, _attrs, _child) { - super(); - // Allow no child widgets - this.supportedWidgetClasses = []; - } - ; - /** - * We can NOT create a script tag containing the content, as this violoates our CSP policy! - * - * @param {string} _content - */ - loadContent(_content) { - try { - var func = new Function(_content); - func.call(window); - } - catch (e) { - this.egw.debug('error', 'Error while executing script: ', _content, e); - } - } -} -et2_register_widget(et2_script, ["script"]); -//# sourceMappingURL=et2_widget_script.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_selectAccount.js b/api/js/etemplate/et2_widget_selectAccount.js deleted file mode 100644 index 30a57018c5..0000000000 --- a/api/js/etemplate/et2_widget_selectAccount.js +++ /dev/null @@ -1,715 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Select account widget - * - * Selecting accounts needs special UI, and displaying needs special consideration - * to avoid sending the entire user list to the client. - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @copyright Nathan Gray 2012 - */ -/*egw:uses - et2_widget_link; -*/ -import { et2_selectbox } from "./et2_widget_selectbox"; -import { et2_createWidget, et2_register_widget } from "./et2_core_widget"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_link_entry, et2_link_string } from "./et2_widget_link"; -import { et2_dialog } from "./et2_widget_dialog"; -import { egw } from "../jsapi/egw_global"; -/** - * Account selection widget - * Changes according to the user's account_selection preference - * - 'none' => Server-side: the read-only widget is used, and no values are sent or displayed - * - 'groupmembers' => Non admins can only select groupmembers (Server side - normal selectbox) - * - 'selectbox' => Selectbox with all accounts and groups (Server side - normal selectbox) - * - 'primary_group' => Selectbox with primary group and search - * - * Only primary_group and popup need anything different from a normal selectbox - * - */ -export class et2_selectAccount extends et2_selectbox { - /** - * Constructor - * - */ - constructor(_parent, _attrs, _child) { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_selectAccount._attributes, _child || {})); - // Type in rows or somewhere else? - if (et2_selectAccount.account_types.indexOf(this.options.empty_label) >= 0 && (et2_selectAccount.account_types.indexOf(this.options.account_type) < 0 || - this.options.account_type == et2_selectAccount._attributes.account_type.default)) { - this.options.account_type = _attrs['empty_label']; - this.options.empty_label = ''; - } - if (jQuery.inArray(_attrs['account_type'], et2_selectAccount.account_types) < 0) { - this.egw().debug("warn", "Invalid account_type: %s Valid options:", _attrs['account_type'], et2_selectAccount.account_types); - } - // Holder for search jQuery nodes - this.search = null; - // Reference to dialog - this.dialog = null; - // Reference to widget within dialog - this.widgets = null; - if (!this.options.empty_label && !this.options.readonly && this.options.multiple) { - this.options.empty_label = this.egw().lang('Select user or group'); - } - // Allow certain widgets inside this one - this.supportedWidgetClasses = [et2_link_entry]; - } - destroy() { - super.destroy.apply(this, arguments); - } - /** - * Single selection - override to add search button - */ - createInputWidget() { - var type = this.egw().preference('account_selection', 'common'); - switch (type) { - case 'none': - if (typeof egw.user('apps').admin == 'undefined') { - this.options.select_options = {}; - break; - } - case 'selectbox': - case 'groupmembers': - default: - this.options.select_options = this._get_accounts(); - break; - } - super.createInputWidget(); - // Add search button - if (type == 'primary_group') { - var button = jQuery(document.createElement("span")) - .addClass("et2_clickable") - .click(this, jQuery.proxy(function (e) { - // Auto-expand - if (this.options.expand_multiple_rows && !this.options.multiple) { - this.set_multiple(true, this.options.expand_multiple_rows); - } - if (this.options.multiple) { - this._open_multi_search(e); - } - else { - this._open_search(e); - } - }, this)) - .attr("title", egw.lang("popup with search")) - .append(''); - this.getSurroundings().insertDOMNode(button[0]); - } - } - /** - * Multiple selection - override to add search button - */ - createMultiSelect() { - var type = this.egw().preference('account_selection', 'common'); - if (type == 'none' && typeof egw.user('apps').admin == 'undefined') - return; - super.createMultiSelect(); - this.options.select_options = this._get_accounts(); - if (type == 'primary_group') { - // Allow search 'inside' this widget - this.supportedWidgetClasses = [et2_link_entry]; - // Add quick search - turn off multiple to get normal result list - this.options.multiple = false; - this._create_search(); - // Clear search box after select - var old_select = this.search_widget.select; - var self = this; - // @ts-ignore - this.search_widget.select = function (e, selected) { - var current = self.getValue(); - // Fix ID as sent from server - must be numeric - selected.item.value = parseInt(selected.item.value); - // This one is important, it makes sure the option is there - old_select.apply(this, arguments); - // Add quick search selection into current selection - current.push(selected.item.value); - // Clear search - this.search.val(''); - self.set_value(current); - }; - // Put search results as a DOM sibling of the options, for proper display - this.search_widget.search.on("autocompleteopen", jQuery.proxy(function () { - this.search_widget.search.data("ui-autocomplete").menu.element - .appendTo(this.node) - .position({ my: 'left top', at: 'left bottom', of: this.multiOptions.prev() }); - }, this)); - this.search = jQuery(document.createElement("li")) - .appendTo(this.multiOptions.prev().find('ul')); - this.options.multiple = true; - // Add search button - var button = jQuery(document.createElement("li")) - .addClass("et2_clickable") - .click(this, this._open_multi_search) - .attr("title", egw.lang("popup with search")) - .append(''); - var type = this.egw().preference('account_selection', 'common'); - // Put it last so check/uncheck doesn't move around - this.multiOptions.prev().find('ul') - .append(button); - } - } - /** - * Override parent to make sure accounts are there as options. - * - * Depending on the widget's attributes and the user's preferences, not all selected - * accounts may be in the cache as options, so we fetch the extras to make sure - * we don't lose any. - * - * As fetching them might only work asynchron (if they are not yet loaded), - * we have to call set_value again, once all labels have arrived from server. - * - * @param {string|array} _value - */ - set_value(_value) { - if (typeof _value == "string" && this.options.multiple && _value.match(this._is_multiple_regexp) !== null) { - _value = _value.split(','); - } - if (_value) { - var search = _value; - if (!jQuery.isArray(search)) { - search = [_value]; - } - var update_options = false; - var num_calls = 0; - var current_call = 0; - for (var j = 0; j < search.length; j++) { - var found = false; - // Not having a value to look up causes an infinite loop - if (!search[j] || search[j] === "0") - continue; - // Options are not indexed, so we must look - for (var i = 0; !found && i < this.options.select_options.length; i++) { - if (typeof this.options.select_options[i] != 'object') { - egw.debug('warn', this.id + ' wrong option ' + i + ' this.options.select_options=', this.options.select_options); - continue; - } - if (this.options.select_options[i].value == search[j]) - found = true; - } - // We only look for numeric IDs, non-numeric IDs cause an exception - if (!found && !isNaN(search[j])) { - // Add it in - var name = this.egw().link_title('api-accounts', search[j]); - if (name) // was already cached on client-side - { - update_options = true; - this.options.select_options.push({ value: search[j], label: name }); - } - else // not available: need to call set_value again, after all arrived from server - { - ++num_calls; - // Add immediately with value as label, we'll replace later - this._appendOptionElement(search[j], search[j]); - this.egw().link_title('api-accounts', search[j], function (name) { - if (++current_call >= num_calls) // only run last callback - { - // Update the label - // Options are not indexed, so we must look - for (var i = 0; i < this.widget.options.select_options.length; i++) { - var opt = this.widget.options.select_options[i]; - if (opt && opt.value && opt.value == this.unknown && opt.label == this.unknown) { - opt.label = name; - this.widget.set_select_options(this.widget.options.select_options); - break; - } - } - this.widget.set_value(_value); - } - }, { widget: this, unknown: search[j] }); - } - } - } - if (update_options) { - this.set_select_options(this.options.select_options); - } - } - super.set_value(_value); - } - /** - * Get account info for select options from common client-side account cache - * - * @return {Array} select options - */ - _get_accounts() { - if (!jQuery.isArray(this.options.select_options)) { - var options = jQuery.extend({}, this.options.select_options); - this.options.select_options = []; - for (var key in options) { - if (typeof options[key] == 'object') { - if (typeof (options[key].key) == 'undefined') { - options[key].value = key; - } - this.options.select_options.push(options[key]); - } - else { - this.options.select_options.push({ value: key, label: options[key] }); - } - } - } - var type = this.egw().preference('account_selection', 'common'); - var accounts = []; - // for primary_group we only display owngroups == own memberships, not other groups - if (type == 'primary_group' && this.options.account_type != 'accounts') { - if (this.options.account_type == 'both') { - accounts = this.egw().accounts('accounts'); - } - accounts = accounts.concat(this.egw().accounts('owngroups')); - } - else { - accounts = this.egw().accounts(this.options.account_type); - } - return this.options.select_options.concat(accounts); - } - /** - * Create & display a way to search & select a single account / group - * Single selection is just link widget - * - * @param e event - */ - _open_search(e) { - var widget = e.data; - var search = widget._create_search(); - // Selecting a single user closes the dialog, this only used if user cleared - var ok_click = function () { - widget.set_value([]); - // Fire change event - if (widget.input) - widget.input.trigger("change"); - jQuery(this).dialog("close"); - }; - widget._create_dialog(search, ok_click); - } - /** - * Create & display a way to search & select multiple accounts / groups - * - * @param e event - */ - _open_multi_search(e) { - var widget = e && e.data ? e.data : this; - var table = widget.search = jQuery('
      '); - table.css("width", "100%").css("height", "100%"); - var search_col = jQuery('#search_col', table); - var select_col = jQuery('#selection_col', table); - // Search / Selection - search_col.append(widget._create_search()); - // Currently selected - select_col.append(widget._create_selected()); - var ok_click = function () { - jQuery(this).dialog("close"); - // Update widget with selected - var ids = []; - var data = {}; - jQuery('#' + widget.getInstanceManager().uniqueId + '_selected li', select_col).each(function () { - var id = jQuery(this).attr("data-id"); - // Add to list - ids.push(id); - // Make sure option is there - if (widget.options.multiple && jQuery('input[id$="_opt_' + id + '"]', widget.multiOptions).length == 0) { - widget._appendMultiOption(id, jQuery('label', this).text()); - } - else if (!widget.options.multiple && jQuery('option[value="' + id + '"]', widget.node).length == 0) { - widget._appendOptionElement(id, jQuery('label', this).text()); - } - }); - widget.set_value(ids); - // Fire change event - if (widget.input) - widget.input.trigger("change"); - }; - var container = jQuery(document.createElement("div")).append(table); - return widget._create_dialog(container, ok_click); - } - /** - * Create / display popup with search / selection widgets - * - * @param {et2_dialog} widgets - * @param {function} update_function - */ - _create_dialog(widgets, update_function) { - this.widgets = widgets; - this.dialog = et2_dialog.show_dialog(undefined, '', this.options.label ? this.options.label : this.egw().lang('Select'), {}, [{ - text: this.egw().lang("ok"), - image: 'check', - click: update_function - }, { - text: this.egw().lang("cancel"), - image: 'cancel' - }]); - this.dialog.set_dialog_type(''); - // Static size for easier layout - this.dialog.div.dialog({ width: "500", height: "370" }); - this.dialog.div.append(widgets.width('100%')); - return widgets; - } - /** - * Search is a link-entry widget, with some special display for multi-select - */ - _create_search() { - var self = this; - var search = this.search = jQuery(document.createElement("div")); - var search_widget = this.search_widget = et2_createWidget('link-entry', { - 'only_app': 'api-accounts', - 'query'(request, response) { - // Clear previous search results for multi-select - if (!request.options) { - search.find('#search_results').empty(); - } - // Restrict to specified account type - if (!request.options || !request.options.filter) { - request.options = { account_type: self.options.account_type }; - } - return true; - }, - 'select'(e, selected) { - // Make sure option is there - var already_there = false; - var last_key = null; - for (last_key in self.options.select_options) { - var option = self.options.select_options[last_key]; - already_there = already_there || (typeof option.value != 'undefined' && option.value == selected.item.value); - } - if (!already_there) { - self.options.select_options[parseInt(last_key) + 1] = selected.item; - self._appendOptionElement(selected.item.value, selected.item.label); - } - self.set_value(selected.item.value); - if (self.dialog && self.dialog.div) { - self.dialog.div.dialog("close"); - } - // Fire change event - if (self.input) - self.input.trigger("change"); - return true; - } - }, this); - // add it where we want it - search.append(search_widget.getDOMNode()); - if (!this.options.multiple) - return search; - // Multiple is more complicated. It uses a custom display for results to - // allow choosing multiples from a match - var results = jQuery(document.createElement("ul")) - .attr("id", "search_results") - .css("height", "230px") - .addClass("ui-multiselect-checkboxes ui-helper-reset"); - jQuery(document.createElement("div")) - .addClass("et2_selectbox") - .css("height", "100%") - .append(results) - .appendTo(search); - // Override link-entry auto-complete for custom display - // Don't show normal drop-down - search_widget.search.data("ui-autocomplete")._suggest = function (items) { - jQuery.each(items, function (index, item) { - // Make sure value is numeric - item.value = parseInt(item.value); - self._add_search_result(results, item); - }); - }; - return search; - } - /** - * Add the selected result to the list of search results - * - * @param list - * @param item - */ - _add_search_result(list, item) { - var node = null; - var self = this; - // Make sure value is numeric - if (item.value) - item.value = parseInt(item.value); - // (containter of) Currently selected users / groups - var selected = jQuery('#' + this.getInstanceManager().uniqueId + "_selected", this.widgets); - // Group - if (item.value && item.value < 0) { - node = jQuery(document.createElement('ul')); - // Add button to show users - if (this.options.account_type != 'groups') { - jQuery('') - .css("float", "left") - .appendTo(node) - .click(function () { - if (jQuery(this).hasClass("ui-icon-circlesmall-plus")) { - jQuery(this).removeClass("ui-icon-circlesmall-plus") - .addClass("ui-icon-circlesmall-minus"); - var group = jQuery(this).parent() - .addClass("expanded"); - if (group.children("li").length == 0) { - // Fetch group members - self.search_widget.query({ - term: "", - options: { filter: { group: item.value } }, - no_cache: true - }, function (items) { - jQuery(items).each(function (index, item) { - self._add_search_result(node, item); - }); - }); - } - else { - group.children("li") - // Only show children that are not selected - .each(function (index, item) { - var j = jQuery(item); - if (jQuery('[data-id="' + j.attr("data-id") + '"]', selected).length == 0) { - j.show(); - } - }); - } - } - else { - jQuery(this).addClass("ui-icon-circlesmall-plus") - .removeClass("ui-icon-circlesmall-minus"); - var group = jQuery(this).parent().children("li").hide(); - } - }); - } - } - // User - else if (item.value) { - node = jQuery(document.createElement('li')); - } - node.attr("data-id", item.value); - jQuery('') - .css("float", "right") - .appendTo(node) - .click(function () { - var button = jQuery(this); - self._add_selected(selected, button.parent().attr("data-id")); - // Hide user, but only hide button for group - if (button.parent().is('li')) { - button.parent().hide(); - } - else { - button.hide(); - } - }); - // If already in list, hide it - if (jQuery('[data-id="' + item.value + '"]', selected).length != 0) { - node.hide(); - } - var label = jQuery(document.createElement('label')) - .addClass("loading") - .appendTo(node); - this.egw().link_title('api-accounts', item.value, function (name) { - label.text(name).removeClass("loading"); - }, label); - node.appendTo(list); - } - _create_selected() { - var node = jQuery(document.createElement("div")) - .addClass("et2_selectbox"); - var header = jQuery(document.createElement("div")) - .addClass("ui-widget-header ui-helper-clearfix") - .appendTo(node); - var selected = jQuery(document.createElement("ul")) - .addClass("ui-multiselect-checkboxes ui-helper-reset") - .attr("id", this.getInstanceManager().uniqueId + "_selected") - .css("height", "230px") - .appendTo(node); - jQuery(document.createElement("span")) - .text(this.egw().lang("Selection")) - .addClass("ui-multiselect-header") - .appendTo(header); - var controls = jQuery(document.createElement("ul")) - .addClass('ui-helper-reset') - .appendTo(header); - jQuery(document.createElement("li")) - .addClass("et2_clickable") - .click(selected, function (e) { jQuery("li", e.data).remove(); }) - .append('') - .appendTo(controls); - // Add in currently selected - if (this.getValue()) { - var value = this.getValue(); - for (var i = 0; i < value.length; i++) { - this._add_selected(selected, value[i]); - } - } - return node; - } - /** - * Add an option to the list of selected accounts - * value is the account / group ID - * - * @param list - * @param value - */ - _add_selected(list, value) { - // Each option only once - var there = jQuery('[data-id="' + value + '"]', list); - if (there.length) { - there.show(); - return; - } - var option = jQuery(document.createElement('li')) - .attr("data-id", value) - .appendTo(list); - jQuery('
      ') - .css("float", "right") - .appendTo(option) - .click(function () { - var id = jQuery(this).parent().attr("data-id"); - jQuery(this).parent().remove(); - // Add 'add' button back, if in results list - list.parents("tr").find("[data-id='" + id + "']").show() - // Show button(s) for group - .children('span').show(); - }); - var label = jQuery(document.createElement('label')) - .addClass("loading") - .appendTo(option); - this.egw().link_title('api-accounts', value, function (name) { this.text(name).removeClass("loading"); }, label); - } - /** - * Overwritten attachToDOM method to modify attachToDOM - */ - attachToDOM() { - let result = super.attachToDOM(); - //Chosen needs to be set after widget dettached from DOM (eg. validation_error), because chosen is not part of the widget node - if (this.egw().preference('account_selection', 'common') == 'primary_group') { - jQuery(this.node).removeClass('chzn-done'); - this.set_tags(this.options.tags, this.options.width); - } - return result; - } -} -et2_selectAccount._attributes = { - 'account_type': { - 'name': 'Account type', - 'default': 'accounts', - 'type': 'string', - 'description': 'Limit type of accounts. One of {accounts,groups,both,owngroups}.' - } -}; -et2_selectAccount.legacyOptions = ['empty_label', 'account_type']; -et2_selectAccount.account_types = ['accounts', 'groups', 'both', 'owngroups']; -et2_register_widget(et2_selectAccount, ["select-account"]); -/** - * et2_selectAccount_ro is the readonly implementation of select account - * It extends et2_link to avoid needing the whole user list on the client. - * Instead, it just asks for the names of the ones needed, as needed. - * - * @augments et2_link_string - */ -export class et2_selectAccount_ro extends et2_link_string { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - /** - Resolve some circular dependency problems here - selectAccount extends link, link is in a file that needs select, - select has menulist wrapper, which needs to know about selectAccount before it allows it - */ - if (_parent.supportedWidgetClasses.indexOf(et2_selectAccount_ro) < 0) { - _parent.supportedWidgetClasses.push(et2_selectAccount_ro); - } - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_selectAccount_ro._attributes, _child || {})); - if (_parent.supportedWidgetClasses.indexOf(et2_selectAccount_ro) > 0) { - _parent.addChild(this); - } - // Legacy options could have row count or empty label in first slot - if (typeof this.options.empty_label == "string") { - if (isNaN(this.options.empty_label)) { - this.options.empty_label = this.egw().lang(this.options.empty_label); - } - } - this.options.application = 'api-accounts'; - // Editable version allows app to set options that aren't accounts, so allow for them - let options = et2_selectbox.find_select_options(this, _attrs['select_options'], this.options); - if (!jQuery.isEmptyObject(options)) { - this.options.select_options = options; - } - // Don't make it look like a link though - this.list.removeClass("et2_link_string").addClass("et2_selectbox"); - } - transformAttributes(_attrs) { - et2_selectbox.prototype.transformAttributes.apply(this, arguments); - } - set_value(_value) { - // Explode csv - if (typeof _value == 'string' && _value.indexOf(',') > 0) { - _value = _value.split(','); - } - // pass objects to link widget right away, as following code can't deal with objects - if (typeof _value == 'object') { - super.set_value(_value); - // Don't make it look like a link though - jQuery('li', this.list).removeClass("et2_link et2_link_string") - // No clicks either - .off(); - return; - } - // Empty it before we fill it - jQuery('li', this.list).remove(); - let found = false; - if (this.options.select_options && !jQuery.isEmptyObject(this.options.select_options) || this.options.empty_label) { - if (!_value) { - // Empty label from selectbox - this.list.append("
    • " + this.options.empty_label + "
    • "); - found = true; - } - else if (typeof _value == 'object') { - // An array with 0 / empty in it? - for (let i = 0; i < _value.length; i++) { - if (!_value[i] || !parseInt(_value[i])) { - this.list.append("
    • " + this.options.empty_label + "
    • "); - return; - } - else if (this.options.select_options[_value]) { - this.list.append("
    • " + this.options.select_options[_value] + "
    • "); - found = true; - } - } - } - else { - // Options are not indexed, so we must look - var search = _value; - if (!jQuery.isArray(search)) { - search = [_value]; - } - for (let j = 0; j < search.length; j++) { - // Not having a value to look up causes an infinite loop - if (!search[j]) - continue; - for (let i in this.options.select_options) { - if (this.options.select_options[i].value == search[j]) { - this.list.append("
    • " + this.options.select_options[i].label + "
    • "); - found = true; - break; - } - } - } - } - } - // if nothing found in select-options let link widget try - if (!found && !isNaN(_value)) { - super.set_value(_value); - // Don't make it look like a link though - jQuery('li', this.list).removeClass("et2_link et2_link_string") - // No clicks either - .off(); - return; - } - } -} -et2_selectAccount_ro._attributes = { - "empty_label": { - "name": "Empty label", - "type": "string", - "default": "", - "description": "Textual label for first row, eg: 'All' or 'None'. ID will be ''", - translate: true - } -}; -et2_selectAccount_ro.legacyOptions = ["empty_label"]; -et2_register_widget(et2_selectAccount_ro, ["select-account_ro"]); -//# sourceMappingURL=et2_widget_selectAccount.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_selectbox.js b/api/js/etemplate/et2_widget_selectbox.js deleted file mode 100644 index 041ff314c7..0000000000 --- a/api/js/etemplate/et2_widget_selectbox.js +++ /dev/null @@ -1,1518 +0,0 @@ -/** - * EGroupware eTemplate2 - JS Selectbox object - * - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package etemplate - * @subpackage api - * @link https://www.egroupware.org - * @author Nathan Gray - * @author Andreas Stöckel - * @copyright Nathan Gray 2011 - */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /api/js/jquery/chosen/chosen.jquery.js; - et2_core_xml; - et2_core_DOMWidget; - et2_core_inputWidget; -*/ -import "../../../vendor/bower-asset/jquery/dist/jquery.min.js"; -import "../jquery/chosen/chosen.jquery.js"; -import { et2_no_init } from "./et2_core_common"; -import { ClassWithAttributes } from "./et2_core_inheritance"; -import { et2_register_widget } from "./et2_core_widget"; -import { et2_inputWidget } from './et2_core_inputWidget'; -import { et2_DOMWidget } from "./et2_core_DOMWidget"; -import { et2_directChildrenByTagName, et2_readAttrWithDefault } from "./et2_core_xml"; -import { egw } from "../jsapi/egw_global"; -import { sprintf } from "../egw_action/egw_action_common.js"; -// all calls to Chosen jQuery plugin as jQuery.(un)chosen() give errors which are currently suppressed with @ts-ignore -// adding npm package @types/chosen-js did NOT help :( -/** - * et2 select(box) widget - */ -export class et2_selectbox extends et2_inputWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_selectbox._attributes, _child || {})); - this.input = null; - this.value = ''; - this.selected_first = true; - /** - * Regular expression, to check string-value contains multiple comma-separated values - */ - this._is_multiple_regexp = /^[,0-9A-Za-z/_ -]+$/; - /** - * Regular expression and replace value for escaping values in jQuery selectors used to find options - */ - this._escape_value_replace = /\\/g; - this._escape_value_with = '\\\\'; - this.input = null; - // Start at '' to avoid infinite loops while setting value/select options - this.value = ''; - // Allow no other widgets inside this one - this.supportedWidgetClasses = []; - // Legacy options could have row count or empty label in first slot - if (typeof this.options.rows == "string") { - if (isNaN(this.options.rows)) { - this.options.empty_label = this.egw().lang(this.options.rows); - this.options.rows = 1; - } - else { - this.options.rows = parseInt(this.options.rows); - } - } - if (this.options.rows > 1) { - this.options.multiple = true; - if (this.options.tags) { - this.createInputWidget(); - } - else { - this.createMultiSelect(); - } - } - else { - this.createInputWidget(); - } - if (!this.options.empty_label && !this.options.readonly && this.options.multiple) { - this.options.empty_label = this.egw().lang('Select some options'); - } - } - destroy() { - if (this.input != null) { - // @ts-ignore - this.input.unchosen(); - } - if (this.expand_button) { - this.expand_button.off(); - this.expand_button.remove(); - this.expand_button = null; - } - super.destroy(); - this.input = null; - } - transformAttributes(_attrs) { - super.transformAttributes(_attrs); - // If select_options are already known, skip the rest - if (this.options && this.options.select_options && !jQuery.isEmptyObject(this.options.select_options) || - _attrs.select_options && !jQuery.isEmptyObject(_attrs.select_options) || - // Allow children to skip select_options - check to make sure default got set to something (should be {}) - typeof _attrs.select_options == 'undefined' || _attrs.select_options === null) { - // do not return inside nextmatch, as get_rows data might have changed select_options - // for performance reasons we only do it for first row, which should have id "0[...]" - if (this.getParent() && this.getParent().getType() != 'rowWidget' || !_attrs.id || _attrs.id[0] != '0') - return; - } - var sel_options = et2_selectbox.find_select_options(this, _attrs['select_options'], _attrs); - if (!jQuery.isEmptyObject(sel_options)) { - _attrs['select_options'] = sel_options; - } - } - /** - * Switch instanciated widget to multi-selection and back, optionally enabeling tags too - * - * If you want to switch tags on too, you need to do so after switching to multiple! - * - * @param {boolean} _multiple - * @param {integer} _size default=3 - */ - set_multiple(_multiple, _size) { - this.options.multiple = _multiple; - if (this.input) { - if (_multiple) { - this.input.attr('size', _size || 3); - this.input.prop('multiple', true); - this.input.attr('name', this.id + '[]'); - if (this.input[0].options.length && this.input[0].options[0].value === '') { - this.input[0].options[0] = null; - } - } - else { - this.input.prop('multiple', false); - this.input.removeAttr('size'); - this.input.attr('name', this.id); - if (this.options.empty_label && this.input[0].options[0].value !== '') { - this._appendOptionElement('', this.options.empty_label); - } - } - if (this.expand_button) { - if (_multiple) { - this.expand_button.addClass('ui-icon-minus').removeClass('ui-icon-plus'); - } - else { - this.expand_button.removeClass('ui-icon-minus').addClass('ui-icon-plus'); - } - } - } - } - change(_node, _widget, _value) { - var valid = super.change.apply(this, arguments); - if (!this.input) - return valid; - var selected = this.input.siblings().find('a.chzn-single'); - var val = _value && _value.selected ? _value.selected : this.input.val(); - switch (this.getType()) { - case 'select-country': - if (selected && selected.length == 1 && val) { - selected.removeClass(function (index, className) { - return (className.match(/(^|\s)flag-\S+/g) || []).join(' '); - }); - selected.find('span.img').remove(); - selected.prepend(''); - selected.addClass('et2_country-select flag-' + val.toLowerCase()); - } - else if (selected) { - selected.removeClass('et2_country-select'); - } - break; - } - return valid; - } - /** - * Overridden from parent to make sure tooltip handler is bound to the correct element - * if tags is on. - */ - getTooltipElement() { - if (this.input && (this.options.tags || this.options.search)) { - return jQuery(this.input.siblings()).get(0); - } - return this.getDOMNode(this); - } - /** - * Add an option to regular drop-down select - * - * @param {string} _value value attribute of option - * @param {string} _label label of option - * @param {string} _title title attribute of option - * @param {node} dom_element parent of new option - * @param {string} _class specify classes of option - */ - _appendOptionElement(_value, _label, _title, dom_element, _class) { - if (_value == "" && (_label == null || _label == "")) { - return; // empty_label is added in set_select_options anyway, ignoring it here to not add it twice - } - if (this.input == null) { - return this._appendMultiOption(_value, _label, _title, dom_element); - } - var option = jQuery(document.createElement("option")) - .attr("value", _value) - .text(_label + ""); - option.addClass(_class); - if (this.options.tags) { - switch (this.getType()) { - case 'select-cat': - option.addClass('cat_' + _value); - break; - case 'select-country': - // jQuery(document.createElement("span")).addClass('et2_country-select').appenTo(option); - option.addClass('et2_country-select flag-' + _value.toLowerCase()); - break; - } - if (this.options.value_class != '') - option.addClass(this.options.value_class + _value); - } - if (typeof _title != "undefined" && _title) { - option.attr("title", _title); - } - if (_label == this.options.empty_label || this.options.empty_label == "" && _value === "") { - // Make sure empty / all option is first - option.prependTo(this.input); - } - else { - option.appendTo(dom_element || this.input); - } - } - /** - * Append a value to multi-select - * - * @param {string} _value value attribute of option - * @param {string} _label label of option - * @param {string} _title title attribute of option - * @param {node} dom_element parent of new option - */ - _appendMultiOption(_value, _label, _title, dom_element) { - var option_data = null; - if (typeof _label == "object") { - option_data = _label; - _label = option_data.label; - } - // Already in header - if (_label == this.options.empty_label) - return; - var opt_id = this.dom_id + "_opt_" + _value; - var label = jQuery(document.createElement("label")) - .attr("for", opt_id) - .hover(function () { jQuery(this).addClass("ui-state-hover"); }, function () { jQuery(this).removeClass("ui-state-hover"); }); - var option = jQuery(document.createElement("input")) - .attr("type", "checkbox") - .attr("id", opt_id) - .attr("value", _value) - .appendTo(label); - if (typeof _title !== "undefined") { - option.attr("title", _title); - } - // Some special stuff for categories - if (option_data) { - if (option_data.icon) { - var img = this.egw().image(option_data.icon); - jQuery(document.createElement(img ? "img" : "div")) - .attr("src", img) - .addClass('cat_icon cat_' + _value) - .appendTo(label); - } - if (option_data.color) { - label.css("background-color", option_data.color) - .addClass('cat_' + _value); - } - } - //added tooltip to multiselect - if (typeof _title == "undefined") { - _title = _label; - } - label.append(jQuery("" + _label + "")); - var li = jQuery(document.createElement("li")).append(label); - if (this.options.value_class != '') - li.addClass(this.options.value_class + _value); - li.appendTo(dom_element || this.multiOptions); - } - /** - * Create a regular drop-down select box - */ - createInputWidget() { - // Create the base input widget - this.input = jQuery(document.createElement("select")) - .addClass("et2_selectbox") - .attr("size", this.options.rows); - this.setDOMNode(this.input[0]); - // Add the empty label - if (this.options.empty_label) { - this._appendOptionElement("", this.options.empty_label); - } - // Set multiple - if (this.options.multiple) { - this.input.attr("multiple", "multiple"); - } - } - /** - * Create a list of checkboxes - */ - createMultiSelect() { - var node = jQuery(document.createElement("div")) - .addClass("et2_selectbox"); - var header = jQuery(document.createElement("div")) - .addClass("ui-widget-header ui-helper-clearfix") - .appendTo(node); - var controls = jQuery(document.createElement("ul")) - .addClass('ui-helper-reset') - .appendTo(header); - jQuery(document.createElement("span")) - .text(this.options.empty_label) - .addClass("ui-multiselect-header") - .appendTo(header); - // Set up for options to be added later - var options = this.multiOptions = jQuery(document.createElement("ul")); - this.multiOptions.addClass("ui-multiselect-checkboxes ui-helper-reset") - .css("height", 1.9 * this.options.rows + "em") - .appendTo(node); - if (this.options.rows >= 5) { - // Check / uncheck all - var header_controls = { - check: { - icon_class: 'ui-icon-check', - label: this.egw().lang('Check all'), - click(e) { - var all_off = false; - jQuery("input[type='checkbox']", e.data).each(function () { - if (!jQuery(this).prop("checked")) - all_off = true; - }); - jQuery("input[type='checkbox']", e.data).prop("checked", all_off); - } - } - }; - for (var key in header_controls) { - jQuery(document.createElement("li")) - .addClass("et2_clickable") - .click(options, header_controls[key].click) - .attr("title", header_controls[key].label) - .append('') - .appendTo(controls); - } - } - this.setDOMNode(node[0]); - } - doLoadingFinished() { - super.doLoadingFinished(); - this.set_tags(this.options.tags, this.options.width); - // Reset dirty again here. super.doLoadingFinished() does it too, but set_tags() & others - // change things. Moving set_tags() before super.doLoadingFinished() breaks tag widgets - this.resetDirty(); - return true; - } - loadFromXML(_node) { - // Handle special case where legacy option for empty label is used (conflicts with rows), and rows is set as an attribute - var legacy = _node.getAttribute("options"); - if (legacy) { - var legacy = legacy.split(","); - if (legacy.length && isNaN(legacy[0])) { - this.options.empty_label = legacy[0]; - } - } - // Read the option-tags - var options = et2_directChildrenByTagName(_node, "option"); - if (options.length) { - // Break reference to content manager, we don't want to add to it - this.options.select_options = jQuery.extend([], this.options.select_options); - } - var egw = this.egw(); - for (var i = 0; i < options.length; i++) { - this.options.select_options.push({ - value: et2_readAttrWithDefault(options[i], "value", options[i].textContent), - // allow options to contain multiple translated sub-strings eg: {Firstname}.{Lastname} - "label": options[i].textContent.replace(/{([^}]+)}/g, function (str, p1) { - return egw.lang(p1); - }), - "title": et2_readAttrWithDefault(options[i], "title", "") - }); - } - this.set_select_options(this.options.select_options); - } - /** - * Find an option by it's value - * - * Taking care of escaping values correctly eg. EGroupware\Api\Mail\Smtp using above regular expression - * - * @param {string} _value - * @return {array} - */ - find_option(_value) { - return jQuery("option[value='" + (typeof _value === 'string' ? _value.replace(this._escape_value_replace, this._escape_value_with) : _value) + "']", this.input); - } - /** - * Set value - * - * @param {string|number} _value - * @param {boolean} _dont_try_set_options true: if _value is not in options, use "" instead of calling set_select_options - * (which would go into an infinit loop) - */ - // @ts-ignore for 2nd parameter - set_value(_value, _dont_try_set_options) { - if (typeof _value == "number") - _value = "" + _value; // convert to string for consitent matching - if (typeof _value == "string" && (this.options.multiple || this.options.expand_multiple_rows) && _value.match(this._is_multiple_regexp) !== null) { - _value = _value.split(','); - } - if (this.input !== null && this.options.select_options && (!jQuery.isEmptyObject(this.options.select_options) || this.options.select_options.length > 0) && this.input.children().length == 0) { - // No options set yet - this.set_select_options(this.options.select_options); - } - // select-cat set/unset right cat_ color for selected value - if ((this.getType() == 'select-cat' || this.options.value_class) && this.options.tags) { - var chosen = this.input.next(); - var prefix_c = this.options.value_class ? this.options.value_class : 'cat_'; - this.input.removeClass(prefix_c + this._oldValue); - this.input.addClass(prefix_c + this.value); - if (chosen.length > 0) { - chosen.removeClass(prefix_c + this._oldValue); - chosen.addClass(prefix_c + this.value); - } - } - if (this.getType() == 'select-country' && this.options.tags) { - var selected = this.input.siblings().find('a.chzn-single'); - if (selected && selected.length == 1 && _value) { - selected.removeClass(function (index, className) { - return (className.match(/(^|\s)flag-\S+/g) || []).join(' '); - }); - selected.find('span.img').remove(); - selected.prepend(''); - selected.addClass('et2_country-select flag-' + _value.toLowerCase()); - } - } - if (this.getType() == "select-bitwise" && _value && !isNaN(_value) && this.options.select_options) { - var new_value = []; - for (var index in this.options.select_options) { - var right = this.options.select_options[index].value; - if (!!(_value & right)) { - new_value.push(right); - } - } - _value = new_value; - } - this._oldValue = this.value; - if (this.input !== null && (this.options.tags || this.options.search)) { - // Value must be a real Array, not an object - this.input.val(typeof _value == 'object' && _value != null ? jQuery.map(_value, function (value, index) { return [value]; }) : _value); - this.input.trigger("liszt:updated"); - var self = this; - if (this.getType() == 'listbox' && this.options.value_class != '') { - var chosen = this.input.next(); - chosen.find('.search-choice-close').each(function (i, v) { - // @ts-ignore - jQuery(v).parent().addClass(self.options.value_class + self.options.select_options[v.rel]['value']); - }); - } - this.value = _value; - return; - } - if (this.input == null) { - return this.set_multi_value(_value); - } - // Auto-expand multiple if not yet turned on, and value has multiple - if (this.options.expand_multiple_rows && !this.options.multiple && jQuery.isArray(_value) && _value.length > 1) { - this.set_multiple(true, this.options.expand_multiple_rows); - } - jQuery("option", this.input).prop("selected", false); - if (typeof _value == "object") { - for (var i in _value) { - this.find_option(_value[i]).prop("selected", true); - } - } - else { - if (_value && this.find_option(_value).prop("selected", true).length == 0) { - if (this.options.select_options[_value] || - this.options.select_options.filter && - this.options.select_options.filter(function (value) { return value == _value; }) && - !_dont_try_set_options) { - // Options not set yet? Do that now, which will try again. - return this.set_select_options(this.options.select_options); - } - else if (_dont_try_set_options) { - this.value = ""; - } - else if (jQuery.isEmptyObject(this.options.select_options)) { - this.egw().debug("warn", "Can't set value to '%s', widget has no options set", _value, this); - this.value = null; - } - else { - var debug_value = _value; - if (debug_value === null) - debug_value == 'NULL'; - this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this); - } - return; - } - } - this.value = _value; - if (this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) { - this.input.change(); - } - } - /** - * Find an option by it's value - * - * Taking care of escaping values correctly eg. EGroupware\Api\Mail\Smtp - * - * @param {string} _value - * @return {array} - */ - find_multi_option(_value) { - return jQuery("input[value='" + - (typeof _value === 'string' ? _value.replace(this._escape_value_replace, this._escape_value_with) : _value) + - "']", this.multiOptions); - } - set_multi_value(_value) { - jQuery("input", this.multiOptions).prop("checked", false); - if (typeof _value == "object") { - for (var i in _value) { - this.find_multi_option(_value[i]).prop("checked", true); - } - } - else { - if (this.find_multi_option(_value).prop("checked", true).length == 0) { - var debug_value = _value; - if (debug_value === null) - debug_value == 'NULL'; - this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this); - } - } - // Sort selected to the top - if (this.selected_first) { - this.multiOptions.find("li:has(input:checked)").prependTo(this.multiOptions); - } - this.value = _value; - } - /** - * Method to check all options of a multi-select, if not all are selected, or none if all where selected - * - * @todo: add an attribute to automatic add a button calling this method - */ - select_all_toggle() { - var all = jQuery("input", this.multiOptions); - all.prop("checked", jQuery("input:checked", this.multiOptions).length == all.length ? false : true); - } - /** - * Add a button to toggle between single select and multi select. - * - * @param {number} _rows How many rows for multi-select - */ - set_expand_multiple_rows(_rows) { - this.options.expand_multiple_rows = _rows; - var surroundings = this.getSurroundings(); - if (_rows <= 1 && this.expand_button) { - // Remove - surroundings.removeDOMNode(this.expand_button.get(0)); - } - else { - if (!this.expand_button) { - var button_id = this.getInstanceManager().uniqueId + '_' + this.id.replace(/\./g, '-') + "_expand"; - this.expand_button = 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} - */ - _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) { - 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 - * @param {Object} _extra Additional (app-specific or special) parameters - * @returns {Boolean} returns false if not successful - */ - share_link(_action, _senders, _target, _writable, _files, _callback, _extra) { - 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; - } - if (typeof _extra === 'undefined') { - _extra = {}; - } - return egw.json('EGroupware\\Api\\Sharing::ajax_create', [_action.id, path, _writable, _files, _extra], _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 } } - }); - } - /** - * Keep a list of all EgwApp instances - * - * This is not just the globals available in window.app, it also includes private instances as well - * - * @private - * @param app_obj - */ - static _register_instance(app_obj) { - // Reject improper objects - if (!app_obj.appname) - return; - EgwApp._instances.push(app_obj); - } - /** - * Iterator over all app instances - * - * Use for(const app of EgwApp) {...} to iterate over all app objects. - */ - static [Symbol.iterator]() { - return EgwApp._instances[Symbol.iterator](); - } -} -/** - * In some cases (CRM) a private, disconnected app instance is created instead of - * using the global. We want to be able to access them for observer() & push(), so - * we track all instances. - */ -EgwApp._instances = []; -// EgwApp need to be global on window, as it's used to iterate through all EgwApp instances -window.EgwApp = EgwApp; -//# sourceMappingURL=egw_app.js.map \ No newline at end of file diff --git a/calendar/js/View.js b/calendar/js/View.js deleted file mode 100644 index d3fd2ee41c..0000000000 --- a/calendar/js/View.js +++ /dev/null @@ -1,329 +0,0 @@ -import { egw } from "../../api/js/jsapi/egw_global"; -export class View { - /** - * Translated label for header - * @param {Object} state - * @returns {string} - */ - static header(state) { - let formatDate = new Date(state.date); - formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000); - return View._owner(state) + date(egw.preference('dateformat'), formatDate); - } - /** - * If one owner, get the owner text - * - * @param {object} state - */ - static _owner(state) { - let owner = ''; - if (state.owner.length && state.owner.length == 1 && app.calendar.sidebox_et2) { - var own = app.calendar.sidebox_et2.getWidgetById('owner').getDOMNode(); - if (own.selectedIndex >= 0) { - owner = own.options[own.selectedIndex].innerHTML + ": "; - } - } - return owner; - } - /** - * Get the start date for this view - * @param {Object} state - * @returns {Date} - */ - static start_date(state) { - const d = state.date ? new Date(state.date) : new Date(); - d.setUTCHours(0); - d.setUTCMinutes(0); - d.setUTCSeconds(0); - d.setUTCMilliseconds(0); - return d; - } - /** - * Get the end date for this view - * @param {Object} state - * @returns {Date} - */ - static end_date(state) { - const d = state.date ? new Date(state.date) : new Date(); - d.setUTCHours(23); - d.setUTCMinutes(59); - d.setUTCSeconds(59); - d.setUTCMilliseconds(0); - return d; - } - /** - * Get the owner for this view - * - * This is always the owner from the given state, we use a function - * to trigger setting the widget value. - * - * @param {number[]|String} state state.owner List of owner IDs, or a comma seperated list - * @returns {number[]|String} - */ - static owner(state) { - return state.owner || 0; - } - /** - * Should the view show the weekends - * - * @param {object} state - * @returns {boolean} Current preference to show 5 or 7 days in weekview - */ - static show_weekend(state) { - return state.weekend; - } - /** - * How big or small are the displayed time chunks? - * - * @param {object} state - */ - static granularity(state) { - var list = egw.preference('use_time_grid', 'calendar'); - if (list == '0' || typeof list === 'undefined') { - return parseInt('' + egw.preference('interval', 'calendar')) || 30; - } - if (typeof list == 'string') - list = list.split(','); - if (!list.indexOf && jQuery.isPlainObject(list)) { - list = jQuery.map(list, function (el) { - return el; - }); - } - return list.indexOf(state.view) >= 0 ? - 0 : - parseInt(egw.preference('interval', 'calendar')) || 30; - } - static extend(sub) { - return jQuery.extend({}, this, { _super: this }, sub); - } - /** - * Determines the new date after scrolling. The default is 1 week. - * - * @param {number} delta Integer for how many 'ticks' to move, positive for - * forward, negative for backward - * @returns {Date} - */ - static scroll(delta) { - var d = new Date(app.calendar.state.date); - d.setUTCDate(d.getUTCDate() + (7 * delta)); - return d; - } -} -// List of etemplates to show for this view -View.etemplates = ['calendar.view']; -/** - * Etemplates and settings for the different views. Some (day view) - * use more than one template, some use the same template as others, - * most need different handling for their various attributes. - */ -export class day extends View { - static header(state) { - var formatDate = new Date(state.date); - formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000); - return date('l, ', formatDate) + super.header(state); - } - static start_date(state) { - var d = super.start_date(state); - state.date = app.calendar.date.toString(d); - return d; - } - static show_weekend(state) { - state.days = '1'; - return true; - } - static scroll(delta) { - var d = new Date(app.calendar.state.date); - d.setUTCDate(d.getUTCDate() + (delta)); - return d; - } -} -day.etemplates = ['calendar.view', 'calendar.todo']; -export class day4 extends View { - static end_date(state) { - var d = super.end_date(state); - state.days = '4'; - d.setUTCHours(24 * 4 - 1); - d.setUTCMinutes(59); - d.setUTCSeconds(59); - d.setUTCMilliseconds(0); - return d; - } - static show_weekend(state) { - state.weekend = 'true'; - return true; - } - static scroll(delta) { - var d = new Date(app.calendar.state.date); - d.setUTCDate(d.getUTCDate() + (4 * delta)); - return d; - } -} -export class week extends View { - static header(state) { - var end_date = state.last; - if (!week.show_weekend(state)) { - end_date = new Date(state.last); - end_date.setUTCDate(end_date.getUTCDate() - 2); - } - return super._owner(state) + app.calendar.egw.lang('Week') + ' ' + - app.calendar.date.week_number(state.first) + ': ' + - app.calendar.date.long_date(state.first, end_date); - } - static start_date(state) { - return app.calendar.date.start_of_week(super.start_date(state)); - } - static end_date(state) { - var d = app.calendar.date.start_of_week(state.date || new Date()); - // Always 7 days, we just turn weekends on or off - d.setUTCHours(24 * 7 - 1); - d.setUTCMinutes(59); - d.setUTCSeconds(59); - d.setUTCMilliseconds(0); - return d; - } -} -export class weekN extends View { - static header(state) { - return super._owner(state) + app.calendar.egw.lang('Week') + ' ' + - app.calendar.date.week_number(state.first) + ' - ' + - app.calendar.date.week_number(state.last) + ': ' + - app.calendar.date.long_date(state.first, state.last); - } - static start_date(state) { - return app.calendar.date.start_of_week(super.start_date(state)); - } - static end_date(state) { - state.days = '' + (state.days >= 5 ? state.days : egw.preference('days_in_weekview', 'calendar') || 7); - var d = app.calendar.date.start_of_week(super.start_date(state)); - // Always 7 days, we just turn weekends on or off - d.setUTCHours(24 * 7 * (parseInt(app.calendar.egw.preference('multiple_weeks', 'calendar')) || 3) - 1); - return d; - } -} -export class month extends View { - static header(state) { - var formatDate = new Date(state.date); - formatDate = new Date(formatDate.valueOf() + formatDate.getTimezoneOffset() * 60 * 1000); - return super._owner(state) + app.calendar.egw.lang(date('F', formatDate)) + ' ' + date('Y', formatDate); - } - static start_date(state) { - var d = super.start_date(state); - d.setUTCDate(1); - return app.calendar.date.start_of_week(d); - } - static end_date(state) { - var d = super.end_date(state); - d = new Date(d.getFullYear(), d.getUTCMonth() + 1, 1, 0, -d.getTimezoneOffset(), 0); - d.setUTCSeconds(d.getUTCSeconds() - 1); - return app.calendar.date.end_of_week(d); - } - static scroll(delta) { - var d = new Date(app.calendar.state.date); - // Set day to 15 so we don't get overflow on short months - // eg. Aug 31 + 1 month = Sept 31 -> Oct 1 - d.setUTCDate(15); - d.setUTCMonth(d.getUTCMonth() + delta); - return d; - } -} -export class planner extends View { - static header(state) { - var startDate = new Date(state.first); - startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000); - var endDate = new Date(state.last); - endDate = new Date(endDate.valueOf() + endDate.getTimezoneOffset() * 60 * 1000); - return super._owner(state) + date(egw.preference('dateformat'), startDate) + - (startDate == endDate ? '' : ' - ' + date(egw.preference('dateformat'), endDate)); - } - static group_by(state) { - return state.sortby ? state.sortby : 0; - } - // Note: Planner uses the additional value of planner_view to determine - // the start & end dates using other view's functions - static start_date(state) { - // Start here, in case we can't find anything better - var d = super.start_date(state); - if (state.sortby && state.sortby === 'month') { - d.setUTCDate(1); - } - else if (state.planner_view && app.classes.calendar.views[state.planner_view]) { - d = app.classes.calendar.views[state.planner_view].start_date.call(this, state); - } - else { - d = app.calendar.date.start_of_week(d); - d.setUTCHours(0); - d.setUTCMinutes(0); - d.setUTCSeconds(0); - d.setUTCMilliseconds(0); - return d; - } - return d; - } - static end_date(state) { - var d = super.end_date(state); - if (state.sortby && state.sortby === 'month') { - d.setUTCDate(0); - d.setUTCFullYear(d.getUTCFullYear() + 1); - } - else if (state.planner_view && app.classes.calendar.views[state.planner_view]) { - d = app.classes.calendar.views[state.planner_view].end_date(state); - } - else if (state.days) { - // This one comes from a grid view, but we'll use it - d.setUTCDate(d.getUTCDate() + parseInt(state.days) - 1); - delete state.days; - } - else { - d = app.calendar.date.end_of_week(d); - } - return d; - } - static hide_empty(state) { - var check = state.sortby == 'user' ? ['user', 'both'] : ['cat', 'both']; - return (check.indexOf(egw.preference('planner_show_empty_rows', 'calendar') + '') === -1); - } - static scroll(delta) { - if (app.calendar.state.planner_view && !isNaN(delta) && app.calendar.state.sortby !== "month") { - return app.classes.calendar.views[app.calendar.state.planner_view].scroll(delta); - } - let d = new Date(app.calendar.state.date); - let days = 1; - delta = parseInt(delta) || 0; - // Yearly view, grouped by month - scroll 1 month - if (app.calendar.state.sortby === 'month') { - d.setUTCMonth(d.getUTCMonth() + delta); - d.setUTCDate(1); - d.setUTCHours(0); - d.setUTCMinutes(0); - return d; - } - // Need to set the day count, or auto date ranging takes over and - // makes things buggy - if (app.calendar.state.first && app.calendar.state.last) { - //@ts-ignore - let diff = new Date(app.calendar.state.last) - new Date(app.calendar.state.first); - days = Math.round(diff / (1000 * 3600 * 24)); - } - d.setUTCDate(d.getUTCDate() + (days * delta)); - if (days > 8) { - d = app.calendar.date.start_of_week(d); - } - return d; - } -} -planner.etemplates = ['calendar.planner']; -export class listview extends View { - static header(state) { - var startDate = new Date(state.first || state.date); - startDate = new Date(startDate.valueOf() + startDate.getTimezoneOffset() * 60 * 1000); - var start_check = '' + startDate.getFullYear() + startDate.getMonth() + startDate.getDate(); - var endDate = new Date(state.last || state.date); - endDate = new Date(endDate.valueOf() + endDate.getTimezoneOffset() * 60 * 1000); - var end_check = '' + endDate.getFullYear() + endDate.getMonth() + endDate.getDate(); - return super._owner(state) + - date(egw.preference('dateformat'), startDate) + - (start_check == end_check ? '' : ' - ' + date(egw.preference('dateformat'), endDate)); - } -} -listview.etemplates = ['calendar.list']; -//# sourceMappingURL=View.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_daycol.js b/calendar/js/et2_widget_daycol.js deleted file mode 100644 index c3a2f95ce7..0000000000 --- a/calendar/js/et2_widget_daycol.js +++ /dev/null @@ -1,988 +0,0 @@ -/* - * Egroupware - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - et2_core_valueWidget; - /calendar/js/et2_widget_event.js; -*/ -import { et2_createWidget, et2_register_widget } from "../../api/js/etemplate/et2_core_widget"; -import { et2_valueWidget } from "../../api/js/etemplate/et2_core_valueWidget"; -import { et2_calendar_timegrid } from "./et2_widget_timegrid"; -import { et2_calendar_view } from "./et2_widget_view"; -import { et2_calendar_event } from "./et2_widget_event"; -import { ClassWithAttributes } from "../../api/js/etemplate/et2_core_inheritance"; -import { et2_no_init } from "../../api/js/etemplate/et2_core_common"; -import { egw } from "../../api/js/jsapi/egw_global"; -import { egwIsMobile } from "../../api/js/egw_action/egw_action_common.js"; -import { CalendarApp } from "./app"; -import { sprintf } from "../../api/js/egw_action/egw_action_common.js"; -/** - * Class which implements the "calendar-timegrid" XET-Tag for displaying a single days - * - * This widget is responsible mostly for positioning its events - * - */ -export class et2_calendar_daycol extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_daycol._attributes, _child || {})); - this.registeredUID = null; - // Init to defaults, just in case - they will be updated from parent - this.display_settings = { - wd_start: 60 * 9, - wd_end: 60 * 17, - granularity: 30, - rowsToDisplay: 10, - rowHeight: 20, - // Percentage; not yet available - titleHeight: 2.0 - }; - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_calDayCol") - .css('width', this.options.width) - .css('left', this.options.left); - this.header = jQuery(document.createElement('div')) - .addClass("calendar_calDayColHeader") - .css('width', this.options.width) - .css('left', this.options.left); - this.title = jQuery(document.createElement('div')) - .addClass('et2_clickable et2_link') - .appendTo(this.header); - this.user_spacer = jQuery(document.createElement('div')) - .addClass("calendar_calDayColHeader_spacer") - .appendTo(this.header); - this.all_day = jQuery(document.createElement('div')) - .addClass("calendar_calDayColAllDay") - .css('max-height', (egw.preference('limit_all_day_lines', 'calendar') || 3) * 1.4 + 'em') - .appendTo(this.header); - this.event_wrapper = jQuery(document.createElement('div')) - .addClass("event_wrapper") - .appendTo(this.div); - this.setDOMNode(this.div[0]); - // Used for its date calculations - note this is a datetime, parent - // uses just a date - this._date_helper = et2_createWidget('date-time', {}, null); - this._date_helper.loadingFinished(); - } - doLoadingFinished() { - let result = super.doLoadingFinished(); - // Parent will have everything we need, just load it from there - if (this.getParent() && this.getParent().options.owner) { - this.set_owner(this.getParent().options.owner); - } - if (this.title.text() === '' && this.options.date && - this.getParent() && this.getParent().instanceOf(et2_calendar_timegrid)) { - // Forces an update - const date = this.options.date; - this.options.date = ''; - this.set_date(date); - } - return result; - } - destroy() { - super.destroy(); - this.div.off(); - this.header.off().remove(); - this.title.off(); - this.div = null; - this.header = null; - this.title = null; - this.user_spacer = null; - // date_helper has no parent, so we must explicitly remove it - this._date_helper.destroy(); - this._date_helper = null; - egw.dataUnregisterUID(this.registeredUID, null, this); - } - getDOMNode(sender) { - if (!sender || sender === this) - return this.div[0]; - if (sender.instanceOf && sender.instanceOf(et2_calendar_event)) { - if (this.display_settings.granularity === 0) { - return this.event_wrapper[0]; - } - if (sender.options.value.whole_day_on_top || - sender.options.value.whole_day && sender.options.value.non_blocking === true) { - return this.all_day[0]; - } - return this.div[0]; - } - } - /** - * Draw the individual divs for clicking to add an event - */ - _draw() { - // Remove any existing - jQuery('.calendar_calAddEvent', this.div).remove(); - // Grab real values from parent - if (this.getParent() && this.getParent().instanceOf(et2_calendar_timegrid)) { - this.display_settings.wd_start = 60 * this.getParent().options.day_start; - this.display_settings.wd_end = 60 * this.getParent().options.day_end; - this.display_settings.granularity = this.getParent().options.granularity; - const header = this.getParent().dayHeader.children(); - // Figure out insert index - let idx = 0; - const siblings = this.getParent().getDOMNode(this).childNodes; - while (idx < siblings.length && siblings[idx] != this.getDOMNode()) { - idx++; - } - // Stick header in the right place - if (idx == 0) { - this.getParent().dayHeader.prepend(this.header); - } - else if (header.length) { - header.eq(Math.min(header.length, idx) - 1).after(this.header); - } - } - this.div.attr('data-date', this.options.date); - } - getDate() { - return this.date; - } - get date_helper() { - return this._date_helper; - } - /** - * Set the date - * - * @param {string|Date} _date New date - * @param {Object[]} events =false List of event data to be displayed, or false to - * automatically fetch data from content array - * @param {boolean} force_redraw =false Redraw even if the date is the same. - * Used for when new data is available. - */ - set_date(_date, events, force_redraw) { - if (typeof events === 'undefined' || !events) { - events = false; - } - if (typeof force_redraw === 'undefined' || !force_redraw) { - force_redraw = false; - } - if (!this.getParent() || !this.getParent().date_helper) { - egw.debug('warn', 'Day col widget "' + this.id + '" is missing its parent.'); - return false; - } - if (typeof _date === "object") { - this.getParent().date_helper.set_value(_date); - } - else if (typeof _date === "string") { - // Need a new date to avoid invalid month/date combinations when setting - // month then day. Use a string to avoid browser timezone. - this.getParent().date_helper.set_value(_date.substring(0, 4) + '-' + (_date.substring(4, 6)) + '-' + _date.substring(6, 8) + 'T00:00:00Z'); - } - this.date = new Date(this.getParent().date_helper.getValue()); - // Keep internal option in Ymd format, it gets passed around in this format - const new_date = "" + this.getParent().date_helper.get_year() + - sprintf("%02d", this.getParent().date_helper.get_month()) + - sprintf("%02d", this.getParent().date_helper.get_date()); - // Set label - if (!this.options.label) { - // Add timezone offset back in, or formatDate will lose those hours - const formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000); - this.title.html('' + jQuery.datepicker.formatDate('DD', formatDate) + - '' + jQuery.datepicker.formatDate('D', formatDate) + '' + - jQuery.datepicker.formatDate('d', formatDate)); - } - this.title - .attr("data-date", new_date) - .toggleClass('et2_label', !!this.options.label); - this.header - .attr('data-date', new_date) - .attr('data-whole_day', true); - // Avoid redrawing if date is the same - if (new_date === this.options.date && - this.display_settings.granularity === this.getParent().options.granularity && - !force_redraw) { - return; - } - const cache_id = CalendarApp._daywise_cache_id(new_date, this.options.owner); - if (this.options.date && this.registeredUID && - cache_id !== this.registeredUID) { - egw.dataUnregisterUID(this.registeredUID, null, this); - // Remove existing events - while (this._children.length > 0) { - const node = this._children[this._children.length - 1]; - this.removeChild(node); - node.destroy(); - } - } - this.options.date = new_date; - // Set holiday and today classes - this.day_class_holiday(); - // Update all the little boxes - this._draw(); - // Register for updates on events for this day - if (this.registeredUID !== cache_id) { - this.registeredUID = cache_id; - egw.dataRegisterUID(this.registeredUID, this._data_callback, this, this.getInstanceManager().execId, this.id); - } - } - /** - * Set the owner of this day - * - * @param {number|number[]|string|string[]} _owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - */ - set_owner(_owner) { - this.title - .attr("data-owner", _owner); - this.header.attr('data-owner', _owner); - this.div.attr('data-owner', _owner); - // Simple comparison, both numbers - if (_owner === this.options.owner) - return; - // More complicated comparison, one or the other is an array - if ((typeof _owner == 'object' || typeof this.options.owner == 'object') && - _owner.toString() == this.options.owner.toString()) { - return; - } - this.options.owner = typeof _owner !== 'object' ? [_owner] : _owner; - const cache_id = CalendarApp._daywise_cache_id(this.options.date, _owner); - if (this.options.date && this.registeredUID && - cache_id !== this.registeredUID) { - egw.dataUnregisterUID(this.registeredUID, null, this); - } - if (this.registeredUID !== cache_id) { - this.registeredUID = cache_id; - egw.dataRegisterUID(this.registeredUID, this._data_callback, this, this.getInstanceManager().execId, this.id); - } - } - set_class(classnames) { - this.header.removeClass(this.class); - super.set_class(classnames); - this.header.addClass(classnames); - } - /** - * Callback used when the daywise data changes - * - * Events should update themselves when their data changes, here we are - * dealing with a change in which events are displayed on this day. - * - * @param {String[]} event_ids - * @returns {undefined} - */ - _data_callback(event_ids) { - const events = []; - if (event_ids == null || typeof event_ids.length == 'undefined') - event_ids = []; - for (let i = 0; i < event_ids.length; i++) { - let event = egw.dataGetUIDdata('calendar::' + event_ids[i]); - event = event && event.data || false; - if (event && event.date && et2_calendar_event.owner_check(event, this) && (event.date === this.options.date || - // Accept multi-day events - new Date(event.start) <= this.date //&& new Date(event.end) >= this.date - )) { - events.push(event); - } - else if (event) { - // Got an ID that doesn't belong - event_ids.splice(i--, 1); - } - } - if (!this.div.is(":visible")) { - // Not visible, defer the layout or it all winds up at the top - // Cancel any existing listener & bind - jQuery(this.getInstanceManager().DOMContainer.parentNode) - .off('show.' + CalendarApp._daywise_cache_id(this.options.date, this.options.owner)) - .one('show.' + CalendarApp._daywise_cache_id(this.options.date, this.options.owner), function () { - this._update_events(events); - }.bind(this)); - return; - } - if (!this.getParent().disabled) - this._update_events(events); - } - set_label(label) { - this.options.label = label; - this.title.text(label); - this.title.toggleClass('et2_clickable et2_link', label === ''); - } - set_left(left) { - if (this.div) { - this.div.css('left', left); - } - } - set_width(width) { - this.options.width = width; - if (this.div) { - this.div.outerWidth(this.options.width); - this.header.outerWidth(this.options.width); - } - } - /** - * Applies class for today, and any holidays for current day - */ - day_class_holiday() { - this.title - // Remove all special day classes - .removeClass('calendar_calToday calendar_calBirthday calendar_calHoliday') - // Except this one... - .addClass("et2_clickable et2_link"); - this.title.attr('data-holiday', ''); - // Set today class - note +1 when dealing with today, as months in JS are 0-11 - const today = new Date(); - today.setUTCMinutes(today.getUTCMinutes() - today.getTimezoneOffset()); - this.title.toggleClass("calendar_calToday", this.options.date === '' + today.getUTCFullYear() + - sprintf("%02d", today.getUTCMonth() + 1) + - sprintf("%02d", today.getUTCDate())); - // Holidays and birthdays - let holidays = et2_calendar_view.get_holidays(this, this.options.date.substring(0, 4)); - const holiday_list = []; - let holiday_pref = (egw.preference('birthdays_as_events', 'calendar') || []); - if (typeof holiday_pref === 'string') { - holiday_pref = holiday_pref.split(','); - } - else { - holiday_pref = jQuery.extend([], holiday_pref); - } - // Show holidays as events on mobile or by preference - const holidays_as_events = egwIsMobile() || egw.preference('birthdays_as_events', 'calendar') === true || - holiday_pref.indexOf('holiday') >= 0; - const birthdays_as_events = egwIsMobile() || holiday_pref.indexOf('birthday') >= 0; - if (holidays && holidays[this.options.date]) { - holidays = holidays[this.options.date]; - for (let i = 0; i < holidays.length; i++) { - if (typeof holidays[i]['birthyear'] !== 'undefined') { - // Show birthdays as events on mobile or by preference - if (birthdays_as_events) { - // Create event - this.getParent().date_helper.set_value(this.options.date.substring(0, 4) + '-' + - (this.options.date.substring(4, 6)) + '-' + this.options.date.substring(6, 8) + - 'T00:00:00Z'); - var event = et2_createWidget('calendar-event', { - id: 'event_' + holidays[i].name, - value: { - title: holidays[i].name, - whole_day: true, - whole_day_on_top: true, - start: new Date(this.getParent().date_helper.get_value()), - end: this.options.date, - owner: this.options.owner, - participants: this.options.owner, - app: 'calendar', - class: 'calendar_calBirthday' - }, - readonly: true, - class: 'calendar_calBirthday' - }, this); - event.doLoadingFinished(); - event._update(); - } - if (!egwIsMobile()) { - //If the birthdays are already displayed as event, don't - //show them in the caption - this.title.addClass('calendar_calBirthday'); - holiday_list.push(holidays[i]['name']); - } - } - else { - // Show holidays as events on mobile - if (holidays_as_events) { - // Create event - this.getParent().date_helper.set_value(this.options.date.substring(0, 4) + '-' + - (this.options.date.substring(4, 6)) + '-' + this.options.date.substring(6, 8) + - 'T00:00:00Z'); - var event = et2_createWidget('calendar-event', { - id: 'event_' + holidays[i].name, - value: { - title: holidays[i].name, - whole_day: true, - whole_day_on_top: true, - start: new Date(this.getParent().date_helper.get_value()), - end: this.options.date, - owner: this.options.owner, - participants: this.options.owner, - app: 'calendar', - class: 'calendar_calHoliday' - }, - readonly: true, - class: 'calendar_calHoliday' - }, this); - event.doLoadingFinished(); - event._update(); - } - else { - this.title.addClass('calendar_calHoliday'); - this.title.attr('data-holiday', holidays[i]['name']); - //If the birthdays are already displayed as event, don't - //show them in the caption - if (!this.options.display_holiday_as_event) { - holiday_list.push(holidays[i]['name']); - } - } - } - } - } - this.title.attr('title', holiday_list.join(', ')); - } - /** - * Load the event data for this day and create event widgets for each. - * - * If event information is not provided, it will be pulled from the content array. - * - * @param {Object[]} [_events] Array of event information, one per event. - */ - _update_events(_events) { - let c; - const events = _events || this.getArrayMgr('content').getEntry(this.options.date) || []; - // Remove extra events - while (this._children.length > 0) { - const node = this._children[this._children.length - 1]; - this.removeChild(node); - node.destroy(); - } - // Make sure children are in cronological order, or columns are backwards - events.sort(function (a, b) { - const start = new Date(a.start) - new Date(b.start); - const end = new Date(a.end) - new Date(b.end); - // Whole day events sorted by ID, normal events by start / end time - if (a.whole_day && b.whole_day) { - return (a.app_id - b.app_id); - } - else if (a.whole_day || b.whole_day) { - return a.whole_day ? -1 : 1; - } - return start ? start : end; - }); - for (c = 0; c < events.length; c++) { - // Create event - var event = et2_createWidget('calendar-event', { - id: 'event_' + events[c].id, - value: events[c] - }, this); - } - // Seperate loop so column sorting finds all children in the right place - let child_length = this._children.length; - for (c = 0; c < events.length && c < child_length; c++) { - let event = this.getWidgetById('event_' + events[c].id); - if (!event) - continue; - if (this.isInTree()) { - event.doLoadingFinished(); - } - } - // Show holidays as events on mobile or by preference - if (egwIsMobile() || egw.preference('birthdays_as_events', 'calendar')) { - this.day_class_holiday(); - } - // Apply styles to hidden events - this._out_of_view(); - } - /** - * Apply styles for out-of-view and partially hidden events - * - * There are 3 different states or modes of display: - * - * - 'Normal' - When showing events positioned by time, the indicator is just - * a bar colored by the last category color. On hover it shows either the - * title of a single event or "x event(s)" if more than one are hidden. - * Clicking adjusts the current view to show the earliest / latest hidden - * event - * - * - Fixed - When showing events positioned by time but in a fixed-height - * week (not auto-sized to fit screen) the indicator is the same as sized. - * On hover it shows the titles of the hidden events, clicking changes - * the view to the selected day. - * - * - GridList - When showing just a list, the indicator shows "x event(s)", - * and on hover shows the category color, title & time. Clicking changes - * the view to the selected day, and opens the event for editing. - */ - _out_of_view() { - // Reset - this.header.children('.hiddenEventBefore').remove(); - this.div.children('.hiddenEventAfter').remove(); - this.event_wrapper.css('overflow', 'visible'); - this.all_day.removeClass('overflown'); - jQuery('.calendar_calEventBody', this.div).css({ 'padding-top': '', 'margin-top': '' }); - const timegrid = this.getParent(); - // elem is jquery div of event - function isHidden(elem) { - // Add an extra 5px top and bottom to include events just on the - // edge of visibility - const docViewTop = timegrid.scrolling.scrollTop() + 5, docViewBottom = docViewTop + (this.display_settings.granularity === 0 ? - this.event_wrapper.height() : - timegrid.scrolling.height() - 10), elemTop = elem.position().top, elemBottom = elemTop + elem.outerHeight(true); - if ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)) { - // Entirely visible - return false; - } - const visible = { - hidden: elemTop > docViewTop ? 'bottom' : 'top', - completely: false - }; - visible.completely = visible.hidden == 'top' ? elemBottom < docViewTop : elemTop > docViewBottom; - return visible; - } - // In gridlist view, we can quickly check if we need it at all - if (this.display_settings.granularity === 0 && this._children.length) { - jQuery('div.calendar_calEvent', this.div).show(0); - if (Math.ceil(this.div.height() / this._children[0].div.height()) > this._children.length) { - return; - } - } - // Check all day overflow - this.all_day.toggleClass('overflown', this.all_day[0].scrollHeight - this.all_day.innerHeight() > 5); - // Check each event - this.iterateOver(function (event) { - // Skip whole day events and events missing value - if (this.display_settings.granularity && ((!event.options || !event.options.value || event.options.value.whole_day_on_top))) { - return; - } - // Reset - event.title.css({ 'top': '', 'background-color': '' }); - event.body.css({ 'padding-top': '', 'margin-top': '' }); - const hidden = isHidden.call(this, event.div); - const day = this; - if (!hidden) { - return; - } - // Only top is hidden, move label - // Bottom hidden is fine - if (hidden.hidden === 'top' && !hidden.completely && !event.div.hasClass('calendar_calEventSmall')) { - const title_height = event.title.outerHeight(); - event.title.css({ - 'top': timegrid.scrolling.scrollTop() - event.div.position().top, - 'background-color': 'transparent' - }); - event.body.css({ - 'padding-top': timegrid.scrolling.scrollTop() - event.div.position().top + title_height, - 'margin-top': -title_height - }); - } - // Too many in gridlist view, show indicator - else if (this.display_settings.granularity === 0 && hidden) { - if (jQuery('.hiddenEventAfter', this.div).length == 0) { - this.event_wrapper.css('overflow', 'hidden'); - } - this._hidden_indicator(event, false, function () { - app.calendar.update_state({ view: 'day', date: day.date }); - }); - // Avoid partially visible events - // We need to hide all, or the next row will be visible - event.div.hide(0); - } - // Completely out of view, show indicator - else if (hidden.completely) { - this._hidden_indicator(event, hidden.hidden == 'top', false); - } - }, this, et2_calendar_event); - } - /** - * Show an indicator that there are hidden events - * - * The indicator works 3 different ways, depending on if the day can be - * scrolled, is fixed, or if in gridview. - * - * @see _out_of_view() - * - * @param {et2_calendar_event} event Event we're creating the indicator for - * @param {boolean} top Events hidden at the top (true) or bottom (false) - * @param {function} [onclick] Callback for when user clicks on the indicator - */ - _hidden_indicator(event, top, onclick) { - let indicator = null; - const day = this; - const timegrid = this.getParent(); - const fixed_height = timegrid.div.hasClass('calendar_calTimeGridFixed'); - // Event is before the displayed times - if (top) { - // Create if not already there - if (jQuery('.hiddenEventBefore', this.header).length === 0) { - indicator = jQuery('
      ') - .appendTo(this.header) - .attr('data-hidden_count', 1); - if (!fixed_height) { - indicator - .text(event.options.value.title) - .on('click', typeof onclick === 'function' ? onclick : function () { - jQuery('.calendar_calEvent', day.div).first()[0].scrollIntoView(); - return false; - }); - } - } - else { - indicator = jQuery('.hiddenEventBefore', this.header); - indicator.attr('data-hidden_count', parseInt(indicator.attr('data-hidden_count')) + 1); - if (!fixed_height) { - indicator.text(day.egw().lang('%1 event(s) %2', indicator.attr('data-hidden_count'), '')); - } - } - } - // Event is after displayed times - else { - indicator = jQuery('.hiddenEventAfter', this.div); - // Create if not already there - if (indicator.length === 0) { - indicator = jQuery('
      ') - .attr('data-hidden_count', 0) - .appendTo(this.div); - if (!fixed_height) { - indicator - .on('click', typeof onclick === 'function' ? onclick : function () { - jQuery('.calendar_calEvent', day.div).last()[0].scrollIntoView(false); - // Better re-run this to clean up - day._out_of_view(); - return false; - }); - } - else { - indicator - .on('mouseover', function () { - indicator.css({ - 'height': (indicator.attr('data-hidden_count') * 1.2) + 'em', - 'margin-top': -(indicator.attr('data-hidden_count') * 1.2) + 'em' - }); - }) - .on('mouseout', function () { - indicator.css({ - 'height': '', - 'margin-top': '' - }); - }); - } - } - const count = parseInt(indicator.attr('data-hidden_count')) + 1; - indicator.attr('data-hidden_count', count); - if (this.display_settings.granularity === 0) { - indicator.append(event.div.clone()); - indicator.attr('data-hidden_label', day.egw().lang('%1 event(s) %2', indicator.attr('data-hidden_count'), '')); - } - else if (!fixed_height) { - indicator.text(day.egw().lang('%1 event(s) %2', indicator.attr('data-hidden_count'), '')); - } - indicator.css('top', timegrid.scrolling.height() + timegrid.scrolling.scrollTop() - indicator.innerHeight()); - } - // Show different stuff for fixed height - if (fixed_height) { - indicator - .append("
      " + - event.options.value.title + - "
      "); - } - // Match color to the event - if (indicator !== null) { - // Avoid white, which is hard to see - // Use border-bottom-color, Firefox doesn't give a value with border-color - const color = jQuery.Color(event.div.css('background-color')).toString() !== jQuery.Color('white').toString() ? - event.div.css('background-color') : event.div.css('border-bottom-color'); - if (color !== 'rgba(0, 0, 0, 0)') { - indicator.css('border-color', color); - } - } - } - /** - * Sort a day's events into minimally overlapping columns - * - * @returns {Array[]} Events sorted into columns - */ - _spread_events() { - if (!this.date) - return []; - let day_start = this.date.valueOf() / 1000; - const dst_check = new Date(this.date); - dst_check.setUTCHours(12); - // if daylight saving is switched on or off, correct $day_start - // gives correct times after 2am, times between 0am and 2am are wrong - const daylight_diff = day_start + 12 * 60 * 60 - (dst_check.valueOf() / 1000); - if (daylight_diff) { - day_start -= daylight_diff; - } - const eventCols = [], col_ends = []; - // Make sure children are in cronological order, or columns are backwards - this._children.sort(function (a, b) { - const start = new Date(a.options.value.start) - new Date(b.options.value.start); - const end = new Date(a.options.value.end) - new Date(b.options.value.end); - // Whole day events sorted by ID, normal events by start / end time - if (a.options.value.whole_day && b.options.value.whole_day) { - // Longer duration comes first so we have nicer bars across the top - const duration = (new Date(b.options.value.end) - new Date(b.options.value.start)) - - (new Date(a.options.value.end) - new Date(a.options.value.start)); - return duration ? duration : (a.options.value.app_id - b.options.value.app_id); - } - else if (a.options.value.whole_day || b.options.value.whole_day) { - return a.options.value.whole_day ? -1 : 1; - } - return start ? start : end; - }); - for (let i = 0; i < this._children.length; i++) { - const event = this._children[i].options.value || false; - if (!event) - continue; - if (event.date && event.date != this.options.date && - // Multi-day events date may be different - (new Date(event.start) >= this.date || new Date(event.end) < this.date)) { - // Still have a child event that has changed date (DnD) - this._children[i].destroy(); - this.removeChild(this._children[i]); - continue; - } - let c = 0; - event['multiday'] = false; - if (typeof event.start !== 'object') { - event.start = new Date(event.start); - } - if (typeof event.end !== 'object') { - event.end = new Date(event.end); - } - event['start_m'] = parseInt(String((event.start.valueOf() / 1000 - day_start) / 60), 10); - if (event['start_m'] < 0) { - event['start_m'] = 0; - event['multiday'] = true; - } - event['end_m'] = parseInt(String((event.end.valueOf() / 1000 - day_start) / 60), 10); - if (event['end_m'] >= 24 * 60) { - event['end_m'] = 24 * 60 - 1; - event['multiday'] = true; - } - if (!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) { - event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); - } - if (!event['whole_day_on_top']) { - for (c = 0; event['start_m'] < col_ends[c]; ++c) - ; - col_ends[c] = event['end_m']; - } - if (typeof eventCols[c] === 'undefined') { - eventCols[c] = []; - } - eventCols[c].push(this._children[i]); - } - return eventCols; - } - /** - * Position the event according to its time and how this widget is laid - * out. - * - * @param {et2_calendar_event} [event] - Event to be updated - * If a single event is not provided, all events are repositioned. - */ - position_event(event) { - // If hidden, skip it - it takes too long - if (!this.div.is(':visible')) - return; - // Sort events into minimally-overlapping columns - const columns = this._spread_events(); - for (let c = 0; c < columns.length; c++) { - // Calculate horizontal positioning - let left = Math.ceil(5 + (1.5 * 100 / (parseFloat(this.options.width) || 100))); - let right = 2; - if (columns.length !== 1) { - right = !c ? 30 : 2; - left += c * (100.0 - left) / columns.length; - } - for (let i = 0; (columns[c].indexOf(event) >= 0 || !event) && i < columns[c].length; i++) { - // Calculate vertical positioning - let top = 0; - let height = 0; - // Position the event - if (this.display_settings.granularity === 0) { - if (this.all_day.has(columns[c][i].div).length) { - columns[c][i].div.prependTo(this.event_wrapper); - } - columns[c][i].div.css('top', ''); - columns[c][i].div.css('height', ''); - columns[c][i].div.css('left', ''); - columns[c][i].div.css('right', ''); - // Strip out of view padding - columns[c][i].body.css('padding-top', ''); - continue; - } - if (columns[c][i].options.value.whole_day_on_top) { - if (!this.all_day.has(columns[c][i].div).length) { - columns[c][i].div.css('top', ''); - columns[c][i].div.css('height', ''); - columns[c][i].div.css('left', ''); - columns[c][i].div.css('right', ''); - columns[c][i].body.css('padding-top', ''); - columns[c][i].div - .appendTo(this.all_day); - this.getParent().resizeTimes(); - } - continue; - } - else { - if (this.all_day.has(columns[c][i].div).length) { - columns[c][i].div.appendTo(this.event_wrapper); - this.getParent().resizeTimes(); - } - top = this._time_to_position(columns[c][i].options.value.start_m); - height = this._time_to_position(columns[c][i].options.value.end_m) - top; - } - // Position the event - if (event && columns[c].indexOf(event) >= 0 || !event) { - columns[c][i].div.css('top', top + '%'); - columns[c][i].div.css('height', height + '%'); - // Remove spacing from border, but only if visible or the height will be wrong - if (columns[c][i].div.is(':visible')) { - const border_diff = columns[c][i].div.outerHeight() - columns[c][i].div.height(); - columns[c][i].div.css('height', 'calc(' + height + '% - ' + border_diff + ')'); - } - // This gives the wrong height - //columns[c][i].div.outerHeight(height+'%'); - columns[c][i].div.css('left', left.toFixed(1) + '%'); - columns[c][i].div.css('right', right.toFixed(1) + '%'); - columns[c][i].div.css('z-index', parseInt(20) + c); - columns[c][i]._small_size(); - } - } - // Only wanted to position this event, leave the other columns alone - if (event && columns[c].indexOf(event) >= 0) { - return; - } - } - } - /** - * Calculates the vertical position based on the time - * - * This calculation is a percentage from 00:00 to 23:59 - * - * @param {int} time in minutes from midnight - * @return {float} position in percent - */ - _time_to_position(time) { - let pos = 0.0; - // 24h - pos = ((time / 60) / 24) * 100; - return pos.toFixed(1); - } - attachToDOM() { - let result = super.attachToDOM(); - // Remove the binding for the click handler, unless there's something - // custom here. - if (!this.onclick) { - jQuery(this.node).off("click"); - } - // But we do want to listen to certain clicks, and handle them internally - jQuery(this.node).on('click.et2_daycol', '.calendar_calDayColHeader,.calendar_calAddEvent', jQuery.proxy(this.click, this)); - return result; - } - /** - * Click handler calling custom handler set via onclick attribute to this.onclick, - * or the default which is to open a new event at that time. - * - * Normally, you don't bind to this one, but the attribute is supported if you - * can get a reference to the widget. - * - * @param {Event} _ev - * @returns {boolean} - */ - click(_ev) { - if (this.getParent().options.readonly) - return; - // Drag to create in progress - if (this.getParent().drag_create.start !== null) - return; - // Click on the title - if (jQuery(_ev.target).hasClass('calendar_calAddEvent')) { - if (this.header.has(_ev.target).length == 0 && !_ev.target.dataset.whole_day) { - // Default handler to open a new event at the selected time - var options = { - date: _ev.target.dataset.date || this.options.date, - hour: _ev.target.dataset.hour || this.getParent().options.day_start, - minute: _ev.target.dataset.minute || 0, - owner: this.options.owner - }; - app.calendar.add(options); - return false; - } - // Header, all day non-blocking - else if (this.header.has(_ev.target).length && !jQuery('.hiddenEventBefore', this.header).has(_ev.target).length || - this.header.is(_ev.target)) { - // Click on the header, but not the title. That's an all-day non-blocking - const end = this.date.getFullYear() + '-' + (this.date.getUTCMonth() + 1) + '-' + this.date.getUTCDate() + 'T23:59'; - let options = { - start: this.date.toJSON(), - end: end, - non_blocking: true, - owner: this.options.owner - }; - app.calendar.add(options); - return false; - } - } - // Day label - else if (this.title.is(_ev.target) || this.title.has(_ev.target).length) { - app.calendar.update_state({ view: 'day', date: this.date.toJSON() }); - return false; - } - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - } - getDetachedNodes() { - return [this.getDOMNode(this)]; - } - setDetachedAttributes(_nodes, _values) { - } - // Resizable interface - /** - * Resize - * - * Parent takes care of setting proper width & height for the containing div - * here we just need to adjust the events to fit the new size. - */ - resize() { - if (this.disabled || !this.div.is(':visible') || this.getParent().disabled) { - return; - } - if (this.display_settings.granularity !== this.getParent().options.granularity) { - // Layout has changed - this._draw(); - // Resize & position all events - this.position_event(); - } - else { - // Don't need to resize & reposition, just clear some stuff - // to reset for _out_of_view() - this.iterateOver(function (widget) { - widget._small_size(); - }, this, et2_calendar_event); - } - this._out_of_view(); - } -} -et2_calendar_daycol._attributes = { - date: { - name: "Date", - type: "any", - description: "What date is this daycol for. YYYYMMDD or Date", - default: et2_no_init - }, - owner: { - name: "Owner", - type: "any", - default: et2_no_init, - description: "Account ID number of the calendar owner, if not the current user" - }, - display_birthday_as_event: { - name: "Birthdays", - type: "boolean", - default: false, - description: "Display birthdays as events" - }, - display_holiday_as_event: { - name: "Holidays", - type: "boolean", - default: false, - description: "Display holidays as events" - } -}; -et2_register_widget(et2_calendar_daycol, ["calendar-daycol"]); -//# sourceMappingURL=et2_widget_daycol.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js deleted file mode 100644 index 816f085ce7..0000000000 --- a/calendar/js/et2_widget_event.js +++ /dev/null @@ -1,1170 +0,0 @@ -/* - * Egroupware Calendar event widget - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - /etemplate/js/et2_core_valueWidget; -*/ -import { et2_createWidget, et2_register_widget } from "../../api/js/etemplate/et2_core_widget"; -import { et2_valueWidget } from "../../api/js/etemplate/et2_core_valueWidget"; -import { ClassWithAttributes } from "../../api/js/etemplate/et2_core_inheritance"; -import { et2_action_object_impl } from "../../api/js/etemplate/et2_core_DOMWidget"; -import { et2_calendar_daycol } from "./et2_widget_daycol"; -import { et2_calendar_planner_row } from "./et2_widget_planner_row"; -import { et2_no_init } from "../../api/js/etemplate/et2_core_common"; -import { egw_getAppObjectManager, egwActionObject } from '../../api/js/egw_action/egw_action.js'; -import { egw } from "../../api/js/jsapi/egw_global"; -import { et2_selectbox } from "../../api/js/etemplate/et2_widget_selectbox"; -import { et2_container } from "../../api/js/etemplate/et2_core_baseWidget"; -import { et2_dialog } from "../../api/js/etemplate/et2_widget_dialog"; -/** - * Class for a single event, displayed in either the timegrid or planner view - * - * It is possible to directly provide all information directly, but calendar - * uses egw.data for caching, so ID is all that is needed. - * - * Note that there are several pieces of information that have 'ID' in them: - * - row_id - used by both et2_calendar_event and the nextmatch to uniquely - * identify a particular entry or entry ocurrence - * - id - Recurring events may have their recurrence as a timestamp after their ID, - * such as '194:1453318200', or not. It's usually (always?) the same as row ID. - * - app_id - the ID according to the source application. For calendar, this - * is the same as ID (but always with the recurrence), for other apps this is - * usually just an integer. With app_id and app, you should be able to call - * egw.open() and get the specific entry. - * - Events from other apps will have their app name prepended to their ID, such - * as 'infolog123', so app_id and id will be different for these events - * - Cache ID is the same as other apps, and looks like 'calendar::' - * - The DOM ID for the containing div is event_ - * - * Events are expected to be added to either et2_calendar_daycol or - * et2_calendar_planner_row rather than either et2_calendar_timegrid or - * et2_calendar_planner directly. - * - */ -export class et2_calendar_event extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_event._attributes, _child || {})); - this._need_actions_linked = false; - const event = this; - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_calEvent") - .addClass(this.options.class) - .css('width', this.options.width) - .on('mouseenter', function () { - // Bind actions on first mouseover for faster creation - if (event._need_actions_linked) { - event._copy_parent_actions(); - } - // Tooltip - if (!event._tooltipElem) { - event.options.statustext_html = true; - event.set_statustext(event._tooltip()); - if (event.statustext) { - return event.div.trigger('mouseenter'); - } - } - // Hacky to remove egw's tooltip border and let the mouse in - window.setTimeout(function () { - jQuery('body .egw_tooltip') - .css('border', 'none') - .on('mouseenter', function () { - event.div.off('mouseleave.tooltip'); - jQuery('body.egw_tooltip').remove(); - jQuery('body').append(this); - jQuery(this).stop(true).fadeTo(400, 1) - .on('mouseleave', function () { - jQuery(this).fadeOut('400', function () { - jQuery(this).remove(); - // Set up to work again - event.set_statustext(event._tooltip()); - }); - }); - }); - }, 105); - }); - this.title = jQuery(document.createElement('div')) - .addClass("calendar_calEventHeader") - .appendTo(this.div); - this.body = jQuery(document.createElement('div')) - .addClass("calendar_calEventBody") - .appendTo(this.div); - this.icons = jQuery(document.createElement('div')) - .addClass("calendar_calEventIcons") - .appendTo(this.title); - this.setDOMNode(this.div[0]); - } - doLoadingFinished() { - super.doLoadingFinished(); - // Already know what is needed to hook to cache - if (this.options.value && this.options.value.row_id) { - egw.dataRegisterUID('calendar::' + this.options.value.row_id, this._UID_callback, this, this.getInstanceManager().execId, this.id); - } - return true; - } - destroy() { - super.destroy(); - if (this._actionObject) { - this._actionObject.remove(); - this._actionObject = null; - } - this.div.off(); - this.title.remove(); - this.title = null; - this.body.remove(); - this.body = null; - this.icons = null; - this.div.remove(); - this.div = null; - jQuery('body.egw_tooltip').remove(); - // Unregister, or we'll continue to be notified... - if (this.options.value) { - const old_app_id = this.options.value.row_id; - egw.dataUnregisterUID('calendar::' + old_app_id, null, this); - } - } - set_value(_value) { - // Un-register for updates - if (this.options.value) { - var old_id = this.options.value.row_id; - if (!_value || !_value.row_id || old_id !== _value.row_id) { - egw.dataUnregisterUID('calendar::' + old_id, null, this); - } - } - this.options.value = _value; - // Register for updates - const id = this.options.value.row_id; - if (!old_id || old_id !== id) { - egw.dataRegisterUID('calendar::' + id, this._UID_callback, this, this.getInstanceManager().execId, this.id); - } - if (_value && !egw.dataHasUID('calendar::' + id)) { - egw.dataStoreUID('calendar::' + id, _value); - } - } - /** - * Callback for changes in cached data - */ - _UID_callback(event) { - // Copy to avoid changes, which may cause nm problems - const value = event === null ? null : jQuery.extend({}, event); - let parent = this.getParent(); - let parent_owner = parent.getDOMNode(parent).dataset['owner'] || parent.getParent().options.owner; - if (parent_owner.indexOf(',') >= 0) { - parent_owner = parent_owner.split(','); - } - // Make sure id is a string, check values - if (value) { - this._values_check(value); - } - // Check for changing days in the grid view - let state = this.getInstanceManager().app_obj.calendar.getState() || app.calendar.getState(); - if (!this._sameday_check(value) || !this._status_check(value, state.status_filter, parent_owner)) { - // May need to update parent to remove out-of-view events - parent.removeChild(this); - if (event === null && parent && parent.instanceOf(et2_calendar_daycol)) { - parent._out_of_view(); - } - // This should now cease to exist, as new events have been created - this.destroy(); - return; - } - // Copy to avoid changes, which may cause nm problems - this.options.value = jQuery.extend({}, value); - if (this.getParent().options.date) { - this.options.value.date = this.getParent().options.date; - } - // Let parent position - could also be et2_calendar_planner_row - this.getParent().position_event(this); - // Parent may remove this if the date isn't the same - if (this.getParent()) { - this._update(); - } - } - /** - * Draw the event - */ - _update() { - // Update to reflect new information - const event = this.options.value; - const id = event.row_id ? event.row_id : event.id + (event.recur_type ? ':' + event.recur_date : ''); - const formatted_start = event.start.toJSON(); - this.set_id('event_' + id); - if (this._actionObject) { - this._actionObject.id = 'calendar::' + id; - } - this._need_actions_linked = !this.options.readonly; - // Make sure category stuff is there - // Fake it to use the cache / call - if already there, these will return - // immediately. - const im = this.getInstanceManager(); - et2_selectbox.cat_options({ - _type: 'select-cat', - getInstanceManager: function () { return im; } - }, { application: event.app || 'calendar' }); - // Need cleaning? (DnD helper removes content) - // @ts-ignore - if (!this.div.has(this.title).length) { - this.div - .empty() - .append(this.title) - .append(this.body); - } - if (!this.getParent().options.readonly && !this.options.readonly && this.div.droppable('instance')) { - this.div - // Let timegrid always get the drag - .droppable('option', 'greedy', false); - } - let tooltip = jQuery(this._tooltip()).text(); - // DOM nodes - this.div - // Set full day flag - .attr('data-full_day', event.whole_day) - // Put everything we need for basic interaction here, so it's available immediately - .attr('data-id', event.id) - .attr('data-app', event.app || 'calendar') - .attr('data-app_id', event.app_id) - .attr('data-start', formatted_start) - .attr('data-owner', event.owner) - .attr('data-recur_type', event.recur_type) - .attr('data-resize', event.whole_day ? 'WD' : '' + (event.recur_type ? 'S' : '')) - .attr('data-priority', event.priority) - // Accessibility - .attr("tabindex", 0) - .attr("aria-label", tooltip) - // Remove any category classes - .removeClass(function (index, css) { - return (css.match(/(^|\s)cat_\S+/g) || []).join(' '); - }) - // Remove any status classes - .removeClass(function (index, css) { - return (css.match(/calendar_calEvent\S+/g) || []).join(' '); - }) - .removeClass('calendar_calEventSmall') - .addClass(event.class) - .toggleClass('calendar_calEventPrivate', typeof event.private !== 'undefined' && event.private); - this.options.class = event.class; - const status_class = this._status_class(); - // Add category classes, if real categories are set - if (event.category && event.category != '0') { - const cats = event.category.split(','); - for (let i = 0; i < cats.length; i++) { - this.div.addClass('cat_' + cats[i]); - } - } - this.div.toggleClass('calendar_calEventUnknown', event.participants[egw.user('account_id')] ? event.participants[egw.user('account_id')][0] === 'U' : false); - this.div.addClass(status_class); - this.body.toggleClass('calendar_calEventBodySmall', event.whole_day_on_top || false); - // Header - const title = !event.is_private ? egw.htmlspecialchars(event['title']) : egw.lang('private'); - this.title - .html('' + this._get_timespan(event) + '
      ') - .append('' + title + ''); - // Colors - don't make them transparent if there is no color - // @ts-ignore - if (jQuery.Color("rgba(0,0,0,0)").toRgbaString() != jQuery.Color(this.div, 'background-color').toRgbaString()) { - // Most statuses use colored borders - this.div.css('border-color', this.div.css('background-color')); - } - this.icons.appendTo(this.title) - .html(this._icons().join('')); - // Body - if (event.whole_day_on_top) { - this.body.html(title); - } - else { - // @ts-ignore - const start_time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: event.start_m / 60, - minute: event.start_m % 60, - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); - this.body - .html('' + title + '') - .append('' + start_time + ''); - if (this.options.value.description.trim()) { - this.body - .append('

      ' + egw.htmlspecialchars(this.options.value.description) + '

      '); - } - } - // Clear tooltip for regeneration - this.set_statustext(''); - // Height specific section - // This can take an unreasonable amount of time if parent is hidden - if (jQuery(this.getParent().getDOMNode(this)).is(':visible')) { - this._small_size(); - } - } - /** - * Calculate display variants for when event is too short for full display - * - * Display is based on the number of visible lines, calculated off the header - * height: - * 1 - show just the event title, with ellipsis - * 2 - Show timespan and title, with ellipsis - * > 4 - Show description as well, truncated to fit - */ - _small_size() { - if (this.options.value.whole_day_on_top) - return; - // Skip for planner view, it's always small - if (this.getParent() && this.getParent().instanceOf(et2_calendar_planner_row)) - return; - // Pre-calculation reset - this.div.removeClass('calendar_calEventSmall'); - this.body.css('height', 'auto'); - const line_height = parseFloat(this.div.css('line-height')); - let visible_lines = Math.floor(this.div.innerHeight() / line_height); - if (!this.title.height()) { - // Handle sizing while hidden, such as when calendar is not the active tab - visible_lines = Math.floor(egw.getHiddenDimensions(this.div).h / egw.getHiddenDimensions(this.title).h); - } - visible_lines = Math.max(1, visible_lines); - if (this.getParent() && this.getParent().instanceOf(et2_calendar_daycol)) { - this.div.toggleClass('calendar_calEventSmall', visible_lines < 4); - this.div - .attr('data-visible_lines', visible_lines); - } - else if (this.getParent() && this.getParent().instanceOf(et2_calendar_planner_row)) { - // Less than 8 hours is small - this.div.toggleClass('calendar_calEventSmall', this.options.value.end.valueOf() - this.options.value.start.valueOf() < 28800000); - } - if (this.body.height() > this.div.height() - this.title.height() && visible_lines >= 4) { - this.body.css('height', Math.floor((visible_lines - 1) * line_height - this.title.height()) + 'px'); - } - else { - this.body.css('height', ''); - } - } - /** - * Examines the participants & returns CSS classname for status - * - * @returns {String} - */ - _status_class() { - let status_class = 'calendar_calEventAllAccepted'; - for (let id in this.options.value.participants) { - let status = this.options.value.participants[id]; - status = et2_calendar_event.split_status(status); - switch (status) { - case 'A': - case '': // app without status - break; - case 'U': - status_class = 'calendar_calEventSomeUnknown'; - return status_class; // break for - default: - status_class = 'calendar_calEventAllAnswered'; - break; - } - } - return status_class; - } - /** - * Create tooltip shown on hover - * - * @return {String} - */ - _tooltip() { - if (!this.div || !this.options.value || !this.options.value.app_id) - return ''; - const border = this.div.css('borderTopColor'); - const bg_color = this.div.css('background-color'); - const header_color = this.title.css('color'); - const timespan = this._get_timespan(this.options.value); - const parent = this.getParent() instanceof et2_calendar_daycol ? this.getParent() : this.getParent(); - parent.date_helper.set_value(this.options.value.start.valueOf ? new Date(this.options.value.start) : this.options.value.start); - const start = parent.date_helper.input_date.val(); - parent.date_helper.set_value(this.options.value.end.valueOf ? new Date(this.options.value.end) : this.options.value.end); - const end = parent.date_helper.input_date.val(); - const times = !this.options.value.multiday ? - '' + this.egw().lang('Time') + ':' + timespan : - '' + this.egw().lang('Start') + ':' + start + ' ' + - '' + this.egw().lang('End') + ':' + end; - let cat_label = ''; - if (this.options.value.category) { - const cat = et2_createWidget('select-cat', { 'readonly': true }, this); - cat.set_value(this.options.value.category); - cat_label = this.options.value.category.indexOf(',') <= 0 ? cat.span.text() : []; - if (typeof cat_label != 'string') { - cat.span.children().each(function () { - cat_label.push(jQuery(this).text()); - }); - cat_label = cat_label.join(', '); - } - cat.destroy(); - } - // Location + Videoconference - let location = ''; - if (this.options.value.location || this.options.value['##videoconference']) { - location += '

      ' + this.egw().lang('Location') + ':' + - egw.htmlspecialchars(this.options.value.location); - if (this.options.value['##videoconference']) { - // Click handler is set in _bind_videoconference() - location += (this.options.value.location.trim() ? '
      ' : '') + - '' + - this.egw().lang('Video conference') + - ''; - this._bind_videoconference(); - } - location += '

      '; - } - // Participants - let participants = ''; - if (this.options.value.participant_types['']) { - participants += this.options.value.participant_types[''].join("
      "); - } - for (let type_name in this.options.value.participant_types) { - if (type_name) { - participants += '

      ' + type_name + ':
      '; - participants += this.options.value.participant_types[type_name].join("
      "); - } - } - return '

      ' + - '
      ' + - '' + timespan + '' + - this.icons[0].outerHTML + - '
      ' + - '
      ' + - '

      ' + egw.htmlspecialchars(this.options.value.title) + '


      ' + - egw.htmlspecialchars(this.options.value.description) + '

      ' + - '

      ' + times + '

      ' + - location + - (cat_label ? '

      ' + this.egw().lang('Category') + ':

      ' + cat_label + '

      ' : '') + - '

      ' + this.egw().lang('Participants') + ':


      ' + - participants + '

      ' + this._participant_summary(this.options.value.participants) + - '
      ' + - '
      '; - } - /** - * Generate participant summary line - * - * @returns {String} - */ - _participant_summary(participants) { - if (Object.keys(this.options.value.participants).length < 2) { - return ''; - } - const participant_status = { A: 0, R: 0, T: 0, U: 0, D: 0 }; - const status_label = { A: 'accepted', R: 'rejected', T: 'tentative', U: 'unknown', D: 'delegated' }; - const participant_summary = Object.keys(this.options.value.participants).length + ' ' + this.egw().lang('Participants') + ': '; - const status_totals = []; - for (let id in this.options.value.participants) { - var status = this.options.value.participants[id].substr(0, 1); - participant_status[status]++; - } - for (let status in participant_status) { - if (participant_status[status] > 0) { - status_totals.push(participant_status[status] + ' ' + this.egw().lang(status_label[status])); - } - } - return participant_summary + status_totals.join(', '); - } - /** - * Get actual icons from list - */ - _icons() { - const icons = []; - if (this.options.value.is_private) { - // Hide everything - icons.push(''); - } - else { - if (this.options.value.icons) { - jQuery.extend(icons, this.options.value.icons); - } - else if (this.options.value.app !== 'calendar') { - let app_icon = "" + (egw.link_get_registry(this.options.value.app, 'icon') || (this.options.value.app + '/navbar')); - icons.push(''); - } - if (this.options.value.priority == 3) { - icons.push(''); - } - if (this.options.value.public == '0') { - // Show private flag - icons.push(''); - } - if (this.options.value['recur_type']) { - icons.push(''); - } - // icons for single user, multiple users or group(s) and resources - const single = ''; - const multiple = ''; - for (const uid in this.options.value['participants']) { - // @ts-ignore - if (Object.keys(this.options.value.participants).length == 1 && !isNaN(uid)) { - icons.push(single); - break; - } - // @ts-ignore - if (!isNaN(uid) && icons.indexOf(multiple) === -1) { - icons.push(multiple); - } - /* - * TODO: resource icons - elseif(!isset($icons[$uid[0]]) && isset($this->bo->resources[$uid[0]]) && isset($this->bo->resources[$uid[0]]['icon'])) - { - $icons[$uid[0]] = html::image($this->bo->resources[$uid[0]]['app'], - ($this->bo->resources[$uid[0]]['icon'] ? $this->bo->resources[$uid[0]]['icon'] : 'navbar'), - lang($this->bo->resources[$uid[0]]['app']), - 'width="16px" height="16px"'); - } - */ - } - if (this.options.value.alarm && !jQuery.isEmptyObject(this.options.value.alarm) && !this.options.value.is_private) { - icons.push(''); - } - if (this.options.value.participants[egw.user('account_id')] && this.options.value.participants[egw.user('account_id')][0] == 'U') { - icons.push(''); - } - if (this.options.value["##videoconference"]) { - icons.push(''); - } - } - // Always include non-blocking, regardless of privacy - if (this.options.value.non_blocking) { - icons.push(''); - } - return icons; - } - /** - * Bind the click handler for opening the video conference - * - * Tooltips are placed in the DOM directly in the body, managed by egw. - */ - _bind_videoconference() { - let vc_event = 'click.calendar_videoconference'; - jQuery('body').off(vc_event) - .on(vc_event, '[data-videoconference]', function (event) { - let data = egw.dataGetUIDdata("calendar::" + this.dataset.id); - app.calendar.joinVideoConference(this.dataset.videoconference, data.data || this.dataset); - }); - } - /** - * Get a text representation of the timespan of the event. Either start - * - end, or 'all day' - * - * @param {Object} event Event to get the timespan for - * @param {number} event.start_m Event start, in minutes from midnight - * @param {number} event.end_m Event end, in minutes from midnight - * - * @return {string} Timespan - */ - _get_timespan(event) { - let timespan = ''; - if (event['start_m'] === 0 && event['end_m'] >= 24 * 60 - 1) { - if (event['end_m'] > 24 * 60) { - // @ts-ignore - timespan = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: event.start_m / 60, - minute: event.start_m % 60, - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") === "12") } - // @ts-ignore - ).trim() + ' - ' + jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: event.end_m / 60, - minute: event.end_m % 60, - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); - } - else { - timespan = this.egw().lang('Whole day'); - } - } - else { - let duration = event.multiday ? - (event.end - event.start) / 60000 : - (event.end_m - event.start_m); - duration = Math.floor(duration / 60) + this.egw().lang('h') + (duration % 60 ? duration % 60 : ''); - // @ts-ignore - timespan = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: event.start_m / 60, - minute: event.start_m % 60, - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); - // @ts-ignore - timespan += ' - ' + jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: event.end_m / 60, - minute: event.end_m % 60, - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") === "12") }).trim(); - timespan += ': ' + duration; - } - return timespan; - } - /** - * Make sure event data has all proper values, and format them as expected - * @param {Object} event - */ - _values_check(event) { - // Make sure ID is a string - if (event.id) { - event.id = '' + event.id; - } - // Parent might be a daycol or a planner_row - let parent = this.getParent(); - // Use dates as objects - if (typeof event.start !== 'object') { - parent.date_helper.set_value(event.start); - event.start = new Date(parent.date_helper.getValue()); - } - if (typeof event.end !== 'object') { - parent.date_helper.set_value(event.end); - event.end = new Date(parent.date_helper.getValue()); - } - // We need minutes for durations - if (typeof event.start_m === 'undefined') { - event.start_m = event.start.getUTCHours() * 60 + event.start.getUTCMinutes(); - event.end_m = event.end.getUTCHours() * 60 + event.end.getUTCMinutes(); - } - if (typeof event.multiday === 'undefined') { - event.multiday = (event.start.getUTCFullYear() !== event.end.getUTCFullYear() || - event.start.getUTCMonth() !== event.end.getUTCMonth() || - event.start.getUTCDate() != event.end.getUTCDate()); - } - if (!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) { - event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); - } - } - /** - * Check to see if the provided event information is for the same date as - * what we're currently expecting, and that it has not been changed. - * - * If the date has changed, we adjust the associated daywise caches to move - * the event's ID to where it should be. This check allows us to be more - * directly reliant on the data cache, and less on any other control logic - * elsewhere first. - * - * @param {Object} event Map of event data from cache - * @param {string} event.date For non-recurring, single day events, this is - * the date the event is on. - * @param {string} event.start Start of the event (used for multi-day events) - * @param {string} event.end End of the event (used for multi-day events) - * - * @return {Boolean} Provided event data is for the same date - */ - _sameday_check(event) { - // Event somehow got orphaned, or deleted - if (!this.getParent() || event === null) { - return false; - } - // Also check participants against owner - const owner_match = et2_calendar_event.owner_check(event, this.getParent()); - // Simple, same day - if (owner_match && this.options.value.date && event.date == this.options.value.date) { - return true; - } - // Multi-day non-recurring event spans days - date does not match - const event_start = new Date(event.start); - const event_end = new Date(event.end); - const parent = this.getParent(); - if (owner_match && (parent instanceof et2_calendar_daycol) && parent.getDate() >= event_start && parent.getDate() <= event_end) { - return true; - } - // Delete all old actions - if (this._actionObject) { - this._actionObject.clear(); - this._actionObject.unregisterActions(); - this._actionObject = null; - } - // Update daywise caches - const new_cache_id = CalendarApp._daywise_cache_id(event.date, this.getParent().options.owner); - let new_daywise = egw.dataGetUIDdata(new_cache_id); - new_daywise = new_daywise && new_daywise.data ? new_daywise.data : []; - let old_cache_id = ''; - if (this.options.value && this.options.value.date) { - old_cache_id = CalendarApp._daywise_cache_id(this.options.value.date, parent.options.owner); - } - if (new_cache_id != old_cache_id) { - let old_daywise = egw.dataGetUIDdata(old_cache_id); - old_daywise = old_daywise && old_daywise.data ? old_daywise.data : []; - old_daywise.splice(old_daywise.indexOf(this.options.value.row_id), 1); - egw.dataStoreUID(old_cache_id, old_daywise); - if (new_daywise.indexOf(event.row_id) < 0) { - new_daywise.push(event.row_id); - } - if (egw.dataHasUID(new_cache_id)) { - egw.dataStoreUID(new_cache_id, new_daywise); - } - } - return false; - } - /** - * Check that the event passes the given status filter. - * Status filter is set in the sidebox and used when fetching several events, but if user changes their status - * for an event, it may no longer match and have to be removed. - * - * @param event - * @param filter - * @param owner The owner of the target / parent, not the event owner - * @private - */ - _status_check(event, filter, owner) { - if (!owner || !event) { - return false; - } - // If we're doing a bunch, just one passing is enough - if (typeof owner !== "string") { - let pass = false; - for (let j = 0; j < owner.length && pass == false; j++) { - pass = pass || this._status_check(event, filter, owner[j]); - } - return pass; - } - // Show also events just owned by selected user - // Group members can be owner too, those get handled when we check group memberships below - if (filter == 'owner' && owner == event.owner) { - return true; - } - // Get the relevant participant - let participant = event.participants[owner]; - // If filter says don't look in groups, skip it all - if (!participant && filter === 'no-enum-groups') { - return false; - } - // Couldn't find the current owner in the participant list, check groups & resources - if (!participant) { - let options = null; - if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - if ((isNaN(parseInt(owner)) || parseInt(owner) < 0) && options && typeof options.find == "function") { - let resource = options.find(function (element) { - return element.id == owner; - }) || {}; - let matching_participant = typeof resource.resources == "undefined" ? - resource : resource === null || resource === void 0 ? void 0 : resource.resources.filter(id => typeof event.participants[id] != "undefined"); - if (matching_participant.length > 0) { - return this._status_check(event, filter, matching_participant); - } - else if (filter == 'owner' && resource && resource.resources && resource.resources.indexOf(event.owner)) { - // owner param was a group but event is owned by someone in that group - return true; - } - } - } - let status = et2_calendar_event.split_status(participant); - switch (filter) { - default: - case 'all': - return true; - case 'default': // Show all status, but rejected - return status !== 'R'; - case 'accepted': //Show only accepted events - return status === 'A'; - case 'unknown': // Show only invitations, not yet accepted or rejected - return status === 'U'; - case 'tentative': // Show only tentative accepted events - return status === 'T'; - case 'delegated': // Show only delegated events - return status === 'D'; - case 'rejected': // Show only rejected events - return status === 'R'; - // Handled above - //case 'owner': // Show also events just owned by selected user - case 'hideprivate': // Show all events, as if they were private - // handled server-side - return true; - case 'showonlypublic': // Show only events flagged as public, -not checked as private - return event.public == '1'; - // Handled above - // case 'no-enum-groups': // Do not include events of group members - case 'not-unknown': // Show all status, but unknown - return status !== 'U'; - case 'deleted': // Show events that have been deleted - return event.deleted; - } - } - attachToDOM() { - let result = super.attachToDOM(); - // Remove the binding for the click handler, unless there's something - // custom here. - if (!this.onclick) { - jQuery(this.node).off("click"); - } - return result; - } - /** - * Click handler calling custom handler set via onclick attribute to this.onclick. - * All other handling is done by the timegrid widget. - * - * @param {Event} _ev - * @returns {boolean} - */ - click(_ev) { - let result = true; - if (typeof this.onclick == 'function') { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - const args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.splice(1, 0, this); - result = this.onclick.apply(this, args); - } - return result; - } - /** - * Show the recur prompt for this event - * - * Calls et2_calendar_event.recur_prompt with this event's value. - * - * @param {et2_calendar_event~prompt_callback} callback - * @param {Object} [extra_data] - */ - recur_prompt(callback, extra_data) { - et2_calendar_event.recur_prompt(this.options.value, callback, extra_data); - } - /** - * Show the series split prompt for this event - * - * Calls et2_calendar_event.series_split_prompt with this event's value. - * - * @param {et2_calendar_event~prompt_callback} callback - */ - series_split_prompt(callback) { - et2_calendar_event.series_split_prompt(this.options.value, this.options.value.recur_date, callback); - } - /** - * Copy the actions set on the parent, apply them to self - * - * This can take a while to do, so we try to do it only when needed - on mouseover - */ - _copy_parent_actions() { - // Copy actions set in parent - if (!this.options.readonly && !this.getParent().options.readonly) { - let action_parent = this; - while (action_parent != null && !action_parent.options.actions && - !(action_parent instanceof et2_container)) { - action_parent = action_parent.getParent(); - } - try { - this._link_actions(action_parent.options.actions || {}); - this._need_actions_linked = false; - } - catch (e) { - // something went wrong, but keep quiet about it - } - } - } - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions(actions) { - if (!this._actionObject) { - // Get the top level element - timegrid or so - var objectManager = this.getParent()._actionObject || this.getParent().getParent()._actionObject || - egw_getAppObjectManager(true).getObjectById(this.getParent().getParent().getParent().id) || egw_getAppObjectManager(true); - this._actionObject = objectManager.getObjectById('calendar::' + this.options.value.row_id); - } - if (this._actionObject == null) { - // Add a new container to the object manager which will hold the widget - // objects - this._actionObject = objectManager.insertObject(false, new egwActionObject('calendar::' + this.options.value.row_id, objectManager, et2_calendar_event.et2_event_action_object_impl(this, this.getDOMNode()), this._actionManager || objectManager.manager.getActionById('calendar::' + this.options.value.row_id) || objectManager.manager)); - } - else { - this._actionObject.setAOI(et2_calendar_event.et2_event_action_object_impl(this, this.getDOMNode(this))); - } - // Delete all old objects - this._actionObject.clear(); - this._actionObject.unregisterActions(); - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - const action_links = this._get_action_links(actions); - action_links.push('egw_link_drag'); - action_links.push('egw_link_drop'); - if (this._actionObject.parent.getActionLink('invite')) { - action_links.push('invite'); - } - this._actionObject.updateActionLinks(action_links); - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - } - getDetachedNodes() { - return [this.getDOMNode()]; - } - setDetachedAttributes(_nodes, _values) { - } - // Static class stuff - /** - * Check event owner against a parent object - * - * As an event is edited, its participants may change. Also, as the state - * changes we may change which events are displayed and show the same event - * in several places for different users. Here we check the event participants - * against an owner value (which may be an array) to see if the event should be - * displayed or included. - * - * @param {Object} event - Event information - * @param {et2_widget_daycol|et2_widget_planner_row} parent - potential parent object - * that has an owner option - * @param {boolean} [owner_too] - Include the event owner in consideration, or only - * event participants - * - * @return {boolean} Should the event be displayed - */ - static owner_check(event, parent, owner_too) { - var _a, _b; - let owner_match = true; - let state = ((_a = parent.getInstanceManager()) === null || _a === void 0 ? void 0 : _a.app_obj.calendar.state) || ((_b = app.calendar) === null || _b === void 0 ? void 0 : _b.state) || {}; - if (typeof owner_too === 'undefined' && state.status_filter) { - owner_too = state.status_filter === 'owner'; - } - let options = null; - if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - else { - options = parent.getArrayMgr("sel_options").getRoot().getEntry('owner'); - } - if (event.participants && typeof parent.options.owner != 'undefined' && parent.options.owner.length > 0) { - var parent_owner = jQuery.extend([], typeof parent.options.owner !== 'object' ? - [parent.options.owner] : - parent.options.owner); - owner_match = false; - const length = parent_owner.length; - for (var i = 0; i < length; i++) { - // Handle groups & grouped resources like mailing lists, they won't match so - // we need the list - pull it from sidebox owner - if ((isNaN(parent_owner[i]) || parent_owner[i] < 0) && options && typeof options.find == "function") { - var resource = options.find(function (element) { return element.id == parent_owner[i]; }) || {}; - if (resource && resource.resources) { - parent_owner.splice(i, 1); - i--; - parent_owner = parent_owner.concat(resource.resources); - } - } - } - let participants = jQuery.extend([], Object.keys(event.participants)); - for (var i = 0; i < participants.length; i++) { - const id = participants[i]; - // Expand group invitations - if (parseInt(id) < 0) { - // Add in groups, if we can get them from options, great - var resource; - if (options && options.find && (resource = options.find(function (element) { return element.id === id; })) && resource.resources) { - participants = participants.concat(resource.resources); - } - else { - // Add in groups, if we can get them (this is asynchronous) - egw.accountData(id, 'account_id', true, function (members) { - participants = participants.concat(Object.keys(members)); - }, this); - } - } - if (parent.options.owner == id || - parent_owner.indexOf && - parent_owner.indexOf(id) >= 0) { - owner_match = true; - break; - } - } - } - if (owner_too && !owner_match) { - owner_match = (parent.options.owner == event.owner || - parent_owner.indexOf && - parent_owner.indexOf(event.owner) >= 0); - } - return owner_match; - } - /** - * @callback et2_calendar_event~prompt_callback - * @param {string} button_id - One of ok, exception, series, single or cancel - * depending on which buttons are on the prompt - * @param {Object} event_data - Event information - whatever you passed in to - * the prompt. - */ - /** - * Recur prompt - * If the event is recurring, asks the user if they want to edit the event as - * an exception, or change the whole series. Then the callback is called. - * - * If callback is not provided, egw.open() will be used to open an edit dialog. - * - * If you call this on a single (non-recurring) event, the callback will be - * executed immediately, with the passed button_id as 'single'. - * - * @param {Object} event_data - Event information - * @param {string} event_data.id - Unique ID for the event, possibly with a - * timestamp - * @param {string|Date} event_data.start - Start date/time for the event - * @param {number} event_data.recur_type - Recur type, or 0 for a non-recurring event - * @param {et2_calendar_event~prompt_callback} [callback] - Callback is - * called with the button (exception, series, single or cancel) and the event - * data. - * @param {Object} [extra_data] - Additional data passed to the callback, used - * as extra parameters for default callback - * - * @augments {et2_calendar_event} - */ - static recur_prompt(event_data, callback, extra_data) { - let egw; - const edit_id = event_data.app_id; - const edit_date = event_data.start; - // seems window.opener somehow in certain conditions could be from different origin - // we try to catch the exception and in this case retrieve the egw object from current window. - try { - egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar'); - } - catch (e) { - egw = window.egw('calendar'); - } - const that = this; - const extra_params = extra_data && typeof extra_data == 'object' ? extra_data : {}; - extra_params.date = edit_date.toJSON ? edit_date.toJSON() : edit_date; - if (typeof callback != 'function') { - callback = function (_button_id) { - switch (_button_id) { - case 'exception': - extra_params.exception = '1'; - egw.open(edit_id, event_data.app || 'calendar', 'edit', extra_params); - break; - case 'series': - case 'single': - egw.open(edit_id, event_data.app || 'calendar', 'edit', extra_params); - break; - case 'cancel': - default: - break; - } - }; - } - if (parseInt(event_data.recur_type)) { - const buttons = [ - { text: egw.lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true }, - { text: egw.lang("Edit series"), id: "series" }, - { text: egw.lang("Cancel"), id: "cancel" } - ]; - et2_dialog.show_dialog(function (button_id) { callback.call(that, button_id, event_data); }, (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + - egw.lang("Do you want to edit this event as an exception or the whole series?"), egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE); - } - else { - callback.call(this, 'single', event_data); - } - } - /** - * Split series prompt - * - * If the event is recurring and the user adjusts the time or duration, we may need - * to split the series, ending the current one and creating a new one with the changes. - * This prompts the user if they really want to do that. - * - * There is no default callback, and nothing happens if you call this on a - * single (non-recurring) event - * - * @param {Object} event_data - Event information - * @param {string} event_data.id - Unique ID for the event, possibly with a timestamp - * @param {string|Date} instance_date - The date of the edited instance of the event - * @param {et2_calendar_event~prompt_callback} callback - Callback is - * called with the button (ok or cancel) and the event data. - * @augments {et2_calendar_event} - */ - static series_split_prompt(event_data, instance_date, callback) { - let egw; - // seems window.opener somehow in certian conditions could be from different origin - // we try to catch the exception and in this case retrieve the egw object from current window. - try { - egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar'); - } - catch (e) { - egw = window.egw('calendar'); - } - const that = this; - if (typeof instance_date == 'string') { - instance_date = new Date(instance_date); - } - // Check for modifying a series that started before today - const tempDate = new Date(); - const today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), tempDate.getHours(), -tempDate.getTimezoneOffset(), tempDate.getSeconds()); - const termination_date = instance_date < today ? egw.lang('today') : date(egw.preference('dateformat'), instance_date); - if (parseInt(event_data.recur_type)) { - et2_dialog.show_dialog(function (button_id) { callback.call(that, button_id, event_data); }, (!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" + - egw.lang("Do you really want to change the start of this series? If you do, the original series will be terminated as of %1 and a new series for the future reflecting your changes will be created.", termination_date), egw.lang("This event is part of a series"), {}, et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.WARNING_MESSAGE); - } - } - static drag_helper(event, ui) { - ui.helper.width(ui.width()); - } - /** - * splits the combined status, quantity and role - * - * @param {string} status - combined value, O: status letter: U, T, A, R - * @param {int} [quantity] - quantity - * @param {string} [role] - * @return string status U, T, A or R, same as $status parameter on return - */ - static split_status(status, quantity, role) { - quantity = 1; - role = 'REQ-PARTICIPANT'; - //error_log(__METHOD__.__LINE__.array2string($status)); - let matches = null; - if (typeof status === 'string' && status.length > 1) { - matches = status.match(/^.([0-9]*)(.*)$/gi); - } - if (matches) { - if (parseInt(matches[1]) > 0) - quantity = parseInt(matches[1]); - if (matches[2]) - role = matches[2]; - status = status[0]; - } - else if (status === true) { - status = 'U'; - } - return status; - } - /** - * The egw_action system requires an egwActionObjectInterface Interface implementation - * to tie actions to DOM nodes. I'm not sure if we need this. - * - * The class extension is different than the widgets - * - * @param {et2_DOMWidget} widget - * @param {Object} node - * - */ - static et2_event_action_object_impl(widget, node) { - const aoi = new et2_action_object_impl(widget, node).getAOI(); - // _outerCall may be used to determine, whether the state change has been - // evoked from the outside and the stateChangeCallback has to be called - // or not. - aoi.doSetState = function (_state, _outerCall) { - }; - return aoi; - } -} -et2_calendar_event._attributes = { - "value": { - type: "any", - default: et2_no_init - }, - "onclick": { - "description": "JS code which is executed when the element is clicked. " + - "If no handler is provided, or the handler returns true and the event is not read-only, the " + - "event will be opened according to calendar settings." - } -}; -et2_register_widget(et2_calendar_event, ["calendar-event"]); -//# sourceMappingURL=et2_widget_event.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_owner.js b/calendar/js/et2_widget_owner.js deleted file mode 100644 index c070f51007..0000000000 --- a/calendar/js/et2_widget_owner.js +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Egroupware - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - et2_widget_taglist; -*/ -import { et2_register_widget } from "../../api/js/etemplate/et2_core_widget"; -import { et2_taglist_email } from "../../api/js/etemplate/et2_widget_taglist"; -/** - * Tag list widget customised for calendar owner, which can be a user - * account or group, or an entry from almost any app, or an email address - * - * A cross between auto complete, selectbox and chosen multiselect - * - * Uses MagicSuggest library - * @see http://nicolasbize.github.io/magicsuggest/ - * @augments et2_selectbox - */ -export class et2_calendar_owner extends et2_taglist_email { - constructor() { - super(...arguments); - // Allows sub-widgets to override options to the library - this.lib_options = { - autoSelect: false, - groupBy: 'app', - minChars: 2, - selectFirst: true, - // This option will also expand when the selection is changed - // via code, which we do not want - //expandOnFocus: true - toggleOnClick: true - }; - } - doLoadingFinished() { - super.doLoadingFinished(); - var widget = this; - // onChange fired when losing focus, which is different from normal - this._oldValue = this.taglist.getValue(); - return true; - } - selectionRenderer(item) { - if (this && this.options && this.options.allowFreeEntries) { - return super.selectionRenderer(item); - } - else { - var label = jQuery('').text(item.label); - if (item.class) - label.addClass(item.class); - if (typeof item.title != 'undefined') - label.attr('title', item.title); - if (typeof item.data != 'undefined') - label.attr('data', item.data); - if (typeof item.icon != 'undefined') { - var wrapper = jQuery('
      ').addClass('et2_taglist_tags_icon_wrapper'); - jQuery('') - .addClass('et2_taglist_tags_icon') - .css({ "background-image": "url(" + (item.icon.match(/^(http|https|\/)/) ? item.icon : egw.image(item.icon, item.app)) + ")" }) - .appendTo(wrapper); - label.appendTo(wrapper); - return wrapper; - } - return label; - } - } - getValue() { - if (this.taglist == null) - return null; - return this.taglist.getValue(); - } - /** - * Override parent to handle our special additional data types (c#,r#,etc.) when they - * are not available client side. - * - * @param {string|string[]} _value array of selected owners, which can be a number, - * or a number prefixed with one character indicating the resource type. - */ - set_value(_value) { - super.set_value(_value); - // If parent didn't find a label, label will be the same as ID so we - // can find them that way - let missing_labels = []; - for (var i = 0; i < this.options.value.length; i++) { - var value = this.options.value[i]; - if (value.id == value.label) { - missing_labels.push(value.id); - } - } - if (Object.keys(missing_labels).length > 0) { - // Proper label was not found by parent - ask directly - egw.json('calendar_owner_etemplate_widget::ajax_owner', [missing_labels], function (data) { - for (let owner in data) { - if (!owner || typeof owner == "undefined") - continue; - let idx = this.options.value.find(element => element.id == owner); - if (idx) { - idx = jQuery.extend(idx, data[owner]); - } - // Put it in the list of options for next time - this.options.select_options.push(data[owner]); - } - this.set_value(this.options.value); - }, this, true, this).sendRequest(); - } - if (this.taglist) { - this.taglist.clear(true); - this.taglist.addToSelection(this.options.value, true); - } - } -} -et2_calendar_owner._attributes = { - "autocomplete_url": { - "default": "calendar_owner_etemplate_widget::ajax_owner" - }, - "autocomplete_params": { - "name": "Autocomplete parameters", - "type": "any", - "default": {}, - "description": "Extra parameters passed to autocomplete URL. It should be a stringified JSON object." - }, - allowFreeEntries: { - "default": false, - ignore: true - }, - select_options: { - "type": "any", - "name": "Select options", - // Set to empty object to use selectbox's option finding - "default": {}, - "description": "Internally used to hold the select options." - } -}; -et2_register_widget(et2_calendar_owner, ["calendar-owner"]); -//# sourceMappingURL=et2_widget_owner.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_planner.js b/calendar/js/et2_widget_planner.js deleted file mode 100644 index c5ebdab1d1..0000000000 --- a/calendar/js/et2_widget_planner.js +++ /dev/null @@ -1,2079 +0,0 @@ -/* - * Egroupware Calendar timegrid - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - /calendar/js/et2_widget_view.js; - /calendar/js/et2_widget_planner_row.js; - /calendar/js/et2_widget_event.js; -*/ -import { et2_register_widget } from "../../api/js/etemplate/et2_core_widget"; -import { ClassWithAttributes } from "../../api/js/etemplate/et2_core_inheritance"; -import { et2_calendar_view } from "./et2_widget_view"; -import { et2_action_object_impl } from "../../api/js/etemplate/et2_core_DOMWidget"; -import { et2_calendar_event } from "./et2_widget_event"; -import { et2_calendar_planner_row } from "./et2_widget_planner_row"; -import { egw } from "../../api/js/jsapi/egw_global"; -import { egw_getObjectManager, egwActionObject } from "../../api/js/egw_action/egw_action.js"; -import { EGW_AI_DRAG_OVER, EGW_AO_FLAG_IS_CONTAINER } from "../../api/js/egw_action/egw_action_constants.js"; -import { et2_compileLegacyJS } from "../../api/js/etemplate/et2_core_legacyJSFunctions"; -import { et2_no_init } from "../../api/js/etemplate/et2_core_common"; -import { CalendarApp } from "./app"; -import { sprintf } from "../../api/js/egw_action/egw_action_common.js"; -/** - * Class which implements the "calendar-planner" XET-Tag for displaying a longer - * ( > 10 days) span of time. Events can be grouped into rows by either user, - * category, or month. Their horizontal position and size in the row is determined - * by their start date and duration relative to the displayed date range. - * - * @augments et2_calendar_view - */ -export class et2_calendar_planner extends et2_calendar_view { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_planner._attributes, _child || {})); - /** - * These handle the differences between the different group types. - * They provide the different titles, labels and grouping - */ - this.groupers = { - // Group by user has one row for each user - user: { - // Title in top left corner - title: function () { - return this.egw().lang('User'); - }, - // Column headers - headers: function () { - var start = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()); - var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()); - var day_count = Math.round((end_date - start_date) / (1000 * 3600 * 24)) + 1; - if (day_count >= 6) { - this.headers.append(this._header_months(start, day_count)); - } - if (day_count < 120) { - var weeks = this._header_weeks(start, day_count); - this.headers.append(weeks); - this.grid.append(weeks); - } - if (day_count < 60) { - var days = this._header_days(start, day_count); - this.headers.append(days); - this.grid.append(days); - } - if (day_count <= 7) { - var hours = this._header_hours(start, day_count); - this.headers.append(hours); - this.grid.append(hours); - } - }, - // Labels for the rows - row_labels: function () { - var labels = []; - var already_added = []; - var options = false; - var resource = null; - if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - else { - options = this.getArrayMgr("sel_options").getRoot().getEntry('owner'); - } - for (var i = 0; i < this.options.owner.length; i++) { - var user = this.options.owner[i]; - // Handle grouped resources like mailing lists - pull it from sidebox owner - // and expand to their contents - if (options && options.find && - ((resource = options.find(function (element) { return element.id == user; }) || {}) || isNaN(user))) { - if (resource && resource.resources) { - for (var j = 0; j < resource.resources.length; j++) { - var id = resource.resources[j]; - if (already_added.indexOf('' + id) < 0) { - labels.push({ - id: id, - label: this._get_owner_name(id) || '', - data: { participants: id, owner: id } - }); - already_added.push('' + id); - } - } - } - else if (user < 0) { - // Group, but no users found. Need those. - egw.accountData(parseInt(user), 'account_fullname', true, function (result) { - this.invalidate(); - }, this); - } - else if (already_added.indexOf('' + user) < 0 && (isNaN(user) || parseInt(user) >= 0)) { - labels.push({ - id: user, - label: this._get_owner_name(user), - data: { participants: user, owner: user } - }); - already_added.push('' + user); - } - } - else if (user < 0) // groups - { - egw.accountData(parseInt(user), 'account_fullname', true, function (result) { - for (var id in result) { - if (already_added.indexOf('' + id) < 0) { - this.push({ id: id, label: result[id] || '', data: { participants: id, owner: id } }); - already_added.push('' + id); - } - } - }, labels); - } - else // users - { - if (already_added.indexOf(user) < 0) { - var label = this._get_owner_name(user) || ''; - labels.push({ id: user, label: label, data: { participants: user, owner: '' } }); - already_added.push('' + user); - } - } - } - return labels.sort(function (a, b) { - return a.label.localeCompare(b.label); - }); - }, - // Group the events into the rows - group: function (labels, rows, event) { - // convert filter to allowed status - var status_to_show = ['U', 'A', 'T', 'D', 'G']; - switch (this.options.filter) { - case 'unknown': - status_to_show = ['U', 'G']; - break; - case 'accepted': - status_to_show = ['A']; - break; - case 'tentative': - status_to_show = ['T']; - break; - case 'rejected': - status_to_show = ['R']; - break; - case 'delegated': - status_to_show = ['D']; - break; - case 'all': - status_to_show = ['U', 'A', 'T', 'D', 'G', 'R']; - break; - default: - status_to_show = ['U', 'A', 'T', 'D', 'G']; - break; - } - var participants = event.participants; - var add_row = function (user, participant) { - var label_index = false; - for (var i = 0; i < labels.length; i++) { - if (labels[i].id == user) { - label_index = i; - break; - } - } - if (participant && label_index !== false && status_to_show.indexOf(participant.substr(0, 1)) >= 0 || - !participant && label_index !== false || - this.options.filter === 'owner' && event.owner === user) { - if (typeof rows[label_index] === 'undefined') { - rows[label_index] = []; - } - rows[label_index].push(event); - } - }; - for (var user in participants) { - var participant = participants[user]; - if (parseInt(user) < 0) // groups - { - var planner = this; - egw.accountData(user, 'account_fullname', true, function (result) { - for (var id in result) { - if (!participants[id]) - add_row.call(planner, id, participant); - } - }, labels); - continue; - } - add_row.call(this, user, participant); - } - }, - // Draw a single row - draw_row: function (sort_key, label, events) { - var row = this._drawRow(sort_key, label, events, this.options.start_date, this.options.end_date); - if (this.options.hide_empty && !events.length) { - row.set_disabled(true); - } - // Highlight current user, sort_key is account_id - if (sort_key === egw.user('account_id')) { - row.set_class('current_user'); - } - // Set account_id so event.owner_check can use it - row.options.owner = sort_key; - // Since the daywise cache is by user, we can tap in here - var t = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - do { - var cache_id = CalendarApp._daywise_cache_id(t, sort_key); - egw.dataRegisterUID(cache_id, row._data_callback, row); - t.setUTCDate(t.getUTCDate() + 1); - } while (t < end); - return row; - } - }, - // Group by month has one row for each month - month: { - title: function () { return this.egw().lang('Month'); }, - headers: function () { - this.headers.append(this._header_day_of_month()); - }, - row_labels: function () { - var labels = []; - var d = new Date(this.options.start_date); - d = new Date(d.valueOf() + d.getTimezoneOffset() * 60 * 1000); - for (var i = 0; i < 12; i++) { - // Not using UTC because we corrected for timezone offset - labels.push({ id: sprintf('%04d-%02d', d.getFullYear(), d.getMonth()), label: this.egw().lang(date('F', d)) + ' ' + d.getFullYear() }); - d.setMonth(d.getMonth() + 1); - } - return labels; - }, - group: function (labels, rows, event) { - // Yearly planner does not show infologs - if (event && event.app && event.app == 'infolog') - return; - var start = new Date(event.start); - start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - var key = sprintf('%04d-%02d', start.getFullYear(), start.getMonth()); - var label_index = false; - for (var i = 0; i < labels.length; i++) { - if (labels[i].id == key) { - label_index = i; - break; - } - } - if (label_index) { - if (typeof rows[label_index] === 'undefined') { - rows[label_index] = []; - } - rows[label_index].push(event); - } - // end in a different month? - var end = new Date(event.end); - end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000); - var end_key = sprintf('%04d-%02d', end.getFullYear(), end.getMonth()); - var year = start.getFullYear(); - var month = start.getMonth(); - key = sprintf('%04d-%02d', year, month); - do { - var end_label_index = typeof label_index == "boolean" ? 0 : label_index; - for (let i = end_label_index; i < labels.length; i++) { - if (labels[i].id == key) { - end_label_index = i; - if (typeof rows[end_label_index] === 'undefined') { - rows[end_label_index] = []; - } - break; - } - } - if (end_label_index != label_index) { - rows[end_label_index].push(event); - } - if (++month > 11) { - ++year; - month = 0; - } - key = sprintf('%04d-%02d', year, month); - } while (key <= end_key); - }, - // Draw a single row, but split up the dates - draw_row: function (sort_key, label, events) { - var key = sort_key.split('-'); - var start = new Date(key[0] + "-" + sprintf("%02d", parseInt(key[1]) + 1) + "-01T00:00:00Z"); - // Use some care to avoid issues with timezones and daylight savings - var end = new Date(start); - end.setUTCMonth(start.getUTCMonth() + 1); - end.setUTCDate(1); - end.setUTCHours(0); - end.setUTCMinutes(0); - end = new Date(end.valueOf() - 1000); - end.setUTCMonth(start.getUTCMonth()); - this._drawRow(sort_key, label, events, start, end); - } - }, - // Group by category has one row for each [sub]category - category: { - title: function () { return this.egw().lang('Category'); }, - headers: function () { - var start = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()); - var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()); - var day_count = Math.round((end_date - start_date) / (1000 * 3600 * 24)) + 1; - if (day_count >= 6) { - this.headers.append(this._header_months(start, day_count)); - } - if (day_count < 120) { - var weeks = this._header_weeks(start, day_count); - this.headers.append(weeks); - this.grid.append(weeks); - } - if (day_count < 60) { - var days = this._header_days(start, day_count); - this.headers.append(days); - this.grid.append(days); - } - if (day_count <= 7) { - var hours = this._header_hours(start, day_count); - this.headers.append(hours); - this.grid.append(hours); - } - }, - row_labels: function () { - var im = this.getInstanceManager(); - var categories = et2_selectbox.cat_options({ - _type: 'select-cat', - getInstanceManager: function () { return im; } - }, { application: 'calendar' }); - var labels = []; - let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar; - if (!app_calendar.state.cat_id || - app_calendar.state.cat_id.toString() === '' || - app_calendar.state.cat_id.toString() == '0') { - app_calendar.state.cat_id = ''; - labels.push({ id: '', value: '', label: egw.lang('none'), main: '', data: {} }); - labels = labels.concat(categories); - } - else { - var cat_id = app_calendar.state.cat_id; - if (typeof cat_id == 'string') { - cat_id = cat_id.split(','); - } - for (var i = 0; i < cat_id.length; i++) { - // Find label for that category - for (var j = 0; j < categories.length; j++) { - if (categories[j].value == cat_id[i]) { - categories[j].id = categories[j].value; - labels.push(categories[j]); - break; - } - } - // Get its children immediately - egw.json('EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options', ['select-cat', ',,,calendar,' + cat_id[i]], function (data) { - labels = labels.concat(data); - }).sendRequest(false); - } - } - for (var i = labels.length - 1; i >= 0; i--) { - labels[i].id = labels[i].value; - labels[i].data = { - cat_id: labels[i].id, - main: labels[i].value == labels[i].main - }; - if (labels[i].children && labels[i].children.length) { - labels[i].data.has_children = true; - } - } - return labels; - }, - group: function (labels, rows, event) { - var cats = event.category; - let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar; - if (typeof event.category === 'string') { - cats = cats.split(','); - } - for (var cat = 0; cat < cats.length; cat++) { - var label_index = false; - var category = cats[cat] ? parseInt(cats[cat], 10) : false; - if (category == 0 || !category) - category = ''; - for (var i = 0; i < labels.length; i++) { - if (labels[i].id == category) { - // If there's no cat filter, only show the top level - if (!app_calendar.state.cat_id) { - for (var j = 0; j < labels.length; j++) { - if (labels[j].id == labels[i].main) { - label_index = j; - break; - } - } - break; - } - label_index = i; - break; - } - } - if (label_index !== false && typeof rows[label_index] === 'undefined') { - rows[label_index] = []; - } - if (label_index !== false && rows[label_index].indexOf(event) === -1) { - rows[label_index].push(event); - } - } - }, - draw_row: function (sort_key, label, events) { - var row = this._drawRow(sort_key, label, events, this.options.start_date, this.options.end_date); - if (this.options.hide_empty && !events.length) { - row.set_disabled(true); - } - return row; - } - } - }; - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_plannerWidget"); - // Header - this.gridHeader = jQuery(document.createElement("div")) - .addClass("calendar_plannerHeader") - .appendTo(this.div); - this.headerTitle = jQuery(document.createElement("div")) - .addClass("calendar_plannerHeaderTitle") - .appendTo(this.gridHeader); - this.headers = jQuery(document.createElement("div")) - .addClass("calendar_plannerHeaderRows") - .appendTo(this.gridHeader); - this.rows = jQuery(document.createElement("div")) - .addClass("calendar_plannerRows") - .appendTo(this.div); - this.grid = jQuery(document.createElement("div")) - .addClass("calendar_plannerGrid") - .appendTo(this.div); - this.vertical_bar = jQuery(document.createElement("div")) - .addClass('verticalBar') - .appendTo(this.div); - this.value = []; - // Update timer, to avoid redrawing twice when changing start & end date - this.update_timer = null; - this.doInvalidate = true; - this.setDOMNode(this.div[0]); - this.registeredCallbacks = []; - this.cache = {}; - this._deferred_row_updates = {}; - } - destroy() { - super.destroy(); - this.div.off(); - for (var i = 0; i < this.registeredCallbacks.length; i++) { - egw.dataUnregisterUID(this.registeredCallbacks[i], null, this); - } - } - doLoadingFinished() { - super.doLoadingFinished(); - // Don't bother to draw anything if there's no date yet - if (this.options.start_date) { - this._drawGrid(); - } - // Automatically bind drag and resize for every event using jQuery directly - // - no action system - - var planner = this; - this.cache = {}; - this._deferred_row_updates = {}; - /** - * If user puts the mouse over an event, then we'll set up resizing so - * they can adjust the length. Should be a little better on resources - * than binding it for every calendar event. - */ - this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function () { - // Load the event - planner._get_event_info(this); - var that = this; - //Resizable event handler - jQuery(this).resizable({ - distance: 10, - grid: [5, 10000], - autoHide: false, - handles: 'e', - containment: 'parent', - /** - * Triggered when the resizable is created. - * - * @param {event} event - * @param {Object} ui - */ - create: function (event, ui) { - var resizeHelper = event.target.getAttribute('data-resize'); - if (resizeHelper == 'WD' || resizeHelper == 'WDS') { - jQuery(this).resizable('destroy'); - } - }, - /** - * If dragging to resize an event, abort drag to create - * - * @param {jQuery.Event} event - * @param {Object} ui - */ - start: function (event, ui) { - if (planner.drag_create.start) { - // Abort drag to create, we're dragging to resize - planner._drag_create_end({}); - } - }, - /** - * Triggered at the end of resizing the calEvent. - * - * @param {event} event - * @param {Object} ui - */ - stop: function (event, ui) { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = { duration: 0 }; - var event_data = planner._get_event_info(this); - var event_widget = planner.getWidgetById(event_data.widget_id); - var sT = event_widget.options.value.start_m; - if (typeof this.dropEnd != 'undefined') { - var eT = parseInt(this.dropEnd.getUTCHours() * 60) + parseInt(this.dropEnd.getUTCMinutes()); - e.data.duration = ((eT - sT) / 60) * 3600; - if (event_widget) { - event_widget.options.value.end_m = eT; - event_widget.options.value.duration = e.data.duration; - } - // Leave the helper there until the update is done - var loading = ui.helper.clone().appendTo(ui.helper.parent()); - // and add a loading icon so user knows something is happening - jQuery('.calendar_timeDemo', loading).after('
      '); - jQuery(this).trigger(e); - // That cleared the resize handles, so remove for re-creation... - jQuery(this).resizable('destroy'); - // Remove loading, done or not - loading.remove(); - } - // Clear the helper, re-draw - if (event_widget) { - event_widget.getParent().position_event(event_widget); - } - }, - /** - * Triggered during the resize, on the drag of the resize handler - * - * @param {event} event - * @param {Object} ui - */ - resize: function (event, ui) { - let position; - if (planner.options.group_by == 'month') { - position = { left: event.clientX, top: event.clientY }; - } - else { - position = { top: ui.position.top, left: ui.position.left + ui.helper.width() }; - } - planner._drag_helper(this, position, ui.helper.outerHeight()); - } - }); - }) - .on('mousemove', function (event) { - // Ignore headers - if (planner.headers.has(event.target).length !== 0) { - planner.vertical_bar.hide(); - return; - } - // Position bar by mouse - planner.vertical_bar.position({ - my: 'right-1', - of: event, - collision: 'fit' - }); - planner.vertical_bar.css('top', '0px'); - // Get time at mouse - if (jQuery(event.target).closest('.calendar_eventRows').length == 0) { - // "Invalid" times, from space after the last planner row, or header - var time = planner._get_time_from_position(event.pageX - planner.grid.offset().left, 10); - } - else if (planner.options.group_by == 'month') { - var time = planner._get_time_from_position(event.clientX, event.clientY); - } - else { - var time = planner._get_time_from_position(event.offsetX, event.offsetY); - } - // Passing to formatter, cancel out timezone - if (time) { - const formatDate = new Date(time.valueOf() + time.getTimezoneOffset() * 60 * 1000); - planner.vertical_bar - .html('' + date(egw.preference('timeformat', 'calendar') == 12 ? 'h:ia' : 'H:i', formatDate) + '') - .show(); - if (planner.drag_create.event && planner.drag_create.parent && planner.drag_create.end) { - planner.drag_create.end.date = time.toJSON(); - planner._drag_update_event(); - } - } - else { - // No (valid) time, just hide - planner.vertical_bar.hide(); - } - }) - .on('mousedown', jQuery.proxy(this._mouse_down, this)) - .on('mouseup', jQuery.proxy(this._mouse_up, this)); - // Actions may be set on a parent, so we need to explicitly get in here - // and get ours - this._link_actions(this.options.actions || this.getParent().options.actions || []); - // Customize and override some draggable settings - this.div.on('dragcreate', '.calendar_calEvent', function (event, ui) { - jQuery(this).draggable('option', 'cancel', '.rowNoEdit'); - // Act like you clicked the header, makes it easier to position - jQuery(this).draggable('option', 'cursorAt', { top: 5, left: 5 }); - }) - .on('dragstart', '.calendar_calEvent', function (event, ui) { - jQuery('.calendar_calEvent', ui.helper).width(jQuery(this).width()) - .height(jQuery(this).outerHeight()) - .css('top', '').css('left', '') - .appendTo(ui.helper); - ui.helper.width(jQuery(this).width()); - // Cancel drag to create, we're dragging an existing event - planner._drag_create_end(); - }); - return true; - } - _createNamespace() { - return true; - } - /** - * Something changed, and the planner needs to be re-drawn. We wait a bit to - * avoid re-drawing twice if start and end date both changed, then recreate. - * - * @param {boolean} trigger =false Trigger an event once things are done. - * Waiting until invalidate completes prevents 2 updates when changing the date range. - * @returns {undefined} - */ - invalidate(trigger) { - // Busy - if (!this.doInvalidate) - return; - // Not yet ready - if (!this.options.start_date || !this.options.end_date) - return; - // Wait a bit to see if anything else changes, then re-draw the days - if (this.update_timer !== null) { - window.clearTimeout(this.update_timer); - } - this.update_timer = window.setTimeout(jQuery.proxy(function () { - this.widget.doInvalidate = false; - // Show AJAX loader - this.widget.loader.show(); - this.widget.cache = {}; - this._deferred_row_updates = {}; - this.widget._fetch_data(); - this.widget._drawGrid(); - if (this.trigger) { - this.widget.change(); - } - this.widget.update_timer = null; - this.widget.doInvalidate = true; - this.widget._updateNow(); - window.setTimeout(jQuery.proxy(function () { if (this.loader) - this.loader.hide(); }, this.widget), 500); - }, { widget: this, "trigger": trigger }), et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); - } - detachFromDOM() { - // Remove the binding to the change handler - jQuery(this.div).off("change.et2_calendar_timegrid"); - return super.detachFromDOM(); - } - attachToDOM() { - let result = super.attachToDOM(); - // Add the binding for the event change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function (e) { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.push(this); - return e.data.event_change.apply(e.data, args); - }); - // Add the binding for the change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function (e) { - return e.data.change.call(e.data, e, this); - }); - return result; - } - getDOMNode(_sender) { - if (_sender === this || !_sender) { - return this.div[0]; - } - if (_sender._parent === this) { - return this.rows[0]; - } - } - /** - * Creates all the DOM nodes for the planner grid - * - * Any existing nodes (& children) are removed, the headers & labels are - * determined according to the current group_by value, and then the rows - * are created. - * - * @method - * @private - * - */ - _drawGrid() { - this.div.css('height', this.options.height); - // Clear old events - var delete_index = this._children.length - 1; - while (this._children.length > 0 && delete_index >= 0) { - this._children[delete_index].destroy(); - this.removeChild(this._children[delete_index--]); - } - // Clear old rows - this.rows.empty() - .append(this.grid); - this.grid.empty(); - var grouper = this.grouper; - if (!grouper) - return; - // Headers - this.headers.empty(); - this.headerTitle.text(grouper.title.apply(this)); - grouper.headers.apply(this); - this.grid.find('*').contents().filter(function () { - return this.nodeType === 3; - }).remove(); - // Get the rows / labels - var labels = grouper.row_labels.call(this); - // Group the events - var events = {}; - for (var i = 0; i < this.value.length; i++) { - grouper.group.call(this, labels, events, this.value[i]); - } - // Set height for rows - this.rows.height(this.div.height() - this.headers.outerHeight()); - // Draw the rows - let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar; - for (var key in labels) { - if (!labels.hasOwnProperty(key)) - continue; - // Skip sub-categories (events are merged into top level) - if (this.options.group_by == 'category' && - (!app_calendar.state.cat_id || app_calendar.state.cat_id == '') && - labels[key].id != labels[key].main) { - continue; - } - var row = grouper.draw_row.call(this, labels[key].id, labels[key].label, events[key] || []); - // Add extra data for clicking on row - if (row) { - for (var extra in labels[key].data) { - row.getDOMNode().dataset[extra] = labels[key].data[extra]; - } - } - } - // Adjust header if there's a scrollbar - if (this.rows.children().last().length) { - this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px'); - } - // Add actual events - for (var key in this._deferred_row_updates) { - window.clearTimeout(key); - } - window.setTimeout(jQuery.proxy(function () { - this._deferred_row_update(); - }, this), et2_calendar_planner.DEFERRED_ROW_TIME); - this.value = []; - } - /** - * Draw a single row of the planner - * - * @param {string} key Index into the grouped labels & events - * @param {string} label - * @param {Array} events - * @param {Date} start - * @param {Date} end - */ - _drawRow(key, label, events, start, end) { - let row = et2_createWidget('calendar-planner_row', { - id: 'planner_row_' + key, - label: label, - start_date: start, - end_date: end, - value: events, - readonly: this.options.readonly - }, this); - if (this.isInTree()) { - row.doLoadingFinished(); - } - return row; - } - _header_day_of_month() { - let day_width = 3.23; // 100.0 / 31; - // month scale with navigation - var content = '
      '; - var start = new Date(this.options.start_date); - start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - var end = new Date(this.options.end_date); - end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000); - var title = this.egw().lang(date('F', start)) + ' ' + date('Y', start) + ' - ' + - this.egw().lang(date('F', end)) + ' ' + date('Y', end); - content += '"; - content += "
      "; // end of plannerScale - // day of month scale - content += '
      '; - for (var left = 0, i = 0; i < 31; left += day_width, ++i) { - content += '
      ' + - (1 + i) + "
      \n"; - } - content += "
      \n"; - return content; - } - /** - * Update the 'now' line - * @private - */ - _updateNow() { - let now = super._updateNow(); - if (now === false || this.grouper == this.groupers.month) { - this.now_div.hide(); - return false; - } - let row = null; - for (let i = 0; i < this._children.length && row == null; i++) { - if (this._children[i].instanceOf(et2_calendar_planner_row)) { - row = this._children[i]; - } - } - if (!row) { - this.now_div.hide(); - return false; - } - this.now_div.appendTo(this.grid) - .show() - .css('left', row._time_to_position(now) + '%'); - } - /** - * Make a header showing the months - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet - */ - _header_months(start, days) { - var content = '
      '; - var days_in_month = 0; - var day_width = 100 / days; - var end = new Date(start); - end.setUTCDate(end.getUTCDate() + days); - var t = new Date(start.valueOf()); - for (var left = 0, i = 0; i < days; t.setUTCDate(1), t.setUTCMonth(t.getUTCMonth() + 1), left += days_in_month * day_width, i += days_in_month) { - var u = new Date(t.getUTCFullYear(), t.getUTCMonth() + 1, 0, -t.getTimezoneOffset() / 60); - days_in_month = 1 + ((u - t) / (24 * 3600 * 1000)); - var first = new Date(t.getUTCFullYear(), t.getUTCMonth(), 1, -t.getTimezoneOffset() / 60); - if (days_in_month <= 0) - break; - if (i + days_in_month > days) { - days_in_month = days - i; - } - var title = this.egw().lang(date('F', new Date(t.valueOf() + t.getTimezoneOffset() * 60 * 1000))); - if (days_in_month > 10) { - title += ' ' + t.getUTCFullYear(); - } - else if (days_in_month < 5) { - title = ' '; - } - content += '
      ' + - title + "
      "; - } - content += "
      "; // end of plannerScale - return content; - } - /** - * Make a header showing the week numbers - * - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet - */ - _header_weeks(start, days) { - var content = '
      '; - var state = ''; - // we're not using UTC so date() formatting function works - var t = new Date(start.valueOf()); - // Make sure we're lining up on the week - let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar; - var week_end = app_calendar.date.end_of_week(start); - var days_in_week = Math.floor(((week_end - start) / (24 * 3600 * 1000)) + 1); - var week_width = 100 / days * (days <= 7 ? days : days_in_week); - for (var left = 0, i = 0; i < days; t.setUTCDate(t.getUTCDate() + 7), left += week_width) { - // Avoid overflow at the end - if (days - i < 7) { - days_in_week = days - i; - } - var usertime = new Date(t.valueOf()); - if (start.getTimezoneOffset() < 0) { - // Gets the right week # east of GMT. West does not need it(?) - usertime.setUTCMinutes(usertime.getUTCMinutes() - start.getTimezoneOffset()); - } - week_width = 100 / days * Math.min(days, days_in_week); - var title = this.egw().lang('Week') + ' ' + app_calendar.date.week_number(usertime); - if (start.getTimezoneOffset() > 0) { - // Gets the right week start west of GMT - usertime.setUTCMinutes(usertime.getUTCMinutes() + start.getTimezoneOffset()); - } - state = app_calendar.date.start_of_week(usertime); - state.setUTCHours(0); - state.setUTCMinutes(0); - state = state.toJSON(); - if (days_in_week > 1 || days == 1) { - content += '"; - } - i += days_in_week; - if (days_in_week != 7) { - t.setUTCDate(t.getUTCDate() - (7 - days_in_week)); - days_in_week = 7; - } - } - content += "
      "; // end of plannerScale - return content; - } - /** - * Make a header for some days - * - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet - */ - _header_days(start, days) { - var day_width = 100 / days; - var content = '
      '; - // we're not using UTC so date() formatting function works - var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - for (var left = 0, i = 0; i < days; t.setDate(t.getDate() + 1), left += day_width, ++i) { - if (!this.options.show_weekend && [0, 6].indexOf(t.getDay()) !== -1) - continue; - var holidays = []; - var tempDate = new Date(t); - tempDate.setMinutes(tempDate.getMinutes() - tempDate.getTimezoneOffset()); - var title = ''; - let state = new Date(t.valueOf() - t.getTimezoneOffset() * 60 * 1000); - var day_class = this.day_class_holiday(state, holidays, days); - if (days <= 3) { - title = this.egw().lang(date('l', t)) + ', ' + date('j', t) + '. ' + this.egw().lang(date('F', t)); - } - else if (days <= 7) { - title = this.egw().lang(date('l', t)) + ' ' + date('j', t); - } - else { - title = this.egw().lang(date('D', t)).substr(0, 2) + '
      ' + date('j', t); - } - content += '\n"; - } - content += "
      "; // end of plannerScale - return content; - } - /** - * Create a header with hours - * - * @param {Date} start - * @param {number} days - * @returns {string} HTML snippet for the header - */ - _header_hours(start, days) { - var divisors = [1, 2, 3, 4, 6, 8, 12]; - var decr = 1; - for (var i = 0; i < divisors.length; i++) // numbers dividing 24 without rest - { - if (divisors[i] > days) - break; - decr = divisors[i]; - } - var hours = days * 24; - if (days === 1) // for a single day we calculate the hours of a days, to take into account daylight saving changes (23 or 25 hours) - { - var t = new Date(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), -start.getTimezoneOffset() / 60); - var s = new Date(start); - s.setUTCHours(23); - s.setUTCMinutes(59); - s.setUTCSeconds(59); - hours = Math.ceil((s.getTime() - t.getTime()) / 3600000); - } - var cell_width = 100 / hours * decr; - var content = '
      '; - // we're not using UTC so date() formatting function works - var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000); - for (var left = 0, i = 0; i < hours; left += cell_width, i += decr) { - if (!this.options.show_weekend && [0, 6].indexOf(t.getDay()) !== -1) - continue; - var title = date(egw.preference('timeformat', 'calendar') == 12 ? 'ha' : 'H', t); - content += '"; - t.setHours(t.getHours() + decr); - } - content += "
      "; // end of plannerScale - return content; - } - /** - * Applies class for today, and any holidays for current day - * - * @param {Date} date - * @param {string[]} holiday_list Filled with a list of holidays for that day - * @param {integer} days Number of days shown in the day header - * - * @return {string} CSS Classes for the day. calendar_calBirthday, calendar_calHoliday, calendar_calToday and calendar_weekend as appropriate - */ - day_class_holiday(date, holiday_list, days) { - if (!date) - return ''; - var day_class = ''; - // Holidays and birthdays - var holidays = et2_calendar_view.get_holidays(this, date.getUTCFullYear()); - // Pass a string rather than the date object, to make sure it doesn't get changed - this.date_helper.set_value(date.toJSON()); - var date_key = '' + this.date_helper.get_year() + sprintf('%02d', this.date_helper.get_month()) + sprintf('%02d', this.date_helper.get_date()); - if (holidays && holidays[date_key]) { - holidays = holidays[date_key]; - for (var i = 0; i < holidays.length; i++) { - if (typeof holidays[i]['birthyear'] !== 'undefined') { - day_class += ' calendar_calBirthday '; - if (typeof days == 'undefined' || days <= 21) { - day_class += ' calendar_calBirthdayIcon '; - } - holiday_list.push(holidays[i]['name']); - } - else { - day_class += 'calendar_calHoliday '; - holiday_list.push(holidays[i]['name']); - } - } - } - holidays = holiday_list.join(','); - var today = new Date(); - if (date_key === '' + today.getFullYear() + - sprintf("%02d", today.getMonth() + 1) + - sprintf("%02d", today.getDate())) { - day_class += "calendar_calToday "; - } - if (date.getUTCDay() == 0 || date.getUTCDay() == 6) { - day_class += "calendar_weekend "; - } - return day_class; - } - /** - * Link the actions to the DOM nodes / widget bits. - * - * @todo This currently does nothing - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions(actions) { - if (!this._actionObject) { - // Get the parent? Might be a grid row, might not. Either way, it is - // just a container with no valid actions - var objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; - var parent = objectManager.getObjectById(this.id, 3) || objectManager.getObjectById(this._parent.id, 3) || objectManager; - if (!parent) { - debugger; - egw.debug('error', 'No parent objectManager found'); - return; - } - for (var i = 0; i < parent.children.length; i++) { - var parent_finder = jQuery('#' + this.div.id, parent.children[i].iface.doGetDOMNode()); - if (parent_finder.length > 0) { - parent = parent.children[i]; - break; - } - } - } - // This binds into the egw action system. Most user interactions (drag to move, resize) - // are handled internally using jQuery directly. - var widget_object = this._actionObject || parent.getObjectById(this.id); - var aoi = new et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); - /** - * Determine if we allow a dropped event to use the invite/change actions, - * and enable or disable them appropriately - * - * @param {egwAction} action - * @param {et2_calendar_event} event The event widget being dragged - * @param {egwActionObject} target Planner action object - */ - var _invite_enabled = function (action, event, target) { - var event = event.iface.getWidget(); - var planner = target.iface.getWidget() || false; - //debugger; - if (event === planner || !event || !planner || - !event.options || !event.options.value.participants || !planner.options.owner) { - return false; - } - var owner_match = false; - var own_row = false; - for (var id in event.options.value.participants) { - planner.iterateOver(function (row) { - // Check scroll section or header section - if (row.div.hasClass('drop-hover') || row.div.has(':hover')) { - owner_match = owner_match || row.node.dataset[planner.options.group_by] === '' + id; - own_row = (row === event.getParent()); - } - }, this, et2_calendar_planner_row); - } - var enabled = !owner_match && - // Not inside its own row - !own_row; - widget_object.getActionLink('invite').enabled = enabled; - widget_object.getActionLink('change_participant').enabled = enabled; - // If invite or change participant are enabled, drag is not - widget_object.getActionLink('egw_link_drop').enabled = !enabled; - }; - aoi.doTriggerEvent = function (_event, _data) { - // Determine target node - var event = _data.event || false; - if (!event) - return; - if (_data.ui.draggable.hasClass('rowNoEdit')) - return; - /* - We have to handle the drop in the normal event stream instead of waiting - for the egwAction system so we can get the helper, and destination - */ - if (event.type === 'drop') { - this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], this.getWidget(), event, _data.ui); - } - var drag_listener = function (event, ui) { - aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', ui.helper)[0], { - top: ui.position.top, - left: ui.position.left - jQuery(this).parent().offset().left - }, 0); - }; - var time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper); - switch (_event) { - // Triggered once, when something is dragged into the timegrid's div - case EGW_AI_DRAG_OVER: - // Listen to the drag and update the helper with the time - // This part lets us drag between different timegrids - _data.ui.draggable.on('drag.et2_timegrid' + widget_object.id, drag_listener); - _data.ui.draggable.on('dragend.et2_timegrid' + widget_object.id, function () { - _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); - }); - if (time.length) { - // The out will trigger after the over, so we count - time.data('count', time.data('count') + 1); - } - else { - _data.ui.helper.prepend('
      '); - } - break; - // Triggered once, when something is dragged out of the timegrid - case EGW_AI_DRAG_OUT: - // Stop listening - _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); - // Remove any highlighted time squares - jQuery('[data-date]', this.doGetDOMNode()).removeClass("ui-state-active"); - // Out triggers after the over, count to not accidentally remove - time.data('count', time.data('count') - 1); - if (time.length && time.data('count') <= 0) { - time.remove(); - } - break; - } - }; - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = parent.insertObject(false, new egwActionObject(this.id, parent, aoi, this._actionManager || parent.manager.getActionById(this.id) || parent.manager), EGW_AO_FLAG_IS_CONTAINER); - } - else { - widget_object.setAOI(aoi); - } - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - this._init_links_dnd(widget_object.manager, action_links); - widget_object.updateActionLinks(action_links); - this._actionObject = widget_object; - } - /** - * Automatically add dnd support for linking - * - * @param {type} mgr - * @param {type} actionLinks - */ - _init_links_dnd(mgr, actionLinks) { - if (this.options.readonly) - return; - var self = this; - var drop_action = mgr.getActionById('egw_link_drop'); - var drop_change_participant = mgr.getActionById('change_participant'); - var drop_invite = mgr.getActionById('invite'); - var drag_action = mgr.getActionById('egw_link_drag'); - var paste_action = mgr.getActionById('egw_paste'); - // Disable paste action - if (paste_action == null) { - paste_action = mgr.addAction('popup', 'egw_paste', egw.lang('Paste'), egw.image('editpaste'), function () { }, true); - } - paste_action.set_enabled(false); - // Check if this app supports linking - if (!egw.link_get_registry(this.dataStorePrefix || 'calendar', 'query') || - egw.link_get_registry(this.dataStorePrefix || 'calendar', 'title')) { - if (drop_action) { - drop_action.remove(); - if (actionLinks.indexOf(drop_action.id) >= 0) { - actionLinks.splice(actionLinks.indexOf(drop_action.id), 1); - } - } - if (drag_action) { - drag_action.remove(); - if (actionLinks.indexOf(drag_action.id) >= 0) { - actionLinks.splice(actionLinks.indexOf(drag_action.id), 1); - } - } - return; - } - // Don't re-add - if (drop_action == null) { - // Create the drop action that links entries - drop_action = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function (action, source, dropped) { - // Extract link IDs - var links = []; - var id = ''; - for (var i = 0; i < source.length; i++) { - if (!source[i].id) - continue; - id = source[i].id.split('::'); - links.push({ app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1] }); - } - if (!links.length) { - return; - } - if (links.length && dropped && dropped.iface.getWidget() && dropped.iface.getWidget().instanceOf(et2_calendar_event)) { - // Link the entries - egw.json(self.egw().getAppName() + ".etemplate_widget_link.ajax_link.etemplate", dropped.id.split('::').concat([links]), function (result) { - if (result) { - this.egw().message('Linked'); - } - }, self, true, self).sendRequest(); - } - }, true); - drop_action.acceptedTypes = ['default', 'link']; - drop_action.hideOnDisabled = true; - // Create the drop action for moving events between planner rows - var invite_action = function (action, source, target) { - // Extract link IDs - var links = []; - var id = ''; - for (var i = 0; i < source.length; i++) { - // Check for no ID (invalid) or same manager (dragging an event) - if (!source[i].id) - continue; - if (source[i].manager === target.manager) { - // Find the row, could have dropped on an event - var row = target.iface.getWidget(); - while (target.parent && row.instanceOf && !row.instanceOf(et2_calendar_planner_row)) { - target = target.parent; - row = target.iface.getWidget(); - } - // Leave the helper there until the update is done - var loading = action.ui.helper.clone(true).appendTo(jQuery('body')); - // and add a loading icon so user knows something is happening - if (jQuery('.calendar_timeDemo', loading).length == 0) { - jQuery('.calendar_calEventHeader', loading).addClass('loading'); - } - else { - jQuery('.calendar_timeDemo', loading).after('
      '); - } - var event_data = egw.dataGetUIDdata(source[i].id).data; - et2_calendar_event.recur_prompt(event_data, function (button_id) { - if (button_id === 'cancel' || !button_id) { - return; - } - var add_owner = [row.node.dataset.participants]; - egw().json('calendar.calendar_uiforms.ajax_invite', [ - button_id === 'series' ? event_data.id : event_data.app_id, - add_owner, - action.id === 'change_participant' ? - [source[i].iface.getWidget().getParent().node.dataset.participants] : - [] - ], function () { loading.remove(); }).sendRequest(true); - }); - // Ok, stop. - return false; - } - } - }; - drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action, true); - drop_change_participant.acceptedTypes = ['calendar']; - drop_change_participant.hideOnDisabled = true; - drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action, true); - drop_invite.acceptedTypes = ['calendar']; - drop_invite.hideOnDisabled = true; - } - if (actionLinks.indexOf(drop_action.id) < 0) { - actionLinks.push(drop_action.id); - } - actionLinks.push(drop_invite.id); - actionLinks.push(drop_change_participant.id); - // Accept other links, and files dragged from the filemanager - // This does not handle files dragged from the desktop. They are - // handled by et2_nextmatch, since it needs DOM stuff - if (drop_action.acceptedTypes.indexOf('link') == -1) { - drop_action.acceptedTypes.push('link'); - } - // Don't re-add - if (drag_action == null) { - // Create drag action that allows linking - drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function (action, selected) { - // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links - // TODO: Need to decide if we need to create a customized helper interface for links anyway - //return helper; - return null; - }, true); - } - // The planner itself is not draggable, the action is there for the children - if (false && actionLinks.indexOf(drag_action.id) < 0) { - actionLinks.push(drag_action.id); - } - drag_action.set_dragType(['link', 'calendar']); - } - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * Here we are only interested in drop events. - * - * @param actions - * @returns {Array} - */ - _get_action_links(actions) { - var action_links = []; - // Only these actions are allowed without a selection (empty actions) - var empty_actions = ['add']; - for (var i in actions) { - var action = actions[i]; - if (empty_actions.indexOf(action.id) !== -1 || action.type === 'drop') { - action_links.push(typeof action.id !== 'undefined' ? action.id : i); - } - } - // Disable automatic paste action, it doesn't have what is needed to work - action_links.push({ - "actionObj": 'egw_paste', - "enabled": false, - "visible": false - }); - return action_links; - } - /** - * Show the current time while dragging - * Used for resizing as well as drag & drop - * - * @param {type} element - * @param {type} position - * @param {type} height - */ - _drag_helper(element, position, height) { - var time = this._get_time_from_position(position.left, position.top); - element.dropEnd = time; - var formatted_time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: time.getUTCHours(), - minute: time.getUTCMinutes(), - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") === "12") }); - element.innerHTML = '
      ' + formatted_time + '
      '; - //jQuery(element).width(jQuery(helper).width()); - } - /** - * Handler for dropping an event on the timegrid - * - * @param {type} planner - * @param {type} event - * @param {type} ui - */ - _event_drop(planner, event, ui) { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = { start: 0 }; - if (typeof this.dropEnd != 'undefined') { - var drop_date = this.dropEnd.toJSON() || false; - var event_data = planner._get_event_info(ui.draggable); - var event_widget = planner.getWidgetById(event_data.widget_id); - if (event_widget) { - event_widget._parent.date_helper.set_value(drop_date); - event_widget.options.value.start = new Date(event_widget._parent.date_helper.getValue()); - // Leave the helper there until the update is done - var loading = ui.helper.clone().appendTo(ui.helper.parent()); - // and add a loading icon so user knows something is happening - jQuery('.calendar_timeDemo', loading).after('
      '); - event_widget.recur_prompt(function (button_id) { - if (button_id === 'cancel' || !button_id) - return; - //Get infologID if in case if it's an integrated infolog event - if (event_data.app === 'infolog') { - // If it is an integrated infolog event we need to edit infolog entry - egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', [event_data.id, event_widget.options.value.start || false], function () { loading.remove(); }).sendRequest(true); - } - else { - //Edit calendar event - egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ - button_id === 'series' ? event_data.id : event_data.app_id, event_data.owner, - event_widget.options.value.start, - planner.options.owner || egw.user('account_id') - ], function () { loading.remove(); }).sendRequest(true); - } - }); - } - } - } - /** - * Use the egw.data system to get data from the calendar list for the - * selected time span. - * - */ - _fetch_data() { - var value = []; - var fetch = false; - this.doInvalidate = false; - for (var i = 0; i < this.registeredCallbacks.length; i++) { - egw.dataUnregisterUID(this.registeredCallbacks[i], false, this); - } - this.registeredCallbacks.splice(0, this.registeredCallbacks.length); - // Remember previous day to avoid multi-days duplicating - var last_data = []; - var t = new Date(this.options.start_date); - var end = new Date(this.options.end_date); - do { - value = value.concat(this._cache_register(t, this.options.owner, last_data)); - t.setUTCDate(t.getUTCDate() + 1); - } while (t < end); - this.doInvalidate = true; - return value; - } - /** - * Deal with registering for data cache - * - * @param Date t - * @param String owner Calendar owner - */ - _cache_register(t, owner, last_data) { - // Cache is by date (and owner, if seperate) - var date = t.getUTCFullYear() + sprintf('%02d', t.getUTCMonth() + 1) + sprintf('%02d', t.getUTCDate()); - var cache_id = CalendarApp._daywise_cache_id(date, owner); - var value = []; - if (egw.dataHasUID(cache_id)) { - var c = egw.dataGetUIDdata(cache_id); - if (c.data && c.data !== null) { - // There is data, pass it along now - for (var j = 0; j < c.data.length; j++) { - if (last_data.indexOf(c.data[j]) === -1 && egw.dataHasUID('calendar::' + c.data[j])) { - value.push(egw.dataGetUIDdata('calendar::' + c.data[j]).data); - } - } - last_data = c.data; - } - } - else { - fetch = true; - // Assume it's empty, if there is data it will be filled later - egw.dataStoreUID(cache_id, []); - } - this.registeredCallbacks.push(cache_id); - egw.dataRegisterUID(cache_id, function (data) { - if (data && data.length) { - var invalidate = true; - // Try to determine rows interested - var labels = []; - var events = {}; - if (this.grouper) { - labels = this.grouper.row_labels.call(this); - invalidate = false; - } - var im = this.getInstanceManager(); - for (var i = 0; i < data.length; i++) { - var event = egw.dataGetUIDdata('calendar::' + data[i]); - if (!event) - continue; - events = {}; - // Try to determine rows interested - if (event.data && this.grouper) { - this.grouper.group.call(this, labels, events, event.data); - } - if (Object.keys(events).length > 0) { - for (var label_id in events) { - var id = "" + labels[label_id].id; - if (typeof this.cache[id] === 'undefined') { - this.cache[id] = []; - } - if (this.cache[id].indexOf(event.data.row_id) === -1) { - this.cache[id].push(event.data.row_id); - } - if (this._deferred_row_updates[id]) { - window.clearTimeout(this._deferred_row_updates[id]); - } - this._deferred_row_updates[id] = window.setTimeout(jQuery.proxy(this._deferred_row_update, this, id), this.DEFERRED_ROW_TIME); - } - } - else { - // Could be an event no row is interested in, could be a problem. - // Just redraw everything - invalidate = true; - continue; - } - // If displaying by category, we need the infolog (or other app) categories too - if (event && event.data && event.data.app && this.options.group_by == 'category') { - // Fake it to use the cache / call - et2_selectbox.cat_options({ - _type: 'select-cat', - getInstanceManager: function () { return im; } - }, { application: event.data.app || 'calendar' }); - } - } - if (invalidate) { - this.invalidate(false); - } - } - }, this, this.getInstanceManager().execId, this.id); - return value; - } - /** - * Because users may be participants in various events and the time it takes - * to create many events, we don't want to update a row too soon - we may have - * to re-draw it if we find the user / category in another event. Pagination - * makes this worse. We wait a bit before updating the row to avoid - * having to re-draw it multiple times. - * - * @param {type} id - * @returns {undefined} - */ - _deferred_row_update(id) { - // Something's in progress, skip - if (!this.doInvalidate) - return; - this.grid.height(0); - var id_list = typeof id === 'undefined' ? Object.keys(this.cache) : [id]; - for (var i = 0; i < id_list.length; i++) { - var cache_id = id_list[i]; - var row = this.getWidgetById('planner_row_' + cache_id); - window.clearTimeout(this._deferred_row_updates[cache_id]); - delete this._deferred_row_updates[cache_id]; - if (row) { - row._data_callback(this.cache[cache_id]); - row.set_disabled(this.options.hide_empty && this.cache[cache_id].length === 0); - } - else { - break; - } - } - // Updating the row may push things longer, update length - // Add 1 to keep the scrollbar, otherwise we need to recalculate the - // header widths too. - this.grid.height(this.rows[0].scrollHeight + 1); - // Adjust header if there's a scrollbar - Firefox needs this re-calculated, - // otherwise the header will be missing the margin space for the scrollbar - // in some cases - if (this.rows.children().last().length) { - this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px'); - } - } - /** - * Provide specific data to be displayed. - * This is a way to set start and end dates, owner and event data in once call. - * - * @param {Object[]} events Array of events, indexed by date in Ymd format: - * { - * 20150501: [...], - * 20150502: [...] - * } - * Days should be in order. - * - */ - set_value(events) { - if (typeof events !== 'object') - return false; - super.set_value(events); - // Planner uses an array, not map - var val = this.value; - var array = []; - Object.keys(this.value).forEach(function (key) { - array.push(val[key]); - }); - this.value = array; - } - /** - * Change the start date - * Planner view uses a date object internally - * - * @param {string|number|Date} new_date New starting date - * @returns {undefined} - */ - set_start_date(new_date) { - super.set_start_date(new_date); - this.options.start_date = new Date(this.options.start_date); - } - /** - * Change the end date - * Planner view uses a date object internally - * - * @param {string|number|Date} new_date New end date - * @returns {undefined} - */ - set_end_date(new_date) { - super.set_end_date(new_date); - this.options.end_date = new Date(this.options.end_date); - } - /** - * Change how the planner is grouped - * - * @param {string|number} group_by 'user', 'month', or an integer category ID - * @returns {undefined} - */ - set_group_by(group_by) { - if (isNaN(group_by) && typeof this.groupers[group_by] === 'undefined') { - throw new Error('Invalid group_by "' + group_by + '"'); - } - var old = this.options.group_by; - this.options.group_by = '' + group_by; - this.grouper = this.groupers[isNaN(this.options.group_by) ? this.options.group_by : 'category']; - if (old !== this.options.group_by && this.isAttached()) { - this.invalidate(true); - } - } - /** - * Set which users to display - * - * Changing the owner will invalidate the display, and it will be redrawn - * after a timeout. Overwriting here to check for groups without members. - * - * @param {number|number[]|string|string[]} _owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - * - * @memberOf et2_calendar_view - */ - set_owner(_owner) { - super.set_owner(_owner); - // If we're grouping by user, we need group members - if (this.update_timer !== null && this.options.group_by == 'user') { - let options = []; - let resource = {}; - let missing_resources = []; - if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - else { - options = this.getArrayMgr("sel_options").getRoot().getEntry('owner'); - } - for (var i = 0; i < this.options.owner.length; i++) { - var user = this.options.owner[i]; - if (isNaN(user) || user >= 0 || !options) - continue; - // Owner is a group, see if we have its members - if (options.find && - ((resource = options.find(function (element) { - return element.id == user; - })))) { - // Members found - continue; - } - // Group, but no users found. Need those. - missing_resources.push(user); - // Maybe api already has them? - egw.accountData(parseInt(user), 'account_fullname', true, function (result) { - missing_resources.splice(missing_resources.indexOf(this), 1); - }.bind(user), user); - } - if (missing_resources.length > 0) { - // Ask server, and WAIT or we have to redraw - egw.json('calendar_owner_etemplate_widget::ajax_owner', [missing_resources], function (data) { - for (let owner in data) { - if (!owner || typeof owner == "undefined") - continue; - options.push(data[owner]); - } - }, this, false, this).sendRequest(false); - } - } - } - /** - * Turn on or off the visibility of weekends - * - * @param {boolean} weekends - */ - set_show_weekend(weekends) { - weekends = weekends ? true : false; - if (this.options.show_weekend !== weekends) { - this.options.show_weekend = weekends; - if (this.isAttached()) { - this.invalidate(); - } - } - } - /** - * Turn on or off the visibility of hidden (empty) rows - * - * @param {boolean} hidden - */ - set_hide_empty(hidden) { - this.options.hide_empty = hidden; - } - /** - * Call change handler, if set - * - * @param {type} event - */ - change(event) { - if (this.onchange) { - if (typeof this.onchange == 'function') { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.push(this); - return this.onchange.apply(this, args); - } - else { - return (et2_compileLegacyJS(this.options.onchange, this, _node))(); - } - } - } - /** - * Call event change handler, if set - * - * @param {type} event - * @param {type} dom_node - */ - event_change(event, dom_node) { - if (this.onevent_change) { - var event_data = this._get_event_info(dom_node); - var event_widget = this.getWidgetById(event_data.widget_id); - et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function (button_id, event_data) { - // No need to continue - if (button_id === 'cancel') - return false; - if (typeof this.onevent_change == 'function') { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(event_widget) == -1) - args.push(event_widget); - // Put button ID in event - event.button_id = button_id; - return this.onevent_change.apply(this, [event, event_widget, button_id]); - } - else { - return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); - } - }, this)); - } - return false; - } - /** - * Click handler calling custom handler set via onclick attribute to this.onclick - * - * This also handles all its own actions, including navigation. If there is - * an event associated with the click, it will be found and passed to the - * onclick function. - * - * @param {Event} _ev - * @returns {boolean} Continue processing event (true) or stop (false) - */ - click(_ev) { - var result = true; - // Drag to create in progress - if (this.drag_create.start !== null) - return; - // Is this click in the event stuff, or in the header? - if (!this.options.readonly && this.gridHeader.has(_ev.target).length === 0 && !jQuery(_ev.target).hasClass('calendar_plannerRowHeader')) { - // Event came from inside, maybe a calendar event - var event = this._get_event_info(_ev.originalEvent.target); - if (typeof this.onclick == 'function') { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.splice(1, 0, this); - result = this.onclick.apply(this, args); - } - if (event.id && result && !this.options.disabled && !this.options.readonly) { - et2_calendar_event.recur_prompt(event); - return false; - } - else if (!event.id) { - // Clicked in row, but not on an event - // Default handler to open a new event at the selected time - if (jQuery(event.target).closest('.calendar_eventRows').length == 0) { - // "Invalid" times, from space after the last planner row, or header - var date = this._get_time_from_position(_ev.pageX - this.grid.offset().left, _ev.pageY - this.grid.offset().top); - } - else if (this.options.group_by == 'month') { - var date = this._get_time_from_position(_ev.clientX, _ev.clientY); - } - else { - var date = this._get_time_from_position(_ev.offsetX, _ev.offsetY); - } - var row = jQuery(_ev.target).closest('.calendar_plannerRowWidget'); - var data = row.length ? row[0].dataset : {}; - if (date) { - app.calendar.add(jQuery.extend({ - start: date.toJSON(), - hour: date.getUTCHours(), - minute: date.getUTCMinutes() - }, data)); - return false; - } - } - return result; - } - else if (this.gridHeader.has(_ev.target).length > 0 && !jQuery.isEmptyObject(_ev.target.dataset) || - jQuery(_ev.target).hasClass('calendar_plannerRowHeader') && !jQuery.isEmptyObject(_ev.target.dataset)) { - // Click on a header, we can go there - _ev.data = jQuery.extend({}, _ev.target.parentNode.dataset, _ev.target.dataset); - for (var key in _ev.data) { - if (!_ev.data[key]) { - delete _ev.data[key]; - } - } - app.calendar.update_state(_ev.data); - } - else if (!this.options.readonly) { - // Default handler to open a new event at the selected time - // TODO: Determine date / time more accurately from position - app.calendar.add({ - date: _ev.target.dataset.date || this.options.start_date.toJSON(), - hour: _ev.target.dataset.hour || this.options.day_start, - minute: _ev.target.dataset.minute || 0 - }); - return false; - } - } - /** - * Get time from position - * - * @param {number} x - * @param {number} y - * @returns {Date|Boolean} A time for the given position, or false if one - * could not be determined. - */ - _get_time_from_position(x, y) { - x = Math.round(x); - y = Math.round(y); - // Round to user's preferred event interval - var interval = egw.preference('interval', 'calendar') || 30; - // Relative horizontal position, as a percentage - var width = 0; - jQuery('.calendar_eventRows', this.div).each(function () { width = Math.max(width, jQuery(this).width()); }); - var rel_x = Math.min(x / width, 1); - // Relative time, in minutes from start - var rel_time = 0; - var day_header = jQuery('.calendar_plannerScaleDay', this.headers); - // Simple math, the x is offset from start date - if (this.options.group_by !== 'month' && ( - // Either all days are visible, or only 1 day (no day header) - this.options.show_weekend || day_header.length === 0)) { - rel_time = (new Date(this.options.end_date) - new Date(this.options.start_date)) * rel_x / 1000; - this.date_helper.set_value(this.options.start_date.toJSON()); - } - // Not so simple math, need to account for missing days - else if (this.options.group_by !== 'month' && !this.options.show_weekend) { - // Find which day - if (day_header.length === 0) - return false; - var day = document.elementFromPoint(day_header.offset().left + rel_x * this.headers.innerWidth(), day_header.offset().top); - // Use day, and find time in that day - if (day && day.dataset && day.dataset.date) { - this.date_helper.set_value(day.dataset.date); - rel_time = ((x - jQuery(day).position().left) / jQuery(day).outerWidth(true)) * 24 * 60; - this.date_helper.set_minutes(Math.round(rel_time / interval) * interval); - return new Date(this.date_helper.getValue()); - } - return false; - } - else { - // Find the correct row so we know which month, then get the offset - var hidden_nodes = []; - var row = null; - // Hide any drag or tooltips that may interfere - do { - row = document.elementFromPoint(x, y); - if (this.div.has(row).length == 0) { - hidden_nodes.push(jQuery(row).hide()); - } - else { - break; - } - } while (row && row.nodeName !== 'BODY'); - if (!row) - return false; - // Restore hidden nodes - for (var i = 0; i < hidden_nodes.length; i++) { - hidden_nodes[i].show(); - } - row = jQuery(row).closest('.calendar_plannerRowWidget'); - var row_widget = null; - for (var i = 0; i < this._children.length && row.length > 0; i++) { - if (this._children[i].div[0] == row[0]) { - row_widget = this._children[i]; - break; - } - } - if (row_widget) { - // Not sure where the extra -1 and +2 are coming from, but it makes it work out - // in FF & Chrome - rel_x = Math.min((x - row_widget.rows.offset().left - 1) / (row_widget.rows.width() + 2), 1); - // 2678400 is the number of seconds in 31 days - rel_time = (2678400) * rel_x; - this.date_helper.set_value(row_widget.options.start_date.toJSON()); - } - else { - return false; - } - } - if (rel_time < 0) - return false; - this.date_helper.set_minutes(Math.round(rel_time / (60 * interval)) * interval); - return new Date(this.date_helper.getValue()); - } - /** - * Mousedown handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_down(event) { - // Only left mouse button - if (event.which !== 1) - return; - // Ignore headers - if (this.headers.has(event.target).length !== 0) - return false; - // Get time at mouse - if (this.options.group_by === 'month') { - var time = this._get_time_from_position(event.clientX, event.clientY); - } - else { - var time = this._get_time_from_position(event.offsetX, event.offsetY); - } - if (!time) - return false; - // Find the correct row so we know the parent - var row = event.target.closest('.calendar_plannerRowWidget'); - for (var i = 0; i < this._children.length && row; i++) { - if (this._children[i].div[0] === row) { - this.drag_create.parent = this._children[i]; - // Clear cached events for re-layout - this._children[i]._cached_rows = []; - break; - } - } - if (!this.drag_create.parent) - return false; - this.div.css('cursor', 'ew-resize'); - return this._drag_create_start(jQuery.extend({}, this.drag_create.parent.node.dataset, { date: time.toJSON() })); - } - /** - * Mouseup handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_up(event) { - // Get time at mouse - if (this.options.group_by === 'month') { - var time = this._get_time_from_position(event.clientX, event.clientY); - } - else { - var time = this._get_time_from_position(event.offsetX, event.offsetY); - } - return this._drag_create_end(time ? { date: time.toJSON() } : false); - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - _attrs.push('start_date', 'end_date'); - } - getDetachedNodes() { - return [this.getDOMNode()]; - } - setDetachedAttributes(_nodes, _values) { - this.div = jQuery(_nodes[0]); - if (_values.start_date) { - this.set_start_date(_values.start_date); - } - if (_values.end_date) { - this.set_end_date(_values.end_date); - } - } - // Resizable interface - resize() { - // Take the whole tab height - var height = Math.min(jQuery(this.getInstanceManager().DOMContainer).height(), jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight()); - // Allow for toolbar - height -= jQuery('#calendar-toolbar', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); - this.options.height = height; - this.div.css('height', this.options.height); - // Set height for rows - this.rows.height(this.div.height() - this.headers.outerHeight()); - this.grid.height(this.rows[0].scrollHeight); - } - /** - * Set up for printing - * - * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up - * (waiting for data) - */ - beforePrint() { - if (this.disabled || !this.div.is(':visible')) { - return; - } - this.rows.css('overflow-y', 'visible'); - var rows = jQuery('.calendar_eventRows'); - var width = rows.width(); - var events = jQuery('.calendar_calEvent', rows) - .each(function () { - var event = jQuery(this); - event.width((event.width() / width) * 100 + '%'); - }); - } - /** - * Reset after printing - */ - afterPrint() { - this.rows.css('overflow-y', 'auto'); - } -} -et2_calendar_planner._attributes = { - group_by: { - name: "Group by", - type: "string", - default: "0", - description: "Display planner by 'user', 'month', or the given category" - }, - filter: { - name: "Filter", - type: "string", - default: '', - description: 'A filter that is used to select events. It is passed along when events are queried.' - }, - show_weekend: { - name: "Weekends", - type: "boolean", - default: egw.preference('days_in_weekview', 'calendar') != 5, - description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." - }, - hide_empty: { - name: "Hide empty rows", - type: "boolean", - default: false, - description: "Hide rows with no events." - }, - value: { - type: "any", - description: "A list of events, optionally you can set start_date, end_date and group_by as keys and events will be fetched" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the date range changes." - }, - "onevent_change": { - "name": "onevent_change", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when an event changes." - } -}; -et2_calendar_planner.DEFERRED_ROW_TIME = 100; -et2_register_widget(et2_calendar_planner, ["calendar-planner"]); -//# sourceMappingURL=et2_widget_planner.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_planner_row.js b/calendar/js/et2_widget_planner_row.js deleted file mode 100644 index 2e5a388e7d..0000000000 --- a/calendar/js/et2_widget_planner_row.js +++ /dev/null @@ -1,649 +0,0 @@ -/* - * Egroupware - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - /calendar/js/et2_widget_view.js; - /calendar/js/et2_widget_daycol.js; - /calendar/js/et2_widget_event.js; -*/ -import { et2_createWidget, et2_register_widget } from "../../api/js/etemplate/et2_core_widget"; -import { et2_valueWidget } from "../../api/js/etemplate/et2_core_valueWidget"; -import { ClassWithAttributes } from "../../api/js/etemplate/et2_core_inheritance"; -import { et2_action_object_impl } from "../../api/js/etemplate/et2_core_DOMWidget"; -import { egw_getObjectManager, egwActionObject } from "../../api/js/egw_action/egw_action.js"; -import { EGW_AI_DRAG_OUT, EGW_AI_DRAG_OVER } from "../../api/js/egw_action/egw_action_constants.js"; -import { egw } from "../../api/js/jsapi/egw_global"; -/** - * Class for one row of a planner - * - * This widget is responsible for the label on the side - * - */ -export class et2_calendar_planner_row extends et2_valueWidget { - /** - * Constructor - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_planner_row._attributes, _child || {})); - this._row_height = 20; - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_plannerRowWidget") - .css('width', this.options.width); - this.title = jQuery(document.createElement('div')) - .addClass("calendar_plannerRowHeader") - .appendTo(this.div); - this.rows = jQuery(document.createElement('div')) - .addClass("calendar_eventRows") - .appendTo(this.div); - this.setDOMNode(this.div[0]); - // Used for its date calculations - this._date_helper = et2_createWidget('date-time', {}, null); - this._date_helper.loadingFinished(); - this.set_start_date(this.options.start_date); - this.set_end_date(this.options.end_date); - this._cached_rows = []; - } - doLoadingFinished() { - super.doLoadingFinished(); - this.set_label(this.options.label); - this._draw(); - // Actions are set on the parent, so we need to explicitly get in here - // and get ours - this._link_actions(this.getParent().options.actions || []); - return true; - } - destroy() { - super.destroy(); - // date_helper has no parent, so we must explicitly remove it - this._date_helper.destroy(); - this._date_helper = null; - } - getDOMNode(_sender) { - if (_sender === this || !_sender) { - return this.div[0]; - } - if (_sender._parent === this) { - return this.rows[0]; - } - } - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions(actions) { - // Get the parent? Might be a grid row, might not. Either way, it is - // just a container with no valid actions - let objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; - let parent = objectManager.getObjectById(this.id, 1) || objectManager.getObjectById(this.getParent().id, 1) || objectManager; - if (!parent) { - egw.debug('error', 'No parent objectManager found'); - return; - } - // This binds into the egw action system. Most user interactions (drag to move, resize) - // are handled internally using jQuery directly. - let widget_object = this._actionObject || parent.getObjectById(this.id); - const aoi = new et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); - const planner = this.getParent(); - for (let i = 0; i < parent.children.length; i++) { - const parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div); - if (parent_finder.length > 0) { - parent = parent.children[i]; - break; - } - } - // Determine if we allow a dropped event to use the invite/change actions - const _invite_enabled = function (action, event, target) { - var event = event.iface.getWidget(); - const row = target.iface.getWidget() || false; - if (event === row || !event || !row || - !event.options || !event.options.value.participants) { - return false; - } - let owner_match = false; - const own_row = event.getParent() === row; - for (let id in event.options.value.participants) { - owner_match = owner_match || row.node.dataset.participants === '' + id; - } - const enabled = !owner_match && - // Not inside its own timegrid - !own_row; - widget_object.getActionLink('invite').enabled = enabled; - widget_object.getActionLink('change_participant').enabled = enabled; - // If invite or change participant are enabled, drag is not - widget_object.getActionLink('egw_link_drop').enabled = !enabled; - }; - aoi.doTriggerEvent = function (_event, _data) { - // Determine target node - var event = _data.event || false; - if (!event) - return; - if (_data.ui.draggable.hasClass('rowNoEdit')) - return; - /* - We have to handle the drop in the normal event stream instead of waiting - for the egwAction system so we can get the helper, and destination - */ - if (event.type === 'drop' && widget_object.getActionLink('egw_link_drop').enabled) { - this.getWidget().getParent()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], this.getWidget().getParent(), event, _data.ui, this.getWidget()); - } - const drag_listener = function (_event, ui) { - if (planner.options.group_by === 'month') { - var position = { left: _event.clientX, top: _event.clientY }; - } - else { - var position = { top: ui.position.top, left: ui.position.left - jQuery(this).parent().offset().left }; - } - aoi.getWidget().getParent()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', ui.helper)[0], position, 0); - let event = _data.ui.draggable.data('selected')[0]; - if (!event || event.id && event.id.indexOf('calendar') !== 0) { - event = false; - } - if (event) { - _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); - } - }; - const time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper); - switch (_event) { - // Triggered once, when something is dragged into the timegrid's div - case EGW_AI_DRAG_OVER: - // Listen to the drag and update the helper with the time - // This part lets us drag between different timegrids - _data.ui.draggable.on('drag.et2_timegrid_row' + widget_object.id, drag_listener); - _data.ui.draggable.on('dragend.et2_timegrid_row' + widget_object.id, function () { - _data.ui.draggable.off('drag.et2_timegrid_row' + widget_object.id); - }); - widget_object.iface.getWidget().div.addClass('drop-hover'); - // Disable invite / change actions for same calendar or already participant - var event = _data.ui.draggable.data('selected')[0]; - if (!event || event.id && event.id.indexOf('calendar') !== 0) { - event = false; - } - if (event) { - _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); - } - if (time.length) { - // The out will trigger after the over, so we count - time.data('count', time.data('count') + 1); - } - else { - _data.ui.helper.prepend('
      '); - } - break; - // Triggered once, when something is dragged out of the timegrid - case EGW_AI_DRAG_OUT: - // Stop listening - _data.ui.draggable.off('drag.et2_timegrid_row' + widget_object.id); - // Remove highlight - widget_object.iface.getWidget().div.removeClass('drop-hover'); - // Out triggers after the over, count to not accidentally remove - time.data('count', time.data('count') - 1); - if (time.length && time.data('count') <= 0) { - time.remove(); - } - break; - } - }; - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = parent.insertObject(false, new egwActionObject(this.id, parent, aoi, this._actionManager || parent.manager.getActionById(this.id) || parent.manager)); - } - else { - widget_object.setAOI(aoi); - } - this._actionObject = widget_object; - // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - const action_links = this._get_action_links(actions); - this.getParent()._init_links_dnd(widget_object.manager, action_links); - widget_object.updateActionLinks(action_links); - } - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * Here we are only interested in drop events. - * - * @param actions - * @returns {Array} - */ - _get_action_links(actions) { - const action_links = []; - // Only these actions are allowed without a selection (empty actions) - const empty_actions = ['add']; - for (let i in actions) { - const action = actions[i]; - if (empty_actions.indexOf(action.id) !== -1 || action.type == 'drop') { - action_links.push(typeof action.id != 'undefined' ? action.id : i); - } - } - return action_links; - } - /** - * Draw the individual divs for weekends and events - */ - _draw() { - // Remove any existing - this.rows.remove('.calendar_eventRowsMarkedDay,.calendar_eventRowsFiller').nextAll().remove(); - let days = 31; - let width = '100'; - if (this.getParent().options.group_by === 'month') { - days = this.options.end_date.getUTCDate(); - if (days < 31) { - const diff = 31 - days; - width = 'calc(' + (diff * 3.23) + '% - ' + (diff * 7) + 'px)'; - } - } - // mark weekends and other special days in yearly planner - if (this.getParent().options.group_by == 'month') { - this.rows.append(this._yearlyPlannerMarkDays(this.options.start_date, days)); - } - if (this.getParent().options.group_by === 'month' && days < 31) { - // add a filler for non existing days in that month - this.rows.after('
      '); - } - } - set_label(label) { - this.options.label = label; - this.title.text(label); - if (this.getParent().options.group_by === 'month') { - this.title.attr('data-date', this.options.start_date.toJSON()); - this.title.attr('data-sortby', 'user'); - this.title.addClass('et2_clickable et2_link'); - } - else { - this.title.attr('data-date', ''); - this.title.removeClass('et2_clickable'); - } - } - /** - * Change the start date - * - * @param {Date} new_date New end date - * @returns {undefined} - */ - set_start_date(new_date) { - if (!new_date || new_date === null) { - throw new TypeError('Invalid end date. ' + new_date.toString()); - } - this.options.start_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); - this.options.start_date.setUTCHours(0); - this.options.start_date.setUTCMinutes(0); - this.options.start_date.setUTCSeconds(0); - } - /** - * Change the end date - * - * @param {string|number|Date} new_date New end date - * @returns {undefined} - */ - set_end_date(new_date) { - if (!new_date || new_date === null) { - throw new TypeError('Invalid end date. ' + new_date.toString()); - } - this.options.end_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON()); - this.options.end_date.setUTCHours(23); - this.options.end_date.setUTCMinutes(59); - this.options.end_date.setUTCSeconds(59); - } - /** - * Mark special days (birthdays, holidays) on the planner - * - * @param {Date} start Start of the month - * @param {number} days How many days in the month - */ - _yearlyPlannerMarkDays(start, days) { - const day_width = 3.23; - const t = new Date(start); - let content = ''; - for (let i = 0; i < days; i++) { - const holidays = []; - // TODO: implement this, pull / copy data from et2_widget_timegrid - const day_class = this.getParent().day_class_holiday(t, holidays); - if (day_class) // no regular weekday - { - content += '
      '; - } - t.setUTCDate(t.getUTCDate() + 1); - } - return content; - } - /** - * Callback used when the daywise data changes - * - * Events should update themselves when their data changes, here we are - * dealing with a change in which events are displayed on this row. - * - * @param {String[]} event_ids - * @returns {undefined} - */ - _data_callback(event_ids) { - const events = []; - if (event_ids == null || typeof event_ids.length == 'undefined') - event_ids = []; - for (let i = 0; i < event_ids.length; i++) { - let event = egw.dataGetUIDdata('calendar::' + event_ids[i]); - event = event && event.data || false; - if (event && event.date) { - events.push(event); - } - else if (event) { - // Got an ID that doesn't belong - event_ids.splice(i--, 1); - } - } - if (!this.getParent().disabled && event_ids.length > 0) { - this.resize(); - this._update_events(events); - } - } - get date_helper() { - return this._date_helper; - } - /** - * Load the event data for this day and create event widgets for each. - * - * If event information is not provided, it will be pulled from the content array. - * - * @param {Object[]} [events] Array of event information, one per event. - */ - _update_events(events) { - // Remove all events - while (this._children.length > 0) { - const node = this._children[this._children.length - 1]; - this.removeChild(node); - node.destroy(); - } - this._cached_rows = []; - for (var c = 0; c < events.length; c++) { - // Create event - var event = et2_createWidget('calendar-event', { - id: 'event_' + events[c].row_id, - value: events[c] - }, this); - } - // Seperate loop so column sorting finds all children in the right place - for (var c = 0; c < events.length; c++) { - let event = this.getWidgetById('event_' + events[c].row_id); - if (!event) - continue; - if (this.isInTree()) { - event.doLoadingFinished(); - } - } - } - /** - * Position the event according to it's time and how this widget is laid - * out. - * - * @param {undefined|Object|et2_calendar_event} event - */ - position_event(event) { - const rows = this._spread_events(); - const height = rows.length * this._row_height; - let row_width = this.rows.width(); - if (row_width == 0) { - // Not rendered yet or something - row_width = this.getParent().gridHeader.width() - this.title.width(); - } - row_width -= 15; - for (let c = 0; c < rows.length; c++) { - // Calculate vertical positioning - const top = c * (100.0 / rows.length); - for (let i = 0; (rows[c].indexOf(event) >= 0 || !event) && i < rows[c].length; i++) { - // Calculate horizontal positioning - const left = this._time_to_position(rows[c][i].options.value.start); - const width = this._time_to_position(rows[c][i].options.value.end) - left; - // Position the event - rows[c][i].div.css('top', top + '%'); - rows[c][i].div.css('height', (100 / rows.length) + '%'); - rows[c][i].div.css('left', left.toFixed(1) + '%'); - rows[c][i].div.outerWidth((width / 100 * row_width) + 'px'); - } - } - if (height) { - this.div.height(height + 'px'); - } - } - /** - * Sort a day's events into non-overlapping rows - * - * @returns {Array[]} Events sorted into rows - */ - _spread_events() { - // Keep it so we don't have to re-do it when the next event asks - let cached_length = 0; - this._cached_rows.map(function (row) { cached_length += row.length; }); - if (cached_length === this._children.length) { - return this._cached_rows; - } - // sorting the events in non-overlapping rows - const rows = []; - const row_end = [0]; - // Sort in chronological order, so earliest ones are at the top - this._children.sort(function (a, b) { - const start = new Date(a.options.value.start) - new Date(b.options.value.start); - const end = new Date(a.options.value.end) - new Date(b.options.value.end); - // Whole day events sorted by ID, normal events by start / end time - if (a.options.value.whole_day && b.options.value.whole_day) { - // Longer duration comes first so we have nicer bars across the top - const duration = (new Date(b.options.value.end) - new Date(b.options.value.start)) - - (new Date(a.options.value.end) - new Date(a.options.value.start)); - return duration ? duration : (a.options.value.app_id - b.options.value.app_id); - } - else if (a.options.value.whole_day || b.options.value.whole_day) { - return a.options.value.whole_day ? -1 : 1; - } - return start ? start : end; - }); - for (let n = 0; n < this._children.length; n++) { - const event = this._children[n].options.value || false; - if (typeof event.start !== 'object') { - this._date_helper.set_value(event.start); - event.start = new Date(this._date_helper.getValue()); - } - if (typeof event.end !== 'object') { - this._date_helper.set_value(event.end); - event.end = new Date(this._date_helper.getValue()); - } - if (typeof event['start_m'] === 'undefined') { - let day_start = event.start.valueOf() / 1000; - const dst_check = new Date(event.start); - dst_check.setUTCHours(12); - // if daylight saving is switched on or off, correct $day_start - // gives correct times after 2am, times between 0am and 2am are wrong - const daylight_diff = day_start + 12 * 60 * 60 - (dst_check.valueOf() / 1000); - if (daylight_diff) { - day_start -= daylight_diff; - } - event['start_m'] = event.start.getUTCHours() * 60 + event.start.getUTCMinutes(); - if (event['start_m'] < 0) { - event['start_m'] = 0; - event['multiday'] = true; - } - event['end_m'] = event.end.getUTCHours() * 60 + event.end.getUTCMinutes(); - if (event['end_m'] >= 24 * 60) { - event['end_m'] = 24 * 60 - 1; - event['multiday'] = true; - } - if (!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59) { - event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0'); - } - } - // Skip events entirely on hidden weekends - if (this._hidden_weekend_event(event)) { - const node = this._children[n]; - this.removeChild(n--); - node.destroy(); - continue; - } - const event_start = new Date(event.start).valueOf(); - for (var row = 0; row_end[row] > event_start; ++row) - ; // find a "free" row (no other event) - if (typeof rows[row] === 'undefined') - rows[row] = []; - rows[row].push(this._children[n]); - row_end[row] = new Date(event['end']).valueOf(); - } - this._cached_rows = rows; - return rows; - } - /** - * Check to see if the event is entirely on a hidden weekend - * - * @param values Array of event values, not an et2_widget_event - */ - _hidden_weekend_event(values) { - if (!this.getParent() || this.getParent().options.group_by == 'month' || this.getParent().options.show_weekend) { - return false; - } - // Starts on Saturday or Sunday, ends Sat or Sun, less than 2 days long - else if ([0, 6].indexOf(values.start.getUTCDay()) !== -1 && [0, 6].indexOf(values.end.getUTCDay()) !== -1 - && values.end - values.start < 2 * 24 * 3600 * 1000) { - return true; - } - return false; - } - /** - * Calculates the horizontal position based on the time given, as a percentage - * between the start and end times - * - * @param {int|Date|string} time in minutes from midnight, or a Date in string or object form - * @param {int|Date|string} start Earliest possible time (0%) - * @param {int|Date|string} end Latest possible time (100%) - * @return {float} position in percent - */ - _time_to_position(time, start, end) { - let pos = 0.0; - // Handle the different value types - start = this.options.start_date; - end = this.options.end_date; - if (typeof start === 'string') { - start = new Date(start); - end = new Date(end); - } - const wd_start = 60 * (parseInt('' + egw.preference('workdaystarts', 'calendar')) || 9); - const wd_end = 60 * (parseInt('' + egw.preference('workdayends', 'calendar')) || 17); - let t = time; - if (typeof time === 'number' && time < 3600) { - t = new Date(start.valueOf() + wd_start * 3600 * 1000); - } - else { - t = new Date(time); - } - // Limits - if (t <= start) - return 0; // We are left of our scale - if (t >= end) - return 100; // We are right of our scale - // Remove space for weekends, if hidden - let weekend_count = 0; - let weekend_before = 0; - let partial_weekend = 0; - if (this.getParent().options.group_by !== 'month' && this.getParent() && !this.getParent().options.show_weekend) { - const counter_date = new Date(start); - do { - if ([0, 6].indexOf(counter_date.getUTCDay()) !== -1) { - if (counter_date.getUTCDate() === t.getUTCDate() && counter_date.getUTCMonth() === t.getUTCMonth()) { - // Event is partially on a weekend - partial_weekend += (t.getUTCHours() * 60 + t.getUTCMinutes()) * 60 * 1000; - } - else if (counter_date < t) { - weekend_before++; - } - weekend_count++; - } - counter_date.setUTCDate(counter_date.getUTCDate() + 1); - } while (counter_date < end); - // Put it in ms - weekend_before *= 24 * 3600 * 1000; - weekend_count *= 24 * 3600 * 1000; - } - // Basic scaling, doesn't consider working times - pos = (t - start - weekend_before - partial_weekend) / (end - start - weekend_count); - // Month view - if (this.getParent().options.group_by !== 'month') { - // Daywise scaling - /* Needs hourly scales that consider working hours - var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate()); - var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate()); - var t_date = new Date(t.getUTCFullYear(), t.getUTCMonth(),t.getUTCDate()); - - var days = Math.round((end_date - start_date) / (24 * 3600 * 1000))+1; - pos = 1 / days * Math.round((t_date - start_date) / (24*3600 * 1000)); - - var time_of_day = typeof t === 'object' ? 60 * t.getUTCHours() + t.getUTCMinutes() : t; - - if (time_of_day >= wd_start) - { - var day_percentage = 0.1; - if (time_of_day > wd_end) - { - day_percentage = 1; - } - else - { - var wd_length = wd_end - wd_start; - if (wd_length <= 0) wd_length = 24*60; - day_percentage = (time_of_day-wd_start) / wd_length; // between 0 and 1 - } - pos += day_percentage / days; - } - */ - } - else { - // 2678400 is the number of seconds in 31 days - pos = (t - start) / 2678400000; - } - pos = 100 * pos; - return pos; - } - // Resizable interface - /** - * Resize - * - * Parent takes care of setting proper width & height for the containing div - * here we just need to adjust the events to fit the new size. - */ - resize() { - if (this.disabled || !this.div.is(':visible') || this.getParent().disabled) { - return; - } - const row = jQuery('
      ').appendTo(this.rows); - this._row_height = (parseInt(window.getComputedStyle(row[0]).getPropertyValue("height")) || 20); - row.remove(); - // Resize & position all events - this.position_event(); - } -} -et2_calendar_planner_row._attributes = { - start_date: { - name: "Start date", - type: "any" - }, - end_date: { - name: "End date", - type: "any" - }, - value: { - type: "any" - } -}; -et2_register_widget(et2_calendar_planner_row, ["calendar-planner_row"]); -//# sourceMappingURL=et2_widget_planner_row.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_timegrid.js b/calendar/js/et2_widget_timegrid.js deleted file mode 100644 index b44be88ded..0000000000 --- a/calendar/js/et2_widget_timegrid.js +++ /dev/null @@ -1,1914 +0,0 @@ -/* - * Egroupware Calendar timegrid - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - /calendar/js/et2_widget_view.js; -*/ -import { et2_createWidget, et2_register_widget } from "../../api/js/etemplate/et2_core_widget"; -import { ClassWithAttributes } from "../../api/js/etemplate/et2_core_inheritance"; -import { et2_calendar_view } from "./et2_widget_view"; -import { et2_action_object_impl } from "../../api/js/etemplate/et2_core_DOMWidget"; -import { et2_dataview_grid } from "../../api/js/etemplate/et2_dataview_view_grid"; -import { et2_calendar_daycol } from "./et2_widget_daycol"; -import { egw } from "../../api/js/jsapi/egw_global"; -import { et2_no_init } from "../../api/js/etemplate/et2_core_common"; -import { et2_IResizeable } from "../../api/js/etemplate/et2_core_interfaces"; -import { et2_calendar_event } from "./et2_widget_event"; -import { egwActionObject, egw_getObjectManager } from "../../api/js/egw_action/egw_action.js"; -import { et2_compileLegacyJS } from "../../api/js/etemplate/et2_core_legacyJSFunctions"; -import { et2_dialog } from "../../api/js/etemplate/et2_widget_dialog"; -import { sprintf } from "../../api/js/egw_action/egw_action_common.js"; -import { EGW_AI_DRAG_OUT, EGW_AI_DRAG_OVER } from "../../api/js/egw_action/egw_action_constants.js"; -/** - * Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days - * - * This widget is responsible for the times on the side, and it is also the - * controller for both positioning and setting the day columns. Day columns are - * recycled rather than removed and re-created to reduce reloading. Similarly, - * the horizontal time grid (when used - see granularity attribute) is only - * redrawn or resized when needed. Unfortunately resizing is needed every time - * the all day section has an event added or removed so the full work day from - * start time to end time is properly displayed. - * - * - * @augments et2_calendar_view - */ -export class et2_calendar_timegrid extends et2_calendar_view { - /** - * Constructor - * - * @memberOf et2_calendar_timegrid - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_timegrid._attributes, _child || {})); - this.daily_owner = false; - // Main container - this.div = jQuery(document.createElement("div")) - .addClass("calendar_calTimeGrid") - .addClass("calendar_TimeGridNoLabel"); - // Headers - this.gridHeader = jQuery(document.createElement("div")) - .addClass("calendar_calGridHeader") - .appendTo(this.div); - this.dayHeader = jQuery(document.createElement("div")) - .appendTo(this.gridHeader); - // Contains times / rows - this.scrolling = jQuery(document.createElement('div')) - .addClass("calendar_calTimeGridScroll") - .appendTo(this.div) - .append('
      '); - // Contains days / columns - this.days = jQuery(document.createElement("div")) - .addClass("calendar_calDayCols") - .appendTo(this.scrolling); - // Used for owners - this.owner = et2_createWidget('description', {}, this); - this._labelContainer = jQuery(document.createElement("label")) - .addClass("et2_label et2_link") - .appendTo(this.gridHeader); - this.gridHover = jQuery('
      '); - // List of dates in Ymd - // The first one should be start_date, last should be end_date - this.day_list = []; - this.day_widgets = []; - // Timer to re-scale time to fit - this.resize_timer = null; - this.setDOMNode(this.div[0]); - } - destroy() { - // Stop listening to tab changes - if (typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) { - jQuery(framework.getApplicationByName('calendar').tab.contentDiv).off('show.' + this.id); - } - super.destroy(); - // Delete all old objects - this._actionObject.clear(); - this._actionObject.unregisterActions(); - this._actionObject.remove(); - this._actionObject = null; - this.div.off(); - this.div = null; - this.gridHeader = null; - this.dayHeader = null; - this.days = null; - this.scrolling = null; - this._labelContainer = null; - // Stop the resize timer - if (this.resize_timer) { - window.clearTimeout(this.resize_timer); - } - } - doLoadingFinished() { - super.doLoadingFinished(); - // Listen to tab show to make sure we scroll to the day start, not top - if (typeof framework !== 'undefined' && framework.getApplicationByName('calendar').tab) { - jQuery(framework.getApplicationByName('calendar').tab.contentDiv) - .on('show.' + this.id, jQuery.proxy(function () { - if (this.scrolling) { - this.scrolling.scrollTop(this._top_time); - } - }, this)); - } - // Need to get the correct internal sizing - this.resize(); - this._drawGrid(); - // Actions may be set on a parent, so we need to explicitly get in here - // and get ours - this._link_actions(this.options.actions || this.getParent().options.actions || []); - // Automatically bind drag and resize for every event using jQuery directly - // - no action system - - var timegrid = this; - /** - * If user puts the mouse over an event, then we'll set up resizing so - * they can adjust the length. Should be a little better on resources - * than binding it for every calendar event, and we won't need exceptions - * for planner view to resize horizontally. - */ - this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function () { - // Only resize in timegrid - if (timegrid.options.granularity === 0) - return; - // Load the event - timegrid._get_event_info(this); - var that = this; - //Resizable event handler - jQuery(this).resizable({ - distance: 10, - // Grid matching preference - grid: [10000, timegrid.rowHeight], - autoHide: false, - handles: 's,se', - containment: 'parent', - /** - * Triggered when the resizable is created. - * - * @param {event} event - * @param {Object} ui - */ - create: function (event, ui) { - var resizeHelper = event.target.getAttribute('data-resize'); - if (resizeHelper == 'WD' || resizeHelper == 'WDS') { - jQuery(this).resizable('destroy'); - } - }, - /** - * If dragging to resize an event, abort drag to create - * - * @param {jQuery.Event} event - * @param {Object} ui - */ - start: function (event, ui) { - if (timegrid.drag_create.start) { - // Abort drag to create, we're dragging to resize - timegrid._drag_create_end({}); - } - }, - /** - * Triggered at the end of resizing the calEvent. - * - * @param {event} event - * @param {Object} ui - */ - stop: function (event, ui) { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = { duration: 0 }; - var event_data = timegrid._get_event_info(this); - var event_widget = timegrid.getWidgetById(event_data.widget_id); - var sT = event_widget.options.value.start_m; - if (typeof this.dropEnd != 'undefined' && this.dropEnd.length == 1) { - var eT = (parseInt(timegrid._drop_data.hour) * 60) + parseInt(timegrid._drop_data.minute); - e.data.duration = ((eT - sT) / 60) * 3600; - if (event_widget) { - event_widget.options.value.end_m = eT; - event_widget.options.value.duration = e.data.duration; - } - jQuery(this).trigger(e); - event_widget._update(event_widget.options.value); - // That cleared the resize handles, so remove for re-creation... - if (jQuery(this).resizable('instance')) { - jQuery(this).resizable('destroy'); - } - } - // Clear the helper, re-draw - if (event_widget && event_widget._parent) { - event_widget._parent.position_event(event_widget); - } - timegrid.div.children('.drop-hover').removeClass('.drop-hover'); - }, - /** - * Triggered during the resize, on the drag of the resize handler - * - * @param {event} event - * @param {Object} ui - */ - resize: function (event, ui) { - // Add a bit for better understanding - it will show _to_ the start, - // covering the 'actual' target - timegrid._get_time_from_position(ui.helper[0].getBoundingClientRect().left, ui.helper[0].getBoundingClientRect().bottom + 5); - timegrid.gridHover.hide(); - var drop = timegrid._drag_helper(this, ui.element[0]); - if (drop && !drop.is(':visible')) { - drop.get(0).scrollIntoView(false); - } - } - }); - }); - // Customize and override some draggable settings - this.div - .on('dragcreate', '.calendar_calEvent', function (event, ui) { - jQuery(this).draggable('option', 'cancel', '.rowNoEdit'); - // Act like you clicked the header, makes it easier to position - // but put it to the side (-5) so we can still do the hover - jQuery(this).draggable('option', 'cursorAt', { top: 5, left: -5 }); - }) - .on('dragstart', '.calendar_calEvent', function (event, ui) { - jQuery('.calendar_calEvent', ui.helper).width(jQuery(this).width()) - .height(jQuery(this).outerHeight()) - .css('top', '').css('left', '') - .appendTo(ui.helper); - ui.helper.width(jQuery(this).width()); - // Cancel drag to create, we're dragging an existing event - timegrid.drag_create.start = null; - timegrid._drag_create_end(); - }) - .on('mousemove', function (event) { - timegrid._get_time_from_position(event.clientX, event.clientY); - }) - .on('mouseout', function (event) { - if (timegrid.div.has(event.relatedTarget).length === 0) { - timegrid.gridHover.hide(); - } - }) - .on('mousedown', jQuery.proxy(this._mouse_down, this)) - .on('mouseup', jQuery.proxy(this._mouse_up, this)); - return true; - } - _createNamespace() { - return true; - } - /** - * Show the current time while dragging - * Used for resizing as well as drag & drop - * - * @param {type} element - * @param {type} helper - * @param {type} height - */ - _drag_helper(element, helper, height) { - if (!element) - return; - element.dropEnd = this.gridHover; - if (element.dropEnd.length) { - this._drop_data = jQuery.extend({}, element.dropEnd[0].dataset || {}); - } - if (typeof element.dropEnd != 'undefined' && element.dropEnd.length) { - // Make sure the target is visible in the scrollable day - if (this.gridHover.is(':visible')) { - if (this.scrolling.scrollTop() > 0 && this.scrolling.scrollTop() >= this.gridHover.position().top - this.rowHeight) { - this.scrolling.scrollTop(this.gridHover.position().top - this.rowHeight); - } - else if (this.scrolling.scrollTop() + this.scrolling.height() <= this.gridHover.position().top + (2 * this.rowHeight)) { - this.scrolling.scrollTop(this.scrolling.scrollTop() + this.rowHeight); - } - } - var time = ''; - if (this._drop_data.whole_day) { - time = this.egw().lang('Whole day'); - } - else if (this.options.granularity === 0) { - // No times, keep what's in the event - // Add class to helper to keep formatting - jQuery(helper).addClass('calendar_calTimeGridList'); - } - else { - // @ts-ignore - time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: element.dropEnd.attr('data-hour'), - minute: element.dropEnd.attr('data-minute'), - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") == "12") }); - } - element.innerHTML = '
      ' + time + '
      '; - } - else { - element.innerHTML = '
      '; - } - jQuery(element).width(jQuery(helper).width()); - return element.dropEnd; - } - /** - * Handler for dropping an event on the timegrid - * - * @param {type} timegrid - * @param {type} event - * @param {type} ui - * @param {type} dropEnd - */ - _event_drop(timegrid, event, ui, dropEnd) { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = { start: 0 }; - if (typeof dropEnd != 'undefined' && dropEnd) { - var drop_date = dropEnd.date || false; - var event_data = timegrid._get_event_info(ui.draggable); - var event_widget = timegrid.getWidgetById(event_data.widget_id); - if (!event_widget) { - // Widget was moved across weeks / owners - event_widget = timegrid.getParent().getWidgetById(event_data.widget_id); - } - if (event_widget) { - // Send full string to avoid rollover between months using set_month() - event_widget._parent.date_helper.set_value(drop_date.substring(0, 4) + '-' + drop_date.substring(4, 6) + '-' + drop_date.substring(6, 8) + - 'T00:00:00Z'); - // Make sure whole day events stay as whole day events by ignoring drop time - if (event_data.app == 'calendar' && event_widget.options.value.whole_day) { - event_widget._parent.date_helper.set_hours(0); - event_widget._parent.date_helper.set_minutes(0); - } - else if (timegrid.options.granularity === 0) { - // List, not time grid - keep time - event_widget._parent.date_helper.set_hours(event_widget.options.value.start.getUTCHours()); - event_widget._parent.date_helper.set_minutes(event_widget.options.value.start.getUTCMinutes()); - } - else { - // Non-whole day events, and integrated apps, can change - event_widget._parent.date_helper.set_hours(dropEnd.whole_day ? 0 : dropEnd.hour || 0); - event_widget._parent.date_helper.set_minutes(dropEnd.whole_day ? 0 : dropEnd.minute || 0); - } - // Leave the helper there until the update is done - var loading = ui.helper.clone(true).appendTo(jQuery('body')); - // and add a loading icon so user knows something is happening - if (jQuery('.calendar_timeDemo', loading).length == 0) { - jQuery('.calendar_calEventHeader', loading).addClass('loading'); - } - else { - jQuery('.calendar_timeDemo', loading).after('
      '); - } - event_widget.recur_prompt(function (button_id) { - if (button_id === 'cancel' || !button_id) { - // Need to refresh the event with original info to clean up - var app_id = event_widget.options.value.app_id ? event_widget.options.value.app_id : event_widget.options.value.id + (event_widget.options.value.recur_type ? ':' + event_widget.options.value.recur_date : ''); - egw().dataStoreUID('calendar::' + app_id, egw.dataGetUIDdata('calendar::' + app_id).data); - loading.remove(); - return; - } - let duration; - //Get infologID if in case if it's an integrated infolog event - if (event_data.app === 'infolog') { - // Duration - infologs are always non-blocking - duration = dropEnd.whole_day ? 86400 - 1 : (event_widget.options.value.whole_day ? (egw().preference('defaultlength', 'calendar') * 60) : false); - // If it is an integrated infolog event we need to edit infolog entry - egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', [event_data.app_id, event_widget._parent.date_helper.getValue() || false, duration], function () { loading.remove(); }).sendRequest(true); - } - else { - //Edit calendar event - // Duration - check for whole day dropped on a time, change it to full days - duration = event_widget.options.value.whole_day && dropEnd.hour ? - // Make duration whole days, less 1 second - (Math.round((event_widget.options.value.end - event_widget.options.value.start) / (1000 * 86400)) * 86400) - 1 : - false; - // Event (whole day or not) dropped on whole day section, change to whole day non blocking - if (dropEnd.whole_day) - duration = 'whole_day'; - // Send the update - var _send = function (series_instance) { - var start = new Date(event_widget._parent.date_helper.getValue()); - egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ - button_id === 'series' ? event_data.id : event_data.app_id, event_data.owner, - start, - timegrid.options.owner || egw.user('account_id'), - duration, - series_instance - ], function () { loading.remove(); }).sendRequest(true); - }; - // Check for modifying a series that started before today - if (event_widget.options.value.recur_type && button_id === 'series') { - event_widget.series_split_prompt(function (_button_id) { - if (_button_id === et2_dialog.OK_BUTTON) { - _send(event_widget.options.value.recur_date); - } - else { - loading.remove(); - } - }); - } - else { - _send(event_widget.options.value.recur_date); - } - } - }); - } - } - } - /** - * Something changed, and the days need to be re-drawn. We wait a bit to - * avoid re-drawing twice if start and end date both changed, then recreate - * the days. - * The whole grid is not regenerated because times aren't expected to change, - * just the days. - * - * @param {boolean} [trigger=false] Trigger an event once things are done. - * Waiting until invalidate completes prevents 2 updates when changing the date range. - * @returns {undefined} - */ - invalidate(trigger) { - // Reset the list of days - this.day_list = []; - // Wait a bit to see if anything else changes, then re-draw the days - if (this.update_timer) { - window.clearTimeout(this.update_timer); - } - this.update_timer = window.setTimeout(jQuery.proxy(function () { - this.widget.update_timer = null; - window.clearTimeout(this.resize_timer); - this.widget.loader.hide().show(); - // Update actions - if (this.widget._actionManager) { - this.widget._link_actions(this.widget._actionManager.children); - } - this.widget._drawDays(); - // We have to completely re-do times, as they may have changed in - // scale to the point where more labels are needed / need to be removed - this.widget._drawTimes(); - if (this.trigger) { - this.widget.change(); - } - this.widget._updateNow(); - // Hide loader - window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this.widget), 200); - }, { widget: this, "trigger": trigger }), et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); - } - detachFromDOM() { - // Remove the binding to the change handler - jQuery(this.div).off(".et2_calendar_timegrid"); - return super.detachFromDOM(); - } - attachToDOM() { - let result = super.attachToDOM(); - // Add the binding for the event change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function (e) { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.push(this); - return e.data.event_change.apply(e.data, args); - }); - // Add the binding for the change handler - jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function (e) { - return e.data.change.call(e.data, e, this); - }); - // Catch resize and prevent it from bubbling further, triggering - // etemplate's resize - this.div.on('resize', this, function (e) { - e.stopPropagation(); - }); - return result; - } - getDOMNode(_sender) { - if (_sender === this || !_sender) { - return this.div ? this.div[0] : null; - } - else if (_sender.instanceOf(et2_calendar_daycol)) { - return this.days ? this.days[0] : null; - } - else if (_sender) { - return this.gridHeader ? this.gridHeader[0] : null; - } - } - set_disabled(disabled) { - var old_value = this.options.disabled; - super.set_disabled(disabled); - if (disabled) { - this.loader.show(); - } - else if (old_value !== disabled) { - // Scroll to start of day - stops jumping in FF - // For some reason on Chrome & FF this doesn't quite get the day start - // to the top, so add 2px; - this.scrolling.scrollTop(this._top_time + 2); - } - } - /** - * Update the 'now' line - * @private - */ - // @ts-ignore - _updateNow() { - let now = super._updateNow(); - if (now === false || this.options.granularity == 0 || !this.div.is(':visible')) { - this.now_div.hide(); - return false; - } - // Position & show line - let set_line = function (line, now, day) { - line.appendTo(day.getDOMNode()).show(); - let pos = day._time_to_position(now.getUTCHours() * 60 + now.getUTCMinutes()); - //this.now_div.position({my: 'left', at: 'left', of: day.getDOMNode()}); - line.css('top', pos + '%'); - }; - // Showing just 1 day, multiple owners - span all - if (this.daily_owner && this.day_list.length == 1) { - let day = this.day_widgets[0]; - set_line(this.now_div, now, day); - this.now_div.css('width', (this.day_widgets.length * 100) + '%'); - return true; - } - // Find the day of the week - for (var i = 0; i < this.day_widgets.length; i++) { - let day = this.day_widgets[i]; - if (day.getDate() >= now) { - day = this.day_widgets[i - 1]; - set_line(this.now_div, now, day); - this.now_div.css('width', '100%'); - break; - } - } - return true; - } - /** - * Clear everything, and redraw the whole grid - */ - _drawGrid() { - this.div.css('height', this.options.height) - .empty(); - this.loader.prependTo(this.div).show(); - // Draw in the horizontal - the times - this._drawTimes(); - // Draw in the vertical - the days - this.invalidate(); - } - /** - * Creates the DOM nodes for the times in the left column, and the horizontal - * lines (mostly via CSS) that span the whole date range. - */ - _drawTimes() { - jQuery('.calendar_calTimeRow', this.div).remove(); - this.div.toggleClass('calendar_calTimeGridList', this.options.granularity === 0); - this.gridHeader - .attr('data-date', this.options.start_date) - .attr('data-owner', this.options.owner) - .append(this._labelContainer) - .append(this.owner.getDOMNode()) - .append(this.dayHeader) - .appendTo(this.div); - // Max with 18 avoids problems when it's not shown - var header_height = Math.max(this.gridHeader.outerHeight(true), 18); - this.scrolling - .appendTo(this.div) - .off(); - // No time grid - list - if (this.options.granularity === 0) { - this.scrolling.css('height', '100%'); - this.days.css('height', '100%'); - this.iterateOver(function (day) { - day.resize(); - }, this, et2_calendar_daycol); - return; - } - var wd_start = 60 * this.options.day_start; - var wd_end = 60 * this.options.day_end; - var granularity = this.options.granularity; - var totalDisplayMinutes = wd_end - wd_start; - var rowsToDisplay = Math.ceil((totalDisplayMinutes + 60) / granularity); - var row_count = (1440 / this.options.granularity); - this.scrolling - .on('scroll', jQuery.proxy(this._scroll, this)); - // Percent - var rowHeight = (100 / rowsToDisplay).toFixed(1); - // Pixels - this.rowHeight = this.scrolling.height() / rowsToDisplay; - // We need a reasonable bottom limit here, but resize will handle it - // if we get too small - if (this.rowHeight < 5 && this.div.is(':visible')) { - if (this.rowHeight === 0) { - // Something is not right... - this.rowHeight = 5; - } - } - // the hour rows - var show = { - 5: [0, 15, 30, 45], - 10: [0, 30], - 15: [0, 30], - 45: [0, 15, 30, 45] - }; - var html = ''; - var line_height = parseInt(this.div.css('line-height')); - this._top_time = 0; - for (var t = 0, i = 0; t < 1440; t += granularity, ++i) { - if (t <= wd_start && t + granularity > wd_start) { - this._top_time = this.rowHeight * (i + 1 + (wd_start - (t + granularity)) / granularity); - } - var working_hours = (t >= wd_start && t < wd_end) ? ' calendar_calWorkHours' : ''; - html += '
      '; - // show time for full hours, always for 45min interval and at least on every 3 row - // @ts-ignore - var time = jQuery.datepicker.formatTime(egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm", { - hour: t / 60, - minute: t % 60, - seconds: 0, - timezone: 0 - }, { "ampm": (egw.preference("timeformat") === "12") }); - var time_label = (typeof show[granularity] === 'undefined' ? t % 60 === 0 : show[granularity].indexOf(t % 60) !== -1) ? time : ''; - if (time_label && egw.preference("timeformat") == "12" && time_label.split(':')[0] < 10) { - time_label = '  ' + time_label; - } - html += '
      ' + time_label + "
      \n"; - } - // Set heights in pixels for scrolling - jQuery('.calendar_calTimeLabels', this.scrolling) - .empty() - .height(this.rowHeight * i) - .append(html); - this.days.css('height', (this.rowHeight * i) + 'px'); - this.gridHover.css('height', this.rowHeight); - // Scroll to start of day - this.scrolling.scrollTop(this._top_time); - } - /** - * As window size and number of all day non-blocking events change, we need - * to re-scale the time grid to make sure the full working day is shown. - * - * We use a timeout to avoid doing it multiple times if redrawing or resizing. - */ - resizeTimes() { - // Hide resizing from user - this.loader.show(); - // Wait a bit to see if anything else changes, then re-draw the times - if (this.resize_timer) { - window.clearTimeout(this.resize_timer); - } - // No point if it is just going to be redone completely - if (this.update_timer) - return; - this.resize_timer = window.setTimeout(jQuery.proxy(function () { - if (this._resizeTimes) { - this.resize_timer = null; - this._resizeTimes(); - } - }, this), 1); - } - /** - * Re-scale the time grid to make sure the full working day is shown. - * This is the timeout callback that does the actual re-size immediately. - */ - _resizeTimes() { - if (!this.div.is(':visible')) { - return; - } - var wd_start = 60 * this.options.day_start; - var wd_end = 60 * this.options.day_end; - var totalDisplayMinutes = wd_end - wd_start; - var rowsToDisplay = Math.ceil((totalDisplayMinutes + 60) / this.options.granularity); - var row_count = (1440 / this.options.granularity); - var new_height = this.scrolling.height() / rowsToDisplay; - var old_height = this.rowHeight; - this.rowHeight = new_height; - jQuery('.calendar_calTimeLabels', this.scrolling).height(this.rowHeight * row_count); - this.days.css('height', this.options.granularity === 0 ? - '100%' : - (this.rowHeight * row_count) + 'px'); - // Scroll to start of day - this._top_time = (wd_start * this.rowHeight) / this.options.granularity; - // For some reason on Chrome & FF this doesn't quite get the day start - // to the top, so add 2px; - this.scrolling.scrollTop(this._top_time + 2); - if (this.rowHeight != old_height) { - this.iterateOver(function (child) { - if (child === this) - return; - child.resize(); - }, this, et2_IResizeable); - } - this.loader.hide(); - } - /** - * Set up the needed day widgets to correctly display the selected date - * range. First we calculate the needed dates, then we create any needed - * widgets. Existing widgets are recycled rather than discarded. - */ - _drawDays() { - this.scrolling.append(this.days); - // If day list is still empty, recalculate it from start & end date - if (this.day_list.length === 0 && this.options.start_date && this.options.end_date) { - this.day_list = this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend); - } - // For a single day, we show each owner in their own daycol - this.daily_owner = this.day_list.length === 1 && - this.options.owner.length > 1 && - this.options.owner.length < (parseInt('' + egw.preference('day_consolidate', 'calendar')) || 6); - var daycols_needed = this.daily_owner ? this.options.owner.length : this.day_list.length; - var day_width = (Math.min(jQuery(this.getInstanceManager().DOMContainer).width(), this.days.width()) / daycols_needed); - if (!day_width || !this.day_list) { - // Hidden on another tab, or no days for some reason - var dim = egw.getHiddenDimensions(this.days, false); - day_width = (dim.w / Math.max(daycols_needed, 1)); - } - // Create any needed widgets - otherwise, we'll just recycle - // Add any needed day widgets (now showing more days) - var add_index = 0; - var before = true; - while (daycols_needed > this.day_widgets.length) { - var existing_index = this.day_widgets[add_index] && !this.daily_owner ? - this.day_list.indexOf(this.day_widgets[add_index].options.date) : - -1; - before = existing_index > add_index; - var day = et2_createWidget('calendar-daycol', { - owner: this.options.owner, - width: (before ? 0 : day_width) + "px" - }, this); - if (this.isInTree()) { - day.doLoadingFinished(); - } - if (existing_index != -1 && parseInt(this.day_list[add_index]) < parseInt(this.day_list[existing_index])) { - this.day_widgets.unshift(day); - jQuery(this.getDOMNode(day)).prepend(day.getDOMNode(day)); - } - else { - this.day_widgets.push(day); - } - add_index++; - } - // Remove any extra day widgets (now showing less) - var delete_index = this.day_widgets.length - 1; - before = false; - while (this.day_widgets.length > daycols_needed) { - // If we're going down to an existing one, just keep it for cool CSS animation - while (delete_index > 1 && this.day_list.indexOf(this.day_widgets[delete_index].options.date) > -1) { - delete_index--; - before = true; - } - if (delete_index < 0) - delete_index = 0; - // Widgets that are before our date shrink, after just get pushed out - if (before) { - this.day_widgets[delete_index].set_width('0px'); - } - this.day_widgets[delete_index].div.hide(); - this.day_widgets[delete_index].header.hide(); - this.day_widgets[delete_index].destroy(); - this.day_widgets.splice(delete_index--, 1); - } - this.set_header_classes(); - // Create / update day widgets with dates and data - for (var i = 0; i < this.day_widgets.length; i++) { - day = this.day_widgets[i]; - // Position - day.set_left((day_width * i) + 'px'); - day.title.removeClass('blue_title'); - if (this.daily_owner) { - // Each 'day' is the same date, different user - day.set_id(this.day_list[0] + '-' + this.options.owner[i]); - day.set_date(this.day_list[0], false); - day.set_owner(this.options.owner[i]); - day.set_label(this._get_owner_name(this.options.owner[i])); - day.title.addClass('blue_title'); - } - else { - // Show user name in day header even if only one - if (this.day_list.length === 1) { - day.set_label(this._get_owner_name(this.options.owner)); - day.title.addClass('blue_title'); - } - else { - // Go back to self-calculated date by clearing the label - day.set_label(''); - } - day.set_id(this.day_list[i]); - day.set_date(this.day_list[i], this.value[this.day_list[i]] || false); - day.set_owner(this.options.owner); - } - day.set_width(day_width + 'px'); - } - // Adjust and scroll to start of day - this.resizeTimes(); - // Don't hold on to value any longer, use the data cache for best info - this.value = {}; - if (this.daily_owner) { - this.set_label(''); - } - // Handle not fully visible elements - this._scroll(); - // Set 'now' line - this._updateNow(); - // TODO: Figure out how to do this with detached nodes - /* - var nodes = this.day_col.getDetachedNodes(); - var supportedAttrs = []; - this.day_col.getDetachedAttributes(supportedAttrs); - supportedAttrs.push("id"); - - for(var i = 0; i < day_count; i++) - { - this.day_col.setDetachedAttributes(nodes.clone(),) - } - */ - } - /** - * Set header classes - * - */ - set_header_classes() { - var day; - let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar; - for (var i = 0; i < this.day_widgets.length; i++) { - day = this.day_widgets[i]; - // Classes - if (app_calendar && app_calendar.state && - this.day_list[i] && parseInt(this.day_list[i].substr(4, 2)) !== new Date(app_calendar.state.date).getUTCMonth() + 1) { - day.set_class('calendar_differentMonth'); - } - else { - day.set_class(''); - } - } - } - /** - * Update UI while scrolling within the selected time - * - * Toggles out of view indicators and adjusts not visible headers - * @param {Event} event Scroll event - */ - _scroll(event) { - if (!this.day_widgets) - return; - // Loop through days, let them deal with it - for (var day = 0; day < this.day_widgets.length; day++) { - this.day_widgets[day]._out_of_view(); - } - } - /** - * Calculate a list of days between start and end date, skipping weekends if - * desired. - * - * @param {Date|string} start_date Date that et2_date widget can understand - * @param {Date|string} end_date Date that et2_date widget can understand - * @param {boolean} show_weekend If not showing weekend, Saturday and Sunday - * will not be in the returned list. - * - * @returns {string[]} List of days in Ymd format - */ - _calculate_day_list(start_date, end_date, show_weekend) { - var day_list = []; - this.date_helper.set_value(end_date); - var end = this.date_helper.date.getTime(); - var i = 1; - this.date_helper.set_value(new Date(start_date)); - do { - if (show_weekend || !show_weekend && [0, 6].indexOf(this.date_helper.date.getUTCDay()) === -1 || end_date === start_date) { - day_list.push('' + this.date_helper.get_year() + sprintf('%02d', this.date_helper.get_month()) + sprintf('%02d', this.date_helper.get_date())); - } - this.date_helper.set_date(this.date_helper.get_date() + 1); - } - // Limit it to 14 days to avoid infinite loops in case something is mis-set, - // though the limit is more based on how wide the screen is - while (end >= this.date_helper.date.getTime() && i++ <= 14); - return day_list; - } - /** - * Link the actions to the DOM nodes / widget bits. - * - * @param {object} actions {ID: {attributes..}+} map of egw action information - */ - _link_actions(actions) { - // Get the parent? Might be a grid row, might not. Either way, it is - // just a container with no valid actions - var objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1); - objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId, 2) || objectManager; - var parent = objectManager.getObjectById(this.id, 1) || objectManager.getObjectById(this.getParent().id, 1) || objectManager; - if (!parent) { - debugger; - egw.debug('error', 'No parent objectManager found'); - return; - } - // This binds into the egw action system. Most user interactions (drag to move, resize) - // are handled internally using jQuery directly. - var widget_object = this._actionObject || parent.getObjectById(this.id); - var aoi = new et2_action_object_impl(this, this.getDOMNode(this)).getAOI(); - for (var i = 0; i < parent.children.length; i++) { - var parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div); - if (parent_finder.length > 0) { - parent = parent.children[i]; - break; - } - } - // Determine if we allow a dropped event to use the invite/change actions - let _invite_enabled = function (action, event, target) { - var event = event.iface.getWidget(); - var timegrid = target.iface.getWidget() || false; - if (event === timegrid || !event || !timegrid || - !event.options || !event.options.value.participants || !timegrid.options.owner) { - return false; - } - var owner_match = false; - var own_timegrid = event.getParent().getParent() === timegrid && !timegrid.daily_owner; - for (var id in event.options.value.participants) { - if (!timegrid.daily_owner) { - if (timegrid.options.owner === id || - timegrid.options.owner.indexOf && - timegrid.options.owner.indexOf(id) >= 0) { - owner_match = true; - } - } - else { - timegrid.iterateOver(function (col) { - // Check scroll section or header section - if (col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) { - owner_match = owner_match || col.options.owner.indexOf(id) !== -1; - own_timegrid = (col === event.getParent()); - } - }, this, et2_calendar_daycol); - } - } - var enabled = !owner_match && - // Not inside its own timegrid - !own_timegrid; - widget_object.getActionLink('invite').enabled = enabled; - widget_object.getActionLink('change_participant').enabled = enabled; - // If invite or change participant are enabled, drag is not - widget_object.getActionLink('egw_link_drop').enabled = !enabled; - }; - aoi.doTriggerEvent = function (_event, _data) { - // Determine target node - var event = _data.event || false; - if (!event) - return; - if (_data.ui.draggable.hasClass('rowNoEdit')) - return; - /* - We have to handle the drop in the normal event stream instead of waiting - for the egwAction system so we can get the helper, and destination - */ - if (event.type === 'drop') { - var dropEnd = false; - var helper = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0]; - if (helper && helper.dropEnd && helper.dropEnd.length >= 1) - if (typeof this.dropEnd !== 'undefined' && this.dropEnd.length >= 1) { - dropEnd = helper.dropEnd[0].dataset || false; - } - this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper)[0], this.getWidget(), event, _data.ui, dropEnd); - } - var drag_listener = function (_event, ui) { - aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', ui.helper)[0], ui.helper[0], 0); - if (aoi.getWidget().daily_owner) { - _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); - } - }; - var time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.helper); - switch (_event) { - // Triggered once, when something is dragged into the timegrid's div - case EGW_AI_DRAG_OVER: - // Listen to the drag and update the helper with the time - // This part lets us drag between different timegrids - _data.ui.draggable.on('drag.et2_timegrid' + widget_object.id, drag_listener); - _data.ui.draggable.on('dragend.et2_timegrid' + widget_object.id, function () { - _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); - }); - // Remove formatting for out-of-view events (full day non-blocking) - jQuery('.calendar_calEventHeader', _data.ui.helper).css('top', ''); - jQuery('.calendar_calEventBody', _data.ui.helper).css('padding-top', ''); - // Disable invite / change actions for same calendar or already participant - var event = _data.ui.draggable.data('selected')[0]; - if (!event || event.id && event.id.indexOf('calendar') !== 0) { - event = false; - } - if (event) { - _invite_enabled(widget_object.getActionLink('invite').actionObj, event, widget_object); - } - if (time.length) { - // The out will trigger after the over, so we count - time.data('count', time.data('count') + 1); - } - else { - _data.ui.helper.prepend('
      '); - } - break; - // Triggered once, when something is dragged out of the timegrid - case EGW_AI_DRAG_OUT: - // Stop listening - _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); - // Remove highlighted time square - var timegrid = aoi.getWidget(); - timegrid.gridHover.hide(); - timegrid.scrolling.scrollTop(timegrid._top_time); - // Out triggers after the over, count to not accidentally remove - time.data('count', time.data('count') - 1); - if (time.length && time.data('count') <= 0) { - time.remove(); - } - break; - } - }; - if (widget_object == null) { - // Add a new container to the object manager which will hold the widget - // objects - widget_object = parent.insertObject(false, new egwActionObject(this.id, parent, aoi, this._actionManager || parent.manager.getActionById(this.id) || parent.manager)); - } - else { - widget_object.setAOI(aoi); - } - this._actionObject = widget_object; - // Delete all old objects - widget_object.clear(); - widget_object.unregisterActions(); - // Go over the widget & add links - this is where we decide which actions are - // 'allowed' for this widget at this time - var action_links = this._get_action_links(actions); - this._init_links_dnd(widget_object.manager, action_links); - widget_object.updateActionLinks(action_links); - } - /** - * Automatically add dnd support for linking - * - * @param {type} mgr - * @param {type} actionLinks - */ - _init_links_dnd(mgr, actionLinks) { - if (this.options.readonly) - return; - var self = this; - var drop_link = mgr.getActionById('egw_link_drop'); - var drop_change_participant = mgr.getActionById('change_participant'); - var drop_invite = mgr.getActionById('invite'); - var drag_action = mgr.getActionById('egw_link_drag'); - // Check if this app supports linking - if (!egw.link_get_registry(this.dataStorePrefix, 'query') || - egw.link_get_registry(this.dataStorePrefix, 'title')) { - if (drop_link) { - drop_link.remove(); - if (actionLinks.indexOf(drop_link.id) >= 0) { - actionLinks.splice(actionLinks.indexOf(drop_link.id), 1); - } - } - if (drag_action) { - drag_action.remove(); - if (actionLinks.indexOf(drag_action.id) >= 0) { - actionLinks.splice(actionLinks.indexOf(drag_action.id), 1); - } - } - return; - } - // Don't re-add - if (drop_link == null) { - // Create the drop action that links entries - drop_link = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function (action, source, target) { - // Extract link IDs - var links = []; - var id = ''; - for (var i = 0; i < source.length; i++) { - // Check for no ID (invalid) or same manager (dragging an event) - if (!source[i].id) - continue; - if (source[i].manager === target.manager) { - // Find the timegrid, could have dropped on an event - var timegrid = target.iface.getWidget(); - while (target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) { - target = target.parent; - timegrid = target.iface.getWidget(); - } - if (timegrid && timegrid._drop_data) { - timegrid._event_drop.call(source[i].iface.getDOMNode(), timegrid, null, action.ui, timegrid._drop_data); - } - timegrid._drop_data = false; - // Ok, stop. - return false; - } - id = source[i].id.split('::'); - links.push({ app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1] }); - } - if (links.length && target && target.iface.getWidget() && target.iface.getWidget().instanceOf(et2_calendar_event)) { - // Link the entries - egw.json(self.egw().getAppName() + ".etemplate_widget_link.ajax_link.etemplate", target.id.split('::').concat([links]), function (result) { - if (result) { - this.egw().message('Linked'); - } - }, self, true, self).sendRequest(); - } - else if (links.length) { - // Get date and time - var params = jQuery.extend({}, jQuery('.drop-hover[data-date]', target.iface.getDOMNode())[0].dataset || {}); - // Add link IDs - var app_registry = egw.link_get_registry('calendar'); - params[app_registry.add_app] = []; - params[app_registry.add_id] = []; - for (var n in links) { - params[app_registry.add_app].push(links[n].app); - params[app_registry.add_id].push(links[n].id); - } - app.calendar.add(params); - } - }, true); - drop_link.acceptedTypes = ['default', 'link']; - drop_link.hideOnDisabled = true; - // Create the drop action for moving events between calendars - var invite_action = function (action, source, target) { - // Extract link IDs - var links = []; - var id = ''; - for (var i = 0; i < source.length; i++) { - // Check for no ID (invalid) or same manager (dragging an event) - if (!source[i].id) - continue; - if (source[i].manager === target.manager) { - // Find the timegrid, could have dropped on an event - var timegrid = target.iface.getWidget(); - while (target.parent && timegrid.instanceOf && !timegrid.instanceOf(et2_calendar_timegrid)) { - target = target.parent; - timegrid = target.iface.getWidget(); - } - // Leave the helper there until the update is done - var loading = action.ui.helper.clone(true).appendTo(jQuery('body')); - // and add a loading icon so user knows something is happening - if (jQuery('.calendar_timeDemo', loading).length == 0) { - jQuery('.calendar_calEventHeader', loading).addClass('loading'); - } - else { - jQuery('.calendar_timeDemo', loading).after('
      '); - } - var event_data = egw.dataGetUIDdata(source[i].id).data; - et2_calendar_event.recur_prompt(event_data, function (button_id) { - if (button_id === 'cancel' || !button_id) { - return; - } - var add_owner = jQuery.extend([], timegrid.options.owner); - if (timegrid.daily_owner) { - timegrid.iterateOver(function (col) { - if (col.div.has(timegrid.gridHover).length || col.header.has(timegrid.gridHover).length) { - add_owner = col.options.owner; - } - }, this, et2_calendar_daycol); - } - egw().json('calendar.calendar_uiforms.ajax_invite', [ - button_id === 'series' ? event_data.id : event_data.app_id, - add_owner, - action.id === 'change_participant' ? - jQuery.extend([], source[i].iface.getWidget().getParent().options.owner) : - [] - ], function () { loading.remove(); }).sendRequest(true); - }); - // Ok, stop. - return false; - } - } - }; - drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action, true); - drop_change_participant.acceptedTypes = ['calendar']; - drop_change_participant.hideOnDisabled = true; - drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action, true); - drop_invite.acceptedTypes = ['calendar']; - drop_invite.hideOnDisabled = true; - } - if (actionLinks.indexOf(drop_link.id) < 0) { - actionLinks.push(drop_link.id); - } - actionLinks.push(drop_invite.id); - actionLinks.push(drop_change_participant.id); - // Don't re-add - if (drag_action == null) { - // Create drag action that allows linking - drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function (action, selected) { - // Drag helper - list titles. - // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links - // TODO: Need to decide if we need to create a customized helper interface for links anyway - //return helper; - return null; - }, true); - } - // The timegrid itself is not draggable, so don't add a link. - // The action is there for the children (events) to use - if (false && actionLinks.indexOf(drag_action.id) < 0) { - actionLinks.push(drag_action.id); - } - drag_action.set_dragType(['link', 'calendar']); - } - /** - * Get all action-links / id's of 1.-level actions from a given action object - * - * Here we are only interested in drop events. - * - * @param actions - * @returns {Array} - */ - _get_action_links(actions) { - var action_links = []; - // TODO: determine which actions are allowed without an action (empty actions) - for (var i in actions) { - var action = actions[i]; - if (action.type == 'drop') { - action_links.push(typeof action.id != 'undefined' ? action.id : i); - } - } - return action_links; - } - /** - * Provide specific data to be displayed. - * This is a way to set start and end dates, owner and event data in one call. - * - * Events will be retrieved automatically from the egw.data cache, so there - * is no great need to provide them. - * - * @param {Object[]} events Array of events, indexed by date in Ymd format: - * { - * 20150501: [...], - * 20150502: [...] - * } - * Days should be in order. - * {string|number|Date} events.start_date - New start date - * {string|number|Date} events.end_date - New end date - * {number|number[]|string|string[]} event.owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - */ - set_value(events) { - if (typeof events !== 'object') - return false; - var use_days_sent = true; - if (events.start_date) { - use_days_sent = false; - } - if (events.end_date) { - use_days_sent = false; - } - super.set_value(events); - if (use_days_sent) { - var day_list = Object.keys(events); - if (day_list.length) { - this.set_start_date(day_list[0]); - this.set_end_date(day_list[day_list.length - 1]); - } - // Sub widgets actually get their own data from egw.data, so we'll - // stick it there - var consolidated = et2_calendar_view.is_consolidated(this.options.owner, this.day_list.length == 1 ? 'day' : 'week'); - for (var day in events) { - let day_list = []; - for (var i = 0; i < events[day].length; i++) { - day_list.push(events[day][i].row_id); - egw.dataStoreUID('calendar::' + events[day][i].row_id, events[day][i]); - } - // Might be split by user, so we have to check that too - for (var i = 0; i < this.options.owner.length; i++) { - var owner = consolidated ? this.options.owner : this.options.owner[i]; - var day_id = CalendarApp._daywise_cache_id(day, owner); - egw.dataStoreUID(day_id, day_list); - if (consolidated) - break; - } - } - } - // Reset and calculate instead of just use the keys so we can get the weekend preference - this.day_list = []; - // None of the above changed anything, hide the loader - if (!this.update_timer) { - window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this), 200); - } - } - /** - * Set which user owns this. Owner is passed along to the individual - * days. - * - * @param {number|number[]} _owner Account ID - * @returns {undefined} - */ - set_owner(_owner) { - var old = this.options.owner || 0; - super.set_owner(_owner); - this.owner.set_label(''); - this.div.removeClass('calendar_TimeGridNoLabel'); - // Check to see if it's our own calendar, with just us showing - if (typeof _owner == 'object' && _owner.length == 1) { - var rowCount = 0; - this.getParent().iterateOver(function (widget) { - if (!widget.disabled) - rowCount++; - }, this, et2_calendar_timegrid); - // Just us, show week number - if (rowCount == 1 && _owner.length == 1 && _owner[0] == egw.user('account_id') || rowCount != 1) - _owner = false; - } - var day_count = this.day_list.length ? this.day_list.length : - this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend).length; - // @ts-ignore - if (typeof _owner == 'string' && isNaN(_owner)) { - this.set_label(''); - this.owner.set_value(this._get_owner_name(_owner)); - // Label is empty, but give extra space for the owner name - this.div.removeClass('calendar_TimeGridNoLabel'); - } - else if (!_owner || typeof _owner == 'object' && _owner.length > 1 || - // Single owner, single day - _owner.length === 1 && day_count === 1) { - // Don't show owners if more than one, show week number - this.owner.set_value(''); - if (this.options.start_date) { - this.set_label(egw.lang('wk') + ' ' + - (app.calendar ? app.calendar.date.week_number(this.options.start_date) : '')); - } - } - else { - this.owner.options.application = 'api-accounts'; - this.owner.set_value(this._get_owner_name(_owner)); - this.set_label(''); - jQuery(this.getDOMNode(this.owner)).prepend(this.owner.getDOMNode()); - } - if (this.isAttached() && (typeof old === "number" && typeof _owner === "number" && old !== this.options.owner || - // Array of ids will not compare as equal - ((typeof old === 'object' || typeof _owner === 'object') && old.toString() !== _owner.toString()) || - // Strings - typeof old === 'string' && '' + old !== '' + this.options.owner)) { - this.invalidate(true); - } - } - /** - * Set a label for this week - * - * May conflict with owner, which is displayed when there's only one owner. - * - * @param {string} label - */ - set_label(label) { - this.options.label = label; - this._labelContainer.html(label); - this.gridHeader.prepend(this._labelContainer); - // If it's a short label (eg week number), don't give it an extra line - // but is empty, but give extra space for a single owner name - this.div.toggleClass('calendar_TimeGridNoLabel', label.trim().length > 0 && label.trim().length <= 6 || - this.options.owner.length > 1); - } - /** - * Set how big the time divisions are - * - * Setting granularity to 0 will remove the time divisions and display - * each days events in a list style. This 'gridlist' is not to be confused - * with the list view, which uses a nextmatch. - * - * @param {number} minutes - */ - set_granularity(minutes) { - // Avoid < 0 - minutes = Math.max(0, minutes); - if (this.options.granularity !== minutes) { - if (this.options.granularity === 0 || minutes === 0) { - this.options.granularity = minutes; - // Need to re-do a bunch to make sure this is propagated - this.invalidate(); - } - else { - this.options.granularity = minutes; - this._drawTimes(); - } - } - else if (!this.update_timer) { - this.resizeTimes(); - } - } - /** - * Turn on or off the visibility of weekends - * - * @param {boolean} weekends - */ - set_show_weekend(weekends) { - weekends = weekends ? true : false; - if (this.options.show_weekend !== weekends) { - this.options.show_weekend = weekends; - if (this.isAttached()) { - this.invalidate(); - } - } - } - /** - * Call change handler, if set - */ - change() { - if (this.onchange) { - if (typeof this.onchange == 'function') { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.push(this); - return this.onchange.apply(this, args); - } - else { - return (et2_compileLegacyJS(this.options.onchange, this, _node))(); - } - } - } - /** - * Call event change handler, if set - * - * @param {type} event - * @param {type} dom_node - */ - event_change(event, dom_node) { - if (this.onevent_change) { - var event_data = this._get_event_info(dom_node); - var event_widget = this.getWidgetById(event_data.widget_id); - et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function (button_id, event_data) { - // No need to continue - if (button_id === 'cancel') - return false; - if (typeof this.onevent_change == 'function') { - // Make sure function gets a reference to the widget - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(event_widget) == -1) - args.push(event_widget); - // Put button ID in event - event.button_id = button_id; - return this.onevent_change.apply(this, [event, event_widget, button_id]); - } - else { - return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))(); - } - }, this)); - } - return false; - } - get_granularity() { - // get option, or user's preference - if (typeof this.options.granularity === 'undefined') { - this.options.granularity = egw.preference('interval', 'calendar') || 30; - } - return parseInt(this.options.granularity); - } - /** - * Click handler calling custom handler set via onclick attribute to this.onclick - * - * This also handles all its own actions, including navigation. If there is - * an event associated with the click, it will be found and passed to the - * onclick function. - * - * @param {Event} _ev - * @returns {boolean} Continue processing event (true) or stop (false) - */ - click(_ev) { - var result = true; - if (this.options.readonly) - return; - // Drag to create in progress - if (this.drag_create.start !== null) - return; - // Is this click in the event stuff, or in the header? - if (_ev.target.dataset.id || jQuery(_ev.target).parents('.calendar_calEvent').length) { - // Event came from inside, maybe a calendar event - var event = this._get_event_info(_ev.originalEvent.target); - if (typeof this.onclick == 'function') { - // Make sure function gets a reference to the widget, splice it in as 2. argument if not - var args = Array.prototype.slice.call(arguments); - if (args.indexOf(this) == -1) - args.splice(1, 0, this); - result = this.onclick.apply(this, args); - } - var event_node = jQuery(event.event_node); - if (event.id && result && !this.disabled && !this.options.readonly && - // Permissions - opening will fail if we try - event_node && !(event_node.hasClass('rowNoView'))) { - if (event.widget_id && this.getWidgetById(event.widget_id)) { - this.getWidgetById(event.widget_id).recur_prompt(); - } - else { - et2_calendar_event.recur_prompt(event); - } - return false; - } - return result; - } - else if (this.gridHeader.is(_ev.target) && _ev.target.dataset || - this._labelContainer.is(_ev.target) && this.gridHeader[0].dataset) { - app.calendar.update_state(jQuery.extend({ view: 'week' }, this._labelContainer.is(_ev.target) ? - this.gridHeader[0].dataset : - _ev.target.dataset)); - } - else if (this.options.owner.length === 1 && jQuery(this.owner.getDOMNode()).is(_ev.target)) { - // Click on the owner in header, show just that owner - app.calendar.update_state({ owner: this.options.owner }); - } - else if (this.dayHeader.has(_ev.target).length) { - // Click on a day header - let day deal with it - // First child is a selectAccount - for (var i = 1; i < this._children.length; i++) { - if (this._children[i].header && (this._children[i].header.has(_ev.target).length || this._children[i].header.is(_ev.target))) { - return this._children[i].click(_ev); - } - } - } - // No time grid, click on a day - else if (this.options.granularity === 0 && - (jQuery(_ev.target).hasClass('event_wrapper') || jQuery(_ev.target).hasClass('.calendar_calDayCol'))) { - // Default handler to open a new event at the selected time - var target = jQuery(_ev.target).hasClass('event_wrapper') ? _ev.target.parentNode : _ev.target; - var options = { - date: target.dataset.date || this.options.date, - hour: target.dataset.hour || this._parent.options.day_start, - minute: target.dataset.minute || 0, - owner: this.options.owner - }; - app.calendar.add(options); - return false; - } - } - /** - * Mousedown handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_down(event) { - if (event.which !== 1) - return; - if (this.options.readonly) - return; - var start = jQuery.extend({}, this.gridHover[0].dataset); - if (start.date) { - // Set parent for event - if (this.daily_owner) { - // Each 'day' is the same date, different user - // Find the correct row so we know the parent - var col = event.target.closest('.calendar_calDayCol'); - for (var i = 0; i < this._children.length && col; i++) { - if (this._children[i].node === col) { - this.drag_create.parent = this._children[i]; - break; - } - } - } - else { - this.drag_create.parent = this.getWidgetById(start.date); - } - // Format date - this.date_helper.set_year(start.date.substring(0, 4)); - this.date_helper.set_month(start.date.substring(4, 6)); - this.date_helper.set_date(start.date.substring(6, 8)); - if (start.hour) { - this.date_helper.set_hours(start.hour); - } - if (start.minute) { - this.date_helper.set_minutes(start.minute); - } - start.date = this.date_helper.get_value(); - this.gridHover.css('cursor', 'ns-resize'); - // Start update - var timegrid = this; - this.div.on('mousemove.dragcreate', function () { - if (timegrid.drag_create.event && timegrid.drag_create.parent && timegrid.drag_create.end) { - var end = jQuery.extend({}, timegrid.gridHover[0].dataset); - if (end.date) { - timegrid.date_helper.set_year(end.date.substring(0, 4)); - timegrid.date_helper.set_month(end.date.substring(4, 6)); - timegrid.date_helper.set_date(end.date.substring(6, 8)); - if (end.hour) { - timegrid.date_helper.set_hours(end.hour); - } - if (end.minute) { - timegrid.date_helper.set_minutes(end.minute); - } - timegrid.drag_create.end.date = timegrid.date_helper.get_value(); - } - try { - timegrid._drag_update_event(); - } - catch (e) { - timegrid._drag_create_end(); - } - } - }); - } - return this._drag_create_start(start); - } - /** - * Mouseup handler to support drag to create - * - * @param {jQuery.Event} event - */ - _mouse_up(event) { - if (this.options.readonly) - return; - var end = jQuery.extend({}, this.gridHover[0].dataset); - if (end.date) { - this.date_helper.set_year(end.date.substring(0, 4)); - this.date_helper.set_month(end.date.substring(4, 6)); - this.date_helper.set_date(end.date.substring(6, 8)); - if (end.hour) { - this.date_helper.set_hours(end.hour); - } - if (end.minute) { - this.date_helper.set_minutes(end.minute); - } - end.date = this.date_helper.get_value(); - } - this.div.off('mousemove.dragcreate'); - this.gridHover.css('cursor', ''); - return this._drag_create_end(this.drag_create.event ? end : undefined); - } - /** - * Get time from position for drag and drop - * - * This does not return an actual time on a clock, but finds the closest - * time node (.calendar_calAddEvent or day column) to the given position. - * - * @param {number} x - * @param {number} y - * @returns {DOMNode[]} time node(s) for the given position - */ - _get_time_from_position(x, y) { - x = Math.round(x); - y = Math.round(y); - var path = []; - var day = null; - var time = null; - var node = document.elementFromPoint(x, y); - var $node = jQuery(node); - // Ignore high level & non-time (grid itself, header parent & week label) - if ([this.node, this.gridHeader[0], this._labelContainer[0]].indexOf(node) !== -1 || - // Day labels - this.gridHeader.has(node).length && !$node.hasClass("calendar_calDayColAllDay") && !$node.hasClass('calendar_calDayColHeader')) { - return []; - } - for (var id in this.gridHover[0].dataset) { - delete this.gridHover[0].dataset[id]; - } - if (this.options.granularity == 0) { - this.gridHover.css('height', ''); - } - while (node && node != this.node && node.tagName != 'BODY' && path.length < 10) { - path.push(node); - node.style.display = 'none'; - $node = jQuery(node); - if ($node.hasClass('calendar_calDayColHeader')) { - for (var id in node.dataset) { - this.gridHover[0].dataset[id] = node.dataset[id]; - } - this.gridHover.css({ - position: 'absolute', - top: '', - bottom: '0px', - // Use 100% height if we're hiding the day labels to avoid - // any remaining space from the hidden labels - height: $node.height() > parseInt($node.css('line-height')) ? - $node.css('padding-bottom') : '100%' - }); - day = node; - this.gridHover - .attr('data-non_blocking', 'true'); - break; - } - if ($node.hasClass('calendar_calDayCol')) { - day = node; - this.gridHover - .attr('data-date', day.dataset.date); - } - if ($node.hasClass('calendar_calTimeRowTime')) { - time = node; - this.gridHover - .attr('data-hour', time.dataset.hour) - .attr('data-minute', time.dataset.minute); - break; - } - node = document.elementFromPoint(x, y); - } - for (var i = 0; i < path.length; i++) { - path[i].style.display = ''; - } - if (!day) { - return []; - } - this.gridHover - .show() - .appendTo(day); - if (time) { - this.gridHover - .height(this.rowHeight) - .position({ my: 'left top', at: 'left top', of: time }); - } - this.gridHover.css('left', ''); - return this.gridHover; - } - /** - * Code for implementing et2_IDetachedDOM - * - * @param {array} _attrs array to add further attributes to - */ - getDetachedAttributes(_attrs) { - _attrs.push('start_date', 'end_date'); - } - getDetachedNodes() { - return [this.getDOMNode(this)]; - } - setDetachedAttributes(_nodes, _values) { - this.div = jQuery(_nodes[0]); - if (_values.start_date) { - this.set_start_date(_values.start_date); - } - if (_values.end_date) { - this.set_end_date(_values.end_date); - } - } - // Resizable interface - /** - * @param {boolean} [_too_small=null] Force the widget to act as if it was too small - */ - resize(_too_small) { - if (this.disabled || !this.div.is(':visible')) { - return; - } - /* - We expect the timegrid to be in a table with 0 or more other timegrids, - 1 per row. We want each timegrid to be as large as possible, but space - shared equally. Height can't be set to a percentage on the rows, because - that doesn't work. However, if any timegrid is too small (1/2 hour < 1 line - height), we change to showing only the working hours with no vertical - scrollbar. Each week gets as much space as it needs, and all scroll together. - */ - // How many rows? - var rowCount = 0; - this.getParent().iterateOver(function (widget) { - if (!widget.disabled) - rowCount++; - }, this, et2_calendar_timegrid); - // Take the whole tab height, or home portlet - if (this.getInstanceManager().app === 'home') { - var height = jQuery(this.getParent().getDOMNode(this)).parentsUntil('.et2_portlet').last().parent().innerHeight(); - // Allow for portlet header - height -= jQuery('.ui-widget-header', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); - } - else { - var height = jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight(); - // Allow for toolbar - height -= jQuery('#calendar-toolbar', this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true); - } - this.options.height = Math.floor(height / rowCount); - // Allow for borders & padding - this.options.height -= 2 * ((this.div.outerWidth(true) - this.div.innerWidth()) + parseInt(this.div.parent().css('padding-top'))); - // Calculate how much space is needed, and - // if too small be bigger - var needed = ((this.day_end - this.day_start) / - (this.options.granularity / 60) * parseInt(this.div.css('line-height'))) + - this.gridHeader.outerHeight(); - var too_small = needed > this.options.height && this.options.granularity != 0; - if (this.getInstanceManager().app === 'home') { - var modify_node = jQuery(this.getParent().getDOMNode(this)).parentsUntil('.et2_portlet').last(); - } - else { - var modify_node = jQuery(this.getInstanceManager().DOMContainer); - } - modify_node - .css({ - 'overflow-y': too_small || _too_small ? 'auto' : 'hidden', - 'overflow-x': 'hidden', - 'height': too_small || _too_small ? height : '100%' - }); - if (too_small || _too_small) { - this.options.height = Math.max(this.options.height, needed); - // Set all others to match - if (!_too_small && rowCount > 1 && this.getParent()) { - window.setTimeout(jQuery.proxy(function () { - if (!this._parent) - return; - this._parent.iterateOver(function (widget) { - if (!widget.disabled) - widget.resize(true); - }, this, et2_calendar_timegrid); - }, this), 1); - return; - } - this.div.addClass('calendar_calTimeGridFixed'); - } - else { - this.div.removeClass('calendar_calTimeGridFixed'); - } - this.div.css('height', this.options.height); - // Re-do time grid - if (!this.update_timer) { - this.resizeTimes(); - } - // Try to resize width, though animations cause problems - var total_width = modify_node.parent().innerWidth() - this.days.position().left; - // Space for todos, if there - total_width -= jQuery(this.getInstanceManager().DOMContainer).siblings().has(':visible').not('#calendar-toolbar').outerWidth(); - var day_width = (total_width > 0 ? total_width : modify_node.width()) / this.day_widgets.length; - // update day widgets - for (var i = 0; i < this.day_widgets.length; i++) { - var day = this.day_widgets[i]; - // Position - day.set_left((day_width * i) + 'px'); - day.set_width(day_width + 'px'); - } - } - /** - * Set up for printing - * - * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up - * (waiting for data) - */ - beforePrint() { - if (this.disabled || !this.div.is(':visible')) { - return; - } - var height_check = this.div.height(); - this.div.css('max-height', '17cm'); - if (this.div.height() != height_check) { - this.div.height('17cm'); - this._resizeTimes(); - } - // update day widgets, if not on single day view - // - // TODO: Find out why don't we update single day view - // Let the single day view participate in print calculation. - if (this.day_widgets.length > 0) { - var day_width = (100 / this.day_widgets.length); - for (var i = 0; i < this.day_widgets.length; i++) { - var day = this.day_widgets[i]; - // Position - day.set_left((i * day_width) + '%'); - day.set_width(day_width + '%'); - // For some reason the column's method does not set it correctly in Chrome - day.header[0].style.width = day_width + '%'; - } - } - // Stop Firefox from scrolling the day to the top - this would break printing in Chrome - if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) { - var height = this.scrolling.scrollTop() + this.scrolling.height(); - this.scrolling - // Disable scroll event, or it will recalculate out of view events - .off('scroll') - // Explicitly transform to the correct place - .css({ - 'transform': 'translateY(-' + this.scrolling.scrollTop() + 'px)', - 'margin-bottom': '-' + this.scrolling.scrollTop() + 'px', - 'height': height + 'px' - }); - this.div.css({ 'height': '', 'max-height': '' }); - } - } - /** - * Reset after printing - */ - afterPrint() { - this.div.css('maxHeight', ''); - this.scrolling.children().css({ 'transform': '', 'overflow': '' }); - this.div.height(this.options.height); - if (navigator.userAgent.match(/(firefox|safari|iceweasel)/i) && !navigator.userAgent.match(/chrome/i)) { - this._resizeTimes(); - this.scrolling - // Re-enable out-of-view formatting on scroll - .on('scroll', jQuery.proxy(this._scroll, this)) - // Remove translation - .css({ 'transform': '', 'margin-bottom': '' }); - } - } -} -et2_calendar_timegrid._attributes = { - value: { - type: "any", - description: "An array of events, indexed by date (Ymd format)." - }, - day_start: { - name: "Day start time", - type: "string", - default: parseInt('' + egw.preference('workdaystarts', 'calendar')) || 9, - description: "Work day start time. If unset, this will default to the current user's preference" - }, - day_end: { - name: "Day end time", - type: "string", - default: parseInt('' + egw.preference('workdayends', 'calendar')) || 17, - description: "Work day end time. If unset, this will default to the current user's preference" - }, - show_weekend: { - name: "Weekends", - type: "boolean", - // @ts-ignore - default: egw.preference('days_in_weekview', 'calendar') != 5, - description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown." - }, - granularity: { - name: "Granularity", - type: "integer", - default: parseInt('' + egw.preference('interval', 'calendar')) || 30, - description: "How many minutes per row, or 0 to display events as a list" - }, - "onchange": { - "name": "onchange", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when the date range changes." - }, - "onevent_change": { - "name": "onevent_change", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when an event changes." - }, - height: { - "default": '100%' - } -}; -et2_register_widget(et2_calendar_timegrid, ["calendar-timegrid"]); -//# sourceMappingURL=et2_widget_timegrid.js.map \ No newline at end of file diff --git a/calendar/js/et2_widget_view.js b/calendar/js/et2_widget_view.js deleted file mode 100644 index 13fb1ed9d8..0000000000 --- a/calendar/js/et2_widget_view.js +++ /dev/null @@ -1,590 +0,0 @@ -/* - * Egroupware - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ -/*egw:uses - /etemplate/js/et2_core_valueWidget; -*/ -import { et2_createWidget } from "../../api/js/etemplate/et2_core_widget"; -import { et2_valueWidget } from "../../api/js/etemplate/et2_core_valueWidget"; -import { ClassWithAttributes } from "../../api/js/etemplate/et2_core_inheritance"; -import { sprintf } from "../../api/js/egw_action/egw_action_common.js"; -/** - * Parent class for the various calendar views to reduce copied code - * - * - * et2_calendar_view is responsible for its own loader div, which is displayed while - * the times & days are redrawn. - * - * @augments et2_valueWidget - */ -export class et2_calendar_view extends et2_valueWidget { - /** - * Constructor - * - */ - constructor(_parent, _attrs, _child) { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_view._attributes, _child || {})); - this.dataStorePrefix = 'calendar'; - this.update_timer = null; - this.now_timer = null; - // Used for its date calculations - this._date_helper = et2_createWidget('date-time', {}, null); - this._date_helper.loadingFinished(); - this.loader = jQuery('
      '); - this.now_div = jQuery('
      '); - this.update_timer = null; - this.now_timer = null; - // Used to support dragging on empty space to create an event - this.drag_create = { - start: null, - end: null, - parent: null, - event: null - }; - } - destroy() { - super.destroy(); - // date_helper has no parent, so we must explicitly remove it - this._date_helper.destroy(); - this._date_helper = null; - // Stop the invalidate timer - if (this.update_timer) { - window.clearTimeout(this.update_timer); - } - // Stop the 'now' line - if (this.now_timer) { - window.clearInterval(this.now_timer); - } - } - doLoadingFinished() { - super.doLoadingFinished(); - this.loader.hide(0).prependTo(this.div); - this.div.append(this.now_div); - if (this.options.owner) - this.set_owner(this.options.owner); - // Start moving 'now' line - this.now_timer = window.setInterval(this._updateNow.bind(this), 60000); - return true; - } - /** - * Something changed, and the view need to be re-drawn. We wait a bit to - * avoid re-drawing twice if start and end date both changed, then recreate - * as needed. - * - * @param {boolean} [trigger_event=false] Trigger an event once things are done. - * Waiting until invalidate completes prevents 2 updates when changing the date range. - * @returns {undefined} - * - * @memberOf et2_calendar_view - */ - invalidate(trigger_event) { - // If this wasn't a stub, we'd set this.update_timer - } - /** - * Returns the current start date - * - * @returns {Date} - * - * @memberOf et2_calendar_view - */ - get_start_date() { - return new Date(this.options.start_date); - } - /** - * Returns the current start date - * - * @returns {Date} - * - * @memberOf et2_calendar_view - */ - get_end_date() { - return new Date(this.options.end_date); - } - /** - * Change the start date - * - * Changing the start date will invalidate the display, and it will be redrawn - * after a timeout. - * - * @param {string|number|Date} new_date New starting date. Strings can be in - * any format understood by et2_widget_date, or Ymd (eg: 20160101). - * @returns {undefined} - * - * @memberOf et2_calendar_view - */ - set_start_date(new_date) { - if (!new_date || new_date === null) { - new_date = new Date(); - } - // Use date widget's existing functions to deal - if (typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) { - this._date_helper.set_value(new_date); - } - else if (typeof new_date === "string") { - this._date_helper.set_year(new_date.substring(0, 4)); - // Avoid overflow into next month, since we re-use date_helper - this._date_helper.set_date(1); - this._date_helper.set_month(new_date.substring(4, 6)); - this._date_helper.set_date(new_date.substring(6, 8)); - } - var old_date = this.options.start_date; - this.options.start_date = new Date(this._date_helper.getValue()); - if (old_date !== this.options.start_date && this.isAttached()) { - this.invalidate(true); - } - } - /** - * Change the end date - * - * Changing the end date will invalidate the display, and it will be redrawn - * after a timeout. - * - * @param {string|number|Date} new_date - New end date. Strings can be in - * any format understood by et2_widget_date, or Ymd (eg: 20160101). - * @returns {undefined} - * - * @memberOf et2_calendar_view - */ - set_end_date(new_date) { - if (!new_date || new_date === null) { - new_date = new Date(); - } - // Use date widget's existing functions to deal - if (typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8) { - this._date_helper.set_value(new_date); - } - else if (typeof new_date === "string") { - this._date_helper.set_year(new_date.substring(0, 4)); - // Avoid overflow into next month, since we re-use date_helper - this._date_helper.set_date(1); - this._date_helper.set_month(new_date.substring(4, 6)); - this._date_helper.set_date(new_date.substring(6, 8)); - } - var old_date = this.options.end_date; - this.options.end_date = new Date(this._date_helper.getValue()); - if (old_date !== this.options.end_date && this.isAttached()) { - this.invalidate(true); - } - } - /** - * Set which users to display - * - * Changing the owner will invalidate the display, and it will be redrawn - * after a timeout. - * - * @param {number|number[]|string|string[]} _owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - * - * @memberOf et2_calendar_view - */ - set_owner(_owner) { - var old = this.options.owner; - // 0 means current user, but that causes problems for comparison, - // so we'll just switch to the actual ID - if (_owner == '0') { - _owner = [egw.user('account_id')]; - } - if (!jQuery.isArray(_owner)) { - if (typeof _owner === "string") { - _owner = _owner.split(','); - } - else { - _owner = [_owner]; - } - } - else { - _owner = jQuery.extend([], _owner); - } - this.options.owner = _owner; - if (this.isAttached() && (typeof old === "number" && typeof _owner === "number" && old !== this.options.owner || - // Array of ids will not compare as equal - ((typeof old === 'object' || typeof _owner === 'object') && old.toString() !== _owner.toString()) || - // Strings - typeof old === 'string' && '' + old !== '' + this.options.owner)) { - this.invalidate(true); - } - } - /** - * Provide specific data to be displayed. - * This is a way to set start and end dates, owner and event data in one call. - * - * If events are not provided in the array, - * @param {Object[]} events Array of events, indexed by date in Ymd format: - * { - * 20150501: [...], - * 20150502: [...] - * } - * Days should be in order. - * {string|number|Date} events.start_date - New start date - * {string|number|Date} events.end_date - New end date - * {number|number[]|string|string[]} event.owner - Owner ID, which can - * be an account ID, a resource ID (as defined in calendar_bo, not - * necessarily an entry from the resource app), or a list containing a - * combination of both. - */ - set_value(events) { - if (typeof events !== 'object') - return false; - if (events.length && events.length > 0 || !jQuery.isEmptyObject(events)) { - this.set_disabled(false); - } - if (events.id) { - this.set_id(events.id); - delete events.id; - } - if (events.start_date) { - this.set_start_date(events.start_date); - delete events.start_date; - } - if (events.end_date) { - this.set_end_date(events.end_date); - delete events.end_date; - } - // set_owner() wants start_date set to get the correct week number - // for the corner label - if (events.owner) { - this.set_owner(events.owner); - delete events.owner; - } - this.value = events || {}; - // None of the above changed anything, hide the loader - if (!this.update_timer) { - window.setTimeout(jQuery.proxy(function () { this.loader.hide(); }, this), 200); - } - } - get date_helper() { - return this._date_helper; - } - _createNamespace() { - return true; - } - /** - * Update the 'now' line - * - * Here we just do some limit checks and return the current date/time. - * Extending widgets should handle position. - * - * @private - */ - _updateNow() { - var tempDate = new Date(); - var now = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), tempDate.getHours(), tempDate.getMinutes() - tempDate.getTimezoneOffset(), 0); - // Use date widget's existing functions to deal - this._date_helper.set_value(now.toJSON()); - now = new Date(this._date_helper.getValue()); - if (this.get_start_date() <= now && this.get_end_date() >= now) { - return now; - } - this.now_div.hide(); - return false; - } - /** - * Calendar supports many different owner types, including users & resources. - * This translates an ID to a user-friendly name. - * - * @param {string} user - * @returns {string} - * - * @memberOf et2_calendar_view - */ - _get_owner_name(user) { - var label = undefined; - if (parseInt(user) === 0) { - // 0 means current user - user = egw.user('account_id'); - } - if (et2_calendar_view.owner_name_cache[user]) { - return et2_calendar_view.owner_name_cache[user]; - } - if (!isNaN(user)) { - user = parseInt(user); - var accounts = egw.accounts('both'); - for (var j = 0; j < accounts.length; j++) { - if (accounts[j].value === user) { - label = accounts[j].label; - break; - } - } - } - if (typeof label === 'undefined') { - // Not found? Ask the sidebox owner widget (it gets updated) or the original arrayMgr - let options = false; - if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); - } - else { - options = this.getArrayMgr("sel_options").getRoot().getEntry('owner'); - } - if (options && options.find) { - var found = options.find(function (element) { return element.id == user; }) || {}; - if (found && found.label && found.label !== user) { - label = found.label; - } - } - if (!label) { - // No sidebox? Must be in home or sitemgr (no caching) - ask directly - label = '?'; - egw.jsonq('calendar_owner_etemplate_widget::ajax_owner', user, function (data) { - et2_calendar_view.owner_name_cache[user] = data; - this.invalidate(true); - // Set owner to make sure labels get set - if (this.owner && typeof this.owner.set_value === 'function') { - this.owner.set_value(data); - } - }.bind(this), this); - } - } - if (label) { - et2_calendar_view.owner_name_cache[user] = label; - } - return label; - } - /** - * Find the event information linked to a given DOM node - * - * @param {HTMLElement} dom_node - It should have something to do with an event - * @returns {Object} - */ - _get_event_info(dom_node) { - // Determine as much relevant info as can be found - var event_node = jQuery(dom_node).closest('[data-id]', this.div)[0]; - var day_node = jQuery(event_node).closest('[data-date]', this.div)[0]; - var result = jQuery.extend({ - event_node: event_node, - day_node: day_node - }, event_node ? event_node.dataset : {}, day_node ? day_node.dataset : {}); - // Widget ID should be the DOM node ID without the event_ prefix - if (event_node && event_node.id) { - var widget_id = event_node.id || ''; - widget_id = widget_id.split('event_'); - widget_id.shift(); - result.widget_id = 'event_' + widget_id.join(''); - } - return result; - } - /** - * Starting (mousedown) handler to support drag to create - * - * Extending classes need to set this.drag_create.parent, which is the - * parent container (child of extending class) that will directly hold the - * event. - * - * @param {String} start Date string (JSON format) - */ - _drag_create_start(start) { - this.drag_create.start = jQuery.extend({}, start); - if (!this.drag_create.start.date) { - this.drag_create.start = null; - } - this.drag_create.end = start; - // Clear some stuff, if last time did not complete - if (this.drag_create.event) { - if (this.drag_create.event.destroy) { - this.drag_create.event.destroy(); - } - this.drag_create.event = null; - } - // Wait a bit before adding an "event", it may be just a click - window.setTimeout(jQuery.proxy(function () { - // Create event - this._drag_create_event(); - }, this), 250); - } - /** - * Create or update an event used for feedback while dragging on empty space, - * so user can see something is happening - */ - _drag_create_event() { - if (!this.drag_create.parent || !this.drag_create.start) { - return; - } - if (!this.drag_create.event) { - this._date_helper.set_value(this.drag_create.start.date); - var value = jQuery.extend({}, this.drag_create.start, this.drag_create.end, { - start: this.drag_create.start.date, - end: this.drag_create.end && this.drag_create.end.date || this.drag_create.start.date, - date: "" + this._date_helper.get_year() + - sprintf("%02d", this._date_helper.get_month()) + - sprintf("%02d", this._date_helper.get_date()), - title: '', - description: '', - owner: this.options.owner, - participants: this.options.owner, - app: 'calendar', - whole_day_on_top: this.drag_create.start.whole_day - }); - this.drag_create.event = et2_createWidget('calendar-event', { - id: 'event_drag', - value: value - }, this.drag_create.parent); - this.drag_create.event._values_check(value); - this.drag_create.event.doLoadingFinished(); - } - } - _drag_update_event() { - if (!this.drag_create.event || !this.drag_create.start || !this.drag_create.end - || !this.drag_create.parent || !this.drag_create.event._type) { - return; - } - else if (this.drag_create.end) { - this.drag_create.event.options.value.end = this.drag_create.end.date; - this.drag_create.event._values_check(this.drag_create.event.options.value); - } - this.drag_create.event._update(); - this.drag_create.parent.position_event(this.drag_create.event); - } - /** - * Ending (mouseup) handler to support drag to create - * - * @param {String} end Date string (JSON format) - */ - _drag_create_end(end) { - this.div.css('cursor', ''); - if (typeof end === 'undefined') { - end = {}; - } - if (this.drag_create.start && end.date && - JSON.stringify(this.drag_create.start.date) !== JSON.stringify(end.date)) { - // Drag from start to end, open dialog - var options = { - start: this.drag_create.start.date < end.date ? this.drag_create.start.date : end.date, - end: this.drag_create.start.date < end.date ? end.date : this.drag_create.start.date - }; - // Whole day needs to go from 00:00 to 23:59 - if (end.whole_day || this.drag_create.start.whole_day) { - var start = new Date(options.start); - start.setUTCHours(0); - start.setUTCMinutes(0); - options.start = start.toJSON(); - var end = new Date(options.end); - end.setUTCHours(23); - end.setUTCMinutes(59); - options.end = end.toJSON(); - } - // Add anything else that was set, but not date - jQuery.extend(options, this.drag_create.start, end); - delete (options.date); - // Make sure parent is set, if needed - let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar; - if (this.drag_create.parent && this.drag_create.parent.options.owner !== app_calendar.state.owner && !options.owner) { - options.owner = this.drag_create.parent.options.owner; - } - // Remove empties - for (var key in options) { - if (!options[key]) - delete options[key]; - } - app.calendar.add(options, this.drag_create.event); - // Wait a bit, having these stops the click - window.setTimeout(jQuery.proxy(function () { - this.drag_create.start = null; - this.drag_create.end = null; - this.drag_create.parent = null; - if (this.drag_create.event) { - this.drag_create.event = null; - } - }, this), 100); - return false; - } - this.drag_create.start = null; - this.drag_create.end = null; - this.drag_create.parent = null; - if (this.drag_create.event) { - try { - if (this.drag_create.event.destroy) { - this.drag_create.event.destroy(); - } - } - catch (e) { } - this.drag_create.event = null; - } - return true; - } - /** - * Check if the view should be consolidated into one, or listed seperately - * based on the user's preferences - * - * @param {string[]} owners List of owners - * @param {string} view Name of current view (day, week) - * @returns {boolean} True of only one is needed, false if each owner needs - * to be listed seperately. - */ - static is_consolidated(owners, view) { - // Seperate owners, or consolidated? - return !(owners.length > 1 && - (view === 'day' && owners.length < parseInt('' + egw.preference('day_consolidate', 'calendar')) || - view === 'week' && owners.length < parseInt('' + egw.preference('week_consolidate', 'calendar')))); - } - /** - * Fetch and cache a list of the year's holidays - * - * @param {et2_calendar_timegrid} widget - * @param {string|numeric} year - * @returns {Array} - */ - static get_holidays(widget, year) { - // Loaded in an iframe or something - var view = egw.window.et2_calendar_view ? egw.window.et2_calendar_view : this; - // No country selected causes error, so skip if it's missing - if (!view || !egw.preference('country', 'common')) - return {}; - var cache = view.holiday_cache[year]; - if (typeof cache == 'undefined') { - // Fetch with json instead of jsonq because there may be more than - // one widget listening for the response by the time it gets back, - // and we can't do that when it's queued. - view.holiday_cache[year] = jQuery.getJSON(egw.link('/calendar/holidays.php', { year: year })); - } - cache = view.holiday_cache[year]; - if (typeof cache.done == 'function') { - // pending, wait for it - cache.done(jQuery.proxy(function (response) { - view.holiday_cache[this.year] = response || undefined; - egw.window.setTimeout(jQuery.proxy(function () { - // Make sure widget hasn't been destroyed while we wait - if (typeof this.widget.free == 'undefined') { - this.widget.day_class_holiday(); - } - }, this), 1); - }, { widget: widget, year: year })) - .fail(jQuery.proxy(function () { - view.holiday_cache[this.year] = undefined; - }, { widget: widget, year: year })); - return {}; - } - else { - return cache; - } - } -} -et2_calendar_view._attributes = { - owner: { - name: "Owner", - type: "any", - default: [egw.user('account_id')], - description: "Account ID number of the calendar owner, if not the current user" - }, - start_date: { - name: "Start date", - type: "any" - }, - end_date: { - name: "End date", - type: "any" - } -}; -/** - * Cache to map owner & resource IDs to names, helps cut down on server requests - */ -et2_calendar_view.owner_name_cache = {}; -et2_calendar_view.holiday_cache = {}; -//# sourceMappingURL=et2_widget_view.js.map \ No newline at end of file diff --git a/filemanager/js/app.js b/filemanager/js/app.js deleted file mode 100644 index 8a5430528e..0000000000 --- a/filemanager/js/app.js +++ /dev/null @@ -1,1360 +0,0 @@ -import { E as EgwApp, a as etemplate2, e as egw, b as et2_dialog, y as et2_file, z as et2_button, A as et2_nextmatch_controller, w as egw_get_file_editor_prefered_mimes, d as et2_createWidget } from '../../chunks/etemplate2-0eb045cf.js'; -import '../../chunks/egw_dragdrop_dhtmlx_tree-31643465.js'; -import '../../chunks/egw-5f30b5ae.js'; -import '../../vendor/bower-asset/jquery/dist/jquery.min.js'; -import '../../vendor/bower-asset/jquery-ui/jquery-ui.js'; -import '../../chunks/egw_json-98998d7e.js'; -import '../../chunks/egw_core-0ec5dc11.js'; -import '../../vendor/tinymce/tinymce/tinymce.min.js'; - -/** - * EGroupware - Filemanager - Javascript UI - * - * @link https://www.egroupware.org - * @package filemanager - * @author Ralf Becker - * @copyright (c) 2008-21 by Ralf Becker - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - */ -/** - * UI for filemanager - */ - -class filemanagerAPP extends EgwApp { - /** - * path widget, by template - */ - path_widget = {}; - /** - * Are files cut into clipboard - need to be deleted at source on paste - */ - - clipboard_is_cut = false; - /** - * Regexp to convert id to a path, use this.id2path(_id) - */ - - remove_prefix = /^filemanager::/; - - /** - * Constructor - * - * @memberOf app.filemanager - */ - constructor() { - // call parent - super('filemanager'); // Loading filemanager in its tab and home causes us problems with - // unwanted destruction, so we check for already existing path widgets - - let lists = etemplate2.getByApplication('home'); - - for (let i = 0; i < lists.length; i++) { - if (lists[i].app == 'filemanager' && lists[i].widgetContainer.getWidgetById('path')) { - this.path_widget[lists[i].uniqueId] = lists[i].widgetContainer.getWidgetById('path'); - } - } - } - /** - * Destructor - */ - - - destroy(_app) { - delete this.et2; // call parent - - super.destroy(_app); - } - /** - * 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 template name - */ - - - et2_ready(et2, name) { - // call parent - super.et2_ready(et2, name); - let path_widget = this.et2.getWidgetById('path'); - - if (path_widget) // do NOT set not found path-widgets, as uploads works on first one only! - { - this.path_widget[et2.DOMContainer.id] = path_widget; // Bind to removal to remove from list - - jQuery(et2.DOMContainer).on('clear', function (e) { - if (app.filemanager && app.filemanager.path_widget) delete app.filemanager.path_widget[e.target.id]; - }); - } - - if (this.et2.getWidgetById('nm')) { - // Legacy JS only supports 2 arguments (event and widget), so set - // to the actual function here - this.et2.getWidgetById('nm').set_onfiledrop(jQuery.proxy(this.filedrop, this)); - } // get clipboard from browser localstore and update button tooltips - - - this.clipboard_tooltips(); // calling set_readonly for initial path - - if (this.et2.getArrayMgr('content').getEntry('initial_path_readonly')) { - this.readonly = [this.et2.getArrayMgr('content').getEntry('nm[path]'), true]; - } - - if (typeof this.readonly != 'undefined') { - this.set_readonly.apply(this, this.readonly); - delete this.readonly; - } - - if (name == 'filemanager.index') { - let fe = egw.link_get_registry('filemanager-editor'); - let new_widget = this.et2.getWidgetById('new'); - - if (fe && fe["edit"]) { - let new_options = this.et2.getArrayMgr('sel_options').getEntry('new'); - new_widget.set_select_options(new_options); - } else if (new_widget) { - new_widget.set_disabled(true); - } - } - } - /** - * Set the application's state to the given state. - * - * Extended from parent to also handle view - * - * - * @param {{name: string, state: object}|string} state Object (or JSON string) for a state. - * Only state is required, and its contents are application specific. - * - * @return {boolean} false - Returns false to stop event propagation - */ - - - setState(state) { - // 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); - } - } - - let result = super.setState(state, 'filemanager.index'); // This has to happen after the parent, changing to tile recreates - // nm controller - - if (typeof state == "object" && state.state && state.state.view) { - let et2 = etemplate2.getById('filemanager-index'); - - if (et2) { - this.et2 = et2.widgetContainer; - this.change_view(state.state.view); - } - } - - return result; - } - /** - * Retrieve the current state of the application for future restoration - * - * Extended from parent to also set view - * - * @return {object} Application specific map representing the current state - */ - - - getState() { - let state = super.getState(); - let et2 = etemplate2.getById('filemanager-index'); - - if (et2) { - let nm = et2.widgetContainer.getWidgetById('nm'); - state.view = nm.view; - } - - return state; - } - /** - * Convert id to path (remove "filemanager::" prefix) - */ - - - id2path(_id) { - return _id.replace(this.remove_prefix, ''); - } - /** - * Convert array of elems to array of paths - */ - - - _elems2paths(_elems) { - let paths = []; - - for (let i = 0; i < _elems.length; i++) { - // If selected has no id, try parent. This happens for the placeholder row - // in empty directories. - paths.push(_elems[i].id ? this.id2path(_elems[i].id) : _elems[i]._context._parentId); - } - - return paths; - } - /** - * Get directory of a path - */ - - - dirname(_path) { - let parts = _path.split('/'); - - parts.pop(); - return parts.join('/') || '/'; - } - /** - * Get name of a path - */ - - - basename(_path) { - return _path.split('/').pop(); - } - /** - * Get current working directory - */ - - - get_path(etemplate_name) { - if (!etemplate_name || typeof this.path_widget[etemplate_name] == 'undefined') { - for (etemplate_name in this.path_widget) break; - } - - let path_widget = this.path_widget[etemplate_name]; - return path_widget ? path_widget.get_value.apply(path_widget) : null; - } - /** - * Open compose with already attached files - * - * @param {(string|string[])} attachments path(s) - * @param {object} params - */ - - - open_mail(attachments, params) { - if (typeof attachments == 'undefined') attachments = this.get_clipboard_files(); - if (!params || typeof params != 'object') params = {}; - if (!(attachments instanceof Array)) attachments = [attachments]; - let content = { - data: { - files: { - file: [] - } - } - }; - - for (let i = 0; i < attachments.length; i++) { - params['preset[file][' + i + ']'] = 'vfs://default' + attachments[i]; - content.data.files.file.push('vfs://default' + attachments[i]); - } - - content.data.files["filemode"] = params['preset[filemode]']; // always open compose in html mode, as attachment links look a lot nicer in html - - params["mimeType"] = 'html'; - return egw.openWithinWindow("mail", "setCompose", content, params, /mail.mail_compose.compose/, true); - } - /** - * Mail files action: open compose with already attached files - * - * @param _action - * @param _elems - */ - - - mail(_action, _elems) { - this.open_mail(this._elems2paths(_elems), { - 'preset[filemode]': _action.id.substr(5) - }); - } - /** - * Mail files action: open compose with already linked files - * We're only interested in hidden upload shares here, open_mail can handle - * the rest - * - * @param {egwAction} _action - * @param {egwActionObject[]} _selected - */ - - - mail_share_link(_action, _selected) { - if (_action.id !== 'mail_shareUploadDir') { - return this.mail(_action, _selected); - } - - let path = this.id2path(_selected[0].id); - this.share_link(_action, _selected, null, false, false, this._mail_link_callback); - return true; - } - /** - * Callback with the share link to append to an email - * - * @param {Object} _data - * @param {String} _data.share_link Link to the share - * @param {String} _data.title Title for the link - * @param {String} [_data.msg] Error message - */ - - - _mail_link_callback(_data) { - debugger; - if (_data.msg || !_data.share_link) window.egw_refresh(_data.msg, this.appname); - let params = { - 'preset[body]': '' + _data.title + '', - 'mimeType': 'html' // always open compose in html mode, as attachment links look a lot nicer in html - - }; - let content = { - mail_htmltext: ['
      ' + _data.title + ''], - mail_plaintext: ["\n" + _data.share_link] - }; - return egw.openWithinWindow("mail", "setCompose", content, params, /mail.mail_compose.compose/); - } - /** - * Trigger Upload after each file is uploaded - * @param {type} _event - */ - - - uploadOnOne(_event) { - this.upload(_event, 1); - } - /** - * Send names of uploaded files (again) to server, to process them: either copy to vfs or ask overwrite/rename - * - * @param {event} _event - * @param {number} _file_count - * @param {string=} _path where the file is uploaded to, default current directory - * @param {string} _conflict What to do if the file conflicts with one on the server - * @param {string} _target Upload processing target. Sharing classes can override this. - */ - - - upload(_event, _file_count, _path, _conflict = "ask", _target = 'filemanager_ui::ajax_action') { - if (typeof _path == 'undefined') { - _path = this.get_path(); - } - - if (_file_count && !jQuery.isEmptyObject(_event.data.getValue())) { - let widget = _event.data; - let value = widget.getValue(); - value.conflict = _conflict; - egw.json(_target, ['upload', value, _path, { - ui_path: this.egw.window.location.pathname - }], this._upload_callback, this, true, this).sendRequest(); - widget.set_value(''); - } - } - /** - * Finish callback for file a file dialog, to get the overwrite / rename prompt - * - * @param {event} _event - * @param {number} _file_count - */ - - - file_a_file_upload(_event, _file_count) { - let widget = _event.data; - let path = widget.getRoot().getWidgetById("path").getValue(); - let action = widget.getRoot().getWidgetById("action").getValue(); - let link = widget.getRoot().getWidgetById("entry").getValue(); - - if (action == 'save_as' && link.app && link.id) { - path = "/apps/" + link.app + "/" + link.id; - } - - let props = widget.getInstanceManager().getValues(widget.getRoot()); - egw.json('filemanager_ui::ajax_action', [action == 'save_as' ? 'upload' : 'link', widget.getValue(), path, props], function (_data) { - app.filemanager._upload_callback(_data); // Remove successful after a delay - - - for (var file in _data.uploaded) { - if (!_data.uploaded[file].confirm || _data.uploaded[file].confirmed) { - // Remove that file from file widget... - widget.remove_file(_data.uploaded[file].name); - } - } - - opener.egw_refresh('', 'filemanager', null, null, 'filemanager'); - }, app.filemanager, true, this).sendRequest(true); - return true; - } - /** - * Callback for server response to upload request: - * - display message and refresh list - * - ask use to confirm overwritting existing files or rename upload - * - * @param {object} _data values for attributes msg, files, ... - */ - - - _upload_callback(_data) { - if (_data.msg || _data.uploaded) window.egw_refresh(_data.msg, this.appname, undefined, undefined, undefined, undefined, undefined, _data.type); - let that = this; - - for (let file in _data.uploaded) { - if (_data.uploaded[file].confirm && !_data.uploaded[file].confirmed) { - let buttons = [{ - text: this.egw.lang("Yes"), - id: "overwrite", - class: "ui-priority-primary", - "default": true, - image: 'check' - }, { - text: this.egw.lang("Rename"), - id: "rename", - image: 'edit' - }, { - text: this.egw.lang("Cancel"), - id: "cancel" - }]; - if (_data.uploaded[file].confirm === "is_dir") buttons.shift(); - let dialog = et2_dialog.show_prompt(function (_button_id, _value) { - let uploaded = {}; - uploaded[this.my_data.file] = this.my_data.data; - - switch (_button_id) { - case "overwrite": - uploaded[this.my_data.file].confirmed = true; - // fall through - - case "rename": - uploaded[this.my_data.file].name = _value; - delete uploaded[this.my_data.file].confirm; // send overwrite-confirmation and/or rename request to server - - egw.json('filemanager_ui::ajax_action', [this.my_data.action, uploaded, this.my_data.path, this.my_data.props], that._upload_callback, that, true, that).sendRequest(); - return; - - case "cancel": - // Remove that file from every file widget... - that.et2.iterateOver(function (_widget) { - _widget.remove_file(this.my_data.data.name); - }, this, et2_file); - } - }, _data.uploaded[file].confirm === "is_dir" ? this.egw.lang("There's already a directory with that name!") : this.egw.lang('Do you want to overwrite existing file %1 in directory %2?', _data.uploaded[file].name, _data.path), this.egw.lang('File %1 already exists', _data.uploaded[file].name), _data.uploaded[file].name, buttons, file); // setting required data for callback in as my_data - - dialog.my_data = { - action: _data.action, - file: file, - path: _data.path, - data: _data.uploaded[file], - props: _data.props - }; - } - } - } - /** - * Get any files that are in the system clipboard - * - * @return {string[]} Paths - */ - - - get_clipboard_files() { - let clipboard_files = []; - - if (typeof window.localStorage != 'undefined' && typeof egw.getSessionItem('phpgwapi', 'egw_clipboard') != 'undefined') { - let clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard')) || { - type: [], - selected: [] - }; - - if (clipboard.type.indexOf('file') >= 0) { - for (let i = 0; i < clipboard.selected.length; i++) { - let split = clipboard.selected[i].id.split('::'); - - if (split[0] == 'filemanager') { - clipboard_files.push(this.id2path(clipboard.selected[i].id)); - } - } - } - } - - return clipboard_files; - } - /** - * Update clickboard tooltips in buttons - */ - - - clipboard_tooltips() { - let paste_buttons = ['button[paste]', 'button[linkpaste]', 'button[mailpaste]']; - - for (let i = 0; i < paste_buttons.length; ++i) { - let button = this.et2.getWidgetById(paste_buttons[i]); - if (button) button.set_statustext(this.get_clipboard_files().join(",\n")); - } - } - /** - * Clip files into clipboard - * - * @param _action - * @param _elems - */ - - - clipboard(_action, _elems) { - this.clipboard_is_cut = _action.id == "cut"; - let clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard')) || { - type: [], - selected: [] - }; - - if (_action.id != "add") { - clipboard = { - type: [], - selected: [] - }; - } // When pasting we need to know the type of data - pull from actions - - - let drag = _elems[0].getSelectedLinks('drag').links; - - for (let k in drag) { - if (drag[k].enabled && drag[k].actionObj.dragType.length > 0) { - clipboard.type = clipboard.type.concat(drag[k].actionObj.dragType); - } - } - - clipboard.type = jQuery.unique(clipboard.type); // egwAction is a circular structure and can't be stringified so just take what we want - // Hopefully that's enough for the action handlers - - for (let k in _elems) { - if (_elems[k].id) clipboard.selected.push({ - id: _elems[k].id, - data: _elems[k].data - }); - } // Save it in session - - - egw.setSessionItem('phpgwapi', 'egw_clipboard', JSON.stringify(clipboard)); - this.clipboard_tooltips(); - } - /** - * Paste files into current directory or mail them - * - * @param _type 'paste', 'linkpaste', 'mailpaste' - */ - - - paste(_type) { - let clipboard_files = this.get_clipboard_files(); - - if (clipboard_files.length == 0) { - alert(this.egw.lang('Clipboard is empty!')); - return; - } - - switch (_type) { - case 'mailpaste': - this.open_mail(clipboard_files); - break; - - case 'paste': - this._do_action(this.clipboard_is_cut ? 'move' : 'copy', clipboard_files); - - if (this.clipboard_is_cut) { - this.clipboard_is_cut = false; - clipboard_files = []; - this.clipboard_tooltips(); - } - - break; - - case 'linkpaste': - this._do_action('symlink', clipboard_files); - - break; - } - } - /** - * Pass action to server - * - * @param _action - * @param _elems - */ - - - action(_action, _elems) { - let paths = this._elems2paths(_elems); - - let path = this.get_path(_action && _action.parent.data.nextmatch.getInstanceManager().uniqueId || false); - - this._do_action(_action.id, paths, true, path); - } - /** - * Prompt user for directory to create - * - * @param {egwAction|undefined} action Action, event or undefined if called directly - * @param {egwActionObject[] | undefined} selected Selected row, or undefined if called directly - */ - - - createdir(action, selected) { - let self = this; - et2_dialog.show_prompt(function (button, dir) { - if (button && dir) { - let path = self.get_path(action && action.parent ? action.parent.data.nextmatch.getInstanceManager().uniqueId : false); - - if (action && action instanceof egwAction) { - let paths = self._elems2paths(selected); - - if (paths[0]) path = paths[0]; // check if target is a file --> use it's directory instead - - if (selected[0].id || path) { - let data = egw.dataGetUIDdata(selected[0].id || 'filemanager::' + path); - - if (data && data.data.mime != 'httpd/unix-directory') { - path = self.dirname(path); - } - } - } - - self._do_action('createdir', egw.encodePathComponent(dir), true, path); // true=synchronous request - - - self.change_dir((path == '/' ? '' : path) + '/' + egw.encodePathComponent(dir)); - } - }, this.egw.lang('New directory'), this.egw.lang('Create directory')); - } - /** - * Prompt user for directory to create - */ - - - symlink() { - let self = this; - et2_dialog.show_prompt(function (button, target) { - if (button && target) { - self._do_action('symlink', target); - } - }, this.egw.lang('Link target'), this.egw.lang('Create link')); - } - /** - * Run a serverside action via an ajax call - * - * @param _type 'move_file', 'copy_file', ... - * @param _selected selected paths - * @param _sync send a synchronous ajax request - * @param _path defaults to current path - */ - - - _do_action(_type, _selected, _sync, _path) { - if (typeof _path == 'undefined') _path = this.get_path(); - egw.json('filemanager_ui::ajax_action', [_type, _selected, _path], this._do_action_callback, this, !_sync, this).sendRequest(); - } - /** - * Callback for _do_action ajax call - * - * @param _data - */ - - - _do_action_callback(_data) { - window.egw_refresh(_data.msg, this.appname, undefined, undefined, undefined, undefined, undefined, _data.type); - } - /** - * Force download of a file by appending '?download' to it's download url - * - * @param _action - * @param _senders - */ - - - force_download(_action, _senders) { - for (let i = 0; i < _senders.length; i++) { - let data = egw.dataGetUIDdata(_senders[i].id); - let url = data ? data.data.download_url : '/webdav.php' + this.id2path(_senders[i].id); - if (url[0] == '/') url = egw.link(url); - let a = document.createElement('a'); - - if (typeof a.download == "undefined") { - window.location = url + "?download"; - return false; - } // Multiple file download for those that support it - - - let $a = jQuery(a).prop('href', url).prop('download', data ? data.data.name : "").appendTo(this.et2.getDOMNode()); - window.setTimeout(jQuery.proxy(function () { - let evt = document.createEvent('MouseEvent'); - evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - this[0].dispatchEvent(evt); - this.remove(); - }, $a), 100 * i); - } - - return false; - } - /** - * Check to see if the browser supports downloading multiple files - * (using a tag download attribute) to enable/disable the context menu - * - * @param {egwAction} action - * @param {egwActionObject[]} selected - */ - - - is_multiple_allowed(action, selected) { - let allowed = typeof document.createElement('a').download != "undefined"; - if (typeof action == "undefined") return allowed; - return (allowed || selected.length <= 1) && action.not_disableClass.apply(action, arguments); - } - /** - * Change directory - * - * @param {string} _dir directory to change to incl. '..' for one up - * @param {et2_widget} widget - */ - - - change_dir(_dir, widget) { - for (var etemplate_name in this.path_widget) break; - - if (widget) etemplate_name = widget.getInstanceManager().uniqueId; // Make sure everything is in place for changing directory - - if (!this.et2 || typeof etemplate_name !== 'string' || typeof this.path_widget[etemplate_name] === 'undefined') { - return false; - } - - switch (_dir) { - case '..': - _dir = this.dirname(this.get_path(etemplate_name)); - break; - - case '~': - _dir = this.et2.getWidgetById('nm').options.settings.home_dir; - break; - } - - this.path_widget[etemplate_name].set_value(_dir); - } - /** - * Toggle view between tiles and rows - * - * @param {string|Event} [view] - Specify what to change the view to. Either 'tile' or 'row'. - * Or, if this is used as a callback view is actually the event, and we need to find the view. - * @param {et2_widget} [button_widget] - The widget that's calling - */ - - - change_view(view, button_widget) { - let et2 = etemplate2.getById('filemanager-index'); - let nm; - - if (et2 && et2.widgetContainer.getWidgetById('nm')) { - nm = et2.widgetContainer.getWidgetById('nm'); - } - - if (!nm) { - egw.debug('warn', 'Could not find nextmatch to change view'); - return; - } - - if (!button_widget) { - button_widget = nm.getWidgetById('button[change_view]'); - } - - if (button_widget && button_widget.instanceOf(et2_button)) { - // Switch view based on button icon, since controller can get re-created - if (typeof view != 'string') { - view = button_widget.options.image.replace('list_', ''); - } // Toggle button icon to the other view - //todo: nm.controller needs to be changed to nm.getController after merging typescript branch into master - - - button_widget.set_image("list_" + (view == et2_nextmatch_controller.VIEW_ROW ? et2_nextmatch_controller.VIEW_TILE : et2_nextmatch_controller.VIEW_ROW)); - button_widget.set_statustext(view == et2_nextmatch_controller.VIEW_ROW ? this.egw.lang("Tile view") : this.egw.lang('List view')); - } - - nm.set_view(view); // Put it into active filters (but don't refresh) - - nm.activeFilters["view"] = view; // Change template to match - - let template = view == et2_nextmatch_controller.VIEW_ROW ? 'filemanager.index.rows' : 'filemanager.tile'; - nm.set_template(template); // Wait for template to load, then refresh - - template = nm.getWidgetById(template); - - if (template && template.loading) { - template.loading.done(function () { - nm.applyFilters({ - view: view - }); - }); - } - } - /** - * Open/active an item - * - * @param _action - * @param _senders - */ - - - open(_action, _senders) { - let data = egw.dataGetUIDdata(_senders[0].id); - let path = this.id2path(_senders[0].id); - this.et2 = this.et2 ? this.et2 : etemplate2.getById('filemanager-index').widgetContainer; - - let mime = this.et2._inst.widgetContainer.getWidgetById('$row'); // try to get mime widget DOM node out of the row DOM - - - let mime_dom = jQuery(_senders[0].iface.getDOMNode()).find("span#filemanager-index_\\$row"); - let fe = egw_get_file_editor_prefered_mimes(); // symlinks dont have mime 'http/unix-directory', but server marks all directories with class 'isDir' - - if (data.data.mime == 'httpd/unix-directory' || data.data['class'] && data.data['class'].split(/ +/).indexOf('isDir') != -1) { - this.change_dir(path, _action.parent.data.nextmatch || this.et2); - } else if (mime && data.data.mime.match(mime.mime_regexp) && mime_dom.length > 0) { - mime_dom.click(); - } else if (mime && this.isEditable(_action, _senders) && fe && fe.edit) { - egw.open_link(egw.link('/index.php', { - menuaction: fe.edit.menuaction, - path: decodeURIComponent(data.data.download_url) - }), '', fe.edit_popup); - } else { - let url; // Build ViewerJS url - - if (data.data.mime.match(/application\/vnd\.oasis\.opendocument/) && egw.preference('document_doubleclick_action', 'filemanager') == 'collabeditor') { - url = '/ViewerJS/#..' + data.data.download_url; - } - - egw.open({ - path: path, - type: data.data.mime, - download_url: url - }, 'file', 'view', null, '_browser'); - } - - return false; - } - /** - * Edit prefs of current directory - * - * @param _action - * @param _senders - */ - - - editprefs(_action, _senders) { - let path = typeof _senders != 'undefined' ? this.id2path(_senders[0].id) : this.get_path(_action && _action.parent.data.nextmatch.getInstanceManager().uniqueId || false); - egw().open_link(egw.link('/index.php', { - menuaction: 'filemanager.filemanager_ui.file', - path: path - }), 'fileprefs', '510x425'); - } - /** - * Callback to check if the paste action is enabled. We also update the - * clipboard historical targets here as well - * - * @param {egwAction} _action drop action we're checking - * @param {egwActionObject[]} _senders selected files - * @param {egwActionObject} _target Drop or context menu activated on this one - * - * @returns boolean true if enabled, false otherwise - */ - - - paste_enabled(_action, _senders, _target) { - // Need files in the clipboard for this - let clipboard_files = this.get_clipboard_files(); - - if (clipboard_files.length === 0) { - return false; - } // Parent action (paste) gets run through here as well, but needs no - // further processing - - - if (_action.id == 'paste') return true; - - if (_action.canHaveChildren.indexOf('drop') == -1) { - _action.canHaveChildren.push('drop'); - } - - let actions = []; // Current directory - - let current_dir = this.get_path(); - let dir = egw.dataGetUIDdata('filemanager::' + current_dir); - let path_widget = etemplate2.getById('filemanager-index').widgetContainer.getWidgetById('button[createdir]'); - actions.push({ - id: _action.id + '_current', - caption: current_dir, - path: current_dir, - enabled: dir && dir.data && dir.data.class && dir.data.class.indexOf('noEdit') === -1 || !dir && path_widget && !path_widget.options.readonly - }); // Target, if directory - - let target_dir = this.id2path(_target.id); - dir = egw.dataGetUIDdata(_target.id); - actions.push({ - id: _action.id + '_target', - caption: target_dir, - path: target_dir, - enabled: _target && _target.iface && jQuery(_target.iface.getDOMNode()).hasClass('isDir') && (dir && dir.data && dir.data.class && dir.data.class.indexOf('noEdit') === -1 || !dir) - }); // Last 10 folders - - let previous_dsts = jQuery.extend([], egw.preference('drop_history', this.appname)); - let action_index = 0; - - for (let i = 0; i < 10; i++) { - let path = i < previous_dsts.length ? previous_dsts[i] : ''; - actions.push({ - id: _action.id + '_target_' + action_index++, - caption: path, - path: path, - group: 2, - enabled: path && !(current_dir && path === current_dir || target_dir && path === target_dir) - }); - } // Common stuff, every action needs these - - - for (let i = 0; i < actions.length; i++) { - //actions[i].type = 'drop', - actions[i].acceptedTypes = _action.acceptedTypes; - actions[i].no_lang = true; - actions[i].hideOnDisabled = true; - } - - _action.updateActions(actions); // Create paste action - // This injects the clipboard data and calls the original handler - - - let paste_exec = function (action, selected) { - // Add in clipboard as a sender - let clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard')); // Set a flag so apps can tell the difference, if they need to - - action.set_onExecute(action.parent.onExecute.fnct); - action.execute(clipboard.selected, selected[0]); // Clear the clipboard, the files are not there anymore - - if (action.id.indexOf('move') !== -1) { - egw.setSessionItem('phpgwapi', 'egw_clipboard', JSON.stringify({ - type: [], - selected: [] - })); - } - }; - - for (let i = 0; i < actions.length; i++) { - _action.getActionById(actions[i].id).onExecute = jQuery.extend(true, {}, _action.onExecute); - - _action.getActionById(actions[i].id).set_onExecute(paste_exec); - } - - return actions.length > 0; - } - /** - * File(s) droped - * - * @param _action - * @param _elems - * @param _target - * @returns - */ - - - drop(_action, _elems, _target) { - let src = this._elems2paths(_elems); // Target will be missing ID if directory is empty - // so start with the current directory - - - let parent = _action; - let nm = _target ? _target.manager.data.nextmatch : null; - - while (!nm && parent.parent) { - parent = parent.parent; - if (parent.data.nextmatch) nm = parent.data.nextmatch; - } - - let nm_dst = this.get_path(nm.getInstanceManager().uniqueId || false); - let dst; // Action specifies a destination, target does not matter - - if (_action.data && _action.data.path) { - dst = _action.data.path; - } // File(s) were dropped on a row, they want them inside - else if (_target) { - dst = ''; - - let paths = this._elems2paths([_target]); - - if (paths[0]) dst = paths[0]; // check if target is a file --> use it's directory instead - - if (_target.id) { - let data = egw.dataGetUIDdata(_target.id); - - if (!data || data.data.mime != 'httpd/unix-directory') { - dst = this.dirname(dst); - } - } - } // Remember the target for next time - - - let previous_dsts = jQuery.extend([], egw.preference('drop_history', this.appname)); - previous_dsts.unshift(dst); - previous_dsts = Array.from(new Set(previous_dsts)).slice(0, 9); - egw.set_preference(this.appname, 'drop_history', previous_dsts); // Actual action id will be something like file_drop_{move|copy|link}[_other_id], - // but we need to send move, copy or link - - let action_id = _action.id.replace("file_drop_", '').split('_', 1)[0]; - - this._do_action(action_id, src, false, dst || nm_dst); - } - /** - * Handle a native / HTML5 file drop from system - * - * This is a callback from nextmatch to prevent the default link action, and just upload instead. - * - * @param {string} row_uid UID of the row the files were dropped on - * @param {Files[]} files - */ - - - filedrop(row_uid, files) { - let self = this; - let data = egw.dataGetUIDdata(row_uid); - files = files || window.event.dataTransfer.files; - let path = typeof data != 'undefined' && data.data.mime == "httpd/unix-directory" ? data.data.path : this.get_path(); - let widget = this.et2.getWidgetById('upload'); // Override finish to specify a potentially different path - - let old_onfinishone = widget.options.onFinishOne; - let old_onfinish = widget.options.onFinish; - - widget.options.onFinishOne = function (_event, _file_count) { - self.upload(_event, _file_count, path); - }; - - widget.options.onFinish = function () { - widget.options.onFinish = old_onfinish; - widget.options.onFinishOne = old_onfinishone; - }; // This triggers the upload - - - widget.set_value(files); // Return false to prevent the link - - return false; - } - /** - * Change readonly state for given directory - * - * Get call/transported with each get_rows call, but should only by applied to UI if matching curent dir - * - * @param {string} _path - * @param {boolean} _ro - */ - - - set_readonly(_path, _ro) { - //alert('set_readonly("'+_path+'", '+_ro+')'); - if (!this.path_widget) // widget not yet ready, try later - { - this.readonly = [_path, _ro]; - return; - } - - for (let id in this.path_widget) { - let path = this.get_path(id); - - if (_path == path) { - let ids = ['button[linkpaste]', 'button[paste]', 'button[createdir]', 'button[symlink]', 'upload', 'new']; - - for (let i = 0; i < ids.length; ++i) { - let widget = etemplate2.getById(id).widgetContainer.getWidgetById(ids[i]); - - if (widget) { - widget.set_readonly(_ro); - } - } - } - } - } - /** - * Row or filename in select-file dialog clicked - * - * @param {jQuery.event} event - * @param {et2_widget} widget - */ - - - select_clicked(event, widget) { - if (widget?.value?.is_dir) // true for "httpd/unix-directory" and "egw/*" - { - let path = null; // Cannot do this, there are multiple widgets named path - // widget.getRoot().getWidgetById("path"); - - widget.getRoot().iterateOver(function (widget) { - if (widget.id == "path") path = widget; - }, null, et2_textbox); - - if (path) { - path.set_value(widget.value.path); - } - } else if (this.et2 && this.et2.getArrayMgr('content').getEntry('mode') != 'open-multiple') { - let editfield = this.et2.getWidgetById('name'); - - if (editfield) { - editfield.set_value(widget.value.name); - } - } else { - let file = widget.value.name; - widget.getParent().iterateOver(function (widget) { - if (widget.options.selected_value == file) { - widget.set_value(widget.get_value() == file ? widget.options.unselected_value : file); - } - }, null, et2_checkbox); - } // Stop event or it will toggle back off - - - event.preventDefault(); - event.stopPropagation(); - return false; - } - /** - * Set Sudo button's label and change its onclick handler according to its action - * - * @param {widget object} _widget sudo buttononly - * @param {string} _action string of action type {login|logout} - */ - - - set_sudoButton(_widget, _action) { - let widget = _widget || this.et2.getWidgetById('sudouser'); - - if (widget) { - switch (_action) { - case 'login': - widget.set_label('Logout'); - widget.getRoot().getInstanceManager().submit(widget); - break; - - default: - widget.set_label('Superuser'); - - widget.onclick = function () { - jQuery('.superuser').css('display', 'inline'); - }; - - } - } - } - /** - * Open file a file dialog from EPL, warn if EPL is not available - */ - - - fileafile() { - if (this.egw.user('apps').stylite) { - this.egw.open_link('/index.php?menuaction=stylite.stylite_filemanager.upload&path=' + this.get_path(), '_blank', '670x320'); - } else { - // This is shown if stylite code is there, but the app is not available - et2_dialog.show_dialog(function (_button) { - if (_button == et2_dialog.YES_BUTTON) window.open('http://www.egroupware.org/EPL', '_blank'); - return true; - }, this.egw.lang('this feature is only available in epl version.') + "\n\n" + this.egw.lang('You can use regular upload [+] button to upload files.') + "\n\n" + this.egw.lang('Do you want more information about EPL subscription?'), this.egw.lang('File a file'), undefined, et2_dialog.BUTTONS_YES_NO, et2_dialog.QUESTION_MESSAGE); - } - } - /** - * create a share-link for the given entry - * Overriden from parent to handle empty directories - * - * @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) { - // Check to see if we're in the empty row (No matches found.) and use current path - let path = _senders[0].id; - - if (!path) { - _senders[0] = { - id: this.get_path() - }; - } // Pass along any action data - - - let _extra = {}; - - for (let i in _action.data) { - if (i.indexOf('share') == 0) { - _extra[i] = _action.data[i]; - } - } - - super.share_link(_action, _senders, _target, _writable, _files, _callback, _extra); - } - /** - * Share-link callback - * @param {object} _data - */ - - - _share_link_callback(_data) { - if (_data.msg || _data.share_link) window.egw_refresh(_data.msg, this.appname); - console.log("_data", _data); - let app = this; - - let copy_link_to_clipboard = function (evt) { - let $target = jQuery(evt.target); - $target.select(); - - try { - let successful = document.execCommand('copy'); - - if (successful) { - egw.message(app.egw.lang('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 () { - jQuery("body").off("click", "[name=share_link]", copy_link_to_clipboard); - return true; - }, - title: _data.title ? _data.title : _data.writable || _data.action === 'shareWritableLink' ? this.egw.lang("Writable share link") : this.egw.lang("Readonly share link"), - template: _data.template, - width: 450, - value: { - content: { - "share_link": _data.share_link - } - } - }); - } - /** - * Check if a row can have the Hidden Uploads action - * Needs to be a directory - */ - - - hidden_upload_enabled(_action, _senders) { - if (_senders[0].id == 'nm') return false; - let data = egw.dataGetUIDdata(_senders[0].id); - let readonly = (data?.data.class || '').split(/ +/).indexOf('noEdit') >= 0; // symlinks dont have mime 'http/unix-directory', but server marks all directories with class 'isDir' - - return !_senders[0].id || data.data.is_dir && !readonly; - } - /** - * View the link from an existing share - * (EPL only) - * - * @param {egwAction} _action The shareLink action - * @param {egwActionObject[]} _senders The row clicked on - */ - - - view_link(_action, _senders) { - let id = egw.dataGetUIDdata(_senders[0].id).data.share_id; - egw.json('stylite_filemanager::ajax_view_link', [id], this._share_link_callback, this, true, this).sendRequest(); - return true; - } - /** - * This function copies the selected file/folder entry as webdav link into clipboard - * - * @param {object} _action egw actions - * @param {object} _senders selected nm row - * @returns {Boolean} returns false if not successful - */ - - - copy_link(_action, _senders) { - let data = egw.dataGetUIDdata(_senders[0].id); - let url = data ? data.data.download_url : '/webdav.php' + this.id2path(_senders[0].id); - if (url[0] == '/') url = egw.link(url); - - if (url.substr(0, 4) == 'http' && url.indexOf('://') <= 5) {// it's already a full url - } else { - let hostUrl = new URL(window.location.href); - url = hostUrl.origin + url; - } - - if (url) { - let elem = jQuery(document.createElement('div')); - let range; - elem.text(url); - elem.appendTo('body'); - - if (document.selection) { - range = document.body.createTextRange(); - range.moveToElementText(elem); - range.select(); - } else if (window.getSelection) { - range = document.createRange(); - range.selectNode(elem[0]); - window.getSelection().removeAllRanges(); - window.getSelection().addRange(range); - } - - let successful = false; - - try { - successful = document.execCommand('copy'); - - if (successful) { - egw.message(this.egw.lang('WebDav link copied into clipboard')); - window.getSelection().removeAllRanges(); - return true; - } - } catch (e) {} - - egw.message('Failed to copy the link!'); - elem.remove(); - return false; - } - } - /** - * Function to check wheter selected file is editable. ATM only .odt is supported. - * - * @param {object} _egwAction egw action object - * @param {object} _senders object of selected row - * - * @returns {boolean} returns true if is editable otherwise false - */ - - - isEditable(_egwAction, _senders) { - if (_senders.length > 1) return false; - let data = egw.dataGetUIDdata(_senders[0].id); - let mime = this.et2.getInstanceManager().widgetContainer.getWidgetById('$row'); - let fe = egw_get_file_editor_prefered_mimes(data.data.mime); - if (fe && fe.mime && !fe.mime[data.data.mime]) return false; - return !!data.data.mime.match(mime.mime_odf_regex); - } - /** - * Method to create a new document - * @param {object} _action either action or node - * @param {object} _selected either widget or selected row - * - * @return {boolean} returns true - */ - - - create_new(_action, _selected) { - let fe = egw.link_get_registry('filemanager-editor'); - - if (fe && fe["edit"]) { - egw.open_link(egw.link('/index.php', { - menuaction: fe["edit"].menuaction - }), '', fe["popup_edit"]); - } - - return true; - } - -} -app.classes.filemanager = filemanagerAPP; - -export { filemanagerAPP }; -//# sourceMappingURL=app.js.map diff --git a/importexport/js/app.js b/importexport/js/app.js deleted file mode 100644 index f2f0f4613c..0000000000 --- a/importexport/js/app.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * EGroupware - Import/Export - Javascript UI - * - * @link http://www.egroupware.org - * @package importexport - * @author Nathan Gray - * @copyright (c) 2013 Nathan Gray - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @version $Id$ - */ -import 'jquery'; -import 'jqueryui'; -import '../jsapi/egw_global'; -import '../etemplate/et2_types'; -import { EgwApp } from '../../api/js/jsapi/egw_app'; -import { egw } from "../../api/js/jsapi/egw_global"; -/** - * JS for Import/Export - * - * @augments AppJS - */ -class ImportExportApp extends EgwApp { - /** - * Constructor - * - * @memberOf app.infolog - */ - constructor() { - // call parent - super('importexport'); - } - /** - * Destructor - */ - destroy(_app) { - // call parent - super.destroy(_app); - } - /** - * 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 {etemplate2} _et2 newly ready object - * @param {string} _name template name - */ - et2_ready(_et2, _name) { - // call parent - super.et2_ready(_et2, _name); - if (this.et2.getWidgetById('export')) { - if (!this.et2.getArrayMgr("content").getEntry("definition")) { - // et2 doesn't understand a disabled button in the normal sense - jQuery(this.et2.getDOMWidgetById('export').getDOMNode()).attr('disabled', 'disabled'); - jQuery(this.et2.getDOMWidgetById('preview').getDOMNode()).attr('disabled', 'disabled'); - } - if (!this.et2.getArrayMgr("content").getEntry("filter")) { - jQuery('input[value="filter"]').parent().hide(); - } - // Disable / hide definition filter if not selected - if (this.et2.getArrayMgr("content").getEntry("selection") != 'filter') { - jQuery('div.filters').hide(); - } - } - } - /** - * Callback to download the file without destroying the etemplate request - * - * @param data URL to get the export file - */ - download(data) { - // Try to get the file to download in the parent window - let app_templates = this.egw.top.etemplate2.getByApplication(framework.activeApp.appName); - if (app_templates.length > 0) { - app_templates[0].download(data); - } - else { - // Couldn't download in opener, download here before popup closes - this.et2.getInstanceManager().download(data); - } - } - export_preview(event, widget) { - var preview = jQuery(widget.getRoot().getWidgetById('preview_box').getDOMNode()); - jQuery('.content', preview).empty() - .append('
      '); - preview - .show(100, jQuery.proxy(function () { - widget.clicked = true; - widget.getInstanceManager().submit(false, true); - widget.clicked = false; - }, this)); - return false; - } - import_preview(event, widget) { - var test = widget.getRoot().getWidgetById('dry-run'); - if (test.getValue() == test.options.unselected_value) - return true; - // Show preview - var preview = jQuery(widget.getRoot().getWidgetById('preview_box').getDOMNode()); - jQuery('.content', preview).empty(); - preview - .addClass('loading') - .show(100, jQuery.proxy(function () { - widget.clicked = true; - widget.getInstanceManager().submit(false, true); - widget.clicked = false; - jQuery(widget.getRoot().getWidgetById('preview_box').getDOMNode()) - .removeClass('loading'); - }, this)); - return false; - } - /** - * Open a popup to run a given definition - * - * @param {egwAction} action - * @param {egwActionObject[]} selected - */ - run_definition(action, selected) { - if (!selected || selected.length != 1) - return; - var id = selected[0].id || null; - var data = egw.dataGetUIDdata(id).data; - if (!data || !data.type) - return; - egw.open_link(egw.link('/index.php', { - menuaction: 'importexport.importexport_' + data.type + '_ui.' + data.type + '_dialog', - appname: data.application, - definition: data.definition_id - }), "", '850x440', data.application); - } - /** - * Allowed users widget has been changed, if 'All users' or 'Just me' - * was selected, turn off any other options. - */ - allowed_users_change(node, widget) { - var value = widget.getValue(); - // Only 1 selected, no checking needed - if (value == null || value.length <= 1) - return; - // Don't jump it to the top, it's weird - widget.selected_first = false; - var index = null; - var specials = ['', 'all']; - for (var i = 0; i < specials.length; i++) { - var special = specials[i]; - if ((index = value.indexOf(special)) >= 0) { - if (window.event.target.value == special) { - // Just clicked all/private, clear the others - value = [special]; - } - else { - // Just added another, clear special - value.splice(index, 1); - } - // A little highlight to call attention to the change - jQuery('input[value="' + special + '"]', node).parent().parent().effect('highlight', {}, 500); - break; - } - } - if (index >= 0) { - widget.set_value(value); - } - } -} -app.classes.importexport = ImportExportApp; -//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/infolog/js/app.js b/infolog/js/app.js deleted file mode 100644 index d0afa26bbf..0000000000 --- a/infolog/js/app.js +++ /dev/null @@ -1,870 +0,0 @@ -import { E as EgwApp, e as egw, a as etemplate2, B as nm_open_popup } from '../../chunks/etemplate2-0eb045cf.js'; -import { C as CRMView } from '../../chunks/CRM-49d7b139.js'; -import '../../chunks/egw_dragdrop_dhtmlx_tree-31643465.js'; -import '../../chunks/egw-5f30b5ae.js'; -import '../../vendor/bower-asset/jquery/dist/jquery.min.js'; -import '../../vendor/bower-asset/jquery-ui/jquery-ui.js'; -import '../../chunks/egw_json-98998d7e.js'; -import '../../chunks/egw_core-0ec5dc11.js'; -import '../../vendor/tinymce/tinymce/tinymce.min.js'; - -/** - * EGroupware - Infolog - Javascript UI - * - * @link: https://www.egroupware.org - * @package infolog - * @author Hadi Nategh - * @copyright (c) 2008-13 by Ralf Becker - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - */ -/** - * UI for Infolog - * - * @augments AppJS - */ - -class InfologApp extends EgwApp { - // These fields help with push filtering & access control to see if we care about a push message - push_grant_fields = ["info_owner", "info_responsible"]; - push_filter_fields = ["info_owner", "info_responsible"]; - /** - * Constructor - * - * @memberOf app.infolog - */ - - constructor() { - // call parent - super('infolog'); - } - /** - * Destructor - */ - - - destroy(_app) { - // call parent - super.destroy(_app); - } - /** - * 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 {etemplate2} _et2 newly ready object - * @param {string} _name template name - */ - - - et2_ready(_et2, _name) { - // call parent - super.et2_ready(_et2, _name); // CRM View - - if (typeof CRMView !== "undefined") { - CRMView.view_ready(_et2, this); - } - - switch (_name) { - case 'infolog.index': - this.filter_change(); // Show / hide descriptions according to details filter - - var nm = this.et2.getWidgetById('nm'); - var filter2 = nm.getWidgetById('filter2'); - this.show_details(filter2.get_value() == 'all', nm.getDOMNode(nm)); // Remove the rule added by show_details() if the template is removed - - jQuery(_et2.DOMContainer).on('clear', jQuery.proxy(function () { - egw.css(this); - }, '#' + nm.getDOMNode(nm).id + ' .et2_box.infoDes')); // Enable decrypt on hover - - if (this.egw.user('apps').stylite) { - this._get_stylite(function () { - this.mailvelopeAvailable(function () { - app.stylite?.decrypt_hover(nm); - }); - }); - } // blur count, if limit modified optimization used - - - if (nm.getController()?.getTotalCount() === 9999) { - this.blurCount(true); - } - - break; - - case 'infolog.edit.print': - if (this.et2.getArrayMgr('content').data.info_des.indexOf(this.begin_pgp_message) != -1) { - this.mailvelopeAvailable(this.printEncrypt); - } else { - // Trigger print command if the infolog oppend for printing purpose - this.infolog_print_preview_onload(); - } - - break; - - case 'infolog.edit': - if (this.et2.getArrayMgr('content').data.info_des && this.et2.getArrayMgr('content').data.info_des.indexOf(this.begin_pgp_message) != -1) { - this._get_stylite(jQuery.proxy(function () { - this.mailvelopeAvailable(jQuery.proxy(function () { - this.toggleEncrypt(); // Decrypt history on hover - - var history = this.et2.getWidgetById('history'); - app.stylite.decrypt_hover(history, 'span'); - jQuery(history.getDOMNode(history)).tooltip('option', 'position', { - my: 'top left', - at: 'top left', - of: history.getDOMNode(history) - }); - }, this)); - }, this)); // This disables the diff in history - - - var history = this.et2.getArrayMgr('content').getEntry('history'); - history['status-widgets'].De = 'description'; - } - - break; - } - } - /** - * Observer method receives update notifications from all applications - * - * InfoLog currently reacts to timesheet updates, as it might show time-sums. - * @todo only trigger update, if times are shown - * - * @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 - */ - - - observer(_msg, _app, _id, _type, _msg_type, _links) { - if (typeof _links != 'undefined') { - if (typeof _links.infolog != 'undefined') { - switch (_app) { - case 'timesheet': - var nm = this.et2 ? this.et2.getWidgetById('nm') : null; - if (nm) nm.applyFilters(); - break; - } - } - } // Refresh handler for Addressbook CRM view - - - if (_app == 'infolog' && this.et2.getInstanceManager() && this.et2.getInstanceManager().app == 'addressbook' && this.et2.getInstanceManager().name == 'infolog.index') { - this.et2._inst.refresh(_msg, _app, _id, _type); - } - } - /** - * Retrieve the current state of the application for future restoration - * - * Reimplemented to add action/action_id from content set by server - * when eg. viewing infologs linked to contacts. - * - * @return {object} Application specific map representing the current state - */ - - - getState() { - let state = { - action: null, - action_id: null - }; - let nm = {}; // Get index etemplate - - var et2 = etemplate2.getById('infolog-index'); - - if (et2) { - state = et2.widgetContainer.getWidgetById("nm").getValue(); - let content = et2.widgetContainer.getArrayMgr('content'); - nm = content && content.data && content.data.nm ? content.data.nm : {}; - } - - state.action = nm.action || null; - state.action_id = nm.action_id || null; - return state; - } - /** - * Set the application's state to the given state. - * - * Reimplemented to also reset action/action_id. - * - * @param {{name: string, state: object}|string} state Object (or JSON string) for a state. - * Only state is required, and its contents are application specific. - * - * @return {boolean} false - Returns false to stop event propagation - */ - - - setState(state) { - // as we have to set state.state.action, we have to set all other - // for "No filter" favorite to work as expected - var to_set = { - col_filter: null, - filter: '', - filter2: '', - cat_id: '', - search: '', - action: null - }; - if (typeof state.state == 'undefined') state.state = {}; - - for (var name in to_set) { - if (typeof state.state[name] == 'undefined') state.state[name] = to_set[name]; - } - - return super.setState(state); - } - /** - * Enable or disable the date filter - * - * If the filter is set to something that needs dates, we enable the - * header_left template. Otherwise, it is disabled. - */ - - - filter_change() { - var filter = this.et2.getWidgetById('filter'); - var nm = this.et2.getWidgetById('nm'); - var dates = this.et2.getWidgetById('infolog.index.dates'); - - if (nm && filter) { - switch (filter.getValue()) { - case 'bydate': - case 'duedate': - if (filter && dates) { - dates.set_disabled(false); - window.setTimeout(function () { - jQuery(dates.getWidgetById('startdate').getDOMNode()).find('input').focus(); - }, 0); - } - - break; - - default: - if (dates) { - dates.set_disabled(true); - } - - break; - } - } - } - /** - * show or hide the details of rows by selecting the filter2 option - * either 'all' for details or 'no_description' for no details - * - * @param {Event} event Change event - * @param {et2_nextmatch} nm The nextmatch widget that owns the filter - */ - - - filter2_change(event, nm) { - var filter2 = nm.getWidgetById('filter2'); - - if (nm && filter2) { - // Show / hide descriptions - this.show_details(filter2.get_value() === 'all', nm.getDOMNode(nm)); - } // Only change columns for a real user event, to avoid interfering with - // favorites - - - if (nm && filter2 && !nm.update_in_progress) { - // Store selection as implicit preference - egw.set_preference('infolog', nm.options.settings.columnselection_pref.replace('-details', '') + '-details-pref', filter2.get_value()); // Change preference location - widget is nextmatch - - nm.options.settings.columnselection_pref = nm.options.settings.columnselection_pref.replace('-details', '') + (filter2.get_value() == 'all' ? '-details' : ''); // Load new preferences - - var colData = nm.columns.slice(); - - for (var i = 0; i < nm.columns.length; i++) colData[i].visible = false; - - if (egw.preference(nm.options.settings.columnselection_pref, 'infolog')) { - nm.set_columns(egw.preference(nm.options.settings.columnselection_pref, 'infolog').split(',')); - } - - nm._applyUserPreferences(nm.columns, colData); // Now apply them to columns - - - for (var i = 0; i < colData.length; i++) { - nm.dataview.getColumnMgr().columns[i].set_width(colData[i].width); - nm.dataview.getColumnMgr().columns[i].set_visibility(colData[i].visible); - } - - nm.dataview.getColumnMgr().updated(); // Update page - set update_in_progress to true to avoid triggering - // the change handler and looping if the user has a custom field - // column change - - var in_progress = nm.update_in_progress; - nm.update_in_progress = true; // Set the actual filter value here - - nm.activeFilters.filter2 = filter2.get_value(); - nm.dataview.updateColumns(); - nm.update_in_progress = in_progress; - } - - return false; - } - /** - * Show or hide details by changing the CSS class - * - * @param {boolean} show - * @param {DOMNode} dom_node - */ - - - show_details(show, dom_node) { - // Show / hide descriptions - egw.css((dom_node && dom_node.id ? "#" + dom_node.id + ' ' : '') + ".et2_box.infoDes", "display:" + (show ? "block;" : "none;")); - - if (egwIsMobile()) { - var $select = jQuery('.infoDetails'); - show ? $select.each(function (i, e) { - jQuery(e).hide(); - }) : $select.each(function (i, e) { - jQuery(e).show(); - }); - } - } - /** - * Confirm delete - * If entry has children, asks if you want to delete children too - * - *@param _action - *@param _senders - */ - - - confirm_delete(_action, _senders) { - let children = false; - let child_button = jQuery('#delete_sub').get(0) || jQuery('[id*="delete_sub"]').get(0); - this._action_all = _action.parent.data.nextmatch?.getSelection().all; - this._action_ids = []; - - if (child_button) { - for (let i = 0; i < _senders.length; i++) { - this._action_ids.push(_senders[i].id.split("::").pop()); - - if (jQuery(_senders[i].iface.getDOMNode()).hasClass('infolog_rowHasSubs')) { - children = true; - break; - } - } - - child_button.style.display = children ? 'block' : 'none'; - } - - nm_open_popup(_action, _senders); - } - - _action_ids = []; - _action_all = false; - /** - * Callback for action using ids set(!) in this._action_ids and this._action_all - * - * @param _action - */ - - actionCallback(_action) { - egw.json("infolog.infolog_ui.ajax_action", [_action, this._action_ids, this._action_all]).sendRequest(true); - } - /** - * Add email from addressbook - * - * @param ab_id - * @param info_cc - */ - - - add_email_from_ab(ab_id, info_cc) { - var ab = document.getElementById(ab_id); - - if (!ab || !ab.value) { - jQuery("tr.hiddenRow").css("display", "table-row"); - } else { - var cc = document.getElementById(info_cc); - - for (var i = 0; i < ab.options.length && ab.options[i].value != ab.value; ++i); - - if (i < ab.options.length) { - cc.value += (cc.value ? ', ' : '') + ab.options[i].text.replace(/^.* <(.*)>$/, '$1'); - ab.value = ''; // @ts-ignore - - ab.onchange(); - jQuery("tr.hiddenRow").css("display", "none"); - } - } - - return false; - } - /** - * If one of info_status, info_percent or info_datecompleted changed --> set others to reasonable values - * - * @param {string} changed_id id of changed element - * @param {string} status_id - * @param {string} percent_id - * @param {string} datecompleted_id - */ - - - status_changed(changed_id, status_id, percent_id, datecompleted_id) { - // Make sure this doesn't get executed while template is loading - if (this.et2 == null || this.et2.getInstanceManager() == null) return; - var status = document.getElementById(status_id); - var percent = document.getElementById(percent_id); - var datecompleted = document.getElementById(datecompleted_id + '[str]'); - - if (!datecompleted) { - datecompleted = jQuery('#' + datecompleted_id + ' input').get(0); - } - - var completed; - - switch (changed_id) { - case status_id: - completed = status.value == 'done' || status.value == 'billed'; - - if (completed || status.value == 'not-started' || status.value == 'ongoing' != (parseFloat(percent.value) > 0 && parseFloat(percent.value) < 100)) { - if (completed) { - percent.value = '100'; - } else if (status.value == 'not-started') { - percent.value = '0'; - } else if (!completed && (parseInt(percent.value) == 0 || parseInt(percent.value) == 100)) { - percent.value = '10'; - } - } - - break; - - case percent_id: - completed = parseInt(percent.value) == 100; - - if (completed != (status.value == 'done' || status.value == 'billed') || status.value == 'not-started' != (parseInt(percent.value) == 0)) { - status.value = parseInt(percent.value) == 0 ? jQuery('[value="not-started"]', status).length ? 'not-started' : 'ongoing' : parseInt(percent.value) == 100 ? 'done' : 'ongoing'; - } - - break; - - case datecompleted_id + '[str]': - case datecompleted_id: - completed = datecompleted.value != ''; - - if (completed != (status.value == 'done' || status.value == 'billed')) { - status.value = completed ? 'done' : 'not-started'; - } - - if (completed != (parseInt(percent.value) == 100)) { - percent.value = completed ? '100' : '0'; - } - - break; - } - - if (!completed && datecompleted && datecompleted.value != '') { - datecompleted.value = ''; - } else if (completed && datecompleted && datecompleted.value == '') {// todo: set current date in correct format - } - } - /** - * handle "print" action from "Actions" selectbox in edit infolog window. - * check if the template is dirty then submit the template otherwise just open new window as print. - * - */ - - - edit_actions() { - var widget = this.et2.getWidgetById('action'); - var template = this.et2._inst; - - if (template) { - var id = template.widgetContainer.getArrayMgr('content').data['info_id']; - } - - if (widget) { - switch (widget.get_value()) { - case 'print': - if (template.isDirty()) { - template.submit(); - } - - egw.open(id, 'infolog', 'edit', { - print: 1 - }); - break; - - case 'ical': - template.postSubmit(); - break; - - default: - template.submit(); - } - } - } - /** - * Open infolog entry for printing - * - * @param {aciton object} _action - * @param {object} _selected - */ - - - infolog_menu_print(_action, _selected) { - var id = _selected[0].id.replace(/^infolog::/g, ''); - - egw.open(id, 'infolog', 'edit', { - print: 1 - }); - } - /** - * Trigger print() onload window - */ - - - infolog_print_preview_onload() { - var that = this; - jQuery('#infolog-edit-print').bind('load', function () { - var isLoadingCompleted = true; - jQuery('#infolog-edit-print').bind("DOMSubtreeModified", function (event) { - isLoadingCompleted = false; - jQuery('#infolog-edit-print').unbind("DOMSubtreeModified"); - }); - setTimeout(function () { - isLoadingCompleted = false; - }, 1000); - var interval = setInterval(function () { - if (!isLoadingCompleted) { - clearInterval(interval); - that.infolog_print_preview(); - } - }, 100); - }); - } - /** - * Trigger print() function to print the current window - */ - - - infolog_print_preview() { - this.egw.message(this.egw.lang('Printing...')); - this.egw.window.print(); - } - /** - * - */ - - - add_link_sidemenu() { - egw.open('', 'infolog', 'add'); - } - /** - * Wrapper so add -> New actions 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, action.id, nm.getArrayMgr('content').getEntry('action'), nm.getArrayMgr('content').getEntry('action_id')); - } - } - /** - * Opens a new edit dialog with some extra url parameters pulled from - * standard locations. Done with a function instead of hardcoding so - * the values can be updated if user changes them in UI. - * - * @param {et2_widget} widget Originating/calling widget - * @param _type string Type of infolog entry - * @param _action string Special action for new infolog entry - * @param _action_id string ID for special action - */ - - - add_with_extras(widget, _type, _action, _action_id) { - // We use widget.getRoot() instead of this.et2 for the case when the - // addressbook tab is viewing a contact + infolog list, there's 2 infolog - // etemplates - var nm = widget.getRoot().getWidgetById('nm'); - var nm_value = nm.getValue() || {}; // It's important that all these keys are here, they override the link - // registry. - - var action_id = nm_value.action_id ? nm_value.action_id : (_action_id != '0' ? _action_id : "") || ""; - - if (typeof action_id == "object" && typeof action_id.length == "undefined") { - // Need a real array here - action_id = jQuery.map(action_id, function (val) { - return val; - }); - } // No action? Try the linked filter, in case it's set - - - if (!_action && !_action_id) { - if (nm_value.col_filter && nm_value.col_filter.linked) { - var split = nm_value.col_filter.linked.split(':') || ''; - _action = split[0] || ''; - action_id = split[1] || ''; - } - } - - var extras = { - type: _type || nm_value.col_filter.info_type || "task", - cat_id: nm_value.cat_id || "", - action: nm_value.action || _action || "", - // egw_link can handle arrays; but server is expecting CSV - action_id: typeof action_id.join != "undefined" ? action_id.join(',') : action_id - }; - egw.open('', 'infolog', 'add', extras); - } - /** - * Get title in order to set it as document title - * @returns {string} - */ - - - getWindowTitle() { - var widget = this.et2.getWidgetById('info_subject'); - if (widget) return widget.options.value; - } - /** - * View parent entry with all children - * - * @param {aciton object} _action - * @param {object} _selected - */ - - - view_parent(_action, _selected) { - var data = egw.dataGetUIDdata(_selected[0].id); - - if (data && data.data && data.data.info_id_parent) { - egw.link_handler(egw.link('/index.php', { - menuaction: "infolog.infolog_ui.index", - action: "sp", - action_id: data.data.info_id_parent, - ajax: "true" - }), "infolog"); - } - } - /** - * Mess with the query for parent widget to exclude self - * - * @param {Object} request - * @param {et2_link_entry} widget - * @returns {boolean} - */ - - - parent_query(request, widget) { - // No ID yet, no need to filter - if (!widget.getRoot().getArrayMgr('content').getEntry('info_id')) { - return true; - } - - if (!request.options) { - request.options = {}; - } // Exclude self from results - no app needed since it's just one app - - - request.options.exclude = [widget.getRoot().getArrayMgr('content').getEntry('info_id')]; - return true; - } - /** - * View a list of timesheets for the linked infolog entry - * - * Only one infolog entry at a time is allowed, we just pick the first one - * - * @param {egwAction} _action - * @param {egwActionObject[]} _selected - */ - - - timesheet_list(_action, _selected) { - var extras = { - link_app: 'infolog', - link_id: false - }; - - for (var i = 0; i < _selected.length; i++) { - // Remove UID prefix for just contact_id - var ids = _selected[i].id.split('::'); - - ids.shift(); - ids = ids.join('::'); - extras.link_id = ids; - break; - } - - egw.open("", "timesheet", "list", extras, 'timesheet'); - } - /** - * Go to parent entry - * - * @param {aciton object} _action - * @param {object} _selected - */ - - - has_parent(_action, _selected) { - var data = egw.dataGetUIDdata(_selected[0].id); - return data && data.data && data.data.info_id_parent > 0; - } - /** - * Submit template if widget has a value - * - * Used for project-selection to update pricelist items from server - * - * @param {DOMNode} _node - * @param {et2_widget} _widget - */ - - - submit_if_not_empty(_node, _widget) { - if (_widget.get_value()) this.et2._inst.submit(); - } - /** - * Toggle encryption - * - * @param {jQuery.Event} _event - * @param {et2_button} _widget - * @param {DOMNode} _node - */ - - - toggleEncrypt(_event, _widget, _node) { - if (!this.egw.user('apps').stylite) { - this.egw.message(this.egw.lang('InfoLog encryption requires EPL Subscription') + ': www.egroupware.org/EPL'); - return; - } - - this._get_stylite(function () { - app.stylite.toggleEncrypt.call(app.stylite, _event, _widget, _node); - }); - } - /** - * Make sure stylite javascript is loaded, and call the given callback when it is - * - * @param {function} callback - * @param {object} attrs - * - */ - - - _get_stylite(callback, attrs) { - // use app object from etemplate2, which might be private and not just window.app - var app = this.et2.getInstanceManager().app_obj; - - if (!app.stylite) { - this.egw.includeJS('/stylite/js/app.js', undefined, undefined, egw.webserverUrl).then(() => { - app.stylite = new app.classes.stylite(); - app.stylite.et2 = this.et2; - - if (callback) { - callback.apply(app.stylite, attrs); - } - }); - } else { - app.stylite.et2 = this.et2; - callback.apply(app.stylite, attrs); - } - } - /** - * OnChange callback for responsible - * - * @param {jQuery.Event} _event - * @param {et2_widget} _widget - */ - - - onchangeResponsible(_event, _widget) { - if (app.stylite && app.stylite.onchangeResponsible) { - app.stylite.onchangeResponsible.call(app.stylite, _event, _widget); - } - } - /** - * Action handler for context menu change responsible action - * - * We populate the dialog with the current value. - * - * @param {egwAction} _action - * @param {egwActionObject[]} _selected - */ - - - change_responsible(_action, _selected) { - var et2 = _selected[0].manager.data.nextmatch.getInstanceManager(); - - var responsible = et2.widgetContainer.getWidgetById('responsible'); - - if (responsible) { - responsible.set_value([]); - et2.widgetContainer.getWidgetById('responsible_action[title]').set_value(''); - et2.widgetContainer.getWidgetById('responsible_action[title]').set_class(''); - et2.widgetContainer.getWidgetById('responsible_action[ok]').set_disabled(_selected.length !== 1); - et2.widgetContainer.getWidgetById('responsible_action[add]').set_disabled(_selected.length === 1); - et2.widgetContainer.getWidgetById('responsible_action[delete]').set_disabled(_selected.length === 1); - } - - if (_selected.length === 1) { - var data = egw.dataGetUIDdata(_selected[0].id); - - if (responsible && data && data.data) { - et2.widgetContainer.getWidgetById('responsible_action[title]').set_value(data.data.info_subject); - et2.widgetContainer.getWidgetById('responsible_action[title]').set_class(data.data.sub_class); - responsible.set_value(data.data.info_responsible); - } - } - - nm_open_popup(_action, _selected); - } - /** - * Handle encrypted info_desc for print purpose - * and triggers print action after decryption - * - * @param {Keyring} _keyring Mailvelope keyring to use - */ - - - printEncrypt(_keyring) { - //this.mailvelopeAvailable(this.toggleEncrypt); - var info_desc = this.et2.getWidgetById('info_des'); - var self = this; - mailvelope.createDisplayContainer('#infolog-edit-print_info_des', info_desc.value, _keyring).then(function (_container) { - var $info_des_dom = jQuery(self.et2.getWidgetById('info_des').getDOMNode()); // $info_des_dom.children('iframe').height($info_des_dom.height()); - - $info_des_dom.children('span').hide(); //Trigger print action - - self.infolog_print_preview(); - }, function (_err) { - self.egw.message(_err, 'error'); - }); - } - /** - * Blur NM count (used for limit modified optimization not returning (an exact) count - * - * @param blur - */ - - - blurCount(blur) { - document.querySelector('div#infolog-index_nm.et2_nextmatch .header_count')?.classList.toggle('blur_count', blur); - } - -} - -app.classes.infolog = InfologApp; -//# sourceMappingURL=app.js.map diff --git a/preferences/js/app.js b/preferences/js/app.js deleted file mode 100644 index d63e8b4f0f..0000000000 --- a/preferences/js/app.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * EGroupware Preferences - * - * @link https://www.egroupware.org - * @author Ralf Becker - * @package preferences - */ - -import {AppJS} from "../../api/js/jsapi/app_base.js"; - -/** - * JavaScript for WebAuthn - * - * @augments AppJS - */ -app.classes.preferences = AppJS.extend( -{ - appname: 'preferences', - - /** - * 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); - - } -}); diff --git a/resources/js/app.js b/resources/js/app.js deleted file mode 100644 index b3a9b620f0..0000000000 --- a/resources/js/app.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * EGroupware - Resources - Javascript UI - * - * @link https://www.egroupware.org - * @package resources - * @author Hadi Nategh - * @copyright (c) 2008-21 by Ralf Becker - * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - */ -import { EgwApp } from "../../api/js/jsapi/egw_app"; -import { fetchAll } from "../../api/js/etemplate/et2_extension_nextmatch_actions.js"; -import { egw } from "../../api/js/jsapi/egw_global"; -/** - * UI for resources - */ -class resourcesApp extends EgwApp { - /** - * Constructor - */ - constructor() { - super('resources'); - } - /** - * Destructor - */ - destroy(_app) { - delete this.et2; - super.destroy(_app); - } - /** - * 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(). - */ - et2_ready(et2, name) { - super.et2_ready(et2, name); - } - /** - * call calendar planner by selected resources - * - * @param {action} _action actions - * @param {action} _senders selected action - * - */ - view_calendar(_action, _senders) { - let res_ids = []; - let matches = []; - let nm = _action.parent.data.nextmatch; - let selection = nm.getSelection(); - let show_calendar = function (res_ids) { - egw(window).message(this.egw.lang('%1 resource(s) View calendar', res_ids.length)); - let current_owners = (app.calendar ? app.calendar.state.owner || [] : []).join(','); - if (current_owners) { - current_owners += ','; - } - this.egw.open_link('calendar.calendar_uiviews.index&view=planner&sortby=user&owner=' + current_owners + 'r' + res_ids.join(',r') + '&ajax=true'); - }.bind(this); - if (selection && selection.all) { - // Get selected ids from nextmatch - it will ask server if user did 'select all' - fetchAll(res_ids, nm, show_calendar); - } - else { - for (let i = 0; i < _senders.length; i++) { - res_ids.push(_senders[i].id); - matches = res_ids[i].match(/^(?:resources::)?([0-9]+)(:([0-9]+))?$/); - if (matches) { - res_ids[i] = matches[1]; - } - } - show_calendar(res_ids); - } - } - /** - * Calendar sidebox hook change handler - * - */ - sidebox_change(ev, widget) { - if (ev[0] != 'r') { - widget.setSubChecked(ev, widget.getValue()[ev].value || false); - } - let owner = jQuery.extend([], app.calendar.state.owner) || []; - for (let i = owner.length - 1; i >= 0; i--) { - if (owner[i][0] == 'r') { - owner.splice(i, 1); - } - } - let value = widget.getValue(); - for (let key in value) { - if (key[0] !== 'r') - continue; - if (value[key].value && owner.indexOf(key) === -1) { - owner.push(key); - } - } - app.calendar.update_state({ owner: owner }); - } - /** - * Book selected resource for calendar - * - * @param {action} _action actions - * @param {action} _senders selected action - */ - book(_action, _senders) { - let res_ids = [], matches = []; - for (let i = 0; i < _senders.length; i++) { - res_ids.push(_senders[i].id); - matches = res_ids[i].match(/^(?:resources::)?([0-9]+)(:([0-9]+))?$/); - if (matches) { - res_ids[i] = matches[1]; - } - } - egw(window).message(this.egw.lang('%1 resource(s) booked', res_ids.length)); - this.egw.open_link('calendar.calendar_uiforms.edit&participants=r' + res_ids.join(',r'), '_blank', '700x700'); - } - /** - * set the picture_src to own_src by uploding own file - * - */ - select_picture_src() { - let rBtn = this.et2.getWidgetById('picture_src'); - if (typeof rBtn != 'undefined') { - rBtn.set_value('own_src'); - } - } -} -app.classes.resources = resourcesApp; -//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/timesheet/js/app.js b/timesheet/js/app.js deleted file mode 100644 index e2973c4845..0000000000 --- a/timesheet/js/app.js +++ /dev/null @@ -1,214 +0,0 @@ -import { E as EgwApp, e as egw } from '../../chunks/etemplate2-0eb045cf.js'; -import '../../chunks/egw_dragdrop_dhtmlx_tree-31643465.js'; -import '../../chunks/egw-5f30b5ae.js'; -import '../../vendor/bower-asset/jquery/dist/jquery.min.js'; -import '../../vendor/bower-asset/jquery-ui/jquery-ui.js'; -import '../../chunks/egw_json-98998d7e.js'; -import '../../chunks/egw_core-0ec5dc11.js'; -import '../../vendor/tinymce/tinymce/tinymce.min.js'; - -/** - * 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 - */ -/** - * UI for timesheet - * - * @augments AppJS - */ - -class TimesheetApp extends EgwApp { - // These fields help with push filtering & access control to see if we care about a push message - push_grant_fields = ["ts_owner"]; - push_filter_fields = ["ts_owner"]; - - constructor() { - super('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) { - // call parent - super.et2_ready(et2, name); - - if (name == 'timesheet.index') { - this.filter_change(); - this.filter2_change(); - } - } - /** - * - */ - - - filter_change() { - var filter = this.et2.getWidgetById('filter'); - var dates = this.et2.getWidgetById('timesheet.index.dates'); - let nm = this.et2.getDOMWidgetById('nm'); - - if (filter && dates) { - dates.set_disabled(filter.get_value() !== "custom"); - if (filter.get_value() == 0) nm.activeFilters.startdate = null; - - 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 = {}; - - 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; - } - /** - * Run action via ajax - * - * @param _action - * @param _senders - */ - - - ajax_action(_action, _senders) { - let all = _action.parent.data.nextmatch?.getSelection().all; - let ids = []; - - for (let i = 0; i < _senders.length; i++) { - ids.push(_senders[i].id.split("::").pop()); - } - - egw.json("timesheet.timesheet_ui.ajax_action", [_action.id, ids, all]).sendRequest(true); - } - -} - -app.classes.timesheet = TimesheetApp; -//# sourceMappingURL=app.js.map