lazy-load codemirror on code injection screen (#99)

refs TryGhost/Ghost#6149
- concats codemirror.js and css on build, keeping them out of vendor.js
- add lazy-loader service to enable loading of external scripts
This commit is contained in:
Austin Burdine 2016-07-05 10:30:14 -06:00 committed by Kevin Ansfield
parent 8c8365720b
commit c0f21b37d3
5 changed files with 187 additions and 13 deletions

View File

@ -1,6 +1,8 @@
/* global CodeMirror */
import Component from 'ember-component';
import run, {bind} from 'ember-runloop';
import run, {bind, scheduleOnce} from 'ember-runloop';
import injectService from 'ember-service/inject';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import {InvokeActionMixin} from 'ember-invoke-action';
@ -18,9 +20,21 @@ const CmEditorComponent = Component.extend(InvokeActionMixin, {
_editor: null, // reference to CodeMirror editor
lazyLoader: injectService(),
didInsertElement() {
this._super(...arguments);
this.get('lazyLoader').loadStyle('codemirror', 'codemirror/codemirror.css');
this.get('lazyLoader').loadScript('codemirror', 'codemirror/codemirror.js').then(() => {
scheduleOnce('afterRender', this, function () {
this._initCodeMirror();
});
});
},
_initCodeMirror() {
let options = this.getProperties('lineNumbers', 'indentUnit', 'mode', 'theme');
let editor = new CodeMirror(this.element, options);

View File

@ -0,0 +1,53 @@
import $ from 'jquery';
import Ember from 'ember';
import RSVP from 'rsvp';
import Service from 'ember-service';
import injectService from 'ember-service/inject';
const {testing} = Ember;
export default Service.extend({
ajax: injectService(),
ghostPaths: injectService(),
// This is needed so we can disable it in unit tests
testing,
scriptPromises: {},
loadScript(key, url) {
if (this.get('testing')) {
return RSVP.resolve();
}
if (this.get(`scriptPromises.${key}`)) {
// Script is already loaded/in the process of being loaded,
// so return that promise
return this.get(`scriptPromises.${key}`);
}
let ajax = this.get('ajax');
let adminRoot = this.get('ghostPaths.adminRoot');
let scriptPromise = ajax.request(`${adminRoot}${url}`, {
dataType: 'script',
cache: true
});
this.set(`scriptPromises.${key}`, scriptPromise);
return scriptPromise;
},
loadStyle(key, url) {
if (this.get('testing')) {
return RSVP.resolve();
}
if (!$(`#${key}-styles`).length) {
let $style = $(`<link rel="stylesheet" id="${key}-styles" />`);
$style.attr('href', `${this.get('ghostPaths.adminRoot')}${url}`);
$('head').append($style);
}
}
});

View File

@ -2,11 +2,15 @@
/* global require, module */
var EmberApp = require('ember-cli/lib/broccoli/ember-app'),
concat = require('broccoli-concat'),
mergeTrees = require('broccoli-merge-trees'),
uglify = require('broccoli-uglify-js'),
cleanCSS = require('broccoli-clean-css'),
environment = EmberApp.env(),
isProduction = environment === 'production',
mythCompress = isProduction || environment === 'test',
disabled = {enabled: false},
assetLocation;
assetLocation, codemirrorAssets;
assetLocation = function (fileName) {
if (isProduction) {
@ -15,6 +19,48 @@ assetLocation = function (fileName) {
return '/assets/' + fileName;
};
codemirrorAssets = function () {
var codemirrorFiles = [
'lib/codemirror.css',
'theme/xq-light.css',
'lib/codemirror.js',
'mode/htmlmixed/htmlmixed.js',
'mode/xml/xml.js',
'mode/css/css.js',
'mode/javascript/javascript.js'
];
if (environment === 'test') {
return {import: codemirrorFiles};
}
return {
public: {
include: codemirrorFiles,
destDir: '/',
processTree: function (tree) {
var jsTree = concat(tree, {
outputFile: 'assets/codemirror/codemirror.js',
headerFiles: ['lib/codemirror.js'],
inputFiles: ['mode/**/*']
});
var cssTree = concat(tree, {
outputFile: 'assets/codemirror/codemirror.css',
inputFiles: ['**/*.css']
});
if (isProduction) {
jsTree = uglify(jsTree);
cssTree = cleanCSS(cssTree);
}
return mergeTrees([jsTree, cssTree]);
}
}
};
};
module.exports = function (defaults) {
var app = new EmberApp(defaults, {
babel: {
@ -45,17 +91,7 @@ module.exports = function (defaults) {
'blueimp-md5': {
import: ['js/md5.js']
},
codemirror: {
import: [
'lib/codemirror.js',
'lib/codemirror.css',
'theme/xq-light.css',
'mode/htmlmixed/htmlmixed.js',
'mode/xml/xml.js',
'mode/css/css.js',
'mode/javascript/javascript.js',
]
},
codemirror: codemirrorAssets(),
'jquery-deparam': {
enabled: EmberApp.env() === 'test',
import: ['jquery-deparam.js']

View File

@ -28,6 +28,10 @@
"blueimp-md5": "2.3.0",
"bower": "1.7.9",
"broccoli-asset-rev": "2.4.4",
"broccoli-clean-css": "1.1.0",
"broccoli-concat": "2.2.0",
"broccoli-merge-trees": "1.1.1",
"broccoli-uglify-js": "0.2.0",
"chalk": "1.1.3",
"codemirror": "5.16.0",
"csscomb": "3.1.8",

View File

@ -0,0 +1,67 @@
/* jshint expr:true */
import { expect } from 'chai';
import {
describeModule,
it
} from 'ember-mocha';
import Pretender from 'pretender';
import RSVP from 'rsvp';
import $ from 'jquery';
describeModule(
'service:lazy-loader',
'Integration: Service: lazy-loader',
{integration: true},
function() {
let server;
let ghostPaths = {
adminRoot: '/assets/'
};
beforeEach(function () {
server = new Pretender();
});
afterEach(function () {
server.shutdown();
});
it('loads a script correctly and only once', function (done) {
let subject = this.subject({
ghostPaths,
scriptPromises: {},
testing: false
});
server.get('/assets/test.js', function ({requestHeaders}) {
expect(requestHeaders.Accept).to.match(/text\/javascript/);
return [200, {'Content-Type': 'text/javascript'}, 'window.testLoadScript = \'testvalue\''];
});
subject.loadScript('test-script', 'test.js').then(() => {
expect(subject.get('scriptPromises.test-script')).to.exist;
expect(window.testLoadScript).to.equal('testvalue');
expect(server.handlers[0].numberOfCalls).to.equal(1);
return subject.loadScript('test-script', 'test.js');
}).then(() => {
expect(server.handlers[0].numberOfCalls).to.equal(1);
done();
});
});
it('loads styles correctly', function () {
let subject = this.subject({
ghostPaths,
testing: false
});
subject.loadStyle('testing', 'style.css');
expect($('#testing-styles').length).to.equal(1);
expect($('#testing-styles').attr('href')).to.equal('/assets/style.css');
});
}
);