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;