diff --git a/framework/core/js/src/common/compat.js b/framework/core/js/src/common/compat.js
index c6fe7e63a..268f33982 100644
--- a/framework/core/js/src/common/compat.js
+++ b/framework/core/js/src/common/compat.js
@@ -47,6 +47,7 @@ import FieldSet from './components/FieldSet';
import Select from './components/Select';
import Navigation from './components/Navigation';
import Alert from './components/Alert';
+import Link from './components/Link';
import LinkButton from './components/LinkButton';
import Checkbox from './components/Checkbox';
import SelectDropdown from './components/SelectDropdown';
@@ -118,6 +119,7 @@ export default {
'components/Select': Select,
'components/Navigation': Navigation,
'components/Alert': Alert,
+ 'components/Link': Link,
'components/LinkButton': LinkButton,
'components/Checkbox': Checkbox,
'components/SelectDropdown': SelectDropdown,
diff --git a/framework/core/js/src/common/components/Link.js b/framework/core/js/src/common/components/Link.js
new file mode 100644
index 000000000..deca94683
--- /dev/null
+++ b/framework/core/js/src/common/components/Link.js
@@ -0,0 +1,47 @@
+import Component from '../Component';
+import extract from '../utils/extract';
+
+/**
+ * The link component enables both internal and external links.
+ * It will return a regular HTML link for any links to external sites,
+ * and it will use Mithril's m.route.Link for any internal links.
+ *
+ * Links will default to internal; the 'external' attr must be set to
+ * `true` for the link to be external.
+ */
+export default class Link extends Component {
+ view(vnode) {
+ let { options = {}, ...attrs } = vnode.attrs;
+
+ attrs.href = attrs.href || '';
+
+ // For some reason, m.route.Link does not like vnode.text, so if present, we
+ // need to convert it to text vnodes and store it in children.
+ const children = vnode.children || { tag: '#', children: vnode.text };
+
+ if (attrs.external) {
+ return {children};
+ }
+
+ // If the href URL of the link is the same as the current page path
+ // we will not add a new entry to the browser history.
+ // This allows us to still refresh the Page component
+ // without adding endless history entries.
+ if (attrs.href === m.route.get()) {
+ if (!('replace' in options)) options.replace = true;
+ }
+
+ // Mithril 2 does not completely rerender the page if a route change leads to the same route
+ // (or the same component handling a different route).
+ // Here, the `force` parameter will use Mithril's key system to force a full rerender
+ // see https://mithril.js.org/route.html#key-parameter
+ if (extract(attrs, 'force')) {
+ if (!('state' in options)) options.state = {};
+ if (!('key' in options.state)) options.state.key = Date.now();
+ }
+
+ attrs.options = options;
+
+ return {children};
+ }
+}
diff --git a/framework/core/js/src/common/components/LinkButton.js b/framework/core/js/src/common/components/LinkButton.js
index a21cdd88c..2dd3f6f9b 100644
--- a/framework/core/js/src/common/components/LinkButton.js
+++ b/framework/core/js/src/common/components/LinkButton.js
@@ -1,4 +1,5 @@
import Button from './Button';
+import Link from './Link';
/**
* The `LinkButton` component defines a `Button` which links to a route.
@@ -22,7 +23,7 @@ export default class LinkButton extends Button {
view(vnode) {
const vdom = super.view(vnode);
- vdom.tag = m.route.Link;
+ vdom.tag = Link;
vdom.attrs.active = String(vdom.attrs.active);
return vdom;
diff --git a/framework/core/js/src/common/utils/patchMithril.js b/framework/core/js/src/common/utils/patchMithril.js
index b6743f696..8360e2b95 100644
--- a/framework/core/js/src/common/utils/patchMithril.js
+++ b/framework/core/js/src/common/utils/patchMithril.js
@@ -1,43 +1,15 @@
-import extract from './extract';
+import Link from '../components/Link';
import withAttr from './withAttr';
import Stream from './Stream';
let deprecatedMPropWarned = false;
let deprecatedMWithAttrWarned = false;
+let deprecatedRouteWarned = false;
+
export default function patchMithril(global) {
const defaultMithril = global.m;
- /**
- * If the href URL of the link is the same as the current page path
- * we will not add a new entry to the browser history.
- *
- * This allows us to still refresh the Page component
- * without adding endless history entries.
- *
- * We also add the `force` attribute that adds a custom state key
- * for when you want to force a complete refresh of the Page
- */
- const defaultLinkView = defaultMithril.route.Link.view;
- const modifiedLink = {
- view: function (vnode) {
- let { href, options = {} } = vnode.attrs;
-
- if (href === m.route.get()) {
- if (!('replace' in options)) options.replace = true;
- }
-
- if (extract(vnode.attrs, 'force')) {
- if (!('state' in options)) options.state = {};
- if (!('key' in options.state)) options.state.key = Date.now();
- }
-
- vnode.attrs.options = options;
-
- return defaultLinkView(vnode);
- },
- };
-
const modifiedMithril = function (comp, ...args) {
const node = defaultMithril.apply(this, arguments);
@@ -48,28 +20,29 @@ export default function patchMithril(global) {
modifiedMithril.bidi(node, node.attrs.bidi);
}
+ // DEPRECATED, REMOVE BETA 15
+
// Allows us to use a "route" attr on links, which will automatically convert the link to one which
// supports linking to other pages in the SPA without refreshing the document.
if (node.attrs.route) {
- node.attrs.href = node.attrs.route;
- node.tag = modifiedLink;
-
- // For some reason, m.route.Link does not like vnode.text, so if present, we
- // need to convert it to text vnodes and store it in children.
- if (node.text) {
- node.children = { tag: '#', children: node.text };
+ if (!deprecatedRouteWarned) {
+ deprecatedRouteWarned = true;
+ console.warn('The route attr patch for links is deprecated, please use the Link component (flarum/components/Link) instead.');
}
+ node.attrs.href = node.attrs.route;
+ node.tag = Link;
+
delete node.attrs.route;
}
+ // END DEPRECATED
+
return node;
};
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
- modifiedMithril.route.Link = modifiedLink;
-
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
modifiedMithril.prop = modifiedMithril.stream = function (...args) {
if (!deprecatedMPropWarned) {
diff --git a/framework/core/js/src/forum/components/DiscussionListItem.js b/framework/core/js/src/forum/components/DiscussionListItem.js
index 438d503b9..ef91d2520 100644
--- a/framework/core/js/src/forum/components/DiscussionListItem.js
+++ b/framework/core/js/src/forum/components/DiscussionListItem.js
@@ -1,4 +1,5 @@
import Component from '../../common/Component';
+import Link from '../../common/components/Link';
import avatar from '../../common/helpers/avatar';
import listItems from '../../common/helpers/listItems';
import highlight from '../../common/helpers/highlight';
@@ -98,8 +99,8 @@ export default class DiscussionListItem extends Component {
{username(user)}
-
+
)}
diff --git a/framework/core/js/src/forum/components/UsersSearchSource.js b/framework/core/js/src/forum/components/UsersSearchSource.js
index 461fbb477..de0a50635 100644
--- a/framework/core/js/src/forum/components/UsersSearchSource.js
+++ b/framework/core/js/src/forum/components/UsersSearchSource.js
@@ -1,6 +1,7 @@
import highlight from '../../common/helpers/highlight';
import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
+import Link from '../../common/components/Link';
/**
* The `UsersSearchSource` finds and displays user search results in the search
@@ -48,10 +49,10 @@ export default class UsersSearchResults {
return (