changeset 492e62d18080 in sao:default
details: https://hg.tryton.org/sao?cmd=changeset;node=492e62d18080
description:
        Add drag and drop and manage sequence

        issue8240
        review265181002
diffstat:

 CHANGELOG              |    2 +
 bower.json             |    3 +-
 images/tryton-drag.svg |    1 +
 index.html             |    1 +
 src/model.js           |   66 ++++++++++
 src/sao.less           |    8 +-
 src/view/form.js       |   31 ++++-
 src/view/tree.js       |  310 +++++++++++++++++++++++++++++++++++++++++++-----
 8 files changed, 384 insertions(+), 38 deletions(-)

diffs (686 lines):

diff -r 98556d1accc8 -r 492e62d18080 CHANGELOG
--- a/CHANGELOG Sat Sep 07 23:32:47 2019 +0200
+++ b/CHANGELOG Sat Sep 07 23:56:25 2019 +0200
@@ -1,3 +1,5 @@
+* Manage sequence
+* Add drag and drop on tree views
 * Support expand on column
 * Set minimal width for columns
 * Order Dict keys by sequence
diff -r 98556d1accc8 -r 492e62d18080 bower.json
--- a/bower.json        Sat Sep 07 23:32:47 2019 +0200
+++ b/bower.json        Sat Sep 07 23:56:25 2019 +0200
@@ -24,7 +24,8 @@
     "papaparse": "^5.0",
     "fullcalendar": "^3.0",
     "mousetrap": "^1.6",
-    "bootstrap-rtl-ondemand": "^3.3.4-ondemand"
+    "bootstrap-rtl-ondemand": "^3.3.4-ondemand",
+    "Sortable": "sortablejs#^1.8.4"
   },
   "devDependencies": {
     "qunit": "^1.18"
diff -r 98556d1accc8 -r 492e62d18080 images/tryton-drag.svg
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/images/tryton-drag.svg    Sat Sep 07 23:56:25 2019 +0200
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="24" height="24" viewBox="0 0 24 
24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 18c0 1.1-.9 2-2 
2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 
2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 
2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 
2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
\ No newline at end of file
diff -r 98556d1accc8 -r 492e62d18080 index.html
--- a/index.html        Sat Sep 07 23:32:47 2019 +0200
+++ b/index.html        Sat Sep 07 23:56:25 2019 +0200
@@ -21,6 +21,7 @@
         <script type="text/javascript" 
src="bower_components/fullcalendar/dist/fullcalendar.min.js"></script>
         <script type="text/javascript" 
src="bower_components/fullcalendar/dist/locale-all.js"></script>
         <script type="text/javascript" 
src="bower_components/mousetrap/mousetrap.min.js"></script>
+        <script type="text/javascript" 
src="bower_components/Sortable/Sortable.js"></script>
 
         <link rel="stylesheet" 
href="bower_components/bootstrap/dist/css/bootstrap.min.css">
         <link rel="stylesheet" 
href="bower_components/bootstrap-rtl-ondemand/dist/css/bootstrap-rtl-ondemand.min.css">
diff -r 98556d1accc8 -r 492e62d18080 src/model.js
--- a/src/model.js      Sat Sep 07 23:32:47 2019 +0200
+++ b/src/model.js      Sat Sep 07 23:56:25 2019 +0200
@@ -480,6 +480,45 @@
             };
             return jQuery.when().then(browse_child);
         };
+        array.set_sequence = function(field) {
+            var changed = false;
+            var prev = null;
+            var record, index, update, value;
+            for (var i=0; i < this.length; i++) {
+                record = this[i];
+                if (record.get_loaded([field]) || changed || record.id < 0) {
+                    if (prev) {
+                        index = prev.field_get(field);
+                    } else {
+                        index = null;
+                    }
+                    update = false;
+                    value = record.field_get(field);
+                    if (value === null) {
+                        if (index) {
+                            update = true;
+                        } else if (prev && (record.id >= 0)) {
+                            update = record.id < prev.id;
+                        }
+                    } else if (value === index) {
+                        if (prev && (record.id >= 0)) {
+                            update = record.id < prev.id;
+                        }
+                    } else if (value <= (index || 0)) {
+                        update = true;
+                    }
+                    if (update) {
+                        if (index === null) {
+                            index = 0;
+                        }
+                        index += 1;
+                        record.field_set_client(field, index);
+                        changed = record;
+                    }
+                }
+                prev = record;
+            }
+        };
         return array;
     };
 
