mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
Replace awesomplete with own component
- Add Tree View
This commit is contained in:
parent
667a966769
commit
110300005e
@ -26,10 +26,14 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
iconSVG() {
|
||||
return feather.icons[this.name].toSvg({
|
||||
width: this.size,
|
||||
height: this.size
|
||||
});
|
||||
const icon = feather.icons[this.name];
|
||||
if (!icon) {
|
||||
return '';
|
||||
}
|
||||
return icon.toSvg({
|
||||
width: this.size,
|
||||
height: this.size
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
87
ui/components/Tree/TreeNode.vue
Normal file
87
ui/components/Tree/TreeNode.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="tree-node">
|
||||
<div class="tree-label px-3 py-2" @click.self="toggleChildren">
|
||||
<div @click="toggleChildren">
|
||||
<feather-icon :name="iconName" v-show="iconName" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['tree-children', expanded ? '' : 'd-none']">
|
||||
<tree-node v-for="child in children" :key="child.label"
|
||||
:label="child.label"
|
||||
:parentValue="child.name"
|
||||
:doctype="doctype"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const TreeNode = {
|
||||
props: ['label', 'parentValue', 'doctype'],
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
children: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
if (this.children && this.children.length ==0) return 'chevron-right';
|
||||
return this.expanded ? 'chevron-down' : 'chevron-right';
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TreeNode: () => Promise.resolve(TreeNode)
|
||||
},
|
||||
mounted() {
|
||||
this.settings = frappe.getMeta(this.doctype).treeSettings;
|
||||
},
|
||||
methods: {
|
||||
async toggleChildren() {
|
||||
await this.getChildren();
|
||||
this.expanded = !this.expanded;
|
||||
},
|
||||
async getChildren() {
|
||||
if (this.children) return;
|
||||
|
||||
this.children = [];
|
||||
|
||||
let filters = {
|
||||
[this.settings.parentField]: this.parentValue
|
||||
};
|
||||
|
||||
const children = await frappe.db.getAll({
|
||||
doctype: this.doctype,
|
||||
filters,
|
||||
fields: [this.settings.parentField, 'isGroup', 'name'],
|
||||
orderBy: 'name',
|
||||
order: 'asc'
|
||||
});
|
||||
|
||||
this.children = children.map(c => {
|
||||
c.label = c.name;
|
||||
return c;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default TreeNode;
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "../../styles/variables";
|
||||
|
||||
.tree-node {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.tree-label {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tree-label:hover {
|
||||
background-color: $dropdown-link-hover-bg;;
|
||||
}
|
||||
.tree-children {
|
||||
padding-left: 2.25rem;
|
||||
}
|
||||
</style>
|
53
ui/components/Tree/index.vue
Normal file
53
ui/components/Tree/index.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="p-3 w-50" v-if="rootNode">
|
||||
<tree-node :label="rootNode.label" :parentValue="''" :doctype="doctype" ref="rootNode"/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import TreeNode from './TreeNode';
|
||||
import { setTimeout } from 'timers';
|
||||
|
||||
export default {
|
||||
props: ['doctype'],
|
||||
components: {
|
||||
TreeNode,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rootNode: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.$refs.rootNode.find('.tree-label').click();
|
||||
}, 500);
|
||||
},
|
||||
async mounted() {
|
||||
this.settings = frappe.getMeta(this.doctype).treeSettings;
|
||||
this.rootNode = {
|
||||
label: await this.settings.getRootLabel()
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async getChildren(parentValue) {
|
||||
let filters = {
|
||||
[this.settings.parentField]: parentValue
|
||||
};
|
||||
|
||||
const children = await frappe.db.getAll({
|
||||
doctype: this.doctype,
|
||||
filters,
|
||||
fields: [this.settings.parentField, 'isGroup', 'name'],
|
||||
orderBy: 'name',
|
||||
order: 'asc'
|
||||
});
|
||||
|
||||
return children.map(c => {
|
||||
c.label = c.name;
|
||||
return c;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,91 +1,104 @@
|
||||
<script>
|
||||
import Awesomplete from 'awesomplete';
|
||||
import Data from './Data';
|
||||
|
||||
export default {
|
||||
extends: Data,
|
||||
data() {
|
||||
return {
|
||||
awesomplete: null
|
||||
popupOpen: false,
|
||||
popupItems: [],
|
||||
highlightedItem: -1
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setupAwesomplete();
|
||||
this.awesomplete.container.classList.add('form-control');
|
||||
this.awesomplete.ul.classList.add('dropdown-menu');
|
||||
},
|
||||
methods: {
|
||||
getInputListeners() {
|
||||
return {
|
||||
input: e => {
|
||||
this.updateList(e.target.value);
|
||||
},
|
||||
'awesomplete-select': e => {
|
||||
const value = e.text.value;
|
||||
this.handleChange(value);
|
||||
keydown: e => {
|
||||
if (e.keyCode === 38) {
|
||||
// up
|
||||
this.highlightedItem -= 1;
|
||||
}
|
||||
if (e.keyCode === 40) {
|
||||
// down
|
||||
this.highlightedItem += 1;
|
||||
}
|
||||
if (e.keyCode === 13) {
|
||||
if (this.highlightedItem > -1) {
|
||||
this.onItemClick(this.popupItems[this.highlightedItem]);
|
||||
this.popupOpen = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
focus: async e => {
|
||||
await this.updateList();
|
||||
this.awesomplete.evaluate();
|
||||
this.awesomplete.open();
|
||||
},
|
||||
blur: () => {
|
||||
setTimeout(() => {
|
||||
this.popupOpen = false;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
},
|
||||
async updateList(value) {
|
||||
this.awesomplete.list = await this.getList(value);
|
||||
getChildrenElement(h) {
|
||||
return [
|
||||
this.getLabelElement(h),
|
||||
this.getInputElement(h),
|
||||
this.getDropdownElement(h)
|
||||
];
|
||||
},
|
||||
getList(text) {
|
||||
return this.docfield.getList(text);
|
||||
getDropdownElement(h) {
|
||||
return h('div', {
|
||||
class: ['dropdown-menu w-100', this.popupOpen ? 'show' : '']
|
||||
}, this.getDropdownItems(h));
|
||||
},
|
||||
setupAwesomplete() {
|
||||
const input = this.$refs.input;
|
||||
this.awesomplete = new Awesomplete(input, {
|
||||
minChars: 0,
|
||||
maxItems: 99,
|
||||
sort: this.sort(),
|
||||
filter: this.filter(),
|
||||
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;
|
||||
}
|
||||
getDropdownItems(h) {
|
||||
return this.popupItems.map((item, i) => {
|
||||
return h('a', {
|
||||
class: ['dropdown-item', this.highlightedItem === i ? 'active text-dark' : ''],
|
||||
attrs: {
|
||||
href: '#',
|
||||
'data-value': item.value
|
||||
},
|
||||
on: {
|
||||
click: e => {
|
||||
e.preventDefault();
|
||||
this.onItemClick(item);
|
||||
this.popupOpen = false;
|
||||
}
|
||||
},
|
||||
domProps: {
|
||||
innerHTML: item.label
|
||||
}
|
||||
})
|
||||
});
|
||||
this.bindEvents();
|
||||
},
|
||||
bindEvents() {
|
||||
onItemClick(item) {
|
||||
this.handleChange(item.value);
|
||||
},
|
||||
sort() {
|
||||
// return a function that handles sorting of items
|
||||
async updateList(keyword) {
|
||||
this.popupItems = await this.getList(keyword);
|
||||
this.popupOpen = this.popupItems.length > 0;
|
||||
},
|
||||
filter() {
|
||||
// return a function that filters list suggestions based on input
|
||||
return Awesomplete.FILTER_CONTAINS
|
||||
async getList(text='') {
|
||||
let list = await this.docfield.getList(text);
|
||||
list = list.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
label: item,
|
||||
value: item
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
return list.filter(item => {
|
||||
const string = (item.label + ' ' + item.value).toLowerCase();
|
||||
return string.includes(text.toLowerCase());
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</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>
|
||||
|
@ -37,7 +37,10 @@ export default {
|
||||
attrs: {
|
||||
'data-fieldname': this.docfield.fieldname
|
||||
}
|
||||
}, [this.getLabelElement(h), this.getInputElement(h)]);
|
||||
}, this.getChildrenElement(h));
|
||||
},
|
||||
getChildrenElement(h) {
|
||||
return [this.getLabelElement(h), this.getInputElement(h)]
|
||||
},
|
||||
getLabelElement(h) {
|
||||
return h('label', {
|
||||
@ -77,9 +80,9 @@ export default {
|
||||
},
|
||||
getInputListeners() {
|
||||
return {
|
||||
change: (e) => {
|
||||
this.handleChange(e.target.value);
|
||||
}
|
||||
change: (e) => {
|
||||
this.handleChange(e.target.value);
|
||||
}
|
||||
};
|
||||
},
|
||||
getInputChildren() {
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import feather from 'feather-icons';
|
||||
import Awesomplete from 'awesomplete';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import FeatherIcon from 'frappejs/ui/components/FeatherIcon';
|
||||
import Form from '../Form/Form';
|
||||
@ -18,9 +17,11 @@ export default {
|
||||
async getList(query) {
|
||||
const list = await frappe.db.getAll({
|
||||
doctype: this.getTarget(),
|
||||
filters: query ? {
|
||||
keywords: ['like', query]
|
||||
} : null,
|
||||
filters: query
|
||||
? {
|
||||
keywords: ['like', query]
|
||||
}
|
||||
: null,
|
||||
fields: ['name'],
|
||||
limit: 50
|
||||
});
|
||||
@ -41,31 +42,31 @@ export default {
|
||||
value: '__newItem'
|
||||
});
|
||||
},
|
||||
getWrapperElement(h) {
|
||||
getChildrenElement(h) {
|
||||
return [
|
||||
this.getLabelElement(h),
|
||||
this.getInputGroupElement(h),
|
||||
this.getDropdownElement(h)
|
||||
];
|
||||
},
|
||||
getInputGroupElement(h) {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: ['form-group', ...this.wrapperClass],
|
||||
attrs: {
|
||||
'data-fieldname': this.docfield.fieldname
|
||||
}
|
||||
class: ['input-group']
|
||||
},
|
||||
[
|
||||
this.getLabelElement(h),
|
||||
this.getInputGroupElement(h)
|
||||
this.getInputElement(h),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: ['input-group-append']
|
||||
},
|
||||
[this.getFollowLink(h)]
|
||||
)
|
||||
]
|
||||
);
|
||||
},
|
||||
getInputGroupElement(h) {
|
||||
return h('div', {
|
||||
class: ['input-group']
|
||||
}, [
|
||||
this.getInputElement(h),
|
||||
h('div', {
|
||||
class: ['input-group-append']
|
||||
}, [this.getFollowLink(h)])
|
||||
]);
|
||||
},
|
||||
getFollowLink(h) {
|
||||
const doctype = this.getTarget();
|
||||
const name = this.value;
|
||||
@ -84,17 +85,21 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
return h('button', {
|
||||
class: ['btn btn-sm btn-outline-light border d-flex'],
|
||||
attrs: {
|
||||
type: 'button'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.$router.push(`/edit/${doctype}/${name}`);
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
class: ['btn btn-sm btn-outline-light border d-flex'],
|
||||
attrs: {
|
||||
type: 'button'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.$router.push(`/edit/${doctype}/${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [arrow])
|
||||
},
|
||||
[arrow]
|
||||
);
|
||||
},
|
||||
getTarget() {
|
||||
return this.docfield.target;
|
||||
@ -103,7 +108,7 @@ export default {
|
||||
return (a, b) => {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
|
||||
|
||||
if (a.value === '__newItem') {
|
||||
return 1;
|
||||
}
|
||||
@ -126,42 +131,37 @@ export default {
|
||||
if (suggestion.value === '__newItem') {
|
||||
return true;
|
||||
}
|
||||
return Awesomplete.FILTER_CONTAINS(suggestion, txt);
|
||||
};
|
||||
},
|
||||
bindEvents() {
|
||||
onItemClick(item) {
|
||||
if (item.value === '__newItem') {
|
||||
this.openFormModal();
|
||||
} else {
|
||||
this.handleChange(item.value);
|
||||
}
|
||||
},
|
||||
async openFormModal() {
|
||||
const input = this.$refs.input;
|
||||
|
||||
input.addEventListener('awesomplete-select', async e => {
|
||||
// new item action
|
||||
if (e.text && e.text.value === '__newItem') {
|
||||
e.preventDefault();
|
||||
const newDoc = await frappe.getNewDoc(this.getTarget());
|
||||
|
||||
this.$formModal.open(
|
||||
newDoc,
|
||||
{
|
||||
defaultValues: {
|
||||
name: input.value !== '__newItem' ? input.value : null
|
||||
},
|
||||
onClose: () => {
|
||||
// if new doc was not created
|
||||
// then reset the input value
|
||||
if (this.value === '__newItem') {
|
||||
this.handleChange('');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
newDoc.on('afterInsert', data => {
|
||||
// if new doc was created
|
||||
// then set the name of the doc in input
|
||||
this.handleChange(newDoc.name);
|
||||
this.$formModal.close();
|
||||
});
|
||||
const newDoc = await frappe.getNewDoc(this.getTarget());
|
||||
this.$formModal.open(newDoc, {
|
||||
defaultValues: {
|
||||
name: input.value !== '__newItem' ? input.value : null
|
||||
},
|
||||
onClose: () => {
|
||||
// if new doc was not created
|
||||
// then reset the input value
|
||||
if (this.value === '__newItem') {
|
||||
this.handleChange('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newDoc.on('afterInsert', data => {
|
||||
// if new doc was created
|
||||
// then set the name of the doc in input
|
||||
this.handleChange(newDoc.name);
|
||||
this.$formModal.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import ListAndForm from '../pages/ListAndForm';
|
||||
import ListAndPrintView from '../pages/ListAndPrintView';
|
||||
import Tree from '../components/Tree';
|
||||
|
||||
export default [
|
||||
{
|
||||
@ -14,6 +15,12 @@ export default [
|
||||
component: ListAndForm,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/tree/:doctype',
|
||||
name: 'Tree',
|
||||
component: Tree,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/print/:doctype/:name',
|
||||
name: 'PrintView',
|
||||
|
Loading…
Reference in New Issue
Block a user