mirror of https://github.com/docusealco/docuseal
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
5.0 KiB
207 lines
5.0 KiB
<template>
|
|
<div
|
|
class="group/contenteditable relative overflow-visible"
|
|
:class="{ 'flex items-center': !iconInline }"
|
|
>
|
|
<span
|
|
ref="contenteditable"
|
|
dir="auto"
|
|
:contenteditable="editable && (!editableOnButton || isEditable)"
|
|
:data-placeholder="placeholder"
|
|
:data-empty="isEmpty"
|
|
:style="{ minWidth }"
|
|
:class="[iconInline ? (isEmpty ? 'inline-block' : 'inline') : 'block', hideIcon ? 'focus:block' : '']"
|
|
class="peer relative inline-block outline-none before:pointer-events-none before:absolute before:left-0 before:top-0 before:select-none before:whitespace-pre before:text-neutral-400 before:content-[attr(data-placeholder)] before:opacity-0 data-[empty=true]:before:opacity-100"
|
|
@paste.prevent="onPaste"
|
|
@keydown.enter.prevent="blurContenteditable"
|
|
@input="updateInputValue"
|
|
@cut="updateInputValue"
|
|
@focus="$emit('focus', $event)"
|
|
@blur="onBlur"
|
|
@click="editable && (!editableOnButton || isEditable) ? '' : $emit('click-contenteditable')"
|
|
>
|
|
{{ value }}
|
|
</span>
|
|
<span
|
|
v-if="withButton"
|
|
class="relative inline"
|
|
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
|
|
>
|
|
<IconPencil
|
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle pl-1"
|
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
|
:title="t('edit')"
|
|
:class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }"
|
|
:width="iconWidth + 4"
|
|
:stroke-width="iconStrokeWidth"
|
|
@click="clickEdit"
|
|
/>
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { IconPencil } from '@tabler/icons-vue'
|
|
|
|
export default {
|
|
name: 'ContenteditableField',
|
|
components: {
|
|
IconPencil
|
|
},
|
|
inject: ['t'],
|
|
props: {
|
|
modelValue: {
|
|
type: String,
|
|
required: false,
|
|
default: ''
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
required: false,
|
|
default: ''
|
|
},
|
|
withButton: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true
|
|
},
|
|
iconInline: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
iconWidth: {
|
|
type: Number,
|
|
required: false,
|
|
default: 30
|
|
},
|
|
hideIcon: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true
|
|
},
|
|
floatIcon: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
selectOnEditClick: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
editableOnButton: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
minWidth: {
|
|
type: String,
|
|
required: false,
|
|
default: '2px'
|
|
},
|
|
editable: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true
|
|
},
|
|
iconStrokeWidth: {
|
|
type: Number,
|
|
required: false,
|
|
default: 2
|
|
}
|
|
},
|
|
emits: ['update:model-value', 'focus', 'blur', 'click-contenteditable'],
|
|
data () {
|
|
return {
|
|
isEditable: false,
|
|
inputValue: '',
|
|
value: ''
|
|
}
|
|
},
|
|
computed: {
|
|
isEmpty () {
|
|
return !this.inputValue.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim()
|
|
}
|
|
},
|
|
watch: {
|
|
modelValue: {
|
|
handler (value) {
|
|
this.value = value || ''
|
|
},
|
|
immediate: true
|
|
}
|
|
},
|
|
mounted () {
|
|
this.updateInputValue()
|
|
},
|
|
methods: {
|
|
updateInputValue () {
|
|
this.inputValue = this.$refs.contenteditable?.textContent || ''
|
|
},
|
|
onPaste (e) {
|
|
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
|
|
|
const selection = this.$el.getRootNode().getSelection()
|
|
|
|
if (selection.rangeCount) {
|
|
selection.deleteFromDocument()
|
|
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
|
selection.collapseToEnd()
|
|
}
|
|
|
|
this.updateInputValue()
|
|
},
|
|
clickEdit (e) {
|
|
this.focusContenteditable()
|
|
|
|
if (this.selectOnEditClick) {
|
|
this.selectContent()
|
|
}
|
|
},
|
|
setText (text) {
|
|
this.$refs.contenteditable.innerText = text
|
|
|
|
this.updateInputValue()
|
|
},
|
|
selectContent () {
|
|
const el = this.$refs.contenteditable
|
|
|
|
const range = document.createRange()
|
|
|
|
range.selectNodeContents(el)
|
|
|
|
const sel = window.getSelection()
|
|
|
|
sel.removeAllRanges()
|
|
|
|
sel.addRange(range)
|
|
},
|
|
onBlur (e) {
|
|
setTimeout(() => {
|
|
if (this.$refs.contenteditable) {
|
|
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
|
this.$emit('update:model-value', this.value)
|
|
}
|
|
|
|
this.$emit('blur', e)
|
|
|
|
this.isEditable = false
|
|
}, 1)
|
|
},
|
|
focusContenteditable () {
|
|
this.isEditable = true
|
|
|
|
this.$nextTick(() => {
|
|
this.$refs.contenteditable.focus()
|
|
|
|
this.updateInputValue()
|
|
})
|
|
},
|
|
blurContenteditable () {
|
|
this.$refs.contenteditable.blur()
|
|
}
|
|
}
|
|
}
|
|
</script>
|