Add support for working with online rendering service when available

This commit is contained in:
David Evans 2018-04-29 15:34:53 +01:00
parent eaef4a3f47
commit 78cec2be8c
8 changed files with 577 additions and 9 deletions

View File

@ -39,7 +39,7 @@ function readEncoded(str, encoding) {
.from(decodeURIComponent(str), 'base64')
.toString('utf8');
case 'uri':
return str.split('/').map((ln) => decodeURIComponent(ln)).join('\n');
return str.split('/').map(decodeURIComponent).join('\n');
default:
throw new HttpError(400, 'Unknown encoding');
}

View File

@ -9,6 +9,7 @@
https://cdnjs.cloudflare.com
'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
;
connect-src 'self';
font-src 'self' data:;
img-src 'self' blob:;
form-action 'none';

View File

@ -10065,6 +10065,7 @@
this.parentNode = null;
this.childNodes = [];
this.attributes = new Map();
this.style = {};
this.listeners = new Map();
}

View File

@ -58,6 +58,7 @@ class ElementNode {
this.parentNode = null;
this.childNodes = [];
this.attributes = new Map();
this.style = {};
this.listeners = new Map();
}

View File

@ -265,7 +265,6 @@ html, body {
.options {
position: absolute;
background: #FFFFFF;
overflow: hidden;
user-select: none;
z-index: 30;
}
@ -284,6 +283,8 @@ html, body {
border-top-left-radius: 5px;
border-top: 1px solid #EEEEEE;
border-left: 1px solid #EEEEEE;
transition: 0.2s ease;
transition-property: box-shadow;
}
.options a {
@ -305,3 +306,85 @@ html, body {
background: #EEEEEE;
color: #6666CC;
}
.urlbuilder {
border-top: 1px solid #EEEEEE;
overflow: auto;
box-sizing: border-box;
transition: 0.2s ease;
transition-property: height, width, padding;
font-size: 0.8em;
text-align: center;
position: relative;
}
.urlbuilder .message {
color: #666666;
font-size: 1.5em;
padding-top: 30px;
}
.urlbuilder .config {
padding-top: 10px;
}
.urlbuilder input[type=number] {
width: 60px;
text-align: right;
}
.urlbuilder .or {
display: block;
margin: 10px 0;
color: #333333;
font-size: 1.2em;
}
.urlbuilder .output {
display: block;
padding: 6px;
height: 30px;
border: 1px solid #999999;
border-right: none;
font-size: 1em;
position: absolute;
bottom: 10px;
left: 10px;
width: calc(100% - 50px);
box-sizing: border-box;
}
.urlbuilder .copy {
display: block;
width: 30px;
height: 30px;
line-height: 28px;
padding-top: 1px;
border: 1px solid #999999;
background: #FFFFFF;
font-size: 1em;
position: absolute;
bottom: 10px;
right: 10px;
box-sizing: border-box;
}
.urlbuilder .copy:active {
background: #EEEEEE;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
padding-top: 2px;
}
.urlbuilder .copied {
display: none;
height: 30px;
border: 1px solid #999999;
font-size: 1em;
position: absolute;
bottom: 10px;
left: 10px;
width: calc(100% - 20px);
line-height: 28px;
background: #99EE99;
box-sizing: border-box;
}

View File

@ -1,3 +1,5 @@
/* eslint-disable max-lines */
import DOMWrapper from '../../scripts/core/DOMWrapper.mjs';
const DELAY_AGENTCHANGE = 500;
@ -36,6 +38,54 @@ function simplifyPreview(code) {
.replace(/[{}]/g, '');
}
function toCappedFixed(v, cap) {
const s = v.toString();
const p = s.indexOf('.');
if(p === -1 || s.length - p - 1 <= cap) {
return s;
}
return v.toFixed(cap);
}
function fetchResource(path) {
if(typeof fetch === 'undefined') {
return Promise.reject(new Error());
}
return fetch(path)
.then((response) => {
if(!response.ok) {
throw new Error(response.statusText);
}
return response;
});
}
/* eslint-disable complexity */
function makeURL(code, {height, width, zoom}) {
/* eslint-enable complexity */
const uri = code
.split('\n')
.map(encodeURIComponent)
.filter((ln) => ln !== '')
.join('/');
let opts = '';
if(!Number.isNaN(width) || !Number.isNaN(height)) {
if(!Number.isNaN(width)) {
opts += 'w' + toCappedFixed(Math.max(width, 0), 4);
}
if(!Number.isNaN(height)) {
opts += 'h' + toCappedFixed(Math.max(height, 0), 4);
}
opts += '/';
} else if(!Number.isNaN(zoom) && zoom !== 1) {
opts += 'z' + toCappedFixed(Math.max(zoom, 0), 4);
opts += '/';
}
return opts + uri + '.svg';
}
function makeSplit(require, nodes, options) {
// Load on demand for progressive enhancement
// (failure to load external module will not block functionality)
@ -142,6 +192,7 @@ export default class Interface {
this._downloadSVGClick = this._downloadSVGClick.bind(this);
this._downloadPNGClick = this._downloadPNGClick.bind(this);
this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
this._downloadURLClick = this._downloadURLClick.bind(this);
this._showDropStyle = this._showDropStyle.bind(this);
this._hideDropStyle = this._hideDropStyle.bind(this);
@ -189,12 +240,169 @@ export default class Interface {
);
this.code.focus();
}
this._hideURLBuilder();
})
.on('dblclick', (element) => {
this.diagram.toggleCollapsed(element.ln);
this._hideURLBuilder();
});
}
buildURLBuilder() {
const copied = this.dom.el('div').setClass('copied')
.add('Copied to Clipboard');
this.urlOutput = this.dom.el('input').setClass('output')
.attr('readonly', 'readonly')
.on('focus', () => {
this.urlOutput.select(0, this.urlOutput.element.value.length);
});
const copy = this.dom.el('button').setClass('copy')
.add('\uD83D\uDCCB')
.attr('title', 'Copy to clipboard')
.on('click', () => {
this.urlOutput
.focus()
.select(0, this.urlOutput.element.value.length)
.element.ownerDocument.execCommand('copy');
copy.focus();
copied.styles({
'display': 'block',
'opacity': 1,
'transition': 'none',
});
setTimeout(() => copied.styles({
'opacity': 0,
'transition': 'opacity 0.5s linear',
}), 1000);
setTimeout(() => copied.styles({'display': 'none'}), 1500);
});
this.urlWidth = this.dom.el('input').attrs({
'min': 0,
'placeholder': 'auto',
'step': 'any',
'type': 'number',
}).on('input', () => {
this.urlZoom.val('1');
this._refreshURL();
});
this.urlHeight = this.dom.el('input').attrs({
'min': 0,
'placeholder': 'auto',
'step': 'any',
'type': 'number',
}).on('input', () => {
this.urlZoom.val('1');
this._refreshURL();
});
this.urlZoom = this.dom.el('input').attrs({
'min': 0,
'step': 'any',
'type': 'number',
'value': 1,
}).on('input', () => {
this.urlWidth.val('');
this.urlHeight.val('');
this._refreshURL();
});
const urlOpts = this.dom.el('div').setClass('config').add(
this.dom.el('label').add('width ', this.urlWidth),
', ',
this.dom.el('label').add('height ', this.urlHeight),
this.dom.el('span').setClass('or').add('or'),
this.dom.el('label').add('zoom ', this.urlZoom),
this.urlOutput,
copy,
copied
);
this.urlBuilder = this.dom.el('div').setClass('urlbuilder')
.styles({'display': 'none'})
.add(
this.dom.el('div').setClass('message')
.add('Loading\u2026')
);
this.renderService = '';
const relativePath = 'render/';
fetchResource(relativePath)
.then((response) => response.text())
.then((content) => {
let path = content.trim();
if(!path || path.startsWith('<svg')) {
path = relativePath;
}
this.renderService = new URL(path, window.location.href).href;
this.urlBuilder.empty().add(urlOpts);
this._refreshURL();
})
.catch(() => {
this.urlBuilder.empty().add(
this.dom.el('div').setClass('message')
.add('No online rendering service available.')
);
});
return this.urlBuilder;
}
_refreshURL() {
this.urlOutput.val(this.renderService + makeURL(this.value(), {
height: Number.parseFloat(this.urlHeight.element.value),
width: Number.parseFloat(this.urlWidth.element.value),
zoom: Number.parseFloat(this.urlZoom.element.value || '1'),
}));
}
_showURLBuilder() {
if(this.builderVisible) {
return;
}
this.builderVisible = true;
this.urlBuilder.styles({
'display': 'block',
'height': '0px',
'padding': '0px',
'width': this.optsHold.element.clientWidth + 'px',
});
clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => {
this.urlBuilder.styles({
'height': '150px',
'padding': '10px',
'width': '400px',
});
this.optsHold.styles({
'box-shadow': '10px 10px 25px 12px rgba(0,0,0,0.3)',
});
}, 0);
this._refreshURL();
}
_hideURLBuilder() {
if(!this.builderVisible) {
return;
}
this.builderVisible = false;
this.urlBuilder.styles({
'height': '0px',
'padding': '0px',
'width': '0px',
});
this.optsHold.styles({
'box-shadow': 'none',
});
clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => {
this.urlBuilder.styles({'display': 'none'});
}, 200);
}
buildOptionsDownloads() {
this.downloadPNG = this.dom.el('a')
.text('Download PNG')
@ -213,8 +421,19 @@ export default class Interface {
})
.on('click', this._downloadSVGClick);
return this.dom.el('div').setClass('options downloads')
.add(this.downloadPNG, this.downloadSVG);
this.downloadURL = this.dom.el('a')
.text('URL')
.attrs({'href': '#'})
.on('click', this._downloadURLClick);
this.optsHold = this.dom.el('div').setClass('options downloads').add(
this.downloadPNG,
this.downloadSVG,
this.downloadURL,
this.buildURLBuilder()
);
return this.optsHold;
}
buildLibrary(container) {
@ -250,7 +469,8 @@ export default class Interface {
buildViewPane() {
this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner')
.add(this.diagram.dom());
.add(this.diagram.dom())
.on('click', () => this._hideURLBuilder());
this.errorMsg = this.dom.el('div').setClass('msg-error');
@ -336,6 +556,14 @@ export default class Interface {
snapOffset: 70,
});
if(typeof window !== 'undefined') {
window.addEventListener('keydown', (e) => {
if(e.keyCode === 27) {
this._hideURLBuilder();
}
});
}
// Delay first update 1 frame to ensure render target is ready
// (prevents initial incorrect font calculations for custom fonts)
setTimeout(this.update.bind(this), 0);
@ -463,6 +691,7 @@ export default class Interface {
}
update(immediate = true) {
this._hideURLBuilder();
const src = this.value();
this.saveCode(src);
let sequence = null;
@ -529,12 +758,24 @@ export default class Interface {
} else if(this.updatePNGLink()) {
e.preventDefault();
}
this._hideURLBuilder();
}
_downloadSVGClick() {
this.forceRender();
const url = this.diagram.getSVGSynchronous();
this.downloadSVG.attr('href', url);
this._hideURLBuilder();
}
_downloadURLClick(e) {
e.preventDefault();
if(this.builderVisible) {
this._hideURLBuilder();
} else {
this._showURLBuilder();
}
}
_enhanceEditor() {

View File

@ -504,6 +504,8 @@
}
}
/* eslint-disable max-lines */
const DELAY_AGENTCHANGE = 500;
const DELAY_STAGECHANGE = 250;
const PNG_RESOLUTION = 4;
@ -540,6 +542,54 @@
.replace(/[{}]/g, '');
}
function toCappedFixed(v, cap) {
const s = v.toString();
const p = s.indexOf('.');
if(p === -1 || s.length - p - 1 <= cap) {
return s;
}
return v.toFixed(cap);
}
function fetchResource(path) {
if(typeof fetch === 'undefined') {
return Promise.reject(new Error());
}
return fetch(path)
.then((response) => {
if(!response.ok) {
throw new Error(response.statusText);
}
return response;
});
}
/* eslint-disable complexity */
function makeURL(code, {height, width, zoom}) {
/* eslint-enable complexity */
const uri = code
.split('\n')
.map(encodeURIComponent)
.filter((ln) => ln !== '')
.join('/');
let opts = '';
if(!Number.isNaN(width) || !Number.isNaN(height)) {
if(!Number.isNaN(width)) {
opts += 'w' + toCappedFixed(Math.max(width, 0), 4);
}
if(!Number.isNaN(height)) {
opts += 'h' + toCappedFixed(Math.max(height, 0), 4);
}
opts += '/';
} else if(!Number.isNaN(zoom) && zoom !== 1) {
opts += 'z' + toCappedFixed(Math.max(zoom, 0), 4);
opts += '/';
}
return opts + uri + '.svg';
}
function makeSplit(require, nodes, options) {
// Load on demand for progressive enhancement
// (failure to load external module will not block functionality)
@ -646,6 +696,7 @@
this._downloadSVGClick = this._downloadSVGClick.bind(this);
this._downloadPNGClick = this._downloadPNGClick.bind(this);
this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
this._downloadURLClick = this._downloadURLClick.bind(this);
this._showDropStyle = this._showDropStyle.bind(this);
this._hideDropStyle = this._hideDropStyle.bind(this);
@ -693,12 +744,169 @@
);
this.code.focus();
}
this._hideURLBuilder();
})
.on('dblclick', (element) => {
this.diagram.toggleCollapsed(element.ln);
this._hideURLBuilder();
});
}
buildURLBuilder() {
const copied = this.dom.el('div').setClass('copied')
.add('Copied to Clipboard');
this.urlOutput = this.dom.el('input').setClass('output')
.attr('readonly', 'readonly')
.on('focus', () => {
this.urlOutput.select(0, this.urlOutput.element.value.length);
});
const copy = this.dom.el('button').setClass('copy')
.add('\uD83D\uDCCB')
.attr('title', 'Copy to clipboard')
.on('click', () => {
this.urlOutput
.focus()
.select(0, this.urlOutput.element.value.length)
.element.ownerDocument.execCommand('copy');
copy.focus();
copied.styles({
'display': 'block',
'opacity': 1,
'transition': 'none',
});
setTimeout(() => copied.styles({
'opacity': 0,
'transition': 'opacity 0.5s linear',
}), 1000);
setTimeout(() => copied.styles({'display': 'none'}), 1500);
});
this.urlWidth = this.dom.el('input').attrs({
'min': 0,
'placeholder': 'auto',
'step': 'any',
'type': 'number',
}).on('input', () => {
this.urlZoom.val('1');
this._refreshURL();
});
this.urlHeight = this.dom.el('input').attrs({
'min': 0,
'placeholder': 'auto',
'step': 'any',
'type': 'number',
}).on('input', () => {
this.urlZoom.val('1');
this._refreshURL();
});
this.urlZoom = this.dom.el('input').attrs({
'min': 0,
'step': 'any',
'type': 'number',
'value': 1,
}).on('input', () => {
this.urlWidth.val('');
this.urlHeight.val('');
this._refreshURL();
});
const urlOpts = this.dom.el('div').setClass('config').add(
this.dom.el('label').add('width ', this.urlWidth),
', ',
this.dom.el('label').add('height ', this.urlHeight),
this.dom.el('span').setClass('or').add('or'),
this.dom.el('label').add('zoom ', this.urlZoom),
this.urlOutput,
copy,
copied
);
this.urlBuilder = this.dom.el('div').setClass('urlbuilder')
.styles({'display': 'none'})
.add(
this.dom.el('div').setClass('message')
.add('Loading\u2026')
);
this.renderService = '';
const relativePath = 'render/';
fetchResource(relativePath)
.then((response) => response.text())
.then((content) => {
let path = content.trim();
if(!path || path.startsWith('<svg')) {
path = relativePath;
}
this.renderService = new URL(path, window.location.href).href;
this.urlBuilder.empty().add(urlOpts);
this._refreshURL();
})
.catch(() => {
this.urlBuilder.empty().add(
this.dom.el('div').setClass('message')
.add('No online rendering service available.')
);
});
return this.urlBuilder;
}
_refreshURL() {
this.urlOutput.val(this.renderService + makeURL(this.value(), {
height: Number.parseFloat(this.urlHeight.element.value),
width: Number.parseFloat(this.urlWidth.element.value),
zoom: Number.parseFloat(this.urlZoom.element.value || '1'),
}));
}
_showURLBuilder() {
if(this.builderVisible) {
return;
}
this.builderVisible = true;
this.urlBuilder.styles({
'display': 'block',
'height': '0px',
'padding': '0px',
'width': this.optsHold.element.clientWidth + 'px',
});
clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => {
this.urlBuilder.styles({
'height': '150px',
'padding': '10px',
'width': '400px',
});
this.optsHold.styles({
'box-shadow': '10px 10px 25px 12px rgba(0,0,0,0.3)',
});
}, 0);
this._refreshURL();
}
_hideURLBuilder() {
if(!this.builderVisible) {
return;
}
this.builderVisible = false;
this.urlBuilder.styles({
'height': '0px',
'padding': '0px',
'width': '0px',
});
this.optsHold.styles({
'box-shadow': 'none',
});
clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => {
this.urlBuilder.styles({'display': 'none'});
}, 200);
}
buildOptionsDownloads() {
this.downloadPNG = this.dom.el('a')
.text('Download PNG')
@ -717,8 +925,19 @@
})
.on('click', this._downloadSVGClick);
return this.dom.el('div').setClass('options downloads')
.add(this.downloadPNG, this.downloadSVG);
this.downloadURL = this.dom.el('a')
.text('URL')
.attrs({'href': '#'})
.on('click', this._downloadURLClick);
this.optsHold = this.dom.el('div').setClass('options downloads').add(
this.downloadPNG,
this.downloadSVG,
this.downloadURL,
this.buildURLBuilder()
);
return this.optsHold;
}
buildLibrary(container) {
@ -754,7 +973,8 @@
buildViewPane() {
this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner')
.add(this.diagram.dom());
.add(this.diagram.dom())
.on('click', () => this._hideURLBuilder());
this.errorMsg = this.dom.el('div').setClass('msg-error');
@ -840,6 +1060,14 @@
snapOffset: 70,
});
if(typeof window !== 'undefined') {
window.addEventListener('keydown', (e) => {
if(e.keyCode === 27) {
this._hideURLBuilder();
}
});
}
// Delay first update 1 frame to ensure render target is ready
// (prevents initial incorrect font calculations for custom fonts)
setTimeout(this.update.bind(this), 0);
@ -967,6 +1195,7 @@
}
update(immediate = true) {
this._hideURLBuilder();
const src = this.value();
this.saveCode(src);
let sequence = null;
@ -1033,12 +1262,24 @@
} else if(this.updatePNGLink()) {
e.preventDefault();
}
this._hideURLBuilder();
}
_downloadSVGClick() {
this.forceRender();
const url = this.diagram.getSVGSynchronous();
this.downloadSVG.attr('href', url);
this._hideURLBuilder();
}
_downloadURLClick(e) {
e.preventDefault();
if(this.builderVisible) {
this._hideURLBuilder();
} else {
this._showURLBuilder();
}
}
_enhanceEditor() {

File diff suppressed because one or more lines are too long