Improve editor and library pages on iOS

This commit is contained in:
David Evans 2018-05-07 20:27:51 +01:00
parent 80175c65f5
commit 135f0b1e0d
12 changed files with 776 additions and 224 deletions

View File

@ -14,6 +14,9 @@
img-src 'self' blob:; img-src 'self' blob:;
form-action 'none'; form-action 'none';
"> ">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, minimal-ui">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
<title>Sequence Diagram</title> <title>Sequence Diagram</title>
<link rel="icon" href="web/resources/favicon.png"> <link rel="icon" href="web/resources/favicon.png">
@ -89,8 +92,8 @@
<p class="loadmsg">Loading&hellip;</p> <p class="loadmsg">Loading&hellip;</p>
<noscript><p class="noscript">This tool requires Javascript!<p></noscript> <noscript><p class="noscript">This tool requires Javascript!<p></noscript>
<nav> <nav>
<a href="library.htm" target="_blank">Library</a> <a href="library.htm" target="_blank" data-touch="API">Library</a>
<a href="https://github.com/davidje13/SequenceDiagram" target="_blank">GitHub</a> <a href="https://github.com/davidje13/SequenceDiagram" target="_blank" data-touch="Git">GitHub</a>
</nav> </nav>
</div> </div>

View File

@ -6984,6 +6984,8 @@
} }
} }
DOMWrapper.WrappedElement = WrappedElement;
function merge(state, newState) { function merge(state, newState) {
for(const k in state) { for(const k in state) {
if(Object.prototype.hasOwnProperty.call(state, k)) { if(Object.prototype.hasOwnProperty.call(state, k)) {

File diff suppressed because one or more lines are too long

View File

@ -6984,6 +6984,8 @@
} }
} }
DOMWrapper.WrappedElement = WrappedElement;
function merge(state, newState) { function merge(state, newState) {
for(const k in state) { for(const k in state) {
if(Object.prototype.hasOwnProperty.call(state, k)) { if(Object.prototype.hasOwnProperty.call(state, k)) {

View File

@ -14,6 +14,7 @@
img-src 'self'; img-src 'self';
form-action 'none'; form-action 'none';
"> ">
<meta name="viewport" content="initial-scale=1.0, minimum-scale=1.0">
<title>Sequence Diagram Library</title> <title>Sequence Diagram Library</title>
<link rel="icon" href="web/resources/favicon.png"> <link rel="icon" href="web/resources/favicon.png">

View File

@ -204,3 +204,5 @@ export default class DOMWrapper {
return this.document.createTextNode(content); return this.document.createTextNode(content);
} }
} }
DOMWrapper.WrappedElement = WrappedElement;

View File

@ -550,6 +550,8 @@
} }
} }
DOMWrapper.WrappedElement = WrappedElement;
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const DELAY_AGENTCHANGE = 500; const DELAY_AGENTCHANGE = 500;
@ -636,14 +638,57 @@
return opts + uri + '.svg'; return opts + uri + '.svg';
} }
function makeSplit(require, nodes, options) { function makeSplit(nodes, options) {
const filteredNodes = [];
const filteredOpts = {
direction: options.direction,
minSize: [],
sizes: [],
snapOffset: options.snapOffset,
};
let total = 0;
for(let i = 0; i < nodes.length; ++ i) {
if(nodes[i]) {
filteredNodes.push(nodes[i]);
filteredOpts.minSize.push(options.minSize[i]);
filteredOpts.sizes.push(options.sizes[i]);
total += options.sizes[i];
}
}
for(let i = 0; i < filteredNodes.length; ++ i) {
filteredOpts.minSize[i] *= 100 / total;
filteredOpts.sizes[i] *= 100 / total;
const percent = filteredOpts.sizes[i] + '%';
if(filteredOpts.direction === 'vertical') {
nodes[i].styles({
boxSizing: 'border-box',
height: percent,
width: '100%',
});
} else {
nodes[i].styles({
boxSizing: 'border-box',
display: 'inline-block',
height: '100%',
verticalAlign: 'top', // Safari fix
width: percent,
});
}
}
if(filteredNodes.length < 2) {
return;
}
// Load on demand for progressive enhancement // Load on demand for progressive enhancement
// (failure to load external module will not block functionality) // (failure to load external module will not block functionality)
require(['split'], (Split) => { options.require(['split'], (Split) => {
// Patches for: // Patches for:
// https://github.com/nathancahill/Split.js/issues/97 // https://github.com/nathancahill/Split.js/issues/97
// https://github.com/nathancahill/Split.js/issues/111 // https://github.com/nathancahill/Split.js/issues/111
const parent = nodes[0].parentNode; const parent = nodes[0].element.parentNode;
const oldAEL = parent.addEventListener; const oldAEL = parent.addEventListener;
const oldREL = parent.removeEventListener; const oldREL = parent.removeEventListener;
parent.addEventListener = (event, callback) => { parent.addEventListener = (event, callback) => {
@ -662,9 +707,13 @@
}; };
let oldCursor = null; let oldCursor = null;
const resolvedOptions = Object.assign({ const cursor = (filteredOpts.direction === 'vertical') ?
cursor: (options.direction === 'vertical') ? 'row-resize' : 'col-resize';
'row-resize' : 'col-resize',
return new Split(
filteredNodes.map((node) => node.element),
Object.assign({
cursor,
direction: 'vertical', direction: 'vertical',
gutterSize: 0, gutterSize: 0,
onDragEnd: () => { onDragEnd: () => {
@ -673,14 +722,19 @@
}, },
onDragStart: () => { onDragStart: () => {
oldCursor = document.body.style.cursor; oldCursor = document.body.style.cursor;
document.body.style.cursor = resolvedOptions.cursor; document.body.style.cursor = cursor;
}, },
}, options); }, filteredOpts)
);
return new Split(nodes, resolvedOptions);
}); });
} }
DOMWrapper.WrappedElement.prototype.split = function(nodes, options) {
this.add(nodes);
makeSplit(nodes, options);
return this;
};
function hasDroppedFile(event, mime) { function hasDroppedFile(event, mime) {
if(!event.dataTransfer.items && event.dataTransfer.files.length === 0) { if(!event.dataTransfer.items && event.dataTransfer.files.length === 0) {
// Work around Safari not supporting dataTransfer.items // Work around Safari not supporting dataTransfer.items
@ -714,6 +768,35 @@
}); });
} }
DOMWrapper.WrappedElement.prototype.fastClick = function() {
const pt = {x: -1, y: 0};
return this
.on('touchstart', (e) => {
const [touch] = e.touches;
pt.x = touch.pageX;
pt.y = touch.pageY;
})
.on('touchend', (e) => {
if(
pt.x === -1 ||
e.touches.length !== 0 ||
e.changedTouches.length !== 1
) {
pt.x = -1;
return;
}
const [touch] = e.changedTouches;
if(
Math.abs(pt.x - touch.pageX) < 10 &&
Math.abs(pt.y - touch.pageY) < 10
) {
e.preventDefault();
e.target.click();
}
pt.x = -1;
});
};
class Interface { class Interface {
constructor({ constructor({
sequenceDiagram, sequenceDiagram,
@ -722,6 +805,7 @@
library = [], library = [],
links = [], links = [],
require = null, require = null,
touchUI = false,
}) { }) {
this.diagram = sequenceDiagram; this.diagram = sequenceDiagram;
this.defaultCode = defaultCode; this.defaultCode = defaultCode;
@ -730,6 +814,7 @@
this.links = links; this.links = links;
this.minScale = 1.5; this.minScale = 1.5;
this.require = require || (() => null); this.require = require || (() => null);
this.touchUI = touchUI;
this.debounced = null; this.debounced = null;
this.latestSeq = null; this.latestSeq = null;
@ -743,7 +828,6 @@
this._downloadPNGClick = this._downloadPNGClick.bind(this); this._downloadPNGClick = this._downloadPNGClick.bind(this);
this._downloadPNGFocus = this._downloadPNGFocus.bind(this); this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
this._downloadURLClick = this._downloadURLClick.bind(this); this._downloadURLClick = this._downloadURLClick.bind(this);
this._showDropStyle = this._showDropStyle.bind(this);
this._hideDropStyle = this._hideDropStyle.bind(this); this._hideDropStyle = this._hideDropStyle.bind(this);
this.diagram this.diagram
@ -808,14 +892,21 @@
}); });
const copy = this.dom.el('button').setClass('copy') const copy = this.dom.el('button').setClass('copy')
.add('\uD83D\uDCCB')
.attr('title', 'Copy to clipboard') .attr('title', 'Copy to clipboard')
.fastClick()
.on('click', () => { .on('click', () => {
if(this.touchUI) {
this.urlOutput.styles({display: 'block'});
}
this.urlOutput this.urlOutput
.focus() .focus()
.select(0, this.urlOutput.element.value.length) .select(0, this.urlOutput.element.value.length)
.element.ownerDocument.execCommand('copy'); .element.ownerDocument.execCommand('copy');
copy.focus(); copy.focus();
this.container.delClass('keyinput');
if(this.touchUI) {
this.urlOutput.styles({display: 'none'});
}
copied.styles({ copied.styles({
'display': 'block', 'display': 'block',
'opacity': 1, 'opacity': 1,
@ -870,7 +961,7 @@
copied copied
); );
this.urlBuilder = this.dom.el('div').setClass('urlbuilder') const urlBuilder = this.dom.el('div').setClass('urlbuilder')
.styles({'display': 'none'}) .styles({'display': 'none'})
.add( .add(
this.dom.el('div').setClass('message') this.dom.el('div').setClass('message')
@ -887,17 +978,17 @@
path = relativePath; path = relativePath;
} }
this.renderService = new URL(path, window.location.href).href; this.renderService = new URL(path, window.location.href).href;
this.urlBuilder.empty().add(urlOpts); urlBuilder.empty().add(urlOpts);
this._refreshURL(); this._refreshURL();
}) })
.catch(() => { .catch(() => {
this.urlBuilder.empty().add( urlBuilder.empty().add(
this.dom.el('div').setClass('message') this.dom.el('div').setClass('message')
.add('No online rendering service available.') .add('No online rendering service available.')
); );
}); });
return this.urlBuilder; return urlBuilder;
} }
_refreshURL() { _refreshURL() {
@ -913,14 +1004,24 @@
return; return;
} }
this.builderVisible = true; this.builderVisible = true;
if(this.touchUI) {
this.urlBuilder.styles({
'bottom': '-210px',
'display': 'block',
});
} else {
this.urlBuilder.styles({ this.urlBuilder.styles({
'display': 'block', 'display': 'block',
'height': '0px', 'height': '0px',
'padding': '0px', 'padding': '0px',
'width': this.optsHold.element.clientWidth + 'px', 'width': this.optsHold.element.clientWidth + 'px',
}); });
}
clearTimeout(this.builderTm); clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => { this.builderTm = setTimeout(() => {
if(this.touchUI) {
this.urlBuilder.styles({'bottom': 0});
} else {
this.urlBuilder.styles({ this.urlBuilder.styles({
'height': '150px', 'height': '150px',
'padding': '10px', 'padding': '10px',
@ -929,6 +1030,7 @@
this.optsHold.styles({ this.optsHold.styles({
'box-shadow': '10px 10px 25px 12px rgba(0,0,0,0.3)', 'box-shadow': '10px 10px 25px 12px rgba(0,0,0,0.3)',
}); });
}
}, 0); }, 0);
this._refreshURL(); this._refreshURL();
@ -939,6 +1041,11 @@
return; return;
} }
this.builderVisible = false; this.builderVisible = false;
if(this.touchUI) {
this.urlBuilder.styles({
'bottom': (-this.urlBuilder.element.clientHeight - 60) + 'px',
});
} else {
this.urlBuilder.styles({ this.urlBuilder.styles({
'height': '0px', 'height': '0px',
'padding': '0px', 'padding': '0px',
@ -947,6 +1054,8 @@
this.optsHold.styles({ this.optsHold.styles({
'box-shadow': 'none', 'box-shadow': 'none',
}); });
}
this.container.delClass('keyinput');
clearTimeout(this.builderTm); clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => { this.builderTm = setTimeout(() => {
this.urlBuilder.styles({'display': 'none'}); this.urlBuilder.styles({'display': 'none'});
@ -955,12 +1064,14 @@
buildOptionsDownloads() { buildOptionsDownloads() {
this.downloadPNG = this.dom.el('a') this.downloadPNG = this.dom.el('a')
.text('Download PNG') .text('Export PNG')
.attrs({ .attrs({
'download': 'SequenceDiagram.png', 'download': 'SequenceDiagram.png',
'href': '#', 'href': '#',
}) })
.on(['focus', 'mouseover', 'mousedown'], this._downloadPNGFocus) .on(['focus', 'mouseover', 'mousedown'], this._downloadPNGFocus)
// Exploit delay between touchend and click on mobile
.on('touchend', this._downloadPNGFocus)
.on('click', this._downloadPNGClick); .on('click', this._downloadPNGClick);
this.downloadSVG = this.dom.el('a') this.downloadSVG = this.dom.el('a')
@ -969,18 +1080,22 @@
'download': 'SequenceDiagram.svg', 'download': 'SequenceDiagram.svg',
'href': '#', 'href': '#',
}) })
.fastClick()
.on('click', this._downloadSVGClick); .on('click', this._downloadSVGClick);
this.downloadURL = this.dom.el('a') this.downloadURL = this.dom.el('a')
.text('URL') .text('URL')
.attrs({'href': '#'}) .attrs({'href': '#'})
.fastClick()
.on('click', this._downloadURLClick); .on('click', this._downloadURLClick);
this.urlBuilder = this.buildURLBuilder();
this.optsHold = this.dom.el('div').setClass('options downloads').add( this.optsHold = this.dom.el('div').setClass('options downloads').add(
this.downloadPNG, this.downloadPNG,
this.downloadSVG, this.downloadSVG,
this.downloadURL, this.downloadURL,
this.buildURLBuilder() this.urlBuilder
); );
return this.optsHold; return this.optsHold;
@ -994,6 +1109,7 @@
const hold = this.dom.el('div') const hold = this.dom.el('div')
.setClass('library-item') .setClass('library-item')
.add(holdInner) .add(holdInner)
.fastClick()
.on('click', this.addCodeBlock.bind(this, lib.code)) .on('click', this.addCodeBlock.bind(this, lib.code))
.attach(container); .attach(container);
@ -1017,10 +1133,33 @@
return container; return container;
} }
buildCodePane() {
this.code = this.dom.el('textarea')
.setClass('editor-simple')
.val(this.loadCode() || this.defaultCode)
.on('input', () => this.update(false));
return this.dom.el('div').setClass('pane-code')
.add(this.code);
}
buildLibPane() {
if(this.library.length === 0) {
return null;
}
return this.dom.el('div').setClass('pane-library')
.add(this.dom.el('div').setClass('pane-library-scroller')
.add(this.buildLibrary(
this.dom.el('div').setClass('pane-library-inner')
)));
}
buildViewPane() { buildViewPane() {
this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner') this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner')
.add(this.diagram.dom()) .add(this.diagram.dom())
.on('click', () => this._hideURLBuilder()); .on('touchstart', () => this._hideURLBuilder())
.on('mousedown', () => this._hideURLBuilder());
this.errorMsg = this.dom.el('div').setClass('msg-error'); this.errorMsg = this.dom.el('div').setClass('msg-error');
@ -1032,53 +1171,10 @@
); );
} }
buildLeftPanes() {
const container = this.dom.el('div').setClass('pane-side');
this.code = this.dom.el('textarea')
.setClass('editor-simple')
.val(this.loadCode() || this.defaultCode)
.on('input', () => this.update(false));
const codePane = this.dom.el('div').setClass('pane-code')
.add(this.code)
.attach(container);
if(this.library.length > 0) {
const libPane = this.dom.el('div').setClass('pane-library')
.add(this.dom.el('div').setClass('pane-library-scroller')
.add(this.buildLibrary(
this.dom.el('div').setClass('pane-library-inner')
)))
.attach(container);
makeSplit(this.require, [codePane.element, libPane.element], {
direction: 'vertical',
minSize: [100, 5],
sizes: [70, 30],
snapOffset: 5,
});
}
return container;
}
build(container) { build(container) {
this.dom = new DOMWrapper(container.ownerDocument); this.dom = new DOMWrapper(container.ownerDocument);
const lPane = this.buildLeftPanes();
const viewPane = this.buildViewPane();
this.container = this.dom.wrap(container) this.container = this.dom.wrap(container)
.add(this.dom.el('div').setClass('pane-hold')
.add(
lPane,
viewPane,
this.dom.el('div').setClass('options links')
.add(this.links.map((link) => this.dom.el('a')
.attrs({'href': link.href, 'target': '_blank'})
.text(link.label))),
this.buildOptionsDownloads()
))
.on('dragover', (event) => { .on('dragover', (event) => {
event.preventDefault(); event.preventDefault();
if(hasDroppedFile(event, 'image/svg+xml')) { if(hasDroppedFile(event, 'image/svg+xml')) {
@ -1097,14 +1193,66 @@
if(file) { if(file) {
this.loadFile(file); this.loadFile(file);
} }
}); })
.on('focusin', () => this.container.addClass('keyinput'))
.on('focusout', () => this.container.delClass('keyinput'));
makeSplit(this.require, [lPane.element, viewPane.element], { const codePane = this.buildCodePane();
const libPane = this.buildLibPane();
const viewPane = this.buildViewPane();
const links = this.links.map((link) => this.dom.el('a')
.attrs({'href': link.href, 'target': '_blank'})
.text(this.touchUI ? link.touchLabel : link.label));
if(this.touchUI) {
this.buildOptionsDownloads();
this.container
.addClass('touch')
.add(
this.dom.el('div').setClass('pane-hold')
.split([viewPane, codePane], {
direction: 'vertical',
minSize: [10, 10],
require: this.require,
sizes: [80, 20],
snapOffset: 20,
}),
libPane.styles({'display': 'none', 'top': '100%'}),
this.urlBuilder,
this.dom.el('div').setClass('optbar')
.add(
...links,
this.downloadPNG.text('PNG'),
this.downloadSVG.text('SVG'),
this.downloadURL.text('URL')
)
);
} else {
this.container
.add(
this.dom.el('div').setClass('pane-hold')
.split([
this.dom.el('div').setClass('pane-side')
.split([codePane, libPane], {
direction: 'vertical',
minSize: [100, 5],
require: this.require,
sizes: [70, 30],
snapOffset: 5,
}),
viewPane,
], {
direction: 'horizontal', direction: 'horizontal',
minSize: [10, 10], minSize: [10, 10],
require: this.require,
sizes: [30, 70], sizes: [30, 70],
snapOffset: 70, snapOffset: 70,
}); }),
this.dom.el('div').setClass('options links').add(links),
this.buildOptionsDownloads()
);
}
if(typeof window !== 'undefined') { if(typeof window !== 'undefined') {
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
@ -1389,6 +1537,8 @@
}); });
}); });
code.on('focus', () => this._hideURLBuilder());
code.on('cursorActivity', () => { code.on('cursorActivity', () => {
const from = code.getCursor('from').line; const from = code.getCursor('from').line;
const to = code.getCursor('to').line; const to = code.getCursor('to').line;
@ -1468,9 +1618,11 @@
const linkElements = nav.getElementsByTagName('a'); const linkElements = nav.getElementsByTagName('a');
const links = []; const links = [];
for(let i = 0; i < linkElements.length; ++ i) { for(let i = 0; i < linkElements.length; ++ i) {
const element = linkElements[i];
links.push({ links.push({
href: linkElements[i].getAttribute('href'), href: element.getAttribute('href'),
label: linkElements[i].textContent, label: element.textContent,
touchLabel: element.dataset.touch,
}); });
} }
@ -1481,6 +1633,7 @@
localStorage: 'src', localStorage: 'src',
require, require,
sequenceDiagram: new SequenceDiagram(), sequenceDiagram: new SequenceDiagram(),
touchUI: ('ontouchstart' in window),
}); });
loader.parentNode.removeChild(loader); loader.parentNode.removeChild(loader);
ui.build(window.document.body); ui.build(window.document.body);

