divio/django-cms

View on GitHub
cms/static/cms/js/modules/cms.pagetree.js

Summary

Maintainability
F
1 wk
Test Coverage
/*
 * Copyright https://github.com/divio/django-cms
 */

import $ from 'jquery';

import Class from 'classjs';
import { Helpers, KEYS } from './cms.base';
import PageTreeDropdowns from './cms.pagetree.dropdown';
import PageTreeStickyHeader from './cms.pagetree.stickyheader';
import { debounce, without } from 'lodash';

import 'jstree';
import '../libs/jstree/jstree.grid.min';

/**
 * The pagetree is loaded via `/admin/cms/page` and has a custom admin
 * templates stored within `templates/admin/cms/page/tree`.
 *
 * @class PageTree
 * @namespace CMS
 */
var PageTree = new Class({
    options: {
        pasteSelector: '.js-cms-tree-item-paste'
    },
    initialize: function initialize(options) {
        // options are loaded from the pagetree html node
        var opts = $('.js-cms-pagetree').data('json');

        this.options = $.extend(true, {}, this.options, opts, options);

        // states and events
        this.click = 'click.cms.pagetree';
        this.clipboard = {
            id: null,
            origin: null,
            type: ''
        };
        this.successTimer = 1000;

        // elements
        this._setupUI();
        this._events();

        Helpers.csrf(this.options.csrf);

        // cancel if pagetree is not available
        if ($.isEmptyObject(opts) || opts.empty) {
            this._getClipboard();
            // attach events to paste
            var that = this;

            this.ui.container.on(this.click, this.options.pasteSelector, function(e) {
                e.preventDefault();
                if ($(this).hasClass('cms-pagetree-dropdown-item-disabled')) {
                    return;
                }
                that._paste(e);
            });
        } else {
            this._setup();
        }
    },

    /**
     * Stores all jQuery references within `this.ui`.
     *
     * @method _setupUI
     * @private
     */
    _setupUI: function _setupUI() {
        var pagetree = $('.cms-pagetree');

        this.ui = {
            container: pagetree,
            document: $(document),
            tree: pagetree.find('.js-cms-pagetree'),
            dialog: $('.js-cms-tree-dialog'),
            siteForm: $('.js-cms-pagetree-site-form')
        };
    },

    /**
     * Setting up the jstree and the related columns.
     *
     * @method _setup
     * @private
     */
    _setup: function _setup() {
        var that = this;
        var columns = [];
        var obj = {
            language: this.options.lang.code,
            openNodes: []
        };
        var data = false;

        // setup column headings
        // eslint-disable-next-line no-shadow
        $.each(this.options.columns, function(index, obj) {
            if (obj.key === '') {
                // the first row is already populated, to avoid overwrites
                // just leave the "key" param empty
                columns.push({
                    wideValueClass: obj.wideValueClass,
                    wideValueClassPrefix: obj.wideValueClassPrefix,
                    header: obj.title,
                    width: obj.width || '1%',
                    wideCellClass: obj.cls
                });
            } else {
                columns.push({
                    wideValueClass: obj.wideValueClass,
                    wideValueClassPrefix: obj.wideValueClassPrefix,
                    header: obj.title,
                    value: function(node) {
                        // it needs to have the "colde" format and not "col-de"
                        // as jstree will convert "col-de" to "colde"
                        // also we strip dashes, in case language code contains it
                        // e.g. zh-hans, zh-cn etc
                        if (node.data) {
                            return node.data['col' + obj.key.replace('-', '')];
                        }

                        return '';
                    },
                    width: obj.width || '1%',
                    wideCellClass: obj.cls
                });
            }
        });

        // prepare data
        if (!this.options.filtered) {
            data = {
                url: this.options.urls.tree,
                cache: false,
                data: function(node) {
                    // '#' is rendered if its the root node, there we only
                    // care about `obj.openNodes`, in the following case
                    // we are requesting a specific node
                    if (node.id === '#') {
                        obj.nodeId = null;
                    } else {
                        obj.nodeId = that._storeNodeId(node.data.nodeId);
                    }

                    // we need to store the opened items inside the localstorage
                    // as we have to load the pagetree with the previous opened
                    // state
                    obj.openNodes = that._getStoredNodeIds();

                    // we need to set the site id to get the correct tree
                    obj.site = that.options.site;

                    return obj;
                }
            };
        }

        // bind options to the jstree instance
        this.ui.tree.jstree({
            core: {
                // disable open/close animations
                animation: 0,
                // core setting to allow actions
                // eslint-disable-next-line max-params
                check_callback: function(operation, node, node_parent, node_position, more) {
                    if ((operation === 'move_node' || operation === 'copy_node') && more && more.pos) {
                        if (more.pos === 'i') {
                            $('#jstree-marker').addClass('jstree-marker-child');
                        } else {
                            $('#jstree-marker').removeClass('jstree-marker-child');
                        }
                    }

                    return that._hasPermission(node_parent, 'add');
                },
                // https://www.jstree.com/api/#/?f=$.jstree.defaults.core.data
                data: data,
                // strings used within jstree that are called using `get_string`
                strings: {
                    'Loading ...': this.options.lang.loading,
                    'New node': this.options.lang.newNode,
                    nodes: this.options.lang.nodes
                },
                error: function(error) {
                    // ignore warnings about dragging parent into child
                    var errorData = JSON.parse(error.data);

                    if (error.error === 'check' && errorData && errorData.chk === 'move_node') {
                        return;
                    }
                    that.showError(error.reason);
                },
                themes: {
                    name: 'django-cms'
                },
                // disable the multi selection of nodes for now
                multiple: false
            },
            // activate drag and drop plugin
            plugins: ['dnd', 'search', 'grid'],
            // https://www.jstree.com/api/#/?f=$.jstree.defaults.dnd
            dnd: {
                inside_pos: 'last',
                // disable the multi selection of nodes for now
                drag_selection: false,
                // disable dragging if filtered
                is_draggable: function(nodes) {
                    return that._hasPermission(nodes[0], 'move') && !that.options.filtered;
                },
                large_drop_target: true,
                copy: true,
                touch: 'selected'
            },
            // https://github.com/deitch/jstree-grid
            grid: {
                // columns are provided from base.html options
                width: '100%',
                columns: columns
            }
        });
    },

    /**
     * Sets up all the event handlers, such as opening and moving.
     *
     * @method _events
     * @private
     */
    _events: function _events() {
        var that = this;

        // set events for the nodeId updates
        this.ui.tree.on('after_close.jstree', function(e, el) {
            that._removeNodeId(el.node.data.nodeId);
        });

        this.ui.tree.on('after_open.jstree', function(e, el) {
            that._storeNodeId(el.node.data.nodeId);

            // `after_open` event can be triggered when pasting
            // is in progress (meaning we are pasting into a leaf node
            // in this case we do not need to update helpers state
            if (this.clipboard && !this.clipboard.isPasting) {
                that._updatePasteHelpersState();
            }
        });

        this.ui.document.on('keydown.pagetree.alt-mode', function(e) {
            if (e.keyCode === KEYS.SHIFT) {
                that.ui.container.addClass('cms-pagetree-alt-mode');
            }
        });

        this.ui.document.on('keyup.pagetree.alt-mode', function(e) {
            if (e.keyCode === KEYS.SHIFT) {
                that.ui.container.removeClass('cms-pagetree-alt-mode');
            }
        });

        $(window)
            .on(
                'mousemove.pagetree.alt-mode',
                debounce(function(e) {
                    if (e.shiftKey) {
                        that.ui.container.addClass('cms-pagetree-alt-mode');
                    } else {
                        that.ui.container.removeClass('cms-pagetree-alt-mode');
                    }
                }, 200) // eslint-disable-line no-magic-numbers
            )
            .on('blur.cms', () => {
                that.ui.container.removeClass('cms-pagetree-alt-mode');
            });

        this.ui.document.on('dnd_start.vakata', function(e, data) {
            var element = $(data.element);
            var node = element.parent();

            that._dropdowns.closeAllDropdowns();

            node.addClass('jstree-is-dragging');
            data.data.nodes.forEach(function(nodeId) {
                var descendantIds = that._getDescendantsIds(nodeId);

                [nodeId].concat(descendantIds).forEach(function(id) {
                    $('.jsgrid_' + id + '_col').addClass('jstree-is-dragging');
                });
            });

            if (!node.hasClass('jstree-leaf')) {
                data.helper.addClass('is-stacked');
            }
        });

        var isCopyClassAdded = false;

        this.ui.document.on('dnd_move.vakata', function(e, data) {
            var isMovingCopy =
                data.data.origin &&
                (data.data.origin.settings.dnd.always_copy ||
                    (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)));

            if (isMovingCopy) {
                if (!isCopyClassAdded) {
                    $('.jstree-is-dragging').addClass('jstree-is-dragging-copy');
                    isCopyClassAdded = true;
                }
            } else if (isCopyClassAdded) {
                $('.jstree-is-dragging').removeClass('jstree-is-dragging-copy');
                isCopyClassAdded = false;
            }

            // styling the #jstree-marker dynamically on dnd_move.vakata
            // because jsTree doesn't support RTL on this specific case
            // and sets the 'left' property without checking document direction
            var ins = $.jstree.reference(data.event.target);

            // make sure we're hovering over a tree node
            if (ins) {
                var marker = $('#jstree-marker');
                var root = $('#changelist');
                var column = $(data.data.origin.element);

                var hover = ins.settings.dnd.large_drop_target ?
                                $(data.event.target)
                                    .closest('.jstree-node') :
                                $(data.event.target)
                                    .closest('.jstree-anchor').parent();

                var width = root.width() - (column.width() - hover.width());

                marker.css({
                    left: `${root.offset().left}px`,
                    width: `${width}px`
                });
            }
        });

        this.ui.document.on('dnd_stop.vakata', function(e, data) {
            var element = $(data.element);
            var node = element.parent();

            node.removeClass('jstree-is-dragging jstree-is-dragging-copy');
            data.data.nodes.forEach(function(nodeId) {
                var descendantIds = that._getDescendantsIds(nodeId);

                [nodeId].concat(descendantIds).forEach(function(id) {
                    $('.jsgrid_' + id + '_col').removeClass('jstree-is-dragging jstree-is-dragging-copy');
                });
            });
        });

        // store moved position node
        this.ui.tree.on('move_node.jstree copy_node.jstree', function(e, obj) {
            if ((!that.clipboard.type && e.type !== 'copy_node') || that.clipboard.type === 'cut') {
                that._moveNode(that._getNodePosition(obj)).done(function() {
                    var instance = that.ui.tree.jstree(true);

                    instance._hide_grid(instance.get_node(obj.parent));
                    if (obj.parent === '#' || (obj.node && obj.node.data && obj.node.data.isHome)) {
                        instance.refresh();
                    } else {
                        // have to refresh parent, because refresh only
                        // refreshes children of the node, never the node itself
                        instance.refresh_node(obj.parent);
                    }
                });
            } else {
                that._copyNode(obj);
            }
            // we need to open the parent node if we trigger an element
            // if not already opened
            that.ui.tree.jstree('open_node', obj.parent);
        });

        // set event for cut and paste
        this.ui.container.on(this.click, '.js-cms-tree-item-cut', function(e) {
            e.preventDefault();
            that._cutOrCopy({ type: 'cut', element: $(this) });
        });

        // set event for cut and paste
        this.ui.container.on(this.click, '.js-cms-tree-item-copy', function(e) {
            e.preventDefault();
            that._cutOrCopy({ type: 'copy', element: $(this) });
        });

        // attach events to paste
        this.ui.container.on(this.click, this.options.pasteSelector, function(e) {
            e.preventDefault();
            if ($(this).hasClass('cms-pagetree-dropdown-item-disabled')) {
                return;
            }
            that._paste(e);
        });

        // advanced settings link handling
        this.ui.container.on(this.click, '.js-cms-tree-advanced-settings', function(e) {
            if (e.shiftKey) {
                e.preventDefault();
                var link = $(this);

                if (link.data('url')) {
                    window.location.href = link.data('url');
                }
            }
        });

        // when adding new pages - expand nodes as well
        this.ui.container.on(this.click, '.js-cms-pagetree-add-page', e => {
            const treeId = this._getNodeId($(e.target));

            const nodeData = this.ui.tree.jstree('get_node', treeId);

            this._storeNodeId(nodeData.data.id);
        });

        // add events for error reload (messagelist)
        this.ui.document.on(this.click, '.messagelist .cms-tree-reload', function(e) {
            e.preventDefault();
            that._reloadHelper();
        });

        // propagate the sites dropdown "li > a" entries to the hidden sites form
        this.ui.container.find('.js-cms-pagetree-site-trigger').on(this.click, function(e) {
            e.preventDefault();
            var el = $(this);

            // prevent if parent is active
            if (el.parent().hasClass('active')) {
                return false;
            }
            that.ui.siteForm.find('select').val(el.data().id).end().submit();
        });

        // additional event handlers
        this._setupDropdowns();
        this._setupSearch();

        // make sure ajax post requests are working
        this._setAjaxPost('.js-cms-tree-item-menu a');
        this._setAjaxPost('.js-cms-tree-lang-trigger');
        this._setAjaxPost('.js-cms-tree-item-set-home a');

        this._setupPageView();
        this._setupStickyHeader();

        this.ui.tree.on('ready.jstree', () => this._getClipboard());
    },

    _getClipboard: function _getClipboard() {
        this.clipboard = CMS.settings.pageClipboard || this.clipboard;

        if (this.clipboard.type && this.clipboard.origin) {
            this._enablePaste();
            this._updatePasteHelpersState();
        }
    },

    /**
     * Helper to process the cut and copy events.
     *
     * @method _cutOrCopy
     * @param {Object} [obj]
     * @param {Number} [obj.type] either 'cut' or 'copy'
     * @param {Number} [obj.element] originated trigger element
     * @private
     * @returns {Boolean|void}
     */
    _cutOrCopy: function _cutOrCopy(obj) {
        // prevent actions if you try to copy a page with an apphook
        if (obj.type === 'copy' && obj.element.data().apphook) {
            this.showError(this.options.lang.apphook);
            return false;
        }

        var jsTreeId = this._getNodeId(obj.element.closest('.jstree-grid-cell'));

        // resets if we click again
        if (this.clipboard.type === obj.type && jsTreeId === this.clipboard.id) {
            this.clipboard.type = null;
            this.clipboard.id = null;
            this.clipboard.origin = null;
            this.clipboard.source_site = null;
            this._disablePaste();
        } else {
            this.clipboard.origin = obj.element.data().id; // this._getNodeId(obj.element);
            this.clipboard.type = obj.type;
            this.clipboard.id = jsTreeId;
            this.clipboard.source_site = this.options.site;
            this._updatePasteHelpersState();
        }
        if (this.clipboard.type === 'copy' || !this.clipboard.type) {
            CMS.settings.pageClipboard = this.clipboard;
            Helpers.setSettings(CMS.settings);
        }
    },

    /**
     * Helper to process the paste event.
     *
     * @method _paste
     * @param {$.Event} event click event
     * @private
     */
    _paste: function _paste(event) {
        // hide helpers after we picked one
        this._disablePaste();

        var copyFromId = this._getNodeId(
            $(`.js-cms-pagetree-options[data-id="${this.clipboard.origin}"]`).closest('.jstree-grid-cell')
        );
        var copyToId = this._getNodeId($(event.currentTarget));

        if (this.clipboard.source_site === this.options.site) {
            if (this.clipboard.type === 'cut') {
                this.ui.tree.jstree('cut', copyFromId);
            } else {
                this.ui.tree.jstree('copy', copyFromId);
            }

            this.clipboard.isPasting = true;
            this.ui.tree.jstree('paste', copyToId, 'last');
        } else {
            const dummyId = this.ui.tree.jstree('create_node', copyToId, 'Loading', 'last');

            if (this.ui.tree.length) {
                this.ui.tree.jstree('cut', dummyId);
                this.clipboard.isPasting = true;
                this.ui.tree.jstree('paste', copyToId, 'last');
            } else {
                if (this.clipboard.type === 'copy') {
                    this._copyNode();
                }
                if (this.clipboard.type === 'cut') {
                    this._moveNode();
                }
            }
        }

        this.clipboard.id = null;
        this.clipboard.type = null;
        this.clipboard.origin = null;
        this.clipboard.isPasting = false;
        CMS.settings.pageClipboard = this.clipboard;
        Helpers.setSettings(CMS.settings);
    },

    /**
     * Retreives a list of nodes from local storage.
     *
     * @method _getStoredNodeIds
     * @private
     * @returns {Array} list of ids
     */
    _getStoredNodeIds: function _getStoredNodeIds() {
        return CMS.settings.pagetree || [];
    },

    /**
     * Stores a node in local storage.
     *
     * @method _storeNodeId
     * @private
     * @param {String} id to be stored
     * @returns {String} id that has been stored
     */
    _storeNodeId: function _storeNodeId(id) {
        var number = id;
        var storage = this._getStoredNodeIds();

        // store value only if it isn't there yet
        if (storage.indexOf(number) === -1) {
            storage.push(number);
        }

        CMS.settings.pagetree = storage;
        Helpers.setSettings(CMS.settings);

        return number;
    },

    /**
     * Removes a node in local storage.
     *
     * @method _removeNodeId
     * @private
     * @param {String} id to be stored
     * @returns {String} id that has been removed
     */
    _removeNodeId: function _removeNodeId(id) {
        const instance = this.ui.tree.jstree(true);
        const childrenIds = instance.get_node({
            id: CMS.$(`[data-node-id=${id}]`).attr('id')
        }).children_d;

        const idsToRemove = [id].concat(
            childrenIds.map(childId => {
                const node = instance.get_node({ id: childId });

                if (!node || !node.data) {
                    return node;
                }

                return node.data.nodeId;
            })
        );

        const storage = without(this._getStoredNodeIds(), ...idsToRemove);

        CMS.settings.pagetree = storage;
        Helpers.setSettings(CMS.settings);

        return id;
    },

    /**
     * Moves a node after drag & drop.
     *
     * @method _moveNode
     * @param {Object} [obj]
     * @param {Number} [obj.id] current element id for url matching
     * @param {Number} [obj.target] target sibling or parent
     * @param {Number} [obj.position] either `left`, `right` or `last-child`
     * @returns {$.Deferred} ajax request object
     * @private
     */
    _moveNode: function _moveNode(obj) {
        var that = this;

        if (!obj.id && this.clipboard.type === 'cut' && this.clipboard.origin) {
            obj.id = this.clipboard.origin;
            obj.source_site = this.clipboard.source_site;
        } else {
            obj.site = that.options.site;
        }

        return $.ajax({
            method: 'post',
            url: that.options.urls.move.replace('{id}', obj.id),
            data: obj
        })
            .done(function(r) {
                if (r.status && r.status === 400) { // eslint-disable-line
                    that.showError(r.content);
                } else {
                    that._showSuccess(obj.id);
                }
            })
            .fail(function(error) {
                that.showError(error.statusText);
            });
    },

    /**
     * Copies a node into the selected node.
     *
     * @method _copyNode
     * @param {Object} obj page obj
     * @private
     */
    _copyNode: function _copyNode(obj) {
        var that = this;
        var node = { position: 0 };

        if (obj) {
            node = that._getNodePosition(obj);
        }

        var data = {
            // obj.original.data.id is for drag copy
            id: this.clipboard.origin || obj.original.data.id,
            position: node.position
        };

        if (this.clipboard.source_site) {
            data.source_site = this.clipboard.source_site;
        } else {
            data.source_site = this.options.site;
        }

        // if there is no target provided, the node lands in root
        if (node.target) {
            data.target = node.target;
        }

        if (that.options.permission) {
            // we need to load a dialog first, to check if permissions should
            // be copied or not
            $.ajax({
                method: 'post',
                url: that.options.urls.copyPermission.replace('{id}', data.id),
                data: data
                // the dialog is loaded via the ajax respons originating from
                // `templates/admin/cms/page/tree/copy_premissions.html`
            })
                .done(function(dialog) {
                    that.ui.dialog.append(dialog);
                })
                .fail(function(error) {
                    that.showError(error.statusText);
                });

            // attach events to the permission dialog
            this.ui.dialog
                .off(this.click, '.cancel')
                .on(this.click, '.cancel', function(e) {
                    e.preventDefault();
                    // remove just copied node
                    that.ui.tree.jstree('delete_node', obj.node.id);
                    $('.js-cms-dialog').remove();
                    $('.js-cms-dialog-dimmer').remove();
                })
                .off(this.click, '.submit')
                .on(this.click, '.submit', function(e) {
                    e.preventDefault();
                    var submitButton = $(this);
                    var formData = submitButton.closest('form').serialize().split('&');

                    submitButton.prop('disabled', true);

                    // loop through form data and attach to obj
                    for (var i = 0; i < formData.length; i++) {
                        data[formData[i].split('=')[0]] = formData[i].split('=')[1];
                    }

                    that._saveCopiedNode(data);
                });
        } else {
            this._saveCopiedNode(data);
        }
    },

    /**
     * Sends the request to copy a node.
     *
     * @method _saveCopiedNode
     * @private
     * @param {Object} data node position information
     * @returns {$.Deferred}
     */
    _saveCopiedNode: function _saveCopiedNode(data) {
        var that = this;

        // send the real ajax request for copying the plugin
        return $.ajax({
            method: 'post',
            url: that.options.urls.copy.replace('{id}', data.id),
            data: data
        })
            .done(function(r) {
                if (r.status && r.status === 400) { // eslint-disable-line
                    that.showError(r.content);
                } else {
                    that._reloadHelper();
                }
            })
            .fail(function(error) {
                that.showError(error.statusText);
            });
    },

    /**
     * Returns element from any sub nodes.
     *
     * @method _getElement
     * @private
     * @param {jQuery} el jQuery node form where to search
     * @returns {String} jsTree node element id
     */
    _getNodeId: function _getNodeId(el) {
        var cls = el.closest('.jstree-grid-cell').attr('class');

        // if it's not a cell, assume it's the root node
        return cls ? cls.replace(/.*jsgrid_(.+?)_col.*/, '$1') : '#';
    },

    /**
     * Gets the new node position after moving.
     *
     * @method _getNodePosition
     * @private
     * @param {Object} obj jstree move object
     * @returns {Object} evaluated object with params
     */
    _getNodePosition: function _getNodePosition(obj) {
        var data = {};
        var node = this.ui.tree.jstree('get_node', obj.node.parent);

        data.position = obj.position;

        // jstree indicates no parent with `#`, in this case we do not
        // need to set the target attribute at all
        if (obj.parent !== '#') {
            data.target = node.data.id;
        }

        // some functions like copy create a new element with a new id,
        // in this case we need to set `data.id` manually
        if (obj.node && obj.node.data) {
            data.id = obj.node.data.id;
        }

        return data;
    },

    /**
     * Sets up general tooltips that can have a list of links or content.
     *
     * @method _setupDropdowns
     * @private
     */
    _setupDropdowns: function _setupDropdowns() {
        this._dropdowns = new PageTreeDropdowns({
            container: this.ui.container
        });
    },

    /**
     * Handles page view click. Usual use case is that after you click
     * on view page in the pagetree - sideframe is no longer needed,
     * so we close it.
     *
     * @method _setupPageView
     * @private
     */
    _setupPageView: function _setupPageView() {
        var win = Helpers._getWindow();
        var parent = win.parent ? win.parent : win;

        this.ui.container.on(this.click, '.js-cms-pagetree-page-view', function() {
            // check if the CM is running inside iframe or not. For Cypress tests
            // this might not always be the case.
            if (parent.CMS && parent.CMS.API && parent.CMS.API.Helpers) {
                parent.CMS.API.Helpers.setSettings(
                    $.extend(true, {}, CMS.settings, {
                        sideframe: {
                            url: null,
                            hidden: true
                        }
                    })
                );
            }
        });
    },

    /**
     * @method _setupStickyHeader
     * @private
     */
    _setupStickyHeader: function _setupStickyHeader() {
        var that = this;

        that.ui.tree.on('ready.jstree', function() {
            that.header = new PageTreeStickyHeader({
                container: that.ui.container
            });
        });
    },

    /**
     * Triggers the links `href` as ajax post request.
     *
     * @method _setAjaxPost
     * @private
     * @param {jQuery} trigger jQuery link target
     */
    _setAjaxPost: function _setAjaxPost(trigger) {
        var that = this;

        this.ui.container.on(this.click, trigger, function(e) {
            e.preventDefault();

            var element = $(this);

            if (element.closest('.cms-pagetree-dropdown-item-disabled').length) {
                return;
            }

            try {
                window.top.CMS.API.Toolbar.showLoader();
            } catch (err) {}

            $.ajax({
                method: 'post',
                url: $(this).attr('href')
            })
                .done(function() {
                    try {
                        window.top.CMS.API.Toolbar.hideLoader();
                    } catch (err) {}

                    if (window.self === window.top) {
                        // simply reload the page
                        that._reloadHelper();
                    } else {
                        // if we're in the sideframe we have to actually
                        // check if we are publishing a page we're currently in
                        // because if the slug did change we would need to
                        // redirect to that new slug
                        // Problem here is that in case of the apphooked page
                        // the model and pk are empty and reloadBrowser doesn't really
                        // do anything - so here we specifically force the data
                        // to be the data about the page and not the model
                        var parent = window.parent ? window.parent : window;
                        var data = {
                            // this shouldn't be hardcoded, but there is no way around it
                            model: 'cms.page',
                            pk: parent.CMS.config.request.page_id
                        };

                        Helpers.reloadBrowser('REFRESH_PAGE', false, true, data);
                    }
                })
                .fail(function(error) {
                    that.showError(error.statusText);
                });
        });
    },

    /**
     * Sets events for the search on the header.
     *
     * @method _setupSearch
     * @private
     */
    _setupSearch: function _setupSearch() {
        var that = this;
        var click = this.click + '.search';

        var filterActive = false;
        var filterTrigger = this.ui.container.find('.js-cms-pagetree-header-filter-trigger');
        var filterContainer = this.ui.container.find('.js-cms-pagetree-header-filter-container');
        var filterClose = filterContainer.find('.js-cms-pagetree-header-search-close');
        var filterClass = 'cms-pagetree-header-filter-active';
        var pageTreeHeader = $('.cms-pagetree-header');

        var visibleForm = this.ui.container.find('.js-cms-pagetree-header-search');
        var hiddenForm = this.ui.container.find('.js-cms-pagetree-header-search-copy form');

        var searchContainer = this.ui.container.find('.cms-pagetree-header-filter');
        var searchField = searchContainer.find('#field-searchbar');
        var timeout = 200;

        // add active class when focusing the search field
        searchField.on('focus', function(e) {
            e.stopImmediatePropagation();
            pageTreeHeader.addClass(filterClass);
        });
        searchField.on('blur', function(e) {
            e.stopImmediatePropagation();
            // timeout is required to prevent the search field from jumping
            // between enlarging and shrinking
            setTimeout(function() {
                if (!filterActive) {
                    pageTreeHeader.removeClass(filterClass);
                }
            }, timeout);
            that.ui.document.off(click);
        });

        // shows/hides filter box
        filterTrigger.add(filterClose).on(click, function(e) {
            e.preventDefault();
            e.stopImmediatePropagation();
            if (filterActive) {
                filterContainer.hide();
                pageTreeHeader.removeClass(filterClass);
                that.ui.document.off(click);
                filterActive = false;
            } else {
                filterContainer.show();
                pageTreeHeader.addClass(filterClass);
                that.ui.document.on(click, function() {
                    filterActive = true;
                    filterTrigger.trigger(click);
                });
                filterActive = true;
            }
        });

        // prevent closing when on filter container
        filterContainer.on('click', function(e) {
            e.stopImmediatePropagation();
        });

        // add hidden fields to the form to maintain filter params
        visibleForm.append(hiddenForm.find('input[type="hidden"]'));
    },

    /**
     * Shows paste helpers.
     *
     * @method _enablePaste
     * @param {String} [selector=this.options.pasteSelector] jquery selector
     * @private
     */
    _enablePaste: function _enablePaste(selector) {
        var sel = typeof selector === 'undefined'
            ? this.options.pasteSelector
            : selector + ' ' + this.options.pasteSelector;
        var dropdownSel = '.js-cms-pagetree-actions-dropdown';

        if (typeof selector !== 'undefined') {
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
        }

        // helpers are generated on the fly, so we need to reference
        // them every single time
        $(sel).removeClass('cms-pagetree-dropdown-item-disabled');

        var data = {};

        if (this.clipboard.type === 'cut') {
            data.has_cut = true;
        } else {
            data.has_copy = true;
        }
        // not loaded actions dropdown have to be updated as well
        $(dropdownSel).data('lazyUrlData', data);
    },

    /**
     * Hides paste helpers.
     *
     * @method _disablePaste
     * @param {String} [selector=this.options.pasteSelector] jquery selector
     * @private
     */
    _disablePaste: function _disablePaste(selector) {
        var sel = typeof selector === 'undefined'
            ? this.options.pasteSelector
            : selector + ' ' + this.options.pasteSelector;
        var dropdownSel = '.js-cms-pagetree-actions-dropdown';

        if (typeof selector !== 'undefined') {
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
        }

        // helpers are generated on the fly, so we need to reference
        // them every single time
        $(sel).addClass('cms-pagetree-dropdown-item-disabled');

        // not loaded actions dropdown have to be updated as well
        $(dropdownSel).removeData('lazyUrlData');
    },

    /**
     * Updates the current state of the helpers after `after_open.jstree`
     * or `_cutOrCopy` is triggered.
     *
     * @method _updatePasteHelpersState
     * @private
     */
    _updatePasteHelpersState: function _updatePasteHelpersState() {
        var that = this;

        if (this.clipboard.type && this.clipboard.id) {
            this._enablePaste();
        }

        // hide cut element and it's descendants' paste helpers if it is visible
        if (
            this.clipboard.type === 'cut' &&
            this.clipboard.origin &&
            this.options.site === this.clipboard.source_site
        ) {
            var descendantIds = this._getDescendantsIds(this.clipboard.id);
            var nodes = [this.clipboard.id];

            if (descendantIds && descendantIds.length) {
                nodes = nodes.concat(descendantIds);
            }

            nodes.forEach(function(id) {
                that._disablePaste('.jsgrid_' + id + '_col');
            });
        }
    },

    /**
     * Shows success message on node after successful action.
     *
     * @method _showSuccess
     * @param {Number} id id of the element to add the success class
     * @private
     */
    _showSuccess: function _showSuccess(id) {
        var element = this.ui.tree.find('li[data-id="' + id + '"]');

        element.addClass('cms-tree-node-success');
        setTimeout(function() {
            element.removeClass('cms-tree-node-success');
        }, this.successTimer);
        // hide elements
        this._disablePaste();
        this.clipboard.id = null;
    },

    /**
     * Checks if we should reload the iframe or entire window. For this we
     * need to skip `CMS.API.Helpers.reloadBrowser();`.
     *
     * @method _reloadHelper
     * @private
     */
    _reloadHelper: function _reloadHelper() {
        if (window.self === window.top) {
            Helpers.reloadBrowser();
        } else {
            window.location.reload();
        }
    },

    /**
     * Displays an error within the django UI.
     *
     * @method showError
     * @param {String} message string message to display
     */
    showError: function showError(message) {
        var messages = $('.messagelist');
        var breadcrumb = $('.breadcrumbs');
        var reload = this.options.lang.reload;
        var tpl =
            '' +
            '<ul class="messagelist">' +
            '   <li class="error">' +
            '       {msg} ' +
            '       <a href="#reload" class="cms-tree-reload"> ' +
            reload +
            ' </a>' +
            '   </li>' +
            '</ul>';
        var msg = tpl.replace('{msg}', '<strong>' + this.options.lang.error + '</strong> ' + message);

        if (messages.length) {
            messages.replaceWith(msg);
        } else {
            breadcrumb.after(msg);
        }
    },

    /**
     * @method _getDescendantsIds
     * @private
     * @param {String} nodeId jstree id of the node, e.g. j1_3
     * @returns {String[]} array of ids
     */
    _getDescendantsIds: function _getDescendantsIds(nodeId) {
        return this.ui.tree.jstree(true).get_node(nodeId).children_d;
    },

    /**
     * @method _hasPermision
     * @private
     * @param {Object} node jstree node
     * @param {String} permission move / add
     * @returns {Boolean}
     */
    _hasPermission: function _hasPermision(node, permission) {
        if (node.id === '#' && permission === 'add') {
            return this.options.hasAddRootPermission;
        } else if (node.id === '#') {
            return false;
        }

        return node.li_attr['data-' + permission + '-permission'] === 'true';
    }
});

PageTree._init = function() {
    new PageTree();
};

// shorthand for jQuery(document).ready();
$(function() {
    // load cms settings beforehand
    // have to set toolbar to "expanded" by default
    // otherwise initialization will be incorrect when you
    // go first to pages and then to normal page
    window.CMS.config = {
        isPageTree: true,
        settings: {
            toolbar: 'expanded',
            version: __CMS_VERSION__
        },
        urls: {
            settings: $('.js-cms-pagetree').data('settings-url')
        }
    };
    window.CMS.settings = Helpers.getSettings();
    // autoload the pagetree
    CMS.PageTree._init();
});

export default PageTree;