@@ -1256,6 +1295,33 @@
             path.reverse();
             return path;
         },
+        children_group: function(field_name) {
+            var group_prm = jQuery.Deferred();
+            if (!field_name) {
+                group_prm.resolve([]);
+                return group_prm;
+            }
+            var load_prm = this._check_load([field_name]);
+            load_prm.done(function() {
+                var group = this._values[field_name];
+                if (group === undefined) {
+                    group_prm.resolve(null);
+                    return;
+                }
+
+                if (group.model.fields !== this.group.model.fields) {
+                    jQuery.extend(this.group.model.fields, group.model.fields);
+                    group.model.fields = this.group.model.fields;
+                }
+                group.on_write = this.group.on_write;
+                group.readonly = this.group.readonly;
+                jQuery.extend(group._context, this.group._context);
+
+                group_prm.resolve(group);
+                return;
+            }.bind(this));
+            return group_prm;
+        },
         get deleted() {
             return Boolean(~this.group.record_deleted.indexOf(this));
         },
diff -r 98556d1accc8 -r 492e62d18080 src/sao.less
--- a/src/sao.less      Sat Sep 07 23:32:47 2019 +0200
+++ b/src/sao.less      Sat Sep 07 23:56:25 2019 +0200
@@ -213,10 +213,13 @@
         width: 100%;
         table-layout: fixed;
 
-        col.selection-state, col.favorite {
+        col.selection-state, col.favorite, col.draggable-handle {
             width: 30px;
         }
 
+        td.draggable-handle {
+            cursor: grab;
+        }
         th.selection-state, td.selection-state {
             > input {
                 .center-block();
@@ -245,6 +248,9 @@
                 vertical-align: middle;
             }
         }
+        > tbody > tr.dragged-row {
+            background-color: @state-info-bg;
+        }
 
         td.prefix {
             padding-right: @table-cell-padding;
diff -r 98556d1accc8 -r 492e62d18080 src/view/form.js
--- a/src/view/form.js  Sat Sep 07 23:32:47 2019 +0200
+++ b/src/view/form.js  Sat Sep 07 23:56:25 2019 +0200
@@ -2959,6 +2959,17 @@
                         !this.position || !access.write || !access.read);
             }
         },