File diff suppressed because one or more lines are too long

View File

@ -28,9 +28,11 @@ window.addEventListener('load', () => {
const linkElements = nav.getElementsByTagName('a'); const linkElements = nav.getElementsByTagName('a');
const links = []; const links = [];
for(let i = 0; i < linkElements.length; ++ i) { for(let i = 0; i < linkElements.length; ++ i) {
const element = linkElements[i];
links.push({ links.push({
href: linkElements[i].getAttribute('href'), href: element.getAttribute('href'),
label: linkElements[i].textContent, label: element.textContent,
touchLabel: element.dataset.touch,
}); });
} }
@ -41,6 +43,7 @@ window.addEventListener('load', () => {
localStorage: 'src', localStorage: 'src',
require, require,
sequenceDiagram: new SequenceDiagram(), sequenceDiagram: new SequenceDiagram(),
touchUI: ('ontouchstart' in window),
}); });
loader.parentNode.removeChild(loader); loader.parentNode.removeChild(loader);
ui.build(window.document.body); ui.build(window.document.body);

View File

@ -86,14 +86,57 @@ function makeURL(code, {height, width, zoom}) {
return opts + uri + '.svg'; return opts + uri + '.svg';
} }
function makeSplit(require, nodes, options) { function makeSplit(nodes, options) {
const filteredNodes = [];
const filteredOpts = {
direction: options.direction,
minSize: [],
sizes: [],
snapOffset: options.snapOffset,
};
let total = 0;
for(let i = 0; i < nodes.length; ++ i) {
if(nodes[i]) {
filteredNodes.push(nodes[i]);
filteredOpts.minSize.push(options.minSize[i]);
filteredOpts.sizes.push(options.sizes[i]);
total += options.sizes[i];
}
}
for(let i = 0; i < filteredNodes.length; ++ i) {
filteredOpts.minSize[i] *= 100 / total;
filteredOpts.sizes[i] *= 100 / total;
const percent = filteredOpts.sizes[i] + '%';
if(filteredOpts.direction === 'vertical') {
nodes[i].styles({
boxSizing: 'border-box',
height: percent,
width: '100%',
});
} else {
nodes[i].styles({
boxSizing: 'border-box',
display: 'inline-block',
height: '100%',
verticalAlign: 'top', // Safari fix
width: percent,
});
}
}
if(filteredNodes.length < 2) {
return;
}
// Load on demand for progressive enhancement // Load on demand for progressive enhancement
// (failure to load external module will not block functionality) // (failure to load external module will not block functionality)
require(['split'], (Split) => { options.require(['split'], (Split) => {
// Patches for: // Patches for:
// https://github.com/nathancahill/Split.js/issues/97 // https://github.com/nathancahill/Split.js/issues/97
// https://github.com/nathancahill/Split.js/issues/111 // https://github.com/nathancahill/Split.js/issues/111
const parent = nodes[0].parentNode; const parent = nodes[0].element.parentNode;
const oldAEL = parent.addEventListener; const oldAEL = parent.addEventListener;
const oldREL = parent.removeEventListener; const oldREL = parent.removeEventListener;
parent.addEventListener = (event, callback) => { parent.addEventListener = (event, callback) => {
@ -112,9 +155,13 @@ function makeSplit(require, nodes, options) {
}; };
let oldCursor = null; let oldCursor = null;
const resolvedOptions = Object.assign({ const cursor = (filteredOpts.direction === 'vertical') ?
cursor: (options.direction === 'vertical') ? 'row-resize' : 'col-resize';
'row-resize' : 'col-resize',
return new Split(
filteredNodes.map((node) => node.element),
Object.assign({
cursor,
direction: 'vertical', direction: 'vertical',
gutterSize: 0, gutterSize: 0,
onDragEnd: () => { onDragEnd: () => {
@ -123,14 +170,19 @@ function makeSplit(require, nodes, options) {
}, },
onDragStart: () => { onDragStart: () => {
oldCursor = document.body.style.cursor; oldCursor = document.body.style.cursor;
document.body.style.cursor = resolvedOptions.cursor; document.body.style.cursor = cursor;
}, },
}, options); }, filteredOpts)
);
return new Split(nodes, resolvedOptions);
}); });
} }
DOMWrapper.WrappedElement.prototype.split = function(nodes, options) {
this.add(nodes);
makeSplit(nodes, options);
return this;
};
function hasDroppedFile(event, mime) { function hasDroppedFile(event, mime) {
if(!event.dataTransfer.items && event.dataTransfer.files.length === 0) { if(!event.dataTransfer.items && event.dataTransfer.files.length === 0) {
// Work around Safari not supporting dataTransfer.items // Work around Safari not supporting dataTransfer.items
@ -164,6 +216,35 @@ function getFileContent(file) {
}); });
} }
DOMWrapper.WrappedElement.prototype.fastClick = function() {
const pt = {x: -1, y: 0};
return this
.on('touchstart', (e) => {
const [touch] = e.touches;
pt.x = touch.pageX;
pt.y = touch.pageY;
})
.on('touchend', (e) => {
if(
pt.x === -1 ||
e.touches.length !== 0 ||
e.changedTouches.length !== 1
) {
pt.x = -1;
return;
}
const [touch] = e.changedTouches;
if(
Math.abs(pt.x - touch.pageX) < 10 &&
Math.abs(pt.y - touch.pageY) < 10
) {
e.preventDefault();
e.target.click();
}
pt.x = -1;
});
};
export default class Interface { export default class Interface {
constructor({ constructor({
sequenceDiagram, sequenceDiagram,
@ -172,6 +253,7 @@ export default class Interface {
library = [], library = [],
links = [], links = [],
require = null, require = null,
touchUI = false,
}) { }) {
this.diagram = sequenceDiagram; this.diagram = sequenceDiagram;
this.defaultCode = defaultCode; this.defaultCode = defaultCode;
@ -180,6 +262,7 @@ export default class Interface {
this.links = links; this.links = links;
this.minScale = 1.5; this.minScale = 1.5;
this.require = require || (() => null); this.require = require || (() => null);
this.touchUI = touchUI;
this.debounced = null; this.debounced = null;
this.latestSeq = null; this.latestSeq = null;
@ -193,7 +276,6 @@ export default class Interface {
this._downloadPNGClick = this._downloadPNGClick.bind(this); this._downloadPNGClick = this._downloadPNGClick.bind(this);
this._downloadPNGFocus = this._downloadPNGFocus.bind(this); this._downloadPNGFocus = this._downloadPNGFocus.bind(this);
this._downloadURLClick = this._downloadURLClick.bind(this); this._downloadURLClick = this._downloadURLClick.bind(this);
this._showDropStyle = this._showDropStyle.bind(this);
this._hideDropStyle = this._hideDropStyle.bind(this); this._hideDropStyle = this._hideDropStyle.bind(this);
this.diagram this.diagram
@ -258,14 +340,21 @@ export default class Interface {
}); });
const copy = this.dom.el('button').setClass('copy') const copy = this.dom.el('button').setClass('copy')
.add('\uD83D\uDCCB')
.attr('title', 'Copy to clipboard') .attr('title', 'Copy to clipboard')
.fastClick()
.on('click', () => { .on('click', () => {
if(this.touchUI) {
this.urlOutput.styles({display: 'block'});
}
this.urlOutput this.urlOutput
.focus() .focus()
.select(0, this.urlOutput.element.value.length) .select(0, this.urlOutput.element.value.length)
.element.ownerDocument.execCommand('copy'); .element.ownerDocument.execCommand('copy');
copy.focus(); copy.focus();
this.container.delClass('keyinput');
if(this.touchUI) {
this.urlOutput.styles({display: 'none'});
}
copied.styles({ copied.styles({
'display': 'block', 'display': 'block',
'opacity': 1, 'opacity': 1,
@ -320,7 +409,7 @@ export default class Interface {
copied copied
); );
this.urlBuilder = this.dom.el('div').setClass('urlbuilder') const urlBuilder = this.dom.el('div').setClass('urlbuilder')
.styles({'display': 'none'}) .styles({'display': 'none'})
.add( .add(
this.dom.el('div').setClass('message') this.dom.el('div').setClass('message')
@ -337,17 +426,17 @@ export default class Interface {
path = relativePath; path = relativePath;
} }
this.renderService = new URL(path, window.location.href).href; this.renderService = new URL(path, window.location.href).href;
this.urlBuilder.empty().add(urlOpts); urlBuilder.empty().add(urlOpts);
this._refreshURL(); this._refreshURL();
}) })
.catch(() => { .catch(() => {
this.urlBuilder.empty().add( urlBuilder.empty().add(
this.dom.el('div').setClass('message') this.dom.el('div').setClass('message')
.add('No online rendering service available.') .add('No online rendering service available.')
); );
}); });
return this.urlBuilder; return urlBuilder;
} }
_refreshURL() { _refreshURL() {
@ -363,14 +452,24 @@ export default class Interface {
return; return;
} }
this.builderVisible = true; this.builderVisible = true;
if(this.touchUI) {
this.urlBuilder.styles({
'bottom': '-210px',
'display': 'block',
});
} else {
this.urlBuilder.styles({ this.urlBuilder.styles({
'display': 'block', 'display': 'block',
'height': '0px', 'height': '0px',
'padding': '0px', 'padding': '0px',
'width': this.optsHold.element.clientWidth + 'px', 'width': this.optsHold.element.clientWidth + 'px',
}); });
}
clearTimeout(this.builderTm); clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => { this.builderTm = setTimeout(() => {
if(this.touchUI) {
this.urlBuilder.styles({'bottom': 0});
} else {
this.urlBuilder.styles({ this.urlBuilder.styles({
'height': '150px', 'height': '150px',
'padding': '10px', 'padding': '10px',
@ -379,6 +478,7 @@ export default class Interface {
this.optsHold.styles({ this.optsHold.styles({
'box-shadow': '10px 10px 25px 12px rgba(0,0,0,0.3)', 'box-shadow': '10px 10px 25px 12px rgba(0,0,0,0.3)',
}); });
}
}, 0); }, 0);
this._refreshURL(); this._refreshURL();
@ -389,6 +489,11 @@ export default class Interface {
return; return;
} }
this.builderVisible = false; this.builderVisible = false;
if(this.touchUI) {
this.urlBuilder.styles({
'bottom': (-this.urlBuilder.element.clientHeight - 60) + 'px',
});
} else {
this.urlBuilder.styles({ this.urlBuilder.styles({
'height': '0px', 'height': '0px',
'padding': '0px', 'padding': '0px',
@ -397,6 +502,8 @@ export default class Interface {
this.optsHold.styles({ this.optsHold.styles({
'box-shadow': 'none', 'box-shadow': 'none',
}); });
}
this.container.delClass('keyinput');
clearTimeout(this.builderTm); clearTimeout(this.builderTm);
this.builderTm = setTimeout(() => { this.builderTm = setTimeout(() => {
this.urlBuilder.styles({'display': 'none'}); this.urlBuilder.styles({'display': 'none'});
@ -405,12 +512,14 @@ export default class Interface {
buildOptionsDownloads() { buildOptionsDownloads() {
this.downloadPNG = this.dom.el('a') this.downloadPNG = this.dom.el('a')
.text('Download PNG') .text('Export PNG')
.attrs({ .attrs({
'download': 'SequenceDiagram.png', 'download': 'SequenceDiagram.png',
'href': '#', 'href': '#',
}) })
.on(['focus', 'mouseover', 'mousedown'], this._downloadPNGFocus) .on(['focus', 'mouseover', 'mousedown'], this._downloadPNGFocus)
// Exploit delay between touchend and click on mobile
.on('touchend', this._downloadPNGFocus)
.on('click', this._downloadPNGClick); .on('click', this._downloadPNGClick);
this.downloadSVG = this.dom.el('a') this.downloadSVG = this.dom.el('a')
@ -419,18 +528,22 @@ export default class Interface {
'download': 'SequenceDiagram.svg', 'download': 'SequenceDiagram.svg',
'href': '#', 'href': '#',
}) })
.fastClick()
.on('click', this._downloadSVGClick); .on('click', this._downloadSVGClick);
this.downloadURL = this.dom.el('a') this.downloadURL = this.dom.el('a')
.text('URL') .text('URL')
.attrs({'href': '#'}) .attrs({'href': '#'})
.fastClick()
.on('click', this._downloadURLClick); .on('click', this._downloadURLClick);
this.urlBuilder = this.buildURLBuilder();
this.optsHold = this.dom.el('div').setClass('options downloads').add( this.optsHold = this.dom.el('div').setClass('options downloads').add(
this.downloadPNG, this.downloadPNG,
this.downloadSVG, this.downloadSVG,
this.downloadURL, this.downloadURL,
this.buildURLBuilder() this.urlBuilder
); );
return this.optsHold; return this.optsHold;
@ -444,6 +557,7 @@ export default class Interface {
const hold = this.dom.el('div') const hold = this.dom.el('div')
.setClass('library-item') .setClass('library-item')
.add(holdInner) .add(holdInner)
.fastClick()
.on('click', this.addCodeBlock.bind(this, lib.code)) .on('click', this.addCodeBlock.bind(this, lib.code))
.attach(container); .attach(container);
@ -467,10 +581,33 @@ export default class Interface {
return container; return container;
} }
buildCodePane() {
this.code = this.dom.el('textarea')
.setClass('editor-simple')
.val(this.loadCode() || this.defaultCode)
.on('input', () => this.update(false));
return this.dom.el('div').setClass('pane-code')
.add(this.code);
}
buildLibPane() {
if(this.library.length === 0) {
return null;
}
return this.dom.el('div').setClass('pane-library')
.add(this.dom.el('div').setClass('pane-library-scroller')
.add(this.buildLibrary(
this.dom.el('div').setClass('pane-library-inner')
)));
}
buildViewPane() { buildViewPane() {
this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner') this.viewPaneInner = this.dom.el('div').setClass('pane-view-inner')
.add(this.diagram.dom()) .add(this.diagram.dom())
.on('click', () => this._hideURLBuilder()); .on('touchstart', () => this._hideURLBuilder())
.on('mousedown', () => this._hideURLBuilder());
this.errorMsg = this.dom.el('div').setClass('msg-error'); this.errorMsg = this.dom.el('div').setClass('msg-error');
@ -482,53 +619,10 @@ export default class Interface {
); );
} }
buildLeftPanes() {
const container = this.dom.el('div').setClass('pane-side');
this.code = this.dom.el('textarea')
.setClass('editor-simple')
.val(this.loadCode() || this.defaultCode)
.on('input', () => this.update(false));
const codePane = this.dom.el('div').setClass('pane-code')
.add(this.code)
.attach(container);
if(this.library.length > 0) {
const libPane = this.dom.el('div').setClass('pane-library')
.add(this.dom.el('div').setClass('pane-library-scroller')
.add(this.buildLibrary(
this.dom.el('div').setClass('pane-library-inner')
)))
.attach(container);
makeSplit(this.require, [codePane.element, libPane.element], {
direction: 'vertical',
minSize: [100, 5],
sizes: [70, 30],
snapOffset: 5,
});
}
return container;
}
build(container) { build(container) {
this.dom = new DOMWrapper(container.ownerDocument); this.dom = new DOMWrapper(container.ownerDocument);
const lPane = this.buildLeftPanes();
const viewPane = this.buildViewPane();
this.container = this.dom.wrap(container) this.container = this.dom.wrap(container)
.add(this.dom.el('div').setClass('pane-hold')
.add(
lPane,
viewPane,
this.dom.el('div').setClass('options links')
.add(this.links.map((link) => this.dom.el('a')
.attrs({'href': link.href, 'target': '_blank'})
.text(link.label))),
this.buildOptionsDownloads()
))
.on('dragover', (event) => { .on('dragover', (event) => {
event.preventDefault(); event.preventDefault();
if(hasDroppedFile(event, 'image/svg+xml')) { if(hasDroppedFile(event, 'image/svg+xml')) {
@ -547,14 +641,66 @@ export default class Interface {
if(file) { if(file) {
this.loadFile(file); this.loadFile(file);
} }
}); })
.on('focusin', () => this.container.addClass('keyinput'))
.on('focusout', () => this.container.delClass('keyinput'));
makeSplit(this.require, [lPane.element, viewPane.element], { const codePane = this.buildCodePane();
const libPane = this.buildLibPane();
const viewPane = this.buildViewPane();
const links = this.links.map((link) => this.dom.el('a')
.attrs({'href': link.href, 'target': '_blank'})
.text(this.touchUI ? link.touchLabel : link.label));
if(this.touchUI) {
this.buildOptionsDownloads();
this.container
.addClass('touch')
.add(
this.dom.el('div').setClass('pane-hold')
.split([viewPane, codePane], {
direction: 'vertical',
minSize: [10, 10],
require: this.require,
sizes: [80, 20],
snapOffset: 20,
}),
libPane.styles({'display': 'none', 'top': '100%'}),
this.urlBuilder,
this.dom.el('div').setClass('optbar')
.add(
...links,
this.downloadPNG.text('PNG'),
this.downloadSVG.text('SVG'),
this.downloadURL.text('URL')
)
);
} else {
this.container
.add(
this.dom.el('div').setClass('pane-hold')
.split([
this.dom.el('div').setClass('pane-side')
.split([codePane, libPane], {
direction: 'vertical',
minSize: [100, 5],
require: this.require,
sizes: [70, 30],
snapOffset: 5,
}),
viewPane,
], {
direction: 'horizontal', direction: 'horizontal',
minSize: [10, 10], minSize: [10, 10],
require: this.require,
sizes: [30, 70], sizes: [30, 70],
snapOffset: 70, snapOffset: 70,
}); }),
this.dom.el('div').setClass('options links').add(links),
this.buildOptionsDownloads()
);
}
if(typeof window !== 'undefined') { if(typeof window !== 'undefined') {
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
@ -839,6 +985,8 @@ export default class Interface {
}); });
}); });
code.on('focus', () => this._hideURLBuilder());
code.on('cursorActivity', () => { code.on('cursorActivity', () => {
const from = code.getCursor('from').line; const from = code.getCursor('from').line;
const to = code.getCursor('to').line; const to = code.getCursor('to').line;

View File

@ -2,6 +2,15 @@ html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font: 1em sans-serif; font: 1em sans-serif;
text-size-adjust: none;
-webkit-text-size-adjust: none;
/* disable whole-page elastic scrolling in iOS */
width: 100%;
height: 100%;
position: fixed;
overflow: hidden;
} }
#loader { #loader {
@ -63,32 +72,22 @@ html, body {
} }
.pane-side { .pane-side {
display: inline-block;
width: 30%;
height: 100%;
border-right: 1px solid #808080; border-right: 1px solid #808080;
box-sizing: border-box;
vertical-align: top; /* Safari fix */
}
.pane-code {
height: 70%;
} }
.pane-library { .pane-library {
background: #EEEEEE; background: #EEEEEE;
user-select: none; user-select: none;
height: 30%; -webkit-user-select: none;
border-top: 1px solid #808080; border-top: 1px solid #808080;
box-sizing: border-box; overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
} }
.pane-view { .pane-view {
display: inline-block;
width: 70%;
height: 100%;
position: relative; position: relative;
vertical-align: top; /* Safari fix */ overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
} }
.gutter { .gutter {
@ -266,6 +265,7 @@ html, body {
position: absolute; position: absolute;
background: #FFFFFF; background: #FFFFFF;
user-select: none; user-select: none;
-webkit-user-select: none;
z-index: 30; z-index: 30;
} }
@ -369,6 +369,10 @@ html, body {
box-sizing: border-box; box-sizing: border-box;
} }
.urlbuilder .copy:before {
content: "\1F4CB";
}
.urlbuilder .copy:active { .urlbuilder .copy:active {
background: #EEEEEE; background: #EEEEEE;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
@ -393,6 +397,127 @@ svg a:active, svg a:hover {
fill: #0080CC; fill: #0080CC;
} }
.touch .pane-hold {
bottom: 50px;
}
.touch.keyinput .pane-hold {
bottom: 0;
}
.touch .pane-code {
border-top: 1px solid #808080;
}
.touch .pane-library {
position: absolute;
width: 100%;
height: 100%;
left: 0;
transition: top 0.2s ease;
}
.touch .urlbuilder {
position: absolute;
width: 100%;
height: 160px;
margin-bottom: 50px;
max-height: 100%;
left: 0;
transition: bottom 0.2s ease;
border-top: 1px solid #808080;
font-size: 1em;
background: #FFFFFF;
z-index: 10;
}
.touch.keyinput .urlbuilder {
margin-bottom: 0;
height: 180px;
}
.touch .urlbuilder input[type=number] {
text-align: left;
}
.touch .urlbuilder .output {
display: none;
}
.touch .urlbuilder .copy {
width: auto;
left: 10px;
height: 36px;
border-radius: 100px;
padding: 0;
line-height: 34px;
}
.touch.keyinput .copy {
bottom: 30px;
}
.touch .urlbuilder .copy:before {
content: "Copy URL to Clipboard \1F4CB";
}
.touch .urlbuilder .copy:active {
background: #FFFFFF;
box-shadow: none;
padding-top: 0;
}
.touch .urlbuilder .copied {
height: 36px;
border-radius: 100px;
line-height: 34px;
}
.touch.keyinput .copied {
bottom: 30px;
}
.touch .options {
display: none;
}
.optbar {
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50px;
box-sizing: border-box;
border-top: 1px solid #AAAAAA;
background: #FAFAFA;
font-size: 1.5em;
font-weight: 100;
z-index: 11;
}
.touch.keyinput .optbar {
display: none;
}
.optbar a:link, .optbar a:visited {
color: #5577FF;
text-decoration: none;
cursor: pointer;
}
.optbar a:active, .optbar a:hover {
color: #2244CC;
}
.optbar a {
display: inline-block;
width: 20%;
height: 49px;
line-height: 49px;
text-align: center;
}
@media print { @media print {
.drop-target:after { .drop-target:after {
display: none; display: none;

View File

@ -1,9 +1,12 @@
body { body {
background: rgb(140, 185, 231); background: rgb(140, 185, 231);
margin: 30px 30px 80px; margin: 30px 0 80px;
padding: 0; padding: 0;
font-family: sans-serif; font-family: sans-serif;
line-height: 1.5; line-height: 1.5;
text-size-adjust: none;
-webkit-text-size-adjust: none;
} }
article { article {
@ -21,6 +24,7 @@ article > header {
padding: 10px 20px; padding: 10px 20px;
background: linear-gradient(#DDEEFF, #FFFFFF); background: linear-gradient(#DDEEFF, #FFFFFF);
border-radius: 2px; border-radius: 2px;
text-align: center;
page-break-inside: avoid; page-break-inside: avoid;
break-inside: avoid; break-inside: avoid;
} }
@ -30,11 +34,11 @@ article > header > h1 {
padding: 0; padding: 0;
font: 3.5em 'Vollkorn', serif; font: 3.5em 'Vollkorn', serif;
clear: both; clear: both;
text-align: center;
} }
article > header > .sequence-diagram { article > header > .sequence-diagram {
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.2)); filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.2));
text-align: left;
} }
article > h2 { article > h2 {
@ -128,6 +132,7 @@ a:hover, a:active {
.sequence-diagram[data-sd-interactive] .region.expanded { .sequence-diagram[data-sd-interactive] .region.expanded {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
-webkit-user-select: none;
} }
.sequence-diagram[data-sd-interactive] .region.collapsed:hover .outline, .sequence-diagram[data-sd-interactive] .region.collapsed:hover .outline,
@ -171,6 +176,115 @@ nav a:hover {
background: #EEEEEE; background: #EEEEEE;
} }
@media (max-width: 860px) {
body {
background: transparent;
margin: 0;
}
article {
max-width: 100%;
background: transparent;
box-shadow: none;
margin: 0px;
padding: 10px;
border-radius: 0;
padding-bottom: 80px;
}
article > header {
margin: -10px -10px 0;
border-radius: 0;
}
article > header > h1 {
font-size: 3em;
}
article > header > .sequence-diagram {
max-height: 200px;
}
article > h2 {
margin: 15px -10px 5px;
padding: 5px 10px 0;
}
article h3 {
margin: 10px -10px 5px;
padding: 5px 10px 0;
}
article h4 {
margin: 5px -10px 5px;
padding: 5px 10px 0;
}
.sequence-diagram {
max-height: calc(100vh - 100px);
}
pre.example {
padding: 5px;
}
nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
@media (max-width: 800px) {
article {
font-size: 0.9em;
}
}
@media (max-width: 700px) {
pre {
box-shadow: none;
}
pre.example {
padding: 5px;
width: calc(100% - 205px);
box-sizing: border-box;
}
.example-diagram {
max-width: none;
width: 200px;
box-sizing: border-box;
margin-left: 0;
background: #FFFFFF;
box-shadow: none;
}
}
@media (max-width: 470px) {
article > header > h1 {
font-size: 2.5em;
}
pre.example {
width: auto;
margin: 5px -5px;
}
.example-diagram {
display: block;
float: none;
margin: 0 auto 10px;
width: 300px;
max-width: 80%;
}
}
@media print { @media print {
body { body {
background: transparent; background: transparent;
@ -237,7 +351,6 @@ nav a:hover {
font-size: 0.8em; font-size: 0.8em;
width: 50%; width: 50%;
box-sizing: border-box; box-sizing: border-box;
display: inline-block;
} }
.example-diagram { .example-diagram {