n8n/packages/editor-ui/src/components/ExpressionInput.vue

371 lines
8.3 KiB
Vue
Raw Normal View History

2019-06-23 03:35:23 -07:00
<template>
<div>
<div ref="expression-editor" :style="editorStyle" class="ignore-key-press" @keydown.stop></div>
2019-06-23 03:35:23 -07:00
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import 'quill/dist/quill.core.css';
import Quill, { DeltaOperation } from 'quill';
// @ts-ignore
import AutoFormat, { AutoformatHelperAttribute } from 'quill-autoformat';
import {
NodeParameterValue,
Workflow,
WorkflowDataProxy,
} from 'n8n-workflow';
import {
IExecutionResponse,
IVariableItemSelected,
IVariableSelectorOption,
} from '@/Interface';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins';
export default mixins(
workflowHelpers,
)
.extend({
name: 'ExpressionInput',
props: [
'rows',
'value',
'parameter',
'path',
'resolvedValue',
],
data () {
return {
editor: null as null | Quill,
};
},
computed: {
editorStyle () {
let rows = 1;
if (this.rows) {
rows = parseInt(this.rows, 10);
}
return {
'height': Math.max((rows * 26 + 10), 40) + 'px',
};
},
workflow (): Workflow {
return this.getWorkflow();
},
},
watch: {
value () {
if (this.resolvedValue) {
// When resolved value gets displayed update the input automatically
this.initValue();
}
},
},
mounted () {
const that = this;
// tslint:disable-next-line
const Inline = Quill.import('blots/inline');
class VariableField extends Inline {
static create (value: string) {
const node = super.create(value);
node.setAttribute('data-value', value);
node.setAttribute('class', 'variable');
return node;
}
static formats (domNode: HTMLElement) {
// For the not resolved one the value can be read directly from the dom
let variableName = domNode.innerHTML.trim();
if (that.resolvedValue) {
// For the resolve done it has to get the one from creation.
// It will not update on change but because the init runs on every change it does not really matter
variableName = domNode.getAttribute('data-value') as string;
}
const newClasses = that.getPlaceholderClasses(variableName);
if (domNode.getAttribute('class') !== newClasses) {
// Only update when it changed else we get an endless loop!
domNode.setAttribute('class', newClasses);
}
return true;
}
}
VariableField.blotName = 'variable';
VariableField.className = 'variable';
VariableField.tagName = 'span';
Quill.register({
'formats/variable': VariableField,
});
AutoFormat.DEFAULTS = {
expression: {
trigger: /\B[\w\s]/,
2019-06-23 03:35:23 -07:00
find: /\{\{[^\s,;:!?}]+\}\}/i,
format: 'variable',
},
};
this.editor = new Quill(this.$refs['expression-editor'] as Element, {
readOnly: !!this.resolvedValue,
modules: {
autoformat: {},
2020-07-15 02:54:03 -07:00
keyboard: {
bindings: {
'list autofill': {
prefix: /^$/,
},
},
},
2019-06-23 03:35:23 -07:00
},
});
this.editor.root.addEventListener('blur', (event: Event) => {
this.$emit('blur', event);
});
this.initValue();
if (!this.resolvedValue) {
// Only call update when not resolved value gets displayed
this.setFocus();
this.editor.on('text-change', () => this.update());
}
},
methods: {
// ------------------------------- EDITOR -------------------------------
customizeVariable (variableName: string) {
const returnData = {
classes: [] as string[],
message: variableName as string,
};
let value;
try {
value = this.resolveExpression(`=${variableName}`);
if (value !== undefined) {
returnData.classes.push('valid');
} else {
returnData.classes.push('invalid');
}
} catch (e) {
returnData.classes.push('invalid');
}
return returnData;
},
// Resolves the given variable. If it is not valid it will return
// an error-string.
resolveParameterString (variableName: string) {
let returnValue;
try {
returnValue = this.resolveExpression(`=${variableName}`);
2020-04-03 10:37:28 -07:00
} catch (error) {
return `[invalid (${error.message})]`;
2019-06-23 03:35:23 -07:00
}
if (returnValue === undefined) {
2020-04-03 10:37:28 -07:00
return '[not found]';
2019-06-23 03:35:23 -07:00
}
return returnValue;
},
getPlaceholderClasses (variableName: string) {
const customizeData = this.customizeVariable(variableName);
return 'variable ' + customizeData.classes.join(' ');
},
getValue () {
if (!this.editor) {
return '';
}
const content = this.editor.getContents();
if (!content || !content.ops) {
return '';
}
let returnValue = '';
// Convert the editor operations into a string
content.ops.forEach((item: DeltaOperation) => {
if (!item.insert) {
return;
}
returnValue += item.insert;
});
// For some unknown reason does the Quill always return a "\n"
// at the end. Remove it here manually
return '=' + returnValue.replace(/\s+$/g, '');
},
setFocus () {
// TODO: There is a bug that when opening ExpressionEditor and typing directly it shows the first letter and
// then adds the second letter in from of the first on
this.editor!.focus();
},
itemSelected (eventData: IVariableItemSelected) {
// We can only get the selection if editor is in focus so make
// sure it is
this.editor!.focus();
const selection = this.editor!.getSelection();
let addIndex = null;
if (selection) {
addIndex = selection.index;
}
if (addIndex) {
// If we have a location to add variable to add it there
this.editor!.insertText(addIndex, `{{${eventData.variable}}}`, 'variable', true);
this.update();
} else {
// If no position got found add it to end
let newValue = this.value;
if (newValue !== '=') {
newValue += ` `;
}
newValue += `{{${eventData.variable}}}\n`;
this.$emit('change', newValue);
if (!this.resolvedValue) {
Vue.nextTick(() => {
this.initValue();
});
}
}
},
initValue () {
if (!this.value) {
return;
}
let currentValue = this.value;
if (currentValue.charAt(0) === '=') {
currentValue = currentValue.slice(1);
}
// Convert the expression string into a Quill Operations
const editorOperations: DeltaOperation[] = [];
currentValue.replace(/\{\{(.*?)\}\}/ig, '*%%#_@^$1*%%#_@').split('*%%#_@').forEach((value: string) => {
2019-06-23 03:35:23 -07:00
if (!value) {
} else if (value.charAt(0) === '^') {
// Is variable
let displayValue = `{{${value.slice(1)}}}` as string | number | boolean | null | undefined;
2019-06-23 03:35:23 -07:00
if (this.resolvedValue) {
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
displayValue = this.resolveParameterString((displayValue as string).toString()) as NodeParameterValue;
2019-06-23 03:35:23 -07:00
}
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
2019-06-23 03:35:23 -07:00
editorOperations.push({
attributes: {
variable: `{{${value.slice(1)}}}`,
},
insert: (displayValue as string).toString(),
2019-06-23 03:35:23 -07:00
});
} else {
// Is text
editorOperations.push({
insert: value,
});
}
});
// @ts-ignore
this.editor!.setContents(editorOperations);
},
update () {
this.$emit('input', this.getValue());
this.$emit('change', this.getValue());
},
},
});
</script>
<style lang="scss">
.variable-wrapper {
text-decoration: none;
}
.variable-value {
font-weight: bold;
color: #000;
background-color: #c0c0c0;
padding: 3px;
border-radius: 3px;
}
.variable-delete {
position: relative;
left: -3px;
top: -8px;
display: none;
color: #fff;
font-weight: bold;
padding: 2px 4px;
}
.variable-wrapper:hover .variable-delete {
display: inline;
background-color: #AA2200;
border-radius: 5px;
}
.variable {
font-weight: bold;
color: #000;
background-color: #c0c0c0;
padding: 3px;
border-radius: 3px;
margin: 0 2px;
&:first-child {
margin-left: 0;
}
&.invalid {
background-color: #e25e5e;
}
&.valid {
background-color: #37ac37;
}
}
.ql-editor {
padding: 0.5em 1em;
}
.ql-disabled .ql-editor {
border-width: 1px;
border: 1px dashed $--custom-expression-text;
color: $--custom-expression-text;
background-color: $--custom-expression-background;
cursor: not-allowed;
}
.ql-disabled .ql-editor .variable {
color: #303030;
}
</style>