diff options
| author | Serge Pavlyuk <flops@users.noreply.github.com> | 2024-02-27 14:48:29 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-02-27 14:48:29 +0300 |
| commit | 8b641801366e7ada09a5530a432a7d14b33abe7d (patch) | |
| tree | b197a2bda7e8416999bde7bfee675c365fc76054 /web | |
| parent | 688ae4da106b30e152de8251d9670498a0db84ed (diff) | |
Added drag'n'drop for routes (#1915)
* Added drag'n'drop for routes
* Drop handler works only for local dnd events
* Cleanup console.log
Diffstat (limited to 'web')
| -rw-r--r-- | web/html/xui/component/sortableTable.html | 218 | ||||
| -rw-r--r-- | web/html/xui/xray.html | 17 |
2 files changed, 229 insertions, 6 deletions
diff --git a/web/html/xui/component/sortableTable.html b/web/html/xui/component/sortableTable.html new file mode 100644 index 00000000..010d8011 --- /dev/null +++ b/web/html/xui/component/sortableTable.html @@ -0,0 +1,218 @@ +{{define "component/sortableTableTrigger"}} + <a-icon type="drag" + style="cursor: move;" + @mouseup="mouseUpHandler" + @mousedown="mouseDownHandler" + @click="clickHandler" /> +{{end}} + +{{define "component/sortableTable"}} +<script> + const DRAGGABLE_ROW_CLASS = 'draggable-row'; + + const findParentRowElement = (el) => { + if (!el || !el.tagName) { + return null; + } else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) { + return el; + } else if (el.parentNode) { + return findParentRowElement(el.parentNode); + } else { + return null; + } + } + + Vue.component('a-table-sortable', { + data() { + return { + sortingElementIndex: null, + newElementIndex: null, + }; + }, + props: ['data-source', 'customRow'], + inheritAttrs: false, + provide() { + const sortable = {} + + Object.defineProperty(sortable, "setSortableIndex", { + enumerable: true, + get: () => this.setCurrentSortableIndex, + }); + + Object.defineProperty(sortable, "resetSortableIndex", { + enumerable: true, + get: () => this.resetSortableIndex, + }); + + return { + sortable, + } + }, + render: function (createElement) { + return createElement( + 'a-table', + { + class: { + 'ant-table-is-sorting': this.isDragging(), + }, + props: { + ...this.$attrs, + 'data-source': this.records, + customRow: (record, index) => this.customRowRender(record, index), + }, + on: this.$listeners, + nativeOn: { + drop: (e) => this.dropHandler(e), + }, + scopedSlots: this.$scopedSlots, + }, + this.$slots.default, + ) + }, + created() { + this.$memoSort = {}; + }, + methods: { + isDragging() { + const currentIndex = this.sortingElementIndex; + return currentIndex !== null && currentIndex !== undefined; + }, + resetSortableIndex(e, index) { + this.sortingElementIndex = null; + this.newElementIndex = null; + this.$memoSort = {}; + }, + setCurrentSortableIndex(e, index) { + this.sortingElementIndex = index; + }, + dragStartHandler(e, index) { + if (!this.isDragging()) { + e.preventDefault(); + return; + } + }, + dragStopHandler(e, index) { + this.resetSortableIndex(e, index); + }, + dragOverHandler(e, index) { + if (!this.isDragging()) { + return; + } + + e.preventDefault(); + + const currentIndex = this.sortingElementIndex; + if (index === currentIndex) { + this.newElementIndex = null; + return; + } + + const row = findParentRowElement(e.target); + if (!row) { + return; + } + + const rect = row.getBoundingClientRect(); + const offsetTop = e.pageY - rect.top; + + if (offsetTop < rect.height / 2) { + this.newElementIndex = Math.max(index - 1, 0); + } else { + this.newElementIndex = index; + } + }, + dropHandler(e) { + if (this.isDragging()) { + this.$emit('onsort', this.sortingElementIndex, this.newElementIndex); + } + }, + customRowRender(record, index) { + const parentMethodResult = this.customRow?.(record, index) || {}; + const newIndex = this.newElementIndex; + const currentIndex = this.sortingElementIndex; + + return { + ...parentMethodResult, + attrs: { + ...(parentMethodResult?.attrs || {}), + draggable: true, + }, + on: { + ...(parentMethodResult?.on || {}), + dragstart: (e) => this.dragStartHandler(e, index), + dragend: (e) => this.dragStopHandler(e, index), + dragover: (e) => this.dragOverHandler(e, index), + }, + class: { + ...(parentMethodResult?.class || {}), + [DRAGGABLE_ROW_CLASS]: true, + ['dragging']: this.isDragging() + ? (newIndex === null ? index === currentIndex : index === newIndex) + : false, + }, + }; + } + }, + computed: { + records() { + const newIndex = this.newElementIndex; + const currentIndex = this.sortingElementIndex; + + if (!this.isDragging() || newIndex === null || currentIndex === newIndex) { + return this.dataSource; + } + + if (this.$memoSort.newIndex === newIndex) { + return this.$memoSort.list; + } + + let list = [...this.dataSource]; + list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]); + + this.$memoSort = { + newIndex, + list, + }; + + return list; + } + } + }); + + Vue.component('table-sort-trigger', { + template: `{{template "component/sortableTableTrigger"}}`, + props: ['item-index'], + inject: ['sortable'], + methods: { + mouseDownHandler(e) { + if (this.sortable) { + this.sortable.setSortableIndex(e, this.itemIndex); + } + }, + mouseUpHandler(e) { + if (this.sortable) { + this.sortable.resetSortableIndex(e, this.itemIndex); + } + }, + clickHandler(e) { + e.preventDefault(); + }, + } + }) +</script> + +<style> + .ant-table-is-sorting .draggable-row td { + background-color: white !important; + } + .dark .ant-table-is-sorting .draggable-row td { + background-color: var(--dark-color-surface-100) !important; + } + .ant-table-is-sorting .dragging { + opacity: 0.5; + } + .ant-table-is-sorting .dragging .ant-table-row-index { + opacity: 0; + } +</style> +{{end}}
\ No newline at end of file diff --git a/web/html/xui/xray.html b/web/html/xui/xray.html index 5ede3633..5afa77c8 100644 --- a/web/html/xui/xray.html +++ b/web/html/xui/xray.html @@ -290,15 +290,19 @@ <a-alert type="warning" style="margin-bottom: 10px; width: fit-content" message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon></a-alert> <a-button type="primary" icon="plus" @click="addRule">{{ i18n "pages.xray.rules.add" }}</a-button> - <a-table :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered + <a-table-sortable :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered :row-key="r => r.key" :data-source="routingRuleData" :scroll="isMobile ? {} : { x: 1000 }" :pagination="false" :indent-size="0" - :style="isMobile ? 'padding: 5px 0' : 'margin-top: 10px;'"> + :style="isMobile ? 'padding: 5px 0' : 'margin-top: 10px;'" + v-on:onSort="replaceRule"> <template slot="action" slot-scope="text, rule, index"> - [[ index+1 ]] + <table-sort-trigger :item-index="index"></table-sort-trigger> + <span class="ant-table-row-index"> + [[ index+1 ]] + </span> <a-dropdown :trigger="['click']"> <a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon> <a-menu slot="overlay" :theme="themeSwitcher.currentTheme"> @@ -404,7 +408,7 @@ </a-button> </a-popover> </template> - </a-table> + </a-table-sortable> </a-tab-pane> <a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true"> <a-row> @@ -530,7 +534,7 @@ <template slot="selector" slot-scope="text, balancer, index"> <a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag> </template> - </a-table> + </a-table> </a-tab-pane> <a-tab-pane key="tpl-6" tab='DNS' style="padding-top: 20px;" force-render="true"> <setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}' desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item> @@ -630,6 +634,7 @@ </a-layout> {{template "js" .}} {{template "component/themeSwitcher" .}} +{{template "component/sortableTable" .}} {{template "component/setting"}} {{template "ruleModal"}} {{template "outModal"}} @@ -1269,7 +1274,7 @@ newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag); } newTemplateSettings.routing.rules = newRules; - + this.templateSettings = newTemplateSettings; }, addDNSServer(){ |
