mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 06:15:36 +00:00
Resolve "Introduce copy/paste functionality"
This commit is contained in:
parent
f160dae1b0
commit
6e80cbc7a9
7 changed files with 176 additions and 42 deletions
|
@ -23,3 +23,4 @@
|
|||
* Use Django REST framework status code constants instead of integers.
|
||||
* Added long text field.
|
||||
* Fixed not refreshing token bug and improved authentication a little bit.
|
||||
* Introduced copy, paste and delete functionality of selected fields.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
<script>
|
||||
import { isElement } from '@baserow/modules/core/utils/dom'
|
||||
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
|
||||
|
||||
export default {
|
||||
name: 'GridViewField',
|
||||
|
@ -105,37 +106,75 @@ export default {
|
|||
}
|
||||
document.body.addEventListener('click', this.$el.clickOutsideEvent)
|
||||
|
||||
// If the tab or arrow keys are pressed we want to select the next field. This
|
||||
// is however out of the scope of this component so we emit the selectNext
|
||||
// event that the GridView can handle.
|
||||
this.$el.keyPressedNextFieldEvent = (event) => {
|
||||
// We will first ask if we can select the next field. If that is not allowed
|
||||
// we don't do anything.
|
||||
if (!this.$refs.field.canSelectNext(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { keyCode } = event
|
||||
// Event that is called when a key is pressed while the field is selected.
|
||||
this.$el.keyDownEvent = (event) => {
|
||||
// If the tab or arrow keys are pressed we want to select the next field. This
|
||||
// is however out of the scope of this component so we emit the selectNext
|
||||
// event that the GridView can handle.
|
||||
const { keyCode, ctrlKey, metaKey } = event
|
||||
const arrowKeysMapping = {
|
||||
37: 'selectPrevious',
|
||||
38: 'selectAbove',
|
||||
39: 'selectNext',
|
||||
40: 'selectBelow',
|
||||
}
|
||||
if (Object.keys(arrowKeysMapping).includes(keyCode.toString())) {
|
||||
if (
|
||||
Object.keys(arrowKeysMapping).includes(keyCode.toString()) &&
|
||||
this.$refs.field.canSelectNext(event)
|
||||
) {
|
||||
event.preventDefault()
|
||||
this.$emit(arrowKeysMapping[keyCode])
|
||||
}
|
||||
|
||||
if (keyCode === 9) {
|
||||
if (keyCode === 9 && this.$refs.field.canSelectNext(event)) {
|
||||
event.preventDefault()
|
||||
this.$emit(event.shiftKey ? 'selectPrevious' : 'selectNext')
|
||||
}
|
||||
|
||||
// Copies the value to the clipboard if ctrl/cmd + c is pressed.
|
||||
if (
|
||||
(ctrlKey || metaKey) &&
|
||||
keyCode === 67 &&
|
||||
this.$refs.field.canCopy(event)
|
||||
) {
|
||||
const rawValue = this.row['field_' + this.field.id]
|
||||
const value = this.$registry
|
||||
.get('field', this.field.type)
|
||||
.prepareValueForCopy(this.field, rawValue)
|
||||
copyToClipboard(value)
|
||||
}
|
||||
|
||||
// Removes the value if the backspace/delete key is pressed.
|
||||
if (
|
||||
(keyCode === 46 || keyCode === 8) &&
|
||||
this.$refs.field.canEmpty(event)
|
||||
) {
|
||||
event.preventDefault()
|
||||
const value = this.$registry
|
||||
.get('field', this.field.type)
|
||||
.getEmptyValue(this.field)
|
||||
const oldValue = this.row['field_' + this.field.id]
|
||||
if (value !== oldValue) {
|
||||
this.update(value, oldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
document.body.addEventListener(
|
||||
'keydown',
|
||||
this.$el.keyPressedNextFieldEvent
|
||||
)
|
||||
document.body.addEventListener('keydown', this.$el.keyDownEvent)
|
||||
|
||||
// Updates the value of the field when a user pastes something in the field.
|
||||
this.$el.pasteEvent = (event) => {
|
||||
if (!this.$refs.field.canPaste(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = this.$registry
|
||||
.get('field', this.field.type)
|
||||
.prepareValueForPaste(this.field, event.clipboardData)
|
||||
const oldValue = this.row['field_' + this.field.id]
|
||||
if (value !== oldValue) {
|
||||
this.update(value, oldValue)
|
||||
}
|
||||
}
|
||||
document.addEventListener('paste', this.$el.pasteEvent)
|
||||
|
||||
// Emit the selected event so that the parent component can take an action like
|
||||
// making sure that the element fits in the viewport.
|
||||
|
@ -152,11 +191,10 @@ export default {
|
|||
this.selected = false
|
||||
})
|
||||
document.body.removeEventListener('click', this.$el.clickOutsideEvent)
|
||||
document.body.removeEventListener(
|
||||
'keydown',
|
||||
this.$el.keyPressedNextFieldEvent
|
||||
)
|
||||
document.body.removeEventListener('keydown', this.$el.keyDownEvent)
|
||||
document.removeEventListener('paste', this.$el.pasteEvent)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
gl
|
||||
|
|
|
@ -116,6 +116,24 @@ export class FieldType extends Registerable {
|
|||
toHumanReadableString(field, value) {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is called before the field's value is copied to the clipboard.
|
||||
* Optionally formatting can be done here. By default the value is always
|
||||
* converted to a string.
|
||||
*/
|
||||
prepareValueForCopy(field, value) {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is called before the field's value is overwritten by the clipboard
|
||||
* data. That data might needs to be prepared so that the field accepts it.
|
||||
* By default the text value if the clipboard data is used.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
return clipboardData.getData('text')
|
||||
}
|
||||
}
|
||||
|
||||
export class TextFieldType extends FieldType {
|
||||
|
@ -194,6 +212,36 @@ export class NumberFieldType extends FieldType {
|
|||
getRowEditFieldComponent() {
|
||||
return RowEditFieldNumber
|
||||
}
|
||||
|
||||
/**
|
||||
* First checks if the value is numeric, if that is the case, the number is going
|
||||
* to be formatted.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text')
|
||||
if (isNaN(parseFloat(value)) || !isFinite(value)) {
|
||||
return null
|
||||
}
|
||||
return this.constructor.formatNumber(field, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the value based on the field's settings. The number will be rounded
|
||||
* if to much decimal places are provided and if negative numbers aren't allowed
|
||||
* they will be set to 0.
|
||||
*/
|
||||
static formatNumber(field, value) {
|
||||
if (value === '' || isNaN(value) || value === undefined || value === null) {
|
||||
return null
|
||||
}
|
||||
const decimalPlaces =
|
||||
field.number_type === 'DECIMAL' ? field.number_decimal_places : 0
|
||||
let number = parseFloat(value)
|
||||
if (!field.number_negative && number < 0) {
|
||||
number = 0
|
||||
}
|
||||
return number.toFixed(decimalPlaces)
|
||||
}
|
||||
}
|
||||
|
||||
export class BooleanFieldType extends FieldType {
|
||||
|
@ -220,4 +268,14 @@ export class BooleanFieldType extends FieldType {
|
|||
getEmptyValue(field) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the clipboard data text contains a string that might indicate if the
|
||||
* value is true.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text').toLowerCase()
|
||||
const allowed = ['1', 'y', 't', 'y', 'yes', 'true', 'on']
|
||||
return allowed.includes(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,5 +56,31 @@ export default {
|
|||
canSelectNext() {
|
||||
return true
|
||||
},
|
||||
/**
|
||||
* If the user presses ctrl/cmd + c while a field is selected, the value is
|
||||
* going to be copied to the clipboard. In some cases, for example when the user
|
||||
* is editing the value, we do not want to copy the value. If false is returned
|
||||
* the value won't be copied.
|
||||
*/
|
||||
canCopy() {
|
||||
return true
|
||||
},
|
||||
/**
|
||||
* If the user presses ctrl/cmd + v while a field is selected, the value is
|
||||
* overwritten with the data of the clipboard. In some cases, for example when the
|
||||
* user is editing the value, we do not want to change the value. If false is
|
||||
* returned the value won't be changed.
|
||||
*/
|
||||
canPaste() {
|
||||
return true
|
||||
},
|
||||
/**
|
||||
* If the user presses delete or backspace while a field is selected, the value is
|
||||
* deleted. In some cases, for example when the user is editing the value, we do
|
||||
* not want to delete the value. If false is returned the value won't be changed.
|
||||
*/
|
||||
canEmpty() {
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -163,5 +163,14 @@ export default {
|
|||
canSaveByPressingEnter(event) {
|
||||
return true
|
||||
},
|
||||
canCopy() {
|
||||
return !this.editing
|
||||
},
|
||||
canPaste() {
|
||||
return !this.editing
|
||||
},
|
||||
canEmpty() {
|
||||
return !this.editing
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { NumberFieldType } from '@baserow/modules/database/fieldTypes'
|
||||
|
||||
/**
|
||||
* This mixin contains some method overrides for validating and formatting the
|
||||
* number field. This mixin is used in both the GridViewFieldNumber and
|
||||
|
@ -21,28 +23,11 @@ export default {
|
|||
return this.getError() === null
|
||||
},
|
||||
/**
|
||||
* Formats the value based on the field's settings. The number will be rounded
|
||||
* if to much decimal places are provided and if negative numbers aren't allowed
|
||||
* they will be set to 0.
|
||||
* Before the numeric value is saved we might need to do some formatting such that
|
||||
* the value is conform the fields requirements.
|
||||
*/
|
||||
beforeSave(value) {
|
||||
if (
|
||||
value === '' ||
|
||||
isNaN(value) ||
|
||||
value === undefined ||
|
||||
value === null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const decimalPlaces =
|
||||
this.field.number_type === 'DECIMAL'
|
||||
? this.field.number_decimal_places
|
||||
: 0
|
||||
let number = parseFloat(value)
|
||||
if (!this.field.number_negative && number < 0) {
|
||||
number = 0
|
||||
}
|
||||
return number.toFixed(decimalPlaces)
|
||||
return NumberFieldType.formatNumber(this.field, value)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
17
web-frontend/modules/database/utils/clipboard.js
Normal file
17
web-frontend/modules/database/utils/clipboard.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Copies the given text to the clipboard by temporarily creating a textarea and
|
||||
* using the documents `copy` command.
|
||||
*/
|
||||
export const copyToClipboard = (text) => {
|
||||
const textarea = document.createElement('textarea')
|
||||
document.body.appendChild(textarea)
|
||||
|
||||
textarea.style.position = 'absolute'
|
||||
textarea.style.left = '-99999px'
|
||||
textarea.style.top = '-99999px'
|
||||
textarea.value = text
|
||||
textarea.select()
|
||||
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
}
|
Loading…
Add table
Reference in a new issue