mirror of
https://github.com/frappe/books.git
synced 2025-01-10 18:24:40 +00:00
More flexible Base, add more controls
- Autocomplete - Check - Code - Currency - Float - Link
This commit is contained in:
parent
0887c4e74c
commit
7eee7cd24f
@ -1,5 +1,17 @@
|
||||
<template>
|
||||
<form class="frappe-form-layout p-3">
|
||||
<div class="row" v-if="layout" v-for="(section, i) in layout" :key="i">
|
||||
<div class="col" v-for="(column, j) in section.columns" :key="j">
|
||||
<frappe-control
|
||||
v-for="fieldname in column.fields"
|
||||
:key="fieldname"
|
||||
:docfield="getDocField(fieldname)"
|
||||
:value="$data[fieldname]"
|
||||
@change="$emit('field-change', fieldname, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!layout">
|
||||
<frappe-control
|
||||
v-for="docfield in fields"
|
||||
:key="docfield.fieldname"
|
||||
@ -7,6 +19,7 @@
|
||||
:value="$data[docfield.fieldname]"
|
||||
@change="$emit('field-change', docfield.fieldname, $event)"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<script>
|
||||
@ -14,7 +27,7 @@ import FrappeControl from './controls/FrappeControl'
|
||||
|
||||
export default {
|
||||
name: 'FormLayout',
|
||||
props: ['doc', 'fields'],
|
||||
props: ['doc', 'fields', 'layout'],
|
||||
data() {
|
||||
const dataObj = {};
|
||||
for (let field of this.fields) {
|
||||
@ -27,6 +40,11 @@ export default {
|
||||
this[fieldname] = doc[fieldname];
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getDocField(fieldname) {
|
||||
return this.fields.find(df => df.fieldname === fieldname);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FrappeControl
|
||||
}
|
||||
|
@ -4,56 +4,71 @@ import Data from './Data';
|
||||
|
||||
export default {
|
||||
extends: Data,
|
||||
created() {
|
||||
data() {
|
||||
return {
|
||||
awesomplete: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setupAwesomplete();
|
||||
this.awesomplete.container.classList.add('form-control');
|
||||
this.awesomplete.ul.classList.add('dropdown-menu');
|
||||
},
|
||||
methods: {
|
||||
getInputListeners() {
|
||||
return {
|
||||
input: async e => {
|
||||
this.awesomplete.list = await this.getList(e.target.value);
|
||||
},
|
||||
'awesomplete-select': e => {
|
||||
this.$emit('change', e.text.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
getList(text) {
|
||||
return this.docfield.getList(text);
|
||||
},
|
||||
setupAwesomplete() {
|
||||
const input = this.$refs.input;
|
||||
this.awesomplete = new Awesomplete(input, {
|
||||
minChars: 0,
|
||||
maxItems: 99,
|
||||
filter: () => true,
|
||||
sort: (a, b) => {
|
||||
if (a.value === '__newitem' || b.value === '__newitem') {
|
||||
return -1;
|
||||
}
|
||||
return a.value > b.value;
|
||||
}
|
||||
});
|
||||
|
||||
// rebuild the list on input
|
||||
this.input.addEventListener('input', async event => {
|
||||
let list = await this.getList(this.input.value);
|
||||
|
||||
// action to add new item
|
||||
list.push({
|
||||
label: frappe._('+ New {0}', this.label),
|
||||
value: '__newItem'
|
||||
});
|
||||
|
||||
this.awesomplete.list = list;
|
||||
});
|
||||
|
||||
// new item action
|
||||
this.input.addEventListener('awesomplete-select', async e => {
|
||||
if (e.text && e.text.value === '__newItem') {
|
||||
e.preventDefault();
|
||||
const newDoc = await frappe.getNewDoc(this.getTarget());
|
||||
const formModal = await frappe.desk.showFormModal(
|
||||
this.getTarget(),
|
||||
newDoc.name
|
||||
);
|
||||
if (formModal.form.doc.meta.hasField('name')) {
|
||||
formModal.form.doc.set('name', this.input.value);
|
||||
}
|
||||
|
||||
formModal.once('save', async () => {
|
||||
await this.updateDocValue(formModal.form.doc.name);
|
||||
});
|
||||
sort: this.sort(),
|
||||
item: (text, input) => {
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('dropdown-item');
|
||||
li.classList.add('d-flex');
|
||||
li.classList.add('align-items-center');
|
||||
li.innerHTML = text.label;
|
||||
|
||||
return li;
|
||||
}
|
||||
});
|
||||
},
|
||||
sort() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "../../styles/variables";
|
||||
@import "~awesomplete/awesomplete.base";
|
||||
|
||||
.awesomplete {
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
||||
&> ul {
|
||||
padding: $dropdown-padding-y 0;
|
||||
}
|
||||
|
||||
.dropdown-menu:not([hidden]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item[aria-selected="true"] {
|
||||
background-color: $dropdown-link-hover-bg;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -8,12 +8,21 @@ export default {
|
||||
id() {
|
||||
return this.docfield.fieldname + '-'
|
||||
+ document.querySelectorAll(`[data-fieldname="${this.docfield.fieldname}"]`).length;
|
||||
},
|
||||
inputClass() {
|
||||
return [];
|
||||
},
|
||||
wrapperClass() {
|
||||
return [];
|
||||
},
|
||||
labelClass() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWrapperElement(h) {
|
||||
return h('div', {
|
||||
class: ['form-group'],
|
||||
class: ['form-group', ...this.wrapperClass],
|
||||
attrs: {
|
||||
'data-fieldname': this.docfield.fieldname
|
||||
}
|
||||
@ -21,6 +30,7 @@ export default {
|
||||
},
|
||||
getLabelElement(h) {
|
||||
return h('label', {
|
||||
class: this.labelClass,
|
||||
attrs: {
|
||||
for: this.id
|
||||
},
|
||||
@ -30,16 +40,19 @@ export default {
|
||||
});
|
||||
},
|
||||
getInputElement(h) {
|
||||
return h('input', {
|
||||
class: ['form-control'],
|
||||
return h(this.getInputTag(), {
|
||||
class: this.getInputClass(),
|
||||
attrs: this.getInputAttrs(),
|
||||
on: {
|
||||
change: (e) => {
|
||||
this.$emit('change', e.target.value)
|
||||
}
|
||||
},
|
||||
on: this.getInputListeners(),
|
||||
domProps: this.getDomProps(),
|
||||
ref: 'input'
|
||||
})
|
||||
}, this.getInputChildren(h));
|
||||
},
|
||||
getInputTag() {
|
||||
return 'input';
|
||||
},
|
||||
getInputClass() {
|
||||
return ['form-control', ...this.inputClass];
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
@ -48,6 +61,22 @@ export default {
|
||||
placeholder: '',
|
||||
value: this.value
|
||||
}
|
||||
},
|
||||
getInputListeners() {
|
||||
return {
|
||||
change: (e) => {
|
||||
this.$emit('change', this.parseValue(e.target.value));
|
||||
}
|
||||
};
|
||||
},
|
||||
getInputChildren() {
|
||||
return null;
|
||||
},
|
||||
getDomProps() {
|
||||
return null;
|
||||
},
|
||||
parseValue(value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
src/components/controls/Check.vue
Normal file
25
src/components/controls/Check.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input class="custom-control-input" type="checkbox" :id="id" v-model="checkboxValue" @change="emitChange">
|
||||
<label class="custom-control-label" :for="id">{{ docfield.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Base from './Base';
|
||||
export default {
|
||||
extends: Base,
|
||||
data() {
|
||||
return {
|
||||
checkboxValue: Boolean(this.value)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitChange(e) {
|
||||
this.$emit('change', Number(this.checkboxValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
11
src/components/controls/Code.vue
Normal file
11
src/components/controls/Code.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
import Text from './Text';
|
||||
export default {
|
||||
extends: Text,
|
||||
computed: {
|
||||
inputClass() {
|
||||
return ['text-monospace'];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
6
src/components/controls/Currency.vue
Normal file
6
src/components/controls/Currency.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import Float from './Float';
|
||||
export default {
|
||||
extends: Float
|
||||
}
|
||||
</script>
|
24
src/components/controls/Float.vue
Normal file
24
src/components/controls/Float.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script>
|
||||
import Base from './Base';
|
||||
|
||||
export default {
|
||||
extends: Base,
|
||||
computed: {
|
||||
inputClass() {
|
||||
return ['text-right']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInputListeners() {
|
||||
return {
|
||||
change: e => {
|
||||
this.$emit('change', e.target.value)
|
||||
},
|
||||
focus: e => {
|
||||
setTimeout(() => this.$refs.input.select(), 100);
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@ -2,21 +2,30 @@
|
||||
<component :is="component" :docfield="docfield" :value="value" @change="$emit('change', $event)"/>
|
||||
</template>
|
||||
<script>
|
||||
import Autocomplete from './Autocomplete';
|
||||
import Check from './Check';
|
||||
import Code from './Code';
|
||||
import Currency from './Currency';
|
||||
import Data from './Data';
|
||||
import Float from './Float';
|
||||
import Link from './Link';
|
||||
import Select from './Select';
|
||||
import Text from './Text';
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
export default {
|
||||
props: ['docfield', 'value'],
|
||||
computed: {
|
||||
component() {
|
||||
return {
|
||||
Autocomplete,
|
||||
Check,
|
||||
Code,
|
||||
Currency,
|
||||
Data,
|
||||
Float,
|
||||
Link,
|
||||
Select,
|
||||
Text,
|
||||
Autocomplete,
|
||||
Link: Autocomplete
|
||||
}[this.docfield.fieldtype];
|
||||
}
|
||||
}
|
||||
|
45
src/components/controls/Link.vue
Normal file
45
src/components/controls/Link.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import feather from 'feather-icons';
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
export default {
|
||||
extends: Autocomplete,
|
||||
methods: {
|
||||
async getList(query) {
|
||||
const list = await frappe.db.getAll({
|
||||
doctype: this.docfield.target,
|
||||
filters: {
|
||||
keywords: ["like", query]
|
||||
},
|
||||
fields: ['name'],
|
||||
limit: 50
|
||||
});
|
||||
|
||||
const plusIcon = feather.icons.plus.toSvg({
|
||||
class: 'm-1',
|
||||
width: 16,
|
||||
height: 16
|
||||
});
|
||||
|
||||
return list
|
||||
.map(d => ({
|
||||
label: d.name,
|
||||
value: d.name
|
||||
}))
|
||||
.concat({
|
||||
label: plusIcon + ' New ' + this.docfield.target,
|
||||
value: '__newItem'
|
||||
})
|
||||
},
|
||||
sort() {
|
||||
return (a, b) => {
|
||||
if (a.value === '__newitem' || b.value === '__newitem') {
|
||||
return -1;
|
||||
}
|
||||
return a.value > b.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -3,8 +3,16 @@ import Base from './Base';
|
||||
export default {
|
||||
extends: Base,
|
||||
methods: {
|
||||
getInputElement(h) {
|
||||
const options = this.docfield.options.map(option =>
|
||||
getInputTag() {
|
||||
return 'select';
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id
|
||||
};
|
||||
},
|
||||
getInputChildren(h) {
|
||||
return this.docfield.options.map(option =>
|
||||
h('option', {
|
||||
attrs: {
|
||||
key: option,
|
||||
@ -15,19 +23,6 @@ export default {
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return h('select', {
|
||||
class: ['form-control'],
|
||||
attrs: {
|
||||
id: this.id
|
||||
},
|
||||
on: {
|
||||
change: (e) => {
|
||||
this.$emit('change', e.target.value)
|
||||
}
|
||||
},
|
||||
ref: 'input'
|
||||
}, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,23 +3,19 @@ import Base from './Base';
|
||||
export default {
|
||||
extends: Base,
|
||||
methods: {
|
||||
getInputElement(h) {
|
||||
return h('textarea', {
|
||||
class: ['form-control'],
|
||||
attrs: {
|
||||
getInputTag() {
|
||||
return 'textarea';
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id,
|
||||
rows: 3
|
||||
};
|
||||
},
|
||||
domProps: {
|
||||
getDomProps() {
|
||||
return {
|
||||
value: this.value
|
||||
},
|
||||
on: {
|
||||
change: (e) => {
|
||||
this.$emit('change', e.target.value)
|
||||
}
|
||||
},
|
||||
ref: 'input'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user