Skip to content

feat: 🎸 migrate image inline to vue #1804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 12, 2025
Merged
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
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
}
],
"import/named": "error",
"import/sort-imports": "error",
"import/order": "error",
"import/no-duplicates": "error",
"import/no-import-assign": "error",
"import/no-self-import": "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { h } from 'vue'
h

type IconProps = {
icon: string | null
icon?: string | null
class?: string
}

Expand Down
163 changes: 163 additions & 0 deletions packages/components/src/__internal__/components/image-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { defineComponent, ref, h, type Ref } from 'vue'
import clsx from 'clsx'
import { customAlphabet } from 'nanoid'
import { Icon } from './icon'

h

const nanoid = customAlphabet('abcdefg', 8)

type ImageInputProps = {
src: Ref<string | undefined>
selected: Ref<boolean>
readonly: Ref<boolean>
setLink: (link: string) => void

imageIcon?: string
uploadButton?: string
confirmButton?: string
uploadPlaceholderText?: string

className?: string

onUpload: (file: File) => Promise<string>
}

export const ImageInput = defineComponent<ImageInputProps>({
props: {
src: {
type: Object,
required: true,
},
selected: {
type: Object,
required: true,
},
readonly: {
type: Object,
required: true,
},
setLink: {
type: Function,
required: true,
},
imageIcon: {
type: String,
required: false,
},
uploadButton: {
type: String,
required: false,
},
confirmButton: {
type: String,
required: false,
},
uploadPlaceholderText: {
type: String,
required: false,
},
onUpload: {
type: Function,
required: true,
},
},
setup({
readonly,
src,
setLink,
onUpload,
imageIcon,
uploadButton,
confirmButton,
uploadPlaceholderText,
className,
}) {
const focusLinkInput = ref(false)
const linkInputRef = ref<HTMLInputElement>()
const currentLink = ref(src.value ?? '')
const uuid = ref(nanoid())
const hidePlaceholder = ref(src.value?.length !== 0)
const onEditLink = (e: Event) => {
const target = e.target as HTMLInputElement
const value = target.value
hidePlaceholder.value = value.length !== 0
currentLink.value = value
}

const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
setLink(linkInputRef.value?.value ?? '')
}
}

const onConfirmLinkInput = () => {
setLink(linkInputRef.value?.value ?? '')
}

const onUploadFile = (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return

onUpload(file)
.then((url) => {
if (!url) return

setLink(url)
hidePlaceholder.value = true
})
.catch((err) => {
console.error('An error occurred while uploading image')
console.error(err)
})
}

return () => {
return (
<div class={clsx('image-edit', className)}>
<Icon icon={imageIcon} class="image-icon" />
<div class={clsx('link-importer', focusLinkInput.value && 'focus')}>
<input
ref={linkInputRef}
draggable="true"
onDragstart={(e) => {
e.preventDefault()
e.stopPropagation()
}}
disabled={readonly.value}
class="link-input-area"
value={currentLink.value}
onInput={onEditLink}
onKeydown={onKeydown}
onFocus={() => (focusLinkInput.value = true)}
onBlur={() => (focusLinkInput.value = false)}
/>
{!hidePlaceholder.value && (
<div class="placeholder">
<input
disabled={readonly.value}
class="hidden"
id={uuid.value}
type="file"
accept="image/*"
onChange={onUploadFile}
/>
<label class="uploader" for={uuid.value}>
<Icon icon={uploadButton} />
</label>
<span class="text" onClick={() => linkInputRef.value?.focus()}>
{uploadPlaceholderText}
</span>
</div>
)}
</div>
{currentLink.value && (
<div class="confirm" onClick={() => onConfirmLinkInput()}>
<Icon icon={confirmButton} />
</div>
)}
</div>
)
}
},
})
8 changes: 4 additions & 4 deletions packages/components/src/image-block/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { $ctx } from '@milkdown/utils'
import { withMeta } from '../__internal__/meta'

export interface ImageBlockConfig {
imageIcon: () => string | null
captionIcon: () => string | null
uploadButton: () => string | null
confirmButton: () => string | null
imageIcon: () => string | undefined
captionIcon: () => string | undefined
uploadButton: () => string | undefined
confirmButton: () => string | undefined
uploadPlaceholderText: string
captionPlaceholderText: string
onUpload: (file: File) => Promise<string>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { h, Fragment, type Ref, defineComponent } from 'vue'
import type { ImageBlockConfig } from '../../config'
import { ImageViewer } from './image-viewer'
import { ImageInput } from './image-input'
import { ImageInput } from '../../../__internal__/components/image-input'

h
Fragment
Expand All @@ -13,8 +13,8 @@ type Attrs = {
}

export type MilkdownImageBlockProps = {
selected: Ref<boolean | undefined>
readonly: Ref<boolean | undefined>
selected: Ref<boolean>
readonly: Ref<boolean>
setAttr: <T extends keyof Attrs>(attr: T, value: Attrs[T]) => void
config: ImageBlockConfig
} & {
Expand Down Expand Up @@ -57,7 +57,19 @@ export const MilkdownImageBlock = defineComponent<MilkdownImageBlockProps>({

return () => {
if (!src.value?.length) {
return <ImageInput {...props} />
return (
<ImageInput
src={props.src}
selected={props.selected}
readonly={props.readonly}
setLink={(link) => props.setAttr('src', link)}
imageIcon={props.config.imageIcon()}
uploadButton={props.config.uploadButton()}
confirmButton={props.config.confirmButton()}
uploadPlaceholderText={props.config.uploadPlaceholderText}
onUpload={props.config.onUpload}
/>
)
}
return <ImageViewer {...props} />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineComponent, ref, h } from 'vue'
import type { MilkdownImageBlockProps } from './image-block'
import clsx from 'clsx'
import { customAlphabet } from 'nanoid'
import { Icon } from '../../../__internal__/icon'
import { Icon } from '../../../__internal__/components/icon'

h

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineComponent, ref, h, Fragment } from 'vue'
import type { MilkdownImageBlockProps } from './image-block'
import { IMAGE_DATA_TYPE } from '../../schema'
import { Icon } from '../../../__internal__/icon'
import { Icon } from '../../../__internal__/components/icon'

h
Fragment
Expand Down
7 changes: 3 additions & 4 deletions packages/components/src/image-block/view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const imageBlockView = $view(
const ratio = ref(initialNode.attrs.ratio)
const selected = ref(false)
const readonly = ref(!view.editable)
const setAttr = (attr: string, value: string) => {
const setAttr = (attr: string, value: unknown) => {
const pos = getPos()
if (pos == null) return
view.dispatch(view.state.tr.setNodeAttribute(pos, attr, value))
Expand All @@ -34,7 +34,7 @@ export const imageBlockView = $view(
const dom = document.createElement('div')
dom.className = 'milkdown-image-block'
app.mount(dom)
const selectedWatcher = watchEffect(() => {
const disposeSelectedWatcher = watchEffect(() => {
const isSelected = selected.value
if (isSelected) {
dom.classList.add('selected')
Expand Down Expand Up @@ -63,7 +63,6 @@ export const imageBlockView = $view(
}

bindAttrs(initialNode)
selected.value = false
return {
dom,
update: (updatedNode) => {
Expand All @@ -84,7 +83,7 @@ export const imageBlockView = $view(
selected.value = false
},
destroy: () => {
selectedWatcher()
disposeSelectedWatcher()
app.unmount()
dom.remove()
},
Expand Down
Loading