1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-06-05 17:16:52 +03:00

Split out codemirror JS to its own module

Added a cache-compatible module loading system/pattern to the codebase.
This commit is contained in:
Dan Brown 2022-02-08 11:10:01 +00:00
parent 130dc05517
commit a2bcf765a8
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 73 additions and 56 deletions

View File

@ -4,9 +4,9 @@
"build:css:dev": "sass ./resources/sass:./public/dist", "build:css:dev": "sass ./resources/sass:./public/dist",
"build:css:watch": "sass ./resources/sass:./public/dist --watch", "build:css:watch": "sass ./resources/sass:./public/dist --watch",
"build:css:production": "sass ./resources/sass:./public/dist -s compressed", "build:css:production": "sass ./resources/sass:./public/dist -s compressed",
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main", "build:js:dev": "esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --format=esm",
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"", "build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main --minify", "build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/*.{js,mjs} --outdir=public/dist/ --sourcemap --target=es2020 --main-fields=module,main --minify --format=esm",
"build": "npm-run-all --parallel build:*:dev", "build": "npm-run-all --parallel build:*:dev",
"production": "npm-run-all --parallel build:*:production", "production": "npm-run-all --parallel build:*:production",
"dev": "npm-run-all --parallel watch livereload", "dev": "npm-run-all --parallel watch livereload",

View File

@ -6,6 +6,12 @@ window.baseUrl = function(path) {
return basePath + '/' + path; return basePath + '/' + path;
}; };
window.importVersioned = function(moduleName) {
const version = document.querySelector('link[href*="/dist/styles.css?version="]').href.split('?version=').pop();
const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`);
return import(importPath);
};
// Set events and http services on window // Set events and http services on window
import events from "./services/events" import events from "./services/events"
import httpInstance from "./services/http" import httpInstance from "./services/http"

View File

@ -98,7 +98,7 @@ const modeMap = {
/** /**
* Highlight pre elements on a page * Highlight pre elements on a page
*/ */
function highlight() { export function highlight() {
const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre'); const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
for (const codeBlock of codeBlocks) { for (const codeBlock of codeBlocks) {
highlightElem(codeBlock); highlightElem(codeBlock);
@ -109,7 +109,7 @@ function highlight() {
* Highlight all code blocks within the given parent element * Highlight all code blocks within the given parent element
* @param {HTMLElement} parent * @param {HTMLElement} parent
*/ */
function highlightWithin(parent) { export function highlightWithin(parent) {
const codeBlocks = parent.querySelectorAll('pre'); const codeBlocks = parent.querySelectorAll('pre');
for (const codeBlock of codeBlocks) { for (const codeBlock of codeBlocks) {
highlightElem(codeBlock); highlightElem(codeBlock);
@ -207,7 +207,7 @@ function getTheme() {
* @param {HTMLElement} elem * @param {HTMLElement} elem
* @returns {{wrap: Element, editor: *}} * @returns {{wrap: Element, editor: *}}
*/ */
function wysiwygView(elem) { export function wysiwygView(elem) {
const doc = elem.ownerDocument; const doc = elem.ownerDocument;
const codeElem = elem.querySelector('code'); const codeElem = elem.querySelector('code');
@ -261,7 +261,7 @@ function getLanguageFromCssClasses(classes) {
* @param {String} modeSuggestion * @param {String} modeSuggestion
* @returns {*} * @returns {*}
*/ */
function popupEditor(elem, modeSuggestion) { export function popupEditor(elem, modeSuggestion) {
const content = elem.textContent; const content = elem.textContent;
return CodeMirror(function(elt) { return CodeMirror(function(elt) {
@ -281,7 +281,7 @@ function popupEditor(elem, modeSuggestion) {
* @param cmInstance * @param cmInstance
* @param modeSuggestion * @param modeSuggestion
*/ */
function setMode(cmInstance, modeSuggestion, content) { export function setMode(cmInstance, modeSuggestion, content) {
cmInstance.setOption('mode', getMode(modeSuggestion, content)); cmInstance.setOption('mode', getMode(modeSuggestion, content));
} }
@ -290,7 +290,7 @@ function setMode(cmInstance, modeSuggestion, content) {
* @param cmInstance * @param cmInstance
* @param codeContent * @param codeContent
*/ */
function setContent(cmInstance, codeContent) { export function setContent(cmInstance, codeContent) {
cmInstance.setValue(codeContent); cmInstance.setValue(codeContent);
setTimeout(() => { setTimeout(() => {
updateLayout(cmInstance); updateLayout(cmInstance);
@ -301,7 +301,7 @@ function setContent(cmInstance, codeContent) {
* Update the layout (codemirror refresh) of a cm instance. * Update the layout (codemirror refresh) of a cm instance.
* @param cmInstance * @param cmInstance
*/ */
function updateLayout(cmInstance) { export function updateLayout(cmInstance) {
cmInstance.refresh(); cmInstance.refresh();
} }
@ -310,7 +310,7 @@ function updateLayout(cmInstance) {
* @param {HTMLElement} elem * @param {HTMLElement} elem
* @returns {*} * @returns {*}
*/ */
function markdownEditor(elem) { export function markdownEditor(elem) {
const content = elem.textContent; const content = elem.textContent;
const config = { const config = {
value: content, value: content,
@ -330,22 +330,10 @@ function markdownEditor(elem) {
} }
/** /**
* Get the 'meta' key dependant on the user's system. * Get the 'meta' key dependent on the user's system.
* @returns {string} * @returns {string}
*/ */
function getMetaKey() { export function getMetaKey() {
let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
return mac ? "Cmd" : "Ctrl"; return mac ? "Cmd" : "Ctrl";
} }
export default {
highlight: highlight,
highlightWithin: highlightWithin,
wysiwygView: wysiwygView,
popupEditor: popupEditor,
setMode: setMode,
setContent: setContent,
updateLayout: updateLayout,
markdownEditor: markdownEditor,
getMetaKey: getMetaKey,
};

View File

@ -1,4 +1,3 @@
import Code from "../services/code";
import {onChildEvent, onEnterPress, onSelect} from "../services/dom"; import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
/** /**
@ -63,13 +62,17 @@ class CodeEditor {
this.show(); this.show();
this.updateEditorMode(language); this.updateEditorMode(language);
Code.setContent(this.editor, code); window.importVersioned('code').then(Code => {
Code.setContent(this.editor, code);
});
} }
show() { async show() {
const Code = await window.importVersioned('code');
if (!this.editor) { if (!this.editor) {
this.editor = Code.popupEditor(this.editorInput, this.languageInput.value); this.editor = Code.popupEditor(this.editorInput, this.languageInput.value);
} }
this.loadHistory(); this.loadHistory();
this.popup.components.popup.show(() => { this.popup.components.popup.show(() => {
Code.updateLayout(this.editor); Code.updateLayout(this.editor);
@ -84,7 +87,8 @@ class CodeEditor {
this.addHistory(); this.addHistory();
} }
updateEditorMode(language) { async updateEditorMode(language) {
const Code = await window.importVersioned('code');
Code.setMode(this.editor, language, this.editor.getValue()); Code.setMode(this.editor, language, this.editor.getValue());
} }

View File

@ -1,8 +1,12 @@
import Code from "../services/code"
class CodeHighlighter { class CodeHighlighter {
constructor(elem) { constructor(elem) {
Code.highlightWithin(elem); const codeBlocks = elem.querySelectorAll('pre');
if (codeBlocks.length > 0) {
window.importVersioned('code').then(Code => {
Code.highlightWithin(elem);
});
}
} }
} }

View File

@ -1,4 +1,3 @@
import Code from "../services/code"
class DetailsHighlighter { class DetailsHighlighter {
constructor(elem) { constructor(elem) {
@ -10,7 +9,11 @@ class DetailsHighlighter {
onToggle() { onToggle() {
if (this.dealtWith) return; if (this.dealtWith) return;
Code.highlightWithin(this.elem); if (this.elem.querySelector('pre')) {
window.importVersioned('code').then(Code => {
Code.highlightWithin(this.elem);
});
}
this.dealtWith = true; this.dealtWith = true;
} }
} }

View File

@ -1,6 +1,5 @@
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
import mdTasksLists from 'markdown-it-task-lists'; import mdTasksLists from 'markdown-it-task-lists';
import code from '../services/code';
import Clipboard from "../services/clipboard"; import Clipboard from "../services/clipboard";
import {debounce} from "../services/util"; import {debounce} from "../services/util";
@ -23,13 +22,20 @@ class MarkdownEditor {
this.displayStylesLoaded = false; this.displayStylesLoaded = false;
this.input = this.elem.querySelector('textarea'); this.input = this.elem.querySelector('textarea');
this.cm = code.markdownEditor(this.input);
this.cm = null;
this.Code = null;
const cmLoadPromise = window.importVersioned('code').then(Code => {
this.cm = Code.markdownEditor(this.input);
this.Code = Code;
return this.cm;
});
this.onMarkdownScroll = this.onMarkdownScroll.bind(this); this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
const displayLoad = () => { const displayLoad = () => {
this.displayDoc = this.display.contentDocument; this.displayDoc = this.display.contentDocument;
this.init(); this.init(cmLoadPromise);
}; };
if (this.display.contentDocument.readyState === 'complete') { if (this.display.contentDocument.readyState === 'complete') {
@ -45,7 +51,7 @@ class MarkdownEditor {
}); });
} }
init() { init(cmLoadPromise) {
let lastClick = 0; let lastClick = 0;
@ -98,7 +104,15 @@ class MarkdownEditor {
toolbarLabel.closest('.markdown-editor-wrap').classList.add('active'); toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
}); });
this.codeMirrorSetup(); cmLoadPromise.then(cm => {
this.codeMirrorSetup(cm);
// Refresh CodeMirror on container resize
const resizeDebounced = debounce(() => this.Code.updateLayout(cm), 100, false);
const observer = new ResizeObserver(resizeDebounced);
observer.observe(this.elem);
});
this.listenForBookStackEditorEvents(); this.listenForBookStackEditorEvents();
// Scroll to text if needed. // Scroll to text if needed.
@ -107,11 +121,6 @@ class MarkdownEditor {
if (scrollText) { if (scrollText) {
this.scrollToText(scrollText); this.scrollToText(scrollText);
} }
// Refresh CodeMirror on container resize
const resizeDebounced = debounce(() => code.updateLayout(this.cm), 100, false);
const observer = new ResizeObserver(resizeDebounced);
observer.observe(this.elem);
} }
// Update the input content and render the display. // Update the input content and render the display.
@ -158,15 +167,14 @@ class MarkdownEditor {
topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'}); topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
} }
codeMirrorSetup() { codeMirrorSetup(cm) {
const cm = this.cm;
const context = this; const context = this;
// Text direction // Text direction
// cm.setOption('direction', this.textDirection); // cm.setOption('direction', this.textDirection);
cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor. cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
// Custom key commands // Custom key commands
let metaKey = code.getMetaKey(); let metaKey = this.Code.getMetaKey();
const extraKeys = {}; const extraKeys = {};
// Insert Image shortcut // Insert Image shortcut
extraKeys[`${metaKey}-Alt-I`] = function(cm) { extraKeys[`${metaKey}-Alt-I`] = function(cm) {

View File

@ -1,5 +1,4 @@
import Clipboard from "clipboard/dist/clipboard.min"; import Clipboard from "clipboard/dist/clipboard.min";
import Code from "../services/code";
import * as DOM from "../services/dom"; import * as DOM from "../services/dom";
import {scrollAndHighlightElement} from "../services/util"; import {scrollAndHighlightElement} from "../services/util";
@ -9,7 +8,7 @@ class PageDisplay {
this.elem = elem; this.elem = elem;
this.pageId = elem.getAttribute('page-display'); this.pageId = elem.getAttribute('page-display');
Code.highlight(); window.importVersioned('code').then(Code => Code.highlight());
this.setupPointer(); this.setupPointer();
this.setupNavHighlighting(); this.setupNavHighlighting();
this.setupDetailsCodeBlockRefresh(); this.setupDetailsCodeBlockRefresh();

View File

@ -1,5 +1,3 @@
import Code from "../services/code";
function elemIsCodeBlock(elem) { function elemIsCodeBlock(elem) {
return elem.className === 'CodeMirrorContainer'; return elem.className === 'CodeMirrorContainer';
} }
@ -31,8 +29,10 @@ function showPopup(editor) {
const editorElem = selectedNode.querySelector('.CodeMirror'); const editorElem = selectedNode.querySelector('.CodeMirror');
const cmInstance = editorElem.CodeMirror; const cmInstance = editorElem.CodeMirror;
if (cmInstance) { if (cmInstance) {
Code.setContent(cmInstance, code); window.importVersioned('code').then(Code => {
Code.setMode(cmInstance, lang, code); Code.setContent(cmInstance, code);
Code.setMode(cmInstance, lang, code);
});
} }
const textArea = selectedNode.querySelector('textarea'); const textArea = selectedNode.querySelector('textarea');
if (textArea) textArea.textContent = code; if (textArea) textArea.textContent = code;
@ -93,7 +93,7 @@ function register(editor, url) {
showPopup(editor); showPopup(editor);
}); });
function parseCodeMirrorInstances() { function parseCodeMirrorInstances(Code) {
// Recover broken codemirror instances // Recover broken codemirror instances
$('.CodeMirrorContainer').filter((index ,elem) => { $('.CodeMirrorContainer').filter((index ,elem) => {
@ -111,17 +111,18 @@ function register(editor, url) {
}); });
} }
editor.on('init', function() { editor.on('init', async function() {
const Code = await window.importVersioned('code');
// Parse code mirror instances on init, but delay a little so this runs after // Parse code mirror instances on init, but delay a little so this runs after
// initial styles are fetched into the editor. // initial styles are fetched into the editor.
editor.undoManager.transact(function () { editor.undoManager.transact(function () {
parseCodeMirrorInstances(); parseCodeMirrorInstances(Code);
}); });
// Parsed code mirror blocks when content is set but wait before setting this handler // Parsed code mirror blocks when content is set but wait before setting this handler
// to avoid any init 'SetContent' events. // to avoid any init 'SetContent' events.
setTimeout(() => { setTimeout(() => {
editor.on('SetContent', () => { editor.on('SetContent', () => {
setTimeout(parseCodeMirrorInstances, 100); setTimeout(() => parseCodeMirrorInstances(Code), 100);
}); });
}, 200); }, 200);
}); });

View File

@ -32,6 +32,10 @@
justify-content: center; justify-content: center;
} }
// Prevent scroll jumps on codemirror clicks
.page-content.mce-content-body .CodeMirror {
pointer-events: none;
}
/** /**
* Dark Mode Overrides * Dark Mode Overrides