+        _sequence: function() {
+            for (var i=0, len = this.screen.views.length; i < len; i++) {
+                var view = this.screen.views[i];
+                if (view.view_type == 'tree') {
+                    var sequence = view.attributes.sequence;
+                    if (sequence) {
+                        return sequence;
+                    }
+                }
+            }
+        },
         display: function() {
             Sao.View.Form.One2Many._super.display.call(this);
 
@@ -3027,7 +3038,7 @@
             domain = ['OR', domain, ['id', 'in', removed_ids]];
             var text = this.wid_text.val();
 
-            // TODO sequence
+            var sequence = self._sequence();
 
             var callback = function(result) {
                 var prm = jQuery.when();
@@ -3039,6 +3050,9 @@
                     }
                     this.screen.group.load(ids, true);
                     prm = this.screen.display();
+                    if (sequence) {
+                        this.screen.group.set_sequence(sequence);
+                    }
                 }
                 prm.done(function() {
                     this.screen.set_cursor();
@@ -3083,17 +3097,22 @@
         new_single: function() {
             var context = jQuery.extend({},
                     this.field.get_context(this.record));
-            // TODO sequence
+            var sequence = this._sequence();
+            var update_sequence = function() {
+                if (sequence) {
+                    this.screen.group.set_sequence(sequence);
+                }
+            }.bind(this);
             if (this.screen.current_view.type == 'form' ||
                     this.screen.current_view.editable) {
-                this.screen.new_();
+                this.screen.new_().then(update_sequence);
                 this.screen.current_view.el.prop('disabled', false);
             } else {
                 var record = this.record;
                 var field_size = record.expr_eval(
                     this.attributes.size) || -1;
                 field_size -= this.field.get_eval(record);
-                var win = new Sao.Window.Form(this.screen, function() {}, {
+                var win = new Sao.Window.Form(this.screen, update_sequence, {
                     new_: true,
                     many: field_size,
                     context: context,
@@ -3162,6 +3181,10 @@
                                 record.set_default(default_value);
                             });
                         });
+                        var sequence = this._sequence();
+                        if (sequence) {
+                            screen.group.set_sequence(sequence);
+                        }
                     };
 
                     search_set();
diff -r 98556d1accc8 -r 492e62d18080 src/view/tree.js
--- a/src/view/tree.js  Sat Sep 07 23:32:47 2019 +0200
+++ b/src/view/tree.js  Sat Sep 07 23:56:25 2019 +0200
@@ -76,6 +76,7 @@
     Sao.View.Tree = Sao.class_(Sao.View, {
         view_type: 'tree',
         xml_parser: Sao.View.TreeXMLViewParser,
+        draggable: false,
         init: function(view_id, screen, xml, children_field) {
             this.children_field = children_field;
             this.sum_widgets = {};
@@ -99,7 +100,7 @@
                 this.treeview.scrollLeft(this.scrollbar.parent().scrollLeft());
             }.bind(this));
 
-            this.expanded = {};
+            this.expanded = new Set();
 
             Sao.View.Tree._super.init.call(this, view_id, screen, xml);
 
@@ -113,10 +114,10 @@
                 this.table.addClass('table-bordered');
             }
             this.treeview.append(this.table);
-            var colgroup = jQuery('<colgroup/>').appendTo(this.table);
+            this.colgroup = jQuery('<colgroup/>').appendTo(this.table);
             var col = jQuery('<col/>', {
                 'class': 'selection-state',
-            }).appendTo(colgroup);
+            }).appendTo(this.colgroup);
             if (this.selection_mode == Sao.common.SELECTION_NONE) {
                 col.css('width', 0);
             }
@@ -146,7 +147,7 @@
             this.columns.forEach(function(column) {
                 col = jQuery('<col/>', {
                     'class': column.attributes.widget,
-                }).appendTo(colgroup);
+                }).appendTo(this.colgroup);
                 th = jQuery('<th/>', {
                     'class': column.attributes.widget,
                 });
@@ -197,6 +198,8 @@
             this.tbody = jQuery('<tbody/>');
             this.table.append(this.tbody);
 
+            this.set_drag_and_drop();
+
             this.display_size = Sao.config.display_size;
         },
         get editable() {
@@ -292,6 +295,195 @@
                 }
             });
         },
