Source: viewsList.js

"use strict";
// SPDX-License-Identifier: GPL-3.0-or-later
// myMPD (c) 2018-2024 Juergen Mang <mail@jcgames.de>
// https://github.com/jcorporation/mympd

/** @module viewsList_js */

/**
 * Initializes a list for drag and drop of list-group-items
 * @param {string} listId table id
 * @returns {void}
 */
function dragAndDropList(listId) {
    const listBody = document.querySelector('#' + listId);
    listBody.addEventListener('dragstart', function(event) {
        if (event.target.classList.contains('list-group-item')) {
            event.target.classList.add('opacity05');
            // @ts-ignore
            event.dataTransfer.setDragImage(event.target, 0, 0);
            event.dataTransfer.effectAllowed = 'move';
            dragEl = event.target;
        }
    }, false);

    listBody.addEventListener('dragenter', function(event) {
        const target = event.target.classList.contains('list-group-item')
            ? event.target
            : event.target.closest('.list-group-item');
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.add('dragover');
        }
    }, false);

    listBody.addEventListener('dragleave', function(event) {
        const target = event.target.classList.contains('list-group-item')
            ? event.target
            : event.target.closest('.list-group-item');
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.remove('dragover');
        }
    }, false);

    listBody.addEventListener('dragover', function(event) {
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
        const target = event.target.classList.contains('list-group-item')
            ? event.target
            : event.target.closest('.list-group-item');
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.add('dragover');
        }
    }, false);

    listBody.addEventListener('drop', function(event) {
        event.stopPropagation();
        event.preventDefault();
        if (dragEl === undefined ||
            dragEl.classList.contains('list-group-item') === false)
        {
            return;
        }
        const target = event.target.classList.contains('list-group-item')
            ? event.target
            : event.target.closest('.list-group-item');
        target.classList.remove('dragover');
        const newPos = getData(target, 'pos');
        const oldPos = getData(dragEl, 'pos');
        if (oldPos === newPos) {
            return;
        }
        // set dragged element uri to undefined to force table row replacement
        setData(dragEl, 'uri', undefined);
        elHide(dragEl);
        // apply new order
        setUpdateViewId(listId);
        switch(app.id) {
            case 'QueueCurrent': {
                queueMoveSong(oldPos, newPos);
                break;
            }
            case 'BrowsePlaylistDetail': {
                currentPlaylistMoveSong(oldPos, newPos);
                break;
            }
            // No Default
        }
    }, false);

    listBody.addEventListener('dragend', function() {
        dragEl.classList.remove('opacity05');
        dragEl = undefined;
    }, false);
}

/**
 * Replaces a list item and tries to keep the selection state
 * @param {boolean} mode the selection mode
 * @param {HTMLElement} item item to replace
 * @param {HTMLElement} el replacement col
 * @returns {void}
 */
function replaceListItem(mode, item, el) {
    if (getData(item, 'uri') === getData(el, 'uri')) {
        if (mode === true &&
            item.firstElementChild.lastElementChild.lastElementChild.textContent === ligatures.checked)
        {
            el.firstElementChild.lastElementChild.lastElementChild.textContent = ligatures.checked;
            el.classList.add('selected');
        }
        if (item.classList.contains('queue-playing')) {
            el.classList.add('queue-playing');
            el.style.background = item.style.background;
        }
    }
    item.replaceWith(el);
}

/**
 * Updates the list from the jsonrpc response
 * @param {object} obj jsonrpc response
 * @param {string} list list name to populate
 * @param {Function} [perCardCallback] callback per card
 * @param {Function} [createCardBodyCallback] callback to create the footer
 * @param {Function} [createCardActionsCallback] callback to create the footer
 * @returns {void}
 */
