From 9beb506bbf0b68153207f4e398a06bd7bf70520a Mon Sep 17 00:00:00 2001 From: alihamuh Date: Wed, 15 Feb 2023 10:56:14 +0500 Subject: [PATCH] web: ui and some functuanality is added. --- .../ui/src/components/menu/datepicker.css | 6 + .../ui/src/components/menu/filterinput.js | 327 ++++++++++++++++++ packages/ui/src/components/menu/maininput.js | 113 ++++++ packages/ui/src/components/menu/search.js | 325 +++++++++++++++++ .../ui/src/components/menu/suggestionrow.js | 56 +++ 5 files changed, 827 insertions(+) create mode 100644 packages/ui/src/components/menu/datepicker.css create mode 100644 packages/ui/src/components/menu/filterinput.js create mode 100644 packages/ui/src/components/menu/maininput.js create mode 100644 packages/ui/src/components/menu/search.js create mode 100644 packages/ui/src/components/menu/suggestionrow.js diff --git a/packages/ui/src/components/menu/datepicker.css b/packages/ui/src/components/menu/datepicker.css new file mode 100644 index 000000000..63c0f43a7 --- /dev/null +++ b/packages/ui/src/components/menu/datepicker.css @@ -0,0 +1,6 @@ +.react-datepicker-wrapper, +.react-datepicker__input-container, +.react-datepicker__input-container input { + display: block; + width: auto; +} diff --git a/packages/ui/src/components/menu/filterinput.js b/packages/ui/src/components/menu/filterinput.js new file mode 100644 index 000000000..617e5547c --- /dev/null +++ b/packages/ui/src/components/menu/filterinput.js @@ -0,0 +1,327 @@ +/* +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 { useEffect, forwardRef, useRef } from "react"; +import { hexToRGB } from "../../utils/color"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import "./datepicker.css"; +import { filterSearchEngine } from "./search"; +import { Text } from "@theme-ui/components"; +import { mainSearchEngine } from "./search"; + +export function FilterInput(props) { + const { + filters, + focusInput, + index, + setFilters, + item, + getSuggestions, + setSuggestions, + onFocus, + onBlur, + inputRef, + searchDefinitions + } = props; + + useEffect(() => { + focusInput(filters.length - 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filters.length]); + + console.log(); + // useEffect(() => { + // (async () => { + // console.log(filterRef.current.target.innerText); + // //await checkErrors(props, inputRef.filter.target.innerText); + // //addDefinition(item.input, searchDefinitions); + // })(); + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [filterRef.current.target.innerText]); + + const setCalenderState = (state) => { + setFilters((filters) => { + let inputs = [...filters]; + inputs[index].input.isCalenderOpen = state; + return inputs; + }); + }; + + const setCalenderDate = () => { + document.getElementById(`inputId_${index}`).textContent = + filters[index].input.date.formatted; + }; + return !item.input.isDateFilter ? ( + { + await checkErrors(props, e.target.textContent); + setSuggestions(await getSuggestions(e.target.textContent, item.input)); + onFocus(e); + }} + onBlur={onBlur} + onKeyDown={async (e) => { + (await onKeyPress(e, props))[e.key](); + }} + /> + ) : ( + } + 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( + //shift to top + await getSuggestions(e.target.textContent, item.input) + ); + onFocus(e); + }} + onKeyDown={async (e) => { + (await onKeyPress(e, props))[e.key](); + }} + /> + ); +} + +const CustomInput = forwardRef((props, refs) => ( + +)); +CustomInput.displayName = "CustomInput"; + +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 addDefinition = (input, definitions) => { + //hard to understand// minimize if else + //for (let filter of filters) console.log(filter.input.state.result); + if (input.state.result) { + let isArrayEmpty = false; + for (let index = 0; index < definitions.length; index++) { + if (definitions[index].srNo === input.id) { + isArrayEmpty = true; + input.state.result.srNo = input.id; + definitions[index] = input.state.result; + } + } + + if (!isArrayEmpty) { + input.state.result.srNo = input.id; + definitions.push(input.state.result); + } + } +}; + +const onClick = (props, query) => { + checkErrors(props, query); +}; + +const checkErrors = async (props, query) => { + const { setFilters, index, item } = props; + query = query.trim(); + let input = item.input; + let result = await (await mainSearchEngine(input.type, query)).result; + setFilters((filters) => { + let _filters = [...filters]; + _filters[index].input.state = filterInputState(input, result, query); + return _filters; + }); +}; + +const filterInputState = (input, result, query) => { + console.log( + "filterInputState", + input.id, + query, + !input.hasSuggestions, + result.length > 0 + ); + if (!input.hasSuggestions) + return { + error: false, + message: "", + result: { type: input.type, value: query } + }; + + if (result.length > 0) + return { error: false, message: "", result: result[0] }; + + return { + error: true, + message: `This ${input.type.replace( + "s", + "" + )} is not present in the database.`, + result: undefined + }; +}; + +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 + } = props; + setSuggestions(await getSuggestions(e.target.textContent, item.input)); + await checkErrors(props, e.target.textContent); + return { + Enter: async () => { + props.onKeyDown(e); + focusInput(index + 1); + let results = await filterSearchEngine(searchDefinitions); + onSearch(results); + setSuggestions([]); + //await checkErrors(props, e.target.innerText); + addDefinition(item.input, searchDefinitions, filters); + e.preventDefault(); + }, + Escape: () => { + setSuggestions([]); + }, + ArrowDown: () => { + moveSelection(suggestions, setSelectionIndex).Down(); + e.preventDefault(); + }, + ArrowUp: () => { + moveSelection(suggestions, setSelectionIndex).Up(); + e.preventDefault(); + }, + ArrowLeft: () => { + if (getCursorPosition(document.getElementById(e.target.id)) == 0) { + addDefinition(item.input, searchDefinitions); + focusInput(index - 1); + } + }, + ArrowRight: () => { + if ( + getCursorPosition(document.getElementById(e.target.id)) == + e.target.textContent.length + ) { + addDefinition(item.input, searchDefinitions); + focusInput(index + 1); + } + }, + Backspace: () => { + if (e.target.textContent === "") { + setSuggestions([]); + setFilters(deleteFilter(filters, index)); + deleteDefinition(searchDefinitions, item.input.id); + focusInput(index - 1); + } + } + }; +}; diff --git a/packages/ui/src/components/menu/maininput.js b/packages/ui/src/components/menu/maininput.js new file mode 100644 index 000000000..2d99ae2ca --- /dev/null +++ b/packages/ui/src/components/menu/maininput.js @@ -0,0 +1,113 @@ +/* +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 { useEffect, useState } from "react"; +import { db } from "../../common/db"; +import { addToSearchHistory } from "./search"; +import { Input } from "@theme-ui/components"; + +export function MainInput(props) { + const [searchHistory, setSearchHistory] = useState(); + + useEffect(async () => { + //correct all the useEffects + //history file to be added in core for search history + let list = await db.searchHistory.getHistory(); + if (props.filters.length < 1) { + setSearchHistory(list); + } else { + setSearchHistory([]); + } + }, [db.searchHistory, props.filters.length]); + + return ( + { + props.refreshFilters(e.target.value, props.setFilters); + props.setSuggestions( + await props.getSuggestions(e.target.value, undefined, searchHistory) + ); + }} + onFocus={async (e) => { + //shift to index + props.setSuggestions( + await props.getSuggestions(e.target.value, undefined, searchHistory) + ); + props.onFocus(e); + }} + onKeyDown={async (e) => { + //keyActions(e); + props.onKeyDown(e); + await onKeyPress[e.key](e, props); + }} + /> + ); +} + +const onKeyPress = { + Enter: async (e, props) => { + props.onSearch(e.target.value); + await addToSearchHistory(e.target.value); + props.setSuggestions([]); + props.refreshFilters(e.target.value, props.setFilters); + e.preventDefault(); + }, + Escape: (e, props) => { + props.setSuggestions([]); + }, + ArrowDown: (e, props) => { + props.moveSelection(props.suggestions, props.setSelectionIndex).Down(); + e.preventDefault(); + }, + ArrowUp: (e, props) => { + props.moveSelection(props.suggestions, props.setSelectionIndex).Up(); + e.preventDefault(); + }, + ArrowLeft: (e, props) => { + if (e.target.selectionStart == 0) { + props.focusInput(props.filters.length - 1); + } + }, + ArrowRight: (e, props) => { + if (e.target.selectionStart == e.target.value.length) { + props.focusInput(0); + } + }, + Backspace: (e, props) => { + if (e.target.selectionStart == 0) { + props.focusInput(props.filters.length - 1); + } + } +}; diff --git a/packages/ui/src/components/menu/search.js b/packages/ui/src/components/menu/search.js new file mode 100644 index 000000000..50c323b85 --- /dev/null +++ b/packages/ui/src/components/menu/search.js @@ -0,0 +1,325 @@ +/* +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 { store } from "../../stores/note-store"; +import { db } from "../../common/db"; +import { filter, parse } from "liqe"; + +export const mainSearchEngine = async (searchType, value) => { + //name is cumbersome + // better name? fetchResults + const context = store.context; + const [lookupType, allData] = await filterItemsToType(searchType, context); + if (lookupType !== undefined && allData !== undefined) { + let result = await db.lookup[lookupType](allData, value); + return { result, allData }; + } else { + return { result: [], allData: [] }; + } +}; + +/* +there is mainly two types of search: +filter search +main search +filter search is again divided into two: Why not make it one? +how to search? +db.notes.all === all notes +notebooks:all the notes in the notebook +topics: all the notes in the topic +tags: all notes in the tag +dates:all notes between the dates +notes: all notes with the word +inTitle: all notes with the word in title +the search will condense. The scope will decrease with the number of filters +*/ +export const filterSearchEngine = async (definitions) => { + //name is cumbersome + let notes = []; + let searchlabels = []; + const organizingNotesFilters = ["notebook", "topic", "tag"]; + let isOrganizingNotesFilter = false; + await db.notes.init(); + console.log("search", definitions); + for (let definition of definitions) { + if (organizingNotesFilters.includes(definition.type)) + isOrganizingNotesFilter = true; + } + for (let definition of definitions) { + switch (definition.type) { + case "notebook": + //isOrganizingNotesFilter = true; //this boolean should be done seperately + for (let topic of definition.topics) { + for (let note of topic.notes) { + if (db.notes.note(note)) notes.push(db.notes.note(note)._note); + } + } + break; + case "topic": + //isOrganizingNotesFilter = true; + for (let note of definition.notes) { + if (db.notes.note(note)) notes.push(db.notes.note(note)._note); + } + break; + case "tag": + //isOrganizingNotesFilter = true; + for (let note of definition.noteIds) { + if (db.notes.note(note)) notes.push(db.notes.note(note)._note); + } + break; + default: + searchlabels.push(definition); + } + } + + // for (let definition of definitions) { //switch statements or consts + // if (definition.type === "notebook") { + // isHigherlabelPresent = true; + // for (let topic of definition.topics) { + // for (let note of topic.notes) { + // if (db.notes.note(note)) notes.push(db.notes.note(note)._note); + // } + // } + // } else if (definition.type === "topic") { + // isHigherlabelPresent = true; + // for (let note of definition.notes) { + // if (db.notes.note(note)) notes.push(db.notes.note(note)._note); + // } + // } else if (definition.type === "tag") { + // isHigherlabelPresent = true; + // for (let note of definition.noteIds) { + // if (db.notes.note(note)) notes.push(db.notes.note(note)._note); + // } + // } else if ( + // definition.type === "before" || + // definition.type === "after" || + // definition.type === "during" || + // definition.type === "notes" || + // definition.type === "intitle" + // ) { + // searchlabels.push(definition); + // //notes = await dateSearch(definition.value, definition.type); + // } + // } + + let result = []; + if (searchlabels.length > 0) { + for (let label of searchlabels) { + if (notes.length > 0) { + if (label.type === "notes") { + result = await db.lookup["notes"](notes, label.value); + } else if (label.type === "intitle") { + result = db.lookup["_byTitle"](notes, label.value); + } else { + result = await dateSearchEngine(label.value, label.type, notes); + } + } else if (!isOrganizingNotesFilter) { + let allNotes = db.notes.all; + if (label.type === "notes") { + let search = await db.lookup["notes"](allNotes, label.value); + result.push(...search); + } else if (label.type === "intitle") { + let search = await db.lookup["_byTitle"](allNotes, label.value); + result.push(...search); + } else { + result = await dateSearchEngine(label.value, label.type, allNotes); + } + } + } + return result; + } else { + return notes; + } +}; + +//definitions will first be sorted in following order: nbks> topics>tags>times>notes>intitle, +// const someMethod = async (definition,notes) => { +// await db.notes.init(); +// return { +// notebook: () => { +// for (let topic of definition.topics) { // noltebooks and topics will be sorted for search in one method +// for (let note of topic.notes) { +// if (db.notes.note(note)) notes.push(db.notes.note(note)._note); +// } +// } +// }, +// topic: () => { +// for (let note of definition.notes) { +// if (db.notes.note(note)) notes.push(db.notes.note(note)._note); +// } +// }, +// tag: () => { //all the notes from tag will be retrived and then mathched with already present notes to sort out relevent notes +// for (let note of definition.noteIds) { +// if (db.notes.note(note)) notes.push(db.notes.note(note)._note); +// } +// }, +// before: () => {//all the notes within dates will be retrived but only those will be sorted out which match with already present notes +// let _notes = notes === [] ? db.notes.all : notes; +// for (let note of _notes) { +// if (note.dateCreated < unixTime) { +// result.push(note); +// } +// } +// }, +// after: () => {}, +// during: () => {} +// }; +// }; + +export const dateSearchEngine = async (date, label, notes) => { + let unixTime = new Date(date).getTime(); + let notesArc = notes ? notes : db.notes.all; + let result = []; + + switch (label) { + case "before": + for (let note of notesArc) { + if (note.dateCreated < unixTime) { + result.push(note); + } + } + break; + case "after": + for (let note of notesArc) { + if (note.dateCreated > unixTime) { + result.push(note); + } + } + break; + case "during": + for (let note of notesArc) { + let dateCreated = new Date(note.dateCreated); + let noteDate = dateFormat(dateCreated).DDMMYY; + let selectedUnix = new Date(unixTime); + let selectedDate = dateFormat(selectedUnix).DDMMYY; + if (noteDate === selectedDate) { + result.push(note); + } + } + break; + } + return result; + // if (label === "before") { + // for (let note of notesArc) { + // if (note.dateCreated < unixTime) { + // result.push(note); + // } + // } + // } else if (label === "after") { + // for (let note of notesArc) { + // if (note.dateCreated > unixTime) { + // result.push(note); + // } + // } + // } else if (label === "during") { + // for (let note of notesArc) { + // let dateCreated = new Date(note.dateCreated); + // let noteDate = + // dateCreated.getDate() + + // "/" + + // dateCreated.getMonth() + + // "/" + + // dateCreated.getFullYear(); + // let selectedUnix = new Date(unixTime); + // let selectedDate = + // selectedUnix.getDate() + + // "/" + + // selectedUnix.getMonth() + + // "/" + + // selectedUnix.getFullYear(); + // if (noteDate === selectedDate) { + // result.push(note); + // } + // } + // } +}; + +const dateFormat = (date) => { + //this should be someplace else + return { + DDMMYY: date.getDate() + "/" + date.getMonth() + "/" + date.getFullYear() + }; +}; + +export async function filterItemsToType(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 notebooks = db.notebooks.all; + if (!notebooks) return ["topics", []]; + let topics = []; + for (let notebook of notebooks) { + let notebookTopics = db.notebooks.notebook(notebook.id).topics.all; + if (notebookTopics.length > 0) { + for (let notebookTopic of notebookTopics) { + topics.push(notebookTopic); + } + } + } + return ["topics", topics]; + case "tags": + return ["tags", db.tags.all]; + case "trash": + return ["trash", db.trash.all]; + case "_byTitle": + await db.notes.init(); + if (!context) return ["notes", db.notes.all]; + let title = context.notes; + return ["_byTitle", title]; + default: + return []; + } +} + +export function filterItems(query, items) { + //naming is wrong + //this is fetch suggestions search + try { + return filter( + parse(`text:"${query.toLowerCase()}"`), + items.map((item) => { + return { item, text: item.query }; + }) + ).map((v) => { + return v.item.query; + }); + } catch { + return []; + } +} + +export const addToSearchHistory = async (value) => { + // const history = await db.searchHistory.getHistory(); + // value = value.trim(); + // let isValueAlreadyPresent = false; + // history.map((item) => { + // if (item.query === value) { + // isValueAlreadyPresent = true; + // } + // }); + // if (isValueAlreadyPresent) { + // return; + // } + // await db.searchHistory.add(value); +}; diff --git a/packages/ui/src/components/menu/suggestionrow.js b/packages/ui/src/components/menu/suggestionrow.js new file mode 100644 index 000000000..dd7d34f35 --- /dev/null +++ b/packages/ui/src/components/menu/suggestionrow.js @@ -0,0 +1,56 @@ +/* +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 ( + + ); +}