reladraw 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/reladraw/SKILL.md +7 -5
- package/.claude/skills/reladraw/reference/syntax.md +44 -5
- package/README.md +2 -2
- package/SYNTAX.md +44 -5
- package/dist/cli.js +15 -1
- package/dist/examples.d.ts +26 -0
- package/dist/examples.js +56 -0
- package/dist/page-data.d.ts +130 -0
- package/dist/page-data.js +226 -0
- package/dist/page.d.ts +46 -2
- package/dist/page.js +1239 -241
- package/dist/serve.d.ts +18 -0
- package/dist/serve.js +544 -0
- package/examples/arch.reladraw +95 -0
- package/examples/around.reladraw +101 -0
- package/examples/borders.reladraw +28 -0
- package/examples/bundles.reladraw +49 -0
- package/examples/circles.reladraw +37 -0
- package/examples/coincident.reladraw +66 -0
- package/examples/contents.reladraw +52 -0
- package/examples/corridors.reladraw +48 -0
- package/examples/deck.reladraw +26 -0
- package/examples/flowchart.reladraw +23 -0
- package/examples/frames.reladraw +79 -0
- package/examples/gap.reladraw +21 -0
- package/examples/gaps.reladraw +25 -0
- package/examples/icons.reladraw +35 -0
- package/examples/lanes.reladraw +54 -0
- package/examples/lines.reladraw +73 -0
- package/examples/linetexts.reladraw +48 -0
- package/examples/marks.reladraw +39 -0
- package/examples/names.reladraw +23 -0
- package/examples/outlines.reladraw +56 -0
- package/examples/overhang.reladraw +24 -0
- package/examples/overlays.reladraw +75 -0
- package/examples/parts.reladraw +31 -0
- package/examples/passes.reladraw +56 -0
- package/examples/points.reladraw +28 -0
- package/examples/regions.reladraw +14 -0
- package/examples/routing.reladraw +88 -0
- package/examples/shapes.reladraw +38 -0
- package/examples/snug.reladraw +8 -0
- package/examples/text.reladraw +69 -0
- package/examples/themes.reladraw +16 -0
- package/examples/urls.reladraw +35 -0
- package/examples/venn.reladraw +15 -0
- package/package.json +2 -1
package/dist/page.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SourceError } from './errors.js';
|
|
3
|
-
import { blame, parse } from './parser.js';
|
|
4
|
-
import { linkLabel, linksOf } from './links.js';
|
|
5
|
-
import { markdownHtml } from './markdown.js';
|
|
6
|
-
import { iconSvg, renderTagged, themeOf } from './render.js';
|
|
7
|
-
import { resolve } from './resolve.js';
|
|
8
|
-
import { DEFAULT_THEME, luminance, THEME_NAMES, THEMES } from './themes.js';
|
|
1
|
+
import { dataOf, partsOf } from './page-data.js';
|
|
9
2
|
import { PAN_ZOOM } from './viewer.js';
|
|
10
3
|
/**
|
|
11
4
|
* `reladraw render --page`: one diagram in one self-contained HTML page, made
|
|
@@ -18,227 +11,56 @@ import { PAN_ZOOM } from './viewer.js';
|
|
|
18
11
|
* none, and the name everything saved from the page is given. The page is a
|
|
19
12
|
* snapshot of the file at `at`, and says so under its title.
|
|
20
13
|
*/
|
|
21
|
-
export function drawPage(source, file, options = {}, at = new Date()) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const run = (text) => {
|
|
25
|
-
const layout = resolve(parse(text), options);
|
|
26
|
-
const found = options.checks === false ? [] : findings(layout, options.checks ? options.checks(fileChecks(layout)) : fileChecks(layout));
|
|
27
|
-
const title = layout.diagram['title'] ?? name;
|
|
28
|
-
// The theme it was drawn in: the one --theme named, else the file's.
|
|
29
|
-
const named = layout.diagram['theme'];
|
|
30
|
-
const current = (options.theme && THEME_NAMES.find((one) => THEMES[one] === options.theme)) ??
|
|
31
|
-
(named !== undefined && THEMES[named] !== undefined ? named : DEFAULT_THEME);
|
|
32
|
-
// The same diagram drawn in every theme, for the page's theme picker. A
|
|
33
|
-
// theme moves nothing, so only the colors differ between them. A page or
|
|
34
|
-
// text color the file states still holds in each, as in the playground.
|
|
35
|
-
// Guides start as the file or --guides says, and the View menu's switch
|
|
36
|
-
// shows the drawing the other way. A diagram with no guides draws the
|
|
37
|
-
// same both ways, so it carries one drawing.
|
|
38
|
-
const guidesShown = options.guides ?? layout.diagram['guides'] === 'show';
|
|
39
|
-
const guideCount = layout.nodes.filter((node) => node.guide).length +
|
|
40
|
-
layout.edges.filter((edge) => edge.guide).length +
|
|
41
|
-
layout.marks.filter((mark) => mark.guide).length;
|
|
42
|
-
const drawAll = (guides) => {
|
|
43
|
-
const themes = {};
|
|
44
|
-
for (const one of THEME_NAMES) {
|
|
45
|
-
const drawnIn = { ...options, guides, theme: options.theme && one === current ? options.theme : THEMES[one] };
|
|
46
|
-
const theme = themeOf(layout, drawnIn);
|
|
47
|
-
themes[one] = { ...renderTagged(layout, drawnIn), theme, colors: colorsOf(theme), scheme: schemeOf(theme) };
|
|
48
|
-
}
|
|
49
|
-
// A --theme that is not one of the named ones is drawn as given, and offered as such.
|
|
50
|
-
if (themes[current] === undefined) {
|
|
51
|
-
const drawnIn = { ...options, guides };
|
|
52
|
-
const theme = themeOf(layout, drawnIn);
|
|
53
|
-
themes[current] = { ...renderTagged(layout, drawnIn), theme, colors: colorsOf(theme), scheme: schemeOf(theme) };
|
|
54
|
-
}
|
|
55
|
-
return themes;
|
|
56
|
-
};
|
|
57
|
-
const themes = drawAll(guidesShown);
|
|
58
|
-
const flipped = guideCount > 0 ? drawAll(!guidesShown) : null;
|
|
59
|
-
return {
|
|
60
|
-
html: page({
|
|
61
|
-
title,
|
|
62
|
-
name,
|
|
63
|
-
drawn,
|
|
64
|
-
source,
|
|
65
|
-
findings: found,
|
|
66
|
-
themes,
|
|
67
|
-
flipped,
|
|
68
|
-
guidesShown,
|
|
69
|
-
guideCount,
|
|
70
|
-
current,
|
|
71
|
-
graph: graphOf(layout, themes),
|
|
72
|
-
}),
|
|
73
|
-
findings: found,
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
try {
|
|
77
|
-
return run(source);
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
throw error instanceof SourceError ? blame(source, error, run) : error;
|
|
81
|
-
}
|
|
14
|
+
export function drawPage(source, file, options = {}, at = new Date(), editing) {
|
|
15
|
+
const parts = partsOf(source, file, options, at);
|
|
16
|
+
return { html: page(parts, editing ?? null), findings: parts.findings };
|
|
82
17
|
}
|
|
83
|
-
/** A description as the panel shows it: its Markdown as HTML, or nothing. */
|
|
84
|
-
const describe = (attrs) => attrs['description'] === undefined ? {} : { description: markdownHtml(attrs['description']) };
|
|
85
|
-
const BADGE_SIDE = 20;
|
|
86
|
-
function graphOf(layout, themes) {
|
|
87
|
-
const views = (links) => (links ?? []).map((link) => {
|
|
88
|
-
const label = linkLabel(link, links);
|
|
89
|
-
const icon = link.icon === undefined
|
|
90
|
-
? { letters: [...label.trim()][0]?.toUpperCase() ?? '?' }
|
|
91
|
-
: 'picture' in link.icon
|
|
92
|
-
? layout.linkIcons[link.icon.picture]
|
|
93
|
-
: link.icon;
|
|
94
|
-
const badges = {};
|
|
95
|
-
for (const [one, view] of Object.entries(themes))
|
|
96
|
-
badges[one] = iconSvg(icon, BADGE_SIDE, view.theme);
|
|
97
|
-
return { href: link.href, label, badges };
|
|
98
|
-
});
|
|
99
|
-
// A node with no links of its own has its container's, as a click on it in
|
|
100
|
-
// the SVG follows its container's.
|
|
101
|
-
const linksFor = (node) => {
|
|
102
|
-
for (let at = node; at; at = at.parent) {
|
|
103
|
-
const own = linksOf(at.attrs);
|
|
104
|
-
if (own !== undefined)
|
|
105
|
-
return own;
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
};
|
|
109
|
-
// A badge's picture is joined to its box's text by `badge:`, not by a
|
|
110
|
-
// `join-with:` anyone wrote.
|
|
111
|
-
const isBadge = (node) => node.joinWith === 'text' && node.attrs['join-with'] === undefined;
|
|
112
|
-
const nodes = {};
|
|
113
|
-
for (const node of layout.nodes) {
|
|
114
|
-
const text = plainText(node.text);
|
|
115
|
-
nodes[node.name] = {
|
|
116
|
-
label: text === '' ? node.name.split('.').pop() : text,
|
|
117
|
-
links: views(linksFor(node)),
|
|
118
|
-
...describe(node.attrs),
|
|
119
|
-
box: [node.x, node.y, node.width, node.height].map((n) => Math.round(n * 10) / 10),
|
|
120
|
-
...(node.parent ? { parent: node.parent.name } : {}),
|
|
121
|
-
children: node.children.filter((child) => !isBadge(child)).map((child) => child.name),
|
|
122
|
-
styles: stylesOf(node.attrs),
|
|
123
|
-
...(node.guide ? { guide: true } : {}),
|
|
124
|
-
...(isBadge(node) ? { badge: true } : {}),
|
|
125
|
-
...(node.kind === 'none' && node.children.length === 0 ? { bare: true } : {}),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const edges = layout.edges.map((edge) => {
|
|
129
|
-
// The upstream end is the one a line runs from: the end without a mark
|
|
130
|
-
// when only the other has one, and otherwise the end written first.
|
|
131
|
-
const [, start = '', , end = ''] = edge.arrow.match(/^(.*?)(-{1,2})(.*)$/) ?? [];
|
|
132
|
-
const backwards = start !== '' && end === '';
|
|
133
|
-
const [up, down] = backwards ? [edge.to, edge.from] : [edge.from, edge.to];
|
|
134
|
-
const text = edge.texts.map((one) => plainText(one.text)).filter((one) => one !== '').join(' · ');
|
|
135
|
-
return {
|
|
136
|
-
up: up.name,
|
|
137
|
-
down: down.name,
|
|
138
|
-
label: text,
|
|
139
|
-
links: views(linksOf(edge.attrs)),
|
|
140
|
-
...describe(edge.attrs),
|
|
141
|
-
styles: stylesOf(edge.attrs),
|
|
142
|
-
...(edge.guide ? { guide: true } : {}),
|
|
143
|
-
};
|
|
144
|
-
});
|
|
145
|
-
return { nodes, edges };
|
|
146
|
-
}
|
|
147
|
-
/** The styles a `style:` names; the parser joins them on a space. */
|
|
148
|
-
const stylesOf = (attrs) => (attrs['style'] === undefined ? [] : attrs['style'].split(' '));
|
|
149
|
-
/** A text as a reader sees it: markup such as `[dim]` taken out, its lines run together. */
|
|
150
|
-
function plainText(text) {
|
|
151
|
-
return text.replace(/\[\/?[a-z][a-z0-9-]*\]/gi, '').replace(/\s*\n\s*/g, ' ').trim();
|
|
152
|
-
}
|
|
153
|
-
/** A local time as `2026-11-05 14:32`, which sorts and reads the same everywhere. */
|
|
154
|
-
function stamp(at) {
|
|
155
|
-
const two = (n) => String(n).padStart(2, '0');
|
|
156
|
-
return `${at.getFullYear()}-${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`;
|
|
157
|
-
}
|
|
158
|
-
const hex = (color) => /^#[0-9a-f]{6}$/i.test(color);
|
|
159
18
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* Split on, its matches are the odd pieces. Matching too much costs nothing;
|
|
163
|
-
* a drawing whose other pieces differ is carried whole.
|
|
19
|
+
* The page with no file: the playground. It opens on the first example and
|
|
20
|
+
* draws whatever its visitor last had in that browser, with the source shown.
|
|
164
21
|
*/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
:
|
|
175
|
-
}
|
|
176
|
-
function colorsOf(theme) {
|
|
177
|
-
return {
|
|
178
|
-
'--page': theme.background,
|
|
179
|
-
'--ink': theme.text,
|
|
180
|
-
'--muted': theme.mutedText,
|
|
181
|
-
'--rule': theme.boxStroke,
|
|
182
|
-
'--accent': theme.primary.color,
|
|
183
|
-
'--on-accent': theme.primary.on,
|
|
184
|
-
};
|
|
22
|
+
export function playgroundPage(playground) {
|
|
23
|
+
const { examples, folder } = playground;
|
|
24
|
+
const parts = partsOf(examples[0]?.source ?? '', 'diagram.reladraw', {}, new Date());
|
|
25
|
+
const editing = folder !== undefined ? { kind: 'local', folder, examples } : { kind: 'hosted', examples };
|
|
26
|
+
return page(parts, editing, {
|
|
27
|
+
title: folder !== undefined ? `reladraw · ${folder}` : 'reladraw playground: write diagrams in the browser',
|
|
28
|
+
heading: 'reladraw',
|
|
29
|
+
drawn: folder !== undefined ? `local playground · ${folder} · not saved to a file` : 'a diagram language where you say where things go',
|
|
30
|
+
head: playground.head ?? '',
|
|
31
|
+
library: playground.library ?? '',
|
|
32
|
+
});
|
|
185
33
|
}
|
|
186
|
-
function page(
|
|
34
|
+
function page(parts, editing, framing) {
|
|
35
|
+
const { themes, current } = parts;
|
|
187
36
|
const shown = themes[current];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
37
|
+
const data = { ...dataOf(parts), edit: editing };
|
|
38
|
+
const title = framing?.title ?? parts.title;
|
|
39
|
+
const noFile = editing !== null && editing.kind !== 'file';
|
|
40
|
+
const filled = {
|
|
41
|
+
panzoom: PAN_ZOOM,
|
|
42
|
+
scheme: shown.scheme,
|
|
43
|
+
title: escapeMarkup(title),
|
|
44
|
+
head: framing?.head ?? '',
|
|
45
|
+
heading: escapeMarkup(framing?.heading ?? parts.title),
|
|
46
|
+
drawn: escapeMarkup(framing?.drawn ?? parts.drawn),
|
|
47
|
+
links: noFile ? LINKS : '',
|
|
48
|
+
colors: [
|
|
49
|
+
...Object.entries(shown.colors).map(([key, value]) => `${key}: ${value};`),
|
|
50
|
+
'--sel: #e0b25a; --up: #22b98c; --down: #e5484d;',
|
|
51
|
+
].join(' '),
|
|
52
|
+
cheatsheet: editing ? CHEATSHEET : '',
|
|
53
|
+
dialogs: noFile ? DIALOGS : '',
|
|
54
|
+
drawing: shown.tagged,
|
|
55
|
+
// A literal </script> in the library's text would end its tag early.
|
|
56
|
+
library: framing?.library ? `<script>\n${framing.library.replace(/<\/script/gi, '<\\/script')}\n</script>` : '',
|
|
57
|
+
data: scriptJson(data),
|
|
201
58
|
};
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
const packAll = (set) => {
|
|
206
|
-
const base = set[current];
|
|
207
|
-
return Object.fromEntries(Object.entries(set).map(([one, view]) => [
|
|
208
|
-
one,
|
|
209
|
-
one === current
|
|
210
|
-
? { svg: view.svg, tagged: view.tagged, colors: view.colors, scheme: view.scheme }
|
|
211
|
-
: { svg: pack(view.svg, base.svg), tagged: pack(view.tagged, base.tagged), colors: view.colors, scheme: view.scheme },
|
|
212
|
-
]));
|
|
213
|
-
};
|
|
214
|
-
const data = {
|
|
215
|
-
name,
|
|
216
|
-
source,
|
|
217
|
-
current,
|
|
218
|
-
varies: VARIES.source,
|
|
219
|
-
themeGroups: { dark: group(false), light: group(true) },
|
|
220
|
-
themes: packAll(themes),
|
|
221
|
-
flipped: flipped && packAll(flipped),
|
|
222
|
-
guidesShown,
|
|
223
|
-
guideCount,
|
|
224
|
-
findings: findings.map((finding) => ({ kind: finding.kind, message: finding.message, line: finding.line })),
|
|
225
|
-
graph,
|
|
226
|
-
};
|
|
227
|
-
// Replaced by functions, so a `$` in a title or a drawing is taken as written.
|
|
228
|
-
// The zoom's code goes in first, so nothing a diagram says can be taken for its marker.
|
|
229
|
-
return PAGE.replace('{{panzoom}}', () => PAN_ZOOM)
|
|
230
|
-
.replace('{{scheme}}', () => shown.scheme)
|
|
231
|
-
.replace('{{title}}', () => escapeHtml(title))
|
|
232
|
-
.replace('{{heading}}', () => escapeHtml(title))
|
|
233
|
-
.replace('{{drawn}}', () => escapeHtml(drawn))
|
|
234
|
-
.replace('{{colors}}', () => [
|
|
235
|
-
...Object.entries(shown.colors).map(([key, value]) => `${key}: ${value};`),
|
|
236
|
-
'--sel: #e0b25a; --up: #22b98c; --down: #e5484d;',
|
|
237
|
-
].join(' '))
|
|
238
|
-
.replace('{{drawing}}', () => shown.tagged)
|
|
239
|
-
.replace('{{data}}', () => scriptJson(data));
|
|
59
|
+
// One pass, so nothing put in (a title, a drawing, the library) is read
|
|
60
|
+
// again as a marker, and a function, so a `$` in it is taken as written.
|
|
61
|
+
return PAGE.replace(/\{\{([a-z]+)\}\}/g, (marker, key) => filled[key] ?? marker);
|
|
240
62
|
}
|
|
241
|
-
function
|
|
63
|
+
function escapeMarkup(text) {
|
|
242
64
|
// `{` too, so a title or file name can't be taken for a later marker.
|
|
243
65
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/\{/g, '{');
|
|
244
66
|
}
|
|
@@ -255,6 +77,7 @@ const PAGE = `<!doctype html>
|
|
|
255
77
|
<meta name="color-scheme" content="{{scheme}}">
|
|
256
78
|
<meta name="generator" content="reladraw">
|
|
257
79
|
<title>{{title}}</title>
|
|
80
|
+
{{head}}
|
|
258
81
|
<style>
|
|
259
82
|
:root { {{colors}} }
|
|
260
83
|
* { box-sizing: border-box; }
|
|
@@ -455,10 +278,136 @@ const PAGE = `<!doctype html>
|
|
|
455
278
|
}
|
|
456
279
|
aside li button.chosen { border-color: var(--accent); }
|
|
457
280
|
aside li.none { padding: 6px 8px; color: var(--muted); font-size: 13px; }
|
|
281
|
+
aside li.error { padding: 6px 8px; margin: 2px 0; border-left: 3px solid var(--down); font-size: 13px; }
|
|
282
|
+
aside li.error .kind { color: var(--down); }
|
|
283
|
+
#live {
|
|
284
|
+
padding: 5px 16px; font-size: 12.5px;
|
|
285
|
+
background: color-mix(in srgb, var(--down) 18%, var(--page)); color: var(--ink);
|
|
286
|
+
border-bottom: 1px solid color-mix(in srgb, var(--down) 45%, transparent);
|
|
287
|
+
}
|
|
288
|
+
#live[hidden] { display: none; }
|
|
289
|
+
|
|
290
|
+
/* The editor: a colored layer under a transparent textarea, every property
|
|
291
|
+
that can move a character the same on both, as in the playground. */
|
|
292
|
+
#editor {
|
|
293
|
+
width: 38%; min-width: 280px; max-width: 760px; display: flex; flex-direction: column;
|
|
294
|
+
border-right: 1px solid color-mix(in srgb, var(--rule) 45%, transparent);
|
|
295
|
+
background: color-mix(in srgb, var(--ink) 4%, var(--page));
|
|
296
|
+
}
|
|
297
|
+
#editor[hidden] { display: none; }
|
|
298
|
+
.src-head { display: flex; align-items: center; gap: 6px; padding: 6px 10px; font-size: 12.5px;
|
|
299
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
300
|
+
.src-head .file { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
301
|
+
#unsaved { color: var(--sel); }
|
|
302
|
+
#unsaved[hidden] { display: none; }
|
|
303
|
+
.src-head .gap, #changed .gap { flex: 1; }
|
|
304
|
+
.src-head kbd { font: 11px system-ui, sans-serif; color: var(--muted); margin-left: 4px; }
|
|
305
|
+
#changed { display: flex; align-items: center; gap: 6px; padding: 6px 10px; font-size: 12.5px;
|
|
306
|
+
background: color-mix(in srgb, var(--sel) 18%, var(--page));
|
|
307
|
+
border-bottom: 1px solid color-mix(in srgb, var(--sel) 45%, transparent); }
|
|
308
|
+
#changed[hidden] { display: none; }
|
|
309
|
+
.code { position: relative; flex: 1; min-height: 0; }
|
|
310
|
+
.code > * {
|
|
311
|
+
position: absolute; inset: 0; margin: 0; border: 0; padding: 12px 14px;
|
|
312
|
+
font: 12.5px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
313
|
+
tab-size: 2; white-space: pre-wrap; overflow-wrap: break-word; word-break: normal;
|
|
314
|
+
}
|
|
315
|
+
#paint { overflow: hidden; color: var(--ink); pointer-events: none; }
|
|
316
|
+
#source { resize: none; outline: 0; background: transparent; color: transparent; caret-color: var(--ink); overflow-y: auto; }
|
|
317
|
+
#source::selection { background: rgba(130, 170, 240, 0.28); color: transparent; }
|
|
318
|
+
:root { --syn-comment: #6a6a78; --syn-string: #9ec99a; --syn-keyword: #82aaf0; --syn-name: #b8c9e8;
|
|
319
|
+
--syn-relation: #c79bdc; --syn-attribute: #6fb3ac; --syn-value: #d3a05f; }
|
|
320
|
+
:root[data-scheme="light"] { --syn-comment: #8a8a96; --syn-string: #3f7a3a; --syn-keyword: #2f5fb8; --syn-name: #34495e;
|
|
321
|
+
--syn-relation: #8a3fa8; --syn-attribute: #217a72; --syn-value: #a0601a; }
|
|
322
|
+
.t-comment, .t-bracket, .t-markup { color: var(--syn-comment); }
|
|
323
|
+
.t-string { color: var(--syn-string); }
|
|
324
|
+
.t-keyword { color: var(--syn-keyword); }
|
|
325
|
+
.t-name { color: var(--syn-name); }
|
|
326
|
+
.t-relation, .t-arrow { color: var(--syn-relation); }
|
|
327
|
+
.t-attribute { color: var(--syn-attribute); }
|
|
328
|
+
.t-value, .t-color { color: var(--syn-value); }
|
|
329
|
+
#edit[aria-pressed="true"] { border-color: var(--sel); color: var(--sel); }
|
|
330
|
+
.menu-list.wide { min-width: 340px; }
|
|
331
|
+
.menu-list .row .when { color: var(--muted); font-variant-numeric: tabular-nums; width: 3.2em; flex: none; }
|
|
332
|
+
.menu-list .row .who { width: 4.5em; flex: none; }
|
|
333
|
+
.menu-list .row .who.you { color: #8fb3ff; }
|
|
334
|
+
.menu-list .row .who.agent { color: var(--up); }
|
|
335
|
+
.menu-list .row .who.disk, .menu-list .row .who.opened { color: var(--muted); }
|
|
336
|
+
.menu-list .row .about { flex: 1; text-align: left; white-space: normal; }
|
|
458
337
|
.where { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
459
338
|
.kind { color: var(--accent); }
|
|
460
339
|
.note { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; }
|
|
340
|
+
aside li.error button.copy { float: right; display: inline-block; width: auto; margin: 0 0 0 8px; padding: 0 6px; font-size: 11px;
|
|
341
|
+
border-color: color-mix(in srgb, var(--rule) 70%, transparent); }
|
|
342
|
+
aside li.error .goto { cursor: pointer; text-decoration: underline dotted; }
|
|
343
|
+
|
|
344
|
+
/* ---- a page with no file: the playground ---- */
|
|
345
|
+
.links { display: inline-flex; gap: 12px; margin-left: 10px; font-size: 12px; }
|
|
346
|
+
.heading .drawn { display: inline; }
|
|
347
|
+
.links a { color: var(--muted); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); }
|
|
348
|
+
.links a:hover { color: var(--ink); }
|
|
349
|
+
/* The divider between the source and the picture: dragged, or moved with the arrows. */
|
|
350
|
+
#split { flex: none; width: 6px; margin: 0 -3px; z-index: 2; cursor: col-resize; touch-action: none; }
|
|
351
|
+
#split[hidden] { display: none; }
|
|
352
|
+
#split:hover, #split.dragging, #split:focus-visible { background: color-mix(in srgb, var(--accent) 45%, transparent); outline: 0; }
|
|
353
|
+
body.dragging { cursor: col-resize; user-select: none; }
|
|
354
|
+
/* The cheatsheet. Closed it is one line; open it takes height off the
|
|
355
|
+
source rather than off the diagram, since it answers what to type. */
|
|
356
|
+
#cheatsheet { flex: none; max-height: 46%; overflow: auto; border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
357
|
+
#cheatsheet summary { padding: 7px 12px; cursor: pointer; color: var(--muted); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; user-select: none; }
|
|
358
|
+
#cheatsheet summary:hover { color: var(--ink); }
|
|
359
|
+
#cheatsheet dl { margin: 0; padding: 0 12px 14px; display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 12px; }
|
|
360
|
+
#cheatsheet dt { color: var(--muted); white-space: nowrap; }
|
|
361
|
+
#cheatsheet dd { margin: 0; line-height: 1.7; }
|
|
362
|
+
#cheatsheet code { font: 11.5px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; display: inline-block;
|
|
363
|
+
padding: 1px 5px; margin-right: 3px; border-radius: 3px; background: color-mix(in srgb, var(--ink) 9%, transparent); }
|
|
364
|
+
#cheatsheet b { color: var(--syn-relation); font-weight: 400; }
|
|
365
|
+
#cheatsheet i { color: var(--muted); font-style: normal; }
|
|
366
|
+
dialog { padding: 0; color: var(--ink); background: var(--page); overflow: hidden;
|
|
367
|
+
border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius: 6px; }
|
|
368
|
+
dialog::backdrop { background: rgba(0, 0, 0, .6); }
|
|
369
|
+
dialog .bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 16px;
|
|
370
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
371
|
+
dialog h2 { margin: 0; font-size: 13px; font-weight: 600; }
|
|
372
|
+
dialog footer { display: flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 12px; color: var(--muted);
|
|
373
|
+
border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
374
|
+
dialog footer .gap { flex: 1; }
|
|
375
|
+
#examples { width: min(1100px, calc(100vw - 32px)); height: min(720px, calc(100vh - 32px)); max-width: none; max-height: none; }
|
|
376
|
+
#examples[open] { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: auto auto minmax(0, 1fr) auto; }
|
|
377
|
+
#examples .bar, #examples footer { grid-column: 1 / -1; }
|
|
378
|
+
#catalog { grid-row: 2 / 4; margin: 0; padding: 6px 0; list-style: none; overflow-y: auto; outline: 0;
|
|
379
|
+
border-right: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
380
|
+
#catalog li { padding: 6px 16px; font-size: 13px; cursor: pointer; border-left: 2px solid transparent; }
|
|
381
|
+
#catalog li:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
|
|
382
|
+
#catalog li[aria-selected="true"] { background: color-mix(in srgb, var(--ink) 13%, transparent); border-left-color: var(--accent); }
|
|
383
|
+
#catalog:focus-visible li[aria-selected="true"] { outline: 1px solid var(--muted); outline-offset: -1px; }
|
|
384
|
+
#about { margin: 0; padding: 10px 16px; font-size: 13px; border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
385
|
+
#preview { min-width: 0; min-height: 0; overflow: auto; padding: 16px; display: flex; }
|
|
386
|
+
#preview svg { max-width: 100%; max-height: 100%; height: auto; margin: auto; }
|
|
387
|
+
#format-settings, #save-as { width: min(640px, calc(100vw - 32px)); max-height: calc(100vh - 32px); }
|
|
388
|
+
#format-settings[open], #save-as[open] { display: flex; flex-direction: column; }
|
|
389
|
+
dialog .about { margin: 0; padding: 10px 16px; font-size: 12px; color: var(--muted); }
|
|
390
|
+
#format-fields { overflow-y: auto; padding: 4px 16px; }
|
|
391
|
+
#format-fields label { display: grid; grid-template-columns: 190px 1fr; gap: 2px 12px; align-items: center; padding: 8px 0; font-size: 13px;
|
|
392
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 30%, transparent); }
|
|
393
|
+
#format-fields label span { grid-column: 2; color: var(--muted); font-size: 11px; }
|
|
394
|
+
#format-fields select, #format-fields input, #save-as input { font: 12px ui-monospace, Menlo, Consolas, monospace; color: var(--ink); background: var(--page);
|
|
395
|
+
border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius: 4px; padding: 4px 6px; min-width: 0; }
|
|
396
|
+
#save-as label { display: flex; align-items: center; gap: 8px; padding: 0 16px 12px; font-size: 13px; }
|
|
397
|
+
#save-as input { flex: 1; font-size: 13px; }
|
|
398
|
+
#format-problem, #save-problem { margin: 0; padding: 0 16px; color: var(--down); font-size: 12px; }
|
|
399
|
+
#format-problem:not(:empty), #save-problem:not(:empty) { padding: 8px 16px; }
|
|
400
|
+
@media (max-width: 760px) {
|
|
401
|
+
#examples[open] { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 2fr) auto minmax(0, 3fr) auto; }
|
|
402
|
+
#catalog { grid-row: auto; border-right: 0; border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
403
|
+
#format-fields label { grid-template-columns: 1fr; }
|
|
404
|
+
#format-fields label span { grid-column: 1; }
|
|
405
|
+
dialog footer .keys { display: none; }
|
|
406
|
+
}
|
|
461
407
|
@media (max-width: 700px) {
|
|
408
|
+
#split { display: none; }
|
|
409
|
+
#editor { width: auto !important; max-width: none; min-width: 0; height: 45%; border-right: 0;
|
|
410
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
462
411
|
main { flex-direction: column; }
|
|
463
412
|
#side { width: auto; max-height: 45%; border-left: 0; border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
464
413
|
}
|
|
@@ -468,9 +417,12 @@ const PAGE = `<!doctype html>
|
|
|
468
417
|
<header>
|
|
469
418
|
<div class="heading">
|
|
470
419
|
<h1>{{heading}}</h1>
|
|
471
|
-
<p class="drawn">{{drawn}}</p>
|
|
420
|
+
<p class="drawn">{{drawn}}</p>{{links}}
|
|
472
421
|
</div>
|
|
473
422
|
<div class="tools">
|
|
423
|
+
<button id="browse" type="button" title="Open one of the examples in the editor" aria-haspopup="dialog" hidden>Examples</button>
|
|
424
|
+
<button id="edit"type="button" title="Show or hide the source (e)" aria-pressed="false" hidden>Edit<kbd class="tag">e</kbd></button>
|
|
425
|
+
<span class="gap" id="edit-gap" hidden></span>
|
|
474
426
|
<button id="out" type="button" title="Zoom out (-)" aria-label="Zoom out">−</button>
|
|
475
427
|
<span id="scale" aria-live="polite">100%</span>
|
|
476
428
|
<button id="in" type="button" title="Zoom in (+)" aria-label="Zoom in">+</button>
|
|
@@ -478,8 +430,12 @@ const PAGE = `<!doctype html>
|
|
|
478
430
|
<button id="actual" type="button" title="The drawing at its own scale (1)">100%</button>
|
|
479
431
|
<span class="gap"></span>
|
|
480
432
|
<button id="full" type="button" title="Full screen (f)"><span class="label">Full screen</span><kbd class="tag">f</kbd></button>
|
|
433
|
+
<span class="menu" id="versions-menu" hidden>
|
|
434
|
+
<button id="versions-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="versions-list">Versions ▾<kbd class="tag">o</kbd></button>
|
|
435
|
+
<span id="versions-list" class="menu-list wide" role="menu" hidden></span>
|
|
436
|
+
</span>
|
|
481
437
|
<span class="menu">
|
|
482
|
-
<button id="save" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="save-list">
|
|
438
|
+
<button id="save" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="save-list">Export ▾<kbd class="tag">x</kbd></button>
|
|
483
439
|
<span id="save-list" class="menu-list" role="menu" hidden>
|
|
484
440
|
<button id="save-page" class="row" type="button" role="menuitem" data-key="p" title="Save this page as one HTML file, to keep or send"><span class="name">This page</span><kbd>p</kbd></button>
|
|
485
441
|
<button id="save-source" class="row" type="button" role="menuitem" data-key="r" title="Save the .reladraw file this page was drawn from"><span class="name">.reladraw source</span><kbd>r</kbd></button>
|
|
@@ -506,7 +462,24 @@ const PAGE = `<!doctype html>
|
|
|
506
462
|
</div>
|
|
507
463
|
</header>
|
|
508
464
|
<div id="strip" hidden><span class="crumbs"></span><span class="hint"></span></div>
|
|
465
|
+
<div id="live" role="status" hidden></div>
|
|
509
466
|
<main>
|
|
467
|
+
<section id="editor" aria-label="Source" hidden>
|
|
468
|
+
<div id="changed" role="alert" hidden><span class="what"></span><span class="gap"></span><button id="load-disk" type="button">Load it</button><button id="keep-mine" type="button">Keep mine</button></div>
|
|
469
|
+
<div class="src-head">
|
|
470
|
+
<span class="file"></span><span id="unsaved" title="Not saved to the file" hidden>●</span><span class="gap"></span>
|
|
471
|
+
<button id="open-file" type="button" title="Open a .reladraw file into the source" hidden>Open…</button>
|
|
472
|
+
<input id="file-input" type="file" accept=".reladraw,text/plain" hidden>
|
|
473
|
+
<button id="format" type="button" title="Lay the source out again, as reladraw format does, with the nearest .reladraw-format (Shift+Alt+F)"><span class="name">Format</span></button>
|
|
474
|
+
<button id="format-open" type="button" aria-haspopup="dialog" title="How Format lays the source out" hidden>Format settings…</button>
|
|
475
|
+
<button id="write" type="button" title="Write the source to the file (⌘S or Ctrl+S)"><span class="name">Save</span><kbd>⌘S</kbd></button>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="code">
|
|
478
|
+
<pre id="paint" aria-hidden="true"></pre>
|
|
479
|
+
<textarea id="source" spellcheck="false" autocapitalize="off" autocomplete="off" aria-label="Source"></textarea>
|
|
480
|
+
</div>{{cheatsheet}}
|
|
481
|
+
</section>
|
|
482
|
+
<div id="split" role="separator" aria-orientation="vertical" aria-label="Resize the source and the diagram" tabindex="0" title="Drag to resize; double-click to reset" hidden></div>
|
|
510
483
|
<div id="view">
|
|
511
484
|
<div id="stage">
|
|
512
485
|
{{drawing}}
|
|
@@ -522,13 +495,18 @@ const PAGE = `<!doctype html>
|
|
|
522
495
|
<ol id="finding-list"></ol>
|
|
523
496
|
</aside>
|
|
524
497
|
</div>
|
|
525
|
-
</main>
|
|
498
|
+
</main>{{dialogs}}
|
|
499
|
+
{{library}}
|
|
526
500
|
<script>
|
|
527
501
|
(function () {
|
|
502
|
+
var DATA = {{data}};
|
|
503
|
+
// How the source is edited, or null on a snapshot, which has no editor.
|
|
504
|
+
var EDIT = DATA.edit;
|
|
528
505
|
// The page as written, taken before anything below changes it, so This
|
|
529
506
|
// page saves what reladraw wrote rather than the current zoom and highlights.
|
|
530
|
-
|
|
531
|
-
var
|
|
507
|
+
// A page with an editor makes a fresh snapshot instead (below).
|
|
508
|
+
var written = EDIT ? '' : '<!doctype html>\\n' + document.documentElement.outerHTML + '\\n';
|
|
509
|
+
document.documentElement.setAttribute('data-scheme', DATA.themes[DATA.current].scheme === 'light' ? 'light' : 'dark');
|
|
532
510
|
var view = document.getElementById('view');
|
|
533
511
|
var stage = document.getElementById('stage');
|
|
534
512
|
var svg = stage.querySelector('svg');
|
|
@@ -638,23 +616,30 @@ const PAGE = `<!doctype html>
|
|
|
638
616
|
return Promise.resolve(blob);
|
|
639
617
|
}
|
|
640
618
|
|
|
641
|
-
// Every way out of the page is under one
|
|
619
|
+
// Every way out of the page is under one Export button: the page, its
|
|
642
620
|
// source, and the picture. The list stays open after a choice, so "Copied"
|
|
643
|
-
// can be read. It is one of the menus (below), opened by a click or by m
|
|
621
|
+
// can be read. It is one of the menus (below), opened by a click or by m x.
|
|
622
|
+
// Save is the served page's writing of the file, so this is Export everywhere.
|
|
644
623
|
var saveButton = document.getElementById('save');
|
|
645
624
|
var saveList = document.getElementById('save-list');
|
|
646
625
|
|
|
647
626
|
var savePage = document.getElementById('save-page');
|
|
648
627
|
savePage.onclick = function () {
|
|
649
628
|
saveFile(savePage, DATA.name + '.html', 'Web page', 'text/html', function () {
|
|
650
|
-
return ready(new Blob([written], { type: 'text/html' }));
|
|
629
|
+
if (!EDIT) return ready(new Blob([written], { type: 'text/html' }));
|
|
630
|
+
// A page with an editor saves a snapshot of the diagram as last drawn,
|
|
631
|
+
// as render --page writes it: this page itself would be the editor,
|
|
632
|
+
// still looking for its server or holding the whole library.
|
|
633
|
+
return libReady.then(function (lib) {
|
|
634
|
+
return new Blob([lib.drawPage(DATA.source, DATA.name + '.reladraw').html], { type: 'text/html' });
|
|
635
|
+
});
|
|
651
636
|
});
|
|
652
637
|
};
|
|
653
638
|
|
|
654
639
|
var saveSource = document.getElementById('save-source');
|
|
655
640
|
saveSource.onclick = function () {
|
|
656
641
|
saveFile(saveSource, DATA.name + '.reladraw', 'reladraw source', 'text/plain', function () {
|
|
657
|
-
return ready(new Blob([DATA.source], { type: 'text/plain' }));
|
|
642
|
+
return ready(new Blob([EDIT ? sourceBox.value : DATA.source], { type: 'text/plain' }));
|
|
658
643
|
});
|
|
659
644
|
};
|
|
660
645
|
|
|
@@ -809,7 +794,6 @@ const PAGE = `<!doctype html>
|
|
|
809
794
|
}
|
|
810
795
|
findingsRow.onclick = flipFindings;
|
|
811
796
|
if (count === 0) {
|
|
812
|
-
list.append(element('li', 'none', 'No findings.'));
|
|
813
797
|
panel.hidden = true;
|
|
814
798
|
} else {
|
|
815
799
|
// Open from the start, unless the panel is to stay out of the way.
|
|
@@ -850,7 +834,41 @@ const PAGE = `<!doctype html>
|
|
|
850
834
|
zoom.pan((room.left + room.width / 2) - (r.l + r.r) / 2, (room.top + room.height / 2) - (r.t + r.b) / 2);
|
|
851
835
|
}
|
|
852
836
|
|
|
853
|
-
|
|
837
|
+
// The list, made again when a served page redraws. An error the file has
|
|
838
|
+
// stopped parsing with heads it, over the findings of the last drawing that
|
|
839
|
+
// parsed, which is still the one shown.
|
|
840
|
+
function listFindings(error) {
|
|
841
|
+
list.textContent = '';
|
|
842
|
+
count = DATA.findings.length + (error ? 1 : 0);
|
|
843
|
+
findingsRow.querySelector('.name').textContent = 'Findings (' + count + ')';
|
|
844
|
+
guidesRow.querySelector('.name').textContent = 'Guides (' + DATA.guideCount + ')';
|
|
845
|
+
if (error) {
|
|
846
|
+
var wrong = element('li', 'error');
|
|
847
|
+
var head = element('span', 'where', error.line ? 'line ' + error.line + ' ' : '');
|
|
848
|
+
if (EDIT) {
|
|
849
|
+
if (error.line) {
|
|
850
|
+
head.classList.add('goto');
|
|
851
|
+
head.title = 'Select the line in the source';
|
|
852
|
+
head.onclick = function () { goToLine(error.line); };
|
|
853
|
+
}
|
|
854
|
+
// For pasting into an issue, or a chat with an agent.
|
|
855
|
+
var copy = element('button', 'copy', 'Copy');
|
|
856
|
+
copy.type = 'button';
|
|
857
|
+
copy.title = 'Copy the error';
|
|
858
|
+
copy.onclick = function () {
|
|
859
|
+
toClipboard((error.line ? 'line ' + error.line + ': ' : '') + error.message)
|
|
860
|
+
.then(function () { flash(copy, 'Copied'); }, function () { flash(copy, 'Blocked'); });
|
|
861
|
+
};
|
|
862
|
+
wrong.append(copy);
|
|
863
|
+
}
|
|
864
|
+
wrong.append(head, element('span', 'kind', 'error'), document.createElement('br'),
|
|
865
|
+
document.createTextNode(error.message), element('span', 'note', 'The picture is the last version that drew.'));
|
|
866
|
+
list.append(wrong);
|
|
867
|
+
}
|
|
868
|
+
if (count === 0) list.append(element('li', 'none', 'No findings.'));
|
|
869
|
+
DATA.findings.forEach(addFinding);
|
|
870
|
+
}
|
|
871
|
+
function addFinding(finding) {
|
|
854
872
|
var item = document.createElement('li');
|
|
855
873
|
var button = document.createElement('button');
|
|
856
874
|
button.type = 'button';
|
|
@@ -867,10 +885,15 @@ const PAGE = `<!doctype html>
|
|
|
867
885
|
note.textContent = 'Nothing drawn comes from that line.';
|
|
868
886
|
button.append(note);
|
|
869
887
|
}
|
|
870
|
-
|
|
888
|
+
// With the source shown, the line it names is selected there too.
|
|
889
|
+
button.onclick = function () {
|
|
890
|
+
light(finding, button);
|
|
891
|
+
if (EDIT && !editorPane.hidden) goToLine(finding.line);
|
|
892
|
+
};
|
|
871
893
|
item.append(button);
|
|
872
894
|
list.append(item);
|
|
873
|
-
}
|
|
895
|
+
}
|
|
896
|
+
listFindings(null);
|
|
874
897
|
|
|
875
898
|
/* ---- selecting a box or a line ---- */
|
|
876
899
|
|
|
@@ -1081,6 +1104,7 @@ const PAGE = `<!doctype html>
|
|
|
1081
1104
|
var next = shown();
|
|
1082
1105
|
Object.keys(next.colors).forEach(function (key) { document.documentElement.style.setProperty(key, next.colors[key]); });
|
|
1083
1106
|
schemeMeta.setAttribute('content', next.scheme);
|
|
1107
|
+
document.documentElement.setAttribute('data-scheme', next.scheme === 'light' ? 'light' : 'dark');
|
|
1084
1108
|
unlight();
|
|
1085
1109
|
hovered = null;
|
|
1086
1110
|
tip.style.display = 'none';
|
|
@@ -1180,6 +1204,8 @@ const PAGE = `<!doctype html>
|
|
|
1180
1204
|
var tools = document.querySelector('.tools');
|
|
1181
1205
|
var viewButton = document.getElementById('view-button');
|
|
1182
1206
|
var viewList = document.getElementById('view-list');
|
|
1207
|
+
var versionsButton = document.getElementById('versions-button');
|
|
1208
|
+
var versionsList = document.getElementById('versions-list');
|
|
1183
1209
|
var strip = document.getElementById('strip');
|
|
1184
1210
|
var level = null;
|
|
1185
1211
|
var cursor = 0;
|
|
@@ -1188,13 +1214,15 @@ const PAGE = `<!doctype html>
|
|
|
1188
1214
|
var STRIP = {
|
|
1189
1215
|
bar: ['<b>Menu</b>', 'letter to open \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1190
1216
|
view: ['Menu \\u203A <b>View</b>', 'letter to flip \\u00B7 \\u2191\\u2193 Enter \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1191
|
-
save: ['Menu \\u203A <b>
|
|
1217
|
+
save: ['Menu \\u203A <b>Export</b>', 'letter to save \\u00B7 \\u2191\\u2193 Enter \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1218
|
+
versions: ['Menu \\u203A <b>Versions</b>', '\\u2191\\u2193 Enter to put one in the editor \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1192
1219
|
theme: ['Menu \\u203A View \\u203A <b>Theme</b>', '\\u2191\\u2193 to try \\u00B7 Enter keep \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1193
1220
|
};
|
|
1194
1221
|
|
|
1195
1222
|
function rows() {
|
|
1196
1223
|
if (level === 'view') return Array.prototype.slice.call(viewList.querySelectorAll(':scope > .row'));
|
|
1197
1224
|
if (level === 'save') return Array.prototype.slice.call(saveList.querySelectorAll(':scope > .row'));
|
|
1225
|
+
if (level === 'versions') return Array.prototype.slice.call(versionsList.querySelectorAll(':scope > .row'));
|
|
1198
1226
|
if (level === 'theme') return themeItems.map(function (item) { return item.row; });
|
|
1199
1227
|
return [];
|
|
1200
1228
|
}
|
|
@@ -1225,6 +1253,9 @@ const PAGE = `<!doctype html>
|
|
|
1225
1253
|
saveList.hidden = level !== 'save';
|
|
1226
1254
|
saveButton.classList.toggle('open', level === 'save');
|
|
1227
1255
|
saveButton.setAttribute('aria-expanded', String(level === 'save'));
|
|
1256
|
+
versionsList.hidden = level !== 'versions';
|
|
1257
|
+
versionsButton.classList.toggle('open', level === 'versions');
|
|
1258
|
+
versionsButton.setAttribute('aria-expanded', String(level === 'versions'));
|
|
1228
1259
|
viewList.hidden = level !== 'view' && level !== 'theme';
|
|
1229
1260
|
viewButton.classList.toggle('open', level === 'view' || level === 'theme');
|
|
1230
1261
|
viewButton.setAttribute('aria-expanded', String(level === 'view' || level === 'theme'));
|
|
@@ -1241,7 +1272,7 @@ const PAGE = `<!doctype html>
|
|
|
1241
1272
|
|
|
1242
1273
|
function back() {
|
|
1243
1274
|
if (level === 'theme') setLevel('view');
|
|
1244
|
-
else if (level === 'view' || level === 'save') setLevel('bar');
|
|
1275
|
+
else if (level === 'view' || level === 'save' || level === 'versions') setLevel('bar');
|
|
1245
1276
|
else setLevel(null);
|
|
1246
1277
|
}
|
|
1247
1278
|
|
|
@@ -1409,9 +1440,15 @@ const PAGE = `<!doctype html>
|
|
|
1409
1440
|
return all.slice(0, count);
|
|
1410
1441
|
}
|
|
1411
1442
|
|
|
1443
|
+
// The styles the diagram uses, which a filter's style: names; made again
|
|
1444
|
+
// when an edited page redraws.
|
|
1412
1445
|
var styleNames = {};
|
|
1413
|
-
|
|
1414
|
-
|
|
1446
|
+
function collectStyles() {
|
|
1447
|
+
styleNames = {};
|
|
1448
|
+
Object.keys(graph.nodes).forEach(function (name) { graph.nodes[name].styles.forEach(function (one) { styleNames[one] = true; }); });
|
|
1449
|
+
graph.edges.forEach(function (edge) { edge.styles.forEach(function (one) { styleNames[one] = true; }); });
|
|
1450
|
+
}
|
|
1451
|
+
collectStyles();
|
|
1415
1452
|
|
|
1416
1453
|
// The filter: every word must match. Returns the matches, and a message
|
|
1417
1454
|
// when a word can match nothing at all, naming what it accepts.
|
|
@@ -1655,6 +1692,7 @@ const PAGE = `<!doctype html>
|
|
|
1655
1692
|
}
|
|
1656
1693
|
|
|
1657
1694
|
saveButton.onclick = function () { setLevel(level === 'save' ? null : 'save'); };
|
|
1695
|
+
versionsButton.onclick = function () { setLevel(level === 'versions' ? null : 'versions'); };
|
|
1658
1696
|
viewButton.onclick = function () { setLevel(level === 'view' || level === 'theme' ? null : 'view'); };
|
|
1659
1697
|
themeRow.onclick = function () { setLevel(level === 'theme' ? 'view' : 'theme'); };
|
|
1660
1698
|
document.addEventListener('click', function (event) {
|
|
@@ -1689,6 +1727,7 @@ const PAGE = `<!doctype html>
|
|
|
1689
1727
|
if (!hintKey(event)) return;
|
|
1690
1728
|
} else if (level === null) {
|
|
1691
1729
|
if (key === 'm' || key === 'M') setLevel('bar');
|
|
1730
|
+
else if (key === 'e' && EDIT) toggleEditor();
|
|
1692
1731
|
else if (key === 's') startHints('all');
|
|
1693
1732
|
else if (key === '/') startHints('all', true);
|
|
1694
1733
|
else if (key === 'n') startHints('neighbors');
|
|
@@ -1711,9 +1750,11 @@ const PAGE = `<!doctype html>
|
|
|
1711
1750
|
var letter = key.toLowerCase();
|
|
1712
1751
|
if (level === 'bar') {
|
|
1713
1752
|
if (letter === 'v') setLevel('view');
|
|
1714
|
-
else if (letter === '
|
|
1753
|
+
else if (letter === 'x') setLevel('save');
|
|
1754
|
+
else if (letter === 'o' && tiedFile()) setLevel('versions');
|
|
1755
|
+
else if (letter === 'e' && EDIT) { setLevel(null); toggleEditor(); }
|
|
1715
1756
|
else if (letter === 'f' && !full.hidden) { setLevel(null); toggleFull(); }
|
|
1716
|
-
} else if (level === 'view' || level === 'save') {
|
|
1757
|
+
} else if (level === 'view' || level === 'save' || level === 'versions') {
|
|
1717
1758
|
rows().forEach(function (row, i) {
|
|
1718
1759
|
if (row.getAttribute('data-key') !== letter) return;
|
|
1719
1760
|
cursor = i;
|
|
@@ -1737,6 +1778,793 @@ const PAGE = `<!doctype html>
|
|
|
1737
1778
|
event.preventDefault();
|
|
1738
1779
|
});
|
|
1739
1780
|
|
|
1781
|
+
/* ---- editing: a page tied to a file, and a page with no file ---- */
|
|
1782
|
+
|
|
1783
|
+
// A page with an editor draws in the browser: with the library the server
|
|
1784
|
+
// that served it also serves, or, on the hosted playground, with the
|
|
1785
|
+
// library inlined in the page. Each drawing keeps the view, the theme and
|
|
1786
|
+
// whatever is still there to be selected. A text that doesn't draw leaves
|
|
1787
|
+
// the last picture that did, with the error at the head of the findings.
|
|
1788
|
+
//
|
|
1789
|
+
// Tied to a file, the page follows it: each time the file changes on disk,
|
|
1790
|
+
// the server sends its text, and Save writes it. With no file, the text is
|
|
1791
|
+
// kept in this browser, the examples and Open are offered, and Save
|
|
1792
|
+
// downloads it; served with no file, Save names a file in the folder, and
|
|
1793
|
+
// from then on the page is tied to that file as if it had been opened with it.
|
|
1794
|
+
var heading = document.querySelector('header h1');
|
|
1795
|
+
var drawnLine = document.querySelector('header .drawn');
|
|
1796
|
+
var liveNote = document.getElementById('live');
|
|
1797
|
+
|
|
1798
|
+
function tiedFile() { return EDIT && EDIT.kind === 'file' ? EDIT.file : null; }
|
|
1799
|
+
|
|
1800
|
+
function adopt(next, title) {
|
|
1801
|
+
next.edit = EDIT;
|
|
1802
|
+
DATA = next;
|
|
1803
|
+
graph = DATA.graph;
|
|
1804
|
+
collectStyles();
|
|
1805
|
+
shownTheme = chosenName && DATA.themes[chosenName] ? chosenName : DATA.current;
|
|
1806
|
+
// A page with no file is headed reladraw, whatever the diagram is called.
|
|
1807
|
+
if (!EDIT || EDIT.kind === 'file') {
|
|
1808
|
+
heading.textContent = title;
|
|
1809
|
+
document.title = title;
|
|
1810
|
+
}
|
|
1811
|
+
themeItems[0].row.querySelector('.name').textContent = "Diagram's theme (" + DATA.current + ')';
|
|
1812
|
+
if (selection && !(selection.kind === 'node' ? graph.nodes[selection.key] : graph.edges[selection.key])) selection = null;
|
|
1813
|
+
if (level === 'pick' || level === 'filter') setLevel(null);
|
|
1814
|
+
listFindings(null);
|
|
1815
|
+
redraw();
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
var libReady = !EDIT ? Promise.resolve(null)
|
|
1819
|
+
: EDIT.kind === 'hosted' ? Promise.resolve(globalThis.reladraw)
|
|
1820
|
+
: Promise.all(['/lib/index.js', '/lib/page.js', '/lib/page-data.js'].map(function (path) { return import(path); }))
|
|
1821
|
+
.then(function (all) { return Object.assign({}, all[0], all[1], all[2]); });
|
|
1822
|
+
var lib = null;
|
|
1823
|
+
|
|
1824
|
+
function clock(at) {
|
|
1825
|
+
return ('0' + at.getHours()).slice(-2) + ':' + ('0' + at.getMinutes()).slice(-2);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function toClipboard(text) {
|
|
1829
|
+
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
|
|
1830
|
+
return Promise.reject(new Error('no clipboard'));
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
var editorPane = document.getElementById('editor');
|
|
1834
|
+
var editButton = document.getElementById('edit');
|
|
1835
|
+
var splitBar = document.getElementById('split');
|
|
1836
|
+
var sourceBox = document.getElementById('source');
|
|
1837
|
+
var paintBox = document.getElementById('paint');
|
|
1838
|
+
var unsavedMark = document.getElementById('unsaved');
|
|
1839
|
+
var writeButton = document.getElementById('write');
|
|
1840
|
+
var formatButton = document.getElementById('format');
|
|
1841
|
+
var openButton = document.getElementById('open-file');
|
|
1842
|
+
var formatOpen = document.getElementById('format-open');
|
|
1843
|
+
var browseButton = document.getElementById('browse');
|
|
1844
|
+
|
|
1845
|
+
// \`quiet\` shows it without taking the keys, as when the page opens.
|
|
1846
|
+
function toggleEditor(show, quiet) {
|
|
1847
|
+
if (!EDIT) return;
|
|
1848
|
+
var on = show === undefined ? editorPane.hidden : show;
|
|
1849
|
+
editorPane.hidden = !on;
|
|
1850
|
+
splitBar.hidden = !on;
|
|
1851
|
+
editButton.setAttribute('aria-pressed', String(on));
|
|
1852
|
+
if (on) { paintSource(); if (!quiet) sourceBox.focus(); }
|
|
1853
|
+
else if (document.activeElement === sourceBox) sourceBox.blur();
|
|
1854
|
+
}
|
|
1855
|
+
editButton.onclick = function () { toggleEditor(); };
|
|
1856
|
+
|
|
1857
|
+
function escapeCode(text) { return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
1858
|
+
// The scanner covers each line end to end, so this rebuilds the text exactly
|
|
1859
|
+
// and only wraps some of it in spans; a lost character would slide the rest
|
|
1860
|
+
// of the line out from under the cursor.
|
|
1861
|
+
function paintSource() {
|
|
1862
|
+
var text = sourceBox.value;
|
|
1863
|
+
var out = '';
|
|
1864
|
+
try {
|
|
1865
|
+
if (!lib) throw new Error('not loaded');
|
|
1866
|
+
text.split(/\\r?\\n/).forEach(function (line) {
|
|
1867
|
+
lib.highlightLine(line).forEach(function (span) {
|
|
1868
|
+
var piece = escapeCode(line.slice(span.start, span.end));
|
|
1869
|
+
out += span.kind === 'plain' ? piece : '<span class="t-' + span.kind + '">' + piece + '</span>';
|
|
1870
|
+
});
|
|
1871
|
+
out += '\\n';
|
|
1872
|
+
});
|
|
1873
|
+
} catch (e) {
|
|
1874
|
+
// Coloring is decoration; the editor has to keep working without it.
|
|
1875
|
+
out = escapeCode(text) + '\\n';
|
|
1876
|
+
}
|
|
1877
|
+
paintBox.innerHTML = out;
|
|
1878
|
+
// A textarea's scroll bar comes out of its own width, so the colored
|
|
1879
|
+
// layer is given the width the text actually gets.
|
|
1880
|
+
paintBox.style.width = sourceBox.clientWidth + 'px';
|
|
1881
|
+
paintBox.scrollTop = sourceBox.scrollTop;
|
|
1882
|
+
}
|
|
1883
|
+
sourceBox.addEventListener('scroll', function () { paintBox.scrollTop = sourceBox.scrollTop; });
|
|
1884
|
+
window.addEventListener('resize', function () { if (EDIT && !editorPane.hidden) paintSource(); });
|
|
1885
|
+
|
|
1886
|
+
// Selects a line of the source, as a finding or an error naming it does.
|
|
1887
|
+
function goToLine(number) {
|
|
1888
|
+
if (!EDIT) return;
|
|
1889
|
+
toggleEditor(true, true);
|
|
1890
|
+
var lines = sourceBox.value.split('\\n');
|
|
1891
|
+
var start = 0;
|
|
1892
|
+
for (var i = 0; i < number - 1 && i < lines.length; i++) start += lines[i].length + 1;
|
|
1893
|
+
var end = start + (lines[number - 1] || '').length;
|
|
1894
|
+
sourceBox.focus();
|
|
1895
|
+
sourceBox.setSelectionRange(start, end);
|
|
1896
|
+
// Chrome doesn't scroll a selection made from outside the field into
|
|
1897
|
+
// view; leaving and coming back is what makes it scroll.
|
|
1898
|
+
sourceBox.blur();
|
|
1899
|
+
sourceBox.focus();
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/* ---- drawing what is typed ---- */
|
|
1903
|
+
|
|
1904
|
+
// The name the diagram is drawn as, which a saved file is given.
|
|
1905
|
+
var fileName = 'diagram.reladraw';
|
|
1906
|
+
var drawnText = DATA.source;
|
|
1907
|
+
var cost = 0;
|
|
1908
|
+
var typing = null;
|
|
1909
|
+
|
|
1910
|
+
function draw(text) {
|
|
1911
|
+
if (text === drawnText || !lib) return;
|
|
1912
|
+
drawnText = text;
|
|
1913
|
+
var started = performance.now();
|
|
1914
|
+
var made;
|
|
1915
|
+
try {
|
|
1916
|
+
made = lib.pageData(text, fileName, {});
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
cost = performance.now() - started;
|
|
1919
|
+
listFindings({ message: error.message, line: error.line });
|
|
1920
|
+
if (panelOn && panel.hidden) showPanel(true);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
cost = performance.now() - started;
|
|
1924
|
+
adopt(made.data, made.title);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// A diagram quick to draw is drawn on every keystroke; a slow one once the
|
|
1928
|
+
// typing pauses, so the keys aren't held up behind it.
|
|
1929
|
+
function drawSoon() {
|
|
1930
|
+
clearTimeout(typing);
|
|
1931
|
+
if (cost < 50) draw(sourceBox.value);
|
|
1932
|
+
else typing = setTimeout(function () { draw(sourceBox.value); }, 150);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
sourceBox.addEventListener('input', function () {
|
|
1936
|
+
paintSource();
|
|
1937
|
+
edited();
|
|
1938
|
+
drawSoon();
|
|
1939
|
+
});
|
|
1940
|
+
sourceBox.addEventListener('keydown', function (event) {
|
|
1941
|
+
if (event.key === 'Escape') { sourceBox.blur(); event.preventDefault(); }
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
// Text put in whole: a version, an example, a file opened, the file as it is on disk.
|
|
1945
|
+
function load(text) {
|
|
1946
|
+
sourceBox.value = text;
|
|
1947
|
+
paintSource();
|
|
1948
|
+
edited();
|
|
1949
|
+
draw(text);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Text put in as one edit the browser can undo, with the cursor kept on its line.
|
|
1953
|
+
function replaceSource(text) {
|
|
1954
|
+
var before = sourceBox.value;
|
|
1955
|
+
var line = before.slice(0, sourceBox.selectionStart).split('\\n').length;
|
|
1956
|
+
var scroll = sourceBox.scrollTop;
|
|
1957
|
+
sourceBox.focus();
|
|
1958
|
+
sourceBox.setSelectionRange(0, before.length);
|
|
1959
|
+
var undoable = false;
|
|
1960
|
+
try { undoable = document.execCommand('insertText', false, text); } catch (e) {}
|
|
1961
|
+
if (!undoable || sourceBox.value !== text) {
|
|
1962
|
+
sourceBox.value = text;
|
|
1963
|
+
sourceBox.dispatchEvent(new Event('input'));
|
|
1964
|
+
}
|
|
1965
|
+
var lines = text.split('\\n');
|
|
1966
|
+
var at = 0;
|
|
1967
|
+
for (var i = 0; i < line - 1 && i < lines.length; i++) at += lines[i].length + 1;
|
|
1968
|
+
sourceBox.setSelectionRange(at, at);
|
|
1969
|
+
sourceBox.scrollTop = scroll;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/* ---- the divider between the source and the picture ---- */
|
|
1973
|
+
|
|
1974
|
+
// Kept as a share of the width rather than pixels, so a remembered split
|
|
1975
|
+
// means the same in a wider or narrower window.
|
|
1976
|
+
var SPLIT = 'reladraw.split';
|
|
1977
|
+
var mainBox = document.querySelector('main');
|
|
1978
|
+
function divide(share, keep) {
|
|
1979
|
+
if (share === null) {
|
|
1980
|
+
editorPane.style.removeProperty('width');
|
|
1981
|
+
editorPane.style.removeProperty('max-width');
|
|
1982
|
+
} else {
|
|
1983
|
+
share = Math.max(0.15, Math.min(0.8, share));
|
|
1984
|
+
editorPane.style.width = (share * 100).toFixed(2) + '%';
|
|
1985
|
+
editorPane.style.maxWidth = 'none';
|
|
1986
|
+
splitBar.setAttribute('aria-valuenow', String(Math.round(share * 100)));
|
|
1987
|
+
}
|
|
1988
|
+
paintSource();
|
|
1989
|
+
if (!keep) return;
|
|
1990
|
+
try {
|
|
1991
|
+
if (share === null) localStorage.removeItem(SPLIT);
|
|
1992
|
+
else localStorage.setItem(SPLIT, String(share));
|
|
1993
|
+
} catch (e) {}
|
|
1994
|
+
}
|
|
1995
|
+
function shareAt(x) {
|
|
1996
|
+
var box = mainBox.getBoundingClientRect();
|
|
1997
|
+
return (x - box.left) / box.width;
|
|
1998
|
+
}
|
|
1999
|
+
var splitting = false;
|
|
2000
|
+
splitBar.addEventListener('pointerdown', function (event) {
|
|
2001
|
+
if (event.button !== 0) return;
|
|
2002
|
+
splitting = true;
|
|
2003
|
+
try { splitBar.setPointerCapture(event.pointerId); } catch (e) {}
|
|
2004
|
+
splitBar.classList.add('dragging');
|
|
2005
|
+
document.body.classList.add('dragging');
|
|
2006
|
+
event.preventDefault();
|
|
2007
|
+
});
|
|
2008
|
+
splitBar.addEventListener('pointermove', function (event) { if (splitting) divide(shareAt(event.clientX), false); });
|
|
2009
|
+
function letGo(event) {
|
|
2010
|
+
if (!splitting) return;
|
|
2011
|
+
splitting = false;
|
|
2012
|
+
splitBar.classList.remove('dragging');
|
|
2013
|
+
document.body.classList.remove('dragging');
|
|
2014
|
+
divide(shareAt(event.clientX), true);
|
|
2015
|
+
zoom.show();
|
|
2016
|
+
}
|
|
2017
|
+
splitBar.addEventListener('pointerup', letGo);
|
|
2018
|
+
splitBar.addEventListener('pointercancel', letGo);
|
|
2019
|
+
splitBar.addEventListener('dblclick', function () { divide(null, true); });
|
|
2020
|
+
splitBar.addEventListener('keydown', function (event) {
|
|
2021
|
+
var step = { ArrowLeft: -0.02, ArrowRight: 0.02 }[event.key];
|
|
2022
|
+
if (!step) return;
|
|
2023
|
+
divide(shareAt(splitBar.getBoundingClientRect().left) + step, true);
|
|
2024
|
+
event.preventDefault();
|
|
2025
|
+
event.stopPropagation();
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
/* ---- formatting ---- */
|
|
2029
|
+
|
|
2030
|
+
// Tied to a file, the settings are the nearest .reladraw-format, as
|
|
2031
|
+
// reladraw format reads them; with no file, the ones set in this browser.
|
|
2032
|
+
function settingsText() {
|
|
2033
|
+
var file = tiedFile();
|
|
2034
|
+
if (file) return fetch('/format-settings?file=' + encodeURIComponent(file)).then(function (r) { return r.text(); });
|
|
2035
|
+
return Promise.resolve(formatText());
|
|
2036
|
+
}
|
|
2037
|
+
function formatSource() {
|
|
2038
|
+
Promise.all([libReady, settingsText()]).then(function (got) {
|
|
2039
|
+
var settings;
|
|
2040
|
+
try {
|
|
2041
|
+
settings = lib.readFormatSettings(got[1]);
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
if (tiedFile()) throw error;
|
|
2044
|
+
openFormatSettings();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
var laid = lib.format(sourceBox.value, settings);
|
|
2048
|
+
if (laid === sourceBox.value) { flash(formatButton, 'Already formatted'); return; }
|
|
2049
|
+
replaceSource(laid);
|
|
2050
|
+
}).catch(function (error) { flash(formatButton, 'Can\\u2019t format: ' + error.message.split('\\n')[0]); });
|
|
2051
|
+
}
|
|
2052
|
+
formatButton.onclick = formatSource;
|
|
2053
|
+
|
|
2054
|
+
// Save on the keyboard works wherever the focus is, and never saves the
|
|
2055
|
+
// page; Shift+Alt+F formats, as in VS Code, by the key's code, since on a
|
|
2056
|
+
// Mac Alt turns F into another character.
|
|
2057
|
+
window.addEventListener('keydown', function (event) {
|
|
2058
|
+
if (!EDIT) return;
|
|
2059
|
+
if ((event.metaKey || event.ctrlKey) && !event.altKey && (event.key === 's' || event.key === 'S')) {
|
|
2060
|
+
event.preventDefault();
|
|
2061
|
+
event.stopImmediatePropagation();
|
|
2062
|
+
saveText();
|
|
2063
|
+
} else if (event.shiftKey && event.altKey && !event.ctrlKey && !event.metaKey && event.code === 'KeyF') {
|
|
2064
|
+
event.preventDefault();
|
|
2065
|
+
event.stopImmediatePropagation();
|
|
2066
|
+
formatSource();
|
|
2067
|
+
}
|
|
2068
|
+
}, true);
|
|
2069
|
+
|
|
2070
|
+
function saveText() {
|
|
2071
|
+
if (tiedFile()) write(false);
|
|
2072
|
+
else if (EDIT.kind === 'local') openSaveAs();
|
|
2073
|
+
else saveDownload();
|
|
2074
|
+
}
|
|
2075
|
+
writeButton.onclick = saveText;
|
|
2076
|
+
|
|
2077
|
+
/* ---- a page tied to a file ---- */
|
|
2078
|
+
|
|
2079
|
+
// What the file holds, as last heard from the server; and what the
|
|
2080
|
+
// editor's text was loaded or saved from, which a save checks the file
|
|
2081
|
+
// against, so a change on disk the person kept their text over is asked
|
|
2082
|
+
// about rather than written over.
|
|
2083
|
+
var disk = null;
|
|
2084
|
+
var base = null;
|
|
2085
|
+
var versions = [];
|
|
2086
|
+
var down = false;
|
|
2087
|
+
var changedStrip = document.getElementById('changed');
|
|
2088
|
+
|
|
2089
|
+
function markUnsaved() {
|
|
2090
|
+
if (!EDIT) return;
|
|
2091
|
+
if (tiedFile()) {
|
|
2092
|
+
unsavedMark.hidden = sourceBox.value === disk;
|
|
2093
|
+
unsavedMark.title = 'Not saved to the file';
|
|
2094
|
+
} else {
|
|
2095
|
+
unsavedMark.hidden = !unsavedHere();
|
|
2096
|
+
unsavedMark.title = 'Not saved to a file';
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// Ties the page to a file whose text is \`text\`: from a served page's start,
|
|
2101
|
+
// or from the first Save of a page with no file, after which the page is
|
|
2102
|
+
// what it would have been opened with the file.
|
|
2103
|
+
function tie(file, text) {
|
|
2104
|
+
var fresh = !!EDIT && EDIT.kind !== 'file';
|
|
2105
|
+
EDIT = DATA.edit = { kind: 'file', file: file };
|
|
2106
|
+
fileName = file.split('/').pop();
|
|
2107
|
+
disk = text;
|
|
2108
|
+
base = text;
|
|
2109
|
+
browseButton.hidden = true;
|
|
2110
|
+
openButton.hidden = true;
|
|
2111
|
+
formatOpen.hidden = true;
|
|
2112
|
+
document.getElementById('versions-menu').hidden = false;
|
|
2113
|
+
writeButton.querySelector('.name').textContent = 'Save';
|
|
2114
|
+
writeButton.title = 'Write the source to the file (\\u2318S or Ctrl+S)';
|
|
2115
|
+
formatButton.title = 'Lay the source out again, as reladraw format does, with the nearest .reladraw-format (Shift+Alt+F)';
|
|
2116
|
+
editorPane.querySelector('.file').textContent = file;
|
|
2117
|
+
drawnLine.textContent = file + ' \\u00b7 live, read ' + clock(new Date());
|
|
2118
|
+
var links = document.querySelector('header .links');
|
|
2119
|
+
if (links) links.remove();
|
|
2120
|
+
if (fresh) {
|
|
2121
|
+
// Drawn again under the file's name, and headed with the diagram's title.
|
|
2122
|
+
drawnText = null;
|
|
2123
|
+
draw(sourceBox.value);
|
|
2124
|
+
}
|
|
2125
|
+
markUnsaved();
|
|
2126
|
+
follow(file);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function who(version) {
|
|
2130
|
+
if (version.who === 'you') return 'you';
|
|
2131
|
+
if (version.who === 'agent') return 'agent' + (version.note ? ': \\u201c' + version.note + '\\u201d' : '');
|
|
2132
|
+
return 'changed on disk';
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// A change on disk loads at once, unless the editor holds text that isn't
|
|
2136
|
+
// in the file: then the text stays and a strip offers the file instead.
|
|
2137
|
+
function heard(message) {
|
|
2138
|
+
var clean = sourceBox.value === disk && base === disk;
|
|
2139
|
+
disk = message.text;
|
|
2140
|
+
versions = message.versions || [];
|
|
2141
|
+
listVersions();
|
|
2142
|
+
if (clean || sourceBox.value === disk) {
|
|
2143
|
+
base = disk;
|
|
2144
|
+
changedStrip.hidden = true;
|
|
2145
|
+
load(disk);
|
|
2146
|
+
} else {
|
|
2147
|
+
var last = versions[versions.length - 1];
|
|
2148
|
+
changedStrip.querySelector('.what').textContent =
|
|
2149
|
+
fileName + ' changed on disk at ' + clock(new Date(message.at)) + (last ? ' (' + who(last) + ')' : '');
|
|
2150
|
+
changedStrip.hidden = false;
|
|
2151
|
+
if (editorPane.hidden) toggleEditor(true);
|
|
2152
|
+
}
|
|
2153
|
+
markUnsaved();
|
|
2154
|
+
}
|
|
2155
|
+
document.getElementById('load-disk').onclick = function () {
|
|
2156
|
+
base = disk;
|
|
2157
|
+
changedStrip.hidden = true;
|
|
2158
|
+
load(disk);
|
|
2159
|
+
};
|
|
2160
|
+
document.getElementById('keep-mine').onclick = function () { changedStrip.hidden = true; };
|
|
2161
|
+
|
|
2162
|
+
function write(replace) {
|
|
2163
|
+
var file = tiedFile();
|
|
2164
|
+
var text = sourceBox.value;
|
|
2165
|
+
return fetch('/save?file=' + encodeURIComponent(file), {
|
|
2166
|
+
method: 'POST',
|
|
2167
|
+
body: JSON.stringify({ text: text, base: base, replace: !!replace }),
|
|
2168
|
+
}).then(function (answer) {
|
|
2169
|
+
if (answer.status === 409) {
|
|
2170
|
+
return answer.json().then(function (now) {
|
|
2171
|
+
var what = fileName + ' changed on disk at ' + clock(new Date(now.at)) +
|
|
2172
|
+
(now.who === 'agent' ? ' (agent' + (now.note ? ': \\u201c' + now.note + '\\u201d' : '') + ')' : '') +
|
|
2173
|
+
' after you started editing. Saving puts your text in its place; the version on disk stays in Versions.\\n\\nReplace the file on disk?';
|
|
2174
|
+
if (confirm(what)) return write(true);
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
if (!answer.ok) throw new Error('the server said ' + answer.status);
|
|
2178
|
+
disk = text;
|
|
2179
|
+
base = text;
|
|
2180
|
+
changedStrip.hidden = true;
|
|
2181
|
+
markUnsaved();
|
|
2182
|
+
flash(writeButton, 'Saved');
|
|
2183
|
+
}).catch(function (error) {
|
|
2184
|
+
flash(writeButton, 'Not saved');
|
|
2185
|
+
liveNote.textContent = 'Couldn\\u2019t save ' + file + ': ' + error.message;
|
|
2186
|
+
liveNote.hidden = false;
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Newest first. Picking one puts it in the editor as unsaved text.
|
|
2191
|
+
function listVersions() {
|
|
2192
|
+
versionsList.textContent = '';
|
|
2193
|
+
versions.slice().reverse().forEach(function (version, i) {
|
|
2194
|
+
var row = element('button', 'row');
|
|
2195
|
+
row.type = 'button';
|
|
2196
|
+
row.setAttribute('role', 'menuitem');
|
|
2197
|
+
var label = version.who === 'opened' ? 'opened' : version.who === 'disk' ? 'on disk' : version.who;
|
|
2198
|
+
var about = version.who === 'opened' ? 'as the server first read it'
|
|
2199
|
+
: version.who === 'disk' ? 'changed on disk'
|
|
2200
|
+
: version.who === 'agent' ? (version.note || 'changed by an agent')
|
|
2201
|
+
: 'saved from this page';
|
|
2202
|
+
row.append(element('span', 'when', clock(new Date(version.at))), element('span', 'who ' + version.who, label),
|
|
2203
|
+
element('span', 'about', about + (i === 0 ? ' (in the file now)' : '')));
|
|
2204
|
+
row.onclick = function () {
|
|
2205
|
+
setLevel(null);
|
|
2206
|
+
toggleEditor(true);
|
|
2207
|
+
load(version.text);
|
|
2208
|
+
};
|
|
2209
|
+
versionsList.append(row);
|
|
2210
|
+
});
|
|
2211
|
+
if (!versions.length) versionsList.append(element('span', 'none', 'No versions yet.'));
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function follow(file) {
|
|
2215
|
+
var events = new EventSource('/events?file=' + encodeURIComponent(file));
|
|
2216
|
+
events.addEventListener('source', function (event) {
|
|
2217
|
+
var message = JSON.parse(event.data);
|
|
2218
|
+
if (down) {
|
|
2219
|
+
down = false;
|
|
2220
|
+
liveNote.hidden = true;
|
|
2221
|
+
writeButton.disabled = false;
|
|
2222
|
+
}
|
|
2223
|
+
drawnLine.textContent = file + ' \\u00b7 live, ' + (message.changed ? 'changed ' : 'read ') + clock(new Date(message.at));
|
|
2224
|
+
heard(message);
|
|
2225
|
+
});
|
|
2226
|
+
events.onerror = function () {
|
|
2227
|
+
if (down) return;
|
|
2228
|
+
down = true;
|
|
2229
|
+
writeButton.disabled = true;
|
|
2230
|
+
drawnLine.textContent = file + ' \\u00b7 not live';
|
|
2231
|
+
liveNote.textContent = 'The server has stopped, so this page can\\u2019t save or follow ' + file +
|
|
2232
|
+
'. Start it again with reladraw serve ' + file + ' in the same folder, and this page picks the file up again. Export \\u25BE still saves what you typed.';
|
|
2233
|
+
liveNote.hidden = false;
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
window.addEventListener('beforeunload', function (event) {
|
|
2238
|
+
if (tiedFile() && sourceBox.value !== disk) { event.preventDefault(); event.returnValue = ''; }
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
/* ---- a page with no file: the text kept in this browser ---- */
|
|
2242
|
+
|
|
2243
|
+
// The same names the playground has always kept them under, so a visitor
|
|
2244
|
+
// finds their diagram where they left it.
|
|
2245
|
+
var STORE = 'reladraw.source';
|
|
2246
|
+
var KEPT_TEXT = 'reladraw.kept';
|
|
2247
|
+
var KEPT_NAME = 'reladraw.name';
|
|
2248
|
+
function stored(key) { try { return localStorage.getItem(key); } catch (e) { return null; } }
|
|
2249
|
+
function store(key, value) { try { localStorage.setItem(key, value); } catch (e) {} }
|
|
2250
|
+
|
|
2251
|
+
// The text as last saved or opened, so opening another file asks first only
|
|
2252
|
+
// when there is something to lose; and the example the editor holds
|
|
2253
|
+
// unchanged, or null once it is edited, since an example left alone is one
|
|
2254
|
+
// click away and nothing to lose.
|
|
2255
|
+
var keptText = null;
|
|
2256
|
+
var current = null;
|
|
2257
|
+
var stowing = null;
|
|
2258
|
+
var EXAMPLES = EDIT && EDIT.examples ? EDIT.examples : [];
|
|
2259
|
+
|
|
2260
|
+
function named(text) {
|
|
2261
|
+
for (var i = 0; i < EXAMPLES.length; i++) if (EXAMPLES[i].source === text) return EXAMPLES[i].name;
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
function unsavedHere() { return sourceBox.value !== keptText && current === null; }
|
|
2265
|
+
|
|
2266
|
+
function edited() {
|
|
2267
|
+
if (EDIT && EDIT.kind !== 'file') {
|
|
2268
|
+
current = named(sourceBox.value);
|
|
2269
|
+
// The stored copy trails the typing: it isn't read until the page is opened again.
|
|
2270
|
+
clearTimeout(stowing);
|
|
2271
|
+
stowing = setTimeout(function () { store(STORE, sourceBox.value); }, 400);
|
|
2272
|
+
}
|
|
2273
|
+
markUnsaved();
|
|
2274
|
+
}
|
|
2275
|
+
function keep(text, name) {
|
|
2276
|
+
keptText = text;
|
|
2277
|
+
fileName = name;
|
|
2278
|
+
store(KEPT_TEXT, text);
|
|
2279
|
+
store(KEPT_NAME, name);
|
|
2280
|
+
if (EDIT.kind === 'hosted') editorPane.querySelector('.file').textContent = name;
|
|
2281
|
+
markUnsaved();
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// A different diagram opens as a fresh page would, not at the last one's zoom.
|
|
2285
|
+
function openText(text) {
|
|
2286
|
+
load(text);
|
|
2287
|
+
zoom.open();
|
|
2288
|
+
sourceBox.focus();
|
|
2289
|
+
sourceBox.setSelectionRange(0, 0);
|
|
2290
|
+
sourceBox.scrollTop = 0;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
var fileInput = document.getElementById('file-input');
|
|
2294
|
+
openButton.onclick = function () {
|
|
2295
|
+
if (unsavedHere() && !confirm('Your edits have not been saved to a file. Open another file in their place?')) return;
|
|
2296
|
+
fileInput.value = '';
|
|
2297
|
+
fileInput.click();
|
|
2298
|
+
};
|
|
2299
|
+
fileInput.onchange = function () {
|
|
2300
|
+
var picked = fileInput.files[0];
|
|
2301
|
+
if (!picked) return;
|
|
2302
|
+
picked.text().then(function (text) {
|
|
2303
|
+
keep(text, picked.name);
|
|
2304
|
+
openText(text);
|
|
2305
|
+
});
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
// Chrome and Edge ask for the name and the folder; elsewhere the file goes
|
|
2309
|
+
// where the browser puts downloads, under the suggested name.
|
|
2310
|
+
function saveDownload() {
|
|
2311
|
+
var text = sourceBox.value;
|
|
2312
|
+
function download() {
|
|
2313
|
+
saveBlob(new Blob([text], { type: 'text/plain' }), fileName);
|
|
2314
|
+
keep(text, fileName);
|
|
2315
|
+
}
|
|
2316
|
+
if (!window.showSaveFilePicker) return download();
|
|
2317
|
+
window.showSaveFilePicker({ suggestedName: fileName, types: [{ description: 'reladraw source', accept: { 'text/plain': ['.reladraw'] } }] })
|
|
2318
|
+
.then(function (handle) {
|
|
2319
|
+
return handle.createWritable().then(function (out) {
|
|
2320
|
+
return out.write(text).then(function () { return out.close(); }).then(function () {
|
|
2321
|
+
keep(text, handle.name);
|
|
2322
|
+
flash(writeButton, 'Saved');
|
|
2323
|
+
});
|
|
2324
|
+
});
|
|
2325
|
+
})
|
|
2326
|
+
.catch(function (e) {
|
|
2327
|
+
if (e && e.name === 'AbortError') return;
|
|
2328
|
+
// Refused outright, as inside a cross-origin frame.
|
|
2329
|
+
download();
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// Served with no file, the first Save names a file in the folder, typed
|
|
2334
|
+
// rather than browsed for, and asks before replacing one that is there.
|
|
2335
|
+
var saveAs = document.getElementById('save-as');
|
|
2336
|
+
function openSaveAs() {
|
|
2337
|
+
var path = document.getElementById('save-path');
|
|
2338
|
+
document.getElementById('save-problem').textContent = '';
|
|
2339
|
+
if (!path.value) path.value = fileName;
|
|
2340
|
+
saveAs.showModal();
|
|
2341
|
+
path.select();
|
|
2342
|
+
}
|
|
2343
|
+
function create(path, replace) {
|
|
2344
|
+
var text = sourceBox.value;
|
|
2345
|
+
var problem = document.getElementById('save-problem');
|
|
2346
|
+
return fetch('/create?file=' + encodeURIComponent(path), {
|
|
2347
|
+
method: 'POST',
|
|
2348
|
+
body: JSON.stringify({ text: text, replace: !!replace }),
|
|
2349
|
+
}).then(function (answer) {
|
|
2350
|
+
return answer.json().then(function (told) {
|
|
2351
|
+
if (answer.status === 409 && told.exists) {
|
|
2352
|
+
if (confirm(told.file + ' is there already. Replace it with this diagram?')) return create(path, true);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (!answer.ok) { problem.textContent = told.error || 'The server said ' + answer.status; return; }
|
|
2356
|
+
saveAs.close();
|
|
2357
|
+
keep(text, told.file.split('/').pop());
|
|
2358
|
+
history.replaceState(null, '', '/' + told.file.split('/').map(encodeURIComponent).join('/'));
|
|
2359
|
+
tie(told.file, text);
|
|
2360
|
+
flash(writeButton, 'Saved');
|
|
2361
|
+
});
|
|
2362
|
+
}).catch(function (error) {
|
|
2363
|
+
problem.textContent = 'Couldn\\u2019t save: ' + error.message + '. Is reladraw serve still running?';
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
if (saveAs) {
|
|
2367
|
+
document.getElementById('save-form').addEventListener('submit', function (event) {
|
|
2368
|
+
event.preventDefault();
|
|
2369
|
+
var path = document.getElementById('save-path').value.trim();
|
|
2370
|
+
if (!path) return;
|
|
2371
|
+
if (!/\\.reladraw$/.test(path)) path += '.reladraw';
|
|
2372
|
+
create(path, false);
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
/* ---- the examples ---- */
|
|
2377
|
+
|
|
2378
|
+
var examplesBox = document.getElementById('examples');
|
|
2379
|
+
var catalog = document.getElementById('catalog');
|
|
2380
|
+
var preview = document.getElementById('preview');
|
|
2381
|
+
var aboutLine = document.getElementById('about');
|
|
2382
|
+
var chosenExample = 0;
|
|
2383
|
+
var previews = {};
|
|
2384
|
+
|
|
2385
|
+
function pickExample(i) {
|
|
2386
|
+
chosenExample = Math.max(0, Math.min(EXAMPLES.length - 1, i));
|
|
2387
|
+
each(catalog.children, function (item, j) { item.setAttribute('aria-selected', String(j === chosenExample)); });
|
|
2388
|
+
var item = catalog.children[chosenExample];
|
|
2389
|
+
catalog.setAttribute('aria-activedescendant', item.id);
|
|
2390
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
2391
|
+
var example = EXAMPLES[chosenExample];
|
|
2392
|
+
if (!(example.name in previews)) {
|
|
2393
|
+
try { previews[example.name] = lib ? lib.compile(example.source) : ''; } catch (e) { previews[example.name] = ''; }
|
|
2394
|
+
}
|
|
2395
|
+
aboutLine.textContent = example.about || '';
|
|
2396
|
+
preview.innerHTML = previews[example.name];
|
|
2397
|
+
}
|
|
2398
|
+
function openExample() {
|
|
2399
|
+
var example = EXAMPLES[chosenExample];
|
|
2400
|
+
examplesBox.close();
|
|
2401
|
+
if (unsavedHere() && !confirm('Your edits have not been saved to a file. Open ' + example.name + ' in their place?')) return;
|
|
2402
|
+
openText(example.source);
|
|
2403
|
+
}
|
|
2404
|
+
// A click on the backdrop lands on the dialog itself, outside its box.
|
|
2405
|
+
function closeOnBackdrop(box) {
|
|
2406
|
+
box.addEventListener('click', function (event) {
|
|
2407
|
+
if (event.target !== box) return;
|
|
2408
|
+
var r = box.getBoundingClientRect();
|
|
2409
|
+
if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) box.close();
|
|
2410
|
+
});
|
|
2411
|
+
each(box.querySelectorAll('[data-close]'), function (button) { button.onclick = function () { box.close(); }; });
|
|
2412
|
+
}
|
|
2413
|
+
if (examplesBox) {
|
|
2414
|
+
EXAMPLES.forEach(function (example, i) {
|
|
2415
|
+
var item = element('li', '', example.name);
|
|
2416
|
+
item.id = 'example-' + i;
|
|
2417
|
+
item.setAttribute('role', 'option');
|
|
2418
|
+
item.onclick = function () { pickExample(i); };
|
|
2419
|
+
item.ondblclick = openExample;
|
|
2420
|
+
catalog.append(item);
|
|
2421
|
+
});
|
|
2422
|
+
browseButton.onclick = function () {
|
|
2423
|
+
var at = 0;
|
|
2424
|
+
EXAMPLES.forEach(function (example, i) { if (example.name === current) at = i; });
|
|
2425
|
+
examplesBox.showModal();
|
|
2426
|
+
pickExample(at);
|
|
2427
|
+
catalog.focus();
|
|
2428
|
+
};
|
|
2429
|
+
catalog.addEventListener('keydown', function (event) {
|
|
2430
|
+
var step = { ArrowDown: 1, ArrowUp: -1, PageDown: 5, PageUp: -5 }[event.key];
|
|
2431
|
+
if (step) pickExample(chosenExample + step);
|
|
2432
|
+
else if (event.key === 'Home') pickExample(0);
|
|
2433
|
+
else if (event.key === 'End') pickExample(EXAMPLES.length - 1);
|
|
2434
|
+
else if (event.key === 'Enter') openExample();
|
|
2435
|
+
else return;
|
|
2436
|
+
event.preventDefault();
|
|
2437
|
+
});
|
|
2438
|
+
document.getElementById('choose').onclick = openExample;
|
|
2439
|
+
closeOnBackdrop(examplesBox);
|
|
2440
|
+
// Every drawing names its arrowheads alike, and a page takes the first of
|
|
2441
|
+
// a name it finds: a preview left in the closed dialog would take the
|
|
2442
|
+
// diagram's arrowheads with it.
|
|
2443
|
+
examplesBox.addEventListener('close', function () { preview.innerHTML = ''; });
|
|
2444
|
+
closeOnBackdrop(saveAs);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/* ---- format settings, for a page with no file ---- */
|
|
2448
|
+
|
|
2449
|
+
// The settings as \`name: value\` lines, the way a .reladraw-format holds
|
|
2450
|
+
// them, so the panel, the stored copy and the copied file are one text and
|
|
2451
|
+
// the library's own reader refuses what it would refuse in a file.
|
|
2452
|
+
var FORMAT = 'reladraw.format';
|
|
2453
|
+
var formatPanel = document.getElementById('format-settings');
|
|
2454
|
+
var formatValues = {};
|
|
2455
|
+
(stored(FORMAT) || '').split('\\n').forEach(function (line) {
|
|
2456
|
+
var colon = line.indexOf(':');
|
|
2457
|
+
if (colon > 0) formatValues[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
|
|
2458
|
+
});
|
|
2459
|
+
function formatText() {
|
|
2460
|
+
if (!lib) return '';
|
|
2461
|
+
return lib.SETTINGS.map(function (setting) {
|
|
2462
|
+
var value = formatValues[setting.name];
|
|
2463
|
+
return setting.name + ': ' + (value === undefined || value === '' ? setting.written : value);
|
|
2464
|
+
}).join('\\n');
|
|
2465
|
+
}
|
|
2466
|
+
function checkSettings() {
|
|
2467
|
+
var problem = document.getElementById('format-problem');
|
|
2468
|
+
try { lib.readFormatSettings(formatText()); problem.textContent = ''; } catch (e) { problem.textContent = e.message; }
|
|
2469
|
+
}
|
|
2470
|
+
// A setting whose values are all words gets a list; one that takes a
|
|
2471
|
+
// number or a list gets a field, written as it would be in the file.
|
|
2472
|
+
function buildFormatFields() {
|
|
2473
|
+
var fields = document.getElementById('format-fields');
|
|
2474
|
+
fields.textContent = '';
|
|
2475
|
+
lib.SETTINGS.forEach(function (setting) {
|
|
2476
|
+
var label = element('label', '');
|
|
2477
|
+
label.append(element('code', '', setting.name));
|
|
2478
|
+
var words = setting.values.every(function (value) { return /^[a-z-]+$/.test(value[0]); });
|
|
2479
|
+
var control;
|
|
2480
|
+
if (words) {
|
|
2481
|
+
control = element('select', '');
|
|
2482
|
+
setting.values.forEach(function (value) {
|
|
2483
|
+
var option = element('option', '', value[0]);
|
|
2484
|
+
option.value = value[0];
|
|
2485
|
+
control.append(option);
|
|
2486
|
+
});
|
|
2487
|
+
} else {
|
|
2488
|
+
control = element('input', '');
|
|
2489
|
+
control.type = 'text';
|
|
2490
|
+
control.spellcheck = false;
|
|
2491
|
+
control.placeholder = setting.written;
|
|
2492
|
+
}
|
|
2493
|
+
control.value = formatValues[setting.name] || setting.written;
|
|
2494
|
+
control.addEventListener(words ? 'change' : 'input', function () {
|
|
2495
|
+
formatValues[setting.name] = control.value.trim();
|
|
2496
|
+
checkSettings();
|
|
2497
|
+
store(FORMAT, formatText());
|
|
2498
|
+
});
|
|
2499
|
+
label.append(control, element('span', '', setting.about.replace(/\\n/g, ' ') + ' ' +
|
|
2500
|
+
setting.values.map(function (value) { return value[0] + ': ' + value[1].replace(/\\n/g, ' '); }).join('; ') + '.'));
|
|
2501
|
+
fields.append(label);
|
|
2502
|
+
});
|
|
2503
|
+
checkSettings();
|
|
2504
|
+
}
|
|
2505
|
+
function openFormatSettings() {
|
|
2506
|
+
libReady.then(function () {
|
|
2507
|
+
buildFormatFields();
|
|
2508
|
+
formatPanel.showModal();
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
if (formatPanel) {
|
|
2512
|
+
formatOpen.onclick = openFormatSettings;
|
|
2513
|
+
closeOnBackdrop(formatPanel);
|
|
2514
|
+
document.getElementById('format-reset').onclick = function () {
|
|
2515
|
+
formatValues = {};
|
|
2516
|
+
store(FORMAT, formatText());
|
|
2517
|
+
buildFormatFields();
|
|
2518
|
+
};
|
|
2519
|
+
var formatCopy = document.getElementById('format-copy');
|
|
2520
|
+
formatCopy.onclick = function () {
|
|
2521
|
+
var text = '// .reladraw-format: how reladraw format lays out the diagrams in this folder and below\\n' + formatText() + '\\n';
|
|
2522
|
+
toClipboard(text).then(function () { flash(formatCopy, 'Copied'); }, function () { flash(formatCopy, 'Blocked'); });
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
/* ---- the start ---- */
|
|
2527
|
+
|
|
2528
|
+
if (EDIT) {
|
|
2529
|
+
editButton.hidden = false;
|
|
2530
|
+
document.getElementById('edit-gap').hidden = false;
|
|
2531
|
+
try {
|
|
2532
|
+
var share = parseFloat(localStorage.getItem(SPLIT));
|
|
2533
|
+
if (share > 0 && share < 1) divide(share, false);
|
|
2534
|
+
} catch (e) {}
|
|
2535
|
+
if (EDIT.kind === 'file') {
|
|
2536
|
+
sourceBox.value = DATA.source;
|
|
2537
|
+
tie(EDIT.file, DATA.source);
|
|
2538
|
+
} else {
|
|
2539
|
+
browseButton.hidden = false;
|
|
2540
|
+
openButton.hidden = false;
|
|
2541
|
+
formatOpen.hidden = false;
|
|
2542
|
+
writeButton.querySelector('.name').textContent = 'Save\\u2026';
|
|
2543
|
+
writeButton.title = EDIT.kind === 'local'
|
|
2544
|
+
? 'Save the source as a file in ' + EDIT.folder + ' (\\u2318S or Ctrl+S)'
|
|
2545
|
+
: 'Save the source as a .reladraw file (\\u2318S or Ctrl+S)';
|
|
2546
|
+
formatButton.title = 'Lay the source out again (Shift+Alt+F)';
|
|
2547
|
+
if (saveAs) saveAs.querySelector('.folder').textContent = EDIT.folder || '';
|
|
2548
|
+
keptText = stored(KEPT_TEXT);
|
|
2549
|
+
fileName = stored(KEPT_NAME) || fileName;
|
|
2550
|
+
editorPane.querySelector('.file').textContent = EDIT.kind === 'local' ? 'not saved to a file' : fileName;
|
|
2551
|
+
var opening = stored(STORE);
|
|
2552
|
+
sourceBox.value = opening !== null ? opening : DATA.source;
|
|
2553
|
+
current = named(sourceBox.value);
|
|
2554
|
+
// The playground opens with the source shown; a file's page with it hidden.
|
|
2555
|
+
toggleEditor(true, true);
|
|
2556
|
+
markUnsaved();
|
|
2557
|
+
}
|
|
2558
|
+
libReady.then(function (loaded) {
|
|
2559
|
+
lib = loaded;
|
|
2560
|
+
paintSource();
|
|
2561
|
+
draw(sourceBox.value);
|
|
2562
|
+
}, function (error) {
|
|
2563
|
+
liveNote.textContent = 'The page couldn\\u2019t load the library it draws with: ' + error.message;
|
|
2564
|
+
liveNote.hidden = false;
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
|
|
1740
2568
|
layoutSide();
|
|
1741
2569
|
zoom.show();
|
|
1742
2570
|
})();
|
|
@@ -1744,3 +2572,173 @@ const PAGE = `<!doctype html>
|
|
|
1744
2572
|
</body>
|
|
1745
2573
|
</html>
|
|
1746
2574
|
`;
|
|
2575
|
+
/** What a page with an editor shows under its source: the language on one screen. */
|
|
2576
|
+
const CHEATSHEET = `
|
|
2577
|
+
<details id="cheatsheet">
|
|
2578
|
+
<summary>Cheatsheet</summary>
|
|
2579
|
+
<dl>
|
|
2580
|
+
<dt>Statements</dt>
|
|
2581
|
+
<dd>
|
|
2582
|
+
<code>node <i>name</i> <i>["text"]</i> <i>[placements]</i> <i>[key: value …]</i></code><br />
|
|
2583
|
+
<code>edge <i>a</i> -> <i>b</i> <i>["text"]</i> <i>[key: value …]</i></code>
|
|
2584
|
+
<i>(<code><-</code> to write the target first, <code><-></code> for both ends)</i><br />
|
|
2585
|
+
<code>point <i>name</i> <i>where</i></code> <code>region <i>name</i> <i>a</i> top-left to <i>b</i> bottom-right</code><br />
|
|
2586
|
+
<code>bundle <i>name</i> via: <i>…</i></code> <i>(a path lines share)</i>
|
|
2587
|
+
<code><b>guide</b> node|edge|point|region …</code> <i>(reference only; turn on Guides in View to see them)</i><br />
|
|
2588
|
+
<code>style <i>name</i> <i>key: value …</i></code><br />
|
|
2589
|
+
<code>diagram theme: nord background: #1e2229 text: (color: #e0e0e0)</code><br />
|
|
2590
|
+
<code>default node|leaf|container|edge <i>key: value …</i></code>
|
|
2591
|
+
<i>(a style every one of that kind wears; the more specific wins)</i><br />
|
|
2592
|
+
<i>The picture is drawn exactly as written: nothing is moved to make room, and lines go only
|
|
2593
|
+
where they are told. Anything that overlaps is drawn as written; the findings beside the diagram report it, as <code>reladraw check</code> does.</i>
|
|
2594
|
+
</dd>
|
|
2595
|
+
|
|
2596
|
+
<dt>Labels</dt>
|
|
2597
|
+
<dd>
|
|
2598
|
+
<code>node parser</code> — no text, so the node takes its name as its text.<br />
|
|
2599
|
+
<code>node parser ""</code> — deliberately blank.<br />
|
|
2600
|
+
<code>"two / lines"</code> — <code> / </code> with spaces either side breaks a line.
|
|
2601
|
+
</dd>
|
|
2602
|
+
|
|
2603
|
+
<dt>Containment</dt>
|
|
2604
|
+
<dd>
|
|
2605
|
+
<code>node server.docker "Docker"</code> — a dotted name is a child. Declare the parent first. With no text of its own, it reads "docker".<br />
|
|
2606
|
+
<code>layout: column</code> or <code>row</code> — the children in written order, with
|
|
2607
|
+
<code>(gap: tight, widths: match, align-children: left)</code>.
|
|
2608
|
+
Without a layout, one child is unplaced and the rest are placed against it.<br />
|
|
2609
|
+
<code>layout: deck</code> — a stack of cards, the first child in front.<br />
|
|
2610
|
+
<code>gather-children: left</code> — where the children sit in spare room.
|
|
2611
|
+
<code>shape: none</code> on a container draws no box.
|
|
2612
|
+
</dd>
|
|
2613
|
+
|
|
2614
|
+
<dt>Placement</dt>
|
|
2615
|
+
<dd>
|
|
2616
|
+
<code><b>above</b> X</code> <code><b>below</b> X</code>
|
|
2617
|
+
<code><b>left of</b> X</code> <code><b>right of</b> X</code>
|
|
2618
|
+
<code><b>above-left of</b> X</code> … <code><b>below-right of</b> X</code><br />
|
|
2619
|
+
<code><b>even with</b> X</code> — share a center line;
|
|
2620
|
+
<code><b>top</b>|<b>bottom</b>|<b>left</b>|<b>right</b> <b>even with</b> X</code> shares that edge.<br />
|
|
2621
|
+
<code>right of A <b>and</b> B</code> — against the box that just bounds them both.<br />
|
|
2622
|
+
<code><b>inside</b> X top-right</code> <code><b>outside</b> X right</code>
|
|
2623
|
+
<code><b>on</b> X bottom-left</code> — against a part of X. The parts are
|
|
2624
|
+
<code>text</code>, the sides <code>top</code> <code>bottom</code> <code>left</code> <code>right</code>,
|
|
2625
|
+
and <code>top-left</code> <code>top-center</code> <code>top-right</code>
|
|
2626
|
+
<code>left-center</code> <code>center</code> <code>right-center</code>
|
|
2627
|
+
<code>bottom-left</code> <code>bottom-center</code> <code>bottom-right</code>.<br />
|
|
2628
|
+
<code><b>on</b> between A and B</code> <code>below down from A and left from B</code> — at a point.<br />
|
|
2629
|
+
<i>One node is unplaced; every other is placed against something. One direction on its own
|
|
2630
|
+
also centers on its target. Each axis is said once.</i>
|
|
2631
|
+
</dd>
|
|
2632
|
+
|
|
2633
|
+
<dt>Gaps</dt>
|
|
2634
|
+
<dd>
|
|
2635
|
+
<code>right of X <b>(gap: wide)</b></code> or <code><b>2</b> right of X</code>, on the placement.
|
|
2636
|
+
One of <code>none</code> <code>tight</code> <code>normal</code> <code>wide</code> (0, 2, 4, 8 units),
|
|
2637
|
+
or a number of units; a box is about 4 units tall. A gap is <i>exact</i>.
|
|
2638
|
+
</dd>
|
|
2639
|
+
|
|
2640
|
+
<dt>Sizes</dt>
|
|
2641
|
+
<dd>
|
|
2642
|
+
<code>width: 30</code> <code>height: menu</code> <code>height: 0.5 menu height</code>
|
|
2643
|
+
<code>height: a top to c bottom</code> <code>min-width: 10</code>
|
|
2644
|
+
</dd>
|
|
2645
|
+
|
|
2646
|
+
<dt>Appearance</dt>
|
|
2647
|
+
<dd>
|
|
2648
|
+
<code>style: <i>name</i></code>
|
|
2649
|
+
<code>fill: #191728</code>
|
|
2650
|
+
<code>border: none</code>
|
|
2651
|
+
<code>corners: sharp</code> <i>(a box; rounded unless told)</i>
|
|
2652
|
+
<code>line: #d2904e</code> <i>(an edge)</i>
|
|
2653
|
+
<code>overlap: allow</code>
|
|
2654
|
+
<code>url: "https://example.com"</code>
|
|
2655
|
+
<code>description: "What it does"</code> <i>(shown on the page; never drawn)</i>
|
|
2656
|
+
</dd>
|
|
2657
|
+
|
|
2658
|
+
<dt>Lines</dt>
|
|
2659
|
+
<dd>
|
|
2660
|
+
<code>line: (path: square, corners: rounded)</code> — everything about an edge's line.
|
|
2661
|
+
<code>path:</code> takes <code>straight</code> (the default) <code>square</code> <code>curved</code>;
|
|
2662
|
+
<code>corners:</code> <code>sharp</code> <code>rounded</code>;
|
|
2663
|
+
<code>crossing:</code> <code>gap</code> <code>arc</code> <code>square</code> <code>none</code>;
|
|
2664
|
+
<code>pattern:</code> <code>solid</code> <code>dashed</code> <code>dotted</code> <code>dash-dot</code>;
|
|
2665
|
+
<code>thickness:</code> <code>thin</code> <code>normal</code> <code>thick</code>, a multiple, or <code>3px</code>;
|
|
2666
|
+
<code>turn: 1/3</code> <code>step-out: tight</code> <code>curve: subtle</code>.
|
|
2667
|
+
<i><code>default edge line: (path: square)</code> sets every edge.</i>
|
|
2668
|
+
</dd>
|
|
2669
|
+
|
|
2670
|
+
<dt>Body</dt>
|
|
2671
|
+
<dd>
|
|
2672
|
+
<code>shape: rectangle|document|circle|none</code> — the outline the node is drawn with;
|
|
2673
|
+
<code>none</code> is text with no box, which is what an annotation is.<br />
|
|
2674
|
+
<code>icon: <i>name</i></code> — the node <i>is</i> that picture, with no box.<br />
|
|
2675
|
+
<code>badge: <i>name</i></code> — the picture is part of the label, beside the text, and the node keeps its box.<br />
|
|
2676
|
+
<code>layout: deck cards: "text" <i>["text" …]</i></code> — a stack of cards, front first, one per text.<br />
|
|
2677
|
+
Pictures: <code>disk</code> <code>desktop</code> <code>laptop</code>
|
|
2678
|
+
<code>package</code> <code>cubes</code> <code>cube</code> <code>database</code>.<br />
|
|
2679
|
+
Your own: <code>icon <i>name</i> """<svg …>…</svg>"""</code> declares one,
|
|
2680
|
+
and <code>icon: <i>name</i></code> or <code>badge: <i>name</i></code> uses it.
|
|
2681
|
+
</dd>
|
|
2682
|
+
|
|
2683
|
+
<dt>Text</dt>
|
|
2684
|
+
<dd>
|
|
2685
|
+
Everything a text says about itself goes in brackets after it:
|
|
2686
|
+
<code>(color: theme-muted)</code>
|
|
2687
|
+
<code>(size: small|normal|large)</code>
|
|
2688
|
+
<code>(wrap: 24)</code> <i>(characters, folds the text)</i>.
|
|
2689
|
+
A title says which side it sits against: <code>"Server" (inside top, align: left)</code>,
|
|
2690
|
+
<code>(on top)</code>, <code>(outside top)</code>, <code>(center)</code>.
|
|
2691
|
+
In a style, which has no text of its own, the bracket hangs off the key:
|
|
2692
|
+
<code>text: (color: theme-muted)</code>.<br />
|
|
2693
|
+
<code>" / "</code> in any text is a line break; <code>\\/</code> is a literal slash.<br />
|
|
2694
|
+
<code>"name / <b>[dim]</b>quieter<b>[/dim]</b>"</code> draws those words in the
|
|
2695
|
+
text color of <code>style dim</code>; <code>\\[</code> is a literal bracket.
|
|
2696
|
+
</dd>
|
|
2697
|
+
|
|
2698
|
+
<dt>Links</dt>
|
|
2699
|
+
<dd>
|
|
2700
|
+
A line is straight between its two boxes, through anything in between, unless told otherwise.<br />
|
|
2701
|
+
<code>from: right</code> <code>to: left</code> name a side;
|
|
2702
|
+
<code>to: bottom-center</code> or <code>to: left 3/4 down</code> a point on it.<br />
|
|
2703
|
+
<code>via: c top, between a and b</code> — the places the line passes, in order:
|
|
2704
|
+
just outside a side, a point, a box's center, <code>through <i>region</i> horizontal</code>.<br />
|
|
2705
|
+
<code>"text" (at: start)</code> <code>(at: 1/4)</code> <code>(above edge)</code>
|
|
2706
|
+
<code>(on: longest level)</code> — where a line's text sits; the middle by default.
|
|
2707
|
+
</dd>
|
|
2708
|
+
|
|
2709
|
+
<dt>Comments</dt>
|
|
2710
|
+
<dd><code>// to the end of the line</code>, anywhere, including after a statement.</dd>
|
|
2711
|
+
</dl>
|
|
2712
|
+
</details>
|
|
2713
|
+
`;
|
|
2714
|
+
/** Where a page with no file sends someone who wants more than the cheatsheet. */
|
|
2715
|
+
const LINKS = `<nav class="links"><a href="https://github.com/reladraw/reladraw">GitHub</a><a href="https://github.com/reladraw/reladraw/blob/main/SYNTAX.md">Syntax</a></nav>`;
|
|
2716
|
+
/**
|
|
2717
|
+
* A page with no file's dialogs: the examples, the format settings, which a
|
|
2718
|
+
* file's page takes from its nearest .reladraw-format instead, and, served,
|
|
2719
|
+
* the name of the file a first Save writes.
|
|
2720
|
+
*/
|
|
2721
|
+
const DIALOGS = `
|
|
2722
|
+
<dialog id="examples" aria-label="Examples">
|
|
2723
|
+
<div class="bar"><h2>Examples</h2><button type="button" data-close>Close</button></div>
|
|
2724
|
+
<ul id="catalog" role="listbox" tabindex="0" aria-label="Examples"></ul>
|
|
2725
|
+
<p id="about" aria-live="polite"></p>
|
|
2726
|
+
<div id="preview"></div>
|
|
2727
|
+
<footer><span class="keys"><kbd>↑</kbd> <kbd>↓</kbd> to preview · <kbd>Enter</kbd> to open · <kbd>Esc</kbd> to close</span><span class="gap"></span><button type="button" id="choose">Open in editor</button></footer>
|
|
2728
|
+
</dialog>
|
|
2729
|
+
<dialog id="format-settings" aria-label="Format settings">
|
|
2730
|
+
<div class="bar"><h2>Format settings</h2><button type="button" data-close>Close</button></div>
|
|
2731
|
+
<p class="about">How Format lays the source out. These are remembered in this browser only; for a project, put them in a <code>.reladraw-format</code> file beside its diagrams.</p>
|
|
2732
|
+
<form id="format-fields"></form>
|
|
2733
|
+
<p id="format-problem" role="alert"></p>
|
|
2734
|
+
<footer><button type="button" id="format-reset">Reset to defaults</button><span class="gap"></span><button type="button" id="format-copy" title="Copy these settings as the text of a .reladraw-format file">Copy as .reladraw-format</button></footer>
|
|
2735
|
+
</dialog>
|
|
2736
|
+
<dialog id="save-as" aria-label="Save as a file">
|
|
2737
|
+
<form method="dialog" id="save-form">
|
|
2738
|
+
<div class="bar"><h2>Save as a file in <span class="folder"></span></h2></div>
|
|
2739
|
+
<p class="about">A path inside that folder, such as <code>diagram.reladraw</code> or <code>docs/flow.reladraw</code>. If the file is there already, you'll be asked before it's replaced. From then on this page is tied to the file.</p>
|
|
2740
|
+
<label>Path <input id="save-path" type="text" spellcheck="false" autocomplete="off" required></label>
|
|
2741
|
+
<p id="save-problem" role="alert"></p>
|
|
2742
|
+
<footer><span class="gap"></span><button type="button" data-close>Cancel</button><button type="submit" id="save-go">Save</button></footer>
|
|
2743
|
+
</form>
|
|
2744
|
+
</dialog>`;
|