reladraw 0.27.0 → 0.29.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 +56 -6
- package/README.md +2 -2
- package/SYNTAX.md +56 -6
- 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 +1595 -265
- package/dist/search.d.ts +104 -0
- package/dist/search.js +490 -0
- package/dist/serve.d.ts +18 -0
- package/dist/serve.js +544 -0
- package/dist/viewer.js +3 -1
- 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,57 @@ 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
|
-
}
|
|
82
|
-
}
|
|
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 };
|
|
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 };
|
|
146
17
|
}
|
|
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
|
-
|
|
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
|
+
// The hosted playground has no line under its name: the page shows what it is.
|
|
30
|
+
drawn: folder !== undefined ? `local playground · ${folder} · not saved to a file` : '',
|
|
31
|
+
head: playground.head ?? '',
|
|
32
|
+
library: playground.library ?? '',
|
|
33
|
+
});
|
|
185
34
|
}
|
|
186
|
-
function page(
|
|
35
|
+
function page(parts, editing, framing) {
|
|
36
|
+
const { themes, current } = parts;
|
|
187
37
|
const shown = themes[current];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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,
|
|
38
|
+
const data = { ...dataOf(parts), edit: editing };
|
|
39
|
+
const title = framing?.title ?? parts.title;
|
|
40
|
+
const noFile = editing !== null && editing.kind !== 'file';
|
|
41
|
+
const filled = {
|
|
42
|
+
panzoom: PAN_ZOOM,
|
|
43
|
+
scheme: shown.scheme,
|
|
44
|
+
title: escapeMarkup(title),
|
|
45
|
+
head: framing?.head ?? '',
|
|
46
|
+
heading: escapeMarkup(framing?.heading ?? parts.title),
|
|
47
|
+
drawn: escapeMarkup(framing?.drawn ?? parts.drawn),
|
|
48
|
+
links: noFile ? LINKS : '',
|
|
49
|
+
colors: [
|
|
50
|
+
...Object.entries(shown.colors).map(([key, value]) => `${key}: ${value};`),
|
|
51
|
+
'--sel: #e0b25a; --up: #22b98c; --down: #e5484d;',
|
|
52
|
+
].join(' '),
|
|
53
|
+
cheatsheet: editing ? CHEATSHEET : '',
|
|
54
|
+
dialogs: noFile ? DIALOGS : '',
|
|
55
|
+
drawing: shown.tagged,
|
|
56
|
+
// A literal </script> in the library's text would end its tag early.
|
|
57
|
+
library: framing?.library ? `<script>\n${framing.library.replace(/<\/script/gi, '<\\/script')}\n</script>` : '',
|
|
58
|
+
data: scriptJson(data),
|
|
226
59
|
};
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
return PAGE.replace(
|
|
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));
|
|
60
|
+
// One pass, so nothing put in (a title, a drawing, the library) is read
|
|
61
|
+
// again as a marker, and a function, so a `$` in it is taken as written.
|
|
62
|
+
return PAGE.replace(/\{\{([a-z]+)\}\}/g, (marker, key) => filled[key] ?? marker);
|
|
240
63
|
}
|
|
241
|
-
function
|
|
64
|
+
function escapeMarkup(text) {
|
|
242
65
|
// `{` too, so a title or file name can't be taken for a later marker.
|
|
243
66
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/\{/g, '{');
|
|
244
67
|
}
|
|
@@ -255,6 +78,7 @@ const PAGE = `<!doctype html>
|
|
|
255
78
|
<meta name="color-scheme" content="{{scheme}}">
|
|
256
79
|
<meta name="generator" content="reladraw">
|
|
257
80
|
<title>{{title}}</title>
|
|
81
|
+
{{head}}
|
|
258
82
|
<style>
|
|
259
83
|
:root { {{colors}} }
|
|
260
84
|
* { box-sizing: border-box; }
|
|
@@ -269,7 +93,8 @@ const PAGE = `<!doctype html>
|
|
|
269
93
|
padding: 10px 16px;
|
|
270
94
|
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent);
|
|
271
95
|
}
|
|
272
|
-
.heading { flex:
|
|
96
|
+
.heading { flex: 0 1 auto; min-width: 0; }
|
|
97
|
+
header .tools { margin-left: auto; }
|
|
273
98
|
h1 { margin: 0; font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
|
|
274
99
|
.drawn { margin: 0; font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
|
|
275
100
|
.menu-list hr { width: 100%; margin: 2px 0; border: 0; border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
@@ -448,6 +273,10 @@ const PAGE = `<!doctype html>
|
|
|
448
273
|
@media (prefers-reduced-motion: reduce) { #view g[data-edge] :is(line, path, polyline) { animation: none !important; } }
|
|
449
274
|
aside h2 { margin: 0; padding: 10px 16px 6px; font-size: 13px; font-weight: 600; }
|
|
450
275
|
aside h2 .what { display: block; font-weight: 400; font-size: 12px; color: var(--muted); }
|
|
276
|
+
/* Close: a small square button, bordered like the buttons beside it. */
|
|
277
|
+
button.close { width: 24px; height: 24px; padding: 0; font-size: 17px; line-height: 1; color: var(--ink);
|
|
278
|
+
display: inline-flex; align-items: center; justify-content: center; }
|
|
279
|
+
aside h2 .close { float: right; margin: -2px -8px 0 8px; }
|
|
451
280
|
aside ol { list-style: none; margin: 0; padding: 0 8px 12px; overflow: auto; }
|
|
452
281
|
aside li button {
|
|
453
282
|
display: block; width: 100%; text-align: left; white-space: normal; border-color: transparent;
|
|
@@ -455,10 +284,214 @@ const PAGE = `<!doctype html>
|
|
|
455
284
|
}
|
|
456
285
|
aside li button.chosen { border-color: var(--accent); }
|
|
457
286
|
aside li.none { padding: 6px 8px; color: var(--muted); font-size: 13px; }
|
|
287
|
+
/* Findings from an older version of the source: dimmed, not cleared. */
|
|
288
|
+
aside li.again, #band .again { padding: 6px 8px; color: var(--muted); font-size: 12px; }
|
|
289
|
+
aside.stale li:not(.again):not(.error), #band.stale .found { opacity: 0.5; }
|
|
290
|
+
/* While the source is open, the findings sit in a band under the picture,
|
|
291
|
+
taking height from it, never width. */
|
|
292
|
+
#picture { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
293
|
+
#band { flex: none; max-height: 35%; overflow: auto; padding: 4px 12px 8px; font-size: 13px;
|
|
294
|
+
border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
295
|
+
#band[hidden] { display: none; }
|
|
296
|
+
#band .head { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
|
|
297
|
+
#band .head b { font-size: 12px; font-weight: 600; color: var(--muted); }
|
|
298
|
+
#band .head .gap { flex: 1; }
|
|
299
|
+
#band .head button:not(.close) { font-size: 12px; padding: 1px 8px; }
|
|
300
|
+
#band .found button { display: block; width: 100%; text-align: left; white-space: normal; border-color: transparent; padding: 3px 6px; }
|
|
301
|
+
#band .found button.chosen { border-color: var(--accent); }
|
|
302
|
+
#band .again { padding: 2px 0; }
|
|
303
|
+
aside li.error { padding: 6px 8px; margin: 2px 0; border-left: 3px solid var(--down); font-size: 13px; }
|
|
304
|
+
aside li.error .kind { color: var(--down); }
|
|
305
|
+
#live {
|
|
306
|
+
padding: 5px 16px; font-size: 12.5px;
|
|
307
|
+
background: color-mix(in srgb, var(--down) 18%, var(--page)); color: var(--ink);
|
|
308
|
+
border-bottom: 1px solid color-mix(in srgb, var(--down) 45%, transparent);
|
|
309
|
+
}
|
|
310
|
+
#live[hidden] { display: none; }
|
|
311
|
+
|
|
312
|
+
/* The editor: a colored layer under a transparent textarea, every property
|
|
313
|
+
that can move a character the same on both, as in the playground. */
|
|
314
|
+
#editor {
|
|
315
|
+
width: 38%; min-width: 280px; max-width: 760px; display: flex; flex-direction: column;
|
|
316
|
+
border-right: 1px solid color-mix(in srgb, var(--rule) 45%, transparent);
|
|
317
|
+
background: color-mix(in srgb, var(--ink) 4%, var(--page));
|
|
318
|
+
}
|
|
319
|
+
#editor[hidden] { display: none; }
|
|
320
|
+
/* The picture's controls float over it: Check at its top left, the zoom
|
|
321
|
+
on a card at its bottom right. They sit beside the view, not in it, so a
|
|
322
|
+
press on them never starts a pan. */
|
|
323
|
+
#canvas { position: relative; flex: 1; display: flex; min-height: 0; }
|
|
324
|
+
.floating { position: absolute; z-index: 4;
|
|
325
|
+
background: color-mix(in srgb, var(--ink) 7%, var(--page));
|
|
326
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); }
|
|
327
|
+
.floating[hidden] { display: none; }
|
|
328
|
+
#check { left: 12px; top: 12px; padding: 5px 14px; }
|
|
329
|
+
#check:hover { background: color-mix(in srgb, var(--ink) 14%, var(--page)); }
|
|
330
|
+
#zoom-card { right: 12px; bottom: 12px; display: flex; align-items: center; gap: 5px; padding: 4px 5px; border-radius: 9px;
|
|
331
|
+
border: 1px solid color-mix(in srgb, var(--rule) 55%, transparent); }
|
|
332
|
+
/* The card a shade darker than its buttons, so each reads as a button. */
|
|
333
|
+
#zoom-card { background: color-mix(in srgb, var(--ink) 4%, var(--page)); }
|
|
334
|
+
#zoom-card button { background: color-mix(in srgb, var(--ink) 13%, var(--page)); }
|
|
335
|
+
#zoom-card button:hover { background: color-mix(in srgb, var(--ink) 20%, var(--page)); }
|
|
336
|
+
#zoom-card button.pm { font-size: 17px; line-height: 1; padding: 2px 9px; min-width: 30px; }
|
|
337
|
+
#zoom-card .divider { width: 1px; height: 18px; margin: 0 3px; background: color-mix(in srgb, var(--rule) 70%, transparent); }
|
|
338
|
+
body:has(.tools.keyed) #check kbd.tag { display: inline; }
|
|
339
|
+
.src-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 6px 10px; font-size: 12.5px;
|
|
340
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
341
|
+
.src-head .file { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
342
|
+
#unsaved { color: var(--sel); }
|
|
343
|
+
#unsaved[hidden] { display: none; }
|
|
344
|
+
.src-head .gap, #changed .gap { flex: 1; }
|
|
345
|
+
#file-menu[hidden] { display: none; }
|
|
346
|
+
#file-list { left: 0; right: auto; min-width: 12rem; }
|
|
347
|
+
#file-button.open { border-color: var(--sel); background: color-mix(in srgb, var(--sel) 22%, transparent); }
|
|
348
|
+
.src-head kbd { font: 11px system-ui, sans-serif; color: var(--muted); margin-left: 4px; }
|
|
349
|
+
#changed { display: flex; align-items: center; gap: 6px; padding: 6px 10px; font-size: 12.5px;
|
|
350
|
+
background: color-mix(in srgb, var(--sel) 18%, var(--page));
|
|
351
|
+
border-bottom: 1px solid color-mix(in srgb, var(--sel) 45%, transparent); }
|
|
352
|
+
#changed[hidden] { display: none; }
|
|
353
|
+
.code { position: relative; flex: 1; min-height: 0; }
|
|
354
|
+
.code > * {
|
|
355
|
+
position: absolute; inset: 0; margin: 0; border: 0; padding: 12px 14px;
|
|
356
|
+
font: 12.5px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
357
|
+
tab-size: 2; white-space: pre-wrap; overflow-wrap: break-word; word-break: normal;
|
|
358
|
+
}
|
|
359
|
+
#paint { overflow: hidden; color: var(--ink); pointer-events: none; }
|
|
360
|
+
#source { resize: none; outline: 0; background: transparent; color: transparent; caret-color: var(--ink); overflow-y: auto; }
|
|
361
|
+
#source::selection { background: rgba(130, 170, 240, 0.28); color: transparent; }
|
|
362
|
+
:root { --syn-comment: #6a6a78; --syn-string: #9ec99a; --syn-keyword: #82aaf0; --syn-name: #b8c9e8;
|
|
363
|
+
--syn-relation: #c79bdc; --syn-attribute: #6fb3ac; --syn-value: #d3a05f; }
|
|
364
|
+
:root[data-scheme="light"] { --syn-comment: #8a8a96; --syn-string: #3f7a3a; --syn-keyword: #2f5fb8; --syn-name: #34495e;
|
|
365
|
+
--syn-relation: #8a3fa8; --syn-attribute: #217a72; --syn-value: #a0601a; }
|
|
366
|
+
.t-comment, .t-bracket, .t-markup { color: var(--syn-comment); }
|
|
367
|
+
.t-string { color: var(--syn-string); }
|
|
368
|
+
.t-keyword { color: var(--syn-keyword); }
|
|
369
|
+
.t-name { color: var(--syn-name); }
|
|
370
|
+
.t-relation, .t-arrow { color: var(--syn-relation); }
|
|
371
|
+
.t-attribute { color: var(--syn-attribute); }
|
|
372
|
+
.t-value, .t-color { color: var(--syn-value); }
|
|
373
|
+
/* Edit | View: which mode the page is in, the one it is in filled. Edit
|
|
374
|
+
is on the left, the side the source opens on. */
|
|
375
|
+
.mode { display: inline-flex; position: relative; }
|
|
376
|
+
.mode[hidden] { display: none; }
|
|
377
|
+
.mode button { border-radius: 0; color: var(--muted); padding: 4px 12px; }
|
|
378
|
+
.mode button:first-child { border-radius: 6px 0 0 6px; }
|
|
379
|
+
.mode button:last-child { border-radius: 0 6px 6px 0; border-left: 0; }
|
|
380
|
+
.mode button[aria-pressed="true"] { background: color-mix(in srgb, var(--ink) 20%, transparent); color: var(--ink); font-weight: 600; }
|
|
381
|
+
.mode button[aria-pressed="true"]:hover { cursor: default; }
|
|
382
|
+
header:has(.tools.keyed) .mode kbd.tag { display: inline; }
|
|
383
|
+
header:has(.tools.keyed) .mode { outline: 2px solid var(--sel); outline-offset: 4px; border-radius: 6px; }
|
|
384
|
+
.menu-list.wide { min-width: 340px; }
|
|
385
|
+
.menu-list .row .when { color: var(--muted); font-variant-numeric: tabular-nums; width: 3.2em; flex: none; }
|
|
386
|
+
.menu-list .row .who { width: 4.5em; flex: none; }
|
|
387
|
+
.menu-list .row .who.you { color: #8fb3ff; }
|
|
388
|
+
.menu-list .row .who.agent { color: var(--up); }
|
|
389
|
+
.menu-list .row .who.disk, .menu-list .row .who.opened { color: var(--muted); }
|
|
390
|
+
.menu-list .row .about { flex: 1; text-align: left; white-space: normal; }
|
|
458
391
|
.where { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
459
392
|
.kind { color: var(--accent); }
|
|
460
393
|
.note { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; }
|
|
394
|
+
aside li.error button.copy { float: right; display: inline-block; width: auto; margin: 0 0 0 8px; padding: 0 6px; font-size: 11px;
|
|
395
|
+
border-color: color-mix(in srgb, var(--rule) 70%, transparent); }
|
|
396
|
+
aside li.error .goto { cursor: pointer; text-decoration: underline dotted; }
|
|
397
|
+
|
|
398
|
+
/* ---- a page with no file: the playground ---- */
|
|
399
|
+
/* GitHub, as its mark beside the name. */
|
|
400
|
+
.heading h1 { display: inline; vertical-align: middle; }
|
|
401
|
+
.links { display: inline-flex; vertical-align: middle; margin-left: 10px; }
|
|
402
|
+
.links a { display: inline-flex; color: var(--muted); padding: 2px; border-radius: 4px; }
|
|
403
|
+
.links a:hover { color: var(--ink); }
|
|
404
|
+
.links a:focus-visible { outline: 2px solid var(--accent); }
|
|
405
|
+
/* A link as a row of a menu, like the buttons around it. */
|
|
406
|
+
.menu-list a.row { color: var(--ink); text-decoration: none; font-size: 13px; border: 1px solid transparent; border-radius: 6px; }
|
|
407
|
+
.menu-list a.row:hover { background: color-mix(in srgb, var(--ink) 10%, transparent); }
|
|
408
|
+
#help-menu[hidden] { display: none; }
|
|
409
|
+
.drawn:empty { display: none; }
|
|
410
|
+
/* The divider between the source and the picture: dragged, or moved with the arrows. */
|
|
411
|
+
#split { flex: none; width: 6px; margin: 0 -3px; z-index: 2; cursor: col-resize; touch-action: none; }
|
|
412
|
+
#split[hidden] { display: none; }
|
|
413
|
+
#split:hover, #split.dragging, #split:focus-visible { background: color-mix(in srgb, var(--accent) 45%, transparent); outline: 0; }
|
|
414
|
+
body.dragging { cursor: col-resize; user-select: none; }
|
|
415
|
+
/* The cheatsheet: a button in the source's bar opens a large window, a
|
|
416
|
+
card per topic, each row an example in the editor's colors beside what
|
|
417
|
+
it does. The filter narrows the rows as you type. */
|
|
418
|
+
#cheatsheet { width: min(1180px, calc(100vw - 32px)); height: min(820px, calc(100vh - 32px)); max-width: none; max-height: none; }
|
|
419
|
+
#cheatsheet[open] { display: flex; flex-direction: column; }
|
|
420
|
+
#cheatsheet .cs-head { display: flex; align-items: center; gap: 12px; padding: 10px 16px;
|
|
421
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
422
|
+
#cheatsheet .cs-head h2 { font-size: 14px; }
|
|
423
|
+
#cheat-filter { flex: 0 1 340px; font: inherit; font-size: 13px; color: var(--ink); padding: 4px 10px; border-radius: 6px;
|
|
424
|
+
background: color-mix(in srgb, var(--ink) 4%, var(--page)); border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); }
|
|
425
|
+
#cheat-filter:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
426
|
+
#cheatsheet .cs-head .gap { flex: 1; }
|
|
427
|
+
#cheatsheet .cs-grid { flex: 1; min-height: 0; overflow: auto; padding: 14px 16px 18px; }
|
|
428
|
+
/* Cards fill three columns top to bottom; the page fills each column as the
|
|
429
|
+
window opens, so the columns come out about the same height. */
|
|
430
|
+
.cs-cols { display: grid; grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); gap: 14px; align-items: start; }
|
|
431
|
+
.cs-col { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
432
|
+
.cs-card { border-radius: 8px; overflow: hidden;
|
|
433
|
+
border: 1px solid color-mix(in srgb, var(--hue) 30%, transparent);
|
|
434
|
+
background: color-mix(in srgb, var(--ink) 3%, var(--page)); }
|
|
435
|
+
.cs-card[hidden] { display: none; }
|
|
436
|
+
.cs-card h3 { margin: 0; padding: 7px 12px; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
|
|
437
|
+
color: var(--hue); background: color-mix(in srgb, var(--hue) 12%, transparent);
|
|
438
|
+
border-bottom: 1px solid color-mix(in srgb, var(--hue) 22%, transparent); }
|
|
439
|
+
.cs-row { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 4px 12px; padding: 6px 12px; font-size: 12.5px;
|
|
440
|
+
border-top: 1px solid color-mix(in srgb, var(--rule) 25%, transparent); }
|
|
441
|
+
.cs-card h3 + .cs-row { border-top: 0; }
|
|
442
|
+
.cs-row[hidden] { display: none; }
|
|
443
|
+
.cs-row code { font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow-wrap: anywhere; color: var(--ink); }
|
|
444
|
+
.cs-row > span { color: var(--muted); }
|
|
445
|
+
.cs-note { margin: 0; padding: 7px 12px 9px; font-size: 12px; color: var(--muted);
|
|
446
|
+
border-top: 1px solid color-mix(in srgb, var(--rule) 25%, transparent); }
|
|
447
|
+
.cs-note code { font: 11.5px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--ink); }
|
|
448
|
+
.cs-none { margin: 0; padding: 24px 16px; color: var(--muted); }
|
|
449
|
+
.cs-none[hidden] { display: none; }
|
|
450
|
+
dialog { padding: 0; color: var(--ink); background: var(--page); overflow: hidden;
|
|
451
|
+
border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius: 6px; }
|
|
452
|
+
dialog::backdrop { background: rgba(0, 0, 0, .6); }
|
|
453
|
+
dialog .bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 16px;
|
|
454
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
455
|
+
dialog h2 { margin: 0; font-size: 13px; font-weight: 600; }
|
|
456
|
+
dialog footer { display: flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 12px; color: var(--muted);
|
|
457
|
+
border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
458
|
+
dialog footer .gap { flex: 1; }
|
|
459
|
+
#examples { width: min(1100px, calc(100vw - 32px)); height: min(720px, calc(100vh - 32px)); max-width: none; max-height: none; }
|
|
460
|
+
#examples[open] { display: grid; grid-template-columns: 220px 1fr; grid-template-rows: auto auto minmax(0, 1fr) auto; }
|
|
461
|
+
#examples .bar, #examples footer { grid-column: 1 / -1; }
|
|
462
|
+
#catalog { grid-row: 2 / 4; margin: 0; padding: 6px 0; list-style: none; overflow-y: auto; outline: 0;
|
|
463
|
+
border-right: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
464
|
+
#catalog li { padding: 6px 16px; font-size: 13px; cursor: pointer; border-left: 2px solid transparent; }
|
|
465
|
+
#catalog li:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }
|
|
466
|
+
#catalog li[aria-selected="true"] { background: color-mix(in srgb, var(--ink) 13%, transparent); border-left-color: var(--accent); }
|
|
467
|
+
#catalog:focus-visible li[aria-selected="true"] { outline: 1px solid var(--muted); outline-offset: -1px; }
|
|
468
|
+
#about { margin: 0; padding: 10px 16px; font-size: 13px; border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
469
|
+
#preview { min-width: 0; min-height: 0; overflow: auto; padding: 16px; display: flex; }
|
|
470
|
+
#preview svg { max-width: 100%; max-height: 100%; height: auto; margin: auto; }
|
|
471
|
+
#format-settings, #save-as { width: min(640px, calc(100vw - 32px)); max-height: calc(100vh - 32px); }
|
|
472
|
+
#format-settings[open], #save-as[open] { display: flex; flex-direction: column; }
|
|
473
|
+
dialog .about { margin: 0; padding: 10px 16px; font-size: 12px; color: var(--muted); }
|
|
474
|
+
#format-fields { overflow-y: auto; padding: 4px 16px; }
|
|
475
|
+
#format-fields label { display: grid; grid-template-columns: 190px 1fr; gap: 2px 12px; align-items: center; padding: 8px 0; font-size: 13px;
|
|
476
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 30%, transparent); }
|
|
477
|
+
#format-fields label span { grid-column: 2; color: var(--muted); font-size: 11px; }
|
|
478
|
+
#format-fields select, #format-fields input, #save-as input { font: 12px ui-monospace, Menlo, Consolas, monospace; color: var(--ink); background: var(--page);
|
|
479
|
+
border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius: 4px; padding: 4px 6px; min-width: 0; }
|
|
480
|
+
#save-as label { display: flex; align-items: center; gap: 8px; padding: 0 16px 12px; font-size: 13px; }
|
|
481
|
+
#save-as input { flex: 1; font-size: 13px; }
|
|
482
|
+
#format-problem, #save-problem { margin: 0; padding: 0 16px; color: var(--down); font-size: 12px; }
|
|
483
|
+
#format-problem:not(:empty), #save-problem:not(:empty) { padding: 8px 16px; }
|
|
484
|
+
@media (max-width: 760px) {
|
|
485
|
+
#examples[open] { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 2fr) auto minmax(0, 3fr) auto; }
|
|
486
|
+
#catalog { grid-row: auto; border-right: 0; border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
487
|
+
#format-fields label { grid-template-columns: 1fr; }
|
|
488
|
+
#format-fields label span { grid-column: 1; }
|
|
489
|
+
dialog footer .keys { display: none; }
|
|
490
|
+
}
|
|
461
491
|
@media (max-width: 700px) {
|
|
492
|
+
#split { display: none; }
|
|
493
|
+
#editor { width: auto !important; max-width: none; min-width: 0; height: 45%; border-right: 0;
|
|
494
|
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
462
495
|
main { flex-direction: column; }
|
|
463
496
|
#side { width: auto; max-height: 45%; border-left: 0; border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
464
497
|
}
|
|
@@ -467,19 +500,20 @@ const PAGE = `<!doctype html>
|
|
|
467
500
|
<body>
|
|
468
501
|
<header>
|
|
469
502
|
<div class="heading">
|
|
470
|
-
<h1>{{heading}}</h1>
|
|
503
|
+
<h1>{{heading}}</h1>{{links}}
|
|
471
504
|
<p class="drawn">{{drawn}}</p>
|
|
472
505
|
</div>
|
|
506
|
+
<span id="mode" class="mode" role="group" aria-label="Mode" hidden>
|
|
507
|
+
<button id="edit" type="button" title="The source beside the picture (e)" aria-pressed="false">Edit<kbd class="tag">e</kbd></button>
|
|
508
|
+
<button id="mode-view" type="button" title="Only the picture (e)" aria-pressed="true">View</button>
|
|
509
|
+
</span>
|
|
473
510
|
<div class="tools">
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
<button id="actual" type="button" title="The drawing at its own scale (1)">100%</button>
|
|
479
|
-
<span class="gap"></span>
|
|
480
|
-
<button id="full" type="button" title="Full screen (f)"><span class="label">Full screen</span><kbd class="tag">f</kbd></button>
|
|
511
|
+
<span class="menu" id="versions-menu" hidden>
|
|
512
|
+
<button id="versions-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="versions-list">Versions ▾<kbd class="tag">o</kbd></button>
|
|
513
|
+
<span id="versions-list" class="menu-list wide" role="menu" hidden></span>
|
|
514
|
+
</span>
|
|
481
515
|
<span class="menu">
|
|
482
|
-
<button id="save" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="save-list">
|
|
516
|
+
<button id="save" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="save-list">Export ▾<kbd class="tag">x</kbd></button>
|
|
483
517
|
<span id="save-list" class="menu-list" role="menu" hidden>
|
|
484
518
|
<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
519
|
<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>
|
|
@@ -490,7 +524,7 @@ const PAGE = `<!doctype html>
|
|
|
490
524
|
</span>
|
|
491
525
|
</span>
|
|
492
526
|
<span class="menu">
|
|
493
|
-
<button id="view-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="view-list">
|
|
527
|
+
<button id="view-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="view-list">Settings ▾<kbd class="tag">s</kbd></button>
|
|
494
528
|
<span id="view-list" class="menu-list" role="menu" hidden>
|
|
495
529
|
<button id="theme-row" class="row" type="button" role="menuitem" data-key="t" aria-haspopup="true" title="Draw the diagram in another theme"><span class="name">Theme <span class="value"></span> ▸</span><kbd>t</kbd></button>
|
|
496
530
|
<hr>
|
|
@@ -498,37 +532,90 @@ const PAGE = `<!doctype html>
|
|
|
498
532
|
<button id="auto-row" class="row switch" type="button" role="menuitemcheckbox" data-key="a" title="Show the side panel only while something is selected or the findings are open"><span class="sw"></span><span class="name">Auto-hide panel</span><span class="word"></span><kbd>a</kbd></button>
|
|
499
533
|
<hr>
|
|
500
534
|
<button id="findings-row" class="row switch" type="button" role="menuitemcheckbox" data-key="f" title="Show or hide the findings in the side panel"><span class="sw"></span><span class="name">Findings</span><span class="word"></span><kbd>f</kbd></button>
|
|
535
|
+
<button id="autocheck-row" class="row switch" type="button" role="menuitemcheckbox" data-key="c" title="Look for findings every time the picture is drawn, without pressing Check" hidden><span class="sw"></span><span class="name">Check as I type</span><span class="word"></span><kbd>c</kbd></button>
|
|
501
536
|
<hr>
|
|
502
537
|
<button id="guides-row" class="row switch" type="button" role="menuitemcheckbox" data-key="g" title="Draw the guides: everything written with guide in front"><span class="sw"></span><span class="name">Guides</span><span class="word"></span><kbd>g</kbd></button>
|
|
503
538
|
<span id="theme-list" class="menu-list sub" role="menu" hidden></span>
|
|
504
539
|
</span>
|
|
505
540
|
</span>
|
|
541
|
+
<button id="full" type="button" title="Full screen (f)"><span class="label">Full screen</span><kbd class="tag">f</kbd></button>
|
|
506
542
|
</div>
|
|
507
543
|
</header>
|
|
508
544
|
<div id="strip" hidden><span class="crumbs"></span><span class="hint"></span></div>
|
|
545
|
+
<div id="live" role="status" hidden></div>
|
|
509
546
|
<main>
|
|
547
|
+
<section id="editor" aria-label="Source" hidden>
|
|
548
|
+
<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>
|
|
549
|
+
<div class="src-head">
|
|
550
|
+
<span class="file"></span><span id="unsaved" title="Not saved to the file" hidden>●</span><span class="gap"></span>
|
|
551
|
+
<span class="menu" id="file-menu" hidden>
|
|
552
|
+
<button id="file-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="file-list"><span class="name">File ▾</span></button>
|
|
553
|
+
<span id="file-list" class="menu-list" role="menu" hidden>
|
|
554
|
+
<button id="browse" class="row" type="button" role="menuitem" title="Open one of the examples in the editor" aria-haspopup="dialog"><span class="name">Examples…</span></button>
|
|
555
|
+
<button id="open-file" class="row" type="button" role="menuitem" title="Open a .reladraw file into the source"><span class="name">Open…</span></button>
|
|
556
|
+
<hr>
|
|
557
|
+
<button id="write-row" class="row" type="button" role="menuitem" title="Save the source as a .reladraw file"><span class="name">Save…</span><kbd>⌘S</kbd></button>
|
|
558
|
+
</span>
|
|
559
|
+
</span>
|
|
560
|
+
<input id="file-input" type="file" accept=".reladraw,text/plain" hidden>
|
|
561
|
+
<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>
|
|
562
|
+
<button id="format-open" type="button" aria-haspopup="dialog" title="How Format lays the source out" hidden>Format settings…</button>
|
|
563
|
+
<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>
|
|
564
|
+
<span class="menu" id="help-menu" hidden>
|
|
565
|
+
<button id="help-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="help-list"><span class="name">Help ▾</span></button>
|
|
566
|
+
<span id="help-list" class="menu-list" role="menu" hidden>
|
|
567
|
+
<button id="cheat-open" class="row" type="button" role="menuitem" title="The language on one screen"><span class="name">Cheatsheet</span><kbd>?</kbd></button>
|
|
568
|
+
<a id="syntax-link" class="row" role="menuitem" href="https://github.com/reladraw/reladraw/blob/main/SYNTAX.md" target="_blank" rel="noopener" title="The full syntax reference, on GitHub"><span class="name">Syntax reference ↗</span></a>
|
|
569
|
+
</span>
|
|
570
|
+
</span>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="code">
|
|
573
|
+
<pre id="paint" aria-hidden="true"></pre>
|
|
574
|
+
<textarea id="source" spellcheck="false" autocapitalize="off" autocomplete="off" aria-label="Source"></textarea>
|
|
575
|
+
</div>{{cheatsheet}}
|
|
576
|
+
</section>
|
|
577
|
+
<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>
|
|
578
|
+
<div id="picture">
|
|
579
|
+
<div id="canvas">
|
|
580
|
+
<button id="check" class="floating" type="button" title="Look for overlaps and other places the picture may not read as meant (c)" hidden>Check<kbd class="tag">c</kbd></button>
|
|
581
|
+
<div id="zoom-card" class="floating">
|
|
582
|
+
<button id="out" class="pm" type="button" title="Zoom out (-)" aria-label="Zoom out">−</button>
|
|
583
|
+
<span id="scale" aria-live="polite">100%</span>
|
|
584
|
+
<button id="in" class="pm" type="button" title="Zoom in (+)" aria-label="Zoom in">+</button>
|
|
585
|
+
<span class="divider"></span>
|
|
586
|
+
<button id="fit" type="button" title="The whole diagram in view (0)">Fit</button>
|
|
587
|
+
<button id="actual" type="button" title="The drawing at its own scale (1)">100%</button>
|
|
588
|
+
</div>
|
|
510
589
|
<div id="view">
|
|
511
590
|
<div id="stage">
|
|
512
591
|
{{drawing}}
|
|
513
592
|
</div>
|
|
514
593
|
<div id="tip"></div>
|
|
515
594
|
</div>
|
|
595
|
+
</div>
|
|
596
|
+
<div id="band" aria-label="Findings" aria-live="polite" hidden></div>
|
|
597
|
+
</div>
|
|
516
598
|
<div id="side">
|
|
517
599
|
<section id="selected" aria-label="Selected" aria-live="polite">
|
|
518
600
|
<p class="empty">Click a box or a line to see its links and what it connects to.</p>
|
|
519
601
|
</section>
|
|
520
602
|
<aside id="findings" aria-label="Findings">
|
|
521
|
-
<h2>Findings <span class="what">where the picture, drawn as written, may not read as meant</span></h2>
|
|
603
|
+
<h2><button id="findings-close" class="close" type="button" title="Close the findings" aria-label="Close the findings">×</button>Findings <span class="what">where the picture, drawn as written, may not read as meant</span></h2>
|
|
522
604
|
<ol id="finding-list"></ol>
|
|
523
605
|
</aside>
|
|
524
606
|
</div>
|
|
525
|
-
</main>
|
|
607
|
+
</main>{{dialogs}}
|
|
608
|
+
{{library}}
|
|
526
609
|
<script>
|
|
527
610
|
(function () {
|
|
611
|
+
var DATA = {{data}};
|
|
612
|
+
// How the source is edited, or null on a snapshot, which has no editor.
|
|
613
|
+
var EDIT = DATA.edit;
|
|
528
614
|
// The page as written, taken before anything below changes it, so This
|
|
529
615
|
// page saves what reladraw wrote rather than the current zoom and highlights.
|
|
530
|
-
|
|
531
|
-
var
|
|
616
|
+
// A page with an editor makes a fresh snapshot instead (below).
|
|
617
|
+
var written = EDIT ? '' : '<!doctype html>\\n' + document.documentElement.outerHTML + '\\n';
|
|
618
|
+
document.documentElement.setAttribute('data-scheme', DATA.themes[DATA.current].scheme === 'light' ? 'light' : 'dark');
|
|
532
619
|
var view = document.getElementById('view');
|
|
533
620
|
var stage = document.getElementById('stage');
|
|
534
621
|
var svg = stage.querySelector('svg');
|
|
@@ -538,7 +625,7 @@ const PAGE = `<!doctype html>
|
|
|
538
625
|
// Another theme is carried as the colors that differ from the first, put
|
|
539
626
|
// back in place the first time it is shown.
|
|
540
627
|
var VARIES = new RegExp(DATA.varies, 'gi');
|
|
541
|
-
// Whether guides are drawn, which the
|
|
628
|
+
// Whether guides are drawn, which the Settings menu's switch changes. The
|
|
542
629
|
// drawings with guides the other way are a second set, packed the same way
|
|
543
630
|
// against its own first theme.
|
|
544
631
|
var guidesShown = DATA.guidesShown;
|
|
@@ -638,23 +725,30 @@ const PAGE = `<!doctype html>
|
|
|
638
725
|
return Promise.resolve(blob);
|
|
639
726
|
}
|
|
640
727
|
|
|
641
|
-
// Every way out of the page is under one
|
|
728
|
+
// Every way out of the page is under one Export button: the page, its
|
|
642
729
|
// 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
|
|
730
|
+
// can be read. It is one of the menus (below), opened by a click or by m x.
|
|
731
|
+
// Save is the served page's writing of the file, so this is Export everywhere.
|
|
644
732
|
var saveButton = document.getElementById('save');
|
|
645
733
|
var saveList = document.getElementById('save-list');
|
|
646
734
|
|
|
647
735
|
var savePage = document.getElementById('save-page');
|
|
648
736
|
savePage.onclick = function () {
|
|
649
737
|
saveFile(savePage, DATA.name + '.html', 'Web page', 'text/html', function () {
|
|
650
|
-
return ready(new Blob([written], { type: 'text/html' }));
|
|
738
|
+
if (!EDIT) return ready(new Blob([written], { type: 'text/html' }));
|
|
739
|
+
// A page with an editor saves a snapshot of the diagram as last drawn,
|
|
740
|
+
// as render --page writes it: this page itself would be the editor,
|
|
741
|
+
// still looking for its server or holding the whole library.
|
|
742
|
+
return libReady.then(function (lib) {
|
|
743
|
+
return new Blob([lib.drawPage(DATA.source, DATA.name + '.reladraw').html], { type: 'text/html' });
|
|
744
|
+
});
|
|
651
745
|
});
|
|
652
746
|
};
|
|
653
747
|
|
|
654
748
|
var saveSource = document.getElementById('save-source');
|
|
655
749
|
saveSource.onclick = function () {
|
|
656
750
|
saveFile(saveSource, DATA.name + '.reladraw', 'reladraw source', 'text/plain', function () {
|
|
657
|
-
return ready(new Blob([DATA.source], { type: 'text/plain' }));
|
|
751
|
+
return ready(new Blob([EDIT ? sourceBox.value : DATA.source], { type: 'text/plain' }));
|
|
658
752
|
});
|
|
659
753
|
};
|
|
660
754
|
|
|
@@ -745,9 +839,22 @@ const PAGE = `<!doctype html>
|
|
|
745
839
|
|
|
746
840
|
var panel = document.getElementById('findings');
|
|
747
841
|
var list = document.getElementById('finding-list');
|
|
748
|
-
var
|
|
749
|
-
|
|
750
|
-
//
|
|
842
|
+
var band = document.getElementById('band');
|
|
843
|
+
// A page with an editor looks for findings only when Check is pressed,
|
|
844
|
+
// unless "Check as I type" is on: on a large diagram the checks would hold
|
|
845
|
+
// up the typing. A page without one carries the findings it was made with.
|
|
846
|
+
var AUTO_CHECK = 'reladraw-page-autocheck';
|
|
847
|
+
var autoCheck = false;
|
|
848
|
+
try { autoCheck = localStorage.getItem(AUTO_CHECK) === 'on'; } catch (e) {}
|
|
849
|
+
// The text last checked and what was found in it. Findings outlive edits,
|
|
850
|
+
// dimmed, since they describe the source as it was.
|
|
851
|
+
var checked = !EDIT || autoCheck ? { text: DATA.source, findings: DATA.findings } : null;
|
|
852
|
+
var shownText = DATA.source;
|
|
853
|
+
function found() { return checked ? checked.findings : []; }
|
|
854
|
+
function stale() { return !!checked && checked.text !== shownText; }
|
|
855
|
+
var count = found().length;
|
|
856
|
+
|
|
857
|
+
// A row of the Settings menu that is on or off: its switch, and the word beside it.
|
|
751
858
|
function setSwitch(row, on) {
|
|
752
859
|
row.setAttribute('aria-checked', String(on));
|
|
753
860
|
row.querySelector('.word').textContent = on ? 'on' : 'off';
|
|
@@ -756,7 +863,6 @@ const PAGE = `<!doctype html>
|
|
|
756
863
|
var autoRow = document.getElementById('auto-row');
|
|
757
864
|
var findingsRow = document.getElementById('findings-row');
|
|
758
865
|
var guidesRow = document.getElementById('guides-row');
|
|
759
|
-
findingsRow.querySelector('.name').textContent = 'Findings (' + count + ')';
|
|
760
866
|
guidesRow.querySelector('.name').textContent = 'Guides (' + DATA.guideCount + ')';
|
|
761
867
|
|
|
762
868
|
/* ---- the side panel: shown, hidden, or shown only when wanted ---- */
|
|
@@ -775,8 +881,15 @@ const PAGE = `<!doctype html>
|
|
|
775
881
|
function keepPanel() {
|
|
776
882
|
try { localStorage.setItem(KEPT, JSON.stringify({ on: panelOn, autoHide: autoHide })); } catch (e) {}
|
|
777
883
|
}
|
|
884
|
+
// While the source is open the panel would squeeze the picture between two
|
|
885
|
+
// columns, so it behaves as if auto-hidden: it comes up for a selection or
|
|
886
|
+
// an error, and for the findings only when asked.
|
|
887
|
+
function editing() {
|
|
888
|
+
var pane = document.getElementById('editor');
|
|
889
|
+
return !!pane && !pane.hidden;
|
|
890
|
+
}
|
|
778
891
|
function layoutSide() {
|
|
779
|
-
side.hidden = !(panelOn && (!autoHide || selection || !panel.hidden));
|
|
892
|
+
side.hidden = !(panelOn && (!(autoHide || editing()) || selection || !panel.hidden));
|
|
780
893
|
// A finding lights what it is about only while the finding can be seen.
|
|
781
894
|
if ((panel.hidden || side.hidden) && lit && lit.length) unlight();
|
|
782
895
|
setSwitch(panelRow, panelOn);
|
|
@@ -808,8 +921,8 @@ const PAGE = `<!doctype html>
|
|
|
808
921
|
else showPanel(panel.hidden);
|
|
809
922
|
}
|
|
810
923
|
findingsRow.onclick = flipFindings;
|
|
924
|
+
document.getElementById('findings-close').onclick = function () { showPanel(false); };
|
|
811
925
|
if (count === 0) {
|
|
812
|
-
list.append(element('li', 'none', 'No findings.'));
|
|
813
926
|
panel.hidden = true;
|
|
814
927
|
} else {
|
|
815
928
|
// Open from the start, unless the panel is to stay out of the way.
|
|
@@ -850,8 +963,127 @@ const PAGE = `<!doctype html>
|
|
|
850
963
|
zoom.pan((room.left + room.width / 2) - (r.l + r.r) / 2, (room.top + room.height / 2) - (r.t + r.b) / 2);
|
|
851
964
|
}
|
|
852
965
|
|
|
853
|
-
|
|
854
|
-
|
|
966
|
+
// Whether the source has stopped parsing, and whether that is what opened
|
|
967
|
+
// the findings, so they close again once it parses.
|
|
968
|
+
var failing = false;
|
|
969
|
+
var openedByError = false;
|
|
970
|
+
// The list, made again when a served page redraws. An error the file has
|
|
971
|
+
// stopped parsing with heads it, over the findings of the last drawing that
|
|
972
|
+
// parsed, which is still the one shown.
|
|
973
|
+
var lastError = null;
|
|
974
|
+
function listFindings(error) {
|
|
975
|
+
failing = !!error;
|
|
976
|
+
lastError = error;
|
|
977
|
+
list.textContent = '';
|
|
978
|
+
count = found().length + (error ? 1 : 0);
|
|
979
|
+
findingsRow.querySelector('.name').textContent = checked || error ? 'Findings (' + count + ')' : 'Findings';
|
|
980
|
+
guidesRow.querySelector('.name').textContent = 'Guides (' + DATA.guideCount + ')';
|
|
981
|
+
panel.classList.toggle('stale', stale());
|
|
982
|
+
if (error) {
|
|
983
|
+
var wrong = element('li', 'error');
|
|
984
|
+
var head = element('span', 'where', error.line ? 'line ' + error.line + ' ' : '');
|
|
985
|
+
if (EDIT) {
|
|
986
|
+
if (error.line) {
|
|
987
|
+
head.classList.add('goto');
|
|
988
|
+
head.title = 'Select the line in the source';
|
|
989
|
+
head.onclick = function () { goToLine(error.line); };
|
|
990
|
+
}
|
|
991
|
+
// For pasting into an issue, or a chat with an agent.
|
|
992
|
+
var copy = element('button', 'copy', 'Copy');
|
|
993
|
+
copy.type = 'button';
|
|
994
|
+
copy.title = 'Copy the error';
|
|
995
|
+
copy.onclick = function () {
|
|
996
|
+
toClipboard((error.line ? 'line ' + error.line + ': ' : '') + error.message)
|
|
997
|
+
.then(function () { flash(copy, 'Copied'); }, function () { flash(copy, 'Blocked'); });
|
|
998
|
+
};
|
|
999
|
+
wrong.append(copy);
|
|
1000
|
+
}
|
|
1001
|
+
wrong.append(head, element('span', 'kind', 'error'), document.createElement('br'),
|
|
1002
|
+
document.createTextNode(error.message), element('span', 'note', 'The picture is the last version that drew.'));
|
|
1003
|
+
list.append(wrong);
|
|
1004
|
+
}
|
|
1005
|
+
if (!checked) {
|
|
1006
|
+
list.append(element('li', 'none', 'Automatic checking is off. Press Check in the bar above to look for overlaps and other places the picture may not read as meant.'));
|
|
1007
|
+
} else {
|
|
1008
|
+
if (stale()) list.append(element('li', 'again', againText()));
|
|
1009
|
+
if (found().length === 0) list.append(element('li', 'none', 'No findings.'));
|
|
1010
|
+
found().forEach(function (finding) {
|
|
1011
|
+
var item = document.createElement('li');
|
|
1012
|
+
item.append(findingButton(finding));
|
|
1013
|
+
list.append(item);
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
showBand();
|
|
1017
|
+
}
|
|
1018
|
+
function againText() {
|
|
1019
|
+
return autoCheck ? 'From the last version that drew; the source has changed since.'
|
|
1020
|
+
: 'The source has changed since this check. Press Check again.';
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// The band under the picture while the source is open: the first few
|
|
1024
|
+
// findings, and View all for the panel with every one.
|
|
1025
|
+
var BAND_ROWS = 3;
|
|
1026
|
+
var bandClosed = false;
|
|
1027
|
+
function showBand() {
|
|
1028
|
+
var list = found();
|
|
1029
|
+
band.textContent = '';
|
|
1030
|
+
band.hidden = !(EDIT && editing() && checked && !bandClosed && (list.length > 0 || !autoCheck));
|
|
1031
|
+
if (band.hidden) return;
|
|
1032
|
+
band.classList.toggle('stale', stale());
|
|
1033
|
+
var head = element('div', 'head');
|
|
1034
|
+
head.append(element('b', '', list.length ? 'Findings \\u00B7 ' + list.length : 'No findings'));
|
|
1035
|
+
head.append(element('span', 'gap'));
|
|
1036
|
+
if (list.length) {
|
|
1037
|
+
var all = element('button', '', list.length > BAND_ROWS ? 'View all ' + list.length : 'View all');
|
|
1038
|
+
all.type = 'button';
|
|
1039
|
+
all.title = 'Open the findings in the side panel';
|
|
1040
|
+
all.onclick = openFindings;
|
|
1041
|
+
head.append(all);
|
|
1042
|
+
}
|
|
1043
|
+
var close = element('button', 'close', '\\u00D7');
|
|
1044
|
+
close.type = 'button';
|
|
1045
|
+
close.title = 'Close until the next check';
|
|
1046
|
+
close.setAttribute('aria-label', 'Close the findings');
|
|
1047
|
+
close.onclick = function () { bandClosed = true; showBand(); };
|
|
1048
|
+
head.append(close);
|
|
1049
|
+
band.append(head);
|
|
1050
|
+
if (stale()) band.append(element('div', 'again', againText()));
|
|
1051
|
+
var rows = element('div', 'found');
|
|
1052
|
+
list.slice(0, BAND_ROWS).forEach(function (finding) { rows.append(findingButton(finding)); });
|
|
1053
|
+
band.append(rows);
|
|
1054
|
+
}
|
|
1055
|
+
// The panel takes the findings over: the band closes until the next check,
|
|
1056
|
+
// and closing the panel doesn't bring it back.
|
|
1057
|
+
function openFindings() {
|
|
1058
|
+
if (!panelOn) { panelOn = true; keepPanel(); }
|
|
1059
|
+
bandClosed = true;
|
|
1060
|
+
showBand();
|
|
1061
|
+
showPanel(true);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Looks for findings in the source as it stands, drawing it again to do so.
|
|
1065
|
+
var checkButton = document.getElementById('check');
|
|
1066
|
+
function runCheck() {
|
|
1067
|
+
if (!EDIT || !lib) return;
|
|
1068
|
+
bandClosed = false;
|
|
1069
|
+
drawnText = null;
|
|
1070
|
+
draw(sourceBox.value, true);
|
|
1071
|
+
// In View mode there is no band, so what was found shows in the panel.
|
|
1072
|
+
if (!editing()) openFindings();
|
|
1073
|
+
}
|
|
1074
|
+
checkButton.onclick = runCheck;
|
|
1075
|
+
var autoCheckRow = document.getElementById('autocheck-row');
|
|
1076
|
+
function setAutoCheck(on) {
|
|
1077
|
+
autoCheck = on;
|
|
1078
|
+
try { localStorage.setItem(AUTO_CHECK, on ? 'on' : 'off'); } catch (e) {}
|
|
1079
|
+
setSwitch(autoCheckRow, on);
|
|
1080
|
+
checkButton.hidden = !EDIT || on;
|
|
1081
|
+
if (on) runCheck();
|
|
1082
|
+
else listFindings(lastError);
|
|
1083
|
+
}
|
|
1084
|
+
autoCheckRow.onclick = function () { setAutoCheck(!autoCheck); };
|
|
1085
|
+
|
|
1086
|
+
function findingButton(finding) {
|
|
855
1087
|
var button = document.createElement('button');
|
|
856
1088
|
button.type = 'button';
|
|
857
1089
|
var where = document.createElement('span');
|
|
@@ -867,10 +1099,14 @@ const PAGE = `<!doctype html>
|
|
|
867
1099
|
note.textContent = 'Nothing drawn comes from that line.';
|
|
868
1100
|
button.append(note);
|
|
869
1101
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1102
|
+
// With the source shown, the line it names is selected there too.
|
|
1103
|
+
button.onclick = function () {
|
|
1104
|
+
light(finding, button);
|
|
1105
|
+
if (EDIT && !editorPane.hidden) goToLine(finding.line);
|
|
1106
|
+
};
|
|
1107
|
+
return button;
|
|
1108
|
+
}
|
|
1109
|
+
listFindings(null);
|
|
874
1110
|
|
|
875
1111
|
/* ---- selecting a box or a line ---- */
|
|
876
1112
|
|
|
@@ -1081,6 +1317,7 @@ const PAGE = `<!doctype html>
|
|
|
1081
1317
|
var next = shown();
|
|
1082
1318
|
Object.keys(next.colors).forEach(function (key) { document.documentElement.style.setProperty(key, next.colors[key]); });
|
|
1083
1319
|
schemeMeta.setAttribute('content', next.scheme);
|
|
1320
|
+
document.documentElement.setAttribute('data-scheme', next.scheme === 'light' ? 'light' : 'dark');
|
|
1084
1321
|
unlight();
|
|
1085
1322
|
hovered = null;
|
|
1086
1323
|
tip.style.display = 'none';
|
|
@@ -1180,6 +1417,8 @@ const PAGE = `<!doctype html>
|
|
|
1180
1417
|
var tools = document.querySelector('.tools');
|
|
1181
1418
|
var viewButton = document.getElementById('view-button');
|
|
1182
1419
|
var viewList = document.getElementById('view-list');
|
|
1420
|
+
var versionsButton = document.getElementById('versions-button');
|
|
1421
|
+
var versionsList = document.getElementById('versions-list');
|
|
1183
1422
|
var strip = document.getElementById('strip');
|
|
1184
1423
|
var level = null;
|
|
1185
1424
|
var cursor = 0;
|
|
@@ -1187,14 +1426,16 @@ const PAGE = `<!doctype html>
|
|
|
1187
1426
|
|
|
1188
1427
|
var STRIP = {
|
|
1189
1428
|
bar: ['<b>Menu</b>', 'letter to open \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1190
|
-
view: ['Menu \\u203A <b>
|
|
1191
|
-
save: ['Menu \\u203A <b>
|
|
1192
|
-
|
|
1429
|
+
view: ['Menu \\u203A <b>Settings</b>', 'letter to flip \\u00B7 \\u2191\\u2193 Enter \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1430
|
+
save: ['Menu \\u203A <b>Export</b>', 'letter to save \\u00B7 \\u2191\\u2193 Enter \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1431
|
+
versions: ['Menu \\u203A <b>Versions</b>', '\\u2191\\u2193 Enter to put one in the editor \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1432
|
+
theme: ['Menu \\u203A Settings \\u203A <b>Theme</b>', '\\u2191\\u2193 to try \\u00B7 Enter keep \\u00B7 \\u232B back \\u00B7 Esc out'],
|
|
1193
1433
|
};
|
|
1194
1434
|
|
|
1195
1435
|
function rows() {
|
|
1196
|
-
if (level === 'view') return Array.prototype.slice.call(viewList.querySelectorAll(':scope > .row'));
|
|
1436
|
+
if (level === 'view') return Array.prototype.slice.call(viewList.querySelectorAll(':scope > .row:not([hidden])'));
|
|
1197
1437
|
if (level === 'save') return Array.prototype.slice.call(saveList.querySelectorAll(':scope > .row'));
|
|
1438
|
+
if (level === 'versions') return Array.prototype.slice.call(versionsList.querySelectorAll(':scope > .row'));
|
|
1198
1439
|
if (level === 'theme') return themeItems.map(function (item) { return item.row; });
|
|
1199
1440
|
return [];
|
|
1200
1441
|
}
|
|
@@ -1225,6 +1466,9 @@ const PAGE = `<!doctype html>
|
|
|
1225
1466
|
saveList.hidden = level !== 'save';
|
|
1226
1467
|
saveButton.classList.toggle('open', level === 'save');
|
|
1227
1468
|
saveButton.setAttribute('aria-expanded', String(level === 'save'));
|
|
1469
|
+
versionsList.hidden = level !== 'versions';
|
|
1470
|
+
versionsButton.classList.toggle('open', level === 'versions');
|
|
1471
|
+
versionsButton.setAttribute('aria-expanded', String(level === 'versions'));
|
|
1228
1472
|
viewList.hidden = level !== 'view' && level !== 'theme';
|
|
1229
1473
|
viewButton.classList.toggle('open', level === 'view' || level === 'theme');
|
|
1230
1474
|
viewButton.setAttribute('aria-expanded', String(level === 'view' || level === 'theme'));
|
|
@@ -1241,7 +1485,7 @@ const PAGE = `<!doctype html>
|
|
|
1241
1485
|
|
|
1242
1486
|
function back() {
|
|
1243
1487
|
if (level === 'theme') setLevel('view');
|
|
1244
|
-
else if (level === 'view' || level === 'save') setLevel('bar');
|
|
1488
|
+
else if (level === 'view' || level === 'save' || level === 'versions') setLevel('bar');
|
|
1245
1489
|
else setLevel(null);
|
|
1246
1490
|
}
|
|
1247
1491
|
|
|
@@ -1409,9 +1653,15 @@ const PAGE = `<!doctype html>
|
|
|
1409
1653
|
return all.slice(0, count);
|
|
1410
1654
|
}
|
|
1411
1655
|
|
|
1656
|
+
// The styles the diagram uses, which a filter's style: names; made again
|
|
1657
|
+
// when an edited page redraws.
|
|
1412
1658
|
var styleNames = {};
|
|
1413
|
-
|
|
1414
|
-
|
|
1659
|
+
function collectStyles() {
|
|
1660
|
+
styleNames = {};
|
|
1661
|
+
Object.keys(graph.nodes).forEach(function (name) { graph.nodes[name].styles.forEach(function (one) { styleNames[one] = true; }); });
|
|
1662
|
+
graph.edges.forEach(function (edge) { edge.styles.forEach(function (one) { styleNames[one] = true; }); });
|
|
1663
|
+
}
|
|
1664
|
+
collectStyles();
|
|
1415
1665
|
|
|
1416
1666
|
// The filter: every word must match. Returns the matches, and a message
|
|
1417
1667
|
// when a word can match nothing at all, naming what it accepts.
|
|
@@ -1655,6 +1905,7 @@ const PAGE = `<!doctype html>
|
|
|
1655
1905
|
}
|
|
1656
1906
|
|
|
1657
1907
|
saveButton.onclick = function () { setLevel(level === 'save' ? null : 'save'); };
|
|
1908
|
+
versionsButton.onclick = function () { setLevel(level === 'versions' ? null : 'versions'); };
|
|
1658
1909
|
viewButton.onclick = function () { setLevel(level === 'view' || level === 'theme' ? null : 'view'); };
|
|
1659
1910
|
themeRow.onclick = function () { setLevel(level === 'theme' ? 'view' : 'theme'); };
|
|
1660
1911
|
document.addEventListener('click', function (event) {
|
|
@@ -1679,16 +1930,21 @@ const PAGE = `<!doctype html>
|
|
|
1679
1930
|
var ZOOM_KEYS = { '+': 1, '=': 1, '-': 1, '_': 1, '0': 1, '1': 1 };
|
|
1680
1931
|
|
|
1681
1932
|
// On the window, before the zoom's own keys, so a menu's letters reach the
|
|
1682
|
-
// menu and not the diagram underneath: g flips guides in
|
|
1933
|
+
// menu and not the diagram underneath: g flips guides in Settings, not a grab.
|
|
1683
1934
|
window.addEventListener('keydown', function (event) {
|
|
1684
1935
|
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
1685
1936
|
if (event.target.closest && event.target.closest('input, textarea, select, [contenteditable]')) return;
|
|
1937
|
+
// A window open over the page has the keys, Escape included.
|
|
1938
|
+
if (document.querySelector('dialog[open]')) return;
|
|
1686
1939
|
var key = event.key;
|
|
1687
1940
|
if (key === 'Escape') plain();
|
|
1688
1941
|
else if (level === 'pick' || level === 'filter') {
|
|
1689
1942
|
if (!hintKey(event)) return;
|
|
1690
1943
|
} else if (level === null) {
|
|
1691
1944
|
if (key === 'm' || key === 'M') setLevel('bar');
|
|
1945
|
+
else if (key === 'e' && EDIT) toggleEditor();
|
|
1946
|
+
else if (key === 'c' && !checkButton.hidden) runCheck();
|
|
1947
|
+
else if (key === '?' && cheatBox) openCheatsheet();
|
|
1692
1948
|
else if (key === 's') startHints('all');
|
|
1693
1949
|
else if (key === '/') startHints('all', true);
|
|
1694
1950
|
else if (key === 'n') startHints('neighbors');
|
|
@@ -1710,10 +1966,13 @@ const PAGE = `<!doctype html>
|
|
|
1710
1966
|
else if (key.length === 1) {
|
|
1711
1967
|
var letter = key.toLowerCase();
|
|
1712
1968
|
if (level === 'bar') {
|
|
1713
|
-
if (letter === '
|
|
1714
|
-
else if (letter === '
|
|
1969
|
+
if (letter === 's') setLevel('view');
|
|
1970
|
+
else if (letter === 'x') setLevel('save');
|
|
1971
|
+
else if (letter === 'o' && tiedFile()) setLevel('versions');
|
|
1972
|
+
else if (letter === 'e' && EDIT) { setLevel(null); toggleEditor(); }
|
|
1973
|
+
else if (letter === 'c' && !checkButton.hidden) { setLevel(null); runCheck(); }
|
|
1715
1974
|
else if (letter === 'f' && !full.hidden) { setLevel(null); toggleFull(); }
|
|
1716
|
-
} else if (level === 'view' || level === 'save') {
|
|
1975
|
+
} else if (level === 'view' || level === 'save' || level === 'versions') {
|
|
1717
1976
|
rows().forEach(function (row, i) {
|
|
1718
1977
|
if (row.getAttribute('data-key') !== letter) return;
|
|
1719
1978
|
cursor = i;
|
|
@@ -1737,6 +1996,928 @@ const PAGE = `<!doctype html>
|
|
|
1737
1996
|
event.preventDefault();
|
|
1738
1997
|
});
|
|
1739
1998
|
|
|
1999
|
+
/* ---- editing: a page tied to a file, and a page with no file ---- */
|
|
2000
|
+
|
|
2001
|
+
// A page with an editor draws in the browser: with the library the server
|
|
2002
|
+
// that served it also serves, or, on the hosted playground, with the
|
|
2003
|
+
// library inlined in the page. Each drawing keeps the view, the theme and
|
|
2004
|
+
// whatever is still there to be selected. A text that doesn't draw leaves
|
|
2005
|
+
// the last picture that did, with the error at the head of the findings.
|
|
2006
|
+
//
|
|
2007
|
+
// Tied to a file, the page follows it: each time the file changes on disk,
|
|
2008
|
+
// the server sends its text, and Save writes it. With no file, the text is
|
|
2009
|
+
// kept in this browser, the examples and Open are offered, and Save
|
|
2010
|
+
// downloads it; served with no file, Save names a file in the folder, and
|
|
2011
|
+
// from then on the page is tied to that file as if it had been opened with it.
|
|
2012
|
+
var heading = document.querySelector('header h1');
|
|
2013
|
+
var drawnLine = document.querySelector('header .drawn');
|
|
2014
|
+
var liveNote = document.getElementById('live');
|
|
2015
|
+
|
|
2016
|
+
function tiedFile() { return EDIT && EDIT.kind === 'file' ? EDIT.file : null; }
|
|
2017
|
+
|
|
2018
|
+
function adopt(next, title) {
|
|
2019
|
+
next.edit = EDIT;
|
|
2020
|
+
DATA = next;
|
|
2021
|
+
graph = DATA.graph;
|
|
2022
|
+
collectStyles();
|
|
2023
|
+
shownTheme = chosenName && DATA.themes[chosenName] ? chosenName : DATA.current;
|
|
2024
|
+
// A page with no file is headed reladraw, whatever the diagram is called.
|
|
2025
|
+
if (!EDIT || EDIT.kind === 'file') {
|
|
2026
|
+
heading.textContent = title;
|
|
2027
|
+
document.title = title;
|
|
2028
|
+
}
|
|
2029
|
+
themeItems[0].row.querySelector('.name').textContent = "Diagram's theme (" + DATA.current + ')';
|
|
2030
|
+
if (selection && !(selection.kind === 'node' ? graph.nodes[selection.key] : graph.edges[selection.key])) selection = null;
|
|
2031
|
+
if (level === 'pick' || level === 'filter') setLevel(null);
|
|
2032
|
+
listFindings(null);
|
|
2033
|
+
if (openedByError) { openedByError = false; if (editing()) showPanel(false); }
|
|
2034
|
+
redraw();
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
var libReady = !EDIT ? Promise.resolve(null)
|
|
2038
|
+
: EDIT.kind === 'hosted' ? Promise.resolve(globalThis.reladraw)
|
|
2039
|
+
: Promise.all(['/lib/index.js', '/lib/page.js', '/lib/page-data.js'].map(function (path) { return import(path); }))
|
|
2040
|
+
.then(function (all) { return Object.assign({}, all[0], all[1], all[2]); });
|
|
2041
|
+
var lib = null;
|
|
2042
|
+
|
|
2043
|
+
function clock(at) {
|
|
2044
|
+
return ('0' + at.getHours()).slice(-2) + ':' + ('0' + at.getMinutes()).slice(-2);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function toClipboard(text) {
|
|
2048
|
+
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
|
|
2049
|
+
return Promise.reject(new Error('no clipboard'));
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
var editorPane = document.getElementById('editor');
|
|
2053
|
+
var editButton = document.getElementById('edit');
|
|
2054
|
+
var splitBar = document.getElementById('split');
|
|
2055
|
+
var sourceBox = document.getElementById('source');
|
|
2056
|
+
var paintBox = document.getElementById('paint');
|
|
2057
|
+
var unsavedMark = document.getElementById('unsaved');
|
|
2058
|
+
var writeButton = document.getElementById('write');
|
|
2059
|
+
// A menu in the source's bar: its button opens and closes its list, and a
|
|
2060
|
+
// row chosen, a click elsewhere or Escape closes it. Returns its closer.
|
|
2061
|
+
function sourceMenu(menu, button, list) {
|
|
2062
|
+
function close() {
|
|
2063
|
+
list.hidden = true;
|
|
2064
|
+
button.classList.remove('open');
|
|
2065
|
+
button.setAttribute('aria-expanded', 'false');
|
|
2066
|
+
}
|
|
2067
|
+
button.onclick = function () {
|
|
2068
|
+
var open = list.hidden;
|
|
2069
|
+
list.hidden = !open;
|
|
2070
|
+
button.classList.toggle('open', open);
|
|
2071
|
+
button.setAttribute('aria-expanded', String(open));
|
|
2072
|
+
};
|
|
2073
|
+
// Choosing a row closes the menu before the row does its work.
|
|
2074
|
+
list.addEventListener('click', function () { close(); }, true);
|
|
2075
|
+
document.addEventListener('click', function (event) {
|
|
2076
|
+
if (!list.hidden && !menu.contains(event.target)) close();
|
|
2077
|
+
});
|
|
2078
|
+
document.addEventListener('keydown', function (event) {
|
|
2079
|
+
if (event.key === 'Escape' && !list.hidden) close();
|
|
2080
|
+
}, true);
|
|
2081
|
+
return close;
|
|
2082
|
+
}
|
|
2083
|
+
// A page with no file keeps Examples, Open and Save in a File menu.
|
|
2084
|
+
var fileMenu = document.getElementById('file-menu');
|
|
2085
|
+
var fileButton = document.getElementById('file-button');
|
|
2086
|
+
var closeFileMenu = sourceMenu(fileMenu, fileButton, document.getElementById('file-list'));
|
|
2087
|
+
// Help: the cheatsheet, and the full reference.
|
|
2088
|
+
var helpMenu = document.getElementById('help-menu');
|
|
2089
|
+
sourceMenu(helpMenu, document.getElementById('help-button'), document.getElementById('help-list'));
|
|
2090
|
+
document.getElementById('write-row').onclick = function () { writeButton.click(); };
|
|
2091
|
+
// Where "Saved" flashes: the Save button, or the File menu it is in.
|
|
2092
|
+
function saveFlash(text) { flash(writeButton.hidden ? fileButton : writeButton, text); }
|
|
2093
|
+
var formatButton = document.getElementById('format');
|
|
2094
|
+
var openButton = document.getElementById('open-file');
|
|
2095
|
+
var formatOpen = document.getElementById('format-open');
|
|
2096
|
+
var browseButton = document.getElementById('browse');
|
|
2097
|
+
|
|
2098
|
+
// \`quiet\` shows it without taking the keys, as when the page opens.
|
|
2099
|
+
function toggleEditor(show, quiet) {
|
|
2100
|
+
if (!EDIT) return;
|
|
2101
|
+
var on = show === undefined ? editorPane.hidden : show;
|
|
2102
|
+
var changed = editorPane.hidden === on;
|
|
2103
|
+
editorPane.hidden = !on;
|
|
2104
|
+
splitBar.hidden = !on;
|
|
2105
|
+
editButton.setAttribute('aria-pressed', String(on));
|
|
2106
|
+
lookButton.setAttribute('aria-pressed', String(!on));
|
|
2107
|
+
// The findings close with the source open, and come back as the page
|
|
2108
|
+
// first shows them when it closes.
|
|
2109
|
+
if (changed) {
|
|
2110
|
+
openedByError = openedByError && on;
|
|
2111
|
+
showPanel(on ? failing : count > 0 && !autoHide);
|
|
2112
|
+
showBand();
|
|
2113
|
+
}
|
|
2114
|
+
if (on) { paintSource(); if (!quiet) sourceBox.focus(); }
|
|
2115
|
+
else if (document.activeElement === sourceBox) sourceBox.blur();
|
|
2116
|
+
}
|
|
2117
|
+
// The View half of the Edit | View switch, apart from the Settings menu.
|
|
2118
|
+
var lookButton = document.getElementById('mode-view');
|
|
2119
|
+
editButton.onclick = function () { toggleEditor(true); };
|
|
2120
|
+
lookButton.onclick = function () { toggleEditor(false); };
|
|
2121
|
+
|
|
2122
|
+
function escapeCode(text) { return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
2123
|
+
// The scanner covers each line end to end, so this rebuilds the text exactly
|
|
2124
|
+
// and only wraps some of it in spans; a lost character would slide the rest
|
|
2125
|
+
// of the line out from under the cursor.
|
|
2126
|
+
// One line of source as colored HTML; the library has to have loaded.
|
|
2127
|
+
function colorLine(line) {
|
|
2128
|
+
var out = '';
|
|
2129
|
+
lib.highlightLine(line).forEach(function (span) {
|
|
2130
|
+
var piece = escapeCode(line.slice(span.start, span.end));
|
|
2131
|
+
out += span.kind === 'plain' ? piece : '<span class="t-' + span.kind + '">' + piece + '</span>';
|
|
2132
|
+
});
|
|
2133
|
+
return out;
|
|
2134
|
+
}
|
|
2135
|
+
function paintSource() {
|
|
2136
|
+
var text = sourceBox.value;
|
|
2137
|
+
var out = '';
|
|
2138
|
+
try {
|
|
2139
|
+
if (!lib) throw new Error('not loaded');
|
|
2140
|
+
text.split(/\\r?\\n/).forEach(function (line) { out += colorLine(line) + '\\n'; });
|
|
2141
|
+
} catch (e) {
|
|
2142
|
+
// Coloring is decoration; the editor has to keep working without it.
|
|
2143
|
+
out = escapeCode(text) + '\\n';
|
|
2144
|
+
}
|
|
2145
|
+
paintBox.innerHTML = out;
|
|
2146
|
+
// A textarea's scroll bar comes out of its own width, so the colored
|
|
2147
|
+
// layer is given the width the text actually gets.
|
|
2148
|
+
paintBox.style.width = sourceBox.clientWidth + 'px';
|
|
2149
|
+
paintBox.scrollTop = sourceBox.scrollTop;
|
|
2150
|
+
}
|
|
2151
|
+
sourceBox.addEventListener('scroll', function () { paintBox.scrollTop = sourceBox.scrollTop; });
|
|
2152
|
+
window.addEventListener('resize', function () { if (EDIT && !editorPane.hidden) paintSource(); });
|
|
2153
|
+
|
|
2154
|
+
// Selects a line of the source, as a finding or an error naming it does.
|
|
2155
|
+
function goToLine(number) {
|
|
2156
|
+
if (!EDIT) return;
|
|
2157
|
+
toggleEditor(true, true);
|
|
2158
|
+
var lines = sourceBox.value.split('\\n');
|
|
2159
|
+
var start = 0;
|
|
2160
|
+
for (var i = 0; i < number - 1 && i < lines.length; i++) start += lines[i].length + 1;
|
|
2161
|
+
var end = start + (lines[number - 1] || '').length;
|
|
2162
|
+
sourceBox.focus();
|
|
2163
|
+
sourceBox.setSelectionRange(start, end);
|
|
2164
|
+
// Chrome doesn't scroll a selection made from outside the field into
|
|
2165
|
+
// view; leaving and coming back is what makes it scroll.
|
|
2166
|
+
sourceBox.blur();
|
|
2167
|
+
sourceBox.focus();
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/* ---- drawing what is typed ---- */
|
|
2171
|
+
|
|
2172
|
+
// The name the diagram is drawn as, which a saved file is given.
|
|
2173
|
+
var fileName = 'diagram.reladraw';
|
|
2174
|
+
var drawnText = DATA.source;
|
|
2175
|
+
var cost = 0;
|
|
2176
|
+
var typing = null;
|
|
2177
|
+
|
|
2178
|
+
// \`check\` looks for findings this once, as Check does.
|
|
2179
|
+
function draw(text, check) {
|
|
2180
|
+
if (text === drawnText || !lib) return;
|
|
2181
|
+
drawnText = text;
|
|
2182
|
+
shownText = text;
|
|
2183
|
+
var checking = autoCheck || !!check;
|
|
2184
|
+
var started = performance.now();
|
|
2185
|
+
var made;
|
|
2186
|
+
try {
|
|
2187
|
+
made = lib.pageData(text, fileName, checking ? {} : { checks: false });
|
|
2188
|
+
if (checking) checked = { text: text, findings: made.data.findings };
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
cost = performance.now() - started;
|
|
2191
|
+
listFindings({ message: error.message, line: error.line });
|
|
2192
|
+
if (panelOn && panel.hidden) { openedByError = true; showPanel(true); }
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
cost = performance.now() - started;
|
|
2196
|
+
adopt(made.data, made.title);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// A diagram quick to draw is drawn on every keystroke; a slow one once the
|
|
2200
|
+
// typing pauses, so the keys aren't held up behind it.
|
|
2201
|
+
function drawSoon() {
|
|
2202
|
+
clearTimeout(typing);
|
|
2203
|
+
if (cost < 50) draw(sourceBox.value);
|
|
2204
|
+
else typing = setTimeout(function () { draw(sourceBox.value); }, 150);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
sourceBox.addEventListener('input', function () {
|
|
2208
|
+
paintSource();
|
|
2209
|
+
edited();
|
|
2210
|
+
drawSoon();
|
|
2211
|
+
});
|
|
2212
|
+
sourceBox.addEventListener('keydown', function (event) {
|
|
2213
|
+
if (event.key === 'Escape') { sourceBox.blur(); event.preventDefault(); }
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
// Text put in whole: a version, an example, a file opened, the file as it is on disk.
|
|
2217
|
+
function load(text) {
|
|
2218
|
+
sourceBox.value = text;
|
|
2219
|
+
paintSource();
|
|
2220
|
+
edited();
|
|
2221
|
+
draw(text);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// Text put in as one edit the browser can undo, with the cursor kept on its line.
|
|
2225
|
+
function replaceSource(text) {
|
|
2226
|
+
var before = sourceBox.value;
|
|
2227
|
+
var line = before.slice(0, sourceBox.selectionStart).split('\\n').length;
|
|
2228
|
+
var scroll = sourceBox.scrollTop;
|
|
2229
|
+
sourceBox.focus();
|
|
2230
|
+
sourceBox.setSelectionRange(0, before.length);
|
|
2231
|
+
var undoable = false;
|
|
2232
|
+
try { undoable = document.execCommand('insertText', false, text); } catch (e) {}
|
|
2233
|
+
if (!undoable || sourceBox.value !== text) {
|
|
2234
|
+
sourceBox.value = text;
|
|
2235
|
+
sourceBox.dispatchEvent(new Event('input'));
|
|
2236
|
+
}
|
|
2237
|
+
var lines = text.split('\\n');
|
|
2238
|
+
var at = 0;
|
|
2239
|
+
for (var i = 0; i < line - 1 && i < lines.length; i++) at += lines[i].length + 1;
|
|
2240
|
+
sourceBox.setSelectionRange(at, at);
|
|
2241
|
+
sourceBox.scrollTop = scroll;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/* ---- the divider between the source and the picture ---- */
|
|
2245
|
+
|
|
2246
|
+
// Kept as a share of the width rather than pixels, so a remembered split
|
|
2247
|
+
// means the same in a wider or narrower window.
|
|
2248
|
+
var SPLIT = 'reladraw.split';
|
|
2249
|
+
var mainBox = document.querySelector('main');
|
|
2250
|
+
function divide(share, keep) {
|
|
2251
|
+
if (share === null) {
|
|
2252
|
+
editorPane.style.removeProperty('width');
|
|
2253
|
+
editorPane.style.removeProperty('max-width');
|
|
2254
|
+
} else {
|
|
2255
|
+
share = Math.max(0.15, Math.min(0.8, share));
|
|
2256
|
+
editorPane.style.width = (share * 100).toFixed(2) + '%';
|
|
2257
|
+
editorPane.style.maxWidth = 'none';
|
|
2258
|
+
splitBar.setAttribute('aria-valuenow', String(Math.round(share * 100)));
|
|
2259
|
+
}
|
|
2260
|
+
paintSource();
|
|
2261
|
+
if (!keep) return;
|
|
2262
|
+
try {
|
|
2263
|
+
if (share === null) localStorage.removeItem(SPLIT);
|
|
2264
|
+
else localStorage.setItem(SPLIT, String(share));
|
|
2265
|
+
} catch (e) {}
|
|
2266
|
+
}
|
|
2267
|
+
function shareAt(x) {
|
|
2268
|
+
var box = mainBox.getBoundingClientRect();
|
|
2269
|
+
return (x - box.left) / box.width;
|
|
2270
|
+
}
|
|
2271
|
+
var splitting = false;
|
|
2272
|
+
splitBar.addEventListener('pointerdown', function (event) {
|
|
2273
|
+
if (event.button !== 0) return;
|
|
2274
|
+
splitting = true;
|
|
2275
|
+
try { splitBar.setPointerCapture(event.pointerId); } catch (e) {}
|
|
2276
|
+
splitBar.classList.add('dragging');
|
|
2277
|
+
document.body.classList.add('dragging');
|
|
2278
|
+
event.preventDefault();
|
|
2279
|
+
});
|
|
2280
|
+
splitBar.addEventListener('pointermove', function (event) { if (splitting) divide(shareAt(event.clientX), false); });
|
|
2281
|
+
function letGo(event) {
|
|
2282
|
+
if (!splitting) return;
|
|
2283
|
+
splitting = false;
|
|
2284
|
+
splitBar.classList.remove('dragging');
|
|
2285
|
+
document.body.classList.remove('dragging');
|
|
2286
|
+
divide(shareAt(event.clientX), true);
|
|
2287
|
+
zoom.show();
|
|
2288
|
+
}
|
|
2289
|
+
splitBar.addEventListener('pointerup', letGo);
|
|
2290
|
+
splitBar.addEventListener('pointercancel', letGo);
|
|
2291
|
+
splitBar.addEventListener('dblclick', function () { divide(null, true); });
|
|
2292
|
+
splitBar.addEventListener('keydown', function (event) {
|
|
2293
|
+
var step = { ArrowLeft: -0.02, ArrowRight: 0.02 }[event.key];
|
|
2294
|
+
if (!step) return;
|
|
2295
|
+
divide(shareAt(splitBar.getBoundingClientRect().left) + step, true);
|
|
2296
|
+
event.preventDefault();
|
|
2297
|
+
event.stopPropagation();
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
/* ---- formatting ---- */
|
|
2301
|
+
|
|
2302
|
+
// Tied to a file, the settings are the nearest .reladraw-format, as
|
|
2303
|
+
// reladraw format reads them; with no file, the ones set in this browser.
|
|
2304
|
+
function settingsText() {
|
|
2305
|
+
var file = tiedFile();
|
|
2306
|
+
if (file) return fetch('/format-settings?file=' + encodeURIComponent(file)).then(function (r) { return r.text(); });
|
|
2307
|
+
return Promise.resolve(formatText());
|
|
2308
|
+
}
|
|
2309
|
+
function formatSource() {
|
|
2310
|
+
Promise.all([libReady, settingsText()]).then(function (got) {
|
|
2311
|
+
var settings;
|
|
2312
|
+
try {
|
|
2313
|
+
settings = lib.readFormatSettings(got[1]);
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
if (tiedFile()) throw error;
|
|
2316
|
+
openFormatSettings();
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
var laid = lib.format(sourceBox.value, settings);
|
|
2320
|
+
if (laid === sourceBox.value) { flash(formatButton, 'Already formatted'); return; }
|
|
2321
|
+
replaceSource(laid);
|
|
2322
|
+
}).catch(function (error) { flash(formatButton, 'Can\\u2019t format: ' + error.message.split('\\n')[0]); });
|
|
2323
|
+
}
|
|
2324
|
+
formatButton.onclick = formatSource;
|
|
2325
|
+
|
|
2326
|
+
// Save on the keyboard works wherever the focus is, and never saves the
|
|
2327
|
+
// page; Shift+Alt+F formats, as in VS Code, by the key's code, since on a
|
|
2328
|
+
// Mac Alt turns F into another character.
|
|
2329
|
+
window.addEventListener('keydown', function (event) {
|
|
2330
|
+
if (!EDIT) return;
|
|
2331
|
+
if ((event.metaKey || event.ctrlKey) && !event.altKey && (event.key === 's' || event.key === 'S')) {
|
|
2332
|
+
event.preventDefault();
|
|
2333
|
+
event.stopImmediatePropagation();
|
|
2334
|
+
saveText();
|
|
2335
|
+
} else if (event.shiftKey && event.altKey && !event.ctrlKey && !event.metaKey && event.code === 'KeyF') {
|
|
2336
|
+
event.preventDefault();
|
|
2337
|
+
event.stopImmediatePropagation();
|
|
2338
|
+
formatSource();
|
|
2339
|
+
}
|
|
2340
|
+
}, true);
|
|
2341
|
+
|
|
2342
|
+
function saveText() {
|
|
2343
|
+
if (tiedFile()) write(false);
|
|
2344
|
+
else if (EDIT.kind === 'local') openSaveAs();
|
|
2345
|
+
else saveDownload();
|
|
2346
|
+
}
|
|
2347
|
+
writeButton.onclick = saveText;
|
|
2348
|
+
|
|
2349
|
+
/* ---- a page tied to a file ---- */
|
|
2350
|
+
|
|
2351
|
+
// What the file holds, as last heard from the server; and what the
|
|
2352
|
+
// editor's text was loaded or saved from, which a save checks the file
|
|
2353
|
+
// against, so a change on disk the person kept their text over is asked
|
|
2354
|
+
// about rather than written over.
|
|
2355
|
+
var disk = null;
|
|
2356
|
+
var base = null;
|
|
2357
|
+
var versions = [];
|
|
2358
|
+
var down = false;
|
|
2359
|
+
var changedStrip = document.getElementById('changed');
|
|
2360
|
+
|
|
2361
|
+
function markUnsaved() {
|
|
2362
|
+
if (!EDIT) return;
|
|
2363
|
+
if (tiedFile()) {
|
|
2364
|
+
unsavedMark.hidden = sourceBox.value === disk;
|
|
2365
|
+
unsavedMark.title = 'Not saved to the file';
|
|
2366
|
+
} else {
|
|
2367
|
+
unsavedMark.hidden = !unsavedHere();
|
|
2368
|
+
unsavedMark.title = 'Not saved to a file';
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// Ties the page to a file whose text is \`text\`: from a served page's start,
|
|
2373
|
+
// or from the first Save of a page with no file, after which the page is
|
|
2374
|
+
// what it would have been opened with the file.
|
|
2375
|
+
function tie(file, text) {
|
|
2376
|
+
var fresh = !!EDIT && EDIT.kind !== 'file';
|
|
2377
|
+
EDIT = DATA.edit = { kind: 'file', file: file };
|
|
2378
|
+
fileName = file.split('/').pop();
|
|
2379
|
+
disk = text;
|
|
2380
|
+
base = text;
|
|
2381
|
+
// A page tied to a file has only Save, so it is a button, not a File menu.
|
|
2382
|
+
closeFileMenu();
|
|
2383
|
+
fileMenu.hidden = true;
|
|
2384
|
+
writeButton.hidden = false;
|
|
2385
|
+
formatOpen.hidden = true;
|
|
2386
|
+
document.getElementById('versions-menu').hidden = false;
|
|
2387
|
+
writeButton.querySelector('.name').textContent = 'Save';
|
|
2388
|
+
writeButton.title = 'Write the source to the file (\\u2318S or Ctrl+S)';
|
|
2389
|
+
formatButton.title = 'Lay the source out again, as reladraw format does, with the nearest .reladraw-format (Shift+Alt+F)';
|
|
2390
|
+
editorPane.querySelector('.file').textContent = file;
|
|
2391
|
+
drawnLine.textContent = file + ' \\u00b7 live, read ' + clock(new Date());
|
|
2392
|
+
var links = document.querySelector('header .links');
|
|
2393
|
+
if (links) links.remove();
|
|
2394
|
+
if (fresh) {
|
|
2395
|
+
// Drawn again under the file's name, and headed with the diagram's title.
|
|
2396
|
+
drawnText = null;
|
|
2397
|
+
draw(sourceBox.value);
|
|
2398
|
+
}
|
|
2399
|
+
markUnsaved();
|
|
2400
|
+
follow(file);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function who(version) {
|
|
2404
|
+
if (version.who === 'you') return 'you';
|
|
2405
|
+
if (version.who === 'agent') return 'agent' + (version.note ? ': \\u201c' + version.note + '\\u201d' : '');
|
|
2406
|
+
return 'changed on disk';
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// A change on disk loads at once, unless the editor holds text that isn't
|
|
2410
|
+
// in the file: then the text stays and a strip offers the file instead.
|
|
2411
|
+
function heard(message) {
|
|
2412
|
+
var clean = sourceBox.value === disk && base === disk;
|
|
2413
|
+
disk = message.text;
|
|
2414
|
+
versions = message.versions || [];
|
|
2415
|
+
listVersions();
|
|
2416
|
+
if (clean || sourceBox.value === disk) {
|
|
2417
|
+
base = disk;
|
|
2418
|
+
changedStrip.hidden = true;
|
|
2419
|
+
load(disk);
|
|
2420
|
+
} else {
|
|
2421
|
+
var last = versions[versions.length - 1];
|
|
2422
|
+
changedStrip.querySelector('.what').textContent =
|
|
2423
|
+
fileName + ' changed on disk at ' + clock(new Date(message.at)) + (last ? ' (' + who(last) + ')' : '');
|
|
2424
|
+
changedStrip.hidden = false;
|
|
2425
|
+
if (editorPane.hidden) toggleEditor(true);
|
|
2426
|
+
}
|
|
2427
|
+
markUnsaved();
|
|
2428
|
+
}
|
|
2429
|
+
document.getElementById('load-disk').onclick = function () {
|
|
2430
|
+
base = disk;
|
|
2431
|
+
changedStrip.hidden = true;
|
|
2432
|
+
load(disk);
|
|
2433
|
+
};
|
|
2434
|
+
document.getElementById('keep-mine').onclick = function () { changedStrip.hidden = true; };
|
|
2435
|
+
|
|
2436
|
+
function write(replace) {
|
|
2437
|
+
var file = tiedFile();
|
|
2438
|
+
var text = sourceBox.value;
|
|
2439
|
+
return fetch('/save?file=' + encodeURIComponent(file), {
|
|
2440
|
+
method: 'POST',
|
|
2441
|
+
body: JSON.stringify({ text: text, base: base, replace: !!replace }),
|
|
2442
|
+
}).then(function (answer) {
|
|
2443
|
+
if (answer.status === 409) {
|
|
2444
|
+
return answer.json().then(function (now) {
|
|
2445
|
+
var what = fileName + ' changed on disk at ' + clock(new Date(now.at)) +
|
|
2446
|
+
(now.who === 'agent' ? ' (agent' + (now.note ? ': \\u201c' + now.note + '\\u201d' : '') + ')' : '') +
|
|
2447
|
+
' after you started editing. Saving puts your text in its place; the version on disk stays in Versions.\\n\\nReplace the file on disk?';
|
|
2448
|
+
if (confirm(what)) return write(true);
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
if (!answer.ok) throw new Error('the server said ' + answer.status);
|
|
2452
|
+
disk = text;
|
|
2453
|
+
base = text;
|
|
2454
|
+
changedStrip.hidden = true;
|
|
2455
|
+
markUnsaved();
|
|
2456
|
+
saveFlash('Saved');
|
|
2457
|
+
}).catch(function (error) {
|
|
2458
|
+
saveFlash('Not saved');
|
|
2459
|
+
liveNote.textContent = 'Couldn\\u2019t save ' + file + ': ' + error.message;
|
|
2460
|
+
liveNote.hidden = false;
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// Newest first. Picking one puts it in the editor as unsaved text.
|
|
2465
|
+
function listVersions() {
|
|
2466
|
+
versionsList.textContent = '';
|
|
2467
|
+
versions.slice().reverse().forEach(function (version, i) {
|
|
2468
|
+
var row = element('button', 'row');
|
|
2469
|
+
row.type = 'button';
|
|
2470
|
+
row.setAttribute('role', 'menuitem');
|
|
2471
|
+
var label = version.who === 'opened' ? 'opened' : version.who === 'disk' ? 'on disk' : version.who;
|
|
2472
|
+
var about = version.who === 'opened' ? 'as the server first read it'
|
|
2473
|
+
: version.who === 'disk' ? 'changed on disk'
|
|
2474
|
+
: version.who === 'agent' ? (version.note || 'changed by an agent')
|
|
2475
|
+
: 'saved from this page';
|
|
2476
|
+
row.append(element('span', 'when', clock(new Date(version.at))), element('span', 'who ' + version.who, label),
|
|
2477
|
+
element('span', 'about', about + (i === 0 ? ' (in the file now)' : '')));
|
|
2478
|
+
row.onclick = function () {
|
|
2479
|
+
setLevel(null);
|
|
2480
|
+
toggleEditor(true);
|
|
2481
|
+
load(version.text);
|
|
2482
|
+
};
|
|
2483
|
+
versionsList.append(row);
|
|
2484
|
+
});
|
|
2485
|
+
if (!versions.length) versionsList.append(element('span', 'none', 'No versions yet.'));
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function follow(file) {
|
|
2489
|
+
var events = new EventSource('/events?file=' + encodeURIComponent(file));
|
|
2490
|
+
events.addEventListener('source', function (event) {
|
|
2491
|
+
var message = JSON.parse(event.data);
|
|
2492
|
+
if (down) {
|
|
2493
|
+
down = false;
|
|
2494
|
+
liveNote.hidden = true;
|
|
2495
|
+
writeButton.disabled = false;
|
|
2496
|
+
}
|
|
2497
|
+
drawnLine.textContent = file + ' \\u00b7 live, ' + (message.changed ? 'changed ' : 'read ') + clock(new Date(message.at));
|
|
2498
|
+
heard(message);
|
|
2499
|
+
});
|
|
2500
|
+
events.onerror = function () {
|
|
2501
|
+
if (down) return;
|
|
2502
|
+
down = true;
|
|
2503
|
+
writeButton.disabled = true;
|
|
2504
|
+
drawnLine.textContent = file + ' \\u00b7 not live';
|
|
2505
|
+
liveNote.textContent = 'The server has stopped, so this page can\\u2019t save or follow ' + file +
|
|
2506
|
+
'. 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.';
|
|
2507
|
+
liveNote.hidden = false;
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
window.addEventListener('beforeunload', function (event) {
|
|
2512
|
+
if (tiedFile() && sourceBox.value !== disk) { event.preventDefault(); event.returnValue = ''; }
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
/* ---- a page with no file: the text kept in this browser ---- */
|
|
2516
|
+
|
|
2517
|
+
// The same names the playground has always kept them under, so a visitor
|
|
2518
|
+
// finds their diagram where they left it.
|
|
2519
|
+
var STORE = 'reladraw.source';
|
|
2520
|
+
var KEPT_TEXT = 'reladraw.kept';
|
|
2521
|
+
var KEPT_NAME = 'reladraw.name';
|
|
2522
|
+
function stored(key) { try { return localStorage.getItem(key); } catch (e) { return null; } }
|
|
2523
|
+
function store(key, value) { try { localStorage.setItem(key, value); } catch (e) {} }
|
|
2524
|
+
|
|
2525
|
+
// The text as last saved or opened, so opening another file asks first only
|
|
2526
|
+
// when there is something to lose; and the example the editor holds
|
|
2527
|
+
// unchanged, or null once it is edited, since an example left alone is one
|
|
2528
|
+
// click away and nothing to lose.
|
|
2529
|
+
var keptText = null;
|
|
2530
|
+
var current = null;
|
|
2531
|
+
var stowing = null;
|
|
2532
|
+
var EXAMPLES = EDIT && EDIT.examples ? EDIT.examples : [];
|
|
2533
|
+
|
|
2534
|
+
function named(text) {
|
|
2535
|
+
for (var i = 0; i < EXAMPLES.length; i++) if (EXAMPLES[i].source === text) return EXAMPLES[i].name;
|
|
2536
|
+
return null;
|
|
2537
|
+
}
|
|
2538
|
+
function unsavedHere() { return sourceBox.value !== keptText && current === null; }
|
|
2539
|
+
|
|
2540
|
+
function edited() {
|
|
2541
|
+
if (EDIT && EDIT.kind !== 'file') {
|
|
2542
|
+
current = named(sourceBox.value);
|
|
2543
|
+
// The stored copy trails the typing: it isn't read until the page is opened again.
|
|
2544
|
+
clearTimeout(stowing);
|
|
2545
|
+
stowing = setTimeout(function () { store(STORE, sourceBox.value); }, 400);
|
|
2546
|
+
}
|
|
2547
|
+
markUnsaved();
|
|
2548
|
+
}
|
|
2549
|
+
function keep(text, name) {
|
|
2550
|
+
keptText = text;
|
|
2551
|
+
fileName = name;
|
|
2552
|
+
store(KEPT_TEXT, text);
|
|
2553
|
+
store(KEPT_NAME, name);
|
|
2554
|
+
if (EDIT.kind === 'hosted') editorPane.querySelector('.file').textContent = name;
|
|
2555
|
+
markUnsaved();
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// A different diagram opens as a fresh page would, not at the last one's zoom.
|
|
2559
|
+
function openText(text) {
|
|
2560
|
+
load(text);
|
|
2561
|
+
zoom.open();
|
|
2562
|
+
sourceBox.focus();
|
|
2563
|
+
sourceBox.setSelectionRange(0, 0);
|
|
2564
|
+
sourceBox.scrollTop = 0;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
var fileInput = document.getElementById('file-input');
|
|
2568
|
+
openButton.onclick = function () {
|
|
2569
|
+
if (unsavedHere() && !confirm('Your edits have not been saved to a file. Open another file in their place?')) return;
|
|
2570
|
+
fileInput.value = '';
|
|
2571
|
+
fileInput.click();
|
|
2572
|
+
};
|
|
2573
|
+
fileInput.onchange = function () {
|
|
2574
|
+
var picked = fileInput.files[0];
|
|
2575
|
+
if (!picked) return;
|
|
2576
|
+
picked.text().then(function (text) {
|
|
2577
|
+
keep(text, picked.name);
|
|
2578
|
+
openText(text);
|
|
2579
|
+
});
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
// Chrome and Edge ask for the name and the folder; elsewhere the file goes
|
|
2583
|
+
// where the browser puts downloads, under the suggested name.
|
|
2584
|
+
function saveDownload() {
|
|
2585
|
+
var text = sourceBox.value;
|
|
2586
|
+
function download() {
|
|
2587
|
+
saveBlob(new Blob([text], { type: 'text/plain' }), fileName);
|
|
2588
|
+
keep(text, fileName);
|
|
2589
|
+
}
|
|
2590
|
+
if (!window.showSaveFilePicker) return download();
|
|
2591
|
+
window.showSaveFilePicker({ suggestedName: fileName, types: [{ description: 'reladraw source', accept: { 'text/plain': ['.reladraw'] } }] })
|
|
2592
|
+
.then(function (handle) {
|
|
2593
|
+
return handle.createWritable().then(function (out) {
|
|
2594
|
+
return out.write(text).then(function () { return out.close(); }).then(function () {
|
|
2595
|
+
keep(text, handle.name);
|
|
2596
|
+
saveFlash('Saved');
|
|
2597
|
+
});
|
|
2598
|
+
});
|
|
2599
|
+
})
|
|
2600
|
+
.catch(function (e) {
|
|
2601
|
+
if (e && e.name === 'AbortError') return;
|
|
2602
|
+
// Refused outright, as inside a cross-origin frame.
|
|
2603
|
+
download();
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// Served with no file, the first Save names a file in the folder, typed
|
|
2608
|
+
// rather than browsed for, and asks before replacing one that is there.
|
|
2609
|
+
var saveAs = document.getElementById('save-as');
|
|
2610
|
+
function openSaveAs() {
|
|
2611
|
+
var path = document.getElementById('save-path');
|
|
2612
|
+
document.getElementById('save-problem').textContent = '';
|
|
2613
|
+
if (!path.value) path.value = fileName;
|
|
2614
|
+
saveAs.showModal();
|
|
2615
|
+
path.select();
|
|
2616
|
+
}
|
|
2617
|
+
function create(path, replace) {
|
|
2618
|
+
var text = sourceBox.value;
|
|
2619
|
+
var problem = document.getElementById('save-problem');
|
|
2620
|
+
return fetch('/create?file=' + encodeURIComponent(path), {
|
|
2621
|
+
method: 'POST',
|
|
2622
|
+
body: JSON.stringify({ text: text, replace: !!replace }),
|
|
2623
|
+
}).then(function (answer) {
|
|
2624
|
+
return answer.json().then(function (told) {
|
|
2625
|
+
if (answer.status === 409 && told.exists) {
|
|
2626
|
+
if (confirm(told.file + ' is there already. Replace it with this diagram?')) return create(path, true);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
if (!answer.ok) { problem.textContent = told.error || 'The server said ' + answer.status; return; }
|
|
2630
|
+
saveAs.close();
|
|
2631
|
+
keep(text, told.file.split('/').pop());
|
|
2632
|
+
history.replaceState(null, '', '/' + told.file.split('/').map(encodeURIComponent).join('/'));
|
|
2633
|
+
tie(told.file, text);
|
|
2634
|
+
saveFlash('Saved');
|
|
2635
|
+
});
|
|
2636
|
+
}).catch(function (error) {
|
|
2637
|
+
problem.textContent = 'Couldn\\u2019t save: ' + error.message + '. Is reladraw serve still running?';
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
if (saveAs) {
|
|
2641
|
+
document.getElementById('save-form').addEventListener('submit', function (event) {
|
|
2642
|
+
event.preventDefault();
|
|
2643
|
+
var path = document.getElementById('save-path').value.trim();
|
|
2644
|
+
if (!path) return;
|
|
2645
|
+
if (!/\\.reladraw$/.test(path)) path += '.reladraw';
|
|
2646
|
+
create(path, false);
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
/* ---- the examples ---- */
|
|
2651
|
+
|
|
2652
|
+
var examplesBox = document.getElementById('examples');
|
|
2653
|
+
var catalog = document.getElementById('catalog');
|
|
2654
|
+
var preview = document.getElementById('preview');
|
|
2655
|
+
var aboutLine = document.getElementById('about');
|
|
2656
|
+
var chosenExample = 0;
|
|
2657
|
+
var previews = {};
|
|
2658
|
+
|
|
2659
|
+
function pickExample(i) {
|
|
2660
|
+
chosenExample = Math.max(0, Math.min(EXAMPLES.length - 1, i));
|
|
2661
|
+
each(catalog.children, function (item, j) { item.setAttribute('aria-selected', String(j === chosenExample)); });
|
|
2662
|
+
var item = catalog.children[chosenExample];
|
|
2663
|
+
catalog.setAttribute('aria-activedescendant', item.id);
|
|
2664
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
2665
|
+
var example = EXAMPLES[chosenExample];
|
|
2666
|
+
if (!(example.name in previews)) {
|
|
2667
|
+
try { previews[example.name] = lib ? lib.compile(example.source) : ''; } catch (e) { previews[example.name] = ''; }
|
|
2668
|
+
}
|
|
2669
|
+
aboutLine.textContent = example.about || '';
|
|
2670
|
+
preview.innerHTML = previews[example.name];
|
|
2671
|
+
}
|
|
2672
|
+
function openExample() {
|
|
2673
|
+
var example = EXAMPLES[chosenExample];
|
|
2674
|
+
examplesBox.close();
|
|
2675
|
+
if (unsavedHere() && !confirm('Your edits have not been saved to a file. Open ' + example.name + ' in their place?')) return;
|
|
2676
|
+
openText(example.source);
|
|
2677
|
+
}
|
|
2678
|
+
// A click on the backdrop lands on the dialog itself, outside its box.
|
|
2679
|
+
function closeOnBackdrop(box) {
|
|
2680
|
+
box.addEventListener('click', function (event) {
|
|
2681
|
+
if (event.target !== box) return;
|
|
2682
|
+
var r = box.getBoundingClientRect();
|
|
2683
|
+
if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) box.close();
|
|
2684
|
+
});
|
|
2685
|
+
each(box.querySelectorAll('[data-close]'), function (button) { button.onclick = function () { box.close(); }; });
|
|
2686
|
+
}
|
|
2687
|
+
if (examplesBox) {
|
|
2688
|
+
EXAMPLES.forEach(function (example, i) {
|
|
2689
|
+
var item = element('li', '', example.name);
|
|
2690
|
+
item.id = 'example-' + i;
|
|
2691
|
+
item.setAttribute('role', 'option');
|
|
2692
|
+
item.onclick = function () { pickExample(i); };
|
|
2693
|
+
item.ondblclick = openExample;
|
|
2694
|
+
catalog.append(item);
|
|
2695
|
+
});
|
|
2696
|
+
browseButton.onclick = function () {
|
|
2697
|
+
var at = 0;
|
|
2698
|
+
EXAMPLES.forEach(function (example, i) { if (example.name === current) at = i; });
|
|
2699
|
+
examplesBox.showModal();
|
|
2700
|
+
pickExample(at);
|
|
2701
|
+
catalog.focus();
|
|
2702
|
+
};
|
|
2703
|
+
catalog.addEventListener('keydown', function (event) {
|
|
2704
|
+
var step = { ArrowDown: 1, ArrowUp: -1, PageDown: 5, PageUp: -5 }[event.key];
|
|
2705
|
+
if (step) pickExample(chosenExample + step);
|
|
2706
|
+
else if (event.key === 'Home') pickExample(0);
|
|
2707
|
+
else if (event.key === 'End') pickExample(EXAMPLES.length - 1);
|
|
2708
|
+
else if (event.key === 'Enter') openExample();
|
|
2709
|
+
else return;
|
|
2710
|
+
event.preventDefault();
|
|
2711
|
+
});
|
|
2712
|
+
document.getElementById('choose').onclick = openExample;
|
|
2713
|
+
closeOnBackdrop(examplesBox);
|
|
2714
|
+
// Every drawing names its arrowheads alike, and a page takes the first of
|
|
2715
|
+
// a name it finds: a preview left in the closed dialog would take the
|
|
2716
|
+
// diagram's arrowheads with it.
|
|
2717
|
+
examplesBox.addEventListener('close', function () { preview.innerHTML = ''; });
|
|
2718
|
+
closeOnBackdrop(saveAs);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/* ---- format settings, for a page with no file ---- */
|
|
2722
|
+
|
|
2723
|
+
// The settings as \`name: value\` lines, the way a .reladraw-format holds
|
|
2724
|
+
// them, so the panel, the stored copy and the copied file are one text and
|
|
2725
|
+
// the library's own reader refuses what it would refuse in a file.
|
|
2726
|
+
var FORMAT = 'reladraw.format';
|
|
2727
|
+
var formatPanel = document.getElementById('format-settings');
|
|
2728
|
+
var formatValues = {};
|
|
2729
|
+
(stored(FORMAT) || '').split('\\n').forEach(function (line) {
|
|
2730
|
+
var colon = line.indexOf(':');
|
|
2731
|
+
if (colon > 0) formatValues[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
|
|
2732
|
+
});
|
|
2733
|
+
function formatText() {
|
|
2734
|
+
if (!lib) return '';
|
|
2735
|
+
return lib.SETTINGS.map(function (setting) {
|
|
2736
|
+
var value = formatValues[setting.name];
|
|
2737
|
+
return setting.name + ': ' + (value === undefined || value === '' ? setting.written : value);
|
|
2738
|
+
}).join('\\n');
|
|
2739
|
+
}
|
|
2740
|
+
function checkSettings() {
|
|
2741
|
+
var problem = document.getElementById('format-problem');
|
|
2742
|
+
try { lib.readFormatSettings(formatText()); problem.textContent = ''; } catch (e) { problem.textContent = e.message; }
|
|
2743
|
+
}
|
|
2744
|
+
// A setting whose values are all words gets a list; one that takes a
|
|
2745
|
+
// number or a list gets a field, written as it would be in the file.
|
|
2746
|
+
function buildFormatFields() {
|
|
2747
|
+
var fields = document.getElementById('format-fields');
|
|
2748
|
+
fields.textContent = '';
|
|
2749
|
+
lib.SETTINGS.forEach(function (setting) {
|
|
2750
|
+
var label = element('label', '');
|
|
2751
|
+
label.append(element('code', '', setting.name));
|
|
2752
|
+
var words = setting.values.every(function (value) { return /^[a-z-]+$/.test(value[0]); });
|
|
2753
|
+
var control;
|
|
2754
|
+
if (words) {
|
|
2755
|
+
control = element('select', '');
|
|
2756
|
+
setting.values.forEach(function (value) {
|
|
2757
|
+
var option = element('option', '', value[0]);
|
|
2758
|
+
option.value = value[0];
|
|
2759
|
+
control.append(option);
|
|
2760
|
+
});
|
|
2761
|
+
} else {
|
|
2762
|
+
control = element('input', '');
|
|
2763
|
+
control.type = 'text';
|
|
2764
|
+
control.spellcheck = false;
|
|
2765
|
+
control.placeholder = setting.written;
|
|
2766
|
+
}
|
|
2767
|
+
control.value = formatValues[setting.name] || setting.written;
|
|
2768
|
+
control.addEventListener(words ? 'change' : 'input', function () {
|
|
2769
|
+
formatValues[setting.name] = control.value.trim();
|
|
2770
|
+
checkSettings();
|
|
2771
|
+
store(FORMAT, formatText());
|
|
2772
|
+
});
|
|
2773
|
+
label.append(control, element('span', '', setting.about.replace(/\\n/g, ' ') + ' ' +
|
|
2774
|
+
setting.values.map(function (value) { return value[0] + ': ' + value[1].replace(/\\n/g, ' '); }).join('; ') + '.'));
|
|
2775
|
+
fields.append(label);
|
|
2776
|
+
});
|
|
2777
|
+
checkSettings();
|
|
2778
|
+
}
|
|
2779
|
+
function openFormatSettings() {
|
|
2780
|
+
libReady.then(function () {
|
|
2781
|
+
buildFormatFields();
|
|
2782
|
+
formatPanel.showModal();
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
if (formatPanel) {
|
|
2786
|
+
formatOpen.onclick = openFormatSettings;
|
|
2787
|
+
closeOnBackdrop(formatPanel);
|
|
2788
|
+
document.getElementById('format-reset').onclick = function () {
|
|
2789
|
+
formatValues = {};
|
|
2790
|
+
store(FORMAT, formatText());
|
|
2791
|
+
buildFormatFields();
|
|
2792
|
+
};
|
|
2793
|
+
var formatCopy = document.getElementById('format-copy');
|
|
2794
|
+
formatCopy.onclick = function () {
|
|
2795
|
+
var text = '// .reladraw-format: how reladraw format lays out the diagrams in this folder and below\\n' + formatText() + '\\n';
|
|
2796
|
+
toClipboard(text).then(function () { flash(formatCopy, 'Copied'); }, function () { flash(formatCopy, 'Blocked'); });
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
/* ---- the cheatsheet ---- */
|
|
2801
|
+
|
|
2802
|
+
// A window of cards, each row an example colored as the editor colors it.
|
|
2803
|
+
// The filter keeps the rows holding every word typed, matching the card's
|
|
2804
|
+
// name too, so "placement" keeps a whole card.
|
|
2805
|
+
var cheatBox = document.getElementById('cheatsheet');
|
|
2806
|
+
var openCheatsheet = function () {};
|
|
2807
|
+
if (cheatBox) {
|
|
2808
|
+
var cheatFilter = document.getElementById('cheat-filter');
|
|
2809
|
+
var cheatNone = cheatBox.querySelector('.cs-none');
|
|
2810
|
+
var cheatColored = false;
|
|
2811
|
+
var cheatGrid = cheatBox.querySelector('.cs-grid');
|
|
2812
|
+
var cheatCards = Array.prototype.slice.call(cheatGrid.querySelectorAll('.cs-card'));
|
|
2813
|
+
var cheatCols = element('div', 'cs-cols');
|
|
2814
|
+
cheatGrid.append(cheatCols);
|
|
2815
|
+
// Each card, in order, goes to the shortest column so far.
|
|
2816
|
+
var layoutCheat = function () {
|
|
2817
|
+
var width = cheatGrid.clientWidth;
|
|
2818
|
+
var count = width >= 1000 ? 3 : width >= 640 ? 2 : 1;
|
|
2819
|
+
cheatCols.style.setProperty('--cols', String(count));
|
|
2820
|
+
cheatCols.textContent = '';
|
|
2821
|
+
var cols = [];
|
|
2822
|
+
var heights = [];
|
|
2823
|
+
for (var i = 0; i < count; i++) {
|
|
2824
|
+
cols.push(element('div', 'cs-col'));
|
|
2825
|
+
heights.push(0);
|
|
2826
|
+
cheatCols.append(cols[i]);
|
|
2827
|
+
}
|
|
2828
|
+
cheatCards.forEach(function (card) {
|
|
2829
|
+
var at = heights.indexOf(Math.min.apply(null, heights));
|
|
2830
|
+
cols[at].append(card);
|
|
2831
|
+
if (!card.hidden) heights[at] += card.offsetHeight + 14;
|
|
2832
|
+
});
|
|
2833
|
+
};
|
|
2834
|
+
window.addEventListener('resize', function () { if (cheatBox.open) layoutCheat(); });
|
|
2835
|
+
openCheatsheet = function () {
|
|
2836
|
+
if (!cheatColored && lib) {
|
|
2837
|
+
cheatColored = true;
|
|
2838
|
+
each(cheatBox.querySelectorAll('.cs-row code'), function (code) {
|
|
2839
|
+
try { code.innerHTML = colorLine(code.textContent); } catch (e) {}
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
cheatBox.showModal();
|
|
2843
|
+
layoutCheat();
|
|
2844
|
+
cheatFilter.focus();
|
|
2845
|
+
cheatFilter.select();
|
|
2846
|
+
};
|
|
2847
|
+
helpMenu.hidden = false;
|
|
2848
|
+
document.getElementById('cheat-open').onclick = openCheatsheet;
|
|
2849
|
+
closeOnBackdrop(cheatBox);
|
|
2850
|
+
cheatFilter.addEventListener('input', function () {
|
|
2851
|
+
var words = cheatFilter.value.toLowerCase().split(/\\s+/).filter(Boolean);
|
|
2852
|
+
var matches = function (text) {
|
|
2853
|
+
text = text.toLowerCase();
|
|
2854
|
+
return words.every(function (word) { return text.indexOf(word) >= 0; });
|
|
2855
|
+
};
|
|
2856
|
+
var any = false;
|
|
2857
|
+
each(cheatBox.querySelectorAll('.cs-card'), function (card) {
|
|
2858
|
+
var title = card.querySelector('h3').textContent;
|
|
2859
|
+
var shown = false;
|
|
2860
|
+
each(card.querySelectorAll('.cs-row'), function (row) {
|
|
2861
|
+
row.hidden = !matches(title + ' ' + row.textContent);
|
|
2862
|
+
if (!row.hidden) shown = true;
|
|
2863
|
+
});
|
|
2864
|
+
var note = card.querySelector('.cs-note');
|
|
2865
|
+
if (note) {
|
|
2866
|
+
note.hidden = !matches(title + ' ' + note.textContent);
|
|
2867
|
+
if (!note.hidden) shown = true;
|
|
2868
|
+
}
|
|
2869
|
+
card.hidden = !shown;
|
|
2870
|
+
if (shown) any = true;
|
|
2871
|
+
});
|
|
2872
|
+
cheatNone.hidden = any;
|
|
2873
|
+
layoutCheat();
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
/* ---- the start ---- */
|
|
2878
|
+
|
|
2879
|
+
if (EDIT) {
|
|
2880
|
+
document.getElementById('mode').hidden = false;
|
|
2881
|
+
checkButton.hidden = autoCheck;
|
|
2882
|
+
autoCheckRow.hidden = false;
|
|
2883
|
+
setSwitch(autoCheckRow, autoCheck);
|
|
2884
|
+
try {
|
|
2885
|
+
var share = parseFloat(localStorage.getItem(SPLIT));
|
|
2886
|
+
if (share > 0 && share < 1) divide(share, false);
|
|
2887
|
+
} catch (e) {}
|
|
2888
|
+
if (EDIT.kind === 'file') {
|
|
2889
|
+
sourceBox.value = DATA.source;
|
|
2890
|
+
tie(EDIT.file, DATA.source);
|
|
2891
|
+
} else {
|
|
2892
|
+
fileMenu.hidden = false;
|
|
2893
|
+
writeButton.hidden = true;
|
|
2894
|
+
formatOpen.hidden = false;
|
|
2895
|
+
writeButton.querySelector('.name').textContent = 'Save\\u2026';
|
|
2896
|
+
writeButton.title = EDIT.kind === 'local'
|
|
2897
|
+
? 'Save the source as a file in ' + EDIT.folder + ' (\\u2318S or Ctrl+S)'
|
|
2898
|
+
: 'Save the source as a .reladraw file (\\u2318S or Ctrl+S)';
|
|
2899
|
+
formatButton.title = 'Lay the source out again (Shift+Alt+F)';
|
|
2900
|
+
if (saveAs) saveAs.querySelector('.folder').textContent = EDIT.folder || '';
|
|
2901
|
+
keptText = stored(KEPT_TEXT);
|
|
2902
|
+
fileName = stored(KEPT_NAME) || fileName;
|
|
2903
|
+
editorPane.querySelector('.file').textContent = EDIT.kind === 'local' ? 'not saved to a file' : fileName;
|
|
2904
|
+
var opening = stored(STORE);
|
|
2905
|
+
sourceBox.value = opening !== null ? opening : DATA.source;
|
|
2906
|
+
current = named(sourceBox.value);
|
|
2907
|
+
// The playground opens with the source shown; a file's page with it hidden.
|
|
2908
|
+
toggleEditor(true, true);
|
|
2909
|
+
markUnsaved();
|
|
2910
|
+
}
|
|
2911
|
+
libReady.then(function (loaded) {
|
|
2912
|
+
lib = loaded;
|
|
2913
|
+
paintSource();
|
|
2914
|
+
draw(sourceBox.value);
|
|
2915
|
+
}, function (error) {
|
|
2916
|
+
liveNote.textContent = 'The page couldn\\u2019t load the library it draws with: ' + error.message;
|
|
2917
|
+
liveNote.hidden = false;
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
|
|
1740
2921
|
layoutSide();
|
|
1741
2922
|
zoom.show();
|
|
1742
2923
|
})();
|
|
@@ -1744,3 +2925,152 @@ const PAGE = `<!doctype html>
|
|
|
1744
2925
|
</body>
|
|
1745
2926
|
</html>
|
|
1746
2927
|
`;
|
|
2928
|
+
/** What a page with an editor shows under its source: the language on one screen. */
|
|
2929
|
+
const CHEATSHEET = `
|
|
2930
|
+
<dialog id="cheatsheet" aria-label="Cheatsheet">
|
|
2931
|
+
<div class="cs-head">
|
|
2932
|
+
<h2>Cheatsheet</h2>
|
|
2933
|
+
<input id="cheat-filter" type="search" placeholder="Filter: above, gap, color…" autocomplete="off" spellcheck="false" aria-label="Filter the cheatsheet">
|
|
2934
|
+
<span class="gap"></span>
|
|
2935
|
+
<button class="close" type="button" data-close title="Close (Esc)" aria-label="Close the cheatsheet">×</button>
|
|
2936
|
+
</div>
|
|
2937
|
+
<div class="cs-grid">
|
|
2938
|
+
<section class="cs-card" style="--hue: var(--syn-keyword)"><h3>Statements</h3>
|
|
2939
|
+
<div class="cs-row"><code>node api "API"</code><span>a box, with its text</span></div>
|
|
2940
|
+
<div class="cs-row"><code>edge a -> b</code><span>a line from a to b</span></div>
|
|
2941
|
+
<div class="cs-row"><code>edge a <- b</code><span>the same line, written target first</span></div>
|
|
2942
|
+
<div class="cs-row"><code>edge a <-> b</code><span>arrows at both ends</span></div>
|
|
2943
|
+
<div class="cs-row"><code>point p 2 above a top-center</code><span>a named spot, to place against or pass through</span></div>
|
|
2944
|
+
<div class="cs-row"><code>region r a top-left to c bottom-right</code><span>a named area, from one corner to another</span></div>
|
|
2945
|
+
<div class="cs-row"><code>bundle trunk via: through r horizontal</code><span>a path that lines share</span></div>
|
|
2946
|
+
<div class="cs-row"><code>guide node g right of a</code><span>for reference only; Guides in Settings shows it</span></div>
|
|
2947
|
+
<div class="cs-row"><code>style hot fill: red</code><span>a named look, worn with style: hot</span></div>
|
|
2948
|
+
<div class="cs-row"><code>default edge line: (path: square)</code><span>a look every edge wears; also node, leaf, container; the more specific wins</span></div>
|
|
2949
|
+
<div class="cs-row"><code>diagram theme: nord background: #1e2229</code><span>the whole diagram's theme and colors</span></div>
|
|
2950
|
+
<p class="cs-note">The picture is drawn exactly as written: nothing moves to make room, and lines go only where they are told. Check reports anything that overlaps.</p>
|
|
2951
|
+
</section>
|
|
2952
|
+
<section class="cs-card" style="--hue: var(--syn-relation)"><h3>Placement</h3>
|
|
2953
|
+
<div class="cs-row"><code>node b right of a</code><span>beside a, centered on it; also above, below, left of</span></div>
|
|
2954
|
+
<div class="cs-row"><code>node b above-left of a</code><span>diagonally, corner to corner; also above-right, below-left, below-right</span></div>
|
|
2955
|
+
<div class="cs-row"><code>node b 3 left of a and 2 above a</code><span>3 units left of a, and 2 above it</span></div>
|
|
2956
|
+
<div class="cs-row"><code>node c right of a and b</code><span>against the box that just bounds a and b</span></div>
|
|
2957
|
+
<div class="cs-row"><code>node b even with a</code><span>on a's center line</span></div>
|
|
2958
|
+
<div class="cs-row"><code>node b top even with a</code><span>tops in line; also bottom, left, right</span></div>
|
|
2959
|
+
<div class="cs-row"><code>node f inside box top-right</code><span>in a corner of box</span></div>
|
|
2960
|
+
<div class="cs-row"><code>node g outside box right</code><span>just outside a side of box</span></div>
|
|
2961
|
+
<div class="cs-row"><code>node h on box bottom-left</code><span>centered on a part of box</span></div>
|
|
2962
|
+
<div class="cs-row"><code>node i on between a and c</code><span>midway between a and c</span></div>
|
|
2963
|
+
<div class="cs-row"><code>node j below down from b and left from a</code><span>at the point straight down from b and left from a</span></div>
|
|
2964
|
+
<p class="cs-note">The parts of a box: <code>text</code>, the sides <code>top</code> <code>bottom</code> <code>left</code> <code>right</code>, and <code>top-left</code> <code>top-center</code> <code>top-right</code> <code>left-center</code> <code>center</code> <code>right-center</code> <code>bottom-left</code> <code>bottom-center</code> <code>bottom-right</code>. One node is unplaced; every other is placed against something. Each axis is said once.</p>
|
|
2965
|
+
</section>
|
|
2966
|
+
<section class="cs-card" style="--hue: var(--syn-value)"><h3>Gaps</h3>
|
|
2967
|
+
<div class="cs-row"><code>node b 2 right of a</code><span>exactly 2 units apart</span></div>
|
|
2968
|
+
<div class="cs-row"><code>node b right of a (gap: wide)</code><span>none, tight, normal or wide: 0, 2, 4 or 8 units</span></div>
|
|
2969
|
+
<p class="cs-note">A box is about 4 units tall. A gap is exact.</p>
|
|
2970
|
+
</section>
|
|
2971
|
+
<section class="cs-card" style="--hue: var(--syn-string)"><h3>Labels</h3>
|
|
2972
|
+
<div class="cs-row"><code>node parser</code><span>no text, so its name is its text</span></div>
|
|
2973
|
+
<div class="cs-row"><code>node parser ""</code><span>deliberately blank</span></div>
|
|
2974
|
+
<div class="cs-row"><code>node a "two / lines"</code><span>a space, slash and space breaks the line</span></div>
|
|
2975
|
+
</section>
|
|
2976
|
+
<section class="cs-card" style="--hue: var(--syn-name)"><h3>Containment</h3>
|
|
2977
|
+
<div class="cs-row"><code>node server.docker "Docker"</code><span>a dotted name is a child; declare the parent first</span></div>
|
|
2978
|
+
<div class="cs-row"><code>node server layout: column</code><span>children top to bottom, in written order; or row</span></div>
|
|
2979
|
+
<div class="cs-row"><code>layout: column (gap: tight, widths: match, align-children: left)</code><span>how the children line up</span></div>
|
|
2980
|
+
<div class="cs-row"><code>layout: deck</code><span>a stack of cards, the first child in front</span></div>
|
|
2981
|
+
<div class="cs-row"><code>gather-children: left</code><span>where the children sit in spare room</span></div>
|
|
2982
|
+
<div class="cs-row"><code>shape: none</code><span>on a container, draws no box</span></div>
|
|
2983
|
+
<p class="cs-note">Without a layout, one child is unplaced and the rest are placed against it.</p>
|
|
2984
|
+
</section>
|
|
2985
|
+
<section class="cs-card" style="--hue: var(--syn-attribute)"><h3>Sizes</h3>
|
|
2986
|
+
<div class="cs-row"><code>width: 30</code><span>30 units wide</span></div>
|
|
2987
|
+
<div class="cs-row"><code>height: menu</code><span>as tall as menu</span></div>
|
|
2988
|
+
<div class="cs-row"><code>height: 0.5 menu height</code><span>half as tall as menu</span></div>
|
|
2989
|
+
<div class="cs-row"><code>height: a top to c bottom</code><span>from a's top to c's bottom</span></div>
|
|
2990
|
+
<div class="cs-row"><code>min-width: 10</code><span>at least 10 units wide</span></div>
|
|
2991
|
+
</section>
|
|
2992
|
+
<section class="cs-card" style="--hue: var(--syn-keyword)"><h3>Appearance</h3>
|
|
2993
|
+
<div class="cs-row"><code>style: hot</code><span>wear a named style</span></div>
|
|
2994
|
+
<div class="cs-row"><code>fill: #191728</code><span>a box's color</span></div>
|
|
2995
|
+
<div class="cs-row"><code>border: none</code><span>no outline</span></div>
|
|
2996
|
+
<div class="cs-row"><code>corners: sharp</code><span>square corners; boxes are rounded unless told</span></div>
|
|
2997
|
+
<div class="cs-row"><code>line: #d2904e</code><span>a line's color</span></div>
|
|
2998
|
+
<div class="cs-row"><code>overlap: allow</code><span>this one may overlap without a finding</span></div>
|
|
2999
|
+
<div class="cs-row"><code>url: "https://example.com"</code><span>a link, shown on the page</span></div>
|
|
3000
|
+
<div class="cs-row"><code>description: "What it does"</code><span>shown on the page; never drawn</span></div>
|
|
3001
|
+
</section>
|
|
3002
|
+
<section class="cs-card" style="--hue: var(--syn-string)"><h3>Lines</h3>
|
|
3003
|
+
<div class="cs-row"><code>line: (path: square, corners: rounded)</code><span>everything about a line goes in line:</span></div>
|
|
3004
|
+
<div class="cs-row"><code>line: (path: curved, curve: subtle)</code><span>straight (the default), square or curved</span></div>
|
|
3005
|
+
<div class="cs-row"><code>line: (path: square, turn: 1/3, step-out: tight)</code><span>where a square line turns, and how far it steps out</span></div>
|
|
3006
|
+
<div class="cs-row"><code>line: (crossing: arc)</code><span>where lines cross: gap, arc, square or none</span></div>
|
|
3007
|
+
<div class="cs-row"><code>line: (pattern: dashed)</code><span>solid, dashed, dotted or dash-dot</span></div>
|
|
3008
|
+
<div class="cs-row"><code>line: (thickness: thick)</code><span>thin, normal, thick, a multiple, or 3px</span></div>
|
|
3009
|
+
</section>
|
|
3010
|
+
<section class="cs-card" style="--hue: var(--syn-relation)"><h3>Where lines go</h3>
|
|
3011
|
+
<div class="cs-row"><code>from: right to: left</code><span>leave and arrive at a side</span></div>
|
|
3012
|
+
<div class="cs-row"><code>to: bottom-center</code><span>arrive at a point on a side</span></div>
|
|
3013
|
+
<div class="cs-row"><code>to: left 3/4 down</code><span>a fraction of the way along a side</span></div>
|
|
3014
|
+
<div class="cs-row"><code>via: c top, between a and b</code><span>the places a line passes, in order</span></div>
|
|
3015
|
+
<div class="cs-row"><code>via: through r horizontal</code><span>straight through a region</span></div>
|
|
3016
|
+
<div class="cs-row"><code>edge a -> b "calls" (at: start)</code><span>where a line's text sits: start, 1/4, the middle by default</span></div>
|
|
3017
|
+
<div class="cs-row"><code>edge a -> b "calls" (above edge)</code><span>text beside the line rather than on it</span></div>
|
|
3018
|
+
<div class="cs-row"><code>edge a -> b "calls" (on: longest level)</code><span>text on the longest level stretch</span></div>
|
|
3019
|
+
<p class="cs-note">A line runs straight between its two boxes, through anything in between, unless told otherwise.</p>
|
|
3020
|
+
</section>
|
|
3021
|
+
<section class="cs-card" style="--hue: var(--syn-name)"><h3>Body</h3>
|
|
3022
|
+
<div class="cs-row"><code>shape: document</code><span>rectangle, document, circle or none</span></div>
|
|
3023
|
+
<div class="cs-row"><code>node note "a note" right of a shape: none</code><span>just text: an annotation</span></div>
|
|
3024
|
+
<div class="cs-row"><code>icon: database</code><span>the node is the picture, with no box</span></div>
|
|
3025
|
+
<div class="cs-row"><code>badge: database</code><span>the picture beside the text, in the box</span></div>
|
|
3026
|
+
<div class="cs-row"><code>layout: deck cards: "v1" "v2"</code><span>a stack of cards, one per text, front first</span></div>
|
|
3027
|
+
<div class="cs-row"><code>icon logo """<svg …>…</svg>"""</code><span>your own picture, used as icon: logo</span></div>
|
|
3028
|
+
<p class="cs-note">Pictures: <code>disk</code> <code>desktop</code> <code>laptop</code> <code>package</code> <code>cubes</code> <code>cube</code> <code>database</code>.</p>
|
|
3029
|
+
</section>
|
|
3030
|
+
<section class="cs-card" style="--hue: var(--syn-value)"><h3>Text</h3>
|
|
3031
|
+
<div class="cs-row"><code>node a "Server" (size: large)</code><span>small, normal or large</span></div>
|
|
3032
|
+
<div class="cs-row"><code>node a "Server" (color: theme-muted)</code><span>the text's color</span></div>
|
|
3033
|
+
<div class="cs-row"><code>node a "A long label" (wrap: 24)</code><span>folds at 24 characters</span></div>
|
|
3034
|
+
<div class="cs-row"><code>node a "Server" (inside top, align: left)</code><span>where a container's title sits: on top, outside top, center</span></div>
|
|
3035
|
+
<div class="cs-row"><code>style dim text: (color: theme-muted)</code><span>in a style, the bracket hangs off text:</span></div>
|
|
3036
|
+
<div class="cs-row"><code>node a "name / [dim]quieter[/dim]"</code><span>those words in style dim's text color</span></div>
|
|
3037
|
+
<div class="cs-row"><code>node a "a \\/ b"</code><span>a slash that doesn't break the line; \\[ for a bracket</span></div>
|
|
3038
|
+
</section>
|
|
3039
|
+
<section class="cs-card" style="--hue: var(--syn-comment)"><h3>Comments</h3>
|
|
3040
|
+
<div class="cs-row"><code>// to the end of the line</code><span>anywhere, including after a statement</span></div>
|
|
3041
|
+
</section>
|
|
3042
|
+
</div>
|
|
3043
|
+
<p class="cs-none" hidden>Nothing matches. Try one word, such as above, gap or color.</p>
|
|
3044
|
+
</dialog>
|
|
3045
|
+
`;
|
|
3046
|
+
/** Where a page with no file sends someone who wants more than the cheatsheet. */
|
|
3047
|
+
const LINKS = `<nav class="links"><a href="https://github.com/reladraw/reladraw" title="reladraw on GitHub" aria-label="reladraw on GitHub"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg></a></nav>`;
|
|
3048
|
+
/**
|
|
3049
|
+
* A page with no file's dialogs: the examples, the format settings, which a
|
|
3050
|
+
* file's page takes from its nearest .reladraw-format instead, and, served,
|
|
3051
|
+
* the name of the file a first Save writes.
|
|
3052
|
+
*/
|
|
3053
|
+
const DIALOGS = `
|
|
3054
|
+
<dialog id="examples" aria-label="Examples">
|
|
3055
|
+
<div class="bar"><h2>Examples</h2><button type="button" data-close>Close</button></div>
|
|
3056
|
+
<ul id="catalog" role="listbox" tabindex="0" aria-label="Examples"></ul>
|
|
3057
|
+
<p id="about" aria-live="polite"></p>
|
|
3058
|
+
<div id="preview"></div>
|
|
3059
|
+
<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>
|
|
3060
|
+
</dialog>
|
|
3061
|
+
<dialog id="format-settings" aria-label="Format settings">
|
|
3062
|
+
<div class="bar"><h2>Format settings</h2><button type="button" data-close>Close</button></div>
|
|
3063
|
+
<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>
|
|
3064
|
+
<form id="format-fields"></form>
|
|
3065
|
+
<p id="format-problem" role="alert"></p>
|
|
3066
|
+
<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>
|
|
3067
|
+
</dialog>
|
|
3068
|
+
<dialog id="save-as" aria-label="Save as a file">
|
|
3069
|
+
<form method="dialog" id="save-form">
|
|
3070
|
+
<div class="bar"><h2>Save as a file in <span class="folder"></span></h2></div>
|
|
3071
|
+
<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>
|
|
3072
|
+
<label>Path <input id="save-path" type="text" spellcheck="false" autocomplete="off" required></label>
|
|
3073
|
+
<p id="save-problem" role="alert"></p>
|
|
3074
|
+
<footer><span class="gap"></span><button type="button" data-close>Cancel</button><button type="submit" id="save-go">Save</button></footer>
|
|
3075
|
+
</form>
|
|
3076
|
+
</dialog>`;
|