-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(client): add components package
- Loading branch information
Showing
13 changed files
with
679 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { App } from 'vue' | ||
import form from 'schemastery-vue' | ||
|
||
export { form as SchemaBase } | ||
|
||
export * from 'schemastery-vue' | ||
|
||
export default function (app: App) { | ||
app.use(form) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.k-link { | ||
cursor: pointer; | ||
text-decoration: underline; | ||
|
||
&:hover { | ||
text-decoration: underline; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { App } from 'vue' | ||
import form from './form' | ||
import virtual from './virtual' | ||
import Comment from './k-comment.vue' | ||
|
||
import './index.scss' | ||
|
||
export * from 'cosmokit' | ||
export * from './form' | ||
export * from './virtual' | ||
|
||
export default function (app: App) { | ||
app.use(form) | ||
app.use(virtual) | ||
app.component('k-comment', Comment) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
<template> | ||
<div class="k-comment" :class="type === 'error' ? 'danger' : type"> | ||
<k-icon :name="icon"></k-icon> | ||
<slot> | ||
<p>{{ title }}</p> | ||
</slot> | ||
</div> | ||
</template> | ||
|
||
<script lang="ts" setup> | ||
import { computed } from 'vue' | ||
const props = defineProps({ | ||
type: { type: String, default: 'primary' }, | ||
title: { type: String, required: false }, | ||
}) | ||
const icon = computed(() => { | ||
switch (props.type) { | ||
case 'success': return 'check-full' | ||
case 'error': return 'times-full' | ||
case 'warning': return 'exclamation-full' | ||
default: return 'info-full' | ||
} | ||
}) | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
@mixin apply-color($name) { | ||
&.#{$name} { | ||
border-left-color: var(--k-color-#{$name}); | ||
background-color: var(--k-color-#{$name}-fade); | ||
.k-icon { | ||
color: var(--k-color-#{$name}); | ||
} | ||
} | ||
} | ||
.k-comment { | ||
padding: 1px 1.5rem; | ||
margin: 2rem 0; | ||
border-left-width: 4px; | ||
border-left-style: solid; | ||
position: relative; | ||
line-height: 1.7; | ||
transition: 0.3s ease; | ||
font-weight: 500; | ||
> .k-icon { | ||
position: absolute; | ||
top: 1.25em; | ||
left: -12px; | ||
width: 20px; | ||
height: 19px; | ||
border-radius: 100%; | ||
background-color: var(--k-card-bg); | ||
} | ||
@include apply-color(primary); | ||
@include apply-color(secondary); | ||
@include apply-color(warning); | ||
@include apply-color(success); | ||
@include apply-color(danger); | ||
& + & { | ||
margin-top: -1rem; | ||
} | ||
:deep(a) { | ||
text-decoration: underline; | ||
&:hover { | ||
text-decoration: underline; | ||
} | ||
} | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"compilerOptions": { | ||
"rootDir": ".", | ||
"target": "es2022", | ||
"module": "esnext", | ||
"declaration": true, | ||
"jsx": "preserve", | ||
"noEmit": true, | ||
"composite": true, | ||
"incremental": true, | ||
"skipLibCheck": true, | ||
"esModuleInterop": true, | ||
"moduleResolution": "node", | ||
"strictBindCallApply": true, | ||
"types": [ | ||
"@cordisjs/client/global", | ||
], | ||
}, | ||
"include": [ | ||
".", | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { App } from 'vue' | ||
import VirtualList from './list.vue' | ||
|
||
export { VirtualList } | ||
|
||
export default function (app: App) { | ||
app.component('virtual-list', VirtualList) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Comment, defineComponent, Directive, Fragment, h, Ref, ref, Text, VNode, watch, withDirectives } from 'vue' | ||
|
||
export const useRefDirective = (ref: Ref): Directive<Element> => ({ | ||
mounted(el) { | ||
ref.value = el | ||
}, | ||
updated(el) { | ||
ref.value = el | ||
}, | ||
beforeUnmount() { | ||
ref.value = null | ||
}, | ||
}) | ||
|
||
function findFirstLegitChild(node: VNode[]): VNode { | ||
if (!node) return null | ||
for (const child of node) { | ||
if (typeof child === 'object') { | ||
switch (child.type) { | ||
case Comment: | ||
continue | ||
case Text: | ||
break | ||
case Fragment: | ||
return findFirstLegitChild(child.children as VNode[]) | ||
default: | ||
if (typeof child.type === 'string') return child | ||
return child | ||
} | ||
} | ||
return h('span', child) | ||
} | ||
} | ||
|
||
const VirtualItem = defineComponent({ | ||
props: { | ||
class: {}, | ||
}, | ||
|
||
emits: ['resize'], | ||
|
||
setup(props, { attrs, slots, emit }) { | ||
let resizeObserver: ResizeObserver | ||
const root = ref<HTMLElement>() | ||
|
||
watch(root, (value) => { | ||
resizeObserver?.disconnect() | ||
if (!value) return | ||
|
||
resizeObserver = new ResizeObserver(dispatchSizeChange) | ||
resizeObserver.observe(value) | ||
}) | ||
|
||
function dispatchSizeChange() { | ||
if (!root.value) return | ||
const marginTop = +(getComputedStyle(root.value).marginTop.slice(0, -2)) | ||
emit('resize', root.value.offsetHeight + marginTop) | ||
} | ||
|
||
const directive = useRefDirective(root) | ||
|
||
return () => { | ||
const head = findFirstLegitChild(slots.default?.(attrs)) | ||
return withDirectives(head, [[directive]]) | ||
} | ||
}, | ||
}) | ||
|
||
export default VirtualItem |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
<template> | ||
<el-scrollbar ref="root" @scroll="onScroll" :max-height="maxHeight"> | ||
<virtual-item v-if="$slots.header" @resize="virtual.saveSize('header', $event)"> | ||
<div><slot name="header"></slot></div> | ||
</virtual-item> | ||
<component :is="tag" class="virtual-list-wrapper" :style="wrapperStyle"> | ||
<virtual-item v-for="(item, index) in dataShown" | ||
@resize="virtual.saveSize(getKey(item), $event)"> | ||
<slot v-bind="item" :index="index + range.start"></slot> | ||
</virtual-item> | ||
</component> | ||
<virtual-item v-if="$slots.footer" @resize="virtual.saveSize('footer', $event)"> | ||
<div><slot name="footer"></slot></div> | ||
</virtual-item> | ||
<div ref="shepherd"></div> | ||
</el-scrollbar> | ||
</template> | ||
|
||
<script lang="ts" setup> | ||
import { ref, computed, watch, nextTick, onActivated, onMounted, PropType } from 'vue' | ||
import type { ElScrollbar } from 'element-plus' | ||
import Virtual from './virtual' | ||
import VirtualItem from './item' | ||
const emit = defineEmits(['item-click', 'scroll', 'top', 'bottom', 'update:activeKey']) | ||
const props = defineProps({ | ||
keyName: { type: String, default: 'id' }, | ||
data: { type: Array, required: true }, | ||
count: { default: 50 }, | ||
estimated: { default: 50 }, | ||
tag: { default: 'div' }, | ||
pinned: Boolean, | ||
activeKey: { default: '' }, | ||
threshold: { default: 0 }, | ||
maxHeight: String, | ||
activate: { | ||
type: String as PropType<'top' | 'bottom' | 'current'>, | ||
default: 'bottom', | ||
}, | ||
}) | ||
const dataShown = computed<any[]>(() => props.data.slice(range.start, range.end)) | ||
const root = ref<typeof ElScrollbar>() | ||
watch(() => props.data.length, () => { | ||
const { scrollTop, clientHeight, scrollHeight } = root.value.wrapRef | ||
if (!props.pinned || Math.abs(scrollTop + clientHeight - scrollHeight) < 1) { | ||
nextTick(scrollToBottom) | ||
} | ||
virtual.updateUids(getUids()) | ||
virtual.handleDataChange() | ||
}) | ||
watch(() => props.activeKey, (value) => { | ||
if (!value) return | ||
emit('update:activeKey', null) | ||
scrollToUid(value, true) | ||
}) | ||
const shepherd = ref<HTMLElement>() | ||
const wrapperStyle = computed(() => { | ||
const { padFront, padBehind } = range | ||
return { padding: `${padFront}px 0px ${padBehind}px` } | ||
}) | ||
const virtual = new Virtual({ | ||
count: props.count, | ||
estimated: props.estimated, | ||
buffer: Math.floor(props.count / 3), | ||
uids: getUids(), | ||
}) | ||
const range = virtual.range | ||
function getUids() { | ||
return props.data.map(getKey) | ||
} | ||
function getKey(item: string) { | ||
const keys = props.keyName.split('.') | ||
return keys.reduce((obj, key) => obj[key], item) | ||
} | ||
onMounted(() => { | ||
if (props.activeKey) { | ||
scrollToUid(props.activeKey) | ||
} else { | ||
scrollToBottom() | ||
} | ||
}) | ||
function scrollToOffset(offset: number, smooth = false) { | ||
if (smooth) { | ||
root.value.wrapRef.scrollTo({ top: offset, behavior: 'smooth' }) | ||
} else { | ||
root.value.wrapRef.scrollTop = offset | ||
} | ||
} | ||
// set current scroll position to a expectant index | ||
function scrollToUid(uid: string, smooth = false) { | ||
scrollToOffset(virtual.getUidOffset(uid), smooth) | ||
} | ||
function scrollToBottom() { | ||
if (shepherd.value) { | ||
const offset = shepherd.value.offsetTop | ||
scrollToOffset(offset) | ||
// check if it's really scrolled to the bottom | ||
// maybe list doesn't render and calculate to last range | ||
// so we need retry in next event loop until it really at bottom | ||
setTimeout(() => { | ||
const offset = Math.ceil(root.value.wrapRef.scrollTop) | ||
const clientLength = Math.ceil(root.value.wrapRef.clientHeight) | ||
const scrollLength = Math.ceil(root.value.wrapRef.scrollHeight) | ||
if (offset + clientLength < scrollLength) { | ||
scrollToBottom() | ||
} | ||
}, 3) | ||
} | ||
} | ||
let scrollTop = 0 | ||
onActivated(() => { | ||
if (props.activate === 'bottom') { | ||
scrollToBottom() | ||
} else if (props.activate === 'current') { | ||
root.value.setScrollTop(scrollTop) | ||
} | ||
}) | ||
function onScroll(ev: MouseEvent) { | ||
const offset = Math.ceil(scrollTop = root.value.wrapRef.scrollTop) | ||
const clientLength = Math.ceil(root.value.wrapRef.clientHeight) | ||
const scrollLength = Math.ceil(root.value.wrapRef.scrollHeight) | ||
// iOS scroll-spring-back behavior will make direction mistake | ||
if (offset < 0 || (offset + clientLength > scrollLength + 1) || !scrollLength) { | ||
return | ||
} | ||
virtual.handleScroll(offset) | ||
emitEvent(offset, clientLength, scrollLength, ev) | ||
} | ||
function emitEvent(offset: number, clientLength: number, scrollLength: number, ev: MouseEvent) { | ||
emit('scroll', ev, virtual.range) | ||
if (virtual.direction < 0 && !!props.data.length && (offset - props.threshold <= 0)) { | ||
emit('top') | ||
} else if (virtual.direction > 0 && (offset + clientLength + props.threshold >= scrollLength)) { | ||
emit('bottom') | ||
} | ||
} | ||
</script> |
Oops, something went wrong.