@echodial/deck 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/LICENSE +21 -0
- package/README.md +1327 -0
- package/bin/deck.mjs +219 -0
- package/build.mjs +338 -0
- package/dist/api-buckets.json +10486 -0
- package/dist/api.json +57896 -0
- package/dist/brand/deck-apple-touch-icon.png +0 -0
- package/dist/brand/deck-logo-dark.svg +1 -0
- package/dist/brand/deck-logo-light.svg +1 -0
- package/dist/brand/deck-logo-stacked.svg +1 -0
- package/dist/brand/deck-logo.svg +1 -0
- package/dist/brand/deck-mark.svg +7 -0
- package/dist/brand/deck-og.png +0 -0
- package/dist/collisions.json +223 -0
- package/dist/deck-adapters.js +482 -0
- package/dist/deck-adapters.min.js +23 -0
- package/dist/deck-extras.js +823 -0
- package/dist/deck-extras.min.js +5 -0
- package/dist/deck-icons.svg +184 -0
- package/dist/deck.bundle.js +2638 -0
- package/dist/deck.bundle.min.js +86 -0
- package/dist/deck.css +7904 -0
- package/dist/deck.esm.js +2644 -0
- package/dist/deck.js +1331 -0
- package/dist/deck.min.css +32 -0
- package/dist/deck.min.js +60 -0
- package/dist/layers/anchor.css +192 -0
- package/dist/layers/buttons.css +210 -0
- package/dist/layers/charts.css +337 -0
- package/dist/layers/combobox.css +248 -0
- package/dist/layers/components.css +868 -0
- package/dist/layers/container.css +199 -0
- package/dist/layers/datagrid.css +335 -0
- package/dist/layers/datepicker.css +305 -0
- package/dist/layers/forms.css +401 -0
- package/dist/layers/gradients.css +302 -0
- package/dist/layers/inputs.css +522 -0
- package/dist/layers/layers.css +87 -0
- package/dist/layers/layout.css +235 -0
- package/dist/layers/logical.css +226 -0
- package/dist/layers/media.css +400 -0
- package/dist/layers/mobile.css +245 -0
- package/dist/layers/motion.css +379 -0
- package/dist/layers/nav.css +771 -0
- package/dist/layers/perf.css +234 -0
- package/dist/layers/print.css +198 -0
- package/dist/layers/reset.css +103 -0
- package/dist/layers/space3d.css +280 -0
- package/dist/layers/toasts.css +201 -0
- package/dist/layers/tokens.css +229 -0
- package/dist/layers/type.css +114 -0
- package/dist/layers/utilities.css +241 -0
- package/dist/sizes.json +52 -0
- package/dist/usage.json +6340 -0
- package/package.json +94 -0
- package/src/00-layers.css +87 -0
- package/src/01-tokens.css +229 -0
- package/src/02-reset.css +103 -0
- package/src/03-type.css +114 -0
- package/src/04-layout.css +235 -0
- package/src/05-buttons.css +210 -0
- package/src/06-forms.css +401 -0
- package/src/07-components.css +868 -0
- package/src/08-mobile.css +245 -0
- package/src/09-utilities.css +241 -0
- package/src/10-datepicker.css +305 -0
- package/src/11-combobox.css +248 -0
- package/src/12-datagrid.css +335 -0
- package/src/13-toasts.css +201 -0
- package/src/14-charts.css +337 -0
- package/src/16-motion.css +379 -0
- package/src/18-container.css +199 -0
- package/src/19-logical.css +226 -0
- package/src/20-gradients.css +302 -0
- package/src/21-space3d.css +280 -0
- package/src/22-nav.css +771 -0
- package/src/23-inputs.css +522 -0
- package/src/24-media.css +400 -0
- package/src/25-anchor.css +192 -0
- package/src/26-perf.css +234 -0
- package/src/99-print.css +198 -0
- package/src/brand/deck-apple-touch-icon.png +0 -0
- package/src/brand/deck-logo-dark.svg +1 -0
- package/src/brand/deck-logo-light.svg +1 -0
- package/src/brand/deck-logo-stacked.svg +1 -0
- package/src/brand/deck-logo.svg +1 -0
- package/src/brand/deck-mark.svg +7 -0
- package/src/brand/deck-og.png +0 -0
- package/src/brand/sources.json +7 -0
- package/src/deck-icons.svg +184 -0
- package/src/js/deck-adapters.js +482 -0
- package/src/js/deck-extras.js +823 -0
- package/src/js/deck.js +1331 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/*! ==========================================================================
|
|
2
|
+
* deck-extras.js v0.1 — behaviour for the extended component set
|
|
3
|
+
* Loads after deck.js and registers itself with Deck.init.
|
|
4
|
+
* No dependencies, including the QR encoder.
|
|
5
|
+
* ======================================================================== */
|
|
6
|
+
|
|
7
|
+
(function (Deck) {
|
|
8
|
+
'use strict';
|
|
9
|
+
if (!Deck) { console.warn('deck-extras.js: load deck.js first'); return; }
|
|
10
|
+
|
|
11
|
+
const $ = (s, r = document) => r.querySelector(s);
|
|
12
|
+
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
|
|
13
|
+
const once = node => { if (node.dataset.deckX) return false; node.dataset.deckX = '1'; return true; };
|
|
14
|
+
const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
15
|
+
|
|
16
|
+
/* ======================================================================
|
|
17
|
+
Carousel
|
|
18
|
+
<div class="carousel" data-deck-carousel data-autoplay="6000">
|
|
19
|
+
====================================================================== */
|
|
20
|
+
|
|
21
|
+
function carousels(root) {
|
|
22
|
+
$$('[data-deck-carousel]', root).forEach(car => {
|
|
23
|
+
if (!once(car)) return;
|
|
24
|
+
const track = $('.carousel-track', car);
|
|
25
|
+
if (!track) return;
|
|
26
|
+
const slides = $$('.carousel-slide', track);
|
|
27
|
+
/* ::scroll-marker and ::scroll-button give us the dots, the arrows, the
|
|
28
|
+
active and disabled states and the keyboard order for free. Where they
|
|
29
|
+
exist the CSS hides the DIY ones, so drop the references here and the
|
|
30
|
+
building and wiring below skips itself. */
|
|
31
|
+
const diy = !CSS.supports('selector(::scroll-marker)');
|
|
32
|
+
const dots = diy ? $('.carousel-dots', car) : null;
|
|
33
|
+
const prev = diy ? $('.carousel-prev', car) : null;
|
|
34
|
+
const next = diy ? $('.carousel-next', car) : null;
|
|
35
|
+
const rtl = () => getComputedStyle(track).direction === 'rtl';
|
|
36
|
+
|
|
37
|
+
if (dots && !dots.children.length) {
|
|
38
|
+
dots.innerHTML = slides.map((s, i) =>
|
|
39
|
+
`<button type="button" class="carousel-dot" data-go="${i}" aria-label="Slide ${i + 1}"></button>`).join('');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const current = () => {
|
|
43
|
+
const mid = track.scrollLeft + track.clientWidth / 2;
|
|
44
|
+
let best = 0, bestD = Infinity;
|
|
45
|
+
slides.forEach((s, i) => {
|
|
46
|
+
const d = Math.abs(s.offsetLeft + s.offsetWidth / 2 - mid);
|
|
47
|
+
if (d < bestD) { bestD = d; best = i; }
|
|
48
|
+
});
|
|
49
|
+
return best;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const go = i => {
|
|
53
|
+
const s = slides[Math.max(0, Math.min(slides.length - 1, i))];
|
|
54
|
+
if (!s) return;
|
|
55
|
+
track.scrollTo({
|
|
56
|
+
left: s.offsetLeft - (track.clientWidth - s.offsetWidth) / 2,
|
|
57
|
+
behavior: reduced() ? 'auto' : 'smooth'
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const paint = () => {
|
|
62
|
+
const i = current();
|
|
63
|
+
if (dots) $$('.carousel-dot', dots).forEach((d, n) =>
|
|
64
|
+
d.setAttribute('aria-current', String(n === i)));
|
|
65
|
+
if (prev) prev.disabled = i === 0;
|
|
66
|
+
if (next) next.disabled = i === slides.length - 1;
|
|
67
|
+
car.dispatchEvent(new CustomEvent('deck:slide', { bubbles: true, detail: { index: i } }));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
track.addEventListener('scroll', () => requestAnimationFrame(paint), { passive: true });
|
|
71
|
+
prev?.addEventListener('click', () => go(current() - 1));
|
|
72
|
+
next?.addEventListener('click', () => go(current() + 1));
|
|
73
|
+
dots?.addEventListener('click', e => {
|
|
74
|
+
const b = e.target.closest('[data-go]');
|
|
75
|
+
if (b) go(Number(b.dataset.go));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
car.addEventListener('keydown', e => {
|
|
79
|
+
const back = rtl() ? 'ArrowRight' : 'ArrowLeft';
|
|
80
|
+
const fwd = rtl() ? 'ArrowLeft' : 'ArrowRight';
|
|
81
|
+
if (e.key === back) { e.preventDefault(); go(current() - 1); }
|
|
82
|
+
if (e.key === fwd) { e.preventDefault(); go(current() + 1); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const ms = Number(car.dataset.autoplay || 0);
|
|
86
|
+
if (ms > 0 && !reduced()) {
|
|
87
|
+
let timer = setInterval(tick, ms);
|
|
88
|
+
function tick() {
|
|
89
|
+
const i = current();
|
|
90
|
+
go(i >= slides.length - 1 ? 0 : i + 1);
|
|
91
|
+
}
|
|
92
|
+
const stop = () => clearInterval(timer);
|
|
93
|
+
const start = () => { stop(); timer = setInterval(tick, ms); };
|
|
94
|
+
car.addEventListener('pointerenter', stop);
|
|
95
|
+
car.addEventListener('pointerleave', start);
|
|
96
|
+
car.addEventListener('focusin', stop);
|
|
97
|
+
document.addEventListener('visibilitychange', () => document.hidden ? stop() : start());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
paint();
|
|
101
|
+
car.carousel = { go, current, paint };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ======================================================================
|
|
106
|
+
Drawer, mega menu, speed dial, banner
|
|
107
|
+
====================================================================== */
|
|
108
|
+
|
|
109
|
+
function drawers(root) {
|
|
110
|
+
$$('[data-deck-drawer]', root).forEach(trigger => {
|
|
111
|
+
if (!once(trigger)) return;
|
|
112
|
+
const target = $(trigger.dataset.deckDrawer);
|
|
113
|
+
if (!target) return;
|
|
114
|
+
trigger.addEventListener('click', () => target.showModal());
|
|
115
|
+
// Click on the backdrop closes it; the dialog element itself does not
|
|
116
|
+
target.addEventListener('click', e => {
|
|
117
|
+
if (e.target === target) target.close();
|
|
118
|
+
});
|
|
119
|
+
$$('[data-drawer-close]', target).forEach(b =>
|
|
120
|
+
b.addEventListener('click', () => target.close()));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function megaMenus(root) {
|
|
125
|
+
$$('[data-deck-mega]', root).forEach(trigger => {
|
|
126
|
+
if (!once(trigger)) return;
|
|
127
|
+
const panel = $(trigger.dataset.deckMega);
|
|
128
|
+
if (!panel) return;
|
|
129
|
+
const place = () => {
|
|
130
|
+
/* 25-anchor.css anchors .mega where the browser supports it, and these
|
|
131
|
+
inline insets would then be resolved against the anchored region and
|
|
132
|
+
double-offset the panel. */
|
|
133
|
+
if (CSS.supports('anchor-name: --a')) return;
|
|
134
|
+
const r = trigger.getBoundingClientRect();
|
|
135
|
+
panel.style.insetBlockStart = (r.bottom + 8) + 'px';
|
|
136
|
+
const w = panel.offsetWidth;
|
|
137
|
+
const start = Math.min(Math.max(8, r.left + r.width / 2 - w / 2), innerWidth - w - 8);
|
|
138
|
+
panel.style.insetInlineStart = start + 'px';
|
|
139
|
+
};
|
|
140
|
+
panel.addEventListener('toggle', e => { if (e.newState === 'open') place(); });
|
|
141
|
+
addEventListener('resize', () => { if (panel.matches(':popover-open')) place(); });
|
|
142
|
+
// Hover intent on pointer devices, click everywhere
|
|
143
|
+
if (matchMedia('(hover: hover)').matches) {
|
|
144
|
+
let t;
|
|
145
|
+
const open = () => { clearTimeout(t); t = setTimeout(() => panel.showPopover(), 90); };
|
|
146
|
+
const close = () => { clearTimeout(t); t = setTimeout(() => panel.hidePopover(), 180); };
|
|
147
|
+
[trigger, panel].forEach(n => {
|
|
148
|
+
n.addEventListener('pointerenter', open);
|
|
149
|
+
n.addEventListener('pointerleave', close);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function speedDials(root) {
|
|
156
|
+
$$('.speed-dial', root).forEach(dial => {
|
|
157
|
+
if (!once(dial)) return;
|
|
158
|
+
const fab = $('.fab', dial);
|
|
159
|
+
const set = on => {
|
|
160
|
+
dial.classList.toggle('is-open', on);
|
|
161
|
+
fab?.setAttribute('aria-expanded', String(on));
|
|
162
|
+
};
|
|
163
|
+
fab?.addEventListener('click', () => set(!dial.classList.contains('is-open')));
|
|
164
|
+
document.addEventListener('click', e => {
|
|
165
|
+
if (!dial.contains(e.target)) set(false);
|
|
166
|
+
});
|
|
167
|
+
dial.addEventListener('keydown', e => { if (e.key === 'Escape') { set(false); fab?.focus(); } });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function banners(root) {
|
|
172
|
+
$$('.banner', root).forEach(banner => {
|
|
173
|
+
if (!once(banner)) return;
|
|
174
|
+
const key = banner.dataset.dismissKey;
|
|
175
|
+
if (key) {
|
|
176
|
+
try { if (localStorage.getItem('deck-banner-' + key)) { banner.hidden = true; return; } } catch (e) {}
|
|
177
|
+
}
|
|
178
|
+
$('.banner-close', banner)?.addEventListener('click', () => {
|
|
179
|
+
banner.hidden = true;
|
|
180
|
+
if (key) { try { localStorage.setItem('deck-banner-' + key, '1'); } catch (e) {} }
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ======================================================================
|
|
186
|
+
Number, phone, range pair
|
|
187
|
+
====================================================================== */
|
|
188
|
+
|
|
189
|
+
function numbers(root) {
|
|
190
|
+
$$('.number', root).forEach(box => {
|
|
191
|
+
if (!once(box)) return;
|
|
192
|
+
const input = $('input', box);
|
|
193
|
+
const step = Number(input.step) || 1;
|
|
194
|
+
const min = input.min === '' ? -Infinity : Number(input.min);
|
|
195
|
+
const max = input.max === '' ? Infinity : Number(input.max);
|
|
196
|
+
const decimals = (String(step).split('.')[1] || '').length;
|
|
197
|
+
|
|
198
|
+
const nudge = dir => {
|
|
199
|
+
const now = Number(input.value) || 0;
|
|
200
|
+
const next = Math.min(max, Math.max(min, now + dir * step));
|
|
201
|
+
input.value = next.toFixed(decimals);
|
|
202
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
203
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
204
|
+
paint();
|
|
205
|
+
};
|
|
206
|
+
const paint = () => {
|
|
207
|
+
const now = Number(input.value);
|
|
208
|
+
$$('button', box).forEach(b => {
|
|
209
|
+
const d = Number(b.dataset.step || (b === box.firstElementChild ? -1 : 1));
|
|
210
|
+
b.disabled = d < 0 ? now <= min : now >= max;
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
$$('button', box).forEach(b => {
|
|
215
|
+
const dir = Number(b.dataset.step || (b === box.firstElementChild ? -1 : 1));
|
|
216
|
+
let hold, repeat;
|
|
217
|
+
const start = e => {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
nudge(dir);
|
|
220
|
+
hold = setTimeout(() => { repeat = setInterval(() => nudge(dir), 70); }, 420);
|
|
221
|
+
};
|
|
222
|
+
const stop = () => { clearTimeout(hold); clearInterval(repeat); };
|
|
223
|
+
b.addEventListener('pointerdown', start);
|
|
224
|
+
['pointerup', 'pointerleave', 'pointercancel'].forEach(ev => b.addEventListener(ev, stop));
|
|
225
|
+
});
|
|
226
|
+
input.addEventListener('input', paint);
|
|
227
|
+
paint();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function phones(root) {
|
|
232
|
+
$$('.phone', root).forEach(box => {
|
|
233
|
+
if (!once(box)) return;
|
|
234
|
+
const input = $('input', box);
|
|
235
|
+
const select = $('select', box);
|
|
236
|
+
const codeEl = $('.phone-code', box);
|
|
237
|
+
const flagEl = $('.phone-flag', box);
|
|
238
|
+
|
|
239
|
+
const sync = () => {
|
|
240
|
+
const opt = select?.selectedOptions[0];
|
|
241
|
+
if (!opt) return;
|
|
242
|
+
if (codeEl) codeEl.textContent = opt.dataset.code || '';
|
|
243
|
+
if (flagEl) flagEl.textContent = opt.dataset.flag || '';
|
|
244
|
+
input.placeholder = opt.dataset.mask
|
|
245
|
+
? opt.dataset.mask.replace(/#/g, '0')
|
|
246
|
+
: input.placeholder;
|
|
247
|
+
};
|
|
248
|
+
select?.addEventListener('change', () => { sync(); format(); });
|
|
249
|
+
|
|
250
|
+
/* FINDINGS 62: this destroys a digit when someone edits the middle of a
|
|
251
|
+
number. The loop stops at the end of the MASK, not the end of the
|
|
252
|
+
digits, so inserting a character shifts everything along and the last
|
|
253
|
+
digit falls off with no warning -- measured: caret at index 8 of
|
|
254
|
+
"(202) 555-0147", type 9, get "(202) 559-5014". The row still reports
|
|
255
|
+
itself .is-valid because it still holds ten digits.
|
|
256
|
+
|
|
257
|
+
Assigning input.value also collapses the caret to the end.
|
|
258
|
+
|
|
259
|
+
Fix: count digits before the caret, rebuild, restore the caret to the
|
|
260
|
+
same digit offset; and refuse the keystroke when the mask is full
|
|
261
|
+
rather than dropping data silently. */
|
|
262
|
+
const format = () => {
|
|
263
|
+
const mask = select?.selectedOptions[0]?.dataset.mask;
|
|
264
|
+
const digits = input.value.replace(/\D/g, '');
|
|
265
|
+
if (!mask) { input.value = digits; return; }
|
|
266
|
+
let out = '', d = 0;
|
|
267
|
+
for (const ch of mask) {
|
|
268
|
+
if (d >= digits.length) break;
|
|
269
|
+
out += ch === '#' ? digits[d++] : ch;
|
|
270
|
+
}
|
|
271
|
+
input.value = out;
|
|
272
|
+
const full = (mask.match(/#/g) || []).length;
|
|
273
|
+
box.classList.toggle('is-valid', digits.length === full);
|
|
274
|
+
};
|
|
275
|
+
input.addEventListener('input', format);
|
|
276
|
+
input.addEventListener('blur', () => {
|
|
277
|
+
box.dispatchEvent(new CustomEvent('deck:change', {
|
|
278
|
+
bubbles: true,
|
|
279
|
+
detail: {
|
|
280
|
+
code: select?.selectedOptions[0]?.dataset.code || '',
|
|
281
|
+
number: input.value.replace(/\D/g, ''),
|
|
282
|
+
e164: (select?.selectedOptions[0]?.dataset.code || '') + input.value.replace(/\D/g, '')
|
|
283
|
+
}
|
|
284
|
+
}));
|
|
285
|
+
});
|
|
286
|
+
sync();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function rangePairs(root) {
|
|
291
|
+
$$('.range-pair', root).forEach(pair => {
|
|
292
|
+
if (!once(pair)) return;
|
|
293
|
+
const [lo, hi] = $$('input[type="range"]', pair);
|
|
294
|
+
if (!lo || !hi) return;
|
|
295
|
+
const min = Number(lo.min || 0), max = Number(lo.max || 100);
|
|
296
|
+
const pct = v => ((v - min) / (max - min)) * 100;
|
|
297
|
+
const gap = Number(pair.dataset.gap || 0);
|
|
298
|
+
|
|
299
|
+
const paint = () => {
|
|
300
|
+
if (Number(lo.value) > Number(hi.value) - gap) {
|
|
301
|
+
// whichever handle moved last gets pushed back
|
|
302
|
+
if (document.activeElement === lo) lo.value = String(Number(hi.value) - gap);
|
|
303
|
+
else hi.value = String(Number(lo.value) + gap);
|
|
304
|
+
}
|
|
305
|
+
pair.style.setProperty('--lo', pct(Number(lo.value)).toFixed(2));
|
|
306
|
+
pair.style.setProperty('--hi', pct(Number(hi.value)).toFixed(2));
|
|
307
|
+
const readout = pair.parentElement?.querySelector('.range-readout');
|
|
308
|
+
if (readout) {
|
|
309
|
+
const [a, b] = readout.children;
|
|
310
|
+
if (a) a.textContent = lo.dataset.prefix ? lo.dataset.prefix + lo.value : lo.value;
|
|
311
|
+
if (b) b.textContent = hi.dataset.prefix ? hi.dataset.prefix + hi.value : hi.value;
|
|
312
|
+
}
|
|
313
|
+
pair.dispatchEvent(new CustomEvent('deck:change', {
|
|
314
|
+
bubbles: true, detail: { min: Number(lo.value), max: Number(hi.value) }
|
|
315
|
+
}));
|
|
316
|
+
};
|
|
317
|
+
[lo, hi].forEach(i => i.addEventListener('input', paint));
|
|
318
|
+
paint();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ======================================================================
|
|
323
|
+
Copy to clipboard
|
|
324
|
+
====================================================================== */
|
|
325
|
+
|
|
326
|
+
async function writeClipboard(text) {
|
|
327
|
+
try {
|
|
328
|
+
await navigator.clipboard.writeText(text);
|
|
329
|
+
return true;
|
|
330
|
+
} catch (e) {
|
|
331
|
+
// http origins and older engines
|
|
332
|
+
const ta = document.createElement('textarea');
|
|
333
|
+
ta.value = text;
|
|
334
|
+
ta.setAttribute('readonly', '');
|
|
335
|
+
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
|
|
336
|
+
document.body.append(ta);
|
|
337
|
+
ta.select();
|
|
338
|
+
let ok = false;
|
|
339
|
+
try { ok = document.execCommand('copy'); } catch (err) {}
|
|
340
|
+
ta.remove();
|
|
341
|
+
return ok;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function copiers(root) {
|
|
346
|
+
$$('[data-deck-copy]', root).forEach(btn => {
|
|
347
|
+
if (!once(btn)) return;
|
|
348
|
+
btn.addEventListener('click', async () => {
|
|
349
|
+
const sel = btn.dataset.deckCopy;
|
|
350
|
+
const text = sel && sel !== 'true'
|
|
351
|
+
? ($(sel)?.value ?? $(sel)?.textContent ?? sel)
|
|
352
|
+
: (btn.closest('.copy')?.querySelector('code, .copy-value')?.textContent
|
|
353
|
+
?? btn.previousElementSibling?.textContent ?? '');
|
|
354
|
+
const ok = await writeClipboard(String(text).trim());
|
|
355
|
+
btn.classList.toggle('is-copied', ok);
|
|
356
|
+
/* FINDINGS 63: this is set AFTER the subtree has already changed, so
|
|
357
|
+
the first press announces nothing -- a live region has to exist
|
|
358
|
+
before its content changes. Every press after that announces,
|
|
359
|
+
because the attribute is by then already there. Declare it in the
|
|
360
|
+
markup, or write into a separate role="status" element. */
|
|
361
|
+
btn.setAttribute('aria-live', 'polite');
|
|
362
|
+
if (ok && !btn.classList.contains('copy-inline') && !$('.copy-done', btn)) {
|
|
363
|
+
Deck.toast({ kind: 'good', title: 'Copied', duration: 2000 });
|
|
364
|
+
}
|
|
365
|
+
setTimeout(() => btn.classList.remove('is-copied'), 1800);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* ======================================================================
|
|
371
|
+
WYSIWYG editor
|
|
372
|
+
====================================================================== */
|
|
373
|
+
|
|
374
|
+
function editors(root) {
|
|
375
|
+
$$('.editor', root).forEach(ed => {
|
|
376
|
+
if (!once(ed)) return;
|
|
377
|
+
const content = $('.editor-content', ed);
|
|
378
|
+
const target = ed.dataset.target ? $(ed.dataset.target) : null;
|
|
379
|
+
const counter = $('.editor-count', ed);
|
|
380
|
+
const limit = Number(ed.dataset.limit || 0);
|
|
381
|
+
if (!content) return;
|
|
382
|
+
content.contentEditable = 'true';
|
|
383
|
+
content.setAttribute('role', 'textbox');
|
|
384
|
+
content.setAttribute('aria-multiline', 'true');
|
|
385
|
+
|
|
386
|
+
const run = (cmd, value = null) => {
|
|
387
|
+
content.focus();
|
|
388
|
+
document.execCommand(cmd, false, value);
|
|
389
|
+
paint();
|
|
390
|
+
sync();
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const paint = () => {
|
|
394
|
+
$$('[data-cmd]', ed).forEach(b => {
|
|
395
|
+
let on = false;
|
|
396
|
+
try { on = document.queryCommandState(b.dataset.cmd); } catch (e) {}
|
|
397
|
+
b.setAttribute('aria-pressed', String(on));
|
|
398
|
+
});
|
|
399
|
+
if (counter) {
|
|
400
|
+
const n = content.textContent.trim().length;
|
|
401
|
+
counter.textContent = limit ? `${n} / ${limit}` : `${n} characters`;
|
|
402
|
+
counter.classList.toggle('is-over', limit > 0 && n > limit);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const sync = () => {
|
|
407
|
+
if (target) target.value = content.innerHTML;
|
|
408
|
+
ed.dispatchEvent(new CustomEvent('deck:change', {
|
|
409
|
+
bubbles: true, detail: { html: content.innerHTML, text: content.textContent }
|
|
410
|
+
}));
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
ed.addEventListener('click', e => {
|
|
414
|
+
const b = e.target.closest('[data-cmd]');
|
|
415
|
+
if (!b) return;
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
const cmd = b.dataset.cmd;
|
|
418
|
+
if (cmd === 'createLink') {
|
|
419
|
+
const url = prompt('Link address');
|
|
420
|
+
if (url) run('createLink', url);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
run(cmd, b.dataset.value || null);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
$$('.editor-select', ed).forEach(sel => {
|
|
427
|
+
sel.addEventListener('change', () => run('formatBlock', sel.value));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Paste as plain text, so a paste from Word does not import its styling
|
|
431
|
+
content.addEventListener('paste', e => {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
const text = (e.clipboardData || window.clipboardData).getData('text/plain');
|
|
434
|
+
document.execCommand('insertText', false, text);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
content.addEventListener('input', () => { paint(); sync(); });
|
|
438
|
+
content.addEventListener('keyup', paint);
|
|
439
|
+
content.addEventListener('mouseup', paint);
|
|
440
|
+
document.addEventListener('selectionchange', () => {
|
|
441
|
+
if (content.contains(document.getSelection()?.anchorNode)) paint();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (target && target.value) content.innerHTML = target.value;
|
|
445
|
+
paint();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* ======================================================================
|
|
450
|
+
Lazy images
|
|
451
|
+
====================================================================== */
|
|
452
|
+
|
|
453
|
+
function lazies(root) {
|
|
454
|
+
const imgs = $$('.lazy img, img[data-src]', root).filter(i => !i.dataset.deckX);
|
|
455
|
+
if (!imgs.length) return;
|
|
456
|
+
const load = img => {
|
|
457
|
+
img.dataset.deckX = '1';
|
|
458
|
+
if (img.dataset.src) img.src = img.dataset.src;
|
|
459
|
+
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
|
|
460
|
+
const done = () => img.classList.add('is-loaded');
|
|
461
|
+
if (img.complete && img.naturalWidth) done();
|
|
462
|
+
else img.addEventListener('load', done, { once: true });
|
|
463
|
+
img.addEventListener('error', () => img.closest('.lazy')?.classList.add('is-error'), { once: true });
|
|
464
|
+
};
|
|
465
|
+
if (!('IntersectionObserver' in window)) return imgs.forEach(load);
|
|
466
|
+
const io = new IntersectionObserver(entries => {
|
|
467
|
+
entries.forEach(e => {
|
|
468
|
+
if (!e.isIntersecting) return;
|
|
469
|
+
load(e.target);
|
|
470
|
+
io.unobserve(e.target);
|
|
471
|
+
});
|
|
472
|
+
}, { rootMargin: '200px' });
|
|
473
|
+
imgs.forEach(i => io.observe(i));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* ======================================================================
|
|
477
|
+
QR code
|
|
478
|
+
A complete encoder: byte mode, versions 1 to 10, error correction
|
|
479
|
+
L / M / Q / H, all eight masks scored by the standard penalty rules.
|
|
480
|
+
No dependency, no network call, no canvas — it emits SVG.
|
|
481
|
+
====================================================================== */
|
|
482
|
+
|
|
483
|
+
const QR = (() => {
|
|
484
|
+
// Galois field GF(256) with the QR primitive polynomial 0x11D
|
|
485
|
+
const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
|
|
486
|
+
for (let i = 0, x = 1; i < 255; i++) {
|
|
487
|
+
EXP[i] = x; LOG[x] = i;
|
|
488
|
+
x <<= 1; if (x & 0x100) x ^= 0x11d;
|
|
489
|
+
}
|
|
490
|
+
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
|
|
491
|
+
const mul = (a, b) => (a && b) ? EXP[LOG[a] + LOG[b]] : 0;
|
|
492
|
+
|
|
493
|
+
// [ecPerBlock, group1Blocks, group1Data, group2Blocks, group2Data] by version, then L M Q H
|
|
494
|
+
const RS = {
|
|
495
|
+
1: [[7,1,19,0,0],[10,1,16,0,0],[13,1,13,0,0],[17,1,9,0,0]],
|
|
496
|
+
2: [[10,1,34,0,0],[16,1,28,0,0],[22,1,22,0,0],[28,1,16,0,0]],
|
|
497
|
+
3: [[15,1,55,0,0],[26,1,44,0,0],[18,2,17,0,0],[22,2,13,0,0]],
|
|
498
|
+
4: [[20,1,80,0,0],[18,2,32,0,0],[26,2,24,0,0],[16,4,9,0,0]],
|
|
499
|
+
5: [[26,1,108,0,0],[24,2,43,0,0],[18,2,15,2,16],[22,2,11,2,12]],
|
|
500
|
+
6: [[18,2,68,0,0],[16,4,27,0,0],[24,4,19,0,0],[28,4,15,0,0]],
|
|
501
|
+
7: [[20,2,78,0,0],[18,4,31,0,0],[18,2,14,4,15],[26,4,13,1,14]],
|
|
502
|
+
8: [[24,2,97,0,0],[22,2,38,2,39],[22,4,18,2,19],[26,4,14,2,15]],
|
|
503
|
+
9: [[30,2,116,0,0],[22,3,36,2,37],[20,4,16,4,17],[24,4,12,4,13]],
|
|
504
|
+
10: [[18,2,68,2,69],[26,4,43,1,44],[24,6,19,2,20],[28,6,15,2,16]]
|
|
505
|
+
};
|
|
506
|
+
const ALIGN = {
|
|
507
|
+
1: [], 2: [6,18], 3: [6,22], 4: [6,26], 5: [6,30],
|
|
508
|
+
6: [6,34], 7: [6,22,38], 8: [6,24,42], 9: [6,26,46], 10: [6,28,50]
|
|
509
|
+
};
|
|
510
|
+
const ECL = { L: 0, M: 1, Q: 2, H: 3 };
|
|
511
|
+
const ECL_BITS = { L: 1, M: 0, Q: 3, H: 2 };
|
|
512
|
+
|
|
513
|
+
function generator(n) {
|
|
514
|
+
let poly = [1];
|
|
515
|
+
for (let i = 0; i < n; i++) {
|
|
516
|
+
const next = new Array(poly.length + 1).fill(0);
|
|
517
|
+
for (let j = 0; j < poly.length; j++) {
|
|
518
|
+
next[j] ^= mul(poly[j], EXP[i]);
|
|
519
|
+
next[j + 1] ^= poly[j];
|
|
520
|
+
}
|
|
521
|
+
poly = next;
|
|
522
|
+
}
|
|
523
|
+
return poly;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function ecBytes(data, n) {
|
|
527
|
+
const gen = generator(n);
|
|
528
|
+
const rem = new Array(n).fill(0);
|
|
529
|
+
for (const byte of data) {
|
|
530
|
+
const factor = byte ^ rem[0];
|
|
531
|
+
rem.shift();
|
|
532
|
+
rem.push(0);
|
|
533
|
+
for (let i = 0; i < n; i++) rem[i] ^= mul(gen[i + 1], factor);
|
|
534
|
+
}
|
|
535
|
+
return rem;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function formatBits(ecl, mask) {
|
|
539
|
+
const data = (ECL_BITS[ecl] << 3) | mask;
|
|
540
|
+
let rem = data;
|
|
541
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >> 9) * 0x537);
|
|
542
|
+
return ((data << 10) | rem) ^ 0x5412;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function versionBits(v) {
|
|
546
|
+
let rem = v;
|
|
547
|
+
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >> 11) * 0x1f25);
|
|
548
|
+
return (v << 12) | rem;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function pickVersion(len, ecl) {
|
|
552
|
+
for (let v = 1; v <= 10; v++) {
|
|
553
|
+
const [ec, b1, d1, b2, d2] = RS[v][ECL[ecl]];
|
|
554
|
+
const total = b1 * d1 + b2 * d2;
|
|
555
|
+
const countBits = v < 10 ? 8 : 16;
|
|
556
|
+
if (len <= Math.floor((total * 8 - 4 - countBits) / 8)) return v;
|
|
557
|
+
}
|
|
558
|
+
throw new Error('QR: content too long for versions 1 to 10. Shorten it or link to it.');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function encode(text, ecl = 'M') {
|
|
562
|
+
const bytes = Array.from(new TextEncoder().encode(text));
|
|
563
|
+
const version = pickVersion(bytes.length, ecl);
|
|
564
|
+
const [ecPer, b1, d1, b2, d2] = RS[version][ECL[ecl]];
|
|
565
|
+
const totalData = b1 * d1 + b2 * d2;
|
|
566
|
+
const countBits = version < 10 ? 8 : 16;
|
|
567
|
+
|
|
568
|
+
// Bit stream: mode 0100, character count, payload, terminator, padding
|
|
569
|
+
const bits = [];
|
|
570
|
+
const push = (val, n) => { for (let i = n - 1; i >= 0; i--) bits.push((val >> i) & 1); };
|
|
571
|
+
push(4, 4);
|
|
572
|
+
push(bytes.length, countBits);
|
|
573
|
+
bytes.forEach(b => push(b, 8));
|
|
574
|
+
for (let i = 0; i < 4 && bits.length < totalData * 8; i++) bits.push(0);
|
|
575
|
+
while (bits.length % 8) bits.push(0);
|
|
576
|
+
const data = [];
|
|
577
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
578
|
+
data.push(parseInt(bits.slice(i, i + 8).join(''), 2));
|
|
579
|
+
}
|
|
580
|
+
const PAD = [0xec, 0x11];
|
|
581
|
+
for (let i = 0; data.length < totalData; i++) data.push(PAD[i % 2]);
|
|
582
|
+
|
|
583
|
+
// Split into blocks, compute error correction, then interleave
|
|
584
|
+
const blocks = [], ecs = [];
|
|
585
|
+
let at = 0;
|
|
586
|
+
for (let i = 0; i < b1 + b2; i++) {
|
|
587
|
+
const size = i < b1 ? d1 : d2;
|
|
588
|
+
const block = data.slice(at, at + size);
|
|
589
|
+
at += size;
|
|
590
|
+
blocks.push(block);
|
|
591
|
+
ecs.push(ecBytes(block, ecPer));
|
|
592
|
+
}
|
|
593
|
+
const out = [];
|
|
594
|
+
const maxData = Math.max(d1, d2);
|
|
595
|
+
for (let i = 0; i < maxData; i++)
|
|
596
|
+
blocks.forEach(b => { if (i < b.length) out.push(b[i]); });
|
|
597
|
+
for (let i = 0; i < ecPer; i++)
|
|
598
|
+
ecs.forEach(b => out.push(b[i]));
|
|
599
|
+
|
|
600
|
+
return { version, ecl, codewords: out };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function build(text, ecl = 'M') {
|
|
604
|
+
const { version, codewords } = encode(text, ecl);
|
|
605
|
+
const size = version * 4 + 17;
|
|
606
|
+
const grid = Array.from({ length: size }, () => new Array(size).fill(null));
|
|
607
|
+
const reserved = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
608
|
+
|
|
609
|
+
const put = (x, y, v, res = true) => {
|
|
610
|
+
if (x < 0 || y < 0 || x >= size || y >= size) return;
|
|
611
|
+
grid[y][x] = v;
|
|
612
|
+
if (res) reserved[y][x] = true;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Finder patterns and separators
|
|
616
|
+
[[0, 0], [size - 7, 0], [0, size - 7]].forEach(([ox, oy]) => {
|
|
617
|
+
for (let y = -1; y <= 7; y++) for (let x = -1; x <= 7; x++) {
|
|
618
|
+
const on = (x >= 0 && x <= 6 && (y === 0 || y === 6)) ||
|
|
619
|
+
(y >= 0 && y <= 6 && (x === 0 || x === 6)) ||
|
|
620
|
+
(x >= 2 && x <= 4 && y >= 2 && y <= 4);
|
|
621
|
+
put(ox + x, oy + y, on ? 1 : 0);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Timing patterns
|
|
626
|
+
for (let i = 8; i < size - 8; i++) {
|
|
627
|
+
put(i, 6, i % 2 === 0 ? 1 : 0);
|
|
628
|
+
put(6, i, i % 2 === 0 ? 1 : 0);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Alignment patterns, skipping the ones that collide with finders
|
|
632
|
+
const centers = ALIGN[version];
|
|
633
|
+
centers.forEach(cy => centers.forEach(cx => {
|
|
634
|
+
if ((cx === 6 && cy === 6) ||
|
|
635
|
+
(cx === 6 && cy === size - 7) ||
|
|
636
|
+
(cx === size - 7 && cy === 6)) return;
|
|
637
|
+
for (let y = -2; y <= 2; y++) for (let x = -2; x <= 2; x++) {
|
|
638
|
+
const on = Math.max(Math.abs(x), Math.abs(y)) !== 1;
|
|
639
|
+
put(cx + x, cy + y, on ? 1 : 0);
|
|
640
|
+
}
|
|
641
|
+
}));
|
|
642
|
+
|
|
643
|
+
// Dark module, then reserve the two format strips so data skips them
|
|
644
|
+
put(8, size - 8, 1);
|
|
645
|
+
for (let i = 0; i < 9; i++) { reserved[8][i] = true; reserved[i][8] = true; }
|
|
646
|
+
for (let i = 0; i < 8; i++) {
|
|
647
|
+
reserved[8][size - 1 - i] = true;
|
|
648
|
+
reserved[size - 1 - i][8] = true;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Version information blocks, versions 7 and up
|
|
652
|
+
if (version >= 7) {
|
|
653
|
+
const vb = versionBits(version);
|
|
654
|
+
for (let i = 0; i < 18; i++) {
|
|
655
|
+
const b = (vb >> i) & 1;
|
|
656
|
+
const a = Math.floor(i / 3), c = i % 3;
|
|
657
|
+
grid[a][size - 11 + c] = b; reserved[a][size - 11 + c] = true;
|
|
658
|
+
grid[size - 11 + c][a] = b; reserved[size - 11 + c][a] = true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Place data in the zig-zag, skipping the timing column
|
|
663
|
+
let bit = 0;
|
|
664
|
+
const stream = [];
|
|
665
|
+
codewords.forEach(cw => { for (let i = 7; i >= 0; i--) stream.push((cw >> i) & 1); });
|
|
666
|
+
|
|
667
|
+
let up = true;
|
|
668
|
+
for (let right = size - 1; right > 0; right -= 2) {
|
|
669
|
+
if (right === 6) right = 5;
|
|
670
|
+
for (let step = 0; step < size; step++) {
|
|
671
|
+
const y = up ? size - 1 - step : step;
|
|
672
|
+
for (let c = 0; c < 2; c++) {
|
|
673
|
+
const x = right - c;
|
|
674
|
+
if (reserved[y][x]) continue;
|
|
675
|
+
grid[y][x] = bit < stream.length ? stream[bit++] : 0;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
up = !up;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Try every mask, keep the one with the lowest penalty
|
|
682
|
+
const maskFn = [
|
|
683
|
+
(x, y) => (x + y) % 2 === 0,
|
|
684
|
+
(x, y) => y % 2 === 0,
|
|
685
|
+
(x, y) => x % 3 === 0,
|
|
686
|
+
(x, y) => (x + y) % 3 === 0,
|
|
687
|
+
(x, y) => (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0,
|
|
688
|
+
(x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
689
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
690
|
+
(x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
let best = null, bestScore = Infinity;
|
|
694
|
+
for (let m = 0; m < 8; m++) {
|
|
695
|
+
const test = grid.map(r => r.slice());
|
|
696
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
697
|
+
if (!reserved[y][x] && maskFn[m](x, y)) test[y][x] ^= 1;
|
|
698
|
+
}
|
|
699
|
+
const fb = formatBits(ecl, m);
|
|
700
|
+
const fbit = i => (fb >> i) & 1;
|
|
701
|
+
// grid is indexed [row][column]
|
|
702
|
+
// First copy: down column 8, then left along row 8
|
|
703
|
+
for (let i = 0; i <= 5; i++) test[i][8] = fbit(i);
|
|
704
|
+
test[7][8] = fbit(6);
|
|
705
|
+
test[8][8] = fbit(7);
|
|
706
|
+
test[8][7] = fbit(8);
|
|
707
|
+
for (let i = 9; i <= 14; i++) test[8][14 - i] = fbit(i);
|
|
708
|
+
// Second copy: along row 8 beside the top-right finder,
|
|
709
|
+
// then down column 8 beside the bottom-left finder
|
|
710
|
+
for (let i = 0; i <= 7; i++) test[8][size - 1 - i] = fbit(i);
|
|
711
|
+
for (let i = 8; i <= 14; i++) test[size - 15 + i][8] = fbit(i);
|
|
712
|
+
test[size - 8][8] = 1;
|
|
713
|
+
const score = penalty(test, size);
|
|
714
|
+
if (score < bestScore) { bestScore = score; best = test; }
|
|
715
|
+
}
|
|
716
|
+
return { modules: best, size, version };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function penalty(m, size) {
|
|
720
|
+
let score = 0;
|
|
721
|
+
// Rule 1: runs of five or more of the same colour
|
|
722
|
+
const run = line => {
|
|
723
|
+
let total = 0, count = 1;
|
|
724
|
+
for (let i = 1; i < size; i++) {
|
|
725
|
+
if (line[i] === line[i - 1]) count++;
|
|
726
|
+
else { if (count >= 5) total += count - 2; count = 1; }
|
|
727
|
+
}
|
|
728
|
+
if (count >= 5) total += count - 2;
|
|
729
|
+
return total;
|
|
730
|
+
};
|
|
731
|
+
for (let y = 0; y < size; y++) score += run(m[y]);
|
|
732
|
+
for (let x = 0; x < size; x++) score += run(m.map(r => r[x]));
|
|
733
|
+
// Rule 2: two by two blocks
|
|
734
|
+
for (let y = 0; y < size - 1; y++) for (let x = 0; x < size - 1; x++) {
|
|
735
|
+
const v = m[y][x];
|
|
736
|
+
if (v === m[y][x + 1] && v === m[y + 1][x] && v === m[y + 1][x + 1]) score += 3;
|
|
737
|
+
}
|
|
738
|
+
// Rule 3: the finder-like 1011101 pattern with four light modules beside it
|
|
739
|
+
const pat1 = [1,0,1,1,1,0,1,0,0,0,0];
|
|
740
|
+
const pat2 = [0,0,0,0,1,0,1,1,1,0,1];
|
|
741
|
+
const scan = line => {
|
|
742
|
+
let s = 0;
|
|
743
|
+
for (let i = 0; i <= size - 11; i++) {
|
|
744
|
+
const slice = line.slice(i, i + 11).join('');
|
|
745
|
+
if (slice === pat1.join('') || slice === pat2.join('')) s += 40;
|
|
746
|
+
}
|
|
747
|
+
return s;
|
|
748
|
+
};
|
|
749
|
+
for (let y = 0; y < size; y++) score += scan(m[y]);
|
|
750
|
+
for (let x = 0; x < size; x++) score += scan(m.map(r => r[x]));
|
|
751
|
+
// Rule 4: deviation from an even split of dark and light
|
|
752
|
+
let dark = 0;
|
|
753
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) dark += m[y][x];
|
|
754
|
+
const ratio = Math.abs((dark * 100) / (size * size) - 50);
|
|
755
|
+
score += Math.floor(ratio / 5) * 10;
|
|
756
|
+
return score;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function svg(text, ecl = 'M') {
|
|
760
|
+
const { modules, size } = build(text, ecl);
|
|
761
|
+
// One path-free approach: merge horizontal runs into rects so the SVG
|
|
762
|
+
// stays small even at version 10
|
|
763
|
+
let rects = '';
|
|
764
|
+
for (let y = 0; y < size; y++) {
|
|
765
|
+
let x = 0;
|
|
766
|
+
while (x < size) {
|
|
767
|
+
if (!modules[y][x]) { x++; continue; }
|
|
768
|
+
let w = 1;
|
|
769
|
+
while (x + w < size && modules[y][x + w]) w++;
|
|
770
|
+
rects += `<rect x="${x}" y="${y}" width="${w}" height="1"/>`;
|
|
771
|
+
x += w;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" role="img"><g>${rects}</g></svg>`;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return { svg, build, encode };
|
|
778
|
+
})();
|
|
779
|
+
|
|
780
|
+
function qrCodes(root) {
|
|
781
|
+
$$('[data-deck-qr]', root).forEach(node => {
|
|
782
|
+
if (!once(node)) return;
|
|
783
|
+
const value = node.dataset.deckQr || node.textContent.trim();
|
|
784
|
+
const level = (node.dataset.ecl || 'M').toUpperCase();
|
|
785
|
+
try {
|
|
786
|
+
node.innerHTML = QR.svg(value, level) + (node.dataset.caption
|
|
787
|
+
? `<span class="qr-caption">${node.dataset.caption}</span>` : '');
|
|
788
|
+
} catch (err) {
|
|
789
|
+
node.innerHTML = `<span class="text-sm text-muted">${err.message}</span>`;
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/* ======================================================================
|
|
795
|
+
Register
|
|
796
|
+
====================================================================== */
|
|
797
|
+
|
|
798
|
+
const baseInit = Deck.init.bind(Deck);
|
|
799
|
+
Deck.init = function (root = document) {
|
|
800
|
+
baseInit(root);
|
|
801
|
+
carousels(root);
|
|
802
|
+
drawers(root);
|
|
803
|
+
megaMenus(root);
|
|
804
|
+
speedDials(root);
|
|
805
|
+
banners(root);
|
|
806
|
+
numbers(root);
|
|
807
|
+
phones(root);
|
|
808
|
+
rangePairs(root);
|
|
809
|
+
copiers(root);
|
|
810
|
+
editors(root);
|
|
811
|
+
lazies(root);
|
|
812
|
+
qrCodes(root);
|
|
813
|
+
return Deck;
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
Deck.qr = QR;
|
|
817
|
+
Deck.copy = writeClipboard;
|
|
818
|
+
|
|
819
|
+
if (document.readyState !== 'loading') Deck.init();
|
|
820
|
+
else document.addEventListener('DOMContentLoaded', () => Deck.init());
|
|
821
|
+
|
|
822
|
+
})(typeof window !== 'undefined' && window.Deck ? window.Deck
|
|
823
|
+
: typeof Deck !== 'undefined' ? Deck : null);
|