From 377a61e7fde079b3fec3a679c4675520c3c07edf Mon Sep 17 00:00:00 2001 From: Andreas Fernandez <a.fernandez@scripting-base.de> Date: Fri, 24 Feb 2023 13:14:19 +0100 Subject: [PATCH] [BUGFIX] Avoid timing issue when loading LiveSearch form There was a timing issue related to the LiveSearch form. `Modal.advanced()` invokes an AJAX request, while the event handler for `typo3-modal-shown` expects that the request sent a response. This might not be the case when either the network is slow or animations are disabled as the event is always faster dispatched than the request can send a response. The generic handling is now moved into an `ajaxCallback`, executed when fetching the data finished. To be able to still focus the search field and mark its content, event handlers for 'modal-loaded' and 'typo3-modal-shown' events are installed, as both events may be dispatched in any order. Resolves: #100025 Releases: main Change-Id: I8aca8ba330af55ce915a414dc5a52ba6eb2af1f1 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77941 Tested-by: core-ci <typo3@b13.com> Tested-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Georg Ringer <georg.ringer@gmail.com> --- .../TypeScript/backend/toolbar/live-search.ts | 118 +++++++++++------- .../Public/JavaScript/toolbar/live-search.js | 2 +- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/Build/Sources/TypeScript/backend/toolbar/live-search.ts b/Build/Sources/TypeScript/backend/toolbar/live-search.ts index 6add4599e150..0057614ed3dd 100644 --- a/Build/Sources/TypeScript/backend/toolbar/live-search.ts +++ b/Build/Sources/TypeScript/backend/toolbar/live-search.ts @@ -72,7 +72,7 @@ class LiveSearch { .map((item: [string, string]): SearchOption => { const trimmedKey = item[0].replace('livesearch-option-', ''); const [key, value] = trimmedKey.split('-', 2); - return { key, value }; + return { key, value } }); const searchOptions = this.composeSearchOptions(persistedSearchOptions); @@ -87,61 +87,83 @@ class LiveSearch { content: url.toString(), title: lll('labels.search'), severity: SeverityEnum.notice, - size: Modal.sizes.medium - }); + size: Modal.sizes.medium, + ajaxCallback: (): void => { + const liveSearchContainer = modal.querySelector('typo3-backend-live-search') + const searchField = liveSearchContainer.querySelector('input[type="search"]') as HTMLInputElement; + const searchForm = searchField.closest('form'); + + new RegularEvent('submit', (e: SubmitEvent): void => { + e.preventDefault(); + + const formData = new FormData(searchForm); + this.search(formData).then((): void => { + const query = formData.get('query').toString(); + BrowserSession.set('livesearch-term', query); + }); + const optionCounterElement = searchForm.querySelector('[data-active-options-counter]') as HTMLElement; + const count = parseInt(optionCounterElement.dataset.activeOptionsCounter, 10); + optionCounterElement.querySelector('output').textContent = count.toString(10); + optionCounterElement.classList.toggle('hidden', count === 0); + }).bindTo(searchForm); + + searchField.clearable({ + onClear: (): void => { + searchForm.requestSubmit(); + }, + }); - modal.addEventListener('typo3-modal-shown', () => { - const liveSearchContainer = modal.querySelector('typo3-backend-live-search'); - const searchField = liveSearchContainer.querySelector('input[type="search"]') as HTMLInputElement; - const searchForm = searchField.closest('form'); + const searchResultContainer: ResultContainer = document.querySelector('typo3-backend-live-search-result-container') as ResultContainer; + new RegularEvent('live-search:item-chosen', (): void => { + Modal.dismiss(); + }).bindTo(searchResultContainer); - new RegularEvent('submit', (e: SubmitEvent): void => { - e.preventDefault(); + new RegularEvent('typo3:live-search:option-invoked', (e: CustomEvent): void => { + const optionCounterElement = searchForm.querySelector('[data-active-options-counter]') as HTMLElement; + let count = parseInt(optionCounterElement.dataset.activeOptionsCounter, 10); + count = e.detail.active ? count + 1 : count - 1; - const formData = new FormData(searchForm); - this.search(formData).then((): void => { - const query = formData.get('query').toString(); - BrowserSession.set('livesearch-term', query); - }); - const optionCounterElement = searchForm.querySelector('[data-active-options-counter]') as HTMLElement; - const count = parseInt(optionCounterElement.dataset.activeOptionsCounter, 10); - optionCounterElement.querySelector('output').textContent = count.toString(10); - optionCounterElement.classList.toggle('hidden', count === 0); - }).bindTo(searchForm); - - searchField.clearable({ - onClear: (): void => { - searchForm.requestSubmit(); - }, - }); - searchField.focus(); - searchField.select(); - - const searchResultContainer: ResultContainer = document.querySelector('typo3-backend-live-search-result-container') as ResultContainer; - new RegularEvent('live-search:item-chosen', (): void => { - Modal.dismiss(); - }).bindTo(searchResultContainer); + // Update data attribute only, the visible text content is updated in the submit handler + optionCounterElement.dataset.activeOptionsCounter = count.toString(10); + }).bindTo(liveSearchContainer); - new RegularEvent('typo3:live-search:option-invoked', (e: CustomEvent): void => { - const optionCounterElement = searchForm.querySelector('[data-active-options-counter]') as HTMLElement; - let count = parseInt(optionCounterElement.dataset.activeOptionsCounter, 10); - count = e.detail.active ? count + 1 : count - 1; + new RegularEvent('hide.bs.dropdown', (): void => { + searchForm.requestSubmit(); + }).bindTo(modal.querySelector(Identifiers.searchOptionDropdownToggle)); - // Update data attribute only, the visible text content is updated in the submit handler - optionCounterElement.dataset.activeOptionsCounter = count.toString(10); - }).bindTo(liveSearchContainer); + new DebounceEvent('input', (): void => { + searchForm.requestSubmit(); + }).bindTo(searchField); - new RegularEvent('hide.bs.dropdown', (): void => { - searchForm.requestSubmit(); - }).bindTo(modal.querySelector(Identifiers.searchOptionDropdownToggle)); + new RegularEvent('keydown', this.handleKeyDown).bindTo(searchField); - new DebounceEvent('input', (): void => { searchForm.requestSubmit(); - }).bindTo(searchField); - - new RegularEvent('keydown', this.handleKeyDown).bindTo(searchField); + } + }); - searchForm.requestSubmit(); + /** + * The events `modal-loaded` and `typo3-modal-shown` are dispatched in any order, therefore we have to listen to + * both events to handle search field focus. Unfortunately, there's currently a bug that makes it impossible using + * Promises instead, which would be much better: https://forge.typo3.org/issues/100026 + * + * Once the aforementioned issue is fixed, we may use this instead: + * + * ``` + * Promise.all([ + * new Promise(res1 => modal.addEventListener('modal-loaded', res1)), + * new Promise(res2 => modal.addEventListener('typo3-modal-shown', res2)) + * ]).then((): void => { + * // do stuff here + * }); + */ + ['modal-loaded', 'typo3-modal-shown'].forEach((eventToListenOn: string) => { + modal.addEventListener(eventToListenOn, () => { + const searchField = modal.querySelector('input[type="search"]') as HTMLInputElement|null; + if (searchField !== null) { + searchField.focus(); + searchField.select(); + } + }); }); } @@ -171,7 +193,7 @@ class LiveSearch { } this.updateSearchResults(resultSet); - }; + } private handleKeyDown(e: KeyboardEvent): void { if (e.key !== 'ArrowDown') { diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/toolbar/live-search.js b/typo3/sysext/backend/Resources/Public/JavaScript/toolbar/live-search.js index ee1173571afc..870a94d115c3 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/toolbar/live-search.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/toolbar/live-search.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import{lll}from"@typo3/core/lit-helper.js";import Modal from"@typo3/backend/modal.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/input/clearable.js";import"@typo3/backend/live-search/element/search-option-item.js";import"@typo3/backend/live-search/element/show-all.js";import"@typo3/backend/live-search/live-search-shortcut.js";import DocumentService from"@typo3/core/document-service.js";import RegularEvent from"@typo3/core/event/regular-event.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import BrowserSession from"@typo3/backend/storage/browser-session.js";import{componentName as resultContainerComponentName}from"@typo3/backend/live-search/element/result/result-container.js";var Identifiers;!function(e){e.toolbarItem=".t3js-topbar-button-search",e.searchOptionDropdownToggle=".t3js-search-provider-dropdown-toggle"}(Identifiers||(Identifiers={}));class LiveSearch{constructor(){this.search=async e=>{let t=null;if(""!==e.get("query").toString()){document.querySelector(resultContainerComponentName).loading=!0;const o=await new AjaxRequest(TYPO3.settings.ajaxUrls.livesearch).post(e);t=await o.raw().json()}this.updateSearchResults(t)},DocumentService.ready().then((()=>{this.registerEvents()}))}registerEvents(){new RegularEvent("click",(()=>{this.openSearchModal()})).delegateTo(document,Identifiers.toolbarItem),new RegularEvent("typo3:live-search:trigger-open",(()=>{Modal.currentModal||this.openSearchModal()})).bindTo(document)}openSearchModal(){const e=new URL(TYPO3.settings.ajaxUrls.livesearch_form,window.location.origin);e.searchParams.set("query",BrowserSession.get("livesearch-term")??"");const t=Object.entries(BrowserSession.getByPrefix("livesearch-option-")).filter((e=>"1"===e[1])).map((e=>{const t=e[0].replace("livesearch-option-",""),[o,r]=t.split("-",2);return{key:o,value:r}})),o=this.composeSearchOptions(t);for(const[t,r]of Object.entries(o))for(const o of r)e.searchParams.append(`${t}[]`,o);const r=Modal.advanced({type:Modal.types.ajax,content:e.toString(),title:lll("labels.search"),severity:SeverityEnum.notice,size:Modal.sizes.medium});r.addEventListener("typo3-modal-shown",(()=>{const e=r.querySelector("typo3-backend-live-search"),t=e.querySelector('input[type="search"]'),o=t.closest("form");new RegularEvent("submit",(e=>{e.preventDefault();const t=new FormData(o);this.search(t).then((()=>{const e=t.get("query").toString();BrowserSession.set("livesearch-term",e)}));const r=o.querySelector("[data-active-options-counter]"),n=parseInt(r.dataset.activeOptionsCounter,10);r.querySelector("output").textContent=n.toString(10),r.classList.toggle("hidden",0===n)})).bindTo(o),t.clearable({onClear:()=>{o.requestSubmit()}}),t.focus(),t.select();const n=document.querySelector("typo3-backend-live-search-result-container");new RegularEvent("live-search:item-chosen",(()=>{Modal.dismiss()})).bindTo(n),new RegularEvent("typo3:live-search:option-invoked",(e=>{const t=o.querySelector("[data-active-options-counter]");let r=parseInt(t.dataset.activeOptionsCounter,10);r=e.detail.active?r+1:r-1,t.dataset.activeOptionsCounter=r.toString(10)})).bindTo(e),new RegularEvent("hide.bs.dropdown",(()=>{o.requestSubmit()})).bindTo(r.querySelector(Identifiers.searchOptionDropdownToggle)),new DebounceEvent("input",(()=>{o.requestSubmit()})).bindTo(t),new RegularEvent("keydown",this.handleKeyDown).bindTo(t),o.requestSubmit()}))}composeSearchOptions(e){const t={};return e.forEach((e=>{void 0===t[e.key]&&(t[e.key]=[]),t[e.key].push(e.value)})),t}handleKeyDown(e){if("ArrowDown"!==e.key)return;e.preventDefault();document.querySelector("typo3-backend-live-search").querySelector("typo3-backend-live-search-result-item")?.focus()}updateSearchResults(e){document.querySelector("typo3-backend-live-search-show-all").parentElement.hidden=null===e||0===e.length;const t=document.querySelector("typo3-backend-live-search-result-container");t.results=e,t.loading=!1}}export default top.TYPO3.LiveSearch??new LiveSearch; \ No newline at end of file +import{lll}from"@typo3/core/lit-helper.js";import Modal from"@typo3/backend/modal.js";import"@typo3/backend/element/icon-element.js";import"@typo3/backend/input/clearable.js";import"@typo3/backend/live-search/element/search-option-item.js";import"@typo3/backend/live-search/element/show-all.js";import"@typo3/backend/live-search/live-search-shortcut.js";import DocumentService from"@typo3/core/document-service.js";import RegularEvent from"@typo3/core/event/regular-event.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import BrowserSession from"@typo3/backend/storage/browser-session.js";import{componentName as resultContainerComponentName}from"@typo3/backend/live-search/element/result/result-container.js";var Identifiers;!function(e){e.toolbarItem=".t3js-topbar-button-search",e.searchOptionDropdownToggle=".t3js-search-provider-dropdown-toggle"}(Identifiers||(Identifiers={}));class LiveSearch{constructor(){this.search=async e=>{let t=null;if(""!==e.get("query").toString()){document.querySelector(resultContainerComponentName).loading=!0;const o=await new AjaxRequest(TYPO3.settings.ajaxUrls.livesearch).post(e);t=await o.raw().json()}this.updateSearchResults(t)},DocumentService.ready().then((()=>{this.registerEvents()}))}registerEvents(){new RegularEvent("click",(()=>{this.openSearchModal()})).delegateTo(document,Identifiers.toolbarItem),new RegularEvent("typo3:live-search:trigger-open",(()=>{Modal.currentModal||this.openSearchModal()})).bindTo(document)}openSearchModal(){const e=new URL(TYPO3.settings.ajaxUrls.livesearch_form,window.location.origin);e.searchParams.set("query",BrowserSession.get("livesearch-term")??"");const t=Object.entries(BrowserSession.getByPrefix("livesearch-option-")).filter((e=>"1"===e[1])).map((e=>{const t=e[0].replace("livesearch-option-",""),[o,r]=t.split("-",2);return{key:o,value:r}})),o=this.composeSearchOptions(t);for(const[t,r]of Object.entries(o))for(const o of r)e.searchParams.append(`${t}[]`,o);const r=Modal.advanced({type:Modal.types.ajax,content:e.toString(),title:lll("labels.search"),severity:SeverityEnum.notice,size:Modal.sizes.medium,ajaxCallback:()=>{const e=r.querySelector("typo3-backend-live-search"),t=e.querySelector('input[type="search"]'),o=t.closest("form");new RegularEvent("submit",(e=>{e.preventDefault();const t=new FormData(o);this.search(t).then((()=>{const e=t.get("query").toString();BrowserSession.set("livesearch-term",e)}));const r=o.querySelector("[data-active-options-counter]"),n=parseInt(r.dataset.activeOptionsCounter,10);r.querySelector("output").textContent=n.toString(10),r.classList.toggle("hidden",0===n)})).bindTo(o),t.clearable({onClear:()=>{o.requestSubmit()}});const n=document.querySelector("typo3-backend-live-search-result-container");new RegularEvent("live-search:item-chosen",(()=>{Modal.dismiss()})).bindTo(n),new RegularEvent("typo3:live-search:option-invoked",(e=>{const t=o.querySelector("[data-active-options-counter]");let r=parseInt(t.dataset.activeOptionsCounter,10);r=e.detail.active?r+1:r-1,t.dataset.activeOptionsCounter=r.toString(10)})).bindTo(e),new RegularEvent("hide.bs.dropdown",(()=>{o.requestSubmit()})).bindTo(r.querySelector(Identifiers.searchOptionDropdownToggle)),new DebounceEvent("input",(()=>{o.requestSubmit()})).bindTo(t),new RegularEvent("keydown",this.handleKeyDown).bindTo(t),o.requestSubmit()}});["modal-loaded","typo3-modal-shown"].forEach((e=>{r.addEventListener(e,(()=>{const e=r.querySelector('input[type="search"]');null!==e&&(e.focus(),e.select())}))}))}composeSearchOptions(e){const t={};return e.forEach((e=>{void 0===t[e.key]&&(t[e.key]=[]),t[e.key].push(e.value)})),t}handleKeyDown(e){if("ArrowDown"!==e.key)return;e.preventDefault();document.querySelector("typo3-backend-live-search").querySelector("typo3-backend-live-search-result-item")?.focus()}updateSearchResults(e){document.querySelector("typo3-backend-live-search-show-all").parentElement.hidden=null===e||0===e.length;const t=document.querySelector("typo3-backend-live-search-result-container");t.results=e,t.loading=!1}}export default top.TYPO3.LiveSearch??new LiveSearch; \ No newline at end of file -- GitLab