Commit ca300de5 authored by Andreas Fernandez's avatar Andreas Fernandez Committed by Anja Leichsenring
Browse files

[TASK] Remove jQuery from some FormEngine components

This patch removes jQuery from a major part of FormEngine components,
including elements, field wizards, enhancers and IRRE.

Additionally, the argument `optionEl` of the method
`FormEngine.setSelectOptionFromExternalSource()` accepts objects of type
`HTMLOptionElement` which renders jQuery objects obsolete, which has
been deprecated.

Resolves: #91911
Releases: master
Change-Id: I5b7d2f4c7b9b5993a5bbe8cec016aa74cabbbecd
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/65009

Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Stefan Froemken's avatarStefan Froemken <froemken@gmail.com>
Tested-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Stefan Froemken's avatarStefan Froemken <froemken@gmail.com>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@richardhaeser.com>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
parent 571ae71a
......@@ -103,7 +103,11 @@ class DateTimePicker {
}
$hiddenField.val(value);
$(document).trigger('formengine.dp.change', [$element]);
document.dispatchEvent(new CustomEvent('formengine.dp.change', {
detail: {
element: $element,
}
}));
});
});
}
......
......@@ -11,11 +11,11 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import {MessageUtility} from '../../Utility/MessageUtility';
import {AjaxDispatcher} from './../InlineRelation/AjaxDispatcher';
import {InlineResponseInterface} from './../InlineRelation/InlineResponseInterface';
import DocumentService = require('TYPO3/CMS/Core/DocumentService');
import NProgress = require('nprogress');
import Sortable = require('Sortable');
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
......@@ -214,7 +214,7 @@ class InlineControlContainer {
* @param {string} elementId
*/
constructor(elementId: string) {
$((): void => {
DocumentService.ready().then((document: Document): void => {
this.container = <HTMLElement>document.getElementById(elementId);
this.ajaxDispatcher = new AjaxDispatcher(this.container.dataset.objectGroup);
......@@ -406,7 +406,7 @@ class InlineControlContainer {
const hiddenClass = 't3-form-field-container-inline-hidden';
const isHidden = recordContainer.classList.contains(hiddenClass);
let toggleIcon: string = '';
let toggleIcon: string;
if (isHidden) {
toggleIcon = 'actions-edit-hide';
......@@ -640,7 +640,7 @@ class InlineControlContainer {
(<HTMLInputElement>formField).value = records.join(',');
(<HTMLInputElement>formField).classList.add('has-change');
$(document).trigger('change');
document.dispatchEvent(new Event('change'));
this.redrawSortingButtons(this.container.dataset.objectGroup, records);
this.setUnique(newUid, selectedValue);
......@@ -669,7 +669,7 @@ class InlineControlContainer {
(<HTMLInputElement>formField).value = records.join(',');
(<HTMLInputElement>formField).classList.add('has-change');
$(document).trigger('change');
document.dispatchEvent(new Event('change'));
this.redrawSortingButtons(this.container.dataset.objectGroup, records);
}
......@@ -722,8 +722,8 @@ class InlineControlContainer {
(<HTMLInputElement>formField).value = records.join(',');
(<HTMLInputElement>formField).classList.add('has-change');
$(document).trigger('inline:sorting-changed');
$(document).trigger('change');
document.dispatchEvent(new Event('inline:sorting-changed'));
document.dispatchEvent(new Event('change'));
this.redrawSortingButtons(this.container.dataset.objectGroup, records);
}
......
......@@ -11,7 +11,7 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
enum Selectors {
fieldContainerSelector = '.t3js-formengine-field-group',
......@@ -25,7 +25,7 @@ enum Selectors {
class SelectBoxFilter {
private selectElement: HTMLSelectElement = null;
private filterText: string = '';
private $availableOptions: JQuery = null;
private availableOptions: NodeListOf<HTMLOptionElement> = null;
constructor(selectElement: HTMLSelectElement) {
this.selectElement = selectElement;
......@@ -39,16 +39,13 @@ class SelectBoxFilter {
return;
}
wizardsElement.addEventListener('keyup', (e: Event): void => {
if ((<HTMLElement>e.target).matches(Selectors.filterTextFieldSelector)) {
this.filter((<HTMLInputElement>e.target).value);
}
});
wizardsElement.addEventListener('change', (e: Event): void => {
if ((<HTMLElement>e.target).matches(Selectors.filterSelectFieldSelector)) {
this.filter((<HTMLInputElement>e.target).value);
}
});
new RegularEvent('input', (e: Event): void => {
this.filter((<HTMLInputElement>e.target).value);
}).delegateTo(wizardsElement, Selectors.filterTextFieldSelector);
new RegularEvent('change', (e: Event): void => {
this.filter((<HTMLInputElement>e.target).value);
}).delegateTo(wizardsElement, Selectors.filterSelectFieldSelector);
}
/**
......@@ -58,17 +55,13 @@ class SelectBoxFilter {
*/
private filter(filterText: string): void {
this.filterText = filterText;
if (!this.$availableOptions) {
this.$availableOptions = $(this.selectElement).find('option').clone();
if (this.availableOptions === null) {
this.availableOptions = this.selectElement.querySelectorAll('option');
}
this.selectElement.innerHTML = '';
const matchFilter = new RegExp(filterText, 'i');
this.$availableOptions.each((i: number, el: HTMLElement): void => {
if (filterText.length === 0 || el.textContent.match(matchFilter)) {
this.selectElement.appendChild(el);
}
this.availableOptions.forEach((option: HTMLOptionElement): void => {
option.hidden = filterText.length > 0 && option.textContent.match(matchFilter) === null;
});
}
}
......
......@@ -11,12 +11,13 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import DocumentService = require('TYPO3/CMS/Core/DocumentService');
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
class InputDateTimeElement {
constructor(elementId: string) {
$((): void => {
DocumentService.ready().then((): void => {
this.registerEventHandler();
require(['../../DateTimePicker'], (DateTimePicker: any): void => {
DateTimePicker.initialize('#' + elementId)
......@@ -25,11 +26,15 @@ class InputDateTimeElement {
}
private registerEventHandler(): void {
$(document).on('formengine.dp.change', (event: JQueryEventObject, $field: JQuery): void => {
new RegularEvent('formengine.dp.change', (e: CustomEvent): void => {
FormEngine.Validation.validate();
FormEngine.Validation.markFieldAsChanged($field);
$('.module-docheader-bar .btn').removeClass('disabled').prop('disabled', false);
});
FormEngine.Validation.markFieldAsChanged(e.detail.element);
document.querySelectorAll('.module-docheader-bar .btn').forEach((btn: HTMLButtonElement): void => {
btn.classList.remove('disabled');
btn.disabled = false;
});
}).bindTo(document);
}
}
......
......@@ -11,7 +11,7 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import DocumentService = require('TYPO3/CMS/Core/DocumentService');
enum Selectors {
toggleSelector = '.t3js-form-field-inputlink-explanation-toggle',
......@@ -28,7 +28,7 @@ class InputLinkElement {
private icon: HTMLSpanElement = null;
constructor(elementId: string) {
$((): void => {
DocumentService.ready().then((document: Document): void => {
this.element = <HTMLSelectElement>document.getElementById(elementId);
this.container = <HTMLElement>this.element.closest('.t3js-form-field-inputlink');
this.toggleSelector = <HTMLButtonElement>this.container.querySelector(Selectors.toggleSelector);
......
......@@ -11,8 +11,8 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import DocumentService = require('TYPO3/CMS/Core/DocumentService');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
enum Identifier {
toggleAll = '.t3js-toggle-checkboxes',
......@@ -22,17 +22,18 @@ enum Identifier {
class SelectCheckBoxElement {
private checkBoxId: string = '';
private $table: JQuery = null;
private checkedBoxes: JQuery = null;
private table: HTMLTableElement = null;
private checkedBoxes: NodeListOf<HTMLInputElement> = null;
/**
* Determines whether all available checkboxes are checked
*
* @param {JQuery} $checkBoxes
* @param {NodeListOf<HTMLInputElement>} checkBoxes
* @return {boolean}
*/
private static allCheckBoxesAreChecked($checkBoxes: JQuery): boolean {
return $checkBoxes.length === $checkBoxes.filter(':checked').length;
private static allCheckBoxesAreChecked(checkBoxes: NodeListOf<HTMLInputElement>): boolean {
const checkboxArray = Array.from(checkBoxes);
return checkBoxes.length === checkboxArray.filter((checkBox: HTMLInputElement) => checkBox.checked).length;
}
/**
......@@ -40,9 +41,9 @@ class SelectCheckBoxElement {
*/
constructor(checkBoxId: string) {
this.checkBoxId = checkBoxId;
$((): void => {
this.$table = $('#' + checkBoxId).closest('table');
this.checkedBoxes = this.$table.find(Identifier.singleItem + ':checked');
DocumentService.ready().then((document: Document): void => {
this.table = document.getElementById(checkBoxId).closest('table');
this.checkedBoxes = this.table.querySelectorAll(Identifier.singleItem + ':checked');
this.enableTriggerCheckBox();
this.registerEventHandler();
......@@ -53,38 +54,39 @@ class SelectCheckBoxElement {
* Registers the events for clicking the "Toggle all" and the single item checkboxes
*/
private registerEventHandler(): void {
this.$table.on('change', Identifier.toggleAll, (e: JQueryEventObject): void => {
const $me = $(e.currentTarget);
const $checkBoxes = this.$table.find(Identifier.singleItem);
const checkIt = !SelectCheckBoxElement.allCheckBoxesAreChecked($checkBoxes);
new RegularEvent('change', (e: Event, currentTarget: HTMLInputElement): void => {
const checkBoxes: NodeListOf<HTMLInputElement> = this.table.querySelectorAll(Identifier.singleItem);
const checkIt = !SelectCheckBoxElement.allCheckBoxesAreChecked(checkBoxes);
$checkBoxes.prop('checked', checkIt);
$me.prop('checked', checkIt);
FormEngine.Validation.markFieldAsChanged($me);
}).on('change', Identifier.singleItem, (): void => {
this.setToggleAllState();
}).on('click', Identifier.revertSelection, (): void => {
this.$table.find(Identifier.singleItem).each((_: number, checkbox: HTMLInputElement): void => {
checkbox.checked = this.checkedBoxes.index(checkbox) > -1;
checkBoxes.forEach((checkBox: HTMLInputElement): void => {
checkBox.checked = checkIt;
});
currentTarget.checked = checkIt;
}).delegateTo(this.table, Identifier.toggleAll);
new RegularEvent('change', this.setToggleAllState.bind(this)).delegateTo(this.table, Identifier.singleItem);
new RegularEvent('click', (): void => {
const checkBoxes = this.table.querySelectorAll(Identifier.singleItem);
const checkedCheckBoxesAsArray = Array.from(this.checkedBoxes);
checkBoxes.forEach((checkBox: HTMLInputElement): void => {
checkBox.checked = checkedCheckBoxesAsArray.includes(checkBox);
});
this.setToggleAllState();
});
}).delegateTo(this.table, Identifier.revertSelection);
}
private setToggleAllState(): void {
const $checkBoxes = this.$table.find(Identifier.singleItem);
const checkIt = SelectCheckBoxElement.allCheckBoxesAreChecked($checkBoxes);
this.$table.find(Identifier.toggleAll).prop('checked', checkIt);
const checkBoxes: NodeListOf<HTMLInputElement> = this.table.querySelectorAll(Identifier.singleItem);
(this.table.querySelector(Identifier.toggleAll) as HTMLInputElement).checked = SelectCheckBoxElement.allCheckBoxesAreChecked(checkBoxes);
}
/**
* Enables the "Toggle all" checkbox on document load if all child checkboxes are checked
*/
private enableTriggerCheckBox(): void {
const $checkBoxes = this.$table.find(Identifier.singleItem);
const checkIt = SelectCheckBoxElement.allCheckBoxesAreChecked($checkBoxes);
$('#' + this.checkBoxId).prop('checked', checkIt);
const checkBoxes: NodeListOf<HTMLInputElement> = this.table.querySelectorAll(Identifier.singleItem);
(document.getElementById(this.checkBoxId) as HTMLInputElement).checked = SelectCheckBoxElement.allCheckBoxesAreChecked(checkBoxes);
}
}
......
......@@ -12,7 +12,7 @@
*/
import {AbstractSortableSelectItems} from './AbstractSortableSelectItems';
import $ from 'jquery';
import DocumentService = require('TYPO3/CMS/Core/DocumentService');
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import SelectBoxFilter = require('./Extra/SelectBoxFilter');
......@@ -23,7 +23,7 @@ class SelectMultipleSideBySideElement extends AbstractSortableSelectItems {
constructor(selectedOptionsElementId: string, availableOptionsElementId: string) {
super();
$((): void => {
DocumentService.ready().then((document: Document): void => {
this.selectedOptionsElement = <HTMLSelectElement>document.getElementById(selectedOptionsElementId);
this.availableOptionsElement = <HTMLSelectElement>document.getElementById(availableOptionsElementId);
this.registerEventHandler();
......@@ -47,7 +47,7 @@ class SelectMultipleSideBySideElement extends AbstractSortableSelectItems {
optionElement.textContent,
optionElement.getAttribute('title'),
exclusiveValues,
$(optionElement),
optionElement,
);
});
}
......
......@@ -11,7 +11,7 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
interface SelectSingleElementOptions {
[key: string]: any;
......@@ -23,43 +23,52 @@ interface SelectSingleElementOptions {
*/
class SelectSingleElement {
public initialize = (selector: string, options: SelectSingleElementOptions): void => {
let $selectElement: JQuery = $(selector);
let $groupIconContainer: JQuery = $selectElement.prev('.input-group-icon');
let selectElement: HTMLSelectElement = document.querySelector(selector);
options = options || {};
$selectElement.on('change', (e: JQueryEventObject): void => {
let $me: JQuery = $(e.target);
new RegularEvent('change', (e: Event): void => {
const target = e.target as HTMLSelectElement;
const groupIconContainer: HTMLElement = target.parentElement.querySelector('.input-group-icon');
// Update prepended select icon
$groupIconContainer.html($selectElement.find(':selected').data('icon'));
if (groupIconContainer !== null) {
groupIconContainer.innerHTML = (target.options[target.selectedIndex].dataset.icon);
}
let $selectIcons: JQuery = $me.closest('.t3js-formengine-field-item').find('.t3js-forms-select-single-icons');
$selectIcons.find('.item.active').removeClass('active');
$selectIcons.find('[data-select-index="' + $me.prop('selectedIndex') + '"]').closest('.item').addClass('active');
});
const selectIcons: HTMLElement = target.closest('.t3js-formengine-field-item').querySelector('.t3js-forms-select-single-icons');
if (selectIcons !== null) {
const activeItem = selectIcons.querySelector('.item.active');
if (activeItem !== null) {
activeItem.classList.remove('active');
}
const selectionIcon = selectIcons.querySelector('[data-select-index="' + target.selectedIndex + '"]');
if (selectionIcon !== null) {
selectionIcon.closest('.item').classList.add('active');
}
}
}).bindTo(selectElement);
// Append optionally passed additional "change" event callback
if (typeof options.onChange === 'function') {
$selectElement.on('change', options.onChange);
new RegularEvent('change', options.onChange).bindTo(selectElement);
}
// Append optionally passed additional "focus" event callback
if (typeof options.onFocus === 'function') {
$selectElement.on('focus', options.onFocus);
new RegularEvent('focus', options.onFocus).bindTo(selectElement);
}
$selectElement.closest('.form-control-wrap').find('.t3js-forms-select-single-icons a').on('click', (e: JQueryEventObject): boolean => {
let $me: JQuery = $(e.target);
let $selectIcon: JQuery = $me.closest('[data-select-index]');
$me.closest('.t3js-forms-select-single-icons').find('.item.active').removeClass('active');
$selectElement
.prop('selectedIndex', $selectIcon.data('selectIndex'))
.trigger('change');
$selectIcon.closest('.item').addClass('active');
new RegularEvent('click', (e: Event, target: HTMLAnchorElement): void => {
const currentActive = target.closest('.t3js-forms-select-single-icons').querySelector('.item.active');
if (currentActive !== null) {
currentActive.classList.remove('active');
}
return false;
});
selectElement.selectedIndex = parseInt(target.dataset.selectIndex, 10);
selectElement.dispatchEvent(new Event('change'));
target.closest('.item').classList.add('active');
}).delegateTo(selectElement.closest('.form-control-wrap'), '.t3js-forms-select-single-icons .item:not(.active) a');
}
}
......
......@@ -11,9 +11,11 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import DocumentService = require('TYPO3/CMS/Core/DocumentService');
import DebounceEvent = require('TYPO3/CMS/Core/Event/DebounceEvent');
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
interface FieldOptions {
pageId: number;
......@@ -63,11 +65,11 @@ enum ProposalModes {
*/
class SlugElement {
private options: FieldOptions = null;
private $fullElement: JQuery = null;
private fullElement: HTMLElement = null;
private manuallyChanged: boolean = false;
private $readOnlyField: JQuery = null;
private $inputField: JQuery = null;
private $hiddenField: JQuery = null;
private readOnlyField: HTMLInputElement = null;
private inputField: HTMLInputElement = null;
private hiddenField: HTMLInputElement = null;
private request: AjaxRequest = null;
private readonly fieldsToListenOn: { [key: string]: string } = {};
......@@ -75,74 +77,77 @@ class SlugElement {
this.options = options;
this.fieldsToListenOn = this.options.listenerFieldNames || {};
$((): void => {
this.$fullElement = $(selector);
this.$inputField = this.$fullElement.find(Selectors.inputField);
this.$readOnlyField = this.$fullElement.find(Selectors.readOnlyField);
this.$hiddenField = this.$fullElement.find(Selectors.hiddenField);
DocumentService.ready().then((document: Document): void => {
this.fullElement = document.querySelector(selector);
this.inputField = this.fullElement.querySelector(Selectors.inputField);
this.readOnlyField = this.fullElement.querySelector(Selectors.readOnlyField);
this.hiddenField = this.fullElement.querySelector(Selectors.hiddenField);
this.registerEvents();
});
}
private registerEvents(): void {
const fieldsToListenOnList = Object.keys(this.getAvailableFieldsForProposalGeneration()).map((k: string) => this.fieldsToListenOn[k]);
const fieldsToListenOnList = Object.values(this.getAvailableFieldsForProposalGeneration()).map((selector: string) => `[data-formengine-input-name="${selector}"]`);
const recreateButton: HTMLButtonElement = this.fullElement.querySelector(Selectors.recreateButton);
// Listen on 'listenerFieldNames' for new pages. This is typically the 'title' field
// of a page to create slugs from the title when title is set / changed.
if (fieldsToListenOnList.length > 0) {
if (this.options.command === 'new') {
$(this.$fullElement).on('keyup', fieldsToListenOnList.join(','), (): void => {
new DebounceEvent('input', (): void => {
if (!this.manuallyChanged) {
this.sendSlugProposal(ProposalModes.AUTO);
}
});
}).delegateTo(document, fieldsToListenOnList.join(','));
}
// Clicking the recreate button makes new slug proposal created from 'title' field
$(this.$fullElement).on('click', Selectors.recreateButton, (e: JQueryEventObject): void => {
new RegularEvent('click', (e: Event): void => {
e.preventDefault();
if (this.$readOnlyField.hasClass('hidden')) {
if (this.readOnlyField.classList.contains('hidden')) {
// Switch to readonly version - similar to 'new' page where field is
// written on the fly with title change
this.$readOnlyField.toggleClass('hidden', false);
this.$inputField.toggleClass('hidden', true);
this.readOnlyField.classList.toggle('hidden', false);
this.inputField.classList.toggle('hidden', true);
}
this.sendSlugProposal(ProposalModes.RECREATE);
});
}).bindTo(recreateButton);
} else {
$(this.$fullElement).find(Selectors.recreateButton).addClass('disabled').prop('disabled', true);
recreateButton.classList.add('disabled');
recreateButton.disabled = true;
}
// Scenario for new pages: Usually, slug is created from the page title. However, if user toggles the
// input field and feeds an own slug, and then changes title again, the slug should stay. manuallyChanged
// is used to track this.
$(this.$inputField).on('keyup', (): void => {
new DebounceEvent('input', (): void => {
this.manuallyChanged = true;
this.sendSlugProposal(ProposalModes.MANUAL);
});
}).bindTo(this.inputField);
// Clicking the toggle button toggles the read only field and the input field.
// Also set the value of either the read only or the input field to the hidden field
// and update the value of the read only field after manual change of the input field.
$(this.$fullElement).on('click', Selectors.toggleButton, (e: JQueryEventObject): void => {
const toggleButton = this.fullElement.querySelector(Selectors.toggleButton);
new RegularEvent('click', (e: Event): void => {
e.preventDefault();
const showReadOnlyField = this.$readOnlyField.hasClass('hidden');
this.$readOnlyField.toggleClass('hidden', !showReadOnlyField);
this.$inputField.toggleClass('hidden', showReadOnlyField);
const showReadOnlyField = this.readOnlyField.classList.contains('hidden');
this.readOnlyField.classList.toggle('hidden', !showReadOnlyField);
this.inputField.classList.toggle('hidden', showReadOnlyField);
if (!showReadOnlyField) {
this.$hiddenField.val(this.$inputField.val());
this.hiddenField.value = this.inputField.value;
return;