Commit 82b2f671 authored by Oliver Hader's avatar Oliver Hader Committed by Stefan Bürk
Browse files

[TASK] Allow global event handlers on click

The backend related global-event-handler is a helper
to especially submit POST forms on various DOM elements.
It supersedes "onClick" events by having a watcher defined
by data attributes.
The patch extends given solution to also act on other
elements, especially the "<a .." tag.
This will be used by the upcoming backend TypoScript
Object browser and in general allows to send POST
requests instead of GET requests on many elements.
The JavaScript implementation has been prepared a while
ago already, but missed a use-case up until now.

Change-Id: I6604fd866543f94addbb9909fd41fb2ba3355bf5
Resolves: #97795
Related: #91052
Releases: main
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74963

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
parent 89e8e0c6
......@@ -34,7 +34,10 @@ type HTMLFormChildElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaE
* + `$form=~s/$value/` URL taken from `form[action]`,
* substituting literal `${value}` and `$[value]` taken from `data-value-selector`
* + `data-global-event="click"`
* + @todo
* + `data-action-submit="..."` submits form data
* + `$form` parent form element of current element is submitted
* + `<any CSS selector>` queried element is submitted (if implementing HTMLFormElement)
* + `data-submit-values="{&quot;key&quot;:&quot;value&quot;}"` JSON encoded object (key/value pairs)
*
* @example
* <form action="..." id="...">
......@@ -69,6 +72,7 @@ class GlobalEventHandler {
private handleClickEvent(evt: Event, resolvedTarget: HTMLElement): void {
evt.preventDefault();
this.handleFormChildSubmitAction(evt, resolvedTarget);
}
private handleSubmitEvent(evt: Event, resolvedTarget: HTMLFormElement): void {
......@@ -81,17 +85,48 @@ class GlobalEventHandler {
if (!actionSubmit) {
return false;
}
// @example [data-action-submit]="$form"
let form: HTMLFormElement = null;
const parentForm = resolvedTarget.closest('form');
const formCandidate = actionSubmit !== '$form' ? document.querySelector(actionSubmit) : null;
// @example `data-action-submit="$form"`
if (actionSubmit === '$form' && this.isHTMLFormChildElement(resolvedTarget)) {
(resolvedTarget as HTMLFormChildElement).form.submit();
return true;
form = (resolvedTarget as HTMLFormChildElement).form;
} else if (actionSubmit === '$form' && parentForm) {
form = parentForm;
// @example `data-action-submit="form#identifier"`
} else if (formCandidate instanceof HTMLFormElement) {
form = formCandidate;
}
const formCandidate = document.querySelector(actionSubmit);
if (formCandidate instanceof HTMLFormElement) {
formCandidate.submit();
return true;
if (!(form instanceof HTMLFormElement)) {
return false;
}
return false;
this.assignFormValues(form, resolvedTarget);
form.submit();
return true;
}
private assignFormValues(form: HTMLFormElement, resolvedTarget: HTMLElement): boolean {
const formValuesJson = resolvedTarget.dataset.formValues;
const formValues = formValuesJson ? JSON.parse(formValuesJson) : null;
if (formValues === null || !(formValues instanceof Object)) {
return false;
}
// assign optional key/value pairs from `data-submit-values="{&quot;key&quot;:&quot;value&quot;}"`
Object.entries(formValues).forEach(([name, value]) => {
let item = form.querySelector('[name=' + CSS.escape(name) + ']');
if (item instanceof HTMLElement) {
this.assignHTMLFormChildElementValue(item as HTMLElement, value.toString());
} else {
item = document.createElement('input');
item.setAttribute('type', 'hidden');
item.setAttribute('name', name);
item.setAttribute('value', value.toString());
form.appendChild(item);
}
});
return true;
}
private handleFormChildNavigateAction(evt: Event, resolvedTarget: HTMLElement): boolean {
......@@ -167,6 +202,29 @@ class GlobalEventHandler {
}
return null;
}
private assignHTMLFormChildElementValue(element: HTMLElement, value: string): void {
const type: string = element.getAttribute('type');
if (element instanceof HTMLSelectElement) {
Array.from(element.options).some((option: HTMLOptionElement, index: number) => {
if (option.value === value) {
element.selectedIndex = index;
return true;
}
return false;
});
} else if (element instanceof HTMLInputElement && type === 'checkbox') {
// used for representing unchecked state as e.g. `data-empty-value="0"`
const emptyValue: string = element.dataset.emptyValue;
if (typeof emptyValue !== 'undefined' && emptyValue === value) {
element.checked = false;
} else if (element.value === value) {
element.checked = true;
}
} else if (element instanceof HTMLInputElement) {
element.value = value;
}
}
}
export default new GlobalEventHandler();
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
import documentService from"@typo3/core/document-service.js";import RegularEvent from"@typo3/core/event/regular-event.js";class GlobalEventHandler{constructor(){this.options={onChangeSelector:'[data-global-event="change"]',onClickSelector:'[data-global-event="click"]',onSubmitSelector:'form[data-global-event="submit"]'},documentService.ready().then(()=>this.registerEvents())}registerEvents(){new RegularEvent("change",this.handleChangeEvent.bind(this)).delegateTo(document,this.options.onChangeSelector),new RegularEvent("click",this.handleClickEvent.bind(this)).delegateTo(document,this.options.onClickSelector),new RegularEvent("submit",this.handleSubmitEvent.bind(this)).delegateTo(document,this.options.onSubmitSelector)}handleChangeEvent(e,t){e.preventDefault(),this.handleFormChildSubmitAction(e,t)||this.handleFormChildNavigateAction(e,t)}handleClickEvent(e,t){e.preventDefault()}handleSubmitEvent(e,t){e.preventDefault(),this.handleFormNavigateAction(e,t)}handleFormChildSubmitAction(e,t){const n=t.dataset.actionSubmit;if(!n)return!1;if("$form"===n&&this.isHTMLFormChildElement(t))return t.form.submit(),!0;const a=document.querySelector(n);return a instanceof HTMLFormElement&&(a.submit(),!0)}handleFormChildNavigateAction(e,t){const n=t.dataset.actionNavigate;if(!n)return!1;const a=this.resolveHTMLFormChildElementValue(t),l=t.dataset.navigateValue;return"$data=~s/$value/"===n&&l&&null!==a?(window.location.href=this.substituteValueVariable(l,a),!0):"$data"===n&&l?(window.location.href=l,!0):!("$value"!==n||!a)&&(window.location.href=a,!0)}handleFormNavigateAction(e,t){const n=t.action,a=t.dataset.actionNavigate;if(!n||!a)return!1;const l=t.dataset.navigateValue,o=t.dataset.valueSelector,i=this.resolveHTMLFormChildElementValue(t.querySelector(o));return"$form=~s/$value/"===a&&l&&null!==i?(window.location.href=this.substituteValueVariable(l,i),!0):"$form"===a&&(window.location.href=n,!0)}substituteValueVariable(e,t){return e.replace(/(\$\{value\}|%24%7Bvalue%7D|\$\[value\]|%24%5Bvalue%5D)/gi,t)}isHTMLFormChildElement(e){return e instanceof HTMLSelectElement||e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}resolveHTMLFormChildElementValue(e){const t=e.getAttribute("type");if(e instanceof HTMLSelectElement)return e.options[e.selectedIndex].value;if(e instanceof HTMLInputElement&&"checkbox"===t){const t=e.dataset.emptyValue;return e.checked?e.value:void 0!==t?t:""}return e instanceof HTMLInputElement?e.value:null}}export default new GlobalEventHandler;
\ No newline at end of file
import documentService from"@typo3/core/document-service.js";import RegularEvent from"@typo3/core/event/regular-event.js";class GlobalEventHandler{constructor(){this.options={onChangeSelector:'[data-global-event="change"]',onClickSelector:'[data-global-event="click"]',onSubmitSelector:'form[data-global-event="submit"]'},documentService.ready().then(()=>this.registerEvents())}registerEvents(){new RegularEvent("change",this.handleChangeEvent.bind(this)).delegateTo(document,this.options.onChangeSelector),new RegularEvent("click",this.handleClickEvent.bind(this)).delegateTo(document,this.options.onClickSelector),new RegularEvent("submit",this.handleSubmitEvent.bind(this)).delegateTo(document,this.options.onSubmitSelector)}handleChangeEvent(e,t){e.preventDefault(),this.handleFormChildSubmitAction(e,t)||this.handleFormChildNavigateAction(e,t)}handleClickEvent(e,t){e.preventDefault(),this.handleFormChildSubmitAction(e,t)}handleSubmitEvent(e,t){e.preventDefault(),this.handleFormNavigateAction(e,t)}handleFormChildSubmitAction(e,t){const n=t.dataset.actionSubmit;if(!n)return!1;let a=null;const l=t.closest("form"),o="$form"!==n?document.querySelector(n):null;return"$form"===n&&this.isHTMLFormChildElement(t)?a=t.form:"$form"===n&&l?a=l:o instanceof HTMLFormElement&&(a=o),a instanceof HTMLFormElement&&(this.assignFormValues(a,t),a.submit(),!0)}assignFormValues(e,t){const n=t.dataset.formValues,a=n?JSON.parse(n):null;return null!==a&&a instanceof Object&&(Object.entries(a).forEach(([t,n])=>{let a=e.querySelector("[name="+CSS.escape(t)+"]");a instanceof HTMLElement?this.assignHTMLFormChildElementValue(a,n.toString()):(a=document.createElement("input"),a.setAttribute("type","hidden"),a.setAttribute("name",t),a.setAttribute("value",n.toString()),e.appendChild(a))}),!0)}handleFormChildNavigateAction(e,t){const n=t.dataset.actionNavigate;if(!n)return!1;const a=this.resolveHTMLFormChildElementValue(t),l=t.dataset.navigateValue;return"$data=~s/$value/"===n&&l&&null!==a?(window.location.href=this.substituteValueVariable(l,a),!0):"$data"===n&&l?(window.location.href=l,!0):!("$value"!==n||!a)&&(window.location.href=a,!0)}handleFormNavigateAction(e,t){const n=t.action,a=t.dataset.actionNavigate;if(!n||!a)return!1;const l=t.dataset.navigateValue,o=t.dataset.valueSelector,i=this.resolveHTMLFormChildElementValue(t.querySelector(o));return"$form=~s/$value/"===a&&l&&null!==i?(window.location.href=this.substituteValueVariable(l,i),!0):"$form"===a&&(window.location.href=n,!0)}substituteValueVariable(e,t){return e.replace(/(\$\{value\}|%24%7Bvalue%7D|\$\[value\]|%24%5Bvalue%5D)/gi,t)}isHTMLFormChildElement(e){return e instanceof HTMLSelectElement||e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}resolveHTMLFormChildElementValue(e){const t=e.getAttribute("type");if(e instanceof HTMLSelectElement)return e.options[e.selectedIndex].value;if(e instanceof HTMLInputElement&&"checkbox"===t){const t=e.dataset.emptyValue;return e.checked?e.value:void 0!==t?t:""}return e instanceof HTMLInputElement?e.value:null}assignHTMLFormChildElementValue(e,t){const n=e.getAttribute("type");if(e instanceof HTMLSelectElement)Array.from(e.options).some((n,a)=>n.value===t&&(e.selectedIndex=a,!0));else if(e instanceof HTMLInputElement&&"checkbox"===n){const n=e.dataset.emptyValue;void 0!==n&&n===t?e.checked=!1:e.value===t&&(e.checked=!0)}else e instanceof HTMLInputElement&&(e.value=t)}}export default new GlobalEventHandler;
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment