Browse Source

feat: formula autocomplete

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/448/head
Pranav C 3 years ago committed by Pranav C
parent
commit
b3910d30da
  1. 139
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  2. 35
      packages/nc-gui/helpers/index.js

139
packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue

@ -1,22 +1,67 @@
<template>
<div>
<!-- todo: autocomplete based on available functions and metadata -->
<v-text-field
v-model="formula.value"
dense
outlined
class="caption"
hide-details="auto"
label="Formula"
persistent-hint
hint="Available formulas are ADD, AVG, CONCAT, +, -, /"
:rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]"
/>
<div class="formula-wrapper">
<v-menu
v-model="autocomplete"
bottom
offset-y
nudge-bottom="-25px"
allow-overflow
>
<template #activator>
<!-- todo: autocomplete based on available functions and metadata -->
<v-text-field
ref="input"
v-model="formula.value"
dense
outlined
class="caption"
hide-details="auto"
label="Formula"
persistent-hint
hint="Available formulas are ADD, AVG, CONCAT, +, -, /"
:rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]"
autocomplete="off"
@input="handleInputDeb"
/>
</template>
<!-- <div class="nc-autocomplete">-->
<v-list v-if="suggestion" dense max-height="50vh">
<v-list-item
v-for="it in suggestion"
:key="it"
dense
@mousedown.prevent="appendText(it)"
>
<span class="caption">{{ it }}</span>
</v-list-item>
</v-list>
<!-- <div>-->
<!-- <h1>Auto-complete...</h1>-->
<!-- <div-->
<!-- ref="input"-->
<!-- contenteditable="true"-->
<!-- @input="handleInputDeb"-->
<!-- />-->
<!-- <div ref="time" />-->
<!-- <div ref="fakeDiv" class="div" />-->
<!-- &lt;!&ndash; </div>&ndash;&gt;-->
<!-- </div>-->
</v-menu>
<pre class="caption">
{{ suggestion }}
</pre>
</div>
</template>
<script>
import NcAutocompleteTree from '@/help/NcAutocompleteTree'
import { insertAtCursor } from '@/helpers'
import debounce from 'debounce'
import jsep from 'jsep'
export default {
@ -26,8 +71,26 @@ export default {
formula: {},
// formulas: ['AVERAGE()', 'COUNT()', 'COUNTA()', 'COUNTALL()', 'SUM()', 'MIN()', 'MAX()', 'AND()', 'OR()', 'TRUE()', 'FALSE()', 'NOT()', 'XOR()', 'ISERROR()', 'IF()', 'LEN()', 'MID()', 'LEFT()', 'RIGHT()', 'FIND()', 'CONCATENATE()', 'T()', 'VALUE()', 'ARRAYJOIN()', 'ARRAYUNIQUE()', 'ARRAYCOMPACT()', 'ARRAYFLATTEN()', 'ROUND()', 'ROUNDUP()', 'ROUNDDOWN()', 'INT()', 'EVEN()', 'ODD()', 'MOD()', 'LOG()', 'EXP()', 'POWER()', 'SQRT()', 'CEILING()', 'FLOOR()', 'ABS()', 'RECORD_ID()', 'CREATED_TIME()', 'ERROR()', 'BLANK()', 'YEAR()', 'MONTH()', 'DAY()', 'HOUR()', 'MINUTE()', 'SECOND()', 'TODAY()', 'NOW()', 'WORKDAY()', 'DATETIME_PARSE()', 'DATETIME_FORMAT()', 'SET_LOCALE()', 'SET_TIMEZONE()', 'DATESTR()', 'TIMESTR()', 'TONOW()', 'FROMNOW()', 'DATEADD()', 'WEEKDAY()', 'WEEKNUM()', 'DATETIME_DIFF()', 'WORKDAY_DIFF()', 'IS_BEFORE()', 'IS_SAME()', 'IS_AFTER()', 'REPLACE()', 'REPT()', 'LOWER()', 'UPPER()', 'TRIM()', 'SUBSTITUTE()', 'SEARCH()', 'SWITCH()', 'LAST_MODIFIED_TIME()', 'ENCODE_URL_COMPONENT()', 'REGEX_EXTRACT()', 'REGEX_MATCH()', 'REGEX_REPLACE()']
availableFunctions: ['AVG', 'ADD', 'CONCAT'],
availableBinOps: ['+', '-', '*', '/']
availableBinOps: ['+', '-', '*', '/'],
autocomplete: true,
suggestion: null
}),
computed: {
suggestionsList() {
return [
...this.meta.columns.map(c => ({ text: c.cn, type: 'column', c })),
...this.availableFunctions.map(fn => ({ text: fn, type: 'function' })),
...this.availableBinOps.map(op => ({ text: op, type: 'op' }))
]
},
acTree() {
const ref = new NcAutocompleteTree()
for (const sug of this.suggestionsList) {
ref.add(sug.text)
}
return ref
}
},
created() {
this.formula = this.value ? { ...this.value } : {}
},
@ -121,11 +184,55 @@ export default {
this.validateAgainstMeta(pt.right, arr)
}
return arr
},
appendText(it) {
if (it.type === 'function') {
insertAtCursor(this.$refs.input.$el.querySelector('input'), it.text + '()', it.text.length + 1)
} else {
insertAtCursor(this.$refs.input.$el.querySelector('input'), it.text)
}
},
_handleInputDeb: debounce(async function(self) {
await self.handleInput()
}, 250),
handleInputDeb() {
this._handleInputDeb(this)
},
handleInput() {
this.autocomplete = true
// const $fakeDiv = this.$refs.fakeDiv
this.suggestion = null
const query = this.formula.value
if (query !== '') {
const parts = query.split(' ')
const wordToComplete = parts.pop()
if (wordToComplete !== '') {
// get best match using popularity
this.suggestion = this.acTree.complete(wordToComplete)
} else {
// $span.textContent = '' // clear ghost span
}
} else {
// $time.textContent = ''
// $span.textContent = '' // clear ghost span
}
}
}
}
</script>
<style scoped>
<style scoped lang="scss">
//.formula-wrapper {
// overflow: visible;
// position: relative;
//
// .nc-autocomplete {
// position: absolute;
// //top: 100%;
// top:0;
// left: 0
// }
//}
</style>

35
packages/nc-gui/helpers/index.js

@ -12,6 +12,41 @@ export const isValidURL = (str) => {
}
export const parseIfInteger = v => /^\d+$/.test(v) ? +v : v
// ref : https://stackoverflow.com/a/11077016
export function insertAtCursor(myField, myValue, len) {
// IE support
if (document.selection) {
myField.focus()
const sel = document.selection.createRange()
sel.text = myValue
}
// MOZILLA and others
else if (myField.selectionStart || myField.selectionStart == '0') {
const startPos = myField.selectionStart
const endPos = myField.selectionEnd
myField.value = myField.value.substring(0, startPos) +
myValue +
myField.value.substring(endPos, myField.value.length)
const pos = +startPos + (len ?? myValue.length)
// https://stackoverflow.com/a/4302688
if (myField.setSelectionRange) {
myField.focus()
myField.setSelectionRange(pos, pos)
} else if (myField.createTextRange) {
const range = myField.createTextRange()
range.collapse(true)
range.moveEnd('character', pos)
range.moveStart('character', pos)
range.select()
}
} else {
myField.value += myValue
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

Loading…
Cancel
Save