+        _add_drag_n_drop: function() {
+            Sortable.create(this.tbody[0], {
+                handle: '.draggable-handle',
+                ghostClass: 'dragged-row'
+            });
+            this.tbody.on('dragstart', this.drag_data_get.bind(this));
+            this.tbody.on('drop', this.drag_data_received.bind(this));
+        },
+        set_drag_and_drop: function() {
+            var dnd = false;
+            var children, parent_name;
+            if (this.children_field) {
+                children = this.screen.model.fields[this.children_field];
+                if (children) {
+                    parent_name = children.description.relation_field;
+                    dnd = Boolean(this.widgets[parent_name]);
+                }
+            } else if (this.attributes.sequence) {
+                dnd = true;
+            }
+            if (this.screen.readonly) {
+                dnd = false;
+            }
+
+            this.draggable = dnd;
+            if (dnd) {
+                this.colgroup.prepend(jQuery('<col/>', {
+                    'class': 'draggable-handle',
+                }));
+                this.thead.children().prepend(jQuery('<th/>', {
+                    'class': 'draggable-handle',
+                }));
+                this._add_drag_n_drop();
+            }
+        },
+        drag_data_get: function(evt) {
+            var row_position = 0;
+            var row_leaves = [];
+            var set_dragged_row = function(row) {
+                if (row.el[0] === evt.target) {
+                    evt.originalEvent.dataTransfer.setData('path', row.path);
+                    evt.originalEvent.dataTransfer.setData(
+                        'position', row_position);
+                }
+                if (row.rows.length === 0) {
+                    row_leaves.push(row);
+                }
+                row_position += 1;
+                row.rows.forEach(set_dragged_row.bind(this));
+            };
+            this.rows.forEach(set_dragged_row.bind(this));
+        },
+        drag_data_received: function(evt) {
+            var dataTransfer = evt.originalEvent.dataTransfer;
+            var origin_path = dataTransfer.getData('path').split('.');
+            if (origin_path.length === 0) {
+                return ;
+            }
+
+            var row = this;
+            while (origin_path.length > 0) {
+                row = row.rows[origin_path[0]];
+                origin_path = origin_path.slice(1);
+            }
+            var record = row.record;
+
+            var parent_row = null;
+            var dest_position;
+            if (evt.ctrlKey && this.children_field) {
+                parent_row = this._find_row(row.el.prev());
+                dest_position = (parent_row || this).rows.length;
+            } else {
+                var sibling_row;
+                if (evt.shiftKey) {
+                    sibling_row = this._find_row(row.el.prev());
+                    if (sibling_row) {
+                        parent_row = sibling_row.parent_;
+                        dest_position = (
+                            (parent_row || this).rows.indexOf(sibling_row) + 
1);
+                    } else {
+                        parent_row = null;
+                        dest_position = 0;
+                    }
+                } else {
+                    sibling_row = this._find_row(row.el.next());
+                    if (sibling_row) {
+                        parent_row = sibling_row.parent_;
+                        dest_position = (
+                            (parent_row || this).rows.indexOf(sibling_row));
+                    } else {
+                        parent_row = null;
+                        dest_position = this.rows.length;
+                    }
+                }
+            }
+
+            var current_row = parent_row;
+            while (current_row && (current_row != row)) {
+                current_row = current_row.parent_;
+            }
+            if (current_row) {
+                // There is a recursion cancel the drop
+                // by moving the row at its previous place
+                var original_position = dataTransfer.getData('position');
+                var successor = jQuery(
+                    this.tbody.children()[original_position]);
+                successor.before(row.el);
+                return;
+            }
+
+            var previous_row = row;
+            var move_child = function(child_row) {
+                previous_row.el.after(child_row.el);
+                previous_row = child_row;
+                child_row.rows.forEach(move_child);
+            };
+            row.rows.forEach(move_child);
+
+            var dest_group_prm;
+            var origin_group, origin_position;
+            origin_group = record.group;
+            origin_position = row.group_position;
+            if (parent_row) {
+                dest_group_prm = parent_row.record.children_group(
+                    this.children_field);
+            } else {
+                dest_group_prm = jQuery.Deferred().resolve(this.group);
+            }
+
+            dest_group_prm.then(function(dest_group) {
+                var origin_rows, dest_rows;
+                if (row.parent_) {
+                    origin_rows = row.parent_.rows;
+                } else {
+                    origin_rows = this.rows;
+                }
+                if (parent_row) {
+                    dest_rows = parent_row.rows;
+                } else {
+                    dest_rows = this.rows;
+                }
+
+                if (origin_group === dest_group) {
+                    if (origin_position < dest_position) {
+                        dest_position -= 1;
+                    }
+                    origin_group.splice(origin_position, 1);
+                    origin_group.splice(dest_position, 0, record);
+                    origin_group.changed();
+                } else {
+                    origin_group.remove(record, true, true, true);
+                    // Don't remove record from previous group
+                    // as the new parent will change the parent
+                    // This prevents concurrency conflict
+                    origin_group.record_removed.splice(
+                        origin_group.record_removed.indexOf(record));
+                    dest_group.add(record, dest_position);
+                    if (!record.parent_name) {
+                        record._changed[origin_group.parent_name] = true;
+                        record._values[origin_group.parent_name] = null;
+                    } else {
+                        record._changed[origin_group.parent_name] = true;
+                    }
+                }
+                dest_rows.splice(dest_position, 0, row);
+                origin_rows.splice(origin_position, 1);
+
+                row.parent_ = parent_row;
+                row.record.group = dest_group;
+                dest_rows.slice(dest_position).forEach(function(r) {
+                    r.reset_path();
+                });
+                origin_rows.slice(origin_position).forEach(function(r) {
+                    r.reset_path();
+                });
+
+                var selected = this.get_selected_paths();
+                row.redraw(selected);
+                var child_redraw = function(child_row) {
+                    child_row.redraw(selected);
+                    child_row.rows.forEach(child_redraw);
+                };
+                row.rows.forEach(child_redraw);
+
+                if (this.attributes.sequence) {
+                    row.record.group.set_sequence(this.attributes.sequence);
+                }
+            }.bind(this));
+        },
         get_fields: function() {
             return Object.keys(this.widgets);
         },
