From c13c80bad81364ba97ce1e1591d8d8bec7a333a2 Mon Sep 17 00:00:00 2001 From: AdrienClairembault Date: Fri, 3 May 2024 15:45:02 +0200 Subject: [PATCH 1/2] Add basic support for form tags --- ajax/form/form_tags.php | 56 +++++++++ css/includes/components/_richtext.scss | 7 ++ js/RichText/FormTags.js | 106 ++++++++++++++++++ .../CommonITILField/ContentField.php | 1 + src/Html.php | 1 + src/RichText/RichText.php | 1 + .../form/basic_inputs_macros.html.twig | 12 ++ tests/cypress/e2e/form_tags.cy.js | 91 +++++++++++++++ tests/units/Glpi/RichText/RichText.php | 33 ++++-- 9 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 ajax/form/form_tags.php create mode 100644 js/RichText/FormTags.js create mode 100644 tests/cypress/e2e/form_tags.cy.js diff --git a/ajax/form/form_tags.php b/ajax/form/form_tags.php new file mode 100644 index 00000000000..b3d556a78bb --- /dev/null +++ b/ajax/form/form_tags.php @@ -0,0 +1,56 @@ +. + * + * --------------------------------------------------------------------- + */ + +use Glpi\Form\Form; + +include('../../inc/includes.php'); + +// The user must be able to respond to forms. +Session::checkRight(Form::$rightname, UPDATE); + +// Get tags +$tags = [ + [ + 'label' => 'Exemple tag 1', + ], + [ + 'label' => 'Exemple tag 2', + ], + [ + 'label' => 'Exemple tag 3', + ], +]; +header('Content-Type: application/json'); +echo json_encode($tags); diff --git a/css/includes/components/_richtext.scss b/css/includes/components/_richtext.scss index 6e571d96b68..1897ccbe779 100644 --- a/css/includes/components/_richtext.scss +++ b/css/includes/components/_richtext.scss @@ -39,6 +39,7 @@ body.mce-content-body { margin: 15px 10px; + [data-form-tag="true"], [data-user-mention="true"] { cursor: default !important; // Overrides "not-allowed" cursor on noneditable content } @@ -100,3 +101,9 @@ body.mce-content-body { border-radius: 3px; padding: 5px; } + +[data-form-tag="true"] { + background: rgba(0, 0, 0, 20%); + border-radius: 3px; + padding: 5px; +} diff --git a/js/RichText/FormTags.js b/js/RichText/FormTags.js new file mode 100644 index 00000000000..f4cd219d5b6 --- /dev/null +++ b/js/RichText/FormTags.js @@ -0,0 +1,106 @@ +/** + * --------------------------------------------------------------------- + * + * GLPI - Gestionnaire Libre de Parc Informatique + * + * http://glpi-project.org + * + * @copyright 2015-2024 Teclib' and contributors. + * @copyright 2003-2014 by the INDEPNET Development Team. + * @licence https://www.gnu.org/licenses/gpl-3.0.html + * + * --------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of GLPI. + * + * 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 . + * + * --------------------------------------------------------------------- + */ + +/* global tinymce */ + +var GLPI = GLPI || {}; +GLPI.RichText = GLPI.RichText || {}; + +/** + * Form tags rich text autocompleter. + * + * @since 11.0.0 + */ +GLPI.RichText.FormTags = class +{ + /** + * Target tinymce editor. + * @type {TinyMCE.Editor} + */ + #editor; + + /** + * @param {Editor} editor + */ + constructor(editor) { + this.#editor = editor; + } + + /** + * Register as autocompleter to editor. + * + * @returns {void} + */ + register() { + // Register autocompleter + this.#editor.ui.registry.addAutocompleter( + 'form_tags', + { + trigger: '#', + minChars: 0, + fetch: () => this.#fetchItems(), + onAction: (autocompleteApi, range, value) => { + this.#insertTag(autocompleteApi, range, value); + } + } + ); + } + + async #fetchItems() { + const url = CFG_GLPI.root_doc + '/ajax/form/form_tags.php'; + const data = await $.get(url); + + return data.map((tag) => ({ + type: 'autocompleteitem', + value: JSON.stringify(tag), + text: tag.label, + })); + } + + #insertTag(autocompleteApi, range, value) { + this.#editor.selection.setRng(range); + this.#editor.insertContent(this.#generateTagHtml(value)); + + autocompleteApi.hide(); + } + + #generateTagHtml(value) { + const tag = JSON.parse(value); + return ` + ${tag.label}  + `; + } +}; diff --git a/src/Form/Destination/CommonITILField/ContentField.php b/src/Form/Destination/CommonITILField/ContentField.php index c3834f3fbb0..8aeccf57e6b 100644 --- a/src/Form/Destination/CommonITILField/ContentField.php +++ b/src/Form/Destination/CommonITILField/ContentField.php @@ -69,6 +69,7 @@ public function renderConfigForm( options|merge({ 'enable_richtext': true, 'enable_images': false, + 'enable_form_tags': true, }) ) }} TWIG; diff --git a/src/Html.php b/src/Html.php index 79c877d9f6e..81b8cbb30d0 100644 --- a/src/Html.php +++ b/src/Html.php @@ -5876,6 +5876,7 @@ public static function requireJs($name) break; case 'tinymce': $_SESSION['glpi_js_toload'][$name][] = 'public/lib/tinymce.js'; + $_SESSION['glpi_js_toload'][$name][] = 'js/RichText/FormTags.js'; $_SESSION['glpi_js_toload'][$name][] = 'js/RichText/UserMention.js'; $_SESSION['glpi_js_toload'][$name][] = 'js/RichText/ContentTemplatesParameters.js'; break; diff --git a/src/RichText/RichText.php b/src/RichText/RichText.php index a7c62b38fc5..19e152979f8 100644 --- a/src/RichText/RichText.php +++ b/src/RichText/RichText.php @@ -591,6 +591,7 @@ private static function getHtmlSanitizer(): HtmlSanitizer // required for user mentions 'data-user-mention', 'data-user-id', + 'data-form-tag', ]; foreach ($rich_text_completion_attributes as $attribute) { $config = $config->allowAttribute($attribute, 'span'); diff --git a/templates/components/form/basic_inputs_macros.html.twig b/templates/components/form/basic_inputs_macros.html.twig index 7933c0b9e98..9e4cb47c178 100644 --- a/templates/components/form/basic_inputs_macros.html.twig +++ b/templates/components/form/basic_inputs_macros.html.twig @@ -308,6 +308,7 @@ 'toolbar_location': 'top', 'init': true, 'placeholder': "", + 'enable_form_tags': false, }|merge(options) %} {% if options.fields_template.isMandatoryField(name) %} @@ -340,6 +341,17 @@ ]) %} {% endif %} + {% if options.enable_form_tags %} + + {% endif %} + {% if options.enable_mentions and config('use_notifications') %} @@ -127,7 +127,7 @@ protected function getSafeHtmlProvider(): iterable 'encode_output_entities' => false, 'expected_result' => <<Test - +

