diff --git a/packages/super-editor/src/components/ProseMirror.vue b/packages/super-editor/src/components/ProseMirror.vue index eaa1a6c7e1..239472407b 100644 --- a/packages/super-editor/src/components/ProseMirror.vue +++ b/packages/super-editor/src/components/ProseMirror.vue @@ -1,13 +1,6 @@ diff --git a/packages/super-editor/src/core/Editor.js b/packages/super-editor/src/core/Editor.js new file mode 100644 index 0000000000..ed5a4031b7 --- /dev/null +++ b/packages/super-editor/src/core/Editor.js @@ -0,0 +1,127 @@ +import { + MarkType, + Node as ProseMirrorNode, + NodeType, + Schema, +} from 'prosemirror-model'; +import { + EditorState, Plugin, PluginKey, Transaction, +} from 'prosemirror-state'; +import { EditorView } from 'prosemirror-view'; +import { history } from "prosemirror-history"; + +import { buildKeymap } from '@core/shortcuts/buildKeymap'; +import { DocxSchema } from '@core/schema/DocxSchema'; +import { SuperConverter } from '@core/SuperConverter'; +import { EventEmitter } from './EventEmitter.js'; +import { initComments as initCommentsExt } from '@extensions/Comments/comments'; + +export class Editor extends EventEmitter { + schema; + + view; + + options = { + element: document.createElement('div'), + content: '', + editorProps: {}, + documentId: null, + onCommentsLoaded: () => null, + } + + constructor(options) { + super(); + + this.setOptions(options); + this.#createSchema(); + this.#createConverter(); + this.#createView(); + + this.on('comments-loaded', this.options.onCommentsLoaded); + this.initComments(); + } + + get state() { + return this.view.state; + } + + get isDestroyed() { + return !this.view?.docView; + } + + setOptions(options) { + this.options = { + ...this.options, + ...options, + } + + if (!this.view || !this.state || this.isDestroyed) { + return; + } + + if (this.options.editorProps) { + this.view.setProps(this.options.editorProps); + } + + this.view.updateState(this.state); + } + + #createConverter() { + this.converter = new SuperConverter({ + docx: this.options.content, + debug: true, + }); + } + + // TODO + #createSchema() { + this.schema = DocxSchema; + } + + #createView() { + let doc; + + try { + const docData = this.converter.getSchema(); + // console.debug('\nSCHEMA', JSON.stringify(docData, null, 2), '\n'); + if (docData) { + doc = this.schema.nodeFromJSON(docData); + } else { + doc = this.schema.topNodeType.createAndFill(); + } + } catch (err) { + console.error(err); + } + + this.view = new EditorView(this.options.element, { + ...this.options.editorProps, + dispatchTransaction: this.#dispatchTransaction.bind(this), + state: EditorState.create({ + doc, + plugins: [ // TODO + history(), + buildKeymap(), + ], + }), + }); + } + + #dispatchTransaction(transaction) { + const state = this.state.apply(transaction); + + this.view.updateState(state); + this.emit('transaction', { + editor: this, + transaction, + }); + } + + initComments() { + const comments = initCommentsExt( + this.view, + this.converter, + this.options.documentId, + ); + this.emit('comments-loaded', { comments }); + } +} diff --git a/packages/super-editor/src/core/EventEmitter.js b/packages/super-editor/src/core/EventEmitter.js new file mode 100644 index 0000000000..dfdd278cd7 --- /dev/null +++ b/packages/super-editor/src/core/EventEmitter.js @@ -0,0 +1,32 @@ +export class EventEmitter { + #callbacks = new Map(); + + on(event, fn) { + const callbacks = this.#callbacks.get(event); + if (callbacks) callbacks.push(fn); + else this.#callbacks.set(event, [fn]); + } + + emit(event, ...args) { + const callbacks = this.#callbacks.get(event); + if (!callbacks) return; + for (const fn of callbacks) { + fn(...args); + // fn.apply(this, args); + } + } + + off(event, fn) { + const callbacks = this.#callbacks.get(event); + if (!callbacks) return; + if (fn) { + this.#callbacks.set(event, callbacks.filter((cb) => cb !== fn)); + } else { + this.#callbacks.delete(event); + } + } + + removeAllListeners() { + this.#callbacks = new Map(); + } +} diff --git a/packages/super-editor/src/core/SuperConverter.js b/packages/super-editor/src/core/SuperConverter.js index cc2b162e03..c521995eb0 100644 --- a/packages/super-editor/src/core/SuperConverter.js +++ b/packages/super-editor/src/core/SuperConverter.js @@ -7,7 +7,7 @@ import xmljs from 'xml-js'; * Will need to be updated as we find new docx tags. * */ -class SuperConverter { +export class SuperConverter { static allowedElements = Object.freeze({ 'w:document': 'doc', @@ -197,9 +197,9 @@ class SuperConverter { const currentLevel = currentLevelDef.elements.find(style => style.name === 'w:numFmt') const listTypeDef = currentLevel.attributes['w:val']; let listType; - if (listTypeDef === 'bullet') listType = 'unorderedList'; + if (listTypeDef === 'bullet') listType = 'bulletList'; else if (listTypeDef === 'decimal') listType = 'orderedList'; - else if (listTypeDef === 'lowerLetter') listType = 'unorderedList'; + else if (listTypeDef === 'lowerLetter') listType = 'bulletList'; // Check for unknown list types, there will be some if (!listType) console.debug('_getNodeListType: No list type:', listTypeDef, attributes) @@ -425,5 +425,3 @@ class SuperConverter { return tags; } } - -export default SuperConverter; diff --git a/packages/super-editor/src/core/schema/DocxSchema.js b/packages/super-editor/src/core/schema/DocxSchema.js index 885740f8b5..208f59500a 100644 --- a/packages/super-editor/src/core/schema/DocxSchema.js +++ b/packages/super-editor/src/core/schema/DocxSchema.js @@ -1,5 +1,7 @@ import { Schema } from "prosemirror-model" +const olDOM = ["ol", 0], ulDOM = ["ul", 0], liDOM = ["li", 0]; + /** * Custom schema for docx files with prose mirror * Reference: https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.ts @@ -7,32 +9,30 @@ import { Schema } from "prosemirror-model" const DocxSchema = new Schema({ nodes: { - - /** - * ❗️ TODO: Implement a custom node view for run nodes that are children of list types - */ - unorderedList: { - content: "text*", - inline: false, - group: "block", - toDOM() { return ["ul", 0]; }, + bulletList: { + content: "listItem+", + group: "block list", parseDOM: [{ tag: "ul" }], - attrs: { - attributes: { default: {} }, - type: { default: null }, - }, + toDOM() { return ulDOM; }, }, orderedList: { - content: "text*", - inline: false, - group: "block", - toDOM() { return ["ol", 0]; }, - parseDOM: [{ tag: "ol" }], - attrs: { - attributes: { default: {} }, - type: { default: null }, + content: "listItem+", + group: "block list", + parseDOM: [{tag: "ol", getAttrs(dom) { + return {order: dom.hasAttribute("start") ? +dom.getAttribute("start") : 1}; + }}], + toDOM(node) { + return node.attrs.order === 1 ? olDOM : ["ol", {start: node.attrs.order}, 0]; }, + attrs: {order: {default: 1}}, + }, + + listItem: { + content: "paragraph block*", + parseDOM: [{tag: "li"}], + toDOM() { return liDOM }, + defining: true, }, commentRangeStart: { @@ -102,7 +102,7 @@ const DocxSchema = new Schema({ }, body: { - content: "(paragraph+ | unorderedList*)", + content: "(paragraph+ | bulletList* | orderedList*)", toDOM() { return ["body", 0]; }, attrs: { attributes: { default: {} }, diff --git a/packages/super-editor/src/core/tests/super-converter/input-tests.js b/packages/super-editor/src/core/tests/super-converter/input-tests.js index 9ee50aa15f..4ed39599b5 100644 --- a/packages/super-editor/src/core/tests/super-converter/input-tests.js +++ b/packages/super-editor/src/core/tests/super-converter/input-tests.js @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { readFileSync } from './helpers'; -import SuperConverter from '../../SuperConverter'; +import { SuperConverter } from '../../SuperConverter'; const showParserLogging = false; diff --git a/packages/super-editor/src/core/tests/super-converter/output-tests.js b/packages/super-editor/src/core/tests/super-converter/output-tests.js index 00f7f6c963..2014a071ac 100644 --- a/packages/super-editor/src/core/tests/super-converter/output-tests.js +++ b/packages/super-editor/src/core/tests/super-converter/output-tests.js @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { readFileSync } from './helpers'; -import SuperConverter from '../../SuperConverter'; +import { SuperConverter } from '../../SuperConverter'; import source1 from './output-tests-source'; const showParserLogging = false; diff --git a/packages/super-editor/src/index.js b/packages/super-editor/src/index.js index 948a6c62f3..fc133e10c2 100644 --- a/packages/super-editor/src/index.js +++ b/packages/super-editor/src/index.js @@ -1,4 +1,4 @@ -import SuperConverter from "@core/SuperConverter"; +import { SuperConverter } from "@core/SuperConverter"; import DocxZipper from '@core/DocxZipper'; import SuperEditor from '@components/SuperEditor.vue'; import BasicUpload from './dev/components/BasicUpload.vue';