Path: blob/master/src/packages/frontend/editors/slate/keyboard/arrow-keys.ts
1697 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6What happens when you hit arrow keys. This defines arrow key behavior for our7Slate editor, including moving the cursor up and down, scrolling the window,8moving to the beginning or end of the document, and handling cases where9selections are not in the DOM.10*/1112import { register } from "./register";13import {14blocksCursor,15moveCursorUp,16moveCursorDown,17moveCursorToBeginningOfBlock,18moveCursorToBeginningOfLine,19moveCursorToEndOfLine,20isAtBeginningOfBlock,21isAtEndOfBlock,22} from "../control";23import { SlateEditor } from "../types";24import { ReactEditor } from "../slate-react";25import { Transforms } from "slate";2627const down = ({ editor }: { editor: SlateEditor }) => {28const { selection } = editor;29setTimeout(() => {30// We have to do this via a timeout, because we don't control the cursor.31// Instead the selection in contenteditable changes via the browser and32// we react to that. Thus this is the only way with our current "sync with33// contenteditable approach". Here we just ensure that a move happens, rather34// than having the cursor be totally stuck, which is super annoying..35if (editor.selection === selection) {36Transforms.move(editor, { unit: "line" });37}38}, 1);3940const cur = editor.selection?.focus;4142if (43cur != null &&44editor.onCursorBottom != null &&45cur.path[0] >= editor.children.length - 1 &&46isAtEndOfBlock(editor, { mode: "highest" })47) {48editor.onCursorBottom();49}50const index = cur?.path[0];51if (52editor.windowedListRef.current != null &&53cur != null &&54index != null &&55cur.path[1] == editor.children[cur.path[0]]["children"]?.length - 156) {57// moving to the next block:58if (editor.scrollIntoDOM(index + 1)) {59// we did actually have to scroll the block below current one into the dom.60setTimeout(() => {61// did cursor move? -- if not, we manually move it.62if (cur == editor.selection?.focus) {63moveCursorDown(editor, true);64moveCursorToBeginningOfBlock(editor);65}66}, 0);67}68}69if (ReactEditor.selectionIsInDOM(editor)) {70// just work in the usual way71if (!blocksCursor(editor, false)) {72// built in cursor movement works fine73return false;74}75moveCursorDown(editor, true);76moveCursorToBeginningOfBlock(editor);77return true;78} else {79// in case of windowing when actual selection is not even80// in the DOM, it's much better to just scroll it into view81// and not move the cursor at all than to have it be all82// wrong (which is what happens with contenteditable and83// selection change). I absolutely don't know how to84// subsequently move the cursor down programatically in85// contenteditable, and it makes no sense to do so in slate86// since the semantics of moving down depend on the exact rendering.87return true;88}89};9091register({ key: "ArrowDown" }, down);9293const up = ({ editor }: { editor: SlateEditor }) => {94const { selection } = editor;95setTimeout(() => {96// We have to do this via a timeout, because we don't control the cursor.97// Instead the selection in contenteditable changes via the browser and98// we react to that. Thus this is the only way with our current "sync with99// contenteditable approach".100if (editor.selection === selection) {101Transforms.move(editor, { unit: "line", reverse: true });102}103}, 1);104105const cur = editor.selection?.focus;106if (107cur != null &&108editor.onCursorTop != null &&109cur?.path[0] == 0 &&110isAtBeginningOfBlock(editor, { mode: "highest" })111) {112editor.onCursorTop();113}114const index = cur?.path[0];115if (editor.windowedListRef.current != null && index && cur.path[1] == 0) {116if (editor.scrollIntoDOM(index - 1)) {117setTimeout(() => {118if (cur == editor.selection?.focus) {119moveCursorUp(editor, true);120moveCursorToBeginningOfBlock(editor);121}122}, 0);123}124}125if (ReactEditor.selectionIsInDOM(editor)) {126if (!blocksCursor(editor, true)) {127// built in cursor movement works fine128return false;129}130moveCursorUp(editor, true);131moveCursorToBeginningOfBlock(editor);132return true;133} else {134return true;135}136};137138register({ key: "ArrowUp" }, up);139140/*141The following functions are needed when using windowing, since142otherwise page up/page down get stuck when the rendered window143is at the edge. This is unavoidable, even if we were to144render a big overscan. If scrolling doesn't move, the code below145forces a manual move by one page.146147NOTE/TODO: none of the code below moves the *cursor*; it only148moves the scroll position on the page. In contrast, word,149google docs and codemirror all move the cursor when you page up/down,150so maybe that should be implemented...?151*/152153function pageWindowed(sign) {154return ({ editor }) => {155const scroller = editor.windowedListRef.current?.getScrollerRef();156if (scroller == null) return false;157const { scrollTop } = scroller;158159setTimeout(() => {160if (scrollTop == scroller.scrollTop) {161scroller.scrollTop += sign * scroller.getBoundingClientRect().height;162}163}, 0);164165return false;166};167}168169const pageUp = pageWindowed(-1);170register({ key: "PageUp" }, pageUp);171172const pageDown = pageWindowed(1);173register({ key: "PageDown" }, pageDown);174175function beginningOfDoc({ editor }) {176const scroller = editor.windowedListRef.current?.getScrollerRef();177if (scroller == null) return false;178scroller.scrollTop = 0;179return true;180}181function endOfDoc({ editor }) {182const scroller = editor.windowedListRef.current?.getScrollerRef();183if (scroller == null) return false;184scroller.scrollTop = 1e20; // basically infinity185// might have to do it again do to measuring size of rows...186setTimeout(() => {187scroller.scrollTop = 1e20;188}, 1);189return true;190}191register({ key: "ArrowUp", meta: true }, beginningOfDoc); // mac192register({ key: "Home", ctrl: true }, beginningOfDoc); // windows193register({ key: "ArrowDown", meta: true }, endOfDoc); // mac194register({ key: "End", ctrl: true }, endOfDoc); // windows195196function endOfLine({ editor }) {197const { selection } = editor;198setTimeout(() => {199// We have to do this via a timeout, because we don't control the cursor.200// Instead the selection in contenteditable changes via the browser and201// we react to that. Thus this is the only way with our current "sync with202// contenteditable approach".203if (editor.selection === selection) {204// stuck!205moveCursorToEndOfLine(editor);206}207}, 1);208return false;209}210211function beginningOfLine({ editor }) {212const { selection } = editor;213setTimeout(() => {214// We have to do this via a timeout, because we don't control the cursor.215// Instead the selection in contenteditable changes via the browser and216// we react to that. Thus this is the only way with our current "sync with217// contenteditable approach".218if (editor.selection === selection) {219// stuck!220moveCursorToBeginningOfLine(editor);221}222}, 1);223return false;224}225226register({ key: "ArrowRight", meta: true }, endOfLine);227register({ key: "ArrowRight", ctrl: true }, endOfLine);228register({ key: "ArrowLeft", meta: true }, beginningOfLine);229register({ key: "ArrowLeft", ctrl: true }, beginningOfLine);230231232