forked from platypush/platypush
[#341] Added support for dynamic context in procedure editor components.
This commit is contained in:
parent
0e40d77bc7
commit
6dd1d481d5
21 changed files with 694 additions and 215 deletions
|
@ -5,14 +5,15 @@
|
||||||
<!-- Supported action arguments -->
|
<!-- Supported action arguments -->
|
||||||
<div class="arg" :key="name" v-for="name in Object.keys(action.args)">
|
<div class="arg" :key="name" v-for="name in Object.keys(action.args)">
|
||||||
<label>
|
<label>
|
||||||
<input type="text"
|
<ContextAutocomplete :value="action.args[name].value"
|
||||||
class="action-arg-value"
|
|
||||||
:class="{required: action.args[name].required}"
|
|
||||||
:disabled="running"
|
:disabled="running"
|
||||||
|
:items="contextAutocompleteItems"
|
||||||
:placeholder="name"
|
:placeholder="name"
|
||||||
:value="action.args[name].value"
|
:quote="true"
|
||||||
|
:select-on-tab="false"
|
||||||
@input="onArgEdit(name, $event)"
|
@input="onArgEdit(name, $event)"
|
||||||
@focus="onSelect(name)">
|
@blur="onSelect(name)"
|
||||||
|
@focus="onSelect(name)" />
|
||||||
<span class="required-flag" v-if="action.args[name].required">*</span>
|
<span class="required-flag" v-if="action.args[name].required">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -36,12 +37,13 @@
|
||||||
@input="onExtraArgNameEdit(i, $event.target.value)">
|
@input="onExtraArgNameEdit(i, $event.target.value)">
|
||||||
</label>
|
</label>
|
||||||
<label class="col-6">
|
<label class="col-6">
|
||||||
<input type="text"
|
<ContextAutocomplete :value="arg.value"
|
||||||
class="action-extra-arg-value"
|
|
||||||
placeholder="Value"
|
|
||||||
:disabled="running"
|
:disabled="running"
|
||||||
:value="arg.value"
|
:items="contextAutocompleteItems"
|
||||||
@input="onExtraArgValueEdit(i, $event.target.value)">
|
:quote="true"
|
||||||
|
:select-on-tab="false"
|
||||||
|
placeholder="Value"
|
||||||
|
@input="onExtraArgValueEdit(i, $event.detail)" />
|
||||||
</label>
|
</label>
|
||||||
<label class="col-1 buttons">
|
<label class="col-1 buttons">
|
||||||
<button type="button" class="action-extra-arg-del" title="Remove argument" @click="$emit('remove', i)">
|
<button type="button" class="action-extra-arg-del" title="Remove argument" @click="$emit('remove', i)">
|
||||||
|
@ -68,10 +70,16 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Argdoc from "./Argdoc"
|
import Argdoc from "./Argdoc"
|
||||||
|
import ContextAutocomplete from "./ContextAutocomplete"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ActionArgs',
|
mixins: [Mixin],
|
||||||
components: { Argdoc },
|
components: {
|
||||||
|
Argdoc,
|
||||||
|
ContextAutocomplete,
|
||||||
|
},
|
||||||
|
|
||||||
emits: [
|
emits: [
|
||||||
'add',
|
'add',
|
||||||
'arg-edit',
|
'arg-edit',
|
||||||
|
@ -117,7 +125,7 @@ export default {
|
||||||
onArgEdit(name, event) {
|
onArgEdit(name, event) {
|
||||||
this.$emit('arg-edit', {
|
this.$emit('arg-edit', {
|
||||||
name: name,
|
name: name,
|
||||||
value: event.target.value,
|
value: event.target?.value || event.detail,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<ActionArgs :action="action"
|
<ActionArgs :action="action"
|
||||||
|
:context="context"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:running="running"
|
:running="running"
|
||||||
:selected-arg="selectedArg"
|
:selected-arg="selectedArg"
|
||||||
|
@ -143,6 +144,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
value: {
|
value: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
|
@ -391,14 +397,14 @@ export default {
|
||||||
this.actionDocsCache[this.action.name].html = this.selectedDoc
|
this.actionDocsCache[this.action.name].html = this.selectedDoc
|
||||||
this.setUrlArgs({action: this.action.name})
|
this.setUrlArgs({action: this.action.name})
|
||||||
|
|
||||||
const firstArg = this.$el.querySelector('.action-arg-value')
|
this.$nextTick(() => {
|
||||||
|
const firstArg = this.$el.querySelector('.args-body input[type=text]')
|
||||||
if (firstArg) {
|
if (firstArg) {
|
||||||
firstArg.focus()
|
firstArg.focus()
|
||||||
} else {
|
} else {
|
||||||
this.$nextTick(() => {
|
|
||||||
this.actionInput.focus()
|
this.actionInput.focus()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.response = undefined
|
this.response = undefined
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
<div class="action-editor-container">
|
<div class="action-editor-container">
|
||||||
<Modal ref="actionEditor" title="Edit Action">
|
<Modal ref="actionEditor" title="Edit Action">
|
||||||
<ActionEditor :value="value"
|
<ActionEditor :value="value"
|
||||||
|
:context="context"
|
||||||
:with-save="!readOnly"
|
:with-save="!readOnly"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
v-if="this.$refs.actionEditor?.$data?.isVisible" />
|
v-if="this.$refs.actionEditor?.$data?.isVisible" />
|
||||||
|
@ -68,9 +69,11 @@ import ActionEditor from "@/components/Action/ActionEditor"
|
||||||
import Draggable from "@/components/elements/Draggable"
|
import Draggable from "@/components/elements/Draggable"
|
||||||
import Droppable from "@/components/elements/Droppable"
|
import Droppable from "@/components/elements/Droppable"
|
||||||
import ExtensionIcon from "@/components/elements/ExtensionIcon"
|
import ExtensionIcon from "@/components/elements/ExtensionIcon"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
emits: [
|
emits: [
|
||||||
'delete',
|
'delete',
|
||||||
'drag',
|
'drag',
|
||||||
|
@ -90,6 +93,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
draggable: {
|
draggable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
<div class="actions-list" :class="actionListClasses">
|
<div class="actions-list" :class="actionListClasses">
|
||||||
<ActionsList :value="value[key]"
|
<ActionsList :value="value[key]"
|
||||||
|
:context="context"
|
||||||
:dragging="dragging"
|
:dragging="dragging"
|
||||||
:has-else="hasElse"
|
:has-else="hasElse"
|
||||||
:indent="indent"
|
:indent="indent"
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
:value="newAction"
|
:value="newAction"
|
||||||
@drop="onDrop(0, $event)">
|
@drop="onDrop(0, $event)">
|
||||||
<ActionTile :value="newAction"
|
<ActionTile :value="newAction"
|
||||||
|
:context="contexts[Object.keys(contexts).length - 1]"
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
@input="addAction" />
|
@input="addAction" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
@ -244,6 +245,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
value: action,
|
value: action,
|
||||||
active: this.isDragging,
|
active: this.isDragging,
|
||||||
|
context: this.contexts[index],
|
||||||
isInsideLoop: !!(this.isInsideLoop || this.getFor(action) || this.getWhile(action)),
|
isInsideLoop: !!(this.isInsideLoop || this.getFor(action) || this.getWhile(action)),
|
||||||
readOnly: this.readOnly,
|
readOnly: this.readOnly,
|
||||||
ref: `action-tile-${index}`,
|
ref: `action-tile-${index}`,
|
||||||
|
@ -314,6 +316,34 @@ export default {
|
||||||
}, {}) || {}
|
}, {}) || {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contexts() {
|
||||||
|
const commonCtx = {...this.context}
|
||||||
|
const contexts = this.newValue?.reduce?.((acc, action, index) => {
|
||||||
|
acc[index] = this.getContext(action, index, commonCtx)
|
||||||
|
return acc
|
||||||
|
}, {}) || {}
|
||||||
|
|
||||||
|
const nContexts = Object.keys(contexts).length
|
||||||
|
if (nContexts > 0) {
|
||||||
|
contexts[nContexts] = this.getContext(
|
||||||
|
null,
|
||||||
|
nContexts,
|
||||||
|
contexts[nContexts - 1]
|
||||||
|
)
|
||||||
|
|
||||||
|
contexts[nContexts] = {
|
||||||
|
...this.context,
|
||||||
|
...commonCtx,
|
||||||
|
...contexts[nContexts - 1],
|
||||||
|
...contexts[nContexts],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contexts[0] = {...this.context}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contexts
|
||||||
|
},
|
||||||
|
|
||||||
dragBlockIndex() {
|
dragBlockIndex() {
|
||||||
if (this.dragIndex == null) {
|
if (this.dragIndex == null) {
|
||||||
return
|
return
|
||||||
|
@ -678,6 +708,33 @@ export default {
|
||||||
this.$emit('collapse')
|
this.$emit('collapse')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getContext(action, index, context) {
|
||||||
|
const ctx = {...(context || this.context || {})}
|
||||||
|
if (index > 0) {
|
||||||
|
ctx.output = {
|
||||||
|
source: 'action',
|
||||||
|
action: this.newValue[index - 1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSet(action)) {
|
||||||
|
Object.keys(action.set).forEach((name) => {
|
||||||
|
if (!name?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
context[name] = { source: 'local' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const iterator = this.getFor(action)?.iterator
|
||||||
|
if (iterator?.length) {
|
||||||
|
context[iterator] = { source: 'for' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
|
|
||||||
getParentBlock(indices) {
|
getParentBlock(indices) {
|
||||||
indices = [...indices]
|
indices = [...indices]
|
||||||
let parent = this.newValue
|
let parent = this.newValue
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
:value="value"
|
:value="value"
|
||||||
v-on="componentsData.on">
|
v-on="componentsData.on">
|
||||||
<ActionTile :value="value"
|
<ActionTile :value="value"
|
||||||
|
:context="context"
|
||||||
:draggable="!readOnly"
|
:draggable="!readOnly"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
:with-delete="!readOnly"
|
:with-delete="!readOnly"
|
||||||
|
@ -47,6 +48,11 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
readOnly: {
|
readOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="condition-block">
|
<div class="condition-block">
|
||||||
<ActionsBlock :value="value"
|
<ActionsBlock :value="value"
|
||||||
:collapsed="collapsed"
|
:collapsed="collapsed"
|
||||||
|
:context="context"
|
||||||
:dragging="isDragging"
|
:dragging="isDragging"
|
||||||
:has-else="hasElse"
|
:has-else="hasElse"
|
||||||
:is-inside-loop="isInsideLoop"
|
:is-inside-loop="isInsideLoop"
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
<EndBlockTile value="end if"
|
<EndBlockTile value="end if"
|
||||||
icon="fas fa-question"
|
icon="fas fa-question"
|
||||||
:active="active"
|
:active="active"
|
||||||
:spacer-bottom="spacerBottom"
|
:spacer-bottom="spacerBottom || dragging_"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
v-if="isElse || !hasElse" />
|
v-if="isElse || !hasElse" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -144,6 +145,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
active: this.active,
|
active: this.active,
|
||||||
|
context: this.context,
|
||||||
readOnly: this.readOnly,
|
readOnly: this.readOnly,
|
||||||
spacerBottom: this.spacerBottom,
|
spacerBottom: this.spacerBottom,
|
||||||
spacerTop: this.spacerTop,
|
spacerTop: this.spacerTop,
|
||||||
|
|
|
@ -46,10 +46,13 @@
|
||||||
:visible="true"
|
:visible="true"
|
||||||
@close="showConditionEditor = false">
|
@close="showConditionEditor = false">
|
||||||
<ExpressionEditor :value="value"
|
<ExpressionEditor :value="value"
|
||||||
|
:context="context"
|
||||||
ref="conditionEditor"
|
ref="conditionEditor"
|
||||||
@input.prevent.stop="onConditionChange"
|
@input.prevent.stop="onConditionChange"
|
||||||
v-if="showConditionEditor">
|
v-if="showConditionEditor">
|
||||||
|
<div class="header">
|
||||||
Condition
|
Condition
|
||||||
|
</div>
|
||||||
</ExpressionEditor>
|
</ExpressionEditor>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,6 +62,7 @@
|
||||||
<script>
|
<script>
|
||||||
import ExpressionEditor from "./ExpressionEditor"
|
import ExpressionEditor from "./ExpressionEditor"
|
||||||
import ListItem from "./ListItem"
|
import ListItem from "./ListItem"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Tile from "@/components/elements/Tile"
|
import Tile from "@/components/elements/Tile"
|
||||||
|
|
||||||
|
@ -76,6 +80,7 @@ export default {
|
||||||
'input',
|
'input',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
mixins: [Mixin],
|
||||||
components: {
|
components: {
|
||||||
ExpressionEditor,
|
ExpressionEditor,
|
||||||
ListItem,
|
ListItem,
|
||||||
|
@ -213,5 +218,11 @@ export default {
|
||||||
.drag-spacer {
|
.drag-spacer {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.expression-editor) {
|
||||||
|
.header {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
<template>
|
||||||
|
<div class="autocomplete-with-context">
|
||||||
|
<Autocomplete
|
||||||
|
v-bind="autocompleteProps"
|
||||||
|
@blur="onBlur"
|
||||||
|
@focus="onFocus"
|
||||||
|
@input="onInput"
|
||||||
|
@select="onSelect"
|
||||||
|
ref="input" />
|
||||||
|
|
||||||
|
<div class="spacer" ref="spacer" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Autocomplete from "@/components/elements/Autocomplete"
|
||||||
|
import AutocompleteProps from "@/mixins/Autocomplete/Props"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ["blur", "focus", "input"],
|
||||||
|
components: { Autocomplete },
|
||||||
|
mixins: [AutocompleteProps],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
quote: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
autocompleteItemsElement() {
|
||||||
|
return this.$refs.input?.$el?.querySelector(".items")
|
||||||
|
},
|
||||||
|
|
||||||
|
autocompleteItemsHeight() {
|
||||||
|
return this.autocompleteItemsElement?.clientHeight || 0
|
||||||
|
},
|
||||||
|
|
||||||
|
autocompleteProps() {
|
||||||
|
return {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(this.$props).filter(([key]) => key !== "quote")
|
||||||
|
),
|
||||||
|
items: this.items,
|
||||||
|
value: this.value,
|
||||||
|
inputOnBlur: false,
|
||||||
|
inputOnSelect: false,
|
||||||
|
showAllItems: true,
|
||||||
|
showResultsWhenBlank: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
textInput() {
|
||||||
|
return this.$refs.input?.$refs?.input
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
emitInput(value) {
|
||||||
|
this.$emit(
|
||||||
|
"input",
|
||||||
|
new CustomEvent("input", {
|
||||||
|
detail: value,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
isWithinQuote(selection) {
|
||||||
|
let ret = false
|
||||||
|
let value = '' + this.value
|
||||||
|
selection = [...selection]
|
||||||
|
|
||||||
|
while (selection[0] > 0) {
|
||||||
|
if (value[selection[0] - 1] === '$' && value[selection[0]] === '{') {
|
||||||
|
ret = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
selection[0]--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
return false
|
||||||
|
|
||||||
|
ret = false
|
||||||
|
while (selection[1] < value.length) {
|
||||||
|
if (value[selection[1]] === '}') {
|
||||||
|
ret = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
selection[1]++
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlur(event) {
|
||||||
|
this.$emit("blur", event)
|
||||||
|
this.$refs.spacer.style.height = "0"
|
||||||
|
},
|
||||||
|
|
||||||
|
onFocus(event) {
|
||||||
|
this.$emit("focus", event)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.spacer.style.height = `${this.autocompleteItemsHeight}px`
|
||||||
|
}, 10)
|
||||||
|
},
|
||||||
|
|
||||||
|
onInput(event) {
|
||||||
|
const selection = this.textSelection()
|
||||||
|
this.emitInput(event)
|
||||||
|
this.resetSelection(selection)
|
||||||
|
},
|
||||||
|
|
||||||
|
onSelect(value) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const selection = this.textSelection()
|
||||||
|
value = this.quote && !this.isWithinQuote(selection) ? `\${${value}}` : value
|
||||||
|
const newValue = typeof this.value === 'string' ? (
|
||||||
|
this.value.slice(0, selection[0]) +
|
||||||
|
value +
|
||||||
|
this.value.slice(selection[1])
|
||||||
|
) : value
|
||||||
|
|
||||||
|
this.emitInput(newValue)
|
||||||
|
this.resetSelection([selection[0] + value.length, selection[0] + value.length])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
resetSelection(selection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.textInput?.focus()
|
||||||
|
this.textInput?.setSelectionRange(selection[0], selection[1])
|
||||||
|
}, 10)
|
||||||
|
},
|
||||||
|
|
||||||
|
textSelection() {
|
||||||
|
return [this.textInput?.selectionStart, this.textInput?.selectionEnd]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.autocomplete-with-context {
|
||||||
|
.item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@include from($tablet) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suffix {
|
||||||
|
display: flex;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: $disabled-fg;
|
||||||
|
|
||||||
|
@include from($tablet) {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,14 +3,11 @@
|
||||||
<label for="expression">
|
<label for="expression">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<input type="text"
|
<ContextAutocomplete :value="newValue"
|
||||||
name="expression"
|
:items="contextAutocompleteItems"
|
||||||
autocomplete="off"
|
:quote="quote"
|
||||||
:autofocus="true"
|
@input.stop="onInput"
|
||||||
:placeholder="placeholder"
|
ref="input" />
|
||||||
:value="value"
|
|
||||||
ref="text"
|
|
||||||
@input.stop="onInput" />
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
|
@ -22,10 +19,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import ContextAutocomplete from "./ContextAutocomplete"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: [
|
emits: ['input'],
|
||||||
'input',
|
mixins: [Mixin],
|
||||||
],
|
components: { ContextAutocomplete },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
|
@ -42,17 +42,23 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
quote: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasChanges: false,
|
hasChanges: false,
|
||||||
|
newValue: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit(event) {
|
onSubmit(event) {
|
||||||
const value = this.$refs.text.value.trim()
|
const value = this.newValue?.trim()
|
||||||
if (!value.length && !this.allowEmpty) {
|
if (!value.length && !this.allowEmpty) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -62,7 +68,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onInput(event) {
|
onInput(event) {
|
||||||
const value = '' + event.target.value
|
if (event?.detail == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
const value = '' + event.detail
|
||||||
if (!value?.trim()?.length) {
|
if (!value?.trim()?.length) {
|
||||||
this.hasChanges = this.allowEmpty
|
this.hasChanges = this.allowEmpty
|
||||||
} else {
|
} else {
|
||||||
|
@ -70,7 +79,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.text.value = value
|
this.newValue = value
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -83,12 +92,14 @@ export default {
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.hasChanges = false
|
this.hasChanges = false
|
||||||
|
this.newValue = this.value
|
||||||
|
|
||||||
if (!this.value?.trim?.()?.length) {
|
if (!this.value?.trim?.()?.length) {
|
||||||
this.hasChanges = this.allowEmpty
|
this.hasChanges = this.allowEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.text.focus()
|
this.textInput?.focus()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -107,8 +118,7 @@ export default {
|
||||||
label {
|
label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row item list-item" :class="itemClass">
|
<div class="row item list-item" :class="itemClass">
|
||||||
<div class="spacer-wrapper" :class="{ hidden: !spacerTop }">
|
<div class="spacer-wrapper" :class="{ hidden: !spacerTop }">
|
||||||
<div class="spacer" :class="{ active }" ref="dropTargetTop">
|
<div class="spacer top" :class="{ active }" ref="dropTargetTop">
|
||||||
<div class="droppable-wrapper">
|
<div class="droppable-wrapper">
|
||||||
<div class="droppable-container">
|
<div class="droppable-container">
|
||||||
<div class="droppable-frame">
|
<div class="droppable-frame">
|
||||||
|
@ -14,14 +14,14 @@
|
||||||
<Droppable :element="$refs.dropTargetTop" :disabled="readOnly" v-on="droppableData.top.on" />
|
<Droppable :element="$refs.dropTargetTop" :disabled="readOnly" v-on="droppableData.top.on" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spacer" v-if="dragging" />
|
<div class="spacer top" v-if="dragging" />
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<div class="spacer" v-if="dragging" />
|
<div class="spacer bottom" v-if="dragging" />
|
||||||
|
|
||||||
<div class="spacer-wrapper" :class="{ hidden: !spacerBottom }">
|
<div class="spacer-wrapper" :class="{ hidden: !spacerBottom }">
|
||||||
<div class="spacer" :class="{ active }" ref="dropTargetBottom">
|
<div class="spacer bottom" :class="{ active }" ref="dropTargetBottom">
|
||||||
<div class="droppable-wrapper">
|
<div class="droppable-wrapper">
|
||||||
<div class="droppable-container">
|
<div class="droppable-container">
|
||||||
<div class="droppable-frame">
|
<div class="droppable-frame">
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="loop-block">
|
<div class="loop-block">
|
||||||
<ActionsBlock :value="value"
|
<ActionsBlock :value="value"
|
||||||
:collapsed="collapsed"
|
:collapsed="collapsed"
|
||||||
|
:context="context_"
|
||||||
:dragging="isDragging"
|
:dragging="isDragging"
|
||||||
:indent="indent"
|
:indent="indent"
|
||||||
:is-inside-loop="true"
|
:is-inside-loop="true"
|
||||||
|
@ -25,7 +26,7 @@
|
||||||
<EndBlockTile :value="`end ${type}`"
|
<EndBlockTile :value="`end ${type}`"
|
||||||
icon="fas fa-arrow-rotate-right"
|
icon="fas fa-arrow-rotate-right"
|
||||||
:active="active"
|
:active="active"
|
||||||
:spacer-bottom="spacerBottom"
|
:spacer-bottom="spacerBottom || dragging"
|
||||||
@drop="onDrop" />
|
@drop="onDrop" />
|
||||||
</template>
|
</template>
|
||||||
</ActionsBlock>
|
</ActionsBlock>
|
||||||
|
@ -134,6 +135,18 @@ export default {
|
||||||
return () => {}
|
return () => {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
context_() {
|
||||||
|
const ctx = {...this.context}
|
||||||
|
const iterator = this.loop?.iterator?.trim()
|
||||||
|
if (iterator?.length) {
|
||||||
|
ctx[iterator] = {
|
||||||
|
source: 'for',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
|
|
||||||
isDragging() {
|
isDragging() {
|
||||||
return this.dragging_ || this.dragging
|
return this.dragging_ || this.dragging
|
||||||
},
|
},
|
||||||
|
@ -159,6 +172,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
...this.loop,
|
...this.loop,
|
||||||
active: this.active,
|
active: this.active,
|
||||||
|
context: this.context_,
|
||||||
readOnly: this.readOnly,
|
readOnly: this.readOnly,
|
||||||
spacerBottom: this.spacerBottom,
|
spacerBottom: this.spacerBottom,
|
||||||
spacerTop: this.spacerTop,
|
spacerTop: this.spacerTop,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:autofocus="true"
|
:autofocus="true"
|
||||||
placeholder="Iterator"
|
placeholder="Iterator"
|
||||||
:value="iterator"
|
:value="newValue.iterator"
|
||||||
ref="iterator"
|
ref="iterator"
|
||||||
@input.stop="onInput('iterator', $event)" />
|
@input.stop="onInput('iterator', $event)" />
|
||||||
</label>
|
</label>
|
||||||
|
@ -15,14 +15,12 @@
|
||||||
in
|
in
|
||||||
|
|
||||||
<label for="iterable">
|
<label for="iterable">
|
||||||
<input type="text"
|
<ContextAutocomplete :value="newValue.iterable"
|
||||||
name="iterable"
|
:items="contextAutocompleteItems"
|
||||||
autocomplete="off"
|
|
||||||
:autofocus="true"
|
|
||||||
placeholder="Iterable"
|
placeholder="Iterable"
|
||||||
:value="iterable"
|
@input.stop="onInput('iterable', $event)"
|
||||||
ref="iterable"
|
ref="iterable" />
|
||||||
@input.stop="onInput('iterable', $event)" />
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="async">
|
<label class="async">
|
||||||
|
@ -44,12 +42,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
import ContextAutocomplete from "./ContextAutocomplete"
|
||||||
emits: [
|
import Mixin from "./Mixin"
|
||||||
'change',
|
|
||||||
'input',
|
|
||||||
],
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['change', 'input'],
|
||||||
|
mixins: [Mixin],
|
||||||
|
components: { ContextAutocomplete },
|
||||||
props: {
|
props: {
|
||||||
async: {
|
async: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -70,6 +69,11 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasChanges: true,
|
hasChanges: true,
|
||||||
|
newValue: {
|
||||||
|
iterator: null,
|
||||||
|
iterable: null,
|
||||||
|
async: null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -86,7 +90,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onInput(target, event) {
|
onInput(target, event) {
|
||||||
const value = '' + event.target.value
|
const value = '' + (event.target?.value || event.detail)
|
||||||
if (!value?.trim()?.length) {
|
if (!value?.trim()?.length) {
|
||||||
this.hasChanges = false
|
this.hasChanges = false
|
||||||
} else {
|
} else {
|
||||||
|
@ -104,7 +108,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
event.target.value = value
|
this.newValue[target] = value
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -112,10 +116,21 @@ export default {
|
||||||
watch: {
|
watch: {
|
||||||
value() {
|
value() {
|
||||||
this.hasChanges = false
|
this.hasChanges = false
|
||||||
|
this.newValue = {
|
||||||
|
iterator: this.iterator,
|
||||||
|
iterable: this.iterable,
|
||||||
|
async: this.async,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.newValue = {
|
||||||
|
iterator: this.iterator,
|
||||||
|
iterable: this.iterable,
|
||||||
|
async: this.async,
|
||||||
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.iterator.focus()
|
this.$refs.iterator.focus()
|
||||||
})
|
})
|
||||||
|
@ -126,20 +141,12 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "common";
|
@import "common";
|
||||||
|
|
||||||
.loop-editor {
|
@mixin label {
|
||||||
min-width: 40em;
|
|
||||||
max-width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
label {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1em;
|
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -151,5 +158,26 @@ export default {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loop-editor {
|
||||||
|
min-width: 40em;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
@include label;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(label) {
|
||||||
|
@include label;
|
||||||
|
|
||||||
|
.autocomplete-with-context,
|
||||||
|
.autocomplete {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -37,12 +37,14 @@
|
||||||
<LoopEditor :iterator="iterator"
|
<LoopEditor :iterator="iterator"
|
||||||
:iterable="iterable"
|
:iterable="iterable"
|
||||||
:async="async"
|
:async="async"
|
||||||
|
:context="context"
|
||||||
@change="onLoopChange"
|
@change="onLoopChange"
|
||||||
v-if="showLoopEditor && type === 'for'">
|
v-if="showLoopEditor && type === 'for'">
|
||||||
Loop
|
Loop
|
||||||
</LoopEditor>
|
</LoopEditor>
|
||||||
|
|
||||||
<ExpressionEditor :value="condition"
|
<ExpressionEditor :value="condition"
|
||||||
|
:context="context"
|
||||||
@input.prevent.stop="onConditionChange"
|
@input.prevent.stop="onConditionChange"
|
||||||
v-else-if="showLoopEditor && type === 'while'">
|
v-else-if="showLoopEditor && type === 'while'">
|
||||||
Loop Condition
|
Loop Condition
|
||||||
|
@ -56,10 +58,12 @@
|
||||||
import ExpressionEditor from "./ExpressionEditor"
|
import ExpressionEditor from "./ExpressionEditor"
|
||||||
import ListItem from "./ListItem"
|
import ListItem from "./ListItem"
|
||||||
import LoopEditor from "./LoopEditor"
|
import LoopEditor from "./LoopEditor"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Tile from "@/components/elements/Tile"
|
import Tile from "@/components/elements/Tile"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
emits: [
|
emits: [
|
||||||
'change',
|
'change',
|
||||||
'click',
|
'click',
|
||||||
|
|
|
@ -1,5 +1,41 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
contextAutocompleteItems() {
|
||||||
|
return Object.entries(this.context).reduce((acc, [key, value]) => {
|
||||||
|
let suffix = '<span class="source">from <b>' + (
|
||||||
|
value?.source === 'local'
|
||||||
|
? 'local context'
|
||||||
|
: value?.source
|
||||||
|
) + '</b>'
|
||||||
|
|
||||||
|
if (value?.source === 'action' && value?.action?.action)
|
||||||
|
suffix += ` (<i>${value?.action?.action}</i>)`
|
||||||
|
|
||||||
|
suffix += '</span>'
|
||||||
|
acc.push(
|
||||||
|
{
|
||||||
|
text: key,
|
||||||
|
suffix: suffix,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
|
||||||
|
textInput() {
|
||||||
|
return this.$refs.input?.$refs?.input?.$refs?.input
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getCondition(value) {
|
getCondition(value) {
|
||||||
value = this.getKey(value) || value
|
value = this.getKey(value) || value
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
@close="showExprEditor = false">
|
@close="showExprEditor = false">
|
||||||
<ExpressionEditor :value="value"
|
<ExpressionEditor :value="value"
|
||||||
:allow-empty="true"
|
:allow-empty="true"
|
||||||
|
:context="context"
|
||||||
|
:quote="true"
|
||||||
placeholder="Optional return value"
|
placeholder="Optional return value"
|
||||||
ref="exprEditor"
|
ref="exprEditor"
|
||||||
@input.prevent.stop="onExprChange"
|
@input.prevent.stop="onExprChange"
|
||||||
|
@ -39,10 +41,12 @@
|
||||||
<script>
|
<script>
|
||||||
import ExpressionEditor from "./ExpressionEditor"
|
import ExpressionEditor from "./ExpressionEditor"
|
||||||
import ListItem from "./ListItem"
|
import ListItem from "./ListItem"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Tile from "@/components/elements/Tile"
|
import Tile from "@/components/elements/Tile"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
emits: [
|
emits: [
|
||||||
'change',
|
'change',
|
||||||
'click',
|
'click',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<ListItem class="set-variables-tile"
|
<ListItem class="set-variables-tile"
|
||||||
:class="{active}"
|
:class="{active}"
|
||||||
|
:dragging="dragging_"
|
||||||
:value="value"
|
:value="value"
|
||||||
:active="active"
|
:active="active"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
|
@ -43,10 +44,12 @@
|
||||||
v-model="newValue[i][0]"> =
|
v-model="newValue[i][0]"> =
|
||||||
</span>
|
</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
<input type="text"
|
<ContextAutocomplete :value="newValue[i][1]"
|
||||||
|
:items="contextAutocompleteItems"
|
||||||
|
:quote="true"
|
||||||
|
:select-on-tab="false"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
@input.prevent.stop
|
@input.prevent.stop="newValue[i][1] = $event.detail" />
|
||||||
v-model="newValue[i][1]">
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -59,12 +62,13 @@
|
||||||
v-model="newVariable.name"> =
|
v-model="newVariable.name"> =
|
||||||
</span>
|
</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
<input type="text"
|
<ContextAutocomplete :value="newVariable.value"
|
||||||
|
:items="contextAutocompleteItems"
|
||||||
|
:quote="true"
|
||||||
|
:select-on-tab="false"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
ref="newVarValue"
|
@input.prevent.stop="newVariable.value = $event.detail"
|
||||||
@blur="onBlur(null)"
|
@blur="onBlur(null)" />
|
||||||
@input.prevent.stop
|
|
||||||
v-model="newVariable.value">
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -80,11 +84,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import ContextAutocomplete from "./ContextAutocomplete"
|
||||||
import ListItem from "./ListItem"
|
import ListItem from "./ListItem"
|
||||||
|
import Mixin from "./Mixin"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Tile from "@/components/elements/Tile"
|
import Tile from "@/components/elements/Tile"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [Mixin],
|
||||||
emits: [
|
emits: [
|
||||||
'click',
|
'click',
|
||||||
'delete',
|
'delete',
|
||||||
|
@ -98,6 +105,7 @@ export default {
|
||||||
],
|
],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
ContextAutocomplete,
|
||||||
ListItem,
|
ListItem,
|
||||||
Modal,
|
Modal,
|
||||||
Tile,
|
Tile,
|
||||||
|
@ -109,6 +117,11 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dragging: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
readOnly: {
|
readOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -162,7 +175,7 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dragging: false,
|
dragging_: false,
|
||||||
newValue: [],
|
newValue: [],
|
||||||
newVariable: {
|
newVariable: {
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -253,17 +266,17 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dragging = true
|
this.dragging_ = true
|
||||||
this.$emit('drag', event)
|
this.$emit('drag', event)
|
||||||
},
|
},
|
||||||
|
|
||||||
onDragEnd(event) {
|
onDragEnd(event) {
|
||||||
this.dragging = false
|
this.dragging_ = false
|
||||||
this.$emit('dragend', event)
|
this.$emit('dragend', event)
|
||||||
},
|
},
|
||||||
|
|
||||||
onDrop(event) {
|
onDrop(event) {
|
||||||
this.dragging = false
|
this.dragging_ = false
|
||||||
if (this.readOnly) {
|
if (this.readOnly) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -315,12 +328,6 @@ export default {
|
||||||
@import "common";
|
@import "common";
|
||||||
|
|
||||||
.set-variables-tile {
|
.set-variables-tile {
|
||||||
&.active {
|
|
||||||
.spacer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.variables {
|
.variables {
|
||||||
margin-left: 2.5em;
|
margin-left: 2.5em;
|
||||||
}
|
}
|
||||||
|
@ -342,6 +349,7 @@ export default {
|
||||||
|
|
||||||
.variable {
|
.variable {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -148,6 +148,7 @@ form {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.args-body) {
|
||||||
.args-list {
|
.args-list {
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -165,6 +166,11 @@ form {
|
||||||
width: $params-desktop-width;
|
width: $params-desktop-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.arg {
|
.arg {
|
||||||
margin-bottom: .25em;
|
margin-bottom: .25em;
|
||||||
@include until($tablet) {
|
@include until($tablet) {
|
||||||
|
@ -173,19 +179,22 @@ form {
|
||||||
|
|
||||||
.required-flag {
|
.required-flag {
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
|
display: inline-block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-left: 0.25em;
|
margin-left: 0.25em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
.autocomplete-with-context {
|
||||||
width: calc(100% - 1.5em);
|
width: calc(100% - 1.5em);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.action-arg-value {
|
.action-arg-value {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.args-body {
|
.args-body {
|
||||||
max-height: calc(50vh - 4.5em);
|
max-height: calc(50vh - 4.5em);
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<ActionsList :value="newValue.actions"
|
<ActionsList :value="newValue.actions"
|
||||||
|
:context="context"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
@input="onActionsEdit" />
|
@input="onActionsEdit" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -312,6 +313,16 @@ export default {
|
||||||
return JSON.stringify(this.newValue)
|
return JSON.stringify(this.newValue)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
context() {
|
||||||
|
return this.newValue?.args?.reduce((acc, arg) => {
|
||||||
|
acc[arg] = {
|
||||||
|
source: 'args',
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
|
||||||
modal_() {
|
modal_() {
|
||||||
if (this.readOnly)
|
if (this.readOnly)
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="autocomplete">
|
<div class="autocomplete" :class="{ 'with-items': showItems }">
|
||||||
<label :text="label">
|
<label :text="label">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -8,73 +8,43 @@
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:value="value"
|
:value="value"
|
||||||
@focus="onFocus"
|
@focus.stop="onFocus"
|
||||||
@input="onInput"
|
@input.stop="onInput"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@keydown="onInputKeyDown"
|
@keydown="onInputKeyDown"
|
||||||
@keyup="onInputKeyUp"
|
@keyup="onInputKeyUp"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="items" v-if="showItems">
|
<div class="items" ref="items" v-if="showItems">
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
:class="{ active: i === curIndex }"
|
:class="{ active: i === curIndex }"
|
||||||
:key="getItemText(item)"
|
:key="getItemText(item)"
|
||||||
:data-item="getItemText(item)"
|
:data-item="getItemText(item)"
|
||||||
v-for="(item, i) in visibleItems"
|
v-for="(item, i) in visibleItems"
|
||||||
@click="onItemSelect(item)">
|
@click.stop="onItemSelect(item)">
|
||||||
<span v-html="item.prefix" v-if="item.prefix"></span>
|
<span class="prefix" v-html="item.prefix" v-if="item.prefix"></span>
|
||||||
<span class="matching" v-if="value?.length">{{ getItemText(item).substr(0, value.length) }}</span>
|
<span class="matching" v-if="value?.length">{{ getItemText(item).substr(0, value.length) }}</span>
|
||||||
<span class="normal">{{ getItemText(item).substr(value?.length || 0) }}</span>
|
<span class="normal">{{ getItemText(item).substr(value?.length || 0) }}</span>
|
||||||
<span v-html="item.suffix" v-if="item.suffix"></span>
|
<span class="suffix" v-html="item.suffix" v-if="item.suffix"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Props from "@/mixins/Autocomplete/Props"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ["input"],
|
emits: ["blur", "focus", "input", "select"],
|
||||||
props: {
|
mixins: [Props],
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
autofocus: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
showResultsWhenBlank: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
visible: false,
|
visible: false,
|
||||||
curIndex: -1,
|
curIndex: null,
|
||||||
|
selectItemTimer: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -84,7 +54,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
visibleItems() {
|
visibleItems() {
|
||||||
if (!this.value?.length)
|
if (!this.value?.length || this.showAllItems)
|
||||||
return this.items
|
return this.items
|
||||||
|
|
||||||
const val = this.value.toUpperCase()
|
const val = this.value.toUpperCase()
|
||||||
|
@ -103,109 +73,140 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getItemText(item) {
|
getItemText(item) {
|
||||||
return item.text || item
|
return item?.text || item
|
||||||
},
|
|
||||||
|
|
||||||
selectNextItem() {
|
|
||||||
this.curIndex++
|
|
||||||
this.normalizeIndex()
|
|
||||||
},
|
|
||||||
|
|
||||||
selectPrevItem() {
|
|
||||||
this.curIndex--
|
|
||||||
this.normalizeIndex()
|
|
||||||
},
|
|
||||||
|
|
||||||
normalizeIndex() {
|
|
||||||
// Go to the beginning after reaching the end
|
|
||||||
if (this.curIndex >= this.visibleItems.length)
|
|
||||||
this.curIndex = 0
|
|
||||||
|
|
||||||
// Go to the end after moving back from the start
|
|
||||||
if (this.curIndex < 0)
|
|
||||||
this.curIndex = this.visibleItems.length - 1
|
|
||||||
|
|
||||||
// Scroll to the element
|
|
||||||
const curText = this.getItemText(this.visibleItems[this.curIndex])
|
|
||||||
const el = this.$el.querySelector(`[data-item='${curText}']`)
|
|
||||||
if (el)
|
|
||||||
el.scrollIntoView({
|
|
||||||
block: "start",
|
|
||||||
inline: "nearest",
|
|
||||||
behavior: "smooth",
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
valueIsInItems() {
|
valueIsInItems() {
|
||||||
|
if (this.showAllItems)
|
||||||
|
return true
|
||||||
|
|
||||||
if (!this.value)
|
if (!this.value)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
return this.itemsText.indexOf(this.value) >= 0
|
return this.itemsText.indexOf(this.value) >= 0
|
||||||
},
|
},
|
||||||
|
|
||||||
onFocus() {
|
onFocus(e) {
|
||||||
|
this.$emit("focus", e)
|
||||||
if (this.showResultsWhenBlank || this.value?.length)
|
if (this.showResultsWhenBlank || this.value?.length)
|
||||||
this.visible = true
|
this.visible = true
|
||||||
},
|
},
|
||||||
|
|
||||||
onInput(e) {
|
onInput(e) {
|
||||||
let val = e.target.value
|
let val = e.target?.value
|
||||||
|
if (val == null) {
|
||||||
|
e.stopPropagation?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.valueIsInItems())
|
if (this.valueIsInItems())
|
||||||
this.visible = false
|
this.visible = false
|
||||||
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.$emit("input", val.text || val)
|
this.$emit("input", val.text || val)
|
||||||
this.curIndex = -1
|
this.curIndex = null
|
||||||
this.visible = true
|
this.visible = true
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlur(e) {
|
onBlur(e) {
|
||||||
|
if (this.inputOnBlur) {
|
||||||
this.onInput(e)
|
this.onInput(e)
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.valueIsInItems())
|
|
||||||
this.visible = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
onItemSelect(item) {
|
|
||||||
this.$emit("input", item.text || item)
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.valueIsInItems()) {
|
if (this.valueIsInItems()) {
|
||||||
this.visible = false
|
this.visible = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
this.$nextTick(() => this.$refs.input.focus())
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.selectItemTimer) {
|
||||||
|
this.$nextTick(() => this.$refs.input.focus())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit("blur", e)
|
||||||
|
if (this.valueIsInItems()) {
|
||||||
|
this.visible = false
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
|
||||||
|
onItemSelect(item) {
|
||||||
|
if (this.selectItemTimer) {
|
||||||
|
clearTimeout(this.selectItemTimer)
|
||||||
|
this.selectItemTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectItemTimer = setTimeout(() => {
|
||||||
|
this.selectItemTimer = null
|
||||||
|
}, 250)
|
||||||
|
|
||||||
|
const text = item.text || item
|
||||||
|
this.$emit("select", text)
|
||||||
|
if (this.inputOnSelect)
|
||||||
|
this.$emit("input", text)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.valueIsInItems()) {
|
||||||
|
if (this.inputOnSelect) {
|
||||||
|
this.visible = false
|
||||||
|
} else {
|
||||||
|
this.visible = true
|
||||||
|
this.curIndex = this.visibleItems.indexOf(item)
|
||||||
|
if (this.curIndex < 0)
|
||||||
|
this.curIndex = null
|
||||||
|
|
||||||
|
this.$refs.input.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputKeyUp(e) {
|
onInputKeyUp(e) {
|
||||||
if (["ArrowUp", "ArrowDown", "Tab", "Enter", "Escape"].indexOf(e.key) >= 0)
|
if (
|
||||||
|
["ArrowUp", "ArrowDown", "Escape"].indexOf(e.key) >= 0 ||
|
||||||
|
(e.key === "Tab" && this.selectOnTab) ||
|
||||||
|
(e.key === "Enter" && this.curIndex != null)
|
||||||
|
) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === "Enter" && this.valueIsInItems()) {
|
if (e.key === "Enter" && this.valueIsInItems() && this.curIndex != null) {
|
||||||
this.$refs.input.blur()
|
this.$refs.input.blur()
|
||||||
this.visible = false
|
this.visible = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputKeyDown(e) {
|
onInputKeyDown(e) {
|
||||||
|
if (!this.showItems)
|
||||||
|
return
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
e.key === 'ArrowDown' ||
|
e.key === 'ArrowDown' ||
|
||||||
(e.key === 'Tab' && !e.shiftKey) ||
|
(e.key === 'Tab' && !e.shiftKey && this.selectOnTab) ||
|
||||||
(e.key === 'j' && e.ctrlKey)
|
(e.key === 'j' && e.ctrlKey)
|
||||||
) {
|
) {
|
||||||
this.selectNextItem()
|
this.curIndex = this.curIndex == null ? 0 : this.curIndex + 1
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
} else if (
|
} else if (
|
||||||
e.key === 'ArrowUp' ||
|
e.key === 'ArrowUp' ||
|
||||||
(e.key === 'Tab' && e.shiftKey) ||
|
(e.key === 'Tab' && e.shiftKey && this.selectOnTab) ||
|
||||||
(e.key === 'k' && e.ctrlKey)
|
(e.key === 'k' && e.ctrlKey)
|
||||||
) {
|
) {
|
||||||
this.selectPrevItem()
|
this.curIndex = this.curIndex == null ? this.visibleItems.length - 1 : this.curIndex - 1
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
if (this.curIndex > -1 && this.visible) {
|
if (this.curIndex != null && this.curIndex >= 0 && this.visible) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.onItemSelect(this.visibleItems[this.curIndex])
|
this.onItemSelect(this.visibleItems[this.curIndex])
|
||||||
this.$refs.input.focus()
|
this.$nextTick(() => this.$refs.input.focus())
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
this.visible = false
|
this.visible = false
|
||||||
|
@ -220,6 +221,33 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
curIndex() {
|
||||||
|
// Do nothing if the index is not set
|
||||||
|
if (this.curIndex == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Go to the beginning after reaching the end
|
||||||
|
if (this.curIndex >= this.visibleItems.length)
|
||||||
|
this.curIndex = 0
|
||||||
|
|
||||||
|
// Go to the end after moving back from the start
|
||||||
|
if (this.curIndex < 0)
|
||||||
|
this.curIndex = this.visibleItems.length - 1
|
||||||
|
|
||||||
|
// Scroll to the element
|
||||||
|
const curText = this.getItemText(this.visibleItems[this.curIndex])
|
||||||
|
const el = this.$el.querySelector(`[data-item='${curText}']`)
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({
|
||||||
|
block: "start",
|
||||||
|
inline: "nearest",
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
document.addEventListener("click", this.onDocumentClick)
|
document.addEventListener("click", this.onDocumentClick)
|
||||||
if (this.autofocus)
|
if (this.autofocus)
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: [String, Number, Object, Array, Boolean],
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
autofocus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
inputOnBlur: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
inputOnSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectOnTab: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
showAllItems: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
showResultsWhenBlank: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
Reference in a new issue