Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions src/components/config-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
NSwitch,
NText,
} from 'naive-ui'
import { isVNode } from 'vue'
import type { ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'

import { useStoreRef } from '~/hooks/use-store-ref'
Expand Down Expand Up @@ -187,7 +186,7 @@ const SchemaSection = defineComponent({
const { definitions, getKey } = inject(JSONSchemaFormInjectKey, {} as any)

return () => {
const { schema, formData, dataKey: key, property } = props
const { schema, formData, dataKey: key } = props

if (!schema) {
return null
Expand All @@ -198,6 +197,33 @@ const SchemaSection = defineComponent({
{Object.keys(schema.properties).map((property) => {
const current = schema.properties[property]

// Check conditional visibility
const uiOptions = current?.['ui:options'] || {}
if (uiOptions.dependsOn) {
const dependsOn = Array.isArray(uiOptions.dependsOn)
? uiOptions.dependsOn
: [uiOptions.dependsOn]

// Check all conditions (AND logic)
const allConditionsMet = dependsOn.every((condition) => {
const { field, value: expectedValue } = condition
const actualValue = get(
formData.value,
`${getKey(key)}.${field}`,
)

if (Array.isArray(expectedValue)) {
return expectedValue.includes(actualValue)
} else {
return actualValue === expectedValue
}
})

if (!allConditionsMet) {
return null
}
}

if (current.$ref) {
const nestSchmea = definitions.value.get(
current.$ref.split('/').at(-1),
Expand Down Expand Up @@ -286,7 +312,7 @@ const ScheamFormItem = defineComponent({
const { type: uiType } = options

switch (uiType) {
case 'select':
case 'select': {
const { values } = options as {
values: { label: string; value: string }[]
}
Expand All @@ -298,8 +324,9 @@ const ScheamFormItem = defineComponent({
}}
options={values}
filterable
></NSelect>
/>
)
}
default:
return (
<NInput
Expand All @@ -321,7 +348,7 @@ const ScheamFormItem = defineComponent({
: undefined
}
clearable
></NInput>
/>
)
}
}
Expand All @@ -332,7 +359,7 @@ const ScheamFormItem = defineComponent({
onUpdateValue={(val) => {
innerValue.value = val
}}
></NDynamicTags>
/>
)
}
case 'boolean': {
Expand All @@ -342,7 +369,7 @@ const ScheamFormItem = defineComponent({
onUpdateValue={(val) => {
innerValue.value = val
}}
></NSwitch>
/>
)
}

Expand Down
36 changes: 36 additions & 0 deletions src/components/editor/codemirror/codemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,40 @@
& .cm-gutter {
@apply !min-w-[3rem];
}

.cm-uploading-widget {
display: inline-block;
font-size: 0.9em;
padding: 0 4px;
user-select: none;
}

.cm-upload-shimmer {
display: inline-block;
background: linear-gradient(
90deg,
#666 0%,
#666 40%,
#3b82f6 50%,
#666 60%,
#666 100%
);
background-size: 200% 100%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shimmer 2s linear infinite;
}

@keyframes shimmer {
0% {
background-position: 100% 0;
}
50% {
background-position: 0% 0;
}
100% {
background-position: 100% 0;
}
}
}
7 changes: 6 additions & 1 deletion src/components/editor/codemirror/codemirror.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable vue/no-setup-props-destructure */
import { useSaveConfirm } from '~/hooks/use-save-confirm'
import { defineComponent } from 'vue'
import type { EditorState } from '@codemirror/state'
import type { PropType } from 'vue'

import { useImageUpload } from '~/hooks/use-image-upload'
import { useSaveConfirm } from '~/hooks/use-save-confirm'

import styles from '../universal/editor.module.css'
import { editorBaseProps } from '../universal/props'

Expand All @@ -24,12 +26,15 @@ export const CodemirrorEditor = defineComponent({
},
},
setup(props, { expose }) {
const { upload } = useImageUpload()

const [refContainer, editorView] = useCodeMirror({
initialDoc: props.text,
onChange: (state) => {
props.onChange(state.doc.toString())
props.onStateChange?.(state)
},
onUploadImage: upload,
})

watch(
Expand Down
91 changes: 89 additions & 2 deletions src/components/editor/codemirror/use-codemirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ import { codemirrorReconfigureExtension } from './extension'
import { syntaxTheme } from './syntax-highlight'
import { useCodeMirrorConfigureFonts } from './use-auto-fonts'
import { useCodeMirrorAutoToggleTheme } from './use-auto-theme'
import {
addUpload,
removeUpload,
uploadStateField,
} from './use-upload-extensions'

interface Props {
initialDoc: string
onChange?: (state: EditorState) => void
onUploadImage?: (file: File) => Promise<string>
}

export const useCodeMirror = <T extends Element>(
Expand All @@ -36,7 +42,7 @@ export const useCodeMirror = <T extends Element>(
const refContainer = ref<T>()
const editorView = ref<EditorView>()
const { general } = useEditorConfig()
const { onChange } = props
const { onChange, onUploadImage } = props

const format = () => {
const ev = editorView.value
Expand Down Expand Up @@ -80,16 +86,97 @@ export const useCodeMirror = <T extends Element>(
})
})
.catch(() => {
console.log('not support wasm')
console.info(
'Editor wasm formatter is not supported by this browser.',
)
})
}
}

const handleImageUpload = (
file: File,
view: EditorView,
insertPos: number,
) => {
if (!onUploadImage) return

const uploadId = crypto.randomUUID()
view.dispatch({
effects: addUpload.of({ id: uploadId, pos: insertPos }),
})

onUploadImage(file)
.then((url) => {
const state = view.state
const decorations = state.field(uploadStateField)
let foundFrom: number | null = null

decorations.between(0, state.doc.length, (from, _to, value) => {
if (value.spec.id === uploadId) {
foundFrom = from
return false // find the decoration by the id set before
}
})

if (foundFrom !== null) {
// replace the widget with the actual image markdown
view.dispatch({
changes: {
from: foundFrom,
insert: `![${file.name}](${url})`,
},
effects: removeUpload.of({ id: uploadId }),
})
}
})
.catch((_err) => {
view.dispatch({
effects: removeUpload.of({ id: uploadId }),
})
})
}

// handle paste event
const handlePaste = (event: ClipboardEvent, view: EditorView) => {
if (!onUploadImage) return

const files = event.clipboardData?.files
if (!files || files.length === 0) return

const file = files[0]
if (!file.type.startsWith('image/')) return

event.preventDefault()
const insertPos = view.state.selection.main.head
handleImageUpload(file, view, insertPos)
}
onMounted(() => {
if (!refContainer.value) return

const startState = EditorState.create({
doc: props.initialDoc,
extensions: [
uploadStateField,

EditorView.domEventHandlers({
paste: (event, view) => handlePaste(event, view),
drop: (event, view) => {
if (!onUploadImage) return

const files = event.dataTransfer?.files
if (!files || files.length === 0) return

const file = files[0]
if (!file.type.startsWith('image/')) return

event.preventDefault()

const pos = view.posAtCoords({ x: event.clientX, y: event.clientY })
if (pos === null) return
view.dispatch({ selection: { anchor: pos } })
handleImageUpload(file, view, pos)
},
}),
keymap.of([
{
key: 'Mod-s',
Expand Down
61 changes: 61 additions & 0 deletions src/components/editor/codemirror/use-upload-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { DecorationSet } from '@codemirror/view'

import { StateEffect, StateField } from '@codemirror/state'
import { Decoration, EditorView, WidgetType } from '@codemirror/view'

export const addUpload = StateEffect.define<{ id: string; pos: number }>()
export const removeUpload = StateEffect.define<{ id: string }>()

class UploadWidget extends WidgetType {
constructor(readonly id: string) {
super()
}

toDOM() {
const container = document.createElement('span')
container.className = 'cm-uploading-widget'

const text = document.createElement('span')
text.className = 'cm-upload-shimmer'
text.textContent = '图片上传中...'

container.appendChild(text)
return container
}

ignoreEvent() {
return false
}
}

export const uploadStateField = StateField.define<DecorationSet>({
create() {
return Decoration.none
},
update(uploads, tr) {
uploads = uploads.map(tr.changes)

for (const effect of tr.effects) {
if (effect.is(addUpload)) {
const decoration = Decoration.widget({
widget: new UploadWidget(effect.value.id),
side: 1, // right side of the position
id: effect.value.id,
})
uploads = uploads.update({
add: [decoration.range(effect.value.pos)],
})
} else if (effect.is(removeUpload)) {
// remove the decoration with the matching id
uploads = uploads.update({
filter: (from, to, value) => {
return value.spec.id !== effect.value.id
},
})
}
}
return uploads
},
// render the decorations
provide: (f) => EditorView.decorations.from(f),
})
Loading