UIUX : Experiment Dolibarr JS context and tools (#36327)

* UX experiment Dolibarr JS tools

* collapse help

* improuve logs
This commit is contained in:
John BOTELLA
2025-11-19 19:42:00 +01:00
committed by GitHub
parent 12c0f73682
commit ec6219c183
5 changed files with 486 additions and 1 deletions

View File

@@ -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(),
),
)
);

View File

@@ -0,0 +1,214 @@
// CustomEvent doesnt 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;
});

View File

@@ -0,0 +1,263 @@
<?php
/*
* Copyright (C) 2025 Anthony Damhet <a.damhet@progiseize.fr>
* Copyright (C) 2025 Frédéric France <frederic.france@free.fr>
*
* 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 <https://www.gnu.org/licenses/>.
*/
// 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(); ?>
<div class="doc-wrapper">
<?php $documentation->showBreadCrumb(); ?>
<div class="doc-content-wrapper">
<h1 class="documentation-title"><?php echo $langs->trans($experimentName); ?></h1>
<?php $documentation->showSummary(); ?>
<div class="documentation-section">
<h2 class="documentation-title">Introduction</h2>
<p>
DolibarrContext is a secure global JavaScript context for Dolibarr.
It provides a single global object <code>window.Dolibarr</code>, which cannot be replaced.
It allows defining non-replaceable tools and managing hooks/events in a modular and secure way.
</p>
<p>
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.
</p>
<p>
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
<code>Dolibarr.on('addline:load:productPricesList', function(e) { ... });</code>
without relying on DOM changes. This enables creating logic that reacts directly to application state changes.
</p>
<p>
Similarly, you can define tools that act as global helpers, like <code>Dolibarr.tools.setEventMessage()</code>.
This tool can display notifications (similar to PHP's <code>setEventMessage()</code> 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.
</p>
<p>
In summary, DolibarrContext provides a secure, extensible foundation for adding tools, monitoring business events,
and standardizing interactions across Dolibarr's frontend modules.
</p>
</div>
<div class="documentation-section">
<h2 class="documentation-title">Console help</h2>
<p>
Open your browser console with <code>F12</code> to view the available commands.<br/>
If the help does not appear automatically, type <code>Dolibarr.tools.showConsoleHelp();</code> in the console to display it.
</p>
</div>
<div class="documentation-section">
<h2 class="documentation-title">JS Dolibarr hooks</h2>
<h3>Event listener : the Dolibarr ready like</h3>
<div class="documentation-example">
<?php
$lines = array(
'<script>',
' // 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);',
' });',
'</script>',
);
echo $documentation->showCode($lines, 'php'); ?>
</div>
<h3>Example of code usage</h3>
<div class="documentation-example">
<?php
$lines = array(
'<script>',
' 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',
' ...',
' });',
'</script>',
);
echo $documentation->showCode($lines, 'php'); ?>
Open your console <code>F12</code> and click on <button class="button" id="try-event-yourCustomHookName">try</button>
<script nonce="<?php print getNonce() ?>" >
document.addEventListener('Dolibarr:Ready', function(e) {
// the dom is ready and you are sure Dolibarr js context is loaded
// 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);
});
document.getElementById('try-event-yourCustomHookName').addEventListener('click', function(e) {
// you can create js hooks
Dolibarr.executeHook('yourCustomHookName', { data01: 'stuff', data02: 'other stuff' })
});
});
</script>
</div>
</div>
<div class="documentation-section">
<h2 class="documentation-title">Example of creating a new context tool</h2>
<h3>Defining Tools</h3>
<p>
You can define reusable and protected tools in the Dolibarr context using <code>Dolibarr.defineTool</code>:
</p>
<div class="documentation-example">
<?php
$lines = array(
'<script>',
' // 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\');',
'</script>',
);
echo $documentation->showCode($lines, 'php'); ?>
<script nonce="<?php print getNonce() ?>" >
// Define a simple tool
Dolibarr.defineTool('alertUser', (msg) => alert('[Dolibarr] ' + msg));
</script>
</div>
<h3>Protected Tools</h3>
<p>
Once a tool is defined on overwrite false, it cannot be replaced. Attempting to redefine it without overwrite will throw an error:
</p>
<div class="documentation-example">
<?php
$lines = array(
'<script>',
' try {',
' Dolibarr.defineTool(\'alertUser\', () => {});',
' } catch (e) {',
' console.error(e.message);',
' }',
'</script>',
);
echo $documentation->showCode($lines, 'php'); ?>
</div>
<h3>Reading Tools</h3>
<p>
You can read the list of available tools using <code>Dolibarr.tools</code>. It returns a frozen copy:
</p>
<div class="documentation-example">
<?php
$lines = array(
'<script>',
' console.log(Dolibarr.tools);',
' if(Dolibarr.checkToolExist(\'Tool name to check\')){/* ... */}else{/* ... */}; ',
'</script>',
);
echo $documentation->showCode($lines, 'php'); ?>
</div>
</div>
</div>
</div>
<?php
// Output close body + html
$documentation->docFooter();
?>

View File

@@ -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

View File

@@ -1925,7 +1925,7 @@ function top_htmlhead($head, $title = '', $disablejs = 0, $disablehead = 0, $arr
print '<script nonce="'.getNonce().'" src="'.DOL_URL_ROOT.'/includes/jquery/plugins/jeditable/jquery.jeditable.js?' . $ext . '"></script>'."\n";
print '<script nonce="'.getNonce().'" src="'.DOL_URL_ROOT.'/includes/jquery/plugins/jeditable/jquery.jeditable.ui-datepicker.js?' . $ext . '"></script>'."\n";
print '<script nonce="'.getNonce().'" src="'.DOL_URL_ROOT.'/includes/jquery/plugins/jeditable/jquery.jeditable.ui-autocomplete.js?' . $ext . '"></script>'."\n";
print '<script>'."\n";
print '<script nonce="'.getNonce().'" >'."\n";
print 'var urlSaveInPlace = \''.DOL_URL_ROOT.'/core/ajax/saveinplace.php\';'."\n";
print 'var urlLoadInPlace = \''.DOL_URL_ROOT.'/core/ajax/loadinplace.php\';'."\n";
print 'var tooltipInPlace = \''.$langs->transnoentities('ClickToEdit').'\';'."\n"; // Added in title attribute of span