mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-08-09 10:22:51 +03:00
Editors: Added lexical editor for testing
Started basic playground for testing lexical as a new WYSIWYG editor. Moved out tinymce to be under wysiwyg-tinymce instead so lexical is the default, but TinyMce code remains.
This commit is contained in:
@@ -58,4 +58,5 @@ export {TriLayout} from './tri-layout';
|
||||
export {UserSelect} from './user-select';
|
||||
export {WebhookEvents} from './webhook-events';
|
||||
export {WysiwygEditor} from './wysiwyg-editor';
|
||||
export {WysiwygEditorTinymce} from './wysiwyg-editor-tinymce';
|
||||
export {WysiwygInput} from './wysiwyg-input';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import {Component} from './component';
|
||||
import {getLoading, htmlToDom} from '../services/dom';
|
||||
import {buildForInput} from '../wysiwyg/config';
|
||||
import {buildForInput} from '../wysiwyg-tinymce/config';
|
||||
|
||||
export class PageComment extends Component {
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import {Component} from './component';
|
||||
import {getLoading, htmlToDom} from '../services/dom';
|
||||
import {buildForInput} from '../wysiwyg/config';
|
||||
import {buildForInput} from '../wysiwyg-tinymce/config';
|
||||
|
||||
export class PageComments extends Component {
|
||||
|
||||
|
48
resources/js/components/wysiwyg-editor-tinymce.js
Normal file
48
resources/js/components/wysiwyg-editor-tinymce.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import {buildForEditor as buildEditorConfig} from '../wysiwyg-tinymce/config';
|
||||
import {Component} from './component';
|
||||
|
||||
export class WysiwygEditorTinymce extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
|
||||
this.tinyMceConfig = buildEditorConfig({
|
||||
language: this.$opts.language,
|
||||
containerElement: this.elem,
|
||||
darkMode: document.documentElement.classList.contains('dark-mode'),
|
||||
textDirection: this.$opts.textDirection,
|
||||
drawioUrl: this.getDrawIoUrl(),
|
||||
pageId: Number(this.$opts.pageId),
|
||||
translations: {
|
||||
imageUploadErrorText: this.$opts.imageUploadErrorText,
|
||||
serverUploadLimitText: this.$opts.serverUploadLimitText,
|
||||
},
|
||||
translationMap: window.editor_translations,
|
||||
});
|
||||
|
||||
window.$events.emitPublic(this.elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});
|
||||
window.tinymce.init(this.tinyMceConfig).then(editors => {
|
||||
this.editor = editors[0];
|
||||
});
|
||||
}
|
||||
|
||||
getDrawIoUrl() {
|
||||
const drawioUrlElem = document.querySelector('[drawio-url]');
|
||||
if (drawioUrlElem) {
|
||||
return drawioUrlElem.getAttribute('drawio-url');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of this editor.
|
||||
* Used by the parent page editor component.
|
||||
* @return {{html: String}}
|
||||
*/
|
||||
getContent() {
|
||||
return {
|
||||
html: this.editor.getContent(),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -1,28 +1,13 @@
|
||||
import {buildForEditor as buildEditorConfig} from '../wysiwyg/config';
|
||||
import {Component} from './component';
|
||||
|
||||
export class WysiwygEditor extends Component {
|
||||
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
this.editArea = this.$refs.editArea;
|
||||
|
||||
this.tinyMceConfig = buildEditorConfig({
|
||||
language: this.$opts.language,
|
||||
containerElement: this.elem,
|
||||
darkMode: document.documentElement.classList.contains('dark-mode'),
|
||||
textDirection: this.$opts.textDirection,
|
||||
drawioUrl: this.getDrawIoUrl(),
|
||||
pageId: Number(this.$opts.pageId),
|
||||
translations: {
|
||||
imageUploadErrorText: this.$opts.imageUploadErrorText,
|
||||
serverUploadLimitText: this.$opts.serverUploadLimitText,
|
||||
},
|
||||
translationMap: window.editor_translations,
|
||||
});
|
||||
|
||||
window.$events.emitPublic(this.elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});
|
||||
window.tinymce.init(this.tinyMceConfig).then(editors => {
|
||||
this.editor = editors[0];
|
||||
window.importVersioned('wysiwyg').then(wysiwyg => {
|
||||
wysiwyg.createPageEditorInstance(this.editArea);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import {Component} from './component';
|
||||
import {buildForInput} from '../wysiwyg/config';
|
||||
import {buildForInput} from '../wysiwyg-tinymce/config';
|
||||
|
||||
export class WysiwygInput extends Component {
|
||||
|
||||
|
109
resources/js/wysiwyg/index.mjs
Normal file
109
resources/js/wysiwyg/index.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
import {$getRoot, createEditor, ElementNode} from 'lexical';
|
||||
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
|
||||
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {$generateNodesFromDOM} from '@lexical/html';
|
||||
|
||||
class CalloutParagraph extends ElementNode {
|
||||
__category = 'info';
|
||||
|
||||
static getType() {
|
||||
return 'callout';
|
||||
}
|
||||
|
||||
static clone(node) {
|
||||
return new CalloutParagraph(node.__category, node.__key);
|
||||
}
|
||||
|
||||
constructor(category, key) {
|
||||
super(key);
|
||||
this.__category = category;
|
||||
}
|
||||
|
||||
createDOM(_config, _editor) {
|
||||
const dom = document.createElement('p');
|
||||
dom.classList.add('callout', this.__category || '');
|
||||
return dom;
|
||||
}
|
||||
|
||||
updateDOM(prevNode, dom) {
|
||||
// Returning false tells Lexical that this node does not need its
|
||||
// DOM element replacing with a new copy from createDOM.
|
||||
return false;
|
||||
}
|
||||
|
||||
static importDOM() {
|
||||
return {
|
||||
p: node => {
|
||||
if (node.classList.contains('callout')) {
|
||||
return {
|
||||
conversion: element => {
|
||||
let category = 'info';
|
||||
const categories = ['info', 'success', 'warning', 'danger'];
|
||||
|
||||
for (const c of categories) {
|
||||
if (element.classList.contains(c)) {
|
||||
category = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
node: new CalloutParagraph(category),
|
||||
};
|
||||
},
|
||||
priority: 3,
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exportJSON() {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'callout',
|
||||
version: 1,
|
||||
category: this.__category,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - Extract callout to own file
|
||||
// TODO - Add helper functions
|
||||
// https://lexical.dev/docs/concepts/nodes#creating-custom-nodes
|
||||
|
||||
export function createPageEditorInstance(editArea) {
|
||||
console.log('creating editor', editArea);
|
||||
|
||||
const config = {
|
||||
namespace: 'BookStackPageEditor',
|
||||
nodes: [HeadingNode, QuoteNode, CalloutParagraph],
|
||||
onError: console.error,
|
||||
};
|
||||
|
||||
const startingHtml = editArea.innerHTML;
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(startingHtml, 'text/html');
|
||||
|
||||
const editor = createEditor(config);
|
||||
editor.setRootElement(editArea);
|
||||
|
||||
mergeRegister(
|
||||
registerRichText(editor),
|
||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||
);
|
||||
|
||||
editor.update(() => {
|
||||
const startingNodes = $generateNodesFromDOM(editor, dom);
|
||||
const root = $getRoot();
|
||||
root.append(...startingNodes);
|
||||
});
|
||||
|
||||
const debugView = document.getElementById('lexical-debug');
|
||||
editor.registerUpdateListener(({editorState}) => {
|
||||
console.log('editorState', editorState.toJSON());
|
||||
debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user