refactor drag & drop treeview
gitea/salix/1757-drag_and_drop_department This commit looks good Details

This commit is contained in:
Joan Sanchez 2019-10-16 08:56:13 +02:00
parent 4b235faa8a
commit 7aaa8f4a5e
18 changed files with 331 additions and 189 deletions

View File

@ -0,0 +1,20 @@
<div class="node clickable">
<vn-icon
class="arrow"
ng-class="{invisible: !$ctrl.item.sons}"
icon="keyboard_arrow_down"
translate-attr="::{title: 'Toggle'}">
</vn-icon>
<section class="content"></section>
<section class="buttons" ng-if="::!$ctrl.treeview.readOnly">
<vn-icon-button translate-attr="::{title: 'Remove'}"
icon="delete"
ng-click="$ctrl.treeview.onRemove($ctrl.item)"
ng-if="$ctrl.item.parent">
</vn-icon-button>
<vn-icon-button translate-attr="::{title: 'Create'}"
icon="add_circle"
ng-click="$ctrl.treeview.onCreate($ctrl.item)">
</vn-icon-button>
</section>
</div>

View File

@ -2,22 +2,33 @@ import ngModule from '../../module';
class Controller {
constructor($element, $scope, $compile) {
this.$element = $element;
this.$scope = $scope;
this.$compile = $compile;
this.element = $element[0];
this.element.$ctrl = this;
this.element.droppable = true;
this.dropCount = 0;
this.element.classList.add('vn-droppable');
}
$onInit() {
const transcludeElement = this.element.querySelector('.content');
const content = angular.element(transcludeElement);
if (this.item.parent) {
this.treeview.$transclude(($clone, $scope) => {
this.$contentScope = $scope;
$scope.item = this.item;
this.$element.append($clone);
content.append($clone);
});
this.element.draggable = true;
this.element.classList.add('vn-draggable');
} else {
let template = `<span translate>{{$ctrl.treeview.rootLabel}}</span>`;
let $clone = this.$compile(template)(this.$scope);
this.$element.append($clone);
content.append($clone);
}
}
@ -25,10 +36,21 @@ class Controller {
if (this.$contentScope)
this.$contentScope.$destroy();
}
dragEnter() {
this.dropCount++;
if (element != this.dropping) {
this.undrop();
if (element) element.classList.add('dropping');
this.dropping = element;
}
}
}
Controller.$inject = ['$element', '$scope', '$compile'];
ngModule.component('vnTreeviewContent', {
ngModule.component('vnTreeviewChild', {
template: require('./child.html'),
controller: Controller,
bindings: {
item: '<'

View File

@ -1,34 +1,11 @@
<ul ng-if="$ctrl.items">
<li ng-repeat="item in $ctrl.items" >
<div
ng-class="{expanded: item.active}"
ng-click="$ctrl.onClick($event, item)"
class="node clickable">
<vn-icon
class="arrow"
ng-class="{invisible: !item.sons}"
icon="keyboard_arrow_down"
translate-attr="::{title: 'Toggle'}">
</vn-icon>
<vn-treeview-content
item="::item">
</vn-treeview-content>
<section class="buttons" ng-if="::!$ctrl.treeview.readOnly">
<vn-icon-button translate-attr="::{title: 'Remove'}"
icon="delete"
ng-click="$ctrl.treeview.onRemove(item)"
ng-if="item.parent">
</vn-icon-button>
<vn-icon-button translate-attr="::{title: 'Create'}"
icon="add_circle"
ng-click="$ctrl.treeview.onCreate(item)">
</vn-icon-button>
</section>
</div>
<li ng-repeat="item in $ctrl.items">
<vn-treeview-child item="item" ng-class="{expanded: item.active}"
ng-click="$ctrl.onClick($event, item)">
</vn-treeview-child>
<vn-treeview-childs
items="item.childs"
parent="::item">
items="item.childs">
</vn-treeview-childs>
</li>
</ul>

View File

@ -1,18 +1,113 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import './style.scss';
import './childs';
import './content';
import './child';
/**
* Treeview
*/
export default class Treeview extends Component {
constructor($element, $scope, $transclude) {
constructor($element, $scope, $transclude, $window) {
super($element, $scope);
this.$transclude = $transclude;
this.$window = $window;
this.readOnly = true;
this.element.addEventListener('dragstart',
event => this.dragStart(event));
this.element.addEventListener('dragend',
event => this.dragEnd(event));
this.element.addEventListener('dragover',
event => this.dragOver(event));
this.element.addEventListener('drop',
event => this.drop(event));
this.element.addEventListener('dragenter',
event => this.dragEnter(event));
this.element.addEventListener('dragleave',
event => this.dragLeave(event));
this.dropCount = 0;
}
undrop() {
if (!this.dropping) return;
this.dropping.classList.remove('dropping');
this.dropping = null;
}
findDroppable(event) {
let element = event.target;
while (element != this.element && !element.droppable)
element = element.parentNode;
if (element == this.element)
return null;
return element;
}
dragOver(event) {
let scrollY = this.$window.scrollY;
if (event.clientY < this.draggingY)
scrollY -= 2;
else scrollY += 2;
this.draggingY = event.clientY;
this.$window.scrollTo(0, scrollY);
// Prevents page reload
event.preventDefault();
}
dragStart(event) {
event.target.classList.add('dragging');
event.dataTransfer.setData('text', event.target.id);
const element = this.findDroppable(event);
this.dragging = element;
}
dragEnd(event) {
event.target.classList.remove('dragging');
this.undrop();
this.dropCount = 0;
this.dragging = null;
}
dragEnter(event) {
let element = this.findDroppable(event);
if (element) this.dropCount++;
if (element != this.dropping) {
this.undrop();
if (element) element.classList.add('dropping');
this.dropping = element;
}
}
dragLeave(event) {
let element = this.findDroppable(event);
if (element) {
this.dropCount--;
if (this.dropCount == 0) this.undrop();
}
}
drop(event) {
event.preventDefault();
this.element.classList.remove('dropping');
const $dropped = this.dropping.$ctrl.item;
const $dragged = this.dragging.$ctrl.item;
if (!$dropped.active && $dropped.sons) {
this.unfold($dropped).then(() => {
this.emit('drop', {$dropped, $dragged});
});
} else
this.emit('drop', {$dropped, $dragged});
}
get data() {
@ -100,8 +195,14 @@ export default class Treeview extends Component {
}
onCreate(parent) {
if (this.createFunc)
this.createFunc({$parent: parent});
if (this.createFunc) {
if (!parent.active && parent.sons) {
this.unfold(parent).then(() => {
this.createFunc({$parent: parent});
});
} else
this.createFunc({$parent: parent});
}
}
create(item) {
@ -120,12 +221,24 @@ export default class Treeview extends Component {
if (parent) parent.sons++;
}
onDrop(item, dragged, dropped) {
this.emit('drop', {item, dragged, dropped});
move(item, newParent) {
if (newParent == item) return;
if (item.parent) {
const parent = item.parent;
const childs = parent.childs;
const index = childs.indexOf(item);
parent.sons--;
childs.splice(index, 1);
}
item.parent = newParent;
this.create(item);
}
}
Treeview.$inject = ['$element', '$scope', '$transclude'];
Treeview.$inject = ['$element', '$scope', '$transclude', '$window'];
ngModule.component('vnTreeview', {
template: require('./index.html'),

View File

@ -10,22 +10,6 @@ vn-treeview-childs {
li {
list-style: none;
& > .node {
@extend %clickable;
display: flex;
padding: 5px;
align-items: center;
}
& > div > .arrow {
min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
& > div.expanded > .arrow {
transform: rotate(180deg);
}
ul {
padding-left: 2.2em;
}
@ -45,8 +29,27 @@ vn-treeview-childs {
.node:hover > .buttons {
display: block
}
.content {
flex-grow: 1
}
}
vn-treeview-content {
flex-grow: 1
}
vn-treeview-child {
display: block;
.node {
@extend %clickable;
display: flex;
padding: 5px;
align-items: center;
}
& > div > .arrow {
min-width: 24px;
margin-right: 10px;
transition: transform 200ms;
}
&.expanded > div > .arrow {
transform: rotate(180deg);
}
}

View File

@ -1,43 +0,0 @@
import ngModule from '../module';
/**
* Enables a draggable element and his drag events
*
* @return {Object} The directive
*/
export function directive() {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
const element = $element[0];
const isDraggable = $attrs.vnDraggable === 'true';
if (!isDraggable) return;
// Set draggable style properties
element.style.cursor = 'move';
// Enable as draggable element
element.setAttribute('draggable', true);
/**
* Fires when a drag event starts
*/
element.addEventListener('dragstart', event => {
element.style.opacity = 0.5;
event.stopPropagation();
});
/**
* Fires when a drag event ends
*/
element.addEventListener('dragend', event => {
element.style.opacity = 1;
event.stopPropagation();
});
}
};
}
ngModule.directive('vnDraggable', directive);

View File

@ -1,68 +1,43 @@
import ngModule from '../module';
import './droppable.scss';
export function directive($parse) {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
const element = $element[0];
const onDropEvent = $parse($attrs.onDrop);
const isDroppable = $attrs.vnDroppable === 'true';
class Controller {
constructor($element, $, $attrs) {
this.element = $element[0];
this.$ = $;
this.$attrs = $attrs;
if (!isDroppable) return;
this.element.addEventListener('dragover',
event => event.preventDefault()); // Prevents page reload
this.element.addEventListener('drop',
event => this.drop(event));
this.element.addEventListener('dragenter',
event => this.dragEnter(event));
this.element.addEventListener('dragleave',
event => this.dragLeave(event));
}
/**
* Captures current dragging element
*/
element.addEventListener('dragstart', () => {
this.dragged = element;
});
dragEnter(event) {
this.droppedElement = event.target;
this.element.classList.add('dropping');
}
/**
* Enter droppable area event
*/
element.addEventListener('dragenter', event => {
element.classList.add('active');
dragLeave(event) {
if (this.droppedElement === event.target)
this.element.classList.remove('dropping');
}
event.stopImmediatePropagation();
event.preventDefault();
}, false);
/**
* Exit droppable area event
*/
element.addEventListener('dragleave', event => {
element.classList.remove('active');
event.stopImmediatePropagation();
event.preventDefault();
});
/**
* Prevent dragover for allowing
* dispatch drop event
*/
element.addEventListener('dragover', event => {
event.stopPropagation();
event.preventDefault();
});
/**
* Fires when a drop events
*/
element.addEventListener('drop', event => {
element.classList.remove('active');
onDropEvent($scope, {event});
event.stopPropagation();
event.preventDefault();
});
}
};
drop(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.element.classList.remove('dropping');
this.$.$eval(this.$attrs.vnDroppable, {$event: event});
}
}
Controller.$inject = ['$element', '$scope', '$attrs'];
directive.$inject = ['$parse'];
ngModule.directive('vnDroppable', directive);
ngModule.directive('vnDroppable', () => {
return {
controller: Controller
};
});

View File

@ -1,11 +1,25 @@
@import "./variables";
.vn-droppable,
.vn-draggable,
[vn-droppable] {
border: 2px dashed transparent;
border-radius: 0.5em;
transition: all 0.5s;
}
&.active {
.vn-droppable,
[vn-droppable] {
display: block;
&.dropping {
background-color: $color-hover-cd;
border: 2px dashed $color-bg-dark;
border-color: $color-bg-dark;
}
}
.vn-draggable.dragging {
background-color: $color-main-light;
border-color: $color-main;
}

View File

@ -11,7 +11,6 @@ import './bind';
import './repeat-last';
import './title';
import './uvc';
import './draggable';
import './droppable';
import './http-click';
import './http-submit';

View File

@ -24,10 +24,12 @@ export default class EventEmitter {
*/
off(callback) {
if (!this.$events) return;
for (let event in this.$events)
for (let i = 0; i < event.length; i++)
for (let event in this.$events) {
for (let i = 0; i < event.length; i++) {
if (event[i].callback === callback)
event.splice(i--, 1);
}
}
}
/**
@ -37,10 +39,12 @@ export default class EventEmitter {
*/
disconnect(thisArg) {
if (!this.$events) return;
for (let event in this.$events)
for (let i = 0; i < event.length; i++)
for (let event in this.$events) {
for (let i = 0; i < event.length; i++) {
if (event[i].thisArg === thisArg)
event.splice(i--, 1);
}
}
}
/**
@ -72,9 +76,10 @@ export default class EventEmitter {
if (this[prop])
this[prop].disconnect(this);
this[prop] = value;
if (value)
if (value) {
for (let event in handlers)
value.on(event, handlers[event], this);
}
}
}
}

View File

@ -108,5 +108,7 @@
"This postal code is not valid": "This postal code is not valid",
"is invalid": "is invalid",
"The postcode doesn't exists. Ensure you put the correct format": "El código postal no existe. Asegúrate de ponerlo con el formato correcto",
"The department name can't be repeated": "El nombre del departamento no puede repetirse"
"The department name can't be repeated": "El nombre del departamento no puede repetirse",
"You cannot move a parent to any of its sons": "You cannot move a parent to any of its sons",
"You cannot move a parent to its own sons": "You cannot move a parent to its own sons"
}

View File

@ -5,7 +5,7 @@
data="$ctrl.photos">
</vn-crud-model>
<section class="drop-zone" vn-droppable="true" on-drop="$ctrl.onDrop(event)">
<section class="drop-zone" vn-droppable="$ctrl.onDrop($event)">
<section><vn-icon icon="add_circle"></vn-icon></section>
<section translate>Drag & Drop files here...</section>
</section>

View File

@ -35,8 +35,9 @@ class Controller {
}
}
onDrop(event) {
const files = event.dataTransfer.files;
onDrop($event) {
console.log($event);
const files = $event.dataTransfer.files;
this.setDefaultParams().then(() => {
this.dms.files = files;
this.create();

View File

@ -0,0 +1,42 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('moveChild', {
description: 'Changes the parent of a child department',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The department id',
http: {source: 'path'}
}, {
arg: 'parentId',
type: 'Number',
description: 'New parent id',
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/moveChild`,
verb: 'POST'
}
});
Self.moveChild = async(id, parentId = null) => {
const models = Self.app.models;
const child = await models.Department.findById(id);
if (id == parentId) return;
if (parentId) {
const parent = await models.Department.findById(parentId);
if (child.lft < parent.lft && child.rgt > parent.rgt)
throw new UserError('You cannot move a parent to its own sons');
}
return child.updateAttribute('parentFk', parentId);
};
};

View File

@ -2,4 +2,5 @@ module.exports = Self => {
require('../methods/department/getLeaves')(Self);
require('../methods/department/createChild')(Self);
require('../methods/department/removeChild')(Self);
require('../methods/department/moveChild')(Self);
};

View File

@ -16,6 +16,15 @@
},
"parentFk": {
"type": "Number"
},
"lft": {
"type": "Number"
},
"rgt": {
"type": "Number"
},
"sons": {
"type": "Number"
}
}
}

View File

@ -10,7 +10,10 @@
fetch-func="$ctrl.onFetch($item)"
remove-func="$ctrl.onRemove($item)"
create-func="$ctrl.onCreate($parent)"
sort-func="$ctrl.onSort($a, $b)">
sort-func="$ctrl.onSort($a, $b)"
on-drop="$ctrl.onDrop($dropped, $dragged)"
on-drag-start="$ctrl.onDragStart(item)"
on-drag-end="$ctrl.onDragEnd(item)">
{{::item.name}}
</vn-treeview>
</vn-card>

View File

@ -23,19 +23,21 @@ class Controller {
return a.name.localeCompare(b.name);
}
/* onDrop(item, dragged, dropped) {
if (dropped.scope.item) {
const droppedItem = dropped.scope.item;
const draggedItem = dragged.scope.item;
onDrop(dropped, dragged) {
if (!dropped.active) {
this.$.treeview.unfold(dropped).then(() =>
this.move(dropped, dragged));
} else
this.move(dropped, dragged);
}
if (droppedItem.childs)
droppedItem.childs.push(Object.assign({}, draggedItem));
dragged.element.remove();
this.$scope.$apply();
}
} */
move(dropped, dragged) {
const params = dropped ? {parentId: dropped.id} : null;
const query = `/api/departments/${dragged.id}/moveChild`;
this.$http.post(query, params).then(() => {
this.$.treeview.move(dragged, dropped);
});
}
onCreate(parent) {
this.newChild = {
@ -62,9 +64,6 @@ class Controller {
if (parent && parent.id)
params.parentId = parent.id;
if (!parent.active)
this.$.treeview.unfold(parent);
const query = `/api/departments/createChild`;
this.$http.post(query, params).then(res => {
const parent = this.newChild.parent;