From a2bcf765a822acc92635a7e66bd0443ea5435bdd Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 8 Feb 2022 11:10:01 +0000 Subject: [PATCH] Split out codemirror JS to its own module Added a cache-compatible module loading system/pattern to the codebase. --- package.json | 4 +-- resources/js/{index.js => app.js} | 6 ++++ resources/js/{services/code.js => code.mjs} | 34 ++++++------------- resources/js/components/code-editor.js | 12 ++++--- resources/js/components/code-highlighter.js | 8 +++-- .../js/components/details-highlighter.js | 7 ++-- resources/js/components/markdown-editor.js | 34 ++++++++++++------- resources/js/components/page-display.js | 3 +- resources/js/wysiwyg/plugin-codeeditor.js | 17 +++++----- resources/sass/_tinymce.scss | 4 +++ 10 files changed, 73 insertions(+), 56 deletions(-) rename resources/js/{index.js => app.js} (77%) rename resources/js/{services/code.js => code.mjs} (93%) diff --git a/package.json b/package.json index 23fc902eb..b1899e232 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "build:css:dev": "sass ./resources/sass:./public/dist", "build:css:watch": "sass ./resources/sass:./public/dist --watch", "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: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", "production": "npm-run-all --parallel build:*:production", "dev": "npm-run-all --parallel watch livereload", diff --git a/resources/js/index.js b/resources/js/app.js similarity index 77% rename from resources/js/index.js rename to resources/js/app.js index ffdb54e19..82748b75e 100644 --- a/resources/js/index.js +++ b/resources/js/app.js @@ -6,6 +6,12 @@ window.baseUrl = function(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 import events from "./services/events" import httpInstance from "./services/http" diff --git a/resources/js/services/code.js b/resources/js/code.mjs similarity index 93% rename from resources/js/services/code.js rename to resources/js/code.mjs index d82db5271..3a7706573 100644 --- a/resources/js/services/code.js +++ b/resources/js/code.mjs @@ -98,7 +98,7 @@ const modeMap = { /** * Highlight pre elements on a page */ -function highlight() { +export function highlight() { const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre'); for (const codeBlock of codeBlocks) { highlightElem(codeBlock); @@ -109,7 +109,7 @@ function highlight() { * Highlight all code blocks within the given parent element * @param {HTMLElement} parent */ -function highlightWithin(parent) { +export function highlightWithin(parent) { const codeBlocks = parent.querySelectorAll('pre'); for (const codeBlock of codeBlocks) { highlightElem(codeBlock); @@ -207,7 +207,7 @@ function getTheme() { * @param {HTMLElement} elem * @returns {{wrap: Element, editor: *}} */ -function wysiwygView(elem) { +export function wysiwygView(elem) { const doc = elem.ownerDocument; const codeElem = elem.querySelector('code'); @@ -261,7 +261,7 @@ function getLanguageFromCssClasses(classes) { * @param {String} modeSuggestion * @returns {*} */ -function popupEditor(elem, modeSuggestion) { +export function popupEditor(elem, modeSuggestion) { const content = elem.textContent; return CodeMirror(function(elt) { @@ -281,7 +281,7 @@ function popupEditor(elem, modeSuggestion) { * @param cmInstance * @param modeSuggestion */ -function setMode(cmInstance, modeSuggestion, content) { +export function setMode(cmInstance, modeSuggestion, content) { cmInstance.setOption('mode', getMode(modeSuggestion, content)); } @@ -290,7 +290,7 @@ function setMode(cmInstance, modeSuggestion, content) { * @param cmInstance * @param codeContent */ -function setContent(cmInstance, codeContent) { +export function setContent(cmInstance, codeContent) { cmInstance.setValue(codeContent); setTimeout(() => { updateLayout(cmInstance); @@ -301,7 +301,7 @@ function setContent(cmInstance, codeContent) { * Update the layout (codemirror refresh) of a cm instance. * @param cmInstance */ -function updateLayout(cmInstance) { +export function updateLayout(cmInstance) { cmInstance.refresh(); } @@ -310,7 +310,7 @@ function updateLayout(cmInstance) { * @param {HTMLElement} elem * @returns {*} */ -function markdownEditor(elem) { +export function markdownEditor(elem) { const content = elem.textContent; const config = { 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} */ -function getMetaKey() { +export function getMetaKey() { let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; return mac ? "Cmd" : "Ctrl"; -} - -export default { - highlight: highlight, - highlightWithin: highlightWithin, - wysiwygView: wysiwygView, - popupEditor: popupEditor, - setMode: setMode, - setContent: setContent, - updateLayout: updateLayout, - markdownEditor: markdownEditor, - getMetaKey: getMetaKey, -}; +} \ No newline at end of file diff --git a/resources/js/components/code-editor.js b/resources/js/components/code-editor.js index 2e3506ec7..a8a2c0c6f 100644 --- a/resources/js/components/code-editor.js +++ b/resources/js/components/code-editor.js @@ -1,4 +1,3 @@ -import Code from "../services/code"; import {onChildEvent, onEnterPress, onSelect} from "../services/dom"; /** @@ -63,13 +62,17 @@ class CodeEditor { this.show(); 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) { this.editor = Code.popupEditor(this.editorInput, this.languageInput.value); } + this.loadHistory(); this.popup.components.popup.show(() => { Code.updateLayout(this.editor); @@ -84,7 +87,8 @@ class CodeEditor { this.addHistory(); } - updateEditorMode(language) { + async updateEditorMode(language) { + const Code = await window.importVersioned('code'); Code.setMode(this.editor, language, this.editor.getValue()); } diff --git a/resources/js/components/code-highlighter.js b/resources/js/components/code-highlighter.js index db6112887..5ffab3775 100644 --- a/resources/js/components/code-highlighter.js +++ b/resources/js/components/code-highlighter.js @@ -1,8 +1,12 @@ -import Code from "../services/code" class CodeHighlighter { constructor(elem) { - Code.highlightWithin(elem); + const codeBlocks = elem.querySelectorAll('pre'); + if (codeBlocks.length > 0) { + window.importVersioned('code').then(Code => { + Code.highlightWithin(elem); + }); + } } } diff --git a/resources/js/components/details-highlighter.js b/resources/js/components/details-highlighter.js index 18c5165fa..1f3b66c67 100644 --- a/resources/js/components/details-highlighter.js +++ b/resources/js/components/details-highlighter.js @@ -1,4 +1,3 @@ -import Code from "../services/code" class DetailsHighlighter { constructor(elem) { @@ -10,7 +9,11 @@ class DetailsHighlighter { onToggle() { 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; } } diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js index e41ab15f6..297d9c8ec 100644 --- a/resources/js/components/markdown-editor.js +++ b/resources/js/components/markdown-editor.js @@ -1,6 +1,5 @@ import MarkdownIt from "markdown-it"; import mdTasksLists from 'markdown-it-task-lists'; -import code from '../services/code'; import Clipboard from "../services/clipboard"; import {debounce} from "../services/util"; @@ -23,13 +22,20 @@ class MarkdownEditor { this.displayStylesLoaded = false; 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); const displayLoad = () => { this.displayDoc = this.display.contentDocument; - this.init(); + this.init(cmLoadPromise); }; if (this.display.contentDocument.readyState === 'complete') { @@ -45,7 +51,7 @@ class MarkdownEditor { }); } - init() { + init(cmLoadPromise) { let lastClick = 0; @@ -98,7 +104,15 @@ class MarkdownEditor { 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(); // Scroll to text if needed. @@ -107,11 +121,6 @@ class MarkdownEditor { if (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. @@ -158,15 +167,14 @@ class MarkdownEditor { topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'}); } - codeMirrorSetup() { - const cm = this.cm; + codeMirrorSetup(cm) { const context = this; // Text direction // 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. // Custom key commands - let metaKey = code.getMetaKey(); + let metaKey = this.Code.getMetaKey(); const extraKeys = {}; // Insert Image shortcut extraKeys[`${metaKey}-Alt-I`] = function(cm) { diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js index cc55fe35e..88254fd3a 100644 --- a/resources/js/components/page-display.js +++ b/resources/js/components/page-display.js @@ -1,5 +1,4 @@ import Clipboard from "clipboard/dist/clipboard.min"; -import Code from "../services/code"; import * as DOM from "../services/dom"; import {scrollAndHighlightElement} from "../services/util"; @@ -9,7 +8,7 @@ class PageDisplay { this.elem = elem; this.pageId = elem.getAttribute('page-display'); - Code.highlight(); + window.importVersioned('code').then(Code => Code.highlight()); this.setupPointer(); this.setupNavHighlighting(); this.setupDetailsCodeBlockRefresh(); diff --git a/resources/js/wysiwyg/plugin-codeeditor.js b/resources/js/wysiwyg/plugin-codeeditor.js index 92781f024..f0ec78afc 100644 --- a/resources/js/wysiwyg/plugin-codeeditor.js +++ b/resources/js/wysiwyg/plugin-codeeditor.js @@ -1,5 +1,3 @@ -import Code from "../services/code"; - function elemIsCodeBlock(elem) { return elem.className === 'CodeMirrorContainer'; } @@ -31,8 +29,10 @@ function showPopup(editor) { const editorElem = selectedNode.querySelector('.CodeMirror'); const cmInstance = editorElem.CodeMirror; if (cmInstance) { - Code.setContent(cmInstance, code); - Code.setMode(cmInstance, lang, code); + window.importVersioned('code').then(Code => { + Code.setContent(cmInstance, code); + Code.setMode(cmInstance, lang, code); + }); } const textArea = selectedNode.querySelector('textarea'); if (textArea) textArea.textContent = code; @@ -93,7 +93,7 @@ function register(editor, url) { showPopup(editor); }); - function parseCodeMirrorInstances() { + function parseCodeMirrorInstances(Code) { // Recover broken codemirror instances $('.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 // initial styles are fetched into the editor. editor.undoManager.transact(function () { - parseCodeMirrorInstances(); + parseCodeMirrorInstances(Code); }); // Parsed code mirror blocks when content is set but wait before setting this handler // to avoid any init 'SetContent' events. setTimeout(() => { editor.on('SetContent', () => { - setTimeout(parseCodeMirrorInstances, 100); + setTimeout(() => parseCodeMirrorInstances(Code), 100); }); }, 200); }); diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss index f98de06a0..642598496 100644 --- a/resources/sass/_tinymce.scss +++ b/resources/sass/_tinymce.scss @@ -32,6 +32,10 @@ justify-content: center; } +// Prevent scroll jumps on codemirror clicks +.page-content.mce-content-body .CodeMirror { + pointer-events: none; +} /** * Dark Mode Overrides