mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
🐛 Fixed Code Injection input fields not being clickable
no issue - lazy loaded scripts such as the CodeMirror asset used on the Code Injection screen could throw errors such as `TypeError: Cannot set property 'modeOption' of undefined` - this was caused by "loading" promise returned from the `lazyLoader` service returning as soon as the network request finished which can be before the loaded script has been parsed and run meaning any processing occurring after the promise returns could be depending on unloaded code - switched the lazyLoader service's loading mechanism from an ajax fetch to insertion of a `<script>` tag which can have `load` event attached which _will_ return after parsing/loading has completed
This commit is contained in:
parent
346fef5947
commit
e7c3b1b0e3
3 changed files with 43 additions and 33 deletions
|
@ -1,6 +1,5 @@
|
||||||
/* global CodeMirror */
|
/* global CodeMirror */
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import RSVP from 'rsvp';
|
|
||||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||||
import {assign} from '@ember/polyfills';
|
import {assign} from '@ember/polyfills';
|
||||||
import {bind, once, scheduleOnce} from '@ember/runloop';
|
import {bind, once, scheduleOnce} from '@ember/runloop';
|
||||||
|
@ -63,10 +62,7 @@ const CmEditorComponent = Component.extend({
|
||||||
|
|
||||||
initCodeMirror: task(function* () {
|
initCodeMirror: task(function* () {
|
||||||
let loader = this.get('lazyLoader');
|
let loader = this.get('lazyLoader');
|
||||||
|
yield loader.loadScript('codemirror', 'assets/codemirror/codemirror.js');
|
||||||
yield RSVP.all([
|
|
||||||
loader.loadScript('codemirror', 'assets/codemirror/codemirror.js')
|
|
||||||
]);
|
|
||||||
|
|
||||||
scheduleOnce('afterRender', this, this._initCodeMirror);
|
scheduleOnce('afterRender', this, this._initCodeMirror);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import RSVP from 'rsvp';
|
import RSVP from 'rsvp';
|
||||||
import Service, {inject as service} from '@ember/service';
|
import Service, {inject as service} from '@ember/service';
|
||||||
import config from 'ghost-admin/config/environment';
|
import config from 'ghost-admin/config/environment';
|
||||||
|
@ -22,31 +21,41 @@ export default Service.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
loadScript(key, url) {
|
loadScript(key, url) {
|
||||||
if (this.get('testing')) {
|
if (this.testing) {
|
||||||
return RSVP.resolve();
|
return RSVP.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.get(`scriptPromises.${key}`)) {
|
if (this.scriptPromises[key]) {
|
||||||
// Script is already loaded/in the process of being loaded,
|
return this.scriptPromises[key];
|
||||||
// so return that promise
|
|
||||||
return this.get(`scriptPromises.${key}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ajax = this.get('ajax');
|
let scriptPromise = new RSVP.Promise((resolve, reject) => {
|
||||||
let adminRoot = this.get('ghostPaths.adminRoot');
|
let {adminRoot} = this.ghostPaths;
|
||||||
|
|
||||||
let scriptPromise = ajax.request(`${adminRoot}${url}`, {
|
let script = document.createElement('script');
|
||||||
dataType: 'script',
|
script.type = 'text/javascript';
|
||||||
cache: true
|
script.async = true;
|
||||||
|
script.src = `${adminRoot}${url}`;
|
||||||
|
|
||||||
|
let el = document.getElementsByTagName('script')[0];
|
||||||
|
el.parentNode.insertBefore(script, el);
|
||||||
|
|
||||||
|
script.addEventListener('load', () => {
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set(`scriptPromises.${key}`, scriptPromise);
|
script.addEventListener('error', () => {
|
||||||
|
reject(new Error(`${url} failed to load`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scriptPromises[key] = scriptPromise;
|
||||||
|
|
||||||
return scriptPromise;
|
return scriptPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
loadStyle(key, url, alternate = false) {
|
loadStyle(key, url, alternate = false) {
|
||||||
if (this.get('testing') || $(`#${key}-styles`).length) {
|
if (this.testing || document.querySelector(`#${key}-styles`)) {
|
||||||
return RSVP.resolve();
|
return RSVP.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +63,7 @@ export default Service.extend({
|
||||||
let link = document.createElement('link');
|
let link = document.createElement('link');
|
||||||
link.id = `${key}-styles`;
|
link.id = `${key}-styles`;
|
||||||
link.rel = alternate ? 'alternate stylesheet' : 'stylesheet';
|
link.rel = alternate ? 'alternate stylesheet' : 'stylesheet';
|
||||||
link.href = `${this.get('ghostPaths.adminRoot')}${url}`;
|
link.href = `${this.ghostPaths.adminRoot}${url}`;
|
||||||
link.onload = () => {
|
link.onload = () => {
|
||||||
if (alternate) {
|
if (alternate) {
|
||||||
// If stylesheet is alternate and we disable the stylesheet before injecting into the DOM,
|
// If stylesheet is alternate and we disable the stylesheet before injecting into the DOM,
|
||||||
|
@ -69,7 +78,7 @@ export default Service.extend({
|
||||||
link.title = key;
|
link.title = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('head').append($(link));
|
document.querySelector('head').appendChild(link);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {setupTest} from 'ember-mocha';
|
||||||
|
|
||||||
describe('Integration: Service: lazy-loader', function () {
|
describe('Integration: Service: lazy-loader', function () {
|
||||||
setupTest('service:lazy-loader', {integration: true});
|
setupTest('service:lazy-loader', {integration: true});
|
||||||
|
|
||||||
let server;
|
let server;
|
||||||
let ghostPaths = {
|
let ghostPaths = {
|
||||||
adminRoot: '/assets/'
|
adminRoot: '/assets/'
|
||||||
|
@ -19,28 +20,32 @@ describe('Integration: Service: lazy-loader', function () {
|
||||||
server.shutdown();
|
server.shutdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads a script correctly and only once', function () {
|
it('loads a script correctly and only once', async function () {
|
||||||
let subject = this.subject({
|
let subject = this.subject({
|
||||||
ghostPaths,
|
ghostPaths,
|
||||||
scriptPromises: {},
|
scriptPromises: {},
|
||||||
testing: false
|
testing: false
|
||||||
});
|
});
|
||||||
|
|
||||||
server.get('/assets/test.js', function ({requestHeaders}) {
|
// first load should add script element
|
||||||
expect(requestHeaders.Accept).to.match(/text\/javascript/);
|
await subject.loadScript('test', 'lazy-test.js')
|
||||||
|
.then(() => {})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
return [200, {'Content-Type': 'text/javascript'}, 'window.testLoadScript = \'testvalue\''];
|
expect(
|
||||||
});
|
document.querySelectorAll('script[src="/assets/lazy-test.js"]').length,
|
||||||
|
'no of script tags on first load'
|
||||||
|
).to.equal(1);
|
||||||
|
|
||||||
return subject.loadScript('test-script', 'test.js').then(() => {
|
// second load should not add another script element
|
||||||
expect(subject.get('scriptPromises.test-script')).to.exist;
|
await subject.loadScript('test', '/assets/lazy-test.js')
|
||||||
expect(window.testLoadScript).to.equal('testvalue');
|
.then(() => { })
|
||||||
expect(server.handlers[0].numberOfCalls).to.equal(1);
|
.catch(() => { });
|
||||||
|
|
||||||
return subject.loadScript('test-script', 'test.js');
|
expect(
|
||||||
}).then(() => {
|
document.querySelectorAll('script[src="/assets/lazy-test.js"]').length,
|
||||||
expect(server.handlers[0].numberOfCalls).to.equal(1);
|
'no of script tags on second load'
|
||||||
});
|
).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads styles correctly', function () {
|
it('loads styles correctly', function () {
|
||||||
|
|
Loading…
Add table
Reference in a new issue