diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx
index db490e4115..4641237d4b 100644
--- a/apps/web/src/components/navigation-menu/index.tsx
+++ b/apps/web/src/components/navigation-menu/index.tsx
@@ -35,7 +35,8 @@ import {
Login,
Circle,
Icon,
- Reminders
+ Reminders,
+ Search
} from "../icons";
import { AnimatedFlex } from "../animated";
import NavigationItem from "./navigation-item";
@@ -95,7 +96,12 @@ const routes: Route[] = [
icon: Reminders,
tag: "Beta"
},
- { title: "Trash", path: "/trash", icon: Trash }
+ { title: "Trash", path: "/trash", icon: Trash },
+ {
+ title: "Search",
+ path: "/search",
+ icon: Search
+ }
];
const settings: Route = {
diff --git a/apps/web/src/components/route-container/index.js b/apps/web/src/components/route-container/index.js
index fa920da95a..205040cdc7 100644
--- a/apps/web/src/components/route-container/index.js
+++ b/apps/web/src/components/route-container/index.js
@@ -97,15 +97,6 @@ function Header(props) {
)}
- {buttons?.search && (
- navigate(`/search/${type}`)}
- />
- )}
-
{!isMobile && createButtonData && (
.
+*/
+import { useEffect, forwardRef } from "react";
+import { hexToRGB } from "../../utils/color";
+import DatePicker from "react-datepicker";
+import "react-datepicker/dist/react-datepicker.css";
+import "./datepicker.css";
+import { Text } from "@theme-ui/components";
+import { db } from "../../common/db";
+
+export function FilterInput(props) {
+ const {
+ filters,
+ focusInput,
+ index,
+ setFilters,
+ item,
+ getSuggestions,
+ setSuggestions,
+ onFocus,
+ onBlur
+ } = props;
+
+ useEffect(() => {
+ focusInput(filters.length - 1);
+ document.getElementById(`inputId_${index}`).innerText = item.input.query;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [filters.length]);
+
+ const setCalenderState = (state) => {
+ setFilters((filters) => {
+ let inputs = [...filters];
+ inputs[index].input.isCalenderOpen = state;
+ return inputs;
+ });
+ };
+
+ const setCalenderDate = () => {
+ document.getElementById(`inputId_${index}`).innerText =
+ filters[index].input.date.formatted;
+ };
+ return !item.input.isDateFilter ? (
+ {
+ await checkErrors(props, e.target.innerText);
+ setSuggestions(await getSuggestions(e.target.innerText, item.input));
+ onFocus(e);
+ }}
+ onBlur={onBlur}
+ onKeyDown={async (e) => {
+ await onKeyPress(e, props);
+ }}
+ />
+ ) : (
+ }
+ peekNextMonth
+ showMonthDropdown
+ showYearDropdown
+ dropdownMode="select"
+ id={`inputId_${index}`}
+ selected={filters[index].input.date.orignal}
+ onCalendarClose={() => {
+ setCalenderDate();
+ document.getElementById(`inputId_${index}`).focus();
+ }}
+ onCalendarOpen={() => {
+ setCalenderDate();
+ }}
+ onChange={(date) => {
+ let inputs = [...filters];
+ inputs[index].input.date.formatted = `${date.getDate()}/${
+ date.getMonth() + 1
+ }/${date.getFullYear()}`;
+ inputs[index].input.date.orignal = date;
+ setFilters(inputs);
+ }}
+ onBlur={(e) => {
+ setCalenderState(false);
+ onBlur(e);
+ }}
+ onFocus={async (e) => {
+ setCalenderState(true);
+ setSuggestions(await getSuggestions(e.target.innerText, item.input));
+ onFocus(e);
+ }}
+ onKeyDown={async (e) => {
+ (await onKeyPress(e, props))[e.key]();
+ }}
+ />
+ );
+}
+
+const CustomInput = forwardRef((props, refs) => (
+
+));
+CustomInput.displayName = "CustomInput";
+
+const checkErrors = async (props, query) => {
+ const { setFilters, index, item } = props;
+ query = query.trim();
+ let input = item.input;
+ let result = await (await db.search.filter(input.filterName, query)).result;
+ let error = true;
+ if (!input.hasSuggestions || result.length > 0) error = false;
+ setFilters((filters) => {
+ let _filters = [...filters];
+ _filters[index].input.error = error;
+ return _filters;
+ });
+};
+
+const deleteDefinition = (definitions, id) => {
+ let index = 0;
+ for (let definition of definitions) {
+ if (definition.srNo === id) {
+ definitions.splice(index, 1);
+ }
+ index++;
+ }
+};
+
+const deleteFilter = (advanceInputs, index) => {
+ advanceInputs.splice(index, 1);
+ return advanceInputs;
+};
+
+const onKeyPress = async (e, props) => {
+ const {
+ filters,
+ focusInput,
+ index,
+ setFilters,
+ item,
+ getSuggestions,
+ setSuggestions,
+ onSearch,
+ searchDefinitions,
+ setSelectionIndex,
+ suggestions,
+ moveSelection,
+ getCursorPosition
+ } = props;
+ setSuggestions(await getSuggestions(e.target.innerText, item.input));
+ await checkErrors(props, e.target.innerText);
+
+ switch (e.key) {
+ case "Enter": {
+ props.onKeyDown(e);
+ focusInput(index + 1);
+ let results = await db.search.filters(searchDefinitions);
+ onSearch(results);
+ setSuggestions([]);
+ e.preventDefault();
+ break;
+ }
+ case "Escape": {
+ setSuggestions([]);
+ break;
+ }
+ case "ArrowDown": {
+ moveSelection(suggestions, setSelectionIndex).Down();
+ e.preventDefault();
+ break;
+ }
+ case "ArrowUp": {
+ moveSelection(suggestions, setSelectionIndex).Up();
+ e.preventDefault();
+ break;
+ }
+ case "ArrowLeft": {
+ if (getCursorPosition(document.getElementById(e.target.id)) == 0) {
+ focusInput(index - 1);
+ e.preventDefault();
+ }
+ break;
+ }
+ case "ArrowRight": {
+ if (
+ getCursorPosition(document.getElementById(e.target.id)) ==
+ e.target.innerText.length
+ ) {
+ focusInput(index + 1);
+ e.preventDefault();
+ }
+ break;
+ }
+ case "Backspace": {
+ if (e.target.innerText === "") {
+ setSuggestions([]);
+ setFilters(deleteFilter(filters, index));
+ deleteDefinition(searchDefinitions, item.input.id);
+ focusInput(index - 1);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+};
diff --git a/apps/web/src/components/search/index.js b/apps/web/src/components/search/index.js
index a50f061acc..3ecd312a66 100644
--- a/apps/web/src/components/search/index.js
+++ b/apps/web/src/components/search/index.js
@@ -17,34 +17,476 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+import { useRef, useState, useEffect } from "react";
import * as Icon from "../icons";
import "./search.css";
-import Field from "../field";
+import { SuggestionRow } from "./suggestionrow";
+import { MainInput } from "./maininput";
+import { hexToRGB } from "../../utils/color";
+import { FilterInput } from "./filterinput";
+import { Button, Flex } from "@theme-ui/components";
+import { db } from "../../common/db";
+import dayjs from "dayjs";
+
+function SearchBox({ onSearch }) {
+ let filterSuggestions = getSuggestionArray(Object.keys(Filters));
+
+ const [suggestionsHover, setSuggestionsHover] = useState(false);
+ const [suggestions, setSuggestions] = useState(filterSuggestions);
+ const [selectionIndex, setSelectionIndex] = useState(-1);
+ const [filters, setFilters] = useState([]);
+ const [previousFocus, setPreviousFocus] = useState([]);
+
+ const focusInput = (index) => {
+ const cursorAtZero = getCursorPosition(document.activeElement) === 0;
+ if (filters[index]) {
+ document.getElementById(`inputId_${index}`).focus();
+ if (cursorAtZero) moveCursorToEnd(index);
+ } else {
+ document.getElementById("general_input").focus();
+ }
+ };
+
+ useEffect(() => {
+ if (suggestions.length < 1) {
+ setSelectionIndex(-1);
+ } else {
+ scrollRef.current.scrollTop =
+ (scrollRef.current.scrollHeight / suggestions.length) *
+ (selectionIndex - 1);
+ }
+ }, [suggestions, selectionIndex]);
+
+ const scrollRef = useRef();
+ const inputRef = useRef();
+ const definitions = useRef([]);
-function SearchBox(props) {
return (
- {
- if (e.key === "Enter") props.onSearch(e.target.value);
- }}
- action={{
- icon: Icon.Search,
- testId: "search-button",
- onClick: () => {
- const searchField = document.getElementById("search");
- if (searchField && searchField.value && searchField.value.length) {
- props.onSearch(searchField.value);
- }
- }
+
+ >
+ 0 ? "0px" : "default",
+ borderBottomRightRadius: suggestions.length > 0 ? "0px" : "default",
+ ":hover:not(:focus)": {
+ boxShadow: "0px 0px 0px 1.5px var(--primary) inset"
+ },
+ ":focus-within": {
+ boxShadow: "0px 0px 0px 1.5px var(--primary) inset"
+ },
+ "::-webkit-scrollbar": { display: "none" },
+ scrollbarWidth: "none",
+ msOverflowStyle: "none"
+ }}
+ >
+ {filters.map((item, index) => (
+
+
+ {
+ if (!suggestionsHover) {
+ setSuggestions([]);
+ }
+ }}
+ onFocus={(e) => {
+ inputRef.current = e;
+ setPreviousFocus([
+ previousFocus[1],
+ { id: e.target.id, index }
+ ]);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ addDefinition(
+ filters,
+ definitions.current,
+ e.target.innerText
+ );
+ if (selectionIndex > -1)
+ e.target.innerText = suggestions[selectionIndex].filter;
+ }
+ }}
+ open={filters[index].input.isCalenderOpen}
+ />
+
+ ))}
+
+ {
+ if (!suggestionsHover) {
+ setSuggestions([]);
+ }
+ }}
+ onFocus={(e) => {
+ inputRef.current = e;
+ setPreviousFocus([
+ previousFocus[1],
+ { id: e.target.id, undefined }
+ ]);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ if (selectionIndex > -1)
+ e.target.value =
+ suggestions[selectionIndex].main1 +
+ suggestions[selectionIndex].main2;
+ }
+ }}
+ />
+
+
+
+ {suggestions.length ? (
+ {
+ setSuggestionsHover(true);
+ }}
+ onMouseLeave={(e) => {
+ setSuggestionsHover(false);
+ }}
+ sx={{
+ //try to minimize these styles
+ flexDirection: "column",
+ alignItems: "flex-start",
+ position: "absolute",
+ left: 0,
+ top: "100%",
+ right: 0,
+ borderTop: "none",
+ borderRadius: "0px 0px 5px 5px",
+ boxShadow: "-1px 4px 10px 3px #00000022",
+ zIndex: 2,
+ height: 200,
+ overflowX: "hidden",
+ "::-webkit-scrollbar": { display: "none" },
+ scrollbarWidth: "none",
+ msOverflowStyle: "none",
+ ":focus": { outline: "none" }
+ }}
+ bg="background"
+ >
+ {suggestions.map((item, index) => (
+ {
+ if (item.filter) {
+ inputRef.current.target.innerText = item.filter;
+ } else {
+ inputRef.current.target.value = item.main1 + item.main2;
+ addFilter(item.main1 + item.main2, setFilters);
+ }
+ inputRef.current.target.focus();
+ setSuggestions([]);
+ }}
+ />
+ ))}
+
+ ) : null}
+
);
}
export default SearchBox;
+
+const moveCursorToEnd = (index) => {
+ var el = document.getElementById(`inputId_${index}`);
+ if (el.childNodes[0]) {
+ var range = document.createRange();
+ var sel = window.getSelection();
+ range.setStart(el?.childNodes[0], el.childNodes[0].textContent.length);
+ range.collapse(true);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+};
+
+const getCursorPosition = (editableDiv) => {
+ //it is a general method, it should be somehwre else
+ var caretPos = 0,
+ sel,
+ range;
+ if (window.getSelection) {
+ sel = window.getSelection();
+ if (sel.rangeCount) {
+ range = sel.getRangeAt(0);
+ if (range.commonAncestorContainer.parentNode == editableDiv) {
+ caretPos = range.endOffset;
+ }
+ }
+ } else if (document.selection && document.selection.createRange) {
+ range = document.selection.createRange();
+ if (range.parentElement() == editableDiv) {
+ var tempEl = document.createElement("span");
+ editableDiv.insertBefore(tempEl, editableDiv.firstChild);
+ var tempRange = range.duplicate();
+ tempRange.moveToElementText(tempEl);
+ tempRange.setEndPoint("EndToEnd", range);
+ caretPos = tempRange.text.length;
+ }
+ }
+ return caretPos;
+};
+
+const definitionTemplate = {
+ filterName: undefined,
+ query: "",
+ srNo: -1
+};
+
+const addDefinition = (filters, definitions, query) => {
+ for (let filter of filters) {
+ let input = filter.input;
+ if (!input.error) {
+ let isArrayEmpty = false;
+ for (let index = 0; index < definitions.length; index++) {
+ if (definitions[index].srNo === input.id) {
+ isArrayEmpty = true;
+ definitionTemplate.filterName = input.filterName;
+ definitionTemplate.query = query;
+ definitionTemplate.srNo = input.id;
+ definitions[index] = definitionTemplate; //make definition
+ }
+ }
+
+ if (!isArrayEmpty) {
+ definitionTemplate.filterName = input.filterName;
+ definitionTemplate.query = query;
+ definitionTemplate.srNo = input.id;
+ definitions.push(definitionTemplate); //definition should be made here not gotten from somewhere else
+ }
+ }
+ }
+};
+
+const addFilter = (searchQuery, setFilters) => {
+ const isFilter = new RegExp("[a-z]+:").test(searchQuery);
+ if (isFilter) {
+ Object.keys(Filters).map((item) => {
+ if (searchQuery.includes(item)) {
+ document.getElementById("general_input").value = "";
+ setFilters((_filters) => {
+ let filter = Filters[item](_filters.length);
+ filter.input.query = filterQuery(searchQuery);
+ console.log(filterQuery(searchQuery), filter);
+ return [..._filters, filter];
+ });
+ }
+ });
+ }
+};
+
+const filterQuery = (query) => {
+ let splitInput = query.split(":");
+ return splitInput[1];
+};
+
+const moveSelection = (suggestions, setSelectionIndex) => ({
+ Up: () => {
+ setSelectionIndex((i) => {
+ if (suggestions.length > 0) {
+ if (--i < 0) i = suggestions.length - 1;
+ return i;
+ } else {
+ return -1;
+ }
+ });
+ },
+ Down: () => {
+ setSelectionIndex((i) => {
+ if (suggestions.length > 0) {
+ if (++i >= suggestions.length) i = 0;
+ return i;
+ } else {
+ return -1;
+ }
+ });
+ }
+});
+
+const filterTemplate = (filterName, index = -1) => {
+ return {
+ label: { filterName: filterName },
+ input: {
+ filterName: filterName,
+ isDateFilter: false,
+ hasSuggestions: false,
+ date: { formatted: getCurrentDate() },
+ isCalenderOpen: true,
+ error: false,
+ query: "",
+ id: `inputId_${index}`
+ }
+ };
+};
+
+const Filters = {
+ notebooks: (index) => {
+ let filter = filterTemplate("notebooks", index);
+ filter.input.hasSuggestions = true;
+ return filter;
+ },
+ notes: (index) => {
+ return filterTemplate(" notes", index);
+ },
+ tags: (index) => {
+ let filter = filterTemplate("tags", index);
+ filter.input.hasSuggestions = true;
+ return filter;
+ },
+ topics: (index) => {
+ let filter = filterTemplate("topics", index);
+ filter.input.hasSuggestions = true;
+ return filter;
+ },
+ intitle: (index) => {
+ return filterTemplate("intitle", index);
+ },
+ before: (index) => {
+ let filter = filterTemplate("before", index);
+ filter.input.isDateFilter = true;
+ return filter;
+ },
+ during: (index) => {
+ let filter = filterTemplate("during", index);
+ filter.input.isDateFilter = true;
+ return filter;
+ },
+ after: (index) => {
+ let filter = filterTemplate("after", index);
+ filter.input.isDateFilter = true;
+ return filter;
+ }
+};
+
+const getSuggestions = async (query, filterInput) => {
+ let dummy = [];
+ if (filterInput) {
+ if (filterInput.hasSuggestions) {
+ let searchResults = await db.search.filter(filterInput.filterName, query);
+ let result = query === "" ? searchResults.data : searchResults.result;
+ result.map((item, index) => {
+ dummy[index] = item.title;
+ });
+ }
+ return getSuggestionArray(dummy, undefined, true);
+ } else {
+ return getSuggestionArray(Object.keys(Filters), query, false);
+ }
+};
+
+function getSuggestionArray(array, query = "", isFilter = false) {
+ return array.map((item) => {
+ return {
+ main1: !isFilter && item + ":",
+ main2: !isFilter && query,
+ filter: isFilter && item
+ };
+ });
+}
+
+function getCurrentDate() {
+ return dayjs().format("DD/MM/YYYY");
+}
diff --git a/apps/web/src/components/search/maininput.js b/apps/web/src/components/search/maininput.js
new file mode 100644
index 0000000000..717e1422e0
--- /dev/null
+++ b/apps/web/src/components/search/maininput.js
@@ -0,0 +1,105 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { Input } from "@theme-ui/components";
+
+export function MainInput(props) {
+ return (
+ {
+ props.refreshFilters(e.target.value, props.setFilters);
+ props.setSuggestions(await props.getSuggestions(e.target.value));
+ }}
+ onFocus={async (e) => {
+ //shift to index
+ props.setSuggestions(await props.getSuggestions(e.target.value));
+ props.onFocus(e);
+ }}
+ onKeyDown={async (e) => {
+ //keyActions(e);
+ props.onKeyDown(e);
+ await onKeyPress(e, props);
+ }}
+ />
+ );
+}
+
+const onKeyPress = async (e, props) => {
+ switch (e.key) {
+ case "Enter": {
+ props.onSearch(e.target.value);
+ props.setSuggestions([]);
+ props.refreshFilters(e.target.value, props.setFilters);
+ e.preventDefault();
+ break;
+ }
+ case "Escape": {
+ props.setSuggestions([]);
+ break;
+ }
+ case "ArrowDown": {
+ props.moveSelection(props.suggestions, props.setSelectionIndex).Down();
+ e.preventDefault();
+ break;
+ }
+ case "ArrowUp": {
+ props.moveSelection(props.suggestions, props.setSelectionIndex).Up();
+ e.preventDefault();
+ break;
+ }
+ case "ArrowLeft": {
+ if (e.target.selectionStart == 0) {
+ props.focusInput(props.filters.length - 1);
+ e.preventDefault();
+ }
+ break;
+ }
+ case "ArrowRight": {
+ if (e.target.selectionStart == e.target.value.length) {
+ props.focusInput(0);
+ e.preventDefault();
+ }
+ break;
+ }
+ case "Backspace": {
+ if (e.target.selectionStart == 0) {
+ props.focusInput(props.filters.length - 1);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+};
diff --git a/apps/web/src/components/search/suggestionrow.js b/apps/web/src/components/search/suggestionrow.js
new file mode 100644
index 0000000000..5dac0778fa
--- /dev/null
+++ b/apps/web/src/components/search/suggestionrow.js
@@ -0,0 +1,57 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { Button, Text } from "@theme-ui/components";
+
+export function SuggestionRow(props) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/navigation/routes.js b/apps/web/src/navigation/routes.js
index f6c4f2422a..664439348a 100644
--- a/apps/web/src/navigation/routes.js
+++ b/apps/web/src/navigation/routes.js
@@ -212,13 +212,13 @@ const routes = {
}
};
},
- "/search/:type": ({ type }) => ({
+ "/search": () => ({
type: "search",
title: "Search",
- component: ,
+ component: ,
buttons: {
back: {
- title: `Go back to ${type}`,
+ title: `Go back to test`,
action: () => window.history.back()
}
}
diff --git a/apps/web/src/views/search.js b/apps/web/src/views/search.js
index b54fee3c91..ec60d0f3c5 100644
--- a/apps/web/src/views/search.js
+++ b/apps/web/src/views/search.js
@@ -17,137 +17,74 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useState } from "react";
import ListContainer from "../components/list-container";
import SearchPlaceholder from "../components/placeholders/search-placeholder";
import { db } from "../common/db";
import SearchBox from "../components/search";
import ProgressBar from "../components/progress-bar";
import { useStore as useNoteStore } from "../stores/note-store";
-import { Flex, Text } from "@theme-ui/components";
-import { showToast } from "../utils/toast";
-import { store as notebookstore } from "../stores/notebook-store";
-import { hardNavigate } from "../navigation";
+import { Text, Button, Flex } from "@theme-ui/components";
-async function typeToItems(type, context) {
- switch (type) {
- case "notes": {
- await db.notes.init();
- if (!context) return ["notes", db.notes.all];
- const notes = context.notes;
- return ["notes", notes];
- }
- case "notebooks":
- return ["notebooks", db.notebooks.all];
- case "topics": {
- const notebookId = notebookstore.get().selectedNotebookId;
- if (!notebookId) return ["topics", []];
- const topics = db.notebooks.notebook(notebookId).topics.all;
- return ["topics", topics];
- }
- case "tags":
- return ["tags", db.tags.all];
- case "trash":
- return ["trash", db.trash.all];
- default:
- return [];
- }
-}
+const filters = ["notes", "notebooks", "topics", "tags"];
-function Search({ type }) {
+function Search() {
const [searchState, setSearchState] = useState({
isSearching: false,
totalItems: 0
});
const [results, setResults] = useState([]);
+ const [selectedResult, setSelectedResult] = useState();
const context = useNoteStore((store) => store.context);
- const nonce = useNoteStore((store) => store.nonce);
- const cachedQuery = useRef();
-
- const onSearch = useCallback(
- async (query) => {
- if (!query) return;
- cachedQuery.current = query;
-
- const [lookupType, items] = await typeToItems(type, context);
- setResults([]);
-
- if (items.length <= 0) {
- showToast("error", `There are no items to search in.`);
- return;
- }
- setSearchState({ isSearching: true, totalItems: items.length });
- const results = await db.lookup[lookupType](items, query);
- setResults(results);
- setSearchState({ isSearching: false, totalItems: 0 });
- },
- [context, type]
- );
+ const [searchItem, setSearchItem] = useState([]);
- const title = useMemo(() => {
- switch (type) {
- case "notes":
- if (!context) return "all notes";
- switch (context.type) {
- case "topic": {
- const notebook = db.notebooks.notebook(context.value.id);
- const topic = notebook.topics.topic(context.value.topic);
- return `notes of ${topic._topic.title} in ${notebook.title}`;
- }
- case "tag": {
- const tag = db.tags.all.find((tag) => tag.id === context.value);
- return `notes in #${tag.title}`;
- }
- case "favorite":
- return "favorite notes";
- case "color": {
- const color = db.colors.all.find((tag) => tag.id === context.value);
- return `notes in color ${color.title}`;
- }
- default:
- return;
- }
- case "notebooks":
- return "all notebooks";
- case "topics": {
- const notebookId = notebookstore.get().selectedNotebookId;
- if (!notebookId) return "";
- const notebook = db.notebooks.notebook(notebookId);
- return `topics in ${notebook.title} notebook`;
- }
- case "tags":
- return "all tags";
- case "trash":
- return "all trash";
- default:
- return "";
- }
- }, [type, context]);
-
- useEffect(() => {
- (async function () {
- const [lookupType, items] = await typeToItems(type, context);
- const results = await db.lookup[lookupType](items, cachedQuery.current);
- setResults(results);
- })();
- }, [nonce, type, context]);
+ useEffect(() => {}, []);
- if (!title) return hardNavigate("/");
+ const type = "notes";
return (
<>
- Searching {title}
+ Searching {type}
-
+ {
+ console.log("onSearch", query);
+ if (typeof query === "object") {
+ setResults(query);
+ setSearchItem(query);
+ } else {
+ if (!query) return;
+
+ let array = [];
+ let totalItems = 0;
+ for (let filter of filters) {
+ const [lookupType, items] = await db.search.getFilterData(filter);
+ totalItems = totalItems + items.length;
+ setSearchState({ isSearching: true, totalItems: totalItems });
+ const result = await db.lookup[lookupType](items, query);
+ array.push(result);
+ }
+ setResults(array);
+ let firstItem = getItem(array);
+ setSearchItem(firstItem);
+ setSelectedResult(array.indexOf(firstItem));
+ setSearchState({ isSearching: false, totalItems: 0 });
+ }
+ }}
+ // onChange={async (e) => {
+ // const { value } = e.target;
+ // if (!value.length) {
+ // return;
+ // }
+ // }}
+ />
{searchState.isSearching ? (
) : (
- (
-
+ <>
+
+ {results.map((result, index) =>
+ result.length > 0 ? (
+
+ ) : (
+ []
+ )
+ )}
+
+ {searchItem.length > 0 ? (
+
+ ) : (
+
+ {"No items to show"}
+
)}
- />
+ >
)}
>
);
}
export default Search;
+
+function SearchResults(props) {
+ return (
+ <>
+
+ {props.title}
+
+
+ >
+ );
+}
+
+function capitalizeFirstLetter(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+function getItem(array) {
+ let firstPopulatedItem = [];
+ for (let item of array) {
+ if (item.length > 0) {
+ firstPopulatedItem = item;
+ break;
+ }
+ }
+ return firstPopulatedItem;
+}
diff --git a/package.json b/package.json
index 28c9b1ad49..63ad1b3f60 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,10 @@
},
"dependencies": {
"@nrwl/nx-cloud": "^14.6.2",
- "eslint-plugin-header": "^3.1.1"
+ "eslint-plugin-header": "^3.1.1",
+ "liqe": "^3.6.0",
+ "react-datepicker": "^4.10.0",
+ "react-fast-compare": "^3.2.0"
},
"license": "GPL-3.0-or-later",
"volta": {
diff --git a/packages/core/api/index.js b/packages/core/api/index.js
index aad79923e4..80bc304f23 100644
--- a/packages/core/api/index.js
+++ b/packages/core/api/index.js
@@ -49,6 +49,7 @@ import { logger } from "../logger";
import Shortcuts from "../collections/shortcuts";
import Reminders from "../collections/reminders";
import Relations from "../collections/relations";
+import Search from "./search";
/**
* @type {EventSource}
@@ -128,6 +129,7 @@ class Database {
this.offers = new Offers();
this.debug = new Debug();
this.pricing = new Pricing();
+ this.search = new Search(this);
// collections
/** @type {Notes} */
diff --git a/packages/core/api/search.js b/packages/core/api/search.js
new file mode 100644
index 0000000000..5fd9ecb1b6
--- /dev/null
+++ b/packages/core/api/search.js
@@ -0,0 +1,204 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+class Search {
+ constructor(db) {
+ this._db = db;
+ }
+ /**
+ *
+ * @param {[{query,filterName}]} definitions
+ * @returns
+ */
+ async filters(definitions) {
+ //name is cumbersome
+ let notes = [];
+ await this._db.notes.init();
+ console.log("search", definitions);
+ let index = 0;
+ for (let definition of definitions) {
+ let _notes = await this.getFilteredNotes(
+ definition,
+ definition.filterName
+ );
+ let newNotes = [];
+ for (let note of notes) {
+ for (let _note of _notes) {
+ if (JSON.stringify(note) == JSON.stringify(_note))
+ newNotes.push(note);
+ }
+ }
+ if (index > 0 && notes.length > 0) notes = [];
+ if (index === 0) notes = _notes;
+ notes.push(...newNotes);
+ index++;
+ }
+ return notes;
+ }
+
+ //filtersearch, single filter search filter
+ filter = async (filterName, query) => {
+ const [_filterName, data] = await this.getFilterData(filterName);
+ if (_filterName !== undefined && data !== undefined) {
+ let result = await this._db.lookup[_filterName](data, query);
+ return { result, data };
+ } else {
+ return { result: [], data: [] };
+ }
+ };
+
+ async getFilteredNotes(definition, filterName) {
+ let notes = [];
+ filterName = filterName.trim();
+ let result = await (await this.filter(filterName, definition.query)).result;
+ console.log("beginSearch", definition, result);
+
+ switch (filterName) {
+ case "notebooks": {
+ for (let notebook of result)
+ for (let topic of notebook.topics) {
+ for (let note of this._db.notebooks
+ .notebook(topic.notebookId)
+ .topics.topic(topic.id).all) {
+ if (this._db.notes.note(note))
+ notes.push(this._db.notes.note(note)._note);
+ }
+ }
+ console.log("beginSearch notebook", notes);
+ return notes;
+ }
+ case "notes": {
+ console.log("beginSearch notes", notes);
+ let allNotes = this._db.notes.all;
+ let notes = await this._db.lookup["notes"](allNotes, definition.query);
+ return notes;
+ }
+ case "topics": {
+ for (let topic of result)
+ for (let note of this._db.notebooks
+ .notebook(topic.notebookId)
+ .topics.topic(topic.id).all) {
+ if (this._db.notes.note(note))
+ notes.push(this._db.notes.note(note)._note);
+ }
+ console.log("beginSearch topic", notes);
+ return notes;
+ }
+ case "tags": {
+ for (let tag of result)
+ for (let note of tag.noteIds) {
+ if (this._db.notes.note(note))
+ notes.push(this._db.notes.note(note)._note);
+ }
+ console.log("beginSearch tag", notes);
+ return notes;
+ }
+ case "intitle": {
+ let allNotes = this._db.notes.all;
+ notes = this._db.lookup["_byTitle"](allNotes, definition.query);
+ console.log("beginSearch intitle", notes);
+ return notes;
+ }
+ case "before": {
+ let unixTime = this.standardDate(definition.query).getTime();
+ let _notes = this._db.notes.all;
+ for (let note of _notes) {
+ if (note.dateCreated < unixTime) {
+ notes.push(note);
+ }
+ }
+ return notes;
+ }
+ case "during": {
+ let unixTime = this.standardDate(definition.query).getTime();
+ let _notes = this._db.notes.all;
+ console.log("beginSearch before", definition.query, unixTime, _notes);
+ for (let note of _notes) {
+ let dateCreated = new Date(note.dateCreated);
+ let noteDate = this.dateFormat(dateCreated).DDMMYY;
+ let selectedUnix = new Date(unixTime);
+ let selectedDate = this.dateFormat(selectedUnix).DDMMYY;
+ if (noteDate === selectedDate) {
+ notes.push(note);
+ }
+ }
+ console.log("beginSearch during", notes);
+ return notes;
+ }
+ case "after": {
+ let unixTime = this.standardDate(definition.query).getTime();
+ let _notes = this._db.notes.all;
+ console.log("beginSearch before", definition.value, unixTime, _notes);
+ for (let note of _notes) {
+ if (note.dateCreated > unixTime) {
+ notes.push(note);
+ }
+ }
+ console.log("beginSearch after", notes);
+ return notes;
+ }
+ default:
+ return notes;
+ }
+ }
+
+ async getFilterData(filterName) {
+ switch (filterName) {
+ case "notes":
+ await this._db.notes.init();
+ return ["notes", this._db.notes.all];
+ case "notebooks":
+ return ["notebooks", this._db.notebooks.all];
+ case "topics": {
+ const notebooks = this._db.notebooks.all;
+ if (!notebooks) return ["topics", []];
+
+ let topics = [];
+ for (let notebook of notebooks) {
+ let _topics = this._db.notebooks.notebook(notebook.id).topics.all;
+ if (_topics.length > 0)
+ for (let _topic of _topics) topics.push(_topic);
+ }
+ return ["topics", topics];
+ }
+ case "tags":
+ return ["tags", this._db.tags.all];
+ case "trash":
+ return ["trash", this._db.trash.all];
+ case "_byTitle": {
+ await this._db.notes.init();
+ return ["notes", this._db.notes.all];
+ }
+ default:
+ return [];
+ }
+ }
+
+ dateFormat(date) {
+ //this should be someplace else
+ return {
+ DDMMYY: date.getDate() + "/" + date.getMonth() + "/" + date.getFullYear()
+ };
+ }
+
+ standardDate(date) {
+ const [day, month, year] = date.split("/");
+ return new Date(year, month - 1, day);
+ }
+}
+export default Search;