oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/inspector-block.mjs — the inspector's Block section: the
2// selected block's props (each field marked data-types with the types
3// that have it), its placement (a region on a flow slide, a box and a
4// layer on a canvas one), and its phone layer (§2.5). fillBlock puts
5// the block's values in the controls and shows the right ones;
6// blockChange turns a changed control into its op.
7//
8// The controls are typed by attribute rather than by name, so a new
9// prop is a new field in editor.html and nothing here:
10//
11// data-prop="level" data-kind="int" a prop → set_block props
12// data-text="label" a shape's label → set_text
13// data-place="region" the region → set_block region
14// data-box="x" a box field → set_block box, or
15// set_phone box in the phone view
16// data-phone="hidden|order|size" the phone layer → set_phone
17//
18// size is view-dependent (switcher.mjs): the desktop Size field goes
19// to the phone map in the phone view, which is what the switcher
20// promises. The phone fieldset's own Size always goes to the map.
21import { propOp, propValue, regionOp, labelOp, boxFieldOp } from './inspector-ops.mjs'
22import { routeEdit, hasOverride } from './switcher.mjs'
23import { defaultRegion } from './layout.mjs'
24
25/** Sets a control's value without disturbing the one the person is typing in. */
26export function setValue (el, value) {
27 if (!el) return
28 if (el.type === 'checkbox') {
29 el.checked = !!value
30 return
31 }
32 // Never write into the control somebody is using: a value replaced
33 // under a caret loses what was half-typed, and a <select> or a colour
34 // picker being changed by a peer's edit mid-choice is the same
35 // surprise by another route.
36 if (el === document.activeElement &&
37 (el.tagName === 'SELECT' || (el.tagName === 'INPUT' && ['text', 'number', 'url', 'color'].includes(el.type)))) return
38 const v = value === null || value === undefined ? '' : String(value)
39 if (el.value !== v) el.value = v
40}
41
42/** Shows the elements under root whose attr names a value in wanted, hides the rest. */
43export function showWhere (root, attr, test) {
44 for (const el of root.querySelectorAll(`[${attr}]`)) {
45 el.hidden = !test(el.getAttribute(attr))
46 }
47}
48
49// The renderer's defaults, so an unset prop shows as what the slide
50// shows rather than as the first option.
51const DEFAULTS = {
52 level: 1, size: 'm', align: 'start', style: 'bullet', fit: 'cover', count: 2,
53 kind: 'rect', radius: 0, strokeWidth: 1, header: false, rows: 2, cols: 2, decorative: false
54}
55const SPACER_SIZE = 'l'
56
57/** The block's text as one plain string (a shape's label). */
58export function plainText (block) {
59 const paras = Array.isArray(block.text) ? block.text : []
60 return paras.map(p => p.map(r => r.t || '').join('')).join('\n')
61}
62
63/** The box the fields show: the phone override's in the phone view when there is one. */
64export function shownBox (block, view) {
65 if (view === 'phone' && block.phone && block.phone.box) return block.phone.box
66 return block.box || null
67}
68
69const pctText = f => (Math.round(f * 1000) / 10).toFixed(1)
70
71export function fillBlock ({ form, slide, block, view, labels }) {
72 const typeLabel = labels.types[block.type] || block.type
73 const head = form.parentElement.querySelector('[slides-block-type]')
74 if (head) head.textContent = typeLabel
75 showWhere(form, 'data-types', v => v.split(' ').includes(block.type))
76 showWhere(form, 'data-layout', v => v === slide.layout)
77 form.querySelector('[slides-phone-badge]').hidden = !hasOverride(block)
78
79 const props = block.props || {}
80 for (const el of form.querySelectorAll('[data-prop]')) {
81 const key = el.dataset.prop
82 let v = props[key]
83 if (v === undefined || v === null) v = key === 'size' && block.type === 'spacer' && el.name === 'rst-block-spacer-size' ? SPACER_SIZE : DEFAULTS[key]
84 setValue(el, v === undefined ? '' : v)
85 }
86 for (const el of form.querySelectorAll('[data-text]')) setValue(el, plainText(block))
87 for (const el of form.querySelectorAll('[data-place="region"]')) setValue(el, block.region || defaultRegion(block.type))
88 const box = shownBox(block, view)
89 for (const el of form.querySelectorAll('[data-box]')) setValue(el, box ? pctText(box[el.dataset.box]) : '')
90 const phone = block.phone || {}
91 for (const el of form.querySelectorAll('[data-phone]')) {
92 const key = el.dataset.phone
93 if (key === 'hidden') setValue(el, phone.hidden === true)
94 else if (key === 'order') setValue(el, Number.isInteger(phone.order) ? phone.order : '')
95 else setValue(el, phone.size || '')
96 }
97}
98
99/** The ops for a changed control in the Block section (usually one; [] when it is not a control this file knows). */
100export function blockChange ({ el, slide, block, view }) {
101 const slideId = slide.id
102 if (el.dataset.prop) {
103 const key = el.dataset.prop
104 const value = el.type === 'checkbox' ? el.checked : el.value
105 if (key === 'size' && block.type !== 'spacer') return [routeEdit({ view, slide: slideId, block, field: 'size', value })]
106 return [propOp(slideId, block, key, propValue(value, el.dataset.kind || ''))]
107 }
108 if (el.dataset.text) return [labelOp(slideId, block, el.value.trim())]
109 if (el.dataset.place === 'region') return [regionOp(slideId, block, el.value)]
110 if (el.dataset.box) {
111 const op = boxFieldOp({ view, slideId, block, key: el.dataset.box, percent: el.value })
112 return op ? [op] : []
113 }
114 if (el.dataset.phone) {
115 const key = el.dataset.phone
116 let value
117 if (key === 'hidden') value = el.checked
118 else if (key === 'order') value = el.value === '' ? null : propValue(el.value, 'int')
119 else value = el.value
120 return [routeEdit({ view: 'phone', slide: slideId, block, field: key, value })]
121 }
122 return []
123}
124