Path: blob/master/src/packages/frontend/editors/slate/search/hook.tsx
1698 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Support for global full text search of our slate.js document.7*/89import React from "react";10import { delay } from "awaiting";11import { Input } from "antd";12const { useCallback, useMemo, useRef, useState } = React;13import { Editor, Point, Range, Transforms } from "slate";14import {15nextMatch,16previousMatch,17SearchControlButtons,18} from "./search-control";19import { ReactEditor } from "../slate-react";20import { IS_MACOS, IS_TOUCH } from "@cocalc/frontend/feature";21import { createSearchDecorate } from "./decorate";22import { Replace } from "./replace";2324const modKey = IS_MACOS ? "⌘" : "ctrl";25const keyboardMessage = `Find Next (${modKey}-G) and Prev (Shift-${modKey}-G).`;2627const EXTRA_INFO_STYLE = {28position: "absolute",29opacity: 0.95,30marginTop: "2px",31zIndex: 1,32background: "white",33width: "100%",34color: "rgb(102,102,102)",35borderLeft: "1px solid lightgrey",36borderBottom: "1px solid lightgrey",37boxShadow: "-3px 5px 2px lightgrey",38} as React.CSSProperties;3940interface Options {41editor: Editor;42}4344export interface SearchHook {45decorate: ([node, path]) => { anchor: Point; focus: Point; search: true }[];46Search: React.JSX.Element;47search: string;48previous: () => void;49next: () => void;50focus: (search?: string) => void;51}5253export const useSearch: (Options) => SearchHook = (options) => {54const { editor } = options;55const [search, setSearch] = useState<string>("");56const inputRef = useRef<any>(null);5758const decorate = useMemo(() => {59return createSearchDecorate(search);60}, [search]);6162const cancel = useCallback(async () => {63setSearch("");64inputRef.current?.blur();65await delay(100); // todo: there must be a better way.66ReactEditor.focus(editor);67return;68}, []);6970const Search = useMemo(71() => (72<div73style={{74border: 0,75width: "100%",76position: "relative",77}}78>79<div style={{ display: "flex" }}>80<Input81ref={inputRef}82allowClear={true}83size="small"84placeholder="Search..."85value={search}86onChange={(e) => setSearch(e.target.value)}87style={{88border: 0,89flex: 1,90}}91onKeyDown={async (event) => {92if (event.metaKey || event.ctrlKey) {93if (event.key == "f") {94event.preventDefault();95return;96}97if (event.key == "g") {98event.preventDefault();99if (event.shiftKey) {100previousMatch(editor, decorate);101} else {102nextMatch(editor, decorate);103}104return;105}106}107if (event.key == "Enter") {108event.preventDefault();109inputRef.current?.blur();110await delay(100);111const { selection } = editor;112if (selection != null) {113const focus = Range.edges(selection)[0];114Transforms.setSelection(editor, { focus, anchor: focus });115}116nextMatch(editor, decorate);117return;118}119if (event.key == "Escape") {120event.preventDefault();121cancel();122return;123}124}}125/>126{search.trim() && (127<SearchControlButtons128editor={editor}129decorate={decorate}130disabled={!search.trim()}131/>132)}133</div>134{search.trim() && (135<div style={EXTRA_INFO_STYLE}>136<Replace137editor={editor}138decorate={decorate}139cancel={cancel}140search={search}141/>142{!IS_TOUCH && (143<div style={{ marginLeft: "7px" }}>{keyboardMessage}</div>144)}145</div>146)}147</div>148),149[search, decorate]150);151152return {153decorate,154Search,155search,156inputRef,157focus: async (search) => {158if (search?.trim()) {159setSearch(search);160await delay(0); // so that the "all" below selects this search.161}162inputRef.current?.focus({ cursor: "all" });163},164next: () => {165nextMatch(editor, decorate);166},167previous: () => {168previousMatch(editor, decorate);169},170};171};172173174