Source: public/src/util/dragdrop.js

/** @module util/dragdrop */

import React from 'react';
import ReactDOM from 'react-dom';
import { Icon, message } from 'react-aiot';
import { ajax, i18n, rmlOpts } from '.';
import $ from 'jquery';

/**
 * On CTRL holding add class 'aiot-helper-method-append' to document body.
 */
export function anyKeyHolding() {
    $(document).on('keydown',e => $('body').addClass('aiot-helper-method-append'));
    $(document).on('keyup', e => $('body').removeClass('aiot-helper-method-append'));
}

/**
 * jQuery's draggable helper container.
 * 
 * @param {object} props Properties
 * @param {int} props.count The count
 * @type React.Element
 */
const DragHelper = ({ count }) => (<div>
    <div className="aiot-helper-method-move">
        <Icon type="swap" /> { i18n(count > 1 ? 'move' : 'moveOne', { count }) }
        <p>{ i18n('moveTip') }</p>
    </div>
    <div className="aiot-helper-method-append">
        <Icon type="copy" /> { i18n(count > 1 ? 'append' : 'appendOne', { count }) }
        <p>{ i18n('appendTip') }</p>
    </div>
</div>);

/**
 * Enables / Reinitializes the droppable nodes. If a draggable item is dropped
 * here the given posts are moved to the category. You have to provide a ReactJS
 * element to reload the tree.
 * 
 * @param {React.Element} element The element
 */
export function droppable(element) {
    const dom = $(element.ref.container).find('.aiot-node.aiot-droppable[data-id!=\'all\']'),
        { attachmentsBrowser } = element;
    dom.droppable({
        activeClass: 'aiot-state-default',
        hoverClass: 'aiot-state-hover',
        tolerance: 'pointer',
        drop: async function(event, ui) {
            const ids = [],
                toTmp = $(event.target).attr('data-id'),
                to = toTmp === 'all' ? toTmp : +toTmp,
                activeId = element.getSelectedId(),
                elements = [],
                fnFade = percent => elements.forEach(obj => obj.fadeTo(250, percent)),
                isCopy = $('body').hasClass('aiot-helper-method-append'),
                { store } = element.props;
            
            // Get dragged items
            iterateDraggedItem(ui.draggable, element, tr => {
                ids.push(+tr.find('input[name="media[]"]').attr("value"));
                elements.push(tr);
            }, (attributes, attachmentsBrowser) => {
                ids.push(attributes.id);
                elements.push(attachmentsBrowser.$el.find('li[data-id="'  + attributes.id + '"]'));
            });
            element.setState({ isTreeLinkDisabled: true }); // Disable tree
            fnFade(0.3);
            
            // Make folders updateable in grid mode
            if (attachmentsBrowser) {
                // If the target is "Uncategorized" the current folder has to be refreshed, too
                store.addFoldersNeedsRefresh(to);
                to === +rmlOpts.rootId && store.addFoldersNeedsRefresh(activeId);
            }
            
            // Get i18n key
            const isOne = ids.length === 1, i18nProps = {
                count: ids.length,
                category: $(event.target).find('.aiot-node-name').html()
            }, i18nGet = key => i18n((isCopy ? 'append' : 'move') + key + (isOne ? 'One' : ''), i18nProps);
            
            const hide = message.loading(i18nGet('LoadingText'));
            try {
                const { counts } = await ajax('attachments/bulk/move', {
                    method: 'PUT',
                    data: { ids, to, isCopy }
                });
                
                message.success(i18nGet('Success'));
                element.fetchCounts(counts);

                // Update items view
                const fadeBack = isCopy || (!isCopy && activeId === to) || activeId === 'all';
                fadeBack ? fnFade(1) : elements.forEach(obj => obj.remove());
                
                // Refresh view if necessery
                if ((activeId === 'all' && isCopy) || (isCopy && activeId === to)) {
                    element.handleReload();
                }
                
                // Deselect for the next bulk selection action
                elements.forEach(obj => {
                    let attachmentPreview = obj.children('.attachment-preview');
                    obj.hasClass('selected') && attachmentPreview.length && attachmentPreview.click();
                });
                
                // Add no media
                if (!element.attachmentsBrowser && !$(".wp-list-table tbody tr").length) {
                    $(".wp-list-table tbody").html('<tr class="no-items"><td class="colspanchange" colspan="6">' + rmlOpts.lang.noEntries + '</td></tr></tbody>');
                }
            } catch (e) {
                message.error(e.responseJSON.message);
                fnFade(1);
            } finally {
                hide();
                element.setState(prevState => ({
                    isTreeLinkDisabled: false
                })); // Enable tree
            }
        }
    });
}

/*
 * Iterates through the UI and gets the collection of dragged items.
 * 
 * @param {jQuery} ui The draggable ui object
 * @param {React.Element} container The AIOT container
 * @param {function} [listMode] Function to iterate over list mode items (<tr> object)
 * @param {function} [gridMode] Function to iterate over grid mode items (attributes, attachmentsBrowser)
 * @returns {int} The count of selected items
 */
function iterateDraggedItem(ui, { attachmentsBrowser }, listMode, gridMode) {
    if (attachmentsBrowser) {
        // Grid mode
        const selection = attachmentsBrowser.options.selection.models;
        if (selection.length) {
            selection.forEach(model => {
                gridMode && gridMode(model.attributes, attachmentsBrowser);
            });
            return selection.length;
        }else{
            const id = ui.data('id'), models = attachmentsBrowser.collection.models;
            gridMode && gridMode(models.filter(model => model.id === id)[0], attachmentsBrowser);
            return 1;
        }
    }else{
        // List mode
        const trs = $('input[name="media[]"]:checked');
        if (trs.length) {
            trs.each(function() {
                listMode && listMode($(this).parents('tr'));
            });
        }else{
            listMode && listMode(ui);
        }
        return trs.length || 1;
    }
}

/**
 * Make the list table draggable if sort mode is not active.
 * 
 * @param {React.Element} element The element
 * @param {boolean} [destroy=false] If true the draggable gets destroyed
 */
export function draggable(element, destroy) {
    // Get selector
    const attachmentsBrowser = element.attachmentsBrowser,
        { isMoveable, isWPAttachmentsSortMode } = element.state,
        selector = attachmentsBrowser ? attachmentsBrowser.$el.find('ul.attachments > li')
            : $('#wpbody-content .wp-list-table tbody tr:not(.no-items)');
            
    // Make draggable
    if (destroy || !isMoveable || isWPAttachmentsSortMode) {
        try {
            selector.draggable('destroy');
        }catch(e) {
            // Silence is golden.
        }
    }else{
        selector.draggable({
            revert: 'invalid',
            revertDuration: 0,
            appendTo: 'body',
            cursorAt: { top: 0, left: 0 },
            distance: 10,
            refreshPositions: true,
            helper: (event) => {
                const helper = $('<div class="aiot-helper"></div>').appendTo($('body')),
                    count = iterateDraggedItem($(event.currentTarget), element);
                ReactDOM.render(<DragHelper count={ count } />, helper.get(0));
                return helper;
            },
            start: event => {
                $('body').addClass('aiot-currently-dragging');
                
                // FIX https://bugs.jqueryui.com/ticket/4261
                $(document.activeElement).blur();
            },
            stop: () => {
                $('body').removeClass("aiot-currently-dragging");
            }
        });
    }
}