btcpayserver/BTCPayServer/wwwroot/js/form-editor.js

240 lines
7.8 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const parseConfig = str => {
try {
return JSON.parse(str)
} catch (err) {
console.error('Error deserializing form config:', err)
}
}
const $config = document.getElementById('FormConfig')
let config = parseConfig($config.value) || {}
const specialFieldTypeOptions = ['fieldset', 'textarea', 'select', 'mirror']
const inputFieldTypeOptions = ['text', 'number', 'password', 'email', 'url', 'tel', 'date', 'hidden']
const fieldTypeOptions = inputFieldTypeOptions.concat(specialFieldTypeOptions)
const getFieldComponent = type => `field-type-${specialFieldTypeOptions.includes(type) ? type : 'input'}`
const fieldProps = {
type: String,
name: String,
label: String,
value: String,
helpText: String,
required: Boolean,
constant: Boolean,
options: Array,
fields: Array,
validationErrors: Array
}
const fieldTypeBase = {
props: {
// internal
path: Array,
// field config
...fieldProps
}
}
const FieldTypeInput = Vue.extend({
mixins: [fieldTypeBase],
name: 'field-type-input',
template: '#field-type-input'
})
const FieldTypeTextarea = Vue.extend({
mixins: [fieldTypeBase],
name: 'field-type-textarea',
template: '#field-type-textarea'
})
const FieldTypeSelect = Vue.extend({
mixins: [fieldTypeBase],
name: 'field-type-select',
template: '#field-type-select',
props: {
options: Array
}
})
const FieldTypeMirror = Vue.extend({
mixins: [fieldTypeBase],
name: 'field-type-mirror',
template: '#field-type-mirror'
})
const components = {
FieldTypeInput,
FieldTypeSelect,
FieldTypeTextarea,
FieldTypeMirror
}
// register fields-editor and field-type-fieldset globally in order to use them recursively
Vue.component('field-type-fieldset', {
mixins: [fieldTypeBase],
template: '#field-type-fieldset',
components,
props: {
fields: Array,
selectedField: fieldProps
}
})
Vue.component('fields-editor', {
template: '#fields-editor',
components,
props: {
path: Array,
fields: Array,
selectedField: fieldProps
},
methods: {
getFieldComponent
}
})
Vue.component('field-editor', {
template: '#field-editor',
components,
data () {
return {
fieldTypeOptions
}
},
props: {
path: Array,
field: fieldProps
},
computed: {
mirroredField() {
return this.field.type === 'mirror' &&
this.$root.allFields.find(f => f.name === this.field.value)
}
},
methods: {
getFieldComponent,
addOption (event) {
if (!this.field.options) this.$set(this.field, 'options', [])
const index = this.field.options.length + 1
this.field.options.push({ value: `newOption${index}`, text: `New option ${index}` })
},
removeOption(event, index) {
this.field.options.splice(index, 1)
},
sortOptions (event) {
const { newIndex, oldIndex } = event
this.field.options.splice(newIndex, 0, this.field.options.splice(oldIndex, 1)[0])
},
addValueMap (event) {
if (!this.field.valuemap) this.$set(this.field, 'valuemap', {})
const index = Object.keys(this.field.valuemap).length + 1;
this.$set(this.field.valuemap, `valuemap_${index}`, '')
},
updateValueMap(oldK, newK, newV) {
if (oldK !== newK) {
Vue.delete(this.field.valuemap, oldK);
}
Vue.set(this.field.valuemap, newK, newV);
},
removeValueMap(event, k) {
Vue.delete(this.field.valuemap, k);
},
}
})
Vue.use(vSortable)
Vue.use(VueSanitizeDirective.default)
new Vue({
el: '#FormEditor',
name: 'form-editor',
data () {
return {
config,
selectedField: null,
editorOffcanvas: null
}
},
computed: {
allFields() {
const getFields = (fields, path) => {
let result = [];
for (const field of fields) {
result.push(field)
if (field.fields && field.fields.length > 0)
result= result.concat(getFields(field.fields, path + field.name));
}
return result;
}
return getFields(this.fields, "")
},
fields() {
return this.config.fields || []
},
configJSON() {
return JSON.stringify(this.config, null, 2)
}
},
methods: {
applyTemplate(id) {
const $template = document.getElementById(`form-template-${id}`)
this.config = JSON.parse($template.innerHTML.trim())
this.selectedField = null
},
updateFromJSON(event) {
const config = parseConfig(event.target.value)
if (!config) return
this.config = config
this.selectedField = null
},
addField(event, path) {
const fields = this.getFieldsForPath(path)
const index = fields.length + 1
const length = fields.push({ type: 'text', name: `newField${index}`, label: `New field ${index}`, fields: [], options: [] })
this.selectedField = fields[length - 1]
this.showOffcanvas()
},
selectField(event, path, index) {
const fields = this.getFieldsForPath(path)
this.selectedField = fields[index]
this.showOffcanvas()
},
removeField(event, path, index) {
const fields = this.getFieldsForPath(path)
fields.splice(index, 1)
this.selectedField = null
},
sortFields(event, path) {
const { newIndex, oldIndex } = event
const fields = this.getFieldsForPath(path)
fields.splice(newIndex, 0, fields.splice(oldIndex, 1)[0])
},
getFieldsForPath (path) {
if (!this.config.fields) this.$set(this.config, 'fields', [])
let fields = this.config.fields
while (path.length) {
const name = path.shift()
const field = fields.find(field => field.name === name)
if (!field.fields) this.$set(field, 'fields', [])
fields = field.fields
}
return fields
},
showOffcanvas() {
if (window.getComputedStyle(this.$refs.editorOffcanvas).visibility === 'hidden')
this.editorOffcanvas.show();
},
hideOffcanvas() {
this.editorOffcanvas.hide();
}
},
mounted () {
if (!this.config.fields || this.config.fields.length === 0) {
this.addField(null,[])
}
this.editorOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(this.$refs.editorOffcanvas);
}
})
})