Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,14 @@
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;
}
}

[data-glpi-form-editor-required-mark] {
display: none !important;
}
}

height: 100%;
Expand Down
38 changes: 32 additions & 6 deletions css/includes/components/form/_form-editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,17 @@
}
}
}

[data-glpi-tinymce-init-on-demand-render] {
margin-left: 5px;
}
}

.editor-footer {
border-top: 1px solid var(--tblr-border-color);
}

// 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;

Expand Down Expand Up @@ -586,3 +584,31 @@ 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;
cursor: text;
}

// 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
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;

// Hide the div when the editor is loading
&:has(.glpi-form-editor-loading-overlay) {
display: none !important;
}
}
}
}
}
11 changes: 11 additions & 0 deletions css/includes/components/form/_item-translations.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
90 changes: 24 additions & 66 deletions js/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -2027,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 = $(`
<div class="glpi-form-editor-loading-overlay position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-75">
<div class="spinner-border spinner-border-sm text-secondary" role="status">
<span class="visually-hidden">${__('Loading...')}</span>
</div>
</div>
`);

textarea.show();
div.css('position', 'relative').append(loadingOverlay);
tinyMCE.init(tinymce_editor_configs[textarea_id]).then((editors) => {
editors[0].focus();
div.remove();
});
});

76 changes: 14 additions & 62 deletions js/modules/Forms/EditorController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -410,19 +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;

// 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(
Expand Down Expand Up @@ -943,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
Expand Down Expand Up @@ -1089,11 +1060,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);
}
Expand Down Expand Up @@ -1406,19 +1372,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
Expand Down Expand Up @@ -1473,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
Expand Down Expand Up @@ -1750,14 +1715,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", getRealInputWidth(input, "1.2rem"));
}

/**
* Set or remove loading state for question type specific content.
* This makes the content appear disabled and non-interactive during condition checks.
Expand Down Expand Up @@ -2151,11 +2108,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);
}
Expand Down
4 changes: 2 additions & 2 deletions js/modules/Forms/QuestionDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export class GlpiFormQuestionTypeDropdown extends GlpiFormQuestionTypeSelectable
* @param {string} inputType
* @param {JQuery<HTMLElement>} 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());

Expand Down
9 changes: 7 additions & 2 deletions js/modules/Forms/QuestionSelectable.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class GlpiFormQuestionTypeSelectable {
*
* @param {JQuery<HTMLElement>} container
*/
constructor(inputType = null, container = null) {
constructor(inputType = null, container = null, is_from_template = false) {
this._inputType = inputType;
this._container = $(container);

Expand All @@ -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') {
Expand Down
7 changes: 7 additions & 0 deletions src/CommonDBTM.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading