Fixed design preview and settings not updating when changing theme

refs https://github.com/TryGhost/Team/issues/1111

We now have a situation where we have modals on modals and we've lost the straightforward built-in "Data down, actions up" communication methods that we'd have workaround across nested routes/controllers. The upshot of that is we didn't have a way to trigger a refresh of the preview when a new theme was activated.

- moved the task responsible for fetching preview html from the design modal onto the `theme-management` service and adjusted it to set a tracked `previewHtml` property rather than updating an iframe directly
- added a `<GhHtmlIframe>` component that renders a basic iframe element and updates it's contents each time the `@html` argument changes
- updated design modal preview to use the new iframe component
This commit is contained in:
Kevin Ansfield 2021-10-05 21:32:42 +01:00
parent 640f028ae9
commit 221db9f11e
5 changed files with 91 additions and 77 deletions

View File

@ -0,0 +1,5 @@
<iframe
{{did-insert this.registerIframe}}
{{did-update this.replaceIframeContents @html}}
...attributes>
</iframe>

View File

@ -0,0 +1,20 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
export default class GhHtmlIframeComponent extends Component {
iframe = null;
@action
registerIframe(iframe) {
this.iframe = iframe;
}
@action
replaceIframeContents(iframe, html) {
if (this.iframe) {
this.iframe.contentWindow.document.open();
this.iframe.contentWindow.document.write(html);
this.iframe.contentWindow.document.close();
}
}
}

View File

@ -14,7 +14,7 @@
{{#if (set-has this.openSections "brand")}}
<div class="pt4">
<Settings::Design::GeneralSettingsForm
@updatePreview={{perform this.updatePreviewTask}}
@updatePreview={{perform this.themeManagement.updatePreviewHtmlTask}}
/>
</div>
{{/if}}
@ -32,7 +32,7 @@
<div class="pt4">
<Settings::Design::ThemeSettingsForm
@themeSettings={{this.siteWideSettings}}
@updatePreview={{perform this.updatePreviewTask}}
@updatePreview={{perform this.themeManagement.updatePreviewHtmlTask}}
/>
</div>
{{/if}}
@ -50,7 +50,7 @@
<div class="pt4">
<Settings::Design::ThemeSettingsForm
@themeSettings={{this.homepageSettings}}
@updatePreview={{perform this.updatePreviewTask}}
@updatePreview={{perform this.themeManagement.updatePreviewHtmlTask}}
/>
</div>
{{/if}}
@ -68,7 +68,7 @@
<div class="pt4">
<Settings::Design::ThemeSettingsForm
@themeSettings={{this.postPageSettings}}
@updatePreview={{perform this.updatePreviewTask}}
@updatePreview={{perform this.themeManagement.updatePreviewHtmlTask}}
/>
</div>
{{/if}}
@ -101,11 +101,7 @@
<div style="height: calc(100% - 96px)">
<GhBrowserPreview @icon={{this.settings.icon}} @title={{this.config.blogTitle}}>
<iframe
id="site=frame"
class="site-frame gh-branding-settings-preview"
{{did-insert this.registerPreviewIframe}}
></iframe>
<GhHtmlIframe id="site-frame" class="site-frame gh-branding-settings-preview" @html={{this.themeManagement.previewHtml}} />
</GhBrowserPreview>
</div>
</section>

View File

@ -1,5 +1,4 @@
import Component from '@glimmer/component';
import config from 'ghost-admin/config/environment';
import {TrackedSet} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
@ -7,18 +6,16 @@ import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';
export default class ModalsDesignCustomizeComponent extends Component {
@service ajax;
@service config;
@service customThemeSettings;
@service settings;
@service themeManagement;
@tracked openSections = new TrackedSet();
previewIframe = null;
constructor() {
super(...arguments);
this.fetchThemeSettingsTask.perform();
this.themeManagement.updatePreviewHtmlTask.perform();
}
get themeSettings() {
@ -37,76 +34,13 @@ export default class ModalsDesignCustomizeComponent extends Component {
return this.customThemeSettings.settings.filter(setting => setting.group === 'post');
}
get previewData() {
const params = new URLSearchParams();
params.append('c', this.settings.get('accentColor') || '#ffffff');
params.append('d', this.settings.get('description'));
params.append('icon', this.settings.get('icon'));
params.append('logo', this.settings.get('logo'));
params.append('cover', this.settings.get('coverImage'));
params.append('custom', JSON.stringify(this.customThemeSettings.keyValueObject));
return params.toString();
}
@action
toggleSection(section) {
this.openSections.has(section) ? this.openSections.delete(section) : this.openSections.add(section);
}
@action
registerPreviewIframe(iframe) {
this.previewIframe = iframe;
this.updatePreviewTask.perform();
}
@action
replacePreviewContents(html) {
if (this.previewIframe) {
this.previewIframe.contentWindow.document.open();
this.previewIframe.contentWindow.document.write(html);
this.previewIframe.contentWindow.document.close();
}
}
@task
*fetchThemeSettingsTask() {
yield this.customThemeSettings.load();
}
@task
*updatePreviewTask() {
// skip during testing because we don't have mocks for the front-end
if (config.environment === 'test' || !this.previewIframe) {
return;
}
// grab the preview html
const ajaxOptions = {
contentType: 'text/html;charset=utf-8',
dataType: 'text',
headers: {
'x-ghost-preview': this.previewData
}
};
// TODO: config.blogUrl always removes trailing slash - switch to always have trailing slash
const frontendUrl = `${this.config.get('blogUrl')}/`;
const previewContents = yield this.ajax.post(frontendUrl, ajaxOptions);
// inject extra CSS to disable navigation and prevent clicks
const injectedCss = `html { pointer-events: none; }`;
const domParser = new DOMParser();
const htmlDoc = domParser.parseFromString(previewContents, 'text/html');
const stylesheet = htmlDoc.querySelector('style');
const originalCSS = stylesheet.innerHTML;
stylesheet.innerHTML = `${originalCSS}\n\n${injectedCss}`;
// replace the iframe contents with the doctored preview html
this.replacePreviewContents(htmlDoc.documentElement.innerHTML);
}
}

View File

@ -1,12 +1,20 @@
import Service from '@ember/service';
import config from 'ghost-admin/config/environment';
import {isEmpty} from '@ember/utils';
import {isThemeValidationError} from 'ghost-admin/services/ajax';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';
export default class ThemeManagementService extends Service {
@service ajax;
@service config;
@service customThemeSettings;
@service limit;
@service modals;
@service settings;
@tracked previewHtml;
@task
*activateTask(theme) {
@ -35,6 +43,9 @@ export default class ThemeManagementService extends Service {
try {
const activatedTheme = yield theme.activate();
this.updatePreviewHtmlTask.perform();
this.customThemeSettings.load();
const {warnings, errors} = activatedTheme;
if (!isEmpty(warnings) || !isEmpty(errors)) {
@ -83,4 +94,52 @@ export default class ThemeManagementService extends Service {
resultModal?.close();
}
}
@task
*updatePreviewHtmlTask() {
// skip during testing because we don't have mocks for the front-end
if (config.environment === 'test') {
return;
}
// grab the preview html
const ajaxOptions = {
contentType: 'text/html;charset=utf-8',
dataType: 'text',
headers: {
'x-ghost-preview': this.previewData
}
};
// TODO: config.blogUrl always removes trailing slash - switch to always have trailing slash
const frontendUrl = `${this.config.get('blogUrl')}/`;
const previewContents = yield this.ajax.post(frontendUrl, ajaxOptions);
// inject extra CSS to disable navigation and prevent clicks
const injectedCss = `html { pointer-events: none; }`;
const domParser = new DOMParser();
const htmlDoc = domParser.parseFromString(previewContents, 'text/html');
const stylesheet = htmlDoc.querySelector('style');
const originalCSS = stylesheet.innerHTML;
stylesheet.innerHTML = `${originalCSS}\n\n${injectedCss}`;
// replace the iframe contents with the doctored preview html
this.previewHtml = htmlDoc.documentElement.innerHTML;
}
get previewData() {
const params = new URLSearchParams();
params.append('c', this.settings.get('accentColor') || '#ffffff');
params.append('d', this.settings.get('description'));
params.append('icon', this.settings.get('icon'));
params.append('logo', this.settings.get('logo'));
params.append('cover', this.settings.get('coverImage'));
params.append('custom', JSON.stringify(this.customThemeSettings.keyValueObject));
return params.toString();
}
}