From 31d6ef6296555a74c667d7ae0a7f6454871ce9bf Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 6 Nov 2018 15:15:27 +0100 Subject: [PATCH] cleanup prosemirror example --- .../ProsemirrorBinding/ProsemirrorBinding.js | 3 - examples/prosemirror/PlaceholderPlugin.js | 73 ++++++++++++++++ examples/prosemirror/README.md | 21 +++++ examples/prosemirror/index.js | 87 ++----------------- .../{rollup.browser.js => rollup.config.js} | 0 5 files changed, 103 insertions(+), 81 deletions(-) create mode 100644 examples/prosemirror/PlaceholderPlugin.js create mode 100644 examples/prosemirror/README.md rename examples/prosemirror/{rollup.browser.js => rollup.config.js} (100%) diff --git a/bindings/ProsemirrorBinding/ProsemirrorBinding.js b/bindings/ProsemirrorBinding/ProsemirrorBinding.js index 12c85c0e..32473c8f 100644 --- a/bindings/ProsemirrorBinding/ProsemirrorBinding.js +++ b/bindings/ProsemirrorBinding/ProsemirrorBinding.js @@ -31,7 +31,6 @@ export default class ProsemirrorBinding { const updatedProps = { dispatchTransaction: function (tr) { // TODO: remove - const time = performance.now() const newState = prosemirror.state.apply(tr) mux(() => { updateYFragment(yDomFragment, newState, mapping) @@ -41,11 +40,9 @@ export default class ProsemirrorBinding { } else { prosemirror.updateState(newState) } - console.info('time for Yjs update: ', performance.now() - time) } } prosemirror.setProps(updatedProps) - yDomFragment.observeDeep(events => { if (events.length === 0) { return diff --git a/examples/prosemirror/PlaceholderPlugin.js b/examples/prosemirror/PlaceholderPlugin.js new file mode 100644 index 00000000..fe00368f --- /dev/null +++ b/examples/prosemirror/PlaceholderPlugin.js @@ -0,0 +1,73 @@ +/* eslint-env browser */ + +import {Plugin} from 'prosemirror-state' +import {Decoration, DecorationSet} from 'prosemirror-view' +import {schema} from 'prosemirror-schema-basic' + +const findPlaceholder = (state, id) => { + let decos = PlaceholderPlugin.getState(state) + let found = decos.find(null, null, spec => spec.id === id) + return found.length ? found[0].from : null +} + +export const startImageUpload = (view, file) => { + // A fresh object to act as the ID for this upload + let id = {} + + // Replace the selection with a placeholder + let tr = view.state.tr + if (!tr.selection.empty) tr.deleteSelection() + tr.setMeta(PlaceholderPlugin, {add: {id, pos: tr.selection.from}}) + view.dispatch(tr) + + uploadFile(file).then(url => { + let pos = findPlaceholder(view.state, id) + // If the content around the placeholder has been deleted, drop + // the image + if (pos == null) return + // Otherwise, insert it at the placeholder's position, and remove + // the placeholder + view.dispatch(view.state.tr + .replaceWith(pos, pos, schema.nodes.image.create({src: url})) + .setMeta(PlaceholderPlugin, {remove: {id}})) + }, () => { + // On failure, just clean up the placeholder + view.dispatch(tr.setMeta(PlaceholderPlugin, {remove: {id}})) + }) +} + +// This is just a dummy that loads the file and creates a data URL. +// You could swap it out with a function that does an actual upload +// and returns a regular URL for the uploaded file. +function uploadFile (file) { + let reader = new FileReader() + return new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result) + reader.onerror = () => reject(reader.error) + // Some extra delay to make the asynchronicity visible + setTimeout(() => reader.readAsDataURL(file), 1500) + }) +} + +export const PlaceholderPlugin = new Plugin({ + state: { + init () { return DecorationSet.empty }, + apply (tr, set) { + // Adjust decoration positions to changes made by the transaction + set = set.map(tr.mapping, tr.doc) + // See if the transaction adds or removes any placeholders + let action = tr.getMeta(this) + if (action && action.add) { + let widget = document.createElement('placeholder') + let deco = Decoration.widget(action.add.pos, widget, {id: action.add.id}) + set = set.add(tr.doc, [deco]) + } else if (action && action.remove) { + set = set.remove(set.find(null, null, spec => spec.id === action.remove.id)) + } + return set + } + }, + props: { + decorations (state) { return this.getState(state) } + } +}) diff --git a/examples/prosemirror/README.md b/examples/prosemirror/README.md new file mode 100644 index 00000000..a898b77d --- /dev/null +++ b/examples/prosemirror/README.md @@ -0,0 +1,21 @@ + +# Prosemirror Example + +### Run basic websockets server + +```sh +node /provider/websocket/server.js +``` + +### Bundle Prosemirror Example + +This example requires external modules and needs to be bundled before shipping it to the browser. + +```sh +cd /examples/prosemirror/ +# bundle prosemirror example +npx rollup -wc +# serve example +npx serve . +``` + diff --git a/examples/prosemirror/index.js b/examples/prosemirror/index.js index a24246ab..cfbd513b 100644 --- a/examples/prosemirror/index.js +++ b/examples/prosemirror/index.js @@ -8,88 +8,12 @@ import {EditorView} from 'prosemirror-view' import {Schema, DOMParser, Mark, Fragment, Node, Slice} from 'prosemirror-model' import {schema} from 'prosemirror-schema-basic' import {exampleSetup} from 'prosemirror-example-setup' -import {Plugin} from 'prosemirror-state' -import {Decoration, DecorationSet} from 'prosemirror-view' - -let placeholderPlugin = new Plugin({ - state: { - init() { return DecorationSet.empty }, - apply(tr, set) { - // Adjust decoration positions to changes made by the transaction - set = set.map(tr.mapping, tr.doc) - // See if the transaction adds or removes any placeholders - let action = tr.getMeta(this) - if (action && action.add) { - let widget = document.createElement("placeholder") - let deco = Decoration.widget(action.add.pos, widget, {id: action.add.id}) - set = set.add(tr.doc, [deco]) - } else if (action && action.remove) { - set = set.remove(set.find(null, null, - spec => spec.id == action.remove.id)) - } - return set - } - }, - props: { - decorations(state) { return this.getState(state) } - } -}) - -function findPlaceholder(state, id) { - let decos = placeholderPlugin.getState(state) - let found = decos.find(null, null, spec => spec.id == id) - return found.length ? found[0].from : null -} - -document.querySelector("#image-upload").addEventListener("change", e => { - if (view.state.selection.$from.parent.inlineContent && e.target.files.length) - startImageUpload(view, e.target.files[0]) - view.focus() -}) - -function startImageUpload(view, file) { - // A fresh object to act as the ID for this upload - let id = {} - - // Replace the selection with a placeholder - let tr = view.state.tr - if (!tr.selection.empty) tr.deleteSelection() - tr.setMeta(placeholderPlugin, {add: {id, pos: tr.selection.from}}) - view.dispatch(tr) - - uploadFile(file).then(url => { - let pos = findPlaceholder(view.state, id) - // If the content around the placeholder has been deleted, drop - // the image - if (pos == null) return - // Otherwise, insert it at the placeholder's position, and remove - // the placeholder - view.dispatch(view.state.tr - .replaceWith(pos, pos, schema.nodes.image.create({src: url})) - .setMeta(placeholderPlugin, {remove: {id}})) - }, () => { - // On failure, just clean up the placeholder - view.dispatch(tr.setMeta(placeholderPlugin, {remove: {id}})) - }) -} - -// This is just a dummy that loads the file and creates a data URL. -// You could swap it out with a function that does an actual upload -// and returns a regular URL for the uploaded file. -function uploadFile (file) { - let reader = new FileReader() - return new Promise((accept, fail) => { - reader.onload = () => accept(reader.result) - reader.onerror = () => fail(reader.error) - // Some extra delay to make the asynchronicity visible - setTimeout(() => reader.readAsDataURL(file), 1500) - }) -} +import { PlaceholderPlugin, startImageUpload } from './PlaceholderPlugin.js' const view = new EditorView(document.querySelector('#editor'), { state: EditorState.create({ doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')), - plugins: exampleSetup({schema}).concat(placeholderPlugin) + plugins: exampleSetup({schema}).concat(PlaceholderPlugin) }) }) @@ -110,3 +34,10 @@ window.Node = Node window.Schema = Schema window.Slice = Slice window.prosemirrorBinding = prosemirrorBinding + +document.querySelector('#image-upload').addEventListener('change', e => { + if (view.state.selection.$from.parent.inlineContent && e.target.files.length) { + startImageUpload(view, e.target.files[0]) + } + view.focus() +}) diff --git a/examples/prosemirror/rollup.browser.js b/examples/prosemirror/rollup.config.js similarity index 100% rename from examples/prosemirror/rollup.browser.js rename to examples/prosemirror/rollup.config.js