diff --git a/README.md b/README.md index 38a308e..37a6296 100644 --- a/README.md +++ b/README.md @@ -106,31 +106,32 @@ plugins: [ ## Props -| Property | Description | Type | Default | -|---------------------------|---------------------------------------------------------------------------------|-----------------------------------| ------- | -| data(v-model) | JSON data, support v-model when use editable | JSON object | - | -| collapsedNodeLength | Objects or arrays which length is greater than this threshold will be collapsed | number | - | -| deep | Paths greater than this depth will be collapsed | number | - | -| showLength | Show the length when collapsed | boolean | false | -| showLine | Show the line | boolean | true | -| showLineNumber | Show the line number | boolean | false | -| showIcon | Show the icon | boolean | false | -| showDoubleQuotes | Show doublequotes on key | boolean | true | -| virtual | Use virtual scroll | boolean | false | -| height | The height of list when using virtual | number | 400 | -| itemHeight | The height of node when using virtual | number | 20 | -| selectedValue(v-model) | Selected data path | string, array | - | -| rootPath | Root data path | string | `root` | -| nodeSelectable | Defines whether a node supports selection | (node) => boolean | - | -| selectableType | Support path select, default none | `multiple` \| `single` | - | -| showSelectController | Show the select controller | boolean | false | -| selectOnClickNode | Trigger select when click node | boolean | true | -| highlightSelectedNode | Support highlighting selected nodes | boolean | true | -| collapsedOnClickBrackets | Support click brackets to collapse | boolean | true | -| renderNodeKey | render node key, or use slot #renderNodeKey | ({ node, defaultKey }) => vNode | - | -| renderNodeValue | render node value, or use slot #renderNodeValue | ({ node, defaultValue }) => vNode | - | -| editable | Support editable | boolean | false | -| editableTrigger | Trigger | `click` \| `dblclick` | `click` | +| Property | Description | Type | Default | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- | +| data(v-model) | JSON data, support v-model when use editable | JSON object | - | +| collapsedNodeLength | Objects or arrays which length is greater than this threshold will be collapsed | number | - | +| deep | Paths greater than this depth will be collapsed | number | - | +| showLength | Show the length when collapsed | boolean | false | +| showLine | Show the line | boolean | true | +| showLineNumber | Show the line number | boolean | false | +| showIcon | Show the icon | boolean | false | +| showDoubleQuotes | Show doublequotes on key | boolean | true | +| virtual | Use virtual scroll | boolean | false | +| height | The height of list when using virtual | number | 400 | +| itemHeight | The height of node when using virtual | number | 20 | +| selectedValue(v-model) | Selected data path | string, array | - | +| rootPath | Root data path | string | `root` | +| nodeSelectable | Defines whether a node supports selection | (node) => boolean | - | +| selectableType | Support path select, default none | `multiple` \| `single` | - | +| showSelectController | Show the select controller | boolean | false | +| selectOnClickNode | Trigger select when click node | boolean | true | +| highlightSelectedNode | Support highlighting selected nodes | boolean | true | +| collapsedOnClickBrackets | Support click brackets to collapse | boolean | true | +| renderNodeKey | render node key, or use slot #renderNodeKey | ({ node, defaultKey }) => vNode | - | +| renderNodeValue | render node value, or use slot #renderNodeValue | ({ node, defaultValue }) => vNode | - | +| editable | Support editable | boolean | false | +| editableTrigger | Trigger | `click` \| `dblclick` | `click` | +| theme | Sets the theme of the component. Options are 'light' or 'dark', with dark mode enhancing visibility on dark backgrounds | `'light' \| 'dark'` | `light` | ## Events @@ -145,7 +146,7 @@ plugins: [ | Slot Name | Description | Parameters | | --------------- | ----------------- | ---------------------- | -| renderNodeKey | render node key | { node, defaultKey } | +| renderNodeKey | render node key | { node, defaultKey } | | renderNodeValue | render node value | { node, defaultValue } | ## Contributors diff --git a/example/App.tsx b/example/App.tsx index 730fd7f..44b4f8b 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,10 +1,11 @@ -import { defineComponent, reactive } from 'vue'; +import { defineComponent, reactive, provide, onMounted, watch } from 'vue'; import Basic from './Basic.vue'; import VirtualList from './VirtualList.vue'; import SelectControl from './SelectControl.vue'; import Editable from './Editable.vue'; // import Tsx from './Tsx'; import './styles.less'; +import { MoonIcon, SunIcon } from './Icons'; const list = [ { @@ -41,6 +42,12 @@ export default defineComponent({ opened: [list[0].key], }); + const globalDarkModeState = reactive({ + isDarkMode: window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches, + }); + + provide('darkModeState', globalDarkModeState); + const onActiveChange = (key: string) => { state.activeKey = key; if (!state.opened.includes(key)) { @@ -48,17 +55,35 @@ export default defineComponent({ } }; + const toggleDarkMode = () => { + globalDarkModeState.isDarkMode = !globalDarkModeState.isDarkMode; + }; + + onMounted(() => { + document.body.classList.toggle('dark-mode', globalDarkModeState.isDarkMode); + }); + + watch( + () => globalDarkModeState.isDarkMode, + newVal => { + document.body.classList.toggle('dark-mode', newVal); + }, + { immediate: true }, + ); + return { state, onActiveChange, + toggleDarkMode, + globalDarkModeState, }; }, render() { - const { state, onActiveChange } = this; + const { state, onActiveChange, toggleDarkMode, globalDarkModeState } = this; return ( - <div class="example"> + <div class={`example ${state.isDarkMode ? 'dark-mode' : ''}`}> <h1>Vue Json Pretty</h1> <p> Welcome to the demo space of Vue Json Pretty, here we provide the following different @@ -67,15 +92,20 @@ export default defineComponent({ <div class="tabs"> <div class="tabs-header"> - {list.map(({ title, key }) => ( - <div - key={key} - class={`tabs-header-item ${key === state.activeKey ? 'is-active' : ''}`} - onClick={() => onActiveChange(key)} - > - {title} - </div> - ))} + <div class="tabs-items-container"> + {list.map(({ title, key }) => ( + <div + key={key} + class={`tabs-header-item ${key === state.activeKey ? 'is-active' : ''}`} + onClick={() => onActiveChange(key)} + > + {title} + </div> + ))} + </div> + <div class="dark-mode-toggle" onClick={toggleDarkMode}> + {globalDarkModeState.isDarkMode ? <SunIcon /> : <MoonIcon />} + </div> </div> <div class="tabs-content"> diff --git a/example/Basic.vue b/example/Basic.vue index ae8d500..64df3a1 100644 --- a/example/Basic.vue +++ b/example/Basic.vue @@ -2,7 +2,7 @@ <div class="example-box"> <div class="block"> <h3>JSON:</h3> - <textarea v-model="state.val" /> + <textarea :class="{ 'dark-textarea': globalDarkModeState }" v-model="state.val"></textarea> <h3>Options:</h3> <div class="options"> @@ -46,6 +46,13 @@ <label>setPathCollapsible</label> <input v-model="state.setPathCollapsible" type="checkbox" /> </div> + <div> + <label>theme</label> + <select v-model="localDarkMode"> + <option value="light">light</option> + <option value="dark">dark</option> + </select> + </div> </div> <h3>Slots:</h3> @@ -63,6 +70,7 @@ <div class="block"> <h3>vue-json-pretty:</h3> <vue-json-pretty + :theme="localDarkMode" :data="state.data" :deep="state.deep" :path-collapsible="state.setPathCollapsible ? pathCollapsible : undefined" @@ -95,6 +103,7 @@ <script> import { defineComponent, reactive, watch } from 'vue'; +import { useDarkMode } from './useDarkMode'; import VueJsonPretty from 'src'; const defaultData = { @@ -147,6 +156,8 @@ export default defineComponent({ showKeyValueSpace: true, }); + const { localDarkMode, toggleLocalDarkMode, globalDarkModeState } = useDarkMode(); + const pathCollapsible = node => { return node.key === 'members'; }; @@ -165,6 +176,9 @@ export default defineComponent({ return { state, pathCollapsible, + localDarkMode, + toggleLocalDarkMode, + globalDarkModeState, }; }, }); diff --git a/example/Editable.vue b/example/Editable.vue index e4ed8b8..c339f31 100644 --- a/example/Editable.vue +++ b/example/Editable.vue @@ -2,7 +2,7 @@ <div class="example-box"> <div class="block"> <h3>JSON:</h3> - <textarea v-model="state.val" /> + <textarea :class="{ 'dark-textarea': globalDarkModeState }" v-model="state.val"></textarea> <h3>Options:</h3> <div class="options"> @@ -34,11 +34,19 @@ </select> </div> </div> + <div> + <label>theme</label> + <select v-model="localDarkMode"> + <option value="light">light</option> + <option value="dark">dark</option> + </select> + </div> </div> <div class="block"> <h3>vue-json-pretty:</h3> <vue-json-pretty v-model:data="state.data" + :theme="localDarkMode" :deep="state.deep" :show-double-quotes="true" :show-line="state.showLine" @@ -53,6 +61,7 @@ <script> import { defineComponent, reactive, watch } from 'vue'; import VueJsonPretty from 'src'; +import { useDarkMode } from './useDarkMode'; const defaultData = { status: 200, @@ -98,6 +107,8 @@ export default defineComponent({ deep: 3, }); + const { localDarkMode, toggleLocalDarkMode, globalDarkModeState } = useDarkMode(); + watch( () => state.val, newVal => { @@ -122,7 +133,11 @@ export default defineComponent({ return { state, + localDarkMode, + toggleLocalDarkMode, + globalDarkModeState, }; }, }); </script> +./useDarkMode diff --git a/example/Icons.tsx b/example/Icons.tsx new file mode 100644 index 0000000..fcf1407 --- /dev/null +++ b/example/Icons.tsx @@ -0,0 +1,39 @@ +export const SunIcon = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <circle cx="12" cy="12" r="5"></circle> + <line x1="12" y1="1" x2="12" y2="3"></line> + <line x1="12" y1="21" x2="12" y2="23"></line> + <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> + <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> + <line x1="1" y1="12" x2="3" y2="12"></line> + <line x1="21" y1="12" x2="23" y2="12"></line> + <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> + <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> + </svg> +); + +export const MoonIcon = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <path d="M21 12.79A9 9 0 0112.21 3C11.66 3 11.11 3.05 10.58 3.15A9 9 0 1021 12.79z"></path> + </svg> +); diff --git a/example/SelectControl.vue b/example/SelectControl.vue index 32a7196..a71755c 100644 --- a/example/SelectControl.vue +++ b/example/SelectControl.vue @@ -2,7 +2,7 @@ <div class="example-box"> <div class="block"> <h3>JSON:</h3> - <textarea v-model="state.val" /> + <textarea :class="{ 'dark-textarea': globalDarkModeState }" v-model="state.val"></textarea> <h3>Options:</h3> <div class="options"> @@ -57,6 +57,13 @@ <option :value="4">4</option> </select> </div> + <div> + <label>theme</label> + <select v-model="localDarkMode"> + <option value="light">light</option> + <option value="dark">dark</option> + </select> + </div> </div> <h3>v-model:selectedValue:</h3> <div>{{ state.selectedValue }}</div> @@ -68,6 +75,7 @@ <vue-json-pretty v-if="state.renderOK" v-model:selectedValue="state.selectedValue" + :theme="localDarkMode" :data="state.data" :root-path="state.rootPath" :deep="state.deep" @@ -94,6 +102,7 @@ <script> import { defineComponent, reactive, watch, nextTick } from 'vue'; import VueJsonPretty from 'src'; +import { useDarkMode } from './useDarkMode'; const defaultData = { status: 200, @@ -148,6 +157,8 @@ export default defineComponent({ showIcon: false, }); + const { localDarkMode, toggleLocalDarkMode, globalDarkModeState } = useDarkMode(); + const handleNodeClick = node => { state.node = node; }; @@ -186,6 +197,9 @@ export default defineComponent({ state, handleNodeClick, handleAll, + localDarkMode, + toggleLocalDarkMode, + globalDarkModeState, }; }, }); diff --git a/example/VirtualList.vue b/example/VirtualList.vue index a8018de..710bd26 100644 --- a/example/VirtualList.vue +++ b/example/VirtualList.vue @@ -2,7 +2,7 @@ <div class="example-box"> <div class="block"> <h3>JSON:</h3> - <textarea v-model="state.val" /> + <textarea :class="{ 'dark-textarea': globalDarkModeState }" v-model="state.val"></textarea> <h3>Options:</h3> <div class="options"> @@ -34,10 +34,18 @@ </select> </div> </div> + <div> + <label>theme</label> + <select v-model="localDarkMode"> + <option value="light">light</option> + <option value="dark">dark</option> + </select> + </div> </div> <div class="block"> <h3>vue-json-pretty(10000+ items):</h3> <vue-json-pretty + :theme="localDarkMode" :collapsed-node-length="state.collapsedNodeLength" :virtual="true" :item-height="+state.itemHeight" @@ -53,6 +61,7 @@ <script> import { defineComponent, reactive, watch } from 'vue'; import VueJsonPretty from 'src'; +import { useDarkMode } from './useDarkMode'; const defaultData = { status: 200, @@ -86,6 +95,8 @@ export default defineComponent({ itemHeight: 20, }); + const { localDarkMode, toggleLocalDarkMode, globalDarkModeState } = useDarkMode(); + watch( () => state.val, newVal => { @@ -99,6 +110,9 @@ export default defineComponent({ return { state, + localDarkMode, + toggleLocalDarkMode, + globalDarkModeState, }; }, }); diff --git a/example/styles.less b/example/styles.less index 6c0d867..4db97c7 100644 --- a/example/styles.less +++ b/example/styles.less @@ -1,4 +1,7 @@ @primary-color: #1890ff; +@dark-background-color: darken(#333, 5%); +@dark-text-color: #fff; +@dark-border-color: #555; * { box-sizing: border-box; @@ -7,37 +10,59 @@ html, body { margin: 0; + transition: background-color 0.3s, color 0.3s; } body { font-size: 14px; background-color: #f9f9f9; + + &.dark-mode { + background-color: @dark-background-color; + color: @dark-text-color; + } } .tabs-header { display: flex; + justify-content: space-between; margin-bottom: 20px; border-bottom: 1px solid #ccc; } -.tabs-header-item { - position: relative; - margin-right: 20px; - padding: 8px 0; - cursor: pointer; - transition: color 0.3s; - - &:hover, - &.is-active { - color: @primary-color; - - &:after { - border-bottom: 2px solid @primary-color; - content: ''; - width: 100%; - position: absolute; - left: 0; - bottom: -1px; +.tabs-items-container { + display: flex; + + .tabs-header-item { + position: relative; + margin-right: 20px; + padding: 8px 0; + cursor: pointer; + transition: color 0.3s; + + &:hover, + &.is-active { + color: @primary-color; + + &:after { + border-bottom: 2px solid @primary-color; + content: ''; + width: 100%; + position: absolute; + left: 0; + bottom: -1px; + } + } + body.dark-mode & { + color: @dark-text-color; + &:hover, + &.is-active { + color: lighten(@primary-color, 20%); + + &:after { + border-bottom: 2px solid lighten(@primary-color, 20%); + } + } } } } @@ -96,3 +121,9 @@ body { text-overflow: ellipsis; } } + +.dark-textarea { + background-color: #333; + color: #fff; + border-color: #444; +} diff --git a/example/useDarkMode.ts b/example/useDarkMode.ts new file mode 100644 index 0000000..66abe61 --- /dev/null +++ b/example/useDarkMode.ts @@ -0,0 +1,22 @@ +import { inject, ref, watch } from 'vue'; + +export function useDarkMode() { + const darkModeState = inject('darkModeState'); + const globalDarkModeState = ref(darkModeState.isDarkMode); + const localDarkMode = ref(darkModeState.isDarkMode ? 'dark' : 'light'); + + watch( + () => darkModeState.isDarkMode, + newVal => { + localDarkMode.value = newVal ? 'dark' : 'light'; + globalDarkModeState.value = newVal; + }, + ); + + const toggleLocalDarkMode = () => { + darkModeState.isDarkMode = !darkModeState.isDarkMode; + localDarkMode.value = darkModeState.isDarkMode ? 'dark' : 'light'; + }; + + return { localDarkMode, toggleLocalDarkMode, globalDarkModeState }; +} diff --git a/src/components/Tree/index.tsx b/src/components/Tree/index.tsx index 8edab29..6957e8a 100644 --- a/src/components/Tree/index.tsx +++ b/src/components/Tree/index.tsx @@ -70,6 +70,10 @@ export default defineComponent({ onSelectedChange: { type: Function as PropType<(newVal: string | string[], oldVal: string | string[]) => void>, }, + theme: { + type: String as PropType<'light' | 'dark'>, + default: 'light', + }, }, slots: ['renderNodeKey', 'renderNodeValue'], @@ -281,6 +285,7 @@ export default defineComponent({ key={item.id} node={item} collapsed={!!state.hiddenPaths[item.path]} + theme={props.theme} showDoubleQuotes={props.showDoubleQuotes} showLength={props.showLength} checked={selectedPaths.value.includes(item.path)} @@ -316,6 +321,7 @@ export default defineComponent({ class={{ 'vjs-tree': true, 'is-virtual': props.virtual, + dark: props.theme === 'dark', }} onScroll={props.virtual ? handleTreeScroll : undefined} style={ diff --git a/src/components/TreeNode/index.tsx b/src/components/TreeNode/index.tsx index fa5b0bb..6c6a27c 100644 --- a/src/components/TreeNode/index.tsx +++ b/src/components/TreeNode/index.tsx @@ -64,6 +64,10 @@ export const treeNodePropsPass = { type: Boolean, default: false, }, + theme: { + type: String as PropType<'light' | 'dark'>, + default: 'light', + }, showKeyValueSpace: { type: Boolean, default: true, @@ -213,6 +217,7 @@ export default defineComponent({ 'has-selector': props.showSelectController, 'has-carets': props.showIcon, 'is-highlight': props.highlightSelectedNode && props.checked, + dark: props.theme === 'dark', }} onClick={handleNodeClick} style={props.style} diff --git a/src/components/TreeNode/styles.less b/src/components/TreeNode/styles.less index 6748a73..57292f9 100644 --- a/src/components/TreeNode/styles.less +++ b/src/components/TreeNode/styles.less @@ -35,6 +35,13 @@ border-left: 1px dashed @border-color; } } + + &.dark { + &.is-highlight, + &:hover { + background-color: @highlight-bg-color-dark; + } + } } .@{css-prefix}-node-index { diff --git a/src/themes.less b/src/themes.less index d857ba0..96d100d 100644 --- a/src/themes.less +++ b/src/themes.less @@ -6,7 +6,7 @@ @color-info: #1d8ce0; @color-error: #ff4949; @color-success: #13ce66; -@color-nil: #D55FDE; +@color-nil: #d55fde; /* field values color */ @color-string: @color-success; @@ -17,6 +17,7 @@ /* highlight */ @highlight-bg-color: #e6f7ff; +@highlight-bg-color-dark: #2e4558; /* comment */ @comment-color: #bfcbd9;