diff --git a/framework/core/js/src/admin/AdminApplication.ts b/framework/core/js/src/admin/AdminApplication.ts index 62fa121b5..301662cdf 100644 --- a/framework/core/js/src/admin/AdminApplication.ts +++ b/framework/core/js/src/admin/AdminApplication.ts @@ -5,6 +5,7 @@ import Application, { ApplicationData } from '../common/Application'; import Navigation from '../common/components/Navigation'; import AdminNav from './components/AdminNav'; import ExtensionData from './utils/ExtensionData'; +import IHistory from '../common/IHistory'; export type Extension = { id: string; @@ -47,13 +48,16 @@ export default class AdminApplication extends Application { language: 10, }; - history = { + history: IHistory = { canGoBack: () => true, - getPrevious: () => {}, + getCurrent: () => null, + getPrevious: () => null, + push: () => {}, backUrl: () => this.forum.attribute('baseUrl'), back: function () { window.location.assign(this.backUrl()); }, + home: () => {}, }; /** diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index 36af01500..4a58c8de6 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -34,6 +34,7 @@ import type Component from './Component'; import type { ComponentAttrs } from './Component'; import Model, { SavedModelData } from './Model'; import fireApplicationError from './helpers/fireApplicationError'; +import IHistory from './IHistory'; export type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd'; @@ -228,6 +229,9 @@ export default class Application { */ drawer!: Drawer; + history: IHistory | null = null; + pane: any = null; + data!: ApplicationData; private _title: string = ''; diff --git a/framework/core/js/src/common/Component.ts b/framework/core/js/src/common/Component.ts index 36c12a83e..fdf244443 100644 --- a/framework/core/js/src/common/Component.ts +++ b/framework/core/js/src/common/Component.ts @@ -156,5 +156,5 @@ export default abstract class Component(attrs: T): void {} + static initAttrs(attrs: unknown): void {} } diff --git a/framework/core/js/src/common/IHistory.ts b/framework/core/js/src/common/IHistory.ts new file mode 100644 index 000000000..936ab643b --- /dev/null +++ b/framework/core/js/src/common/IHistory.ts @@ -0,0 +1,15 @@ +export interface HistoryEntry { + name: string; + title: string; + url: string; +} + +export default interface IHistory { + canGoBack(): boolean; + getCurrent(): HistoryEntry | null; + getPrevious(): HistoryEntry | null; + push(name: string, title: string, url: string): void; + back(): void; + backUrl(): string; + home(): void; +} diff --git a/framework/core/js/src/common/components/Badge.js b/framework/core/js/src/common/components/Badge.tsx similarity index 81% rename from framework/core/js/src/common/components/Badge.js rename to framework/core/js/src/common/components/Badge.tsx index 39f63e16b..4261e8d95 100644 --- a/framework/core/js/src/common/components/Badge.js +++ b/framework/core/js/src/common/components/Badge.tsx @@ -1,8 +1,15 @@ import Tooltip from './Tooltip'; -import Component from '../Component'; +import Component, { ComponentAttrs } from '../Component'; import icon from '../helpers/icon'; import classList from '../utils/classList'; +export interface IBadgeAttrs extends ComponentAttrs { + icon: string; + type?: string; + label?: string; + color?: string; +} + /** * The `Badge` component represents a user/discussion badge, indicating some * status (e.g. a discussion is stickied, a user is an admin). @@ -16,7 +23,7 @@ import classList from '../utils/classList'; * * All other attrs will be assigned as attributes on the badge element. */ -export default class Badge extends Component { +export default class Badge extends Component { view() { const { type, icon: iconName, label, color, style = {}, ...attrs } = this.attrs; diff --git a/framework/core/js/src/common/components/Checkbox.js b/framework/core/js/src/common/components/Checkbox.tsx similarity index 74% rename from framework/core/js/src/common/components/Checkbox.js rename to framework/core/js/src/common/components/Checkbox.tsx index bc7ed5db1..975dc17c3 100644 --- a/framework/core/js/src/common/components/Checkbox.js +++ b/framework/core/js/src/common/components/Checkbox.tsx @@ -1,8 +1,16 @@ -import Component from '../Component'; +import Component, { ComponentAttrs } from '../Component'; import LoadingIndicator from './LoadingIndicator'; import icon from '../helpers/icon'; import classList from '../utils/classList'; import withAttr from '../utils/withAttr'; +import type Mithril from 'mithril'; + +export interface ICheckboxAttrs extends ComponentAttrs { + state?: boolean; + loading?: boolean; + disabled?: boolean; + onchange: (checked: boolean, component: Checkbox) => void; +} /** * The `Checkbox` component defines a checkbox input. @@ -16,12 +24,8 @@ import withAttr from '../utils/withAttr'; * - `onchange` A callback to run when the checkbox is checked/unchecked. * - `children` A text label to display next to the checkbox. */ -export default class Checkbox extends Component { - view(vnode) { - // Sometimes, false is stored in the DB as '0'. This is a temporary - // conversion layer until a more robust settings encoding is introduced - if (this.attrs.state === '0') this.attrs.state = false; - +export default class Checkbox extends Component { + view(vnode: Mithril.Vnode) { const className = classList([ 'Checkbox', this.attrs.state ? 'on' : 'off', @@ -43,21 +47,15 @@ export default class Checkbox extends Component { /** * Get the template for the checkbox's display (tick/cross icon). - * - * @return {import('mithril').Children} - * @protected */ - getDisplay() { + protected getDisplay(): Mithril.Children { return this.attrs.loading ? : icon(this.attrs.state ? 'fas fa-check' : 'fas fa-times'); } /** * Run a callback when the state of the checkbox is changed. - * - * @param {boolean} checked - * @protected */ - onchange(checked) { + protected onchange(checked: boolean): void { if (this.attrs.onchange) this.attrs.onchange(checked, this); } } diff --git a/framework/core/js/src/common/components/GroupBadge.js b/framework/core/js/src/common/components/GroupBadge.js deleted file mode 100644 index 5479f7db0..000000000 --- a/framework/core/js/src/common/components/GroupBadge.js +++ /dev/null @@ -1,16 +0,0 @@ -import Badge from './Badge'; - -export default class GroupBadge extends Badge { - static initAttrs(attrs) { - super.initAttrs(attrs); - - if (attrs.group) { - attrs.icon = attrs.group.icon(); - attrs.color = attrs.group.color(); - attrs.label = typeof attrs.label === 'undefined' ? attrs.group.nameSingular() : attrs.label; - attrs.type = 'group--' + attrs.group.id(); - - delete attrs.group; - } - } -} diff --git a/framework/core/js/src/common/components/GroupBadge.tsx b/framework/core/js/src/common/components/GroupBadge.tsx new file mode 100644 index 000000000..4910a004f --- /dev/null +++ b/framework/core/js/src/common/components/GroupBadge.tsx @@ -0,0 +1,21 @@ +import Badge, { IBadgeAttrs } from './Badge'; +import Group from '../models/Group'; + +export interface IGroupAttrs extends IBadgeAttrs { + group?: Group; +} + +export default class GroupBadge extends Badge { + static initAttrs(attrs: IGroupAttrs): void { + super.initAttrs(attrs); + + if (attrs.group) { + attrs.icon = attrs.group.icon() || ''; + attrs.color = attrs.group.color() || ''; + attrs.label = typeof attrs.label === 'undefined' ? attrs.group.nameSingular() : attrs.label; + attrs.type = 'group--' + attrs.group.id(); + + delete attrs.group; + } + } +} diff --git a/framework/core/js/src/common/components/Navigation.js b/framework/core/js/src/common/components/Navigation.tsx similarity index 80% rename from framework/core/js/src/common/components/Navigation.js rename to framework/core/js/src/common/components/Navigation.tsx index 157d07538..de2cb3e2a 100644 --- a/framework/core/js/src/common/components/Navigation.js +++ b/framework/core/js/src/common/components/Navigation.tsx @@ -2,6 +2,7 @@ import app from '../../common/app'; import Component from '../Component'; import Button from './Button'; import LinkButton from './LinkButton'; +import type Mithril from 'mithril'; /** * The `Navigation` component displays a set of navigation buttons. Typically @@ -28,41 +29,35 @@ export default class Navigation extends Component { onmouseenter={pane && pane.show.bind(pane)} onmouseleave={pane && pane.onmouseleave.bind(pane)} > - {history.canGoBack() ? [this.getBackButton(), this.getPaneButton()] : this.getDrawerButton()} + {history?.canGoBack() ? [this.getBackButton(), this.getPaneButton()] : this.getDrawerButton()} ); } /** * Get the back button. - * - * @return {import('mithril').Children} - * @protected */ - getBackButton() { + protected getBackButton(): Mithril.Children { const { history } = app; - const previous = history.getPrevious() || {}; + const previous = history?.getPrevious(); return LinkButton.component({ className: 'Button Navigation-back Button--icon', - href: history.backUrl(), + href: history?.backUrl(), icon: 'fas fa-chevron-left', - 'aria-label': previous.title, - onclick: (e) => { + 'aria-label': previous?.title, + onclick: (e: MouseEvent) => { if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return; e.preventDefault(); - history.back(); + history?.back(); }, }); } /** * Get the pane pinned toggle button. - * - * @return {import('mithril').Children} - * @protected */ - getPaneButton() { + protected getPaneButton(): Mithril.Children { const { pane } = app; if (!pane || !pane.active) return ''; @@ -76,11 +71,8 @@ export default class Navigation extends Component { /** * Get the drawer toggle button. - * - * @return {import('mithril').Children} - * @protected */ - getDrawerButton() { + protected getDrawerButton(): Mithril.Children { if (!this.attrs.drawer) return ''; const { drawer } = app; @@ -88,7 +80,7 @@ export default class Navigation extends Component { return Button.component({ className: 'Button Button--icon Navigation-drawer' + (user && user.newNotificationCount() ? ' new' : ''), - onclick: (e) => { + onclick: (e: MouseEvent) => { e.stopPropagation(); drawer.show(); }, diff --git a/framework/core/js/src/common/components/Switch.js b/framework/core/js/src/common/components/Switch.tsx similarity index 77% rename from framework/core/js/src/common/components/Switch.js rename to framework/core/js/src/common/components/Switch.tsx index 531c4347a..faf928f66 100644 --- a/framework/core/js/src/common/components/Switch.js +++ b/framework/core/js/src/common/components/Switch.tsx @@ -1,11 +1,11 @@ -import Checkbox from './Checkbox'; +import Checkbox, { ICheckboxAttrs } from './Checkbox'; /** * The `Switch` component is a `Checkbox`, but with a switch display instead of * a tick/cross one. */ export default class Switch extends Checkbox { - static initAttrs(attrs) { + static initAttrs(attrs: ICheckboxAttrs) { super.initAttrs(attrs); attrs.className = (attrs.className || '') + ' Checkbox--switch'; diff --git a/framework/core/js/src/forum/utils/History.ts b/framework/core/js/src/forum/utils/History.ts index faa8e8da4..778533386 100644 --- a/framework/core/js/src/forum/utils/History.ts +++ b/framework/core/js/src/forum/utils/History.ts @@ -1,10 +1,5 @@ import setRouteWithForcedRefresh from '../../common/utils/setRouteWithForcedRefresh'; - -export interface HistoryEntry { - name: string; - title: string; - url: string; -} +import IHistory, { HistoryEntry } from '../../common/IHistory'; /** * The `History` class keeps track and manages a stack of routes that the user @@ -17,7 +12,7 @@ export interface HistoryEntry { * popping the history stack will still take them back to the discussion list * rather than the previous discussion. */ -export default class History { +export default class History implements IHistory { /** * The stack of routes that have been navigated to. */