NodeBB/NodeBB

View on GitHub
public/src/admin/manage/categories.js

Summary

Maintainability
A
45 mins
Test Coverage
'use strict';

define('admin/manage/categories', [
    'translator',
    'benchpress',
    'categorySelector',
    'api',
    'Sortable',
    'bootbox',
    'alerts',
], function (translator, Benchpress, categorySelector, api, Sortable, bootbox, alerts) {
    Sortable = Sortable.default;
    const Categories = {};
    let newCategoryId = -1;
    let sortables;

    Categories.init = function () {
        categorySelector.init($('[component="category-selector"]'), {
            parentCid: ajaxify.data.selectedCategory ? ajaxify.data.selectedCategory.cid : 0,
            onSelect: function (selectedCategory) {
                ajaxify.go('/admin/manage/categories' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : ''));
            },
            cacheList: false,
            localCategories: [],
            template: 'admin/partials/category/selector-dropdown-right',
        });
        Categories.render(ajaxify.data.categoriesTree);

        $('button[data-action="create"]').on('click', Categories.throwCreateModal);

        // Enable/Disable toggle events
        $('.categories').on('click', '.category-tools [data-action="toggle"]', function () {
            const $this = $(this);
            const cid = $this.attr('data-disable-cid');
            const parentEl = $this.parents('li[data-cid="' + cid + '"]');
            const disabled = parentEl.hasClass('disabled');
            const childrenEls = parentEl.find('li[data-cid]');
            const childrenCids = childrenEls.map(function () {
                return $(this).attr('data-cid');
            }).get();

            Categories.toggle([cid].concat(childrenCids), !disabled);
        });

        $('.categories').on('click', '.toggle', function () {
            const el = $(this);
            el.find('i').toggleClass('fa-chevron-down').toggleClass('fa-chevron-right');
            el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden');
            const hasMoreEl = el.closest('[data-cid]').find('> ul.has-more-categories');
            if (parseInt(hasMoreEl.attr('data-hasmore'), 10) === 1) {
                hasMoreEl.toggleClass('hidden');
            }
        });

        $('.categories').on('click', '.set-order', function () {
            const cid = $(this).attr('data-cid');
            const order = $(this).attr('data-order');
            const modal = bootbox.dialog({
                title: '[[admin/manage/categories:set-order]]',
                message: '<input type="number" min="1" class="form-control input-lg" value=' + order + ' /><p class="form-text">[[admin/manage/categories:set-order-help]]</p>',
                show: true,
                buttons: {
                    save: {
                        label: '[[modules:bootbox.confirm]]',
                        className: 'btn-primary',
                        callback: function () {
                            const val = modal.find('input').val();
                            if (val && cid) {
                                const modified = {};
                                modified[cid] = { order: Math.max(1, parseInt(val, 10)) };
                                api.put('/categories/' + cid, modified[cid]).then(function () {
                                    ajaxify.refresh();
                                }).catch(alerts.error);
                            } else {
                                return false;
                            }
                        },
                    },
                },
            });
        });

        $('#toggle-collapse-all').on('click', function () {
            const $this = $(this);
            const isCollapsed = parseInt($this.attr('data-collapsed'), 10) === 1;
            toggleAll(isCollapsed);
            $this.attr('data-collapsed', isCollapsed ? 0 : 1)
                .translateText(isCollapsed ?
                    '[[admin/manage/categories:collapse-all]]' :
                    '[[admin/manage/categories:expand-all]]');
        });

        function toggleAll(expand) {
            const el = $('.categories .toggle');
            el.find('i').toggleClass('fa-chevron-down', expand).toggleClass('fa-chevron-right', !expand);
            el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden', !expand);
        }
    };

    Categories.throwCreateModal = function () {
        Benchpress.render('admin/partials/categories/create', {}).then(function (html) {
            const modal = bootbox.dialog({
                title: '[[admin/manage/categories:alert.create]]',
                message: html,
                buttons: {
                    save: {
                        label: '[[global:create]]',
                        className: 'btn-primary',
                        callback: submit,
                    },
                },
            });
            const options = {
                localCategories: [
                    {
                        cid: 0,
                        name: '[[admin/manage/categories:parent-category-none]]',
                        icon: 'fa-none',
                    },
                ],
                template: 'admin/partials/category/selector-dropdown-left',
            };
            const parentSelector = categorySelector.init(modal.find('#parentCidGroup [component="category-selector"]'), options);
            const cloneFromSelector = categorySelector.init(modal.find('#cloneFromCidGroup [component="category-selector"]'), options);
            function submit() {
                const formData = modal.find('form').serializeObject();
                formData.description = '';
                formData.icon = 'fa-comments';
                formData.uid = app.user.uid;
                formData.parentCid = parentSelector.getSelectedCid();
                formData.cloneFromCid = cloneFromSelector.getSelectedCid();

                Categories.create(formData);
                modal.modal('hide');
                return false;
            }

            $('#cloneChildren').on('change', function () {
                const check = $(this);
                const parentSelect = modal.find('#parentCidGroup [component="category-selector"] .dropdown-toggle');

                if (check.prop('checked')) {
                    parentSelect.attr('disabled', 'disabled');
                    parentSelector.selectCategory(0);
                } else {
                    parentSelect.removeAttr('disabled');
                }
            });

            modal.find('form').on('submit', submit);
        });
    };

    Categories.create = function (payload) {
        api.post('/categories', payload, function (err, data) {
            if (err) {
                return alerts.error(err);
            }

            alerts.alert({
                alert_id: 'category_created',
                title: '[[admin/manage/categories:alert.created]]',
                message: '[[admin/manage/categories:alert.create-success]]',
                type: 'success',
                timeout: 2000,
            });

            ajaxify.go('admin/manage/categories/' + data.cid);
        });
    };

    Categories.render = function (categories) {
        const container = $('.categories');

        if (!categories || !categories.length) {
            translator.translate('[[admin/manage/categories:alert.none-active]]', function (text) {
                $('<div></div>')
                    .addClass('alert alert-info text-center')
                    .text(text)
                    .appendTo(container);
            });
        } else {
            sortables = {};
            renderList(categories, container, { cid: 0 });
        }
    };

    Categories.toggle = function (cids, disabled) {
        const listEl = document.querySelector('.categories [data-cid="0"]');
        Promise.all(cids.map(cid => api.put('/categories/' + cid, {
            disabled: disabled ? 1 : 0,
        }).then(() => {
            const categoryEl = listEl.querySelector(`li[data-cid="${cid}"]`);
            categoryEl.classList[disabled ? 'add' : 'remove']('disabled');
            $(categoryEl).find('li a[data-action="toggle"]').first().translateText(disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]');
        }).catch(alerts.error)));
    };

    function itemDidAdd(e) {
        newCategoryId = e.to.dataset.cid;
    }

    function itemDragDidEnd(e) {
        const isCategoryUpdate = parseInt(newCategoryId, 10) !== -1;

        // Update needed?
        if ((e.newIndex != null && parseInt(e.oldIndex, 10) !== parseInt(e.newIndex, 10)) || isCategoryUpdate) {
            const cid = e.item.dataset.cid;
            const modified = {};
            // on page 1 baseIndex is 0, on page n baseIndex is (n - 1) * ajaxify.data.categoriesPerPage
            // this makes sure order is correct when drag & drop is used on pages > 1
            const baseIndex = (ajaxify.data.pagination.currentPage - 1) * ajaxify.data.categoriesPerPage;
            modified[cid] = {
                order: baseIndex + e.newIndex + 1,
            };

            if (isCategoryUpdate) {
                modified[cid].parentCid = newCategoryId;

                // Show/hide expand buttons after drag completion
                const oldParentCid = parseInt(e.from.getAttribute('data-cid'), 10);
                const newParentCid = parseInt(e.to.getAttribute('data-cid'), 10);
                if (oldParentCid !== newParentCid) {
                    const toggle = document.querySelector(`.categories li[data-cid="${newParentCid}"] .toggle`);
                    if (toggle) {
                        toggle.classList.toggle('invisible', false);
                    }

                    const children = document.querySelectorAll(`.categories li[data-cid="${oldParentCid}"] ul[data-cid] li[data-cid]`);
                    if (!children.length) {
                        const toggle = document.querySelector(`.categories li[data-cid="${oldParentCid}"] .toggle`);
                        if (toggle) {
                            toggle.classList.toggle('invisible', true);
                        }
                    }

                    e.item.dataset.parentCid = newParentCid;
                }
            }

            newCategoryId = -1;
            api.put('/categories/' + cid, modified[cid]).catch(alerts.error);
        }
    }

    /**
     * Render categories - recursively
     *
     * @param categories {array} categories tree
     * @param level {number} current sub-level of rendering
     * @param container {object} parent jquery element for the list
     * @param parentId {number} parent category identifier
     */
    function renderList(categories, container, parentCategory) {
        // Translate category names if needed
        let count = 0;
        const parentId = parentCategory.cid;
        categories.forEach(function (category, idx, parent) {
            translator.translate(category.name, function (translated) {
                if (category.name !== translated) {
                    category.name = translated;
                }
                count += 1;

                if (count === parent.length) {
                    continueRender();
                }
            });
        });

        if (!categories.length) {
            continueRender();
        }

        function continueRender() {
            app.parseAndTranslate('admin/partials/categories/category-rows', {
                cid: parentCategory.cid,
                categories: categories,
                parentCategory: parentCategory,
            }, function (html) {
                if (container.find('.category-row').length) {
                    container.find('.category-row').after(html);
                } else {
                    container.append(html);
                }

                // Disable expand toggle
                if (!categories.length) {
                    const toggleEl = container.get(0).querySelector('.toggle');
                    toggleEl.classList.toggle('invisible', true);
                }

                // Handle and children categories in this level have
                for (let x = 0, numCategories = categories.length; x < numCategories; x += 1) {
                    renderList(categories[x].children, $('li[data-cid="' + categories[x].cid + '"]'), categories[x]);
                }

                // Make list sortable
                sortables[parentId] = Sortable.create($('ul[data-cid="' + parentId + '"]')[0], {
                    group: 'cross-categories',
                    animation: 150,
                    handle: '.information',
                    dataIdAttr: 'data-cid',
                    ghostClass: 'placeholder',
                    onAdd: itemDidAdd,
                    onEnd: itemDragDidEnd,
                });
            });
        }
    }

    return Categories;
});