// eslint-disable-next-line no-redeclare // noinspection JSVoidFunctionReturnValueUsed // eslint-disable-next-line no-redeclare let jsPanel = { version: '4.16.1', date: '2022-11-03 09:18', ajaxAlwaysCallbacks: [], autopositionSpacing: 4, closeOnEscape: (() => { document.addEventListener('keydown', e => { if (e.key === 'Escape' || e.code === 'Escape' || e.key === 'Esc') { jsPanel .getPanels(panel => panel.classList.contains('jsPanel')) // Array is sorted by z-index (the highest first) .some(item => { if (item.options.closeOnEscape) { if (typeof item.options.closeOnEscape === 'function') { return item.options.closeOnEscape.call(item, item); // if return value is falsy next panel in sequence will close, otherwise processing of Array.prototype.some() stops } else { item.close(null, true); return true; } } return false; }); } }, false); })(), defaults: { boxShadow: 3, container: 'window', contentSize: { width: '400px', height: '200px' }, // must be an object dragit: { cursor: 'move', handles: '.jsPanel-headerlogo, .jsPanel-titlebar, .jsPanel-ftr', // do not use .jsPanel-headerbar opacity: 0.8, disableOnMaximized: true, }, header: true, headerTitle: 'jsPanel', headerControls: {size: 'md'}, // must be an object iconfont: undefined, maximizedMargin: 0, minimizeTo: 'default', paneltype: 'standard', position: {my: 'center', at: 'center'}, // default position.of MUST NOT be set with new positioning engine resizeit: { handles: 'n, e, s, w, ne, se, sw, nw', minWidth: 128, minHeight: 38, }, theme: 'default' }, defaultAutocloseConfig: {time: '8s', progressbar: true}, defaultSnapConfig: { sensitivity: 70, trigger: 'panel', active: 'both', }, extensions: {}, globalCallbacks: false, icons: { close: ``, maximize: ``, normalize: ``, minimize: ``, smallify: ``, }, idCounter: 0, isIE: (() => {return document.documentMode || false;})(), pointerdown: 'onpointerdown' in window ? ['pointerdown'] : 'ontouchend' in window ? ['touchstart', 'mousedown'] : ['mousedown'], pointermove: 'onpointermove' in window ? ['pointermove'] : 'ontouchend' in window ? ['touchmove', 'mousemove'] : ['mousemove'], pointerup: 'onpointerup' in window ? ['pointerup'] : 'ontouchend' in window ? ['touchend', 'mouseup'] : ['mouseup'], polyfills: (() => { // Polyfills for IE11 only // Object.assign polyfill - https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/assign if (!Object.assign) { Object.defineProperty(Object, 'assign', { enumerable: false, configurable: true, writable: true, value: function (target) { if (target === undefined || target === null) { throw new TypeError('Cannot convert first argument to object'); } let to = Object(target); for (let i = 1; i < arguments.length; i++) { let nextSource = arguments[i]; if (nextSource === undefined || nextSource === null) { continue; } nextSource = Object(nextSource); let keysArray = Object.keys(Object(nextSource)); for (let nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { let nextKey = keysArray[nextIndex]; let desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); if (desc !== undefined && desc.enumerable) { to[nextKey] = nextSource[nextKey]; } } } return to; }, }); } // Object.entries() - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries if (!Object.entries) { Object.entries = function( obj ){ // noinspection ES6ConvertVarToLetConst var ownProps = Object.keys( obj ), i = ownProps.length, resArray = new Array(i); // preallocate the Array while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]]; return resArray; }; } // NodeList.prototype.forEach() polyfill - https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach if (window.NodeList && !NodeList.prototype.forEach) { NodeList.prototype.forEach = function (callback, thisArg) { thisArg = thisArg || window; for (let i = 0; i < this.length; i++) { callback.call(thisArg, this[i], i, this); } }; } // .append() polyfill - https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append (function (arr) { arr.forEach(function (item) { item.append = item.append || function () { let argArr = Array.prototype.slice.call(arguments), docFrag = document.createDocumentFragment(); argArr.forEach(function (argItem) { let isNode = argItem instanceof Node; docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem))); }); this.appendChild(docFrag); }; }); })([Element.prototype, Document.prototype, DocumentFragment.prototype]); // Element.closest() polyfill - https://developer.mozilla.org/en-US/docs/Web/API/Element/closest if (window.Element && !Element.prototype.closest) { // noinspection JSValidateTypes Element.prototype.closest = function (s) { // noinspection JSUnresolvedVariable let matches = (this.document || this.ownerDocument).querySelectorAll(s), i, el = this; do { i = matches.length; // eslint-disable-next-line no-empty while (--i >= 0 && matches.item(i) !== el) {} } while (i < 0 && (el = el.parentElement)); return el; }; } // CustomEvent - https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent (function () { if (typeof window.CustomEvent === 'function') return false; function CustomEvent(event, params) { params = params || { bubbles: false, cancelable: false, detail: undefined, }; let evt = document.createEvent('CustomEvent'); // noinspection JSDeprecatedSymbols evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; } // noinspection JSValidateTypes CustomEvent.prototype = window.Event.prototype; // noinspection JSValidateTypes window.CustomEvent = CustomEvent; })(); // String.prototype.endsWith() - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith if (!String.prototype.endsWith) { String.prototype.endsWith = function(search, this_len) { if (this_len === undefined || this_len > this.length) { this_len = this.length; } return this.substring(this_len - search.length, this_len) === search; }; } // String.prototype.startsWith() - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith if (!String.prototype.startsWith) { Object.defineProperty(String.prototype, 'startsWith', { value: function(search, rawPos) { // noinspection ES6ConvertVarToLetConst var pos = rawPos > 0 ? rawPos|0 : 0; return this.substring(pos, pos + search.length) === search; } }); } // String.prototype.includes() - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes if (!String.prototype.includes) { String.prototype.includes = function(search, start) { 'use strict'; if (search instanceof RegExp) { throw TypeError('first argument must not be a RegExp'); } if (start === undefined) { start = 0; } return this.indexOf(search, start) !== -1; }; } // String.prototype repeat() - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat if (!String.prototype.repeat) { String.prototype.repeat = function(count) { 'use strict'; if (this == null) throw new TypeError('can\'t convert ' + this + ' to object'); // noinspection ES6ConvertVarToLetConst var str = '' + this; count = +count; if (count != count) count = 0; if (count < 0) throw new RangeError('repeat count must be non-negative'); if (count == Infinity) throw new RangeError('repeat count must be less than infinity'); count = Math.floor(count); if (str.length == 0 || count == 0) return ''; if (str.length * count >= 1 << 28) throw new RangeError('repeat count must not overflow maximum string size'); // noinspection ES6ConvertVarToLetConst var maxCount = str.length * count; count = Math.floor(Math.log(count) / Math.log(2)); while (count) { str += str; count--; } str += str.substring(0, maxCount - str.length); return str; }; } // Number.isInteger() - Number.isInteger = Number.isInteger || function (value) { return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; }; // Array.prototype.includes() - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, 'includes', { value: function (searchElement, fromIndex) { if (this == null) { throw new TypeError('"this" is null or not defined'); } // 1. Let O be ? ToObject(this value). let o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). let len = o.length >>> 0; // 3. If len is 0, return false. if (len === 0) { return false; } // 4. Let n be ? ToInteger(fromIndex). // (If fromIndex is undefined, this step produces the value 0.) let n = fromIndex | 0; // 5. If n ≥ 0, then // a. Let k be n. // 6. Else n < 0, // a. Let k be len + n. // b. If k < 0, let k be 0. let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); function sameValueZero(x, y) { return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)); } // 7. Repeat, while k < len while (k < len) { // a. Let elementK be the result of ? Get(O, ! ToString(k)). // b. If SameValueZero(searchElement, elementK) is true, return true. if (sameValueZero(o[k], searchElement)) { return true; } // c. Increase k by 1. k++; } // 8. Return false return false; }, }); } })(), ziBase: 100, colorFilledLight: 0.81, colorFilledDark: 0.08, colorFilled: 0, colorBrightnessThreshold: 0.55, colorNames: { default: 'b0bec5', // Material Design bluegray200 secondary: 'b0bec5', primary: '01579b', // Material Design lightblue900 info: '039be5', // Material Design lightblue600 success: '2e7d32', // Material Design green800 warning: 'f57f17', // Material Design yellow900 danger: 'dd2c00', // Material Design deeporangeA700 light: 'e0e0e0', // Material Design gray300 dark: '263238', // Material Design bluegray900 // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords aliceblue: 'f0f8ff', antiquewhite: 'faebd7', aqua: '00ffff', aquamarine: '7fffd4', azure: 'f0ffff', beige: 'f5f5dc', bisque: 'ffe4c4', black: '000000', blanchedalmond: 'ffebcd', blue: '0000ff', blueviolet: '8a2be2', brown: 'a52a2a', burlywood: 'deb887', cadetblue: '5f9ea0', chartreuse: '7fff00', chocolate: 'd2691e', coral: 'ff7f50', cornflowerblue: '6495ed', cornsilk: 'fff8dc', crimson: 'dc143c', cyan: '00ffff', darkblue: '00008b', darkcyan: '008b8b', darkgoldenrod: 'b8860b', darkgray: 'a9a9a9', darkgrey: 'a9a9a9', darkgreen: '006400', darkkhaki: 'bdb76b', darkmagenta: '8b008b', darkolivegreen: '556b2f', darkorange: 'ff8c00', darkorchid: '9932cc', darkred: '8b0000', darksalmon: 'e9967a', darkseagreen: '8fbc8f', darkslateblue: '483d8b', darkslategray: '2f4f4f', darkslategrey: '2f4f4f', darkturquoise: '00ced1', darkviolet: '9400d3', deeppink: 'ff1493', deepskyblue: '00bfff', dimgray: '696969', dimgrey: '696969', dodgerblue: '1e90ff', firebrick: 'b22222', floralwhite: 'fffaf0', forestgreen: '228b22', fuchsia: 'ff00ff', gainsboro: 'dcdcdc', ghostwhite: 'f8f8ff', gold: 'ffd700', goldenrod: 'daa520', gray: '808080', grey: '808080', green: '008000', greenyellow: 'adff2f', honeydew: 'f0fff0', hotpink: 'ff69b4', indianred: 'cd5c5c', indigo: '4b0082', ivory: 'fffff0', khaki: 'f0e68c', lavender: 'e6e6fa', lavenderblush: 'fff0f5', lawngreen: '7cfc00', lemonchiffon: 'fffacd', lightblue: 'add8e6', lightcoral: 'f08080', lightcyan: 'e0ffff', lightgoldenrodyellow: 'fafad2', lightgray: 'd3d3d3', lightgrey: 'd3d3d3', lightgreen: '90ee90', lightpink: 'ffb6c1', lightsalmon: 'ffa07a', lightseagreen: '20b2aa', lightskyblue: '87cefa', lightslategray: '778899', lightslategrey: '778899', lightsteelblue: 'b0c4de', lightyellow: 'ffffe0', lime: '00ff00', limegreen: '32cd32', linen: 'faf0e6', magenta: 'ff00ff', maroon: '800000', mediumaquamarine: '66cdaa', mediumblue: '0000cd', mediumorchid: 'ba55d3', mediumpurple: '9370d8', mediumseagreen: '3cb371', mediumslateblue: '7b68ee', mediumspringgreen: '00fa9a', mediumturquoise: '48d1cc', mediumvioletred: 'c71585', midnightblue: '191970', mintcream: 'f5fffa', mistyrose: 'ffe4e1', moccasin: 'ffe4b5', navajowhite: 'ffdead', navy: '000080', oldlace: 'fdf5e6', olive: '808000', olivedrab: '6b8e23', orange: 'ffa500', orangered: 'ff4500', orchid: 'da70d6', palegoldenrod: 'eee8aa', palegreen: '98fb98', paleturquoise: 'afeeee', palevioletred: 'd87093', papayawhip: 'ffefd5', peachpuff: 'ffdab9', peru: 'cd853f', pink: 'ffc0cb', plum: 'dda0dd', powderblue: 'b0e0e6', purple: '800080', rebeccapurple: '663399', red: 'ff0000', rosybrown: 'bc8f8f', royalblue: '4169e1', saddlebrown: '8b4513', salmon: 'fa8072', sandybrown: 'f4a460', seagreen: '2e8b57', seashell: 'fff5ee', sienna: 'a0522d', silver: 'c0c0c0', skyblue: '87ceeb', slateblue: '6a5acd', slategray: '708090', slategrey: '708090', snow: 'fffafa', springgreen: '00ff7f', steelblue: '4682b4', tan: 'd2b48c', teal: '008080', thistle: 'd8bfd8', tomato: 'ff6347', turquoise: '40e0d0', violet: 'ee82ee', wheat: 'f5deb3', white: 'ffffff', whitesmoke: 'f5f5f5', yellow: 'ffff00', yellowgreen: '9acd32', /* Material Design Colors https://material.io/design/color/the-color-system.html#tools-for-picking-colors */ grey50: 'fafafa', grey100: 'f5f5f5', grey200: 'eeeeee', grey300: 'e0e0e0', grey400: 'bdbdbd', grey500: '9e9e9e', grey600: '757575', grey700: '616161', grey800: '424242', grey900: '212121', gray50: 'fafafa', gray100: 'f5f5f5', gray200: 'eeeeee', gray300: 'e0e0e0', gray400: 'bdbdbd', gray500: '9e9e9e', gray600: '757575', gray700: '616161', gray800: '424242', gray900: '212121', bluegrey50: 'eceff1', bluegrey100: 'CFD8DC', bluegrey200: 'B0BEC5', bluegrey300: '90A4AE', bluegrey400: '78909C', bluegrey500: '607D8B', bluegrey600: '546E7A', bluegrey700: '455A64', bluegrey800: '37474F', bluegrey900: '263238', bluegray50: 'eceff1', bluegray100: 'CFD8DC', bluegray200: 'B0BEC5', bluegray300: '90A4AE', bluegray400: '78909C', bluegray500: '607D8B', bluegray600: '546E7A', bluegray700: '455A64', bluegray800: '37474F', bluegray900: '263238', red50: 'FFEBEE', red100: 'FFCDD2', red200: 'EF9A9A', red300: 'E57373', red400: 'EF5350', red500: 'F44336', red600: 'E53935', red700: 'D32F2F', red800: 'C62828', red900: 'B71C1C', reda100: 'FF8A80', reda200: 'FF5252', reda400: 'FF1744', reda700: 'D50000', pink50: 'FCE4EC', pink100: 'F8BBD0', pink200: 'F48FB1', pink300: 'F06292', pink400: 'EC407A', pink500: 'E91E63', pink600: 'D81B60', pink700: 'C2185B', pink800: 'AD1457', pink900: '880E4F', pinka100: 'FF80AB', pinka200: 'FF4081', pinka400: 'F50057', pinka700: 'C51162', purple50: 'F3E5F5', purple100: 'E1BEE7', purple200: 'CE93D8', purple300: 'BA68C8', purple400: 'AB47BC', purple500: '9C27B0', purple600: '8E24AA', purple700: '7B1FA2', purple800: '6A1B9A', purple900: '4A148C', purplea100: 'EA80FC', purplea200: 'E040FB', purplea400: 'D500F9', purplea700: 'AA00FF', deeppurple50: 'EDE7F6', deeppurple100: 'D1C4E9', deeppurple200: 'B39DDB', deeppurple300: '9575CD', deeppurple400: '7E57C2', deeppurple500: '673AB7', deeppurple600: '5E35B1', deeppurple700: '512DA8', deeppurple800: '4527A0', deeppurple900: '311B92', deeppurplea100: 'B388FF', deeppurplea200: '7C4DFF', deeppurplea400: '651FFF', deeppurplea700: '6200EA', indigo50: 'E8EAF6', indigo100: 'C5CAE9', indigo200: '9FA8DA', indigo300: '7986CB', indigo400: '5C6BC0', indigo500: '3F51B5', indigo600: '3949AB', indigo700: '303F9F', indigo800: '283593', indigo900: '1A237E', indigoa100: '8C9EFF', indigoa200: '536DFE', indigoa400: '3D5AFE', indigoa700: '304FFE', blue50: 'E3F2FD', blue100: 'BBDEFB', blue200: '90CAF9', blue300: '64B5F6', blue400: '42A5F5', blue500: '2196F3', blue600: '1E88E5', blue700: '1976D2', blue800: '1565C0', blue900: '0D47A1', bluea100: '82B1FF', bluea200: '448AFF', bluea400: '2979FF', bluea700: '2962FF', lightblue50: 'E1F5FE', lightblue100: 'B3E5FC', lightblue200: '81D4FA', lightblue300: '4FC3F7', lightblue400: '29B6F6', lightblue500: '03A9F4', lightblue600: '039BE5', lightblue700: '0288D1', lightblue800: '0277BD', lightblue900: '01579B', lightbluea100: '80D8FF', lightbluea200: '40C4FF', lightbluea400: '00B0FF', lightbluea700: '0091EA', cyan50: 'E0F7FA', cyan100: 'B2EBF2', cyan200: '80DEEA', cyan300: '4DD0E1', cyan400: '26C6DA', cyan500: '00BCD4', cyan600: '00ACC1', cyan700: '0097A7', cyan800: '00838F', cyan900: '006064', cyana100: '84FFFF', cyana200: '18FFFF', cyana400: '00E5FF', cyana700: '00B8D4', teal50: 'E0F2F1', teal100: 'B2DFDB', teal200: '80CBC4', teal300: '4DB6AC', teal400: '26A69A', teal500: '009688', teal600: '00897B', teal700: '00796B', teal800: '00695C', teal900: '004D40', teala100: 'A7FFEB', teala200: '64FFDA', teala400: '1DE9B6', teala700: '00BFA5', green50: 'E8F5E9', green100: 'C8E6C9', green200: 'A5D6A7', green300: '81C784', green400: '66BB6A', green500: '4CAF50', green600: '43A047', green700: '388E3C', green800: '2E7D32', green900: '1B5E20', greena100: 'B9F6CA', greena200: '69F0AE', greena400: '00E676', greena700: '00C853', lightgreen50: 'F1F8E9', lightgreen100: 'DCEDC8', lightgreen200: 'C5E1A5', lightgreen300: 'AED581', lightgreen400: '9CCC65', lightgreen500: '8BC34A', lightgreen600: '7CB342', lightgreen700: '689F38', lightgreen800: '558B2F', lightgreen900: '33691E', lightgreena100: 'CCFF90', lightgreena200: 'B2FF59', lightgreena400: '76FF03', lightgreena700: '64DD17', lime50: 'F9FBE7', lime100: 'F0F4C3', lime200: 'E6EE9C', lime300: 'DCE775', lime400: 'D4E157', lime500: 'CDDC39', lime600: 'C0CA33', lime700: 'AFB42B', lime800: '9E9D24', lime900: '827717', limea100: 'F4FF81', limea200: 'EEFF41', limea400: 'C6FF00', limea700: 'AEEA00', yellow50: 'FFFDE7', yellow100: 'FFF9C4', yellow200: 'FFF59D', yellow300: 'FFF176', yellow400: 'FFEE58', yellow500: 'FFEB3B', yellow600: 'FDD835', yellow700: 'FBC02D', yellow800: 'F9A825', yellow900: 'F57F17', yellowa100: 'FFFF8D', yellowa200: 'FFFF00', yellowa400: 'FFEA00', yellowa700: 'FFD600', amber50: 'FFF8E1', amber100: 'FFECB3', amber200: 'FFE082', amber300: 'FFD54F', amber400: 'FFCA28', amber500: 'FFC107', amber600: 'FFB300', amber700: 'FFA000', amber800: 'FF8F00', amber900: 'FF6F00', ambera100: 'FFE57F', ambera200: 'FFD740', ambera400: 'FFC400', ambera700: 'FFAB00', orange50: 'FFF3E0', orange100: 'FFE0B2', orange200: 'FFCC80', orange300: 'FFB74D', orange400: 'FFA726', orange500: 'FF9800', orange600: 'FB8C00', orange700: 'F57C00', orange800: 'EF6C00', orange900: 'E65100', orangea100: 'FFD180', orangea200: 'FFAB40', orangea400: 'FF9100', orangea700: 'FF6D00', deeporange50: 'FBE9E7', deeporange100: 'FFCCBC', deeporange200: 'FFAB91', deeporange300: 'FF8A65', deeporange400: 'FF7043', deeporange500: 'FF5722', deeporange600: 'F4511E', deeporange700: 'E64A19', deeporange800: 'D84315', deeporange900: 'BF360C', deeporangea100: 'FF9E80', deeporangea200: 'FF6E40', deeporangea400: 'FF3D00', deeporangea700: 'DD2C00', brown50: 'EFEBE9', brown100: 'D7CCC8', brown200: 'BCAAA4', brown300: 'A1887F', brown400: '8D6E63', brown500: '795548', brown600: '6D4C41', brown700: '5D4037', brown800: '4E342E', brown900: '3E2723', /* Material Design for Bootstrap v4 themes https://mdbootstrap.com/docs/b4/jquery/css/colors/#mdb-colors */ 'mdb-default': '2BBBAD', 'mdb-default-dark': '00695c', 'mdb-primary': '4285F4', 'mdb-primary-dark': '0d47a1', 'mdb-secondary': 'aa66cc', 'mdb-secondary-dark': '9933CC', 'mdb-danger': 'ff4444', 'mdb-danger-dark': 'CC0000', 'mdb-warning': 'ffbb33', 'mdb-warning-dark': 'FF8800', 'mdb-success': '00C851', 'mdb-success-dark': '007E33', 'mdb-info': '33b5e5', 'mdb-info-dark': '0099CC', 'mdb-elegant': '2E2E2E', 'mdb-elegant-dark': '212121', 'mdb-stylish': '4B515D', 'mdb-stylish-dark': '3E4551', 'mdb-unique': '3F729B', 'mdb-unique-dark': '1C2331', 'mdb-special': '37474F', 'mdb-special-dark': '263238' }, errorReporting: 1, modifier: false, helper: (() => { document.addEventListener('keydown', e => jsPanel.modifier = e); document.addEventListener('keyup', () => jsPanel.modifier = false); })(), usePointerEvents(use = true) { if (!use) { this.pointerdown = 'ontouchend' in window ? ['touchstart', 'mousedown'] : ['mousedown']; this.pointermove = 'ontouchend' in window ? ['touchmove', 'mousemove'] : ['mousemove']; this.pointerup = 'ontouchend' in window ? ['touchend', 'mouseup'] : ['mouseup']; } else { this.pointerdown = 'onpointerdown' in window ? ['pointerdown'] : 'ontouchend' in window ? ['touchstart', 'mousedown'] : ['mousedown']; this.pointermove = 'onpointermove' in window ? ['pointermove'] : 'ontouchend' in window ? ['touchmove', 'mousemove'] : ['mousemove']; this.pointerup = 'onpointerup' in window ? ['pointerup'] : 'ontouchend' in window ? ['touchend', 'mouseup'] : ['mouseup']; } }, // normalize some values passed via panel's options object pOcontainer(container) { if (container === 'window') { return document.body; } else if (typeof container === 'string') { let list = document.querySelectorAll(container); // a returned list is a NodeList return list.length && list.length > 0 ? list : false; } else if (container.nodeType === 1) { return container; } else if (container.length) { return container[0]; } return false; }, pOcontainment(arg) { let value = arg; if (typeof arg === 'function') { value = arg(); } if (typeof value === 'number') { // value: 20 => value: [20, 20, 20, 20] return [value, value, value, value]; } else if (Array.isArray(value)) { if (value.length === 1) { // value: [20] => value: [20, 20, 20, 20] return [value[0], value[0], value[0], value[0]]; } else if (value.length === 2) { // value: [20, 40] => value: [20, 40, 20, 40] return value.concat(value); } else if (value.length === 3) { value[3] = value[1]; } } return value; // assumed to be an array with 4 values }, pOsize(panel, size) { let values = size || this.defaults.contentSize; const parent = panel.parentElement; if (typeof values === 'string') { const nums = values.trim().split(' '); values = {}; values.width = nums[0]; nums.length === 2 ? (values.height = nums[1]) : (values.height = nums[0]); } else { if (values.width && !values.height) { // noinspection JSSuspiciousNameCombination values.height = values.width; } else if (values.height && !values.width) { // noinspection JSSuspiciousNameCombination values.width = values.height; } } if (String(values.width).match(/^[\d.]+$/gi)) { // if number only values.width += 'px'; } else if (typeof values.width === 'string' && values.width.endsWith('%')) { if (parent === document.body) { values.width = window.innerWidth * (parseFloat(values.width) / 100) + 'px'; } else { const prtStyles = window.getComputedStyle(parent), border = parseFloat(prtStyles.borderLeftWidth) + parseFloat(prtStyles.borderRightWidth); values.width = (parseFloat(prtStyles.width) - border) * (parseFloat(values.width) / 100) + 'px'; } } else if (typeof values.width === 'function') { values.width = values.width.call(panel, panel); if (typeof values.width === 'number') { values.width += 'px'; } else if (typeof values.width === 'string' && values.width.match(/^[\d.]+$/gi)) { values.width += 'px'; } } if (String(values.height).match(/^[\d.]+$/gi)) { // if number only values.height += 'px'; } else if (typeof values.height === 'string' && values.height.endsWith('%')) { if (parent === document.body) { values.height = window.innerHeight * (parseFloat(values.height) / 100) + 'px'; } else { const prtStyles = window.getComputedStyle(parent), border = parseFloat(prtStyles.borderTopWidth) + parseFloat(prtStyles.borderBottomWidth); values.height = (parseFloat(prtStyles.height) - border) * (parseFloat(values.height) / 100) + 'px'; } } else if (typeof values.height === 'function') { values.height = values.height.call(panel, panel); if (typeof values.height === 'number') { values.height += 'px'; } else if (typeof values.height === 'string' && values.height.match(/^[\d.]+$/gi)) { values.height += 'px'; } } return values; // return value must be an object {width: xxx, height: xxx} }, pOborder(str) { let result = []; // remove all unnecessary whitepsace let border = str.trim() .replace(/\s*\(\s*/g, '(') // remove whitespace around opening brackets .replace(/\s*\)/g, ')') // remove whitespace around closing brackets .replace(/\s+/g, ' ') // replace all other whitespace(s) with a single whitespace .split(' '); // replace css custom props/variables with values border.forEach((val, index) => { if(val.startsWith('--') || val.startsWith('var')) { border[index] = jsPanel.getCssVariableValue(val); } }); // border is now an array like ['5px', 'solid', 'red'] // check values for type (border width, style or color) and add to result array border.forEach(val => { if (jsPanel.colorNames[val]) { result[2] = '#' + jsPanel.colorNames[val]; } else if (val.match(/(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)/)) { result[1] = val; } else if (val.match(/(thin|medium|thick)|(\d*\.?\d*(cap|ch|em|ex|ic|lh|rem|rlh|vh|vw|vmax|vmin|vb|vi|px|cm|mm|Q|in|pc|pt))/)) { result[0] = val; } else { result[2] = val; } }); // set default values if needed if (!result[0]) {result[0] = 'medium';} if (!result[1]) {result[1] = 'solid';} if (!result[2]) {result[2] = '';} return result; }, pOheaderControls(oHdrCtrls) { if (typeof oHdrCtrls === 'string') { let setting = {}, str = oHdrCtrls.toLowerCase(), sizeMatch = str.match(/xl|lg|md|sm|xs/), ctrlMatch = str.match(/closeonly|none/); if (sizeMatch) { setting.size = sizeMatch[0]; } if (ctrlMatch) { setting = Object.assign({}, setting, { maximize: 'remove', normalize: 'remove', minimize: 'remove', smallify: 'remove', }); if (ctrlMatch[0] === 'none') { setting.close = 'remove'; } } return Object.assign({}, this.defaults.headerControls, setting); } else { return Object.assign({}, this.defaults.headerControls, oHdrCtrls); } }, pOtheme(optionTheme) { optionTheme = optionTheme.trim(); let color, filling = ''; if (optionTheme.match(/^(rgb|hsl|var)/)) { // for themes starting with rgb , hsl or var let devide = optionTheme.indexOf(')'); color = optionTheme.slice(0, devide + 1).replace(/\s+/g, ''); if (color.startsWith('var')) { color = jsPanel.getCssVariableValue(color); } filling = optionTheme.slice(devide + 1, optionTheme.length).trim(); } else if (optionTheme.match(/^(#|\w|--)/)) { // for themes starting with #, [A-Za-z0-9_] or -- let devide = optionTheme.indexOf(' '); if (devide > 0) { color = optionTheme.slice(0,devide + 1).replace(/\s+/g, ''); filling = optionTheme.slice(devide + 1, optionTheme.length).trim(); } else { color = optionTheme; } if (color.startsWith('--')) { color = jsPanel.getCssVariableValue(color); } } if (color.match(/^([\da-f]{3}|[\da-f]{6})$/gi)) { color = '#' + color; } if (filling.startsWith('fillcolor')) { let devide = filling.indexOf(' '); filling = filling.slice(devide + 1, filling.length).trim().replace(/\s+/g, ''); if (filling.match(/^([\da-f]{3}|[\da-f]{6})$/gi)) { filling = '#' + filling; } else if (jsPanel.colorNames[filling]) { filling = '#' + jsPanel.colorNames[filling]; } else if (filling.match(/^(--|var)/)) { filling = jsPanel.getCssVariableValue(filling); } else { filling = '#fff'; } } return {color: color, colors: false, filling: filling}; }, // color methods --------------- color(val) { let color = val.toLowerCase(), r, g, b, h, s, l, match, channels, hsl, result = {}; const hexPattern = /^#?([\da-f]{3}|[\da-f]{6})$/gi, // matches "#123" or "#f05a78" with or without "#" RGBAPattern = /^rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3}),?(0|1|0\.\d{1,2}|\.\d{1,2})?\)$/gi, // matches rgb/rgba color values, whitespace allowed HSLAPattern = /^hsla?\((\d{1,3}),(\d{1,3}%),(\d{1,3}%),?(0|1|0\.\d{1,2}|\.\d{1,2})?\)$/gi, namedColors = this.colorNames; // change named color to corresponding hex value if (namedColors[color]) { color = namedColors[color]; } // check val for hex color if (color.match(hexPattern) !== null) { // '#' remove color = color.replace('#', ''); // color has either 3 or 6 characters if (color.length % 2 === 1) { // color has 3 char -> convert to 6 char r = color.slice(0,1).repeat(2); g = color.slice(1,2).repeat(2); b = color.slice(2,3).repeat(2); result.rgb = { r: parseInt(r, 16), g: parseInt(g, 16), b: parseInt(b, 16), }; result.hex = `#${r}${g}${b}`; } else { // color has 6 char result.rgb = { r: parseInt(color.slice(0, 2), 16), g: parseInt(color.slice(2, 4), 16), b: parseInt(color.slice(4, 6), 16) }; result.hex = `#${color}`; } hsl = this.rgbToHsl(result.rgb.r, result.rgb.g, result.rgb.b); result.hsl = hsl; result.rgb.css = `rgb(${result.rgb.r},${result.rgb.g},${result.rgb.b})`; } // check val for rgb/rgba color else if (color.match(RGBAPattern)) { match = RGBAPattern.exec(color); result.rgb = { css: color, r: match[1], g: match[2], b: match[3] }; result.hex = this.rgbToHex(match[1], match[2], match[3]); hsl = this.rgbToHsl(match[1], match[2], match[3]); result.hsl = hsl; } // check val for hsl/hsla color else if (color.match(HSLAPattern)) { match = HSLAPattern.exec(color); h = match[1] / 360; s = match[2].slice(0, match[2].length - 1) / 100; l = match[3].slice(0, match[3].length - 1) / 100; channels = this.hslToRgb(h, s, l); result.rgb = { css: `rgb(${channels[0]},${channels[1]},${channels[2]})`, r: channels[0], g: channels[1], b: channels[2], }; result.hex = this.rgbToHex(result.rgb.r, result.rgb.g, result.rgb.b); result.hsl = { css: `hsl(${match[1]},${match[2]},${match[3]})`, h: match[1], s: match[2], l: match[3], }; } // or return #f5f5f5 else { result.hex = '#f5f5f5'; result.rgb = { css: 'rgb(245,245,245)', r: 245, g: 245, b: 245 }; result.hsl = { css: 'hsl(0,0%,96%)', h: 0, s: '0%', l: '96%' }; } return result; }, calcColors(primaryColor) { const threshold = this.colorBrightnessThreshold, primeColor = this.color(primaryColor), filledlightColor = this.lighten(primaryColor, this.colorFilledLight), filledColor = this.darken(primaryColor, this.colorFilled), fontColorForPrimary = this.perceivedBrightness(primaryColor) <= threshold ? '#ffffff' : '#000000', fontColorFilledlight = this.perceivedBrightness(filledlightColor) <= threshold ? '#ffffff' : '#000000', fontColorFilled = this.perceivedBrightness(filledColor) <= threshold ? '#ffffff' : '#000000', filleddarkColor = this.lighten(primaryColor, this.colorFilledDark), fontColorFilleddark = this.perceivedBrightness(filleddarkColor) <= threshold ? '#ffffff' : '#000000'; return [ primeColor.hsl.css, filledlightColor, filledColor, fontColorForPrimary, fontColorFilledlight, fontColorFilled, filleddarkColor, fontColorFilleddark, ]; }, darken(val, amount) { // amount is value between 0 and 1 const hsl = this.color(val).hsl, l = parseFloat(hsl.l), lnew = Math.round(l - l * amount) + '%'; return `hsl(${hsl.h},${hsl.s},${lnew})`; }, lighten(val, amount) { // amount is value between 0 and 1 const hsl = this.color(val).hsl, l = parseFloat(hsl.l), lnew = Math.round(l + (100 - l) * amount) + '%'; return `hsl(${hsl.h},${hsl.s},${lnew})`; }, hslToRgb(h, s, l) { // h, s and l must be values between 0 and 1 let r, g, b; if (s === 0) { r = g = b = l; // achromatic } else { let hue2rgb = (p, q, t) => { if (t < 0) { t += 1; } if (t > 1) { t -= 1; } if (t < 1 / 6) { return p + (q - p) * 6 * t; } if (t < 1 / 2) { return q; } if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6; } return p; }; let q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; }, rgbToHsl(r, g, b) { r /= 255; g /= 255; b /= 255; let max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } //return [ h, s, l ]; h = Math.round(h * 360); s = Math.round(s * 100) + '%'; l = Math.round(l * 100) + '%'; return { css: 'hsl(' + h + ',' + s + ',' + l + ')', h: h, s: s, l: l }; }, rgbToHex(r, g, b) { let red = Number(r).toString(16), green = Number(g).toString(16), blue = Number(b).toString(16); if (red.length === 1) { red = `0${red}`; } if (green.length === 1) { green = `0${green}`; } if (blue.length === 1) { blue = `0${blue}`; } return `#${red}${green}${blue}`; }, perceivedBrightness(val) { const rgb = this.color(val).rgb; // return value is in the range 0 - 1 and input rgb values must also be in the range 0 - 1 // https://www.w3.org/TR/WCAG20-TECHS/G18.html return (rgb.r / 255) * 0.2126 + (rgb.g / 255) * 0.7152 + (rgb.b / 255) * 0.0722; }, // positioning methods --------------- pOposition(positionshorthand) { let result = {}; // remove leading and trailing whitespace and split position shorthand string into array let pos = positionshorthand.trim().split(/\s+/); // find autoposition value and assign to result, must be the first item to find and remove let auto = pos.filter(item => item.match(/^(down|right|up|left)$/i)); if (auto.length) { result.autoposition = auto[0]; pos.splice(pos.indexOf(auto[0]), 1); } // find my and at values and assign to result let my_at = pos.filter(item => item.match(/^(left-|right-)(top|center|bottom)$|(^center-)(top|bottom)$|(^center$)/i)); if (my_at.length) { result.my = my_at[0]; result.at = my_at[1] || my_at[0]; pos.splice(pos.indexOf(my_at[0]), 1); if (my_at[1]) { pos.splice(pos.indexOf(my_at[1]), 1); } } else { result.my = 'center'; result.at = 'center'; } // find offset and assign to result let offsets = pos.filter(item => item.match(/^[+-]?\d*\.?\d+[a-z%]*$/i)); if (offsets.length) { result.offsetX = offsets[0].match(/^[+-]?\d*\.?\d+$/i) ? `${offsets[0]}px` : offsets[0]; if (offsets[1]) { result.offsetY = offsets[1].match(/^[+-]?\d*\.?\d+$/i) ? `${offsets[1]}px` : offsets[1]; } else { // noinspection JSSuspiciousNameCombination result.offsetY = result.offsetX; } pos.splice(pos.indexOf(offsets[0]), 1); if (offsets[1]) { pos.splice(pos.indexOf(offsets[1]), 1); } } // last to find and assign is of value and must be all the rest (if there is a rest) if (pos.length) { result.of = pos.join(' '); } return result; }, position(panel, position) { // @panel: selector string | Element | jQuery object // - usually the jsPanel to position // @position: object // - positioning configuration // - if panel config uses a position shorthand string it must be converted to object before it's passed to this function // if @position is not set return panel if (!position) { panel.style.opacity = 1; return panel; } // merge position defaults with @position if (typeof position === 'string') { position = Object.assign({}, this.defaults.position, this.pOposition(position)); } else { position = Object.assign({}, this.defaults.position, position); } // process parameter functions for 'my', 'at', 'of' // 'offsetX', 'offsetY', 'minLeft', 'maxLeft', 'minTop', 'maxTop' are processed when params are applied ['my', 'at', 'of'].forEach(item => { if (typeof position[item] === 'function') { position[item] = position[item].call(panel, panel); } }); // panel uses option.container: 'window' position is always fixed if (panel.options.container === 'window') { panel.style.position = 'fixed'; } // normalize param @panel to ensure it's an Element object if (typeof panel === 'string') { panel = document.querySelector(panel); } else if (Object.getPrototypeOf(panel).jquery) { panel = panel[0]; } // else panel is assumed to be element object // container is either 'window' or the panel's parent element const container = panel.options.container === 'window' ? 'window' : panel.parentElement; // get base values in order to calculate position deltas // since getBoundingClientRect() calculates values relative to the viewport the parentElement of panel/elmtToPositionAgainst is irrelevant const panelRect = panel.getBoundingClientRect(), containerDomRect = panel.parentElement.getBoundingClientRect(), containerRect = container === 'window' ? { left: 0, top: 0, width: document.documentElement.clientWidth, height: window.innerHeight, } // fake window.getBoundingClientRect() return value : //: panel.parentElement.getBoundingClientRect(); // using 'container' instead of 'panel.parentElement' produces an error // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes // due to the infos from above link IE and EDGE (old version not based on Chromium) report an error in strict mode -> line of code above replaced with line below { width: containerDomRect.width, height: containerDomRect.height, left: containerDomRect.left, top: containerDomRect.top, }; // calculate scale factors, needed for correct positioning if container is scaled - transform: scale() // window is never scaled --> scale factors default to 1 const scaleFactor = container === 'window' ? {x: 1, y: 1} : { x: containerRect.width / container.offsetWidth, y: containerRect.height / container.offsetHeight, }; // get and apply border width values of container - needed for positioning corrections due to positioning with %-values const containerStyle = container === 'window' ? { borderTopWidth: '0px', borderRightWidth: '0px', borderBottomWidth: '0px', borderLeftWidth: '0px', } // fake getComputedStyle(window) return value : window.getComputedStyle(container); containerRect.width -= (parseFloat(containerStyle.borderLeftWidth) + parseFloat(containerStyle.borderRightWidth)) * scaleFactor.x; containerRect.height -= (parseFloat(containerStyle.borderTopWidth) + parseFloat(containerStyle.borderBottomWidth)) * scaleFactor.y; // calculate @position.of rect if @position.of is set let positionOfRect; if (!position.of) { positionOfRect = containerRect; } else { if (typeof position.of === 'string') { positionOfRect = position.of === 'window' ? { borderTopWidth: '0px', borderRightWidth: '0px', borderBottomWidth: '0px', borderLeftWidth: '0px', } // fake getComputedStyle(window) return value : document.querySelector(position.of).getBoundingClientRect(); } else if (Object.getPrototypeOf(position.of).jquery) { positionOfRect = position.of[0].getBoundingClientRect(); } else { positionOfRect = position.of.getBoundingClientRect(); } } // check for scrollbar width values let scrollbarwidthsWindow = this.getScrollbarWidth(document.body), scrollbarwidthsContainer = this.getScrollbarWidth(panel.parentElement); // calc css left for @panel in regard of @position.my and @position.at let left = '0px'; if (position.my.startsWith('left-')) { if (position.at.startsWith('left-')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) + 'px'; } else { left = '0px'; } } else if (position.at.startsWith('center')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) + positionOfRect.width / 2 + 'px'; } else { left = containerRect.width / 2 + 'px'; } } else if (position.at.startsWith('right-')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) + positionOfRect.width + 'px'; } else { left = containerRect.width + 'px'; } } } else if (position.my.startsWith('center')) { if (position.at.startsWith('left-')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) - panelRect.width / 2 + 'px'; } else { left = -panelRect.width / 2 + 'px'; } } else if (position.at.startsWith('center')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) - (panelRect.width - positionOfRect.width) / 2 + 'px'; } else { left = containerRect.width / 2 - panelRect.width / 2 + 'px'; } } else if (position.at.startsWith('right-')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) + (positionOfRect.width - panelRect.width / 2) + 'px'; } else { left = containerRect.width - panelRect.width / 2 + 'px'; } } } else if (position.my.startsWith('right-')) { if (position.at.startsWith('left-')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) - panelRect.width + 'px'; } else { left = -panelRect.width + 'px'; } } else if (position.at.startsWith('center')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) - panelRect.width + positionOfRect.width / 2 + 'px'; } else { left = containerRect.width / 2 - panelRect.width + 'px'; } } else if (position.at.startsWith('right-')) { if (position.of) { left = positionOfRect.left - containerRect.left - parseFloat(containerStyle.borderLeftWidth) + positionOfRect.width - panelRect.width + 'px'; } else { left = containerRect.width - panelRect.width + 'px'; } // correction for vertical scrollbar only needed for panels using my: 'right-*' together with at: 'right-*' if (container !== 'window') { left = parseFloat(left) - scrollbarwidthsContainer.y + 'px'; } } } // calc css top for @panel in regard of @position.my and @position.at let top = '0px'; if (position.my.endsWith('-top')) { if (position.at.endsWith('-top')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) + 'px'; } else { top = '0px'; } } else if (position.at.endsWith('center')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) + positionOfRect.height / 2 + 'px'; } else { top = containerRect.height / 2 + 'px'; } } else if (position.at.endsWith('-bottom')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) + positionOfRect.height + 'px'; } else { top = containerRect.height + 'px'; } } } else if (position.my.endsWith('center')) { if (position.at.endsWith('-top')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) - panelRect.height / 2 + 'px'; } else { top = -panelRect.height / 2 + 'px'; } } else if (position.at.endsWith('center')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) - panelRect.height / 2 + positionOfRect.height / 2 + 'px'; } else { top = containerRect.height / 2 - panelRect.height / 2 + 'px'; } } else if (position.at.endsWith('-bottom')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) - panelRect.height / 2 + positionOfRect.height + 'px'; } else { top = containerRect.height - panelRect.height / 2 + 'px'; } } } else if (position.my.endsWith('-bottom')) { if (position.at.endsWith('-top')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) - panelRect.height + 'px'; } else { top = -panelRect.height + 'px'; } } else if (position.at.endsWith('center')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) - panelRect.height + positionOfRect.height / 2 + 'px'; } else { top = containerRect.height / 2 - panelRect.height + 'px'; } } else if (position.at.endsWith('-bottom')) { if (position.of) { top = positionOfRect.top - containerRect.top - parseFloat(containerStyle.borderTopWidth) - panelRect.height + positionOfRect.height + 'px'; } else { top = containerRect.height - panelRect.height + 'px'; } // correction for horizontal scrollbar only needed for panels using my: '*-bottom' together with at: '*-bottom' if (container !== 'window') { top = parseFloat(top) - scrollbarwidthsContainer.x + 'px'; } else { top = parseFloat(top) - scrollbarwidthsWindow.x + 'px'; } } } panel.style.left = scaleFactor.x === 1 ? left : parseFloat(left) / scaleFactor.x + 'px'; panel.style.top = scaleFactor.y === 1 ? top : parseFloat(top) / scaleFactor.y + 'px'; // at this point panels are correctly positioned according to my/at values let panelStyle = getComputedStyle(panel); // eslint-disable-next-line no-unused-vars let pos = { left: panelStyle.left, top: panelStyle.top }; //console.log('pos after applying my/at/of:', pos); // apply autoposition only if ... if ( position.autoposition && position.my === position.at && ['left-top', 'center-top', 'right-top', 'left-bottom', 'center-bottom', 'right-bottom'].indexOf(position.my) >= 0 ) { pos = this.applyPositionAutopos(panel, pos, position); //console.log('let pos after applying autoposition:', pos); } // apply position.offsetX and position.offsetY if (position.offsetX || position.offsetY) { pos = this.applyPositionOffset(panel, pos, position); //console.log('pos after applying offsets:', pos); } // calculate and apply position.minLeft, position.minTop, position.maxLeft and position.maxTop if (position.minLeft || position.minTop || position.maxLeft || position.maxTop) { pos = this.applyPositionMinMax(panel, pos, position); //console.log('pos after applying minLeft, maxLeft, maxTop, minTop:', pos); } // apply position.modify // must be function returning an object with keys left/top, each with valid css length value if (position.modify) { // eslint-disable-next-line no-unused-vars pos = this.applyPositionModify(panel, pos, position); //console.log('pos after applying modify():', pos); } typeof panel.options.opacity === 'number' ? (panel.style.opacity = panel.options.opacity) : (panel.style.opacity = 1); return panel; }, applyPositionAutopos(panel, pos, position) { // add class with position and autoposition direction const newClass = `${position.my}-${position.autoposition.toLowerCase()}`; panel.classList.add(newClass); // get all panels with same class const newClassAll = Array.prototype.slice.call(document.querySelectorAll(`.${newClass}`)), ownIndex = newClassAll.indexOf(panel); // if more than 1 position new panel if (newClassAll.length > 1) { switch (position.autoposition) { case 'down': // collect heights of all elmts to calc new top position newClassAll.forEach((item, index) => { if (index > 0 && index <= ownIndex) { pos.top = parseFloat(pos.top) + newClassAll[--index].getBoundingClientRect().height + jsPanel.autopositionSpacing + 'px'; } }); break; case 'up': newClassAll.forEach((item, index) => { if (index > 0 && index <= ownIndex) { pos.top = parseFloat(pos.top) - newClassAll[--index].getBoundingClientRect().height - jsPanel.autopositionSpacing + 'px'; } }); break; case 'right': // collect widths of all elmts to calc new left position newClassAll.forEach((item, index) => { if (index > 0 && index <= ownIndex) { pos.left = parseFloat(pos.left) + newClassAll[--index].getBoundingClientRect().width + jsPanel.autopositionSpacing + 'px'; } }); break; case 'left': newClassAll.forEach((item, index) => { if (index > 0 && index <= ownIndex) { pos.left = parseFloat(pos.left) - newClassAll[--index].getBoundingClientRect().width - jsPanel.autopositionSpacing + 'px'; } }); break; } panel.style.left = pos.left; panel.style.top = pos.top; } return { left: pos.left, top: pos.top }; }, applyPositionOffset(panel, pos, position) { ['offsetX', 'offsetY'].forEach(offset => { if (position[offset]) { if (typeof position[offset] === 'function') { position[offset] = position[offset].call(pos, pos, position); } if (isNaN(position[offset]) === false) { // if an offset's value type is integer it's interpreted as pixel value position[offset] = `${position[offset]}px`; } // else it's assumed offsets are strings with valid css length values } else { position[offset] = '0px'; } }); panel.style.left = `calc(${panel.style.left} + ${position.offsetX})`; // offsetX panel.style.top = `calc(${panel.style.top} + ${position.offsetY})`; // offsetY const panelStyle = getComputedStyle(panel); return { left: panelStyle.left, top: panelStyle.top }; }, applyPositionMinMax(panel, pos, position) { ['minLeft', 'minTop', 'maxLeft', 'maxTop'].forEach(val => { if (position[val]) { if (typeof position[val] === 'function') { position[val] = position[val].call(pos, pos, position); } if (Number.isInteger(position[val]) || position[val].match(/^\d+$/)) { // if val type is integer it's interpreted as pixel value position[val] = `${position[val]}px`; } // else it's assumed val is string with valid css length value } }); // process minLeft if (position.minLeft) { // apply minLeft value in order to compare with previous left (pos.left) panel.style.left = position.minLeft; // now get computed css left let left = getComputedStyle(panel).left; // returns string with pixel value // now compare current left (minLeft) with pos.left if (parseFloat(left) < parseFloat(pos.left)) { // if minLeft is less than pos.left return to pos.left panel.style.left = pos.left; } else { // if minLeft is greater than pos.left keep minLeft and reset pos.left to new value pos.left = left; } } // process minTop if (position.minTop) { panel.style.top = position.minTop; let top = getComputedStyle(panel).top; if (parseFloat(top) < parseFloat(pos.top)) { panel.style.top = pos.top; } else { pos.top = top; } } // process maxLeft if (position.maxLeft) { panel.style.left = position.maxLeft; let left = getComputedStyle(panel).left; if (parseFloat(left) > parseFloat(pos.left)) { panel.style.left = pos.left; } else { pos.left = left; } } // process maxTop if (position.maxTop) { panel.style.top = position.maxTop; let top = getComputedStyle(panel).top; if (parseFloat(top) > parseFloat(pos.top)) { panel.style.top = pos.top; } else { pos.top = top; } } const panelStyle = getComputedStyle(panel); return { left: panelStyle.left, top: panelStyle.top }; }, applyPositionModify(panel, pos, position) { if (position.modify && typeof position.modify === 'function') { const modifiedPosition = position.modify.call(pos, pos, position); panel.style.left = Number.isInteger(modifiedPosition.left) || modifiedPosition.left.match(/^\d+$/) ? `${modifiedPosition.left}px` : modifiedPosition.left; panel.style.top = Number.isInteger(modifiedPosition.top) || modifiedPosition.top.match(/^\d+$/) ? `${modifiedPosition.top}px` : modifiedPosition.top; } const panelStyle = getComputedStyle(panel); return { left: panelStyle.left, top: panelStyle.top }; }, autopositionRemaining(panel) { let autoPos, parent = panel.options.container; [ 'left-top-down', 'left-top-right', 'center-top-down', 'right-top-down', 'right-top-left', 'left-bottom-up', 'left-bottom-right', 'center-bottom-up', 'right-bottom-up', 'right-bottom-left', ].forEach(item => { if (panel.classList.contains(item)) { autoPos = item; } }); if (autoPos) { const box = parent === 'window' ? document.body : typeof parent === 'string' ? document.querySelector(parent) : parent; box.querySelectorAll(`.${autoPos}`).forEach(item => item.reposition()); } }, // theming methods ------------------------- getThemeDetails(th) { const theme = this.pOtheme(th); if (theme.color.startsWith('bootstrap-')) { // works with bootstrap 3 and 4 let index = theme.color.indexOf('-'), btn = document.createElement('button'); btn.className = 'btn btn' + theme.color.slice(index); document.body.appendChild(btn); theme.color = getComputedStyle(btn).backgroundColor.replace(/\s+/gi, ''); document.body.removeChild(btn); // noinspection JSUnusedAssignment btn = undefined; } theme.colors = this.calcColors(theme.color); return theme; }, clearTheme(panel, cb) { panel.content.classList.remove('jsPanel-content-filled', 'jsPanel-content-filledlight'); panel.header.classList.remove('jsPanel-hdr-light'); panel.header.classList.remove('jsPanel-hdr-dark'); panel.style.backgroundColor = ''; this.setStyles(panel.headertoolbar, { boxShadow: '', width: '', marginLeft: '', borderTopColor: 'transparent', }); this.setStyles(panel.content, { background: '', borderTopColor: 'transparent', }); panel.header.style.background = ''; Array.prototype.slice .call(panel.controlbar.querySelectorAll('.jsPanel-icon')) .concat([panel.headerlogo, panel.headertitle, panel.headertoolbar, panel.content]) .forEach(item => item.style.color = ''); if (cb) { cb.call(panel, panel); } return panel; }, applyColorTheme(panel, themeDetails) { panel.style.backgroundColor = themeDetails.colors[0]; panel.header.style.backgroundColor = themeDetails.colors[0]; panel.header.style.color = themeDetails.colors[3]; ['.jsPanel-headerlogo', '.jsPanel-title', '.jsPanel-hdr-toolbar'].forEach(item => panel.querySelector(item).style.color = themeDetails.colors[3]); panel.querySelectorAll('.jsPanel-controlbar .jsPanel-btn').forEach(item => item.style.color = themeDetails.colors[3]); // apply border to content only themes 'filled' if (typeof panel.options.theme === 'string' && themeDetails.filling === 'filled') { panel.content.style.borderTop = themeDetails.colors[3] === '#000000' ? '1px solid rgba(0,0,0,0.15)' : '1px solid rgba(255,255,255,0.15)'; } if (themeDetails.colors[3] === '#000000') { panel.header.classList.add('jsPanel-hdr-light'); } else { panel.header.classList.add('jsPanel-hdr-dark'); } if (themeDetails.filling) { switch (themeDetails.filling) { case 'filled': this.setStyles(panel.content, { backgroundColor: themeDetails.colors[2], color: themeDetails.colors[3], }); break; case 'filledlight': panel.content.style.backgroundColor = themeDetails.colors[1]; break; case 'filleddark': this.setStyles(panel.content, { backgroundColor: themeDetails.colors[6], color: themeDetails.colors[7], }); break; default: panel.content.style.backgroundColor = themeDetails.filling; panel.content.style.color = this.perceivedBrightness(themeDetails.filling) <= this.colorBrightnessThreshold ? '#fff' : '#000'; } } return panel; }, applyCustomTheme(panel, theme) { let defaults = { bgPanel: '#ffffff', bgContent: '#ffffff', bgFooter: '#f5f5f5', colorHeader: '#000000', colorContent: '#000000', colorFooter: '#000000', border: undefined, borderRadius: undefined }, passedTheme = typeof theme === 'object' ? Object.assign(defaults, theme) : defaults; let bgPanel = passedTheme.bgPanel, bgContent = passedTheme.bgContent, colorHeader = passedTheme.colorHeader, colorContent = passedTheme.colorContent, bgFooter = passedTheme.bgFooter, colorFooter = passedTheme.colorFooter; // set background panel/header if (this.colorNames[bgPanel]) { panel.style.background = '#' + this.colorNames[bgPanel]; } else { panel.style.background = this.getCssVariableValue(bgPanel); // getCssVariableValue(bgPanel) returns bgPanel unchanged if value does not start with either '--' or 'var' } // set font color header if (this.colorNames[colorHeader]) { colorHeader = '#' + this.colorNames[colorHeader]; } ['.jsPanel-headerlogo', '.jsPanel-title', '.jsPanel-hdr-toolbar'].forEach(item => panel.querySelector(item).style.color = this.getCssVariableValue(colorHeader)); panel.querySelectorAll('.jsPanel-controlbar .jsPanel-btn').forEach(item => item.style.color = this.getCssVariableValue(colorHeader)); // set content background if (this.colorNames[bgContent]) { panel.content.style.background = '#' + this.colorNames[bgContent]; } else { panel.content.style.background = this.getCssVariableValue(bgContent); // getCssVariableValue(bgContent) returns bgContent unchanged if value does not start with either '--' or 'var' } // set content font color this.colorNames[colorContent] ? (panel.content.style.color = '#' + this.colorNames[colorContent]) : (panel.content.style.color = this.getCssVariableValue(colorContent)); // getCssVariableValue(val) returns val unchanged if val does not start with either '--' or 'var' // set border-top for header toolbar and add header class const pbPanel = this.perceivedBrightness(colorHeader); if (pbPanel > this.colorBrightnessThreshold) { panel.header.classList.add('jsPanel-hdr-dark'); } else { panel.header.classList.add('jsPanel-hdr-light'); } // set border-top for content const pbContent = this.perceivedBrightness(colorContent); pbContent > this.colorBrightnessThreshold ? (panel.content.style.borderTop = '1px solid rgba(255,255,255,0.15)') : (panel.content.style.borderTop = '1px solid rgba(0,0,0,0.15)'); // set footer background if (this.colorNames[bgFooter]) { panel.footer.style.background = '#' + this.colorNames[bgFooter]; } else { panel.footer.style.background = this.getCssVariableValue(bgFooter); // getCssVariableValue(bgFooter) returns bgFooter unchanged if value does not start with either '--' or 'var' } // set footer font color this.colorNames[colorFooter] ? (panel.footer.style.color = '#' + this.colorNames[colorFooter]) : (panel.footer.style.color = this.getCssVariableValue(colorFooter)); // set panel border (option.border does not work for themes using an object) if (passedTheme.border) { panel.setBorder(passedTheme.border); } if (passedTheme.borderRadius) { panel.options.borderRadius = undefined; panel.setBorderRadius(passedTheme.borderRadius); } return panel; }, // methods dealing with css and html ------- getCssVariableValue(str) { // str may be something like '--my-color' or 'var(--my-color)' // if str does not start with '--' or 'var' str is returned unchanged // str = str.replace(/\s+/g, ''); do not uncomment this line !! if (str.startsWith('--')) { return getComputedStyle(document.documentElement).getPropertyValue(str).replace(/\s+/g, ''); } else if (str.startsWith('var')) { let v = str.slice(str.indexOf('(')+1, str.indexOf(')')); return getComputedStyle(document.documentElement).getPropertyValue(v).replace(/\s+/g, ''); } return str; }, getScrollbarWidth(elmt = document.body) { if (elmt === document.body) { return { y: window.innerWidth - document.documentElement.clientWidth, x: window.innerHeight - document.documentElement.clientHeight, }; } else { let styles = getComputedStyle(elmt); return { y: elmt.offsetWidth - elmt.clientWidth - parseFloat(styles.borderRightWidth) - parseFloat(styles.borderLeftWidth), x: elmt.offsetHeight - elmt.clientHeight - parseFloat(styles.borderBottomWidth) - parseFloat(styles.borderTopWidth), }; } }, remClass(elmt, classnames) { classnames .trim() .split(/\s+/) .forEach(item => elmt.classList.remove(item)); return elmt; }, setClass(elmt, classnames) { classnames .trim() .split(/\s+/) .forEach(item => elmt.classList.add(item)); return elmt; }, setStyles(elmt, stylesobject) { for (const [prop, value] of Object.entries(stylesobject)) { elmt.style[prop] = typeof value === 'string' ? jsPanel.getCssVariableValue(value) : value; } return elmt; }, setStyle(elmt, stylesobject) { return this.setStyles.call(elmt, elmt, stylesobject); }, // alias for setStyles() strToHtml(str) { // TODO: add param to strip script tags from returned DocumentFragment /* str has to be an HTMLString * returns a DocumentFragment - https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment * after inserting executes inline script and script tags */ return document.createRange().createContextualFragment(str); }, toggleClass(elmt, classnames) { // IE11 doesn't support https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/toggle classnames .trim() .split(/\s+/) .forEach(classname => elmt.classList.contains(classname) ? elmt.classList.remove(classname) : elmt.classList.add(classname)); return elmt; }, emptyNode(node) { while (node.firstChild) { node.removeChild(node.firstChild); } return node; }, addScript(path, type = 'application/javascript', cb) { if (!document.querySelector(`script[src="${path}"]`)) { const script = document.createElement('script'); script.src = path; script.type = type; document.head.appendChild(script); if (cb) { script.onload = cb; } } }, ajax(ajaxConfig, panel) { let config, urlParts, xhr = new XMLHttpRequest(); const configDefaults = { method: 'GET', async: true, user: '', pwd: '', done: function () { if (panel) { let res = jsPanel.strToHtml(this.responseText); if (config.urlSelector) { res = res.querySelector(config.urlSelector); } panel.contentRemove(); panel.content.append(res); } }, autoresize: true, autoreposition: true, }; if (panel && typeof ajaxConfig === 'string') { config = Object.assign({}, configDefaults, { url: ajaxConfig, }); } else if (typeof ajaxConfig === 'object' && ajaxConfig.url) { config = Object.assign({}, configDefaults, ajaxConfig); config.url = ajaxConfig.url; // reset timeout to 0, withCredentials & responseType to false if request is synchronous if (config.async === false) { config.timeout = 0; if (config.withCredentials) { config.withCredentials = undefined; } if (config.responseType) { config.responseType = undefined; } } } else { if (this.errorReporting) { let err = 'XMLHttpRequest seems to miss the url parameter!'; jsPanel.errorpanel(err); } return; } // check url for added selector urlParts = config.url.trim().split(/\s+/); config.url = encodeURI(urlParts[0]); if (urlParts.length > 1) { urlParts.shift(); config.urlSelector = urlParts.join(' '); } xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { panel ? config.done.call(xhr, xhr, panel) : config.done.call(xhr, xhr); } else { if (config.fail) { panel ? config.fail.call(xhr, xhr, panel) : config.fail.call(xhr, xhr); } } if (config.always) { panel ? config.always.call(xhr, xhr, panel) : config.always.call(xhr, xhr); } // resize and/or reposition panel if either width or height is set to 'auto' if (panel) { if (config.autoresize || config.autoreposition) { jsPanel.ajaxAutoresizeAutoreposition(panel, config); } } // allows plugins to add callback functions to the ajax always callback if (jsPanel.ajaxAlwaysCallbacks.length) { jsPanel.ajaxAlwaysCallbacks.forEach(item => { panel ? item.call(xhr, xhr, panel) : item.call(xhr, xhr); }); } } }; xhr.open(config.method, config.url, config.async, config.user, config.pwd); xhr.timeout = config.timeout || 0; if (config.withCredentials) { xhr.withCredentials = config.withCredentials; } if (config.responseType) { xhr.responseType = config.responseType; } if (config.beforeSend) { panel ? config.beforeSend.call(xhr, xhr, panel) : config.beforeSend.call(xhr, xhr); } config.data ? xhr.send(config.data) : xhr.send(null); }, fetch(fetchConfig, panel) { let config; const configDefaults = { bodyMethod: 'text', autoresize: true, autoreposition: true, done: function (response, panel) { if (panel) { let res = jsPanel.strToHtml(response); panel.contentRemove(); panel.content.append(res); } }, }; if (panel && typeof fetchConfig === 'string') { config = Object.assign({}, configDefaults, { resource: encodeURI(fetchConfig), }); } else if (typeof fetchConfig === 'object' && fetchConfig.resource) { config = Object.assign({}, configDefaults, fetchConfig); config.resource = encodeURI(fetchConfig.resource); } else { if (this.errorReporting) { let err = 'Fetch Request seems to miss the resource parameter!'; jsPanel.errorpanel(err); } return; } const fetchInit = config.fetchInit || {}; if (config.beforeSend) { panel ? config.beforeSend.call(fetchConfig, fetchConfig, panel) : config.beforeSend.call(fetchConfig, fetchConfig); } fetch(config.resource, fetchInit) .then(response => { if (response.ok) { return response[config.bodyMethod](); } }) .then(response => { panel ? config.done.call(response, response, panel) : config.done.call(response, response); if (panel) { // resize and/or reposition panel if either width or height is set to 'auto' if (config.autoresize || config.autoreposition) { jsPanel.ajaxAutoresizeAutoreposition(panel, config); } } }); }, ajaxAutoresizeAutoreposition(panel, ajaxOrFetchConfig) { const oContentSize = panel.options.contentSize; if (typeof oContentSize === 'string' && oContentSize.match(/auto/i)) { const parts = oContentSize.split(' '), sizes = Object.assign({}, { width: parts[0], height: parts[1] }); if (ajaxOrFetchConfig.autoresize) { panel.resize(sizes); } if (!panel.classList.contains('jsPanel-contextmenu')) { if (ajaxOrFetchConfig.autoreposition) { panel.reposition(); } } } else if (typeof oContentSize === 'object' && (oContentSize.width === 'auto' || oContentSize.height === 'auto')) { const sizes = Object.assign({}, oContentSize); if (ajaxOrFetchConfig.autoresize) { panel.resize(sizes); } if (!panel.classList.contains('jsPanel-contextmenu')) { if (ajaxOrFetchConfig.autoreposition) { panel.reposition(); } } } }, // templates and other elements ---------- createPanelTemplate(dataAttr = true) { const panel = document.createElement('div'); panel.className = 'jsPanel'; panel.style.left = '0'; panel.style.top = '0'; if (dataAttr) { ['close', 'maximize', 'normalize', 'minimize', 'smallify'].forEach(item => { panel.setAttribute(`data-btn${item}`, 'enabled'); }); } panel.innerHTML = `
`; return panel; }, createMinimizedTemplate() { const panel = document.createElement('div'); panel.className = 'jsPanel-replacement'; panel.innerHTML = `
`; return panel; }, createSnapArea(panel, pos, snapsens) { const el = document.createElement('div'), parent = panel.parentElement; el.className = `jsPanel-snap-area jsPanel-snap-area-${pos}`; if (pos === 'lt' || pos === 'rt' || pos === 'rb' || pos === 'lb') { el.style.width = snapsens + 'px'; el.style.height = snapsens + 'px'; } else if (pos === 'ct' || pos === 'cb') { el.style.height = snapsens + 'px'; } else if (pos === 'lc' || pos === 'rc') { el.style.width = snapsens + 'px'; } if (parent !== document.body) { el.style.position = 'absolute'; } if (!document.querySelector(`.jsPanel-snap-area.jsPanel-snap-area-${pos}`)) { panel.parentElement.appendChild(el); } }, removeSnapAreas() { document.querySelectorAll('.jsPanel-snap-area').forEach(el => el.parentElement.removeChild(el)); }, // various helpers ------- extend(obj) { // obj needs to be a plain object (to extend the individual panel, not the global object) if (Object.prototype.toString.call(obj) === '[object Object]') { for (let ext in obj) { if (Object.prototype.hasOwnProperty.call(obj, ext)) { this.extensions[ext] = obj[ext]; } } } }, getPanels(condition = function() {return this.classList.contains('jsPanel-standard');}) { return Array.prototype.slice .call(document.querySelectorAll('.jsPanel')) .filter(value => condition.call(value, value)) .sort((a, b) => b.style.zIndex - a.style.zIndex); }, processCallbacks(panel, arg, someOrEvery = 'some', param, param2) { // if arg != array make it one if (typeof arg === 'function') { arg = [arg]; } // some(): execute callbacks until one is found returning a truthy value // every(): execute callbacks until one is found returning a falsy value // truthy values are: '0' (string with single zero), 'false' (string with text false), [] (empty array), {} (empty object), function(){} ("empty" function) // falsy values are: false, 0, '', "", null, undefined, NaN if (someOrEvery) { return arg[someOrEvery](cb => cb.call(panel, panel, param, param2)); } else { arg.forEach(cb => cb.call(panel, panel, param, param2)); } }, resetZi() { this.zi = ((startValue = jsPanel.ziBase) => { let val = startValue; return { next: () => val++ }; })(); Array.prototype.slice .call(document.querySelectorAll('.jsPanel-standard')) .sort((a, b) => a.style.zIndex - b.style.zIndex) .forEach(panel => panel.style.zIndex = jsPanel.zi.next()); }, errorpanel(e) { this.create({ paneltype: 'error', dragit: false, resizeit: false, theme: { bgPanel: 'white', bgContent: 'white', colorHeader: 'rebeccapurple', colorContent: '#333333', border: '2px solid rebeccapurple', }, borderRadius: '.33rem', headerControls: 'closeonly xs', headerTitle: '⚠ jsPanel Error', contentSize: { width: '50%', height: 'auto' }, position: 'center-top 0 5 down', animateIn: 'jsPanelFadeIn', content: `

${e}

`, }); }, // METHOD CREATING THE PANEL --------------------------------------------- create(options = {}, cb) { // initialize z-index generator if (!jsPanel.zi) { jsPanel.zi = ((startValue = jsPanel.ziBase) => { let val = startValue; return { next: () => val++ }; })(); } if (options.config) { options = Object.assign({}, this.defaults, options.config, options); delete options.config; } else { options = Object.assign({}, this.defaults, options); } if (!options.id) { options.id = `jsPanel-${(jsPanel.idCounter += 1)}`; } else if (typeof options.id === 'function') { options.id = options.id(); } const p = document.getElementById(options.id); if (p !== null) { // if a panel with passed id already exists, front it and return false if (p.classList.contains('jsPanel')) { p.front(); } if (this.errorReporting) { let err = `◀ COULD NOT CREATE NEW JSPANEL ►
An element with the ID ${options.id} already exists in the document.`; jsPanel.errorpanel(err); } return false; } // check whether container is valid -> if not return and throw error let panelContainer = this.pOcontainer(options.container); // panelContainer might be a NodeList, so use only first node in list if (typeof panelContainer === 'object' && panelContainer.length && panelContainer.length > 0) { panelContainer = panelContainer[0]; } if (!panelContainer) { if (this.errorReporting) { let err = '◀ COULD NOT CREATE NEW JSPANEL ►
The container to append the panel to does not exist'; jsPanel.errorpanel(err); } return false; } // normalize on... callbacks // these callbacks must be an array of function(s) in order to be able to dynamically add/remove callbacks (for example in extensions) [ 'onbeforeclose', 'onbeforemaximize', 'onbeforeminimize', 'onbeforenormalize', 'onbeforesmallify', 'onbeforeunsmallify', 'onclosed', 'onfronted', 'onmaximized', 'onminimized', 'onnormalized', 'onsmallified', 'onstatuschange', 'onunsmallified', ].forEach(item => { if (options[item]) { if (typeof options[item] === 'function') { options[item] = [options[item]]; } } else { options[item] = []; } }); const self = options.template || this.createPanelTemplate(); // Create an abort controller and signal to detach dragit/resize events // when the panel is closed const abortController = new AbortController(); const abortSignal = abortController.signal; // Properties self.options = options; self.closetimer = undefined; self.status = 'initialized'; self.currentData = {}; self.header = self.querySelector('.jsPanel-hdr'); // complete header section self.headerbar = self.header.querySelector('.jsPanel-headerbar'); // log, title and controls self.titlebar = self.header.querySelector('.jsPanel-titlebar'); // div surrounding title div self.headerlogo = self.headerbar.querySelector('.jsPanel-headerlogo'); // logo only self.headertitle = self.headerbar.querySelector('.jsPanel-title'); // title div self.controlbar = self.headerbar.querySelector('.jsPanel-controlbar'); // div surrounding all controls self.headertoolbar = self.header.querySelector('.jsPanel-hdr-toolbar'); self.content = self.querySelector('.jsPanel-content'); self.footer = self.querySelector('.jsPanel-ftr'); self.snappableTo = false; self.snapped = false; self.droppableTo = false; self.progressbar = self.autocloseProgressbar = self.querySelector('.jsPanel-progressbar'); // self.autocloseProgressbar kept for compatibility // Events const jspanelloaded = new CustomEvent('jspanelloaded', { detail: options.id, cancelable: true, }), jspanelstatuschange = new CustomEvent('jspanelstatuschange', { detail: options.id, cancelable: true, }), jspanelbeforenormalize = new CustomEvent('jspanelbeforenormalize', { detail: options.id, cancelable: true, }), jspanelnormalized = new CustomEvent('jspanelnormalized', { detail: options.id, cancelable: true, }), jspanelbeforemaximize = new CustomEvent('jspanelbeforemaximize', { detail: options.id, cancelable: true, }), jspanelmaximized = new CustomEvent('jspanelmaximized', { detail: options.id, cancelable: true, }), jspanelbeforeminimize = new CustomEvent('jspanelbeforeminimize', { detail: options.id, cancelable: true, }), jspanelminimized = new CustomEvent('jspanelminimized', { detail: options.id, cancelable: true, }), jspanelbeforesmallify = new CustomEvent('jspanelbeforesmallify', { detail: options.id, cancelable: true, }), jspanelsmallified = new CustomEvent('jspanelsmallified', { detail: options.id, cancelable: true, }), jspanelsmallifiedmax = new CustomEvent('jspanelsmallifiedmax', { detail: options.id, cancelable: true, }), jspanelbeforeunsmallify = new CustomEvent('jspanelbeforeunsmallify', { detail: options.id, cancelable: true, }), jspanelfronted = new CustomEvent('jspanelfronted', { detail: options.id, cancelable: true, }), jspanelbeforeclose = new CustomEvent('jspanelbeforeclose', { detail: options.id, cancelable: true, }), jspanelclosed = new CustomEvent('jspanelclosed', { detail: options.id, cancelable: true, }), jspanelcloseduser = new CustomEvent('jspanelcloseduser', { detail: options.id, cancelable: true, }); // make panel available as event object property 'panel' [ jspanelloaded, jspanelstatuschange, jspanelbeforenormalize, jspanelnormalized, jspanelbeforemaximize, jspanelmaximized, jspanelbeforeminimize, jspanelminimized, jspanelbeforesmallify, jspanelsmallified, jspanelsmallifiedmax, jspanelbeforeunsmallify, jspanelfronted, jspanelbeforeclose, ].forEach(evt => evt.panel = self); // controls buttons const closeBtn = self.querySelector('.jsPanel-btn-close'), maxBtn = self.querySelector('.jsPanel-btn-maximize'), normBtn = self.querySelector('.jsPanel-btn-normalize'), smallBtn = self.querySelector('.jsPanel-btn-smallify'), minBtn = self.querySelector('.jsPanel-btn-minimize'); if (closeBtn) { jsPanel.pointerup.forEach(item => { closeBtn.addEventListener(item, e => { e.preventDefault(); // disable close for all mouse buttons but left if (e.button && e.button > 0) { return false; } self.close(null, true); // true indicates panel closed by using the close control }); }); } if (maxBtn) { jsPanel.pointerup.forEach(item => { maxBtn.addEventListener(item, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } self.maximize(); }); }); } if (normBtn) { jsPanel.pointerup.forEach(item => { normBtn.addEventListener(item, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } self.normalize(); }); }); } if (smallBtn) { jsPanel.pointerup.forEach(item => { smallBtn.addEventListener(item, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } if (self.status === 'normalized' || self.status === 'maximized') { self.smallify(); } else if (self.status === 'smallified' || self.status === 'smallifiedmax') { self.unsmallify(); } }); }); } if (minBtn) { jsPanel.pointerup.forEach(item => { minBtn.addEventListener(item, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } self.minimize(); }); }); } // import extensions (extensions of the individual panel, not the global object jsPanel) let extensions = jsPanel.extensions; for (let ext in extensions) { if (Object.prototype.hasOwnProperty.call(extensions, ext)) { self[ext] = extensions[ext]; } } // panel methods self.setBorder = val => { let border = jsPanel.pOborder(val); if (!border[2].length) { border[2] = self.style.backgroundColor; } border = border.join(' '); self.style.border = border; self.options.border = border; return self; }; self.setBorderRadius = rad => { if (typeof rad === 'string') { if (rad.startsWith('--') || rad.startsWith('var')) { rad = rad.replace(/\s*\(\s*/g, '(') // remove whitespace around opening brackets .replace(/\s*\)/g, ')') // remove whitespace around closing brackets .replace(/\s+/g, ' '); // replace all other whitespace(s) with a single whitespace rad = jsPanel.getCssVariableValue(rad); } } if (typeof rad === 'number') { rad += 'px'; } self.style.borderRadius = rad; const br = getComputedStyle(self); // set border-radius of either header or content section depending on presence of header if (self.options.header) { self.header.style.borderTopLeftRadius = br.borderTopLeftRadius; self.header.style.borderTopRightRadius = br.borderTopRightRadius; } else { self.content.style.borderTopLeftRadius = br.borderTopLeftRadius; self.content.style.borderTopRightRadius = br.borderTopRightRadius; } // set border-radius of either footer or content section depending on presence of footer if (self.options.footerToolbar) { self.footer.style.borderBottomRightRadius = br.borderBottomRightRadius; self.footer.style.borderBottomLeftRadius = br.borderBottomLeftRadius; } else { self.content.style.borderBottomRightRadius = br.borderBottomRightRadius; self.content.style.borderBottomLeftRadius = br.borderBottomLeftRadius; } return self; }; self.setTheme = (theme = options.theme, cb) => { // if panel is minimized normalize it for theme change let minimized; if (self.status === 'minimized') { minimized = true; self.normalize(); } // first remove all theme related styles jsPanel.clearTheme(self); if (typeof theme === 'object') { options.border = undefined; jsPanel.applyCustomTheme(self, theme); } else if (typeof theme === 'string') { if (theme === 'none') { theme = 'white'; } let themeDetails = jsPanel.getThemeDetails(theme); jsPanel.applyColorTheme(self, themeDetails); } // minimize again if panel was minimized prior theme change if (minimized) { self.minimize(); } if (cb) { cb.call(self, self); } return self; }; self.remove = (id, closedBy, cb) => { // self.remove() is just a helper func used in self.close() self.parentElement.removeChild(self); if (!document.getElementById(id)) { self.removeMinimizedReplacement(); self.status = 'closed'; if (closedBy) { document.dispatchEvent(jspanelcloseduser); } document.dispatchEvent(jspanelclosed); if (self.options.onclosed) { jsPanel.processCallbacks(self, self.options.onclosed, 'every', closedBy); } jsPanel.autopositionRemaining(self); if (cb) { cb.call(id, id); } } else { if (cb) { cb.call(self, id, self); } } window.removeEventListener('resize', self.windowResizeHandler); document.removeEventListener('jspanelresize', self.parentResizeHandler); // Send abort signal to remove events that can't be removed manually abortController.abort(); }; self.close = (cb, closedByUser) => { // if panel does not exist return if (!self.parentElement) {return;} if (self.closetimer) { window.clearInterval(self.closetimer); } document.dispatchEvent(jspanelbeforeclose); self.statusBefore = self.status; if ( self.options.onbeforeclose && self.options.onbeforeclose.length > 0 && !jsPanel.processCallbacks(self, self.options.onbeforeclose, 'some', self.status, closedByUser) ) { return self; } if (self.options.animateOut) { if (self.options.animateIn) { jsPanel.remClass(self, self.options.animateIn); } jsPanel.setClass(self, self.options.animateOut); self.addEventListener('animationend', e => { e.stopPropagation(); self.remove(self.id, closedByUser, cb); }); } else { self.remove(self.id, closedByUser, cb); } }; self.maximize = (cb, donotfront) => { // Note: do not disable maximize method for already maximized panels -> onContainerResize wouldn't work self.statusBefore = self.status; if ( options.onbeforemaximize && options.onbeforemaximize.length > 0 && !jsPanel.processCallbacks(self, options.onbeforemaximize, 'some', self.statusBefore) ) { return self; } document.dispatchEvent(jspanelbeforemaximize); const parent = self.parentElement, margins = jsPanel.pOcontainment(options.maximizedMargin); // normalize maximizedMargin if (parent === document.body) { // maximize within window /* When clientHeight is used on the root element (the element), (or on if the document is in quirks mode), the viewport's height (excluding any scrollbar) is returned. This is a special case of clientHeight. See https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight document.documentElement in the code below returns the element */ self.style.width = document.documentElement.clientWidth - margins[1] - margins[3] + 'px'; self.style.height = document.documentElement.clientHeight - margins[0] - margins[2] + 'px'; self.style.left = margins[3] + 'px'; self.style.top = margins[0] + 'px'; } else { // maximize within parentElement self.style.width = parent.clientWidth - margins[1] - margins[3] + 'px'; self.style.height = parent.clientHeight - margins[0] - margins[2] + 'px'; self.style.left = margins[3] + 'px'; self.style.top = margins[0] + 'px'; } smallBtn.style.transform = 'unset'; self.removeMinimizedReplacement(); self.status = 'maximized'; self.setControls(['.jsPanel-btn-maximize']); if (!donotfront) { self.front(); } document.dispatchEvent(jspanelmaximized); document.dispatchEvent(jspanelstatuschange); if (options.onstatuschange) { jsPanel.processCallbacks(self, options.onstatuschange, 'every', self.statusBefore); } if (cb) { cb.call(self, self, self.statusBefore); } if (options.onmaximized) { jsPanel.processCallbacks(self, options.onmaximized, 'every', self.statusBefore); } return self; }; self.minimize = cb => { if (self.status === 'minimized') { return self; } self.statusBefore = self.status; if ( options.onbeforeminimize && options.onbeforeminimize.length > 0 && !jsPanel.processCallbacks(self, options.onbeforeminimize, 'some', self.statusBefore) ) { return self; } document.dispatchEvent(jspanelbeforeminimize); // create container for minimized replacements if not already there if (!document.getElementById('jsPanel-replacement-container')) { const replacementContainer = document.createElement('div'); replacementContainer.id = 'jsPanel-replacement-container'; document.body.append(replacementContainer); } self.style.left = '-9999px'; self.status = 'minimized'; document.dispatchEvent(jspanelminimized); document.dispatchEvent(jspanelstatuschange); if (options.onstatuschange) { jsPanel.processCallbacks(self, options.onstatuschange, 'every', self.statusBefore); } if (options.minimizeTo) { let replacement = self.createMinimizedReplacement(), container, parent, list; switch (options.minimizeTo) { case 'default': document.getElementById('jsPanel-replacement-container').append(replacement); break; case 'parentpanel': parent = self.closest('.jsPanel-content').parentElement; list = parent.querySelectorAll('.jsPanel-minimized-box'); container = list[list.length - 1]; container.append(replacement); break; case 'parent': parent = self.parentElement; container = parent.querySelector('.jsPanel-minimized-container'); if (!container) { container = document.createElement('div'); container.className = 'jsPanel-minimized-container'; parent.append(container); } container.append(replacement); break; default: // all other strings are assumed to be selector strings returning a single element to append the min replacement to document.querySelector(options.minimizeTo).append(replacement); } } if (cb) { cb.call(self, self, self.statusBefore); } if (options.onminimized) { jsPanel.processCallbacks(self, options.onminimized, 'every', self.statusBefore); } return self; }; self.normalize = cb => { if (self.status === 'normalized') { return self; } self.statusBefore = self.status; // ensure smallify/unsmallify transition is turned off when resizing begins //self.style.transition = 'unset'; if ( options.onbeforenormalize && options.onbeforenormalize.length > 0 && !jsPanel.processCallbacks(self, options.onbeforenormalize, 'some', self.statusBefore) ) { return self; } document.dispatchEvent(jspanelbeforenormalize); self.style.width = self.currentData.width; self.style.height = self.currentData.height; if (self.snapped) { // if panel is snapped before minimizing restore snapped position self.snap(self.snapped, true); } else { self.style.left = self.currentData.left; self.style.top = self.currentData.top; } smallBtn.style.transform = 'unset'; self.removeMinimizedReplacement(); self.status = 'normalized'; self.setControls(['.jsPanel-btn-normalize']); self.front(); document.dispatchEvent(jspanelnormalized); document.dispatchEvent(jspanelstatuschange); if (options.onstatuschange) { jsPanel.processCallbacks(self, options.onstatuschange, 'every', self.statusBefore); } if (cb) { cb.call(self, self, self.statusBefore); } if (options.onnormalized) { jsPanel.processCallbacks(self, options.onnormalized, 'every', self.statusBefore); } return self; }; self.smallify = cb => { if (self.status === 'smallified' || self.status === 'smallifiedmax') { return self; } self.statusBefore = self.status; if ( options.onbeforesmallify && options.onbeforesmallify.length > 0 && !jsPanel.processCallbacks(self, options.onbeforesmallify, 'some', self.statusBefore) ) { return self; } document.dispatchEvent(jspanelbeforesmallify); self.style.overflow = 'hidden'; const selfStyles = window.getComputedStyle(self), selfHeaderHeight = parseFloat(window.getComputedStyle(self.headerbar).height); self.style.height = parseFloat(selfStyles.borderTopWidth) + parseFloat(selfStyles.borderBottomWidth) + selfHeaderHeight + 'px'; smallBtn.style.transform = 'rotate(180deg)'; if (self.status === 'normalized') { self.setControls(['.jsPanel-btn-normalize']); self.status = 'smallified'; document.dispatchEvent(jspanelsmallified); document.dispatchEvent(jspanelstatuschange); if (options.onstatuschange) { jsPanel.processCallbacks(self, options.onstatuschange, 'every', self.statusBefore); } } else if (self.status === 'maximized') { self.setControls(['.jsPanel-btn-maximize']); self.status = 'smallifiedmax'; document.dispatchEvent(jspanelsmallifiedmax); document.dispatchEvent(jspanelstatuschange); if (options.onstatuschange) { jsPanel.processCallbacks(self, options.onstatuschange, 'every', self.statusBefore); } } const minBoxes = self.querySelectorAll('.jsPanel-minimized-box'); minBoxes[minBoxes.length - 1].style.display = 'none'; if (cb) { cb.call(self, self, self.statusBefore); } if (options.onsmallified) { jsPanel.processCallbacks(self, options.onsmallified, 'every', self.statusBefore); } return self; }; self.unsmallify = cb => { self.statusBefore = self.status; if (self.status === 'smallified' || self.status === 'smallifiedmax') { if ( options.onbeforeunsmallify && options.onbeforeunsmallify.length > 0 && !jsPanel.processCallbacks(self, options.onbeforeunsmallify, 'some', self.statusBefore) ) { return self; } document.dispatchEvent(jspanelbeforeunsmallify); self.style.overflow = 'visible'; self.front(); if (self.status === 'smallified') { self.style.height = self.currentData.height; self.setControls(['.jsPanel-btn-normalize']); self.status = 'normalized'; document.dispatchEvent(jspanelnormalized); document.dispatchEvent(jspanelstatuschange); if (options.onstatuschange) { jsPanel.processCallbacks(self, options.onstatuschange, 'every', self.statusBefore); } } else if (self.status === 'smallifiedmax') { self.maximize(); } else if (self.status === 'minimized') { self.normalize(); } smallBtn.style.transform = 'rotate(0deg)'; const minBoxes = self.querySelectorAll('.jsPanel-minimized-box'); minBoxes[minBoxes.length - 1].style.display = 'flex'; if (cb) { cb.call(self, self, self.statusBefore); } if (options.onunsmallified) { jsPanel.processCallbacks(self, options.onunsmallified, 'every', self.statusBefore); } } return self; }; self.front = (callback, execOnFrontedCallbacks = true) => { if (self.status === 'minimized') { self.statusBefore === 'maximized' ? self.maximize() : self.normalize(); } else { const newArr = Array.prototype.slice.call(document.querySelectorAll('.jsPanel-standard')).map(panel => panel.style.zIndex); if (Math.max(...newArr) > self.style.zIndex) { self.style.zIndex = jsPanel.zi.next(); } jsPanel.resetZi(); } document.dispatchEvent(jspanelfronted); if (callback) { callback.call(self, self); } if (options.onfronted && execOnFrontedCallbacks) { jsPanel.processCallbacks(self, options.onfronted, 'every', self.status); } return self; }; self.snap = (pos, alreadySnapped = false) => { // store panel size before it snaps, only if not snapped already if (!alreadySnapped) { self.currentData.beforeSnap = { width: self.currentData.width, height: self.currentData.height, }; } // snap panel if (pos && typeof pos === 'function' && !alreadySnapped) { pos.call(self, self, self.snappableTo); } else if (pos !== false) { let offsets = [0, 0]; if (self.options.dragit.snap.containment) { if (self.options.dragit.containment) { const containment = jsPanel.pOcontainment(self.options.dragit.containment), position = self.snappableTo; if (position.startsWith('left')) { offsets[0] = containment[3]; } else if (position.startsWith('right')) { offsets[0] = -containment[1]; } if (position.endsWith('top')) { offsets[1] = containment[0]; } else if (position.endsWith('bottom')) { offsets[1] = -containment[2]; } } } self.reposition(`${self.snappableTo} ${offsets[0]} ${offsets[1]}`); } if (!alreadySnapped) { self.snapped = self.snappableTo; } }; self.move = (target, cb) => { let overlaps = self.overlaps(target, 'paddingbox'), source = self.parentElement; target.appendChild(self); self.options.container = target; self.style.left = overlaps.left + 'px'; self.style.top = overlaps.top + 'px'; self.saveCurrentDimensions(); self.saveCurrentPosition(); self.calcSizeFactors(); // important for option.onContainerResize if (cb) { cb.call(self, self, target, source); } return self; }; self.closeChildpanels = cb => { self.getChildpanels().forEach(item => item.close()); if (cb) { cb.call(self, self); } return self; }; self.getChildpanels = cb => { const childpanels = self.content.querySelectorAll('.jsPanel'); if (cb) { childpanels.forEach((panel, index, list) => { cb.call(panel, panel, index, list); }); } return childpanels; }; self.isChildpanel = cb => { const pp = self.closest('.jsPanel-content'), parentpanel = pp ? pp.parentElement : null; if (cb) { cb.call(self, self, parentpanel); } // if panel is childpanel of another panel returns parentpanel, otherwise false return pp ? parentpanel : false; }; self.contentRemove = cb => { jsPanel.emptyNode(self.content); if (cb) { cb.call(self, self); } return self; }; self.createMinimizedReplacement = () => { const tpl = jsPanel.createMinimizedTemplate(), color = window.getComputedStyle(self.headertitle).color, selfStyles = window.getComputedStyle(self), font = options.iconfont, controlbar = tpl.querySelector('.jsPanel-controlbar'); // if panel background is an image (that includes gradients) instead of a color value if (self.options.header !== 'auto-show-hide') { jsPanel.setStyles(tpl, { backgroundColor: selfStyles.backgroundColor, backgroundPositionX: selfStyles.backgroundPositionX, backgroundPositionY: selfStyles.backgroundPositionY, backgroundRepeat: selfStyles.backgroundRepeat, backgroundAttachment: selfStyles.backgroundAttachment, backgroundImage: selfStyles.backgroundImage, backgroundSize: selfStyles.backgroundSize, backgroundOrigin: selfStyles.backgroundOrigin, backgroundClip: selfStyles.backgroundClip, }); } else { tpl.style.backgroundColor = window.getComputedStyle(self.header).backgroundColor; } tpl.id = self.id + '-min'; tpl.querySelector('.jsPanel-headerbar').replaceChild(self.headerlogo.cloneNode(true), tpl.querySelector('.jsPanel-headerlogo')); tpl.querySelector('.jsPanel-titlebar').replaceChild(self.headertitle.cloneNode(true), tpl.querySelector('.jsPanel-title')); tpl.querySelector('.jsPanel-titlebar').setAttribute('title', self.headertitle.textContent); tpl.querySelector('.jsPanel-title').style.color = color; controlbar.style.color = color; controlbar.querySelectorAll('button').forEach(btn => btn.style.color = color); ['jsPanel-hdr-dark', 'jsPanel-hdr-light'].forEach(item => { if (self.header.classList.contains(item)) { tpl.querySelector('.jsPanel-hdr').classList.add(item); } }); // set iconfont self.setIconfont(font, tpl); if (self.dataset.btnnormalize === 'enabled') { jsPanel.pointerup.forEach(evt => { tpl.querySelector('.jsPanel-btn-normalize').addEventListener(evt, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } self.normalize(); }); }); } else { controlbar.querySelector('.jsPanel-btn-normalize').style.display = 'none'; } if (self.dataset.btnmaximize === 'enabled') { jsPanel.pointerup.forEach(evt => { tpl.querySelector('.jsPanel-btn-maximize').addEventListener(evt, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } self.maximize(); }); }); } else { controlbar.querySelector('.jsPanel-btn-maximize').style.display = 'none'; } if (self.dataset.btnclose === 'enabled') { jsPanel.pointerup.forEach(evt => { tpl.querySelector('.jsPanel-btn-close').addEventListener(evt, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } self.close(null, true); }); }); } else { controlbar.querySelector('.jsPanel-btn-close').style.display = 'none'; } return tpl; }; self.removeMinimizedReplacement = () => { const elmt = document.getElementById(`${self.id}-min`); if (elmt) { elmt.parentElement.removeChild(elmt); } }; self.drag = (options = {}) => { let dragstarted, dragElmt, opts; const jspaneldragstart = new CustomEvent('jspaneldragstart', { detail: self.id }), jspaneldrag = new CustomEvent('jspaneldrag', { detail: self.id }), jspaneldragstop = new CustomEvent('jspaneldragstop', { detail: self.id }); // make panel available as event object property 'panel' [jspaneldragstart, jspaneldrag, jspaneldragstop].forEach(evt => evt.panel = self); const camelcase = string => { // 'left-top' converted to 'snapLeftTop' let str = string.split('-'); str.forEach((word, index) => { str[index] = word.charAt(0).toUpperCase() + word.slice(1); }); return 'snap' + str.join(''); }; function windowListener(e) { if (e.relatedTarget === null) { jsPanel.pointermove.forEach(evt => { document.removeEventListener(evt, dragElmt, false); self.style.opacity = 1; }); } } // attach handler to each drag handle let handles = options.handles || jsPanel.defaults.dragit.handles; let cursor = options.cursor || jsPanel.defaults.dragit.cursor; function pointerUpHandlerDragit(e) { jsPanel.pointermove.forEach(e => document.removeEventListener(e, dragElmt)); jsPanel.removeSnapAreas(); if (dragstarted) { self.style.opacity = 1; dragstarted = undefined; if (opts.snap) { switch (self.snappableTo) { case 'left-top': self.snap(opts.snap.snapLeftTop); break; case 'center-top': self.snap(opts.snap.snapCenterTop); break; case 'right-top': self.snap(opts.snap.snapRightTop); break; case 'right-center': self.snap(opts.snap.snapRightCenter); break; case 'right-bottom': self.snap(opts.snap.snapRightBottom); break; case 'center-bottom': self.snap(opts.snap.snapCenterBottom); break; case 'left-bottom': self.snap(opts.snap.snapLeftBottom); break; case 'left-center': self.snap(opts.snap.snapLeftCenter); break; } if (opts.snap.callback && self.snappableTo && typeof opts.snap.callback === 'function') { opts.snap.callback.call(self, self); if (opts.snap.repositionOnSnap && opts.snap[camelcase(self.snappableTo)] !== false) { self.repositionOnSnap(self.snappableTo); } } if (self.snappableTo && opts.snap.repositionOnSnap && opts.snap[camelcase(self.snappableTo)]) { self.repositionOnSnap(self.snappableTo); } } // opts.drop if (self.droppableTo && self.droppableTo) { let sourceContainer = self.parentElement; self.move(self.droppableTo); if (opts.drop.callback) { opts.drop.callback.call(self, self, self.droppableTo, sourceContainer); } } document.dispatchEvent(jspaneldragstop); if (opts.stop.length) { let stopStyles = window.getComputedStyle(self), paneldata = { left: parseFloat(stopStyles.left), top: parseFloat(stopStyles.top), width: parseFloat(stopStyles.width), height: parseFloat(stopStyles.height), }; jsPanel.processCallbacks(self, opts.stop, false, paneldata, e); } self.saveCurrentPosition(); self.calcSizeFactors(); // important for options onwindowresize/onparentresize } self.controlbar.style.pointerEvents = 'inherit'; self.content.style.pointerEvents = 'inherit'; // restore other panel's css pointer-events document.querySelectorAll('iframe').forEach(frame => frame.style.pointerEvents = 'auto'); document.removeEventListener(e, pointerUpHandlerDragit); } self.querySelectorAll(handles).forEach(handle => { handle.style.touchAction = 'none'; handle.style.cursor = cursor; jsPanel.pointerdown.forEach(evt => { handle.addEventListener(evt, e => { // disable dragging for all mouse buttons but left if (e.button && e.button > 0) { return false; } // setup and normalize dragit options opts = Object.assign({}, jsPanel.defaults.dragit, options); if (opts.disableOnMaximized && self.status === 'maximized') { return false; } if (opts.containment || opts.containment === 0) { opts.containment = jsPanel.pOcontainment(opts.containment); } if (opts.grid) { if (Array.isArray(opts.grid)) { if (opts.grid.length === 1) { opts.grid[1] = opts.grid[0]; } } } if (opts.snap) { if (typeof opts.snap === 'object') { opts.snap = Object.assign({}, jsPanel.defaultSnapConfig, opts.snap); } else { opts.snap = jsPanel.defaultSnapConfig; } } // footer elmts with the class "jsPanel-ftr-btn" don't drag a panel // do not compare e.target with e.currentTarget because there might be footer elmts supposed to drag the panel // noinspection JSUnresolvedFunction if (e.target.closest('.jsPanel-ftr-btn')) { return; } self.controlbar.style.pointerEvents = 'none'; self.content.style.pointerEvents = 'none'; // without this code handler might not be unbound when content has iframe or object tag // prevents iframes in other panel from interfering with drag action of dragged panel document.querySelectorAll('iframe').forEach(frame => frame.style.pointerEvents = 'none'); let startStyles = window.getComputedStyle(self), startLeft = parseFloat(startStyles.left), startTop = parseFloat(startStyles.top), startWidth = parseFloat(startStyles.width), startHeight = parseFloat(startStyles.height), psx = e.touches ? e.touches[0].clientX : e.clientX, // pointer x on mousedown (don't use pageX, doesn't work on FF for Android) psy = e.touches ? e.touches[0].clientY : e.clientY, // same as above parent = self.parentElement, parentRect = parent.getBoundingClientRect(), parentStyles = window.getComputedStyle(parent), scaleFactor = self.getScaleFactor(), startLeftCorrection = 0, scrollbarwidths = jsPanel.getScrollbarWidth(parent); // function actually dragging the elmt dragElmt = e => { e.preventDefault(); if (!dragstarted) { document.dispatchEvent(jspaneldragstart); self.style.opacity = opts.opacity; // if configured restore panel size to a value before snap and reposition reasonable before drag actually starts if (self.snapped && opts.snap.resizeToPreSnap && self.currentData.beforeSnap) { self.resize(self.currentData.beforeSnap.width + ' ' + self.currentData.beforeSnap.height); self.setControls(['.jsPanel-btn-normalize']); let intermediateStyles = self.getBoundingClientRect(), delta = psx - (intermediateStyles.left + intermediateStyles.width), wHalf = intermediateStyles.width / 2; if (delta > -wHalf) { startLeftCorrection = delta + wHalf; } } self.front(); self.snapped = false; // panel is maximized on dragstart if (self.status === 'maximized') { self.setControls(['.jsPanel-btn-normalize']); self.status = 'normalized'; } // opts.drop if (opts.drop && opts.drop.dropZones) { //opts.drop.dropZones = opts.drop.dropZones.map(zone => jsPanel.pOcontainer(zone)); let dropzones = opts.drop.dropZones.map(zone => jsPanel.pOcontainer(zone)); // -> array where each item is a NodeList let dropzonelist = []; dropzones.forEach(function (nodelist) { if (nodelist.length) { // an element node does not have a length property nodelist.forEach(function (node) { dropzonelist.push(node); }); } else { dropzonelist.push(nodelist); } }); // filter list to have only unique values dropzonelist = dropzonelist.filter(function (value, index, self) { return self.indexOf(value) === index; }); opts.drop.dropZones = dropzonelist; } // dragstart callback if (opts.start.length) { jsPanel.processCallbacks( self, opts.start, false, { left: startLeft, top: startTop, width: startWidth, height: startHeight, }, e ); } } dragstarted = 1; let elmtL, elmtL2, elmtT, elmtT2, elmtR, elmtR2, elmtB, elmtB2, right, bottom; let pmx = e.touches ? e.touches[0].clientX : e.clientX, // current pointer x while pointer moves (don't use pageX, doesn't work on FF for Android) pmy = e.touches ? e.touches[0].clientY : e.clientY, // current pointer y while pointer moves (don't use pageY, doesn't work on FF for Android) dragStyles = window.getComputedStyle(self), // get current styles while dragging overlaps; // EDGE reports "auto" instead of pixel value using getComputedStyle(), so some values need to be calculated different // this whole block of code could be removed if EDGE not based on Chromium doesn't need to be supported if (parent === document.body) { let elmtRect = self.getBoundingClientRect(); right = window.innerWidth - parseInt(parentStyles.borderLeftWidth, 10) - parseInt(parentStyles.borderRightWidth, 10) - (elmtRect.left + elmtRect.width); bottom = window.innerHeight - parseInt(parentStyles.borderTopWidth, 10) - parseInt(parentStyles.borderBottomWidth, 10) - (elmtRect.top + elmtRect.height); } else { right = parseInt(parentStyles.width, 10) - parseInt(parentStyles.borderLeftWidth, 10) - parseInt(parentStyles.borderRightWidth, 10) - (parseInt(dragStyles.left, 10) + parseInt(dragStyles.width, 10)); bottom = parseInt(parentStyles.height, 10) - parseInt(parentStyles.borderTopWidth, 10) - parseInt(parentStyles.borderBottomWidth, 10) - (parseInt(dragStyles.top, 10) + parseInt(dragStyles.height, 10)); } // -- -- -- elmtL = parseFloat(dragStyles.left); elmtT = parseFloat(dragStyles.top); elmtR = right; // replace line with parseFloat(dragStyles.right); if EDGE code block above is removed elmtB = bottom; // replace line with parseFloat(dragStyles.bottom); if EDGE code block above is removed if (opts.snap) { if (opts.snap.trigger === 'panel') { elmtL2 = elmtL ** 2; elmtT2 = elmtT ** 2; elmtR2 = elmtR ** 2; elmtB2 = elmtB ** 2; } else if (opts.snap.trigger === 'pointer') { if (self.options.container === 'window') { elmtL = pmx; elmtT = pmy; elmtR = window.innerWidth - pmx; elmtB = window.innerHeight - pmy; elmtL2 = pmx ** 2; elmtT2 = elmtT ** 2; elmtR2 = elmtR ** 2; elmtB2 = elmtB ** 2; } else { overlaps = self.overlaps(parent, 'paddingbox', e); elmtL = overlaps.pointer.left; elmtT = overlaps.pointer.top; elmtR = overlaps.pointer.right; elmtB = overlaps.pointer.bottom; elmtL2 = overlaps.pointer.left ** 2; elmtT2 = overlaps.pointer.top ** 2; elmtR2 = overlaps.pointer.right ** 2; elmtB2 = overlaps.pointer.bottom ** 2; } } } let lefttopVectorDrag = Math.sqrt(elmtL2 + elmtT2), leftbottomVectorDrag = Math.sqrt(elmtL2 + elmtB2), righttopVectorDrag = Math.sqrt(elmtR2 + elmtT2), rightbottomVectorDrag = Math.sqrt(elmtR2 + elmtB2), horizontalDeltaDrag = Math.abs(elmtL - elmtR) / 2, verticalDeltaDrag = Math.abs(elmtT - elmtB) / 2, leftVectorDrag = Math.sqrt(elmtL2 + verticalDeltaDrag ** 2), topVectorDrag = Math.sqrt(elmtT2 + horizontalDeltaDrag ** 2), rightVectorDrag = Math.sqrt(elmtR2 + verticalDeltaDrag ** 2), bottomVectorDrag = Math.sqrt(elmtB2 + horizontalDeltaDrag ** 2); // prevent selections while dragging window.getSelection().removeAllRanges(); // trigger drag permanently while dragging document.dispatchEvent(jspaneldrag); // move elmt and apply axis option if (!opts.axis || opts.axis === 'x') { self.style.left = startLeft + (pmx - psx) / scaleFactor.x + startLeftCorrection + 'px'; // set new css left of elmt depending on opts.axis } if (!opts.axis || opts.axis === 'y') { self.style.top = startTop + (pmy - psy) / scaleFactor.y + 'px'; // set new css top of elmt depending on opts.axis } // apply grid option if (opts.grid) { let grid = opts.grid, axis = opts.axis; // formula rounds to the nearest multiple of grid // https://www.webveteran.com/blog/web-coding/javascript-round-to-any-multiple-of-a-specific-number/ let x = grid[0] * Math.round((startLeft + (pmx - psx)) / grid[0]), y = grid[1] * Math.round((startTop + (pmy - psy)) / grid[1]); if (!axis || axis === 'x') { self.style.left = `${x}px`; } if (!axis || axis === 'y') { self.style.top = `${y}px`; } } // apply containment option if (opts.containment || opts.containment === 0) { let containment = opts.containment; let maxLeft, maxTop; // calc maxLeft and maxTop (minLeft and MinTop is equal to containment setting) if (self.options.container === 'window') { maxLeft = window.innerWidth - parseFloat(dragStyles.width) - containment[1] - scrollbarwidths.y; maxTop = window.innerHeight - parseFloat(dragStyles.height) - containment[2] - scrollbarwidths.x; } else { let xCorr = parseFloat(parentStyles.borderLeftWidth) + parseFloat(parentStyles.borderRightWidth), yCorr = parseFloat(parentStyles.borderTopWidth) + parseFloat(parentStyles.borderBottomWidth); maxLeft = parentRect.width / scaleFactor.x - parseFloat(dragStyles.width) - containment[1] - xCorr - scrollbarwidths.y; maxTop = parentRect.height / scaleFactor.y - parseFloat(dragStyles.height) - containment[2] - yCorr - scrollbarwidths.x; } if (parseFloat(self.style.left) <= containment[3]) { self.style.left = containment[3] + 'px'; } if (parseFloat(self.style.top) <= containment[0]) { self.style.top = containment[0] + 'px'; } if (parseFloat(self.style.left) >= maxLeft) { self.style.left = maxLeft + 'px'; } if (parseFloat(self.style.top) >= maxTop) { self.style.top = maxTop + 'px'; } } // callback while dragging if (opts.drag.length) { let paneldata = { left: elmtL, top: elmtT, right: elmtR, bottom: elmtB, width: parseFloat(dragStyles.width), height: parseFloat(dragStyles.height), }; jsPanel.processCallbacks(self, opts.drag, false, paneldata, e); } // apply snap options if (opts.snap) { let snapSens = opts.snap.sensitivity, topSensAreaLength = parent === document.body ? window.innerWidth / 8 : parentRect.width / 8, sideSensAreaLength = parent === document.body ? window.innerHeight / 8 : parentRect.height / 8; self.snappableTo = false; jsPanel.removeSnapAreas(); if (lefttopVectorDrag < snapSens) { if (opts.snap.snapLeftTop !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'left-top'; jsPanel.createSnapArea(self, 'lt', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.left > 0 && overlaps.pointer.top > 0) { self.snappableTo = 'left-top'; jsPanel.createSnapArea(self, 'lt', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } else if (leftbottomVectorDrag < snapSens) { if (opts.snap.snapLeftBottom !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'left-bottom'; jsPanel.createSnapArea(self, 'lb', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.left > 0 && overlaps.pointer.bottom > 0) { self.snappableTo = 'left-bottom'; jsPanel.createSnapArea(self, 'lb', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } else if (righttopVectorDrag < snapSens) { if (opts.snap.snapRightTop !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'right-top'; jsPanel.createSnapArea(self, 'rt', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.right > 0 && overlaps.pointer.top > 0) { self.snappableTo = 'right-top'; jsPanel.createSnapArea(self, 'rt', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } else if (rightbottomVectorDrag < snapSens) { if (opts.snap.snapRightBottom !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'right-bottom'; jsPanel.createSnapArea(self, 'rb', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.right > 0 && overlaps.pointer.bottom > 0) { self.snappableTo = 'right-bottom'; jsPanel.createSnapArea(self, 'rb', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } else if (elmtT < snapSens && topVectorDrag < topSensAreaLength) { if (opts.snap.snapCenterTop !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'center-top'; jsPanel.createSnapArea(self, 'ct', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.top > 0) { self.snappableTo = 'center-top'; jsPanel.createSnapArea(self, 'ct', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } else if (elmtL < snapSens && leftVectorDrag < sideSensAreaLength) { if (opts.snap.snapLeftCenter !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'left-center'; jsPanel.createSnapArea(self, 'lc', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.left > 0) { self.snappableTo = 'left-center'; jsPanel.createSnapArea(self, 'lc', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } else if (elmtR < snapSens && rightVectorDrag < sideSensAreaLength) { if (opts.snap.snapRightCenter !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'right-center'; jsPanel.createSnapArea(self, 'rc', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.right > 0) { self.snappableTo = 'right-center'; jsPanel.createSnapArea(self, 'rc', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } else if (elmtB < snapSens && bottomVectorDrag < topSensAreaLength) { if (opts.snap.snapCenterBottom !== false) { if (!opts.snap.active || opts.snap.active === 'both') { self.snappableTo = 'center-bottom'; jsPanel.createSnapArea(self, 'cb', snapSens); } else if (opts.snap.trigger === 'pointer' && opts.snap.active && opts.snap.active === 'inside') { if (overlaps.pointer.bottom > 0) { self.snappableTo = 'center-bottom'; jsPanel.createSnapArea(self, 'cb', snapSens); } else { self.snappableTo = false; jsPanel.removeSnapAreas(); } } } } } // opts.drop if (opts.drop && opts.drop.dropZones) { // IE doesn't offer document.elementsFromPoint() but document.msElementsFromPoint() let elementsFromPoint = jsPanel.isIE ? 'msElementsFromPoint' : 'elementsFromPoint'; let elementsFrom = document[elementsFromPoint](e.clientX, e.clientY); // document.msElementsFromPoint() returns a nodeList -> convert to array if (!Array.isArray(elementsFrom)) { elementsFrom = Array.prototype.slice.call(elementsFrom); } opts.drop.dropZones.forEach(zone => { // Array.prototype.includes() needs polyfill in IE if (elementsFrom.includes(zone)) { self.droppableTo = zone; } }); // do not include following if statement in this.options.dragit.drop.dropZones.forEach !!!! if (!elementsFrom.includes(self.droppableTo)) { self.droppableTo = false; } } }; jsPanel.pointermove.forEach(e => document.addEventListener(e, dragElmt)); // remove drag handler when mouse leaves browser window (mouseleave doesn't work) // include the abort signal here so this event can be removed when the panel is closed window.addEventListener('mouseout', windowListener, false, {capture: false, signal: abortSignal}); }); }); jsPanel.pointerup.forEach(event => { // include the abort signal here so this event can be removed when the panel is closed document.addEventListener(event, pointerUpHandlerDragit, { signal: abortSignal}); window.removeEventListener('mouseout', windowListener); }); // dragit is initialized - now disable if set if (options.disable) { handle.style.pointerEvents = 'none'; } }); return self; }; self.dragit = string => { const dragitOptions = Object.assign({}, jsPanel.defaults.dragit, options.dragit), handles = self.querySelectorAll(dragitOptions.handles); if (string === 'disable') { handles.forEach(handle => handle.style.pointerEvents = 'none'); } else { handles.forEach(handle => handle.style.pointerEvents = 'auto'); } return self; }; self.sizeit = (options = {}) => { const jspanelresizestart = new CustomEvent('jspanelresizestart', { detail: self.id }), jspanelresize = new CustomEvent('jspanelresize', { detail: self.id }), jspanelresizestop = new CustomEvent('jspanelresizestop', { detail: self.id }); // make panel available as event object property 'panel' [jspanelresizestart, jspanelresize, jspanelresizestop].forEach(evt => evt.panel = self); let opts = {}, resizePanel, resizestarted, w, h, startWidth, startHeight; opts.handles = options.handles || jsPanel.defaults.resizeit.handles; opts.handles.split(',').forEach(item => { const node = document.createElement('DIV'); node.className = `jsPanel-resizeit-handle jsPanel-resizeit-${item.trim()}`; //node.style.zIndex = 90; self.append(node); }); // cache option aspectRatio of original resizeit configuration (is restored on pointerup) let cachedOptionAspectRatio = options.aspectRatio ? options.aspectRatio : false; function windowListener(e) { if (e.relatedTarget === null) { jsPanel.pointermove.forEach(evt => document.removeEventListener(evt, resizePanel, false)); } } function pointerUpHandlerResizeit(e) { jsPanel.pointermove.forEach(evt => document.removeEventListener(evt, resizePanel, false)); if (e.target.classList && e.target.classList.contains('jsPanel-resizeit-handle')) { let isLeftChange, isTopChange, cl = e.target.className; if (cl.match(/jsPanel-resizeit-nw|jsPanel-resizeit-w|jsPanel-resizeit-sw/i)) { isLeftChange = true; } if (cl.match(/jsPanel-resizeit-nw|jsPanel-resizeit-n|jsPanel-resizeit-ne/i)) { isTopChange = true; } // snap panel to grid (doesn't work that well if inside function resizePanel) if (opts.grid && Array.isArray(opts.grid)) { if (opts.grid.length === 1) { opts.grid[1] = opts.grid[0]; } const cw = parseFloat(self.style.width), ch = parseFloat(self.style.height), modW = cw % opts.grid[0], modH = ch % opts.grid[1], cx = parseFloat(self.style.left), cy = parseFloat(self.style.top), modX = cx % opts.grid[0], modY = cy % opts.grid[1]; if (modW < opts.grid[0] / 2) { self.style.width = cw - modW + 'px'; } else { self.style.width = cw + (opts.grid[0] - modW) + 'px'; } if (modH < opts.grid[1] / 2) { self.style.height = ch - modH + 'px'; } else { self.style.height = ch + (opts.grid[1] - modH) + 'px'; } if (isLeftChange) { if (modX < opts.grid[0] / 2) { self.style.left = cx - modX + 'px'; } else { self.style.left = cx + (opts.grid[0] - modX) + 'px'; } } if (isTopChange) { if (modY < opts.grid[1] / 2) { self.style.top = cy - modY + 'px'; } else { self.style.top = cy + (opts.grid[1] - modY) + 'px'; } } } } if (resizestarted) { self.content.style.pointerEvents = 'inherit'; resizestarted = undefined; self.saveCurrentDimensions(); self.saveCurrentPosition(); self.calcSizeFactors(); let smallifyBtn = self.controlbar.querySelector('.jsPanel-btn-smallify'); let elmtRect = self.getBoundingClientRect(); if (smallifyBtn && elmtRect.height > startHeight + 5) { smallifyBtn.style.transform = 'rotate(0deg)'; } document.dispatchEvent(jspanelresizestop); if (opts.stop.length) { let stopStyles = window.getComputedStyle(self), paneldata = { left: parseFloat(stopStyles.left), top: parseFloat(stopStyles.top), width: parseFloat(stopStyles.width), height: parseFloat(stopStyles.height), }; jsPanel.processCallbacks(self, opts.stop, false, paneldata, e); } } self.content.style.pointerEvents = 'inherit'; // restore other panel's css pointer-events document.querySelectorAll('iframe').forEach(frame => frame.style.pointerEvents = 'auto'); // restore option aspectRatio to original configuration opts.aspectRatio = cachedOptionAspectRatio; document.removeEventListener(e, pointerUpHandlerResizeit); } self.querySelectorAll('.jsPanel-resizeit-handle').forEach(handle => { handle.style.touchAction = 'none'; jsPanel.pointerdown.forEach(event => { handle.addEventListener(event, e => { // prevent window scroll while resizing elmt e.preventDefault(); e.stopPropagation(); // disable resizing for all mouse buttons but left if (e.button && e.button > 0) { return false; } // factor is needed only for the modifier key Shift feature let factor = 1; // setup and normalize resizeit options opts = Object.assign({}, jsPanel.defaults.resizeit, options); if (opts.containment || opts.containment === 0) { opts.containment = jsPanel.pOcontainment(opts.containment); } // legacy line: aspectRatio should be either 'panel' or 'content', not just true if (opts.aspectRatio && opts.aspectRatio === true) { opts.aspectRatio = 'panel'; } // set aspectRatio according to modifier key if (jsPanel.modifier) { let modifier = jsPanel.modifier; if (modifier.altKey) { opts.aspectRatio = 'content'; } else if (modifier.ctrlKey) { opts.aspectRatio = 'panel'; } else if (modifier.shiftKey) { opts.aspectRatio = false; factor = 2; // does work only with 2 as value } } // noinspection JSUnresolvedFunction let maxWidth = typeof opts.maxWidth === 'function' ? opts.maxWidth() : opts.maxWidth || 10000, maxHeight = typeof opts.maxHeight === 'function' ? opts.maxHeight() : opts.maxHeight || 10000, minWidth = typeof opts.minWidth === 'function' ? opts.minWidth() : opts.minWidth, minHeight = typeof opts.minHeight === 'function' ? opts.minHeight() : opts.minHeight; self.content.style.pointerEvents = 'none'; // prevents iframes in other panel from interfering with resize action of dragged panel document.querySelectorAll('iframe').forEach(frame => frame.style.pointerEvents = 'none'); // noinspection JSUnresolvedVariable const elmtParent = self.parentElement, elmtParentTagName = elmtParent.tagName.toLowerCase(), elmtRect = self.getBoundingClientRect(), elmtParentRect = elmtParent.getBoundingClientRect(), elmtParentStyles = window.getComputedStyle(elmtParent, null), elmtParentBLW = parseInt(elmtParentStyles.borderLeftWidth, 10), elmtParentBTW = parseInt(elmtParentStyles.borderTopWidth, 10), elmtParentPosition = elmtParentStyles.getPropertyValue('position'), startX = (e.clientX || e.clientX === 0) || e.touches[0].clientX, startY = (e.clientY || e.clientY === 0) || e.touches[0].clientY, startRatio = startX / startY, resizeHandleClassList = e.target.classList, scaleFactor = self.getScaleFactor(), aspectRatio = elmtRect.width / elmtRect.height, elmtContentRect = self.content.getBoundingClientRect(), aspectRatioContent = elmtContentRect.width / elmtContentRect.height, hdrHeight = self.header.getBoundingClientRect().height, // needed in aspectRatio ftrHeight = self.footer.getBoundingClientRect().height || 0; // needed in aspectRatio let startLeft = elmtRect.left, startTop = elmtRect.top, maxWidthEast = 10000, maxWidthWest = 10000, maxHeightSouth = 10000, maxHeightNorth = 10000; startWidth = elmtRect.width; startHeight = elmtRect.height; if (elmtParentTagName !== 'body') { startLeft = elmtRect.left - elmtParentRect.left + elmtParent.scrollLeft; startTop = elmtRect.top - elmtParentRect.top + elmtParent.scrollTop; } // calc min/max left/top values if containment is set - code from jsDraggable if (elmtParentTagName === 'body' && opts.containment) { maxWidthEast = document.documentElement.clientWidth - elmtRect.left; maxHeightSouth = document.documentElement.clientHeight - elmtRect.top; maxWidthWest = elmtRect.width + elmtRect.left; maxHeightNorth = elmtRect.height + elmtRect.top; } else { // if panel is NOT in body if (opts.containment) { if (elmtParentPosition === 'static') { maxWidthEast = elmtParentRect.width - elmtRect.left + elmtParentBLW; maxHeightSouth = elmtParentRect.height + elmtParentRect.top - elmtRect.top + elmtParentBTW; maxWidthWest = elmtRect.width + (elmtRect.left - elmtParentRect.left) - elmtParentBLW; maxHeightNorth = elmtRect.height + (elmtRect.top - elmtParentRect.top) - elmtParentBTW; } else { maxWidthEast = elmtParent.clientWidth - (elmtRect.left - elmtParentRect.left) / scaleFactor.x + elmtParentBLW; maxHeightSouth = elmtParent.clientHeight - (elmtRect.top - elmtParentRect.top) / scaleFactor.y + elmtParentBTW; maxWidthWest = (elmtRect.width + elmtRect.left - elmtParentRect.left) / scaleFactor.x - elmtParentBLW; maxHeightNorth = self.clientHeight + (elmtRect.top - elmtParentRect.top) / scaleFactor.y - elmtParentBTW; } } } // if original opts.containment is an array if (opts.containment) { maxWidthWest -= opts.containment[3]; maxHeightNorth -= opts.containment[0]; maxWidthEast -= opts.containment[1]; maxHeightSouth -= opts.containment[2]; } // calculate corrections for rotated panels const computedStyle = window.getComputedStyle(self), wDif = parseFloat(computedStyle.width) - elmtRect.width, hDif = parseFloat(computedStyle.height) - elmtRect.height; let xDif = parseFloat(computedStyle.left) - elmtRect.left, yDif = parseFloat(computedStyle.top) - elmtRect.top; if (elmtParent !== document.body) { xDif += elmtParentRect.left; yDif += elmtParentRect.top; } // used in aspectRatio code let borderTopWidth = parseInt(computedStyle.borderTopWidth, 10), borderRightWidth = parseInt(computedStyle.borderRightWidth, 10), borderBottomWidth = parseInt(computedStyle.borderBottomWidth, 10), borderLeftWidth = parseInt(computedStyle.borderLeftWidth, 10); resizePanel = evt => { evt.preventDefault(); // trigger resizestarted only once per resize if (!resizestarted) { document.dispatchEvent(jspanelresizestart); if (opts.start.length) { jsPanel.processCallbacks( self, opts.start, false, { width: startWidth, height: startHeight, left: startLeft, top: startTop, }, evt ); } self.front(); // if panel is maximized on resize start set status to normalized and swap maximize/normalize buttons if (self.status === 'maximized') { self.status = 'normalized'; if (self.controlbar.querySelector('.jsPanel-btn-maximize')) { self.setControlStatus('maximize', 'show'); } if (self.controlbar.querySelector('.jsPanel-btn-normalize')) { self.setControlStatus('normalize', 'hide'); } } if (elmtRect.height > startHeight + 5) { self.status = 'normalized'; self.setControls(['.jsPanel-btn-normalize']); } } resizestarted = 1; // trigger resize permanently while resizing document.dispatchEvent(jspanelresize); // possibly updated while resizing let eventX = evt.touches ? evt.touches[0].clientX : evt.clientX, eventY = evt.touches ? evt.touches[0].clientY : evt.clientY, overlaps; if (resizeHandleClassList.contains('jsPanel-resizeit-e')) { //w = startWidth + (eventX - startX) / scaleFactor.x + wDif; w = startWidth + ((eventX - startX) * factor) / scaleFactor.x + wDif; // needs left adjust, for width and height adjust factor may be either 1 (no adjust) or 2 if (w >= maxWidthEast) { w = maxWidthEast; } if (w >= maxWidth) { w = maxWidth; } if (w <= minWidth) { w = minWidth; } self.style.width = w + 'px'; if (factor === 2) { // factor works only with value of 2 when adjusting left or top self.style.left = startLeft - (eventX - startX) + 'px'; } if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.height = (w - borderRightWidth - borderLeftWidth) / aspectRatioContent + hdrHeight + ftrHeight + borderTopWidth + borderBottomWidth + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.bottom <= opts.containment[2]) { self.style.height = maxHeightSouth + 'px'; self.style.width = maxHeightSouth * aspectRatioContent + 'px'; } } } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.height = w / aspectRatio + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.bottom <= opts.containment[2]) { self.style.height = maxHeightSouth + 'px'; self.style.width = maxHeightSouth * aspectRatio + 'px'; } } } } else if (resizeHandleClassList.contains('jsPanel-resizeit-s')) { //h = startHeight + (eventY - startY) / scaleFactor.y + hDif; h = startHeight + ((eventY - startY) * factor) / scaleFactor.y + hDif; // needs top adjust if (h >= maxHeightSouth) { h = maxHeightSouth; } if (h >= maxHeight) { h = maxHeight; } if (h <= minHeight) { h = minHeight; } self.style.height = h + 'px'; if (factor === 2) { self.style.top = startTop - (eventY - startY) + 'px'; } if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.width = (h - hdrHeight - ftrHeight - borderTopWidth - borderBottomWidth) * aspectRatioContent + borderTopWidth + borderBottomWidth + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatioContent + 'px'; } } } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.width = h * aspectRatio + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatio + 'px'; } } } } else if (resizeHandleClassList.contains('jsPanel-resizeit-w')) { //w = startWidth + (startX - eventX) / scaleFactor.x + wDif; w = startWidth + ((startX - eventX) * factor) / scaleFactor.x + wDif; // doesn't need left adjust if (w <= maxWidth && w >= minWidth && w <= maxWidthWest) { self.style.left = startLeft + (eventX - startX) / scaleFactor.x + xDif + 'px'; } if (w >= maxWidthWest) { w = maxWidthWest; } if (w >= maxWidth) { w = maxWidth; } if (w <= minWidth) { w = minWidth; } self.style.width = w + 'px'; if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.height = (w - borderRightWidth - borderLeftWidth) / aspectRatioContent + hdrHeight + ftrHeight + borderTopWidth + borderBottomWidth + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.bottom <= opts.containment[2]) { self.style.height = maxHeightSouth + 'px'; self.style.width = maxHeightSouth * aspectRatioContent + 'px'; } } } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.height = w / aspectRatio + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.bottom <= opts.containment[2]) { self.style.height = maxHeightSouth + 'px'; self.style.width = maxHeightSouth * aspectRatio + 'px'; } } } } else if (resizeHandleClassList.contains('jsPanel-resizeit-n')) { //h = startHeight + (startY - eventY) / scaleFactor.y + hDif; h = startHeight + ((startY - eventY) * factor) / scaleFactor.y + hDif; // doesn't need top adjust if (h <= maxHeight && h >= minHeight && h <= maxHeightNorth) { self.style.top = startTop + (eventY - startY) / scaleFactor.y + yDif + 'px'; } if (h >= maxHeightNorth) { h = maxHeightNorth; } if (h >= maxHeight) { h = maxHeight; } if (h <= minHeight) { h = minHeight; } self.style.height = h + 'px'; if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.width = (h - hdrHeight - ftrHeight - borderTopWidth - borderBottomWidth) * aspectRatioContent + borderTopWidth + borderBottomWidth + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatioContent + 'px'; } } } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.width = h * aspectRatio + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatio + 'px'; } } } } else if (resizeHandleClassList.contains('jsPanel-resizeit-se')) { //w = startWidth + (eventX - startX) / scaleFactor.x + wDif; w = startWidth + ((eventX - startX) * factor) / scaleFactor.x + wDif; // needs left adjust if (w >= maxWidthEast) { w = maxWidthEast; } if (w >= maxWidth) { w = maxWidth; } if (w <= minWidth) { w = minWidth; } self.style.width = w + 'px'; if (factor === 2) { self.style.left = startLeft - (eventX - startX) + 'px'; } if (opts.aspectRatio) { self.style.height = w / aspectRatio + 'px'; } //h = startHeight + (eventY - startY) / scaleFactor.y + hDif; h = startHeight + ((eventY - startY) * factor) / scaleFactor.y + hDif; // needs top adjust if (h >= maxHeightSouth) { h = maxHeightSouth; } if (h >= maxHeight) { h = maxHeight; } if (h <= minHeight) { h = minHeight; } self.style.height = h + 'px'; if (factor === 2) { self.style.top = startTop - (eventY - startY) + 'px'; } if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.width = (h - hdrHeight - ftrHeight - borderTopWidth - borderBottomWidth) * aspectRatioContent + borderTopWidth + borderBottomWidth + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatioContent + 'px'; } } } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.width = h * aspectRatio + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatio + 'px'; } } } } else if (resizeHandleClassList.contains('jsPanel-resizeit-sw')) { //h = startHeight + (eventY - startY) / scaleFactor.y + hDif; h = startHeight + ((eventY - startY) * factor) / scaleFactor.y + hDif; // needs top adjust if (h >= maxHeightSouth) { h = maxHeightSouth; } if (h >= maxHeight) { h = maxHeight; } if (h <= minHeight) { h = minHeight; } self.style.height = h + 'px'; if (factor === 2) { self.style.top = startTop - (eventY - startY) + 'px'; } if (opts.aspectRatio) { self.style.width = h * aspectRatio + 'px'; } //w = startWidth + (startX - eventX) / scaleFactor.x + wDif; w = startWidth + ((startX - eventX) * factor) / scaleFactor.x + wDif; // doesn't need left adjust if (w <= maxWidth && w >= minWidth && w <= maxWidthWest) { self.style.left = startLeft + (eventX - startX) / scaleFactor.x + xDif + 'px'; } if (w >= maxWidthWest) { w = maxWidthWest; } if (w >= maxWidth) { w = maxWidth; } if (w <= minWidth) { w = minWidth; } self.style.width = w + 'px'; if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.height = (w - borderRightWidth - borderLeftWidth) / aspectRatioContent + hdrHeight + ftrHeight + borderTopWidth + borderBottomWidth + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.bottom <= opts.containment[2]) { self.style.height = maxHeightSouth + 'px'; self.style.width = maxHeightSouth * aspectRatioContent + 'px'; } } } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.height = w / aspectRatio + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.bottom <= opts.containment[2]) { self.style.height = maxHeightSouth + 'px'; self.style.width = maxHeightSouth * aspectRatio + 'px'; } } } } else if (resizeHandleClassList.contains('jsPanel-resizeit-ne')) { //w = startWidth + (eventX - startX) / scaleFactor.x + wDif; w = startWidth + ((eventX - startX) * factor) / scaleFactor.x + wDif; // needs left adjust if (w >= maxWidthEast) { w = maxWidthEast; } if (w >= maxWidth) { w = maxWidth; } if (w <= minWidth) { w = minWidth; } self.style.width = w + 'px'; if (factor === 2) { self.style.left = startLeft - (eventX - startX) + 'px'; } if (opts.aspectRatio) { self.style.height = w / aspectRatio + 'px'; } h = startHeight + ((startY - eventY) * factor) / scaleFactor.y + hDif; // doesn't need top adjust if (h <= maxHeight && h >= minHeight && h <= maxHeightNorth) { self.style.top = startTop + (eventY - startY) / scaleFactor.y + yDif + 'px'; } if (h >= maxHeightNorth) { h = maxHeightNorth; } if (h >= maxHeight) { h = maxHeight; } if (h <= minHeight) { h = minHeight; } self.style.height = h + 'px'; if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.width = (h - hdrHeight - ftrHeight - borderTopWidth - borderBottomWidth) * aspectRatioContent + borderTopWidth + borderBottomWidth + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatioContent + 'px'; } } } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.width = h * aspectRatio + 'px'; if (opts.containment) { overlaps = self.overlaps(elmtParent); if (overlaps.right <= opts.containment[1]) { self.style.width = maxWidthEast + 'px'; self.style.height = maxWidthEast / aspectRatio + 'px'; } } } } else if (resizeHandleClassList.contains('jsPanel-resizeit-nw')) { if (opts.aspectRatio && resizeHandleClassList.contains('jsPanel-resizeit-nw')) { eventX = eventY * startRatio; eventY = eventX / startRatio; } //w = startWidth + (startX - eventX) / scaleFactor.x + wDif; w = startWidth + ((startX - eventX) * factor) / scaleFactor.x + wDif; // doesn't need left adjust if (w <= maxWidth && w >= minWidth && w <= maxWidthWest) { self.style.left = startLeft + (eventX - startX) / scaleFactor.x + xDif + 'px'; } if (w >= maxWidthWest) { w = maxWidthWest; } if (w >= maxWidth) { w = maxWidth; } if (w <= minWidth) { w = minWidth; } self.style.width = w + 'px'; if (opts.aspectRatio) { self.style.height = w / aspectRatio + 'px'; } h = startHeight + ((startY - eventY) * factor) / scaleFactor.y + hDif; // doesn't need top adjust if (h <= maxHeight && h >= minHeight && h <= maxHeightNorth) { self.style.top = startTop + (eventY - startY) / scaleFactor.y + yDif + 'px'; } if (h >= maxHeightNorth) { h = maxHeightNorth; } if (h >= maxHeight) { h = maxHeight; } if (h <= minHeight) { h = minHeight; } self.style.height = h + 'px'; if (opts.aspectRatio === 'content') { // if aspectRatio is true and set to 'content' the panels content section maintains its aspect ratio self.style.width = (h - hdrHeight - ftrHeight - borderTopWidth - borderBottomWidth) * aspectRatioContent + borderTopWidth + borderBottomWidth + 'px'; } else if (opts.aspectRatio === 'panel') { // otherwise the complete panel maintains its aspect ratio self.style.width = h * aspectRatio + 'px'; } } window.getSelection().removeAllRanges(); // get current position and size values while resizing const styles = window.getComputedStyle(self), values = { left: parseFloat(styles.left), top: parseFloat(styles.top), right: parseFloat(styles.right), bottom: parseFloat(styles.bottom), width: parseFloat(styles.width), height: parseFloat(styles.height), }; // callback while resizing if (opts.resize.length) { jsPanel.processCallbacks(self, opts.resize, false, values, evt); } }; jsPanel.pointermove.forEach(event => document.addEventListener(event, resizePanel, false)); // remove resize handler when mouse leaves browser window (mouseleave doesn't work) // include the abort signal here so the event can be removed when the panel is closed window.addEventListener('mouseout', windowListener, {capture: false, signal: abortSignal}); }); }); jsPanel.pointerup.forEach(function (event) { // include the abort signal here so the event can be removed when the panel is closed document.addEventListener(event, pointerUpHandlerResizeit, {signal:abortSignal}); window.removeEventListener('mouseout', windowListener); }); // resizeit is initialized - now disable if set if (options.disable) { handle.style.pointerEvents = 'none'; } }); return self; }; self.resizeit = string => { const handles = self.querySelectorAll('.jsPanel-resizeit-handle'); if (string === 'disable') { handles.forEach(handle => handle.style.pointerEvents = 'none'); } else { handles.forEach(handle => handle.style.pointerEvents = 'auto'); } return self; }; self.getScaleFactor = () => { const rect = self.getBoundingClientRect(); return { x: rect.width / self.offsetWidth, y: rect.height / self.offsetHeight, }; }; self.calcSizeFactors = () => { const styles = window.getComputedStyle(self); if (options.container === 'window') { self.hf = parseFloat(styles.left) / (window.innerWidth - parseFloat(styles.width)); self.vf = parseFloat(styles.top) / (window.innerHeight - parseFloat(styles.height)); } else { if (self.parentElement) { let parentStyles = self.parentElement.getBoundingClientRect(); self.hf = parseFloat(styles.left) / (parentStyles.width - parseFloat(styles.width)); self.vf = parseFloat(styles.top) / (parentStyles.height - parseFloat(styles.height)); } } }; self.saveCurrentDimensions = () => { const normData = window.getComputedStyle(self); self.currentData.width = normData.width; self.currentData.height = normData.height; }; self.saveCurrentPosition = () => { const normData = window.getComputedStyle(self); self.currentData.left = normData.left; self.currentData.top = normData.top; }; self.reposition = (...params) => { let pos = options.position, updateCache = true, callback; params.forEach(value => { if (typeof value === 'string' || typeof value === 'object') { pos = value; } else if (typeof value === 'boolean') { updateCache = value; } else if (typeof value === 'function') { callback = value; } }); jsPanel.position(self, pos); // check whether self has docked panels -> reposition docked panels as well if (self.slaves && self.slaves.size > 0) { self.slaves.forEach(slave => slave.reposition()); } if (updateCache) { self.saveCurrentPosition(); } if (callback) { callback.call(self, self); } return self; }; self.repositionOnSnap = pos => { let offsetX = '0', offsetY = '0', margins = jsPanel.pOcontainment(options.dragit.containment); // calculate offsets if (options.dragit.snap.containment) { switch (pos) { case 'left-top': offsetX = margins[3]; offsetY = margins[0]; break; case 'right-top': offsetX = -margins[1]; offsetY = margins[0]; break; case 'right-bottom': offsetX = -margins[1]; offsetY = -margins[2]; break; case 'left-bottom': offsetX = margins[3]; offsetY = -margins[2]; break; case 'center-top': offsetX = margins[3] / 2 - margins[1] / 2; offsetY = margins[0]; break; case 'center-bottom': offsetX = margins[3] / 2 - margins[1] / 2; offsetY = -margins[2]; break; case 'left-center': offsetX = margins[3]; offsetY = margins[0] / 2 - margins[2] / 2; break; case 'right-center': offsetX = -margins[1]; offsetY = margins[0] / 2 - margins[2] / 2; break; } } /* jsPanel.position(self, `${pos} ${offsetX} ${offsetY}`); For some reason I could not find the line above does not work (pos and offsets in one string), but only when center-bottom is used with different settings for left/right margin. */ jsPanel.position(self, pos); jsPanel.setStyles(self, { left: `calc(${self.style.left} + ${offsetX}px)`, top: `calc(${self.style.top} + ${offsetY}px)`, }); }; self.overlaps = (reference, elmtBox, event) => { let panelRect = self.getBoundingClientRect(), parentStyle = getComputedStyle(self.parentElement), scaleFactor = self.getScaleFactor(), parentBorderWidth = { top: 0, right: 0, bottom: 0, left: 0 }, referenceRect, evtX = 0, evtY = 0, evtXparent = 0, evtYparent = 0; if (self.options.container !== 'window' && elmtBox === 'paddingbox') { parentBorderWidth.top = parseInt(parentStyle.borderTopWidth, 10) * scaleFactor.y; parentBorderWidth.right = parseInt(parentStyle.borderRightWidth, 10) * scaleFactor.x; parentBorderWidth.bottom = parseInt(parentStyle.borderBottomWidth, 10) * scaleFactor.y; parentBorderWidth.left = parseInt(parentStyle.borderLeftWidth, 10) * scaleFactor.x; } if (typeof reference === 'string') { if (reference === 'window') { referenceRect = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight, }; } else if (reference === 'parent') { referenceRect = self.parentElement.getBoundingClientRect(); } else { referenceRect = document.querySelector(reference).getBoundingClientRect(); } } else { referenceRect = reference.getBoundingClientRect(); } if (event) { evtX = event.touches ? event.touches[0].clientX : event.clientX; evtY = event.touches ? event.touches[0].clientY : event.clientY; evtXparent = evtX - referenceRect.left; evtYparent = evtY - referenceRect.top; } let overlapsX = panelRect.left < referenceRect.right && panelRect.right > referenceRect.left, overlapsY = panelRect.top < referenceRect.bottom && panelRect.bottom > referenceRect.top, overlaps = overlapsX && overlapsY; return { overlaps: overlaps, top: panelRect.top - referenceRect.top - parentBorderWidth.top, right: referenceRect.right - panelRect.right - parentBorderWidth.right, bottom: referenceRect.bottom - panelRect.bottom - parentBorderWidth.bottom, left: panelRect.left - referenceRect.left - parentBorderWidth.left, parentBorderWidth: parentBorderWidth, panelRect: panelRect, referenceRect: referenceRect, pointer: { // pointer coords relative to window and reference clientX: evtX, clientY: evtY, left: evtXparent - parentBorderWidth.left, top: evtYparent - parentBorderWidth.top, right: referenceRect.width - evtXparent - parentBorderWidth.right, bottom: referenceRect.height - evtYparent - parentBorderWidth.bottom, }, }; }; self.setSize = () => { if (options.panelSize) { const values = jsPanel.pOsize(self, options.panelSize); self.style.width = values.width; self.style.height = values.height; } else if (options.contentSize) { const values = jsPanel.pOsize(self, options.contentSize); self.content.style.width = values.width; self.content.style.height = values.height; self.style.width = values.width; // explicitly assign current width/height to panel self.content.style.width = '100%'; } return self; }; self.resize = (...params) => { let dimensions = window.getComputedStyle(self), size = { width: dimensions.width, height: dimensions.height }, updateCache = true, callback; params.forEach(value => { if (typeof value === 'string') { size = value; } else if (typeof value === 'object') { size = Object.assign(size, value); } else if (typeof value === 'boolean') { updateCache = value; } else if (typeof value === 'function') { callback = value; } }); let values = jsPanel.pOsize(self, size); self.style.width = values.width; self.style.height = values.height; // check whether self has docked panels -> reposition docked panels if (self.slaves && self.slaves.size > 0) { self.slaves.forEach(slave => slave.reposition()); } if (updateCache) { self.saveCurrentDimensions(); } self.status = 'normalized'; let smallifyBtn = self.controlbar.querySelector('.jsPanel-btn-smallify'); if (smallifyBtn) { smallifyBtn.style.transform = 'rotate(0deg)'; } if (callback) { callback.call(self, self); } self.calcSizeFactors(); return self; }; self.windowResizeHandler = e => { if (e.target === window) { // see https://bugs.jqueryui.com/ticket/7514 let status = self.status, onWindowResize = options.onwindowresize, left, top; if (status === 'maximized' && onWindowResize) { self.maximize(false, true); } else if (self.snapped && status !== 'minimized') { self.snap(self.snapped, true); } else if (status === 'normalized' || status === 'smallified' || status === 'maximized') { let settingType = typeof onWindowResize; if (settingType === 'boolean') { left = (window.innerWidth - self.offsetWidth) * self.hf; self.style.left = left <= 0 ? 0 : left + 'px'; top = (window.innerHeight - self.offsetHeight) * self.vf; self.style.top = top <= 0 ? 0 : top + 'px'; } else if (settingType === 'function') { onWindowResize.call(self, e, self); } else if (settingType === 'object') { // { preset: boolean, callback: function(event, panel){} } if (onWindowResize.preset === true) { left = (window.innerWidth - self.offsetWidth) * self.hf; self.style.left = left <= 0 ? 0 : left + 'px'; top = (window.innerHeight - self.offsetHeight) * self.vf; self.style.top = top <= 0 ? 0 : top + 'px'; onWindowResize.callback.call(self, e, self); } else { onWindowResize.callback.call(self, e, self); } } } else if (status === 'smallifiedmax' && onWindowResize) { self.maximize(false, true).smallify(); } // check whether self has docked panels -> reposition docked panels as well if (self.slaves && self.slaves.size > 0) { self.slaves.forEach(slave => slave.reposition()); } } }; self.setControls = (sel, cb) => { self.header.querySelectorAll('.jsPanel-btn').forEach(item => { const btnClass = item.className.split('-'), btn = btnClass[btnClass.length - 1]; if (self.getAttribute(`data-btn${btn}`) !== 'hidden') { item.style.display = 'block'; } }); sel.forEach(item => { const btn = self.controlbar.querySelector(item); if (btn) { btn.style.display = 'none'; } }); if (cb) { cb.call(self, self); } return self; }; self.setControlStatus = (ctrl, action = 'enable', cb) => { const btn = self.controlbar.querySelector(`.jsPanel-btn-${ctrl}`); switch (action) { case 'disable': if (self.getAttribute(`data-btn${ctrl}`) !== 'removed') { self.setAttribute(`data-btn${ctrl}`, 'disabled'); btn.style.pointerEvents = 'none'; btn.style.opacity = 0.4; btn.style.cursor = 'default'; } break; case 'hide': if (self.getAttribute(`data-btn${ctrl}`) !== 'removed') { self.setAttribute(`data-btn${ctrl}`, 'hidden'); btn.style.display = 'none'; } break; case 'show': if (self.getAttribute(`data-btn${ctrl}`) !== 'removed') { self.setAttribute(`data-btn${ctrl}`, 'enabled'); btn.style.display = 'block'; btn.style.pointerEvents = 'auto'; btn.style.opacity = 1; btn.style.cursor = 'pointer'; } break; case 'enable': if (self.getAttribute(`data-btn${ctrl}`) !== 'removed') { if (self.getAttribute(`data-btn${ctrl}`) === 'hidden') { btn.style.display = 'block'; } self.setAttribute(`data-btn${ctrl}`, 'enabled'); btn.style.pointerEvents = 'auto'; btn.style.opacity = 1; btn.style.cursor = 'pointer'; } break; case 'remove': self.controlbar.removeChild(btn); self.setAttribute(`data-btn${ctrl}`, 'removed'); break; } if (cb) { cb.call(self, self); } return self; }; self.setControlSize = ctrlSize => { // does not work with Font Awesome webfont (only svg/js) const size = ctrlSize.toLowerCase(), icons = self.controlbar.querySelectorAll('.jsPanel-btn'); icons.forEach(icon => { ['jsPanel-btn-xl', 'jsPanel-btn-lg', 'jsPanel-btn-md', 'jsPanel-btn-sm', 'jsPanel-btn-xs'].forEach(item => icon.classList.remove(item)); icon.classList.add(`jsPanel-btn-${size}`); }); // adjust font-size of title if (size === 'xl') { self.titlebar.style.fontSize = '1.5rem'; } else if (size === 'lg') { self.titlebar.style.fontSize = '1.25rem'; } else if (size === 'md') { self.titlebar.style.fontSize = '1.05rem'; } else if (size === 'sm') { self.titlebar.style.fontSize = '.9rem'; } else if (size === 'xs') { self.titlebar.style.fontSize = '.8rem'; } }; self.setHeaderControls = cb => { // add custom controls if (self.options.headerControls.add) { let customControls = self.options.headerControls.add; if (!Array.isArray(customControls)) { // if options.addControls is not an array make it one customControls = [customControls]; } customControls.forEach(ctrl => self.addControl(ctrl)); } // get all control names including controls added with option.addControls let controls = [], ctrls = self.controlbar.querySelectorAll('.jsPanel-btn'); ctrls.forEach(ctrl => { let match = ctrl.className.match(/jsPanel-btn-[a-z\d]{3,}/i), ctrlName = match[0].substring(12); controls.push(ctrlName); }); // normalize option headerControls and reset it accordingly const option = jsPanel.pOheaderControls(options.headerControls); options.headerControls = option; // set status of controls controls.forEach(item => { if (option[item]) { self.setControlStatus(item, option[item]); } }); // set size of controls self.setControlSize(option.size); if (cb) { cb.call(self, self); } return self; }; self.setHeaderLogo = (logo, cb) => { let logos = [self.headerlogo], minPanel = document.querySelector('#' + self.id + '-min'); if (minPanel) { logos.push(minPanel.querySelector('.jsPanel-headerlogo')); } if (typeof logo === 'string') { if (!logo.startsWith('<')) { // is assumed to be an img url logos.forEach(item => { jsPanel.emptyNode(item); let img = document.createElement('img'); img.src = logo; item.append(img); }); } else { logos.forEach(item => item.innerHTML = logo); } } else { // assumed to be a node object logos.forEach(item => { jsPanel.emptyNode(item); item.append(logo); }); } // images must not be draggable, otherwise dragit interactions locks self.headerlogo.childNodes.forEach(logo => { if (logo.nodeName && logo.nodeName === 'IMG') { logo.setAttribute('draggable', 'false'); } }); if (cb) { cb.call(self, self); } return self; }; self.setHeaderRemove = cb => { self.removeChild(self.header); self.content.classList.add('jsPanel-content-noheader'); ['close', 'maximize', 'normalize', 'minimize', 'smallify'].forEach(item => self.setAttribute(`data-btn${item}`, 'removed')); if (cb) { cb.call(self, self); } return self; }; self.setHeaderTitle = (hdrTitle, cb) => { let titles = [self.headertitle], minPanel = document.querySelector('#' + self.id + '-min'); if (minPanel) { titles.push(minPanel.querySelector('.jsPanel-title')); } if (typeof hdrTitle === 'string') { titles.forEach(item => item.innerHTML = hdrTitle); } else if (typeof hdrTitle === 'function') { titles.forEach(item => { jsPanel.emptyNode(item); item.innerHTML = hdrTitle(); }); } else { // assumed to be a node object titles.forEach(item => { jsPanel.emptyNode(item); item.append(hdrTitle); }); } if (cb) { cb.call(self, self); } return self; }; self.setIconfont = (font, panel = self, cb) => { if (font) { let classArray, textArray; if (font === 'fa' || font === 'far' || font === 'fal' || font === 'fas' || font === 'fad') { classArray = [ `${font} fa-window-close`, `${font} fa-window-maximize`, `${font} fa-window-restore`, `${font} fa-window-minimize`, `${font} fa-chevron-up`, ]; } else if (font === 'material-icons') { classArray = [font, font, font, font, font, font]; textArray = ['close', 'fullscreen', 'fullscreen_exit', 'call_received', 'expand_less']; } else if (Array.isArray(font)) { classArray = [ `custom-control-icon ${font[4]}`, `custom-control-icon ${font[3]}`, `custom-control-icon ${font[2]}`, `custom-control-icon ${font[1]}`, `custom-control-icon ${font[0]}`, ]; } else if (font === 'bootstrap' || font === 'glyphicon') { classArray = [ 'glyphicon glyphicon-remove', 'glyphicon glyphicon-fullscreen', 'glyphicon glyphicon-resize-full', 'glyphicon glyphicon-minus', 'glyphicon glyphicon-chevron-up', ]; } else { return panel; } panel.querySelectorAll('.jsPanel-controlbar .jsPanel-btn').forEach(item => jsPanel.emptyNode(item).innerHTML = ''); Array.prototype.slice .call(panel.querySelectorAll('.jsPanel-controlbar .jsPanel-btn > span')) .reverse() .forEach((item, i) => { item.className = classArray[i]; if (font === 'material-icons') { item.textContent = textArray[i]; } }); } if (cb) { cb.call(panel, panel); } return panel; }; self.addToolbar = (place, tb, cb) => { if (place === 'header') { place = self.headertoolbar; } else if (place === 'footer') { place = self.footer; } if (typeof tb === 'string') { place.innerHTML = tb; } else if (Array.isArray(tb)) { tb.forEach(item => { if (typeof item === 'string') { place.innerHTML += item; } else { place.append(item); } }); } else if (typeof tb === 'function') { let tool = tb.call(self, self); if (typeof tool === 'string') { place.innerHTML = tool; } else { place.append(tool); } } else { place.append(tb); } place.classList.add('active'); if (cb) { cb.call(self, self); } return self; }; self.addCloseControl = () => { let ctrl = document.createElement('button'), colorContent = self.content.style.color; ctrl.classList.add('jsPanel-addCloseCtrl'); ctrl.innerHTML = jsPanel.icons.close; ctrl.style.color = colorContent; if (self.options.rtl) { ctrl.classList.add('rtl'); } self.appendChild(ctrl); jsPanel.pointerup.forEach(evt => { ctrl.addEventListener(evt, e => { e.preventDefault(); // disable close for all mouse buttons but left if (e.button && e.button > 0) { return false; } self.close(null, true); }); }); // pointerdown handler needed to prevent side effect with resize handles jsPanel.pointerdown.forEach(evt => { ctrl.addEventListener(evt, e => e.preventDefault()); }); return self; }; self.addControl = obj => { // obj.name - string, the name of the control, results for example in "jsPanel-btn jsPanel-btn-menu" // obj.html - string, the html of the control to add, for example "" // obj.handler - function(panel, control), the event handler to assign to the new control // obj.position - number, the position within the controlbar where the control is to be added if (!obj.html) { return self; } if (!obj.position) { obj.position = 1; } const count = self.controlbar.querySelectorAll('.jsPanel-btn').length; let control = document.createElement('button'); control.innerHTML = obj.html; control.className = `jsPanel-btn jsPanel-btn-${obj.name} jsPanel-btn-${options.headerControls.size}`; control.style.color = self.header.style.color; if (obj.position > count) { // new control is the first from right (default ltr text-direction) or the first from left (if option rtl is used) self.controlbar.append(control); } else { self.controlbar.insertBefore(control, self.querySelector(`.jsPanel-controlbar .jsPanel-btn:nth-child(${obj.position})`)); } const ariaLabel = obj.ariaLabel || obj.name; if (ariaLabel) { control.setAttribute('aria-label', ariaLabel); } jsPanel.pointerup.forEach(evt => { control.addEventListener(evt, e => { e.preventDefault(); if (e.button && e.button > 0) { return false; } obj.handler.call(self, self, control); }); }); if (obj.afterInsert) { obj.afterInsert.call(control, control); } return self; }; self.setRtl = () => { [self.header, self.content, self.footer].forEach(item => { item.dir = 'rtl'; if (options.rtl.lang) { item.lang = options.rtl.lang; } }); }; // option.id self.id = options.id; // option.paneltype classname self.classList.add('jsPanel-' + options.paneltype); // set z-index and paneltype class if (options.paneltype === 'standard') { self.style.zIndex = this.zi.next(); } // option.container panelContainer.append(self); self.front(false, false); // just to ensure iframe code in self.front() works for very first panel as well, second false prevents onfronted callbacks to be executed // option.theme self.setTheme(options.theme); // option.boxShadow if (options.boxShadow) { self.classList.add(`jsPanel-depth-${options.boxShadow}`); } /* option.header, option.iconfont, option.headerControls, option.headerLogo, option.headerTitle */ if (options.header) { if (options.headerLogo) { self.setHeaderLogo(options.headerLogo); } self.setIconfont(options.iconfont); self.setHeaderTitle(options.headerTitle); self.setHeaderControls(); // now handles controls added with option addControls as well // compatibility code for IE11 due to flex bug - https://caniuse.com/#feat=flexbox if (jsPanel.isIE) { let bars = [self.headerbar, self.controlbar]; switch (self.options.headerControls.size) { case 'md': bars.forEach(bar => { bar.style.height = '34px'; }); break; case 'xs': bars.forEach(bar => { bar.style.height = '26px'; }); break; case 'sm': bars.forEach(bar => { bar.style.height = '30px'; }); break; case 'lg': bars.forEach(bar => { bar.style.height = '38px'; }); break; case 'xl': bars.forEach(bar => { bar.style.height = '42px'; }); break; } } // end - - - - - - - - - if (options.header === 'auto-show-hide') { let boxShadow = 'jsPanel-depth-' + options.boxShadow; self.header.style.opacity = 0; self.style.backgroundColor = 'transparent'; this.remClass(self, boxShadow); this.setClass(self.content, boxShadow); self.header.addEventListener('mouseenter', () => { self.header.style.opacity = 1; jsPanel.setClass(self, boxShadow); jsPanel.remClass(self.content, boxShadow); }); self.header.addEventListener('mouseleave', () => { self.header.style.opacity = 0; jsPanel.remClass(self, boxShadow); jsPanel.setClass(self.content, boxShadow); }); } } else { self.setHeaderRemove(); if (options.addCloseControl) { self.addCloseControl(); } } // option.headerToolbar if (options.headerToolbar) { self.addToolbar(self.headertoolbar, options.headerToolbar); } // option.footerToolbar if (options.footerToolbar) { self.addToolbar(self.footer, options.footerToolbar); } // option.border if (options.border) { self.setBorder(options.border); } // option.borderRadius if (options.borderRadius) { self.setBorderRadius(options.borderRadius); } // option.css - add custom css classes to the panel html if (options.css) { for (const [selector, classname] of Object.entries(options.css)) { // option is a string used to build the desired class selector like `.jsPanel-${option}` except for the outermost DIV where option must be simply 'panel' // value is a string with either a single class name or a space separated list of class names like 'classA classB classC' if (selector === 'panel') { // handles the special case outermost DIV of the panel self.className += ` ${classname}`; // don't remove space at the beginning of template string } else { // handles all other elements within the panel template let elmt = self.querySelector(`.jsPanel-${selector}`); if (elmt) { elmt.className += ` ${classname}`; // don't remove space at the beginning of template string } } } } // option.content if (options.content) { if (typeof options.content === 'function') { options.content.call(self, self); } else if (typeof options.content === 'string') { self.content.innerHTML = options.content; } else { self.content.append(options.content); } } // option.contentAjax if (options.contentAjax) { this.ajax(options.contentAjax, self); } // option.contentFetch if (options.contentFetch) { this.fetch(options.contentFetch, self); } // option.contentOverflow if (options.contentOverflow) { const value = options.contentOverflow.split(' '); if (value.length === 1) { self.content.style.overflow = value[0]; } else if (value.length === 2) { self.content.style.overflowX = value[0]; self.content.style.overflowY = value[1]; } } // option.autoclose -- should be before option.size if (options.autoclose) { if (typeof options.autoclose === 'number') { options.autoclose = { time: options.autoclose + 'ms' }; } else if (typeof options.autoclose === 'string') { options.autoclose = { time: options.autoclose }; } let autoclose = Object.assign({}, jsPanel.defaultAutocloseConfig, options.autoclose); if (autoclose.time && typeof autoclose.time === 'number') { autoclose.time += 'ms'; } let slider = self.progressbar.querySelector('div'); slider.addEventListener('animationend', e => { e.stopPropagation(); self.progressbar.classList.remove('active'); self.close(); }); if (autoclose.progressbar) { self.progressbar.classList.add('active'); if (autoclose.background) { if (jsPanel.colorNames[autoclose.background]) { self.progressbar.style.background = '#' + jsPanel.colorNames[autoclose.background]; } else { self.progressbar.style.background = autoclose.background; } } else { self.progressbar.classList.add('success-bg'); // default background for progressbar } } slider.style.animation = `${autoclose.time} progressbar`; } // option.rtl if (options.rtl) { self.setRtl(); } // option.size -- should be after option.theme self.setSize(); // option.position self.status = 'normalized'; // if option.position evaluates to false panel will not be positioned at all if (options.position) { this.position(self, options.position); } else { self.style.opacity = 1; } document.dispatchEvent(jspanelnormalized); self.calcSizeFactors(); // option.animateIn if (options.animateIn) { // remove class again on animationend, otherwise opacity doesn't change when panel is dragged self.addEventListener('animationend', () => { this.remClass(self, options.animateIn); }); this.setClass(self, options.animateIn); } // option.dragit AND option.resizeit AND option.syncMargins if (options.syncMargins) { let containment = this.pOcontainment(options.maximizedMargin); if (options.dragit) { options.dragit.containment = containment; if (options.dragit.snap === true) { // options.dragit.snap must be object in order to set options.dragit.snap.containment = true; options.dragit.snap = jsPanel.defaultSnapConfig; options.dragit.snap.containment = true; } else if (options.dragit.snap) { options.dragit.snap.containment = true; } } if (options.resizeit) { options.resizeit.containment = containment; } } if (options.dragit) { // callbacks must be an array of function(s) in order to be able to dynamically add/remove callbacks (for example in extensions) ['start', 'drag', 'stop'].forEach(item => { if (options.dragit[item]) { if (typeof options.dragit[item] === 'function') { options.dragit[item] = [options.dragit[item]]; } } else { options.dragit[item] = []; } }); self.drag(options.dragit); // do not use self.options.dragit.stop.push() !!! self.addEventListener('jspaneldragstop', e => { if (e.panel === self) {self.calcSizeFactors();} }, false); } else { self.titlebar.style.cursor = 'default'; } if (options.resizeit) { // callbacks must be an array of function(s) in order to be able to dynamically add/remove callbacks (for example in extensions) ['start', 'resize', 'stop'].forEach(item => { if (options.resizeit[item]) { if (typeof options.resizeit[item] === 'function') { options.resizeit[item] = [options.resizeit[item]]; } } else { options.resizeit[item] = []; } }); self.sizeit(options.resizeit); let startstatus = void 0; // do not use self.options.resizeit.start.push() !!! self.addEventListener('jspanelresizestart', e => { if (e.panel === self) {startstatus = self.status;} }, false); // do not use self.options.resizeit.stop.push() !!! self.addEventListener( 'jspanelresizestop', e => { if (e.panel === self) { if ( (startstatus === 'smallified' || startstatus === 'smallifiedmax' || startstatus === 'maximized') && parseFloat(self.style.height) > parseFloat(window.getComputedStyle(self.header).height) ) { self.setControls(['.jsPanel-btn-normalize']); self.status = 'normalized'; document.dispatchEvent(jspanelnormalized); document.dispatchEvent(jspanelstatuschange); if (options.onstatuschange) { jsPanel.processCallbacks(self, options.onstatuschange, 'every'); } self.calcSizeFactors(); } } }, false ); } // initialize self.currentData - must be after options position & size //self.saveCurrentDimensions(true); // not clear why 'true' was added here or why this param is needed at all in the method self.saveCurrentDimensions(); self.saveCurrentPosition(); // option.setStatus if (options.setStatus) { if (options.setStatus === 'smallifiedmax') { self.maximize().smallify(); } else if (options.setStatus === 'smallified') { self.smallify(); } else { // remove last char ('d') from end of string to get function name to call self[options.setStatus.slice(0, -1)](); } } // front panel on mousedown this.pointerdown.forEach(item => { self.addEventListener( item, e => { if (!e.target.closest('.jsPanel-btn-close') && !e.target.closest('.jsPanel-btn-minimize') && options.paneltype === 'standard') { self.front(); } }, false ); }); // option onwindowresize if (options.onwindowresize) { // if container is 'window' if (self.options.container === 'window') { window.addEventListener('resize', self.windowResizeHandler, false); } } // option onparentresize if (options.onparentresize) { let onResize = options.onparentresize, settingType = typeof onResize, parentPanel = self.isChildpanel(); if (parentPanel) { const parentContainer = parentPanel.content; let parentContainerSize = []; self.parentResizeHandler = e => { // if resized panel is the parent panel of the one whose option onContentResize is set to true if (e.panel === parentPanel) { // get dimensions of parent panel's content section parentContainerSize[0] = parentContainer.offsetWidth; parentContainerSize[1] = parentContainer.offsetHeight; let status = self.status, left, top; if (status === 'maximized' && onResize) { self.maximize(); } else if (self.snapped && status !== 'minimized') { self.snap(self.snapped, true); } else if (status === 'normalized' || status === 'smallified' || status === 'maximized') { if (settingType === 'function') { onResize.call(self, self, { width: parentContainerSize[0], height: parentContainerSize[1], }); } else if (settingType === 'object' && onResize.preset === true) { left = (parentContainerSize[0] - self.offsetWidth) * self.hf; self.style.left = left <= 0 ? 0 : left + 'px'; top = (parentContainerSize[1] - self.offsetHeight) * self.vf; self.style.top = top <= 0 ? 0 : top + 'px'; onResize.callback.call(self, self, { width: parentContainerSize[0], height: parentContainerSize[1], }); } else if (settingType === 'boolean') { left = (parentContainerSize[0] - self.offsetWidth) * self.hf; self.style.left = left <= 0 ? 0 : left + 'px'; top = (parentContainerSize[1] - self.offsetHeight) * self.vf; self.style.top = top <= 0 ? 0 : top + 'px'; } } else if (status === 'smallifiedmax' && onResize) { self.maximize().smallify(); } } }; document.addEventListener('jspanelresize', self.parentResizeHandler, false); } } // global callbacks if (this.globalCallbacks) { if (Array.isArray(this.globalCallbacks)) { this.globalCallbacks.forEach(item => item.call(self, self)); } else { this.globalCallbacks.call(self, self); } } // option.callback if (options.callback) { if (Array.isArray(options.callback)) { options.callback.forEach(item => item.call(self, self)); } else { options.callback.call(self, self); } } // constructor callback // if (cb) {cb.call(self, self);} if (cb) { if (Array.isArray(cb)) { cb.forEach(item => item.call(self, self)); } else { cb.call(self, self); } } document.dispatchEvent(jspanelloaded); return self; }, };