From 435eb780eab8117d7b3f3af3a8d1df788699ca36 Mon Sep 17 00:00:00 2001 From: Alexandre Delaunay Date: Thu, 25 Sep 2025 15:03:26 +0200 Subject: [PATCH 01/18] better solution for computeDynamicInputSize --- js/common.js | 66 ---------------------------- js/modules/Forms/EditorController.js | 2 +- 2 files changed, 1 insertion(+), 67 deletions(-) diff --git a/js/common.js b/js/common.js index 23a79f743cd..685c0f1f150 100644 --- a/js/common.js +++ b/js/common.js @@ -1676,72 +1676,6 @@ function waitForElement(selector) { }); } -/** - * Get the ideal width of an input element based on its content. - * This allow to make dynamic inputs that grow and shrink based on their content. - * - * Inspired by: https://phuoc.ng/collection/html-dom/resize-the-width-of-a-text-box-to-fit-its-content-automatically/ - * - * @param {HTMLElement} input - * @param {String} real_font_size It seems the font size computed by styles.fontSize - * is not really accurate when using rem units. - * This parameter allows to directly provide the - * accurate font size if it's known. - * - * @return {String} The ideal width of the input element - */ -function getRealInputWidth(input, real_font_size = null) -{ - let fakeEle = $("#fake_dom_getRealInputWidth"); - - // Initialize our fake element only once to prevent useless computations - if (fakeEle.length === 0) { - // Create a div element - fakeEle = document.createElement('div'); - fakeEle.id = "fake_dom_getRealInputWidth"; - - // Hide it completely - fakeEle.style.position = 'absolute'; - fakeEle.style.top = '0'; - fakeEle.style.left = '0'; - fakeEle.style.left = '-9999px'; - fakeEle.style.overflow = 'hidden'; - fakeEle.style.visibility = 'hidden'; - fakeEle.style.whiteSpace = 'nowrap'; - fakeEle.style.height = '0'; - - // Append the fake element to `body` - document.body.appendChild(fakeEle); - } else { - fakeEle = fakeEle[0]; - } - - // We copy some styles from the textbox that effect the width - const styles = window.getComputedStyle(input); - - // Copy font styles from the textbox - fakeEle.style.fontFamily = styles.fontFamily; - fakeEle.style.fontSize = real_font_size ?? styles.fontSize; - fakeEle.style.fontStyle = styles.fontStyle; - fakeEle.style.fontWeight = styles.fontWeight; - fakeEle.style.letterSpacing = styles.letterSpacing; - fakeEle.style.textTransform = styles.textTransform; - - fakeEle.style.borderLeftWidth = styles.borderLeftWidth; - fakeEle.style.borderRightWidth = styles.borderRightWidth; - fakeEle.style.paddingLeft = styles.paddingLeft; - fakeEle.style.paddingRight = styles.paddingRight; - - // Compute width - const string = input.value || input.getAttribute('placeholder') || ''; - fakeEle.innerHTML = string.replace(/\s/g, '&' + 'nbsp;'); - - const fakeEleStyles = window.getComputedStyle(fakeEle); - const width = fakeEleStyles.width; - - return width; -} - /** * Get UUID using crypto.randomUUID() if possible * Else fallback to uniqid() diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index b21e596099d..2b2d6b6387d 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -1755,7 +1755,7 @@ export class GlpiFormEditorController * @param {HTMLElement} input */ #computeDynamicInputSize(input) { - $(input).css("width", getRealInputWidth(input, "1.2rem")); + $(input).css("width", Math.max(input.value.length + 1, 15) + "ch"); } /** From f606f4debb6e8cac55dc6e26c498ee0faeeae3be Mon Sep 17 00:00:00 2001 From: Alexandre Delaunay Date: Fri, 26 Sep 2025 15:24:17 +0200 Subject: [PATCH 02/18] remove entirely --- js/modules/Forms/EditorController.js | 32 +--------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index 2b2d6b6387d..8dbf6dbf56d 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -31,7 +31,7 @@ * --------------------------------------------------------------------- */ -/* global _, tinymce_editor_configs, getUUID, getRealInputWidth, sortable, tinymce, glpi_toast_info, glpi_toast_error, bootstrap, setupAjaxDropdown, setupAdaptDropdown, setHasUnsavedChanges, hasUnsavedChanges */ +/* global _, tinymce_editor_configs, getUUID, sortable, tinymce, glpi_toast_info, glpi_toast_error, bootstrap, setupAjaxDropdown, setupAdaptDropdown, setHasUnsavedChanges, hasUnsavedChanges */ import { GlpiFormConditionVisibilityEditorController } from '/js/modules/Forms/ConditionVisibilityEditorController.js'; import { GlpiFormConditionValidationEditorController } from '/js/modules/Forms/ConditionValidationEditorController.js'; @@ -130,13 +130,6 @@ export class GlpiFormEditorController this.#initEventHandlers(); this.#refreshUX(); - // Adjust dynamics inputs size - $(this.#target) - .find("[data-glpi-form-editor-dynamic-input]") - .each((index, input) => { - this.#computeDynamicInputSize(input); - }); - // These computations are only needed if the form will be edited. if (!this.#is_readonly) { // Validate default question type @@ -418,11 +411,6 @@ export class GlpiFormEditorController ); break; - // Compute the ideal width of the given input based on its content - case "compute-dynamic-input": - this.#computeDynamicInputSize(target[0]); - break; - // Change the type category of the target question case "change-question-type-category": await this.#changeQuestionTypeCategory( @@ -1089,11 +1077,6 @@ export class GlpiFormEditorController .find("[data-glpi-form-editor-question-details-name]")[0] .focus(); - // Compute dynamic inputs size - new_question.find("[data-glpi-form-editor-dynamic-input]").each((index, input) => { - this.#computeDynamicInputSize(input); - }); - // Enable sortable on the new question this.#enableSortable(new_question); } @@ -1750,14 +1733,6 @@ export class GlpiFormEditorController throw new Error(`Field not found: ${field}`); } - /** - * Compute the ideal width of the given input based on its content. - * @param {HTMLElement} input - */ - #computeDynamicInputSize(input) { - $(input).css("width", Math.max(input.value.length + 1, 15) + "ch"); - } - /** * Set or remove loading state for question type specific content. * This makes the content appear disabled and non-interactive during condition checks. @@ -2151,11 +2126,6 @@ export class GlpiFormEditorController .find("[data-glpi-form-editor-comment-details-name]")[0] .focus(); - // Compute dynamic inputs size - new_comment.find("[data-glpi-form-editor-dynamic-input]").each((index, input) => { - this.#computeDynamicInputSize(input); - }); - // Enable sortable on the new comment this.#enableSortable(new_comment); } From 04c67b578faa2bc50b6c6aef50fcd0177c74094f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Mon, 29 Sep 2025 10:51:26 +0200 Subject: [PATCH 03/18] --wip-- [skip ci] --- src/Html.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Html.php b/src/Html.php index 47e413faf25..35660b30fbe 100644 --- a/src/Html.php +++ b/src/Html.php @@ -3734,7 +3734,28 @@ public static function initEditorSystem( // Init tinymce if ({$init}) { - tinyMCE.init(tinymce_editor_configs['{$id}']); + // tinyMCE.init(tinymce_editor_configs['{$id}']); + + const textarea = $('#' + $.escapeSelector('{$id}')); + const div = $(`
\${textarea.val()}
`); + textarea.after(div).hide(); + + const loadingOverlay = $(` +
+
+ \${__('Loading...')} +
+
+ `); + + div.on('click', function() { + textarea.show(); + div.css('position', 'relative').append(loadingOverlay); + tinyMCE.init(tinymce_editor_configs['{$id}']).then((editors) => { + editors[0].focus(); + div.remove(); + }); + }); } }); JS; From 8fb1390f213b4f4f2746ffef9e2693689e2632dd Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Mon, 29 Sep 2025 13:53:47 +0200 Subject: [PATCH 04/18] Do not render state for each selectable on init --- js/modules/Forms/EditorController.js | 2 ++ js/modules/Forms/QuestionSelectable.js | 9 +++++++-- .../Form/QuestionType/AbstractQuestionTypeSelectable.php | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index 8dbf6dbf56d..86e3fbdfd15 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -601,6 +601,8 @@ export class GlpiFormEditorController computeState() { const global_block_indices = { 'question': 0, 'comment': 0 }; + console.log("compute state"); + // Find all sections const sections = $(this.#target).find("[data-glpi-form-editor-section]"); sections.each((s_index, section) => { diff --git a/js/modules/Forms/QuestionSelectable.js b/js/modules/Forms/QuestionSelectable.js index 2decec4365c..70a17d49c53 100644 --- a/js/modules/Forms/QuestionSelectable.js +++ b/js/modules/Forms/QuestionSelectable.js @@ -54,7 +54,7 @@ export class GlpiFormQuestionTypeSelectable { * * @param {JQuery} container */ - constructor(inputType = null, container = null) { + constructor(inputType = null, container = null, is_from_template = false) { this._inputType = inputType; this._container = $(container); @@ -67,7 +67,12 @@ export class GlpiFormQuestionTypeSelectable { .siblings('div[data-glpi-form-editor-question-extra-details]') .each((index, option) => this._registerOptionListeners($(option))); - this.#getFormController().computeState(); + if (is_from_template) { + // From template = new question added after the initial rendering. + // We only compute the state in this case as it would be useful + // during the initial rendering as nothing was changed yet. + this.#getFormController().computeState(); + } // Restore the checked state if (this._inputType === 'radio') { diff --git a/src/Glpi/Form/QuestionType/AbstractQuestionTypeSelectable.php b/src/Glpi/Form/QuestionType/AbstractQuestionTypeSelectable.php index 5b50cb5601c..b3d6d0052a8 100644 --- a/src/Glpi/Form/QuestionType/AbstractQuestionTypeSelectable.php +++ b/src/Glpi/Form/QuestionType/AbstractQuestionTypeSelectable.php @@ -122,7 +122,7 @@ protected function getFormInlineScript(): string const container = question.find('div[data-glpi-form-editor-selectable-question-options]'); container.data( 'manager', - new m.GlpiFormQuestionTypeSelectable('{{ input_type|escape('js') }}', container) + new m.GlpiFormQuestionTypeSelectable('{{ input_type|escape('js') }}', container, true) ); } }); @@ -133,7 +133,7 @@ protected function getFormInlineScript(): string const container = new_question.find('div[data-glpi-form-editor-selectable-question-options]'); container.data( 'manager', - new m.GlpiFormQuestionTypeSelectable('{{ input_type|escape('js') }}', container) + new m.GlpiFormQuestionTypeSelectable('{{ input_type|escape('js') }}', container, true) ); } }); From 0715d053fd30ea95949db1e5e4d2ba1542211cdf Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Mon, 29 Sep 2025 15:02:13 +0200 Subject: [PATCH 05/18] Remove required mark --- .../form/_form-editor-horizontal-layout.scss | 4 ---- .../components/form/_form-editor.scss | 6 ----- js/modules/Forms/EditorController.js | 23 ------------------- .../pages/admin/form/form_comment.html.twig | 1 - .../pages/admin/form/form_question.html.twig | 4 +--- 5 files changed, 1 insertion(+), 37 deletions(-) diff --git a/css/includes/components/form/_form-editor-horizontal-layout.scss b/css/includes/components/form/_form-editor-horizontal-layout.scss index 8130ae00e6d..f524203e15f 100644 --- a/css/includes/components/form/_form-editor-horizontal-layout.scss +++ b/css/includes/components/form/_form-editor-horizontal-layout.scss @@ -79,10 +79,6 @@ top: 0; } } - - [data-glpi-form-editor-required-mark] { - display: none !important; - } } height: 100%; diff --git a/css/includes/components/form/_form-editor.scss b/css/includes/components/form/_form-editor.scss index a80829a3322..e361496d01a 100644 --- a/css/includes/components/form/_form-editor.scss +++ b/css/includes/components/form/_form-editor.scss @@ -132,12 +132,6 @@ } // Show required mark on mandatory question -.mandatory-question { - [data-glpi-form-editor-required-mark] { - display: inline !important; - } -} - [data-glpi-form-editor-form] { height: fit-content; diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index 86e3fbdfd15..1cec2dec53b 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -403,14 +403,6 @@ export class GlpiFormEditorController ); break; - // Toggle mandatory class on the target question - case "toggle-mandatory-question": - this.#toggleMandatoryClass( - target.closest("[data-glpi-form-editor-question]"), - target.prop("checked") - ); - break; - // Change the type category of the target question case "change-question-type-category": await this.#changeQuestionTypeCategory( @@ -601,8 +593,6 @@ export class GlpiFormEditorController computeState() { const global_block_indices = { 'question': 0, 'comment': 0 }; - console.log("compute state"); - // Find all sections const sections = $(this.#target).find("[data-glpi-form-editor-section]"); sections.each((s_index, section) => { @@ -1391,19 +1381,6 @@ export class GlpiFormEditorController } } - /** - * Toggle the mandatory class for the given question. - * @param {jQuery} question - * @param {boolean} is_mandatory - */ - #toggleMandatoryClass(question, is_mandatory) { - if (is_mandatory) { - question.addClass("mandatory-question"); - } else { - question.removeClass("mandatory-question"); - } - } - /** * Get the template for the given question type. * @param {string} question_type diff --git a/templates/pages/admin/form/form_comment.html.twig b/templates/pages/admin/form/form_comment.html.twig index 93c601774a9..936a7eab29d 100644 --- a/templates/pages/admin/form/form_comment.html.twig +++ b/templates/pages/admin/form/form_comment.html.twig @@ -84,7 +84,6 @@ data-glpi-form-editor-on-input="compute-dynamic-input" data-glpi-form-editor-comment-details-name /> - *
diff --git a/templates/pages/admin/form/form_question.html.twig b/templates/pages/admin/form/form_question.html.twig index eaefd3888f1..404a5115750 100644 --- a/templates/pages/admin/form/form_question.html.twig +++ b/templates/pages/admin/form/form_question.html.twig @@ -59,7 +59,7 @@ data-glpi-form-editor-on-click="set-active" data-glpi-form-editor-question-details data-glpi-form-editor-allow-anonymous="{{ not allow_unauthenticated_access or question_type.isAllowedForUnauthenticatedAccess() ? 1 : 0 }}" - class="card flex-grow-1 {{ question is not null and question.fields.is_mandatory ? 'mandatory-question' : '' }}" + class="card flex-grow-1" aria-label="{{ __("Question details") }}" >
- * {% if question is null %} {% set question_strategy = enum('Glpi\\Form\\Condition\\VisibilityStrategy').ALWAYS_VISIBLE %} @@ -319,7 +318,6 @@ type="checkbox" value="1" {{ question is not null and question.fields.is_mandatory ? 'checked' : '' }} - data-glpi-form-editor-on-change="toggle-mandatory-question" > {{ __("Mandatory") }} From c84b5075871a547ab3fe6bbb09f2c4bc5f1fc007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Mon, 29 Sep 2025 10:51:26 +0200 Subject: [PATCH 06/18] Dynamic loading of tinyMCE instances in form editor --- .../components/form/_form-editor.scss | 27 +++++++++++++++++++ js/modules/Forms/EditorController.js | 9 ------- .../QuestionType/QuestionTypeLongText.php | 3 ++- src/Html.php | 12 ++++++--- .../form/basic_inputs_macros.html.twig | 5 ++++ .../pages/admin/form/form_comment.html.twig | 7 ++--- .../pages/admin/form/form_editor.html.twig | 2 ++ .../pages/admin/form/form_question.html.twig | 7 ++--- .../pages/admin/form/form_section.html.twig | 3 ++- 9 files changed, 51 insertions(+), 24 deletions(-) diff --git a/css/includes/components/form/_form-editor.scss b/css/includes/components/form/_form-editor.scss index e361496d01a..33bb6889a68 100644 --- a/css/includes/components/form/_form-editor.scss +++ b/css/includes/components/form/_form-editor.scss @@ -580,3 +580,30 @@ div:has(> [data-glpi-form-editor-section-details].d-none) { pointer-events: none; } } + +[data-glpi-form-editor-question-details] { + div[data-glpi-tinymce-init-on-demand-render] { + margin-left: 5px; + } + + // Specific style for long text question type + &:has(input[value="Glpi\\\\Form\\\\QuestionType\\\\QuestionTypeLongText"]) { + [data-glpi-form-editor-question-type-specific] { + div[data-glpi-tinymce-init-on-demand-render] { + margin-left: 0; + + // Draw a border around the div to mimic the textarea style + height: 100px; + padding: 10px; + border: var(--tblr-border-width) solid var(--tblr-border-color) !important; + border-radius: var(--tblr-border-radius); + box-shadow: var(--tblr-box-shadow-input) !important; + + // Hide the div when the editor is loading + &:has(.glpi-form-editor-loading-overlay) { + display: none !important; + } + } + } + } +} diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index 1cec2dec53b..c9edcc43ce4 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -923,15 +923,6 @@ export class GlpiFormEditorController return; } - // Lazy load descriptions - item_container.find('textarea[data-glpi-loaded=false]').each(function() { - // Get editor config for this field - const id = $(this).attr("id"); - const config = window.tinymce_editor_configs[id]; - tinyMCE.init(config); - $(this).attr('data-glpi-loaded', "true"); - }); - // Lazy load dropdowns item_container.find('select[data-glpi-loaded=false]').each(function() { // Get editor config for this field diff --git a/src/Glpi/Form/QuestionType/QuestionTypeLongText.php b/src/Glpi/Form/QuestionType/QuestionTypeLongText.php index 345976ad295..0c169eed97b 100644 --- a/src/Glpi/Form/QuestionType/QuestionTypeLongText.php +++ b/src/Glpi/Form/QuestionType/QuestionTypeLongText.php @@ -124,7 +124,8 @@ public function renderAdministrationTemplate(?Question $question): string 'enable_richtext': true, 'editor_height' : "100", 'rows' : 1, - 'init' : question is not null ? true: false, + 'init' : false, + 'init_on_demand' : true, 'is_horizontal' : false, 'full_width' : true, 'no_label' : true, diff --git a/src/Html.php b/src/Html.php index 35660b30fbe..55a2081efe8 100644 --- a/src/Html.php +++ b/src/Html.php @@ -3472,6 +3472,7 @@ public static function initEditorSystem( array $add_body_classes = [], string $toolbar_location = 'top', bool $init = true, + bool $init_on_demand = false, string $placeholder = '', bool $toolbar = true, bool $statusbar = true, @@ -3555,6 +3556,9 @@ public static function initEditorSystem( // Compute init option as "string boolean" so it can be inserted directly into the js output $init = $init ? 'true' : 'false'; + // Compute init_on_demand option as "string boolean" so it can be inserted directly into the js output + $init_on_demand = $init_on_demand ? 'true' : 'false'; + // Compute toolbar option as "string boolean" so it can be inserted directly into the js output $toolbar = $toolbar ? 'true' : 'false'; @@ -3734,10 +3738,12 @@ public static function initEditorSystem( // Init tinymce if ({$init}) { - // tinyMCE.init(tinymce_editor_configs['{$id}']); + tinyMCE.init(tinymce_editor_configs['{$id}']); + } + if ({$init_on_demand}) { const textarea = $('#' + $.escapeSelector('{$id}')); - const div = $(`
\${textarea.val()}
`); + const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); textarea.after(div).hide(); const loadingOverlay = $(` @@ -3748,7 +3754,7 @@ public static function initEditorSystem(
`); - div.on('click', function() { + div.one('click', function() { textarea.show(); div.css('position', 'relative').append(loadingOverlay); tinyMCE.init(tinymce_editor_configs['{$id}']).then((editors) => { diff --git a/templates/components/form/basic_inputs_macros.html.twig b/templates/components/form/basic_inputs_macros.html.twig index 2a5295295b6..dab32aa4833 100644 --- a/templates/components/form/basic_inputs_macros.html.twig +++ b/templates/components/form/basic_inputs_macros.html.twig @@ -326,6 +326,7 @@ 'toolbar': true, 'toolbar_location': 'top', 'init': true, + 'init_on_demand': false, 'placeholder': "", 'enable_form_tags': false, 'form_tags_form_id': null, @@ -353,6 +354,9 @@ {% if not options.aria_label is empty %} aria-label="{{ options.aria_label }}" {% endif %} + {% if not options.placeholder is empty %} + placeholder="{{ options.placeholder }}" + {% endif %} {{ options.disabled ? 'disabled' : '' }} {{ options.readonly ? 'readonly' : '' }} {{ options.required ? 'required' : '' }}>{{ options.enable_richtext ? value|safe_html|escape : value }} @@ -368,6 +372,7 @@ options.add_body_classes, options.toolbar_location, options.init, + options.init_on_demand, options.placeholder, options.toolbar, options.statusbar, diff --git a/templates/pages/admin/form/form_comment.html.twig b/templates/pages/admin/form/form_comment.html.twig index 936a7eab29d..435a58e4149 100644 --- a/templates/pages/admin/form/form_comment.html.twig +++ b/templates/pages/admin/form/form_comment.html.twig @@ -149,7 +149,6 @@ {# Mark as secondary if empty #} {{ comment is null or comment.fields.description|length == 0 ? "data-glpi-form-editor-active-comment-extra-details" : "" }} > - {% set load_editor = comment is not null and comment.fields.description|length > 0 %} {{ fields.textareaField( 'description', comment is not null ? comment.fields.description : '', @@ -162,11 +161,9 @@ 'editor_height': "0", 'rows' : 1, 'toolbar_location': 'bottom', - 'init': load_editor, + 'init': false, + 'init_on_demand': true, 'mb': 'mb-0', - 'additional_attributes': { - 'data-glpi-loaded': load_editor ? "true" : "false" - } }) ) }} diff --git a/templates/pages/admin/form/form_editor.html.twig b/templates/pages/admin/form/form_editor.html.twig index afb66a9e320..1405e4231ac 100644 --- a/templates/pages/admin/form/form_editor.html.twig +++ b/templates/pages/admin/form/form_editor.html.twig @@ -152,6 +152,8 @@ 'mb': 'mb-0', 'aria_label': __("Form description"), 'placeholder': __("Add a description to your form..."), + 'init': false, + 'init_on_demand': true, }) ) }} diff --git a/templates/pages/admin/form/form_question.html.twig b/templates/pages/admin/form/form_question.html.twig index 404a5115750..a2d868be27e 100644 --- a/templates/pages/admin/form/form_question.html.twig +++ b/templates/pages/admin/form/form_question.html.twig @@ -195,7 +195,6 @@ {# Mark as secondary if empty #} {{ question is null or question.fields.description|length == 0 ? "data-glpi-form-editor-question-extra-details" : "" }} > - {% set load_editor = question is not null and question.fields.description|length > 0 %} {{ fields.textareaField( 'description', question is not null ? question.fields.description : '', @@ -208,11 +207,9 @@ 'editor_height': "0", 'rows' : 1, 'toolbar_location': 'bottom', - 'init': load_editor, + 'init': false, + 'init_on_demand': true, 'mb': 'mb-0', - 'additional_attributes': { - 'data-glpi-loaded': load_editor ? "true" : "false" - } }) ) }} diff --git a/templates/pages/admin/form/form_section.html.twig b/templates/pages/admin/form/form_section.html.twig index e4d059b23a6..ec03f006a1b 100644 --- a/templates/pages/admin/form/form_section.html.twig +++ b/templates/pages/admin/form/form_section.html.twig @@ -109,7 +109,8 @@ 'editor_height': "0", 'rows' : 1, 'toolbar_location': 'bottom', - 'init': section is not null ? true : false, + 'init': false, + 'init_on_demand': true, 'mb': 'mb-0', }) ) }} From 7137045288e6a8086b542aeae1278f6e2628ff11 Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Mon, 29 Sep 2025 15:57:12 +0200 Subject: [PATCH 07/18] Prevent duplicated SQL queries for condition data --- src/CommonDBTM.php | 7 ++++ src/Glpi/Form/Form.php | 15 +++++++++ src/Glpi/Toolbox/UuidStore.php | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/Glpi/Toolbox/UuidStore.php diff --git a/src/CommonDBTM.php b/src/CommonDBTM.php index e08b1b3ad9e..895887facc5 100644 --- a/src/CommonDBTM.php +++ b/src/CommonDBTM.php @@ -50,6 +50,7 @@ use Glpi\Search\FilterableInterface; use Glpi\Search\SearchOption; use Glpi\Socket; +use Glpi\Toolbox\UuidStore; use function Safe\getimagesize; use function Safe\preg_grep; @@ -6685,6 +6686,12 @@ public static function getPostFormAction(string $form_action, bool $action_succe public static function getByUuid(string $uuid): ?static { + $store = UuidStore::getInstance(); + $content = $store->get($uuid); + if ($content instanceof static) { + return $content; + } + $item = new static(); if ($item->getFromDBByCrit(['uuid' => $uuid])) { return $item; diff --git a/src/Glpi/Form/Form.php b/src/Glpi/Form/Form.php index 1e6edcd49c5..ae463a9a61a 100644 --- a/src/Glpi/Form/Form.php +++ b/src/Glpi/Form/Form.php @@ -65,6 +65,7 @@ use Glpi\Helpdesk\Tile\FormTile; use Glpi\ItemTranslation\Context\ProvideTranslationsInterface; use Glpi\ItemTranslation\Context\TranslationHandler; +use Glpi\Toolbox\UuidStore; use Glpi\UI\IllustrationManager; use Html; use InvalidArgumentException; @@ -178,6 +179,19 @@ public function showForm($id, array $options = []) $types_manager = QuestionTypesManager::getInstance(); + // Use the uuid store to prevent condition data from fetching the same + // questions over and over + $store = UuidStore::getInstance(); + foreach ($this->getSections() as $section) { + $store->addToStore($section->fields['uuid'], $section); + } + foreach ($this->getQuestions() as $question) { + $store->addToStore($question->fields['uuid'], $question); + } + foreach ($this->getFormComments() as $comment) { + $store->addToStore($comment->fields['uuid'], $comment); + } + // Render twig template $twig = TemplateRenderer::getInstance(); $twig->display('pages/admin/form/form_editor.html.twig', [ @@ -188,6 +202,7 @@ public function showForm($id, array $options = []) 'invalid_questions' => $this->getInvalidQuestions(), 'allow_unauthenticated_access' => FormAccessControlManager::getInstance()->allowUnauthenticatedAccess($this), ]); + $store->purge(); return true; } diff --git a/src/Glpi/Toolbox/UuidStore.php b/src/Glpi/Toolbox/UuidStore.php new file mode 100644 index 00000000000..65ccbd2e35b --- /dev/null +++ b/src/Glpi/Toolbox/UuidStore.php @@ -0,0 +1,60 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Toolbox; + +use CommonDBTM; + +final class UuidStore +{ + use SingletonTrait; + + private array $store = []; + + public function addToStore(string $uuid, CommonDBTM $item): void + { + $this->store[$uuid] = $item; + } + + public function get(string $uuid): ?CommonDBTM + { + return $this->store[$uuid] ?? null; + } + + public function purge(): void + { + $this->store = []; + } +} From 9e932742d9892fb653c1c815be70d1cd837a1f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Mon, 29 Sep 2025 16:18:28 +0200 Subject: [PATCH 08/18] Enhance accessibility for TinyMCE initialization by adding role and tabindex attributes --- src/Html.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Html.php b/src/Html.php index 55a2081efe8..97ad120872e 100644 --- a/src/Html.php +++ b/src/Html.php @@ -3743,7 +3743,7 @@ public static function initEditorSystem( if ({$init_on_demand}) { const textarea = $('#' + $.escapeSelector('{$id}')); - const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); + const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); textarea.after(div).hide(); const loadingOverlay = $(` @@ -3754,7 +3754,7 @@ public static function initEditorSystem( `); - div.one('click', function() { + div.one('focus', function() { textarea.show(); div.css('position', 'relative').append(loadingOverlay); tinyMCE.init(tinymce_editor_configs['{$id}']).then((editors) => { From fcc71c7cbd8f00ef9ffdbc13266bc0c31ff9fb31 Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Mon, 29 Sep 2025 16:39:06 +0200 Subject: [PATCH 09/18] Remove outdated handlers --- templates/pages/admin/form/form_comment.html.twig | 1 - templates/pages/admin/form/form_question.html.twig | 1 - templates/pages/admin/form/form_section.html.twig | 1 - 3 files changed, 3 deletions(-) diff --git a/templates/pages/admin/form/form_comment.html.twig b/templates/pages/admin/form/form_comment.html.twig index 435a58e4149..646f29fcccc 100644 --- a/templates/pages/admin/form/form_comment.html.twig +++ b/templates/pages/admin/form/form_comment.html.twig @@ -81,7 +81,6 @@ placeholder="{{ __("New comment") }}" aria-label="{{ __("Comment title") }}" data-glpi-form-editor-dynamic-input - data-glpi-form-editor-on-input="compute-dynamic-input" data-glpi-form-editor-comment-details-name /> diff --git a/templates/pages/admin/form/form_question.html.twig b/templates/pages/admin/form/form_question.html.twig index a2d868be27e..c9038e289ee 100644 --- a/templates/pages/admin/form/form_question.html.twig +++ b/templates/pages/admin/form/form_question.html.twig @@ -94,7 +94,6 @@ value="{{ question is not null ? question.fields.name : '' }}" placeholder="{{ __("New question") }}" data-glpi-form-editor-dynamic-input - data-glpi-form-editor-on-input="compute-dynamic-input" data-glpi-form-editor-question-details-name /> diff --git a/templates/pages/admin/form/form_section.html.twig b/templates/pages/admin/form/form_section.html.twig index ec03f006a1b..ad64c317fed 100644 --- a/templates/pages/admin/form/form_section.html.twig +++ b/templates/pages/admin/form/form_section.html.twig @@ -70,7 +70,6 @@ placeholder="{{ __("New section") }}" data-glpi-form-editor-section-details-name data-glpi-form-editor-dynamic-input - data-glpi-form-editor-on-input="compute-dynamic-input" /> From 9377c36974dda6a2794df174a8e74be97bf19d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Tue, 30 Sep 2025 09:48:42 +0200 Subject: [PATCH 10/18] Dynamic loading of tinyMCE in translation editor + duplicate name fix --- css/includes/components/form/_form-editor.scss | 3 ++- .../components/form/_item-translations.scss | 11 +++++++++++ src/Entity.php | 5 ++++- src/Glpi/Form/Comment.php | 6 +++++- src/Glpi/Form/Form.php | 11 ++++++++--- src/Glpi/Form/Question.php | 6 +++++- src/Glpi/Form/Section.php | 6 +++++- src/Glpi/Helpdesk/Tile/ExternalPageTile.php | 6 +++++- src/Glpi/Helpdesk/Tile/GlpiPageTile.php | 6 +++++- .../Context/TranslationHandler.php | 18 +++++++++++++++++- .../admin/form/form_translation.html.twig | 6 ++++-- .../admin/helpdesk_home_translation.html.twig | 6 ++++-- 12 files changed, 75 insertions(+), 15 deletions(-) diff --git a/css/includes/components/form/_form-editor.scss b/css/includes/components/form/_form-editor.scss index 33bb6889a68..0f5f762ba9c 100644 --- a/css/includes/components/form/_form-editor.scss +++ b/css/includes/components/form/_form-editor.scss @@ -584,6 +584,7 @@ div:has(> [data-glpi-form-editor-section-details].d-none) { [data-glpi-form-editor-question-details] { div[data-glpi-tinymce-init-on-demand-render] { margin-left: 5px; + cursor: text; } // Specific style for long text question type @@ -593,7 +594,7 @@ div:has(> [data-glpi-form-editor-section-details].d-none) { margin-left: 0; // Draw a border around the div to mimic the textarea style - height: 100px; + min-height: 100px; padding: 10px; border: var(--tblr-border-width) solid var(--tblr-border-color) !important; border-radius: var(--tblr-border-radius); diff --git a/css/includes/components/form/_item-translations.scss b/css/includes/components/form/_item-translations.scss index 53ea85a7306..edc62e686a2 100644 --- a/css/includes/components/form/_item-translations.scss +++ b/css/includes/components/form/_item-translations.scss @@ -63,4 +63,15 @@ padding-bottom: 0.5rem; white-space: nowrap; } + + div[data-glpi-tinymce-init-on-demand-render] { + cursor: text; + + // Draw a border around the div to mimic the textarea style + min-height: 100px; + padding: 10px; + border: var(--tblr-border-width) solid var(--tblr-border-color) !important; + border-radius: var(--tblr-border-radius); + box-shadow: var(--tblr-box-shadow-input) !important; + } } diff --git a/src/Entity.php b/src/Entity.php index 99c68def452..20deb3402bc 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -3383,7 +3383,8 @@ public function getTilesConfigInformationText(): ?string public function listTranslationsHandlers(): array { $handlers = []; - $key = sprintf('%s: %s', self::getTypeName(), $this->getName()); + $key = sprintf('%s_%d', self::getType(), $this->getID()); + $category_name = sprintf('%s: %s', self::getTypeName(), $this->getName()); if ( !empty($this->fields['custom_helpdesk_home_title']) && $this->fields['custom_helpdesk_home_title'] != self::CONFIG_PARENT @@ -3393,6 +3394,8 @@ public function listTranslationsHandlers(): array key: self::TRANSLATION_KEY_CUSTOM_HELPDESK_HOME_TITLE, name: __('Custom main title'), value: $this->fields['custom_helpdesk_home_title'], + is_rich_text: false, + category: $category_name ); } diff --git a/src/Glpi/Form/Comment.php b/src/Glpi/Form/Comment.php index 3325940fa35..204e64c13df 100644 --- a/src/Glpi/Form/Comment.php +++ b/src/Glpi/Form/Comment.php @@ -175,7 +175,8 @@ private function prepareInput($input): array #[Override] public function listTranslationsHandlers(): array { - $key = sprintf('%s: %s', self::getTypeName(), $this->getName()); + $key = sprintf('%s_%d', self::getType(), $this->getID()); + $category_name = sprintf('%s: %s', self::getTypeName(), $this->getName()); $handlers = []; if (!empty($this->fields['name'])) { @@ -184,6 +185,8 @@ public function listTranslationsHandlers(): array key: self::TRANSLATION_KEY_NAME, name: __('Comment title'), value: $this->fields['name'], + is_rich_text: false, + category: $category_name ); } @@ -194,6 +197,7 @@ public function listTranslationsHandlers(): array name: __('Comment description'), value: $this->fields['description'], is_rich_text: true, + category: $category_name ); } diff --git a/src/Glpi/Form/Form.php b/src/Glpi/Form/Form.php index ae463a9a61a..cb2bc2dbe6f 100644 --- a/src/Glpi/Form/Form.php +++ b/src/Glpi/Form/Form.php @@ -448,7 +448,8 @@ public static function showMassiveActionsSubForm(MassiveAction $ma): bool #[Override] public function listTranslationsHandlers(): array { - $key = __('Form properties'); + $key = sprintf('%s_%d', self::getType(), $this->getID()); + $category_name = __('Form properties'); $handlers = []; if (!empty($this->fields['name'])) { $handlers[$key][] = new TranslationHandler( @@ -456,6 +457,8 @@ public function listTranslationsHandlers(): array key: self::TRANSLATION_KEY_NAME, name: __('Form title'), value: $this->fields['name'], + is_rich_text: false, + category: $category_name ); } @@ -465,7 +468,8 @@ public function listTranslationsHandlers(): array key: self::TRANSLATION_KEY_HEADER, name: __('Form description'), value: $this->fields['header'], - is_rich_text: true + is_rich_text: true, + category: $category_name ); } @@ -475,7 +479,8 @@ public function listTranslationsHandlers(): array key: self::TRANSLATION_KEY_DESCRIPTION, name: __('Service catalog description'), value: $this->fields['description'], - is_rich_text: true + is_rich_text: true, + category: $category_name ); } diff --git a/src/Glpi/Form/Question.php b/src/Glpi/Form/Question.php index ca035014925..4c918104edd 100644 --- a/src/Glpi/Form/Question.php +++ b/src/Glpi/Form/Question.php @@ -132,7 +132,8 @@ public function cleanDBonPurge() #[Override] public function listTranslationsHandlers(): array { - $key = sprintf('%s: %s', self::getTypeName(), $this->getName()); + $key = sprintf('%s_%d', self::getType(), $this->getID()); + $category_name = sprintf('%s: %s', self::getTypeName(), $this->getName()); $handlers = []; if (!empty($this->fields['name'])) { $handlers[$key][] = new TranslationHandler( @@ -140,6 +141,8 @@ public function listTranslationsHandlers(): array key: self::TRANSLATION_KEY_NAME, name: __('Question name'), value: $this->fields['name'], + is_rich_text: false, + category: $category_name ); } @@ -150,6 +153,7 @@ public function listTranslationsHandlers(): array name: __('Question description'), value: $this->fields['description'], is_rich_text: true, + category: $category_name ); } diff --git a/src/Glpi/Form/Section.php b/src/Glpi/Form/Section.php index 5cbc1036582..8b9b2ee3d47 100644 --- a/src/Glpi/Form/Section.php +++ b/src/Glpi/Form/Section.php @@ -156,7 +156,8 @@ public function listTranslationsHandlers(): array } $handlers = []; - $key = sprintf('%s: %s', self::getTypeName(), $this->getName()); + $key = sprintf('%s_%d', self::getType(), $this->getID()); + $category_name = sprintf('%s: %s', self::getTypeName(), $this->getName()); if (count($form->getSections()) > 1) { if (!empty($this->fields['name'])) { $handlers[$key][] = new TranslationHandler( @@ -164,6 +165,8 @@ public function listTranslationsHandlers(): array key: self::TRANSLATION_KEY_NAME, name: __('Section title'), value: $this->fields['name'], + is_rich_text: false, + category: $category_name ); } @@ -174,6 +177,7 @@ public function listTranslationsHandlers(): array name: __('Section description'), value: $this->fields['description'], is_rich_text: true, + category: $category_name ); } } diff --git a/src/Glpi/Helpdesk/Tile/ExternalPageTile.php b/src/Glpi/Helpdesk/Tile/ExternalPageTile.php index ce686018693..857fd88ebb9 100644 --- a/src/Glpi/Helpdesk/Tile/ExternalPageTile.php +++ b/src/Glpi/Helpdesk/Tile/ExternalPageTile.php @@ -128,13 +128,16 @@ public function cleanDBonPurge() public function listTranslationsHandlers(): array { $handlers = []; - $key = sprintf('%s: %s', $this->getLabel(), $this->fields['title'] ?? NOT_AVAILABLE); + $key = sprintf('%s_%d', self::getType(), $this->getID()); + $category_name = sprintf('%s: %s', $this->getLabel(), $this->fields['title'] ?? NOT_AVAILABLE); if (!empty($this->getTitle())) { $handlers[$key][] = new TranslationHandler( item: $this, key: self::TRANSLATION_KEY_TITLE, name: __('Title'), value: $this->getTitle(), + is_rich_text: false, + category: $category_name ); } if (!empty($this->getDescription())) { @@ -144,6 +147,7 @@ public function listTranslationsHandlers(): array name: __('Description'), value: $this->getDescription(), is_rich_text: true, + category: $category_name ); } diff --git a/src/Glpi/Helpdesk/Tile/GlpiPageTile.php b/src/Glpi/Helpdesk/Tile/GlpiPageTile.php index 0152359f3ed..f53929c2310 100644 --- a/src/Glpi/Helpdesk/Tile/GlpiPageTile.php +++ b/src/Glpi/Helpdesk/Tile/GlpiPageTile.php @@ -181,13 +181,16 @@ public function cleanDBonPurge() public function listTranslationsHandlers(): array { $handlers = []; - $key = sprintf('%s: %s', $this->getLabel(), $this->fields['title'] ?? NOT_AVAILABLE); + $key = sprintf('%s_%d', self::getType(), $this->getID()); + $category_name = sprintf('%s: %s', $this->getLabel(), $this->fields['title'] ?? NOT_AVAILABLE); if (!empty($this->getTitle())) { $handlers[$key][] = new TranslationHandler( item: $this, key: self::TRANSLATION_KEY_TITLE, name: __('Title'), value: $this->getTitle(), + is_rich_text: false, + category: $category_name ); } if (!empty($this->getDescription())) { @@ -197,6 +200,7 @@ public function listTranslationsHandlers(): array name: __('Description'), value: $this->getDescription(), is_rich_text: true, + category: $category_name ); } diff --git a/src/Glpi/ItemTranslation/Context/TranslationHandler.php b/src/Glpi/ItemTranslation/Context/TranslationHandler.php index 624cd3f248a..472144a58b2 100644 --- a/src/Glpi/ItemTranslation/Context/TranslationHandler.php +++ b/src/Glpi/ItemTranslation/Context/TranslationHandler.php @@ -57,25 +57,31 @@ final class TranslationHandler /** @var bool Whether this field contains rich text that should be edited in a rich text editor */ private bool $is_rich_text; + /** @var string|null The category name for grouping translations */ + private ?string $category; + /** * @param CommonDBTM $item The item to translate * @param string $key The key of the field to translate * @param string $name The human-readable name of the field * @param string $value The default value (in the default language) * @param bool $is_rich_text Whether this field contains rich text + * @param string|null $category The category name for grouping translations */ public function __construct( CommonDBTM $item, string $key, string $name, string $value, - bool $is_rich_text = false + bool $is_rich_text = false, + ?string $category = null ) { $this->item = $item; $this->key = $key; $this->name = $name; $this->value = $value; $this->is_rich_text = $is_rich_text; + $this->category = $category; } /** @@ -127,4 +133,14 @@ public function isRichText(): bool { return $this->is_rich_text; } + + /** + * Get the category name for grouping translations + * + * @return string|null + */ + public function getCategory(): ?string + { + return $this->category; + } } diff --git a/templates/pages/admin/form/form_translation.html.twig b/templates/pages/admin/form/form_translation.html.twig index 88492bb4b2c..e2d7469ed2a 100644 --- a/templates/pages/admin/form/form_translation.html.twig +++ b/templates/pages/admin/form/form_translation.html.twig @@ -68,6 +68,8 @@ 'enable_richtext': true, 'statusbar' : false, 'editor_height' : 0, + 'init' : false, + 'init_on_demand' : true, } ) }} {% else %} @@ -84,9 +86,9 @@ {% endmacro %} {% set entries = [] %} -{% for category, handlers in form.listTranslationsHandlers() %} +{% for handlers in form.listTranslationsHandlers() %} {% set entries = entries|merge([{ - 'type': '' ~ category ~ '', + 'type': '' ~ handlers|first.getCategory() ~ '', 'type_colspan': 3, 'type_aria_label': _n('Category', 'Categories', 1) }]) %} diff --git a/templates/pages/admin/helpdesk_home_translation.html.twig b/templates/pages/admin/helpdesk_home_translation.html.twig index f35e887af8f..518f82fc317 100644 --- a/templates/pages/admin/helpdesk_home_translation.html.twig +++ b/templates/pages/admin/helpdesk_home_translation.html.twig @@ -68,6 +68,8 @@ 'enable_richtext': true, 'statusbar' : false, 'editor_height' : 0, + 'init' : false, + 'init_on_demand' : true, } ) }} {% else %} @@ -84,9 +86,9 @@ {% endmacro %} {% set entries = [] %} -{% for category, handlers in item.listTranslationsHandlers() %} +{% for handlers in item.listTranslationsHandlers() %} {% set entries = entries|merge([{ - 'type': '' ~ category ~ '', + 'type': '' ~ handlers|first.getCategory() ~ '', 'type_colspan': 3, 'type_aria_label': _n('Category', 'Categories', 1) }]) %} From df61fbb832225da7fa6f80f07e4044dcc00ef8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Tue, 30 Sep 2025 09:53:29 +0200 Subject: [PATCH 11/18] Reorder parameters in initEditorSystem method --- src/Html.php | 4 ++-- templates/components/form/basic_inputs_macros.html.twig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Html.php b/src/Html.php index 97ad120872e..974465cbe61 100644 --- a/src/Html.php +++ b/src/Html.php @@ -3472,11 +3472,11 @@ public static function initEditorSystem( array $add_body_classes = [], string $toolbar_location = 'top', bool $init = true, - bool $init_on_demand = false, string $placeholder = '', bool $toolbar = true, bool $statusbar = true, - string $content_style = '' + string $content_style = '', + bool $init_on_demand = false ) { global $CFG_GLPI, $DB; diff --git a/templates/components/form/basic_inputs_macros.html.twig b/templates/components/form/basic_inputs_macros.html.twig index dab32aa4833..947e466873e 100644 --- a/templates/components/form/basic_inputs_macros.html.twig +++ b/templates/components/form/basic_inputs_macros.html.twig @@ -372,11 +372,11 @@ options.add_body_classes, options.toolbar_location, options.init, - options.init_on_demand, options.placeholder, options.toolbar, options.statusbar, options.content_style, + options.init_on_demand, ]) %} {% endif %} {% if options.enable_form_tags %} From b9fcba79aa5682a4cc55493709f0d94960068278 Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Tue, 30 Sep 2025 10:12:37 +0200 Subject: [PATCH 12/18] Fix multiple dropdowns --- src/Glpi/Form/QuestionType/QuestionTypeDropdown.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Glpi/Form/QuestionType/QuestionTypeDropdown.php b/src/Glpi/Form/QuestionType/QuestionTypeDropdown.php index 30995f5bf32..960a6ec1727 100644 --- a/src/Glpi/Form/QuestionType/QuestionTypeDropdown.php +++ b/src/Glpi/Form/QuestionType/QuestionTypeDropdown.php @@ -131,7 +131,7 @@ protected function getFormInlineScript(): string const container = question.find('div[data-glpi-form-editor-selectable-question-options]'); container.data( 'manager', - new m.GlpiFormQuestionTypeDropdown('{{ input_type|escape('js') }}', container) + new m.GlpiFormQuestionTypeDropdown('{{ input_type|escape('js') }}', container, true) ); } }); @@ -142,7 +142,7 @@ protected function getFormInlineScript(): string const container = new_question.find('div[data-glpi-form-editor-selectable-question-options]'); container.data( 'manager', - new m.GlpiFormQuestionTypeDropdown('{{ input_type|escape('js') }}', container) + new m.GlpiFormQuestionTypeDropdown('{{ input_type|escape('js') }}', container, true) ); } }); From 3fb5dd001e389cb782111a55023bbc9c2458e15a Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Tue, 30 Sep 2025 10:19:03 +0200 Subject: [PATCH 13/18] Update constructor --- js/modules/Forms/QuestionDropdown.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/modules/Forms/QuestionDropdown.js b/js/modules/Forms/QuestionDropdown.js index 9022b59d75b..b950e630f94 100644 --- a/js/modules/Forms/QuestionDropdown.js +++ b/js/modules/Forms/QuestionDropdown.js @@ -43,8 +43,8 @@ export class GlpiFormQuestionTypeDropdown extends GlpiFormQuestionTypeSelectable * @param {string} inputType * @param {JQuery} container */ - constructor(inputType = null, container = null) { - super(inputType, container); + constructor(inputType = null, container = null, is_from_template = false) { + super(inputType, container, is_from_template); this._container.on('sortupdate', () => this.#handleSortableUpdate()); From a754f8cea38ab82edc8599ec857f5143ed8d271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Tue, 30 Sep 2025 11:15:04 +0200 Subject: [PATCH 14/18] Adjust dynamic input width in horizontal layout --- .../components/form/_form-editor-horizontal-layout.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/css/includes/components/form/_form-editor-horizontal-layout.scss b/css/includes/components/form/_form-editor-horizontal-layout.scss index f524203e15f..12ff25ec04a 100644 --- a/css/includes/components/form/_form-editor-horizontal-layout.scss +++ b/css/includes/components/form/_form-editor-horizontal-layout.scss @@ -72,8 +72,9 @@ position: relative; [data-glpi-form-editor-dynamic-input] { - transform: rotate(90deg); + transform: rotate(90deg) translateX(-0.5rem); transform-origin: left; + width: 100cqh; font-size: 1rem; left: 0.5rem; top: 0; From 2727fc62f9a48a48b9b67f3424a906bc93af29d4 Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Tue, 30 Sep 2025 12:19:13 +0200 Subject: [PATCH 15/18] Fix tests --- .../components/form/_form-editor.scss | 4 ++++ js/common.js | 24 +++++++++++++++++++ js/modules/Forms/EditorController.js | 14 ++++++++++- src/Html.php | 20 ++-------------- .../pages/admin/form/form_comment.html.twig | 2 +- .../pages/admin/form/form_question.html.twig | 2 +- .../pages/admin/form/form_section.html.twig | 2 +- tests/cypress/e2e/form/editor/editor.cy.js | 8 +++++++ tests/cypress/support/commands.js | 1 + 9 files changed, 55 insertions(+), 22 deletions(-) diff --git a/css/includes/components/form/_form-editor.scss b/css/includes/components/form/_form-editor.scss index 0f5f762ba9c..ff16e5dbce3 100644 --- a/css/includes/components/form/_form-editor.scss +++ b/css/includes/components/form/_form-editor.scss @@ -125,6 +125,10 @@ } } } + + [data-glpi-tinymce-init-on-demand-render] { + margin-left: 5px; + } } .editor-footer { diff --git a/js/common.js b/js/common.js index 685c0f1f150..72da960b085 100644 --- a/js/common.js +++ b/js/common.js @@ -1961,3 +1961,27 @@ document.addEventListener('hidden.bs.modal', (e) => { modal.setAttribute('data-cy-shown', 'false'); } }); + +// Tinymce on click loading +$(document).on('click', 'div[data-glpi-tinymce-init-on-demand-render]', function() { + const div = $(this); + const textarea_id = div.attr('data-glpi-tinymce-init-on-demand-render'); + div.removeAttr('data-glpi-tinymce-init-on-demand-render'); + const textarea = $("#" + textarea_id); + + const loadingOverlay = $(` +
+
+ ${__('Loading...')} +
+
+ `); + + textarea.show(); + div.css('position', 'relative').append(loadingOverlay); + tinyMCE.init(tinymce_editor_configs[textarea_id]).then((editors) => { + editors[0].focus(); + div.remove(); + }); +}); + diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index c9edcc43ce4..abd0c48402e 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -1426,11 +1426,23 @@ export class GlpiFormEditorController // the rich text editor until the template is inserted into // its final DOM destination config.selector = `#${CSS.escape(id)}`; - tiny_mce_to_init.push(config); // Store config with udpated ID in case we need to re render // this question window.tinymce_editor_configs[id] = config; + + // Update on demand id if needed + const div = $(this).parent().find( + 'div[data-glpi-tinymce-init-on-demand-render]' + ); + if (div.length > 0) { + div.attr( + 'data-glpi-tinymce-init-on-demand-render', + CSS.escape(id), + ); + } else { + tiny_mce_to_init.push(config); + } }); // Look for select2 to init diff --git a/src/Html.php b/src/Html.php index 974465cbe61..98e7f86ba6f 100644 --- a/src/Html.php +++ b/src/Html.php @@ -3743,25 +3743,9 @@ public static function initEditorSystem( if ({$init_on_demand}) { const textarea = $('#' + $.escapeSelector('{$id}')); - const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); + const textarea_id = $.escapeSelector('{$id}'); + const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); textarea.after(div).hide(); - - const loadingOverlay = $(` -
-
- \${__('Loading...')} -
-
- `); - - div.one('focus', function() { - textarea.show(); - div.css('position', 'relative').append(loadingOverlay); - tinyMCE.init(tinymce_editor_configs['{$id}']).then((editors) => { - editors[0].focus(); - div.remove(); - }); - }); } }); JS; diff --git a/templates/pages/admin/form/form_comment.html.twig b/templates/pages/admin/form/form_comment.html.twig index 646f29fcccc..3a4eaed9e9a 100644 --- a/templates/pages/admin/form/form_comment.html.twig +++ b/templates/pages/admin/form/form_comment.html.twig @@ -161,7 +161,7 @@ 'rows' : 1, 'toolbar_location': 'bottom', 'init': false, - 'init_on_demand': true, + 'init_on_demand': section is not null ? true : false, 'mb': 'mb-0', }) ) }} diff --git a/templates/pages/admin/form/form_question.html.twig b/templates/pages/admin/form/form_question.html.twig index c9038e289ee..9dddd022e9c 100644 --- a/templates/pages/admin/form/form_question.html.twig +++ b/templates/pages/admin/form/form_question.html.twig @@ -207,7 +207,7 @@ 'rows' : 1, 'toolbar_location': 'bottom', 'init': false, - 'init_on_demand': true, + 'init_on_demand': question is not null ? true : false, 'mb': 'mb-0', }) ) }} diff --git a/templates/pages/admin/form/form_section.html.twig b/templates/pages/admin/form/form_section.html.twig index ad64c317fed..795c1bc9d59 100644 --- a/templates/pages/admin/form/form_section.html.twig +++ b/templates/pages/admin/form/form_section.html.twig @@ -109,7 +109,7 @@ 'rows' : 1, 'toolbar_location': 'bottom', 'init': false, - 'init_on_demand': true, + 'init_on_demand': section is not null ? true : false, 'mb': 'mb-0', }) ) }} diff --git a/tests/cypress/e2e/form/editor/editor.cy.js b/tests/cypress/e2e/form/editor/editor.cy.js index 23828c5d326..38ab6fd5912 100644 --- a/tests/cypress/e2e/form/editor/editor.cy.js +++ b/tests/cypress/e2e/form/editor/editor.cy.js @@ -725,6 +725,10 @@ describe ('Form editor', () => { // Change question type cy.getDropdownByLabelText("Question type").selectDropdownValue('Date and time'); + // TODO: find something better than wait here + // eslint-disable-next-line + cy.wait(500); + // Save and reload cy.saveFormEditorAndReload(); @@ -856,6 +860,10 @@ describe ('Form editor', () => { }); cy.getDropdownByLabelText("Question type").selectDropdownValue('Long answer'); + // TODO: find something better than wait here + // eslint-disable-next-line + cy.wait(500); + // Save and reload cy.saveFormEditorAndReload(); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 43b49a53f2f..166d0b9f62b 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -181,6 +181,7 @@ Cypress.Commands.add('iframe', {prevSubject: 'element'}, (iframe, url_pattern) = Cypress.Commands.add('awaitTinyMCE', { prevSubject: 'element', }, (subject) => { + cy.wrap(subject).parent().click(); // Trigger lazy loading cy.wrap(subject).parent().find('div.tox-tinymce').should('exist').find('iframe').iframe('about:srcdoc').find('p', {timeout: 10000}); }); From cf2497c632f2850140a66f3cf5fd915a1af00038 Mon Sep 17 00:00:00 2001 From: Adrien Clairembault Date: Tue, 30 Sep 2025 13:56:11 +0200 Subject: [PATCH 16/18] Fix last test --- tests/cypress/e2e/ITILObject/ticket_form.cy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cypress/e2e/ITILObject/ticket_form.cy.js b/tests/cypress/e2e/ITILObject/ticket_form.cy.js index 508590b4e1d..5124954f55a 100644 --- a/tests/cypress/e2e/ITILObject/ticket_form.cy.js +++ b/tests/cypress/e2e/ITILObject/ticket_form.cy.js @@ -187,7 +187,7 @@ describe("Ticket Form", () => { }); cy.get('#modal_search_knowbaseitem').should('not.exist'); cy.get('@content').then((content) => { - cy.get('textarea[name="content"]').awaitTinyMCE().should('contain.text', content.trim()); + cy.get('textarea[name="content"]').eq(2).awaitTinyMCE().should('contain.text', content.trim()); }); cy.visit(`/front/ticket.form.php?id=${search_sol_ticket_id}`); @@ -210,7 +210,7 @@ describe("Ticket Form", () => { }); cy.get('#modal_search_knowbaseitem').should('not.exist'); cy.get('@content').then((content) => { - cy.get('textarea[name="content"]').awaitTinyMCE().should('contain.text', content.trim()); + cy.get('textarea[name="content"]').eq(0).awaitTinyMCE().should('contain.text', content.trim()); }); }); From 50bfcb8eafe3c6d55ec7306fc9356865d475fcb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= <42278610+ccailly@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:15:21 +0200 Subject: [PATCH 17/18] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cédric Anne --- js/modules/Forms/EditorController.js | 2 +- src/Html.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/js/modules/Forms/EditorController.js b/js/modules/Forms/EditorController.js index abd0c48402e..7f98ff15bae 100644 --- a/js/modules/Forms/EditorController.js +++ b/js/modules/Forms/EditorController.js @@ -1438,7 +1438,7 @@ export class GlpiFormEditorController if (div.length > 0) { div.attr( 'data-glpi-tinymce-init-on-demand-render', - CSS.escape(id), + id, ); } else { tiny_mce_to_init.push(config); diff --git a/src/Html.php b/src/Html.php index 98e7f86ba6f..5f37c035258 100644 --- a/src/Html.php +++ b/src/Html.php @@ -3742,9 +3742,8 @@ public static function initEditorSystem( } if ({$init_on_demand}) { - const textarea = $('#' + $.escapeSelector('{$id}')); - const textarea_id = $.escapeSelector('{$id}'); - const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); + const textarea = $('#{$id}')); + const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); textarea.after(div).hide(); } }); From 608087bcb01b90e1d1097b174f9110674b5b12e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cailly?= Date: Tue, 30 Sep 2025 16:18:32 +0200 Subject: [PATCH 18/18] Fix typo --- src/Html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Html.php b/src/Html.php index 5f37c035258..4d81679ed9f 100644 --- a/src/Html.php +++ b/src/Html.php @@ -3742,7 +3742,7 @@ public static function initEditorSystem( } if ({$init_on_demand}) { - const textarea = $('#{$id}')); + const textarea = $('#{$id}'); const div = $(`
\${textarea.val() || textarea.attr('placeholder') || ''}
`); textarea.after(div).hide(); }