diff --git a/app/src/search/util.ts b/app/src/search/util.ts
index 686388f77..9d92d60aa 100644
--- a/app/src/search/util.ts
+++ b/app/src/search/util.ts
@@ -3,8 +3,21 @@ import {getWndByLayout, resizeTabs} from "../layout/util";
import {Tab} from "../layout/Tab";
import {Search} from "./index";
import {Wnd} from "../layout/Wnd";
+import {Constants} from "../constants";
+import {escapeHtml} from "../util/escape";
+import {fetchPost} from "../util/fetch";
+import {openFileById} from "../editor/util";
+import {showMessage} from "../dialog/message";
+import {reloadProtyle} from "../protyle/util/reload";
+import {MenuItem} from "../menus/Menu";
+import {getDisplayName, getNotebookName, movePathTo} from "../util/pathName";
+import {Protyle} from "../protyle";
+import {disabledProtyle, onGet} from "../protyle/util/onGet";
+import {addLoading} from "../protyle/ui/initUI";
+import {getIconByType} from "../editor/getIcon";
+import {unicode2Emoji} from "../emoji";
-export const openGlobalSearch = (text: string, replace:boolean) => {
+export const openGlobalSearch = (text: string, replace: boolean) => {
text = text.trim();
let wnd: Wnd;
const searchModel = getAllModels().search.find((item, index) => {
@@ -35,3 +48,539 @@ export const openGlobalSearch = (text: string, replace:boolean) => {
});
wnd.split("lr").addTab(tab);
};
+
+export const genSearch = (config: ISearchOption, element: Element, closeCB?: () => void) => {
+ element.innerHTML = `
+
+
+
+
+
+
+ ${config.hPath}
+
+
+
+
+
+
+
+
+

`
+ const searchPanelElement = element.querySelector("#searchList");
+ const searchInputElement = element.querySelector("#searchInput") as HTMLInputElement;
+ const replaceInputElement = element.querySelector("#replaceInput") as HTMLInputElement;
+ const replaceHistoryElement = element.querySelector("#replaceHistoryList");
+ const historyElement = element.querySelector("#searchHistoryList");
+
+ const lineHeight = 30;
+ const edit = new Protyle(element.querySelector("#searchPreview") as HTMLElement, {
+ blockId: "",
+ render: {
+ gutter: true,
+ breadcrumbDocName: true
+ },
+ });
+ let clickTimeout: number;
+ let inputTimeout: number;
+
+ searchInputElement.value = config.k || "";
+ replaceInputElement.value = config.r || "";
+ searchInputElement.select();
+
+ element.addEventListener("click", (event: MouseEvent) => {
+ let target = event.target as HTMLElement
+ while (target && !target.isSameNode(element)) {
+ if (target.id === "searchPath") {
+ movePathTo([], undefined, (toPath) => {
+ config.idPath = toPath;
+ fetchPost("/api/filetree/getHPathsByPaths", {paths: [toPath]}, (response) => {
+ config.hPath = escapeHtml(response.data.join(", "));
+ element.querySelector("#searchPathInput").innerHTML = escapeHtml(response.data.join(", "));
+ });
+ inputTimeout = inputEvent(element, config, inputTimeout, edit);
+ });
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.id === "searchReplace") {
+ element.querySelector("#replaceHistoryBtn").parentElement.classList.toggle("fn__none");
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.id === "searchRefresh") {
+ inputTimeout = inputEvent(element, config, inputTimeout, edit)
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.id === "searchFilter") {
+ window.siyuan.menus.menu.remove();
+ addConfigMenu(config, window.siyuan.languages.math, "mathBlock", edit, element);
+ addConfigMenu(config, window.siyuan.languages.table, "table", edit, element);
+ addConfigMenu(config, window.siyuan.languages.quote, "blockquote", edit, element);
+ addConfigMenu(config, window.siyuan.languages.superBlock, "superBlock", edit, element);
+ addConfigMenu(config, window.siyuan.languages.paragraph, "paragraph", edit, element);
+ addConfigMenu(config, window.siyuan.languages.doc, "document", edit, element);
+ addConfigMenu(config, window.siyuan.languages.headings, "heading", edit, element);
+ addConfigMenu(config, window.siyuan.languages.list1, "list", edit, element);
+ addConfigMenu(config, window.siyuan.languages.listItem, "listItem", edit, element);
+ addConfigMenu(config, window.siyuan.languages.code, "codeBlock", edit, element);
+ addConfigMenu(config, "HTML", "htmlBlock", edit, element);
+ window.siyuan.menus.menu.popup({x: event.clientX - 16, y: event.clientY - 16}, true);
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.id === "includeChildCheck") {
+ target.classList.toggle("b3-button--cancel");
+ let reload = false;
+ if (target.classList.contains("b3-button--cancel")) {
+ if (!config.idPath.endsWith(".sy")) {
+ config.idPath = config.idPath + ".sy";
+ reload = true;
+ }
+ } else {
+ if (config.hPath) {
+ reload = true;
+ }
+ if (config.idPath.endsWith(".sy")) {
+ config.idPath = config.idPath.replace(".sy", "");
+ reload = true;
+ }
+ }
+ if (reload) {
+ localStorage.setItem(Constants.LOCAL_SEARCHEDATA, JSON.stringify(config));
+ inputTimeout = inputEvent(element, config, inputTimeout, edit);
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.id === "searchSyntaxCheck") {
+ target.classList.toggle("b3-button--cancel");
+ config.querySyntax = !target.classList.contains("b3-button--cancel");
+ inputTimeout = inputEvent(element, config, inputTimeout, edit);
+ localStorage.setItem(Constants.LOCAL_SEARCHEDATA, JSON.stringify(config));
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.id === "searchHistoryBtn") {
+ let html = "";
+ (config.list || []).forEach((s: string) => {
+ if (s !== searchInputElement.value) {
+ html += `
${escapeHtml(s)}
`;
+ }
+ });
+ historyElement.classList.remove("fn__none");
+ historyElement.innerHTML = html;
+ replaceHistoryElement.classList.add("fn__none");
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ } else if (target.id === "replaceHistoryBtn") {
+ let html = "";
+ (config.replaceList || []).forEach((s: string) => {
+ if (s !== replaceInputElement.value) {
+ html += `
${escapeHtml(s)}
`;
+ }
+ });
+ replaceHistoryElement.classList.remove("fn__none");
+ replaceHistoryElement.innerHTML = html;
+ historyElement.classList.add("fn__none");
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ } else if (target.id === "replaceAllBtn") {
+ replace(element, config, edit, true);
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.id === "replaceBtn") {
+ replace(element, config, edit, false);
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.classList.contains("b3-list-item")) {
+ if (target.parentElement.id === "searchHistoryList") {
+ searchInputElement.value = target.textContent;
+ searchInputElement.dispatchEvent(new CustomEvent("blur"));
+ inputTimeout = inputEvent(element, config, inputTimeout, edit);
+ } else if (target.parentElement.id === "replaceHistoryList") {
+ replaceInputElement.value = target.textContent;
+ replaceHistoryElement.classList.add("fn__none");
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ } else if (target.getAttribute("data-type") === "search-item") {
+ if (event.detail === 1) {
+ clickTimeout = window.setTimeout(() => {
+ if (window.siyuan.altIsPressed) {
+ const id = target.getAttribute("data-node-id");
+ fetchPost("/api/block/checkBlockFold", {id}, (foldResponse) => {
+ openFileById({
+ id,
+ action: foldResponse.data ? [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL] : [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT],
+ zoomIn: foldResponse.data,
+ position: "right"
+ });
+ if (closeCB) {
+ closeCB();
+ }
+ });
+ } else {
+ searchPanelElement.querySelector(".b3-list-item--focus").classList.remove("b3-list-item--focus");
+ target.classList.add("b3-list-item--focus");
+ getArticle({
+ edit,
+ id: target.getAttribute("data-node-id"),
+ k: getKey(target)
+ });
+ searchInputElement.focus();
+ }
+ }, Constants.TIMEOUT_DBLCLICK);
+ } else if (event.detail === 2) {
+ clearTimeout(clickTimeout);
+ const id = target.getAttribute("data-node-id");
+ fetchPost("/api/block/checkBlockFold", {id}, (foldResponse) => {
+ openFileById({
+ id,
+ action: foldResponse.data ? [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL] : [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT],
+ zoomIn: foldResponse.data
+ });
+ if (closeCB) {
+ closeCB();
+ }
+ });
+ }
+ window.siyuan.menus.menu.remove();
+ event.preventDefault();
+ event.stopPropagation();
+ break;
+ }
+ target = target.parentElement;
+ }
+ historyElement.classList.add("fn__none");
+ replaceHistoryElement.classList.add("fn__none");
+ }, false);
+
+ searchInputElement.addEventListener("compositionend", (event: InputEvent) => {
+ inputTimeout = inputEvent(element, config, inputTimeout, edit, event);
+ });
+ searchInputElement.addEventListener("input", (event: InputEvent) => {
+ inputTimeout = inputEvent(element, config, inputTimeout, edit, event);
+ });
+ searchInputElement.addEventListener("blur", () => {
+ let searches: string[] = config.list || [];
+ searches.splice(0, 0, searchInputElement.value);
+ searches = Array.from(new Set(searches));
+ if (searches.length > window.siyuan.config.search.limit) {
+ searches.splice(window.siyuan.config.search.limit, searches.length - window.siyuan.config.search.limit);
+ }
+ config.list = searches;
+ config.k = searchInputElement.value;
+ localStorage.setItem(Constants.LOCAL_SEARCHEDATA, JSON.stringify(config));
+ });
+ searchInputElement.addEventListener("focus", () => {
+ historyElement.classList.add("fn__none");
+ replaceHistoryElement.classList.add("fn__none");
+ });
+ searchInputElement.addEventListener("keydown", (event: KeyboardEvent) => {
+ let currentList: HTMLElement = searchPanelElement.querySelector(".b3-list-item--focus");
+ if (!currentList || event.isComposing) {
+ return;
+ }
+ if (event.key === "ArrowDown") {
+ currentList.classList.remove("b3-list-item--focus");
+ if (!currentList.nextElementSibling) {
+ searchPanelElement.children[0].classList.add("b3-list-item--focus");
+ } else {
+ currentList.nextElementSibling.classList.add("b3-list-item--focus");
+ }
+ currentList = searchPanelElement.querySelector(".b3-list-item--focus");
+ if (searchPanelElement.scrollTop < currentList.offsetTop - searchPanelElement.clientHeight + lineHeight ||
+ searchPanelElement.scrollTop > currentList.offsetTop) {
+ searchPanelElement.scrollTop = currentList.offsetTop - searchPanelElement.clientHeight + lineHeight;
+ }
+ getArticle({
+ id: currentList.getAttribute("data-node-id"),
+ k: getKey(currentList),
+ edit
+ });
+ event.preventDefault();
+ } else if (event.key === "ArrowUp") {
+ currentList.classList.remove("b3-list-item--focus");
+ if (!currentList.previousElementSibling) {
+ const length = searchPanelElement.children.length;
+ searchPanelElement.children[length - 1].classList.add("b3-list-item--focus");
+ } else {
+ currentList.previousElementSibling.classList.add("b3-list-item--focus");
+ }
+ currentList = searchPanelElement.querySelector(".b3-list-item--focus");
+ if (searchPanelElement.scrollTop < currentList.offsetTop - searchPanelElement.clientHeight + lineHeight ||
+ searchPanelElement.scrollTop > currentList.offsetTop - lineHeight * 2) {
+ searchPanelElement.scrollTop = currentList.offsetTop - lineHeight * 2;
+ }
+ getArticle({
+ id: currentList.getAttribute("data-node-id"),
+ k: getKey(currentList),
+ edit
+ });
+ event.preventDefault();
+ } else if (event.key === "Enter") {
+ const id = currentList.getAttribute("data-node-id");
+ fetchPost("/api/block/checkBlockFold", {id}, (foldResponse) => {
+ openFileById({
+ id,
+ action: foldResponse.data ? [Constants.CB_GET_FOCUS, Constants.CB_GET_ALL] : [Constants.CB_GET_FOCUS, Constants.CB_GET_CONTEXT],
+ zoomIn: foldResponse.data
+ });
+ if (closeCB) {
+ closeCB();
+ }
+ });
+ event.preventDefault();
+ }
+ });
+ replaceInputElement.addEventListener("focus", () => {
+ historyElement.classList.add("fn__none");
+ replaceHistoryElement.classList.add("fn__none");
+ });
+ replaceInputElement.addEventListener("keydown", (event: KeyboardEvent) => {
+ if (event.isComposing || event.key !== "Enter") {
+ return;
+ }
+ replace(element, config, edit, false);
+ event.preventDefault();
+ });
+ inputTimeout = inputEvent(element, config, inputTimeout, edit);
+}
+
+const addConfigMenu = (config: ISearchOption, lang: string, key: "mathBlock" | "table" | "blockquote" | "superBlock" | "paragraph" | "document" | "heading" | "list" | "listItem" | "codeBlock" | "htmlBlock",
+ edit: Protyle, element: Element) => {
+ window.siyuan.menus.menu.append(new MenuItem({
+ label: `
${lang}
+
`,
+ bind(menuItemElement) {
+ menuItemElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
+ const inputElement = menuItemElement.querySelector("input");
+ if (event.target.tagName !== "INPUT") {
+ inputElement.checked = !inputElement.checked;
+ }
+ config.types[key] = inputElement.checked;
+ inputEvent(element, config, undefined, edit);
+ localStorage.setItem(Constants.LOCAL_SEARCHEDATA, JSON.stringify(config));
+ window.siyuan.menus.menu.remove();
+ });
+ }
+ }).element);
+}
+
+const getKey = (element: HTMLElement) => {
+ const keys: string[] = [];
+ element.querySelectorAll("mark").forEach(item => {
+ keys.push(item.textContent);
+ });
+ return [...new Set(keys)].join(" ");
+};
+
+const getArticle = (options: {
+ id: string,
+ k: string,
+ edit: Protyle
+}) => {
+ console.log(options.edit);
+ fetchPost("/api/block/checkBlockFold", {id: options.id}, (foldResponse) => {
+ options.edit.protyle.scroll.lastScrollTop = 0;
+ addLoading(options.edit.protyle);
+ fetchPost("/api/filetree/getDoc", {
+ id: options.id,
+ k: options.k,
+ mode: foldResponse.data ? 0 : 3,
+ size: foldResponse.data ? Constants.SIZE_GET_MAX : window.siyuan.config.editor.dynamicLoadBlocks,
+ }, getResponse => {
+ onGet(getResponse, options.edit.protyle, foldResponse.data ? [Constants.CB_GET_ALL, Constants.CB_GET_HTML] : [Constants.CB_GET_HL, Constants.CB_GET_HTML]);
+ const matchElement = options.edit.protyle.wysiwyg.element.querySelector(`div[data-node-id="${options.id}"] span[data-type="search-mark"]`);
+ if (matchElement) {
+ matchElement.scrollIntoView();
+ }
+ });
+ });
+};
+
+const replace = (element: Element, config: ISearchOption, edit: Protyle, isAll: boolean) => {
+ const searchPanelElement = element.querySelector("#searchList");
+ const replaceInputElement = element.querySelector("#replaceInput") as HTMLInputElement;
+
+ const loadElement = replaceInputElement.nextElementSibling;
+ if (!loadElement.classList.contains("fn__none")) {
+ return;
+ }
+ let searches: string[] = config.replaceList || [];
+ searches.splice(0, 0, replaceInputElement.value);
+ searches = Array.from(new Set(searches));
+ if (searches.length > window.siyuan.config.search.limit) {
+ searches.splice(window.siyuan.config.search.limit, searches.length - window.siyuan.config.search.limit);
+ }
+ config.replaceList = searches;
+ config.r = replaceInputElement.value;
+ localStorage.setItem(Constants.LOCAL_SEARCHEDATA, JSON.stringify(config));
+
+ let currentList: HTMLElement = searchPanelElement.querySelector(".b3-list-item--focus");
+ if (!currentList) {
+ return;
+ }
+ loadElement.classList.remove("fn__none");
+ let ids: string[] = [];
+ let rootIds: string[] = [];
+ if (isAll) {
+ searchPanelElement.querySelectorAll(".b3-list-item").forEach(item => {
+ ids.push(item.getAttribute("data-node-id"));
+ rootIds.push(item.getAttribute("data-root-id"));
+ });
+ } else {
+ ids = [currentList.getAttribute("data-node-id")];
+ rootIds = [currentList.getAttribute("data-root-id")];
+ }
+ fetchPost("/api/search/findReplace", {
+ k: getKey(currentList),
+ r: replaceInputElement.value,
+ ids,
+ types: config.types,
+ }, (response) => {
+ loadElement.classList.add("fn__none");
+ if (response.code === 1) {
+ showMessage(response.msg);
+ return;
+ }
+ if (ids.length > 1) {
+ return;
+ }
+ getAllModels().editor.forEach(item => {
+ if (rootIds[0] === item.editor.protyle.block.rootID) {
+ reloadProtyle(item.editor.protyle);
+ }
+ });
+ if (!currentList.nextElementSibling && searchPanelElement.children[0]) {
+ searchPanelElement.children[0].classList.add("b3-list-item--focus");
+ } else {
+ currentList.nextElementSibling.classList.add("b3-list-item--focus");
+ }
+ currentList.remove();
+ if (searchPanelElement.childElementCount === 0) {
+ searchPanelElement.innerHTML = `
${window.siyuan.languages.emptyContent}
`;
+ edit.protyle.element.classList.add("fn__none");
+ return;
+ }
+ currentList = searchPanelElement.querySelector(".b3-list-item--focus");
+ if (searchPanelElement.scrollTop < currentList.offsetTop - searchPanelElement.clientHeight + 30 ||
+ searchPanelElement.scrollTop > currentList.offsetTop) {
+ searchPanelElement.scrollTop = currentList.offsetTop - searchPanelElement.clientHeight + 30;
+ }
+ getArticle({
+ edit,
+ id: currentList.getAttribute("data-node-id"),
+ k: getKey(currentList)
+ });
+ });
+};
+
+const inputEvent = (element: Element, config: ISearchOption, inputTimeout: number, edit: Protyle, event?: InputEvent) => {
+ if (event && event.isComposing) {
+ return;
+ }
+ const searchInputElement = element.querySelector("#searchInput") as HTMLInputElement;
+ const loadingElement = element.querySelector(".fn__loading--top");
+ clearTimeout(inputTimeout);
+ inputTimeout = window.setTimeout(() => {
+ loadingElement.classList.remove("fn__none");
+ const inputValue = searchInputElement.value;
+ if (inputValue === "") {
+ fetchPost("/api/block/getRecentUpdatedBlocks", {}, (response) => {
+ onSearch(response.data, edit, element);
+ loadingElement.classList.add("fn__none");
+ element.querySelector("#searchResult").innerHTML = "";
+ });
+ } else {
+ fetchPost("/api/search/fullTextSearchBlock", {
+ query: inputValue,
+ querySyntax: config.querySyntax,
+ types: config.types,
+ path: config.hPath ? config.idPath : ""
+ }, (response) => {
+ onSearch(response.data.blocks, edit, element);
+ element.querySelector("#searchResult").innerHTML = window.siyuan.languages.findInDoc.replace("${x}", response.data.matchedRootCount).replace("${y}", response.data.matchedBlockCount);
+ loadingElement.classList.add("fn__none");
+ });
+ }
+ }, Constants.TIMEOUT_SEARCH);
+ return inputTimeout;
+};
+
+const onSearch = (data: IBlock[], edit: Protyle, element: Element) => {
+ let resultHTML = "";
+ data.forEach((item, index) => {
+ const title = escapeHtml(getNotebookName(item.box)) + getDisplayName(item.hPath, false);
+ resultHTML += `
+
+${unicode2Emoji(item.ial.icon)}${item.ial.icon ? " " : ""}${item.content}
+${title}
+
`;
+ });
+
+ if (data[0]) {
+ if (edit) {
+ edit.protyle.element.classList.remove("fn__none");
+ } else {
+ element.querySelector("#searchPreview").classList.remove("fn__none");
+ }
+ const contentElement = document.createElement("div");
+ contentElement.innerHTML = data[0].content;
+ getArticle({
+ edit,
+ id: data[0].id,
+ k: getKey(contentElement),
+ });
+ } else {
+ if (edit) {
+ edit.protyle.element.classList.add("fn__none");
+ } else {
+ element.querySelector("#searchPreview").classList.add("fn__none");
+ }
+ }
+ element.querySelector("#searchList").innerHTML = resultHTML || `
${window.siyuan.languages.emptyContent}
`;
+};
diff --git a/app/src/types/index.d.ts b/app/src/types/index.d.ts
index 83595e55a..044bd7245 100644
--- a/app/src/types/index.d.ts
+++ b/app/src/types/index.d.ts
@@ -48,6 +48,31 @@ interface Window {
hideKeyboardToolbar(): void
}
+interface ISearchOption {
+ hasReplace: boolean,
+ querySyntax: boolean,
+ hPath: string
+ notebookId: string
+ idPath: string
+ k: string
+ r: string
+ replaceList?: string[]
+ list?: string[]
+ types: {
+ mathBlock: boolean
+ table: boolean
+ blockquote: boolean
+ superBlock: boolean
+ paragraph: boolean
+ document: boolean
+ heading: boolean
+ list: boolean
+ listItem: boolean
+ codeBlock: boolean
+ htmlBlock: boolean
+ }
+}
+
interface ITextOption {
color?: string,
type: string