livebook/assets/js/session/index.js

137 lines
4.3 KiB
JavaScript
Raw Normal View History

import { getAttributeOrThrow, parseBoolean } from "../lib/attribute";
import { isMacOS, isEditableElement } from "../lib/utils";
import KeyBuffer from "./key_buffer";
/**
* A hook managing the whole session.
*
* Registers event listeners to handle keybindings and focus events.
*
* Configuration:
*
* * `data-focused-cell-id` - id of the cell currently being focused
*/
const Session = {
mounted() {
this.props = getProps(this);
// Keybindings
// Note: make sure to keep the shortcuts help modal up to date.
const keyBuffer = new KeyBuffer();
this.handleDocumentKeydown = (event) => {
if (event.repeat) {
return;
}
const cmd = isMacOS() ? event.metaKey : event.ctrlKey;
const key = event.key;
if (this.props.insertMode) {
keyBuffer.reset();
if (key === "Escape") {
this.pushEvent("set_insert_mode", { enabled: false });
} else if (
this.props.focusedCellType === "elixir" &&
cmd &&
key === "Enter"
) {
cancelEvent(event);
this.pushEvent("queue_focused_cell_evaluation");
}
} else {
if (isEditableElement(event.target)) {
keyBuffer.reset();
return;
}
keyBuffer.push(event.key);
if (keyBuffer.tryMatch(["d", "d"])) {
this.pushEvent("delete_focused_cell", {});
} else if (
this.props.focusedCellType === "elixir" &&
keyBuffer.tryMatch(["e", "e"])
) {
this.pushEvent("queue_focused_cell_evaluation", {});
} else if (keyBuffer.tryMatch(["e", "s"])) {
this.pushEvent("queue_section_cells_evaluation", {});
} else if (keyBuffer.tryMatch(["e", "j"])) {
this.pushEvent("queue_child_cells_evaluation", {});
} else if (keyBuffer.tryMatch(["e", "x"])) {
this.pushEvent("cancel_focused_cell_evaluation", {});
} else if (keyBuffer.tryMatch(["?"])) {
this.pushEvent("show_shortcuts", {});
} else if (key === "i") {
this.pushEvent("set_insert_mode", { enabled: true });
} else if (key === "j") {
this.pushEvent("move_cell_focus", { offset: 1 });
} else if (key === "k") {
this.pushEvent("move_cell_focus", { offset: -1 });
} else if (key === "n") {
this.pushEvent("insert_cell_below_focused", { type: "elixir" });
} else if (key === "N") {
this.pushEvent("insert_cell_above_focused", { type: "elixir" });
} else if (key === "m") {
this.pushEvent("insert_cell_below_focused", { type: "markdown" });
} else if (key === "M") {
this.pushEvent("insert_cell_above_focused", { type: "markdown" });
}
}
};
document.addEventListener("keydown", this.handleDocumentKeydown, true);
// Focus/unfocus a cell when the user clicks somewhere
this.handleDocumentClick = (event) => {
// Find the parent with cell id info, if there is one
const cell = event.target.closest("[data-cell-id]");
const cellId = cell ? cell.dataset.cellId : null;
if (cellId !== this.props.focusedCellId) {
this.pushEvent("focus_cell", { cell_id: cellId });
}
// Depending if the click targets editor or not disable/enable insert mode.
if (cell) {
const editorContainer = cell.querySelector("[data-editor-container]");
const editorClicked = editorContainer.contains(event.target);
this.pushEvent("set_insert_mode", { enabled: editorClicked });
}
};
document.addEventListener("click", this.handleDocumentClick);
},
updated() {
this.props = getProps(this);
},
destroyed() {
document.removeEventListener("keydown", this.handleDocumentKeydown);
document.removeEventListener("click", this.handleDocumentClick);
},
};
function getProps(hook) {
return {
insertMode: getAttributeOrThrow(hook.el, "data-insert-mode", parseBoolean),
focusedCellId: getAttributeOrThrow(
hook.el,
"data-focused-cell-id",
(value) => value || null
),
focusedCellType: getAttributeOrThrow(hook.el, "data-focused-cell-type"),
};
}
function cancelEvent(event) {
// Cancel any default browser behavior.
event.preventDefault();
// Stop event propagation (e.g. so it doesn't reach the editor).
event.stopPropagation();
}
export default Session;