// fixtures/commands.mjs — the editor's structural commands, one place // for the three surfaces that offer them: the Slide menu in the bar // (and its shortcuts), the inspector's buttons, and the strip's item // menu. Each is a batch of ops through commit() — the same vocabulary // the API takes — and nothing here touches the stage: the entry's // observers re-fetch what the ops changed once they are saved. // // ctx is read live (its fields are functions) because a resync // replaces the document, the stage and the undo manager under a // running editor. import { slideIDs, blockIDs } from './stage.mjs' import { newID } from './ids.mjs' import { moveSlideOp, moveBlockOp, zOps, deckOp } from './inspector-ops.mjs' import { resetPhone } from './switcher.mjs' /** * ctx: {ydoc(), commit(ops), flush(), slideId(), selected(), slideJSON(id), * blockJSON(slideId, id), undo(), redo(), present(), clearSelection(), * confirmEl} — returns {run(name), strip(name, slideId)}. */ export function createCommands (ctx) { // A new slide right after `after` (the current one by default), with // a heading to type into; then the stage moves to it once it exists // on the server. async function newSlide (after = ctx.slideId()) { const id = newID() const op = { op: 'add_slide', id, layout: 'flow', template: 'title-body' } if (after) op.after = after ctx.commit([op, { op: 'add_block', slide: id, id: newID(), type: 'heading', region: 'heading', props: { level: 1 }, markdown: '' }]) await ctx.flush() location.hash = '#' + id } async function duplicateSlide (from = ctx.slideId()) { if (!from) return const id = newID() ctx.commit([{ op: 'duplicate_slide', id: from, new_id: id, new_block_ids: blockIDs(ctx.ydoc(), from).map(() => newID()) }]) await ctx.flush() location.hash = '#' + id } function moveSlide (id, dir) { const op = moveSlideOp(slideIDs(ctx.ydoc()), id, dir) if (op) ctx.commit([op]) } // Delete asks first, in the Slide section's own callout, and // remembers which slide was asked about: the strip's menu can ask // about a slide that is not yet on the stage. const confirmEl = ctx.confirmEl let deleteTarget = '' function askDelete (id) { deleteTarget = id || ctx.slideId() if (!deleteTarget) return if (deleteTarget !== ctx.slideId()) location.hash = '#' + deleteTarget confirmEl.hidden = false confirmEl.querySelector('[data-command="slide-delete"]').focus() } function deleteSlide () { confirmEl.hidden = true const id = deleteTarget deleteTarget = '' if (id && slideIDs(ctx.ydoc()).includes(id)) ctx.commit([{ op: 'remove_slide', id }]) } // A block command runs on the selected block of the current slide. const withBlock = fn => () => { const slideId = ctx.slideId() const block = ctx.blockJSON(slideId, ctx.selected()) if (block) fn(slideId, block, ctx.slideJSON(slideId)) } const moveBlock = dir => withBlock((s, b) => { const op = moveBlockOp(s, blockIDs(ctx.ydoc(), s), b.id, dir) if (op) ctx.commit([op]) }) const layer = dir => withBlock((s, b, slide) => { const ops = zOps(s, slide.blocks, b.id, dir) if (ops.length) ctx.commit(ops) }) const commands = { 'new-slide': () => newSlide(), 'slide-add-after': () => newSlide(), 'duplicate-slide': () => duplicateSlide(), 'slide-move-up': () => moveSlide(ctx.slideId(), 'up'), 'slide-move-down': () => moveSlide(ctx.slideId(), 'down'), 'slide-delete-ask': () => askDelete(''), 'slide-delete': deleteSlide, 'slide-delete-cancel': () => { confirmEl.hidden = true; deleteTarget = '' }, 'accent-reset': () => ctx.commit([deckOp('accent-reset')]), 'block-move-up': moveBlock('up'), 'block-move-down': moveBlock('down'), 'block-forward': layer('forward'), 'block-back': layer('back'), 'block-delete': withBlock((s, b) => { ctx.clearSelection(); ctx.commit([{ op: 'remove_block', slide: s, id: b.id }]) }), 'phone-reset': withBlock((s, b) => ctx.commit([resetPhone(s, b)])), undo: () => ctx.undo(), redo: () => ctx.redo(), present: () => ctx.present() } return { has: name => name in commands, run (name) { if (name in commands) commands[name]() }, // The strip's item menu names a slide that may not be the current one. strip (name, id) { if (name === 'add-after') newSlide(id) else if (name === 'duplicate') duplicateSlide(id) else if (name === 'move-up' || name === 'move-down') moveSlide(id, name.slice(5)) else if (name === 'delete') askDelete(id) }, moveSlide } }