import { getAttributeOrThrow } from "../lib/attribute"; import LiveEditor from "./live_editor"; import Markdown from "./markdown"; import { globalPubSub } from "../lib/pub_sub"; import { smoothlyScrollToElement } from "../lib/utils"; import scrollIntoView from "scroll-into-view-if-needed"; /** * A hook managing a single cell. * * Mounts and manages the collaborative editor, * takes care of markdown rendering and focusing the editor when applicable. * * Configuration: * * * `data-cell-id` - id of the cell being edited * * `data-type` - editor type (i.e. language), either "markdown" or "elixir" is expected */ const Cell = { mounted() { this.props = getProps(this); this.state = { liveEditor: null, isFocused: false, insertMode: false, }; this.pushEvent("cell_init", { cell_id: this.props.cellId }, (payload) => { const { source, revision } = payload; const editorContainer = this.el.querySelector( `[data-element="editor-container"]` ); // Remove the content placeholder. editorContainer.firstElementChild.remove(); // Create an empty container for the editor to be mounted in. const editorElement = document.createElement("div"); editorContainer.appendChild(editorElement); // Setup the editor instance. this.state.liveEditor = new LiveEditor( this, editorElement, this.props.cellId, this.props.type, source, revision ); // Setup markdown rendering. if (this.props.type === "markdown") { const markdownContainer = this.el.querySelector( `[data-element="markdown-container"]` ); const baseUrl = this.props.sessionPath; const markdown = new Markdown(markdownContainer, source, baseUrl); this.state.liveEditor.onChange((newSource) => { markdown.setContent(newSource); }); } // Once the editor is created, reflect the current state. if (this.state.isFocused && this.state.insertMode) { this.state.liveEditor.focus(); // If the element is being scrolled to, focus interrupts it, // so ensure the scrolling continues. smoothlyScrollToElement(this.el); } this.state.liveEditor.onBlur(() => { // Prevent from blurring unless the state changes. // For example when we move cell using buttons // the editor should keep focus. if (this.state.isFocused && this.state.insertMode) { this.state.liveEditor.focus(); } }); }); this.handleSessionEvent = (event) => handleSessionEvent(this, event); globalPubSub.subscribe("session", this.handleSessionEvent); }, destroyed() { globalPubSub.unsubscribe("session", this.handleSessionEvent); if (this.state.liveEditor) { this.state.liveEditor.destroy(); } }, updated() { this.props = getProps(this); }, }; function getProps(hook) { return { cellId: getAttributeOrThrow(hook.el, "data-cell-id"), type: getAttributeOrThrow(hook.el, "data-type"), sessionPath: getAttributeOrThrow(hook.el, "data-session-path"), }; } /** * Handles client-side session event. */ function handleSessionEvent(hook, event) { if (event.type === "cell_focused") { handleCellFocused(hook, event.cellId); } else if (event.type === "insert_mode_changed") { handleInsertModeChanged(hook, event.enabled); } else if (event.type === "cell_moved") { handleCellMoved(hook, event.cellId); } else if (event.type === "cell_upload") { handleCellUpload(hook, event.cellId, event.url); } } function handleCellFocused(hook, cellId) { if (hook.props.cellId === cellId) { hook.state.isFocused = true; hook.el.setAttribute("data-js-focused", "true"); smoothlyScrollToElement(hook.el); } else if (hook.state.isFocused) { hook.state.isFocused = false; hook.el.removeAttribute("data-js-focused"); } } function handleInsertModeChanged(hook, insertMode) { if (hook.state.isFocused) { hook.state.insertMode = insertMode; if (hook.state.liveEditor) { if (hook.state.insertMode) { hook.state.liveEditor.focus(); // The insert mode may be enabled as a result of clicking the editor, // in which case we want to wait until editor handles the click // and sets new cursor position. // To achieve this, we simply put this task at the end of event loop, // ensuring all click handlers are executed first. setTimeout(() => { scrollIntoView(document.activeElement, { scrollMode: "if-needed", behavior: "smooth", block: "center", }); }, 0); } else { hook.state.liveEditor.blur(); } } } } function handleCellMoved(hook, cellId) { if (hook.state.isFocused && cellId === hook.props.cellId) { smoothlyScrollToElement(hook.el); } } function handleCellUpload(hook, cellId, url) { if (hook.props.cellId === cellId) { const markdown = `![](${url})`; hook.state.liveEditor.insert(markdown); } } export default Cell;