diff --git a/app/components/viewer/document/index.hbs b/app/components/viewer/document/index.hbs index b887ef330f66a0a6e64ddfd235a746d636b35c6c..3f6e66c0134b5ce718eee2b74c687f60aa53b8d7 100644 --- a/app/components/viewer/document/index.hbs +++ b/app/components/viewer/document/index.hbs @@ -7,6 +7,8 @@ @onDblClick={{this.onThumbnailDblClick}} @onCheckboxChange={{@onCheckboxChange}} @onThumbnailsPositionChanged={{@onThumbnailsPositionChanged}} + @onAddThumbnailPlaceholderAt={{@onAddThumbnailPlaceholderAt}} + @onRemoveThumbnailPlaceholder={{@onRemoveThumbnailPlaceholder}} @onIncomingPages={{@onIncomingPages}} /> {{/if}} <Viewer::ThumbnailsSwitch diff --git a/app/components/viewer/index.hbs b/app/components/viewer/index.hbs index 693d352c88fd8c9e992c00f7586886d9620790cd..1925961bb7d809f71d10d11cc373c90fb5b96709 100644 --- a/app/components/viewer/index.hbs +++ b/app/components/viewer/index.hbs @@ -53,5 +53,7 @@ @thumbnails_visible={{this.thumbnails_visible}} @onCheckboxChange={{this.onThumbnailCheckboxChange}} @onThumbnailsPositionChanged={{this.onThumbnailsPositionChanged}} + @onAddThumbnailPlaceholderAt={{this.onAddThumbnailPlaceholderAt}} + @onRemoveThumbnailPlaceholder={{this.onRemoveThumbnailPlaceholder}} @onIncomingPages={{this.onIncomingPages}} /> </div> diff --git a/app/components/viewer/index.js b/app/components/viewer/index.js index 4066871d8382112a82c93c3b0c3dd3ab2eed459f..3afae65deb38ea79b212693c65b9f392ee01e9b1 100644 --- a/app/components/viewer/index.js +++ b/app/components/viewer/index.js @@ -5,7 +5,6 @@ import { action } from '@ember/object'; import { A } from '@ember/array'; import { reposition_items, - get_id, detect_order_changes } from 'papermerge/utils/array'; @@ -153,16 +152,64 @@ export default class ViewerComponent extends Component { } @action - onThumbnailsPositionChanged({original_pos, drop_pos, page_ids}) { - let all_pages = this.pages; + onThumbnailsPositionChanged(page_ids) { + /* + ``page_ids`` will be moved to the new position + indicated by drop placeholder. + Page is drop placeholder if ``page.is_drop_placeholder`` is true. + */ + let pages_without_placeholder, + drop_placeholder_pos; + + // learn where user wants to move pages by + // findind drop placeholder position + drop_placeholder_pos = this.pages.findIndex( + item => item.is_drop_placeholder + ); + + // remove placeholder from pages array + this.pages.splice(drop_placeholder_pos, 1); + pages_without_placeholder = this.pages; + console.log(`repositioning items page_ids=${page_ids}`); + console.log(`repositioning items drop_pos=${drop_placeholder_pos}`); + // reposition pages this.pages = reposition_items({ - items: all_pages, + items: pages_without_placeholder, selected_ids: page_ids, - drop_pos: drop_pos + drop_pos: drop_placeholder_pos }); } + @action + onAddThumbnailPlaceholderAt(pos) { + let new_pages, + drop_placeholder; + + new_pages = Array.from(this.pages); + drop_placeholder = {'is_drop_placeholder': true}; + + if (!new_pages.find(item => item.is_drop_placeholder)) { + // Only one drop placeholder is allowed + new_pages.splice(pos, 0, drop_placeholder); + this.pages = new_pages; + } + } + + @action + onRemoveThumbnailPlaceholder() { + let new_pages, + drop_placeholder_pos; + + new_pages = Array.from(this.pages); + drop_placeholder_pos = new_pages.findIndex(item => item.is_drop_placeholder); + + if (drop_placeholder_pos >= 0) { + new_pages.splice(drop_placeholder_pos, 1); + this.pages = new_pages; + } + } + @action async onIncomingPages({page_ids, drop_pos}) { await this.requests.moveToDocument({ diff --git a/app/components/viewer/pages.hbs b/app/components/viewer/pages.hbs index 8c6e3616228753cb1328123a33798243f8b2efb4..59746e7fc377fcc7080dd74d32a7f4f99c31880a 100644 --- a/app/components/viewer/pages.hbs +++ b/app/components/viewer/pages.hbs @@ -1,10 +1,14 @@ <div class="d-flex flex-column pages" {{adjust_element_height}}> {{#each @pages as |page|}} - <Viewer::Page - @page={{page}} - @scroll_to_page={{@scroll_to_page}} - @zoom_factor={{@zoom_factor}} /> + {{#if page.is_drop_placeholder}} + <!-- no op --> + {{else}} + <Viewer::Page + @page={{page}} + @scroll_to_page={{@scroll_to_page}} + @zoom_factor={{@zoom_factor}} /> + {{/if}} {{/each}} <Viewer::Zoom @onZoomIn={{@onZoomIn}} diff --git a/app/components/viewer/thumbnail/drop_placeholder.hbs b/app/components/viewer/thumbnail/drop_placeholder.hbs new file mode 100644 index 0000000000000000000000000000000000000000..479f4a726c2a31f6a83b96e8458e35f6e555fb42 --- /dev/null +++ b/app/components/viewer/thumbnail/drop_placeholder.hbs @@ -0,0 +1,2 @@ +<div class='drop-placeholder thumbnail d-flex flex-column align-items-center px-2'> +</div> diff --git a/app/components/viewer/thumbnail/index.js b/app/components/viewer/thumbnail/index.js index 268c59a982a006d158d6835e8ba03e6a9d2fdbbf..efaec2e5a2ed6db965e176d41ad720713482b58c 100644 --- a/app/components/viewer/thumbnail/index.js +++ b/app/components/viewer/thumbnail/index.js @@ -31,9 +31,12 @@ export default class ViewerThumbnailComponent extends Component { pages: items, page: model, original_pos: original_pos, - source_doc_id: this.args.doc.id + source_doc_id: this.args.doc.id, + element: element }; + element.classList.add('is-being-dragged'); + event.dataTransfer.setData( 'application/x.page', JSON.stringify(data) diff --git a/app/components/viewer/thumbnails/index.hbs b/app/components/viewer/thumbnails/index.hbs index a5d37e3691be78985db3202833a9cc667093367a..c28be67f3386586bafa16cecb8b8e4ac8d13ab0d 100644 --- a/app/components/viewer/thumbnails/index.hbs +++ b/app/components/viewer/thumbnails/index.hbs @@ -8,13 +8,17 @@ {{adjust_element_height}}> {{#each @pages as |page|}} - <Viewer::Thumbnail - @doc={{@doc}} - @page={{page}} - @selectedPages={{@selectedPages}} - @onDblClick={{@onDblClick}} - @onDragendSuccess={{this.onDragendSuccess}} - @onDragendCancel={{this.onDragendCancel}} - @onCheckboxChange={{@onCheckboxChange}} /> + {{#if page.is_drop_placeholder}} + <Viewer::Thumbnail::DropPlaceholder /> + {{else}} + <Viewer::Thumbnail + @doc={{@doc}} + @page={{page}} + @selectedPages={{@selectedPages}} + @onDblClick={{@onDblClick}} + @onDragendSuccess={{this.onDragendSuccess}} + @onDragendCancel={{this.onDragendCancel}} + @onCheckboxChange={{@onCheckboxChange}} /> + {{/if}} {{/each}} </div> diff --git a/app/components/viewer/thumbnails/index.js b/app/components/viewer/thumbnails/index.js index 10729f555557f7e762031990469546d098ac3836..48b4bc37fd70d7c883fc663e3054cd287eab59ac 100644 --- a/app/components/viewer/thumbnails/index.js +++ b/app/components/viewer/thumbnails/index.js @@ -1,17 +1,18 @@ import Component from '@glimmer/component'; import Point from 'papermerge/utils/point'; import { action } from '@ember/object'; -import { get_cursor_pos_within_element } from 'papermerge/utils/dom'; export default class ViewerThumbnailsComponent extends Component { @action - onDragendCancel() { + onDragendCancel({event, element}) { + element.classList.remove('is-being-dragged'); } @action - onDragendSuccess() { + onDragendSuccess({event, element}) { + element.classList.remove('is-being-dragged'); } @action @@ -19,32 +20,23 @@ export default class ViewerThumbnailsComponent extends Component { let data, json_data, page_ids, - original_pos, drop_pos, source_doc_id; - event.preventDefault(); data = event.dataTransfer.getData('application/x.page'); + if (!data) { console.warn('Accepts only application/x.page data'); return; } json_data = JSON.parse(data); - original_pos = json_data['original_pos'] page_ids = json_data['pages'].map(page => page.id); source_doc_id = json_data['source_doc_id']; - drop_pos = get_cursor_pos_within_element( - element, - new Point(event.clientX, event.clientY) - ); - if (source_doc_id == this.args.doc.id) { // pages moved within same document - this.args.onThumbnailsPositionChanged({ - original_pos, drop_pos, page_ids - }); + this.args.onThumbnailsPositionChanged(page_ids); } else { // pages moved to another document this.args.onIncomingPages({ @@ -54,7 +46,78 @@ export default class ViewerThumbnailsComponent extends Component { } @action - onDragOver() { + onDragOver({event, element}) { + /* + Creates DOM placeholder suggesting to user that here he/she can drop the page. + + Only one placeholder DOM element is allowed. + */ + let thumbnail_dom_items, + cursor_coord, + suggested_pos, + rect, + cursor_before_child = 0, + outside_all_thumbnails = true, + svg_element, + data, + json_data, + original_pos; + + if (!element) { + return; + } + + data = event.dataTransfer.getData('application/x.page'); + if (!data) { + console.warn('Accepts only application/x.page data'); + return; + } + json_data = JSON.parse(data); + + original_pos = json_data['original_pos'] + + cursor_coord = new Point(event.clientX, event.clientY); + thumbnail_dom_items = Array.from(element.children); + + thumbnail_dom_items.forEach(thumbnail_dom_item => { + // page_item is DOM element which may be real thumbnail of the page or + // it may be a paceholder used as suggestion that it is OK to drop page here. + // Real page thumbnail DOM element contains DOM element for image/svg + // and DOM element denoting page number + svg_element = thumbnail_dom_item.querySelector('svg'); + if (svg_element) { // in case of thumbnail placeholder, there won't be SVG element + rect = svg_element.getBoundingClientRect(); + + if (cursor_coord.y <= rect.bottom || cursor_coord.y <= rect.top) { + cursor_before_child += 1; + } + // Check if cursor position is outside of any thumbnail i.e. + // position to drop will be suggested only in case cursor coordinate + // is BETWEEN thumbnails images/svg + if ((cursor_coord.y < rect.bottom) && (cursor_coord.y > rect.top)) { + outside_all_thumbnails = false; + } + } + }); + + // position where to suggest page drop + if (element.querySelector('.drop-placeholder')) { + suggested_pos = thumbnail_dom_items.length - cursor_before_child - 1; + } else { + suggested_pos = thumbnail_dom_items.length - cursor_before_child; + } + + if (outside_all_thumbnails) { + // suggest position to drop ONLY if cursor is outside of all thumbnails + console.log(`suggested_pos=${suggested_pos}; original_pos=${original_pos}`); + if (Math.abs(original_pos - suggested_pos) >= 1 && suggested_pos != original_pos + 1) { + // prevent default to allow drop + event.preventDefault(); + this.args.onAddThumbnailPlaceholderAt(suggested_pos); + } + } else { + this.args.onRemoveThumbnailPlaceholder(); + } } @action diff --git a/app/modifiers/draggable.js b/app/modifiers/draggable.js index b35828a23a135a12f8e094ca493b17e891c5052a..3bbfb3887be14a8296d39fa4f9455a22e7007c6c 100644 --- a/app/modifiers/draggable.js +++ b/app/modifiers/draggable.js @@ -112,9 +112,19 @@ export default class DraggableModifier extends Modifier { const ondragend_cancel = this.args.named['onDragendCancel']; if (event.dataTransfer.dropEffect === "move") { - ondragend_success(this.model, this.selected_items); + ondragend_success({ + event: this.event, + element: this.element, + model: this.model, + selected_items: this.selected_items + }); } else { - ondragend_cancel(this.model, this.selected_items); + ondragend_cancel({ + event: this.event, + element: this.element, + model: this.model, + selected_items: this.selected_items + }); } } } diff --git a/app/modifiers/droppable.js b/app/modifiers/droppable.js index 32577290db2a8629905ccc29484334badf75c5c6..1822f8b6019aa21ad8619c2ed3cc6b38ec0015fb 100644 --- a/app/modifiers/droppable.js +++ b/app/modifiers/droppable.js @@ -38,11 +38,17 @@ export default class DrappableModifier extends Modifier { @action onDragOver(event) { - const isNode = event.dataTransfer.types.includes("application/x.node"); + //const isNode = event.dataTransfer.types.includes("application/x.node"); - event.preventDefault(); - if (isNode) { + //event.preventDefault(); + //if (isNode) { //console.log(`dragging over a node`); + //} + let _onDragOver = this.args.named['onDragOver'], element; + + element = this.element; + if (_onDragOver) { + _onDragOver({event, element}); } } diff --git a/app/styles/document_version.scss b/app/styles/document_version.scss index d1baf3eff65c06ce5927f36dbb548f0eb7455342..ee5f580c56971c9ba4442ff8433a540f61de4d87 100644 --- a/app/styles/document_version.scss +++ b/app/styles/document_version.scss @@ -30,6 +30,16 @@ outline: 1px solid #A6DAFF; } } + + .drop-placeholder { + margin: 0.85rem; + min-height: 7rem; + border: 1px dashed #aaa; + } + + .is-being-dragged { + opacity: 0.5; + } } .thumbnails-switch { diff --git a/app/utils/array.js b/app/utils/array.js index 46b92d115b0f28062144cc5258b20781ff5fac73..67bcecce12294ed515cc4c6aac77b6f5f70bc38a 100644 --- a/app/utils/array.js +++ b/app/utils/array.js @@ -124,7 +124,7 @@ function detect_order_changes(arr1, arr2) { } if (arr1.length != arr2.length) { - throw 'Invalid input. Both arrays need to be of same length'; + return false; } arr1.forEach((item, index) => {