oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/commands.mjs — the editor's structural commands, one place
2// for the three surfaces that offer them: the Slide menu in the bar
3// (and its shortcuts), the inspector's buttons, and the strip's item
4// menu. Each is a batch of ops through commit() — the same vocabulary
5// the API takes — and nothing here touches the stage: the entry's
6// observers re-fetch what the ops changed once they are saved.
7//
8// ctx is read live (its fields are functions) because a resync
9// replaces the document, the stage and the undo manager under a
10// running editor.
11import { slideIDs, blockIDs } from './stage.mjs'
12import { newID } from './ids.mjs'
13import { moveSlideOp, moveBlockOp, zOps, deckOp } from './inspector-ops.mjs'
14import { resetPhone } from './switcher.mjs'
15
16/**
17 * ctx: {ydoc(), commit(ops), flush(), slideId(), selected(), slideJSON(id),
18 * blockJSON(slideId, id), undo(), redo(), present(), clearSelection(),
19 * confirmEl} — returns {run(name), strip(name, slideId)}.
20 */
21export function createCommands (ctx) {
22 // A new slide right after `after` (the current one by default), with
23 // a heading to type into; then the stage moves to it once it exists
24 // on the server.
25 async function newSlide (after = ctx.slideId()) {
26 const id = newID()
27 const op = { op: 'add_slide', id, layout: 'flow', template: 'title-body' }
28 if (after) op.after = after
29 ctx.commit([op, { op: 'add_block', slide: id, id: newID(), type: 'heading', region: 'heading', props: { level: 1 }, markdown: '' }])
30 await ctx.flush()
31 location.hash = '#' + id
32 }
33 async function duplicateSlide (from = ctx.slideId()) {
34 if (!from) return
35 const id = newID()
36 ctx.commit([{ op: 'duplicate_slide', id: from, new_id: id, new_block_ids: blockIDs(ctx.ydoc(), from).map(() => newID()) }])
37 await ctx.flush()
38 location.hash = '#' + id
39 }
40 function moveSlide (id, dir) {
41 const op = moveSlideOp(slideIDs(ctx.ydoc()), id, dir)
42 if (op) ctx.commit([op])
43 }
44
45 // Delete asks first, in the Slide section's own callout, and
46 // remembers which slide was asked about: the strip's menu can ask
47 // about a slide that is not yet on the stage.
48 const confirmEl = ctx.confirmEl
49 let deleteTarget = ''
50 function askDelete (id) {
51 deleteTarget = id || ctx.slideId()
52 if (!deleteTarget) return
53 if (deleteTarget !== ctx.slideId()) location.hash = '#' + deleteTarget
54 confirmEl.hidden = false
55 confirmEl.querySelector('[data-command="slide-delete"]').focus()
56 }
57 function deleteSlide () {
58 confirmEl.hidden = true
59 const id = deleteTarget
60 deleteTarget = ''
61 if (id && slideIDs(ctx.ydoc()).includes(id)) ctx.commit([{ op: 'remove_slide', id }])
62 }
63
64 // A block command runs on the selected block of the current slide.
65 const withBlock = fn => () => {
66 const slideId = ctx.slideId()
67 const block = ctx.blockJSON(slideId, ctx.selected())
68 if (block) fn(slideId, block, ctx.slideJSON(slideId))
69 }
70 const moveBlock = dir => withBlock((s, b) => {
71 const op = moveBlockOp(s, blockIDs(ctx.ydoc(), s), b.id, dir)
72 if (op) ctx.commit([op])
73 })
74 const layer = dir => withBlock((s, b, slide) => {
75 const ops = zOps(s, slide.blocks, b.id, dir)
76 if (ops.length) ctx.commit(ops)
77 })
78
79 const commands = {
80 'new-slide': () => newSlide(),
81 'slide-add-after': () => newSlide(),
82 'duplicate-slide': () => duplicateSlide(),
83 'slide-move-up': () => moveSlide(ctx.slideId(), 'up'),
84 'slide-move-down': () => moveSlide(ctx.slideId(), 'down'),
85 'slide-delete-ask': () => askDelete(''),
86 'slide-delete': deleteSlide,
87 'slide-delete-cancel': () => { confirmEl.hidden = true; deleteTarget = '' },
88 'accent-reset': () => ctx.commit([deckOp('accent-reset')]),
89 'block-move-up': moveBlock('up'),
90 'block-move-down': moveBlock('down'),
91 'block-forward': layer('forward'),
92 'block-back': layer('back'),
93 'block-delete': withBlock((s, b) => { ctx.clearSelection(); ctx.commit([{ op: 'remove_block', slide: s, id: b.id }]) }),
94 'phone-reset': withBlock((s, b) => ctx.commit([resetPhone(s, b)])),
95 undo: () => ctx.undo(),
96 redo: () => ctx.redo(),
97 present: () => ctx.present()
98 }
99
100 return {
101 has: name => name in commands,
102 run (name) {
103 if (name in commands) commands[name]()
104 },
105 // The strip's item menu names a slide that may not be the current one.
106 strip (name, id) {
107 if (name === 'add-after') newSlide(id)
108 else if (name === 'duplicate') duplicateSlide(id)
109 else if (name === 'move-up' || name === 'move-down') moveSlide(id, name.slice(5))
110 else if (name === 'delete') askDelete(id)
111 },
112 moveSlide
113 }
114}
115