diff --git a/js/src/common/Application.tsx b/js/src/common/Application.tsx index cfcda3c89..b966fc1cc 100644 --- a/js/src/common/Application.tsx +++ b/js/src/common/Application.tsx @@ -9,6 +9,7 @@ import Translator from './Translator'; import Store, { ApiPayload, ApiResponse, ApiResponsePlural, ApiResponseSingle, payloadIsPlural } from './Store'; import Session from './Session'; import extract from './utils/extract'; +import extractText from './utils/extractText'; import Drawer from './utils/Drawer'; import mapRoutes from './utils/mapRoutes'; import RequestError, { InternalFlarumRequestOptions } from './utils/RequestError'; @@ -365,9 +366,21 @@ export default class Application { updateTitle(): void { const count = this.titleCount ? `(${this.titleCount}) ` : ''; - const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : ''; - const title = this.forum.attribute('title'); - document.title = count + pageTitleWithSeparator + title; + const onHomepage = m.route.get() === this.forum.attribute('basePath') + '/'; + + const params = { + pageTitle: this.title, + forumName: this.forum.attribute('title'), + // Until we add page numbers to the frontend, this is constant at 1 + // so that the page number portion doesn't show up in the URL. + pageNumber: 1, + }; + + const title = + onHomepage || !this.title + ? extractText(app.translator.trans('core.lib.meta_titles.without_page_title', params)) + : extractText(app.translator.trans('core.lib.meta_titles.with_page_title', params)); + document.title = count + title; } protected transformRequestOptions(flarumOptions: FlarumRequestOptions): InternalFlarumRequestOptions { diff --git a/js/src/common/components/ModalManager.tsx b/js/src/common/components/ModalManager.tsx index d46899eec..8db7e25bf 100644 --- a/js/src/common/components/ModalManager.tsx +++ b/js/src/common/components/ModalManager.tsx @@ -24,16 +24,19 @@ export default class ModalManager extends Component { view(vnode: Mithril.VnodeDOM): Mithril.Children { const modal = this.attrs.state.modal; + const Tag = modal?.componentClass; return (
- {!!modal && - modal.componentClass.component({ - ...modal.attrs, - animateShow: this.animateShow.bind(this), - animateHide: this.animateHide.bind(this), - state: this.attrs.state, - })} + {!!Tag && ( + + )}
); } diff --git a/js/src/common/states/ModalManagerState.ts b/js/src/common/states/ModalManagerState.ts index 445d002b1..a4a36093d 100644 --- a/js/src/common/states/ModalManagerState.ts +++ b/js/src/common/states/ModalManagerState.ts @@ -22,8 +22,15 @@ export default class ModalManagerState { modal: null | { componentClass: UnsafeModalClass; attrs?: Record; + key: number; } = null; + /** + * Used to force re-initialization of modals if a modal + * is replaced by another of the same type. + */ + private key = 0; + private closeTimeout?: NodeJS.Timeout; /** @@ -48,7 +55,7 @@ export default class ModalManagerState { if (this.closeTimeout) clearTimeout(this.closeTimeout); - this.modal = { componentClass, attrs }; + this.modal = { componentClass, attrs, key: this.key++ }; m.redraw.sync(); } diff --git a/js/src/common/utils/Drawer.js b/js/src/common/utils/Drawer.js index 323e62fd0..823b9290f 100644 --- a/js/src/common/utils/Drawer.js +++ b/js/src/common/utils/Drawer.js @@ -27,7 +27,10 @@ export default class Drawer { }); this.appElement = document.getElementById('app'); - this.focusTrap = createFocusTrap('#drawer', { allowOutsideClick: true }); + // Despite the `focus-trap` documentation, both `clickOutsideDeactivates` + // and `allowOutsideClick` are necessary so that inputs in modals triggered + // from the drawer's nav components can be interacted with. + this.focusTrap = createFocusTrap('#drawer', { allowOutsideClick: true, clickOutsideDeactivates: true }); this.drawerAvailableMediaQuery = window.matchMedia( `(max-width: ${getComputedStyle(document.documentElement).getPropertyValue('--screen-phone-max')})` ); diff --git a/js/src/forum/components/Search.tsx b/js/src/forum/components/Search.tsx index 645fa9241..054c0b307 100644 --- a/js/src/forum/components/Search.tsx +++ b/js/src/forum/components/Search.tsx @@ -88,7 +88,7 @@ export default class Search extends Compone /** * An array of SearchSources. */ - protected sources!: SearchSource[]; + protected sources?: SearchSource[]; /** * The number of sources that are still loading results. @@ -192,7 +192,7 @@ export default class Search extends Compone this.setIndex(this.getCurrentNumericIndex()); // If there are no sources, the search view is not shown. - if (!this.sources.length) return; + if (!this.sources?.length) return; this.updateMaxHeight(); } @@ -200,6 +200,10 @@ export default class Search extends Compone oncreate(vnode: Mithril.VnodeDOM) { super.oncreate(vnode); + // If there are no sources, we shouldn't initialize logic for + // search elements, as they will not be shown. + if (!this.sources?.length) return; + const search = this; const state = this.searchState; @@ -237,7 +241,7 @@ export default class Search extends Compone if (state.isCached(query)) return; if (query.length >= (search.constructor as typeof Search).MIN_SEARCH_LEN) { - search.sources.map((source) => { + search.sources?.map((source) => { if (!source.search) return; search.loadingSources++; diff --git a/locale/core.yml b/locale/core.yml index 7c4812140..b0e0b055e 100644 --- a/locale/core.yml +++ b/locale/core.yml @@ -542,6 +542,11 @@ core: loading_indicator: accessible_label: => core.ref.loading + # Translations in this namespace are used to format page meta titles. + meta_titles: + with_page_title: "{pageNumber, plural, =1 {{pageTitle} - {forumName}} other {{pageTitle}: Page # - {forumName}}}" + without_page_title: "{pageNumber, plural, =1 {{forumName}} other {Page # - {forumName}}}" + # These translations are used in modals. modal: close: Close @@ -628,11 +633,6 @@ core: submit_button: => core.ref.save_changes title: => core.ref.reset_your_password - # Translations in this namespace are used to format page meta titles. - meta_titles: - with_page_title: "{pageNumber, plural, =1 {{pageTitle} - {forumName}} other {{pageTitle}: Page # - {forumName}}}" - without_page_title: "{pageNumber, plural, =1 {{forumName}} other {Page # - {forumName}}}" - # Translations in this namespace are used in messages output by the API. api: invalid_username_message: "The username may only contain letters, numbers, and dashes." diff --git a/src/Frontend/Driver/BasicTitleDriver.php b/src/Frontend/Driver/BasicTitleDriver.php index b87272149..36929602e 100644 --- a/src/Frontend/Driver/BasicTitleDriver.php +++ b/src/Frontend/Driver/BasicTitleDriver.php @@ -37,7 +37,7 @@ class BasicTitleDriver implements TitleDriverInterface ]; return $onHomePage || ! $document->title - ? $this->translator->trans('core.views.meta_titles.without_page_title', $params) - : $this->translator->trans('core.views.meta_titles.with_page_title', $params); + ? $this->translator->trans('core.lib.meta_titles.without_page_title', $params) + : $this->translator->trans('core.lib.meta_titles.with_page_title', $params); } }