@@ -344,7 +536,7 @@
             }.bind(this);
             var min_display_size = Math.min(
                     this.group.length, this.display_size);
-            // XXX find better check to keep focus
+            // XXX find better check to keep focus and expanded
             if (this.children_field) {
                 this.construct();
             } else if ((min_display_size > this.rows.length) &&
@@ -411,7 +603,8 @@
                 if (column.header.hasClass('invisible')) {
                     column.col.css('width', 0);
                     column.col.hide();
-                } else if (!column.col.hasClass('selection-state') &&
+                } else if (!column.col.hasClass('draggable-handle') &&
+                    !column.col.hasClass('selection-state') &&
                     !column.col.hasClass('favorite')) {
                     var width = {
                         'integer': 6,
@@ -458,6 +651,9 @@
             if (!extend) {
                 this.rows = [];
                 this.tbody = jQuery('<tbody/>');
+                if (this.draggable) {
+                    this._add_drag_n_drop();
+                }
                 this.edited_row = null;
             } else {
                 this.tbody.find('tr.more-row').remove();
@@ -818,6 +1014,18 @@
                 row.set_editable();
             }
             this.edited_row = row;
+        },
+        _find_row: function(tr) {
+            var row = null;
+            var find_row = function(r) {
+                if (r.el[0] == tr[0]) {
+                    row = r;
+                    return;
+                }
+                r.rows.forEach(find_row);
+            };
+            this.rows.forEach(find_row);
+            return row;
         }
     });
 
@@ -853,18 +1061,40 @@
             this.parent_ = parent;
             this.children_field = tree.children_field;
             this.expander = null;
+            this._group_position = null;
+            this._path = null;
             this._drawed_record = null;
-            var path = [];
-            if (parent) {
-                path = jQuery.extend([], parent.path.split('.'));
-            }
-            path.push(pos);
-            this.path = path.join('.');
             this.el = jQuery('<tr/>');
             this.el.on('click', this.select_row.bind(this));
         },
