Packages
mishka_chelekom
0.0.10-alpha.5
0.0.10-alpha.5
0.0.10-alpha.4
0.0.10-alpha.3
0.0.10-alpha.2
0.0.10-alpha.1
0.0.9
0.0.9-rc.2
0.0.9-rc.1
0.0.9-beta.5
0.0.9-beta.4
0.0.9-beta.3
0.0.9-beta.2
0.0.9-beta.1
0.0.9-alpha.20
0.0.9-alpha.18
0.0.9-alpha.17
0.0.9-alpha.16
0.0.9-alpha.15
0.0.9-alpha.14
0.0.9-alpha.13
0.0.9-alpha.12
0.0.9-alpha.11
0.0.9-alpha.10
0.0.9-alpha.9
0.0.9-alpha.8
0.0.9-alpha.7
0.0.9-alpha.6
0.0.9-alpha.5
0.0.9-alpha.4
0.0.9-alpha.3
0.0.9-alpha.2
0.0.9-alpha.1
0.0.8
0.0.8-rc.2
0.0.8-rc.1
0.0.8-beta.5
0.0.8-beta.4
0.0.8-beta.3
0.0.8-beta.2
0.0.8-beta.1
0.0.8-alpha.4
0.0.8-alpha.3
0.0.8-alpha.2
0.0.8-alpha.1
0.0.7
0.0.6
0.0.6-alpha.2
0.0.6-alpha.1
0.0.5
0.0.5-beta.2
0.0.5-beta.1
0.0.5-alpha.12
0.0.5-alpha.11
0.0.5-alpha.10
0.0.5-alpha.9
0.0.5-alpha.8
0.0.5-alpha.7
0.0.5-alpha.6
0.0.5-alpha.5
0.0.5-alpha.4
0.0.5-alpha.3
0.0.5-alpha.2
0.0.5-alpha.1
0.0.4
0.0.4-beta.3
0.0.4-beta.2
0.0.4-beta.1
0.0.4-alpha.9
0.0.4-alpha.8
0.0.4-alpha.7
0.0.4-alpha.6
0.0.4-alpha.5
0.0.4-alpha.4
0.0.4-alpha.3
0.0.4-alpha.2
0.0.4-alpha.1
0.0.3
0.0.3-alpha.3
0.0.3-alpha.2
0.0.3-alpha.1
0.0.2
0.0.2-rc.2
0.0.2-rc.1
0.0.2-beta.4
0.0.2-beta.3
0.0.2-beta.2
0.0.2-beta.1
0.0.2-alpha.3
0.0.2-alpha.2
0.0.2-alpha.1
0.0.1
Mishka Chelekom is a fully featured components and UI kit library for Phoenix & Phoenix LiveView
Current section
Files
Jump to
Current section
Files
priv/assets/js/editor_code_mirror.js
// Editor (CodeMirror 6) — code editing engine for the headless `editor` component.
//
// Same element contract and same hook name as every other editor engine, so the component's
// markup never changes when you switch `--lib`. See editor_tiptap.js for the full contract table.
//
// Format: this engine stores PLAIN TEXT (`format="text"`). There is no JSON/HTML document —
// `view.state.doc.toString()` is the whole value.
//
// Extensions are hand-picked rather than `basicSetup`, which is documented as non-customizable
// and additionally drags in the find/replace panel UI. The array below is basicSetup minus
// search, so `editor_extensions.js` can add to it and a consumer can drop a line.
import { EditorState, Compartment } from "@codemirror/state";
import {
EditorView,
keymap,
lineNumbers,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
drawSelection,
dropCursor,
rectangularSelection,
crosshairCursor,
} from "@codemirror/view";
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
import {
syntaxHighlighting,
defaultHighlightStyle,
indentOnInput,
bracketMatching,
foldGutter,
foldKeymap,
} from "@codemirror/language";
import { javascript } from "@codemirror/lang-javascript";
import userConfig from "./editor_extensions.js";
function toggle(el, attr, on) {
if (!el) return;
if (on) el.setAttribute(attr, "");
else el.removeAttribute(attr);
}
const Editor = {
mounted() {
const el = this.el;
this.rootId = el.getAttribute("data-root-id");
this.root = document.getElementById(this.rootId) || el.parentElement;
this.hidden = document.getElementById(el.getAttribute("data-value-id"));
this.debounceMs = parseInt(el.getAttribute("data-debounce"), 10) || 300;
this.onChange = el.getAttribute("data-on-change");
this.onFocus = el.getAttribute("data-on-focus");
this.onBlur = el.getAttribute("data-on-blur");
this.timer = null;
this.remote = false;
const config = userConfig || {};
const editable = el.getAttribute("data-editable") !== "false";
// Compartments let `editable` be reconfigured live. Rebuilding the view instead would lose
// the cursor, the selection and the undo history on every server patch.
this.editableComp = new Compartment();
this.readOnlyComp = new Compartment();
this.view = new EditorView({
doc: el.getAttribute("data-value") || "",
parent: el,
extensions: [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap, indentWithTab]),
javascript(),
this.editableComp.of(EditorView.editable.of(editable)),
this.readOnlyComp.of(EditorState.readOnly.of(!editable)),
EditorView.updateListener.of((update) => {
if (update.focusChanged) this.render();
if (!update.docChanged) return;
// Read from update.state, not this.view.state: during the callback the view's own
// state is mid-transaction.
this.schedule(update.state.doc.toString());
}),
...(config.extensions || []),
],
});
this.setRef = this.handleEvent("chelekom:editor", (payload) => {
if (!payload || (payload.id && payload.id !== this.rootId)) return;
this.applyRemote(payload.value ?? "");
});
this.bindSurfaceFocus();
this.mirrorPlaceholder();
this.render();
},
updated() {
if (!this.view) return;
const editable = this.el.getAttribute("data-editable") !== "false";
this.view.dispatch({
effects: [
this.editableComp.reconfigure(EditorView.editable.of(editable)),
this.readOnlyComp.reconfigure(EditorState.readOnly.of(!editable)),
],
});
this.render();
},
// The editable node is stretched by CSS, but the surface can still carry padding of its own, and
// every engine nests its node differently. A click there is a click on a non-editable box, so
// place the caret explicitly instead of making the user aim at the text.
bindSurfaceFocus() {
this._onSurfaceDown = (event) => {
if (event.target.closest(".ProseMirror, .cm-editor, [data-part=\"lexical-content\"]")) return;
this.view?.focus();
};
this.el.addEventListener("mousedown", this._onSurfaceDown);
},
// `attr()` can only read from the element the pseudo-element is on, so the placeholder text has
// to live on the editable node itself, wherever the engine put it.
mirrorPlaceholder() {
const placeholder = this.el.getAttribute("data-placeholder");
if (!placeholder) return;
const node = this.el.querySelector(".cm-content");
if (node) node.setAttribute("data-placeholder", placeholder);
},
destroyed() {
this.el.removeEventListener("mousedown", this._onSurfaceDown);
if (this.timer) clearTimeout(this.timer);
if (this.setRef) this.removeHandleEvent(this.setRef);
// Synchronous and complete: removes the DOM, unregisters handlers, notifies plugins.
// Note a dispatch after destroy() silently no-ops rather than throwing, so null the ref or a
// leaked callback fails invisibly.
this.view?.destroy();
this.view = null;
},
render() {
if (!this.view) return;
toggle(this.root, "data-empty", this.view.state.doc.length === 0);
toggle(this.root, "data-focused", this.view.hasFocus);
},
schedule(text) {
this.render();
if (this.remote) return;
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.commit(text), this.debounceMs);
},
commit(text) {
if (!this.view || !this.hidden) return;
const next = text ?? this.view.state.doc.toString();
if (this.hidden.value === next) return;
this.hidden.value = next;
this.hidden.dispatchEvent(new Event("input", { bubbles: true }));
this.push(this.onChange, next);
},
applyRemote(value) {
if (!this.view) return;
this.remote = true;
try {
this.view.dispatch({
changes: { from: 0, to: this.view.state.doc.length, insert: value },
});
if (this.hidden) this.hidden.value = value;
this.render();
} finally {
this.remote = false;
}
},
push(event, value) {
if (!event || !this.view) return;
this.pushEventTo(this.el, event, { value: value ?? this.view.state.doc.toString() });
},
};
export default Editor;