From 44ea73fb05af414428f2e7696202cc36d52793c5 Mon Sep 17 00:00:00 2001 From: Alex Hoyau Date: Sat, 2 Apr 2022 00:26:28 +0200 Subject: [PATCH] Fix BUG: Dragging multiple blocks reverses the order #3547 --- src/utils/Sorter.js | 69 ++++++++++++++++++++- test/specs/utils/Sorter.js | 121 ++++++++++++++++++++++++++----------- 2 files changed, 153 insertions(+), 37 deletions(-) diff --git a/src/utils/Sorter.js b/src/utils/Sorter.js index fe0da416f..c8939d25b 100644 --- a/src/utils/Sorter.js +++ b/src/utils/Sorter.js @@ -982,6 +982,35 @@ export default Backbone.View.extend({ if (h) plh.style.height = h; }, + /** + * Build an array of all the parents, including the component itself + * @return {Model|null} + */ + parents(model) { + return model ? [model].concat(this.parents(model.parent())) : []; + }, + + /** + * Sort according to the position in the dom + * @param {Object} obj1 contains {model, parents} + * @param {Object} obj2 contains {model, parents} + */ + sort(obj1, obj2) { + // common ancesters + const ancesters = obj1.parents.filter(p => obj2.parents.includes(p)); + const ancester = ancesters[0]; + if (!ancester) { + // this is never supposed to happen + return obj2.model.index() - obj1.model.index(); + } + // find siblings in the common ancester + // the sibling is the element inside the ancester + const s1 = obj1.parents[obj1.parents.indexOf(ancester) - 1]; + const s2 = obj2.parents[obj2.parents.indexOf(ancester) - 1]; + // order according to the position in the DOM + return s2.index() - s1.index(); + }, + /** * Leave item * @param event @@ -1013,9 +1042,43 @@ export default Backbone.View.extend({ if (this.moved && target) { const toMove = this.toMove; const toMoveArr = isArray(toMove) ? toMove : toMove ? [toMove] : [src]; - toMoveArr.forEach(model => { - moved.push(this.move(target, model, lastPos)); - }); + let domPositionOffset = 0; + if (toMoveArr.length === 1) { + // do not sort the array in this case + // there are cases for the sorter where toMoveArr is [undefined] + // which allows the drop from blocks, native D&D and sort of layers in Style Manager + this.move(target, toMoveArr[0], lastPos); + } else { + toMoveArr + // add the model's parents + .map(model => ({ + model, + parents: this.parents(model), + })) + // sort based on elements positions in the dom + .sort(this.sort) + // move each component to the new parent and position + .forEach(({ model }) => { + // store state before move + const index = model.index(); + const parent = model.parent().getEl(); + // move the component to the desired position + moved.push( + this.move(target, model, { + ...lastPos, + indexEl: lastPos.indexEl - domPositionOffset, + index: lastPos.index - domPositionOffset, + }) + ); + // when the element is dragged to the same parent and after its position + // it will be removed from the children list + // in that case we need to adjust the following elements target position + if (parent === target && index <= lastPos.index) { + // the next elements will be inserted 1 element before this one + domPositionOffset++; + } + }); + } } if (this.plh) this.plh.style.display = 'none'; diff --git a/test/specs/utils/Sorter.js b/test/specs/utils/Sorter.js index d4f32ecee..5dc92227e 100644 --- a/test/specs/utils/Sorter.js +++ b/test/specs/utils/Sorter.js @@ -13,7 +13,7 @@ describe('Sorter', () => { var plh; var html; - beforeAll(function() { + beforeAll(function () { fixture = $('
').get(0); }); @@ -260,7 +260,7 @@ describe('Sorter', () => { [0, 0, 50, 100, true], [50, 0, 50, 100, true], [100, 0, 50, 100, true], - [150, 0, 50, 70, true] + [150, 0, 50, 70, true], ]; }); @@ -276,7 +276,7 @@ describe('Sorter', () => { obj.onMove({ pageX: 0, pageY: 0, - target: target + target: target, }); expect(obj.moved).toEqual(1); }); @@ -293,7 +293,7 @@ describe('Sorter', () => { el.style.position = 'absolute'; el.style.top = '0'; var ch = obj.getChildrenDim(el); - ch = ch.map(function(v) { + ch = ch.map(function (v) { return v.slice(0, 5); }); var subPos = obj.offset(sib1); @@ -303,7 +303,7 @@ describe('Sorter', () => { [top, left, 50, 100, true], [top + 50, left + 0, 50, 100, true], [top + 100, left + 0, 50, 100, true], - [top + 100, left + 100, 50, 70, true] + [top + 100, left + 100, 50, 70, true], ]; expect(ch).toEqual(result); }); @@ -335,15 +335,15 @@ describe('Sorter', () => { [top, left, 50, 100, true], [top + 50, left + 0, 50, 100, true], [topSib3, leftSib3, 50, 100, true], - [top + 100, left + 100, 50, 70, true] + [top + 100, left + 100, 50, 70, true], ]; var resultChildren = [ [topSib3, leftSib3, 30, 30, true], - [topSib3 + 30, left + 0, 20, 30, true] + [topSib3 + 30, left + 0, 20, 30, true], ]; var dims = obj.dimsFromTarget(sib3); - dims = dims.map(function(v) { + dims = dims.map(function (v) { return v.slice(0, 5); }); @@ -351,30 +351,22 @@ describe('Sorter', () => { // Inside target var dims = obj.dimsFromTarget(sib3, leftSib3 + 15, topSib3 + 15); - dims = dims.map(function(v) { + dims = dims.map(function (v) { return v.slice(0, 5); }); expect(dims).toEqual(resultChildren); // Exactly on border var bOffset = obj.borderOffset; - var dims = obj.dimsFromTarget( - sib3, - leftSib3 + bOffset, - topSib3 + bOffset - ); - dims = dims.map(function(v) { + var dims = obj.dimsFromTarget(sib3, leftSib3 + bOffset, topSib3 + bOffset); + dims = dims.map(function (v) { return v.slice(0, 5); }); expect(dims).toEqual(resultChildren); // Slightly near border - var dims = obj.dimsFromTarget( - sib3, - leftSib3 + bOffset - 3, - topSib3 + bOffset - ); - dims = dims.map(function(v) { + var dims = obj.dimsFromTarget(sib3, leftSib3 + bOffset - 3, topSib3 + bOffset); + dims = dims.map(function (v) { return v.slice(0, 5); }); expect(dims).toEqual(resultParent); @@ -411,7 +403,7 @@ describe('Sorter', () => { [0, 10, 50, 100, true], [50, 20, 50, 70, true], [100, 30, 50, 100, true], - [150, 40, 50, 70, true] + [150, 40, 50, 70, true], ]; }); @@ -444,10 +436,10 @@ describe('Sorter', () => { parentModel = new Component({ droppable: (srcModel, trgModel) => { return srcModel.getEl().className === 'canDrop'; - } + }, }); parentView = new ComponentTextView({ - model: parentModel + model: parentModel, }); }); @@ -460,10 +452,10 @@ describe('Sorter', () => { tagName: 'div', draggable: true, content: 'Content text', - attributes: { class: 'canDrop' } + attributes: { class: 'canDrop' }, }); var srcView = new ComponentTextView({ - model: srcModel + model: srcModel, }); expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(true); @@ -474,10 +466,10 @@ describe('Sorter', () => { tagName: 'div', draggable: true, content: 'Content text', - attributes: { class: 'cannotDrop' } + attributes: { class: 'cannotDrop' }, }); var srcView = new ComponentTextView({ - model: srcModel + model: srcModel, }); expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(false); @@ -492,10 +484,10 @@ describe('Sorter', () => { srcModel = new Component({ draggable: (srcModel, trgModel) => { return trgModel.getEl().className === 'canDrag'; - } + }, }); srcView = new ComponentTextView({ - model: srcModel + model: srcModel, }); }); @@ -508,10 +500,10 @@ describe('Sorter', () => { tagName: 'div', droppable: true, content: 'Content text', - attributes: { class: 'canDrag' } + attributes: { class: 'canDrag' }, }); var parentView = new ComponentTextView({ - model: parentModel + model: parentModel, }); expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(true); @@ -522,14 +514,75 @@ describe('Sorter', () => { tagName: 'div', droppable: true, content: 'Content text', - attributes: { class: 'cannotDrag' } + attributes: { class: 'cannotDrag' }, }); var parentView = new ComponentTextView({ - model: parentModel + model: parentModel, }); expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(false); }); }); }); + describe('Parents', () => { + var child00; + var child01; + var child0; + var child10; + var child1; + var child2; + var root; + beforeAll(() => { + child00 = new Component({ + tagName: 'div', + name: 'child00', + }); + child01 = new Component({ + tagName: 'div', + name: 'child01', + }); + child0 = new Component({ + tagName: 'div', + name: 'child0', + components: [child00, child01], + }); + child10 = new Component({ + tagName: 'div', + name: 'child10', + }); + child1 = new Component({ + tagName: 'div', + name: 'child1', + components: [child10], + }); + child2 = new Component({ + tagName: 'div', + name: 'child2', + }); + root = new Component({ + tagName: 'div', + name: 'root', + components: [child0, child1, child2], + }); + }); + test('Parents', () => { + expect(obj.parents(root)).toEqual([root]); + expect(obj.parents(child0)).toEqual([child0, root]); + expect(obj.parents(child00)).toEqual([child00, child0, root]); + }); + test('Sort', () => { + const withParents = model => ({ model, parents: obj.parents(model) }); + expect(obj.sort(withParents(child00), withParents(child1))).toEqual(1); + expect(obj.sort(withParents(child00), withParents(child01))).toEqual(1); + expect(obj.sort(withParents(child00), withParents(child10))).toEqual(1); + expect(obj.sort(withParents(child1), withParents(child2))).toEqual(1); + expect(obj.sort(withParents(child10), withParents(child2))).toEqual(1); + + expect(obj.sort(withParents(child1), withParents(child00))).toEqual(-1); + expect(obj.sort(withParents(child01), withParents(child00))).toEqual(-1); + expect(obj.sort(withParents(child10), withParents(child00))).toEqual(-1); + expect(obj.sort(withParents(child2), withParents(child1))).toEqual(-1); + expect(obj.sort(withParents(child2), withParents(child10))).toEqual(-1); + }); + }); });