Fix to work without errors in Nuxt Universal (#723)
by @patarapolw * allow manual init and destroy for use with Nuxt * fix onDestroy-related methods * avoid global scope, and use function scope instead * add createInstance function to window.REMARK42 * 1. allow DOMNode to be put in remark_config 2. check remark_config before try to attach node * move createInstance function outside * update embed.ts * avoid ?. * 📚 Docs: doc on how to make it work with SPAs * 📚 Docs: fix spa.md to be more flexible * ✨ Feat: add REMARK42::ready event * remove nuxt-specific terminologies * tell MutationObserver to disconnect on destroy * update docs/spa.md
This commit is contained in:
@@ -476,6 +476,8 @@ And then add this node in the place where you want to see Remark42 widget:
|
||||
|
||||
After that widget will be rendered inside this node.
|
||||
|
||||
If you want to set this up on a Single Page App, see [/docs/spa.md](/docs/spa.md).
|
||||
|
||||
##### Themes
|
||||
|
||||
Right now Remark has two themes: light and dark.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
- [How to configure remark42 with nginx reverse proxy](nginx-proxy.md)
|
||||
- [How to configure remark42 without a subdomain](subdomain.md) with Nginx or Caddy
|
||||
- [How to configure remark42 for Single Page Apps (SPA)](spa.md)
|
||||
- [Telegram notifications](telegram.md)
|
||||
- [Setup email authentication and\or email notifications](email.md)
|
||||
- [How to add new translation to remark42](translation.md)
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
## How to configure remark42 for Single Page Apps (SPA)
|
||||
|
||||
Originally tested on [Nuxt.js](https://nuxtjs.org/), but it should be applicable to all SPAs.
|
||||
|
||||
- Add the following JavaScript to your `index.html`, which in this case, it is identical to `<script defer src="$HOST/web/embed.js"></script>`
|
||||
|
||||
```js
|
||||
;(function () {
|
||||
var host = // Your remark42 host
|
||||
var components = ['embed'] // Your choice of remark42 components
|
||||
|
||||
;(function(c) {
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
const d = document
|
||||
const s = d.createElement('script')
|
||||
s.src = remark_config.host + '/web/' + c[i] + '.js'
|
||||
s.defer = true
|
||||
;(d.head || d.body).appendChild(s)
|
||||
}
|
||||
})(components)
|
||||
})
|
||||
```
|
||||
|
||||
- Created `remark42Instance` when the `div` containing remark42 has appear, usually at `mounted` or `componentDidMount` of the SPA lifecycle. Destroy the previous instance first, if neccessary.
|
||||
|
||||
```ts
|
||||
initRemark42() {
|
||||
if (window.REMARK42) {
|
||||
if (this.remark42Instance) {
|
||||
this.remark42Instance.destroy()
|
||||
}
|
||||
|
||||
this.remark42Instance = window.REMARK42.createInstance({
|
||||
node: this.$refs.remark42 as HTMLElement,
|
||||
...remark42_config // See <https://github.com/patarapolw/remark42#setup-on-your-website>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mounted() {
|
||||
if (window.REMARK42) {
|
||||
this.initRemark42()
|
||||
} else {
|
||||
window.addEventListener('REMARK42::ready', () => {
|
||||
this.initRemark42()
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Ensure that this is called every time route changes
|
||||
|
||||
```ts
|
||||
@Watch('$route.path')
|
||||
onRouteChange() {
|
||||
this.initRemark42()
|
||||
}
|
||||
```
|
||||
|
||||
- And, destroyed before routeLeave
|
||||
|
||||
```ts
|
||||
beforeRouteLeave() {
|
||||
if (this.remark42Instance) {
|
||||
this.remark42Instance.destroy()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = [
|
||||
{
|
||||
path: 'public/embed.js',
|
||||
limit: '2.55 KB',
|
||||
limit: '2.7 KB',
|
||||
},
|
||||
{
|
||||
limit: '86 KB',
|
||||
|
||||
Vendored
+10
-1
@@ -5,7 +5,16 @@ declare global {
|
||||
interface Window {
|
||||
remark_config: CommentsConfig;
|
||||
REMARK42: {
|
||||
changeTheme(theme: Theme): void;
|
||||
changeTheme?: (theme: Theme) => void;
|
||||
destroy?: () => void;
|
||||
createInstance: (
|
||||
remark_config: CommentsConfig
|
||||
) =>
|
||||
| {
|
||||
changeTheme(theme: Theme): void;
|
||||
destroy(): void;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface CommentsConfig {
|
||||
max_shown_comments?: number;
|
||||
theme?: Theme;
|
||||
page_title?: string;
|
||||
node?: string;
|
||||
node?: string | HTMLElement;
|
||||
locale?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
__colors__?: any;
|
||||
|
||||
+114
-74
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from '@app/common/constants.config';
|
||||
import { UserInfo, Theme } from '@app/common/types';
|
||||
import { CommentsConfig } from './common/config-types';
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -47,41 +48,50 @@ function createFrame({
|
||||
}
|
||||
|
||||
function init() {
|
||||
const node = document.getElementById(window.remark_config.node || NODE_ID);
|
||||
window.REMARK42 = window.REMARK42 || {};
|
||||
window.REMARK42.createInstance = createInstance;
|
||||
|
||||
if (window.remark_config) {
|
||||
createInstance(window.remark_config);
|
||||
}
|
||||
|
||||
window.dispatchEvent(new Event('REMARK42::ready'));
|
||||
}
|
||||
|
||||
function createInstance(config: CommentsConfig) {
|
||||
let initDataAnimationTimeout: number | null = null;
|
||||
let titleObserver: MutationObserver | null = null;
|
||||
|
||||
try {
|
||||
config = config || {};
|
||||
} catch (e) {
|
||||
console.error('Remark42: Config object is undefined.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.site_id) {
|
||||
console.error('Remark42: Site ID is undefined.');
|
||||
return;
|
||||
}
|
||||
|
||||
config.url = (config.url || window.location.origin + window.location.pathname).split('#')[0];
|
||||
|
||||
const node = config.node instanceof HTMLElement ? config.node : document.getElementById(config.node || NODE_ID);
|
||||
|
||||
if (!node) {
|
||||
console.error("Remark42: Can't find root node.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.remark_config = window.remark_config || {};
|
||||
} catch (e) {
|
||||
console.error('Remark42: Config object is undefined.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.remark_config.site_id) {
|
||||
console.error('Remark42: Site ID is undefined.');
|
||||
return;
|
||||
}
|
||||
|
||||
window.remark_config.url = (window.remark_config.url || window.location.origin + window.location.pathname).split(
|
||||
'#'
|
||||
)[0];
|
||||
|
||||
window.REMARK42 = window.REMARK42 || {};
|
||||
window.REMARK42.changeTheme = changeTheme;
|
||||
|
||||
const query = Object.keys(window.remark_config)
|
||||
const query = Object.keys(config)
|
||||
.filter(key => key !== '__colors__')
|
||||
.map(key => {
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(
|
||||
window.remark_config[key as keyof typeof window.remark_config]
|
||||
)}`;
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(config[key as keyof typeof config])}`;
|
||||
})
|
||||
.join('&');
|
||||
const iframe = createFrame({ host: BASE_URL, query, __colors__: window.remark_config.__colors__ });
|
||||
const iframe =
|
||||
(document.querySelector('iframe[title=Remark42]') as HTMLIFrameElement) ||
|
||||
createFrame({ host: BASE_URL, query, __colors__: config.__colors__ });
|
||||
|
||||
node.appendChild(iframe);
|
||||
|
||||
@@ -91,7 +101,8 @@ function init() {
|
||||
|
||||
const titleElement = document.querySelector('title');
|
||||
if (titleElement) {
|
||||
new MutationObserver(mutations => postTitleToIframe(mutations[0].target.textContent!)).observe(titleElement, {
|
||||
titleObserver = new MutationObserver(mutations => postTitleToIframe(mutations[0].target.textContent!));
|
||||
titleObserver.observe(titleElement, {
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
@@ -126,54 +137,54 @@ function init() {
|
||||
this.style.setAttribute('rel', 'stylesheet');
|
||||
this.style.setAttribute('type', 'text/css');
|
||||
this.style.innerHTML = `
|
||||
#${remarkRootId}-node {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 400px;
|
||||
transition: transform 0.4s ease-out;
|
||||
max-width: 100%;
|
||||
transform: translate(400px, 0);
|
||||
}
|
||||
#${remarkRootId}-node[data-animation] {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
#${remarkRootId}-back {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease-out;
|
||||
}
|
||||
#${remarkRootId}-back[data-animation] {
|
||||
opacity: 1;
|
||||
}
|
||||
#${remarkRootId}-node {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 400px;
|
||||
transition: transform 0.4s ease-out;
|
||||
max-width: 100%;
|
||||
transform: translate(400px, 0);
|
||||
}
|
||||
#${remarkRootId}-node[data-animation] {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
#${remarkRootId}-back {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease-out;
|
||||
}
|
||||
#${remarkRootId}-back[data-animation] {
|
||||
opacity: 1;
|
||||
}
|
||||
#${remarkRootId}-close {
|
||||
top: 0px;
|
||||
right: 400px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
margin-right: 4px;
|
||||
background-color: transparent;
|
||||
}
|
||||
@media all and (max-width: 430px) {
|
||||
#${remarkRootId}-close {
|
||||
top: 0px;
|
||||
right: 400px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
margin-right: 4px;
|
||||
background-color: transparent;
|
||||
right: 0px;
|
||||
font-size: 20px;
|
||||
color: black;
|
||||
}
|
||||
@media all and (max-width: 430px) {
|
||||
#${remarkRootId}-close {
|
||||
right: 0px;
|
||||
font-size: 20px;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
}
|
||||
if (!this.node) {
|
||||
this.node = document.createElement('div');
|
||||
@@ -202,10 +213,10 @@ function init() {
|
||||
document.body.appendChild(this.back);
|
||||
document.body.appendChild(this.node);
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
setTimeout(() => {
|
||||
initDataAnimationTimeout = window.setTimeout(() => {
|
||||
this.back!.setAttribute('data-animation', '');
|
||||
this.node!.setAttribute('data-animation', '');
|
||||
this.iframe!.focus();
|
||||
iframe.focus();
|
||||
}, 400);
|
||||
},
|
||||
close() {
|
||||
@@ -306,4 +317,33 @@ function init() {
|
||||
function changeTheme(theme: Theme) {
|
||||
iframe.contentWindow!.postMessage(JSON.stringify({ theme }), '*');
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (initDataAnimationTimeout) {
|
||||
clearTimeout(initDataAnimationTimeout);
|
||||
}
|
||||
|
||||
window.removeEventListener('message', receiveMessages);
|
||||
window.removeEventListener('hashchange', postHashToIframe);
|
||||
document.removeEventListener('click', postClickOutsideToIframe);
|
||||
|
||||
if (titleObserver) {
|
||||
titleObserver.disconnect();
|
||||
}
|
||||
|
||||
iframe.remove();
|
||||
}
|
||||
|
||||
// TODO: These do not appear in Chrome DevTools
|
||||
window.REMARK42.changeTheme = changeTheme;
|
||||
window.REMARK42.destroy = () => {
|
||||
destroy();
|
||||
delete window.REMARK42.changeTheme;
|
||||
delete window.REMARK42.destroy;
|
||||
};
|
||||
|
||||
return {
|
||||
changeTheme,
|
||||
destroy,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user