+        get group_position() {
+            if (this._group_position === null) {
+                this._group_position = this.record.group.indexOf(this.record);
+            }
+            return this._group_position;
+        },
+        get path() {
+            if (!this._path) {
+                var path, position;
+                if (this.parent_) {
+                    path = jQuery.extend([], this.parent_.path.split('.'));
+                } else {
+                    path = [];
+                }
+                path.push(this.group_position);
+                this._path = path.join('.');
+            }
+            return this._path;
+        },
+        reset_path: function() {
+            this._group_position = null;
+            this._path = null;
+            for (var i=0; i < this.rows.length; i++) {
+                this.rows[i].reset_path();
+            }
+        },
         is_expanded: function() {
-            return (this.path in this.tree.expanded);
+            return this.tree.expanded.has(this);
         },
         get_last_child: function() {
             if (!this.children_field || !this.is_expanded() ||
@@ -892,8 +1122,15 @@
                 el_node.removeChild(el_node.firstChild);
             }
 
-            var td;
+            var td, drag_img;
             this.tree.el.uniqueId();
+            if (this.tree.draggable) {
+                td = jQuery('<td/>', {
+                    'class': 'draggable-handle'
+                });
+                td.append(Sao.common.ICONFACTORY.get_icon_img('tryton-drag'));
+                this.el.append(td);
+            }
             td = jQuery('<td/>', {
                 'class': 'selection-state',
             }).click(function(event_) {
@@ -911,7 +1148,6 @@
             this.selection.change(this.selection_changed.bind(this));
             td.append(this.selection);
 
-            var depth = this.path.split('.').length;
             for (var i = 0; i < this.tree.columns.length; i++) {
                 var column = this.tree.columns[i];
                 td = jQuery('<td/>', {
@@ -945,11 +1181,6 @@
                         'class': 'icon',
                     });
                     this.expander.html('&nbsp;');
-                    var margin = 'margin-left';
-                    if (Sao.i18n.rtl) {
-                        margin = 'margin-right';
-                    }
-                    this.expander.css(margin, (depth - 1) + 'em');
                     this.expander.on('click keypress',
                             
Sao.common.click_press(this.toggle_row.bind(this)));
                     row.append(jQuery('<td/>', {
@@ -988,7 +1219,11 @@
         },
         _get_column_td: function(column_index, row) {
             row = row || this.el;
-            return jQuery(row.children()[column_index + 1]);
+            var offset = 1;  // take into account the selection column
+            if (this.tree.draggable) {
+                offset += 1;
+            }
+            return jQuery(row.children()[column_index + offset]);
         },
         redraw: function(selected, expanded) {
             selected = selected || [];
@@ -1061,19 +1296,24 @@
             var row_id_path = this.get_id_path();
             this.set_selection(Sao.common.contains(selected, row_id_path));
             if (this.children_field) {
+                var depth = this.path.split('.').length;
+                var margin = 'margin-left';
+                if (Sao.i18n.rtl) {
+                    margin = 'margin-right';
+                }
+                this.expander.css(margin, (depth - 1) + 'em');
+
                 var update_expander = function() {
-                    var children = this.record.field_get_client(
-                        this.children_field);
-                    if (this.is_expanded() ||
-                        Sao.common.contains(expanded, row_id_path) &&
-                        !jQuery.isEmptyObject(children)) {
+                    var length = this.record.field_get_client(
+                        this.children_field).length;
+                    if (length && (
+                        this.is_expanded() ||
+                        Sao.common.contains(expanded, row_id_path))) {
                         this.expander.css('visibility', 'visible');
-                        this.tree.expanded[this.path] = this;
+                        this.tree.expanded.add(this);
                         this.expand_children(selected, expanded);
                         this.update_expander(true);
                     } else {
-                        var length = this.record.field_get_client(
-                            this.children_field).length;
                         this.expander.css('visibility',
                             length ? 'visible' : 'hidden');
                         this.update_expander(false);
@@ -1094,7 +1334,7 @@
         toggle_row: function() {
             if (this.is_expanded()) {
                 this.update_expander(false);
-                delete this.tree.expanded[this.path];
+                this.tree.expanded.delete(this);
                 this.collapse_children();
             } else {
                 if (this.tree.n_children(this) > Sao.config.limit) {
@@ -1102,7 +1342,7 @@
                     this.tree.screen.switch_view('form');
                 } else {
                     this.update_expander(true);
-                    this.tree.expanded[this.path] = this;
+                    this.tree.expanded.add(this);
                     this.expand_children();
                 }
             }
@@ -1512,7 +1752,13 @@
                                     if (access.create && !limit) {
                                         prm.then(function() {
                                             return this.tree.screen.new_();
-                                        }.bind(this));
+                                        }.bind(this))
+                                            .then(function(record) {
+                                                var sequence = 
this.tree.attributes.sequence;
+                                                if (sequence) {
+                                                    
record.group.set_sequence(sequence);
+                                                }
+                                            }.bind(this));
                                     }
                                 } else {
                                     this._get_column_td(next_column, next_row)

Reply via email to