diff --git a/package-lock.json b/package-lock.json index 3121d2b..8ce5397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1114,20 +1114,6 @@ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", "dev": true }, - "node_modules/@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", - "dev": true, - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "fflate": "~0.8.2", - "meshoptimizer": "~1.1.1" - } - }, "node_modules/@types/webxr": { "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", @@ -1146,6 +1132,12 @@ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@webgpu/types": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", + "dev": true + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -2320,12 +2312,6 @@ "semver": "bin/semver.js" } }, - "node_modules/meshoptimizer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", - "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "dev": true - }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -3088,11 +3074,6 @@ "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==" }, - "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==" - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -3766,11 +3747,6 @@ "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/w2ui": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w2ui/-/w2ui-2.0.0.tgz", - "integrity": "sha512-oMvLitt2jMholqCpX0mDZ0b70ITjlLpfV56gswk7/cD2oPmlibV+DqcizESSs3+t5VJHiG0Xlcb23e4AoCN1Qw==" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3882,17 +3858,42 @@ "dependencies": { "jquery": "^3.7.1", "jstree": "^3.3.17", - "three": "^0.184.0", - "w2ui": "^2.0.0" + "three": "^0.183.2" }, "devDependencies": { "@types/jquery": "^3.5.32", "@types/jstree": "^3.3.46", - "@types/three": "^0.184.1", + "@types/three": "^0.183.0", "@vitejs/plugin-basic-ssl": "^1.2.0", "typescript": "^5.7.2", "vite": "^6.0.7" } + }, + "web/node_modules/@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "dev": true, + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, + "web/node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "dev": true + }, + "web/node_modules/three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==" } }, "dependencies": { @@ -4475,20 +4476,6 @@ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", "dev": true }, - "@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", - "dev": true, - "requires": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "fflate": "~0.8.2", - "meshoptimizer": "~1.1.1" - } - }, "@types/webxr": { "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", @@ -4502,6 +4489,12 @@ "dev": true, "requires": {} }, + "@webgpu/types": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", + "dev": true + }, "abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -4824,7 +4817,7 @@ "@types/node": "^24.0.0", "bcryptjs": "^2.4.3", "cos-nodejs-sdk-v5": "^2.15.4", - "dotenv": "*", + "dotenv": "^17.4.2", "fastify": "^5.2.1", "nanoid": "^5.0.9", "tsx": "^4.19.2", @@ -4837,14 +4830,41 @@ "requires": { "@types/jquery": "^3.5.32", "@types/jstree": "^3.3.46", - "@types/three": "^0.184.1", + "@types/three": "^0.183.0", "@vitejs/plugin-basic-ssl": "^1.2.0", "jquery": "^3.7.1", "jstree": "^3.3.17", - "three": "^0.184.0", + "three": "^0.183.2", "typescript": "^5.7.2", - "vite": "^6.0.7", - "w2ui": "^2.0.0" + "vite": "^6.0.7" + }, + "dependencies": { + "@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "dev": true, + "requires": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, + "meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "dev": true + }, + "three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==" + } } }, "dot-prop": { @@ -5360,12 +5380,6 @@ } } }, - "meshoptimizer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", - "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "dev": true - }, "mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -5868,11 +5882,6 @@ } } }, - "three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==" - }, "tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6205,11 +6214,6 @@ } } }, - "w2ui": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w2ui/-/w2ui-2.0.0.tgz", - "integrity": "sha512-oMvLitt2jMholqCpX0mDZ0b70ITjlLpfV56gswk7/cD2oPmlibV+DqcizESSs3+t5VJHiG0Xlcb23e4AoCN1Qw==" - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/web/package.json b/web/package.json index 1769f3a..feafb79 100644 --- a/web/package.json +++ b/web/package.json @@ -11,13 +11,12 @@ "dependencies": { "jquery": "^3.7.1", "jstree": "^3.3.17", - "three": "^0.184.0", - "w2ui": "^2.0.0" + "three": "^0.183.2" }, "devDependencies": { "@types/jquery": "^3.5.32", "@types/jstree": "^3.3.46", - "@types/three": "^0.184.1", + "@types/three": "^0.183.0", "@vitejs/plugin-basic-ssl": "^1.2.0", "typescript": "^5.7.2", "vite": "^6.0.7" diff --git a/web/src/main.ts b/web/src/main.ts index 46392c2..7e6f253 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,5 +1,5 @@ import "jstree/dist/themes/default/style.min.css"; -import "w2ui/w2ui-2.0.min.css"; +import "./vendor/w2ui/w2ui.css"; import "./styles/base.css"; import { api } from "./services/api"; import { getToken, setCurrentUser, setSession, clearSession } from "./services/authState"; diff --git a/web/src/pages/app/layout.ts b/web/src/pages/app/layout.ts index 62713a0..a524e08 100644 --- a/web/src/pages/app/layout.ts +++ b/web/src/pages/app/layout.ts @@ -1,4 +1,4 @@ -import { w2layout, w2ui } from "w2ui"; +import { w2layout, w2ui } from "../../vendor/w2ui"; import { getCurrentUser, clearSession } from "../../services/authState"; import { loadFolders } from "./modules/folders"; import { bindModelActions } from "./modules/models"; diff --git a/web/src/pages/app/modules/preview.ts b/web/src/pages/app/modules/preview.ts index b47d885..1a4dcf3 100644 --- a/web/src/pages/app/modules/preview.ts +++ b/web/src/pages/app/modules/preview.ts @@ -1,4 +1,4 @@ -import { w2popup } from "w2ui"; +import { w2popup } from "../../../vendor/w2ui"; import type { Object3D, PerspectiveCamera, Scene, Texture, WebGLRenderer } from "three"; import type { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import type { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; diff --git a/web/src/ui/dialogs.ts b/web/src/ui/dialogs.ts index 49b9635..b1e795f 100644 --- a/web/src/ui/dialogs.ts +++ b/web/src/ui/dialogs.ts @@ -1,4 +1,4 @@ -import { w2alert, w2confirm, w2popup } from "w2ui"; +import { w2alert, w2confirm, w2popup } from "../vendor/w2ui"; type PopupActionEvent = { detail: { diff --git a/web/src/vendor/w2ui.ts b/web/src/vendor/w2ui.ts new file mode 100644 index 0000000..cf59f96 --- /dev/null +++ b/web/src/vendor/w2ui.ts @@ -0,0 +1,53 @@ +// 与主程序 src/publicJs/w2ui 保持同步的模型库本地副本,这里只做类型适配。 +// @ts-expect-error w2ui 本地 JS 暂无配套 d.ts,运行时由 Vite 正常打包。 +import * as w2uiLocal from "./w2ui/w2ui.es6.js"; + +type W2EventHandler = (event: unknown) => void; +type W2EventHost = { + on(eventName: string, handler: W2EventHandler): W2EventHost; +}; + +type W2Popup = { + open(options: { + title?: string; + width?: number; + height?: number; + modal?: boolean; + body?: string; + actions?: Record void | Promise>; + }): { + self: W2EventHost; + }; + close(): void; +}; + +type W2LayoutOptions = { + name: string; + padding?: number; + panels: Array<{ + type: "left" | "right" | "top" | "bottom" | "main" | "preview"; + size?: number | string; + minSize?: number; + maxSize?: number | false; + resizable?: boolean; + overflow?: string; + html?: string; + }>; +}; + +type W2Layout = { + render(target: HTMLElement): void; + destroy(): void; +}; + +const local = w2uiLocal as Record; + +export const w2popup = local.w2popup as W2Popup; +export const w2layout = local.w2layout as new (options: W2LayoutOptions) => W2Layout; +export const w2ui = local.w2ui as Record void } | undefined>; +export const w2alert = local.w2alert as (message: string, title?: string) => unknown; +export const w2confirm = local.w2confirm as ( + message: string | { msg: string; title?: string; yes?: string; no?: string }, + title?: string, + callback?: (action: string) => void +) => unknown; diff --git a/web/src/vendor/w2ui/w2ui.css b/web/src/vendor/w2ui/w2ui.css new file mode 100644 index 0000000..9dc0056 --- /dev/null +++ b/web/src/vendor/w2ui/w2ui.css @@ -0,0 +1,4250 @@ +/* w2ui 2.0.x (nightly) (7/4/2025, 6:27:39 AM) (c) http://w2ui.com, vitmalina@gmail.com */ +/** +* TODO: +* - remove default styling, only keep w2ui-* styles +* - split out font +* - split out global css (popup/tooltip) and component css (grid/form, etc) +* - there should be no !important +* - conservative variables (default font +*/ +@font-face { + font-family: "w2ui-font"; + src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); + font-weight: normal; + font-style: normal; +} +[class^="w2ui-icon-"]:before, +[class*=" w2ui-icon-"]:before { + font-family: "w2ui-font"; + display: inline-block; + vertical-align: middle; + line-height: 1; + margin-top: 0px; + font-weight: normal; + font-style: normal; + speak: none; + text-decoration: inherit; + text-transform: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.w2ui-icon-box:before { + content: "A"; +} +.w2ui-icon-check:before { + content: "B"; +} +.w2ui-icon-colors:before { + content: "C"; +} +.w2ui-icon-columns:before { + content: "D"; +} +.w2ui-icon-cross:before { + content: "E"; +} +.w2ui-icon-drop:before { + content: "F"; +} +.w2ui-icon-empty:before { + content: "G"; +} +.w2ui-icon-eye-dropper:before { + content: "H"; +} +.w2ui-icon-info:before { + content: "I"; +} +.w2ui-icon-paste:before { + content: "J"; +} +.w2ui-icon-pencil:before { + content: "K"; +} +.w2ui-icon-plus:before { + content: "L"; +} +.w2ui-icon-reload:before { + content: "M"; +} +.w2ui-icon-search:before { + content: "N"; +} +.w2ui-icon-settings:before { + content: "O"; +} +@font-face { + font-family: 'OpenSans'; + src: url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAARAQAABAAQR0RFRgt8DNQAAXd0AAAALkdQT1MAGQAMAAF3pAAAABBHU1VC47MpuAABd7QAAALuT1MvMqE2nskAAUdAAAAAYGNtYXCuu/X7AAFHoAAAA4hjdnQgD00YpAABU+gAAACiZnBnbX5hthEAAUsoAAAHtGdhc3AAFQAjAAF3ZAAAABBnbHlmdDiZSwAAARwAAS+0aGVhZAK6Y3AAAThIAAAANmhoZWENzAlzAAFHHAAAACRobXR46DU83QABOIAAAA6abG9jYSkU3PEAATDwAAAHVm1heHAFQwIKAAEw0AAAACBuYW1lW5KAHwABVIwAAAPScG9zdH+4CW8AAVhgAAAfA3ByZXBDt5akAAFS3AAAAQkAAgDBAAAECgW2AAMABwAVtwQDBQIEAwcAAC8yLzMBLzMvMzEwEyERITchESHBA0n8t2gCef2HBbb6SmgE5gACAJj/4wGJBbYAAwAOACtAFAMJCQIEBA8QAQEMAgwGT1kMFgIDAD8/KxESADkYLxESATkRMzMRMzEwASMDMwM0MzIWFRQGIyImAUZpM8/heDo/QDk0RAGTBCP6tIhGQkBHPwAAAgCFA6YCsAW2AAMABwAfQA0AAwcEAwQICQYCBwMDAD8zzTIREgE5OREzETMxMAEDIwMhAyMDAT8oaSkCKyloKQW2/fACEP3wAhAAAAIAMwAABPYFtgAbAB8AmUBVCB8cFQQUCREMDAkSDw4LBAoTExQWHR4HBAYXBAEAGQQYBQUGFAYKIQMaFwMYChggIQgEDA0MTlkcAQ0fABAREE5ZGRURTw0BTxEBDRENEQUXEwMKBQAvMz8zEjk5Ly9dXREzMysRADMzETMzKxEAMzMREgE5OREXMxESOTkRMxESFzkREhc5ETMREhc5MjIRMxESFzkxMAEDIRUhAyMTIQMjEyE1IRMhNSETMwMhEzMDIRUBIRMhA9VCARv+zVSJVP7RUohQ/voBH0T+6wErUotSATFUhlQBCPzlAS9C/tEDg/6sgf5SAa7+UgGugQFUfwG0/kwBtP5Mf/6sAVQAAwCD/4kEDAYSACAAJgAtAGZANScRJR0XBAQqFA0FIQAAGQURCQUuLyUNBg1NWQMGJA4qDkxZHSorHBQcTVkXKhQGFAYUBRYFAC8vEjk5Ly8SOTIrEQAzETMrEQAzETMrEQAzERIBFzkRMxEzMzMzETMzMxEzMTABFAYHFSM1IiYnNRYWMxEmJjU0Njc1MxUWFwcmJxEeAgc0JicRNgEUFhcRBgYEDMy3gXDSQ1PZWc2ly6eBuKs0lZqdnEqqWYDZ/d1ab2NmAcGIsRfo3yMfnCUvAbhBrIiDqBK2tAVFgzsL/k4yX3tlSFks/nseAwdMXCkBgxBdAAAFAGj/7AYtBcsACQAVACEALQAxAEVAJAAQBQoWKBwiIi4oCjAQBjIzAw0fKw0rDSswMQYwGBklGQcTBwA/Mz8zPz8SOTkvLxEzETMREgEXOREzETMRMxEzMTATFBYzMhEQIyIGBRQGIyImNTQ2MzIWARQWMzI2NTQmIyIGBRQGIyImNTQ2MzIWAQEjAfJKU6SkU0oBypmUjJuVkpGcAaZKVFRQUFRUSgHLmZSOmZWSjp/+/vzVkwMrBAKqqgFUAVKoquTp7t/j5u7826upp62rpaWr4+nu3uPm6wMg+koFtgAAAwBx/+wF0wXNAAsAFQA1AFFAMBMWAB0GIyorListIw4mGR0WCTY3MwxJWTMTDyctDjAFLwMZJgMqKiAvEiAJSlkgBAA/KwAYPxI5Lxc5Ehc5PysREgEXOREzETMRMxEzMTABFBYXNjY1NCYjIgYTMjcBDgIVFBYlNDY3LgI1NDYzMhYVFAYHATY2NzMCBwEjJwYGIyImAZ5IV4FlZ1ZZb5vxn/5Lb1wsm/65i7RVPSTEr6K6iJ0BlzhDF6hEiQEr5bl29JbX7QSTRX1YS39TTWFg+52aAahEWWZBdYn6gshmX2JqOZaop5VrtV3+eT6nY/7ilP7dsmpc1AAAAQCFA6YBPwW2AAMAFLcAAwMEBQIDAwA/zRESATkRMzEwAQMjAwE/KGkpBbb98AIQAAABAFL+vAIhBbYADQAcQAwHAAoEAAQODwsnAwMAPz8REgE5OREzETMxMBMQEjczBgIVFBIXIyYCUpuSopCRlIugk5oCMQEJAc6uwf4y9PD+Nr2qAcYAAAEAPf68AgwFtgANABxADAQKBwAKAA4PCgMEJwA/PxESATk5ETMRMzEwARACByM2EjU0AiczFhICDJuSoIuUkZCik5oCMf75/jqovAHL8PQBzsGv/jEAAQBWAn8EDgYUAA4AMEAbAwUEAQcNCgkLCQ8QBAoBDQIMDA0KBwQGCA4AAD/EMhc5ETMRMxEzERIBFzkxMAEDJRcFEwcDAycTJTcFAwKRKwGOGv6D+KywoLDy/ocdAYcrBhT+dW+2H/66XgFq/pZeAUYftm8BiwAAAQBoAOMEKQTDAAsAKEATAAQECQUFDA0DBwgHUFkADwgBCAAvXTMrEQAzERIBOREzMxEzMTABIRUhESMRITUhETMCjQGc/mSL/mYBmosDF4r+VgGqigGsAAEAP/74AW0A7gAIABG1BQAJCgUAAC/NERIBOTkxMCUXBgIHIzYSNwFeDxpiNX0bQQ3uF2T+93JoATJcAAEAVAHZAj8CcQADABG1AgAFBAABAC8zERIBOTkxMBM1IRVUAesB2ZiYAAEAmP/jAYkA8gALABhACwYAAAwNCQNPWQkWAD8rERIBOREzMTA3NDYzMhYVFAYjIiaYPTk6QUI5M0NqQ0VFQ0FGPwAAAQAUAAAC2wW2AAMAE7cCAAQFAwMCEgA/PxESATk5MTABASMBAtv936YCIQW2+koFtgACAGb/7AQtBc0ACwAXAChAFBIADAYABhkYCRVLWQkHAw9LWQMZAD8rABg/KxESATk5ETMRMzEwARACIyICERASMzISARASMzISERACIyICBC3v9uz27vTu9/zhlqSmlZWmpJYC3f6F/ooBfwFyAX4Bcv5+/pL+wf7dAScBOwE7ASX+3wABALwAAALLBbYACgAkQBAJAAEIAQsMBAkHBwEJBgEYAD8/EjkvEjkREgE5OREzMzEwISMRNDcGBgcnATMCy6IIFTTUWAGDjAQSgnQVLqxyASsAAQBkAAAEJQXLABkAK0AXGAEHEwATDgEEGhsQCktZEAcBGExZARgAPysAGD8rERIBFzkRMxEzMTAhITUBPgI1NCYjIgYHJzYzMhYVFAIHARUhBCX8PwGBsHA4jn5bo2RYyu7O6pzW/sAC8I8Bg7KYkFN1iTxPcajTsov+8ND+xwgAAAEAXv/sBBsFywAnAENAJBsAEwcHAAMWIg0GKCkDFxYXFktZFxcKJSUeS1klBwoRS1kKGQA/KwAYPysREgA5GC8rERIAORESARc5ETMRMzEwARQGBxUWFhUUBCEiJic1FhYzIBEQISM1MzI2NTQmIyIGByc2NjMyFgPunZCwqv7e/vV0wVtf12ABe/5ekJKryJN+YKptVFrrgtXsBF6Msh4IFrSS0eEjLJ4vMQEpAQqPl4ZrejRGcEdRwwAAAgArAAAEagW+AAoAEgA8QB4SBQkCAgsHAwADBQMTFAEFEgVMWQkPBxISAwcGAxgAPz8SOS8SOTMrEQAzERIBFzkRMzMzETMRMzEwASMRIxEhNQEzETMhETQ3IwYHAQRq2Z/9OQK2sNn+iAoIMCr+NwFQ/rABUJED3fwpAeaPtGA//XYAAQCF/+wEHQW2ABoAOkAfDwMZFAgUFwMEHBsAEUtZAAAGFRUYTFkVBgYMS1kGGQA/KwAYPysREgA5GC8rERIBFzkRMxEzMTABMgQVFAAjIic1FhYzMjY1ECEiBycTIRUhAzYCLecBCf7f/veCRtBlsMP+iV+fVjcC1/23JXMDfeXH4/7+T6AtM6adATIdNwKsmf5JFwAAAgB1/+wELwXLABYAJABEQCMaEQshIQAABhEDJiUMCw4dTVkLDg4UAxQXS1kUGQMITVkDBwA/KwAYPysREgA5GC85KxEAMxESARc5ETMRMxEzMTATEAAhMhcVJiMiAgMzNjMyFhUUAiMiAAUyNjU0JiMiBgYVFBYWdQFPAUhxQU1j6/gMDG7uxeP51OP+9gHrjp2SkVqWWVCTAnEBrwGrE48Z/tv+xqzuzOT++wFVyLOpkaZKgkZnsmgAAQBeAAAEKwW2AAYAH0AQAQUFAAIDBwgDAkxZAwYAGAA/PysREgEXOREzMTAhASE1IRUBAR0CXvzjA839qgUdmYX6zwADAGj/7AQpBcsAFgAiAC4ATUApFw8mFCwDHQkJAwYRFA8GLzAGESkgKSBLWSkpDAAMGk1ZDBkAI01ZAAcAPysAGD8rERIAORgvKxESADk5ERIBFzkRMxEzETMRMzEwATIWFRQGBxYWFRQGIyImNTQlJiY1NDYDFBYzMjY1NCYnBgYBIgYVFBYXNjY1NCYCSMjqhpOylv7d6vwBMop463enl5WmnMKVhgE6fY52n493kQXLuqRssklVu3u22c28+4xOtXCfvfumeIaMemGXR0CbA2d4ZFyEQjyKXGV3AAACAGr/7AQlBcsAFwAlAEFAIhsRIgoKAAAEEQMmJw4eTVkLFA4OAhQUGEtZFAcCB01ZAhkAPysAGD8rERIAORgvEjkrERIBFzkRMxEzETMxMAEQISInNRYzMhITIwYGIyImNTQSMzIWEgEiBhUUFjMyNjY1NCYmBCX9aHREUGbw9QsMN7ZywuT/0JXfeP4Uj5yQk1uZWFKTA0b8phSPGgEpATNTV+jQ5AEImf7bATC4pJClSoBGabJmAAACAJj/4wGJBGQACwAVAChAFBAGBgwAABYXDhNPWQ4QCQNPWQkWAD8rABg/KxESATkRMzMRMzEwNzQ2MzIWFRQGIyImETQzMhUUBiMiJpg9OTpBQjkzQ3Z7QjkzQ2pDRUVDQUY/A7uHh0FGPwACAD/++AGFBGQACAASACJAEAENDQUJCRQTCxBPWQsQBQAAL80/KxESATkRMzMRMzEwJRcGAgcjNhI3AzQzMhUUBiMiJgFeDxpiNX0bQQ0Vd3tCOTo97hdk/vdyaAEyXALvh4dBRkYAAAEAaADyBCkE2QAGABVACQQABQEEBwgDAAAvLxESARc5MTAlATUBFQEBBCn8PwPB/PIDDvIBpmIB35X+jf64AAACAHcBwQQZA+MAAwAHACpAFQcCBAACAAkIBAVQWQQBAFBZDwEBAQAvXSsAGC8rERIBOTkRMxEzMTATNSEVATUhFXcDovxeA6IDWomJ/meJiQAAAQBoAPIEKQTZAAYAFUAJBQECAAQHCAYDAC8vERIBFzkxMBMBATUBFQFoAw/88QPB/D8BiQFGAXWV/iFi/loAAAIAG//jAzkFywAbACYAOUAdIRwbAAcTEwAcDgQnKAAAJBAkHk9ZJBYQCklZEAQAPysAGD8rERIAORgvERIBFzkRMxEzETMxMAE1NDY3NjY1NCYjIgYHJzYzMhYVFAYGBwYGFRUDNDMyFhUUBiMiJgEhSGKIR4N7T5ZhO73Ov9QnTH5lQbJ4Oj9AOTREAZM2dZdUc3RSZm8lMYdjvKtJb2NuVnJfIf7XiEZCQEc/AAIAef9GBrgFtAA1AD8ARUAiIy42DjsHFBsAACkUDi4FQEEYODgEPQgRCxELESsfMgMmKwAvMz8zEjk5Ly8SOTIzMxEzERIBFzkRMxEzMxEzETMxMAEUBgYjIiYnIwYGIyImNTQ2MzIWFwMVFDMyNjU0AiQjIgQCFRAAITI3FQYjIAAREBIkITIEEgEUMzITEyYjIgYGuFigaFZ2CwgolWaWqezARKxFGYVbcpT+77Hf/rauAUIBL9LiwPT+lf5v1gGMAQDXAU+3+/bDzxIOSFWCkwLZjuyCaFFXYs2wzP8ZFv4qFrLXrLUBEJO5/qnh/s/+uFaFVAGPAWYBBAGW37X+s/6k/gE5AQUUtAACAAAAAAUQBbwABwAOADlAHgIOCwgBBQADAAcDBAcEEA8OAklZCwUODgQFAwAEEgA/Mz8SOS8SOSsREgE5OREzETMREhc5MTAhAyEDIwEzAQEDJicGBwMEYLb9trSsAkKPAj/+ZaohIxYprAHR/i8FvPpEAmoBxVZ9YHP+OwADAMkAAAS+BbYADgAXACAASUAmEwQdCg8ZGQ4KBAcOBCEiCA8YDxhKWQ8PDgAOGUpZDhIAF0pZAAMAPysAGD8rERIAORgvKxESADkREgEXOREzETMRMxEzMTATISAEFRQGBxUEERQEIyETITI2NTQmIyMRESEyNjU0JiPJAZ0BIwEEkYsBTf737v4CqgEYtJ6wwPoBMbGzt7sFtq68gqkZCjn+28TcA0Rxhntt/ZH93YmSiIAAAAEAff/sBM8FywAWACZAFAMOFAkOAxcYEgBJWRIECwZJWQsTAD8rABg/KxESARc5ETMxMAEiABEQADMyNxUGIyAAETQSJDMyFwcmAzvx/ukBDfmZxJjf/r3+oakBP9jmrEimBTP+v/7p/uH+xzeVOQGIAWniAVS4VJJOAAACAMkAAAVYBbYACAARAChAFA4ECQAEABITBQ1KWQUDBA5KWQQSAD8rABg/KxESATk5ETMRMzEwARAAISERISAAAxAAISMRMyAABVj+d/6P/msBwAFVAXq0/uH+5ffPATABMgLp/pb+gQW2/ob+pwEeASL7cAErAAABAMkAAAP4BbYACwA6QB8GCgoBBAAIAQQMDQYJSVkGBgECAgVJWQIDAQpJWQESAD8rABg/KxESADkYLysREgEXOREzETMxMCEhESEVIREhFSERIQP4/NEDL/17Al79ogKFBbaX/imW/eYAAQDJAAAD+AW2AAkAMkAaBgAAAQMIAQMKCwYJSVkGBgECAgVJWQIDARIAPz8rERIAORgvKxESARc5ETMRMzEwISMRIRUhESEVIQFzqgMv/XsCXv2iBbaX/emXAAABAH3/7AU9BcsAGwA6QB8UCBkCAg4bCAQcHQAbSVkAAAUMDBFJWQwEBRdJWQUTAD8rABg/KxESADkYLysREgEXOREzETMxMAEhEQYGIyAAETQSJDMyFwcmIyAAERAAITI3ESEDTAHxdPCe/rT+jrcBWOfqykLGt/71/tQBIQEYmJH+uQL+/TklJgGLAWTkAVe1VpZU/sL+5v7Y/s4jAcIAAQDJAAAFHwW2AAsAM0AZCQEBAAgEBAUABQ0MCANJWQgIBQoGAwEFEgA/Mz8zEjkvKxESATk5ETMRMxEzETMxMCEjESERIxEzESERMwUfqvz+qqoDAqoCsP1QBbb9kgJuAAABAFQAAAJWBbYACwA3QBwFAQoDCAAAAwEDDA0JBAYESlkGAwoDAQNKWQESAD8rEQAzGD8rEQAzERIBFzkRMxEzETMxMCEhNTcRJzUhFQcRFwJW/f6srAICrKxiIwSqJWJiJftWIwAB/2D+fwFoBbYADQAdQA0LCAgODwkDAAVJWQAiAD8rABg/ERIBOREzMTADIic1FjMyNjURMxEUBgxeNkdNY2eqwP5/G5EUeHEFtvpYvtEAAAEAyQAABOkFtgALACpAFQgEBAUFAgsKAAUNDAIIBQkGAwEFEgA/Mz8zEjk5ERIBFzkRMxEzMTAhIwEHESMRMxEBMwEE6cj965mqqgKXyf20AsWI/cMFtv0rAtX9hQABAMkAAAP4BbYABQAfQA4DAAAEBgcBAwADSVkAEgA/KwAYPxESATk5ETMxMDMRMxEhFcmqAoUFtvrkmgABAMkAAAZxBbYAEwAyQBgIBQUGCw4ODQYNFBUBChEDBgsHAw4ABhIAPzMzPzMSFzkREgE5OREzETMRMxEzMTAhASMWFREjESEBMwEzESMRNDcjAQNQ/hAIDp0BAAHPCAHT/qoOCP4MBRCa1PxeBbb7SgS2+koDrqK++vIAAQDJAAAFPwW2ABAALkAVCQYGBwEPDwAHABESCwMHDwgDAQcSAD8zPzMSOTkREgE5OREzETMRMxEzMTAhIwEjFhURIxEzATMmAjcRMwU/wvzhCBCdwAMdCAIOAp8Ey9i0/MEFtvs6GwElPwNHAAACAH3/7AW+Bc0ACwAXAChAFBIADAYABhkYCRVJWQkEAw9JWQMTAD8rABg/KxESATk5ETMRMzEwARAAISAAERAAISAAARASMzISERACIyICBb7+nf7E/r3+oQFgAUQBOwFi+3P98fP49/Lz/QLd/qH+bgGLAWgBZQGJ/nD+oP7X/s0BMgEqAScBMf7NAAIAyQAABGgFtgAJABIANEAaCgUFBg4ABgATFAoESlkKCgYHBxJKWQcDBhIAPz8rERIAORgvKxESATk5ETMRMxEzMTABFAQhIxEjESEgATMyNjU0JiMjBGj+0f7mrKoBewIk/QuZ4sq+yb4EDN7v/cEFtv0bkqGRjgAAAgB9/qQFvgXNAA8AGwA0QBsQChYAAAQDCgQcHQMNBw0ZSVkNBAcTSVkFBxMAP8YrABg/KxESADkREgEXOREzETMxMAEQAgcBIwEHIAAREAAhIAABEBIzMhIREAIjIgIFvuLOAVz3/uM3/r3+oQFgAUQBOwFi+3P98fP49/Lz/QLd/uf+jEL+lgFKAgGLAWgBZQGJ/nD+oP7X/s0BMgEqAScBMf7NAAIAyQAABM8FtgAMABUASEAlDQEBAgwJEQcLCgoHCQIEFhcJDQANAEpZDQ0CAwMVSVkDAwsCEgA/Mz8rERIAORgvKxESADkREgEXOREzETMRMxEzETMxMAERIxEhIAQVEAUBIwElMzI2NTQmIyMBc6oBkQENAQH+2gGNyf6e/s/ptKirvd0CYP2gBbbOz/7eZv1vAmCSj4+RgAABAGr/7AQCBcsAJAA0QBseEwwAABgTBQQlJgweAxYWG0lZFgQDCUlZAxMAPysAGD8rERIAOTkREgEXOREzETMxMAEUBCMgJzUWFjMyNjU0JiYnJiY1NDYzMhcHJiMiBhUUFhYXFhYEAv7o8P78jFrUaKqsPY+SzK/+0dq3NbWrh5g4hYnmrQGFwdhDpCYsgXNMYVI0ScihqchQlEx0Z0xhUTFSvAAAAQASAAAEWgW2AAcAJEASAAEFAQMDCAkHAwQDSVkEAwESAD8/KxEAMxESARc5ETMxMCEjESE1IRUhAouq/jEESP4xBR+XlwAAAQC6/+wFGQW2ABEAJUAREAEKBwEHExIRCAMEDUlZBBMAPysAGD8zERIBOTkRMxEzMTABERQAISAANREzERQWMzI2NREFGf7S/vj++P7fqsjCucgFtvxO+v7iASD8A678RrfExbgDuAABAAAAAATDBbYACgAaQAsBBAwLCAMABAMDEgA/PzMSORESATk5MTABMwEjATMBFhc2NwQMt/3xqP30tAFQOiIkOgW2+koFtvxOo5qioQABABsAAAdMBbYAGQAkQBAZChsaFQ4OBQkYEQoDAQkSAD8zPzMzEjk5ETMREgE5OTEwISMBJiYnBgcBIwEzExYXNjcBMwEWFzY3EzMFxaj+2RU0ARYw/uKo/nu05zAWGzUBBrQBEzAhEzXmtAPTQcYUhJ38MwW2/Hm+mrevA3n8f5vDjswDhQAAAQAIAAAElgW2AAsAI0ASBAYFCwoABg0MAggECQYDAQQSAD8zPzMSOTkREgEXOTEwISMBASMBATMBATMBBJbB/nf+cLQB5v47vAFrAW61/jsCg/19AvwCuv29AkP9TAAAAQAAAAAEewW2AAgAIEAPBAUCBQcDCQoABQEHAwUSAD8/MxI5ERIBFzkRMzEwAQEzAREjEQEzAj0Bhrj+GKz+GboC2wLb/IH9yQIvA4cAAQBSAAAEPwW2AAkAK0AXCAEDBwAHBAEECgsFBElZBQMBCElZARIAPysAGD8rERIBFzkRMxEzMTAhITUBITUhFQEhBD/8EwMI/RADv/z4Ax6FBJiZhftpAAEApv68Am8FtgAHACBADgYBBAABAAgJBQIDBgEnAD8zPzMREgE5OREzETMxMAEhESEVIREhAm/+NwHJ/t8BIf68BvqN+iEAAAEAFwAAAt0FtgADABO3AwEEBQMDAhIAPz8REgE5OTEwEwEjAboCI6b94AW2+koFtgAAAQAz/rwB/AW2AAcAIEAOAwABBgAGCAkABycDBAMAPzM/MxESATk5ETMRMzEwFyERITUhESEzASH+3wHJ/je2Bd+N+QYAAAEAMQInBCMFwQAGABhACQADBwgFAgAEAgAvLzMSORESATk5MTATATMBIwEBMQGyYwHdmP6M/rICJwOa/GYC6f0XAAH//P7FA5r/SAADABG1AAUBBAECAC8zEQEzETMxMAEhNSEDmvxiA57+xYMAAQGJBNkDEgYhAAkAE7YABAsKBoABAC8azRESATk5MTABIyYmJzUzFhYXAxJuQbIoyyByLATZNMA/FUW1NQACAF7/7APNBFoAGQAkAEdAJSIICx4eGRkSCAMlJgECCx5HWQILCwAVFQ9GWRUQBRpGWQUWABUAPz8rABg/KxESADkYLzkrEQAzERIBFzkRMxEzETMxMCEnIwYGIyImNRAlNzU0JiMiByc2NjMyFhURJTI2NTUHBgYVFBYDUiEIUqN6o7kCE7pveomtM1HBYcS9/g6bsabGr22cZ0momwFMEAZEgXtUfywyrsD9FHWqmWMHB21zWl4AAgCw/+wEdQYUABMAHwBEQCIKFxcPDwwdAwwDICENAAwVEhEKEQYABhpGWQYWABRGWQAQAD8rABg/KxESADk5ETMYPz8REgE5OREzETMRMxEzMTABMhIREAIjIiYnIwcjETMRFAczNhciBhUUFjMyNjU0JgKu2O/x1muxPAwjd6YICHTMqpaaqpmWlgRa/tn+8v7y/tVPUo0GFP6Gf2Wki8Pn58ff0dbSAAABAHP/7AOLBFwAFgAmQBQPAwMVCQMYFwYNRlkGEAASRlkAFgA/KwAYPysREgEXOREzMTAFIgAREAAzMhYXByYmIyARFBYzMjcVBgJm7v77AQn1T54tMzeCMv6yo6CJkG4UASUBDAETASwiF40WHf5Wytg7kzkAAgBz/+wENwYUABIAHwBCQCEdBhcADg4RBhEgIRIVDwAAAQEMAwkJGkZZCRADE0ZZAxYAPysAGD8rERIAOTkRMxg/PxESATk5ETMRMzMRMzEwJSMGIyICERASMzIXMycnETMRIyUyNjU1NCYjIgYVFBYDmglz5dfv8Nbfdw0HBKaH/p6qmZuqkpuak6cBJgEPAQ8BLKJPTQG++ex3uc4j6cfjz9LWAAIAc//sBBIEXAATABoAO0AfGAoXCwMDEQoDHBsXC0ZZFxcABgYURlkGEAAORlkAFgA/KwAYPysREgA5GC8rERIBFzkRMzMRMzEwBSIAERAAMzISFRUhFhYzMjcVBgYDIgYHITQmAn/z/ucBBdzO8P0NBbmosa1YnZyEnQ4CPYwUASgBBwEJATj+8d5pwchKlCYhA+WsmJ2nAAABAB0AAAMOBh8AFAA5QB0UDAwTAgIHAwUDFRYKD0ZZCgABBQcFRlkTBw8DFQA/PzMrEQAzGD8rERIBOTkRMzMRMzMSOTEwASERIxEjNTc1ECEyFwcmIyIGFRUhAp7+6abExAFhV3UrYEReWgEXA8f8OQPHSzw9AZQjhR99ikcAAAMAJ/4UBDEEXAAqADcAQQBuQD4rGTglDB89BTETARMFAioiHB8lGQpCQxwPNQ81RlkIO0dZCiIIKg8IDwgWKioCR1kqDyg/R1koEBYuR1kWGwA/KwAYPysAGD8rERIAOTkYLy8REjk5KysREgA5ERIBFzkRMxEzETMRMxEzMTABFQcWFhUUBiMiJwYVFBYzMzIWFRQEISImNTQ2NyYmNTQ2NyYmNTQ2MzIXARQWMzI2NTQmIyMiBhMUFjMyNTQjIgYEMcscLNzAMStqSlrCsr/+3P7o1+mAdCo5QEVVa9jGVkX+EZaM0clumMdxflqCdPP2dX4ESGkYI3FHocAIOFUtK5aPtr+gkmSSGhNQNTxaKiOobLTDFPsAWVx9a1lFbAM8c3bs934AAQCwAAAERAYUABYAM0AZDgwICAkAFgkWFxgOCRISBEZZEhAKAAAJFQA/Mz8/KxESADkREgE5OREzETMRMzMxMCERNCYjIgYVESMRMxEUBzM2NjMyFhURA556gq2fpqYICjG1dMnJAsWGhLzW/cMGFP4pVThPW7/Q/TUAAAIAogAAAWYF3wADAA8AI0ARCgAABAEBEBENB0hZDQIPARUAPz/OKxESATkRMzMRMzEwISMRMwM0NjMyFhUUBiMiJgFWpqa0OCooOjooKjgESAEpOTU2ODg3NwAAAv+R/hQBZgXfAAwAGAAsQBYTCwsNCAgZGhYQSFkWQAkPAAVGWQAbAD8rABg/Gs4rERIBOREzMxEzMTATIic1FjMyNjURMxEQAzQ2MzIWFRQGIyImK187RUNOSaa0OCooOjooKjj+FBmHFFVXBPz7EP68B105NTY4ODc3AAEAsAAABB0GFAAQADZAGxAOCgoLCwgGBAUIBBESDAAAEBAICAMHCxUDDwA/PzMSOS85ETM/ERIBFzkROREzETMzMTABNjcBMwEBIwEHESMRMxEUBwFUK1gBYsX+RAHbyf59faSkCAIxPWMBd/4t/YsCBmz+ZgYU/Mc3cwABALAAAAFWBhQAAwAWQAkAAQEEBQIAARUAPz8REgE5ETMxMCEjETMBVqamBhQAAQCwAAAGywRcACMARkAjFREREggJACMJEiMDJCUcFhUVEhkEDRkNRlkfGRATDwkAEhUAPzMzPz8zKxEAMxESORgvMzMREgEXOREzETMRMxEzMTAhETQmIyIGFREjETQmIyIGFREjETMXMzY2MyAXMzY2MzIWFREGJXB2m5SmcHeckaaHGwgvq2oBAU8IMbp3urkCyYODsrn9nALJg4O71f3BBEiWUFq6VmS/0v01AAABALAAAAREBFwAFAAxQBgAFAwICAkUCRYVDAkQEARGWRAQCg8ACRUAPzM/PysREgA5ERIBOTkRMxEzETMxMCERNCYjIgYVESMRMxczNjYzMhYVEQOeeoKsoKaHGwgzuHHGyALFhoS61v3BBEiWUVm/0v01AAIAc//sBGIEXAAMABgAKEAUEwANBwAHGhkKFkZZChADEEZZAxYAPysAGD8rERIBOTkRMxEzMTABEAAjIiYCNRAAMzIAARQWMzI2NTQmIyIGBGL+8u6T5HwBDO7mAQ/8vaijo6mppaOmAiX+9P7TigECrQEMASv+zv770tzb09HZ1gACALD+FAR1BFwAFAAhAD9AIBkLBAcHCB8SCBIiIwQLAA8PFUZZDxAJDwgbABxGWQAWAD8rABg/Pz8rERIAOTkREgE5OREzETMRMzMzMTAFIiYnIxYVESMRMxczNjYzMhIREAIDIgYHFRQWMzI2NTQmAq5rsTwMDKaHFwhAqm7a7fHuqJYCmqqOoaEUT1JgVv49BjSWWlD+1v7z/vL+1QPjussl58fmys3bAAIAc/4UBDcEXAAMAB8AREAiChAdFgMaGhkQGSAhGhsXDx0eHhYNExMHRlkTEA0ARlkNFgA/KwAYPysREgA5OREzGD8/ERIBOTkRMxEzMzMRMzEwJTI2NzU0JiMiBhUUFhciAhEQEjMyFzM3MxEjETQ3IwYCTqaYBZypkpuZfdTu8NbheQkYg6YLDXN3stMl5srjz8/ZiwEqAQsBDQEuqpb5zAHVZEanAAEAsAAAAycEXAAQACpAFA0JCQoKAhESCw8NAAoVAAVGWQAQAD8rABg/Ejk/ERIBOTkRMxEzMTABMhcHJiMiBhURIxEzFzM2NgKkSToXRDSFvaaJEwg9rARcDJoP2KH9tARIy2t0AAEAav/sA3MEXAAkADZAHB4TDAAAGAUTBCUmDB4DFhYbRlkWEAYDCUZZAxYAPysAGC8/KxESADk5ERIBFzkRMxEzMTABFAYjIic1FhYzMjY1NCYnLgI1NDYzMhcHJiMiBhUUFhYXFhYDc+TO2npPtVSCjG+hmYE/2r6xqTulhnZ4LWSOw4kBK5mmRZooLlNVQFs+OVVsS4abSIdESkEsPjg1R5AAAQAf/+wCqAVGABYANEAbEBQUCQsJEgMEGBcKExATR1kOQBAPBwBGWQcWAD8rABg/Gs0rEQAzERIBFzkRMxEzMTAlMjY3FQYGIyARESM1NzczFSEVIREUFgISLFIYG2kq/sKdnUZgAT7+wl51DQd/DREBTwKMUEXq/oH9e2NqAAABAKT/7AQ5BEgAFAA0QBkBEwcMDAoTChUWDA0NEAgUDxAERlkQFgsVAD8/KwAYPzMSOREzERIBOTkRMxEzETMxMAERFBYzMjY1ETMRIycjBgYjIiY1EQFMeoKsn6aJGAkztXTIxwRI/TmGhLzVAkD7uJNRVr7RAs0AAAEAAAAABAIESAALABhACgEKDA0FCQEPABUAPz8zORESATk5MTAhATMTFhczNhITMwEBoP5gsuxQDggLdcyy/mAESP125EQ1AU0CMPu4AAEAFwAABiMESAAcACxAFAkbHR4XFg4NAwQNBAgaEgkPAAgVAD8zPzMzEjk5ETMRMzMzERIBOTkxMCEDJicjBgcDIwEzEhIXMzY2NxMzExYXMzY2EzMBBC/JEzQIKB7PwP7VrmpvCAgLMRLJtMQ4FAgEI7+s/tECgzvRr1/9fwRI/mP+UEs5tTUCdf2LrHUklgLc+7gAAAEAJwAABAgESAALACJAEQcFBgABBQwNCQMBCAsVBAEPAD8zPzMSOTkREgEXOTEwAQEzAQEzAQEjAQEjAbj+g70BIQEgu/6DAZG8/s3+yrwCMQIX/lwBpP3p/c8BvP5EAAEAAv4UBAYESAAVACRAEgkPAAMWFwQNAA0SRlkNGwgADwA/Mj8rERIAORESARc5MTATMxMWFzM2NhMzAQYGIyInNRYzMjc3ArLwTxMIDVPmsv4pRruITEo3RKtJPQRI/Y/WXzP3Anz7ILmbEYUMwJwAAAEAUgAAA20ESAAJACtAFwgBAwcABwQBBAoLBQRHWQUPAQhHWQEVAD8rABg/KxESARc5ETMRMzEwISE1ASE1IRUBIQNt/OUCVv3PAuf9sgJdcQNWgYH8ugABAD3+vALBBbYAHAAsQBUZGhoLFwAADwcUAwMHCwMdHhMDBCcAPz8REgEXOREzETMzETMRMxEzMTAlFBYXFSYmNRE0JiM1NjY1ETQ2MxUGFREUBxUWFQHbdXG+0H54gnTYtubf3wxmXAKMAqqaAS9oWY0CXGABMpusiwbB/tnXJwwn1wABAe7+EAJ7BhQAAwAWQAkCAwMEBQMbAAAAPz8REgE5ETMxMAEzESMB7o2NBhT3/AABAEj+vALLBbYAHQAsQBUVBQoSEgIZAB0dDg4ZBQMeHxUnBgMAPz8REgEXOREzETMRMzMRMxEzMTABJjURNCc1MhYVERQWFxUiBhURFAYHNTY2NRE0NjcCCt/juNN2gnp+zb5vdG5xAj8n1wEnwQaLrpn+zmFbAo1ZaP7RmasCjAJcZgEpcngUAAABAGgCUAQpA1QAFwAkQBEDDxgZEgxQWQMSDwYGAFBZBgAvKwAQGMQvxCsREgE5OTEwASIGBzU2MzIWFxYWMzI2NxUGIyImJyYmAVI1fzZkkERxWUJiLzaANmaOSH5IS1oCyUM2l20cJhwbQDmWbiEgIBgAAAIAmP6LAYkEXgADAA4AK0AUAgQEAwkJDxAAAAMMDAZPWQwQAyIAPz8rERIAORgvERIBOREzMxEzMTATMxMjExQjIiY1NDYzMhbbaTPP4Xk8PD85M0YCrPvfBUyHR0A/SEAAAQC+/+wD2wXLABsAPkAeFggNAwMKBAAQEAQIAxwdGQUCEwoNAg0CDQQLBwQZAD8/Ejk5Ly8RMzMRMzMREgEXOREzETMzETMRMzEwJQYHFSM1JgI1ECU1MxUWFhcHJiMiBhUUFjMyNwPLaZOFy8EBjIdLjjExhW2sop+njY7wNgbIziABEfoB/D6spAMhF4wz09nUyzsAAQA/AAAERAXJAB0ASEAmGBMJDQ0aFhECCxYTBR4fDBgZGE5ZCRkZEwATEExZExgABUtZAAcAPysAGD8rERIAORgvMysRADMREgEXOREzMxEzETMxMAEyFwcmIyIGFREhFSEVFAYHIRUhNTY1NSM1MxE0NgKqvqo9mo97fQGm/lpBSgMb+/vNxsbgBclUhU18jP7Zf91kiCyajS/0338BPLLNAAACAHsBBgQXBKAAGwAnACBADRwAIg4ADigpHxUVJQcALzMzLzMREgE5OREzETMxMBM0Nyc3FzYzMhc3FwcWFRQHFwcnBiMiJwcnNyY3FBYzMjY1NCYjIga4Sodeh2iCf2aJX4ZKSoNciWZ/hmSHXIVKgZ10dJ6gcnSdAtN6a4xchUlJhVyKcXaDZ4dchUdJhVyIa3xwoJ9xcqKkAAABAB8AAARxBbYAFgBWQC4SDgcLCxAMBQkCCQMMFA4VBxcYCg4OBw8GEhIDABMVDxMfEwIPEw8TDAEVBgwYAD8/MxI5OS8vXRESOTIyETMRMzMRMxESARc5ETMRMzMRMxEzMTABATMBIRUhFSEVIREjESE1ITUhNSEBMwJIAXuu/mABBv7DAT3+w6T+xAE8/sQBAP5lsgLfAtf8/n+qf/70AQx/qn8DAgACAe7+EAJ7BhQAAwAHACRAEAIGBgMHBwgJBAMEAwcbAAAAPz85OS8vERIBOREzMxEzMTABMxEjETMRIwHujY2NjQYU/Pj+Dfz3AAIAe//4A5YGHQAxAD0AQ0AmMgATBioeOBkZHgwGACMGPj8VAzs2HC0GIQkhJ0dZIRUJEEdZCQAAPysAGD8rERIAFzkREgEXOREzETMRMxEzMTATNDY3JiY1NDYzMhYXByYmIyIGFRQWFxYWFRQGBxYVFAYjIic1FhYzMjY1NCYmJy4CNxQWFxc2NTQmJwYGi1ZOSlTPxV6fYTVih0x0dHuaupZSSpnq1NqATsJSho0wbHOOhkKShKcxiZO5RFUDKVaJJShvVXmLHSeDJxs7QDxUN0SXa1qNKVGSjJlBlCUtTEcuOjorNFpyYk1pPRNQb1NwORNkAAIBNQUOA2gF0wALABcAHkAMBgAMEgASGBkPAxUJAC8zzTIREgE5OREzETMxMAE0NjMyFhUUBiMiJiU0NjMyFhUUBiMiJgE1NSUmNzcmJTUBfTUlJTc3JSU1BXE0Li40MjExMjQuLjQyMTEAAAMAZP/sBkQFywAWACYANgBGQCcnFwMPLx8fFAkPFwU3OAYMABIPDB8MAgASEBICDBIMEhsrIxMzGwQAPzM/MxI5OS8vXV0RMxEzERIBFzkRMxEzETMxMAEiBhUUFjMyNxUGBiMiJjU0NjMyFwcmATQSJDMyBBIVFAIEIyIkAjcUEgQzMiQSNTQCJCMiBAIDfX2Hf4NWfTBlRsLQ3b+Adjps/JfIAV7KyAFeysL+otDP/qLDaa4BLayuASqvrv7XsK7+1q8EI66aqKItfBQc8djR9jx2M/64yAFeysj+osrF/qbQzwFaxq3+062uASmwrgEqr67+1wAAAgBGAxQCcQXHABYAHwA3QBwXBhsKAQEWFhAGAyAhHAoKEhkWAAMQAwIDDRIfAD8z1F3EMxI5LzMREgEXOREzETMzETMxMAEnBiMiJjU0Njc3NTQjIgcnNjMyFhURJRQzMjU1BwYGAhQYXIxfb5qldZRkaCtyhYKJ/lBwyWJwZwMhVGFjZmZpBgQnhTNgOGl5/jy8ZLQxBAQ5AAIAUgB1A6oDvgAGAA0AKUATAwYKDQIECwkJBA0GBA4PDAUIAQAvMy8zERIBFzkRMxEzETMRMzEwEwEXAQEHASUBFwEBBwFSAVZ3/t8BIXf+qgGLAVh1/uEBH3X+qAInAZdF/qL+oUcBlxsBl0X+ov6hRwGXAAABAGgBCAQpAxcABQAbQAwCAQQBBgcFBFBZBQIALy8rERIBOTkRMzEwAREjESE1BCmJ/MgDF/3xAYWKAP//AFQB2QI/AnECBgAQAAAABABk/+wGRAXLAAgAFgAmADYAXUAzJxcAERESBAkvHx8NCQwSFwY3OAwQEAAADhMOEggTDxIfEgIAExATAhITEhMbKyMTMxsEAD8zPzMSOTkvL11dETMRMxESOS8zETMREgEXOREzETMRMxEzETMxMAEzMjY1NCYjIwUUBgcTIwMjESMRITIWATQSJDMyBBIVFAIEIyIkAjcUEgQzMiQSNTQCJCMiBAIC02xQYVZdagGyVU3uqM+HlAEFppv738gBXsrIAV7Kwv6i0M/+osNprgEtrK4BKq+u/tewrv7WrwL6U0BLQYhQex7+dQFi/p4De4L+xcgBXsrI/qLKxf6m0M8BWsat/tOtrgEpsK4BKq+u/tcAAf/6BhQEBgaTAAMAEbUABQEEAQIALzMRATMRMzEwASE1IQQG+/QEDAYUfwACAH8DXALuBcsADAAYACFADg0AEwYABhkaEArAFgMEAD8zGswyERIBOTkRMxEzMTATNDYzMhYVFAYGIyImNxQWMzI2NTQmIyIGf7WCgrZSklSCtXN1UVBzcVJTcwSTgra1g1SPVLSDUnJxU1RxcgD//wBoAAEEKQTDAiYADgAAAAcCKwAA/XQAAQAxAkoCjQXJABgAI0ARBxMXAQEOEwAEGhkKEB8XASAAPzM/MxESARc5ETMRMzEwASE1Nz4CNTQmIyIGByc2MzIWFRQGBwchAo39pOxZUiFQPzRiRUKDmISTWZOuAbgCSmjmVmFMNkRFJjJYb4JwUJeKpQABACECOQKNBckAIwA5QCIPBQUAAxIeCgYkJRJdE20TAkwTAQsTGxMCExMIGiEfDQghAD8zPzMSOS9dXV0zERIBFzkRMzEwARQGBxYVFAYjIic1FjMyNTQjIzUzMjY1NCYjIgYHJzY2MzIWAnNSRLC4qJh0k3vT53V3Z2NQQ0JwOEU/jF6InQTnUGcXL6KAjzh7RKKRa09EPUQrI1otNncAAQGJBNkDEgYhAAkAE7YJBAoLBIAJAC8azRESATk5MTABNjY3MxUGBgcjAYkwbyDKLK5AbwTyPrBBFUG+NAABALD+FAREBEgAFgA1QBoFCgoIEAATExQIFBgXBhUPFBsNAkZZDRYJFQA/PysAGD8/MxESATk5ETMRMzMRMxEzMTABEDMyNjURMxEjJyMGIyInIxYVESMRMwFW/qufpogaCm/lllgKCqamAX3++r3UAkD7uJOnXFSg/sAGNAABAHH+/ARgBhQADwAnQBIEBQEAAAULAxARCAgFAw8FAQUALzM/MxI5LxESARc5ETMRMzEwASMRIxEjEQYjIiY1EDYzIQRgctVzPlTYy9roAi3+/Aaw+VADMxL6+wEE/gABAJgCTAGJA1oACwAXQAoGAAANDAMJT1kDAC8rERIBOREzMTATNDYzMhYVFAYjIiaYPjg6QUI5M0MC00JFRUJBRj8AAAEAJf4UAbQAAAASACRAEBEOCwAADgUDExQOEREIAxAAL8wyOS8zERIBFzkRMxEzMTABFAYjIic1FjMyNjU0Jic3MwcWAbSZljMtLTtPUU9tWG43tP7fYWoJaggoNis1EbJzJwABAEwCSgHhBbYACgAgQA4CAAMDCgwLCQkDIAYAHgA/Mj85LxESATk5ETMzMTABMxEjETQ3BgYHJwFSj4UGFjaHQwW2/JQCQ1taFi1fYAACAEIDFAK+BccACwAXACVAEgwGEgAGABgZDwADEAMCAxUJHwA/M8RdMhESATk5ETMRMzEwARQGIyImNTQ2MzIWBRQWMzI2NTQmIyIGAr6rlpKpqJeYpf3+W2hpXFxpZ1wEb6S3uqGjtbaienp6ent2dgACAFAAdQOoA74ABgANACNAEQsJBAIAAwcCCgkGDg8MBQgBAC8zLzMREgEXOREzETMxMAEBJwEBNwEFAScBATcBA6j+qHUBH/7hdQFY/nX+qHUBH/7hdQFYAgz+aUcBXwFeRf5pG/5pRwFfAV5F/mn//wBLAAAF0QW2ACcCFwKDAAAAJgB7/wABBwI8Ax39twAJswMCEhgAPzU1AP//AC4AAAXbBbYAJwIXAj8AAAAmAHviAAEHAHQDTv23AAeyAhAYAD81AP//ABoAAAYhBckAJgB1+QAAJwIXAt8AAAEHAjwDbf23AAmzAwIrGAA/NTUAAAIAM/53A1QEXgAdACgAQUAiCBQeIwEcDxwjFAQpKgAdAQwDHR0RJiYgT1kmEBELSVkRIwA/KwAYPysREgA5GC9fXl0REgEXOREzETMRMzEwARUUBgcOAhUUFjMyNjcXBiMiJjU0PgI3NjY1NRMUIyImNTQ2MzIWAk5LYXk9GYR6UJZiO8XGvtgjQFk2ZUG0eTs+QjczRgKsM3qUVGpLTThkcSYwh2C6qkZpWVIvWHRdHwErh0VCQEdA//8AAAAABRAHcwImACQAAAEHAEP/wgFSAAizAhAFJgArNf//AAAAAAUQB3MCJgAkAAABBwB2AIUBUgAIswIYBSYAKzX//wAAAAAFEAdzAiYAJAAAAQcBSwAjAVIACLMCHQUmACs1//8AAAAABRAHLwImACQAAAEHAVIABAFSAAizAhgFJgArNf//AAAAAAUQByUCJgAkAAABBwBqADcBUgAKtAMCJAUmACs1Nf//AAAAAAUQBwYCJgAkAAAABwFQADkAgQAC//4AAAaBBbYADwATAE5ALAoODhEBAAgMARAFBRUFFAkTBhNJWRADSVkKDUlZEAoQCgEGAwUSAQ5JWQESAD8rABg/PxI5OS8vKysrEQAzEQEzERIXOREzMxEzMTAhIREhAyMBIRUhESEVIREhASERIwaB/RL9/uOwAroDyf28Ah394wJE+1QBvnYB0f4vBbaX/imW/eYB0gK1AP//AH3+FATPBcsCJgAmAAAABwB6AgIAAP//AMkAAAP4B3MCJgAoAAABBwBD/7cBUgAIswENBSYAKzX//wDJAAAD+AdzAiYAKAAAAQcAdgA/AVIACLMBFQUmACs1//8AyQAAA/gHcwImACgAAAEHAUv/+wFSAAizARoFJgArNf//AMkAAAP4ByUCJgAoAAABBwBqABIBUgAKtAIBIQUmACs1Nf//ADwAAAJWB3MCJgAsAAABBwBD/rMBUgAIswENBSYAKzX//wBUAAACcwdzAiYALAAAAQcAdv9hAVIACLMBFQUmACs1/////wAAAqEHcwImACwAAAEHAUv+8wFSAAizARoFJgArNf//ADwAAAJvByUCJgAsAAABBwBq/wcBUgAKtAIBIQUmACs1NQACAC8AAAVIBbYADAAXAFdAMhEVFQgEDQAAEwQGBBgZFAYHBklZEQ8HPwevB88H3wcFCwMHBwQJCRBKWQkDBBVKWQQSAD8rABg/KxESADkYL19eXTMrEQAzERIBFzkRMxEzMxEzMTABEAAhIREjNTMRISAAAxAhIxEhFSERMyAFSP53/o/+e5qaAbIBUQF8tf3H5wF7/oW+AmIC6f6W/oECiZYCl/6J/qQCQP38lv4K//8AyQAABT8HLwImADEAAAEHAVIAkwFSAAizARoFJgArNf//AH3/7AW+B3MCJgAyAAABBwBDAHkBUgAIswIZBSYAKzX//wB9/+wFvgdzAiYAMgAAAQcAdgEKAVIACLMCIQUmACs1//8Aff/sBb4HcwImADIAAAEHAUsAtAFSAAizAiYFJgArNf//AH3/7AW+By8CJgAyAAABBwFSAJoBUgAIswIhBSYAKzX//wB9/+wFvgclAiYAMgAAAQcAagDVAVIACrQDAi0FJgArNTUAAQCFARAEDASYAAsAGUAJBwkDAQkBDA0IABkvERIBOTkRMxEzMTABFwEBBwEBJwEBNwEDrGD+oAFeYP6e/qRlAV7+oGQBYQSYY/6e/qBjAV/+oWMBYAFgZf6dAAADAH3/wwW+BfYAEwAbACMATkAsFh8XHgQcFBwKFAAAEg8FCAoGJCUWHiEZDSFJWQ8SCAUEAxANBAMZSVkGAxMAP8YrABg/xhIXOSsREgA5ORESARc5ETMRMxESFzkxMAEQACEiJwcnNyYREAAhMhc3FwcWAxAnARYzMhIBEBcBJiMiAgW+/p3+xOuUZXhssgFgAUTRnWF4asC0bv1gc7Dz+PwnZQKdaqjz/QLd/qH+bmSNT5rGAW0BZQGJXodQlMr+lQEQmvxMUgEyASr++poDr0n+zQD//wC6/+wFGQdzAiYAOAAAAQcAQwBGAVIACLMBEwUmACs1//8Auv/sBRkHcwImADgAAAEHAHYAzwFSAAizARsFJgArNf//ALr/7AUZB3MCJgA4AAABBwFLAH0BUgAIswEgBSYAKzX//wC6/+wFGQclAiYAOAAAAQcAagCYAVIACrQCAScFJgArNTX//wAAAAAEewdzAiYAPAAAAQcAdgAxAVIACLMBEgUmACs1AAIAyQAABHkFtgAMABUANkAcDQkFBQYRAAYAFhcNBEpZCRVKWQ0JDQkGBwMGEgA/PxI5OS8vKysREgE5OREzETMRMzMxMAEUBCEjESMRMxEzIAQBMzI2NTQmIyMEef7R/uG4qqrXARkBFvz6qOLKvsrMAxDj7v7BBbb/AM/96o+klYoAAAEAsP/sBJwGHwAwAEFAIikqBR0jABcMDAAdESoFMTISEiouLiZGWS4AKhUPFUZZDxYAPysAGD8/KxESADkYLxESARc5ETMRMxEzETMxMAEUBwYGFRQWFhcWFhUUBiMiJzUWFjMyNTQmJyYmNTQ2NzY2NTQmIyAVESMRNDYzMhYEGY9YOBtHToxmwrO8az+cSNdTbn9gRUdLQIh//uym3N7O4QTyh3NGQyEgKjkzX51loKtFmicvtktrRlJ7VD9qNTlaNVBV3/tMBLKyu53//wBe/+wDzQYhAiYARAAAAQYAQ44AAAizAiYRJgArNf//AF7/7APNBiECJgBEAAABBgB2KwAACLMCLhEmACs1//8AXv/sA80GIQImAEQAAAEGAUvYAAAIswIzESYAKzX//wBe/+wDzQXdAiYARAAAAQYBUr0AAAizAi4RJgArNf//AF7/7APNBdMCJgBEAAABBgBq4gAACrQDAjoRJgArNTX//wBe/+wDzQaFAiYARAAAAQYBUPcAAAq0AwIoESYAKzU1AAMAXv/sBnMEXAApADQAOwBhQDMqACQRMDgZGQQwORgYHzALAAU8PRstJy1GWRkxBDFHWTgkJxEEBA4iJxY1CA4IRlkUDhAAPzMrEQAzGD8zEjkvORI5MysRADMrEQAzERIBFzkRMxEzMxEzEjk5ETMxMBM0Njc3NTQmIyIHJzY2MzIWFzY2MzISFRUhEiEyNjcVBgYjICcGBiMiJjcUFjMyNjU1BwYGASIGByE0Jl74/rh0d5CjNErHYoKlKTWrbsDo/UMIATpbnVRWlWX+331RxYajua5rWJGonrqkA715iwsCB4ABL6GzCAZEgXtUfyk1V19YYP713mv+dSMnlCYh6X9qqpdfWamaYwcIbQIypp6cqAD//wBz/hQDiwRcAiYARgAAAAcAegFGAAD//wBz/+wEEgYhAiYASAAAAQYAQ7UAAAizAhwRJgArNf//AHP/7AQSBiECJgBIAAABBgB2TgAACLMCJBEmACs1//8Ac//sBBIGIQImAEgAAAEGAUv3AAAIswIpESYAKzX//wBz/+wEEgXTAiYASAAAAQYAagoAAAq0AwIwESYAKzU1////2gAAAWMGIQImAPMAAAEHAEP+UQAAAAizAQURJgArNf//AKkAAAIyBiECJgDzAAABBwB2/yAAAAAIswENESYAKzX///+zAAACVQYhAiYA8wAAAQcBS/6nAAAACLMBEhEmACs1////7AAAAh8F0wImAPMAAAEHAGr+twAAAAq0AgEZESYAKzU1AAIAcf/sBGIGIQAbACYASkArIQYMHBwAABgZFg4RExAGCScoCR9GWQsDFhEZDg8FFAkJAxcUAQMkRlkDFgA/KwAYPzMSOS8SFzkSOSsREgEXOREzETMRMzEwARAAIyIANTQAMzIXNyYnBSc3Jic3Fhc3FwcWEgM0JiMgERQWMzI2BGL++/fe/ukBB9ziZAg5zf7xSelcXkWcZu5Mz5ilqLSc/q+voq+hAjP+5/7SAQ3i5gEGeQTWv5tshT4xdUlLimt3j/5y/uiTqv6Yp7fJAP//ALAAAAREBd0CJgBRAAABBgFSDgAACLMBHhEmACs1//8Ac//sBGIGIQImAFIAAAEGAEPUAAAIswIaESYAKzX//wBz/+wEYgYhAiYAUgAAAQYAdlYAAAizAiIRJgArNf//AHP/7ARiBiECJgBSAAABBgFLDgAACLMCJxEmACs1//8Ac//sBGIF3QImAFIAAAEGAVLxAAAIswIiESYAKzX//wBz/+wEYgXTAiYAUgAAAQYAahsAAAq0AwIuESYAKzU1AAMAaAD8BCkEqAADAA8AGwAzQBgWCgoQBAIEAQMcHRkTEwEHDQ0BAQBQWQEALysRADMYLzMRMy8zERIBFzkRMzMRMzEwEzUhFQE0NjMyFhUUBiMiJhE0NjMyFhUUBiMiJmgDwf2uOzY0OjszND07NjQ6OzM0PQKNior+6Dw9Pzo5QD8C9Dw9Pzo5QD8AAwBz/7wEYgSHABMAGwAjAEtAKRcfHBQUChwAABIPBQgKBiQlFh4hGQ0ZRlkPEggFBAMQDRADIUZZBgMWAD/GKwAYP8YSFzkrERIAOTkREgEXOREzETMREjk5MTABEAAjIicHJzcmERAAMzIXNxcHFgUUFwEmIyIGBTQnARYzMjYEYv7y7ppwVHJegQEM7pp0VHVhf/y9NQHRS3KjpgKXM/4vR3GjqQIl/vT+00V1ToOYAQABDAErTHdMhZj5q2YChjXW1KRk/X0z2wD//wCk/+wEOQYhAiYAWAAAAQYAQ8QAAAizARYRJgArNf//AKT/7AQ5BiECJgBYAAABBgB2cQAACLMBHhEmACs1//8ApP/sBDkGIQImAFgAAAEGAUsSAAAIswEjESYAKzX//wCk/+wEOQXTAiYAWAAAAQYAaiEAAAq0AgEqESYAKzU1//8AAv4UBAYGIQImAFwAAAEGAHYSAAAIswEfESYAKzUAAgCw/hQEdQYUABYAIgA+QB8gBhsUEBARBhEkIxIAERsMFgkDCR5GWQkWAxdGWQMQAD8rABg/KxESADk5GD8/ERIBOTkRMxEzMxEzMTABNjYzMhIREAIjIicjFxYVESMRMxEUByUiBgcVFBYzIBE0JgFYQqpq1/Dx1t56DAQIpqYGAUiomAKaqgEvlAO0WU/+1P71/vT+06EiTT/+NQgA/i40Whu4ySnnxwGw19H//wAC/hQEBgXTAiYAXAAAAQYAarUAAAq0AgErESYAKzU1//8AAAAABRAGtAImACQAAAEHAU0APwFSAAizAhIFJgArNf//AF7/7APNBWICJgBEAAABBgFN9QAACLMCKBEmACs1//8AAAAABRAHNwImACQAAAEHAU4AKwFSAAizAg8FJgArNf//AF7/7APNBeUCJgBEAAABBgFO5AAACLMCJREmACs1//8AAP5CBREFvAImACQAAAAHAVEDoAAA//8AXv5CBAAEWgImAEQAAAAHAVECjwAA//8Aff/sBM8HcwImACYAAAEHAHYBCAFSAAizASAFJgArNf//AHP/7AOLBiECJgBGAAABBgB2RAAACLMBIBEmACs1//8Aff/sBM8HcwImACYAAAEHAUsArAFSAAizASUFJgArNf//AHP/7AOLBiECJgBGAAABBgFL1AAACLMBJREmACs1//8Aff/sBM8HMQImACYAAAEHAU8CGwFSAAizASAFJgArNf//AHP/7AOLBd8CJgBGAAABBwFPAVAAAAAIswEgESYAKzX//wB9/+wEzwdzAiYAJgAAAQcBTADBAVIACLMBIgUmACs1//8Ac//sA6EGIQImAEYAAAEGAUzzAAAIswEiESYAKzX//wDJAAAFWAdzAiYAJwAAAQcBTABYAVIACLMCHQUmACs1//8Ac//sBYEGFAImAEcAAAEHAjgDDAAAAAeyAiMAAD81AP//AC8AAAVIBbYCBgCSAAAAAgBz/+wE0wYUABoAJwBkQDclBhIOAB4eFRkWGRAGBCgpGhUYEBEQR1kVDxEfES8RAwkDEREJEwABDAMJCSJGWQkQAxtGWQMWAD8rABg/KxESADk5GD8SOS9fXl0zKxEAMxg/ERIBFzkRMzMRMzMzETMxMCUjBiMiAhEQEjMyFzMmNTUhNSE1MxUzFSMRIyUyNjU1NCYjIgYVFBYDmglz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AAizAhkRJgArNf//AH3/7AW+B3MCJgAyAAABBwFTARQBUgAKtAMCKwUmACs1Nf//AHP/7ARiBiECJgBSAAABBgFTWgAACrQDAiwRJgArNTUAAgB9/+wG5wXNABQAHwBTQC4YBg8TEx0ADREdBgUgIQ8SSVkPDwALCw5JWQsDCRVJWQkEAxtJWQMSABNJWQASAD8rABg/KwAYPysAGD8rERIAORgvKxESARc5ETMRMxEzMTAhIQYjIAAREAAhMhchFSERIRUhESEBIgAREAAzMjcRJgbn/QBmXP65/p8BXAFAZloDDv2zAif92QJN/ET5/v8BAfdwV1cUAYkBagFoAYYXl/4plv3mBJ3+z/7Z/tf+zSEEdR4AAwBx/+wHHwRaAB4AKgAxAFVALR8IDgIWFiUvFRUcJQgEMjMrKAsoRlkuFkZZAgUOCy4uBRELEBgiBSJGWQAFFgA/MysRADMYPzMSOS8SORI5KysRADMREgEXOREzETMSOTkRMzEwBSAnBgYjIgAREAAzMhYXNjYzMhIVFSESITI2NxUGBgEUFjMyNjU0JiMiBiUiBgchNCYFlv7bfT7Rid/+9AEG64PNPjrAfsnu/ScIAUpeoVdYmPshmKejmZulppUER3+RDAIghBTrdHcBMQEIAQkBLHdycHn+9+Jp/ncjJ5QnIAI509vV0d3V2Niknp6k//8AyQAABM8HcwImADUAAAEHAHYAeQFSAAizAh8FJgArNf//ALAAAAMnBiECJgBVAAABBgB23AAACLMBGhEmACs1//8Ayf47BM8FtgImADUAAAAGAjl9AP//AGD+OwMnBFwCJgBVAAAABwI5/u8AAP//AMkAAATPB3MCJgA1AAABBwFMABsBUgAIswIhBSYAKzX//wCCAAADJwYhAiYAVQAAAQcBTP92AAAACLMBHBEmACs1//8Aav/sBAIHcwImADYAAAEHAHYAUAFSAAizAS4FJgArNf//AGr/7ANzBiECJgBWAAABBgB26gAACLMBLhEmACs1//8Aav/sBAIHcwImADYAAAEHAUv/6gFSAAizATMFJgArNf//AGr/7ANzBiECJgBWAAABBgFLlwAACLMBMxEmACs1//8Aav4UBAIFywImADYAAAAHAHoBJwAA//8Aav4UA3MEXAImAFYAAAAHAHoA1QAA//8Aav/sBAIHcwImADYAAAEH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WONZAlkhWUBZdVm3WfxaTVpuWtNbJ1snWydbJ1snWydbJ1snWydbJ1snWydbJ1snXHFczFzdXOVdbF2nXgteHF4tXjleRV5XXoxew17TXuNfQF+XX+BgMWA6YENgTGB6YJlgqmC7YMtg22FOYZlh7WI7Ypti/mM/Y4Bj1mQsZI9k9GVpZeBmjGcwZzhnQGedZ/ZoL2hnaHloi2kBaQ1pgGnzap1rO2vRbDpsfWy/bQNtM21gbYZtrG6QbxtvgW/fcDFwgnDXcUNxe3G0cgZyVXKocvtzB3MTc1BzjHPNdBB0WHSsdOZ1HnVddaJ13XYddnN2xndCd7l3xXfReAJ4NHg8eG94rXjxeTB5cXmueex6MHpzer97C3tDe3p76HxLfMF9LX01fUZ9V32sffx+RH6Hfsx/FX9Vf5Z/2oAegG+AvYDFgNaA5oD4gQmBEYEZgSqBOoGLgdqB7IH9gg+CIYIzgkSCkILaguuC+4MNgx6DMINBg0mDUYNjg3SDhoOXg6iDuIPKg9uD7YP+hBCEIYRMhHeEiYSbhKeEsoS+hMqFEIVWhZSFnIX2hmSGyYcnh4GH1IgriHmIxIkTiWaJsInvii2KioqSip6Kqoq2isKK04rkivaLCIsaiyyLPotQi2KLdIuJi52Lr4vBi9OL5Yv3jAmMG4wtjEKMVoxijG6Mf4yQjKGMsYzDjNWM54z5jQuNHY0vjUGNVo1qjXuNjI2YjaSNsI28jc2N3o3wjgKOFI4mjjiOSo5cjm6Og46XjqiOuI7JjtmO6o77jwyPHI8ojzSPQI9Mj12Pbo9/j4+PoI+wj8GP0o/jj/OP/5ALkBeQI5A0kEWQVpBmkHKQppDhkR2RapHCkfqSMpJ7ks2S9ZMYkzuTRJODk62T7pROlJOU3pTmlQmVEZVulXqV9pYClg6WcZaBlpGWopaylseW2JbplvqXDJcdly6XP5dKl1uXZ5d5l4GXk5ebl62XtZe9l86X2gAAAAEAAAABGdsfPbV9Xw889QAJCAAAAAAAyTUxiwAAAADVK8zV+5r91QmiCGIAAAAJAAIAAAAAAAAEzQDBAAAAAAQUAAACFAAAAiMAmAM1AIUFKwAzBJMAgwaWAGgF1wBxAcUAhQJeAFICXgA9BGoAVgSTAGgB9gA/ApMAVAIhAJgC8AAUBJMAZgSTALwEkwBkBJMAXgSTACsEkwCFBJMAdQSTAF4EkwBoBJMAagIhAJgCIQA/BJMAaASTAHcEkwBoA28AGwcxAHkFEAAABS8AyQUMAH0F1QDJBHMAyQQhAMkF0wB9BecAyQKqAFQCI/9gBOkAyQQnAMkHOQDJBggAyQY7AH0E0QDJBjsAfQTyAMkEZABqBG0AEgXTALoEwwAAB2gAGwSeAAgEewAABJEAUgKiAKYC8AAXAqIAMwRWADEDlv/8BJ4BiQRzAF4E5wCwA88AcwTnAHMEfQBzArYAHQRiACcE6QCwAgYAogIG/5EEMwCwAgYAsAdxALAE6QCwBNUAcwTnALAE5wBzA0QAsAPRAGoC0wAfBOkApAQCAAAGOQAXBDEAJwQIAAIDvgBSAwgAPQRoAe4DCABIBJMAaAIUAAACIwCYBJMAvgSTAD8EkwB7BJMAHwRoAe4EIQB7BJ4BNQaoAGQC1QBGA/oAUgSTAGgCkwBUBqgAZAQA//oDbQB/BJMAaALHADECxwAhBJ4BiQT0ALAFPQBxAiEAmAHRACUCxwBMAwAAQgP6AFAGPQBLBj0ALgY9ABoDbwAzBRAAAAUQAAAFEAAABRAAAAUQAAAFEAAABvz//gUMAH0EcwDJBHMAyQRzAMkEcwDJAqoAPAKqAFQCqv//AqoAPAXHAC8GCADJBjsAfQY7AH0GOwB9BjsAfQY7AH0EkwCFBjsAfQXTALoF0wC6BdMAugXTALoEewAABOMAyQT6ALAEcwBeBHMAXgRzAF4EcwBeBHMAXgRzAF4G3QBeA88AcwR9AHMEfQBzBH0AcwR9AHMCBv/aAgYAqQIG/7MCBv/sBMUAcQTpALAE1QBzBNUAcwTVAHME1QBzBNUAcwSTAGgE1QBzBOkApATpAKQE6QCkBOkApAQIAAIE5wCwBAgAAgUQAAAEcwBeBRAAAARzAF4FEAAABHMAXgUMAH0DzwBzBQwAfQPPAHMFDAB9A88AcwUMAH0DzwBzBdUAyQTnAHMFxwAvBOcAcwRzAMkEfQBzBHMAyQR9AHMEcwDJBH0AcwRzAMkEfQBzBHMAyQR9AHMF0wB9BGIAJwXTAH0EYgAnBdMAfQRiACcF0wB9BGIAJwXnAMkE6QCwBecAAATpABQCqv/iAgb/kAKqACoCBv/aAqoAHgIG/8wCqgBUAgYANQKqAFQCBgCwBM0AVAQMAKICI/9gAgb/kQTpAMkEMwCwBCUAsAQnAMkCBgCjBCcAyQIGAFkEJwDJAgYAsAQnAMkCgwCwBC8AHQIX//wGCADJBOkAsAYIAMkE6QCwBggAyQTpALAFcwABBggAyQTpALAGOwB9BNUAcwY7AH0E1QBzBjsAfQTVAHMHYgB9B4kAcQTyAMkDRACwBPIAyQNEAGAE8gDJA0QAggRkAGoD0QBqBGQAagPRAGoEZABqA9EAagRkAGoD0QBqBG0AEgLTAB8EbQASAtMAHwRtABIC0wAfBdMAugTpAKQF0wC6BOkApAXTALoE6QCkBdMAugTpAKQF0wC6BOkApAXTALoE6QCkB2gAGwY5ABcEewAABAgAAgR7AAAEkQBSA74AUgSRAFIDvgBSBJEAUgO+AFICjwCwBJ4AwwUUAAAEcwBeBvz//gbdAF4GOwB9BNUAcwRkAGoD0QBqBLwBDAS8AQwEsgEtBLwBJQIGAKIEngFvAZMAJQS8AQgEngDnBJ4B/ASeARsFEAAAAiEAmATy/9QGff/UA5j/5AaB/+QFhf/UBoH/5AK2/+kFEAAABS8AyQQpAMkEkwAnBHMAyQSRAFIF5wDJBjsAfQKqAFQE6QDJBNMAAAc5AMkGCADJBG0ASAY7AH0F1QDJBNEAyQSJAEoEbQASBHsAAAZiAGoEngAIBl4AbQZCAFACqgA8BHsAAATjAHMDzQBaBOkAsAK2AKgE3wCkBOMAcwUGALAEGQAKBKQAcQPNAFoD3QBzBOkAsAS8AHMCtgCoBCUAsARG//IE9ACwBFYAAAPNAHEE1QBzBTMAGQTVAKYD2wBzBOcAcwPJABIE3wCkBb4AcwRe/+wGBgCkBi8AcwK2AAkE3wCkBNUAcwTfAKQGLwBzBHMAyQXfABIEKQDJBR0AfQRkAGoCqgBUAqoAPAIj/2AHbwAAB6AAyQXfABIE5QDJBPgAGwXVAMkFEAAABOcAyQUvAMkEKQDJBXcADgRzAMkGwQACBKYASgYZAMsGGQDLBOUAyQWiAAAHOQDJBecAyQY7AH0F1QDJBNEAyQUMAH0EbQASBPgAGwZiAGoEngAIBeUAyQWPAKoIQgDJCEQAyQWBABIG0wDJBSUAyQUKAD0IZgDJBRcAMwRzAF4ExQB3BI0AsANtALAEkwApBH0AcwXjAAQD3QBEBRIAsAUSALAEJwCwBJEAEAXhALAFEgCwBNUAcwT4ALAE5wCw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AAsAOAHsAAMAAQQJAAwAXAIkAAMAAQQJAA0AXAKAAAMAAQQJAA4AVALcAEQAaQBnAGkAdABpAHoAZQBkACAAZABhAHQAYQAgAGMAbwBwAHkAcgBpAGcAaAB0ACAAqQAgADIAMAAxADAALQAyADAAMQAxACwAIABHAG8AbwBnAGwAZQAgAEMAbwByAHAAbwByAGEAdABpAG8AbgAuAE8AcABlAG4AIABTAGEAbgBzAFIAZQBnAHUAbABhAHIAMQAuADEAMAA7ADEAQQBTAEMAOwBPAHAAZQBuAFMAYQBuAHMALQBSAGUAZwB1AGwAYQByAE8AcABlAG4AIABTAGEAbgBzACAAUgBlAGcAdQBsAGEAcgBWAGUAcgBzAGkAbwBuACAAMQAuADEAMABPAHAAZQBuAFMAYQBuAHMALQBSAGUAZwB1AGwAYQByAE8AcABlAG4AIABTAGEAbgBzACAAaQBzACAAYQAgAHQAcgBhAGQAZQBtAGEAcgBrACAAbwBmACAARwBvAG8AZwBsAGUAIABhAG4AZAAgAG0AYQB5ACAAYgBlACAAcgBlAGcAaQBzAHQAZQByAGUAZAAgAGkAbgAgAGMAZQByAHQAYQBpAG4AIABqAHUAcgBpAHMAZABpAGMAdABpAG8AbgBzAC4AQQBzAGMAZQBuAGQAZQByACAAQwBvAHIAcABvAHIAYQB0AGkAbwBuAGgAdAB0AHAAOgAvAC8AdwB3AHcALgBhAHMAYwBlAG4AZABlAHIAYwBvAHIAcAAuAGMAbwBtAC8AaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGEAcwBjAGUAbgBkAGUAcgBjAG8AcgBwAC4AYwBvAG0ALwB0AHkAcABlAGQAZQBzAGkAZwBuAGUAcgBzAC4AaAB0AG0AbABMAGkAYwBlAG4AcwBlAGQAIAB1AG4AZABlAHIAIAB0AGgAZQAgAEEAcABhAGMAaABlACAATABpAGMAZQBuAHMAZQAsACAAVgBlAHIAcwBpAG8AbgAgADIALgAwAGgAdAB0AHAAOgAvAC8AdwB3AHcALgBhAHAAYQBjAGgAZQAuAG8AcgBnAC8AbABpAGMAZQBuAHMAZQBzAC8ATABJAEMARQBOAFMARQAtADIALgAwAAAAAgAAAAAAAP9mAGYAAAAAAAAAAAAAAAAAAAAAAAAAAAOqAAABAgACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4AHwAgACEAIgAjACQAJQAmACcAKAApACoAKwEDAC0ALgAvADAAMQAyADMANAA1ADYANwA4ADkAOgA7ADwAPQA+AD8AQABBAEIAQwBEAEUARgBHAEgASQBKAEsATABNAE4ATwBQAFEAUgBTAFQAVQBWAFcAWABZAFoAWwBcAF0AXgBfAGAAYQCsAKMAhACFAL0AlgDoAIYAjgCLAJ0AqQCkAQQAigEFAIMAkwDyAPMAjQCXAIgAwwDeAPEAngCqAPUA9AD2AKIArQDJAMcArgBiAGMAkABkAMsAZQDIAMoBBgEHAQgBCQDpAGYA0wDQANEArwBnAPAAkQDWANQA1QBoAOsA7QCJAGoAaQBrAG0AbABuAKAAbwBxAHAAcgBzAHUAdAB2AHcA6gB4AHoAeQB7AH0AfAC4AKEAfwB+AIAAgQDsAO4AugEKAQsBDAENAQ4BDwD9AP4BEAERARIBEwD/AQABFAEVARYBAQEXARgBGQEaARsBHAEdAR4BHwEgASEBIgD4APkBIwEkASUBJgEnASgBKQEqASsBLAEtAS4BLwEwATEBMgEzANcBNAE1ATYBNwE4ATkBOgE7ATwBPQE+AT8BQAFBAUIA4gDjAUMBRAFFAUYBRwFIAUkBSgFLAUwBTQFOAU8BUAFRALAAsQFSAVMBVAFVAVYBVwFYAVkBWgFbAPsA/ADkAOUBXAFdAV4BXwFgAWEBYgFjAWQBZQFmAWcBaAFpAWoBawFsAW0BbgFvAXABcQC7AXIBcwF0AXUA5gDnAXYApgF3AXgBeQF6AXsBfAF9AX4A2ADhANoA2wDcAN0A4ADZAN8BfwGAAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcAmwG4AbkBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgHHAcgByQHKAcsBzAHNAc4BzwHQAdEB0gHTAdQB1QHWAdcB2AHZAdoB2wHcAd0B3gHfAeAB4QHiAeMB5AHlAeYB5wHoAekB6gHrAewB7QHuAe8B8AHxAfIB8wH0AfUB9gH3AfgB+QH6AfsB/AH9Af4B/wIAAgECAgIDAgQCBQIGAgcCCAIJAgoCCwIMAg0CDgIPAhACEQISAhMCFAIVAhYCFwIYAhkCGgIbAhwCHQIeAh8CIAIhAiICIwIkAiUCJgInAigCKQIqAisAsgCzAiwCLQC2ALcAxAIuALQAtQDFAIIAwgCHAKsAxgIvAjAAvgC/AjEAvAIyAPcCMwI0AjUCNgI3AjgAjACfAjkCOgI7AjwCPQCYAKgAmgCZAO8ApQCSAJwApwCPAJQAlQC5Aj4CPwJAAkECQgJDAkQCRQJGAkcCSAJJAkoCSwJMAk0CTgJPAlACUQJSAlMCVAJVAlYCVwJYAlkCWgJbAlwCXQJeAl8CYAJhAmICYwJkAmUCZgJnAmgCaQJqAmsCbAJtAm4CbwJwAnECcgJzAnQCdQJ2AncCeAJ5AnoCewJ8An0CfgJ/AoACgQKCAoMChAKFAoYChwKIAokCigKLAowCjQKOAo8CkAKRApICkwKUApUClgKXApgCmQKaApsCnAKdAp4CnwKgAqECogKjAqQCpQKmAqcCqAKpAqoCqwKsAq0CrgKvArACsQKyArMCtAK1ArYCtwK4ArkCugK7ArwCvQK+Ar8CwALBAsICwwLEAsUCxgLHAsgCyQLKAssCzALNAs4CzwLQAtEC0gLTAtQC1QLWAtcC2ALZAtoC2wLcAt0C3gLfAuAC4QLiAuMC5ALlAuYC5wLoAukC6gLrAuwC7QLuAu8C8ALxAvIC8wL0AvUC9gL3AvgC+QL6AvsC/AL9Av4C/wMAAwEDAgMDAwQDBQMGAwcDCAMJAwoDCwMMAw0DDgMPAxADEQMSAxMDFAMVAxYDFwMYAxkDGgMbAxwDHQMeAx8DIAMhAyIDIwMkAyUDJgMnAygDKQMqAysDLAMtAy4DLwMwAzEDMgMzAzQDNQM2AzcDOAM5AzoDOwM8Az0DPgM/A0ADQQNCA0MDRANFA0YDRwNIA0kDSgNLA0wDTQNOA08DUANRA1IDUwNUA1UDVgNXA1gDWQNaA1sDXANdA14DXwNgA2EDYgNjA2QDZQNmA2cDaANpA2oDawNsA20DbgNvA3ADcQNyA3MDdAN1A3YDdwN4A3kDegN7A3wDfQN+A38DgAOBA4IDgwOEA4UDhgOHA4gDiQOKA4sDjAONA44DjwOQA5EDkgOTA5QDlQOWA5cDmAOZA5oDmwOcA50DngOfACwAzwDMAM0AzgOgA6EDogOjAPoDpAOlA6YDpwOoA6kDqgOrA6wDrQRudWxsBUkuYWx0B3VuaTAwQUQJb3ZlcnNjb3JlCklncmF2ZS5hbHQKSWFjdXRlLmFsdA9JY2lyY3VtZmxleC5hbHQNSWRpZXJlc2lzLmFsdAdBbWFjcm9uB2FtYWNyb24GQWJyZXZlBmFicmV2ZQdBb2dvbmVrB2FvZ29uZWsLQ2NpcmN1bWZsZXgLY2NpcmN1bWZsZXgEQ2RvdARjZG90BkRjYXJvbgZkY2Fyb24GRGNyb2F0B0VtYWNyb24HZW1hY3JvbgZFYnJldmUGZWJyZXZlCkVkb3RhY2NlbnQKZWRvdGFjY2VudAdFb2dvbmVrB2VvZ29uZWsGRWNhcm9uBmVjYXJvbgtHY2lyY3VtZmxleAtnY2lyY3VtZmxleARHZG90BGdkb3QMR2NvbW1hYWNjZW50DGdjb21tYWFjY2VudAtIY2lyY3VtZmxleAtoY2lyY3VtZmxleARIYmFyBGhiYXIKSXRpbGRlLmFsdAZpdGlsZGULSW1hY3Jvbi5hbHQHaW1hY3JvbgpJYnJldmUuYWx0BmlicmV2ZQtJb2dvbmVrLmFsdAdpb2dvbmVrDklkb3RhY2NlbnQuYWx0BklKLmFsdAJpagtKY2lyY3VtZmxleAtqY2lyY3VtZmxleAxLY29tbWFhY2NlbnQMa2NvbW1hYWNjZW50DGtncmVlbmxhbmRpYwZMYWN1dGUGbGFjdXRlDExjb21tYWFjY2VudAxsY29tbWFhY2NlbnQGTGNhcm9uBmxjYXJvbgRMZG90BGxkb3QGTmFjdXRlBm5hY3V0ZQxOY29tbWFhY2NlbnQMbmNvbW1hYWNjZW50Bk5jYXJvbgZuY2Fyb24LbmFwb3N0cm9waGUDRW5nA2VuZwdPbWFjcm9uB29tYWNyb24GT2JyZXZlBm9icmV2ZQ1PaHVuZ2FydW1sYXV0DW9odW5nYXJ1bWxhdXQGUmFjdXRlBnJhY3V0ZQxSY29tbWFhY2NlbnQMcmNvbW1hYWNjZW50BlJjYXJvbgZyY2Fyb24GU2FjdXRlBnNhY3V0ZQtTY2lyY3VtZmxleAtzY2lyY3VtZmxleAxUY29tbWFhY2NlbnQMdGNvbW1hYWNjZW50BlRjYXJvbgZ0Y2Fyb24EVGJhcgR0YmFyBlV0aWxkZQZ1dGlsZGUHVW1hY3Jvbgd1bWFjcm9uBlVicmV2ZQZ1YnJldmUFVXJpbmcFdXJpbmcNVWh1bmdhcnVtbGF1dA11aHVuZ2FydW1sYXV0B1VvZ29uZWsHdW9nb25lawtXY2lyY3VtZmxleAt3Y2lyY3VtZmxleAtZY2lyY3VtZmxleAt5Y2lyY3VtZmxleAZaYWN1dGUGemFjdXRlClpkb3RhY2NlbnQKemRvdGFjY2VudAVsb25ncwpBcmluZ2FjdXRlCmFyaW5nYWN1dGUHQUVhY3V0ZQdhZWFjdXRlC09zbGFzaGFjdXRlC29zbGFzaGFjdXRlDFNjb21tYWFjY2VudAxzY29tbWFhY2NlbnQFdG9ub3MNZGllcmVzaXN0b25vcwpBbHBoYXRvbm9zCWFub3RlbGVpYQxFcHNpbG9udG9ub3MIRXRhdG9ub3MNSW90YXRvbm9zLmFsdAxPbWljcm9udG9ub3MMVXBzaWxvbnRvbm9zCk9tZWdhdG9ub3MRaW90YWRpZXJlc2lzdG9ub3MFQWxwaGEEQmV0YQVHYW1tYQd1bmkwMzk0B0Vwc2lsb24EWmV0YQNFdGEFVGhldGEISW90YS5hbHQFS2FwcGEGTGFtYmRhAk11Ak51AlhpB09taWNyb24CUGkDUmhvBVNpZ21hA1RhdQdVcHNpbG9uA1BoaQNDaGkDUHNpB3VuaTAzQTkQSW90YWRpZXJlc2lzLmFsdA9VcHNpbG9uZGllcmVzaXMKYWxwaGF0b25vcwxlcHNpbG9udG9ub3MIZXRhdG9ub3MJaW90YXRvbm9zFHVwc2lsb25kaWVyZXNpc3Rvbm9zBWFscGhhBGJldGEFZ2FtbWEFZGVsdGEHZXBzaWxvbgR6ZXRhA2V0YQV0aGV0YQRpb3RhBWthcHBhBmxhbWJkYQd1bmkwM0JDAm51AnhpB29taWNyb24DcmhvBnNpZ21hMQVzaWdtYQN0YXUHdXBzaWxvbgNwaGkDY2hpA3BzaQVvbWVnYQxpb3RhZGllcmVzaXMPdXBzaWxvbmRpZXJlc2lzDG9taWNyb250b25vcwx1cHNpbG9udG9ub3MKb21lZ2F0b25vcwlhZmlpMTAwMjMJYWZpaTEwMDUxCWFmaWkxMDA1MglhZmlpMTAwNTMJYWZpaTEwMDU0DWFmaWkxMDA1NS5hbHQNYWZpaTEwMDU2LmFsdAlhZmlpMTAwNTcJYWZpaTEwMDU4CWFmaWkxMDA1OQlhZmlpMTAwNjAJYWZpaTEwMDYxCWFmaWkxMDA2MglhZmlpMTAxNDUJYWZpaTEwMDE3CWFmaWkxMDAxOAlhZmlpMTAwMTkJYWZpaTEwMDIwCWFmaWkxMDAyMQlhZmlpMTAwMjIJYWZpaTEwMDI0CWFmaWkxMDAyNQlhZmlpMTAwMjYJYWZpaTEwMDI3CWFmaWkxMDAyOAlhZmlpMTAwMjkJYWZpaTEwMDMwCWFmaWkxMDAzMQlhZmlpMTAwMzIJYWZpaTEwMDMzCWFmaWkxMDAzNAlhZmlpMTAwMzUJYWZpaTEwMDM2CWFmaWkxMDAzNwlhZmlpMTAwMzgJYWZpaTEwMDM5CWFmaWkxMDA0MAlhZmlpMTAwNDEJYWZpaTEwMDQyCWFmaWkxMDA0MwlhZmlpMTAwNDQJYWZpaTEwMDQ1CWFmaWkxMDA0NglhZmlpMTAwNDcJYWZpaTEwMDQ4CWFmaWkxMDA0OQlhZmlpMTAwNjUJYWZpaTEwMDY2CWFmaWkxMDA2NwlhZmlpMTAwNjgJYWZpaTEwMDY5CWFmaWkxMDA3MAlhZmlpMTAwNzIJYWZpaTEwMDczCWFmaWkxMDA3NAlhZmlpMTAwNzUJYWZpaTEwMDc2CWFmaWkxMDA3NwlhZmlpMTAwNzgJYWZpaTEwMDc5CWFmaWkxMDA4MAlhZmlpMTAwODEJYWZpaTEwMDgyCWFmaWkxMDA4MwlhZmlpMTAwODQJYWZpaTEwMDg1CWFmaWkxMDA4NglhZmlpMTAwODcJYWZpaTEwMDg4CWFmaWkxMDA4OQlhZmlpMTAwOTAJYWZpaTEwMDkxCWFmaWkxMDA5MglhZmlpMTAwOTMJYWZpaTEwMDk0CWFmaWkxMDA5NQlhZmlpMTAwOTYJYWZpaTEwMDk3CWFmaWkxMDA3MQlhZmlpMTAwOTkJYWZpaTEwMTAwCWFmaWkxMDEwMQlhZmlpMTAxMDIJYWZpaTEwMTAzCWFmaWkxMDEwNAlhZmlpMTAxMDUJYWZpaTEwMTA2CWFmaWkxMDEwNwlhZmlpMTAxMDgJYWZpaTEwMTA5CWFmaWkxMDExMAlhZmlpMTAxOTMJYWZpaTEwMDUwCWFmaWkxMDA5OAZXZ3JhdmUGd2dyYXZlBldhY3V0ZQZ3YWN1dGUJV2RpZXJlc2lzCXdkaWVyZXNpcwZZZ3JhdmUGeWdyYXZlCWFmaWkwMDIwOA11bmRlcnNjb3JlZGJsDXF1b3RlcmV2ZXJzZWQGbWludXRlBnNlY29uZAlleGNsYW1kYmwJbnN1cGVyaW9yCWFmaWkwODk0MQZwZXNldGEERXVybwlhZmlpNjEyNDgJYWZpaTYxMjg5CWFmaWk2MTM1Mgllc3RpbWF0ZWQJb25lZWlnaHRoDHRocmVlZWlnaHRocwtmaXZlZWlnaHRocwxzZXZlbmVpZ2h0aHMHdW5pRkIwMQd1bmlGQjAyDWN5cmlsbGljYnJldmUIZG90bGVzc2oQY2Fyb25jb21tYWFjY2VudAtjb21tYWFjY2VudBFjb21tYWFjY2VudHJvdGF0ZQx6ZXJvc3VwZXJpb3IMZm91cnN1cGVyaW9yDGZpdmVzdXBlcmlvcgtzaXhzdXBlcmlvcg1zZXZlbnN1cGVyaW9yDWVpZ2h0c3VwZXJpb3IMbmluZXN1cGVyaW9yB3VuaTIwMDAHdW5pMjAwMQd1bmkyMDAyB3VuaTIwMDMHdW5pMjAwNAd1bmkyMDA1B3VuaTIwMDYHdW5pMjAwNwd1bmkyMDA4B3VuaTIwMDkHdW5pMjAwQQd1bmkyMDBCB3VuaUZFRkYHdW5pRkZGQwd1bmlGRkZEB3VuaTAxRjAHdW5pMDJCQwd1bmkwM0QxB3VuaTAzRDIHdW5pMDNENgd1bmkxRTNFB3VuaTFFM0YHdW5pMUUwMAd1bmkxRTAxB3VuaTFGNEQHdW5pMDJGMwlkYXNpYW94aWEHdW5pRkIwMwd1bmlGQjA0BU9ob3JuBW9ob3JuBVVob3JuBXVob3JuB3VuaTAzMDAHdW5pMDMwMQd1bmkwMzAzBGhvb2sIZG90YmVsb3cHdW5pMDQwMAd1bmkwNDBEB3VuaTA0NTAHdW5pMDQ1RAd1bmkwNDYwB3VuaTA0NjEHdW5pMDQ2Mgd1bmkwNDYzB3VuaTA0NjQHdW5pMDQ2NQd1bmkwNDY2B3VuaTA0NjcHdW5pMDQ2OAd1bmkwNDY5B3VuaTA0NkEHdW5pMDQ2Qgd1bmkwNDZDB3VuaTA0NkQHdW5pMDQ2RQd1bmkwNDZGB3VuaTA0NzAHdW5pMDQ3MQd1bmkwNDcyB3VuaTA0NzMHdW5pMDQ3NAd1bmkwNDc1B3VuaTA0NzYHdW5pMDQ3Nwd1bmkwNDc4B3VuaTA0NzkHdW5pMDQ3QQd1bmkwNDdCB3VuaTA0N0MHdW5pMDQ3RAd1bmkwNDdFB3VuaTA0N0YHdW5pMDQ4MAd1bmkwNDgxB3VuaTA0ODIHdW5pMDQ4Mwd1bmkwNDg0B3VuaTA0ODUHdW5pMDQ4Ngd1bmkwNDg4B3VuaTA0ODkHdW5pMDQ4QQd1bmkwNDhCB3VuaTA0OEMHdW5pMDQ4RAd1bmkwNDhFB3VuaTA0OEYHdW5pMDQ5Mgd1bmkwNDkzB3VuaTA0OTQHdW5pMDQ5NQd1bmkwNDk2B3VuaTA0OTcHdW5pMDQ5OAd1bmkwNDk5B3VuaTA0OUEHdW5pMDQ5Qgd1bmkwNDlDB3VuaTA0OUQHdW5pMDQ5RQd1bmkwNDlGB3VuaTA0QTAHdW5pMDRBMQd1bmkwNEEyB3VuaTA0QTMHdW5pMDRBNAd1bmkwNEE1B3VuaTA0QTYHdW5pMDRBNwd1bmkwNEE4B3VuaTA0QTkHdW5pMDRBQQd1bmkwNEFCB3VuaTA0QUMHdW5pMDRBRAd1bmkwNEFFB3VuaTA0QUYHdW5pMDRCMAd1bmkwNEIxB3VuaTA0QjIHdW5pMDRCMwd1bmkwNEI0B3VuaTA0QjUHdW5pMDRCNgd1bmkwNEI3B3VuaTA0QjgHdW5pMDRCOQd1bmkwNEJBB3VuaTA0QkIHdW5pMDRCQwd1bmkwNEJEB3VuaTA0QkUHdW5pMDRCRgt1bmkwNEMwLmFsdAd1bmkwNEMxB3VuaTA0QzIHdW5pMDRDMwd1bmkwNEM0B3VuaTA0QzUHdW5pMDRDNgd1bmkwNEM3B3VuaTA0QzgHdW5pMDRDOQd1bmkwNENBB3VuaTA0Q0IHdW5pMDRDQwd1bmkwNENEB3VuaTA0Q0ULdW5pMDRDRi5hbHQHdW5pMDREMAd1bmkwNEQxB3VuaTA0RDIHdW5pMDREMwd1bmkwNEQ0B3VuaTA0RDUHdW5pMDRENgd1bmkwNEQ3B3VuaTA0RDgHdW5pMDREOQd1bmkwNERBB3VuaTA0REIHdW5pMDREQwd1bmkwNEREB3VuaTA0REUHdW5pMDRERgd1bmkwNEUwB3VuaTA0RTEHdW5pMDRFMgd1bmkwNEUzB3VuaTA0RTQHdW5pMDRFNQd1bmkwNEU2B3VuaTA0RTcHdW5pMDRFOAd1bmkwNEU5B3VuaTA0RUEHdW5pMDRFQgd1bmkwNEVDB3VuaTA0RUQHdW5pMDRFRQd1bmkwNEVGB3VuaTA0RjAHdW5pMDRGMQd1bmkwNEYyB3VuaTA0RjMHdW5pMDRGNAd1bmkwNEY1B3VuaTA0RjYHdW5pMDRGNwd1bmkwNEY4B3VuaTA0RjkHdW5pMDRGQQd1bmkwNEZCB3VuaTA0RkMHdW5pMDRGRAd1bmkwNEZFB3VuaTA0RkYHdW5pMDUwMAd1bmkwNTAxB3VuaTA1MDIHdW5pMDUwMwd1bmkwNTA0B3VuaTA1MDUHdW5pMDUwNgd1bmkwNTA3B3VuaTA1MDgHdW5pMDUwOQd1bmkwNTBBB3VuaTA1MEIHdW5pMDUwQwd1bmkwNTBEB3VuaTA1MEUHdW5pMDUwRgd1bmkwNTEwB3VuaTA1MTEHdW5pMDUxMgd1bmkwNTEzB3VuaTFFQTAHdW5pMUVBMQd1bmkxRUEyB3VuaTFFQTMHdW5pMUVBNAd1bmkxRUE1B3VuaTFFQTYHdW5pMUVBNwd1bmkxRUE4B3VuaTFFQTkHdW5pMUVBQQd1bmkxRUFCB3VuaTFFQUMHdW5pMUVBRAd1bmkxRUFFB3VuaTFFQUYHdW5pMUVCMAd1bmkxRUIxB3VuaTFFQjIHdW5pMUVCMwd1bmkxRUI0B3VuaTFFQjUHdW5pMUVCNgd1bmkxRUI3B3VuaTFFQjgHdW5pMUVCOQd1bmkxRUJBB3VuaTFFQkIHdW5pMUVCQwd1bmkxRUJEB3VuaTFFQkUHdW5pMUVCRgd1bmkxRUMwB3VuaTFFQzEHdW5pMUVDMgd1bmkxRUMzB3VuaTFFQzQHdW5pMUVDNQd1bmkxRUM2B3VuaTFFQzcLdW5pMUVDOC5hbHQHdW5pMUVDOQt1bmkxRUNBLmFsdAd1bmkxRUNCB3VuaTFFQ0MHdW5pMUVDRAd1bmkxRUNFB3VuaTFFQ0YHdW5pMUVEMAd1bmkxRUQxB3VuaTFFRDIHdW5pMUVEMwd1bmkxRUQ0B3VuaTFFRDUHdW5pMUVENgd1bmkxRUQ3B3VuaTFFRDgHdW5pMUVEOQd1bmkxRURBB3VuaTFFREIHdW5pMUVEQwd1bmkxRUREB3VuaTFFREUHdW5pMUVERgd1bmkxRUUwB3VuaTFFRTEHdW5pMUVFMgd1bmkxRUUzB3VuaTFFRTQHdW5pMUVFNQd1bmkxRUU2B3VuaTFFRTcHdW5pMUVFOAd1bmkxRUU5B3VuaTFFRUEHdW5pMUVFQgd1bmkxRUVDB3VuaTFFRUQHdW5pMUVFRQd1bmkxRUVGB3VuaTFFRjAHdW5pMUVGMQd1bmkxRUY0B3VuaTFFRjUHdW5pMUVGNgd1bmkxRUY3B3VuaTFFRjgHdW5pMUVGOQd1bmkyMEFCB3VuaTAzMEYTY2lyY3VtZmxleGFjdXRlY29tYhNjaXJjdW1mbGV4Z3JhdmVjb21iEmNpcmN1bWZsZXhob29rY29tYhNjaXJjdW1mbGV4dGlsZGVjb21iDmJyZXZlYWN1dGVjb21iDmJyZXZlZ3JhdmVjb21iDWJyZXZlaG9va2NvbWIOYnJldmV0aWxkZWNvbWIQY3lyaWxsaWNob29rbGVmdBFjeXJpbGxpY2JpZ2hvb2tVQxFjeXJpbGxpY2JpZ2hvb2tMQwhvbmUucG51bQd6ZXJvLm9zBm9uZS5vcwZ0d28ub3MIdGhyZWUub3MHZm91ci5vcwdmaXZlLm9zBnNpeC5vcwhzZXZlbi5vcwhlaWdodC5vcwduaW5lLm9zAmZmB3VuaTIxMjAIVGNlZGlsbGEIdGNlZGlsbGEFZy5hbHQPZ2NpcmN1bWZsZXguYWx0CmdicmV2ZS5hbHQIZ2RvdC5hbHQQZ2NvbW1hYWNjZW50LmFsdAZJdGlsZGUHSW1hY3JvbgZJYnJldmUHSW9nb25lawJJSglJb3RhdG9ub3MESW90YQxJb3RhZGllcmVzaXMJYWZpaTEwMDU1CWFmaWkxMDA1Ngd1bmkwNEMwB3VuaTA0Q0YHdW5pMUVDOAd1bmkxRUNBAAABAAMACAAKAA0AB///AA8AAQAAAAwAAAAAAAAAAgAFAAACNQABAjcCNwABAjsCWwABAl0DdgABA4IDqQABAAAAAQAAAAoADAAOAAAAAAAAAAEAAAAKAG4BWgABbGF0bgAIABAAAk1PTCAAKFJPTSAAQgAA//8ACQADAAgACwAAAA4AEQAUABcAGgAA//8ACgAEAAYACQAMAAEADwASABUAGAAbAAD//wAKAAUABwAKAA0AAgAQABMAFgAZABwAHWxpZ2EAsGxpZ2EAsGxpZ2EAsGxudW0AtmxudW0AtmxudW0AtmxvY2wAvGxvY2wAvG9udW0Awm9udW0Awm9udW0AwnBudW0AynBudW0AynBudW0AynNhbHQA0HNhbHQA0HNhbHQA0HNzMDEA0HNzMDEA0HNzMDEA0HNzMDIA2HNzMDIA2HNzMDIA2HNzMDMA3nNzMDMA3nNzMDMA3nRudW0A5HRudW0A5HRudW0A5AAAAAEACQAAAAEABwAAAAEACAAAAAIAAgADAAAAAQAEAAAAAgAAAAEAAAABAAAAAAABAAEAAAACAAUABgAKABYAPAB8AJQAzADgAO4BAgEuAVAAAQAAAAEACAACABAABQORA5IDkwOUA5UAAQAFAEoA3wDhAOMA5QABAAAAAQAIAAIALgAUACwAjgCPAJAAkQDqAOwA7gDwAPIA9AFaAWcBdwGhAaICyQLYA0UDRwACAAEDlgOpAAAAAQAAAAEACAABAAYDcAACAAEAEwAcAAAAAQAAAAEACAACABoACgODA4UDhgOHA4gDiQOKA4sDjAOEAAIAAwATABMAAAAVABwAAQOCA4IACQABAAAAAQAIAAEABgNuAAEAAQAUAAEAAAABAAgAAQA8/JAAAQAAAAEACAABAAb8kgABAAEDggABAAAAAQAIAAIAGgAKABMDggAVABYAFwAYABkAGgAbABwAAgABA4MDjAAAAAEAAAABAAgAAgAOAAQDjwOQASABIQABAAQBJAElAUkBSgAEAAAAAQAIAAEANgABAAgABQAMABQAHAAiACgCXgADAEkATwJdAAMASQBMA40AAgBJAjUAAgBPAjQAAgBMAAEAAQBJAAA=") format("truetype"); + font-weight: normal; + font-style: normal; +} +/************************************************* +* --- Reset (used for all w2ui widgets) +* --- The reset is needed to coexist with other CSS +* --- on the same page (for example bootstrap) +*/ +.w2ui-reset { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; + font-family: OpenSans; + font-size: 1.2rem; +} +.w2ui-reset * { + line-height: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; + margin: 0px; + padding: 0px; +} +.w2ui-reset table { + max-width: none; + background-color: transparent; + border-collapse: separate; + border-spacing: 0; + border: none; +} +.w2ui-reset table tr th, +.w2ui-reset table tr td { + font-family: OpenSans; + font-size: 1.4rem; +} +.w2ui-reset input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]), +.w2ui-reset select, +.w2ui-reset textarea { + display: inline-block; + width: auto; + height: auto; + vertical-align: baseline; + padding: 6px; + margin: 0; + font-size: 1.2rem; + background-color: #f8fafa; + border: 1px solid #e0e0e0; +} +.w2ui-reset input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]):focus, +.w2ui-reset select:focus, +.w2ui-reset textarea:focus { + background-color: white; +} +.w2ui-reset select { + padding: 5px; + height: 26px; + font-size: 1.2rem; +} +.w2ui-centered { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + text-align: center; + padding: 10px; +} +.w2ui-disabled, +.w2ui-readonly { + background-color: #f1f1f1; + color: #777; +} +input.w2ui-focus:not(button), +textarea.w2ui-focus, +select.w2ui-focus, +div[contenteditable].w2ui-focus { + outline-style: auto; + outline-color: #72B2FF; +} +/************************************************* +* ---- Input Controls ---- +*/ +select.w2ui-input:focus, +div.w2ui-input:focus { + outline-color: #72B2FF; +} +input:not([type=button]):not([type=submit]).w2ui-input, +textarea.w2ui-input { + padding: 6px; + border: 1px solid #e0e0e0; + border-radius: 3px; + color: black; + background-color: #f8fafa; + line-height: normal; +} +input:not([type=button]):not([type=submit]).w2ui-input:focus, +textarea.w2ui-input:focus { + outline-color: #72B2FF; + background-color: white; +} +input:not([type=button]):not([type=submit]).w2ui-input:disabled, +textarea.w2ui-input:disabled, +input:not([type=button]):not([type=submit]).w2ui-input[readonly], +textarea.w2ui-input[readonly] { + background-color: #f1f1f1; + color: #777; +} +select.w2ui-input { + color: black; + padding: 0px 20px 0px 7px; + line-height: 1.8; + border-radius: 3px; + border: 1px solid #e0e0e0; + -webkit-appearance: none; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAALCAQAAACnzwd+AAAAcklEQVR4AcXMsQFBQQDG4P9tAgC0gJYRQJZgKQMwCqCku6vVAAAA+NJHP4KHOk0aV2pRw61n4BBmyOxKQ8I4ehZeuhd3HTx6DQEGZ7sBfr2OOOOj3Yi43kMKs9sZknofOexqZ8npMygwWZTX51CipP+YA1OiZJbYYg9lAAAAAElFTkSuQmCC'), linear-gradient(to bottom, #f8f8f8 20%, #f8f8f8 50%, #f8f8f8 52%, #f8f8f8 100%); + background-size: 17px 6px, 100% 100%; + background-position: right center, left top; + background-repeat: no-repeat, no-repeat; +} +.w2ui-icon-expand:before { + position: relative; + top: 1px; + left: 1px; + content: ' '; + width: 5px; + height: 5px; + border: 2px solid rgba(150, 150, 150, 0.8); + border-bottom: 0; + border-left: 0; + transform: rotateZ(45deg); +} +.w2ui-icon-collapse:before { + position: relative; + top: -1px; + left: 3px; + content: ' '; + width: 5px; + height: 5px; + border: 2px solid rgba(150, 150, 150, 0.8); + border-bottom: 0; + border-left: 0; + transform: rotateZ(135deg); +} +/* On/Off switch */ +input[type="checkbox"].w2ui-toggle { + position: absolute; + opacity: 0; + width: 46px; + height: 22px; + padding: 0px; + margin: 0px; + margin-left: 2px; + /* Knob */ + /* Blue */ + /* Green */ +} +input[type="checkbox"].w2ui-toggle:focus { + box-shadow: 0px 0px 1px 2px #a8cfff; +} +input[type="checkbox"].w2ui-toggle + div { + display: inline-block; + width: 46px; + height: 22px; + border: 1px solid #bbb; + border-radius: 30px; + background-color: #eee; + transition-duration: 0.3s; + transition-property: background-color, box-shadow; + box-shadow: inset 0 0 0 0px rgba(0, 0, 0, 0.4); + margin-left: 2px; + margin-top: 3px; +} +input[type="checkbox"].w2ui-toggle.w2ui-small { + width: 30px; + height: 23px; +} +input[type="checkbox"].w2ui-toggle.w2ui-small + div { + width: 30px; + height: 16px; + margin-top: 5px; +} +input[type="checkbox"].w2ui-toggle:focus + div { + box-shadow: 0px 0px 3px 2px #91baed; +} +input[type="checkbox"].w2ui-toggle:disabled + div { + opacity: 0.3; +} +input[type="checkbox"].w2ui-toggle + div > div { + float: left; + width: 22px; + height: 22px; + border-radius: inherit; + background: #f5f5f5; + transition-duration: 0.3s; + transition-property: transform, background-color, box-shadow; + box-shadow: 0px 0px 1px #323232, 0 0 0 1px rgba(200, 200, 200, 0.6); + pointer-events: none; + margin-top: -1px; + margin-left: -1px; +} +input[type="checkbox"].w2ui-toggle.w2ui-small + div > div { + width: 16px; + height: 16px; +} +input[type="checkbox"].w2ui-toggle:checked + div > div { + transform: translate3d(24px, 0, 0); + background-color: #ffffff; +} +input[type="checkbox"].w2ui-toggle.w2ui-small:checked + div > div { + transform: translate3d(14px, 0, 0); +} +input[type="checkbox"].w2ui-toggle:focus { + outline: none; +} +input[type="checkbox"].w2ui-toggle:checked + div { + border: 1px solid #206FAD; + box-shadow: inset 0 0 0 12px #35A6EB; +} +input[type="checkbox"].w2ui-toggle:checked:focus + div { + box-shadow: 0px 0px 3px 2px #91baed, inset 0 0 0 12px #35A6EB; +} +input[type="checkbox"].w2ui-toggle:checked + div > div { + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3), 0px 0px 0 1px #206FAD; +} +input[type="checkbox"].w2ui-toggle.green:checked + div { + border: 1px solid #00a23f; + box-shadow: inset 0 0 0 12px #54B350; +} +input[type="checkbox"].w2ui-toggle.green:checked:focus + div { + box-shadow: 0px 0px 3px 2px #91baed, inset 0 0 0 12px #54B350; +} +input[type="checkbox"].w2ui-toggle.green:checked + div > div { + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3), 0 0 0 1px #00a23f; +} +input[type=range] { + height: 5px; + -webkit-appearance: none; + margin: 10px 0 0 0; + width: 120px; + border: none !important; + background-color: transparent !important; +} +input[type=range]:focus { + outline: none !important; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + box-shadow: 0px 0px 0px #000000; + background: #E8E8E8; + border-radius: 0px; + border: 0px solid #000000; +} +input[type=range]::-webkit-slider-thumb { + box-shadow: 0px 0px 0px #000000; + border: 1px solid #2290D9; + height: 13px; + width: 13px; + border-radius: 25px; + background: #D0DBF2; + cursor: pointer; + -webkit-appearance: none; + margin-top: -5px; +} +input[type=range]:focus::-webkit-slider-runnable-track { + background: #E8E8E8; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 4px; + cursor: pointer; + box-shadow: 0px 0px 0px #000000; + background: #E8E8E8; + border-radius: 0px; + border: 0px solid #000000; +} +input[type=range]::-moz-range-thumb { + box-shadow: 0px 0px 0px #000000; + border: 1px solid #2290D9; + height: 13px; + width: 13px; + border-radius: 25px; + background: #D0DBF2; + cursor: pointer; +} +input[type=range]::-ms-track { + width: 100%; + height: 4px; + cursor: pointer; + background: transparent; + border-color: transparent; + color: transparent; +} +input[type=range]::-ms-fill-lower { + background: #E8E8E8; + border: 0px solid #000000; + border-radius: 0px; + box-shadow: 0px 0px 0px #000000; +} +input[type=range]::-ms-fill-upper { + background: #E8E8E8; + border: 0px solid #000000; + border-radius: 0px; + box-shadow: 0px 0px 0px #000000; +} +input[type=range]::-ms-thumb { + margin-top: 1px; + box-shadow: 0px 0px 0px #000000; + border: 1px solid #2290D9; + height: 13px; + width: 13px; + border-radius: 25px; + background: #D0DBF2; + cursor: pointer; +} +input[type=range]:focus::-ms-fill-lower { + background: #E8E8E8; +} +input[type=range]:focus::-ms-fill-upper { + background: #E8E8E8; +} +/************************************************* +* ---- Common Classes ---- +*/ +.w2ui-marker { + background-color: rgba(214, 161, 252, 0.5); +} +.w2ui-spinner { + display: inline-block; + background-size: 100%; + background-repeat: no-repeat; + background-image: url(data:image/gif;base64,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); +} +/* common icons */ +.w2ui-icon { + background-repeat: no-repeat; + height: 16px; + width: 16px; + overflow: hidden; + margin: 2px 2px; + display: inline-block; +} +.w2ui-icon.icon-folder { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGrSURBVDjLxZO7ihRBFIa/6u0ZW7GHBUV0UQQTZzd3QdhMQxOfwMRXEANBMNQX0MzAzFAwEzHwARbNFDdwEd31Mj3X7a6uOr9BtzNjYjKBJ6nicP7v3KqcJFaxhBVtZUAK8OHlld2st7Xl3DJPVONP+zEUV4HqL5UDYHr5xvuQAjgl/Qs7TzvOOVAjxjlC+ePSwe6DfbVegLVuT4r14eTr6zvA8xSAoBLzx6pvj4l+DZIezuVkG9fY2H7YRQIMZIBwycmzH1/s3F8AapfIPNF3kQk7+kw9PWBy+IZOdg5Ug3mkAATy/t0usovzGeCUWTjCz0B+Sj0ekfdvkZ3abBv+U4GaCtJ1iEm6ANQJ6fEzrG/engcKw/wXQvEKxSEKQxRGKE7Izt+DSiwBJMUSm71rguMYhQKrBygOIRStf4TiFFRBvbRGKiQLWP29yRSHKBTtfdBmHs0BUpgvtgF4yRFR+NUKi0XZcYjCeCG2smkzLAHkbRBmP0/Uk26O5YnUActBp1GsAI+S5nRJJJal5K1aAMrq0d6Tm9uI6zjyf75dAe6tx/SsWeD//o2/Ab6IH3/h25pOAAAAAElFTkSuQmCC) no-repeat center; +} +.w2ui-icon.icon-page { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAINSURBVBgZBcG/r55zGAfg6/4+z3va01NHlYgzEfE7MdCIGISFgS4Gk8ViYyM2Mdlsko4GSf8Do0FLRCIkghhYJA3aVBtEz3nP89wf11VJvPDepdd390+8Nso5nESBQoq0pfvXm9fzWf19453LF85vASqJlz748vInb517dIw6EyYBIIG49u+xi9/c9MdvR//99MPPZ7+4cP4IZhhTPbwzT2d+vGoaVRRp1rRliVvHq+cfvM3TD82+7mun0o/ceO7NT+/4/KOXjwZU1ekk0840bAZzMQ2mooqh0A72d5x/6sB9D5zYnff3PoYBoWBgFKPKqDKqjCpjKr//dcu9p489dra88cydps30KswACfNEKanSaxhlntjJ8Mv12Paie+vZ+0+oeSwwQ0Iw1xAR1CiFNJkGO4wu3ZMY1AAzBI0qSgmCNJsJUEOtJSMaCTBDLyQ0CknAGOgyTyFFiLI2awMzdEcSQgSAAKVUmAeNkxvWJWCGtVlDmgYQ0GFtgg4pNtOwbBcwQy/Rife/2yrRRVI0qYCEBly8Z+P4qMEMy7JaVw72N568e+iwhrXoECQkfH91kY7jwwXMsBx1L93ZruqrK6uuiAIdSnTIKKPLPFcvay8ww/Hh+ufeznTXu49v95IMoQG3784gYXdTqvRmqn/Wpa/ADFX58MW3L71SVU9ETgEIQQQIOOzub+fhIvwPRDgeVjWDahIAAAAASUVORK5CYII=) no-repeat center; +} +/************************************************* +* ---- Locking portion of the screen (in grid, form, layout) +*/ +.w2ui-lock { + display: none; + position: absolute; + z-index: 14000; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.15; + filter: alpha(opacity=15); + background-color: #333; +} +.w2ui-lock-msg { + display: none; + position: absolute; + z-index: 14000; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + min-width: 100px; + max-width: 95%; + padding: 30px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: 1.3rem; + font-family: OpenSans; + opacity: 0.8; + background-color: #555; + color: white; + text-align: center; + border-radius: 5px; + border: 2px solid #444; +} +.w2ui-lock-msg .w2ui-spinner { + display: inline-block; + width: 24px; + height: 24px; + margin: -3px 8px -7px -10px; +} +/************************************************* +* ---- Scroll contet, used in toolbar and tabs ---- +*/ +.w2ui-scroll-wrapper { + overflow: hidden; +} +.w2ui-scroll-left, +.w2ui-scroll-right { + top: 0; + width: 18px; + height: 100%; + cursor: default; + z-index: 10; + display: none; + position: absolute; +} +.w2ui-scroll-left:hover, +.w2ui-scroll-right:hover { + background-color: #ddd; +} +.w2ui-scroll-left { + left: 0; + box-shadow: 0px 0px 7px #5F5F5F; + background: #F7F7F7 url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAzklEQVR4Ae2THRDEMBCFzy1ucatb3eJ2uhi3uNUtbnGrW9zi1rOdNzdvdl7nDpvYt/9/r7+/51myZZf/zXkD2iMHHRSb0x3oskwMieK05PwEXqP4ExSL0wp0ROao2OOuMPOMdUL6XU1/oGLcFWb+NqyTd2W/P/2qTr9h+nFXhOkHXRHiNyjrgp/U/V+WaQcaNY13zZI0A1JvcVqAnrGDTdtDtZUHjHIJhxxVLN0iqXgCP1l/7h8U9kc6abyJ4/eNWPpGdBv+XdUK0K8cnvcBly2rDr7C1HQAAAAASUVORK5CYII=') center center no-repeat; + background-size: 15px 12px; +} +.w2ui-scroll-right { + right: 0; + box-shadow: 0px 0px 7px #5F5F5F; + background: #F7F7F7 url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVR4Ae2UG7TGMBCEr1vd4la3uMUtuli3utWtbnGLW9zi9l/bDMzJG7u12cfJfLunf1+UEC9Bv0vVQwJ8hjRCaZafflb1C9RQf4OD0gSDE+i+PiJAabFhQc1y1AYYsJGLY3lgxM17uWPO56yPiFDqVPWgRtpIHSd1zPnwkBsdI58OlNwx4fP2X0TgfMTOoHSdKOXkpyNvEyQh7ul+4swxJSTQuwNDxz68l/ukVNbu0Neen5Z+KvzWxBAqHds349uPFJ/jVOrPjxUq++OLf+20q5+noXo0AAAAAElFTkSuQmCC') center center no-repeat; + background-size: 15px 13px; +} +/************************************* +* ---- Notification message ---- +*/ +#w2ui-notify { + pointer-events: none; + position: absolute; + display: flex; + flex-direction: column; + align-items: center; + left: 0px; + right: 0px; + bottom: 15px; + z-index: 10000; + overflow: hidden; +} +#w2ui-notify > div { + pointer-events: all; + position: relative; + background-color: #292828ba; + color: #ffffff; + padding: 8px 44px 8px 16px; + border-radius: 4px; + box-shadow: 3px 3px 10px #9c9c9c; + max-height: 76px; + min-width: 100px; + max-width: 800px; + font-size: 1.6rem; + text-shadow: 1px 0 0 black; +} +#w2ui-notify > div a { + color: #6cd0e8; + text-decoration: none; + cursor: pointer; +} +#w2ui-notify > div a:hover { + color: #a2f0ff; +} +#w2ui-notify > div span.w2ui-notify-close { + padding: 6px 6px; + border-radius: 3px; + font-size: 1.3rem; + color: #c3c3c3; + position: absolute; + right: 5px; + top: 3px; +} +#w2ui-notify > div span.w2ui-notify-close:hover { + background-color: #807e7e; + color: #ffffff; +} +#w2ui-notify > div.w2ui-notify-error { + text-shadow: none; + background-color: rgba(255, 0, 0, 0.8); +} +#w2ui-notify > div.w2ui-notify-error .w2ui-notify-close { + color: #ffffff; +} +#w2ui-notify > div.w2ui-notify-error .w2ui-notify-close:hover { + background-color: #fcadad; + color: rgba(255, 0, 0, 0.8); +} +#w2ui-notify > div.w2ui-notify-success { + text-shadow: none; + background-color: rgba(21, 192, 70, 0.8); +} +#w2ui-notify > div.w2ui-notify-success .w2ui-notify-close { + color: #ffffff; +} +#w2ui-notify > div.w2ui-notify-success .w2ui-notify-close:hover { + background-color: #7ae097; + color: rgba(21, 192, 70, 0.8); +} +input[type=button].w2ui-btn, +button.w2ui-btn { + position: relative; + display: inline-block; + border-radius: 0.25rem; + margin: 0px 3px; + padding: 6px 12px; + color: #666; + font-size: 1.2rem; + border: 1px solid transparent; + background-image: linear-gradient(#E8E8EE 0%, #E8E8EE 100%); + outline: none; + box-shadow: 0px 1px 0px white; + cursor: default; + min-width: 75px; + line-height: 110%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +input[type=button].w2ui-btn:hover, +button.w2ui-btn:hover { + text-decoration: none; + background-image: linear-gradient(#ddd 0%, #DDDDDD 100%); + color: #333; +} +input[type=button].w2ui-btn:active, +button.w2ui-btn:active, +input[type=button].w2ui-btn.clicked, +button.w2ui-btn.clicked { + background-image: linear-gradient(#ccc 0%, #ccc 100%); + text-shadow: 1px 1px 1px #eee; +} +input[type=button].w2ui-btn:focus:before, +button.w2ui-btn:focus:before { + content: ""; + border: 1px dashed #aaa; + border-radius: 0px; + position: absolute; + top: 2px; + bottom: 2px; + left: 2px; + right: 2px; + pointer-events: none; +} +input[type=button].w2ui-btn-blue, +button.w2ui-btn-blue { + color: white; + background-image: linear-gradient(#269DF0 0%, #269DF0 100%); + border: 1px solid #269DF0; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-blue:hover, +button.w2ui-btn-blue:hover { + color: white; + background-image: linear-gradient(#2391DD 0%, #2391DD 100%); + border: 1px solid #2391DD; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-blue:active, +button.w2ui-btn-blue:active, +input[type=button].w2ui-btn-blue.clicked, +button.w2ui-btn-blue.clicked { + color: white; + background-image: linear-gradient(#1E83C9 0%, #1E83C9 100%); + border: 1px solid #1268A6; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-blue:focus:before, +button.w2ui-btn-blue:focus:before { + border: 1px dashed #e8e8e8; +} +input[type=button].w2ui-btn-green, +button.w2ui-btn-green { + color: white; + background-image: linear-gradient(#52A452 0%, #52A452 100%); + border: 1px solid #52A452; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-green:hover, +button.w2ui-btn-green:hover { + color: white; + background-image: linear-gradient(#3F8F3D 0%, #3F8F3D 100%); + border: 1px solid #3F8F3D; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-green:active, +button.w2ui-btn-green:active, +input[type=button].w2ui-btn-green.clicked, +button.w2ui-btn-green.clicked { + color: white; + background-image: linear-gradient(#377D36 0%, #377D36 100%); + border: 1px solid #555; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-green:focus:before, +button.w2ui-btn-green:focus:before { + border: 1px dashed #e8e8e8; +} +input[type=button].w2ui-btn-orange, +button.w2ui-btn-orange { + color: white; + background-image: linear-gradient(#FB8822 0%, #FB8822 100%); + border: 1px solid #FB8822; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-orange:hover, +button.w2ui-btn-orange:hover { + color: white; + background-image: linear-gradient(#F1731F 0%, #F1731F 100%); + border: 1px solid #F1731F; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-orange:active, +button.w2ui-btn-orange:active, +input[type=button].w2ui-btn-orange.clicked, +button.w2ui-btn-orange.clicked { + color: white; + border: 1px solid #666; + background-image: linear-gradient(#B98747 0%, #B98747 100%); + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-orange:focus:before, +button.w2ui-btn-orange:focus:before { + border: 1px dashed #f9f9f9; +} +input[type=button].w2ui-btn-red, +button.w2ui-btn-red { + color: white; + background-image: linear-gradient(#f9585a 0%, #f9585a 100%); + border: 1px solid #f9585a; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-red:hover, +button.w2ui-btn-red:hover { + color: white; + background-image: linear-gradient(#de4446 0%, #de4446 100%); + border: 1px solid #de4446; + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-red:active, +button.w2ui-btn-red:active, +input[type=button].w2ui-btn-red.clicked, +button.w2ui-btn-red.clicked { + color: white; + border: 1px solid #861C1E; + background-image: linear-gradient(#9C2123 0%, #9C2123 100%); + text-shadow: 0px 0px 1px #111; +} +input[type=button].w2ui-btn-red:focus:before, +button.w2ui-btn-red:focus:before { + border: 1px dashed #ddd; +} +input[type=button].w2ui-btn-small, +button.w2ui-btn-small { + padding: 5px; + border-radius: 4px; + margin: 0px; + min-width: 0px; +} +input[type=button].w2ui-btn-small:focus:before, +button.w2ui-btn-small:focus:before { + border-radius: 2px; + top: 2px; + bottom: 2px; + left: 2px; + right: 2px; +} +input[type=button].w2ui-btn:disabled, +button.w2ui-btn:disabled { + border: 1px solid #e6e6e6; + background: #f7f7f7; + color: #bdbcbc; + text-shadow: none; +} +/* general overlay */ +.w2ui-overlay { + --tip-size: 10px; + position: fixed; + z-index: 17000; + opacity: 0; + transition: opacity 0.1s; + border-radius: 4px; +} +.w2ui-overlay * { + box-sizing: border-box; +} +.w2ui-overlay .w2ui-draggable { + cursor: move; +} +.w2ui-overlay .w2ui-overlay-body { + display: inline-block; + border-radius: 2px; + padding: 4px 8px; + margin: 0px; + font-size: 1.4rem; + font-family: OpenSans; + box-shadow: 0 0 10px #00000042; + color: white; + border: 1px solid #555; + background-color: #69717a; + text-shadow: 0px 1px 1px #444; + line-height: 1.4; + letter-spacing: 0.1px; + overflow: auto; + /* yellow */ + /* bright white */ +} +.w2ui-overlay .w2ui-overlay-body.w2ui-light { + color: #3c3c3c; + text-shadow: none; + background-color: #fffde9; + border: 1px solid #a0a0a0; + box-shadow: 0 0 10px #00000008; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-white { + color: #3c3c3c; + text-shadow: none; + background-color: #fafafa; + border: 1px solid #a0a0a0; + /*box-shadow: 0 2px 15px #00000008;*/ + /*outline: 1px solid white;*/ +} +.w2ui-overlay .w2ui-overlay-body.w2ui-arrow-right:before { + content: ""; + position: absolute; + left: calc(var(--tip-size, 8px) * -0.5 - 1px); + top: calc(50% - 1px); + transform: rotate(-45deg) translateY(-50%); + transform-origin: top center; + margin: 0; + border: inherit; + border-color: inherit; + background-color: inherit; + width: var(--tip-size, 8px); + height: var(--tip-size, 8px); + border-bottom-right-radius: 200px; + border-bottom-width: 0; + border-right-width: 0; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-arrow-left:after { + content: ""; + position: absolute; + right: calc(var(--tip-size, 8px) * -0.5 - 1px); + top: calc(50% - 1px); + transform: rotate(135deg) translateY(-50%); + transform-origin: top center; + margin: 0; + border: inherit; + border-color: inherit; + background-color: inherit; + width: var(--tip-size, 8px); + height: var(--tip-size, 8px); + border-bottom-right-radius: 200px; + border-bottom-width: 0; + border-right-width: 0; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-arrow-bottom:before { + content: ""; + position: absolute; + top: calc(var(--tip-size, 8px) * -0.5); + left: 50%; + transform: rotate(45deg) translateX(-50%); + transform-origin: center left; + margin: 0; + border: inherit; + border-color: inherit; + background-color: inherit; + width: var(--tip-size, 8px); + height: var(--tip-size, 8px); + border-bottom-right-radius: 200px; + border-bottom-width: 0; + border-right-width: 0; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-arrow-top:after { + content: ""; + position: absolute; + bottom: calc(var(--tip-size, 8px) * -0.5 + 5px); + left: 50%; + transform-origin: center left; + transform: rotate(-135deg) translateX(-50%); + margin: 0; + border: inherit; + border-color: inherit; + background-color: inherit; + width: var(--tip-size, 8px); + height: var(--tip-size, 8px); + border-bottom-right-radius: 200px; + border-bottom-width: 0; + border-right-width: 0; +} +/* color overlay */ +.w2ui-colors-header { + padding: 4px 11px; + border-bottom: 1px solid #dddddd; + font-weight: bold; + font-size: 1.4rem; + cursor: move; +} +.w2ui-colors { + padding: 8px; + padding-bottom: 0px; + background-color: white; + border-radius: 3px; + overflow: hidden; + width: 270px; + height: 270px; + user-select: none; +} +.w2ui-colors * { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; +} +.w2ui-colors .w2ui-color-tabs { + display: flex; + background-color: #f7f7f7; + height: 34px; + margin: 14px -8px 0px -8px; + border-top: 1px solid #d6d6d6; +} +.w2ui-colors .w2ui-color-tabs .w2ui-color-tab { + display: inline-block; + width: 65px; + height: 32px; + border: 0; + border-top: 2px solid transparent; + border-radius: 1px; + margin: -1.5px 4px; + text-align: center; + font-size: 1.4rem; + padding-top: 4px; + color: #7b7b7b; +} +.w2ui-colors .w2ui-color-tabs .w2ui-color-tab:hover { + background-color: #e1e1e1; +} +.w2ui-colors .w2ui-color-tabs .w2ui-color-tab.w2ui-selected { + border-top-color: #0175ff; +} +.w2ui-colors .w2ui-color-tabs .w2ui-color-tab .w2ui-icon { + padding-top: 0; + margin-top: -2px; + width: 30px; + height: 21px; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row { + display: flex; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row .w2ui-color { + cursor: default; + text-align: center; + display: inline-block; + width: 18px; + height: 18px; + padding: 6px; + margin: 1.5px; + border: 1px solid transparent; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row .w2ui-color:hover { + outline: 1px solid #666; + border: 1px solid #fff; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row .w2ui-color.w2ui-no-color { + border: 1px solid #efefef; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABlBMVEX/////TgCFoIUYAAAAGUlEQVR42uXHIQEAAACDsNO/NJ4Kn9uC8wsJkAARUrXAjwAAAABJRU5ErkJggg==) 16px 16px; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row .w2ui-color.w2ui-selected:before { + content: '\2022'; + position: relative; + left: -1px; + top: -8px; + color: white; + font-size: 1.4rem; + text-shadow: 0px 0px 2px #222; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row .w2ui-color.w2ui-color-picker { + position: absolute; + right: 11px; + margin-top: 1px; + cursor: pointer; + border: 1px solid #b9b5b5; + width: 28px; + height: 23px; + color: #6e7e9f; + background-color: #e6e3e3; + border-radius: 3px; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row .w2ui-color.w2ui-color-picker:hover { + color: #444; + outline: 1px solid #888; + border: 1px solid #e0e1e5; + background-color: #e0e1e5; +} +.w2ui-colors .w2ui-tab-content.tab-1 .w2ui-color-row .w2ui-color.w2ui-color-picker .w2ui-icon { + position: relative; + top: -7px; + left: -2px; + margin: 0; + padding: 0; + font-size: 1.4rem; + height: 20px; +} +.w2ui-colors .w2ui-tab-content.tab-2 { + height: 213px; + padding: 1px 2px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .palette { + position: relative; + width: 150px; + height: 125px; + outline: 1px solid #d2d2d2; +} +.w2ui-colors .w2ui-tab-content.tab-2 .palette .palette-bg { + height: 100%; + background-image: linear-gradient(0deg, #000000, rgba(204, 154, 129, 0)); + pointer-events: none; +} +.w2ui-colors .w2ui-tab-content.tab-2 .rainbow { + position: relative; + width: 150px; + height: 12px; + margin: 12px 0px 0px 0px; + background: linear-gradient(90deg, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); +} +.w2ui-colors .w2ui-tab-content.tab-2 .alpha { + position: relative; + width: 150px; + height: 12px; + margin: 14px 0px 0px 0px; + background-color: #fff; + background-image: linear-gradient(45deg, #bbb 25%, transparent 25%, transparent 75%, #bbb 75%, #bbb), linear-gradient(45deg, #bbb 25%, transparent 25%, transparent 75%, #bbb 75%, #bbb); + background-size: 12px 12px; + background-position: 0 0, 6px 6px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .alpha .alpha-bg { + height: 100%; + background-image: linear-gradient(90deg, rgba(80, 80, 80, 0) 0%, #505050 100%); + pointer-events: none; +} +.w2ui-colors .w2ui-tab-content.tab-2 .value1 { + pointer-events: none; + position: absolute; + top: 0px; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 10px; + border: 1px solid #999; + outline: 1px solid #bbb; + background-color: transparent; + box-shadow: 0px 0px 1px white; + transform: translateX(-3px) translateY(-3px); +} +.w2ui-colors .w2ui-tab-content.tab-2 .value2 { + pointer-events: none; + position: absolute; + top: -2px; + display: inline-block; + width: 8px; + height: 16px; + border-radius: 2px; + border: 1px solid #696969; + background-color: #ffffff; + box-shadow: 0px 0px 1px white; + transform: translateX(-1px); +} +.w2ui-colors .w2ui-tab-content.tab-2 input.final { + font-size: 1.2rem; + border-radius: 2px; + border: 1px solid #ccc; + width: 100px; + padding: 4px; + color: #333; + margin-top: 14px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info { + float: right; + margin-right: -5px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-preview-bg { + box-shadow: 0 0 1px #c3c3c3; + height: 40px; + background-color: #fff; + background-image: linear-gradient(45deg, #bbb 25%, transparent 25%, transparent 75%, #bbb 75%, #bbb), linear-gradient(45deg, #bbb 25%, transparent 25%, transparent 75%, #bbb 75%, #bbb); + background-size: 16px 16px; + background-position: 0 0, 8px 8px; + margin-bottom: 10px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-preview, +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-original { + height: 40px; + width: 50px; + float: left; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-part { + padding-top: 7px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-part span { + display: inline-block; + width: 8px; + margin: 2px 1px 2px 5px; + color: #666; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-part input { + font-size: 1.2rem; + border-radius: 2px; + border: 1px solid #ccc; + width: 30px; + text-align: right; + padding: 4px; + color: #333; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-part.opacity { + margin: 6px 0px 0px 8px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-part.opacity span { + width: 42px; + margin-left: 0px; + margin-top: 5px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .color-info .color-part.opacity input { + width: 38px; + margin-left: 6px; + text-align: center; +} +.w2ui-colors .w2ui-tab-content.tab-2 .w2ui-color-picker { + position: relative; + left: 5px; + top: 9px; + display: inline-block; + cursor: pointer; + border: 1px solid #b9b5b5; + width: 28px; + height: 24px; + color: #6e7e9f; + background-color: #e6e3e3; + border-radius: 3px; +} +.w2ui-colors .w2ui-tab-content.tab-2 .w2ui-color-picker:hover { + color: #444; + outline: 1px solid #888; + border: 1px solid #e0e1e5; + background-color: #e0e1e5; +} +.w2ui-colors .w2ui-tab-content.tab-2 .w2ui-color-picker .w2ui-icon { + position: relative; + top: -3px; + left: 3px; + font-size: 1.4rem; + height: 20px; +} +/* menu overlay */ +.w2ui-menu-top, +.w2ui-menu-search { + position: sticky; + top: 0; + background-color: white; + border-bottom: 1px dotted silver; +} +.w2ui-menu-search { + padding: 6px 4px; +} +.w2ui-menu-search .w2ui-icon { + position: absolute; + top: 8px; + left: 6px; + color: #90819c; + font-size: 1.4rem; +} +.w2ui-menu-search #menu-search { + width: 100%; + padding: 5px 5px 5px 25px; +} +.w2ui-menu { + display: block; + color: black; + padding: 5px 0px; + border-radius: 5px; + overflow-x: hidden; + cursor: default; + user-select: none; + -webkit-user-select: none; +} +.w2ui-menu .w2ui-menu-item { + display: flex; + align-items: center; + align-content: stretch; + padding: 2px 10px; + user-select: none; +} +.w2ui-menu .w2ui-menu-item.w2ui-even { + color: inherit; + background-color: #FFF; +} +.w2ui-menu .w2ui-menu-item.w2ui-odd { + color: inherit; + background-color: #fbfbfb; +} +.w2ui-menu .w2ui-menu-item.expanded, +.w2ui-menu .w2ui-menu-item:hover { + background-color: #eaeaea; +} +.w2ui-menu .w2ui-menu-item.w2ui-selected { + background-color: #e1e7ff; +} +.w2ui-menu .w2ui-menu-item.w2ui-disabled { + opacity: 0.4; + color: inherit; + background-color: transparent; +} +.w2ui-menu .w2ui-menu-item .menu-icon { + flex: none; + /*width: 26px;*/ + /*height: 16px;*/ + padding: 0; + margin: 0; +} +.w2ui-menu .w2ui-menu-item .menu-icon span { + width: 18px; + font-size: 1.4rem; + color: #8D99A7; + display: flex; + padding-top: 1px; +} +.w2ui-menu .w2ui-menu-item .menu-text { + margin-left: 2px; + flex-grow: 1; + white-space: nowrap; +} +.w2ui-menu .w2ui-menu-item .menu-extra { + flex: none; + min-width: 10px; +} +.w2ui-menu .w2ui-menu-item .menu-extra span { + border: 1px solid #F6FCF4; + border-radius: 20px; + width: auto; + height: 18px; + padding: 2px 7px; + margin: 0 0 0 10px; + background-color: #F2F8F0; + color: #666; + box-shadow: 0px 0px 2px #474545; + text-shadow: 1px 1px 0px #FFF; +} +.w2ui-menu .w2ui-menu-item .menu-extra span.menu-hotkey { + border: none; + border-radius: 0px; + background-color: transparent; + color: #888; + box-shadow: none; + text-shadow: none; +} +.w2ui-menu .w2ui-menu-item .menu-extra span.menu-help { + color: #838383; + background-color: #f7f7f7; + border-color: #d1d1d1; + box-shadow: none; + padding: 0 4px 0 5px; + border-radius: 15px; + position: relative; + margin-top: -1px; + display: block; + height: 20px; + width: 24px; + line-height: 1.3; + text-align: center; + user-select: none; + opacity: 0; + font-family: "Courier New"; + font-size: 1.4rem; +} +.w2ui-menu .w2ui-menu-item .menu-extra span.menu-help:hover { + background-color: #edfaf3; + border-color: #b3cfc1; + color: #199533; +} +.w2ui-menu .w2ui-menu-item .menu-extra span.menu-remove { + background-color: transparent; + border-color: transparent; + box-shadow: none; + padding: 0 4px 0 5px; + border-radius: 3px; + position: relative; + margin-top: -1px; + display: block; + height: 20px; + width: 20px; + opacity: 0; + line-height: 1.3; + text-align: center; + user-select: none; +} +.w2ui-menu .w2ui-menu-item .menu-extra span.menu-remove:hover { + background-color: #f9e7e7; + border: 1px solid #f1c0c0; +} +.w2ui-menu .w2ui-menu-item .menu-extra span.menu-remove:active { + background-color: #ffd1d1; +} +.w2ui-menu .w2ui-menu-item:hover .menu-extra span.menu-remove { + opacity: 1; +} +.w2ui-menu .w2ui-menu-item:hover .menu-extra span.menu-help { + opacity: 1; +} +.w2ui-menu .w2ui-menu-divider { + padding: 5px 0; +} +.w2ui-menu .w2ui-menu-divider .line { + border-top: 1px solid silver; +} +.w2ui-menu .w2ui-menu-divider.has-text { + height: 26px; + background-color: #fafafa; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + text-align: center; +} +.w2ui-menu .w2ui-menu-divider.has-text .line { + display: block; + margin-top: 7px; +} +.w2ui-menu .w2ui-menu-divider.has-text .text { + display: inline-block; + position: relative; + top: -10px; + background-color: #fafafa; + padding: 0px 7px; + color: darkgrey; +} +.w2ui-menu .w2ui-no-items { + padding: 5px 15px; + text-align: center; + color: gray; +} +.w2ui-menu .w2ui-no-items .w2ui-spinner { + position: relative; + left: -2px; + margin-bottom: -5px; + width: 18px; + height: 18px; +} +.w2ui-menu .w2ui-sub-menu-box { + background-color: #fafafd; + border-top: 1px solid #d6e2e6; + border-bottom: 1px solid #d6e2e6; + padding: 0 3px; +} +.w2ui-menu .has-sub-menu .menu-extra span { + position: relative; + border-color: transparent; + background-color: transparent; + box-shadow: none; + padding: 0px 6px; + border-radius: 0px; + margin-left: 5px; +} +.w2ui-menu .has-sub-menu .menu-extra span:after { + content: ""; + position: absolute; + border-left: 5px solid #808080; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + transform: rotateZ(0deg); + pointer-events: none; + margin-left: -2px; + margin-top: 4px; +} +.w2ui-menu .has-sub-menu .menu-extra span:hover { + border-color: transparent; + background-color: transparent; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu-item { + color: #d9d9d9; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu-item.expanded, +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu-item:hover { + background-color: #595d67; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu-item.w2ui-selected { + color: white; + background-color: #4bb8ff; + text-shadow: 0px 1px 1px #66686d; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu-item.w2ui-disabled { + color: #a3a3a3; + background-color: inherit; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu-item .menu-icon span { + color: inherit; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu-item .menu-extra span { + border: 1px solid transparent; + color: #d9d9d9; + text-shadow: 0px 0px 1px #141414; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-sub-menu-box { + background-color: #696f73; + border-top: 1px solid #434444; + border-bottom: 1px solid #434444; +} +.w2ui-overlay .w2ui-overlay-body.w2ui-dark .w2ui-menu .has-sub-menu .menu-extra span:after { + border-left: 5px solid #eaeaea; +} +/* date overlay */ +.w2ui-calendar { + margin: 0px; + line-height: 1.1; + user-select: none; +} +.w2ui-calendar.w2ui-overlay-body { + border: 1px solid #cccace; + color: #3c3c3c; + text-shadow: none; + background-color: #fff; + box-shadow: 0 0 10px #00000008; +} +.w2ui-calendar .w2ui-cal-title, +.w2ui-calendar .w2ui-time-title { + margin: 0px; + padding: 7px 2px; + background-color: #fafafa; + border-top: 1px solid #fefefe; + border-bottom: 1px solid #ddd; + color: #555; + text-align: center; + text-shadow: 1px 1px 1px #eee; + font-size: 1.6rem; + cursor: pointer; +} +.w2ui-calendar .w2ui-cal-title .arrow-down, +.w2ui-calendar .w2ui-time-title .arrow-down { + position: relative; + top: -3px; + left: 5px; + opacity: 0.6; +} +.w2ui-calendar .w2ui-cal-previous, +.w2ui-calendar .w2ui-cal-next { + width: 30px; + height: 30px; + color: #666; + border: 1px solid transparent; + border-radius: 3px; + padding: 7px 5px; + margin: -4px 1px 0px 1px; + cursor: default; +} +.w2ui-calendar .w2ui-cal-previous:hover, +.w2ui-calendar .w2ui-cal-next:hover { + color: black; + border: 1px solid #f5f5f5; + background-color: #f9f7f7; +} +.w2ui-calendar .w2ui-cal-previous:active, +.w2ui-calendar .w2ui-cal-next:active { + color: black; + background-color: #f2f1f4; + border: 1px solid #e6dbfb; +} +.w2ui-calendar .w2ui-cal-previous > div, +.w2ui-calendar .w2ui-cal-next > div { + position: absolute; + border-left: 4px solid #888; + border-top: 4px solid #888; + border-right: 4px solid transparent; + border-bottom: 4px solid transparent; + width: 0px; + height: 0px; + padding: 0px; + margin: 3px 0px 0px 0px; +} +.w2ui-calendar .w2ui-cal-previous { + float: left; +} +.w2ui-calendar .w2ui-cal-previous > div { + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -o-transform: rotate(-45deg); + transform: rotate(-45deg); + margin-left: 6px; +} +.w2ui-calendar .w2ui-cal-next { + float: right; +} +.w2ui-calendar .w2ui-cal-next > div { + -webkit-transform: rotate(135deg); + -moz-transform: rotate(135deg); + -ms-transform: rotate(135deg); + -o-transform: rotate(135deg); + transform: rotate(135deg); + margin-left: 2px; + margin-right: 2px; +} +.w2ui-calendar .w2ui-cal-jump { + display: flex; + background-color: #fdfdfd; +} +.w2ui-calendar .w2ui-cal-jump .w2ui-jump-month, +.w2ui-calendar .w2ui-cal-jump .w2ui-jump-year { + cursor: default; + text-align: center; + border: 1px solid transparent; + border-radius: 3px; + font-size: 1.4rem; +} +.w2ui-calendar .w2ui-cal-jump #w2ui-jump-month { + width: 186px; + padding: 10px 5px 4px 3px; + border-right: 1px solid #efefef; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(4, 52px); + grid-gap: 4px; +} +.w2ui-calendar .w2ui-cal-jump #w2ui-jump-month .w2ui-jump-month { + padding: 15px 0 0 0; +} +.w2ui-calendar .w2ui-cal-jump #w2ui-jump-year { + width: 90px; + height: 240px; + overflow-x: hidden; + overflow-y: auto; + margin: 0 2px; + display: flex; + flex-wrap: wrap; +} +.w2ui-calendar .w2ui-cal-jump #w2ui-jump-year .w2ui-jump-year { + width: 95%; + height: 30px; + padding: 5px 0; + margin: 1px 0; +} +.w2ui-calendar .w2ui-cal-jump .w2ui-jump-month:hover, +.w2ui-calendar .w2ui-cal-jump .w2ui-jump-year:hover { + color: black; + border: 1px solid #f5f5f5; + background-color: #f9f7f7; +} +.w2ui-calendar .w2ui-cal-jump .w2ui-jump-month.w2ui-selected, +.w2ui-calendar .w2ui-cal-jump .w2ui-jump-year.w2ui-selected { + color: black; + background-color: #f2f1f4; + border: 1px solid #e6dbfb; +} +.w2ui-calendar .w2ui-cal-now { + cursor: default; + padding: 3px; + text-align: center; + background-color: #f4f4f4; + margin: 5px; + border: 1px solid #e5e5e5; + border-radius: 4px; +} +.w2ui-calendar .w2ui-cal-now:hover { + color: #28759e; + border: 1px solid #c3d6df; +} +.w2ui-calendar .w2ui-cal-days { + width: 280px; + height: 240px; + padding: 2px; + display: grid; + grid-template-columns: repeat(7, 1fr); +} +.w2ui-calendar .w2ui-cal-days .w2ui-day { + border: 1px solid #fff; + border-radius: 3px; + color: black; + background-color: #f7f7f7; + padding: 8px 0 0 0; + cursor: default; + text-align: center; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-saturday, +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-sunday { + border: 1px solid #fff; + color: #c8493b; + background-color: #f7f7f7; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-today { + background-color: #e2f7cd; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day:hover { + background-color: #f2f1f4; + border: 1px solid #e6dbfb; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day:active { + background-color: #eeebf3; + border: 1px solid #cec2e5; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-selected { + border: 1px solid #8cb067; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-weekday { + text-align: center; + background-color: #fff; + color: #a99cc2; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-weekday:hover { + border: 1px solid #fff; + background-color: #fff; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.outside { + color: #b5b5b5; + background-color: #fff; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-blocked { + color: #555; + background-color: #fff; + border: 1px solid #fff; +} +.w2ui-calendar .w2ui-cal-days .w2ui-day.w2ui-blocked:after { + content: " "; + position: absolute; + color: #b3b3b378; + font-size: 2.7rem; + padding: 0px; + font-family: verdana; + transform: translate(-15px, 15px) rotate(-36deg); + border-top: 1px solid #c9c2c2; + width: 24px; + transform-origin: top left; +} +/* time overlay */ +.w2ui-cal-time { + display: grid; + grid-template-columns: repeat(3, 1fr); + background-color: #ffffff; + cursor: default; +} +.w2ui-cal-time .w2ui-cal-column { + width: 90px; + display: flex; + flex-wrap: wrap; + padding: 4px; +} +.w2ui-cal-time .w2ui-cal-column:nth-child(even) { + background-color: #fafafa; +} +.w2ui-cal-time .w2ui-cal-column span { + width: 100%; + padding: 8px; + margin: 1px; + text-align: center; + border: 1px solid transparent; + border-radius: 2px; + white-space: nowrap; +} +.w2ui-cal-time .w2ui-cal-column span:hover { + background-color: #f2f1f4; + border: 1px solid #e6dbfb; +} +.w2ui-cal-time .w2ui-cal-column span:active { + background-color: #eeebf3; + border: 1px solid #cec2e5; +} +.w2ui-cal-time .w2ui-cal-column span.w2ui-blocked { + pointer-events: none; + text-decoration: line-through; + color: silver; +} +/************************************************* +* ---- Forms ---- +*/ +.w2ui-form { + position: relative; + color: black; + background-color: #fcfcfb; + border: 1px solid #e1e1e1; + border-radius: 3px; + padding: 0px; + overflow: hidden; +} +.w2ui-form > .w2ui-form-box { + position: absolute; + overflow: hidden; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.w2ui-form .w2ui-form-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 36px; + padding: 10px; + overflow: hidden; + font-size: 1.6rem; + color: #444; + background-color: #ffffff; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-bottom: 1px solid #f1f1f1; +} +.w2ui-form .w2ui-form-toolbar { + position: absolute; + left: 0; + right: 0; + margin: 0; + padding: 2px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom: 1px solid #f1f1f1; +} +.w2ui-form .w2ui-form-tabs { + position: absolute; + left: 0; + right: 0; + margin: 0; + padding: 0; + height: 32px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + padding-top: 4px; + background-color: #fff; +} +.w2ui-form .w2ui-form-tabs .w2ui-tab.active { + background-color: #fcfcfb; +} +.w2ui-form .w2ui-page { + position: absolute; + left: 0; + right: 0; + overflow: auto; + padding: 10px 5px 0 5px; + border-left: 1px solid inherit; + border-right: 1px solid inherit; + background-color: inherit; + border-radius: 3px; +} +.w2ui-form .w2ui-column-container { + display: flex; + padding: 0; +} +.w2ui-form .w2ui-column-container .w2ui-column { + width: 100%; +} +.w2ui-form .w2ui-column-container .w2ui-column.col-0, +.w2ui-form .w2ui-column-container .w2ui-column.col-1, +.w2ui-form .w2ui-column-container .w2ui-column.col-2, +.w2ui-form .w2ui-column-container .w2ui-column.col-3, +.w2ui-form .w2ui-column-container .w2ui-column.col-4, +.w2ui-form .w2ui-column-container .w2ui-column.col-5, +.w2ui-form .w2ui-column-container .w2ui-column.col-6, +.w2ui-form .w2ui-column-container .w2ui-column.col-7, +.w2ui-form .w2ui-column-container .w2ui-column.col-8, +.w2ui-form .w2ui-column-container .w2ui-column.col-9, +.w2ui-form .w2ui-column-container .w2ui-column.col-10 { + padding: 0; + padding-left: 10px; +} +.w2ui-form .w2ui-column-container .w2ui-column.col-0 { + padding-left: 0px; +} +.w2ui-form .w2ui-field-columns { + display: flex; + flex-flow: row; +} +.w2ui-form .w2ui-buttons { + position: absolute; + left: 0; + right: 0; + bottom: 0; + text-align: center; + border-top: 1px solid #f1f1f1; + border-bottom: 0px solid #f1f1f1; + background-color: #fff; + padding: 15px 0px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} +.w2ui-form .w2ui-buttons input[type="button"], +.w2ui-form .w2ui-buttons button { + min-width: 80px; + margin-right: 5px; +} +.w2ui-form input[type=checkbox]:not(.w2ui-toggle), +.w2ui-form input[type=radio] { + margin-top: 4px; + margin-bottom: 4px; + width: 14px; + height: 14px; +} +.w2ui-group-title { + padding: 5px 2px 0px 5px; + color: #656164cc; + text-shadow: 1px 1px 2px #fdfdfd; + font-size: 120%; +} +.w2ui-group-fields { + background-color: #fff; + margin: 5px 0px 14px 0px; + padding: 10px 5px; + border-top: 1px dotted #e1e1e1; + border-bottom: 1px dotted #e1e1e1; +} +.w2ui-field > label { + display: block; + float: left; + margin-top: 10px; + margin-bottom: 0px; + width: 120px; + padding: 0px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: right; + min-height: 20px; + color: #666; +} +.w2ui-field > div { + /* do not include width */ + margin-bottom: 3px; + margin-left: 128px; + padding: 4px; + min-height: 28px; + float: none; +} +.w2ui-field .w2ui-form-switch { + display: inline-block; + position: relative; + max-width: 100%; + height: 34px; + margin-top: -5px; + padding: 0px; + border-radius: 2px; + background-color: transparent; +} +.w2ui-field .w2ui-form-switch.w2ui-tb-focus { + outline: 1px dashed gray; +} +.w2ui-field.w2ui-required > div { + position: relative; +} +.w2ui-field.w2ui-required:not(.w2ui-field-inline) > div::before { + content: '*'; + position: absolute; + margin-top: 7px; + margin-left: -8px; + color: red; +} +.w2ui-field.w2ui-required.w2ui-field-inline > div .w2ui-inline-label::after { + content: '*'; + color: red; +} +.w2ui-field.w2ui-disabled { + background-color: transparent !important; +} +.w2ui-field.w2ui-disabled > label, +.w2ui-field.w2ui-disabled.w2ui-field-inline span.w2ui-inline-label { + opacity: 0.45; +} +.w2ui-field.w2ui-disabled > div > input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]), +.w2ui-field.w2ui-disabled > div > select, +.w2ui-field.w2ui-disabled > div > textarea { + opacity: 0.45; + color: #777; + border: 1px solid #bdc0c3 !important; + background-color: #f3f3f3 !important; +} +.w2ui-field > div > .w2ui-inline-label { + display: inline-block; + overflow: hidden; + text-align: right; +} +.w2ui-field.w2ui-span-none > label { + margin: 0; + padding: 5px 12px 0 4px; + display: block; + width: 98%; + text-align: left; +} +.w2ui-field.w2ui-span-none > div { + margin-left: 0; +} +.w2ui-field .w2ui-anchor-span-none { + color: gray; + position: relative; + top: -15px; + left: 3px; +} +.w2ui-field.w2ui-span0 > label, +.w2ui-field.w2ui-span0 > div > .w2ui-inline-label { + display: none; +} +.w2ui-field.w2ui-span0 > div { + margin-left: 0; +} +.w2ui-field.w2ui-span1 > label, +.w2ui-field.w2ui-span1 > div > .w2ui-inline-label { + width: 20px; +} +.w2ui-field.w2ui-span1 > div { + margin-left: 28px; +} +.w2ui-field.w2ui-span2 > label, +.w2ui-field.w2ui-span2 > div > .w2ui-inline-label { + width: 40px; +} +.w2ui-field.w2ui-span2 > div { + margin-left: 48px; +} +.w2ui-field.w2ui-span3 > label, +.w2ui-field.w2ui-span3 > div > .w2ui-inline-label { + width: 60px; +} +.w2ui-field.w2ui-span3 > div { + margin-left: 68px; +} +.w2ui-field.w2ui-span4 > label, +.w2ui-field.w2ui-span4 > div > .w2ui-inline-label { + width: 80px; +} +.w2ui-field.w2ui-span4 > div { + margin-left: 88px; +} +.w2ui-field.w2ui-span5 > label, +.w2ui-field.w2ui-span5 > div > .w2ui-inline-label { + width: 100px; +} +.w2ui-field.w2ui-span5 > div { + margin-left: 108px; +} +.w2ui-field.w2ui-span6 > label, +.w2ui-field.w2ui-span6 > div > .w2ui-inline-label { + width: 120px; +} +.w2ui-field.w2ui-span6 > div { + margin-left: 128px; +} +.w2ui-field.w2ui-span7 > label, +.w2ui-field.w2ui-span7 > div > .w2ui-inline-label { + width: 140px; +} +.w2ui-field.w2ui-span7 > div { + margin-left: 148px; +} +.w2ui-field.w2ui-span8 > label, +.w2ui-field.w2ui-span8 > div > .w2ui-inline-label { + width: 160px; +} +.w2ui-field.w2ui-span8 > div { + margin-left: 168px; +} +.w2ui-field.w2ui-span9 > label, +.w2ui-field.w2ui-span9 > div > .w2ui-inline-label { + width: 180px; +} +.w2ui-field.w2ui-span9 > div { + margin-left: 188px; +} +.w2ui-field.w2ui-span10 > label, +.w2ui-field.w2ui-span10 > div > .w2ui-inline-label { + width: 200px; +} +.w2ui-field.w2ui-span10 > div { + margin-left: 208px; +} +.w2ui-field.w2ui-field-inline { + display: inline; +} +.w2ui-field.w2ui-field-inline > div { + display: inline; + margin: 0; + padding: 0; +} +.w2ui-field .w2ui-box-label { + user-select: none; + vertical-align: middle; +} +.w2ui-field .w2ui-box-label span, +.w2ui-field .w2ui-box-label input { + display: inline-block; + vertical-align: middle; +} +.w2ui-field .w2ui-box-label span { + padding-left: 3px; +} +.w2ui-field .w2ui-box-label input { + margin: 4px 0px 3px 0; +} +input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]).w2ui-error, +textarea.w2ui-error { + border: 1px solid #FFA8A8; + background-color: #FFF4EB; +} +.w2field { + padding: 3px; + border-radius: 3px; + border: 1px solid silver; +} +.w2ui-field-helper { + position: absolute; + display: inline-block; + line-height: 100%; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} +.w2ui-field-helper .w2ui-field-up { + position: absolute; + top: 0px; + padding: 2px 3px; + cursor: pointer; + pointer-events: all; +} +.w2ui-field-helper .w2ui-field-down { + position: absolute; + bottom: 0px; + padding: 2px 3px; + cursor: pointer; + pointer-events: all; +} +.w2ui-field-helper .arrow-up:hover { + border-bottom-color: #81C6FF; +} +.w2ui-field-helper .arrow-down:hover { + border-top-color: #81C6FF; +} +.w2ui-field-helper .w2ui-icon-search { + position: absolute; + margin: 8px 0px 0px -2px; + display: none; + color: #777777; + width: 21px !important; + font-size: 1.3rem; +} +.w2ui-field-helper .w2ui-icon-search.show-search { + display: block; +} +.w2ui-field-helper.w2ui-list { + color: inherit; + position: absolute; + padding: 0px; + margin: 0px; + min-height: 28px; + overflow: auto; + border: 1px solid #e0e0e0; + border-radius: 3px; + font-size: 0.6rem; + line-height: 100%; + box-sizing: border-box; + pointer-events: all; + background-color: #f7fafa; +} +.w2ui-field-helper.w2ui-list.has-focus, +.w2ui-field-helper.w2ui-list:focus-within { + outline: auto #72B2FF; + background-color: white; +} +.w2ui-field-helper.w2ui-list input[type=text] { + -webkit-box-shadow: none; + -moz-box-shadow: none; + -ms-box-shadow: none; + -o-box-shadow: none; + box-shadow: none; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items { + position: absolute; + display: inline-block; + margin: 0px; + padding: 0px; + pointer-events: none; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item { + pointer-events: all; + float: left; + margin: 3px 0px 0px 5px; + border-radius: 15px; + width: auto; + padding: 3px 24px 1px 12px; + border: 1px solid #b4d0de; + background-color: #EFF3F5; + white-space: nowrap; + cursor: default; + font-family: OpenSans; + font-size: 1.1rem; + line-height: 100%; + height: 20px; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item:hover { + background-color: #d0dbe1; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item:last-child { + border-radius: 0px; + border: 1px solid transparent; + background-color: transparent; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item:last-child input { + padding: 1px; + padding-top: 0px; + margin: 0px; + border: 0px; + outline: none; + height: auto; + line-height: 100%; + font-size: inherit; + font-family: inherit; + background-color: transparent; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item .w2ui-icon { + float: left; + color: #828aa7; + margin: 1px 2px 0 -6px; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item .w2ui-list-remove { + float: right; + width: 16px; + height: 16px; + margin: -2px -20px 0px 0px; + border-radius: 2px; + font-size: 1.2rem; + border: 1px solid transparent; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item .w2ui-list-remove:hover { + background-color: #f6e5e5; + border: 1px solid #fac2c2; + color: red; + opacity: 1; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item .w2ui-list-remove:before { + position: relative; + display: inline-block; + left: 4px; + opacity: 0.7; + content: 'x'; + line-height: 1; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-item > span.file-size { + pointer-events: none; + color: #777; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-search { + float: left; + margin: 0; + padding: 0; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-search input[type=text] { + pointer-events: all; + width: 0; + height: 20px; + padding: 3px 0 3px 0; + margin: 3px 0 0 5px; + border: 0; + background-color: transparent; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-items .li-search input[type=text]:focus { + outline: none; + border: 0; +} +.w2ui-field-helper.w2ui-list .w2ui-multi-file { + position: absolute; + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; +} +.w2ui-field-helper.w2ui-list.w2ui-readonly .w2ui-multi-items > .li-item:hover { + background-color: #EFF3F5; +} +.w2ui-field-helper.w2ui-list.w2ui-file-dragover { + background-color: #E4FFDA; + border: 1px solid #93E07D; +} +.w2ui-field-helper.w2ui-list .w2ui-enum-placeholder { + display: inline; + position: absolute; + pointer-events: none; + color: #999; + box-sizing: border-box; +} +.w2ui-overlay .w2ui-file-preview { + padding: 1px; + background-color: white; +} +.w2ui-overlay .w2ui-file-info { + display: grid; + grid-template-columns: 1fr 2fr; + color: white; + padding: 6px 0; +} +.w2ui-overlay .w2ui-file-info .file-caption { + text-align: right; + color: silver; + padding-right: 10px; +} +.w2ui-overlay .w2ui-file-info .file-value { + color: white; +} +.w2ui-overlay .w2ui-file-info .file-type { + max-width: 200px; + display: block-inline; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.arrow-up { + background: none; + width: 0; + height: 0; + border-left: 4px solid transparent; + /* left arrow slant */ + border-right: 4px solid transparent; + /* right arrow slant */ + border-bottom: 5px solid #777; + /* bottom, add background color here */ + font-size: 0; + line-height: 0; +} +.arrow-down { + background: none; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid #777; + font-size: 0; + line-height: 0; +} +.arrow-left { + background: none; + width: 0; + height: 0; + border-bottom: 4px solid transparent; + /* left arrow slant */ + border-top: 4px solid transparent; + /* right arrow slant */ + border-right: 5px solid #777; + /* bottom, add background color here */ + font-size: 0; + line-height: 0; +} +.arrow-right { + background: none; + width: 0; + height: 0; + border-bottom: 4px solid transparent; + /* left arrow slant */ + border-top: 4px solid transparent; + /* right arrow slant */ + border-left: 5px solid #777; + /* bottom, add background color here */ + font-size: 0; + line-height: 0; +} +.w2ui-select { + cursor: default; + color: black !important; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAALCAQAAACnzwd+AAAAcklEQVR4AcXMsQFBQQDG4P9tAgC0gJYRQJZgKQMwCqCku6vVAAAA+NJHP4KHOk0aV2pRw61n4BBmyOxKQ8I4ehZeuhd3HTx6DQEGZ7sBfr2OOOOj3Yi43kMKs9sZknofOexqZ8npMygwWZTX51CipP+YA1OiZJbYYg9lAAAAAElFTkSuQmCC'); + background-size: 17px 6px; + background-position: right center; + background-repeat: no-repeat; +} +.w2ui-select.has-focus { + outline: auto #72B2FF; + background-color: white !important; +} +.w2ui-select[readonly], +.w2ui-select[disabled] { + background-image: none; + background-color: #f1f1f1 !important; + color: #777 !important; +} +/************************************************* +* ---- Layout ---- +*/ +.w2ui-layout { + position: relative; + overflow: hidden; +} +.w2ui-layout > div { + position: absolute; + overflow: hidden; + left: 0; + top: 0; + right: 0; + bottom: 0; +} +.w2ui-layout > div .w2ui-panel { + display: none; + position: absolute; + z-index: 120; +} +.w2ui-layout > div .w2ui-panel .w2ui-panel-title { + position: absolute; + left: 0px; + top: 0px; + right: 0px; + padding: 5px; + background-color: #fff; + color: #656164cc; + border: 1px solid #efefef; + border-bottom: 1px solid #f5f5f5; +} +.w2ui-layout > div .w2ui-panel .w2ui-panel-tabs { + position: absolute; + left: 0px; + top: 0px; + right: 0px; + z-index: 2; + display: none; + overflow: hidden; + background-color: #fff; + padding: 0px; +} +.w2ui-layout > div .w2ui-panel .w2ui-panel-tabs > .w2ui-tab.active { + background-color: #fcfcfc; +} +.w2ui-layout > div .w2ui-panel .w2ui-panel-toolbar { + position: absolute; + left: 0px; + top: 0px; + right: 0px; + z-index: 2; + display: none; + overflow: hidden; + background-color: #FAFAFA; + border-bottom: 1px solid #efefef; + padding: 2px; +} +.w2ui-layout > div .w2ui-panel .w2ui-panel-content { + position: absolute; + left: 0px; + top: 0px; + right: 0px; + bottom: 0px; + z-index: 1; + color: inherit; + background-color: #fcfcfc; +} +.w2ui-layout > div .w2ui-resizer { + display: none; + position: absolute; + z-index: 121; + background-color: transparent; +} +.w2ui-layout > div .w2ui-resizer:hover, +.w2ui-layout > div .w2ui-resizer.active { + background-color: #C8CAD1; +} +/************************************************* +* ---- Grid ---- +*/ +.w2ui-grid { + position: relative; + border: 1px solid #e1e1e1; + border-radius: 2px; + overflow: hidden !important; +} +.w2ui-grid > .w2ui-grid-box { + position: absolute; + overflow: hidden; + left: 0; + top: 0; + right: 0; + bottom: 0; +} +.w2ui-grid .w2ui-grid-header { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 36px; + padding: 10px; + overflow: hidden; + font-size: 1.6rem; + color: #444; + background-color: #fff; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-bottom: 1px solid #e1e1e1 !important; +} +.w2ui-grid .w2ui-grid-toolbar { + position: absolute; + border-bottom: 1px solid #efefef; + background-color: #fafafa; + height: 25px; + padding: 0 ; + margin: 0px; + box-shadow: 0px 1px 2px #f5f5f5; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-tb-button .w2ui-tb-icon { + margin: 3px 0px 0px 0px!important; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input { + position: relative; + width: 300px; + left: 0px; + top: -3px; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-down { + position: absolute; + top: 5px; + left: 4px; + color: #8c99a7; + font-size: 1.3rem; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-grid-search-name { + position: absolute; + margin: 3px 0px 0px 3px; + padding: 5px 27px 6px 10px; + background-color: #fbfbfb; + border: 1px solid #b9b9b9; + border-radius: 15px; + pointer-events: none; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-grid-search-name .name-icon { + position: absolute; + margin-left: -6px; + margin-top: -1px; + color: #8c99a7; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-grid-search-name .name-text { + padding-left: 14px; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-grid-search-name .name-cross { + position: absolute; + margin-top: -5px; + margin-left: 7px; + padding: 4px 5px; + pointer-events: all; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-grid-search-name .name-cross:hover { + color: red; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-all { + outline: none !important; + border-radius: 4px !important; + line-height: normal !important; + height: 30px !important; + width: 300px !important; + border: 1px solid #e1e1e1 !important; + color: #000 !important; + background-color: #f1f1f1 !important; + padding: 1px 28px 0px 24px !important; + margin: 0px !important; + font-size: 1.3rem !important; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-all:focus { + border: 1px solid #007cff !important; + background-color: white !important; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-drop { + position: absolute; + right: 2px; + top: 2px; + height: 26px; + width: 26px; + font-size: 1.6rem; + cursor: pointer; + padding: 7px 2px 7px 2px; + border-radius: 4px; + background-color: transparent; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-drop span.w2ui-icon-drop { + position: relative; + top: -3px; + color: #8d99a7; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-drop:hover, +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-drop.checked { + background-color: #56a1e2; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-drop:hover span.w2ui-icon-drop, +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-search-input .w2ui-search-drop.checked span.w2ui-icon-drop { + color: #fff; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + border-top: 1px solid #ececec; + border-bottom: 1px solid #ececec; + background-color: #fcfdff; + margin: 5px 0; + padding: 0; + height: 25px; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches > div { + white-space: nowrap; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches > span { + min-width: 48px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + border: 1px solid #88c3f7; + border-radius: 15px; + padding: 4px 12px; + margin: 0px 4px; + color: #4c9ad6; + font-size: 1.2rem; + font-weight: bold; + background-color: #F5F9FE; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches > span > span { + font-size: 0.9rem; + position: relative; + top: -1px; + left: 2px; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches .grid-search-line { + border-left: 1px solid #ececec; + width: 11px; + height: 22px; + margin-left: 7px; + margin-top: 1px; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches .w2ui-grid-search-logic { + border: 1px solid #c8c9ca !important; + color: #676767 !important; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches button.grid-search-btn { + margin: 0px 3px; + padding: 0px; + height: 24px; + font-size: 1.1rem; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches button.grid-search-btn.btn-remove { + min-width: 26px; + position: absolute; + left: calc(100% - 35px); +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches .grid-search-count { + background-color: #4cb1fd; + border-radius: 10px; + color: white; + padding: 0px 6px 1px 6px; + font-size: 1.1rem !important; + position: relative !important; + top: 0px !important; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches .grid-search-list li { + padding: 5px; +} +.w2ui-grid .w2ui-grid-toolbar .w2ui-grid-searches .grid-search-list input { + position: relative; + top: 2px; + left: -3px; +} +.w2ui-grid .w2ui-grid-save-search { + padding-top: 30px; + text-align: center; +} +.w2ui-grid .w2ui-grid-save-search span { + width: 280px; + display: inline-block; + text-align: left; + padding-bottom: 4px; +} +.w2ui-grid .w2ui-grid-save-search .search-name { + width: 280px !important; +} +.w2ui-grid .w2ui-grid-body { + position: absolute; + overflow: hidden; + padding: 0px; + background-color: white; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} +.w2ui-grid .w2ui-grid-body input, +.w2ui-grid .w2ui-grid-body select, +.w2ui-grid .w2ui-grid-body textarea { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + -o-user-select: text; + user-select: text; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-columns, +.w2ui-grid .w2ui-grid-body .w2ui-grid-fcolumns { + overflow: hidden; + position: absolute; + left: 0px; + top: 0px; + right: 0px; + box-shadow: 0px 1px 4px #efefef; + height: auto; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-columns table, +.w2ui-grid .w2ui-grid-body .w2ui-grid-fcolumns table { + height: auto; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-columns .w2ui-resizer, +.w2ui-grid .w2ui-grid-body .w2ui-grid-fcolumns .w2ui-resizer { + position: absolute; + z-index: 1000; + display: block; + background-image: none; + background-color: rgba(0, 0, 0, 0); + /* needed for IE */ + padding: 0px; + margin: 0px; + width: 6px; + height: 12px; + cursor: ew-resize; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords { + position: absolute; + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-odd { + color: inherit; + background-color: #fff; + border-bottom: 1px solid #f5f5f5; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd:hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-odd:hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd.w2ui-record-hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-odd.w2ui-record-hover { + color: inherit; + background-color: #f3f3f3; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd.w2ui-empty-record:hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-odd.w2ui-empty-record:hover { + background-color: #fff; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-even { + color: inherit; + background-color: #fbfbfb; + border-bottom: 1px dotted #f5f5f5; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even:hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-even:hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even.w2ui-record-hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-even.w2ui-record-hover { + color: inherit; + background-color: #f3f3f3; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even.w2ui-empty-record:hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-even.w2ui-empty-record:hover { + background-color: #fbfbfb; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-selected, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-selected, +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-selected, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr td.w2ui-selected { + color: black !important; + background-color: #d9eaff !important; + border-bottom: 1px solid transparent; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr.w2ui-inactive, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-inactive, +.w2ui-grid .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-inactive, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords table tr td.w2ui-inactive { + background-color: #e8edf5 !important; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded1, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-expanded1 { + height: 0px; + border-bottom: 1px solid #b2bac0; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded1 > div, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-expanded1 > div { + height: 0px; + border: 0px; + transition: height 0.3s, opacity 0.3s; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded2, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-expanded2 { + height: 0px; + border-radius: 0px; + border-bottom: 1px solid #b2bac0; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-expanded2 > div, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-expanded2 > div { + height: 0px; + border: 0px; + transition: height 0.3s, opacity 0.3s; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-load-more { + cursor: pointer; + background-color: rgba(233, 237, 243, 0.5); + border-right: 1px solid #f1f1f1; + height: 43px; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more > div, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-load-more > div { + text-align: center; + color: #777; + background-color: rgba(233, 237, 243, 0.5); + padding: 10px 0px 15px 0px; + height: 43px; + border-top: 1px dashed #D6D5D7; + border-bottom: 1px dashed #D6D5D7; + font-size: 1.2rem; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-load-more > div:hover, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-load-more > div:hover { + color: #438ba2; + background-color: #f3f3f3; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-records .w2ui-reoder-empty, +.w2ui-grid .w2ui-grid-body .w2ui-grid-frecords .w2ui-reoder-empty { + background-color: #eee; + border-bottom: 1px dashed #aaa; + border-top: 1px dashed #aaa; +} +.w2ui-grid .w2ui-grid-body table { + border-spacing: 0px; + border-collapse: collapse; + table-layout: fixed; + width: 1px; +} +.w2ui-grid .w2ui-grid-body table .w2ui-head { + margin: 0px; + padding: 0px; + border-right: 1px solid #dcdcdc; + border-bottom: 1px solid #dcdcdc; + color: #656164; + background-image: linear-gradient(#ffffff, #f9f9f9); +} +.w2ui-grid .w2ui-grid-body table .w2ui-head > div { + padding: 7px 6px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + position: relative; +} +.w2ui-grid .w2ui-grid-body table td { + border-right: 1px solid #f1f1f1; + border-bottom: 0px solid #D6D5D7; + cursor: default; + overflow: hidden; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-soft-span, +.w2ui-grid .w2ui-grid-body table td.w2ui-soft-hidden { + border-right-color: transparent; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data { + margin: 0px; + padding: 0px; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data .w2ui-info { + position: relative; + top: 0px; + left: -1px; + font-size: 1.3rem; + color: #8D99A7; + cursor: pointer; + width: 18px; + display: inline-block; + margin-right: 3px; + text-align: center; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data .w2ui-clipboard-copy { + float: right; + margin-top: -15px; + width: 20px; + height: 16px; + padding: 0px; + text-align: center; + cursor: pointer; + font-size: 1.3rem; + color: #8d98a7; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data .w2ui-clipboard-copy:hover { + color: #545961; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data > div { + padding: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data > div.flexible-record { + height: auto; + overflow: visible; + white-space: normal; +} +.w2ui-grid .w2ui-grid-body table td.w2ui-grid-data .w2ui-show-children { + width: 16px; + height: 10px; + display: inline-block; + position: relative; + top: -1px; + cursor: pointer; +} +.w2ui-grid .w2ui-grid-body table td:last-child { + border-right: 0px; +} +.w2ui-grid .w2ui-grid-body table td:last-child div { + text-overflow: clip; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-number { + width: 34px; + color: #777; + background-color: rgba(233, 237, 243, 0.5); +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-number div { + padding: 0px 7px 0px 3px; + text-align: right; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-number.w2ui-head { + cursor: pointer; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-select { + width: 26px; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-select div { + padding: 0px 0px; + text-align: center; + overflow: hidden; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-select div input[type=checkbox] { + margin-top: 3px; + margin-bottom: 0px; + position: relative; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-expand { + width: 26px; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-expand div { + padding: 0px 0px; + text-align: center; + font-weight: bold; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-order { + width: 26px; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-order.w2ui-grid-data div { + cursor: move; + height: 18px; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHN0eWxlPSJmaWxsOiAjYWFhOyIgeD0iMCIgeT0iNCIgaGVpZ2h0PSIzIiB3aWR0aD0iMTYiPjwvcmVjdD4KICA8cmVjdCBzdHlsZT0iZmlsbDogI2FhYTsiIHg9IjAiIHk9IjkiIGhlaWdodD0iMyIgd2lkdGg9IjE2Ij48L3JlY3Q+Cjwvc3ZnPg=="); + background-position: 5px 2px; + background-size: 14px 12px; + background-repeat: no-repeat; +} +.w2ui-grid .w2ui-grid-body table .w2ui-col-selected { + background-color: #d1d1d1 !important; +} +.w2ui-grid .w2ui-grid-body table .w2ui-row-selected { + background-color: #e2e2e2 !important; +} +.w2ui-grid .w2ui-grid-body .w2ui-intersection-marker { + position: absolute; + top: 0; + left: 0; + margin-left: -5px; + height: 26px; + width: 10px; +} +.w2ui-grid .w2ui-grid-body .w2ui-intersection-marker.left { + left: 0; + margin-left: -5px; +} +.w2ui-grid .w2ui-grid-body .w2ui-intersection-marker.right { + right: 0; + margin-right: 5px; +} +.w2ui-grid .w2ui-grid-body .w2ui-intersection-marker .top-marker { + position: absolute; + top: 0; + height: 0; + width: 0; + border-top: 5px solid #72B2FF; + border-left: 5px solid transparent; + border-right: 5px solid transparent; +} +.w2ui-grid .w2ui-grid-body .w2ui-intersection-marker .bottom-marker { + position: absolute; + bottom: 0; + height: 0; + width: 0; + border-bottom: 5px solid #72B2FF; + border-left: 5px solid transparent; + border-right: 5px solid transparent; +} +.w2ui-grid .w2ui-grid-body div.w2ui-col-header { + height: auto !important; + width: 100%; + overflow: hidden; + padding-right: 10px !important; +} +.w2ui-grid .w2ui-grid-body div.w2ui-col-header > div.w2ui-sort-up { + border: 4px solid transparent; + border-bottom: 5px solid #8D99A7; + margin-top: -2px; + margin-right: -7px; + float: right; +} +.w2ui-grid .w2ui-grid-body div.w2ui-col-header > div.w2ui-sort-down { + border: 4px solid transparent; + border-top: 5px solid #8D99A7; + margin-top: 2px; + margin-right: -7px; + float: right; +} +.w2ui-grid .w2ui-grid-body .w2ui-col-group { + text-align: center; +} +.w2ui-grid .w2ui-grid-body .w2ui-grid-scroll1 { + position: absolute; + left: 0px; + bottom: 0px; + border-top: 1px solid #ddd; + border-right: 1px solid #ddd; + background-color: #FAFAFA; +} +.w2ui-grid .w2ui-grid-empty-msg { + position: absolute; + top: 27px; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.65); +} +.w2ui-grid .w2ui-grid-empty-msg > div { + position: absolute; + left: 0; + right: 0; + top: 45%; + transform: translateY(-45%); + text-align: center; + font-size: 1.3rem; + color: #666; +} +.w2ui-grid .w2ui-changed { + background: url(data:image/gif;base64,R0lGODlhCgAKAJEAALAABf///wAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAIPlI8Hy8mbxIsSUnup3rQAADs=) no-repeat top right; +} +.w2ui-grid .w2ui-edit-box { + position: absolute; + z-index: 1001; + border: 1.5px solid #6299DA; + pointer-events: auto; + padding: 2px !important; + margin: 0px !important; + background-color: white; +} +.w2ui-grid .w2ui-edit-box .w2ui-editable div.w2ui-input { + outline: none; + padding: 0.5px 1.5px !important; +} +.w2ui-grid .w2ui-edit-box .w2ui-editable input { + top: -2px !important; + padding: 1.5px !important; +} +.w2ui-grid .w2ui-editable { + overflow: hidden; + height: 100% !important; + margin: 0 !important; + padding: 3.5px 2px 2px 2px !important; +} +.w2ui-grid .w2ui-editable input { + top: -1px; + border: 0 !important; + border-radius: 0 !important; + border-color: transparent !important; + padding: 3px !important; + display: inline-block; + width: 100% !important; + height: 100% !important; + pointer-events: auto !important; +} +.w2ui-grid .w2ui-editable div.w2ui-input { + position: relative; + top: -0.5px; + border: 0 transparent; + border-radius: 0 !important; + margin: 0px !important; + padding: 5px 3px !important; + display: inline-block; + width: 100% !important; + height: 100% !important; + pointer-events: auto !important; + background-color: white; + white-space: pre; + overflow: hidden; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + -o-user-select: text; + user-select: text; +} +.w2ui-grid .w2ui-grid-summary { + position: absolute; + border-top: 1px solid gainsboro; + box-shadow: 0px -1px 4px #f0eeee; +} +.w2ui-grid .w2ui-grid-summary table { + color: inherit; +} +.w2ui-grid .w2ui-grid-summary table .w2ui-odd { + background-color: #fff; +} +.w2ui-grid .w2ui-grid-summary table .w2ui-even { + background-color: #fbfbfb; +} +.w2ui-grid .w2ui-grid-footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin: 0px; + padding: 0px; + text-align: center; + font-size: 1.1rem; + height: 24px; + overflow: hidden; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + -o-user-select: text; + user-select: text; + box-shadow: 0px -1px 4px #f5f5f5; + color: #444; + background-color: #F8F8F8; + border-top: 1px solid #e4e4e4; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} +.w2ui-grid .w2ui-grid-footer .w2ui-footer-left { + float: left; + padding-top: 5px; + padding-left: 5px; +} +.w2ui-grid .w2ui-grid-footer .w2ui-footer-right { + float: right; + padding-top: 5px; + padding-right: 5px; +} +.w2ui-grid .w2ui-grid-footer .w2ui-footer-center { + padding: 2px; + text-align: center; +} +.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav { + width: 110px; + margin: 0 auto; + padding: 0px; + text-align: center; +} +.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav input[type=text] { + padding: 1px 2px 2px 2px; + border-radius: 3px; + width: 40px; + text-align: center; +} +.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav a.w2ui-footer-btn { + display: inline-block; + border-radius: 3px; + cursor: pointer; + font-size: 1.1rem; + line-height: 16px; + padding: 1px 5px; + width: 30px; + height: 18px; + margin-top: -1px; + color: black; + background-color: transparent; +} +.w2ui-grid .w2ui-grid-footer .w2ui-footer-center .w2ui-footer-nav a.w2ui-footer-btn:hover { + color: #000; + background-color: #AEC8FF; +} +.w2ui-grid .w2ui-grid-focus-input { + position: absolute; + top: 0; + right: 0; + z-index: -1; + opacity: 0; + overflow: hidden; + padding: 0px; + margin: 0px; + width: 1px; + height: 1px; + resize: none; + border: 0px; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-selected { + background-color: #EEF4FE !important; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr td.w2ui-inactive { + background-color: #F4F6F9 !important; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table td { + border-right-width: 1px; + border-bottom: 1px solid #efefef; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd, +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even, +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-odd:hover, +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr.w2ui-even:hover { + background-color: inherit; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-records table tr:first-child td { + border-top: 0px; + border-bottom: 0px; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table tr td.w2ui-selected { + background-color: #EEF4FE !important; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table tr td.w2ui-inactive { + background-color: #F4F6F9 !important; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table td { + border-right-width: 1px; + border-bottom: 1px solid #efefef; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-odd, +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-even, +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-odd:hover, +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table tr.w2ui-even:hover { + background-color: inherit; +} +.w2ui-ss .w2ui-grid-body .w2ui-grid-frecords table tr:first-child td { + border-bottom: 0px; +} +.w2ui-ss .w2ui-grid-body .w2ui-selection { + position: absolute; + z-index: 1000; + border: 1.5px solid #6299DA; + /* #457FC2; */ + border-radius: 2px; + pointer-events: none; +} +.w2ui-ss .w2ui-grid-body .w2ui-selection.w2ui-selection-preview { + background-color: #5a91ea08; + border: 1px dashed silver; +} +.w2ui-ss .w2ui-grid-body .w2ui-selection.w2ui-selection-expand { + background-color: #e7f2e844; + border: 1px dotted silver; +} +.w2ui-ss .w2ui-grid-body .w2ui-selection .w2ui-selection-resizer { + cursor: crosshair; + position: absolute; + bottom: 0px; + right: 0px; + width: 6px; + height: 6px; + margin-right: -3px; + margin-bottom: -3px; + background-color: #457FC2; + border: 0.5px solid #fff; + outline: 1px solid white; + pointer-events: auto; +} +.w2ui-ss .w2ui-grid-body .w2ui-selection.w2ui-inactive { + border: 1.5px solid #C0C2C5; +} +.w2ui-ss .w2ui-grid-body .w2ui-selection.w2ui-inactive .w2ui-selection-resizer { + background-color: #B0B0B0; +} +.w2ui-ss .w2ui-grid-body .w2ui-soft-range { + position: absolute; + pointer-events: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.w2ui-ss .w2ui-grid-body .w2ui-changed { + background: inherit; + /* do not show changed for spreadsheet */ +} +.w2ui-ss .w2ui-grid-body .w2ui-editable input { + outline: none !important; +} +.w2ui-info-bubble table { + font-family: OpenSans; + font-size: 1.2rem; + color: white; + text-shadow: 1px 1px solid #999; +} +.w2ui-info-bubble table tr td:first-child { + white-space: nowrap; + padding: 2px; + padding-right: 10px; + color: #ddd; + vertical-align: top; +} +.w2ui-info-bubble table tr td:last-child { + white-space: pre; + padding: 2px; +} +.w2ui-overlay .w2ui-grid-search-suggest { + border-top-left-radius: 5px; + border-top-right-radius: 5px; + padding: 10px; + background-color: white; + border-bottom: 1px solid #e6e6e6; + color: #444444; +} +.w2ui-overlay .w2ui-grid-search-single { + font-size: 1.2rem; + padding-top: 10px; +} +.w2ui-overlay .w2ui-grid-search-single .field { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + border: 1px solid #a9b6c2; + border-radius: 4px; + padding: 4px 12px; + margin: 0 2px; + color: #4295d4; + background-color: #F5F9FE; +} +.w2ui-overlay .w2ui-grid-search-single .operator { + display: inline-block; + color: black; + background-color: #e6e6e6; + border-radius: 4px; + margin: 0 4px; + padding: 6px 10px; +} +.w2ui-overlay .w2ui-grid-search-single .value { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + border: 1px solid #a9b6c2; + border-radius: 4px; + margin: 0 2px; + padding: 4px 12px; +} +.w2ui-overlay .w2ui-grid-search-single .buttons { + text-align: left; + padding: 15px 10px 10px 0px; +} +.w2ui-overlay .w2ui-grid-search-advanced { + text-align: left; + padding: 0px; + background-color: #fff; + text-shadow: none; + border: 1px solid #cdcdd8; + box-shadow: 0px 3px 14px 1px #e8e8e8; +} +.w2ui-overlay .w2ui-grid-search-advanced .search-title { + padding: 20px 0px 9px 20px; + font-size: 1.7rem; + font-weight: bold; + color: #555; + display: none; +} +.w2ui-overlay .w2ui-grid-search-advanced .search-title .search-logic { + float: right; + padding-right: 10px; +} +.w2ui-overlay .w2ui-grid-search-advanced .search-body { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} +.w2ui-overlay .w2ui-grid-search-advanced .search-bottom { + padding: 10px 10px; +} +.w2ui-overlay .w2ui-grid-search-advanced table { + color: #5f5f5f; + font-size: 1.3rem; + padding: 12px 4px 0 4px; +} +.w2ui-overlay .w2ui-grid-search-advanced table td { + padding: 4px; + min-height: 40px; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.caption { + text-align: right; + padding-right: 5px; + padding-left: 20px; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.operator { + text-align: left; + padding: 5px; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.operator select { + width: 100%; + color: black; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.value { + padding-right: 5px; + padding-left: 5px; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.value input[type=text] { + border-radius: 3px; + padding: 5px; + margin-right: 3px; + height: 28px; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.value select { + padding: 0px 20px 5px 5px; + margin-right: 3px; + height: 28px; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.actions:nth-child(1) { + padding: 25px 10px 10px 10px; + text-align: left; +} +.w2ui-overlay .w2ui-grid-search-advanced table td.actions:nth-child(2) { + padding: 25px 10px 10px 10px; + text-align: right; + background-color: #fff; +} +.w2ui-grid-skip { + width: 50px; + margin: -6px 3px; + padding: 3px !important; +} +.w2ui-popup { + position: fixed; + z-index: 1600; + overflow: hidden; + font-family: OpenSans; + border-radius: 6px; + padding: 0px; + margin: 0px; + border: 1px solid #777; + background-color: #fff; + box-shadow: 0 0 25px #0000005d; +} +.w2ui-popup.w2ui-non-blocking { + border: 1px solid #c8c8c8; + box-shadow: 0 0 2px #555; +} +.w2ui-popup, +.w2ui-popup * { + box-sizing: border-box; +} +.w2ui-popup.w2ui-anim-open { + opacity: 0; + transform: scale(0.8); +} +.w2ui-popup.w2ui-anim-close { + opacity: 0; + transform: scale(0.9); +} +.w2ui-popup .w2ui-popup-title-btns { + position: absolute; + z-index: 1021; + right: 0; + margin: 0 10px 0 0; + font-size: 1.7rem; +} +.w2ui-popup .w2ui-popup-title-btns .w2ui-popup-button { + float: right; + width: 25px; + height: 23px; + cursor: pointer; + color: #888; + margin: 0 0 0 2px; + z-index: 301; + position: relative; +} +.w2ui-popup .w2ui-popup-title-btns .w2ui-popup-button span.w2ui-icon { + width: 24px; + height: 23px; + margin-top: 0px; +} +.w2ui-popup .w2ui-popup-title-btns .w2ui-popup-button.w2ui-popup-close:hover { + color: #222; +} +.w2ui-popup .w2ui-popup-title-btns .w2ui-popup-button.w2ui-popup-max:hover { + color: #222; +} +.w2ui-popup .w2ui-popup-title { + padding: 5px 40px 5px 10px; + border-radius: 6px 6px 0px 0px; + background-color: #fff; + border-bottom: 1px solid #eee; + position: absolute; + overflow: hidden; + height: 30px; + left: 0px; + right: 0px; + top: 0px; + text-overflow: ellipsis; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + cursor: move; + font-size: 1.4rem; + color: #555; + z-index: 300; +} +.w2ui-popup .w2ui-popup-title.active { + background-color: #ddebff; +} +.w2ui-popup .w2ui-box, +.w2ui-popup .w2ui-box-temp { + position: absolute; + left: 0px; + right: 0px; + top: 38px; + bottom: 58px; + z-index: 100; +} +.w2ui-popup .w2ui-popup-body { + font-size: 1.4rem; + /*line-height: 130%;*/ + padding: 0px 7px 7px 7px; + color: #000; + background-color: #fff; + position: absolute; + overflow: auto; + width: 100%; + height: 100%; +} +.w2ui-popup .w2ui-popup-buttons { + font-size: 1.1rem; + padding: 14px; + border-radius: 0px 0px 6px 6px; + border-top: 1px solid #eee; + background-color: #fff; + text-align: center; + position: absolute; + overflow: hidden; + height: 56px; + left: 0px; + right: 0px; + bottom: 0px; + z-index: 200; +} +.w2ui-popup .w2ui-popup-no-title { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + top: 0px; +} +.w2ui-popup .w2ui-popup-no-buttons { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + bottom: 0px; +} +.w2ui-popup .w2ui-msg-text { + font-size: 1.4rem; + line-height: 1.5; +} +.w2ui-popup .w2ui-prompt { + font-size: 1.2rem; + padding: 0 10px; +} +.w2ui-popup .w2ui-prompt.textarea { + margin-top: 20px; +} +.w2ui-popup .w2ui-prompt > div { + margin-bottom: 5px; +} +.w2ui-popup .w2ui-prompt > label { + margin-right: 5px; +} +.w2ui-popup .w2ui-prompt input { + width: 230px; +} +.w2ui-popup .w2ui-prompt textarea { + width: 100%; + height: 50px; + resize: none; +} +.w2ui-popup .w2ui-popup-resizer { + z-index: 201; + position: absolute; + bottom: 2px; + right: 2px; + width: 10px; + height: 10px; + cursor: nwse-resize; + color: #aaa; + box-shadow: none; +} +.w2ui-popup .w2ui-popup-resizer.resize-point { + box-sizing: border-box; + display: inline-block; + font-size: inherit; + font-style: normal; + text-indent: -9999px; + vertical-align: middle; + width: 1em; + height: 1em; + line-height: 100%; +} +.w2ui-popup .w2ui-popup-resizer.resize-point::before, +.w2ui-popup .w2ui-popup-resizer.resize-point::after { + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); +} +.w2ui-popup .w2ui-popup-resizer.resize-icon::before { + border: 0.1rem solid currentColor; + border-right: 0; + border-top: 0; + border-left: 0; + height: 1.4em; + transform: translate(-84%, -84%) rotate(-45deg); + width: 1.4em; +} +.w2ui-popup .w2ui-popup-resizer.resize-icon::after { + border: 0.1rem solid currentColor; + border-right: 0; + border-top: 0; + border-left: 0; + height: 0.7em; + transform: translate(-50%, -50%) rotate(-45deg); + width: 0.7em; +} +.w2ui-message { + font-size: 1.4rem; + position: absolute; + z-index: 250; + background-color: #fcfcfc; + border: 1px solid #999; + box-shadow: 0 0 15px #0000003d; + box-sizing: border-box; + border-top: 0px; + border-radius: 6px; + overflow: auto; +} +.w2ui-message .w2ui-msg-text { + font-size: 1.4rem; + line-height: 1.5; +} +.w2ui-message .w2ui-message-title { + border-bottom: 1px solid #efefef; + background-color: #ebf6f9; + position: absolute; + top: 0px; + bottom: 100px; + left: 0px; + right: 0px; + padding: 5px 5px; +} +.w2ui-message .w2ui-message-body { + position: absolute; + top: 30px; + bottom: 45px; + left: 0px; + right: 0px; + overflow: auto; +} +.w2ui-message .w2ui-message-body .w2ui-centered { + line-height: 1.5; +} +.w2ui-message .w2ui-message-buttons { + position: absolute; + height: 36px; + bottom: 0px; + left: 0px; + right: 0px; + border-top: 1px solid #efefef; + background-color: white; + text-align: center; + padding: 5px; +} +.w2ui-message .w2ui-prompt { + font-size: 1.2rem; + padding: 0 10px; +} +.w2ui-message .w2ui-prompt.textarea { + margin-top: 20px; +} +.w2ui-message .w2ui-prompt > div { + margin-bottom: 5px; +} +.w2ui-message .w2ui-prompt > label { + margin-right: 5px; +} +.w2ui-message .w2ui-prompt input { + width: 230px; +} +.w2ui-message .w2ui-prompt textarea { + width: 100%; + height: 50px; + resize: none; +} +/************************************************* +* ---- Sidebar ---- +*/ +.w2ui-sidebar { + position: relative; + cursor: default; + overflow: hidden; + background-color: #fbfbfb; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; +} +.w2ui-sidebar * { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; +} +.w2ui-sidebar > div { + position: absolute; + overflow: hidden; +} +.w2ui-sidebar .w2ui-sidebar-top { + position: absolute; + z-index: 2; + top: 0px; + left: 0px; + right: 0px; +} +.w2ui-sidebar .w2ui-sidebar-top .w2ui-flat-left, +.w2ui-sidebar .w2ui-sidebar-top .w2ui-flat-right { + position: absolute; + right: 2px; + top: 2px; + height: 24px; + padding: 5px; + border-radius: 2px; + background-size: 16px 12px; + background-position: center center; + background-repeat: no-repeat; + background-color: #fbfbfb; +} +.w2ui-sidebar .w2ui-sidebar-top .w2ui-flat-left:hover, +.w2ui-sidebar .w2ui-sidebar-top .w2ui-flat-right:hover { + background-color: #f1f1f1; +} +.w2ui-sidebar .w2ui-sidebar-top .w2ui-flat-left { + left: auto; + width: 25px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAzklEQVR4Ae2THRDEMBCFzy1ucatb3eJ2uhi3uNUtbnGrW9zi1rOdNzdvdl7nDpvYt/9/r7+/51myZZf/zXkD2iMHHRSb0x3oskwMieK05PwEXqP4ExSL0wp0ROao2OOuMPOMdUL6XU1/oGLcFWb+NqyTd2W/P/2qTr9h+nFXhOkHXRHiNyjrgp/U/V+WaQcaNY13zZI0A1JvcVqAnrGDTdtDtZUHjHIJhxxVLN0iqXgCP1l/7h8U9kc6abyJ4/eNWPpGdBv+XdUK0K8cnvcBly2rDr7C1HQAAAAASUVORK5CYII='); +} +.w2ui-sidebar .w2ui-sidebar-top .w2ui-flat-right { + left: 2px; + width: auto; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVR4Ae2UG7TGMBCEr1vd4la3uMUtuli3utWtbnGLW9zi9l/bDMzJG7u12cfJfLunf1+UEC9Bv0vVQwJ8hjRCaZafflb1C9RQf4OD0gSDE+i+PiJAabFhQc1y1AYYsJGLY3lgxM17uWPO56yPiFDqVPWgRtpIHSd1zPnwkBsdI58OlNwx4fP2X0TgfMTOoHSdKOXkpyNvEyQh7ul+4swxJSTQuwNDxz68l/ukVNbu0Neen5Z+KvzWxBAqHds349uPFJ/jVOrPjxUq++OLf+20q5+noXo0AAAAAElFTkSuQmCC'); +} +.w2ui-sidebar .w2ui-sidebar-bottom { + position: absolute; + z-index: 2; + bottom: 0px; + left: 0px; + right: 0px; +} +.w2ui-sidebar .w2ui-sidebar-body { + position: absolute; + z-index: 1; + overflow: auto; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + padding: 2px 0px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node { + position: relative; + border-radius: 4px; + margin: 0px 3px; + padding: 1px 0px; + border: 1px solid transparent; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node .w2ui-node-text { + color: black; + text-shadow: 0px 0px 0px #fff; + pointer-events: none; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node .w2ui-node-text:hover { + color: inherit; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node .w2ui-node-image > span { + color: #737485; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node .w2ui-node-handle { + display: inline-block; + padding: 0px; + margin: 0px; + height: 100%; + position: absolute; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node:hover { + background-color: #f1f1f1; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node .w2ui-node-image { + width: 22px; + text-align: center; + pointer-events: none; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node .w2ui-node-image > span { + color: #888; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled:hover { + background: transparent; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled .w2ui-node-text, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled:hover .w2ui-node-text, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled .w2ui-node-image, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled:hover .w2ui-node-image, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled .w2ui-node-image > span, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-disabled:hover .w2ui-node-image > span { + opacity: 0.4; + filter: alpha(opacity=40); + color: black; + text-shadow: 0px 0px 0px #fff; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-editing .w2ui-node-badge { + display: none; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-reorder-empty { + border: 1px dashed silver; + background-color: #fbfcfe; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-ghost { + position: absolute; + z-index: 999999; + pointer-events: none; + width: calc(100% - 4px); + opacity: 0.8; + background-color: #f3f3f3; + border-top: 1px dotted silver; + border-bottom: 1px dotted silver; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node.w2ui-ghost > * { + pointer-events: none; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node input, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node button { + pointer-events: auto; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-ghost-line { + position: absolute; + z-index: 999999; + pointer-events: none; + width: calc(100% - 4px); + background-color: #82a6ff; + height: 2px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected:hover { + background-color: #f3f5ff; + position: relative; + border: 1px solid #dee1ff; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected .w2ui-node-text, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected:hover .w2ui-node-text, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected .w2ui-node-image, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected:hover .w2ui-node-image, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected .w2ui-node-image > span, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected:hover .w2ui-node-image > span { + color: inherit; + text-shadow: 0px 0px 0px #fff; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-selected:before { + content: ""; + border: 1px dashed transparent; + border-radius: 4px; + position: absolute; + top: -1px; + bottom: -1px; + left: -1px; + right: -1px; + pointer-events: none; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-text { + white-space: nowrap; + padding: 5px 0px 5px 3px; + margin: 1px 0px 1px 22px; + position: relative; + z-index: 1; + font-size: 1.2rem; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-text.no-icon { + margin-left: 0px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-group { + white-space: nowrap; + overflow: hidden; + padding: 10px 0px 10px 10px; + margin: 0px; + cursor: default; + color: #6a5e88; + background-color: transparent; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-group :nth-child(1) { + /* show / hide link */ + margin-right: 10px; + float: right; + color: transparent; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-group :nth-child(2) { + /* title text */ + font-weight: normal; + text-transform: uppercase; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-sub { + overflow: hidden; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-sub .w2ui-reorder-empty-sub { + border: 1px dashed silver; + background-color: #fbfcfe; + height: calc(100% - 4px); + margin: 2px 4px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data { + padding: 2px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-node-image { + padding: 4px 0px 0px 0px; + float: left; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-node-image > span { + font-size: 1.6rem; + color: #737485; + text-shadow: 0px 0px 0px #fff; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-node-image.w2ui-icon { + margin-top: 3px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-node-badge { + display: flex; + flex-direction: row; + float: right; + width: auto; + padding: 2px; + margin: 3px 4px -2px 0; + color: #666; + text-shadow: 1px 1px 0px #FFF; + position: relative; + z-index: 2; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-node-badge.w2ui-node-count { + padding: 2px 7px; + border: 1px solid #F6FCF4; + border-radius: 20px; + box-shadow: 0px 0px 2px #474545; + background-color: #F2F8F0; + margin-right: 0px !important; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-collapsed, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-expanded { + float: right; + width: 15px; + height: 25px; + position: relative; + z-index: 2; + padding-left: 5px; + border-radius: 4px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-collapsed:hover, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-expanded:hover { + background-color: #e1e1e1; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-collapsed.w2ui-left-toggle, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-expanded.w2ui-left-toggle { + float: left; + margin-left: -14px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-collapsed span, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-expanded span { + border-color: transparent; + background-color: transparent; + box-shadow: none; + padding: 2px 5px; + border-radius: 0px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-collapsed span:after, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-expanded span:after { + content: ""; + position: absolute; + border-left: 5px solid #808080; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + transform: rotateZ(-90deg); + pointer-events: none; + margin-left: -4px; + margin-top: 7px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-collapsed span:hover, +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-expanded span:hover { + border-color: transparent; + background-color: transparent; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-data .w2ui-collapsed span:after { + transform: rotateZ(90deg); +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-flat { + min-height: 32px; + margin-bottom: 4px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-flat .w2ui-node-data { + display: block; + padding: 4px 2px; + text-align: center; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-flat .w2ui-node-image { + float: none; + text-align: center; + width: auto; + padding: 1px 0px; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-flat .w2ui-node-image > span { + font-size: 1.6rem; + color: #737485; + text-shadow: 0px 0px 0px #fff; +} +.w2ui-sidebar .w2ui-sidebar-body .w2ui-node-flat .w2ui-node-image.w2ui-icon { + width: 21px; +} +/************************************************* +* ---- Tabs ---- +*/ +.w2ui-tabs { + cursor: default; + overflow: hidden; + position: relative; + background-color: #fff; + min-height: 28px; + padding: 0px; + margin: 0px; +} +.w2ui-tabs .w2ui-tabs-line { + position: absolute; + left: 0px; + right: 0px; + bottom: 0px; + z-index: 1; + border: 0; + height: 1px; + background-color: #e2e2e2; +} +.w2ui-tabs .w2ui-scroll-left, +.w2ui-tabs .w2ui-scroll-right { + z-index: 30; + display: flex; +} +.w2ui-tabs .w2ui-scroll-wrapper { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-content: flex-start; + padding: 0px; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab { + height: 28px; + position: relative; + z-index: 20; + padding: 7px 20px 4px 20px; + text-align: center; + color: black; + background-color: transparent; + /*border: 2px solid transparent;*/ + white-space: nowrap; + /*margin: 0px 1px;*/ + border-radius: 0; + cursor: default; + user-select: none; + border-bottom: 1px solid #e2e2e2; /* 右边框 */ +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab.active { + color: #0175ff; + background-color: transparent; + /*border: 2px solid transparent;*/ + /*border-bottom: 2px solid #0175ff;*/ + margin-bottom: 0; + border-top: 1px solid #e2e2e2; /* 上边框 */ + border-left: 1px solid #e2e2e2; /* 左边框 */ + border-right: 1px solid #e2e2e2; /* 右边框 */ + border-bottom: none; /* 明确去掉下边框 */ +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab:hover { + background-color: #dfe1e630; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab.moving { + color: inherit; + background-color: #eee; + border: 2px solid transparent; + border-radius: 0px; + margin-bottom: 0px; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab.closable { + padding: 6px 28px 6px 20px; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab .w2ui-tab-close { + position: absolute; + right: 3px; + top: 5px; + color: #555; + float: right; + margin-top: -3px; + padding: 2px 4px; + width: 20px; + height: 20px; + opacity: 0.6; + border: 0px; + border-top: 3px solid transparent; + border-radius: 3px; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab .w2ui-tab-close:hover { + background-color: #f9e7e7; + color: red; + opacity: 1; + font-weight: bold; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab .w2ui-tab-close:active { + background-color: #ffd1d1; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tab .w2ui-tab-close:before { + position: relative; + top: -2px; + left: 0px; + color: inherit; + text-shadow: inherit; + content: 'x'; +} +.w2ui-tabs .w2ui-scroll-wrapper .w2ui-tabs-right { + padding: 8px 2px; + width: 100%; + text-align: right; + white-space: nowrap; + border-bottom: 1px solid #e2e2e2; +} +.w2ui-tabs.w2ui-tabs-up .w2ui-tabs-line { + top: 0px; + bottom: auto; +} +.w2ui-tabs.w2ui-tabs-up .w2ui-scroll-wrapper .w2ui-tab { + border: 2px solid transparent; + border-top: 2px solid transparent; + border-radius: 0 0 4px 4px; +} +.w2ui-tabs.w2ui-tabs-up .w2ui-scroll-wrapper .w2ui-tab.active { + border: 2px solid transparent; + border-top: 2px solid #0175ff; + margin-top: 0px; +} +/************************************************* +* ---- Toolbar ---- +*/ +.w2ui-toolbar { + background-color: transparent; + user-select: none; + padding: 2px; +} +.w2ui-toolbar .w2ui-tb-line { + overflow: hidden; + position: relative; + min-height: 28px; + padding: 0; + margin: 0; +} +.w2ui-toolbar .disabled { + opacity: 0.3; + filter: alpha(opacity=30); +} +.w2ui-toolbar .w2ui-scroll-left, +.w2ui-toolbar .w2ui-scroll-right { + z-index: 30; + display: flex; +} +.w2ui-toolbar .w2ui-tb-line:nth-child(2), +.w2ui-toolbar .w2ui-tb-line:nth-child(3), +.w2ui-toolbar .w2ui-tb-line:nth-child(4) { + border-top: 1px solid #e7e7e7; + margin: 0; +} +/*.w2ui-toolbar .w2ui-scroll-wrapper {*/ +/* display: flex;*/ +/* flex-direction: row;*/ +/* flex-wrap: nowrap;*/ +/* justify-content: flex-start;*/ +/* align-content: flex-start;*/ +/* align-items: center;*/ +/* !*padding: 2px;*!*/ +/*}*/ +.w2ui-toolbar .w2ui-scroll-wrapper { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-content: flex-start; + padding: 2px +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button { + position: relative; + z-index: 20; + height: 30px; + min-width: 30px; + padding: 2px; + border: 1px solid transparent; + border-radius: 4px; + color: #000; + background-color: transparent; + white-space: nowrap; + margin: 0 1px; + cursor: default; + user-select: none; + flex-shrink: 0 +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-icon { + float: left; + width: 22px; + margin: 0; + text-align: center; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-icon > span { + font-size: 1.4rem; + color: #8D99A7; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-text { + margin-left: 20px; + /*padding: 0px 0px 0 2px;*/ + /*margin: 0 2px;*/ +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-text .w2ui-tb-color-box { + display: inline-block; + height: 13px; + width: 13px; + margin: 0 2px -2px 0; + border-radius: 1px; + border: 1px solid #fff; + box-shadow: 0 0 1px #555; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-text .w2ui-tb-count { + padding: 0 0 0 4px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-text .w2ui-tb-count > span { + border: 1px solid #F6FCF4; + border-radius: 11px; + width: auto; + height: 18px; + padding: 0 6px 1px 6px; + background-color: #F2F8F0; + color: #666; + box-shadow: 0px 0px 2px #474545; + text-shadow: 1px 1px 0px #FFF; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-text .w2ui-tb-down { + width: 10px; + float: right; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button .w2ui-tb-text .w2ui-tb-down > span { + display: inline-block; + position: relative; + top: 3px; + left: 3px; + border: 4px solid transparent; + border-top: 5px solid #aeb6c0; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button.over { + border: 1px solid transparent; + background-color: #eaeaed; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button.over .w2ui-tb-text { + color: black; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button.checked { + border: 1px solid #d2d2d2; + background-color: rgba(255, 255, 255, 0.78); +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button.checked .w2ui-tb-text { + color: black; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button.down { + border: 1px solid #ccc; + background-color: #eaeaed; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button.down .w2ui-tb-text { + color: #666; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-button.no-icon .w2ui-tb-text { + margin-left: 0; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-right { + width: 100%; + text-align: right; + white-space: nowrap; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-break { + background-image: linear-gradient(to bottom, rgba(153, 153, 153, 0.1) 0%, #999 40%, #999 60%, rgba(153, 153, 153, 0.1) 100%); + z-index: 4; + width: 1px; + height: 24px; + padding: 0; + margin: 3px 6px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-html { + white-space: nowrap; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input { + padding: 1px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-input-label { + color: #474a51; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]):not([type=range]) { + width: 100px; + height: 26px; + padding: 5px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]):not([type=range]):focus { + outline-color: #549eff; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input input:not([type=button]):not([type=submit]):not([type=checkbox]):not([type=radio]):not([type=range]).w2ui-has-spinner { + width: 95px; + padding: 5px 30px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input input[type=range] { + position: relative; + top: 5px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-dec, +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-inc { + position: absolute; + padding-top: 2px; + margin: 2px; + background-color: #f9fafa; + width: 22px; + height: 22px; + font-size: 1.6rem; + text-align: center; + color: #888; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-dec:hover, +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-inc:hover { + background-color: #e4e4e4; + color: #222; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-dec:active, +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-inc:active { + background-color: #d4d4d4; + color: #000; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-dec { + border-right: 1px solid #e0e0e0; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-input .w2ui-spinner-inc { + margin-left: -24px; + border-left: 1px solid #e0e0e0; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.w2ui-toolbar .w2ui-scroll-wrapper .w2ui-tb-spacer { + width: 100%; +} diff --git a/web/src/vendor/w2ui/w2ui.es6.js b/web/src/vendor/w2ui/w2ui.es6.js new file mode 100644 index 0000000..e6cfe86 --- /dev/null +++ b/web/src/vendor/w2ui/w2ui.es6.js @@ -0,0 +1,24634 @@ +/* w2ui 2.0.x (nightly) (10/31/2025, 8:43:03 AM) (c) http://w2ui.com, vitmalina@gmail.com */ +const dmtW2uiZIndexState = globalThis.dmtW2uiZIndexState ??= { value: 1500 } + +/** + * Part of w2ui 2.0 library + * - Dependencies: w2utils + * - on/off/trigger methods id not showing in help + * - refactored with event object + * + * Chanes in 2.0.+ + * - added unmount that cleans up the box + * + */ + +class w2event { + constructor(owner, edata) { + Object.assign(this, { + type: edata.type ?? null, + detail: edata, + owner, + target: edata.target ?? null, + phase: edata.phase ?? 'before', + object: edata.object ?? null, + execute: null, + isStopped: false, + isCancelled: false, + onComplete: null, + listeners: [] + }) + delete edata.type + delete edata.target + delete edata.object + this.complete = new Promise((resolve, reject) => { + this._resolve = resolve + this._reject = reject + }) + // needed empty catch function so that promise will not show error in the console + this.complete.catch(() => {}) + } + finish(detail) { + if (detail) { + w2utils.extend(this.detail, detail) + } + this.phase = 'after' + this.owner.trigger.call(this.owner, this) + } + done(func) { + this.listeners.push(func) + } + preventDefault() { + this._reject() + this.isCancelled = true + } + stopPropagation() { + this.isStopped = true + } +} +class w2base { + /** + * Initializes base object for w2ui, registers it with w2ui object + * + * @param {string} name - name of the object + * @returns + */ + constructor(name) { + this.activeEvents = [] // events that are currently processing + this.listeners = [] // event listeners + // register globally + if (typeof name !== 'undefined') { + if (!w2utils.checkName(name)) return + w2ui[name] = this + } + this.debug = false // if true, will trigger all events + } + /** + * Adds event listener, supports event phase and event scoping + * + * @param {*} edata - an object or string, if string "eventName:phase.scope" + * @param {*} handler + * @returns itself + */ + on(events, handler) { + if (typeof events === 'string') { + events = events.split(/[,\s]+/) // separate by comma or space + } else { + events = [events] + } + events.forEach(edata => { + const name = typeof edata === 'string' ? edata : (edata.type + ':' + edata.execute + '.' + edata.scope) + if (typeof edata === 'string') { + const [eventName, scope] = edata.split('.') + const [type, execute] = eventName.replace(':complete', ':after').replace(':done', ':after').split(':') + edata = { type, execute: execute ?? 'before', scope } + } + edata = w2utils.extend({ type: null, execute: 'before', onComplete: null }, edata) + // errors + if (!edata.type) { console.log('ERROR: You must specify event type when calling .on() method of ' + this.name); return } + if (!handler) { console.log('ERROR: You must specify event handler function when calling .on() method of ' + this.name); return } + if (!Array.isArray(this.listeners)) this.listeners = [] + this.listeners.push({ name, edata, handler }) + if (this.debug) { + console.log('w2base: add event', { name, edata, handler }) + } + }) + return this + } + /** + * Removes event listener, supports event phase and event scoping + * + * @param {*} edata - an object or string, if string "eventName:phase.scope" + * @param {*} handler + * @returns itself + */ + off(events, handler) { + if (typeof events === 'string') { + events = events.split(/[,\s]+/) // separate by comma or space + } else { + events = [events] + } + events.forEach(edata => { + const name = typeof edata === 'string' ? edata : (edata.type + ':' + edata.execute + '.' + edata.scope) + if (typeof edata === 'string') { + const [eventName, scope] = edata.split('.') + const [type, execute] = eventName.replace(':complete', ':after').replace(':done', ':after').split(':') + edata = { type: type || '*', execute: execute || '', scope: scope || '' } + } + edata = w2utils.extend({ type: null, execute: null, onComplete: null }, edata) + // errors + if (!edata.type && !edata.scope) { console.log('ERROR: You must specify event type when calling .off() method of ' + this.name); return } + if (!handler) { handler = null } + let count = 0 + // remove listener + this.listeners = this.listeners.filter(curr => { + if ((edata.type === '*' || edata.type === curr.edata.type) && + (edata.execute === '' || edata.execute === curr.edata.execute) && + (edata.scope === '' || edata.scope === curr.edata.scope) && + (edata.handler == null || edata.handler === curr.edata.handler) + ) { + count++ // how many listeners removed + return false + } else { + return true + } + }) + if (this.debug) { + console.log(`w2base: remove event (${count})`, { name, edata, handler }) + } + }) + return this // needed for chaining + } + /** + * Triggers even listeners for a specific event, loops through this.listeners + * + * @param {Object} edata - Object + * @returns modified edata + */ + trigger(eventName, edata) { + if (arguments.length == 1) { + if (typeof eventName === 'string') { + edata = { type: eventName, target: this } + } else { + edata = eventName + } + } else { + edata.type = eventName + edata.target = edata.target ?? this + } + if (w2utils.isPlainObject(edata) && edata.phase == 'after') { + // find event + edata = this.activeEvents.find(event => { + if (event.type == edata.type && event.target == edata.target) { + return true + } + return false + }) + if (!edata) { + console.log(`ERROR: Cannot find even handler for "${edata.type}" on "${edata.target}".`) + return + } + console.log('NOTICE: This syntax "edata.trigger({ phase: \'after\' })" is outdated. Use edata.finish() instead.') + } else if (!(edata instanceof w2event)) { + edata = new w2event(this, edata) + this.activeEvents.push(edata) + } + let args, fun, tmp + if (!Array.isArray(this.listeners)) this.listeners = [] + if (this.debug) { + console.log(`w2base: trigger "${edata.type}:${edata.phase}"`, edata) + } + // process events in REVERSE order + for (let h = this.listeners.length - 1; h >= 0; h--) { + const item = this.listeners[h] + if (item != null && (item.edata.type === edata.type || item.edata.type === '*') && + (item.edata.target === edata.target || item.edata.target == null) && + (item.edata.execute === edata.phase || item.edata.execute === '*' || item.edata.phase === '*')) { + // add extra params if there + Object.keys(item.edata).forEach(key => { + if (edata[key] == null && item.edata[key] != null) { + edata[key] = item.edata[key] + } + }) + // check handler arguments + args = [] + tmp = new RegExp(/\((.*?)\)/).exec(String(item.handler).split('=>')[0]) + if (tmp) args = tmp[1].split(/\s*,\s*/) + if (args.length === 2) { + item.handler.call(this, edata.target, edata) // old way for back compatibility + if (this.debug) console.log(' - call (old)', item.handler) + } else { + item.handler.call(this, edata) // new way + if (this.debug) console.log(' - call', item.handler) + } + if (edata.isStopped === true || edata.stop === true) return edata // back compatibility edata.stop === true + } + } + // main object events + const funName = 'on' + edata.type.substr(0, 1).toUpperCase() + edata.type.substr(1) + if (edata.phase === 'before' && typeof this[funName] === 'function') { + fun = this[funName] + // check handler arguments + args = [] + tmp = new RegExp(/\((.*?)\)/).exec(String(fun).split('=>')[0]) + if (tmp) args = tmp[1].split(/\s*,\s*/) + if (args.length === 2) { + fun.call(this, edata.target, edata) // old way for back compatibility + if (this.debug) console.log(' - call: on[Event] (old)', fun) + } else { + fun.call(this, edata) // new way + if (this.debug) console.log(' - call: on[Event]', fun) + } + if (edata.isStopped === true || edata.stop === true) return edata // back compatibility edata.stop === true + } + // item object events + if (edata.object != null && edata.phase === 'before' && typeof edata.object[funName] === 'function') { + fun = edata.object[funName] + // check handler arguments + args = [] + tmp = new RegExp(/\((.*?)\)/).exec(String(fun).split('=>')[0]) + if (tmp) args = tmp[1].split(/\s*,\s*/) + if (args.length === 2) { + fun.call(this, edata.target, edata) // old way for back compatibility + if (this.debug) console.log(' - call: edata.object (old)', fun) + } else { + fun.call(this, edata) // new way + if (this.debug) console.log(' - call: edata.object', fun) + } + if (edata.isStopped === true || edata.stop === true) return edata + } + // execute onComplete + if (edata.phase === 'after') { + if (typeof edata.onComplete === 'function') edata.onComplete.call(this, edata) + for (let i = 0; i < edata.listeners.length; i++) { + if (typeof edata.listeners[i] === 'function') { + edata.listeners[i].call(this, edata) + if (this.debug) console.log(' - call: done', fun) + } + } + edata._resolve(edata) + if (this.debug) { + console.log(`w2base: trigger "${edata.type}:${edata.phase}"`, edata) + } + // clean up activeEvents + const ind = this.activeEvents.indexOf(edata) + if (ind !== -1) this.activeEvents.splice(ind, 1) + } + return edata + } + /** + * This method renders component into the box. It is overwritten in descendents and in this base + * component it is empty. + */ + render(box) { + // intentionally left blank + } + /** + * Removes all classes that start with w2ui-* and sets box to null. It is needed so that control will + * release the box to be used for other widgets + */ + unmount() { + const edata = this.trigger('unmount', { target: this.name }) + if (edata.isCancelled) { + return + } + const remove = [] + // find classes that start with "w2ui-*" + if (this.box instanceof HTMLElement) { + this.box.classList.forEach(cl => { + if (cl.startsWith('w2ui-')) remove.push(cl) + }) + } + query(this.box) + .off() // removes all events attached to this box previously + .removeClass(remove) + .removeAttr('name') + .html('') + this.box = null + // event after + edata.finish() + } +} +/** + * Part of w2ui 2.0 library + * - Dependencies: none + * + * These are the master locale settings that will be used by w2utils + * + * "locale" should be the IETF language tag in the form xx-YY, + * where xx is the ISO 639-1 language code ( see https://en.wikipedia.org/wiki/ISO_639-1 ) and + * YY is the ISO 3166-1 alpha-2 country code ( see https://en.wikipedia.org/wiki/ISO_3166-2 ) + */ +const w2locale = { + 'locale': 'en-US', + 'dateFormat': 'm/d/yyyy', + 'timeFormat': 'hh:mi pm', + 'datetimeFormat': 'm/d/yyyy|hh:mi pm', + 'currencyPrefix': '$', + 'currencySuffix': '', + 'currencyPrecision': 2, + 'groupSymbol': ',', // aka "thousands separator" + 'decimalSymbol': '.', + 'shortmonths': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + 'fullmonths': ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + 'shortdays': ['M', 'T', 'W', 'T', 'F', 'S', 'S'], + 'fulldays': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], + 'weekStarts': 'S', // can be "M" for Monday or "S" for Sunday + // phrases used in w2ui, should be empty for original language + // keep these up-to-date and in sorted order + // value = "---" to easier see what to translate + 'phrases': { + '${count} letters or more...': '---', + 'Add new record': '---', + 'Add New': '---', + 'Advanced Search': '---', + 'after': '---', + 'AJAX error. See console for more details.': '---', + 'All Fields': '---', + 'All': '---', + 'Any': '---', + 'Are you sure you want to delete ${count} ${records}?': '---', + 'Attach files by dragging and dropping or Click to Select': '---', + 'before': '---', + 'begins with': '---', + 'begins': '---', + 'between': '---', + 'buffered': '---', + 'Cancel': '---', + 'Close': '---', + 'Column': '---', + 'Confirmation': '---', + 'contains': '---', + 'Copied': '---', + 'Copy to clipboard': '---', + 'Current Date & Time': '---', + 'Delete selected records': '---', + 'Delete': '---', + 'Do you want to delete search item "${item}"?': '---', + 'Edit selected record': '---', + 'Edit': '---', + 'Empty list': '---', + 'ends with': '---', + 'ends': '---', + 'Field should be at least ${count} characters.': '---', + 'Hide': '---', + 'in': '---', + 'is not': '---', + 'is': '---', + 'less than': '---', + 'Line #': '---', + 'Load ${count} more...': '---', + 'Loading...': '---', + 'Maximum number of files is ${count}': '---', + 'Maximum total size is ${count}': '---', + 'Modified': '---', + 'more than': '---', + 'Multiple Fields': '---', + 'Name': '---', + 'No items found': '---', + 'No matches': '---', + 'No': '---', + 'none': '---', + 'Not a float': '---', + 'Not a hex number': '---', + 'Not a valid date': '---', + 'Not a valid email': '---', + 'Not alpha-numeric': '---', + 'Not an integer': '---', + 'Not in money format': '---', + 'not in': '---', + 'Notification': '---', + 'of': '---', + 'Ok': '---', + 'Opacity': '---', + 'Record ID': '---', + 'record': '---', + 'records': '---', + 'Refreshing...': '---', + 'Reload data in the list': '---', + 'Remove': '---', + 'Remove This Field': '---', + 'Request aborted.': '---', + 'Required field': '---', + 'Reset': '---', + 'Restore Default State': '---', + 'Returned data is not in valid JSON format.': '---', + 'Save changed records': '---', + 'Save Grid State': '---', + 'Save': '---', + 'Saved Searches': '---', + 'Saving...': '---', + 'Search took ${count} seconds': '---', + 'Search': '---', + 'Select Hour': '---', + 'Select Minute': '---', + 'selected': '---', + 'Server Response ${count} seconds': '---', + 'Show/hide columns': '---', + 'Show': '---', + 'Size': '---', + 'Skip': '---', + 'Sorting took ${count} seconds': '---', + 'Type to search...': '---', + 'Type': '---', + 'Yes': '---', + 'Yesterday': '---', + 'Your remote data source record count has changed, reloading from the first record.': '---' + } +} +/* mQuery 0.7 (nightly) (8/15/2023, 11:44:12 AM), vitmalina@gmail.com */ +class Query { + static version = 0.8 + constructor(selector, context) { + this.context = context ?? document + let nodes = [] + if (Array.isArray(selector)) { + nodes = selector + } else if (selector instanceof Node || selector instanceof Window) { // any html element or Window + nodes = [selector] + } else if (selector instanceof Query) { + nodes = selector.nodes + } else if (typeof selector === 'string') { + if (typeof this.context.querySelector !== 'function') { + throw new Error('Invalid context') + } + nodes = Array.from(this.context.querySelectorAll(selector)) + } else if (selector == null) { + nodes = [] + } else { + // if selector is itterable, then try to create nodes from it, also supports jQuery + const arr = Array.from(selector ?? []) + if (typeof selector === 'object' && Array.isArray(arr)) { + nodes = arr + } else { + throw new Error(`Invalid selector "${selector}"`) + } + } + this.nodes = nodes + this.length = nodes.length + // map nodes to object propoerties + this.each((node, ind) => { + this[ind] = node + }) + } + static _fragment(html) { + const tmpl = document.createElement('template') + tmpl.innerHTML = html + tmpl.content.childNodes.forEach(node => { + const newNode = Query._scriptConvert(node) + if (newNode != node) { + tmpl.content.replaceChild(newNode, node) + } + }) + return tmpl.content + } + // innerHTML, append, etc. script tags will not be executed unless they are proper script tags + static _scriptConvert(node) { + const convert = (txtNode) => { + const doc = txtNode.ownerDocument + const scNode = doc.createElement('script') + scNode.text = txtNode.text + const attrs = txtNode.attributes + for (let i = 0; i < attrs.length; i++) { + scNode.setAttribute(attrs[i].name, attrs[i].value) + } + return scNode + } + if (node.tagName == 'SCRIPT') { + node = convert(node) + } + if (node.querySelectorAll) { + node.querySelectorAll('script').forEach(textNode => { + textNode.parentNode.replaceChild(convert(textNode), textNode) + }) + } + return node + } + static _fixProp(name) { + const fixes = { + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing', + class: 'className', + colspan: 'colSpan', + contenteditable: 'contentEditable', + for: 'htmlFor', + frameborder: 'frameBorder', + maxlength: 'maxLength', + readonly: 'readOnly', + rowspan: 'rowSpan', + tabindex: 'tabIndex', + usemap: 'useMap' + } + return fixes[name] ? fixes[name] : name + } + _insert(method, html) { + const nodes = [] + const len = this.length + if (len < 1) return + let self = this + // TODO: need good unit test coverage for this function + if (typeof html === 'string') { + this.each(node => { + const clone = Query._fragment(html) + nodes.push(...clone.childNodes) + node[method](clone) + }) + } else if (html instanceof Query) { + const single = (len == 1) // if inserting into a single container, then move it there + html.each(el => { + this.each(node => { + // if insert before a single node, just move new one, else clone and move it + const clone = (single ? el : el.cloneNode(true)) + nodes.push(clone) + node[method](clone) + Query._scriptConvert(clone) + }) + }) + if (!single) html.remove() + } else if (html instanceof Node) { // any HTML element + this.each(node => { + // if insert before a single node, just move new one, else clone and move it + const clone = (len === 1 ? html : Query._fragment(html.outerHTML)) + nodes.push(...(len === 1 ? [html] : clone.childNodes)) + node[method](clone) + }) + if (len > 1) html.remove() + } else { + throw new Error(`Incorrect argument for "${method}(html)". It expects one string argument.`) + } + if (method == 'replaceWith') { + self = new Query(nodes, this.context) // must return a new collection + } + return self + } + _save(node, name, value) { + node._mQuery = node._mQuery ?? {} + if (Array.isArray(value)) { + node._mQuery[name] = node._mQuery[name] ?? [] + node._mQuery[name].push(...value) + } else if (value != null) { + node._mQuery[name] = value + } else { + delete node._mQuery[name] + } + } + get(index) { + if (index < 0) index = this.length + index + const node = this[index] + if (node) { + return node + } + if (index != null) { + return null + } + return this.nodes + } + eq(index) { + if (index < 0) index = this.length + index + let nodes = [this[index]] + if (nodes[0] == null) nodes = [] + return new Query(nodes, this.context) // must return a new collection + } + then(fun) { + const ret = fun(this) + return ret != null ? ret : this + } + find(selector) { + const nodes = [] + this.each(node => { + const nn = Array.from(node.querySelectorAll(selector)) + if (nn.length > 0) { + nodes.push(...nn) + } + }) + return new Query(nodes, this.context) // must return a new collection + } + filter(selector) { + const nodes = [] + this.each(node => { + if (node === selector || + (typeof selector === 'string' && node.matches && node.matches(selector)) || + (typeof selector === 'function' && selector(node)) + ) { + nodes.push(node) + } + }) + return new Query(nodes, this.context) // must return a new collection + } + next() { + const nodes = [] + this.each(node => { + const nn = node.nextElementSibling + if (nn) { nodes.push(nn) } + }) + return new Query(nodes, this.context) // must return a new collection + } + prev() { + const nodes = [] + this.each(node => { + const nn = node.previousElementSibling + if (nn) { nodes.push(nn) } + }) + return new Query(nodes, this.context) // must return a new collection + } + shadow(selector) { + const nodes = [] + this.each(node => { + // select shadow root if available + if (node.shadowRoot) nodes.push(node.shadowRoot) + }) + const col = new Query(nodes, this.context) + return selector ? col.find(selector) : col + } + closest(selector) { + const nodes = [] + this.each(node => { + const nn = node.closest(selector) + if (nn) { + nodes.push(nn) + } + }) + return new Query(nodes, this.context) // must return a new collection + } + host(all) { + const nodes = [] + // find shadow root or body + const top = (node) => { + if (node.parentNode) { + return top(node.parentNode) + } else { + return node + } + } + const fun = (node) => { + const nn = top(node) + nodes.push(nn.host ? nn.host : nn) + if (nn.host && all) fun(nn.host) + } + this.each(node => { + fun(node) + }) + return new Query(nodes, this.context) // must return a new collection + } + parent(selector) { + return this.parents(selector, true) + } + parents(selector, firstOnly) { + const nodes = [] + const add = (node) => { + if (nodes.indexOf(node) == -1) { + nodes.push(node) + } + if (!firstOnly && node.parentNode) { + return add(node.parentNode) + } + } + this.each(node => { + if (node.parentNode) add(node.parentNode) + }) + const col = new Query(nodes, this.context) + return selector ? col.filter(selector) : col + } + add(more) { + const nodes = more instanceof Query ? more.nodes : (Array.isArray(more) ? more : [more]) + return new Query(this.nodes.concat(nodes), this.context) // must return a new collection + } + each(func) { + this.nodes.forEach((node, ind) => { func(node, ind, this) }) + return this + } + append(html) { + return this._insert('append', html) + } + prepend(html) { + return this._insert('prepend', html) + } + after(html) { + return this._insert('after', html) + } + before(html) { + return this._insert('before', html) + } + replace(html) { + return this._insert('replaceWith', html) + } + remove() { + // remove from dom, but keep in current query + this.each(node => { node.remove() }) + return this + } + css(key, value) { + let css = key + const len = arguments.length + if (len === 0 || (len === 1 && typeof key === 'string')) { + if (this[0]) { + const st = this[0].style + // do not do computedStyleMap as it is not what on immediate element + if (typeof key === 'string') { + const pri = st.getPropertyPriority(key) + return st.getPropertyValue(key) + (pri ? '!' + pri : '') + } else { + return Object.fromEntries( + this[0].style.cssText + .split(';') + .filter(a => !!a) // filter non-empty + .map(a => { + return a.split(':').map(a => a.trim()) // trim strings + }) + ) + } + } else { + return undefined + } + } else { + if (typeof key !== 'object') { + css = {} + css[key] = value + } + this.each((el, ind) => { + Object.keys(css).forEach(key => { + const imp = String(css[key]).toLowerCase().includes('!important') ? 'important' : '' + el.style.setProperty(key, String(css[key]).replace(/\!important/i, ''), imp) + }) + }) + return this + } + } + addClass(classes) { + this.toggleClass(classes, true) + return this + } + removeClass(classes) { + this.toggleClass(classes, false) + return this + } + toggleClass(classes, force) { + // split by comma or space + if (typeof classes === 'string') classes = classes.split(/[,\s]+/) + this.each(node => { + let classes2 = classes + // if not defined, remove all classes + if (classes2 == null && force === false) classes2 = Array.from(node.classList) + classes2.forEach(className => { + if (className !== '') { + let act = 'toggle' + if (force != null) act = force ? 'add' : 'remove' + node.classList[act](className) + } + }) + }) + return this + } + hasClass(classes) { + // split by comma or space + if (typeof classes === 'string') classes = classes.split(/[,\s]+/) + if (classes == null && this.length > 0) { + return Array.from(this[0].classList) + } + let ret = false + this.each(node => { + ret = ret || classes.every(className => { + return Array.from(node.classList ?? []).includes(className) + }) + }) + return ret + } + on(events, options, callback) { + if (typeof options === 'function') { + callback = options + options = undefined + } + let delegate + if (options?.delegate) { + delegate = options.delegate + delete options.delegate // not to pass to addEventListener + } + events = events.split(/[,\s]+/) // separate by comma or space + events.forEach(eventName => { + const [event, scope] = String(eventName).toLowerCase().split('.') + if (delegate) { + const fun = callback + callback = (event) => { + // event.target or any ancestors match delegate selector + const parent = query(event.target).parents(delegate) + if (parent.length > 0) { event.delegate = parent[0] } else { event.delegate = event.target } + if (event.target.matches(delegate) || parent.length > 0) { + fun(event) + } + } + } + this.each(node => { + this._save(node, 'events', [{ event, scope, callback, options }]) + node.addEventListener(event, callback, options) + }) + }) + return this + } + off(events, options, callback) { + if (typeof options === 'function') { + callback = options + options = undefined + } + events = (events ?? '').split(/[,\s]+/) // separate by comma or space + events.forEach(eventName => { + const [event, scope] = String(eventName).toLowerCase().split('.') + this.each(node => { + if (Array.isArray(node._mQuery?.events)) { + for (let i = node._mQuery.events.length - 1; i >= 0; i--) { + const evt = node._mQuery.events[i] + if (scope == null || scope === '') { + // if no scope, has to be exact match + if ((evt.event == event || event === '') && (evt.callback == callback || callback == null)) { + node.removeEventListener(evt.event, evt.callback, evt.options) + node._mQuery.events.splice(i, 1) + } + } else { + if ((evt.event == event || event === '') && evt.scope == scope) { + node.removeEventListener(evt.event, evt.callback, evt.options) + node._mQuery.events.splice(i, 1) + } + } + } + } + }) + }) + return this + } + trigger(name, options) { + let event + const mevent = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove'] + const kevent = ['keydown', 'keyup', 'keypress'] + if (name instanceof Event || name instanceof CustomEvent) { + // MouseEvent and KeyboardEvent are instances of Event, no need to explicitly add + event = name + } else if (mevent.includes(name)) { + event = new MouseEvent(name, options) + } else if (kevent.includes(name)) { + event = new KeyboardEvent(name, options) + } else { + event = new Event(name, options) + } + this.each(node => { node.dispatchEvent(event) }) + return this + } + attr(name, value) { + if (value === undefined && typeof name === 'string') { + return this[0] ? this[0].getAttribute(name) : undefined + } else { + let obj = {} + if (typeof name === 'object') obj = name; else obj[name] = value + this.each(node => { + Object.entries(obj).forEach(([nm, val]) => { node.setAttribute(nm, val) }) + }) + return this + } + } + removeAttr() { + this.each(node => { + Array.from(arguments).forEach(attr => { + node.removeAttribute(attr) + }) + }) + return this + } + prop(name, value) { + if (value === undefined && typeof name === 'string') { + return this[0] ? this[0][name] : undefined + } else { + let obj = {} + if (typeof name === 'object') obj = name; else obj[name] = value + this.each(node => { + Object.entries(obj).forEach(([nm, val]) => { + const prop = Query._fixProp(nm) + node[prop] = val + if (prop == 'innerHTML') { + Query._scriptConvert(node) + } + }) + }) + return this + } + } + removeProp() { + this.each(node => { + Array.from(arguments).forEach(prop => { delete node[Query._fixProp(prop)] }) + }) + return this + } + data(key, value) { + if (key instanceof Object) { + Object.entries(key).forEach(item => { this.data(item[0], item[1]) }) + return + } + if (key && key.indexOf('-') != -1) { + console.error(`Key "${key}" contains "-" (dash). Dashes are not allowed in property names. Use camelCase instead.`) + } + if (arguments.length < 2) { + if (this[0]) { + const data = Object.assign({}, this[0].dataset) + Object.keys(data).forEach(key => { + if (data[key].startsWith('[') || data[key].startsWith('{')) { + try { data[key] = JSON.parse(data[key]) } catch (e) {} + } + }) + return key ? data[key] : data + } else { + return undefined + } + } else { + this.each(node => { + if (value != null) { + node.dataset[key] = value instanceof Object ? JSON.stringify(value) : value + } else { + delete node.dataset[key] + } + }) + return this + } + } + removeData(key) { + if (typeof key === 'string') key = key.split(/[,\s]+/) + this.each(node => { + key.forEach(k => { delete node.dataset[k] }) + }) + return this + } + show() { + return this.toggle(true) + } + hide() { + return this.toggle(false) + } + toggle(force) { + return this.each(node => { + const prev = node.style.display + const dsp = getComputedStyle(node).display + const isHidden = (prev == 'none' || dsp == 'none') + if (isHidden && (force == null || force === true)) { // show + const def = node instanceof HTMLTableRowElement + ? 'table-row' + : node instanceof HTMLTableCellElement + ? 'table-cell' + : 'block' + node.style.display = node._mQuery?.prevDisplay ?? (prev == dsp && dsp != 'none' ? '' : def) + this._save(node, 'prevDisplay', null) + } + if (!isHidden && (force == null || force === false)) { // hide + if (dsp != 'none') this._save(node, 'prevDisplay', dsp) + node.style.setProperty('display', 'none') + } + }) + } + empty() { + return this.html('') + } + html(html) { + if (html instanceof HTMLElement) { + return this.empty().append(html) + } else { + return this.prop('innerHTML', html) + } + } + text(text) { + return this.prop('textContent', text) + } + val(value) { + return this.prop('value', value) // must be prop + } + change() { + return this.trigger('change') + } + click() { + return this.trigger('click') + } +} +// create a new object each time +const query = function(selector, context) { + // if a function, use as onload event + if (typeof selector === 'function') { + if (document.readyState == 'complete') { + selector() + } else { + window.addEventListener('load', selector) + } + } else { + return new Query(selector, context) + } +} +// str -> doc-fragment +query.html = (str) => { const frag = Query._fragment(str); return query(frag.children, frag) } +query.version = Query.version +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base, w2locale + * + * == TODO == + * - add w2utils.lang wrap for all captions in all buttons. + * - check transition (also with layout) + * - deprecate w2utils.tooltip + * + * == 2.0 changes + * - CSP - fixed inline events (w2utils.tooltip still has it) + * - transition returns a promise + * - removed jQuery + * - refactores w2utils.message() + * - added w2utils.confirm() + * - added isPlainObject + * - added stripSpaces + * - implemented marker - can now take an element or just html + * - cssPrefix - deprecated + * - w2utils.debounce + * - w2utils.prepareParams + * - w2utils.getStrHeight + * - w2utils.alrert() - same as w2utils.message() + * - w2utils.prompt() - similar to w2prompt + * - w2utils.normMenu(..., options) got options parameter that can have itemMap + * - w2utils.getNested() + * - w2utils.wait() - async timer + */ + +// variable that holds all w2ui objects +const w2ui = {} +class Utils { + constructor() { + this.version = '2.0.x' + this.tmp = {} + this.settings = this.extend({}, { + 'dataType': 'JSON', // can be HTTP, JSON, RESTFULL (case sensitive) + 'dateStartYear': 1950, // start year for date-picker + 'dateEndYear': 2030, // end year for date picker + 'macButtonOrder': false, // if true, Yes on the right side + 'warnNoPhrase': false // call console.warn if lang() encounters a missing phrase + }, w2locale, { phrases: null }), // if there are no phrases, then it is original language + this.i18nCompare = Intl.Collator().compare + this.hasLocalStorage = testLocalStorage() + // some internal variables + this.isMac = /Mac/i.test(navigator.platform) + this.isMobile = /(iphone|ipod|mobile|android)/i.test(navigator.userAgent) + this.isIOS = /(iphone|ipod|ipad)/i.test(navigator.platform) + this.isAndroid = /(android)/i.test(navigator.userAgent) + this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) + this.isFirefox = /(Firefox)/i.test(navigator.userAgent) + // Formatters: Primarily used in grid + this.formatters = { + 'number'(record, extra) { + if (extra == undefined) extra = record + let { value, params } = extra + if (parseInt(params) > 20) params = 20 + if (parseInt(params) < 0) params = 0 + if (value == null || value === '') return '' + return w2utils.formatNumber(parseFloat(value), params, true) + }, + 'float'(record, extra) { + return w2utils.formatters.number(record, extra) + }, + 'int'(record, extra) { + return w2utils.formatters.number(record, extra) + }, + 'money'(record, extra) { + if (extra == undefined) extra = record + const { value, params } = extra + if (value == null || value === '') return '' + const data = w2utils.formatNumber(Number(value), w2utils.settings.currencyPrecision, true) + return (w2utils.settings.currencyPrefix || '') + data + (w2utils.settings.currencySuffix || '') + }, + 'currency'(record, extra) { + return w2utils.formatters.money(record, extra) + }, + 'percent'(record, extra) { + if (extra == undefined) extra = record + const { value, params } = extra + if (value == null || value === '') return '' + return w2utils.formatNumber(value, params || 1) + '%' + }, + 'size'(record, extra) { + if (extra == undefined) extra = record + const { value, params } = extra + if (value == null || value === '') return '' + return w2utils.formatSize(parseInt(value)) + }, + 'date'(record, extra) { + if (extra == undefined) extra = record + let { value, params } = extra + if (params === '') params = w2utils.settings.dateFormat + if (value == null || value === 0 || value === '') return '' + let dt = w2utils.isDateTime(value, params, true) + if (dt === false) dt = w2utils.isDate(value, params, true) + return '' + w2utils.formatDate(dt, params) + '' + }, + 'datetime'(record, extra) { + if (extra == undefined) extra = record + let { value, params } = extra + if (params === '') params = w2utils.settings.datetimeFormat + if (value == null || value === 0 || value === '') return '' + let dt = w2utils.isDateTime(value, params, true) + if (dt === false) dt = w2utils.isDate(value, params, true) + return '' + w2utils.formatDateTime(dt, params) + '' + }, + 'time'(record, extra) { + if (extra == undefined) extra = record + let { value, params } = extra + if (params === '') params = w2utils.settings.timeFormat + if (params === 'h12') params = 'hh:mi pm' + if (params === 'h24') params = 'h24:mi' + if (value == null || value === 0 || value === '') return '' + let dt = w2utils.isDateTime(value, params, true) + if (dt === false) dt = w2utils.isDate(value, params, true) + return '' + w2utils.formatTime(value, params) + '' + }, + 'timestamp'(record, extra) { + if (extra == undefined) extra = record + let { value, params } = extra + if (params === '') params = w2utils.settings.datetimeFormat + if (value == null || value === 0 || value === '') return '' + let dt = w2utils.isDateTime(value, params, true) + if (dt === false) dt = w2utils.isDate(value, params, true) + return dt.toString ? dt.toString() : '' + }, + 'gmt'(record, extra) { + if (extra == undefined) extra = record + let { value, params } = extra + if (params === '') params = w2utils.settings.datetimeFormat + if (value == null || value === 0 || value === '') return '' + let dt = w2utils.isDateTime(value, params, true) + if (dt === false) dt = w2utils.isDate(value, params, true) + return dt.toUTCString ? dt.toUTCString() : '' + }, + 'age'(record, extra) { + if (extra == undefined) extra = record + const { value, params } = extra + if (value == null || value === 0 || value === '') return '' + let dt = w2utils.isDateTime(value, null, true) + if (dt === false) dt = w2utils.isDate(value, null, true) + return '' + w2utils.age(value) + (params ? (' ' + params) : '') + '' + }, + 'interval'(record, extra) { + if (extra == undefined) extra = record + const { value, params } = extra + if (value == null || value === 0 || value === '') return '' + return w2utils.interval(value) + (params ? (' ' + params) : '') + }, + 'toggle'(record, extra) { + if (extra == undefined) extra = record + const { value, params } = extra + return (value ? w2utils.lang('Yes') : '') + }, + 'password'(record, extra) { + if (extra == undefined) extra = record + const { value, params } = extra + let ret = '' + if (!value) return ret + for (let i = 0; i < value.length; i++) { + ret += '*' + } + return ret + } + } + return + function testLocalStorage() { + // test if localStorage is available, see issue #1282 + const str = 'w2ui_test' + try { + localStorage.setItem(str, str) + localStorage.removeItem(str) + return true + } catch (e) { + return false + } + } + } + isBin(val) { + const re = /^[0-1]+$/ + return re.test(val) + } + isInt(val) { + const re = /^[-+]?[0-9]+$/ + return re.test(val) + } + isFloat(val) { + if (typeof val === 'string') { + val = val.replace(new RegExp(this.settings.groupSymbol, 'g'), '') + .replace(this.settings.decimalSymbol, '.') + } + return (typeof val === 'number' || (typeof val === 'string' && val !== '')) && !isNaN(Number(val)) + } + isMoney(val) { + if (typeof val === 'object' || val === '') return false + if (this.isFloat(val)) return true + const se = this.settings + const re = new RegExp('^' + (se.currencyPrefix ? '\\' + se.currencyPrefix + '?' : '') + + '[-+]?' + (se.currencyPrefix ? '\\' + se.currencyPrefix + '?' : '') + + '[0-9]*[\\' + se.decimalSymbol + ']?[0-9]+' + (se.currencySuffix ? '\\' + se.currencySuffix + '?' : '') + '$', 'i') + if (typeof val === 'string') { + val = val.replace(new RegExp(se.groupSymbol, 'g'), '') + } + return re.test(val) + } + isHex(val) { + const re = /^(0x)?[0-9a-fA-F]+$/ + return re.test(val) + } + isAlphaNumeric(val) { + const re = /^[a-zA-Z0-9_-]+$/ + return re.test(val) + } + isEmail(val) { + const email = /^[a-zA-Z0-9._%\-+]+@[а-яА-Яa-zA-Z0-9.-]+\.[а-яА-Яa-zA-Z]+$/ + return email.test(val) + } + isIpAddress(val) { + const re = new RegExp('^' + + '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}' + + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' + + '$') + return re.test(val) + } + isDate(val, format, retDate) { + if (!val) return false + let dt = 'Invalid Date' + let month, day, year + if (format == null) format = this.settings.dateFormat + if (typeof val.getFullYear === 'function') { // date object + year = val.getFullYear() + month = val.getMonth() + 1 + day = val.getDate() + } else if (parseInt(val) == val && parseInt(val) > 0) { + val = new Date(parseInt(val)) + year = val.getFullYear() + month = val.getMonth() + 1 + day = val.getDate() + } else { + val = String(val) + // convert month formats + if (new RegExp('mon', 'ig').test(format)) { + format = format.replace(/month/ig, 'm').replace(/mon/ig, 'm').replace(/dd/ig, 'd').replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase() + val = val.replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase() + for (let m = 0, len = this.settings.fullmonths.length; m < len; m++) { + const t = this.settings.fullmonths[m] + val = val.replace(new RegExp(t, 'ig'), (parseInt(m) + 1)).replace(new RegExp(t.substr(0, 3), 'ig'), (parseInt(m) + 1)) + } + } + // format date + const tmp = val.replace(/-/g, '/').replace(/\./g, '/').toLowerCase().split('/') + const tmp2 = format.replace(/-/g, '/').replace(/\./g, '/').toLowerCase() + if (tmp2 === 'mm/dd/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2] } + if (tmp2 === 'm/d/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2] } + if (tmp2 === 'dd/mm/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2] } + if (tmp2 === 'd/m/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2] } + if (tmp2 === 'yyyy/dd/mm') { month = tmp[2]; day = tmp[1]; year = tmp[0] } + if (tmp2 === 'yyyy/d/m') { month = tmp[2]; day = tmp[1]; year = tmp[0] } + if (tmp2 === 'yyyy/mm/dd') { month = tmp[1]; day = tmp[2]; year = tmp[0] } + if (tmp2 === 'yyyy/m/d') { month = tmp[1]; day = tmp[2]; year = tmp[0] } + if (tmp2 === 'mm/dd/yy') { month = tmp[0]; day = tmp[1]; year = tmp[2] } + if (tmp2 === 'm/d/yy') { month = tmp[0]; day = tmp[1]; year = parseInt(tmp[2]) + 1900 } + if (tmp2 === 'dd/mm/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900 } + if (tmp2 === 'd/m/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900 } + if (tmp2 === 'yy/dd/mm') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900 } + if (tmp2 === 'yy/d/m') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900 } + if (tmp2 === 'yy/mm/dd') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900 } + if (tmp2 === 'yy/m/d') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900 } + } + if (!this.isInt(year)) return false + if (!this.isInt(month)) return false + if (!this.isInt(day)) return false + year = +year + month = +month + day = +day + dt = new Date(year, month - 1, day) + dt.setFullYear(year) + // do checks + if (month == null) return false + if (String(dt) === 'Invalid Date') return false + if ((dt.getMonth() + 1 !== month) || (dt.getDate() !== day) || (dt.getFullYear() !== year)) return false + if (retDate === true) return dt; else return true + } + isTime(val, retTime) { + // Both formats 10:20pm and 22:20 + if (val == null) return false + let max, am, pm + // -- process american format + val = String(val) + val = val.toUpperCase() + am = val.indexOf('AM') >= 0 + pm = val.indexOf('PM') >= 0 + const ampm = (pm || am) + if (ampm) max = 12; else max = 24 + val = val.replace('AM', '').replace('PM', '').trim() + // --- + const tmp = val.split(':') + let h = parseInt(tmp[0] || 0); const m = parseInt(tmp[1] || 0); const s = parseInt(tmp[2] || 0) + // accept edge case: 3PM is a good timestamp, but 3 (without AM or PM) is NOT: + if ((!ampm || tmp.length !== 1) && tmp.length !== 2 && tmp.length !== 3) { return false } + if (tmp[0] === '' || h < 0 || h > max || !this.isInt(tmp[0]) || tmp[0].length > 2) { return false } + if (tmp.length > 1 && (tmp[1] === '' || m < 0 || m > 59 || !this.isInt(tmp[1]) || tmp[1].length !== 2)) { return false } + if (tmp.length > 2 && (tmp[2] === '' || s < 0 || s > 59 || !this.isInt(tmp[2]) || tmp[2].length !== 2)) { return false } + // check the edge cases: 12:01AM is ok, as is 12:01PM, but 24:01 is NOT ok while 24:00 is (midnight; equivalent to 00:00). + // meanwhile, there is 00:00 which is ok, but 0AM nor 0PM are okay, while 0:01AM and 0:00AM are. + if (!ampm && max === h && (m !== 0 || s !== 0)) { return false } + if (ampm && tmp.length === 1 && h === 0) { return false } + if (retTime === true) { + if (pm && h !== 12) h += 12 // 12:00pm - is noon + if (am && h === 12) h += 12 // 12:00am - is midnight + return { + hours: h, + minutes: m, + seconds: s + } + } + return true + } + isDateTime(val, format, retDate) { + if (typeof val.getFullYear === 'function') { // date object + if (retDate !== true) return true + return val + } + const intVal = parseInt(val) + if (intVal === val) { + if (intVal < 0) return false + else if (retDate !== true) return true + else return new Date(intVal) + } + const tmp = String(val).indexOf(' ') + if (tmp < 0) { + if (String(val).indexOf('T') < 0 || String(new Date(val)) == 'Invalid Date') return false + else if (retDate !== true) return true + else return new Date(val) + } else { + if (format == null) format = this.settings.datetimeFormat + const formats = format.split('|') + const values = [val.substr(0, tmp), val.substr(tmp).trim()] + formats[0] = formats[0].trim() + if (formats[1]) formats[1] = formats[1].trim() + // check + const tmp1 = this.isDate(values[0], formats[0], true) + const tmp2 = this.isTime(values[1], true) + if (tmp1 !== false && tmp2 !== false) { + if (retDate !== true) return true + tmp1.setHours(tmp2.hours) + tmp1.setMinutes(tmp2.minutes) + tmp1.setSeconds(tmp2.seconds) + return tmp1 + } else { + return false + } + } + } + age(dateStr) { + let d1 + if (dateStr === '' || dateStr == null) return '' + if (typeof dateStr.getFullYear === 'function') { // date object + d1 = dateStr + } else if (parseInt(dateStr) == dateStr && parseInt(dateStr) > 0) { + d1 = new Date(parseInt(dateStr)) + } else { + d1 = new Date(dateStr) + } + if (String(d1) === 'Invalid Date') return '' + const d2 = new Date() + const sec = (d2.getTime() - d1.getTime()) / 1000 + let amount = '' + let type = '' + if (sec < 0) { + amount = 0 + type = 'sec' + } else if (sec < 60) { + amount = Math.floor(sec) + type = 'sec' + if (sec < 0) { amount = 0; type = 'sec' } + } else if (sec < 60 * 60) { + amount = Math.floor(sec / 60) + type = 'min' + } else if (sec < 24 * 60 * 60) { + amount = Math.floor(sec / 60 / 60) + type = 'hour' + } else if (sec < 30 * 24 * 60 * 60) { + amount = Math.floor(sec / 24 / 60 / 60) + type = 'day' + } else if (sec < 365 * 24 * 60 * 60) { + amount = Math.floor(sec / 30 / 24 / 60 / 60 * 10) / 10 + type = 'month' + } else if (sec < 365 * 4 * 24 * 60 * 60) { + amount = Math.floor(sec / 365 / 24 / 60 / 60 * 10) / 10 + type = 'year' + } else if (sec >= 365 * 4 * 24 * 60 * 60) { + // factor in leap year shift (only older then 4 years) + amount = Math.floor(sec / 365.25 / 24 / 60 / 60 * 10) / 10 + type = 'year' + } + return amount + ' ' + type + (amount > 1 ? 's' : '') + } + interval(value) { + let ret = '' + if (value < 100) { + ret = '< 0.01 sec' + } else if (value < 1000) { + ret = (Math.floor(value / 10) / 100) + ' sec' + } else if (value < 10000) { + ret = (Math.floor(value / 100) / 10) + ' sec' + } else if (value < 60000) { + ret = Math.floor(value / 1000) + ' secs' + } else if (value < 3600000) { + ret = Math.floor(value / 60000) + ' mins' + } else if (value < 86400000) { + ret = Math.floor(value / 3600000 * 10) / 10 + ' hours' + } else if (value < 2628000000) { + ret = Math.floor(value / 86400000 * 10) / 10 + ' days' + } else if (value < 3.1536e+10) { + ret = Math.floor(value / 2628000000 * 10) / 10 + ' months' + } else { + ret = Math.floor(value / 3.1536e+9) / 10 + ' years' + } + return ret + } + date(dateStr) { + if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return '' + let d1 = new Date(dateStr) + if (this.isInt(dateStr)) d1 = new Date(Number(dateStr)) // for unix timestamps + if (String(d1) === 'Invalid Date') return '' + const months = this.settings.shortmonths + const d2 = new Date() // today + const d3 = new Date() + d3.setTime(d3.getTime() - 86400000) // yesterday + const dd1 = months[d1.getMonth()] + ' ' + d1.getDate() + ', ' + d1.getFullYear() + const dd2 = months[d2.getMonth()] + ' ' + d2.getDate() + ', ' + d2.getFullYear() + const dd3 = months[d3.getMonth()] + ' ' + d3.getDate() + ', ' + d3.getFullYear() + const time = (d1.getHours() - (d1.getHours() > 12 ? 12 : 0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am') + const time2 = (d1.getHours() - (d1.getHours() > 12 ? 12 : 0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ':' + (d1.getSeconds() < 10 ? '0' : '') + d1.getSeconds() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am') + let dsp = dd1 + if (dd1 === dd2) dsp = time + if (dd1 === dd3) dsp = this.lang('Yesterday') + return '' + dsp + '' + } + formatSize(sizeStr) { + if (!this.isFloat(sizeStr) || sizeStr === '') return '' + sizeStr = parseFloat(sizeStr) + if (sizeStr === 0) return 0 + const sizes = ['Bt', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB'] + const i = parseInt(Math.floor(Math.log(sizeStr) / Math.log(1024))) + return (Math.floor(sizeStr / Math.pow(1024, i) * 10) / 10).toFixed(i === 0 ? 0 : 1) + ' ' + (sizes[i] || '??') + } + formatNumber(val, fraction, useGrouping) { + if (val == null || val === '' || typeof val === 'object') return '' + const options = { + minimumFractionDigits: parseInt(fraction), + maximumFractionDigits: parseInt(fraction), + useGrouping: !!useGrouping + } + if (fraction == null || fraction < 0) { + options.minimumFractionDigits = 0 + options.maximumFractionDigits = 20 + } + return parseFloat(val).toLocaleString(this.settings.locale, options) + } + formatDate(dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String + if (!format) format = this.settings.dateFormat + if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return '' + let dt = new Date(dateStr) + if (this.isInt(dateStr)) dt = new Date(Number(dateStr)) // for unix timestamps + if (String(dt) === 'Invalid Date') return '' + const year = dt.getFullYear() + const month = dt.getMonth() + const date = dt.getDate() + return format.toLowerCase() + .replace('month', this.settings.fullmonths[month]) + .replace('mon', this.settings.shortmonths[month]) + .replace(/yyyy/g, ('000' + year).slice(-4)) + .replace(/yyy/g, ('000' + year).slice(-4)) + .replace(/yy/g, ('0' + year).slice(-2)) + .replace(/(^|[^a-z$])y/g, '$1' + year) // only y's that are not preceded by a letter + .replace(/mm/g, ('0' + (month + 1)).slice(-2)) + .replace(/dd/g, ('0' + date).slice(-2)) + .replace(/th/g, (date == 1 ? 'st' : 'th')) + .replace(/th/g, (date == 2 ? 'nd' : 'th')) + .replace(/th/g, (date == 3 ? 'rd' : 'th')) + .replace(/(^|[^a-z$])m/g, '$1' + (month + 1)) // only y's that are not preceded by a letter + .replace(/(^|[^a-z$])d/g, '$1' + date) // only y's that are not preceded by a letter + } + formatTime(dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String + if (!format) format = this.settings.timeFormat + if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return '' + let dt = new Date(dateStr) + if (this.isInt(dateStr)) dt = new Date(Number(dateStr)) // for unix timestamps + if (this.isTime(dateStr)) { + const tmp = this.isTime(dateStr, true) + dt = new Date() + dt.setHours(tmp.hours) + dt.setMinutes(tmp.minutes) + } + if (String(dt) === 'Invalid Date') return '' + if (format == 'h12') format = 'hh:mi pm' + let type = 'am' + let hour = dt.getHours() + const h24 = dt.getHours() + let min = dt.getMinutes() + let sec = dt.getSeconds() + if (min < 10) min = '0' + min + if (sec < 10) sec = '0' + sec + if (format.indexOf('am') !== -1 || format.indexOf('pm') !== -1) { + if (hour >= 12) type = 'pm' + if (hour > 12) hour = hour - 12 + if (hour === 0) hour = 12 + } + return format.toLowerCase() + .replace('am', type) + .replace('pm', type) + .replace('hhh', (hour < 10 ? '0' + hour : hour)) + .replace('hh24', (h24 < 10 ? '0' + h24 : h24)) + .replace('h24', h24) + .replace('hh', hour) + .replace('mm', min) + .replace('mi', min) + .replace('ss', sec) + .replace(/(^|[^a-z$])h/g, '$1' + hour) // only y's that are not preceded by a letter + .replace(/(^|[^a-z$])m/g, '$1' + min) // only y's that are not preceded by a letter + .replace(/(^|[^a-z$])s/g, '$1' + sec) // only y's that are not preceded by a letter + } + formatDateTime(dateStr, format) { + let fmt + if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return '' + if (typeof format !== 'string') { + fmt = [this.settings.dateFormat, this.settings.timeFormat] + } else { + fmt = format.split('|') + fmt[0] = fmt[0].trim() + fmt[1] = (fmt.length > 1 ? fmt[1].trim() : this.settings.timeFormat) + } + // older formats support + if (fmt[1] === 'h12') fmt[1] = 'h:m pm' + if (fmt[1] === 'h24') fmt[1] = 'h24:m' + return this.formatDate(dateStr, fmt[0]) + ' ' + this.formatTime(dateStr, fmt[1]) + } + stripSpaces(html) { + if (html == null) return html + switch (typeof html) { + case 'number': + break + case 'string': + html = String(html).replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\s\s+/g, ' ').trim() + break + case 'object': + // does not modify original object, but creates a copy + if (Array.isArray(html)) { + html = this.extend([], html) + html.forEach((key, ind) => { + html[ind] = this.stripSpaces(key) + }) + } else { + html = this.extend({}, html) + Object.keys(html).forEach(key => { + html[key] = this.stripSpaces(html[key]) + }) + } + break + } + return html + } + stripTags(html) { + if (html == null) return html + switch (typeof html) { + case 'number': + break + case 'string': + html = String(html).replace(/<(?:[^>=]|='[^']*'|="[^"]*"|=[^'"][^\s>]*)*>/ig, '') + break + case 'object': + // does not modify original object, but creates a copy + if (Array.isArray(html)) { + html = this.extend([], html) + html.forEach((key, ind) => { + html[ind] = this.stripTags(key) + }) + } else { + html = this.extend({}, html) + Object.keys(html).forEach(key => { + html[key] = this.stripTags(html[key]) + }) + } + break + } + return html + } + encodeTags(html) { + if (html == null) return html + switch (typeof html) { + case 'number': + break + case 'string': + html = String(html).replace(/&/g, '&').replace(/>/g, '>').replace(/ { + html[ind] = this.encodeTags(key) + }) + } else { + html = this.extend({}, html) + Object.keys(html).forEach(key => { + html[key] = this.encodeTags(html[key]) + }) + } + break + } + return html + } + decodeTags(html) { + if (html == null) return html + switch (typeof html) { + case 'number': + break + case 'string': + html = String(html).replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/&/g, '&') + break + case 'object': + // does not modify original object, but creates a copy + if (Array.isArray(html)) { + html = this.extend([], html) + html.forEach((key, ind) => { + html[ind] = this.decodeTags(key) + }) + } else { + html = this.extend({}, html) + Object.keys(html).forEach(key => { + html[key] = this.decodeTags(html[key]) + }) + } + break + } + return html + } + escapeId(id) { + // This logic is borrowed from jQuery + if (id === '' || id == null) return '' + const re = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g + return (id + '').replace(re, (ch, asCodePoint) => { + if (asCodePoint) { + if (ch === '\0') return '\uFFFD' + return ch.slice(0, -1) + '\\' + ch.charCodeAt(ch.length - 1).toString(16) + ' ' + } + return '\\' + ch + }) + } + unescapeId(id) { + // This logic is borrowed from jQuery + if (id === '' || id == null) return '' + const re = /\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\([^\r\n\f])/g + return id.replace(re, (escape, nonHex) => { + const high = '0x' + escape.slice(1) - 0x10000 + return nonHex || (high < 0 + ? String.fromCharCode(high + 0x10000) + : String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00)) + }) + } + base64encode(str) { + // Fast Native support in Chrome since 2010 + const utf8Bytes = new TextEncoder().encode(str) + let binaryString = '' + for (const byte of utf8Bytes) { + binaryString += String.fromCharCode(byte) + } + return btoa(binaryString) + } + base64decode(encodedStr) { + // Fast Native support in Chrome since 2010 + const binaryString = atob(encodedStr) + const utf8Bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + utf8Bytes[i] = binaryString.charCodeAt(i) + } + return new TextDecoder().decode(utf8Bytes) + } + async sha256(str) { + const utf8 = new TextEncoder().encode(str) + return crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => { + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((bytes) => bytes.toString(16).padStart(2, '0')).join('') + }) + } + transition(div_old, div_new, type, callBack) { + return new Promise((resolve, reject) => { + const styles = getComputedStyle(div_old) + const width = parseInt(styles.width) + const height = parseInt(styles.height) + const time = 0.5 + if (!div_old || !div_new) { + console.log('ERROR: Cannot do transition when one of the divs is null') + return + } + div_old.parentNode.style.cssText += 'perspective: 900px; overflow: hidden;' + div_old.style.cssText += '; position: absolute; z-index: 1019; backface-visibility: hidden' + div_new.style.cssText += '; position: absolute; z-index: 1020; backface-visibility: hidden' + switch (type) { + case 'slide-left': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)' + div_new.style.cssText += 'overflow: hidden; transform: translate3d(' + width + 'px, 0, 0)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: translate3d(0, 0, 0)' + div_old.style.cssText += 'transition: ' + time + 's; transform: translate3d(-' + width + 'px, 0, 0)' + }, 1) + break + case 'slide-right': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)' + div_new.style.cssText += 'overflow: hidden; transform: translate3d(-' + width + 'px, 0, 0)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: translate3d(0px, 0, 0)' + div_old.style.cssText += 'transition: ' + time + 's; transform: translate3d(' + width + 'px, 0, 0)' + }, 1) + break + case 'slide-down': + // init divs + div_old.style.cssText += 'overflow: hidden; z-index: 1; transform: translate3d(0, 0, 0)' + div_new.style.cssText += 'overflow: hidden; z-index: 0; transform: translate3d(0, 0, 0)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: translate3d(0, 0, 0)' + div_old.style.cssText += 'transition: ' + time + 's; transform: translate3d(0, ' + height + 'px, 0)' + }, 1) + break + case 'slide-up': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)' + div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, ' + height + 'px, 0)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: translate3d(0, 0, 0)' + div_old.style.cssText += 'transition: ' + time + 's; transform: translate3d(0, 0, 0)' + }, 1) + break + case 'flip-left': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: rotateY(0deg)' + div_new.style.cssText += 'overflow: hidden; transform: rotateY(-180deg)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: rotateY(0deg)' + div_old.style.cssText += 'transition: ' + time + 's; transform: rotateY(180deg)' + }, 1) + break + case 'flip-right': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: rotateY(0deg)' + div_new.style.cssText += 'overflow: hidden; transform: rotateY(180deg)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: rotateY(0deg)' + div_old.style.cssText += 'transition: ' + time + 's; transform: rotateY(-180deg)' + }, 1) + break + case 'flip-down': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: rotateX(0deg)' + div_new.style.cssText += 'overflow: hidden; transform: rotateX(180deg)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: rotateX(0deg)' + div_old.style.cssText += 'transition: ' + time + 's; transform: rotateX(-180deg)' + }, 1) + break + case 'flip-up': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: rotateX(0deg)' + div_new.style.cssText += 'overflow: hidden; transform: rotateX(-180deg)' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: rotateX(0deg)' + div_old.style.cssText += 'transition: ' + time + 's; transform: rotateX(180deg)' + }, 1) + break + case 'pop-in': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)' + div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); transform: scale(.8); opacity: 0;' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; transform: scale(1); opacity: 1;' + div_old.style.cssText += 'transition: ' + time + 's;' + }, 1) + break + case 'pop-out': + // init divs + div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); transform: scale(1); opacity: 1;' + div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); opacity: 0;' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; opacity: 1;' + div_old.style.cssText += 'transition: ' + time + 's; transform: scale(1.7); opacity: 0;' + }, 1) + break + default: + // init divs + div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)' + div_new.style.cssText += 'overflow: hidden; translate3d(0, 0, 0); opacity: 0;' + query(div_new).show() + // -- need a timing function because otherwise not working + setTimeout(() => { + div_new.style.cssText += 'transition: ' + time + 's; opacity: 1;' + div_old.style.cssText += 'transition: ' + time + 's' + }, 1) + break + } + setTimeout(() => { + if (type === 'slide-down') { + query(div_old).css('z-index', '1019') + query(div_new).css('z-index', '1020') + } + if (div_new) { + query(div_new) + .css({ 'opacity': '1' }) + .css({ 'transition': '', 'transform': '' }) + } + if (div_old) { + query(div_old) + .css({ 'opacity': '1' }) + .css({ 'transition': '', 'transform': '' }) + } + if (typeof callBack === 'function') callBack() + resolve() + }, time * 1000) + }) + } + lock(box, options = {}) { + if (box == null) return + if (typeof options === 'string') { + options = { msg: options } + } + if (arguments[2]) { + options.spinner = arguments[2] + } + options = this.extend({ + spinner: false + }, options) + // for backward compatibility + if (box?.[0] instanceof Node) { + box = Array.isArray(box) ? box : box.get() + } + if (!options.msg && options.msg !== 0) options.msg = '' + this.unlock(box) + const el = query(box).get(0) + const pWidth = el.scrollWidth + const pHeight = el.scrollHeight + // if it is body and only has absolute elements, its height will be 0, need to lock entire window + let style = `height: ${pHeight}px; width: ${pWidth}px` + if (el.tagName == 'BODY') { + style = 'position: fixed; right: 0; bottom: 0;' + } + query(box).prepend( + `
` + + '
' + ) + const $lock = query(box).find('.w2ui-lock') + const $mess = query(box).find('.w2ui-lock-msg') + if (!options.msg) { + $mess.css({ + 'background-color': 'transparent', + 'background-image': 'none', + 'border': '0px', + 'box-shadow': 'none' + }) + } + if (options.spinner === true) { + options.msg = `
` + + options.msg + } + if (options.msg) { + $mess.html(options.msg).css('display', 'block') + } else { + $mess.remove() + } + if (options.opacity != null) { + $lock.css('opacity', options.opacity) + } + $lock.css({ display: 'block' }) + if (options.bgColor) { + $lock.css({ 'background-color': options.bgColor }) + } + const styles = getComputedStyle($lock.get(0)) + const opacity = styles.opacity ?? 0.15 + $lock + .on('mousedown', function() { + if (typeof options.onClick === 'function') { + options.onClick() + } else { + $lock.css({ + 'transition': '.2s', + 'opacity': opacity * 1.5 + }) + } + }) + .on('mouseup', function() { + if (typeof options.onClick !== 'function') { + $lock.css({ + 'transition': '.2s', + 'opacity': opacity + }) + } + }) + .on('mousewheel', function(event) { + if (event) { + event.stopPropagation() + event.preventDefault() + } + }) + } + lockSetTxt(box, text) { + if (box == null) return + if (box?.[0] instanceof Node) { + box = Array.isArray(box) ? box : box.get() + } + query(box).find('.w2ui-lock-msg').text(text) + } + unlock(box, speed) { + if (box == null) return + clearTimeout(box._prevUnlock) + // for backward compatibility + if (box?.[0] instanceof Node) { + box = Array.isArray(box) ? box : box.get() + } + if (this.isInt(speed) && speed > 0) { + query(box).find('.w2ui-lock').css({ + transition: (speed / 1000) + 's', + opacity: 0 + }) + const _box = query(box).get(0) + clearTimeout(_box._prevUnlock) + _box._prevUnlock = setTimeout(() => { + query(box).find('.w2ui-lock').remove() + }, speed) + query(box).find('.w2ui-lock-msg').remove() + } else { + query(box).find('.w2ui-lock').remove() + query(box).find('.w2ui-lock-msg').remove() + } + } + /** + * Opens a context message, similar in parameters as w2popup.open() + * + * Sample Calls + * w2utils.message({ box: '#div', text: 'message' }).ok(() => {}) + * w2utils.message({ box: '#div', text: 'message', width: 300 }).ok(() => {}) + * w2utils.message({ box: '#div', text: 'message', actions: ['Save'] }).Save(() => {}) + * + * Used in w2grid, w2form, w2layout (should be in w2popup too) + * should be called with .call(...) method + * + * @param where = { + * box, // where to open + * after, // title if any, adds title heights + * param // additional parameters, used in layouts for panel + * } + * @param options { + * width, // (int), width in px, if negative, then it is maxWidth - width + * height, // (int), height in px, if negative, then it is maxHeight - height + * text, // centered text + * title, // title of the message + * body, // body of the message + * buttons, // buttons of the message + * html, // if body & buttons are not defined, then html is the entire message + * focus, // int or id with a selector, default is 0 + * hideOn, // ['esc', 'click'], default is ['esc'] + * actions, // array of actions (only if buttons is not defined) + * onOpen, // event when opened + * onClose, // event when closed + * onAction, // event on action + * } + */ + message(where, options) { + let closeTimer, openTimer, edata + const removeLast = () => { + const msgs = query(where?.box).find('.w2ui-message') + if (msgs.length == 0) return // no messages already + options = msgs.get(0)._msg_options || {} + if (typeof options?.close === 'function') { + options.close() + } + } + const closeComplete = (options) => { + const focus = options.box._msg_prevFocus + if (query(where.box).find('.w2ui-message').length <= 1) { + if (where.owner) { + where.owner.unlock(where.param, 150) + } else { + this.unlock(where.box, 150) + } + } else { + query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex - 1}`).css('z-index', 15000) + } + if (focus) { + const msg = query(focus).closest('.w2ui-message') + if (msg.length > 0) { + const opt = msg.get(0)._msg_options + opt.setFocus(focus) + } else { + focus.focus() + } + } else { + if (typeof where.owner?.focus === 'function') where.owner.focus() + } + query(options.box).remove() + if (options.msgIndex === 0) { + head.css('z-index', options.tmp.zIndex) + query(where.box).css('overflow', options.tmp.overflow) + } + // event after + if (options.trigger) { + edata.finish() + } + } + if (typeof options === 'string' || typeof options === 'number') { + options = { + width: (String(options).length < 300 ? 350 : 550), + height: (String(options).length < 300 ? 170 : 250), + text: String(options) + } + } + if (arguments.length == 1 || options == null) { + options = where + } + options ??= {} + if (options == null || (options.text === '' || options.text == null) && (options.body === '' || options.body == null)) { + removeLast() + return + } + if (options.title != null) options.title = `
${options.title}
` + if (options.text != null) options.body = `
${options.text}
` + if (options.width == null) options.width = 330 + if (options.height == null) options.height = 130 + if (options.hideOn == null) options.hideOn = ['esc'] + options.cancelAction ??= 'Ok' + // mix in events + if (options.on == null) { + const opts = options + options = new w2base() + w2utils.extend(options, opts) // needs to be w2utils + } + options.on('open', (event) => { + w2utils.bindEvents(query(options.box).find('.w2ui-eaction'), options) // options is w2base object + query(event.detail.box).find('button, input, textarea, [name=hidden-first]') + .off('.message') + .on('keydown.message', function(evt) { + if (evt.keyCode == 27 && options.hideOn.includes('esc')) { + if (options.cancelAction) { + options.action(options.cancelAction) + } else { + options.close() + } + } + }) + // timeout is needed because messages opens over 0.3 seconds + setTimeout(() => options.setFocus(options.focus), 300) + }) + options.off('.prom') + const prom = { + self: options, + action(callBack) { + options.on('action.prom', callBack) + return prom + }, + close(callBack) { + options.on('close.prom', callBack) + return prom + }, + open(callBack) { + options.on('open.prom', callBack) + return prom + }, + then(callBack) { + options.on('open:after.prom', callBack) + return prom + } + } + if (options.actions == null && options.buttons == null && options.html == null) { + options.actions = { Ok(event) { event.detail.self.close() } } + } + options.off('.buttons') + if (options.actions != null) { + options.buttons = '' + Object.keys(options.actions).forEach((action) => { + const handler = options.actions[action] + let btnAction = action + if (typeof handler === 'function') { + options.buttons += `` + } + if (typeof handler === 'object') { + options.buttons += `` + btnAction = Array.isArray(options.actions) ? handler.text : action + } + if (typeof handler === 'string') { + options.buttons += `` + btnAction = handler + } + if (typeof btnAction === 'string') { + btnAction = btnAction[0].toLowerCase() + btnAction.substr(1).replace(/\s+/g, '') + } + prom[btnAction] = function(callBack) { + options.on('action.buttons', (event) => { + const target = event.detail.action[0].toLowerCase() + event.detail.action.substr(1).replace(/\s+/g, '') + if (target == btnAction) callBack(event) + }) + return prom + } + }) + } + // trim if any + Array('html', 'body', 'buttons').forEach(param => { + options[param] = String(options[param] ?? '').trim() + }) + if (options.body !== '' || options.buttons !== '') { + + options.html = ` +
${options.body || ''}
+
${options.buttons || ''}
+ ` + if (options.title) { + options.html = `
${options.title}
${options.html}` + } + } + let styles = getComputedStyle(query(where.box).get(0)) + const pWidth = parseFloat(styles.width) + const pHeight = parseFloat(styles.height) + let titleHeight = 0 + if (query(where.after).length > 0) { + styles = getComputedStyle(query(where.after).get(0)) + titleHeight = parseInt(styles.display != 'none' ? parseInt(styles.height) : 0) + } + if (options.width > pWidth) options.width = pWidth - 10 + if (options.height > pHeight - titleHeight) options.height = pHeight - 10 - titleHeight + options.originalWidth = options.width + options.originalHeight = options.height + if (parseInt(options.width) < 0) options.width = pWidth + options.width + if (parseInt(options.width) < 10) options.width = 10 + if (parseInt(options.height) < 0) options.height = pHeight + options.height - titleHeight + if (parseInt(options.height) < 10) options.height = 10 + // negative value means margin + if (options.originalHeight < 0) options.height = pHeight + options.originalHeight - titleHeight + if (options.originalWidth < 0) options.width = pWidth + options.originalWidth * 2 // x 2 because there is left and right margin + const head = query(where.box).find(where.after) // needed for z-index manipulations + if (!options.tmp) { + options.tmp = { + zIndex: head.css('z-index'), + overflow: styles.overflow + } + } + // remove message + if (options.html === '' && options.body === '' && options.buttons === '') { + removeLast() + } else { + options.msgIndex = query(where.box).find('.w2ui-message').length + if (options.msgIndex === 0 && typeof this.lock === 'function') { + query(where.box).css('overflow', 'hidden') + if (where.owner) { // where.praram is used in the panel + where.owner.lock(where.param) + } else { + this.lock(where.box) + } + } + // send back previous messages + query(where.box).find('.w2ui-message').css('z-index', 1390) + head.css('z-index', 1501) + // add message + const content = ` +
+ + ${options.html} + +
` + if (query(where.after).length > 0) { + query(where.box).find(where.after).after(content) + } else { + query(where.box).prepend(content) + } + options.box = query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`)[0] + w2utils.bindEvents(options.box, this) + query(options.box) + .addClass('animating') + // remember options and prev focus + options.box._msg_options = options + options.box._msg_prevFocus = document.activeElement + // timeout is needs so that callBacks are setup + setTimeout(() => { + // before event + edata = options.trigger('open', { target: this.name, box: options.box, self: options }) + if (edata.isCancelled === true) { + query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`).remove() + if (options.msgIndex === 0) { + head.css('z-index', options.tmp.zIndex) + query(where.box).css('overflow', options.tmp.overflow) + } + return + } + // slide down + query(options.box).css({ + transition: '0.3s', + transform: 'translateY(0px)' + }) + }, 0) + // timeout is needed so that animation can finish + openTimer = setTimeout(() => { + // has to be on top of lock + query(where.box) + .find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`) + .removeClass('animating') + .css({ 'transition': '0s' }) + // event after + edata.finish() + }, 300) + } + // action handler + options.action = (action, event) => { + let click = options.actions?.[action] + if (click instanceof Object && click.onClick) click = click.onClick + // event before + const edata = options.trigger('action', { target: this.name, action, self: options, + originalEvent: event, value: options.input ? options.input.value : null }) + if (edata.isCancelled === true) return + // default actions + if (typeof click === 'function') click(edata) + // event after + edata.finish() + } + options.close = () => { + edata = options.trigger('close', { target: 'self', box: options.box, self: options }) + if (edata.isCancelled === true) return + clearTimeout(openTimer) + if (query(options.box).hasClass('animating')) { + clearTimeout(closeTimer) + closeComplete(options) + return + } + // default behavior + query(options.box) + .addClass('w2ui-closing animating') + .css({ + 'transition': '0.15s', + 'transform': 'translateY(-' + options.height + 'px)' + }) + if (options.msgIndex !== 0) { + // previous message + query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex - 1}`).css('z-index', 1499) + } + closeTimer = setTimeout(() => { closeComplete(options) }, 150) + } + options.setFocus = (focus) => { + // in message or popup + const cnt = query(where.box).find('.w2ui-message').length - 1 + const box = query(where.box).find(`#w2ui-message-${where.owner?.name}-${cnt}`) + const sel = 'input, button, select, textarea, [contentEditable], .w2ui-input' + if (focus != null) { + const el = isNaN(focus) + ? box.find(sel).filter(focus).get(0) + : box.find(sel).get(focus) + el?.focus() + } else { + box.find('[name=hidden-first]').get(0)?.focus() + } + // clear focus if there are other messages + query(where.box) + .find('.w2ui-message') + .find(sel + ',[name=hidden-first],[name=hidden-last]') + .off('.keep-focus') + // keep focus/blur inside popup + query(box) + .find(sel + ',[name=hidden-first],[name=hidden-last]') + .on('blur.keep-focus', function(event) { + setTimeout(() => { + const focus = document.activeElement + const inside = query(box).find(sel).filter(focus).length > 0 + const name = query(focus).attr('name') + if (!inside && focus && focus !== document.body) { + query(box).find(sel).get(0)?.focus() + } + if (name == 'hidden-last') { + query(box).find(sel).get(0)?.focus() + } + if (name == 'hidden-first') { + query(box).find(sel).get(-1)?.focus() + } + }, 1) + }) + } + return prom + } + alert(where, options) { + return this.message(...arguments) + } + /** + * Shows a prompt as a context message. It will use same where: { box: ... } as w2utils.message() function + * but it will have options similar to w2prompt dialog + * + * Example: + * - w2utils.conrirm({ + * box: '#custom', + * text: 'Some message' + * }) + * .yes(event => console.log(event)) + */ + confirm(where, options) { + if (['string', 'number'].includes(typeof options)) { + options = { text: options } + } + if (arguments.length == 1) { + options = where + } + w2utils.normButtons(options, { yes: 'Yes', no: 'No' }) + options.cancelAction ??= 'No' + const prom = w2utils.message(where, options) + if (prom) { + prom.action(event => { + event.detail.self.close() + }) + } + return prom + } + /** + * Shows a prompt as a context message. It will use same where: { box: ... } as w2utils.message() function + * but it will have options similar to w2prompt dialog + * + * Example: + * - w2utils.prompt({ + * box: '#custom', + * label: 'Enter Name', + * textarea: false, + * attrs: 'style="border: 1px solid red"' + * }) + * .ok(event => console.log(event)) + */ + prompt(where, options) { + let prom + if (['string', 'number'].includes(typeof options)) { + options = { label: options } + } + if (arguments.length == 1) { + options = where + } + options.cancelAction ??= 'Cancel' + if (options.label) { + options.focus = 0 // the input should be in focus, which is first in the popup + options.body = (options.textarea + ? `
+
${options.label}
+ +
` + : `
+ + +
` + ) + } + w2utils.normButtons(options, { ok: w2utils.lang('Ok'), cancel: w2utils.lang('Cancel') }) + prom = w2utils.message(where, options) + if (prom) { + prom.change = function(callBack) { + prom.self.on('change.prom', callBack) + return prom + } + prom + .action(event => { + event.detail.self.close() + }) + .then(event => { + event.detail.self.input = query(event.detail.box).find('#w2prompt').get(0) + query(event.detail.box) + .find('#w2prompt') + .on('keydown', event => { + if (event.keyCode == 13 && event.shiftKey === false) { + event.preventDefault() + } + }) + .on('keyup', event => { + const edata = prom.self.trigger('change', { value: event.target.value, input: event.target, originalEvent: event }) + if (event.keyCode == 13 && event.shiftKey === false) { + prom.self.action('Ok', event) + } + edata.finish() + }) + }) + } + return prom + } + /** + * Normalizes yes, no buttons for confirmation dialog + * + * @param {*} options + * @returns options + */ + normButtons(options, btn) { + options.actions = options.actions ?? {} + const btns = Object.keys(btn) + btns.forEach(name => { + const action = options['btn_' + name] + if (action) { + btn[name] = { + text: w2utils.lang(action.text ?? btn[name] ?? ''), + class: action.class ?? '', + style: action.style ?? '', + attrs: action.attrs ?? '' + } + delete options['btn_' + name] + } + Array('text', 'class', 'style', 'attrs').forEach(suffix => { + if (options[name + '_' + suffix]) { + if (typeof btn[name] === 'string') { + btn[name] = { text: btn[name] } + } + btn[name][suffix] = options[name + '_' + suffix] + delete options[name + '_' + suffix] + } + }) + }) + if (btns.includes('yes') && btns.includes('no')) { + if (w2utils.settings.macButtonOrder) { + w2utils.extend(options.actions, { no: btn.no, yes: btn.yes }) + } else { + w2utils.extend(options.actions, { yes: btn.yes, no: btn.no }) + } + } + if (btns.includes('ok') && btns.includes('cancel')) { + if (w2utils.settings.macButtonOrder) { + w2utils.extend(options.actions, { cancel: btn.cancel, ok: btn.ok }) + } else { + w2utils.extend(options.actions, { ok: btn.ok, cancel: btn.cancel }) + } + } + return options + } + /** + * Shows small notification message at the bottom of the page, or containter that you specify + * in options.where (could be element or a selector) + * + * w2utils.notify('Document saved') + * w2utils.notify('Mesage sent ${udon}', { actions: { undo: function () {...} }}) + * + * @param {String/Object} options can be { + * text: string, // message, can be html + * where: el/selector, // element or selector where to show, default is document.body + * timeout: int, // timeout when to hide, if 0 - indefinite + * error: boolean, // add error clases + * class: string, // additional class strings + * actions: object // object with action functions, it should correspot to templated text: '... ${action} ...' + * } + * @returns promise + */ + notify(text, options) { + return new Promise(resolve => { + if (typeof text === 'object') { + options = text + text = options.text + } + options = options || {} + options.where = options.where ?? document.body + options.timeout = options.timeout ?? 15_000 // 15 secodns or will be hidden on route change + if (typeof this.tmp.notify_resolve === 'function') { + this.tmp.notify_resolve() + query(this.tmp.notify_where).find('#w2ui-notify').remove() + } + this.tmp.notify_resolve = resolve + this.tmp.notify_where = options.where + clearTimeout(this.tmp.notify_timer) + if (text) { + if (typeof options.actions === 'object') { + const actions = {} + Object.keys(options.actions).forEach(action => { + actions[action] = `${action}` + }) + text = this.execTemplate(text, actions) + } + const html = ` +
+
+ ${text} + +
+
` + query(options.where).append(html) + query(options.where).find('#w2ui-notify').find('.w2ui-notify-close') + .on('click', event => { + query(options.where).find('#w2ui-notify').remove() + resolve() + }) + if (options.actions) { + query(options.where).find('#w2ui-notify .w2ui-notify-link') + .on('click', event => { + const value = query(event.target).attr('value') + options.actions[value]() + query(options.where).find('#w2ui-notify').remove() + resolve() + }) + } + if (options.timeout > 0) { + this.tmp.notify_timer = setTimeout(() => { + query(options.where).find('#w2ui-notify').remove() + resolve() + }, options.timeout) + } + } + }) + } + getSize(el, type) { + el = query(el) // for backward compatibility + let ret = 0 + if (el.length > 0) { + el = el[0] + const styles = getComputedStyle(el) + switch (type) { + case 'width' : + ret = parseFloat(styles.width) + if (styles.width === 'auto') ret = 0 + break + case 'height' : + ret = parseFloat(styles.height) + if (styles.height === 'auto') ret = 0 + break + default: + ret = parseFloat(styles[type] ?? 0) || 0 + break + } + } + return ret + } + getStrWidth(str, styles, raw) { + let div = query('body > #_tmp_width') + if (div.length === 0) { + query('body').append('
') + div = query('body > #_tmp_width') + } + div.html(raw ? str : this.encodeTags(str ?? '')).attr('style', `position: absolute; top: -9000px; ${styles || ''}`) + return div[0].clientWidth + } + getStrHeight(str, styles, raw) { + let div = query('body > #_tmp_width') + if (div.length === 0) { + query('body').append('
') + div = query('body > #_tmp_width') + } + div.html(raw ? str : this.encodeTags(str ?? '')).attr('style', `position: absolute; top: -9000px; ${styles || ''}`) + return div[0].clientHeight + } + execTemplate(str, replace_obj) { + if (typeof str !== 'string' || !replace_obj || typeof replace_obj !== 'object') { + return str + } + return str.replace(/\${([^}]+)?}/g, function($1, $2) { return replace_obj[$2] || $2 }) + } + marker(el, items, options = { onlyFirst: false, wholeWord: false }) { + options.tag ??= 'span' + options.class ??= 'w2ui-marker' + options.raplace = (matched) => `<${options.tag} class="${options.class}">${matched}` + if (!Array.isArray(items)) { + if (items != null && items !== '') { + items = [items] + } else { + items = [] + } + } + if (typeof el === 'string') { + _clearMerkers(el) + items.forEach(item => { + el = _replace(el, item, options.raplace) + }) + } else { + query(el).each(el => { + _clearMerkers(el) + items.forEach(item => { + el.innerHTML = _replace(el.innerHTML, item, options.raplace) + }) + }) + } + return el + function _replace(html, term, replaceWith) { + const ww = options.wholeWord + if (typeof term !== 'string') term = String(term) + // escape regex special chars + term = term + .replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + .replace(/&/g, '&') + .replace(//g, '<') + // only outside tags + // and only outside of quotes + // let regex = new RegExp((ww ? '\\b' : '') + term + (ww ? '\\b' : '') + '(?=([a-z-0-9]+="[^"]*")*?[^"]+$)' + '(?!([^<]+)?>)', 'i' + (!options.onlyFirst ? 'g' : '')) + // -- the one above would not match inside html tags + const regex = new RegExp((ww ? '\\b' : '') + term + (ww ? '\\b' : '') + '(?![^<]*>)', 'i' + (!options.onlyFirst ? 'g' : '')) + return html = html.replace(regex, replaceWith) + } + function _clearMerkers(el) { + const markerRE = new RegExp(`<${options.tag}[^>]*class=["']${options.class.replace(/-/g, '\\-')}["'][^>]*>([\\s\\S]*?)<\\/${options.tag}>`, 'ig') + if (typeof el === 'string') { + while (el.indexOf(`<${options.tag} class="${options.class}"`) !== -1) { + el = el.replace(markerRE, '$1') // unmark + } + } else { + while (el.innerHTML.indexOf(`<${options.tag} class="${options.class}"`) !== -1) { + el.innerHTML = el.innerHTML.replace(markerRE, '$1') // unmark + } + } + } + } + lang(phrase, params) { + if (!phrase || this.settings.phrases == null || // if no phrases at all + typeof phrase !== 'string' || '<=>='.includes(phrase)) { + return this.execTemplate(phrase, params) + } + let translation = this.settings.phrases[phrase] + if (translation == null) { + translation = phrase + if (this.settings.warnNoPhrase) { + if (!this.settings.missing) { + this.settings.missing = {} + } + this.settings.missing[phrase] = '---' // collect phrases for translation, warn once + this.settings.phrases[phrase] = '---' + console.log(`Missing translation for "%c${phrase}%c", see %c w2utils.settings.phrases %c with value "---"`, + 'color: orange', '', + 'color: #999', '') + } + } else if (translation === '---' && !this.settings.warnNoPhrase) { + translation = phrase + } + if (translation === '---') { + translation = `---` + } + return this.execTemplate(translation, params) + } + locale(locale, keepPhrases, noMerge) { + return new Promise((resolve, reject) => { + // if locale is an array we call this function recursively and merge the results + if (Array.isArray(locale)) { + this.settings.phrases = {} + const proms = [] + const files = {} + locale.forEach((file, ind) => { + if (file.length === 5) { + file = 'locale/' + file.toLowerCase() + '.json' + locale[ind] = file + } + proms.push(this.locale(file, true, false)) + }) + Promise.allSettled(proms) + .then(res => { + // order of files is important to merge + res.forEach(r => { if (r.value) files[r.value.file] = r.value.data }) + locale.forEach(file => { + this.settings = this.extend({}, this.settings, files[file]) + }) + resolve() + }) + return + } + if (!locale) locale = 'en-us' + // if locale is an object, then merge it with w2utils.settings + if (locale instanceof Object) { + this.settings = this.extend({}, this.settings, w2locale, locale) + return + } + if (locale.length === 5) { + locale = 'locale/' + locale.toLowerCase() + '.json' + } + // load from the file + fetch(locale, { method: 'GET' }) + .then(res => res.json()) + .then(data => { + if (noMerge !== true) { + if (keepPhrases) { + // keep phrases, useful for recursive calls + this.settings = this.extend({}, this.settings, data) + } else { + // clear phrases from language before merging + this.settings = this.extend({}, this.settings, w2locale, { phrases: {}}, data) + } + } + resolve({ file: locale, data }) + }) + .catch((err) => { + console.log('ERROR: Cannot load locale ' + locale) + reject(err) + }) + }) + } + scrollBarSize() { + if (this.tmp.scrollBarSize) return this.tmp.scrollBarSize + const html = ` +
+
1
+
+ ` + query('body').append(html) + this.tmp.scrollBarSize = 100 - query('#_scrollbar_width > div')[0].clientWidth + query('#_scrollbar_width').remove() + return this.tmp.scrollBarSize + } + checkName(name) { + if (name == null) { + console.log('ERROR: Property "name" is required but not supplied.') + return false + } + if (w2ui[name] != null) { + console.log(`ERROR: Object named "${name}" is already registered as w2ui.${name}.`) + return false + } + if (!this.isAlphaNumeric(name)) { + console.log('ERROR: Property "name" has to be alpha-numeric (a-z, 0-9, dash and underscore).') + return false + } + return true + } + checkUniqueId(id, items, desc, obj) { + if (!Array.isArray(items)) items = [items] + let isUnique = true + items.forEach(item => { + if (item.id === id) { + console.log(`ERROR: The item id="${id}" is not unique within the ${desc} "${obj}".`, items) + isUnique = false + } + }) + return isUnique + } + /** + * Takes an object and encodes it into params string to be passed as a url + * { a: 1, b: 'str'} => "a=1&b=str" + * { a: 1, b: { c: 2 }} => "a=1&b[c]=2" + * { a: 1, b: {c: { k: 'dfdf' } } } => "a=1&b[c][k]=dfdf" + */ + encodeParams(obj, prefix = '') { + let str = '' + Object.keys(obj).forEach(key => { + if (str != '') str += '&' + if (typeof obj[key] === 'object') { + str += this.encodeParams(obj[key], prefix + key + (prefix ? ']' : '') + '[') + } else { + str += `${prefix}${key}${prefix ? ']' : ''}=${obj[key]}` + } + }) + return str + } + parseRoute(route) { + const keys = [] + const path = route + .replace(/\/\(/g, '(?:/') + .replace(/\+/g, '__plus__') + .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash, format, key, capture, optional) => { + keys.push({ name: key, optional: !!optional }) + slash = slash || '' + return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '') + }) + .replace(/([\/.])/g, '\\$1') + .replace(/__plus__/g, '(.+)') + .replace(/\*/g, '(.*)') + return { + path: new RegExp('^' + path + '$', 'i'), + keys: keys + } + } + getCursorPosition(input) { + if (input == null) return null + let caretOffset = 0 + const doc = input.ownerDocument || input.document + const win = doc.defaultView || doc.parentWindow + let sel + if (['INPUT', 'TEXTAREA'].includes(input.tagName)) { + caretOffset = input.selectionStart + } else { + if (win.getSelection) { + sel = win.getSelection() + if (sel.rangeCount > 0) { + const range = sel.getRangeAt(0) + const preCaretRange = range.cloneRange() + preCaretRange.selectNodeContents(input) + preCaretRange.setEnd(range.endContainer, range.endOffset) + caretOffset = preCaretRange.toString().length + } + } else if ((sel = doc.selection) && sel.type !== 'Control') { + const textRange = sel.createRange() + const preCaretTextRange = doc.body.createTextRange() + preCaretTextRange.moveToElementText(input) + preCaretTextRange.setEndPoint('EndToEnd', textRange) + caretOffset = preCaretTextRange.text.length + } + } + return caretOffset + } + setCursorPosition(input, pos, posEnd) { + if (input == null) return + const range = document.createRange() + let el; const sel = window.getSelection() + if (['INPUT', 'TEXTAREA'].includes(input.tagName)) { + input.setSelectionRange(pos, posEnd ?? pos) + } else { + for (let i = 0; i < input.childNodes.length; i++) { + let tmp = query(input.childNodes[i]).text() + if (input.childNodes[i].tagName) { + tmp = query(input.childNodes[i]).html() + tmp = tmp.replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/ /g, ' ') + } + if (pos <= tmp.length) { + el = input.childNodes[i] + if (el.childNodes && el.childNodes.length > 0) el = el.childNodes[0] + if (el.childNodes && el.childNodes.length > 0) el = el.childNodes[0] + break + } else { + pos -= tmp.length + } + } + if (el == null) return + if (pos > el.length) pos = el.length + range.setStart(el, pos) + if (posEnd) { + range.setEnd(el, posEnd) + } else { + range.collapse(true) + } + sel.removeAllRanges() + sel.addRange(range) + } + } + parseColor(str) { + if (typeof str !== 'string') return null; else str = str.trim().toUpperCase() + if (str[0] === '#') str = str.substr(1) + let color = {} + if (str.length === 3) { + color = { + r: parseInt(str[0] + str[0], 16), + g: parseInt(str[1] + str[1], 16), + b: parseInt(str[2] + str[2], 16), + a: 1 + } + } else if (str.length === 6) { + color = { + r: parseInt(str.substr(0, 2), 16), + g: parseInt(str.substr(2, 2), 16), + b: parseInt(str.substr(4, 2), 16), + a: 1 + } + } else if (str.length === 8) { + color = { + r: parseInt(str.substr(0, 2), 16), + g: parseInt(str.substr(2, 2), 16), + b: parseInt(str.substr(4, 2), 16), + a: Math.round(parseInt(str.substr(6, 2), 16) / 255 * 100) / 100 // alpha channel 0-1 + } + } else if (str.length > 4 && str.substr(0, 4) === 'RGB(') { + const tmp = str.replace('RGB', '').replace(/\(/g, '').replace(/\)/g, '').split(',') + color = { + r: parseInt(tmp[0], 10), + g: parseInt(tmp[1], 10), + b: parseInt(tmp[2], 10), + a: 1 + } + } else if (str.length > 5 && str.substr(0, 5) === 'RGBA(') { + const tmp = str.replace('RGBA', '').replace(/\(/g, '').replace(/\)/g, '').split(',') + color = { + r: parseInt(tmp[0], 10), + g: parseInt(tmp[1], 10), + b: parseInt(tmp[2], 10), + a: parseFloat(tmp[3]) + } + } else { + // word color + return null + } + return color + } + colorContrast(color1, color2) { + const lum1 = calcLumens(color1) + const lum2 = calcLumens(color2) + const ratio = (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05) + return ratio.toFixed(2) + function calcLumens(color) { + const { r, g, b } = w2utils.parseColor(color) ?? { r: 0, g: 0, b: 0 } + const gamma = 2.2 + const normR = r / 255 + const normG = g / 255 + const normB = b / 255 + const sR = (normR <= 0.03928) ? normR / 12.92 : Math.pow((normR + 0.055) / 1.055, gamma) + const sG = (normG <= 0.03928) ? normG / 12.92 : Math.pow((normG + 0.055) / 1.055, gamma) + const sB = (normB <= 0.03928) ? normB / 12.92 : Math.pow((normB + 0.055) / 1.055, gamma) + return 0.2126 * sR + 0.7152 * sG + 0.0722 * sB + } + } + // h=0..360, s=0..100, v=0..100 + hsv2rgb(h, s, v, a) { + let r, g, b, i, f, p, q, t + if (arguments.length === 1) { + s = h.s; v = h.v; a = h.a; h = h.h + } + h = h / 360 + s = s / 100 + v = v / 100 + i = Math.floor(h * 6) + f = h * 6 - i + p = v * (1 - s) + q = v * (1 - f * s) + t = v * (1 - (1 - f) * s) + switch (i % 6) { + case 0: r = v, g = t, b = p; break + case 1: r = q, g = v, b = p; break + case 2: r = p, g = v, b = t; break + case 3: r = p, g = q, b = v; break + case 4: r = t, g = p, b = v; break + case 5: r = v, g = p, b = q; break + } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255), + a: (a != null ? a : 1) + } + } + // r=0..255, g=0..255, b=0..255 + rgb2hsv(r, g, b, a) { + if (arguments.length === 1) { + g = r.g; b = r.b; a = r.a; r = r.r + } + const max = Math.max(r, g, b); const min = Math.min(r, g, b) + const d = max - min + let h + const s = (max === 0 ? 0 : d / max) + const v = max / 255 + switch (max) { + case min: h = 0; break + case r: h = (g - b) + d * (g < b ? 6 : 0); h /= 6 * d; break + case g: h = (b - r) + d * 2; h /= 6 * d; break + case b: h = (r - g) + d * 4; h /= 6 * d; break + } + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + v: Math.round(v * 100), + a: (a != null ? a : 1) + } + } + tooltip(html, options) { + let actions + let showOn = 'mouseenter' + let hideOn = 'mouseleave' + if (typeof html === 'object') { + options = html + } + options = options || {} + if (typeof html === 'string') { + options.html = html + } + if (options.showOn) { + showOn = options.showOn + delete options.showOn + } + if (options.hideOn) { + hideOn = options.hideOn + delete options.hideOn + } + if (!options.name) options.name = 'no-name' + // base64 is needed to avoid '"<> and other special chars conflicts + actions = ` on${showOn}="w2tooltip.show(this, ` + + `JSON.parse(w2utils.base64decode('${this.base64encode(JSON.stringify(options))}')))" ` + + `on${hideOn}="w2tooltip.hide('${options.name}')"` + return actions + } + // determins if it is plain Object, not DOM element, nor a function, event, etc. + isPlainObject(value) { + if (value == null) { // null or undefined + return false + } + if (Object.prototype.toString.call(value) !== '[object Object]') { + return false + } + if (value.constructor === undefined) { + return true + } + const proto = Object.getPrototypeOf(value) + return proto === null || proto === Object.prototype + } + /** + * Deep copy of an object or an array. Function, events and HTML elements will not be cloned, + * you can choose to include them or not, by default they are included. + * You can also exclude certain elements from final object if used with options: { exclude } + */ + clone(obj, options) { + let ret + options = Object.assign({ functions: true, elements: true, events: true, exclude: [] }, options ?? {}) + if (Array.isArray(obj)) { + ret = Array.from(obj) + ret.forEach((value, ind) => { + ret[ind] = this.clone(value, { ...options, parent: (options.parent ?? '') + '[]' }) + }) + } else if (this.isPlainObject(obj)) { + ret = {} + Object.assign(ret, obj) + // delete excluded keys + if (Array.isArray(options.exclude)) { + options.exclude.forEach(key => { delete ret[key] }) + } + Object.keys(ret).forEach(key => { + if (typeof options.exclude === 'function' && options.exclude(key, { obj, parent: options.parent ?? '' })) { + ret[key] = undefined + } else { + ret[key] = this.clone(ret[key], { ...options, parent: (options.parent ?? '') + (options.parent != null ? '.' : '') + key }) + } + if (ret[key] === undefined) delete ret[key] // do not include undefined elements + }) + } else { + if ((obj instanceof Function && !options.functions) || + (obj instanceof Node && !options.elements) || + (obj instanceof Event && !options.events) + ) { + // do not include these objects, otherwise include them uncloned + } else { + // primitive variable or function, event, dom element, etc, - all these are not cloned + ret = obj + } + } + return ret + } + /** + * Deep extend an object, if an array, it overwrrites it, cloning objects in the process + * target, source1, source2, ... + */ + extend(target, source) { + if (Array.isArray(target)) { + if (Array.isArray(source)) { + target.splice(0, target.length) // empty array but keep the reference + source.forEach(s => { target.push(this.clone(s)) }) + } else { + throw new Error('Arrays can be extended with arrays only') + } + } else if (target instanceof Node || target instanceof Event) { + throw new Error('HTML elmenents and events cannot be extended') + } else if (target && typeof target === 'object' && source != null) { + if (typeof source !== 'object') { + throw new Error('Object can be extended with other objects only.') + } + Object.keys(source).forEach(key => { + if (target[key] != null && typeof target[key] === 'object' && + source[key] != null && typeof source[key] === 'object') { + const src = this.clone(source[key]) + // do not extend HTML elements and events, but overwrite them + if (target[key] instanceof Node || target[key] instanceof Event) { + target[key] = src + } else { + // if an array needs to be extended with an object, then convert it to empty object + if (Array.isArray(target[key]) && this.isPlainObject(src)) { + target[key] = {} + } + this.extend(target[key], src) + } + } else { + target[key] = this.clone(source[key]) + } + }) + } else if (source != null) { + throw new Error('Object is not extendable, only {} or [] can be extended.') + } + // other arguments + if (arguments.length > 2) { + for (let i = 2; i < arguments.length; i++) { + this.extend(target, arguments[i]) + } + } + return target + } + /* + * @author Lauri Rooden (https://github.com/litejs/natural-compare-lite) + * @license MIT License + */ + naturalCompare(a, b) { + let i; let codeA + let codeB = 1 + let posA = 0 + let posB = 0 + const alphabet = String.alphabet + function getCode(str, pos, code) { + if (code) { + for (i = pos; code = getCode(str, i), code < 76 && code > 65;) ++i + return +str.slice(pos - 1, i) + } + code = alphabet && alphabet.indexOf(str.charAt(pos)) + return code > -1 ? code + 76 : ((code = str.charCodeAt(pos) || 0), code < 45 || code > 127) ? code + : code < 46 ? 65 // - + : code < 48 ? code - 1 + : code < 58 ? code + 18 // 0-9 + : code < 65 ? code - 11 + : code < 91 ? code + 11 // A-Z + : code < 97 ? code - 37 + : code < 123 ? code + 5 // a-z + : code - 63 + } + + if ((a += '') != (b += '')) { + for (;codeB;) { + codeA = getCode(a, posA++) + codeB = getCode(b, posB++) + if (codeA < 76 && codeB < 76 && codeA > 66 && codeB > 66) { + codeA = getCode(a, posA, posA) + codeB = getCode(b, posB, posA = i) + posB = i + } + if (codeA != codeB) return (codeA < codeB) ? -1 : 1 + } + } + return 0 + } + /** + * Takes a menu (used in drop downs, context menu, field: list/combo/enum) and normalizes it to the common structure, which + * is { id: ..., text: ... }. In options you can pass { itemMap: { id: 'id_field', text: 'text_field' }} that will be used + * to find out id and text fields. + */ + normMenu(menu, options = {}) { + if (Array.isArray(menu)) { + menu.forEach((it, m) => { + if (typeof it === 'string' || typeof it === 'number') { + menu[m] = { id: it, text: String(it) } + } else if (it != null) { + if (options.itemMap != null) { + let val = w2utils.getNested(it, options.itemMap.id) + if (options.itemMap.id != null && val != null) { + it.id = val + } + val = w2utils.getNested(it, options.itemMap.text) + if (options.itemMap.text != null && val) { + it.text = val + } + } + if (it.caption != null && it.text == null) it.text = it.caption + if (it.text != null && it.id == null) it.id = it.text + if (it.text == null && it.id != null) it.text = it.id + } else { + menu[m] = { id: null, text: 'null' } + } + }) + return menu + } else if (typeof menu === 'function') { + const newMenu = menu.call(this, menu, options) + return w2utils.normMenu.call(this, newMenu, options) + } else if (typeof menu === 'object') { + return Object.keys(menu).map(key => { return { id: key, text: menu[key] } }) + } + } + /** + * Takes Url object and fetchOptions and changes it in place applying selected user dataType. Since + * dataType is in w2utils. This method is used in grid, form and tooltip to prepare fetch parameters + */ + prepareParams(url, fetchOptions, options = {}) { + const dataType = options?.dataType ?? w2utils.settings.dataType + let postParams = fetchOptions.body + fetchOptions.method = String(fetchOptions.method).toUpperCase() + switch (dataType) { + /** + * Will submit GET, POST, PUT, DELETE + * - if GET - it will be in URL + * - if POST, PUT, DELETE it will be JSON encoded + */ + case 'RESTFULL': + case 'RESTFULJSON': { + if (['POST', 'PUT', 'DELETE'].includes(fetchOptions.method)) { + fetchOptions.headers['Content-Type'] = 'application/json' + } + if (fetchOptions.method == 'GET') { + if (dataType == 'RESTFULLJSON') { + postParams = { request: postParams } + } + body2params() + } + break + } + /** + * Will submit either GET or POST and + * - if POST it will be JSON encoded + * - if GET it will be in URL + * - if HTTPJSON and GET then it will be JSON encoded + */ + case 'HTTP': + case 'HTTPJSON': + case 'JSON': { + if (fetchOptions.method == 'GET') { + if (dataType == 'JSON' || dataType === 'HTTPJSON') { + postParams = { request: postParams } + } + body2params() + } else { + fetchOptions.headers['Content-Type'] = 'application/json' + fetchOptions.method = 'POST' + } + break + } + default: { + if (typeof dataType === 'fuction') { + // do nothing, it is custom function that will handle everything + fetchOptions = dataType(url, fetchOptions, options) + } else { + console.log(`ERROR: Unsupported dataType "${dataType}". Supported types are JSON (default), HTTP, RESTFULL. For backward compatibility HTTPJSON is same as JSON. RESTULFLJSON will encode GET request as JSON.`) + } + } + } + if (fetchOptions.body != null) { + fetchOptions.body = typeof fetchOptions.body === 'string' ? fetchOptions.body : JSON.stringify(fetchOptions.body) + } + return fetchOptions + function body2params() { + Object.keys(postParams).forEach(key => { + let param = postParams[key] + if (typeof param === 'object') param = JSON.stringify(param) + url.searchParams.append(key, param) + }) + delete fetchOptions.body + } + } + bindEvents(selector, subject) { + // format is + //
='["","param1","param2",...]'> -- should be valid JSON (no undefined) + //
="|param1|param2"> + // -- can have "event", "this", "stop", "stopPrevent", "alert" - as predefined objects + if (selector.length == 0) return + // for backward compatibility + if (selector?.[0] instanceof Node) { + selector = Array.isArray(selector) ? selector : selector.get() + } + query(selector).each((el) => { + const actions = query(el).data() + Object.keys(actions).forEach(name => { + const events = ['click', 'dblclick', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousedown', 'mousemove', 'mouseup', + 'contextmenu', 'focus', 'focusin', 'focusout', 'blur', 'input', 'change', 'keydown', 'keyup', 'keypress'] + if (events.indexOf(String(name).toLowerCase()) == -1) { + return + } + let params = actions[name] + if (typeof params === 'string') { + params = params.split('|').map(key => { + if (key === 'true') key = true + if (key === 'false') key = false + if (key === 'undefined') key = undefined + if (key === 'null') key = null + if (parseFloat(key) == key) key = parseFloat(key) + const quotes = ['\'', '"', '`'] + if (typeof key === 'string' && quotes.includes(key[0]) && quotes.includes(key[key.length - 1])) { + key = key.substring(1, key.length - 1) + } + return key + }) + } + const method = params[0] + params = params.slice(1) // should be new array + query(el) + .off(name + '.w2utils-bind') + .on(name + '.w2utils-bind', function(event) { + switch (method) { + case 'alert': + alert(params[0]) // for testing purposes + break + case 'stop': + event.stopPropagation() + break + case 'prevent': + event.preventDefault() + break + case 'stopPrevent': + event.stopPropagation() + event.preventDefault() + return false + break + default: + if (subject[method] == null) { + throw new Error(`Cannot dispatch event as the method "${method}" does not exist.`) + } + subject[method].apply(subject, params.map((key, ind) => { + switch (String(key).toLowerCase()) { + case 'event': + return event + case 'this': + return this + default: + return key + } + })) + } + }) + }) + }) + } + debounce(func, wait = 250) { + let timeout + return (...args) => { + clearTimeout(timeout) + timeout = setTimeout(() => { func(...args) }, wait) + } + } + async wait(time = 0) { + return new Promise(resolve => { + setTimeout(() => resolve(), time) + }) + } + getNested(obj, prop) { + let val + try { // need this to make sure no error in props + val = obj + const tmp = String(prop).split('.') + for (let i = 0; i < tmp.length; i++) { + val = val[tmp[i]] + } + } catch (event) { + val = undefined + } + return val + } +} +var w2utils = new Utils() // eslint-disable-line -- needs to be functional/module scope variable +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base + * + * == 2.0 changes + * - CSP - fixed inline events + * - removed jQuery dependency + * - popup.open - returns promise like object + * - popup.confirm - refactored + * - popup.message - refactored + * - removed popup.options.mutliple + * - refactores w2alert, w2confirm, w2prompt + * - add w2popup.open().on('') + * - removed w2popup.restoreTemplate + * - deprecated onMsgOpen and onMsgClose + * - deprecated options.bgColor + * - rename focus -> setFocus + * - added center() // will auto center on window resize + * - close(immediate), also refactored if popup is closed when opening + * - options.resizable + * - actions in popup can be just html (for example separator) + * - resize - returns promise + */ + +class Dialog extends w2base { + constructor() { + super() + this.defaults = { + id: 'w2ui-popup', + title: '', + text: '', // just a text (will be centered) + body: '', + icon: '', + buttons: '', + width: 450, + height: 250, + focus: null, // brings focus to the element, can be a number or selector + actions: null, // actions object + style: '', // style of the message div + speed: 0.3, + blockPage: true, + modal: false, + maximized: false, // this is a flag to show the state - to open the popup maximized use openMaximized instead + keyboard: true, // will close popup on esc if not modal + showClose: true, + showMax: false, + resizable: false, + transition: null, + openMaximized: false, + moved: false + } + this.name = 'popup' + this.status = 'closed' // string that describes current status + this.onOpen = null + this.onClose = null + this.onMax = null + this.onMin = null + this.onToggle = null + this.onKeydown = null + this.onAction = null + this.onMove = null + this.zIndex = dmtW2uiZIndexState.value++ // 分配z-index + this.tmp = {} + // 添加默认的 promise 占位函数 + this._promCreated = () => {} + this._promOpened = () => {} + this._promClosing = () => {} + this._promClosed = () => {} + // event handler for resize + this.handleResize = (event) => { + // if it was moved by the user, do not auto resize + if (!this.options.moved) { + this.center(undefined, undefined, true) + } + } + } + /** + * Sample calls + * - w2popup.open('ddd').ok(() => { w2popup.close() }) + * - w2popup.open('ddd', { height: 120 }).ok(() => { w2popup.close() }) + * - w2popup.open({ body: 'text', title: 'caption', actions: ["Close"] }).close(() => { w2popup.close() }) + * - w2popup.open({ body: 'text', title: 'caption', actions: { Close() { w2popup.close() }} }) + * @param options + * @param {string} [options.id] + * @param {string} [options.title] + * @param {string} [options.body] + * @param {number} [options.width] + * @param {number} [options.height] + * @param {boolean} [options.blockPage] + * @param {boolean} [options.modal] + * @param {boolean} [options.showClose] + * @param {boolean} [options.icon] + */ + open(options) { + const self = this + // if (this.status === 'closing' || query('#w2ui-popup' + options.id).hasClass('animating')) { + // // if called when previous is closing + // this.close(true) + // } + // get old options and merge them + const old_options = this.options + if (['string', 'number'].includes(typeof options)) { + options = w2utils.extend({ + title: 'Notification', + body: `
${options}
`, + actions: { Ok() { self.close() } }, + cancelAction: 'ok' + }, arguments[1] ?? {}) + } + if (options.text != null) options.body = `
${options.text}
` + options = Object.assign({}, this.defaults, old_options, { title: '', body: '' }, options, { maximized: false }) + this.options = options + // if new - reset event handlers + // if (query('#w2ui-popup').length === 0) { + if (query('#w2ui-popup' + this.options.id).length === 0) { + this.off('*') + Object.keys(this).forEach(key => { + if (key.startsWith('on') && key !== 'on') this[key] = null + }) + } + // reassign events + Object.keys(options).forEach(key => { + if (key.startsWith('on') && key !== 'on' && options[key]) { + this[key] = options[key] + } + }) + options.width = parseInt(options.width) + options.height = parseInt(options.height) + let edata, msg, tmp + const { top, left, width, height } = this.center() + // make sure popup is not bigger then available screen + if (options.width > width) options.width = width + if (options.height > height) options.height = height + const prom = { + self: this, + action(callBack) { + self.on('action.prom', callBack) + return prom + }, + close(callBack) { + self.on('close.prom', callBack) + return prom + }, + then(callBack) { + self.on('open:after.prom', callBack) + return prom + } + } + // convert action arrays into buttons + if (options.actions != null && !options.buttons) { + options.buttons = '' + Object.keys(options.actions).forEach((action) => { + const handler = options.actions[action] + let btnAction = action + if (typeof handler === 'function') { + options.buttons += `` + } + if (typeof handler === 'object') { + options.buttons += `` + btnAction = Array.isArray(options.actions) ? handler.text : action + } + if (typeof handler === 'string') { + if (handler.trim().startsWith('<')) { + btnAction = 'none' + options.buttons += handler + } else { + btnAction = handler[0].toLowerCase() + handler.substr(1).replace(/\s+/g, '') + options.buttons += `` + } + } + if (typeof btnAction === 'string') { + btnAction = btnAction[0].toLowerCase() + btnAction.substr(1).replace(/\s+/g, '') + } + prom[btnAction] = function(callBack) { + self.on('action.buttons', (event) => { + const target = event.detail.action[0].toLowerCase() + event.detail.action.substr(1).replace(/\s+/g, '') + if (target === btnAction) callBack(event) + }) + return prom + } + }) + } + // check if message is already displayed + let titleBtns = '' + if (options.showClose) { + titleBtns += `
+ +
` + } + if (options.showMax) { + titleBtns += `
+ +
` + } + // if (query('#w2ui-popup').length === 0) { + if (query('#w2ui-popup' + this.options.id).length === 0) { + if (options.icon != null && options.title) options.title = options.icon + options.title + // trigger event + edata = this.trigger('open', { target: 'popup', present: false }) + if (edata.isCancelled === true) return + this.status = 'opening' + // output message + if (options.blockPage) { + w2utils.lock(document.body, { + opacity: 0.3, + onClick: options.modal ? null : () => { this.close() } + }) + } + // first insert just body + let styles = ` + left: ${left}px; + top: ${top}px; + width: ${parseInt(options.width)}px; + height: ${parseInt(options.height)}px; + transition: ${options.speed}s; + z-index: ${this.zIndex}; + ` + msg = `
` + query('body').append(msg) + query('#w2ui-popup' + this.options.id)[0]._w2popup = { + self: this, + created: new Promise((resolve) => { this._promCreated = resolve }), + opened: new Promise((resolve) => { this._promOpened = resolve }), + closing: new Promise((resolve) => { this._promClosing = resolve }), + closed: new Promise((resolve) => { this._promClosed = resolve }) + } + // then content + styles = `${!options.title ? 'top: 0px !important;' : ''} ${!options.buttons ? 'bottom: 0px !important;' : ''}` + msg = ` + +
${titleBtns}
+
+
+
+
+
+
+
+ + ` + query('#w2ui-popup' + this.options.id).html(msg) + if (options.title) query('#w2ui-popup' + this.options.id + ' .w2ui-popup-title').append(w2utils.lang(options.title)) + if (options.buttons) query('#w2ui-popup' + this.options.id + ' .w2ui-popup-buttons').append(options.buttons) + if (options.body) query('#w2ui-popup' + this.options.id + ' .w2ui-popup-body').append(options.body) + // allow element to render + setTimeout(() => { + query('#w2ui-popup' + this.options.id) + .css('transition', options.speed + 's') + .removeClass('w2ui-anim-open') + w2utils.bindEvents('#w2ui-popup' + this.options.id + ' .w2ui-eaction', this) + query('#w2ui-popup' + this.options.id).find('.w2ui-popup-body').show() + this._promCreated() + }, 1) + // clean transform + clearTimeout(this._timer) + this.bringToFront() + this._timer = setTimeout(() => { + this.status = 'open' + // self.setFocus(options.focus) + // event after + edata.finish() + this._promOpened() + query('#w2ui-popup' + this.options.id).removeClass('animating') + }, options.speed * 1000) + } else { + // trigger event + edata = this.trigger('open', { target: 'popup', present: true }) + if (edata.isCancelled === true) return + // check if size changed + this.status = 'opening' + if (old_options != null) { + if (!old_options.maximized && (old_options.width != options.width || old_options.height != options.height)) { + this.resize(options.width, options.height) + } + options.prevSize = options.width + 'px:' + options.height + 'px' + options.maximized = old_options.maximized + } + // show new items + const cloned = query('#w2ui-popup' + this.options.id + ' .w2ui-box').get(0).cloneNode(true) + query(cloned).removeClass('w2ui-box').addClass('w2ui-box-temp').find('.w2ui-popup-body').empty().append(options.body) + query('#w2ui-popup' + this.options.id + ' .w2ui-box').after(cloned) + if (options.buttons) { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-buttons').show().html('').append(options.buttons) + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-body').removeClass('w2ui-popup-no-buttons') + query('#w2ui-popup' + this.options.id + ' .w2ui-box, #w2ui-popup' + this.options.id + ' .w2ui-box-temp').css('bottom', '') + } else { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-buttons').hide().html('') + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-body').addClass('w2ui-popup-no-buttons') + query('#w2ui-popup' + this.options.id + ' .w2ui-box, #w2ui-popup' + this.options.id + ' .w2ui-box-temp').css('bottom', '0px') + } + if (options.title) { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-title') + .show() + .html(w2utils.lang(options.title)) + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-body').removeClass('w2ui-popup-no-title') + query('#w2ui-popup' + this.options.id + ' .w2ui-box, #w2ui-popup' + this.options.id + ' .w2ui-box-temp').css('top', '') + } else { + query('#w2ui-popup .w2ui-popup-title').hide().html('') + query('#w2ui-popup .w2ui-popup-body').addClass('w2ui-popup-no-title') + query('#w2ui-popup' + this.options.id + ' .w2ui-box, #w2ui-popup' + this.options.id + ' .w2ui-box-temp').css('top', '0px') + } + if (titleBtns) { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-title-btns') + .show() + .html(titleBtns) + } else { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-title-btns') + .hide() + } + // transition + const div_old = query('#w2ui-popup' + this.options.id + ' .w2ui-box')[0] + const div_new = query('#w2ui-popup' + this.options.id + ' .w2ui-box-temp')[0] + query('#w2ui-popup' + this.options.id).addClass('animating') + w2utils.transition(div_old, div_new, options.transition, () => { + // clean up + query(div_old).remove() + query(div_new).removeClass('w2ui-box-temp').addClass('w2ui-box') + const $body = query(div_new).find('.w2ui-popup-body') + if ($body.length === 1) { + $body[0].style.cssText = options.style + $body.show() + } + // focus on first button + // self.setFocus(options.focus) + query('#w2ui-popup' + this.options.id).removeClass('animating') + }) + // call event onOpen + this.status = 'open' + edata.finish() + w2utils.bindEvents('#w2ui-popup' + this.options.id + ' .w2ui-eaction', this) + query('#w2ui-popup' + this.options.id).find('.w2ui-popup-body').show() + } + if (options.openMaximized) { + this.max() + } + // save new options + options._last_focus = document.activeElement + // keyboard events + if (options.keyboard) { + query(document.body) + .off('.w2popup') + .on('keydown.w2popup', (event) => { + this.keydown(event) + }) + } + query(window).on('resize', this.handleResize) + // initialize move + tmp = { + changing: false, + mvMove: mvMove, + mvStop: mvStop + } + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-title') + .off('mousedown') + .on('mousedown', function(event) { + self.bringToFront() + if (!self.options.maximized) mvStart(event) + }) + if (options.resizable) { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-resizer').show() + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-resizer') + .off('mousedown') + .on('mousedown', event => { + mvStart(event, true) + }) + } else { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-resizer').hide() + } + return prom + // handlers + function mvStart(evt, resizer) { + if (!evt) evt = window.event + self.status = resizer ? 'resizing' : 'moving' + const rect = query('#w2ui-popup' + options.id).get(0).getBoundingClientRect() + Object.assign(tmp, { + changing: true, + isLocked: query('#w2ui-popup' + options.id + ' > .w2ui-lock').length === 1, + x: evt.screenX, + y: evt.screenY, + pos_x: rect.x, + pos_y: rect.y, + width: rect.width, + height: rect.height + }) + if (!tmp.isLocked) self.lock({ opacity: 0 }) + query(document.body) + .on('mousemove.w2ui-popup', tmp.mvMove) + .on('mouseup.w2ui-popup', tmp.mvStop) + if (evt.stopPropagation) evt.stopPropagation(); else evt.cancelBubble = true + if (evt.preventDefault) evt.preventDefault(); else return false + } + function mvMove(evt) { + if (tmp.changing !== true) return + if (!evt) evt = window.event + tmp.div_x = evt.screenX - tmp.x + tmp.div_y = evt.screenY - tmp.y + // trigger event + const edata = self.trigger('move', { target: 'popup', div_x: tmp.div_x, div_y: tmp.div_y, originalEvent: evt }) + if (edata.isCancelled === true) return + // default behavior + if (self.status === 'moving') { + query('#w2ui-popup' + options.id).css({ + 'transition': 'none', + 'transform': 'translate3d(' + tmp.div_x + 'px, ' + tmp.div_y + 'px, 0px)' + }) + self.options.moved = true + } else { + query('#w2ui-popup' + options.id).css({ + transition: 'none', + width: (tmp.width + tmp.div_x) + 'px', + height: (tmp.height + tmp.div_y) + 'px' + }) + } + // event after + edata.finish() + } + function mvStop(evt) { + if (tmp.changing != true) return + if (!evt) evt = window.event + tmp.div_x = (evt.screenX - tmp.x) + tmp.div_y = (evt.screenY - tmp.y) + if (self.status === 'moving') { + query('#w2ui-popup' + options.id) + .css({ + 'left': (tmp.pos_x + tmp.div_x) + 'px', + 'top': (tmp.pos_y + tmp.div_y) + 'px' + }) + .css({ + 'transition': 'none', + 'transform': 'translate3d(0px, 0px, 0px)' + }) + } else { + query('#w2ui-popup' + options.id).css({ + transition: 'none', + width: (tmp.width + tmp.div_x) + 'px', + height: (tmp.height + tmp.div_y) + 'px' + }) + self.resizeMessages() + } + tmp.changing = false + self.status = 'open' + query(document.body).off('.w2ui-popup') + if (!tmp.isLocked) self.unlock() + } + } + + /** + * 更新dialog的标题 + * @param {string} title + */ + updateTitle(title) { + if(this.options.icon) title = this.options.icon + title + this.options.title = title + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-title').html(this.options.title) + } + load(options) { + return new Promise((resolve, reject) => { + if (typeof options === 'string') { + options = { url: options } + } + if (options.url == null) { + console.log('ERROR: The url is not defined.') + reject('The url is not defined') + return + } + this.status = 'loading' + const [url, selector] = String(options.url).split('#') + if (url) { + fetch(url).then(res => res.text()).then(html => { + resolve(this.template(html, selector, options)) + }) + } + }) + } + template(data, id, options = {}) { + let html + try { + html = query(data) + } catch (e) { + html = query.html(data) + } + if (id) html = html.filter('#' + id) + Object.assign(options, { + width: parseInt(query(html).css('width')), + height: parseInt(query(html).css('height')), + title: query(html).find('[rel=title]').html(), + body: query(html).find('[rel=body]').html(), + buttons: query(html).find('[rel=buttons]').html(), + style: query(html).find('[rel=body]').get(0).style.cssText + }) + return this.open(options) + } + action(action, event) { + let click = this.options.actions[action] + if (click instanceof Object && click.onClick) click = click.onClick + // event before + const edata = this.trigger('action', { action, target: 'popup', self: this, + originalEvent: event, value: this.input ? this.input.value : null }) + if (edata.isCancelled === true) return + // default actions + if (typeof click === 'function') click.call(this, event) + // event after + edata.finish() + } + keydown(event) { + if (this.options && !this.options.keyboard) return + // trigger event + const edata = this.trigger('keydown', { target: 'popup', originalEvent: event }) + if (edata.isCancelled === true) return + // default behavior + switch (event.keyCode) { + case 27: + event.preventDefault() + if (query('#w2ui-popup' + this.options.id + ' .w2ui-message').length == 0) { + if (this.options.cancelAction) { + this.action(this.options.cancelAction) + } else { + this.close() + } + } + break + } + // event after + edata.finish() + } + close(immediate) { + // trigger event + const edata = this.trigger('close', { target: 'popup' }) + if (edata.isCancelled === true) return + const cleanUp = () => { + // return template + query('#w2ui-popup' + this.options.id).remove() + // restore active + if (this.options._last_focus && this.options._last_focus.length > 0) this.options._last_focus.focus() + this.status = 'closed' + this.options = {} + // event after + edata.finish() + this._promClosed() + } + if (query('#w2ui-popup' + this.options.id).length === 0 || this.status == 'closed') { // already closed + return + } + if (this.status === 'opening') { // if it is opening + immediate = true + } + if (this.status === 'closing' && immediate === true) { + cleanUp() + clearTimeout(this.tmp.closingTimer) + w2utils.unlock(document.body, 0) + return + } + // default behavior + this.status = 'closing' + query('#w2ui-popup' + this.options.id) + .css('transition', this.options.speed + 's') + .addClass('w2ui-anim-close animating') + w2utils.unlock(document.body, 300) + this._promClosing() + if (immediate) { + cleanUp() + } else { + this.tmp.closingTimer = setTimeout(cleanUp, this.options.speed * 1000) + } + // remove keyboard events + if (this.options.keyboard) { + query(document.body).off('keydown', this.keydown) + } + query(window).off('resize', this.handleResize) + } + toggle() { + const edata = this.trigger('toggle', { target: 'popup' }) + if (edata.isCancelled === true) return + // default action + if (this.options.maximized === true) this.min(); else this.max() + // event after + setTimeout(() => { + edata.finish() + }, (this.options.speed * 1000) + 50) + } + max() { + if (this.options.maximized === true) return + // trigger event + const edata = this.trigger('max', { target: 'popup' }) + if (edata.isCancelled === true) return + // default behavior + this.status = 'resizing' + const rect = query('#w2ui-popup' + this.options.id).get(0).getBoundingClientRect() + this.options.prevSize = rect.width + ':' + rect.height + // do resize + this.resize(10000, 10000, () => { + this.status = 'open' + this.options.maximized = true + edata.finish() + }) + } + min() { + if (this.options.maximized !== true) return + const size = this.options.prevSize.split(':') + // trigger event + const edata = this.trigger('min', { target: 'popup' }) + if (edata.isCancelled === true) return + // default behavior + this.status = 'resizing' + // do resize + this.options.maximized = false + this.resize(parseInt(size[0]), parseInt(size[1]), () => { + this.status = 'open' + this.options.prevSize = null + edata.finish() + }) + } + clear() { + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-title').html('') + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-body').html('') + query('#w2ui-popup' + this.options.id + ' .w2ui-popup-buttons').html('') + } + reset() { + this.open(this.defaults) + } + message(options) { + return w2utils.message({ + owner: this, + box: query('#w2ui-popup' + this.options.id).get(0), + after: '.w2ui-popup-title' + }, options) + } + confirm(options) { + return w2utils.confirm({ + owner: this, + box: query('#w2ui-popup' + this.options.id), + after: '.w2ui-popup-title' + }, options) + } + setFocus(focus) { + const box = query('#w2ui-popup' + this.options.id) + const sel = 'input, button, select, textarea, [contentEditable], [tabindex], .w2ui-input' + if (focus != null) { + const el = isNaN(focus) + ? box.find(sel).filter(focus).filter(':not([name=hidden-first])').get(0) + : box.find(sel).filter(':not([name=hidden-first])').get(focus) + el?.focus() + } else { + const el = box.find('[name=hidden-first]').get(0) + if (el) el.focus() + } + // keep focus/blur inside popup + query(box).find(sel) + .off('.keep-focus') + .on('blur.keep-focus', function(event) { + setTimeout(() => { + const focus = document.activeElement + const inside = query(box).find(sel).filter(focus).length > 0 + const name = query(focus).attr('name') + if (!inside && focus && focus !== document.body) { + query(box).find(sel).get(0)?.focus() + } + if (name === 'hidden-last') { + query(box).find(sel).get(1)?.focus() + } + if (name === 'hidden-first') { + query(box).find(sel).get(-2)?.focus() + } + }, 1) + }) + } + bringToFront() { + const box = query('#w2ui-popup' + this.options.id) + if (this.zIndex < dmtW2uiZIndexState.value - 1) { + this.zIndex = dmtW2uiZIndexState.value++ + box[0].style.zIndex = this.zIndex + } + // 移除其他 Dialog 的 active 类 + document.querySelectorAll('.w2ui-popup-title').forEach(el => { + el.classList.remove('active') + }) + + // 当前 Dialog 高亮 + box[0].classList.add('active') + box[0].querySelector('.w2ui-popup-title').classList.add('active') + } + lock(msg, showSpinner) { + const args = Array.from(arguments) + args.unshift(query('#w2ui-popup' + this.options.id)) + w2utils.lock(...args) + } + unlock(speed) { + w2utils.unlock(query('#w2ui-popup' + this.options.id), speed) + } + center(width, height, force) { + let maxW, maxH + if (window.innerHeight == undefined) { + maxW = parseInt(document.documentElement.offsetWidth) + maxH = parseInt(document.documentElement.offsetHeight) + } else { + maxW = parseInt(window.innerWidth) + maxH = parseInt(window.innerHeight) + } + width = parseInt(width ?? this.options.width) + height = parseInt(height ?? this.options.height) + if (this.options.maximized === true) { + width = maxW + height = maxH + } + if (maxW - 10 < width) width = maxW - 10 + if (maxH - 10 < height) height = maxH - 10 + const top = (maxH - height) / 3 // it is my oppinion that it is more estatic to show closer to top then in exact middle + const left = (maxW - width) / 2 + if (force) { + query('#w2ui-popup' + this.options.id).css({ + 'transition': 'none', + 'top': top + 'px', + 'left': left + 'px', + 'width': width + 'px', + 'height': height + 'px' + }) + this.resizeMessages() // then messages resize nicely + } + return { top, left, width, height } + } + resize(newWidth, newHeight, callBack) { + return new Promise(resolve => { + const self = this + if (this.options.speed == null) this.options.speed = 0 + // calculate new position + const { top, left, width, height } = this.center(newWidth, newHeight) + const speed = this.options.speed + query('#w2ui-popup' + this.options.id).css({ + 'transition': `${speed}s width, ${speed}s height, ${speed}s left, ${speed}s top`, + 'top': top + 'px', + 'left': left + 'px', + 'width': width + 'px', + 'height': height + 'px' + }) + const tmp_int = setInterval(() => { self.resizeMessages() }, 10) // then messages resize nicely + setTimeout(() => { + clearInterval(tmp_int) + self.resizeMessages() + if (typeof callBack === 'function') callBack() + resolve() + }, (this.options.speed * 1000) + 50) // give extra 50 ms + }) + } + // internal function + resizeMessages() { + // see if there are messages and resize them + query('#w2ui-popup' + this.options.id + ' .w2ui-message').each(msg => { + const mopt = msg._msg_options + const popup = query('#w2ui-popup' + this.options.id) + if (parseInt(mopt.width) < 10) mopt.width = 10 + if (parseInt(mopt.height) < 10) mopt.height = 10 + const rect = popup[0].getBoundingClientRect() + const titleHeight = parseInt(popup.find('.w2ui-popup-title')[0].clientHeight) + const pWidth = parseInt(rect.width) + const pHeight = parseInt(rect.height) + // re-calc width + mopt.width = mopt.originalWidth + if (mopt.width > pWidth - 10) { + mopt.width = pWidth - 10 + } + // re-calc height + mopt.height = mopt.originalHeight + if (mopt.height > pHeight - titleHeight - 5) { + mopt.height = pHeight - titleHeight - 5 + } + if (mopt.originalHeight < 0) mopt.height = pHeight + mopt.originalHeight - titleHeight + if (mopt.originalWidth < 0) mopt.width = pWidth + mopt.originalWidth * 2 // x 2 because there is left and right margin + query(msg).css({ + left: ((pWidth - mopt.width) / 2) + 'px', + width: mopt.width + 'px', + height: mopt.height + 'px' + }) + }) + } +} +function w2alert(msg, title, callBack) { + let prom + const options = { + title: w2utils.lang(title ?? 'Notification'), + body: `
${msg}
`, + showClose: false, + actions: { ok: w2utils.lang('Ok') }, + cancelAction: 'ok' + } + if (query('#w2ui-popup' + this.options.id).length > 0 && w2popup.status !== 'closing') { + prom = w2popup.message(options) + } else { + prom = w2popup.open(options) + } + prom.ok(event => { + if (typeof event.detail.self?.close === 'function') { + event.detail.self.close() + } + if (typeof callBack === 'function') callBack() + }) + return prom +} +function w2confirm(msg, title, callBack) { + let prom + let options = msg + if (['string', 'number'].includes(typeof options)) { + options = { msg: options } + } + if (options.msg) { + options.body = `
${options.msg}
`, + delete options.msg + } + if (typeof title === 'function' && callBack == null) { + callBack = title + title = undefined + } + w2utils.extend(options, { + title: w2utils.lang(title ?? options.title ?? 'Confirmation'), + showClose: false, + modal: true, + cancelAction: 'no' + }) + if (callBack == null && options.callBack != null) { + callBack = options.callBack + } + w2utils.normButtons(options, { yes: w2utils.lang('Yes'), no: w2utils.lang('No') }) + if (query('#w2ui-popup' + options.id).length > 0 && w2popup.status !== 'closing') { + prom = w2popup.message(options) + } else { + prom = w2popup.open(options) + } + prom.self + .off('.confirm') + .on('action:after.confirm', (event) => { + if (typeof event.detail.self?.close === 'function') { + event.detail.self.close() + } + if (typeof callBack === 'function') callBack(event.detail.action) + }) + return prom +} +function w2prompt(label, title, callBack) { + let prom + let options = label + if (['string', 'number'].includes(typeof options)) { + options = { label: options } + } + if (options.label) { + options.focus = 0 // the input should be in focus, which is first in the popup + options.body = (options.textarea + ? `
+
${options.label}
+ +
` + : `
+ + +
` + ) + } + w2utils.extend(options, { + title: w2utils.lang(title ?? options.title ?? 'Notification'), + showClose: false, + modal: true, + cancelAction: 'cancel' + }) + w2utils.normButtons(options, { ok: w2utils.lang('Ok'), cancel: w2utils.lang('Cancel') }) + if (query('#w2ui-popup' + this.options.id).length > 0 && w2popup.status !== 'closing') { + prom = w2popup.message(options) + } else { + prom = w2popup.open(options) + } + if (prom.self.box) { + prom.self.input = query(prom.self.box).find('#w2prompt').get(0) + } else { + prom.self.input = query('#w2ui-popup' + this.options.id + ' .w2ui-popup-body #w2prompt').get(0) + } + if (options.value != null) { + prom.self.input.value = options.value + prom.self.input.select() + } + prom.change = function(callback) { + prom.self.on('change', callback) + return this + } + prom.self + .off('.prompt') + .on('open:after.prompt', (event) => { + const box = event.detail.box ? event.detail.box : query('#w2ui-popup' + this.options.id + ' .w2ui-popup-body').get(0) + w2utils.bindEvents(query(box).find('#w2prompt'), { + keydown(evt) { + if (evt.keyCode === 27) evt.stopPropagation() + }, + change(evt) { + const edata = prom.self.trigger('change', { target: 'prompt', originalEvent: evt }) + if (edata.isCancelled === true) return + if (evt.keyCode === 13 && (evt.ctrlKey || evt.metaKey || evt.target.tagName !== 'TEXTAREA')) { + prom.self.action('Ok', evt) + } + if (evt.keyCode === 27) { + prom.self.action('Cancel', evt) + } + edata.finish() + } + }) + query(box).find('.w2ui-eaction').trigger('keyup') + }) + .on('action:after.prompt', (event) => { + if (typeof event.detail.self?.close === 'function') { + event.detail.self.close() + } + if (typeof callBack === 'function') callBack(event.detail.action) + }) + return prom +} +const w2popup = new Dialog() +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base + * + * TODO: + * - need help pages + * + * 2.0 Changes + * - multiple tooltips to the same anchor + * - options.contextMenu + * - options.prefilter - if true, it will show prefiltered items for w2menu, otherwise all + * - menu.item.help, menu.item.hotkey, menu.item.extra + * - options.selected -> for w2menu + * - options.tooltip => {} + * - w2menu event onTooltip + * - added onMouseEnter and onMouseLeave for w2menu + */ + +class Tooltip { + // no need to extend w2base, as each individual tooltip extends it + static active = {} // all defined tooltips + constructor() { + this.defaults = { + name: null, // name for the overlay, otherwise input id is used + html: '', // text or html + style: '', // additional style for the overlay + class: '', // add class for w2ui-tooltip-body + position: 'top|bottom', // can be left, right, top, bottom + draggable: false, // if true, then tooltip can be move with mouse + align: '', // can be: both, both:XX left, right, both, top, bottom + anchor: null, // element it is attached to, if anchor is body, then it is context menu + contextMenu: false, // if true, then it is context menu + anchorClass: '', // add class for anchor when tooltip is shown + anchorStyle: '', // add style for anchor when tooltip is shown + autoShow: false, // if autoShow true, then tooltip will show on mouseEnter and hide on mouseLeave + autoShowOn: null, // when options.autoShow = true, mouse event to show on + autoHideOn: null, // when options.autoShow = true, mouse event to hide on + arrowSize: 8, // size of the carret + screenMargin: 2, // min margin from screen to tooltip + autoResize: true, // auto resize based on content size and available size + margin: 1, // distance from the anchor + offsetX: 0, // delta for left coordinate + offsetY: 0, // delta for top coordinate + maxWidth: null, // max width + maxHeight: null, // max height + watchScroll: null, // attach to onScroll event // TODO: + watchResize: null, // attach to onResize event // TODO: + hideOn: null, // events when to hide tooltip, ['click', 'change', 'key', 'focus', 'blur'], + onThen: null, // called when displayed + onShow: null, // callBack when shown + onHide: null, // callBack when hidden + onUpdate: null, // callback when tooltip gets updated + onMove: null // callback when tooltip is moved + } + } + static observeRemove = new MutationObserver((mutations) => { + let cnt = 0 + Object.keys(Tooltip.active).forEach(name => { + const overlay = Tooltip.active[name] + if (overlay.displayed) { + if (!overlay.anchor || !overlay.anchor.isConnected) { + overlay.hide() + } else { + cnt++ + } + } + }) + // remove observer, as there is no active tooltips + if (cnt === 0) { + Tooltip.observeRemove.disconnect() + } + }) + trigger(event, data) { + if (arguments.length == 2) { + const type = event + event = data + data.type = type + } + if (event.overlay) { + return event.overlay.trigger(event) + } else { + console.log('ERROR: cannot find overlay where to trigger events') + } + } + get(name) { + if (arguments.length == 0) { + return Object.keys(Tooltip.active) + } else if (name === true) { + return Tooltip.active + } else { + return Tooltip.active[name.replace(/[\s\.#]/g, '_')] + } + } + attach(anchor, text) { + let options, overlay + const self = this + if (arguments.length == 0) { + return + } else if (arguments.length == 1 && anchor instanceof Object) { + options = anchor + anchor = options.anchor + } else if (arguments.length === 2 && typeof text === 'string') { + options = { anchor, html: text } + text = options.html + } else if (arguments.length === 2 && text != null && typeof text === 'object') { + options = text + text = options.html + } + options = w2utils.extend({}, this.defaults, options || {}) + if (!text && options.text) text = options.text + if (!text && options.html) text = options.html + // anchor is func var + delete options.anchor + // define tooltip + let name = (options.name ? options.name : anchor?.id) + if (anchor == document || anchor == null) { + anchor = document.body + } + if (options.contextMenu) { + anchor = document.body + name = name ?? 'context-menu' + } + if (!name) { + name = 'noname-' + Object.keys(Tooltip.active).length + console.log('NOTICE: name property is not defined for tooltip, could lead to too many instances') + } + // clean name as it is used as id and css selector + name = name.replace(/[\s\.#]/g, '_') + if (Tooltip.active[name]) { + overlay = Tooltip.active[name] + overlay.prevOptions = overlay.options + overlay.options = options // do not merge or extend, otherwiser menu items get merged too + // overlay.options = w2utils.extend({}, overlay.options, options) + overlay.anchor = anchor // as HTML elements are not copied + if (overlay.prevOptions.html != overlay.options.html || overlay.prevOptions.class != overlay.options.class || + overlay.prevOptions.style != overlay.options.style) { + overlay.needsUpdate = true + } + options = overlay.options // it was recreated + // clear all previous overlay events + Object.keys(overlay).forEach(key => { + const val = overlay[key] + if (key.startsWith('on') && typeof val === 'function') { + delete overlay[key] + } + }) + } else { + overlay = new w2base() + Object.assign(overlay, { + id: 'w2overlay-' + name, + name, options, anchor, self, + displayed: false, + tmp: { + observeTooltipResize: new ResizeObserver(() => { + this.resize(overlay.name) + }), + observeAnchorResize: new ResizeObserver(() => { + this.resize(overlay.name) + }), + observeAnchorMove: new MutationObserver((mutations) => { + const target = mutations[0].target // all targets are the same + const currRect = target.getBoundingClientRect() + const lastRect = target._lastBoundingRect + if (!target._lastBoundingRect) { + target._lastBoundingRect = currRect + } else if (currRect.left !== lastRect.left || currRect.top !== lastRect.top) { + this.resize(overlay.name) + target._lastBoundingRect = currRect + } + }) + }, + hide() { + self.hide(name) + } + }) + Tooltip.active[name] = overlay + } + // move events on to overlay layer + Object.keys(overlay.options).forEach(key => { + const val = overlay.options[key] + if (key.startsWith('on') && typeof val === 'function') { + overlay[key] = val + delete overlay.options[key] + } + }) + // add event for auto show/hide + if (options.autoShow === true) { + options.autoShowOn = options.autoShowOn ?? 'mouseenter' + options.autoHideOn = options.autoHideOn ?? 'mouseleave' + options.autoShow = false + options._keep = true + } + if (options.autoShowOn) { + const scope = 'autoShow-' + overlay.name + query(anchor) + .off(`.${scope}`) + .on(`${options.autoShowOn}.${scope}`, event => { + self.show(overlay.name) + event.stopPropagation() + }) + delete options.autoShowOn + options._keep = true + } + if (options.autoHideOn) { + const scope = 'autoHide-' + overlay.name + query(anchor) + .off(`.${scope}`) + .on(`${options.autoHideOn}.${scope}`, event => { + self.hide(overlay.name) + event.stopPropagation() + }) + delete options.autoHideOn + options._keep = true + } + overlay.off('.attach') + const ret = { + overlay, + then: (callback) => { + overlay.on('show:after.attach', event => { callback(event) }) + return ret + }, + show: (callback) => { + overlay.on('show.attach', event => { callback(event) }) + return ret + }, + hide: (callback) => { + overlay.on('hide.attach', event => { callback(event) }) + return ret + }, + update: (callback) => { + overlay.on('update.attach', event => { callback(event) }) + return ret + }, + move: (callback) => { + overlay.on('move.attach', event => { callback(event) }) + return ret + } + } + return ret + } + update(name, html) { + const overlay = Tooltip.active[name] + if (overlay) { + overlay.needsUpdate = true + overlay.options.html = html + this.show(name) + } else { + console.log(`Tooltip "${name}" is not displayed. Cannot update it.`) + } + } + show(name) { + if (name instanceof HTMLElement || name instanceof Object) { + let options = name + if (name instanceof HTMLElement) { + options = arguments[1] || {} + options.anchor = name + } + const ret = this.attach(options) + ret.overlay.tmp.hidden = false + query(ret.overlay.anchor) + .off('.autoShow-' + ret.overlay.name) + .off('.autoHide-' + ret.overlay.name) + /** + * Need a timer, so that events in the 'return ret` would be preperty set as it is using chaning mechanism + * to set listeners: w2tooltip.show({}).then(...).show(...). Since it could be hidden before timer kick in + * to show it, need the check in the timeout. + */ + setTimeout(() => { + if (!ret.overlay.tmp.hidden) { + this.show(ret.overlay.name) + if (this.initControls) { + this.initControls(ret.overlay) + } + } + }, 1) + return ret + } + let edata + const self = this + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + if (!overlay) return + const options = overlay.options + if (!overlay || (overlay.displayed && !overlay.needsUpdate)) { + this.resize(overlay?.name) + return + } + const position = options.position.split('|') + const isVertical = ['top', 'bottom'].includes(position[0]) + // enforce nowrap only when align=both and vertical + let overlayStyles = (options.align == 'both' && isVertical ? '' : 'white-space: nowrap;') + if (options.maxWidth && w2utils.getStrWidth(options.html, '') > options.maxWidth) { + overlayStyles = 'width: ' + options.maxWidth + 'px; white-space: inherit; overflow: auto;' + } + overlayStyles += ' max-height: ' + (options.maxHeight ? options.maxHeight : window.innerHeight - 4) + 'px;' + // if empty content - then hide it + if (options.html === '' || options.html == null) { + self.hide(name) + return + } else if (overlay.box) { + // if already present, update it + edata = this.trigger('update', { target: name, overlay }) + if (edata.isCancelled === true) { + // restore previous options + if (overlay.prevOptions) { + overlay.options = overlay.prevOptions + delete overlay.prevOptions + } + return + } + query(overlay.box) + .find('.w2ui-overlay-body') + .attr('style', (options.style || '') + '; ' + overlayStyles) + .removeClass() // removes all classes + .addClass('w2ui-overlay-body ' + options.class + (options.draggable ? ' w2ui-draggable' : '')) + .html(options.html) + this.resize(overlay.name) + } else { + // event before + edata = this.trigger('show', { target: name, overlay }) + if (edata.isCancelled === true) return + // normal processing + query('body').append( + // pointer-events will be re-enabled leter + ``) + overlay.box = query('#' + w2utils.escapeId(overlay.id))[0] + overlay.displayed = true + const names = query(overlay.anchor).data('tooltipName') ?? [] + names.push(name) + query(overlay.anchor).data('tooltipName', names) // make available to element overlay attached to + w2utils.bindEvents(overlay.box, {}) + // remember anchor's original styles + overlay.tmp.originalCSS = '' + if (query(overlay.anchor).length > 0) { + overlay.tmp.originalCSS = query(overlay.anchor)[0].style.cssText + } + this.resize(overlay.name) + } + if (options.anchorStyle) { + overlay.anchor.style.cssText += ';' + options.anchorStyle + } + if (options.anchorClass) { + // do not add w2ui-focus to body + if (!(options.anchorClass == 'w2ui-focus' && overlay.anchor == document.body)) { + query(overlay.anchor).addClass(options.anchorClass) + } + } + // add on hide events + if (typeof options.hideOn === 'string') options.hideOn = [options.hideOn] + if (!Array.isArray(options.hideOn)) options.hideOn = [] + // initial scroll + Object.assign(overlay.tmp, { + scrollLeft: document.body.scrollLeft, + scrollTop: document.body.scrollTop + }) + addHideEvents() + addWatchEvents(document.body) + // first show empty tooltip, so it will popup up in the right position + query(overlay.box).show() + overlay.tmp.observeTooltipResize.observe(overlay.box) + overlay.tmp.observeAnchorResize.observe(overlay.anchor) + overlay.tmp.observeAnchorMove.observe(overlay.anchor, { attributes: true }) + // observer element removal from DOM + Tooltip.observeRemove.observe(document.body, { subtree: true, childList: true }) + // then insert html and it will adjust + query(overlay.box) + .css('opacity', 1) + .find('.w2ui-overlay-body') + .html(options.html) + /** + * pointer-events: none is needed to avoid cases when popup is shown right under the cursor + * or it will trigger onmouseout, onmouseleave and other events. + */ + setTimeout(() => { query(overlay.box).css({ 'pointer-events': 'auto' }).data('ready', 'yes') }, 100) + // bind events + w2utils.bindEvents(query(overlay.box).find('.w2ui-eaction'), this) + delete overlay.needsUpdate + // expose overlay to DOM element + overlay.box.overlay = overlay + // event after + if (edata) edata.finish() + return { overlay } + function addWatchEvents(el) { + const scope = 'tooltip-' + overlay.name + let queryEl = el + if (el.tagName == 'BODY') { + queryEl = el.ownerDocument + } + query(queryEl) + .off(`.${scope}`) + .on(`scroll.${scope}`, event => { + Object.assign(overlay.tmp, { + scrollLeft: el.scrollLeft, + scrollTop: el.scrollTop + }) + self.resize(overlay.name) + }) + } + function addHideEvents() { + const hide = (event) => { self.hide(overlay.name) } + const $anchor = query(overlay.anchor) + const scope = 'tooltip-' + overlay.name + // document click + query('html').off(`.${scope}`) + if (options.hideOn.includes('doc-click')) { + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + // otherwise hides on click to focus + $anchor + .off(`.${scope}-doc`) + .on(`click.${scope}-doc`, (event) => { event.stopPropagation() }) + } + query('html').on(`click.${scope}`, hide) + } + if (options.hideOn.includes('focus-change') || options.hideOn.includes('blur')) { + query('html') + .on(`focusin.${scope}`, (e) => { + if (document.activeElement != overlay.anchor) { + self.hide(overlay.name) + } + }) + } + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + $anchor.off(`.${scope}`) + options.hideOn.forEach(event => { + if (['doc-click', 'focus-change', 'blur'].indexOf(event) == -1) { + $anchor.on(`${event}.${scope}`, { once: true }, hide) + } + }) + } + } + } + hide(name) { + let overlay + if (arguments.length == 0) { + // hide all tooltips + Object.keys(Tooltip.active).forEach(name => { this.hide(name) }) + return + } + if (name instanceof HTMLElement) { + const names = query(name).data('tooltipName') ?? [] + names.forEach(name => { this.hide(name) }) + return + } + if (typeof name === 'string') { + name = name.replace(/[\s\.#]/g, '_') + overlay = Tooltip.active[name] + } + if (overlay?.tmp) overlay.tmp.hidden = true // it could be hidden before it is actually shown + if (!overlay || !overlay.box) return + // event before + const edata = this.trigger('hide', { target: name, overlay }) + if (edata.isCancelled === true) return + // normal processing + if (!overlay.options._keep) delete Tooltip.active[name] + const scope = 'tooltip-' + overlay.name + overlay.tmp.observeTooltipResize?.disconnect() + overlay.tmp.observeAnchorResize?.disconnect() + overlay.tmp.observeAnchorMove?.disconnect() + if (overlay.options.watchScroll) { + query(overlay.options.watchScroll) + .off('.w2scroll-' + overlay.name) + } + // if no active tooltip then disable observeRemove + let cnt = 0 + Object.keys(Tooltip.active).forEach(key => { + const overlay = Tooltip.active[key] + if (overlay.displayed) { + cnt++ + } + }) + if (cnt == 0) { + Tooltip.observeRemove.disconnect() + } + query('html').off(`.${scope}`) // hide to click event here + query(document).off(`.${scope}`) // scroll event here + // remove element + overlay.box?.remove() + overlay.box = null + overlay.displayed = false + // remove name from anchor properties + const names = query(overlay.anchor).data('tooltipName') ?? [] + const ind = names.indexOf(overlay.name) + if (ind != -1) names.splice(names.indexOf(overlay.name), 1) + if (names.length == 0) { + query(overlay.anchor).removeData('tooltipName') + } else { + query(overlay.anchor).data('tooltipName', names) + } + // restore original CSS, only if anchor styles where extended + if (overlay.options.anchorStyle) { + overlay.anchor.style.cssText = overlay.tmp.originalCSS + } + query(overlay.anchor) + .off(`.${scope}`) + .removeClass(overlay.options.anchorClass) + // for remote data source + if (overlay.options.url) { + // remove all cached items + overlay.options.items.splice(0) + overlay.tmp.remote.hasMore = true + overlay.tmp.remote.search = null + } + // event after + edata.finish() + } + resize(name) { + const state = { moved: false, resize: false } + if (arguments.length == 0) { + Object.keys(Tooltip.active).forEach(key => { + const overlay = Tooltip.active[key] + if (overlay.displayed) this.resize(overlay.name) + }) + return { multiple: true } + } + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + const pos = this.getPosition(overlay.name) + const newPos = pos.left + 'x' + pos.top + const newSize = pos.width + 'x' + pos.height + let edata1, edata2 + if (overlay.tmp.lastPos != newPos) { + edata1 = this.trigger('move', { target: name, overlay, pos }) + state.moved = true + } + if (overlay.tmp.lastSize != newSize) { + edata2 = this.trigger('resize', { target: name, overlay, pos }) + state.moved = true + } + query(overlay.box) + .css({ + left: pos.left + 'px', + top: pos.top + 'px' + }) + .then(query => { + if (pos.width != null) { + query.css('width', pos.width + 'px') + .find('.w2ui-overlay-body') + .css('width', '100%') + } + if (pos.height != null) { + query.css('height', pos.height + 'px') + .find('.w2ui-overlay-body') + .css('height', '100%') + } + }) + .find('.w2ui-overlay-body') + .removeClass('w2ui-arrow-right w2ui-arrow-left w2ui-arrow-top w2ui-arrow-bottom') + .addClass(pos.arrow.class) + .closest('.w2ui-overlay') + .find('style') + .text(pos.arrow.style) + if (overlay.tmp.lastPos != newPos && edata1) { + overlay.tmp.lastPos = newPos + edata1.finish() + } + if (overlay.tmp.lastSize != newSize && edata2) { + overlay.tmp.lastSize = newSize + edata2.finish() + } + return state + } + getPosition(name) { + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + if (!overlay || !overlay.box) { + return + } + const options = overlay.options + if (overlay.tmp.resizedY || overlay.tmp.resizedX) { + query(overlay.box).css({ width: '', height: '', scroll: 'auto' }) + } + const scrollSize = w2utils.scrollBarSize() + const hasScrollBarX = !(document.body.scrollWidth == document.body.clientWidth) + const hasScrollBarY = !(document.body.scrollHeight == document.body.clientHeight) + const max = { + width: window.innerWidth - (hasScrollBarY ? scrollSize : 0), + height: window.innerHeight - (hasScrollBarX ? scrollSize : 0) + } + const position = options.position == 'auto' + ? 'top|bottom|right|left'.split('|') + : Array.isArray(options.position) ? options.position : options.position.split('|') + const isVertical = ['top', 'bottom'].includes(position[0]) + const content = overlay.box.getBoundingClientRect() + let anchor = overlay.anchor?.getBoundingClientRect?.() + if (overlay.anchor == document.body) { + // context menu + let evt = options.originalEvent + while (evt?.originalEvent) { evt = evt.originalEvent } + const { x, y, width, height } = evt ?? { x: options.x, y: options.y, width: 0, height: 10 } + anchor = { + left: x - (options.contextMenu ? 4 : 0), + top: y - (options.contextMenu ? 4 : 0), + width: width ?? 0, + height: height ?? 10, + arrow: options.contextMenu ? 'none' : null + } + } + let arrowSize = options.arrowSize + if (anchor.arrow == 'none') arrowSize = 0 + if (isNaN(arrowSize)) arrowSize = this.defaults.arrowSize + // space available + const available = { // tipsize adjustment should be here, not in max.width/max.height + top: anchor.top - arrowSize, + bottom: max.height - arrowSize - (anchor.top + anchor.height) - (hasScrollBarX ? scrollSize : 0) - 2, + left: anchor.left, + right: max.width - (anchor.left + anchor.width) + (hasScrollBarY ? scrollSize : 0) + } + // size of empty tooltip + if (content.width < 22) content.width = 22 + if (content.height < 14) content.height = 14 + let left, top, width, height // tooltip position + let found = '' + const arrow = { + offset: 0, + class: '', + style: `#${overlay.id} { --tip-size: ${arrowSize}px; }` + } + const adjust = { left: 0, top: 0 } + const bestFit = { posX: '', x: 0, posY: '', y: 0 } + // find best position + position.forEach(pos => { + if (['top', 'bottom'].includes(pos)) { + if (!found && (content.height + arrowSize / 1.893) < available[pos]) { // 1.893 = 1 + sin(90) + found = pos + } + if (available[pos] > bestFit.y) { + Object.assign(bestFit, { posY: pos, y: available[pos] }) + } + } + if (['left', 'right'].includes(pos)) { + if (!found && (content.width + arrowSize / 1.893) < available[pos]) { // 1.893 = 1 + sin(90) + found = pos + } + if (available[pos] > bestFit.x) { + Object.assign(bestFit, { posX: pos, x: available[pos] }) + } + } + }) + // if not found, use best (greatest available space) position + if (!found) { + if (isVertical) { + found = bestFit.posY + } else { + found = bestFit.posX + } + } + if (options.autoResize) { + if (['top', 'bottom'].includes(found)) { + if (content.height > available[found]) { + height = available[found] + overlay.tmp.resizedY = true + } else { + overlay.tmp.resizedY = false + } + } + if (['left', 'right'].includes(found)) { + if (content.width > available[found]) { + width = available[found] + overlay.tmp.resizedX = true + } else { + overlay.tmp.resizedX = false + } + } + } + usePosition(found) + if (isVertical) anchorAlignment() + // user offset + top += parseFloat(options.offsetY * (found == 'top' ? -1 : 1)) // offset will always move from original position + left += parseFloat(options.offsetX * (found == 'left' ? -1 : 1)) // offset will always move from original position + // make sure it is inside visible screen area + screenAdjust() + // adjust for scrollbar + const extraTop = (found == 'top' ? -options.margin : (found == 'bottom' ? options.margin : 0)) + const extraLeft = (found == 'left' ? -options.margin : (found == 'right' ? options.margin : 0)) + top = Math.floor((top + parseFloat(extraTop)) * 100) / 100 + left = Math.floor((left + parseFloat(extraLeft)) * 100) / 100 + return { left, top, arrow, adjust, width, height, pos: found } + function usePosition(pos) { + arrow.class = anchor.arrow ? anchor.arrow : `w2ui-arrow-${pos}` + switch (pos) { + case 'top': { + left = anchor.left + (anchor.width - (width ?? content.width)) / 2 + top = anchor.top - (height ?? content.height) - arrowSize / 1.5 + 1 + break + } + case 'bottom': { + left = anchor.left + (anchor.width - (width ?? content.width)) / 2 + top = anchor.top + anchor.height + arrowSize / 1.25 + 1 + break + } + case 'left': { + left = anchor.left - (width ?? content.width) - arrowSize / 1.2 - 1 + top = anchor.top + (anchor.height - (height ?? content.height)) / 2 + break + } + case 'right': { + left = anchor.left + anchor.width + arrowSize / 1.2 + 1 + top = anchor.top + (anchor.height - (height ?? content.height)) / 2 + break + } + } + } + function anchorAlignment() { + // top/bottom alignments + if (options.align == 'left') { + adjust.left = anchor.left - left + left = anchor.left + } + if (options.align == 'right') { + adjust.left = (anchor.left + anchor.width - (width ?? content.width)) - left + left = anchor.left + anchor.width - (width ?? content.width) + } + if (['top', 'bottom'].includes(found) && options.align.startsWith('both')) { + const minWidth = options.align.split(':')[1] ?? 50 + if (anchor.width >= minWidth) { + left = anchor.left + width = anchor.width + } + } + // left/right alignments + if (options.align == 'top') { + adjust.top = anchor.top - top + top = anchor.top + } + if (options.align == 'bottom') { + adjust.top = (anchor.top + anchor.height - (height ?? content.height)) - top + top = anchor.top + anchor.height - (height ?? content.height) + } + if (['left', 'right'].includes(found) && options.align.startsWith('both')) { + const minHeight = options.align.split(':')[1] ?? 50 + if (anchor.height >= minHeight) { + top = anchor.top + height = anchor.height + } + } + } + function screenAdjust() { + let adjustArrow + // adjust tip if needed after alignment + if ((['left', 'right'].includes(options.align) && anchor.width < (width ?? content.width)) || + (['top', 'bottom'].includes(options.align) && anchor.height < (height ?? content.height)) + ) { + adjustArrow = true + } + // if off screen then adjust + const minLeft = (found == 'right' ? arrowSize : options.screenMargin) + const minTop = (found == 'bottom' ? arrowSize : options.screenMargin) + const maxLeft = max.width - (width ?? content.width) - (found == 'left' ? arrowSize : options.screenMargin) + const maxTop = max.height - (height ?? content.height) - (found == 'top' ? arrowSize : options.screenMargin) + 3 + // adjust X + if (['top', 'bottom'].includes(found) || options.autoResize) { + if (left < minLeft) { + adjustArrow = true + adjust.left -= left + left = minLeft + } + if (left > maxLeft) { + adjustArrow = true + adjust.left -= left - maxLeft + left += maxLeft - left + } + } + // adjust Y + if (['left', 'right'].includes(found) || options.autoResize) { + if (top < minTop) { + adjustArrow = true + adjust.top -= top + top = minTop + } + if (top > maxTop) { + adjustArrow = true + adjust.top -= top - maxTop + top += maxTop - top + } + } + // moves carret to adjust it with element width + if (adjustArrow) { + const aType = isVertical ? 'left' : 'top' + const sType = isVertical ? 'width' : 'height' + arrow.offset = -adjust[aType] + const maxOffset = content[sType] / 2 - arrowSize + if (Math.abs(arrow.offset) > maxOffset + arrowSize) { + arrow.class = '' // no arrow + } + if (Math.abs(arrow.offset) > maxOffset) { + arrow.offset = arrow.offset < 0 ? -maxOffset : maxOffset + } + arrow.style = w2utils.stripSpaces(`#${overlay.id} .w2ui-overlay-body:after, + #${overlay.id} .w2ui-overlay-body:before { + --tip-size: ${arrowSize}px; + margin-${aType}: ${arrow.offset}px; + }`) + } + } + } + startDrag(event) { + let initial + const el = query(event.target).closest('.w2ui-overlay') + initial = { + el, + x: parseFloat(el.css('left')), + y: parseFloat(el.css('top')), + pageX: event.pageX, + pageY: event.pageY + } + query('html') + .off('.w2color') + .on('mousemove.w2color', mouseMove) + .on('mouseup.w2color', mouseUp) + function mouseUp(event) { + query('html').off('.w2color') + } + function mouseMove(event) { + const divX = event.pageX - initial.pageX + const divY = event.pageY - initial.pageY + initial.el.css({ + left: initial.x + divX + 'px', + top: initial.y + divY + 'px' + }) + if (!initial._removed) { + initial._removed = true + initial.el.find(':scope > .w2ui-overlay-body') + .removeClass('w2ui-arrow-right w2ui-arrow-left w2ui-arrow-top w2ui-arrow-bottom') + } + } + } +} +class ColorTooltip extends Tooltip { + static custom_colors = [] + constructor() { + super() + this.palette = [ + ['000000', '333333', '555555', '777777', '888888', '999999', 'AAAAAA', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'F7F7F7', 'FFFFFF'], + ['FF011B', 'FF9838', 'FFC300', 'FFFD59', '86FF14', '14FF7A', '2EFFFC', '2693FF', '006CE7', '9B24F4', 'FF21F5', 'FF0099'], + ['FFEAEA', 'FCEFE1', 'FCF4DC', 'FFFECF', 'EBFFD9', 'D9FFE9', 'E0FFFF', 'E8F4FF', 'ECF4FC', 'EAE6F4', 'FFF5FE', 'FCF0F7'], + ['F4CCCC', 'FCE5CD', 'FFF1C2', 'FFFDA1', 'D5FCB1', 'B5F7D0', 'BFFFFF', 'D6ECFF', 'CFE2F3', 'D9D1E9', 'FFE3FD', 'FFD9F0'], + ['EA9899', 'F9CB9C', 'FFE48C', 'F7F56F', 'B9F77E', '84F0B1', '83F7F7', 'B5DAFF', '9FC5E8', 'B4A7D6', 'FAB9F6', 'FFADDE'], + ['E06666', 'F6B26B', 'DEB737', 'E0DE51', '8FDB48', '52D189', '4EDEDB', '76ACE3', '6FA8DC', '8E7CC3', 'E07EDA', 'F26DBD'], + ['CC0814', 'E69138', 'AB8816', 'B5B20E', '6BAB30', '27A85F', '1BA8A6', '3C81C7', '3D85C6', '674EA7', 'A14F9D', 'BF4990'], + ['99050C', 'B45F17', '80650E', '737103', '395E14', '10783D', '13615E', '094785', '0A5394', '351C75', '780172', '782C5A'] + ] + this.defaults = w2utils.extend({}, this.defaults, { + advanced: false, + transparent: true, + position: 'top|bottom', + class: 'w2ui-white', + color: '', + updateInput: true, + arrowSize: 12, + autoResize: false, + anchorClass: 'w2ui-focus', + autoShowOn: 'focus', + hideOn: ['doc-click', 'focus-change'], + onSelect: null, + onLiveUpdate: null + }) + } + attach(anchor, text) { + let options + if (arguments.length == 1 && anchor instanceof Object) { + options = anchor + anchor = options.anchor + } else if (arguments.length === 2 && text != null && typeof text === 'object') { + options = text + options.anchor = anchor + } + const prevHideOn = options.hideOn + options = w2utils.extend({}, this.defaults, options || {}) + if (prevHideOn) { + options.hideOn = prevHideOn + } + options.style += '; padding: 0;' + // add remove transparent color + if (options.transparent && this.palette[0][1] == '333333') { + this.palette[0].splice(1, 1) + this.palette[0].push('TRANSPARENT') + } + if (!options.transparent && this.palette[0][1] != '333333') { + this.palette[0].splice(1, 0, '333333') + this.palette[0].pop() + } + if (options.color) options.color = String(options.color).toUpperCase() + if (typeof options.color === 'string' && options.color.substr(0, 1) === '#') options.color = options.color.substr(1) + // needed for keyboard navigation + this.index = [-1, -1] + const ret = super.attach(options) + const overlay = ret.overlay + overlay.options.html = this.getColorHTML(overlay.name, options) + overlay.on('show.attach', event => { + const overlay = event.detail.overlay + const anchor = overlay.anchor + const options = overlay.options + if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && !options.color && anchor.value) { + overlay.tmp.initColor = anchor.value + } + delete overlay.newColor + }) + overlay.on('show:after.attach', event => { + if (ret.overlay?.box) { + const actions = query(ret.overlay.box).find('.w2ui-eaction') + w2utils.bindEvents(actions, this) + this.initControls(ret.overlay) + } + }) + overlay.on('update:after.attach', event => { + if (ret.overlay?.box) { + const actions = query(ret.overlay.box).find('.w2ui-eaction') + w2utils.bindEvents(actions, this) + this.initControls(ret.overlay) + } + }) + overlay.on('hide.attach', event => { + const overlay = event.detail.overlay + const anchor = overlay.anchor + const color = overlay.newColor ?? overlay.options.color ?? '' + // color has been selected + if (color !== '') { + if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && anchor.value != color && overlay.options.updateInput) { + anchor.value = color + } + const edata = this.trigger('select', { color, target: overlay.name, overlay }) + if (edata.isCancelled === true) return + // event after + edata.finish() + } + }) + ret.liveUpdate = (callback) => { + overlay.on('liveUpdate.attach', (event) => { callback(event) }) + return ret + } + ret.select = (callback) => { + overlay.on('select.attach', (event) => { callback(event) }) + return ret + } + return ret + } + // regular panel handler, adds selection class + select(color, name) { + let target + this.index = [-1, -1] + if (typeof name !== 'string') { + target = name.target + this.index = query(target).attr('index').split(':') + name = query(target).closest('.w2ui-overlay').attr('name') + } + const overlay = this.get(name) + // event before + const edata = this.trigger('liveUpdate', { color, target: name, overlay, param: arguments[1] }) + if (edata.isCancelled === true) return + // if anchor is input - live update + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName) && overlay.options.updateInput) { + query(overlay.anchor).val(color) + } + overlay.newColor = color + query(overlay.box).find('.w2ui-color.w2ui-selected').removeClass('w2ui-selected') + if (target) { + query(target).addClass('w2ui-selected') + } + // event after + edata.finish() + } + // used for keyboard navigation, if any + nextColor(direction) { // TODO: check it + const pal = this.palette + switch (direction) { + case 'up': + this.index[0]-- + break + case 'down': + this.index[0]++ + break + case 'right': + this.index[1]++ + break + case 'left': + this.index[1]-- + break + } + if (this.index[0] < 0) this.index[0] = 0 + if (this.index[0] > pal.length - 2) this.index[0] = pal.length - 2 + if (this.index[1] < 0) this.index[1] = 0 + if (this.index[1] > pal[0].length - 1) this.index[1] = pal[0].length - 1 + return pal[this.index[0]][this.index[1]] + } + tabClick(index, name) { + if (typeof name !== 'string') { + name = query(name.target).closest('.w2ui-overlay').attr('name') + } + const overlay = this.get(name) + const tab = query(overlay.box).find(`.w2ui-color-tab:nth-child(${index})`) + query(overlay.box).find('.w2ui-color-tab').removeClass('w2ui-selected') + query(tab).addClass('w2ui-selected') + query(overlay.box) + .find('.w2ui-tab-content') + .hide() + .closest('.w2ui-colors') + .find('.tab-' + index) + .show() + } + // generate HTML with color pallent and controls + getColorHTML(name, options) { + let html = ` +
+ Colors +
+
+
` + for (let i = 0; i < this.palette.length; i++) { + html += '
' + for (let j = 0; j < this.palette[i].length; j++) { + const color = this.palette[i][j] + let border = '' + if (color === 'FFFFFF') border = '; border: 1px solid #efefef' + html += ` +
  +
` + } + html += '
' + if (i < 2) html += '
' + } + // custom colors + html += ` +
+
+ ${this.getCustomColorsHTML(name)} +
` + html += '
' + // advanced tab + html += ` + ` + // color tabs on the bottom + html += ` +
+
+
+
+ ${(typeof options.html === 'string' ? options.html : '')} +
+
` + return html + } + getCustomColorsHTML(name) { + const options = this.get(name)?.options + let html = '
' + ColorTooltip.custom_colors.forEach((color, i) => { + let border = '' + if (color === 'FFFFFF') border = '; border: 1px solid #efefef' + html += ` +
  +
` + }) + html += ` +
+ +
+
` + return html + } + // bind advanced tab controls + initControls(overlay) { + let initial // used for mouse events + const self = this + const options = overlay.options + const color = options.color || overlay.tmp.initColor + let rgb = w2utils.parseColor(color) + if (rgb == null) { + rgb = { r: 140, g: 150, b: 160, a: 1 } + } + let hsv = w2utils.rgb2hsv(rgb) + if (options.advanced === true) { + this.tabClick(2, overlay.name) + } + setColor(hsv, true, color ?? '') // should not be null or undefined + // even for rgb, hsv inputs + query(overlay.box) + .off('.w2color') + .on('contextmenu.w2color', event => { + event.preventDefault() // prevent browser context menu + }) + .find('input') + .off('.w2color') + .on('change.w2color', (event) => { + const el = query(event.target) + let val = parseFloat(el.val()) + const max = parseFloat(el.attr('max')) + if (isNaN(val)) { + val = 0 + el.val(0) + } + if (max > 1) val = parseInt(val) // trancate fractions + if (max > 0 && val > max) { + el.val(max) + val = max + } + if (val < 0) { + el.val(0) + val = 0 + } + const name = el.attr('name') + const color = {} + if (['r', 'g', 'b', 'a'].indexOf(name) !== -1) { + rgb[name] = val + hsv = w2utils.rgb2hsv(rgb) + } else if (['h', 's', 'v'].indexOf(name) !== -1) { + color[name] = val + } + setColor(color, true) + }) + // click on original color resets it + query(overlay.box).find('.color-original') + .off('.w2color') + .on('click.w2color', (event) => { + const tmp = w2utils.parseColor(query(event.target).css('background-color')) + if (tmp != null) { + rgb = tmp + hsv = w2utils.rgb2hsv(rgb) + setColor(hsv, true) + } + }) + // color sliders events + const mDown = `${!w2utils.isMobile ? 'mousedown' : 'touchstart'}.w2color` + const mUp = `${!w2utils.isMobile ? 'mouseup' : 'touchend'}.w2color` + const mMove = `${!w2utils.isMobile ? 'mousemove' : 'touchmove'}.w2color` + query(overlay.box).find('.palette, .rainbow, .alpha') + .off('.w2color') + .on(`${mDown}.w2color`, mouseDown) + this.setColor = setColor + return + function setColor(color, fullUpdate, initial) { + if (color.h != null) hsv.h = color.h + if (color.s != null) hsv.s = color.s + if (color.v != null) hsv.v = color.v + if (color.a != null) { rgb.a = color.a; hsv.a = color.a } + rgb = w2utils.hsv2rgb(hsv) + const rgba = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + rgb.a + ')' + const cl = [ + Number(rgb.r).toString(16).toUpperCase(), + Number(rgb.g).toString(16).toUpperCase(), + Number(rgb.b).toString(16).toUpperCase(), + (Math.round(Number(rgb.a) * 255)).toString(16).toUpperCase() + ] + cl.forEach((item, ind) => { if (item.length === 1) cl[ind] = '0' + item }) + let newColor = cl[0] + cl[1] + cl[2] + cl[3] + if (rgb.a === 1) { + newColor = cl[0] + cl[1] + cl[2] + } + query(overlay.box).find('.color-preview').css('background-color', '#' + newColor) + query(overlay.box).find('input').each(el => { + if (el.name) { + if (rgb[el.name] != null) el.value = rgb[el.name] + if (hsv[el.name] != null) el.value = hsv[el.name] + if (el.name === 'a') el.value = rgb.a + if (el.name == 'hex') el.value = newColor + if (el.name == 'rgb') el.value = rgba + } + }) + // if it is in pallette + if (initial != null) { + const color = overlay.tmp.initColor || newColor + query(overlay.box).find('.color-original') + .css('background-color', '#' + color) + query(overlay.box).find('.w2ui-color.w2ui-selected') + .removeClass('w2ui-selected') + query(overlay.box).find(`.w2ui-colors [name="${color}"], .w2ui-colors [name="${initial}"]`) // color conversion might be slightly off + .addClass('w2ui-selected') + // if has transparent color, open advanced tab + if (newColor.length == 8) { + self.tabClick(2, overlay.name) + } + } else { + self.select(newColor, overlay.name) + } + if (fullUpdate) { + updateSliders() + refreshPalette() + } + } + function updateSliders() { + const el1 = query(overlay.box).find('.palette .value1') + const el2 = query(overlay.box).find('.rainbow .value2') + const el3 = query(overlay.box).find('.alpha .value2') + if (!el1[0] || !el2[0] || !el3[0]) return + const offset1 = parseInt(el1[0].clientWidth) / 2 + const offset2 = parseInt(el2[0].clientWidth) / 2 + el1.css({ + 'left': (hsv.s * 150 / 100 - offset1) + 'px', + 'top': ((100 - hsv.v) * 125 / 100 - offset1) + 'px' + }) + el2.css('left', (hsv.h / (360 / 150) - offset2) + 'px') + el3.css('left', (rgb.a * 150 - offset2) + 'px') + } + function refreshPalette() { + const cl = w2utils.hsv2rgb(hsv.h, 100, 100) + const rgb = `${cl.r},${cl.g},${cl.b}` + query(overlay.box).find('.palette') + .css('background-image', `linear-gradient(90deg, rgba(${rgb},0) 0%, rgba(${rgb},1) 100%)`) + } + function mouseDown(event) { + const el = query(this).find('.value1, .value2') + const offset = parseInt(el.prop('clientWidth')) / 2 + if (el.hasClass('move-x')) el.css({ left: (event.offsetX - offset) + 'px' }) + if (el.hasClass('move-y')) el.css({ top: (event.offsetY - offset) + 'px' }) + initial = { + el: el, + x: event.pageX, + y: event.pageY, + width: el.prop('parentNode').clientWidth, + height: el.prop('parentNode').clientHeight, + left: parseInt(el.css('left')), + top: parseInt(el.css('top')) + } + mouseMove(event) + query('html') + .off('.w2color') + .on(mMove, mouseMove) + .on(mUp, mouseUp) + } + function mouseUp(event) { + query('html').off('.w2color') + } + function mouseMove(event) { + const el = initial.el + const divX = event.pageX - initial.x + const divY = event.pageY - initial.y + let newX = initial.left + divX + let newY = initial.top + divY + const offset = parseInt(el.prop('clientWidth')) / 2 + if (newX < -offset) newX = -offset + if (newY < -offset) newY = -offset + if (newX > initial.width - offset) newX = initial.width - offset + if (newY > initial.height - offset) newY = initial.height - offset + if (el.hasClass('move-x')) el.css({ left: newX + 'px' }) + if (el.hasClass('move-y')) el.css({ top: newY + 'px' }) + // move + const name = query(el.get(0).parentNode).attr('name') + const x = parseInt(el.css('left')) + offset + const y = parseInt(el.css('top')) + offset + if (name === 'palette') { + setColor({ + s: Math.round(x / initial.width * 100), + v: Math.round(100 - (y / initial.height * 100)) + }) + } + if (name === 'rainbow') { + const h = Math.round(360 / 150 * x) + setColor({ h: h }) + refreshPalette() + } + if (name === 'alpha') { + setColor({ a: parseFloat(Number(x / 150).toFixed(2)) }) + } + } + } + addCustomColor(color, name) { + if (typeof color === 'string' && color.substr(0, 1) == '#' && [7, 9].includes(color.length)) { + color = color.substr(1).toUpperCase() + const custom = ColorTooltip.custom_colors + if (custom.includes(color)) { + custom.splice(custom.indexOf(color), 1) + } + if (custom.length >= 5) { + custom.pop() // removes last one + } + custom.unshift(color) + } + return color + } + async pickAndSelect(name, event) { + const color = await this.pickColor() + if (typeof color === 'string' && color.substr(0, 1) == '#' && [7, 9].includes(color.length)) { + this.addCustomColor(color, name) + const cnt = query(event.target).closest('.w2ui-colors-custom') + cnt.html(this.getCustomColorsHTML(name)) + w2utils.bindEvents(cnt.find('.w2ui-eaction'), this) + this.select(color.substr(1), name) + // this.hide(name) + } + } + async pickAndUse(name) { + const color = await this.pickColor() + if (typeof color === 'string' && color.substr(0, 1) == '#' && [7, 9].includes(color.length)) { + const hsv = w2utils.rgb2hsv(w2utils.parseColor(color)) + this.setColor(hsv, true) + } + } + async pickColor() { + if (!window.EyeDropper) { + console.error('EyeDropper API is not supported in this browser.') + return + } + const eyeDropper = new EyeDropper() + try { + const result = await eyeDropper.open() + return result.sRGBHex + } catch (err) { + console.error('Error picking color:', err) + } + return '' + } +} +class MenuTooltip extends Tooltip { + constructor() { + super() + // ITEM STRUCTURE + // item : { + // id : null, + // text : '', + // style : '', + // icon : '', + // count : '', + // tooltip : '', + // hotkey : '', + // removable: false, + // help : '', // text for help tooltip + // hotkey ; '', // hotkey text for the items + // items : [] + // indent : 0, + // type : null, // check/radio + // group : false, // groupping for checks + // expanded : false, + // hidden : false, + // checked : null, + // disabled : false + // ... + // } + this.defaults = w2utils.extend({}, this.defaults, { + type: 'normal', // can be normal, radio, check + items: [], + selected: null, // current selected + render: null, + spinner: false, + msgNoItems: w2utils.lang('No items found'), + topHTML: '', + menuStyle: '', + search: false, // search input inside tooltip + filter: false, // will apply filter, if anchor is INPUT or TEXTAREA + match: 'contains', // is, begins, ends, contains + markSearch: false, + prefilter: false, + altRows: false, + arrowSize: 10, + align: 'left', + position: 'bottom|top', + class: 'w2ui-white', + anchorClass: 'w2ui-focus', + autoShowOn: 'focus', + hideOn: ['doc-click', 'focus-change', 'select'], // also can 'item-remove' + onSelect: null, + onSubMenu: null, + onRemove: null, + onTooltip: null, + onMouseEnter: null, + onMouseLeave: null + }) + } + attach(anchor, text) { + let options + if (arguments.length == 1 && anchor instanceof Object) { + options = anchor + anchor = options.anchor + } else if (arguments.length === 2 && text != null && typeof text === 'object') { + options = text + options.anchor = anchor + } + const prevHideOn = options.hideOn + options = w2utils.extend({}, this.defaults, options || {}) + if (prevHideOn) { + options.hideOn = prevHideOn + } + options.style += '; padding: 0;' + if (options.items == null) { + options.items = [] + } + if (options.cacheMax <= 0) { + console.log(`The option "cacheMax" is ${options.cacheMax} but should be more than 0`) + } + options.items = w2utils.normMenu(options.items, options) + options.html = this.getMenuHTML(options) + const ret = super.attach(options) + const overlay = ret.overlay + overlay.on('show:after.attach, update:after.attach', event => { + if (ret.overlay?.box) { + let search = '' + // reset selected and active chain + overlay.selected = overlay.options.selected // this is needed so that menu item can be preselected + const index = overlay.anchor.dataset?.selectedIndex + if ((overlay.options.selected !== false && overlay.options.selected !== -1) || index != null) { + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + search = overlay.anchor.value + overlay.selected = null // no element should be pre-selected + if (index != null) { + overlay.selected = index + } + } + } + const actions = query(ret.overlay.box).find('.w2ui-eaction') + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + overlay.tmp._new_search = false + query(overlay.anchor).on('input.search-trigger', () => { + overlay.tmp._new_search = true + query(overlay.anchor).off('input.search-trigger') + }) + } + w2utils.bindEvents(actions, this) + this.applyFilter(overlay.name, null, search) + .then(data => { + if (!Tooltip.active[overlay.name].displayed) { + // if toolitp is not visible, do not proceed as it would make it visible + return + } + this.getActiveChain(overlay.name, options.items) // need this to update chain for up/down key navigation + overlay.tmp.searchCount = data.count + overlay.tmp.search = data.search + if (options.prefilter || search !== '') { + // if selected is not in searched items + if (data.count === 0 || !this.getActiveChain(overlay.name, options.items).includes(overlay.selected)) { + overlay.selected = null + } + this.refreshSearch(overlay.name) + } + this.initControls(ret.overlay) + this.refreshIndex(overlay.name, true) + }) + } + }) + overlay.next = () => { + const chain = this.getActiveChain(overlay.name) + if (overlay.selected == null || overlay.selected?.length == 0) { + overlay.selected = chain[0] + } else { + const ind = chain.indexOf(String(overlay.selected)) // if nested menu, selected will be "2-2" + // selected not in chain of items + if (ind == -1) { + overlay.selected = chain[0] + } + // not the last item + if (ind < chain.length - 1) { + overlay.selected = chain[ind + 1] + } + } + this.refreshIndex(overlay.name) + this.showTooltip(overlay.name) + } + overlay.prev = () => { + const chain = this.getActiveChain(overlay.name) + if (overlay.selected == null || overlay.selected?.length == 0) { + overlay.selected = chain[chain.length - 1] + } else { + const ind = chain.indexOf(String(overlay.selected)) // if nested menu, selected will be "2-2" + // selected not in chain of items + if (ind == -1) { + overlay.selected = chain[chain.length - 1] + } + // not first item + if (ind > 0) { + overlay.selected = chain[ind - 1] + } + } + this.refreshIndex(overlay.name) + this.showTooltip(overlay.name) + } + overlay.click = () => { + $(overlay.box).find('.w2ui-selected').click() + } + overlay.on('hide:after.attach', event => { + w2tooltip.hide(overlay.name + '-tooltip') + }) + ret.select = (callback) => { + overlay.on('select.attach', (event) => { callback(event) }) + return ret + } + ret.remove = (callback) => { + overlay.on('remove.attach', (event) => { callback(event) }) + return ret + } + ret.subMenu = (callback) => { + overlay.on('subMenu.attach', (event) => { callback(event) }) + return ret + } + return ret + } + update(name, items) { + const overlay = Tooltip.active[name] + if (overlay) { + const options = overlay.options + if (options.items != items) { + options.items = items + } + const menuHTML = this.getMenuHTML(options) + if (options.html != menuHTML) { + options.html = menuHTML + overlay.needsUpdate = true + this.show(name) + } + } else { + console.log(`Tooltip "${name}" is not displayed. Cannot update it.`) + } + } + initControls(overlay) { + let mdown = 'mousedown' + let mclick = 'click' + if (w2utils.isMobile) { + mdown = 'touchstart' + mclick = 'touchend' + } + query(overlay.box).find('.w2ui-menu:not(.w2ui-sub-menu)') + .off('.w2menu') + .on('contextmenu.w2menu', event => { + event.preventDefault() // prevent browser context menu + }) + .on(`${mdown}.w2menu`, { delegate: '.w2ui-menu-item' }, event => { + const dt = event.delegate.dataset + this.menuDown(overlay, event, dt.index, dt.parents) + if (w2utils.isMobile) { + // need it for mobile so that it would not generate onclick (items under menu receive focus) + event.preventDefault() + } + }) + .on(`${mclick}.w2menu`, { delegate: '.w2ui-menu-item' }, event => { + const dt = event.delegate.dataset + this.menuClick(overlay, event, parseInt(dt.index), dt.parents) + }) + .find('.w2ui-menu-item') + .off('.w2menu') + .on('mouseEnter.w2menu', event => { + const dt = event.target.dataset + const item = overlay.options.items[dt.index] + const edata = this.trigger('mouseEnter', { overlay, item, originalEvent: event }) + if (edata.isCancelled) { + return + } + const tooltip = item?.tooltip + if (tooltip && dt.hassubmenu != 'yes') { + this.showTooltip(overlay.name, { tooltip, anchor: event.target }) + } + // hide previous sub-menu if any + const _menu = query(event.target).closest('.w2ui-menu').get(0) + if (_menu._evt && _menu._evt.target != event.target) { + this.closeSubMenu(_menu._evt) + } + // show new sub-menu + if (dt.hassubmenu == 'yes') { + const _evt = { + index: parseInt(dt.index), + parents: _menu.dataset.parents !== '' ? _menu.dataset.parents.split('-').map(ind => parseInt(ind)) : [], + target: event.target, + originalEvent: event, + overlay + } + _menu._evt = _evt + this.openSubMenu(_evt) + } + edata.finish() + }) + .on('mouseLeave.w2menu', event => { + const dt = event.target.dataset + const item = overlay.options.items[dt.index] + const edata = this.trigger('mouseLeave', { overlay, item, originalEvent: event }) + if (edata.isCancelled) { + return + } + w2tooltip.hide(overlay.name + '-tooltip') + edata.finish() + }) + .find('.menu-help') + .off('.w2menu') + .on('mouseEnter.w2menu', event => { + const dt = event.target.parentNode.parentNode.dataset + const tooltip = overlay.options.items[dt.index]?.help + if (tooltip) { + w2tooltip.show({ + name: overlay.name + '-help-tp', + anchor: event.target, + html: tooltip, + position: 'right|left', + hideOn: ['doc-click'] + }) + } + }) + .on('mouseLeave.w2menu', event => { + w2tooltip.hide(overlay.name + '-help-tp') + }) + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + query(overlay.anchor) + .off('.w2menu') + .on('input.w2menu', event => { + // if user types, clear selection + // let dt = event.target.dataset + // delete dt.selected + // delete dt.selectedIndex + }) + .on('keyup.w2menu', event => { + event._searchType = 'filter' + this.keyUp(overlay, event) + }) + } + if (overlay.options.search) { + query(overlay.box).find('#menu-search') + .off('.w2menu') + .on('keyup.w2menu', event => { + event._searchType = 'search' + this.keyUp(overlay, event) + }) + } + } + getCurrent(name, id) { + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + const options = overlay.options + const selected = String(id || (overlay.selected || '')).split('-') + if (selected?.[0] === '') { + selected.shift() + } + const last = selected.length - 1 + let index = selected[last] + const parents = selected.slice(0, selected.length - 1).join('-') + index = w2utils.isInt(index) ? parseInt(index) : 0 + // items + let items = options.items + selected.forEach((id, ind) => { + // do not go to the last one + if (ind < selected.length - 1) { + items = items[id].items + } + }) + return { last, index, items, item: items[index], parents } + } + getMenuHTML(options) { + if (options.spinner) { + return ` +
+
+
+ ${w2utils.lang('Loading...')} +
+
` + } + const parents = options.parents ?? [] + let items = options.items + if (!Array.isArray(items)) items = [] + let count = 0 + let icon = null + let topHTML = '' + if (options.search) { + topHTML += ` + ` + items.forEach(item => item.hidden = false) + } + if (options.topHTML) { + topHTML += `
${options.topHTML}
` + } + let menu_html = ` + ${topHTML} +
+ ` + items.forEach((mitem, f) => { + icon = mitem.icon + const index = (parents.length > 0 ? parents.join('-') + '-' : '') + f + if (icon == null) icon = null // icon might be undefined + if (['radio', 'check'].includes(options.type) && !Array.isArray(mitem.items) && mitem.group !== false) { + if (mitem.checked === true) icon = 'w2ui-icon-check'; else icon = 'w2ui-icon-empty' + } + if (mitem.hidden !== true) { + let txt = mitem.text + let icon_dsp = '' + if (typeof options.render === 'function') txt = options.render(mitem, options) + if (typeof txt === 'function') txt = txt(mitem, options) + if (icon) { + const first = String(icon).trim().slice(0, 1) + if (first == '#') { + icon = `` + } else if (first !== '<') { + icon = `` + } + icon_dsp = `` + } + // for backward compatibility + if (mitem.removable == null && mitem.remove != null) { + mitem.removable = mitem.remove + } + // render only if non-empty + if (mitem.type !== 'break' && txt != null && txt !== '' && String(txt).substr(0, 2) != '--') { + const classes = ['w2ui-menu-item'] + if (options.altRows == true) { + classes.push(count % 2 === 0 ? 'w2ui-even' : 'w2ui-odd') + } + let colspan = 1 + if (icon_dsp === '') colspan++ + if (mitem.count == null && mitem.hotkey == null && mitem.removable !== true && mitem.items == null) colspan++ + if (mitem.tooltip == null && mitem.hint != null) mitem.tooltip = mitem.hint // for backward compatibility + let count_dsp = '' + if (mitem.removable === true) { + count_dsp = 'x' + } else if (mitem.items != null) { + classes.push('has-sub-menu') + count_dsp = '' // used as drop arrow + } else { + if (mitem.count != null) count_dsp += '' + mitem.count + '' + if (mitem.hotkey != null) count_dsp += '' + mitem.hotkey + '' + if (mitem.help != null) count_dsp += '?' + } + if (mitem.disabled === true) classes.push('w2ui-disabled') + if (mitem._noSearchInside === true) classes.push('w2ui-no-search-inside') + menu_html += ` +
+
+ ${icon_dsp} + + +
` + count++ + } else { + // horizontal line + const divText = (txt ?? '').replace(/^-+/g, '') + menu_html += ` +
+
+ ${divText ? `
${divText}
` : ''} +
` + } + } + items[f] = mitem + }) + if (count === 0 && options.msgNoItems) { + const overlay = Tooltip.active[options.name.replace(/[\s\.#]/g, '_')] + const remote = overlay?.tmp.remote + let msg = options.msgNoItems + if (options.url) { + if (count == 0 && remote?.hasMore === false) { + // if search is applied, but there are no items + msg = options.msgNoItems + } else { + msg = options.msgSearch + } + } + menu_html += ` +
+ ${w2utils.lang(msg)} +
` + } + menu_html += '
' + return menu_html + } + openSubMenu(event) { + const anchor = query(event.originalEvent.target).get(0) + const { overlay } = event + const { items } = overlay.options + // build sub-items list + const mitem = items[event.index] + let _items = [] + if (typeof mitem.items === 'function') { + _items = mitem.items(mitem) + } else if (Array.isArray(mitem.items)) { + _items = mitem.items + } + const prev = w2menu.get(overlay.name + '-submenu') + if (prev) { + prev.hide() + } + query(event.target).addClass('expanded') + w2menu.show({ + name: overlay.name + '-submenu', + anchor: anchor, + items: _items, + class: overlay.options.class + ' ' + mitem.overlay?.class, + offsetX: -7, + arrowSize: 0, + parentOverlay: overlay, + parents: [...event.parents, event.index], + position: 'right|left', + hideOn: ['doc-click', 'select'] + }) + .hide(evt => { + query(event.target).removeClass('expanded') + }) + // indicates if user cursor is over sub menu + setTimeout(() => { + query('#w2overlay-' + overlay.name + '-submenu') + .on('mouseenter', event => { event.target._keepSubOpen = true }) + .on('mouseleave', event => { event.target._keepSubOpen = false }) + }, 10) + } + closeSubMenu(event) { + const { overlay } = event + if (event.target._keepSubOpen !== true) { + const prev = w2menu.get(overlay.name + '-submenu') + if (prev) { + prev.hide() + } + } + } + // Refreshed only selected item highligh, used in keyboard navigation + refreshIndex(name, instant) { + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + if (!overlay) return + if (!overlay.displayed) { + this.show(overlay.name) + } + const view = query(overlay.box).find('.w2ui-overlay-body').get(0) + const search = query(overlay.box).find('.w2ui-menu-search, .w2ui-menu-top').get(0) + query(overlay.box).find('.w2ui-menu-item.w2ui-selected') + .removeClass('w2ui-selected') + const el = query(overlay.box).find(`.w2ui-menu-item[index="${overlay.selected}"]`) + .addClass('w2ui-selected') + .get(0) + if (el) { + if (el.offsetTop + el.clientHeight > view.clientHeight + view.scrollTop) { + el.scrollIntoView({ + behavior: instant ? 'instant' : 'smooth', + block: instant ? 'center' : 'start', + inline: instant ? 'center' : 'start' + }) + } + if (el.offsetTop < view.scrollTop + (search ? search.clientHeight : 0)) { + el.scrollIntoView({ + behavior: instant ? 'instant' : 'smooth', + block: instant ? 'center' : 'end', + inline: instant ? 'center' : 'end' + }) + } + } + w2tooltip.hide(overlay.name + '-tooltip') + } + showTooltip(name, options) { + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + if (!overlay || !overlay.displayed) return + const anchor = options?.anchor ?? query(overlay.box).find(`.w2ui-menu-item[index="${overlay.selected}"]`).get(0) + const tooltip = options?.tooltip ?? overlay.options.items[overlay.selected]?.tooltip + if (tooltip) { + const html = tooltip.html ?? tooltip + w2tooltip.show(Object.assign({ + name: overlay.name + '-tooltip', + anchor, + html, + position: 'right|left', + hideOn: ['doc-click'], + onShow(event) { + overlay.self.trigger('tooltip', { overlay, action: 'show', originalEvent: event }) + }, + onHide(event) { + overlay.self.trigger('tooltip', { overlay, action: 'hide', originalEvent: event }) + } + }, typeof tooltip === 'object' && tooltip != null ? tooltip : {})) + } + } + // show/hide searched items + refreshSearch(name) { + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + if (!overlay) return + if (!overlay.displayed) { + this.show(overlay.name) + } + w2tooltip.hide(overlay.name + '-tooltip') + query(overlay.box).find('.w2ui-no-items').hide() + query(overlay.box).find('.w2ui-menu-item, .w2ui-menu-divider').each(el => { + const cur = this.getCurrent(name, el.getAttribute('index')) + if (cur.item?.hidden) { + query(el).hide() + } else { + const search = overlay.tmp?.search + if (overlay.options.markSearch) { + w2utils.marker(el, search, { onlyFirst: overlay.options.match == 'begins' }) + } + query(el).show() + } + }) + // hide empty menus + query(overlay.box).find('.w2ui-sub-menu').each(sub => { + const hasItems = query(sub).find('.w2ui-menu-item').get().some(el => { + return el.style.display != 'none' + }) + const parent = this.getCurrent(name, sub.dataset.parent) + // only if parent is expaneded + if (parent.item.expanded) { + if (!hasItems) { + query(sub).parent().hide() + } else { + query(sub).parent().show() + } + } + }) + // show empty message + if (overlay.tmp.searchCount == 0 || overlay.options?.items?.length == 0) { + if (query(overlay.box).find('.w2ui-no-items').length == 0) { + query(overlay.box).find('.w2ui-menu:not(.w2ui-sub-menu)').append(` +
+ ${w2utils.lang(overlay.options.msgNoItems)} +
`) + } + query(overlay.box).find('.w2ui-no-items').show() + } + } + /** + * Loops through the items and markes item.hidden = true for those that need to be hidden, and item.hidden = false + * for those that are visible. Return a promise (since items can be on the server) with the number of visible items. + */ + applyFilter(name, items, search, debounce) { + let count = 0 + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + const options = overlay.options + let resolve, reject + const prom = new Promise((res, rej) => { + resolve = res + reject = rej + }) + if (overlay.tmp._skip_filter === true) { + return prom + } + if (search == null) { + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + search = overlay.anchor.value + } else { + search = '' + } + } + /** + * If it is input control that has a menu, and if filter is true, then show all the items unfiltered, unless user + * starts filtering again. + */ + if (overlay.tmp._new_search === false) { + search = '' + } + let selectedIds = [] + if (options.selected) { + if (Array.isArray(options.selected)) { + selectedIds = options.selected.map(item => { + return item?.id ?? item + }) + } else if (options.selected?.id) { + selectedIds = [options.selected.id] + } + } + overlay.tmp.activeChain = null + // if url is defined, get items from it + const remote = overlay.tmp.remote ?? { hasMore: true, emptySet: false, search: null, cached: -1 } + if (remote.hasMore == false) { + const len = remote.hasMore_search.length + if (search.substr(0, len) != remote.hasMore_search) { + remote.hasMore = true + } + } + if (items == null && options.url && remote.hasMore && remote.search !== search) { + let proceed = true + // only when items == null because it is case of nested items + let msg = w2utils.lang('Loading...') + if (search.length < options.minLength && remote.emptySet !== true) { + msg = w2utils.lang('${count} letters or more...', { count: options.minLength }) + proceed = false + if (search === '') { + msg = w2utils.lang(options.msgSearch) + } + // if there are items - then clear them + if (options.items?.length > 0) { + this.update(name, []) + this.applyFilter(name, null, search) + } + } + query(overlay.box).find('.w2ui-no-items').html(msg) + remote.search = search + options.items = [] + overlay.tmp.remote = remote + if (proceed) { + this.request(overlay, search, debounce) + .then(remoteItems => { + overlay.tmp._skip_filter = true + this.update(name, remoteItems) + delete overlay.tmp._skip_filter + overlay.tmp._new_search = true + this.applyFilter(name, remoteItems, search).then(data => { + this.getActiveChain(overlay.name, options.items) // need this to update chain for up/down key navigation + overlay.tmp.searchCount = data.count + overlay.tmp.search = data.search + if (options.prefilter || search !== '') { + // if selected is not in searched items + if (data.count === 0 || !this.getActiveChain(overlay.name, options.items).includes(overlay.selected)) { + overlay.selected = null + } + this.refreshSearch(overlay.name) + } + this.initControls(overlay) + this.refreshIndex(overlay.name, true) + resolve(data) + }) + }) + .catch(error => { + console.log('Server Request error', error) + }) + } + return prom + } + let edata + // only trigger search event when data is present and for the top level + if (items == null) { + edata = this.trigger('search', { search, overlay, prom, resolve, reject }) + if (edata.isCancelled === true) { + return prom + } + } + if (items == null) { + items = overlay.options.items + } + if (options.filter === false) { + resolve({ count: -1, search }) + return prom + } + items.forEach(item => { + let prefix = '' + let suffix = '' + if (['is', 'begins', 'begins with'].indexOf(options.match) !== -1) prefix = '^' + if (['is', 'ends', 'ends with'].indexOf(options.match) !== -1) suffix = '$' + try { + const re = new RegExp(prefix + search + suffix, 'i') + if (re.test(item.text) || item.text === '...') { + item.hidden = false + } else { + item.hidden = true + } + } catch (e) {} + // do not show selected items + if (options.hideSelected && selectedIds.includes(item.id)) { + item.hidden = true + } + // search nested items + if (Array.isArray(item.items) && item.items.length > 0) { + delete item._noSearchInside + this.applyFilter(name, item.items, search).then(data => { + const subCount = data.count + if (subCount > 0) { + count += subCount + if (item.hidden) item._noSearchInside = true + // only expand items if search is not empty + if (search) item.expanded = true + item.hidden = false + } + }) + } + if (item.hidden !== true) count++ + }) + resolve({ count, search }) + edata?.finish() + return prom + } + request(overlay, search, debounce) { + const options = overlay.options + const remote = overlay.tmp.remote + let resolve, reject // promise functions + if ((options.items.length === 0 && remote.cached !== 0) || + (remote.cached == options.cacheMax && search.length > remote.search.length) || + (search.length >= remote.search.length && search.substr(0, remote.search.length) !== remote.search) || + (search.length < remote.search.length)) { + // Aabort previous request if any + if (remote.controller) { + remote.controller.abort() + } + remote.loading = true + clearTimeout(remote.timeout) + remote.timeout = setTimeout(() => { + let url = options.url + const postData = { search, max: options.cacheMax } + Object.assign(postData, options.postData) + // trigger event + const edata = this.trigger('request', { + search, overlay, url, postData, + httpMethod: options.method ?? 'GET', + httpHeaders: {} + }) + if (edata.isCancelled === true) return + // if event updated url and postData, use it + url = new URL(edata.detail.url, location) + const fetchOptions = w2utils.prepareParams(url, { + method: edata.detail.httpMethod, + headers: edata.detail.httpHeaders, + body: edata.detail.postData + }, { caller: this, overlay, search }) + // Create new abort controller + remote.controller = new AbortController() + fetchOptions.signal = remote.controller.signal + // send request + fetch(url, fetchOptions) + .then(resp => resp.json()) + .then(data => { + remote.controller = null + // trigger event + const edata = overlay.trigger('load', { search: postData.search, overlay, data }) + if (edata.isCancelled === true) return + // default behavior + data = edata.detail.data + if (typeof data === 'string') data = JSON.parse(data) + // if server just returns array + if (Array.isArray(data)) { + data = { records: data } + } + // needed for backward compatibility + if (data.records == null && data.items != null) { + data.records = data.items + delete data.items + } + // handles Golang marshal of empty arrays to null + if (!data.error && data.records == null) { + data.records = [] + } + if (!Array.isArray(data.records)) { + console.error('ERROR: server did not return proper JSON data structure', '\n', + ' - it should return', { records: [{ id: 1, text: 'item' }] }, '\n', + ' - or just an array ', [{ id: 1, text: 'item' }], '\n', + ' - or if errorr ', { error: true, message: 'error message' }) + return + } + // remove all extra items if more then needed for cache + if (data.records.length >= options.cacheMax) { + data.records.splice(options.cacheMax, data.records.length) + remote.hasMore = true + } else { + remote.hasMore = false + remote.hasMore_search = search + } + // map id and text + if (options.recId == null && options.recid != null) options.recId = options.recid // since lower-case recid is used in grid + if (options.recId || options.recText) { + data.records.forEach((item) => { + if (typeof options.recId === 'string') item.id = item[options.recId] + if (typeof options.recId === 'function') item.id = options.recId(item) + if (typeof options.recText === 'string') item.text = item[options.recText] + if (typeof options.recText === 'function') item.text = options.recText(item) + }) + } + // remember stats + remote.loading = false + remote.search = search + remote.cached = data.records.length == 0 ? -1 : data.records.length + remote.lastError = '' + remote.emptySet = (!!(search === '' && data.records.length === 0)) + // event after + edata.finish() + resolve(w2utils.normMenu(data.records, data)) + }) + .catch(error => { + const edata = this.trigger('error', { overlay, search, error }) + if (edata.isCancelled === true) return + // default behavior + if (error?.name !== 'AbortError') { + console.error('ERROR: Server communication failed.', '\n', + ' - it should return', { records: [{ id: 1, text: 'item' }] }, '\n', + ' - or just an array ', [{ id: 1, text: 'item' }], '\n', + ' - or if errorr ', { error: true, message: 'error message' }) + } + // reset stats + remote.loading = false + remote.search = '' + remote.cached = -1 + remote.emptySet = true + remote.lastError = (edata.detail.error || 'Server communication failed') + options.items = [] + // event after + edata.finish() + reject() + }) + // event after + edata.finish() + }, debounce ? (options.debounce ?? 350) : 0) + } + return new Promise((res, rej) => { + resolve = res + reject = rej + }) + } + /** + * Builds an array of item ids that sequencial order for navigation with up/down keys. Skips hidden and disabled items + * and goes into nested structures. It will remember last active chain in 'overlay.tmp.activeChain' + */ + getActiveChain(name, items, parents = [], res = [], noSave) { + const overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')] + if (overlay.tmp.activeChain != null) { + return overlay.tmp.activeChain + } + if (items == null) items = overlay.options.items + items.forEach((item, ind) => { + if (!item.hidden && !item.disabled && !item?.text?.startsWith?.('--')) { + res.push(parents.concat([ind]).join('-')) + if (Array.isArray(item.items) && item.items.length > 0 && item.expanded) { + parents.push(ind) + this.getActiveChain(name, item.items, parents, res, true) + parents.pop() + } + } + }) + if (noSave == null) { + overlay.tmp.activeChain = res + } + return res + } + menuDown(overlay, event, index, parents) { + const options = overlay.options + let items = options.items + const icon = query(event.delegate).find('.w2ui-icon') + const menu = query(event.target).closest('.w2ui-menu:not(.w2ui-sub-menu)') + if (typeof parents === 'string' && parents !== '') { + const ids = parents.split('-') + ids.forEach(id => { + items = items[id].items + }) + } + if (typeof items === 'function') { + items = items({ overlay, index, parents, event }) + } + const item = items[index] + if (item == null || item.disabled) { + return + } + const uncheck = (items, parent) => { + items.forEach((other, ind) => { + if (other.id == item.id) return + if (other.group === item.group && other.checked) { + menu + .find(`.w2ui-menu-item[index="${(parent ? parent + '-' : '') + ind}"] .w2ui-icon`) + .removeClass('w2ui-icon-check') + .addClass('w2ui-icon-empty') + items[ind].checked = false + } + if (Array.isArray(other.items)) { + uncheck(other.items, ind) + } + }) + } + if ((options.type === 'check' || options.type === 'radio') && item.group !== false && + !query(event.target).hasClass('menu-remove') && + !query(event.target).hasClass('menu-help') && + !query(event.target).closest('.w2ui-menu-item').hasClass('has-sub-menu')) { + item.checked = options.type == 'radio' ? true : !item.checked + if (item.checked) { + if (options.type === 'radio') { + query(event.target).closest('.w2ui-menu').find('.w2ui-icon') + .removeClass('w2ui-icon-check') + .addClass('w2ui-icon-empty') + } + if (options.type === 'check' && item.group != null) { + uncheck(options.items) + } + icon.removeClass('w2ui-icon-empty').addClass('w2ui-icon-check') + } else if (options.type === 'check') { + icon.removeClass('w2ui-icon-check').addClass('w2ui-icon-empty') + } + } + // highlight record + if (!query(event.target).hasClass('menu-remove') && !query(event.target).hasClass('menu-help')) { + menu.find('.w2ui-menu-item').removeClass('w2ui-selected') + // click on the item that has submenu will not select the item + if (!query(event.delegate).hasClass('has-sub-menu')) { + query(event.delegate).addClass('w2ui-selected') + } + } + } + menuClick(overlay, event, index, parents) { + const options = overlay.options + let items = options.items + const $item = query(event.delegate).closest('.w2ui-menu-item') + let keepOpen = !options.hideOn.includes('select') + if (event.shiftKey || event.metaKey || event.ctrlKey) { + keepOpen = true + } + if (typeof parents === 'string' && parents !== '') { + parents = parents.split('-') + } else if (!Array.isArray(parents)) { + parents = null + } + // parse through parents to get to right items + if (parents) { + parents.forEach(id => { + items = items[id].items + }) + } + if (typeof items === 'function') { + items = items({ overlay, index, parents, event }) + } + const item = items[index] + if (!item || (item.disabled && !query(event.target).hasClass('menu-remove'))) { + return + } + let edata + const overlays = [overlay] + let topOverlay = overlay + let parentOverlay + while (topOverlay.options.parentOverlay) { + topOverlay = topOverlay.options.parentOverlay + parentOverlay ??= topOverlay // not top overlay, but parent overlay items + overlays.push(topOverlay) + } + if (query(event.target).hasClass('menu-remove')) { + edata = topOverlay.trigger('remove', { + originalEvent: event, target: overlay.name, overlay, topOverlay, parentOverlay, + item, index, el: $item[0] + }) + if (edata.isCancelled === true) { + return + } + // delete from items + const items = options.items + const ind = items.findIndex(it => it.id == item.id) + if (ind != -1) { + const tmp = items.splice(ind, 1) + // delete from the parent too + if (overlay.options.parents) { + const pind = overlay.options.parents[overlay.options.parents.length - 1] + const pitems = parentOverlay.options.items[pind].items + if (pitems[ind].id == tmp[0].id) { + pitems.splice(ind, 1) + } + } + } + keepOpen = !options.hideOn.includes('item-remove') + const name = $item.closest('.w2ui-overlay').attr('name') + overlay.self.update(name, items) + } else if ($item.hasClass('has-sub-menu')) { + edata = topOverlay.trigger('subMenu', { + originalEvent: event, target: overlay.name, overlay, topOverlay, parentOverlay, + item, index, el: $item[0] + }) + if (edata.isCancelled === true) { + return + } + keepOpen = true + } else { + // find items that are selected + const selected = this.findChecked(options.items) + const a_index = $item.attr('index') + overlay.selected = isNaN(a_index) ? a_index : parseInt(a_index) + edata = topOverlay.trigger('select', { + originalEvent: event, target: overlay.name, overlay, topOverlay, parentOverlay, + item, index, selected, keepOpen, el: $item[0] + }) + if (edata.isCancelled === true) { + return + } + if (item.keepOpen != null) { + keepOpen = item.keepOpen + } + if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + overlay.anchor.dataset.selected = item.id + overlay.anchor.dataset.selectedIndex = overlay.selected + } + } + if (!keepOpen) { + overlays.forEach(overlay => this.hide(overlay.name)) + } + // if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) { + // overlay.anchor.focus() + // } + // event after + edata.finish() + } + findChecked(items) { + let found = [] + items.forEach(item => { + if (item.checked) found.push(item) + if (Array.isArray(item.items)) { + found = found.concat(this.findChecked(item.items)) + } + }) + return found + } + keyUp(overlay, event) { + const options = overlay.options + const search = event.target.value + let filter = true + let refreshIndex = false + switch (event.keyCode) { + case 46: // delete + case 8: { // backspace + // if search empty and delete is clicked, do not filter nor show overlay + if (search === '' && !overlay.displayed) filter = false + break + } + case 13: { // enter + if (!overlay.displayed || !overlay.selected) return + const { index, parents } = this.getCurrent(overlay.name) + event.delegate = query(overlay.box).find('.w2ui-selected').get(0) + // reset active chain for folders + this.menuClick(overlay, event, parseInt(index), parents) + filter = false + break + } + case 27: { // escape + filter = false + if (overlay.displayed) { + this.hide(overlay.name) + } else { + // clear selected + const el = overlay.anchor + if (['INPUT', 'TEXTAREA'].includes(el.tagName)) { + el.value = '' + delete el.dataset.selected + delete el.dataset.selectedIndex + } + } + break + } + case 37: { // left + if (!overlay.displayed) return + let { item, index, parents } = this.getCurrent(overlay.name) + // collapse parent if any + if (parents) { + item = options.items[parents] + index = parseInt(parents) + parents = '' + refreshIndex = true + } + if (Array.isArray(item?.items) && item.items.length > 0 && item.expanded) { + event.delegate = query(overlay.box).find(`.w2ui-menu-item[index="${index}"]`).get(0) + overlay.selected = index + this.menuClick(overlay, event, parseInt(index), parents) + } + filter = false + break + } + case 39: { // right + if (!overlay.displayed) return + const { item, index, parents } = this.getCurrent(overlay.name) + if (Array.isArray(item?.items) && item.items.length > 0 && !item.expanded) { + event.delegate = query(overlay.box).find('.w2ui-selected').get(0) + this.menuClick(overlay, event, parseInt(index), parents) + } + filter = false + break + } + case 38: { // up + if (!overlay.displayed) { + break + } + overlay.prev() + filter = false + event.preventDefault() + break + } + case 40: { // down + if (!overlay.displayed) { + break + } + overlay.next() + filter = false + event.preventDefault() + break + } + } + // filter + if (filter && overlay.displayed && + ((options.filter && event._searchType == 'filter') || (options.search && event._searchType == 'search'))) { + this.applyFilter(overlay.name, null, search, true) + .then(data => { + overlay.tmp.searchCount = data.count + overlay.tmp.search = data.search + // if selected is not in searched items + if (data.count === 0 || !this.getActiveChain(overlay.name).includes(overlay.selected)) { + overlay.selected = null + } + this.refreshSearch(overlay.name) + }) + } + if (refreshIndex) { + this.refreshIndex(overlay.name) + } + } +} +class DateTooltip extends Tooltip { + constructor() { + super() + const td = new Date() + this.daysCount = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + this.today = td.getFullYear() + '/' + (Number(td.getMonth()) + 1) + '/' + td.getDate() + this.defaults = w2utils.extend({}, this.defaults, { + position: 'top|bottom', + class: 'w2ui-calendar', + type: 'date', // can be date/time/datetime + value: '', // initial date (in w2utils.settings format) + format: '', + start: null, + end: null, + btnNow: false, + blockDates: [], // array of blocked dates + blockWeekdays: [], // blocked weekdays 0 - sunday, 1 - monday, etc + colored: {}, // ex: { '3/13/2022': 'bg-color|text-color' } + arrowSize: 12, + autoResize: false, + anchorClass: 'w2ui-focus', + autoShowOn: 'focus', + hideOn: ['doc-click', 'focus-change'], + onSelect: null + }) + } + attach(anchor, text) { + let options + if (arguments.length == 1 && anchor instanceof Object) { + options = anchor + anchor = options.anchor + } else if (arguments.length === 2 && text != null && typeof text === 'object') { + options = text + options.anchor = anchor + } + const prevHideOn = options.hideOn + options = w2utils.extend({}, this.defaults, options || {}) + if (prevHideOn) { + options.hideOn = prevHideOn + } + if (!options.format) { + const df = w2utils.settings.dateFormat + const tf = w2utils.settings.timeFormat + if (options.type == 'date') { + options.format = df + } else if (options.type == 'time') { + options.format = tf + } else { + options.format = df + '|' + tf + } + } + const cal = options.type == 'time' ? this.getHourHTML(options) : this.getMonthHTML(options) + options.style += '; padding: 0;' + options.html = cal.html + const ret = super.attach(options) + const overlay = ret.overlay + Object.assign(overlay.tmp, cal) + overlay.on('show.attach', event => { + const overlay = event.detail.overlay + const anchor = overlay.anchor + const options = overlay.options + if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && !options.value && anchor.value) { + overlay.tmp.initValue = anchor.value + } + delete overlay.newValue + delete overlay.newDate + }) + overlay.on('show:after.attach', event => { + if (ret.overlay?.box) { + this.initControls(ret.overlay) + } + }) + overlay.on('update:after.attach', event => { + if (ret.overlay?.box) { + this.initControls(ret.overlay) + } + }) + overlay.on('hide.attach', event => { + const overlay = event.detail.overlay + const anchor = overlay.anchor + if (overlay.newValue != null) { + if (overlay.newDate) { + overlay.newValue = overlay.newDate + ' ' + overlay.newValue + } + if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && anchor.value != overlay.newValue) { + anchor.value = overlay.newValue + } + const edata = this.trigger('select', { date: overlay.newValue, target: overlay.name, overlay }) + if (edata.isCancelled === true) return + // event after + edata.finish() + } + }) + ret.select = (callback) => { + overlay.on('select.attach', (event) => { callback(event) }) + return ret + } + return ret + } + initControls(overlay) { + const options = overlay.options + const moveMonth = (inc) => { + let { month, year } = overlay.tmp + month += inc + if (month > 12) { + month = 1 + year++ + } + if (month < 1) { + month = 12 + year-- + } + const cal = this.getMonthHTML(options, month, year) + Object.assign(overlay.tmp, cal) + query(overlay.box).find('.w2ui-overlay-body').html(cal.html) + this.initControls(overlay) + } + const checkJump = (event, dblclick) => { + query(event.target).parent().find('.w2ui-jump-month, .w2ui-jump-year') + .removeClass('w2ui-selected') + query(event.target).addClass('w2ui-selected') + const dt = new Date() + let { jumpMonth, jumpYear } = overlay.tmp + if (dblclick) { + if (jumpYear == null) jumpYear = dt.getFullYear() + if (jumpMonth == null) jumpMonth = dt.getMonth() + 1 + } + if (jumpMonth && jumpYear) { + const cal = this.getMonthHTML(options, jumpMonth, jumpYear) + Object.assign(overlay.tmp, cal) + query(overlay.box).find('.w2ui-overlay-body').html(cal.html) + overlay.tmp.jump = false + this.initControls(overlay) + } + } + // events for next/prev buttons and title + query(overlay.box) + .find('.w2ui-cal-title') + .off('.calendar') + // click on title + .on('click.calendar', event => { + Object.assign(overlay.tmp, { jumpYear: null, jumpMonth: null }) + if (overlay.tmp.jump) { + const { month, year } = overlay.tmp + const cal = this.getMonthHTML(options, month, year) + query(overlay.box).find('.w2ui-overlay-body').html(cal.html) + overlay.tmp.jump = false + } else { + query(overlay.box).find('.w2ui-overlay-body .w2ui-cal-days') + .replace(this.getYearHTML()) + const el = query(overlay.box).find(`[name="${overlay.tmp.year}"]`).get(0) + if (el) el.scrollIntoView(true) + overlay.tmp.jump = true + } + this.initControls(overlay) + event.stopPropagation() + }) + // prev button + .find('.w2ui-cal-previous') + .off('.calendar') + .on('click.calendar', event => { + moveMonth(-1) + event.stopPropagation() + }) + .parent() + // next button + .find('.w2ui-cal-next') + .off('.calendar') + .on('click.calendar', event => { + moveMonth(1) + event.stopPropagation() + }) + // now button + query(overlay.box).find('.w2ui-cal-now') + .off('.calendar') + .on('click.calendar', event => { + if (options.type == 'datetime') { + if (overlay.newDate) { + overlay.newValue = w2utils.formatTime(new Date(), options.format.split('|')[1]) + } else { + overlay.newValue = w2utils.formatDateTime(new Date(), options.format) + } + } else if (options.type == 'date') { + overlay.newValue = w2utils.formatDate(new Date(), options.format) + } else if (options.type == 'time') { + overlay.newValue = w2utils.formatTime(new Date(), options.format) + } + this.hide(overlay.name) + }) + // events for dates + query(overlay.box) + .off('.calendar') + .on('contextmenu.calendar', event => { + event.preventDefault() // prevent browser context menu + }) + .on('click.calendar', { delegate: '.w2ui-day.w2ui-date' }, event => { + if (options.type == 'datetime') { + overlay.newDate = query(event.target).attr('date') + query(overlay.box).find('.w2ui-overlay-body').html(this.getHourHTML(overlay.options).html) + this.initControls(overlay) + } else { + overlay.newValue = query(event.target).attr('date') + this.hide(overlay.name) + } + }) + // click on month + .on('click.calendar', { delegate: '.w2ui-jump-month' }, event => { + overlay.tmp.jumpMonth = parseInt(query(event.target).attr('name')) + checkJump(event) + }) + // double click on month + .on('dblclick.calendar', { delegate: '.w2ui-jump-month' }, event => { + overlay.tmp.jumpMonth = parseInt(query(event.target).attr('name')) + checkJump(event, true) + }) + // click on year + .on('click.calendar', { delegate: '.w2ui-jump-year' }, event => { + overlay.tmp.jumpYear = parseInt(query(event.target).attr('name')) + checkJump(event) + }) + // dbl click on year + .on('dblclick.calendar', { delegate: '.w2ui-jump-year' }, event => { + overlay.tmp.jumpYear = parseInt(query(event.target).attr('name')) + checkJump(event, true) + }) + // click on hour + .on('click.calendar', { delegate: '.w2ui-time.hour' }, event => { + const hour = query(event.target).attr('hour') + let min = this.str2min(options.value) % 60 + if (overlay.tmp.initValue && !options.value) { + min = this.str2min(overlay.tmp.initValue) % 60 + } + if (options.noMinutes) { + overlay.newValue = this.min2str(hour * 60, options.format) + this.hide(overlay.name) + } else { + overlay.newValue = hour + ':' + min + const html = this.getMinHTML(hour, options).html + query(overlay.box).find('.w2ui-overlay-body').html(html) + this.initControls(overlay) + } + }) + // click on minute + .on('click.calendar', { delegate: '.w2ui-time.min' }, event => { + const hour = Math.floor(this.str2min(overlay.newValue) / 60) + const time = (hour * 60) + parseInt(query(event.target).attr('min')) + overlay.newValue = this.min2str(time, options.format) + this.hide(overlay.name) + }) + } + getMonthHTML(options, month, year) { + const days = w2utils.settings.fulldays.slice() // creates copy of the array + const sdays = w2utils.settings.shortdays.slice() // creates copy of the array + if (w2utils.settings.weekStarts !== 'M') { + days.unshift(days.pop()) + sdays.unshift(sdays.pop()) + } + let DT = new Date() + const dayLengthMil = 1000 * 60 * 60 * 24 + const selected = options.type === 'datetime' + ? w2utils.isDateTime(options.value, options.format, true) + : w2utils.isDate(options.value, options.format, true) + const selected_dsp = w2utils.formatDate(selected) + // normalize date + if (month == null || year == null) { + year = selected ? selected.getFullYear() : DT.getFullYear() + month = selected ? selected.getMonth() + 1 : DT.getMonth() + 1 + } + if (month > 12) { month -= 12; year++ } + if (month < 1 || month === 0) { month += 12; year-- } + if (year / 4 == Math.floor(year / 4)) { this.daysCount[1] = 29 } else { this.daysCount[1] = 28 } + options.current = month + '/' + year + let weekDaysHeaderHTML = '' + const st = w2utils.settings.weekStarts + for (let i = 0; i < sdays.length; i++) { + const isSat = !!((st == 'M' && i == 5) || (st != 'M' && i == 6)) + const isSun = !!((st == 'M' && i == 6) || (st != 'M' && i == 0)) + weekDaysHeaderHTML += `
${sdays[i]}
` + } + let html = ` +
+
+
+
+
+
+
+ ${w2utils.settings.fullmonths[month - 1]}, ${year} + +
+
+ ${weekDaysHeaderHTML} + ` + // start with the required date + DT = new Date(year, month - 1, 1) + /** + * Move to noon, instead of midnight. If not, then the date when time saving happens + * will be duplicated in the calendar + */ + DT = new Date(DT.getTime() + dayLengthMil * 0.5) + // calendar offset + let weekDayOffset = DT.getDay() + if (w2utils.settings.weekStarts == 'M') { + // offset should be 1 day more, but not negative (Sunday) + weekDayOffset = weekDayOffset > 0 ? weekDayOffset - 1 : 6 + } + // apply the offset for the first day in the calendar + DT = new Date(DT.getTime() - (weekDayOffset * dayLengthMil)) + const DaySat = 6; const DaySun = 0 + for (let ci = 0; ci < 42; ci++) { + const className = [] + const dt = `${DT.getFullYear()}/${DT.getMonth() + 1}/${DT.getDate()}` + if (DT.getDay() === DaySat) className.push('w2ui-saturday') + if (DT.getDay() === DaySun) className.push('w2ui-sunday') + if (DT.getMonth() + 1 !== month) className.push('outside') + if (dt == this.today) className.push('w2ui-today') + const dspDay = DT.getDate() + let col = '' + let bgcol = '' + let tmp_dt, tmp_dt_fmt + if (options.type === 'datetime') { + tmp_dt = w2utils.formatDateTime(dt, options.format) + tmp_dt_fmt = w2utils.formatDate(dt, w2utils.settings.dateFormat) + } else { + tmp_dt = w2utils.formatDate(dt, options.format) + tmp_dt_fmt = tmp_dt + } + if (options.colored && options.colored[tmp_dt_fmt] !== undefined) { // if there is predefined colors for dates + const tmp = options.colored[tmp_dt_fmt].split('|') + bgcol = 'background-color: ' + tmp[0] + ';' + col = 'color: ' + tmp[1] + ';' + } + html += `
+ ${dspDay} +
` + DT = new Date(DT.getTime() + dayLengthMil) + } + html += '
' + if (options.btnNow) { + const label = w2utils.lang('Today' + (options.type == 'datetime' ? ' & Now' : '')) + html += `
${label}
` + } + return { html, month, year } + } + getYearHTML() { + let mhtml = '' + let yhtml = '' + for (let m = 0; m < w2utils.settings.fullmonths.length; m++) { + mhtml += `
${w2utils.settings.shortmonths[m]}
` + } + for (let y = w2utils.settings.dateStartYear; y <= w2utils.settings.dateEndYear; y++) { + yhtml += `
${y}
` + } + return `
+
${mhtml}
+
${yhtml}
+
` + } + getHourHTML(options) { + options = options ?? {} + if (!options.format) options.format = w2utils.settings.timeFormat + const h24 = (options.format.indexOf('h24') > -1) + const value = options.value ? options.value : (options.anchor ? options.anchor.value : '') + const tmp = [] + for (let a = 0; a < 24; a++) { + let time = (a >= 12 && !h24 ? a - 12 : a) + ':00' + (!h24 ? (a < 12 ? ' am' : ' pm') : '') + if (a == 12 && !h24) time = '12:00 pm' + if (!tmp[Math.floor(a / 8)]) tmp[Math.floor(a / 8)] = '' + let tm1 = this.min2str(this.str2min(time)) + let tm2 = this.min2str(this.str2min(time) + 59) + if (options.type === 'datetime') { + const dt = w2utils.isDateTime(value, options.format, true) + const fm = options.format.split('|')[0].trim() + tm1 = w2utils.formatDate(dt, fm) + ' ' + tm1 + tm2 = w2utils.formatDate(dt, fm) + ' ' + tm2 + } + const valid = this.inRange(tm1, options) || this.inRange(tm2, options) + tmp[Math.floor(a / 8)] += `${time}` + } + const html = `
+
${w2utils.lang('Select Hour')}
+
+
${tmp[0]}
+
${tmp[1]}
+
${tmp[2]}
+
+ ${options.btnNow ? `
${w2utils.lang('Now')}
` : ''} +
` + return { html } + } + getMinHTML(hour, options) { + if (hour == null) hour = 0 + options = options ?? {} + if (!options.format) options.format = w2utils.settings.timeFormat + const h24 = (options.format.indexOf('h24') > -1) + const value = options.value ? options.value : (options.anchor ? options.anchor.value : '') + const tmp = [] + for (let a = 0; a < 60; a += 5) { + const time = (hour > 12 && !h24 ? hour - 12 : hour) + ':' + (a < 10 ? 0 : '') + a + ' ' + (!h24 ? (hour < 12 ? 'am' : 'pm') : '') + let tm = time + const ind = a < 20 ? 0 : (a < 40 ? 1 : 2) + if (!tmp[ind]) tmp[ind] = '' + if (options.type === 'datetime') { + const dt = w2utils.isDateTime(value, options.format, true) + const fm = options.format.split('|')[0].trim() + tm = w2utils.formatDate(dt, fm) + ' ' + tm + } + tmp[ind] += `${time}` + } + const html = `
+
${w2utils.lang('Select Minute')}
+
+
${tmp[0]}
+
${tmp[1]}
+
${tmp[2]}
+
+ ${options.btnNow ? `
${w2utils.lang('Now')}
` : ''} +
` + return { html } + } + // checks if date is in range (loost at start, end, blockDates, blockWeekdays) + inRange(str, options, dateOnly) { + let inRange = false + if (options.type === 'date') { + const dt = w2utils.isDate(str, options.format, true) + if (dt) { + // enable range + if (options.start || options.end) { + const st = (typeof options.start === 'string' ? options.start : query(options.start).val()) + const en = (typeof options.end === 'string' ? options.end : query(options.end).val()) + let start = w2utils.isDate(st, options.format, true) + let end = w2utils.isDate(en, options.format, true) + const current = new Date(dt) + if (!start) start = current + if (!end) end = current + if (current >= start && current <= end) inRange = true + } else { + inRange = true + } + // block predefined dates + if (Array.isArray(options.blockDates) && options.blockDates.includes(str)) inRange = false + // block weekdays + if (Array.isArray(options.blockWeekdays) && options.blockWeekdays.includes(dt.getDay())) inRange = false + } + } else if (options.type === 'time') { + if (options.start || options.end) { + const tm = this.str2min(str) + let tm1 = this.str2min(options.start) + let tm2 = this.str2min(options.end) + if (!tm1) tm1 = tm + if (!tm2) tm2 = tm + if (tm >= tm1 && tm <= tm2) inRange = true + } else { + inRange = true + } + } else if (options.type === 'datetime') { + const dt = w2utils.isDateTime(str, options.format, true) + if (dt) { + const format = options.format.split('|').map(format => format.trim()) + if (dateOnly) { + const date = w2utils.formatDate(dt, format[0]) + const opts = w2utils.extend({}, options, { type: 'date', format: format[0] }) + if (this.inRange(date, opts)) inRange = true + } else { + const time = w2utils.formatTime(dt, format[1]) + const opts = { type: 'time', format: format[1], start: options.startTime, end: options.endTime } + if (this.inRange(time, opts)) inRange = true + } + } + } + return inRange + } + // converts time into number of minutes since midnight -- '11:50am' => 710 + str2min(str) { + if (typeof str !== 'string') return null + const tmp = str.split(':') + if (tmp.length === 2) { + tmp[0] = parseInt(tmp[0]) + tmp[1] = parseInt(tmp[1]) + if (str.indexOf('pm') !== -1 && tmp[0] !== 12) tmp[0] += 12 + if (str.includes('am') && tmp[0] == 12) tmp[0] = 0 // 12:00am - is midnight + } else { + return null + } + return tmp[0] * 60 + tmp[1] + } + // converts minutes since midnight into time str -- 710 => '11:50am' + min2str(time, format) { + let ret = '' + if (time >= 24 * 60) time = time % (24 * 60) + if (time < 0) time = 24 * 60 + time + const hour = Math.floor(time / 60) + const min = ((time % 60) < 10 ? '0' : '') + (time % 60) + if (!format) { format = w2utils.settings.timeFormat } + if (format.indexOf('h24') !== -1) { + ret = hour + ':' + min + } else { + ret = (hour <= 12 ? hour : hour - 12) + ':' + min + ' ' + (hour >= 12 ? 'pm' : 'am') + } + return ret + } +} +const w2tooltip = new Tooltip() +const w2menu = new MenuTooltip() +const w2color = new ColorTooltip() +const w2date = new DateTooltip() + +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base, w2tooltip, w2color, w2menu + * + * == TODO == + * - tab navigation (index state) + * - vertical toolbar + * - w2menu on second click of tb button should hide + * - button display groups for each show/hide, possibly add state: { single: t/f, multiple: t/f, type: 'font' } + * - item.count - should just support html, so a custom block can be created, such as a colored line + * + * == 2.0 changes + * - CSP - fixed inline events + * - removed jQuery dependency + * - item.icon - can be class or or + * - new w2tooltips and w2menu + * - scroll returns promise + * - added onMouseEntter, onMouseLeave, onMouseDown, onMouseUp events + * - add(..., skipRefresh), insert(..., skipRefresh) + * - item.items can be a function + * - item.icon_style - style for the icon + * - item.icon - can be a function + * - item.type = 'label', item.type = 'input' + * - item.placeholder + * - item.input: { spinner, style, min, max, step, precision, suffix } + * - item.backColor + * - onLiveUpdate - for colors + */ + +class w2toolbar extends w2base { + constructor(options) { + super(options.name) + this.box = null // DOM Element that holds the element + this.name = null // unique name for w2ui + this.routeData = {} // data for dynamic routes + this.items = [] + this.right = '' // HTML text on the right of toolbar + this.tooltip = 'top|left'// can be top, bottom, left, right + this.onClick = null + this.onChange = null + this.onMouseDown = null + this.onMouseUp = null + this.onMouseEnter = null // mouse enter the button event + this.onMouseLeave = null + this.onRender = null + this.onRefresh = null + this.onResize = null + this.onDestroy = null + this.onLiveUpdate = null + this.item_template = { + id: null, // command to be sent to all event handlers + type: 'button', // button, check, radio, drop, menu, menu-radio, menu-check, break, html, label, input spacer + text: null, + html: '', + tooltip: null, // w2toolbar.tooltip should be + count: null, + hidden: false, + disabled: false, + checked: false, // used for radio buttons + icon: null, + route: null, // if not null, it is route to go + arrow: null, // arrow down for drop/menu types + style: null, // extra css style for caption + group: null, // used for radio buttons + items: null, // for type menu* it is an array of items in the menu + selected: null, // used for menu-check, menu-radio + color: null, // color value - used in color pickers + backColor: null, // background color value for color pickter + overlay: { // additional options for overlay + anchorClass: '' + }, + onClick: null, + onRefresh: null + } + this.last = { + badge: {}, + pendingRefresh: {} // what should be refreshed with a debounce + } + /** + * This _refresh function is needed for speed. It will store what should be refreshed in this.last.refesh + * obect and then call _refreshDebounced(), which will do it withing 15 ms. However, if new items are added + * they will not cause multiple unnecessary refreshes + */ + this._refresh = ({ effected, resize, refreshTooltip }) => { + const options = this.last.pendingRefresh + options.ids ??= [] + options.ids.push(...effected) + Object.assign(options, { resize, refreshTooltip }) + this._refreshDebounced() + } + this._refreshDebounced = w2utils.debounce(() => { + const options = this.last.pendingRefresh + // new Set will make array unique + new Set(options.ids).forEach(id => { + this.refresh(id) + if (options.hideTooltip) this.tooltipHide(id) + }) + if (options.resize) this.resize() + // once refresh is complete, then clear refresh object + this.last.pendingRefresh = {} + }, 15) + // mix in options, w/o items + const items = options.items + delete options.items + Object.assign(this, options) + // add item via method to makes sure item_template is applied + if (Array.isArray(items)) this.add(items, true) + // need to reassign back to keep it in config + options.items = items + // render if box specified + if (typeof this.box === 'string') this.box = query(this.box).get(0) + if (this.box) this.render(this.box) + } + add(items, skipRefresh) { + this.insert(null, items, skipRefresh) + } + insert(id, items, skipRefresh) { + if (!Array.isArray(items)) items = [items] + items.forEach((item, idx, arr) => { + if (typeof item === 'string') { + item = arr[idx] = { id: item, text: item } + } + // checks + const valid = ['button', 'check', 'radio', 'drop', 'menu', 'menu-radio', 'menu-check', 'color', 'text-color', 'html', 'label', 'input', + 'group', 'break', 'spacer', 'new-line'] + if (!valid.includes(String(item.type))) { + console.log('ERROR: The parameter "type" should be one of the following:', valid, `, but ${item.type} is supplied.`, item) + return + } + if (item.id == null && !['break', 'spacer', 'new-line'].includes(item.type)) { + console.log('ERROR: The parameter "id" is required but not supplied.', item) + return + } + if (item.type == null) { + console.log('ERROR: The parameter "type" is required but not supplied.', item) + return + } + if (!w2utils.checkUniqueId(item.id, this.items, 'toolbar', this.name)) return + // add item + const newItem = w2utils.extend({}, this.item_template, item) + if (newItem.type === 'group' && Array.isArray(newItem.items)) { + newItem.items.forEach((it, ind) => { + newItem.items[ind] = w2utils.extend({}, this.item_template, newItem.items[ind]) + }) + } + if (newItem.type === 'menu-check') { + if (!Array.isArray(newItem.selected)) newItem.selected = [] + if (Array.isArray(newItem.items)) { + newItem.items.forEach(it => { + if (typeof it === 'string') { + it = arr[idx] = { id: it, text: it } + } + if (it.checked && !newItem.selected.includes(it.id)) newItem.selected.push(it.id) + if (!it.checked && newItem.selected.includes(it.id)) it.checked = true + if (it.checked == null) it.checked = false + }) + } + } else if (newItem.type === 'menu-radio') { + if (Array.isArray(newItem.items)) { + newItem.items.forEach((it, idx, arr) => { + if (typeof it === 'string') { + it = arr[idx] = { id: it, text: it } + } + if (it.checked && newItem.selected == null) newItem.selected = it.id; else it.checked = false + if (!it.checked && newItem.selected === it.id) it.checked = true + if (it.checked == null) it.checked = false + }) + } + } + if (id == null) { + this.items.push(newItem) + } else { + const middle = this.get(id, true) + this.items = this.items.slice(0, middle).concat([newItem], this.items.slice(middle)) + } + newItem.line = newItem.line ?? 1 + if (skipRefresh !== true) this.refresh(newItem.id) + }) + if (skipRefresh !== true) this.resize() + } + remove() { + let effected = 0 + Array.from(arguments).forEach(item => { + const it = this.get(item) + if (!it || String(item).indexOf(':') !== -1) return + effected++ + // remove from screen + query(this.box).find('#tb_' + this.name + '_item_' + w2utils.escapeId(it.id)).remove() + // remove from array + const ind = this.get(it.id, true) + if (ind != null) this.items.splice(ind, 1) + }) + this.resize() + return effected + } + set(id, newOptions) { + const item = this.get(id) + if (item == null) return false + Object.assign(item, newOptions) + this.refresh(String(id).split(':')[0]) + return true + } + get(id, returnIndex, items) { + if (arguments.length === 0) { + const all = [] + for (let i1 = 0; i1 < this.items.length; i1++) { + const it = this.items[i1] + if (it.id != null) all.push(it.id) + if (it.type === 'group') { + for (let i2 = 0; i2 < it.items.length; i2++) { + if (it.items[i2].id != null) all.push(it.items[i2].id) + } + } + } + return all + } + const tmp = String(id).split(':') + if (items == null) items = this.items + for (let i1 = 0; i1 < items.length; i1++) { + const it = items[i1] + // find a menu item + if (['menu', 'menu-radio', 'menu-check'].includes(it.type) && tmp.length == 2 && it.id == tmp[0]) { + let subItems = it.items + if (typeof subItems === 'function') subItems = subItems(this) + for (let i = 0; i < subItems.length; i++) { + const item = subItems[i] + if (item.id === tmp[1] || (item.id == null && item.text === tmp[1])) { + if (returnIndex === true) return i; else return item + } + if (Array.isArray(item.items)) { + for (let j = 0; j < item.items.length; j++) { + if (item.items[j].id === tmp[1] || (item.items[j].id == null && item.items[j].text === tmp[1])) { + if (returnIndex === true) return i; else return item.items[j] + } + } + } + } + } else if (it.id === tmp[0]) { + if (returnIndex === true) return i1; else return it + } else if (it.type === 'group') { + const sub = this.get(id, returnIndex, it.items) + if (sub != null) return sub + } + } + return null + } + setCount(id, count, className, style) { + const btn = query(this.box).find(`#tb_${this.name}_item_${w2utils.escapeId(id)} .w2ui-tb-count > span`) + if (btn.length > 0) { + btn.removeClass() + .addClass(className ?? '') + .text(count) + .get(0).style.cssText = style ?? '' + this.last.badge[id] = { + className: className ?? '', + style: style ?? '' + } + const item = this.get(id) + item.count = count + } else { + this.set(id, { count: count }) + this.setCount(...arguments) // to update styles + } + } + show() { + const effected = [] + Array.from(arguments).forEach(item => { + const it = this.get(item) + if (!it) return + // since group can have style, it should still be shown + it.hidden = false + effected.push(String(item).split(':')[0]) + if (it.type === 'group') { + it.items.forEach(itm => this.show(itm.id)) + } + }) + this._refresh({ effected, resize: true }) // debounced, needed for speed + return effected + } + hide() { + const effected = [] + Array.from(arguments).forEach(item => { + const it = this.get(item) + if (!it) return + // since group can have style, it should still be hidden + it.hidden = true + effected.push(String(item).split(':')[0]) + if (it.type === 'group') { + it.items.forEach(itm => this.hide(itm.id)) + } + }) + this._refresh({ effected, hideTooltip: true, resize: true }) // debounced, needed for speed + return effected + } + enable() { + const effected = [] + Array.from(arguments).forEach(item => { + const it = this.get(item) + if (!it) return + if (it.type === 'group') { + it.items.forEach(itm => this.enable(itm.id)) + } else { + it.disabled = false + effected.push(String(item).split(':')[0]) + } + }) + this._refresh({ effected }) // debounced, needed for speed + return effected + } + disable() { + const effected = [] + Array.from(arguments).forEach(item => { + const it = this.get(item) + if (!it) return + if (it.type === 'group') { + it.items.forEach(itm => this.disable(itm.id)) + } else { + it.disabled = true + effected.push(String(item).split(':')[0]) + } + }) + this._refresh({ effected, hideTooltip: true }) // debounced, needed for speed + return effected + } + check() { + const effected = [] + Array.from(arguments).forEach(item => { + const it = this.get(item) + if (!it || String(item).indexOf(':') != -1) return + if (it.type === 'group') { + it.items.forEach(itm => this.check(itm.id)) + } else { + it.checked = true + effected.push(String(item).split(':')[0]) + } + }) + this._refresh({ effected }) // debounced, needed for speed + return effected + } + uncheck() { + const effected = [] + Array.from(arguments).forEach(item => { + const it = this.get(item) + if (!it || String(item).indexOf(':') !== -1) return + // remove overlay + if (['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(it.type) && it.checked) { + w2tooltip.hide(this.name + '-drop') + } + if (it.type === 'group') { + it.items.forEach(itm => this.uncheck(itm.id)) + } else { + it.checked = false + effected.push(String(item).split(':')[0]) + } + }) + this._refresh({ effected }) // debounced, needed for speed + return effected + } + click(id, event) { + // click on menu items + const tmp = String(id).split(':') + const it = this.get(tmp[0]) + let items = (it && it.items ? w2utils.normMenu.call(this, it.items, it) : []) + if (tmp.length > 1) { + const subItem = this.get(id) + if (subItem && !subItem.disabled) { + this.menuClick({ name: this.name, item: it, subItem: subItem, originalEvent: event }) + } + return + } + if (it && !it.disabled) { + // event before + const edata = this.trigger('click', { + target: (id != null ? id : this.name), + item: it, object: it, originalEvent: event + }) + if (edata.isCancelled === true) return + // read items again, they might have been changed in the click event handler + items = (it && it.items ? w2utils.normMenu.call(this, it.items, it) : []) + const btn = '#tb_' + this.name + '_item_' + w2utils.escapeId(it.id) + query(this.box).find(btn).removeClass('down') // need to re-query at the moment -- as well as elsewhere in this function + if (it.type === 'radio') { + for (let i = 0; i < this.items.length; i++) { + const itt = this.items[i] + if (itt.type === 'group') { + for (let i1 = 0; i1 < itt.items.length; i1++) { + const itt1 = itt.items[i1] + if (itt1 == null || itt1.id === it.id || itt1.type !== 'radio') continue + if (itt1.group === it.group && itt1.checked) { + itt1.checked = false + this.refresh(itt1.id) + } + } + } + if (itt == null || itt.id === it.id || itt.type !== 'radio') continue + if (itt.group === it.group && itt.checked) { + itt.checked = false + this.refresh(itt.id) + } + } + it.checked = true + query(this.box).find(btn).addClass('checked') + } + if (['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(it.type)) { + this.tooltipHide(id) + if (it.checked) { + w2tooltip.hide(this.name + '-drop') + return + } else { + /** + * Need to clear all previous event listeners, since tooltip name is reused and it finds the old configuration and + * extends it. If events are not cleared, it would trigger old listeners too. + */ + const overlay = w2tooltip.get(this.name + '-drop') + if (overlay?.displayed) overlay.hide() + overlay?.listeners?.splice(0) + // timeout is needed to make sure previous overlay hides + setTimeout(() => { + const hideDrop = (id, btn) => { + // need a closure to capture id variable + const self = this + return function() { + self.set(id, { checked: false }) + } + } + const el = query(this.box).find('#tb_' + this.name + '_item_' + w2utils.escapeId(it.id)) + if (!w2utils.isPlainObject(it.overlay)) it.overlay = {} + if (it.type === 'drop') { + w2tooltip.show(w2utils.extend({ + html: it.html, + class: 'w2ui-white', + hideOn: ['doc-click'] + }, it.overlay, { + anchor: el[0], + name: this.name + '-drop', + data: { item: it, btn } + })) + .hide(hideDrop(it.id, btn)) + } + if (['menu', 'menu-radio', 'menu-check'].includes(it.type)) { + let menuType = 'normal' + if (it.type === 'menu-radio') { + menuType = 'radio' + items.forEach((item) => { + if (it.selected == item.id) item.checked = true; else item.checked = false + }) + } + if (it.type === 'menu-check') { + menuType = 'check' + items.forEach((item) => { + if (Array.isArray(it.selected) && it.selected.includes(item.id)) item.checked = true; else item.checked = false + }) + } + w2menu.show(w2utils.extend({ + items, + selected: -1, + align: it.text ? 'left' : 'none' // if there is no text, then no alignent + }, it.overlay, { + type: menuType, + name: this.name + '-drop', + anchor: el[0], + data: { item: it, btn } + })) + .hide(hideDrop(it.id, btn)) + .remove(event => { + this.menuClick({ name: this.name, remove: true, item: it, subItem: event.detail.item, + originalEvent: event }) + }) + .select(event => { + this.menuClick({ name: this.name, item: it, subItem: event.detail.item, + originalEvent: event }) + }) + } + if (['color', 'text-color'].includes(it.type)) { + w2color.show(w2utils.extend({ + color: it.color + }, it.overlay, { + anchor: el[0], + name: this.name + '-drop', + data: { item: it, btn } + })) + .hide(hideDrop(it.id, btn)) + .liveUpdate(event => { + const edata = this.trigger('liveUpdate', { name: this.name, item: it, color: event.detail.color }) + edata.finish() + }) + .select(event => { + if (event.detail.color != null) { + this.colorClick({ name: this.name, item: it, color: event.detail.color }) + } + }) + } + }, 0) + } + } + if (['check', 'menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(it.type)) { + it.checked = !it.checked + if (it.checked) { + query(this.box).find(btn).addClass('checked') + } else { + query(this.box).find(btn).removeClass('checked') + } + } + // route processing + if (it.route) { + let route = String('/' + it.route).replace(/\/{2,}/g, '/') + const info = w2utils.parseRoute(route) + if (info.keys.length > 0) { + for (let k = 0; k < info.keys.length; k++) { + route = route.replace((new RegExp(':' + info.keys[k].name, 'g')), this.routeData[info.keys[k].name]) + } + } + setTimeout(() => { window.location.hash = route }, 1) + } + // need to refresh toolbar as it might be dynamic + this.tooltipShow(id) + // event after + edata.finish() + } + } + scroll(direction, line, instant) { + return new Promise((resolve, reject) => { + const scrollBox = query(this.box).find(`.w2ui-tb-line:nth-child(${line}) .w2ui-scroll-wrapper`) + const scrollLeft = scrollBox.get(0).scrollLeft + const right = scrollBox.find('.w2ui-tb-right').get(0) + const width1 = scrollBox.parent().get(0).getBoundingClientRect().width + const width2 = scrollLeft + parseInt(right.offsetLeft) + parseInt(right.clientWidth) + switch (direction) { + case 'left': { + scroll = scrollLeft - width1 + 50 // 35 is width of both button + if (scroll <= 0) scroll = 0 + scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' }) + break + } + case 'right': { + scroll = scrollLeft + width1 - 50 // 35 is width of both button + if (scroll >= width2 - width1) scroll = width2 - width1 + scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' }) + break + } + } + /** + * Timeout is needed because browser animates scroll. Also, I found that 500ms is not enough + * as it could take longer then that, but animation seems to be around 500ms + */ + setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 600) + }) + } + render(box) { + const time = Date.now() + if (typeof box === 'string') box = query(box).get(0) + // event before + const edata = this.trigger('render', { target: this.name, box: box ?? this.box }) + if (edata.isCancelled === true) return + // defaul action + if (box != null) { + this.unmount() // clean previous control + this.box = box + } + if (!this.box) return + if (!Array.isArray(this.right)) { + this.right = [this.right] + } + // render all buttons + let html = '' + let line = 0 + for (let i = 0; i < this.items.length; i++) { + const it = this.items[i] + if (it == null) continue + if (it.id == null) it.id = 'item_' + i + if (it.caption != null) { + console.log('NOTICE: toolbar item.caption property is deprecated, please use item.text. Item -> ', it) + } + if (it.hint != null) { + console.log('NOTICE: toolbar item.hint property is deprecated, please use item.tooltip. Item -> ', it) + } + if (i === 0 || it.type === 'new-line') { + line++ + html += ` +
+
+
${this.right[line - 1] ?? ''}
+
+
+
+
+ ` + } + it.line = line + } + query(this.box) + .attr('name', this.name) + .addClass('w2ui-reset w2ui-toolbar') + .html(html) + if (query(this.box).length > 0) { + query(this.box)[0].style.cssText += this.style + } + // overflow buttons + w2utils.bindEvents(query(this.box).find('.w2ui-tb-line .w2ui-eaction'), this) + // observe div resize + this.last.observeResize = new ResizeObserver(() => { this.resize() }) + this.last.observeResize.observe(this.box) + // refresh all + this.refresh() + this.resize() + // event after + edata.finish() + return Date.now() - time + } + refresh(id) { + const time = Date.now() + // event before + const edata = this.trigger('refresh', { target: (id != null ? id : this.name), item: this.get(id) }) + if (edata.isCancelled === true) return + let edata2 + // refresh all + if (id == null) { + for (let i = 0; i < this.items.length; i++) { + const it1 = this.items[i] + if (it1.id == null) it1.id = 'item_' + i + this.refresh(it1.id) + } + return + } + // create or refresh only one item + const it = this.get(id) + if (it == null) return false + if (typeof it.onRefresh === 'function') { + edata2 = this.trigger('refresh', { target: id, item: it, object: it }) + if (edata2.isCancelled === true) return + } + const selector = `#tb_${this.name}_item_${w2utils.escapeId(it.id)}` + const btn = query(this.box).find(selector) + const html = this.getItemHTML(it) + // hide tooltip + this.tooltipHide(id) + // if there is a spacer, then right HTML is not 100% + if (it.type === 'spacer') { + query(this.box).find(`.w2ui-tb-line:nth-child(${it.line ?? 1})`).find('.w2ui-tb-right').css('width', 'auto') + } + if (btn.length === 0) { + const next = parseInt(this.get(id, true)) + 1 + let $next = query(this.box).find(`#tb_${this.name}_item_${w2utils.escapeId(this.items[next] ? this.items[next].id : '--')}`) // "--" is needed or it will insert wrong + if ($next.length === 0) { + $next = query(this.box).find(`.w2ui-tb-line:nth-child(${it.line}`).find('.w2ui-tb-right').before(html) + } else { + $next.after(html) + } + w2utils.bindEvents(query(this.box).find(`${selector}, ${selector} .w2ui-eaction`), this) + } else { + // refresh + query(this.box).find(selector).replace(query.html(html)) + const newBtn = query(this.box).find(selector) + w2utils.bindEvents(newBtn, this) + w2utils.bindEvents(newBtn.find('.w2ui-eaction'), this) + // update overlay's anchor if changed + const overlays = w2tooltip.get(true) + Object.keys(overlays).forEach(key => { + if (overlays[key].anchor === btn.get(0)) { + overlays[key].anchor = newBtn.get(0) + } + }) + } + if (['menu', 'menu-radio', 'menu-check'].includes(it.type) && it.checked) { + // check selected items + const selected = Array.isArray(it.selected) ? it.selected : [it.selected] + const items = typeof it.items === 'function' ? it.items(it) : [...it.items] + items.forEach((item) => { + if (selected.includes(item.id)) item.checked = true; else item.checked = false + }) + w2menu.update(this.name + '-drop', items) + } + // event after + if (typeof it.onRefresh === 'function') { + edata2.finish() + } + edata.finish() + return Date.now() - time + } + resize() { + const time = Date.now() + // event before + const edata = this.trigger('resize', { target: this.name }) + if (edata.isCancelled === true) return + query(this.box).find('.w2ui-tb-line').each(el => { + // show hide overflow buttons + const box = query(el) + box.find('.w2ui-scroll-left, .w2ui-scroll-right').hide() + const scrollBox = box.find('.w2ui-scroll-wrapper').get(0) + const $right = box.find('.w2ui-tb-right') + const boxWidth = box.get(0).getBoundingClientRect().width + // Do not use $right[0].getBoundingClientRect(). right box is the most left div + const itemsWidth = ($right.length > 0 ? $right[0].offsetLeft + $right[0].clientWidth : 0) + if (boxWidth < itemsWidth) { + // we have overflown content + if (scrollBox.scrollLeft > 0) { + box.find('.w2ui-scroll-left').show() + } + if (boxWidth < itemsWidth - scrollBox.scrollLeft) { + box.find('.w2ui-scroll-right').show() + } + } + }) + // event after + edata.finish() + return Date.now() - time + } + destroy() { + // event before + const edata = this.trigger('destroy', { target: this.name }) + if (edata.isCancelled === true) return + // clean up + if (query(this.box).find('.w2ui-scroll-wrapper').length > 0) { + this.unmount() + } + delete w2ui[this.name] + // event after + edata.finish() + } + unmount() { + super.unmount() + this.last.observeResize?.disconnect() + } + // ======================================== + // --- Internal Functions + getItemHTML(item) { + let html = '' + if (item.caption != null && item.text == null) item.text = item.caption // for backward compatibility + if (item.text == null) item.text = '' + if (item.tooltip == null && item.hint != null) item.tooltip = item.hint // for backward compatibility + if (item.tooltip == null) item.tooltip = '' + if (typeof item.get !== 'function' && (Array.isArray(item.items) || typeof item.items === 'function')) { + item.get = function get(id) { // need scope, cannot be arrow func + let tmp = item.items + if (typeof tmp === 'function') tmp = item.items(item) + return tmp.find(it => it.id == id) + } + } + let icon = '' + let text = (typeof item.text === 'function' ? item.text.call(this, item) : item.text) + if (item.icon) { + icon = item.icon + if (typeof item.icon === 'function') { + icon = item.icon.call(this, item) + } + if (String(icon).slice(0, 1) !== '<') { + icon = `` + } + icon = `
${icon}
` + } + const classes = ['w2ui-tb-button', 'w2ui-eaction'] + if (item.checked) classes.push('checked') + if (item.disabled) classes.push('disabled') + if (item.hidden) classes.push('hidden') + if (!icon) classes.push('no-icon') + switch (item.type) { + case 'color': + case 'text-color': + if (typeof item.color === 'string') { + if (item.color.slice(0, 1) == '#') item.color = item.color.slice(1) + if ([3, 6, 8].includes(item.color.length)) item.color = '#' + item.color + } + if (item.type == 'color') { + text = ` + ${(item.text ? `
${w2utils.lang(item.text)}
` : '')}` + } + if (item.type == 'text-color') { + const color = (item.color != null ? item.color : '#444') + let bcolor = item.backColor + if (item.backColor === true) { + bcolor = '#fff' + if (w2utils.colorContrast('#fff', color) < 2) { + bcolor = '#555' + } + } + text = `${item.text + ? w2utils.lang(item.text) + : (item.backColor + ? `Ab` + : 'Ab' + ) + }` + } + case 'menu': + case 'menu-check': + case 'menu-radio': + case 'button': + case 'check': + case 'radio': + case 'label': + case 'drop': { + const arrow = (item.arrow === true || + (item.arrow !== false && ['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(item.type))) + html = ` +
+ ${icon} + ${(text !== '' && text != null) || item.count != null || arrow + ? `
+ ${w2utils.lang(text)} + ${item.count != null + ? w2utils.stripSpaces(` + + ${item.count} + `) + : '' +} + ${arrow + ? `` + : '' +} +
` + : '' +} +
+ ` + break + } + case 'break': { + html = `
+   +
` + break + } + case 'spacer': { + html = `
+
` + break + } + case 'html': { + html = `
+ ${(typeof item.html === 'function' ? item.html.call(this, item) : item.html)} +
` + break + } + case 'input': { + const ph = item.placeholder + let val = item.value + // For backword compatibility + if (item.spinner && typeof item.spinner === 'object') { + item.input ??= {} + Object.assign(item.input, item.spinner, { spinner: true }) + } + // round to step + if (val != null && String(val).trim() !== '' && item.input?.spinner) { + const step = item.input?.step ?? 1 + const prec = item.input?.precision ?? String(step).split('.')[1]?.length ?? 0 + val = isNaN(val) ? val : Number(val).toFixed(prec) + } + html = `
+ ${item.text ?? ''} + ${item.input?.spinner + ? `` + : ''} + + ${item.input?.spinner + ? ` + ` + : ''} +
` + break + } + case 'group': { + html = `
` + if (Array.isArray(item.items)) { + item.items.forEach(it => { + html += this.getItemHTML(it) + }) + } else { + console.log('ERROR: toolbar group is empty') + } + html += '
' + } + } + return html + } + spinner(id, action, event) { + const it = this.get(id) + let inc = 0 + const edata = this.trigger('keyDown', { id, item: it, originalEvent: event }) + switch (action) { + case 'inc': { + inc = (it.input?.step ?? 1) + break + } + case 'dec': { + inc = -(it.input?.step ?? 1) + break + } + case 'key': { + if (it.input?.spinner || it.input?.step != null) { + let mult = 1 + if (event.shiftKey || event.metaKey) mult = 10 + if (event.altKey) mult = 0.1 + switch (event.key) { + case 'ArrowUp': { + inc = (it.input?.step ?? 1) * mult + event.preventDefault() + break + } + case 'ArrowDown': { + inc = -(it.input?.step ?? 1) * mult + event.preventDefault() + break + } + } + } + break + } + } + if (inc !== 0) { + this.change(id, parseFloat(it.value ?? 0) + inc) + } + edata.finish() + } + change(id, value, dynamic) { + const it = this.get(id) + const input = query(this.box).find('#tb_' + this.name + '_item_' + w2utils.escapeId(id)).find('input.w2ui-toolbar-input') + if (value instanceof HTMLInputElement) { + value = value.value + } + if (value == null) value = input.val() + if (it.input?.spinner || it.input?.min != null || it.input?.max != null || it.input?.step != null) { + value = parseFloat(value) + } + // remove suffix if it is there + if (it.input?.suffix != null && String(value).substr(-it.input.suffix.length) == it.input.suffix) { + value = String(value).substr(0, value.length - it.input.suffix.length) + } + // min/max + if (it.input?.min != null && it.input.min > value) { + value = it.input.min + } + if (it.input?.max != null && it.input.max < value) { + value = it.input.max + } + // round to step + if (it.input?.step != null) { + if (isNaN(value)) value = it.input.min ?? 0 + const step = it.input.step ?? 1 + const prec = it.input.precision ?? String(step).split('.')[1]?.length ?? 0 + value = Number(value).toFixed(prec) + } + // event beofre + const edata = this.trigger(dynamic ? 'input' : 'change', { target: id, id, value, item: it }) + if (edata.isCancelled) { + return + } + it.value = value + let suffix = '' + if (it.input?.suffix != null && String(value).substr(-it.input.suffix.length) != it.input.suffix) { + suffix = it.input.suffix + } + if (!dynamic) input.val(value + suffix) + // event after + edata.finish() + } + tooltipShow(id) { + if (this.tooltip == null) return + const el = query(this.box).find('#tb_' + this.name + '_item_' + w2utils.escapeId(id)).get(0) + const item = this.get(id) + const overlay = (typeof this.tooltip === 'string' ? { position: this.tooltip } : this.tooltip) + let txt = item.tooltip + if (typeof txt === 'function') txt = txt.call(this, item) + // not for opened drop downs + if (['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(item.type) && + item.checked == true) { + return + } + w2tooltip.show({ + anchor: el, + name: this.name + '-tooltip', + html: txt, + ...overlay + }) + return + } + tooltipHide(id) { + if (this.tooltip == null) return + w2tooltip.hide(this.name + '-tooltip') + } + menuClick(event) { + if (event.item && !event.item.disabled) { + // event before + const edata = this.trigger((event.remove !== true ? 'click' : 'remove'), { + target: event.item.id + ':' + event.subItem.id, item: event.item, + subItem: event.subItem, originalEvent: event.originalEvent + }) + if (edata.isCancelled === true) return + // route processing + const it = event.subItem + const item = this.get(event.item.id) + let items = item.items + if (typeof items === 'function') items = item.items() + if (item.type == 'menu') { + item.selected = it.id + } + if (item.type == 'menu-radio') { + item.selected = it.id + if (Array.isArray(items)) { + items.forEach((item) => { + if (item.checked === true) delete item.checked + if (Array.isArray(item.items)) { + item.items.forEach((item) => { + if (item.checked === true) delete item.checked + }) + } + }) + } + it.checked = true + } + if (item.type == 'menu-check') { + if (!Array.isArray(item.selected)) item.selected = [] + if (it.group == null) { + const ind = item.selected.indexOf(it.id) + if (ind == -1) { + item.selected.push(it.id) + it.checked = true + } else { + item.selected.splice(ind, 1) + it.checked = false + } + } else if (it.group === false) { + // if group is false, then it is not part of checkboxes + } else { + const unchecked = [] + const ind = item.selected.indexOf(it.id) + const checkNested = (items) => { + items.forEach((sub) => { + if (sub.group === it.group) { + const ind = item.selected.indexOf(sub.id) + if (ind != -1) { + if (sub.id != it.id) unchecked.push(sub.id) + item.selected.splice(ind, 1) + } + } + if (Array.isArray(sub.items)) checkNested(sub.items) + }) + } + checkNested(items) + if (ind == -1) { + item.selected.push(it.id) + it.checked = true + } + } + } + if (typeof it.route === 'string') { + let route = it.route !== '' ? String('/' + it.route).replace(/\/{2,}/g, '/') : '' + const info = w2utils.parseRoute(route) + if (info.keys.length > 0) { + for (let k = 0; k < info.keys.length; k++) { + if (this.routeData[info.keys[k].name] == null) continue + route = route.replace((new RegExp(':' + info.keys[k].name, 'g')), this.routeData[info.keys[k].name]) + } + } + setTimeout(() => { window.location.hash = route }, 1) + } + this.refresh(event.item.id) + // event after + edata.finish() + } + } + colorClick(event) { + const obj = this + if (event.item && !event.item.disabled) { + // event before + const edata = this.trigger('click', { target: event.item.id, item: event.item, color: event.color, final: true }) + if (edata.isCancelled === true) return + // default behavior + event.item.color = event.color + obj.refresh(event.item.id) + // event after + edata.finish() + } + } + mouseAction(event, target, action, id) { + const btn = this.get(id) + const edata = this.trigger('mouse' + action, { target: id, item: btn, object: btn, originalEvent: event }) + if (edata.isCancelled === true || btn.disabled || btn.hidden) return + switch (action) { + case 'Enter': + if (!['label', 'input'].includes(btn.type)) { + query(target).addClass('over') + } + this.tooltipShow(id) + break + case 'Leave': + if (!['label', 'input'].includes(btn.type)) { + query(target).removeClass('over down') + } + this.tooltipHide(id) + break + case 'Down': + if (!['label', 'input'].includes(btn.type)) { + query(target).addClass('down') + } + break + case 'Up': + if (!['label', 'input'].includes(btn.type)) { + query(target).removeClass('down') + } + break + } + edata.finish() + } +} +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base, w2tooltip, w2menu + * + * == TODO == + * - dbl click should be like it is in grid (with timer not HTML dbl click event) + * - node.style is misleading - should be there to apply color for example + * - node.plus - is not working + * + * == 2.0 changes + * - remove jQuery dependency + * - deprecarted obj.img, node.img + * - CSP - fixed inline events + * - observeResize for the box + * - search(..., compare) - comparison function + * - editable = true + * - edit(id) - new method + * - onEdit, onRename - new events + * - reorder = true - to allow reorder + * - mouseDown - for reorder + * - onReorder, onDragStart, onDragOver - events + * - this.mutlti - for multi select (ctrl for one at a time and shift for range) + * - onSelect, onUnselect - new events + * - prev(), next(), getChain() + */ + +class w2sidebar extends w2base { + constructor(options) { + super(options.name) + this.name = null + this.box = null + this.sidebar = null + this.parent = null + this.nodes = [] // Sidebar child nodes + this.menu = [] + this.routeData = {} // data for dynamic routes + this.selected = null // current selected node (readonly) + this.icon = null + this.style = '' + this.topHTML = '' + this.bottomHTML = '' + this.multi = false + this.editable = false + this.reorder = false + this.flatButton = false + this.keyboard = true + this.flat = false + this.hasFocus = false + this.levelPadding = 12 + this.toggleAlign = 'right' // can be left or right + this.skipRefresh = false + this.tabIndex = null // will only be set if > 0 and not null + this.handle = { width: 0, style: '', text: '', tooltip: '' }, + this.badge = null + this.onClick = null // Fire when user click on Node Text + this.onSelect = null + this.onUnselect = null + this.onDblClick = null // Fire when user dbl clicks + this.onMouseEnter = null // mouse enter/leave over an item + this.onMouseLeave = null + this.onContextMenu = null + this.onMenuClick = null // when context menu item selected + this.onExpand = null // Fire when node expands + this.onCollapse = null // Fire when node collapses + this.onKeydown = null + this.onRender = null + this.onRefresh = null + this.onResize = null + this.onDestroy = null + this.onFocus = null + this.onBlur = null + this.onFlat = null + this.onEdit = null + this.onRename = null + this.onReorder = null + this.onDragStart = null + this.onDragOver = null + this.node_template = { + id: null, + text: '', + order: null, + count: null, + icon: null, + nodes: [], + style: '', // additional style for subitems + route: null, + selected: false, + expanded: false, + hidden: false, + disabled: false, + group: false, // if true, it will build as a group + groupShowHide: true, + collapsible: false, + plus: false, // if true, plus will be shown even if there is no sub nodes + childOffset: 0, + // events + onClick: null, + onDblClick: null, + onContextMenu: null, + onExpand: null, + onCollapse: null, + // internal + parent: null, // node object + sidebar: null + } + this.last = { + badge: {}, + renaming: false, + move: null // object, move details + } + const nodes = options.nodes + delete options.nodes + // mix in options + Object.assign(this, options) + // add item via method to makes sure item_template is applied + if (Array.isArray(nodes)) this.add(nodes) + // need to reassign back to keep it in config + options.nodes = nodes + // render if box specified + if (typeof this.box === 'string') this.box = query(this.box).get(0) + if (this.box) this.render(this.box) + } + add(parent, nodes) { + if (arguments.length == 1) { + // need to be in reverse order + nodes = arguments[0] + parent = this + } + if (typeof parent === 'string') parent = this.get(parent) + if (parent == null || parent == '') parent = this + return this.insert(parent, null, nodes) + } + insert(parent, before, nodes) { + let txt, ind, tmp, node, nd + if (arguments.length == 2 && typeof parent === 'string') { + // need to be in reverse order + nodes = arguments[1] + before = arguments[0] + if (before != null) { + ind = this.get(before) + if (ind == null) { + if (!Array.isArray(nodes)) nodes = [nodes] + if (nodes[0].caption != null && nodes[0].text == null) { + console.log('NOTICE: sidebar node.caption property is deprecated, please use node.text. Node -> ', nodes[0]) + nodes[0].text = nodes[0].caption + } + txt = nodes[0].text + console.log('ERROR: Cannot insert node "' + txt + '" because cannot find node "' + before + '" to insert before.') + return null + } + parent = this.get(before).parent + } else { + parent = this + } + } + if (typeof parent === 'string') parent = this.get(parent) + if (parent == null || parent == '') parent = this + if (!Array.isArray(nodes)) nodes = [nodes] + for (let o = 0; o < nodes.length; o++) { + node = nodes[o] + if (node.caption != null && node.text == null) { + console.log('NOTICE: sidebar node.caption property is deprecated, please use node.text') + node.text = node.caption + } + if (typeof node.id == null) { + txt = node.text + console.log('ERROR: Cannot insert node "' + txt + '" because it has no id.') + continue + } + if (this.get(this, node.id) != null) { + console.log('ERROR: Cannot insert node with id=' + node.id + ' (text: ' + node.text + ') because another node with the same id already exists.') + continue + } + tmp = Object.assign({}, this.node_template, node) + tmp.sidebar = this + tmp.parent = parent + nd = tmp.nodes || [] + tmp.nodes = [] // very important to re-init empty nodes array + if (before == null) { // append to the end + parent.nodes.push(tmp) + } else { + ind = this.get(parent, before, true) + if (ind == null) { + console.log('ERROR: Cannot insert node "' + node.text + '" because cannot find node "' + before + '" to insert before.') + return null + } + parent.nodes.splice(ind, 0, tmp) + } + if (nd.length > 0) { + this.insert(tmp, null, nd) + } + } + if (!this.skipRefresh) this.refresh(parent.id) + return tmp + } + remove() { // multiple arguments + let effected = 0 + let node + Array.from(arguments).forEach(arg => { + node = this.get(arg) + if (node == null) return + if (this.selected != null) { + if (Array.isArray(this.selected)) { + this.selected.splice(this.selected.indexOf(node.id), 1) + } else if (this.selected === node.id) { + this.selected = null + } + } + const ind = this.get(node.parent, arg, true) + if (ind == null) return + if (node.parent.nodes[ind].selected) node.sidebar.unselect(node.id) + node.parent.nodes.splice(ind, 1) + node.parent.collapsible = node.parent.nodes.length > 0 + effected++ + }) + if (!this.skipRefresh) { + if (effected > 0 && arguments.length == 1) this.refresh(node.parent.id); else this.refresh() + } + return effected + } + set(parent, id, node) { + if (arguments.length == 2) { + // need to be in reverse order + node = id + id = parent + parent = this + } + // searches all nested nodes + if (typeof parent === 'string') parent = this.get(parent) + if (parent.nodes == null) return null + for (let i = 0; i < parent.nodes.length; i++) { + if (parent.nodes[i].id === id) { + // see if quick update is possible + const res = this.update(id, node) + if (Object.keys(res).length != 0) { + // make sure nodes inserted correctly + const nodes = node.nodes + w2utils.extend(parent.nodes[i], node, (nodes != null ? { nodes: [] } : {})) + if (nodes != null) { + this.add(parent.nodes[i], nodes) + } + if (!this.skipRefresh) this.refresh(id) + } + return true + } else { + const rv = this.set(parent.nodes[i], id, node) + if (rv) return true + } + } + return false + } + get(parent, id, returnIndex) { // can be just called get(id) or get(id, true) + if (arguments.length === 0) { + const all = [] + const tmp = this.find({}) + for (let t = 0; t < tmp.length; t++) { + if (tmp[t].id != null) all.push(tmp[t].id) + } + return all + } else { + if (arguments.length == 1 || (arguments.length == 2 && id === true)) { + // need to be in reverse order + returnIndex = id + id = parent + parent = this + } + // searches all nested nodes + if (typeof parent === 'string') parent = this.get(parent) + if (parent.nodes == null) return null + for (let i = 0; i < parent.nodes.length; i++) { + if (parent.nodes[i].id == id) { + if (returnIndex === true) return i; else return parent.nodes[i] + } else { + const rv = this.get(parent.nodes[i], id, returnIndex) + if (rv || rv === 0) return rv + } + } + return null + } + } + setCount(id, count, options = {}) { + const node = this.get(id) + if (node.group) { + console.log(`Node "${id}" is a group and groups cannot have counts or badges.`) + return + } + this.last.badge[id] = { + className: options.className ?? '', + style: options.style ?? '' + } + const btn = query(this.box).find(`#node_${w2utils.escapeId(id)} .w2ui-node-badge`) + if (btn.length > 0) { + btn.removeClass() + .addClass(`w2ui-node-badge ${options.className ?? 'w2ui-node-count'}`) + .text(count) + .get(0).style.cssText = options.style || '' + const item = this.get(id) + item.count = count + } else if (!options.noRepeat) { + this.set(id, { count }) + options.noRepeat = true + queueMicrotask(() => this.setCount(id, count, options)) // to update styles + } + } + find(parent, params, results) { // can be just called find({ selected: true }) + // TODO: rewrite with this.each() + if (arguments.length == 1) { + // need to be in reverse order + params = parent + parent = this + } + if (!results) results = [] + // searches all nested nodes + if (typeof parent === 'string') parent = this.get(parent) + if (parent.nodes == null) return results + for (let i = 0; i < parent.nodes.length; i++) { + let match = true + for (const prop in params) { // params is an object + if (parent.nodes[i][prop] != params[prop]) match = false + } + if (match) results.push(parent.nodes[i]) + if (parent.nodes[i].nodes.length > 0) results = this.find(parent.nodes[i], params, results) + } + return results + } + sort(options, nodes) { + // default options + if (!options || typeof options !== 'object') options = {} + if (options.foldersFirst == null) options.foldersFirst = true + if (options.caseSensitive == null) options.caseSensitive = false + if (options.reverse == null) options.reverse = false + if (nodes == null) { + nodes = this.nodes + } + nodes.sort((a, b) => { + // folders first + const isAfolder = (a.nodes && a.nodes.length > 0) + const isBfolder = (b.nodes && b.nodes.length > 0) + // both folder or both not folders + if (options.foldersFirst === false || (!isAfolder && !isBfolder) || (isAfolder && isBfolder)) { + let aText = a.text + let bText = b.text + if (a.order != null) aText = a.order + if (b.order != null) bText = b.order + if (!options.caseSensitive) { + aText = aText.toLowerCase() + bText = bText.toLowerCase() + } + const cmp = w2utils.naturalCompare(aText, bText) + return (cmp === 1 || cmp === -1) & options.reverse ? -cmp : cmp + } + if (isAfolder && !isBfolder) { + return !options.reverse ? -1 : 1 + } + if (!isAfolder && isBfolder) { + return !options.reverse ? 1 : -1 + } + }) + nodes.forEach(node => { + if (node.nodes && node.nodes.length > 0) { + this.sort(options, node.nodes) + } + }) + } + each(fn, nodes) { + if (nodes == null) nodes = this.nodes + nodes.forEach((node) => { + fn.call(this, node) + if (node.nodes && node.nodes.length > 0) { + this.each(fn, node.nodes) + } + }) + } + search(str, compare = null) { + let count = 0 + const str2 = str.toLowerCase() + this.each((node) => { + let match = false + if (typeof compare === 'function') { + match = compare(str, node) + } else { + match = !(node.text.toLowerCase().indexOf(str2) === -1) + } + if (match) { + count++ + showParents(node) + node.hidden = false + } else { + node.hidden = true + } + }) + this.refresh() + return count + function showParents(node) { + if (node.parent) { + node.parent.hidden = false + showParents(node.parent) + } + } + } + show() { // multiple arguments + const effected = [] + Array.from(arguments).forEach(it => { + const node = this.get(it) + if (node == null || node.hidden === false) return + node.hidden = false + effected.push(node.id) + }) + if (effected.length > 0) { + if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh() + } + return effected + } + hide() { // multiple arguments + const effected = [] + Array.from(arguments).forEach(it => { + const node = this.get(it) + if (node == null || node.hidden === true) return + node.hidden = true + effected.push(node.id) + }) + if (effected.length > 0) { + if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh() + } + return effected + } + enable() { // multiple arguments + const effected = [] + Array.from(arguments).forEach(it => { + const node = this.get(it) + if (node == null || node.disabled === false) return + node.disabled = false + effected.push(node.id) + }) + if (effected.length > 0) { + if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh() + } + return effected + } + disable() { // multiple arguments + const effected = [] + Array.from(arguments).forEach(it => { + const node = this.get(it) + if (node == null || node.disabled === true) return + node.disabled = true + if (node.selected) this.unselect(node.id) + effected.push(node.id) + }) + if (effected.length > 0) { + if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh() + } + return effected + } + select(id) { + if (Array.isArray(id)) { + [...id].forEach(id => this.select(id)) + return + } + const new_node = this.get(id) + if (!new_node) return false + // event before + const edata = this.trigger('select', { target: id, id, node: new_node }) + if (edata.isCancelled === true) { + return true + } + // if already selected + if (!this.multi && this.selected == id && new_node.selected) { + return false + } + const $el = query(this.box).find('#node_' + w2utils.escapeId(id)) + $el.addClass('w2ui-selected') + .find('.w2ui-icon') + .addClass('w2ui-icon-selected') + if ($el.length > 0) { + if (!this.inView(id)) this.scrollIntoView(id) + } + new_node.selected = true + if (this.multi) { + if (!Array.isArray(this.selected)) { + this.selected = this.selected ? [this.selected] : [] + } + this.selected.push(id) + } else { + this.selected = this.multi ? [id] : id + } + edata.finish() + return true + } + unselect(id) { + // if no arguments provided, unselect selected node + if (arguments.length === 0) { + id = this.selected + } + if (Array.isArray(id)) { + [...id].forEach(id => this.unselect(id)) + return + } + const current = this.get(id) + if (!current) return false + // event before + const edata = this.trigger('unselect', { target: id, id, node: current }) + if (edata.isCancelled === true) { + return true + } + current.selected = false + query(this.box).find('#node_' + w2utils.escapeId(id)) + .removeClass('w2ui-selected') + .find('.w2ui-icon').removeClass('w2ui-icon-selected') + if (typeof this.selected === 'string' && this.selected == id) { + this.selected = null + } + if (this.multi && Array.isArray(this.selected)) { + const ind = this.selected.indexOf(id) + if (ind != -1) this.selected.splice(ind, 1) + } + edata.finish() + return true + } + toggle(id) { + const nd = this.get(id) + if (nd == null) return false + if (nd.plus) { + this.set(id, { plus: false }) + this.expand(id) + this.refresh(id) + return + } + if (nd.nodes.length === 0) return false + if (!nd.collapsible) return false + if (this.get(id).expanded) return this.collapse(id); else return this.expand(id) + } + collapse(id) { + const nd = this.get(id) + if (nd == null) return false + // event before + const edata = this.trigger('collapse', { target: id, object: nd, node: nd }) + if (edata.isCancelled === true) return + // default action + query(this.box).find('#node_' + w2utils.escapeId(id) + '_sub').hide() + query(this.box).find('#node_' + w2utils.escapeId(id) + ' .w2ui-expanded') + .removeClass('w2ui-expanded') + .addClass('w2ui-collapsed') + nd.expanded = false + // event after + edata.finish() + this.refresh(id) + return true + } + expand(id) { + const nd = this.get(id) + // event before + const edata = this.trigger('expand', { target: id, object: nd, node: nd }) + if (edata.isCancelled === true) return + // default action + query(this.box).find('#node_' + w2utils.escapeId(id) + '_sub') + .show() + query(this.box).find('#node_' + w2utils.escapeId(id) + ' .w2ui-collapsed') + .removeClass('w2ui-collapsed') + .addClass('w2ui-expanded') + nd.expanded = true + // event after + edata.finish() + this.refresh(id) + return true + } + collapseAll(parent) { + if (parent == null) parent = this + if (typeof parent === 'string') parent = this.get(parent) + if (parent.nodes == null) return false + for (let i = 0; i < parent.nodes.length; i++) { + if (parent.nodes[i].expanded === true) parent.nodes[i].expanded = false + if (parent.nodes[i].nodes && parent.nodes[i].nodes.length > 0) this.collapseAll(parent.nodes[i]) + } + this.refresh(parent.id) + return true + } + expandAll(parent) { + if (parent == null) parent = this + if (typeof parent === 'string') parent = this.get(parent) + if (parent.nodes == null) return false + for (let i = 0; i < parent.nodes.length; i++) { + if (parent.nodes[i].expanded === false) parent.nodes[i].expanded = true + if (parent.nodes[i].nodes && parent.nodes[i].nodes.length > 0) this.expandAll(parent.nodes[i]) + } + this.refresh(parent.id) + } + expandParents(id) { + const node = this.get(id) + if (node == null) return false + if (node.parent) { + if (!node.parent.expanded) { + node.parent.expanded = true + this.refresh(node.parent.id) + } + this.expandParents(node.parent.id) + } + return true + } + click(id, event) { + const obj = this + const nd = this.get(id) + if (nd == null) return + if (nd.disabled || nd.group) { + // even if disabled, it should still emit click event + const edata = obj.trigger('click', { target: id, originalEvent: event, node: nd, object: nd }) + edata.finish() + return + } + // select new one + const newNode = query(obj.box).find('#node_' + w2utils.escapeId(id)) + newNode.addClass('w2ui-selected').find('.w2ui-icon').addClass('w2ui-icon-selected') + // need timeout to allow rendering + setTimeout(() => { + // event before + const edata = obj.trigger('click', { target: id, originalEvent: event, node: nd, object: nd }) + if (edata.isCancelled === true) { + // restore selection + newNode.removeClass('w2ui-selected').find('.w2ui-icon').removeClass('w2ui-icon-selected') + return + } + // default action + if (this.multi) { + /** + * Multi select with shift or ctrl/meta + */ + const isShift = event?.shiftKey ?? false + const isCtrl = (event?.ctrlKey || event?.metaKey) ?? false + if (typeof this.selected === 'string') { + this.selected = [this.selected] + } + if (isCtrl && !isShift) { // only Ctrl + if (this.selected?.includes(id)) { + this.unselect(id) + return + } else { + this.select(id) + } + } else if (!isCtrl && isShift) { // only Shift + // select range in between + const chain = this.getChain() + const ind1 = Math.min(this.selected.map(sel => chain.indexOf(sel))) // first item in selection + const ind2 = chain.indexOf(id) + for (let i = Math.min(ind1, ind2); i < chain.length && i <= Math.max(ind1, ind2); i++) { + const node = this.get(chain[i]) + if (!this.selected.includes(chain[i]) && node.hidden != true) { + this.select(chain[i]) + } + } + } else { // neither + const ids = this.selected?.filter(sid => sid != id && this.selected.includes(sid)) + this.unselect(ids) + // only select if it is not selected + if (!this.selected?.includes(id)) { + this.select(id) + } + } + } else if (this.selected !== id) { + /** + * Single selection at a time + */ + if (this.selected != null) this.unselect(this.selected) + this.select(id) + // route processing + if (typeof nd.route === 'string') { + let route = nd.route !== '' ? String('/' + nd.route).replace(/\/{2,}/g, '/') : '' + const info = w2utils.parseRoute(route) + if (info.keys.length > 0) { + for (let k = 0; k < info.keys.length; k++) { + if (obj.routeData[info.keys[k].name] == null) continue + route = route.replace((new RegExp(':' + info.keys[k].name, 'g')), obj.routeData[info.keys[k].name]) + } + } + setTimeout(() => { window.location.hash = route }, 1) + } + // if sidebar is flat - show menu + if (this.flat) { + const items = _getItems(nd.nodes) + if (items.length > 0) { + this.flatMenu(newNode, items) + } + function _getItems(nodes) { + const items = nodes.map(it => { + const items = it.nodes.length > 0 ? _getItems(it.nodes) : null + return { id: it.id, text: it.text, icon: it.icon, items } + }) + return items + } + } + } + // event after + edata.finish() + }, 1) + } + flatMenu(el, items) { + const self = this + const $el = query(el).find('.w2ui-node-data') + w2menu.show({ + anchor: $el.get(0), + name: this.name + '_flat-menu', + items, + // class: 'w2ui-dark', + position: 'right|left', + onSelect(event) { + self.unselect() + self.click(event.detail.item.id, event.detail.originalEvent) + }, + onHide(event) { + self.unselect() + } + }) + w2tooltip.hide(this.name + '_tooltip') + } + focus(event) { + const self = this + // event before + const edata = this.trigger('focus', { target: this.name, originalEvent: event }) + if (edata.isCancelled === true) return false + // default behaviour + this.hasFocus = true + query(this.box).find('.w2ui-sidebar-body').addClass('w2ui-focus') + setTimeout(() => { + const input = query(self.box).find('#sidebar_' + self.name + '_focus').get(0) + if (document.activeElement != input) input.focus() + }, 10) + // event after + edata.finish() + } + blur(event) { + // event before + const edata = this.trigger('blur', { target: this.name, originalEvent: event }) + if (edata.isCancelled === true) return false + // default behaviour + this.hasFocus = false + query(this.box).find('.w2ui-sidebar-body').removeClass('w2ui-focus') + // event after + edata.finish() + } + next(node, noSubs) { + if (node == null) return null + const parent = node.parent + const ind = this.get(node.id, true) + let nextNode = null + // jump inside + if (node.expanded && node.nodes.length > 0 && noSubs !== true) { + const nd = node.nodes[0] + if (nd.hidden || nd.disabled || nd.group) nextNode = this.next(nd); else nextNode = nd + } else { + if (parent && ind + 1 < parent.nodes.length) { + nextNode = parent.nodes[ind + 1] + } else { + nextNode = this.next(parent, true) // jump to the parent + } + } + if (nextNode != null && (nextNode.hidden || nextNode.disabled || nextNode.group)) nextNode = this.next(nextNode) + return nextNode + } + prev(node) { + if (node == null) return null + const parent = node.parent + const ind = this.get(node.id, true) + const lastChild = (node) => { + if (node.expanded && node.nodes.length > 0) { + const nd = node.nodes[node.nodes.length - 1] + if (nd.hidden || nd.disabled || nd.group) return this.prev(nd); else return lastChild(nd) + } + return node + } + let prevNode = (ind > 0) ? lastChild(parent.nodes[ind - 1]) : parent + if (prevNode != null && (prevNode.hidden || prevNode.disabled || prevNode.group)) prevNode = this.prev(prevNode) + return prevNode + } + // returns ids of expanded elements as a flat array + getChain(nodes, options = {}) { + options.returnDisabled ??= false + options.returnGroups ??= false + const ids = [] + if (nodes == null) nodes = this.nodes + nodes.forEach(node => { + // can skip disabled if needed + if ((!node.disabled && !node.group) || (node.disabled && options.returnDisabled) || (node.group && options.returnGroups)) { + ids.push(node.id) + } + if (Array.isArray(node.nodes) && node.expanded) { + ids.push(...this.getChain(node.nodes, options)) + } + }) + return ids + } + keydown(event) { + const self = this + const first = Array.isArray(this.selected) ? this.selected[0] : this.selected + let nd = self.get(first) + if (this.keyboard !== true) return + if (!nd) nd = this.nodes[0] + // if user hits esc and there is active move + if (event.keyCode == 27) { + const mv = this.last.move + if (mv?.reorder && mv?.moved) { + mv.restore() + return + } + } + // trigger event + const edata = this.trigger('keydown', { target: this.name, originalEvent: event }) + if (edata.isCancelled === true) return + // default behaviour + if (event.keyCode == 13 || event.keyCode == 32) { // enter or space + if (event.keyCode == 13 && this.editable && !event.ctrlKey && !event.metaKey) { + this.edit(first) + } else { + if (nd.nodes.length > 0) { + this.toggle(first) + } + } + } + if (event.keyCode == 37) { // left + if (nd.nodes.length > 0 && nd.expanded) { + this.collapse(first) + } else { + selectNode(nd.parent) + if (!nd.parent.group) this.collapse(nd.parent.id) + } + } + if (event.keyCode == 39) { // right + if ((nd.nodes.length > 0 || nd.plus) && !nd.expanded) this.expand(first) + } + if (event.keyCode == 38) { // up + if (this.get(first) == null) { + selectNode(this.nodes[0] || null) + } else { + selectNode(neighbor(nd, this.prev)) + } + } + if (event.keyCode == 40) { // down + if (this.get(first) == null) { + selectNode(this.nodes[0] || null) + } else { + selectNode(neighbor(nd, this.next)) + } + } + // cancel event if needed + if ([13, 32, 37, 38, 39, 40].includes(event.keyCode)) { + if (event.preventDefault) event.preventDefault() + if (event.stopPropagation) event.stopPropagation() + } + // event after + edata.finish() + function selectNode(node, event) { + if (node != null && !node.hidden && !node.disabled && !node.group) { + self.click(node.id, event) + if (!self.inView(node.id)) self.scrollIntoView(node.id) + } + } + function neighbor(node, neighborFunc) { + node = neighborFunc.call(self, node) + while (node != null && (node.hidden || node.disabled)) { + if (node.group) break; else node = neighborFunc(node) + } + return node + } + } + inView(id) { + const item = query(this.box).find('#node_' + w2utils.escapeId(id)).get(0) + if (!item) { + return false + } + const div = query(this.box).find('.w2ui-sidebar-body').get(0) + if (item.offsetTop < div.scrollTop || (item.offsetTop + item.clientHeight > div.clientHeight + div.scrollTop)) { + return false + } + return true + } + scrollIntoView(id, instant) { + return new Promise((resolve, reject) => { + if (id == null) id = Array.isArray(this.selected) ? this.selected[0] : this.selected + const nd = this.get(id) + if (nd == null) return + const item = query(this.box).find('#node_' + w2utils.escapeId(id)).get(0) + item.scrollIntoView({ block: 'center', inline: 'center', behavior: instant ? 'atuo' : 'smooth' }) + setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 500) + }) + } + dblClick(id, event) { + const nd = this.get(id) + // event before + const edata = this.trigger('dblClick', { target: id, originalEvent: event, object: nd }) + if (edata.isCancelled === true) return + // default action + if (this.editable) { + this.edit(id) + } else if (!this.flat) { + this.toggle(id) + } + // event after + edata.finish() + } + /** + * This is needed for not reorder + */ + mouseDown(id, event) { + const self = this + if (this.reorder) { + this.last.move = { + x: event.screenX, + y: event.screenY, + divX: 0, + divY: 0, + reorder: true, + moved: false + } + // display empty record and ghost record + const mv = this.last.move + const body = query(this.box).find('.w2ui-sidebar-body') + if (!mv.ghost) { + const node = query(this.box).find(`#node_${w2utils.escapeId(id)}`) + mv.offsetY = event.offsetY + mv.target = id + mv.pos = { top: node.get(0).offsetTop - 1, left: node.get(0).offsetLeft } + // ghost content + const clone = query(node.find('.w2ui-node-data').get(0).cloneNode(true)) + mv.node = node + mv.nodeSub = node.next() + body.append('') + query(this.box).find('#sidebar_' + this.name + '_ghost').append(clone) + mv.ghost = query(this.box).find('#sidebar_' + this.name + '_ghost') + mv.ghost.css({ display: 'none' }) + mv.restore = () => { + mv.resetReorder() + this.refresh() + } + mv.resetReorder = () => { + this.last.move = null + query(this.box).find(`#sidebar_${this.name}_ghost`).remove() + query(document).off(`.w2ui-${this.name}-reorder`) + } + } + // add mouse move and stop events + query(document) + .on(`mousemove.w2ui-${this.name}-reorder`, _mouseMove) + .on(`mouseup.w2ui-${this.name}-reorder`, _mouseStop) + } + function _mouseMove(event) { + if (!event.target.tagName) { + // element has no tagName - most likely the target is the #document itself + // this can happen is you click+drag and move the mouse out of the DOM area, + // e.g. into the browser's toolbar area + return + } + const mv = self.last.move + mv.divX = (event.screenX - mv.x) + mv.divY = (event.screenY - mv.y) + if (Math.abs(mv.divX) <= 1 && Math.abs(mv.divY) <= 1) return // only if moved more then 1px + if (self.reorder == true && mv.reorder && !mv.moved) { + const edata = self.trigger('dragStart', { target: mv.target, moved: true, node: self.get(mv.target), mv, originalEvent: event }) + if (edata.isCancelled === true) { + mv.restore() + return + } + const rect = mv.node.get(0).getBoundingClientRect() + mv.moved = true + mv.node.html('') + .removeAttr('id', 'data-id') + .addClass('w2ui-reorder-empty') + .css({ height: rect.height + 'px' }) + // if there are children + if (mv.node.next().css('display') !== 'none') { + const rect = mv.node.next().get(0).getBoundingClientRect() + mv.node.next() + .html('
') + .css({ height: rect.height + 'px' }) + } + mv.ghost.css({ display: 'block' }) + // event after + edata.finish() + } + // move ghost mode + mv.ghost.css({ + top: (mv.pos.top + mv.divY) + 'px', + left: 0 + }) + const over = query(event.target).closest('.w2ui-node, .w2ui-node-group') + const id = over.attr('data-id') + // append to the end + if (query(event.target).hasClass('w2ui-sidebar-body') && event.layerY > 5 && !mv.append) { + const edata = self.trigger('dragOver', { target: mv.target, append: true, mv, originalEvent: event }) + if (edata.isCancelled === true) { + return + } + mv.ghost.before(mv.node) + mv.ghost.before(mv.nodeSub) + mv.append = true + mv.moveBefore = null + // event after + edata.finish() + } else if (id != null && id != mv.moveBefore) { + mv.append = false + mv.moveBefore = id + // reorder nodes + const edata = self.trigger('dragOver', { target: mv.target, moveBefore: id, mv, originalEvent: event }) + if (edata.isCancelled === true) { + return + } + const el = query(self.box).find(`#node_${w2utils.escapeId(id)}`) + el.before(mv.node) + el.before(mv.nodeSub) + // event after + edata.finish() + } + } + function _mouseStop(event) { + const mv = self.last.move + mv.resetReorder() + if (mv.moved) { + if (((mv.moveBefore != null && mv.target != mv.moveBefore) || mv.append)) { + const edata = self.trigger('reorder', { target: mv.target, moveBefore: mv.moveBefore, append: mv.append, originalEvent: event }) + if (edata.isCancelled === true) { + self.refresh() + return + } + // remove + const target = self.get(mv.target) + const targetInd = target.parent.nodes.indexOf(target) + const cut = target.parent.nodes.splice(targetInd, 1) + // insert + if (mv.append) { + self.nodes.push(...cut) + cut.forEach(nd => nd.parent = self) + } else { + const before = self.get(mv.moveBefore) + const beforeInd = before.parent.nodes.indexOf(before) + cut.forEach(nd => nd.parent = before.parent) + before.parent.nodes.splice(beforeInd, 0, ...cut) + } + // refresh + self.refresh() + // event after + edata.finish() + } else { + self.refresh() + } + } + } + } + edit(id) { + const self = this + const node = query(this.box).find('#node_' + w2utils.escapeId(id)) + const text = node.find('.w2ui-node-text') + // event before + const edata = this.trigger('edit', { target: id, el: node, textEl: text }) + if (edata.isCancelled === true) { + return + } + this.last.renaming = true + node.addClass('w2ui-editing') + text.addClass('w2ui-focus') + .css('pointer-events', 'all') + .attr('contenteditable', w2utils.isFirefox ? 'true' : 'plaintext-only') + .on('blur.node-editing', event => { + // timeout is needed to add to the end of the event loop + setTimeout(_rename, 0) + }) + .on('keydown.node-editing', event => { + if (event.keyCode == 13) _rename(event) + if (event.keyCode == 27) _rename(event, true) + }) + .get(0).focus() + const original = text.text() + // select everything inside + w2utils.setCursorPosition(text[0], 0, text.text().length) + // event after + edata.finish() + return text.get(0) // return editable input + function _rename(event, cancel) { + const renameTo = text.text() + node.removeClass('w2ui-editing') + text.removeClass('w2ui-focus') + .css('pointer-events', 'none') + .removeAttr('contenteditable') + .off('.node-editing') + // send event if it was not cancelled + if (!cancel && self.last.renaming && original !== renameTo) { + const edata = self.trigger('rename', { target: id, text_previous: original, text_new: renameTo, originalEvent: event }) + if (edata.isCancelled === true) { + text.text(original) + self.last.renaming = false + self.focus() + return + } + self.set(id, { text: renameTo }) + edata.finish() + } + if (cancel) { + self.set(id, { text: original }) + } + self.last.renaming = false + self.focus() + } + } + contextMenu(id, event) { + const nd = this.get(id) + if (Array.isArray(this.selected)) { + if (!this.selected.includes(id)) this.click(id) + } else { + if (id != this.selected) this.click(id) + } + // event before + const edata = this.trigger('contextMenu', { target: id, originalEvent: event, object: nd, allowOnDisabled: false }) + if (edata.isCancelled === true) return + // default action + if (nd.disabled && !edata.allowOnDisabled) return + if (this.menu.length > 0) { + w2menu.hide(this.name + '_menu') // hide previous if any needed when other item's menu is shown + w2menu.show({ + name: this.name + '_menu', + anchor: document.body, + contextMenu: true, + items: this.menu, + originalEvent: event + }) + .select(evt => { + this.menuClick(id, evt.detail) + }) + } + // prevent default context menu + if (event.preventDefault) event.preventDefault() + // event after + edata.finish() + } + menuClick(itemId, detail = {}) { + // event before + const edata = this.trigger('menuClick', { target: itemId, ...detail }) + if (edata.isCancelled === true) return + // default action + // -- empty + // event after + edata.finish() + } + goFlat() { + // event before + const edata = this.trigger('flat', { goFlat: !this.flat }) + if (edata.isCancelled === true) return + // default action + this.flat = !this.flat + this.refresh() + if (this.flat) { + // collapse all unless it is a group + this.nodes.forEach(node => { + if (!node.group) { + this.collapse(node.id) + this.collapseAll(node.id) // sub items too + } + }) + this.unselect() // unselects all + } else { + // expand all unless it is a group + this.nodes.forEach(node => { + if (!node.group) { + this.expand(node.id) + this.expandAll(node.id) // sub items too + } + }) + } + // event after + edata.finish() + } + render(box) { + const time = Date.now() + const obj = this + if (typeof box === 'string') box = query(box).get(0) + // event before + const edata = this.trigger('render', { target: this.name, box: box ?? this.box }) + if (edata.isCancelled === true) return + // default action + if (box != null) { + this.unmount() // clean previous control + this.box = box + } + if (!this.box) return + query(this.box) + .attr('name', this.name) + .addClass('w2ui-reset w2ui-sidebar') + .html(`
+
+ +
+
+
`) + const rect = query(this.box).get(0).getBoundingClientRect() + query(this.box).find(':scope > div').css({ + width: rect.width + 'px', + height: rect.height + 'px' + }) + query(this.box).get(0).style.cssText += this.style + // focus + let kbd_timer + query(this.box).find('#sidebar_' + this.name + '_focus') + .on('focus', function(event) { + clearTimeout(kbd_timer) + if (!obj.hasFocus) obj.focus(event) + }) + .on('blur', function(event) { + kbd_timer = setTimeout(() => { + if (obj.hasFocus) { obj.blur(event) } + }, 100) + }) + .on('keydown', function(event) { + // do not cancel tab key (keyCode=9) so that event is dispatched to self + w2ui[obj.name].keydown.call(w2ui[obj.name], event) + }) + query(this.box).off('mousedown') + .on('mousedown', function(event) { + // set focus to sidebar + setTimeout(() => { + // if input then do not focus + if (['INPUT', 'TEXTAREA', 'SELECT'].indexOf(event.target.tagName.toUpperCase()) == -1) { + const $input = query(obj.box).find('#sidebar_' + obj.name + '_focus') + if (document.activeElement != $input.get(0) && $input.length > 0) { + $input.get(0).focus() + } + } + }, 1) + }) + /** + * FlatHTML is always present and in .refresh() it is just refreshed. However topHTML and buttomHTML should be here + * because it should never be refreshed, as it could create recursive refresh loop + */ + const flatHTML = `
` + if (this.topHTML !== '' || flatHTML !== '') { + query(this.box).find('.w2ui-sidebar-top').html(this.topHTML + flatHTML) + query(this.box).find('.w2ui-sidebar-body') + .css('top', query(this.box).find('.w2ui-sidebar-top').get(0)?.clientHeight + 'px') + query(this.box).find('.w2ui-flat') + .off('click') + .on('click', event => { this.goFlat() }) + } + if (this.bottomHTML !== '') { + query(this.box).find('.w2ui-sidebar-bottom').html(this.bottomHTML) + query(this.box).find('.w2ui-sidebar-body') + .css('bottom', query(this.box).find('.w2ui-sidebar-bottom').get(0)?.clientHeight + 'px') + } + // observe div resize + this.last.observeResize = new ResizeObserver(() => { this.resize() }) + this.last.observeResize.observe(this.box) + // event after + edata.finish() + // --- + this.refresh() + return Date.now() - time + } + update(id, options = {}) { + // quick function to refresh just this item (not sub nodes) + // - icon, class, style, text, count + const nd = this.get(id) + let level + if (nd) { + const $el = query(this.box).find('#node_' + w2utils.escapeId(nd.id)) + if (nd.group) { + if (options.text) { + nd.text = options.text + $el.find('.w2ui-group-text').replace(typeof nd.text === 'function' + ? nd.text.call(this, nd) + : '' + nd.text + '') + delete options.text + } + if (options.class) { + nd.class = options.class + level = $el.data('level') + $el.get(0).className = 'w2ui-node-group w2ui-level-' + level + (nd.class ? ' ' + nd.class : '') + delete options.class + } + if (options.style) { + nd.style = options.style + $el.get(0).nextElementSibling.style = nd.style + ';' + (!nd.hidden && nd.expanded ? '' : 'display: none;') + delete options.style + } + } else { + if (options.icon) { + const $icon = $el.find('.w2ui-node-image > span') + if ($icon.length > 0) { + nd.icon = options.icon + $icon[0].className = (typeof nd.icon === 'function' ? nd.icon.call(this, nd) : nd.icon) + delete options.icon + } + } + if (options.count != null) { + nd.count = options.count + // update counts + let txt = nd.count ?? this.badge.text + const style = this.badge?.style + const last = this.last.badge[nd.id] + if (typeof txt === 'function') txt = txt.call(this, nd, level) + $el.find('.w2ui-node-badge') + .html(txt) + .attr('style', `${style}; ${last?.style ?? ''}`) + if ($el.find('.w2ui-node-badge').length > 0) delete options.count + } + if (options.class && $el.length > 0) { + nd.class = options.class + level = $el.data('level') + $el[0].className = 'w2ui-node w2ui-level-' + level + (nd.selected ? ' w2ui-selected' : '') + (nd.disabled ? ' w2ui-disabled' : '') + (nd.class ? ' ' + nd.class : '') + delete options.class + } + if (options.text != null) { + nd.text = options.text + $el.find('.w2ui-node-text').html(typeof nd.text === 'function' ? nd.text.call(this, nd) : nd.text) + delete options.text + } + if (options.style && $el.length > 0) { + const $txt = $el.find('.w2ui-node-text') + nd.style = options.style + $txt[0].style = nd.style + delete options.style + } + } + } + // return what was not set + return options + } + refresh(id, options = {}) { + if (this.box == null) return + const body = query(this.box).find(':scope > div > .w2ui-sidebar-body').get(0) + const { scrollTop, scrollLeft } = body ?? {} + const time = Date.now() + const self = this + // event before + const edata = this.trigger('refresh', { + target: (id != null ? id : this.name), + nodeId: (id != null ? id : null), + fullRefresh: (id == null) + }) + if (edata.isCancelled === true) return + if (this.flatButton == true) { + query(this.box).find('.w2ui-sidebar-top .w2ui-flat').show() + .removeClass('w2ui-flat-left w2ui-flat-right') + .addClass(` w2ui-flat-${(this.flat ? 'right' : 'left')}`) + } else { + query(this.box).find('.w2ui-sidebar-top .w2ui-flat').hide() + } + // default action + query(this.box).find(':scope > div').removeClass('w2ui-sidebar-flat').addClass(this.flat ? 'w2ui-sidebar-flat' : '').css({ + width: query(this.box).get(0)?.clientWidth + 'px', + height: query(this.box).get(0)?.clientHeight + 'px' + }) + // if no parent - reset nodes + if (this.nodes.length > 0 && this.nodes[0].parent == null) { + const tmp = this.nodes + this.nodes = [] + this.add(this, tmp) + } + const obj = this + let node + let nodeSubId + if (id == null) { + node = this + nodeSubId = '.w2ui-sidebar-body' + } else { + node = this.get(id) + if (node == null) return + nodeSubId = '#node_' + w2utils.escapeId(node.id) + '_sub' + } + const nodeId = '#node_' + w2utils.escapeId(node.id) + let nodeHTML + if (node !== this) { + nodeHTML = getNodeHTML(node) + query(this.box).find(nodeId).before('') + query(this.box).find(nodeId).remove() + query(this.box).find(nodeSubId).remove() + query(this.box).find('#sidebar_' + this.name + '_tmp').before(nodeHTML) + query(this.box).find('#sidebar_' + this.name + '_tmp').remove() + } + // remember scroll position + const div = query(this.box).find(':scope > div').get(0) + const scroll = { + top: div?.scrollTop, + left: div?.scrollLeft + } + // refresh sub nodes + query(this.box).find(nodeSubId).html('') + for (let i = 0; i < node.nodes.length; i++) { + const subNode = node.nodes[i] + nodeHTML = getNodeHTML(subNode) + query(this.box).find(nodeSubId).append(nodeHTML) + if (subNode.nodes.length !== 0) { + // TODO: here + this.refresh(subNode.id, { recursive: true }) + } else { + // trigger event + const edata2 = this.trigger('refresh', { target: subNode.id }) + if (edata2.isCancelled === true) return + // event after + edata2.finish() + } + } + // reset scroll + if (div) { + div.scrollTop = scroll.top + div.scrollLeft = scroll.left + } + // bind events + if (!options.recursive) { + const els = query(this.box).find(`${nodeId}, ${nodeId} .w2ui-eaction, ${nodeSubId} .w2ui-eaction`) + w2utils.bindEvents(els, this) + // restore scroll position + query(body).prop({ scrollLeft, scrollTop }) + } + // event after + edata.finish() + return Date.now() - time + function getNodeHTML(nd) { + let html = '' + let icon = nd.icon + if (icon == null) icon = obj.icon + // -- find out level + let tmp = nd.parent + let level = 0 + while (tmp && tmp.parent != null) { + // if (tmp.group) level--; + tmp = tmp.parent + level++ + } + if (nd.caption != null && nd.text == null) nd.text = nd.caption + if (nd.caption != null) { + console.log('NOTICE: sidebar node.caption property is deprecated, please use node.text. Node -> ', nd) + nd.text = nd.caption + } + if (Array.isArray(nd.nodes) && nd.nodes.length > 0) nd.collapsible = true + if (nd.group) { + let text = w2utils.lang(typeof nd.text === 'function' ? nd.text.call(obj, nd, level) : nd.text) + if (String(text).substr(0, 5) != '${text}` + } + html = ` +
+ ${nd.groupShowHide && nd.collapsible + ? `${!nd.hidden && nd.expanded ? w2utils.lang('Hide') : w2utils.lang('Show')}` + : '' +} ${text} +
+
+
` + if (obj.flat) { + html = ` +
 
+
` + } + } else { + if (nd.selected && !nd.disabled) { + if (obj.multi) { + obj.selected ??= [] + if (!obj.selected.includes(nd.id)) { + obj.selected.push(nd.id) + } + } else { + obj.selected = nd.id + } + } + // icon or image + let image = '' + if (icon) { + if (icon instanceof Object) { + const text = (typeof icon.text === 'function' ? (icon.text.call(obj, nd, level) ?? '') : icon.text) + image = ` +
+ ${text} +
+ ` + } else { + image = ` +
+ +
` + } + } + let expand = '' + let counts = '' + if (self.badge != null || nd.count != null) { + let txt = nd.count ?? self.badge?.text + const style = self.badge?.style + const last = obj.last.badge[nd.id] + if (typeof txt === 'function') txt = txt.call(self, nd, level) + if (txt || txt === 0) { // can be number 0 + counts = ` +
+ ${txt} +
` + } + } + // array with classes + const classes = ['w2ui-node', `w2ui-level-${level}`, 'w2ui-eaction'] + if (nd.selected) classes.push('w2ui-selected') + if (nd.disabled) classes.push('w2ui-disabled') + if (nd.class) classes.push(nd.class) + // collapsible + if (nd.collapsible === true) { + const toggleClasses = ['w2ui-sb-toggle', 'w2ui-eaction', (nd.expanded ? 'w2ui-expanded' : 'w2ui-collapsed')] + if (self.toggleAlign == 'left') toggleClasses.push('w2ui-left-toggle') + expand = `
` + classes.push('w2ui-has-children') + } + const text = w2utils.lang(typeof nd.text === 'function' ? nd.text.call(obj, nd, level) : nd.text) + let nodeOffset = nd.parent?.childOffset ?? 0 + if (level === 0 && nd.collapsible === true && self.toggleAlign == 'left') { + nodeOffset += 12 + } + html = ` +
+ ${obj.handle.text + ? `
+ ${typeof obj.handle.text === 'function' ? obj.handle.text.call(obj, nd, level) ?? '' : obj.handle.text} +
` + : '' +} +
+ ${expand} ${image} ${counts} +
${text}
+
+
+
` + if (obj.flat) { + html = ` +
+
${image}
+
+
` + } + } + return html + } + } + mouseAction(action, anchor, nodeId, event, type) { + let edata + const node = this.get(nodeId) + if (type == null) { + edata = this.trigger('mouse' + action, { target: node.id, node, originalEvent: event }) + } + if (type == 'tooltip') { + // this tooltip shows for flat sidebars + const text = w2utils.lang(typeof node.text === 'function' ? node.text.call(this, node) : node.text) + let tooltip = text + (node.count != null + ? ' - ' + node.count + '' + : '') + if (action == 'Leave' || this.selected == node.id) tooltip = '' + this.tooltip(anchor, tooltip) + } + if (type == 'handle') { + if (action == 'click') { + const onClick = this.handle.onClick + if (typeof onClick === 'function') { + onClick.call(this, node, event) + } + } else { + let tooltip = this.handle.tooltip + if (typeof tooltip === 'function') { + tooltip = tooltip.call(this, node, event) + } + if (action == 'Leave') tooltip = '' + this.otherTooltip(anchor, tooltip) + } + } + if (type == 'icon') { + if (action == 'click') { + const onClick = this.icon.onClick + if (typeof onClick === 'function') { + onClick.call(this, node, event) + } + } else { + let tooltip = this.icon.tooltip + if (typeof tooltip === 'function') { + tooltip = tooltip.call(this, node, event) + } + if (action == 'Leave') tooltip = '' + this.otherTooltip(anchor, tooltip) + } + } + if (type == 'badge') { + if (action == 'click') { + const onClick = this.badge?.onClick + if (typeof onClick === 'function') { + onClick.call(this, node, event) + } + } else { + let tooltip = this.badge?.tooltip + if (typeof tooltip === 'function') { + tooltip = tooltip.call(this, node, event) + } + if (action == 'Leave') tooltip = '' + this.otherTooltip(anchor, tooltip) + } + } + edata?.finish() + } + tooltip(el, text) { + const $el = query(el).find('.w2ui-node-data') + if (text !== '') { + w2tooltip.show({ + anchor: $el.get(0), + name: this.name + '_tooltip', + html: text, + position: 'right|left' + }) + } else { + w2tooltip.hide(this.name + '_tooltip') + } + } + otherTooltip(el, text) { + if (text !== '') { + w2tooltip.show({ + anchor: el, + name: this.name + '_tooltip', + html: text, + position: 'top|bottom' + }) + } else { + w2tooltip.hide(this.name + '_tooltip') + } + } + showPlus(el, color) { + query(el).find('span:nth-child(1)').css('color', color) + } + resize() { + const time = Date.now() + // event before + const edata = this.trigger('resize', { target: this.name }) + if (edata.isCancelled === true) return + // default action + if (this.box != null) { + const rect = query(this.box).get(0).getBoundingClientRect() + query(this.box).css('overflow', 'hidden') // container should have no overflow + query(this.box).find(':scope > div').css({ + width: rect.width + 'px', + height: rect.height + 'px' + }) + } + // event after + edata.finish() + return Date.now() - time + } + destroy() { + // event before + const edata = this.trigger('destroy', { target: this.name }) + if (edata.isCancelled === true) return + // clean up + if (query(this.box).find('.w2ui-sidebar-body').length > 0) { + this.unmount() + } + delete w2ui[this.name] + // event after + edata.finish() + } + unmount() { + super.unmount() + this.last.observeResize?.disconnect() + } + lock(msg, showSpinner) { + const args = Array.from(arguments) + args.unshift(this.box) + w2utils.lock(...args) + } + unlock(speed) { + w2utils.unlock(this.box, speed) + } +} +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base, w2tooltip + * + * == 2.0 changes + * - CSP - fixed inline events + * - removed jQuery dependency + * - observeResize for the box + * - refactored w2events + * - scrollIntoView - removed callback + * - scroll, scrollIntoView return promise + * - animateInsert, animateClose - returns a promise + * - add, insert return a promise + * - onMouseEnter, onMouseLeave, onMouseDown, onMouseUp + */ + +class w2tabs extends w2base { + constructor(options) { + super(options.name) + this.box = null // DOM Element that holds the element + this.name = null // unique name for w2ui + this.active = null + this.reorder = false + this.flow = 'down' // can be down or up + this.tooltip = 'top|left' // can be top, bottom, left, right + this.tabs = [] + this.routeData = {} // data for dynamic routes + this.last = {} // placeholder for internal variables + this.right = '' + this.style = '' + this.onClick = null + this.onMouseEnter = null // mouse enter and lease + this.onMouseLeave = null + this.onMouseDown = null + this.onMouseUp = null + this.onClose = null + this.onRender = null + this.onRefresh = null + this.onResize = null + this.onDestroy = null + this.tab_template = { + id: null, + text: null, + icon: null, + route: null, + hidden: false, + disabled: false, + closable: false, + tooltip: null, + style: '', + onClick: null, + onRefresh: null, + onClose: null + } + const tabs = options.tabs + delete options.tabs + // mix in options + Object.assign(this, options) + // add item via method to makes sure item_template is applied + if (Array.isArray(tabs)) this.add(tabs) + // need to reassign back to keep it in config + options.tabs = tabs + // render if box specified + if (typeof this.box === 'string') this.box = query(this.box).get(0) + if (this.box) this.render(this.box) + } + add(tab) { + return this.insert(null, tab) + } + insert(id, tabs) { + if (!Array.isArray(tabs)) tabs = [tabs] + // assume it is array + const proms = [] + tabs.forEach(tab => { + // checks + if (tab.id == null) { + console.log(`ERROR: The parameter "id" is required but not supplied. (obj: ${this.name})`) + return + } + if (!w2utils.checkUniqueId(tab.id, this.tabs, 'tabs', this.name)) return + // add tab + const it = Object.assign({}, this.tab_template, tab) + if (id == null) { + this.tabs.push(it) + proms.push(this.animateInsert(null, it)) + } else { + const middle = this.get(id, true) + const before = this.tabs[middle].id + this.tabs.splice(middle, 0, it) + proms.push(this.animateInsert(before, it)) + } + }) + return Promise.all(proms) + } + remove() { + let effected = 0 + Array.from(arguments).forEach(it => { + const tab = this.get(it) + if (!tab) return + effected++ + // remove from array + this.tabs.splice(this.get(tab.id, true), 1) + // remove from screen + query(this.box).find(`#tabs_${this.name}_tab_${w2utils.escapeId(tab.id)}`).remove() + }) + this.resize() + return effected + } + select(id) { + if (this.active == id || this.get(id) == null) return false + this.active = id + this.refresh() + return true + } + set(id, tab) { + const index = this.get(id, true) + if (index == null) return false + w2utils.extend(this.tabs[index], tab) + this.refresh(id) + return true + } + get(id, returnIndex) { + if (arguments.length === 0) { + const all = [] + for (let i1 = 0; i1 < this.tabs.length; i1++) { + if (this.tabs[i1].id != null) { + all.push(this.tabs[i1].id) + } + } + return all + } else { + for (let i2 = 0; i2 < this.tabs.length; i2++) { + if (this.tabs[i2].id == id) { // need to be == since id can be numeric + return (returnIndex === true ? i2 : this.tabs[i2]) + } + } + } + return null + } + show() { + const effected = [] + Array.from(arguments).forEach(it => { + const tab = this.get(it) + if (!tab || tab.hidden === false) return + tab.hidden = false + effected.push(tab.id) + }) + setTimeout(() => { effected.forEach(it => { this.refresh(it); this.resize() }) }, 15) // needs timeout + return effected + } + hide() { + const effected = [] + Array.from(arguments).forEach(it => { + const tab = this.get(it) + if (!tab || tab.hidden === true) return + tab.hidden = true + effected.push(tab.id) + }) + setTimeout(() => { effected.forEach(it => { this.refresh(it); this.resize() }) }, 15) // needs timeout + return effected + } + enable() { + const effected = [] + Array.from(arguments).forEach(it => { + const tab = this.get(it) + if (!tab || tab.disabled === false) return + tab.disabled = false + effected.push(tab.id) + }) + setTimeout(() => { effected.forEach(it => { this.refresh(it) }) }, 15) // needs timeout + return effected + } + disable() { + const effected = [] + Array.from(arguments).forEach(it => { + const tab = this.get(it) + if (!tab || tab.disabled === true) return + tab.disabled = true + effected.push(tab.id) + }) + setTimeout(() => { effected.forEach(it => { this.refresh(it) }) }, 15) // needs timeout + return effected + } + dragMove(event) { + if (!this.last.reordering) return + const self = this + const info = this.last.moving + const tab = this.tabs[info.index] + const next = _find(info.index, 1) + const prev = _find(info.index, -1) + const $el = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(tab.id)) + if (info.divX > 0 && next) { + const $nextEl = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(next.id)) + let width1 = parseInt($el.get(0).clientWidth) + let width2 = parseInt($nextEl.get(0).clientWidth) + if (width1 < width2) { + width1 = Math.floor(width1 / 3) + width2 = width2 - width1 + } else { + width1 = Math.floor(width2 / 3) + width2 = width2 - width1 + } + if (info.divX > width2) { + const index = this.tabs.indexOf(next) + this.tabs.splice(info.index, 0, this.tabs.splice(index, 1)[0]) // reorder in the array + info.$tab.before($nextEl.get(0)) + info.$tab.css('opacity', 0) + Object.assign(this.last.moving, { + index: index, + divX: -width1, + x: event.pageX + width1, + left: info.left + info.divX + width1 + }) + return + } + } + if (info.divX < 0 && prev) { + const $prevEl = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(prev.id)) + let width1 = parseInt($el.get(0).clientWidth) + let width2 = parseInt($prevEl.get(0).clientWidth) + if (width1 < width2) { + width1 = Math.floor(width1 / 3) + width2 = width2 - width1 + } else { + width1 = Math.floor(width2 / 3) + width2 = width2 - width1 + } + if (Math.abs(info.divX) > width2) { + const index = this.tabs.indexOf(prev) + this.tabs.splice(info.index, 0, this.tabs.splice(index, 1)[0]) // reorder in the array + $prevEl.before(info.$tab) + info.$tab.css('opacity', 0) + Object.assign(info, { + index: index, + divX: width1, + x: event.pageX - width1, + left: info.left + info.divX - width1 + }) + return + } + } + function _find(ind, inc) { + ind += inc + let tab = self.tabs[ind] + if (tab && tab.hidden) { + tab = _find(ind, inc) + } + return tab + } + } + mouseAction(action, id, event) { + const tab = this.get(id) + const edata = this.trigger('mouse' + action, { target: id, tab, object: tab, originalEvent: event }) + if (edata.isCancelled === true || tab?.disabled || tab?.hidden) return + switch (action) { + case 'Enter': + this.tooltipShow(id) + break + case 'Leave': + this.tooltipHide(id) + break + case 'Down': + this.initReorder(id, event) + break + case 'Up': + break + } + edata.finish() + } + tooltipShow(id) { + const tab = this.get(id) + const el = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(id)).get(0) + if (this.tooltip == null || tab?.disabled || this.last.reordering) { + return + } + const pos = this.tooltip + let txt = tab?.tooltip + if (typeof txt === 'function') txt = txt.call(this, tab) + w2tooltip.show({ + anchor: el, + name: this.name + '_tooltip', + html: txt, + position: pos + }) + } + tooltipHide(id) { + if (this.tooltip == null) return + w2tooltip.hide(this.name + '_tooltip') + } + getTabHTML(id) { + const index = this.get(id, true) + const tab = this.tabs[index] + if (tab == null) return false + if (tab.text == null && tab.caption != null) tab.text = tab.caption + if (tab.tooltip == null && tab.hint != null) tab.tooltip = tab.hint // for backward compatibility + if (tab.caption != null) { + console.log('NOTICE: tabs tab.caption property is deprecated, please use tab.text. Tab -> ', tab) + } + if (tab.hint != null) { + console.log('NOTICE: tabs tab.hint property is deprecated, please use tab.tooltip. Tab -> ', tab) + } + let text = tab.text + if (typeof text === 'function') text = text.call(this, tab) + if (text == null) text = '' + let closable = '' + let addStyle = '' + if (tab.hidden) { addStyle += 'display: none;' } + if (tab.disabled) { addStyle += 'opacity: 0.2;' } + if (tab.closable && !tab.disabled) { + closable = `
+
` + } + let icon = '' + if (tab.icon) { + icon = `` + } + return ` +
+ ${icon + w2utils.lang(text) + closable} +
` + } + refresh(id) { + const time = Date.now() + if (this.flow == 'up') { + query(this.box).addClass('w2ui-tabs-up') + } else { + query(this.box).removeClass('w2ui-tabs-up') + } + // event before + const edata = this.trigger('refresh', { target: (id != null ? id : this.name), object: this.get(id) }) + if (edata.isCancelled === true) return + if (id == null) { + // refresh all + for (let i = 0; i < this.tabs.length; i++) { + this.refresh(this.tabs[i].id) + } + } else { + // create or refresh only one item + const selector = '#tabs_' + this.name + '_tab_' + w2utils.escapeId(id) + const $tab = query(this.box).find(selector) + const tabHTML = this.getTabHTML(id) + if ($tab.length === 0) { + query(this.box).find('#tabs_' + this.name + '_right').before(tabHTML) + } else { + if (query(this.box).find('.tab-animate-insert').length == 0) { + $tab.replace(tabHTML) + } + } + w2utils.bindEvents(query(this.box).find(`${selector}, ${selector} .w2ui-eaction`), this) + } + // right html + query(this.box).find('#tabs_' + this.name + '_right').html(this.right) + // event after + edata.finish() + // this.resize(); + return Date.now() - time + } + render(box) { + const time = Date.now() + if (typeof box === 'string') box = query(box).get(0) + // event before + const edata = this.trigger('render', { target: this.name, box: box ?? this.box }) + if (edata.isCancelled === true) return + // default action + if (box != null) { + this.unmount() // clean previous control + this.box = box + } + if (!this.box) return false + // render all buttons + const html = ` + +
+
${this.right}
+
+
+
` + query(this.box) + .attr('name', this.name) + .addClass('w2ui-reset w2ui-tabs') + .html(html) + if (query(this.box).length > 0) { + query(this.box)[0].style.cssText += this.style + } + w2utils.bindEvents(query(this.box).find('.w2ui-eaction'), this) + // observe div resize + this.last.observeResize = new ResizeObserver(() => { this.resize() }) + this.last.observeResize.observe(this.box) + // event after + edata.finish() + this.refresh() + this.resize() + return Date.now() - time + } + initReorder(id, event) { + if (!this.reorder) return + const self = this + const $tab = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(id)) + const tabIndex = this.get(id, true) + const $ghost = query($tab.get(0).cloneNode(true)) + let edata + $ghost.attr('id', '#tabs_' + this.name + '_tab_ghost') + this.last.moving = { + index: tabIndex, + indexFrom: tabIndex, + $tab: $tab, + $ghost: $ghost, + divX: 0, + left: $tab.get(0).getBoundingClientRect().left, + parentX: query(this.box).get(0).getBoundingClientRect().left, + x: event.pageX, + opacity: $tab.css('opacity') + } + query(document) + .off('.w2uiTabReorder') + .on('mousemove.w2uiTabReorder', function(event) { + if (!self.last.reordering) { + // event before + edata = self.trigger('reorder', { target: self.tabs[tabIndex].id, indexFrom: tabIndex, tab: self.tabs[tabIndex] }) + if (edata.isCancelled === true) return + w2tooltip.hide(this.name + '_tooltip') + self.last.reordering = true + $ghost.addClass('moving') + $ghost.css({ + 'pointer-events': 'none', + 'position': 'absolute', + 'left': $tab.get(0).getBoundingClientRect().left + }) + $tab.css('opacity', 0) + query(self.box).find('.w2ui-scroll-wrapper').append($ghost.get(0)) + query(self.box).find('.w2ui-tab-close').hide() + } + self.last.moving.divX = event.pageX - self.last.moving.x + $ghost.css('left', (self.last.moving.left - self.last.moving.parentX + self.last.moving.divX) + 'px') + self.dragMove(event) + }) + .on('mouseup.w2uiTabReorder', function() { + query(document).off('.w2uiTabReorder') + $ghost.css({ + 'transition': '0.1s', + 'left': self.last.moving.$tab.get(0).getBoundingClientRect().left - self.last.moving.parentX + }) + query(self.box).find('.w2ui-tab-close').show() + $ghost.remove() + $tab.css({ opacity: self.last.moving.opacity }) + // self.render() + if (self.last.reordering) { + edata.finish({ indexTo: self.last.moving.index }) + } + self.last.reordering = false + }) + } + scroll(direction, instant) { + return new Promise((resolve, reject) => { + const scrollBox = query(this.box).find('.w2ui-scroll-wrapper') + const scrollLeft = scrollBox.get(0).scrollLeft + const right = scrollBox.find('.w2ui-tabs-right').get(0) + const width1 = scrollBox.parent().get(0).getBoundingClientRect().width + const width2 = scrollLeft + parseInt(right.offsetLeft) + parseInt(right.clientWidth) + switch (direction) { + case 'left': { + let scroll = scrollLeft - width1 + 50 // 35 is width of both button + if (scroll <= 0) scroll = 0 + scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' }) + break + } + case 'right': { + let scroll = scrollLeft + width1 - 50 // 35 is width of both button + if (scroll >= width2 - width1) scroll = width2 - width1 + scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' }) + break + } + } + setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 350) + }) + } + scrollIntoView(id, instant) { + return new Promise((resolve, reject) => { + if (id == null) id = this.active + const tab = this.get(id) + if (tab == null) return + const tabEl = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(id)).get(0) + tabEl.scrollIntoView({ block: 'start', inline: 'center', behavior: instant ? 'atuo' : 'smooth' }) + setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 500) + }) + } + resize() { + const time = Date.now() + if (this.box == null) return + // event before + const edata = this.trigger('resize', { target: this.name }) + if (edata.isCancelled === true) return + // show hide overflow buttons + if (this.box != null) { + const box = query(this.box) + box.find('.w2ui-scroll-left, .w2ui-scroll-right').hide() + const scrollBox = box.find('.w2ui-scroll-wrapper').get(0) + const $right = box.find('.w2ui-tabs-right') + const boxWidth = box.get(0).getBoundingClientRect().width + const itemsWidth = ($right.length > 0 ? $right[0].offsetLeft + $right[0].clientWidth : 0) + if (boxWidth < itemsWidth) { + // we have overflown content + if (scrollBox.scrollLeft > 0) { + box.find('.w2ui-scroll-left').show() + } + if (boxWidth < itemsWidth - scrollBox.scrollLeft) { + box.find('.w2ui-scroll-right').show() + } + } + } + // event after + edata.finish() + return Date.now() - time + } + destroy() { + // event before + const edata = this.trigger('destroy', { target: this.name }) + if (edata.isCancelled === true) return + // clean up + if (query(this.box).find('#tabs_' + this.name + '_right').length > 0) { + this.unmount() + } + delete w2ui[this.name] + // event after + edata.finish() + } + unmount() { + super.unmount() + this.last.observeResize?.disconnect() + } + // =================================================== + // -- Internal Event Handlers + click(id, event) { + if (event && query(event.target).hasClass('w2ui-tab-close')) { + // do not consider click on close button as tab click + return + } + const tab = this.get(id) + if (tab == null || tab.disabled || this.last.reordering) return false + // event before + const edata = this.trigger('click', { target: id, tab: tab, object: tab, originalEvent: event }) + if (edata.isCancelled === true) return + // default action + query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(this.active)).removeClass('active') + this.active = tab.id + query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(this.active)).addClass('active') + // route processing + if (typeof tab.route === 'string') { + let route = tab.route !== '' ? String('/' + tab.route).replace(/\/{2,}/g, '/') : '' + const info = w2utils.parseRoute(route) + if (info.keys.length > 0) { + for (let k = 0; k < info.keys.length; k++) { + if (this.routeData[info.keys[k].name] == null) continue + route = route.replace((new RegExp(':' + info.keys[k].name, 'g')), this.routeData[info.keys[k].name]) + } + } + setTimeout(() => { window.location.hash = route }, 1) + } + // event after + edata.finish() + } + clickClose(id, event) { + const tab = this.get(id) + if (tab == null || tab.disabled) return false + // event before + const edata = this.trigger('close', { target: id, object: tab, tab, originalEvent: event }) + if (edata.isCancelled === true) return + this.animateClose(id).then(() => { + this.remove(id) + edata.finish() + this.refresh() + }) + event?.stopPropagation() + } + animateClose(id) { + return new Promise((resolve, reject) => { + const $tab = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(id)) + const width = parseInt($tab.get(0).clientWidth || 0) + const anim = `
` + const $anim = $tab.replace(anim) + setTimeout(() => { $anim.css({ width: '0px' }) }, 1) + setTimeout(() => { + $anim.remove() + this.resize() + resolve() + }, 500) + }) + } + animateInsert(id, tab) { + return new Promise((resolve, reject) => { + let $before = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(id)) + const $tab = query.html(this.getTabHTML(tab.id)) + if ($before.length == 0) { + $before = query(this.box).find('#tabs_tabs_right') + $before.before($tab) + this.resize() + } else { + $tab.css({ opacity: 0 }) + // first insert tab on the right to get its proper dimentions + query(this.box).find('#tabs_tabs_right').before($tab.get(0)) + const $tmp = query(this.box).find('#' + $tab.attr('id')) + const width = $tmp.get(0)?.clientWidth ?? 0 + // insert animation div + const $anim = query.html('
') + $before.before($anim) + // hide tab and move it in the right position + $tab.hide() + $anim.before($tab[0]) + setTimeout(() => { $anim.css({ width: width + 'px' }) }, 1) + setTimeout(() => { + $anim.remove() + $tab.css({ opacity: 1 }).show() + this.refresh(tab.id) + this.resize() + resolve() + }, 500) + } + }) + } +} +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base, w2tabs, w2toolbar + * + * == 2.0 changes + * - CSP - fixed inline events + * - remove jQuery dependency + * - layout.confirm - refactored + * - layout.message - refactored + * - panel.removed + * - assignTabs + */ + +const w2panels = ['top', 'left', 'main', 'preview', 'right', 'bottom'] +class w2layout extends w2base { + constructor(options) { + super(options.name) + this.box = null // DOM Element that holds the element + this.name = null // unique name for w2ui + this.panels = [] + this.last = {} + this.padding = 1 // panel padding + this.resizer = 4 // resizer width or height + this.style = '' + this.onShow = null + this.onHide = null + this.onResizing = null + this.onResizerClick = null + this.onRender = null + this.onRefresh = null + this.onChange = null + this.onResize = null + this.onDestroy = null + this.panel_template = { + type: null, // left, right, top, bottom + title: '', + size: 100, // width or height depending on panel name + minSize: 20, + maxSize: false, + hidden: false, + resizable: false, + overflow: 'auto', + style: '', + html: '', // can be String or Object with .render(box) method + tabs: null, + toolbar: null, + width: null, // read only + height: null, // read only + show: { + toolbar: false, + tabs: false + }, + removed: null, // function to call when content is overwritten + onRefresh: null, + onShow: null, + onHide: null + } + // mix in options + Object.assign(this, options) + if (!Array.isArray(this.panels)) this.panels = [] + // add defined panels + this.panels.forEach((panel, ind) => { + this.panels[ind] = w2utils.extend({}, this.panel_template, panel) + if (w2utils.isPlainObject(panel.tabs) || Array.isArray(panel.tabs)) initTabs(this, panel.type) + if (w2utils.isPlainObject(panel.toolbar) || Array.isArray(panel.toolbar)) initToolbar(this, panel.type) + }) + // add all other panels + w2panels.forEach(tab => { + if (this.get(tab) != null) return + this.panels.push(w2utils.extend({}, this.panel_template, { type: tab, hidden: (tab !== 'main'), size: 50 })) + }) + // render if box specified + if (typeof this.box === 'string') this.box = query(this.box).get(0) + if (this.box) this.render(this.box) + function initTabs(object, panel, tabs) { + const pan = object.get(panel) + if (pan != null && tabs == null) tabs = pan.tabs + if (pan == null || tabs == null) return false + // instantiate tabs + if (Array.isArray(tabs)) tabs = { tabs: tabs } + const name = object.name + '_' + panel + '_tabs' + if (w2ui[name]) w2ui[name].destroy() // destroy if existed + pan.tabs = new w2tabs(w2utils.extend({}, tabs, { owner: object, name: object.name + '_' + panel + '_tabs' })) + pan.show.tabs = true + return true + } + function initToolbar(object, panel, toolbar) { + const pan = object.get(panel) + if (pan != null && toolbar == null) toolbar = pan.toolbar + if (pan == null || toolbar == null) return false + // instantiate toolbar + if (Array.isArray(toolbar)) toolbar = { items: toolbar } + const name = object.name + '_' + panel + '_toolbar' + if (w2ui[name]) w2ui[name].destroy() // destroy if existed + pan.toolbar = new w2toolbar(w2utils.extend({}, toolbar, { owner: object, name: object.name + '_' + panel + '_toolbar' })) + pan.show.toolbar = true + return true + } + } + html(panel, data, transition) { + const p = this.get(panel) + const promise = { + panel: panel, + html: p.html, + error: false, + cancelled: false, + removed(cb) { + if (typeof cb === 'function') { + p.removed = cb + } + } + } + if (typeof p.removed === 'function') { + p.removed({ panel: panel, html: p.html, html_new: data, transition: transition || 'none' }) + p.removed = null // this is one time call back only + } + // if it is CSS panel + if (panel == 'css') { + query(this.box).find('#layout_' + this.name + '_panel_css').html('') + promise.status = true + return promise + } + if (p == null) { + console.log('ERROR: incorrect panel name. Panel name can be main, left, right, top, bottom, preview or css') + promise.error = true + return promise + } + if (data == null) { + return promise + } + // event before + const edata = this.trigger('change', { target: panel, panel: p, html_new: data, transition: transition }) + if (edata.isCancelled === true) { + promise.cancelled = true + return promise + } + const pname = '#layout_' + this.name + '_panel_' + p.type + const current = query(this.box).find(pname + '> [data-role="panel-content"]') + let panelTop = 0 + if (current.length > 0) { + query(this.box).find(pname).get(0).scrollTop = 0 + panelTop = query(current).css('top') + } + // clean up previous content + if (typeof p.html.unmount === 'function') p.html.unmount() + current.addClass('w2ui-panel-content') + current.removeAttr('style') // styles could have added manually, but all necessary will be added by resizeBoxes + this.resizeBoxes(panel) + if (p.html === '') { + p.html = data + this.refresh(panel) + } else { + p.html = data + if (!p.hidden) { + if (transition != null && transition !== '') { + // apply transition + query(this.box).addClass('animating') + const div1 = query(this.box).find(pname + '> [data-role="panel-content"]') + div1.after('
') + const div2 = query(this.box).find(pname + '> [data-role="panel-content"].new-panel') + div1.css('top', panelTop) + div2.css('top', panelTop) + if (typeof data === 'object') { + data.box = div2[0] // do not do .render(box); + data.render() + } else { + div2.hide().html(data) + } + w2utils.transition(div1[0], div2[0], transition, () => { + div1.remove() + div2.removeClass('new-panel') + div2.css('overflow', p.overflow) + // make sure only one content left + query(query(this.box).find(pname + '> [data-role="panel-content"]').get(1)).remove() + query(this.box).removeClass('animating') + this.refresh(panel) + }) + } else { + this.refresh(panel) + } + } + } + // event after + edata.finish() + return promise + } + message(panel, options) { + const p = this.get(panel) + const box = query(this.box).find('#layout_' + this.name + '_panel_' + p.type) + const oldOverflow = box.css('overflow') + box.css('overflow', 'hidden') + const prom = w2utils.message({ + owner: this, + box: box.get(0), + after: '.w2ui-panel-title', + param: panel + }, options) + if (prom) { + prom.self.on('close:after', () => { + box.css('overflow', oldOverflow) + }) + } + return prom + } + confirm(panel, options) { + const p = this.get(panel) + const box = query(this.box).find('#layout_' + this.name + '_panel_' + p.type) + const oldOverflow = box.css('overflow') + box.css('overflow', 'hidden') + const prom = w2utils.confirm({ + owner: this, + box: box.get(0), + after: '.w2ui-panel-title', + param: panel + }, options) + if (prom) { + prom.self.on('close:after', () => { + box.css('overflow', oldOverflow) + }) + } + return prom + } + load(panel, url, transition) { + return new Promise((resolve, reject) => { + if ((panel == 'css' || this.get(panel) != null) && url != null) { + fetch(url) + .then(resp => resp.text()) + .then(text => { + this.resize() + resolve(this.html(panel, text, transition)) + }) + } else { + reject() + } + }) + } + sizeTo(panel, size, instant) { + const pan = this.get(panel) + if (pan == null) return false + // resize + query(this.box).find(':scope > div > .w2ui-panel') + .css('transition', (instant !== true ? '.2s' : '0s')) + setTimeout(() => { this.set(panel, { size: size }) }, 1) + // clean + setTimeout(() => { + query(this.box).find(':scope > div > .w2ui-panel').css('transition', '0s') + this.resize() + }, 300) + return true + } + show(panel, immediate) { + // event before + const edata = this.trigger('show', { target: panel, thisect: this.get(panel), immediate: immediate }) + if (edata.isCancelled === true) return + const p = this.get(panel) + if (p == null) return false + p.hidden = false + if (immediate === true) { + query(this.box).find('#layout_' + this.name + '_panel_' + panel) + .css({ 'opacity': '1' }) + edata.finish() + this.resize() + } else { + // resize + query(this.box).addClass('animating') + query(this.box).find('#layout_' + this.name + '_panel_' + panel) + .css({ 'opacity': '0' }) + query(this.box).find(':scope > div > .w2ui-panel') + .css('transition', '.2s') + setTimeout(() => { this.resize() }, 1) + // show + setTimeout(() => { + query(this.box).find('#layout_' + this.name + '_panel_' + panel).css({ 'opacity': '1' }) + }, 250) + // clean + setTimeout(() => { + query(this.box).find(':scope > div > .w2ui-panel') + .css('transition', '0s') + query(this.box).removeClass('animating') + edata.finish() + this.resize() + }, 300) + } + return true + } + hide(panel, immediate) { + // event before + const edata = this.trigger('hide', { target: panel, object: this.get(panel), immediate: immediate }) + if (edata.isCancelled === true) return + const p = this.get(panel) + if (p == null) return false + p.hidden = true + if (immediate === true) { + query(this.box).find('#layout_' + this.name + '_panel_' + panel) + .css({ 'opacity': '0' }) + edata.finish() + this.resize() + } else { + // hide + query(this.box).addClass('animating') + query(this.box).find(':scope > div > .w2ui-panel') + .css('transition', '.2s') + query(this.box).find('#layout_' + this.name + '_panel_' + panel) + .css({ 'opacity': '0' }) + setTimeout(() => { this.resize() }, 1) + // clean + setTimeout(() => { + query(this.box).find(':scope > div > .w2ui-panel') + .css('transition', '0s') + query(this.box).removeClass('animating') + edata.finish() + this.resize() + }, 300) + } + return true + } + toggle(panel, immediate) { + const p = this.get(panel) + if (p == null) return false + if (p.hidden) return this.show(panel, immediate); else return this.hide(panel, immediate) + } + set(panel, options) { + const ind = this.get(panel, true) + if (ind == null) return false + w2utils.extend(this.panels[ind], options) + // refresh only when content changed + if (options.html != null || options.resizable != null) { + this.refresh(panel) + } + // show/hide resizer + this.resize() // resize is needed when panel size is changed + return true + } + get(panel, returnIndex) { + for (let p = 0; p < this.panels.length; p++) { + if (this.panels[p].type == panel) { + if (returnIndex === true) return p; else return this.panels[p] + } + } + return null + } + el(panel) { + const el = query(this.box).find('#layout_' + this.name + '_panel_' + panel + '> [data-role="panel-content"]') + if (el.length != 1) return null + return el[0] + } + hideToolbar(panel) { + const pan = this.get(panel) + if (!pan) return + pan.show.toolbar = false + query(this.box).find(`#layout_${this.name}_panel_${panel} > [data-role="panel-toolbar"]`).hide() + this.resize() + } + showToolbar(panel) { + const pan = this.get(panel) + if (!pan) return + pan.show.toolbar = true + query(this.box).find(`#layout_${this.name}_panel_${panel} > [data-role="panel-toolbar"]`).show() + this.resize() + } + toggleToolbar(panel) { + const pan = this.get(panel) + if (!pan) return + if (pan.show.toolbar) this.hideToolbar(panel); else this.showToolbar(panel) + } + assignToolbar(panel, toolbar) { + if (typeof toolbar === 'string' && w2ui[toolbar] != null) toolbar = w2ui[toolbar] + const pan = this.get(panel) + pan.toolbar = toolbar + const tmp = query(this.box).find(panel + '> [data-role="panel-toolbar"]') + if (pan.toolbar != null) { + if (tmp.attr('name') != pan.toolbar.name) { + pan.toolbar.render(tmp.get(0)) + } else if (pan.toolbar != null) { + pan.toolbar.refresh() + } + toolbar.owner = this + this.showToolbar(panel) + this.refresh(panel) + } else { + tmp.html('') + this.hideToolbar(panel) + } + } + hideTabs(panel) { + const pan = this.get(panel) + if (!pan) return + pan.show.tabs = false + query(this.box).find('#layout_' + this.name + '_panel_' + panel + '> [data-role="panel-tabs"]').hide() + this.resize() + } + showTabs(panel) { + const pan = this.get(panel) + if (!pan) return + pan.show.tabs = true + query(this.box).find('#layout_' + this.name + '_panel_' + panel + '> [data-role="panel-tabs"]').show() + this.resize() + } + toggleTabs(panel) { + const pan = this.get(panel) + if (!pan) return + if (pan.show.tabs) this.hideTabs(panel); else this.showTabs(panel) + } + assignTabs(panel, tabs) { + if (typeof tabs === 'string' && w2ui[tabs] != null) tabs = w2ui[tabs] + const pan = this.get(panel) + pan.tabs = tabs + const tmp = query(this.box).find(panel + '> [data-role="panel-tabs"]') + if (pan.tabs != null) { + if (tmp.attr('name') != pan.tabs.name) { + pan.tabs.render(tmp.get(0)) + } else if (pan.tabs != null) { + pan.tabs.refresh() + } + tabs.owner = this + this.showTabs(panel) + this.refresh(panel) + } else { + tmp.html('') + this.hideTabs(panel) + } + } + render(box) { + const time = Date.now() + const self = this + if (typeof box === 'string') box = query(box).get(0) + // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection + // event before + const edata = this.trigger('render', { target: this.name, box: box ?? this.box }) + if (edata.isCancelled === true) return + // default action + if (box != null) { + this.unmount() // clean previous control + this.box = box + } + if (!this.box) return false + // render layout + query(this.box) + .attr('name', this.name) + .addClass('w2ui-layout') + .html('
') + if (query(this.box).length > 0) { + query(this.box)[0].style.cssText += this.style + } + // create all panels + for (let p1 = 0; p1 < w2panels.length; p1++) { + const html = '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + query(this.box).find(':scope > div').append(html) + } + query(this.box).find(':scope > div') + .append('
') + this.refresh() // if refresh is not called here, the layout will not be available right after initialization + // observe div resize + this.last.observeResize = new ResizeObserver(() => { this.resize() }) + this.last.observeResize.observe(this.box) + // process event + edata.finish() + // re-init events + setTimeout(() => { // needed this timeout to allow browser to render first if there are tabs or toolbar + self.last.events = { resizeStart, mouseMove, mouseUp } + this.resize() + }, 0) + return Date.now() - time + function resizeStart(type, evnt) { + if (!self.box) return + if (!evnt) evnt = window.event + query(document) + .off('mousemove', self.last.events.mouseMove) + .on('mousemove', self.last.events.mouseMove) + query(document) + .off('mouseup', self.last.events.mouseUp) + .on('mouseup', self.last.events.mouseUp) + self.last.resize = { + type: type, + x: evnt.screenX, + y: evnt.screenY, + diff_x: 0, + diff_y: 0, + value: 0 + } + // lock all panels + w2panels.forEach(panel => { + const $tmp = query(self.el(panel)).find('.w2ui-lock') + if ($tmp.length > 0) { + $tmp.data('locked', 'yes') + } else { + self.lock(panel, { opacity: 0 }) + } + }) + const el = query(self.box).find('#layout_' + self.name + '_resizer_' + type).get(0) + if (type == 'left' || type == 'right') { + self.last.resize.value = parseInt(el.style.left) + } + if (type == 'top' || type == 'preview' || type == 'bottom') { + self.last.resize.value = parseInt(el.style.top) + } + } + function mouseUp(evnt) { + if (!self.box) return + if (!evnt) evnt = window.event + query(document).off('mousemove', self.last.events.mouseMove) + query(document).off('mouseup', self.last.events.mouseUp) + if (self.last.resize == null) return + // unlock all panels + w2panels.forEach(panel => { + const $tmp = query(self.el(panel)).find('.w2ui-lock') + if ($tmp.data('locked') == 'yes') { + $tmp.removeData('locked') + } else { + self.unlock(panel) + } + }) + // set new size + if (self.last.diff_x !== 0 || self.last.resize.diff_y !== 0) { // only recalculate if changed + const ptop = self.get('top') + const pbottom = self.get('bottom') + const panel = self.get(self.last.resize.type) + const width = w2utils.getSize(query(self.box), 'width') + const height = w2utils.getSize(query(self.box), 'height') + const str = String(panel.size) + let ns, nd + switch (self.last.resize.type) { + case 'top': + ns = parseInt(panel.sizeCalculated) + self.last.resize.diff_y + nd = 0 + break + case 'bottom': + ns = parseInt(panel.sizeCalculated) - self.last.resize.diff_y + nd = 0 + break + case 'preview': + ns = parseInt(panel.sizeCalculated) - self.last.resize.diff_y + nd = (ptop && !ptop.hidden ? ptop.sizeCalculated : 0) + + (pbottom && !pbottom.hidden ? pbottom.sizeCalculated : 0) + break + case 'left': + ns = parseInt(panel.sizeCalculated) + self.last.resize.diff_x + nd = 0 + break + case 'right': + ns = parseInt(panel.sizeCalculated) - self.last.resize.diff_x + nd = 0 + break + } + // set size + if (str.substr(str.length - 1) == '%') { + panel.size = Math.floor(ns * 100 / (panel.type == 'left' || panel.type == 'right' ? width : height - nd) * 100) / 100 + '%' + } else { + if (String(panel.size).substr(0, 1) == '-') { + panel.size = parseInt(panel.size) - panel.sizeCalculated + ns + } else { + panel.size = ns + } + } + self.resize() + } + query(self.box) + .find('#layout_' + self.name + '_resizer_' + self.last.resize.type) + .removeClass('active') + delete self.last.resize + } + function mouseMove(evnt) { + if (!self.box) return + if (!evnt) evnt = window.event + if (self.last.resize == null) return + const panel = self.get(self.last.resize.type) + // event before + const tmp = self.last.resize + const edata = self.trigger('resizing', { target: self.name, object: panel, originalEvent: evnt, + panel: tmp ? tmp.type : 'all', diff_x: tmp ? tmp.diff_x : 0, diff_y: tmp ? tmp.diff_y : 0 }) + if (edata.isCancelled === true) return + const p = query(self.box).find('#layout_' + self.name + '_resizer_' + tmp.type) + let resize_x = (evnt.screenX - tmp.x) + let resize_y = (evnt.screenY - tmp.y) + const mainPanel = self.get('main') + if (!p.hasClass('active')) p.addClass('active') + switch (tmp.type) { + case 'left': + if (panel.minSize - resize_x > panel.width) { + resize_x = panel.minSize - panel.width + } + if (panel.maxSize && (panel.width + resize_x > panel.maxSize)) { + resize_x = panel.maxSize - panel.width + } + if (mainPanel.minSize + resize_x > mainPanel.width) { + resize_x = mainPanel.width - mainPanel.minSize + } + break + case 'right': + if (panel.minSize + resize_x > panel.width) { + resize_x = panel.width - panel.minSize + } + if (panel.maxSize && (panel.width - resize_x > panel.maxSize)) { + resize_x = panel.width - panel.maxSize + } + if (mainPanel.minSize - resize_x > mainPanel.width) { + resize_x = mainPanel.minSize - mainPanel.width + } + break + case 'top': + if (panel.minSize - resize_y > panel.height) { + resize_y = panel.minSize - panel.height + } + if (panel.maxSize && (panel.height + resize_y > panel.maxSize)) { + resize_y = panel.maxSize - panel.height + } + if (mainPanel.minSize + resize_y > mainPanel.height) { + resize_y = mainPanel.height - mainPanel.minSize + } + break + case 'preview': + case 'bottom': + if (panel.minSize + resize_y > panel.height) { + resize_y = panel.height - panel.minSize + } + if (panel.maxSize && (panel.height - resize_y > panel.maxSize)) { + resize_y = panel.height - panel.maxSize + } + if (mainPanel.minSize - resize_y > mainPanel.height) { + resize_y = mainPanel.minSize - mainPanel.height + } + break + } + tmp.diff_x = resize_x + tmp.diff_y = resize_y + switch (tmp.type) { + case 'top': + case 'preview': + case 'bottom': + tmp.diff_x = 0 + if (p.length > 0) p[0].style.top = (tmp.value + tmp.diff_y) + 'px' + break + case 'left': + case 'right': + tmp.diff_y = 0 + if (p.length > 0) p[0].style.left = (tmp.value + tmp.diff_x) + 'px' + break + } + // event after + edata.finish() + } + } + unmount() { + super.unmount() + this.panels.forEach(panel => { + panel.tabs?.unmount?.() + panel.toolbar?.unmount?.() + }) + this.last.observeResize?.disconnect() + } + destroy() { + // event before + const edata = this.trigger('destroy', { target: this.name }) + if (edata.isCancelled === true) return + if (w2ui[this.name] == null) return false + // clean up + this.panels.forEach(panel => { + panel.tabs?.destroy?.() + panel.toolbar?.destroy?.() + }) + if (query(this.box).find('#layout_' + this.name + '_panel_main').length > 0) { + this.unmount() + } + delete w2ui[this.name] + // event after + edata.finish() + if (this.last.events && this.last.events.resize) { + query(window).off('resize', this.last.events.resize) + } + return true + } + refresh(panel) { + const self = this + // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection + if (panel == null) panel = null + const time = Date.now() + // event before + const edata = self.trigger('refresh', { target: (panel != null ? panel : self.name), object: self.get(panel) }) + if (edata.isCancelled === true) return + // self.unlock(panel); + if (typeof panel === 'string') { + const p = self.get(panel) + if (p == null) return + const pname = '#layout_' + self.name + '_panel_' + p.type + const rname = '#layout_' + self.name + '_resizer_' + p.type + // apply properties to the panel + query(self.box).find(pname).css({ display: p.hidden ? 'none' : 'block' }) + if (p.resizable) { + query(self.box).find(rname).show() + } else { + query(self.box).find(rname).hide() + } + // insert content + if (typeof p.html === 'object' && typeof p.html.render === 'function') { + p.html.box = query(self.box).find(pname + '> [data-role="panel-content"]')[0] + setTimeout(() => { + // need to remove unnecessary classes + if (query(self.box).find(pname + '> [data-role="panel-content"]').length > 0) { + query(self.box).find(pname + '> [data-role="panel-content"]') + .removeClass() + .removeAttr('name') + .addClass('w2ui-panel-content') + .css('overflow', p.overflow)[0].style.cssText += ';' + p.style + } + if (p.html && typeof p.html.render === 'function') { + p.html.render() // do not do .render(box); + } + }, 1) + } else { + // need to remove unnecessary classes + if (query(self.box).find(pname + '> [data-role="panel-content"]').length > 0) { + query(self.box).find(pname + '> [data-role="panel-content"]') + .removeClass() + .removeAttr('name') + .addClass('w2ui-panel-content') + .html(p.html) + .css('overflow', p.overflow)[0].style.cssText += ';' + p.style + } + } + // if there are tabs and/or toolbar - render it + let tmp = query(self.box).find(pname + '> [data-role="panel-tabs"]') + if (p.show.tabs) { + if (tmp.attr('name') != p.tabs.name && p.tabs != null) { + p.tabs.render(tmp.get(0)) + } else { + p.tabs.refresh() + } + tmp.addClass('w2ui-panel-tabs') + } else { + tmp.html('').removeAttr('name').removeClass('w2ui-tabs').hide() + } + tmp = query(self.box).find(pname + '> [data-role="panel-toolbar"]') + if (p.show.toolbar) { + if (tmp.attr('name') != p.toolbar.name && p.toolbar != null) { + p.toolbar.render(tmp.get(0)) + } else { + p.toolbar.refresh() + } + tmp.addClass('w2ui-panel-toolbar') + } else { + tmp.html('').removeAttr('name').removeClass('w2ui-toolbar').hide() + } + // show title + tmp = query(self.box).find(pname + '> .w2ui-panel-title') + if (p.title) { + tmp.html(p.title).show() + } else { + tmp.html('').hide() + } + } else { + if (query(self.box).find('#layout_' + self.name + '_panel_main').length === 0) { + self.render() + return + } + self.resize() + // refresh all of them + for (let p1 = 0; p1 < this.panels.length; p1++) { self.refresh(this.panels[p1].type) } + } + edata.finish() + return Date.now() - time + } + resize() { + // if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection + if (!this.box) return false + const time = Date.now() + // event before + let tmp = this.last.resize + const edata = this.trigger('resize', { target: this.name, + panel: tmp ? tmp.type : 'all', diff_x: tmp ? tmp.diff_x : 0, diff_y: tmp ? tmp.diff_y : 0 }) + if (edata.isCancelled === true) return + if (this.padding < 0) this.padding = 0 + // layout itself + // width includes border and padding, we need to exclude that so panels + // are sized correctly + const width = w2utils.getSize(query(this.box), 'width') + const height = w2utils.getSize(query(this.box), 'height') + const self = this + // panels + const pmain = this.get('main') + const pprev = this.get('preview') + const pleft = this.get('left') + const pright = this.get('right') + const ptop = this.get('top') + const pbottom = this.get('bottom') + const sprev = (!!(pprev != null && pprev.hidden !== true)) + const sleft = (!!(pleft != null && pleft.hidden !== true)) + const sright = (!!(pright != null && pright.hidden !== true)) + const stop = (!!(ptop != null && ptop.hidden !== true)) + const sbottom = (!!(pbottom != null && pbottom.hidden !== true)) + let l, t, w, h + // calculate % + for (let p = 0; p < w2panels.length; p++) { + if (w2panels[p] === 'main') continue + tmp = this.get(w2panels[p]) + if (!tmp) continue + const str = String(tmp.size || 0) + if (str.substr(str.length - 1) == '%') { + let tmph = height + if (tmp.type == 'preview') { + tmph = tmph - + (ptop && !ptop.hidden ? ptop.sizeCalculated : 0) - + (pbottom && !pbottom.hidden ? pbottom.sizeCalculated : 0) + } + tmp.sizeCalculated = parseInt((tmp.type == 'left' || tmp.type == 'right' ? width : tmph) * parseFloat(tmp.size) / 100) + } else { + tmp.sizeCalculated = parseInt(tmp.size) + } + tmp.sizeCalculated = Math.max(tmp.sizeCalculated, parseInt(tmp.minSize)) + } + // negative size + if (parseInt(pright.size) < 0) { + if (sleft && parseInt(pleft.size) < 0) { + console.log('ERROR: you cannot have both left panel.size and right panel.size be negative.') + } else { + pright.sizeCalculated = width - (sleft ? pleft.sizeCalculated : 0) + parseInt(pright.size) + } + } + if (parseInt(pleft.size) < 0) { + if (sright && parseInt(pright.size) < 0) { + console.log('ERROR: you cannot have both left panel.size and right panel.size be negative.') + } else { + pleft.sizeCalculated = width - (sright ? pright.sizeCalculated : 0) + parseInt(pleft.size) + } + } + if (parseInt(pprev.size) < 0) { + pprev.sizeCalculated = height + parseInt(pprev.size) + if (pprev.sizeCalculated > height) pprev.sizeCalculated = height + } + // top if any + if (ptop != null && ptop.hidden !== true) { + l = 0 + t = 0 + w = width + h = ptop.sizeCalculated + query(this.box).find('#layout_' + this.name + '_panel_top') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px' + }) + ptop.width = w + ptop.height = h + // resizer + if (ptop.resizable) { + t = ptop.sizeCalculated - (this.padding === 0 ? this.resizer : 0) + h = (this.resizer > this.padding ? this.resizer : this.padding) + query(this.box).find('#layout_' + this.name + '_resizer_top') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px', + 'cursor': 'ns-resize' + }) + .off('mousedown') + .on('mousedown', function(event) { + event.preventDefault() + // event before + const edata = self.trigger('resizerClick', { target: 'top', originalEvent: event }) + if (edata.isCancelled === true) return + // default action + w2ui[self.name].last.events.resizeStart('top', event) + // event after + edata.finish() + return false + }) + } + } else { + query(this.box).find('#layout_' + this.name + '_panel_top').hide() + query(this.box).find('#layout_' + this.name + '_resizer_top').hide() + } + // left if any + if (pleft != null && pleft.hidden !== true) { + l = 0 + t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0) + w = pleft.sizeCalculated + h = height - (stop ? ptop.sizeCalculated + this.padding : 0) - + (sbottom ? pbottom.sizeCalculated + this.padding : 0) + query(this.box).find('#layout_' + this.name + '_panel_left') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px' + }) + pleft.width = w + pleft.height = h + // resizer + if (pleft.resizable) { + l = pleft.sizeCalculated - (this.padding === 0 ? this.resizer : 0) + w = (this.resizer > this.padding ? this.resizer : this.padding) + query(this.box).find('#layout_' + this.name + '_resizer_left') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px', + 'cursor': 'ew-resize' + }) + .off('mousedown') + .on('mousedown', function(event) { + event.preventDefault() + // event before + const edata = self.trigger('resizerClick', { target: 'left', originalEvent: event }) + if (edata.isCancelled === true) return + // default action + w2ui[self.name].last.events.resizeStart('left', event) + // event after + edata.finish() + return false + }) + } + } else { + query(this.box).find('#layout_' + this.name + '_panel_left').hide() + query(this.box).find('#layout_' + this.name + '_resizer_left').hide() + } + // right if any + if (pright != null && pright.hidden !== true) { + l = width - pright.sizeCalculated + t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0) + w = pright.sizeCalculated + h = height - (stop ? ptop.sizeCalculated + this.padding : 0) - + (sbottom ? pbottom.sizeCalculated + this.padding : 0) + query(this.box).find('#layout_' + this.name + '_panel_right') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px' + }) + pright.width = w + pright.height = h + // resizer + if (pright.resizable) { + l = l - this.padding + w = (this.resizer > this.padding ? this.resizer : this.padding) + query(this.box).find('#layout_' + this.name + '_resizer_right') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px', + 'cursor': 'ew-resize' + }) + .off('mousedown') + .on('mousedown', function(event) { + event.preventDefault() + // event before + const edata = self.trigger('resizerClick', { target: 'right', originalEvent: event }) + if (edata.isCancelled === true) return + // default action + w2ui[self.name].last.events.resizeStart('right', event) + // event after + edata.finish() + return false + }) + } + } else { + query(this.box).find('#layout_' + this.name + '_panel_right').hide() + query(this.box).find('#layout_' + this.name + '_resizer_right').hide() + } + // bottom if any + if (pbottom != null && pbottom.hidden !== true) { + l = 0 + t = height - pbottom.sizeCalculated + w = width + h = pbottom.sizeCalculated + query(this.box).find('#layout_' + this.name + '_panel_bottom') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px' + }) + pbottom.width = w + pbottom.height = h + // resizer + if (pbottom.resizable) { + t = t - (this.padding === 0 ? 0 : this.padding) + h = (this.resizer > this.padding ? this.resizer : this.padding) + query(this.box).find('#layout_' + this.name + '_resizer_bottom') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px', + 'cursor': 'ns-resize' + }) + .off('mousedown') + .on('mousedown', function(event) { + event.preventDefault() + // event before + const edata = self.trigger('resizerClick', { target: 'bottom', originalEvent: event }) + if (edata.isCancelled === true) return + // default action + w2ui[self.name].last.events.resizeStart('bottom', event) + // event after + edata.finish() + return false + }) + } + } else { + query(this.box).find('#layout_' + this.name + '_panel_bottom').hide() + query(this.box).find('#layout_' + this.name + '_resizer_bottom').hide() + } + // main - always there + l = 0 + (sleft ? pleft.sizeCalculated + this.padding : 0) + t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0) + w = width - (sleft ? pleft.sizeCalculated + this.padding : 0) - + (sright ? pright.sizeCalculated + this.padding : 0) + h = height - (stop ? ptop.sizeCalculated + this.padding : 0) - + (sbottom ? pbottom.sizeCalculated + this.padding : 0) - + (sprev ? pprev.sizeCalculated + this.padding : 0) + query(this.box) + .find('#layout_' + this.name + '_panel_main') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px' + }) + pmain.width = w + pmain.height = h + // preview if any + if (pprev != null && pprev.hidden !== true) { + l = 0 + (sleft ? pleft.sizeCalculated + this.padding : 0) + t = height - (sbottom ? pbottom.sizeCalculated + this.padding : 0) - pprev.sizeCalculated + w = width - (sleft ? pleft.sizeCalculated + this.padding : 0) - + (sright ? pright.sizeCalculated + this.padding : 0) + h = pprev.sizeCalculated + query(this.box).find('#layout_' + this.name + '_panel_preview') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px' + }) + pprev.width = w + pprev.height = h + // resizer + if (pprev.resizable) { + t = t - (this.padding === 0 ? 0 : this.padding) + h = (this.resizer > this.padding ? this.resizer : this.padding) + query(this.box).find('#layout_' + this.name + '_resizer_preview') + .css({ + 'display': 'block', + 'left': l + 'px', + 'top': t + 'px', + 'width': w + 'px', + 'height': h + 'px', + 'cursor': 'ns-resize' + }) + .off('mousedown') + .on('mousedown', function(event) { + event.preventDefault() + // event before + const edata = self.trigger('resizerClick', { target: 'preview', originalEvent: event }) + if (edata.isCancelled === true) return + // default action + w2ui[self.name].last.events.resizeStart('preview', event) + // event after + edata.finish() + return false + }) + } + } else { + query(this.box).find('#layout_' + this.name + '_panel_preview').hide() + query(this.box).find('#layout_' + this.name + '_resizer_preview').hide() + } + // resizes boxes for header, tabs, toolbar inside the panel + this.resizeBoxes() + edata.finish() + return Date.now() - time + } + resizeBoxes(panel) { + let panels = w2panels + if (!panel && typeof panel === 'string') panels = [panel] + // display tabs and toolbar if needed + panels.forEach((pname, ind) => { + const pan = this.get(w2panels[ind]) + const tmp2 = `#layout_${this.name}_panel_${pname} > ` + let topHeight = 0 + if (pan) { + if (pan.title) { + const el = query(this.box).find(tmp2 + '.w2ui-panel-title').css({ top: topHeight + 'px', display: 'block' }) + topHeight += w2utils.getSize(el, 'height') + } + if (pan.show.tabs) { + const el = query(this.box).find(tmp2 + '[data-role="panel-tabs"]').css({ top: topHeight + 'px', display: 'block' }) + topHeight += w2utils.getSize(el, 'height') + } + if (pan.show.toolbar) { + const el = query(this.box).find(tmp2 + '[data-role="panel-toolbar"]').css({ top: topHeight + 'px', display: 'block' }) + topHeight += w2utils.getSize(el, 'height') + } + } + query(this.box).find(tmp2 + '[data-role="panel-content"]') + .css({ + display: 'block', + top: topHeight + 'px' + }) + }) + } + lock(panel, msg, showSpinner) { + if (w2panels.indexOf(panel) == -1) { + console.log('ERROR: First parameter needs to be the a valid panel name.') + return + } + const args = Array.from(arguments) + args[0] = '#layout_' + this.name + '_panel_' + panel + w2utils.lock(...args) + } + unlock(panel, speed) { + if (w2panels.indexOf(panel) == -1) { + console.log('ERROR: First parameter needs to be the a valid panel name.') + return + } + const nm = '#layout_' + this.name + '_panel_' + panel + w2utils.unlock(nm, speed) + } +} +/** + * Part of w2ui 2.0 library + * - Dependencies: jQuery, w2utils, w2base, w2toolbar, w2field + * + * == TODO == + * - problem with .set() and arrays, array get extended too, but should be replaced + * - allow functions in routeData (also add routeData to list/enum) + * - send parsed URL to the event if there is routeData + * - add selectType: 'none' so that no selection can be make but with mouse + * - focus/blur for selectType = cell not display grayed out selection + * - allow enum in inline edit (see https://github.com/vitmalina/w2ui/issues/911#issuecomment-107341193) + * - remote source, but localSort/localSearch + * - promise for request, load, save, etc. + * - onloadmore event (so it will be easy to implement remote data source with local sort) + * - status() - clears on next select, etc. Should not if it is off + * + * == DEMOS To create == + * - batch for disabled buttons + * - natural sort + * + * == 2.0 changes + * - toolbarInput - deprecated, toolbarSearch stays + * - searchSuggest + * - searchSave, searchSelected, savedSearches, defaultSearches, useLocalStorage, searchFieldTooltip + * - cache, cacheSave + * - onSearchSave, onSearchRemove, onSearchSelect + * - show.searchLogic + * - show.searchSave + * - refreshSearch + * - initAllFields -> searchInitInput + * - textSearch - deprecated in favor of defaultOperator + * - grid.confirm - refactored + * - grid.message - refactored + * - search.type == 'text' can have 'in' and 'not in' operators, then it will switch to enum + * - grid.find(..., displayedOnly) + * - column.render(..., this) - added + * - observeResize for the box + * - remove edit.type == 'select' + * - editDone(...) + * - liveSearch + * - deprecated onUnselect event + * - requestComplete(data, action, callBack, resolve, reject) - new argument list + * - msgAJAXError -> msgHTTPError + * - aded msgServerError + * - added mouseEnter/mouseLeave + * - grid.show.columnReorder -> grid.reorderRows + * - updagte docs search.label (not search.text) + * - added columnAutoSize - which resizes column based on text in it + * - added grid.replace() + * - grid.compareSelection + * - this.showContextMenu(event, { recid, column, index }) - arguments changed + * - this.parseField + */ + +class w2grid extends w2base { + constructor(options) { + super(options.name) + this.name = null + this.box = null // HTML element that hold this element + this.columns = [] // { field, text, size, attr, render, hidden, gridMinWidth, editable } + this.columnGroups = [] // { span: int, text: 'string', main: true/false, style: 'string' } + this.records = [] // { recid: int(required), field1: 'value1', ... fieldN: 'valueN', style: 'string', changes: object } + this.summary = [] // array of summary records, same structure as records array + this.searches = [] // { type, label, field, attr, text, hidden } + this.toolbar = {} // if not empty object; then it is toolbar object + this.ranges = [] + this.contextMenu = [] + this.searchMap = {} // re-map search fields + this.searchData = [] + this.sortMap = {} // re-map sort fields + this.sortData = [] + this.savedSearches = [] + this.defaultSearches = [] + this.total = 0 // server total + this.recid = null // field from records to be used as recid + // internal + this.last = { + field: '', // last search field, e.g. 'all' + label: '', // last search field label, e.g. 'All Fields' + logic: 'AND', // last search logic, e.g. 'AND' or 'OR' + search: '', // last search text + searchIds: [], // last search IDs + selection: { // last selection details + indexes: [], + columns: {} + }, + saved_sel: null, // last result of selectionSave() + multi: false, // last multi flag, true when searching for multiple fields + fetch: { + action: '', // last fetch command, e.g. 'load' + offset: null, // last fetch offset, integer + start: 0, // timestamp of start of last fetch request + response: 0, // time it took to complete the last fetch request in seconds + options: null, + controller: null, + loaded: false, // data is loaded from the server + hasMore: false // flag to indicate if there are more items to pull from the server + }, + vscroll: { + scrollTop: 0, // last scrollTop position + scrollLeft: 0, // last scrollLeft position + recIndStart: null, // record index for first record in DOM + recIndEnd: null, // record index for last record in DOM + colIndStart: 0, // for column virtual scrolling + colIndEnd: 0, // for column virtual scrolling + pull_more: false, + pull_refresh: true, + show_extra: 0 // last show extra for virtual scrolling + }, + sel_ind: null, // last selected cell index + sel_col: null, // last selected column + sel_type: null, // last selection type, e.g. 'click' or 'key' + sel_recid: null, // last selected record id + idCache: {}, // object, id cache for get() + move: null, // object, move details + cancelClick: null, // boolean flag to indicate if the click event should be ignored, set during mouseMove() + inEditMode: false, // flag to indicate if we're currently in edit mode during inline editing + _edit: null, // object with details on the last edited cell, { value, index, column, recid } + kbd_timer: null, // last id of blur() timer + marker_timer: null, // last id of markSearch() timer + click_time: null, // timestamp of last click + click_recid: null, // last clicked record id + bubbleEl: null, // last bubble element + colResizing: false, // flag to indicate that a column is currently being resized + tmp: null, // object with last column resizing details + copy_event: null, // last copy event + userSelect: '', // last user select type, e.g. 'text' + columnDrag: false, // false or an object with a remove() method + state: null, // last grid state + toolbar_height: 0 // height of grid's toolbar + } + this.header = '' + this.url = '' + this.limit = 100 + this.offset = 0 // how many records to skip (for infinite scroll) when pulling from server + this.postData = {} + this.routeData = {} + this.httpHeaders = {} + this.show = { + header: false, + toolbar: false, + footer: false, + columnMenu: true, + columnHeaders: true, + lineNumbers: false, + expandColumn: false, + selectColumn: false, + emptyRecords: true, + toolbarReload: true, + toolbarColumns: false, + toolbarSearch: true, + toolbarAdd: false, + toolbarEdit: false, + toolbarDelete: false, + toolbarSave: false, + searchAll: true, + searchLogic: true, + searchHiddenMsg: false, + searchSave: true, + statusRange: true, + statusBuffered: false, + statusRecordID: true, + statusSelection: true, + statusResponse: true, + statusSort: false, + statusSearch: false, + recordTitles: false, + selectionBorder: true, + selectionResizer: true, + skipRecords: true, + saveRestoreState: true + } + this.stateId = null // Custom state name for stateSave, stateRestore and stateReset + this.hasFocus = false + this.autoLoad = true // for infinite scroll + this.fixedBody = true // if false; then grid grows with data + this.recordHeight = 32 + this.lineNumberWidth = 34 + this.keyboard = true + this.selectType = 'row' // can be row|cell + this.liveSearch = false // if true, it will auto search if typed in search_all + this.multiSearch = true + this.multiSelect = true + this.multiSort = true + this.reorderColumns = false + this.reorderRows = false + this.showExtraOnSearch = 0 // show extra records before and after on search + this.markSearch = true + this.columnTooltip = 'top|bottom' // can be top, bottom, left, right + this.disableCVS = false // disable Column Virtual Scroll + this.nestedFields = true // use field name containing dots as separator to look into object + this.vs_start = 150 + this.vs_extra = 5 + this.style = '' + this.tabIndex = null + this.dataType = null // if defined, then overwrites w2utils.settings.dataType + this.parser = null + this.advanceOnEdit = true // automatically begin editing the next cell after submitting an inline edit? + this.useLocalStorage = true + // default values for the column + this.colTemplate = { + text: '', // column text (can be a function) + field: '', // field name to map the column to a record + size: null, // size of column in px or % + min: 20, // minimum width of column in px + max: null, // maximum width of column in px + gridMinWidth: null, // minimum width of the grid when column is visible + sizeCorrected: null, // read only, corrected size (see explanation below) + sizeCalculated: null, // read only, size in px (see explanation below) + sizeOriginal: null, // size as defined + sizeType: null, // px or % + hidden: false, // indicates if column is hidden + sortable: false, // indicates if column is sortable + sortMode: null, // sort mode ('default'|'natural'|'i18n') or custom compare function + searchable: false, // bool/string: int,float,date,... or an object to create search field + resizable: true, // indicates if column is resizable + hideable: true, // indicates if column can be hidden + autoResize: null, // indicates if column can be auto-resized by double clicking on the resizer + attr: '', // string that will be inside the tag + style: '', // additional style for the td tag + render: null, // string or render function + title: null, // string or function for the title property for the column cells + tooltip: null, // string for the title property for the column header + editable: {}, // editable object (see explanation below) + frozen: false, // indicates if the column is fixed to the left + info: null, // info bubble, can be bool/object + clipboardCopy: false // if true (or string or function), it will display clipboard copy icon + } + // these column properties will be saved in stateSave() + this.stateColProps = { + text: false, + field: true, + size: true, + min: false, + max: false, + gridMinWidth: false, + sizeCorrected: false, + sizeCalculated: true, + sizeOriginal: true, + sizeType: true, + hidden: true, + sortable: false, + sortMode: true, + searchable: false, + resizable: false, + hideable: false, + autoResize: false, + attr: false, + style: false, + render: false, + title: false, + tooltip: false, + editable: false, + frozen: true, + info: false, + clipboardCopy: false + } + this.msgDelete = 'Are you sure you want to delete ${count} ${records}?' + this.msgNotJSON = 'Returned data is not in valid JSON format.' + this.msgHTTPError = 'HTTP error. See console for more details.' + this.msgServerError = 'Server error' + this.msgRefresh = 'Refreshing...' + this.msgNeedReload = 'Your remote data source record count has changed, reloading from the first record.' + this.msgEmpty = '' // if not blank, then it is message when server returns no records + this.buttons = { + 'reload': { type: 'button', id: 'w2ui-reload', icon: 'w2ui-icon-reload', tooltip: w2utils.lang('Reload data in the list') }, + 'columns': { type: 'menu-check', id: 'w2ui-column-on-off', icon: 'w2ui-icon-columns', tooltip: w2utils.lang('Show/hide columns'), + overlay: { align: 'none' } + }, + 'search': { type: 'html', id: 'w2ui-search', + html: '' + }, + 'add': { type: 'button', id: 'w2ui-add', text: 'Add New', tooltip: w2utils.lang('Add new record'), icon: 'w2ui-icon-plus' }, + 'edit': { type: 'button', id: 'w2ui-edit', text: 'Edit', tooltip: w2utils.lang('Edit selected record'), icon: 'w2ui-icon-pencil', batch: 1, disabled: true }, + 'delete': { type: 'button', id: 'w2ui-delete', text: 'Delete', tooltip: w2utils.lang('Delete selected records'), icon: 'w2ui-icon-cross', batch: true, disabled: true }, + 'save': { type: 'button', id: 'w2ui-save', text: 'Save', tooltip: w2utils.lang('Save changed records'), icon: 'w2ui-icon-check' } + } + this.operators = { // for search fields + 'text': ['is', 'begins', 'contains', 'ends'], // could have "in" and "not in" + 'number': ['=', 'between', '>', '<', '>=', '<='], + 'date': ['is', { oper: 'less', text: 'before' }, { oper: 'more', text: 'since' }, 'between'], + 'list': ['is'], + 'hex': ['is', 'between'], + 'color': ['is', 'begins', 'contains', 'ends'], + 'enum': ['in', 'not in'] + // -- all possible + // "text" : ['is', 'begins', 'contains', 'ends'], + // "number" : ['is', 'between', 'less:less than', 'more:more than', 'null:is null', 'not null:is not null'], + // "list" : ['is', 'null:is null', 'not null:is not null'], + // "enum" : ['in', 'not in', 'null:is null', 'not null:is not null'] + } + this.defaultOperator = { + 'text': 'begins', + 'number': '=', + 'date': 'is', + 'list': 'is', + 'enum': 'in', + 'hex': 'begins', + 'color': 'begins' + } + // map search field type to operator + this.operatorsMap = { + 'text': 'text', + 'int': 'number', + 'float': 'number', + 'money': 'number', + 'currency': 'number', + 'percent': 'number', + 'hex': 'hex', + 'alphanumeric': 'text', + 'color': 'color', + 'date': 'date', + 'time': 'date', + 'datetime': 'date', + 'list': 'list', + 'combo': 'text', + 'enum': 'enum', + 'file': 'enum', + 'select': 'list', + 'radio': 'list', + 'checkbox': 'list', + 'toggle': 'list' + } + // events + this.onAdd = null + this.onEdit = null + this.onRequest = null // called on any server event + this.onLoad = null + this.onDelete = null + this.onSave = null + this.onSelect = null + this.onClick = null + this.onDblClick = null + this.onContextMenu = null + this.onContextMenuClick = null // when context menu item selected + this.onColumnClick = null + this.onColumnDblClick = null + this.onColumnContextMenu = null + this.onColumnResize = null + this.onColumnAutoResize = null + this.onSort = null + this.onSearch = null + this.onSearchOpen = null + this.onChange = null // called when editable record is changed + this.onRestore = null // called when editable record is restored + this.onExpand = null + this.onCollapse = null + this.onError = null + this.onKeydown = null + this.onToolbar = null // all events from toolbar + this.onColumnOnOff = null + this.onCopy = null + this.onPaste = null + this.onSelectionExtend = null + this.onEditField = null + this.onRender = null + this.onRefresh = null + this.onReload = null + this.onResize = null + this.onDestroy = null + this.onStateSave = null + this.onStateRestore = null + this.onFocus = null + this.onBlur = null + this.onReorderRow = null + this.onSearchSave = null + this.onSearchRemove = null + this.onSearchSelect = null + this.onColumnSelect = null + this.onColumnDragStart = null + this.onColumnDragEnd = null + this.onResizerDblClick = null + this.onMouseEnter = null // mouse enter over record event + this.onMouseLeave = null + // need deep merge, should be extend, not objectAssign + w2utils.extend(this, options) + // check if there are records without recid + if (Array.isArray(this.records)) { + const remove = [] // remove from records as they are summary + this.records.forEach((rec, ind) => { + if (rec[this.recid] != null) { + rec.recid = rec[this.recid] + } + if (rec.recid == null) { + console.log('ERROR: Cannot add records without recid. (obj: ' + this.name + ')') + } + if (rec.w2ui?.summary === true) { + this.summary.push(rec) + remove.push(ind) // cannot remove here as it will mess up array walk thru + } + }) + remove.sort() + for (let t = remove.length - 1; t >= 0; t--) { + this.records.splice(remove[t], 1) + } + } + // add searches + if (Array.isArray(this.columns)) { + this.columns.forEach((col, ind) => { + col = w2utils.extend({}, this.colTemplate, col) + this.columns[ind] = col + const search = col.searchable + if (search == null || search === false || this.getSearch(col.field) != null) return + if (w2utils.isPlainObject(search)) { + this.addSearch(w2utils.extend({ field: col.field, label: col.text, type: 'text' }, search)) + } else { + let stype = col.searchable + let attr = '' + if (col.searchable === true) { + stype = 'text' + attr = 'size="20"' + } + this.addSearch({ field: col.field, label: col.text, type: stype, attr: attr }) + } + }) + } + // add icon to default searches if not defined + if (Array.isArray(this.defaultSearches)) { + this.defaultSearches.forEach((search, ind) => { + search.id = 'default-' + ind + search.icon ??= 'w2ui-icon-search' + }) + } + // check if there are saved searches in localStorage + const data = this.cache('searches') + if (Array.isArray(data)) { + data.forEach(search => { + this.savedSearches.push({ + id: search.id ?? 'none', + text: search.text ?? 'none', + icon: 'w2ui-icon-search', + remove: true, + logic: search.logic ?? 'AND', + data: search.data ?? [] + }) + }) + } + // init toolbar + this.initToolbar() + // render if box specified + if (typeof this.box === 'string') this.box = query(this.box).get(0) + if (this.box) this.render(this.box) + } + add(record, first) { + if (!Array.isArray(record)) record = [record] + let added = 0 + for (let i = 0; i < record.length; i++) { + const rec = record[i] + if (rec[this.recid] != null) { + rec.recid = rec[this.recid] + } + if (rec.recid == null) { + console.log('ERROR: Cannot add record without recid. (obj: ' + this.name + ')') + continue + } + if (rec.w2ui?.summary === true) { + if (first) this.summary.unshift(rec); else this.summary.push(rec) + } else { + if (first) this.records.unshift(rec); else this.records.push(rec) + } + added++ + } + const url = this.url?.get ?? this.url + if (!url) { + this.total = this.records.length + this.localSort(false, true) + this.localSearch() + // only refresh if it is in virtual view + const indStart = this.records.length - record.length + const indEnd = indStart + record.length + if (this.last.vscroll.recIndStart <= indEnd && this.last.vscroll.recIndEnd >= indStart) { + this.refresh() + } else { + // just update total if it it there + query(this.box) + .find('#grid_' + this.name + '_footer .w2ui-footer-right .w2ui-total') + .html(w2utils.formatNumber(this.total)) + } + } else { + this.refresh() + } + return added + } + find(obj, returnIndex, displayedOnly) { + if (obj == null) obj = {} + const recs = [] + let hasDots = false + // check if property is nested - needed for speed + for (const o in obj) if (String(o).indexOf('.') != -1) hasDots = true + // look for an item + const start = displayedOnly ? this.last.vscroll.recIndStart : 0 + let end = displayedOnly ? this.last.vscroll.recIndEnd + 1 : this.records.length + if (end > this.records.length) end = this.records.length + for (let i = start; i < end; i++) { + let match = true + for (const o in obj) { + let val = this.records[i][o] + if (hasDots && String(o).indexOf('.') != -1) val = this.parseField(this.records[i], o) + if (obj[o] == 'not-null') { + if (val == null || val === '') match = false + } else { + if (obj[o] != val) match = false + } + } + if (match && returnIndex !== true) recs.push(this.records[i].recid) + if (match && returnIndex === true) recs.push(i) + } + return recs + } + // does not delete existing, but overrides on top of it + set(recid, record, noRefresh) { + if ((typeof recid === 'object') && (recid !== null)) { + noRefresh = record + record = recid + recid = null + } + // update all records + if (recid == null) { + for (let i = 0; i < this.records.length; i++) { + w2utils.extend(this.records[i], record) // recid is the whole record + } + if (noRefresh !== true) this.refresh() + } else { // find record to update + const ind = this.get(recid, true) + if (ind == null) return false + const isSummary = (this.records[ind]?.recid != recid) + if (isSummary) { + w2utils.extend(this.summary[ind], record) + } else { + w2utils.extend(this.records[ind], record) + } + if (noRefresh !== true) this.refreshRow(recid, ind) // refresh only that record + } + return true + } + // replaces existing record + replace(recid, record, noRefresh) { + const ind = this.get(recid, true) + if (ind == null) return false + const isSummary = (this.records[ind]?.recid != recid) + if (isSummary) { + this.summary[ind] = record + } else { + this.records[ind] = record + } + if (noRefresh !== true) this.refreshRow(recid, ind) // refresh only that record + return true + } + get(recid, returnIndex) { + // search records + if (Array.isArray(recid)) { + const recs = [] + for (let i = 0; i < recid.length; i++) { + const v = this.get(recid[i], returnIndex) + if (v !== null) { recs.push(v) } + } + return recs + } else { + // get() must be fast, implements a cache to bypass loop over all records + // most of the time. + let idCache = this.last.idCache + if (!idCache) { + this.last.idCache = idCache = {} + } + let i = idCache[recid] + if (typeof (i) === 'number') { + if (i >= 0 && i < this.records.length && this.records[i].recid == recid) { + if (returnIndex === true) return i; else return this.records[i] + } + // summary indexes are stored as negative numbers, try them now. + i = ~i + if (i >= 0 && i < this.summary.length && this.summary[i].recid == recid) { + if (returnIndex === true) return i; else return this.summary[i] + } + // wrong index returned, clear cache + this.last.idCache = idCache = {} + } + for (let i = 0; i < this.records.length; i++) { + if (this.records[i].recid == recid) { + idCache[recid] = i + if (returnIndex === true) return i; else return this.records[i] + } + } + // search summary + for (let i = 0; i < this.summary.length; i++) { + if (this.summary[i].recid == recid) { + idCache[recid] = ~i + if (returnIndex === true) return i; else return this.summary[i] + } + } + return null + } + } + getFirst(offset) { + if (this.records.length == 0) return null + let rec = this.records[0] + const tmp = this.last.searchIds + if (this.searchData.length > 0) { + if (Array.isArray(tmp) && tmp.length > 0) { + rec = this.records[tmp[offset || 0]] + } else { + rec = null + } + } + return rec + } + remove() { + let removed = 0 + for (let a = 0; a < arguments.length; a++) { + for (let r = this.records.length - 1; r >= 0; r--) { + if (this.records[r].recid == arguments[a]) { this.records.splice(r, 1); removed++ } + } + for (let r = this.summary.length - 1; r >= 0; r--) { + if (this.summary[r].recid == arguments[a]) { this.summary.splice(r, 1); removed++ } + } + } + const url = this.url?.get ?? this.url + if (!url) { + this.localSort(false, true) + this.localSearch() + this.total = this.records.length + } + this.refresh() + return removed + } + addColumn(before, columns) { + let added = 0 + if (arguments.length == 1) { + columns = before + before = this.columns.length + } else { + if (typeof before === 'string') before = this.getColumn(before, true) + if (before == null) before = this.columns.length + } + if (!Array.isArray(columns)) columns = [columns] + for (let i = 0; i < columns.length; i++) { + const col = w2utils.extend({}, this.colTemplate, columns[i]) + this.columns.splice(before, 0, col) + // if column is searchable, add search field + if (columns[i].searchable) { + let stype = columns[i].searchable + let attr = '' + if (columns[i].searchable === true) { stype = 'text'; attr = 'size="20"' } + this.addSearch({ field: columns[i].field, label: columns[i].text, type: stype, attr: attr }) + } + before++ + added++ + } + this.refresh() + return added + } + removeColumn() { + let removed = 0 + for (let a = 0; a < arguments.length; a++) { + for (let r = this.columns.length - 1; r >= 0; r--) { + if (this.columns[r].field == arguments[a]) { + if (this.columns[r].searchable) this.removeSearch(arguments[a]) + this.columns.splice(r, 1) + removed++ + } + } + } + this.refresh() + return removed + } + getColumn(field, returnIndex) { + // no arguments - return fields of all columns + if (arguments.length === 0) { + const ret = [] + for (let i = 0; i < this.columns.length; i++) ret.push(this.columns[i].field) + return ret + } + // find column + for (let i = 0; i < this.columns.length; i++) { + if (this.columns[i].field == field) { + if (returnIndex === true) return i; else return this.columns[i] + } + } + return null + } + updateColumn(fields, updates) { + let effected = 0 + fields = (Array.isArray(fields) ? fields : [fields]) + fields.forEach((colName) => { + this.columns.forEach((col) => { + if (col.field == colName) { + const _updates = w2utils.clone(updates) + Object.keys(_updates).forEach((key) => { + // if it is a function + if (typeof _updates[key] === 'function') { + _updates[key] = _updates[key](col) + } + if (col[key] != _updates[key]) effected++ + }) + w2utils.extend(col, _updates) + } + }) + }) + if (effected > 0) { + this.refresh() // need full refresh due to colgroups not reassigning properly + } + return effected + } + toggleColumn() { + return this.updateColumn(Array.from(arguments), { hidden(col) { return !col.hidden } }) + } + showColumn() { + return this.updateColumn(Array.from(arguments), { hidden: false }) + } + hideColumn() { + return this.updateColumn(Array.from(arguments), { hidden: true }) + } + addSearch(before, search) { + let added = 0 + if (arguments.length == 1) { + search = before + before = this.searches.length + } else { + if (typeof before === 'string') before = this.getSearch(before, true) + if (before == null) before = this.searches.length + } + if (!Array.isArray(search)) search = [search] + for (let i = 0; i < search.length; i++) { + this.searches.splice(before, 0, search[i]) + before++ + added++ + } + this.searchClose() + return added + } + removeSearch() { + let removed = 0 + for (let a = 0; a < arguments.length; a++) { + for (let r = this.searches.length - 1; r >= 0; r--) { + if (this.searches[r].field == arguments[a]) { this.searches.splice(r, 1); removed++ } + } + } + this.searchClose() + return removed + } + getSearch(field, returnIndex) { + // no arguments - return fields of all searches + if (arguments.length === 0) { + const ret = [] + for (let i = 0; i < this.searches.length; i++) ret.push(this.searches[i].field) + return ret + } + // find search + for (let i = 0; i < this.searches.length; i++) { + if (this.searches[i].field == field) { + if (returnIndex === true) return i; else return this.searches[i] + } + } + return null + } + toggleSearch() { + let effected = 0 + for (let a = 0; a < arguments.length; a++) { + for (let r = this.searches.length - 1; r >= 0; r--) { + if (this.searches[r].field == arguments[a]) { + this.searches[r].hidden = !this.searches[r].hidden + effected++ + } + } + } + this.searchClose() + return effected + } + showSearch() { + let shown = 0 + for (let a = 0; a < arguments.length; a++) { + for (let r = this.searches.length - 1; r >= 0; r--) { + if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== false) { + this.searches[r].hidden = false + shown++ + } + } + } + this.searchClose() + return shown + } + hideSearch() { + let hidden = 0 + for (let a = 0; a < arguments.length; a++) { + for (let r = this.searches.length - 1; r >= 0; r--) { + if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== true) { + this.searches[r].hidden = true + hidden++ + } + } + } + this.searchClose() + return hidden + } + getSearchData(field) { + for (let i = 0; i < this.searchData.length; i++) { + if (this.searchData[i].field == field) return this.searchData[i] + } + return null + } + localSort(silent, noResetRefresh) { + const obj = this + const url = this.url?.get ?? this.url + if (url) { + console.log('ERROR: grid.localSort can only be used on local data source, grid.url should be empty.') + return 0 // time it took + } + if (Object.keys(this.sortData).length === 0) { + // restore original sorting + const os = this.last.originalSort + if (os) { + this.records.sort((a, b) => { + const aInd = os.indexOf(a.recid) + const bInd = os.indexOf(b.recid) + // order cann be equal, so, no need to return 0 + return aInd > bInd ? 1 : -1 + }) + } + return 0 // time it took + } + let time = Date.now() + // process date fields + this.selectionSave() + this.prepareData() + if (!noResetRefresh) { + this.reset() + } + // process sortData + for (let i = 0; i < this.sortData.length; i++) { + const column = this.getColumn(this.sortData[i].field) + if (!column) return // TODO: ability to sort columns when they are not part of colums array + if (typeof column.render === 'string') { + if (['date', 'age'].indexOf(column.render.split(':')[0]) != -1) { + this.sortData[i].field_ = column.field + '_' + } + if (['time'].indexOf(column.render.split(':')[0]) != -1) { + this.sortData[i].field_ = column.field + '_' + } + } + } + // prepare paths and process sort + preparePaths() + this.records.sort((a, b) => { + return compareRecordPaths(a, b) + }) + cleanupPaths() + this.selectionRestore(noResetRefresh) + time = Date.now() - time + if (silent !== true && this.show.statusSort) { + setTimeout(() => { + this.status(w2utils.lang('Sorting took ${count} seconds', { count: time / 1000 })) + }, 10) + } + return time + // grab paths before sorting for efficiency and because calling obj.get() + // while sorting 'obj.records' is unsafe, at least on webkit + function preparePaths() { + for (let i = 0; i < obj.records.length; i++) { + const rec = obj.records[i] + if (rec.w2ui?.parent_recid != null) { + rec.w2ui._path = getRecordPath(rec) + } + } + } + // cleanup and release memory allocated by preparePaths() + function cleanupPaths() { + for (let i = 0; i < obj.records.length; i++) { + const rec = obj.records[i] + if (rec.w2ui?.parent_recid != null) { + rec.w2ui._path = null + } + } + } + // compare two paths, from root of tree to given records + function compareRecordPaths(a, b) { + if ((!a.w2ui || a.w2ui.parent_recid == null) && (!b.w2ui || b.w2ui.parent_recid == null)) { + return compareRecords(a, b) // no tree, fast path + } + const pa = getRecordPath(a) + const pb = getRecordPath(b) + for (let i = 0; i < Math.min(pa.length, pb.length); i++) { + const diff = compareRecords(pa[i], pb[i]) + if (diff !== 0) return diff // different subpath + } + if (pa.length > pb.length) return 1 + if (pa.length < pb.length) return -1 + console.log('ERROR: two paths should not be equal.') + return 0 + } + // return an array of all records from root to and including 'rec' + function getRecordPath(rec) { + if (!rec.w2ui || rec.w2ui.parent_recid == null) return [rec] + if (rec.w2ui._path) { return rec.w2ui._path } + // during actual sort, we should never reach this point + const subrec = obj.get(rec.w2ui.parent_recid) + if (!subrec) { + console.log('ERROR: no parent record: ' + rec.w2ui.parent_recid) + return [rec] + } + return (getRecordPath(subrec).concat(rec)) + } + // compare two records according to sortData and finally recid + function compareRecords(a, b) { + if (a === b) return 0 // optimize, same object + for (let i = 0; i < obj.sortData.length; i++) { + const fld = obj.sortData[i].field + const sortFld = (obj.sortData[i].field_) ? obj.sortData[i].field_ : fld + let aa = a[sortFld] + let bb = b[sortFld] + if (String(fld).indexOf('.') != -1) { + aa = obj.parseField(a, sortFld) + bb = obj.parseField(b, sortFld) + } + const col = obj.getColumn(fld) + if (col && Object.keys(col.editable).length > 0) { // for drop editable fields and drop downs + if (w2utils.isPlainObject(aa) && aa.text) aa = aa.text + if (w2utils.isPlainObject(bb) && bb.text) bb = bb.text + } + const ret = compareCells(aa, bb, i, obj.sortData[i].direction, col.sortMode || 'default') + if (ret !== 0) return ret + } + // break tie for similar records, + // required to have consistent ordering for tree paths + const ret = compareCells(a.recid, b.recid, -1, 'asc') + return ret + } + // compare two values, aa and bb, producing consistent ordering + function compareCells(aa, bb, i, direction, sortMode) { + // if both objects are strictly equal, we're done + if (aa === bb) { return 0 } + // all nulls, empty and undefined on bottom + if ((aa == null || aa === '') && (bb != null && bb !== '')) { return 1 } + if ((aa != null && aa !== '') && (bb == null || bb === '')) { return -1 } + const dir = (direction.toLowerCase() === 'asc') ? 1 : -1 + // for different kind of objects, sort by object type + if (typeof aa !== typeof bb) { return (typeof aa > typeof bb) ? dir : -dir } + // for different kind of classes, sort by classes + if (aa.constructor.name != bb.constructor.name) { return (aa.constructor.name > bb.constructor.name) ? dir : -dir } + // if we're dealing with non-null objects, call valueOf(). + // this mean that Date() or custom objects will compare properly. + if (aa && typeof aa === 'object') { aa = aa.valueOf() } + if (bb && typeof bb === 'object') { bb = bb.valueOf() } + // if we're still dealing with non-null objects that have + // a useful Object => String conversion, convert to string. + const defaultToString = {}.toString + if (aa && typeof aa === 'object' && aa.toString != defaultToString) { aa = String(aa) } + if (bb && typeof bb === 'object' && bb.toString != defaultToString) { bb = String(bb) } + // do case-insensitive string comparison + if (typeof aa === 'string') { aa = aa.toLowerCase().trim() } + if (typeof bb === 'string') { bb = bb.toLowerCase().trim() } + switch (sortMode) { + case 'natural': + sortMode = w2utils.naturalCompare + break + case 'i18n': + sortMode = w2utils.i18nCompare + break + } + if (typeof sortMode === 'function') { + return sortMode(aa, bb) * dir + } + // compare both objects + if (aa > bb) { return dir } + if (aa < bb) { return -dir } + return 0 + } + } + localSearch(silent) { + const obj = this + const url = this.url?.get ?? this.url + if (url) { + console.log('ERROR: grid.localSearch can only be used on local data source, grid.url should be empty.') + return + } + let time = Date.now() + const defaultToString = {}.toString + const duplicateMap = {} + this.total = this.records.length + // mark all records as shown + this.last.searchIds = [] + // prepare date/time fields + this.prepareData() + // hide records that did not match + if (this.searchData.length > 0 && !url) { + this.total = 0 + for (let i = 0; i < this.records.length; i++) { + const rec = this.records[i] + const match = searchRecord(rec) + if (match) { + if (rec?.w2ui) addParent(rec.w2ui.parent_recid) + if (this.showExtraOnSearch > 0) { + let before = this.showExtraOnSearch + let after = this.showExtraOnSearch + if (i < before) before = i + if (i + after > this.records.length) after = this.records.length - i + if (before > 0) { + for (let j = i - before; j < i; j++) { + if (this.last.searchIds.indexOf(j) < 0) { this.last.searchIds.push(j) } + } + } + if (this.last.searchIds.indexOf(i) < 0) this.last.searchIds.push(i) + if (after > 0) { + for (let j = (i + 1); j <= (i + after); j++) { + if (this.last.searchIds.indexOf(j) < 0) this.last.searchIds.push(j) + } + } + } else { + this.last.searchIds.push(i) + } + } + } + this.total = this.last.searchIds.length + } + time = Date.now() - time + if (silent !== true && this.show.statusSearch) { + setTimeout(() => { + this.status(w2utils.lang('Search took ${count} seconds', { count: time / 1000 })) + }, 10) + } + return time + // check if a record (or one of its closed children) matches the search data + function searchRecord(rec) { + let fl = 0; let val1; let val2; let val3; let tmp + let orEqual = false + for (let j = 0; j < obj.searchData.length; j++) { + const sdata = obj.searchData[j] + let search = obj.getSearch(sdata.field) + if (sdata == null) continue + if (search == null) search = { field: sdata.field, type: sdata.type } + const val1b = obj.parseField(rec, search.field) + val1 = (val1b != null && (typeof val1b !== 'object' || val1b.toString != defaultToString)) + ? String(val1b).toLowerCase() + : '' // do not match a bogus string + if (sdata.value != null) { + if (!Array.isArray(sdata.value)) { + val2 = String(sdata.value).toLowerCase() + } else { + val2 = sdata.value[0] + val3 = sdata.value[1] + } + } + switch (sdata.operator) { + case '=': + case 'is': + if (obj.parseField(rec, search.field) == sdata.value) fl++ // do not hide record + else if (search.type == 'date') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.formatDate(tmp, 'yyyy-mm-dd') + val2 = w2utils.formatDate(w2utils.isDate(val2, w2utils.settings.dateFormat, true), 'yyyy-mm-dd') + if (val1 == val2) fl++ + } else if (search.type == 'time') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.formatTime(tmp, 'hh24:mi') + val2 = w2utils.formatTime(val2, 'hh24:mi') + if (val1 == val2) fl++ + } else if (search.type == 'datetime') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.formatDateTime(tmp, 'yyyy-mm-dd|hh24:mm:ss') + val2 = w2utils.formatDateTime(w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true), 'yyyy-mm-dd|hh24:mm:ss') + if (val1 == val2) fl++ + } + break + case 'between': + if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) { + if (parseFloat(obj.parseField(rec, search.field)) >= parseFloat(val2) && parseFloat(obj.parseField(rec, search.field)) <= parseFloat(val3)) fl++ + } else if (search.type == 'date') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.isDate(tmp, w2utils.settings.dateFormat, true) + val2 = w2utils.isDate(val2, w2utils.settings.dateFormat, true) + val3 = w2utils.isDate(val3, w2utils.settings.dateFormat, true) + if (val3 != null) val3 = new Date(val3.getTime() + 86400000) // 1 day + if (val1 >= val2 && val1 < val3) fl++ + } else if (search.type == 'time') { + val1 = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val2 = w2utils.isTime(val2, true) + val3 = w2utils.isTime(val3, true) + val2 = (new Date()).setHours(val2.hours, val2.minutes, val2.seconds ? val2.seconds : 0, 0) + val3 = (new Date()).setHours(val3.hours, val3.minutes, val3.seconds ? val3.seconds : 0, 0) + if (val1 >= val2 && val1 < val3) fl++ + } else if (search.type == 'datetime') { + val1 = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val2 = w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true) + val3 = w2utils.isDateTime(val3, w2utils.settings.datetimeFormat, true) + if (val3) val3 = new Date(val3.getTime() + 86400000) // 1 day + if (val1 >= val2 && val1 < val3) fl++ + } + break + case '<=': + orEqual = true + case '<': + case 'less': + if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) { + val1 = parseFloat(obj.parseField(rec, search.field)) + val2 = parseFloat(sdata.value) + if (val1 < val2 || (orEqual && val1 === val2)) fl++ + } else if (search.type == 'date') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.isDate(tmp, w2utils.settings.dateFormat, true) + val2 = w2utils.isDate(val2, w2utils.settings.dateFormat, true) + if (val1 < val2 || (orEqual && val1 === val2)) fl++ + } else if (search.type == 'time') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.formatTime(tmp, 'hh24:mi') + val2 = w2utils.formatTime(val2, 'hh24:mi') + if (val1 < val2 || (orEqual && val1 === val2)) fl++ + } else if (search.type == 'datetime') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.formatDateTime(tmp, 'yyyy-mm-dd|hh24:mm:ss') + val2 = w2utils.formatDateTime(w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true), 'yyyy-mm-dd|hh24:mm:ss') + if (val1.length == val2.length && (val1 < val2 || (orEqual && val1 === val2))) fl++ + } + break + case '>=': + orEqual = true + case '>': + case 'more': + if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) { + val1 = parseFloat(obj.parseField(rec, search.field)) + val2 = parseFloat(sdata.value) + if (val1 > val2 || (orEqual && val1 === val2)) fl++ + } else if (search.type == 'date') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.isDate(tmp, w2utils.settings.dateFormat, true) + val2 = w2utils.isDate(val2, w2utils.settings.dateFormat, true) + if (val1 > val2 || (orEqual && val1 === val2)) fl++ + } else if (search.type == 'time') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.formatTime(tmp, 'hh24:mi') + val2 = w2utils.formatTime(val2, 'hh24:mi') + if (val1 > val2 || (orEqual && val1 === val2)) fl++ + } else if (search.type == 'datetime') { + tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field)) + val1 = w2utils.formatDateTime(tmp, 'yyyy-mm-dd|hh24:mm:ss') + val2 = w2utils.formatDateTime(w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true), 'yyyy-mm-dd|hh24:mm:ss') + if (val1.length == val2.length && (val1 > val2 || (orEqual && val1 === val2))) fl++ + } + break + case 'in': + tmp = sdata.value + if (sdata.svalue) tmp = sdata.svalue + if ((tmp.indexOf(w2utils.isFloat(val1b) ? parseFloat(val1b) : val1b) !== -1) || (tmp.indexOf(val1) !== -1 && val1 !== '')) fl++ + break + case 'not in': + tmp = sdata.value + if (sdata.svalue) tmp = sdata.svalue + if (!((tmp.indexOf(w2utils.isFloat(val1b) ? parseFloat(val1b) : val1b) !== -1) || (tmp.indexOf(val1) !== -1 && val1 !== ''))) fl++ + break + case 'begins': + case 'begins with': // need for back compatibility + if (val1.indexOf(val2) === 0) fl++ // do not hide record + break + case 'contains': + if (val1.indexOf(val2) >= 0) fl++ // do not hide record + break + case 'null': + if (obj.parseField(rec, search.field) == null) fl++ // do not hide record + break + case 'not null': + if (obj.parseField(rec, search.field) != null) fl++ // do not hide record + break + case 'ends': + case 'ends with': // need for back compatibility + const lastIndex = val1.lastIndexOf(val2) + if (lastIndex !== -1 && lastIndex == val1.length - val2.length) fl++ // do not hide record + break + } + } + if ((obj.last.logic == 'OR' && fl !== 0) || (obj.last.logic == 'AND' && fl == obj.searchData.length)) { + return true + } + if (rec.w2ui?.children && rec.w2ui?.expanded !== true) { + // there are closed children, search them too. + for (let r = 0; r < rec.w2ui.children.length; r++) { + const subRec = rec.w2ui.children[r] + if (searchRecord(subRec)) { + return true + } + } + } + return false + } + // add parents nodes recursively + function addParent(recid) { + const i = obj.get(recid, true) + if (i == null || recid == null || duplicateMap[recid] || obj.last.searchIds.includes(i)) { + return + } + duplicateMap[recid] = true + const rec = obj.records[i] + if (rec?.w2ui) { + addParent(rec.w2ui.parent_recid) + } + obj.last.searchIds.push(i) + } + } + getRangeData(range, extra) { + const rec1 = this.get(range[0].recid, true) + const rec2 = this.get(range[1].recid, true) + const col1 = range[0].column + const col2 = range[1].column + const res = [] + if (col1 == col2) { // one row + for (let r = rec1; r <= rec2; r++) { + const record = this.records[r] + const dt = record[this.columns[col1].field] || null + if (extra !== true) { + res.push(dt) + } else { + res.push({ data: dt, column: col1, index: r, record: record }) + } + } + } else if (rec1 == rec2) { // one line + const record = this.records[rec1] + for (let i = col1; i <= col2; i++) { + const dt = record[this.columns[i].field] || null + if (extra !== true) { + res.push(dt) + } else { + res.push({ data: dt, column: i, index: rec1, record: record }) + } + } + } else { + for (let r = rec1; r <= rec2; r++) { + const record = this.records[r] + res.push([]) + for (let i = col1; i <= col2; i++) { + const dt = record[this.columns[i].field] + if (extra !== true) { + res[res.length - 1].push(dt) + } else { + res[res.length - 1].push({ data: dt, column: i, index: r, record: record }) + } + } + } + } + return res + } + addRange(ranges) { + let added = 0; let first; let last + if (this.selectType == 'row') return added + if (!Array.isArray(ranges)) ranges = [ranges] + // if it is selection + for (let i = 0; i < ranges.length; i++) { + if (typeof ranges[i] !== 'object') ranges[i] = { name: 'selection' } + if (ranges[i].name == 'selection') { + if (this.show.selectionBorder === false) continue + const sel = this.getSelection() + if (sel.length === 0) { + this.removeRange('selection') + continue + } else { + first = sel[0] + last = sel[sel.length - 1] + } + } else { // other range + first = ranges[i].range[0] + last = ranges[i].range[1] + } + if (first) { + const rg = { + name: ranges[i].name, + range: [{ recid: first.recid, column: first.column }, { recid: last.recid, column: last.column }], + style: ranges[i].style || '', + class: ranges[i].class + } + // add range + let ind = false + for (let j = 0; j < this.ranges.length; j++) if (this.ranges[j].name == ranges[i].name) { ind = j; break } + if (ind !== false) { + this.ranges[ind] = rg + } else { + this.ranges.push(rg) + } + added++ + } + } + this.refreshRanges() + return added + } + removeRange() { + let removed = 0 + for (let a = 0; a < arguments.length; a++) { + const name = arguments[a] + query(this.box).find('#grid_' + this.name + '_' + name).remove() + query(this.box).find('#grid_' + this.name + '_f' + name).remove() + for (let r = this.ranges.length - 1; r >= 0; r--) { + if (this.ranges[r].name == name) { + this.ranges.splice(r, 1) + removed++ + } + } + } + return removed + } + refreshRanges() { + if (this.ranges.length === 0) return + const self = this + let range + const time = Date.now() + const rec1 = query(this.box).find(`#grid_${this.name}_frecords`) + const rec2 = query(this.box).find(`#grid_${this.name}_records`) + for (let i = 0; i < this.ranges.length; i++) { + const rg = this.ranges[i] + const first = rg.range[0] + const last = rg.range[1] + if (first.index == null) first.index = this.get(first.recid, true) + if (last.index == null) last.index = this.get(last.recid, true) + let td1 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(first.recid) + ' td[col="' + first.column + '"]') + let td2 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(last.recid) + ' td[col="' + last.column + '"]') + let td1f = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(first.recid) + ' td[col="' + first.column + '"]') + let td2f = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(last.recid) + ' td[col="' + last.column + '"]') + let _lastColumn = last.column + // adjustment due to column virtual scroll + if (first.column < this.last.vscroll.colIndStart && last.column > this.last.vscroll.colIndStart) { + td1 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(first.recid) + ' td[col="start"]') + } + if (first.column < this.last.vscroll.colIndEnd && last.column > this.last.vscroll.colIndEnd) { + td2 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(last.recid) + ' td[col="end"]') + _lastColumn = '"end"' + } + // if virtual scrolling kicked in + const index_top = parseInt(query(this.box).find('#grid_' + this.name + '_rec_top').next().attr('index')) + const index_bottom = parseInt(query(this.box).find('#grid_' + this.name + '_rec_bottom').prev().attr('index')) + const index_ftop = parseInt(query(this.box).find('#grid_' + this.name + '_frec_top').next().attr('index')) + const index_fbottom = parseInt(query(this.box).find('#grid_' + this.name + '_frec_bottom').prev().attr('index')) + if (td1.length === 0 && first.index < index_top && last.index > index_top) { + td1 = query(this.box).find('#grid_' + this.name + '_rec_top').next().find('td[col="' + first.column + '"]') + } + if (td2.length === 0 && last.index > index_bottom && first.index < index_bottom) { + td2 = query(this.box).find('#grid_' + this.name + '_rec_bottom').prev().find('td[col="' + _lastColumn + '"]') + } + if (td1f.length === 0 && first.index < index_ftop && last.index > index_ftop) { // frozen + td1f = query(this.box).find('#grid_' + this.name + '_frec_top').next().find('td[col="' + first.column + '"]') + } + if (td2f.length === 0 && last.index > index_fbottom && first.index < index_fbottom) { // frozen + td2f = query(this.box).find('#grid_' + this.name + '_frec_bottom').prev().find('td[col="' + last.column + '"]') + } + // do not show selection cell if it is editable + const edit = query(this.box).find('#grid_' + this.name + '_editable') + const tmp = edit.find('.w2ui-input') + const tmp_ind = tmp.attr('index') + const tmp1 = this.records[tmp_ind]?.recid + const tmp2 = tmp.attr('column') + if (rg.name == 'selection' && rg.range[0].recid == tmp1 && rg.range[0].column == tmp2) continue + // frozen regular columns range + range = query(this.box).find('#grid_' + this.name + '_f' + rg.name) + if (td1f.length > 0 || td2f.length > 0) { + if (range.length === 0) { + rec1.append('
' + + (rg.name == 'selection' && this.show.selectionResizer ? '
' : '') + + '
') + range = query(this.box).find('#grid_' + this.name + '_f' + rg.name) + } else { + range.attr('style', rg.style) + range.find('.w2ui-selection-resizer').show() + } + if (td2f.length === 0) { + td2f = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(last.recid) + ' td:last-child') + if (td2f.length === 0) td2f = query(this.box).find('#grid_' + this.name + '_frec_bottom td:first-child') + range.css('border-right', '0px') + range.find('.w2ui-selection-resizer').hide() + } + if (first.recid != null && last.recid != null && td1f.length > 0 && td2f.length > 0) { + const style = getComputedStyle(td2f[0]) + const top1 = (td1f.prop('offsetTop') - td1f.prop('scrollTop')) + const left1 = (td1f.prop('offsetLeft') + td1f.prop('scrollLeft')) + const top2 = (td2f.prop('offsetTop') - td2f.prop('scrollTop')) + const left2 = (td2f.prop('offsetLeft') + td2f.prop('scrollLeft')) + range.show().css({ + top: (top1 > 0 ? top1 : 0) + 'px', + left: (left1 > 0 ? left1 : 0) + 'px', + width: (left2 - left1 + parseFloat(style.width) - 1) + 'px', + height: (top2 - top1 + parseFloat(style.height) - 1) + 'px' + }) + } else { + range.hide() + } + } else { + range.hide() + } + // regular columns range + range = query(this.box).find('#grid_' + this.name + '_' + rg.name) + if (td1.length > 0 || td2.length > 0) { + if (range.length === 0) { + rec2.append(` +
+ ${rg.name == 'selection' && this.show.selectionResizer + ? `
` + : '' +} +
+ `) + range = query(this.box).find('#grid_' + this.name + '_' + rg.name) + } else { + range.attr('style', rg.style) + } + if (td1.length === 0) { + td1 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(first.recid) + ' td:first-child') + if (td1.length === 0) td1 = query(this.box).find('#grid_' + this.name + '_rec_top td:first-child') + } + if (td2f.length !== 0) { + range.css('border-left', '0px') + } + if (first.recid != null && last.recid != null && td1.length > 0 && td2.length > 0) { + const style = getComputedStyle(td2[0]) + const top1 = (td1.prop('offsetTop') - td1.prop('scrollTop')) + const left1 = (td1.prop('offsetLeft') + td1.prop('scrollLeft')) + const top2 = (td2.prop('offsetTop') - td2.prop('scrollTop')) + const left2 = (td2.prop('offsetLeft') + td2.prop('scrollLeft')) + range.show().css({ + top: (top1 > 0 ? top1 : 0) + 'px', + left: (left1 > 0 ? left1 : 0) + 'px', + width: (left2 - left1 + parseFloat(style.width) - 1) + 'px', + height: (top2 - top1 + parseFloat(style.height) - 1) + 'px' + }) + } else { + range.hide() + } + } else { + range.hide() + } + } + // add resizer events + query(this.box).find('.w2ui-selection-resizer') + .off('.resizer') + .on('mousedown.resizer', mouseStart) + .on('dblclick.resizer', (event) => { + const edata = this.trigger('resizerDblClick', { target: this.name, originalEvent: event }) + if (edata.isCancelled === true) return + edata.finish() + }) + // this variables are needed for selection expantion + let edata + const detail = { target: this.name, originalRange: null, newRange: null } + const letters = 'abcdefghijklmnopqrstuvwxyz' + return Date.now() - time + function mouseStart(event) { + const sel = self.getSelection() + const first = sel[0] + const last = sel[sel.length - 1] + self.last.move = { + type: 'expand', + x: event.screenX, + y: event.screenY, + divX: 0, + divY: 0, + index: first.index, + recid: first.recid, + column: first.column, + name: letters[first.column] + (first.index + 1) + ':' + letters[last.column] + (last.index + 1), + originalRange: [w2utils.clone(first), w2utils.clone(last)], + newRange: [w2utils.clone(first), w2utils.clone(last)] + } + detail.originalName = self.last.move.name + detail.originalRange = self.last.move.originalRange + query('body') + .off('.w2ui-' + self.name) + .on('mousemove.w2ui-' + self.name, mouseMove) + .on('mouseup.w2ui-' + self.name, mouseStop) + // do not blur grid + event.preventDefault() + } + function mouseMove(event) { + const mv = self.last.move + if (!mv || mv.type != 'expand') return + mv.divX = (event.screenX - mv.x) + mv.divY = (event.screenY - mv.y) + // find new cell + let recid, index, column + let tmp = event.target + if (tmp.tagName.toUpperCase() != 'TD') tmp = query(tmp).closest('td')[0] + if (query(tmp).attr('col') != null) column = parseInt(query(tmp).attr('col')) + if (column == null) { + return + } + tmp = query(tmp).closest('tr')[0] + index = parseInt(query(tmp).attr('index')) + recid = self.records[index]?.recid + // new range + if (mv.newRange[1].recid == recid && mv.newRange[1].column == column) { + // if range did not change + return + } + const prevNewRange = w2utils.clone(mv.newRange) + mv.newRange = [{ recid: mv.recid, index: mv.index, column: mv.column }, { recid, index, column }] + // remember update ranges + detail.newName = letters[mv.column] + (mv.index + 1) + ':' + letters[column] + (index + 1) + detail.newRange = w2utils.clone(mv.newRange) + // event before + edata = self.trigger('selectionExtend', detail) + if (edata.isCancelled === true) { + mv.newRange = prevNewRange + detail.newRange = prevNewRange + return + } else { + // default behavior + self.addRange({ + name: 'selection-expand', + range: mv.newRange, + class: 'w2ui-selection-expand' + }) + } + } + function mouseStop(event) { + // default behavior + self.removeRange('selection-expand') + query('body').off('.w2ui-' + self.name) + // event after + if (self.last.move?.type == 'expand' && edata.finish) { + edata.finish() + } + delete self.last.move + } + } + select() { + if (arguments.length === 0) return 0 + let selected = 0 + const sel = this.last.selection + if (!this.multiSelect) this.selectNone(true) + // if too many arguments > 150k, then it errors off + let args = Array.from(arguments) + if (Array.isArray(args[0])) args = args[0] + // event before + const tmp = { target: this.name } + if (args.length == 1) { + tmp.multiple = false + if (w2utils.isPlainObject(args[0])) { + tmp.clicked = { + recid: args[0].recid, + column: args[0].column + } + } else { + tmp.recid = args[0] + } + } else { + tmp.multiple = true + tmp.clicked = { recids: args } + } + if (this.compareSelection(args).select.length == 0) { + // if all needed records are already selected + return + } + const edata = this.trigger('select', tmp) + if (edata.isCancelled === true) return 0 + // default action + if (this.selectType == 'row') { + for (let a = 0; a < args.length; a++) { + const recid = typeof args[a] === 'object' ? args[a].recid : args[a] + const index = this.get(recid, true) + if (index == null) continue + let recEl1 = null + let recEl2 = null + if (this.searchData.length !== 0 || (index + 1 >= this.last.vscroll.recIndStart && index + 1 <= this.last.vscroll.recIndEnd)) { + recEl1 = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(recid)) + recEl2 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(recid)) + } + if (this.selectType == 'row') { + if (sel.indexes.indexOf(index) != -1) continue + sel.indexes.push(index) + if (recEl1 && recEl2) { + recEl1.addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected') + recEl2.addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected') + recEl1.find('.w2ui-grid-select-check').prop('checked', true) + } + selected++ + } + } + } else { + // normalize for performance + const new_sel = {} + for (let a = 0; a < args.length; a++) { + const recid = typeof args[a] === 'object' ? args[a].recid : args[a] + const column = typeof args[a] === 'object' ? args[a].column : null + new_sel[recid] = new_sel[recid] || [] + if (Array.isArray(column)) { + new_sel[recid] = column + } else if (w2utils.isInt(column)) { + new_sel[recid].push(column) + } else { + for (let i = 0; i < this.columns.length; i++) { if (this.columns[i].hidden) continue; new_sel[recid].push(parseInt(i)) } + } + } + // add all + const col_sel = [] + for (const recid in new_sel) { + const index = this.get(recid, true) + if (index == null) continue + let recEl1 = null + let recEl2 = null + if (index + 1 >= this.last.vscroll.recIndStart && index + 1 <= this.last.vscroll.recIndEnd) { + recEl1 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(recid)) + recEl2 = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(recid)) + } + const s = sel.columns[index] || [] + // default action + if (sel.indexes.indexOf(index) == -1) { + sel.indexes.push(index) + } + // only only those that are new + for (let t = 0; t < new_sel[recid].length; t++) { + if (s.indexOf(new_sel[recid][t]) == -1) s.push(new_sel[recid][t]) + } + s.sort((a, b) => { return a - b }) // sort function must be for numerical sort + for (let t = 0; t < new_sel[recid].length; t++) { + const col = new_sel[recid][t] + if (col_sel.indexOf(col) == -1) col_sel.push(col) + if (recEl1) { + recEl1.find('#grid_' + this.name + '_data_' + index + '_' + col).addClass('w2ui-selected') + recEl1.find('.w2ui-col-number').addClass('w2ui-row-selected') + recEl1.find('.w2ui-grid-select-check').prop('checked', true) + } + if (recEl2) { + recEl2.find('#grid_' + this.name + '_data_' + index + '_' + col).addClass('w2ui-selected') + recEl2.find('.w2ui-col-number').addClass('w2ui-row-selected') + recEl2.find('.w2ui-grid-select-check').prop('checked', true) + } + selected++ + } + // save back to selection object + sel.columns[index] = s + } + // select columns (need here for speed) + for (let c = 0; c < col_sel.length; c++) { + query(this.box).find('#grid_' + this.name + '_column_' + col_sel[c] + ' .w2ui-col-header').addClass('w2ui-col-selected') + } + } + // need to sort new selection for speed + sel.indexes.sort((a, b) => { return a - b }) + // all selected? + const areAllSelected = (this.records.length > 0 && sel.indexes.length == this.records.length) + const areAllSearchedSelected = (sel.indexes.length > 0 && this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length) + if (areAllSelected || areAllSearchedSelected) { + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', true) + } else { + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', false) + } + this.status() + this.addRange('selection') + this.updateToolbar(sel, areAllSelected) + // event after + edata.finish() + return selected + } + unselect() { + let unselected = 0 + const sel = this.last.selection + // if too many arguments > 150k, then it errors off + let args = Array.from(arguments) + if (Array.isArray(args[0])) args = args[0] + // event before + const tmp = { target: this.name } + if (args.length == 1) { + tmp.multiple = false + if (w2utils.isPlainObject(args[0])) { + tmp.clicked = { + recid: args[0].recid, + column: args[0].column + } + } else { + tmp.clicked = { recid: args[0] } + } + } else { + tmp.multiple = true + tmp.recids = args + } + if (this.compareSelection(args).unselect.length == 0) { + // if all needed records are already unselected + return + } + const edata = this.trigger('select', tmp) + if (edata.isCancelled === true) return 0 + for (let a = 0; a < args.length; a++) { + const recid = typeof args[a] === 'object' ? args[a].recid : args[a] + const record = this.get(recid) + if (record == null) continue + const index = this.get(record.recid, true) + const recEl1 = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(recid)) + const recEl2 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(recid)) + if (this.selectType == 'row') { + if (sel.indexes.indexOf(index) == -1) continue + // default action + sel.indexes.splice(sel.indexes.indexOf(index), 1) + recEl1.removeClass('w2ui-selected w2ui-inactive').find('.w2ui-col-number').removeClass('w2ui-row-selected') + recEl2.removeClass('w2ui-selected w2ui-inactive').find('.w2ui-col-number').removeClass('w2ui-row-selected') + if (recEl1.length != 0) { + recEl1[0].style.cssText = 'height: ' + this.recordHeight + 'px; ' + recEl1.attr('custom_style') + recEl2[0].style.cssText = 'height: ' + this.recordHeight + 'px; ' + recEl2.attr('custom_style') + } + recEl1.find('.w2ui-grid-select-check').prop('checked', false) + unselected++ + } else { + const col = args[a].column + if (!w2utils.isInt(col)) { // unselect all columns + const cols = [] + for (let i = 0; i < this.columns.length; i++) { if (this.columns[i].hidden) continue; cols.push({ recid: recid, column: i }) } + return this.unselect(cols) + } + const s = sel.columns[index] + if (!Array.isArray(s) || s.indexOf(col) == -1) continue + // default action + s.splice(s.indexOf(col), 1) + query(this.box).find(`#grid_${this.name}_rec_${w2utils.escapeId(recid)} > td[col="${col}"]`).removeClass('w2ui-selected w2ui-inactive') + query(this.box).find(`#grid_${this.name}_frec_${w2utils.escapeId(recid)} > td[col="${col}"]`).removeClass('w2ui-selected w2ui-inactive') + // check if any row/column still selected + let isColSelected = false + let isRowSelected = false + const tmp = this.getSelection() + for (let i = 0; i < tmp.length; i++) { + if (tmp[i].column == col) isColSelected = true + if (tmp[i].recid == recid) isRowSelected = true + } + if (!isColSelected) { + query(this.box).find(`.w2ui-grid-columns td[col="${col}"] .w2ui-col-header, .w2ui-grid-fcolumns td[col="${col}"] .w2ui-col-header`).removeClass('w2ui-col-selected') + } + if (!isRowSelected) { + query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(recid)).find('.w2ui-col-number').removeClass('w2ui-row-selected') + } + unselected++ + if (s.length === 0) { + delete sel.columns[index] + sel.indexes.splice(sel.indexes.indexOf(index), 1) + recEl1.find('.w2ui-grid-select-check').prop('checked', false) + } + } + } + // all selected? + const areAllSelected = (this.records.length > 0 && sel.indexes.length == this.records.length) + const areAllSearchedSelected = (sel.indexes.length > 0 && this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length) + if (areAllSelected || areAllSearchedSelected) { + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', true) + } else { + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', false) + } + // show number of selected + this.status() + this.addRange('selection') + this.updateToolbar(sel, areAllSelected) + // event after + edata.finish() + return unselected + } + compareSelection(newSel) { + const sel = this.getSelection() + const select = [] + const unselect = [] + if (this.selectType == 'row') { + // normalize + newSel.forEach((sel, ind) => { + if (typeof sel === 'object') newSel[ind] = sel.recid + }) + // add items + for (let i = 0; i < newSel.length; i++) { + if (!sel.includes(newSel[i])) { + select.push(newSel[i]) + } + } + // remove items + for (let i = 0; i < newSel.length; i++) { + if (sel.includes(newSel[i])) { + unselect.push(newSel[i]) + } + } + } else { + // add more items + for (let ns = 0; ns < newSel.length; ns++) { + let flag = false + for (let s = 0; s < sel.length; s++) if (newSel[ns].recid == sel[s].recid && newSel[ns].column == sel[s].column) flag = true + if (!flag) select.push({ recid: newSel[ns].recid, column: newSel[ns].column }) + } + // remove items + for (let s = 0; s < sel.length; s++) { + let flag = false + for (let ns = 0; ns < newSel.length; ns++) if (newSel[ns].recid == sel[s].recid && newSel[ns].column == sel[s].column) flag = true + if (!flag) unselect.push({ recid: sel[s].recid, column: sel[s].column }) + } + } + return { select, unselect } + } + selectAll() { + const time = Date.now() + if (this.multiSelect === false) return + // default action + const url = this.url?.get ?? this.url + let sel = w2utils.clone(this.last.selection) + const cols = [] + for (let i = 0; i < this.columns.length; i++) cols.push(i) + // if local data source and searched + sel.indexes = [] + if (!url && this.searchData.length !== 0) { + // local search applied + for (let i = 0; i < this.last.searchIds.length; i++) { + sel.indexes.push(this.last.searchIds[i]) + if (this.selectType != 'row') sel.columns[this.last.searchIds[i]] = cols.slice() // .slice makes copy of the array + } + } else { + let buffered = this.records.length + if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length + for (let i = 0; i < buffered; i++) { + sel.indexes.push(i) + if (this.selectType != 'row') sel.columns[i] = cols.slice() // .slice makes copy of the array + } + } + // event before + const edata = this.trigger('select', { target: this.name, multiple: true, all: true, clicked: sel }) + if (edata.isCancelled === true) return + this.last.selection = sel + // add selected class + if (this.selectType == 'row') { + query(this.box).find('.w2ui-grid-records tr:not(.w2ui-empty-record)') + .addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected') + query(this.box).find('.w2ui-grid-frecords tr:not(.w2ui-empty-record)') + .addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected') + query(this.box).find('input.w2ui-grid-select-check').prop('checked', true) + } else { + query(this.box).find('.w2ui-grid-columns td .w2ui-col-header, .w2ui-grid-fcolumns td .w2ui-col-header').addClass('w2ui-col-selected') + query(this.box).find('.w2ui-grid-records tr .w2ui-col-number').addClass('w2ui-row-selected') + query(this.box).find('.w2ui-grid-records tr:not(.w2ui-empty-record)') + .find('.w2ui-grid-data:not(.w2ui-col-select)').addClass('w2ui-selected') + query(this.box).find('.w2ui-grid-frecords tr .w2ui-col-number').addClass('w2ui-row-selected') + query(this.box).find('.w2ui-grid-frecords tr:not(.w2ui-empty-record)') + .find('.w2ui-grid-data:not(.w2ui-col-select)').addClass('w2ui-selected') + query(this.box).find('input.w2ui-grid-select-check').prop('checked', true) + } + // enable/disable toolbar buttons + sel = this.getSelection(true) + this.addRange('selection') + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', true) + this.status() + this.updateToolbar({ indexes: sel }, true) + // event after + edata.finish() + return Date.now() - time + } + selectNone(skipEvent) { + const time = Date.now() + // event before + let edata + if (!skipEvent) { + edata = this.trigger('select', { target: this.name, clicked: [] }) + if (edata.isCancelled === true) return + } + // default action + const sel = this.last.selection + // remove selected class + if (this.selectType == 'row') { + query(this.box).find('.w2ui-grid-records tr.w2ui-selected').removeClass('w2ui-selected w2ui-inactive') + .find('.w2ui-col-number').removeClass('w2ui-row-selected') + query(this.box).find('.w2ui-grid-frecords tr.w2ui-selected').removeClass('w2ui-selected w2ui-inactive') + .find('.w2ui-col-number').removeClass('w2ui-row-selected') + query(this.box).find('input.w2ui-grid-select-check').prop('checked', false) + } else { + query(this.box).find('.w2ui-grid-columns td .w2ui-col-header, .w2ui-grid-fcolumns td .w2ui-col-header').removeClass('w2ui-col-selected') + query(this.box).find('.w2ui-grid-records tr .w2ui-col-number').removeClass('w2ui-row-selected') + query(this.box).find('.w2ui-grid-frecords tr .w2ui-col-number').removeClass('w2ui-row-selected') + query(this.box).find('.w2ui-grid-data.w2ui-selected').removeClass('w2ui-selected w2ui-inactive') + query(this.box).find('input.w2ui-grid-select-check').prop('checked', false) + } + sel.indexes = [] + sel.columns = {} + this.removeRange('selection') + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', false) + this.status() + this.updateToolbar(sel, false) + // event after + if (!skipEvent) { + edata.finish() + } + return Date.now() - time + } + updateToolbar(sel) { + const obj = this + const cnt = sel && sel.indexes ? sel.indexes.length : 0 + // if there is no toolbar + if (!this.toolbar.render) { + return + } + this.toolbar.items.forEach((item) => { + _checkItem(item, '') + if (Array.isArray(item.items)) { + item.items.forEach((it) => { + _checkItem(it, item.id + ':') + }) + } + }) + // enable/disable toolbar search button + if (this.show.toolbarSave) { + if (this.getChanges().length > 0) { + this.toolbar.enable('w2ui-save') + } else { + this.toolbar.disable('w2ui-save') + } + } + function _checkItem(item, prefix) { + if (item.batch != null) { + let enabled = false + if (item.batch === true) { + if (cnt > 0) enabled = true + } else if (typeof item.batch === 'number') { + if (cnt === item.batch) enabled = true + } else if (typeof item.batch === 'function') { + enabled = item.batch({ cnt, sel }) + } + if (enabled) { + obj.toolbar.enable(prefix + item.id) + } else { + obj.toolbar.disable(prefix + item.id) + } + } + } + } + getSelection(returnIndex) { + const ret = [] + const sel = this.last.selection + if (this.selectType == 'row') { + for (let i = 0; i < sel.indexes.length; i++) { + if (!this.records[sel.indexes[i]]) continue + if (returnIndex === true) ret.push(sel.indexes[i]); else ret.push(this.records[sel.indexes[i]].recid) + } + return ret + } else { + for (let i = 0; i < sel.indexes.length; i++) { + const cols = sel.columns[sel.indexes[i]] + if (!this.records[sel.indexes[i]]) continue + for (let j = 0; j < cols.length; j++) { + ret.push({ recid: this.records[sel.indexes[i]].recid, index: parseInt(sel.indexes[i]), column: cols[j] }) + } + } + return ret + } + } + search(field, value) { + const url = this.url?.get ?? this.url + const searchData = [] + let last_multi = this.last.multi + let last_logic = this.last.logic + let last_field = this.last.field + let last_search = this.last.search + let hasHiddenSearches = false + const overlay = query(`#w2overlay-${this.name}-search-overlay`) + // if emty sting, same as no search + if (value === '') value = null + // add hidden searches + for (let i = 0; i < this.searches.length; i++) { + if (!this.searches[i].hidden || this.searches[i].value == null) continue + searchData.push({ + field: this.searches[i].field, + operator: this.searches[i].operator || 'is', + type: this.searches[i].type, + value: this.searches[i].value || '' + }) + hasHiddenSearches = true + } + if (arguments.length === 0 && overlay.length === 0) { + if (this.multiSearch) { + field = this.searchData + value = this.last.logic + } else { + field = this.last.field + value = this.last.search + } + } + // 1: search() - advanced search (reads from popup) + if (arguments.length === 0 && overlay.length !== 0) { + this.focus() // otherwise search drop down covers searches + last_logic = overlay.find(`#grid_${this.name}_logic`).val() + last_search = '' + // advanced search + for (let i = 0; i < this.searches.length; i++) { + const search = this.searches[i] + const operator = overlay.find('#grid_' + this.name + '_operator_' + i).val() + const field1 = overlay.find('#grid_' + this.name + '_field_' + i) + const field2 = overlay.find('#grid_' + this.name + '_field2_' + i) + let value1 = field1.val() + let value2 = field2.val() + let svalue = null + let text = null + if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) { + const fld1 = field1[0]._w2field + const fld2 = field2[0]._w2field + if (fld1) value1 = fld1.clean(value1) + if (fld2) value2 = fld2.clean(value2) + } + if (['list', 'enum'].indexOf(search.type) != -1 || ['in', 'not in'].indexOf(operator) != -1) { + value1 = field1[0]._w2field.selected || {} + if (Array.isArray(value1)) { + svalue = [] + for (let j = 0; j < value1.length; j++) { + svalue.push(w2utils.isFloat(value1[j].id) ? parseFloat(value1[j].id) : String(value1[j].id).toLowerCase()) + delete value1[j].hidden + } + if (Object.keys(value1).length === 0) value1 = '' + } else { + text = value1.text || '' + value1 = value1.id || '' + } + } + if ((value1 !== '' && value1 != null) || (value2 != null && value2 !== '')) { + const tmp = { + field: search.field, + type: search.type, + operator: operator + } + if (operator == 'between') { + w2utils.extend(tmp, { value: [value1, value2] }) + } else if (operator == 'in' && typeof value1 === 'string') { + w2utils.extend(tmp, { value: value1.split(',') }) + } else if (operator == 'not in' && typeof value1 === 'string') { + w2utils.extend(tmp, { value: value1.split(',') }) + } else { + w2utils.extend(tmp, { value: value1 }) + } + if (svalue) w2utils.extend(tmp, { svalue: svalue }) + if (text) w2utils.extend(tmp, { text: text }) + // convert date to unix time + try { + if (search.type == 'date' && operator == 'between') { + tmp.value[0] = value1 // w2utils.isDate(value1, w2utils.settings.dateFormat, true).getTime(); + tmp.value[1] = value2 // w2utils.isDate(value2, w2utils.settings.dateFormat, true).getTime(); + } + if (search.type == 'date' && operator == 'is') { + tmp.value = value1 // w2utils.isDate(value1, w2utils.settings.dateFormat, true).getTime(); + } + } catch (e) { + } + searchData.push(tmp) + last_multi = true // if only hidden searches, then do not set + } + } + } + // 2: search(field, value) - regular search + if (typeof field === 'string') { + // if only one argument - search all + if (arguments.length == 1) { + value = field + field = 'all' + } + last_field = field + last_search = value + last_multi = false + last_logic = (hasHiddenSearches ? 'AND' : 'OR') + // loop through all searches and see if it applies + if (value != null) { + if (field.toLowerCase() == 'all') { + // if there are search fields loop thru them + if (this.searches.length > 0) { + for (let i = 0; i < this.searches.length; i++) { + const search = this.searches[i] + if (search.type == 'text' || (search.type == 'alphanumeric' && w2utils.isAlphaNumeric(value)) || + (search.type == 'int' && w2utils.isInt(value)) || (search.type == 'float' && w2utils.isFloat(value)) || + (search.type == 'percent' && w2utils.isFloat(value)) || ((search.type == 'hex' || search.type == 'color') && w2utils.isHex(value)) || + (search.type == 'currency' && w2utils.isMoney(value)) || (search.type == 'money' && w2utils.isMoney(value)) || + (search.type == 'date' && w2utils.isDate(value)) || (search.type == 'time' && w2utils.isTime(value)) || + (search.type == 'datetime' && w2utils.isDateTime(value)) || (search.type == 'datetime' && w2utils.isDate(value)) || + (search.type == 'enum' && w2utils.isAlphaNumeric(value)) || (search.type == 'list' && w2utils.isAlphaNumeric(value)) + ) { + const def = this.defaultOperator[this.operatorsMap[search.type]] + const tmp = { + field: search.field, + type: search.type, + operator: (search.operator != null ? search.operator : def), + value: value + } + if (String(value).trim() != '') searchData.push(tmp) + } + // range in global search box + if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) { + const t = String(value).trim().split('-').map(v => v.trim()).filter(v => w2utils.isFloat(v)) + if (t.length == 2) { + const tmp = { + field: search.field, + type: search.type, + operator: 'between', + value: [t[0], t[1]] + } + searchData.push(tmp) + } + } + // lists fields + if (['list', 'enum'].indexOf(search.type) != -1) { + const new_values = [] + if (search.options == null) search.options = {} + if (!Array.isArray(search.options.items)) search.options.items = [] + for (let j = 0; j < search.options.items; j++) { + const tmp = search.options.items[j] + try { + const re = new RegExp(value, 'i') + if (re.test(tmp)) new_values.push(j) + if (tmp.text && re.test(tmp.text)) new_values.push(tmp.id) + } catch (e) {} + } + if (new_values.length > 0) { + const tmp = { + field: search.field, + type: search.type, + operator: (search.operator != null ? search.operator : 'in'), + value: new_values + } + searchData.push(tmp) + } + } + } + } else { + // no search fields, loop thru columns + for (let i = 0; i < this.columns.length; i++) { + const tmp = { + field: this.columns[i].field, + type: 'text', + operator: this.defaultOperator.text, + value: value + } + searchData.push(tmp) + } + } + /** + * If user searched ALL field and there was no matching searches then add a bogus field, so that no result will be + * shown. Otherwise search string is not empty, but no fields is actually applied and all fields are shown + */ + if (searchData.length == 0) { + const tmp = { + field: 'All', + type: 'text', + operator: this.defaultOperator.text, + value: value + } + searchData.push(tmp) + } + } else { + const el = overlay.find('#grid_' + this.name + '_search_all') + let search = this.getSearch(field) + if (search == null) search = { field: field, type: 'text' } + if (search.field == field) this.last.label = search.label + if (value !== '') { + let op = this.defaultOperator[this.operatorsMap[search.type]] + let val = value + if (['date', 'time', 'datetime'].indexOf(search.type) != -1) op = 'is' + if (['list', 'enum'].indexOf(search.type) != -1) { + op = 'is' + const tmp = el._w2field?.get() + if (tmp && Object.keys(tmp).length > 0) val = tmp.id; else val = '' + } + if (search.type == 'int' && value !== '') { + op = 'is' + if (String(value).indexOf('-') != -1) { + const tmp = value.split('-') + if (tmp.length == 2) { + op = 'between' + val = [parseInt(tmp[0]), parseInt(tmp[1])] + } + } + if (String(value).indexOf(',') != -1) { + const tmp = value.split(',') + op = 'in' + val = [] + for (let i = 0; i < tmp.length; i++) val.push(tmp[i]) + } + } + if (search.operator != null) op = search.operator + const tmp = { + field: search.field, + type: search.type, + operator: op, + value: val + } + searchData.push(tmp) + } + } + } + } + // 3: search([{ field, value, [operator,] [type] }, { field, value, [operator,] [type] } ], logic) - submit whole structure + if (Array.isArray(field)) { + let logic = 'AND' + if (typeof value === 'string') { + logic = value.toUpperCase() + if (logic != 'OR' && logic != 'AND') logic = 'AND' + } + last_search = '' + last_multi = true + last_logic = logic + for (let i = 0; i < field.length; i++) { + const data = field[i] + if (typeof data.value === 'number' && data.operator == null) data.operator = this.defaultOperator.number + if (typeof data.value === 'string' && data.operator == null) data.operator = this.defaultOperator.text + if (Array.isArray(data.value) && data.operator == null) data.operator = this.defaultOperator.enum + if (w2utils.isDate(data.value) && data.operator == null) data.operator = this.defaultOperator.date + // merge current field and search if any + searchData.push(data) + } + } + // event before + const edata = this.trigger('search', { + target: this.name, + multi: (arguments.length === 0), + searchField: (field || 'multi'), + searchValue: (field ? value : 'multi'), + searchData: searchData, + searchLogic: last_logic + }) + if (edata.isCancelled === true) return + // default action + this.searchData = edata.detail.searchData + this.last.field = last_field + this.last.search = last_search + this.last.multi = last_multi + this.last.logic = edata.detail.searchLogic + this.last.vscroll.scrollTop = 0 + this.last.vscroll.scrollLeft = 0 + this.last.selection.indexes = [] + this.last.selection.columns = {} + // -- clear all search field + this.searchClose() + // apply search + if (url) { + this.last.fetch.offset = 0 + this.reload() + } else { + // local search + this.localSearch() + this.refresh() + } + // event after + edata.finish() + } + // open advanced search popover + searchOpen() { + if (!this.box) return + if (this.searches.length === 0) return + // event before + const edata = this.trigger('searchOpen', { target: this.name }) + if (edata.isCancelled === true) { + return + } + const $btn = query(this.toolbar.box).find('.w2ui-grid-search-input .w2ui-search-drop') + $btn.addClass('checked') + // show search + w2tooltip.show({ + name: this.name + '-search-overlay', + anchor: query(this.box).find('#grid_' + this.name + '_search_all').get(0), + position: 'bottom|top', + html: this.getSearchesHTML(), + align: 'left', + arrowSize: 12, + class: 'w2ui-grid-search-advanced', + hideOn: ['doc-click'] + }) + .then(event => { + this.initSearches() + this.last.search_opened = true + const overlay = query(`#w2overlay-${this.name}-search-overlay`) + overlay + .data('gridName', this.name) + .off('.grid-search') + .on('click.grid-search', () => { + // hide any tooltip opened by searches + overlay.find('input, select').each(el => { + const names = query(el).data('tooltipName') + if (names) { + names.forEach(name => { + w2tooltip.hide(name) + }) + } + }) + }) + w2utils.bindEvents(overlay.find('select, input, button'), this) + // init first field + const sfields = query(`#w2overlay-${this.name}-search-overlay *[rel=search]`) + if (sfields.length > 0) sfields[0].focus() + // event after + edata.finish() + }) + .hide(event => { + $btn.removeClass('checked') + this.last.search_opened = false + }) + } + searchClose() { + w2tooltip.hide(this.name + '-search-overlay') + } + // if clicked on a field in the search strip + searchFieldTooltip(ind, sd_ind, el) { + const sf = this.searches[ind] + const sd = this.searchData[sd_ind] + let oper = sd.operator + if (oper == 'more' && sd.type == 'date') oper = 'since' + if (oper == 'less' && sd.type == 'date') oper = 'before' + let options = '' + let val = sd.value + if (Array.isArray(sd.value)) { // && Array.isArray(sf.options.items)) { + sd.value.forEach(opt => { + options += `${opt.text || opt}` + }) + if (sd.type == 'date') { + options = '' + sd.value.forEach(opt => { + options += `${w2utils.formatDate(opt)}` + }) + } + } else { + if (sd.type == 'date') { + val = w2utils.formatDateTime(val) + } + } + w2tooltip.hide(this.name + '-search-props') + w2tooltip.show({ + name: this.name + '-search-props', + anchor: el, + class: 'w2ui-white', + hideOn: 'doc-click', + html: ` +
+ ${sf.label} + ${w2utils.lang(oper)} + ${Array.isArray(sd.value) + ? `${options}` + : `${val}` +} +
+ +
+
` + }).then(event => { + query(event.detail.overlay.box).find('#remove').on('click', () => { + this.searchData.splice(`${sd_ind}`, 1) + this.reload() + this.localSearch() + w2tooltip.hide(this.name + '-search-props') + }) + }) + } + // drop down with save searches + searchSuggest(imediate, forceHide, input) { + clearTimeout(this.last.kbd_timer) + clearTimeout(this.last.overlay_timer) + this.searchShowFields(true) + this.searchClose() + if (forceHide === true) { + w2tooltip.hide(this.name + '-search-suggest') + return + } + if (query(`#w2overlay-${this.name}-search-suggest`).length > 0) { + // already shown + return + } + if (!imediate) { + this.last.overlay_timer = setTimeout(() => { this.searchSuggest(true) }, 100) + return + } + const el = query(this.box).find(`#grid_${this.name}_search_all`).get(0) + const searches = [ + ...this.defaultSearches ?? [], + ...this.defaultSearches?.length > 0 && this.savedSearches?.length > 0 ? ['--'] : [], + ...this.savedSearches ?? [] + ] + if (Array.isArray(searches) && searches.length > 0) { + w2menu.show({ + name: this.name + '-search-suggest', + anchor: el, + align: 'both', + items: searches, + selected: false, + filter: true, + hideOn: ['doc-click', 'sleect', 'remove'], + render(item) { + let ret = item.text + if (item.isDefault) ret = `${ret}` + return ret + } + }) + .select(event => { + const edata = this.trigger('searchSelect', { + target: this.name, + index: event.detail.index, + item: event.detail.item + }) + if (edata.isCancelled === true) { + event.preventDefault() + return + } + event.detail.overlay.hide() + this.last.logic = event.detail.item.logic || 'AND' + this.last.search = '' + this.last.label = '[Multiple Fields]' + this.searchData = w2utils.clone(event.detail.item.data) + this.searchSelected = w2utils.clone(event.detail.item, { exclude: ['icon', 'remove'] }) + this.reload() + edata.finish() + }) + .remove(event => { + const item = event.detail.item + const edata = this.trigger('searchRemove', { target: this.name, index: event.detail.index, item }) + if (edata.isCancelled === true) { + event.preventDefault() + return + } + queueMicrotask(() => event.detail.overlay.hide()) + this.confirm(w2utils.lang('Do you want to delete search "${item}"?', { item: item.text })) + .yes(evt => { + // remove from searches + const search = this.savedSearches.findIndex((s) => s.id == item.id) + if (search !== -1) { + this.savedSearches.splice(search, 1) + } + this.cacheSave('searches', this.savedSearches.map(s => w2utils.clone(s, { exclude: ['remove', 'icon'] }))) + evt.detail.self.close() + // evt after + edata.finish() + }) + .no(evt => { + evt.detail.self.close() + }) + }) + } + } + searchSave() { + let value = '' + if (this.searchSelected) { + value = this.searchSelected.text + } + const ind = this.savedSearches.findIndex(s => { return s.id == this.searchSelected?.id }) + // event before + const edata = this.trigger('searchSave', { target: this.name, saveLocalStorage: true }) + if (edata.isCancelled === true) return + this.message({ + width: 350, + height: 150, + body: ``, + buttons: ` + + + ` + }).open(async(event) => { + query(event.detail.box).find('input, button').eq(0).val(value) + await event.complete + query(event.detail.box).find('#grid-search-cancel').on('click', () => { + this.message() + }) + query(event.detail.box).find('#grid-search-save').on('click', () => { + const input = query(event.detail.box).find('.w2ui-message .search-name') + const name = input.val() + // save in savedSearches + if (this.searchSelected && ind != -1) { + Object.assign(this.savedSearches[ind], { + id: name, + text: name, + logic: this.last.logic, + data: w2utils.clone(this.searchData) + }) + } else { + this.savedSearches.push({ + id: name, + text: name, + icon: 'w2ui-icon-search', + remove: true, + logic: this.last.logic, + data: this.searchData + }) + } + // save local storage + this.cacheSave('searches', this.savedSearches.map(s => w2utils.clone(s, { exclude: ['remove', 'icon'] }))) + this.message() + // update on screen + if (this.searchSelected) { + this.searchSelected.text = name + query(this.box).find(`#grid_${this.name}_search_name .name-text`).html(name) + } else { + this.searchSelected = { + text: name, + logic: this.last.logic, + data: w2utils.clone(this.searchData) + } + query(event.detail.box).find(`#grid_${this.name}_search_all`).val(' ').prop('readOnly', true) + query(event.detail.box).find(`#grid_${this.name}_search_name`).show().find('.name-text').html(name) + } + edata.finish({ name }) + }) + await w2utils.wait(100) // need this for dialog to be ready (sliding down) for focus to work + query(event.detail.box).find('input, button') + .off('.message') + .on('keydown.message', evt => { + const val = String(query(event.detail.box).find('.w2ui-message-body input').val()).trim() + if (evt.keyCode == 13 && val != '') { + query(event.detail.box).find('#grid-search-save').trigger('click') // enter + } + if (evt.keyCode == 27) { // escape + this.message() + } + }) + .eq(0) + .on('input.message', evt => { + const $save = query(event.detail.box).closest('.w2ui-message').find('#grid-search-save') + if (String(query(event.detail.box).val()).trim() === '') { + $save.prop('disabled', true) + } else { + $save.prop('disabled', false) + } + }) + .get(0) + .focus() + }) + } + cache(type) { + if (w2utils.hasLocalStorage && this.useLocalStorage) { + try { + const data = JSON.parse(localStorage.w2ui || '{}') + data[(this.stateId || this.name)] ??= {} + return data[(this.stateId || this.name)][type] + } catch (e) { + } + } + return null + } + cacheSave(type, value) { + if (w2utils.hasLocalStorage && this.useLocalStorage) { + try { + const data = JSON.parse(localStorage.w2ui || '{}') + data[(this.stateId || this.name)] ??= {} + data[(this.stateId || this.name)][type] = value + localStorage.w2ui = JSON.stringify(data) + return true + } catch (e) { + delete localStorage.w2ui + } + } + return false + } + searchReset(noReload) { + const searchData = [] + let hasHiddenSearches = false + // add hidden searches + for (let i = 0; i < this.searches.length; i++) { + if (!this.searches[i].hidden || this.searches[i].value == null) continue + searchData.push({ + field: this.searches[i].field, + operator: this.searches[i].operator || 'is', + type: this.searches[i].type, + value: this.searches[i].value || '' + }) + hasHiddenSearches = true + } + // event before + const edata = this.trigger('search', { reset: true, target: this.name, searchData: searchData }) + if (edata.isCancelled === true) return + // default action + const input = query(this.box).find('#grid_' + this.name + '_search_all') + this.searchData = edata.detail.searchData + this.searchSelected = null + this.last.search = '' + this.last.logic = (hasHiddenSearches ? 'AND' : this.last.logic) + // advanced search button + if (this.multiSearch) { + input.next().show() + } else { + input.next().hide() + } + this.last.multi = false + this.last.fetch.offset = 0 + // reset scrolling position + this.last.vscroll.scrollTop = 0 + this.last.vscroll.scrollLeft = 0 + this.last.selection.indexes = [] + this.last.selection.columns = {} + // -- clear all search field + this.searchClose() + const all = input.val('').get(0) + if (all?._w2field) { all._w2field.reset() } + // apply search + if (!noReload) { + this.reload() + } + // event after + edata.finish() + } + searchShowFields(forceHide) { + if (forceHide === true) { + w2tooltip.hide(this.name + '-search-fields') + return + } + const items = [] + for (let s = -1; s < this.searches.length; s++) { + let search = this.searches[s] + const sField = (search ? search.field : null) + const column = this.getColumn(sField) + let disabled = false + let tooltip = null + if (this.show.searchHiddenMsg == true && s != -1 && + (column == null || (column.hidden === true && column.hideable !== false))) { + disabled = true + tooltip = w2utils.lang(`This column ${column == null ? 'does not exist' : 'is hidden'}`) + } + if (s == -1) { // -1 is All Fields search + if (!this.multiSearch || !this.show.searchAll) continue + search = { field: 'all', label: 'All Fields' } + } else { + if (column != null && column.hideable === false) continue + if (search.hidden === true) { + tooltip = w2utils.lang('This column is hidden') + // don't show hidden (not simple) searches + if (search.simple === false) continue + } + } + if (search.label == null && search.caption != null) { + console.log('NOTICE: grid search.caption property is deprecated, please use search.label. Search ->', search) + search.label = search.caption + } + items.push({ + id: search.field, + text: w2utils.lang(search.label), + search, + tooltip, + disabled, + checked: (search.field == this.last.field) + }) + } + w2menu.show({ + type: 'radio', + name: this.name + '-search-fields', + anchor: query(this.box).find('#grid_' + this.name + '_search_name').parent().find('.w2ui-search-down').get(0), + items, + align: 'none', + hideOn: ['doc-click', 'select'] + }) + .select(event => { + this.searchInitInput(event.detail.item.search.field) + }) + } + searchInitInput(field, value) { + let search + const el = query(this.box).find('#grid_' + this.name + '_search_all') + if (field == 'all') { + search = { field: 'all', label: w2utils.lang('All Fields') } + } else { + search = this.getSearch(field) + if (search == null) return + } + // update field + if (this.last.search != '') { + this.last.label = search.label + this.search(search.field, this.last.search) + } else { + this.last.field = search.field + this.last.label = search.label + } + el.attr('placeholder', w2utils.lang('Search') + ' ' + w2utils.lang(search.label || search.caption || search.field, true)) + // if there is pre-selected search + if (this.searchSelected) { + query(this.box).find(`#grid_${this.name}_search_all`).val(' ').prop('readOnly', true) + query(this.box).find(`#grid_${this.name}_search_name`).show().find('.name-text').html(this.searchSelected.text) + } else { + query(this.box).find(`#grid_${this.name}_search_all`).prop('readOnly', false) + query(this.box).find(`#grid_${this.name}_search_name`).hide().find('.name-text').html('') + } + } + // clears records and related params + clear(noRefresh) { + this.total = 0 + this.records = [] + this.summary = [] + this.last.fetch.offset = 0 // need this for reload button to work on remote data set + this.last.idCache = {} // optimization to free memory + this.last.selection = { indexes: [], columns: {}} + this.reset(true) + // refresh + if (!noRefresh) this.refresh() + } + // clears scroll position, selection, ranges + reset(noRefresh) { + // position + this.last.vscroll.scrollTop = 0 + this.last.vscroll.scrollLeft = 0 + this.last.vscroll.recIndStart = null + this.last.vscroll.recIndEnd = null + // additional + query(this.box).find(`#grid_${this.name}_records`).prop('scrollTop', 0) + // refresh + if (!noRefresh) this.refresh() + } + skip(offset, callBack) { + const url = this.url?.get ?? this.url + if (url) { + this.offset = parseInt(offset) + if (this.offset > this.total) this.offset = this.total - this.limit + if (this.offset < 0 || !w2utils.isInt(this.offset)) this.offset = 0 + this.clear(true) + this.reload(callBack) + } else { + console.log('ERROR: grid.skip() can only be called when you have remote data source.') + } + } + load(url, callBack) { + if (url == null) { + console.log('ERROR: You need to provide url argument when calling .load() method of "' + this.name + '" object.') + return new Promise((resolve, reject) => { reject() }) + } + // default action + this.clear(true) + return this.request('load', {}, url, callBack) + } + reload(callBack) { + const grid = this + const url = this.url?.get ?? this.url + grid.selectionSave() + if (url) { + // need to remember selection (not just last.selection object) + return this.load(url, () => { + grid.selectionRestore() + if (typeof callBack === 'function') callBack() + }) + } else { + this.reset(true) + this.localSearch() + this.selectionRestore() + if (typeof callBack === 'function') callBack({ status: 'success' }) + return new Promise(resolve => { resolve() }) + } + } + request(action, postData, url, callBack) { + const self = this + let resolve, reject + const requestProm = new Promise((res, rej) => { resolve = res; reject = rej }) + if (postData == null) postData = {} + if (!url) url = this.url + if (!url) return new Promise((resolve, reject) => { reject() }) + // build parameters list + if (!w2utils.isInt(this.offset)) this.offset = 0 + if (!w2utils.isInt(this.last.fetch.offset)) this.last.fetch.offset = 0 + // add list params + let edata + const params = { + limit: this.limit, + offset: parseInt(this.offset) + parseInt(this.last.fetch.offset), + searchLogic: this.last.logic, + search: this.searchData.map((search) => { + const _search = w2utils.clone(search) + if (this.searchMap && this.searchMap[_search.field]) _search.field = this.searchMap[_search.field] + return _search + }), + sort: this.sortData.map((sort) => { + const _sort = w2utils.clone(sort) + if (this.sortMap && this.sortMap[_sort.field]) _sort.field = this.sortMap[_sort.field] + return _sort + }) + } + if (this.searchData.length === 0) { + delete params.search + delete params.searchLogic + } + if (this.sortData.length === 0) { + delete params.sort + } + // append other params + w2utils.extend(params, this.postData) + w2utils.extend(params, postData) + // other actions + if (action == 'delete' || action == 'save') { + delete params.limit + delete params.offset + params.action = action + if (action == 'delete') { + params[this.recid || 'recid'] = this.getSelection() + } + } + // event before + if (action == 'load') { + edata = this.trigger('request', { target: this.name, url, postData: params, httpMethod: 'GET', + httpHeaders: this.httpHeaders }) + if (edata.isCancelled === true) return new Promise((resolve, reject) => { reject() }) + } else { + edata = { detail: { + url, + postData: params, + httpMethod: action == 'save' ? 'PUT' : 'DELETE', + httpHeaders: this.httpHeaders + }} + } + // call server to get data + if (this.last.fetch.offset === 0) { + this.lock(w2utils.lang(this.msgRefresh), true) + } + if (this.last.fetch.controller) try { this.last.fetch.controller.abort() } catch (e) {} + // URL + url = edata.detail.url + switch (action) { + case 'save': + if (url?.save) url = url.save + break + case 'delete': + if (url?.remove) url = url.remove + break + default: + url = url?.get ?? url + } + // process url with routeData + if (Object.keys(this.routeData).length > 0) { + const info = w2utils.parseRoute(url) + if (info.keys.length > 0) { + for (let k = 0; k < info.keys.length; k++) { + if (this.routeData[info.keys[k].name] == null) continue + url = url.replace((new RegExp(':' + info.keys[k].name, 'g')), this.routeData[info.keys[k].name]) + } + } + } + url = new URL(url, location) + // ajax options + const fetchOptions = w2utils.prepareParams(url, { + method: edata.detail.httpMethod, + headers: edata.detail.httpHeaders, + body: edata.detail.postData + }, { dataType: this.dataType, caller: this, action }) + Object.assign(this.last.fetch, { + action: action, + options: fetchOptions, + controller: new AbortController(), + start: Date.now(), + loaded: false + }) + fetchOptions.signal = this.last.fetch.controller.signal + fetch(url, fetchOptions) + .catch(processError) + .then(resp => { + if (resp == null) return // request aborted + if (resp?.status != 200) { + processError(resp ?? {}) + return + } + resp.json() + .catch(processError) + .then(data => { + this.requestComplete(data ?? {}, action, callBack, resolve, reject) + }) + .finally(() => self.unlock()) + }) + if (action == 'load') { + // event after + edata.finish() + } + return requestProm + function processError(response) { + if (response?.name === 'AbortError') { + // request was aborted by the grid + return + } + self.unlock() + // trigger event + const edata2 = self.trigger('error', { response, lastFetch: self.last.fetch }) + if (edata2.isCancelled === true) return + // default behavior + if (response.status && response.status != 200) { + response.json().then((data) => { + self.error(response.status + ': ' + data.message ?? response.statusText) + }).catch(() => { + self.error(response.status + ': ' + response.statusText) + }) + } else { + console.log('ERROR: Server communication failed.', + '\n EXPECTED:', { total: 5, records: [{ recid: 1, field: 'value' }] }, + '\n OR:', { error: true, message: 'error message' }) + self.requestComplete({ error: true, message: w2utils.lang(self.msgHTTPError), response }, action, callBack, resolve, reject) + } + // event after + edata2.finish() + } + } + requestComplete(data, action, callBack, resolve, reject) { + let error = data.error ?? false + if (data.error == null && data.status === 'error') error = true + this.last.fetch.response = (Date.now() - this.last.fetch.start) / 1000 + setTimeout(() => { + if (this.show.statusResponse) { + this.status(w2utils.lang('Server Response ${count} seconds', { count: this.last.fetch.response })) + } + }, 10) + this.last.vscroll.pull_more = false + this.last.vscroll.pull_refresh = true + // event before + let event_name = 'load' + if (this.last.fetch.action == 'save') event_name = 'save' + if (this.last.fetch.action == 'delete') event_name = 'delete' + const edata = this.trigger(event_name, { target: this.name, error, data, lastFetch: this.last.fetch }) + if (edata.isCancelled === true) { + reject() + return + } + // parse server response + if (!error) { + // default action + if (typeof this.parser === 'function') { + data = this.parser(data) + if (typeof data !== 'object') { + console.log('ERROR: Your parser did not return proper object') + } + } else { + if (data == null) { + data = { + error: true, + message: w2utils.lang(this.msgNotJSON) + } + } else if (Array.isArray(data)) { + // if it is plain array, assume these are records + data = { + error, + records: data, + total: data.length + } + } + } + if (action == 'load') { + if (data.total == null) data.total = -1 + if (data.records == null) { + data.records = [] + } + if (data.records.length == this.limit) { + const loaded = this.records.length + data.records.length + this.last.fetch.hasMore = (loaded != this.total) + } else { + this.last.fetch.hasMore = false + this.total = this.offset + this.last.fetch.offset + data.records.length + } + if (!this.last.fetch.hasMore) { + // if no more records, then hide spinner + query(this.box).find('#grid_' + this.name + '_rec_more, #grid_' + this.name + '_frec_more').hide() + } + if (this.last.fetch.offset === 0) { + this.records = [] + this.summary = [] + } else { + if (data.total != -1 && parseInt(data.total) != parseInt(this.total)) { + const grid = this + this.message(w2utils.lang(this.msgNeedReload)) + .ok(() => { + delete grid.last.fetch.offset + grid.reload() + }) + return new Promise(resolve => { resolve() }) + } + } + if (w2utils.isInt(data.total)) this.total = parseInt(data.total) + // records + if (data.records) { + data.records.forEach(rec => { + if (this.recid) { + rec.recid = this.parseField(rec, this.recid) + } + if (rec.recid == null) { + rec.recid = 'recid-' + this.records.length + } + if (rec.w2ui?.summary === true) { + this.summary.push(rec) + } else { + this.records.push(rec) + } + }) + } + // summary records (if any) + if (data.summary) { + this.summary = [] // reset summary with each call + data.summary.forEach(rec => { + if (this.recid) { + rec.recid = this.parseField(rec, this.recid) + } + if (rec.recid == null) { + rec.recid = 'recid-' + this.summary.length + } + this.summary.push(rec) + }) + } + } else if (action == 'delete') { + this.reset() // unselect old selections + return this.reload() + } + } else { + this.error(w2utils.lang(data.message ?? this.msgServerError)) + reject(data) + } + // event after + const url = this.url?.get ?? this.url + if (!url) { + this.localSort() + this.localSearch() + } + this.total = parseInt(this.total) + // do not refresh if loading on infinite scroll + if (this.last.fetch.offset === 0) { + this.refresh() + } else { + this.scroll() + this.resize() + } + // call back + if (typeof callBack === 'function') callBack(data) // need to be before event:after + resolve(data) + // after event + edata.finish() + this.last.fetch.loaded = true + } + error(msg) { + // let the management of the error outside of the grid + const edata = this.trigger('error', { target: this.name, message: msg }) + if (edata.isCancelled === true) { + return + } + this.message(msg) + // event after + edata.finish() + } + getChanges(recordsBase) { + const changes = [] + if (typeof recordsBase === 'undefined') { + recordsBase = this.records + } + for (let r = 0; r < recordsBase.length; r++) { + const rec = recordsBase[r] + if (rec?.w2ui) { + if (rec.w2ui.changes != null) { + const obj = {} + obj[this.recid || 'recid'] = rec.recid + changes.push(w2utils.extend(obj, rec.w2ui.changes)) + } + // recursively look for changes in non-expanded children + if (rec.w2ui.expanded !== true && rec.w2ui.children && rec.w2ui.children.length) { + changes.push(...this.getChanges(rec.w2ui.children)) + } + } + } + return changes + } + mergeChanges() { + const changes = this.getChanges() + for (let c = 0; c < changes.length; c++) { + const record = this.get(changes[c][this.recid || 'recid']) + for (const s in changes[c]) { + if (s == 'recid' || (this.recid && s == this.recid)) continue // do not allow to change recid + if (typeof changes[c][s] === 'object') changes[c][s] = changes[c][s].text + try { + _setValue(record, s, changes[c][s]) + } catch (e) { + console.log('ERROR: Cannot merge. ', e.message || '', e) + } + if (record.w2ui) delete record.w2ui.changes + } + } + this.refresh() + function _setValue(obj, field, value) { + const fld = field.split('.') + if (fld.length == 1) { + obj[field] = value + } else { + obj = obj[fld[0]] + fld.shift() + _setValue(obj, fld.join('.'), value) + } + } + } + save(callBack) { + const changes = this.getChanges() + const url = this.url?.save ?? this.url + // event before + const edata = this.trigger('save', { target: this.name, changes: changes }) + if (edata.isCancelled === true) return + if (url) { + this.request('save', { 'changes': edata.detail.changes }, null, + (data) => { + if (!data.error) { + // only merge changes, if save was successful + this.mergeChanges() + } + // event after + edata.finish() + // call back + if (typeof callBack === 'function') callBack(data) + } + ) + } else { + this.mergeChanges() + // event after + edata.finish() + if (typeof callBack === 'function') callBack(edata) + } + } + editField(recid, column, value, event) { + const self = this + if (this.last.inEditMode === true) { + // This is triggerign when user types fast + if (event && event.keyCode == 13) { + const { index, column, value } = this.last._edit + this.editChange({ type: 'custom', value }, index, column, event) + this.editDone(index, column, event) + } else { + // when 2 chars entered fast (spreadsheet) + const input = query(this.box).find('div.w2ui-edit-box .w2ui-input') + if (input.length > 0) { + if (input.get(0).tagName == 'DIV') { + input.text(input.text() + value) + w2utils.setCursorPosition(input.get(0), input.text().length) + } else { + input.val(input.val() + value) + w2utils.setCursorPosition(input.get(0), input.val().length) + } + } + } + return + } + const index = this.get(recid, true) + const edit = this.getCellEditable(index, column) + if (!edit || ['checkbox', 'check'].includes(edit.type)) return + const rec = this.records[index] + const col = this.columns[column] + const prefix = (col.frozen === true ? '_f' : '_') + if (['enum', 'file'].indexOf(edit.type) != -1) { + console.log('ERROR: input types "enum" and "file" are not supported in inline editing.') + return + } + // event before + const edata = this.trigger('editField', { target: this.name, recid, column, value, index, originalEvent: event }) + if (edata.isCancelled === true) return + value = edata.detail.value + // default behaviour + this.last.inEditMode = true + this.last.editColumn = column + this.last._edit = { value: value, index: index, column: column, recid: recid } + this.selectNone(true) // no need to trigger select event + this.select({ recid: recid, column: column }) + // create input element + const tr = query(this.box).find('#grid_' + this.name + prefix + 'rec_' + w2utils.escapeId(recid)) + let div = tr.find('[col="' + column + '"] > div') // TD -> DIV + this.last._edit.tr = tr + this.last._edit.div = div + // clear previous if any (spreadsheet) + query(this.box).find('div.w2ui-edit-box').remove() + // for spreadsheet - insert into selection + if (this.selectType != 'row') { + query(this.box).find('#grid_' + this.name + prefix + 'selection') + .attr('id', 'grid_' + this.name + '_editable') + .removeClass('w2ui-selection') + .addClass('w2ui-edit-box') + .prepend('
') + .find('.w2ui-selection-resizer') + .remove() + div = query(this.box).find('#grid_' + this.name + '_editable > div:first-child') + } + edit.attr = edit.attr ?? '' + edit.text = edit.text ?? '' + edit.style = edit.style ?? '' + edit.items = edit.items ?? [] + let val = (rec.w2ui?.changes?.[col.field] != null + ? w2utils.stripTags(rec.w2ui.changes[col.field]) + : w2utils.stripTags(self.parseField(rec, col.field))) + if (val == null) val = '' + let prevValue = (typeof val !== 'object' ? val : '') + if (edata.detail.prevValue != null) prevValue = edata.detail.prevValue + if (value != null) val = value + let addStyle = (col.style != null ? col.style + ';' : '') + if (typeof col.render === 'string') { + const tmp = col.render.replace('|', ':').split(':') + if (['number', 'int', 'float', 'money', 'currency', 'percent', 'size'].includes(tmp[0])) { + addStyle += 'text-align: right;' + } + } + // normalize items, if not yet normlized + if (edit.items.length > 0 && !w2utils.isPlainObject(edit.items[0])) { + edit.items = w2utils.normMenu(edit.items, edit) + } + let input + const dropTypes = ['date', 'time', 'datetime', 'color', 'list', 'combo'] + const styles = getComputedStyle(tr.find('[col="' + column + '"] > div').get(0)) + const font = `font-family: ${styles['font-family']}; font-size: ${styles['font-size']};` + switch (edit.type) { + case 'div': { + div.addClass('w2ui-editable') + .html(w2utils.stripSpaces(`
+
${edit.text}`)) + input = div.find('div.w2ui-input').get(0) + input.innerText = (typeof val !== 'object' ? val : '') + if (value != null) { + w2utils.setCursorPosition(input, input.innerText.length) + } else { + w2utils.setCursorPosition(input, 0, input.innerText.length) + } + break + } + default: { + div.addClass('w2ui-editable') + .html(w2utils.stripSpaces(`${edit.text}`)) + input = div.find('input').get(0) + // issue #499 + if (edit.type == 'number') { + val = w2utils.formatNumber(val) + } + if (edit.type == 'date') { + val = w2utils.formatDate(w2utils.isDate(val, edit.format, true) || new Date(), edit.format) + } + input.value = (typeof val !== 'object' ? val : '') + // init w2field, attached to input._w2field + const doHide = (event) => { + const escKey = this.last._edit?.escKey + // check if any element is selected in drop down + let selected = false + const name = query(input).data('tooltipName') + if (name && w2tooltip.get(name[0])?.selected != null) { + selected = true + } + // trigger change on new value if selected from overlay + if (this.last.inEditMode && !escKey && dropTypes.includes(edit.type) && // drop down types + (event.detail.overlay.anchor?.id == this.last._edit.input?.id || edit.type == 'list')) { + this.editChange() + this.editDone(undefined, undefined, { keyCode: selected ? 13 : 0 }) // advance on select + } + } + new w2field(w2utils.extend({}, edit, { + el: input, + selected: val, + onSelect: doHide, + onHide: doHide + })) + if (value == null && input) { + // if no new value, then select content + input.select() + } + } + } + Object.assign(this.last._edit, { input, edit }) + query(input) + .off('.w2ui-editable') + .on('blur.w2ui-editable', (event) => { + if (this.last.inEditMode) { + const type = this.last._edit.edit.type + const name = query(input).data('tooltipName') // if popup is open + if ((name && dropTypes.includes(type)) || event.target._keepOpen === true) { + delete event.target._keepOpen + // drop downs finish edit when popover is closed + return + } + this.editChange(input, index, column, event) + this.editDone() + } + }) + .on('mousedown.w2ui-editable', (event) => { + event.stopPropagation() + }) + .on('click.w2ui-editable', (event) => { + expand.call(input, event) + }) + .on('paste.w2ui-editable', (event) => { + // clean paste to be plain text + event.preventDefault() + const text = event.clipboardData.getData('text/plain') + document.execCommand('insertHTML', false, text) + }) + .on('keyup.w2ui-editable', (event) => { + expand.call(input, event) + }) + .on('keydown.w2ui-editable', (event) => { + switch (event.keyCode) { + case 8: // backspace; + if (edit.type == 'list' && !input._w2field) { // cancel backspace when deleting element + event.preventDefault() + } + break + case 9: + case 13: + event.preventDefault() + break + case 27: // esc button exits edit mode, but if in a popup, it will also close the popup, hence + // if tooltip is open - hide it + const name = query(input).data('tooltipName') + if (name && name.length > 0) { + this.last._edit.escKey = true + w2tooltip.hide(name[0]) + event.preventDefault() + return // keep input editable just close tooltip + } + event.stopPropagation() + break + } + // need timeout so, this handler is executed after key is processed by browser + setTimeout(() => { + switch (event.keyCode) { + case 9: { // tab + const next = event.shiftKey + ? self.prevCell(index, column, true) + : self.nextCell(index, column, true) + if (next != null) { + const recid = self.records[next.index].recid + this.editChange(input, index, column, event) + this.editDone(index, column, event) + if (self.selectType != 'row') { + self.selectNone(true) // no need to trigger select event + self.select({ recid, column: next.colIndex }) + } else { + self.editField(recid, next.colIndex, null, event) + } + if (event.preventDefault) event.preventDefault() + } + break + } + case 13: { // enter + // check if any element is selected in drop down + let selected = false + const name = query(input).data('tooltipName') + if (name && w2tooltip.get(name[0]).selected != null) { + selected = true + } + // if tooltip is not open or no element is selected + if ((!name || !selected) && input._keepOpen !== true) { + this.editChange(input, index, column, event) + this.editDone(index, column, event) + } else { + delete input._keepOpen + } + break + } + case 27: { // escape + this.last._edit.escKey = false + let old = self.parseField(rec, col.field) + if (rec.w2ui?.changes?.[col.field] != null) old = rec.w2ui.changes[col.field] + if (input._prevValue != null) old = input._prevValue + if (input.tagName == 'DIV') { + input.innerText = old != null ? old : '' + } else { + input.value = old != null ? old : '' + } + this.editDone(index, column, event) + setTimeout(() => { self.select({ recid: recid, column: column }) }, 1) + break + } + } + // if input too small - expand + expand(input) + }, 1) + }) + // save previous value + if (input) input._prevValue = prevValue + // focus and select + if (edit.type != 'list') { + setTimeout(() => { + if (!this.last.inEditMode) return + if (input) { + input.focus() + clearTimeout(this.last.kbd_timer) // keep focus + input.resize = expand + expand(input) + } + }, 50) + } + // event after + edata.finish({ input }) + return + function expand(input) { + try { + const styles = getComputedStyle(input) + const val = (input.tagName.toUpperCase() == 'DIV' ? input.innerText : input.value) + const editBox = query(self.box).find('#grid_' + self.name + '_editable').get(0) + const style = `font-family: ${styles['font-family']}; font-size: ${styles['font-size']}; white-space: no-wrap;` + const width = w2utils.getStrWidth(val, style) + if (width + 20 > editBox.clientWidth) { + query(editBox).css('width', width + 20 + 'px') + } + } catch (e) { + } + } + } + editChange(input, index, column, event) { + // if params are not specified + input = input ?? this.last._edit.input + index = index ?? this.last._edit.index + column = column ?? this.last._edit.column + event = event ?? {} + // all other fields + const summary = index < 0 + index = index < 0 ? -index - 1 : index + const records = summary ? this.summary : this.records + const rec = records[index] + const col = this.columns[column] + let new_val = (input?.tagName == 'DIV' ? input.innerText : input.value) + const fld = input._w2field + if (fld) { + if (fld.type == 'list') { + new_val = fld.selected + } + if (new_val == null || Object.keys(new_val).length === 0) new_val = '' + if (!w2utils.isPlainObject(new_val)) new_val = fld.clean(new_val) + } + if (input.type == 'checkbox') { + if (rec.w2ui?.editable === false) input.checked = !input.checked + new_val = input.checked + } + const old_val = this.parseField(rec, col.field) + const prev_val = (rec.w2ui?.changes && rec.w2ui.changes.hasOwnProperty(col.field) ? rec.w2ui.changes[col.field] : old_val) + // change/restore event + let edata = { + target: this.name, input, + recid: rec.recid, index, column, + colField: col.field, + originalEvent: event, + value: { + new: new_val, + previous: prev_val, + original: old_val + } + } + if (event.target?._prevValue != null) edata.value.previous = event.target._prevValue + let count = 0 // just in case to avoid infinite loop + while (count < 20) { + count++ + new_val = edata.value.new + if ((typeof new_val !== 'object' && String(old_val) !== String(new_val)) || + (typeof new_val === 'object' && new_val && new_val.id !== old_val && + (typeof old_val !== 'object' || old_val == null || new_val.id !== old_val.id))) { + // change event + edata = this.trigger('change', edata) + if (edata.isCancelled !== true) { + if (new_val !== edata.detail.value.new) { + // re-evaluate the type of change to be made + continue + } + // default action + if ((edata.detail.value.new === '' || edata.detail.value.new == null) && (prev_val === '' || prev_val == null)) { + // value did not change, was empty is empty + } else { + rec.w2ui = rec.w2ui ?? {} + rec.w2ui.changes = rec.w2ui.changes ?? {} + rec.w2ui.changes[col.field] = edata.detail.value.new + } + // event after + edata.finish() + } + } else { + // restore event + edata = this.trigger('restore', edata) + if (edata.isCancelled !== true) { + if (new_val !== edata.detail.value.new) { + // re-evaluate the type of change to be made + continue + } + // default action + if (rec.w2ui?.changes) { + delete rec.w2ui.changes[col.field] + if (Object.keys(rec.w2ui.changes).length === 0) { + delete rec.w2ui.changes + } + } + // event after + edata.finish() + } + } + break + } + } + editDone(index, column, event) { + // if params are not specified + index = index ?? this.last._edit.index + column = column ?? this.last._edit.column + event = event ?? {} + // removal of input happens when TR is redrawn + if (this.advanceOnEdit && event.keyCode === 13) { + let next = event.shiftKey ? this.prevRow(index, column, 1) : this.nextRow(index, column, 1) + if (next == null) next = index // keep the same + setTimeout(() => { + if (this.selectType !== 'row') { + this.selectNone(true) // no need to trigger select event + this.select({ recid: this.records[next].recid, column: column }) + } else { + this.editField(this.records[next].recid, column, null, event) + } + }, 1) + } + const summary = index < 0 + const cell = query(this.last._edit?.tr).find('[col="' + column + '"]') + const rec = this.records[index] + const col = this.columns[column] + // need to set before remove, as remove will trigger blur + this.last.inEditMode = false + this.last._edit = null + // remove - by updating cell data + if (!summary) { + if (rec.w2ui?.changes?.[col.field] != null) { + cell.addClass('w2ui-changed') + } else { + cell.removeClass('w2ui-changed') + } + cell.replace(this.getCellHTML(index, column, summary)) + } + // remove - spreadsheet + query(this.box).find('div.w2ui-edit-box').remove() + // update toolbar buttons + this.updateToolbar() + // keep grid in focus if needed + setTimeout(() => { + const input = query(this.box).find(`#grid_${this.name}_focus`).get(0) + if (document.activeElement !== input && !this.last.inEditMode) { + input.focus() + } + }, 10) + } + 'delete'(force) { + // event before + const edata = this.trigger('delete', { target: this.name, force: force }) + if (force) this.message() // close message + if (edata.isCancelled === true) return + force = edata.detail.force + // default action + const recs = this.getSelection() + if (recs.length === 0) return + if (this.msgDelete != '' && !force) { + this.confirm({ + text: w2utils.lang(this.msgDelete, { + count: recs.length, + records: w2utils.lang(recs.length == 1 ? 'record' : 'records') + }), + width: 380, + height: 170, + yes_text: w2utils.lang('Delete'), + yes_class: 'w2ui-btn-red', + no_text: w2utils.lang('Cancel') + }) + .yes(event => { + event.detail.self.close() + this.delete(true) + }) + .no(event => { + event.detail.self.close() + }) + return + } + // call delete script + const url = this.url?.remove ?? this.url + if (url) { + this.request('delete') + } else { + if (typeof recs[0] !== 'object') { + this.selectNone() + this.remove.apply(this, recs) + } else { + // clear cells + for (let r = 0; r < recs.length; r++) { + const fld = this.columns[recs[r].column].field + const ind = this.get(recs[r].recid, true) + const rec = this.records[ind] + if (ind != null && fld != 'recid') { + this.records[ind][fld] = '' + if (rec.w2ui?.changes) delete rec.w2ui.changes[fld] + // -- style should not be deleted + // if (rec.style != null && w2utils.isPlainObject(rec.style) && rec.style[recs[r].column]) { + // delete rec.style[recs[r].column]; + // } + } + } + this.update() + } + } + // event after + edata.finish() + } + click(recid, event) { + const time = Date.now() + let column = null + if (this.last.cancelClick == true || (event && event.altKey)) return + if ((typeof recid === 'object') && (recid !== null)) { + column = recid.column + recid = recid.recid + } + if (event == null) event = {} + // check for double click + if (time - parseInt(this.last.click_time) < 350 && this.last.click_recid == recid && event.type == 'click') { + this.dblClick(recid, event) + return + } + // hide bubble + if (this.last.bubbleEl) { + this.last.bubbleEl = null + } + this.last.click_time = time + const last_recid = this.last.click_recid + this.last.click_recid = recid + // column user clicked on + if (column == null && event.target) { + let trg = event.target + if (trg.tagName != 'TD') trg = query(trg).closest('td')[0] + if (query(trg).attr('col') != null) column = parseInt(query(trg).attr('col')) + } + // event before + const edata = this.trigger('click', { target: this.name, recid, column, originalEvent: event }) + if (edata.isCancelled === true) return + // default action + const sel = this.getSelection() + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', false) + const ind = this.get(recid, true) + const selectColumns = [] + this.last.sel_ind = ind + this.last.sel_col = column + this.last.sel_recid = recid + this.last.sel_type = 'click' + // multi select with shift key + let start, end, t1, t2 + if (event.shiftKey && sel.length > 0 && this.multiSelect) { + if (sel[0].recid) { + start = this.get(sel[0].recid, true) + end = this.get(recid, true) + if (column > sel[0].column) { + t1 = sel[0].column + t2 = column + } else { + t1 = column + t2 = sel[0].column + } + for (let c = t1; c <= t2; c++) selectColumns.push(c) + } else { + start = this.get(last_recid, true) + end = this.get(recid, true) + } + const sel_add = [] + if (start > end) { const tmp = start; start = end; end = tmp } + const url = this.url?.get ?? this.url + for (let i = start; i <= end; i++) { + if (this.searchData.length > 0 && !url && !this.last.searchIds.includes(i)) continue + if (this.selectType == 'row') { + sel_add.push(this.records[i].recid) + } else { + for (let sc = 0; sc < selectColumns.length; sc++) { + sel_add.push({ recid: this.records[i].recid, column: selectColumns[sc] }) + } + } + // sel.push(this.records[i].recid); + } + this.select(sel_add) + } else { + const last = this.last.selection + let flag = (last.indexes.indexOf(ind) != -1) + let fselect = false + // if clicked on the checkbox + if (query(event.target).closest('td').hasClass('w2ui-col-select')) fselect = true + // clear other if necessary + if (((!event.ctrlKey && !event.shiftKey && !event.metaKey && !fselect) || !this.multiSelect) && !this.showSelectColumn) { + if (this.selectType != 'row' && !last.columns[ind]?.includes(column)) { + flag = false + } + if (flag === true && sel.length == 1) { + this.unselect({ recid: recid, column: column }) + } else { + this.selectNone(true) // no need to trigger select event + this.select({ recid: recid, column: column }) + } + } else { + if (this.selectType != 'row') flag = false + if (flag === true) { + this.unselect({ recid: recid, column: column }) + } else { + this.select({ recid: recid, column: column }) + } + } + } + this.status() + this.initResize() + // event after + edata.finish() + } + columnClick(field, event) { + // ignore click if column was resized + if (this.last.colResizing === true) { + return + } + // event before + let edata = this.trigger('columnClick', { target: this.name, field: field, originalEvent: event }) + if (edata.isCancelled === true) return + // default behaviour + if (this.selectType == 'row') { + const column = this.getColumn(field) + if (column && column.sortable) this.sort(field, null, (!!(event && (event.ctrlKey || event.metaKey || event.shiftKey)))) + if (edata.detail.field == 'line-number') { + if (this.getSelection().length >= this.records.length) { + this.selectNone() + } else { + this.selectAll() + } + } + } else { + if (event.altKey) { + const column = this.getColumn(field) + if (column && column.sortable) this.sort(field, null, (!!(event && (event.ctrlKey || event.metaKey || event.shiftKey)))) + } + // select entire column + if (edata.detail.field == 'line-number') { + if (this.getSelection().length >= this.records.length) { + this.selectNone() + } else { + this.selectAll() + } + } else { + if (!event.shiftKey && !event.metaKey && !event.ctrlKey) { + this.selectNone(true) + } + const tmp = this.getSelection() + const column = this.getColumn(edata.detail.field, true) + const sel = [] + const cols = [] + // check if there was a selection before + if (tmp.length != 0 && event.shiftKey) { + let start = column + let end = tmp[0].column + if (start > end) { + start = tmp[0].column + end = column + } + for (let i = start; i <= end; i++) cols.push(i) + } else { + cols.push(column) + } + edata = this.trigger('columnSelect', { target: this.name, columns: cols }) + if (edata.isCancelled !== true) { + for (let i = 0; i < this.records.length; i++) { + sel.push({ recid: this.records[i].recid, column: cols }) + } + this.select(sel) + } + edata.finish() + } + } + // event after + edata.finish() + } + columnDblClick(field, event) { + // event before + const edata = this.trigger('columnDblClick', { target: this.name, field: field, originalEvent: event }) + if (edata.isCancelled === true) return + // event after + edata.finish() + } + columnContextMenu(field, event) { + const edata = this.trigger('columnContextMenu', { target: this.name, field: field, originalEvent: event }) + if (edata.isCancelled === true) return + // show menu + w2menu.show({ + type: 'check', + contextMenu: true, + originalEvent: event, + items: this.initColumnOnOff() + }) + .then(() => { + query('#w2overlay-context-menu .w2ui-grid-skip') + .off('.w2ui-grid') + .on('click.w2ui-grid', evt => { + evt.stopPropagation() + }) + .on('keypress', evt => { + if (evt.keyCode == 13) { + this.skip(evt.target.value) + this.toolbar.click('w2ui-column-on-off') // close menu + } + }) + }) + .select((event) => { + const id = event.detail.item.id + if (['w2ui-stateSave', 'w2ui-stateReset'].includes(id)) { + this[id.substring(5)]() + } else if (id == 'w2ui-skip') { + // empty + } else { + this.columnOnOff(event, event.detail.item.id) + } + clearTimeout(this.last.kbd_timer) // keep grid in focus + }) + clearTimeout(this.last.kbd_timer) // keep grid in focus + // cancel default + event.preventDefault() + edata.finish() + } + // if called w/o arguments, then will resize all columns + columnAutoSize(colIndex) { + if (arguments.length == 0) { + // autoSize all columns + this.columns.forEach((col, i) => this.columnAutoSize(i)) + return + } + const col = this.columns[colIndex] + const el = query(`#grid_${this.name}_column_${colIndex} .w2ui-col-header`)[0] + if (col.autoResize === false || col.hidden === true || !el) { + return true + } + const style = getComputedStyle(el) + let maxWidth = w2utils.getStrWidth(el.innerHTML, `font-family: ${style.fontFamily}; font-size: ${style.fontSize}`, true) + + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + 4 + query(this.box).find(`.w2ui-grid-records td[col="${colIndex}"] > div`, this.box).each(el => { + const style = getComputedStyle(el) + const width = w2utils.getStrWidth(el.innerHTML, `font-family: ${style.fontFamily}; font-size: ${style.fontSize}`, true) + + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + 4 // add some extra because of the border + if (maxWidth < width) { + maxWidth = width + } + }) + // event before + const edata = this.trigger('columnAutoResize', { maxWidth, originalEvent: event, target: this.name, column: col }) + if (edata.isCancelled === true) { return } + if (maxWidth > 0) { + if (col.sizeOriginal == null) col.sizeOriginal = col.size + col.size = Math.min(Math.abs(maxWidth), col.max || Infinity) + 'px' + this.resizeRecords() + this.resizeRecords() // Why do we have to call it twice in order to show the scrollbar? + this.scroll() + } + // event after + edata.finish() + } + columnAutoSizeAll() { + this.columns.forEach((col, ind) => this.columnAutoSize(ind)) + } + focus(event) { + // event before + const edata = this.trigger('focus', { target: this.name, originalEvent: event }) + if (edata.isCancelled === true) return false + // default behaviour + this.hasFocus = true + query(this.box).removeClass('w2ui-inactive').find('.w2ui-inactive').removeClass('w2ui-inactive') + setTimeout(() => { + const txt = query(this.box).find(`#grid_${this.name}_focus`).get(0) + if (txt && document.activeElement != txt) { + txt.focus() + } + }, 10) + // event after + edata.finish() + } + blur(event) { + // event before + const edata = this.trigger('blur', { target: this.name, originalEvent: event }) + if (edata.isCancelled === true) return false + // default behaviour + this.hasFocus = false + query(this.box).addClass('w2ui-inactive').find('.w2ui-selected').addClass('w2ui-inactive') + query(this.box).find('.w2ui-selection').addClass('w2ui-inactive') + // event after + edata.finish() + } + keydown(event) { + // this method is called from w2utils + const obj = this + const url = this.url?.get ?? this.url + if (obj.keyboard !== true) return + // trigger event + const edata = obj.trigger('keydown', { target: obj.name, originalEvent: event }) + if (edata.isCancelled === true) return + // default behavior + if (query(this.box).find('.w2ui-message').length > 0) { + // if there are messages + if (event.keyCode == 27) this.message() + return + } + let empty = false + const records = query(obj.box).find('#grid_' + obj.name + '_records') + const sel = obj.getSelection() + if (sel.length === 0) empty = true + let recid = sel[0] || null + let columns = [] + let recid2 = sel[sel.length - 1] + if (typeof recid === 'object' && recid != null) { + recid = sel[0].recid + columns = [] + let ii = 0 + while (true) { + if (!sel[ii] || sel[ii].recid != recid) break + columns.push(sel[ii].column) + ii++ + } + recid2 = sel[sel.length - 1].recid + } + const ind = obj.get(recid, true) + const ind2 = obj.get(recid2, true) + const recEL = query(obj.box).find(`#grid_${obj.name}_rec_${(ind != null ? w2utils.escapeId(obj.records[ind].recid) : 'none')}`) + const pageSize = Math.floor(records[0].clientHeight / obj.recordHeight) + let cancel = false + const key = event.keyCode + const shiftKey = event.shiftKey + switch (key) { + case 8: // backspace + case 46: { // delete + // delete if button is visible + obj.delete() + cancel = true + event.stopPropagation() + break + } + case 27: { // escape + if (obj.last.move?.type) { + delete obj.last.move + obj.removeRange('selection-preview') + obj.removeRange('selection-expand') + cancel = true + } else { + obj.selectNone() + cancel = true + } + break + } + case 65: { // cmd + A + if (!event.metaKey && !event.ctrlKey) break + obj.selectAll() + cancel = true + break + } + case 13: { // enter + // if expandable columns - expand it + if (this.selectType == 'row' && obj.show.expandColumn === true) { + if (recEL.length <= 0) break + obj.toggle(recid, event) + cancel = true + } else { // or enter edit + for (let c = 0; c < this.columns.length; c++) { + const edit = this.getCellEditable(ind, c) + if (edit) { + columns.push(parseInt(c)) + break + } + } + // edit last column that was edited + if (this.selectType == 'row' && this.last._edit && this.last._edit.column) { + columns = [this.last._edit.column] + } + if (columns.length > 0) { + obj.editField(recid, columns[0] ?? this.last.editColumn, null, event) + cancel = true + } + } + break + } + case 37: { // left + moveLeft() + break + } + case 39: { // right + moveRight() + break + } + case 33: { // + moveUp(pageSize) + break + } + case 34: { // + moveDown(pageSize) + break + } + case 35: { // + moveDown(-1) + break + } + case 36: { // + moveUp(-1) + break + } + case 38: { // up + // ctrl (or cmd) + up -> same as home + moveUp(event.metaKey || event.ctrlKey ? -1 : 1) + break + } + case 40: { // down + // ctrl (or cmd) + up -> same as end + moveDown(event.metaKey || event.ctrlKey ? -1 : 1) + break + } + // copy & paste + case 17: // ctrl key + case 91: { // cmd key + // SLOW: 10k records take 7.0 + if (empty) break + // in Safari need to copy to buffer on cmd or ctrl key (otherwise does not work) + if (w2utils.isSafari) { + obj.last.copy_event = obj.copy(false, event) + const focus = query(obj.box).find('#grid_' + obj.name + '_focus') + focus.val(obj.last.copy_event.detail.text) + focus[0].select() + } + break + } + case 67: { // - c + // this fill trigger event.onComplete + if (event.metaKey || event.ctrlKey) { + if (w2utils.isSafari) { + obj.copy(obj.last.copy_event, event) + } else { + obj.last.copy_event = obj.copy(false, event) + const focus = query(obj.box).find('#grid_' + obj.name + '_focus') + focus.val(obj.last.copy_event.detail.text) + focus[0].select() + obj.copy(obj.last.copy_event, event) + } + } + break + } + case 88: { // x - cut + if (empty) break + if (event.ctrlKey || event.metaKey) { + if (w2utils.isSafari) { + obj.copy(obj.last.copy_event, event) + } else { + obj.last.copy_event = obj.copy(false, event) + const focus = query(obj.box).find('#grid_' + obj.name + '_focus') + focus.val(obj.last.copy_event.detail.text) + focus[0].select() + obj.copy(obj.last.copy_event, event) + } + } + break + } + } + const tmp = [32, 187, 189, 192, 219, 220, 221, 186, 222, 188, 190, 191] // other typeable chars + for (let i = 48; i <= 111; i++) tmp.push(i) // 0-9,a-z,A-Z,numpad + if (tmp.indexOf(key) != -1 && !event.ctrlKey && !event.metaKey && !cancel) { + if (columns.length === 0) columns.push(0) + cancel = false + // move typed key into edit + setTimeout(() => { + const focus = query(obj.box).find('#grid_' + obj.name + '_focus') + const key = focus.val() + focus.val('') + obj.editField(recid, columns[0], key, event) + }, 1) + } + if (cancel) { // cancel default behaviour + if (event.preventDefault) event.preventDefault() + } + // event after + edata.finish() + function moveLeft() { + if (empty) { // no selection + selectTopRecord() + return + } + if (obj.selectType == 'row') { + if (recEL.length <= 0) return + const tmp = obj.records[ind].w2ui || {} + if (tmp && tmp.parent_recid != null && (!Array.isArray(tmp.children) || tmp.children.length === 0 || !tmp.expanded)) { + obj.unselect(recid) + obj.collapse(tmp.parent_recid, event) + obj.select(tmp.parent_recid) + } else { + obj.collapse(recid, event) + } + } else { + let prev = obj.prevCell(ind, columns[0]) + if (prev?.index != ind) { + prev = null + } else { + prev = prev?.colIndex + } + if (!shiftKey && prev == null) { + obj.selectNone(true) + prev = 0 + } + if (prev != null) { + if (shiftKey && obj.multiSelect) { + if (tmpUnselect()) return + const tmp = [] + const newSel = [] + const unSel = [] + if (columns.indexOf(obj.last.sel_col) === 0 && columns.length > 1) { + for (let i = 0; i < sel.length; i++) { + if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid) + unSel.push({ recid: sel[i].recid, column: columns[columns.length - 1] }) + } + obj.unselect(unSel) + obj.scrollIntoView(ind, columns[columns.length - 1], true) + } else { + for (let i = 0; i < sel.length; i++) { + if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid) + newSel.push({ recid: sel[i].recid, column: prev }) + } + obj.select(newSel) + obj.scrollIntoView(ind, prev, true) + } + } else { + obj.click({ recid: recid, column: prev }, event) + obj.scrollIntoView(ind, prev, true) + } + } else { + // if selected more then one, then select first + if (!shiftKey) { + obj.selectNone(true) + } + } + } + cancel = true + } + function moveRight() { + if (empty) { + selectTopRecord() + return + } + if (obj.selectType == 'row') { + if (recEL.length <= 0) return + obj.expand(recid, event) + } else { + let next = obj.nextCell(ind, columns[columns.length - 1]) // columns is an array of selected columns + if (next?.index != ind) { + next = null + } else { + next = next.colIndex + } + if (!shiftKey && next == null) { + obj.selectNone(true) + next = obj.columns.length - 1 + } + if (next != null) { + if (shiftKey && key == 39 && obj.multiSelect) { + if (tmpUnselect()) return + const tmp = [] + const newSel = [] + const unSel = [] + if (columns.indexOf(obj.last.sel_col) == columns.length - 1 && columns.length > 1) { + for (let i = 0; i < sel.length; i++) { + if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid) + unSel.push({ recid: sel[i].recid, column: columns[0] }) + } + obj.unselect(unSel) + obj.scrollIntoView(ind, columns[0], true) + } else { + for (let i = 0; i < sel.length; i++) { + if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid) + newSel.push({ recid: sel[i].recid, column: next }) + } + obj.select(newSel) + obj.scrollIntoView(ind, next, true) + } + } else { + obj.click({ recid: recid, column: next }, event) + obj.scrollIntoView(ind, next, true) + } + } else { + // if selected more then one, then select first + if (!shiftKey) { + obj.selectNone(true) + } + } + } + cancel = true + } + function moveUp(numRows) { + if (empty) selectTopRecord() + if (recEL.length <= 0) return + // move to the previous record + let prev = obj.prevRow(ind, obj.selectType == 'row' ? 0 : sel[0].column, numRows) + if (!shiftKey && prev == null) { + if (obj.searchData.length != 0 && !url) { + prev = obj.last.searchIds[0] + } else { + prev = 0 + } + } + if (prev != null) { + if (shiftKey && obj.multiSelect) { // expand selection + if (tmpUnselect()) return + if (obj.selectType == 'row') { + if (obj.last.sel_ind > prev && obj.last.sel_ind != ind2) { + obj.unselect(obj.records[ind2].recid) + } else { + obj.select(obj.records[prev].recid) + } + } else { + if (obj.last.sel_ind > prev && obj.last.sel_ind != ind2) { + prev = ind2 + const tmp = [] + for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[prev].recid, column: columns[c] }) + obj.unselect(tmp) + } else { + const tmp = [] + for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[prev].recid, column: columns[c] }) + obj.select(tmp) + } + } + } else { // move selected record + obj.selectNone(true) // no need to trigger select event + obj.click({ recid: obj.records[prev].recid, column: columns[0] }, event) + } + obj.scrollIntoView(prev, null, true, numRows != 1) // top align record + if (event.preventDefault) event.preventDefault() + } else { + // if selected more then one, then select first + if (!shiftKey) { + obj.selectNone(true) + } + } + } + function moveDown(numRows) { + if (empty) selectTopRecord() + if (recEL.length <= 0) return + // move to the next record + let next = obj.nextRow(ind2, obj.selectType == 'row' ? 0 : sel[0].column, numRows) + if (!shiftKey && next == null) { + if (obj.searchData.length != 0 && !url) { + next = obj.last.searchIds[obj.last.searchIds.length - 1] + } else { + next = obj.records.length - 1 + } + } + if (next != null) { + if (shiftKey && obj.multiSelect) { // expand selection + if (tmpUnselect()) return + if (obj.selectType == 'row') { + if (obj.last.sel_ind < next && obj.last.sel_ind != ind) { + obj.unselect(obj.records[ind].recid) + } else { + obj.select(obj.records[next].recid) + } + } else { + if (obj.last.sel_ind < next && obj.last.sel_ind != ind) { + next = ind + const tmp = [] + for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[next].recid, column: columns[c] }) + obj.unselect(tmp) + } else { + const tmp = [] + for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[next].recid, column: columns[c] }) + obj.select(tmp) + } + } + } else { // move selected record + obj.selectNone(true) // no need to trigger select event + obj.click({ recid: obj.records[next].recid, column: columns[0] }, event) + } + obj.scrollIntoView(next, null, true, numRows != 1) // top align record + cancel = true + } else { + // if selected more then one, then select first + if (!shiftKey) { + obj.selectNone(true) // no need to trigger select event + } + } + } + function selectTopRecord() { + if (!obj.records || obj.records.length === 0) return + let ind = Math.floor(records[0].scrollTop / obj.recordHeight) + 1 + if (!obj.records[ind] || ind < 2) ind = 0 + if (typeof obj.records[ind] === 'undefined') return + obj.select({ recid: obj.records[ind].recid, column: 0 }) + } + function tmpUnselect() { + if (obj.last.sel_type != 'click') return false + if (obj.selectType != 'row') { + obj.last.sel_type = 'key' + if (sel.length > 1) { + for (let s = 0; s < sel.length; s++) { + if (sel[s].recid == obj.last.sel_recid && sel[s].column == obj.last.sel_col) { + sel.splice(s, 1) + break + } + } + obj.unselect(sel) + return true + } + return false + } else { + obj.last.sel_type = 'key' + if (sel.length > 1) { + sel.splice(sel.indexOf(obj.records[obj.last.sel_ind].recid), 1) + obj.unselect(sel) + return true + } + return false + } + } + } + scrollIntoView(ind, column, instant, recTop) { + let buffered = this.records.length + if (this.searchData.length != 0 && !this.url) buffered = this.last.searchIds.length + if (buffered === 0) return + if (ind == null) { + const sel = this.getSelection() + if (sel.length === 0) return + if (w2utils.isPlainObject(sel[0])) { + ind = sel[0].index + column = sel[0].column + } else { + ind = this.get(sel[0], true) + } + } + const records = query(this.box).find(`#grid_${this.name}_records`) + const recWidth = records[0].clientWidth + const recHeight = records[0].clientHeight + const recSTop = records[0].scrollTop + const recSLeft = records[0].scrollLeft + // if all records in view + const len = this.last.searchIds.length + if (len > 0) ind = this.last.searchIds.indexOf(ind) // if search is applied + // smooth or instant + records.css({ 'scroll-behavior': instant ? 'auto' : 'smooth' }) + // vertical + if (recHeight < this.recordHeight * (len > 0 ? len : buffered) && records.length > 0) { + // scroll to correct one + const t1 = Math.floor(recSTop / this.recordHeight) + const t2 = t1 + Math.floor(recHeight / this.recordHeight) + if (ind == t1) { + records.prop('scrollTop', recSTop - recHeight / 1.3) + } + if (ind == t2) { + records.prop('scrollTop', recSTop + recHeight / 1.3) + } + if (ind < t1 || ind > t2) { + records.prop('scrollTop', (ind - 1) * this.recordHeight) + } + if (recTop === true) { + records.prop('scrollTop', ind * this.recordHeight) + } + } + // horizontal + if (column != null) { + let x1 = 0 + let x2 = 0 + const sb = w2utils.scrollBarSize() + for (let i = 0; i <= column; i++) { + const col = this.columns[i] + if (col.frozen || col.hidden) continue + x1 = x2 + x2 += parseInt(col.sizeCalculated) + } + if (recWidth < x2 - recSLeft) { // right + records.prop('scrollLeft', x1 - sb) + } else if (x1 < recSLeft) { // left + records.prop('scrollLeft', x2 - recWidth + sb * 2) + } + } + } + scrollToColumn(field) { + if (field == null) { return } + let sWidth = 0 + let found = false + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + if (col.field == field) { + found = true + break + } + if (col.frozen || col.hidden) { continue } + const cSize = parseInt(col.sizeCalculated ? col.sizeCalculated : col.size) + sWidth += cSize + } + if (!found) { return } + this.last.vscroll.scrollLeft = sWidth + 1 + this.scroll() + } + + dblClick(recid, event) { + // find columns + let column = null + if ((typeof recid === 'object') && (recid !== null)) { + column = recid.column + recid = recid.recid + } + if (event == null) event = {} + // column user clicked on + if (column == null && event.target) { + let tmp = event.target + if (tmp.tagName.toUpperCase() != 'TD') tmp = query(tmp).closest('td')[0] + column = parseInt(query(tmp).attr('col')) + } + const index = this.get(recid, true) + const rec = this.records[index] + // event before + const edata = this.trigger('dblClick', { target: this.name, recid: recid, column: column, originalEvent: event }) + if (edata.isCancelled === true) return + // default action + this.selectNone(true) // no need to trigger select event + const edit = this.getCellEditable(index, column) + if (edit) { + this.editField(recid, column, null, event) + } else { + this.select({ recid: recid, column: column }) + if (this.show.expandColumn || (rec && rec.w2ui && Array.isArray(rec.w2ui.children))) this.toggle(recid) + } + // event after + edata.finish() + } + showContextMenu(event, options) { + const { recid, index, column } = options + if (this.last.userSelect == 'text') return + if (event == null) { + event = { offsetX: 0, offsetY: 0, target: query(this.box).find(`#grid_${this.name}_rec_${recid}`)[0] } + } + if (event.offsetX == null) { + event.offsetX = event.layerX - event.target.offsetLeft + event.offsetY = event.layerY - event.target.offsetTop + } + // if (w2utils.isFloat(recid)) recid = parseFloat(recid) + const sel = this.getSelection() + if (this.selectType == 'row') { + if (recid != null && sel.indexOf(recid) == -1) { + this.click(recid) + } + } else { + let sel_col = false // any cell in a column + let sel_row = false // any cell in a row + let sel_cell = false // this exact cell + sel.forEach(rec => { + if (rec.recid == recid) sel_row = true + if (rec.column == column) sel_col = true + if (rec.recid == recid && rec.column == column) sel_cell = true + }) + if (!sel_row && recid != null && column === null) this.click({ recid }) // select entire row + if (!sel_col && recid === null && column != null) this.columnClick(this.columns[column].field, event) + if (!sel_cell && recid != null && column != null) this.click({ recid, column }) // select a cell + } + // event before + const edata = this.trigger('contextMenu', { target: this.name, originalEvent: event, recid, index, column }) + if (edata.isCancelled === true) return + // default action + if (this.contextMenu?.length > 0) { + w2menu.show({ + contextMenu: true, + originalEvent: event, + items: this.contextMenu + }) + .select((event) => { + clearTimeout(this.last.kbd_timer) // keep grid in focus + this.contextMenuClick(recid, column, event) + }) + } + // cancel browser context menu + event.preventDefault() + clearTimeout(this.last.kbd_timer) // keep grid in focus + // event after + edata.finish() + } + contextMenuClick(recid, column, event) { + // event before + const edata = this.trigger('contextMenuClick', { + target: this.name, recid, column, originalEvent: event.detail.originalEvent, + menuEvent: event, menuIndex: event.detail.index, menuItem: event.detail.item + }) + if (edata.isCancelled === true) return + // no default action + edata.finish() + } + toggle(recid) { + const rec = this.get(recid) + if (rec == null) return + rec.w2ui = rec.w2ui ?? {} + if (rec.w2ui.expanded === true) return this.collapse(recid); else return this.expand(recid) + } + expand(recid, noRefresh) { + const ind = this.get(recid, true) + const rec = this.records[ind] + rec.w2ui = rec.w2ui ?? {} + const id = w2utils.escapeId(recid) + const children = rec.w2ui.children + let edata + if (Array.isArray(children)) { + if (rec.w2ui.expanded === true || children.length === 0) return false // already shown + edata = this.trigger('expand', { target: this.name, recid: recid }) + if (edata.isCancelled === true) return false + rec.w2ui.expanded = true + children.forEach((child) => { + child.w2ui = child.w2ui ?? {} + child.w2ui.parent_recid = rec.recid + if (child.w2ui.children == null) child.w2ui.children = [] + }) + this.records.splice.apply(this.records, [ind + 1, 0].concat(children)) + if (this.total !== -1) { + this.total += children.length + } + const url = this.url?.get ?? this.url + if (!url) { + this.localSort(true, true) + if (this.searchData.length > 0) { + this.localSearch(true) + } + } + if (noRefresh !== true) this.refresh() + edata.finish() + } else { + if (query(this.box).find('#grid_' + this.name + '_rec_' + id + '_expanded_row').length > 0 || this.show.expandColumn !== true) return false + if (rec.w2ui.expanded == 'none') return false + // insert expand row + query(this.box).find('#grid_' + this.name + '_rec_' + id).after( + ` + +
+ + + `) + query(this.box).find('#grid_' + this.name + '_frec_' + id).after( + ` + ${this.show.lineNumbers ? '' : ''} + +
+ + `) + // event before + edata = this.trigger('expand', { target: this.name, recid: recid, + box_id: 'grid_' + this.name + '_rec_' + recid + '_expanded', fbox_id: 'grid_' + this.name + '_frec_' + recid + '_expanded' }) + if (edata.isCancelled === true) { + query(this.box).find('#grid_' + this.name + '_rec_' + id + '_expanded_row').remove() + query(this.box).find('#grid_' + this.name + '_frec_' + id + '_expanded_row').remove() + return false + } + // expand column + const row1 = query(this.box).find('#grid_' + this.name + '_rec_' + recid + '_expanded') + const row2 = query(this.box).find('#grid_' + this.name + '_frec_' + recid + '_expanded') + const innerHeight = row1.find(':scope div:first-child')[0]?.clientHeight ?? 50 + if (row1[0].clientHeight < innerHeight) { + row1.css({ height: innerHeight + 'px' }) + } + if (row2[0].clientHeight < innerHeight) { + row2.css({ height: innerHeight + 'px' }) + } + // default action + query(this.box).find('#grid_' + this.name + '_rec_' + id).attr('expanded', 'yes').addClass('w2ui-expanded') + query(this.box).find('#grid_' + this.name + '_frec_' + id).attr('expanded', 'yes').addClass('w2ui-expanded') + query(this.box).find('#grid_' + this.name + '_cell_' + this.get(recid, true) + '_expand div').html('-') + rec.w2ui.expanded = true + // event after + edata.finish() + this.resizeRecords() + } + return true + } + collapse(recid, noRefresh) { + const ind = this.get(recid, true) + const rec = this.records[ind] + rec.w2ui = rec.w2ui || {} + const id = w2utils.escapeId(recid) + const children = rec.w2ui.children + let edata + if (Array.isArray(children)) { + if (rec.w2ui.expanded !== true) return false // already hidden + edata = this.trigger('collapse', { target: this.name, recid: recid }) + if (edata.isCancelled === true) return false + clearExpanded(rec) + const stops = [] + for (let r = rec; r != null; r = this.get(r.w2ui.parent_recid)) { stops.push(r.w2ui.parent_recid) } + // stops contains 'undefined' plus the ID of all nodes in the path from 'rec' to the tree root + const start = ind + 1 + let end = start + while (true) { + if (this.records.length <= end + 1 || this.records[end + 1].w2ui == null || + stops.indexOf(this.records[end + 1].w2ui.parent_recid) >= 0) { + break + } + end++ + } + this.records.splice(start, end - start + 1) + if (this.total !== -1) { + this.total -= end - start + 1 + } + const url = this.url?.get ?? this.url + if (!url) { + if (this.searchData.length > 0) { + this.localSearch(true) + } + } + if (noRefresh !== true) this.refresh() + edata.finish() + } else { + if (query(this.box).find('#grid_' + this.name + '_rec_' + id + '_expanded_row').length === 0 || this.show.expandColumn !== true) return false + // event before + edata = this.trigger('collapse', { target: this.name, recid: recid, + box_id: 'grid_' + this.name + '_rec_' + recid + '_expanded', fbox_id: 'grid_' + this.name + '_frec_' + recid + '_expanded' }) + if (edata.isCancelled === true) return false + // default action + query(this.box).find('#grid_' + this.name + '_rec_' + id).removeAttr('expanded').removeClass('w2ui-expanded') + query(this.box).find('#grid_' + this.name + '_frec_' + id).removeAttr('expanded').removeClass('w2ui-expanded') + query(this.box).find('#grid_' + this.name + '_cell_' + this.get(recid, true) + '_expand div').html('+') + query(this.box).find('#grid_' + this.name + '_rec_' + id + '_expanded').css('height', '0px') + query(this.box).find('#grid_' + this.name + '_frec_' + id + '_expanded').css('height', '0px') + setTimeout(() => { + query(this.box).find('#grid_' + this.name + '_rec_' + id + '_expanded_row').remove() + query(this.box).find('#grid_' + this.name + '_frec_' + id + '_expanded_row').remove() + rec.w2ui.expanded = false + // event after + edata.finish() + this.resizeRecords() + }, 300) + } + return true + function clearExpanded(rec) { + rec.w2ui.expanded = false + for (let i = 0; i < rec.w2ui.children.length; i++) { + const subRec = rec.w2ui.children[i] + if (subRec.w2ui.expanded) { + clearExpanded(subRec) + } + } + } + } + sort(field, direction, multiField) { // if no params - clears sort + // event before + const edata = this.trigger('sort', { target: this.name, field, direction, multiField }) + if (edata.isCancelled === true) return + // check if needed to quit + if (field != null) { + // default action + let sortIndex = this.sortData.length + for (let s = 0; s < this.sortData.length; s++) { + if (this.sortData[s].field == field) { + sortIndex = s + break + } + } + if (direction == null) { + direction = this.sortData[sortIndex]?.direction + if (direction == null) { + // save original sort, so it can be restored + if (this.last.originalSort == null) { + this.last.originalSort = this.records.map(rec => rec.recid) + } + direction = 'asc' + } else { + switch (direction.toLowerCase()) { + case 'asc': { + direction = 'desc' + break + } + case 'desc': { + direction = '' + break + } + default: { + direction = 'asc' + break + } + } + } + } + if (multiField != true) { + this.sortData = [] + sortIndex = 0 + } + if (direction === '') { + this.sortData.splice(sortIndex, 1) + } else { + // set new sort + this.sortData[sortIndex] ??= {} + Object.assign(this.sortData[sortIndex], { field, direction }) + } + } else { + this.sortData = [] + } + // if local + const url = this.url?.get ?? this.url + if (!url) { + this.localSort(false, true) + if (this.searchData.length > 0) this.localSearch(true) + // reset vertical scroll + this.last.vscroll.scrollTop = 0 + query(this.box).find(`#grid_${this.name}_records`).prop('scrollTop', 0) + // event after + edata.finish({ direction }) + this.refresh() + } else { + // event after + edata.finish({ direction }) + this.last.fetch.offset = 0 + this.reload() + } + } + copy(flag, oEvent) { + if (this.keyboard !== true) return '' + if (w2utils.isPlainObject(flag)) { + // event after + flag.finish() + return flag.text + } + // generate text to copy + const sel = this.getSelection() + if (sel.length === 0) return '' + let text = '' + if (typeof sel[0] === 'object') { // cell copy + // find min/max column + let minCol = sel[0].column + let maxCol = sel[0].column + const recs = [] + for (let s = 0; s < sel.length; s++) { + if (sel[s].column < minCol) minCol = sel[s].column + if (sel[s].column > maxCol) maxCol = sel[s].column + if (recs.indexOf(sel[s].index) == -1) recs.push(sel[s].index) + } + recs.sort((a, b) => { return a - b }) // sort function must be for numerical sort + for (let r = 0; r < recs.length; r++) { + const ind = recs[r] + for (let c = minCol; c <= maxCol; c++) { + const col = this.columns[c] + if (col.hidden === true) continue + text += this.getCellCopy(ind, c) + '\t' + } + text = text.substr(0, text.length - 1) // remove last \t + text += '\n' + } + } else { // row copy + // copy headers + for (let c = 0; c < this.columns.length; c++) { + const col = this.columns[c] + if (col.hidden === true) continue + let colName = (col.text ? col.text : col.field) + if (col.text && col.text.length < 3 && col.tooltip) colName = col.tooltip // if column name is less then 3 char and there is tooltip - use it + text += '"' + w2utils.stripTags(colName) + '"\t' + } + text = text.substr(0, text.length - 1) // remove last \t + text += '\n' + // copy selected text + for (let s = 0; s < sel.length; s++) { + const ind = this.get(sel[s], true) + for (let c = 0; c < this.columns.length; c++) { + const col = this.columns[c] + if (col.hidden === true) continue + text += '"' + this.getCellCopy(ind, c) + '"\t' + } + text = text.substr(0, text.length - 1) // remove last \t + text += '\n' + } + } + text = text.substr(0, text.length - 1) + // if called without params + let edata + if (flag == null) { + // before event + edata = this.trigger('copy', { target: this.name, text: text, + cut: (oEvent.keyCode == 88), originalEvent: oEvent }) + if (edata.isCancelled === true) return '' + text = edata.detail.text + // event after + edata.finish() + return text + } else if (flag === false) { // only before event + // before event + edata = this.trigger('copy', { target: this.name, text: text, + cut: (oEvent.keyCode == 88), originalEvent: oEvent }) + if (edata.isCancelled === true) return '' + text = edata.detail.text + return edata + } + } + /** + * Gets value to be copied to the clipboard + * @param ind index of the record + * @param col_ind index of the column + * @returns the displayed value of the field's record associated with the cell + */ + getCellCopy(ind, col_ind) { + return w2utils.stripTags(this.getCellHTML(ind, col_ind)) + } + paste(text, event) { + if (this.keyboard !== true) { + if (event?.preventDefault) event.preventDefault() + return + } + const sel = this.getSelection() + let ind = this.get(sel[0].recid, true) + const col = sel[0].column + // before event + const edata = this.trigger('paste', { target: this.name, text: text, index: ind, column: col, originalEvent: event }) + if (edata.isCancelled === true) return + text = edata.detail.text + // default action + if (this.selectType == 'row' || sel.length === 0) { + console.log('ERROR: You can paste only if grid.selectType = \'cell\' and when at least one cell selected.') + // event after + edata.finish() + return + } + if (typeof text !== 'object') { + const newSel = [] + text = text.split('\n') + for (let t = 0; t < text.length; t++) { + const tmp = text[t].split('\t') + let cnt = 0 + const rec = this.records[ind] + const cols = [] + if (rec == null) continue + for (let dt = 0; dt < tmp.length; dt++) { + if (!this.columns[col + cnt]) continue + setCellPaste(rec, this.columns[col + cnt].field, tmp[dt]) + cols.push(col + cnt) + cnt++ + } + for (let c = 0; c < cols.length; c++) newSel.push({ recid: rec.recid, column: cols[c] }) + ind++ + } + this.selectNone(true) // no need to trigger select event + this.select(newSel) + } else { + this.selectNone(true) // no need to trigger select event + this.select([{ recid: this.records[ind], column: col }]) + } + this.refresh() + // event after + edata.finish() + function setCellPaste(rec, field, paste) { + rec.w2ui = rec.w2ui ?? {} + rec.w2ui.changes = rec.w2ui.changes || {} + rec.w2ui.changes[field] = paste + } + } + // ================================================== + // --- Common functions + resize() { + const time = Date.now() + // make sure the box is right + if (!this.box || query(this.box).attr('name') != this.name) return + // event before + const edata = this.trigger('resize', { target: this.name }) + if (edata.isCancelled === true) return + // resize + if (this.box != null) { + this.resizeBoxes() + this.resizeRecords() + } + // event after + edata.finish() + return Date.now() - time + } + update({ cells, fullCellRefresh, ignoreColumns } = {}) { + const time = Date.now() + const self = this + if (this.box == null) return 0 + if (Array.isArray(cells)) { + for (let i = 0; i < cells.length; i++) { + const index = cells[i].index + const column = cells[i].column + if (index < 0) continue + if (index == null || column == null) { + console.log('ERROR: Wrong argument for grid.update({ cells }), cells should be [{ index: X, column: Y }, ...]') + continue + } + const rec = this.records[index] ?? {} + rec.w2ui = rec.w2ui ?? {} + rec.w2ui._update = rec.w2ui._update ?? { cells: [] } + let row1 = rec.w2ui._update.row1 + let row2 = rec.w2ui._update.row2 + if (row1 == null || !row1.isConnected || row2 == null || !row2.isColSelected) { + row1 = this.box.querySelector(`#grid_${this.name}_rec_${w2utils.escapeId(rec.recid)}`) + row2 = this.box.querySelector(`#grid_${this.name}_frec_${w2utils.escapeId(rec.recid)}`) + rec.w2ui._update.row1 = row1 + rec.w2ui._update.row2 = row2 + } + _update(rec, row1, row2, index, column) + } + } else { + for (let i = this.last.vscroll.recIndStart - 1; i <= this.last.vscroll.recIndEnd; i++) { + let index = i + if (this.last.searchIds.length > 0) { // if search is applied + index = this.last.searchIds[i] + } else { + index = i + } + const rec = this.records[index] + if (index < 0 || rec == null) continue + rec.w2ui = rec.w2ui ?? {} + rec.w2ui._update = rec.w2ui._update ?? { cells: [] } + let row1 = rec.w2ui._update.row1 + let row2 = rec.w2ui._update.row2 + if (row1 == null || !row1.isConnected || row2 == null || !row2.isColSelected) { + row1 = this.box.querySelector(`#grid_${this.name}_rec_${w2utils.escapeId(rec.recid)}`) + row2 = this.box.querySelector(`#grid_${this.name}_frec_${w2utils.escapeId(rec.recid)}`) + rec.w2ui._update.row1 = row1 + rec.w2ui._update.row2 = row2 + } + for (let column = 0; column < this.columns.length; column++) { + _update(rec, row1, row2, index, column) + } + } + } + return Date.now() - time + function _update(rec, row1, row2, index, column) { + const pcol = self.columns[column] + if (Array.isArray(ignoreColumns) && (ignoreColumns.includes(column) || ignoreColumns.includes(pcol.field))) { + return + } + let cell = rec.w2ui._update.cells[column] + if (cell == null || !cell.isConnected) { + cell = self.box.querySelector(`#grid_${self.name}_data_${index}_${column}`) + rec.w2ui._update.cells[column] = cell + } + if (cell == null) return + if (fullCellRefresh) { + query(cell).replace(self.getCellHTML(index, column, false)) + // need to reselect as it was replaced + cell = self.box.querySelector(`#grid_${self.name}_data_${index}_${column}`) + rec.w2ui._update.cells[column] = cell + } else { + const div = cell.children[0] // there is always a div inside a cell + // value, attr, style, className, divAttr -- all on TD level except divAttr + const { value, style, className } = self.getCellValue(index, column, false, true) + if (div.innerHTML != value) { + div.innerHTML = value + } + if (style != '' && cell.style.cssText != style) { + cell.style.cssText = style + } + if (className != '') { + const ignore = ['w2ui-grid-data'] + const remove = [] + const add = className.split(' ').filter(cl => !!cl) // remove empty + cell.classList.forEach(cl => { if (!ignore.includes(cl)) remove.push(cl) }) + cell.classList.remove(...remove) + cell.classList.add(...add) + } + } + // column styles if any (lower priority) + if (self.columns[column].style && self.columns[column].style != cell.style.cssText) { + cell.style.cssText = self.columns[column].style ?? '' + } + // record class if any + if (rec.w2ui.class != null) { + if (typeof rec.w2ui.class === 'string') { + const ignore = ['w2ui-odd', 'w2ui-even', 'w2ui-record'] + const remove = [] + const add = rec.w2ui.class.split(' ').filter(cl => !!cl) // remove empty + if (row1 && row2) { + row1.classList.forEach(cl => { if (!ignore.includes(cl)) remove.push(cl) }) + row1.classList.remove(...remove) + row1.classList.add(...add) + row2.classList.remove(...remove) + row2.classList.add(...add) + } + } + if (w2utils.isPlainObject(rec.w2ui.class) && typeof rec.w2ui.class[pcol.field] === 'string') { + const ignore = ['w2ui-grid-data'] + const remove = [] + const add = rec.w2ui.class[pcol.field].split(' ').filter(cl => !!cl) + cell.classList.forEach(cl => { if (!ignore.includes(cl)) remove.push(cl) }) + cell.classList.remove(...remove) + cell.classList.add(...add) + } + } + // record styles if any + if (rec.w2ui.style != null) { + if (row1 && row2 && typeof rec.w2ui.style === 'string' && row1.style.cssText !== rec.w2ui.style) { + row1.style.cssText = 'height: ' + self.recordHeight + 'px;' + rec.w2ui.style + row1.setAttribute('custom_style', rec.w2ui.style) + row2.style.cssText = 'height: ' + self.recordHeight + 'px;' + rec.w2ui.style + row2.setAttribute('custom_style', rec.w2ui.style) + } + if (w2utils.isPlainObject(rec.w2ui.style) && typeof rec.w2ui.style[pcol.field] === 'string' && + cell.style.cssText !== rec.w2ui.style[pcol.field]) { + cell.style.cssText = rec.w2ui.style[pcol.field] + } + } + } + } + refreshCell(recid, field) { + const index = this.get(recid, true) + const col_ind = this.getColumn(field, true) + const isSummary = (!(this.records[index] && this.records[index].recid == recid)) + const cell = query(this.box).find(`${isSummary ? '.w2ui-grid-summary ' : ''}#grid_${this.name}_data_${index}_${col_ind}`) + if (cell.length == 0) return false + // set cell html and changed flag + cell.replace(this.getCellHTML(index, col_ind, isSummary)) + return true + } + refreshRow(recid, ind = null) { + let tr1 = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(recid)) + let tr2 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(recid)) + if (tr1.length > 0) { + if (ind == null) ind = this.get(recid, true) + const line = tr1.attr('line') + const isSummary = (!(this.records[ind] && this.records[ind].recid == recid)) + // if it is searched, find index in search array + const url = this.url?.get ?? this.url + if (this.searchData.length > 0 && !url) for (let s = 0; s < this.last.searchIds.length; s++) if (this.last.searchIds[s] == ind) ind = s + const rec_html = this.getRecordHTML(ind, line, isSummary) + tr1.replace(rec_html[0]) + tr2.replace(rec_html[1]) + // apply style to row if it was changed in render functions + let st = (this.records[ind].w2ui ? this.records[ind].w2ui.style : '') + if (typeof st === 'string') { + tr1 = query(this.box).find('#grid_' + this.name + '_frec_' + w2utils.escapeId(recid)) + tr2 = query(this.box).find('#grid_' + this.name + '_rec_' + w2utils.escapeId(recid)) + tr1.attr('custom_style', st) + tr2.attr('custom_style', st) + if (tr1.hasClass('w2ui-selected')) { + st = st.replace('background-color', 'none') + } + tr1[0].style.cssText = 'height: ' + this.recordHeight + 'px;' + st + tr2[0].style.cssText = 'height: ' + this.recordHeight + 'px;' + st + } + if (isSummary) { + this.resize() + } + return true + } + return false + } + refresh() { + const time = Date.now() + const url = this.url?.get ?? this.url + if (this.total <= 0 && !url && this.searchData.length === 0) { + this.total = this.records.length + } + if (!this.box) return + // event before + const edata = this.trigger('refresh', { target: this.name }) + if (edata.isCancelled === true) return + // -- header + if (this.show.header) { + query(this.box).find(`#grid_${this.name}_header`).html(w2utils.lang(this.header) + ' ').show() + } else { + query(this.box).find(`#grid_${this.name}_header`).hide() + } + // -- toolbar + if (this.show.toolbar) { + query(this.box).find('#grid_' + this.name + '_toolbar').show() + } else { + query(this.box).find('#grid_' + this.name + '_toolbar').hide() + } + // -- make sure search is closed + this.searchClose() + // --- default search field + const getFirstSearchField = () => { + let tmp = 0 + while (tmp < this.searches.length && (this.searches[tmp].hidden || this.searches[tmp].simple === false)) { + tmp++ + } + if (tmp >= this.searches.length) return { field: '', label: '' } // all searches are hidden or simple + return this.searches[tmp] + } + if (!this.multiSearch && this.last.field === 'all') { + const fld = getFirstSearchField() + this.last.field = fld.field + this.last.label = fld.label + } + if (this.last.field === 'all' && !this.show.searchAll) { + this.last.field = '' + } + if (!this.last.field) { + if (this.show.searchAll) { + this.last.field = 'all' + this.last.label = 'All Fields' + } else { + const fld = getFirstSearchField() + this.last.field = fld.field + this.last.label = fld.label + } + } + const sInput = query(this.box).find('#grid_' + this.name + '_search_all') + // find right search label + for (let ss = 0; ss < this.searches.length; ss++) { + if (this.searches[ss].field === this.last.field) { + this.last.label = this.searches[ss].label + } + } + if (this.last.multi) { + sInput.attr('placeholder', '[' + w2utils.lang('Multiple Fields') + ']') + } else { + sInput.attr('placeholder', w2utils.lang('Search') + ' ' + w2utils.lang(this.last.label, true)) + } + if (sInput.val() !== this.last.search) { + let val = this.last.search + const tmp = sInput._w2field + if (tmp) val = tmp.format(val) + sInput.val(val) + } + this.refreshSearch() + this.refreshBody() + // -- footer + if (this.show.footer) { + query(this.box).find(`#grid_${this.name}_footer`).html(this.getFooterHTML()).show() + } else { + query(this.box).find(`#grid_${this.name}_footer`).hide() + } + // all selected? + const sel = this.last.selection + const areAllSelected = (this.records.length > 0 && sel.indexes.length == this.records.length) + const areAllSearchedSelected = (sel.indexes.length > 0 && this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length) + if (areAllSelected || areAllSearchedSelected) { + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', true) + } else { + query(this.box).find('#grid_' + this.name + '_check_all').prop('checked', false) + } + // show number of selected + this.status() + // collapse all records + const rows = this.find({ 'w2ui.expanded': true }, true, true) + for (let r = 0; r < rows.length; r++) { + const tmp = this.records[rows[r]].w2ui + if (tmp && !Array.isArray(tmp.children)) { + tmp.expanded = false + } + } + // mark selection + if (this.markSearch) { + setTimeout(() => { + // mark all search strings + const search = [] + for (let s = 0; s < this.searchData.length; s++) { + const sdata = this.searchData[s] + const fld = this.getSearch(sdata.field) + if (!fld || fld.hidden) continue + const ind = this.getColumn(sdata.field, true) + search.push({ field: sdata.field, search: sdata.value, col: ind }) + } + if (search.length > 0) { + search.forEach((item) => { + const el = query(this.box).find('td[col="' + item.col + '"]:not(.w2ui-head)') + w2utils.marker(el, item.search) + }) + } + }, 50) + } + this.updateToolbar(this.last.selection) + // event after + edata.finish() + this.resize() + this.addRange('selection') + setTimeout(() => { // allow to render first + this.resize() // needed for horizontal scroll to show (do not remove) + this.scroll() + }, 1) + if (this.reorderColumns && !this.last.columnDrag) { + this.last.columnDrag = this.initColumnDrag() + } else if (!this.reorderColumns && this.last.columnDrag) { + this.last.columnDrag.remove() + } + return Date.now() - time + } + refreshSearch() { + if (this.multiSearch && this.searchData.length > 0) { + if (query(this.box).find('.w2ui-grid-searches').length == 0) { + query(this.box).find('.w2ui-grid-toolbar') + .css('height', (this.last.toolbar_height + 35) + 'px') + .append(`
`) + } + let searches = ` + +
` + this.searchData.forEach((sd, sd_ind) => { + const ind = this.getSearch(sd.field, true) + const sf = this.searches[ind] + let display + if (sf?.type == 'enum' && Array.isArray(sd.value)) { + display = `${sd.value.length}` + } else if (sf?.type == 'list') { + display = !!sd.text && sd.text !== sd.value ? `: ${sd.text}` : `: ${sd.value}` + } else { + display = `: ${sd.value}` + } + if (sf && sf.type == 'date') { + if (sd.operator == 'between') { + let dsp1 = sd.value[0] + let dsp2 = sd.value[1] + if (Number(dsp1) === dsp1) { + dsp1 = w2utils.formatDate(dsp1) + } + if (Number(dsp2) === dsp2) { + dsp2 = w2utils.formatDate(dsp2) + } + display = `: ${dsp1} - ${dsp2}` + } else { + let dsp = sd.value + if (Number(dsp) == dsp) { + dsp = w2utils.formatDate(dsp) + } + let oper = sd.operator + if (oper == 'more') oper = 'since' + if (oper == 'less') oper = 'before' + if (oper.substr(0, 5) == 'more:') { + oper = 'since' + } + display = `: ${oper} ${dsp}` + } + } + searches += ` + ${sf ? (sf.label ?? sf.field) : sd.field} + ${display} + + ` + }) + // clear and save + searches += ` + ${this.show.searchSave + ? `
+ + ` + : '' +} + + ` + query(this.box).find(`#grid_${this.name}_searches`).html(searches) + query(this.box).find(`#grid_${this.name}_search_logic`).html(w2utils.lang(this.last.logic == 'AND' ? 'All' : 'Any')) + } else { + query(this.box).find('.w2ui-grid-toolbar') + .css('height', this.last.toolbar_height + 'px') + .find('.w2ui-grid-searches') + .remove() + } + if (this.searchSelected) { + query(this.box).find(`#grid_${this.name}_search_all`).val(' ').prop('readOnly', true) + query(this.box).find(`#grid_${this.name}_search_name`).show().find('.name-text').html(this.searchSelected.text) + } else { + query(this.box).find(`#grid_${this.name}_search_all`).prop('readOnly', false) + query(this.box).find(`#grid_${this.name}_search_name`).hide().find('.name-text').html('') + } + w2utils.bindEvents(query(this.box).find(`#grid_${this.name}_searches .w2ui-action, #grid_${this.name}_searches button`), this) + } + refreshBody() { + this.scroll() // need to calculate virtual scrolling for columns + const recHTML = this.getRecordsHTML() + const colHTML = this.getColumnsHTML() + const bodyHTML = + '
' + + recHTML[0] + + '
' + + '
' + + recHTML[1] + + '
' + + '
' + + // Columns need to be after to be able to overlap + '
' + + ' ' + colHTML[0] + '
' + + '
' + + '
' + + ' ' + colHTML[1] + '
' + + '
' + + `` + const gridBody = query(this.box).find(`#grid_${this.name}_body`, this.box).html(bodyHTML) + const records = query(this.box).find(`#grid_${this.name}_records`, this.box) + const frecords = query(this.box).find(`#grid_${this.name}_frecords`, this.box) + if (this.selectType === 'row') { + records.on('mouseover mouseout', { delegate: 'tr' }, (event) => { + const ind = query(event.delegate).attr('index') // don't read recid directly as it could be a number or a string + const recid = this.records[ind]?.recid + query(this.box).find(`#grid_${this.name}_frec_${w2utils.escapeId(recid)}`) + .toggleClass('w2ui-record-hover', event.type === 'mouseover') + }) + frecords.on('mouseover mouseout', { delegate: 'tr' }, (event) => { + const ind = query(event.delegate).attr('index') // don't read recid directly as it could be a number or a string + const recid = this.records[ind]?.recid + query(this.box).find(`#grid_${this.name}_rec_${w2utils.escapeId(recid)}`) + .toggleClass('w2ui-record-hover', event.type === 'mouseover') + }) + } + if (w2utils.isMobile) { + records.append(frecords) + .on('click', { delegate: 'tr' }, (event) => { + const index = query(event.delegate).attr('index') // don't read recid directly as it could be a number or a string + const recid = this.records[index]?.recid + this.click(recid, event) + }) + .on('touchstart', { delegate: 'tr' }, (event) => { + const index = query(event.delegate).attr('index') // don't read recid directly as it could be a number or a string + const recid = this.records[index]?.recid + // emulate double click + if (this.last.mobile_touch && Date.now() - this.last.mobile_touch < 350) { + event.preventDefault() + this.dblClick(recid, event) + } + this.last.mobile_touch = Date.now() + setTimeout(() => this.last.mobile_touch = null, 350) + }) + .on('contextmenu', { delegate: 'tr' }, (event) => { + const index = parseInt(query(event.delegate).attr('index')) // don't read recid directly as it could be a number or a string + const recid = this.records[index]?.recid + const td = query(event.target).closest('td') + const column = td.attr('col') ? parseInt(td.attr('col')) : null + this.showContextMenu(event, { recid, column, index }) + }) + } else { + records.add(frecords) + .on('click', { delegate: 'tr' }, (event) => { + const index = query(event.delegate).attr('index') // don't read recid directly as it could be a number or a string + const recid = this.records[index]?.recid + // do not generate click if empty record is clicked + if (recid !== '-none-' && !this.last.inEditMode) { + this.click(recid, event) + } + }) + .on('contextmenu', { delegate: 'tr' }, (event) => { + const index = parseInt(query(event.delegate).attr('index')) // don't read recid directly as it could be a number or a string + const recid = this.records[index]?.recid + const td = query(event.target).closest('td') + const column = td.attr('col') ? parseInt(td.attr('col')) : null + this.showContextMenu(event, { recid, column, index }) + }) + .on('mouseover', { delegate: 'tr' }, (event) => { + this.last.rec_out = false + const index = query(event.delegate).attr('index') // don't read recid directly as it could be a number or a string + const recid = this.records[index]?.recid + if (index !== this.last.rec_over) { + this.last.rec_over = index + // setTimeout is needed for correct event order enter/leave + setTimeout(() => { + delete this.last.rec_out + const edata = this.trigger('mouseEnter', { target: this.name, originalEvent: event, index, recid }) + edata.finish() + }) + } + }) + .on('mouseout', { delegate: 'tr' }, (event) => { + const index = query(event.delegate).attr('index') // don't read recid directly as it could be a number or a string + const recid = this.records[index]?.recid + this.last.rec_out = true + // setTimeouts are needed for correct event order enter/leave + setTimeout(() => { + const recLeave = () => { + const edata = this.trigger('mouseLeave', { target: this.name, originalEvent: event, index, recid }) + edata.finish() + } + if (index !== this.last.rec_over) { + recLeave() + } + setTimeout(() => { + if (this.last.rec_out) { + delete this.last.rec_out + delete this.last.rec_over + recLeave() + } + }) + }) + }) + } + // enable scrolling on frozen records, + gridBody + .data('scroll', { lastDelta: 0, lastTime: 0 }) + .find('.w2ui-grid-frecords') + .on('mousewheel DOMMouseScroll ', (event) => { + event.preventDefault() + // TODO: improve, scroll is not smooth, if scrolled to the end, it takes a while to return + const scroll = gridBody.data('scroll') + const container = gridBody.find('.w2ui-grid-records') + let amount = typeof event.wheelDelta !== 'undefined' ? -event.wheelDelta : (event.detail || event.deltaY) + const newScrollTop = container.prop('scrollTop') + scroll.lastDelta += amount + amount = Math.round(scroll.lastDelta) + gridBody.data('scroll', scroll) + // make scroll amount dependent on visible rows + // amount *= (Math.round(records.prop('clientHeight') / self.recordHeight) - 1) * self.recordHeight / 4 + container.get(0).scroll({ top: newScrollTop + amount, behavior: 'smooth' }) + }) + // scroll on records (and frozen records) + records.off('.body-global') + .on('scroll.body-global', { delegate: '.w2ui-grid-records' }, event => { + this.scroll(event) + }) + query(this.box).find('.w2ui-grid-body') // gridBody + .off('.body-global') + // header column click + .on('click.body-global dblclick.body-global contextmenu.body-global', { delegate: 'td.w2ui-head' }, event => { + const col_ind = parseInt(query(event.delegate).attr('col')) + const col = this.columns[col_ind] ?? { field: col_ind } // it could be line number + switch (event.type) { + case 'click': + this.columnClick(col.field, event) + break + case 'dblclick': + this.columnDblClick(col.field, event) + break + case 'contextmenu': + if (this.show.columnMenu) { + this.columnContextMenu(col.field, event) + } else { + this.showContextMenu(event, { column: col_ind, recid: null, index: null }) + } + break + } + }) + .on('mouseover.body-global', { delegate: '.w2ui-col-header' }, event => { + const col = query(event.delegate).parent().attr('col') + this.columnTooltipShow(col, event) + query(event.delegate) + .off('.tooltip') + .on('mouseleave.tooltip', () => { + this.columnTooltipHide(col, event) + }) + }) + // select all + .on('click.body-global', { delegate: 'input.w2ui-select-all' }, event => { + if (event.delegate.checked) { this.selectAll() } else { this.selectNone() } + event.stopPropagation() + clearTimeout(this.last.kbd_timer) // keep grid in focus + }) + // tree-like grid (or expandable column) expand/collapse + .on('click.body-global', { delegate: '.w2ui-show-children, .w2ui-col-expand' }, event => { + event.stopPropagation() + const ind = query(event.target).parents('tr').attr('index') + this.toggle(this.records[ind].recid) + }) + // info bubbles + .on('click.body-global mouseover.body-global', { delegate: '.w2ui-info' }, event => { + const td = query(event.delegate).closest('td') + const tr = td.parent() + const col = this.columns[td.attr('col')] + const isSummary = tr.parents('.w2ui-grid-body').hasClass('w2ui-grid-summary') + if (['mouseenter', 'mouseover'].includes(col.info?.showOn?.toLowerCase()) && event.type == 'mouseover') { + this.showBubble(parseInt(tr.attr('index')), parseInt(td.attr('col')), isSummary) + .then(() => { + query(event.delegate) + .off('.tooltip') + .on('mouseleave.tooltip', () => { w2tooltip.hide(this.name + '-bubble') }) + }) + } else if (event.type == 'click') { + w2tooltip.hide(this.name + '-bubble') + this.showBubble(parseInt(tr.attr('index')), parseInt(td.attr('col')), isSummary) + } + }) + // clipborad copy icon + .on('mouseover.body-global', { delegate: '.w2ui-clipboard-copy' }, event => { + if (event.delegate._tooltipShow) return + const td = query(event.delegate).parent() + const tr = td.parent() + const col = this.columns[td.attr('col')] + const isSummary = tr.parents('.w2ui-grid-body').hasClass('w2ui-grid-summary') + w2tooltip.show({ + name: this.name + '-bubble', + anchor: event.delegate, + html: w2utils.lang(typeof col.clipboardCopy === 'string' ? col.clipboardCopy : 'Copy to clipboard'), + position: 'top|bottom', + offsetY: -2 + }) + .hide(evt => { + event.delegate._tooltipShow = false + query(event.delegate).off('.tooltip') + }) + query(event.delegate) + .off('.tooltip') + .on('mouseleave.tooltip', evt => { + w2tooltip.hide(this.name + '-bubble') + }) + .on('click.tooltip', evt => { + evt.stopPropagation() + w2tooltip.update(this.name + '-bubble', w2utils.lang('Copied')) + this.clipboardCopy(tr.attr('index'), td.attr('col'), isSummary) + }) + event.delegate._tooltipShow = true + }) + .on('click.body-global', { delegate: '.w2ui-editable-checkbox' }, event => { + const dt = query(event.delegate).data() + this.editChange.call(this, event.delegate, dt.changeind, dt.colind, event) + this.updateToolbar() + }) + // show empty message + if (this.records.length === 0 && this.msgEmpty) { + query(this.box).find(`#grid_${this.name}_body`) + .append(`
${w2utils.lang(this.msgEmpty)}
`) + } else if (query(this.box).find(`#grid_${this.name}_empty_msg`).length > 0) { + query(this.box).find(`#grid_${this.name}_empty_msg`).remove() + } + // show summary records + if (this.summary.length > 0) { + const sumHTML = this.getSummaryHTML() + query(this.box).find(`#grid_${this.name}_fsummary`).html(sumHTML[0]).show() + query(this.box).find(`#grid_${this.name}_summary`).html(sumHTML[1]).show() + } else { + query(this.box).find(`#grid_${this.name}_fsummary`).hide() + query(this.box).find(`#grid_${this.name}_summary`).hide() + } + } + render(box) { + const time = Date.now() + const obj = this + if (typeof box === 'string') box = query(box).get(0) + // event before + const edata = this.trigger('render', { target: this.name, box: box ?? this.box }) + if (edata.isCancelled === true) return + // default action + if (box != null) { + this.unmount() // clean previous control + this.box = box + } + if (!this.box) return + const url = this.url?.get ?? this.url + // reset needed if grid existed + this.reset(true) + // insert elements + query(this.box) + .attr('name', this.name) + .addClass('w2ui-reset w2ui-grid w2ui-inactive') + .html('
' + + '
' + + '
' + + '
' + + '
' + + '
' + + ' ' + + ' ' + // readonly needed on android not to open keyboard + '
') + if (this.selectType != 'row') query(this.box).addClass('w2ui-ss') + if (query(this.box).length > 0) query(this.box)[0].style.cssText += this.style + // render toolbar + const tb_box = query(this.box).find(`#grid_${this.name}_toolbar`) + if (this.toolbar != null) this.toolbar.render(tb_box[0]) + this.last.toolbar_height = tb_box.prop('offsetHeight') + // re-init search_all + if (this.last.field && this.last.field != 'all') { + const sd = this.searchData + setTimeout(() => { this.searchInitInput(this.last.field, (sd.length == 1 ? sd[0].value : null)) }, 1) + } + // init footer + query(this.box).find(`#grid_${this.name}_footer`).html(this.getFooterHTML()) + // refresh + if (!this.last.state) this.last.state = this.stateSave(true) // initial default state + this.stateRestore() + if (url) { this.clear(); this.refresh() } // show empty grid (need it) - should it be only for remote data source + // if hidden searches - apply it + let hasHiddenSearches = false + for (let i = 0; i < this.searches.length; i++) { + if (this.searches[i].hidden) { hasHiddenSearches = true; break } + } + if (hasHiddenSearches) { + this.searchReset(false) // will call reload + if (!url) setTimeout(() => { this.searchReset() }, 1) + } else { + this.reload() + } + // focus + query(this.box).find(`#grid_${this.name}_focus`) + .on('focus', (event) => { + clearTimeout(this.last.kbd_timer) + if (!this.hasFocus) this.focus() + }) + .on('blur', (event) => { + clearTimeout(this.last.kbd_timer) + this.last.kbd_timer = setTimeout(() => { + if (this.hasFocus) { this.blur() } + }, 100) // need this timer to be 100 ms + }) + .on('paste', (event) => { + if (this.keyboard !== true) { + event.preventDefault() + return + } + const cd = (event.clipboardData ? event.clipboardData : null) + if (cd) { + let items = cd.items + if (items.length == 2) { + if (items.length == 2 && items[1].kind == 'file') { + items = [items[1]] + } + if (items.length == 2 && items[0].type == 'text/plain' && items[1].type == 'text/html') { + items = [items[1]] + } + } + let items2send = [] + // might contain data in different formats, but it is a single paste + for (const index in items) { + const item = items[index] + if (item.kind === 'file') { + const file = item.getAsFile() + items2send.push({ kind: 'file', data: file }) + } else if (item.kind === 'string' && (item.type === 'text/plain' || item.type === 'text/html')) { + event.preventDefault() + let text = cd.getData('text/plain') + if (text.indexOf('\r') != -1 && text.indexOf('\n') == -1) { + text = text.replace(/\r/g, '\n') + } + items2send.push({ kind: (item.type == 'text/html' ? 'html' : 'text'), data: text }) + } + } + if (items2send.length === 1 && items2send[0].kind != 'file') { + items2send = items2send[0].data + } + w2ui[this.name].paste(items2send, event) + event.preventDefault() + } + }) + .on('keydown', function(event) { + w2ui[obj.name].keydown.call(w2ui[obj.name], event) + }) + // init mouse events for mouse selection + let edataCol // event for column select + query(this.box).off('mousedown.mouseStart').on('mousedown.mouseStart', mouseStart) + this.updateToolbar() + // event after + edata.finish() + // observe div resize + this.last.observeResize = new ResizeObserver(() => { + this.resize() + this.scroll() + }) + this.last.observeResize.observe(this.box) + return Date.now() - time + function mouseStart(event) { + if (event.which != 1) return // if not left mouse button + // restore css user-select + if (obj.last.userSelect == 'text') { + obj.last.userSelect = '' + query(obj.box).find('.w2ui-grid-body').css('user-select', 'none') + } + // regular record select + if (obj.selectType == 'row' && (query(event.target).parents().hasClass('w2ui-head') || query(event.target).hasClass('w2ui-head'))) return + if (obj.last.move && obj.last.move.type == 'expand') return + // if altKey - alow text selection + if (event.altKey) { + query(obj.box).find('.w2ui-grid-body').css('user-select', 'text') + obj.selectNone() + obj.last.move = { type: 'text-select' } + obj.last.userSelect = 'text' + } else { + let tmp = event.target + const pos = { + x: event.offsetX - 10, + y: event.offsetY - 10 + } + let tmps = false + while (tmp) { + if (tmp.classList && tmp.classList.contains('w2ui-grid')) break + if (tmp.tagName && tmp.tagName.toUpperCase() == 'TD') tmps = true + if (tmp.tagName && tmp.tagName.toUpperCase() != 'TR' && tmps == true) { + pos.x += tmp.offsetLeft + pos.y += tmp.offsetTop + } + tmp = tmp.parentNode + } + const index = query(event.target).parents('tr').attr('index') + const recid = obj.records[index]?.recid + // if cell selection, on initial click start selection + if (obj.selectType == 'cell' && !event.shiftKey) { + let column1 = parseInt(query(event.target).closest('td').attr('col')) + let column2 = column1 + if (isNaN(column1)) { + column1 = 0 + column2 = obj.columns.length - 1 + } + obj.addRange({ + name: 'selection-preview', + range: [{ recid, column: column1 }, { recid, column: column2 }], + class: 'w2ui-selection-preview' + }) + } + obj.last.move = { + x: event.screenX, + y: event.screenY, + divX: 0, + divY: 0, + focusX: pos.x, + focusY: pos.y, + recid: recid, + column: parseInt(event.target.tagName.toUpperCase() == 'TD' ? query(event.target).attr('col') : query(event.target).parents('td').attr('col')), + type: 'select', + ghost: false, + start: true + } + if (obj.last.move.recid == null && obj.records.length > 0) { + obj.last.move.type = 'select-column' + const column = parseInt(query(event.target).closest('td').attr('col')) + const start = obj.records[0].recid + const end = obj.records[obj.records.length - 1].recid + obj.addRange({ + name: 'selection-preview', + range: [{ recid: start, column }, { recid: end, column }], + class: 'w2ui-selection-preview' + }) + } + // set focus to grid + const target = event.target + const $input = query(obj.box).find('#grid_' + obj.name + '_focus') + // move input next to cursor so screen does not jump + if (obj.last.move) { + let sLeft = obj.last.move.focusX + let sTop = obj.last.move.focusY + const $owner = query(target).parents('table').parent() + if ($owner.hasClass('w2ui-grid-records') || $owner.hasClass('w2ui-grid-frecords') || + $owner.hasClass('w2ui-grid-columns') || $owner.hasClass('w2ui-grid-fcolumns') || + $owner.hasClass('w2ui-grid-summary')) { + sLeft = obj.last.move.focusX - query(obj.box).find('#grid_' + obj.name + '_records').prop('scrollLeft') + sTop = obj.last.move.focusY - query(obj.box).find('#grid_' + obj.name + '_records').prop('scrollTop') + } + if (query(target).hasClass('w2ui-grid-footer') || query(target).parents('div.w2ui-grid-footer').length > 0) { + sTop = query(obj.box).find('#grid_' + obj.name + '_footer').get(0).offsetTop + } + // if clicked on toolbar + if ($owner.hasClass('w2ui-scroll-wrapper') && $owner.parent().hasClass('w2ui-toolbar')) { + sLeft = obj.last.move.focusX - $owner.prop('scrollLeft') + } + $input.css({ + left: sLeft - 10, + top: sTop + }) + } + // if toolbar input is clicked + setTimeout(() => { + if (!obj.last.inEditMode) { + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) { + target.focus() + } else { + if ($input.get(0) !== document.active) $input.get(0)?.focus({ preventScroll: true }) + } + } + }, 50) + // disable click select for this condition + if (!obj.multiSelect && !obj.reorderRows && obj.last.move.type == 'drag') { + delete obj.last.move + } + } + if (obj.reorderRows == true) { + let el = event.target + if (el.tagName.toUpperCase() != 'TD') el = query(el).parents('td')[0] + if (query(el).hasClass('w2ui-col-number') || query(el).hasClass('w2ui-col-order')) { + obj.selectNone() + obj.last.move.reorder = true + // suppress hover + const eColor = query(obj.box).find('.w2ui-even.w2ui-empty-record').css('background-color') + const oColor = query(obj.box).find('.w2ui-odd.w2ui-empty-record').css('background-color') + query(obj.box).find('.w2ui-even td').filter(':not(.w2ui-col-number)').css('background-color', eColor) + query(obj.box).find('.w2ui-odd td').filter(':not(.w2ui-col-number)').css('background-color', oColor) + // display empty record and ghost record + const mv = obj.last.move + const recs = query(obj.box).find('.w2ui-grid-records') + if (!mv.ghost) { + const row = query(obj.box).find(`#grid_${obj.name}_rec_${mv.recid}`) + const tmp = row.parents('table').find('tr:first-child').get(0).cloneNode(true) + mv.offsetY = event.offsetY + mv.from = mv.recid + mv.pos = { top: row.get(0).offsetTop - 1, left: row.get(0).offsetLeft } + mv.ghost = query(row.get(0).cloneNode(true)) + mv.ghost.removeAttr('id') + mv.ghost.find('td').css({ + 'border-top': '1px solid silver', + 'border-bottom': '1px solid silver' + }) + row.find('td').remove() + row.append(`
`) + recs.append('
') + recs.append('
') + query(obj.box).find('#grid_' + obj.name + '_ghost').append(tmp).append(mv.ghost) + } + const ghost = query(obj.box).find('#grid_' + obj.name + '_ghost') + ghost.css({ + top: mv.pos.top + 'px', + left: mv.pos.left + 'px' + }) + } else { + obj.last.move.reorder = false + } + } + query(document) + .on('mousemove.w2ui-' + obj.name, mouseMove) + .on('mouseup.w2ui-' + obj.name, mouseStop) + // needed when grid grids are nested, see issue #1275 + event.stopPropagation() + } + function mouseMove(event) { + if (!event.target.tagName) { + // element has no tagName - most likely the target is the #document itself + // this can happen is you click+drag and move the mouse out of the DOM area, + // e.g. into the browser's toolbar area + return + } + const mv = obj.last.move + if (!mv || !['select', 'select-column'].includes(mv.type)) return + mv.divX = (event.screenX - mv.x) + mv.divY = (event.screenY - mv.y) + if (Math.abs(mv.divX) <= 1 && Math.abs(mv.divY) <= 1) return // only if moved more then 1px + obj.last.cancelClick = true + if (obj.reorderRows == true && obj.last.move.reorder) { + const tmp = query(event.target).parents('tr') + const ind = tmp.attr('index') + let recid = obj.records[ind]?.recid + if (recid == '-none-' || recid == null) recid = 'bottom' + if (recid != mv.from) { + // let row1 = query(obj.box).find('#grid_'+ obj.name + '_rec_'+ mv.recid) + const row2 = query(obj.box).find('#grid_' + obj.name + '_rec_' + recid) + query(obj.box).find('.insert-before') + row2.addClass('insert-before') + // MOVABLE GHOST + // if (event.screenY - mv.lastY < 0) row1.after(row2); else row2.after(row1); + mv.lastY = event.screenY + mv.to = recid + // line to insert before + const pos = { top: row2.get(0)?.offsetTop, left: row2.get(0)?.offsetLeft } + const ghost_line = query(obj.box).find('#grid_' + obj.name + '_ghost_line') + if (pos) { + ghost_line.css({ + top: pos.top + 'px', + left: mv.pos.left + 'px', + 'border-top': '2px solid #769EFC' + }) + } else { + ghost_line.css({ + 'border-top': '2px solid transparent' + }) + } + } + const ghost = query(obj.box).find('#grid_' + obj.name + '_ghost') + ghost.css({ + top: (mv.pos.top + mv.divY) + 'px', + left: mv.pos.left + 'px' + }) + return + } + if (obj.selectType == 'row' && mv.start && mv.recid) { + obj.selectNone() + mv.start = false + } + const newSel = [] + const ind = (event.target.tagName.toUpperCase() == 'TR' ? query(event.target).attr('index') : query(event.target).parents('tr').attr('index')) + const recid = obj.records[ind]?.recid + if (recid == null) { + // select by dragging columns + if (obj.selectType == 'row') return + if (obj.last.move && obj.last.move.type == 'select') return + const col = parseInt(query(event.target).parents('td').attr('col')) + if (isNaN(col)) { + obj.removeRange('column-selection') + query(obj.box).find('.w2ui-grid-columns .w2ui-col-header, .w2ui-grid-fcolumns .w2ui-col-header').removeClass('w2ui-col-selected') + query(obj.box).find('.w2ui-col-number').removeClass('w2ui-row-selected') + delete mv.colRange + } else { + // add all columns in between + let newRange = col + '-' + col + if (mv.column < col) newRange = mv.column + '-' + col + if (mv.column > col) newRange = col + '-' + mv.column + // array of selected columns + const cols = [] + const tmp = newRange.split('-') + for (let ii = parseInt(tmp[0]); ii <= parseInt(tmp[1]); ii++) { + cols.push(ii) + } + if (mv.colRange != newRange && mv.type == 'select-column') { + edataCol = obj.trigger('columnSelect', { target: obj.name, columns: cols }) + if (edataCol.isCancelled !== true) { + // show new range + mv.colRange = newRange + const start = obj.records[0].recid + const end = obj.records[obj.records.length - 1].recid + obj.addRange({ + name: 'selection-preview', + range: [{ recid: start, column: tmp[0] }, { recid: end, column: tmp[1] }], + class: 'w2ui-selection-preview' + }) + } + } + } + } else { // regular selection + let ind1 = obj.get(mv.recid, true) + // this happens when selection is started on summary row + if (ind1 == null || (obj.records[ind1] && obj.records[ind1].recid != mv.recid)) return + let ind2 = obj.get(recid, true) + // this happens when selection is extended into summary row (a good place to implement scrolling) + if (ind2 == null) return + let col1 = parseInt(mv.column) + let col2 = parseInt(event.target.tagName.toUpperCase() == 'TD' ? query(event.target).attr('col') : query(event.target).parents('td').attr('col')) + if (isNaN(col1) && isNaN(col2)) { // line number select entire record + col1 = 0 + col2 = obj.columns.length - 1 + } + if (ind1 > ind2) { const tmp = ind1; ind1 = ind2; ind2 = tmp } + // check if need to refresh + const tmp = 'ind1:' + ind1 + ',ind2;' + ind2 + ',col1:' + col1 + ',col2:' + col2 + if (mv.range == tmp) return + mv.range = tmp + for (let i = ind1; i <= ind2; i++) { + if (obj.last.searchIds.length > 0 && obj.last.searchIds.indexOf(i) == -1) continue + if (obj.selectType != 'row') { + if (col1 > col2) { const tmp = col1; col1 = col2; col2 = tmp } + for (let c = col1; c <= col2; c++) { + if (obj.columns[c].hidden) continue + newSel.push({ recid: obj.records[i].recid, column: parseInt(c) }) + } + } else { + newSel.push(obj.records[i].recid) + } + } + if (obj.selectType != 'row') { + const start = newSel[0] + const end = newSel[newSel.length - 1] + obj.addRange({ + name: 'selection-preview', + range: [{ recid: start?.recid, column: start?.column }, { recid: end?.recid, column: end?.column }], + class: 'w2ui-selection-preview' + }) + mv.newRange = newSel + } else { + if (obj.multiSelect) { + const sel = obj.getSelection() + for (let ns = 0; ns < newSel.length; ns++) { + if (sel.indexOf(newSel[ns]) == -1) obj.select(newSel[ns]) // add more items + } + for (let s = 0; s < sel.length; s++) { + if (newSel.indexOf(sel[s]) == -1) obj.unselect(sel[s]) // remove items + } + } + } + } + } + function mouseStop(event) { + const mv = obj.last.move + setTimeout(() => { + delete obj.last.cancelClick + }, 1) + if (query(event.target).parents().hasClass('.w2ui-head') || query(event.target).hasClass('.w2ui-head')) return + obj.removeRange('selection-preview') + if (mv && ['select', 'select-column'].includes(mv.type)) { + if (mv.colRange != null && edataCol.isCancelled !== true) { + const tmp = mv.colRange.split('-') + const sel = [] + for (let i = 0; i < obj.records.length; i++) { + const cols = [] + for (let j = parseInt(tmp[0]); j <= parseInt(tmp[1]); j++) cols.push(j) + sel.push({ recid: obj.records[i].recid, column: cols }) + } + edataCol.finish() + obj.selectNone(true) + obj.select(sel) + } else if (mv.newRange != null) { + obj.selectNone(true) + obj.select(...mv.newRange) + } + if (obj.reorderRows == true && obj.last.move.reorder) { + if (mv.to != null) { + // event + const edata = obj.trigger('reorderRow', { target: obj.name, recid: mv.from, moveBefore: mv.to }) + if (edata.isCancelled === true) { + resetRowReorder() + delete obj.last.move + return + } + // default behavior + const ind1 = obj.get(mv.from, true) + let ind2 = obj.get(mv.to, true) + if (mv.to == 'bottom') ind2 = obj.records.length // end of list + const tmp = obj.records[ind1] + // swap records + if (ind1 != null && ind2 != null) { + obj.records.splice(ind1, 1) + if (ind1 > ind2) { + obj.records.splice(ind2, 0, tmp) + } else { + obj.records.splice(ind2 - 1, 0, tmp) + } + } + // clear sortData + obj.sortData = [] + query(obj.box) + .find(`#grid_${obj.name}_columns .w2ui-col-header`) + .removeClass('w2ui-col-sorted') + resetRowReorder() + // event after + edata.finish() + } else { + resetRowReorder() + } + } + } + delete obj.last.move + query(document).off('.w2ui-' + obj.name) + } + function resetRowReorder() { + query(obj.box).find(`#grid_${obj.name}_ghost`).remove() + query(obj.box).find(`#grid_${obj.name}_ghost_line`).remove() + obj.refresh() + delete obj.last.move + } + } + unmount() { + super.unmount() + this.toolbar?.unmount() + this.last.observeResize?.disconnect() + } + destroy() { + // event before + const edata = this.trigger('destroy', { target: this.name }) + if (edata.isCancelled === true) return + // clean up + this.toolbar?.destroy?.() + if (query(this.box).find(`#grid_${this.name}_body`).length > 0) { + this.unmount() + } + delete w2ui[this.name] + // event after + edata.finish() + } + // =========================================== + // --- Internal Functions + initColumnOnOff() { + const items = [ + { id: 'line-numbers', text: 'Line #', checked: this.show.lineNumbers } + ] + // columns + for (let c = 0; c < this.columns.length; c++) { + const col = this.columns[c] + let text = this.columns[c].text + if (col.hideable === false) continue + if (!text && this.columns[c].tooltip) text = this.columns[c].tooltip + if (!text) text = '- column ' + (parseInt(c) + 1) + ' -' + items.push({ id: col.field, text: w2utils.stripTags(text), checked: !col.hidden }) + } + const url = this.url?.get ?? this.url + if ((url && this.show.skipRecords) || this.show.saveRestoreState) { + items.push({ text: '--' }) + } + // skip records + if (this.show.skipRecords) { + const skip = w2utils.lang('Skip') + + `` + + w2utils.lang('records') + items.push({ id: 'w2ui-skip', text: skip, group: false, icon: 'w2ui-icon-empty' }) + } + // save/restore state + if (this.show.saveRestoreState) { + items.push( + { id: 'w2ui-stateSave', text: w2utils.lang('Save Grid State'), icon: 'w2ui-icon-empty', group: false }, + { id: 'w2ui-stateReset', text: w2utils.lang('Restore Default State'), icon: 'w2ui-icon-empty', group: false } + ) + } + const selected = [] + items.forEach(item => { + item.text = w2utils.lang(item.text) // translate + if (item.checked) selected.push(item.id) + }) + this.toolbar.set('w2ui-column-on-off', { selected, items }) + return items + } + initColumnDrag(box) { + // throw error if using column groups + if (this.columnGroups && this.columnGroups.length) { + throw 'Draggable columns are not currently supported with column groups.' + } + const self = this + let dragData = { + pressed: false, + targetPos: null, + columnHead: null + } + const hasInvalidClass = (target, lastColumn) => { + const iClass = ['w2ui-col-number', 'w2ui-col-expand', 'w2ui-col-select'] + if (lastColumn !== true) iClass.push('w2ui-head-last') + for (let i = 0; i < iClass.length; i++) { + if (query(target).closest('.w2ui-head').hasClass(iClass[i])) { + return true + } + } + return false + } + // attach original event listener + query(self.box) + .off('.colDrag') + .on('mousedown.colDrag', dragColStart) + function dragColStart(event) { + if (dragData.pressed || dragData.numberPreColumnsPresent === 0 || event.button !== 0) return + let edata, columns, origColumn, origColumnNumber + const preColHeadersSelector = '.w2ui-head.w2ui-col-number, .w2ui-head.w2ui-col-expand, .w2ui-head.w2ui-col-select' + // do nothing if it is not a header + if (!query(event.target).parents().hasClass('w2ui-head') || hasInvalidClass(event.target)) return + dragData.pressed = true + dragData.initialX = event.pageX + dragData.initialY = event.pageY + dragData.numberPreColumnsPresent = query(self.box).find(preColHeadersSelector).length + // start event for drag start + dragData.columnHead = origColumn = query(event.target).closest('.w2ui-head') + dragData.originalPos = origColumnNumber = parseInt(origColumn.attr('col'), 10) + edata = self.trigger('columnDragStart', { originalEvent: event, origColumnNumber, target: origColumn[0] }) + if (edata.isCancelled === true) return false + columns = dragData.columns = query(self.box).find('.w2ui-head:not(.w2ui-head-last)') + // add events + query(document).on('mouseup.colDrag', dragColEnd) + query(document).on('mousemove.colDrag', dragColOver) + const col = self.columns[dragData.originalPos] + const colText = w2utils.lang(typeof col.text === 'function' ? col.text(col) : col.text) + dragData.ghost = query.html(`${colText}`)[0] + query(document.body).append(dragData.ghost) + query(dragData.ghost) + .css({ + display: 'none', + left: event.pageX, + top: event.pageY, + opacity: 1, + margin: '3px 0 0 20px', + padding: '3px', + 'background-color': 'white', + position: 'fixed', + 'z-index': 999999 + }) + .addClass('.w2ui-grid-ghost') + + // establish current offsets + dragData.offsets = [] + for (let i = 0, l = columns.length; i < l; i++) { + const rect = columns[i].getBoundingClientRect() + dragData.offsets.push(rect.left) + } + // conclude event + edata.finish() + } + function dragColOver(event) { + if (!dragData.pressed || !dragData.columnHead) return + const cursorX = event.pageX + const cursorY = event.pageY + if (!hasInvalidClass(event.target, true)) { + markIntersection(event) + } + trackGhost(cursorX, cursorY) + } + function dragColEnd(event) { + if (!dragData.pressed || !dragData.columnHead) return + dragData.pressed = false + let edata, target, selected, columnConfig + const finish = () => { + const ghosts = query(self.box).find('.w2ui-grid-ghost') + query(self.box).find('.w2ui-intersection-marker').hide() + query(dragData.ghost).remove() + ghosts.remove() + // dragData.columns.css({ overflow: '' }).children('div').css({ overflow: '' }); + query(document).off('.colDrag') + dragData = {} + } + // if no move, then click event for sorting + if (event.pageX == dragData.initialX && event.pageY == dragData.initialY) { + self.columnClick(self.columns[dragData.originalPos].field, event) + finish() + return + } + // start event for drag start + edata = self.trigger('columnDragEnd', { originalEvent: event, target: dragData.columnHead[0], dragData }) + if (edata.isCancelled === true) return false + selected = self.columns[dragData.originalPos] + columnConfig = self.columns + if (dragData.originalPos != dragData.targetPos && dragData.targetPos != null) { + columnConfig.splice(dragData.targetPos, 0, w2utils.clone(selected)) + columnConfig.splice(columnConfig.indexOf(selected), 1) + } + finish() + self.refresh() + edata.finish({ targetColumn: target - 1 }) + } + function markIntersection(event) { + // if mouse over is not over table + if (query(event.target).closest('td').length == 0) { + return + } + const td = query(event.target).closest('td') + const newPos = td.hasClass('w2ui-head-last') ? self.columns.length : parseInt(td.attr('col')) + if (dragData.targetPos != newPos) { + // if mouse over invalid column + const rect1 = query(self.box).find('.w2ui-grid-body').get(0).getBoundingClientRect() + const rect2 = query(event.target).closest('td').get(0).getBoundingClientRect() + query(self.box).find('.w2ui-intersection-marker') + .show() + .css({ + left: (rect2.left - rect1.left) + 'px', + height: rect2.height + 'px' + }) + dragData.targetPos = newPos + } + return + } + function trackGhost(cursorX, cursorY) { + query(dragData.ghost) + .css({ + left: (cursorX - 10) + 'px', + top: (cursorY - 10) + 'px' + }) + .show() + } + // return an object to remove drag if it has ever been enabled + return { + remove() { + query(self.box).off('.colDrag') + self.last.columnDrag = false + } + } + } + columnOnOff(event, field) { + // event before + const edata = this.trigger('columnOnOff', { target: this.name, field: field, originalEvent: event }) + if (edata.isCancelled === true) return + // collapse expanded rows + const rows = this.find({ 'w2ui.expanded': true }, true) + for (let r = 0; r < rows.length; r++) { + const tmp = this.records[r].w2ui + if (tmp && !Array.isArray(tmp.children)) { + this.records[r].w2ui.expanded = false + } + } + // show/hide + if (field == 'line-numbers') { + this.show.lineNumbers = !this.show.lineNumbers + this.refresh() + } else { + const col = this.getColumn(field) + if (col.hidden) { + this.showColumn(col.field) + } else { + this.hideColumn(col.field) + } + } + // event after + edata.finish() + } + initToolbar() { + // if it is already initiazlied + if (this.toolbar.render != null) { + return + } + let tb_items = this.toolbar.items || [] + this.toolbar.items = [] + this.toolbar = new w2toolbar(w2utils.extend({}, this.toolbar, { name: this.name + '_toolbar', owner: this })) + if (this.show.toolbarReload) { + this.toolbar.items.push(w2utils.extend({}, this.buttons.reload)) + } + if (this.show.toolbarColumns) { + this.toolbar.items.push(w2utils.extend({}, this.buttons.columns)) + } + if (this.show.toolbarSearch) { + const html = ` +
+ ${this.buttons.search.html} +
+ + + x +
+ +
+ +
+
` + this.toolbar.items.push({ + id: 'w2ui-search', + type: 'html', + html, + onRefresh: async(event) => { + await event.complete + const input = query(this.box).find(`#grid_${this.name}_search_all`) + w2utils.bindEvents(query(this.box).find(`#grid_${this.name}_search_all, .w2ui-action`), this) + // slow down live search calls + const slowSearch = w2utils.debounce((event) => { + const val = event.target.value + if (this.liveSearch && this.last.liveText != val) { + this.last.liveText = val + this.search(this.last.field, val) + } + }, 250) + input + .on('blur', () => { this.last.liveText = '' }) + .on('keyup', (event) => { + switch (event.keyCode) { + case 40: { + // show saved searches on arrow down + this.searchSuggest(true) + break + } + case 38: { + // hide saved searches on arrow up + this.searchSuggest(true, true) + break + } + case 13: { + // search on enter key + w2menu.hide(this.name + '-search-suggest') + this.search(this.last.field, event.target.value) + break + } + default: { + // live search (if enabled) + slowSearch(event) + break + } + } + }) + } + }) + } + if (Array.isArray(tb_items)) { + const ids = tb_items.map(item => item.id) + if (this.show.toolbarAdd && !ids.includes(this.buttons.add.id)) { + this.toolbar.items.push(w2utils.extend({}, this.buttons.add)) + } + if (this.show.toolbarEdit && !ids.includes(this.buttons.edit.id)) { + this.toolbar.items.push(w2utils.extend({}, this.buttons.edit)) + } + if (this.show.toolbarDelete && !ids.includes(this.buttons.delete.id)) { + this.toolbar.items.push(w2utils.extend({}, this.buttons.delete)) + } + if (this.show.toolbarSave && !ids.includes(this.buttons.save.id)) { + if (this.show.toolbarAdd || this.show.toolbarDelete || this.show.toolbarEdit) { + this.toolbar.items.push({ type: 'break', id: 'w2ui-break2' }) + } + this.toolbar.items.push(w2utils.extend({}, this.buttons.save)) + } + // fill in overwritten items with default buttons + // ids are w2ui-* but in this.buttons the map is just [add, edit, delete] + // must specify at least {id, name} in this.toolbar.items if you want to keep order + tb_items = tb_items.map(item => this.buttons[item.name] + ? w2utils.extend({}, this.buttons[item.name], item) : item) + } + // add original buttons + this.toolbar.items.push(...tb_items) + // ============================================= + // ------ Toolbar onClick processing + this.toolbar.on('click', (event) => { + const edata = this.trigger('toolbar', { target: event.target, originalEvent: event }) + if (edata.isCancelled === true) return + let edata2 + switch (event.detail.item.id) { + case 'w2ui-reload': + edata2 = this.trigger('reload', { target: this.name }) + if (edata2.isCancelled === true) return false + this.reload() + edata2.finish() + break + case 'w2ui-column-on-off': + // TODO: tap on columns will hide menu before opening, only in grid not in toolbar + if (event.detail.subItem) { + const id = event.detail.subItem.id + if (['w2ui-stateSave', 'w2ui-stateReset'].includes(id)) { + this[id.substring(5)]() + } else if (id == 'w2ui-skip') { + // empty + } else { + this.columnOnOff(event, event.detail.subItem.id) + } + } else { + this.initColumnOnOff() + // init input control with records to skip + setTimeout(() => { + query(`#w2overlay-${this.name}_toolbar-drop .w2ui-grid-skip`) + .off('.w2ui-grid') + .on('click.w2ui-grid', evt => { + evt.stopPropagation() + }) + .on('keypress', evt => { + if (evt.keyCode == 13) { + this.skip(evt.target.value) + this.toolbar.click('w2ui-column-on-off') // close menu + } + }) + }, 100) + } + break + case 'w2ui-add': + // events + edata2 = this.trigger('add', { target: this.name, recid: null }) + if (edata2.isCancelled === true) return false + edata2.finish() + break + case 'w2ui-edit': { + const sel = this.getSelection() + let recid = null + if (sel.length == 1) recid = sel[0] + // events + edata2 = this.trigger('edit', { target: this.name, recid: recid }) + if (edata2.isCancelled === true) return false + edata2.finish() + break + } + case 'w2ui-delete': + this.delete() + break + case 'w2ui-save': + this.save() + break + } + // no default action + edata.finish() + }) + this.toolbar.on('refresh', (event) => { + if (event.target == 'w2ui-search') { + const sd = this.searchData + setTimeout(() => { + this.searchInitInput(this.last.field, (sd.length == 1 ? sd[0].value : null)) + }, 1) + } + }) + } + initResize() { + const obj = this + query(this.box).find('.w2ui-resizer') + .off('.grid-col-resize') + .on('click.grid-col-resize', function(event) { + event.stopPropagation() + event.preventDefault() + }) + .on('mousedown.grid-col-resize', function(event) { + if (!event) event = window.event + obj.last.colResizing = true + obj.last.tmp = { + x: event.screenX, + y: event.screenY, + gx: event.screenX, + gy: event.screenY, + col: parseInt(query(this).attr('name')) + } + // find tds that will be resized + obj.last.tmp.tds = query(obj.box).find('#grid_' + obj.name + '_body table tr:first-child td[col="' + obj.last.tmp.col + '"]') + event.stopPropagation() + event.preventDefault() + // fix sizes + for (let c = 0; c < obj.columns.length; c++) { + if (obj.columns[c].hidden) continue + if (obj.columns[c].sizeOriginal == null) obj.columns[c].sizeOriginal = obj.columns[c].size + obj.columns[c].size = obj.columns[c].sizeCalculated + } + const edata = obj.trigger('columnResize', { + target: obj.name, resizeBy: 0, originalEvent: event, + column: obj.last.tmp.col, field: obj.columns[obj.last.tmp.col].field + }) + // set move event + let timer + const mouseMove = function(event) { + if (obj.last.colResizing != true) return + if (!event) event = window.event + // event before + const edata2 = obj.trigger('columnResizeMove', w2utils.extend(edata.detail, { resizeBy: (event.screenX - obj.last.tmp.gx), originalEvent: event })) + if (edata2.isCancelled === true) { return } + // default action + obj.last.tmp.x = (event.screenX - obj.last.tmp.x) + obj.last.tmp.y = (event.screenY - obj.last.tmp.y) + const newWidth = (parseInt(obj.columns[obj.last.tmp.col].size) + obj.last.tmp.x) + 'px' + obj.columns[obj.last.tmp.col].size = newWidth + if (timer) clearTimeout(timer) + timer = setTimeout(() => { + obj.resizeRecords() + obj.scroll() + }, 100) + // quick resize + obj.last.tmp.tds.css({ width: newWidth }) + // reset + obj.last.tmp.x = event.screenX + obj.last.tmp.y = event.screenY + // event after + edata2.finish() + } + const mouseUp = function(event) { + query(document).off('.grid-col-resize') + obj.resizeRecords() + obj.scroll() + // event after + edata.finish({ originalEvent: event }) + // need timeout to finish processing events + setTimeout(() => { obj.last.colResizing = false }, 1) + } + query(document) + .off('.grid-col-resize') + .on('mousemove.grid-col-resize', mouseMove) + .on('mouseup.grid-col-resize', mouseUp) + }) + .on('dblclick.grid-col-resize', function(event) { + const ind = parseInt(query(this).attr('name')) + obj.columnAutoSize(ind) + // prevent default + event.stopPropagation() + event.preventDefault() + }) + .each(el => { + const td = query(el).get(0).parentNode + query(el).css({ + 'height': td.clientHeight + 'px', + 'margin-left': (td.clientWidth - 3) + 'px' + }) + }) + } + resizeBoxes() { + // elements + const header = query(this.box).find(`#grid_${this.name}_header`) + const toolbar = query(this.box).find(`#grid_${this.name}_toolbar`) + const fsummary = query(this.box).find(`#grid_${this.name}_fsummary`) + const summary = query(this.box).find(`#grid_${this.name}_summary`) + const footer = query(this.box).find(`#grid_${this.name}_footer`) + const body = query(this.box).find(`#grid_${this.name}_body`) + if (this.show.header) { + header.css({ + top: '0px', + left: '0px', + right: '0px' + }) + } + if (this.show.toolbar) { + toolbar.css({ + top: (0 + (this.show.header ? w2utils.getSize(header, 'height') : 0)) + 'px', + left: '0px', + right: '0px' + }) + } + if (this.summary.length > 0) { + fsummary.css({ + bottom: (0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0)) + 'px' + }) + summary.css({ + bottom: (0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0)) + 'px', + right: '0px' + }) + } + if (this.show.footer) { + footer.css({ + bottom: '0px', + left: '0px', + right: '0px' + }) + } + body.css({ + top: (0 + (this.show.header ? w2utils.getSize(header, 'height') : 0) + (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0)) + 'px', + bottom: (0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0) + (this.summary.length > 0 ? w2utils.getSize(summary, 'height') : 0)) + 'px', + left: '0px', + right: '0px' + }) + } + resizeRecords() { + const obj = this + // remove empty records + query(this.box).find('.w2ui-empty-record').remove() + // -- Calculate Column size in PX + const box = query(this.box) + const grid = query(this.box).find(':scope > div.w2ui-grid-box') + const header = query(this.box).find(`#grid_${this.name}_header`) + const toolbar = query(this.box).find(`#grid_${this.name}_toolbar`) + const summary = query(this.box).find(`#grid_${this.name}_summary`) + const fsummary = query(this.box).find(`#grid_${this.name}_fsummary`) + const footer = query(this.box).find(`#grid_${this.name}_footer`) + const body = query(this.box).find(`#grid_${this.name}_body`) + const columns = query(this.box).find(`#grid_${this.name}_columns`) + const fcolumns = query(this.box).find(`#grid_${this.name}_fcolumns`) + const records = query(this.box).find(`#grid_${this.name}_records`) + const frecords = query(this.box).find(`#grid_${this.name}_frecords`) + const scroll1 = query(this.box).find(`#grid_${this.name}_scroll1`) + let lineNumberWidth = String(this.total).length * 8 + 10 + if (lineNumberWidth < 34) lineNumberWidth = 34 // 3 digit width + if (this.lineNumberWidth != null) lineNumberWidth = this.lineNumberWidth + let bodyOverflowX = false + let bodyOverflowY = false + let sWidth = 0 + for (let i = 0; i < this.columns.length; i++) { + if (this.columns[i].frozen || this.columns[i].hidden) continue + const cSize = parseInt(this.columns[i].sizeCalculated ? this.columns[i].sizeCalculated : this.columns[i].size) + sWidth += cSize + } + if (records[0]?.clientWidth < sWidth) bodyOverflowX = true + if (body[0]?.clientHeight - (columns[0]?.clientHeight ?? 0) < + (query(records).find(':scope > table')[0]?.clientHeight ?? 0) + (bodyOverflowX ? w2utils.scrollBarSize() : 0)) { + bodyOverflowY = true + } + // body might be expanded by data + if (!this.fixedBody) { + // allow it to render records, then resize + const bodyHeight = w2utils.getSize(columns, 'height') + + w2utils.getSize(query(this.box).find('#grid_' + this.name + '_records table'), 'height') + + (bodyOverflowX ? w2utils.scrollBarSize() : 0) + const calculatedHeight = bodyHeight + + (this.show.header ? w2utils.getSize(header, 'height') : 0) + + (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0) + + (summary.css('display') != 'none' ? w2utils.getSize(summary, 'height') : 0) + + (this.show.footer ? w2utils.getSize(footer, 'height') : 0) + grid.css('height', calculatedHeight + 'px') + body.css('height', bodyHeight + 'px') + box.css('height', w2utils.getSize(grid, 'height') + 'px') + } else { + // fixed body height + const calculatedHeight = grid[0]?.clientHeight - + (this.show.header ? w2utils.getSize(header, 'height') : 0) - + (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0) - + (summary.css('display') != 'none' ? w2utils.getSize(summary, 'height') : 0) - + (this.show.footer ? w2utils.getSize(footer, 'height') : 0) + body.css('height', calculatedHeight + 'px') + } + let buffered = this.records.length + const url = this.url?.get ?? this.url + if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length + // apply overflow + if (!this.fixedBody) { bodyOverflowY = false } + if (bodyOverflowX || bodyOverflowY) { + columns.find(':scope > table > tbody > tr:nth-child(1) td.w2ui-head-last') + .css('width', w2utils.scrollBarSize() + 'px') + .show() + records.css({ + top: ((this.columnGroups.length > 0 && this.show.columns ? 1 : 0) + w2utils.getSize(columns, 'height')) + 'px', + '-webkit-overflow-scrolling': 'touch', + 'overflow-x': (bodyOverflowX ? 'auto' : 'hidden'), + 'overflow-y': (bodyOverflowY ? 'auto' : 'hidden') + }) + } else { + columns.find(':scope > table > tbody > tr:nth-child(1) td.w2ui-head-last').hide() + records.css({ + top: ((this.columnGroups.length > 0 && this.show.columns ? 1 : 0) + w2utils.getSize(columns, 'height')) + 'px', + overflow: 'hidden' + }) + if (records.length > 0) { this.last.vscroll.scrollTop = 0; this.last.vscroll.scrollLeft = 0 } // if no scrollbars, always show top + } + if (bodyOverflowX) { + frecords.css('margin-bottom', w2utils.scrollBarSize() + 'px') + scroll1.show() + } else { + frecords.css('margin-bottom', 0) + scroll1.hide() + } + frecords.css({ overflow: 'hidden', top: records.css('top') }) + if (this.show.emptyRecords && !bodyOverflowY) { + let max = Math.floor((records[0]?.clientHeight ?? 0) / this.recordHeight) - 1 + let leftover = 0 + if (records[0]) leftover = records[0].scrollHeight - max * this.recordHeight + if (leftover >= this.recordHeight) { + leftover -= this.recordHeight + max++ + } + if (this.fixedBody) { + for (let di = buffered; di < max; di++) { + addEmptyRow(di, this.recordHeight, this) + } + addEmptyRow(max, leftover, this) + } + } + function addEmptyRow(row, height, grid) { + let html1 = '' + let html2 = '' + let htmlp = '' + html1 += '' + html2 += '' + if (grid.show.lineNumbers) html1 += '' + if (grid.show.selectColumn) html1 += '' + if (grid.show.expandColumn) html1 += '' + html2 += '' + if (grid.reorderRows) html2 += '' + for (let j = 0; j < grid.columns.length; j++) { + const col = grid.columns[j] + if ((col.hidden || j < grid.last.vscroll.colIndStart || j > grid.last.vscroll.colIndEnd) && !col.frozen) continue + htmlp = '' + if (col.frozen) html1 += htmlp; else html2 += htmlp + } + html1 += ' ' + html2 += ' ' + query(grid.box).find('#grid_' + grid.name + '_frecords > table').append(html1) + query(grid.box).find('#grid_' + grid.name + '_records > table').append(html2) + } + let width_box, percent + if (body.length > 0) { + let width_max = parseInt(body[0].clientWidth) - + (bodyOverflowY ? w2utils.scrollBarSize() : 0) - + (this.show.lineNumbers ? lineNumberWidth : 0) - + (this.reorderRows ? 26 : 0) - + (this.show.selectColumn ? 26 : 0) - + (this.show.expandColumn ? 26 : 0) - + 1 // left is 1px due to border width + width_box = width_max + percent = 0 + // gridMinWidth processing + let restart = false + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + if (col.gridMinWidth > 0) { + if (col.gridMinWidth > width_box && col.hidden !== true) { + col.hidden = true + restart = true + } + if (col.gridMinWidth < width_box && col.hidden === true) { + col.hidden = false + restart = true + } + } + } + if (restart === true) { + this.refresh() + return + } + // assign PX column s + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + if (col.hidden) continue + if (String(col.size).substr(String(col.size).length - 2).toLowerCase() == 'px') { + width_max -= parseFloat(col.size) + this.columns[i].sizeCalculated = col.size + this.columns[i].sizeType = 'px' + } else { + percent += parseFloat(col.size) + this.columns[i].sizeType = '%' + delete col.sizeCorrected + } + } + // if sum != 100% -- reassign proportionally + if (percent != 100 && percent > 0) { + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + if (col.hidden) continue + if (col.sizeType == '%') { + col.sizeCorrected = Math.round(parseFloat(col.size) * 100 * 100 / percent) / 100 + '%' + } + } + } + // calculate % columns + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + if (col.hidden) continue + if (col.sizeType == '%') { + if (this.columns[i].sizeCorrected != null) { + // make it 1px smaller, so margin of error can be calculated correctly + this.columns[i].sizeCalculated = Math.floor(width_max * parseFloat(col.sizeCorrected) / 100) - 1 + 'px' + } else { + // make it 1px smaller, so margin of error can be calculated correctly + this.columns[i].sizeCalculated = Math.floor(width_max * parseFloat(col.size) / 100) - 1 + 'px' + } + } + } + } + // fix margin of error that is due percentage calculations + let width_cols = 0 + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + if (col.hidden) continue + if (col.min == null) col.min = 20 + if (parseInt(col.sizeCalculated) < parseInt(col.min)) col.sizeCalculated = col.min + 'px' + if (parseInt(col.sizeCalculated) > parseInt(col.max)) col.sizeCalculated = col.max + 'px' + width_cols += parseInt(col.sizeCalculated) + } + let width_diff = parseInt(width_box) - parseInt(width_cols) + if (width_diff > 0 && percent > 0) { + let i = 0 + while (true) { + const col = this.columns[i] + if (col == null) { i = 0; continue } + if (col.hidden || col.sizeType == 'px') { i++; continue } + col.sizeCalculated = (parseInt(col.sizeCalculated) + 1) + 'px' + width_diff-- + if (width_diff === 0) break + i++ + } + } else if (width_diff > 0) { + columns.find(':scope > table > tbody > tr:nth-child(1) td.w2ui-head-last') + .css('width', w2utils.scrollBarSize() + 'px') + .show() + } + // find width of frozen columns + let fwidth = 1 + if (this.show.lineNumbers) fwidth += lineNumberWidth + if (this.show.selectColumn) fwidth += 26 + // if (this.reorderRows) fwidth += 26; + if (this.show.expandColumn) fwidth += 26 + for (let i = 0; i < this.columns.length; i++) { + if (this.columns[i].hidden) continue + if (this.columns[i].frozen) fwidth += parseInt(this.columns[i].sizeCalculated) + } + fcolumns.css('width', fwidth + 'px') + frecords.css('width', fwidth + 'px') + fsummary.css('width', fwidth + 'px') + scroll1.css('width', fwidth + 'px') + /** + * 0.5 is needed due to imperfection of table layout. There was a very small shift between right border of the column headers + * and records. I checked it had exact same offset, but still felt like 1px off. This adjustment fixes it. + */ + columns.css({ left: fwidth + 'px', 'padding-left': '0.5px' }) + records.css({ left: fwidth + 'px' }) + summary.css({ left: fwidth + 'px' }) + // resize columns + columns.find(':scope > table > tbody > tr:nth-child(1) td') + .add(fcolumns.find(':scope > table > tbody > tr:nth-child(1) td')) + .each(el => { + // line numbers + if (query(el).hasClass('w2ui-col-number')) { + query(el).css('width', lineNumberWidth + 'px') + } + // records + const ind = query(el).attr('col') + if (ind != null) { + if (ind == 'start') { + let width = 0 + for (let i = 0; i < obj.last.vscroll.colIndStart; i++) { + if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue + width += parseInt(obj.columns[i].sizeCalculated) + } + query(el).css('width', width + 'px') + } + if (obj.columns[ind]) query(el).css('width', obj.columns[ind].sizeCalculated) // already has px + } + // last column + if (query(el).hasClass('w2ui-head-last')) { + if (obj.last.vscroll.colIndEnd + 1 < obj.columns.length) { + let width = 0 + for (let i = obj.last.vscroll.colIndEnd + 1; i < obj.columns.length; i++) { + if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue + width += parseInt(obj.columns[i].sizeCalculated) + } + query(el).css('width', width + 'px') + } else { + query(el).css('width', w2utils.scrollBarSize() + (width_diff > 0 && percent === 0 ? width_diff : 0) + 'px') + } + } + }) + // if there are column groups - hide first row (needed for sizing) + if (columns.find(':scope > table > tbody > tr').length == 3) { + columns.find(':scope > table > tbody > tr:nth-child(1) td') + .add(fcolumns.find(':scope > table > tbody > tr:nth-child(1) td')) + .html('').css({ + 'height': '0', + 'border': '0', + 'padding': '0', + 'margin': '0' + }) + } + // resize records + records.find(':scope > table > tbody > tr:nth-child(1) td') + .add(frecords.find(':scope > table > tbody > tr:nth-child(1) td')) + .each(el => { + // line numbers + if (query(el).hasClass('w2ui-col-number')) { + query(el).css('width', lineNumberWidth + 'px') + } + // records + const ind = query(el).attr('col') + if (ind != null) { + if (ind == 'start') { + let width = 0 + for (let i = 0; i < obj.last.vscroll.colIndStart; i++) { + if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue + width += parseInt(obj.columns[i].sizeCalculated) + } + query(el).css('width', width + 'px') + } + if (obj.columns[ind]) query(el).css('width', obj.columns[ind].sizeCalculated) + } + // last column + if (query(el).hasClass('w2ui-grid-data-last') && query(el).parents('.w2ui-grid-frecords').length === 0) { // not in frecords + if (obj.last.vscroll.colIndEnd + 1 < obj.columns.length) { + let width = 0 + for (let i = obj.last.vscroll.colIndEnd + 1; i < obj.columns.length; i++) { + if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue + width += parseInt(obj.columns[i].sizeCalculated) + } + query(el).css('width', width + 'px') + } else { + query(el).css('width', (width_diff > 0 && percent === 0 ? width_diff : 0) + 'px') + } + } + }) + // resize summary + summary.find(':scope > table > tbody > tr:nth-child(1) td') + .add(fsummary.find(':scope > table > tbody > tr:nth-child(1) td')) + .each(el => { + // line numbers + if (query(el).hasClass('w2ui-col-number')) { + query(el).css('width', lineNumberWidth + 'px') + } + // records + const ind = query(el).attr('col') + if (ind != null) { + if (ind == 'start') { + let width = 0 + for (let i = 0; i < obj.last.vscroll.colIndStart; i++) { + if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue + width += parseInt(obj.columns[i].sizeCalculated) + } + query(el).css('width', width + 'px') + } + if (obj.columns[ind]) query(el).css('width', obj.columns[ind].sizeCalculated) + } + // last column + if (query(el).hasClass('w2ui-grid-data-last') && query(el).parents('.w2ui-grid-frecords').length === 0) { // not in frecords + query(el).css('width', w2utils.scrollBarSize() + (width_diff > 0 && percent === 0 ? width_diff : 0) + 'px') + } + }) + this.initResize() + this.refreshRanges() + // apply last scroll if any + if ((this.last.vscroll.scrollTop || this.last.vscroll.scrollLeft) && records.length > 0) { + columns.prop('scrollLeft', this.last.vscroll.scrollLeft) + records.prop('scrollTop', this.last.vscroll.scrollTop) + records.prop('scrollLeft', this.last.vscroll.scrollLeft) + } + // Improved performance when scrolling through tables + columns.css('will-change', 'scroll-position') + } + getSearchesHTML() { + let html = ` +
+ ${w2utils.lang('Advanced Search')} + + + +
+ ` + const columns = [] + let col_ind = 0 + columns.push('
') + for (let i = 0; i < this.searches.length; i++) { + const s = this.searches[i] + s.type = String(s.type).toLowerCase() + if (s.hidden) continue + if (s.type == 'new-column') { + columns[col_ind] += '
' + columns.push('
') + col_ind++ + continue + } + if (s.attr == null) s.attr = '' + if (s.text == null) s.text = '' + if (s.style == null) s.style = '' + if (s.type == null) s.type = 'text' + if (s.label == null && s.caption != null) { + console.log('NOTICE: grid search.caption property is deprecated, please use search.label. Search ->', s) + s.label = s.caption + } + const operator = ` + + ` + columns[col_ind] += ` + + + ' + + '' + } + columns[col_ind] += '
${(w2utils.lang(s.label ?? s.field) || '')}${operator}` + let tmpStyle + switch (s.type) { + case 'text': + case 'alphanumeric': + case 'hex': + case 'color': + case 'list': + case 'combo': + case 'enum': + tmpStyle = 'width: 250px;' + if (['hex', 'color'].indexOf(s.type) != -1) tmpStyle = 'width: 90px;' + columns[col_ind] += `` + break + case 'int': + case 'float': + case 'money': + case 'currency': + case 'percent': + case 'date': + case 'time': + case 'datetime': + tmpStyle = 'width: 90px;' + if (s.type == 'datetime') tmpStyle = 'width: 140px;' + columns[col_ind] += ` + ` + break + case 'select': + columns[col_ind] += `` + break + } + columns[col_ind] += s.text + + '
' + html += ` +
+ ${columns.join('')} +
+
+ +
+ + +
+
+ ` + return html + } + getOperators(type, opers) { + let operators = this.operators[this.operatorsMap[type]] || [] + if (opers != null && Array.isArray(opers)) { + operators = opers + } + let html = '' + operators.forEach(oper => { + let displayText = oper + let operValue = oper + if (Array.isArray(oper)) { + displayText = oper[1] + operValue = oper[0] + } else if (w2utils.isPlainObject(oper)) { + displayText = oper.text + operValue = oper.oper + } + if (displayText == null) displayText = oper + html += `\n` + }) + return html + } + initOperator(ind) { + let options + const search = this.searches[ind] + const sdata = this.getSearchData(search.field) + const overlay = query(`#w2overlay-${this.name}-search-overlay`) + const $rng = overlay.find(`#grid_${this.name}_range_${ind}`) + const $fld1 = overlay.find(`#grid_${this.name}_field_${ind}`) + const $fld2 = overlay.find(`#grid_${this.name}_field2_${ind}`) + const $oper = overlay.find(`#grid_${this.name}_operator_${ind}`) + const oper = $oper.val() + $fld1.show() + $rng.hide() + // init based on operator value + switch (oper) { + case 'between': + $rng.css('display', 'inline') + break + case 'null': + case 'not null': + $fld1.hide() + $fld1.val(oper) // need to insert something for search to activate + $fld1.trigger('change') + break + } + // init based on search type + switch (search.type) { + case 'text': + case 'alphanumeric': + const fld = $fld1[0]._w2field + if (fld) { fld.reset() } + break + case 'int': + case 'float': + case 'hex': + case 'color': + case 'money': + case 'currency': + case 'percent': + case 'date': + case 'time': + case 'datetime': + if (!$fld1[0]._w2field) { + // init fields + new w2field(search.type, { el: $fld1[0], ...search.options }) + new w2field(search.type, { el: $fld2[0], ...search.options }) + setTimeout(() => { // convert to date if it is number + $fld1.trigger('keydown') + $fld2.trigger('keydown') + }, 1) + } + break + case 'list': + case 'combo': + case 'enum': + options = search.options + if (search.type == 'list') options.selected = {} + if (search.type == 'enum') options.selected = [] + if (sdata) options.selected = sdata.value + if (!$fld1[0]._w2field) { + const fld = new w2field(search.type, { + el: $fld1[0], + ...options, + onSelect: async(event) => { + await event.complete + this.initSearchLists(search.field) + }, + onRemove: async(event) => { + await event.complete + this.initSearchLists(search.field) + } + }) + if (sdata && sdata.text != null) { + fld.set({ id: sdata.value, text: sdata.text }) + } + search._w2field = fld + } + break + case 'select': + // build options + options = '' + for (let i = 0; i < search.options.items.length; i++) { + const si = search.options.items[i] + if (w2utils.isPlainObject(search.options.items[i])) { + let val = si.id + let txt = si.text + if (val == null && si.value != null) val = si.value + if (txt == null && si.text != null) txt = si.text + if (val == null) val = '' + options += '' + } else { + options += '' + } + } + $fld1.html(options) + break + } + this.initSearchLists() + } + initSearchLists(changedField) { + const fields = this.getSearch() + fields.forEach(field => { + const search = this.getSearch(field) + if (search.options?.parentList != null) { + const parent = this.getSearch(search.options.parentList) + let values = this.getSearch(parent.field)._w2field?.get() + if (Array.isArray(values)) { + values = values.map(vv => vv.id) + } else { + values = values?.id != null ? [values.id] : [] + } + search._w2field?.options?.items?.forEach?.(item => { + const parent = w2utils.getNested(item, search.options.parentField ?? 'parentId') + if (parent == null) { + return + } + const possible = w2utils.clone(Array.isArray(parent) ? parent : [parent]) + possible.unshift('') + const includes = values.some(item => possible.includes(item)) + if (includes && item.hidden === true) { + item.hidden = false + } else if (!includes && item.hidden !== true) { + item.hidden = true + } + }) + } + }) + // set all fields that refer to changed one to blank + if (changedField != null) { + fields.forEach(field => { + const search = this.getSearch(field) + if (search.options?.parentList == changedField) { + const fld = search._w2field + const items = fld.options.items.filter(it => !it.hidden).map(it => it.id) + if (fld.type == 'list' && !items.includes(fld.get()?.id)) { + fld.set(null) + } + if (fld.type == 'enum') { + const new_sel = fld.get()?.filter(it => items.includes(it.id)) + fld.set(new_sel || []) + } + } + }) + } + } + initSearches() { + const overlay = query(`#w2overlay-${this.name}-search-overlay`) + // init searches + for (let ind = 0; ind < this.searches.length; ind++) { + const search = this.searches[ind] + const sdata = this.getSearchData(search.field) + search.type = String(search.type).toLowerCase() + if (search.type == 'new-column') { + continue + } + if (typeof search.options !== 'object') search.options = {} + // operators + let operator = search.operator + let operators = [...this.operators[this.operatorsMap[search.type]]] || [] // need a copy + if (search.operators) operators = search.operators + // normalize + if (w2utils.isPlainObject(operator)) operator = operator.oper + operators.forEach((oper, ind) => { + if (w2utils.isPlainObject(oper)) operators[ind] = oper.oper + }) + if (sdata && sdata.operator) { + operator = sdata.operator + } + // default operator + const def = this.defaultOperator[this.operatorsMap[search.type]] + if (operators.indexOf(operator) == -1) { + operator = def + } + overlay.find(`#grid_${this.name}_operator_${ind}`).val(operator) + this.initOperator(ind) + // populate field value + const $fld1 = overlay.find(`#grid_${this.name}_field_${ind}`) + const $fld2 = overlay.find(`#grid_${this.name}_field2_${ind}`) + if (sdata != null) { + if (!Array.isArray(sdata.value)) { + if (sdata.value != null) $fld1.val(sdata.value).trigger('change') + } else { + if (['in', 'not in'].includes(sdata.operator)) { + $fld1[0]._w2field.set(sdata.value) + } else { + $fld1.val(sdata.value[0]).trigger('change') + $fld2.val(sdata.value[1]).trigger('change') + } + } + } + } + // add on change event + overlay.find('.w2ui-grid-search-advanced *[rel=search]') + .on('keypress', evnt => { + if (evnt.keyCode == 13) { + this.search() + w2tooltip.hide(this.name + '-search-overlay') + } + }) + } + getColumnsHTML() { + const self = this + let html1 = '' + let html2 = '' + if (this.show.columnHeaders) { + if (this.columnGroups.length > 0) { + const tmp1 = getColumns(true) + const tmp2 = getGroups() + const tmp3 = getColumns(false) + html1 = tmp1[0] + tmp2[0] + tmp3[0] + html2 = tmp1[1] + tmp2[1] + tmp3[1] + } else { + const tmp = getColumns(true) + html1 = tmp[0] + html2 = tmp[1] + } + } + return [html1, html2] + function getGroups() { + let html1 = '' + let html2 = '' + let tmpf = '' + // add empty group at the end + const tmp = self.columnGroups.length - 1 + if (self.columnGroups[tmp].text == null && self.columnGroups[tmp].caption != null) { + console.log('NOTICE: grid columnGroup.caption property is deprecated, please use columnGroup.text. Group -> ', self.columnGroups[tmp]) + self.columnGroups[tmp].text = self.columnGroups[tmp].caption + } + if (self.columnGroups[self.columnGroups.length - 1].text != '') self.columnGroups.push({ text: '' }) + if (self.show.lineNumbers) { + html1 += '' + + '
 
' + + '' + } + if (self.show.selectColumn) { + html1 += '' + + '
 
' + + '' + } + if (self.show.expandColumn) { + html1 += '' + + '
 
' + + '' + } + let ii = 0 + html2 += `` + if (self.reorderRows) { + html2 += '' + + '
 
' + + '' + } + for (let i = 0; i < self.columnGroups.length; i++) { + const colg = self.columnGroups[i] + const col = self.columns[ii] || {} + if (colg.colspan != null) colg.span = colg.colspan + if (colg.span == null || colg.span != parseInt(colg.span)) colg.span = 1 + if (col.text == null && col.caption != null) { + console.log('NOTICE: grid column.caption property is deprecated, please use column.text. Column ->', col) + col.text = col.caption + } + let colspan = 0 + for (let jj = ii; jj < ii + colg.span; jj++) { + if (self.columns[jj] && !self.columns[jj].hidden) { + colspan++ + } + } + if (i == self.columnGroups.length - 1) { + colspan = 100 // last column + } + if (colspan <= 0) { + // do nothing here, all columns in the group are hidden. + } else if (colg.main === true) { + let sortStyle = '' + for (let si = 0; si < self.sortData.length; si++) { + if (self.sortData[si].field == col.field) { + if ((self.sortData[si].direction || '').toLowerCase() === 'asc') sortStyle = 'w2ui-sort-up' + if ((self.sortData[si].direction || '').toLowerCase() === 'desc') sortStyle = 'w2ui-sort-down' + } + } + let resizer = '' + if (col.resizable !== false) { + resizer = `
` + } + const text = w2utils.lang(typeof col.text === 'function' ? col.text(col) : col.text) + tmpf = `` + resizer + + `
` + + `
` + (!text ? ' ' : text) + + '
' + + '' + if (col && col.frozen) html1 += tmpf; else html2 += tmpf + } else { + const gText = w2utils.lang(typeof colg.text === 'function' ? colg.text(colg) : colg.text) + tmpf = `` + + `
${!gText ? ' ' : gText}
` + + '' + if (col && col.frozen) html1 += tmpf; else html2 += tmpf + } + ii += colg.span + } + html1 += '' // need empty column for border-right + html2 += `` + return [html1, html2] + } + function getColumns(main) { + let html1 = '' + let html2 = '' + if (self.show.lineNumbers) { + html1 += '' + + '
#
' + + '' + } + if (self.show.selectColumn) { + html1 += '' + + '
' + + ` ' + + '
' + + '' + } + if (self.show.expandColumn) { + html1 += '' + + '
 
' + + '' + } + let ii = 0 + let id = 0 + let colg + html2 += `` + if (self.reorderRows) { + html2 += '' + + '
 
' + + '' + } + for (let i = 0; i < self.columns.length; i++) { + const col = self.columns[i] + if (col.text == null && col.caption != null) { + console.log('NOTICE: grid column.caption property is deprecated, please use column.text. Column -> ', col) + col.text = col.caption + } + if (col.size == null) col.size = '100%' + if (i == id) { // always true on first iteration + colg = self.columnGroups[ii++] || {} + id = id + colg.span + } + if ((i < self.last.vscroll.colIndStart || i > self.last.vscroll.colIndEnd) && !col.frozen) { continue } + if (col.hidden) { continue } + if (colg.main !== true || main) { // grouping of columns + const colCellHTML = self.getColumnCellHTML(i) + if (col && col.frozen) html1 += colCellHTML; else html2 += colCellHTML + } + } + html1 += '
 
' + html2 += '
 
' + html1 += '' + html2 += '' + return [html1, html2] + } + } + getColumnCellHTML(i) { + const col = this.columns[i] + if (col == null) return '' + // reorder style + const reorderCols = (this.reorderColumns && (!this.columnGroups || !this.columnGroups.length)) ? ' w2ui-col-reorderable ' : '' + // sort style + let sortStyle = '' + for (let si = 0; si < this.sortData.length; si++) { + if (this.sortData[si].field == col.field) { + if ((this.sortData[si].direction || '').toLowerCase() === 'asc') sortStyle = 'w2ui-sort-up' + if ((this.sortData[si].direction || '').toLowerCase() === 'desc') sortStyle = 'w2ui-sort-down' + } + } + // col selected + const tmp = this.last.selection.columns + let selected = false + for (const t in tmp) { + for (let si = 0; si < tmp[t].length; si++) { + if (tmp[t][si] == i) selected = true + } + } + const text = w2utils.lang(typeof col.text === 'function' ? col.text(col) : col.text) + const html = '' + + (col.resizable !== false ? '
' : '') + + '
' + + '
' + + (!text ? ' ' : text) + + '
' + + '' + return html + } + columnTooltipShow(ind, event) { + const $el = query(this.box).find('#grid_' + this.name + '_column_' + ind) + const item = this.columns[ind] + const pos = this.columnTooltip + w2tooltip.show({ + name: this.name + '-column-tooltip', + anchor: $el.get(0), + html: item?.tooltip, + position: pos + }) + } + columnTooltipHide(ind, event) { + w2tooltip.hide(this.name + '-column-tooltip') + } + getRecordsHTML() { + let buffered = this.records.length + const url = this.url?.get ?? this.url + if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length + // larger number works better with chrome, smaller with FF. + if (buffered > this.vs_start) this.last.vscroll.show_extra = this.vs_extra; else this.last.vscroll.show_extra = this.vs_start + const records = query(this.box).find(`#grid_${this.name}_records`) + let limit = Math.floor((records.get(0)?.clientHeight || 0) / this.recordHeight) + this.last.vscroll.show_extra + 1 + if (limit < this.vs_start) { + limit = this.vs_start + } + if (!this.fixedBody || limit > buffered) limit = buffered + // always need first record for resizing purposes + let rec_html = this.getRecordHTML(-1, 0) + let html1 = '' + rec_html[0] + let html2 = '
' + rec_html[1] + // first empty row with height + html1 += '' + + ' ' + + '' + html2 += '' + + ' ' + + '' + for (let i = 0; i < limit; i++) { + rec_html = this.getRecordHTML(i, i + 1) + html1 += rec_html[0] + html2 += rec_html[1] + } + const h2 = (buffered - limit) * this.recordHeight + html1 += '' + + ' ' + + '' + + '' + + ' ' + + '' + + '
' + html2 += '' + + ' ' + + '' + + '' + + ' ' + + '' + + '' + this.last.vscroll.recIndStart = 0 + this.last.vscroll.recIndEnd = limit + return [html1, html2] + } + getSummaryHTML() { + if (this.summary.length === 0) return + let rec_html = this.getRecordHTML(-1, 0) // need this in summary too for colspan to work properly + let html1 = '' + rec_html[0] + let html2 = '
' + rec_html[1] + for (let i = 0; i < this.summary.length; i++) { + rec_html = this.getRecordHTML(i, i + 1, true) + html1 += rec_html[0] + html2 += rec_html[1] + } + html1 += '
' + html2 += '' + return [html1, html2] + } + scroll(event) { + const obj = this + const url = this.url?.get ?? this.url + const records = query(this.box).find(`#grid_${this.name}_records`) + const frecords = query(this.box).find(`#grid_${this.name}_frecords`) + // sync scroll positions + if (event) { + const sTop = event.target.scrollTop + const sLeft = event.target.scrollLeft + this.last.vscroll.scrollTop = sTop + this.last.vscroll.scrollLeft = sLeft + const cols = query(this.box).find(`#grid_${this.name}_columns`)[0] + const summary = query(this.box).find(`#grid_${this.name}_summary`)[0] + if (cols) cols.scrollLeft = sLeft + if (summary) summary.scrollLeft = sLeft + if (frecords[0]) frecords[0].scrollTop = sTop + } + // hide bubble + if (this.last.bubbleEl) { + w2tooltip.hide(this.name + '-bubble') + this.last.bubbleEl = null + } + // column virtual scroll + let colStart = null + let colEnd = null + if (this.disableCVS || this.columnGroups.length > 0) { + // disable virtual scroll + colStart = 0 + colEnd = this.columns.length - 1 + } else { + const sWidth = records.prop('clientWidth') + let cLeft = 0 + for (let i = 0; i < this.columns.length; i++) { + if (this.columns[i].frozen || this.columns[i].hidden) continue + const cSize = parseInt(this.columns[i].sizeCalculated ? this.columns[i].sizeCalculated : this.columns[i].size) + if (cLeft + cSize + 30 > this.last.vscroll.scrollLeft && colStart == null) colStart = i + if (cLeft + cSize - 30 > this.last.vscroll.scrollLeft + sWidth && colEnd == null) colEnd = i + cLeft += cSize + } + if (colEnd == null) colEnd = this.columns.length - 1 + } + if (colStart != null) { + if (colStart < 0) colStart = 0 + if (colEnd < 0) colEnd = 0 + if (colStart == colEnd) { + if (colStart > 0) colStart--; else colEnd++ // show at least one column + } + // --------- + if (colStart != this.last.vscroll.colIndStart || colEnd != this.last.vscroll.colIndEnd) { + const $box = query(this.box) + const deltaStart = Math.abs(colStart - this.last.vscroll.colIndStart) + const deltaEnd = Math.abs(colEnd - this.last.vscroll.colIndEnd) + // add/remove columns for small jumps + if (deltaStart < 5 && deltaEnd < 5) { + const $cfirst = $box.find(`.w2ui-grid-columns #grid_${this.name}_column_start`) + const $clast = $box.find('.w2ui-grid-columns .w2ui-head-last') + const $rfirst = $box.find(`#grid_${this.name}_records .w2ui-grid-data-spacer`) + const $rlast = $box.find(`#grid_${this.name}_records .w2ui-grid-data-last`) + const $sfirst = $box.find(`#grid_${this.name}_summary .w2ui-grid-data-spacer`) + const $slast = $box.find(`#grid_${this.name}_summary .w2ui-grid-data-last`) + // remove on left + if (colStart > this.last.vscroll.colIndStart) { + for (let i = this.last.vscroll.colIndStart; i < colStart; i++) { + $box.find('#grid_' + this.name + '_columns #grid_' + this.name + '_column_' + i).remove() // column + $box.find('#grid_' + this.name + '_records td[col="' + i + '"]').remove() // record + $box.find('#grid_' + this.name + '_summary td[col="' + i + '"]').remove() // summary + } + } + // remove on right + if (colEnd < this.last.vscroll.colIndEnd) { + for (let i = this.last.vscroll.colIndEnd; i > colEnd; i--) { + $box.find('#grid_' + this.name + '_columns #grid_' + this.name + '_column_' + i).remove() // column + $box.find('#grid_' + this.name + '_records td[col="' + i + '"]').remove() // record + $box.find('#grid_' + this.name + '_summary td[col="' + i + '"]').remove() // summary + } + } + // add on left + if (colStart < this.last.vscroll.colIndStart) { + for (let i = this.last.vscroll.colIndStart - 1; i >= colStart; i--) { + if (this.columns[i] && (this.columns[i].frozen || this.columns[i].hidden)) continue + $cfirst.after(this.getColumnCellHTML(i)) // column + // record + $rfirst.each(el => { + const index = query(el).parent().attr('index') + let td = '' // width column + if (index != null) td = this.getCellHTML(parseInt(index), i, false) + query(el).after(td) + }) + // summary + $sfirst.each(el => { + const index = query(el).parent().attr('index') + let td = '' // width column + if (index != null) td = this.getCellHTML(parseInt(index), i, true) + query(el).after(td) + }) + } + } + // add on right + if (colEnd > this.last.vscroll.colIndEnd) { + for (let i = this.last.vscroll.colIndEnd + 1; i <= colEnd; i++) { + if (this.columns[i] && (this.columns[i].frozen || this.columns[i].hidden)) continue + $clast.before(this.getColumnCellHTML(i)) // column + // record + $rlast.each(el => { + const index = query(el).parent().attr('index') + let td = '' // width column + if (index != null) td = this.getCellHTML(parseInt(index), i, false) + query(el).before(td) + }) + // summary + $slast.each(el => { + const index = query(el).parent().attr('index') || -1 + const td = this.getCellHTML(parseInt(index), i, true) + query(el).before(td) + }) + } + } + this.last.vscroll.colIndStart = colStart + this.last.vscroll.colIndEnd = colEnd + this.resizeRecords() + } else { + this.last.vscroll.colIndStart = colStart + this.last.vscroll.colIndEnd = colEnd + // dot not just call this.refresh(); + const colHTML = this.getColumnsHTML() + const recHTML = this.getRecordsHTML() + const sumHTML = this.getSummaryHTML() + const $columns = $box.find(`#grid_${this.name}_columns`) + const $records = $box.find(`#grid_${this.name}_records`) + const $frecords = $box.find(`#grid_${this.name}_frecords`) + const $summary = $box.find(`#grid_${this.name}_summary`) + $columns.find('tbody').html(colHTML[1]) + $frecords.html(recHTML[0]) + $records.prepend(recHTML[1]) + if (sumHTML != null) $summary.html(sumHTML[1]) + // need timeout to clean up (otherwise scroll problem) + setTimeout(() => { + $records.find(':scope > table').filter(':not(table:first-child)').remove() + if ($summary[0]) $summary[0].scrollLeft = this.last.vscroll.scrollLeft + }, 1) + this.resizeRecords() + } + } + } + // perform virtual scroll + let buffered = this.records.length + if (buffered > this.total && this.total !== -1) buffered = this.total + if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length + if (buffered === 0 || records.length === 0 || records.prop('clientHeight') === 0) return + if (buffered > this.vs_start) this.last.vscroll.show_extra = this.vs_extra; else this.last.vscroll.show_extra = this.vs_start + // update footer + let t1 = Math.round(records.prop('scrollTop') / this.recordHeight + 1) + let t2 = t1 + (Math.round(records.prop('clientHeight') / this.recordHeight) - 1) + if (t1 > buffered) t1 = buffered + if (t2 >= buffered - 1) t2 = buffered + query(this.box).find('#grid_' + this.name + '_footer .w2ui-footer-right').html( + (this.show.statusRange + ? w2utils.formatNumber(this.offset + t1) + '-' + w2utils.formatNumber(this.offset + t2) + + (this.total != -1 ? ' ' + w2utils.lang('of') + ' ' + w2utils.formatNumber(this.total) + '' : '') + : '') + + (url && this.show.statusBuffered ? ' (' + w2utils.lang('buffered') + ' ' + w2utils.formatNumber(buffered) + '' + + (this.offset > 0 ? ', skip ' + w2utils.formatNumber(this.offset) : '') + ')' : '') + ) + // only for local data source, else no extra records loaded + if (!url && (!this.fixedBody || (this.total != -1 && this.total <= this.vs_start))) return + // regular processing + let start = Math.floor(records.prop('scrollTop') / this.recordHeight) - this.last.vscroll.show_extra + let end = start + Math.floor(records.prop('clientHeight') / this.recordHeight) + this.last.vscroll.show_extra * 2 + 1 + // let div = start - this.last.vscroll.recIndStart; + if (start < 1) start = 1 + if (end > this.total && this.total != -1) end = this.total + const tr1 = records.find('#grid_' + this.name + '_rec_top') + const tr2 = records.find('#grid_' + this.name + '_rec_bottom') + const tr1f = frecords.find('#grid_' + this.name + '_frec_top') + const tr2f = frecords.find('#grid_' + this.name + '_frec_bottom') + // if row is expanded + if (String(tr1.next().prop('id')).indexOf('_expanded_row') != -1) { + tr1.next().remove() + tr1f.next().remove() + } + if (this.total > end && String(tr2.prev().prop('id')).indexOf('_expanded_row') != -1) { + tr2.prev().remove() + tr2f.prev().remove() + } + const first = parseInt(tr1.next().attr('line')) + const last = parseInt(tr2.prev().attr('line')) + let tmp, tmp1, tmp2, rec_start, rec_html + if (first <= start || first == 1 || this.last.vscroll.pull_refresh) { // scroll down + if (end <= last + this.last.vscroll.show_extra - 2 && end != this.total) return + this.last.vscroll.pull_refresh = false + // remove from top + while (true) { + tmp1 = frecords.find('#grid_' + this.name + '_frec_top').next() + tmp2 = records.find('#grid_' + this.name + '_rec_top').next() + if (tmp2.attr('line') == 'bottom') break + if (parseInt(tmp2.attr('line')) < start) { + tmp1.remove() + tmp2.remove() + } else { + break + } + } + // add at bottom + tmp = records.find('#grid_' + this.name + '_rec_bottom').prev() + rec_start = tmp.attr('line') + if (rec_start == 'top') rec_start = start + for (let i = parseInt(rec_start) + 1; i <= end; i++) { + if (!this.records[i - 1]) continue + tmp2 = this.records[i - 1].w2ui + if (tmp2 && !Array.isArray(tmp2.children)) { + tmp2.expanded = false + } + rec_html = this.getRecordHTML(i - 1, i) + tr2.before(rec_html[1]) + tr2f.before(rec_html[0]) + } + markSearch() + setTimeout(() => { this.refreshRanges() }, 0) + } else { // scroll up + if (start >= first - this.last.vscroll.show_extra + 2 && start > 1) return + // remove from bottom + while (true) { + tmp1 = frecords.find('#grid_' + this.name + '_frec_bottom').prev() + tmp2 = records.find('#grid_' + this.name + '_rec_bottom').prev() + if (tmp2.attr('line') == 'top') break + if (parseInt(tmp2.attr('line')) > end) { + tmp1.remove() + tmp2.remove() + } else { + break + } + } + // add at top + tmp = records.find('#grid_' + this.name + '_rec_top').next() + rec_start = tmp.attr('line') + if (rec_start == 'bottom') rec_start = end + for (let i = parseInt(rec_start) - 1; i >= start; i--) { + if (!this.records[i - 1]) continue + tmp2 = this.records[i - 1].w2ui + if (tmp2 && !Array.isArray(tmp2.children)) { + tmp2.expanded = false + } + rec_html = this.getRecordHTML(i - 1, i) + tr1.after(rec_html[1]) + tr1f.after(rec_html[0]) + } + markSearch() + setTimeout(() => { this.refreshRanges() }, 0) + } + // first/last row size + const h1 = (start - 1) * this.recordHeight + let h2 = (buffered - end) * this.recordHeight + if (h2 < 0) h2 = 0 + tr1.css('height', h1 + 'px') + tr1f.css('height', h1 + 'px') + tr2.css('height', h2 + 'px') + tr2f.css('height', h2 + 'px') + this.last.vscroll.recIndStart = start + this.last.vscroll.recIndEnd = end + // load more if needed + const s = Math.floor(records.prop('scrollTop') / this.recordHeight) + const e = s + Math.floor(records.prop('clientHeight') / this.recordHeight) + if (e + 10 > buffered && this.last.vscroll.pull_more !== true && (buffered < this.total - this.offset || (this.total == -1 && this.last.fetch.hasMore))) { + if (this.autoLoad === true) { + this.last.vscroll.pull_more = true + this.last.fetch.offset += this.limit + this.request('load') + } + // scroll function + const more = query(this.box).find('#grid_' + this.name + '_rec_more, #grid_' + this.name + '_frec_more') + more.show() + .eq(1) // only main table + .off('.load-more') + .on('click.load-more', function() { + // show spinner + query(this).find('td').html('
') + // load more + obj.last.vscroll.pull_more = true + obj.last.fetch.offset += obj.limit + obj.request('load') + }) + .find('td') + .html(obj.autoLoad + ? '
' + : '
' + w2utils.lang('Load ${count} more...', { count: obj.limit }) + '
' + ) + } + function markSearch() { + // mark search + if (!obj.markSearch) return + clearTimeout(obj.last.marker_timer) + obj.last.marker_timer = setTimeout(() => { + // mark all search strings + const search = [] + for (let s = 0; s < obj.searchData.length; s++) { + const sdata = obj.searchData[s] + const fld = obj.getSearch(sdata.field) + if (!fld || fld.hidden) continue + const ind = obj.getColumn(sdata.field, true) + search.push({ field: sdata.field, search: sdata.value, col: ind }) + } + if (search.length > 0) { + search.forEach((item) => { + const el = query(obj.box).find('td[col="' + item.col + '"]:not(.w2ui-head)') + w2utils.marker(el, item.search) + }) + } + }, 50) + } + } + getRecordHTML(ind, lineNum, summary) { + let tmph = '' + let rec_html1 = '' + let rec_html2 = '' + const sel = this.last.selection + let record + // first record needs for resize purposes + if (ind == -1) { + rec_html1 += '' + rec_html2 += '' + if (this.show.lineNumbers) rec_html1 += '' + if (this.show.selectColumn) rec_html1 += '' + if (this.show.expandColumn) rec_html1 += '' + rec_html2 += '' + if (this.reorderRows) rec_html2 += '' + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + tmph = '' + if (col.frozen && !col.hidden) { + rec_html1 += tmph + } else { + if (col.hidden || i < this.last.vscroll.colIndStart || i > this.last.vscroll.colIndEnd) continue + rec_html2 += tmph + } + } + rec_html1 += '' + rec_html2 += '' + rec_html1 += '' + rec_html2 += '' + return [rec_html1, rec_html2] + } + // regular record + const url = this.url?.get ?? this.url + if (summary !== true) { + if (this.searchData.length > 0 && !url) { + if (ind >= this.last.searchIds.length) return '' + ind = this.last.searchIds[ind] + record = this.records[ind] + } else { + if (ind >= this.records.length) return '' + record = this.records[ind] + } + } else { + if (ind >= this.summary.length) return '' + record = this.summary[ind] + } + if (!record) return '' + if (record.recid == null && this.recid != null) { + const rid = this.parseField(record, this.recid) + if (rid != null) record.recid = rid + } + let isRowSelected = false + if (sel.indexes.indexOf(ind) !== -1) isRowSelected = true + let rec_style = (record.w2ui ? record.w2ui.style : '') + if (rec_style == null || typeof rec_style !== 'string') rec_style = '' + let rec_class = (record.w2ui ? record.w2ui.class : '') + if (rec_class == null || typeof rec_class !== 'string') rec_class = '' + // render TR + rec_html1 += '' + rec_html2 += '' + if (this.show.lineNumbers) { + rec_html1 += '' + + (summary !== true ? this.getLineHTML(lineNum, record) : '') + + '' + } + if (this.show.selectColumn) { + rec_html1 += + '' + + (summary !== true && !(record.w2ui && record.w2ui.hideCheckBox === true) + ? '
' + + ' ' + + '
' + : '') + + '' + } + if (this.show.expandColumn) { + let tmp_img = '' + if (record.w2ui?.expanded === true) tmp_img = '-'; else tmp_img = '+' + if ((record.w2ui?.expanded == 'none' || !Array.isArray(record.w2ui?.children) || !record.w2ui?.children.length)) tmp_img = '+' + if (record.w2ui?.expanded == 'spinner') tmp_img = '
' + rec_html1 += + '' + + (summary !== true ? `
${tmp_img}
` : '') + + '' + } + // insert empty first column + rec_html2 += '' + if (this.reorderRows) { + rec_html2 += + '' + + (summary !== true ? '
 
' : '') + + '' + } + let col_ind = 0 + let col_skip = 0 + while (true) { + let col_span = 1 + const col = this.columns[col_ind] + if (col == null) break + if (col.hidden) { + col_ind++ + if (col_skip > 0) col_skip-- + continue + } + if (col_skip > 0) { + col_ind++ + if (this.columns[col_ind] == null) break + record.w2ui.colspan[this.columns[col_ind - 1].field] = 0 // need it for other methods + col_skip-- + continue + } else if (record.w2ui) { + const tmp1 = record.w2ui.colspan + const tmp2 = this.columns[col_ind].field + if (tmp1 && tmp1[tmp2] === 0) { + delete tmp1[tmp2] // if no longer colspan then remove 0 + } + } + // column virtual scroll + if ((col_ind < this.last.vscroll.colIndStart || col_ind > this.last.vscroll.colIndEnd) && !col.frozen) { + col_ind++ + continue + } + if (record.w2ui) { + if (typeof record.w2ui.colspan === 'object') { + const span = parseInt(record.w2ui.colspan[col.field]) || null + if (span > 1) { + // if there are hidden columns, then no colspan on them + let hcnt = 0 + for (let i = col_ind; i < col_ind + span; i++) { + if (i >= this.columns.length) break + if (this.columns[i].hidden) hcnt++ + } + col_span = span - hcnt + col_skip = span - 1 + } + } + } + const rec_cell = this.getCellHTML(ind, col_ind, summary, col_span) + if (col.frozen) rec_html1 += rec_cell; else rec_html2 += rec_cell + col_ind++ + } + rec_html1 += '' + rec_html2 += '' + rec_html1 += '' + rec_html2 += '' + return [rec_html1, rec_html2] + } + getLineHTML(lineNum) { + return '
' + lineNum + '
' + } + getCellHTML(ind, col_ind, summary, col_span) { + const obj = this + const col = this.columns[col_ind] + if (col == null) return '' + const record = (summary !== true ? this.records[ind] : this.summary[ind]) + // value, attr, style, className, divAttr + let { value, style, className, attr, divAttr } = this.getCellValue(ind, col_ind, summary, true) + const edit = (ind !== -1 ? this.getCellEditable(ind, col_ind) : '') + let divStyle = 'max-height: ' + parseInt(this.recordHeight) + 'px;' + (col.clipboardCopy ? 'margin-right: 20px' : '') + const isChanged = !summary && record?.w2ui?.changes && record.w2ui.changes[col.field] != null + const sel = this.last.selection + let isRowSelected = false + let infoBubble = '' + if (sel.indexes.indexOf(ind) != -1) isRowSelected = true + if (col_span == null) { + if (record?.w2ui?.colspan && record.w2ui.colspan[col.field]) { + col_span = record.w2ui.colspan[col.field] + } else { + col_span = 1 + } + } + // expand icon + if (col_ind === 0 && Array.isArray(record?.w2ui?.children)) { + let level = 0 + let subrec = this.get(record.w2ui.parent_recid, true) + while (true) { + if (subrec != null) { + level++ + const tmp = this.records[subrec].w2ui + if (tmp != null && tmp.parent_recid != null) { + subrec = this.get(tmp.parent_recid, true) + } else { + break + } + } else { + break + } + } + if (record.w2ui.parent_recid) { + for (let i = 0; i < level; i++) { + infoBubble += '' + } + } + const className = record.w2ui.children.length > 0 + ? (record.w2ui.expanded ? 'w2ui-icon-collapse' : 'w2ui-icon-expand') + : 'w2ui-icon-empty' + infoBubble += `` + } + // info bubble + if (col.info === true) col.info = {} + if (col.info != null) { + let infoIcon = 'w2ui-icon-info' + if (typeof col.info.icon === 'function') { + infoIcon = col.info.icon(record, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) + } else if (typeof col.info.icon === 'object') { + infoIcon = col.info.icon[this.parseField(record, col.field)] || '' + } else if (typeof col.info.icon === 'string') { + infoIcon = col.info.icon + } + let infoStyle = col.info.style || '' + if (typeof col.info.style === 'function') { + infoStyle = col.info.style(record, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) + } else if (typeof col.info.style === 'object') { + infoStyle = col.info.style[this.parseField(record, col.field)] || '' + } else if (typeof col.info.style === 'string') { + infoStyle = col.info.style + } + infoBubble += `` + } + let data = value + // if editable checkbox + if (edit && ['checkbox', 'check'].indexOf(edit.type) != -1) { + const changeInd = summary ? -(ind + 1) : ind + divStyle += 'text-align: center;' + data = `` + infoBubble = '' + } + data = `
${infoBubble}${String(data)}
` + if (data == null) data = '' + // --> cell TD + if (typeof col.render === 'string') { + const tmp = col.render.replace('|', ':').split(':') + if (['number', 'int', 'float', 'money', 'currency', 'percent', 'size'].includes(tmp[0])) { + style += 'text-align: right;' + } + } + if (record?.w2ui) { + if (typeof record.w2ui.style === 'object') { + if (typeof record.w2ui.style[col_ind] === 'string') style += record.w2ui.style[col_ind] + ';' + if (typeof record.w2ui.style[col.field] === 'string') style += record.w2ui.style[col.field] + ';' + } + if (typeof record.w2ui.class === 'object') { + if (typeof record.w2ui.class[col_ind] === 'string') className += record.w2ui.class[col_ind] + ' ' + if (typeof record.w2ui.class[col.field] === 'string') className += record.w2ui.class[col.field] + ' ' + } + } + let isCellSelected = false + if (isRowSelected && sel.columns[ind]?.includes(col_ind)) isCellSelected = true + // clipboardCopy + let clipboardIcon + if (this.keyboard === true && col.clipboardCopy) { + clipboardIcon = '' + } + // data + data = ' 1 ? 'colspan="' + col_span + '"' : '') + + '>' + data + (clipboardIcon && w2utils.stripTags(data) ? clipboardIcon : '') + '' + // summary top row + if (ind === -1 && summary === true) { + data = ' 1 ? 'colspan="' + col_span + '"' : '') + + '>' + } + return data + function getTitle(cellData) { + let title + if (obj.show.recordTitles) { + if (col.title != null) { + if (typeof col.title === 'function') { + title = col.title.call(obj, record, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) + } + if (typeof col.title === 'string') title = col.title + } else { + title = w2utils.stripTags(String(cellData).replace(/"/g, '\'\'')) + } + } + return (title != null) ? 'title="' + String(title) + '"' : '' + } + } + clipboardCopy(ind, col_ind, summary) { + if (this.keyboard !== true) return + const rec = summary ? this.summary[ind] : this.records[ind] + const col = this.columns[col_ind] + let txt = (col ? this.parseField(rec, col.field) : '') + if (typeof col.clipboardCopy === 'function') { + txt = col.clipboardCopy(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) + } + query(this.box).find('#grid_' + this.name + '_focus').text(txt).get(0).select() + document.execCommand('copy') + } + showBubble(ind, col_ind, summary) { + const info = this.columns[col_ind].info + if (!info) return + let html = '' + const rec = this.records[ind] + const el = query(this.box).find(`${summary ? '.w2ui-grid-summary' : ''} #grid_${this.name}_data_${ind}_${col_ind} .w2ui-info`) + if (this.last.bubbleEl) { + w2tooltip.hide(this.name + '-bubble') + } + this.last.bubbleEl = el + // if no fields defined - show all + if (info.fields == null) { + info.fields = [] + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + info.fields.push(col.field + (typeof col.render === 'string' ? ':' + col.render : '')) + } + } + let fields = info.fields + if (typeof fields === 'function') { + fields = fields(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) // custom renderer + } + // generate html + if (typeof info.render === 'function') { + html = info.render(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) + } else if (Array.isArray(fields)) { + // display mentioned fields + html = '' + for (let i = 0; i < fields.length; i++) { + const tmp = String(fields[i]).split(':') + if (tmp[0] == '' || tmp[0] == '-' || tmp[0] == '--' || tmp[0] == '---') { + html += '' + continue + } + let col = this.getColumn(tmp[0]) + if (col == null) col = { field: tmp[0], caption: tmp[0] } // if not found in columns + let val = (col ? this.parseField(rec, col.field) : '') + // if change by inline editing + if (rec?.w2ui?.changes?.[col.field] != null) { + val = rec.w2ui.changes[col.field] + } + if (tmp.length > 1) { + if (w2utils.formatters[tmp[1]]) { + const extra = { + self: this, + value: val, + params: tmp[2] || null, + field: this.columns[col_ind].field, + index: ind, + colIndex: col_ind + } + val = w2utils.formatters[tmp[1]].call(this, rec, extra) + } else { + console.log('ERROR: w2utils.formatters["' + tmp[1] + '"] does not exists.') + } + } + if (typeof val === 'object' && val.text != null) val = val.text + if (info.showEmpty !== true && (val == null || val == '')) continue + if (info.maxLength != null && typeof val === 'string' && val.length > info.maxLength) val = val.substr(0, info.maxLength) + '...' + html += '' + } + html += '
' + col.text + '' + ((val === 0 ? '0' : val) || '') + '
' + } else if (w2utils.isPlainObject(fields)) { + // display some fields + html = '' + for (const caption in fields) { + const fld = fields[caption] + if (fld == '' || fld == '-' || fld == '--' || fld == '---') { + html += '' + continue + } + const tmp = String(fld).split(':') + let col = this.getColumn(tmp[0]) + if (col == null) col = { field: tmp[0], caption: tmp[0] } // if not found in columns + let val = (col ? this.parseField(rec, col.field) : '') + // if change by inline editing + if (rec?.w2ui?.changes?.[col.field] != null) { + val = rec.w2ui.changes[col.field] + } + if (tmp.length > 1) { + if (w2utils.formatters[tmp[1]]) { + const extra = { + self: this, + value: val, + params: tmp[2] || null, + field: this.columns[col_ind].field, + index: ind, + colIndex: col_ind + } + val = w2utils.formatters[tmp[1]].call(this, rec, extra) + } else { + console.log('ERROR: w2utils.formatters["' + tmp[1] + '"] does not exists.') + } + } + if (typeof fld === 'function') { + val = fld(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) + } + if (val?.text != null) val = val.text + if (info.showEmpty !== true && (val == null || val == '')) continue + if (info.maxLength != null && typeof val === 'string' && val.length > info.maxLength) val = val.substr(0, info.maxLength) + '...' + html += '' + } + html += '
' + caption + '' + ((val === 0 ? '0' : val) || '') + '
' + } + return w2tooltip.show(w2utils.extend({ + name: this.name + '-bubble', + html, + anchor: el.get(0), + position: 'top|bottom', + class: 'w2ui-info-bubble', + style: '', + hideOn: ['doc-click'] + }, info.options ?? {})) + .hide(() => [ + this.last.bubbleEl = null + ]) + } + // return null or the editable object if the given cell is editable + getCellEditable(ind, col_ind) { + const col = this.columns[col_ind] + const rec = this.records[ind] + if (!rec || !col) return null + let edit = (rec.w2ui ? rec.w2ui.editable : null) + if (edit === false) return null + if (edit == null || edit === true) { + edit = ((typeof col.editable === 'function' || (col.editable && Object.keys(col.editable).length > 0)) ? col.editable : null) + if (typeof edit === 'function') { + const value = this.getCellValue(ind, col_ind, false) + // same arguments as col.render() + edit = edit.call(this, rec, { self: this, value, index: ind, colIndex: col_ind }) + } + } + return edit + } + getCellValue(ind, col_ind, summary, extra) { + const col = this.columns[col_ind] + const record = (summary !== true ? this.records[ind] : this.summary[ind]) + let value = this.parseField(record, col.field) + let className = ''; let style = ''; let attr = ''; let divAttr = '' + // if change by inline editing + if (record?.w2ui?.changes?.[col.field] != null) { + value = record.w2ui.changes[col.field] + } + // if there is a cell renderer + if (col.render != null && ind !== -1) { + let render = col.render + let params + // predefined formatters + if (typeof render === 'string') { + const tmp = col.render.toLowerCase().replace('|', ':').split(':') + // formatters + let func = w2utils.formatters[tmp[0]] + if (col.options && col.options.autoFormat === false) { + func = null + } + render = func + params = tmp[1] + } + if (typeof render === 'function' && record != null) { + let html + try { + html = render.call(this, record, { + self: this, + value, params, + field: this.columns[col_ind].field, + index: ind, + colIndex: col_ind, + summary: !!summary + }) + } catch (e) { + throw new Error(`Render function for column "${col.field}" in grid "${this.name}": -- ` + e.message) + } + if (html != null && typeof html === 'object' && typeof html !== 'function') { + if (html.id != null && html.text != null) { + // normalized menu kind of return + value = html.text + } else if (typeof html.html === 'string' || typeof html.html === 'number') { + value = String(html.html ?? '').trim() + } else { + value = '' + console.log('ERROR: render function should return a primitive or an object of the following structure.', + { html: '', attr: '', style: '', class: '', divAttr: '' }, '... but it returned:', html) + } + attr = html.attr ?? '' + style = html.style ?? '' + className = html.class ?? '' + divAttr = html.divAttr ?? '' + } else { + value = String(html || '').trim() + } + } + // if it is an object + if (typeof render === 'object') { + const tmp = render[value] + if (tmp != null && tmp !== '') { + value = tmp + } + } + } + if (value == null) value = '' + return !extra ? value : { value, attr, style, className, divAttr } + } + getFooterHTML() { + return '
' + + ' ' + + ' ' + + ' ' + + '
' + } + status(msg) { + if (msg != null) { + query(this.box).find(`#grid_${this.name}_footer`).find('.w2ui-footer-left').html(msg) + } else { + // show number of selected + let msgLeft = '' + const sel = this.getSelection() + if (sel.length > 0) { + if (this.show.statusSelection && sel.length > 1) { + msgLeft = String(sel.length).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + w2utils.settings.groupSymbol) + ' ' + w2utils.lang('selected') + } + if (this.show.statusRecordID && sel.length == 1) { + let tmp = sel[0] + if (typeof tmp === 'object') tmp = tmp.recid + ', ' + w2utils.lang('Column') + ': ' + tmp.column + msgLeft = w2utils.lang('Record ID') + ': ' + tmp + ' ' + } + } + query(this.box).find('#grid_' + this.name + '_footer .w2ui-footer-left').html(msgLeft) + } + } + lock(msg, showSpinner) { + const args = Array.from(arguments) + args.unshift(this.box) + setTimeout(() => { + // hide empty msg if any + query(this.box).find('#grid_' + this.name + '_empty_msg').remove() + w2utils.lock(...args) + }, 10) + } + unlock(speed) { + setTimeout(() => { + // do not unlock if there is a message + if (query(this.box).find('.w2ui-message').hasClass('w2ui-closing')) return + w2utils.unlock(this.box, speed) + }, 25) // needed timer so if server fast, it will not flash + } + stateSave(returnOnly) { + const state = { + columns: [], + show: w2utils.clone(this.show), + last: { + search: this.last.search, + multi: this.last.multi, + logic: this.last.logic, + label: this.last.label, + field: this.last.field, + scrollTop: this.last.vscroll.scrollTop, + scrollLeft: this.last.vscroll.scrollLeft + }, + sortData: [], + searchData: [] + } + let prop_val + for (let i = 0; i < this.columns.length; i++) { + const col = this.columns[i] + const col_save_obj = {} + // iterate properties to save + Object.keys(this.stateColProps).forEach((prop, idx) => { + if (this.stateColProps[prop]) { + // check if the property is defined on the column + if (col[prop] !== undefined) { + prop_val = col[prop] + } else { + // use fallback or null + prop_val = this.colTemplate[prop] || null + } + col_save_obj[prop] = prop_val + } + }) + state.columns.push(col_save_obj) + } + for (let i = 0; i < this.sortData.length; i++) state.sortData.push(w2utils.clone(this.sortData[i])) + for (let i = 0; i < this.searchData.length; i++) state.searchData.push(w2utils.clone(this.searchData[i])) + // event before + const edata = this.trigger('stateSave', { target: this.name, state: state }) + if (edata.isCancelled === true) { + return + } + // save into local storage + if (returnOnly !== true) { + this.cacheSave('state', state) + } + // event after + edata.finish() + return state + } + stateRestore(newState) { + const url = this.url?.get ?? this.url + if (!newState) { + newState = this.cache('state') + } + // event before + const edata = this.trigger('stateRestore', { target: this.name, state: newState }) + if (edata.isCancelled === true) { + return + } + // default behavior + if (w2utils.isPlainObject(newState)) { + w2utils.extend(this.show, newState.show ?? {}) + w2utils.extend(this.last, newState.last ?? {}) + const sTop = this.last.vscroll.scrollTop + const sLeft = this.last.vscroll.scrollLeft + for (let c = 0; c < newState.columns?.length; c++) { + const tmp = newState.columns[c] + const col_index = this.getColumn(tmp.field, true) + if (col_index !== null) { + w2utils.extend(this.columns[col_index], tmp) + // restore column order from saved state + if (c !== col_index) this.columns.splice(c, 0, this.columns.splice(col_index, 1)[0]) + } + } + this.sortData.splice(0, this.sortData.length) + for (let c = 0; c < newState.sortData?.length; c++) { + this.sortData.push(newState.sortData[c]) + } + this.searchData.splice(0, this.searchData.length) + for (let c = 0; c < newState.searchData?.length; c++) { + this.searchData.push(newState.searchData[c]) + } + // apply sort and search + setTimeout(() => { + // needs timeout as records need to be populated + // ez 10.09.2014 this --> + if (!url) { + if (this.sortData.length > 0) this.localSort() + if (this.searchData.length > 0) this.localSearch() + } + this.last.vscroll.scrollTop = sTop + this.last.vscroll.scrollLeft = sLeft + this.refresh() + }, 1) + console.log(`INFO (w2ui): state restored for "${this.name}"`) + } + // event after + edata.finish() + return true + } + stateReset() { + this.stateRestore(this.last.state) + this.cacheSave('state', null) + } + parseField(obj, field) { + let val + if (this.nestedFields) { + val = w2utils.getNested(obj, field) + } else { + val = obj?.[field] + } + return (val != null ? val : '') + } + prepareData() { + const obj = this + // loops thru records and prepares date and time objects + for (let r = 0; r < this.records.length; r++) { + const rec = this.records[r] + prepareRecord(rec) + } + // prepare date and time objects for the 'rec' record and its closed children + function prepareRecord(rec) { + for (let c = 0; c < obj.columns.length; c++) { + const column = obj.columns[c] + if (rec[column.field] == null || typeof column.render !== 'string') continue + // number + if (['number', 'int', 'float', 'money', 'currency', 'percent'].indexOf(column.render.split(':')[0]) != -1) { + if (typeof rec[column.field] !== 'number') rec[column.field] = parseFloat(rec[column.field]) + } + // date + if (['date', 'age'].indexOf(column.render.split(':')[0]) != -1) { + if (!rec[column.field + '_']) { + let dt = rec[column.field] + if (w2utils.isInt(dt)) dt = parseInt(dt) + rec[column.field + '_'] = new Date(dt) + } + } + // time + if (['time'].indexOf(column.render) != -1) { + if (w2utils.isTime(rec[column.field])) { // if string + const tmp = w2utils.isTime(rec[column.field], true) + const dt = new Date() + dt.setHours(tmp.hours, tmp.minutes, (tmp.seconds ? tmp.seconds : 0), 0) // sets hours, min, sec, mills + if (!rec[column.field + '_']) rec[column.field + '_'] = dt + } else { // if date object + let tmp = rec[column.field] + if (w2utils.isInt(tmp)) tmp = parseInt(tmp) + tmp = (tmp != null ? new Date(tmp) : new Date()) + const dt = new Date() + dt.setHours(tmp.getHours(), tmp.getMinutes(), tmp.getSeconds(), 0) // sets hours, min, sec, mills + if (!rec[column.field + '_']) rec[column.field + '_'] = dt + } + } + } + if (rec.w2ui?.children && rec.w2ui?.expanded !== true) { + // there are closed children, prepare them too. + for (let r = 0; r < rec.w2ui.children.length; r++) { + const subRec = rec.w2ui.children[r] + prepareRecord(subRec) + } + } + } + } + nextCell(index, col_ind, editable) { + const check = col_ind + 1 + if (check >= this.columns.length) { + index = this.nextRow(index) + return index == null ? index : this.nextCell(index, -1, editable) + } + const tmp = this.records[index].w2ui + const col = this.columns[check] + const span = (tmp && tmp.colspan && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1) + if (col == null) return null + if (col && col.hidden || span === 0) return this.nextCell(index, check, editable) + if (editable) { + const edit = this.getCellEditable(index, check) + if (edit == null || ['checkbox', 'check'].indexOf(edit.type) != -1) { + return this.nextCell(index, check, editable) + } + } + return { index, colIndex: check } + } + prevCell(index, col_ind, editable) { + const check = col_ind - 1 + if (check < 0) { + index = this.prevRow(index) + return index == null ? index : this.prevCell(index, this.columns.length, editable) + } + if (check < 0) return null + const tmp = this.records[index].w2ui + const col = this.columns[check] + const span = (tmp && tmp.colspan && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1) + if (col == null) return null + if (col && col.hidden || span === 0) return this.prevCell(index, check, editable) + if (editable) { + const edit = this.getCellEditable(index, check) + if (edit == null || ['checkbox', 'check'].indexOf(edit.type) != -1) { + return this.prevCell(index, check, editable) + } + } + return { index, colIndex: check } + } + nextRow(ind, col_ind, numRows) { + const sids = this.last.searchIds + let ret = null + if (numRows == null) numRows = 1 + if (numRows == -1) { + return this.records.length - 1 + } + if ((ind + numRows < this.records.length && sids.length === 0) || // if there are more records + (sids.length > 0 && ind < sids[sids.length - numRows])) { + ind += numRows + if (sids.length > 0) { + while (true) { + if (sids.includes(ind) || ind > this.records.length) break + ind += numRows + } + } + // colspan + const tmp = this.records[ind].w2ui + const col = this.columns[col_ind] + const span = (tmp && tmp.colspan && col != null && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1) + if (span === 0) { + ret = this.nextRow(ind, col_ind, numRows) + } else { + ret = ind + } + } + return ret + } + prevRow(ind, col_ind, numRows) { + const sids = this.last.searchIds + let ret = null + if (numRows == null) numRows = 1 + if (numRows === -1) { + return 0 + } + if ((ind - numRows >= 0 && sids.length === 0) || // if there are more records + (sids.length > 0 && ind > sids[0])) { + ind -= numRows + if (sids.length > 0) { + while (true) { + if (sids.includes(ind) || ind < 0) break + ind -= numRows + } + } + // colspan + const tmp = this.records[ind].w2ui + const col = this.columns[col_ind] + const span = (tmp && tmp.colspan && col != null && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1) + if (span === 0) { + ret = this.prevRow(ind, col_ind, numRows) + } else { + ret = ind + } + } + return ret + } + selectionSave() { + this.last.saved_sel = this.getSelection() + return this.last.saved_sel + } + selectionRestore(noRefresh) { + const time = Date.now() + this.last.selection = { indexes: [], columns: {}} + const sel = this.last.selection + const lst = this.last.saved_sel + if (lst) { + for (let i = 0; i < lst.length; i++) { + if (w2utils.isPlainObject(lst[i])) { + // selectType: cell + const tmp = this.get(lst[i].recid, true) + if (tmp != null) { + if (sel.indexes.indexOf(tmp) == -1) sel.indexes.push(tmp) + if (!sel.columns[tmp]) sel.columns[tmp] = [] + sel.columns[tmp].push(lst[i].column) + } + } else { + // selectType: row + const tmp = this.get(lst[i], true) + if (tmp != null) sel.indexes.push(tmp) + } + } + } + delete this.last.saved_sel + if (noRefresh !== true) this.refresh() + return Date.now() - time + } + message(options) { + return w2utils.message({ + owner: this, + box: this.box, + after: '.w2ui-grid-header' + }, options) + } + confirm(options) { + return w2utils.confirm({ + owner: this, + box: this.box, + after: '.w2ui-grid-header' + }, options) + } +} + +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base, w2tabs, w2toolbar, w2tooltip, w2field + * + * == TODO == + * - include delta on save + * - tabs below some fields (could already be implemented) + * - form with toolbar & tabs + * - promise for load, save, etc. + * + * == 2.0 changes + * - CSP - fixed inline events + * - removed jQuery dependency + * - better groups support tabs now + * - form.confirm - refactored + * - form.message - refactored + * - observeResize for the box + * - removed msgNotJSON, msgAJAXerror + * - applyFocus -> setFocus + * - getFieldValue(fieldName) = returns { curent, previous, original } + * - setFieldVallue(fieldName, value) + * - getValue(..., original) -- return original if any + * - added .hideErrors() + * - reuqest, save, submit - return promises + * - this.recid = null if no record needs to be pulled + * - remove form.multiplart + * - this.method - for saving only + * - added field.html.class + * - setValue(..., noRefresh) + * - rememberOriginal() + * - saveCleanRecord + * - added options.itemMap = { id: 'id', text: 'text' } - to map id, text fields if needed + * - hideGroup/showGroup - new methods + * - getAction/actionHide/actionShow/actionDisable/actionEnable - new methods + */ + +class w2form extends w2base { + constructor(options) { + super(options.name) + this.name = null + this.header = '' + this.box = null // HTML element that hold this element + this.url = '' + this.method = null // if defined, it will be http method when saving + this.routeData = {} // data for dynamic routes + this.formURL = '' // url where to get form HTML + this.formHTML = '' // form HTML (might be loaded from the url) + this.page = 0 // current page + this.pageStyle = '' + this.recid = null // if not null, then load record + this.fields = [] + this.actions = {} + this.record = {} + this.original = null + this.dataType = null // only used when not null, otherwise from w2utils.settings.dataType + this.saveCleanRecord = true // if true, it will submit clean record when saving + this.postData = {} + this.httpHeaders = {} + this.toolbar = {} // if not empty, then it is toolbar + this.tabs = {} // if not empty, then it is tabs object + this.style = '' + this.focus = 0 // focus first or other element + this.autosize = true // autosize, if false the container must have a height set + this.nestedFields = true // use field name containing dots as separator to look into object + this.tabindexBase = 0 // this will be added to the auto numbering + this.isGenerated = false + this.last = { + fetchCtrl: null, // last fetch AbortController + fetchOptions: null, // last fetch options + errors: [] + } + this.onRequest = null + this.onLoad = null + this.onValidate = null + this.onSubmit = null + this.onProgress = null + this.onSave = null + this.onChange = null + this.onInput = null + this.onRender = null + this.onRefresh = null + this.onResize = null + this.onDestroy = null + this.onAction = null + this.onToolbar = null + this.onError = null + this.msgRefresh = 'Loading...' + this.msgSaving = 'Saving...' + this.msgServerError = 'Server error' + this.ALL_TYPES = ['text', 'textarea', 'email', 'pass', 'password', 'int', 'float', 'money', 'currency', + 'percent', 'hex', 'alphanumeric', 'color', 'date', 'time', 'datetime', 'toggle', 'checkbox', 'radio', + 'check', 'checks', 'list', 'combo', 'enum', 'file', 'select', 'switch', 'map', 'array', 'div', 'custom', 'html', + 'empty', 'columns'] + this.LIST_TYPES = ['select', 'radio', 'check', 'checks', 'list', 'combo', 'enum', 'switch'] + this.W2FIELD_TYPES = ['int', 'float', 'money', 'currency', 'percent', 'hex', 'alphanumeric', 'color', + 'date', 'time', 'datetime', 'list', 'combo', 'enum', 'file'] + // mix in options + w2utils.extend(this, options) + // remember items + const record = options.record + const original = options.original + const fields = options.fields + const toolbar = options.toolbar + let tabs = options.tabs + // extend items + Object.assign(this, { record: {}, original: null, fields: [], tabs: {}, toolbar: {}, handlers: [] }) + // preprocess fields + if (fields) { + const sub = _processFields(fields) + this.fields = sub.fields + if (!tabs && sub.tabs.length > 0) { + tabs = sub.tabs + } + } + // prepare tabs + if (Array.isArray(tabs)) { + w2utils.extend(this.tabs, { tabs: [] }) + for (let t = 0; t < tabs.length; t++) { + const tmp = tabs[t] + if (typeof tmp === 'object') { + this.tabs.tabs.push(tmp) + if (tmp.active === true) { + this.tabs.active = tmp.id + } + } else { + this.tabs.tabs.push({ id: tmp, text: tmp }) + } + } + } else { + w2utils.extend(this.tabs, tabs) + } + w2utils.extend(this.toolbar, toolbar) + for (const p in record) { // it is an object + if (w2utils.isPlainObject(record[p])) { + this.record[p] = w2utils.clone(record[p]) + } else { + this.record[p] = record[p] + } + } + for (const p in original) { // it is an object + if (w2utils.isPlainObject(original[p])) { + this.original[p] = w2utils.clone(original[p]) + } else { + this.original[p] = original[p] + } + } + // generate html if necessary + if (this.formURL !== '') { + fetch(this.formURL) + .then(resp => resp.text()) + .then(text => { + this.formHTML = text + this.isGenerated = true + if (this.box) this.render(this.box) + }) + } else if (!this.formURL && !this.formHTML) { + this.formHTML = this.generateHTML() + this.isGenerated = true + } else if (this.formHTML) { + this.isGenerated = true + } + // render if box specified + if (typeof this.box === 'string') this.box = query(this.box).get(0) + if (this.box) this.render(this.box) + function _processFields(fields) { + const newFields = [] + const tabs = [] + // if it is an object + if (w2utils.isPlainObject(fields)) { + const tmp = fields + fields = [] + Object.keys(tmp).forEach((key) => { + const fld = tmp[key] + if (fld.type == 'group') { + fld.text = key + if (w2utils.isPlainObject(fld.fields)) { + const tmp2 = fld.fields + fld.fields = [] + Object.keys(tmp2).forEach((key2) => { + const fld2 = tmp2[key2] + fld2.field = key2 + fld.fields.push(_process(fld2)) + }) + } + fields.push(fld) + } else if (fld.type == 'tab') { + // add tab + const tab = { id: key, text: key } + if (fld.style) { + tab.style = fld.style + } + tabs.push(tab) + // add page to fields + const sub = _processFields(fld.fields).fields + sub.forEach(fld2 => { + fld2.html = fld2.html || {} + fld2.html.page = tabs.length - 1 + _process2(fld, fld2) + }) + fields.push(...sub) + } else { + fld.field = key + fields.push(_process(fld)) + } + }) + function _process(fld) { + const ignore = ['html'] + if (fld.html == null) fld.html = {} + Object.keys(fld).forEach(key => { + if (ignore.indexOf(key) != -1) return + if (['label', 'attr', 'style', 'text', 'span', 'page', 'column', 'anchor', + 'group', 'groupStyle', 'groupTitleStyle', 'groupCollapsible'].indexOf(key) != -1) { + fld.html[key] = fld[key] + delete fld[key] + } + }) + return fld + } + function _process2(fld, fld2) { + const ignore = ['style', 'html'] + Object.keys(fld).forEach(key => { + if (ignore.indexOf(key) != -1) return + if (['span', 'column', 'attr', 'text', 'label'].indexOf(key) != -1) { + if (fld[key] && !fld2.html[key]) { + fld2.html[key] = fld[key] + } + } + }) + } + } + // process groups + fields.forEach(field => { + if (field.type == 'group') { + // group properties + const group = { + group: field.text || '', + groupStyle: field.style || '', + groupTitleStyle: field.titleStyle || '', + groupCollapsible: field.collapsible === true + } + // loop through fields + if (Array.isArray(field.fields)) { + field.fields.forEach(gfield => { + const fld = w2utils.clone(gfield) + if (fld.html == null) fld.html = {} + w2utils.extend(fld.html, group) + Array('span', 'column', 'attr', 'label', 'page').forEach(key => { + if (fld.html[key] == null && field[key] != null) { + fld.html[key] = field[key] + } + }) + if (fld.field == null && fld.name != null) { + console.log('NOTICE: form field.name property is deprecated, please use field.field. Field ->', field) + fld.field = fld.name + } + newFields.push(fld) + }) + } + } else { + const fld = w2utils.clone(field) + if (fld.field == null && fld.name != null) { + console.log('NOTICE: form field.name property is deprecated, please use field.field. Field ->', field) + fld.field = fld.name + } + newFields.push(fld) + } + }) + return { fields: newFields, tabs } + } + } + get(field, returnIndex) { + if (arguments.length === 0) { + const all = [] + for (let f1 = 0; f1 < this.fields.length; f1++) { + if (this.fields[f1].field != null) all.push(this.fields[f1].field) + } + return all + } else { + for (let f2 = 0; f2 < this.fields.length; f2++) { + if (this.fields[f2].field == field) { + if (returnIndex === true) return f2; else return this.fields[f2] + } + } + return null + } + } + set(field, obj) { + for (let f = 0; f < this.fields.length; f++) { + if (this.fields[f].field == field) { + w2utils.extend(this.fields[f], obj) + delete this.fields[f].w2field // otherwise options are not updates + this.refresh(field) + return true + } + } + return false + } + getValue(field, original) { + if (this.nestedFields) { + let val + try { // need this to make sure no error in fields + const rec = original === true ? this.original : this.record + val = String(field).split('.').reduce((rec, i) => { return rec[i] }, rec) + } catch (event) { + } + return val + } else { + return this.record[field] + } + } + setValue(field, value, noRefresh) { + // will not refresh the form! + if (value === '' || value == null || + (Array.isArray(value) && value.length === 0) || + (w2utils.isPlainObject(value) && Object.keys(value).length == 0)) { + value = null + } + if (this.nestedFields) { + try { // need this to make sure no error in fields + let rec = this.record + String(field).split('.').map((fld, i, arr) => { + if (arr.length - 1 !== i) { + if (rec[fld]) rec = rec[fld]; else { rec[fld] = {}; rec = rec[fld] } + } else { + rec[fld] = value + } + }) + if (!noRefresh) this.setFieldValue(field, value) + return true + } catch (event) { + return false + } + } else { + this.record[field] = value + if (!noRefresh) this.setFieldValue(field, value) + return true + } + } + rememberOriginal() { + // remember original + if (this.original == null) { + if (Object.keys(this.record).length > 0) { + this.original = w2utils.clone(this.record) + } else { + this.original = {} + } + } + } + getFieldValue(name) { + const field = this.get(name) + if (field == null) return + const el = field.el + let previous = this.getValue(name) + const original = this.getValue(name, true) + // orginary input control + let current = el.value + // should not be set to '', incosistent logic + // if (previous == null) previous = '' + // clean extra chars + if (['int', 'float', 'percent', 'money', 'currency'].includes(field.type)) { + current = field.w2field.clean(current) + } + // radio list + if (['radio'].includes(field.type)) { + const selected = query(el).closest('.w2ui-field-group').find('input:checked').get(0) + if (selected) { + const item = field.options.items[query(selected).data('index')] + current = item.id + } else { + current = null + } + } + // single checkbox + if (['toggle', 'checkbox'].includes(field.type)) { + current = el.checked + } + // check list + if (['check', 'checks'].indexOf(field.type) !== -1) { + current = [] + const selected = query(el).closest('.w2ui-field-group').find('input:checked') + if (selected.length > 0) { + selected.each(el => { + const item = field.options.items[query(el).data('index')] + current.push(item.id) + }) + } + if (!Array.isArray(previous)) previous = [] + } + // lists + const selected = field.w2field?.selected // drop downs and other w2field objects + if (['list', 'enum', 'file'].includes(field.type) && selected) { + const nv = selected + const cv = previous + if (Array.isArray(nv)) { + current = [] + for (let i = 0; i < nv.length; i++) current[i] = w2utils.clone(nv[i]) // clone array + } + if (Array.isArray(cv)) { + previous = [] + for (let i = 0; i < cv.length; i++) previous[i] = w2utils.clone(cv[i]) // clone array + } + if (w2utils.isPlainObject(nv)) { + current = w2utils.clone(nv) // clone object + } + if (w2utils.isPlainObject(cv)) { + previous = w2utils.clone(cv) // clone object + } + } + // map, array + if (['map', 'array'].includes(field.type)) { + current = (field.type == 'map' ? {} : []) + field.$el.parent().find('.w2ui-map-field').each((div, ind) => { + const key = query(div).find('.w2ui-map.key').val() + const value = query(div).find('.w2ui-map.value').val() + if (typeof field.html?.render === 'function') { + current[ind] ??= {} + query(div).find('input').each(inp => { + const name = inp.dataset.name ?? inp.name + if (name != null && name != '') { + current[ind][name] = ['checkbox', 'radio'].includes(inp.type) ? inp.checked : inp.value + } + }) + } else if (field.type == 'map') { + current[key] = value + } else { + current.push(value) + } + }) + } + return { current, previous, original } // current - in input, previous - in form.record, original - before form change + } + findItem(item, items) { + return items.find(it => (it.id === item || it.id === item?.id)) + } + setFieldValue(name, value) { + const field = this.get(name) + if (field == null) return + const el = field.el + switch (field.type) { + case 'toggle': + case 'checkbox': { + el.checked = !!value + break + } + case 'radio': { + value = value?.id ?? value + const inputs = query(el).closest('.w2ui-field-group').find('input') + const items = field.options.items + items.forEach((it, ind) => { + const input = inputs.filter(`[data-index="${ind}"]`) + if (it.id === value) { // need exact match so to match empty string and 0 + input.prop('checked', true) + } else { + input.prop('checked', false) + } + // show or hide the whole line + if (it.hidden === true) { + input.closest('.w2ui-field-item').hide() + } else { + input.closest('.w2ui-field-item').show() + } + }) + break + } + case 'check': + case 'checks': { + if (!Array.isArray(value)) { + if (value != null) { + value = [value] + } else { + value = [] + } + } + value = value.map(val => val?.id ?? val) // convert if array of objects + const inputs = query(el).closest('div.w2ui-field-group').find('input') + const items = field.options.items + items.forEach((it, ind) => { + const input = inputs.filter(`[data-index="${ind}"]`) + input.prop('checked', !!value.includes(it.id)) + // show or hide the whole line + if (it.hidden === true) { + input.closest('.w2ui-field-item').hide() + } else { + input.closest('.w2ui-field-item').show() + } + }) + break + } + case 'list': + case 'combo': + let item = value + const map = field.options?.itemMap + /** + * if it is a "simple" value, then find item in options.items + */ + if (item?.id == null && Array.isArray(field.options?.items)) { + field.options.items.forEach(it => { + const val = w2utils.getNested(it, map?.id ?? 'id') + if (val === value) item = it + }) + } + /** + * If item.id is there, but item.text is not there, then look up item.text in options.items + */ + if (item?.id != null && item?.text == null && Array.isArray(field.options?.items)) { + field.options.items.forEach(it => { + const id = w2utils.getNested(it, map?.id ?? 'id') + if (id === item.id) { + item.text = w2utils.getNested(it, map.text ?? 'text') + } + }) + } + // if item is found in field.options, update it in the this.records + if (item != value) { + this.setValue(field.name, item, true) + } + if (field.type == 'list') { + field.w2field.selected = item + field.w2field.refresh() + } else { + field.el.value = item?.text ?? value + } + break + case 'switch': { + el.value = value + field.toolbar.uncheck(...field.toolbar.get()) + field.toolbar.check(value) + break + } + case 'enum': + case 'file': { + if (!Array.isArray(value)) { + value = value != null ? [value] : [] + } + const items = [...value] + // find item in options.items, if any + let updated = false + items.forEach((item, ind) => { + if (item?.id == null && Array.isArray(field.options.items)) { + field.options.items.forEach(it => { + if (it.id == item) { + items[ind] = it + updated = true + } + }) + } + }) + if (updated) { + this.setValue(field.name, items, true) + } + field.w2field.selected = items + field.w2field.refresh() + break + } + case 'map': + case 'array': { + // init map + if (field.type == 'map' && (value == null || !w2utils.isPlainObject(value))) { + this.setValue(field.field, {}, true) + value = this.getValue(field.field) + } + if (field.type == 'array' && (value == null || !Array.isArray(value))) { + this.setValue(field.field, [], true) + value = this.getValue(field.field) + } + const container = query(field.el).parent().find('.w2ui-map-container') + field.el.mapRefresh(value, container) + break + } + case 'div': + case 'custom': { + query(el).html(value) + break + } + case 'color': { + el.value = value ?? '' + field.w2field.refresh() + break + } + case 'html': + case 'empty': + break + default: + // all other fields, text, int, float + if (value != null && el._w2field?.format) { + const obj = el._w2field + value = obj.format(obj.clean(value)) + } + el.value = value ?? '' + break + } + // now go through all fields and see if there is a dependent field + this.fields.forEach(fld => { + if (fld?.options?.parentList != null) { + let updated + let values = this.getValue(fld.options.parentList) + if (Array.isArray(values)) { + values = values.map(vv => vv.id) + } else { + values = values?.id != null ? [values.id] : [] + } + fld.options?.items?.forEach?.(item => { + const parent = w2utils.getNested(item, fld.options.parentField ?? 'parentId') + if (parent == null) { + return + } + const possible = w2utils.clone(Array.isArray(parent) ? parent : [parent]) + possible.unshift('') + const includes = values.some(item => possible.includes(item)) + if (includes && item.hidden === true) { + item.hidden = false + updated = true + } else if (!includes && item.hidden !== true) { + item.hidden = true + updated = true + } + }) + if (updated) { + let value = this.getValue(fld.field) + if (value?.id != null) value = value.id + // if item is not visible, then clear its field + if (fld.type == 'enum') { + const valid = fld.options.items.filter(it => !it.hidden).map(it => it.id) + let values = this.getValue(fld.field) + if (!Array.isArray(values)) values = [values] + // make sure they are objects + values = values.map(it => { + if (typeof it === 'string' || typeof it === 'number') { + it = fld.options.items.find(ii => ii.id == it) + } + return it + }) + const new_values = values.filter(it => valid.includes(it.id)) + this.setValue(fld.field, new_values, true) + } else { + fld.options.items.forEach(it => { + if (it.id == value && it.hidden) { + this.setValue(fld.field, null, true) + } + }) + } + // set new items + this.set(fld.field, { items: fld.options.items }) + } + } + }) + } + show() { + const effected = [] + for (let a = 0; a < arguments.length; a++) { + const fld = this.get(arguments[a]) + if (fld && fld.hidden) { + fld.hidden = false + effected.push(fld.field) + } + } + if (effected.length > 0) this.refresh.apply(this, effected) + this.updateEmptyGroups() + return effected + } + hide() { + const effected = [] + for (let a = 0; a < arguments.length; a++) { + const fld = this.get(arguments[a]) + if (fld && !fld.hidden) { + fld.hidden = true + effected.push(fld.field) + } + } + if (effected.length > 0) this.refresh.apply(this, effected) + this.updateEmptyGroups() + return effected + } + enable() { + const effected = [] + for (let a = 0; a < arguments.length; a++) { + const fld = this.get(arguments[a]) + if (fld && fld.disabled) { + fld.disabled = false + effected.push(fld.field) + } + } + if (effected.length > 0) this.refresh.apply(this, effected) + return effected + } + disable() { + const effected = [] + for (let a = 0; a < arguments.length; a++) { + const fld = this.get(arguments[a]) + if (fld && !fld.disabled) { + fld.disabled = true + effected.push(fld.field) + } + } + if (effected.length > 0) this.refresh.apply(this, effected) + return effected + } + updateEmptyGroups() { + // hide empty groups + query(this.box).find('.w2ui-group').each((group) => { + if (isHidden(query(group).find('.w2ui-field'))) { + query(group).hide() + } else { + query(group).show() + } + }) + function isHidden($els) { + let flag = true + $els.each((el) => { + if (el.style.display != 'none') flag = false + }) + return flag + } + } + hideGroup(groupName) { + const fields = [] + let current = '' + this.fields.forEach(fld => { + if (fld.html.group != null && fld.html.group !== '') { + current = String(fld.html.group).toLowerCase() + } + if (groupName.toLowerCase() == current) { + fields.push(fld.field) + } + }) + this.hide(...fields) + this.resize() + } + showGroup(groupName) { + const fields = [] + let current = '' + this.fields.forEach(fld => { + if (fld.html.group != null && fld.html.group !== '') { + current = String(fld.html.group).toLowerCase() + } + if (groupName.toLowerCase() == current) { + fields.push(fld.field) + } + }) + this.show(...fields) + this.resize() + } + /** + * When user clicks on group title, it will toggle the group (collapse or expand it). + */ + toggleGroup(groupName, show) { + const el = query(this.box).find('.w2ui-group-title[data-group="' + w2utils.base64encode(groupName) + '"]') + if (el.length === 0) return + const el_next = query(el.prop('nextElementSibling')) + if (typeof show === 'undefined') { + show = (el_next.css('display') == 'none') + } + if (show) { + el_next.show() + el.find('span').addClass('w2ui-icon-collapse').removeClass('w2ui-icon-expand') + } else { + el_next.hide() + el.find('span').addClass('w2ui-icon-expand').removeClass('w2ui-icon-collapse') + } + } + change() { + Array.from(arguments).forEach((field) => { + const tmp = this.get(field) + if (tmp.$el) tmp.$el.change() + }) + } + reload(callBack) { + const url = (typeof this.url !== 'object' ? this.url : this.url.get) + if (url && this.recid != null) { + // this.clear(); + return this.request(callBack) // returns promise + } else { + // this.refresh(); // no need to refresh + if (typeof callBack === 'function') callBack() + return new Promise(resolve => { resolve() }) // resolved promise + } + } + clear() { + if (arguments.length != 0) { + Array.from(arguments).forEach((field) => { + let rec = this.record + String(field).split('.').map((fld, i, arr) => { + if (arr.length - 1 !== i) rec = rec[fld]; else delete rec[fld] + }) + this.refresh(field) + }) + } else { + this.recid = null + this.record = {} + this.original = null + this.refresh() + this.hideErrors() + } + } + error(msg) { + // let the management of the error outside of the form + const edata = this.trigger('error', { + target: this.name, + message: msg, + fetchCtrl: this.last.fetchCtrl, + fetchOptions: this.last.fetchOptions + }) + if (edata.isCancelled === true) return + // need a time out because message might be already up) + setTimeout(() => { this.message(msg) }, 1) + // event after + edata.finish() + } + message(options) { + return w2utils.message({ + owner: this, + box: this.box, + after: '.w2ui-form-header' + }, options) + } + confirm(options) { + return w2utils.confirm({ + owner: this, + box: this.box, + after: '.w2ui-form-header' + }, options) + } + validate(showErrors) { + if (showErrors == null) showErrors = true + // validate before saving + const errors = [] + for (let f = 0; f < this.fields.length; f++) { + const field = this.fields[f] + if (field.type == 'columns' || field.field == null) { + continue + } + if (this.getValue(field.field) == null) this.setValue(field.field, '') + if (['int', 'float', 'currency', 'money'].indexOf(field.type) != -1) { + const val = this.getValue(field.field) + const min = field.options.min + const max = field.options.max + if (min != null && val != null && val < min) { + errors.push({ field: field, error: w2utils.lang('Should be more than ${min}', { min }) }) + } + if (max != null && val != null && val > max) { + errors.push({ field: field, error: w2utils.lang('Should be less than ${max}', { max }) }) + } + } + switch (field.type) { + case 'alphanumeric': + if (this.getValue(field.field) && !w2utils.isAlphaNumeric(this.getValue(field.field))) { + errors.push({ field: field, error: w2utils.lang('Not alpha-numeric') }) + } + break + case 'int': + if (this.getValue(field.field) && !w2utils.isInt(this.getValue(field.field))) { + errors.push({ field: field, error: w2utils.lang('Not an integer') }) + } + break + case 'percent': + case 'float': + if (this.getValue(field.field) && !w2utils.isFloat(this.getValue(field.field))) { + errors.push({ field: field, error: w2utils.lang('Not a float') }) + } + break + case 'currency': + case 'money': + if (this.getValue(field.field) && !w2utils.isMoney(this.getValue(field.field))) { + errors.push({ field: field, error: w2utils.lang('Not in money format') }) + } + break + case 'color': + case 'hex': + if (this.getValue(field.field) && !w2utils.isHex(this.getValue(field.field))) { + errors.push({ field: field, error: w2utils.lang('Not a hex number') }) + } + break + case 'email': + if (this.getValue(field.field) && !w2utils.isEmail(this.getValue(field.field))) { + errors.push({ field: field, error: w2utils.lang('Not a valid email') }) + } + break + case 'checkbox': + // convert true/false + if (this.getValue(field.field) == true) { + this.setValue(field.field, true) + } else { + this.setValue(field.field, false) + } + break + case 'date': + // format date before submit + if (!field.options.format) field.options.format = w2utils.settings.dateFormat + if (this.getValue(field.field) && !w2utils.isDate(this.getValue(field.field), field.options.format)) { + errors.push({ field: field, error: w2utils.lang('Not a valid date') + ': ' + field.options.format }) + } + break + case 'list': + case 'combo': + case 'switch': + break + case 'enum': + break + } + // === check required - if field is '0' it should be considered not empty + const val = this.getValue(field.field) + if (field.hidden !== true && field.required && + !['div', 'custom', 'html', 'empty'].includes(field.type) && + (val == null || val === '' || (Array.isArray(val) && val.length === 0) || + (w2utils.isPlainObject(val) && Object.keys(val).length == 0))) { + errors.push({ field: field, error: w2utils.lang('Required field') }) + } + if (field.hidden !== true && field.options?.minLength > 0 && + !['enum', 'list', 'combo'].includes(field.type) && // since minLength is used there for other purpose + (val == null || val.length < field.options.minLength)) { + errors.push({ field: field, error: w2utils.lang('Field should be at least ${count} characters.', + { count: field.options.minLength }) }) + } + } + // event before + const edata = this.trigger('validate', { target: this.name, errors: errors }) + if (edata.isCancelled === true) return + // show error + this.last.errors = errors + if (showErrors) this.showErrors() + // event after + edata.finish() + return errors + } + showErrors() { + // TODO: check edge cases + // -- invisible pages + // -- form refresh + const errors = this.last.errors + if (errors.length <= 0) return + // show errors + this.goto(errors[0].field.page) + query(errors[0].field.$el).parents('.w2ui-field')[0].scrollIntoView({ block: 'nearest', inline: 'nearest' }) + // show errors + // show only for visible controls + errors.forEach(error => { + const opt = w2utils.extend({ + anchorClass: 'w2ui-error', + class: 'w2ui-light', + position: 'right|left', + hideOn: ['input'] + }, error.options) + if (error.field == null) return + let anchor = error.field.el + if (error.field.type === 'radio') { // for radio and checkboxes + anchor = query(error.field.el).closest('div').get(0) + } else if (['enum', 'file'].includes(error.field.type)) { + // TODO: check + // anchor = (error.field.el).data('w2field').helpers.multi + // $(fld).addClass('w2ui-error') + } + w2tooltip.show(w2utils.extend({ + anchor, + name: `${this.name}-${error.field.field}-error`, + html: error.error + }, opt)) + }) + // on scroll update errors so they will appear in correct places + this.last.errorsShown = true + query(errors[0].field.$el).parents('.w2ui-page') + .off('.hideErrors') + .on('scroll.hideErrors', (evt) => { + if (this.last.errorsShown) { + this.showErrors() + } + }) + } + hideErrors() { + this.last.errorsShown = false + this.fields.forEach(field => { + w2tooltip.hide(`${this.name}-${field.field}-error`) + }) + } + getChanges() { + // TODO: not working on nested structures + let diff = {} + if (this.original != null && typeof this.original === 'object' && Object.keys(this.record).length !== 0) { + diff = doDiff(this.record, this.original, {}) + } + return diff + function doDiff(record, original, result) { + if (Array.isArray(record) && Array.isArray(original)) { + while (record.length < original.length) { + record.push(null) + } + } + for (const i in record) { + if (record[i] != null && typeof record[i] === 'object') { + result[i] = doDiff(record[i], original[i] || {}, {}) + if (!result[i] || (Object.keys(result[i]).length == 0 && Object.keys(original[i].length == 0))) delete result[i] + } else if (record[i] != original[i] || (record[i] == null && original[i] != null)) { // also catch field clear + result[i] = record[i] + } + } + return Object.keys(result).length != 0 ? result : null + } + } + getCleanRecord(strict) { + const data = w2utils.clone(this.record) + this.fields.forEach((fld) => { + if (fld.type == 'columns' || fld.field == null) { + return + } + if (['list', 'combo', 'enum'].indexOf(fld.type) != -1) { + const tmp = { nestedFields: true, record: data } + const val = this.getValue.call(tmp, fld.field) + if (w2utils.isPlainObject(val) && val.id != null) { // should be true if val.id === '' + this.setValue.call(tmp, fld.field, val.id) + } + if (Array.isArray(val)) { + val.forEach((item, ind) => { + if (w2utils.isPlainObject(item) && item.id) { + val[ind] = item.id + } + }) + } + } + if (fld.type == 'map') { + const tmp = { nestedFields: true, record: data } + const val = this.getValue.call(tmp, fld.field) + if (val._order) delete val._order + } + if (fld.type == 'file') { + const tmp = { nestedFields: true, record: data } + const val = this.getValue.call(tmp, fld.field) ?? [] + val.forEach(v => { + delete v.file + delete v.modified + }) + this.setValue.call(tmp, fld.field, val) + } + }) + // return only records present in fields + if (strict === true) { + Object.keys(data).forEach((key) => { + if (!this.get(key)) delete data[key] + }) + } + return data + } + request(postData, callBack) { // if (1) param then it is call back if (2) then postData and callBack + const self = this + let resolve, reject + const responseProm = new Promise((res, rej) => { resolve = res; reject = rej }) + // check for multiple params + if (typeof postData === 'function') { + callBack = postData + postData = null + } + if (postData == null) postData = {} + if (!this.url || (typeof this.url === 'object' && !this.url.get)) return + // build parameters list + const params = {} + // add list params + params.action = 'get' + params.recid = this.recid + params.name = this.name + // append other params + w2utils.extend(params, this.postData) + w2utils.extend(params, postData) + // event before + const edata = this.trigger('request', { target: this.name, url: this.url, httpMethod: 'GET', + postData: params, httpHeaders: this.httpHeaders }) + if (edata.isCancelled === true) return + // default action + this.record = {} + this.original = null + // call server to get data + this.lock(w2utils.lang(this.msgRefresh)) + let url = edata.detail.url + if (typeof url === 'object' && url.get) url = url.get + if (this.last.fetchCtrl) try { this.last.fetchCtrl.abort() } catch (e) {} + // process url with routeData + if (Object.keys(this.routeData).length != 0) { + const info = w2utils.parseRoute(url) + if (info.keys.length > 0) { + for (let k = 0; k < info.keys.length; k++) { + if (this.routeData[info.keys[k].name] == null) continue + url = url.replace((new RegExp(':' + info.keys[k].name, 'g')), this.routeData[info.keys[k].name]) + } + } + } + url = new URL(url, location) + const fetchOptions = w2utils.prepareParams(url, { + method: edata.detail.httpMethod, + headers: edata.detail.httpHeaders, + body: edata.detail.postData + }, { dataType: this.dataType, caller: this, action: 'request' }) + this.last.fetchCtrl = new AbortController() + fetchOptions.signal = this.last.fetchCtrl.signal + this.last.fetchOptions = fetchOptions + fetch(url, fetchOptions) + .catch(processError) + .then((resp) => { + if (resp?.status != 200) { + // if resp is undefined, it means request was aborted + if (resp) processError(resp) + return + } + resp.json() + .catch(processError) + .then(data => { + // event before + const edata = self.trigger('load', { + target: self.name, + fetchCtrl: this.last.fetchCtrl, + fetchOptions: this.last.fetchOptions, + data + }) + if (edata.isCancelled === true) return + // for backward compatibility + if (data.error == null && data.status === 'error') { + data.error = true + } + // if data.record is not present, then assume that entire response is the record + if (!data.record) { + Object.assign(data, { record: w2utils.clone(data) }) + } + // server response error, not due to network issues + if (data.error === true) { + self.error(w2utils.lang(data.message ?? this.msgServerError)) + } else { + self.record = w2utils.clone(data.record) + } + // event after + self.unlock() + edata.finish() + self.refresh() + // self.setFocus() + // call back + if (typeof callBack === 'function') callBack(data) + resolve(data) + }) + }) + // event after + edata.finish() + return responseProm + function processError(response) { + if (response.name === 'AbortError') { + // request was aborted by the form + return + } + self.unlock() + // trigger event + const edata2 = self.trigger('error', { response, fetchCtrl: self.last.fetchCtrl, fetchOptions: self.last.fetchOptions }) + if (edata2.isCancelled === true) return + // default behavior + if (response.status && response.status != 200) { + self.error(response.status + ': ' + response.statusText) + } else { + console.log('ERROR: Server request failed.', response, '. ', + 'Expected Response:', { error: false, record: { field1: 1, field2: 'item' }}, + 'OR:', { error: true, message: 'Error description' }) + self.error(String(response)) + } + // event after + edata2.finish() + reject(response) + } + } + submit(postData, callBack) { + return this.save(postData, callBack) + } + save(postData, callBack) { + const self = this + let resolve, reject + const saveProm = new Promise((res, rej) => { resolve = res; reject = rej }) + // check for multiple params + if (typeof postData === 'function') { + callBack = postData + postData = null + } + // validation + const errors = self.validate(true) + if (errors.length !== 0) return + // submit save + if (postData == null) postData = {} + if (!self.url || (typeof self.url === 'object' && !self.url.save)) { + console.log('ERROR: Form cannot be saved because no url is defined.') + return + } + self.lock(w2utils.lang(self.msgSaving) + ' ') + // build parameters list + const params = {} + // add list params + params.action = 'save' + params.recid = self.recid + params.name = self.name + // append other params + w2utils.extend(params, self.postData) + w2utils.extend(params, postData) + params.record = w2utils.clone(self.saveCleanRecord ? self.getCleanRecord() : self.record) + // event before + const edata = self.trigger('submit', { target: self.name, url: self.url, httpMethod: this.method ?? 'POST', + postData: params, httpHeaders: self.httpHeaders }) + if (edata.isCancelled === true) return + // default action + let url = edata.detail.url + if (typeof url === 'object' && url.save) url = url.save + if (self.last.fetchCtrl) self.last.fetchCtrl.abort() + // process url with routeData + if (Object.keys(self.routeData).length > 0) { + const info = w2utils.parseRoute(url) + if (info.keys.length > 0) { + for (let k = 0; k < info.keys.length; k++) { + if (self.routeData[info.keys[k].name] == null) continue + url = url.replace((new RegExp(':' + info.keys[k].name, 'g')), self.routeData[info.keys[k].name]) + } + } + } + url = new URL(url, location) + const fetchOptions = w2utils.prepareParams(url, { + method: edata.detail.httpMethod, + headers: edata.detail.httpHeaders, + body: edata.detail.postData + }, { dataType: this.dataType, caller: this, action: 'save' }) + this.last.fetchCtrl = new AbortController() + fetchOptions.signal = this.last.fetchCtrl.signal + this.last.fetchOptions = fetchOptions + fetch(url, fetchOptions) + .catch(processError) + .then(resp => { + self.unlock() + if (resp?.status != 200) { + processError(resp ?? {}) + return + } + // parse server response + resp.json() + .catch(processError) + .then(data => { + // event before + const edata = self.trigger('save', { + target: self.name, + fetchCtrl: this.last.fetchCtrl, + fetchOptions: this.last.fetchOptions, + data + }) + if (edata.isCancelled === true) return + // server error, not due to network issues + if (data.error === true) { + self.error(w2utils.lang(data.message ?? this.msgServerError)) + } else { + self.original = null + } + // event after + edata.finish() + self.refresh() + // call back + if (typeof callBack === 'function') callBack(data) + resolve(data) + }) + }) + // event after + edata.finish() + return saveProm + function processError(response) { + if (response?.name === 'AbortError') { + // request was aborted by the form + return + } + self.unlock() + // trigger event + const edata2 = self.trigger('error', { response, fetchCtrl: self.last.fetchCtrl, fetchOptions: self.last.fetchOptions }) + if (edata2.isCancelled === true) return + // default behavior + if (response.status && response.status != 200) { + response.json().then((data) => { + self.error(response.status + ': ' + data.message ?? response.statusText) + }).catch(() => { + self.error(response.status + ': ' + response.statusText) + }) + } else { + console.log('ERROR: Server request failed.', response, '. ', + 'Expected Response:', { error: false, record: { field1: 1, field2: 'item' }}, + 'OR:', { error: true, message: 'Error description' }) + self.error(String(response)) + } + // event after + edata2.finish() + reject() + } + } + lock(msg, showSpinner) { + const args = Array.from(arguments) + args.unshift(this.box) + w2utils.lock(...args) + } + unlock(speed) { + const box = this.box + w2utils.unlock(box, speed) + } + lockPage(page, msg, spinner) { + const $page = query(this.box).find('.page-' + page) + if ($page.length) { + // page found + w2utils.lock($page, msg, spinner) + return true + } + // page with this id not found! + return false + } + unlockPage(page, speed) { + const $page = query(this.box).find('.page-' + page) + if ($page.length) { + // page found + w2utils.unlock($page, speed) + return true + } + // page with this id not found! + return false + } + goto(page) { + if (this.page === page) return // already on this page + if (page != null) this.page = page + // if it was auto size, resize it + if (query(this.box).data('autoSize') === true) { + query(this.box).get(0).clientHeight = 0 + } + this.refresh() + } + generateHTML() { + const pages = [] // array for each page + let group = '' + let page + let column + let tabindex + let tabindex_str + for (let f = 0; f < this.fields.length; f++) { + let html = '' + tabindex = this.tabindexBase + f + 1 + tabindex_str = ' tabindex="' + tabindex + '"' + const field = this.fields[f] + if (field.html == null) field.html = {} + if (typeof field.html === 'string') { + field.html = { + html: field.html, + span: 0, + attr: 'tabindex' + } + tabindex_str = '' + } + if (field.options == null) field.options = {} + if (field.html.caption != null && field.html.label == null) { + console.log('NOTICE: form field.html.caption property is deprecated, please use field.html.label. Field ->', field) + field.html.label = field.html.caption + } + if (field.html.label == null) field.html.label = field.field + if (field.html.anchor != null && field.html.span == null) { + field.html.span = '' + } + field.html = w2utils.extend({ label: '', span: 6, attr: '', text: '', style: '', page: 0, column: 0 }, field.html) + if (page == null) page = field.html.page + if (column == null) column = field.html.column + // input control + let input = `` + switch (field.type) { + case 'pass': + case 'password': + input = input.replace('type="text"', 'type="password"') + break + case 'checkbox': { + input = ` + ` + break + } + case 'check': + case 'checks': { + if (field.options.items == null && field.html.items != null) field.options.items = field.html.items + let items = field.options.items + input = `
` + // normalized options + if (!Array.isArray(items)) items = [] + if (items.length > 0) { + items = w2utils.normMenu.call(this, items, field.options) + } + // generate + for (let i = 0; i < items.length; i++) { + input += ` +
+ +
` + } + input += '
' + break + } + case 'radio': { + input = `
` + // normalized options + if (field.options.items == null && field.html.items != null) field.options.items = field.html.items + let items = field.options.items + if (!Array.isArray(items)) items = [] + if (items.length > 0) { + items = w2utils.normMenu.call(this, items, field.options) + } + // generate + for (let i = 0; i < items.length; i++) { + input += ` +
+ +
` + } + input += '
' + break + } + case 'select': { + input = `' + break + } + case 'switch': { + input = ` +
+
+ + ${field.html.text ?? ''} +
+ ` + break + } + case 'textarea': + input = `` + break + case 'toggle': + input = ` +
` + break + case 'map': + case 'array': + field.html.key = field.html.key || {} + field.html.value = field.html.value || {} + field.html.tabindex = tabindex + field.html.tabindex_str = tabindex_str + input = '' + (field.html.text || '') + '' + + '' + + '
' + break + case 'div': + case 'custom': + input = `
` + + (field && field.html && field.html.html ? field.html.html : '') + + '
' + break + case 'html': + case 'empty': + input = `
` + + (field && field.html ? (field.html.html || '') + (field.html.text || '') : '') + + '
' + break + } + if (group !== '') { + if (page != field.html.page || column != field.html.column || (field.html.group && (group != field.html.group))) { + pages[page][column] += '\n
\n
' + group = '' + } + } + if (field.html.group && (group != field.html.group)) { + let collapsible = '' + if (field.html.groupCollapsible) { + collapsible = '' + } + html += '\n
' + + '\n
' + + collapsible + w2utils.lang(field.html.group) + '
\n' + + '
' + group = field.html.group + } + if (field.type == 'columns') { + html += `
` + field.columns.forEach(col => { + html += `
${col.content}
` + }) + html += '
' + } else if (field.html.col_anchor != null) { + let span = (field.html.span != null ? 'w2ui-span' + field.html.span : '') + if (field.html.span == -1) span = 'w2ui-span-none' + let label = ` + ` + if (!field.html.label) label = '' + const text = (field.type != 'array' && field.type != 'map' ? w2utils.lang(field.type != 'checkbox' ? field.html.text : '') : '') + pages[field.html.page].anchors ??= {} + pages[field.html.page].anchors[field.html.col_anchor] = ` +
+ ${label} + ${['empty', 'switch', 'radio', 'check', 'checks'].includes(field.type) + ? input + : `
${input + text}
` +} +
` + } else if (field.html.anchor != null) { + const span = (field.html.span != null ? 'w2ui-span' + field.html.span : 'w2ui-span0') + let label = w2utils.lang(field.type != 'checkbox' ? field.html.label : field.html.text, true) + const text = w2utils.lang(field.type != 'checkbox' ? field.html.text : '') + if (field.html.span == -1) { + label = ` ${label} ` + } else { + label = ` ${label} ` + } + pages[field.html.page].anchors ??= {} + pages[field.html.page].anchors[field.html.anchor] = ` +
+ ${((field.type === 'empty' || field.type == 'switch') + ? input + : `
+ ${label} ${input} ${text} +
` + )} +
` + } else { + let span = (field.html.span != null ? 'w2ui-span' + field.html.span : '') + if (field.html.span == -1) span = 'w2ui-span-none' + let label = ` + ` + if (!field.html.label) label = '' + const text = (field.type != 'array' && field.type != 'map' ? w2utils.lang(field.type != 'checkbox' ? field.html.text : '') : '') + html += ` +
+ ${label} + ${['empty', 'switch', 'radio', 'check', 'checks'].includes(field.type) + ? input + : `
${input + text}
` +} +
` + } + if (pages[field.html.page] == null) pages[field.html.page] = {} + if (pages[field.html.page][field.html.column] == null) pages[field.html.page][field.html.column] = '' + pages[field.html.page][field.html.column] += html + page = field.html.page + column = field.html.column + } + if (group !== '') pages[page][column] += '\n
\n
' + if (this.tabs.tabs) { + for (let i = 0; i < this.tabs.tabs.length; i++) if (pages[i] == null) pages[i] = [] + } + // buttons if any + let buttons = '' + if (Object.keys(this.actions).length > 0) { + buttons += '\n
' + tabindex = this.tabindexBase + this.fields.length + 1 + for (const a in this.actions) { // it is an object + const act = this.actions[a] + const info = { text: '', style: '', 'class': '' } + if (w2utils.isPlainObject(act)) { + if (act.text == null && act.caption != null) { + console.log('NOTICE: form action.caption property is deprecated, please use action.text. Action ->', act) + act.text = act.caption + } + if (act.text) info.text = act.text + if (act.style) info.style = act.style + if (act.class) info.class = act.class + } else { + info.text = a + if (['save', 'update', 'create'].indexOf(a.toLowerCase()) !== -1) info.class = 'w2ui-btn-blue'; else info.class = '' + } + buttons += '\n ' + tabindex++ + } + buttons += '\n
' + } + let html = '' + for (let p = 0; p < pages.length; p++) { + html += '
' + if (!pages[p]) { + console.log(`ERROR: Page ${p} does not exist`) + return false + } + if (pages[p].before) { + html += pages[p].before + } + html += '
' + Object.keys(pages[p]).sort().forEach((c, ind) => { + if (c == parseInt(c)) { + html += '
' + (pages[p][c] || '') + '\n
' + } + }) + html += '\n
' + if (pages[p].after) { + html += pages[p].after + } + html += '\n
' + // process page anchors + if (pages[p].anchors) { + Object.keys(pages[p].anchors).forEach((key, ind) => { + html = html.replace(key, pages[p].anchors[key]) + }) + } + } + html += buttons + return html + } + action(action, event) { + const act = this.actions[action] + let click = act + if (w2utils.isPlainObject(act) && act.onClick) click = act.onClick + // event before + const edata = this.trigger('action', { target: action, action: act, originalEvent: event }) + if (edata.isCancelled === true) return + // default actions + if (typeof click === 'function') click.call(this, event) + // event after + edata.finish() + } + getAction(action) { + const ret = query(this.box).find('.w2ui-buttons button[name="' + action + '"]') + if (ret.length === 0) { + console.log('ERROR: Action "' + action + '" not found. Valid actions are: ' + Object.keys(this.actions).join(', ')) + } + return ret + } + actionHide(action) { + this.getAction(action).hide() + } + actionShow(action) { + this.getAction(action).show() + } + actionDisable(action) { + this.getAction(action).prop('disabled', true) + } + actionEnable(action) { + this.getAction(action).prop('disabled', false) + } + resize() { + const self = this + // event before + const edata = this.trigger('resize', { target: this.name }) + if (edata.isCancelled === true) return + // default behaviour + if (this.box != null) { + const header = query(this.box).find(':scope > div .w2ui-form-header') + const toolbar = query(this.box).find(':scope > div .w2ui-form-toolbar') + const tabs = query(this.box).find(':scope > div .w2ui-form-tabs') + const page = query(this.box).find(':scope > div .w2ui-page') + const dpage = query(this.box).find(':scope > div .w2ui-page.page-' + this.page + ' > div') + const buttons = query(this.box).find(':scope > div .w2ui-buttons') + // if no height, calculate it + const { headerHeight, tbHeight, tabsHeight } = resizeElements() + if (this.autosize) { // we don't need autosize every time + const cHeight = query(this.box).get(0).clientHeight + if (cHeight === 0 || query(this.box).data('autosize') == 'yes') { + query(this.box).css({ + height: headerHeight + tbHeight + tabsHeight + 15 + // 15 is extra height + (page.length > 0 ? w2utils.getSize(dpage, 'height') : 0) + + (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0) + + 'px' + }) + query(this.box).data('autosize', 'yes') + } + resizeElements() + } + // resize tabs and toolbar if any + this.tabs?.resize?.() + this.toolbar?.resize?.() + // resize switch fields + this.fields.forEach(field => { + if (field.type == 'switch') { + field.toolbar?.resize?.() + } + }) + function resizeElements() { + const headerHeight = (self.header !== '' ? w2utils.getSize(header, 'height') : 0) + const tbHeight = (Array.isArray(self.toolbar?.items) && self.toolbar?.items?.length > 0) + ? w2utils.getSize(toolbar, 'height') + : 0 + const tabsHeight = (Array.isArray(self.tabs?.tabs) && self.tabs?.tabs?.length > 0) + ? w2utils.getSize(tabs, 'height') + : 0 + // resize elements + toolbar.css({ top: headerHeight + 'px' }) + tabs.css({ top: headerHeight + tbHeight + 'px' }) + page.css({ + top: headerHeight + tbHeight + tabsHeight + 'px', + bottom: (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0) + 'px' + }) + // return some params + return { headerHeight, tbHeight, tabsHeight } + } + } + // event after + edata.finish() + } + refresh() { + const time = Date.now() + const self = this + if (!this.box) return + if (!this.isGenerated || !query(this.box).html()) return + // event before + const edata = this.trigger('refresh', { target: this.name, page: this.page, field: arguments[0], fields: arguments }) + if (edata.isCancelled === true) return + let fields = Array.from(this.fields.keys()) + if (arguments.length > 0) { + fields = Array.from(arguments) + .map((fld, ind) => { + if (typeof fld !== 'string') console.log('ERROR: Arguments in refresh functions should be field names') + return this.get(fld, true) // get index of field + }) + .filter((fld, ind) => { + if (fld != null) return true; else return false + }) + } else { + // update field.page with page it belongs too + query(this.box).find('input, textarea, select').each(el => { + const name = (query(el).attr('name') != null ? query(el).attr('name') : query(el).attr('id')) + const field = this.get(name) + if (field) { + // find page + const div = query(el).closest('.w2ui-page') + if (div.length > 0) { + for (let i = 0; i < 100; i++) { + if (div.hasClass('page-' + i)) { field.page = i; break } + } + } + } + }) + // default action + query(this.box).find('.w2ui-page').hide() + query(this.box).find('.w2ui-page.page-' + this.page).show() + query(this.box).find('.w2ui-form-header').html(w2utils.lang(this.header)) + // refresh tabs if needed + if (typeof this.tabs === 'object' && Array.isArray(this.tabs.tabs) && this.tabs.tabs.length > 0) { + query(this.box).find('#form_' + this.name + '_tabs').show() + this.tabs.active = this.tabs.tabs[this.page].id + this.tabs.refresh() + } else { + query(this.box).find('#form_' + this.name + '_tabs').hide() + } + // refresh tabs if needed + if (typeof this.toolbar === 'object' && Array.isArray(this.toolbar.items) && this.toolbar.items.length > 0) { + query(this.box).find('#form_' + this.name + '_toolbar').show() + this.toolbar.refresh() + } else { + query(this.box).find('#form_' + this.name + '_toolbar').hide() + } + } + // refresh values of fields + for (let f = 0; f < fields.length; f++) { + const field = this.fields[fields[f]] + if (field.name == null && field.field != null) field.name = field.field + if (field.field == null && field.name != null) field.field = field.name + field.$el = query(this.box).find(`[name='${String(field.name).replace(/\\/g, '\\\\')}']`) + field.el = field.$el.get(0) + if (field.el) field.el.id = field.name + if (field.w2field) { + field.w2field.reset() + } + field.$el + .off('.w2form') + .on('change.w2form', function(event) { + const value = self.getFieldValue(field.field) + // clear error class + if (['enum', 'file'].includes(field.type)) { + const helper = field.w2field?.helpers?.multi + query(helper).removeClass('w2ui-error') + } + if (this._previous != null) { + value.previous = this._previous + delete this._previous + } + // event before + const edata2 = self.trigger('change', { target: this.name, field: this.name, value, originalEvent: event }) + if (edata2.isCancelled === true) return + // default behavior + self.setValue(this.name, value.current) + // event after + edata2.finish() + }) + .on('input.w2form', function(event) { + self.rememberOriginal() + const value = self.getFieldValue(field.field) + // save previous for change event + if (this._previous == null) { + this._previous = value.previous + } + // event before + const edata2 = self.trigger('input', { target: self.name, field, value, originalEvent: event }) + if (edata2.isCancelled === true) return + // default action + self.setValue(this.name, value.current, true) + // event after + edata2.finish() + }) + // required + if (field.required) { + field.$el.closest('.w2ui-field').addClass('w2ui-required') + } else { + field.$el.closest('.w2ui-field').removeClass('w2ui-required') + } + // disabled + if (field.disabled != null) { + if (field.disabled) { + if (field.$el.data('tabIndex') == null) { + field.$el.data('tabIndex', field.$el.prop('tabIndex')) + } + field.$el + .prop('disabled', true) + .prop('tabIndex', -1) + .closest('.w2ui-field') + .addClass('w2ui-disabled') + } else { + field.$el + .prop('disabled', false) + .prop('tabIndex', field.$el.data('tabIndex') ?? field.$el.prop('tabIndex') ?? 0) + .closest('.w2ui-field') + .removeClass('w2ui-disabled') + } + } + // hidden + let tmp = field.el + if (!tmp) tmp = query(this.box).find('#' + field.field) + if (field.hidden) { + query(tmp).closest('.w2ui-field').hide() + } else { + query(tmp).closest('.w2ui-field').show() + } + } + // attach actions on buttons + query(this.box).find('button, input[type=button]').each(el => { + query(el).off('click').on('click', function(event) { + let action = this.value + if (this.id) action = this.id + if (this.name) action = this.name + self.action(action, event) + }) + }) + // init controls with record + for (let f = 0; f < fields.length; f++) { + const field = this.fields[fields[f]] + if (!field.el) continue + if (!field.$el.hasClass('w2ui-input')) field.$el.addClass('w2ui-input') + field.type = String(field.type).toLowerCase() + if (!field.options) field.options = {} + // list type + if (this.LIST_TYPES.includes(field.type)) { + const items = field.options.items + if (items == null) field.options.items = [] + if (field.type == 'switch') { + // should not have .text if it is not explicitly set, or toolbar will have text + items.forEach((item, ind) => { + return items[ind] = typeof item !== 'object' + ? { id: item, text: item } + : item + }) + } else { + field.options.items = w2utils.normMenu.call(this, items ?? [], field.options) + } + } + // switch + if (field.type == 'switch') { + if (field.toolbar) { + w2ui[this.name + '_' + field.name + '_tb'].destroy() + } + field.options?.items?.forEach?.(it => it.text == null ? it.text = '' : '') + const items = w2utils.normMenu.call(this, field.options.items, field.options) + items.forEach(item => item.type ??= 'radio') + field.toolbar = new w2toolbar({ + box: field.$el.prev().get(0), + name: this.name + '_' + field.name + '_tb', + items, + onClick(event) { + self.rememberOriginal() + const value = self.getFieldValue(field.name) + value.current = event.detail.item.id + const edata = self.trigger('change', { target: field.name, field: field.name, value, originalEvent: event }) + if (edata.isCancelled === true) { + return + } + self.record[field.name] = value.current + self.setFieldValue(field.name, value.current) + edata.finish() + } + }) + field.$el.prev().addClass('w2ui-form-switch') // need to add this class, as toolbar render will remove all w2ui-* classes + field.toolbar.resize() + field.$el + .off('.form-input') + .on('focus.form-input', event => { + const ind = field.toolbar.get(field.$el.val(), true) + query(event.target).prop('_index', ind) + query(field.toolbar.box).addClass('w2ui-tb-focus') + }) + .on('blur.form-input', event => { + query(event.target).removeProp('_index') + query(`#${field.name}-tb .w2ui-tb-button`).removeClass('over') + query(field.toolbar.box).removeClass('w2ui-tb-focus') + }) + .on('keydown.form-input', event => { + let ind = query(event.target).prop('_index') + switch (event.key) { + case 'ArrowLeft': { + if (ind > 0) ind-- + query(`#${field.name}-tb .w2ui-tb-button`) + .removeClass('over') + .eq(ind) + .addClass('over') + query(event.target).prop('_index', ind) + break + } + case 'ArrowRight': { + if (ind < field.toolbar.items.length - 1) ind++ + query(`#${field.name}-tb .w2ui-tb-button`) + .removeClass('over') + .eq(ind) + .addClass('over') + query(event.target).prop('_index', ind) + break + } + } + if (event.keyCode == 32 || event.keyCode == 13) { + // space or enter - apply selected + self.rememberOriginal() + const value = self.getFieldValue(field.name) + value.current = field.toolbar.items[ind].id + const edata = self.trigger('change', { target: field.name, field: field.name, value, originalEvent: event }) + if (edata.isCancelled === true) { + return + } + self.record[field.name] = value.current + self.setFieldValue(field.name, value.current) + edata.finish() + query(`#${field.name}-tb .w2ui-tb-button`).removeClass('over') + } + // do not allow any input, besides a tab + if (!event.metaKey && !event.ctrlKey && event.keyCode != 9) { + event.preventDefault() + } + }) + } + // HTML select + if (field.type == 'select') { + // generate options + const items = field.options.items + let options = '' + items.forEach(item => { + options += `` + }) + field.$el.html(options) + } + // w2fields + if (this.W2FIELD_TYPES.includes(field.type)) { + field.w2field = field.w2field ?? + new w2field(w2utils.extend({}, field.options, { type: field.type })) + field.w2field.render(field.el) + } + // map and arrays + if (['map', 'array'].includes(field.type)) { + // need closure + (function(obj, field) { + let keepFocus + field.el.mapAdd = function(field, div, cnt, empty) { + const attr = (field.disabled ? ' readOnly ' : '') + (field.html.tabindex_str || '') + let html = `` + + `${field.html.value.text || ''}` + if (typeof field.html.render === 'function') { + html = field.html.render.call(self, { empty: empty === true, ind: cnt, field, div }) + // make sure all inputs have names as it is important for array objects + if (!field.el._errorDisplayed) { + query.html(html).filter('input').each(inp => { + const name = inp.dataset.name ?? inp.name + if (name == null || name == '') { + console.log(`ERROR: All inputs of the field %c"${field.name}"%c must have name attribute defined. No name for %c${inp.outerHTML}`, + 'color: blue', '', 'color: red') + } + }) + field.el._errorDisplayed = true + } + } else if (field.type == 'map') { + // has key input in front + html = ` + ${field.html.key.text || ''} + ` + html + } + div.append(`
${html}
`) + if (typeof field.html.render === 'function') { + const box = div.find(`[data-index="${cnt}"]`) + box.find('input').each(el => { + // set only if it is not defined in the HTML + if (query(el).attr('tabindex') == null) { + query(el).attr('tabindex', field.html.tabindex) + } + }) + if (typeof field.html.onRefresh === 'function') { + field.html.onRefresh.call(self, { index: cnt, empty, box: box.get(0) }) + } + } + } + field.el.mapRefresh = function(map, div) { + // generate options + let keys, $k, $v + if (field.type == 'map') { + if (!w2utils.isPlainObject(map)) map = {} + if (map._order == null) map._order = Object.keys(map) + keys = map._order + } + if (field.type == 'array') { + if (!Array.isArray(map)) map = [] + keys = map.map((item, ind) => { return ind }) + } + // delete fields (including empty one) + div.find('.w2ui-map-field').remove() + for (let ind = 0; ind < keys.length; ind++) { + const key = keys[ind] + let fld = div.find(`div[data-index='${ind}']`) + // add if does not exists + if (fld.length == 0) { + field.el.mapAdd(field, div, ind) + fld = div.find(`div[data-index='${ind}']`) + } + fld.attr('data-key', key) + if (typeof field.html?.render === 'function') { + const val = map[key] + fld.find('input').each(inp => { + const name = inp.dataset.name ?? inp.name // + if (inp.type == 'checkbox') { + inp.checked = val[name] ?? false + } else if (inp.type == 'radio') { + inp.checked = val[name] ?? false + } else { + inp.value = val[name] ?? '' + } + }) + } else { + $k = fld.find('.w2ui-map.key') + $v = fld.find('.w2ui-map.value') + let val = map[key] + if (field.type == 'array') { + const tmp = map.filter((it) => { return it?.key == key }) + if (tmp.length > 0) val = tmp[0].value + } + $k.val(key) + $v.val(val) + if (field.disabled === true || field.disabled === false) { + $k.prop('readOnly', !!field.disabled) + $v.prop('readOnly', !!field.disabled) + } + } + // call refresh + if (typeof field.html.onRefresh === 'function') { + field.html.onRefresh.call(self, { index: ind, box: div.find(`[data-index="${ind}"]`).get(0) }) + } + } + if (typeof field.html.render === 'function') { + $v = div.find('.w2ui-map-field:last-child input:first-child') + } + const cnt = keys.length + const curr = div.find(`div[data-index='${cnt}']`) + // if not disabled - add next if needed + if (curr.length === 0 && (!$k || $k.val() != '' || $v.val() != '') && + !($k && ($k.prop('readOnly') === true || $k.prop('disabled') === true)) + ) { + field.el.mapAdd(field, div, cnt, true) + } + if (field.disabled === true || field.disabled === false) { + curr.find('.key').prop('readOnly', !!field.disabled) + curr.find('.value').prop('readOnly', !!field.disabled) + } + // attach events + const container = query(field.el).get(0)?.nextSibling // should be div + query(container) + .off('.mapChange') + .on('mouseup.mapChange', 'input', function(event) { + /** * + * This hack is needed for the cases when this field is refreshed and focus in bettween of mousedown and mouse up. + * In such a case, the field will not get focused, but should be as there was mouse click. + */ + if (document.activeElement != event.target) { + event.target.focus() + } + }) + .on('keyup.mapChange', 'input', function(event) { + const $div = query(event.target).closest('.w2ui-map-field') + const next = $div.get(0).nextElementSibling + const prev = $div.get(0).previousElementSibling + if (event.keyCode == 13) { + const el = keepFocus ?? next + if (el instanceof HTMLElement) { + const inp = query(el).find('input') + if (inp.length > 0) { + inp.get(0).focus() + } + } + keepFocus = undefined + } + const className = query(event.target).hasClass('key') ? 'key' : 'value' + if (event.keyCode == 38 && prev) { // up key + query(prev).find(`input.${className}, input[name="${event.target.name}"]`).get(0).select() + event.preventDefault() + } + if (event.keyCode == 40 && next) { // down key + event.target.blur() // blur is neeeded because because it will trigger change which will re-render fields + const next = $div.get(0).nextElementSibling // need to query it again because it was re-rendered + query(next).find(`input.${className}, input[name="${event.target.name}"]`).get(0).select() + event.preventDefault() + } + }) + .on('keydown.mapChange', 'input', function(event) { + if (event.keyCode == 9) { // tab + /** + * In some cases, when elements are added dynamically after element was focused, hitting tab would not + * consider newly created elements are focusable, therefore we check here if focus goes to body on tab key + * then move it next input + */ + setTimeout(() => { + if (document.activeElement?.tagName == 'BODY') { + query(event.target.parentNode).next().find('input').get(0)?.focus() + } + }, 10) + } + if (event.keyCode == 38 || event.keyCode == 40) { + event.preventDefault() + } + }) + .on('input.mapChange', 'input', function(event) { + const fld = query(event.target).closest('div') + const cnt = fld.data('index') + const next = fld.get(0).nextElementSibling + // if last one, add new empty + let isEmpty = true + query(fld).find('input').each(el => { + if (!['checkbox', 'button'].includes(el.type) && el.value != '') isEmpty = false + }) + let isNextEmpty = true + query(next).find('input').each(el => { + if (!['checkbox', 'button'].includes(el.type) && el.value != '') isNextEmpty = false + }) + if (!isEmpty && !next) { + field.el.mapAdd(field, div, parseInt(cnt) + 1, true) + } else if (isEmpty && next && isNextEmpty) { + query(next).remove() + } + }) + .on('change.mapChange', 'input', function(event) { + self.rememberOriginal() + // event before + let { current, previous, original } = self.getFieldValue(field.field) + const $cnt = query(event.target).closest('.w2ui-map-container') + // delete empty + if (typeof field.html?.render === 'function') { + current = current.filter(kk => { + const val = [...(new Set(Object.values(kk).filter(vv => typeof vv !== 'boolean')))] + return !(val.length == 0 || (val.length == 1 && val[0] === '')) + }) + } else if (field.type == 'map') { + current._order = [] + $cnt.find('.w2ui-map.key').each(el => { current._order.push(el.value) }) + current._order = current._order.filter(k => k !== '') + delete current[''] + } else if (field.type == 'array') { + current = current.filter(k => k !== '') + } + const edata = self.trigger('change', { target: field.field, field: field.field, originalEvent: event, + value: { current, previous, original } + }) + if (edata.isCancelled === true) { + return + } + if (query(event.target).parent().find('input').val() == '') { + keepFocus = event.target + } + self.setValue(field.field, current) + field.el.mapRefresh(current, div) + // event after + edata.finish() + }) + } + })(this, field) + } + // set value to HTML input field + this.setFieldValue(field.field, this.getValue(field.name)) + } + // event after + edata.finish() + this.resize() + return Date.now() - time + } + render(box) { + const time = Date.now() + const self = this + if (typeof box === 'string') box = query(box).get(0) + // event before + const edata = this.trigger('render', { target: this.name, box: box ?? this.box }) + if (edata.isCancelled === true) return + // default action + if (box != null) { + this.unmount() // clean previous control + this.box = box + } + if (!this.isGenerated && !this.formHTML) return + if (!this.box) return + // render form + const html = '
' + + (this.header !== '' ? '
' + w2utils.lang(this.header) + '
' : '') + + ' ' + + ' ' + + this.formHTML + + '
' + query(this.box).attr('name', this.name) + .addClass('w2ui-reset w2ui-form') + .html(html) + if (query(this.box).length > 0) query(this.box)[0].style.cssText += this.style + w2utils.bindEvents(query(this.box).find('.w2ui-eaction'), this) + // init toolbar regardless it is defined or not + if (typeof this.toolbar.render !== 'function') { + this.toolbar = new w2toolbar(w2utils.extend({}, this.toolbar, { name: this.name + '_toolbar', owner: this })) + this.toolbar.on('click', function(event) { + const edata = self.trigger('toolbar', { target: event.target, originalEvent: event }) + if (edata.isCancelled === true) return + // no default action + edata.finish() + }) + } + if (typeof this.toolbar === 'object' && typeof this.toolbar.render === 'function') { + this.toolbar.render(query(this.box).find('#form_' + this.name + '_toolbar')[0]) + } + // init tabs regardless it is defined or not + if (typeof this.tabs.render !== 'function') { + this.tabs = new w2tabs(w2utils.extend({}, this.tabs, { name: this.name + '_tabs', owner: this, active: this.tabs.active })) + this.tabs.on('click', function(event) { + self.goto(this.get(event.target, true)) + }) + } + if (typeof this.tabs === 'object' && typeof this.tabs.render === 'function') { + this.tabs.render(query(this.box).find('#form_' + this.name + '_tabs')[0]) + if (this.tabs.active) this.tabs.click(this.tabs.active) + } + // event after + edata.finish() + // after render actions + this.resize() + const url = (typeof this.url !== 'object' ? this.url : this.url.get) + if (url && this.recid != null) { + this.request().catch(error => this.refresh()) // even if there was error, still need refresh + } else { + this.refresh() + } + // observe div resize + this.last.observeResize = new ResizeObserver(() => { this.resize() }) + this.last.observeResize.observe(this.box) + // focus on load + if (this.focus != -1) { + // let setCount = 0 + // const setFocus = () => { + // if (query(self.box).find('input, select, textarea').length > 0) { + // // self.setFocus() + // } else { + // setCount++ + // if (setCount < 20) setTimeout(setFocus, 50) // 1 sec max + // } + // } + // setFocus() + } + return Date.now() - time + } + unmount() { + super.unmount() + this.tabs?.unmount?.() + this.toolbar?.unmount?.() + this.last.observeResize?.disconnect() + } + destroy() { + // event before + const edata = this.trigger('destroy', { target: this.name }) + if (edata.isCancelled === true) return + // clean up + this.tabs?.destroy?.() + this.toolbar?.destroy?.() + if (query(this.box).find('#form_' + this.name + '_tabs').length > 0) { + this.unmount() + } + this.last.observeResize?.disconnect() + delete w2ui[this.name] + // event after + edata.finish() + } + setFocus(focus) { + if (typeof focus === 'undefined') { + // no argument - use form's focus property + focus = this.focus + } + let $input + // focus field by index + if (w2utils.isInt(focus)) { + if (focus < 0) { + return + } + const inputs = query(this.box) + .find('div:not(.w2ui-field-helper) > input, select, textarea, div > label:nth-child(1) > [type=radio]') + .filter(':not(.file-input)') + // find visible (offsetParent == null for any element is not visible) + while (inputs[focus].offsetParent == null && inputs.length > focus) { + focus++ + } + if (inputs[focus]) { + $input = query(inputs[focus]) + } + } else if (typeof focus === 'string') { + // focus field by name + $input = query(this.box).find(`[name='${focus}']`) + } + if ($input.length > 0) { + $input.get(0).focus() + } + return $input + } +} +/** + * Part of w2ui 2.0 library + * - Dependencies: mQuery, w2utils, w2base, w2tooltip, w2color, w2menu, w2date + * + * == TODO == + * - upload (regular files) + * - BUG with prefix/postfix and arrows (test in different contexts) + * - multiple date selection + * - month selection, year selections + * - MultiSelect - Allow Copy/Paste for single and multi values + * - add routeData to list/enum + * - ENUM, LIST: should have same as grid (limit, offset, search, sort) + * - ENUM, LIST: should support wild chars + * - add selection of predefined times (used for appointments) + * - options.items - can be an array + * - options.msgNoItems - can be a function + * - REMOTE fields + * + * == 2.0 changes + * - removed jQuery dependency + * - enum options.autoAdd + * - [numeric, date] - options.autoCorrect to enforce range and validity + * - remote source response items => records or just an array + * - deprecated "success" field for remote source response + * - CSP - fixed inline events + * - remove clear, use reset instead + * - options.msgSearch + * - options.msgNoItems + */ + +class w2field extends w2base { + constructor(type, options) { + super() + // sanitization + if (typeof type === 'string' && options == null) { + options = { type: type } + } + if (typeof type === 'object' && options == null) { + options = w2utils.clone(type) + } + if (typeof type === 'string' && typeof options === 'object') { + options.type = type + } + options.type = String(options.type).toLowerCase() + this.el = options.el ?? null + this.selected = null + this.helpers = {} // object or helper elements + this.type = options.type ?? 'text' + this.options = w2utils.clone(options) + this.onClick = options.onClick ?? null + this.onAdd = options.onAdd ?? null + this.onNew = options.onNew ?? null + this.onRemove = options.onRemove ?? null + this.onMouseEnter = options.onMouseEnter ?? null + this.onMouseLeave = options.onMouseLeave ?? null + this.onScroll = options.onScroll ?? null + this.tmp = {} // temp object + // clean up some options + delete this.options.type + delete this.options.onClick + delete this.options.onMouseEnter + delete this.options.onMouseLeave + delete this.options.onScroll + if (this.el) { + this.render(this.el) + } + } + render(el) { + if (!(el instanceof HTMLElement)) { + console.log('ERROR: Cannot init w2field on empty subject') + return + } + el._w2field?.reset?.() // will remove all previous events + el._w2field = this + this.el = el + this.init() + } + init() { + let options = this.options + let defaults + // only for INPUT or TEXTAREA + if (!['INPUT', 'TEXTAREA'].includes(this.el.tagName.toUpperCase())) { + console.log('ERROR: w2field could only be applied to INPUT or TEXTAREA.', this.el) + return + } + switch (this.type) { + case 'text': + case 'int': + case 'float': + case 'money': + case 'currency': + case 'percent': + case 'alphanumeric': + case 'bin': + case 'hex': { + defaults = { + min: null, + max: null, + step: 1, + autoFormat: true, + autoCorrect: true, + currency: { + prefix: w2utils.settings.currencyPrefix, + suffix: w2utils.settings.currencySuffix, + precision: w2utils.settings.currencyPrecision + }, + decimalSymbol: w2utils.settings.decimalSymbol, + groupSymbol: w2utils.settings.groupSymbol, + arrows: false, + keyboard: true, + precision: null, + prefix: '', + suffix: '' + } + this.options = w2utils.extend({}, defaults, options) + options = this.options // since object is re-created, need to re-assign + options.numberRE = new RegExp('[' + options.groupSymbol + ']', 'g') + options.moneyRE = new RegExp('[' + options.currency.prefix + options.currency.suffix + options.groupSymbol + ']', 'g') + options.percentRE = new RegExp('[' + options.groupSymbol + '%]', 'g') + // no keyboard support needed + if (['text', 'alphanumeric', 'hex', 'bin'].includes(this.type)) { + options.arrows = false + options.keyboard = false + } + break + } + case 'color': { + const size = parseInt(getComputedStyle(this.el)['font-size']) || 12 + defaults = { + prefix: '#', + suffix: `
 
`, + arrows: false, + advanced: null, // open advanced by default + transparent: true + } + this.options = w2utils.extend({}, defaults, options) + options = this.options // since object is re-created, need to re-assign + break + } + case 'date': { + defaults = { + format: w2utils.settings.dateFormat, // date format + keyboard: true, // if true, allows to select date with format + autoCorrect: true, // correc date or shows the error + start: null, // first date allowed to select + end: null, // last date allowed to select + blockDates: [], // array of blocked dates + blockWeekdays: [], // blocked weekdays 0 - sunday, 1 - monday, etc + colored: {}, // ex: { '3/13/2022': 'bg-color|text-color' } + btnNow: true // if true, displays Now button + } + this.options = w2utils.extend({ type: 'date' }, defaults, options) + options = this.options // since object is re-created, need to re-assign + if (query(this.el).attr('placeholder') == null) { + query(this.el).attr('placeholder', options.format) + } + break + } + case 'time': { + defaults = { + format: w2utils.settings.timeFormat, + keyboard: true, + autoCorrect: true, + start: null, + end: null, + btnNow: true, + noMinutes: false + } + this.options = w2utils.extend({ type: 'time' }, defaults, options) + options = this.options // since object is re-created, need to re-assign + if (query(this.el).attr('placeholder') == null) { + query(this.el).attr('placeholder', options.format) + } + break + } + case 'datetime': { + defaults = { + format: w2utils.settings.dateFormat + '|' + w2utils.settings.timeFormat, + keyboard: true, + autoCorrect: true, + start: null, + end: null, + startTime: null, + endTime: null, + blockDates: [], // array of blocked dates + blockWeekdays: [], // blocked weekdays 0 - sunday, 1 - monday, etc + colored: {}, // ex: { '3/13/2022': 'bg-color|text-color' } + btnNow: true, + noMinutes: false + } + this.options = w2utils.extend({ type: 'datetime' }, defaults, options) + options = this.options // since object is re-created, need to re-assign + if (query(this.el).attr('placeholder') == null) { + query(this.el).attr('placeholder', options.placeholder || options.format) + } + break + } + case 'list': + case 'combo': { + defaults = { + items: [], // array of items, can be a function + selected: {}, // selected item + itemMap: null, // can be { id: 'id', text: 'text' } to specify field mapping for an item + match: 'begins', // ['contains', 'is', 'begins', 'ends'] + filter: true, // weather to filter at all + compare: null, // compare function for filtering + prefix: '', // prefix for input + suffix: '', // sufix for input + icon: null, // icon class for selected item + iconStyle: '', // icon style for selected item + // -- remote items -- + url: null, // remove data source for items + method: null, // default comes from w2utils.settings.dataType + postData: {}, // additional data to submit to URL + recId: null, // map retrieved data from url to id, can be string or function + recText: null, // map retrieved data from url to text, can be string or function + debounce: 250, // number of ms to wait before sending server call on search + minLength: 1, // min number of chars when trigger search + cacheMax: 250, + // -- drop items -- + renderDrop: null, // render function for drop down item + maxDropHeight: 350, // max height for drop down menu + maxDropWidth: null, // if null then auto set + minDropWidth: null, // if null then auto set + // -- misc -- + markSearch: false, // if true, highlights search phrase + align: 'both', // align with the input ['left', 'right', 'both', 'none'] + altRows: true, // alternate row color for drop itesm + openOnFocus: false, // if true, shows drop items on focus + hideSelected: false, // hide selected item from drop down + msgNoItems: 'No matches', + msgSearch: 'Type to search...', + // -- events -- + onSearch: null, // when search needs to be performed + onRequest: null, // when request is submitted + onLoad: null, // when data is received + onError: null // when data fails to load due to server error + } + if (typeof options.items === 'function') { + options._items_fun = options.items + } + // need to be first + options.items = w2utils.normMenu.call(this, options.items, options) + if (this.type === 'list') { + // defaults.search = (options.items && options.items.length >= 10 ? true : false); + query(this.el).addClass('w2ui-select') + // if simple value - look it up + if (!w2utils.isPlainObject(options.selected) && Array.isArray(options.items)) { + options.items.forEach(item => { + if (item && item.id === options.selected) { + options.selected = w2utils.clone(item) + } + }) + } + } + options = w2utils.extend({}, defaults, options) + // validate match + const valid = ['is', 'begins', 'contains', 'ends'] + if (!valid.includes(options.match)) { + console.log(`ERROR: invalid value "${options.match}" for option.match. It should be one of following: ${valid.join(', ')}.`) + } + this.options = options + if (!w2utils.isPlainObject(options.selected)) options.selected = {} + this.selected = options.selected + query(this.el) + .attr('autocapitalize', 'off') + .attr('autocomplete', 'off') + .attr('autocorrect', 'off') + .attr('spellcheck', 'false') + if (options.selected.text != null) { + query(this.el).val(options.selected.text) + } + break + } + case 'enum': { + defaults = { + items: [], // id, text, tooltip, icon + selected: [], + itemMap: null, // can be { id: 'id', text: 'text' } to specify field mapping for an item + max: 0, // max number of selected items, 0 - unlimited + match: 'begins', // ['contains', 'is', 'begins', 'ends'] + filter: true, // if true, will apply filtering + compare: null, // compare function for filtering + // -- remote items -- + url: null, // remove source for items + method: null, // default httpMethod + postData: {}, + recId: null, // map retrieved data from url to id, can be string or function + recText: null, // map retrieved data from url to text, can be string or function + debounce: 250, // number of ms to wait before sending server call on search + minLength: 1, // min number of chars when trigger search + cacheMax: 250, + // -- item and drop items -- + maxItemWidth: 250, // max width for a single item + maxDropHeight: 350, // max height for drop down menu + maxDropWidth: null, // if null then auto set + renderItem: null, // render selected item + renderDrop: null, // render function for drop down item + // -- misc -- + style: '', // style for container div + openOnFocus: false, // if true, opens drop down on focus + markSearch: false, // if true, highlights search phrase + align: 'both', // align with the input ['left', 'right', 'both', 'none'] + altRows: true, // if ture, will use alternate row colors + hideSelected: true, // hide selected items from drop down + msgNoItems: 'No matches', + msgSearch: 'Type to search...', + // -- events -- + onAdd: null, // when item is selected from drop down + onNew: null, // when new item should be added + onRemove: null, // when item is removed + onSearch: null, // when search is triggered + onClick: null, // when item is clicked + onRequest: null, // when data is requested + onLoad: null, // when data is received + onError: null, // when data fails to load due to server error + onScroll: null, // when div with selected items is scrolled + onMouseEnter: null, // when mouse enters item + onMouseLeave: null // when mouse leaves item + } + options = w2utils.extend({}, defaults, options, { suffix: '' }) + if (typeof options.items === 'function') { + options._items_fun = options.items + } + // validate match + const valid = ['is', 'begins', 'contains', 'ends'] + if (!valid.includes(options.match)) { + console.log(`ERROR: invalid value "${options.match}" for option.match. It should be one of following: ${valid.join(', ')}.`) + } + options.items = w2utils.normMenu.call(this, options.items, options) + options.selected = w2utils.normMenu.call(this, options.selected, options) + this.options = options + if (!Array.isArray(options.selected)) options.selected = [] + this.selected = options.selected + break + } + case 'file': { + defaults = { + selected: [], // array of selected files + max: 0, // max number of selected files, 0 - unlim + maxSize: 0, // max size of all files, 0 - unlimited + maxFileSize: 0, // max size of a single file, 0 -unlimited + renderItem: null, // render function fo the selected item + // -- misc -- + maxItemWidth: 250, // max width for a single item + maxDropHeight: 350, // max height for drop down menu + maxDropWidth: null, // if null then auto set + readContent: true, // if true, it will readAsDataURL content of the file + showErrors: true, // if not true, will show errors + align: 'both', // align with the input ['left', 'right', 'both', 'none'] + altRows: true, // alternate row color for drop itesm + style: '', // style for container div + // -- events -- + onClick: null, // when item is clicked + onAdd: null, // when item is added + onRemove: null, // when item is removed + onMouseEnter: null, // when item is mouse over + onMouseLeave: null // when item is mouse out + } + options = w2utils.extend({}, defaults, options) + this.options = options + if (!Array.isArray(options.selected)) options.selected = [] + this.selected = options.selected + if (query(this.el).attr('placeholder') == null) { + query(this.el).attr('placeholder', w2utils.lang('Attach files by dragging and dropping or Click to Select')) + } + break + } + default: { + console.log(`ERROR: field type "${this.type}" is not supported.`) + break + } + } + // attach events + query(this.el) + .css('box-sizing', 'border-box') + .addClass('w2field w2ui-input') + .off('.w2field') + .on('change.w2field', (event) => { this.change(event) }) + .on('click.w2field', (event) => { this.click(event) }) + .on('focus.w2field', (event) => { this.focus(event) }) + .on('blur.w2field', (event) => { if (this.type !== 'list') this.blur(event) }) + .on('keydown.w2field', (event) => { this.keyDown(event) }) + .on('keyup.w2field', (event) => { this.keyUp(event) }) + // suffix and prefix need to be after styles + this.addPrefix() // only will add if needed + this.addSuffix() // only will add if needed + this.addSearch() + this.addMultiSearch() + // this.refresh() // do not call refresh, on change will trigger refresh (for list at list) + // format initial value + this.change(new Event('change')) + } + get() { + let ret + if (['list', 'enum', 'file'].indexOf(this.type) !== -1) { + ret = this.selected + } else { + ret = query(this.el).val() + } + return ret + } + set(val, append) { + if (['list', 'enum', 'file'].indexOf(this.type) !== -1) { + const overlay = w2menu.get(this.el.id + '_menu') + overlay?.hide() + if (this.type !== 'list' && append) { + if (!Array.isArray(this.selected)) this.selected = [] + this.selected.push(val) + // update selected array in overlay + if (overlay) overlay.options.selected = this.selected + query(this.el).trigger('input').trigger('change') + } else { + if (val == null) val = [] + const it = (this.type === 'enum' && !Array.isArray(val) ? [val] : val) + this.selected = it + query(this.el).trigger('input').trigger('change') + } + this.refresh() + } else { + query(this.el).val(val) + } + } + setIndex(ind, append) { + if (['list', 'enum'].indexOf(this.type) !== -1) { + const overlay = w2menu.get(this.el.id + '_menu') + overlay?.hide() + const items = this.options.items + if (items && items[ind]) { + if (this.type == 'list') { + this.selected = items[ind] + } + if (this.type == 'enum') { + if (!append) this.selected = [] + this.selected.push(items[ind]) + } + if (overlay) overlay.options.selected = this.selected + query(this.el).trigger('input').trigger('change') + this.refresh() + return true + } + } + return false + } + refresh() { + const options = this.options + const time = Date.now() + const styles = getComputedStyle(this.el) + // update color + if (this.type == 'color') { + let color = this.el.value + if (color.substr(0, 1) != '#' && color.substr(0, 3) != 'rgb') { + color = '#' + color + } + query(this.helpers.suffix).find(':scope > div').css('background-color', color) + } + // enum + if (this.type == 'list') { + // next line will not work in a form with span: -1 + // query(this.el).parent().css('white-space', 'nowrap') // needs this for arrow always to appear on the right side + // hide focus and show text + if (this.helpers.prefix) this.helpers.prefix.hide() + if (!this.helpers.search) return + // if empty show no icon + if (this.selected == null && options.icon) { + options.prefix = ` + + ` + this.addPrefix() + } else { + options.prefix = '' + this.addPrefix() + } + // focus helper + const focus = query(this.helpers.search_focus) + const icon = query(focus[0].previousElementSibling) + focus.css({ outline: 'none' }) + if (focus.val() === '') { + focus.css('opacity', 0) + icon.css('opacity', 0) + if (this.selected?.id != null) { // id could be "", then it is valid + const text = this.selected.text + const ind = this.findItemIndex(options.items, this.selected.id) + if (text != null) { + query(this.el) + .val(w2utils.lang(text)) + .data({ + selected: text, + selectedIndex: ind[0] + }) + } + } else { + this.el.value = '' + query(this.el).removeData('selected selectedIndex') + } + } else { + focus.css('opacity', 1) + icon.css('opacity', 1) + query(this.el).val('') + setTimeout(() => { + if (this.helpers.prefix) this.helpers.prefix.hide() + if (options.icon) { + focus.css('margin-left', '17px') + query(this.helpers.search).find('.w2ui-icon-search') + .addClass('show-search') + } else { + focus.css('margin-left', '0px') + query(this.helpers.search).find('.w2ui-icon-search') + .removeClass('show-search') + } + }, 1) + } + // if readonly or disabled + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) { + setTimeout(() => { + if (this.helpers.prefix) query(this.helpers.prefix).css('opacity', '0.6') + if (this.helpers.suffix) query(this.helpers.suffix).css('opacity', '0.6') + }, 1) + } else { + setTimeout(() => { + if (this.helpers.prefix) query(this.helpers.prefix).css('opacity', '1') + if (this.helpers.suffix) query(this.helpers.suffix).css('opacity', '1') + }, 1) + } + } + // multi select control + const div = this.helpers.multi + if (['enum', 'file'].includes(this.type) && div) { + let html = '' + if (Array.isArray(this.selected)) { + this.selected.forEach((it, ind) => { + if (it == null) return + html += ` +
+ ${ + typeof options.renderItem === 'function' + ? options.renderItem(it, ind, `
  
`) + : ` + ${it.icon ? `` : ''} +
  
+ ${(this.type === 'enum' ? it.text : it.name) ?? it.id ?? it} + ${it.size ? ` - ${w2utils.formatSize(it.size)}` : ''} + ` +} +
` + }) + } + const ul = div.find('.w2ui-multi-items') + if (options.style) { + div.attr('style', div.attr('style') + ';' + options.style) + } + query(this.el).css('z-index', '-1') + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) { + setTimeout(() => { + div[0].scrollTop = 0 // scroll to the top + div.addClass('w2ui-readonly') + .find('.li-item').css('opacity', '0.9') + .parent().find('.li-search').hide() + .find('input').prop('readOnly', true) + .closest('.w2ui-multi-items') + .find('.w2ui-list-remove').hide() + }, 1) + } else { + setTimeout(() => { + div.removeClass('w2ui-readonly') + .find('.li-item').css('opacity', '1') + .parent().find('.li-search').show() + .find('input').prop('readOnly', false) + .closest('.w2ui-multi-items') + .find('.w2ui-list-remove').show() + }, 1) + } + // clean + if (this.selected?.length > 0) { + query(this.el).attr('placeholder', '') + } + div.find('.w2ui-enum-placeholder').remove() + ul.find('.li-item').remove() + // add new list + if (html !== '') { + ul.prepend(html) + } else if (query(this.el).attr('placeholder') != null && div.find('input').val() === '') { + const style = w2utils.stripSpaces(` + padding-top: ${styles['padding-top']}; + padding-left: ${styles['padding-left']}; + box-sizing: ${styles['box-sizing']}; + line-height: ${styles['line-height']}; + font-size: ${styles['font-size']}; + font-family: ${styles['font-family']}; + `) + div.prepend(`
${query(this.el).attr('placeholder')}
`) + } + // ITEMS events + div.off('.w2item') + .on('scroll.w2item', (event) => { + const edata = this.trigger('scroll', { target: this.el, originalEvent: event }) + if (edata.isCancelled === true) return + // hide tooltip if any + w2tooltip.hide(this.el.id + '_preview') + // event after + edata.finish() + }) + .find('.li-item') + .on('click.w2item', (event) => { + const target = query(event.target).closest('.li-item') + const index = target.attr('index') + const item = this.selected[index] + if (query(target).hasClass('li-search')) return + event.stopPropagation() + let edata + // default behavior + if (query(event.target).hasClass('w2ui-list-remove')) { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + // trigger event + edata = this.trigger('remove', { target: this.el, originalEvent: event, item }) + if (edata.isCancelled === true) return + // remove file from input element + const transfer = new DataTransfer() + const input = query(event.target).closest('.w2ui-list').find('input.file-input').get(0) + if (input) { + Array.from(input.files) + .filter(f => f.name != item.name) + .forEach(f => transfer.items.add(f)) + input.files = transfer.files + } + // remove placeholder in the field + this.selected.splice(index, 1) + query(this.el).trigger('input').trigger('change') + query(event.target).remove() + } else { + // trigger event + edata = this.trigger('click', { target: this.el, originalEvent: event.originalEvent, item }) + if (edata.isCancelled === true) return + // if file - show image preview + let preview = item.tooltip + if (this.type === 'file') { + if ((/image/i).test(item.type)) { // image + preview = ` +
+ +
` + } + preview += ` +
+
${w2utils.lang('Name')}:
+
${item.name}
+
${w2utils.lang('Size')}:
+
${w2utils.formatSize(item.size)}
+
${w2utils.lang('Type')}:
+
${item.type}
+
${w2utils.lang('Modified')}:
+
${w2utils.date(item.modified)}
+
` + } + if (preview) { + const name = this.el.id + '_preview' + w2tooltip.show({ + name, + anchor: target.get(0), + html: preview, + hideOn: ['doc-click'], + class: '' + }) + .show((event) => { + const $img = query(`#w2overlay-${name} img`) + $img.on('load', function(event) { + const w = this.clientWidth + const h = this.clientHeight + if (w < 300 & h < 300) return + if (w >= h && w > 300) query(this).css('width', '300px') + if (w < h && h > 300) query(this).css('height', '300px') + }) + .on('error', function(event) { + this.style.display = 'none' + }) + }) + } + } + edata.finish() + }) + .on('mouseenter.w2item', (event) => { + const target = query(event.target).closest('.li-item') + if (query(target).hasClass('li-search')) return + const item = this.selected[query(event.target).attr('index')] + // trigger event + const edata = this.trigger('mouseEnter', { target: this.el, originalEvent: event, item }) + if (edata.isCancelled === true) return + // event after + edata.finish() + }) + .on('mouseleave.w2item', (event) => { + const target = query(event.target).closest('.li-item') + if (query(target).hasClass('li-search')) return + const item = this.selected[query(event.target).attr('index')] + // trigger event + const edata = this.trigger('mouseLeave', { target: this.el, originalEvent: event, item }) + if (edata.isCancelled === true) return + // event after + edata.finish() + }) + // update size for enum, hide for file + if (this.type === 'enum') { + const search = this.helpers.multi.find('input') + search.css({ width: '15px' }) + } else { + this.helpers.multi.find('.li-search').hide() + } + this.resize() + } + return Date.now() - time + } + // resizing width of list, enum, file controls + resize() { + const width = this.el.clientWidth + // let height = this.el.clientHeight + // if (this.tmp.current_width == width && height > 0) return + const styles = getComputedStyle(this.el) + const focus = this.helpers.search + const multi = this.helpers.multi + const suffix = this.helpers.suffix + const prefix = this.helpers.prefix + // resize helpers + if (focus) { + query(focus).css('width', width) + } + if (multi) { + query(multi).css('width', width - parseInt(styles['margin-left'], 10) - parseInt(styles['margin-right'], 10)) + } + if (suffix) { + this.addSuffix() + } + if (prefix) { + this.addPrefix() + } + // enum or file + const div = this.helpers.multi + if (['enum', 'file'].includes(this.type) && div) { + // adjust height + query(this.el).css('height', '') + let cntHeight = query(div).find(':scope div.w2ui-multi-items').get(0).clientHeight + 5 + if (cntHeight < 20) cntHeight = 20 + // max height + if (cntHeight > this.tmp['max-height']) { + cntHeight = this.tmp['max-height'] + } + // min height + if (cntHeight < this.tmp['min-height']) { + cntHeight = this.tmp['min-height'] + } + const inpHeight = w2utils.getSize(this.el, 'height') - 2 + if (inpHeight > cntHeight) cntHeight = inpHeight + query(div).css({ + 'height': cntHeight + 'px', + overflow: (cntHeight == this.tmp['max-height'] ? 'auto' : 'hidden') + }) + query(div).css('height', cntHeight + 'px') + query(this.el).css({ 'height': cntHeight + 'px' }) + } + // remember width + this.tmp.current_width = width + } + reset() { + // restore paddings + if (this.tmp != null) { + query(this.el).css('height', '') + Array('padding-left', 'padding-right', 'background-color', 'border-color').forEach(prop => { + if (this.tmp && this.tmp['old-' + prop] != null) { + query(this.el).css(prop, this.tmp['old-' + prop]) + delete this.tmp['old-' + prop] + } + }) + // remove resize watcher + clearInterval(this.tmp.sizeTimer) + } + // remove events and (data) + query(this.el) + .val(this.clean(query(this.el).val())) + .removeClass('w2field w2ui-input') + .removeData('selected selectedIndex') + .off('.w2field') // remove only events added by w2field + // remove helpers + Object.keys(this.helpers).forEach(key => { + query(this.helpers[key]).remove() + }) + this.helpers = {} + delete this.el._w2field + } + clean(val) { + // issue #499 + if (typeof val === 'number') { + return val + } + const options = this.options + val = String(val).trim() + // clean + if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) { + if (typeof val === 'string') { + if (options.autoFormat) { + if (['money', 'currency'].includes(this.type)) { + val = String(val).replace(options.moneyRE, '') + } + if (this.type === 'percent') { + val = String(val).replace(options.percentRE, '') + } + if (['int', 'float'].includes(this.type)) { + val = String(val).replace(options.numberRE, '') + } + } + // escape group symbol for regex as it could be a ., which is wild card in regex + const esc_gsroupSymbol = options.groupSymbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const esc_decimalSymbol = options.decimalSymbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + val = val.replace(/\s+/g, '') + .replace(new RegExp(esc_gsroupSymbol, 'g'), '') + .replace(new RegExp(esc_decimalSymbol, 'g'), '.') + } + if (val !== '' && w2utils.isFloat(val)) val = Number(val); else val = '' + } + return val + } + format(val) { + const options = this.options + // auto format numbers or money + if (options.autoFormat && val !== '') { + switch (this.type) { + case 'money': + case 'currency': + val = w2utils.formatNumber(val, options.currency.precision, true) + if (val !== '') val = options.currency.prefix + val + options.currency.suffix + break + case 'percent': + val = w2utils.formatNumber(val, options.precision, true) + if (val !== '') val += '%' + break + case 'float': + val = w2utils.formatNumber(val, options.precision, true) + break + case 'int': + val = w2utils.formatNumber(val, 0, true) + break + } + // if default group symbol does not match - replase it + const group = parseInt(1000).toLocaleString(w2utils.settings.locale, { useGrouping: true }).slice(1, 2) + if (group !== this.options.groupSymbol) { + val = val.replaceAll(group, this.options.groupSymbol) + } + } + return val + } + change(event) { + // numeric + if (['int', 'float', 'money', 'currency', 'percent'].indexOf(this.type) !== -1) { + // check max/min + const val = query(this.el).val() + const new_val = this.format(this.clean(query(this.el).val())) + // if was modified + if (val !== '' && val != new_val) { + query(this.el).val(new_val) + // cancel event + event.stopPropagation() + event.preventDefault() + return false + } + } + // color + if (this.type === 'color') { + let color = query(this.el).val() + if (color.substr(0, 3).toLowerCase() !== 'rgb') { + color = '#' + color + const len = query(this.el).val().length + if (len !== 8 && len !== 6 && len !== 3) color = '' + } + const next = query(this.el).get(0).nextElementSibling + query(next).find('div').css('background-color', color) + if (query(this.el).hasClass('has-focus')) { + this.updateOverlay() + } + } + // list, enum + if (['list', 'enum', 'file'].indexOf(this.type) !== -1) { + this.refresh() + } + // date, time + if (['date', 'time', 'datetime'].indexOf(this.type) !== -1) { + // convert linux timestamps + let tmp = parseInt(this.el.value) + if (w2utils.isInt(this.el.value) && tmp > 3000) { + if (this.type === 'time') tmp = w2utils.formatTime(new Date(tmp), this.options.format) + if (this.type === 'date') tmp = w2utils.formatDate(new Date(tmp), this.options.format) + if (this.type === 'datetime') tmp = w2utils.formatDateTime(new Date(tmp), this.options.format) + query(this.el).val(tmp).trigger('input').trigger('change') + } + } + } + click(event) { + // lists + if (['list', 'combo', 'enum'].includes(this.type)) { + if (!query(this.el).hasClass('has-focus')) { + this.focus(event) + } + if (this.type == 'list' || this.type == 'combo') { + // if overlay is already open (and not just opened on focus event) then hide it + if (!this.tmp.openedOnFocus) { + const name = this.el.id + '_menu' + const overlay = w2menu.get(name) + if (overlay?.displayed) { + w2menu.hide(name) + } else { + this.updateOverlay() + } + } + delete this.tmp.openedOnFocus + if (this.type == 'list') { + // since list has separate search input, in order to keep the overlay open, need to stop + event.stopPropagation() + } + } + if (this.type == 'enum') { + this.updateOverlay() + } + } + // other fields with drops + if (['date', 'time', 'datetime', 'color'].includes(this.type)) { + this.updateOverlay() + } + } + focus(event) { + if (this.type == 'list' && document.activeElement == this.el) { + this.helpers.search_focus.focus() + // update overlay if needed + if (event.showMenu !== false && (this.options.openOnFocus !== false && query(this.el).hasClass('has-focus')) && + !this.tmp.overlay?.overlay?.displayed) { + setTimeout(() => { + this.tmp.openedOnFocus = true + this.updateOverlay() + }, 0) // execute at the end of event loop + } + return + } + // color, date, time + if (['color', 'date', 'time', 'datetime'].indexOf(this.type) !== -1) { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + this.updateOverlay() + } + // menu + if (['list', 'combo', 'enum'].indexOf(this.type) !== -1) { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) { + // still add focus + query(this.el).addClass('has-focus') + return + } + // regenerate items + if (typeof this.options._items_fun === 'function') { + this.options.items = w2utils.normMenu.call(this, this.options._items_fun, this.options) + } + if (this.helpers.search) { + const search = this.helpers.search_focus + search.value = '' + search.select() + } + if (this.type == 'enum') { + // file control in particular need to receive focus after file select + const search = query(this.el.previousElementSibling).find('.li-search input').get(0) + if (document.activeElement !== search) { + search.focus() + } + } + this.resize() + // update overlay if needed + if (event.showMenu !== false && (this.options.openOnFocus !== false && query(this.el).hasClass('has-focus')) && + !this.tmp.overlay?.overlay?.displayed) { + setTimeout(() => { + this.tmp.openedOnFocus = true + this.updateOverlay() + }, 0) // execute at the end of event loop + } + } + if (this.type == 'file') { + const prev = query(this.el).get(0).previousElementSibling + query(prev).addClass('has-focus') + } + query(this.el).addClass('has-focus') + } + blur(event) { + const val = query(this.el).val().trim() + query(this.el).removeClass('has-focus') + if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) { + if (val !== '') { + let newVal = val + let error = '' + if (!this.isStrValid(val)) { // validity is also checked in blur + newVal = '' + } else { + const rVal = this.clean(val) + if (this.options.min != null && rVal < this.options.min) { + newVal = this.options.min + error = `Should be >= ${this.options.min}` + } + if (this.options.max != null && rVal > this.options.max) { + newVal = this.options.max + error = `Should be <= ${this.options.max}` + } + } + if (this.options.autoCorrect) { + query(this.el).val(newVal).trigger('input').trigger('change') + if (error) { + w2tooltip.show({ + name: this.el.id + '_error', + anchor: this.el, + html: error + }) + setTimeout(() => { w2tooltip.hide(this.el.id + '_error') }, 3000) + } + } + } + } + // date or time + if (['date', 'time', 'datetime'].includes(this.type) && this.options.autoCorrect) { + if (val !== '') { + const check = this.type == 'date' ? w2utils.isDate + : (this.type == 'time' ? w2utils.isTime : w2utils.isDateTime) + if (!w2date.inRange(this.el.value, this.options) || + !check.bind(w2utils)(this.el.value, this.options.format)) { + // if not in range or wrong value - clear it + query(this.el).val('').trigger('input').trigger('change') + } + } + } + // clear search input + if (this.type === 'enum') { + query(this.helpers.multi).find('input').val('').css('width', '15px') + // don't hide menu on blur, it should be hidden on tab key up instead, or it will not alow select with click + // w2menu.hide(this.el.id + '_menu') + } + if (this.type == 'file') { + const prev = this.el.previousElementSibling + query(prev).removeClass('has-focus') + } + if (this.type === 'list') { + this.el.value = this.selected?.text ?? '' + // don't hide menu on blur, it should be hidden on tab key up instead, or it will not alow select with click + // w2menu.hide(this.el.id + '_menu') + } + } + keyDown(event, extra) { + const options = this.options + const key = event.keyCode || (extra && extra.keyCode) + let cancel = false + let val, inc, daymil, dt, newValue, newDT + // ignore wrong pressed key + if (['int', 'float', 'money', 'currency', 'percent', 'hex', 'bin', 'color', 'alphanumeric'].includes(this.type)) { + if (!event.metaKey && !event.ctrlKey && !event.altKey) { + if (!this.isStrValid(event.key ?? '1', true) && // valid & is not arrows, dot, comma, etc keys + ![9, 8, 13, 27, 37, 38, 39, 40, 46].includes(event.keyCode)) { + event.preventDefault() + if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true + return false + } + } + } + // numeric + if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) { + if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + val = parseFloat(query(this.el).val().replace(options.moneyRE, '')) || 0 + inc = options.step + if (event.ctrlKey || event.metaKey) inc = options.step * 10 + switch (key) { + case 38: // up + if (event.shiftKey) break // no action if shift key is pressed + newValue = (val + inc <= options.max || options.max == null ? Number((val + inc).toFixed(12)) : options.max) + query(this.el).val(newValue).trigger('input').trigger('change') + cancel = true + break + case 40: // down + if (event.shiftKey) break // no action if shift key is pressed + newValue = (val - inc >= options.min || options.min == null ? Number((val - inc).toFixed(12)) : options.min) + query(this.el).val(newValue).trigger('input').trigger('change') + cancel = true + break + } + if (cancel) { + event.preventDefault() + this.moveCaret2end() + } + } + // date/datetime + if (['date', 'datetime'].includes(this.type)) { + if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + const is = (this.type == 'date' ? w2utils.isDate : w2utils.isDateTime).bind(w2utils) + const format = (this.type == 'date' ? w2utils.formatDate : w2utils.formatDateTime).bind(w2utils) + daymil = 24 * 60 * 60 * 1000 + inc = 1 + if (event.ctrlKey || event.metaKey) inc = 10 // by month + dt = is(query(this.el).val(), options.format, true) + if (!dt) { dt = new Date(); daymil = 0 } + switch (key) { + case 38: // up + if (event.shiftKey) break // no action if shift key is pressed + if (inc == 10) { + dt.setMonth(dt.getMonth() + 1) + } else { + dt.setTime(dt.getTime() + daymil) + } + newDT = format(dt.getTime(), options.format) + query(this.el).val(newDT).trigger('input').trigger('change') + cancel = true + break + case 40: // down + if (event.shiftKey) break // no action if shift key is pressed + if (inc == 10) { + dt.setMonth(dt.getMonth() - 1) + } else { + dt.setTime(dt.getTime() - daymil) + } + newDT = format(dt.getTime(), options.format) + query(this.el).val(newDT).trigger('input').trigger('change') + cancel = true + break + } + if (cancel) { + event.preventDefault() + this.moveCaret2end() + this.updateOverlay() + } + } + // time + if (this.type === 'time') { + if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + inc = (event.ctrlKey || event.metaKey ? 60 : 1) + val = query(this.el).val() + let time = w2date.str2min(val) || w2date.str2min((new Date()).getHours() + ':' + ((new Date()).getMinutes() - 1)) + switch (key) { + case 38: // up + if (event.shiftKey) break // no action if shift key is pressed + time += inc + cancel = true + break + case 40: // down + if (event.shiftKey) break // no action if shift key is pressed + time -= inc + cancel = true + break + } + if (cancel) { + event.preventDefault() + query(this.el).val(w2date.min2str(time)).trigger('input').trigger('change') + this.moveCaret2end() + } + } + // list/enum + if (['list', 'enum'].includes(this.type)) { + switch (key) { + case 8: // delete + case 46: // backspace + if (this.type == 'list') { + const search = query(this.helpers.search_focus) + if (search.val() == '') { + const edata = this.trigger('remove', { target: this.el, originalEvent: event, item: this.selected }) + if (edata.isCancelled === true) return + this.selected = null + w2menu.hide(this.el.id + '_menu') + query(this.el).val('').trigger('input').trigger('change') + edata.finish() + } + } else { + const search = query(this.helpers.multi).find('input') + if (search.val() == '') { + const edata = this.trigger('remove', { target: this.el, originalEvent: event, item: this.selected[this.selected.length - 1] }) + if (edata.isCancelled === true) return + w2menu.hide(this.el.id + '_menu') + this.selected.pop() + // update selected array in overlay + const overlay = w2menu.get(this.el.id + '_menu') + if (overlay) overlay.options.selected = this.selected + this.refresh() + edata.finish() + } + } + break + case 9: // tab key + case 16: // shift key (when shift+tab) + w2menu.hide(this.el.id + '_menu') + break + case 27: // escape + w2menu.hide(this.el.id + '_menu') + this.refresh() + break + default: { + // intentionally blank + } + } + } + } + keyUp(event) { + if (this.type == 'list') { + const search = query(this.helpers.search_focus) + if (search.val() !== '') { + query(this.el).attr('placeholder', '') + } else { + query(this.el).attr('placeholder', this.tmp.pholder) + } + if (event.keyCode == 13) { + setTimeout(() => { + search.val('') + w2menu.hide(this.el.id + '_menu') + this.refresh() + }, 1) + } + // if arrows are clicked, it will show overlay + if ([38, 40].includes(event.keyCode) && !this.tmp.overlay?.overlay?.displayed) { + this.updateOverlay() + } + this.refresh() + } + if (this.type == 'combo') { + if (![9, 16, 27].includes(event.keyCode) && this.options.openOnFocus !== true) { + // do not show when receives focus on tab or shift + tab or on esc + this.updateOverlay() + } + // if arrows are clicked, it will show overlay + if ([38, 40].includes(event.keyCode) && !this.tmp.overlay?.overlay?.displayed) { + this.updateOverlay() + } + } + if (this.type == 'enum') { + const search = this.helpers.multi.find('input') + const styles = getComputedStyle(search.get(0)) + const width = w2utils.getStrWidth(search.val(), + `font-family: ${styles['font-family']}; font-size: ${styles['font-size']};`) + search.css({ width: (width + 15) + 'px' }) + this.resize() + // if delete, backspace, tab, shift, escape - hide menu + if ([8, 46, 9, 16, 27].includes(event.keyCode)) { + if (this.tmp.overlay?.overlay?.displayed) { + w2menu.hide(this.el.id + '_menu') + } + } else { + this.updateOverlay() + } + } + } + findItemIndex(items, id, parents) { + let inds = [] + if (!parents) parents = [] + if (['list', 'combo', 'enum'].includes(this.type) && this.options.url) { + // remove source, so get it from overlay + const overlay = w2menu.get(this.el.id + '_menu') + if (overlay) { + items = overlay.options.items + this.options.items = items + } + } + items.forEach((item, ind) => { + if (item.id === id) { + inds = parents.concat([ind]) + this.options.index = [ind] + } + if (inds.length == 0 && item.items && item.items.length > 0) { + parents.push(ind) + inds = this.findItemIndex(item.items, id, parents) + parents.pop() + } + }) + return inds + } + updateOverlay(indexOnly) { + const options = this.options + let params + // color + if (this.type === 'color') { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + w2color.show(w2utils.extend({ + name: this.el.id + '_color', + anchor: this.el, + transparent: options.transparent, + advanced: options.advanced, + color: this.el.value, + liveUpdate: true + }, this.options)) + .select(event => { + const color = event.detail.color + query(this.el).val(color).trigger('input').trigger('change') + }) + .liveUpdate(event => { + const color = event.detail.color + query(this.helpers.suffix).find(':scope > div').css('background-color', '#' + color) + }) + } + // list + if (['list', 'combo', 'enum'].includes(this.type)) { + let el = this.el + let input = this.el + if (this.type === 'enum') { + el = this.helpers.multi.get(0) + input = query(el).find('input').get(0) + } + if (this.type === 'list') { + const sel = this.selected + if (w2utils.isPlainObject(sel) && Object.keys(sel).length > 0) { + const ind = this.findItemIndex(options.items, sel.id) + if (ind.length > 0) { + options.index = ind + } + } + input = this.helpers.search_focus + } + if (query(this.el).hasClass('has-focus') && !this.el.readOnly && !this.el.disabled) { + params = w2utils.extend({}, options, { + name: this.el.id + '_menu', + anchor: input, + selected: this.selected, + search: false, + render: options.renderDrop, + anchorClass: '', + offsetY: 5, + maxHeight: options.maxDropHeight, // TODO: check + maxWidth: options.maxDropWidth, // TODO: check + minWidth: options.minDropWidth // TODO: check + }) + this.tmp.overlay = w2menu.show(params) + .select(event => { + if (['list', 'combo'].includes(this.type)) { + this.selected = event.detail.item + query(input).val('') + query(this.el).val(this.selected.text).trigger('input').trigger('change') + this.focus({ showMenu: false }) + } else { + const selected = this.selected + const newItem = event.detail?.item + if (newItem) { + // trigger event + const edata = this.trigger('add', { target: this.el, item: newItem, originalEvent: event }) + if (edata.isCancelled === true) return + // default behavior + if (selected.length >= options.max && options.max > 0) selected.pop() + delete newItem.hidden + selected.push(newItem) + query(this.el).trigger('input').trigger('change') + query(this.helpers.multi).find('input').val('') + // updaet selected array in overlays + const overlay = w2menu.get(this.el.id + '_menu') + if (overlay) overlay.options.selected = this.selected + // event after + edata.finish() + } + } + }) + } + } + // date + if (['date', 'time', 'datetime'].includes(this.type)) { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + w2date.show(w2utils.extend({ + name: this.el.id + '_date', + anchor: this.el, + value: this.el.value + }, this.options)) + .select(event => { + const date = event.detail.date + if (date != null) { + query(this.el).val(date).trigger('input').trigger('change') + } + }) + } + } + /* + * INTERNAL FUNCTIONS + */ + isStrValid(ch, loose) { + let isValid = true + switch (this.type) { + case 'int': + if (loose && ['-', this.options.groupSymbol].includes(ch)) { + isValid = true + } else { + isValid = w2utils.isInt(ch.replace(this.options.numberRE, '')) + } + break + case 'percent': + ch = ch.replace(/%/g, '') + case 'float': + if (loose && ['-', '', this.options.decimalSymbol, this.options.groupSymbol].includes(ch)) { + isValid = true + } else { + isValid = w2utils.isFloat(ch.replace(this.options.numberRE, '')) + } + break + case 'money': + case 'currency': + if (loose && ['-', this.options.decimalSymbol, this.options.groupSymbol, this.options.currency.prefix, + this.options.currency.suffix].includes(ch)) { + isValid = true + } else { + isValid = w2utils.isFloat(ch.replace(this.options.moneyRE, '')) + } + break + case 'bin': + isValid = w2utils.isBin(ch) + break + case 'color': + case 'hex': + isValid = w2utils.isHex(ch) + break + case 'alphanumeric': + isValid = w2utils.isAlphaNumeric(ch) + break + } + return isValid + } + addPrefix() { + if (!this.options.prefix) { + return + } + let helper + const styles = getComputedStyle(this.el) + if (this.tmp['old-padding-left'] == null) { + this.tmp['old-padding-left'] = styles['padding-left'] + } + // remove if already displayed + if (this.helpers.prefix) query(this.helpers.prefix).remove() + query(this.el).before(`
${this.options.prefix}
`) + helper = query(this.el).get(0).previousElementSibling + query(helper) + .css({ + 'color': styles.color, + 'font-family': styles['font-family'], + 'font-size': styles['font-size'], + 'height': this.el.clientHeight + 'px', + 'padding-top': parseInt(styles['padding-top'], 10) + 1 + 'px', + 'padding-bottom': parseInt(styles['padding-bottom'], 10) - 1 + 'px', + 'padding-left': this.tmp['old-padding-left'], + 'padding-right': 0, + 'margin-top': (parseInt(styles['margin-top'], 10)) + 'px', + 'margin-bottom': (parseInt(styles['margin-bottom'], 10)) + 'px', + 'margin-left': styles['margin-left'], + 'margin-right': 0, + 'z-index': 1, + 'display': 'inline-flex', + 'align-items': 'center' + }) + // only if visible + query(this.el).css('padding-left', helper.clientWidth + 'px !important') + // remember helper + this.helpers.prefix = helper + } + addSuffix() { + if (!this.options.suffix && !this.options.arrows) { + return + } + let helper + const self = this + const styles = getComputedStyle(this.el) + if (this.tmp['old-padding-right'] == null) { + this.tmp['old-padding-right'] = styles['padding-right'] + } + let pr = parseInt(styles['padding-right'] || 0) + if (this.options.arrows) { + // remove if already displayed + if (this.helpers.arrows) query(this.helpers.arrows).remove() + // add fresh + query(this.el).after( + '
 ' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
') + helper = query(this.el).get(0).nextElementSibling + query(helper).css({ + 'color': styles.color, + 'font-family': styles['font-family'], + 'font-size': styles['font-size'], + 'height': this.el.clientHeight + 'px', + 'padding': 0, + 'margin-top': (parseInt(styles['margin-top'], 10) + 1) + 'px', + 'margin-bottom': 0, + 'border-left': '1px solid silver', + 'width': '16px', + 'transform': 'translateX(-100%)' + }) + .on('mousedown', function(event) { + if (query(event.target).hasClass('arrow-up')) { + self.keyDown(event, { keyCode: 38 }) + } + if (query(event.target).hasClass('arrow-down')) { + self.keyDown(event, { keyCode: 40 }) + } + }) + pr += helper.clientWidth // width of the control + query(this.el).css('padding-right', pr + 'px !important') + this.helpers.arrows = helper + } + if (this.options.suffix !== '') { + // remove if already displayed + if (this.helpers.suffix) query(this.helpers.suffix).remove() + // add fresh + query(this.el).after(`
${this.options.suffix}
`) + helper = query(this.el).get(0).nextElementSibling + query(helper) + .css({ + 'color': styles.color, + 'font-family': styles['font-family'], + 'font-size': styles['font-size'], + 'height': this.el.clientHeight + 'px', + 'padding-top': styles['padding-top'], + 'padding-bottom': styles['padding-bottom'], + 'padding-left': 0, + 'padding-right': styles['padding-right'], + 'margin-top': (parseInt(styles['margin-top'], 10) + 2) + 'px', + 'margin-bottom': (parseInt(styles['margin-bottom'], 10) + 1) + 'px', + 'transform': 'translateX(-100%)' + }) + query(this.el).css('padding-right', helper.clientWidth + 'px !important') + this.helpers.suffix = helper + } + } + // Only used for list + addSearch() { + if (this.type !== 'list') return + // clean up & init + if (this.helpers.search) query(this.helpers.search).remove() + // remember original tabindex + let tabIndex = parseInt(query(this.el).attr('tabIndex')) + if (!isNaN(tabIndex) && tabIndex !== -1) this.tmp['old-tabIndex'] = tabIndex + if (this.tmp['old-tabIndex']) tabIndex = this.tmp['old-tabIndex'] + if (tabIndex == null || isNaN(tabIndex)) tabIndex = 0 + // if there is id, add to search with "_search" + let searchId = '' + if (query(this.el).attr('id') != null) { + searchId = 'id="' + query(this.el).attr('id') + '_search"' + } + // build helper + const html = ` +
+ + +
` + query(this.el).attr('tabindex', -1).before(html) + const helper = query(this.el).get(0).previousElementSibling + this.helpers.search = helper + this.helpers.search_focus = query(helper).find('input').get(0) + const styles = getComputedStyle(this.el) + query(helper).css({ + width: this.el.clientWidth + 'px', + 'margin-top': styles['margin-top'], + 'margin-left': styles['margin-left'], + 'margin-bottom': styles['margin-bottom'], + 'margin-right': styles['margin-right'] + }) + .find('input') + .css({ + cursor: 'default', + width: '100%', + opacity: 1, + padding: styles.padding, + margin: styles.margin, + border: '1px solid transparent', + 'background-color': 'transparent' + }) + // INPUT events + query(helper).find('input') + .off('.w2ui-helper') + .on('focus.w2ui-helper', event => { + query(event.target).val('') + this.tmp.pholder = query(this.el).attr('placeholder') ?? '' + this.focus(event) + event.stopPropagation() + }) + .on('blur.w2ui-helper', event => { + query(event.target).val('') + if (this.tmp.pholder != null) query(this.el).attr('placeholder', this.tmp.pholder) + this.blur(event) + event.stopPropagation() + }) + .on('keydown.w2ui-helper', event => { this.keyDown(event) }) + .on('keyup.w2ui-helper', event => { this.keyUp(event) }) + // MAIN div + query(helper) + .off('.w2ui-helper') + .on('click.w2ui-helper', event => { + query(helper).find('input').get(0).focus() + }) + } + // Used in enum/file + addMultiSearch() { + if (!['enum', 'file'].includes(this.type)) { + return + } + // clean up & init + query(this.helpers.multi).remove() + // build helper + let html = '' + const styles = getComputedStyle(this.el) + const margin = w2utils.stripSpaces(` + margin-top: 0px; + margin-bottom: 0px; + margin-left: ${styles['margin-left']}; + margin-right: ${styles['margin-right']}; + width: ${(w2utils.getSize(this.el, 'width') - parseInt(styles['margin-left'], 10) - + parseInt(styles['margin-right'], 10))}px; + `) + if (this.tmp['min-height'] == null) { + const min = this.tmp['min-height'] = parseInt((styles['min-height'] != 'none' ? styles['min-height'] : 0) || 0) + const current = parseInt(styles.height) + this.tmp['min-height'] = Math.max(min, current) + } + if (this.tmp['max-height'] == null && styles['max-height'] != 'none') { + this.tmp['max-height'] = parseInt(styles['max-height']) + } + // if there is id, add to search with "_search" + let searchId = '' + if (query(this.el).attr('id') != null) { + searchId = `id="${query(this.el).attr('id')}_search"` + } + // remember original tabindex + let tabIndex = parseInt(query(this.el).attr('tabIndex')) + if (!isNaN(tabIndex) && tabIndex !== -1) this.tmp['old-tabIndex'] = tabIndex + if (this.tmp['old-tabIndex']) tabIndex = this.tmp['old-tabIndex'] + if (tabIndex == null || isNaN(tabIndex)) tabIndex = 0 + if (this.type === 'enum') { + html = ` +
+
+ +
+
` + } + if (this.type === 'file') { + html = ` +
+
+ +
+
+ +
+
` + } + // old bg and border + this.tmp['old-background-color'] = styles['background-color'] + this.tmp['old-border-color'] = styles['border-color'] + query(this.el) + .before(html) + .css({ + 'border-color': 'transparent', + 'background-color': 'transparent' + }) + const div = query(this.el.previousElementSibling) + this.helpers.multi = div + query(this.el).attr('tabindex', -1) + // click anywhere on the field + div.on('mousedown', event => { query(event.target).addClass('has-focus') }) // this is needed so that visually focus is there + .on('mouseup', event => { query(event.target).removeClass('has-focus') }) + .on('click', event => { this.focus(event); this.updateOverlay() }) + // search field (small and growing one) + div.find('input:not(.file-input)') + .on('click', event => { this.click(event) }) + .on('focus', event => { this.focus(event) }) + .on('blur', event => { this.blur(event) }) + .on('keydown', event => { this.keyDown(event) }) + .on('keyup', event => { this.keyUp(event) }) + // file input + if (this.type === 'file') { + div.find('input.file-input') + .off('.drag') + .on('click.drag', (event) => { + event.stopPropagation() + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + this.focus(event) + }) + .on('dragenter.drag', (event) => { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + div.addClass('w2ui-file-dragover') + }) + .on('dragleave.drag', (event) => { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + div.removeClass('w2ui-file-dragover') + }) + .on('drop.drag', (event) => { + if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return + div.removeClass('w2ui-file-dragover') + const files = Array.from(event.dataTransfer.files) + files.forEach(file => { this.addFile(file) }) + this.focus(event) + // cancel to stop browser behaviour + event.preventDefault() + event.stopPropagation() + }) + .on('dragover.drag', (event) => { + // cancel to stop browser behaviour + event.preventDefault() + event.stopPropagation() + }) + .on('change.drag', (event) => { + if (typeof event.target.files !== 'undefined') { + Array.from(event.target.files).forEach(file => { this.addFile(file) }) + } + this.focus(event) + }) + } + this.refresh() + } + addFile(file) { + const options = this.options + const selected = this.selected + const newItem = { + name: file.name, + type: file.type, + modified: file.lastModifiedDate, + size: file.size, + content: null, + file: file + } + let size = 0 + let cnt = 0 + const errors = [] + if (Array.isArray(selected)) { + selected.forEach(item => { + if (item.name == file.name && item.size == file.size) { + errors.push(w2utils.lang('The file "${name}" (${size}) is already added.', { + name: file.name, size: w2utils.formatSize(file.size) })) + } + size += item.size + cnt++ + }) + } + if (options.maxFileSize !== 0 && newItem.size > options.maxFileSize) { + errors.push(w2utils.lang('Maximum file size is ${size}', { size: w2utils.formatSize(options.maxFileSize) })) + } + if (options.maxSize !== 0 && size + newItem.size > options.maxSize) { + errors.push(w2utils.lang('Maximum total size is ${size}', { size: w2utils.formatSize(options.maxSize) })) + } + if (options.max !== 0 && cnt >= options.max) { + errors.push(w2utils.lang('Maximum number of files is ${count}', { count: options.max })) + } + // trigger event + const edata = this.trigger('add', { target: this.el, file: newItem, total: cnt, totalSize: size, errors }) + if (edata.isCancelled === true) return + // if errors + if (errors.length > 0) { + if (options.showErrors) { + w2tooltip.show({ + anchor: this.el, + html: 'Errors: ' + errors.join('
'), + hideOn: ['input', 'doc-click'] + }) + } + console.log('ERRORS (while adding files): ', errors) + return + } + // check params + selected.push(newItem) + // read file as base64 + if (typeof FileReader !== 'undefined' && options.readContent === true) { + const reader = new FileReader() + const self = this + // need a closure + reader.onload = (function onload() { + return function closure(event) { + const fl = event.target.result + const ind = fl.indexOf(',') + newItem.content = fl.substr(ind + 1) + self.refresh() + query(self.el).trigger('input').trigger('change') + // event after + edata.finish() + } + })() + reader.readAsDataURL(file) + } else { + this.refresh() + query(this.el).trigger('input').trigger('change') + edata.finish() + } + } + // move cursror to end + moveCaret2end() { + setTimeout(() => { + this.el.setSelectionRange(this.el.value.length, this.el.value.length) + }, 0) + } +} +export { + w2ui, w2utils, query, w2locale, w2event, w2base, + w2popup, w2alert, w2confirm, w2prompt, Dialog, + w2tooltip, w2menu, w2color, w2date, Tooltip, + w2toolbar, w2sidebar, w2tabs, w2layout, w2grid, w2form, w2field +}