mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-29 08:06:58 +09:00
* chore: replace removed jQuery $.isFunction() with typeof checks jQuery 4.0 removes $.isFunction(). Swap the three call sites in navigationBox.js and navigationBox.tree.js for behaviour-identical 'typeof x === function' checks, removing the only jQuery-4-removed API in Cacti's own front-end code. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix: correct typeof negation in the mark.js availability check Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
1315 lines
51 KiB
JavaScript
1315 lines
51 KiB
JavaScript
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDTool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
window.midwinter = window.midwinter || {};
|
|
midwinter.navigationBox = {
|
|
_plugins: {},
|
|
|
|
/**
|
|
* registers a plugin by its namespace path
|
|
* @param {string} path - string path to the plugin
|
|
*/
|
|
registerPlugin: function (path) {
|
|
const parts = path.split('.');
|
|
const plugin = parts.reduce((obj, key) => (obj && obj[key] ? obj[key] : null), window);
|
|
|
|
if (plugin && typeof plugin.getDefaultConfig === 'function') {
|
|
// Check if the plugin is actually allowed to run (e.g., dependency check)
|
|
const config = plugin.getDefaultConfig();
|
|
|
|
if (config !== null) {
|
|
const type = parts.pop();
|
|
this._plugins[type] = plugin;
|
|
// console.log(`[Plugin] Registered: ${type}`);
|
|
} else {
|
|
// Plugin aborted registration itself due to missing dependencies
|
|
console.warn(`[Plugin] Registration aborted for ${path} (dependencies not met).`);
|
|
}
|
|
}
|
|
},
|
|
|
|
buildConfigs: function (boxDefinitions) {
|
|
return boxDefinitions.map(def => {
|
|
if (def.type && this._plugins[def.type]) {
|
|
// get the default config from the plugin
|
|
const config = this._plugins[def.type].getDefaultConfig(def.overrides || {});
|
|
if (config) {
|
|
// we just ensure the type is set so add() can pick it up
|
|
config.pluginType = def.type;
|
|
}
|
|
return config;
|
|
}
|
|
return def;
|
|
}).filter(config => config !== null);
|
|
},
|
|
|
|
refreshPlugins: function() {
|
|
Object.keys(this._plugins).forEach(type => {
|
|
const plugin = this._plugins[type];
|
|
|
|
// Only target boxes that have the data-refresh attribute set to true
|
|
$(`[data-plugin="${type}"][data-refresh="true"]`).each((index, el) => {
|
|
const $box = $(el);
|
|
|
|
if (typeof plugin.content === 'function') {
|
|
$box.find('.navBox-content').html(plugin.content($box));
|
|
}
|
|
if (typeof plugin.init === 'function') {
|
|
plugin.init($box);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
class cactiNavigation {
|
|
#refreshStorage = false;
|
|
#prefix = 'mdw';
|
|
#storageKey = 'navigationBox';
|
|
|
|
target;
|
|
|
|
#nav = {
|
|
refs: {},
|
|
session: {},
|
|
storage: typeof Storages !== 'undefined' ? Storages.localStorage : null
|
|
};
|
|
|
|
#navOptions = {
|
|
container: `${this.#prefix}-SideBarContainer`,
|
|
|
|
dock: {
|
|
enabled: true,
|
|
wrapper: `${this.#prefix}-Main`,
|
|
top: true, left: true, right: true, bottom: true
|
|
},
|
|
|
|
overlay: {enabled: true, left: true, right: true},
|
|
window: {enabled: true, wrapper: `${this.#prefix}-Windows`}
|
|
};
|
|
|
|
constructor(options = {}) {
|
|
/* identify caller */
|
|
this.target = new.target;
|
|
|
|
/* restore local storage */
|
|
this._restoreLocalStorage();
|
|
|
|
/* setup limited to parent instance only */
|
|
if (this.target === cactiNavigation) {
|
|
this._validateOptions(options, this.#navOptions);
|
|
this.#nav.session = $.extend(true, {}, this.#navOptions, this.#nav.session);
|
|
this._checkContainers();
|
|
this._initDockResizables();
|
|
this._applySavedSplits();
|
|
this._refreshLocalStorage(this.#refreshStorage);
|
|
}
|
|
}
|
|
|
|
_restoreLocalStorage() {
|
|
if (this.#nav.storage?.isSet(this.#storageKey)) {
|
|
try {
|
|
const data = this.#nav.storage.get(this.#storageKey);
|
|
this.#nav.session = typeof data === 'string' ? JSON.parse(data) : data;
|
|
// Handle potential double stringification
|
|
if (typeof this.#nav.session === 'string') {
|
|
this.#nav.session = JSON.parse(this.#nav.session);
|
|
}
|
|
} catch (e) {
|
|
console.error("Navigation: Storage data corrupted.");
|
|
}
|
|
}
|
|
|
|
if (!this._isObject(this.#nav.session)) {
|
|
this.#nav.session = $.extend(true, {}, this.#navOptions);
|
|
this._refreshLocalStorage(true);
|
|
}
|
|
}
|
|
|
|
_refreshLocalStorage(force = false) {
|
|
if (force === true && this.#nav.storage) {
|
|
this.#nav.storage.set(this.#storageKey, JSON.stringify(this.#nav.session));
|
|
}
|
|
}
|
|
|
|
_checkContainers() {
|
|
const session = this.#nav.session;
|
|
this._ensureContainer(session.container);
|
|
|
|
if (session.dock.enabled) {
|
|
const $wrapper = $(`#${session.dock.wrapper}`);
|
|
if ($wrapper.length) {
|
|
const positions = ["top", "left", "right", "bottom"];
|
|
for (const [pos, value] of Object.entries(session.dock)) {
|
|
if (positions.includes(pos) && value === true) {
|
|
const name = `${this.#prefix}-Dock${this._ucFirst(pos)}`;
|
|
const dirs = ['left', 'right'].includes(pos) ? ['Top', 'Bottom'] : ['Left', 'Right'];
|
|
const innerHtml = dirs.map(d => `<div class="${this.#prefix}-DockInner${d}"></div>`).join('');
|
|
this._ensureContainer(name, $wrapper, innerHtml);
|
|
}
|
|
}
|
|
} else {
|
|
session.dock.enabled = false;
|
|
this.#refreshStorage = true;
|
|
}
|
|
}
|
|
|
|
// restore dock with / height
|
|
const savedSizes = this.#nav.session.dockSizes || {};
|
|
|
|
for (const [dockId, size] of Object.entries(savedSizes)) {
|
|
const $dock = $(`#${this.#prefix}-Dock${dockId}`);
|
|
if ($dock.length) {
|
|
const dimension = (dockId === 'Top' || dockId === 'Bottom') ? 'height' : 'width';
|
|
$dock.css(dimension, size);
|
|
}
|
|
}
|
|
}
|
|
|
|
_initDockResizables() {
|
|
const prefix = this.#prefix;
|
|
const self = this;
|
|
|
|
const docks = [
|
|
{ id: 'Top', handle: 's', dim: 'height' },
|
|
{ id: 'Bottom', handle: 'n', dim: 'height' },
|
|
{ id: 'Left', handle: 'e', dim: 'width' },
|
|
{ id: 'Right', handle: 'w', dim: 'width' }
|
|
];
|
|
|
|
docks.forEach(dock => {
|
|
const $el = $(`#${prefix}-Dock${dock.id}`);
|
|
if (!$el.length) return;
|
|
|
|
$el.resizable({
|
|
handles: dock.handle,
|
|
stop: function(event, ui) {
|
|
|
|
const newVal = ui.size[dock.dim] + 'px';
|
|
|
|
if (!self.#nav.session.dockSizes) self.#nav.session.dockSizes = {};
|
|
self.#nav.session.dockSizes[dock.id] = newVal;
|
|
|
|
self._refreshLocalStorage(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
// built dock container split
|
|
$(`.${prefix}-DockInnerTop, .${prefix}-DockInnerLeft`).each(function() {
|
|
const $this = $(this);
|
|
const isVerticalSplit = $this.hasClass(`${prefix}-DockInnerTop`);
|
|
const handle = isVerticalSplit ? 's' : 'e';
|
|
const dimension = isVerticalSplit ? 'height' : 'width';
|
|
|
|
$this.resizable({
|
|
handles: handle,
|
|
resize: function(event, ui) {
|
|
const $parent = $this.parent();
|
|
|
|
const parentSize = isVerticalSplit ? $parent.innerHeight() : $parent.innerWidth();
|
|
const currentSize = isVerticalSplit ? $this.outerHeight() : $this.outerWidth();
|
|
|
|
// calculate split percentage value of split position in relation to outer width or height
|
|
// this is important to keep relation if user resizes the browser window (e.g. switch to fullscreen)
|
|
let percent = (currentSize / parentSize) * 100;
|
|
percent = Math.max(10, Math.min(90, percent)); // Safety-Margin
|
|
|
|
// fix position in DOM
|
|
$this.css(dimension, percent + '%');
|
|
$this.siblings().css(dimension, (100 - percent) + '%');
|
|
|
|
// keep layout persistent
|
|
const dockId = $parent.attr('id');
|
|
if (!self.#nav.session.dockSplits) self.#nav.session.dockSplits = {};
|
|
self.#nav.session.dockSplits[dockId] = percent;
|
|
},
|
|
stop: function(event, ui) {
|
|
// avoid updating local storage too often
|
|
self._refreshLocalStorage(true);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
_applySavedSplits() {
|
|
const splits = this.getConfig('dockSplits');
|
|
if (!splits || !this._isObject(splits)) return;
|
|
|
|
for (const [dockId, percent] of Object.entries(splits)) {
|
|
const $dock = $(`#${dockId}`);
|
|
if (!$dock.length) continue;
|
|
|
|
const isVerticalDock = dockId.includes('Left') || dockId.includes('Right');
|
|
const firstClass = isVerticalDock ? 'Top' : 'Left';
|
|
const secondClass = isVerticalDock ? 'Bottom' : 'Right';
|
|
const dimension = isVerticalDock ? 'height' : 'width';
|
|
|
|
const $first = $dock.find(`.${this.#prefix}-DockInner${firstClass}`);
|
|
const $second = $dock.find(`.${this.#prefix}-DockInner${secondClass}`);
|
|
|
|
if ($first.length && $second.length) {
|
|
$first.css(dimension, percent + '%');
|
|
$second.css(dimension, (100 - percent) + '%');
|
|
}
|
|
}
|
|
}
|
|
|
|
_ensureContainer(id, parent = 'body', content = '') {
|
|
let $el = $(`#${id}`);
|
|
if (!$el.length) {
|
|
$el = $(`<div id="${id}" class="${id}">${content}</div>`).appendTo(parent);
|
|
}
|
|
return $el;
|
|
}
|
|
|
|
_validateOptions(options, defaults) {
|
|
if (!this._isObject(options)) return;
|
|
|
|
for (const [key, value] of Object.entries(options)) {
|
|
if (defaults.hasOwnProperty(key)) {
|
|
const defaultType = typeof defaults[key];
|
|
const valueType = typeof value;
|
|
|
|
// Allow type match OR allow an object if the default was a string
|
|
// (to support our {on, off} icon/tooltip logic)
|
|
if (defaultType === valueType || (defaultType === 'string' && valueType === 'object')) {
|
|
|
|
if (this._isObject(value) && this._isObject(defaults[key])) {
|
|
// Deep merge for nested objects (like 'dock' or 'layout')
|
|
this._validateOptions(options[key], defaults[key]);
|
|
} else {
|
|
// Direct assignment for primitives or our special toggle-objects
|
|
defaults[key] = value;
|
|
if (this.target === cactiNavigation) {
|
|
this.#refreshStorage = true;
|
|
}
|
|
}
|
|
} else {
|
|
console.warn(`Navigation: Type mismatch for property '${key}'. Expected ${defaultType}, got ${valueType}.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* helper & utility methods */
|
|
|
|
_readConfigOption(obj, option = '', defVal = false) {
|
|
const session = this.#nav.session;
|
|
if (session.hasOwnProperty(obj)) {
|
|
if (option === '') return session[obj];
|
|
if (session[obj].hasOwnProperty(option)) return session[obj][option];
|
|
}
|
|
return defVal;
|
|
}
|
|
|
|
_isFunction(funcName) {
|
|
if (!funcName || funcName === '') return false;
|
|
|
|
// check local instance method
|
|
if (typeof this[funcName] === 'function') return true;
|
|
|
|
// helper to resolve path in a root object
|
|
const resolvePath = (path, root) => {
|
|
if (!root) return undefined;
|
|
return path.split('.').reduce((prev, curr) => prev ? prev[curr] : undefined, root);
|
|
};
|
|
|
|
// check in registry
|
|
if (typeof registry !== 'undefined') {
|
|
if (typeof resolvePath(funcName, registry) === 'function') return true;
|
|
}
|
|
|
|
// check in window scope (supporting dot notation)
|
|
return typeof resolvePath(funcName, window) === 'function';
|
|
}
|
|
|
|
_runFunction(path, args = null) {
|
|
if (!path || typeof path !== 'string') return '';
|
|
|
|
try {
|
|
let func = undefined;
|
|
let context = null;
|
|
|
|
// search in current class instance
|
|
if (typeof this[path] === 'function') {
|
|
func = this[path];
|
|
context = this;
|
|
}
|
|
|
|
// helper to resolve path and keep track of the parent (context)
|
|
const resolvePath = (root) => {
|
|
let parent = null;
|
|
const target = path.split('.').reduce((obj, key) => {
|
|
parent = obj;
|
|
return (obj && typeof obj === 'object') ? obj[key] : undefined;
|
|
}, root);
|
|
return { target, parent };
|
|
};
|
|
|
|
// search in registry
|
|
if (typeof func !== 'function' && typeof registry !== 'undefined') {
|
|
const res = resolvePath(registry);
|
|
func = res.target;
|
|
context = res.parent;
|
|
}
|
|
|
|
// fallback: window scope
|
|
if (typeof func !== 'function') {
|
|
const res = resolvePath(window);
|
|
func = res.target;
|
|
context = res.parent;
|
|
}
|
|
|
|
// execution
|
|
if (typeof func === 'function') {
|
|
// use apply to maintain context and support multiple arguments (like key, opt)
|
|
const finalArgs = Array.isArray(args) ? args : [args];
|
|
return func.apply(context, finalArgs);
|
|
} else {
|
|
console.warn(`Navigation: Path '${path}' not found.`);
|
|
}
|
|
} catch (e) {
|
|
console.error(`Navigation: Error executing function at path '${path}':`, e);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
_runKeyEvent(event) {
|
|
// Only trigger on Enter (13) or Space (32)
|
|
if (!(event.keyCode === 13 || event.keyCode === 32)) return;
|
|
event.preventDefault();
|
|
|
|
const funcName = event.data.function;
|
|
if (typeof window[funcName] === 'function') {
|
|
window[funcName](event);
|
|
}
|
|
}
|
|
|
|
_isObject(obj) {
|
|
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
|
|
}
|
|
|
|
_ucFirst(str) {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
|
|
_getBox(helper) {
|
|
return $(`[class^="${this.#prefix}-ConsoleNavigationBox"][data-helper="${helper}"]`);
|
|
}
|
|
|
|
_getButton(helper) {
|
|
return $(`[class^="compact_nav_icon"][data-helper="${helper}"]`);
|
|
}
|
|
|
|
getConfig(path = '') {
|
|
return path.split('.').reduce((obj, key) => obj?.[key], this.#nav.session);
|
|
}
|
|
|
|
_updateNavigationBox($box, status = null) {
|
|
if (typeof status === "boolean") {
|
|
$box.toggleClass('visible', status);
|
|
} else {
|
|
$box.toggleClass('visible');
|
|
}
|
|
}
|
|
|
|
_updateNavigationButton(helper, status = false) {
|
|
// uses the existing _getButton helper
|
|
this._getButton(helper).toggleClass('selected', status);
|
|
}
|
|
|
|
_hideSiblings($navigationBox) {
|
|
const helper = $navigationBox.attr('data-helper');
|
|
// find all other visible boxes in the same container and hide them
|
|
$navigationBox.parent().find(`[class^='${this.#prefix}-ConsoleNavigationBox']:not([data-helper='${helper}'])`)
|
|
.each((_, el) => {
|
|
const $el = $(el);
|
|
$el.removeClass('visible').attr('data-status', 'closed');
|
|
this._updateNavigationButton($el.attr('data-helper'), false);
|
|
// ensure persistence
|
|
window.dispatchEvent(new CustomEvent('box:stateChanged', {
|
|
detail: {
|
|
helper: $el.attr('data-helper'),
|
|
attributes: ['status']
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
|
|
_checkDockInnerSiblings($destination) {
|
|
const $sibling = $destination.siblings().eq(0);
|
|
|
|
// combined check: Box must be logically OPEN and visually VISIBLE
|
|
const hasChildVisible = $destination.find('[data-status="open"]').length > 0;
|
|
const hasNieceVisible = $sibling.find('[data-status="open"]').length > 0;
|
|
|
|
$sibling.toggleClass('noSibling', !hasChildVisible && hasNieceVisible);
|
|
$destination.toggleClass('noSibling', hasChildVisible && !hasNieceVisible);
|
|
}
|
|
|
|
checkConfigurationIntegrity(boxConfigs, btnConfigs) {
|
|
const boxHelpers = boxConfigs.map(b => b.helper);
|
|
const btnHelpers = btnConfigs.map(b => b.helper);
|
|
|
|
// CRITICAL: Check for Boxes that have no Button to open them
|
|
boxHelpers.forEach(helper => {
|
|
if (!btnHelpers.includes(helper)) {
|
|
console.error(`Navigation Integrity Error: Box '${helper}' is defined but has no corresponding Button. It will be unreachable in the UI!`);
|
|
}
|
|
});
|
|
|
|
// INFO: Buttons without boxes are allowed (standalone actions)
|
|
btnHelpers.forEach(helper => {
|
|
if (!boxHelpers.includes(helper)) {
|
|
console.info(`Navigation Info: Button '${helper}' is a standalone action (no box linked).`);
|
|
}
|
|
});
|
|
}
|
|
|
|
toggleBox(helper, action = 'toggle', filter = null) {
|
|
const $box = this._getBox(helper);
|
|
if (!$box.length) return;
|
|
|
|
const style = $box.attr('data-style');
|
|
const isExclusive = $box.attr('data-exclusive') !== 'false'; // Default to true
|
|
const currentStatus = $box.attr('data-status');
|
|
const isVisible = currentStatus === 'open';
|
|
|
|
// determine if we are about to open the box
|
|
const opening = (action === 'force_open' || (action === 'toggle' && !isVisible));
|
|
|
|
if (opening) {
|
|
if (style === 'dock') {
|
|
this._hideSiblings($box);
|
|
} else if (isExclusive) {
|
|
this.closeAllBoxes(helper); // Close other overlays
|
|
}
|
|
|
|
$box.addClass('visible');
|
|
$box.attr('data-status', 'open');
|
|
this._updateNavigationButton(helper, true);
|
|
|
|
// Handle search filter if provided
|
|
if (filter) {
|
|
const $searchField = $box.find("input[name='navBox-header-search']");
|
|
$box.find('.navBox-header-search').removeClass('hide');
|
|
if (filter !== 'reset') {
|
|
$searchField.focus().val(filter).trigger('input');
|
|
} else {
|
|
$searchField.val('').trigger('input').blur();
|
|
}
|
|
}
|
|
} else {
|
|
// closing logic
|
|
$box.removeClass('visible');
|
|
$box.attr('data-status', 'closed');
|
|
this._updateNavigationButton(helper, false);
|
|
}
|
|
|
|
// Always check siblings for dock style
|
|
if (style === 'dock') {
|
|
this._checkDockInnerSiblings($box.parent());
|
|
}
|
|
|
|
if (style === 'dock' || style === 'window') {
|
|
// synchronize internal cache and storage of a boxes with static style type
|
|
window.dispatchEvent(new CustomEvent('box:stateChanged', {
|
|
detail: {
|
|
helper: helper,
|
|
attributes: ['status']
|
|
}
|
|
}));
|
|
|
|
}
|
|
}
|
|
|
|
closeAllBoxes(excludeHelper = null) {
|
|
// Only close overlays, as docked boxes are usually permanent
|
|
$(`[class*="ConsoleNavigationBox"][data-style="overlay"].visible`).each((_, el) => {
|
|
const helper = $(el).attr('data-helper');
|
|
if (helper !== excludeHelper) {
|
|
this.toggleBox(helper, 'force_close');
|
|
}
|
|
});
|
|
}
|
|
|
|
setBoxPresence(helper, visible) {
|
|
const $box = this._getBox(helper);
|
|
if (!$box.length) return;
|
|
|
|
const currentStatus = $box.attr('data-status'); // 'open' or 'closed'
|
|
const isUserOpen = (currentStatus === 'open');
|
|
|
|
if (visible) {
|
|
// CONTENT IS THERE:
|
|
// Only show if the user actually wants it open (respect storage)
|
|
if (isUserOpen) {
|
|
this._updateNavigationBox($box, true);
|
|
}
|
|
} else {
|
|
// NO CONTENT:
|
|
// Hide visually, but DO NOT change data-status or fire events
|
|
this._updateNavigationBox($box, false);
|
|
}
|
|
|
|
// Always update Dock layout if it's a dock style
|
|
if ($box.attr('data-style') === 'dock') {
|
|
this._checkDockInnerSiblings($box.parent());
|
|
}
|
|
}
|
|
}
|
|
|
|
class cactiButton extends cactiNavigation {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
add(options = {}) {
|
|
|
|
const config = {
|
|
title : '',
|
|
helper : '',
|
|
tooltip : '',
|
|
iconClass : '',
|
|
destination : '',
|
|
onclick : 'auto',
|
|
param : 'on',
|
|
enabled : true,
|
|
};
|
|
|
|
// validate and merge
|
|
this._validateOptions(options, config);
|
|
|
|
// early exit if button is disabled (e.g., permissions)
|
|
if (config.enabled === false) return this;
|
|
|
|
// handle potential object for toggling icons
|
|
const icon = typeof config.iconClass === 'object' ? config.iconClass.off : config.iconClass;
|
|
const tooltip = typeof config.tooltip === 'object' ? config.tooltip.off : config.tooltip;
|
|
|
|
// use 'toggleConsoleNavigationBox' if set to 'auto'
|
|
const clickFunc = config.onclick === 'auto' ? 'toggleConsoleNavigationBox' : config.onclick;
|
|
|
|
// build HTML
|
|
const containerHtml = `
|
|
<div class="compact_nav_icon hint--info hint--right hint--rounded"
|
|
data-subtitle="${config.title}"
|
|
data-helper="${config.helper}"
|
|
aria-label="${tooltip}"
|
|
role="button"
|
|
tabindex="0"
|
|
aria-pressed="false">
|
|
<i class="${icon}"></i>
|
|
</div>`;
|
|
|
|
const $destination = $(config.destination);
|
|
if (!$destination.length) {
|
|
console.warn(`Button: Destination '${config.destination}' not found for '${config.helper}'.`);
|
|
return this;
|
|
}
|
|
|
|
// Avoid Duplicates and Append
|
|
let $button = $destination.find(`[data-helper="${config.helper}"]`);
|
|
|
|
if ($button.length === 0) {
|
|
$button = $(containerHtml).appendTo($destination);
|
|
|
|
// Bind Events
|
|
if (this._isFunction(clickFunc)) {
|
|
$button.on("click", { param: config.param }, (e) => {
|
|
// First priority: Method on this instance (like toggleConsoleNavigationBox)
|
|
if (typeof this[clickFunc] === 'function') {
|
|
this[clickFunc](e);
|
|
}
|
|
// Second priority: Global window function
|
|
else if (typeof window[clickFunc] === 'function') {
|
|
window[clickFunc](e);
|
|
}
|
|
});
|
|
|
|
$button.on("keydown", {
|
|
param: config.param,
|
|
function: clickFunc
|
|
}, (e) => this._runKeyEvent(e));
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
show(helper) {
|
|
this._getButton(helper).removeClass('hide');
|
|
return this;
|
|
}
|
|
|
|
hide(helper) {
|
|
this._getButton(helper).addClass('hide');
|
|
return this;
|
|
}
|
|
|
|
toggleConsoleNavigationBox(event) {
|
|
const $caller = $(event.currentTarget);
|
|
const helper = $caller.attr('data-helper');
|
|
const param = event.data?.param; // 'on', 'force_open', 'force_close'
|
|
const filter = event.data?.filter;
|
|
|
|
/* hide any open context menus */
|
|
$('.context-menu-list').trigger('contextmenu:hide');
|
|
|
|
// map the param to our new toggleBox actions
|
|
let action = 'toggle';
|
|
if (param === 'force_open') action = 'force_open';
|
|
if (param === 'force_close') action = 'force_close';
|
|
if (param === 'on' && $caller.hasClass('selected')) action = 'force_close';
|
|
if (param === 'on' && !$caller.hasClass('selected')) action = 'force_open';
|
|
|
|
// execute central logic
|
|
this.toggleBox(helper, action, filter);
|
|
|
|
// re-initialize resizable if box just became visible
|
|
const $box = this._getBox(helper);
|
|
if ($box.hasClass('visible') && $box.attr('data-style') === 'overlay' && typeof $.fn.resizable === 'function') {
|
|
$box.resizable({ handles: 'e' });
|
|
}
|
|
}
|
|
}
|
|
|
|
class cactiBox extends cactiNavigation {
|
|
|
|
#searchTimer = null;
|
|
|
|
constructor(options = {}) {
|
|
super();
|
|
|
|
// dynamic prefix detection
|
|
this.prefix = this.getConfig('container').split('-')[0];
|
|
|
|
// merge options over defaults
|
|
const config = {
|
|
className: 'ConsoleNavigationBox',
|
|
...options
|
|
};
|
|
|
|
// same classes for all boxes
|
|
this.baseClass = `${this.prefix}${config.className}`;
|
|
this.className = config.className;
|
|
|
|
// unique storage key
|
|
this.storageKey = `navigationBoxStates`;
|
|
|
|
// cache local storage
|
|
const saved = localStorage.getItem(this.storageKey);
|
|
this.allSavedStates = saved ? JSON.parse(saved) : {};
|
|
|
|
// listen to our parent for synchronization
|
|
window.addEventListener('box:stateChanged', (e) => {
|
|
const { helper, attributes } = e.detail;
|
|
this.#saveBoxState(helper, attributes);
|
|
});
|
|
}
|
|
|
|
add(options = {}) {
|
|
let config = {
|
|
title : 'box_title',
|
|
helper : 'helper',
|
|
buttons : { close: true, search: '', menu: true },
|
|
content : '',
|
|
contentLoader : '',
|
|
contextMenuItems: '',
|
|
layout : { style: 'overlay', dock: 'top-left', overlay: 'auto', align: 'left', height: 'full', exclusive: true },
|
|
header : '',
|
|
control : '',
|
|
controlLoader : '',
|
|
status : 'closed',
|
|
initCallback : '',
|
|
pluginType : '',
|
|
isRefreshable : false,
|
|
};
|
|
|
|
// validate and merge
|
|
this._validateOptions(options, config);
|
|
|
|
// persistence check
|
|
const saveStates = this.allSavedStates[config.helper];
|
|
|
|
if (saveStates) {
|
|
// overwrite layout values with stored ones
|
|
config.status = saveStates.status || config.status;
|
|
config.layout.height = saveStates.height || config.layout.height;
|
|
config.layout.width = saveStates.width || config.layout.width;
|
|
config.layout.align = saveStates.align || config.layout.align;
|
|
config.layout.style = saveStates.style || config.layout.style;
|
|
config.layout.dock = saveStates.dock || config.layout.dock;
|
|
config.layout.overlay = saveStates.overlay || config.layout.overlay;
|
|
config.layout.exclusive = saveStates.exclusive || config.layout.exclusive;
|
|
|
|
// check if we have a style specif width value stored
|
|
const styleSpecificWidth = saveStates[`width_${config.layout.style}`];
|
|
|
|
if (styleSpecificWidth) {
|
|
config.width = styleSpecificWidth;
|
|
} else {
|
|
config.width = saveStates.width || config.width;
|
|
}
|
|
}
|
|
|
|
if (!config.header) config.header = config.title;
|
|
|
|
const { buttons, helper, layout, title, header, width, content, contentLoader, control, controlLoader } = config;
|
|
|
|
// content & control loading via registry
|
|
const finalContent = content || (contentLoader ? this._runFunction(contentLoader) : '');
|
|
const finalControl = control || (controlLoader ? this._runFunction(controlLoader) : '');
|
|
|
|
// generate UI components
|
|
const btnSearch = buttons.search ? `<div class="navBox-header-button ti-icon-search" data-action="search" data-helper="${helper}" role="button" tabindex="0"></div>` : '';
|
|
const inputSearch = buttons.search ? `<div class="navBox-header-search"><input type="search" name="navBox-header-search" placeholder="Search in ${title}" data-helper="${helper}" tabindex="0"></div>` : '';
|
|
const btnMenu = buttons.menu ? `<div class="navBox-header-button context-menu ti-icon-dots-vertical" data-action="menu" data-helper="${helper}" role="button" tabindex="0"></div>` : '';
|
|
const btnClose = buttons.close ? `<div class="navBox-header-button ti-icon-minus" data-action="close" data-helper="${helper}" role="button" tabindex="0"></div>` : '';
|
|
const rightButtons = (btnMenu + btnClose) || '<div class="navBox-header-dropdown invisible"></div>';
|
|
|
|
// assemble container
|
|
const containerHtml = `
|
|
<div class="${this.prefix}-${this.className}" data-title="${title}" data-helper="${helper}"
|
|
data-plugin="${config.pluginType}"
|
|
data-refresh="${config.isRefreshable}"
|
|
data-status="${config.status}"
|
|
data-height="${layout.height}" data-width="${width}" data-align="${layout.align}"
|
|
data-style="${layout.style}" data-dock="${layout.dock}" data-overlay="${layout.overlay}"
|
|
data-exclusive="${layout.exclusive}">
|
|
<div class="navBox-header">
|
|
${btnSearch}
|
|
${inputSearch}
|
|
<div class="navBox-header-filler"></div>
|
|
<div class="navBox-header-title"><span>${header}</span></div>
|
|
${rightButtons}
|
|
</div>
|
|
<div class="navBox-control">${finalControl}</div>
|
|
<div class="navBox-content">${finalContent}</div>
|
|
</div>`;
|
|
|
|
return this.#build(config, containerHtml);
|
|
}
|
|
|
|
#build(config, containerHtml) {
|
|
const destinationId = this._readConfigOption('container');
|
|
const $box = $(containerHtml).appendTo(`#${destinationId}`);
|
|
|
|
// register close button
|
|
if (config.buttons.close) {
|
|
$box.find('[data-action="close"]').on('click', () => {
|
|
this.toggleBox(config.helper, 'force_close');
|
|
});
|
|
}
|
|
|
|
// register menu button
|
|
if (config.buttons.menu) {
|
|
|
|
$.contextMenu({
|
|
// ensure memory isolation
|
|
selector: `.${this.prefix}-${this.className}[data-helper="${config.helper}"] .context-menu`,
|
|
trigger: 'left',
|
|
autoHide: false,
|
|
items: this.#prepareContextMenuItems(config)
|
|
});
|
|
}
|
|
|
|
// register search button
|
|
if (config.buttons.search) {
|
|
const $input = $box.find('input[name="navBox-header-search"]');
|
|
const $searchBtn = $box.find('[data-action="search"]');
|
|
|
|
$searchBtn.on('click', function(e) {
|
|
const isPressed = $(this).attr('aria-pressed') === 'true';
|
|
const $header = $(this).closest('.navBox-header');
|
|
|
|
$(this).attr('aria-pressed', !isPressed);
|
|
$header.toggleClass('search-active', !isPressed);
|
|
|
|
if (!isPressed) {
|
|
setTimeout(() => $input.focus(), 100);
|
|
} else {
|
|
$input.val('').trigger('input').blur();
|
|
}
|
|
e.preventDefault();
|
|
});
|
|
|
|
const searchPath = config.buttons.search;
|
|
|
|
// debounce logic to keep CPU utilization low
|
|
const runDebouncedSearch = this.#debounce((query) => {
|
|
this._runFunction(searchPath, {
|
|
query: query,
|
|
helper: config.helper,
|
|
$box: $box
|
|
});
|
|
}, 250);
|
|
|
|
$input.on("input", (e) => {
|
|
const query = $(e.currentTarget).val();
|
|
// Trigger the debounced function instead of running it immediately
|
|
runDebouncedSearch(query);
|
|
});
|
|
}
|
|
|
|
// save box states if states are not in cache ( and not in local storage, too)
|
|
if(!this.allSavedStates[config.helper]) {
|
|
this.#saveBoxState(config.helper);
|
|
}
|
|
|
|
// execute initCallback if defined (e.g., to start first navigationBox plugin 'tree')
|
|
if (config.initCallback) {
|
|
this._runFunction(config.initCallback, $box);
|
|
}
|
|
|
|
// return the jQuery object
|
|
return $box;
|
|
}
|
|
|
|
#prepareContextMenuItems(config) {
|
|
// define basic menu structure
|
|
let menuItems = {
|
|
"search": {
|
|
name: "Search",
|
|
icon: "ti ti-search",
|
|
callback: (key, opt) => this.#setFocusSearch(opt)
|
|
}
|
|
};
|
|
|
|
// recursive helper to process nested items
|
|
const processItems = (items) => {
|
|
let processed = {};
|
|
|
|
for (let [key, item] of Object.entries(items)) {
|
|
// handle separators
|
|
if (typeof item === 'string') {
|
|
processed[key] = item;
|
|
continue;
|
|
}
|
|
|
|
// create copy to avoid reference issues
|
|
let newItem = { ...item };
|
|
const safeKey = `plugin_${key}`;
|
|
|
|
// process sub-items if they exist
|
|
if (newItem.items && typeof newItem.items === 'object') {
|
|
newItem.items = processItems(newItem.items);
|
|
}
|
|
|
|
// bind string-based callbacks to our safe runner
|
|
if (typeof newItem.callback === 'string') {
|
|
const funcPath = newItem.callback;
|
|
newItem.visible = () => this._isFunction(funcPath);
|
|
newItem.callback = (key, opt) => this._runFunction(funcPath, [key, opt]);
|
|
}
|
|
|
|
processed[safeKey] = newItem;
|
|
}
|
|
return processed;
|
|
};
|
|
|
|
// process and merge menu items added by plugins
|
|
if (config.contextMenuItems && Object.keys(config.contextMenuItems).length > 0) {
|
|
menuItems["sep_plugin"] = "---------";
|
|
const pluginItems = processItems(config.contextMenuItems);
|
|
menuItems = { ...menuItems, ...pluginItems };
|
|
}
|
|
|
|
// add remaining default options
|
|
menuItems["sep_style"] = "---------";
|
|
menuItems["style"] = {
|
|
name: "Display Style",
|
|
items: {
|
|
"dock": {
|
|
name: "Dock",
|
|
icon: "ti ti-layout-sidebar",
|
|
disabled: (key, opt) => this.#isDisabled('style', key, opt),
|
|
callback: (key, opt) => this.#setDisplayStyle(key, opt),
|
|
visible: (key, opt) => this._readConfigOption(key, 'enabled')
|
|
},
|
|
"overlay": {
|
|
name: "Sidebar",
|
|
icon: "ti ti-layout-sidebar-filled",
|
|
disabled: (key, opt) => this.#isDisabled('style', key, opt),
|
|
callback: (key, opt) => this.#setDisplayStyle(key, opt),
|
|
visible: (key, opt) => this._readConfigOption(key, 'enabled')
|
|
},
|
|
"window": {
|
|
name: "Window",
|
|
icon: "ti ti-box-model-2",
|
|
disabled: (key, opt) => this.#isDisabled('style', key, opt),
|
|
callback: (key, opt) => this.#setDisplayStyle(key, opt),
|
|
visible: (key, opt) => this._readConfigOption(key, 'enabled')
|
|
}
|
|
}
|
|
}
|
|
|
|
menuItems["sep_dock"] = "---------";
|
|
menuItems["dock"] = {
|
|
name: "Dock",
|
|
items: {
|
|
"left-top": { name: "Top Left",
|
|
icon: "ti ti-box-align-top-left",
|
|
disabled: (key, opt) => this.#isDisabled('dock',key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('left-top', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'top')
|
|
},
|
|
"right-top": { name: "Top Right",
|
|
icon: "ti ti-box-align-top-right",
|
|
disabled: (key, opt) => this.#isDisabled('dock',key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('right-top', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'top')
|
|
},
|
|
"top-left": { name: "Left Top",
|
|
icon: "ti ti-box-align-left",
|
|
disabled: (key, opt) => this.#isDisabled('dock', key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('top-left', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'left')
|
|
},
|
|
"bottom-left": { name: "Left Bottom",
|
|
icon: "ti ti-box-align-left",
|
|
disabled: (key, opt) => this.#isDisabled('dock',key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('bottom-left', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'left')
|
|
},
|
|
"top-right": { name: "Right Top",
|
|
icon: "ti ti-box-align-right",
|
|
disabled: (key, opt) => this.#isDisabled('dock',key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('top-right', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'right')
|
|
},
|
|
"bottom-right": { name: "Right Bottom ",
|
|
icon: "ti ti-box-align-right",
|
|
disabled: (key, opt) => this.#isDisabled('dock',key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('bottom-right', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'right')
|
|
},
|
|
"left-bottom": { name: "Bottom Left",
|
|
icon: "ti ti-box-align-bottom-left",
|
|
disabled: (key, opt) => this.#isDisabled('dock',key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('left-bottom', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'bottom')
|
|
},
|
|
"right-bottom": { name: "Bottom Right",
|
|
icon: "ti ti-box-align-bottom-right",
|
|
disabled: (key, opt) => this.#isDisabled('dock',key, opt),
|
|
callback: (key, opt) => this.#setDockPosition('right-bottom', opt),
|
|
visible: (key, opt) => this._readConfigOption('dock', 'bottom')
|
|
},
|
|
},
|
|
visible: (key, opt) => this._readConfigOption(key, 'enabled')
|
|
};
|
|
|
|
menuItems["overlay"] = {
|
|
name: "Sidebar",
|
|
disabled: (key, opt) => !this.#isDisabled('style',key, opt),
|
|
items: {
|
|
"left": { name: "Left",
|
|
icon: "ti ti-layout-sidebar-left-collapse",
|
|
disabled: (key, opt) => this.#isDisabled('align',key, opt),
|
|
callback: (key, opt) => this.#setDataAttribute('align', key, opt)
|
|
},
|
|
"right": { name: "Right",
|
|
icon: "ti ti-layout-sidebar-right-collapse",
|
|
disabled: (key, opt) => this.#isDisabled('align',key, opt),
|
|
callback: (key, opt) => this.#setDataAttribute('align', key, opt)
|
|
},
|
|
"sep": "---------",
|
|
"manual": { name: "Manual",
|
|
icon: "ti ti-arrow-bar-both",
|
|
disabled: (key, opt) => this.#isDisabled('overlay',key, opt),
|
|
callback: (key, opt) => this.#setOverlaySize(key, opt)
|
|
},
|
|
"sep1": "---------",
|
|
"auto": { name: "Auto",
|
|
icon: "ti ti-arrow-autofit-width",
|
|
disabled: (key, opt) => this.#isDisabled('overlay',key, opt),
|
|
callback: (key, opt) => this.#setOverlaySize(key, opt)
|
|
},
|
|
"maximum": {
|
|
name: "Maximum",
|
|
icon: "ti ti-arrow-bar-to-right-dashed",
|
|
disabled: (key, opt) => this.#isDisabled('overlay',key, opt),
|
|
callback: (key, opt) => this.#setOverlaySize(key, opt)
|
|
},
|
|
"minimum": {
|
|
name: "Minimum",
|
|
icon: "ti ti-arrow-bar-to-left-dashed",
|
|
disabled: (key, opt) => this.#isDisabled('overlay',key, opt),
|
|
callback: (key, opt) => this.#setOverlaySize(key, opt)
|
|
},
|
|
},
|
|
visible: (key, opt) => this._readConfigOption(key, 'enabled')
|
|
};
|
|
menuItems["sep_footer"] = "---------";
|
|
menuItems["hide"] = {
|
|
name: "Close",
|
|
icon: "ti ti-x",
|
|
callback: function() { return true; }
|
|
};
|
|
|
|
return menuItems;
|
|
}
|
|
|
|
#setDisplayStyle(mode, opt) {
|
|
let box = opt.$trigger.parents(`div.${this.prefix}-${this.className}`)[0];
|
|
if (!box) return;
|
|
|
|
box.dataset.style = mode;
|
|
const $box = opt.$trigger.closest('[class*="ConsoleNavigationBox"]');
|
|
//$box.attr("data-style", mode);
|
|
mode === "dock" ? this.#moveToDock($box) : this.#moveToSideBar($box);
|
|
|
|
// ensure persistence
|
|
const helper = box.dataset.helper;
|
|
this.#saveBoxState(helper);
|
|
}
|
|
|
|
#setDockPosition(mode, opt) {
|
|
let box = opt.$trigger.parents(`div.${this.prefix}-${this.className}`)[0];
|
|
if (!box) return;
|
|
|
|
box.dataset.dock = mode;
|
|
this.#setDisplayStyle("dock", opt);
|
|
}
|
|
|
|
#moveToDock($box, restoral = null) {
|
|
const [inner, outer] = $box.attr("data-dock").split('-');
|
|
const prefix = $box.attr('class').split('-')[0];
|
|
const destinationSelector = `#${prefix}-Dock${this._ucFirst(outer)} > .${prefix}-DockInner${this._ucFirst(inner)}`;
|
|
|
|
const $destination = $(destinationSelector);
|
|
const $oldParent = $box.parent();
|
|
|
|
$box.detach().appendTo($destination);
|
|
|
|
if (typeof restoral !== "boolean") {
|
|
console.log('here')
|
|
this._updateNavigationButton($box, true);
|
|
this._hideSiblings($box);
|
|
|
|
if ($oldParent.is(`[class*="DockInner"]`)) {
|
|
this._checkDockInnerSiblings($oldParent);
|
|
}
|
|
}
|
|
this._checkDockInnerSiblings($destination);
|
|
}
|
|
|
|
#moveToSideBar($box) {
|
|
const $oldParent = $box.parent();
|
|
const containerId = this.getConfig('container');
|
|
|
|
$box.detach().appendTo(`#${containerId}`);
|
|
|
|
if ($oldParent.is(`[class*="DockInner"]`)) this._checkDockInnerSiblings($oldParent);
|
|
this._hideSiblings($box);
|
|
}
|
|
|
|
#setFocusSearch(opt) {
|
|
let box = opt.$trigger.parents(`div.${this.prefix}-${this.className}`)[0];
|
|
if (!box) return;
|
|
const searchBtn = box.querySelector('[data-action="search"]');
|
|
if (searchBtn) {
|
|
searchBtn.click();
|
|
}
|
|
}
|
|
|
|
#setDataAttribute(attr, value, opt) {
|
|
let box = opt.$trigger.parents(`div.${this.prefix}-${this.className}`)[0];
|
|
if (!box) return;
|
|
|
|
box.dataset[attr] = value;
|
|
|
|
// ensure persistence
|
|
const helper = box.dataset.helper;
|
|
this.#saveBoxState( helper,[attr] );
|
|
}
|
|
|
|
#isDisabled(attr, value, opt) {
|
|
const box = opt.$trigger.closest(`div.${this.prefix}-${this.className}`)[0];
|
|
if (!box) return;
|
|
return value === box.dataset[attr];
|
|
}
|
|
|
|
#setOverlaySize(mode, opt) {
|
|
const box = opt.$trigger.closest(`div.${this.prefix}-${this.className}`)[0];
|
|
if (!box) return;
|
|
|
|
box.dataset.overlay = mode;
|
|
|
|
switch (mode) {
|
|
case "manual":
|
|
box.dataset.width = '0';
|
|
break;
|
|
case "auto":
|
|
box.dataset.width = 'auto'
|
|
box.style.width = 'auto';
|
|
break;
|
|
case "maximum":
|
|
box.dataset.width = '0';
|
|
box.style.width = 'auto';
|
|
break;
|
|
case "minimum":
|
|
box.dataset.width = '0';
|
|
box.style.width = 'min-content';
|
|
break;
|
|
default:
|
|
}
|
|
|
|
// ensure persistence
|
|
const helper = box.dataset.helper;
|
|
this.#saveBoxState(helper, ['style', 'overlay', 'width']);
|
|
}
|
|
|
|
#saveBoxState(helper, attributes = null) {
|
|
const boxSelector = `[class*="NavigationBox"][data-helper="${helper}"]`;
|
|
const box = document.querySelector(boxSelector);
|
|
if (!box) return;
|
|
|
|
let currentData = { ...(this.allSavedStates[helper] || {}) };
|
|
|
|
if (attributes && Array.isArray(attributes)) {
|
|
// update only dedicated attributes
|
|
attributes.forEach(attr => {
|
|
if (box.dataset[attr] !== undefined) {
|
|
currentData[attr] = box.dataset[attr];
|
|
}
|
|
});
|
|
|
|
} else {
|
|
// update all attributes (except title)
|
|
const newData = { ...box.dataset };
|
|
delete newData.title;
|
|
// merge new over old data
|
|
currentData = { ...currentData, ...newData };
|
|
}
|
|
|
|
// cache style specific width
|
|
const currentStyle = box.dataset.style;
|
|
const currentWidth = box.dataset.width;
|
|
if (currentStyle && currentWidth) {
|
|
currentData[`width_${currentStyle}`] = currentWidth;
|
|
}
|
|
|
|
// updated internal cache
|
|
this.allSavedStates[helper] = currentData;
|
|
|
|
// finally update local storage
|
|
localStorage.setItem(this.storageKey, JSON.stringify(this.allSavedStates));
|
|
}
|
|
|
|
#debounce(callback, delay = 250) {
|
|
return (...args) => {
|
|
clearTimeout(this.#searchTimer);
|
|
this.#searchTimer = setTimeout(() => {
|
|
callback.apply(this, args);
|
|
}, delay);
|
|
};
|
|
}
|
|
|
|
searchToHighlight(data) {
|
|
// critical check: ensure external library mark.js is loaded
|
|
if (typeof $.fn.markRegExp !== 'function') {
|
|
console.error("NavigationBox Error: 'mark.js' is not loaded! The search function requires this library.");
|
|
return;
|
|
}
|
|
|
|
const query = typeof data === 'object' ? data.query : data;
|
|
const $container = data.$box ? data.$box.find('.navBox-content') : this._getBox(data.helper).find('.navBox-content');
|
|
|
|
if (!$container.length) return;
|
|
|
|
// prepare search pattern
|
|
const pattern = '.*' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '.*';
|
|
const re = new RegExp(pattern, 'gmiu');
|
|
|
|
// execute mark.js logic
|
|
$("li.menuitem", $container).removeClass('hide');
|
|
$("a[role='menuitem'], li.menuitem", $container).unmark({
|
|
done: () => {
|
|
if (query) {
|
|
$("a[role='menuitem'], li.menuitem", $container).markRegExp(re, {
|
|
"accuracy": "complementary",
|
|
"separateWordSearch": false,
|
|
"done": () => {
|
|
$("li.menuitem", $container).not(":has(mark)").addClass('hide');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
restore(helper) {
|
|
// fetch box-specific state from 'navigationBoxStates'
|
|
const savedBox = this.allSavedStates[helper];
|
|
|
|
// exit if no state exists
|
|
if (!savedBox) return;
|
|
|
|
// determine the current layout style or fallback to overlay
|
|
const currentStyle = savedBox.style || 'overlay';
|
|
|
|
// check if the layout style is globally enabled in the navigation config
|
|
const isStyleEnabled = this.getConfig(`${currentStyle}.enabled`);
|
|
|
|
if (!isStyleEnabled) {
|
|
console.warn(`Navigation: cannot restore ${helper} as ${currentStyle} mode is globally disabled.`);
|
|
return;
|
|
}
|
|
|
|
const $box = this._getBox(helper);
|
|
if (!$box.length) return;
|
|
|
|
// apply saved width dimensions for the specific style
|
|
const savedWidth = savedBox[`width_${currentStyle}`] || savedBox.width;
|
|
if (savedWidth && savedWidth !== '0' && savedWidth !== 'auto' && savedWidth !== 'undefined') {
|
|
$box.css('width', savedWidth);
|
|
}
|
|
|
|
// prevent dynamic boxes ( overlay / window ) from opening automatically during startup
|
|
if (currentStyle !== 'dock') return;
|
|
|
|
// verify if the specific dock position is allowed by the global configuration
|
|
if (currentStyle === 'dock') {
|
|
const [inner, outer] = (savedBox.dock || '').split('-');
|
|
const isDockPosEnabled = this.getConfig(`dock.${outer}`);
|
|
|
|
if (!isDockPosEnabled) {
|
|
console.warn(`Navigation: dock position '${outer}' is globally disabled. skipping restore for ${helper}.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// move the box to its designated dock using restoral mode to keep siblings visible
|
|
if (currentStyle === 'dock') {
|
|
this.#moveToDock($box, true);
|
|
}
|
|
|
|
// synchronize the visual state of the box and its corresponding button
|
|
if (savedBox.status === 'open') {
|
|
this._updateNavigationBox($box, true);
|
|
this._updateNavigationButton(helper, true);
|
|
}
|
|
}
|
|
} |