<template> <span ref="editable" :contenteditable="editing" :placeholder="placeholder" :class="{ 'forced-user-select-initial': editing, 'whitespace-pre-wrap': multiline, 'editable--multi-line': multiline, }" @input="update" @keydown="keydown" @focusout="change" @paste="paste" >{{ value }}</span > </template> <script> import { focusEnd } from '@baserow/modules/core/utils/dom' export default { name: 'Editable', props: { value: { type: String, required: true, }, placeholder: { type: String, required: false, default: '', }, multiline: { type: Boolean, required: false, default: false, }, }, data() { return { editing: false, oldValue: '', newValue: '', oldTextOverflow: '', } }, watch: { value(value) { this.set(value) }, }, mounted() { this.set(this.value) }, methods: { /** * This method must be called when the is going to be edited. It will enable the * contenteditable state and will focus the element. */ edit() { this.editing = true this.$emit('editing', true) this.$nextTick(() => { focusEnd(this.$refs.editable) // In almost all use cases, the parent has overflow hidden and in that case we // need to see if the scrollLeft must be changed so that we can see the cursor // which has been placed at the end. const parent = this.$el.parentElement parent.scrollLeft = parent.scrollWidth - parent.clientWidth parent.classList.add('forced-text-overflow-initial') }) }, /** * This method is called when the value has changed and needs to be saved. It will * change the editing state and will emit a change event if the new value has * changed. */ change() { this.editing = false this.$emit('editing', false) this.$nextTick(() => { // In almost all use cases, the parent has overflow hidden and it could be that // because of the cursor, the scrollLeft value has changed. Here we change it // back to what is was before. const parent = this.$el.parentElement parent.classList.remove('forced-text-overflow-initial') if (parent.scrollWidth > parent.clientWidth) { parent.scrollLeft = 0 } }) if (this.oldValue === this.newValue) { return } this.$emit('change', { oldValue: this.value, value: this.newValue, }) this.oldValue = this.newValue }, /** * Everytime a key is pressed inside the editable this event will be trigger which * will update the new value. */ update(event) { const target = event.target let text = target.textContent if (this.multiline && target.innerText !== undefined) { // textContent doesn't support new lines // so we need innerText in multiline mode text = target.innerText } this.newValue = text }, /** * When someone pastes something we want to only insert the plain text instead of * the styled content. */ paste(event) { event.preventDefault() const text = (event.originalEvent || event).clipboardData.getData( 'text/plain' ) document.execCommand('insertText', false, text) }, /** * If a key is pressed and it is an enter or esc key the change event will be called * to end the editing and save the value. */ keydown(event) { // Allow users to create new lines with Shift+Enter if (event.key === 'Enter' && event.shiftKey && this.multiline) { return false } if (event.key === 'Enter' || event.key === 'Escape') { event.preventDefault() this.change() return false } }, /** * */ set(value) { this.oldValue = this.value this.newValue = this.value this.$refs.editable.textContent = this.value }, }, } </script>