Browse Source

feat: Simple formula autocompletion

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
df75d6f70f
  1. 104
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  2. 33
      packages/nc-gui/helpers/index.js

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

@ -9,6 +9,8 @@
> >
<template #activator> <template #activator>
<!-- todo: autocomplete based on available functions and metadata --> <!-- todo: autocomplete based on available functions and metadata -->
<v-tooltip color="info">
<template #activator="{on}">
<v-text-field <v-text-field
ref="input" ref="input"
v-model="formula.value" v-model="formula.value"
@ -21,46 +23,45 @@
hint="Available formulas are ADD, AVG, CONCAT, +, -, /" hint="Available formulas are ADD, AVG, CONCAT, +, -, /"
:rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]" :rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]"
autocomplete="off" autocomplete="off"
v-on="on"
@input="handleInputDeb" @input="handleInputDeb"
@keydown.down.prevent="suggestionListDown"
@keydown.up.prevent="suggestionListUp"
@keydown.enter.prevent="selectText"
/> />
</template> </template>
<!-- <div class="nc-autocomplete">--> <span class="caption">Example: AVG(column1, column2, column3)</span>
<v-list v-if="suggestion" dense max-height="50vh"> </v-tooltip>
</template>
<v-list v-if="suggestion" dense max-height="50vh" class="background-color">
<v-list-item-group
v-model="selected"
color="primary"
>
<v-list-item <v-list-item
v-for="it in suggestion" v-for="(it,i) in suggestion"
:key="it" :key="i"
dense dense
selectable
@mousedown.prevent="appendText(it)" @mousedown.prevent="appendText(it)"
> >
<span class="caption">{{ it }}</span> <span
class="caption"
:class="{
'primary--text text--lighten-2 font-weight-bold': it.type ==='function'
}"
>{{ it.text }}<span v-if="it.type ==='function'">(...)</span></span>
</v-list-item> </v-list-item>
</v-list-item-group>
</v-list> </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> </v-menu>
<pre class="caption">
{{ suggestion }}
</pre>
</div> </div>
</template> </template>
<script> <script>
import NcAutocompleteTree from '@/help/NcAutocompleteTree' import NcAutocompleteTree from '@/help/NcAutocompleteTree'
import { insertAtCursor } from '@/helpers' import { getWordUntilCaret, insertAtCursor } from '@/helpers'
import debounce from 'debounce' import debounce from 'debounce'
import jsep from 'jsep' import jsep from 'jsep'
@ -73,20 +74,23 @@ export default {
availableFunctions: ['AVG', 'ADD', 'CONCAT'], availableFunctions: ['AVG', 'ADD', 'CONCAT'],
availableBinOps: ['+', '-', '*', '/'], availableBinOps: ['+', '-', '*', '/'],
autocomplete: true, autocomplete: true,
suggestion: null suggestion: null,
wordToComplete: '',
selected: 0,
tooltip: true
}), }),
computed: { computed: {
suggestionsList() { suggestionsList() {
return [ return [
...this.meta.columns.map(c => ({ text: c.cn, type: 'column', c })),
...this.availableFunctions.map(fn => ({ text: fn, type: 'function' })), ...this.availableFunctions.map(fn => ({ text: fn, type: 'function' })),
...this.meta.columns.map(c => ({ text: c.cn, type: 'column', c })),
...this.availableBinOps.map(op => ({ text: op, type: 'op' })) ...this.availableBinOps.map(op => ({ text: op, type: 'op' }))
] ]
}, },
acTree() { acTree() {
const ref = new NcAutocompleteTree() const ref = new NcAutocompleteTree()
for (const sug of this.suggestionsList) { for (const sug of this.suggestionsList) {
ref.add(sug.text) ref.add(sug)
} }
return ref return ref
} }
@ -186,10 +190,11 @@ export default {
return arr return arr
}, },
appendText(it) { appendText(it) {
const text = it.text.slice(this.wordToComplete.length)
if (it.type === 'function') { if (it.type === 'function') {
insertAtCursor(this.$refs.input.$el.querySelector('input'), it.text + '()', it.text.length + 1) this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text + '()', text.length + 1))
} else { } else {
insertAtCursor(this.$refs.input.$el.querySelector('input'), it.text) this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text))
} }
}, },
_handleInputDeb: debounce(async function(self) { _handleInputDeb: debounce(async function(self) {
@ -199,25 +204,40 @@ export default {
this._handleInputDeb(this) this._handleInputDeb(this)
}, },
handleInput() { handleInput() {
this.autocomplete = true this.selected = 0
// const $fakeDiv = this.$refs.fakeDiv // const $fakeDiv = this.$refs.fakeDiv
this.suggestion = null this.suggestion = null
const query = this.formula.value const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input')) // this.formula.value
if (query !== '') { // if (query !== '') {
const parts = query.split(' ') const parts = query.split(/\W+/)
const wordToComplete = parts.pop() this.wordToComplete = parts.pop()
if (wordToComplete !== '') { // if (this.wordToComplete !== '') {
// get best match using popularity // get best match using popularity
this.suggestion = this.acTree.complete(wordToComplete) this.suggestion = this.acTree.complete(this.wordToComplete)
} else {
// $span.textContent = '' // clear ghost span this.autocomplete = !!this.suggestion.length
} // } else {
} else { // // $span.textContent = '' // clear ghost span
// $time.textContent = '' // }
// $span.textContent = '' // clear ghost span // } else {
// this.autocomplete = false
// // $time.textContent = ''
// // $span.textContent = '' // clear ghost span
// }
},
selectText() {
if (this.selected > -1 && this.selected < this.suggestion.length) {
this.appendText(this.suggestion[this.selected])
this.autocomplete = false
} }
},
suggestionListDown() {
this.selected = ++this.selected % this.suggestion.length
},
suggestionListUp() {
this.selected = --this.selected > -1 ? this.selected : this.suggestion.length - 1
} }
} }
} }

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

@ -45,6 +45,39 @@ export function insertAtCursor(myField, myValue, len) {
} else { } else {
myField.value += myValue myField.value += myValue
} }
return myField.value
}
function ReturnWord(text, caretPos) {
const index = text.indexOf(caretPos)
const preText = text.substring(0, caretPos)
if (preText.indexOf(' ') > 0) {
const words = preText.split(' ')
return words[words.length - 1] // return last word
} else {
return preText
}
}
export function getWordUntilCaret(ctrl) {
const caretPos = GetCaretPosition(ctrl)
const word = ReturnWord(ctrl.value, caretPos)
return word || ''
}
function GetCaretPosition(ctrl) {
let CaretPos = 0 // IE Support
if (document.selection) {
ctrl.focus()
const Sel = document.selection.createRange()
Sel.moveStart('character', -ctrl.value.length)
CaretPos = Sel.text.length
}
// Firefox support
else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
CaretPos = ctrl.selectionStart
}
return (CaretPos)
} }
/** /**

Loading…
Cancel
Save