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 ...@@ -34,7 +34,10 @@ type HTMLFormChildElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaE
* + `$form=~s/$value/` URL taken from `form[action]`, * + `$form=~s/$value/` URL taken from `form[action]`,
* substituting literal `${value}` and `$[value]` taken from `data-value-selector` * substituting literal `${value}` and `$[value]` taken from `data-value-selector`
* + `data-global-event="click"` * + `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 * @example
* <form action="..." id="..."> * <form action="..." id="...">
...@@ -69,6 +72,7 @@ class GlobalEventHandler { ...@@ -69,6 +72,7 @@ class GlobalEventHandler {
private handleClickEvent(evt: Event, resolvedTarget: HTMLElement): void { private handleClickEvent(evt: Event, resolvedTarget: HTMLElement): void {
evt.preventDefault(); evt.preventDefault();
this.handleFormChildSubmitAction(evt, resolvedTarget);
} }
private handleSubmitEvent(evt: Event, resolvedTarget: HTMLFormElement): void { private handleSubmitEvent(evt: Event, resolvedTarget: HTMLFormElement): void {
...@@ -81,17 +85,48 @@ class GlobalEventHandler { ...@@ -81,17 +85,48 @@ class GlobalEventHandler {
if (!actionSubmit) { if (!actionSubmit) {
return false; 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)) { if (actionSubmit === '$form' && this.isHTMLFormChildElement(resolvedTarget)) {
(resolvedTarget as HTMLFormChildElement).form.submit(); form = (resolvedTarget as HTMLFormChildElement).form;
return true; } 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 (!(form instanceof HTMLFormElement)) {
if (formCandidate instanceof HTMLFormElement) { return false;
formCandidate.submit();
return true;
} }
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 { private handleFormChildNavigateAction(evt: Event, resolvedTarget: HTMLElement): boolean {
...@@ -167,6 +202,29 @@ class GlobalEventHandler { ...@@ -167,6 +202,29 @@ class GlobalEventHandler {
} }
return null; 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(); export default new GlobalEventHandler();
...@@ -10,4 +10,4 @@ ...@@ -10,4 +10,4 @@
* *
* The TYPO3 project - inspiring people to share! * 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; 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 \ No newline at end of file
Supports Markdown
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