{ "version": 3, "sources": ["../../../../wh/whtree/modules/system/js/dompack/src/promise.es", "../../../../wh/whtree/modules/system/js/dompack/extra/storage.es", "../../../../wh/whtree/modules/system/js/dompack/extra/cookie.es", "../../../../wh/whtree/modules/system/js/dompack/src/debug.es", "../../../../wh/whtree/modules/system/js/dompack/src/events.es", "../../../../wh/whtree/modules/system/js/dompack/src/busy.es", "../../../../wh/whtree/modules/system/js/dompack/src/tree.es", "../../../../wh/whtree/modules/system/js/dompack/src/create.es", "../../../../wh/whtree/modules/system/js/dompack/src/components.es", "../../../../wh/whtree/modules/system/js/dompack/index.es", "../../../../wh/whtree/modules/system/js/wh/integration.es", "../../../../wh/whtree/modules/system/js/dompack/extra/keyboard.es", "../../../../wh/whtree/node_modules/events/events.js", "../../../../wh/whtree/modules/system/js/net/requester.es", "../../../../wh/whtree/modules/system/js/net/jsonrpc.es", "../../../../wh/whtree/modules/wrd/js/auth.es", "../../../../wh/whtree/modules/system/js/dompack/browserfix/focus.es", "../../../../wh/whtree/modules/system/js/dompack/types/text.es", "../../../../wh/whtree/modules/tollium/js/gettid.es", "../../../../wh/whtree/modules/tollium/web/ui/common.lang.json", "../../../../wh/whtree/modules/tollium/web/ui/js/support.es", "../../../../wh/whtree/modules/tollium/web/ui/js/componentbase.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/base/compbase.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/action/actionforwardbase.es", "../../../../wh/whtree/modules/system/js/dompack/extra/browser.es", "../../../../wh/whtree/modules/publisher/js/feedback/screenshot.es", "../../../../wh/whtree/modules/publisher/js/feedback/dompointer.es", "../../../../wh/whtree/modules/system/js/wh/rpc.es", "../../../../wh/whtree/modules/publisher/js/feedback/internal/feedback.rpc.json", "../../../../wh/whtree/modules/publisher/js/feedback/index.es", "../../../../wh/whtree/modules/tollium/js/internal/todd.rpc.json", "../../../../wh/whtree/modules/tollium/js/icons.es", "../../../../wh/whtree/modules/tollium/web/ui/js/dialogs/simplescreen.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/js/feedback.es", "../../../../wh/whtree/modules/system/js/compat/download.es", "../../../../wh/whtree/modules/tollium/web/ui/js/dialogs/uploadcontroller.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/imageeditor.lang.json", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/jpeg.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/exif.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/date.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/simplify.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/exif-tags.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/parser.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/dom-bufferstream.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/lib/bufferstream.js", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/exif-parser/index.js", "../../../../wh/whtree/modules/tollium/web/ui/components/toolbar/toolbars.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/surface.es", "../../../../wh/whtree/modules/system/js/dompack/browserfix/movable.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/surfacetool.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/smartcrop.js", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/crop.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/scaling.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/refpoint.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/filters.es", "../../../../wh/whtree/modules/tollium/web/ui/components/imageeditor/index.es", "../../../../wh/whtree/modules/tollium/web/ui/js/dialogs/imgeditcontroller.es", "../../../../wh/whtree/modules/system/js/compat/upload.es", "../../../../wh/whtree/modules/tollium/web/ui/js/upload.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/action/action.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/base/actionable.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/button/button.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/base/tools.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/buttongroup/buttongroup.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/checkbox/checkbox.es", "../../../../wh/whtree/modules/tollium/js/internal/scrollmonitor.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/codeedit/codeedit.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/customhtml/customhtml.es", "../../../../wh/whtree/modules/publisher/js/forms/internal/datehelpers.es", "../../../../wh/whtree/modules/publisher/js/forms/internal/datepicker.es", "../../../../wh/whtree/modules/publisher/js/forms/fields/datetime.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/datetime/datetime.lang.json", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/datetime/datetime.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/frame/dirtylistener.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/action/forward.es", "../../../../wh/whtree/modules/tollium/js/internal/keyboard.es", "../../../../wh/whtree/modules/tollium/web/ui/js/dragdrop.es", "../../../../wh/whtree/modules/tollium/web/ui/components/focuszones.es", "../../../../wh/whtree/modules/tollium/web/ui/components/basecontrols/menu.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/frame/frame.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/hr/hr.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/iframe/iframe.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/node_modules/@webhare/dompack-overlays/src/index.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/image/image.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/text/text.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/panel/panel.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/inlineblock/inlineblock.es", "../../../../wh/whtree/modules/system/js/dompack/types/email.es", "../../../../wh/whtree/modules/system/js/util/emailvalidation.es", "../../../../wh/whtree/modules/tollium/web/ui/components/listview/listcolumns.es", "../../../../wh/whtree/modules/system/js/internal/findasyoutype.es", "../../../../wh/whtree/modules/tollium/web/ui/components/listview/listview.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/list/list.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/menuitem/menuitem.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/progress/progress.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/frame/proxy.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/base/html.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/pulldown/pulldown.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/radiobutton/radiobutton.es", "../../../../wh/whtree/modules/system/js/internal/utf8.es", "../../../../wh/whtree/modules/tollium/web/ui/components/basecontrols/counter.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/support.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/dom/range.es", "../../../../wh/whtree/modules/system/js/frameworks/rangy/rangy13.js", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/domlevel.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/richdebug.es", "../../../../wh/whtree/modules/publisher/js/forms/internal/form.rpc.json", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/structured/lists.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/parsedstructure.es", "../../../../wh/whtree/modules/system/js/dompack/extra/preload.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/selection.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/tableeditor.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/editorbase.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/pastecleanup.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/structurededitor.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/free-editor.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/internal/toolbar.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/richeditor.lang.json", "../../../../wh/whtree/modules/system/js/internal/converthtmltoplaintext.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/editor.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/styling.es", "../../../../wh/whtree/modules/tollium/web/ui/components/richeditor/index.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/rte/rte.es", "../../../../wh/whtree/modules/tollium/web/ui/components/basecontrols/slider.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/slider/slider.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/spacer/spacer.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/split/split.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/table/table.es", "../../../../wh/whtree/modules/system/js/dompack/browserfix/scroll.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/tabs/tabs.es", "../../../../wh/whtree/modules/system/js/dompack/src/index.es", "../../../../wh/whtree/modules/system/js/dompack/components/internal/selectlist.es", "../../../../wh/whtree/modules/system/js/dompack/components/autosuggest/index.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/textedit/textedit.es", "../../../../wh/whtree/modules/tollium/web/ui/components/tagedit/tagedit.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/tagedit/tagedit.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/textarea/textarea.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/toolbar/toolbar.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/components/index.es", "../../../../wh/whtree/modules/tollium/web/ui/js/comm/linkendpoint.es", "../../../../wh/whtree/node_modules/stackframe/stackframe.js", "../../../../wh/whtree/node_modules/error-stack-parser/error-stack-parser.js", "../../../../wh/whtree/node_modules/stack-generator/stack-generator.js", "../../../../wh/whtree/node_modules/stacktrace-gps/node_modules/source-map/lib/util.js", "../../../../wh/whtree/node_modules/stacktrace-gps/node_modules/source-map/lib/binary-search.js", "../../../../wh/whtree/node_modules/stacktrace-gps/node_modules/source-map/lib/array-set.js", "../../../../wh/whtree/node_modules/stacktrace-gps/node_modules/source-map/lib/base64.js", "../../../../wh/whtree/node_modules/stacktrace-gps/node_modules/source-map/lib/base64-vlq.js", "../../../../wh/whtree/node_modules/stacktrace-gps/node_modules/source-map/lib/quick-sort.js", "../../../../wh/whtree/node_modules/stacktrace-gps/node_modules/source-map/lib/source-map-consumer.js", "../../../../wh/whtree/node_modules/stacktrace-gps/stacktrace-gps.js", "../../../../wh/whtree/node_modules/stacktrace-js/stacktrace.js", "../../../../wh/whtree/modules/system/js/wh/errorreporting.es", "../../../../wh/whtree/modules/tollium/web/ui/js/comm/transportbase.es", "../../../../wh/whtree/modules/tollium/web/ui/js/comm/toddservice.rpc.json", "../../../../wh/whtree/modules/tollium/web/ui/js/comm/jsonrpctransport.es", "../../../../wh/whtree/modules/tollium/web/ui/js/comm/websocket.es", "../../../../wh/whtree/modules/tollium/web/ui/js/comm/transportmanager.es", "../../../../wh/whtree/modules/system/js/wh/connect.es", "../../../../wh/whtree/modules/tollium/web/ui/js/debugging/magicmenu.es", "../../../../wh/whtree/modules/tollium/web/ui/js/shell/whcheck.es", "../../../../wh/whtree/modules/tollium/web/ui/js/shell/mousehandling.es", "../../../../wh/whtree/modules/tollium/web/ui/js/application/docpanel.es", "../../../../wh/whtree/modules/tollium/web/ui/js/application.es", "../../../../wh/whtree/modules/tollium/web/ui/js/components/jsx.es", "../../../../wh/whtree/modules/tollium/web/ui/js/shell/applicationbar.es", "../../../../wh/whtree/modules/tollium/web/ui/js/apps/dashboard.es", "../../../../wh/whtree/modules/tollium/web/ui/js/apps/login.es", "../../../../wh/whtree/modules/tollium/web/ui/js/apps/oauth.es", "../../../../wh/whtree/modules/tollium/web/ui/js/loginrequests.es", "../../../../wh/whtree/modules/tollium/web/ui/js/shell/towl.es", "../../../../wh/whtree/modules/system/js/net/eventserver.es", "../../../../wh/whtree/modules/tollium/web/ui/js/shell.es", "../../../../wh/whtree/modules/publisher/js/internal/polyfills/modern.es", "../../../../wh/whtree/modules/tollium/webdesigns/webinterface/webinterface.es", "../../../../../:entrypoint.js?%5B%22%2Fopt%2Fwh%2Fwhtree%2Fmodules%2Fpublisher%2Fjs%2Finternal%2Fpolyfills%2Fmodern.es%22%2C%22%2Fopt%2Fwh%2Fwhtree%2Fmodules%2Ftollium%2Fwebdesigns%2Fwebinterface%2Fwebinterface%22%5D"], "sourceRoot": "@mod-humpty/dumpty/had/a/great/fall/humpty/dumpty/fell/of/the/wall.js", "sourcesContent": ["// Workaround babel error that 'Promise.defer = ' isn't converted to the Babel promise type\nlet PromiseType = Promise;\nif(!PromiseType.prototype.finally)\n{\n /** Finally function for promises (executes callback without parameters, waits on returned thenables, then fulfills with\n original result\n */\n PromiseType.prototype.finally = function(callback)\n {\n // From https://github.com/domenic/promises-unwrapping/issues/18\n var constructor = this.constructor;\n return this.then(\n function(value) { return constructor.resolve(callback()).then(function() { return value; }); },\n function(reason) { return constructor.resolve(callback()).then(function() { throw reason; }); });\n };\n}\n\n/** Create a promise together with resolve & reject functions\n @return\n @cell return.promise\n @cell return.resolve\n @cell return.reject\n*/\nexport function createDeferred()\n{\n var deferred =\n { promise: null\n , resolve: null\n , reject: null\n };\n\n deferred.promise = new Promise(function(resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; });\n return deferred;\n}\n", "/* @recommendedimport: import * as storage from 'dompack/extra/storage';\n*/\n\nlet backupsession = {}, backuplocal = {};\nlet sessionfail, localfail;\n\n//isolate us when running previews, CI tests use same Chrome for both preview and tests so the previews start increasing visitorcounts behind our back\nconst isolated = \"whIsolateStorage\" in document.documentElement.dataset;\n\n/** @return True if our storage is fully isolated */\nexport function isIsolated()\n{\n return isolated;\n}\n\nexport function setSession(key, value)\n{\n try\n {\n if(value !== null)\n {\n backupsession[key] = value;\n if(!isolated)\n window.sessionStorage.setItem(key, JSON.stringify(value));\n }\n else\n {\n delete backupsession[key];\n if(!isolated)\n window.sessionStorage.removeItem(key);\n }\n\n if(sessionfail)\n {\n console.log(\"storage.setSession succeed after earlier fail\");\n sessionfail = false;\n }\n }\n catch(e)\n {\n if(!sessionfail)\n {\n console.log(\"storage.setSession failed\", e);\n sessionfail = true;\n }\n }\n}\n\nexport function getSession(key)\n{\n if(!isolated)\n {\n try\n {\n let retval = window.sessionStorage[key];\n try\n {\n return retval ? JSON.parse(retval) : null;\n }\n catch(e)\n {\n console.log(\"Failed to parse sessionStorage\",e,key);\n return null;\n }\n }\n catch(e)\n {\n if(!sessionfail)\n {\n console.log(\"getSessionStorage failed\", e);\n sessionfail = true;\n }\n }\n }\n return key in backupsession ? backupsession[key] : null;\n}\n\nexport function setLocal(key, value)\n{\n try\n {\n if(value !== null)\n {\n backuplocal[key] = value;\n if(!isolated)\n window.localStorage.setItem(key, JSON.stringify(value));\n }\n else\n {\n delete backuplocal[key];\n if(!isolated)\n window.localStorage.removeItem(key);\n }\n\n if(localfail)\n {\n console.log(\"storage.setLocal succeed after earlier fail\");\n localfail = false;\n }\n }\n catch(e)\n {\n if(!localfail)\n {\n console.log(\"storage.setLocal failed\", e);\n localfail = true;\n }\n }\n}\n\nexport function getLocal(key)\n{\n if(!isolated)\n {\n try\n {\n let retval = window.localStorage[key];\n try\n {\n return retval ? JSON.parse(retval) : null;\n }\n catch(e)\n {\n console.log(\"Failed to parse localStorage\",e,key);\n return null;\n }\n }\n catch(e)\n {\n if(!localfail)\n {\n console.log(\"getLocalStorage failed\", e);\n localfail = true;\n }\n }\n }\n return key in backuplocal ? backuplocal[key] : null;\n}\n", "/** This is currently more or less based on the mootools Cookie library */\n/* eslint no-useless-escape: off */\n\nimport { isIsolated } from './storage.es';\nlet isolatedcookies = {};\n\nfunction escapeRegExp(xx)\n{\n return xx.replace(/([-.*+?^${}()|[\\]\\/\\\\])/g, '\\\\$1');\n}\n\n//based on mootools cookie\nclass Cookie\n{\n constructor(key,options)\n {\n if(!options)\n options={};\n\n this.key = key;\n this.options = { path: 'path' in options ? options.path : '/'\n , domain: 'domain' in options ? options.domain : false\n , duration: 'duration' in options ? options.duration : false\n , secure: 'secure' in options ? options.secure : false\n , encode: 'encode' in options ? options.encode : true\n , httponly: 'httpOnly' in options ? options.httpOnly : 'httponly' in options ? options.httponly : false\n , samesite: 'samesite' in options ? options.samesite : ''\n };\n }\n write(value)\n {\n if(isIsolated())\n {\n isolatedcookies[\"c.\" + this.key] = String(value);\n return;\n }\n\n if (this.options.encode)\n value = encodeURIComponent(value);\n if (this.options.domain)\n value += '; domain=' + this.options.domain;\n if (this.options.path)\n value += '; path=' + this.options.path;\n if (this.options.duration)\n {\n var date = new Date();\n date.setTime(date.getTime() + this.options.duration * 24 * 60 * 60 * 1000);\n value += '; expires=' + date.toGMTString();\n }\n if (this.options.secure)\n value += '; secure';\n if (this.options.httponly)\n value += '; HttpOnly';\n if (this.options.samesite)\n value += '; SameSite='+this.options.samesite;\n\n document.cookie = this.key + '=' + value;\n return this;\n }\n read()\n {\n if(isIsolated())\n return isolatedcookies[\"c.\" + this.key] || null;\n\n var value = document.cookie.match('(?:^|;)\\\\s*' + escapeRegExp(this.key) + '=([^;]*)');\n return (value) ? decodeURIComponent(value[1]) : null;\n }\n remove()\n {\n if(isIsolated())\n {\n delete isolatedcookies[\"c.\" + this.key];\n return;\n }\n new Cookie(this.key, Object.assign({}, this.options, {duration: -1})).write('');\n }\n}\n\nexport function list()\n{\n if(isIsolated())\n return Object.entries(isolatedcookies).map(entry => ({ name: entry[0].substr(2), value: entry[1] }));\n\n return document.cookie.split(';').map(cookie =>\n {\n let parts = cookie.split('=');\n return { name: decodeURIComponent(parts[0].trim()), value:decodeURIComponent(parts[1]||'') };\n });\n}\n\nexport function write(key, value, options)\n{\n return new Cookie(key, options).write(value);\n}\n\nexport function read(key)\n{\n return new Cookie(key).read();\n}\n\nexport function remove(key, options)\n{\n new Cookie(key, options).remove();\n}\n", "export let debugflags = {};\nimport * as domcookie from '../extra/cookie.es';\n\n/** Extract a specific variable from the URL\n @param varname Variable name, eg dompack-debug\n*/\nexport function parseDebugURL(varname)\n{\n //FIXME proper regex escape for varname, but fortunately this isn't user input\n let urldebugvar = window.location.href.match(new RegExp('[?&#]' + varname + '=([^&#?]*)'));\n if(urldebugvar)\n {\n let debugstr = decodeURIComponent(urldebugvar[1]).split(',');\n if(debugstr.length)\n addDebugFlags(debugstr);\n }\n}\nexport function addDebugFlags(flags)\n{\n flags.forEach(flagname =>\n {\n if(flagname.startsWith('sig='))\n return;\n\n debugflags[flagname] = true;\n document.documentElement.classList.add(\"dompack--debug-\" + flagname);\n });\n\n if(debugflags.dompack)\n console.log('[dompack] debugging flags: ' + Object.keys(debugflags).join(', '));\n}\n\nexport function initDebug()\n{\n //no-op but there are still external callers which need fixing\n}\n\n//initialize debugging support (read debugflags etc)\nparseDebugURL('wh-debug');\n\nlet debugcookie = domcookie.read(\"wh-debug\");\nif(debugcookie)\n addDebugFlags(debugcookie.split('.'));\n", "let eventconstructor = null;\n\nif(typeof window !== 'undefined')\n{\n try //IE11 does not ship with CustomEvent\n {\n new window.CustomEvent(\"test\");\n eventconstructor = window.CustomEvent;\n }\n catch(e)\n {\n eventconstructor = function(event, params)\n {\n var evt;\n params = params || {\n bubbles: false,\n cancelable: false,\n detail: undefined\n };\n\n evt = document.createEvent(\"CustomEvent\");\n evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);\n return evt;\n };\n eventconstructor.prototype = window.Event.prototype;\n }\n}\n\nexport let CustomEvent = eventconstructor;\n\nexport function dispatchDomEvent(element, eventtype, options)\n{\n //see here https://developer.mozilla.org/en-US/docs/Web/Events whether an event is bubbles/cancelabel\n options = { bubbles: [\"input\",\"change\",\"click\",\"contextmenu\",\"dblclick\",\n \"reset\",\"submit\"].includes(eventtype)\n , cancelable: [\"animationstart\",\"animationcancel\",\"animationend\",\"animationiteration\"\n ,\"beforeunload\"\n ,\"click\",\"contextmenu\",\"dblclick\"\n ,\"reset\",\"submit\"\n ,\"transitionstart\",\"transitioncancel\",\"transitionend\",\"transitionrun\"].includes(eventtype)\n , ...options\n };\n\n if(!element.ownerDocument)\n return true; //the element has left the dom... so there's no more bubbling. just drop it\n\n let createtype = [\"load\",\"scroll\"].includes(eventtype) == \"load\" ? \"UIEvents\" : [\"focus\",\"blur\",\"focusin\",\"focusout\"].includes(eventtype) ? \"FocusEvent\" : eventtype == \"click\" ? \"MouseEvents\" : \"HTMLEvents\";\n\n var evt = element.ownerDocument.createEvent(createtype);\n evt.initEvent(eventtype, options.bubbles, options.cancelable);\n if(options.detail)\n evt.detail = options.detail;\n if(options.relatedTarget) //its a readonly prop, so redefine it\n Object.defineProperty(evt, 'relatedTarget', { value:options.relatedTarget, writable: false });\n\n if(eventtype == 'click' && window.IScroll)\n evt._constructed = true; //ensure IScroll doesn't blindly cancel our synthetic clicks\n\n return element.dispatchEvent(evt);\n}\n\n//fire the proper modified events (input and/or change) on the element after changing its value - DEPRECATED, you should fire the proper input and change events according to the situation\nexport function fireModifiedEvents(element, options)\n{\n fireHTMLEvent(element, 'input', options);\n fireHTMLEvent(element, 'change', options);\n}\n\n//manually fire 'onchange' events. needed for event simulation - DEPRECATED\nexport function fireHTMLEvent(element, type, options)\n{\n return dispatchDomEvent(element, type, options);\n}\n\n/** Fire a custom event\n @param node node to fire the event on\n @param event event type\n @param params\n @cell params.bubbles\n @cell params.cancelable\n @cell params.detail\n @cell params.defaulthandler Handler to execute if the default isn't prevented by a event listener\n @return true if the default wasn't prevented\n*/\nexport function dispatchCustomEvent(node, event, params)\n{\n if(!params)\n params={};\n ['bubbles','cancelable'].forEach(prop =>\n {\n if(!(prop in params))\n throw new Error(`Missing '${prop}' in dispatchCustomEvent parameter`);\n });\n\n let evt = new CustomEvent(event, { bubbles: params.bubbles\n , cancelable: params.cancelable\n , detail: params.detail\n });\n let defaultaction = true;\n try\n {\n if(!node.dispatchEvent(evt))\n defaultaction = false; //defaultPrevented is unreliable on IE11, so double check\n }\n finally\n {\n if(!evt.defaultPrevented && params.defaulthandler && defaultaction)\n {\n params.defaulthandler(evt);\n }\n }\n return defaultaction && !evt.defaultPrevented;\n}\n\n/** Change the value of a form element, and fire the correct events as if it were a user change\n @param element Element to change\n @param newvalue New value */\nexport function changeValue(element, newvalue)\n{\n if(element instanceof Array || element instanceof NodeList)\n {\n Array.from(element).forEach(node => changeValue(node, newvalue));\n return;\n }\n\n if(element.nodeName=='INPUT' && ['radio','checkbox'].includes(element.type))\n {\n if(!!element.checked == !!newvalue)\n return;\n element.checked=!!newvalue;\n }\n else\n {\n if(element.value == newvalue)\n return;\n\n element.value = newvalue;\n }\n dispatchDomEvent(element, 'input');\n dispatchDomEvent(element, 'change');\n}\n\nlet keydata;\nfunction initKeyMapping()\n{\n keydata =\n {\n // Mapping from keyIdentifier/key to key. If not found, translate U+XXXX to the unicode char and return that.\n\n // List of all current key mappings (and current inconsistencies\n // at https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values\n mapping:\n { \"Del\": \"Delete\" // IE/edge use 'Del' instead of 'Delete'\n , \"Esc\": \"Escape\" // IE/edge use 'Esc' instead of 'Escape'\n , \"OS\": \"Meta\" // Meta key is called OS on IE 9 and Firefox (tested v50)\n , \"Win\": \"Meta\" // Meta key is called Win on IE/Edge\n , \"Scroll\": \"ScrollLock\" // More IE/Edge, from tests\n , \"PrintScreen\": \"Print\" // More IE/Edge, from tests\n , \"MozHomeScreen\": \"GoHome\" // Prior to Firefox 37, the Home button generated a key code of \"Exit\". Starting in Firefox 37, the button generates the key code \"MozHomeScreen\".\n\n // Internet Explorer 9 and Firefox 36 and earlier return \"Left\", \"Right\", \"Up\", and \"Down\" for the arrow keys, instead of \"ArrowLeft\", \"ArrowRight\", \"ArrowUp\", and \"ArrowDown\".\n , \"Left\": \"ArrowLeft\"\n , \"Right\": \"ArrowRight\"\n , \"Up\": \"ArrowUp\"\n , \"Down\": \"ArrowDown\"\n // More IE and old Firefox stuff\n , \"Crsel\": \"CrSel\"\n , \"Exsel\": \"ExSel\"\n , \"Nonconvert\": \"NonConvert\"\n // Internet Explorer 9 and Firefox 36 and earlier report \"Apps\" instead of \"ContextMenu\" for the context menu key.\n , \"Apps\": \"ContextMenu\"\n // Internet Explorer 9 and Firefox 36 and earlier use \"MediaNextTrack\" and \"MediaPreviousTrack\" instead of \"MediaTrackNext\" and \"MediaTrackPrevious\".\n , \"MediaNextTrack\": \"MediaTrackNext\"\n , \"MediaPreviousTrack\": \"MediaTrackPrevious\"\n // In Internet Explorer 9, and prior to Firefox 49, \"AudioVolumeUp\", \"AudioVolumeDown\", and \"AudioVolumeMute\" were \"VolumeUp\", \"VolumeDown\", and \"VolumeMute\".\n // In Firefox 49 they were updated to match the latest specification. The old names are still used on Boot to Gecko.\n , \"VolumeUp\": \"AudioVolumeUp\"\n , \"VolumeDown\": \"AudioVolumeDown\"\n , \"VolumeMute\": \"AudioVolumeMute\"\n // Firefox added proper support for the \"TV\" key in Firefox 37; before that, this key generated the key code \"Live\".\n , \"Live\": \"TV\"\n // Internet Explorer 9 and Firefox 36 and earlier identify the zoom toggle button as \"Zoom\". Firefox 37 corrects this to \"ZoomToggle\".\n , \"Zoom\": \"ZoomToggle\"\n // Internet Explorer 9 and Firefox 36 and earlier use \"SelectMedia\" instead of \"LaunchMediaPlayer\". Firefox 37 through Firefox 48 use \"MediaSelect\". Firefox 49 has been updated to match the latest specification, and to return \"LaunchMediaPlayer\".\n , \"SelectMedia\": \"LaunchMediaPlayer\"\n , \"MediaSelect\": \"LaunchMediaPlayer\"\n // Google Chrome returns \"LaunchCalculator\" instead of \"LaunchApplication1\". See Chromium bug 612743 for more information.\n // Google Chrome returns \"LaunchMyComputer\" instead of \"LaunchApplication2\". See Chromium bug 612743 for more information.\n // (LaunchCalculator and LaunchMyComputer are valid too, so no translation)\n\n // While older browsers used words like \"Add\", \"Decimal\", \"Multiply\", and so forth modern browsers identify these using the actual character (\"+\", \".\", \"*\", and so forth).\n , \"Multiply\": \"*\"\n , \"Add\": \"+\"\n , \"Divide\": \"/\"\n , \"Subtract\": \"-\"\n , \"Decimal\": \".\" // (mozilla Key_Values doc says depends on region)\n , \"Separator\": \".\" // (mozilla Key_Values doc says depends on region)\n\n // keyIdenfier spec mapping of non-printable keys: https://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#keyset-keyidentifiers\n // Used in safari\n , \"U+0008\": \"Backspace\"\n , \"U+0009\": \"Tab\"\n , \"U+000D\": \"Enter\"\n , \"U+0018\": \"Cancel\"\n , \"U+001B\": \"Escape\"\n , \"U+007F\": \"Delete\"\n , \"U+0300\": \"DeadGrave\"\n , \"U+0301\": \"DeadEacute\"\n , \"U+0302\": \"DeadCircumflex\"\n , \"U+0303\": \"DeadTilde\"\n , \"U+0304\": \"DeadMacron\"\n , \"U+0306\": \"DeadBreve\"\n , \"U+0307\": \"DeadAboveDot\"\n , \"U+0308\": \"DeadUmlaut\"\n , \"U+030A\": \"DeadAboveRing\"\n , \"U+030B\": \"DeadDoubleacute\"\n , \"U+030C\": \"DeadCaron\"\n , \"U+0327\": \"DeadCedilla\"\n , \"U+0328\": \"DeadOgonek\"\n , \"U+0345\": \"DeadIota\"\n , \"U+3099\": \"DeadVoicedSound\"\n , \"U+309A\": \"DeadSemivoicedSound\"\n\n // keyIdenfier spec mapping to characters: https://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#keyset-keyidentifiers\n , \"Exclamation\": \"!\"\n , \"DoubleQuote\": \"\\\"\"\n , \"Hash\": \"#\"\n , \"Dollar\": \"$\"\n , \"Ampersand\": \"&\"\n , \"LeftParen\": \"(\"\n , \"RightParen\": \")\"\n , \"Asterisk\": \"*\"\n , \"Plus\": \"+\"\n , \"Percent\": \"%\"\n , \"Comma\": \",\"\n , \"HyphenMinus\": \"-\"\n , \"Period\": \".\"\n , \"Solidus\": \"/\"\n , \"Colon\": \":\"\n , \"Semicolon\": \";\"\n , \"LessThan\": \"<\"\n , \"Equals\": \"=\"\n , \"GreaterThan\": \">\"\n , \"QuestionMark\": \"?\"\n , \"At\": \"@\"\n , \"LeftSquareBracket\": \"[\"\n , \"Backslash\": \"\\\\\"\n , \"RightSquareBracket\": \"]\"\n , \"Circumflex\": \"^\"\n , \"Underscore\": \"_\"\n , \"Grave\": \"`\"\n , \"LeftCurlyBracket\": \"{\"\n , \"Pipe\": \"|\"\n , \"RightCurlyBracket\": \"}\"\n , \"Euro\": \"\u20AC\"\n , \"InvertedExclamation\": \"\u00A1\"\n\n // Safari fixes: viewed in local tests\n , \"U+0010\": \"ContextMenu\"\n }\n };\n}\n\n/** Returns normalized keyboard event properties, following the current W3C UI Events spec\n @param evt Keyboard event\n @return Normalized keyboard event data\n*/\nexport function normalizeKeyboardEventData(evt)\n{\n // event.key is supported from chrome:51, edge, ff: 29, ie: 9, not in safari\n // event.keyIdentifier in chrome 26-54, opera 15-41, safara: 5.1\n // safari doesn't provide either in keypress events, use U+evt.keyCode (uppercase 4-byte hex)\n\n let key = evt.key;\n if (key && key.charCodeAt(0) < 32) // IE11 under selenium gives back control chars at keypress\n key = \"\";\n\n key = key || evt.keyIdentifier || (evt.keyCode ? \"U+\" + (\"000\" + evt.keyCode.toString(16)).substr(-4).toUpperCase() : \"\");\n if (!key)\n key = \"Unidentified\";\n if (!keydata)\n initKeyMapping();\n if (keydata.mapping.hasOwnProperty(key))\n key = keydata.mapping[key];\n else if (key.startsWith(\"U+\")) // U+xxxx code\n key = String.fromCodePoint(parseInt(key.substr(2), 16));\n\n // IE11/edge numpad '.' with numlock returns 'Del' in .key in keypress event.\n if (evt.type === \"keypress\" && evt.char === \".\")\n key = \".\";\n else if (evt.key == \"\\u0000\" && evt.code == \"NumpadDecimal\") // seen in chrome 56.0.2924.76 on linux, numpad '.' without numlock returns key \"\\u0000\"\n key = \".\";\n\n return (\n { type: evt.type\n , target: evt.target\n , key: key\n , code: evt.code || \"Unidentified\"\n , ctrlKey: evt.ctrlKey\n , altKey: evt.altKey\n , location: evt.location\n , shiftKey: evt.shiftKey\n , metaKey: evt.metaKey\n , repeat: evt.repeat\n , isComposing: evt.isComposing\n });\n}\n\n/** Stop, fully, an event */\nexport function stop(event)\n{\n event.preventDefault();\n event.stopImmediatePropagation();\n}\n", "import * as domdebug from './debug.es';\nimport * as dompromise from './promise.es';\nimport * as domevents from './events.es';\n\nlet lockmgr = null;\nlet locallocks = [];\nlet ischild = false;\n\nclass LockManager\n{\n //this object is not for external consumption\n constructor()\n {\n this.locks = [];\n this.busycounter = 0;\n this.deferreduipromise = null;\n this.uiwatcher = null;\n this.modallocked = false;\n }\n anyModalLocks()\n {\n for(var lock of this.locks)\n if(lock.ismodal)\n return true;\n return false;\n }\n add(lock)\n {\n this.locks.push(lock);\n let returnvalue = this.busycounter++;\n\n if(lock.ismodal && !this.modallocked)\n {\n this.modallocked = true;\n\n if(domevents.dispatchCustomEvent(window, 'dompack:busymodal', { bubbles: true, cancelable: true, detail: { islock: true } }))\n document.documentElement.classList.add(\"dompack--busymodal\");\n }\n return returnvalue;\n }\n release(lock)\n {\n let pos = this.locks.indexOf(lock);\n if(pos==-1)\n {\n if(domdebug.debugflags.bus)\n {\n console.error(\"Duplicate release of busy lock #\" + lock.locknum);\n console.log(\"Lock allocated:\");\n console.log(lock.acquirestack);\n console.log(\"Lock first released:\");\n console.log(lock.releasestack);\n }\n throw new Error(\"Duplicate release of busy lock\");\n }\n\n this.locks.splice(pos,1);\n this.prepWatcher();\n }\n prepWatcher()\n {\n if(!this.uiwatcher && this.locks.length==0 && (this.deferreduipromise || this.modallocked))\n {\n this.uiwatcher = setTimeout(() => this.checkUIFree(),0);\n }\n }\n getNumLocks()\n {\n return this.locks.length;\n }\n checkUIFree()\n {\n this.uiwatcher = null;\n if(this.locks.length == 0)\n {\n if(this.deferreduipromise)\n {\n this.deferreduipromise.resolve(true);\n this.deferreduipromise = null;\n }\n if(this.modallocked && !this.anyModalLocks())\n {\n this.modallocked = false;\n if(domevents.dispatchCustomEvent(window, 'dompack:busymodal', { bubbles: true, cancelable: true, detail: { islock: false } }))\n document.documentElement.classList.remove(\"dompack--busymodal\");\n }\n }\n }\n waitUIFree()\n {\n if(!this.deferreduipromise)\n this.deferreduipromise = dompromise.createDeferred();\n this.prepWatcher();\n return this.deferreduipromise.promise;\n }\n logLocks()\n {\n this.locks.forEach( (lock,idx) => console.log('[bus] lock #' + lock.locknum, lock.acquirestack, lock) );\n console.log(\"[bus] total \" + this.locks.length + \" locks\");\n }\n getLockIds()\n {\n return this.locks.map(l => \"#\" + l.locknum).join(\", \");\n }\n}\n\nexport class Lock\n{\n constructor(options)\n {\n this.ismodal = options && options.ismodal;\n\n this.locknum = lockmgr.add(this);\n if(ischild)\n locallocks.push(this);\n\n if(domdebug.debugflags.bus)\n {\n this.acquirestack = (new Error).stack;\n console.trace('[bus] Busy lock #' + this.locknum + ' taken. ' + lockmgr.getNumLocks() + \" locks active now: \" + lockmgr.getLockIds());\n }\n }\n release()\n {\n if(domdebug.debugflags.bus)\n this.releasestack = (new Error).stack;\n\n lockmgr.release(this);\n if(ischild)\n {\n let lockpos = locallocks.indexOf(this);\n locallocks.splice(lockpos,1);\n }\n\n if(domdebug.debugflags.bus)\n {\n console.trace('[bus] Busy lock #' + this.locknum + ' released. ' + lockmgr.getNumLocks() + \" locks active now: \" + lockmgr.getLockIds());\n }\n }\n}\n\n/** Return a promise resolving as soon as the UI is free for at least one tick */\nexport function waitUIFree()\n{\n return lockmgr.waitUIFree();\n}\n\n/** flag userinterface as busy. tests then know not to interact with the UI until the busy flag is released\n @param options Options\n @cell options.ismodal Whether the lock is a modal lock\n*/\nexport function flagUIBusy(options)\n{\n return new Lock(options);\n}\n\nexport function getUIBusyCounter()\n{\n return lockmgr.busycounter;\n}\n\ntry //we're accessing a parent window, so we may hit security exceptions\n{\n if(window.parent && window.parent.$dompack$busylockmanager)\n {\n lockmgr = window.parent.$dompack$busylockmanager;\n ischild = true;\n //if we connected to a parent... deregister our locks, eg. if parent navigated our frame away\n window.addEventListener(\"unload\", () =>\n {\n if(domdebug.debugflags.bus)\n console.log(\"[bus] Frame unloading, \" + locallocks.length + \" locks pending.\", locallocks.map(l=>\"#\"+l.locknum).join(\", \"), locallocks);\n\n //switch to local instance as we'll be unable to auto-release\n let locallockmgr = new LockManager;\n locallocks.forEach(lock => { lockmgr.release(lock); locallockmgr.add(lock); });\n locallocks = [];\n lockmgr = locallockmgr;\n });\n }\n}\ncatch(e)\n{\n}\n\nif(!lockmgr)\n lockmgr = new LockManager;\n\nif(typeof window != 'undefined')\n window.$dompack$busylockmanager = lockmgr;\n", "\n/* Regex to identify dimensionless style sttributes. copied from old version of preact/src/constants.js (MIT)\n meant to capture:\n { boxFlex:1, boxFlexGroup:1, columnCount:1, fillOpacity:1, flex:1, flexGrow:1,\n flexPositive:1, flexShrink:1, flexNegative:1, fontWeight:1, lineClamp:1, lineHeight:1,\n opacity:1, order:1, orphans:1, strokeOpacity:1, widows:1, zIndex:1, zoom:1\n*/\nconst IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i;\n\nfunction generateInsertList(nodes)\n{\n if(nodes.length==1)\n return typeof nodes[0]==='string' ? document.createTextNode(nodes[0]) : nodes[0];\n\n let frag = document.createDocumentFragment();\n nodes.forEach(node => frag.appendChild(typeof node === 'string' ? document.createTextNode(node) : node));\n return frag;\n}\n\nexport function matches(node, selector)\n{\n var tocall = node.matches || node.matchesSelector || node.msMatchesSelector || node.webkitMatchesSelector;\n //if none of the 'matches' was found, this might be a textnode or something. returning false is probably safest\n return tocall && tocall.apply(node, [selector]);\n}\nexport function closest(node, selector)\n{\n if(node.closest)\n return node.closest(selector);\n for(;node&&!matches(node,selector);node=node.parentElement)\n /*iterate*/;\n return node;\n}\n//implements contains. TODO we only really need this on IE11, which doesn't consider a text node a child, we can probably fall back to native elsewhere ?\nexport function contains(ancestor, child)\n{\n for(;child;child=child.parentNode)\n if(child===ancestor)\n return true;\n return false;\n}\n//insert a range of nodes before a node: https://dom.spec.whatwg.org/#dom-childnode-before\nexport function before(node,...nodes)\n{\n if(node.before)\n {\n node.before(...nodes);\n return;\n }\n if(node.parentNode)\n node.parentNode.insertBefore(generateInsertList(nodes), node);\n}\n//insert a range of nodes after a node: https://dom.spec.whatwg.org/#dom-childnode-after\nexport function after(node,...nodes)\n{\n if(node.after)\n {\n node.after(...nodes);\n return;\n }\n if(node.parentNode)\n node.parentNode.insertBefore(generateInsertList(nodes), node.nextSibling);\n}\n//replace node with a set of nodes : https://dom.spec.whatwg.org/#dom-childnode-replacewith\nexport function replaceWith(node,...nodes)\n{\n if(node.replaceWith)\n {\n node.replaceWith(...nodes);\n return;\n }\n if(node.parentNode)\n node.parentNode.replaceChild(generateInsertList(nodes), node);\n}\n//remove node with a set of nodes : https://dom.spec.whatwg.org/#dom-childnode-remove\nexport function remove(node)\n{\n if(node.parentNode)\n node.parentNode.removeChild(node);\n}\n//insert nodes at start: https://dom.spec.whatwg.org/#dom-parentnode-prepend\nexport function prepend(node, ...nodes)\n{\n node.insertBefore(generateInsertList(nodes), node.firstChild);\n}\n//insert nodes at end: https://dom.spec.whatwg.org/#dom-parentnode-append\nexport function append(node, ...nodes)\n{\n node.appendChild(generateInsertList(nodes));\n}\n\n//offer toggleClass ourselves as IE11's native version is broken - does not understand the last parameter\n/** Toggle a single class */\nexport function toggleClass(node, classname, settoggle)\n{\n if (arguments.length === 2)\n node.classList.toggle(classname);\n else if (settoggle)\n node.classList.add(classname);\n else\n node.classList.remove(classname);\n}\n\n/** Toggle classes in a node\n @param node Node which classes to toggle\n @param toggles Object, all keys will be added/removed based on the truthyness of their values\n*/\nexport function toggleClasses(node, toggles)\n{\n if (typeof(toggles) !== \"object\")\n throw new Error(\"Expected an object with keys as classnames\");\n Object.keys(toggles).forEach(key => node.classList[toggles[key] ? \"add\" : \"remove\"](key));\n}\n\n/* remove the contents of an existing node */\nexport function empty(node)\n{\n //node.innerHTML=''; // this does NOT work for IE11, it destroys all nodes instead of unlinking them\n while(node.lastChild)\n node.removeChild(node.lastChild);\n}\n\n/** get the relative bound difference between two elements, and return a writable copy */\nexport function getRelativeBounds(node, relativeto)\n{\n if(!relativeto)\n relativeto = node.ownerDocument.documentElement;\n\n var nodecoords = node.getBoundingClientRect();\n var relcoords = relativeto.getBoundingClientRect();\n return { top: nodecoords.top - relcoords.top\n , left: nodecoords.left - relcoords.left\n , right: nodecoords.right - relcoords.left\n , bottom: nodecoords.bottom - relcoords.top\n , width: nodecoords.width\n , height: nodecoords.height\n };\n}\n\nexport function isDomReady()\n{\n return document.readyState == \"interactive\" || document.readyState == \"complete\";\n}\n\n/* run the specified function 'on ready'. adds to DOMContentLoaded if dom is not ready yet. Exceptions from the ready handler will not be fatal to the rest of code execution */\nexport function onDomReady(callback)\n{\n if(isDomReady())\n {\n try\n {\n callback();\n }\n catch(e)\n {\n console.error(\"Exception executing a domready handler\");\n console.log(e,e.stack);\n\n if (window.onerror)\n {\n // Send to onerror to trigger exception reporting\n try\n {\n window.onerror(e.message, e.fileName || \"\", e.lineNumber || 1, e.columNumber || 1, e);\n }\n catch (e)\n {\n }\n }\n }\n }\n else\n document.addEventListener(\"DOMContentLoaded\", callback);\n}\n\n//parse JSON data, throw with more info on parse failure\nexport function getJSONAttribute(node, attributename)\n{\n try\n {\n return JSON.parse(node.getAttribute(attributename));\n }\n catch(e)\n {\n console.error(\"JSON parse failure on attribute '\" +attributename+ \"' of node\", node);\n throw e;\n }\n}\n\n/** Get the base URI of the current document. IE11 doesn't implement document.baseURI\n @param doc Document to query. Defaults to window.document\n*/\nexport function getBaseURI(doc)\n{\n if(!doc)\n doc=window.document;\n if(doc.baseURI)\n return doc.baseURI;\n\n let base = doc.querySelector('base');\n if(base && base.href)\n return base.href;\n return doc.URL;\n}\n\n//queryselector quick wrapper\nexport function qS(node_or_selector, selector)\n{\n if(typeof node_or_selector == 'string')\n return document.querySelector(node_or_selector);\n else\n return node_or_selector.querySelector(selector);\n}\n\n//queryselectorall quick wrapper\nexport function qSA(node_or_selector, selector)\n{\n if(typeof node_or_selector == 'string')\n return Array.from(document.querySelectorAll(node_or_selector));\n else\n return Array.from(node_or_selector.querySelectorAll(selector));\n}\n\n\n/** Sets multiple styles on a node, automatically adding 'px' to numbers when appropriate\n (can be used as replacement for Mootools .setStyles)\n*/\nexport function setStyles(node, value)\n{\n if (!value || typeof value === 'string')\n node.style.cssText = value || '';\n else if (typeof value === 'object')\n {\n for (let i in value)\n {\n // for numbers, add 'px' if the constant isn't dimensionless (eg zIndex)\n node.style[i] = typeof value[i] === 'number' && IS_NON_DIMENSIONAL.test(i) === false\n ? value[i] + 'px'\n : value[i];\n }\n }\n}\n", "import { append, setStyles } from './tree.es';\n\nfunction flattenArray(list)\n{\n return list.reduce((acc, elt) => acc.concat(Array.isArray(elt) ? flattenArray(elt) : elt), []);\n}\n\nfunction setClassName(node, value)\n{\n if (!value || typeof value === 'string')\n node.className = value || '';\n else if (Array.isArray(value))\n node.className = value.filter(elt => elt && typeof elt === 'string').join(\" \");\n else\n {\n let str = \"\";\n Object.keys(value).forEach((key, idx) => { if (value[key]) str += (idx ? \" \" : \"\") + key; });\n node.className = str;\n }\n}\n\n/** Matches non-first uppercase characters\n (when the second char is uppercases, the first char is passed too)\n*/\nconst MATCH_UPCASE = /([A-Z])/g;\nconst MATCH_DASH_AND_CHAR = /-([a-zA-Z])/g;\n\n/** Convert a camelCased identifier to a dashed string\n*/\nexport function toDashed(value)\n{\n if (value)\n return (value.substring(0, 1) + value.substring(1).replace(MATCH_UPCASE, \"-$1\")).toLowerCase();\n}\n\n/** Convert a dashed string to a camelCase identifier\n*/\nexport function toCamel(value)\n{\n return value.replace(MATCH_DASH_AND_CHAR, (item, match_1) => match_1.toUpperCase());\n}\n\nfunction attrHasBooleanValue(propname)\n{\n return ['disabled','checked','selected','readonly','multiple','ismap'].includes(propname);\n}\n\nfunction createElement(elementname, attributes, toattrs)\n{\n var node = document.createElement(elementname);\n if(attributes)\n {\n Object.keys(attributes).forEach(attrname =>\n {\n if(attrname == 'events')\n throw new Error(\"Use 'on' instead of 'events' in dompack.create\");\n if(attrname == 'styles')\n throw new Error(\"Use 'style' instead of 'styles' in dompack.create\");\n if(attrname == 'children')\n {\n // allow null 'children' property for jsxcreate, property delete is detrimental to performance.\n if (attributes[attrname])\n throw new Error(\"Use 'childNodes' instead of 'children' in dompack.create\");\n return;\n }\n\n let value = attributes[attrname];\n\n if (attrname == 'on') //create event listeners\n return void Object.keys(value).forEach(eventname => node.addEventListener(eventname, value[eventname], false));\n else if (attrname.startsWith(\"on\"))\n return void node.addEventListener(toDashed(attrname.substring(2)), value, false);\n\n if (attrname == \"className\" || attrname == \"class\")\n {\n if(node.className) // already modified the class?\n throw new Error(\"Specify either 'className' or 'class' to dompack.create, but not both\");\n setClassName(node, value);\n return;\n }\n\n if(attrname == 'style')\n return void setStyles(node, value);\n\n if(attrname == 'dataset') //explicitly assign\n return void Object.assign(node[attrname], value);\n\n if(attrname == 'childNodes') //append as children\n return void append(node, ...attributes.childNodes.filter(child => child != null && child !== true && child !== false));\n\n if(toattrs && attrHasBooleanValue(attrname))\n {\n if(value)\n node.setAttribute(attrname,\"\");\n else\n node.removeAttribute(attrname);\n return;\n }\n\n if (toattrs && !attrname.startsWith(\"prop\"))\n {\n if (value != null) // matches not null and not undefined\n {\n if (value && typeof value == \"object\")\n throw new Error(\"Cannot store non-null objects in attributes, use a property starting with 'prop'\");\n node.setAttribute(attrname, attributes[attrname]);\n }\n }\n else\n node[attrname] = attributes[attrname];\n });\n }\n return node;\n}\n\n/* create elements. sets properties (not attributes!) immediately.\n everything inside 'on' is added as an addEventListener without capture\n everything inside 'childNodes' is appended to the node. nulls are ignored inside childNodes\n\n Examples:\n domtools.create(\"input\", { type:\"file\", className: \"myupload\", style: { display: \"none\" }));\n\n*/\nexport function create(elementname, attributes)\n{\n return createElement(elementname, attributes, false);\n}\n\n/** Function to create for jsx, create elements directly (instead of virtual dom nodes).\n\n import * as dompack from 'dompack';\n\n /* @jsx dompack.jsxcreate *\\/\n your code\n*/\nexport function jsxcreate(element, attributes, ...childNodes)\n{\n // Ensure attributes\n attributes = attributes || {};\n // Flatten childnodes arrays, convert numbers to strings. Also support children property (React uses that)\n let parts = (attributes.childNodes || []).concat(attributes.children || []).concat(childNodes);\n if (attributes.children)\n attributes.children = null;\n parts = flattenArray(parts);\n parts = parts.map((elt) => typeof elt === \"number\" ? String(elt) : elt);\n // Create the element\n attributes.childNodes = parts;\n if (typeof element === \"function\")\n return element(attributes, null, null);\n return createElement(element, attributes, true);\n}\n\n", "import * as domtree from './tree.es';\nimport * as domevents from './events.es';\n\nlet components = [];\nconst map = new WeakMap();\n\n\n//is a node completely in the dom? if we can find a sibling anywhere, it must be closed\nfunction isNodeCompletelyInDom(node)\n{\n for(;node;node=node.parentNode)\n if(node.nextSibling)\n return true;\n return false;\n}\nfunction processRegistration(item, reg, domready)\n{\n if(!domready && !isNodeCompletelyInDom(item))\n return; //not safe to register\n\n if (!map.has(item))\n map.set(item, [ reg.num ]);\n else\n {\n let list = map.get(item);\n if (list.includes(reg.num))\n return;\n list.push(reg.num);\n }\n reg.handler(item, reg.index++); //note: if an exception is reported from Object.handler,\n}\nfunction applyRegistration(reg, startnode)\n{\n let domready = domtree.isDomReady();\n if(reg.afterdomready && !domready)\n return;\n\n let items = Array.from( (startnode || document).querySelectorAll(reg.selector));\n if(startnode && domtree.matches(startnode,reg.selector))\n items.unshift(startnode);\n\n items.forEach(item =>\n {\n try\n {\n processRegistration(item, reg, domready);\n }\n catch(e)\n {\n console.error(\"Exception handling registration of\",item,\"for rule\",reg.selector);\n console.log(\"Registration\",reg);\n console.log(e,e.stack);\n if (window.onerror)\n {\n // Send to onerror to trigger exception reporting\n try\n {\n window.onerror(e.message, e.fileName || \"\", e.lineNumber || 1, e.columNumber || 1, e);\n }\n catch (e)\n {\n }\n }\n }\n });\n}\n\n/** getBoundingClientRect, but as a plain copyable object.. Debugging and other code often needs this\n @param node Node to query\n @param srcret Offset rectangle\n @return top,bottom,left,right,width,height like getBCR, but spreadable/assignable/copyable etc*/\nexport function getRect(node, srcrect)\n{\n const bcr = node.getBoundingClientRect();\n let rect = { top: bcr.top\n , bottom: bcr.bottom\n , left: bcr.left\n , right: bcr.right\n , width: bcr.width\n , height: bcr.height\n };\n\n if(srcrect)\n {\n rect.top = rect.top - srcrect.top;\n rect.bottom = rect.bottom - srcrect.top;\n rect.left = rect.left - srcrect.left;\n rect.right = rect.right - srcrect.left;\n }\n return rect;\n}\n\n/* A focus implementation that allows the node to intercept focused, allowing eg\n radio/checkbox replacements to redirect focus but also explicitly preventing\n focus of a disabled element\n Returns true when the focus operation was successfull or handled by an event handler.\n @param node Node to focus\n @param options.preventScroll Prevent scroll to focused element\n*/\nexport function focus(node, options)\n{\n if(!domevents.dispatchCustomEvent(node, 'dompack:takefocus', { bubbles: true, cancelable: true, detail: {options} }))\n return true;\n\n if(node.disabled)\n return false;\n\n // IE likes to throw errors when setting focus\n try\n {\n node.focus(options);\n }\n catch(e)\n {\n return false;\n }\n return true;\n}\n\n/** Deprecated, invoke scrollIntoView directly on the nodes */\nexport function scrollIntoView(node, options)\n{\n node.scrollIntoView(options);\n return true;\n}\n\n/** @short Register a component for auto-initialization.\n @param selector Selector the component must match\n @param handler Handler\n @param options Any unrecognized options are passed to the handler\n\n The handler will be invoked with two parameters\n - the node to register\n - the index of the node (a unique counter for this selector - first is 0) */\n\nexport function register(selector, handler, options)\n{\n let newreg = { selector: selector\n , handler: handler\n , index: 0\n , num: components.length\n , afterdomready: !options || options.afterdomready\n };\n if(components.length==0 && !domtree.isDomReady()) //first component... we'll need a ready handler\n domtree.onDomReady(() => registerMissed());\n\n components.push(newreg);\n applyRegistration(newreg, null);\n}\n\n// register any components we missed on previous scans\nexport function registerMissed(startnode)\n{\n let todo = components.slice(0);\n todo.forEach(item => applyRegistration(item, startnode));\n}\n", "/* @importstatement:\n import * as dompack from 'dompack';\n\n This function is our public api. Any direct inclusions from src/.... not mentioned here are not a stable API\n*/\n\nexport { createDeferred } from './src/promise.es';\nexport { flagUIBusy } from './src/busy.es';\nexport { dispatchCustomEvent, dispatchDomEvent, fireModifiedEvents, changeValue, normalizeKeyboardEventData\n , stop\n } from './src/events.es';\nexport { qS, qSA, contains, closest, matches\n , empty, isDomReady, onDomReady, getJSONAttribute\n , before, after, replaceWith, remove, prepend, append\n , toggleClass, toggleClasses\n , setStyles\n , getBaseURI, getRelativeBounds } from './src/tree.es';\nexport { create, jsxcreate } from './src/create.es';\nexport { focus, register, registerMissed, scrollIntoView, getRect } from './src/components.es';\nexport { debugflags, parseDebugURL, addDebugFlags, initDebug } from './src/debug.es';\n", "/**\nimport * as whintegration from '@mod-system/js/wh/integration';\n*/\n\nimport * as dompack from 'dompack';\n\nexport let config = {};\n\nfunction generateForm(action, values, method)\n{\n var form = dompack.create(\"form\", { action: action, method: method || \"POST\", charset: \"utf-8\" });\n if(values instanceof Array)\n {\n values.forEach(function(item)\n {\n form.appendChild(dompack.create(\"input\", { type: \"hidden\", name: item.name, value: item.value }));\n });\n }\n else Object.keys(values, key =>\n {\n form.appendChild(dompack.create(\"input\", { type: \"hidden\", name: key, value: values[key] }));\n });\n return form;\n}\n\nexport function submitForm(action, values, method)\n{\n var form = generateForm(action, values, method);\n document.body.appendChild(form);\n form.submit();\n}\n\nexport function executeSubmitInstruction(instr, options)\n{\n if(!instr)\n throw Error(\"Unknown instruction received\");\n\n options = Object.assign({ ismodal: true }, options);\n //Are there any cirumstances where you would want to reelase this lock?\n dompack.flagUIBusy({ ismodal: options.ismodal });\n\n if (options.iframe)\n {\n switch (instr.type)\n {\n case \"redirect\":\n {\n options.iframe.src = instr.url;\n } break;\n\n case \"form\":\n {\n // FIXME: Clear iframe if document is not cross-domain accessible\n var idoc = options.iframe.document || options.iframe.contentDocument || options.iframe.contentWindow.document;\n\n var form = generateForm(instr.form.action, instr.form.vars, instr.method);\n var adopted_form = idoc.adoptNode(form);\n idoc.body.appendChild(adopted_form);\n adopted_form.submit();\n } break;\n\n default:\n {\n throw Error(\"Unknown submit instruction '\" + instr.type + \"' for iframe received\");\n }\n }\n return;\n }\n\n switch (instr.type)\n {\n case \"redirect\":\n {\n location.href=instr.url;\n } break;\n\n case \"form\":\n {\n submitForm(instr.form.action, instr.form.vars, instr.form.method);\n } break;\n\n case \"refresh\":\n case \"reload\":\n {\n window.location.reload();\n } break;\n\n case \"postmessage\":\n {\n if (!instr.target || instr.target === \"parent\")\n parent.postMessage(instr.message, \"*\");\n else if (instr.target === \"opener\")\n {\n opener.postMessage(instr.message, \"*\");\n window.close();\n }\n else\n throw Error(\"Unknown postmessage target '\" + instr.target + \"' received\");\n } break;\n\n case \"close\":\n {\n window.close();\n } break;\n\n default:\n {\n throw new Error(\"Unknown submit instruction '\" + instr.type + \"' received\");\n }\n }\n}\n\nif(typeof window !== 'undefined') //check we're in a browser window, ie not serverside or some form of worker\n{\n let whconfigel = typeof document != \"undefined\" ? document.querySelector('script#wh-config') : null;\n if(whconfigel)\n config = JSON.parse(whconfigel.textContent);\n\n // Make sure we have obj/site as some sort of object, to prevent crashes on naive 'if ($wh.config.obj.x)' tests'\n if(!config.obj)\n config.obj={};\n if(!config.site)\n config.site={};\n\n let errhandler = config[\"system:errorhandler\"];\n if(errhandler)\n console.error(errhandler.statuscode + \" \" + errhandler.statusmessage);\n}\n", "/** @import: import KeyboardHandler from \"dompack/extra/keyboard\";\n\n*/\n\n/* All keynames should be mixed-cased, as done here: http://www.w3.org/TR/DOM-Level-3-Events-key/\n\n Modifiers should be in the ordering Alt+Control+Meta+Shift+key (ie alphabetical)\n\n To simply prevent keys from propagating up (ie to keep Enter inside its textarea)\n new KeyboardHandler(this.textarea, {}, { dontpropagate: ['Enter']});\n*/\n\nimport { normalizeKeyboardEventData } from '../src/events.es';\nimport { debugflags } from '../src/debug.es';\n\nvar propnames = { \"shiftKey\": \"Shift\"\n , \"ctrlKey\": navigator.platform == \"MacIntel\" ? \"Control\" : [ \"Accel\", \"Control\" ]\n , \"metaKey\": navigator.platform == \"MacIntel\" ? [ \"Accel\", \"Meta\" ] : \"Meta\"\n , \"altKey\": \"Alt\"\n };\n\nfunction getFinalKey(event) //get the naem for the 'final' key, eg the 'D' in 'alt+control+d'\n{\n if(event.code.startsWith('Key') && event.code.length==4)\n return event.code.substr(3,1).toUpperCase();\n if(event.code.startsWith('Digit') && event.code.length==6)\n return event.code.substr(5,1).toUpperCase();\n return event.key.length === 1 ? event.key.toUpperCase() : event.key;\n}\n\nfunction getKeyNames(event)\n{\n let names = [[]];\n\n/*\n // Firefix under selenium on linux always says 'Unidentified' as key. Backup for some keys.\n if (basekey == \"Unidentified\")\n basekey = selenium_backup[event.keyCode];\n*/\n // Create the modifiers in the names array (omit the basekey, so we can sort on modifier first)\n Object.keys(propnames).forEach(function(propname)\n {\n if (event[propname])\n {\n // The key is pressed. Add the modifier name to all current names.\n var modifier = propnames[propname];\n if (!Array.isArray(modifier))\n names.forEach(function(arr) { arr.push(modifier); });\n else\n {\n // Multiple modifiers map to this key, duplicate all result sequences for every modifier\n var newkeys = [];\n modifier.forEach(function(singlemodifier)\n {\n names.forEach(function(arr)\n {\n newkeys.push(arr.concat([ singlemodifier ]));\n });\n });\n names = newkeys;\n }\n }\n });\n\n names = names.map(function(arr)\n {\n // Sort the modifier names\n arr = arr.sort();\n arr.push(getFinalKey(event));\n return arr.join(\"+\");\n });\n\n return names;\n}\n\nfunction validateKeyName(key)\n{\n var modifiers = key.split(\"+\");\n modifiers.pop();\n\n // Check for allowed modifiers\n modifiers.forEach(function(mod)\n {\n if (![ \"Accel\", \"Alt\", \"Control\", \"Meta\", \"Shift\"].includes(mod))\n throw new Error(\"Illegal modifier name '\" + mod + \"' in key '\" + key + \"'\");\n });\n\n var original_order = modifiers.join('+');\n modifiers.sort();\n if (modifiers.join('+') != original_order)\n throw new Error(\"Illegal key name \" + key + \", modifiers must be sorted alphabetically\");\n}\n\n/** node: The node to attach to\n keymap: Keymap\n options.stopmapped - preventDefault and stopPropagation on any key we have in our map\n options.dontpropagate - string array of keys not to propagate out of this object\n options.onkeypress - when set, call for all keypresses. signature: function(event, key). Should always return true and preventDefault (and/or stop) the event to cancel its handling\n options.listenoptions - addEventListener options (eg {capture:true})\n*/\nexport default class KeyboardHandler\n{\n constructor(node, keymap, options)\n {\n this.node = node;\n this.keymap = {};\n this.stopmapped = options&&options.stopmapped;\n this.dontpropagate = options && options.dontpropagate ? [...options.dontpropagate].map(name => name.toUpperCase()) : [];\n this.onkeypress = options&&options.onkeypress;\n this.captureunsafekeys = options&&options.captureunsafekeys;\n this._listenoptions = (options && options.listenoptions) || {};\n\n Object.keys(keymap).forEach(keyname =>\n {\n if (debugflags.key)\n validateKeyName(keyname);\n this.keymap[keyname.toUpperCase()] = keymap[keyname];\n });\n\n this._onkeydown = (event) => this._onKeyDown(event);\n this._onkeypress = (event) => this._onKeyPress(event);\n node.addEventListener('keydown', this._onkeydown, this._listenoptions);\n node.addEventListener('keypress', this._onkeypress, this._listenoptions);\n }\n\n destroy()\n {\n this.node.removeEventListener('keydown', this._onkeydown, this._listenoptions);\n this.node.removeEventListener('keypress', this._onkeypress, this._listenoptions);\n }\n\n /** Returns thether the current pressed special key should be ignored for the current target node\n Used to detect input/textarea/rte's\n @param target Current target node for keyboard event\n @param key Parsed key (as returned by GetKeyNames)\n @return Whether the key must be ignored by KeyboardHandler, default browser behaviour should be triggered.\n */\n _mustIgnoreKey(target, key, keynames)\n {\n var tag = target.nodeName.toLowerCase();\n if (tag == \"select\")\n {\n if ([\"ArrowUp\", \"ArrowDown\", \"Home\", \"End\", \"PageUp\", \"PageDown\"].indexOf(key) != -1)\n return true;\n }\n else if (tag == \"input\" || tag == \"textarea\" || target.isContentEditable)\n {\n // These keys we ignore, regardless of the modifier\n if ([ \"ArrowLeft\", \"ArrowRight\", \"ArrowUp\", \"ArrowDown\"\n , \"PageUp\", \"PageDown\"\n , \"Home\", \"End\"\n , \"Insert\", \"Delete\", \"Backspace\"\n ].indexOf(key) != -1)\n return true;\n\n var is_special_combo = false;\n\n // only input doesn't want exact combo 'Enter', the rest does\n if (tag != \"input\" && keynames.indexOf(\"Enter\") != -1)\n is_special_combo = true;\n\n // Only contenteditable wants \"Shift+Enter\"\n if (target.isContentEditable && keynames.indexOf(\"Shift+Enter\") != -1)\n is_special_combo = true;\n\n // These exact combo's are wanted by all inputs\n [ \"Accel+A\", \"Accel+V\", \"Accel+C\", \"Accel+X\" ].forEach(function(name)\n {\n is_special_combo = is_special_combo || keynames.indexOf(name) != -1;\n });\n return is_special_combo;\n }\n return false;\n }\n\n addKey(keybinding, handler)\n {\n if(debugflags.key)\n {\n validateKeyName(keybinding);\n console.log(\"[key] KeyDown handler registered for \" + keybinding);\n }\n this.keymap[keybinding.toUpperCase()] = handler;\n }\n removeKey(keybinding)\n {\n delete this.keymap[keybinding.toUpperCase()];\n }\n _onKeyDown(event)\n {\n let keydata = normalizeKeyboardEventData(event);\n\n // Get all possible names for this key\n let keynames = getKeyNames(keydata);\n if (!keydata.key || !keynames.length)\n {\n if(debugflags.key)\n console.log(\"[key] KeyDown handler for \", this.node, \" did not recognize key from event\",event);\n return true;\n }\n\n if(debugflags.key)\n console.log(\"[key] KeyDown handler for \", this.node, \" got key \", keydata.key, \" with target \", event.target, \" keynames:\",keynames);\n\n /* Some keys we ignore, unless we're explicitly bound to a node, so we don't inadvertly break eg a node inside\n a listview we're handling or otherwise break a user's expectation. Set the option 'captureunsafekeys' if you explicitly\n want to be able to capture any key */\n\n if (!this.captureunsafekeys && this._mustIgnoreKey(event.target, keydata.key, keynames))\n {\n if(debugflags.key)\n console.log(\"[key] KeyDown event will not be intercepted, it's an unsafe key to intercept\");\n return true;\n }\n\n if (this.dontpropagate)\n {\n keynames.forEach(keyname =>\n {\n if (this.dontpropagate.includes(keyname))\n {\n if(debugflags.key)\n console.log(\"[key] KeyDown event will not bubbleup because of our dontpropagate option (but may still trigger a default action)\");\n event.stopPropagation();\n }\n });\n }\n\n for (var i = 0; i < keynames.length; ++i)\n {\n let mapping = this.keymap[keynames[i].toUpperCase()];\n if(!mapping)\n continue;\n\n if (this.stopmapped)\n {\n if(debugflags.key)\n console.log(\"[key] KeyDown event will not bubbleup or trigger default, because we're configured to block any mapped key\");\n event.stopPropagation();\n event.preventDefault();\n }\n\n let ishandled = mapping.apply(this.node,[event]);\n if(ishandled && !event.defaultPrevented)\n {\n console.warn(`The key handler for '${keynames[i]}' should preventDefault (or dompack.stop) the event to block fruther propagation`);\n event.stopPropagation();\n event.preventDefault();\n if(debugflags.key)\n console.log(\"[key] KeyDown event will not bubbleup or trigger default, because the keyhandler indicated the key was handled\");\n }\n\n if(!event.defaultPrevented && debugflags.key)\n console.log(\"[key] KeyDown event was not blocked by its explicitly configured handler\");\n }\n return true;\n }\n _onKeyPress(event)\n {\n let keydata = normalizeKeyboardEventData(event);\n\n if (this.onkeypress)\n {\n if (!this.onkeypress.apply(this.node, [ event, keydata.key ]))\n {\n if(!event.defaultPrevented)\n console.warn(\"The onkeypress handler should preventDefault (or dompack.stop) the event to block fruther propagation\");\n event.stopPropagation();\n event.preventDefault();\n }\n }\n }\n}\n\nexport function getEventKeyNames(event)\n{\n let keydata = normalizeKeyboardEventData(event);\n return getKeyNames(keydata);\n}\n\nKeyboardHandler.getEventKeyNames = function(event)\n{\n let keydata = normalizeKeyboardEventData(event);\n return getKeyNames(keydata);\n};\n\n/** Is the native 'copy' modifier for this platform pressed? */\nKeyboardHandler.hasNativeEventCopyKey = function(event)\n{\n return event && (navigator.platform == \"MacIntel\" ? event.altKey : event.ctrlKey);\n};\n\n/** Is the native 'multiselect' modifier for this platform pressed? */\nKeyboardHandler.hasNativeEventMultiSelectKey = function(event)\n{\n return event && (navigator.platform == \"MacIntel\" ? event.metaKey : event.ctrlKey);\n};\n\nKeyboardHandler.getDragModeOverride = function(event)\n{\n const modifiers =\n (event.altKey?\"Alt+\":\"\") +\n (event.ctrlKey?\"Control+\":\"\") +\n (event.metaKey?\"Meta+\":\"\") +\n (event.shiftKey?\"Shift+\":\"\") +\n (navigator.platform === \"MacIntel\" ? \"Mac\" : \"Other\");\n\n let override = \"\";\n switch (modifiers)\n {\n case \"Shift+Other\":\n case \"Meta+Other\": override = \"move\"; break;\n case \"Control+Other\":\n case \"Alt+Mac\": override = \"copy\"; break;\n case \"Control+Shift+Other\":\n case \"Alt+Other\":\n case \"Control+Mac\": override = \"link\"; break;\n }\n\n return override;\n};\n", "// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n'use strict';\n\nvar R = typeof Reflect === 'object' ? Reflect : null\nvar ReflectApply = R && typeof R.apply === 'function'\n ? R.apply\n : function ReflectApply(target, receiver, args) {\n return Function.prototype.apply.call(target, receiver, args);\n }\n\nvar ReflectOwnKeys\nif (R && typeof R.ownKeys === 'function') {\n ReflectOwnKeys = R.ownKeys\n} else if (Object.getOwnPropertySymbols) {\n ReflectOwnKeys = function ReflectOwnKeys(target) {\n return Object.getOwnPropertyNames(target)\n .concat(Object.getOwnPropertySymbols(target));\n };\n} else {\n ReflectOwnKeys = function ReflectOwnKeys(target) {\n return Object.getOwnPropertyNames(target);\n };\n}\n\nfunction ProcessEmitWarning(warning) {\n if (console && console.warn) console.warn(warning);\n}\n\nvar NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {\n return value !== value;\n}\n\nfunction EventEmitter() {\n EventEmitter.init.call(this);\n}\nmodule.exports = EventEmitter;\n\n// Backwards-compat with node 0.10.x\nEventEmitter.EventEmitter = EventEmitter;\n\nEventEmitter.prototype._events = undefined;\nEventEmitter.prototype._eventsCount = 0;\nEventEmitter.prototype._maxListeners = undefined;\n\n// By default EventEmitters will print a warning if more than 10 listeners are\n// added to it. This is a useful default which helps finding memory leaks.\nvar defaultMaxListeners = 10;\n\nfunction checkListener(listener) {\n if (typeof listener !== 'function') {\n throw new TypeError('The \"listener\" argument must be of type Function. Received type ' + typeof listener);\n }\n}\n\nObject.defineProperty(EventEmitter, 'defaultMaxListeners', {\n enumerable: true,\n get: function() {\n return defaultMaxListeners;\n },\n set: function(arg) {\n if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {\n throw new RangeError('The value of \"defaultMaxListeners\" is out of range. It must be a non-negative number. Received ' + arg + '.');\n }\n defaultMaxListeners = arg;\n }\n});\n\nEventEmitter.init = function() {\n\n if (this._events === undefined ||\n this._events === Object.getPrototypeOf(this)._events) {\n this._events = Object.create(null);\n this._eventsCount = 0;\n }\n\n this._maxListeners = this._maxListeners || undefined;\n};\n\n// Obviously not all Emitters should be limited to 10. This function allows\n// that to be increased. Set to zero for unlimited.\nEventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {\n if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {\n throw new RangeError('The value of \"n\" is out of range. It must be a non-negative number. Received ' + n + '.');\n }\n this._maxListeners = n;\n return this;\n};\n\nfunction _getMaxListeners(that) {\n if (that._maxListeners === undefined)\n return EventEmitter.defaultMaxListeners;\n return that._maxListeners;\n}\n\nEventEmitter.prototype.getMaxListeners = function getMaxListeners() {\n return _getMaxListeners(this);\n};\n\nEventEmitter.prototype.emit = function emit(type) {\n var args = [];\n for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);\n var doError = (type === 'error');\n\n var events = this._events;\n if (events !== undefined)\n doError = (doError && events.error === undefined);\n else if (!doError)\n return false;\n\n // If there is no 'error' event listener then throw.\n if (doError) {\n var er;\n if (args.length > 0)\n er = args[0];\n if (er instanceof Error) {\n // Note: The comments on the `throw` lines are intentional, they show\n // up in Node's output if this results in an unhandled exception.\n throw er; // Unhandled 'error' event\n }\n // At least give some kind of context to the user\n var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));\n err.context = er;\n throw err; // Unhandled 'error' event\n }\n\n var handler = events[type];\n\n if (handler === undefined)\n return false;\n\n if (typeof handler === 'function') {\n ReflectApply(handler, this, args);\n } else {\n var len = handler.length;\n var listeners = arrayClone(handler, len);\n for (var i = 0; i < len; ++i)\n ReflectApply(listeners[i], this, args);\n }\n\n return true;\n};\n\nfunction _addListener(target, type, listener, prepend) {\n var m;\n var events;\n var existing;\n\n checkListener(listener);\n\n events = target._events;\n if (events === undefined) {\n events = target._events = Object.create(null);\n target._eventsCount = 0;\n } else {\n // To avoid recursion in the case that type === \"newListener\"! Before\n // adding it to the listeners, first emit \"newListener\".\n if (events.newListener !== undefined) {\n target.emit('newListener', type,\n listener.listener ? listener.listener : listener);\n\n // Re-assign `events` because a newListener handler could have caused the\n // this._events to be assigned to a new object\n events = target._events;\n }\n existing = events[type];\n }\n\n if (existing === undefined) {\n // Optimize the case of one listener. Don't need the extra array object.\n existing = events[type] = listener;\n ++target._eventsCount;\n } else {\n if (typeof existing === 'function') {\n // Adding the second element, need to change to array.\n existing = events[type] =\n prepend ? [listener, existing] : [existing, listener];\n // If we've already got an array, just append.\n } else if (prepend) {\n existing.unshift(listener);\n } else {\n existing.push(listener);\n }\n\n // Check for listener leak\n m = _getMaxListeners(target);\n if (m > 0 && existing.length > m && !existing.warned) {\n existing.warned = true;\n // No error code for this since it is a Warning\n // eslint-disable-next-line no-restricted-syntax\n var w = new Error('Possible EventEmitter memory leak detected. ' +\n existing.length + ' ' + String(type) + ' listeners ' +\n 'added. Use emitter.setMaxListeners() to ' +\n 'increase limit');\n w.name = 'MaxListenersExceededWarning';\n w.emitter = target;\n w.type = type;\n w.count = existing.length;\n ProcessEmitWarning(w);\n }\n }\n\n return target;\n}\n\nEventEmitter.prototype.addListener = function addListener(type, listener) {\n return _addListener(this, type, listener, false);\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.prependListener =\n function prependListener(type, listener) {\n return _addListener(this, type, listener, true);\n };\n\nfunction onceWrapper() {\n if (!this.fired) {\n this.target.removeListener(this.type, this.wrapFn);\n this.fired = true;\n if (arguments.length === 0)\n return this.listener.call(this.target);\n return this.listener.apply(this.target, arguments);\n }\n}\n\nfunction _onceWrap(target, type, listener) {\n var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };\n var wrapped = onceWrapper.bind(state);\n wrapped.listener = listener;\n state.wrapFn = wrapped;\n return wrapped;\n}\n\nEventEmitter.prototype.once = function once(type, listener) {\n checkListener(listener);\n this.on(type, _onceWrap(this, type, listener));\n return this;\n};\n\nEventEmitter.prototype.prependOnceListener =\n function prependOnceListener(type, listener) {\n checkListener(listener);\n this.prependListener(type, _onceWrap(this, type, listener));\n return this;\n };\n\n// Emits a 'removeListener' event if and only if the listener was removed.\nEventEmitter.prototype.removeListener =\n function removeListener(type, listener) {\n var list, events, position, i, originalListener;\n\n checkListener(listener);\n\n events = this._events;\n if (events === undefined)\n return this;\n\n list = events[type];\n if (list === undefined)\n return this;\n\n if (list === listener || list.listener === listener) {\n if (--this._eventsCount === 0)\n this._events = Object.create(null);\n else {\n delete events[type];\n if (events.removeListener)\n this.emit('removeListener', type, list.listener || listener);\n }\n } else if (typeof list !== 'function') {\n position = -1;\n\n for (i = list.length - 1; i >= 0; i--) {\n if (list[i] === listener || list[i].listener === listener) {\n originalListener = list[i].listener;\n position = i;\n break;\n }\n }\n\n if (position < 0)\n return this;\n\n if (position === 0)\n list.shift();\n else {\n spliceOne(list, position);\n }\n\n if (list.length === 1)\n events[type] = list[0];\n\n if (events.removeListener !== undefined)\n this.emit('removeListener', type, originalListener || listener);\n }\n\n return this;\n };\n\nEventEmitter.prototype.off = EventEmitter.prototype.removeListener;\n\nEventEmitter.prototype.removeAllListeners =\n function removeAllListeners(type) {\n var listeners, events, i;\n\n events = this._events;\n if (events === undefined)\n return this;\n\n // not listening for removeListener, no need to emit\n if (events.removeListener === undefined) {\n if (arguments.length === 0) {\n this._events = Object.create(null);\n this._eventsCount = 0;\n } else if (events[type] !== undefined) {\n if (--this._eventsCount === 0)\n this._events = Object.create(null);\n else\n delete events[type];\n }\n return this;\n }\n\n // emit removeListener for all listeners on all events\n if (arguments.length === 0) {\n var keys = Object.keys(events);\n var key;\n for (i = 0; i < keys.length; ++i) {\n key = keys[i];\n if (key === 'removeListener') continue;\n this.removeAllListeners(key);\n }\n this.removeAllListeners('removeListener');\n this._events = Object.create(null);\n this._eventsCount = 0;\n return this;\n }\n\n listeners = events[type];\n\n if (typeof listeners === 'function') {\n this.removeListener(type, listeners);\n } else if (listeners !== undefined) {\n // LIFO order\n for (i = listeners.length - 1; i >= 0; i--) {\n this.removeListener(type, listeners[i]);\n }\n }\n\n return this;\n };\n\nfunction _listeners(target, type, unwrap) {\n var events = target._events;\n\n if (events === undefined)\n return [];\n\n var evlistener = events[type];\n if (evlistener === undefined)\n return [];\n\n if (typeof evlistener === 'function')\n return unwrap ? [evlistener.listener || evlistener] : [evlistener];\n\n return unwrap ?\n unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);\n}\n\nEventEmitter.prototype.listeners = function listeners(type) {\n return _listeners(this, type, true);\n};\n\nEventEmitter.prototype.rawListeners = function rawListeners(type) {\n return _listeners(this, type, false);\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n if (typeof emitter.listenerCount === 'function') {\n return emitter.listenerCount(type);\n } else {\n return listenerCount.call(emitter, type);\n }\n};\n\nEventEmitter.prototype.listenerCount = listenerCount;\nfunction listenerCount(type) {\n var events = this._events;\n\n if (events !== undefined) {\n var evlistener = events[type];\n\n if (typeof evlistener === 'function') {\n return 1;\n } else if (evlistener !== undefined) {\n return evlistener.length;\n }\n }\n\n return 0;\n}\n\nEventEmitter.prototype.eventNames = function eventNames() {\n return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];\n};\n\nfunction arrayClone(arr, n) {\n var copy = new Array(n);\n for (var i = 0; i < n; ++i)\n copy[i] = arr[i];\n return copy;\n}\n\nfunction spliceOne(list, index) {\n for (; index + 1 < list.length; index++)\n list[index] = list[index + 1];\n list.pop();\n}\n\nfunction unwrapListeners(arr) {\n var ret = new Array(arr.length);\n for (var i = 0; i < ret.length; ++i) {\n ret[i] = arr[i].listener || arr[i];\n }\n return ret;\n}\n", "import * as dompack from 'dompack';\nconst EventEmitter = require('events');\n\nclass InternetRequester extends EventEmitter\n{\n constructor(options)\n {\n super();\n if(!options)\n options={};\n //Host url of event server\n this.options = { url: options.url || ''\n , log: Boolean(options.log || dompack.debugflags.rpc)\n , withcredentials: 'withCredentials' in options && options.withcredentials\n };\n\n // XMLHttpRequest\n this.conn = null;\n\n // used for estimating the server date\n this.__date_server = null;\n this.__date_client = null;\n this.__date_diff = null;\n }\n\n destroy()\n {\n this.stopCurrentRequest();\n this.conn = null;\n }\n\n stopCurrentRequest()\n {\n if(this.conn)\n {\n this.conn.onreadystatechange = null;\n this.conn.onloadend = null;\n\n this.conn.abort();\n }\n if (this.jsoncheckinterval)\n {\n clearTimeout(this.jsoncheckinterval);\n this.jsoncheckinterval = null;\n }\n }\n\n ensureConnection()\n {\n if (!this.conn)\n this.conn = new XMLHttpRequest();\n }\n\n startXMLHTTPRequest(method, url, body, options)\n {\n this.ensureConnection();\n\n var async = !options || !options.synchronous;\n\n // Because aborting the connection may result in a readystatechange event (yes, we're looking at you, Titanium's\n // TiNetworkHTTPClient...), we have to reset the have_response flag _after_ aborting the connection, so the response for\n // the previous request isn't used for the new request\n\n this.laststateevent = null; //make sure we don't accidentally cancel the previous request\n this.conn.abort();\n this.have_response = false;\n\n this.conn.open(method.toUpperCase(), url, async);\n if(options && options.headers)\n Object.keys(options.headers).forEach(key => { this.conn.setRequestHeader(key,options.headers[key]); });\n\n if(this.options.withcredentials)\n this.conn.withCredentials = true;\n\n this.conn.onreadystatechange = this.onStateChange.bind(this);\n // Required for Firefox 12 (+firebug?), without it statechange to 4 doesn't seem to be fired sometimes\n this.conn.onloadend = this.onStateChange.bind(this);\n this.conn.onabort = this.onAbort.bind(this);\n\n this.emit(\"requeststart\", { target: this });\n this.conn.send(body);\n\n if (!async)\n this.onStateChange();\n }\n\n onAbort(event)\n {\n if(this.laststateevent)\n this.laststateevent.isaborted = true;\n }\n\n onStateChange (event)\n {\n if (this.conn.readyState != 4 || this.have_response)\n return;\n\n this.have_response = true;\n\n var datestr = this.conn.getResponseHeader(\"date\");\n if (datestr != \"\")\n {\n var parseddate = Date.parse(datestr);\n this.__date_server = parseddate;\n this.__date_client = new Date();\n this.__date_diff = this.__date_server - this.__date_client;\n }\n\n var evt = { target: this\n , success: this.conn.status == 200\n , internalerror: this.conn.status == 500\n , message: this.conn.status\n\n , responsetext: this.conn.responseText\n , responsejson: null\n };\n\n //FIXME only decode JSON data if the mimetype specified it was JSON, and then log any errors\n try\n {\n evt.responsejson = JSON.parse(evt.responsetext);\n }\n catch(e)\n {\n }\n\n this.laststateevent = evt;\n this.emit(\"requestend\", evt);\n }\n}\n\nmodule.exports = InternetRequester;\n", "/** @require: var JSONRPC = require('@mod-system/js/net/jsonrpc')\n*/\nconst InternetRequester = require('./requester');\nimport * as whintegration from '@mod-system/js/wh/integration';\nimport * as dompack from 'dompack';\n\nvar rpcscriptid = Math.floor(Math.random()* 1000);\n\nclass JSONRPC extends InternetRequester\n{\n /** @short RPC status codes (defined as getter-only properties as long as we don't have static const properties) */\n static get HTTP_ERROR() { return -1; } // Error connecting to the RPC server\n static get JSON_ERROR() { return -2; } // The returned value could not be decoded into a JSON object\n static get PROTOCOL_ERROR() { return -3; } // The return object did not contain an id, or the id did not match the request id\n static get RPC_ERROR() { return -4; } // The RPC returned an error\n static get OFFLINE_ERROR() { return -5; } // The application is not online (only returned if the onlineonly option was set)\n static get TIMEOUT_ERROR() { return -6; } // The request could not be sent or was not answered before within the timeout set in the options\n static get SERVER_ERROR() { return -7; } // The server encountered an internal error\n\n constructor(options)\n {\n super(options);\n if(!options)\n options={};\n\n this.lastid = 0;\n this.requestqueue = [];\n this.cachecounter = 0;\n this.activerequest = null;\n this.haveresponse = false;\n //timeout after which we trigger a 'wait' action, eg a spinner\n this.options.waittimeout = 'waittimeout' in options ? options.waittimeout : 500;\n this.options.appendfunctionname = 'appendfunctionname' in options ? options.appendfunctionname : false;\n this.waitcallback = null;\n this.waittimeoutid = null;\n this.waitingnow = false;\n\n this.on(\"requestend\", this.onResponse.bind(this));\n }\n\n destroy()\n {\n super.destroy();\n this.requestqueue = [];\n this.activerequest = null;\n\n if (this.waittimeoutid)\n {\n clearTimeout(this.waittimeoutid);\n this.waittimeoutid = null;\n }\n }\n\n promiseRequest(method, params, options)\n {\n let deferred = dompack.createDeferred();\n let req = this.request(method, params, deferred.resolve, (errorcode, errormsg, rpcid) => { deferred.reject(new Error(errormsg)); }, options);\n deferred.promise.__jsonrpcinfo = { deferred, req };\n return deferred.promise;\n }\n async(method, ...params)\n {\n return this.promiseRequest(method, params);\n }\n\n _doAsyncAbort(promise, result, rejection)\n {\n if(!promise.__jsonrpcinfo)\n throw new Error(\"The promise is not an async JSONRPC request\");\n if(!rejection)\n promise.__jsonrpcinfo.deferred.resolve(result);\n else\n promise.__jsonrpcinfo.deferred.reject(rejection);\n promise.__jsonrpcinfo.req.cancel();\n }\n\n rpcResolve(promise, result)\n {\n this._doAsyncAbort(promise, result);\n }\n rpcReject(promise, rejection)\n {\n this._doAsyncAbort(promise, null, rejection);\n }\n\n\n/**\n * @short Queue an RPC request\n * @param method The RPC method to call\n * @param params Params for the RPC method\n * @param callback The callback which is called, with:\n * param status A JSONRPC. value\n * param result The result object as sent by the RPC, or an error message string sent by the RPC, or an error\n * message\n * param id The request id\n * @param options Options\n * @param options.url The URL to connect to\n * @param options.timeout Timeout in ms after which the request will fail (callback is called with ERROR_TIMEOUT error)\n * @param options.waittimeout Timeout in ms after which the request will set waiting status to TRUE (via the waitCallback)\n * Set negative to not trigger waiting status.\n * @return The request id\n */\n request(method, params, onsuccess, onfailure, options)\n {\n if(!params || typeof params != \"object\" || params.length === undefined)\n throw new Error(\"The parameters passed to request must be an Array\");\n\n var id = ++this.lastid;\n\n var url;\n if(options && options.url)\n url = options.url + (options.appendfunctionname ? (options.url.match(/\\/$/) ? '' : '/') + method : '');\n else if(this.options.url)\n url = this.options.url + (this.options.appendfunctionname ? (this.options.url.match(/\\/$/) ? '' : '/') + method : '');\n else\n url = location.href; //we do not support appendfunctionname for self-posts\n\n var timeout = Math.max((options && typeof options.timeout == \"number\") ? options.timeout : 0, 0);\n var waittimeout = (options && typeof options.waittimeout == \"number\") ? options.waittimeout : this.options.waittimeout;\n var synchronous = options && options.synchronous || false;\n var errortrace = options && options.errortrace || null;\n\n if (this.options.log)\n console.log(\"JSONRPC request\", method, params, options, 'timeout:', timeout, 'waitTimeout:', waittimeout);\n\n var request = new Request(this, id, method, params, url, timeout, waittimeout, onsuccess, onfailure, synchronous, errortrace);\n if (this.options.log || !whintegration.config || !whintegration.config.islive)\n request.stack = new Error().stack;\n\n this.requestqueue.push(request);\n if (this.options.log)\n console.log(\"JSONRPC request is on queue\");\n this.processNextRequest();\n return request;\n }\n\n handleError(onfailure, errorcode, errormsg, rpcid)\n {\n if(onfailure)\n setTimeout( () => onfailure(errorcode, errormsg, rpcid), 0);\n\n setTimeout( () => this.emit([ \"error\", { target: this, errorcode: errorcode, errormessage: errormsg, rpcid: rpcid } ]), 0);\n }\n\n //is a json request pending?\n isRequestPending()\n {\n return this.activerequest !== null || this.requestqueue.length;\n }\n\n //ADDME is it possible for the 'next' response to already be .delay/setTimeout() scheduled, racing against our cancel ?\n __cancelRequest(id)\n {\n if(typeof id != 'number')\n return;\n\n if (this.activerequest == id)\n {\n this.stopCurrentRequest();\n this.activerequest = null;\n\n var request = this.requestqueue.shift();\n if (request.timeout && typeof request.timeout != \"boolean\")\n clearTimeout(request.timeout);\n\n this.processNextRequest();\n }\n else\n {\n for (var i = 0; i < this.requestqueue.length; ++i)\n if (this.requestqueue[i].id == id)\n {\n this.requestqueue.splice(i, 1);\n break;\n }\n }\n }\n\n processNextRequest()\n {\n if (this.activerequest)\n {\n if(this.options.log)\n console.log(\"JSONRPC request #\" + this.activerequest + \" pending, not scheduling a new one yet\");\n this.handleWaitTimeouts();\n return;\n }\n\n var request = null;\n while (!request)\n {\n request = this.requestqueue[0];\n if (!request)\n {\n if(this.options.log)\n console.log(\"JSONRPC request - processNextRequest, queue is empty\");\n return;\n }\n if (request.timeout && typeof request.timeout == \"boolean\")\n {\n this.requestqueue = this.requestqueue.filter(el => el != request);\n request = this.requestqueue[0];\n }\n }\n\n this.activerequest = request.id;\n\n if (request.timeout)\n request.timeout = setTimeout( () => this.onTimeout(request), request.timeout);\n\n if(this.options.log)\n console.log(\"JSONRPC request #\" + request.id + \" offering for XMLHTTP\");\n this.startXMLHTTPRequest(\n \"post\",\n request.url,\n JSON.stringify(request.request),\n { headers: { \"Content-Type\": \"application/json; charset=utf-8\" }\n , synchronous: request.synchronous\n });\n this.handleWaitTimeouts();\n }\n\n onResponse(event)\n {\n this.activerequest = null;\n\n var request = this.requestqueue[0];\n if (!request)\n return;\n\n this.requestqueue = this.requestqueue.slice(1);\n\n if (request.timeout)\n {\n if (typeof request.timeout == \"boolean\")\n {\n this.processNextRequest();\n return;\n }\n clearTimeout(request.timeout);\n }\n\n var status = -1;\n var result = null;\n\n if (!event.success)\n {\n status = JSONRPC.HTTP_ERROR;\n result = \"HTTP Error: \" + event.message;\n\n if (event.internalerror)\n {\n let json = null;\n try\n {\n json = event.responsejson;\n var trace;\n if(json && json.error && json.error.data)\n {\n trace = json.error.data.trace || json.error.data.errors || json.error.data.list || [];\n\n console.group();\n var line = \"RPC #\" + rpcscriptid +\":\"+ request.id + \" failed: \" + json.error.message;\n console.warn(line);\n if (request.errortrace)\n request.errortrace.push(line);\n trace.forEach(rec =>\n {\n if (rec.filename || rec.line)\n {\n var line = rec.filename + '#' + rec.line + '#' + rec.col + (rec.func ? ' (' + rec.func + ')' : '');\n console.warn(line);\n if (request.errortrace)\n request.errortrace.push(line);\n }\n });\n console.groupEnd();\n }\n status = JSONRPC.SERVER_ERROR;\n result = json.error && `${json.error.message} from ${request.url}` || \"Unknown error\";\n }\n catch (e)\n {\n }\n }\n }\n else\n {\n let json = event.responsejson;\n\n if (!json)\n {\n status = JSONRPC.JSON_ERROR;\n result = \"Invalid JSON response\";\n }\n else if (json.id === null || json.id != request.id)\n {\n status = JSONRPC.PROTOCOL_ERROR;\n result = \"Protocol error: invalid id\";\n }\n else if (json.error !== null)\n {\n status = JSONRPC.RPC_ERROR;\n result = json.error;\n if(this.options.log)\n console.log('RPC error:', result.message ? result.message : '*no message*');\n }\n else if (\"result\" in json)\n {\n status = 0;\n result = json.result;\n }\n else\n {\n status = JSONRPC.PROTOCOL_ERROR;\n result = \"Could not interpret response\";\n }\n }\n\n this.processNextRequest();\n\n if (this.options.log)\n {\n console.log(\"JSONRPC request\", request.request.method, 'status:', status, 'time:', (new Date).getTime()- request.scheduled, 'ms, result:');\n console.log(result);\n }\n\n /*\n console.log({ serverdate: this.__date_server\n , clientdate: this.__date_client\n , diff: this.__date_diff\n });\n */\n setTimeout( () => request.__completedCall(status, result, event),0 );\n }\n\n onTimeout(request)\n {\n request.timeout = true;\n if (this.activerequest == request.id)\n {\n this.activerequest = null;\n this.stopCurrentRequest();\n this.processNextRequest();\n }\n this.handleError(request.onfailure, JSONRPC.TIMEOUT_ERROR, \"Timeout while waiting for response\", request.id);\n }\n\n onWaitTimeout()\n {\n this.waittimeoutid = null;\n this.handleWaitTimeouts();\n }\n\n handleWaitTimeouts()\n {\n if (this.waittimeoutid)\n {\n clearTimeout(this.waittimeoutid);\n this.waittimeoutid = null;\n }\n\n if (!this.waitCallback)\n return;\n\n var waiting = false;\n var nextTimeout = -1;\n\n var now = (new Date).getTime();\n for (var i = 0; i < this.requestqueue.length; ++i)\n {\n var req = this.requestqueue[i];\n if (req.waitTimeout >= 0)\n {\n var waitLength = now - req.scheduled;\n\n if (waitLength >= req.waitTimeout)\n waiting = true;\n else\n {\n var toGo = req.waitTimeout - waitLength;\n if (nextTimeout < 0 || nextTimeout > toGo)\n nextTimeout = toGo;\n }\n }\n }\n\n if (this.waitingNow != waiting)\n {\n this.waitingNow = waiting;\n setTimeout( () => this.waitCallback(waiting), 0);\n }\n\n if (nextTimeout >= 0)\n this.waittimeoutid = setTimeout( () => this.onWaitTimeout(), nextTimeout);\n }\n\n getEstimatedServerTime()\n {\n return new Date().getTime() + this.__date_diff;\n }\n\n /** @short estimate the server's datetime based on the known descrepancy between the date of an reponse from the server and the time on the client\n */\n getEstimatedServerDate()\n {\n return new Date(this.getEstimatedServerTime());\n }\n}\n\nclass Request //extends PreloadableAsset\n{\n constructor(parent, id, method, params, url, timeout, waittimeout, onsuccess, onfailure, synchronous, errortrace)\n {\n// super();\n\n this.cancelled = false;\n this.stack = null;\n\n if (parent.options.log)\n console.log('req',this);\n this.parent=parent;\n this.id = id;\n this.request = { id: id\n , method: method\n , params: params || []\n };\n this.url = url;\n this.onsuccess = onsuccess;\n this.onfailure = onfailure;\n this.timeout = timeout;\n this.scheduled = new Date-0;\n this.waittimeout = waittimeout;\n this.synchronous = synchronous;\n this.errortrace = errortrace;\n\n //this.startPreload();\n }\n onStartPreload()\n {\n\n }\n cancel()\n {\n //we need to prevent a race when our parent invokes cancel(), but we actually had our __completedCall already queued up. if we still fire onsuccess/onfailure, our parent might think we completed the _next_ request our parent submitted\n this.cancelled=true;\n this.parent.__cancelRequest(this.id);\n }\n\n __completedCall(status,result,event)\n {\n if(event.isaborted)\n this.cancelled=true;\n\n if(status == 0)\n {\n if(this.onsuccess && !this.cancelled)\n this.onsuccess(result);\n //this.donePreload(true);\n }\n else\n {\n if(!this.cancelled)\n {\n if(this.stack)\n {\n console.log(\"Stack at calling point:\");\n console.log(this.stack);\n }\n this.parent.handleError(this.onfailure, status, result, this.id);\n }\n //this.donePreload(false);\n }\n }\n}\n\nmodule.exports=JSONRPC;\n", "//ADDME move cookie state to sessionstorage, we don't need to transmit _c cookies on each request\n\nimport * as dompack from 'dompack';\nimport * as domcookie from 'dompack/extra/cookie';\nimport * as whintegration from '@mod-system/js/wh/integration';\nimport Keyboard from 'dompack/extra/keyboard';\n\nimport JSONRPC from '@mod-system/js/net/jsonrpc';\n\nvar defaultauth = null;\n\nfunction getBackVar(backurl)\n{\n backurl = backurl.split('/').slice(3).join('/'); //strip origin, make relative to current server\n return backurl ? '?b=' + encodeURIComponent(backurl) : '';\n}\n\nfunction getURLOrigin(url)\n{\n return url.split('/').slice(0,3).join('/');\n}\n\nclass WRDAuthenticationProvider\n{\n constructor(options)\n {\n if(!options)\n options={};\n\n this.cookiename = 'cookiename' in options ? options.cookiename : \"webharelogin\";\n\n this.refresh();\n }\n\n refresh()\n {\n this.isloggedin = false;\n this.userinfo = null;\n this.logouturl = \"\";\n this.loginservice = new JSONRPC( { url: '/wh_services/wrd/auth' });\n\n var jsstate = domcookie.read(this.cookiename + '_j');\n var currentstate = domcookie.read(this.cookiename + '_c');\n\n if(dompack.debugflags.aut)\n {\n console.log(\"[aut] \" + this.cookiename + \"_j=\" + jsstate);\n console.log(\"[aut] \" + this.cookiename + \"_c=\" + currentstate);\n }\n if(!jsstate)\n return;\n\n if(!currentstate || currentstate.substr(0, jsstate.length) != jsstate)\n {\n location.replace('/.wrd/auth/restoresession.shtml' + getBackVar(location.href));\n return;\n }\n else\n {\n if(dompack.debugflags.aut)\n console.log(\"[aut] looks like we're still logged in\");\n\n this.isloggedin = true;\n if(currentstate.length > 1)\n try\n {\n this.userinfo = JSON.parse(currentstate.substr(jsstate.length));\n }\n catch(e)\n {\n }\n }\n }\n\n //Get the current session id - use this if you need to discard settings\n getCurrentSessionId()\n {\n return domcookie.read(this.cookiename + '_j') || '';\n }\n\n logout()\n {\n let backurl = location.href;\n if(this.logouturl)\n {\n let logouturl = new URL(this.logouturl, backurl).toString();\n if(getURLOrigin(backurl) != getURLOrigin(logouturl))\n throw new Error(\"A logout URL is not allowed to change the origin\"); //we won't be an open redirect. and getBackVar will clear the origin anyway\n\n backurl = logouturl;\n }\n\n let redirectto = '/.wrd/auth/logout.shtml' + getBackVar(backurl);\n location.replace(redirectto);\n }\n\n setupLoginForm(form)\n {\n if(!form)\n throw new Error(\"No such form\");\n\n new Keyboard(form, { \"Enter\": evt => this._handleLoginForm(form, evt) });\n form.addEventListener(\"submit\", evt => this._handleLoginForm(form, evt));\n form.addEventListener(\"click\", evt => this._handleLoginClick(form, evt));\n }\n _handleLoginClick(form, event)\n {\n if(event.target.closest('.wh-wrdauth__loginbutton'))\n return this._handleLoginForm(form, event); //will stop the event too\n }\n _handleLoginForm(form, event)\n {\n dompack.stop(event);\n\n var loginfield = form.querySelector('*[name=\"login\"]');\n var passwordfield = form.querySelector('*[name=\"password\"]');\n var persistentfield = form.querySelector('*[name=\"persistent\"]');\n\n if(!loginfield)\n throw new Error(\"No field named 'login' found\");\n if(!passwordfield)\n throw new Error(\"No field named 'password' found\");\n\n var persistentlogin = persistentfield && persistentfield.checked;\n this._tryLogin(form, loginfield.value, passwordfield.value, { persistent: persistentlogin });\n }\n login(login, password, options)\n {\n options = {...options};\n return new Promise( (resolve, reject) =>\n {\n var url = new URL(location.href);\n\n var opts =\n { logincontrol: url.searchParams.get(\"wrdauth_logincontrol\") || \"\"\n };\n\n return this.loginservice.request('Login'\n , [ location.href\n , login\n , password\n , Boolean(options.persistent)\n , opts\n ]\n , function(response)\n { //success handler\n resolve(response);\n }\n , function(error)\n {\n reject(error);//FIXME translate to exception\n }\n );\n });\n }\n\n loginSecondFactor(loginproof, type, data, options)\n {\n return new Promise( (resolve, reject) =>\n {\n var url = new URL(location.href);\n\n var opts =\n { logincontrol: url.searchParams.get(\"wrdauth_logincontrol\") || \"\"\n };\n\n return this.loginservice.request('LoginSecondFactor'\n , [ location.href\n , loginproof\n , type\n , { ...data}\n , opts\n ]\n , function(response)\n { //success handler\n resolve(response);\n }\n , function(error)\n {\n reject(error);//FIXME translate to exception\n }\n );\n });\n }\n\n /** Get the afterlogin submitinstruction from the wrdauth_logincontrol webvariable\n @cell(string) opts.logincontrol Override wrdauth_logincontrol variable from the url\n @return Submit instruction. The defult instruction is { \"type\": \"reload\" }.\n */\n getAfterLoginSubmitInstruction(opts = {})\n {\n const url = new URL(location.href);\n const logincontrol = opts.logincontrol || url.searchParams.get(\"wrdauth_logincontrol\") || \"\";\n\n return new Promise( (resolve, reject) =>\n {\n this.loginservice.request('getAfterLoginSubmitInstruction',\n [ location.href, logincontrol ],\n function(response)\n { //success handler\n resolve(response);\n }\n , function(error)\n {\n reject(error);//FIXME translate to exception\n }\n );\n });\n }\n\n //ADDME do we have direct callers or can we _tryLogin this?\n //FIXME be more wh-form like, at least BEM the 'submitting' class\n _tryLogin(form, login, password, options)\n {\n let loginlock = dompack.flagUIBusy();\n if(form)\n form.classList.add(\"submitting\");\n\n this.login(login, password, options).then( result => this.onLoginSuccess(loginlock, form, result) )\n .catch( error => this._onLoginFailure(loginlock, form, options, error));\n }\n onLoginSuccess(loginlock, form, response)\n {\n if(form)\n form.classList.remove(\"submitting\");\n\n let completion = () => this._completeLoginSuccess(loginlock, response, form);\n dompack.dispatchCustomEvent(form || document.documentElement, 'wh:wrdauth-onlogin',\n { bubbles: true\n , cancelable: true\n , detail: { callback: completion, userinfo: response.userinfo }\n , defaulthandler: completion\n });\n }\n _completeLoginSuccess(loginlock, response, form)\n {\n loginlock.release();\n if(response.success)\n {\n if (response.submitinstruction)\n {\n whintegration.executeSubmitInstruction(response.submitinstruction);\n return;\n }\n\n //The user has succesfully logged in\n location.reload(true);\n return;\n }\n\n this._failLogin(/* FIXME? Locale.get('wh-common.authentication.loginfail') || */'The specified login data is incorrect.', response, form);\n }\n _onLoginFailure(loginlock, form, options, code, msg)\n {\n if(form)\n form.classList.remove(\"submitting\");\n loginlock.release();\n\n this._failLogin(/* FIXME? Locale.get('wh-common.authentication.loginerror') || */'An error has occurred.', { code: code }, form);\n }\n _failLogin(message, response, form)\n {\n let evtdetail = { message: message\n , code: response.code\n , data: response.data\n };\n\n let cancelled = !dompack.dispatchCustomEvent(form || document.documentElement, \"wh:wrdauth-loginfailed\", { bubbles: true, cancelable: true, detail: evtdetail });\n if(!cancelled)\n {\n /*\n if($wh.Popup && $wh.Popup.Dialog)\n new $wh.Popup.Dialog( { text: failevent.message, buttons: [{ result: 'ok', title: \"Ok\" }] });\n else*/\n alert(message);\n }\n }\n isLoggedIn()\n {\n return this.isloggedin;\n }\n getUserInfo()\n {\n return this.userinfo;\n }\n setLogoutURL(url)\n {\n this.logouturl = url;\n }\n\n startLogin(type, sp_tag, options)\n {\n options = options || {};\n var defer = dompack.createDeferred();\n\n this.loginservice.request('StartLogin'\n , [ type, sp_tag, location.href, options ]\n , defer.resolve\n , defer.reject //FIXME translate to exception\n );\n\n return defer.promise;\n }\n startSAMLLogin(sp_tag, options)\n {\n return this.startLogin('saml', sp_tag, options);\n }\n\n //Setup the page with loginstate. automatically invoked on the default auth provider\n setupPage()\n {\n document.documentElement.classList.toggle(\"wh-wrdauth-loggedin\", this.isLoggedIn()); //legacy! will be removed\n document.documentElement.classList.toggle(\"wh-wrdauth--isloggedin\", this.isLoggedIn());\n }\n}\n\nWRDAuthenticationProvider.getDefaultAuth = function()\n{\n return defaultauth;\n};\n\nif(window.$wh && window.$wh.WRDAuthenticationProvider)\n{\n console.log(\"Both designfiles wrd.auth and @mod-wrd/js/auth are loaded. @mod-wrd/js/auth will not activate\");\n}\nelse if(whintegration.config[\"wrd:auth\"])\n{\n defaultauth = new WRDAuthenticationProvider(whintegration.config[\"wrd:auth\"]);\n defaultauth.setupPage();\n\n dompack.register('.wh-wrdauth__logout, .whplugin-wrdauth-logout', node =>\n {\n node.whplugin_processed = true;\n node.addEventListener(\"click\", event =>\n {\n event.stopPropagation();\n event.preventDefault();\n defaultauth.logout();\n });\n });\n dompack.register('.wh-wrdauth__loginform, .whplugin-wrdauth-loginform', node =>\n {\n node.whplugin_processed = true;\n defaultauth.setupLoginForm(node);\n });\n\n if(defaultauth.userinfo)\n {\n dompack.register(\"*[data-wrdauth-text]\", node =>\n {\n var elname = node.dataset.wrdauthText;\n if(elname in defaultauth.userinfo)\n node.textContent = defaultauth.userinfo[elname];\n });\n dompack.register(\"*[data-wrdauth-value]\", node =>\n {\n var elname = node.dataset.wrdauthValue;\n if(elname in defaultauth.userinfo)\n node.value = defaultauth.userinfo[elname];\n });\n }\n}\n\nmodule.exports = WRDAuthenticationProvider;\n", "/** @import: import * as domfocus from 'dompack/browserfix/focus';\n*/\n\nexport function getActiveElement(doc)\n{\n try\n {\n //activeElement can reportedly throw on IE9 and _definately_ on IE11\n return doc.activeElement;\n }\n catch(e)\n {\n return null;\n }\n}\n\nexport function getToplevelWindow()\n{\n let toplevelwindow = window;\n while(toplevelwindow.frameElement)\n toplevelwindow = toplevelwindow.parent;\n return toplevelwindow;\n}\n/** Find the currently focused element\n @param limitwin If set, only return compontents in the specified document (prevents editable iframes from returning subframes) */\nexport function getCurrentlyFocusedElement(limitdoc)\n{\n try\n {\n var focused = getActiveElement(getToplevelWindow().document);\n while(true)\n {\n if (focused.tagName == \"IFRAME\" && (!limitdoc || focused.ownerDocument != limitdoc))\n focused = getActiveElement(focused.contentDocument);\n else\n break;\n }\n if(focused && limitdoc && focused.ownerDocument != limitdoc)\n return null;\n return focused;\n }\n catch(e)\n {\n return null;\n }\n}\n\nfunction isHTMLElement(node)\n{\n return node.nodeType == 1 && typeof node.className == \"string\";\n}\n\nfunction getIframeFocusableNodes(body, currentnode, recurseframes)\n{\n //ADDME force body into list?\n var subnodes = [];\n try\n {\n const body = (currentnode.contentDocument || currentnode.contentWindow.document).body;\n if (body.isContentEditable)\n return subnodes;\n\n subnodes = getFocusableComponents(body, recurseframes);\n }\n catch (e)\n {\n console.log(\"failed to descend into iframe\",e);\n }\n\n return subnodes;\n}\n\n// whether the node is reachable for focus by keyboard navigation\n// (because tabIndex == -1 will be seen a non(keyboard)focusable by this function)\nexport function canFocusTo(node) //returns if a -visible- node is focusable (this function does not check for visibility itself)\n{\n try\n {\n if(node.nodeType != 1)\n return false;\n if(node.contentEditable === \"true\")\n return true;\n\n // http://dev.w3.org/html5/spec-preview/editing.html#focusable\n if(node.tabIndex == -1) //explicitly disabled\n return false;\n\n return (parseInt(node.getAttribute('tabIndex')) >= 0) //we cannot read the property tabIndex directly, as IE <= 8 will return '0' even if the tabIndex is missing\n || ([\"A\",\"LINK\"].includes(node.nodeName) && node.href)\n || (!node.disabled && ([\"BUTTON\",\"SELECT\",\"TEXTAREA\",\"COMMAND\"].includes(node.nodeName)\n || (node.nodeName==\"INPUT\" && node.type != \"hidden\")));\n }\n catch(e)\n {\n return false; //the code above may fail eg on IE11 if it's a Flash object that'ss still loading\n }\n}\n\n\nexport function getFocusableComponents(startnode, recurseframes)\n{\n var focusable=[];\n for(var currentnode=startnode.firstChild;currentnode;currentnode=currentnode.nextSibling) //can't use element.getChildren, startnode may be document\n {\n if(!isHTMLElement(currentnode))\n {\n //if(currentnode.getStyle) console.log(\"getFocusableComponents skipping\",currentnode, $(currentnode).getStyle(\"display\"), currentnode.getStyle(\"visibility\"))\n continue;\n }\n\n // Get current style (avoid mootools due to cross-frame issues)\n var currentstyle = getComputedStyle(currentnode);\n if (!currentstyle || currentstyle.display == \"none\" || currentstyle.visibility == \"hidden\")\n {\n //if(currentnode.getStyle) console.log(\"getFocusableComponents skipping\",currentnode, $(currentnode).getStyle(\"display\"), currentnode.getStyle(\"visibility\"))\n continue;\n }\n\n if(recurseframes && currentnode.nodeName == \"IFRAME\") //might contain more things to focus\n {\n const subnodes = getIframeFocusableNodes(currentnode, currentnode, recurseframes);\n if(subnodes.length)\n focusable=focusable.concat(subnodes);\n }\n else if(canFocusTo(currentnode))\n {\n focusable.push(currentnode);\n }\n\n if (currentnode.isContentEditable)\n continue;\n\n const subnodes = getFocusableComponents(currentnode, recurseframes);\n if(subnodes.length)\n focusable = focusable.concat(subnodes);\n }\n return focusable;\n}\n\nexport function getAllFocusableComponents()\n{\n return getFocusableComponents(getToplevelWindow().document, true);\n}\n", "// WARNING: This file is loaded by both webpack (babel) and nodejs code and\n// should avoid babel-only features not yet supported by nodejs\n\n// import * as texttype from 'dompack/types/text';\n\nexport function encodeTextNode(str)\n{\n return str.split('&').join('&')\n .split('<').join('<')\n .split('>').join('>');\n}\n\nexport function encodeValue(str)\n{\n return str.split('&').join('&')\n .split('<').join('<')\n .split('>').join('>')\n .split('\"').join('"')\n .split(\"'\").join(''');\n}\n\nexport function decodeValue(str)\n{\n return str.replace(/
/g, \"\\n\")\n .replace(/&#(\\d+);/g, (match, dec) => String.fromCharCode(dec))\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/&/g, \"&\");\n}\n\nexport function encodeJSCompatibleJSON(s)\n{\n return JSON.stringify(s).replace(/\\u2028/g, '\\\\u2028').replace(/\\u2029/g, '\\\\u2029');\n}\n", "import * as encoding from \"dompack/types/text.es\";\nimport * as domdebug from \"dompack/src/debug.es\";\nimport * as wh from \"@mod-system/js/wh/integration.es\";\n\n/*\nSupported debug flags:\n gtd Debug get(Rich)Tid\n*/\n\nlet allTids = {};\nlet curLang = \"\";\n\nfunction encodeHTML(input)\n{\n return input.split('&').join('&')\n .split('<').join('<')\n .split('>').join('>')\n .split('\\n').join('
');\n}\n\nfunction executeCompiledTidText(text, params, rich)\n{\n if(typeof text == \"object\" && !Array.isArray(text))\n text = text?.[\"\"];\n if (text == null)\n return text;\n if(typeof text == \"string\")\n return rich ? encodeHTML(text) : text;\n\n let output = '';\n for(let tok of text)\n {\n if(typeof tok == \"string\")\n {\n output += rich ? encodeHTML(tok) : tok;\n }\n else if (typeof tok == \"number\")\n {\n if (tok >= 1)\n {\n let get_param = params?.[tok-1];\n if(get_param)\n {\n output += rich ? encodeHTML(get_param) : get_param;\n }\n }\n }\n else if(tok.t == \"tag\")\n {\n let sub = executeCompiledTidText(tok.subs, params, rich);\n output += rich ? `<${tok.tag}>${sub}` : sub;\n }\n else if(tok.t == \"ifparam\")\n {\n let get_param = params?.[tok.p-1] || '';\n output += executeCompiledTidText(get_param.toUpperCase() == tok.value.toUpperCase() ? tok.subs : tok.subselse, params, rich);\n }\n else if(tok.t == \"a\")\n {\n let sub = executeCompiledTidText(tok.subs, params, rich);\n if(rich)\n {\n let link = tok.link;\n if(tok.linkparam > 0 && tok.linkparam <= params.length)\n link = params[tok.linkparam - 1];\n if(link)\n output += `${sub}`;\n else\n output += sub;\n }\n else\n {\n output += sub;\n }\n }\n }\n return output;\n}\n\nfunction resolveTid(tid, params, options)\n{\n if(curLang=='debug')\n {\n return '{' + tid + (params.length ? '|' + params.join('|') : '') + '}';\n }\n\n // Make sure we have 4 string params\n for (let i = 0; i < 4; ++i)\n if (params.length == i)\n params.push(\"\");\n else if (typeof params[i] == \"number\")\n params[i] = \"\" + params[i];\n else if (!params[i])\n params[i] = \"\";\n params = params.slice(0, 4);\n\n // Initialize text with the 'cannot find text' message\n let text = domdebug.debugflags.sut ? \".\" + tid.split(\".\").pop() : \"(cannot find text:\" + tid + \")\";\n\n // Check if the module is defined\n let module = tid.substr(0, tid.indexOf(\":\"));\n if (!module || !(module in allTids))\n {\n if (!wh.config.islive || domdebug.debugflags.gtd)\n console.warn(\"No language texts found for module '\" + module + \"'\");\n return /*cannot find*/ text;\n }\n\n let language = options?.overridelanguage || getTidLanguage();\n if (!(language in allTids[module]))\n {\n if (!wh.config.islive || domdebug.debugflags.gtd)\n console.warn(\"No language texts found for language '\" + language + \"'\");\n return /*cannot find*/ text;\n }\n\n try\n {\n if (domdebug.debugflags.gtd)\n {\n console.group(`Resolving tid '${tid}'`);\n console.info(tid, params, options, language);\n }\n\n // Dig into the module gid structure\n let context = allTids[module][language];\n tid = tid.substr(module.length + 1);\n if (!tid.split(\".\").every(part =>\n {\n let found = part in context;\n if (found)\n context = context[part];\n else if (domdebug.debugflags.gtd)\n console.warn(\"Subpart '\"+ part + \"' not found\");\n\n return found; // If not found, break 'every' loop\n }))\n {\n return /*cannot find*/ text;\n }\n\n const executed = executeCompiledTidText(context, params, options?.html);\n if (executed == null)\n {\n if (domdebug.debugflags.gtd)\n console.warn(`Tid '${module}:${tid}'' is a group node`);\n return /*cannot find*/ text;\n }\n if (domdebug.debugflags.gtd)\n console.info(\"getTid\", `${module}:${tid}`, params, executed);\n\n return executed;\n }\n finally\n {\n if (domdebug.debugflags.gtd)\n console.groupEnd();\n }\n}\n\nfunction getTid(tid, p1, p2, p3, p4)\n{\n return resolveTid(tid, Array.prototype.slice.call(arguments, 1));\n}\n\nfunction getHTMLTid(tid, p1, p2, p3, p4)\n{\n return resolveTid(tid, Array.prototype.slice.call(arguments, 1), { html: true });\n}\n\nfunction getTidLanguage()\n{\n if (curLang)\n return curLang;\n\n // Read the document's language, if there is a DOM context\n if (typeof document != \"undefined\")\n curLang = (document.documentElement.lang||'').substr(0,2);\n\n return curLang;\n}\n\nfunction setTidLanguage(lang)\n{\n curLang = lang;\n}\n\nfunction tidMerge(readContext, writeContext)\n{\n for (let key of Object.keys(readContext))\n {\n if (typeof readContext[key] != \"object\" || Array.isArray(readContext[key])) //a leaf, safe to copy\n {\n writeContext[key] = readContext[key];\n }\n else\n {\n if (!(key in writeContext))\n writeContext[key] = {};\n tidMerge(readContext[key], writeContext[key]);\n }\n }\n}\n\nfunction registerTexts(module, language, tids)\n{\n if (!(module in allTids))\n {\n allTids[module] = {};\n }\n if (!(language in allTids[module]))\n {\n allTids[module][language] = tids;\n return;\n }\n tidMerge(tids, allTids[module][language]);\n}\n\n// Fill nodes with a data-texttid attribute with the translated text\nfunction convertElementTids(scope = document.body)\n{\n // Only available in a DOM context and if the DOM is ready\n if (typeof document == \"undefined\" || !scope)\n return;\n Array.from(scope.querySelectorAll(\"*[data-texttid]\")).forEach(function(node)\n {\n node.textContent = getTid(node.getAttribute(\"data-texttid\"));\n });\n}\n\n// If this script is run within a DOM context, convert data-texttid attributes automatically\nif (typeof document != \"undefined\")\n document.addEventListener(\"DOMContentLoaded\", event => convertElementTids());\n\n\n// Define 'tidLanguage' as a property on the main export (so you can use getTid.tidLanguage)\nObject.defineProperty(getTid, \"tidLanguage\", { get: getTidLanguage, set: setTidLanguage });\n// Define 'html' as a method on the main export (so you can use getTid.html)\ngetTid.html = getHTMLTid;\n\n// Export getTid as the default function, explicitly export getTid, getHTMLTid and registerTexts as well\nexport { getTid as default\n , getTid\n , getTidLanguage\n , getHTMLTid\n , convertElementTids\n , registerTexts\n };\n", "// Auto-generated language file from /opt/wh/whtree/modules/tollium/web/ui/common.lang.json\nvar registerTexts = require(\"@mod-tollium/js/gettid\").registerTexts;\nregisterTexts(\"tollium\",\"en\",{\"common\":{\"actions\":{\"about\":\"About\",\"about_app\":[\"About \",1],\"add\":\"Add\",\"addasportlet\":\"Add as Portlet\",\"addtostartmenu\":\"Show in Favorites\",\"apply\":\"Apply\",\"browse\":\"Browse\",\"cancel\":\"Cancel\",\"change\":\"Change\",\"clear\":\"Clear\",\"close\":\"Close\",\"columns\":\"Select Columns\",\"connect\":\"Connect\",\"copy\":\"Copy\",\"copyto\":\"Copy To\",\"cut\":\"Cut\",\"delete\":\"Delete\",\"disable\":\"Disable\",\"disconnect\":\"Disconnect\",\"down\":\"Down\",\"download\":\"Download\",\"duplicate\":\"Duplicate\",\"edit\":\"Edit\",\"enable\":\"Enable\",\"exit\":\"Exit\",\"export\":\"Export\",\"finish\":\"Finish\",\"help\":\"Help\",\"import\":\"Import\",\"inspect\":\"Inspect\",\"invertselection\":\"Invert Selection\",\"link\":\"Link to\",\"logoff\":\"Logoff\",\"menu\":\"Menu\",\"moveto\":\"Move To\",\"new\":\"New\",\"newinstance\":\"New Instance\",\"next\":\"Next\",\"no\":\"No\",\"ok\":\"OK\",\"open\":\"Open\",\"ordering\":\"Set ordering\",\"paste\":\"Paste\",\"preferences\":\"Preferences\",\"previous\":\"Previous\",\"print\":\"Print\",\"properties\":\"Properties\",\"redo\":\"Redo\",\"refresh\":\"Refresh\",\"rename\":\"Rename\",\"replace\":\"Replace\",\"reset\":\"Reset\",\"save\":\"Save\",\"saveas\":\"Save As\",\"search\":\"Search\",\"select\":\"Select\",\"selectall\":\"Select All\",\"settings\":\"Settings\",\"startmenuhint\":\"Open the Start Menu\",\"synchronize\":\"Synchronize\",\"tobottom\":\"To bottom\",\"today\":\"Today\",\"totop\":\"To top\",\"undo\":\"Undo\",\"up\":\"Up\",\"upload\":\"Upload\",\"view\":\"View\",\"yes\":\"Yes\",\"yestoall\":\"Yes to All\"},\"errors\":{\"databasereadonly\":\"The database is currently read-only and changes cannot be made. Please try again later.\",\"field_required\":[\"Field '\",1,\"' is a required field\"],\"image_toosmall\":[\"Field '\",1,\"' must contain an image of at least \",2,\" - it is \",3,\".\"],\"invalid_date\":[\"Field '\",1,\"' contains an invalid date\"],\"invalid_float\":[\"Field '\",1,\"' must contain a valid value\"],\"invalid_hostname\":[\"Field '\",1,\"' must contain a hostname (eg 'myserver.example.net')\"],\"invalid_integer\":[\"Field '\",1,\"' must contain a valid numeric value\"],\"invalid_money\":[\"Field '\",1,\"' must contain a valid value\"],\"invalid_time\":[\"Field '\",1,\"' contains an invalid time\"],\"invalid_year\":[\"Field '\",1,\"' contains an invalid year; you need to enter at least 4 digits (prepend with zeroes for years before the year 1000, eg. 0954)\"],\"passwordmismatch\":\"The entered passwords do not match\",\"tagedit_unprocessed_text\":[\"Field '\",1,\"' contains a tag that has not yet been committed\"],\"too_long\":[\"Field '\",1,\"' may not contain more than \",2,\" characters\"],\"too_many_decimals\":[\"Field '\",1,\"' may not contain \",{\"p\":2,\"subs\":[\"any decimals\"],\"subselse\":[\"more than \",2,\" \",{\"p\":2,\"subs\":[\"decimal\"],\"subselse\":[\"decimals\"],\"t\":\"ifparam\",\"value\":\"1\"}],\"t\":\"ifparam\",\"value\":\"0\"}],\"too_short\":[\"Field '\",1,\"' must contain \",2,\" or more characters\"],\"upload_allowsuspiciousfile\":\"Do you want to continue this upload?\",\"upload_blockedsuspiciousfile\":\"Contact the system operator if you still need to upload this file.\",\"upload_cannotcheck\":\"We are currently unable to verify this file.\",\"upload_suspicious\":[\"There appears to be an issue with this file (\",1,\") and it cannot be uploaded.\"],\"value_out_of_range\":[\"Field '\",1,\"' contains an invalid value; you need to enter a value \",{\"p\":2,\"subs\":[\"of at most \",3],\"subselse\":[{\"p\":3,\"subs\":[\"of at least \",2],\"subselse\":[\"between \",2,\" and \",3],\"t\":\"ifparam\",\"value\":\"\"}],\"t\":\"ifparam\",\"value\":\"\"}]},\"examples\":{\"email\":\"email@example.com\",\"emailcomma\":\"email@example.com, another@example.net, ...\",\"http-url\":\"http://www.example.com/folder/\",\"https-url\":\"https://www.example.com/folder/\",\"urls\":[\"https://www.example.com/folder/\",\"\\n\",\"http://www.anotherexample.com/\"]},\"labels\":{\"bksp\":\"Backspace\",\"creationdate\":\"Creation date\",\"del\":\"Del\",\"deletiondate\":\"Deletion date\",\"description\":\"Description\",\"down\":\"Down\",\"email\":\"Email\",\"end\":\"End\",\"enter\":\"Enter\",\"esc\":\"Escape\",\"home\":\"Home\",\"id\":\"ID\",\"keywords\":\"Keywords\",\"left\":\"Left\",\"modificationdate\":\"Modification date\",\"name\":\"Name\",\"no\":\"No\",\"none\":\"None\",\"ordering\":\"Ordering\",\"pgdn\":\"Page Down\",\"pgup\":\"Page Up\",\"right\":\"Right\",\"status\":\"Status\",\"subject\":\"Subject\",\"tab\":\"Tab\",\"tag\":\"Tag\",\"title\":\"Title\",\"type\":\"Type\",\"up\":\"Up\",\"yes\":\"Yes\"},\"languages\":{\"aa\":\"Afar\",\"ab\":\"Abkhaz\",\"ae\":\"Avestan\",\"af\":\"Afrikaans\",\"ak\":\"Akan\",\"am\":\"Amharic\",\"an\":\"Aragonese\",\"ar\":\"Arabic\",\"as\":\"Assamese\",\"av\":\"Avaric\",\"ay\":\"Aymara\",\"az\":\"Azerbaijani\",\"ba\":\"Bashkir\",\"be\":\"Belarusian\",\"bg\":\"Bulgarian\",\"bh\":\"Bihari\",\"bi\":\"Bislama\",\"bm\":\"Bambara\",\"bn\":\"Bengali\",\"bo\":\"Tibetan\",\"br\":\"Breton\",\"bs\":\"Bosnian\",\"ca\":\"Catalan\",\"ce\":\"Chechen\",\"ch\":\"Chamorro\",\"co\":\"Corsican\",\"cr\":\"Cree\",\"cs\":\"Czech\",\"cu\":\"Old Church Slavonic\",\"cv\":\"Chuvash\",\"cy\":\"Welsh\",\"da\":\"Danish\",\"de\":\"German\",\"dv\":\"Divehi\",\"dz\":\"Dzongkha\",\"ee\":\"Ewe\",\"el\":\"Greek\",\"en\":\"English\",\"eo\":\"Esperanto\",\"es\":\"Spanish\",\"et\":\"Estonian\",\"eu\":\"Basque\",\"fa\":\"Persian\",\"ff\":\"Fula\",\"fi\":\"Finnish\",\"fj\":\"Fijian\",\"fo\":\"Faroese\",\"fr\":\"French\",\"fy\":\"Western Frisian\",\"ga\":\"Irish\",\"gd\":\"Gaelic\",\"gl\":\"Galician\",\"gn\":\"Guaran\u00ED\",\"gu\":\"Gujarati\",\"gv\":\"Manx\",\"ha\":\"Hausa\",\"he\":\"Hebrew\",\"hi\":\"Hindi\",\"ho\":\"Hiri Motu\",\"hr\":\"Croatian\",\"ht\":\"Haitian\",\"hu\":\"Hungarian\",\"hy\":\"Armenian\",\"hz\":\"Herero\",\"ia\":\"Interlingua\",\"id\":\"Indonesian\",\"ie\":\"Interlingue\",\"ig\":\"Igbo\",\"ii\":\"Nuosu\",\"ik\":\"Inupiaq\",\"io\":\"Ido\",\"is\":\"Icelandic\",\"it\":\"Italian\",\"iu\":\"Inuktitut\",\"ja\":\"Japanese\",\"jv\":\"Javanese\",\"ka\":\"Georgian\",\"kg\":\"Kongo\",\"ki\":\"Kikuyu\",\"kj\":\"Kwanyama\",\"kk\":\"Kazakh\",\"kl\":\"Kalaallisut\",\"km\":\"Khmer\",\"kn\":\"Kannada\",\"ko\":\"Korean\",\"kr\":\"Kanuri\",\"ks\":\"Kashmiri\",\"ku\":\"Kurdish\",\"kv\":\"Komi\",\"kw\":\"Cornish\",\"ky\":\"Kyrgyz\",\"la\":\"Latin\",\"lb\":\"Luxembourgish\",\"lg\":\"Ganda\",\"li\":\"Limburgish\",\"ln\":\"Lingala\",\"lo\":\"Lao\",\"lt\":\"Lithuanian\",\"lu\":\"Luba-Katanga\",\"lv\":\"Latvian\",\"mg\":\"Malagasy\",\"mh\":\"Marshallese\",\"mi\":\"Maori\",\"mk\":\"Macedonian\",\"ml\":\"Malayalam\",\"mn\":\"Mongolian\",\"mo\":\"Moldovan\",\"mr\":\"Marathi\",\"ms\":\"Malay\",\"mt\":\"Maltese\",\"my\":\"Burmese\",\"na\":\"Nauru\",\"nb\":\"Norwegian Bokmal\",\"nd\":\"North Ndebele\",\"ne\":\"Nepali\",\"ng\":\"Ndonga\",\"nl\":\"Dutch\",\"nn\":\"Norwegian Nynorsk\",\"no\":\"Norwegian\",\"nr\":\"South Ndebele\",\"nv\":\"Navajo\",\"ny\":\"Chichewa\",\"oc\":\"Occitan\",\"oj\":\"Ojibwe\",\"om\":\"Oromo\",\"or\":\"Oriya\",\"os\":\"Ossetian\",\"pa\":\"Panjabi\",\"pi\":\"Pali\",\"pl\":\"Polish\",\"ps\":\"Pashto, Pushto\",\"pt\":\"Portuguese\",\"qu\":\"Quechua\",\"rm\":\"Romansh\",\"rn\":\"Kirundi\",\"ro\":\"Romanian\",\"ru\":\"Russian\",\"rw\":\"Kinyarwanda\",\"sa\":\"Sanskrit\",\"sc\":\"Sardinian\",\"sd\":\"Sindhi\",\"se\":\"Northern Sami\",\"sg\":\"Sango\",\"sh\":\"Serbo-Croatian\",\"si\":\"Sinhala\",\"sk\":\"Slovak\",\"sl\":\"Slovene\",\"sm\":\"Samoan\",\"sn\":\"Shona\",\"so\":\"Somali\",\"sq\":\"Albanian\",\"sr\":\"Serbian\",\"ss\":\"Swati\",\"st\":\"Southern Sotho\",\"su\":\"Sundanese\",\"sv\":\"Swedish\",\"sw\":\"Swahili\",\"ta\":\"Tamil\",\"te\":\"Telugu\",\"tg\":\"Tajik\",\"th\":\"Thai\",\"ti\":\"Tigrinya\",\"tk\":\"Turkmen\",\"tl\":\"Tagalog\",\"tn\":\"Tswana\",\"to\":\"Tonga\",\"tr\":\"Turkish\",\"ts\":\"Tsonga\",\"tt\":\"Tatar\",\"tw\":\"Twi\",\"ty\":\"Tahitian\",\"ug\":\"Uyghur\",\"uk\":\"Ukrainian\",\"ur\":\"Urdu\",\"uz\":\"Uzbek\",\"ve\":\"Venda\",\"vi\":\"Vietnamese\",\"vo\":\"Volap\u00FCk\",\"wa\":\"Walloon\",\"wo\":\"Wolof\",\"xh\":\"Xhosa\",\"yi\":\"Yiddish\",\"yo\":\"Yoruba\",\"za\":\"Zhuang\",\"zh\":\"Chinese\",\"zu\":\"Zulu\"},\"menus\":{\"edit\":\"Edit\",\"file\":\"File\",\"help\":\"Help\",\"new\":\"New\",\"tools\":\"Tools\",\"view\":\"View\"},\"messages\":{\"dontaskagain\":\"Remember my choice\",\"dontshowagain\":\"Do not show me this message again\",\"searchnoresults\":[\"No results were found when searching for '\",1,\"'.\"]},\"richstyles\":{\"blocktext\":\"Block Text\",\"bodytext\":\"Body Text\",\"bodytext2\":\"Body Text 2\",\"bodytext3\":\"Body Text 3\",\"bodytextfirstindent\":\"Body Text First Indent\",\"bodytextfirstindent2\":\"Body Text First Indent 2\",\"bodytextindent\":\"Body Text Indent\",\"bodytextindent2\":\"Body Text Indent 2\",\"bodytextindent3\":\"Body Text Indent 3\",\"caption\":\"Caption\",\"closing\":\"Closing\",\"commentreference\":\"Comment Reference\",\"commenttext\":\"Comment Text\",\"date\":\"Date\",\"defaultparagraphfont\":\"Default Paragraph Font\",\"documentmap\":\"Document Map\",\"emphasis\":\"Emphasis\",\"endnotereference\":\"Endnote Reference\",\"endnotetext\":\"Endnote Text\",\"envelopeaddress\":\"Envelope Address\",\"envelopereturn\":\"Envelope Return\",\"followedhyperlink\":\"Followed Hyperlink\",\"footer\":\"Footer\",\"footnotereference\":\"Footnote Reference\",\"footnotetext\":\"Footnote Text\",\"header\":\"Header\",\"heading1\":\"Heading 1\",\"heading2\":\"Heading 2\",\"heading3\":\"Heading 3\",\"heading4\":\"Heading 4\",\"heading5\":\"Heading 5\",\"heading6\":\"Heading 6\",\"heading7\":\"Heading 7\",\"heading8\":\"Heading 8\",\"heading9\":\"Heading 9\",\"hyperlink\":\"Hyperlink\",\"index1\":\"Index 1\",\"index2\":\"Index 2\",\"index3\":\"Index 3\",\"index4\":\"Index 4\",\"index5\":\"Index 5\",\"index6\":\"Index 6\",\"index7\":\"Index 7\",\"index8\":\"Index 8\",\"index9\":\"Index 9\",\"indexheading\":\"Index Heading\",\"linenumber\":\"Line Number\",\"list\":\"List\",\"list2\":\"List 2\",\"list3\":\"List 3\",\"list4\":\"List 4\",\"list5\":\"List 5\",\"listbullet\":\"List Bullet\",\"listbullet2\":\"List Bullet 2\",\"listbullet3\":\"List Bullet 3\",\"listbullet4\":\"List Bullet 4\",\"listbullet5\":\"List Bullet 5\",\"listcontinue\":\"List Continue\",\"listcontinue2\":\"List Continue 2\",\"listcontinue3\":\"List Continue 3\",\"listcontinue4\":\"List Continue 4\",\"listcontinue5\":\"List Continue 5\",\"listnumber\":\"List Number\",\"listnumber2\":\"List Number 2\",\"listnumber3\":\"List Number 3\",\"listnumber4\":\"List Number 4\",\"listnumber5\":\"List Number 5\",\"macrotext\":\"Macro Text\",\"messageheader\":\"Message Header\",\"normal\":\"Normal\",\"normalindent\":\"Normal Indent\",\"noteheadiong\":\"Note Heading\",\"ordered\":\"Ordered list\",\"pagenumber\":\"Page Number\",\"plaintext\":\"Plain Text\",\"salutation\":\"Salutation\",\"signature\":\"Signature\",\"strong\":\"Strong\",\"subtitle\":\"Subtitle\",\"tableofauthorities\":\"Table of Authorities\",\"tableoffigures\":\"Table of Figures\",\"title\":\"Title\",\"toaheading\":\"TOA Heading\",\"toc1\":\"TOC 1\",\"toc2\":\"TOC 2\",\"toc3\":\"TOC 3\",\"toc4\":\"TOC 4\",\"toc5\":\"TOC 5\",\"toc6\":\"TOC 6\",\"toc7\":\"TOC 7\",\"toc8\":\"TOC 8\",\"toc9\":\"TOC 9\",\"unordered\":\"Unordered list\"},\"units\":{\"centuries\":\"centuries\",\"century\":\"century\",\"day\":\"day\",\"days\":\"days\",\"hour\":\"hour\",\"hours\":\"hours\",\"millisecond\":\"millisecond\",\"milliseconds\":\"milliseconds\",\"minute\":\"minute\",\"minutes\":\"minutes\",\"month\":\"month\",\"months\":\"months\",\"second\":\"second\",\"seconds\":\"seconds\",\"week\":\"week\",\"weeks\":\"weeks\",\"year\":\"year\",\"years\":\"years\"}},\"shell\":{\"checks\":{\"errors\":[{\"p\":1,\"subs\":[2],\"subselse\":[\"'\",2,\"' and \",1,\" more \",{\"p\":1,\"subs\":[\"error\"],\"subselse\":[\"errors\"],\"t\":\"ifparam\",\"value\":\"1\"}],\"t\":\"ifparam\",\"value\":\"0\"}],\"unresolvedissues\":\"There are unresolved issues!\"},\"controller\":{\"neededrights\":[\"The following rights are needed: \",1],\"rightsproblem\":[\"You don't have access to this application (logged in as '\",1,\"')\"],\"sessionexpired\":\"Your login session has expired. Please log in again to restart this application.\",\"untrustedparams\":\"You are not allowed to open this application with additional URL parameters.\"},\"dashboard\":{\"apptitle\":\"WebHare\",\"connect-success\":\"Received new WebHare connect-helper settings\",\"connect-title\":\"WebHare connect\",\"logout\":\"Log out\",\"mountwebhare\":\"Mount server over WebDav\",\"noapps\":[\"No match for '\",1,\"'\"],\"resetimagecache\":\"Reset image cache\",\"showallapps\":\"Show all apps\"},\"editfavorites\":\"Manage\",\"errors\":{\"appstartfailed\":\"Unable to start the application. The server may be unreachable or you may need to login again\",\"appstartfailed-development\":[\"Unable to start the application. The server may be unreachable or you may need to login again\",\"\\n\",\"\\n\",\"You may also need to softreset WebHare (wh softreset) or inspect the log files for issues (wh watchlog)\"],\"debug\":\"Debug\",\"encounterederror\":\"The application encountered an error and has terminated.\",\"errordialogtitle\":\"Application\",\"errortitle\":\"An error took place, please contact the system operator.\",\"restart\":\"Restart\"},\"feedback\":{\"button-general\":\"General\",\"button-specific\":\"Specific\",\"message\":\"Do you want to submit feedback about a specific interface element or general feedback?\",\"title\":\"Feedback\"},\"frontendclose\":\"Application sessions have expired\",\"frontendclose_description\":\"The connection between your browser and WebHare was broken. The impacted applications have been closed.\",\"loadingapp\":\"Starting application...\",\"login\":{\"apptitle\":\"WebHare\",\"cancel\":\"Cancel\",\"closedlogin\":\"It's currently not possible to login. Please try again later.\",\"disabledlogin\":\"Login failed: this user account is disabled\",\"enterauthenticatorcode\":\"Please enter your one-time code\",\"enterbackupcode\":\"Too many incorrect codes entered, please enter a backup code\",\"enterusernameandpassword\":\"Please enter your username and password.\",\"forgotpassword\":\"Forgot password\",\"genericerror\":\"An error occurred. Please try again, if the error keeps occurring please contact your system administrator.\",\"infotitle\":\"Important information\",\"invaliddata\":\"Your login session has expired, please login again\",\"invalidlogin\":\"Invalid username and/or password.\",\"loginbutton\":\"Login\",\"loginidentityservices\":\"Login using identity services\",\"loginsecondfactor\":\"Second factor login\",\"logintitle\":\"Login\",\"loginwithwebhareaccount\":\"Login with a WebHare account\",\"musteditauthsettings\":\"Your authentication settings must be updated before proceeding.\",\"notloggedin\":\"You no longer seem to be logged in. You may need to reload this webpage to relogin.\",\"nowebhareaccount\":\"No corresponding WebHare account found. Please contact your system administrator.\",\"password\":\"Password\",\"savelogin\":\"Remember me\",\"secondfactorauthentication\":\"Second factor login\",\"totpattemptsleft\":[\"Please enter your one-time code (you have \",1,\" attempts left)\"],\"totpcode\":\"One-time login code\",\"totpinvalidcode\":\"This code is not valid\",\"totplocked\":\"You have entered too many invalid codes, you will have to enter a backup code to login in\",\"totpreusedcode\":\"This code has already been used to log in, please wait until the next code before trying again\",\"unexpectedprotocolversion\":[\"It looks like your browser is running an outdated version of this webpage. Please try Shift-F5 (or Shift-Cmd-R on a Mac or Alt-Cmd-R in Safari) to reload this page.\",\"\\n\",\"\\n\",\"If that doesn't work please try restarting your browser.\"],\"username\":\"Username\"},\"logout\":{\"surelogout\":\"Are you sure you want to logout?\",\"title\":\"Log out\"},\"messagebox\":{\"defaulttitle\":\"Message\"},\"nowebhareconnect\":\"WebHare connect unavailable\",\"nowebhareconnect_description\":[\"You have enabled WebHare connect for this server, but the service could not be contacted on \",1],\"oauth\":{\"apptitle\":\"OAuth authorization\",\"clientid\":\"Client\",\"errortitle\":\"An error has occurred\",\"explanation\":\"The following server wants to get access to this server:\",\"messages\":{\"invalid_redirect\":\"An invalid redirect URL has been specified (must lie within the client URL.)\",\"missing_client\":\"No client has been specified.\",\"missing_redirect\":\"No redirect URL has been specified.\",\"missing_scopes\":\"No scopes have been specified.\",\"unknownclient\":[\"Client '\",1,\"' has not been registered in Webhare Connect.\"],\"unknownerror\":\"An unknown error has occurred. Please try again, and if this error keeps occurring please contact the system administrator.\",\"webhareconnecterror\":\"Could not connect to Webhare Connect. Please contact your system administrator if this problem persists.\"},\"oauthtitle\":\"Authorization\",\"question\":\"The requesting server will have access to everything your account has access to on this server. Are you sure you want to grant this access?\",\"scopes\":\"Scopes\"},\"offline\":\"Connection lost\",\"offline_description\":\"The connection to the server has been lost. Please wait for it to recover.\",\"openedas\":[\"Opened as '\",1,\"'\"],\"personalmenu\":\"Favorites\",\"restartapp\":\"Restart application\",\"towl\":{\"gonativedescription\":\"Click here to show notifications on your desktop.\",\"gonativetitle\":\"Desktop notifications\",\"notificationtitle\":\"WebHare notification\"},\"upload\":{\"messages\":{\"errortitle\":\"An error occurred\",\"unknownerror\":\"Something went wrong during uploading. Please try again.\"},\"progress\":{\"calculating\":\"Calculating...\",\"progress\":\"Progress\",\"size\":\"Size\",\"speed\":\"Speed\",\"title\":\"Uploading\"}},\"webhareupdated\":\"WebHare updated\",\"webhareupdated_description\":\"The webpage restarted because the WebHare server received an update\"}});\nregisterTexts(\"tollium\",\"nl\",{\"common\":{\"actions\":{\"about\":\"Over\",\"about_app\":[\"Over \",1],\"add\":\"Toevoegen\",\"addasportlet\":\"Toon als portlet\",\"addtostartmenu\":\"Toon in favorieten\",\"apply\":\"Toepassen\",\"browse\":\"Bladeren\",\"cancel\":\"Annuleren\",\"change\":\"Aanpassen\",\"clear\":\"Leegmaken\",\"close\":\"Sluiten\",\"columns\":\"Kolommen instellen\",\"connect\":\"Verbinden\",\"copy\":\"Kopi\u00EBren\",\"copyto\":\"Kopi\u00EBren naar\",\"cut\":\"Knippen\",\"delete\":\"Verwijderen\",\"disable\":\"Uitschakelen\",\"disconnect\":\"Verbreken\",\"down\":\"Omlaag\",\"download\":\"Downloaden\",\"duplicate\":\"Dupliceren\",\"edit\":\"Bewerken\",\"enable\":\"Inschakelen\",\"exit\":\"Afsluiten\",\"export\":\"Exporteren\",\"finish\":\"Voltooien\",\"help\":\"Help\",\"import\":\"Importeren\",\"inspect\":\"Inspecteren\",\"invertselection\":\"Selectie omkeren\",\"link\":\"Koppel aan\",\"logoff\":\"Uitloggen\",\"menu\":\"Menu\",\"moveto\":\"Verplaatsen naar\",\"new\":\"Nieuw\",\"newinstance\":\"Nieuwe instantie\",\"next\":\"Volgende\",\"no\":\"Nee\",\"ok\":\"OK\",\"open\":\"Openen\",\"ordering\":\"Volgorde instellen\",\"paste\":\"Plakken\",\"preferences\":\"Voorkeuren\",\"previous\":\"Vorige\",\"print\":\"Afdrukken\",\"properties\":\"Eigenschappen\",\"redo\":\"Opnieuw\",\"refresh\":\"Verversen\",\"rename\":\"Hernoemen\",\"replace\":\"Vervangen\",\"reset\":\"Herstellen\",\"save\":\"Opslaan\",\"saveas\":\"Opslaan als\",\"search\":\"Zoeken\",\"select\":\"Selecteren\",\"selectall\":\"Alles selecteren\",\"settings\":\"Instellingen\",\"startmenuhint\":\"Open het Start menu\",\"synchronize\":\"Synchroniseren\",\"tobottom\":\"Onderaan\",\"today\":\"Vandaag\",\"totop\":\"Bovenaan\",\"undo\":\"Ongedaan maken\",\"up\":\"Omhoog\",\"upload\":\"Uploaden\",\"view\":\"Bekijken\",\"yes\":\"Ja\",\"yestoall\":\"Ja op alles\"},\"errors\":{\"databasereadonly\":\"De database is in alleen-lezen mode en daardoor kunnen er nu geen wijzigingen worden gemaakt. Probeer het later nog eens.\",\"field_required\":[\"Het veld '\",1,\"' dient ingevuld te worden\"],\"image_toosmall\":[\"Het veld '\",1,\"' dient een afbeelding te bevatten van minimaal \",2,\" - is nu \",3,\".\"],\"invalid_date\":[\"Het veld '\",1,\"' bevat een ongeldige datum\"],\"invalid_float\":[\"Het veld '\",1,\"' dient een geldige waarde te bevatten\"],\"invalid_hostname\":[\"Het veld '\",1,\"' dient een hostname te bevatten (bv 'myserver.example.net')\"],\"invalid_integer\":[\"Het veld '\",1,\"' dient een geldige numerieke waarde te bevatten\"],\"invalid_money\":[\"Het veld '\",1,\"' dient een geldige waarde te bevatten\"],\"invalid_time\":[\"Het veld '\",1,\"' bevat een ongeldig tijdstip\"],\"invalid_year\":[\"Het veld '\",1,\"' bevat een ongeldig jaar; u dient ten minste 4 cijfers in te vullen (voeg nullen aan het begin van het getal toe voor jaren voor het jaar 1000, bijvoorbeeld 0954)\"],\"passwordmismatch\":\"De ingevoerde wachtwoorden komen niet overeen\",\"tagedit_unprocessed_text\":[\"Het veld '\",1,\"' bevat een tag die nog moet worden opgeslagen\"],\"too_long\":[\"Het veld '\",1,\"' mag niet meer dan \",2,\" karakters bevatten\"],\"too_many_decimals\":[\"Het veld '\",1,\"' mag \",{\"p\":2,\"subs\":[\"geen decimalen\"],\"subselse\":[\"niet meer dan \",2,\" \",{\"p\":2,\"subs\":[\"decimaal\"],\"subselse\":[\"decimalen\"],\"t\":\"ifparam\",\"value\":\"1\"}],\"t\":\"ifparam\",\"value\":\"0\"},\" bevatten\"],\"too_short\":[\"Het veld '\",1,\"' moet \",2,\" of meer karakters bevatten\"],\"upload_allowsuspiciousfile\":\"Wil u deze upload toch toestaan?\",\"upload_blockedsuspiciousfile\":\"Neem contact op met uw systeembeheerder als u dit bestand toch moet uploaden.\",\"upload_cannotcheck\":\"Dit bestand kan momenteel niet gecontroleerd worden.\",\"upload_suspicious\":[\"Er lijkt een probleem met dit bestand te zijn (\",1,\") en het kan daardoor niet geupload worden\"],\"value_out_of_range\":[\"Het veld '\",1,\"' bevat een ongeldige waarde; u dient een waarde \",{\"p\":2,\"subs\":[\"van maximaal \",3],\"subselse\":[{\"p\":3,\"subs\":[\"van ten minste \",2],\"subselse\":[\"tussen \",2,\" en \",3],\"t\":\"ifparam\",\"value\":\"\"}],\"t\":\"ifparam\",\"value\":\"\"},\" in te vullen\"]},\"examples\":{\"email\":\"email@voorbeeld.nl\",\"emailcomma\":\"email@example.nl, anders@example.nl, ...\",\"http-url\":\"http://www.example.nl/map/\",\"https-url\":\"https://www.example.nl/map/\",\"urls\":[\"https://www.example.nl/map/\",\"\\n\",\"http://www.anotherexample.com/\"]},\"labels\":{\"bksp\":\"Backspace\",\"creationdate\":\"Aanmaakdatum\",\"del\":\"Del\",\"deletiondate\":\"Verwijderdatum\",\"description\":\"Omschrijving\",\"down\":\"Omlaag\",\"email\":\"E-mailadres\",\"end\":\"End\",\"enter\":\"Enter\",\"esc\":\"Escape\",\"home\":\"Home\",\"id\":\"ID\",\"keywords\":\"Sleutelwoorden\",\"left\":\"Links\",\"modificationdate\":\"Wijzigingsdatum\",\"name\":\"Naam\",\"no\":\"Nee\",\"none\":\"Geen\",\"ordering\":\"Ordening\",\"pgdn\":\"Page Down\",\"pgup\":\"Page Up\",\"right\":\"Rechts\",\"status\":\"Status\",\"subject\":\"Onderwerp\",\"tab\":\"Tab\",\"tag\":\"Tag\",\"title\":\"Titel\",\"type\":\"Type\",\"up\":\"Omhoog\",\"yes\":\"Ja\"},\"languages\":{\"aa\":\"Afar\",\"ab\":\"Abchazisch\",\"ae\":\"Avestisch\",\"af\":\"Afrikaans\",\"ak\":\"Akan\",\"am\":\"Amhaars\",\"an\":\"Aragonees\",\"ar\":\"Arabisch\",\"as\":\"Assamees\",\"av\":\"Avaars\",\"ay\":\"Aymara\",\"az\":\"Azerbeidzjaans\",\"ba\":\"Basjkiers\",\"be\":\"Wit-Russisch\",\"bg\":\"Bulgaars\",\"bh\":\"Bihari\",\"bi\":\"Bislama\",\"bm\":\"Bambara\",\"bn\":\"Bengaals\",\"bo\":\"Tibetaans\",\"br\":\"Bretons\",\"bs\":\"Bosnisch\",\"ca\":\"Catalaans\",\"ce\":\"Tsjetsjeens\",\"ch\":\"Chamorro\",\"co\":\"Corsicaans\",\"cr\":\"Cree\",\"cs\":\"Tsjechisch\",\"cu\":\"Kerkslavisch\",\"cv\":\"Tsjoevasjisch\",\"cy\":\"Welsh\",\"da\":\"Deens\",\"de\":\"Duits\",\"dv\":\"Divehi\",\"dz\":\"Dzongkha\",\"ee\":\"Ewe\",\"el\":\"Grieks\",\"en\":\"Engels\",\"eo\":\"Esperanto\",\"es\":\"Spaans\",\"et\":\"Estisch\",\"eu\":\"Baskisch\",\"fa\":\"Perzisch\",\"ff\":\"Fula\",\"fi\":\"Fins\",\"fj\":\"Fijisch\",\"fo\":\"Faer\u00F6ers\",\"fr\":\"Frans\",\"fy\":\"Fries\",\"ga\":\"Iers-Gaelisch\",\"gd\":\"Schots-Gaelisch\",\"gl\":\"Galicisch\",\"gn\":\"Guaran\u00ED\",\"gu\":\"Gujarati\",\"gv\":\"Manx-Gaelisch\",\"ha\":\"Hausa\",\"he\":\"Hebreeuws\",\"hi\":\"Hindi\",\"ho\":\"Hiri Motu\",\"hr\":\"Kroatisch\",\"ht\":\"Krey\u00F2l\",\"hu\":\"Hongaars\",\"hy\":\"Armeens\",\"hz\":\"Herero\",\"ia\":\"Interlingua\",\"id\":\"Indonesisch\",\"ie\":\"Interlingue\",\"ig\":\"Igbo\",\"ii\":\"Yi\",\"ik\":\"Inupiak\",\"io\":\"Ido\",\"is\":\"IJslands\",\"it\":\"Italiaans\",\"iu\":\"Inuktitut\",\"ja\":\"Japans\",\"jv\":\"Javaans\",\"ka\":\"Georgisch\",\"kg\":\"Kikongo\",\"ki\":\"Gikuyu\",\"kj\":\"Kwanyama\",\"kk\":\"Kazachs\",\"kl\":\"Groenlands\",\"km\":\"Khmer\",\"kn\":\"Kannada\",\"ko\":\"Koreaans\",\"kr\":\"Kanuri\",\"ks\":\"Kasjmiri\",\"ku\":\"Koerdisch\",\"kv\":\"Zurjeens\",\"kw\":\"Cornisch\",\"ky\":\"Kirgizisch\",\"la\":\"Latijn\",\"lb\":\"Luxemburgs\",\"lg\":\"Luganda\",\"li\":\"Limburgs\",\"ln\":\"Lingala\",\"lo\":\"Laotiaans\",\"lt\":\"Litouws\",\"lu\":\"Luba-Katanga\",\"lv\":\"Lets\",\"mg\":\"Plateaumalagasi\",\"mh\":\"Marshallees\",\"mi\":\"Maori\",\"mk\":\"Macedonisch\",\"ml\":\"Malayalam\",\"mn\":\"Mongools\",\"mo\":\"Moldavisch\",\"mr\":\"Marathi\",\"ms\":\"Maleis\",\"mt\":\"Maltees\",\"my\":\"Birmaans\",\"na\":\"Nauruaans\",\"nb\":\"Norwegian Bokmal\",\"nd\":\"Noord-Ndebele\",\"ne\":\"Nepalees\",\"ng\":\"Ndonga\",\"nl\":\"Nederlands\",\"nn\":\"Nynorsk\",\"no\":\"Noors\",\"nr\":\"Zuid-Ndebele\",\"nv\":\"Navajo\",\"ny\":\"Nyanja\",\"oc\":\"Occitaans\",\"oj\":\"Ojibweg\",\"om\":\"Afaan Oromo\",\"or\":\"Oriya\",\"os\":\"Ossetisch\",\"pa\":\"Punjabi\",\"pi\":\"Pali\",\"pl\":\"Pools\",\"ps\":\"Pasjtoe\",\"pt\":\"Portugees\",\"qu\":\"Quechua\",\"rm\":\"Reto-Romaans\",\"rn\":\"Kirundi\",\"ro\":\"Roemeens\",\"ru\":\"Russisch\",\"rw\":\"Kinyarwanda\",\"sa\":\"Sanskriet\",\"sc\":\"Sardijns\",\"sd\":\"Sindhi\",\"se\":\"Noord-Samisch\",\"sg\":\"Sangho\",\"sh\":\"Servo-Kroatisch\",\"si\":\"Singalees\",\"sk\":\"Slowaaks\",\"sl\":\"Sloveens\",\"sm\":\"Samoaans\",\"sn\":\"Shona\",\"so\":\"Somalisch\",\"sq\":\"Albanees\",\"sr\":\"Servisch\",\"ss\":\"Swazi\",\"st\":\"Zuid-Sotho\",\"su\":\"Soendanees\",\"sv\":\"Zweeds\",\"sw\":\"Swahili\",\"ta\":\"Tamil\",\"te\":\"Telugu\",\"tg\":\"Tadzjieks\",\"th\":\"Thai\",\"ti\":\"Tigrinya\",\"tk\":\"Turkmeens\",\"tl\":\"Tagalog\",\"tn\":\"Tswana\",\"to\":\"Tongaans\",\"tr\":\"Turks\",\"ts\":\"Tsonga\",\"tt\":\"Tataars\",\"tw\":\"Twi\",\"ty\":\"Tahitiaans\",\"ug\":\"Oeigoers\",\"uk\":\"Oekra\u00EFens\",\"ur\":\"Urdu\",\"uz\":\"Oezbeeks\",\"ve\":\"Venda\",\"vi\":\"Vietnamees\",\"vo\":\"Volap\u00FCk\",\"wa\":\"Waals\",\"wo\":\"Wolof\",\"xh\":\"Xhosa\",\"yi\":\"Jiddisch\",\"yo\":\"Yoruba\",\"za\":\"Zhuang\",\"zh\":\"Chinees\",\"zu\":\"Zoeloe\"},\"menus\":{\"edit\":\"Bewerken\",\"file\":\"Bestand\",\"help\":\"Help\",\"new\":\"Nieuw\",\"tools\":\"Extra\",\"view\":\"Beeld\"},\"messages\":{\"dontaskagain\":\"Onthoud mijn keuze\",\"dontshowagain\":\"Laat deze melding niet meer zien\",\"searchnoresults\":[\"Geen resultaten gevonden bij het zoeken naar '\",1,\"'.\"]},\"richstyles\":{\"blocktext\":\"Block Text\",\"bodytext\":\"Body Text\",\"bodytext2\":\"Body Text 2\",\"bodytext3\":\"Body Text 3\",\"bodytextfirstindent\":\"Body Text First Indent\",\"bodytextfirstindent2\":\"Body Text First Indent 2\",\"bodytextindent\":\"Body Text Indent\",\"bodytextindent2\":\"Body Text Indent 2\",\"bodytextindent3\":\"Body Text Indent 3\",\"caption\":\"Caption\",\"closing\":\"Closing\",\"commentreference\":\"Comment Reference\",\"commenttext\":\"Comment Text\",\"date\":\"Date\",\"defaultparagraphfont\":\"Default Paragraph Font\",\"documentmap\":\"Document Map\",\"emphasis\":\"Emphasis\",\"endnotereference\":\"Endnote Reference\",\"endnotetext\":\"Endnote Text\",\"envelopeaddress\":\"Envelope Address\",\"envelopereturn\":\"Envelope Return\",\"followedhyperlink\":\"Followed Hyperlink\",\"footer\":\"Footer\",\"footnotereference\":\"Footnote Reference\",\"footnotetext\":\"Footnote Text\",\"header\":\"Header\",\"heading1\":\"Kop 1\",\"heading2\":\"Kop 2\",\"heading3\":\"Kop 3\",\"heading4\":\"Kop 4\",\"heading5\":\"Kop 5\",\"heading6\":\"Kop 6\",\"heading7\":\"Kop 7\",\"heading8\":\"Kop 8\",\"heading9\":\"Kop 9\",\"hyperlink\":\"Hyperlink\",\"index1\":\"Index 1\",\"index2\":\"Index 2\",\"index3\":\"Index 3\",\"index4\":\"Index 4\",\"index5\":\"Index 5\",\"index6\":\"Index 6\",\"index7\":\"Index 7\",\"index8\":\"Index 8\",\"index9\":\"Index 9\",\"indexheading\":\"Index Heading\",\"linenumber\":\"Line Number\",\"list\":\"List\",\"list2\":\"List 2\",\"list3\":\"List 3\",\"list4\":\"List 4\",\"list5\":\"List 5\",\"listbullet\":\"List Bullet\",\"listbullet2\":\"List Bullet 2\",\"listbullet3\":\"List Bullet 3\",\"listbullet4\":\"List Bullet 4\",\"listbullet5\":\"List Bullet 5\",\"listcontinue\":\"List Continue\",\"listcontinue2\":\"List Continue 2\",\"listcontinue3\":\"List Continue 3\",\"listcontinue4\":\"List Continue 4\",\"listcontinue5\":\"List Continue 5\",\"listnumber\":\"List Number\",\"listnumber2\":\"List Number 2\",\"listnumber3\":\"List Number 3\",\"listnumber4\":\"List Number 4\",\"listnumber5\":\"List Number 5\",\"macrotext\":\"Macro Text\",\"messageheader\":\"Message Header\",\"normal\":\"Normaal\",\"normalindent\":\"Normal Indent\",\"noteheadiong\":\"Note Heading\",\"ordered\":\"Lijst genummerd\",\"pagenumber\":\"Page Number\",\"plaintext\":\"Plain Text\",\"salutation\":\"Salutation\",\"signature\":\"Signature\",\"strong\":\"Strong\",\"subtitle\":\"Subtitle\",\"tableofauthorities\":\"Table of Authorities\",\"tableoffigures\":\"Table of Figures\",\"title\":\"Title\",\"toaheading\":\"TOA Heading\",\"toc1\":\"TOC 1\",\"toc2\":\"TOC 2\",\"toc3\":\"TOC 3\",\"toc4\":\"TOC 4\",\"toc5\":\"TOC 5\",\"toc6\":\"TOC 6\",\"toc7\":\"TOC 7\",\"toc8\":\"TOC 8\",\"toc9\":\"TOC 9\",\"unordered\":\"Lijst opsom.teken\"},\"units\":{\"centuries\":\"eeuwen\",\"century\":\"eeuw\",\"day\":\"dag\",\"days\":\"dagen\",\"hour\":\"uur\",\"hours\":\"uren\",\"millisecond\":\"milliseconde\",\"milliseconds\":\"milliseconden\",\"minute\":\"minuut\",\"minutes\":\"minuten\",\"month\":\"maand\",\"months\":\"maanden\",\"second\":\"seconde\",\"seconds\":\"seconden\",\"week\":\"week\",\"weeks\":\"weken\",\"year\":\"jaar\",\"years\":\"jaar\"}},\"shell\":{\"checks\":{\"errors\":[{\"p\":1,\"subs\":[2],\"subselse\":[\"'\",2,\"' en nog \",{\"p\":1,\"subs\":[\"1 fout\"],\"subselse\":[1,\" fouten\"],\"t\":\"ifparam\",\"value\":\"1\"}],\"t\":\"ifparam\",\"value\":\"0\"}],\"unresolvedissues\":\"Er zijn nog onopgeloste fouten!\"},\"controller\":{\"neededrights\":[\"De volgende rechten zijn benodigd: \",1],\"rightsproblem\":[\"U heeft geen toegang tot deze applicatie (ingelogd als '\",1,\"')\"],\"sessionexpired\":\"Uw inlogsessie is verlopen. U moet opnieuw inloggen in om deze applicatie te herstarten.\",\"untrustedparams\":\"U mag deze applicatie niet openen met URL parameters.\"},\"dashboard\":{\"apptitle\":\"WebHare\",\"connect-success\":\"Nieuwe WebHare connect-helper instellingen ontvangen\",\"connect-title\":\"WebHare connect\",\"logout\":\"Uitloggen\",\"mountwebhare\":\"Server via WebDav mounten\",\"noapps\":[\"Niets gevonden voor '\",1,\"'\"],\"resetimagecache\":\"Afbeeldingscache leegmaken\",\"showallapps\":\"Toon alle applicaties\"},\"editfavorites\":\"Beheren\",\"errors\":{\"appstartfailed\":\"Het is niet gelukt de applicatie op te starten. Mogelijk is de server onbereikbaar of dient u opnieuw in te loggen\",\"appstartfailed-development\":[\"Het is niet gelukt de applicatie op te starten. Mogelijk is de server onbereikbaar of dient u opnieuw in te loggen\",\"\\n\",\"\\n\",\"Mogelijk moet u ook WebHare softresetten (wh softreset) of de logbestanden controleren op mogelijke problemen (wh watchlog)\"],\"debug\":\"Debuggen\",\"encounterederror\":\"De applicatie is tegen een fout aangelopen en is afgesloten.\",\"errordialogtitle\":\"Applicatie\",\"errortitle\":\"Er heeft een fout plaatsgevonden, neem a.u.b contact op met de systeembeheerder.\",\"restart\":\"Herstarten\"},\"feedback\":{\"button-general\":\"Algemeen\",\"button-specific\":\"Specifiek\",\"message\":\"Wilt u feedback versturen voor een specifiek interface-element of algemene feedback?\",\"title\":\"Feedback\"},\"frontendclose\":\"Applicatiesessies zijn verlopen\",\"frontendclose_description\":\"De verbinding tussen uw browser en WebHare is verbroken. De betreffende applicaties in WebHare zijn gesloten.\",\"loadingapp\":\"Applicatie starten...\",\"login\":{\"apptitle\":\"WebHare\",\"cancel\":\"Annuleren\",\"closedlogin\":\"Het is momenteel niet mogelijk in te loggen. Probeer het later nog eens.\",\"disabledlogin\":\"Inloggen mislukt: dit account is niet actief\",\"enterauthenticatorcode\":\"Voer uw inlogcode in\",\"enterbackupcode\":\"Te veel incorrecte codes ingevoerd, voer een backup code in\",\"enterusernameandpassword\":\"Vult u a.u.b. uw gebruikersnaam en wachtwoord in.\",\"forgotpassword\":\"Wachtwoord vergeten\",\"genericerror\":\"Er is een fout opgetreden, probeer het a.u.b. opnieuw. Als de fout blijft optreden, neem dan contact op met uw systeembeheerder.\",\"infotitle\":\"Belangrijke informatie\",\"invaliddata\":\"Uw inlogsessie is verlopen, log a.u.b. opnieuw in\",\"invalidlogin\":\"U heeft een ongeldige gebruikersnaam en/of wachtwoord ingevoerd. Probeert u het a.u.b. opnieuw.\",\"loginbutton\":\"Inloggen\",\"loginidentityservices\":\"Inloggen met identiteitsservices\",\"loginsecondfactor\":\"Tweede factor inlog\",\"logintitle\":\"Inloggen\",\"loginwithwebhareaccount\":\"Inloggen met een WebHare account\",\"musteditauthsettings\":\"Uw authenticatie-instellingen moeten worden aangepast voordat u verder gaat.\",\"notloggedin\":\"U lijkt niet meer ingelogd te zijn. Mogelijk moet u deze pagina opnieuw inladen om weer in te loggen.\",\"nowebhareaccount\":\"Er is geen gekoppeld WebHare account gevonden. Neem contact op met uw systeembeheerder.\",\"password\":\"Wachtwoord\",\"savelogin\":\"Onthoud mij\",\"secondfactorauthentication\":\"Tweede factor inlog\",\"totpattemptsleft\":[\"Voer uw inlogcode in (u heeft nog \",1,\" pogingen over)\"],\"totpcode\":\"Eenmalige toegangscode\",\"totpinvalidcode\":\"Dit is geen geldige code\",\"totplocked\":\"Te veel ongeldige codes ingevoerd, u moet nu een backup code invoeren om in te loggen\",\"totpreusedcode\":\"Deze code is al gebruikt om in te loggen, wacht tot de volgende code beschikbaar is voordat u het opnieuw probeert.\",\"unexpectedprotocolversion\":[\"Het lijkt erop dat uw browser een verouderde versie van deze webpagina heeft ingeladen. Probeer Shift-F5 (of Shift-Cmd-R op een Mac of Alt-Cmd-R in Safari) om deze pagina opnieuw in te laden.\",\"\\n\",\"\\n\",\"Mocht dit niet werken probeer dan uw browser opnieuw te starten.\"],\"username\":\"Gebruikersnaam\"},\"logout\":{\"surelogout\":\"Weet u zeker dat u wilt uitloggen?\",\"title\":\"Uitloggen\"},\"messagebox\":{\"defaulttitle\":\"Bericht\"},\"nowebhareconnect\":\"WebHare connect niet beschikbaar\",\"nowebhareconnect_description\":[\"U heeft WebHare connect ingeschakeld voor deze server, maar de service kon niet bereikt worden op \",1],\"oauth\":{\"apptitle\":\"OAuth autorisatie\",\"clientid\":\"Client\",\"errortitle\":\"Er is een fout opgetreden\",\"explanation\":\"De volgende server wil toegang tot deze server verkrijgen:\",\"messages\":{\"invalid_redirect\":\"Er is een ongeldige redirect-URL opgegeven (deze moet binnen de client-URL vallen.)\",\"missing_client\":\"Er is geen client opgegeven.\",\"missing_redirect\":\"Er is geen redirect-URL opgegeven.\",\"missing_scopes\":\"Er zijn geen scopes opgegeven.\",\"unknownclient\":[\"Client '\",1,\"' is niet in Webhare Connect geregistreerd.\"],\"unknownerror\":\"Er is een onbekende fout opgetreden. Probeer het a.u.b. opnieuw. Als deze foutmelding dan weer terugkomt, neem dan a.u.b contact op met de systembeheerder.\",\"webhareconnecterror\":\"Kon niet met Webhare Connect verbinden. Neem a.u.b. contact op met systeembeheerder als dit probleem blijft voorkomen.\"},\"oauthtitle\":\"Autorisatie\",\"question\":\"De vragende server zal rechten hebben op alles wat uw account op deze server mag. Weet u zeker dat u deze toegang wil verlenen?\",\"scopes\":\"Scopes\"},\"offline\":\"Verbinding verbroken\",\"offline_description\":\"De verbinding met de server is verbroken. Wacht a.u.b. tot deze weer hersteld is.\",\"openedas\":[\"Geopend als '\",1,\"'\"],\"personalmenu\":\"Favorieten\",\"restartapp\":\"Applicatie herstarten\",\"towl\":{\"gonativedescription\":\"Klik hier om de notificaties weer te geven op uw desktop.\",\"gonativetitle\":\"Desktop notificaties\",\"notificationtitle\":\"WebHare-melding\"},\"upload\":{\"messages\":{\"errortitle\":\"Fout bij uploaden\",\"unknownerror\":\"Er is een fout opgetreden tijdens het uploaden. Probeer het alstublieft opnieuw.\"},\"progress\":{\"calculating\":\"Berekenen...\",\"progress\":\"Voortgang\",\"size\":\"Grootte\",\"speed\":\"Snelheid\",\"title\":\"Uploaden\"}},\"webhareupdated\":\"WebHare bijgewerkt\",\"webhareupdated_description\":\"De webpagina is opnieuw gestart omdat de WebHare server een update ontvangen heeft\"}});\n// Adding dependency: /opt/wh/whtree/modules/tollium/language/default.xml\n// Adding dependency: /opt/wh/whtree/modules/tollium/language/nl.xml\n// Adding dependency: /opt/wh/whtree/modules/tollium/language/default.xml\n", "import * as dompack from 'dompack';\n\nrequire(\"../common.lang.json\");\n\n/****************************************************************************************************************************\n * *\n * SUPPORT FUNCTIONS *\n * *\n ****************************************************************************************************************************/\n\n/* Log messages only when the given target is enabled. A user can specify targets in the URL by using the hash bookmark (e.g.\n ?$tolliumdebug=true#dimensions,communication will show messages with targets 'dimensions' and 'communication'). Currently\n the following targets are used:\n - dimensions (calculating component sizes)\n - rpc (show the individual components/messages being rfcd)\n - communication (communication between the web application and the server)\n - actionenabler (how the enabled state of actions is determined using enableons)\n - messages (the messages, updates and components being processed by the screens)\n - ui (generic UI stuff, eg focus handling)\n - all (show all messages)\n*/\n\nlet enabledlogtypes = [];\n\nvar $todd = {};\n\nexport const gridlineTopMargin = 2; // pixels to add to the top of a grid line\nexport const gridlineBottomMargin = 3; // pixels to add to the bottom of a grid line\nexport const gridlineTotalMargin = gridlineTopMargin + gridlineBottomMargin;\nexport const gridlineHeight = 28; //grid vertical size (28 pixels) including margins\nexport const gridlineInnerHeight = gridlineHeight - gridlineTotalMargin;\nexport const gridlineSnapMax = 8; //never add more than this amount of pixels to snap. an attempt to prevent inlineblocks from wildly generating empty space. this is mostly manually tuning and maybe we shouldn't do it\n\n//workaround not having fully switched to export yet:\n$todd.gridlineTopMargin = gridlineTopMargin;\n$todd.gridlineBottomMargin = gridlineBottomMargin;\n$todd.gridlineTotalMargin = gridlineTotalMargin;\n$todd.gridlineHeight = gridlineHeight;\n$todd.gridlineInnerHeight = gridlineInnerHeight;\n$todd.gridlineSnapMax = gridlineSnapMax;\n\n$todd.settings =\n{ tab_stacked_vpadding_inactive: 1 // border-bottom: 1px (only for inactive!)\n, textedit_defaultwidth: 150\n, list_column_padding: 8 // 2x4 padding\n, list_column_minwidth: 24 // minimum width for an icon (16) + 2x4 padding\n, gridline_topmargin: gridlineTopMargin\n, gridline_bottommargin: gridlineBottomMargin // pixels to add to the top of a grid line\n, grid_vsize: gridlineHeight //grid vertical size (28 pixels) including margins\n, tabspace_vsize: 32 //vertical size inside the tab-space layout\n\n//size of spacers in a sync with apps.scss. SYNC-SPACERS/SYNC-SPACERS-DEBUG\n, spacer_top: 10\n, spacer_bottom: 10\n, spacer_left: 10\n, spacer_right: 10\n//margin between line components. SYNC-SPACERWIDTH\n, spacerwidth: 4\n//size of spacers in a sync with apps.scss. SYNC-BORDERS\n, border_top: 1\n, border_bottom: 1\n, border_left: 1\n, border_right: 1\n\n, listview_padleft: 8\n, listview_padright: 8\n, listview_checkboxholder_width: 20\n, listview_expanderholder_width: 12 //\n, listview_iconholder_width: 20 // iconholder (image 16px + margin 4px)\n\n, fullscreen_maxx: 0.9 //maximum fraction of x width to use for fullscreen windows\n, fullscreen_maxy: 1.0 //maximum fraction of y height to use for fullscreen windows\n\n, buttonheight_intoolbar: 72\n, buttonheight_intabsspace: 27\n\n};\n\n\n$todd.applications = [];\n$todd.applicationstack = [];\n$todd.resourcebase = \"\";\n$todd.customactions = {};\n$todd.dummyimage = null;\n\n$todd.intolerant = window.location.href.indexOf('intolerant=1') != -1;\n$todd.fastunload= window.location.href.indexOf('fastunload=1') != -1;\n\n$todd.getActiveApplication = function()\n{\n return $todd.applicationstack.slice(-1)[0];\n};\n\n/****************************************************************************************************************************\n * Text functions\n */\n\n\n// Replaces \"{param_[n]}\" with p[n] in str for n in [1, 4]\n$todd.FormatString = function(str, p1, p2, p3, p4)\n{\n return str.substitute({ param_1: p1, param_2: p2, param_3: p3, param_4: p4 });\n};\n\n\n/****************************************************************************************************************************\n * Layout\n */\n\n$todd.textsize = { cache: {}\n , node: null\n , styles: { \"font-size\": \"\"\n , \"font-style\": \"\"\n , \"font-weight\": \"\"\n , \"text-decoration\": \"\"\n }\n };\n\n$todd.UpToGridsize = function (size, gridsize)\n{\n if(!gridsize || gridsize<=1)\n return size;\n\n var remainder = size % gridsize;\n if(remainder !== 0)\n size += gridsize - remainder;\n return size;\n};\n\n\n\n$todd.ResetCachedTextSizes = function()\n{\n $todd.textsize.cache = {};\n};\n\n$todd.GetCalculateTextStyles = function()\n{\n return Object.keys($todd.textsize.styles);\n};\n\n$todd.CalculateSize=function(node)\n{\n if (!$todd.calcsizenode)\n {\n $todd.calcsizenode = dompack.create(\"div\", { style: { \"backgroundColor\": \"#ffffff\"\n , color: \"#000000\"\n , position: \"absolute\"\n , visibility: \"hidden\" // Comment this out for debugging\n , width: '1px' //Encourage content collapsing (shrink-wrap)\n }});\n dompack.qS('#todd-measurements').appendChild($todd.calcsizenode);\n }\n $todd.calcsizenode.appendChild(node);\n var size = node.getBoundingClientRect();\n node.remove();\n return { x: Math.ceil(size.width), y: Math.ceil(size.height) };\n};\n\n// text: string with text to calculate size for\n// width: maximum width in pixels for wrapping text, or 0 for no wrapping\n// styles: getStyle-compatible object with font/text settings\n$todd.CalculateTextSize = function(text, width, styles, ishtml)\n{\n if (!$todd.textsize.node)\n {\n $todd.textsize.node = dompack.create(\"div\", { style: { \"backgroundColor\": \"#ffffff\"\n , color: \"#000000\"\n , position: \"absolute\"\n , visibility: \"hidden\" // Comment this out for debugging\n }\n });\n dompack.qS('#todd-measurements').appendChild($todd.textsize.node);\n }\n\n if (typeof (text) != \"string\")\n text = \"\";\n if (typeof (width) != \"number\")\n width = 0;\n\n // Apply only the sanctioned styles\n var applystyles = $todd.textsize.styles;\n if (typeof (styles) == \"object\")\n {\n // merge modifies the first argument, so clone it first\n applystyles = { ...applystyles\n };\n // take the subset of styles we seem to care about\n $todd.GetCalculateTextStyles().forEach(subsetstyle =>\n {\n if(subsetstyle in styles)\n applystyles[subsetstyle] = styles[subsetstyle];\n });\n }\n\n // Check if we have calculated this before\n var key = encodeURIComponent(text) + \"\\t\" + width + \"\\t\" + JSON.stringify(applystyles) + \"\\t\" + (ishtml?1:0);\n var size = $todd.textsize.cache[key];\n if (size)\n return size;\n\n // Set node width if specified\n if (width)\n {\n $todd.textsize.node.style.width = width + 'px';\n $todd.textsize.node.style.whiteSpace = \"normal\";\n }\n else\n {\n $todd.textsize.node.style.width = \"auto\";\n $todd.textsize.node.style.whiteSpace = \"nowrap\";\n }\n\n dompack.setStyles($todd.textsize.node, applystyles);\n\n // Calculate and cache text size\n $todd.textsize.node[ishtml ? \"innerHTML\" : \"textContent\"] = text;\n var rect = $todd.textsize.node.getBoundingClientRect();\n // Rounding up here to avoid returning rounded-down values which would result in elements too small to contain the given text\n // (getBoundingClientRect should return frational values, and not return rounded values)\n size = { x: Math.ceil(rect.width)\n , y: Math.ceil(rect.height)\n };\n $todd.textsize.cache[key] = size;\n return size;\n};\n\n$todd.ReadSize = function(sizeval)\n{\n if(!sizeval)\n return null;\n if(sizeval.substr(sizeval.length-2)=='gr')\n return { type: 5, size: parseInt(sizeval, 10) };\n if(sizeval.substr(sizeval.length-2)=='px')\n return { type: 2, size: parseInt(sizeval, 10) };\n if(sizeval.substr(sizeval.length-2)=='pr')\n return { type: 1, size: parseInt(sizeval, 10) };\n if(sizeval.substr(sizeval.length-1)=='x')\n return { type: 3, size: parseInt(sizeval, 10) };\n if(sizeval=='sp')\n return { type: 4, size: 1 };\n return null;\n};\n$todd.IsAbsoluteParsedSize = function(size)\n{\n return size && size.type != 1;\n};\n\n// Return the set width/height, or the xml width/height, for a component's size object\n$todd.ReadSetWidth = function(sizeobj)\n{\n return $todd.ReadSetSize(sizeobj, true);\n};\n$todd.ReadSetHeight = function(sizeobj)\n{\n return $todd.ReadSetSize(sizeobj, false);\n};\n$todd.ReadSetSize = function(sizeobj, horizontal)\n{\n var size = sizeobj.new_set;\n if (size === null)\n {\n var xml = $todd.ReadSize(sizeobj.xml_set);\n size = $todd.IsFixedSize(sizeobj.xml_set) ? $todd.CalcAbsSize(xml, horizontal) : 0;\n }\n return size;\n};\n$todd.CalcAbsWidth = function(size)\n{\n return $todd.CalcAbsSize(size, true);\n};\n//Calculate the absolute height for a block element (where 2gr = 56)\n$todd.CalcAbsHeight = function(size)\n{\n return $todd.CalcAbsSize(size, false);\n};\n//Calculate the absolute height for an inline element (where 2gr = 51)\n$todd.CalcAbsInlineHeight = function(size)\n{\n return $todd.CalcAbsSize(size, false, true);\n};\n$todd.CalcAbsSize = function(size, horizontal, inline)\n{\n if (!size)\n return 0;\n\n if (typeof(size) == \"number\")\n return size;\n\n if (typeof(size) == \"string\") // XML size specification\n {\n if(size.substr(size.length-2) == 'px')\n return parseInt(size, 10);\n if(size.substr(size.length-2) == 'gr')\n {\n if(horizontal)\n {\n console.error(\"'gr' units not supported horizontally\");\n if($todd.intolerant)\n throw new Error(\"'gr' units not supported horizontally\");\n }\n\n return parseInt(size, 10) * $todd.gridlineHeight - (inline ? $todd.gridlineTotalMargin : 0);\n }\n if(size.substr(size.length-1) == 'x')\n {\n if (horizontal)\n return parseInt(size, 10) * $todd.desktop.x_width;\n // Round to grid size\n return $todd.UpToGridsize(parseInt(size, 10) * $todd.desktop.x_height);\n }\n if(size == 'sp')\n return $todd.settings.spacerwidth;\n return parseInt(size, 10);\n }\n\n if (typeof(size) == \"object\") // Internal size record (as returned by ReadSize)\n {\n if (size.type == 2)\n return size.size;\n if (size.type == 3)\n {\n if (horizontal)\n return size.size * $todd.desktop.x_width;\n return $todd.UpToGridsize(size.size * $todd.desktop.x_height);\n }\n if (size.type == 4)\n return $todd.settings.spacerwidth;\n if (size.type == 5) //'gr'\n return parseInt(size, 10) * $todd.gridlineHeight - (inline ? $todd.gridlineTotalMargin : 0);\n }\n\n return 0;\n};\n\n$todd.IsFixedSize = function(size)\n{\n return size && (size.substr(size.length-1)=='x' //matches both 'px' and 'x' :)\n || size.substr(size.length-2)=='gr'\n || size=='sp'\n || ((parseInt(size) + \"\") == size) // matches numbers in strings\n );\n};\n\nfunction readXMLSize(min, set, iswidth, inline)\n{\n // Initialize width settings (ADDME switch all code to use xml_set_parsed?)\n return { xml_min: iswidth ? $todd.CalcAbsWidth(min) : inline ? $todd.CalcAbsInlineHeight(min) : $todd.CalcAbsHeight(min) // min width as set by xml\n , xml_set: set // width as set by xml (absolute or proportional size)\n , xml_set_parsed: $todd.ReadSize(set)\n , servermin: min //The unparsed versions. deprecate xml_min,xml_set,xml_min_parsed!\n , serverset: set\n , dirty: true // calc should be recalculated\n , min: 0 // min required width\n , calc: 0 // calculated width\n , set: 0 // allocated width\n , new_set: null\n };\n}\n\n//ADDME why can't we receive widths already in the proper format as much as possible?\n$todd.ReadXMLWidths = function(xmlnode) //xmlnode may be null to init a default width object\n{\n return readXMLSize(xmlnode && xmlnode.minwidth ? xmlnode.minwidth : ''\n ,xmlnode && xmlnode.width ? xmlnode.width : ''\n ,true\n );\n};\n$todd.ReadXMLHeights = function(xmlnode, inline)\n{\n return readXMLSize(xmlnode && xmlnode.minheight ? xmlnode.minheight : ''\n ,xmlnode && xmlnode.height ? xmlnode.height : ''\n ,false\n ,inline\n );\n};\n\n\n/****************************************************************************************************************************\n * Events\n */\n\n$todd.highpriority = false;\n$todd.mousedragthreshold = 3; // Amount of pixels to move before drag kicks in\n$todd.globalevents = {}; // Global event handlers\n\n// Fallback settings for user preferences\n$todd.fallback =\n { lang: \"en\"\n , dateformat: \"%d-%m-%Y\"\n , timeformat: \"%H:%M\"\n };\n\n// Desktop properties, will be calculated after initialization (and on resizing/zooming)\n$todd.desktop =\n { node: null\n // Dimensions\n , top: 0\n , left: 0\n , width: 0\n , height: 0\n // Orientation\n , orientation: 0 // Browser orientation (portable devices) in degrees\n , portrait: true // If device is oriented vertically\n , landscape: false // If device is oriented horizontally\n // x dimensions\n , x_width: 7 // The width of an 'x' character\n , x_height: 16 // The (line) height of an 'x' character\n };\n\n$todd.uploadmethod = '';\n$todd.downloadmethod = '';\n$todd.tolliumservice = '';\n\n\n/****************************************************************************************************************************\n * Window events\n */\n\n$todd.mouse = { clickstatus: null\n , hoverstatus: { tooltipshowtimeout: null\n , tooltiphidetimeout: null\n , curcomp: null\n , dragcomp: null\n }\n , dragstatus: null\n };\n\n\n$todd.globalevents.OnDragFiles = function(event)\n{\n // We want to handle this ourselves\n event.preventDefault();\n};\n\n/****************************************************************************************************************************\n * Globally unique id's\n */\n\n$todd.globalidcounter = 0;\n$todd.getGlobalId = function()\n{\n return (++$todd.globalidcounter).toString(16); //FIXME deprecate\n};\n\n\n/****************************************************************************************************************************\n * Some experimental and implementation test functions\n */\n\n$todd.componentsToMessages=function(components)\n{\n /* ADDME: updateScreen is currently an attempt at a 'prettier' API for screen management but we should probably merge with processMessages eventually (perhaps todd controller should change its format)\n */\n var messages=[];\n Object.keys(components).forEach(name =>\n {\n let obj = components[name];\n if(!obj.messages || Object.keys(obj).length>1) //not only sending messages\n {\n var compmsg = {...obj\n , instr: \"component\"\n , target: name\n , type: obj.type || '-shouldalreadyexist'\n , name: name\n , width: obj.width\n , height: obj.height\n , minwidth: obj.minwidth\n , minheight: obj.minheight\n , enabled: obj.enabled !== false\n };\n delete compmsg.messages;\n messages.push(compmsg);\n }\n\n if(obj.messages)\n obj.messages.forEach(msg =>\n {\n var copymsg = { ...msg\n , msg: 'message'\n , target: name\n }; //FIXME copying is probably overkill, but i'm trying to avoid touching source objects.. need to better align various syntaxes\n\n messages.push(copymsg);\n });\n });\n\n return messages;\n};\n\n$todd.IsDebugTypeEnabled = function(type)\n{\n return enabledlogtypes.includes('all') || enabledlogtypes.includes(type);\n};\n\n$todd.DebugTypedLog = function(target)\n{\n var type;\n if (typeof(target) == \"string\")\n {\n target = target.split(\":\");\n type = target[0];\n if (target.length > 1)\n target = target[1];\n }\n\n if (typeof(target) != \"string\" || !([ \"log\", \"info\", \"warn\", \"error\" ].includes(target)))\n target = \"log\";\n\n // Check if the requested type should be shown\n if (!$todd.IsDebugTypeEnabled(type))\n return;\n\n // Strip first element (type) from arguments\n var args = Array.prototype.slice.call(arguments);\n args.splice(0, 1);\n console[target].apply(console, args);\n};\n\nfunction checkLogTypes()\n{\n // Check for specific debug log types (see DebugTypedLog)\n if (document.location.hash)\n {\n var hash = document.location.hash.substr(1);\n enabledlogtypes = hash.split(\",\");\n }\n\n if (enabledlogtypes.includes('all'))\n console.warn(\"Showing all typed debug messages\");\n else if (enabledlogtypes.length)\n {\n console.warn(\"Showing typed debug messages with types \" + enabledlogtypes.join(\", \"));\n }\n}\n\n/* The CSS Color Module Working Draft[1] defines hex notation for RGB color with an alpha value, but these are not supported\n by (all?) browser (yet?). This functions rewrites them to rgba() notation.\n [1] https://drafts.csswg.org/css-color/#hex-notation\n https://caniuse.com/#search=rgba - IE11 still fails */\n$todd.fixupColor = function(color)\n{\n if (color.match(/\\#[0-9a-z]{8}$/))\n {\n return \"rgba(\" + parseInt(color.substr(1, 2), 16) + \",\"\n + parseInt(color.substr(3, 2), 16) + \",\"\n + parseInt(color.substr(5, 2), 16) + \",\"\n + (parseInt(color.substr(7, 2), 16) / 255) + \")\";\n }\n if (color.match(/\\#[0-9a-z]{4}$/))\n {\n return \"rgba(\" + parseInt(color.substr(1, 1) + color.substr(1, 1), 16) + \",\"\n + parseInt(color.substr(2, 1) + color.substr(2, 1), 16) + \",\"\n + parseInt(color.substr(3, 1) + color.substr(3, 1), 16) + \",\"\n + (parseInt(color.substr(4, 1) + color.substr(4, 1), 16) / 255) + \")\";\n }\n return color;\n};\n\n\n/** @short\n @param flags The flags which must be checked against (useually gathered from selected options/rows)\n For example:\n [{ selectable := true, hasurl := false }\n ,{ selectable := false, hasurl := false }\n ]\n @param checkflags Array of string's with the name of flags which must match to enable\n A flag starting with '!' means that to match the flag must NOT TRUE (meaning FALSE) in each object in the 'flags' array.\n Otherwise it's a match if the flag is TRUE in all objects in the flags array.\n @param min minimum amount of items in the flags list\n @param max maximum amount of items in the flags list\n @param selectionmatch (\"all\", \"any\")\n @return whether the action should be enabled (all checkflags match each item in flags)\n*/\n$todd.checkEnabledFlags = function(flags, checkflags, min, max, selectionmatch) //FIXME rename and move out of Screen... compbase?\n{\n // This code should be synchronized with checkEnabledFlags in tollium/include/internal/support.whlib\n $todd.DebugTypedLog(\"actionenabler\", \"- - Checking checkflags [\"+checkflags.join(\", \")+\"], \"+flags.length+\" in [\"+min+\",\"+(max >= 0 ? max+\"]\" : \"->\")+\" (\"+selectionmatch+\")\");\n\n // Check correct number of selected items\n if (flags.length < min || (max >= 0 && flags.length > max))\n {\n $todd.DebugTypedLog(\"actionenabler\", \"- - Wrong number of selected items (\"+flags.length+\"), action should be disabled\");\n return false;\n }\n\n // This action is enabled if the flags are enabled for each selected item\n // If the checkflags for this action are empty, the action is always enabled\n // (the right number of items is already selected) and the selected flags\n // don't have to be checked, so i is initialized with the length of the\n // selected flags.\n if (checkflags.length == 0 || (checkflags.length == 1 && checkflags[0] == ''))\n {\n $todd.DebugTypedLog(\"actionenabler\", \"- - No checkflags, action should be enabled\");\n return true;\n }\n var i = 0;\n var any = false;\n for (; i < flags.length; ++i)\n {\n if (!flags[i])\n {\n $todd.DebugTypedLog(\"actionenabler\", \"- - Flag \"+i+\" undefined, continue to next flag\");\n break;\n }\n var j = 0;\n for (; j < checkflags.length; ++j)\n {\n var checkflag = checkflags[j];\n var checkvalue = true;\n if (checkflag.charAt(0) == '!')\n {\n checkflag = checkflag.slice(1);\n checkvalue = false;\n }\n $todd.DebugTypedLog(\"actionenabler\", \"- - Checkflag '\"+checkflag+\"': \"+flags[i][checkflag]+\"=\"+checkvalue+\"?\");\n if (flags[i][checkflag] != checkvalue)\n {\n $todd.DebugTypedLog(\"actionenabler\", \"- - Checkflag '\"+checkflag+\"' not enabled for selected item \"+i);\n break;\n }\n }\n if (j < checkflags.length)\n {\n // This item does not match, so if all must match, the action should be disabled\n if (selectionmatch == \"all\")\n break;\n }\n else if (selectionmatch == \"any\")\n {\n // This item does match, so if any must match, the action should be enabled\n any = true;\n break;\n }\n }\n // If selectionmatch = \"all\", i should point beyond the end of the flags list (all items are checked and all passed)\n // If selectionmatch = \"any\", any should be true\n var enabled = (selectionmatch == \"all\" && i >= flags.length) || (selectionmatch == \"any\" && any);\n $todd.DebugTypedLog(\"actionenabler\", \"- - Action should be \"+(enabled ? \"enabled\" : \"disabled\"));\n return enabled;\n};\n\nexport default $todd;\nwindow.__todd = $todd; //test framework currently requires it. FIX THAT\n\ncheckLogTypes();\nwindow.addEventListener(\"hashchange\", checkLogTypes);\n", "import * as whintegration from '@mod-system/js/wh/integration';\nimport * as dompack from 'dompack';\nimport * as domfocus from 'dompack/browserfix/focus';\nimport $todd from \"@mod-tollium/web/ui/js/support\";\n\n// Mutators should be defined first, so they can be used inside the ObjLayout Class!\n\nlet urlgencounter = 0;\n\n/****************************************************************************************************************************\n * *\n * COMPONENT BASE *\n * *\n ****************************************************************************************************************************/\nclass ToddCompBase\n{\n\n/****************************************************************************************************************************\n* Initialization\n*/\n\n /* @short Initialize the component with the given component data\n (This is a combination of what component initialization with Construct, InitLayoutFromXML, InitLayoutFromData,\n InitFromXML, InitFromData and FinishSetup used to be)\n @param parent The parent component (null for frame)\n @param data The component initialization data\n @param replacingcomp The old component, if this is a new version of an existing component (for tollium components only)\n @return If this is the first initialize (true), or an update (false)\n */\n constructor(parentcomp, data, replacingcomp)\n {\n this.componenttype = \"component\";\n\n // The parent component\n // (This is what parent used to be, but MooTools uses this.parent to call ancestor functions within updated functions)\n this.parentcomp = null; // old 'parent'\n\n // List children components that have this component as parentcomp\n this.childrencomps = [];\n\n // The component window's frame component\n // (This is what windowroot used to be)\n this.owner = null; // old 'windowroot'\n\n // Whether to destroy this component when its parent is destroyed\n this.destroywithparent = false;\n\n // Initial property values\n this.enabled = true;\n this.visible = true;\n\n this.mousedown = false; // True after mousedown, false after mouseup\n\n this.listeningtoactions = []; //names of actions for which we're listeninn\n this.enablecomponents = [];\n\n /** List of components that need to be destroyed when this component is destroyed\n A component is inserted in this list in its parent when 'destroywithparent' is true in @a initialize.\n */\n this.cascadedestroys = [];\n\n this.node = null; //'legacy' support\n this.nodes = {};\n\n // Width settings\n this.width = {};\n\n // Height settings\n this.height = {};\n\n this.gotskinsettings = false;\n this.skinsettings = null;\n\n\n if(parentcomp == null) //we are the toplevel screen/frame\n {\n this.objectmap = {}; // We need to do this because frame can't yet and it will crash registerComponent\n }\n\n this.title = null;\n this.value = null;\n this.tooltip = null;\n\n // If we're on a line, the line can tell us if we're in an inline element\n this.isinline = parentcomp && parentcomp.holdsinlineitems;\n\n if(parentcomp===null && data===null)\n return; //the table subcomponents don't fully initialize their subs, so this is a hack for them\n\n this.parentcomp = parentcomp;\n this.owner = parentcomp ? parentcomp.owner : this;\n this.destroywithparent = parentcomp && data.destroywithparent || false;\n\n if (parentcomp)\n parentcomp.childrencomps.push(this);\n\n this.name = data.target;\n if(!this.name)\n throw new Error(\"Please ensure all components have a name ('target' field)\"); //uniquely numbered components leak very easily in the objectmap[]...\n\n this.initializeSizes(data);\n\n this.unmasked_events = data.unmasked_events;\n this.enablecomponents = data.enablecomponents ? data.enablecomponents : [];\n this.xml_enabled = data.enabled === true;\n this.visible = data.visible !== false;\n\n this.hint = data.hint ? data.hint : '';\n this.shortcut = data.shortcut ? data.shortcut : '';\n\n this.owner.registerComponent(this);\n this.firstlayout = true;\n// this.lineminheight = 0;\n }\n afterConstructor(data) //needed to run actions that affect buildNode\n {\n if(data.defaultbutton)\n this.node.dataset.toddDefaultButton = data.defaultbutton;\n }\n getTitle()\n {\n return this.title;\n }\n setTitle(title)\n {\n this.title = title;\n }\n getEnabled()\n {\n return this.enabled;\n }\n setEnabled(enabled)\n {\n this.enabled = enabled;\n }\n getValue()\n {\n return this.value;\n }\n setValue(value)\n {\n this.value = value;\n }\n getVisible()\n {\n return this.visible;\n }\n setVisible(visible)\n {\n this.visible = visible;\n }\n getTooltop()\n {\n return this.tooltip;\n }\n setTooltip(tooltip)\n {\n this.tooltip = tooltip;\n }\n\n\n destroy()\n {\n this.setInterestingActions([]);\n\n // Unregister and rename to indicate destroyed components\n if(this.name)\n {\n this.owner.unregisterComponent(this, true);\n if (this.name.substr(this.name.length-11) != \" (replaced)\")\n this.name += \" (destroyed)\";\n }\n\n // Destroy all children marked as 'destroywithparent'. Destroyed children will unregister themselves, so iterate over a copy.\n var copy = this.childrencomps.slice();\n copy.forEach(comp =>\n {\n if (comp.destroywithparent)\n comp.destroy();\n else\n comp.parentcomp = null;\n });\n\n this.childrencomps = [];\n\n // Keep childrencomps in parent up-to-date\n if (this.parentcomp) //erase us from parent\n this.parentcomp.childrencomps = this.parentcomp.childrencomps.filter(comp => comp != this);\n\n this.parentcomp = null;\n this.owner = null;\n }\n\n getDestroyableNodes()\n {\n var retval = [];\n if (this.node)\n retval.push(this.node);\n for (var i in this.nodes)\n if (this.nodes.hasOwnProperty(i))\n retval.push(this.nodes[i]);\n\n return retval;\n }\n\n initializeSizes(data)\n {\n this.width = $todd.ReadXMLWidths(data);\n this.height = $todd.ReadXMLHeights(data, this.isinline);\n }\n\n setSizeToMaxOf(sizeproperty, nodes, addspace)\n {\n var calc=0, min=0;\n nodes.filter(node=>!!node).forEach(node =>\n {\n calc = Math.max(calc, node[sizeproperty].calc);\n min = Math.max(min, node[sizeproperty].min);\n });\n\n this[sizeproperty].calc = calc + (addspace||0);\n this[sizeproperty].min = min + (addspace||0);\n }\n\n setSizeToSumOf(sizeproperty, nodes, addspace)\n {\n var calc=0,min=0;\n nodes.filter(node=>!!node).forEach(node =>\n {\n calc += node[sizeproperty].calc;\n min += node[sizeproperty].min;\n });\n\n this[sizeproperty].calc = calc + (addspace||0);\n this[sizeproperty].min = min + (addspace||0);\n }\n checkEnabled()\n {\n }\n getVisibleChildren()\n {\n return [];\n }\n\n//set the list of actions we care about.\n setInterestingActions(actionlist)\n {\n //ADDME optimize: don't unregister/reregister\n\n //unregister any current actions\n for(let i=0;i\n {\n let callback = () => resolve();\n this.owner.tryProcessMessage(this.name, type, data, options.modal, callback);\n });\n }\n\n // Should this component be submitted at all? By default, only when enabled\n shouldSubmitValue()\n {\n return this.enabled;\n }\n\n // If this function returns null, its value is not submitted\n getSubmitValue()\n {\n return null;\n }\n\n // Check if the given event is unmasked for this component\n isEventUnmasked(eventname)\n {\n if(!this.owner)\n return false;\n return this.owner.hasEventListener(this.name, eventname) || (this.unmasked_events && this.unmasked_events.includes(eventname));\n }\n\n // Check enableon rules\n enabledOn(checkflags, min, max, selectionmatch)\n {\n this.debugLog(\"actionenabler\", \"does not support enabling actions\");\n return false;\n }\n\n // Apply a passive update (readd this component to its parent using an updated version of the component)\n applyUpdatedComp(data)\n {\n if (!this.parentcomp)\n return;\n\n this.parentcomp.readdComponent(this);\n }\n\n // Apply a dynamic update\n applyUpdate(data)\n {\n if (data.type == \"messages\")\n {\n data.messages.forEach(msg =>\n {\n this.processIncomingMessage(msg.type, msg.data);\n });\n return;\n }\n\n console.log(data);\n console.error(\"Received update '\" + data.type + \"' for component '\" + this.name + \"' but not handled\");\n }\n\n processIncomingMessage(type, data)\n {\n let expectcallback = \"onMsg\" + type;\n if(this[expectcallback])\n return this[expectcallback].apply(this,[data]);\n\n console.warn(`Missing handler '${expectcallback}' to process message of type '${type}'`, data);\n }\n\n/****************************************************************************************************************************\n* Component management\n*/\n\n // Readd the given component using an updated version of the component\n readdComponent(comp)\n {\n console.error('Child replacement not implemented by component ' + this.name + ' (' + this.componenttype + ')');\n }\n\n focusComponent()\n {\n let tofocus = domfocus.getFocusableComponents(this.node)[0];\n if(tofocus)\n dompack.focus(tofocus);\n else if (domfocus.canFocusTo(this.node))\n dompack.focus(this.node);\n }\n\n hasfocus()\n {\n return this.node.contains(document.activeElement);\n }\n\n getToddElementForNode(node)\n {\n for(;node;node=node.parentNode)\n if(node.getAttribute && node.getAttribute('data-name'))\n return node.getAttribute('data-name');\n return null;\n }\n //prevent stealing focus, _if_ the click landed in a dom element owned by this element\n mouseDownNoFocusSteal(event)\n {\n if(this.getToddElementForNode(event.target) == this.name)\n {\n console.log(\"*** mouseDownNoFocusSteal on '\" + this.name + \"' WOULD have tried to prevent this focus\"); //FIXME remove this warning if nothing broke because of it\n //console.warn(\"onmousedown on '\" + this.name + \"' preventing loss of focus\"); //FIXME remove this warning if nothing broke because of it\n //event.preventDefault();\n }\n return true;\n }\n/****************************************************************************************************************************\n* Property getters & setters\n*/\n\n\n setDefault(isdefault)\n {\n\n }\n\n\n/****************************************************************************************************************************\n* DOM\n*/\n\n // Build the DOM node(s) for this component\n buildNode()\n {\n this.node = dompack.create(\"span\", { textContext: \"(not implemented: \" + this.componenttype + \")\" });\n }\n getNode()\n {\n if(!this.node)\n throw new Error(\"Trying to request node but not initialized yet\");\n return this.node;\n }\n\n // Return the principal DOM node of this component (returns this.node by default, if defined)\n // (This is what GetDOMNode used to be, but by using toElement, one can simply call $(component) to get the DOM node)\n toElement()\n {\n if(!whintegration.config.islive)\n throw new Error(\"Avoid toElement, especially implicit calls! - replace with an explicit getNode call\");\n // Placeholder for non-implemented component types\n return this.node ? this.node : dompack.create(\"span\", { textContent: \"(not implemented: \" + this.componenttype + \")\" });\n }\n\n\n/****************************************************************************************************************************\n* Dimensions\n*/\n\n beforeRelayout()\n {\n for (const comp of this.getVisibleChildren())\n comp.beforeRelayout();\n }\n\n //new dimensional APIs\n updateSkinSettings()\n {\n if(this.node && !this.gotskinsettings)\n {\n this.skinsettings = this.getSkinSettings();\n this.gotskinsettings = true;\n }\n\n for (const comp of this.getVisibleChildren())\n comp.updateSkinSettings();\n }\n\n getSkinSettings()\n {\n return null;\n }\n\n dim(horizontal)\n {\n return horizontal ? this.width : this.height;\n }\n // If the dim should be calculated (because the dim of this component or any child components is dirty)\n isDimensionDirty(horizontal)\n {\n return this.dim(horizontal).dirty || this.getVisibleChildren().some( function(child) { return child.isDimensionDirty(horizontal); });\n }\n // If no minimum is set but an absolute size is given, set the minimum to it. This implements taking a height as minheight, needed to prevent components from suddenly shrinking\n setMinToAbs(sizeprop)\n {\n if(!sizeprop.servermin && $todd.IsFixedSize(sizeprop.serverset))\n sizeprop.servermin = sizeprop.serverset;\n }\n calculateDimension(horizontal)\n {\n //beginWidth|Height\n var prop = this.dim(horizontal);\n if(!this.isDimensionDirty(horizontal))\n {\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.log(this.getDebugName() + (horizontal ? \": CW:\" : \": CH:\") + \" not dirty, skipping recalculation. min: \" + prop.min + \", calc: \" + prop.calc + \" (current set: \" + prop.set + \")\");\n\n return;\n }\n\n var children = this.getVisibleChildren();\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n {\n console.group(this.getDebugName() + (horizontal ? \": CW:\" : \": CH:\") + \" recalculating. \" + (children.length ? \"(\" + children.length + \" children) \" : \"\"), this.node);\n }\n if(children.includes(null))\n console.error(this.getDebugName() + \" children contains a null!\", children);\n\n for (const comp of children)\n comp.calculateDimension(horizontal);\n\n prop.calc = 0;\n prop.min = 0;\n\n if(horizontal)\n this.calculateDimWidth();\n else\n this.calculateDimHeight();\n\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.log(this.getDebugName() + (horizontal ? \": CW: \" : \": CH: \") + \" min:\" + prop.min + \" calc:\" + prop.calc);\n\n //apply minimums from XML\n if(prop.servermin)\n {\n let calcmin = $todd.CalcAbsSize(prop.servermin, horizontal, this.isinline);\n if(calcmin > prop.min)\n {\n prop.min = calcmin;\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.log(this.getDebugName() + (horizontal ? \": CW: \" : \": CH: \") + \" server pulls up minimum to \" + prop.min);\n }\n }\n\n //fixup calculated using XML and min\n if(prop.new_set)\n {\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.log(this.getDebugName() + (horizontal ? \": CW: \" : \": CH: \") + \" (user-set) setting calc of \" + prop.calc + ' to ' + prop.new_set);\n prop.calc = prop.new_set;\n }\n else if($todd.IsFixedSize(prop.serverset))\n {\n var newsize = $todd.CalcAbsSize(prop.serverset, horizontal, this.isinline);\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.log(this.getDebugName() + (horizontal ? \": CW: \" : \": CH: \") + \" (screen-set) setting calc of \" + prop.calc + ' to ' + newsize);\n prop.calc = newsize;\n }\n\n prop.min = Math.ceil(prop.min);\n prop.calc = Math.ceil(Math.max(prop.calc, prop.min));\n\n if(horizontal)\n this.fixupCalculatedWidths();\n else\n this.fixupCalculatedHeights();\n\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n {\n console.groupEnd();\n console.log(this.getDebugName() + (horizontal ? \": CW: \" : \": CH: \") + \" final min: \" + prop.min + ' calc:' + prop.calc);\n }\n prop.dirty = false;\n if(horizontal)\n this.height.dirty = true;\n }\n applyDimension(horizontal)\n {\n var dim = this.dim(horizontal);\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.group(this.getDebugName() + (horizontal ? \": AW: \" : \": AH: \") + \" applying \" + dim.set + \" (min=\" + dim.min + \", calc=\" + dim.calc + \")\", this.node);\n\n if(horizontal)\n this.applySetWidth();\n else\n this.applySetHeight();\n\n for (const comp of this.getVisibleChildren())\n comp.applyDimension(horizontal);\n this.updateNodeSizeData(); //FIXME make this debugging only\n\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n {\n console.groupEnd();\n }\n }\n\n\n setWidth(setwidth)\n {\n if (setwidth < this.width.min)\n {\n console.error(this.getDebugName() + ' \"' + this.name + '\": Setting width to less than minimum (', setwidth, 'vs', this.width.min, ')', this.node);\n if ($todd.intolerant)\n throw new Error(\"Component got less width than needed\");\n setwidth = this.width.min;\n }\n\n this.width.set = setwidth;\n //FIXME - normal dimension application should arrange for: this.applyDimension(true);\n }\n setHeight(setheight)\n {\n if (setheight < this.height.min)\n {\n console.error(this.componenttype + ' \"' + this.name + '\": Setting height to less than minimum (', setheight, 'vs', this.height.min, ')', this.node);\n if ($todd.intolerant)\n throw new Error(\"Component got less height than needed\");\n setheight = this.height.min;\n }\n\n this.height.set = setheight;\n //FIXME - normal dimension application should arrange for: this.applyDimension(false);\n }\n\n\n setNewWidth(newwidth) //FIXME rename to eg 'client_set' or 'user_set' ?\n {\n this.width.new_set = newwidth;\n this.width.dirty = true;\n }\n setNewHeight(newheight)\n {\n this.height.new_set = newheight;\n this.height.dirty = true;\n }\n /** applySetWidth should apply this.width.set to its children (ie, it should not be updating the DOM yet)\n applySetWidth does not need to raise this.width.set to this.width.min, we will have done that\n */\n applySetWidth()\n {\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.log(this.getDebugName() + \" does not implement applySetWidth\");\n }\n applySetHeight()\n {\n if($todd.IsDebugTypeEnabled(\"dimensions\"))\n console.log(this.getDebugName() + \" does not implement applySetHeight\");\n }\n\n /** calculateDimWidth should set this.width.min and this.width.calc\n calculateDimWidth does not need to raise this.width.min/calc to any XML settings, we will do that\n */\n calculateDimWidth()\n {\n console.error(this.getDebugName() + \" did not implement calculateDimWidth\");\n }\n calculateDimHeight()\n {\n console.error(this.getDebugName() + \" did not implement calculateDimHeight\");\n }\n fixupCalculatedWidths()\n {\n }\n fixupCalculatedHeights()\n {\n }\n\n // Get the top margin of the component within its line\n getVerticalPosition()\n {\n if(!this.parentcomp && $todd.intolerant)\n throw new Error(\"No parent component for current element\");\n if (!this.parentcomp || this.parentcomp.componenttype != \"panel.line\" || this.parentcomp.layout == \"tabs-space\")\n return 0;\n return Math.max(Math.round((this.height.set - this.height.calc) / 2), 0);\n }\n\n /* relayout the component based on this.width.set and this.width.height\n invoke relayout on children */\n relayout() {}\n\n // Get node size data\n getNodeSizeData()\n {\n return [ \"min: \" + this.width.min+ \",\" + this.height.min\n , \"calc: \" + this.width.calc + \",\" + this.height.calc\n , \"set: \" + this.width.set + \",\" + this.height.set\n , \"xmlmin:\" + this.width.servermin + \",\" + this.height.servermin\n , \"xmlset:\" + this.width.serverset + \",\" + this.height.serverset\n ].join(\", \");\n }\n\n // Update the size data in the 'todd-sizes' attribute of the component's DOM node\n updateNodeSizeData()\n {\n this.node.setAttribute(\"todd-sizes\", this.getNodeSizeData());\n }\n\n/* Distributes available pixels over the given size objects (component.width or component.height). Leftover pixels are\n assigned to sizeobjs[leftoverobj], or distributed evenly over the sizeobjs if leftoverobj < 0.\n\n sizeobjs should be created by ReadXMLWidths/ReadXMLHeights */\n distributeSizes(available, sizeobjs, horizontal, leftoverobj)\n {\n return distributeSizes(available, sizeobjs, horizontal, leftoverobj);\n }\n\n distributeSizeProps(property, available, items, horizontal, leftoverobj)\n {\n var sizeobjs=[];\n items.forEach(item => sizeobjs.push(item[property]));\n return this.distributeSizes(available, sizeobjs, horizontal, leftoverobj);\n }\n\n\n/****************************************************************************************************************************\n* Events\n*/\n\n // Called when window is added to DOM, but before it is made visible\n // Return false to prevent window from showing\n onShow()\n {\n return true;\n }\n\n // Called before the component is added to another component\n onBeforeReparent() {}\n\n // Called to get the component's tooltip\n // Return a string to show as tooltip, or nothing to not show the tooltip\n onTooltip()\n {\n if(this.hint)\n {\n //??Fixme\n return true;\n }\n return false;\n }\n\n onActionUpdated()\n {\n }\n\n/****************************************************************************************************************************\n* Public API\n*/\n\n getFileTransferBaseURL(options)\n {\n var url = $todd.resourcebase + \"filetransfer.shtml\";\n if (options && options.filename)\n url += \"/\" + encodeURIComponent(options.filename);\n url += '?l=' + encodeURIComponent(this.owner.hostapp.whsid);\n url += '&w=' + encodeURIComponent(this.owner.screenname);\n url += '&n=' + encodeURIComponent(this.name);\n return url;\n }\n\n /** @param type Type of message\n @param data Data to send\n */\n getFileTransferURL(type, data, options)\n {\n var ftid = 'FT:c' + ++urlgencounter;\n var url = this.getFileTransferBaseURL(options);\n url += '&t=' + encodeURIComponent(type);\n if (data)\n url += \"&d=\" + encodeURIComponent(JSON.stringify(data));\n url += \"&s=\" + ftid;\n return { url: url, id: ftid };\n }\n isMyFileTransferURL(url)\n {\n // Check if this is a url generated by TolliumWebController::GetComponentFileTransferURL\n var baseurl = this.getFileTransferBaseURL();\n return url.substr(0,baseurl.length)==baseurl;\n }\n\n/****************************************************************************************************************************\n* Debugging\n*/\n getDebugName()\n {\n return this.componenttype + \" \" + (this.parentcomp ? this.parentcomp.name + \"->\" : \"\") + (this.name ||'');\n }\n debugLog(type)\n {\n var args = Array.prototype.slice.call(arguments);\n\n //prefix first argument with item name, if possible\n if(args.length>=2 && typeof args[1]=='string')\n {\n args[1] = this.getDebugName() + \": \" + args[1];\n }\n else\n {\n args.splice(1, 0, this.getDebugName() + \": \" + args[1]);\n }\n $todd.DebugTypedLog.apply(null, args);\n }\n}\n\nexport function distributeSizes(available, sizeobjs, horizontal, leftoverobj, options)\n{\n let intolerant = $todd.intolerant || (options && options.intolerant);\n\n if(!(available>=0)) //guard against negative or non-number availables\n {\n console.error(\"distributeSizes got invalid available space\",available,sizeobjs,leftoverobj);\n if (intolerant)\n throw new Error(\"Invalid 'available space' given to distributeSizes\");\n available = 100; // just give some\n }\n\n var logdistribute = $todd.IsDebugTypeEnabled(\"distribute\");\n if(logdistribute)\n console.log(\"DistributeSizes over \" + available + \"px, horizontal=\"+horizontal+\" leftoverobj=\" + leftoverobj + \", sizeobjs=\" + sizeobjs.length, sizeobjs);\n\n var total_prop=0, total_pixels=0, added_size = 0;\n var tempsizes = [];//Temporay store for calculated sizes\n sizeobjs.forEach(function(sizeobj, idx)\n {\n tempsizes[idx] = { set: 0, min: 0, pref: 0, prop: 0 };\n\n // If a size is already set, use that, otherwise read the size set in xml\n var is_fixedsize = false, setsize = 0;\n if (typeof sizeobj.new_set == \"number\")\n {\n //ADDME: Take original sizes (pr?) into account?\n if(logdistribute)\n console.log(\"Child \" + idx + \" new_set was set. setsize=\" + sizeobj.new_set);\n setsize = sizeobj.new_set;\n is_fixedsize = true;\n }\n else\n {\n if(!sizeobj.serverset || $todd.IsFixedSize(sizeobj.serverset))\n {\n is_fixedsize=true;\n setsize = $todd.CalcAbsSize(sizeobj.serverset, horizontal, sizeobj.isinline);\n }\n if(logdistribute)\n console.log(\"Child \" + idx + \" xmlsize=\" + sizeobj.serverset + \", is_fixedsize=\" + is_fixedsize + \", setsize=\" + setsize);\n }\n\n tempsizes[idx].min = sizeobj.min;\n\n if(is_fixedsize) // absolute: (p)x\n {\n var calc = setsize || sizeobj.calc;\n tempsizes[idx].pref = Math.max(calc, tempsizes[idx].min);\n if(logdistribute)\n console.log(\"Child \" + idx + \" calc=\" + calc + \" pref=\" + tempsizes[idx].pref + \" min=\" + tempsizes[idx].min);\n total_pixels += tempsizes[idx].pref;\n if(tempsizes[idx].pref > tempsizes[idx].min)\n added_size += tempsizes[idx].pref - tempsizes[idx].min;\n }\n else // proportional: pr\n {\n tempsizes[idx].prop = parseInt(sizeobj.serverset,10);\n total_prop += tempsizes[idx].prop;\n\n if(logdistribute)\n console.log(\"Child \" + idx + \" prop=\" + tempsizes[idx].prop + \" min=\" + tempsizes[idx].min);\n }\n });\n\n /* - if we have any proportionally sized items\n - remaining_for_prop = (available - total_absolutes)\n - size_per_prop = maximum of (remaining_for_prop / total_props, minimum size)\n */\n var takeaway_prop = 0;\n var propleft;\n if(total_prop>0)\n {\n var spaceleft = available - total_pixels;\n propleft = total_prop;\n var prop = Math.floor(spaceleft / total_prop);\n\n if(logdistribute)\n console.log(\"Distribute remainders: props=\" + propleft + \" available=\" + spaceleft);\n sizeobjs.forEach(function(sizeobj, idx)\n {\n if(tempsizes[idx].prop)\n {\n // var part = Math.floor(spaceleft * tempsizes[idx].set.size / propleft);\n var part = prop * tempsizes[idx].prop;\n if(logdistribute)\n console.log(\"Child \" + idx + \" receiving \" + tempsizes[idx].prop + \"/\" + propleft + \" of \" + spaceleft + \"=\" + part + \" pixels, min=\" + tempsizes[idx].min);\n\n tempsizes[idx].pref = Math.max(part, tempsizes[idx].min);\n if (tempsizes[idx].pref > tempsizes[idx].min)\n takeaway_prop += tempsizes[idx].prop;\n spaceleft -= part;\n propleft -= tempsizes[idx].prop;\n\n total_pixels += tempsizes[idx].pref;\n if(tempsizes[idx].pref > tempsizes[idx].min)\n added_size += (tempsizes[idx].pref - tempsizes[idx].min);\n }\n });\n }\n\n propleft = takeaway_prop;\n while(total_pixels > available)\n {\n // ADDME: We could distribute the burden of handing over overcommitted pixels better\n if(logdistribute)\n console.log(\"Overcommitted (preferred sizes exceeded available) distribute the damage: overcommit=\"+(total_pixels-available)+\", available=\" + added_size + \", propleft=\" + propleft);\n\n var takenaway = 0;\n sizeobjs.forEach(function(sizeobj, idx)\n {\n var takeaway = 0;\n if (propleft && tempsizes[idx].prop)\n {\n // Using ceil to take at least 1 pixel if there are any pixels available (otherwise we could end up in an endless loop)\n takeaway = Math.ceil((total_pixels - available) * tempsizes[idx].prop / propleft);\n }\n else\n takeaway = total_pixels - available;\n takeaway = Math.min(tempsizes[idx].pref - tempsizes[idx].min, takeaway);\n if(takeaway)\n {\n if(logdistribute)\n console.log(\"Taking away \" + takeaway + \" pixels from child \" + idx);\n tempsizes[idx].pref -= takeaway;\n total_pixels -= takeaway;\n // If there are no more pixels to take away from this sizeobj, remove it from the remaining props\n if (tempsizes[idx].set && tempsizes[idx].prop == 1 && tempsizes[idx].pref == tempsizes[idx].min)\n propleft -= tempsizes[idx].prop;\n }\n takenaway += takeaway;\n });\n\n if (!takenaway)\n {\n console.error(\"distributeSizes was unable to fix its overcommit. Total needed=\" + total_pixels + \", available=\" + available + \" pixels\", sizeobjs);\n if (intolerant)\n throw new Error(\"distributeSizes was unable to fix its overcommit\");\n break;\n }\n }\n\n var remaining = available-total_pixels;\n if (remaining)\n {\n if (leftoverobj >= 0 && leftoverobj < sizeobjs.length)\n {\n if(logdistribute)\n console.log(\"We have \" + (available-total_pixels) + \" unassigned pixels, assign to child #\" + leftoverobj);\n tempsizes[leftoverobj].pref += remaining;\n remaining = 0;\n }\n else if (leftoverobj == -2)\n {\n if(logdistribute)\n console.log(\"We have \" + (available-total_pixels) + \" unassigned pixels, try to distribute evently over proportionals\");\n\n tempsizes.some(function(size, i)\n {\n if (size.prop)\n {\n ++size.pref;\n --remaining;\n }\n return !remaining; // stop if no pixels remaining\n });\n }\n }\n\n // Set sizes and fix any leftovers immediately\n sizeobjs.forEach(function(sizeobj, idx)\n {\n if(logdistribute)\n console.log(\"Child \" + idx + \" minimum=\" + tempsizes[idx].min + \" final=\" + tempsizes[idx].pref);\n sizeobj.set = tempsizes[idx].pref;\n });\n\n if(logdistribute)\n console.log(\"Finished layouting (\" + remaining + \" pixels remaining)\");\n return remaining;\n}\n\nclass ActionableComponent extends ToddCompBase\n{\n constructor(parentcomp, data, replacingcomp)\n {\n super(parentcomp, data, replacingcomp);\n }\n afterConstructor(data)\n {\n this.setEnabled(data.enabled);\n this.setAction(data.action);\n super.afterConstructor(data);\n }\n canBeFocusable()\n {\n return true;\n }\n setAction(newaction)\n {\n this.action=newaction;\n this.setInterestingActions(newaction ? [newaction] : []);\n this.onActionUpdated();\n }\n onActionUpdated()\n {\n this.node.classList.toggle(\"todd--disabled\", !this.getEnabled());\n }\n\n getEnabled()\n {\n // Check if the action is already available\n var action = this.action ? this.owner.getComponent(this.action) : null;\n // The button is enabled if it hasn't been disabled directly and it either has an enabled action or no action at all\n return this.enabled && (action ? action.isEnabled() : !this.action);\n }\n\n setEnabled(value)\n {\n this.enabled = value;\n this.node.setAttribute(\"tabindex\", this.getEnabled() && this.canBeFocusable() ? '0' : '-1');\n this.onActionUpdated();\n }\n}\n\nexport { ToddCompBase\n , ActionableComponent\n };\n", "import * as component from '@mod-tollium/web/ui/js/componentbase.es';\nimport $todd from \"@mod-tollium/web/ui/js/support\";\n\nlet ComponentBase = component.ToddCompBase;\nexport default ComponentBase;\n", "import ComponentBase from '@mod-tollium/webdesigns/webinterface/components/base/compbase';\n\nexport default class ActionForwardBase extends ComponentBase\n{\n constructor(parentcomp, data, replacingcomp)\n {\n super(parentcomp, data, replacingcomp);\n this.shortcut = data.shortcut;\n\n this.setEnabled(data.enabled);\n this.owner.registerComponentShortcut(this);\n }\n\n destroy()\n {\n this.owner.unregisterComponentShortcut(this);\n super.destroy();\n }\n\n onShortcut(event)\n {\n this.onExecute();\n }\n}\n", "/* @recommendedimport: import * as browser from 'dompack/extra/browser';\n\n Identify devices for the purpose of analytics/tracing\n NOT a library for feature detection!\n\n Based on Mootools.Browser\n*/\n/* eslint no-useless-escape: off */\n\nexport function parseUserAgent(ua)\n{\n ua = ua.toLowerCase();\n\n // chrome is included in the edge UA, so need to check for edge first, before checking if it's chrome.\n // safari is included in the miuibrowser UA, so need to check for miuibrowser first, before checking if it's safari.\n var UA = ua.match(/(edge|miuibrowser)[\\s\\/:]([\\w\\d\\.]+)/);\n if (!UA)\n UA = ua.match(/(opera|ie|firefox|chrome|trident|crios|version)[\\s\\/:]([\\w\\d\\.]+)?.*?(safari|(?:rv[\\s\\/:]|version[\\s\\/:])([\\w\\d\\.]+)|$)/);\n if (!UA) //try ios 11.4.1\n {\n UA = ua.match(/; cpu os ([\\d]+)/);\n if(UA)\n UA = [null, 'safari', parseInt(UA[1]) ];\n }\n if (!UA)\n UA = [null, 'unknown', 0];\n\n if (UA[1] == 'trident'){\n UA[1] = 'ie';\n if (UA[4]) UA[2] = UA[4];\n } else if (UA[1] == 'crios'){\n UA[1] = 'chrome';\n }\n\n let platform = ua.match(/ip(?:ad|od|hone)/) ? 'ios' : (ua.match(/(?:webos|android)/) || ua.match(/mac|win|linux/) || ['other'])[0];\n if (platform == 'win') platform = 'windows';\n\n let ret = { name: (UA[1] == 'version') ? UA[3] : UA[1],\n version: parseInt((UA[1] == 'opera' && UA[4]) ? UA[4] : UA[2]),\n platform: platform,\n device: ua.match(/ipad/) ? 'tablet' : [ 'ios', 'webos', 'android' ].includes(platform) ? 'mobile' : [ 'mac', 'windows', 'linux' ].includes(platform) ? 'desktop' : ''\n };\n if (ret.name == 'ie' && !ret.version && document.documentMode)\n ret.version = document.documentMode;\n\n return ret;\n}\n\n//module.exports =\nlet browser = parseUserAgent(navigator.userAgent);\n\nexport function getName()\n{\n return browser.name;\n}\nexport function getVersion()\n{\n return browser.version;\n}\nexport function getPlatform()\n{\n return browser.platform;\n}\nexport function getDevice()\n{\n return browser.device;\n}\nexport function getTriplet()\n{\n return browser.platform + '-' + browser.name + '-' + browser.version;\n}\n", "import * as dompack from \"dompack\";\nimport * as browser from \"dompack/extra/browser\";\n\nconst screenshotVersion = 1;\n\n/** Take a DOM snapshot\n*/\nexport default function takeScreenshot(domFilterCallback)\n{\n let bodyNode = document.createElement(\"div\");\n cloneNodeContents(document.body, bodyNode, domFilterCallback);\n\n const styleSheets = [ ...document.styleSheets ].map(sheet => [ ...sheet.cssRules ].map(rule =>\n {\n // In IE11, the cssText of a keyframe(s) rule cannot be accessed\n if (rule.type != CSSRule.KEYFRAME_RULE && rule.type != CSSRule.KEYFRAMES_RULE)\n return rule.cssText;\n return \"\";\n }).join(''));\n const htmlAttrs = [ ...document.documentElement.attributes ].map(attr => { return { name: attr.name, value: attr.value }; });\n const bodyAttrs = [ ...document.body.attributes ].map(attr => { return { name: attr.name, value: attr.value }; });\n\n return (\n { version: screenshotVersion\n , screenshot:\n { htmlAttrs\n , styleSheets\n , bodyAttrs\n , bodyContents: bodyNode.innerHTML\n }\n , size: { width: window.innerWidth, height: window.innerHeight }\n , browser: browser.getTriplet()\n , device: browser.getDevice()\n , userAgent: window.navigator.userAgent\n }\n );\n}\n\nfunction cloneNodeContents(source, target, domFilterCallback)\n{\n for (const childNode of Array.from(source.childNodes))\n {\n if (isNodeVisible(childNode))\n {\n if (!domFilterCallback || domFilterCallback(childNode))\n {\n let childClone = childNode.cloneNode(false);\n if (childClone.nodeType === Node.ELEMENT_NODE && childClone.nodeName === \"IFRAME\")\n {\n childClone.removeAttribute(\"src\");\n childClone.setAttribute(\"sandbox\", \"\");\n }\n if (childNode.scrollTop)\n childClone.dataset.whScreenshotScrollTop = childNode.scrollTop;\n if (childNode.scrollLeft)\n childClone.dataset.whScreenshotScrollLeft = childNode.scrollLeft;\n\n cloneNodeContents(childNode, childClone);\n dompack.append(target, childClone);\n }\n }\n }\n}\n\nfunction isNodeVisible(node)\n{\n if (!node.getBoundingClientRect)\n return true;\n const rect = node.getBoundingClientRect();\n return (rect.width || rect.height)\n && rect.right >= 0 && rect.bottom >= 0\n && rect.left <= window.innerWidth && rect.top <= window.innerHeight;\n}\n", "import * as dompack from \"dompack\";\n\nlet deferred, highlighter, highlightCallback;\n\nexport default function pointAtDOM(event, options)\n{\n if (deferred)\n return Promise.reject(new Error(\"Already pointing at DOM\"));\n\n highlightCallback = options.highlightCallback;\n\n deferred = dompack.createDeferred();\n if (!highlighter)\n highlighter =
;\n\n activateDOMPointer();\n\n if (event)\n highlightDOM(event);\n\n return deferred.promise;\n}\n\nfunction activateDOMPointer()\n{\n window.addEventListener(\"mousemove\", highlightDOM, true);\n window.addEventListener(\"click\", captureDOMNode, true);\n window.addEventListener(\"keydown\", maybeCancelDOMPointer, true);\n}\n\nfunction deactivateDOMPointer()\n{\n window.removeEventListener(\"mousemove\", highlightDOM, true);\n window.removeEventListener(\"click\", captureDOMNode, true);\n window.removeEventListener(\"keydown\", maybeCancelDOMPointer, true);\n}\n\nfunction resolveWithResult(result)\n{\n deactivateDOMPointer();\n const resolve = deferred.resolve;\n deferred = null;\n document.body.removeChild(highlighter);\n resolve(result);\n}\n\nfunction maybeCancelDOMPointer(event)\n{\n dompack.stop(event);\n\n if (event.which === 27 && deferred)\n resolveWithResult();\n}\n\nfunction highlightDOM(event)\n{\n const hoverNode = getHoveredDOMNode(event);\n if (hoverNode) {\n const rect = hoverNode.getBoundingClientRect();\n highlighter.style.top = rect.top + \"px\";\n highlighter.style.left = rect.left + \"px\";\n highlighter.style.width = rect.width + \"px\";\n highlighter.style.height = rect.height + \"px\";\n document.body.appendChild(highlighter);\n } else\n document.body.removeChild(highlighter);\n}\n\nfunction captureDOMNode(event)\n{\n dompack.stop(event);\n\n if (deferred) {\n const hoverNode = getHoveredDOMNode(event);\n if (!hoverNode)\n resolveWithResult();\n else {\n const rect = hoverNode.getBoundingClientRect();\n resolveWithResult({ top: rect.top, left: rect.left, width: rect.width, height: rect.height });\n }\n }\n}\n\nfunction getHoveredDOMNode(event)\n{\n const el = document.elementFromPoint(event.clientX, event.clientY);\n return highlightCallback ? highlightCallback(el) : el;\n}\n", "import * as dompack from 'dompack';\n\n//just number RPCs globally instead of per server, makes debug ouput more useful\nlet globalseqnr = 1;\n\nfunction getDebugAppend()\n{\n let urldebugvar = window.location.href.match(new RegExp('[?&#]wh-debug=([^&#?]*)'));\n return urldebugvar ? '?wh-debug='+urldebugvar[1] : '';\n}\n\n/* this is the followup for net/jsonrpc.es - we can hopefully clear net/ someday\n and move net/eventserver to wh/eventserver.es then */\n\nclass ControlledCall\n{\n constructor(client, method, stack, id, options, callurl, fetchoptions)\n {\n this.client = client;\n this.options = options;\n\n\n // if(options.timeout || options.signal) //as long as rpcResolve exists, we'll ALWAYS need to setup a controller\n {\n this.abortcontroller = new AbortController;\n fetchoptions.signal = this.abortcontroller.signal;\n\n if(options.timeout > 0)\n {\n this.timeout = options.timeout;\n setTimeout(() => this._handleTimeout(), options.timeout);\n }\n if(options.signal)\n {\n options.signal.addEventListener(\"abort\", () => this._abort());\n }\n }\n\n this._callurl = callurl;\n this._fetchoptions = fetchoptions;\n\n let fetchpromise = fetch(this._callurl, this._fetchoptions);\n this.promise = this._completeCall(method, stack, id, fetchpromise);\n this.promise.__jsonrpcinfo = this;\n }\n _handleTimeout()\n {\n this.timedout = true;\n this.abortcontroller.abort();\n }\n _abort()\n {\n this.aborted = true;\n this.abortcontroller.abort();\n }\n _legacyResolve(resolution)\n {\n this.legacyresolve = resolution;\n this.abortcontroller.abort();\n }\n async _completeCall(method, stack, id, fetchpromise)\n {\n let response;\n try\n {\n while(true) //loop for 429\n {\n response = await fetchpromise;\n if(response.status == 429 && !(\"retry429\" in this.options && !this.options.retry429) && response.headers.get(\"Retry-After\"))\n {\n let retryafter = parseInt(response.headers.get(\"Retry-After\"));\n if(this.options.debug)\n console.warn(`[rpc] We are being throttled (429 Too Many Requests) - retrying after ${retryafter} seconds`);\n\n await new Promise( resolve => setTimeout(resolve, retryafter*1000) );\n fetchpromise = fetch(this._callurl, this._fetchoptions);\n continue;\n }\n break;\n }\n }\n catch(exception)\n {\n if(this.options.debug)\n console.log(`[rpc] #${id} Exception invoking '${method}'`, exception);\n\n if(this.aborted)\n throw new Error(`RPC Aborted`);\n else if(this.timedout)\n throw new Error(`RPC Timeout: timeout was set to ${this.timeout} milliseconds`);\n else if(this.legacyresolve && this.legacyresolve.resolve)\n return this.legacyresolve.resolve;\n else\n throw new Error(`RPC Failed: exception: ` + exception);\n }\n\n let jsonresponse;\n try\n {\n jsonresponse = await response.json();\n if(this.options.debug)\n console.log(`[rpc] #${id} Received response to '${method}'`, jsonresponse);\n }\n catch(exception)\n {\n if(this.options.debug)\n console.warn(`[rpc] #${id} Response was not valid JSON`, exception);\n }\n\n if(!jsonresponse)\n throw new Error(\"RPC Failed: Invalid JSON/RPC response received\");\n\n if(jsonresponse && jsonresponse.error)\n {\n this.client._tryLogError(stack, jsonresponse.error);\n throw new Error(\"RPC Error: \" + (jsonresponse.error.message || \"Unknown error\"));\n }\n\n if(response.status == 200 && jsonresponse && jsonresponse.id !== id)\n throw new Error(\"RPC Failed: Invalid JSON/RPC response received\");\n\n if(this.options.wrapresult)\n {\n return { status: response.status\n , result: jsonresponse.result || null\n , error: jsonresponse.error || null\n , retryafter: response.headers.get(\"Retry-After\") ? parseInt(response.headers.get(\"Retry-After\")) : null\n };\n }\n\n return jsonresponse.result;\n }\n}\n\n/** Invokes (WebHare) JSON/RPC\n @param url URL to invoke (leave empty or pass no parameters at all to callback to the current page)\n @cell options.timeout Default timeout for all calls\n @cell options.debug Debug (Follows 'rpc' debugflag if not explicity specified) */\nexport default class RPCClient\n{\n constructor(url, options)\n {\n this.options = { timeout: 0\n , debug: dompack.debugflags.rpc\n , ...options\n };\n\n let whservicematch;\n if(url)\n {\n whservicematch = url.match(/^([a-z0-9_]+):([a-z0-9_]+)$/);\n if(whservicematch)\n this.url = `${location.origin}/wh_services/${whservicematch[1]}/${whservicematch[2]}`;\n else\n this.url = url;\n }\n else\n {\n this.url = location.href; //invoke ourselves directly if no path specified\n }\n\n //if shorthand syntax is used, we know we're talking to our local webhare. add function names and the profiling flag if needed\n this.addfunctionname = this.options.addfunctionname !== undefined ? this.options.addfunctionname : !!whservicematch;\n this.urlappend = this.options.urlappend !== undefined ? this.options.urlappend : whservicematch ? getDebugAppend() : \"\";\n }\n\n setOptions(options)\n {\n this.options = {...this.options, ...options};\n }\n\n _handleLegacyRPCResolve(promise, result)\n {\n if(!promise.__jsonrpcinfo)\n throw new Error(\"The promise is not an async JSONRPC request\");\n promise.__jsonrpcinfo._legacyResolve({resolve:result});\n }\n\n _tryLogError(stack,error)\n {\n let trace = error.data ? (error.data.trace || error.data.list || []) : [];\n\n console.group();\n console.warn(\"RPC failed:\", error.message);\n trace.forEach(rec =>\n {\n if (rec.filename || rec.line)\n {\n var line = rec.filename + '#' + rec.line + '#' + rec.col + (rec.func ? ' (' + rec.func + ')' : '');\n console.log(line);\n }\n });\n if(stack)\n {\n console.warn(\"Stack at calling point\");\n console.log(stack);\n }\n console.groupEnd();\n }\n\n invoke(...params)\n {\n let options;\n if(typeof params[0] == \"object\")\n options = {...this.options, ...params.shift()};\n else\n options = this.options;\n\n let method = params.shift();\n\n //build the URL, add profiling and function parameters where needed\n let callurl = this.url;\n if(this.addfunctionname) //simplifies log analysis, ignored by the server\n callurl += `/${method}`;\n callurl += this.urlappend;\n\n let id = ++globalseqnr;\n let stack;\n\n if(options.debug)\n {\n stack = new Error().stack;\n console.log(`[rpc] #${id} Invoking '${method}'`, params, callurl);\n }\n\n let fetchoptions = { method: \"POST\"\n , credentials: 'same-origin' //this is the default since 2017-08-25, but Edge pre-18 is still around and will fail here\n , headers: { \"Accept\": \"application/json\"\n , \"Content-Type\": \"application/json; charset=utf-8\"\n }\n , body: JSON.stringify(\n { id: id\n , method: method\n , params: params || []\n })\n , keepalive: Boolean(options.keepalive)\n };\n\n return new ControlledCall(this, method, stack, id, options, callurl, fetchoptions).promise;\n }\n}\n", "// Auto-generated RPC interface from /opt/wh/whtree/modules/publisher/js/feedback/internal/feedback.rpc.json\nvar RPCClient = require(\"@mod-system/js/wh/rpc\").default;\nvar request = exports.rpcclient = new RPCClient(\"publisher:feedback\");\nexports.rpcResolve = function(promise, result) { request._handleLegacyRPCResolve(promise, result) };\nexports.invoke = function() { return request.invoke.apply(request,Array.prototype.slice.call(arguments)); }\n\nObject.defineProperty(module.exports, \"HTTP_ERROR\", { get: function() { return JSONRPC.HTTP_ERROR; }});\nObject.defineProperty(module.exports, \"JSON_ERROR\", { get: function() { return JSONRPC.JSON_ERROR; }});\nObject.defineProperty(module.exports, \"PROTOCOL_ERROR\", { get: function() { return JSONRPC.PROTOCOL_ERROR; }});\nObject.defineProperty(module.exports, \"RPC_ERROR\", { get: function() { return JSONRPC.RPC_ERROR; }});\nObject.defineProperty(module.exports, \"OFFLINE_ERROR\", { get: function() { return JSONRPC.OFFLINE_ERROR; }});\nObject.defineProperty(module.exports, \"TIMEOUT_ERROR\", { get: function() { return JSONRPC.TIMEOUT_ERROR; }});\nObject.defineProperty(module.exports, \"SERVER_ERROR\", { get: function() { return JSONRPC.SERVER_ERROR; }});\n\n// Adding dependency: '/opt/wh/whtree/modules/publisher/lib/internal/feedback/rpc.whlib'\n\nexports.storeFeedback = exports.StoreFeedback = /*RECORD*/function(/*STRING*/ pathname, /*STRING*/ scope, /*RECORD*/ data)\n{\nreturn request.invoke.apply(request,[\"StoreFeedback\"].concat(Array.prototype.slice.call(arguments)));\n}\n", "import takeScreenshot from \"./screenshot\";\nimport pointAtDOM from \"./dompointer\";\nimport * as service from \"./internal/feedback.rpc.json\";\nimport \"@mod-publisher/web/common/feedback/styles.css\";\n\nconst defaultOptions =\n { scope: \"\"\n , addElement: true\n , highlightCallback: null\n , domFilterCallback: null\n , feedbackPromise: null\n };\nlet feedbackOptions;\n\n/** @short Initialize the global feedback options\n @param options New options\n @cell(string) options.scope The feedback scope (required)\n @cell(boolean) options.addElement If the user should be asked to point at an element\n @cell(function) options.highlightCallback A function that, given a hovered element, returns the element that should be\n highlighted (optional, by default the hovered element is highlighted)\n @cell(function) options.domFilterCallback A function that, given a DOM element, returns whether the element returns if\n the element should be included in the screenshot (optional, by default all elements are included)\n @cell(function) options.feedbackPromise A function that returns a Promise, which resolves with extra data (a record-like\n object) to add to the feedback\n*/\nexport function initFeedback(options)\n{\n feedbackOptions = { ...defaultOptions, ...options };\n}\n\n/** @short Get feedback\n @param event The event that caused requesting the feedback (optional)\n @param extraOptions Extra option, overwriting the global options @includecelldef %initFeedback.options\n @return The result\n @cell(boolean) return.success If the feedback was successfully stored\n @cell(string) return.guid If successful, the feedback GUID\n @cell(string) return.error If not successful, an error message\n*/\nexport async function getFeedback(event, extraOptions)\n{\n const options = { ...feedbackOptions, ...extraOptions };\n if (!options.scope)\n console.error(`No scope supplied for feedback`);\n const element = options.addElement ? await pointAtDOM(event, options) : null;\n if (!options.addElement || element)\n {\n const data = takeScreenshot(options.domFilterCallback);\n const extraData = options.feedbackPromise ? await options.feedbackPromise() : {};\n if (extraData)\n return await service.storeFeedback(location.pathname, options.scope, { ...data, element, extraData });\n }\n return { success: false, error: \"cancelled\" };\n}\n", "// Auto-generated RPC interface from /opt/wh/whtree/modules/tollium/js/internal/todd.rpc.json\nvar RPCClient = require(\"@mod-system/js/wh/rpc\").default;\nvar request = exports.rpcclient = new RPCClient(\"tollium:todd\");\nexports.rpcResolve = function(promise, result) { request._handleLegacyRPCResolve(promise, result) };\nexports.invoke = function() { return request.invoke.apply(request,Array.prototype.slice.call(arguments)); }\n\nObject.defineProperty(module.exports, \"HTTP_ERROR\", { get: function() { return JSONRPC.HTTP_ERROR; }});\nObject.defineProperty(module.exports, \"JSON_ERROR\", { get: function() { return JSONRPC.JSON_ERROR; }});\nObject.defineProperty(module.exports, \"PROTOCOL_ERROR\", { get: function() { return JSONRPC.PROTOCOL_ERROR; }});\nObject.defineProperty(module.exports, \"RPC_ERROR\", { get: function() { return JSONRPC.RPC_ERROR; }});\nObject.defineProperty(module.exports, \"OFFLINE_ERROR\", { get: function() { return JSONRPC.OFFLINE_ERROR; }});\nObject.defineProperty(module.exports, \"TIMEOUT_ERROR\", { get: function() { return JSONRPC.TIMEOUT_ERROR; }});\nObject.defineProperty(module.exports, \"SERVER_ERROR\", { get: function() { return JSONRPC.SERVER_ERROR; }});\n\n// Adding dependency: '/opt/wh/whtree/modules/tollium/lib/todd/internal/service.whlib'\n\nexports.runToddComm = exports.RunToddComm = /*RECORD*/function(/*RECORD*/ request)\n{\nreturn request.invoke.apply(request,[\"RunToddComm\"].concat(Array.prototype.slice.call(arguments)));\n}\n\nexports.retrieveImages = exports.RetrieveImages = /*RECORD*/function(/*RECORD ARRAY*/ images, /*BOOLEAN*/ nocache, /*BOOLEAN*/ nobroken, /*BOOLEAN*/ rewritestyles)\n{\nreturn request.invoke.apply(request,[\"RetrieveImages\"].concat(Array.prototype.slice.call(arguments)));\n}\n\nexports.getWebdavOpenInfo = exports.GetWebdavOpenInfo = /*RECORD*/function(/*STRING*/ pathname, /*STRING*/ item, /*RECORD*/ data)\n{\nreturn request.invoke.apply(request,[\"GetWebdavOpenInfo\"].concat(Array.prototype.slice.call(arguments)));\n}\n", "import * as dompack from \"dompack\";\nimport * as browser from \"dompack/extra/browser\";\nimport * as toddrpc from \"./internal/todd.rpc.json\";\n\nlet usecanvas = browser.getName()==\"ie\" || dompack.debugflags.ilc;\n\n// Canvas pixel ratio\nvar canvasRatio = (function()\n{\n let ctx = document.createElement(\"canvas\").getContext(\"2d\");\n let devicePixelRatio = window.devicePixelRatio || 1\n , backingStoreRatio = ctx.webkitBackingStorePixelRatio ||\n ctx.mozBackingStorePixelRatio ||\n ctx.msBackingStorePixelRatio ||\n ctx.oBackingStorePixelRatio ||\n ctx.backingStorePixelRatio || 1;\n return devicePixelRatio / backingStoreRatio;\n})();\n\n// The images to load\nvar imagequeue = new Map();\n\n// The images that are already loaded\nvar imagecache = new Map();\n\n// Used to coalesce image loading\nvar loadimgtimeout = null;\n\n// Load the image(s) and apply to an node src\nexport function updateCompositeImage(imgnode, imgnames, width, height, color)\n{\n if(usecanvas && imgnode.nodeName=='IMG')\n {\n console.error(\"img should be a \", usecanvas);\n throw new Error(\"img should be a \");\n }\n\n // If a white image is requested, fallback to (inverted) black if white not directly available\n if (color == \"w\")\n color = \"w,b\";\n // If a color image is requested, fallback to black if color not directly available\n else if (color == \"c\")\n color = \"c,b\";\n\n let data = { imgnames, width, height, color: color || \"\" };\n imgnames = imgnames.join(\"+\");\n let key = `${imgnames}|${data.width}|${data.height}|${data.color}`;\n let cached = imagecache.get(key);\n\n // The data-toddimg attribute is used to reload the image after the cache is cleared\n if (imgnode.dataset.toddimg == key)\n return; //already set.\n\n // Check if the image isn't already on the queue\n for (let [, value] of imagecache)\n {\n if (value.imgs.includes(imgnode))\n {\n value.imgs.splice(value.imgs.indexOf(imgnode), 1);\n break;\n }\n }\n\n // Check if this image src is already loaded\n if (cached && cached.result)\n {\n if (dompack.debugflags.ild)\n console.log(\"Applying cached \"+key);\n applyLoadedResultToImage(cached,imgnode);\n return imgnode;\n }\n\n // Add the image to the list of images to load\n if (!cached)\n imagecache.set(key, { key, data, imgs: [] });\n if (imgnode)\n imagecache.get(key).imgs.push(imgnode);\n\n if (!imagequeue.has(key))\n {\n imagequeue.set(key, data);\n if (dompack.debugflags.ild)\n console.log(\"Loading image \"+key+\", image queue size: \"+imagequeue.size);\n }\n\n // Try to coalesce multiple loadImages calls into one\n if (!loadimgtimeout)\n {\n if (dompack.debugflags.ild)\n console.log(\"Setting image loading timeout\");\n loadimgtimeout = window.setTimeout(loadImages, 1);\n }\n}\n\nasync function loadImages()\n{\n loadimgtimeout = window.clearTimeout(loadimgtimeout);\n let lock = dompack.flagUIBusy();\n\n // Make a list of images to load\n let toload = [];\n if (dompack.debugflags.ild)\n console.warn(\"Image queue size: \"+imagequeue.size);\n for (let [key, data] of imagequeue)\n {\n let cached = imagecache.get(key);\n if (dompack.debugflags.ild)\n console.log(key,cached);\n // If nobody is waiting for this image, or if it's already loaded, skip it\n if (!cached.imgs.length || cached.result)\n {\n applyLoadedResult(cached);\n continue;\n }\n toload.push({ key, data });\n }\n imagequeue.clear();\n\n // Load the images\n if (dompack.debugflags.ild)\n console.info(\"Loading \"+toload.length+\" images\");\n\n try\n {\n let result = await toddrpc.retrieveImages(toload, !!dompack.debugflags.ild, !usecanvas, browser.getName()=='ie');\n\n if (dompack.debugflags.ild)\n console.info(\"Received \"+result.images.length+\" images\",result);\n\n // Store the loaded images\n let loaded = await Promise.all(result.images.map(res =>\n {\n // Get the cache entry\n let cached = imagecache.get(res.key);\n\n // Process the image (invert, apply overlays)\n return processImage(res.key, res.images, cached.data);\n }));\n\n // loaded holds all resolved promise results\n if (dompack.debugflags.ild)\n console.info(\"Applying images\");\n\n loaded.forEach((loadedimg) =>\n {\n if (!loadedimg)\n return;\n // Get the cache entry\n let cached = imagecache.get(loadedimg.key);\n\n if (!cached.imgs.length)\n return;\n\n cached.result = loadedimg.result;\n\n // If no src was returned, the image is broken\n if (!cached.result && !usecanvas)\n cached.result = \"/.tollium/ui/img/broken.svg\";\n\n // Store the loaded image\n applyLoadedResult(cached);\n });\n }\n catch (e)\n {\n console.error(e);\n }\n finally\n {\n lock.release();\n }\n}\n\nfunction applyLoadedResult(cached)\n{\n if (cached.imgs.length && dompack.debugflags.ild)\n console.log(\"Applying \"+cached.key+\" to \"+cached.imgs.length+\" images\");\n\n // Set the src attribute of the img nodes waiting for this image and clear the list\n cached.imgs.forEach(img => applyLoadedResultToImage(cached,img));\n cached.imgs = [];\n}\n\nfunction applyLoadedResultToImage(cached,img)\n{\n img.width = cached.data.width;\n img.height = cached.data.height;\n if(!usecanvas)\n {\n img.src = cached.result;\n }\n else\n {\n let ctx = img.getContext(\"2d\");\n ctx.clearRect(0,0,img.width,img.height);\n ctx.drawImage(cached.result, 0, 0, img.width, img.height);\n }\n\n img.dataset.toddimg = cached.key;\n}\n\nasync function processImage(key, images, data)\n{\n // Check if the base image was found (a default record is returned for broken images)\n if (!images.length || !images[0])\n {\n console.error(\"Broken \"+(images.length > 1 ? \"base \" : \"\")+\"image \"+key);\n // Return an empty src, so the 'broken' image is shown\n return { key };\n }\n let baseimg = images[0];\n let basetype = baseimg.type;\n if (dompack.debugflags.ild)\n console.log(\"Received image \"+key+\" of type \"+basetype);\n\n\n // basedata is the base64-encoded base image data\n let basedata = baseimg.data;\n\n // If the image is black and a white image is wanted, invert the image\n if (baseimg.invertable && basetype == \"image/svg+xml\" && baseimg.color == \"b\" && data.color == \"w,b\")\n {\n if (dompack.debugflags.ild)\n console.log(\"Inverting image\");\n basedata = invertImage(basedata);\n }\n\n // Apply overlays, if any\n if (images.length == 1 && !usecanvas)\n {\n // No extra processing has to be done; return the current image data as data URI\n return { key, result: \"data:\" + basetype + \";base64,\" + basedata };\n }\n\n // Base image and overlays are drawn on a canvas, taking pixel ratio into account\n let canvaswidth = data.width * canvasRatio, canvasheight = data.height * canvasRatio;\n\n // imgnodes is the list of img nodes that are drawn on the composite canvas\n // imgloads is the list of promises that resolve for each loaded img node (or rejected for broken overlays)\n let imgnodes = [], imgloads = [];\n images.forEach((overlayimg, idx) =>\n {\n // Check if the overlay image (idx > 0) was found (a default record is returned for broken imagesw)\n if (idx && !overlayimg)\n {\n // Add a rejected promise\n imgloads.push(Promise.reject(new Error(\"Broken overlay \"+idx+\" for \"+key)));\n return;\n }\n // The image data is either the base image data or the overlay image data\n let imgdata = idx ? overlayimg.data : basedata;\n // If this is a black overlay and a white image is wanted, invert the overlay\n if (idx && overlayimg.invertable && overlayimg.type == \"image/svg+xml\" && overlayimg.color == \"b\" && data.color == \"w,b\")\n {\n if (dompack.debugflags.ild)\n console.log(\"Inverting overlay \"+idx);\n imgdata = invertImage(imgdata);\n }\n\n let imgsrc = \"data:\"+overlayimg.type+\";base64,\" + imgdata;\n var imgnode = new Image(canvaswidth, canvasheight);\n imgnode.knockout = overlayimg.knockout;\n imgnode.translatex = overlayimg.translatex;\n imgnode.translatey = overlayimg.translatey;\n\n //ADDME: In Safari, icons are sometimes rendered smaller than they actually are, don't know what causes this...\n\n // Load the images into drawable img nodes\n imgloads.push(new Promise((resolve, reject) =>\n {\n if (dompack.debugflags.ild)\n console.log(\"Reading image \"+idx);\n imgnode.onload = function()\n {\n if (dompack.debugflags.ild)\n console.log(\"Read image \"+idx);\n resolve(idx);\n };\n imgnode.onerror = function(e)\n {\n reject(\"Error reading image \"+idx+\" for \"+key+\" (\"+imgsrc+\")\");\n };\n imgnode.src = imgsrc;\n }));\n imgnodes.push(imgnode);\n });\n\n try\n {\n // Wait for all images to be loaded\n await Promise.all(imgloads);\n\n if (dompack.debugflags.ild)\n console.info(\"Combining layers\");\n\n let canvas = document.createElement(\"canvas\");\n canvas.width = canvaswidth;\n canvas.height = canvasheight;\n let ctx = canvas.getContext(\"2d\");\n let layercanvas, layerctx;\n let canvasstack = [ canvas ];\n\n // Draw the layers\n let idx = 0;\n for (let imgnode of imgnodes)\n {\n // If the layercanvas exists, it is the current backgrondlayer, don't knockout this layer\n let knockout = !layercanvas;\n if (knockout)\n {\n layercanvas = document.createElement(\"canvas\");\n layercanvas.width = canvaswidth;\n layercanvas.height = canvasheight;\n layerctx = layercanvas.getContext(\"2d\");\n }\n\n if (browser.getName() == \"edge\") // Explicitly setting destination size messes up scaling in Edge\n layerctx.drawImage(imgnode, imgnode.translatex, imgnode.translatey);\n else\n {\n try\n {\n layerctx.drawImage(imgnode, imgnode.translatex, imgnode.translatey, canvaswidth, canvasheight);\n }\n catch (e)\n {\n // IE 11 sometimes doesn't want to render SVG on load event, wait a millisecond sometimes fixes it\n await new Promise(resolve => setTimeout(resolve, 1));\n layerctx.drawImage(imgnode, imgnode.translatex, imgnode.translatey, canvaswidth, canvasheight);\n }\n }\n\n if (idx && knockout) // Knockout overlay\n {\n if (dompack.debugflags.ild)\n console.info(\"Knockout overlay \"+idx);\n\n // Draw a knockout shape by drawing the overlay with a \"destination-out\" composite operation at positions 1\n // logical pixel to the top, left, right and bottom\n ctx.save();\n ctx.globalCompositeOperation = \"destination-out\";\n ctx.imageSmoothingEnabled = false;\n\n // For very large images (> 192 pixels), widen the knockout outline\n let range = Math.max(Math.round(canvaswidth / canvasRatio) / 128, 1 * canvasRatio);\n for (let x = -range; x <= range; ++x)\n for (let y = -range; y <= range; ++y)\n ctx.drawImage(layercanvas, x, y);\n\n ctx.restore();\n }\n\n // Draw the image\n ctx.drawImage(layercanvas, 0, 0);\n\n if (imgnode.knockout)\n {\n // Next layer can knockout this layer\n layercanvas = null;\n layerctx = null;\n }\n else\n {\n if (dompack.debugflags.ild)\n console.info(\"Adding stack layer for non-knockout layer \"+idx);\n\n // Create a new canvas\n canvas = document.createElement(\"canvas\");\n canvas.width = canvaswidth;\n canvas.height = canvasheight;\n ctx = canvas.getContext(\"2d\");\n canvasstack.push(canvas);\n }\n\n ++idx;\n }\n\n // Get and return the image data url\n if (dompack.debugflags.ild)\n console.log(\"Setting cached src \"+key);\n\n // Combine the layers into a single image\n canvas = document.createElement(\"canvas\");\n canvas.width = canvaswidth;\n canvas.height = canvasheight;\n ctx = canvas.getContext(\"2d\");\n for (layercanvas of canvasstack)\n ctx.drawImage(layercanvas, 0, 0);\n\n return { key, result: usecanvas ? canvas : canvas.toDataURL() };\n }\n catch (e)\n {\n // An overlay could not be loaded, return an empty src, so the 'broken' image is shown\n console.error(e);\n return { key };\n }\n}\n\nfunction invertImage(svgdata)\n{\n svgdata = window.atob(svgdata);\n//console.log(svgdata);\n // Switch '#4a4a4a' and '#f3f3f3', adding a space to prevent double substitution\n // And using doublespaces now, as the CSS rewriter for IE (RewriteImgStyles) will already add the first space\n svgdata = svgdata.replace(/(stroke|fill)\\: ?#4a4a4a;/gi, \"$1\\: #f3f3f3;\");\n svgdata = svgdata.replace(/(stroke|fill)\\: ?#f3f3f3;/gi, \"$1\\: #4a4a4a;\");\n svgdata = svgdata.replace(/(stroke|fill)\\: ?rgb\\(74,74,74\\);/gi, \"$1\\: #f3f3f3;\");\n svgdata = svgdata.replace(/(stroke|fill)\\: ?rgb\\(243,243,243\\);/gi, \"$1\\: #4a4a4a;\");\n svgdata = svgdata.replace(/(stroke|fill)=\"#4a4a4a/gi, \"$1=\\\" #f3f3f3\");\n svgdata = svgdata.replace(/(stroke|fill)=\"#f3f3f3/gi, \"$1=\\\" #4a4a4a\");\n svgdata = svgdata.replace(/(stroke|fill)=\"rgb\\(74,74,74\\)/gi, \"$1=\\\" #f3f3f3\");\n svgdata = svgdata.replace(/(stroke|fill)=\"rgb\\(243,243,243\\)/gi, \"$1=\\\" #4a4a4a\");\n//console.log(svgdata);\n return window.btoa(svgdata);\n}\n\nexport function resetImageCache()\n{\n if (dompack.debugflags.ild)\n console.warn(\"Clearing image cache\");\n\n imagequeue.clear();\n imagecache.clear();\n loadimgtimeout = window.clearTimeout(loadimgtimeout);\n\n loadMissingImages({ force: true });\n}\n\nexport function loadMissingImages({ force, node })\n{\n for (let img of (node || document).querySelectorAll(\"[data-toddimg]\"))\n {\n if ( (!img.src || force) && img.dataset.toddimg)\n {\n let data = img.dataset.toddimg.split(\"|\");\n img.dataset.toddimg = data.slice(0, 4).join(\"|\") + \"|reloading\";\n updateCompositeImage(img, data[0].split(\"+\"), parseInt(data[1]), parseInt(data[2]), data[3]);\n }\n }\n}\n\nexport function createImage(imgname, width, height, color, eloptions)\n{\n return createCompositeImage(imgname.split(\"+\"), width, height, color, eloptions);\n}\n\nexport function createCompositeImage(imgnames, width, height, color, eloptions)\n{\n let imgnode = dompack.create(usecanvas ? 'canvas':'img', { width, height, ...eloptions });\n updateCompositeImage(imgnode, imgnames, width, height, color);\n return imgnode;\n}\n\nexport function updateImage(imgnode, imgname, width, height, color)\n{\n if(usecanvas && imgnode.nodeName=='IMG')\n {\n console.error(\"img should be a \", usecanvas);\n throw new Error(\"img should be a \");\n }\n return updateCompositeImage(imgnode, imgname.split(\"+\"), width, height, color);\n}\n\nexport function requireCanvas()\n{\n return usecanvas;\n}\n", "import $todd from \"@mod-tollium/web/ui/js/support\";\nimport * as dompack from 'dompack';\nimport { getTid } from \"@mod-tollium/js/gettid\";\nimport \"../../common.lang.json\";\n\n/** Create a message box\n @param app Parent application\n @param options Options\n @cell options.text\n @cell options.icon \"confirmation\", \"error\", \"information\", \"question\", \"unrecoverable\", \"warning\"\n @cell options.onclose Called when a buttons is clicked. Signature: function(buttonname)\n @cell options.buttons List of buttons\n @cell options.buttons.name Name of button\n @cell options.buttons.title Title of button\n*/\nexport async function runSimpleScreen(app, options) //TODO move API closer to tollium's RunSimpleScreen\n{\n let busylock;\n if(app)\n busylock = app.getBusyLock(); //as we may be loading components, lock just to be sasfe\n\n let defer = dompack.createDeferred();\n try\n {\n await app.promiseComponentTypes(['panel','button','action','text']);\n\n var dialog =\n { frame: { bodynode: 'root'\n , specials: []\n , allowresize: false\n , title: options.title || getTid(\"tollium:shell.messagebox.defaulttitle\")\n , defaultbutton: options.defaultbutton ? 'button_' + options.defaultbutton : ''\n }\n\n , root: { type: 'panel', lines: [{ layout: \"block\", items: [ {item:\"body\"} ], height:'1pr' }\n ,{ layout: \"block\", items: [ {item:\"footer\"} ]}\n ]\n , height:'1pr'\n }\n , body: { type: 'panel', lines: [{title: '', layout: 'left', items:[{item:'text'}]}]\n , height: '1pr'\n , spacers: { top:true, bottom:true, left:true, right:true }\n , width:'1pr'\n }\n , footer: { type: 'panel'\n , lines: [{items: [],layout:'right'}]\n , spacers: { top:true, bottom:true, left:true, right:true }\n , isfooter: true\n , width:'1pr'\n }\n , text: { type: 'text', value: options.text }\n };\n\n if (options.icon)\n {\n dialog.body.lines[0].items.unshift({item:'icon'});\n dialog.icon = { type: 'image'\n , settings: { imgname: \"tollium:messageboxes/\" + options.icon, width: 32, height: 32, color: \"b\" }\n , width: \"32px\", height: \"32px\"\n };\n }\n\n options.buttons.forEach(button =>\n {\n dialog['action_' + button.name] = { type: 'action', hashandler: true, unmasked_events: ['execute'] };\n dialog['button_' + button.name] = { type: 'button', title: button.title, action: 'action_' + button.name };\n dialog.frame.specials.push('action_' + button.name);\n dialog.footer.lines[0].items.push({item:'button_' + button.name});\n });\n\n var newscreen = app.createNewScreenObject('dialog', 'frame', $todd.componentsToMessages(dialog));\n options.buttons.forEach(button =>\n {\n newscreen.setMessageHandler(\"action_\" + button.name, \"execute\", function(data, callback)\n {\n //ADDME if (! onclick or something like that i think) ?\n newscreen.terminateScreen();\n if(options.onclose)\n options.onclose(button.name);\n\n defer.resolve(button.name);\n callback(); //finalize the action\n });\n });\n\n return defer.promise;\n }\n finally\n {\n if(busylock)\n busylock.release();\n }\n}\n", "import * as dompack from \"dompack\";\nimport * as feedback from \"@mod-publisher/js/feedback\";\nimport getTid from \"@mod-tollium/js/gettid\";\nimport { createImage } from \"@mod-tollium/js/icons\";\nimport { runSimpleScreen } from \"@mod-tollium/web/ui/js/dialogs/simplescreen\";\nimport $todd from \"@mod-tollium/web/ui/js/support\";\n\nexport default class TolliumFeedbackAPI\n{\n constructor()\n {\n // Add a trigger node\n this.trigger =\n \n { createImage(\"tollium:objects/bug\", 24, 24, \"b\") }\n ;\n\n this.trigger.addEventListener(\"click\", async event =>\n {\n this.trigger.classList.add(\"wh-tollium__feedback--active\");\n await this.run(event, this.trigger);\n this.trigger.classList.remove(\"wh-tollium__feedback--active\");\n });\n document.body.append(this.trigger);\n }\n\n remove()\n {\n this.trigger.remove();\n this.trigger = null;\n }\n\n async run(event)\n {\n return run(event, { scope: this.scope });\n }\n}\n\nexport async function run(event, options)\n{\n // Ask if the user wants to give feedback for a certain DOM element\n const which = await runSimpleScreen($todd.getActiveApplication(),\n { text: getTid(\"tollium:shell.feedback.message\")\n , title: getTid(\"tollium:shell.feedback.title\")\n , buttons:\n [ { name: \"specific\"\n , title: getTid(\"tollium:shell.feedback.button-specific\")\n }\n , { name: \"general\"\n , title: getTid(\"tollium:shell.feedback.button-general\")\n }\n , { name: \"cancel\"\n , title: getTid(\"tollium:common.actions.cancel\")\n }\n ]\n , defaultbutton: \"specific\"\n , icon: \"question\"\n });\n\n if (which !== \"cancel\")\n {\n // Get the feedback data with the screenshot\n const result = await feedback.getFeedback(event, { addElement: which === \"specific\", ...options });\n if (result.success)\n {\n // Ask for extra information\n window.$shell.startBackendApplication(\"publisher:submitfeedback\", null, { target: { guid: result.guid } });\n }\n }\n}\n\n\nfunction filterDOM(node)\n{\n // Don't include the trigger element in the screenshot\n return node.nodeType != Node.ELEMENT_NODE || !node.classList.contains(\"wh-tollium__feedback\");\n}\n\n// Initialize the feedback options - we always init, as backend apps can trigger feedback too\nfeedback.initFeedback({\n scope: \"tollium:webharebackend\",\n domFilterCallback: filterDOM\n});\n", "import * as dompack from \"dompack\";\nimport * as cookie from \"dompack/extra/cookie.es\";\n\n\nclass DownloadManager\n{\n constructor(url)\n {\n this.cookieinterval = null;\n\n this.url = url;\n this.downloadid = (Math.random().toString().substr(2)) + (++DownloadManager.dlid);\n this.cookiename = \"wh-download-\" + this.downloadid;\n }\n\n destroy()\n {\n if (this.dlframe)\n dompack.remove(this.dlframe);\n\n if (this.cookieinterval)\n {\n window.clearInterval(this.cookieinterval);\n this.cookieinterval = null;\n }\n\n if (this.defer)\n this.defer.resolve({ started: false, errorinfo: null });\n }\n\n _cookieCheck()\n {\n var data = cookie.read(this.cookiename);\n if(!data)\n return;\n\n cookie.remove(this.cookiename);\n window.clearInterval(this.cookieinterval);\n this.cookieinterval = null;\n\n if (this.destroyed)\n return;\n\n this.defer.resolve({ started: true, errorinfo: null });\n }\n\n _onDownloadFailure(errorinfo)\n {\n window.clearInterval(this.cookieinterval);\n this.cookieinterval = null;\n\n if(this.destroyed)\n return;\n\n this.defer.resolve({ started: false, errorinfo });\n }\n\n startDownload()\n {\n if (!this.defer)\n {\n this.defer = dompack.createDeferred();\n const dlurl = this.url + (this.url.indexOf('?')==-1 ? '?' : '&') + 'wh-download=' + this.downloadid;\n\n this.dlframe = dompack.create(\"iframe\",\n { style: { \"display\":\"none\" }\n , src: dlurl\n });\n\n this.dlframe.__whDownloadManagerFailureCallback = (data) => this._onDownloadFailure(data);\n document.body.appendChild(this.dlframe);\n this.cookieinterval = window.setInterval(() => this._cookieCheck(), 100);\n }\n return this.defer.promise;\n }\n}\n\nDownloadManager.dlid = 0;\nwindow.__wh_downloadfailurecallback = function(iframe, data)\n{\n iframe.__whDownloadManagerFailureCallback(data);\n};\n\nexport default DownloadManager;\n", "import \"../../common.lang.json\";\nimport * as dompack from \"dompack\";\nimport { getTid } from \"@mod-tollium/js/gettid\";\n\n\n/** Uploads a wh.net.upload UploadItem / UploadItemGroup, while displaying a progress dialog.\n On finish, a load (or error) event is fired. If no items are present, the load event is\n fired immediately.\n @param screen Owner screen\n @param group Group to upload (upload starts immediately)\n*/\nclass UploadDialogController\n{ constructor(screen, uploadsession)\n {\n this.screen = null;\n this.dialog = null;\n //this.group = null;\n this.done = false;\n this.busylock = null;\n\n this.uploadsession = uploadsession;\n this.screen = screen;\n\n if(this.uploadsession.isStarted())\n throw new Error(\"UploadDialogController must be set up before starting the uploadsession\");\n\n // Mark the ui busy for testing purposes\n this.busylock = dompack.flagUIBusy();\n\n this.uploadsession.addEventListener(\"wh:upload-start\", evt => this.gotStart());\n this.uploadsession.addEventListener(\"wh:upload-progress\", evt => this.gotProgress());\n this.uploadsession.addEventListener(\"wh:upload-end\", evt => this.gotEnd(evt.detail));\n }\n\n /** Compute division factor, postfix and presentation values for a list of byte-sites\n Uses the max value to compute the best presentation\n */\n computePresentationSizes(values)\n {\n var max = Math.max.apply(null, values);\n var divider = 1024, postfix = 'KB';\n if (max > 1250 * 1024)\n divider = 1024*1024, postfix = 'MB';\n\n return { divider: divider\n , postfix: postfix\n , values: values.map(function(i) { return { txt: (i / divider).toFixed(1) }; })\n };\n }\n\n /// Calculate the progress texts to show\n computeTexts()\n {\n var state = this.uploadsession.getStatus();\n var size_stuff = this.computePresentationSizes([ state.uploaded, state.size ]);\n var speed_stuff = this.computePresentationSizes([ state.speed ]);\n\n var retval =\n { progress: 100 * state.uploaded / state.size\n , sizes: size_stuff.values[0].txt + ' / ' + size_stuff.values[1].txt + ' ' + size_stuff.postfix\n , speed: state.speed ? speed_stuff.values[0].txt + ' ' + speed_stuff.postfix + '/s' : getTid('tollium:shell.upload.progress.calculating')\n };\n return retval;\n }\n\n gotStart()\n {\n var texts = this.computeTexts();\n this.dialog = this.screen.displayapp.createScreen(\n { frame: { bodynode: 'root'\n , specials: ['cancelaction']\n , title: getTid('tollium:shell.upload.progress.title')\n }\n , root: { type: 'panel', lines: [{ layout: \"block\", items: [ {item:\"body\"} ]}\n ,{ layout: \"block\", items: [ {item:\"footer\"} ]}\n ]\n }\n , body: { type: 'panel'\n , lines: [ { title: getTid('tollium:shell.upload.progress.progress'), items: [{item:\"progress\"}]}\n , { title: getTid('tollium:shell.upload.progress.size'), items: [{item:\"sizestxt\"}] }\n , { title: getTid('tollium:shell.upload.progress.speed'), items: [{item:\"speedtxt\"}] }\n ]\n , spacers: { top:true, bottom:true, left:true, right:true }\n , width: '75x'\n }\n , footer: { type: 'panel'\n , lines: [{items: [{item:\"cancelbutton\"}], layout:'right'}\n ]\n , spacers: { top:true, bottom:true, left:true, right:true }\n , isfooter: true\n , width:'1pr'\n }\n , progress: { type: 'progress', width: '1pr' }\n , sizestxt: { type: 'text', value: texts.sizes }\n , speedtxt: { type: 'text', value: texts.speed }\n , cancelaction: { type: 'action', hashandler: true, unmasked_events: ['execute'] } //ADDME can we lose the hashandler requirement? perhaps even unmasked_events ?\n , cancelbutton: { type: 'button', title: getTid('tollium:common.actions.cancel'), action: 'cancelaction' }\n });\n\n this.dialog.getComponent('progress').onMsgSetValMax({ max: 100, value: texts.progress });\n this.dialog.setMessageHandler(\"cancelaction\", \"execute\", this.wantAbort.bind(this));\n this.dialog.setMessageHandler(\"frame\", \"close\", this.wantAbort.bind(this));\n }\n\n gotProgress()\n {\n if (this.dialog)\n {\n var texts = this.computeTexts();\n this.dialog.getComponent('progress').onMsgSetValMax({ max: 100, value: texts.progress });\n this.dialog.getComponent('sizestxt').setValue(texts.sizes, false);\n this.dialog.getComponent('speedtxt').setValue(texts.speed, false);\n }\n }\n\n gotEnd(detail)\n {\n if (this.dialog)\n {\n // Disable cancel for visual feedback\n this.dialog.getComponent('cancelbutton').setEnabled(false);\n }\n\n if(!detail.success)\n {\n //TODO can't we use simplescreen.es here?\n this.done = true;\n\n var errormessagedialog = this.screen.displayapp.createScreen(\n { frame: { bodynode: 'root', specials: ['closeaction'], title: getTid('tollium:shell.upload.messages.errortitle') }\n , root: { type: 'panel', lines: [{ layout: \"block\", items: [ {item:\"body\"} ]}\n ,{ layout: \"block\", items: [ {item:\"footer\"} ]}\n ]\n }\n , body: { type: 'panel'\n , lines: [ { items: [{item:\"text\"}], layout:'left' }\n ]\n , spacers: { top:true, bottom:true, left:true, right:true }\n }\n , footer: { type: 'panel'\n , lines: [ { items: [{item:\"closebutton\"}], layout:'right' }\n ]\n , spacers: { top:true, bottom:true, left:true, right:true }\n , isfooter: true\n , width:'1pr'\n }\n , text: { type: 'text', value: getTid('tollium:shell.upload.messages.unknownerror') }\n , closeaction: { type: 'action', hashandler: true, unmasked_events: ['execute'] } //ADDME can we lose the hashandler requirement? perhaps even unmasked_events ?\n , closebutton: { type: 'button', title: getTid('tollium:common.actions.close'), action: 'closeaction' }\n });\n\n errormessagedialog.setMessageHandler(\"closeaction\", \"execute\", this.gotErrorDialogClose.bind(this, errormessagedialog));\n errormessagedialog.setMessageHandler(\"frame\", \"close\", this.gotErrorDialogClose.bind(this, errormessagedialog));\n }\n }\n\n gotErrorDialogClose(errordialog, data, callback)\n {\n // Unbusy for this handler\n callback();\n\n // Close the error dialog, then the progress dialog\n errordialog.terminateScreen();\n this.close();\n }\n\n wantAbort(data, callback)\n {\n // Unbusy for this handler\n callback();\n\n // If already done (and still showing the dialog) we're waiting for tollium callbacks to close the dialog.\n // So ignore user abort.\n if (this.done)\n return;\n\n // Abort upload & close dialog\n this.uploadsession.abort();\n //this.close();\n }\n\n close()\n {\n // Abort group (noop if already done with loading)\n //this.group.abort();\n\n // Close progress dialog if still present\n if (this.dialog)\n this.dialog.terminateScreen();\n this.dialog = null;\n\n // Close busylock if still present\n if (this.busylock)\n this.busylock.release();\n this.busylock = null;\n }\n}\n\nexport default UploadDialogController;\n", "// Auto-generated language file from /opt/wh/whtree/modules/tollium/web/ui/components/imageeditor/imageeditor.lang.json\nvar registerTexts = require(\"@mod-tollium/js/gettid\").registerTexts;\nregisterTexts(\"tollium\",\"en\",{\"components\":{\"imgedit\":{\"add_image\":\"Upload an image\",\"clear_image\":\"Clear the image\",\"dominantcolor\":\"Dominant color\",\"edit_image\":\"Edit the image\",\"editor\":{\"autocontrast\":\"Automatic Contrast\",\"blur\":\"Blur\",\"brightnesscontrast\":\"Brightness and Contrast\",\"coloradjust\":\"Adjust Colors\",\"crop\":\"Crop\",\"delrefpoint\":\"Delete Reference Point\",\"filters\":\"Apply Filters\",\"findfaces\":\"Find face\",\"fliphorizontal\":\"Flip Horizontal\",\"flipvertical\":\"Flip Vertical\",\"grayscale\":\"Grayscale\",\"invert\":\"Invert\",\"posterize\":\"Limit colors\",\"refpoint\":\"Reference Point\",\"rotate\":\"Rotate\",\"rotateleft\":\"Rotate 90\u00B0 Left\",\"rotateright\":\"Rotate 90\u00B0 Right\",\"sepia\":\"Sepia\",\"sharpen\":\"Sharpen\",\"smartcrop\":\"Automatic Crop\",\"title\":\"Edit image\"},\"filename\":\"File name\",\"filters\":{\"autocontrast\":\"Automatic Contrast\",\"blue\":\"Blue\",\"blur\":\"Blur\",\"brightness\":\"Brightness\",\"brightnesscontrast\":\"Brightness and Contrast\",\"coloradjust\":\"Adjust Colors\",\"contrast\":\"Contrast\",\"green\":\"Green\",\"level\":\"Level\",\"posterize\":\"Limit colors\",\"preview\":\"Preview\",\"radius\":\"Radius\",\"red\":\"Red\"},\"hints\":{\"add_image\":\"Upload an image\",\"save_image\":\"Save image\"},\"imagedimensions\":\"Image dimensions\",\"imagesizehint-expectedsize\":[\"The image will be shown with dimensions \",1,\"x\",2],\"imagesizehint-minsize\":[\"The image must be at least \",1,\"x\",2],\"imagesizehint-recommendedsize\":[\"For an optimal display the image should be at least \",1,\"x\",2],\"invalidextension\":[\"The file name should end in '\",1,\"'\"],\"media_library\":\"Select an image from the media library\",\"messages\":{\"confirmdiscardchanges\":[\"There are unsaved changes.\",\"\\n\",\"\\n\",\"Do you want to apply the changes before closing the editor?\"],\"confirmdiscardtool\":[\"The tool is not yet applied.\",\"\\n\",\"\\n\",\"Are you sure you want to close the editor without applying the tool?\"],\"confirmreset\":[\"This will discard all current changes and reset the image to its original.\",\"\\n\",\"\\n\",\"Are you sure you want to reset the image?\"],\"corruptimage\":\"This image cannot be loaded, it is corrupt or has an unsupported format.\",\"maxsizewarning\":\"The image is downscaled because of browser limitations\",\"minsizewarning\":\"The image will be upscaled to meet the requested size requirement\",\"unsupportedtype\":\"The image has an unsupported format.\"},\"moreinfo\":\"More info\u2026\",\"moreinformation\":\"More information\",\"no_image\":\"No image set\",\"other_actions\":\"Other actions\",\"replace-medialibrary\":\"Replace from media library\",\"replace-publisher\":\"Replace from Publisher\",\"replace-upload\":\"Replace by upload\",\"select_image\":\"Select an image from the Publisher\",\"sizehint\":[\"Recommended size: \",1,\"x\",2]}}});\nregisterTexts(\"tollium\",\"nl\",{\"components\":{\"imgedit\":{\"add_image\":\"Een afbeelding uploaden\",\"clear_image\":\"Afbeelding leegmaken\",\"dominantcolor\":\"Dominante kleur\",\"edit_image\":\"Afbeelding bewerken\",\"editor\":{\"autocontrast\":\"Automatisch contrast\",\"blur\":\"Vager maken\",\"brightnesscontrast\":\"Helderheid en contrast\",\"coloradjust\":\"Kleuren aanpassen\",\"crop\":\"Bijsnijden\",\"delrefpoint\":\"Referentiepunt verwijderen\",\"filters\":\"Filters toepassen\",\"findfaces\":\"Gezichtsherkenning\",\"fliphorizontal\":\"Horizontaal spiegelen\",\"flipvertical\":\"Verticaal spiegelen\",\"grayscale\":\"Grijswaarden\",\"invert\":\"Inverteren\",\"posterize\":\"Kleuren beperken\",\"refpoint\":\"Referentiepunt\",\"rotate\":\"Draaien\",\"rotateleft\":\"90\u00B0 linksom draaien\",\"rotateright\":\"90\u00B0 rechtsom draaien\",\"sepia\":\"Sepia\",\"sharpen\":\"Scherper maken\",\"smartcrop\":\"Automatisch bijsnijden\",\"title\":\"Afbeelding bewerken\"},\"filename\":\"Bestandsnaam\",\"filters\":{\"autocontrast\":\"Automatisch contrast\",\"blue\":\"Blauw\",\"blur\":\"Vager maken\",\"brightness\":\"Helderheid\",\"brightnesscontrast\":\"Helderheid en contrast\",\"coloradjust\":\"Kleuren aanpassen\",\"contrast\":\"Contrast\",\"green\":\"Groen\",\"level\":\"Niveau\",\"posterize\":\"Kleuren beperken\",\"preview\":\"Voorvertoning\",\"radius\":\"Radius\",\"red\":\"Rood\"},\"hints\":{\"add_image\":\"Een afbeelding uploaden\",\"save_image\":\"Afbeelding opslaan\"},\"imagedimensions\":\"Afmetingen afbeelding\",\"imagesizehint-expectedsize\":[\"De afbeelding wordt weergegeven met de afmetingen \",1,\"x\",2],\"imagesizehint-minsize\":[\"De afbeelding moet minimaal \",1,\"x\",2,\" zijn\"],\"imagesizehint-recommendedsize\":[\"Voor de optimale weergave moet de afbeelding minimaal \",1,\"x\",2,\" zijn\"],\"invalidextension\":[\"De bestandsnaam moet eindigen op '\",1,\"'\"],\"media_library\":\"Een afbeelding selecteren uit de mediabibliotheek\",\"messages\":{\"confirmdiscardchanges\":[\"Er zijn nog niet-opgeslagen wijzigingen.\",\"\\n\",\"\\n\",\"Wilt u de bewerkingen toepassen voor u de editor sluit?\"],\"confirmdiscardtool\":[\"De bewerking is nog niet toegepast.\",\"\\n\",\"\\n\",\"Weet u zeker dat u de editor wilt sluiten zonder de bewerking toe te passen?\"],\"confirmreset\":[\"De afbeelding wordt hersteld naar het origineel. Alle huidige wijzigingen gaan hierbij verloren.\",\"\\n\",\"\\n\",\"Weet u zeker dat u de afbeelding wilt herstellen?\"],\"corruptimage\":\"De afbeelding kan niet ingeladen worden, het is corrupt of heeft een niet-ondersteund formaat.\",\"maxsizewarning\":\"De afbeelding is verkleind vanwege browserbeperkingen\",\"minsizewarning\":\"De afbeelding wordt vergroot om te voldoen aan de gewenste grootte\",\"unsupportedtype\":\"De afbeelding heeft een niet-ondersteund formaat.\"},\"moreinfo\":\"Meer info\u2026\",\"moreinformation\":\"Meer informatie\",\"no_image\":\"Geen afbeelding ingesteld\",\"other_actions\":\"Andere acties\",\"replace-medialibrary\":\"Vanuit mediabibliotheek vervangen\",\"replace-publisher\":\"Vanuit Publisher vervangen\",\"replace-upload\":\"Via upload vervangen\",\"select_image\":\"Een afbeelding selecteren uit de Publisher\",\"sizehint\":[\"Aangeraden afmeting: \",1,\"x\",2]}}});\n// Adding dependency: /opt/wh/whtree/modules/tollium/language/default.xml\n// Adding dependency: /opt/wh/whtree/modules/tollium/language/nl.xml\n// Adding dependency: /opt/wh/whtree/modules/tollium/language/default.xml\n", "/*jslint browser: true, devel: true, bitwise: false, debug: true, eqeq: false, es5: true, evil: false, forin: false, newcap: false, nomen: true, plusplus: true, regexp: false, unparam: false, sloppy: true, stupid: false, sub: false, todo: true, vars: true, white: true */\n\nmodule.exports = {\n\tparseSections: function(stream, iterator) {\n\t\tvar len, markerType;\n\t\tstream.setBigEndian(true);\n\t\t//stop reading the stream at the SOS (Start of Stream) marker,\n\t\t//because its length is not stored in the header so we can't\n\t\t//know where to jump to. The only marker after that is just EOI (End Of Image) anyway\n\t\twhile(stream.remainingLength() > 0 && markerType !== 0xDA) {\n\t\t\tif(stream.nextUInt8() !== 0xFF) {\n\t\t\t\tthrow new Error('Invalid JPEG section offset');\n\t\t\t}\n\t\t\tmarkerType = stream.nextUInt8();\n\t\t\t//don't read size from markers that have no datas\n\t\t\tif((markerType >= 0xD0 && markerType <= 0xD9) || markerType === 0xDA) {\n\t\t\t\tlen = 0;\n\t\t\t} else {\n\t\t\t\tlen = stream.nextUInt16() - 2;\n\t\t\t}\n\t\t\titerator(markerType, stream.branch(0, len));\n\t\t\tstream.skip(len);\n\t\t}\n\t},\n\t//stream should be located after SOF section size and in big endian mode, like passed to parseSections iterator\n\tgetSizeFromSOFSection: function(stream) {\n\t\tstream.skip(1);\n\t\treturn {\n\t\t\theight: stream.nextUInt16(),\n\t\t\twidth: stream.nextUInt16()\n\t\t};\n\t},\n\tgetSectionName: function(markerType) {\n\t\tvar name, index;\n\t\tswitch(markerType) {\n\t\t\tcase 0xD8: name = 'SOI'; break;\n\t\t\tcase 0xC4: name = 'DHT'; break;\n\t\t\tcase 0xDB: name = 'DQT'; break;\n\t\t\tcase 0xDD: name = 'DRI'; break;\n\t\t\tcase 0xDA: name = 'SOS'; break;\n\t\t\tcase 0xFE: name = 'COM'; break;\n\t\t\tcase 0xD9: name = 'EOI'; break;\n\t\t\tdefault:\n\t\t\t\tif(markerType >= 0xE0 && markerType <= 0xEF) {\n\t\t\t\t\tname = 'APP';\n\t\t\t\t\tindex = markerType - 0xE0;\n\t\t\t\t}\n\t\t\t\telse if(markerType >= 0xC0 && markerType <= 0xCF && markerType !== 0xC4 && markerType !== 0xC8 && markerType !== 0xCC) {\n\t\t\t\t\tname = 'SOF';\n\t\t\t\t\tindex = markerType - 0xC0;\n\t\t\t\t}\n\t\t\t\telse if(markerType >= 0xD0 && markerType <= 0xD7) {\n\t\t\t\t\tname = 'RST';\n\t\t\t\t\tindex = markerType - 0xD0;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t}\n\t\tvar nameStruct = {\n\t\t\tname: name\n\t\t};\n\t\tif(typeof index === 'number') {\n\t\t\tnameStruct.index = index;\n\t\t}\n\t\treturn nameStruct;\n\t}\n};", "/*jslint browser: true, devel: true, bitwise: false, debug: true, eqeq: false, es5: true, evil: false, forin: false, newcap: false, nomen: true, plusplus: true, regexp: false, unparam: false, sloppy: true, stupid: false, sub: false, todo: true, vars: true, white: true */\n\nfunction readExifValue(format, stream) {\n\tswitch(format) {\n\t\tcase 1: return stream.nextUInt8();\n\t\tcase 3: return stream.nextUInt16();\n\t\tcase 4: return stream.nextUInt32();\n\t\tcase 5: return [stream.nextUInt32(), stream.nextUInt32()];\n\t\tcase 6: return stream.nextInt8();\n\t\tcase 8: return stream.nextUInt16();\n\t\tcase 9: return stream.nextUInt32();\n\t\tcase 10: return [stream.nextInt32(), stream.nextInt32()];\n\t\tcase 11: return stream.nextFloat();\n\t\tcase 12: return stream.nextDouble();\n\t\tdefault: throw new Error('Invalid format while decoding: ' + format);\n\t}\n}\n\nfunction getBytesPerComponent(format) {\n\tswitch(format) {\n\t\tcase 1:\n\t\tcase 2:\n\t\tcase 6:\n\t\tcase 7:\n\t\t\treturn 1;\n\t\tcase 3:\n\t\tcase 8:\n\t\t\treturn 2;\n\t\tcase 4:\n\t\tcase 9:\n\t\tcase 11:\n\t\t\treturn 4;\n\t\tcase 5:\n\t\tcase 10:\n\t\tcase 12:\n\t\t\treturn 8;\n\t\tdefault:\n\t\t\tthrow new Error('Invalid format: ' + format);\n\t}\n}\n\nfunction readExifTag(tiffMarker, stream) {\n\tvar tagType = stream.nextUInt16(),\n\t\tformat = stream.nextUInt16(),\n\t\tbytesPerComponent = getBytesPerComponent(format),\n\t\tcomponents = stream.nextUInt32(),\n\t\tvalueBytes = bytesPerComponent * components,\n\t\tvalues,\n\t\tvalue,\n\t\tc;\n\n\t/* if the value is bigger then 4 bytes, the value is in the data section of the IFD\n\tand the value present in the tag is the offset starting from the tiff header. So we replace the stream\n\twith a stream that is located at the given offset in the data section. s*/\n\tif(valueBytes > 4) {\n\t\tstream = tiffMarker.openWithOffset(stream.nextUInt32());\n\t}\n\t//we don't want to read strings as arrays\n\tif(format === 2) {\n\t\tvalues = stream.nextString(components);\n\t\t//cut off \\0 characters\n\t\tvar lastNull = values.indexOf('\\0');\n\t\tif(lastNull !== -1) {\n\t\t\tvalues = values.substr(0, lastNull);\n\t\t}\n\t}\n\telse if(format === 7) {\n\t\tvalues = stream.nextBuffer(components);\n\t}\n\telse {\n\t\tvalues = [];\n\t\tfor(c = 0; c < components; ++c) {\n\t\t\tvalues.push(readExifValue(format, stream));\n\t\t}\n\t}\n\t//since our stream is a stateful object, we need to skip remaining bytes\n\t//so our offset stays correct\n\tif(valueBytes < 4) {\n\t\tstream.skip(4 - valueBytes);\n\t}\n\n\treturn [tagType, values, format];\n}\n\nfunction readIFDSection(tiffMarker, stream, iterator) {\n\tvar numberOfEntries = stream.nextUInt16(), tag, i;\n\tfor(i = 0; i < numberOfEntries; ++i) {\n\t\ttag = readExifTag(tiffMarker, stream);\n\t\titerator(tag[0], tag[1], tag[2]);\n\t}\n}\n\nfunction readHeader(stream) {\n\tvar exifHeader = stream.nextString(6);\n\tif(exifHeader !== 'Exif\\0\\0') {\n\t\tthrow new Error('Invalid EXIF header');\n\t}\n\n\tvar tiffMarker = stream.mark();\n\tvar tiffHeader = stream.nextUInt16();\n\tif(tiffHeader === 0x4949) {\n\t\tstream.setBigEndian(false);\n\t} else if(tiffHeader === 0x4D4D) {\n\t\tstream.setBigEndian(true);\n\t} else {\n\t\tthrow new Error('Invalid TIFF header');\n\t}\n\tif(stream.nextUInt16() !== 0x002A) {\n\t\tthrow new Error('Invalid TIFF data');\n\t}\n\treturn tiffMarker;\n}\n\nmodule.exports = {\n\tIFD0: 1,\n\tIFD1: 2,\n\tGPSIFD: 3,\n\tSubIFD: 4,\n\tInteropIFD: 5,\n\tparseTags: function(stream, iterator) {\n\t\tvar tiffMarker;\n\t\ttry {\n\t\t\ttiffMarker = readHeader(stream);\n\t\t} catch(e) {\n\t\t\treturn false;\t//ignore APP1 sections with invalid headers\n\t\t}\n\t\tvar subIfdOffset, gpsOffset, interopOffset;\n\t\tvar ifd0Stream = tiffMarker.openWithOffset(stream.nextUInt32()),\n\t\t\tIFD0 = this.IFD0;\n\t\treadIFDSection(tiffMarker, ifd0Stream, function(tagType, value, format) {\n\t\t\tswitch(tagType) {\n\t\t\t\tcase 0x8825: gpsOffset = value[0]; break;\n\t\t\t\tcase 0x8769: subIfdOffset = value[0]; break;\n\t\t\t\tdefault: iterator(IFD0, tagType, value, format); break;\n\t\t\t}\n\t\t});\n\t\tvar ifd1Offset = ifd0Stream.nextUInt32();\n\t\tif(ifd1Offset !== 0) {\n\t\t\tvar ifd1Stream = tiffMarker.openWithOffset(ifd1Offset);\n\t\t\treadIFDSection(tiffMarker, ifd1Stream, iterator.bind(null, this.IFD1));\n\t\t}\n\n\t\tif(gpsOffset) {\n\t\t\tvar gpsStream = tiffMarker.openWithOffset(gpsOffset);\n\t\t\treadIFDSection(tiffMarker, gpsStream, iterator.bind(null, this.GPSIFD));\n\t\t}\n\n\t\tif(subIfdOffset) {\n\t\t\tvar subIfdStream = tiffMarker.openWithOffset(subIfdOffset), InteropIFD = this.InteropIFD;\n\t\t\treadIFDSection(tiffMarker, subIfdStream, function(tagType, value, format) {\n\t\t\t\tif(tagType === 0xA005) {\n\t\t\t\t\tinteropOffset = value[0];\n\t\t\t\t} else {\n\t\t\t\t\titerator(InteropIFD, tagType, value, format);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif(interopOffset) {\n\t\t\tvar interopStream = tiffMarker.openWithOffset(interopOffset);\n\t\t\treadIFDSection(tiffMarker, interopStream, iterator.bind(null, this.InteropIFD));\n\t\t}\n\t\treturn true;\n\t}\n};", "function parseNumber(s) {\n\treturn parseInt(s, 10);\n}\n\n//in seconds\nvar hours = 3600;\nvar minutes = 60;\n\n//take date (year, month, day) and time (hour, minutes, seconds) digits in UTC\n//and return a timestamp in seconds\nfunction parseDateTimeParts(dateParts, timeParts) {\n\tdateParts = dateParts.map(parseNumber);\n\ttimeParts = timeParts.map(parseNumber);\n\tvar date = new Date();\n\tdate.setUTCFullYear(dateParts[0]);\n\tdate.setUTCMonth(dateParts[1] - 1);\n\tdate.setUTCDate(dateParts[2]);\n\tdate.setUTCHours(timeParts[0]);\n\tdate.setUTCMinutes(timeParts[1]);\n\tdate.setUTCSeconds(timeParts[2]);\n\tdate.setUTCMilliseconds(0);\n\tvar timestamp = date.getTime() / 1000;\n\treturn timestamp;\n}\n\n//parse date with \"2004-09-04T23:39:06-08:00\" format,\n//one of the formats supported by ISO 8601, and\n//convert to utc timestamp in seconds\nfunction parseDateWithTimezoneFormat(dateTimeStr) {\n\n\tvar dateParts = dateTimeStr.substr(0, 10).split('-');\n\tvar timeParts = dateTimeStr.substr(11, 8).split(':');\n\tvar timezoneStr = dateTimeStr.substr(19, 6);\n\tvar timezoneParts = timezoneStr.split(':').map(parseNumber);\n\tvar timezoneOffset = (timezoneParts[0] * hours) + \n\t\t(timezoneParts[1] * minutes);\n\n\tvar timestamp = parseDateTimeParts(dateParts, timeParts);\n\t//minus because the timezoneOffset describes\n\t//how much the described time is ahead of UTC\n\ttimestamp -= timezoneOffset;\n\n\tif(typeof timestamp === 'number' && !isNaN(timestamp)) {\n\t\treturn timestamp;\n\t}\n}\n\n//parse date with \"YYYY:MM:DD hh:mm:ss\" format, convert to utc timestamp in seconds\nfunction parseDateWithSpecFormat(dateTimeStr) {\n\tvar parts = dateTimeStr.split(' '),\n\t\tdateParts = parts[0].split(':'),\n\t\ttimeParts = parts[1].split(':');\n\t\n\tvar timestamp = parseDateTimeParts(dateParts, timeParts);\n\n\tif(typeof timestamp === 'number' && !isNaN(timestamp)) {\n\t\treturn timestamp;\n\t}\n}\n\nfunction parseExifDate(dateTimeStr) {\n\t//some easy checks to determine two common date formats\n\n\t//is the date in the standard \"YYYY:MM:DD hh:mm:ss\" format?\n\tvar isSpecFormat = dateTimeStr.length === 19 &&\n\t\tdateTimeStr.charAt(4) === ':';\n\t//is the date in the non-standard format,\n\t//\"2004-09-04T23:39:06-08:00\" to include a timezone?\n\tvar isTimezoneFormat = dateTimeStr.length === 25 &&\n\t\tdateTimeStr.charAt(10) === 'T';\n\tvar timestamp;\n\n\tif(isTimezoneFormat) {\n\t\treturn parseDateWithTimezoneFormat(dateTimeStr);\n\t}\n\telse if(isSpecFormat) {\n\t\treturn parseDateWithSpecFormat(dateTimeStr);\n\t}\n}\n\nmodule.exports = {\n\tparseDateWithSpecFormat: parseDateWithSpecFormat,\n\tparseDateWithTimezoneFormat: parseDateWithTimezoneFormat,\n\tparseExifDate: parseExifDate\n};", "var exif = require('./exif');\nvar date = require('./date');\n\nvar degreeTags = [{\n\tsection: exif.GPSIFD,\n\ttype: 0x0002,\n\tname: 'GPSLatitude',\n\trefType: 0x0001,\n\trefName: 'GPSLatitudeRef',\n\tposVal: 'N'\n},\n{\n\tsection: exif.GPSIFD,\n\ttype: 0x0004,\n\tname: 'GPSLongitude',\n\trefType: 0x0003,\n\trefName: 'GPSLongitudeRef',\n\tposVal: 'E'\n}];\nvar dateTags = [{\n\tsection: exif.SubIFD,\n\ttype: 0x9003,\n\tname: 'DateTimeOriginal'\n},\n{\n\tsection: exif.SubIFD,\n\ttype: 0x9004,\n\tname: 'CreateDate'\n}];\n\nmodule.exports = {\n\tcastDegreeValues: function(getTagValue, setTagValue) {\n\t\tdegreeTags.forEach(function(t) {\n\t\t\tvar degreeVal = getTagValue(t);\n\t\t\tif(degreeVal) {\n\t\t\t\tvar degreeRef = getTagValue({section: t.section, type: t.refType, name: t.refName});\n\t\t\t\tvar degreeNumRef = degreeRef === t.posVal ? 1 : -1;\n\t\t\t\tvar degree = (degreeVal[0] + (degreeVal[1] / 60) + (degreeVal[2] / 3600)) * degreeNumRef;\n\t\t\t\tsetTagValue(t, degree);\n\t\t\t}\n\t\t});\n\t},\n\tcastDateValues: function(getTagValue, setTagValue) {\n\t\tdateTags.forEach(function(t) {\n\t\t\tvar dateStrVal = getTagValue(t);\n\t\t\tif(dateStrVal) {\n\t\t\t\t//some easy checks to determine two common date formats\n\t\t\t\tvar timestamp = date.parseExifDate(dateStrVal);\n\t\t\t\tif(typeof timestamp !== 'undefined') {\n\t\t\t\t\tsetTagValue(t, timestamp);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\tsimplifyValue: function(values, format) {\n\t\tif(Array.isArray(values)) {\n\t\t\tvalues = values.map(function(value) {\n\t\t\t\tif(format === 10 || format === 5) {\n\t\t\t\t\treturn value[0] / value[1];\n\t\t\t\t}\n\t\t\t\treturn value;\n\t\t\t});\n\t\t\tif(values.length === 1) {\n\t\t\t\tvalues = values[0];\n\t\t\t}\n\t\t}\n\t\treturn values;\n\t}\n};\n", "module.exports = {\n\texif : {\n\t\t0x0001 : \"InteropIndex\",\n\t\t0x0002 : \"InteropVersion\",\n\t\t0x000B : \"ProcessingSoftware\",\n\t\t0x00FE : \"SubfileType\",\n\t\t0x00FF : \"OldSubfileType\",\n\t\t0x0100 : \"ImageWidth\",\n\t\t0x0101 : \"ImageHeight\",\n\t\t0x0102 : \"BitsPerSample\",\n\t\t0x0103 : \"Compression\",\n\t\t0x0106 : \"PhotometricInterpretation\",\n\t\t0x0107 : \"Thresholding\",\n\t\t0x0108 : \"CellWidth\",\n\t\t0x0109 : \"CellLength\",\n\t\t0x010A : \"FillOrder\",\n\t\t0x010D : \"DocumentName\",\n\t\t0x010E : \"ImageDescription\",\n\t\t0x010F : \"Make\",\n\t\t0x0110 : \"Model\",\n\t\t0x0111 : \"StripOffsets\",\n\t\t0x0112 : \"Orientation\",\n\t\t0x0115 : \"SamplesPerPixel\",\n\t\t0x0116 : \"RowsPerStrip\",\n\t\t0x0117 : \"StripByteCounts\",\n\t\t0x0118 : \"MinSampleValue\",\n\t\t0x0119 : \"MaxSampleValue\",\n\t\t0x011A : \"XResolution\",\n\t\t0x011B : \"YResolution\",\n\t\t0x011C : \"PlanarConfiguration\",\n\t\t0x011D : \"PageName\",\n\t\t0x011E : \"XPosition\",\n\t\t0x011F : \"YPosition\",\n\t\t0x0120 : \"FreeOffsets\",\n\t\t0x0121 : \"FreeByteCounts\",\n\t\t0x0122 : \"GrayResponseUnit\",\n\t\t0x0123 : \"GrayResponseCurve\",\n\t\t0x0124 : \"T4Options\",\n\t\t0x0125 : \"T6Options\",\n\t\t0x0128 : \"ResolutionUnit\",\n\t\t0x0129 : \"PageNumber\",\n\t\t0x012C : \"ColorResponseUnit\",\n\t\t0x012D : \"TransferFunction\",\n\t\t0x0131 : \"Software\",\n\t\t0x0132 : \"ModifyDate\",\n\t\t0x013B : \"Artist\",\n\t\t0x013C : \"HostComputer\",\n\t\t0x013D : \"Predictor\",\n\t\t0x013E : \"WhitePoint\",\n\t\t0x013F : \"PrimaryChromaticities\",\n\t\t0x0140 : \"ColorMap\",\n\t\t0x0141 : \"HalftoneHints\",\n\t\t0x0142 : \"TileWidth\",\n\t\t0x0143 : \"TileLength\",\n\t\t0x0144 : \"TileOffsets\",\n\t\t0x0145 : \"TileByteCounts\",\n\t\t0x0146 : \"BadFaxLines\",\n\t\t0x0147 : \"CleanFaxData\",\n\t\t0x0148 : \"ConsecutiveBadFaxLines\",\n\t\t0x014A : \"SubIFD\",\n\t\t0x014C : \"InkSet\",\n\t\t0x014D : \"InkNames\",\n\t\t0x014E : \"NumberofInks\",\n\t\t0x0150 : \"DotRange\",\n\t\t0x0151 : \"TargetPrinter\",\n\t\t0x0152 : \"ExtraSamples\",\n\t\t0x0153 : \"SampleFormat\",\n\t\t0x0154 : \"SMinSampleValue\",\n\t\t0x0155 : \"SMaxSampleValue\",\n\t\t0x0156 : \"TransferRange\",\n\t\t0x0157 : \"ClipPath\",\n\t\t0x0158 : \"XClipPathUnits\",\n\t\t0x0159 : \"YClipPathUnits\",\n\t\t0x015A : \"Indexed\",\n\t\t0x015B : \"JPEGTables\",\n\t\t0x015F : \"OPIProxy\",\n\t\t0x0190 : \"GlobalParametersIFD\",\n\t\t0x0191 : \"ProfileType\",\n\t\t0x0192 : \"FaxProfile\",\n\t\t0x0193 : \"CodingMethods\",\n\t\t0x0194 : \"VersionYear\",\n\t\t0x0195 : \"ModeNumber\",\n\t\t0x01B1 : \"Decode\",\n\t\t0x01B2 : \"DefaultImageColor\",\n\t\t0x01B3 : \"T82Options\",\n\t\t0x01B5 : \"JPEGTables\",\n\t\t0x0200 : \"JPEGProc\",\n\t\t0x0201 : \"ThumbnailOffset\",\n\t\t0x0202 : \"ThumbnailLength\",\n\t\t0x0203 : \"JPEGRestartInterval\",\n\t\t0x0205 : \"JPEGLosslessPredictors\",\n\t\t0x0206 : \"JPEGPointTransforms\",\n\t\t0x0207 : \"JPEGQTables\",\n\t\t0x0208 : \"JPEGDCTables\",\n\t\t0x0209 : \"JPEGACTables\",\n\t\t0x0211 : \"YCbCrCoefficients\",\n\t\t0x0212 : \"YCbCrSubSampling\",\n\t\t0x0213 : \"YCbCrPositioning\",\n\t\t0x0214 : \"ReferenceBlackWhite\",\n\t\t0x022F : \"StripRowCounts\",\n\t\t0x02BC : \"ApplicationNotes\",\n\t\t0x03E7 : \"USPTOMiscellaneous\",\n\t\t0x1000 : \"RelatedImageFileFormat\",\n\t\t0x1001 : \"RelatedImageWidth\",\n\t\t0x1002 : \"RelatedImageHeight\",\n\t\t0x4746 : \"Rating\",\n\t\t0x4747 : \"XP_DIP_XML\",\n\t\t0x4748 : \"StitchInfo\",\n\t\t0x4749 : \"RatingPercent\",\n\t\t0x800D : \"ImageID\",\n\t\t0x80A3 : \"WangTag1\",\n\t\t0x80A4 : \"WangAnnotation\",\n\t\t0x80A5 : \"WangTag3\",\n\t\t0x80A6 : \"WangTag4\",\n\t\t0x80E3 : \"Matteing\",\n\t\t0x80E4 : \"DataType\",\n\t\t0x80E5 : \"ImageDepth\",\n\t\t0x80E6 : \"TileDepth\",\n\t\t0x827D : \"Model2\",\n\t\t0x828D : \"CFARepeatPatternDim\",\n\t\t0x828E : \"CFAPattern2\",\n\t\t0x828F : \"BatteryLevel\",\n\t\t0x8290 : \"KodakIFD\",\n\t\t0x8298 : \"Copyright\",\n\t\t0x829A : \"ExposureTime\",\n\t\t0x829D : \"FNumber\",\n\t\t0x82A5 : \"MDFileTag\",\n\t\t0x82A6 : \"MDScalePixel\",\n\t\t0x82A7 : \"MDColorTable\",\n\t\t0x82A8 : \"MDLabName\",\n\t\t0x82A9 : \"MDSampleInfo\",\n\t\t0x82AA : \"MDPrepDate\",\n\t\t0x82AB : \"MDPrepTime\",\n\t\t0x82AC : \"MDFileUnits\",\n\t\t0x830E : \"PixelScale\",\n\t\t0x8335 : \"AdventScale\",\n\t\t0x8336 : \"AdventRevision\",\n\t\t0x835C : \"UIC1Tag\",\n\t\t0x835D : \"UIC2Tag\",\n\t\t0x835E : \"UIC3Tag\",\n\t\t0x835F : \"UIC4Tag\",\n\t\t0x83BB : \"IPTC-NAA\",\n\t\t0x847E : \"IntergraphPacketData\",\n\t\t0x847F : \"IntergraphFlagRegisters\",\n\t\t0x8480 : \"IntergraphMatrix\",\n\t\t0x8481 : \"INGRReserved\",\n\t\t0x8482 : \"ModelTiePoint\",\n\t\t0x84E0 : \"Site\",\n\t\t0x84E1 : \"ColorSequence\",\n\t\t0x84E2 : \"IT8Header\",\n\t\t0x84E3 : \"RasterPadding\",\n\t\t0x84E4 : \"BitsPerRunLength\",\n\t\t0x84E5 : \"BitsPerExtendedRunLength\",\n\t\t0x84E6 : \"ColorTable\",\n\t\t0x84E7 : \"ImageColorIndicator\",\n\t\t0x84E8 : \"BackgroundColorIndicator\",\n\t\t0x84E9 : \"ImageColorValue\",\n\t\t0x84EA : \"BackgroundColorValue\",\n\t\t0x84EB : \"PixelIntensityRange\",\n\t\t0x84EC : \"TransparencyIndicator\",\n\t\t0x84ED : \"ColorCharacterization\",\n\t\t0x84EE : \"HCUsage\",\n\t\t0x84EF : \"TrapIndicator\",\n\t\t0x84F0 : \"CMYKEquivalent\",\n\t\t0x8546 : \"SEMInfo\",\n\t\t0x8568 : \"AFCP_IPTC\",\n\t\t0x85B8 : \"PixelMagicJBIGOptions\",\n\t\t0x85D8 : \"ModelTransform\",\n\t\t0x8602 : \"WB_GRGBLevels\",\n\t\t0x8606 : \"LeafData\",\n\t\t0x8649 : \"PhotoshopSettings\",\n\t\t0x8769 : \"ExifOffset\",\n\t\t0x8773 : \"ICC_Profile\",\n\t\t0x877F : \"TIFF_FXExtensions\",\n\t\t0x8780 : \"MultiProfiles\",\n\t\t0x8781 : \"SharedData\",\n\t\t0x8782 : \"T88Options\",\n\t\t0x87AC : \"ImageLayer\",\n\t\t0x87AF : \"GeoTiffDirectory\",\n\t\t0x87B0 : \"GeoTiffDoubleParams\",\n\t\t0x87B1 : \"GeoTiffAsciiParams\",\n\t\t0x8822 : \"ExposureProgram\",\n\t\t0x8824 : \"SpectralSensitivity\",\n\t\t0x8825 : \"GPSInfo\",\n\t\t0x8827 : \"ISO\",\n\t\t0x8828 : \"Opto-ElectricConvFactor\",\n\t\t0x8829 : \"Interlace\",\n\t\t0x882A : \"TimeZoneOffset\",\n\t\t0x882B : \"SelfTimerMode\",\n\t\t0x8830 : \"SensitivityType\",\n\t\t0x8831 : \"StandardOutputSensitivity\",\n\t\t0x8832 : \"RecommendedExposureIndex\",\n\t\t0x8833 : \"ISOSpeed\",\n\t\t0x8834 : \"ISOSpeedLatitudeyyy\",\n\t\t0x8835 : \"ISOSpeedLatitudezzz\",\n\t\t0x885C : \"FaxRecvParams\",\n\t\t0x885D : \"FaxSubAddress\",\n\t\t0x885E : \"FaxRecvTime\",\n\t\t0x888A : \"LeafSubIFD\",\n\t\t0x9000 : \"ExifVersion\",\n\t\t0x9003 : \"DateTimeOriginal\",\n\t\t0x9004 : \"CreateDate\",\n\t\t0x9101 : \"ComponentsConfiguration\",\n\t\t0x9102 : \"CompressedBitsPerPixel\",\n\t\t0x9201 : \"ShutterSpeedValue\",\n\t\t0x9202 : \"ApertureValue\",\n\t\t0x9203 : \"BrightnessValue\",\n\t\t0x9204 : \"ExposureCompensation\",\n\t\t0x9205 : \"MaxApertureValue\",\n\t\t0x9206 : \"SubjectDistance\",\n\t\t0x9207 : \"MeteringMode\",\n\t\t0x9208 : \"LightSource\",\n\t\t0x9209 : \"Flash\",\n\t\t0x920A : \"FocalLength\",\n\t\t0x920B : \"FlashEnergy\",\n\t\t0x920C : \"SpatialFrequencyResponse\",\n\t\t0x920D : \"Noise\",\n\t\t0x920E : \"FocalPlaneXResolution\",\n\t\t0x920F : \"FocalPlaneYResolution\",\n\t\t0x9210 : \"FocalPlaneResolutionUnit\",\n\t\t0x9211 : \"ImageNumber\",\n\t\t0x9212 : \"SecurityClassification\",\n\t\t0x9213 : \"ImageHistory\",\n\t\t0x9214 : \"SubjectArea\",\n\t\t0x9215 : \"ExposureIndex\",\n\t\t0x9216 : \"TIFF-EPStandardID\",\n\t\t0x9217 : \"SensingMethod\",\n\t\t0x923A : \"CIP3DataFile\",\n\t\t0x923B : \"CIP3Sheet\",\n\t\t0x923C : \"CIP3Side\",\n\t\t0x923F : \"StoNits\",\n\t\t0x927C : \"MakerNote\",\n\t\t0x9286 : \"UserComment\",\n\t\t0x9290 : \"SubSecTime\",\n\t\t0x9291 : \"SubSecTimeOriginal\",\n\t\t0x9292 : \"SubSecTimeDigitized\",\n\t\t0x932F : \"MSDocumentText\",\n\t\t0x9330 : \"MSPropertySetStorage\",\n\t\t0x9331 : \"MSDocumentTextPosition\",\n\t\t0x935C : \"ImageSourceData\",\n\t\t0x9C9B : \"XPTitle\",\n\t\t0x9C9C : \"XPComment\",\n\t\t0x9C9D : \"XPAuthor\",\n\t\t0x9C9E : \"XPKeywords\",\n\t\t0x9C9F : \"XPSubject\",\n\t\t0xA000 : \"FlashpixVersion\",\n\t\t0xA001 : \"ColorSpace\",\n\t\t0xA002 : \"ExifImageWidth\",\n\t\t0xA003 : \"ExifImageHeight\",\n\t\t0xA004 : \"RelatedSoundFile\",\n\t\t0xA005 : \"InteropOffset\",\n\t\t0xA20B : \"FlashEnergy\",\n\t\t0xA20C : \"SpatialFrequencyResponse\",\n\t\t0xA20D : \"Noise\",\n\t\t0xA20E : \"FocalPlaneXResolution\",\n\t\t0xA20F : \"FocalPlaneYResolution\",\n\t\t0xA210 : \"FocalPlaneResolutionUnit\",\n\t\t0xA211 : \"ImageNumber\",\n\t\t0xA212 : \"SecurityClassification\",\n\t\t0xA213 : \"ImageHistory\",\n\t\t0xA214 : \"SubjectLocation\",\n\t\t0xA215 : \"ExposureIndex\",\n\t\t0xA216 : \"TIFF-EPStandardID\",\n\t\t0xA217 : \"SensingMethod\",\n\t\t0xA300 : \"FileSource\",\n\t\t0xA301 : \"SceneType\",\n\t\t0xA302 : \"CFAPattern\",\n\t\t0xA401 : \"CustomRendered\",\n\t\t0xA402 : \"ExposureMode\",\n\t\t0xA403 : \"WhiteBalance\",\n\t\t0xA404 : \"DigitalZoomRatio\",\n\t\t0xA405 : \"FocalLengthIn35mmFormat\",\n\t\t0xA406 : \"SceneCaptureType\",\n\t\t0xA407 : \"GainControl\",\n\t\t0xA408 : \"Contrast\",\n\t\t0xA409 : \"Saturation\",\n\t\t0xA40A : \"Sharpness\",\n\t\t0xA40B : \"DeviceSettingDescription\",\n\t\t0xA40C : \"SubjectDistanceRange\",\n\t\t0xA420 : \"ImageUniqueID\",\n\t\t0xA430 : \"OwnerName\",\n\t\t0xA431 : \"SerialNumber\",\n\t\t0xA432 : \"LensInfo\",\n\t\t0xA433 : \"LensMake\",\n\t\t0xA434 : \"LensModel\",\n\t\t0xA435 : \"LensSerialNumber\",\n\t\t0xA480 : \"GDALMetadata\",\n\t\t0xA481 : \"GDALNoData\",\n\t\t0xA500 : \"Gamma\",\n\t\t0xAFC0 : \"ExpandSoftware\",\n\t\t0xAFC1 : \"ExpandLens\",\n\t\t0xAFC2 : \"ExpandFilm\",\n\t\t0xAFC3 : \"ExpandFilterLens\",\n\t\t0xAFC4 : \"ExpandScanner\",\n\t\t0xAFC5 : \"ExpandFlashLamp\",\n\t\t0xBC01 : \"PixelFormat\",\n\t\t0xBC02 : \"Transformation\",\n\t\t0xBC03 : \"Uncompressed\",\n\t\t0xBC04 : \"ImageType\",\n\t\t0xBC80 : \"ImageWidth\",\n\t\t0xBC81 : \"ImageHeight\",\n\t\t0xBC82 : \"WidthResolution\",\n\t\t0xBC83 : \"HeightResolution\",\n\t\t0xBCC0 : \"ImageOffset\",\n\t\t0xBCC1 : \"ImageByteCount\",\n\t\t0xBCC2 : \"AlphaOffset\",\n\t\t0xBCC3 : \"AlphaByteCount\",\n\t\t0xBCC4 : \"ImageDataDiscard\",\n\t\t0xBCC5 : \"AlphaDataDiscard\",\n\t\t0xC427 : \"OceScanjobDesc\",\n\t\t0xC428 : \"OceApplicationSelector\",\n\t\t0xC429 : \"OceIDNumber\",\n\t\t0xC42A : \"OceImageLogic\",\n\t\t0xC44F : \"Annotations\",\n\t\t0xC4A5 : \"PrintIM\",\n\t\t0xC580 : \"USPTOOriginalContentType\",\n\t\t0xC612 : \"DNGVersion\",\n\t\t0xC613 : \"DNGBackwardVersion\",\n\t\t0xC614 : \"UniqueCameraModel\",\n\t\t0xC615 : \"LocalizedCameraModel\",\n\t\t0xC616 : \"CFAPlaneColor\",\n\t\t0xC617 : \"CFALayout\",\n\t\t0xC618 : \"LinearizationTable\",\n\t\t0xC619 : \"BlackLevelRepeatDim\",\n\t\t0xC61A : \"BlackLevel\",\n\t\t0xC61B : \"BlackLevelDeltaH\",\n\t\t0xC61C : \"BlackLevelDeltaV\",\n\t\t0xC61D : \"WhiteLevel\",\n\t\t0xC61E : \"DefaultScale\",\n\t\t0xC61F : \"DefaultCropOrigin\",\n\t\t0xC620 : \"DefaultCropSize\",\n\t\t0xC621 : \"ColorMatrix1\",\n\t\t0xC622 : \"ColorMatrix2\",\n\t\t0xC623 : \"CameraCalibration1\",\n\t\t0xC624 : \"CameraCalibration2\",\n\t\t0xC625 : \"ReductionMatrix1\",\n\t\t0xC626 : \"ReductionMatrix2\",\n\t\t0xC627 : \"AnalogBalance\",\n\t\t0xC628 : \"AsShotNeutral\",\n\t\t0xC629 : \"AsShotWhiteXY\",\n\t\t0xC62A : \"BaselineExposure\",\n\t\t0xC62B : \"BaselineNoise\",\n\t\t0xC62C : \"BaselineSharpness\",\n\t\t0xC62D : \"BayerGreenSplit\",\n\t\t0xC62E : \"LinearResponseLimit\",\n\t\t0xC62F : \"CameraSerialNumber\",\n\t\t0xC630 : \"DNGLensInfo\",\n\t\t0xC631 : \"ChromaBlurRadius\",\n\t\t0xC632 : \"AntiAliasStrength\",\n\t\t0xC633 : \"ShadowScale\",\n\t\t0xC634 : \"DNGPrivateData\",\n\t\t0xC635 : \"MakerNoteSafety\",\n\t\t0xC640 : \"RawImageSegmentation\",\n\t\t0xC65A : \"CalibrationIlluminant1\",\n\t\t0xC65B : \"CalibrationIlluminant2\",\n\t\t0xC65C : \"BestQualityScale\",\n\t\t0xC65D : \"RawDataUniqueID\",\n\t\t0xC660 : \"AliasLayerMetadata\",\n\t\t0xC68B : \"OriginalRawFileName\",\n\t\t0xC68C : \"OriginalRawFileData\",\n\t\t0xC68D : \"ActiveArea\",\n\t\t0xC68E : \"MaskedAreas\",\n\t\t0xC68F : \"AsShotICCProfile\",\n\t\t0xC690 : \"AsShotPreProfileMatrix\",\n\t\t0xC691 : \"CurrentICCProfile\",\n\t\t0xC692 : \"CurrentPreProfileMatrix\",\n\t\t0xC6BF : \"ColorimetricReference\",\n\t\t0xC6D2 : \"PanasonicTitle\",\n\t\t0xC6D3 : \"PanasonicTitle2\",\n\t\t0xC6F3 : \"CameraCalibrationSig\",\n\t\t0xC6F4 : \"ProfileCalibrationSig\",\n\t\t0xC6F5 : \"ProfileIFD\",\n\t\t0xC6F6 : \"AsShotProfileName\",\n\t\t0xC6F7 : \"NoiseReductionApplied\",\n\t\t0xC6F8 : \"ProfileName\",\n\t\t0xC6F9 : \"ProfileHueSatMapDims\",\n\t\t0xC6FA : \"ProfileHueSatMapData1\",\n\t\t0xC6FB : \"ProfileHueSatMapData2\",\n\t\t0xC6FC : \"ProfileToneCurve\",\n\t\t0xC6FD : \"ProfileEmbedPolicy\",\n\t\t0xC6FE : \"ProfileCopyright\",\n\t\t0xC714 : \"ForwardMatrix1\",\n\t\t0xC715 : \"ForwardMatrix2\",\n\t\t0xC716 : \"PreviewApplicationName\",\n\t\t0xC717 : \"PreviewApplicationVersion\",\n\t\t0xC718 : \"PreviewSettingsName\",\n\t\t0xC719 : \"PreviewSettingsDigest\",\n\t\t0xC71A : \"PreviewColorSpace\",\n\t\t0xC71B : \"PreviewDateTime\",\n\t\t0xC71C : \"RawImageDigest\",\n\t\t0xC71D : \"OriginalRawFileDigest\",\n\t\t0xC71E : \"SubTileBlockSize\",\n\t\t0xC71F : \"RowInterleaveFactor\",\n\t\t0xC725 : \"ProfileLookTableDims\",\n\t\t0xC726 : \"ProfileLookTableData\",\n\t\t0xC740 : \"OpcodeList1\",\n\t\t0xC741 : \"OpcodeList2\",\n\t\t0xC74E : \"OpcodeList3\",\n\t\t0xC761 : \"NoiseProfile\",\n\t\t0xC763 : \"TimeCodes\",\n\t\t0xC764 : \"FrameRate\",\n\t\t0xC772 : \"TStop\",\n\t\t0xC789 : \"ReelName\",\n\t\t0xC791 : \"OriginalDefaultFinalSize\",\n\t\t0xC792 : \"OriginalBestQualitySize\",\n\t\t0xC793 : \"OriginalDefaultCropSize\",\n\t\t0xC7A1 : \"CameraLabel\",\n\t\t0xC7A3 : \"ProfileHueSatMapEncoding\",\n\t\t0xC7A4 : \"ProfileLookTableEncoding\",\n\t\t0xC7A5 : \"BaselineExposureOffset\",\n\t\t0xC7A6 : \"DefaultBlackRender\",\n\t\t0xC7A7 : \"NewRawImageDigest\",\n\t\t0xC7A8 : \"RawToPreviewGain\",\n\t\t0xC7B5 : \"DefaultUserCrop\",\n\t\t0xEA1C : \"Padding\",\n\t\t0xEA1D : \"OffsetSchema\",\n\t\t0xFDE8 : \"OwnerName\",\n\t\t0xFDE9 : \"SerialNumber\",\n\t\t0xFDEA : \"Lens\",\n\t\t0xFE00 : \"KDC_IFD\",\n\t\t0xFE4C : \"RawFile\",\n\t\t0xFE4D : \"Converter\",\n\t\t0xFE4E : \"WhiteBalance\",\n\t\t0xFE51 : \"Exposure\",\n\t\t0xFE52 : \"Shadows\",\n\t\t0xFE53 : \"Brightness\",\n\t\t0xFE54 : \"Contrast\",\n\t\t0xFE55 : \"Saturation\",\n\t\t0xFE56 : \"Sharpness\",\n\t\t0xFE57 : \"Smoothness\",\n\t\t0xFE58 : \"MoireFilter\"\n\t\t\n\t},\n\tgps : {\t\n\t\t0x0000 : 'GPSVersionID',\n\t\t0x0001 : 'GPSLatitudeRef',\n\t\t0x0002 : 'GPSLatitude',\n\t\t0x0003 : 'GPSLongitudeRef',\n\t\t0x0004 : 'GPSLongitude',\n\t\t0x0005 : 'GPSAltitudeRef',\n\t\t0x0006 : 'GPSAltitude',\n\t\t0x0007 : 'GPSTimeStamp',\n\t\t0x0008 : 'GPSSatellites',\n\t\t0x0009 : 'GPSStatus',\n\t\t0x000A : 'GPSMeasureMode',\n\t\t0x000B : 'GPSDOP',\n\t\t0x000C : 'GPSSpeedRef',\n\t\t0x000D : 'GPSSpeed',\n\t\t0x000E : 'GPSTrackRef',\n\t\t0x000F : 'GPSTrack',\n\t\t0x0010 : 'GPSImgDirectionRef',\n\t\t0x0011 : 'GPSImgDirection',\n\t\t0x0012 : 'GPSMapDatum',\n\t\t0x0013 : 'GPSDestLatitudeRef',\n\t\t0x0014 : 'GPSDestLatitude',\n\t\t0x0015 : 'GPSDestLongitudeRef',\n\t\t0x0016 : 'GPSDestLongitude',\n\t\t0x0017 : 'GPSDestBearingRef',\n\t\t0x0018 : 'GPSDestBearing',\n\t\t0x0019 : 'GPSDestDistanceRef',\n\t\t0x001A : 'GPSDestDistance',\n\t\t0x001B : 'GPSProcessingMethod',\n\t\t0x001C : 'GPSAreaInformation',\n\t\t0x001D : 'GPSDateStamp',\n\t\t0x001E : 'GPSDifferential',\n\t\t0x001F : 'GPSHPositioningError'\n\t}\n};", "/*jslint browser: true, devel: true, bitwise: false, debug: true, eqeq: false, es5: true, evil: false, forin: false, newcap: false, nomen: true, plusplus: true, regexp: false, unparam: false, sloppy: true, stupid: false, sub: false, todo: true, vars: true, white: true */\n\nvar jpeg = require('./jpeg'),\n\texif = require('./exif'),\n\tsimplify = require('./simplify');\n\nfunction ExifResult(startMarker, tags, imageSize, thumbnailOffset, thumbnailLength, thumbnailType, app1Offset) {\n\tthis.startMarker = startMarker;\n\tthis.tags = tags;\n\tthis.imageSize = imageSize;\n\tthis.thumbnailOffset = thumbnailOffset;\n\tthis.thumbnailLength = thumbnailLength;\n\tthis.thumbnailType = thumbnailType;\n\tthis.app1Offset = app1Offset;\n}\n\nExifResult.prototype = {\n\thasThumbnail: function(mime) {\n\t\tif(!this.thumbnailOffset || !this.thumbnailLength) {\n\t\t\treturn false;\n\t\t}\n\t\tif(typeof mime !== 'string') {\n\t\t\treturn true;\n\t\t}\n\t\tif(mime.toLowerCase().trim() === 'image/jpeg') {\n\t\t\treturn this.thumbnailType === 6;\n\t\t}\n\t\tif(mime.toLowerCase().trim() === 'image/tiff') {\n\t\t\treturn this.thumbnailType === 1;\n\t\t}\n\t\treturn false;\n\t},\n\tgetThumbnailOffset: function() {\n\t\treturn this.app1Offset + 6 + this.thumbnailOffset;\n\t},\n\tgetThumbnailLength: function() {\n\t\treturn this.thumbnailLength;\n\t},\n\tgetThumbnailBuffer: function() {\n\t\treturn this._getThumbnailStream().nextBuffer(this.thumbnailLength);\n\t},\n\t_getThumbnailStream: function() {\n\t\treturn this.startMarker.openWithOffset(this.getThumbnailOffset());\n\t},\n\tgetImageSize: function() {\n\t\treturn this.imageSize;\n\t},\n\tgetThumbnailSize: function() {\n\t\tvar stream = this._getThumbnailStream(), size;\n\t\tjpeg.parseSections(stream, function(sectionType, sectionStream) {\n\t\t\tif(jpeg.getSectionName(sectionType).name === 'SOF') {\n\t\t\t\tsize = jpeg.getSizeFromSOFSection(sectionStream);\n\t\t\t}\n\t\t});\n\t\treturn size;\n\t}\n};\n\nfunction Parser(stream) {\n\tthis.stream = stream;\n\tthis.flags = {\n\t\treadBinaryTags: false,\n\t\tresolveTagNames: true,\n\t\tsimplifyValues: true,\n\t\timageSize: true,\n\t\thidePointers: true,\n\t\treturnTags: true\n\t};\n}\n\nParser.prototype = {\n\tenableBinaryFields: function(enable) {\n\t\tthis.flags.readBinaryTags = !!enable;\n\t\treturn this;\n\t},\n\tenablePointers: function(enable) {\n\t\tthis.flags.hidePointers = !enable;\n\t\treturn this;\n\t},\n\tenableTagNames: function(enable) {\n\t\tthis.flags.resolveTagNames = !!enable;\n\t\treturn this;\n\t},\n\tenableImageSize: function(enable) {\n\t\tthis.flags.imageSize = !!enable;\n\t\treturn this;\n\t},\n\tenableReturnTags: function(enable) {\n\t\tthis.flags.returnTags = !!enable;\n\t\treturn this;\n\t},\n\tenableSimpleValues: function(enable) {\n\t\tthis.flags.simplifyValues = !!enable;\n\t\treturn this;\n\t},\n\tparse: function() {\n\t\tvar start = this.stream.mark(),\n\t\t\tstream = start.openWithOffset(0),\n\t\t\tflags = this.flags,\n\t\t\ttags,\n\t\t\timageSize,\n\t\t\tthumbnailOffset,\n\t\t\tthumbnailLength,\n\t\t\tthumbnailType,\n\t\t\tapp1Offset,\n\t\t\ttagNames,\n\t\t\tgetTagValue, setTagValue;\n\t\tif(flags.resolveTagNames) {\n\t\t\ttagNames = require('./exif-tags');\n\t\t}\n\t\tif(flags.resolveTagNames) {\n\t\t\ttags = {};\n\t\t\tgetTagValue = function(t) {\n\t\t\t\treturn tags[t.name];\n\t\t\t};\n\t\t\tsetTagValue = function(t, value) {\n\t\t\t\ttags[t.name] = value;\n\t\t\t};\n\t\t} else {\n\t\t\ttags = [];\n\t\t\tgetTagValue = function(t) {\n\t\t\t\tvar i;\n\t\t\t\tfor(i = 0; i < tags.length; ++i) {\n\t\t\t\t\tif(tags[i].type === t.type && tags[i].section === t.section) {\n\t\t\t\t\t\treturn tags.value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tsetTagValue = function(t, value) {\n\t\t\t\tvar i;\n\t\t\t\tfor(i = 0; i < tags.length; ++i) {\n\t\t\t\t\tif(tags[i].type === t.type && tags[i].section === t.section) {\n\t\t\t\t\t\ttags.value = value;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\n\t\tjpeg.parseSections(stream, function(sectionType, sectionStream) {\n\t\t\tvar validExifHeaders, sectionOffset = sectionStream.offsetFrom(start);\n\t\t\tif(sectionType === 0xE1) {\n\t\t\t\tvalidExifHeaders = exif.parseTags(sectionStream, function(ifdSection, tagType, value, format) {\n\t\t\t\t\t//ignore binary fields if disabled\n\t\t\t\t\tif(!flags.readBinaryTags && format === 7) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif(tagType === 0x0201) {\n\t\t\t\t\t\tthumbnailOffset = value[0];\n\t\t\t\t\t\tif(flags.hidePointers) {return;}\n\t\t\t\t\t} else if(tagType === 0x0202) {\n\t\t\t\t\t\tthumbnailLength = value[0];\n\t\t\t\t\t\tif(flags.hidePointers) {return;}\n\t\t\t\t\t} else if(tagType === 0x0103) {\n\t\t\t\t\t\tthumbnailType = value[0];\n\t\t\t\t\t\tif(flags.hidePointers) {return;}\n\t\t\t\t\t}\n\t\t\t\t\t//if flag is set to not store tags, return here after storing pointers\n\t\t\t\t\tif(!flags.returnTags) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif(flags.simplifyValues) {\n\t\t\t\t\t\tvalue = simplify.simplifyValue(value, format);\n\t\t\t\t\t}\n\t\t\t\t\tif(flags.resolveTagNames) {\n\t\t\t\t\t\tvar sectionTagNames = ifdSection === exif.GPSIFD ? tagNames.gps : tagNames.exif;\n\t\t\t\t\t\tvar name = sectionTagNames[tagType];\n\t\t\t\t\t\tif(!name) {\n\t\t\t\t\t\t\tname = tagNames.exif[tagType];\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttags[name] = value;\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttags.push({\n\t\t\t\t\t\t\tsection: ifdSection,\n\t\t\t\t\t\t\ttype: tagType,\n\t\t\t\t\t\t\tvalue: value\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tif(validExifHeaders) {\n\t\t\t\t\tapp1Offset = sectionOffset;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if(flags.imageSize && jpeg.getSectionName(sectionType).name === 'SOF') {\n\t\t\t\timageSize = jpeg.getSizeFromSOFSection(sectionStream);\n\t\t\t}\n\t\t});\n\n\t\tif(flags.simplifyValues) {\n\t\t\tsimplify.castDegreeValues(getTagValue, setTagValue);\n\t\t\tsimplify.castDateValues(getTagValue, setTagValue);\n\t\t}\n\n\t\treturn new ExifResult(start, tags, imageSize, thumbnailOffset, thumbnailLength, thumbnailType, app1Offset);\n\t}\n};\n\n\n\nmodule.exports = Parser;", "/*jslint browser: true, devel: true, bitwise: false, debug: true, eqeq: false, es5: true, evil: false, forin: false, newcap: false, nomen: true, plusplus: true, regexp: false, unparam: false, sloppy: true, stupid: false, sub: false, todo: true, vars: true, white: true */\n\nfunction DOMBufferStream(arrayBuffer, offset, length, bigEndian, global, parentOffset) {\n\tthis.global = global;\n\toffset = offset || 0;\n\tlength = length || (arrayBuffer.byteLength - offset);\n\tthis.arrayBuffer = arrayBuffer.slice(offset, offset + length);\n\tthis.view = new global.DataView(this.arrayBuffer, 0, this.arrayBuffer.byteLength);\n\tthis.setBigEndian(bigEndian);\n\tthis.offset = 0;\n\tthis.parentOffset = (parentOffset || 0) + offset;\n}\n\nDOMBufferStream.prototype = {\n\tsetBigEndian: function(bigEndian) {\n\t\tthis.littleEndian = !bigEndian;\n\t},\n\tnextUInt8: function() {\n\t\tvar value = this.view.getUint8(this.offset);\n\t\tthis.offset += 1;\n\t\treturn value;\n\t},\n\tnextInt8: function() {\n\t\tvar value = this.view.getInt8(this.offset);\n\t\tthis.offset += 1;\n\t\treturn value;\n\t},\n\tnextUInt16: function() {\n\t\tvar value = this.view.getUint16(this.offset, this.littleEndian);\n\t\tthis.offset += 2;\n\t\treturn value;\n\t},\n\tnextUInt32: function() {\n\t\tvar value = this.view.getUint32(this.offset, this.littleEndian);\n\t\tthis.offset += 4;\n\t\treturn value;\n\t},\n\tnextInt16: function() {\n\t\tvar value = this.view.getInt16(this.offset, this.littleEndian);\n\t\tthis.offset += 2;\n\t\treturn value;\n\t},\n\tnextInt32: function() {\n\t\tvar value = this.view.getInt32(this.offset, this.littleEndian);\n\t\tthis.offset += 4;\n\t\treturn value;\n\t},\n\tnextFloat: function() {\n\t\tvar value = this.view.getFloat32(this.offset, this.littleEndian);\n\t\tthis.offset += 4;\n\t\treturn value;\n\t},\n\tnextDouble: function() {\n\t\tvar value = this.view.getFloat64(this.offset, this.littleEndian);\n\t\tthis.offset += 8;\n\t\treturn value;\n\t},\n\tnextBuffer: function(length) {\n\t\t//this won't work in IE10\n\t\tvar value = this.arrayBuffer.slice(this.offset, this.offset + length);\n\t\tthis.offset += length;\n\t\treturn value;\n\t},\n\tremainingLength: function() {\n\t\treturn this.arrayBuffer.byteLength - this.offset;\n\t},\n\tnextString: function(length) {\n\t\tvar value = this.arrayBuffer.slice(this.offset, this.offset + length);\n\t\tvalue = String.fromCharCode.apply(null, new this.global.Uint8Array(value));\n\t\tthis.offset += length;\n\t\treturn value;\n\t},\n\tmark: function() {\n\t\tvar self = this;\n\t\treturn {\n\t\t\topenWithOffset: function(offset) {\n\t\t\t\toffset = (offset || 0) + this.offset;\n\t\t\t\treturn new DOMBufferStream(self.arrayBuffer, offset, self.arrayBuffer.byteLength - offset, !self.littleEndian, self.global, self.parentOffset);\n\t\t\t},\n\t\t\toffset: this.offset,\n\t\t\tgetParentOffset: function() {\n\t\t\t\treturn self.parentOffset;\n\t\t\t}\n\t\t};\n\t},\n\toffsetFrom: function(marker) {\n\t\treturn this.parentOffset + this.offset - (marker.offset + marker.getParentOffset());\n\t},\n\tskip: function(amount) {\n\t\tthis.offset += amount;\n\t},\n\tbranch: function(offset, length) {\n\t\tlength = typeof length === 'number' ? length : this.arrayBuffer.byteLength - (this.offset + offset);\n\t\treturn new DOMBufferStream(this.arrayBuffer, this.offset + offset, length, !this.littleEndian, this.global, this.parentOffset);\n\t}\n};\n\nmodule.exports = DOMBufferStream;\n", "function BufferStream(buffer, offset, length, bigEndian) {\n\tthis.buffer = buffer;\n\tthis.offset = offset || 0;\n\tlength = typeof length === 'number' ? length : buffer.length;\n\tthis.endPosition = this.offset + length;\n\tthis.setBigEndian(bigEndian);\n}\n\nBufferStream.prototype = {\n\tsetBigEndian: function(bigEndian) {\n\t\tthis.bigEndian = !!bigEndian;\n\t},\n\tnextUInt8: function() {\n\t\tvar value = this.buffer.readUInt8(this.offset);\n\t\tthis.offset += 1;\n\t\treturn value;\n\t},\n\tnextInt8: function() {\n\t\tvar value = this.buffer.readInt8(this.offset);\n\t\tthis.offset += 1;\n\t\treturn value;\n\t},\n\tnextUInt16: function() {\n\t\tvar value = this.bigEndian ? this.buffer.readUInt16BE(this.offset) : this.buffer.readUInt16LE(this.offset);\n\t\tthis.offset += 2;\n\t\treturn value;\n\t},\n\tnextUInt32: function() {\n\t\tvar value = this.bigEndian ? this.buffer.readUInt32BE(this.offset) : this.buffer.readUInt32LE(this.offset);\n\t\tthis.offset += 4;\n\t\treturn value;\n\t},\n\tnextInt16: function() {\n\t\tvar value = this.bigEndian ? this.buffer.readInt16BE(this.offset) : this.buffer.readInt16LE(this.offset);\n\t\tthis.offset += 2;\n\t\treturn value;\n\t},\n\tnextInt32: function() {\n\t\tvar value = this.bigEndian ? this.buffer.readInt32BE(this.offset) : this.buffer.readInt32LE(this.offset);\n\t\tthis.offset += 4;\n\t\treturn value;\n\t},\n\tnextFloat: function() {\n\t\tvar value = this.bigEndian ? this.buffer.readFloatBE(this.offset) : this.buffer.readFloatLE(this.offset);\n\t\tthis.offset += 4;\n\t\treturn value;\n\t},\n\tnextDouble: function() {\n\t\tvar value = this.bigEndian ? this.buffer.readDoubleBE(this.offset) : this.buffer.readDoubleLE(this.offset);\n\t\tthis.offset += 8;\n\t\treturn value;\n\t},\n\tnextBuffer: function(length) {\n\t\tvar value = this.buffer.slice(this.offset, this.offset + length);\n\t\tthis.offset += length;\n\t\treturn value;\n\t},\n\tremainingLength: function() {\n\t\treturn this.endPosition - this.offset;\n\t},\n\tnextString: function(length) {\n\t\tvar value = this.buffer.toString('ascii', this.offset, this.offset + length);\n\t\tthis.offset += length;\n\t\treturn value;\n\t},\n\tmark: function() {\n\t\tvar self = this;\n\t\treturn {\n\t\t\topenWithOffset: function(offset) {\n\t\t\t\toffset = (offset || 0) + this.offset;\n\t\t\t\treturn new BufferStream(self.buffer, offset, self.endPosition - offset, self.bigEndian);\n\t\t\t},\n\t\t\toffset: this.offset\n\t\t};\n\t},\n\toffsetFrom: function(marker) {\n\t\treturn this.offset - marker.offset;\n\t},\n\tskip: function(amount) {\n\t\tthis.offset += amount;\n\t},\n\tbranch: function(offset, length) {\n\t\tlength = typeof length === 'number' ? length : this.endPosition - (this.offset + offset);\n\t\treturn new BufferStream(this.buffer, this.offset + offset, length, this.bigEndian);\n\t}\n};\n\nmodule.exports = BufferStream;\n", "var Parser = require('./lib/parser');\n\nfunction getGlobal() {\n\treturn this;\n}\n\nmodule.exports = {\n\tcreate: function(buffer, global) {\n\t\tglobal = global || getGlobal();\n\t\tif(buffer instanceof global.ArrayBuffer) {\n\t\t\tvar DOMBufferStream = require('./lib/dom-bufferstream');\n\t\t\treturn new Parser(new DOMBufferStream(buffer, 0, buffer.byteLength, true, global));\n\t\t} else {\n\t\t\tvar NodeBufferStream = require('./lib/bufferstream');\n\t\t\treturn new Parser(new NodeBufferStream(buffer, 0, buffer.length, true));\n\t\t}\n\t}\n};", "require('./toolbars.css');\nimport * as dompack from 'dompack';\n\nclass ToolbarButton\n{\n constructor(toolbar, options)\n {\n this.toolbar = toolbar;\n this.options =\n { label: null\n , classnames: null\n , hint: null\n , icon: null\n , enabled: true\n , pressed: false\n , ...options\n };\n\n this.node = dompack.create(\"div\",{ className: [\"wh-toolbar-button\"].concat(this.options.classnames || []).join(\" \")\n , on: { \"click\": this.executeAction.bind(this) }\n , title: this.options.hint || \"\"\n });\n if (this.options.icon)\n {\n this.options.icon.classList.add(\"wh-toolbar-button-img\");\n this.node.appendChild(this.options.icon);\n }\n if (this.options.label)\n this.node.appendChild(dompack.create(\"span\", { \"textContent\": this.options.label }));\n if (!this.options.enabled)\n this.node.classList.add(\"disabled\");\n if (this.options.pressed)\n this.node.classList.add(\"pressed\");\n\n if (this.options.onExecute)\n this.toElement().addEventListener(\"execute\", this.options.onExecute);\n }\n\n toElement()\n {\n return this.node;\n }\n\n executeAction()\n {\n if (this.options.enabled)\n dompack.dispatchCustomEvent(this.toElement(), \"execute\", { bubbles: false, cancelable: false });\n }\n\n setEnabled(enabled)\n {\n enabled = !!enabled;\n if (enabled != this.options.enabled)\n {\n this.options.enabled = enabled;\n dompack.toggleClasses(this.node, { disabled: !this.options.enabled });\n }\n }\n\n setPressed(pressed)\n {\n pressed = !!pressed;\n if (pressed != this.options.pressed)\n {\n this.options.pressed = pressed;\n dompack.toggleClasses(this.node, { pressed: this.options.pressed });\n }\n }\n}\n\nclass ToolbarSeparator extends ToolbarButton\n{\n constructor(toolbar, options)\n {\n super(toolbar, options);\n this.node = dompack.create(\"div\",{\"className\":[\"wh-toolbar-separator\"].concat(this.options.classnames || []).join(\" \")});\n }\n}\n\nclass ToolbarPanel\n{\n constructor(options)\n {\n this.options = { ...options };\n this.panel = dompack.create(\"div\",{\"className\":\"wh-toolbar-panel open\"});\n\n if (this.options.onClose)\n this.toElement().addEventListener(\"close\", this.options.onClose);\n if (this.options.onApply)\n this.toElement().addEventListener(\"apply\", this.options.onApply);\n }\n\n toElement()\n {\n return this.panel;\n }\n\n addButton(button)\n {\n if(typeof button != 'object')\n throw new Error(\"Specify explicit element to addButton\"); //might have sneaked through when we did $(button)\n this.addComponent(button);\n }\n\n addComponent(comp)\n {\n this.panel.appendChild(comp.toElement());\n }\n}\n\nclass Toolbar\n{\n constructor(options)\n {\n this.modalpanel = null;\n this.options =\n { applyicon: null\n , applylabel: \"Apply\"\n , closeicon: null\n , closelabel: \"Revert\"\n , classnames: null\n , ...options\n };\n\n this.buttonbar = dompack.create(\"div\",{ className: [\"wh-toolbar\"].concat(this.options.classnames || []).join(\" \")\n });\n\n this.mainpanel = new ToolbarPanel();\n this.buttonbar.appendChild(this.mainpanel.toElement());\n\n this.modalholder = dompack.create(\"div\", { className: \"wh-toolbar-modalholder\" });\n this.buttonbar.appendChild(this.modalholder);\n\n var modalbuttons = dompack.create(\"div\", { className: \"wh-toolbar-modalbuttons\" });\n this.modalholder.append(modalbuttons);\n\n var button = dompack.create(\"div\", { className: \"wh-toolbar-button wh-toolbar-button-applymodal\"\n , on: { \"click\": this.onModalApply.bind(this) }\n });\n modalbuttons.append(button);\n if (this.options.applyicon)\n {\n this.options.applyicon.classList.add(\"wh-toolbar-button-img\");\n button.appendChild(this.options.applyicon);\n }\n if (this.options.applylabel)\n button.appendChild(dompack.create(\"span\", { textContent: this.options.applylabel }));\n\n button = dompack.create(\"div\", { className: \"wh-toolbar-button wh-toolbar-button-revertmodal\"\n , on: { \"click\": this.onModalCancel.bind(this) }\n });\n modalbuttons.append(button);\n if (this.options.closeicon)\n {\n this.options.closeicon.classList.add(\"wh-toolbar-button-img\");\n button.appendChild(this.options.closeicon);\n }\n if (this.options.closelabel)\n button.appendChild(dompack.create(\"span\", { textContent: this.options.closelabel }));\n }\n\n toElement()\n {\n return this.buttonbar;\n }\n\n setSize(width, height)\n {\n Object.assign(this.buttonbar.style,\n { width: width + \"px\"\n , height: height + \"px\"\n });\n }\n\n addButton(button)\n {\n this.mainpanel.addButton(button);\n }\n\n addComponent(comp)\n {\n this.mainpanel.addComponent(comp);\n }\n\n activateModalPanel(subpanel)\n {\n if(this.modalpanel)\n this.closeModalPanel();\n\n this.mainpanel.toElement().classList.remove('open');\n this.modalpanel = subpanel;\n this.modalholder.appendChild(this.modalpanel.panel);\n this.modalholder.classList.add('open');\n dompack.dispatchCustomEvent(\n this.toElement(),\n \"modal-opened\",\n { bubbles: false\n , cancelable: false\n , detail:\n { apply: this.onModalApply.bind(this)\n , cancel: this.onModalCancel.bind(this)\n , panel: subpanel\n }\n });\n }\n\n closeModalPanel()\n {\n if(!this.modalpanel)\n return;\n\n dompack.dispatchCustomEvent(this.modalpanel.toElement(), \"close\", { bubbles: false, cancelable: false });\n this.mainpanel.toElement().classList.add('open');\n this.modalholder.classList.remove('open');\n this.modalholder.removeChild(this.modalpanel.panel);\n this.modalpanel = null;\n dompack.dispatchCustomEvent(this.toElement(), \"modal-closed\", { bubbles: false, cancelable: false });\n }\n\n onModalApply()\n {\n dompack.dispatchCustomEvent(this.modalpanel.toElement(), \"apply\", { bubbles: false, cancelable: false });\n this.closeModalPanel();\n }\n\n onModalCancel()\n {\n dompack.dispatchCustomEvent(this.modalpanel.toElement(), \"cancel\", { bubbles: false, cancelable: false });\n this.closeModalPanel();\n }\n}\n\nToolbar.Button = ToolbarButton;\nToolbar.Panel = ToolbarPanel;\nToolbar.Separator = ToolbarSeparator;\n\nmodule.exports = Toolbar;\n", "import * as dompack from 'dompack';\nvar Toolbar = require('../toolbar/toolbars');\nvar getTid = require(\"@mod-tollium/js/gettid\").getTid;\nrequire(\"./imageeditor.lang.json\");\nvar toddImages = require(\"@mod-tollium/js/icons\");\n\n//image canvas\nclass ImageSurface\n{\n constructor(imgeditornode, toolbar, options)\n {\n this.imgeditornode = imgeditornode;\n this.container = null;\n this.img = null;\n this.imgdata = {};\n this.viewport = null;\n this.canvasdata = {};\n this.canvas = null;\n this.previewcanvas= null;\n this.canvasscale= 1;\n this.previewscale= 1;\n this.imagelimited= false;\n this.ctx = null;\n this.refpoint = null; // { x= 0, y= 0 }\n this.orgrefpoint= null; // initial reference point, used to reset refpoint on undo\n this.undostack = []; //contains all steps done\n this.redostack = []; //contains all steps undone\n this.undobutton = null;\n this.redobutton = null;\n this.busylock = null;\n this.options = { getBusyLock: null\n , editorBackground: \"\"\n , maxLength: 0\n , maxArea: 0\n , ...options\n };\n\n this.container =
\n {this.canvas = }\n {this.maskcanvas = }\n
\n if (this.options.editorBackground)\n this.container.style.background = this.options.editorBackground;\n }\n fireEvent(name, detail)\n {\n dompack.dispatchCustomEvent(this.imgeditornode, 'tollium-imageeditor:' + name, { bubbles:true, cancelable:false, detail});\n }\n toElement()\n {\n return this.container;\n }\n setSize(w,h)\n {\n dompack.setStyles(this.container, { width: w, height: h });\n if (this.ctx)\n {\n this.viewport = { x: w, y: h };\n this.setupCanvas();\n this.fireEvent(\"resized\", { width: w\n , height: h\n });\n }\n }\n setImg(img, settings)\n {\n this.orgrefpoint = settings.refpoint;\n\n this.undostack = [];\n this.redostack = [];\n if (this.undobutton)\n this.undobutton.setEnabled(false);\n if (this.redobutton)\n this.redobutton.setEnabled(false);\n\n let containersize = this.container.getBoundingClientRect();\n this.viewport = { x: containersize.width, y: containersize.height };\n this.setupFromImage(img, settings.orientation);\n\n this.ctx = this.canvas.getContext(\"2d\");\n\n this.setupCanvas();\n this.fireEvent('ready',this.imgdata);\n }\n\n // Are there changes?\n isDirty()\n {\n return this.undostack.length > 0;\n }\n\n // Are there image data modifying changes?\n isModified()\n {\n // Returns true if there is at least one image modifying state on the undo stack\n return this.undostack.findIndex(function(state)\n {\n return !state.meta;\n }) >= 0;\n }\n\n setBusy(busy)\n {\n if (!this.options.getBusyLock)\n return true; // No busy lock available\n // If busylock exists, don't accept 'true' as it's already busy, and vice versa\n if ((this.busylock !== null) == busy)\n return false; // Already busy\n\n if (busy)\n {\n this.busylock = this.options.getBusyLock();\n }\n else\n {\n if (this.busylock)\n this.busylock.release();\n this.busylock = null;\n }\n return true;\n }\n\n stop()\n {\n }\n\n reduceActions(cursteps)\n {\n //ADDME: more reduction if possible\n\n var steps = [];\n\n/*\nMerge:\n-Same sequentially actions to one step\n-Orientation if no cropping in between\n-Scale if no cropping in between\n*/\n var stepindex = -1;\n var stepaction = \"\";\n for(var c = 0; c < cursteps.length; c++)\n {\n if (stepaction != cursteps[c].action)\n {\n stepindex = -1;\n stepaction = cursteps[c].action;\n }\n if(cursteps[c].action == 'crop')\n {\n if(stepindex > -1)\n {\n var w = steps[stepindex].props.crop[1] - steps[stepindex].props.crop[3];\n var h = steps[stepindex].props.crop[2] - steps[stepindex].props.crop[0];\n\n steps[stepindex].props.crop[0]+=h*cursteps[c].props.crop[0]; //top\n steps[stepindex].props.crop[1]*=cursteps[c].props.crop[1]; //right\n steps[stepindex].props.crop[2]*=cursteps[c].props.crop[2]; //bottom\n steps[stepindex].props.crop[3]+=w*cursteps[c].props.crop[3]; //left\n }\n else\n {\n stepindex = steps.length;\n steps.push(cursteps[c]);\n }\n }\n else if(cursteps[c].action == 'scale')\n {\n if(stepindex > -1)\n {\n steps[stepindex].props.scale.x*=cursteps[c].props.scale.x;\n steps[stepindex].props.scale.y*=cursteps[c].props.scale.y;\n }\n else\n {\n stepindex = steps.length;\n steps.push(cursteps[c]);\n }\n }\n else if(cursteps[c].action == 'rotate')\n {\n if(stepindex > -1)\n {\n steps[stepindex].props.angle+=cursteps[c].props.angle;\n steps[stepindex].props.angle-=Math.floor(steps[stepindex].props.angle / 360) * 360;//keep range between 0 and 360\n steps[stepindex].props.scale.x*=cursteps[c].props.scale.x;\n steps[stepindex].props.scale.y*=cursteps[c].props.scale.y;\n }\n else\n {\n stepindex = steps.length;\n steps.push(cursteps[c]);\n }\n }\n else if(cursteps[c].action == 'filters')\n {\n if(stepindex > -1)\n {\n steps[stepindex].props.data=cursteps[c].props.data;\n }\n else\n {\n stepindex = steps.length;\n steps.push(cursteps[c]);\n }\n }\n else if(cursteps[c].action == 'refpoint')\n {\n if(stepindex > -1)\n {\n steps[stepindex].props.refpoint=cursteps[c].props.refpoint;\n }\n else\n {\n stepindex = steps.length;\n steps.push(cursteps[c]);\n }\n }\n }\n\n return steps;\n }\n\n setupFromImage(img, orientation)\n {\n var width = img.width;\n var height = img.height;\n\n // Restrict image width and height\n if (this.options.maxLength > 0 && (width > this.options.maxLength || height > this.options.maxLength))\n {\n let s = this.options.maxLength / Math.max(width, height);\n width = Math.floor(width * s);\n height = Math.floor(height * s);\n this.imagelimited = true;\n }\n // Restrict image area\n if (this.options.maxArea && width * height > this.options.maxArea)\n {\n let s = Math.sqrt(this.options.maxArea / (width * height));\n width = Math.floor(width * s);\n height = Math.floor(height * s);\n this.imagelimited = true;\n }\n if (this.imagelimited)\n console.warn(\"Restricting image dimensions from \" + img.width + \"x\" + img.height + \" to \" + width + \"x\" + height);\n\n orientation = orientation || 0;\n var rotated = [ 5, 6, 7, 8 ].includes(orientation);\n var scale = { 'x': 1, 'y': 1 };//use separate scale x/y for error reduction rounding\n var orgsize = { 'x': rotated ? height : width, 'y': rotated ? width : height };\n\n this.img = img;\n this.imgdata = { 'size' : { 'x': rotated ? height : width, 'y': rotated ? width : height }\n , 'scale' : scale\n , 'orgsize' : orgsize\n , 'aspect' : (orgsize.x / orgsize.y)\n , 'orientation': orientation\n };\n }\n\n setupCanvas()\n {\n this.refpoint = this.orgrefpoint;\n this.canvas.width = this.imgdata.size.x;\n this.canvas.height = this.imgdata.size.y;\n this.maskcanvas.width = this.viewport.x;\n this.maskcanvas.height = this.viewport.y;\n\n //what scale to use to fit image on canvas in current position\n var canvasscalex = this.canvas.width / this.viewport.x;\n var canvasscaley = this.canvas.height / this.viewport.y;\n var canvasscale = canvasscalex > canvasscaley ? canvasscalex : canvasscaley;\n if(canvasscale < 1)\n canvasscale = 1;//don't scale up\n this.canvasscale = 1 / canvasscale;\n\n var cssw = Math.round(this.canvas.width / canvasscale);\n var cssh = Math.round(this.canvas.height / canvasscale);\n this.canvasdata = { 'csssize' : {'x' : cssw, 'y' : cssh}\n , 'scale' : {'x' : (this.canvas.width/cssw), 'y' : (this.canvas.height/cssh)}\n , 'realsize': {'x' : this.imgdata.orgsize.x, 'y' : this.imgdata.orgsize.y}\n };\n\n dompack.setStyles(this.canvas, { 'position' : 'absolute'\n , 'top' : '50%'\n , 'left' : '50%'\n , 'width' : this.canvasdata.csssize.x + 'px'\n , 'height' : this.canvasdata.csssize.y + 'px'\n , 'margin-left': Math.ceil(this.canvasdata.csssize.x*-0.5) + 'px'\n , 'margin-top' : Math.ceil(this.canvasdata.csssize.y*-0.5) + 'px'\n });\n\n var drawwidth = this.imgdata.size.x;\n var drawheight = this.imgdata.size.y;\n if ([ 5, 6, 7, 8 ].includes(this.imgdata.orientation))\n {\n var tmp = drawwidth;\n drawwidth = drawheight;\n drawheight = tmp;\n }\n // See: http://stackoverflow.com/a/6010475\n switch (this.imgdata.orientation)\n {\n case 1: // rotated 0\u00B0, not mirrored\n break;\n case 2: // rotated 0\u00B0, mirrored\n this.ctx.scale(-1, 1);\n this.ctx.translate(-drawwidth, 0);\n break;\n case 3: // rotated 180\u00B0, not mirrored\n this.ctx.translate(drawwidth, drawheight);\n this.ctx.rotate(Math.PI);\n break;\n case 4: // rotated 180\u00B0, mirrored\n this.ctx.scale(1, -1);\n this.ctx.translate(0, -drawheight);\n break;\n case 5: // rotated 270\u00B0, mirrored\n this.ctx.rotate(-Math.PI / 2);\n this.ctx.scale(-1, 1);\n break;\n case 6: // rotated 270\u00B0, not mirrored\n this.ctx.translate(drawheight, 0);\n this.ctx.rotate(Math.PI / 2);\n break;\n case 7: // rotated 90\u00B0, mirrored\n this.ctx.scale(-1, 1);\n this.ctx.translate(-drawheight, drawwidth);\n this.ctx.rotate(3 * Math.PI / 2);\n break;\n case 8: // rotated 90\u00B0, not mirrored\n this.ctx.translate(0, drawwidth);\n this.ctx.rotate(3 * Math.PI / 2);\n break;\n }\n this.ctx.drawImage(this.img, 0, 0, drawwidth, drawheight);\n this.showScale();\n this.fireEvent('reset');\n }\n\n setPreviewCanvas(canvas, contentRect)\n {\n var oldcanvas = this.previewcanvas;\n if (this.previewcanvas)\n {\n this.hidePreviewCanvas();\n this.previewcanvas.remove();\n this.previewcanvas = null;\n this.previewscale = 1;\n }\n if (canvas)\n {\n this.previewcanvas = canvas;\n this.previewrect = contentRect || { left: 0\n , top: 0\n , width: this.previewcanvas.width\n , height: this.previewcanvas.height\n , offsetx: 0\n , offsety: 0\n };\n if (this.previewrect.width > this.viewport.x || this.previewrect.height > this.viewport.y)\n {\n this.previewscale = Math.min(this.viewport.x / this.previewrect.width, this.viewport.y / this.previewrect.height);\n this.previewcanvas.style.transform = \"scale(\" + this.previewscale + \")\";\n }\n else\n {\n this.previewscale = 1;\n this.previewcanvas.style.transform = \"\";\n }\n\n var left = Math.floor((this.viewport.x - this.previewcanvas.width) / 2) - Math.floor(this.previewscale * this.previewrect.offsetx);\n var top = Math.floor((this.viewport.y - this.previewcanvas.height) / 2) - Math.floor(this.previewscale * this.previewrect.offsety);\n this.previewcanvas.style.marginLeft = left + \"px\";\n this.previewcanvas.style.marginTop = top + \"px\";\n\n this.previewmask = { left: left + Math.floor(this.previewrect.left * this.previewscale) + Math.floor((this.previewcanvas.width - this.previewscale * this.previewcanvas.width) / 2)\n , top: top + Math.floor(this.previewrect.top * this.previewscale) + Math.floor((this.previewcanvas.height - this.previewscale * this.previewcanvas.height) / 2)\n , width: Math.round(this.previewrect.width * this.previewscale)\n , height: Math.round(this.previewrect.height * this.previewscale)\n };\n this.fireEvent(\"updatepreview\", { oldcanvas: oldcanvas });\n this.showPreviewCanvas();\n }\n }\n updateMaskCanvas(contentRect)\n {\n contentRect = contentRect || { left: Math.floor((this.maskcanvas.width - this.canvasdata.csssize.x) / 2)\n , top: Math.floor((this.maskcanvas.height - this.canvasdata.csssize.y) / 2)\n , width: Math.round(this.canvasdata.csssize.x)\n , height: Math.round(this.canvasdata.csssize.y)\n };\n var ctx = this.maskcanvas.getContext(\"2d\");\n // Clear the mask\n ctx.clearRect(0, 0, this.maskcanvas.width, this.maskcanvas.height);\n // Fill with transparent black\n ctx.fillStyle = \"rgba(0, 0, 0, .6)\";\n ctx.fillRect(0, 0, this.maskcanvas.width, this.maskcanvas.height);\n // Cut out the image rect, compensate for scaling\n ctx.clearRect(contentRect.left, contentRect.top, contentRect.width, contentRect.height);\n }\n showPreviewCanvas()\n {\n if (this.previewcanvas)\n {\n if (this.canvas.parentNode)\n this.container.removeChild(this.canvas);\n this.container.insertBefore(this.previewcanvas, this.container.firstChild);\n if (!this.maskcanvas.parentNode)\n this.container.appendChild(this.maskcanvas);\n else\n this.updateMaskCanvas(this.previewmask);\n this.fireEvent(\"showpreview\");\n }\n this.showScale();\n }\n hidePreviewCanvas(hidemask)\n {\n if (this.previewcanvas)\n {\n this.fireEvent(\"hidepreview\");\n this.container.removeChild(this.previewcanvas);\n this.container.insertBefore(this.canvas, this.container.firstChild);\n if (hidemask)\n this.container.removeChild(this.maskcanvas);\n else\n this.updateMaskCanvas();\n this.showScale(this.canvasscale);\n }\n }\n\n showScale(scale)\n {\n this.hideScale();\n if (!scale)\n scale = this.previewcanvas ? this.previewscale : this.canvasscale;\n this.container.appendChild({Math.round(100 * scale) + \"%\"});\n this.scaletimeout = setTimeout(() => this.hideScale(), 2500);\n }\n\n hideScale()\n {\n clearTimeout(this.scaletimeout);\n dompack.qSA(this.container,\".wh-imageeditor-scale\").forEach(node => node.remove());\n }\n\n apply()\n {\n\n }\n\n pushUndo(state, replace_same_action)\n {\n // If pushing the same action, replace the previous state if the redo stack is empty\n if (replace_same_action\n && this.undostack.length\n && !this.redostack.length\n && this.undostack[this.undostack.length - 1].action == state.action)\n this.undostack[this.undostack.length - 1] = state;\n else\n this.undostack.push(state);\n this.redostack = [];\n if (this.undobutton)\n this.undobutton.setEnabled(true);\n if (this.redobutton)\n this.redobutton.setEnabled(false);\n }\n\n popUndo()\n {\n if(this.undostack.length === 0)\n return;\n\n // Remove last action from undo stack and push it to redo stack\n this.redostack.push(this.undostack.pop());\n if (this.undobutton)\n this.undobutton.setEnabled(this.undostack.length > 0);\n if (this.redobutton)\n this.redobutton.setEnabled(true);\n\n // Restore original\n this.setupCanvas();\n\n // Reconstruct previous actions with minimum steps\n this.reduceActions(this.undostack).forEach(step =>\n {\n step.comp.applyCanvas(step.props);\n });\n\n this.fireEvent(\"undo\");\n }\n\n popRedo()\n {\n if(this.redostack.length === 0)\n return;\n\n // Remove last action from redo stack and push it to undo stack\n this.undostack.push(this.redostack.pop());\n if (this.redobutton)\n this.redobutton.setEnabled(this.redostack.length > 0);\n if (this.undobutton)\n this.undobutton.setEnabled(true);\n\n // Restore original\n this.setupCanvas();\n\n // Reconstruct previous actions with minimum steps\n this.reduceActions(this.undostack).forEach(step =>\n {\n step.comp.applyCanvas(step.props);\n });\n\n this.fireEvent(\"redo\");\n }\n\n cloneCanvas(options)\n {\n console.log('Copying canvas');\n var copy = document.createElement(\"canvas\");\n copy.width = this.canvas.width;\n copy.height = this.canvas.height;\n\n var ctx = copy.getContext('2d');\n ctx.drawImage(this.canvas, 0, 0);\n\n if (options && options.clearoriginal)\n {\n console.log('Clearing original');\n this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n return { canvas: copy, ctx: ctx };\n }\n}\n\nImageSurface.addUndoButton = function(toolbar, surface, options)\n{\n var button = new Toolbar.Button(toolbar,\n { label: getTid(\"tollium:common.actions.undo\")\n , icon: toddImages.createImage(\"tollium:actions/undo\", 24, 24, \"b\")\n , onExecute: surface.popUndo.bind(surface)\n , enabled: false\n });\n toolbar.addButton(button);\n surface.undobutton = button;\n return { button: button };\n};\n\nImageSurface.addRedoButton = function(toolbar, surface, options)\n{\n var button = new Toolbar.Button(toolbar,\n { label: getTid(\"tollium:common.actions.redo\")\n , icon: toddImages.createImage(\"tollium:actions/redo\", 24, 24, \"b\")\n , onExecute: surface.popRedo.bind(surface)\n , enabled: false\n });\n toolbar.addButton(button);\n surface.redobutton = button;\n return { button: button };\n};\n\nmodule.exports = ImageSurface;\n", "import { dispatchCustomEvent, stop } from '../src/events.es';\n\n// Store data about the current move\nlet moveeventdata = null;\nlet lastcoordinates = null;\n\n// Fire a move event and return the resulting event\nfunction fireMoveEvent(eventtype, listener, event, cancelable)\n{\n //FIXME properly wait for 'last' touch when multitouchmoving ?\n\n listener = moveeventdata ? moveeventdata.listener : listener;\n let originaltarget = moveeventdata ? moveeventdata.target : event.target;\n let coordinatesource = event.type == \"touchmove\" ? event.touches[0] : event.type == \"touchend\" ? lastcoordinates : event;\n if(event.type == \"touchmove\")\n lastcoordinates = cloneCoordinates(coordinatesource);\n\n let movedX = moveeventdata ? coordinatesource.clientX - moveeventdata.startX : 0;\n let movedY = moveeventdata ? coordinatesource.clientY - moveeventdata.startY : 0;\n\n let eventdata = { movedX: movedX, movedY: movedY\n , pageX: coordinatesource.pageX, pageY: coordinatesource.pageY\n , clientX: coordinatesource.clientX, clientY: coordinatesource.clientY\n , screenX: coordinatesource.screenX, screenY: coordinatesource.screenY\n , listener: listener\n , currentTarget: event.target\n };\n\n return dispatchCustomEvent(originaltarget, eventtype, { detail: eventdata, cancelable: cancelable, bubbles: true });\n}\n\n// Activate the global mouse handlers and store the original event target (the 'movable' element) and start position to\n// calculate mouse movement\nfunction startMove(listener, target, startX, startY)\n{\n moveeventdata = { listener, target, startX, startY };\n window.addEventListener(\"mousemove\", moveMouseMove, true);\n window.addEventListener(\"mouseup\", moveMouseUp, true);\n window.addEventListener(\"touchmove\", moveMouseMove, true);\n window.addEventListener(\"touchend\", moveMouseUp, true);\n}\n\n// Deactivate the global mouse handlers and remove stored move data\nfunction stopMove()\n{\n moveeventdata = null;\n window.removeEventListener(\"mousemove\", moveMouseMove, true);\n window.removeEventListener(\"mouseup\", moveMouseUp, true);\n window.removeEventListener(\"touchmove\", moveMouseMove, true);\n window.removeEventListener(\"touchend\", moveMouseUp, true);\n}\n\nfunction cloneCoordinates(coordinatesource)\n{\n return { pageX: coordinatesource.pageX\n , pageY: coordinatesource.pageY\n , clientX: coordinatesource.clientX\n , clientY: coordinatesource.clientY\n , screenX: coordinatesource.screenX\n , screenY: coordinatesource.screenY\n };\n}\n\n// Handle a mousedown event on a movable element\nfunction moveMouseDown(event)\n{\n if (event.button) //not the main button\n return;\n\n // Start the move by firing the movestart event\n if(!fireMoveEvent(\"dompack:movestart\", this, event, true))\n return;\n\n // Start the move action\n let coordinatesource = event.touches ? event.touches[0] : event;\n lastcoordinates = cloneCoordinates(coordinatesource);\n startMove(this, event.target, coordinatesource.clientX, coordinatesource.clientY);\n\n // Prevent default to prevent selecting text or click\n stop(event);\n}\n\n// Handle a (global) mousemove event\nfunction moveMouseMove(event)\n{\n // Check if we have data (we should have, but check just in case)\n if (moveeventdata)\n {\n stop(event);\n // Fire the move event on the original target, use the current event target as relatedTarget\n fireMoveEvent(\"dompack:move\", null, event, false);\n }\n\n}\n\n// Handle a (global) mouseup event\nfunction moveMouseUp(event)\n{\n // Check if we have data (we should have, but check just in case)\n if (moveeventdata)\n {\n stop(event);\n fireMoveEvent(\"dompack:moveend\", null, event, false);\n }\n // We're done, stop the move action\n stopMove();\n}\n\nexport function enable(el)\n{\n el.addEventListener(\"mousedown\", moveMouseDown);\n el.addEventListener(\"touchstart\", moveMouseDown);\n}\nexport function disable(el)\n{\n el.removeEventListener(\"mousedown\", moveMouseDown);\n el.removeEventListener(\"touchstart\", moveMouseDown);\n}\n\nexport function cancelMove()\n{\n if (moveeventdata)\n stopMove();\n}\n", "export class SurfaceTool\n{\n constructor(surface)\n {\n this.surface = surface;\n }\n\n refreshSurface()\n {\n this.surface.fireEvent(\"refresh\");\n }\n}\n", "/* eslint-disable */\n/** smart-crop.js\n * A javascript library implementing content aware image cropping\n *\n * Copyright (C) 2014 Jonas Wagner\n *\n * Permission is hereby granted, free of charge, to any person obtaining\n * a copy of this software and associated documentation files (the\n * \"Software\"), to deal in the Software without restriction, including\n * without limitation the rights to use, copy, modify, merge, publish,\n * distribute, sublicense, and/or sell copies of the Software, and to\n * permit persons to whom the Software is furnished to do so, subject to\n * the following conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n(function(){\n\"use strict\";\n\nfunction SmartCrop(options){\n this.options = extend({}, SmartCrop.DEFAULTS, options);\n}\nSmartCrop.DEFAULTS = {\n width: 0,\n height: 0,\n aspect: 0,\n cropWidth: 0,\n cropHeight: 0,\n detailWeight: 0.2,\n skinColor: [0.78, 0.57, 0.44],\n skinBias: 0.01,\n skinBrightnessMin: 0.2,\n skinBrightnessMax: 1.0,\n skinThreshold: 0.8,\n skinWeight: 1.8,\n saturationBrightnessMin: 0.05,\n saturationBrightnessMax: 0.9,\n saturationThreshold: 0.4,\n saturationBias: 0.2,\n saturationWeight: 0.3,\n // step * minscale rounded down to the next power of two should be good\n scoreDownSample: 8,\n step: 8,\n scaleStep: 0.1,\n minScale: 0.9,\n maxScale: 1.0,\n edgeRadius: 0.4,\n edgeWeight: -20.0,\n outsideImportance: -0.5,\n ruleOfThirds: true,\n prescale: true,\n canvasFactory: null,\n debug: false\n};\nSmartCrop.crop = function(image, options, callback){\n if(options.aspect){\n options.width = options.aspect;\n options.height = 1;\n }\n\n // work around images scaled in css by drawing them onto a canvas\n if(image.naturalWidth && (image.naturalWidth != image.width || image.naturalHeight != image.height)){\n var c = new SmartCrop(options).canvas(image.naturalWidth, image.naturalHeight),\n cctx = c.getContext('2d');\n c.width = image.naturalWidth;\n c.height = image.naturalHeight;\n cctx.drawImage(image, 0, 0);\n image = c;\n }\n\n var scale = 1,\n prescale = 1;\n if(options.width && options.height) {\n scale = min(image.width/options.width, image.height/options.height);\n options.cropWidth = ~~(options.width * scale);\n options.cropHeight = ~~(options.height * scale);\n // img = 100x100, width = 95x95, scale = 100/95, 1/scale > min\n // don't set minscale smaller than 1/scale\n // -> don't pick crops that need upscaling\n options.minScale = min(options.maxScale || SmartCrop.DEFAULTS.maxScale, max(1/scale, (options.minScale||SmartCrop.DEFAULTS.minScale)));\n }\n var smartCrop = new SmartCrop(options);\n if(options.width && options.height) {\n if(options.prescale !== false){\n prescale = 1/scale/options.minScale;\n if(prescale < 1) {\n var prescaledCanvas = smartCrop.canvas(image.width*prescale, image.height*prescale),\n ctx = prescaledCanvas.getContext('2d');\n ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, prescaledCanvas.width, prescaledCanvas.height);\n image = prescaledCanvas;\n smartCrop.options.cropWidth = ~~(options.cropWidth*prescale);\n smartCrop.options.cropHeight = ~~(options.cropHeight*prescale);\n }\n else {\n prescale = 1;\n }\n }\n }\n var result = smartCrop.analyse(image);\n for(var i = 0, i_len = result.crops.length; i < i_len; i++) {\n var crop = result.crops[i];\n crop.x = ~~(crop.x/prescale);\n crop.y = ~~(crop.y/prescale);\n crop.width = ~~(crop.width/prescale);\n crop.height = ~~(crop.height/prescale);\n }\n callback(result);\n return result;\n};\n// check if all the dependencies are there\nSmartCrop.isAvailable = function(options){\n try {\n var s = new this(options),\n c = s.canvas(16, 16);\n return typeof c.getContext === 'function';\n }\n catch(e){\n return false;\n }\n};\nSmartCrop.prototype = {\n canvas: function(w, h){\n if(this.options.canvasFactory !== null){\n return this.options.canvasFactory(w, h);\n }\n var c = document.createElement('canvas');\n c.width = w;\n c.height = h;\n return c;\n },\n edgeDetect: function(i, o){\n var id = i.data,\n od = o.data,\n w = i.width,\n h = i.height;\n for(var y = 0; y < h; y++) {\n for(var x = 0; x < w; x++) {\n var p = (y*w+x)*4,\n lightness;\n if(x === 0 || x >= w-1 || y === 0 || y >= h-1){\n lightness = sample(id, p);\n }\n else {\n lightness = sample(id, p)*4 - sample(id, p-w*4) - sample(id, p-4) - sample(id, p+4) - sample(id, p+w*4);\n }\n od[p+1] = lightness;\n }\n }\n },\n skinDetect: function(i, o){\n var id = i.data,\n od = o.data,\n w = i.width,\n h = i.height,\n options = this.options;\n for(var y = 0; y < h; y++) {\n for(var x = 0; x < w; x++) {\n var p = (y*w+x)*4,\n lightness = cie(id[p], id[p+1], id[p+2])/255,\n skin = this.skinColor(id[p], id[p+1], id[p+2]);\n if(skin > options.skinThreshold && lightness >= options.skinBrightnessMin && lightness <= options.skinBrightnessMax){\n od[p] = (skin-options.skinThreshold)*(255/(1-options.skinThreshold));\n }\n else {\n od[p] = 0;\n }\n }\n }\n },\n saturationDetect: function(i, o){\n var id = i.data,\n od = o.data,\n w = i.width,\n h = i.height,\n options = this.options;\n for(var y = 0; y < h; y++) {\n for(var x = 0; x < w; x++) {\n var p = (y*w+x)*4,\n lightness = cie(id[p], id[p+1], id[p+2])/255,\n sat = saturation(id[p], id[p+1], id[p+2]);\n if(sat > options.saturationThreshold && lightness >= options.saturationBrightnessMin && lightness <= options.saturationBrightnessMax){\n od[p+2] = (sat-options.saturationThreshold)*(255/(1-options.saturationThreshold));\n }\n else {\n od[p+2] = 0;\n }\n }\n }\n },\n crops: function(image){\n var crops = [],\n width = image.width,\n height = image.height,\n options = this.options,\n minDimension = min(width, height),\n cropWidth = options.cropWidth || minDimension,\n cropHeight = options.cropHeight || minDimension;\n for(var scale = options.maxScale; scale >= options.minScale; scale -= options.scaleStep){\n for(var y = 0; y+cropHeight*scale <= height; y+=options.step) {\n for(var x = 0; x+cropWidth*scale <= width; x+=options.step) {\n crops.push({\n x: x,\n y: y,\n width: cropWidth*scale,\n height: cropHeight*scale\n });\n }\n }\n }\n return crops;\n },\n score: function(output, crop){\n var score = {\n detail: 0,\n saturation: 0,\n skin: 0,\n total: 0\n },\n options = this.options,\n od = output.data,\n downSample = options.scoreDownSample,\n invDownSample = 1/downSample,\n outputHeightDownSample = output.height*downSample,\n outputWidthDownSample = output.width*downSample,\n outputWidth = output.width;\n for(var y = 0; y < outputHeightDownSample; y+=downSample) {\n for(var x = 0; x < outputWidthDownSample; x+=downSample) {\n var p = (~~(y*invDownSample)*outputWidth+~~(x*invDownSample))*4,\n importance = this.importance(crop, x, y),\n detail = od[p+1]/255;\n score.skin += od[p]/255*(detail+options.skinBias)*importance;\n score.detail += detail*importance;\n score.saturation += od[p+2]/255*(detail+options.saturationBias)*importance;\n }\n\n }\n score.total = (score.detail*options.detailWeight + score.skin*options.skinWeight + score.saturation*options.saturationWeight)/crop.width/crop.height;\n return score;\n },\n importance: function(crop, x, y){\n var options = this.options;\n\n if (crop.x > x || x >= crop.x+crop.width || crop.y > y || y >= crop.y+crop.height) return options.outsideImportance;\n x = (x-crop.x)/crop.width;\n y = (y-crop.y)/crop.height;\n var px = abs(0.5-x)*2,\n py = abs(0.5-y)*2,\n // distance from edge\n dx = Math.max(px-1.0+options.edgeRadius, 0),\n dy = Math.max(py-1.0+options.edgeRadius, 0),\n d = (dx*dx+dy*dy)*options.edgeWeight;\n var s = 1.41-sqrt(px*px+py*py);\n if(options.ruleOfThirds){\n s += (Math.max(0, s+d+0.5)*1.2)*(thirds(px)+thirds(py));\n }\n return s+d;\n },\n skinColor: function(r, g, b){\n var mag = sqrt(r*r+g*g+b*b),\n options = this.options,\n rd = (r/mag-options.skinColor[0]),\n gd = (g/mag-options.skinColor[1]),\n bd = (b/mag-options.skinColor[2]),\n d = sqrt(rd*rd+gd*gd+bd*bd);\n return 1-d;\n },\n analyse: function(image){\n var result = {},\n options = this.options,\n canvas = this.canvas(image.width, image.height),\n ctx = canvas.getContext('2d');\n ctx.drawImage(image, 0, 0);\n var input = ctx.getImageData(0, 0, canvas.width, canvas.height),\n output = ctx.getImageData(0, 0, canvas.width, canvas.height);\n this.edgeDetect(input, output);\n this.skinDetect(input, output);\n this.saturationDetect(input, output);\n\n var scoreCanvas = this.canvas(ceil(image.width/options.scoreDownSample), ceil(image.height/options.scoreDownSample)),\n scoreCtx = scoreCanvas.getContext('2d');\n\n ctx.putImageData(output, 0, 0);\n scoreCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, scoreCanvas.width, scoreCanvas.height);\n\n var scoreOutput = scoreCtx.getImageData(0, 0, scoreCanvas.width, scoreCanvas.height);\n\n var topScore = -Infinity,\n topCrop = null,\n crops = this.crops(image);\n\n for(var i = 0, i_len = crops.length; i < i_len; i++) {\n var crop = crops[i];\n crop.score = this.score(scoreOutput, crop);\n if(crop.score.total > topScore){\n topCrop = crop;\n topScore = crop.score.total;\n }\n\n }\n\n result.crops = crops;\n result.topCrop = topCrop;\n\n if(options.debug && topCrop){\n ctx.fillStyle = 'rgba(255, 0, 0, 0.1)';\n ctx.fillRect(topCrop.x, topCrop.y, topCrop.width, topCrop.height);\n for (var y = 0; y < output.height; y++) {\n for (var x = 0; x < output.width; x++) {\n var p = (y * output.width + x) * 4;\n var importance = this.importance(topCrop, x, y);\n if (importance > 0) {\n output.data[p + 1] += importance * 32;\n }\n\n if (importance < 0) {\n output.data[p] += importance * -64;\n }\n output.data[p + 3] = 255;\n }\n }\n ctx.putImageData(output, 0, 0);\n ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';\n ctx.strokeRect(topCrop.x, topCrop.y, topCrop.width, topCrop.height);\n result.debugCanvas = canvas;\n }\n return result;\n }\n};\n\n// aliases and helpers\nvar min = Math.min,\n max = Math.max,\n abs = Math.abs,\n ceil = Math.ceil,\n sqrt = Math.sqrt;\n\nfunction extend(o){\n for(var i = 1, i_len = arguments.length; i < i_len; i++) {\n var arg = arguments[i];\n if(arg){\n for(var name in arg){\n o[name] = arg[name];\n }\n }\n }\n return o;\n}\n\n// gets value in the range of [0, 1] where 0 is the center of the pictures\n// returns weight of rule of thirds [0, 1]\nfunction thirds(x){\n x = ((x-(1/3)+1.0)%2.0*0.5-0.5)*16;\n return Math.max(1.0-x*x, 0.0);\n}\n\nfunction cie(r, g, b){\n return 0.5126*b + 0.7152*g + 0.0722*r;\n}\nfunction sample(id, p) {\n return cie(id[p], id[p+1], id[p+2]);\n}\nfunction saturation(r, g, b){\n var maximum = max(r/255, g/255, b/255), minumum = min(r/255, g/255, b/255);\n if(maximum === minumum){\n return 0;\n }\n var l = (maximum + minumum) / 2,\n d = maximum-minumum;\n return l > 0.5 ? d/(2-maximum-minumum) : d/(maximum+minumum);\n}\n\n// amd\nif (typeof define !== 'undefined' && define.amd) define(function(){return SmartCrop;});\n//common js\nif (typeof exports !== 'undefined') exports.SmartCrop = SmartCrop;\n// browser\nelse if (typeof navigator !== 'undefined') window.SmartCrop = SmartCrop;\n// nodejs\nif (typeof module !== 'undefined') {\n module.exports = SmartCrop;\n}\n})();\n", "import * as dompack from 'dompack';\nimport * as movable from 'dompack/browserfix/movable';\n\nimport \"./imageeditor.lang.json\";\nimport { getTid } from \"@mod-tollium/js/gettid\";\nimport { SurfaceTool } from './surfacetool.es';\n\nvar Toolbar = require('../toolbar/toolbars');\nvar SmartCrop = require('./smartcrop.js');\nvar toddImages = require(\"@mod-tollium/js/icons\");\n\nclass PhotoCrop extends SurfaceTool\n{\n constructor(surface, options)\n {\n super(surface, options)\n\n this.crop = null;\n this.aspect = 0;\n this.draggers = [];\n this.masks = [];\n this.reference = null;\n this.cropbox = null;\n this.gridholder = null;\n this.gridanchor = null;\n this.active = false;\n this.fx = null;\n this.options = { fixedsize: null // { width: 0, height: 0 }\n , ratiosize: null // { width: 0, height: 0 }\n , setStatus: null\n , ...options\n };\n\n this.croppanel = new Toolbar.Panel(\n { onClose: this.stop.bind(this)\n , onApply: this.apply.bind(this)\n });\n this.croppanel._imgedittool = \"crop\";\n this.autobutton = new Toolbar.Button(this.croppanel,\n { label: getTid(\"tollium:components.imgedit.editor.smartcrop\")\n , icon: toddImages.createImage(\"tollium:actions/resetcrop\", 24, 24, \"b\")\n , onExecute: () => this.smartCrop()\n });\n this.croppanel.addButton(this.autobutton);\n }\n\n startCropping(toolbar)\n {\n toolbar.activateModalPanel(this.croppanel);\n this.surface.hidePreviewCanvas(true);\n this.start();\n }\n\n start()\n {\n this.active = false;\n this.fixedsize = this.options.fixedsize || { width: 0, height: 0 };\n\n var styles = this.surface.canvas.style.cssText;\n\n this.cropbox =
\n this.surface.container.append(this.cropbox);\n\n let canvaspos = this.surface.canvas.getBoundingClientRect();\n this.reference = { x: canvaspos.left, y: canvaspos.top };\n\n\n\n //viewport (used to display 33% grid)\n this.gridholder =
;\n this.cropbox.append(this.gridholder);\n movable.enable(this.gridholder);\n this.gridholder.addEventListener(\"dompack:move\", evt => this.onDragMove(this.gridholder, evt));\n this.gridholder.addEventListener(\"dompack:end\", evt => this.gridanchor = null);\n\n this.gridholder.append(
\n ,
\n ,
\n ,
);\n\n this.masksize = this.surface.container.getBoundingClientRect();\n\n //set draggers:\n this.draggers = [];\n this.masks = [];\n for( var c = 0; c < 4; c++ )\n {\n let dragger =
\n dragger.classList.add([\"wh-cropbox-dragger-nw\",\"wh-cropbox-dragger-sw\",\"wh-cropbox-dragger-ne\",\"wh-cropbox-dragger-se\"][c]);\n this.cropbox.append(dragger);\n this.draggers.push(dragger);\n\n var pos = { x : 0, y : 0 };\n if ( c == 1 )\n pos = { x : 0, y : this.surface.canvasdata.csssize.y };\n else if ( c == 2 )\n pos = { x : this.surface.canvasdata.csssize.x, y : 0 };\n else if ( c == 3)\n pos = { x : this.surface.canvasdata.csssize.x, y : this.surface.canvasdata.csssize.y };\n\n this.draggers[c].wh_pos = pos;\n dompack.setStyles(this.draggers[c], {'top' : pos.y + 'px', 'left' : pos.x + 'px'});\n\n movable.enable(this.draggers[c]);\n this.draggers[c].addEventListener(\"dompack:move\", evt => this.onDragMove(dragger, evt));\n\n\n var mask =
\n this.cropbox.append(mask);\n this.masks.push(mask);\n }\n\n //initial crop values\n this.crop = [0,1,1,0];\n\n this.setAspectratio(this.options.ratiosize, function()\n {\n this.active = true;\n }.bind(this));\n }\n\n onDragMove(dragnode,ev)\n {\n var c;\n var movegrid = dragnode.classList.contains('wh-cropbox-viewport');\n if(movegrid)\n {//get upperleft dragger as reference for grid movement\n dragnode = this.draggers[0];\n for(c = 1; c < this.draggers.length; c++)\n {\n if(this.draggers[c].wh_pos.x < dragnode.wh_pos.x)\n dragnode = this.draggers[c];\n else if(this.draggers[c].wh_pos.y < dragnode.wh_pos.y)\n dragnode = this.draggers[c];\n }\n\n if(!this.gridanchor)//mouse snap position relative to upperleft dragger\n this.gridanchor = { x : dragnode.wh_pos.x - (ev.detail.pageX - this.reference.x)\n , y : dragnode.wh_pos.y - (ev.detail.pageY - this.reference.y)\n , width : this.crop[1] - this.crop[3]\n , height : this.crop[2] - this.crop[0]\n };\n }\n else\n {\n this.gridanchor = null;\n }\n\n //css w/h canvas\n var w = this.crop[1]*this.surface.canvasdata.csssize.x - this.crop[3]*this.surface.canvasdata.csssize.x;\n var h = this.crop[2]*this.surface.canvasdata.csssize.y - this.crop[0]*this.surface.canvasdata.csssize.y;\n\n //mouse position relative to upperleft viewport\n var dx = ev.detail.pageX - this.reference.x;\n var dy = ev.detail.pageY - this.reference.y;\n\n if(this.gridanchor)\n {//if moving whole clipbox, compensate mouse position with (start) grab position\n dx+=this.gridanchor.x;\n dy+=this.gridanchor.y;\n }\n\n //some bounds checks:\n if(dx < 0)\n dx = 0;\n else if(dx > this.surface.canvasdata.csssize.x)\n dx = this.surface.canvasdata.csssize.x;\n\n if(dy < 0)\n dy = 0;\n else if(dy > this.surface.canvasdata.csssize.y)\n dy = this.surface.canvasdata.csssize.y;\n\n //sortout dragnodes in respect to current dragnode\n var hpairednode = null;\n var vpairednode = null;\n var diagonalnode = null;\n for(c = 0; c < this.draggers.length; c++)\n {\n if(this.draggers[c] != dragnode)\n {\n if(!hpairednode && this.draggers[c].wh_pos.y == dragnode.wh_pos.y && this.draggers[c].wh_pos.x != dragnode.wh_pos.x)\n {\n hpairednode = this.draggers[c];\n }\n else if(!vpairednode && this.draggers[c].wh_pos.x == dragnode.wh_pos.x && this.draggers[c].wh_pos.y != dragnode.wh_pos.y)\n {\n vpairednode = this.draggers[c];\n }\n else if(!diagonalnode)\n {\n diagonalnode = this.draggers[c];\n }\n }\n }\n\n if(!hpairednode || !vpairednode)\n {//draggers have all the same position\n hpairednode = null;//reset\n vpairednode = null;\n diagonalnode = null;\n //assign directly:\n for(c = 0; c < this.draggers.length; c++)\n {\n if(this.draggers[c] != dragnode)\n {\n if(!hpairednode)\n hpairednode = this.draggers[c];\n else if(!vpairednode)\n vpairednode = this.draggers[c];\n else if(!diagonalnode)\n diagonalnode = this.draggers[c];\n }\n }\n }\n\n\n if(!movegrid && this.aspect > 0 && !(this.fixedsize.width > 0 || this.fixedsize.height > 0))\n {\n //use smallest displacement voor ratio correction\n if(Math.abs(dx - dragnode.wh_pos.x) < Math.abs(dy - dragnode.wh_pos.y))\n {\n w = Math.abs(dx - hpairednode.wh_pos.x);\n h = w/this.aspect;\n\n if(dy < vpairednode.wh_pos.y)\n dy = vpairednode.wh_pos.y - h;\n else if(dy > vpairednode.wh_pos.y)\n dy = vpairednode.wh_pos.y + h;\n\n if(dy < 0)\n dy = 0;\n else if(dy > this.surface.canvasdata.csssize.y)\n dy = this.surface.canvasdata.csssize.y;\n }\n else\n {\n h = Math.abs(dy - vpairednode.wh_pos.y);\n w = h*this.aspect;\n\n if(dx < hpairednode.wh_pos.x)\n dx = hpairednode.wh_pos.x - w;\n else if(dx > hpairednode.wh_pos.x)\n dx = hpairednode.wh_pos.x + w;\n\n if(dx < 0)\n dx = 0;\n else if(dx > this.surface.canvasdata.csssize.x)\n dx = this.surface.canvasdata.csssize.x;\n }\n }\n\n dragnode.wh_pos = {x:Math.round(dx), y: Math.round(dy)};\n hpairednode.wh_pos.y = Math.round(dy);\n vpairednode.wh_pos.x = Math.round(dx);\n\n if(movegrid)\n {//moveing clipbox, then keep orginal width/height\n hpairednode.wh_pos.x = Math.round(w + dx);\n vpairednode.wh_pos.y = Math.round(h + dy);\n }\n\n //handling of dragnodes if fixed width or height is given\n if(!movegrid && (this.fixedsize.width > 0 || this.fixedsize.height > 0))\n {\n var fixedw = this.fixedsize.width;\n var fixedh = this.fixedsize.height;\n if(this.aspect > 0)\n {\n if(fixedw <= 0)\n fixedw = fixedh * this.aspect;\n else if(fixedh <= 0)\n fixedh = fixedw / this.aspect;\n }\n\n if(fixedw > 0)\n {\n w = fixedw / (this.surface.canvasdata.scale.x * this.surface.imgdata.scale.x);\n\n if(hpairednode.wh_pos.x < dragnode.wh_pos.x)\n {\n //check bounds\n if(dragnode.wh_pos.x - w < 0)\n {\n dragnode.wh_pos.x = Math.round(w);\n vpairednode.wh_pos.x = dragnode.wh_pos.x;\n }\n hpairednode.wh_pos.x = Math.round(dragnode.wh_pos.x - w);\n }\n else\n {\n hpairednode.wh_pos.x = Math.round(dragnode.wh_pos.x + w);\n }\n\n }\n\n if(fixedh > 0)\n {\n h = fixedh / (this.surface.canvasdata.scale.y * this.surface.imgdata.scale.y);\n if(vpairednode.wh_pos.y < dragnode.wh_pos.y)\n {\n //check bounds\n if(dragnode.wh_pos.y - h < 0)\n {\n dragnode.wh_pos.y = Math.round(h);\n hpairednode.wh_pos.y = dragnode.wh_pos.y;\n }\n vpairednode.wh_pos.y = Math.round(dragnode.wh_pos.y - h);\n }\n else\n {\n vpairednode.wh_pos.y = Math.round(dragnode.wh_pos.y + h);\n }\n }\n\n }\n\n diagonalnode.wh_pos = {x:hpairednode.wh_pos.x, y: vpairednode.wh_pos.y};\n\n //sortout positions:\n var toppx = this.draggers[0].wh_pos.y;\n var rightpx = this.draggers[0].wh_pos.x;\n var bottompx= this.draggers[0].wh_pos.y;\n var leftpx = this.draggers[0].wh_pos.x;\n for(c = 1; c < this.draggers.length; c++)\n {\n if(this.draggers[c].wh_pos.x > rightpx)\n rightpx = this.draggers[c].wh_pos.x;\n\n if(this.draggers[c].wh_pos.x < leftpx)\n leftpx = this.draggers[c].wh_pos.x;\n\n if(this.draggers[c].wh_pos.y < toppx)\n toppx = this.draggers[c].wh_pos.y;\n\n if(this.draggers[c].wh_pos.y > bottompx)\n bottompx = this.draggers[c].wh_pos.y;\n }\n\n var d;\n //check if grid is within bounds else correct positions\n if(rightpx > this.surface.canvasdata.csssize.x)\n {\n d = this.surface.canvasdata.csssize.x - rightpx;\n rightpx+=d;\n leftpx+=d;\n\n for(c = 0; c < this.draggers.length; c++)\n this.draggers[c].wh_pos.x+=d;\n }\n if(bottompx > this.surface.canvasdata.csssize.y)\n {\n d = this.surface.canvasdata.csssize.y - bottompx;\n bottompx+=d;\n toppx+=d;\n\n for(c = 0; c < this.draggers.length; c++)\n this.draggers[c].wh_pos.y+=d;\n }\n\n if(rightpx > this.surface.canvasdata.csssize.x)\n rightpx = this.surface.canvasdata.csssize.x;\n if(leftpx < 0)\n leftpx = 0;\n\n if(bottompx > this.surface.canvasdata.csssize.y)\n bottompx = this.surface.canvasdata.csssize.y;\n if(toppx < 0)\n toppx = 0;\n\n this.crop[0] = toppx / this.surface.canvasdata.csssize.y;\n this.crop[1] = rightpx / this.surface.canvasdata.csssize.x;\n this.crop[2] = bottompx / this.surface.canvasdata.csssize.y;\n this.crop[3] = leftpx / this.surface.canvasdata.csssize.x;\n\n //reduce rounding errors of crop size:\n if(this.fixedsize.width > 0)\n this.crop[1] = this.crop[3] + (this.fixedsize.width / this.surface.canvasdata.realsize.x);\n if(this.fixedsize.height > 0)\n this.crop[2] = this.crop[0] + (this.fixedsize.height / this.surface.canvasdata.realsize.y);\n if(movegrid)\n {//moving whole grid\n this.crop[1] = this.crop[3] + this.gridanchor.width;\n this.crop[2] = this.crop[0] + this.gridanchor.height;\n }\n else if(this.aspect > 0)\n {\n if(this.fixedsize.width === 0)\n {\n this.crop[1] = this.crop[3] + ((bottompx - toppx) * this.aspect) / this.surface.canvasdata.csssize.x;\n }\n else\n this.crop[2] = this.crop[0] + (rightpx - leftpx) / (this.aspect * this.surface.canvasdata.csssize.y);\n }\n\n this.showCrop();\n }\n\n setAspectratio(aspect, callback)\n {\n var crop = null;\n if(typeof aspect == \"object\")\n {\n crop = aspect;\n if (!crop || !crop.width || !crop.height)\n aspect = 0;\n else\n aspect = crop.width / crop.height;\n }\n\n this.aspect = aspect > 0 ? aspect : 0;\n\n var maxw = this.fixedsize.width > 0 ? this.fixedsize.width : crop ? crop.width : this.surface.canvasdata.realsize.x;\n var maxh = this.fixedsize.height > 0 ? this.fixedsize.height : crop ? crop.height : this.surface.canvasdata.realsize.y;\n var w = maxw;\n var h = maxh;\n\n if(this.aspect > 0)\n {//set crop to optimal fit\n h = Math.round(w / this.aspect);\n if(h > maxh)\n {\n h = maxh;\n w = Math.round(h*this.aspect);\n }\n\n if(this.fixedsize.width > 0 || this.fixedsize.height > 0)\n this.fixedsize = { 'width' : w, 'height' : h};\n }\n\n if (!this.surface.setBusy(true))\n return;\n\n var options = { width: w || this.surface.canvasdata.realsize.x\n , height: h || this.surface.canvasdata.realsize.y\n , debug: dompack.debugflags.isc\n };\n SmartCrop.crop(this.surface.canvas, options, function(result)\n {\n//ADDME: if (options.debug && result.debugCanvas)\n// this.tmpcanvas.getContext(\"2d\").drawImage(result.debugCanvas, 0, 0, this.tmpcanvas.width, this.tmpcanvas.height);\n this.setClipValues(result.topCrop.x, result.topCrop.y, result.topCrop.y + result.topCrop.height, result.topCrop.x + result.topCrop.width);\n this.showCrop();\n this.surface.setBusy(false);\n if (callback)\n callback({'width' : result.topCrop.width, 'height' : result.topCrop.height});\n }.bind(this));\n }\n\n smartCrop(callback)\n {\n this.setAspectratio(this.options.ratiosize, callback);\n }\n\n setClipValues(leftpx, toppx, bottompx, rightpx)\n {\n this.crop[0] = toppx / this.surface.canvasdata.realsize.y;\n this.crop[1] = rightpx / this.surface.canvasdata.realsize.x;\n this.crop[2] = bottompx / this.surface.canvasdata.realsize.y;\n this.crop[3] = leftpx / this.surface.canvasdata.realsize.x;\n\n //covert to css positions current canvas\n toppx = Math.round(toppx / (this.surface.imgdata.scale.y * this.surface.canvasdata.scale.y));\n rightpx = Math.round(rightpx / (this.surface.imgdata.scale.x * this.surface.canvasdata.scale.x));\n bottompx = Math.round(bottompx / (this.surface.imgdata.scale.y * this.surface.canvasdata.scale.y));\n leftpx = Math.round(leftpx / (this.surface.imgdata.scale.x * this.surface.canvasdata.scale.x));\n\n this.draggers[0].wh_pos = { x: leftpx, y: toppx };\n this.draggers[1].wh_pos = { x: leftpx, y: bottompx };\n this.draggers[2].wh_pos = { x: rightpx, y: toppx };\n this.draggers[3].wh_pos = { x: rightpx, y: bottompx };\n }\n\n setClipCenterValues(w,h)\n {\n var leftpx = 0.5*(this.surface.canvasdata.realsize.x - w);\n var toppx = 0.5*(this.surface.canvasdata.realsize.y - h);\n var bottompx = toppx + h;\n var rightpx = leftpx + w;\n this.setClipValues(leftpx, toppx, bottompx, rightpx);\n }\n\n setWidth (w, fixed)\n {\n var inputwidth = Math.round(w);\n\n if(w > this.surface.canvasdata.realsize.x)\n w = this.surface.canvasdata.realsize.x;\n var h = Math.round(this.crop[2]*this.surface.canvasdata.realsize.y - this.crop[0]*this.surface.canvasdata.realsize.y);\n\n if(this.aspect > 0 && w > 0)\n {\n //calc maximal width by given aspectratio\n var aw = this.surface.canvasdata.realsize.x;\n var ah = aw / this.aspect;\n if(ah > this.surface.canvasdata.realsize.y)\n {\n ah = this.surface.canvasdata.realsize.y;\n aw = ah*this.aspect;\n }\n if(w > aw)\n w = aw;\n\n h = w / this.aspect;\n }\n\n if(w < 0)\n w = 0;\n\n w = Math.round(w);\n h = Math.round(h);\n\n var isvalid = inputwidth == w;\n if(isvalid)\n {\n if(fixed)\n {\n if(this.fixedsize.height > 0 && this.fixedsize.height != h)\n this.fixedsize.height = h;\n this.fixedsize.width = w;\n }\n\n if(w > 0)\n {//resize clip area\n this.setClipCenterValues(w, h);\n this.showCrop();\n }\n }\n\n return isvalid;\n }\n\n setHeight (h, fixed)\n {\n var inputheight = Math.round(h);\n\n if(h > this.surface.canvasdata.realsize.y)\n h = this.surface.canvasdata.realsize.y;\n var w = Math.round(this.crop[1]*this.surface.canvasdata.realsize.x - this.crop[3]*this.surface.canvasdata.realsize.x);\n\n if(this.aspect > 0 && h > 0)\n {\n //calc maximal height by given aspectratio\n var aw = this.surface.canvasdata.realsize.x;\n var ah = aw / this.aspect;\n if(ah > this.surface.canvasdata.realsize.y)\n {\n ah = this.surface.canvasdata.realsize.y;\n aw = ah*this.aspect;\n }\n if(h > ah)\n h = ah;\n\n w = h * this.aspect;\n }\n\n if(h < 0)\n h = 0;\n\n w = Math.round(w);\n h = Math.round(h);\n\n var isvalid = inputheight == h;\n if(isvalid)\n {\n if(fixed)\n {\n if(this.fixedsize.width > 0 && this.surface.canvasdata.realsize.x != w)\n this.fixedsize.width = w;\n this.fixedsize.height = h;\n }\n if(h > 0)\n {//resize clip area\n this.setClipCenterValues(w, h);\n this.showCrop();\n }\n }\n\n return isvalid;\n }\n\n showCrop()\n {\n var x1 = this.draggers[0].wh_pos.x;\n var y1 = this.draggers[0].wh_pos.y;\n var x2 = x1;\n var y2 = y1;\n for(var c = 0; c < this.draggers.length; c++)\n {\n dompack.setStyles(this.draggers[c], {'top' : this.draggers[c].wh_pos.y + 'px', 'left' : this.draggers[c].wh_pos.x + 'px'});\n if(c > 0)\n {\n if(this.draggers[c].wh_pos.x > x2)\n x2 = this.draggers[c].wh_pos.x;\n\n if(this.draggers[c].wh_pos.x < x1)\n x1 = this.draggers[c].wh_pos.x;\n\n if(this.draggers[c].wh_pos.y < y1)\n y1 = this.draggers[c].wh_pos.y;\n\n if(this.draggers[c].wh_pos.y > y2)\n y2 = this.draggers[c].wh_pos.y;\n }\n }\n\n dompack.setStyles(this.gridholder, { 'top': y1 + 'px'\n , 'right': x2 + 'px'\n , 'bottom': y2 + 'px'\n , 'left': x1 + 'px'\n , 'width': (x2 - x1) + 'px'\n , 'height': (y2 - y1) + 'px'\n });\n\n var canvasscale = Math.max(0, this.surface.canvasdata.realsize.x / this.surface.viewport.x, this.surface.canvasdata.realsize.y / this.surface.viewport.y);\n this.options.setStatus(Math.round((x2 - x1) * canvasscale), Math.round((y2 - y1) * canvasscale), this.surface.canvasdata.realsize.x, this.surface.canvasdata.realsize.y);\n\n this.masks[0].style.top = (y2 - this.masksize.height) + \"px\";\n this.masks[0].style.left = (x1 - this.masksize.width) + \"px\";\n this.masks[1].style.top = (y1 - this.masksize.height) + \"px\";\n this.masks[1].style.left = x1 + \"px\";\n this.masks[2].style.top = y1 + \"px\";\n this.masks[2].style.left = x2 + \"px\";\n this.masks[3].style.top = y2 + \"px\";\n this.masks[3].style.left = (x2 - this.masksize.width) + \"px\";\n }\n\n stop()\n {\n this.surface.showPreviewCanvas();\n this.cropbox.remove();\n this.refreshSurface();\n }\n\n apply()\n {\n this.surface.showPreviewCanvas();\n if(this.crop[0] == 0 && this.crop[1] == 1 && this.crop[2] == 1 && this.crop[3] == 0)\n return; //no changes\n\n this.applyCanvas({crop : this.crop});\n this.surface.pushUndo({action: \"crop\", comp: this, props: {crop : this.crop}, width:this.surface.canvas.width, height:this.surface.canvas.height, meta: false});\n this.refreshSurface();\n }\n\n applyCanvas(props)\n { //props is an array with top,right,bottom,left fractions (0..1)\n var newwidth = Math.round(props.crop[1]*this.surface.canvas.width - props.crop[3]*this.surface.canvas.width);\n var newheight = Math.round(props.crop[2]*this.surface.canvas.height - props.crop[0]*this.surface.canvas.height);\n\n //crop image\n var idata = this.surface.ctx.getImageData(Math.round(props.crop[3]*this.surface.canvas.width), Math.round(props.crop[0]*this.surface.canvas.height), newwidth, newheight);\n this.surface.canvas.width = newwidth;\n this.surface.canvas.height = newheight;\n this.surface.ctx.putImageData(idata,0,0);\n\n //correct css styling:\n var canvasscalex = newwidth / this.surface.viewport.x;\n var canvasscaley = newheight / this.surface.viewport.y;\n var canvasscale = canvasscalex > canvasscaley ? canvasscalex : canvasscaley;\n if(canvasscale < 1)\n canvasscale = 1;//don't scale up\n this.surface.canvasscale = 1 / canvasscale;\n\n var cssw = Math.round(newwidth / canvasscale);\n var cssh = Math.round(newheight / canvasscale);\n this.surface.canvasdata.csssize = {'x' : cssw, 'y' : cssh};\n this.surface.canvasdata.scale = {'x' : (newwidth/cssw), 'y' : (newheight/cssh)};\n //this.surface.canvasdata.realsize = {'x' : Math.round(props.crop[1]*imgedit.canvasdata.realsize.x - props.crop[3]*imgedit.canvasdata.realsize.x), 'y' : Math.round(props.crop[2]*imgedit.canvasdata.realsize.y - props.crop[0]*imgedit.canvasdata.realsize.y)};\n\n dompack.setStyles(this.surface.canvas, { 'width' : this.surface.canvasdata.csssize.x + 'px'\n , 'height' : this.surface.canvasdata.csssize.y + 'px'\n , 'margin-left': Math.floor(this.surface.canvasdata.csssize.x*-0.5) + 'px'\n , 'margin-top' : Math.floor(this.surface.canvasdata.csssize.y*-0.5) + 'px'\n });\n this.surface.showScale();\n }\n}\n\nfunction addImageCropButton(toolbar, surface, options)\n{\n var cropper = new PhotoCrop(surface, options);\n\n var button = new Toolbar.Button(toolbar,\n { label: getTid(\"tollium:components.imgedit.editor.crop\")\n , icon: toddImages.createImage(\"tollium:actions/crop\", 24, 24, \"b\")\n , onExecute: cropper.startCropping.bind(cropper, toolbar)\n });\n toolbar.addButton(button);\n\n return { button: button, comp: cropper };\n}\n\nexports.addImageCropButton = addImageCropButton;\n", "var Toolbar = require('../toolbar/toolbars');\nvar getTid = require(\"@mod-tollium/js/gettid\").getTid;\nrequire(\"./imageeditor.lang.json\");\nvar toddImages = require(\"@mod-tollium/js/icons\");\nimport { SurfaceTool } from './surfacetool.es';\nimport * as dompack from 'dompack';\n\nclass PhotoRotate extends SurfaceTool\n{\n constructor(surface, options)\n {\n super(surface, options);\n\n this.angle = 0;\n this.scale = {x:1, y:1};\n this.active = false;\n this.canvasscale = 1;\n\n this.options = { setStatus: null\n , ...options\n };\n\n this.scalepanel = new Toolbar.Panel(\n { onClose: this.stop.bind(this)\n , onApply: this.apply.bind(this)\n });\n this.scalepanel._imgedittool = \"rotate\";\n this.scalepanel.addButton(new Toolbar.Button(this.scalepanel,\n { label: getTid(\"tollium:components.imgedit.editor.rotateleft\")\n , icon: toddImages.createImage(\"tollium:actions/rotateleft\", 24, 24, \"b\")\n , onExecute: this.rotate.bind(this, -90)\n }));\n this.scalepanel.addButton(new Toolbar.Button(this.scalepanel,\n { label: getTid(\"tollium:components.imgedit.editor.rotateright\")\n , icon: toddImages.createImage(\"tollium:actions/rotateright\", 24, 24, \"b\")\n , onExecute: this.rotate.bind(this, 90)\n }));\n this.scalepanel.addButton(new Toolbar.Separator(this.scalepanel));\n this.scalepanel.addButton(new Toolbar.Button(this.scalepanel,\n { label: getTid(\"tollium:components.imgedit.editor.fliphorizontal\")\n , icon: toddImages.createImage(\"tollium:actions/fliphorizontal\", 24, 24, \"b\")\n , onExecute: this.fliphorizontal.bind(this)\n }));\n this.scalepanel.addButton(new Toolbar.Button(this.scalepanel,\n { label: getTid(\"tollium:components.imgedit.editor.flipvertical\")\n , icon: toddImages.createImage(\"tollium:actions/flipvertical\", 24, 24, \"b\")\n , onExecute: this.flipvertical.bind(this)\n }));\n }\n\n startScaling(toolbar)\n {\n toolbar.activateModalPanel(this.scalepanel);\n this.surface.hidePreviewCanvas();\n this.start();\n }\n\n start()\n {\n\n //initial values\n this.angle = 0;\n this.scale = {x:1,y:1};\n\n //what scale to use to fit image on canvas in current position\n var canvasscalex = this.surface.canvas.width / this.surface.viewport.x;\n var canvasscaley = this.surface.canvas.height / this.surface.viewport.y;\n this.canvasscale = canvasscalex > canvasscaley ? canvasscalex : canvasscaley;\n\n //what scale if rotated 90deg.:\n var canvasscalexr = this.surface.canvas.width / this.surface.viewport.y;\n var canvasscaleyr = this.surface.canvas.height / this.surface.viewport.x;\n this.canvasscale = canvasscalexr > this.canvasscale ? canvasscalexr : this.canvasscale;\n this.canvasscale = canvasscaleyr > this.canvasscale ? canvasscaleyr : this.canvasscale;\n if(this.canvasscale < 1)\n this.canvasscale = 1;//don't scale up\n this.surface.showScale(1 / this.canvasscale);\n\n this.active = true;\n\n //resize canvas so it fits if rotated\n var cssw = Math.round(this.surface.canvas.width / this.canvasscale);\n var cssh = Math.round(this.surface.canvas.height / this.canvasscale);\n this.surface.canvasdata.csssize = {'x' : cssw, 'y' : cssh};\n this.surface.canvasdata.scale = {'x' : (this.surface.canvas.width/cssw), 'y' : (this.surface.canvas.height/cssh)};\n\n dompack.setStyles(this.surface.canvas, { 'width' : this.surface.canvasdata.csssize.x + 'px'\n , 'height' : this.surface.canvasdata.csssize.y + 'px'\n , 'margin-left': Math.ceil(this.surface.canvasdata.csssize.x*-0.5) + 'px'\n , 'margin-top' : Math.ceil(this.surface.canvasdata.csssize.y*-0.5) + 'px'\n });\n this.surface.updateMaskCanvas();\n\n this.setStatus();\n }\n\n stop()\n {\n this.surface.showPreviewCanvas();\n\n this.scale = {x:1,y:1};\n this.angle = 0;\n this.rotate(0);\n\n //what scale to use to fit image on canvas in current position\n var canvasscalex = this.surface.canvas.width / this.surface.viewport.x;\n var canvasscaley = this.surface.canvas.height / this.surface.viewport.y;\n this.canvasscale = canvasscalex > canvasscaley ? canvasscalex : canvasscaley;\n if(this.canvasscale < 1)\n this.canvasscale = 1;//don't scale up\n\n this.active = false;\n //resize canvas so it fits if rotated\n\n var cssw = Math.round(this.surface.canvas.width / this.canvasscale);\n var cssh = Math.round(this.surface.canvas.height / this.canvasscale);\n this.surface.canvasdata.csssize = {'x' : cssw, 'y' : cssh};\n this.surface.canvasdata.scale = {'x' : (this.surface.canvas.width/cssw), 'y' : (this.surface.canvas.height/cssh)};\n\n dompack.setStyles(this.surface.canvas, { 'width' : this.surface.canvasdata.csssize.x + 'px'\n , 'height' : this.surface.canvasdata.csssize.y + 'px'\n , 'margin-left': Math.ceil(this.surface.canvasdata.csssize.x*-0.5) + 'px'\n , 'margin-top' : Math.ceil(this.surface.canvasdata.csssize.y*-0.5) + 'px'\n });\n this.surface.updateMaskCanvas();\n this.refreshSurface();\n }\n\n apply()\n {\n this.surface.showPreviewCanvas();\n this.active = false;\n\n if(this.angle == 0 && this.scale.x == 1 && this.scale.y == 1)\n return;//no changes\n\n var newprops = {angle : this.angle, scale : this.scale};\n this.applyCanvas(newprops);\n\n this.surface.pushUndo({action: \"rotate\", comp: this, props: newprops, meta: false});\n\n //and setback initial values:\n this.scale = {x:1,y:1};\n this.angle = 0;\n this.rotate(0);\n }\n\n applyCanvas(props)\n {\n var neww = this.surface.canvas.width;\n var newh = this.surface.canvas.height;\n if(Math.round(Math.cos(props.angle*Math.PI/180)*100) == 0)\n {//rotated 90 or 270 deg.\n neww = this.surface.canvas.height;\n newh = this.surface.canvas.width;\n\n //switch scalefactors\n var scalex = this.surface.imgdata.scale.x;\n this.surface.imgdata.scale.x = this.surface.imgdata.scale.y;\n this.surface.imgdata.scale.y = scalex;\n\n var rx = this.surface.canvasdata.realsize.x;\n this.surface.canvasdata.realsize.x = this.surface.canvasdata.realsize.y;\n this.surface.canvasdata.realsize.y = rx;\n }\n else if(Math.round(Math.sin(props.angle*Math.PI/180)*100) == 0)\n {//rotated 0 or 360 deg.\n //no change in dimensions\n }\n else\n {//arbitrary angle\n //FIXME?\n }\n\n var copy;\n if(neww != this.surface.canvas.width)\n {//resize canvas to fit image\n //Copy image\n\n var idata = this.surface.ctx.getImageData(0, 0, this.surface.canvas.width, this.surface.canvas.height);\n this.surface.ctx.clearRect(0, 0, this.surface.canvas.width, this.surface.canvas.height);\n\n var prevw = this.surface.canvas.width;\n var prevh = this.surface.canvas.height;\n\n //set needed canvas size to fit rotation\n var max = newh > neww ? newh : neww;\n this.surface.canvas.width = max;\n this.surface.canvas.height = max;\n this.surface.ctx.putImageData(idata,Math.floor(0.5*(max - prevw)), Math.floor(0.5*(max - prevh)), 0, 0, prevw, prevh);\n\n copy = this.surface.cloneCanvas({ clearoriginal: true });\n\n //Rotate and or flip canvas\n this.surface.ctx.save();\n this.surface.ctx.setTransform(1,0,0,1,0,0);\n this.surface.ctx.translate(this.surface.canvas.width / 2, this.surface.canvas.height / 2);\n this.surface.ctx.scale(props.scale.x,props.scale.y);//scaling is -1 or 1 (flip vertical/horizontal)\n this.surface.ctx.rotate(props.angle*Math.PI/180);\n\n// this.surface.ctx.globalCompositeOperation = 'copy';//disabled because of bug in webkit\n// as far we use steps of 90deg. this is no problem because we crop the image after rotation\n// will be an issue if we use free rotation\n this.surface.ctx.drawImage(copy.canvas, -this.surface.canvas.width/2, -this.surface.canvas.height/2);\n this.surface.ctx.restore();\n\n //crop the transparent parts\n idata = this.surface.ctx.getImageData(Math.floor(0.5*(max - neww)), Math.floor(0.5*(max - newh)), neww, newh);\n this.surface.ctx.clearRect(0, 0, this.surface.canvas.width, this.surface.canvas.height);\n\n this.surface.canvas.width = neww;\n this.surface.canvas.height = newh;\n this.surface.ctx.putImageData(idata,0,0);\n }\n else\n {\n copy = this.surface.cloneCanvas({ clearoriginal: true });\n\n this.surface.ctx.save();\n this.surface.ctx.setTransform(1,0,0,1,0,0);\n this.surface.ctx.translate(this.surface.canvas.width / 2, this.surface.canvas.height / 2);\n this.surface.ctx.scale(props.scale.x,props.scale.y);//scaling is -1 or 1 (flip vertical/horizontal)\n this.surface.ctx.rotate(props.angle*props.scale.x*props.scale.y*Math.PI/180);//to rotate correct direction, multiply with scaling which is -1 or 1 (flip vertical/horizontal)\n\n this.surface.ctx.drawImage(copy.canvas, -this.surface.canvas.width/2, -this.surface.canvas.height/2);\n this.surface.ctx.restore();\n }\n\n if(!this.active)\n {//used if direct call from history\n //what scale to use to fit image on canvas in current position\n var canvasscalex = this.surface.canvas.width / this.surface.viewport.x;\n var canvasscaley = this.surface.canvas.height / this.surface.viewport.y;\n this.canvasscale = canvasscalex > canvasscaley ? canvasscalex : canvasscaley;\n if(this.canvasscale < 1)\n this.canvasscale = 1;//don't scale up\n }\n this.surface.canvasscale = 1 / this.canvasscale;\n\n //correct css position/dimensions\n var cssw = Math.round(this.surface.canvas.width / this.canvasscale);\n var cssh = Math.round(this.surface.canvas.height / this.canvasscale);\n\n this.surface.canvasdata.csssize = {'x' : cssw, 'y' : cssh};\n this.surface.canvasdata.scale = {'x' : (this.surface.canvas.width/cssw), 'y' : (this.surface.canvas.height/cssh)};\n\n dompack.setStyles(this.surface.canvas, { 'width' : this.surface.canvasdata.csssize.x + 'px'\n , 'height' : this.surface.canvasdata.csssize.y + 'px'\n , 'margin-left': Math.ceil(this.surface.canvasdata.csssize.x*-0.5) + 'px'\n , 'margin-top' : Math.ceil(this.surface.canvasdata.csssize.y*-0.5) + 'px'\n });\n this.surface.updateMaskCanvas();\n this.surface.showScale();\n this.refreshSurface();\n }\n\n fliphorizontal()\n {\n this.scale.x*=-1;\n this.rotate(0);\n }\n\n flipvertical()\n {\n this.scale.y*=-1;\n this.rotate(0);\n }\n\n rotate(degrees)\n {\n this.angle+=degrees;\n this.angle-=Math.floor(this.angle / 360) * 360;//keep range between 0 and 360\n\n this.surface.canvas.style.transform = 'scale('+this.scale.x+','+this.scale.y+') rotate('+this.angle+'deg)';\n\n this.setStatus();\n }\n\n setStatus()\n {\n if (!this.active)\n return;\n var neww = this.surface.canvas.width;\n var newh = this.surface.canvas.height;\n if(Math.round(Math.cos(this.angle*Math.PI/180)*100) === 0)\n {//rotated 90 or 270 deg.\n neww = this.surface.canvas.height;\n newh = this.surface.canvas.width;\n this.surface.updateMaskCanvas({ left: Math.floor((this.surface.maskcanvas.width - this.surface.canvasdata.csssize.y) / 2)\n , top: Math.floor((this.surface.maskcanvas.height - this.surface.canvasdata.csssize.x) / 2)\n , width: this.surface.canvasdata.csssize.y\n , height: this.surface.canvasdata.csssize.x\n });\n }\n else\n this.surface.updateMaskCanvas();\n //ADDME: scaling?\n this.options.setStatus(neww, newh);\n }\n}\n\nfunction addImageRotateButton(toolbar, surface, options)\n{\n var rotator = new PhotoRotate(surface, options);\n\n var button = new Toolbar.Button(toolbar,\n { label: getTid(\"tollium:components.imgedit.editor.rotate\")\n , icon: toddImages.createImage(\"tollium:actions/rotate\", 24, 24, \"b\")\n , onExecute: rotator.startScaling.bind(rotator, toolbar)\n });\n toolbar.addButton(button);\n\n return { button: button, comp: rotator };\n}\n\nexports.addImageRotateButton = addImageRotateButton;\n", "import * as dompack from 'dompack';\nimport * as movable from 'dompack/browserfix/movable';\nvar Toolbar = require('../toolbar/toolbars');\nvar getTid = require(\"@mod-tollium/js/gettid\").getTid;\nrequire(\"./imageeditor.lang.json\");\nvar toddImages = require(\"@mod-tollium/js/icons\");\nimport Keyboard from 'dompack/extra/keyboard';\nimport { SurfaceTool } from './surfacetool.es';\n\n// Set to true to activate 'inline' mode, without the modal toolbar\nvar tool_inline = false;\n\nvar buttonicon;\n\nclass PhotoPoint extends SurfaceTool\n{\n constructor(surface, options)\n {\n super(surface, options);\n\n this.refpoint = null;// { x: 0, y: 0 }\n this.isactive = false;\n this.activated = false;\n this.options = {...options};\n\n this.refpointpanel = new Toolbar.Panel(\n { onClose: this.stop.bind(this)\n , onApply: this.apply.bind(this)\n , onCancel: this.cancel.bind(this)\n });\n this.refpointpanel._imgedittool = \"refpoint\";\n\n this.delbutton = new Toolbar.Button(this.refpointpanel,\n { label: getTid(\"tollium:components.imgedit.editor.delrefpoint\")\n , icon: toddImages.createImage(\"tollium:actions/delete\", 24, 24, \"b\")\n , onExecute: this.clearPoint.bind(this)\n });\n this.refpointpanel.addButton(this.delbutton);\n\n this._setPoint = this.setPoint.bind(this);\n this.keyboard = new Keyboard(this.surface.container, { Delete: this.clearPoint.bind(this) });\n if (tool_inline)\n {\n this.surface.imgeditornode.addEventListener(\"tollium-imageeditor:reset\", () => this.resetPoint());\n this.surface.imgeditornode.addEventListener(\"tollium-imageeditor:showpreview\", () => this.activate(true));\n this.surface.imgeditornode.addEventListener(\"tollium-imageeditor:hidepreview\", () => this.activate(false));\n }\n else\n {\n this.surface.imgeditornode.addEventListener(\"tollium-imageeditor:updatepreview\", evt => this.previewCanvasChanged(evt));\n }\n }\n\n togglePointing(button)\n {\n if (!this.isactive)\n {\n this.start();\n toddImages.updateImage(buttonicon, \"tollium:actions/reference\", 24, 24, \"w\");\n }\n else\n {\n this.stop();\n toddImages.updateImage(buttonicon, \"tollium:actions/reference\", 24, 24, \"b\");\n }\n button.setPressed(this.isactive);\n }\n\n start(toolbar)\n {\n if (!tool_inline)\n toolbar.activateModalPanel(this.refpointpanel);\n this.refpoint = this.surface.refpoint;\n this.isactive = true;\n\n this.updateRefs();\n\n this.refpointer =
\n movable.enable(this.refpointer);\n this.refpointer.addEventListener(\"dompack:movestart\", evt => this.moveStart(evt));\n this.refpointer.addEventListener(\"dompack:move\", evt => this.move(evt));\n this.refpointer.addEventListener(\"dompack:moveend\", evt => this.moveEnd(evt));\n\n this.activate(true);\n }\n\n cancel()\n {\n // Reset surface refpoint when cancelling\n this.surface.refpoint = this.surface.orgrefpoint;\n }\n\n stop()\n {\n this.activate(false);\n this.isactive = false;\n\n this.refpointer.remove();\n this.refpointer= null;\n this.refreshSurface();\n }\n\n apply(fireapply)\n {\n this.applyCanvas({ refpoint : this.refpoint });\n if (fireapply !== false)\n {\n this.surface.pushUndo({action: \"refpoint\", comp: this, props: { refpoint : this.refpoint }, meta: true}, tool_inline);\n }\n }\n\n applyCanvas(props)\n {\n this.refpoint = props.refpoint;\n this.surface.refpoint = this.refpoint;\n this.refreshSurface();\n this.updatePoint();\n }\n\n activate(active)\n {\n if (!this.isactive)\n return;\n\n active = !!active;\n if (active != this.activated)\n {\n var canvas = this.surface.previewcanvas || this.surface.canvas;\n this.activated = active;\n if (active)\n {\n canvas.addEventListener(\"click\", this._setPoint);\n this.surface.container.classList.add(\"wh-refbox\");\n this.updatePoint();\n }\n else\n {\n canvas.removeEventListener(\"click\", this._setPoint);\n this.surface.container.classList.remove(\"wh-refbox\");\n this.updatePoint(true);\n }\n }\n }\n\n previewCanvasChanged(event)\n {\n if (this.activated && event.detail.oldcanvas)\n {\n event.detail.oldcanvas.removeEventListener(\"click\", this._setPoint);\n var canvas = this.surface.previewcanvas || this.surface.canvas;\n canvas.addEventListener(\"click\", this._setPoint);\n }\n }\n\n moveStart(event)\n {\n event.stopPropagation();\n this.updateRefs();\n }\n\n move(event)\n {\n event.stopPropagation();\n var x = Math.max(this.reference.relpos.x, Math.min(this.reference.imgsize.x + this.reference.relpos.x, Math.round(this.refpoint.x * this.reference.canvasscale) + event.detail.movedX + this.reference.relpos.x));\n var y = Math.max(this.reference.relpos.y, Math.min(this.reference.imgsize.y + this.reference.relpos.y, Math.round(this.refpoint.y * this.reference.canvasscale) + event.detail.movedY + this.reference.relpos.y));\n dompack.setStyles(this.refpointer, { left: x , top: y });\n }\n\n moveEnd(event)\n {\n event.stopPropagation();\n this.refpoint = { x: (parseInt(getComputedStyle(this.refpointer).left) - this.reference.relpos.x) / this.reference.canvasscale\n , y: (parseInt(getComputedStyle(this.refpointer).top) - this.reference.relpos.y) / this.reference.canvasscale\n };\n this.apply(tool_inline);\n }\n\n updateRefs()\n {\n var canvas = this.surface.previewcanvas || this.surface.canvas;\n let canvaspos = canvas.getBoundingClientRect();\n let surfacepos = this.surface.container.getBoundingClientRect();\n this.reference = { abspos: { x: canvaspos.left, y: canvaspos.top }\n , relpos: { x: canvaspos.left - surfacepos.left, y: canvaspos.top - surfacepos.top }\n , imgsize: { x: canvaspos.width, y: canvaspos.height }\n };\n this.reference.canvasscale = this.reference.imgsize.x / this.surface.canvasdata.realsize.x;\n }\n\n updatePoint(hide)\n {\n if (!this.refpointer)\n return;\n this.updateRefs();\n if (!hide && this.refpoint)\n {\n dompack.setStyles(this.refpointer, { left: Math.round(this.refpoint.x * this.reference.canvasscale + this.reference.relpos.x)\n , top: Math.round(this.refpoint.y * this.reference.canvasscale + this.reference.relpos.y)\n });\n this.surface.container.append(this.refpointer);\n }\n else\n {\n this.refpointer.remove();\n }\n }\n\n setPoint(event)\n {\n this.refpoint = { x: (event.clientX - this.reference.abspos.x) / this.reference.canvasscale\n , y: (event.clientY - this.reference.abspos.y) / this.reference.canvasscale\n };\n this.apply(tool_inline);\n }\n\n clearPoint()\n {\n if (!this.isactive)\n return;\n this.refpoint = null;\n this.apply(tool_inline);\n }\n\n resetPoint()\n {\n this.refpoint = this.surface.refpoint;\n if (this.isactive)\n this.updatePoint();\n }\n};\n\nfunction addRefPointButton(toolbar, surface, options)\n{\n var pointer = new PhotoPoint(surface, options);\n\n buttonicon = toddImages.createImage(\"tollium:actions/reference\", 24, 24, \"b\");\n var button = new Toolbar.Button(toolbar,\n { label: getTid(\"tollium:components.imgedit.editor.refpoint\")\n , icon: buttonicon\n });\n\n if (tool_inline)\n button.toElement().addEventListener(\"execute\", pointer.togglePointing.bind(pointer, button));\n else\n button.toElement().addEventListener(\"execute\", pointer.start.bind(pointer, toolbar));\n toolbar.addButton(button);\n\n return { button: button, comp: pointer };\n}\n\nexports.addRefPointButton = addRefPointButton;\n", "import * as dompack from 'dompack';\nvar Toolbar = require('../toolbar/toolbars');\nvar getTid = require(\"@mod-tollium/js/gettid\").getTid;\nrequire(\"./imageeditor.lang.json\");\nvar toddImages = require(\"@mod-tollium/js/icons\");\nimport { SurfaceTool } from './surfacetool.es';\n\nvar CCV, faceCascade;\n//ADDME: Uncomment these to activate face recognition filter\n//CCV = require('./ccv.js');\n//faceCascade = require('./face.js');\n\nclass PhotoFilters extends SurfaceTool\n{\n constructor(surface, options)\n {\n super(surface, options);\n\n this.filterdata = null;\n this.filtertime = 0;\n this.previewdata = null;\n this.options = { resourcebase: \"\"\n , setProgress: null\n , setStatus: null\n , createScreen: null\n , getAllowedFilters: null\n , setModalLayerOpacity: null\n , ...options\n };\n\n\n this.filterpanel = new Toolbar.Panel(\n { onClose: this.stop.bind(this)\n , onApply: this.apply.bind(this)\n });\n this.filterpanel._imgedittool = \"filters\";\n this.grayscalebutton = new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.grayscale\")\n , icon: toddImages.createImage(\"tollium:actions/grayscale\", 24, 24, \"b\")\n , onExecute: this.grayscale.bind(this)\n });\n this.filterpanel.addButton(this.grayscalebutton);\n this.invertbutton = new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.invert\")\n , icon: toddImages.createImage(\"tollium:actions/invert\", 24, 24, \"b\")\n , onExecute: this.invert.bind(this)\n });\n this.filterpanel.addButton(this.invertbutton);\n this.sharpenbutton = new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.sharpen\")\n , icon: toddImages.createImage(\"tollium:actions/sharpen\", 24, 24, \"b\")\n , onExecute: this.sharpen.bind(this)\n });\n this.filterpanel.addButton(this.sharpenbutton);\n this.blurbutton = new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.blur\")\n , icon: toddImages.createImage(\"tollium:actions/blur\", 24, 24, \"b\")\n , onExecute: this.blur.bind(this)\n });\n this.filterpanel.addButton(this.blurbutton);\n this.brightnesscontrastbutton = new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.brightnesscontrast\")\n , icon: toddImages.createImage(\"tollium:actions/brightnesscontrast\", 24, 24, \"b\")\n , onExecute: this.brightnessContrast.bind(this)\n });\n this.filterpanel.addButton(this.brightnesscontrastbutton);\n this.autocontrastbutton = new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.autocontrast\")\n , icon: toddImages.createImage(\"tollium:actions/autocontrast\", 24, 24, \"b\")\n , onExecute: this.autocontrast.bind(this)\n });\n this.filterpanel.addButton(this.autocontrastbutton);\n this.coloradjustbutton = new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.coloradjust\")\n , icon: toddImages.createImage(\"tollium:actions/colors\", 24, 24, \"b\")\n , onExecute: this.colorAdjust.bind(this)\n });\n this.filterpanel.addButton(this.coloradjustbutton);\n if (dompack.debugflags.ixf)\n {\n this.filterpanel.addButton(new Toolbar.Separator(this.filterpanel));\n this.filterpanel.addButton(new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.sepia\")\n , icon: toddImages.createImage(\"tollium:actions/sepia\", 24, 24, \"b\")\n , onExecute: this.sepia.bind(this)\n }));\n this.filterpanel.addButton(new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.posterize\")\n , icon: toddImages.createImage(\"tollium:actions/posterize\", 24, 24, \"b\")\n , onExecute: this.posterize.bind(this)\n }));\n if (typeof CCV == \"object\")\n {\n this.filterpanel.addButton(new Toolbar.Button(this.filterpanel,\n { label: getTid(\"tollium:components.imgedit.editor.findfaces\")\n , icon: toddImages.createImage(\"tollium:actions/findfaces\", 24, 24, \"b\")\n , onExecute: this.findFaces.bind(this)\n }));\n }\n }\n }\n\n updateFilterButtons()\n {\n var allowedfilters = this.options.getAllowedFilters();\n var allallowed = allowedfilters.indexOf(\"all\") >= 0;\n this.grayscalebutton.node.style.display = allallowed || allowedfilters.indexOf(\"grayscale\") >= 0 ? \"\" : \"none\";\n this.invertbutton.node.style.display = allallowed || allowedfilters.indexOf(\"invert\") >= 0 ? \"\" : \"none\";\n this.sharpenbutton.node.style.display = allallowed || allowedfilters.indexOf(\"sharpen\") >= 0 ? \"\" : \"none\";\n this.blurbutton.node.style.display = allallowed || allowedfilters.indexOf(\"blur\") >= 0 ? \"\" : \"none\";\n this.brightnesscontrastbutton.node.style.display = allallowed || allowedfilters.indexOf(\"brightnesscontrast\") >= 0 ? \"\" : \"none\";\n this.autocontrastbutton.node.style.display = allallowed || allowedfilters.indexOf(\"autocontrast\") >= 0 ? \"\" : \"none\";\n this.coloradjustbutton.node.style.display = allallowed || allowedfilters.indexOf(\"coloradjust\") >= 0 ? \"\" : \"none\";\n }\n\n startFiltering(toolbar)\n {\n this.updateFilterButtons();\n toolbar.activateModalPanel(this.filterpanel);\n this.surface.hidePreviewCanvas();\n\n this.worker = new Worker(this.options.resourcebase + \"components/imageeditor/filters-worker.js\");\n this.worker.addEventListener(\"message\", evt => this.onFilterResult(evt));\n\n this.start();\n }\n\n start()\n {\n this.filterbox =
;\n this.surface.container.append(this.filterbox);\n\n this.tmpcanvas = ;\n this.filterbox.append(this.tmpcanvas);\n var tmpctx = this.tmpcanvas.getContext('2d');\n tmpctx.drawImage(this.surface.canvas, 0, 0, this.surface.canvas.width, this.surface.canvas.height);\n\n this.options.setStatus(this.surface.canvas.width, this.surface.canvas.height);\n\n this.filterdata = null;\n }\n\n stop()\n {\n this.worker.terminate();\n this.worker = null;\n\n this.surface.showPreviewCanvas();\n this.filterbox.remove();\n this.refreshSurface();\n }\n\n apply()\n {\n this.surface.showPreviewCanvas();\n if(!this.filterdata)\n return; //no changes\n\n this.applyCanvas({filterdata : this.filterdata});\n this.surface.pushUndo({action: \"filters\", comp: this, props: {filterdata : this.filterdata}, meta: false});\n this.refreshSurface();\n }\n\n applyCanvas(props)\n {\n var pixels = this.getPixels(this.surface.canvas);\n for (var i = 0; i < props.filterdata.length; ++i)\n pixels.data[i] = props.filterdata[i];\n this.setPixels(this.surface.canvas, pixels);\n }\n\n grayscale()\n {\n this.runFilter(\"grayscale\");\n }\n\n sepia()\n {\n this.runFilter(\"sepiaTone\");\n }\n\n posterize()\n {\n var components =\n { level: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.level\")\n , min: 2, max: 256, step: 1, value: 4\n , width: \"1pr\" }\n };\n this.runFilterDialog(getTid(\"tollium:components.imgedit.filters.posterize\"), components, values =>\n {\n var level = parseInt(values.level);\n this.runFilter(\"posterize\", level);\n });\n }\n\n invert()\n {\n this.runFilter(\"invert\");\n }\n\n colorAdjust()\n {\n var components =\n { /*advanced: { type: \"checkbox\", title: \"\", label: \"advanced\" }\n , */red: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.red\")\n , min: 0, max: 100, step: 1, value: 100\n , width: \"1pr\" }\n , green: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.green\")\n , min: 0, max: 100, step: 1, value: 100\n , width: \"1pr\" }\n , blue: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.blue\")\n , min: 0, max: 100, step: 1, value: 100\n , width: \"1pr\" }\n };\n this.runFilterDialog(getTid(\"tollium:components.imgedit.filters.coloradjust\"), components, values =>\n {\n var redfraction = parseInt(values.red) / 100\n , greenfraction = parseInt(values.green) / 100\n , bluefraction = parseInt(values.blue) / 100;\n\n // Run the filter\n this.runFilter(\"adjustColors\", redfraction, greenfraction, bluefraction);\n });\n }\n\n brightnessContrast()\n {\n //ADDME: Currently using linear brightness/contrast adjustment (which Photoshop calls 'legacy'), maybe switch to\n // non-linear adjustment using histogram curves?\n var components =\n { brightness: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.brightness\")\n , min: -100, max: 100, step: 1, value: 0\n , width: \"1pr\" }\n , contrast: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.contrast\")\n , min: -50, max: 100, step: 1, value: 0\n , width: \"1pr\" }\n };\n this.runFilterDialog(getTid(\"tollium:components.imgedit.filters.brightnesscontrast\"), components, values =>\n {\n // Brightness has range -1..0..1, with -1 resulting in black and 1 resulting in white\n // We'll map the -100..0..100 input range to -0.5..0..0.5\n var brightness = parseInt(values.brightness) / 200;\n // Contrast has range 0..1..127, with 0 resulting in gray\n // We'll map the -50..0..100 input range to about ~0.05..1..~21 using ((x/100)+1)^4.4 (which maps -100 to 0, 0 to 1\n // and 200 to ~126).\n var contrast = Math.pow(((parseInt(values.contrast) / 100) + 1), 4.4);\n\n // Run the filter\n this.runFilter(\"brightnessContrast\", brightness, contrast);\n });\n }\n\n autocontrast()\n {\n //this.runFilter(\"equalizeHistogram\");\n var components =\n { level: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.level\")\n , min: 1, max: 50, step: 1, value: 5\n , width: \"1pr\" }\n };\n this.runFilterDialog(getTid(\"tollium:components.imgedit.filters.autocontrast\"), components, values =>\n {\n var level = parseInt(values.level);\n this.runFilter(\"autoContrast\", level);\n });\n }\n\n sharpen()\n {\n this.runFilter(\"convolve\",\n [ 0, -1, 0\n , -1, 5, -1\n , 0, -1, 0\n ]);\n }\n\n blur()\n {\n var components =\n { radius: { type: \"slider\", title: getTid(\"tollium:components.imgedit.filters.radius\")\n , min: 1, max: 100, step: 1, value: 1\n , width: \"1pr\" }\n };\n this.runFilterDialog(getTid(\"tollium:components.imgedit.filters.blur\"), components, values =>\n {\n var radius = parseInt(values.radius);\n this.runFilter(\"gaussianBlur\", radius);\n });\n }\n\n findFaces()\n {\n var canvas = document.createElement(\"canvas\");\n var context = canvas.getContext(\"2d\");\n canvas.width = this.surface.canvas.width;\n canvas.height = this.surface.canvas.height;\n context.drawImage(this.surface.canvas, 0, 0);\n\n var starttime = Date.now();\n var options = { \"canvas\": CCV.grayscale(canvas)\n , \"cascade\": faceCascade\n , \"interval\": 5\n , \"min_neighbors\": 1\n //, \"async\": true\n //, \"worker\": 1\n };\n var result = CCV.detect_objects(options);\n console.info(\"detection-time\", Math.round(Date.now() - starttime));\n console.info(\"num-faces\", result.length.toString());\n for (var i = 0; i < result.length; i++)\n console.info(\"face #\"+i+\": \"+result[i].width+\"x\"+result[i].height+\" @\"+result[i].x+\".\"+result[i].y);\n /*\n ctx.lineWidth = 2;\n ctx.strokeStyle = 'rgba(230,87,0,0.8)';\n // Draw detected area\n for (var i = 0; i < result.length; i++) {\n ctx.beginPath();\n ctx.arc((result[i].x + result[i].width * 0.5) * scale, (result[i].y + result[i].height * 0.5) * scale,\n (result[i].width + result[i].height) * 0.25 * scale * 1.2, 0, Math.PI * 2);\n ctx.stroke();\n }\n */\n }\n\n getPixels(canvas)\n {\n return canvas.getContext(\"2d\").getImageData(0, 0, canvas.width, canvas.height);\n }\n\n setPixels(canvas, pixels)\n {\n canvas.getContext(\"2d\").putImageData(pixels, 0, 0);\n }\n\n runFilter(filter, var_args)\n {\n if (!this.surface.setBusy(true))\n return; // Already busy\n\n this.filtertime = Date.now();\n\n var args = Array.prototype.slice.apply(arguments);\n // When previewing, the previewdata property will contain the unfiltered data\n args[0] = this.previewdata || this.getPixels(this.tmpcanvas); // Replace 'filter' argument with the image data (first argument to filter functions)\n var output = this.tmpcanvas.getContext(\"2d\").createImageData(this.tmpcanvas.width, this.tmpcanvas.height);\n\n console.log(\"Starting filter\", filter, \"with arguments\", args, \"and output\", output);\n this.worker.postMessage({ name: filter, args: args, output: output });\n }\n\n onFilterResult(event)\n {\n var data = event.data;\n if (data)\n {\n switch (data.type)\n {\n case \"result\":\n {\n if (this.options.setProgress)\n {\n this.options.setProgress(0, 0);\n }\n else if (this.progress)\n {\n this.progress.remove();\n this.progress = null;\n }\n\n requestAnimationFrame(function()\n {\n this.filterdata = data.result.data;\n this.setPixels(this.tmpcanvas, data.result);\n\n console.log(\"Got filter result in \" + (Date.now() - this.filtertime) + \"ms\");\n this.surface.setBusy(false);\n }.bind(this));\n\n break;\n }\n case \"progress\":\n {\n if (this.options.setProgress)\n {\n this.options.setProgress(data.progress, 100);\n }\n else\n {\n if (!this.progress)\n {\n this.progress = \n this.filterbox.append(this.progress);\n }\n this.progress.value = data.progress;\n }\n break;\n }\n case \"debug\":\n {\n console.info(data.debug);\n break;\n }\n }\n }\n }\n\n // @param title Dialog title\n // @param components Filter-specific components, { name: spec, name: spec } object (each object is rendered on its own line\n // within the dialog body, 'spec' is a createScreen-compatible component description)\n // @param runfilter The function that actually runs the filter, which is supplied a { name: value, name: value } object\n // with the getValue() value for each component from components\n runFilterDialog(title, components, runfilter)\n {\n var curdata = this.filterdata;\n var curpixels = this.getPixels(this.tmpcanvas);\n var previewed = false;\n\n // This will automatically run the dialog\n new FilterDialogController(\n { title: title\n , components: components\n , createScreen: this.options.createScreen\n , onButton: result =>\n {\n // Apply the filter if previewing, or if the 'ok' button is pressed and the filter is not yet previewed\n if (result.button == \"preview\" || (result.button == \"ok\" && !previewed))\n {\n previewed = result.button == \"preview\";\n\n // Use the initial canvas for running the filter (prevent re-applying the filter on multiple previews)\n this.previewdata = curpixels;\n\n // Run the filter\n runfilter(result.values);\n }\n // Reset the filterdata and canvas if the 'cancel' button is pressed and the filter has been previewed\n else if (result.button == \"cancel\" && previewed)\n {\n this.filterdata = curdata;\n this.setPixels(this.tmpcanvas, curpixels);\n }\n // Clear the preview initial canvas\n if (result.button != \"preview\")\n {\n this.previewdata = null;\n }\n }\n });\n // Make modal layer fully transparent, so the actual image is visible\n this.options.setModalLayerOpacity(0);\n }\n}\n\nclass FilterDialogController\n{ constructor(options)\n {\n this.options = { title: null\n , components: null\n , createScreen: null\n , ...options\n };\n this.options.components = {...this.options.components};\n this._createDialog();\n }\n\n _createDialog()\n {\n var dialog =\n { frame: { bodynode: 'root'\n , specials: ['previewaction','okaction','cancelaction']\n , title: this.options.title\n , defaultbutton: \"okbutton\"\n , allowclose: true\n }\n , root: { type: 'panel', lines: [{ layout: \"block\", items: [ {item:\"body\"} ], width: \"1pr\", height: \"1pr\"}\n ,{ layout: \"block\", items: [ {item:\"footer\"} ]}\n ]\n }\n , body: { type: 'panel'\n , lines: []\n , spacers: { top:true, bottom:true, left:true, right:true }\n , width: \"1pr\", height: \"1pr\"\n }\n , footer: { type: 'panel'\n , lines: [{items: [ {item:\"previewbutton\"}\n , {item:\"spacer\"}\n , {item:\"okbutton\"}\n , {item:\"cancelbutton\"}\n ]}\n ]\n , spacers: { top:true, bottom:true, left:true, right:true }\n , isfooter: true\n , width:'1pr'\n }\n , previewaction: { type: 'action', hashandler: true, unmasked_events: ['execute'] } //ADDME can we lose the hashandler requirement? perhaps even unmasked_events ?\n , previewbutton: { type: 'button', title: getTid(\"tollium:components.imgedit.filters.preview\"), action: 'previewaction' }\n , spacer: { type: 'text', width: \"1pr\", value: \"\" }\n , okaction: { type: 'action', hashandler: true, unmasked_events: ['execute'] } //ADDME can we lose the hashandler requirement? perhaps even unmasked_events ?\n , okbutton: { type: 'button', title: getTid(\"tollium:common.actions.ok\"), action: 'okaction' }\n , cancelaction: { type: 'action', hashandler: true, unmasked_events: ['execute'] } //ADDME can we lose the hashandler requirement? perhaps even unmasked_events ?\n , cancelbutton: { type: 'button', title: getTid(\"tollium:common.actions.cancel\"), action: 'cancelaction' }\n , ...this.options.components\n };\n\n Object.keys(this.options.components).forEach(key =>\n {\n dialog.body.lines.push({ title: this.options.components[key].title, items: [ { item: key } ] });\n });\n\n this.dialog = this.options.createScreen(dialog);\n\n this.dialog.setMessageHandler(\"previewaction\", \"execute\", this._onFilterPreviewButton.bind(this));\n this.dialog.setMessageHandler(\"okaction\", \"execute\", this._onFilterOkButton.bind(this));\n this.dialog.setMessageHandler(\"cancelaction\", \"execute\", this._onFilterCancelButton.bind(this));\n this.dialog.setMessageHandler(\"frame\", \"close\", this._onFilterCancelButton.bind(this));\n }\n\n _closeDialog()\n {\n // Close editor dialog if still present\n if (this.dialog)\n this.dialog.terminateScreen();\n this.dialog = null;\n\n // Close busylock if still present\n if (this.busylock)\n this.busylock.release();\n this.busylock = null;\n }\n\n _getComponentValues()\n {\n var values = {};\n\n Object.keys(this.options.components).forEach(key =>\n {\n values[key] = this.dialog.getComponent(key).getValue();\n });\n return values;\n }\n\n _onFilterPreviewButton(data, callback)\n {\n callback();\n this.options.onButton({ button: \"preview\", values: this._getComponentValues() });\n }\n\n _onFilterOkButton(data, callback)\n {\n callback();\n this.options.onButton({ button: \"ok\", values: this._getComponentValues() });\n this._closeDialog();\n }\n\n _onFilterCancelButton(data, callback)\n {\n callback();\n this.options.onButton({ button: \"cancel\" });\n this._closeDialog();\n }\n};\n\nfunction addFiltersButton(toolbar, surface, options)\n{\n var filters = new PhotoFilters(surface, options);\n\n var button = new Toolbar.Button(toolbar,\n { label: getTid(\"tollium:components.imgedit.editor.filters\")\n , icon: toddImages.createImage(\"tollium:misc/levers\", 24, 24, \"b\")\n , onExecute: filters.startFiltering.bind(filters, toolbar)\n });\n toolbar.addButton(button);\n\n return { button: button, comp: filters };\n}\n\nexports.addFiltersButton = addFiltersButton;\n", "var Toolbar = require('../toolbar/toolbars');\nrequire('./imageeditor.css');\nvar ImageSurface = require('./surface');\nvar Crop = require('./crop');\nvar Scaling = require('./scaling');\nvar Refpoint = require('./refpoint');\nvar Filters = require('./filters');\nvar getTid = require(\"@mod-tollium/js/gettid\").getTid;\nimport \"./imageeditor.lang.json\";\nimport \"../../common.lang.json\";\nvar toddImages = require(\"@mod-tollium/js/icons\");\nimport * as dompack from 'dompack';\n\n// Impose some limits on image sizes\n//ADDME: Should these be different for other platforms, e.g. mobile?\nvar MAX_IMAGE_LENGTH = 32767; // Max length of one size\nvar MAX_IMAGE_AREA = 15000000; // Max number of pixels\n\n/*\nSupported debug flags:\n isc Set SmartCrop debug flag\n ixf Enable experimental filters\n*/\n\nclass ImageEditor\n{\n constructor(el,options)\n {\n this.el = null;\n this.toolbar = null;\n this.surface = null;\n this.cropper = null;\n this.rotator = null;\n this.mimetype = \"\";\n this.filename = \"\";\n this.orgblob = null;\n this.cropsize = null; // { width: 0, height: 0 }\n this.cropratio = null; // { width: 0, height: 0 }\n this.fixorientation = true;\n this.allowedactions = [];\n this.allowedfilters = [];\n this.previewing = false;\n this.dirty = false;\n this.options = { width: 640\n , height: 320 //ADDME default toolbar height!\n , toolbarheight: 72\n , imgsize: null\n , resourcebase: \"\"\n , getBusyLock: null\n , setStatus: null\n , createScreen: null\n , setModalLayerOpacity: null\n , editorBackground: \"\"\n , ...options\n , maxLength: MAX_IMAGE_LENGTH\n , maxArea: MAX_IMAGE_AREA\n };\n\n this.el = el;\n\n this.toolbar = new Toolbar({ applyicon: toddImages.createImage(\"tollium:actions/apply\", 24, 24, \"b\")\n , applylabel: getTid(\"tollium:common.actions.apply\")\n , closeicon: toddImages.createImage(\"tollium:actions/cancel\", 24, 24, \"b\")\n , closelabel: getTid(\"tollium:common.actions.cancel\")\n });\n this.surface = new ImageSurface(this.el, this.toolbar, options);\n this.el.addEventListener(\"tollium-imageeditor:ready\", evt => this.onLoad(evt));\n this.el.addEventListener(\"tollium-imageeditor:refresh\", evt => this.previewImgSize(evt) );\n this.el.addEventListener(\"tollium-imageeditor:undo\", evt => this.previewImgSize(evt) );\n this.el.addEventListener(\"tollium-imageeditor:redo\", evt => this.previewImgSize(evt) );\n\n dompack.empty(this.el);\n this.el.appendChild(this.toolbar.toElement());\n this.el.appendChild(this.surface.toElement());\n this.setSize(this.options.width, this.options.height);\n\n // Add toolbar buttons\n this.undobutton = ImageSurface.addUndoButton(this.toolbar, this.surface).button;\n this.redobutton = ImageSurface.addRedoButton(this.toolbar, this.surface).button;\n if (this.options.resetImage)\n {\n this.toolbar.addButton(new Toolbar.Button(this.toolbar,\n { label: getTid(\"tollium:common.actions.reset\")\n , icon: toddImages.createImage(\"tollium:actions/reset\", 24, 24, \"b\")\n , onExecute: this.resetImage.bind(this)\n }));\n }\n this.toolbar.addButton(new Toolbar.Separator(this.toolbar));\n\n this.cropper = Crop.addImageCropButton(this.toolbar, this.surface,\n { fixedsize: this.cropsize\n , ratiosize: this.cropratio\n , setStatus: this.setStatus.bind(this)\n });\n this.rotator = Scaling.addImageRotateButton(this.toolbar, this.surface,\n { setStatus: this.setStatus.bind(this)\n });\n this.filters = Filters.addFiltersButton(this.toolbar, this.surface,\n { resourcebase: this.options.resourcebase\n , setStatus: this.setStatus.bind(this)\n , setProgress: options.setProgress\n , createScreen: this.options.createScreen\n , getAllowedFilters: this.getAllowedFilters.bind(this)\n , setModalLayerOpacity: this.options.setModalLayerOpacity\n });\n this.pointer = Refpoint.addRefPointButton(this.toolbar, this.surface,\n { setStatus: this.setStatus.bind(this)\n });\n if (this.options.imgsize)\n {\n this.previewing = true;\n this.applyImgSize();\n }\n }\n onLoad(event)\n {\n this.previewImgSize();\n this.surface.fireEvent(\"load\", { target: this, width: event.detail.size.x, height: event.detail.size.y }); //who was listening ??\n }\n setSize(w,h)\n {\n this.toolbar.setSize(w, this.options.toolbarheight);\n this.surface.setSize(w, h-this.options.toolbarheight);\n this.previewImgSize();\n }\n setImg(img, options)\n {\n this.mimetype = options.mimetype;\n this.filename = options.filename;\n this.orgblob = options.orgblob;\n this.surface.setImg(img, options);\n }\n getImageAsBlob(callback)\n {\n if(!this.surface.ctx)\n {\n setTimeout(function()\n {\n callback(null); //not ready yet\n }, 1);\n return;\n }\n\n var canvas = this.surface.canvas;\n var mimetype = this.mimetype;\n\n var settings = { refpoint: this.surface.refpoint ? { x: Math.round(this.surface.refpoint.x)\n , y: Math.round(this.surface.refpoint.y)\n } : null};\n if (this.options.imgsize)\n {\n // If the image didn't actually change, we can return the original blob directly\n if (!this.surface.isModified() && !ImageEditor.resizeMethodApplied(this.options.imgsize, canvas.width, canvas.height, mimetype))\n {\n // Call callback after a delay; maybe the caller doesn't expect the callback to be called directly\n var blob = this.orgblob;\n setTimeout(function()\n {\n callback(blob, settings);\n }, 1);\n return;\n }\n var res = resizeCanvasWithMethod(canvas, this.options.imgsize, this.surface.refpoint || this.isRefpointAllowed(), true);\n if (res)\n {\n if (res.rect && res.rect.refpoint)\n settings.refpoint = { x: Math.round(res.rect.refpoint.x)\n , y: Math.round(res.rect.refpoint.y)\n };\n canvas = res.canvas;\n }\n mimetype = this.options.imgsize.format || mimetype;\n }\n\n canvas.toBlob(function(blob)\n {\n callback(blob, settings);\n }, mimetype, 0.85);\n }\n stop()\n {\n this.surface.stop();\n }\n isDirty()\n {\n return this.dirty || this.surface.isDirty();\n }\n applyImgSize()\n {\n if (this.options.imgsize)\n {\n if (this.options.imgsize.setwidth > 0 && this.options.imgsize.setheight > 0)\n {\n this.cropratio = { width: this.options.imgsize.setwidth\n , height: this.options.imgsize.setheight\n };\n if (this.cropper)\n this.cropper.comp.options.ratiosize = this.cropratio;\n }\n\n this.fixorientation = this.options.imgsize.fixorientation;\n this.allowedactions = this.options.imgsize.allowedactions;\n this.allowedfilters = this.options.imgsize.allowedfilters;\n }\n else\n {\n this.allowedactions = [];\n this.allowedfilters = [];\n }\n\n this.updateActionButtons();\n this.previewImgSize();\n }\n previewImgSize(event)\n {\n if(!this.surface.ctx)\n return; //not ready yet\n\n if (event && event.norefresh)\n return;\n\n var canvas = this.surface.canvas;\n if (this.previewing && this.options.imgsize)\n {\n var resized = resizeCanvasWithMethod(canvas, this.options.imgsize, this.surface.refpoint || this.isRefpointAllowed());\n if (resized)\n {\n this.surface.setPreviewCanvas(resized.canvas, resized.rect);\n this.setStatus(resized.rect ? resized.rect.width : resized.canvas.width,\n resized.rect ? resized.rect.height : resized.canvas.height,\n canvas.width, canvas.height);\n }\n else\n {\n this.surface.setPreviewCanvas(null);\n this.setStatus(canvas.width, canvas.height);\n }\n this.previewing = true;\n }\n }\n setStatus(width, height, orgwidth, orgheight)\n {\n var status = (this.filename ? this.filename + \": \" : \"\")\n + width + \"\\u00d7\" + height\n + (orgwidth && orgheight ? \" (\" + orgwidth + \"\\u00d7\" + orgheight + \")\" : \"\");\n var minwarning = (orgwidth > 0 && orgwidth < width) || (orgheight > 0 && orgheight < height);\n var maxwarning = (orgwidth > 0 || orgheight > 0)\n && this.surface.imagelimited\n && !this.surface.undostack.some(function(item) { return item.action == \"crop\"; });\n this.options.setStatus(status, minwarning ? \"min\" : maxwarning ? \"max\" : null);\n }\n updateActionButtons()\n {\n var allallowed = this.allowedactions.indexOf(\"all\") >= 0;\n this.cropper.button.node.style.display = allallowed || this.allowedactions.indexOf(\"crop\") >= 0 ? \"\" : \"none\";\n this.rotator.button.node.style.display = allallowed || this.allowedactions.indexOf(\"rotate\") >= 0 ? \"\" : \"none\";\n this.filters.button.node.style.display = allallowed || this.allowedactions.indexOf(\"filters\") >= 0 ? \"\" : \"none\";\n this.pointer.button.node.style.display = this.isRefpointAllowed() ? \"\" : \"none\";\n }\n isRefpointAllowed()\n {\n // Setting the reference point only makes sense if the image is not resized (it may be resized in the image cache using\n // the reference point) or if the resize method is fill (which actually crops the image). It is not enabled when 'all'\n // actions are allowed; it has to be enabled explicitly.\n var method_refpoint = !this.options.imgsize || this.options.imgsize.method == \"none\" || this.options.imgsize.method == \"fill\";\n return method_refpoint && this.allowedactions.indexOf(\"refpoint\") >= 0;\n }\n getAllowedFilters()\n {\n return this.allowedfilters;\n }\n resetImage()\n {\n this.options.resetImage().then(function(result)\n {\n this.dirty = this.dirty || result == \"yes\";\n }.bind(this));\n }\n}\n\nfunction resizeCanvasWithMethod(canvas, imgsize, refpoint, forupload)\n{\n let resizemethod = imgsize.method;\n if (resizemethod === \"\")\n return;\n\n if (resizemethod === \"none\")\n {\n // Use 'fill' method for previewing refpoint when method is 'none'\n if (refpoint && !forupload)\n resizemethod = \"fill\";\n else\n return;\n }\n\n var canvaswidth = imgsize.setwidth;\n var canvasheight = imgsize.setheight;\n if (canvaswidth || canvasheight)\n {\n var imagewidth = canvas.width;\n var imageheight = canvas.height;\n var imagetop = 0;\n var imageleft = 0;\n if (!canvaswidth)\n {\n // If only height is restricted, scale width proportionally\n canvaswidth = Math.round(canvasheight * imagewidth / imageheight);\n }\n else if (!canvasheight)\n {\n // If only width is restricted, scale height proportionally\n canvasheight = Math.round(canvaswidth * imageheight / imagewidth);\n }\n\n if (resizemethod == \"stretch\")\n {\n // Just stretch to canvas\n imagewidth = canvaswidth;\n imageheight = canvasheight;\n }\n else if (resizemethod.indexOf(\"fit\") === 0 && imagewidth <= canvaswidth && imageheight <= canvasheight)\n {\n // Don't resize\n if (resizemethod == \"fit\")\n {\n canvaswidth = imagewidth;\n canvasheight = imageheight;\n }\n }\n else if (canvaswidth / canvasheight > imagewidth / imageheight)\n {\n // canvas is more wide than image\n if (resizemethod.indexOf(\"scale\") === 0\n || (resizemethod.indexOf(\"fit\") === 0 && imageheight > canvasheight))\n {\n // Scale width proportionally, keep height\n imagewidth = Math.round(canvasheight * imagewidth / imageheight);\n imageheight = canvasheight;\n // If not scaling to canvas, only keep image width\n if (resizemethod.indexOf(\"canvas\") < 0)\n canvaswidth = imagewidth;\n }\n else if (resizemethod == \"fill\")\n {\n // Scale height proportionally, keep width\n imageheight = Math.round(canvaswidth * imageheight / imagewidth);\n imagewidth = canvaswidth;\n }\n }\n else\n {\n // canvas is more tall than image\n if (resizemethod.indexOf(\"scale\") === 0\n || (resizemethod.indexOf(\"fit\") === 0 && imagewidth > canvaswidth))\n {\n // Scale height proportionally, keep width\n imageheight = Math.round(canvaswidth * imageheight / imagewidth);\n imagewidth = canvaswidth;\n // If not scaling to canvas, only keep image height\n if (resizemethod.indexOf(\"canvas\") < 0)\n canvasheight = imageheight;\n }\n else if (resizemethod == \"fill\")\n {\n // Scale width proportionally, keep height\n imagewidth = Math.round(canvasheight * imagewidth / imageheight);\n imageheight = canvasheight;\n }\n }\n\n // Center image\n imagetop = Math.round((canvasheight - imageheight) / 2);\n imageleft = Math.round((canvaswidth - imagewidth) / 2);\n\n var rect = null;\n if (resizemethod == \"fill\")\n {\n // When filling, either top or left is 0, the other is <0\n rect = { left: Math.abs(imageleft)\n , top: Math.abs(imagetop)\n , offsetx: 0\n , offsety: 0\n , width: canvaswidth\n , height: canvasheight\n , refpoint: null // Refpoint relative to resized image\n };\n if (refpoint && refpoint !== true)\n {\n if (!rect.left)\n {\n var curtop = rect.top;\n var scalex = imagewidth / canvas.width;\n rect.top = (refpoint.y * scalex / imageheight) * (imageheight - canvasheight);\n rect.offsety = rect.top - curtop;\n rect.refpoint = { x: refpoint.x * scalex\n , y: refpoint.y * scalex - rect.top\n };\n }\n else if (!rect.top)\n {\n var curleft = rect.left;\n var scaley = imageheight / canvas.height;\n rect.left = (refpoint.x * scaley / imagewidth) * (imagewidth - canvaswidth);\n rect.offsetx = rect.left - curleft;\n rect.refpoint = { x: refpoint.x * scaley - rect.left\n , y: refpoint.y * scaley\n };\n }\n }\n\n if (!forupload)\n {\n canvaswidth = imagewidth;\n canvasheight = imageheight;\n imagetop = 0;\n imageleft = 0;\n }\n else\n {\n imagetop -= rect.offsety;\n imageleft -= rect.offsetx;\n }\n }\n\n // Create the resized canvas\n var resized = \n var ctx = resized.getContext(\"2d\");\n // Set background color, if specified\n if (imgsize.bgcolor !== \"\" && imgsize.bgcolor != \"transparent\")\n {\n ctx.fillStyle = imgsize.bgcolor;\n ctx.fillRect(0, 0, canvaswidth, canvasheight);\n }\n // Draw (and possibly resize) the editor image onto the resized canvas\n ctx.drawImage(canvas, imageleft, imagetop, imagewidth, imageheight);\n return { canvas: resized, rect: rect };\n }\n}\n\n// Check if the given resize method is applied for an image with given widht, height and MIME type\nImageEditor.resizeMethodApplied = function(imgsize, width, height, mimetype)\n{\n // If preserveifunchanged is not set (unless resize method is \"none\"), the method is applied\n if (!imgsize.noforce && imgsize.method != \"none\")\n return true;\n\n // If the image doesn't have the expected MIME type, the method is applied\n if (imgsize.format !== \"\" && mimetype != imgsize.format)\n return true;\n\n switch (imgsize.method)\n {\n case \"none\":\n {\n // The image would not be resized, skip editor\n return false;\n }\n case \"fill\":\n case \"fitcanvas\":\n case \"scalecanvas\":\n case \"stretch\":\n {\n // Image method is applied if the image doesn't match both the set width and height exactly\n //ADDME: If image has transparency, only skip editor if conversionbackground is transparent\n return width != imgsize.setwidth || height != imgsize.setheight;\n }\n case \"fit\":\n {\n // Image method is applied if the image is bigger than to the set width and/or height\n return (imgsize.setwidth > 0 && width > imgsize.setwidth)\n || (imgsize.setheight > 0 && height > imgsize.setheight);\n }\n case \"scale\":\n {\n // Image method is applied if the image size has an incorrect width and/or height\n return (imgsize.setwidth > 0 && width != imgsize.setwidth)\n || (imgsize.setheight > 0 && height != imgsize.setheight);\n }\n }\n // Don't know, assume it's applied\n return true;\n};\n\nmodule.exports = ImageEditor;\n", "/* globals webkitURL */\nrequire(\"../../common.lang.json\");\nrequire(\"../../components/imageeditor/imageeditor.lang.json\");\nimport * as whintegration from '@mod-system/js/wh/integration';\nimport { runSimpleScreen } from '@mod-tollium/web/ui/js/dialogs/simplescreen';\n\nimport * as dompack from 'dompack';\n//TODO how did webpack find this? require(\"exif-parser\");\nvar ExifParser = require(\"@mod-tollium/webdesigns/webinterface/node_modules/exif-parser\");\n\nvar getTid = require(\"@mod-tollium/js/gettid\").getTid;\n\nimport $todd from \"@mod-tollium/web/ui/js/support\";\nvar ImageEditor = require(\"../../components/imageeditor\");\n\n// http://www.nixtu.info/2013/06/how-to-upload-canvas-data-to-server.html\nfunction dataURItoBlob(dataURI)\n{\n // convert base64 to raw binary data held in a string\n // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this\n var byteString = atob(dataURI.split(',')[1]);\n\n // separate out the mime component\n var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];\n\n // write the bytes of the string to an ArrayBuffer\n var ab = new ArrayBuffer(byteString.length);\n var dw = new DataView(ab);\n for(var i = 0; i < byteString.length; i++)\n {\n dw.setUint8(i, byteString.charCodeAt(i));\n }\n\n // write the ArrayBuffer to a blob, and you're done\n return new Blob([ ab ], { type: mimeString });\n};\n\n\nclass ImgeditDialogController\n{\n constructor (screen, options)\n {\n this.screen = null;\n this.dialog = null;\n this.editor = null;\n this.busylock = null;\n this.imageurl = null;\n this.editorsize = null;\n this.activetool = null;\n this.options =\n { imgsize: null\n , action: 'upload'\n , resetImage: null\n , ...options\n };\n\n this.defer = dompack.createDeferred();\n this.screen = screen;\n\n var desktopsize = screen.displayapp.container.getBoundingClientRect();\n this.editorsize = { x: parseInt(0.7 * desktopsize.width)\n , y: parseInt(0.9 * desktopsize.height)\n };\n }\n\n loadImageBlob(blob, settings)\n {\n if (this.busylock)\n return;\n\n // Take a busy lock during loading\n this.busylock = this.screen.displayapp.getBusyLock();\n\n this._readImageFile(blob, settings);\n }\n\n loadImageSrc(src, settings)\n {\n if (this.busylock)\n return;\n\n // Take a busy lock during loading\n this.busylock = this.screen.displayapp.getBusyLock();\n\n if (src.indexOf(\"data:\") == 0)\n {\n //console.log(\"Convert image data from data URL to blob\");\n var blob = dataURItoBlob(src);\n this._readImageFile(blob, settings);\n }\n else\n {\n var request = new XMLHttpRequest();\n request.onload = evt =>\n {\n //console.log(\"Received image file as Blob\");\n var mimeType = request.getResponseHeader(\"Content-Type\");\n var blob = new Blob([ request.response ], { type: mimeType });\n this._readImageFile(blob, settings);\n };\n request.open(\"GET\", src, true);\n request.responseType = \"blob\";\n //console.log(\"Load image file as Blob\");\n request.send();\n }\n }\n\n _readImageFile(file, settings)\n {\n var reader = new FileReader();\n var fixorientation = this.editor ? this.editor.fixorientation : this.options.imgsize ? this.options.imgsize.fixorientation : true;\n\n // Read the image as ArrayBuffer, so we can read its EXIF data\n reader.onload = () =>\n {\n var exifdata;\n try\n {\n if (fixorientation)\n {\n var parser = ExifParser.create(reader.result);\n exifdata = parser.parse();\n }\n }\n catch (e) {}\n //console.log(\"Parsed EXIF data\", exifdata);\n\n var objecturl = (URL || webkitURL).createObjectURL(file);\n var options = { orientation: exifdata && exifdata.tags.Orientation\n , mimetype: file.type\n , refpoint: null\n , filename: \"\"\n , ...settings\n }\n options.orgblob = file;\n this._loadImageUrl(objecturl, options);\n };\n //console.log(\"Read image file as ArrayBuffer\");\n reader.readAsArrayBuffer(file);\n }\n\n _loadImageUrl(url, options)\n {\n var img = new Image(); //FIXME error handler\n img.addEventListener(\"load\", (function()\n {\n (URL || webkitURL).revokeObjectURL(url);\n\n if (this.editor)\n {\n // The editor dialog is already opened, load the image into the editor\n //console.log(\"Load image into editor using object URL\");\n this.editor.setImg(img, options);\n }\n else\n {\n // If this is an uploaded image which would not be changed by the image resize method, upload it directly\n if (this._skipEditor(img.width, img.height, options.mimetype))\n {\n //console.log(\"Fire 'done' event to upload the blob\");\n this._closeImageEditor(options.orgblob, null);\n }\n else\n {\n //console.log(\"Create image editor dialog with object URL\");\n this._createDialog();\n\n // Set image in a delay, so it's set after the relayout when showing the dialog, preventing an initial image resize\n setTimeout(() => this.editor.setImg(img, options),1);\n }\n }\n }).bind(this));\n\n img.addEventListener(\"error\", e =>\n {\n console.error(\"Error loading image in imgeditor element\", e);\n if (this.busylock)\n this.busylock.release();\n this.busylock = null;\n\n runSimpleScreen(this.screen.displayapp,\n { title: getTid(\"tollium:components.imgedit.editor.title\")\n , text: getTid(\"tollium:components.imgedit.messages.corruptimage\")\n , icon: \"warning\"\n , buttons: [ { name: \"close\", title: getTid(\"tollium:common.actions.close\") }\n ]\n });\n });\n img.src = url;\n }\n\n _skipEditor(width, height, mimetype)\n {\n //console.log(this.options.action,this.options.imgsize,width,height,mimetype);\n // When editing, show editor\n if (this.options.action == \"edit\")\n return false;\n\n return !ImageEditor.resizeMethodApplied(this.options.imgsize, width, height, mimetype);\n }\n\n _createDialog()\n {\n this.dialog = this.screen.displayapp.createScreen(\n { frame: { bodynode: 'root'\n , specials: ['okaction','cancelaction']\n , title: getTid(\"tollium:components.imgedit.editor.title\")\n , defaultbutton: \"okbutton\"\n //, allowresize: true\n , allowclose: true\n , width: this.editorsize.x + \"px\", height: this.editorsize.y + \"px\"\n }\n , root: { type: 'panel', lines: [ { layout: \"block\", items: [ {item:\"body\"} ], width: \"1pr\", height: \"1pr\"}\n , { layout: \"block\", items: [ {item:\"footer\"} ]}\n ]\n }\n , body: { type: 'panel'\n , lines: [ { layout: \"block\", title: \"\", items: [{item:\"imageeditor\"}], width: \"1pr\", height: \"1pr\" }\n ]\n , width: \"1pr\", height: \"1pr\"\n }\n , footer: { type: 'panel'\n , lines: [{items: [ {item:\"minsizewarning\"}\n , {item:\"maxsizewarning\"}\n , {item:\"status\"}\n , {item:\"progress\"}\n , {item:\"okbutton\"}\n , {item:\"cancelbutton\"}\n ]}\n ]\n , spacers: { top:true, bottom:true, left:true, right:true }\n , isfooter: true\n , width:'1pr'\n }\n , minsizewarning: { type: 'image', width: \"16px\", height: \"16px\", hint: getTid(\"tollium:components.imgedit.messages.minsizewarning\")\n , imgwidth: 16, imgheight: 16, settings: { imgname: \"tollium:status/warning\", width: 16, height: 16, color: \"b\" }\n , visible: false\n }\n , maxsizewarning: { type: 'image', width: \"16px\", height: \"16px\", hint: getTid(\"tollium:components.imgedit.messages.maxsizewarning\")\n , imgwidth: 16, imgheight: 16, settings: { imgname: \"tollium:status/warning\", width: 16, height: 16, color: \"b\" }\n , visible: false\n }\n , status: { type: 'text', width: \"1pr\", ellipsis: true, value: \"\" }\n , progress: { type: 'progress', width: \"150px\", max: 0, value: 0, visible: false }\n , okaction: { type: 'action', hashandler: true, unmasked_events: ['execute'] } //ADDME can we lose the hashandler requirement? perhaps even unmasked_events ?\n , okbutton: { type: 'button', title: getTid(\"tollium:common.actions.save\"), action: 'okaction' }\n , cancelaction: { type: 'action', hashandler: true, unmasked_events: ['execute'] } //ADDME can we lose the hashandler requirement? perhaps even unmasked_events ?\n , cancelbutton: { type: 'button', title: getTid(\"tollium:common.actions.cancel\"), action: 'cancelaction' }\n , imageeditor: { type: 'customhtml', width: \"1pr\", height: \"1pr\" }\n });\n this.modallayer = this.dialog.node.querySelector(\".modallayer\");\n\n this.dialog.setMessageHandler(\"okaction\", \"execute\", this._onEditorOkButton.bind(this));\n this.dialog.setMessageHandler(\"cancelaction\", \"execute\", this._onEditorCancelButton.bind(this, false));\n this.dialog.setMessageHandler(\"frame\", \"close\", this._onEditorCancelButton.bind(this, true));\n\n // Initialize the image editor - ADDME can't we promote the imageeditor or its wrapper to a 'real' tollium element instead of the customhtml hack?\n var containercomp = this.dialog.getComponent(\"imageeditor\");\n var container = containercomp.getContainer();\n container.addEventListener(\"tollium:resized\", evt => this._onEditorResized(evt.detail));\n\n var options = { width: container.offsetWidth\n , height: container.offsetHeight\n , imgsize: this.options.imgsize\n , resourcebase: $todd.resourcebase\n , getBusyLock: this.screen.displayapp.getBusyLock.bind(this.screen.displayapp)\n , setStatus: this._setStatus.bind(this)\n , setProgress: this._setProgress.bind(this)\n , createScreen: this.screen.displayapp.createScreen.bind(this.screen.displayapp)\n , setModalLayerOpacity: this._setModalLayerOpacity.bind(this)\n , editorBackground: \"#ffffff url(\" + whintegration.config.obj.checkered_background + \") top left\"\n };\n\n if (this.options.action == \"edit\")\n {\n if (this.options.resetImage)\n options.resetImage = this.options.resetImage;\n //ADDME: Drag-n-drop file upload in image editor?\n }\n\n this.editor = new ImageEditor(container, options);\n container.addEventListener(\"tollium-imageeditor:load\", () => this._onEditorReady());\n this.editor.toolbar.toElement().addEventListener(\"modal-opened\", this._onEditorOpenTool.bind(this));\n this.editor.toolbar.toElement().addEventListener(\"modal-closed\", this._onEditorCloseTool.bind(this));\n }\n\n _relayoutDialog()\n {\n var frame = this.dialog.getComponent(\"frame\");\n frame.recalculateDimensions();\n frame.relayout();\n }\n\n _closeDialog()\n {\n // Close editor dialog if still present\n if (this.dialog)\n this.dialog.terminateScreen();\n this.dialog = null;\n\n if (this.editor)\n this.editor.stop();\n this.editor = null;\n\n // Close busylock if still present\n if (this.busylock)\n this.busylock.release();\n this.busylock = null;\n }\n\n _setStatus(status, warning)\n {\n this.dialog.getComponent(\"minsizewarning\").setVisible(warning === \"min\");\n this.dialog.getComponent(\"maxsizewarning\").setVisible(warning === \"max\");\n this.dialog.getComponent(\"status\").setValue(status);\n this._relayoutDialog();\n }\n\n _setProgress(value, max)\n {\n this.dialog.getComponent(\"progress\").onMsgSetValMax({ value: value, max: max });\n this.dialog.getComponent(\"progress\").setVisible(!!max);\n this._relayoutDialog();\n }\n\n _setModalLayerOpacity(opacity)\n {\n this.modallayer.style.opacity = opacity;\n }\n\n // sendblob true: retrieve image from editor\n // sendblob not null: send sendblob\n // sendblob null: don't send blob\n _closeImageEditor(sendblob, callback)\n {\n if (sendblob === true)\n {\n // Retrieve the image from the editor and close the dialog\n this.busylock = this.screen.displayapp.getBusyLock();\n this.editor.getImageAsBlob((blob, settings) =>\n {\n this.defer.resolve({ blob: blob, settings: settings, editcallback: () =>\n {\n if (callback)\n callback();\n this._closeDialog();\n }});\n });\n }\n else\n {\n // Upload the given blob and close the dialog\n this.defer.resolve({ blob: sendblob, settings: null, editcallback: () =>\n {\n if (callback)\n callback();\n this._closeDialog();\n }});\n }\n }\n\n _onEditorReady()\n {\n if (this.busylock)\n this.busylock.release();\n this.busylock = null;\n }\n\n _onEditorResized(data)\n {\n this.editorsize = { x: data.x, y: data.y };\n if (this.editor)\n this.editor.setSize(this.editorsize.x, this.editorsize.y);\n }\n\n _onEditorOpenTool(event)\n {\n this.activetool = event.detail;\n this.dialog.getComponent(\"okbutton\").setTitle(getTid(\"tollium:common.actions.ok\"));\n this.dialog.getComponent(\"frame\").setTitle(getTid(\"tollium:components.imgedit.editor.\" + this.activetool.panel._imgedittool));\n this._relayoutDialog();\n }\n\n _onEditorCloseTool()\n {\n this.activetool = null;\n this.dialog.getComponent(\"okbutton\").setTitle(getTid(\"tollium:common.actions.save\"));\n this.dialog.getComponent(\"frame\").setTitle(getTid(\"tollium:components.imgedit.editor.title\"));\n this._relayoutDialog();\n }\n\n _onEditorOkButton(data, callback)\n {\n if (this.activetool)\n {\n // Apply the active tool\n this.activetool.apply();\n callback();\n }\n else\n {\n this._closeImageEditor(true, callback, null);\n }\n }\n\n async _onEditorCancelButton(frameclose, data, callback)\n {\n if (this.activetool)\n {\n // Closing the window when a tool is active\n if (frameclose)\n {\n let dialog = runSimpleScreen(this.screen.displayapp,\n { title: getTid(\"tollium:components.imgedit.editor.title\")\n , text: getTid(\"tollium:components.imgedit.messages.confirmdiscardtool\")\n , icon: \"warning\"\n , buttons: [ { name: \"yes\", title: getTid(\"tollium:common.actions.yes\") }\n , { name: \"no\", title: getTid(\"tollium:common.actions.no\") }\n ]\n });\n callback();\n\n if(await dialog == 'yes')\n this._closeImageEditor();\n\n return;\n }\n\n // Cancel the active tool\n this.activetool.cancel();\n callback();\n }\n else\n {\n // Closing the window when the image is edited\n if (this.editor.isDirty())\n {\n let dialog = runSimpleScreen(this.screen.displayapp,\n { title: getTid(\"tollium:components.imgedit.editor.title\")\n , text: getTid(\"tollium:components.imgedit.messages.confirmdiscardchanges\")\n , icon: \"warning\"\n , buttons: [ { name: \"yes\", title: getTid(\"tollium:common.actions.yes\") }\n , { name: \"no\", title: getTid(\"tollium:common.actions.no\") }\n , { name: \"cancel\", title: getTid(\"tollium:common.actions.cancel\") }\n ]\n });\n\n callback();\n\n if(await dialog == 'yes')\n this._closeImageEditor(true);\n else if(await dialog == 'no')\n this._closeImageEditor();\n\n return;\n }\n\n this._closeImageEditor(null, callback);\n }\n }\n}\n\nImgeditDialogController.checkTypeAllowed = function(screen, type)\n{\n let allowed_mimetypes = [ \"image/jpeg\", \"image/png\", \"image/gif\" ];\n if (!allowed_mimetypes.includes(type))\n {\n runSimpleScreen(screen.displayapp,\n { title: getTid(\"tollium:components.imgedit.editor.title\")\n , text: getTid(\"tollium:components.imgedit.messages.unsupportedtype\")\n , icon: \"warning\"\n , buttons: [ { name: \"close\", title: getTid(\"tollium:common.actions.close\") }\n ]\n });\n return false;\n }\n return true;\n};\n\nexport default ImgeditDialogController;\n", "import * as dompack from 'dompack';\nimport * as browser from 'dompack/extra/browser';\nimport '@mod-system/js/wh/integration'; //make debugflags work\n\n\n/// Global queue manager object\nvar queue_manager = null;\n\nlet default_upload_chunk_size = 10000000; // 10 MB\nvar moving_average_max_history = 20000; // average current speed over max 20000 ms of history\nvar moving_average_min_history = 2000; // Need min 2000ms of history\n\n\nexport default class EventTarget\n{\n constructor()\n {\n this.handlers = {};\n }\n addEventListener(eventtype, fn)\n {\n let eventhandlers = this.handlers[eventtype];\n if(!eventhandlers)\n eventhandlers = this.handlers[eventtype] = [];\n eventhandlers.push(fn);\n }\n removeEventListener(eventtype, fn)\n {\n let eventhandlers = this.handlers[eventtype];\n if(eventhandlers)\n eventhandlers = eventhandlers.filter(el => el!=fn);\n }\n dispatchEvent(event)\n {\n if(!('defaultPrevented' in event))\n throw new Error(\"Parameter passed is not an event\");\n\n let eventhandlers = this.handlers[event.type];\n if(eventhandlers)\n eventhandlers.forEach(fn => fn.call(this, event));\n return event.defaultPrevented;\n }\n}\n\n/** Upload item. Might be a group, or an uploader.\n Fires loadstart, progress*, abort/load/error, loadend events.\n*/\nclass RawUploadItem extends EventTarget\n{\n constructor()\n {\n super();\n\n /** Current status of this upload\n '': Not started or busy\n 'loaded' Upload complete\n 'aborted' Aborted\n 'error' An error occurred\n */\n this.status = ''; // '', 'loaded', 'aborted', 'error'\n\n /// Session id of the item (used to group uploads into one session)\n this.pvt_sessionid = '';\n\n /// Parent group (used for sharing session ids)\n this.pvt_parentgroup = null;\n\n /// Starting time of upload\n this.pvt_start = null;\n\n /// History of progress events (of last moving_average_max_history ms)\n this.pvt_history = [];\n\n /// Finishing time of upload\n this.pvt_end = null;\n }\n\n /// Returns the total size of this item\n getUploadSize()\n {\n return 0;\n }\n\n /// Returns the number of bytes uploaded\n getUploaded()\n {\n return 0;\n }\n\n /// Schedule this item at a queue (or fire events when empty)\n schedule()\n {\n }\n\n /// Abort upload of this item. Must fire events (loadstart, abort, loadend) when not yet scheduled!\n abort()\n {\n }\n\n /// Returns time elapsed, in seconds\n getElapsedTime()\n {\n var now = (new Date).getTime();\n if (!this.pvt_start || this.pvt_start == now)\n return 0;\n\n if (this.pvt_end)\n now = this.pvt_end;\n\n return (now - this.pvt_start) / 1000;\n }\n\n /// Time remaing in seconds (0 if unknown / very long / n/a)\n getRemainingTime()\n {\n var speed = this.getCurrentSpeed();\n if (!speed)\n return 0;\n var remainingbytes = this.getUploadSize() - this.getUploaded();\n return remainingbytes ? (remainingbytes / speed || 1) : 0;\n }\n\n /// Returns the average speed over the whole upload\n getAverageSpeed()\n {\n return this.getUploaded() / this.getElapsedTime();\n }\n\n /// Returns speed over last X seconds\n getCurrentSpeed()\n {\n if (this.pvt_history.length <= 1)\n return null;\n\n var last = this.pvt_history[this.pvt_history.length-1];\n var first = this.pvt_history[0];\n\n if (last.date - first.date < (this.status == 'loaded' ? 1 : moving_average_min_history))\n return null;\n\n return (last.loaded - first.loaded) / ((last.date - first.date) / 1000);\n }\n\n getCompletedFiles()\n {\n return [];\n }\n\n getFileTokens()\n {\n return [];\n }\n\n getSessionId()\n {\n return this.pvt_sessionid || (this.pvt_parentgroup && this.pvt_parentgroup.getSessionId()) || '';\n }\n\n setSessionId (sessionid)\n {\n this.pvt_sessionid = sessionid;\n if (this.pvt_parentgroup)\n this.pvt_parentgroup.setSessionId(sessionid);\n }\n\n getEventDetail()\n {\n return { uploaded: this.getUploaded()\n , size: this.getUploadSize()\n , speed: this.getCurrentSpeed()\n };\n }\n\n fireLoadStart()\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] firing loadstart\", this);\n\n this.pvt_start = (new Date).getTime();\n dompack.dispatchCustomEvent(this, 'loadstart', { bubbles:false, cancelable:false, detail: { type: 'loadstart' }});\n }\n\n fireProgress()\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] firing loadprogress\", this);\n\n var size = this.getUploadSize();\n var loaded = this.getUploaded();\n\n this.addProgressToHistory(loaded);\n dompack.dispatchCustomEvent(this, 'progress', { bubbles:false, cancelable:false, detail: { loaded: loaded, size: size }});\n }\n\n fireLoad()\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] firing load\", this);\n\n var size = this.getUploadSize();\n var loaded = this.getUploaded();\n this.pvt_end = (new Date).getTime();\n\n this.addProgressToHistory(loaded);\n dompack.dispatchCustomEvent(this, 'load', { bubbles:false, cancelable:false, detail: { loaded: loaded, size: size }});\n }\n\n addProgressToHistory(loaded)\n {\n var now = (new Date).getTime();\n this.pvt_history.push({ date: now, loaded: loaded });\n while ((now - this.pvt_history[0].date) > moving_average_max_history) //\n this.pvt_history.splice(0, 1);\n }\n\n fireLoadEnd()\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] firing loadend\", this);\n\n if (!this.pvt_end)\n this.pvt_end = (new Date).getTime();\n dompack.dispatchCustomEvent(this, \"loadend\", { bubbles:false, cancelable:false });\n }\n}\n\n/** Upload item that does uploading by itself\n Fires loadstart, progress*, abort/load/error, loadend\n*/\nclass SchedulableRawUploadItem extends RawUploadItem\n{\n constructor()\n {\n super();\n }\n schedule()\n {\n queue_manager.schedule(this);\n }\n\n canStart()\n {\n }\n\n start()\n {\n }\n\n getCompletedFiles()\n {\n return [];\n }\n\n getFileTokens()\n {\n return [];\n }\n}\n\n/** Aggregates multiple uploader items into one unified upload (all sub-items are aborted upon error). Fires events\n as if the group is one big uploaded item\n\n This is used to group the chunks of a single file upload, but also to group the files in a multifile upload\n*/\nclass UploaderAggregator extends RawUploadItem\n{\n constructor()\n {\n super();\n this.pvt_subitems=[];\n this.pvt_aborting = false;\n this.pvt_sentloadstart = false;\n this.pvt_sentloadend = false;\n }\n\n setItems(subitems)\n {\n this.status = '';\n this.pvt_subitems = subitems;\n this.pvt_aborting = false;\n this.pvt_sentloadstart = false;\n this.pvt_sentloadend = false;\n\n // Listen to events of the sub-items\n this.pvt_subitems.forEach(function(i)\n {\n i.pvt_parentgroup = this;\n i.addEventListener('loadstart', this.gotLoadStart.bind(this));\n i.addEventListener('progress', this.fireProgress.bind(this));\n i.addEventListener('abort', this.gotAbort.bind(this));\n i.addEventListener('error', this.gotError.bind(this));\n i.addEventListener('load', this.gotLoad.bind(this));\n i.addEventListener(\"loadend\", this.gotLoadEnd.bind(this));\n }.bind(this));\n }\n\n /// Schedule all subitems, run some events when empty\n schedule()\n {\n this.pvt_subitems.forEach(function(i,idx) { i.schedule(); });\n\n if (!this.pvt_subitems.length) //simulate an upload\n {\n this.gotLoadStart(null);\n this.gotLoad(null);\n this.gotLoadEnd(null);\n }\n }\n\n getUploadSize()\n {\n var size = 0;\n this.pvt_subitems.forEach(function(i) { size += i.getUploadSize(); });\n return size;\n }\n\n getUploaded()\n {\n var loaded = 0;\n this.pvt_subitems.forEach(function(i) { loaded += i.getUploaded(); });\n return loaded;\n }\n\n abort()\n {\n if (this.pvt_subitems.length)\n {\n if (!this.pvt_aborting)\n this.pvt_aborting = true;\n this.pvt_subitems.forEach(i => { if (!i.status) i.abort(); });\n }\n else // Always send an abort back, even when not having items yet.\n {\n this.gotLoadStart(null);\n this.gotAbort(null);\n this.gotLoadEnd(null);\n }\n }\n\n getCompletedFiles()\n {\n var result = [];\n if (this.status == 'loaded')\n this.pvt_subitems.forEach(function(i) { result = result.concat(i.getCompletedFiles()); });\n //sanitize the result, don't leak internal data\n\n return result.map( file => ({ name: file.name, filetoken: file.filetoken, size: file.size, fileinfo:file.fileinfo, type: file.type, url: file.downloadurl, fullpath: file.fullpath }));\n }\n\n getFileTokens()\n {\n var result = [];\n if (this.status == 'loaded')\n this.pvt_subitems.forEach(function(i) { result = result.concat(i.getFileTokens()); });\n return result;\n }\n\n gotLoadStart(event)\n {\n if (!this.pvt_sendloadstart)\n {\n this.pvt_sendloadstart = true;\n this.fireLoadStart();\n }\n }\n\n gotAbort(event)\n {\n if (!this.status)\n {\n this.status = 'aborted';\n dompack.dispatchCustomEvent(this, 'abort', { bubbles:false, cancelable:false });\n this.abort();\n }\n }\n\n gotError(event)\n {\n if (!this.status)\n {\n this.status = 'error';\n dompack.dispatchCustomEvent(this, 'error', { bubbles:false, cancelable:false });\n this.abort();\n }\n }\n\n gotLoad(event)\n {\n if (!this.status && !this.pvt_subitems.some(function(i) { return i.status != 'loaded'; }))\n {\n this.status = 'loaded';\n this.fireLoad();\n }\n }\n\n gotLoadEnd(event)\n {\n if (!this.pvt_subitems.some(function(i) { return i.status == ''; }) && !this.pvt_sendloadend)\n {\n this.pvt_sendloadend = true;\n this.fireLoadEnd();\n }\n }\n}\n\n\n/** HTML 5 upload items, wraps a HTML5 file\n*/\nexport class Html5UploadItem extends UploaderAggregator\n{\n constructor(host, html5file, options)\n {\n super();\n\n /// Name of the file\n this.name = '';\n\n /// Size of the file\n this.size = 0;\n\n /// Default upload chunk size\n this.upload_chunk_size = (options ? options.uploadchunksize : 0) || default_upload_chunk_size;\n\n /// Content-type of the file\n this.type = '';\n\n /// File token (to retrieve the file on the server)\n this.filetoken = '';\n\n /// Detectfiletype info\n this.fileinfo = null;\n\n /// Original File object (if applicable)\n this.file = null;\n\n /// Parameters to send in request\n this.params = {};\n\n /// Base transfer url\n this.transferbaseurl = '';\n\n this.pvt_host = '';\n this.pvt_fileid = 0;\n\n this.pvt_host = host;\n this.name = html5file.name;\n this.size = html5file.size;\n this.type = html5file.type;\n this.fullpath = html5file.fullpath || '';\n this.file = html5file;\n this.params = options&&options.params?{...options.params}:{};\n this.pvt_file = html5file;\n }\n\n schedule()\n {\n var items = [];\n\n var total = this.file.size;\n if(!(total >= 0))\n throw new Error(\"Invalid file size received\"); //would cause an endless loop!\n\n var ofs = 0;\n while (true)\n {\n // Upload in chunks\n var chunksize = Math.min(this.upload_chunk_size, total - ofs);\n\n items.push(new Html5SingleChunk(this,\n { offset: ofs\n , size: chunksize\n , host: this.pvt_host\n }));\n\n ofs += chunksize;\n if (ofs == total)\n break;\n }\n\n this.setItems(items);\n this.transferbaseurl = items[0].transferbaseurl;\n super.schedule();\n }\n\n getCompletedFiles()\n {\n return this.status == 'loaded' ? [ this ] : [];\n }\n\n getFileTokens()\n {\n return this.filetoken ? [ this.filetoken ] : [];\n }\n}\n\n/** This component uploads a html5 chunk to the upload receiver\n*/\nclass Html5SingleChunk extends SchedulableRawUploadItem\n{\n /** @param uploadfile Upload file\n @param firstchunk For second+ chunks, reference to first chunk (needed to stitch them together at server side)\n @param options\n @cell options.name Name of chunk (needed for first chunk)\n @cell options.fullsize Full size of file (needed for first chunk)\n @cell options.offset Offset of chunk within file\n */\n constructor(uploadfile, options)\n {\n super();\n this.uploadfile = uploadfile;\n this.xmlhttp = null;\n this.pvt_loaded = 0;\n this.pvt_sendloadstart = false;\n this.pvt_sendloadend = false;\n this.options = { offset: 0, size:0, host: '', ...options};\n this.transferbaseurl = (new URL(\"/.system/filetransfer/filetransfer.shtml\", this.options.host)).toString();\n }\n\n getUploadSize()\n {\n return this.options.size;\n }\n\n getUploaded()\n {\n return this.pvt_loaded;\n }\n\n /// Returns whether this chunk can start uploading (either first chunk or first chunk has completed)\n canStart()\n {\n return this.options.offset == 0 || this.uploadfile.sessionid != '';\n }\n\n /** Start upload. Events will be sent (loadstart + progress* + (abort|error|load) + loadend) during upload\n */\n start()\n {\n this.xmlhttp = new XMLHttpRequest;\n if (this.xmlhttp.overrideMimeType) // IE doesn't have this.\n this.xmlhttp.overrideMimeType(\"application/octet-stream\");\n\n if (!this.canStart())\n throw new Error(\"First chunk must have finished for rest of chunks to be sent\");\n\n var url = this.transferbaseurl + \"?type=upload-html5&offset=\" + this.options.offset\n + \"&chunksize=\" + this.options.size\n + \"&sessionid=\" + this.getSessionId();\n if (this.options.offset != 0)\n url += \"&fileid=\" + this.uploadfile.pvt_fileid;\n else\n {\n url += \"&size=\" + this.uploadfile.size\n + \"&filename=\" + encodeURIComponent(this.uploadfile.name);\n Object.keys(this.uploadfile.params).forEach( key => { url += \"&\" + encodeURIComponent(key) + \"=\" + encodeURIComponent(this.uploadfile.params[key]); });\n }\n\n this.xmlhttp.upload.addEventListener('progress', this.gotProgress.bind(this));\n this.xmlhttp.addEventListener('loadstart', this.gotLoadStart.bind(this));\n this.xmlhttp.addEventListener('abort', this.gotAbort.bind(this));\n this.xmlhttp.addEventListener('error', this.gotError.bind(this));\n this.xmlhttp.addEventListener('load', this.gotLoad.bind(this));\n this.xmlhttp.addEventListener(\"loadend\", this.gotLoadEnd.bind(this));\n\n this.xmlhttp.open(\"POST\", url, true, \"\", \"\");\n\n // Slice only when we are are really a subset of the data to be sent\n let data;\n if (this.options.offset != 0 || this.options.size != this.uploadfile.file.size)\n data = this.uploadfile.file.slice(this.options.offset, this.options.offset + this.options.size);\n else\n data = this.uploadfile.file;\n\n /* FIXME: it seems that android browser doesn't like this code -\n work around it!\n */\n this.xmlhttp.send(data);\n }\n\n /// Aborts upload\n abort()\n {\n if (!this.status)\n {\n if (this.xmlhttp)\n this.xmlhttp.abort();\n else\n {\n this.gotAbort(null);\n this.gotLoadEnd(null);\n }\n }\n }\n\n gotLoadStart(event)\n {\n if (!this.pvt_sentloadstart)\n {\n this.pvt_sentloadstart = true;\n this.fireLoadStart();\n }\n }\n\n gotProgress(event)\n {\n this.pvt_loaded = event.loaded;\n this.fireProgress();\n }\n\n gotAbort(event)\n {\n if (!this.status)\n {\n this.status = 'aborted';\n dompack.dispatchCustomEvent(this, 'abort', { bubbles:false, cancelable:false });\n }\n }\n\n gotError(event)\n {\n if (!this.status)\n {\n this.status = 'error';\n dompack.dispatchCustomEvent(this, 'error', { bubbles:false, cancelable:false });\n }\n }\n\n gotLoad(event)\n {\n if (this.xmlhttp.status == 200)\n {\n this.pvt_loaded = this.options.size;\n var data = JSON.parse(this.xmlhttp.responseText);\n if (data && data.sessionid)\n this.setSessionId(data.sessionid);\n if (!this.uploadfile.pvt_fileid)\n this.uploadfile.pvt_fileid = (data && data.fileid) || 0;\n if (data && data.filetoken)\n this.uploadfile.filetoken = data.filetoken;\n if (data && data.fileinfo)\n this.uploadfile.fileinfo = data.fileinfo;\n\n if (data && data.complete)\n {\n this.uploadfile.type = data.contenttype;\n this.uploadfile.downloadurl = data.downloadurl;\n }\n this.status = 'loaded';\n this.fireLoad();\n }\n else\n this.gotError(event);\n }\n\n gotLoadEnd(event)\n {\n if (!this.pvt_sentloadend)\n {\n this.pvt_sentloadend = true;\n this.fireLoadEnd();\n }\n }\n}\n\n\n/** A group of upload items\n*/\nexport class UploadItemGroup extends UploaderAggregator\n{\n getItems()\n {\n return this.pvt_subitems.slice();\n }\n}\n\n/// Generate a group of items from a file input element\nUploadItemGroup.fromFileList = function (uploadhost, filelist, options)\n{\n var items = [];\n for (var i = 0; i < filelist.length; ++i)\n items.push(new Html5UploadItem(uploadhost, filelist[i], options));\n\n var group = new UploadItemGroup;\n group.setItems(items);\n return group;\n};\n\n/** Upload manager\n*/\nclass UploadManager\n{\n constructor()\n {\n this.pending=[];\n this.running=[];\n }\n schedule(item)\n {\n if (item instanceof SchedulableRawUploadItem)\n {\n item.addEventListener(\"loadend\", this.gotEnd.bind(this, item));\n this.pending.push(item);\n }\n else\n item.schedule();\n\n this.processQueue();\n }\n\n gotEnd(item)\n {\n if(this.pending.indexOf(item) >= 0)\n this.pending.splice(this.pending.indexOf(item),1);\n if(this.running.indexOf(item) >= 0)\n this.running.splice(this.running.indexOf(item),1);\n this.processQueue();\n }\n\n processQueue()\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] process queue, running: \" + this.running.length + \" pending: \" + this.pending.length, this);\n\n if (this.running.length < 1 && this.pending.length)\n {\n for (var i = 0; i < this.pending.length; ++i)\n {\n var item = this.pending[i];\n if (item.canStart())\n {\n this.pending.splice(i, 1);\n --i;\n this.running.push(item);\n item.start();\n if (this.running.length == 1)\n break;\n }\n }\n }\n\n if (this.running.length < 1 && this.pending.length)\n throw new Error(\"Got blocked items in the queue\");\n }\n}\n\nqueue_manager = new UploadManager;\n\n\n// Last input used for selecting a file that doesn't have files set\nlet lastinputnode = null;\n\n/** Open a file selection dialog and upload one or more files. Can only be called within a click handler!\n @param options\n @cell options.multiple Whether to allow multiple file upload\n @cell options.mimetypes Array of mime types of files that are accepted (can also contain \"image/*\", \"audio/*\" or \"video/*\")\n @cell options.capture Optional input capture's attribute ('capture', 'user', 'environment', etc)\n @return Selection result object. Fires 'load' or 'abort'\n @cell return.input Used input element\n @cell return.files List of selected files (only valid when 'load' event has fired)\n*/\nexport function selectFiles(options)\n{\n options = {...options};\n let uploaddefer = dompack.createDeferred();\n\n let inputOptions = { type: \"file\"\n , accept: (options.mimetypes || []).join(\",\")\n , multiple: options.multiple\n , style: { display: \"none\" }\n };\n\n if (options.capture)\n inputOptions.capture = options.capture;\n\n let input = dompack.create('input', inputOptions);\n\n //let selectlock = dompack.flagUIBusy();\n\n // IE 10 & 11 won't open the file browser if the input element isn't in the DOM\n if (browser.getName() == 'ie')\n {\n if (lastinputnode)\n document.body.removeChild(lastinputnode);\n lastinputnode = input;\n document.body.appendChild(input);\n }\n\n // Set a handler on next action to capture someone cancelling the upload without telling us (browsers dont inform us the dialog is gone)\n var canceluploadhandler = function()\n {\n uploaddefer.resolve([]);\n window.removeEventListener('mousedown', canceluploadhandler, true);\n window.removeEventListener('keydown', canceluploadhandler, true);\n };\n window.addEventListener('mousedown', canceluploadhandler, true);\n window.addEventListener('keydown', canceluploadhandler, true);\n\n input.addEventListener(\"change\", (event) =>\n {\n // Store files in input, destroy input element\n uploaddefer.resolve(input.files || []);\n });\n input.addEventListener(\"wh:upload-fake\", (event) =>\n {\n uploaddefer.resolve(event.detail.files || []);\n });\n\n let uploader = null;\n try\n {\n uploader = window.top.wh_testapi_fakeupload;\n if(uploader)\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] Need to invoke callback to simulate upload\");\n\n window.top.wh_testapi_fakeupload = null;\n setTimeout(() => uploader(input), 0);\n return uploaddefer.promise;\n }\n }\n catch(e)\n {\n //ignore fialure to grab the fake upload\n }\n\n if(dompack.debugflags.upl)\n console.log(\"[upl] Invoking browser's sendfile\");\n // On IE, this blocks. Delay starting the upload on IE giving the user a consistent interface - loadstart event signals start\n input.click();\n\n return uploaddefer.promise;\n}\n\nexport class UploadSession extends EventTarget\n{\n constructor(files,options)\n {\n super();\n if(dompack.debugflags.upl)\n console.log(\"[upl] Create upload session\",files,options);\n\n options = {...options};\n let host = options.host || dompack.getBaseURI();\n this.started = false;\n this.anyerror = false;\n\n /* Note: we explicitly let an empty file list pass. for event resolution\n purposes, we'll pretend it was an abort */\n if(files.length)\n {\n this.group = new UploadItemGroup(options);\n let items = Array.from(files).map(function(item)\n {\n return new Html5UploadItem(host, item, { params: options.params });\n });\n\n this.group.setItems(items);\n }\n }\n\n isStarted()\n {\n return this.started;\n }\n\n getStatus()\n {\n return this.group ? this.group.getEventDetail() : { uploaded:0, size:0, speed:0 };\n }\n\n abort()\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] Upload session abort invoked\",this);\n this.gotabort=true;\n this.group.abort();\n }\n\n upload()\n {\n let uploaddefer = dompack.createDeferred();\n this.started=true;\n if(!this.group) //empty file list - like an abort, but never send the events\n {\n uploaddefer.resolve([]);\n return uploaddefer.promise;\n }\n\n this.group.addEventListener(\"loadstart\", evt =>\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] Upload session dispatching wh:upload-start\", this);\n this.started = true;\n dompack.dispatchCustomEvent(this, \"wh:upload-start\", { bubbles: false\n , cancelable:false\n });\n });\n this.group.addEventListener(\"progress\", evt =>\n {\n if(dompack.debugflags.upl)\n console.log(\"[upl] Upload session dispatching wh:upload-progress\");\n dompack.dispatchCustomEvent(this, \"wh:upload-progress\", { bubbles: false\n , cancelable:false\n });\n });\n this.group.addEventListener(\"error\", event => this.anyerror = true);\n this.group.addEventListener(\"loadend\", evt =>\n {\n let result = this.gotabort || this.anyerror ? [] : this.group.getCompletedFiles();\n if(dompack.debugflags.upl)\n console.log(\"[upl] Upload session dispatching wh:upload-end\", this, result);\n\n dompack.dispatchCustomEvent(this, \"wh:upload-end\", { bubbles: false\n , cancelable:false\n , detail: { success: this.gotabort || !this.anyerror\n , files: result\n }\n });\n uploaddefer.resolve(result);\n });\n\n this.group.schedule();\n return uploaddefer.promise;\n }\n}\n\nexport function getFileAsDataURL(file)\n{\n return new Promise( (resolve,reject) =>\n {\n let reader = new FileReader;\n reader.onload = function(readdata)\n {\n resolve(reader.result);\n };\n reader.onerror = function()\n {\n reject(new Error(\"Failed to load file\"));\n };\n reader.readAsDataURL(file);\n });\n}\n\nexport function setDefaultUploadChunkSize(newchunksize)\n{\n default_upload_chunk_size = newchunksize;\n}\n\n", "import UploadDialogController from './dialogs/uploadcontroller';\nimport ImgeditDialogController from './dialogs/imgeditcontroller';\nimport * as compatupload from '@mod-system/js/compat/upload';\n\nrequire(\"../common.lang.json\");\n\n\nfunction getUploadTolliumData(component)\n{\n return JSON.stringify(\n { l: component.owner.hostapp.whsid\n , w: component.owner.screenname\n , n: component.name\n });\n}\n\n/** Presents a HTML5 file selection dialog, uploads selected files to a component (with progress dialog). On success,\n calls processing callback that must close the progress dialog by callback.\n @param component Component\n @param uploadcallback Signature: function(files, dialogclosecallback)\n @param options\n @cell options.mimetypes Array of mime types of files that are accepted (can also contain \"image/*\", \"audio/*\" or \"video/*\")\n @cell options.multiple\n*/\nexport async function uploadFiles(component, uploadedcallback, options)\n{\n //Note: this works because selectAndUploadFile will always yield at some point, allowing us to receive the value of group, and allowing onLoadstart to use it\n options={...options};\n\n let files = await compatupload.selectFiles({ mimetype:options.mimetypes\n , multiple:options.multiple\n });\n\n uploadBlobs(component, files, uploadedcallback);\n}\n\n/** Presents a HTML5 file selection dialog, receive selected files. On success, calls processing callback.\n @param component Component\n @param uploadcallback Signature: function(files)\n @param options\n @cell options.mimetypes Array of mime types of files that are accepted (can also contain \"image/*\", \"audio/*\" or \"video/*\")\n @cell options.multiple\n*/\nexport async function receiveFiles(component, options)\n{\n options = options || {};\n return compatupload.selectFiles({ mimetype:options.mimetypes\n , multiple:options.multiple\n });\n}\n\nexport async function uploadBlobs(component, blobs, uploadedcallback, options)\n{\n let uploader = new compatupload.UploadSession(blobs, { params: { tolliumdata: getUploadTolliumData(component) } });\n let uploadcontroller = new UploadDialogController(component.owner, uploader);\n let result = await uploader.upload();\n\n try\n {\n uploadedcallback(result, () => uploadcontroller.close());\n }\n catch(e)\n {\n console.error(\"upload exception\",e);\n uploadedcallback([], () => uploadcontroller.close());\n }\n}\n\nasync function gatherUploadFiles(items)\n{\n let files = [];\n\n for (let i=0;i\n {\n let reader = items[i].createReader();\n reader.readEntries(resolve);\n });\n files = files.concat(await gatherUploadFiles(contents));\n }\n else\n {\n files.push(await new Promise((resolve,reject)=>\n {\n items[i].file(blob =>\n {\n blob.fullpath = items[i].fullPath;\n resolve(blob);\n });\n }));\n }\n }\n return files;\n}\n\n\n\n/** Given an accepted drop, upload files to a component (with progress dialog), call callback when done (successfully)\n Marks tollium as busy until callback is called.\n @param component\n @param dragdata Dragdata (return value of $todd.checkDropTarget)\n @param callback Callback to call when done uploading. Signature: function(draginfo, dialogclosecallback)\n @cell draginfo.source Source: 'local'/'files'/'external'\n @cell draginfo.sourcecomp Source component name (only if source == 'local')\n @cell items List of items (for type='file', with cells 'token' and 'name')\n @cell dialogclosecallback Callback to close the progress dialog after drop has finished)\n*/\nexport async function uploadFilesForDrop(component, dragdata, callback)\n{\n var draginfo = dragdata.getData();\n var files = dragdata.getFiles();\n\n var islocal = !dragdata.hasExternalSource() && draginfo && draginfo.source.owner == component.owner;\n var gotfiles = files && files.length;\n\n var msg =\n { source: islocal ? 'local' : gotfiles ? 'files' : 'external'\n , sourcecomp: islocal ? draginfo.source.name : ''\n , items: draginfo ? draginfo.items : []\n , dropeffect: dragdata.getDropEffect()\n };\n\n if (!gotfiles)\n {\n // No files? Just a busy lock is good enough\n var busylock = component.owner.displayapp.getBusyLock();\n callback(msg, busylock.release.bind(busylock));\n return;\n }\n\n // If this is a drop through an accept rule, open the image editor before uploading\n if (files.length == 1 && dragdata.acceptrule && dragdata.acceptrule.imageaction == \"edit\")\n {\n var file = files[0];\n if (!ImgeditDialogController.checkTypeAllowed(component.owner, file.type))\n return;\n\n const options = { imgsize: dragdata.acceptrule.imgsize\n };\n const dialog = new ImgeditDialogController(component.owner, options);\n dialog.loadImageBlob(file, { filename: file.name });\n\n const done = await dialog.defer.promise;\n\n if (done.blob)\n {\n // Start upload of the file\n uploadBlobs(component, [done.blob],\n function(files, closedialogcallback)\n {\n if (!files.length)\n {\n // got an error uploading the file\n closedialogcallback();\n done.editcallback();\n return;\n }\n\n // There is only 1 file uploaded\n var filename = ensureExtension(files[0].name, files[0].fileinfo.extension);\n\n msg.items.push({ type: 'file', token: files[0].filetoken, name: filename, extradata: null, fullpath: file.fullpath });\n\n callback(msg, function()\n {\n closedialogcallback();\n done.editcallback();\n });\n });\n }\n else\n {\n // Nothing to upload, we're done\n done.editcallback();\n }\n }\n else\n {\n let items = dragdata.getItems();\n if(items.length && items[0].webkitGetAsEntry)\n {\n //we'll build a new filelist\n files = await gatherUploadFiles(items.map(item => item.webkitGetAsEntry()));\n }\n\n // Start upload of the file\n uploadBlobs(component, files,\n function(files, closedialogcallback)\n {\n // got an error uploading the file?\n if (!files.length)\n return void closedialogcallback();\n\n // Files are uploaded, add them to the items list\n files.forEach(file =>\n {\n msg.items.push({ type: 'file', token: file.filetoken, name: file.name, fullpath: file.fullpath });\n });\n\n callback(msg, closedialogcallback);\n });\n }\n}\n\nexport function ensureExtension(filename, extension)\n{\n if (!filename || !extension)\n return filename;\n if (extension.indexOf(\".\") != 0)\n extension = \".\" + extension;\n\n // Check for the right extension (png vs jpg, depending on lossless)\n var extdot = filename.lastIndexOf(\".\");\n if (extdot < 0)\n filename += extension;\n else if (filename.substr(extdot) != extension)\n filename = filename.substr(0, extdot) + extension;\n return filename;\n}\n", "import * as dompack from 'dompack';\nimport ActionForwardBase from './actionforwardbase';\nimport * as feedback from \"../../js/feedback\";\n\nimport { getTid } from \"@mod-tollium/js/gettid\";\nimport DownloadManager from '@mod-system/js/compat/download';\n\nimport * as toddupload from '@mod-tollium/web/ui/js/upload';\nimport ImgeditDialogController from '@mod-tollium/web/ui/js/dialogs/imgeditcontroller';\nimport $todd from \"@mod-tollium/web/ui/js/support\";\nrequire(\"@mod-tollium/web/ui/common.lang.json\");\nrequire(\"@mod-tollium/web/ui/components/imageeditor/imageeditor.lang.json\");\n\n/****************************************************************************************************************************\n * *\n * ACTION *\n * *\n ****************************************************************************************************************************/\n\nexport default class ObjAction extends ActionForwardBase\n{\n constructor(parentcomp, data, replacingcomp)\n {\n super(parentcomp, data, replacingcomp);\n this.componenttype = \"action\";\n this.lastenabled = null;\n this.pendingdownloads = [];\n\n this.customaction = data.customaction;\n this.target = data.targetname;\n\n this.frameflags = data.frameflags||[];\n this.enableons = data.enableons || [];\n this.mimetypes = data.mimetypes || [];\n this.multiple = !(\"multiple\" in data) || data.multiple;\n this.imageaction = !!data.imageaction;\n this.actiontype = data.actiontype;\n this.imgsize = data.imgsize;\n this._onexecute = data.onexecute;\n this.source = data.source; //for copy action\n this.scope = data.scope; //for handlefeedback actions\n\n /*\n if (this.shortcut)\n {\n var enableonsources = [];\n for (var idx=0; idx\n {\n if (files.length)\n this.handleImageUploaded(data, files[0]);\n }).finally(() => busylock.release());\n return;\n }\n case \"edit\":\n {\n if (!this.editimage)\n {\n console.warn(\"imageaction edit called without image\");\n return;\n }\n // Edit image directly without uploading\n this.handleImageUploaded(data, this.editimage);\n return;\n }\n }\n }\n else\n {\n let busylock = dompack.flagUIBusy();\n toddupload.uploadFiles(this, function(files, callback)\n {\n busylock.release();\n if (!files.length)\n {\n callback();\n return;\n }\n data.items = files.map(function(i) { return { type: \"file\", filename: i.filename, token: i.filetoken }; });\n this.asyncMessage(\"upload\", data).then(callback);\n }.bind(this), { mimetypes: this.mimetypes\n , multiple: this.multiple\n });\n }\n }\n\n executeDownloadAction(data)\n {\n var fturl = this.getFileTransferURL('asyncdownload');\n\n var dl = new DownloadManager(fturl.url, {});\n dl.startDownload().then(result =>\n {\n if (result.started)\n this.onDownloadStarted(dl, fturl.id);\n else\n this.onDownloadFailed(dl, fturl.id);\n });\n\n this.pendingdownloads.push(dl);\n this.queueMessage('download', { rule: data.rule, ftid: fturl.id }, true);\n }\n\n executeWindowOpenAction(data)\n {\n var fturl = this.getFileTransferURL('asyncwindowopen');\n\n window.open(fturl.url, this.target || \"_blank\");\n this.queueMessage('windowopen', { rule: data.rule, ftid: fturl.id }, true);\n }\n\n executeHandleFeedback(data)\n {\n feedback.run(null, {scope: this.scope});\n }\n\n executeCopyToClipboard(data)\n {\n let comp = this.owner.getComponent(this.source);\n if(comp)\n comp.doCopyToClipboard();\n }\n\n onDownloadStarted(dl, id)\n {\n this.pendingdownloads = this.pendingdownloads.filter(item => item != dl); //erase\n this.queueMessage(\"download-started\", { ftid: id }, true);\n }\n\n onDownloadFailed(dl, id)\n {\n this.pendingdownloads = this.pendingdownloads.filter(item => item != dl); //erase\n this.queueMessage(\"download-failed\", { ftid: id }, true);\n }\n\n onMsgTarget(data)\n {\n this.target = data.target;\n }\n\n handleImageReset()\n {\n return new Promise(function(resolve)\n {\n $todd.createMessageBox(this.owner.displayapp,\n { title: getTid(\"tollium:components.imgedit.editor.title\")\n , text: getTid(\"tollium:components.imgedit.messages.confirmreset\")\n , icon: \"question\"\n , buttons: [ { name: \"yes\", title: getTid(\"tollium:common.actions.yes\") }\n , { name: \"no\", title: getTid(\"tollium:common.actions.no\") }\n ]\n , onclose:function(result)\n {\n if (result == \"yes\")\n this.queueMessage(\"resend\", {}, true);\n resolve(result);\n }.bind(this)\n });\n }.bind(this));\n }\n\n async handleImageUploaded(data, file)\n {\n if (!file || !ImgeditDialogController.checkTypeAllowed(this.owner, file.type))\n return;\n\n var options = { mimetype: file.type\n , imgsize: this.imgsize\n , action: this.actiontype\n , resetImage: file.source_fsobject ? this.handleImageReset.bind(this) : null\n };\n\n let imageeditdialog = new ImgeditDialogController(this.owner, options);\n let settings = { refpoint: file.refpoint\n , filename: file.name\n };\n\n if (file.url)\n imageeditdialog.loadImageSrc(file.url, settings);\n else\n imageeditdialog.loadImageBlob(file, settings);\n\n let done = await imageeditdialog.defer.promise;\n\n // Note: settings is null when the image wasn't edited after upload\n if (done.blob)\n {\n toddupload.uploadBlobs(this, [done.blob], (files, uploadcallback) =>\n {\n // Only called when a file is actually uploaded\n var filename = toddupload.ensureExtension(file.name, files[0].fileinfo.extension);\n\n var extradata = { imageeditor: { source_fsobject: parseInt(file.source_fsobject) || 0\n , refpoint: done.settings && done.settings.refpoint\n }};\n data.items = [{ type: \"file\", name: filename, token: files[0].filetoken, extradata: extradata }];\n this.asyncMessage(\"upload\", data).then( () =>\n {\n uploadcallback();\n done.editcallback();\n });\n });\n }\n else\n {\n // Nothing to upload, we're done\n done.editcallback();\n }\n }\n\n/****************************************************************************************************************************\n* Events\n*/\n\n applyUpdate(data)\n {\n switch(data.type)\n {\n case \"execute\":\n {\n this.editimage = data.image;\n this.onExecute({ ignorebusy: true });\n return;\n }\n }\n super.applyUpdate(data);\n }\n}\n", "import * as component from '@mod-tollium/web/ui/js/componentbase.es';\n\nlet ActionableBase = component.ActionableComponent;\nexport default ActionableBase;\n", "import * as dompack from 'dompack';\nimport ActionableBase from '@mod-tollium/webdesigns/webinterface/components/base/actionable';\nimport * as icons from '@mod-tollium/js/icons';\nimport $todd from \"@mod-tollium/web/ui/js/support\";\nimport Keyboard from 'dompack/extra/keyboard';\nimport './button.scss';\n\n/****************************************************************************************************************************\n * *\n * BUTTON *\n * *\n ****************************************************************************************************************************/\n\nlet toolbarbutton = { width: 24, height: 24 };\n\nexport default class ObjButton extends ActionableBase\n{\n constructor(parentcomp, data, replacingcomp)\n {\n super(parentcomp, data, replacingcomp);\n this.componenttype = \"button\";\n this.iconsize = 0;\n this.menuopen = false;\n this.ismenubutton = false;\n this.isactive = false;\n this.setTitle(data.title);\n\n this.icon = data.icon;\n this.type = data.buttontype; // \"standard\" or \"icon\"\n this.pressed = data.ispressed || false;\n this.ismenubutton = data.ismenubutton;\n\n // Build our DOM\n this.buildNode();\n this.setMenu(data.menu);\n\n new Keyboard(this.node, { \" \": evt => this.onClick(evt)\n , \"Enter\": evt => this.onClick(evt)\n }, {stopmapped: true} );\n }\n setMenu(newmenu)\n {\n //this.menu = this.owner.addComponent(this, newmenu);\n //dompack.toggleClasses (this.node, { showmenu: this.isToolbarButton() && this.menu });\n this.menuname = newmenu;\n dompack.toggleClasses (this.node, { showmenu: this.isToolbarButton() && this.menuname });\n }\n/****************************************************************************************************************************\n * Property getters & setters\n */\n\n setTitle(value)\n {\n if (value == this.title)\n return;\n\n this.title = value;\n if (this.textnode)\n this.textnode.textContent = this.title;\n this.width.dirty = true;\n }\n\n readdComponent(comp)\n {\n // Replace the offending component\n //if(!comp.parentsplititem)\n if(comp.parentcomp != this)\n return console.error('Child ' + comp.name + ' not inside the textedit is trying to replace itself');\n\n var newcomp = this.owner.addComponent(this, comp.name);\n this.buttons.splice(this.buttons.indexOf(comp), 1, newcomp);\n\n comp.getNode().replaceWith(newcomp.getNode());\n }\n\n\n/****************************************************************************************************************************\n* DOM\n*/\n canBeFocusable()\n {\n return !this.isToolbarButton();\n }\n\n isTabsSpaceButton()\n {\n return !!this.node.closest('div.tabs-space');\n }\n isToolbarButton()\n {\n return this.parentcomp && this.parentcomp.componenttype=='toolbar';\n }\n // Build the DOM node(s) for this component\n buildNode()\n {\n this.node = dompack.create(\"t-button\", { on: { click: evt => this.onClick(evt)\n , mousedown: evt=> this.onMouseDown(evt)\n , mouseup: evt=> this.cancelActiveState(evt)\n , mouseleave: evt=> this.cancelActiveState(evt)\n , \"wh:menu-open\": evt => this.onMenuState(true, evt)\n , \"wh:menu-close\": evt => this.onMenuState(false, evt)\n }\n , dataset: { name: this.name, toddDefaultButton: \"\" }\n , title: this.hint || ''\n , className: { ismenubutton: this.ismenubutton }\n , tabIndex: 0\n });\n this.node.propTodd = this;\n\n if(this.isToolbarButton())\n {\n this.iconnode = icons.createImage(this.icon, toolbarbutton.width, toolbarbutton.height, 'w', { className: \"button__img\" });\n this.node.appendChild(this.iconnode);\n this.textnode = {this.title};\n this.node.appendChild(this.textnode);\n }\n else\n {\n if (this.icon)\n {\n if (this.type == \"icon\" || !this.title)\n this.node.classList.add(\"icon\");\n\n this.iconsize = 16; //ADDME: Adjust according to button size?\n this.iconnode = icons.createImage(this.icon, this.iconsize, this.iconsize, 'b', {className:\"button__img\"});\n this.node.appendChild(this.iconnode);\n }\n\n if (this.type != \"icon\" && this.title)\n {\n this.textnode = {this.title};\n this.node.appendChild(this.textnode);\n }\n }\n this.node.classList.toggle(\"pressed\", this.pressed);\n }\n\n\n/****************************************************************************************************************************\n* Dimensions\n*/\n\n calculateDimWidth()\n {\n if (this.isToolbarButton())\n {\n var text = this.title;\n var arrow_space = 0;\n if (this.menuname && this.title) // need extra 5 pixels + size of \\u25bc char for dropdown symbol (with 70% size)\n arrow_space = 5 + $todd.CalculateTextSize(\"\\u25bc\", 0, { \"font-size\": \"70%\" }).x;\n\n let contentwidth = Math.max(65, $todd.CalculateTextSize(text, 0, { \"font-size\" : 11 }).x + arrow_space) + 8;/* toolbar button text is 11px plus 2*4px padding */\n this.width.min = contentwidth;\n this.width.calc = contentwidth;\n // we can handle the width from CSS, since the toolbar takes up the whole width of the screen\n }\n else\n {\n var width = $todd.ReadSetWidth(this.width);\n\n // FIXME: nakijken, we hebben toch buttons met icon EN title ????\n\n //ADDME: If word wrapped, take width into account!\n let contentwidth = 0;\n\n if (this.type != \"icon\" && this.title != \"\") // for buttons of type 'icon' we hide the title\n contentwidth += $todd.CalculateTextSize(this.title).x;\n\n //console.log(\"Width\", contentwidth, \"for title\", this.title, \" + (skinsettings.xpad)\", this.skinsettings.xpad);\n\n const buttonhorizontaloverhead = 12; //2 for t-button border and 10 for t-button padding\n this.width.min = contentwidth + buttonhorizontaloverhead;\n this.width.min = Math.max(this.icon ? this.isTabsSpaceButton() ? 27 : 26 : 84,this.width.min);\n\n this.width.calc = width + buttonhorizontaloverhead;\n }\n if(isNaN(this.width.min))\n {\n console.error(this.name + \" failed width calculations!\", this.width, this.skinsettings, this.isToolbarButton());\n }\n }\n\n calculateDimHeight()\n {\n if (this.isToolbarButton())\n this.height.min = 56;\n else\n this.height.min = $todd.gridlineInnerHeight;\n }\n\n relayout()\n {\n this.debugLog(\"dimensions\", \"relayouting set width=\" + this.width.set + \", set height=\"+ this.height.set);\n if (!this.isToolbarButton())\n this.node.style.width = this.width.set + 'px';\n }\n\n\n/****************************************************************************************************************************\n* Component state\n*/\n\n setDefault(isdefault)\n {\n this.node.classList.toggle(\"default\", isdefault);\n }\n\n\n/****************************************************************************************************************************\n* Events\n*/\n\n applyUpdate(data)\n {\n switch(data.type)\n {\n case \"title\":\n this.setTitle(data.title);\n return;\n case 'pressed':\n this.pressed = data.pressed;\n this.node.classList.toggle(\"pressed\", this.pressed);\n return;\n }\n super.applyUpdate(data);\n }\n\n onClick(event)\n {\n if (!this.getEnabled() || event.button)\n return;\n\n if (this.menuname)\n {\n let menu = this.owner.getComponent(this.menuname);\n if(menu)\n {\n this.menunode = menu.openMenuAt(this.node, { direction: 'bottom'\n , align: this.ismenubutton ? 'right' : 'left'\n , ismenubutton: this.ismenubutton\n });\n this.updateActiveState();\n }\n return;\n }\n //ADDME: Differentiate between menu-only buttons and buttons with both an action and a menu. For now, we'll just support\n // either menu buttons or action buttons.\n if (this.action)\n {\n this.owner.executeAction(this.action);\n return;\n }\n\n if(this.isEventUnmasked(\"click\"))\n {\n this.queueEvent(this.owner.screenname + \".\" + this.name, \"click\", true);\n return;\n }\n }\n\n onMenuState(newstate, event)\n {\n if(event.detail.depth > 1)\n return;\n\n this.menuopen = newstate;\n this.updateActiveState();\n }\n\n onMouseDown(event)\n {\n event.preventDefault(); // Don't steal focus (FIXME: that not only stop's the default behaviour of getting focus, but also prevents :active from being applied)\n if (!this.getEnabled() || event.rightClick)\n return;\n\n this.isactive=true;\n this.updateActiveState();\n }\n updateActiveState()\n {\n // NOTE: The :active pseudo-class won't work because we have used event.preventDefault() to prevent focus stealing\n this.node.classList.toggle(\"button--active\", this.menuopen || this.isactive);\n }\n cancelActiveState(event)\n {\n this.isactive=false;\n this.updateActiveState();\n // FIXME: doesn't reactivate after leaving and reentering the button while keeping the mousebutton down\n }\n}\n", "import * as dompack from 'dompack';\nimport * as domfocus from \"dompack/browserfix/focus\";\nimport $todd from \"@mod-tollium/web/ui/js/support\";\n\nexport function getBorderWidth(borders)\n{\n return (borders && borders.left ? $todd.settings.border_left : 0) + (borders && borders.right ? $todd.settings.border_right : 0)\n}\nexport function getBorderHeight(borders)\n{\n return (borders && borders.top ? $todd.settings.border_top : 0) + (borders && borders.bottom ? $todd.settings.border_bottom : 0)\n}\nexport function getSpacerWidth(spacers)\n{\n return (spacers && spacers.left ? $todd.settings.spacer_left : 0) + (spacers && spacers.right ? $todd.settings.spacer_right : 0)\n}\nexport function getSpacerHeight(spacers)\n{\n return (spacers && spacers.top ? $todd.settings.spacer_top : 0) + (spacers && spacers.bottom ? $todd.settings.spacer_bottom : 0)\n}\n\nexport function copyValueToClipboard(node)\n{\n let alreadyfocused = node == domfocus.getCurrentlyFocusedElement();\n node.select();\n if(!alreadyfocused)\n dompack.focus(node);\n\n document.execCommand(\"copy\");\n\n if(alreadyfocused) //flash if already focused\n {\n node.selectionStart=0;\n node.selectionEnd=0\n window.setTimeout( () => node.select(), 100);\n }\n}\n", "import * as dompack from 'dompack';\nimport ComponentBase from '@mod-tollium/webdesigns/webinterface/components/base/compbase';\nimport './buttongroup.scss';\nimport * as toddtools from '@mod-tollium/webdesigns/webinterface/components/base/tools';\nimport $todd from \"@mod-tollium/web/ui/js/support\";\n\n/****************************************************************************************************************************\n * *\n * BUTTONGROUP *\n * *\n ****************************************************************************************************************************/\n\n\nexport default class ObjButtonGroup extends ComponentBase\n{\n\n/****************************************************************************************************************************\n* Initialization\n*/\n\n constructor(parentcomp, data, replacingcomp)\n {\n super(parentcomp, data, replacingcomp);\n\n this.componenttype = \"buttongroup\";\n this.layout = data.layout;\n this.borders = data.borders;\n this.buttons = [];\n data.buttons.forEach(button =>\n {\n var comp = this.owner.addComponent(this, button);\n if (!comp.getNode())\n return; //ignore this component for further consideration\n\n this.buttons.push(comp);\n });\n\n //we *almost* have the whole layout sorted out, but buttongroups are inline components that want to take up more vertical space. so for now, we cheat... if this is the only showstopper it won't stop us now\n this.tabsspacecheat = parentcomp && parentcomp.layout === \"tabs-space\";\n\n this.buildNode();\n }\n\n\n/****************************************************************************************************************************\n* Component management\n*/\n\n readdComponent(comp)\n {\n // Replace the offending component\n //if(!comp.parentsplititem)\n if(comp.parentcomp != this)\n return console.error('Child ' + comp.name + ' not inside the buttongroup is trying to replace itself');\n\n var newcomp = this.owner.addComponent(this, comp.name);\n this.buttons.splice(this.buttons.indexOf(comp), 1, newcomp);\n comp.getNode().replaceWith(newcomp.getNode());\n }\n\n/****************************************************************************************************************************\n* DOM\n*/\n\n // Build the DOM node(s) for this component\n buildNode()\n {\n this.node = \n {this.buttons.map( (button,idx) =>\n [ idx > 0 ?
: null\n , button.getNode()\n ])}\n
;\n\n if(this.tabsspacecheat)\n {\n this.node.style.marginTop = (-$todd.gridlineTopMargin) + \"px\";\n }\n\n ['top','bottom','left','right'].forEach(dir =>\n {\n if(this.borders && this.borders[dir])\n this.node.classList.add(\"border-\" + dir);\n });\n }\n\n\n/****************************************************************************************************************************\n* Dimensions\n*/\n getVisibleChildren()\n {\n return this.buttons;\n }\n\n calculateDimWidth()\n {\n let borderwidth = toddtools.getBorderWidth(this.borders);\n\n if(this.layout == \"horizontal\")\n {\n let divideroverhead = Math.max(0, this.buttons.length-1) * 1;\n this.width.overhead = divideroverhead + borderwidth;\n this.setSizeToSumOf('width', this.buttons, this.width.overhead);\n }\n else\n {\n this.width.overhead = borderwidth;\n this.setSizeToMaxOf('width', this.buttons, this.width.overhead);\n }\n }\n calculateDimHeight()\n {\n let borderheight = toddtools.getBorderHeight(this.borders);\n\n if(this.layout == \"horizontal\")\n {\n this.height.overhead = borderheight;\n this.setSizeToMaxOf('height', this.buttons, this.height.overhead);\n }\n else\n {\n let divideroverhead = Math.max(0, this.buttons.length-1) * 1;\n this.height.overhead = divideroverhead + borderheight;\n this.setSizeToSumOf('height', this.buttons, this.height.overhead);\n }\n }\n\n applySetWidth()\n {\n var setwidth = this.width.set - this.width.overhead;\n if (this.layout == \"horizontal\")\n this.distributeSizeProps('width', setwidth, this.buttons, true);\n else\n this.buttons.forEach(button => button.setWidth(setwidth));\n }\n applySetHeight()\n {\n var setheight = this.height.set - this.height.overhead;\n if (this.layout == \"horizontal\")\n this.buttons.forEach(button => button.setHeight(setheight));\n else\n this.distributeSizeProps('height', setheight, this.buttons, false);\n }\n\n relayout()\n {\n dompack.setStyles(this.node, { \"width\": this.width.set\n , \"height\": this.height.set + ( this.tabsspacecheat ? $todd.gridlineTotalMargin : 0)\n });\n this.buttons.forEach(button => button.relayout());\n }\n}\n", "import * as dompack from 'dompack';\nimport \"./checkbox.scss\";\nimport ComponentBase from '@mod-tollium/webdesigns/webinterface/components/base/compbase';\n\nexport default class ObjCheckbox extends ComponentBase\n{ // ---------------------------------------------------------------------------\n //\n // Initialization\n //\n\n constructor(parentcomp, data, replacingcomp)\n {\n super(parentcomp, data, replacingcomp);\n\n this.componenttype = \"checkbox\";\n this.flags = data.flags || [];\n this.buildNode();\n\n this.setValue(data.value, data.indeterminate);\n this.setReadOnly(data.readonly);\n this.setEnabled(data.enabled);\n }\n\n // ---------------------------------------------------------------------------\n //\n // Communications\n //\n\n enabledOn(checkflags, min, max, selectionmatch)\n {\n return (min > 0 && max != 0 && this.getValue().value)\n || (min <= 0 && max == 0 && !this.getValue().value);\n }\n\n // ---------------------------------------------------------------------------\n //\n // Property getters & setters\n //\n\n getSubmitValue()\n {\n return this.getValue();\n }\n\n getValue()\n {\n return { indeterminate: this.checkboxnode.indeterminate\n , value: this.checkboxnode.checked\n };\n }\n\n setValue(value, indeterminate)\n {\n this.checkboxnode.checked = value;\n this.checkboxnode.indeterminate = indeterminate;\n }\n\n toggle()\n {\n if (this.enabled && !this.readonly)\n {\n this.setValue( !this.getValue().value, false);\n this.gotControlChange();\n this.checkboxnode.focus();\n }\n }\n\n setReadOnly(value)\n {\n if (value != this.readonly)\n {\n this.readonly = value;\n this.checkboxnode.disabled = !(this.enabled && !this.readonly);\n }\n }\n\n setEnabled(value)\n {\n if (value != this.enabled)\n {\n this.enabled = value;\n this.checkboxnode.disabled = !(this.enabled && !this.readonly);\n }\n }\n\n // ---------------------------------------------------------------------------\n //\n // DOM\n //\n\n // Build the DOM node(s) for this component\n buildNode()\n {\n this.node =\n
{ this.toggle(); }}\n hint={this.hint || \"\"}>\n { this.checkboxnode =\n this.gotControlChange(ev)} /> }\n
;\n }\n\n // ---------------------------------------------------------------------------\n //\n // Dimensions\n //\n\n getSkinSettings()\n {\n var dims = this.node.getBoundingClientRect();\n return { width: parseInt(dims.width)\n , height: parseInt(dims.height)\n };\n }\n\n calculateDimWidth()\n {\n this.width.calc = this.skinsettings.width;\n this.width.min = this.width.calc;\n }\n\n calculateDimHeight()\n {\n this.height.calc = this.skinsettings.height;\n this.height.min = this.height.calc;\n }\n\n relayout()\n {\n this.debugLog(\"dimensions\", \"relayouting set width=\" + this.width.set + \", set height=\"+ this.height.set);\n this.node.style.marginTop = this.getVerticalPosition() + 'px';\n }\n\n // ---------------------------------------------------------------------------\n //\n // Events & callbacks\n //\n\n gotControlChange(ev)\n {\n this.value = this.checkboxnode.checked;\n this.setDirty();\n if(this.isEventUnmasked(\"change\") || this.enablecomponents.length)\n this.transferState(true);\n\n this.owner.actionEnabler();\n }\n\n applyUpdate(data)\n {\n switch(data.type)\n {\n case 'value':\n this.setValue(data.value, data.indeterminate);\n return;\n case 'enablecomponents':\n this.enablecomponents = data.value;\n return;\n }\n\n super.applyUpdate(data);\n }\n}\n\n", "import * as browser from 'dompack/extra/browser.es';\nimport * as dompack from 'dompack';\n\n/////////////////////////////////////////////////////////////////////////\n//\n// ScrollMonitor\n//\n// Browsers may reset scroll position if elements leave the dom or on focus.\n// Watch and restore scroll position. tollium.lists.testjump tests this\n\n//list of delayed scroll fixes\nlet scrollfixlist = [];\n\nfunction watchScroll(evt)\n{\n let node = evt.target;\n if(dompack.debugflags.scm)\n console.log(\"[scm] SCROLL \",node, ` to ${node.scrollLeft},${node.scrollTop}`);\n ScrollMonitor.saveScrollPosition(node);\n}\n\nfunction applyScrollFixList()\n{\n let savedlist = scrollfixlist; //not expecting sideeffects, but save it just in case\n scrollfixlist = [];\n\n for(let tofix of savedlist)\n {\n if(dompack.debugflags.scm)\n console.log(`[scm] Delayed resetting scroll from ${tofix.node.scrollLeft},${tofix.node.scrollTop} to ${tofix.left},${tofix.top} for `, tofix.node);\n tofix.node.scrollLeft = tofix.left + 1;\n tofix.node.scrollLeft = tofix.left;\n tofix.node.scrollTop = tofix.top;\n }\n}\n\nfunction doFixScrollPosition(node)\n{\n if([\"ie\",\"edge\",\"firefox\"].includes(browser.getName()))\n { //IE, EDGE and Firefox delay scroll resets, so we'll need to delay our fix.\n\n if(scrollfixlist.length==0)\n {\n // Animationframe is more reliable than timeout for firefox\n if(\"firefox\" === browser.getName())\n requestAnimationFrame(() => applyScrollFixList());\n else\n setTimeout(applyScrollFixList, 1);\n }\n else if(scrollfixlist.find(tofix => tofix.node == node))\n return; //already have this on our fixlist\n\n scrollfixlist.push( {node: node, top: node.dompack_savedScrollTop, left: node.dompack_savedScrollLeft } );\n }\n else //we can fix it right away\n {\n if(dompack.debugflags.scm)\n console.log(`[scm] Resetting scroll from ${node.scrollLeft},${node.scrollTop} to ${node.dompack_savedScrollLeft},${node.dompack_savedScrollTop} for `, node);\n node.scrollLeft = node.dompack_savedScrollLeft;\n node.scrollTop = node.dompack_savedScrollTop;\n }\n return true;\n}\n\nfunction onFocusCheckScroll()\n{\n if(dompack.debugflags.scm)\n console.log(\"[scm] FOCUS\",this, this.scrollTop, this.dompack_savedScrollTop);\n //'this' is the element on which we registered the scroll event (and the one we're watching)\n doFixScrollPosition(this); //unconditionally fix it\n}\n\nexport default class ScrollMonitor\n{\n constructor(node)\n {\n this.node = node;\n this.node.addEventListener('scroll', watchScroll, true);\n }\n fixupPositions()\n {\n if(dompack.debugflags.scm)\n console.log(\"[scm] FixupPositions()\",this.node);\n for(let node of this.node.querySelectorAll('.dompack--scrollmonitor'))\n ScrollMonitor.fixScrollPosition(node);\n }\n}\n\nScrollMonitor.fixScrollPosition = function(node)\n{\n if(node.scrollTop == node.dompack_savedScrollTop && node.scrollLeft == node.dompack_savedScrollLeft)\n return false;\n doFixScrollPosition(node);\n};\n\n//can also force sync positions to be reparsed, needed after manual scrollTop/Left update\nScrollMonitor.saveScrollPosition = function(node)\n{\n if(! ('dompack_savedScrollTop' in node)) //set a class for watched nodes, so we can quickly find them\n {\n if(dompack.debugflags.scm)\n console.log(\"[scm] Starting to record scroll positions for \",node);\n node.classList.add('dompack--scrollmonitor');\n\n //At least chrome will scroll back a component on focus (eg RTD) and needs restoration\n node.addEventListener('focus', onFocusCheckScroll, true);\n }\n\n node.dompack_savedScrollTop = node.scrollTop;\n node.dompack_savedScrollLeft = node.scrollLeft;\n};\nScrollMonitor.setScrollPosition = function(node,x,y)\n{\n node.scrollTop = y;\n node.scrollLeft = x;\n this.saveScrollPosition(node);\n dompack.dispatchDomEvent(node, \"scroll\"); //update the list immediately, this fixes some races (such as testFindAsYouType) as the scroll evnet will otherwise fire asynchronously\n};\n", "import * as dompack from 'dompack';\nimport ComponentBase from '@mod-tollium/webdesigns/webinterface/components/base/compbase';\nimport ScrollMonitor from '@mod-tollium/js/internal/scrollmonitor';\nimport Keyboard from 'dompack/extra/keyboard';\n\n// ---------------------------------------------------------------------------\n//\n// Codeedit\n//\n\nexport default class ObjCodeEdit extends ComponentBase\n{\n // ---------------------------------------------------------------------------\n //\n// Constructor\n //\n\n constructor(parentcomp, data, response, replacingcomp)\n {\n super(parentcomp, data, response, replacingcomp);\n\n this.componenttype = \"codeedit\";\n\n this.linenumberholder = null;\n this.linenumberdiv = null;\n this.linenumberpre = null;\n this.textarea = null;\n this.enabled = null;\n this.isactive = false;\n this.markerholderdiv = null;\n this.markerscrolldiv = null;\n\n this.donelinenumbers = 0;\n this.pendinggotoline = 0;\n this.pendinggotoline_attop = false;\n this.synctimer = null;\n\n this.markers = [];\n\n this.linenumberswidth = 36;\n\n this.buildNode();\n this.textarea.value = data.value;\n this.markers = data.markers;\n\n this.syncLineNumbers();\n this.syncScroll();\n\n this.syncMarkers();\n\n this.setEnabled(data.enabled);\n this.executeActions(data.actions);\n\n setTimeout(()=> this.syncMarkers(), 1);\n }\n\n // ---------------------------------------------------------------------------\n //\n // Helper stuff\n //\n\n buildNode()\n {\n this.node =\n \n { this.linenumberbg =
}\n { this.markerholderdiv =\n
\n { this.markerscrolldiv =
}\n
\n }\n { this.linenumberdiv =
this.gotGutterClick(event) }}\n />\n }\n { this.textarea =