oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/switcher.mjs — the desktop/phone switcher (spec §3.2), and
2// the one rule it changes: where a layout edit lands.
3//
4// Three views. Desktop is the stage as the projector sees it; phone
5// renders the same slide at 390 CSS pixels with ?vp=phone, the phone
6// layer applied; both shows the two side by side, the phone one
7// read-only. In the phone view an edit to a block's box, size, order
8// or hidden goes to its phone override map (set_phone, §2.5) rather
9// than to the block, and text goes to the text, because there is one
10// text. hidden and order live only in the phone map, so they go there
11// from either view.
12//
13// routeEdit is the pure rule (switcher.test.mjs). The view persists in
14// localStorage — a per-person convenience, never document state — and
15// the URL is not used for it.
16import { fixBox } from './canvas.mjs'
17
18export const VIEWS = ['desktop', 'phone', 'both']
19export const STORAGE_KEY = 'slides.editor.view'
20
21/** The fields whose destination depends on the view. */
22export const PHONE_ROUTED = ['box', 'size', 'hidden', 'order']
23
24/** set_phone always replaces the whole map, so a change carries the rest of it along. */
25export function phoneMap (block) {
26 const p = block && block.phone ? block.phone : {}
27 const out = {}
28 if (typeof p.hidden === 'boolean') out.hidden = p.hidden
29 if (Number.isInteger(p.order)) out.order = p.order
30 if (typeof p.size === 'string' && p.size) out.size = p.size
31 if (p.box && typeof p.box === 'object') out.box = fixBox(p.box)
32 return out
33}
34
35/**
36 * The op for editing field on block (the view's block JSON) on slide,
37 * given the view the stage is in. value null clears the field: a
38 * phone field goes from the map, a desktop box is left alone (a block
39 * always has one), a desktop size goes from props.
40 */
41export function routeEdit ({ view, slide, block, field, value }) {
42 if (!PHONE_ROUTED.includes(field)) throw new Error(`routeEdit: ${field} is not a view-dependent field`)
43 const toPhone = view === 'phone' || field === 'hidden' || field === 'order'
44 if (toPhone) {
45 const phone = phoneMap(block)
46 if (value === null || value === undefined || value === '' || value === false) delete phone[field]
47 else phone[field] = field === 'box' ? fixBox(value) : value
48 return { op: 'set_phone', slide, id: block.id, phone }
49 }
50 if (field === 'box') return { op: 'set_block', slide, id: block.id, box: fixBox(value) }
51 return { op: 'set_block', slide, id: block.id, props: { size: value === '' ? null : value } }
52}
53
54/** The op that clears a block's phone layer: "Reset to desktop". */
55export function resetPhone (slide, block) {
56 return { op: 'set_phone', slide, id: block.id, phone: {} }
57}
58
59/** Whether a block carries any phone override. */
60export function hasOverride (block) {
61 return Object.keys(phoneMap(block)).length > 0
62}
63
64/** The view to open with: what was stored, if it is one of the three. */
65export function readView (storage) {
66 try {
67 const v = storage.getItem(STORAGE_KEY)
68 return VIEWS.includes(v) ? v : 'desktop'
69 } catch {
70 return 'desktop'
71 }
72}
73
74export function writeView (storage, view) {
75 try { storage.setItem(STORAGE_KEY, view) } catch { /* private mode, quota: the choice is for this session then */ }
76}
77
78/**
79 * Mounts the switcher over its fieldset of radios. onChange(view) is
80 * called on every change and once at mount with the stored view.
81 */
82export function mountSwitcher ({ root, storage, onChange }) {
83 const radios = Array.from(root.querySelectorAll('input[type="radio"]'))
84 let current = readView(storage)
85 function apply (view) {
86 current = view
87 for (const r of radios) r.checked = r.value === view
88 writeView(storage, view)
89 onChange(view)
90 }
91 root.addEventListener('change', e => {
92 if (e.target.type === 'radio' && VIEWS.includes(e.target.value)) apply(e.target.value)
93 })
94 apply(current)
95 return {
96 get view () { return current },
97 set: apply
98 }
99}
100