diff --git a/htdocs/admin/tools/ui/class/documentation.class.php b/htdocs/admin/tools/ui/class/documentation.class.php index 8c797b2b2a4..a64bd36b3ca 100644 --- a/htdocs/admin/tools/ui/class/documentation.class.php +++ b/htdocs/admin/tools/ui/class/documentation.class.php @@ -252,6 +252,12 @@ class Documentation 'submenu' => array(), 'summary' => array(), ), + 'UxDolibarrContext' => array( + 'url' => dol_buildpath($this->baseUrl.'/experimental/experiments/dolibarr-context/index.php', 1), + 'icon' => 'fas fa-flask', + 'submenu' => array(), + 'summary' => array(), + ), ) ); diff --git a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-context.umd.js b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-context.umd.js new file mode 100644 index 00000000000..ac2f38b621b --- /dev/null +++ b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/assets/dolibarr-context.umd.js @@ -0,0 +1,214 @@ +// CustomEvent doesn’t show up until IE 11 and Safari 10. Fortunately a simple polyfill pushes support back to any IE 9. +(function () { + if ( typeof window.CustomEvent === "function" ) return false; + function CustomEvent ( event, params ) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } + CustomEvent.prototype = window.Event.prototype; + window.CustomEvent = CustomEvent; +})(); +// End old browsers support + +/** + * Dolibarr Global Context (UMD) + * Provides a secure global object window.Dolibarr + * with non-replaceable tools, events and debug mode. + */ +(function (root, factory) { + // Support AMD + if (typeof define === "function" && define.amd) { + define([], factory); + + // Support CommonJS (Node, bundlers) + } else if (typeof exports === "object") { + module.exports = factory(); + + // Fallback global (browser) + } else { + root.Dolibarr = root.Dolibarr || factory(); + } +})(typeof self !== "undefined" ? self : this, function () { + + // Prevent double initialization if script loaded twice + if (typeof window !== "undefined" && window.Dolibarr) { + return window.Dolibarr; + } + + // Private storage for secure tools (non-replaceable) + const _tools = {}; + + // Native event dispatcher (standard DOM) + const _events = new EventTarget(); + + // Debug flag (disabled by default) + let _debug = false; + + const Dolibarr = { + + /** + * Returns a frozen copy of the registered tools. + * Tools cannot be modified or replaced from outside. + */ + get tools() { + return Object.freeze({ ..._tools }); + }, + + /** + * Defines a new secure tool. + * @param {string} name Name of the tool + * @param {*} value Function, class or object + * @param {boolean} overwrite Explicitly allow overwriting an existing tool + */ + defineTool(name, value, overwrite = false, triggerHook = true) { + // Prevent silent overrides unless "overwrite" is true + if (!overwrite && this.checkToolExist(name)) { + throw new Error(`Dolibarr: Tool '${name}' already defined`); + } + + // Define the tool as read-only and non-configurable + Object.defineProperty(_tools, name, { + value, + writable: false, + configurable: false, + enumerable: true, + }); + + this.log(`Tool defined: ${name}, triggerHook: ${triggerHook}`); + if(triggerHook) { + Dolibarr.executeHook('defineTool', { toolName: name, overwrite: overwrite }); + } + }, + + /** + * Checks if a tool already exists. + * @param {string} name Tool name + * @returns {boolean} true if exists + */ + checkToolExist(name) { + return Object.prototype.hasOwnProperty.call(_tools, name); + }, + + /** + * Enables or disables debug mode. + * When enabled, Dolibarr.log() writes to the console. + */ + debugMode(state) { + _debug = !!state; + // Sauvegarde dans localStorage + if (typeof window !== "undefined" && window.localStorage) { + localStorage.setItem('DolibarrDebugMode', _debug ? '1' : '0'); + } + this.log(`Debug mode: ${_debug}`); + }, + + /** + * Internal logger (only active when debug mode is enabled). + */ + log(msg) { + if (_debug) console.log(`Dolibarr: ${msg}`); + }, + + /** + * Executes a hook-like JS event with CustomEvent. + * @param {string} hookName Hook identifier + * @param {object} data Extra information passed to listeners + */ + executeHook(hookName, data = {}) { + this.log(`Hook executed: ${hookName}`); + + const ev = new CustomEvent(hookName, { detail: data }); + + // Dispatch on internal EventTarget + _events.dispatchEvent(ev); + + // Dispatch globally on document so document.addEventListener('DolibarrHook:' + hookName) can catch it + if (typeof document !== "undefined") { + document.dispatchEvent(new CustomEvent('Dolibarr:' + hookName, { detail: data })); + } + }, + + /** + * Registers an event listener. + * @param {string} eventName Event to listen to + * @param {function} callback Listener function + */ + on(eventName, callback) { + _events.addEventListener(eventName, callback); + }, + + /** + * Unregisters an event listener. + * @param {string} eventName Event name + * @param {function} callback Listener previously added + */ + off(eventName, callback) { + _events.removeEventListener(eventName, callback); + } + }; + + // Lock core object to prevent tampering + Object.freeze(Dolibarr); + + // Expose Dolibarr to window in a protected, non-writable way + if (typeof window !== "undefined") { + Object.defineProperty(window, "Dolibarr", { + value: Dolibarr, + writable: false, + configurable: false, + enumerable: true, + }); + } + + // Restaurer debug mode depuis localStorage + if (typeof window !== "undefined" && window.localStorage) { + const saved = localStorage.getItem('DolibarrDebugMode'); + if (saved === '1') { + Dolibarr.debugMode(true); + } + } + + + /** + * Display help in console log + */ + Dolibarr.defineTool('showConsoleHelp', () => { + console.groupCollapsed( + "%cDolibarr JS Developers HELP", + "background-color: #95cf04 ; color: #ffffff ; font-weight: bold ; padding: 4px ;" + ); + console.log( "Show this help : %cDolibarr.tools.showConsoleHelp();","font-weight: bold ;"); + + console.groupCollapsed('Dolibarr debug mode'); + console.log( "Activate Dolibarr debug mode : %cDolibarr.debugMode(true);","font-weight: bold ;"); + console.log( "Disable Dolibarr debug mode : %cDolibarr.debugMode(false);","font-weight: bold ;"); + console.log( "Note : debug mode status is persistent"); + console.groupEnd(); + + console.groupEnd(); + }, false, false); + + Dolibarr.tools.showConsoleHelp(); + + // Trigger DolibarrContext:init as DOM ready + (function triggerContextInit() { + const initHook = () => { + Dolibarr.executeHook('Ready', { context: Dolibarr }); + Dolibarr.log('Context initialized'); + }; + + if (document.readyState === 'complete' || document.readyState === 'interactive') { + // DOM is already ready + initHook(); + } else { + // Wait for DOM to be ready + document.addEventListener('DOMContentLoaded', initHook); + } + })(); + + + + return Dolibarr; +}); diff --git a/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/index.php b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/index.php new file mode 100644 index 00000000000..39bd757cf5c --- /dev/null +++ b/htdocs/admin/tools/ui/experimental/experiments/dolibarr-context/index.php @@ -0,0 +1,263 @@ + + * Copyright (C) 2025 Frédéric France + * + * 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 3 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Load Dolibarr environment +require '../../../../../../main.inc.php'; + +/** + * @var DoliDB $db + * @var HookManager $hookmanager + * @var Translate $langs + * @var User $user + */ + +// Protection if external user +if ($user->socid > 0) { + accessforbidden(); +} + +// Includes +require_once DOL_DOCUMENT_ROOT . '/admin/tools/ui/class/documentation.class.php'; + +// Load documentation translations +$langs->load('uxdocumentation'); + +// +$documentation = new Documentation($db); +$group = 'ExperimentalUx'; +$experimentName = 'UxDolibarrContext'; + +$experimentAssetsPath = $documentation->baseUrl . '/experimental/experiments/dolibarr-context/assets/'; +$js = [ + '/includes/ace/src/ace.js', + '/includes/ace/src/ext-statusbar.js', + '/includes/ace/src/ext-language_tools.js', + $experimentAssetsPath . '/dolibarr-context.umd.js', +]; +$css = []; + +// Output html head + body - Param is Title +$documentation->docHeader($langs->trans($experimentName, $group), $js, $css); + +// Set view for menu and breadcrumb +$documentation->view = [$group, $experimentName]; + +// Output sidebar +$documentation->showSidebar(); ?> + +
+ + showBreadCrumb(); ?> + +
+ +

