Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 18 additions & 52 deletions packages/super-editor/src/components/ProseMirror.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
<script setup>
import { ref, onMounted } from 'vue';
import { EditorState } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
import { history } from "prosemirror-history"

import { DocxSchema } from '@core/schema/DocxSchema';
import { initComments } from '@extensions/comments/comments';
import { buildKeymap } from '@core/shortcuts/buildKeymap';
import SuperConverter from '@core/SuperConverter';
import { Editor } from '@core/Editor';

// The editor needs to be agnostic to the data source
// It should be able to work with any DOCX, plain text, and HTML.
Expand All @@ -23,6 +16,7 @@ import SuperConverter from '@core/SuperConverter';
// We will need all relevant data passed in. This includes multiple files from the docx zip.

const emit = defineEmits(['comments-loaded']);

const props = defineProps({
mode: {
type: String,
Expand All @@ -36,66 +30,38 @@ const props = defineProps({

data: {
type: Object,
}
},
});

const editor = ref(null);
let editorView, editorState;
const loadedComments = ref([]);
const converter = new SuperConverter({ docx: props.data, debug: true });

// Document data
const documentData = ref(null);
const initData = () => {
documentData.value = converter.getSchema();
console.debug('\nSCHEMA', JSON.stringify(documentData.value, null, 2), '\n')
}

// Editor initialization
const handleTransaction = (transaction) => {
console.debug('Transaction', transaction)
const state = editorView.state.apply(transaction);
editorView.updateState(state);
}
const editorElem = ref(null);

const getEditorData = () => {
if (documentData.value) return DocxSchema.nodeFromJSON(documentData.value)
return DocxSchema.topNodeType.createAndFill()
}
const onCommentsLoaded = ({ comments }) => {
console.log({ comments });
// Let super doc know we have comments
emit('comments-loaded', comments);
};

const initEditor = () => {
// Initialize the document state, either from xmlDocument.value or blank
editorState = EditorState.create({
doc: getEditorData(),
plugins: [
history(),
buildKeymap(),
],
const editor = new Editor({
element: editorElem.value,
content: props.data,
documentId: props.documentId,
onCommentsLoaded,
});

// Initialize the editor
editorView = new EditorView(editor.value, {
state: editorState,
dispatchTransaction: handleTransaction,
editor.on('transaction', ({ transaction }) => {
console.log({ transaction });
});

const comments = initComments(editorView, converter, props.documentId);
loadedComments.value = comments;

// Let super doc know we have comments
emit('comments-loaded', loadedComments.value);
};

onMounted(() => {
if (props.data) initData();
initEditor();
})

});
</script>

<template>
<div class="super-editor" v-if="props.data">
<div ref="editor" class="editor"></div>
<div ref="editorElem" class="editor"></div>
</div>
</template>

Expand Down
127 changes: 127 additions & 0 deletions packages/super-editor/src/core/Editor.js
Original file line number Diff line number Diff line change
@@ -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 });
}
}
32 changes: 32 additions & 0 deletions packages/super-editor/src/core/EventEmitter.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
8 changes: 3 additions & 5 deletions packages/super-editor/src/core/SuperConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -425,5 +425,3 @@ class SuperConverter {
return tags;
}
}

export default SuperConverter;
44 changes: 22 additions & 22 deletions packages/super-editor/src/core/schema/DocxSchema.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
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
*/
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: {
Expand Down Expand Up @@ -102,7 +102,7 @@ const DocxSchema = new Schema({
},

body: {
content: "(paragraph+ | unorderedList*)",
content: "(paragraph+ | bulletList* | orderedList*)",
toDOM() { return ["body", 0]; },
attrs: {
attributes: { default: {} },
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/super-editor/src/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down