mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
N°8772 - Form dependencies manager implementation
- Form SDK implementation - Basic Forms - Dynamics Forms - Basic Blocks + Data Model Block - Form Compilation - Turbo integration
This commit is contained in:
45
js/forms/choices_element.js
Normal file
45
js/forms/choices_element.js
Normal file
@@ -0,0 +1,45 @@
|
||||
class ChoicesElement extends HTMLSelectElement {
|
||||
|
||||
// register the custom element
|
||||
static {
|
||||
customElements.define('choices-element', ChoicesElement, {extends: 'select'});
|
||||
}
|
||||
|
||||
plugins = [];
|
||||
connectedCallback() {
|
||||
|
||||
if (this.tomselect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getAttribute('multiple')) {
|
||||
this.plugins.push('remove_button');
|
||||
}
|
||||
|
||||
const options = {
|
||||
plugins: this.plugins,
|
||||
wrapperClass: 'ts-wrapper ibo-input-wrapper ibo-input-select-wrapper--with-buttons ibo-input-select-autocomplete-wrapper',
|
||||
controlClass: 'ts-control ibo-input ibo-input-select ibo-input-select-autocomplete',
|
||||
dropdownParent: 'body',
|
||||
render: {
|
||||
dropdown: function (data, escape) {
|
||||
return `<div class="selectize-dropdown"></div>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (this.getAttribute('data-tom-select-disable-auto-complete')) {
|
||||
// options.controlInput = null;
|
||||
}
|
||||
if (this.getAttribute('data-tom-select-max-items-selected') && this.getAttribute('data-tom-select-max-items-selected') !== '') {
|
||||
options.maxItems = parseInt(this.getAttribute('data-tom-select-max-items-selected'));
|
||||
}
|
||||
if (this.getAttribute('data-tom-select-placehelder')) {
|
||||
options.placeholder = this.getAttribute('data-tom-select-placehelder');
|
||||
}
|
||||
|
||||
new TomSelect(this, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
39
js/forms/collection_element.js
Normal file
39
js/forms/collection_element.js
Normal file
@@ -0,0 +1,39 @@
|
||||
class CollectionElement extends HTMLElement {
|
||||
|
||||
#eBtnAdd;
|
||||
|
||||
// register the custom element
|
||||
static {
|
||||
customElements.define('collection-element', CollectionElement);
|
||||
}
|
||||
|
||||
addFormToCollection(e) {
|
||||
const collectionHolder = document.querySelector('.'+e.currentTarget.dataset.collectionHolderClass);
|
||||
const item = document.createElement('div');
|
||||
|
||||
const collectionHolderList = collectionHolder.querySelector('[role="list"]');
|
||||
|
||||
item.innerHTML = collectionHolder
|
||||
.dataset
|
||||
.prototype
|
||||
.replace(
|
||||
/__name__/g,
|
||||
collectionHolder.dataset.index
|
||||
);
|
||||
|
||||
collectionHolderList.appendChild(item.firstChild);
|
||||
collectionHolder.dataset.index++;
|
||||
|
||||
this.querySelectorAll('collection-entry-element').forEach((entry) => {
|
||||
console.log('test');
|
||||
entry.updateButtonStates();
|
||||
});
|
||||
}
|
||||
|
||||
/** connectedCallback **/
|
||||
connectedCallback() {
|
||||
this.#eBtnAdd = this.querySelector('.add_item_link');
|
||||
this.#eBtnAdd.addEventListener('click', this.addFormToCollection.bind(this));
|
||||
}
|
||||
|
||||
}
|
||||
112
js/forms/collection_entry_element.js
Normal file
112
js/forms/collection_entry_element.js
Normal file
@@ -0,0 +1,112 @@
|
||||
class CollectionEntryElement extends HTMLElement {
|
||||
|
||||
// Button elements
|
||||
#eBtnDelete;
|
||||
#eBtnMoveUp;
|
||||
#eBtnMoveDown;
|
||||
|
||||
// register the custom element
|
||||
static {
|
||||
customElements.define('collection-entry-element', CollectionEntryElement);
|
||||
}
|
||||
|
||||
/** connectedCallback **/
|
||||
connectedCallback() {
|
||||
|
||||
if ((this.dataset.new || this.dataset.allowDelete) && this.#eBtnDelete === undefined) {
|
||||
this.#eBtnDelete = this.#createButton('Remove', 'ibo-button ibo-is-regular ibo-is-danger');
|
||||
this.#eBtnDelete.addEventListener('click', this.#removeCollectionItem.bind(this));
|
||||
this.appendChild(this.#eBtnDelete);
|
||||
}
|
||||
|
||||
if (this.dataset.allowOrdering) {
|
||||
if (this.#eBtnMoveUp === undefined) {
|
||||
this.#eBtnMoveUp = this.#createButton('Move Up', 'ibo-button ibo-is-regular');
|
||||
this.#eBtnMoveUp.addEventListener('click', this.#moveUp.bind(this));
|
||||
this.appendChild(this.#eBtnMoveUp);
|
||||
}
|
||||
if (this.#eBtnMoveDown === undefined) {
|
||||
this.#eBtnMoveDown = this.#createButton('Move Down', 'ibo-button ibo-is-regular');
|
||||
this.#eBtnMoveDown.addEventListener('click', this.#moveDown.bind(this));
|
||||
this.appendChild(this.#eBtnMoveDown);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateButtonStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of the buttons (enabled/disabled).
|
||||
*
|
||||
*/
|
||||
updateButtonStates() {
|
||||
|
||||
if (this.dataset.allowOrdering) {
|
||||
|
||||
if (this.previousElementSibling === null) {
|
||||
this.#eBtnMoveUp.setAttribute('disabled', 'disabled');
|
||||
} else {
|
||||
this.#eBtnMoveUp.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
if (this.nextElementSibling === null) {
|
||||
this.#eBtnMoveDown.setAttribute('disabled', 'disabled');
|
||||
} else {
|
||||
this.#eBtnMoveDown.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a button element.
|
||||
*
|
||||
* @param label
|
||||
* @param className
|
||||
* @returns {HTMLButtonElement}
|
||||
*/
|
||||
#createButton(label, className) {
|
||||
|
||||
const btnElement = document.createElement('button');
|
||||
btnElement.type = 'button';
|
||||
btnElement.className = className;
|
||||
btnElement.textContent = label;
|
||||
|
||||
return btnElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this collection item up.
|
||||
*
|
||||
*/
|
||||
#moveUp() {
|
||||
const prev = this.previousElementSibling;
|
||||
if (prev) {
|
||||
this.parentNode.insertBefore(this, prev);
|
||||
this.updateButtonStates();
|
||||
prev.updateButtonStates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this collection item down.
|
||||
*
|
||||
*/
|
||||
#moveDown() {
|
||||
const next = this.nextElementSibling;
|
||||
if (next) {
|
||||
this.parentNode.insertBefore(next, this);
|
||||
this.updateButtonStates();
|
||||
next.updateButtonStates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this collection item.
|
||||
*
|
||||
*/
|
||||
#removeCollectionItem() {
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
95
js/forms/form_element.js
Normal file
95
js/forms/form_element.js
Normal file
@@ -0,0 +1,95 @@
|
||||
class FormElement extends HTMLFormElement
|
||||
{
|
||||
static #TURBO_REFRESHING_CLASS = 'turbo-refreshing';
|
||||
static #TURBO_TRIGGER_FIELD = '_turbo_trigger';
|
||||
|
||||
#aFormBlockDataTransmittedData = {};
|
||||
|
||||
// register the custom element
|
||||
static {
|
||||
customElements.define('itop-form-element', FormElement, {extends: 'form'});
|
||||
}
|
||||
|
||||
TriggerTurbo(oElement) {
|
||||
|
||||
// Get the name and id of the element triggering turbo
|
||||
const sName = oElement.getAttribute('name');
|
||||
const sId = oElement.getAttribute('id');
|
||||
|
||||
if(FormElement.IsCheckbox(oElement) || this.#aFormBlockDataTransmittedData[sName] !== oElement.value) {
|
||||
|
||||
// Refresh UI
|
||||
this.#StartRefreshingUI(sId);
|
||||
|
||||
// Pre Submit
|
||||
this.#PreSubmitTurboForm(sName);
|
||||
|
||||
// Submit
|
||||
oElement.form.requestSubmit();
|
||||
|
||||
// Post Submit
|
||||
this.#PostSubmitTurboForm(sName)
|
||||
|
||||
this.#aFormBlockDataTransmittedData[sName] = oElement.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Start refreshing UI.
|
||||
*
|
||||
* @param sId
|
||||
* @constructor
|
||||
*/
|
||||
#StartRefreshingUI(sId)
|
||||
{
|
||||
Array.from(this.querySelectorAll(`.ibo-content-block`)).forEach(block => {
|
||||
if(block.dataset.impactedBy !== undefined){
|
||||
const aImpactedBy = block.dataset.impactedBy.split(',');
|
||||
if(aImpactedBy.includes(sId)){
|
||||
block.classList.add(FormElement.#TURBO_REFRESHING_CLASS);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre submit the form.
|
||||
* Set the turbo trigger field in the form and disable validation
|
||||
*
|
||||
* @param sName
|
||||
* @constructor
|
||||
*/
|
||||
#PreSubmitTurboForm(sName)
|
||||
{
|
||||
this.querySelector(`[name="${this.getAttribute("name")}[${FormElement.#TURBO_TRIGGER_FIELD}]"]`).value = sName;
|
||||
this.setAttribute('novalidate', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post submit the form.
|
||||
* Reset the turbo trigger field and restore form validation.
|
||||
*
|
||||
* @param sName
|
||||
* @constructor
|
||||
*/
|
||||
#PostSubmitTurboForm(sName)
|
||||
{
|
||||
this.querySelector(`[name="${this.getAttribute("name")}[${FormElement.#TURBO_TRIGGER_FIELD}]"]`).value = null;
|
||||
this.removeAttribute('novalidate');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param oElement
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static IsCheckbox (oElement)
|
||||
{
|
||||
return oElement instanceof HTMLInputElement
|
||||
&& oElement.getAttribute('type') === 'checkbox'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
123
js/forms/oql_element.js
Normal file
123
js/forms/oql_element.js
Normal file
@@ -0,0 +1,123 @@
|
||||
class OqlElement extends HTMLTextAreaElement {
|
||||
|
||||
static #DEBONCE = 400;
|
||||
|
||||
// register the custom element
|
||||
static{
|
||||
customElements.define('oql-element', OqlElement, {extends: 'textarea'});
|
||||
}
|
||||
|
||||
// variables
|
||||
#url = '../pages/ajax.render.php?route=oql.validate_query';
|
||||
#iconValid = 'fa-check-double';
|
||||
#iconNotValid = 'fa-exclamation-triangle';
|
||||
#debounceTimer = null;
|
||||
#debounce = OqlElement.#DEBONCE;
|
||||
|
||||
/** connectedCallback **/
|
||||
connectedCallback() {
|
||||
this.addEventListener('input', this.#onInput.bind(this));
|
||||
this.#callValidateQuery();
|
||||
|
||||
this.addEventListener('focus', this.#onFocus.bind(this));
|
||||
|
||||
const oBtnBook = this.closest('.ibo-content-block').querySelector('[data-role="ibo-button"][data-action="book"]');
|
||||
oBtnBook.addEventListener('click', this.#search.bind(this))
|
||||
|
||||
const oBtnRun = this.closest('.ibo-content-block').querySelector('[data-role="ibo-button"][data-action="run"]');
|
||||
oBtnRun.addEventListener('click', this.#run.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Call oql verification with debounce when input event is fired.
|
||||
*/
|
||||
#onInput() {
|
||||
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
||||
this.#debounceTimer = setTimeout(() => {
|
||||
this.#callValidateQuery(true);
|
||||
}, this.#debounce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call oql verification with debounce when focus event is fired.
|
||||
*/
|
||||
#onFocus() {
|
||||
this.#callValidateQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the ajax to validate the query.
|
||||
*
|
||||
* @param fireChange flag to handle change event
|
||||
*/
|
||||
#callValidateQuery(fireChange = false) {
|
||||
|
||||
fetch(this.#url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Combodo-Ajax': true
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: this.value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// fire change event only if the query is valid
|
||||
if (fireChange && response.is_valid){
|
||||
this.#fireChangeEvent();
|
||||
}
|
||||
// update the icon color
|
||||
const fieldEl = this.closest('.ibo-field');
|
||||
const marqueeEl = fieldEl.querySelector('[role="marquee"]');
|
||||
marqueeEl.style.color = response.is_valid ? 'green' : 'orange';
|
||||
marqueeEl.classList.toggle(this.#iconNotValid, !response.is_valid);
|
||||
marqueeEl.classList.toggle(this.#iconValid, response.is_valid);
|
||||
marqueeEl.setAttribute('title', response.is_valid ? Dict.S(this.dataset.validQueryText) : Dict.S(this.dataset.invalidQueryText));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a change event.
|
||||
*/
|
||||
#fireChangeEvent() {
|
||||
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
|
||||
this.dispatchEvent(changeEvent);
|
||||
}
|
||||
|
||||
#search(){
|
||||
const sId = this.getAttribute('id');
|
||||
const sDialogId = `ac_dlg_${sId}`;
|
||||
|
||||
const sModalTitle = Dict.S(this.dataset.modalTitleText);
|
||||
const sEmptyText = Dict.S(this.dataset.emptyText);
|
||||
|
||||
// Instance the widget
|
||||
const oACWidget = new ExtKeyWidget(sId, 'QueryOQL', 'SELECT QueryOQL WHERE is_template = \'yes\'', sModalTitle, true, null, null, true, true, 'oql');
|
||||
oACWidget.emptyHtml = `<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p><${sEmptyText}/p></div>`;
|
||||
|
||||
// Store in window to be accessible from dialog
|
||||
window[`oACWidget_${sId}`] = oACWidget;
|
||||
|
||||
// Open the dialog
|
||||
if ($(`#${sDialogId}`).length === 0)
|
||||
{
|
||||
$('body').append(`<div id="${sDialogId}"></div>`);
|
||||
$(`#${sDialogId}`).dialog({
|
||||
width: $(window).width()*0.8,
|
||||
height: $(window).height()*0.8,
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
resizeStop: oACWidget.UpdateSizes,
|
||||
});
|
||||
}
|
||||
|
||||
// Start searching
|
||||
oACWidget.Search();
|
||||
}
|
||||
|
||||
#run(){
|
||||
window.open('../pages/run_query.php?expression=' + encodeURI(this.value), '_blank');
|
||||
}
|
||||
}
|
||||
26
js/forms/turbo_stream_event_element.js
Normal file
26
js/forms/turbo_stream_event_element.js
Normal file
@@ -0,0 +1,26 @@
|
||||
class TurboStreamEvent extends HTMLElement {
|
||||
|
||||
// register the custom element
|
||||
static {
|
||||
customElements.define('turbo-stream-event', TurboStreamEvent);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.style.display = 'none';
|
||||
|
||||
const event = new CustomEvent("itop:TurboStreamEvent", {
|
||||
detail: {
|
||||
id: this.getAttribute('id'),
|
||||
form_id: this.dataset.formId,
|
||||
block_class: this.dataset.formBlockClass,
|
||||
view_data: this.dataset.viewData,
|
||||
valid: this.dataset.valid,
|
||||
},
|
||||
});
|
||||
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user