starboard-notebook
Version:
Starboard Notebook
281 lines • 12.1 kB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
/**
* NOTE: TODO:
* This file needs a complete refactor..
*/
import { html, LitElement, render } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { customElement, query } from "lit/decorators.js";
import { copyToClipboard } from "./helpers/clipboard";
import { trySetLocalStorage } from "./helpers/localStorage";
import Dropdown from "bootstrap/js/dist/dropdown";
import { getMarkdownItWithDefaultPlugins } from "./helpers/markdown";
const EDITOR_PREFERENCE_KEY = "starboard_notebook_text_editor_preference";
// Global state shared between all editors
// Note: somewhat problematic for garbage collection if no editor is ever chosen..
let notifyOnEditorChosen = [];
/**
* This promise is used to prevent two editor instances being loaded in the same frame.
* This helps keep things responsive when loading huge notebooks.
*/
let globalLoadEditorLockPromise = Promise.resolve();
let codeMirrorModule;
let monacoModule;
let currentEditor;
try {
// Use ternary condition to be robust to other invalid values
if (localStorage[EDITOR_PREFERENCE_KEY] !== undefined) {
currentEditor = localStorage[EDITOR_PREFERENCE_KEY] === "monaco" ? "monaco" : "codemirror";
}
}
catch (e) {
console.warn("Could not read editor preference (localStorage is probably not available)");
}
const mdloader = getMarkdownItWithDefaultPlugins();
const md = mdloader.md;
/**
* StarboardTextEditor abstracts over different text editors that are loaded dynamically.
* The user can choose: monaco for desktop devices, or a more minimal editor for mobile phones.
*
* TODO: this file needs a big cleanup..
*/
let StarboardTextEditor = class StarboardTextEditor extends LitElement {
constructor(cell, runtime, opts = {}) {
super();
this.opts = {};
this.runtime = runtime;
this.cell = cell;
this.opts = opts;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
}
handleDblClick() {
if (currentEditor === undefined) {
this.initEditor();
}
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
[].slice.call(document.querySelectorAll(".dropdown-toggle")).map((e) => new Dropdown(e));
if (currentEditor === "codemirror" || currentEditor === "monaco" || this.runtime.config.defaultTextEditor) {
this.initEditor();
// While it loads, render markdown
const mdText = md.render("```" + `${this.opts.language}\n${this.cell.textContent}\n` + "```");
render(html `<div class="cell-popover cell-select-editor-popover">
Loading ${currentEditor || this.runtime.config.defaultTextEditor} editor..
</div>
${unsafeHTML(mdText)}`, this.editorMountpoint);
}
else {
this.editorMountpoint.addEventListener("dblclick", () => this.handleDblClick(), { once: true, passive: true });
const mdText = md.render("```" + `${this.opts.language}\n${this.cell.textContent}\n` + "```");
render(html `
<div class="cell-popover cell-select-editor-popover">
<div style="display: flex; align-items: center;">
<b style="font-size: 1em; margin-right: 4px">Please select a text editor</b>
<button
=${() => this.switchToMonacoEditor()}
title="Monaco Editor (advanced, desktop only)"
class="cell-popover-icon-button"
>
Monaco
</button>
<button
=${() => this.switchToCodeMirrorEditor()}
title="CodeMirror Editor (simpler, touchscreen friendly)"
class="cell-popover-icon-button"
>
CodeMirror
</button>
</div>
<span style="font-size: 0.85em"
><b>Monaco</b> is more powerful, but is larger (4MB) and has poor touchscreen support.</span
>
</div>
${unsafeHTML(mdText)}
`, this.editorMountpoint);
notifyOnEditorChosen.push(() => this.initEditor());
}
}
async initEditor() {
// Note: this entire class really needs a refactor..
if (currentEditor === "codemirror") {
this.switchToCodeMirrorEditor();
}
else if (currentEditor === "monaco") {
this.switchToMonacoEditor();
}
else {
const newEditor = this.runtime.config.defaultTextEditor;
newEditor === "monaco" ? this.switchToMonacoEditor() : this.switchToCodeMirrorEditor();
}
}
switchToCodeMirrorEditor() {
if (currentEditor === "monaco" && this.editorInstance) {
this.editorInstance.dispose();
}
currentEditor = "codemirror";
trySetLocalStorage(EDITOR_PREFERENCE_KEY, "codemirror");
if (!codeMirrorModule) {
codeMirrorModule = import(/* webpackChunkName: "codemirrorEditor" */ "./editor/codemirror/editor");
document
.querySelectorAll(".cell-select-editor-popover")
.forEach((e) => (e.innerHTML = "<b>Loading CodeMirror editor..</b>"));
notifyOnEditorChosen.forEach((c) => c());
notifyOnEditorChosen = [];
}
codeMirrorModule.then((m) => {
globalLoadEditorLockPromise = globalLoadEditorLockPromise.then(() => {
return new Promise((resolve) => {
this.editorMountpoint.innerHTML = "";
this.editorInstance = m.createCodeMirrorEditor(this.editorMountpoint, this.cell, this.opts, this.runtime);
this.requestUpdate();
setTimeout(() => resolve(), 0);
});
});
});
}
switchToMonacoEditor() {
const shouldCleanUpCodeMirror = currentEditor === "codemirror" && this.editorInstance;
currentEditor = "monaco";
trySetLocalStorage(EDITOR_PREFERENCE_KEY, "monaco");
if (!monacoModule) {
monacoModule = import(/* webpackChunkName: "monaco" */ "./editor/monaco");
document
.querySelectorAll(".cell-select-editor-popover")
.forEach((e) => (e.innerHTML = "<b>Loading Monaco editor..</b>"));
notifyOnEditorChosen.forEach((c) => c());
notifyOnEditorChosen = [];
}
monacoModule.then((m) => {
globalLoadEditorLockPromise = globalLoadEditorLockPromise.then(() => {
return new Promise((resolve) => {
if (shouldCleanUpCodeMirror)
this.editorInstance.dom.remove();
this.editorMountpoint.innerHTML = "";
this.editorInstance = m.createMonacoEditor(this.editorMountpoint, this.cell, this.opts, this.runtime);
this.requestUpdate();
setTimeout(() => resolve(), 0);
});
});
});
}
copyCellText() {
copyToClipboard(this.cell.textContent);
const copyButton = this.querySelector("#copy-button");
if (copyButton) {
copyButton.innerText = "Copied!";
setTimeout(() => (copyButton.innerText = "Copy Text"), 2000);
}
}
render() {
return html `
<div style="position: relative; width: 100%; height: 0">
<div class="starboard-text-editor-controls">
<div class="dropdown">
<button
data-bs-toggle="dropdown"
class="btn btn-small transparent p-1 px-1 me-1"
style="color: #00000066"
title="Editor Actions"
>
<span class="bi bi-gear"></span>
</button>
<div class="dropdown-menu">
<li>
${currentEditor === "monaco"
? html `<button
class="dropdown-item"
=${() => this.switchToCodeMirrorEditor()}
title="Switch to CodeMirror based editor, simpler and smartphone friendly"
>
Switch to Simple Editor
</button>`
: html `<button
class="dropdown-item"
=${() => this.switchToMonacoEditor()}
title="Switch to Monaco based editor, a few MB in size, smartphone unfriendly"
>
Switch to Advanced Editor
</button>`}
</li>
<li>
<button
class="dropdown-item"
=${() => this.copyCellText()}
title="Copy the text in this cell to clipboard"
>
Copy Text
</button>
</li>
</div>
</div>
</div>
</div>
<div class="starboard-text-editor"></div>
`;
}
focus() {
if (this.editorInstance) {
this.editorInstance.focus();
}
}
setCaretPosition(position) {
var _a, _b, _c;
if (this.editorInstance) {
// Feels like a hack
if (currentEditor === "monaco") {
if (position === "start") {
this.editorInstance.setSelection({ startLineNumber: 0, endLineNumber: 0, startColumn: 0, endColumn: 0 });
}
else if (position === "end") {
const lastLine = (_a = this.editorInstance.getModel()) === null || _a === void 0 ? void 0 : _a.getLineCount();
if (lastLine !== undefined) {
const lastColumn = (_c = (_b = this.editorInstance.getModel()) === null || _b === void 0 ? void 0 : _b.getLineMaxColumn()) !== null && _c !== void 0 ? _c : 0;
this.editorInstance.setSelection({
startLineNumber: lastLine,
endLineNumber: lastLine,
startColumn: lastColumn,
endColumn: lastColumn,
});
}
}
}
else if (currentEditor === "codemirror") {
if (position === "start") {
this.editorInstance.dispatch({
selection: { anchor: 0 },
});
}
else if (position === "end") {
this.editorInstance.dispatch({
selection: { anchor: this.editorInstance.state.doc.length },
});
}
}
}
}
dispose() {
this.remove();
}
};
__decorate([
query(".starboard-text-editor")
], StarboardTextEditor.prototype, "editorMountpoint", void 0);
StarboardTextEditor = __decorate([
customElement("starboard-text-editor")
], StarboardTextEditor);
export { StarboardTextEditor };
//# sourceMappingURL=textEditor.js.map