diff --git a/js/package-lock.json b/js/package-lock.json index 9e788e9d1..a39b216f4 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1075,6 +1075,11 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/mithril": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mithril/-/mithril-2.0.3.tgz", + "integrity": "sha512-cZHOdO2IiXYeyjeDYdbOisSdfaJRzfmRo3zVzgu33IWTMA0KEQObp9fdvqcuYdPz93iJ1yCl19GcEjo/9yv+yA==" + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -3807,9 +3812,9 @@ } }, "mithril": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/mithril/-/mithril-0.2.8.tgz", - "integrity": "sha512-9XuGnVmS2OyFexUuP/CcJFFJjHLM+RGYBxyVRNyQ6khbMfDJIF/xyZ4zq18ZRfPagpFmWUFpjHd5ZqPULGZyNg==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mithril/-/mithril-2.0.4.tgz", + "integrity": "sha512-mgw+DMZlhMS4PpprF6dl7ZoeZq5GGcAuWnrg5e12MvaGauc4jzWsDZtVGRCktsiQczOEUr2K5teKbE5k44RlOg==" }, "mixin-deep": { "version": "1.3.2", diff --git a/js/package.json b/js/package.json index 96a521dc9..f4c0af8a2 100644 --- a/js/package.json +++ b/js/package.json @@ -3,6 +3,7 @@ "name": "@flarum/core", "dependencies": { "@babel/preset-typescript": "^7.10.1", + "@types/mithril": "^2.0.3", "bootstrap": "^3.4.1", "classnames": "^2.2.5", "color-thief-browser": "^2.0.2", @@ -13,7 +14,7 @@ "jquery.hotkeys": "^0.1.0", "lodash-es": "^4.17.14", "m.attrs.bidi": "github:tobscure/m.attrs.bidi", - "mithril": "^0.2.8", + "mithril": "^2.0.4", "punycode": "^2.1.1", "spin.js": "^3.1.0", "webpack": "^4.43.0", diff --git a/js/shims.d.ts b/js/shims.d.ts new file mode 100644 index 000000000..a2d31e316 --- /dev/null +++ b/js/shims.d.ts @@ -0,0 +1,48 @@ +// Mithril +import * as Mithril from 'mithril'; +import Stream from 'mithril/stream'; + +// Other third-party libs +import * as _dayjs from 'dayjs'; +import * as _$ from 'jquery'; + +// Globals from flarum/core +import Application from './src/common/Application'; + +/** + * Helpers that flarum/core patches into Mithril + */ +interface m extends Mithril.Static { + prop: typeof Stream; +} + +/** + * Export Mithril typings globally. + * + * This lets us use these typings without an extra import everywhere we use + * Mithril in a TypeScript file. + */ +export as namespace Mithril; + +/** + * flarum/core exposes several extensions globally: + * + * - jQuery for convenient DOM manipulation + * - Mithril for VDOM and components + * - dayjs for date/time operations + * + * Since these are already part of the global namespace, extensions won't need + * to (and should not) bundle these themselves. + */ +declare global { + const $: typeof _$; + const m: m; + const dayjs: typeof _dayjs; +} + +/** + * All global variables owned by flarum/core. + */ +declare global { + const app: Application; +} diff --git a/js/src/admin/AdminApplication.js b/js/src/admin/AdminApplication.js index e0d9464e4..03669d6e0 100644 --- a/js/src/admin/AdminApplication.js +++ b/js/src/admin/AdminApplication.js @@ -27,13 +27,18 @@ export default class AdminApplication extends Application { * @inheritdoc */ mount() { - m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true })); - m.mount(document.getElementById('header-navigation'), Navigation.component()); - m.mount(document.getElementById('header-primary'), HeaderPrimary.component()); - m.mount(document.getElementById('header-secondary'), HeaderSecondary.component()); - m.mount(document.getElementById('admin-navigation'), AdminNav.component()); + m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) }); + m.mount(document.getElementById('header-navigation'), Navigation); + m.mount(document.getElementById('header-primary'), HeaderPrimary); + m.mount(document.getElementById('header-secondary'), HeaderSecondary); + m.mount(document.getElementById('admin-navigation'), AdminNav); + + // Mithril does not render the home route on https://example.com/admin, so + // we need to go to https://example.com/admin#/ explicitly. + if (!document.location.hash) document.location.hash = '#/'; + + m.route.prefix = '#'; - m.route.mode = 'hash'; super.mount(); // If an extension has just been enabled, then we will run its settings diff --git a/js/src/admin/components/AdminLinkButton.js b/js/src/admin/components/AdminLinkButton.js index 2602bc273..ff98f7130 100644 --- a/js/src/admin/components/AdminLinkButton.js +++ b/js/src/admin/components/AdminLinkButton.js @@ -10,11 +10,7 @@ import LinkButton from '../../common/components/LinkButton'; export default class AdminLinkButton extends LinkButton { - getButtonContent() { - const content = super.getButtonContent(); - - content.push(
{this.props.description}
); - - return content; + getButtonContent(children) { + return [...super.getButtonContent(children),
{this.attrs.description}
]; } } diff --git a/js/src/admin/components/AdminNav.js b/js/src/admin/components/AdminNav.js index f96a4e162..f41d2a1a6 100644 --- a/js/src/admin/components/AdminNav.js +++ b/js/src/admin/components/AdminNav.js @@ -31,62 +31,74 @@ export default class AdminNav extends Component { items.add( 'dashboard', - AdminLinkButton.component({ - href: app.route('dashboard'), - icon: 'far fa-chart-bar', - children: app.translator.trans('core.admin.nav.dashboard_button'), - description: app.translator.trans('core.admin.nav.dashboard_text'), - }) + AdminLinkButton.component( + { + href: app.route('dashboard'), + icon: 'far fa-chart-bar', + description: app.translator.trans('core.admin.nav.dashboard_text'), + }, + app.translator.trans('core.admin.nav.dashboard_button') + ) ); items.add( 'basics', - AdminLinkButton.component({ - href: app.route('basics'), - icon: 'fas fa-pencil-alt', - children: app.translator.trans('core.admin.nav.basics_button'), - description: app.translator.trans('core.admin.nav.basics_text'), - }) + AdminLinkButton.component( + { + href: app.route('basics'), + icon: 'fas fa-pencil-alt', + description: app.translator.trans('core.admin.nav.basics_text'), + }, + app.translator.trans('core.admin.nav.basics_button') + ) ); items.add( 'mail', - AdminLinkButton.component({ - href: app.route('mail'), - icon: 'fas fa-envelope', - children: app.translator.trans('core.admin.nav.email_button'), - description: app.translator.trans('core.admin.nav.email_text'), - }) + AdminLinkButton.component( + { + href: app.route('mail'), + icon: 'fas fa-envelope', + description: app.translator.trans('core.admin.nav.email_text'), + }, + app.translator.trans('core.admin.nav.email_button') + ) ); items.add( 'permissions', - AdminLinkButton.component({ - href: app.route('permissions'), - icon: 'fas fa-key', - children: app.translator.trans('core.admin.nav.permissions_button'), - description: app.translator.trans('core.admin.nav.permissions_text'), - }) + AdminLinkButton.component( + { + href: app.route('permissions'), + icon: 'fas fa-key', + description: app.translator.trans('core.admin.nav.permissions_text'), + }, + app.translator.trans('core.admin.nav.permissions_button') + ) ); items.add( 'appearance', - AdminLinkButton.component({ - href: app.route('appearance'), - icon: 'fas fa-paint-brush', - children: app.translator.trans('core.admin.nav.appearance_button'), - description: app.translator.trans('core.admin.nav.appearance_text'), - }) + AdminLinkButton.component( + { + href: app.route('appearance'), + icon: 'fas fa-paint-brush', + description: app.translator.trans('core.admin.nav.appearance_text'), + }, + app.translator.trans('core.admin.nav.appearance_button') + ) ); items.add( 'extensions', - AdminLinkButton.component({ - href: app.route('extensions'), - icon: 'fas fa-puzzle-piece', - children: app.translator.trans('core.admin.nav.extensions_button'), - description: app.translator.trans('core.admin.nav.extensions_text'), - }) + AdminLinkButton.component( + { + href: app.route('extensions'), + icon: 'fas fa-puzzle-piece', + description: app.translator.trans('core.admin.nav.extensions_text'), + }, + app.translator.trans('core.admin.nav.extensions_button') + ) ); return items; diff --git a/js/src/admin/components/AppearancePage.js b/js/src/admin/components/AppearancePage.js index 809bd6826..f77d50d98 100644 --- a/js/src/admin/components/AppearancePage.js +++ b/js/src/admin/components/AppearancePage.js @@ -6,15 +6,16 @@ import EditCustomHeaderModal from './EditCustomHeaderModal'; import EditCustomFooterModal from './EditCustomFooterModal'; import UploadImageButton from './UploadImageButton'; import saveSettings from '../utils/saveSettings'; +import withAttr from '../../common/utils/withAttr'; export default class AppearancePage extends Page { - init() { - super.init(); + oninit(vnode) { + super.oninit(vnode); - this.primaryColor = m.prop(app.data.settings.theme_primary_color); - this.secondaryColor = m.prop(app.data.settings.theme_secondary_color); - this.darkMode = m.prop(app.data.settings.theme_dark_mode); - this.coloredHeader = m.prop(app.data.settings.theme_colored_header); + this.primaryColor = m.stream(app.data.settings.theme_primary_color); + this.secondaryColor = m.stream(app.data.settings.theme_secondary_color); + this.darkMode = m.stream(app.data.settings.theme_dark_mode); + this.coloredHeader = m.stream(app.data.settings.theme_colored_header); } view() { @@ -27,40 +28,34 @@ export default class AppearancePage extends Page {
{app.translator.trans('core.admin.appearance.colors_text')}
- - + +
- {Switch.component({ - state: this.darkMode(), - children: app.translator.trans('core.admin.appearance.dark_mode_label'), - onchange: this.darkMode, - })} + {Switch.component( + { + state: this.darkMode(), + onchange: this.darkMode, + }, + app.translator.trans('core.admin.appearance.dark_mode_label') + )} - {Switch.component({ - state: this.coloredHeader(), - children: app.translator.trans('core.admin.appearance.colored_header_label'), - onchange: this.coloredHeader, - })} + {Switch.component( + { + state: this.coloredHeader(), + onchange: this.coloredHeader, + }, + app.translator.trans('core.admin.appearance.colored_header_label') + )} - {Button.component({ - className: 'Button Button--primary', - type: 'submit', - children: app.translator.trans('core.admin.appearance.submit_button'), - loading: this.loading, - })} + {Button.component( + { + className: 'Button Button--primary', + type: 'submit', + loading: this.loading, + }, + app.translator.trans('core.admin.appearance.submit_button') + )} @@ -79,31 +74,37 @@ export default class AppearancePage extends Page {
{app.translator.trans('core.admin.appearance.custom_header_heading')}
{app.translator.trans('core.admin.appearance.custom_header_text')}
- {Button.component({ - className: 'Button', - children: app.translator.trans('core.admin.appearance.edit_header_button'), - onclick: () => app.modal.show(EditCustomHeaderModal), - })} + {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomHeaderModal), + }, + app.translator.trans('core.admin.appearance.edit_header_button') + )}
{app.translator.trans('core.admin.appearance.custom_footer_heading')}
{app.translator.trans('core.admin.appearance.custom_footer_text')}
- {Button.component({ - className: 'Button', - children: app.translator.trans('core.admin.appearance.edit_footer_button'), - onclick: () => app.modal.show(EditCustomFooterModal), - })} + {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomFooterModal), + }, + app.translator.trans('core.admin.appearance.edit_footer_button') + )}
{app.translator.trans('core.admin.appearance.custom_styles_heading')}
{app.translator.trans('core.admin.appearance.custom_styles_text')}
- {Button.component({ - className: 'Button', - children: app.translator.trans('core.admin.appearance.edit_css_button'), - onclick: () => app.modal.show(EditCustomCssModal), - })} + {Button.component( + { + className: 'Button', + onclick: () => app.modal.show(EditCustomCssModal), + }, + app.translator.trans('core.admin.appearance.edit_css_button') + )}
diff --git a/js/src/admin/components/BasicsPage.js b/js/src/admin/components/BasicsPage.js index 68f9ba142..9b5a7dc7f 100644 --- a/js/src/admin/components/BasicsPage.js +++ b/js/src/admin/components/BasicsPage.js @@ -5,10 +5,11 @@ import Button from '../../common/components/Button'; import saveSettings from '../utils/saveSettings'; import ItemList from '../../common/utils/ItemList'; import Switch from '../../common/components/Switch'; +import withAttr from '../../common/utils/withAttr'; export default class BasicsPage extends Page { - init() { - super.init(); + oninit(vnode) { + super.oninit(vnode); this.loading = false; @@ -25,7 +26,7 @@ export default class BasicsPage extends Page { this.values = {}; const settings = app.data.settings; - this.fields.forEach((key) => (this.values[key] = m.prop(settings[key]))); + this.fields.forEach((key) => (this.values[key] = m.stream(settings[key]))); this.localeOptions = {}; const locales = app.data.locales; @@ -49,45 +50,51 @@ export default class BasicsPage extends Page {
- {FieldSet.component({ - label: app.translator.trans('core.admin.basics.forum_title_heading'), - children: [], - })} + {FieldSet.component( + { + label: app.translator.trans('core.admin.basics.forum_title_heading'), + }, + [] + )} - {FieldSet.component({ - label: app.translator.trans('core.admin.basics.forum_description_heading'), - children: [ + {FieldSet.component( + { + label: app.translator.trans('core.admin.basics.forum_description_heading'), + }, + [
{app.translator.trans('core.admin.basics.forum_description_text')}
, -