| 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. |
| 21 | import { propOp, propValue, regionOp, labelOp, boxFieldOp } from './inspector-ops.mjs' |
| 22 | import { routeEdit, hasOverride } from './switcher.mjs' |
| 23 | import { defaultRegion } from './layout.mjs' |
| 24 | |
| 25 | /** Sets a control's value without disturbing the one the person is typing in. */ |
| 26 | export 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. */ |
| 43 | export 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. |
| 51 | const 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 | } |
| 55 | const SPACER_SIZE = 'l' |
| 56 | |
| 57 | /** The block's text as one plain string (a shape's label). */ |
| 58 | export 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. */ |
| 64 | export function shownBox (block, view) { |
| 65 | if (view === 'phone' && block.phone && block.phone.box) return block.phone.box |
| 66 | return block.box || null |
| 67 | } |
| 68 | |
| 69 | const pctText = f => (Math.round(f * 1000) / 10).toFixed(1) |
| 70 | |
| 71 | export 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). */ |
| 100 | export 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 | |