Hello world!

HTML, ]; @@ -240,12 +240,12 @@ public function () {

- + OK - + Opt 1 Opt 2 - + Some textarea content
@@ -278,9 +278,9 @@ public function () {

Comments and CDATA should be removed

- + Legit text - +

Uppercase tag will be normalized to lowercase tag

@@ -386,6 +386,17 @@ public function () {

HTML, ]; + yield 'Do not remove content editable on span' => [ + 'content' => 'Editable content', + 'encode_output_entities' => false, + 'expected_result' => 'Editable content', + ]; + + yield 'Do not remove data-form-tag property' => [ + 'content' => 'Tag label', + 'encode_output_entities' => false, + 'expected_result' => 'Tag label', + ]; } /** @@ -505,7 +516,7 @@ protected function getTextFromHtmlProvider(): iterable * el 1 * el 2 - [an image] [{$base_url}/glpi/front/computer.form.php?id=150] Should I yell FOR THE IMPORTANT WORDS? + [an image] [{$base_url}/glpi/front/computer.form.php?id=150] Should I yell FOR THE IMPORTANT WORDS? PLAINTEXT, ]; @@ -526,7 +537,7 @@ protected function getTextFromHtmlProvider(): iterable * el 1 * el 2 - [an image] Should I yell FOR THE IMPORTANT WORDS? + [an image] Should I yell FOR THE IMPORTANT WORDS? PLAINTEXT, ]; @@ -547,7 +558,7 @@ protected function getTextFromHtmlProvider(): iterable * el 1 * el 2 - [an image] Should I yell for the important words? + [an image] Should I yell for the important words? PLAINTEXT, ]; } From 383c54baa1a9277247410fa91f5cc987faec7e0f Mon Sep 17 00:00:00 2001 From: AdrienClairembault Date: Wed, 15 May 2024 10:10:53 +0200 Subject: [PATCH 2/2] Remove accidental whitespace changes --- tests/units/Glpi/RichText/RichText.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/units/Glpi/RichText/RichText.php b/tests/units/Glpi/RichText/RichText.php index 152b31bbcc6..92c1daf9de8 100644 --- a/tests/units/Glpi/RichText/RichText.php +++ b/tests/units/Glpi/RichText/RichText.php @@ -113,13 +113,13 @@ protected function getSafeHtmlProvider(): iterable

Test

- + - +

Hello world!

@@ -127,7 +127,7 @@ protected function getSafeHtmlProvider(): iterable 'encode_output_entities' => false, 'expected_result' => <<Test - +

Hello world!

HTML, ]; @@ -240,12 +240,12 @@ public function () {

- + OK - + Opt 1 Opt 2 - + Some textarea content
@@ -278,9 +278,9 @@ public function () {

Comments and CDATA should be removed

- + Legit text - +

Uppercase tag will be normalized to lowercase tag

@@ -516,7 +516,7 @@ protected function getTextFromHtmlProvider(): iterable * el 1 * el 2 - [an image] [{$base_url}/glpi/front/computer.form.php?id=150] Should I yell FOR THE IMPORTANT WORDS? + [an image] [{$base_url}/glpi/front/computer.form.php?id=150] Should I yell FOR THE IMPORTANT WORDS? PLAINTEXT, ]; @@ -537,7 +537,7 @@ protected function getTextFromHtmlProvider(): iterable * el 1 * el 2 - [an image] Should I yell FOR THE IMPORTANT WORDS? + [an image] Should I yell FOR THE IMPORTANT WORDS? PLAINTEXT, ]; @@ -558,7 +558,7 @@ protected function getTextFromHtmlProvider(): iterable * el 1 * el 2 - [an image] Should I yell for the important words? + [an image] Should I yell for the important words? PLAINTEXT, ]; }