diff --git a/packages/apps/client/src/PrompterStyles.css b/packages/apps/client/src/PrompterStyles.css new file mode 100644 index 0000000..e0f788a --- /dev/null +++ b/packages/apps/client/src/PrompterStyles.css @@ -0,0 +1,48 @@ +.Prompter { + --background-color: #000; + --foreground-color: #fff; + --header-color: #999; + + color: var(--foreground-color); + background: var(--background-color); + + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; +} + +.Prompter p { + margin: 0; + padding: 0; +} + +.Prompter b { + font-weight: 700; +} + +.Prompter i { + font-style: italic; +} + +.Prompter u { + text-decoration: underline; +} + +.Prompter s { + text-decoration: none; + opacity: 0.3; +} + +.Prompter rev { + background-color: var(--foreground-color); + color: var(--background-color); +} + +.Prompter h1, +.Prompter h2, +.Prompter h3 { + background-color: var(--header-color); + color: var(--background-color); + user-select: none; + margin: 1em 0; +} diff --git a/packages/apps/client/src/ScriptEditor/Editor.tsx b/packages/apps/client/src/ScriptEditor/Editor.tsx index efda6b1..7cceaae 100644 --- a/packages/apps/client/src/ScriptEditor/Editor.tsx +++ b/packages/apps/client/src/ScriptEditor/Editor.tsx @@ -7,6 +7,10 @@ import { baseKeymap } from 'prosemirror-commands' import { schema } from './scriptSchema' import 'prosemirror-view/style/prosemirror.css' import { updateModel } from './plugins/updateModel' +import { readOnlyNodeFilter } from './plugins/readOnlyNodeFilter' +import { randomId } from '../lib/lib' +import { formatingKeymap } from './keymaps' +import { deselectAll } from './commands/deselectAll' export function Editor({ initialValue, @@ -22,12 +26,81 @@ export function Editor({ void initialValue + useEffect(() => { + function onKeyDown(ev: KeyboardEvent) { + console.log(ev) + if (ev.key === 'w' && ev.ctrlKey) { + ev.preventDefault() + } + } + + function onBeforeUnload(ev: BeforeUnloadEvent) { + ev.stopPropagation() + ev.preventDefault() + return false + } + + window.addEventListener('keydown', onKeyDown) + window.addEventListener('beforeunload', onBeforeUnload, { capture: true }) + + return () => { + window.removeEventListener('keydown', onKeyDown) + window.removeEventListener('beforeunload', onBeforeUnload, { capture: true }) + } + }, []) + useEffect(() => { if (!containerEl.current) return + const rundown = schema.node(schema.nodes.rundown, undefined, [ + schema.node(schema.nodes.rundownTitle, undefined, schema.text('Rundown Title')), + schema.node(schema.nodes.segment, undefined, [ + schema.node(schema.nodes.segmentTitle, undefined, schema.text('Segment Title')), + schema.node( + schema.nodes.part, + { + partId: randomId(), + }, + [ + schema.node(schema.nodes.partTitle, undefined, schema.text('Part title')), + schema.node(schema.nodes.paragraph, undefined, schema.text('Script...')), + ] + ), + schema.node( + schema.nodes.part, + { + partId: randomId(), + }, + [ + schema.node(schema.nodes.partTitle, undefined, schema.text('Part title')), + schema.node(schema.nodes.paragraph, undefined, schema.text('Script...')), + ] + ), + schema.node( + schema.nodes.part, + { + partId: randomId(), + }, + [ + schema.node(schema.nodes.partTitle, undefined, schema.text('Part title')), + schema.node(schema.nodes.paragraph, undefined, schema.text('Script...')), + ] + ), + ]), + ]) + const doc = schema.node(schema.nodes.doc, undefined, [rundown]) + const state = EditorState.create({ - schema, - plugins: [history(), keymap({ 'Mod-z': undo, 'Mod-y': redo }), keymap(baseKeymap), updateModel()], + plugins: [ + history(), + keymap({ 'Mod-z': undo, 'Mod-y': redo }), + keymap({ Escape: deselectAll }), + keymap(formatingKeymap), + keymap(baseKeymap), + readOnlyNodeFilter(), + updateModel(), + ], + doc, }) const view = new EditorView(containerEl.current, { state, @@ -41,7 +114,7 @@ export function Editor({ } }, []) - return
+ return
} type OnChangeEvent = { diff --git a/packages/apps/client/src/ScriptEditor/ScriptEditor.tsx b/packages/apps/client/src/ScriptEditor/ScriptEditor.tsx index 4e4bf06..5ed7553 100644 --- a/packages/apps/client/src/ScriptEditor/ScriptEditor.tsx +++ b/packages/apps/client/src/ScriptEditor/ScriptEditor.tsx @@ -1,10 +1,12 @@ import React from 'react' import { Editor } from './Editor' +import '../PrompterStyles.css' + export function ScriptEditor(): React.JSX.Element { return ( <> - + ) } diff --git a/packages/apps/client/src/ScriptEditor/commands/deselectAll.ts b/packages/apps/client/src/ScriptEditor/commands/deselectAll.ts new file mode 100644 index 0000000..690e72e --- /dev/null +++ b/packages/apps/client/src/ScriptEditor/commands/deselectAll.ts @@ -0,0 +1,9 @@ +import { Command, TextSelection } from 'prosemirror-state' + +export const deselectAll: Command = (state, dispatch): boolean => { + const newSelection = new TextSelection(state.selection.$to) + const transaction = state.tr.setSelection(newSelection) + if (dispatch) dispatch(transaction) + + return true +} diff --git a/packages/apps/client/src/ScriptEditor/keymaps.ts b/packages/apps/client/src/ScriptEditor/keymaps.ts new file mode 100644 index 0000000..e3daf5a --- /dev/null +++ b/packages/apps/client/src/ScriptEditor/keymaps.ts @@ -0,0 +1,11 @@ +import { Command } from 'prosemirror-state' +import { toggleMark } from 'prosemirror-commands' +import { schema } from './scriptSchema' + +export const formatingKeymap: Record = { + 'Mod-b': toggleMark(schema.marks.bold), + 'Mod-i': toggleMark(schema.marks.italic), + 'Mod-u': toggleMark(schema.marks.underline), + 'Mod-q': toggleMark(schema.marks.reverse), + 'Mod-F10': toggleMark(schema.marks.hidden), +} diff --git a/packages/apps/client/src/ScriptEditor/plugins/readOnlyNodeFilter.ts b/packages/apps/client/src/ScriptEditor/plugins/readOnlyNodeFilter.ts new file mode 100644 index 0000000..766dfea --- /dev/null +++ b/packages/apps/client/src/ScriptEditor/plugins/readOnlyNodeFilter.ts @@ -0,0 +1,22 @@ +import { Plugin } from 'prosemirror-state' + +export function readOnlyNodeFilter() { + return new Plugin({ + filterTransaction: (tr, state): boolean => { + if (!tr.docChanged) return true + + let editAllowed = true + for (const step of tr.steps) { + step.getMap().forEach((oldStart, oldEnd) => { + state.doc.nodesBetween(oldStart, oldEnd, (node, _index, parent) => { + if (node.type.spec.locked || parent?.type.spec.locked) { + editAllowed = false + } + }) + }) + } + + return editAllowed + }, + }) +} diff --git a/packages/apps/client/src/ScriptEditor/plugins/updateModel.ts b/packages/apps/client/src/ScriptEditor/plugins/updateModel.ts index 370cb91..e712b78 100644 --- a/packages/apps/client/src/ScriptEditor/plugins/updateModel.ts +++ b/packages/apps/client/src/ScriptEditor/plugins/updateModel.ts @@ -2,8 +2,8 @@ import { Plugin } from 'prosemirror-state' export function updateModel() { return new Plugin({ - appendTransaction: (_tr, _oldState, newState) => { - console.log(newState.doc.toJSON()) + appendTransaction: () => { + // console.log(newState) return null }, }) diff --git a/packages/apps/client/src/ScriptEditor/scriptSchema.ts b/packages/apps/client/src/ScriptEditor/scriptSchema.ts index ca51085..9762023 100644 --- a/packages/apps/client/src/ScriptEditor/scriptSchema.ts +++ b/packages/apps/client/src/ScriptEditor/scriptSchema.ts @@ -3,34 +3,112 @@ import { nodes } from 'prosemirror-schema-basic' export const schema = new Schema({ nodes: { - unmarkedText: { - inline: true, - marks: '', - }, text: nodes.text, + paragraph: nodes.paragraph, - segmentHeading: { - group: 'block', - content: 'unmarkedText', + + partTitle: { + group: 'title', + content: 'text*', atom: true, + marks: '', + isolating: true, + draggable: false, + selectable: false, + locked: true, + toDOM() { + return ['h2', { class: 'PartSlug', contenteditable: 'false' }, 0] + }, }, - partHeading: { + part: { group: 'block', - content: 'unmarkedText', + content: 'partTitle paragraph*', + partId: { + default: null, + }, + isolating: true, + draggable: false, + selectable: false, + toDOM() { + return ['div', { class: 'part' }, 0] + }, + }, + + segmentTitle: { + group: 'title', + content: 'text*', atom: true, + isolating: true, + draggable: false, + selectable: false, + locked: true, + marks: '', + toDOM() { + return ['h2', { class: 'SegmentTitle', contenteditable: 'false' }, 0] + }, }, - rundownHeading: { + segment: { group: 'block', - content: 'unmarkedText', + content: 'segmentTitle part*', + isolating: true, + draggable: false, + selectable: false, + toDOM() { + return ['div', { class: 'segment' }, 0] + }, + }, + + rundownTitle: { + group: 'title', + content: 'text*', atom: true, + isolating: true, + draggable: false, + selectable: false, + locked: true, + marks: '', + toDOM() { + return ['h1', { class: 'RundownTitle', contenteditable: 'false' }, 0] + }, }, - doc: { content: 'block*' }, + rundown: { + group: 'block', + content: 'rundownTitle segment*', + isolating: true, + draggable: false, + selectable: false, + toDOM() { + return ['div', { class: 'rundown' }, 0] + }, + }, + + doc: { content: 'rundown*' }, }, marks: { - bold: {}, - italic: {}, - underline: {}, - hidden: {}, - reverse: {}, + bold: { + toDOM() { + return ['b', 0] + }, + }, + italic: { + toDOM() { + return ['i', 0] + }, + }, + underline: { + toDOM() { + return ['u', 0] + }, + }, + hidden: { + toDOM() { + return ['s', 0] + }, + }, + reverse: { + toDOM() { + return ['rev', 0] + }, + }, }, })