/** * EGroupware clientside API object * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel (as AT stylite.de) * @author Ralf Becker */ /*egw:uses egw_core; */ import './egw_core.js'; egw.extend('utils', egw.MODULE_GLOBAL, function() { "use strict"; function json_escape_string(input) { var len = input.length; var res = ""; for (var i = 0; i < len; i++) { switch (input.charAt(i)) { case '"': res += '\\"'; break; case '\n': res += '\\n'; break; case '\r': res += '\\r'; break; case '\\': res += '\\\\'; break; case '\/': res += '\\/'; break; case '\b': res += '\\b'; break; case '\f': res += '\\f'; break; case '\t': res += '\\t'; break; default: res += input.charAt(i); } } return res; } function json_encode_simple(input) { switch (input.constructor) { case String: return '"' + json_escape_string(input) + '"'; case Number: return input.toString(); case Boolean: return input ? 'true' : 'false'; default: return null; } } function json_encode(input) { if (input == null || !input && input.length == 0) return 'null'; var simple_res = json_encode_simple(input); if (simple_res == null) { switch (input.constructor) { case Array: var buf = []; for (var k in input) { //Filter non numeric entries if (!isNaN(k)) buf.push(json_encode(input[k])); } return '[' + buf.join(',') + ']'; case Object: var buf = []; for (var k in input) { buf.push(json_encode_simple(k) + ':' + json_encode(input[k])); } return '{' + buf.join(',') + '}'; default: switch(typeof input) { case 'array': var buf = []; for (var k in input) { //Filter non numeric entries if (!isNaN(k)) buf.push(json_encode(input[k])); } return '[' + buf.join(',') + ']'; case 'object': var buf = []; for (var k in input) { buf.push(json_encode_simple(k) + ':' + json_encode(input[k])); } return '{' + buf.join(',') + '}'; } return 'null'; } } else { return simple_res; } } /** * Try some deprecated ways of copying to the OS clipboard * * @param event Optional, but if you have an event we can try some things on it * @param target_element Element whose contents you're trying to copy * @param text Actual text. Usually target_element.value. * @returns {boolean} */ function fallbackCopyTextToClipboard(event, target_element, text) { const win = target_element?.ownerDocument.defaultView ?? target_element.ownerDocument.parentWindow ?? window; // Cancel any no-select css if (target_element) { let old_select = target_element.style.userSelect; target_element.style.userSelect = 'all' let range = document.createRange(); range.selectNode(target_element); win.getSelection().removeAllRanges(); win.getSelection().addRange(range); target_element.style.userSelect = old_select; // detect we are in IE via checking setActive, since it's // only supported in IE, and make sure there's clipboardData object if (event && typeof event.target.setActive != 'undefined' && win.clipboardData) { win.clipboardData.setData('Text', target_element.textContent.trim()); } if (event && event.clipboardData) { event.clipboardData.setData('text/plain', target_element.textContent.trim()); event.clipboardData.setData('text/html', target_element.outerHTML); } } let textArea; if (!win.clipboardData) { textArea = document.createElement("textarea"); textArea.value = text; // Avoid scrolling to bottom textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.position = "fixed"; win.document.body.appendChild(textArea); textArea.focus(); textArea.select(); } let successful = false; try { successful = win.document.execCommand('copy'); const msg = successful ? 'successful' : 'unsuccessful'; console.log('Fallback: Copying text command was ' + msg); } catch (err) { successful = false; } win.document.body.removeChild(textArea); return successful; } var uid_counter = 0; /** * Global cache shared between all EGroupware windows * @type {{}} */ const cache = {}; // Create the utils object which contains references to all functions // covered by it. var utils = { /** * Get a cache object shared between all EGroupware windows * * @param {string} _name unique name for the cache-object * @return {*} */ getCache: function(_name) { if (typeof cache[_name] === 'undefined') cache[_name] = {}; return cache[_name]; }, /** * Invalidate / delete given part of the cache * * @param {string} _name unique name of cache-object * @param {string|RegExp|undefined} _attr undefined: invalidate/unset whole object or just the given attribute _attr or matching RegExp _attr */ invalidateCache: function(_name, _attr) { // string with regular expression like "/^something/i" if (typeof _attr === 'string' && _attr[0] === '/', _attr.indexOf('/', 1) !== -1) { let parts = _attr.split('/'); parts.shift(); const flags = parts.pop(); _attr = new RegExp(parts.join('/'), flags); } if (typeof _attr === 'undefined' || typeof cache[_name] === 'undefined') { delete cache[_name]; } else if (typeof _attr === 'object' && _attr.constructor.name === 'RegExp') { for(const attr in cache[_name]) { if (attr.match(_attr)) delete cache[_name][attr]; } } else { delete cache[_name][_attr]; } }, ajaxUrl: function(_menuaction) { if(_menuaction.indexOf('menuaction=') >= 0) { return _menuaction; } return this.webserverUrl + '/json.php?menuaction=' + _menuaction; }, elemWindow: function(_elem) { var res = _elem.ownerDocument.parentNode || _elem.ownerDocument.defaultView; return res; }, uid: function() { return (uid_counter++).toString(16); }, /** * Decode encoded vfs special chars * * @param {string} _path path to decode * @return {string} */ decodePath: function(_path) { try { return decodeURIComponent(_path); } catch(e) { // ignore decoding errors, as they usually only mean _path is not encoded egw.debug("error", "decodePath('"+_path+"'): "+e.stack); } return _path; }, /** * Encode vfs special chars excluding / * * @param {string} _path path to decode * @return {string} */ encodePath: function(_path) { var components = _path.split('/'); for(var n=0; n < components.length; n++) { components[n] = this.encodePathComponent(components[n]); } return components.join('/'); }, /** * Encode vfs special chars removing / * * '%' => '%25', * '#' => '%23', * '?' => '%3F', * '/' => '', // better remove it completly * * @param {string} _comp path to decode * @return {string} */ encodePathComponent: function(_comp) { return _comp.replace(/%/g,'%25').replace(/#/g,'%23').replace(/\?/g,'%3F').replace(/\//g,''); }, /** * Hash a string * * @param string */ async hashString(string) { const data = (new TextEncoder()).encode(string); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); return hashHex; }, /** * Escape HTML special chars, just like PHP * * @param {string} s String to encode * * @return {string} */ htmlspecialchars: function(s) { return s.replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); }, /** * If an element has display: none (or a parent like that), it has no size. * Use this to get its dimensions anyway. * * @param element HTML element * @param boolOuter Pass true to get outerWidth() / outerHeight() instead of width() / height() * * @return Object [w: width, h: height] * * @author Ryan Wheale * @see http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/ */ getHiddenDimensions: function(element, boolOuter) { var $item = jQuery(element); var props = { position: "absolute", visibility: "hidden", display: "block" }; var dim = { "w":0, "h":0 , "left":0, "top":0}; var $hiddenParents = $item.parents().andSelf().not(":visible"); var oldProps = []; $hiddenParents.each(function() { var old = {}; if (this.styles) { for (var name in props) { old[name] = this.style[name]; } } else if (this.computedStyleMap) { for (var name in props) { let s = this.computedStyleMap().get(name) if (s) { old[name] = s.value || ""; } } } jQuery(this).show(); oldProps.push(old); }); dim.w = (boolOuter === true) ? $item.outerWidth() : $item.width(); dim.h = (boolOuter === true) ? $item.outerHeight() : $item.height(); dim.top = $item.offset().top; dim.left = $item.offset().left; $hiddenParents.each(function(i) { var old = oldProps[i]; if (this.style) { for (var name in props) { this.style[name] = old[name]; } } }); //$.log(”w: ” + dim.w + ”, h:” + dim.h) return dim; }, /** * Store a window's name in egw.store so we can have a list of open windows * * @param {string} appname * @param {Window} popup */ storeWindow: function(appname, popup) { if (popup.opener && popup.opener.framework) popup.opener.framework.popups_garbage_collector(); // Don't store if it has no name if(!popup.name || ['_blank'].indexOf(popup.name) >= 0) { return; } var _target_app = appname || this.appName || egw_appName || 'common'; var open_windows = JSON.parse(this.getSessionItem(_target_app, 'windows')) || {}; open_windows[popup.name] = Date.now(); this.setSessionItem(_target_app, 'windows', JSON.stringify(open_windows)); // We don't want to start the timer on the popup here, because this is the function that updates the timeout, so it would set a timer each time. Timer is started in egw.js }, /** * Get a list of the names of open popups * * Using the name, you can get a reference to the popup using: * window.open('', name); * Popups that were not given a name when they were opened are not tracked. * * @param {string} appname Application that owns/opened the popup * @param {string} regex Optionally filter names by the given regular expression * * @returns {string[]} List of window names */ getOpenWindows: function(appname, regex) { var open_windows = JSON.parse(this.getSessionItem(appname, 'windows')) || {}; if(typeof regex == 'undefined') { return open_windows; } var list = []; var now = Date.now(); for(var i in open_windows) { // Expire old windows (5 seconds since last update) if(now - open_windows[i] > 5000) { egw.windowClosed(appname,i); continue; } if(i.match(regex)) { list.push(i); } } return list; }, /** * Notify egw of closing a named window, which removes it from the list * * @param {String} appname * @param {Window|String} closed Window that was closed, or its name * @returns {undefined} */ windowClosed: function (appname, closed) { var closed_name = typeof closed == "string" ? closed : closed.name; var closed_window = typeof closed == "string" ? null : closed; window.setTimeout(function () { if (closed_window != null && !closed_window.closed) { return; } var open_windows = JSON.parse(egw().getSessionItem(appname, 'windows')) || {}; delete open_windows[closed_name]; egw.setSessionItem(appname, 'windows', JSON.stringify(open_windows)); }, 100); }, /** * Copy text to the clipboard * * @param text Actual text to copy. Usually target_element.value * @param target_element Optional, but useful for fallback copy attempts * @param event Optional, but if you have an event we can try some fallback options with it * * @returns {Promise|Promise} */ copyTextToClipboard: function (text, target_element, event) { if (!navigator.clipboard) { let success = fallbackCopyTextToClipboard(event, target_element, text); return Promise.resolve(success ? undefined : false); } // Use Clipboard API const win = target_element?.ownerDocument.defaultView ?? target_element.ownerDocument.parentWindow ?? window; return win.navigator.clipboard.writeText(text); } }; // Check whether the browser already supports encoding JSON -- if yes, use // its implementation, otherwise our own if (typeof window.JSON !== 'undefined' && typeof window.JSON.stringify !== 'undefined') { utils["jsonEncode"] = JSON.stringify; } else { utils["jsonEncode"] = json_encode; } // Return the extension return utils; });