orz-slides 0.1.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/README.md +192 -0
- package/assets/app.js +683 -0
- package/assets/themes/base.css +777 -0
- package/assets/themes/theme-architect.css +64 -0
- package/assets/themes/theme-chalk.css +111 -0
- package/assets/themes/theme-executive.css +65 -0
- package/assets/themes/theme-neon.css +107 -0
- package/assets/themes/theme-paper.css +50 -0
- package/assets/themes/theme-poppy.css +58 -0
- package/assets/themes/theme-sage.css +50 -0
- package/dist/browser-entry.js +615 -0
- package/dist/cli.js +187 -0
- package/dist/layout.js +42 -0
- package/dist/orz-slides.browser.js +388 -0
- package/dist/render-slide.js +149 -0
- package/dist/slide-parser.js +622 -0
- package/dist/template.js +275 -0
- package/dist/types.js +12 -0
- package/orz-slides-skills/SKILL.md +386 -0
- package/package.json +50 -0
package/assets/app.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* orz-slides in-file runtime — present + edit.
|
|
3
|
+
*
|
|
4
|
+
* Present mode is reveal.js (driven by the bundled engine, window.orzslides).
|
|
5
|
+
* Edit mode docks a CodeMirror panel under the deck: it shows the CURRENT
|
|
6
|
+
* slide's source, and the live preview is the real reveal slide, re-rendered as
|
|
7
|
+
* you type. The embedded #orz-deck source is the single source of truth; Save
|
|
8
|
+
* re-serialises the whole document with the updated source (self-reproducing).
|
|
9
|
+
*
|
|
10
|
+
* Ported from orz-mdhtml's app.js (save / IndexedDB handle / version check /
|
|
11
|
+
* served-page notice), adapted to the per-slide deck model.
|
|
12
|
+
*/
|
|
13
|
+
(function () {
|
|
14
|
+
var CFG = window.__ORZ_SLIDES__ || {};
|
|
15
|
+
var root = document.documentElement;
|
|
16
|
+
var API = window.orzslides;
|
|
17
|
+
|
|
18
|
+
var dirty = false;
|
|
19
|
+
var fileHandle = null;
|
|
20
|
+
var editing = false;
|
|
21
|
+
var editingDeck = false; // editing the <!-- deck --> preamble vs a slide
|
|
22
|
+
var cm = null;
|
|
23
|
+
var curIndex = 0;
|
|
24
|
+
var suppressChange = false;
|
|
25
|
+
var rerenderTimer = null;
|
|
26
|
+
var rerenderAllTimer = null;
|
|
27
|
+
var currentTheme = CFG.defaultTheme;
|
|
28
|
+
|
|
29
|
+
// Deck source split into preamble (the <!-- deck --> block) + per-slide chunks.
|
|
30
|
+
var preamble = '';
|
|
31
|
+
var slides = [];
|
|
32
|
+
|
|
33
|
+
// ---- source helpers ------------------------------------------------------
|
|
34
|
+
function escapeSource(s) { return String(s).replace(/<\/(script)/gi, '<\\/$1'); }
|
|
35
|
+
function unescapeSource(s) { return String(s).replace(/<\\\/(script)/gi, '</$1'); }
|
|
36
|
+
|
|
37
|
+
function embeddedSource() {
|
|
38
|
+
var el = document.getElementById('orz-deck');
|
|
39
|
+
var raw = el ? el.textContent || '' : '';
|
|
40
|
+
return unescapeSource(raw).replace(/^\n/, '').replace(/\n\s*$/, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Split on the slide markers, keeping each marker with its slide.
|
|
44
|
+
function splitDeck(src) {
|
|
45
|
+
var i = src.search(/<!--\s*slide\b/);
|
|
46
|
+
var pre = i >= 0 ? src.slice(0, i) : src;
|
|
47
|
+
var rest = i >= 0 ? src.slice(i) : '';
|
|
48
|
+
var parts = rest ? rest.split(/(?=<!--\s*slide\b)/) : [];
|
|
49
|
+
return { preamble: pre, slides: parts };
|
|
50
|
+
}
|
|
51
|
+
function fullSource() { return preamble + slides.join(''); }
|
|
52
|
+
|
|
53
|
+
function loadParts() {
|
|
54
|
+
var p = splitDeck(embeddedSource());
|
|
55
|
+
preamble = p.preamble;
|
|
56
|
+
slides = p.slides.length ? p.slides : ['<!-- slide -->\n## New slide\n'];
|
|
57
|
+
}
|
|
58
|
+
function writeDeck() {
|
|
59
|
+
var el = document.getElementById('orz-deck');
|
|
60
|
+
if (el) el.textContent = '\n' + escapeSource(fullSource()) + '\n';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function themeById(id) {
|
|
64
|
+
var list = CFG.themes || [];
|
|
65
|
+
for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
|
|
66
|
+
return list[0] || { id: id, scheme: 'light', href: '' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- live rendering ------------------------------------------------------
|
|
70
|
+
function sections() { return document.querySelectorAll('.reveal .slides > section.orz-slide'); }
|
|
71
|
+
|
|
72
|
+
function renderCurrentSlide() {
|
|
73
|
+
var src = fullSource();
|
|
74
|
+
var deck = API.parseDeck(src);
|
|
75
|
+
var secs = sections();
|
|
76
|
+
// structural change (a slide added/removed in-place) → full re-render
|
|
77
|
+
if (deck.slides.length !== secs.length) { API.renderAll(src); return; }
|
|
78
|
+
var section = secs[curIndex];
|
|
79
|
+
var slide = deck.slides[curIndex];
|
|
80
|
+
if (!section || !slide) return;
|
|
81
|
+
var tmp = document.createElement('div');
|
|
82
|
+
tmp.innerHTML = API.renderSlide(slide, API.md, deck.config);
|
|
83
|
+
var fresh = tmp.firstElementChild;
|
|
84
|
+
if (!fresh) return;
|
|
85
|
+
// Update the existing <section> in place — replacing the element would
|
|
86
|
+
// orphan reveal.js's reference to the current slide (it would lose its
|
|
87
|
+
// `present` class). Copy the rendered content + render-time attributes; let
|
|
88
|
+
// reveal keep managing the state classes (present/past/future).
|
|
89
|
+
section.innerHTML = fresh.innerHTML;
|
|
90
|
+
['data-fit', 'data-kind', 'data-template', 'data-background-color', 'data-transition', 'data-step'].forEach(function (a) {
|
|
91
|
+
if (fresh.hasAttribute(a)) section.setAttribute(a, fresh.getAttribute(a));
|
|
92
|
+
else section.removeAttribute(a);
|
|
93
|
+
});
|
|
94
|
+
try { API.reveal.sync(); } catch (e) {}
|
|
95
|
+
API.refresh();
|
|
96
|
+
}
|
|
97
|
+
function scheduleRerender() {
|
|
98
|
+
if (rerenderTimer) clearTimeout(rerenderTimer);
|
|
99
|
+
rerenderTimer = setTimeout(renderCurrentSlide, 160);
|
|
100
|
+
}
|
|
101
|
+
// Deck-config edits affect every slide (footer, etc.) → re-render the whole deck.
|
|
102
|
+
function scheduleRerenderAll() {
|
|
103
|
+
if (rerenderAllTimer) clearTimeout(rerenderAllTimer);
|
|
104
|
+
rerenderAllTimer = setTimeout(function () { API.renderAll(fullSource()); }, 240);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function curH() { return (API.reveal && API.reveal.getIndices) ? API.reveal.getIndices().h : 0; }
|
|
108
|
+
function gotoSlide(i) { if (API.reveal) API.reveal.slide(Math.max(0, Math.min(slides.length - 1, i)), 0); }
|
|
109
|
+
|
|
110
|
+
// ---- editor --------------------------------------------------------------
|
|
111
|
+
function loadScript(src) {
|
|
112
|
+
return new Promise(function (res, rej) {
|
|
113
|
+
if (!src || document.querySelector('script[data-lib="' + src + '"]')) return res();
|
|
114
|
+
var s = document.createElement('script');
|
|
115
|
+
s.src = src; s.async = true; s.setAttribute('data-lib', src);
|
|
116
|
+
s.onload = function () { res(); }; s.onerror = function () { rej(); };
|
|
117
|
+
document.head.appendChild(s);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function loadCss(href) {
|
|
121
|
+
if (!href || document.querySelector('link[data-lib="' + href + '"]')) return;
|
|
122
|
+
var l = document.createElement('link');
|
|
123
|
+
l.rel = 'stylesheet'; l.href = href; l.setAttribute('data-lib', href);
|
|
124
|
+
document.head.appendChild(l);
|
|
125
|
+
}
|
|
126
|
+
function ensureEditorLibs() {
|
|
127
|
+
var L = CFG.editorLibs || {};
|
|
128
|
+
loadCss(L.codemirrorCss);
|
|
129
|
+
// Load BOTH editor themes so cmTheme() can switch freely (dark editor on
|
|
130
|
+
// dark slide themes, light on light) without a reload.
|
|
131
|
+
loadCss(L.codemirrorLightThemeCss);
|
|
132
|
+
loadCss(L.codemirrorDarkThemeCss);
|
|
133
|
+
return loadScript(L.codemirrorJs)
|
|
134
|
+
.then(function () { return loadScript(L.codemirrorMarkdownJs); })
|
|
135
|
+
.then(function () { return loadScript(L.codemirrorContinuelistJs); });
|
|
136
|
+
}
|
|
137
|
+
function cmTheme() { return themeById(currentTheme).scheme === 'dark' ? 'material-darker' : 'eclipse'; }
|
|
138
|
+
|
|
139
|
+
function initEditor() {
|
|
140
|
+
return ensureEditorLibs().then(function () {
|
|
141
|
+
if (cm || !window.CodeMirror) return;
|
|
142
|
+
var ta = document.getElementById('orz-ta');
|
|
143
|
+
cm = window.CodeMirror.fromTextArea(ta, {
|
|
144
|
+
mode: 'markdown', theme: cmTheme(), lineNumbers: true, lineWrapping: true,
|
|
145
|
+
viewportMargin: Infinity,
|
|
146
|
+
extraKeys: { Enter: 'newlineAndIndentContinueMarkdownList' },
|
|
147
|
+
});
|
|
148
|
+
cm.on('change', function () {
|
|
149
|
+
if (suppressChange) return;
|
|
150
|
+
markDirty();
|
|
151
|
+
if (editingDeck) {
|
|
152
|
+
preamble = cm.getValue();
|
|
153
|
+
writeDeck();
|
|
154
|
+
scheduleRerenderAll();
|
|
155
|
+
} else {
|
|
156
|
+
slides[curIndex] = cm.getValue();
|
|
157
|
+
writeDeck();
|
|
158
|
+
scheduleRerender();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function loadSlideIntoEditor(i) {
|
|
165
|
+
editingDeck = false;
|
|
166
|
+
curIndex = i;
|
|
167
|
+
if (!cm) return;
|
|
168
|
+
suppressChange = true;
|
|
169
|
+
cm.setValue(slides[i] != null ? slides[i] : '');
|
|
170
|
+
suppressChange = false;
|
|
171
|
+
updatePos();
|
|
172
|
+
setTimeout(function () { cm.refresh(); }, 0);
|
|
173
|
+
}
|
|
174
|
+
function updatePos() {
|
|
175
|
+
var el = document.getElementById('orz-pos');
|
|
176
|
+
if (!el) return;
|
|
177
|
+
el.textContent = editingDeck ? 'Deck config' : ((curIndex + 1) + ' / ' + slides.length);
|
|
178
|
+
var deckBtn = document.getElementById('orz-deck-btn');
|
|
179
|
+
if (deckBtn) deckBtn.classList.toggle('active', editingDeck);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function enterEdit() {
|
|
183
|
+
editing = true;
|
|
184
|
+
editingDeck = false;
|
|
185
|
+
root.setAttribute('data-mode', 'edit');
|
|
186
|
+
checkVersion(); // edit view only — broad viewers never see the update banner
|
|
187
|
+
initEditor().then(function () {
|
|
188
|
+
curIndex = curH();
|
|
189
|
+
loadSlideIntoEditor(curIndex);
|
|
190
|
+
if (API.reveal) API.reveal.layout();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Load the <!-- deck --> preamble into the editor (theme/footer/ratio/title).
|
|
194
|
+
function editDeck() {
|
|
195
|
+
editing = true;
|
|
196
|
+
root.setAttribute('data-mode', 'edit');
|
|
197
|
+
initEditor().then(function () {
|
|
198
|
+
editingDeck = true;
|
|
199
|
+
suppressChange = true;
|
|
200
|
+
if (cm) cm.setValue(preamble);
|
|
201
|
+
suppressChange = false;
|
|
202
|
+
updatePos();
|
|
203
|
+
if (API.reveal) API.reveal.layout();
|
|
204
|
+
if (cm) setTimeout(function () { cm.refresh(); cm.focus(); }, 0);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function done() {
|
|
208
|
+
editing = false;
|
|
209
|
+
root.setAttribute('data-mode', 'present');
|
|
210
|
+
if (API.reveal) { API.reveal.layout(); API.refresh(); }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Drag the panel's top edge to set the editor/deck height split (--orz-vsplit);
|
|
214
|
+
// the deck refits live (rAF-throttled) and on release.
|
|
215
|
+
function wireVDivider() {
|
|
216
|
+
var d = document.getElementById('orz-vdivider'); if (!d || d.__wired) return; d.__wired = true;
|
|
217
|
+
var dragging = false, rafPending = false;
|
|
218
|
+
d.addEventListener('mousedown', function (e) {
|
|
219
|
+
dragging = true; d.classList.add('dragging'); e.preventDefault();
|
|
220
|
+
document.body.style.userSelect = 'none';
|
|
221
|
+
});
|
|
222
|
+
document.addEventListener('mousemove', function (e) {
|
|
223
|
+
if (!dragging) return;
|
|
224
|
+
var pct = Math.max(20, Math.min(80, ((window.innerHeight - e.clientY) / window.innerHeight) * 100));
|
|
225
|
+
root.style.setProperty('--orz-vsplit', pct + '%');
|
|
226
|
+
if (!rafPending) { rafPending = true; requestAnimationFrame(function () { rafPending = false; if (API.reveal) API.reveal.layout(); }); }
|
|
227
|
+
});
|
|
228
|
+
document.addEventListener('mouseup', function () {
|
|
229
|
+
if (!dragging) return;
|
|
230
|
+
dragging = false; d.classList.remove('dragging'); document.body.style.userSelect = '';
|
|
231
|
+
if (cm) cm.refresh(); if (API.reveal) API.reveal.layout();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---- deck ops ------------------------------------------------------------
|
|
236
|
+
function rebuildFrom(newSlides, focus) {
|
|
237
|
+
slides = newSlides;
|
|
238
|
+
writeDeck();
|
|
239
|
+
API.renderAll(fullSource());
|
|
240
|
+
markDirty();
|
|
241
|
+
setTimeout(function () {
|
|
242
|
+
gotoSlide(focus);
|
|
243
|
+
curIndex = focus;
|
|
244
|
+
if (editing) loadSlideIntoEditor(focus);
|
|
245
|
+
updatePos();
|
|
246
|
+
}, 30);
|
|
247
|
+
}
|
|
248
|
+
function addSlide() {
|
|
249
|
+
var s = slides.slice();
|
|
250
|
+
s.splice(curIndex + 1, 0, '<!-- slide -->\n## New slide\n\n');
|
|
251
|
+
rebuildFrom(s, curIndex + 1);
|
|
252
|
+
}
|
|
253
|
+
function dupSlide() {
|
|
254
|
+
var s = slides.slice();
|
|
255
|
+
s.splice(curIndex + 1, 0, slides[curIndex]);
|
|
256
|
+
rebuildFrom(s, curIndex + 1);
|
|
257
|
+
}
|
|
258
|
+
function delSlide() {
|
|
259
|
+
if (slides.length <= 1) { toast('Cannot delete the only slide'); return; }
|
|
260
|
+
var s = slides.slice();
|
|
261
|
+
s.splice(curIndex, 1);
|
|
262
|
+
rebuildFrom(s, Math.max(0, curIndex - 1));
|
|
263
|
+
}
|
|
264
|
+
function moveSlide(dir) {
|
|
265
|
+
var j = curIndex + dir;
|
|
266
|
+
if (j < 0 || j >= slides.length) return;
|
|
267
|
+
var s = slides.slice();
|
|
268
|
+
var tmp = s[curIndex]; s[curIndex] = s[j]; s[j] = tmp;
|
|
269
|
+
rebuildFrom(s, j);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---- theme ---------------------------------------------------------------
|
|
273
|
+
// Inline mode embeds every theme as a <style data-theme-css>; activate one by
|
|
274
|
+
// matching media. Returns false if no inline themes (CDN mode).
|
|
275
|
+
function applyInlineTheme(id) {
|
|
276
|
+
var blocks = document.querySelectorAll('style[data-theme-css]');
|
|
277
|
+
if (!blocks.length) return false;
|
|
278
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
279
|
+
blocks[i].media = blocks[i].getAttribute('data-theme-css') === id ? 'all' : 'not all';
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
function setTheme(id) {
|
|
284
|
+
currentTheme = id;
|
|
285
|
+
root.setAttribute('data-theme', id);
|
|
286
|
+
if (!applyInlineTheme(id)) {
|
|
287
|
+
// CDN mode: swap the theme link (loads from jsDelivr).
|
|
288
|
+
var link = document.getElementById('orz-theme-override');
|
|
289
|
+
if (!link) {
|
|
290
|
+
link = document.createElement('link');
|
|
291
|
+
link.id = 'orz-theme-override'; link.rel = 'stylesheet';
|
|
292
|
+
document.head.appendChild(link);
|
|
293
|
+
}
|
|
294
|
+
link.href = themeById(id).href;
|
|
295
|
+
}
|
|
296
|
+
if (cm) cm.setOption('theme', cmTheme());
|
|
297
|
+
markDirty();
|
|
298
|
+
if (API.reveal) setTimeout(function () { API.reveal.layout(); API.refresh(); }, 60);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---- dirty / save (self-reproducing) -------------------------------------
|
|
302
|
+
function markDirty() { if (!dirty) { dirty = true; root.setAttribute('data-dirty', '1'); } }
|
|
303
|
+
function clearDirty() { dirty = false; root.setAttribute('data-dirty', '0'); }
|
|
304
|
+
|
|
305
|
+
function serializeDoc() {
|
|
306
|
+
var clone = root.cloneNode(true);
|
|
307
|
+
var deckEl = clone.querySelector('#orz-deck');
|
|
308
|
+
if (deckEl) deckEl.textContent = '\n' + escapeSource(fullSource()) + '\n';
|
|
309
|
+
clone.setAttribute('data-mode', 'present');
|
|
310
|
+
clone.setAttribute('data-theme', currentTheme);
|
|
311
|
+
clone.removeAttribute('data-dirty');
|
|
312
|
+
// never bake in the (edit-only) update banner so a viewer can't see it
|
|
313
|
+
var ub = clone.querySelector('#orz-update'); if (ub) { ub.classList.remove('show'); ub.removeAttribute('data-latest'); }
|
|
314
|
+
// Reset reveal's rendered DOM so the reopened file re-renders from #orz-deck.
|
|
315
|
+
var reveal = clone.querySelector('.reveal');
|
|
316
|
+
if (reveal) { reveal.className = 'reveal'; reveal.innerHTML = '<div class="slides"></div>'; }
|
|
317
|
+
// Reset the live editor back to a clean textarea.
|
|
318
|
+
var ed = clone.querySelector('#orz-editor-host');
|
|
319
|
+
if (ed) ed.innerHTML = '<textarea id="orz-ta" spellcheck="false"></textarea>';
|
|
320
|
+
return '<!DOCTYPE html>\n' + clone.outerHTML + '\n';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function idbOpen() {
|
|
324
|
+
return new Promise(function (res, rej) {
|
|
325
|
+
var r = indexedDB.open('orz-slides', 1);
|
|
326
|
+
r.onupgradeneeded = function () { r.result.createObjectStore('handles'); };
|
|
327
|
+
r.onsuccess = function () { res(r.result); };
|
|
328
|
+
r.onerror = function () { rej(r.error); };
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function idbGet(key) {
|
|
332
|
+
return idbOpen().then(function (db) {
|
|
333
|
+
return new Promise(function (res, rej) {
|
|
334
|
+
var t = db.transaction('handles', 'readonly');
|
|
335
|
+
var g = t.objectStore('handles').get(key);
|
|
336
|
+
g.onsuccess = function () { res(g.result || null); };
|
|
337
|
+
g.onerror = function () { rej(g.error); };
|
|
338
|
+
});
|
|
339
|
+
}).catch(function () { return null; });
|
|
340
|
+
}
|
|
341
|
+
function idbPut(key, val) {
|
|
342
|
+
return idbOpen().then(function (db) {
|
|
343
|
+
return new Promise(function (res, rej) {
|
|
344
|
+
var t = db.transaction('handles', 'readwrite');
|
|
345
|
+
var p = t.objectStore('handles').put(val, key);
|
|
346
|
+
p.onsuccess = function () { res(); };
|
|
347
|
+
p.onerror = function () { rej(p.error); };
|
|
348
|
+
});
|
|
349
|
+
}).catch(function () {});
|
|
350
|
+
}
|
|
351
|
+
function pickAndStore() {
|
|
352
|
+
return window.showSaveFilePicker({
|
|
353
|
+
suggestedName: (CFG.filename || 'deck') + '.slides.html',
|
|
354
|
+
types: [{ description: 'Slides HTML', accept: { 'text/html': ['.slides.html', '.html'] } }],
|
|
355
|
+
}).then(function (h) { fileHandle = h; if (CFG.docId) idbPut(CFG.docId, h); return h; });
|
|
356
|
+
}
|
|
357
|
+
function acquireHandle() {
|
|
358
|
+
if (fileHandle) return Promise.resolve(fileHandle);
|
|
359
|
+
if (!CFG.docId) return pickAndStore();
|
|
360
|
+
return idbGet(CFG.docId).then(function (saved) {
|
|
361
|
+
if (!saved || !saved.queryPermission) return pickAndStore();
|
|
362
|
+
return saved.queryPermission({ mode: 'readwrite' }).then(function (p) {
|
|
363
|
+
if (p === 'granted') return saved;
|
|
364
|
+
return saved.requestPermission({ mode: 'readwrite' }).then(function (p2) {
|
|
365
|
+
return p2 === 'granted' ? saved : null;
|
|
366
|
+
});
|
|
367
|
+
}).then(function (h) { if (h) { fileHandle = h; return h; } return pickAndStore(); });
|
|
368
|
+
}).catch(function () { return pickAndStore(); });
|
|
369
|
+
}
|
|
370
|
+
function isServed() { return location.protocol === 'http:' || location.protocol === 'https:'; }
|
|
371
|
+
|
|
372
|
+
function save() {
|
|
373
|
+
if (cm) { slides[curIndex] = cm.getValue(); writeDeck(); }
|
|
374
|
+
var html = serializeDoc();
|
|
375
|
+
if (isServed() && !fileHandle) { if (dirty) showServedNote(); return; }
|
|
376
|
+
if (window.showSaveFilePicker) {
|
|
377
|
+
acquireHandle()
|
|
378
|
+
.then(function (h) { return h.createWritable(); })
|
|
379
|
+
.then(function (w) { return Promise.resolve(w.write(html)).then(function () { return w.close(); }); })
|
|
380
|
+
.then(function () { clearDirty(); toast('Saved'); })
|
|
381
|
+
.catch(function (err) { if (err && err.name === 'AbortError') return; downloadFile(html); clearDirty(); toast('Saved a local copy'); });
|
|
382
|
+
} else {
|
|
383
|
+
downloadFile(html); clearDirty(); toast('Saved a local copy');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function downloadFile(text) {
|
|
387
|
+
var blob = new Blob([text], { type: 'text/html' });
|
|
388
|
+
var url = URL.createObjectURL(blob);
|
|
389
|
+
var a = document.createElement('a');
|
|
390
|
+
a.href = url; a.download = (CFG.filename || 'deck') + '.slides.html';
|
|
391
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
392
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
|
|
393
|
+
}
|
|
394
|
+
function exportCopy() {
|
|
395
|
+
if (cm) { slides[curIndex] = cm.getValue(); writeDeck(); }
|
|
396
|
+
downloadFile(serializeDoc()); toast('Downloaded a local copy');
|
|
397
|
+
}
|
|
398
|
+
function showServedNote() { var n = document.getElementById('orz-served-note'); if (n) n.classList.add('show'); }
|
|
399
|
+
|
|
400
|
+
// ---- version check -------------------------------------------------------
|
|
401
|
+
function isNewer(a, b) {
|
|
402
|
+
var pa = String(a).split('.'), pb = String(b).split('.');
|
|
403
|
+
for (var i = 0; i < 3; i++) {
|
|
404
|
+
var x = parseInt(pa[i], 10) || 0, y = parseInt(pb[i], 10) || 0;
|
|
405
|
+
if (x > y) return true; if (x < y) return false;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
// SECURITY: the update source is HARDCODED here, never read from the file's
|
|
410
|
+
// config — a tampered/forged file cannot redirect "Update" to attacker code.
|
|
411
|
+
// Host is fixed to jsDelivr/HTTPS; the exact URLs are confirmed with the user.
|
|
412
|
+
// (Protects genuine files; a wholly-malicious file controls this code too — see
|
|
413
|
+
// the README security note. Clicking Update trusts npm + jsDelivr.)
|
|
414
|
+
var UPD = {
|
|
415
|
+
host: 'https://cdn.jsdelivr.net/npm/',
|
|
416
|
+
manifest: 'https://data.jsdelivr.com/v1/packages/npm/orz-slides-browser/resolved',
|
|
417
|
+
enginePkg: 'orz-slides-browser', engineFile: 'orz-slides.browser.js', appPkg: 'orz-slides'
|
|
418
|
+
};
|
|
419
|
+
function checkVersion() {
|
|
420
|
+
if (!CFG.rendererVersion) return;
|
|
421
|
+
try {
|
|
422
|
+
var cached = JSON.parse(localStorage.getItem('orz-slides:vercheck') || 'null');
|
|
423
|
+
if (cached && (Date.now() - cached.t) < 86400000) {
|
|
424
|
+
if (cached.v && isNewer(cached.v, CFG.rendererVersion)) showUpdate(cached.v);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
} catch (e) {}
|
|
428
|
+
fetch(UPD.manifest).then(function (r) { return r.json(); }).then(function (j) {
|
|
429
|
+
var latest = j && j.version;
|
|
430
|
+
try { localStorage.setItem('orz-slides:vercheck', JSON.stringify({ t: Date.now(), v: latest })); } catch (e) {}
|
|
431
|
+
if (latest && isNewer(latest, CFG.rendererVersion)) showUpdate(latest);
|
|
432
|
+
}).catch(function () {});
|
|
433
|
+
}
|
|
434
|
+
function showUpdate(latest) {
|
|
435
|
+
var bar = document.getElementById('orz-update'); if (!bar) return;
|
|
436
|
+
bar.querySelector('.upd-text').textContent = 'Framework ' + latest + ' available (file uses ' + CFG.rendererVersion + ').';
|
|
437
|
+
bar.setAttribute('data-latest', latest);
|
|
438
|
+
bar.classList.add('show');
|
|
439
|
+
}
|
|
440
|
+
/** One-click update: re-fetch the engine bundle + app.js at the latest version,
|
|
441
|
+
* re-inline them, bump the version, save in place, and reload. */
|
|
442
|
+
function applyUpdate() {
|
|
443
|
+
var bar = document.getElementById('orz-update'); var latest = bar && bar.getAttribute('data-latest'); if (!latest) return;
|
|
444
|
+
var engineUrl = UPD.host + UPD.enginePkg + '@' + latest + '/' + UPD.engineFile;
|
|
445
|
+
var appUrl = UPD.host + UPD.appPkg + '@' + latest + '/assets/app.js';
|
|
446
|
+
if (!window.confirm('Update the framework to ' + latest + '?\n\nThis downloads and runs code from:\n ' + engineUrl + '\n ' + appUrl + '\n\nOnly proceed if you trust this document and its publisher.')) return;
|
|
447
|
+
toast('Downloading framework ' + latest + '…');
|
|
448
|
+
Promise.all([
|
|
449
|
+
fetch(engineUrl).then(function (r) { if (!r.ok) throw new Error('engine'); return r.text(); }),
|
|
450
|
+
fetch(appUrl).then(function (r) { if (!r.ok) throw new Error('app'); return r.text(); }),
|
|
451
|
+
]).then(function (res) {
|
|
452
|
+
var es = document.querySelector('script[data-orz-asset="engine"]');
|
|
453
|
+
if (es) { if (es.getAttribute('src')) es.setAttribute('src', engineUrl); else es.textContent = res[0]; }
|
|
454
|
+
var as = document.querySelector('script[data-orz-asset="app"]');
|
|
455
|
+
if (as) as.textContent = res[1];
|
|
456
|
+
var cs = document.querySelector('script[data-orz-asset="config"]');
|
|
457
|
+
if (cs) { CFG.version = latest; CFG.rendererVersion = latest; cs.textContent = 'window.__ORZ_SLIDES__ = ' + JSON.stringify(CFG) + ';'; }
|
|
458
|
+
bar.classList.remove('show');
|
|
459
|
+
var html = serializeDoc();
|
|
460
|
+
if (isServed() && !fileHandle) { showServedNote(); return; }
|
|
461
|
+
if (window.showSaveFilePicker) {
|
|
462
|
+
return acquireHandle()
|
|
463
|
+
.then(function (h) { return h.createWritable(); })
|
|
464
|
+
.then(function (w) { return Promise.resolve(w.write(html)).then(function () { return w.close(); }); })
|
|
465
|
+
.then(function () { toast('Updated to ' + latest + ' — reloading…'); setTimeout(function () { location.reload(); }, 700); });
|
|
466
|
+
}
|
|
467
|
+
downloadFile(html); toast('Updated copy downloaded — reopen it to use the new framework.');
|
|
468
|
+
}).catch(function () { toast('Update failed — check your connection.'); });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- toast ---------------------------------------------------------------
|
|
472
|
+
var toastTimer = null;
|
|
473
|
+
function toast(msg) {
|
|
474
|
+
var t = document.getElementById('orz-toast'); if (!t) return;
|
|
475
|
+
t.textContent = msg; t.classList.add('show');
|
|
476
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
477
|
+
toastTimer = setTimeout(function () { t.classList.remove('show'); }, 1800);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Position the edit control just above reveal's left/right arrows, centered on
|
|
481
|
+
// the cluster — it reads as part of the controls. Kept in sync on nav/resize.
|
|
482
|
+
// (reveal's .controls box is a 0-size anchor, so we measure the arrows.)
|
|
483
|
+
function positionEditCtrl() {
|
|
484
|
+
var btn = document.getElementById('orz-edit-fab');
|
|
485
|
+
if (!btn) return;
|
|
486
|
+
var left = document.querySelector('.reveal .controls .navigate-left');
|
|
487
|
+
var right = document.querySelector('.reveal .controls .navigate-right');
|
|
488
|
+
if (!left || !right) return;
|
|
489
|
+
var lr = left.getBoundingClientRect(), rr = right.getBoundingClientRect();
|
|
490
|
+
if (!lr.width && !rr.width) return; // controls hidden → keep the corner default
|
|
491
|
+
// Bottom-left corner, vertically centered on the arrow row.
|
|
492
|
+
var rowCenterY = (lr.top + lr.height / 2 + rr.top + rr.height / 2) / 2;
|
|
493
|
+
btn.style.left = '24px';
|
|
494
|
+
btn.style.top = (rowCenterY - btn.offsetHeight / 2) + 'px';
|
|
495
|
+
btn.style.right = 'auto';
|
|
496
|
+
btn.style.bottom = 'auto';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---- layout picker -------------------------------------------------------
|
|
500
|
+
// Tiny SVG thumbnails (currentColor) + the markdown skeleton each layout inserts.
|
|
501
|
+
function lsvg(inner) {
|
|
502
|
+
return '<svg viewBox="0 0 64 40" fill="none" aria-hidden="true">' +
|
|
503
|
+
'<rect x="1.5" y="1.5" width="61" height="37" rx="3" stroke="currentColor" stroke-opacity="0.4"/>' + inner + '</svg>';
|
|
504
|
+
}
|
|
505
|
+
function lbox(x, y, w, h) {
|
|
506
|
+
return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="1.5" fill="currentColor" fill-opacity="0.16" stroke="currentColor" stroke-opacity="0.3"/>';
|
|
507
|
+
}
|
|
508
|
+
function lbar(x, y, w, h, op) {
|
|
509
|
+
return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="1" fill="currentColor" fill-opacity="' + (op == null ? 0.5 : op) + '"/>';
|
|
510
|
+
}
|
|
511
|
+
function lrow(y, w) { return '<circle cx="9" cy="' + (y + 1.5) + '" r="1.4" fill="currentColor" fill-opacity="0.5"/>' + lbar(13, y, w, 3, 0.3); }
|
|
512
|
+
|
|
513
|
+
var LAYOUTS = [
|
|
514
|
+
{ key: 'title', name: 'Title', thumb: lsvg(lbar(16, 15, 32, 6) + lbar(22, 25, 20, 3, 0.28)),
|
|
515
|
+
md: '<!-- slide template=title -->\n# Title\n## Subtitle\nAuthor · Date\n' },
|
|
516
|
+
{ key: 'section', name: 'Section', thumb: lsvg(lbar(8, 16, 32, 7) + lbar(8, 27, 16, 3, 0.28)),
|
|
517
|
+
md: '<!-- slide template=section -->\n# Section title\nOptional subtitle\n' },
|
|
518
|
+
{ key: 'bullets', name: 'Bullets', thumb: lsvg(lbar(7, 7, 24, 4) + lrow(16, 44) + lrow(23, 44) + lrow(30, 34)),
|
|
519
|
+
md: '<!-- slide -->\n## Slide title\n\n- Point one\n- Point two\n- Point three\n' },
|
|
520
|
+
{ key: '2col', name: 'Two columns', thumb: lsvg(lbar(6, 6, 30, 4) + lbox(6, 13, 25, 21) + lbox(33, 13, 25, 21)),
|
|
521
|
+
md: '<!-- slide 2col -->\n## Slide title\n\n<!-- @left -->\n\n\n<!-- @right -->\n\n' },
|
|
522
|
+
{ key: 'main-side', name: 'Main + side', thumb: lsvg(lbar(6, 6, 30, 4) + lbox(6, 13, 34, 21) + lbox(42, 13, 16, 21)),
|
|
523
|
+
md: '<!-- slide main-side -->\n## Slide title\n\n<!-- @main -->\n\n\n<!-- @side -->\n\n' },
|
|
524
|
+
{ key: '3col', name: 'Three columns', thumb: lsvg(lbar(6, 6, 30, 4) + lbox(6, 13, 16, 21) + lbox(24, 13, 16, 21) + lbox(42, 13, 16, 21)),
|
|
525
|
+
md: '<!-- slide 3col -->\n## Slide title\n\n<!-- @left -->\n\n\n<!-- @mid -->\n\n\n<!-- @right -->\n\n' },
|
|
526
|
+
{ key: '2row', name: 'Two rows', thumb: lsvg(lbar(6, 6, 30, 4) + lbox(6, 13, 52, 10) + lbox(6, 25, 52, 10)),
|
|
527
|
+
md: '<!-- slide 2row -->\n## Slide title\n\n<!-- @top -->\n\n\n<!-- @bottom -->\n\n' },
|
|
528
|
+
{ key: 'quad', name: 'Quad (2×2)', thumb: lsvg(lbar(6, 6, 30, 4) + lbox(6, 13, 25, 10) + lbox(33, 13, 25, 10) + lbox(6, 25, 25, 10) + lbox(33, 25, 25, 10)),
|
|
529
|
+
md: '<!-- slide quad -->\n## Slide title\n\n<!-- @tl -->\n\n\n<!-- @tr -->\n\n\n<!-- @bl -->\n\n\n<!-- @br -->\n\n' },
|
|
530
|
+
{ key: 'outline', name: 'Outline', thumb: lsvg(lbar(8, 7, 22, 4) + lbar(10, 16, 40, 3, 0.32) + lbar(10, 23, 40, 3, 0.32) + lbar(10, 30, 30, 3, 0.32)),
|
|
531
|
+
md: '<!-- slide template=outline -->\n## Outline\n\n1. First topic\n2. Second topic\n3. Third topic\n' },
|
|
532
|
+
{ key: 'closing', name: 'Closing', thumb: lsvg(lbar(18, 16, 28, 6) + lbar(24, 26, 16, 3, 0.28)),
|
|
533
|
+
md: '<!-- slide template=closing -->\n# Thank you\nQuestions? · your.contact\n' },
|
|
534
|
+
];
|
|
535
|
+
function layoutByKey(k) { for (var i = 0; i < LAYOUTS.length; i++) if (LAYOUTS[i].key === k) return LAYOUTS[i]; return null; }
|
|
536
|
+
|
|
537
|
+
function buildLayoutMenu() {
|
|
538
|
+
var menu = document.getElementById('orz-layout-menu'); if (!menu) return;
|
|
539
|
+
menu.innerHTML = LAYOUTS.map(function (l) {
|
|
540
|
+
return '<button class="orz-layout-tile" type="button" role="menuitem" data-layout="' + l.key + '" title="' + l.name + '">' + l.thumb + '<span class="lname">' + l.name + '</span></button>';
|
|
541
|
+
}).join('') +
|
|
542
|
+
'<a class="orz-layout-more" href="https://markdown.orz.how/slides.html" target="_blank" rel="noopener noreferrer">More layouts & syntax →</a>';
|
|
543
|
+
menu.addEventListener('click', function (e) {
|
|
544
|
+
if (e.target.closest && e.target.closest('.orz-layout-more')) { closeLayoutMenu(); return; }
|
|
545
|
+
var t = e.target.closest ? e.target.closest('.orz-layout-tile') : null;
|
|
546
|
+
if (!t) return;
|
|
547
|
+
applyLayout(t.getAttribute('data-layout'));
|
|
548
|
+
closeLayoutMenu();
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function openLayoutMenu() {
|
|
552
|
+
var menu = document.getElementById('orz-layout-menu'), btn = document.getElementById('orz-layout-btn');
|
|
553
|
+
if (!menu || !btn) return;
|
|
554
|
+
menu.hidden = false;
|
|
555
|
+
var r = btn.getBoundingClientRect(), w = menu.offsetWidth || 320, h = menu.offsetHeight;
|
|
556
|
+
menu.style.left = Math.max(8, Math.min(r.left, window.innerWidth - w - 8)) + 'px';
|
|
557
|
+
menu.style.top = (r.bottom + h + 8 <= window.innerHeight ? r.bottom + 4 : Math.max(8, r.top - h - 4)) + 'px';
|
|
558
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
559
|
+
}
|
|
560
|
+
function closeLayoutMenu() {
|
|
561
|
+
var menu = document.getElementById('orz-layout-menu'), btn = document.getElementById('orz-layout-btn');
|
|
562
|
+
if (menu) menu.hidden = true;
|
|
563
|
+
if (btn) btn.setAttribute('aria-expanded', 'false');
|
|
564
|
+
}
|
|
565
|
+
function toggleLayoutMenu() {
|
|
566
|
+
var menu = document.getElementById('orz-layout-menu');
|
|
567
|
+
if (menu && menu.hidden) openLayoutMenu(); else closeLayoutMenu();
|
|
568
|
+
}
|
|
569
|
+
// Apply a layout to the current slide. New/empty slide → drop the skeleton in;
|
|
570
|
+
// a slide with content → keep the skeleton and tuck the old content into an
|
|
571
|
+
// HTML comment (with any nested "-->" neutralised) so it can be copy-pasted.
|
|
572
|
+
function applyLayout(key) {
|
|
573
|
+
var l = layoutByKey(key); if (!l || !cm) return;
|
|
574
|
+
if (editingDeck) { toast('Switch to a slide to apply a layout'); return; }
|
|
575
|
+
var src = cm.getValue();
|
|
576
|
+
var m = src.match(/^[ \t]*<!--\s*slide\b[^]*?-->[ \t]*\r?\n?/);
|
|
577
|
+
var body = m ? src.slice(m[0].length) : src;
|
|
578
|
+
var trimmed = body.replace(/^\s+|\s+$/g, '');
|
|
579
|
+
var isEmpty = trimmed === '' || /^##\s+new slide$/i.test(trimmed);
|
|
580
|
+
var next;
|
|
581
|
+
if (isEmpty) {
|
|
582
|
+
next = l.md;
|
|
583
|
+
} else {
|
|
584
|
+
var safe = body.replace(/-->/g, '-->').replace(/\s+$/, '');
|
|
585
|
+
next = l.md.replace(/\s+$/, '') +
|
|
586
|
+
'\n\n<!--\nPrevious content — move it into the regions above, then delete this block.\n\n' +
|
|
587
|
+
safe + '\n-->\n';
|
|
588
|
+
}
|
|
589
|
+
cm.setValue(next);
|
|
590
|
+
cm.focus();
|
|
591
|
+
markDirty();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ---- wiring --------------------------------------------------------------
|
|
595
|
+
function on(id, ev, fn) { var el = document.getElementById(id); if (el) el.addEventListener(ev, fn); }
|
|
596
|
+
function navTo(i) { editingDeck = false; gotoSlide(i); }
|
|
597
|
+
function wireUi() {
|
|
598
|
+
on('orz-edit-fab', 'click', enterEdit);
|
|
599
|
+
on('orz-close', 'click', done);
|
|
600
|
+
wireVDivider();
|
|
601
|
+
on('orz-deck-btn', 'click', function () { if (editingDeck) loadSlideIntoEditor(curH()); else editDeck(); });
|
|
602
|
+
on('orz-save', 'click', save);
|
|
603
|
+
on('orz-download', 'click', exportCopy);
|
|
604
|
+
on('orz-add', 'click', addSlide);
|
|
605
|
+
on('orz-dup', 'click', dupSlide);
|
|
606
|
+
on('orz-del', 'click', delSlide);
|
|
607
|
+
on('orz-up', 'click', function () { moveSlide(-1); });
|
|
608
|
+
on('orz-down', 'click', function () { moveSlide(1); });
|
|
609
|
+
on('orz-prev', 'click', function () { navTo(curIndex - 1); });
|
|
610
|
+
on('orz-next', 'click', function () { navTo(curIndex + 1); });
|
|
611
|
+
on('orz-served-download', 'click', function () { exportCopy(); var n = document.getElementById('orz-served-note'); if (n) n.classList.remove('show'); });
|
|
612
|
+
on('orz-served-dismiss', 'click', function () { var n = document.getElementById('orz-served-note'); if (n) n.classList.remove('show'); });
|
|
613
|
+
on('orz-upd-dismiss', 'click', function () { var u = document.getElementById('orz-update'); if (u) u.classList.remove('show'); });
|
|
614
|
+
on('orz-upd-apply', 'click', applyUpdate);
|
|
615
|
+
var sel = document.getElementById('orz-theme');
|
|
616
|
+
if (sel) sel.addEventListener('change', function () { setTheme(this.value); });
|
|
617
|
+
|
|
618
|
+
buildLayoutMenu();
|
|
619
|
+
on('orz-layout-btn', 'click', function (e) { e.stopPropagation(); toggleLayoutMenu(); });
|
|
620
|
+
document.addEventListener('click', function (e) {
|
|
621
|
+
var menu = document.getElementById('orz-layout-menu');
|
|
622
|
+
if (!menu || menu.hidden || menu.contains(e.target)) return;
|
|
623
|
+
var btn = document.getElementById('orz-layout-btn');
|
|
624
|
+
if (btn && (e.target === btn || btn.contains(e.target))) return;
|
|
625
|
+
closeLayoutMenu();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
document.addEventListener('keydown', function (e) {
|
|
629
|
+
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === 's') { e.preventDefault(); save(); }
|
|
630
|
+
else if (e.key === 'Escape') {
|
|
631
|
+
var menu = document.getElementById('orz-layout-menu');
|
|
632
|
+
if (menu && !menu.hidden) closeLayoutMenu();
|
|
633
|
+
else if (editing) done();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
window.addEventListener('beforeunload', function (e) { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ---- boot ----------------------------------------------------------------
|
|
640
|
+
function boot() {
|
|
641
|
+
if (!API) return; // engine bundle missing
|
|
642
|
+
currentTheme = root.getAttribute('data-theme') || CFG.defaultTheme;
|
|
643
|
+
var sel = document.getElementById('orz-theme');
|
|
644
|
+
if (sel) {
|
|
645
|
+
var opts = (CFG.themes || []).map(function (t) { return '<option value="' + t.id + '">' + t.name + '</option>'; }).join('');
|
|
646
|
+
sel.innerHTML = opts; sel.value = currentTheme;
|
|
647
|
+
}
|
|
648
|
+
// The deck's <!-- deck theme: --> may differ from the file's data-theme; if a
|
|
649
|
+
// saved override link exists it already wins. Keep currentTheme in sync.
|
|
650
|
+
if (root.getAttribute('data-theme') && root.getAttribute('data-theme') !== currentTheme) currentTheme = root.getAttribute('data-theme');
|
|
651
|
+
applyInlineTheme(currentTheme); // assert the active inline theme on load
|
|
652
|
+
|
|
653
|
+
function start() {
|
|
654
|
+
loadParts();
|
|
655
|
+
writeDeck();
|
|
656
|
+
updatePos();
|
|
657
|
+
positionEditCtrl();
|
|
658
|
+
if (API.reveal && API.reveal.on) {
|
|
659
|
+
API.reveal.on('slidechanged', function () {
|
|
660
|
+
curIndex = curH();
|
|
661
|
+
if (editing && !editingDeck) loadSlideIntoEditor(curIndex);
|
|
662
|
+
updatePos();
|
|
663
|
+
positionEditCtrl();
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
window.addEventListener('resize', function () { setTimeout(positionEditCtrl, 80); });
|
|
667
|
+
[300, 900].forEach(function (t) { setTimeout(positionEditCtrl, t); });
|
|
668
|
+
}
|
|
669
|
+
// The engine mounts on DOMContentLoaded too; wait until reveal exists.
|
|
670
|
+
if (API.reveal) start();
|
|
671
|
+
else {
|
|
672
|
+
var tries = 0;
|
|
673
|
+
var iv = setInterval(function () {
|
|
674
|
+
if (API.reveal || tries++ > 100) { clearInterval(iv); start(); }
|
|
675
|
+
}, 30);
|
|
676
|
+
}
|
|
677
|
+
wireUi();
|
|
678
|
+
// version check runs on entering edit (edit view only), not on load
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
|
|
682
|
+
else boot();
|
|
683
|
+
})();
|