function updateList(obj, list, perCardCallback, createCardBodyCallback, createCardActionsCallback) {
    const grid = elGetById(list + 'List');
    let cols = grid.querySelectorAll('.list-group-item');
    const mode = grid.getAttribute('data-mode') === 'select'
        ? true
        : false;

    const footer = elCreateEmpty('div', {"class": ["list-actions", "col", "text-end"]});
    addActionLinks(footer);

    for (let i = 0; i < obj.result.returnedEntities; i++) {
        const card = elCreateEmpty('div', {"class": ["list-group-item", "list-group-item-action", "clickable"]});
        const row = elCreateEmpty('div', {'class': ['row', 'p-1']});
        if (perCardCallback !== undefined &&
            typeof(perCardCallback) === 'function')
        {
            perCardCallback(card, obj.result.data[i], obj.result);
        }
        setEntryData(card, obj.result.data[i]);
        if (settings['view' + app.id].fields.includes('Thumbnail') &&
            obj.result.data[i].Thumbnail !== undefined)
        {
            row.appendChild(
                elCreateEmpty('div', {"class": ["col", "list-image"]})
            );
            if (userAgentData.hasIO === true) {
                const observer = new IntersectionObserver(setListImage, {root: null, rootMargin: '0px'});
                observer.observe(card);
            }
            else {
                card.firstChild.style.backgroundImage = obj.result.data[i].Thumbnail;
            }
        }
        const body = elCreateEmpty('div', {"class": ["col", "ps-4"]});
        if (createCardBodyCallback !== undefined &&
            typeof(createCardBodyCallback) === 'function')
        {
            //custom body content
            createCardBodyCallback(body, obj.result.data[i], obj.result);
        }
        else {
            //default body content
            createListBody(body, obj.result.data[i], list);
        }
        row.appendChild(body);
        if (createCardActionsCallback !== undefined &&
            typeof(createCardActionsCallback) === 'function')
        {
            //custom footer content
            const customFooter = elCreateEmpty('div', {"class": ["list-actions", "col", "text-end"]});
            createCardActionsCallback(customFooter, obj.result.data[i], obj.result);
            row.appendChild(customFooter);
        }
        else {
            //default footer content
            row.appendChild(footer.cloneNode(true));
        }
        card.appendChild(row);
        if (i < cols.length) {
            replaceListItem(mode, cols[i], card);
        }
        else {
            grid.append(card);
        }
    }
    //remove obsolete cards
    cols = grid.querySelectorAll('.list-group-item');
    for (let i = cols.length - 1; i >= obj.result.returnedEntities; i--) {
        cols[i].remove();
    }

    unsetUpdateView(grid);
    setPagination(obj.result.totalEntities, obj.result.returnedEntities);
    setScrollViewHeight(grid);
    scrollToPosY(grid.parentNode, app.current.scrollPos);
}

/**
 * Populates the list body
 * @param {Element} body list element body to populate
 * @param {object} data data to populate
 * @param {string} list view name
 * @returns {void}
 */
function createListBody(body, data, list) {
    let i = 0;
    for (const tag of settings['view' + list].fields) {
        if (tag === 'Thumbnail') {
            i++;
            continue;
        }
        if (i === 0) {
            body.appendChild(
                elCreateNode('h5', {"class": ["d-block"], "data-col": tag},
                    printValue(tag, data[tag], data)
                )
            );
        }
        else if (isEmptyTag(data[tag]) === false) {
            body.appendChild(
                elCreateNodes('div', {"class": ["row"]}, [
                    elCreateTextTn('small', {"class": ["col-3"]}, tag),
                    elCreateNode('span', {"data-col": tag, "class": ["col-9"]},
                        printValue(tag, data[tag], data)
                    )
                ])
            );
        }
        i++;
    }
}

/**
 * Callback function for intersection observer to lazy load cover images
 * @param {object} changes IntersectionObserverEntry objects
 * @param {object} observer IntersectionObserver
 * @returns {void}
 */
function setListImage(changes, observer) {
    changes.forEach(change => {
        if (change.intersectionRatio > 0) {
            observer.unobserve(change.target);
            const body = change.target.querySelector('.list-image');
            if (body) {
                body.style.backgroundImage = getData(change.target, 'cssImageUrl');
            }
        }
    });
}