Merge pull request #20 from chanakyabhardwajj/main
For multiword scenarios, ignore activation keys when a match is in progress.
This commit is contained in:
Коммит
33a0e74d79
|
@ -44,6 +44,7 @@ With a script tag:
|
|||
|
||||
- `key`: The matched key; for example: `:`.
|
||||
- `text`: The matched text; for example: `cat`, for `:cat`.
|
||||
- If the `key` is specified in the `multiword` attribute then the matched text can contain multiple words; for example `cat and dog` for `:cat and dog`.
|
||||
- `provide`: A function to be called when you have the menu results. Takes a `Promise` with `{matched: boolean, fragment: HTMLElement}` where `matched` tells the element whether a suggestion is available, and `fragment` is the menu content to be displayed on the page.
|
||||
|
||||
```js
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</text-expander>
|
||||
|
||||
<h2>Multiword text-expander element</h2>
|
||||
<text-expander keys="#" multiword>
|
||||
<text-expander keys="#" multiword="#">
|
||||
<textarea autofocus rows="10" cols="40"></textarea>
|
||||
</text-expander>
|
||||
|
||||
|
@ -34,7 +34,9 @@
|
|||
for (const issue of [
|
||||
'#1 Implement a text-expander element',
|
||||
'#2 Implement multi word option',
|
||||
'#3 Fix tpoy'
|
||||
'#3 Fix tpoy',
|
||||
'#4 Implement #12',
|
||||
'#5 Implement #123 and #456',
|
||||
]) {
|
||||
if (issue.toLowerCase().includes(text.toLowerCase())) {
|
||||
const item = document.createElement('li')
|
||||
|
|
20
src/query.ts
20
src/query.ts
|
@ -6,6 +6,7 @@ type Query = {
|
|||
type QueryOptions = {
|
||||
lookBackIndex: number
|
||||
multiWord: boolean
|
||||
lastMatchPosition: number | null
|
||||
}
|
||||
|
||||
const boundary = /\s|\(|\[/
|
||||
|
@ -15,19 +16,30 @@ export default function query(
|
|||
text: string,
|
||||
key: string,
|
||||
cursor: number,
|
||||
{multiWord, lookBackIndex}: QueryOptions = {multiWord: false, lookBackIndex: 0}
|
||||
{multiWord, lookBackIndex, lastMatchPosition}: QueryOptions = {
|
||||
multiWord: false,
|
||||
lookBackIndex: 0,
|
||||
lastMatchPosition: null
|
||||
}
|
||||
): Query | void {
|
||||
// Activation key not found in front of the cursor.
|
||||
const keyIndex = text.lastIndexOf(key, cursor - 1)
|
||||
let keyIndex = text.lastIndexOf(key, cursor - 1)
|
||||
if (keyIndex === -1) return
|
||||
|
||||
// Stop matching at the lookBackIndex
|
||||
if (keyIndex < lookBackIndex) return
|
||||
|
||||
if (multiWord) {
|
||||
// Space immediately after activation key
|
||||
if (lastMatchPosition != null) {
|
||||
// If the current activation key is the same as last match
|
||||
// i.e. consecutive activation keys, then return.
|
||||
if (lastMatchPosition === keyIndex) return
|
||||
keyIndex = lastMatchPosition - 1
|
||||
}
|
||||
|
||||
// Space immediately after activation key followed by the cursor
|
||||
const charAfterKey = text[keyIndex + 1]
|
||||
if (charAfterKey === ' ') return
|
||||
if (charAfterKey === ' ' && cursor >= keyIndex + 2) return
|
||||
|
||||
// New line the cursor and previous activation key.
|
||||
const newLineIndex = text.lastIndexOf('\n', cursor - 1)
|
||||
|
|
|
@ -135,6 +135,7 @@ class TextExpander {
|
|||
this.input.selectionStart = cursor
|
||||
this.input.selectionEnd = cursor
|
||||
this.lookBackIndex = cursor
|
||||
this.match = null
|
||||
}
|
||||
|
||||
private onBlur() {
|
||||
|
@ -179,10 +180,14 @@ class TextExpander {
|
|||
const cursor = this.input.selectionEnd || 0
|
||||
const text = this.input.value
|
||||
if (cursor <= this.lookBackIndex) {
|
||||
this.lookBackIndex = 0
|
||||
this.lookBackIndex = cursor - 1
|
||||
}
|
||||
for (const {key, multiWord} of this.expander.keys) {
|
||||
const found = query(text, key, cursor, {multiWord, lookBackIndex: this.lookBackIndex})
|
||||
const found = query(text, key, cursor, {
|
||||
multiWord,
|
||||
lookBackIndex: this.lookBackIndex,
|
||||
lastMatchPosition: this.match ? this.match.position : null
|
||||
})
|
||||
if (found) {
|
||||
return {text: found.text, key, position: found.position}
|
||||
}
|
||||
|
@ -208,6 +213,7 @@ class TextExpander {
|
|||
|
||||
private onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
this.match = null
|
||||
if (this.deactivate()) {
|
||||
this.lookBackIndex = this.input.selectionEnd || this.lookBackIndex
|
||||
event.stopImmediatePropagation()
|
||||
|
|
|
@ -124,6 +124,39 @@ describe('text-expander multi word parsing', function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe('text-expander multi word parsing with multiple activation keys', function() {
|
||||
it('does not match consecutive activation keys', function() {
|
||||
let found = query('::', ':', 2, {multiWord: true})
|
||||
assert(found == null)
|
||||
|
||||
found = query('::', ':', 3, {multiWord: true})
|
||||
assert(found == null)
|
||||
|
||||
found = query('hi :: there', ':', 5, {multiWord: true})
|
||||
assert(found == null)
|
||||
|
||||
found = query('hi ::: there', ':', 6, {multiWord: true})
|
||||
assert(found == null)
|
||||
|
||||
found = query('hi ::', ':', 5, {multiWord: true})
|
||||
assert(found == null)
|
||||
|
||||
found = query('hi :::', ':', 6, {multiWord: true})
|
||||
assert(found == null)
|
||||
})
|
||||
|
||||
it('uses lastMatchPosition to match', function() {
|
||||
let found = query('hi :cat :bye', ':', 12, {multiWord: true, lastMatchPosition: 4})
|
||||
assert.deepEqual(found, {text: 'cat :bye', position: 4})
|
||||
|
||||
found = query('hi :cat :bye :::', ':', 16, {multiWord: true, lastMatchPosition: 4})
|
||||
assert.deepEqual(found, {text: 'cat :bye :::', position: 4})
|
||||
|
||||
found = query(':hi :cat :bye :::', ':', 17, {multiWord: true, lastMatchPosition: 1})
|
||||
assert.deepEqual(found, {text: 'hi :cat :bye :::', position: 1})
|
||||
})
|
||||
})
|
||||
|
||||
describe('text-expander limits the lookBack after commit', function() {
|
||||
it('does not match if lookBackIndex is bigger than activation key index', function() {
|
||||
const found = query('hi :cat bye', ':', 11, {multiWord: true, lookBackIndex: 7})
|
||||
|
|
|
@ -128,6 +128,36 @@ describe('text-expander element', function() {
|
|||
assert.equal('#', key)
|
||||
assert.equal('some text @match word', text)
|
||||
})
|
||||
|
||||
it('dispatches change event for the first activation key even if it is typed again', async function() {
|
||||
const expander = document.querySelector('text-expander')
|
||||
const input = expander.querySelector('textarea')
|
||||
|
||||
let result = once(expander, 'text-expander-change')
|
||||
triggerInput(input, '#step 1')
|
||||
let event = await result
|
||||
let {key, text} = event.detail
|
||||
assert.equal('#', key)
|
||||
assert.equal('step 1', text)
|
||||
|
||||
await waitForAnimationFrame()
|
||||
|
||||
result = once(expander, 'text-expander-change')
|
||||
triggerInput(input, ' #step 2', true) //<-- At this point the text inside the input field is "#step 1 #step 2"
|
||||
event = await result
|
||||
;({key, text} = event.detail)
|
||||
assert.equal('#', key)
|
||||
assert.equal('step 1 #step 2', text)
|
||||
|
||||
await waitForAnimationFrame()
|
||||
|
||||
result = once(expander, 'text-expander-change')
|
||||
triggerInput(input, ' #step 3', true) //<-- At this point the text inside the input field is "#step 1 #step 2 #step 3"
|
||||
event = await result
|
||||
;({key, text} = event.detail)
|
||||
assert.equal('#', key)
|
||||
assert.equal('step 1 #step 2 #step 3', text)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -137,8 +167,8 @@ function once(element, eventName) {
|
|||
})
|
||||
}
|
||||
|
||||
function triggerInput(input, value) {
|
||||
input.value = value
|
||||
function triggerInput(input, value, onlyAppend = false) {
|
||||
input.value = onlyAppend ? input.value + value : value
|
||||
return input.dispatchEvent(new InputEvent('input'))
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче