Use progressive enhancement when loading external modules

This commit is contained in:
David Evans 2017-12-10 15:46:09 +00:00
parent c1d604aacc
commit 4b33114824
4 changed files with 183 additions and 103 deletions

View File

@ -22,7 +22,7 @@
"devDependencies": {
"almond": "^0.3.3",
"http-server": "^0.10.0",
"requirejs": "^2.3.5",
"requirejs": "2.3.5",
"uglify-es": "^3.1.10"
}
}

View File

@ -1,10 +1,4 @@
define([
'split',
'cm/lib/codemirror',
'cm/addon/hint/show-hint',
'cm/addon/edit/trailingspace',
'cm/addon/comment/comment',
], (Split, CodeMirror) => {
define(['require'], (require) => {
'use strict';
const DELAY_AGENTCHANGE = 500;
@ -25,6 +19,13 @@ define([
return o;
}
function addNewline(value) {
if(value.length > 0 && value.charAt(value.length - 1) !== '\n') {
return value + '\n';
}
return value;
}
function on(element, events, fn) {
events.forEach((event) => element.addEventListener(event, fn));
}
@ -43,44 +44,48 @@ define([
}
function makeSplit(nodes, options) {
// Patches for:
// https://github.com/nathancahill/Split.js/issues/97
// https://github.com/nathancahill/Split.js/issues/111
const parent = nodes[0].parentNode;
const oldAEL = parent.addEventListener;
const oldREL = parent.removeEventListener;
parent.addEventListener = (event, callback) => {
if(event === 'mousemove' || event === 'touchmove') {
window.addEventListener(event, callback, {passive: true});
} else {
oldAEL.call(parent, event, callback);
}
};
parent.removeEventListener = (event, callback) => {
if(event === 'mousemove' || event === 'touchmove') {
window.removeEventListener(event, callback);
} else {
oldREL.call(parent, event, callback);
}
};
// Load on demand for progressive enhancement
// (failure to load external module will not block functionality)
require(['split'], (Split) => {
// Patches for:
// https://github.com/nathancahill/Split.js/issues/97
// https://github.com/nathancahill/Split.js/issues/111
const parent = nodes[0].parentNode;
const oldAEL = parent.addEventListener;
const oldREL = parent.removeEventListener;
parent.addEventListener = (event, callback) => {
if(event === 'mousemove' || event === 'touchmove') {
window.addEventListener(event, callback, {passive: true});
} else {
oldAEL.call(parent, event, callback);
}
};
parent.removeEventListener = (event, callback) => {
if(event === 'mousemove' || event === 'touchmove') {
window.removeEventListener(event, callback);
} else {
oldREL.call(parent, event, callback);
}
};
let oldCursor = null;
const resolvedOptions = Object.assign({
direction: 'vertical',
cursor: (options.direction === 'vertical') ?
'row-resize' : 'col-resize',
gutterSize: 1,
onDragStart: () => {
oldCursor = document.body.style.cursor;
document.body.style.cursor = resolvedOptions.cursor;
},
onDragEnd: () => {
document.body.style.cursor = oldCursor;
oldCursor = null;
},
}, options);
let oldCursor = null;
const resolvedOptions = Object.assign({
direction: 'vertical',
cursor: (options.direction === 'vertical') ?
'row-resize' : 'col-resize',
gutterSize: 1,
onDragStart: () => {
oldCursor = document.body.style.cursor;
document.body.style.cursor = resolvedOptions.cursor;
},
onDragEnd: () => {
document.body.style.cursor = oldCursor;
oldCursor = null;
},
}, options);
return new Split(nodes, resolvedOptions);
return new Split(nodes, resolvedOptions);
});
}
return class Interface {
@ -98,8 +103,6 @@ define([
this.links = links;
this.minScale = 1.5;
this.diagram.registerCodeMirrorMode(CodeMirror);
this.debounced = null;
this.latestSeq = null;
this.renderedSeq = null;
@ -111,6 +114,8 @@ define([
this._downloadSVGClick = this._downloadSVGClick.bind(this);
this._downloadPNGClick = this._downloadPNGClick.bind(this);
this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
this._enhanceEditor();
}
buildOptionsLinks() {
@ -154,58 +159,21 @@ define([
buildEditor(container) {
const value = this.loadCode() || this.defaultCode;
const code = new CodeMirror(container, {
value,
mode: 'sequence',
globals: {
themes: this.diagram.getThemeNames(),
},
lineNumbers: true,
showTrailingSpace: true,
extraKeys: {
'Tab': (cm) => cm.execCommand('indentMore'),
'Shift-Tab': (cm) => cm.execCommand('indentLess'),
'Cmd-/': (cm) => cm.toggleComment({padding: ''}),
'Ctrl-/': (cm) => cm.toggleComment({padding: ''}),
'Ctrl-Space': 'autocomplete',
'Ctrl-Enter': 'autocomplete',
'Cmd-Enter': 'autocomplete',
},
});
let lastKey = 0;
code.on('keydown', (cm, event) => {
lastKey = event.keyCode;
});
code.on('change', (cm, change) => {
if(change.origin === '+input') {
if(lastKey === 13) {
lastKey = 0;
return;
}
} else if(change.origin !== 'complete') {
return;
}
CodeMirror.commands.autocomplete(cm, null, {
completeSingle: false,
});
});
const code = makeNode('textarea', {'class': 'editor-simple'});
code.value = value;
container.appendChild(code);
return code;
}
registerListeners() {
this.code.on('change', () => this.update(false));
this.code.on('cursorActivity', () => {
const from = this.code.getCursor('from').line;
const to = this.code.getCursor('to').line;
this.diagram.setHighlight(Math.min(from, to));
});
this.code.addEventListener('input', () => this.update(false));
this.diagram.addEventListener('mouseover', (element) => {
if(this.marker) {
this.marker.clear();
}
if(element.ln !== undefined) {
if(element.ln !== undefined && this.code.markText) {
this.marker = this.code.markText(
{line: element.ln, ch: 0},
{line: element.ln + 1, ch: 0},
@ -231,7 +199,7 @@ define([
this.marker.clear();
this.marker = null;
}
if(element.ln !== undefined) {
if(element.ln !== undefined && this.code.setSelection) {
this.code.setSelection(
{line: element.ln, ch: 0},
{line: element.ln + 1, ch: 0},
@ -262,6 +230,7 @@ define([
container: holdInner,
});
} catch(e) {
window.console.log('Failed to render preview', e);
hold.setAttribute('class', 'library-item broken');
holdInner.appendChild(makeText(lib.code));
}
@ -348,16 +317,32 @@ define([
}
addCodeBlock(block) {
const cur = this.code.getCursor('head');
const pos = {line: cur.line + ((cur.ch > 0) ? 1 : 0), ch: 0};
const lines = block.split('\n').length;
this.code.replaceRange(
block + '\n',
pos,
null,
'library'
);
this.code.setCursor({line: pos.line + lines, ch: 0});
if(this.code.getCursor) {
const cur = this.code.getCursor('head');
const pos = {line: cur.line + ((cur.ch > 0) ? 1 : 0), ch: 0};
this.code.replaceRange(
addNewline(block),
pos,
null,
'library'
);
this.code.setCursor({line: pos.line + lines, ch: 0});
} else {
const value = this.value();
const cur = this.code.selectionStart;
const pos = ('\n' + value + '\n').indexOf('\n', cur);
const replaced = (
addNewline(value.substr(0, pos)) +
addNewline(block)
);
this.code.value = replaced + value.substr(pos);
this.code.selectionStart = replaced.length;
this.code.selectionEnd = replaced.length;
this.update(false);
}
this.code.focus();
}
@ -412,8 +397,16 @@ define([
this.errorMsg.setAttribute('class', 'msg-error');
}
value() {
if(this.code.getDoc) {
return this.code.getDoc().getValue();
} else {
return this.code.value;
}
}
update(immediate = true) {
const src = this.code.getDoc().getValue();
const src = this.value();
this.saveCode(src);
let sequence = null;
try {
@ -484,5 +477,67 @@ define([
const url = this.diagram.getSVGSynchronous();
this.downloadSVG.setAttribute('href', url);
}
_enhanceEditor() {
// Load on demand for progressive enhancement
// (failure to load external module will not block functionality)
require([
'cm/lib/codemirror',
'cm/addon/hint/show-hint',
'cm/addon/edit/trailingspace',
'cm/addon/comment/comment',
], (CodeMirror) => {
this.diagram.registerCodeMirrorMode(CodeMirror);
const code = new CodeMirror(this.code.parentNode, {
value: this.code.value,
mode: 'sequence',
globals: {
themes: this.diagram.getThemeNames(),
},
lineNumbers: true,
showTrailingSpace: true,
extraKeys: {
'Tab': (cm) => cm.execCommand('indentMore'),
'Shift-Tab': (cm) => cm.execCommand('indentLess'),
'Cmd-/': (cm) => cm.toggleComment({padding: ''}),
'Ctrl-/': (cm) => cm.toggleComment({padding: ''}),
'Ctrl-Space': 'autocomplete',
'Ctrl-Enter': 'autocomplete',
'Cmd-Enter': 'autocomplete',
},
});
this.code.parentNode.removeChild(this.code);
let lastKey = 0;
code.on('keydown', (cm, event) => {
lastKey = event.keyCode;
});
code.on('change', (cm, change) => {
this.update(false);
if(change.origin === '+input') {
if(lastKey === 13) {
lastKey = 0;
return;
}
} else if(change.origin !== 'complete') {
return;
}
CodeMirror.commands.autocomplete(cm, null, {
completeSingle: false,
});
});
code.on('cursorActivity', () => {
const from = code.getCursor('from').line;
const to = code.getCursor('to').line;
this.diagram.setHighlight(Math.min(from, to));
});
this.code = code;
});
}
};
});

View File

@ -4,6 +4,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
// Thanks, https://stackoverflow.com/a/23522755/1180785
const safari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const defaultCode = 'my default code';
let sequenceDiagram = null;
let container = null;
let ui = null;
@ -31,7 +32,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
ui = new Interface({
sequenceDiagram,
defaultCode: 'my default code',
defaultCode,
});
});
@ -41,10 +42,17 @@ defineDescribe('Interface', ['./Interface'], (Interface) => {
expect(container.appendChild).toHaveBeenCalled();
});
it('creates a code mirror instance with the given code', () => {
it('creates a code mirror instance with the given code', (done) => {
ui.build(container);
const constructorArgs = ui.code.constructor;
expect(constructorArgs.options.value).toEqual('my default code');
const check = setInterval(() => {
const constructorArgs = ui.code.constructor;
if(!constructorArgs.options) {
return;
}
clearInterval(check);
expect(constructorArgs.options.value).toEqual(defaultCode);
done();
}, 50);
});
});

View File

@ -46,19 +46,23 @@ html, body {
.pane-side {
display: inline-block;
width: 30%;
height: 100%;
}
.pane-code {
height: 70%;
}
.pane-library {
background: #EEEEEE;
user-select: none;
height: 30%;
}
.pane-view {
display: inline-block;
width: 70%;
height: 100%;
position: relative;
}
@ -94,6 +98,19 @@ html, body {
background: #EEEEEE;
}
.pane-code .editor-simple {
width: 100%;
height: 100%;
border: none;
background: #EEEEEE;
color: #222222;
font: 1em monospace;
margin: 0;
padding: 4px 8px 16px 8px;
resize: none;
white-space: pre;
}
.CodeMirror-line.error {
background: rgba(255, 0, 0, 0.2);
}