-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components): add virtual and chat components
- Loading branch information
Showing
12 changed files
with
660 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
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
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,40 @@ | ||
<script lang="ts"> | ||
import { h } from 'vue' | ||
import segment from '@satorijs/element' | ||
import renderChildren from './render' | ||
export default { | ||
name: 'message-content', | ||
props: { | ||
content: { | ||
type: String, | ||
required: true, | ||
}, | ||
}, | ||
setup(props, ctx) { | ||
return () => h('div', { class: 'message-content' }, renderChildren(segment.parse(props.content), ctx)) | ||
}, | ||
} | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
.message-content { | ||
white-space: break-spaces; | ||
line-height: 1.5; | ||
position: relative; | ||
:deep(img), :deep(audio), :deep(video) { | ||
display: block; | ||
max-height: 320px; | ||
max-width: 100%; | ||
} | ||
:deep(p) { | ||
margin: 0; | ||
} | ||
} | ||
</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,4 @@ | ||
import ChatInput from './input.vue' | ||
import MessageContent from './content.vue' | ||
|
||
export { ChatInput, MessageContent } |
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,96 @@ | ||
<template> | ||
<div class="textarea"> | ||
<input | ||
autocomplete="off" | ||
step="any" | ||
:value="text" | ||
:placeholder="placeholder" | ||
@input="onInput" | ||
@paste="onPaste" | ||
@keydown.enter.stop="onEnter" | ||
/> | ||
</div> | ||
</template> | ||
|
||
<script lang="ts" setup> | ||
import { computed } from 'vue' | ||
import { useEventListener } from '@vueuse/core' | ||
import segment from '@satorijs/element' | ||
const emit = defineEmits(['send', 'update:modelValue']) | ||
const props = withDefaults(defineProps<{ | ||
target?: HTMLElement | Document | ||
modelValue?: string | ||
placeholder?: string | ||
}>(), { | ||
target: () => document, | ||
modelValue: '', | ||
}) | ||
const text = computed({ | ||
get: () => props.modelValue, | ||
set: (value) => emit('update:modelValue', value), | ||
}) | ||
function onEnter() { | ||
if (!text.value) return | ||
emit('send', segment.escape(text.value)) | ||
text.value = '' | ||
} | ||
function onInput(event: Event) { | ||
text.value = (event.target as HTMLInputElement).value | ||
} | ||
function handleDataTransfer(event: Event, transfer: DataTransfer) { | ||
for (const item of transfer.items) { | ||
if (item.kind !== 'file') continue | ||
event.preventDefault() | ||
const file = item.getAsFile() | ||
const [type] = file.type.split('/', 1) | ||
if (!['image', 'audio', 'video'].includes(type)) { | ||
console.warn('Unsupported file type:', file.type) | ||
return | ||
} | ||
const reader = new FileReader() | ||
reader.addEventListener('load', function () { | ||
emit('send', segment(type, { url: reader.result }).toString()) | ||
}, false) | ||
reader.readAsDataURL(file) | ||
} | ||
} | ||
useEventListener(props.target, 'drop', (event: DragEvent) => { | ||
handleDataTransfer(event, event.dataTransfer) | ||
}) | ||
useEventListener(props.target, 'dragover', (event: DragEvent) => { | ||
event.preventDefault() | ||
}) | ||
async function onPaste(event: ClipboardEvent) { | ||
handleDataTransfer(event, event.clipboardData) | ||
} | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
input { | ||
padding: 0; | ||
width: 100%; | ||
border: none; | ||
outline: none; | ||
font-size: 1em; | ||
height: inherit; | ||
color: inherit; | ||
display: inline-block; | ||
transition: 0.3s ease; | ||
box-sizing: border-box; | ||
background-color: transparent; | ||
} | ||
</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,30 @@ | ||
import segment from '@satorijs/element' | ||
import { FunctionalComponent, h } from 'vue' | ||
|
||
const inline = ['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'del', 'code'] | ||
|
||
const render: FunctionalComponent<segment[]> = (elements, ctx) => { | ||
return elements.map(({ type, attrs, children }) => { | ||
if (type === 'text') { | ||
return attrs.content | ||
} else if (type === 'at') { | ||
return h('span', `@${attrs.name}`) | ||
} else if (type === 'image') { | ||
return h('img', { src: attrs.url }) | ||
} else if (type === 'audio') { | ||
return h('audio', { src: attrs.url, controls: true }) | ||
} else if (type === 'video') { | ||
return h('video', { src: attrs.url, controls: true }) | ||
} else if (inline.includes(type)) { | ||
return h(type, render(children, ctx)) | ||
} else if (type === 'spl') { | ||
return h('span', { class: 'spoiler' }, render(children, ctx)) | ||
} else if (type === 'p' || type === 'message') { | ||
return h('p', render(children, ctx)) | ||
} else { | ||
return render(children, ctx) | ||
} | ||
}) | ||
} | ||
|
||
export default render |
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
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 |
Oops, something went wrong.