mirror of
https://github.com/Dolibarr/dolibarr.git
synced 2025-12-15 22:11:36 +01:00
UIUX : Experiment Dolibarr JS context and tools (#36327)
* UX experiment Dolibarr JS tools * collapse help * improuve logs
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user