Ver código fonte

Added drag'n'drop for routes (#1915)

* Added drag'n'drop for routes

* Drop handler works only for local dnd events

* Cleanup console.log
Serge Pavlyuk 1 ano atrás
pai
commit
8b64180136
2 arquivos alterados com 229 adições e 6 exclusões
  1. 218 0
      web/html/xui/component/sortableTable.html
  2. 11 6
      web/html/xui/xray.html

+ 218 - 0
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}}

+ 11 - 6
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(){