trans($experimentName); ?>

+ + showSummary(); ?> + +
+

Introduction

+ +

+ DolibarrContext is a secure global JavaScript context for Dolibarr. + It provides a single global object window.Dolibarr, which cannot be replaced. + It allows defining non-replaceable tools and managing hooks/events in a modular and secure way. +

+ +

+ This system is designed to provide long-term flexibility and maintainability. You can define reusable tools + that encapsulate functionality such as standardized AJAX requests and responses, ensuring consistent data handling across Dolibarr modules. + For example, tools can be created to wrap API calls and automatically process returned data in a uniform format, + reducing repetitive code and preventing errors. +

+ +

+ Beyond DOM-based events, DolibarrContext allows monitoring and reacting to business events using a + hook-like mechanism. For instance, you can listen to events such as + Dolibarr.on('addline:load:productPricesList', function(e) { ... }); + without relying on DOM changes. This enables creating logic that reacts directly to application state changes. +

+ +

+ Similarly, you can define tools that act as global helpers, like Dolibarr.tools.setEventMessage(). + This tool can display notifications (similar to PHP's setEventMessage() in Dolibarr), + initially using jNotify or any other library. In the future, the underlying library can change without affecting + the way modules or external code call this tool, maintaining compatibility and reducing maintenance. +

+ +

+ In summary, DolibarrContext provides a secure, extensible foundation for adding tools, monitoring business events, + and standardizing interactions across Dolibarr's frontend modules. +

+
+ +
+

Console help

+ +

+ Open your browser console with F12 to view the available commands.
+ If the help does not appear automatically, type Dolibarr.tools.showConsoleHelp(); in the console to display it. +

+
+ +
+

JS Dolibarr hooks

+ +

Event listener : the Dolibarr ready like

+
+ ', + ' // Add a listener to the Dolibarr theEventName event', + ' Dolibarr.on(\'theEventName\', function(e) {', + ' console.log(\'Dolibarr theEventName\', e.detail);', + ' });', + + ' // But this work too on document', + ' document.addEventListener(\'Dolibarr:theEventName\', function(e) {', + ' console.log(\'Dolibarr theEventName\', e.detail);', + ' });', + '', + ); + echo $documentation->showCode($lines, 'php'); ?> +
+ +

Example of code usage

+
+ ', + ' document.addEventListener(\'Dolibarr:Ready\', function(e) {', + ' // the dom is ready and you are sure Dolibarr js context is loaded', + ' ...', + ' // Do your stuff', + ' ...', + '', + ' // Add a listener to the yourCustomHookName event', + ' Dolibarr.on(\'yourCustomHookName\', function(e) {', + ' console.log(\'e.detail will contain { data01: \'stuff\', data02: \'other stuff\' }\', e.detail);', + ' });', + '', + ' // you can trigger js hooks', + ' document.getElementById(\'try-event-yourCustomHookName\').addEventListener(\'click\', function(e) {', + ' Dolibarr.executeHook(\'yourCustomHookName\', { data01: \'stuff\', data02: \'other stuff\' })', + ' });', + '', + ' ...', + ' // Do your stuff', + ' ...', + + ' });', + '', + ); + echo $documentation->showCode($lines, 'php'); ?> + + Open your console F12 and click on + + +
+ +
+ +
+

Example of creating a new context tool

+ +

Defining Tools

+

+ You can define reusable and protected tools in the Dolibarr context using Dolibarr.defineTool: +

+ +
+ ', + ' // Define a simple tool', + ' let overwrite = false; // Once a tool is defined, it cannot be replaced.', + ' Dolibarr.defineTool(\'alertUser\', (msg) => alert(\'[Dolibarr] \' + msg), overwrite);', + '', + ' // Use the tool', + ' Dolibarr.tools.alertUser(\'hello world\');', + '', + ); + echo $documentation->showCode($lines, 'php'); ?> + +
+ +

Protected Tools

+

+ Once a tool is defined on overwrite false, it cannot be replaced. Attempting to redefine it without overwrite will throw an error: +

+ +
+ ', + ' try {', + ' Dolibarr.defineTool(\'alertUser\', () => {});', + ' } catch (e) {', + ' console.error(e.message);', + ' }', + '', + ); + echo $documentation->showCode($lines, 'php'); ?> +
+ +

Reading Tools

+

+ You can read the list of available tools using Dolibarr.tools. It returns a frozen copy: +

+ +
+ ', + ' console.log(Dolibarr.tools);', + ' if(Dolibarr.checkToolExist(\'Tool name to check\')){/* ... */}else{/* ... */}; ', + '', + ); + echo $documentation->showCode($lines, 'php'); ?> +
+ +
+ +
+ +
+docFooter(); +?> diff --git a/htdocs/langs/en_US/uxdocumentation.lang b/htdocs/langs/en_US/uxdocumentation.lang index aed8f1310e6..329af207651 100644 --- a/htdocs/langs/en_US/uxdocumentation.lang +++ b/htdocs/langs/en_US/uxdocumentation.lang @@ -164,6 +164,8 @@ ExperimentalUxContributionTxt02 = The different variants of this experiment will ExperimentalUxContributionTxt03 = In some cases, variants may be incompatible with each other. It is often challenging to make different behaviors coexist when they are based on the same interaction. For this reason, it may be necessary to separate the demos in such situations. ExperimentalUxContributionEnd = This structure ensures a clear and modular organization of UX experiments, making testing and future integration more efficient. +UxDolibarrContext = JS Dolibarr context + # Start experiements menu title ExperimentalUxInputAjaxFeedback = Input feedback # End experiements manu tyile diff --git a/htdocs/main.inc.php b/htdocs/main.inc.php index 4ccb8fe0191..383b0f0bec4 100644 --- a/htdocs/main.inc.php +++ b/htdocs/main.inc.php @@ -1925,7 +1925,7 @@ function top_htmlhead($head, $title = '', $disablejs = 0, $disablehead = 0, $arr print ''."\n"; print ''."\n"; print ''."\n"; - print '