diff --git a/build.zip b/build.zip deleted file mode 100644 index b4369a9..0000000 Binary files a/build.zip and /dev/null differ diff --git a/src/app/feature/jq/queryInput.tsx b/src/app/feature/jq/queryInput.tsx index 135f06c..df60785 100644 --- a/src/app/feature/jq/queryInput.tsx +++ b/src/app/feature/jq/queryInput.tsx @@ -1,27 +1,61 @@ -import { useEffect, useState } from "react"; -import { useDebounce } from "./useDebuonce"; +import { useEffect, useRef, useState } from "react"; +import { + getHistory, + remoevHistoryAll, + removeHistory, +} from "../../../lib/queryHistoryFromLocalStrage"; type P = { initialJqQuery: string; }; export const QueryInput: React.FC

= (props) => { const [jqQuery, setJqQuery] = useState(props.initialJqQuery); - const debouncedInputText = useDebounce(jqQuery ?? "", 200); + const [jqQueryHistories, setJqQueryHistory] = useState([]); + const [historyKeyIndex, setHistoryKeyIndex] = useState(-1); + const [suggestMode, setSuggestMode] = useState(false); + const queryInputSuggestRef = useRef(null); + + const updateHistoryFromLocalStrage = async () => { + const res = await getHistory(); + setJqQueryHistory(res ?? []); + }; useEffect(() => { chrome.runtime.sendMessage({ + type: "query", + text: props.initialJqQuery, + }); + + updateHistoryFromLocalStrage(); + }, []); + + useEffect(() => { + setSuggestMode(false); + }, [props.initialJqQuery]); + + // ↑↓で履歴をinput要素にセットする + useEffect(() => { + if (historyKeyIndex >= 0 && jqQueryHistories.length > historyKeyIndex) { + setJqQuery(jqQueryHistories[historyKeyIndex]); + } + }, [historyKeyIndex]); + + // Enterや送信ボタンでjq発火 + const executeJq = async (jqQuery: string) => { + setSuggestMode(false); + await chrome.runtime.sendMessage({ type: "query", text: jqQuery, }); - }, [debouncedInputText]); - function onClickHandler() { - return () => { - const url = new URL(document.URL); - url.searchParams.set("chromeExtentionJqQuery", jqQuery); - history.pushState(null, "", url.toString()); - }; - } + if (jqQuery) { + updateHistoryFromLocalStrage(); + } + setJqQuery(jqQuery); + const url = new URL(document.URL); + url.searchParams.set("chromeExtentionJqQuery", jqQuery); + history.pushState(null, "", url.toString()); + }; return (

= (props) => { e.preventDefault(); }} > - { - setJqQuery(e.currentTarget.value); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - onClickHandler(); - } - }} - value={jqQuery} - /> - +
+
+ { + setJqQuery(e.currentTarget.value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + setSuggestMode(false); + executeJq(jqQuery); + e.preventDefault(); + e.stopPropagation(); + } + if (e.key === "ArrowUp") { + setHistoryKeyIndex((s) => (s >= 0 ? s - 1 : 0)); + !suggestMode && setSuggestMode(true); + } + if (e.key === "ArrowDown") { + setHistoryKeyIndex((s) => + s + 1 >= jqQueryHistories.length ? s : s + 1 + ); + !suggestMode && setSuggestMode(true); + } + if (e.ctrlKey && e.key === "r") { + setSuggestMode((s) => !s); + } + if (e.key === "Escape") { + setSuggestMode(false); + } + }} + onBlur={() => { + setSuggestMode(false); + }} + value={jqQuery} + /> + +
+ +
+ + {suggestMode && jqQueryHistories.length > 0 && ( + + )}
); }; diff --git a/src/app/feature/jq/useDebuonce.ts b/src/app/feature/jq/useDebuonce.ts deleted file mode 100644 index 661a7eb..0000000 --- a/src/app/feature/jq/useDebuonce.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useState, useEffect } from "react"; - -export function useDebounce(value: string, delay: number) { - const [debouncedValue, setDebouncedValue] = useState(value); - - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - return () => { - clearTimeout(timer); - }; - }, [value, delay]); - - return debouncedValue; -} diff --git a/src/app/feature/jsonPreview/__test__/__snapshots__/jsonPreview.spec.tsx.snap b/src/app/feature/jsonPreview/__test__/__snapshots__/jsonPreview.spec.tsx.snap index 2892e5c..6e7f51f 100644 --- a/src/app/feature/jsonPreview/__test__/__snapshots__/jsonPreview.spec.tsx.snap +++ b/src/app/feature/jsonPreview/__test__/__snapshots__/jsonPreview.spec.tsx.snap @@ -1,360 +1,428 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`jsonPreview snapshot json preview 1`] = ` - - - { - -
- - +
+ - posts - + { + 3 + } + - +
- [ + posts -
- - - 0 + [ - - - - { - -
+ [ + 1 + ] +
+ +
- - - id - - - - 1 - - - - title - - +
+
- json-server + { - - - author - - - - typicode - + + { + 3 + } + + +
+ + id + + + + 1 + + +
+
+ + title + + + + json-server + + +
+
+ + author + + + + typicode + + +
+
+
+
+ + } -
-
- - } - + +
-
+
+ + + ] - - - ] - -
- - - comments - - - + + +
- [ + comments -
- - - 0 + [ - - - - { - -
+ [ + 1 + ] +
+ +
- - - id - - - - 1 - - - - body - - +
+
- some comment + { - - - postId - - - - 1 - + + { + 3 + } + + +
+ + id + + + + 1 + + +
+
+ + body + + + + some comment + + +
+
+ + postId + + + + 1 + + +
+
+
+
+ + } -
- - - } - + +
- + + + + ] - - - ] - - - - - profile - - - + + +
- { + profile -
- - - name - - - - John Doe - - - - age + { - - - 30 - - - - url - - + { + 7 + } + - https://example.com - - - - address - - - - - { - -
- + name + + - city + John Doe + +
+
+ + age + + - - New York - + 30 + +
+
+ + url + + - postal_code + https://example.com - +
+
+ + address + +
+
- 10001 + { - - - street - - - - + { - -
+ - - - name + city - Broadway + New York +
+
- number + postal_code - 123 + 10001 +
+
- apartment + street - - +
{
- + + { + 3 + } + - - floor - - - 5 + name - - - number - - - 502 + + Broadway + - +
+
+ + number + + + + 123 + + +
+
+ + apartment + +
+
+ + { + +
+ + { + 2 + } + + +
+ + floor + + + + 5 + + +
+
+ + number + + + + 502 + + +
+
+
+
+ + } + +
+
- - } - +
+ + } - - - - - } +
+
-
+ + + + } - - - + +
- } - - - - - contact - - - - - { - -
- - - email - - +
+
- john.doe@example.com + { - - - phone - - - - + { - -
+ - - - home + email - 123-456-7890 + john.doe@example.com +
+
- work + phone - - - 987-654-3210 + + { + +
+ + { + 2 + } + + +
+ + home + + + + 123-456-7890 + + +
+
+ + work + + + + 987-654-3210 + + +
+
+
+
+ + } - - -
- - } +
+
- + + + + } - - - + +
- } - - - - - interests - - - - - [ - -
- - - 0 - - - - programming - - - - 1 - - +
+
- hiking + [ - - - 2 - - - - reading - - - -
- - ] - -
-
- - test - - - - - [ - -
- - - - 0 - - - - - { - -
+ - - - a + 0 - 1 + programming - -
- - } - -
-
- - 1 - - - - - { - -
- - +
- a + 1 - 2 + hiking - -
- - } - -
-
- - 2 - - - - - { - -
- - +
- a + 2 - 3 + reading - -
+
+
+ + + + ] + + + +
+ + test + +
+
+ + [ + +
+ + [ + 3 + ] + - } +
+ + 0 + +
+
+ + { + +
+ + { + 1 + } + + +
+ + a + + + + 1 + + +
+
+
+
+ + } + +
+
+
+ + 1 + +
+
+ + { + +
+ + { + 1 + } + + +
+ + a + + + + 2 + + +
+
+
+
+ + } + +
+
+
+ + 2 + +
+
+ + { + +
+ + { + 1 + } + + +
+ + a + + + + 3 + + +
+
+
+
+ + } + +
+
- +
+
+ + ] - - - - ] - +
+
- + + + + } - - - } - - + + - - - + + , + } - - + , +] `; diff --git a/src/app/feature/jsonPreview/convertJSONToHTML.tsx b/src/app/feature/jsonPreview/convertJSONToHTML.tsx index 2fdd606..83f525a 100644 --- a/src/app/feature/jsonPreview/convertJSONToHTML.tsx +++ b/src/app/feature/jsonPreview/convertJSONToHTML.tsx @@ -10,17 +10,22 @@ export function convertJSONToHTML(json: JSON) { const nodes = []; for (const [key, value] of Object.entries(json)) { - nodes.push({key}); if (typeof value === "object") { const surruondChar = getSurroundCharactor(value); nodes.push( - - {surroundParentheses(convertJSONToHTML(value), surruondChar)} - +
+ {key} +
+ {surroundParentheses(convertJSONToHTML(value), surruondChar)} +
+
); } else { nodes.push( - {convertJsonValueToHTML(value)} +
+ {key} + {convertJsonValueToHTML(value)} +
); } } diff --git a/src/app/feature/jsonPreview/parentheses.tsx b/src/app/feature/jsonPreview/parentheses.tsx index 8e83702..70406d5 100644 --- a/src/app/feature/jsonPreview/parentheses.tsx +++ b/src/app/feature/jsonPreview/parentheses.tsx @@ -25,13 +25,19 @@ export function surroundParentheses( surroundChars: SurroundChars ) { return ( - - {surroundChars.start} -
- - {text} -
- {surroundChars.end} -
+ <> +
+ {surroundChars.start} +
+ + {surroundChars.start} + {text.length} + {surroundChars.end} + + {text} +
+
+ {surroundChars.end} + ); } diff --git a/src/background.ts b/src/background.ts index 570f078..170d620 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,5 +1,6 @@ import jq from "jq-web/jq.wasm.js"; import { logger } from "./lib/logger"; +import { getHistory, addHistory } from "./lib/queryHistoryFromLocalStrage"; type MessageType = { type: "road" | "query"; @@ -46,5 +47,11 @@ chrome.runtime.onMessage.addListener(async (message: MessageType, sender) => { } catch (e) { logger.debug(e); } + + // 履歴保存 + if (!jqQuery) return; + + const histories = await getHistory(); + addHistory(jqQuery, histories); } }); diff --git a/src/lib/queryHistoryFromLocalStrage.ts b/src/lib/queryHistoryFromLocalStrage.ts new file mode 100644 index 0000000..dc5931b --- /dev/null +++ b/src/lib/queryHistoryFromLocalStrage.ts @@ -0,0 +1,50 @@ +import { logger } from "./logger"; + +const historyKey = "jqHistory"; +const HISTORY_MAX_SIZE = 300; +export const getHistory = async (): Promise => { + try { + const historyData = await chrome.storage.local.get(historyKey); + if (historyData && Array.isArray(historyData[historyKey])) { + return historyData[historyKey] as string[]; + } else { + return []; + } + } catch (e) { + logger.debug(e); + return []; + } +}; + +export const addHistory = async (jqQuery: string, histories: string[]) => { + // 空白やデフォルトは履歴に保持しない + if (!jqQuery || jqQuery === "." || histories.length + 1 >= HISTORY_MAX_SIZE) { + return; + } + try { + await chrome.storage.local.set({ + [historyKey]: Array.from(new Set([...histories, jqQuery])), + }); + } catch (e) { + logger.debug(e); + } +}; + +export const removeHistory = async (jqQuery: string, histories: string[]) => { + if (!jqQuery) { + return; + } + + const res = histories.filter((h) => h !== jqQuery); + try { + await chrome.storage.local.set({ + [historyKey]: Array.from(new Set(res)), + }); + } catch (e) { + logger.debug(e); + } +}; + +export const remoevHistoryAll = () => { + chrome.storage.local.clear(); +}; diff --git a/src/styles/json-preview.scss b/src/styles/json-preview.scss index ae94626..738e3aa 100644 --- a/src/styles/json-preview.scss +++ b/src/styles/json-preview.scss @@ -1,3 +1,18 @@ +// ##################### reset css ################### +input, +button, +textarea, +select { + background-color: transparent; + border: none; + cursor: pointer; + outline: none; + padding: 0; + appearance: none; + color: #000; +} + +// ##################### reset css ↑ ################### body { padding: 8px; display: flex; @@ -5,9 +20,8 @@ body { flex-direction: column; } -.jsonObject { - display: block; - padding-left: 20px; +button:focus { + border: solid 1px white; } .jsonKey { @@ -40,35 +54,129 @@ body { &::before, &::after { content: '"'; + color: rgb(70, 70, 70); } } } -.jsonMiddleKey { - padding-left: 20px; - display: block; +.jsonRow { + display: flex; } -summary { - color: gray; +.jsonObject { + &Detail { + padding-left: 8px; + } + + &Summary { + color: rgb(70, 70, 70); + // font-size: small; + } } -.surroundChar--start::after { - padding-left: -20px; +.jsonMiddle { + &Key { + @extend .jsonKey; + } + &Value { + padding-left: 8px; + } + + &Wrapper { + display: flex; + position: relative; + &::before { + content: ""; + position: absolute; + top: 30px; /* 上端のオフセット */ + left: 2px; + height: calc(100% - 30px); + width: 1px; + border-left: dotted 0.5px rgb(70, 70, 70); + } + } } // FIXME リファクタで場所を移す .queryInput { + width: 100%; + min-height: 64px; display: flex; + flex-direction: column; + align-items: center; + justify-content: start; + &SearchBox { + display: flex; + justify-content: space-around; + width: 100%; + + &InputWrapper { + width: 85%; + background: white; + border-radius: 8px; + padding: 8px 16px; + color: black; + position: relative; + } + } + &Input { - width: 90%; - background: white; - border-radius: 8px; - padding: 8px 16px; - color: black; + width: 100%; } &ShareButton { background: rgb(150, 150, 239); + color: white; + padding: 8px 16px; + } + &HistoryButton { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + background-color: rgba(70, 70, 70, 0.7); + color: white; + height: 80%; + padding: 4px 8px; + border-radius: 4px; + } + + &Suggest { + position: relative; + overflow-y: scroll; + margin: 0 16px 0px; + padding: 8px 16px 8px; + max-height: 20vh; + width: 60%; + border-radius: 0 0 8px 8px; + background-color: rgba(244, 241, 241, 0.5); + list-style: none; + + &Button { + min-width: 90%; + display: flex; + justify-content: flex-start; + color: white; + font-size: large; + } + &--active { + background-color: rgba(144, 238, 144, 0.9); + } + &RemoveButton { + color: white; + width: 64px; + } + + &AllRemoveButton { + border-top: solid 1px white; + color: white; + width: 100%; + } + + &List { + display: flex; + width: 100%; + justify-content: space-between; + } } }