From b0f459227f16ee2aeddc0bc9a0199960bcc17302 Mon Sep 17 00:00:00 2001 From: Eugen Ciur <eugen@papermerge.com> Date: Fri, 11 Mar 2022 22:02:12 +0100 Subject: [PATCH] drag and drop for pages --- app/components/commander/index.hbs | 4 +- app/components/commander/index.js | 73 ++++++++++++++++++- app/components/document/index.hbs | 6 +- app/components/folder/index.hbs | 5 +- app/components/node.js | 20 +++++ app/components/viewer/document/index.hbs | 2 +- app/components/viewer/thumbnail/index.hbs | 7 +- app/components/viewer/thumbnail/index.js | 20 ++++- .../{thumbnails.hbs => thumbnails/index.hbs} | 8 ++ app/components/viewer/thumbnails/index.js | 36 +++++++++ app/modifiers/draggable.js | 52 ++++++------- app/modifiers/droppable.js | 70 +++--------------- app/utils/array.js | 39 ++++++---- 13 files changed, 233 insertions(+), 109 deletions(-) rename app/components/viewer/{thumbnails.hbs => thumbnails/index.hbs} (55%) create mode 100644 app/components/viewer/thumbnails/index.js diff --git a/app/components/commander/index.hbs b/app/components/commander/index.hbs index 455f2d2..8330202 100644 --- a/app/components/commander/index.hbs +++ b/app/components/commander/index.hbs @@ -1,7 +1,9 @@ <div class="panel commander col m-2 p-2 user-select-none" {{droppable onDrop=this.onDrop - onDragOver=this.onDragOver }} + onDragOver=this.onDragOver + onDragEnter=this.onDragEnter + onDragLeave=this.onDragLeave }} {{uiSelect view_mode=this.view_mode enabled_on='grid'}} {{contextMenu}}> diff --git a/app/components/commander/index.js b/app/components/commander/index.js index 789840e..bd35b4f 100644 --- a/app/components/commander/index.js +++ b/app/components/commander/index.js @@ -170,7 +170,64 @@ export default class CommanderComponent extends Component { } @action - async onDrop(data) { + onDrop({event, element}) { + let data, files_list; + const isNodeDrop = event.dataTransfer.types.includes("application/x.node"); + + event.preventDefault(); + element.classList.remove('droparea'); + + if (isNodeDrop) { + // drop incoming from another panel + data = event.dataTransfer.getData('application/x.node'); + if (data) { + this.drop_callback({ + 'application/x.node': JSON.parse(data) + }); + } + } else if (this._is_desktop_drop(event)) { + files_list = this._get_desktop_files(event); + this.drop_callback({ + 'application/x.desktop': files_list + }); + } + } + + _is_desktop_drop(event) { + // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop + let items = event.dataTransfer.items; + let files = event.dataTransfer.files; + + if (items && items.length > 0) { + return true; + } + + return files && files.length > 0; + } + + _get_desktop_files(event) { + // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop + let result = [], i; + + if (event.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (i = 0; i < event.dataTransfer.items.length; i++) { + // If dropped items aren't files, reject them + if (event.dataTransfer.items[i].kind === 'file') { + result.push(event.dataTransfer.items[i].getAsFile()); + } + } + } else { + // Use DataTransfer interface to access the file(s) + for (i = 0; i < event.dataTransfer.files.length; i++) { + result.push(event.dataTransfer.files[i]); + } + } + + return result; + } + + drop_callback(data) { /** * data is a dictionary of following format: * { @@ -239,6 +296,18 @@ export default class CommanderComponent extends Component { } + @action + onDragEnter({event, element}) { + event.preventDefault(); + element.classList.add('droparea'); + } + + @action + onDragLeave({event, element}) { + event.preventDefault(); + element.classList.remove('droparea'); + } + @action onDragendCancel(model) { /** @@ -252,7 +321,7 @@ export default class CommanderComponent extends Component { } @action - onDragendSuccess(model, sel_nodes) { + onDragendSuccess() { /** Action invoked when drag operation for one or multiple nodes succeeded. It is invoked on the SOURCE panel. diff --git a/app/components/document/index.hbs b/app/components/document/index.hbs index 67b7740..86085aa 100644 --- a/app/components/document/index.hbs +++ b/app/components/document/index.hbs @@ -1,7 +1,7 @@ <div class="node document {{if this.is_selected "checked"}}" {{draggable @model - selectedNodes=@selectedNodes - sourceParent=@sourceParen + selectedItems=@selectedNodes + onDragStart=this.onDragStart onDragendSuccess=@onDragendSuccess onDragendCancel=@onDragendCancel}}> <Input @@ -19,4 +19,4 @@ {{yield}} </div> </div> -</div> \ No newline at end of file +</div> diff --git a/app/components/folder/index.hbs b/app/components/folder/index.hbs index 102ae6f..34d8a2d 100644 --- a/app/components/folder/index.hbs +++ b/app/components/folder/index.hbs @@ -1,7 +1,8 @@ <div class="node folder {{if this.is_selected "checked"}}" {{draggable @model - selectedNodes=@selectedNodes + selectedItems=@selectedNodes + onDragStart=this.onDragStart onDragendSuccess=@onDragendSuccess onDragendCancel=@onDragendCancel}}> <Input @@ -12,4 +13,4 @@ <div class="title"> {{yield}} </div> -</div> \ No newline at end of file +</div> diff --git a/app/components/node.js b/app/components/node.js index 24ef3a9..d75fb05 100644 --- a/app/components/node.js +++ b/app/components/node.js @@ -2,6 +2,7 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; + export default class NodeComponent extends Component { /** * Receives arguments: @@ -37,4 +38,23 @@ export default class NodeComponent extends Component { is_selected: is_checked }); } + + @action + onDragStart({event, model, items, canvas}) { + let data; + + data = { + nodes: items, + source_parent: { + id: model.parent.get('id') + } + } + + event.dataTransfer.setData( + "application/x.node", + JSON.stringify(data) + ); + + event.dataTransfer.setDragImage(canvas, 0, -15); + } } diff --git a/app/components/viewer/document/index.hbs b/app/components/viewer/document/index.hbs index 9a7a17b..3b3a77e 100644 --- a/app/components/viewer/document/index.hbs +++ b/app/components/viewer/document/index.hbs @@ -16,4 +16,4 @@ @onZoomOut={{this.onZoomOut}} @onZoomFit={{this.onZoomFit}} @zoom_factor={{this.zoom_factor}} /> -</div> \ No newline at end of file +</div> diff --git a/app/components/viewer/thumbnail/index.hbs b/app/components/viewer/thumbnail/index.hbs index 30de6f9..1a85c20 100644 --- a/app/components/viewer/thumbnail/index.hbs +++ b/app/components/viewer/thumbnail/index.hbs @@ -1,5 +1,10 @@ <div class="thumbnail d-flex flex-column align-items-center px-2 {{if this.is_selected 'checked'}}" + {{draggable @page + selectedItems=@selectedPages + onDragStart=this.onDragStart + onDragendSuccess=@onDragendSuccess + onDragendCancel=@onDragendCancel}} {{on "dblclick" this.onDblClick}} > <Input class="align-self-start m-1" @@ -14,4 +19,4 @@ <div class="number fs-3 m-3"> {{@page.number}} </div> -</div> \ No newline at end of file +</div> diff --git a/app/components/viewer/thumbnail/index.js b/app/components/viewer/thumbnail/index.js index c4d9bb0..6bea713 100644 --- a/app/components/viewer/thumbnail/index.js +++ b/app/components/viewer/thumbnail/index.js @@ -19,6 +19,24 @@ export default class ViewerThumbnailComponent extends Component { }); } + @action + onDragStart({event, model, items, canvas, element}) { + let data; + + data = { + pages: items, + page: model + }; + + event.dataTransfer.setData( + 'application/x.page', + JSON.stringify(data) + ); + + event.dataTransfer.setDragImage(canvas, 0, -15); + console.log(`Thumbnails onDragStart elment=${element}`); + } + get is_selected() { let page = this.args.page, selected_page_ids; @@ -41,4 +59,4 @@ export default class ViewerThumbnailComponent extends Component { set is_selected(value) { } -} \ No newline at end of file +} diff --git a/app/components/viewer/thumbnails.hbs b/app/components/viewer/thumbnails/index.hbs similarity index 55% rename from app/components/viewer/thumbnails.hbs rename to app/components/viewer/thumbnails/index.hbs index b8fc91c..292336e 100644 --- a/app/components/viewer/thumbnails.hbs +++ b/app/components/viewer/thumbnails/index.hbs @@ -1,11 +1,19 @@ <div class="d-flex flex-column thumbnails" + {{droppable + onDrop=this.onDrop + onDragOver=this.onDragOver + onDragEnter=this.onDragEnter + onDragLeave=this.onDragLeave }} + {{adjust_element_height}}> {{#each @pages as |page|}} <Viewer::Thumbnail @page={{page}} @selectedPages={{@selectedPages}} @onDblClick={{@onDblClick}} + @onDragendSuccess={{this.onDragendSuccess}} + @onDragendCancel={{this.onDragendCancel}} @onCheckboxChange={{@onCheckboxChange}} /> {{/each}} </div> diff --git a/app/components/viewer/thumbnails/index.js b/app/components/viewer/thumbnails/index.js new file mode 100644 index 0000000..3268148 --- /dev/null +++ b/app/components/viewer/thumbnails/index.js @@ -0,0 +1,36 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + + +export default class ViewerThumbnailsComponent extends Component { + @action + onDragendCancel() { + } + + @action + onDragendSuccess() { + } + + @action + onDrop({event, element}) { + let data, json_data, page_ids; + + event.preventDefault(); + data = event.dataTransfer.getData('application/x.page'); + json_data = JSON.parse(data); + page_ids = json_data['pages'].map(page => page.id); + console.log(`Thumbnails received: dropped page_ids=${page_ids}`); + } + + @action + onDragOver() { + } + + @action + onDragEnter() { + } + + @action + onDragLeave() { + } +} diff --git a/app/modifiers/draggable.js b/app/modifiers/draggable.js index 733367b..b35828a 100644 --- a/app/modifiers/draggable.js +++ b/app/modifiers/draggable.js @@ -1,6 +1,6 @@ import { action } from '@ember/object'; import Modifier from 'ember-modifier'; -import { merge_nodes } from 'papermerge/utils/array'; +import { merge_items } from 'papermerge/utils/array'; export default class DraggableModifier extends Modifier { @@ -15,6 +15,7 @@ export default class DraggableModifier extends Modifier { Usage: {{draggable @model + onDragStart=this.onDragStart onDragendSuccess=@onDragendSuccess onDragendCancel=@onDragendCancel}} @@ -59,32 +60,33 @@ export default class DraggableModifier extends Modifier { @action onDragStart(event) { - let data, nodes, canvas; + let model, + selected_items, + _onDragStart, + canvas, + items, + element; - this.model = this.args.positional[0]; - this.selected_nodes = this.args.named['selectedNodes']; + model = this.model = this.args.positional[0]; + selected_items = this.selected_items = this.args.named['selectedItems']; + _onDragStart = this.args.named['onDragStart']; // Merge model from which user started dragging - // with rest of selected nodes (in case there are some) - // into one single list of {id: <node_id>} objects. + // with rest of selected items (in case there are some) + // into one single list of {id: <item_id>} objects. // Resulted list won't have any duplicates. - nodes = merge_nodes(this.model.id, this.selected_nodes); - - data = { - nodes: nodes, - source_parent: { - id: this.model.parent.get('id') - } - } - - canvas = this.get_drag_canvas(nodes.length); - - event.dataTransfer.setData( - "application/x.node", - JSON.stringify(data) - ); - - event.dataTransfer.setDragImage(canvas, 0, -15); + items = merge_items(model.id, selected_items); + + element = this.element; + canvas = this.get_drag_canvas(items.length); + + _onDragStart({ + event, + element, + model, + items, + canvas + }); } get_drag_canvas(count) { @@ -110,9 +112,9 @@ export default class DraggableModifier extends Modifier { const ondragend_cancel = this.args.named['onDragendCancel']; if (event.dataTransfer.dropEffect === "move") { - ondragend_success(this.model, this.selected_nodes); + ondragend_success(this.model, this.selected_items); } else { - ondragend_cancel(this.model, this.selected_nodes); + ondragend_cancel(this.model, this.selected_items); } } } diff --git a/app/modifiers/droppable.js b/app/modifiers/droppable.js index 02f2560..3257729 100644 --- a/app/modifiers/droppable.js +++ b/app/modifiers/droppable.js @@ -30,28 +30,10 @@ export default class DrappableModifier extends Modifier { @action onDrop(event) { - let data, files_list; - const isNodeDrop = event.dataTransfer.types.includes("application/x.node"); - const callback = this.args.named['onDrop']; - - event.preventDefault(); - this.element.classList.remove('droparea'); - - if (isNodeDrop && callback) { - // drop incoming from another panel - data = event.dataTransfer.getData('application/x.node'); - if (data) { - callback({ - 'application/x.node': JSON.parse(data) - }); - } - } else if (this._is_desktop_drop(event)) { - files_list = this._get_desktop_files(event); - callback({ - 'application/x.desktop': files_list - }); - } + let _onDrop = this.args.named['onDrop'], element; + element = this.element; + _onDrop({event, element}); } @action @@ -66,47 +48,19 @@ export default class DrappableModifier extends Modifier { @action onDragEnter(event) { - event.preventDefault(); - this.element.classList.add('droparea'); + let _onDragEnter = this.args.named['onDragEnter'], + element; + + element = this.element; + _onDragEnter({event, element}); } @action onDragLeave(event) { - event.preventDefault(); - this.element.classList.remove('droparea'); - } - - _is_desktop_drop(event) { - // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop - let items = event.dataTransfer.items; - let files = event.dataTransfer.files; - - if (items && items.length > 0) { - return true; - } - - return files && files.length > 0; - } - - _get_desktop_files(event) { - // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop - let result = [], i; - - if (event.dataTransfer.items) { - // Use DataTransferItemList interface to access the file(s) - for (i = 0; i < event.dataTransfer.items.length; i++) { - // If dropped items aren't files, reject them - if (event.dataTransfer.items[i].kind === 'file') { - result.push(event.dataTransfer.items[i].getAsFile()); - } - } - } else { - // Use DataTransfer interface to access the file(s) - for (i = 0; i < event.dataTransfer.files.length; i++) { - result.push(event.dataTransfer.files[i]); - } - } + let _onDragLeave = this.args.named['onDragLeave'], + element; - return result; + element = this.element; + _onDragLeave({event, element}); } } diff --git a/app/utils/array.js b/app/utils/array.js index d7f1e17..8744ab5 100644 --- a/app/utils/array.js +++ b/app/utils/array.js @@ -1,32 +1,41 @@ -function merge_nodes(node_id, nodes) { + +function get_id(item) { + if (item.id) { + return item.id; + } + + return item.get('id'); +} + +function merge_items(item_id, items) { /* - Returns a list of {id: <node.id>} objects with no duplicates. - List contains as node_id given as first parameter as well as all nodes + Returns a list of {id: <item.id>} objects with no duplicates. + List contains as item_id given as first parameter as well as all items given as second parameter. */ - let source_nodes; + let result_items; - if (!nodes) { - return [{id: node_id}]; + if (!items) { + return [{id: item_id}]; } - if (!nodes.length) { - return [{id: node_id}]; + if (!items.length) { + return [{id: item_id}]; } - source_nodes = nodes.map(item => { - return {'id': item.get('id')}; + result_items = items.map(item => { + return {'id': get_id(item)}; }); - // if by concatinating nodes with node_id there + // if by concatinating items with item_id there // will be no duplicates: - if (!source_nodes.find(item => item.id == node_id)) { - return [{id: node_id}].concat(source_nodes) + if (!result_items.find(item => item.id == item_id)) { + return [{id: item_id}].concat(result_items) } - return source_nodes; + return result_items; } export { - merge_nodes + merge_items } \ No newline at end of file -- GitLab