• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divio / django-cms / #29886

25 Aug 2025 09:29AM UTC coverage: 75.073% (-0.1%) from 75.176%
#29886

push

travis-ci

web-flow
Merge 665db9320 into dc20af8ef

1081 of 1628 branches covered (66.4%)

1 of 1 new or added line in 1 file covered. (100.0%)

127 existing lines in 1 file now uncovered.

2566 of 3418 relevant lines covered (75.07%)

26.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

35.73
/cms/static/cms/js/modules/cms.pagetree.js
1
/*
2
 * Copyright https://github.com/divio/django-cms
3
 */
4

5
import $ from 'jquery';
6
import URL from 'urijs';
7

8
import Class from 'classjs';
9
import { Helpers, KEYS } from './cms.base';
10
import PageTreeDropdowns from './cms.pagetree.dropdown';
11
import PageTreeStickyHeader from './cms.pagetree.stickyheader';
12
import { debounce, without } from 'lodash';
13

14
import 'jstree';
15
import '../libs/jstree/jstree.grid.min';
16

17
/**
18
 * The pagetree is loaded via `/admin/cms/page` and has a custom admin
19
 * templates stored within `templates/admin/cms/page/tree`.
20
 *
21
 * @class PageTree
22
 * @namespace CMS
23
 */
24
var PageTree = new Class({
1✔
25
    options: {
26
        pasteSelector: '.js-cms-tree-item-paste'
27
    },
28
    initialize: function initialize(options) {
29
        // options are loaded from the pagetree html node
30
        var opts = $('.js-cms-pagetree').data('json');
21✔
31

32
        this.options = $.extend(true, {}, this.options, opts, options);
21✔
33

34
        // states and events
35
        this.click = 'click.cms.pagetree';
21✔
36
        this.clipboard = {
21✔
37
            id: null,
38
            origin: null,
39
            type: ''
40
        };
41
        this.successTimer = 1000;
21✔
42

43
        // elements
44
        this._setupUI();
21✔
45
        this._events();
21✔
46

47
        Helpers.csrf(this.options.csrf);
21✔
48

49
        this._setupLanguages();
21✔
50

51
        // cancel if pagetree is not available
52
        if ($.isEmptyObject(opts) || opts.empty) {
21✔
53
            this._getClipboard();
1✔
54
            // attach events to paste
55
            var that = this;
1✔
56

57
            this.ui.container.on(this.click, this.options.pasteSelector, function(e) {
1✔
58
                e.preventDefault();
×
59
                if ($(this).hasClass('cms-pagetree-dropdown-item-disabled')) {
×
60
                    return;
×
61
                }
62
                that._paste(e);
×
63
            });
64
        } else {
65
            this._setup();
20✔
66
        }
67
    },
68

69
    /**
70
     * Stores all jQuery references within `this.ui`.
71
     *
72
     * @method _setupUI
73
     * @private
74
     */
75
    _setupUI: function _setupUI() {
76
        var pagetree = $('.cms-pagetree');
21✔
77

78
        this.ui = {
21✔
79
            container: pagetree,
80
            document: $(document),
81
            tree: pagetree.find('.js-cms-pagetree'),
82
            dialog: $('.js-cms-tree-dialog'),
83
            siteForm: $('.js-cms-pagetree-site-form'),
84
            languagesSelect: $('.js-cms-pagetree-languages')
85
        };
86
    },
87

88
    _setupLanguages: function _setupLanguages() {
89
        this.ui.languagesSelect.on('change', () => {
21✔
90
            const newLanguage = this.ui.languagesSelect.val();
×
91

92
            const url = new URL(window.location.href).removeSearch('language')
×
93
                .addSearch('language', newLanguage).toString();
94

95
            window.location.href = url;
×
96
        });
97
    },
98

99
    /**
100
     * Setting up the jstree and the related columns.
101
     *
102
     * @method _setup
103
     * @private
104
     */
105
    _setup: function _setup() {
106
        var that = this;
20✔
107
        var columns = [];
20✔
108
        var obj = {
20✔
109
            language: this.options.lang.code,
110
            openNodes: []
111
        };
112
        var data = false;
20✔
113

114
        // setup column headings
115
        // eslint-disable-next-line no-shadow
116
        $.each(this.options.columns, function(index, obj) {
20✔
117
            if (obj.key === '') {
200✔
118
                // the first row is already populated, to avoid overwrites
119
                // just leave the "key" param empty
120
                columns.push({
20✔
121
                    wideValueClass: obj.wideValueClass,
122
                    wideValueClassPrefix: obj.wideValueClassPrefix,
123
                    header: obj.title,
124
                    width: obj.width || '1%',
20!
125
                    wideCellClass: obj.cls
126
                });
127
            } else {
128
                columns.push({
180✔
129
                    wideValueClass: obj.wideValueClass,
130
                    wideValueClassPrefix: obj.wideValueClassPrefix,
131
                    header: obj.title,
132
                    value: function(node) {
133
                        // it needs to have the "colde" format and not "col-de"
134
                        // as jstree will convert "col-de" to "colde"
135
                        // also we strip dashes, in case language code contains it
136
                        // e.g. zh-hans, zh-cn etc
137
                        if (node.data) {
×
138
                            return node.data['col' + obj.key.replace('-', '')];
×
139
                        }
140

141
                        return '';
×
142
                    },
143
                    width: obj.width || '1%',
360✔
144
                    wideCellClass: obj.cls
145
                });
146
            }
147
        });
148

149
        // prepare data
150
        if (!this.options.filtered) {
20!
151
            data = {
20✔
152
                url: this.options.urls.tree,
153
                cache: false,
154
                data: function(node) {
155
                    // '#' is rendered if its the root node, there we only
156
                    // care about `obj.openNodes`, in the following case
157
                    // we are requesting a specific node
158
                    if (node.id === '#') {
20!
159
                        obj.nodeId = null;
20✔
160
                    } else {
161
                        obj.nodeId = that._storeNodeId(node.data.nodeId);
×
162
                    }
163

164
                    // we need to store the opened items inside the localstorage
165
                    // as we have to load the pagetree with the previous opened
166
                    // state
167
                    obj.openNodes = that._getStoredNodeIds();
20✔
168

169
                    // we need to set the site id to get the correct tree
170
                    obj.site = that.options.site;
20✔
171

172
                    return obj;
20✔
173
                }
174
            };
175
        }
176

177
        // bind options to the jstree instance
178
        this.ui.tree.jstree({
20✔
179
            core: {
180
                // disable open/close animations
181
                animation: 0,
182
                // core setting to allow actions
183
                // eslint-disable-next-line max-params
184
                check_callback: function(operation, node, node_parent, node_position, more) {
185
                    if ((operation === 'move_node' || operation === 'copy_node') && more && more.pos) {
×
186
                        if (more.pos === 'i') {
×
187
                            $('#jstree-marker').addClass('jstree-marker-child');
×
188
                        } else {
189
                            $('#jstree-marker').removeClass('jstree-marker-child');
×
190
                        }
191
                    }
192

193
                    return that._hasPermission(node_parent, 'add');
×
194
                },
195
                // https://www.jstree.com/api/#/?f=$.jstree.defaults.core.data
196
                data: data,
197
                // strings used within jstree that are called using `get_string`
198
                strings: {
199
                    'Loading ...': this.options.lang.loading,
200
                    'New node': this.options.lang.newNode,
201
                    nodes: this.options.lang.nodes
202
                },
203
                error: function(error) {
204
                    // ignore warnings about dragging parent into child
205
                    var errorData = JSON.parse(error.data);
×
206

207
                    if (error.error === 'check' && errorData && errorData.chk === 'move_node') {
×
208
                        return;
×
209
                    }
210
                    that.showError(error.reason);
×
211
                },
212
                themes: {
213
                    name: 'django-cms'
214
                },
215
                // disable the multi selection of nodes for now
216
                multiple: false
217
            },
218
            // activate drag and drop plugin
219
            plugins: ['dnd', 'search', 'grid'],
220
            // https://www.jstree.com/api/#/?f=$.jstree.defaults.dnd
221
            dnd: {
222
                inside_pos: 'last',
223
                // disable the multi selection of nodes for now
224
                drag_selection: false,
225
                // disable dragging if filtered
226
                is_draggable: function(nodes) {
227
                    return that._hasPermission(nodes[0], 'move') && !that.options.filtered;
×
228
                },
229
                large_drop_target: true,
230
                copy: true,
231
                touch: 'selected'
232
            },
233
            // https://github.com/deitch/jstree-grid
234
            grid: {
235
                // columns are provided from base.html options
236
                width: '100%',
237
                columns: columns
238
            }
239
        });
240
    },
241

242
    /**
243
     * Sets up all the event handlers, such as opening and moving.
244
     *
245
     * @method _events
246
     * @private
247
     */
248
    _events: function _events() {
249
        var that = this;
21✔
250

251
        // set events for the nodeId updates
252
        this.ui.tree.on('after_close.jstree', function(e, el) {
21✔
253
            that._removeNodeId(el.node.data.nodeId);
×
254
        });
255

256
        this.ui.tree.on('after_open.jstree', function(e, el) {
21✔
257
            that._storeNodeId(el.node.data.nodeId);
×
258

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

267
        this.ui.document.on('keydown.pagetree.alt-mode', function(e) {
21✔
268
            if (e.keyCode === KEYS.SHIFT) {
466!
269
                that.ui.container.addClass('cms-pagetree-alt-mode');
×
270
            }
271
        });
272

273
        this.ui.document.on('keyup.pagetree.alt-mode', function(e) {
21✔
274
            if (e.keyCode === KEYS.SHIFT) {
463!
275
                that.ui.container.removeClass('cms-pagetree-alt-mode');
×
276
            }
277
        });
278

279
        $(window)
21✔
280
            .on(
281
                'mousemove.pagetree.alt-mode',
282
                debounce(function(e) {
283
                    if (e.shiftKey) {
1!
284
                        that.ui.container.addClass('cms-pagetree-alt-mode');
×
285
                    } else {
286
                        that.ui.container.removeClass('cms-pagetree-alt-mode');
1✔
287
                    }
288
                }, 200) // eslint-disable-line no-magic-numbers
289
            )
290
            .on('blur.cms', () => {
291
                that.ui.container.removeClass('cms-pagetree-alt-mode');
21✔
292
            });
293

294
        this.ui.document.on('dnd_start.vakata', function(e, data) {
21✔
295
            var element = $(data.element);
×
296
            var node = element.parent();
×
297

298
            that._dropdowns.closeAllDropdowns();
×
299

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

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

309
            if (!node.hasClass('jstree-leaf')) {
×
310
                data.helper.addClass('is-stacked');
×
311
            }
312
        });
313

314
        var isCopyClassAdded = false;
21✔
315

316
        this.ui.document.on('dnd_move.vakata', function(e, data) {
21✔
317
            var isMovingCopy =
318
                data.data.origin &&
×
319
                (data.data.origin.settings.dnd.always_copy ||
320
                    (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)));
321

322
            if (isMovingCopy) {
×
323
                if (!isCopyClassAdded) {
×
324
                    $('.jstree-is-dragging').addClass('jstree-is-dragging-copy');
×
325
                    isCopyClassAdded = true;
×
326
                }
327
            } else if (isCopyClassAdded) {
×
328
                $('.jstree-is-dragging').removeClass('jstree-is-dragging-copy');
×
329
                isCopyClassAdded = false;
×
330
            }
331

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

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

343
                var hover = ins.settings.dnd.large_drop_target ?
×
344
                                $(data.event.target)
345
                                    .closest('.jstree-node') :
346
                                $(data.event.target)
347
                                    .closest('.jstree-anchor').parent();
348

349
                var width = root.width() - (column.width() - hover.width());
×
350

351
                marker.css({
×
352
                    left: `${root.offset().left}px`,
353
                    width: `${width}px`
354
                });
355
            }
356
        });
357

358
        this.ui.document.on('dnd_stop.vakata', function(e, data) {
21✔
359
            var element = $(data.element);
×
360
            var node = element.parent();
×
361

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

366
                [nodeId].concat(descendantIds).forEach(function(id) {
×
367
                    $('.jsgrid_' + id + '_col').removeClass('jstree-is-dragging jstree-is-dragging-copy');
×
368
                });
369
            });
370
        });
371

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

378
                    instance._hide_grid(instance.get_node(obj.parent));
×
379
                    if (obj.parent === '#' || (obj.node && obj.node.data && obj.node.data.isHome)) {
×
380
                        instance.refresh();
×
381
                    } else {
382
                        // have to refresh parent, because refresh only
383
                        // refreshes children of the node, never the node itself
384
                        instance.refresh_node(obj.parent);
×
385
                    }
386
                });
387
            } else {
388
                that._copyNode(obj);
×
389
            }
390
            // we need to open the parent node if we trigger an element
391
            // if not already opened
392
            that.ui.tree.jstree('open_node', obj.parent);
×
393
        });
394

395
        // set event for cut and paste
396
        this.ui.container.on(this.click, '.js-cms-tree-item-cut', function(e) {
21✔
397
            e.preventDefault();
×
398
            that._cutOrCopy({ type: 'cut', element: $(this) });
×
399
        });
400

401
        // set event for cut and paste
402
        this.ui.container.on(this.click, '.js-cms-tree-item-copy', function(e) {
21✔
403
            e.preventDefault();
×
404
            that._cutOrCopy({ type: 'copy', element: $(this) });
×
405
        });
406

407
        // attach events to paste
408
        this.ui.container.on(this.click, this.options.pasteSelector, function(e) {
21✔
409
            e.preventDefault();
×
410
            if ($(this).hasClass('cms-pagetree-dropdown-item-disabled')) {
×
411
                return;
×
412
            }
413
            that._paste(e);
×
414
        });
415

416
        // advanced settings link handling
417
        this.ui.container.on(this.click, '.js-cms-tree-advanced-settings', function(e) {
21✔
418
            if (e.shiftKey) {
×
419
                e.preventDefault();
×
420
                var link = $(this);
×
421

422
                if (link.data('url')) {
×
423
                    window.location.href = link.data('url');
×
424
                }
425
            }
426
        });
427

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

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

434
            this._storeNodeId(nodeData.data.id);
×
435
        });
436

437
        // add events for error reload (messagelist)
438
        this.ui.document.on(this.click, '.messagelist .cms-tree-reload', function(e) {
21✔
439
            e.preventDefault();
×
440
            that._reloadHelper();
×
441
        });
442

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

448
            // prevent if parent is active
UNCOV
449
            if (el.parent().hasClass('active')) {
×
UNCOV
450
                return false;
×
451
            }
UNCOV
452
            that.ui.siteForm.find('select').val(el.data().id).end().submit();
×
453
        });
454

455
        // additional event handlers
456
        this._setupDropdowns();
21✔
457
        this._setupSearch();
21✔
458

459
        // make sure ajax post requests are working
460
        this._setAjaxPost('.js-cms-tree-item-menu a');
21✔
461
        this._setAjaxPost('.js-cms-tree-lang-trigger');
21✔
462
        this._setAjaxPost('.js-cms-tree-item-set-home a');
21✔
463

464
        this._setupPageView();
21✔
465
        this._setupStickyHeader();
21✔
466

467
        this.ui.tree.on('ready.jstree', () => this._getClipboard());
21✔
468
    },
469

470
    _getClipboard: function _getClipboard() {
471
        this.clipboard = CMS.settings.pageClipboard || this.clipboard;
1✔
472

473
        if (this.clipboard.type && this.clipboard.origin) {
1!
UNCOV
474
            this._enablePaste();
×
UNCOV
475
            this._updatePasteHelpersState();
×
476
        }
477
    },
478

479
    /**
480
     * Helper to process the cut and copy events.
481
     *
482
     * @method _cutOrCopy
483
     * @param {Object} [obj]
484
     * @param {Number} [obj.type] either 'cut' or 'copy'
485
     * @param {Number} [obj.element] originated trigger element
486
     * @private
487
     * @returns {Boolean|void}
488
     */
489
    _cutOrCopy: function _cutOrCopy(obj) {
490
        // prevent actions if you try to copy a page with an apphook
491
        if (obj.type === 'copy' && obj.element.data().apphook) {
×
492
            this.showError(this.options.lang.apphook);
×
493
            return false;
×
494
        }
495

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

498
        // resets if we click again
499
        if (this.clipboard.type === obj.type && jsTreeId === this.clipboard.id) {
×
UNCOV
500
            this.clipboard.type = null;
×
501
            this.clipboard.id = null;
×
502
            this.clipboard.origin = null;
×
503
            this.clipboard.source_site = null;
×
UNCOV
504
            this._disablePaste();
×
505
        } else {
UNCOV
506
            this.clipboard.origin = obj.element.data().id; // this._getNodeId(obj.element);
×
UNCOV
507
            this.clipboard.type = obj.type;
×
UNCOV
508
            this.clipboard.id = jsTreeId;
×
UNCOV
509
            this.clipboard.source_site = this.options.site;
×
UNCOV
510
            this._updatePasteHelpersState();
×
511
        }
UNCOV
512
        if (this.clipboard.type === 'copy' || !this.clipboard.type) {
×
UNCOV
513
            CMS.settings.pageClipboard = this.clipboard;
×
UNCOV
514
            Helpers.setSettings(CMS.settings);
×
515
        }
516
    },
517

518
    /**
519
     * Helper to process the paste event.
520
     *
521
     * @method _paste
522
     * @param {$.Event} event click event
523
     * @private
524
     */
525
    _paste: function _paste(event) {
526
        // hide helpers after we picked one
527
        this._disablePaste();
5✔
528

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

534
        if (this.clipboard.source_site === this.options.site) {
5!
UNCOV
535
            if (this.clipboard.type === 'cut') {
×
UNCOV
536
                this.ui.tree.jstree('cut', copyFromId);
×
537
            } else {
UNCOV
538
                this.ui.tree.jstree('copy', copyFromId);
×
539
            }
540

541
            this.clipboard.isPasting = true;
×
UNCOV
542
            this.ui.tree.jstree('paste', copyToId, 'last');
×
543
        } else {
544
            const dummyId = this.ui.tree.jstree('create_node', copyToId, 'Loading', 'last');
5✔
545

546
            if (this.ui.tree.length) {
5!
547
                this.ui.tree.jstree('cut', dummyId);
5✔
548
                this.clipboard.isPasting = true;
5✔
549
                this.ui.tree.jstree('paste', copyToId, 'last');
5✔
550
            } else {
UNCOV
551
                if (this.clipboard.type === 'copy') {
×
UNCOV
552
                    this._copyNode();
×
553
                }
UNCOV
554
                if (this.clipboard.type === 'cut') {
×
UNCOV
555
                    this._moveNode();
×
556
                }
557
            }
558
        }
559

560
        this.clipboard.id = null;
5✔
561
        this.clipboard.type = null;
5✔
562
        this.clipboard.origin = null;
5✔
563
        this.clipboard.isPasting = false;
5✔
564
        CMS.settings.pageClipboard = this.clipboard;
5✔
565
        Helpers.setSettings(CMS.settings);
5✔
566
    },
567

568
    /**
569
     * Retreives a list of nodes from local storage.
570
     *
571
     * @method _getStoredNodeIds
572
     * @private
573
     * @returns {Array} list of ids
574
     */
575
    _getStoredNodeIds: function _getStoredNodeIds() {
576
        return CMS.settings.pagetree || [];
20✔
577
    },
578

579
    /**
580
     * Stores a node in local storage.
581
     *
582
     * @method _storeNodeId
583
     * @private
584
     * @param {String} id to be stored
585
     * @returns {String} id that has been stored
586
     */
587
    _storeNodeId: function _storeNodeId(id) {
588
        var number = id;
×
UNCOV
589
        var storage = this._getStoredNodeIds();
×
590

591
        // store value only if it isn't there yet
UNCOV
592
        if (storage.indexOf(number) === -1) {
×
UNCOV
593
            storage.push(number);
×
594
        }
595

UNCOV
596
        CMS.settings.pagetree = storage;
×
UNCOV
597
        Helpers.setSettings(CMS.settings);
×
598

UNCOV
599
        return number;
×
600
    },
601

602
    /**
603
     * Removes a node in local storage.
604
     *
605
     * @method _removeNodeId
606
     * @private
607
     * @param {String} id to be stored
608
     * @returns {String} id that has been removed
609
     */
610
    _removeNodeId: function _removeNodeId(id) {
UNCOV
611
        const instance = this.ui.tree.jstree(true);
×
UNCOV
612
        const childrenIds = instance.get_node({
×
613
            id: CMS.$(`[data-node-id=${id}]`).attr('id')
614
        }).children_d;
615

UNCOV
616
        const idsToRemove = [id].concat(
×
617
            childrenIds.map(childId => {
UNCOV
618
                const node = instance.get_node({ id: childId });
×
619

620
                if (!node || !node.data) {
×
UNCOV
621
                    return node;
×
622
                }
623

UNCOV
624
                return node.data.nodeId;
×
625
            })
626
        );
627

UNCOV
628
        const storage = without(this._getStoredNodeIds(), ...idsToRemove);
×
629

UNCOV
630
        CMS.settings.pagetree = storage;
×
UNCOV
631
        Helpers.setSettings(CMS.settings);
×
632

UNCOV
633
        return id;
×
634
    },
635

636
    /**
637
     * Moves a node after drag & drop.
638
     *
639
     * @method _moveNode
640
     * @param {Object} [obj]
641
     * @param {Number} [obj.id] current element id for url matching
642
     * @param {Number} [obj.target] target sibling or parent
643
     * @param {Number} [obj.position] either `left`, `right` or `last-child`
644
     * @returns {$.Deferred} ajax request object
645
     * @private
646
     */
647
    _moveNode: function _moveNode(obj) {
UNCOV
648
        var that = this;
×
649

UNCOV
650
        if (!obj.id && this.clipboard.type === 'cut' && this.clipboard.origin) {
×
UNCOV
651
            obj.id = this.clipboard.origin;
×
652
            obj.source_site = this.clipboard.source_site;
×
653
        } else {
UNCOV
654
            obj.site = that.options.site;
×
655
        }
656

UNCOV
657
        return $.ajax({
×
658
            method: 'post',
659
            url: that.options.urls.move.replace('{id}', obj.id),
660
            data: obj
661
        })
662
            .done(function(r) {
UNCOV
663
                if (r.status && r.status === 400) { // eslint-disable-line
×
UNCOV
664
                    that.showError(r.content);
×
665
                } else {
UNCOV
666
                    that._showSuccess(obj.id);
×
667
                }
668
            })
669
            .fail(function(error) {
UNCOV
670
                that.showError(error.statusText);
×
671
            });
672
    },
673

674
    /**
675
     * Copies a node into the selected node.
676
     *
677
     * @method _copyNode
678
     * @param {Object} obj page obj
679
     * @private
680
     */
681
    _copyNode: function _copyNode(obj) {
UNCOV
682
        var that = this;
×
UNCOV
683
        var node = { position: 0 };
×
684

685
        if (obj) {
×
UNCOV
686
            node = that._getNodePosition(obj);
×
687
        }
688

UNCOV
689
        var data = {
×
690
            // obj.original.data.id is for drag copy
691
            id: this.clipboard.origin || obj.original.data.id,
×
692
            position: node.position
693
        };
694

695
        if (this.clipboard.source_site) {
×
UNCOV
696
            data.source_site = this.clipboard.source_site;
×
697
        } else {
698
            data.source_site = this.options.site;
×
699
        }
700

701
        // if there is no target provided, the node lands in root
UNCOV
702
        if (node.target) {
×
UNCOV
703
            data.target = node.target;
×
704
        }
705

706
        if (that.options.permission) {
×
707
            // we need to load a dialog first, to check if permissions should
708
            // be copied or not
709
            $.ajax({
×
710
                method: 'post',
711
                url: that.options.urls.copyPermission.replace('{id}', data.id),
712
                data: data
713
                // the dialog is loaded via the ajax respons originating from
714
                // `templates/admin/cms/page/tree/copy_premissions.html`
715
            })
716
                .done(function(dialog) {
UNCOV
717
                    that.ui.dialog.append(dialog);
×
718
                })
719
                .fail(function(error) {
720
                    that.showError(error.statusText);
×
721
                });
722

723
            // attach events to the permission dialog
724
            this.ui.dialog
×
725
                .off(this.click, '.cancel')
726
                .on(this.click, '.cancel', function(e) {
UNCOV
727
                    e.preventDefault();
×
728
                    // remove just copied node
UNCOV
729
                    that.ui.tree.jstree('delete_node', obj.node.id);
×
UNCOV
730
                    $('.js-cms-dialog').remove();
×
731
                    $('.js-cms-dialog-dimmer').remove();
×
732
                })
733
                .off(this.click, '.submit')
734
                .on(this.click, '.submit', function(e) {
735
                    e.preventDefault();
×
UNCOV
736
                    var submitButton = $(this);
×
UNCOV
737
                    var formData = submitButton.closest('form').serialize().split('&');
×
738

UNCOV
739
                    submitButton.prop('disabled', true);
×
740

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

UNCOV
746
                    that._saveCopiedNode(data);
×
747
                });
748
        } else {
UNCOV
749
            this._saveCopiedNode(data);
×
750
        }
751
    },
752

753
    /**
754
     * Sends the request to copy a node.
755
     *
756
     * @method _saveCopiedNode
757
     * @private
758
     * @param {Object} data node position information
759
     * @returns {$.Deferred}
760
     */
761
    _saveCopiedNode: function _saveCopiedNode(data) {
UNCOV
762
        var that = this;
×
763

764
        // send the real ajax request for copying the plugin
UNCOV
765
        return $.ajax({
×
766
            method: 'post',
767
            url: that.options.urls.copy.replace('{id}', data.id),
768
            data: data
769
        })
770
            .done(function(r) {
UNCOV
771
                if (r.status && r.status === 400) { // eslint-disable-line
×
UNCOV
772
                    that.showError(r.content);
×
773
                } else {
UNCOV
774
                    that._reloadHelper();
×
775
                }
776
            })
777
            .fail(function(error) {
UNCOV
778
                that.showError(error.statusText);
×
779
            });
780
    },
781

782
    /**
783
     * Returns element from any sub nodes.
784
     *
785
     * @method _getElement
786
     * @private
787
     * @param {jQuery} el jQuery node form where to search
788
     * @returns {String} jsTree node element id
789
     */
790
    _getNodeId: function _getNodeId(el) {
791
        var cls = el.closest('.jstree-grid-cell').attr('class');
2✔
792

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

797
    /**
798
     * Gets the new node position after moving.
799
     *
800
     * @method _getNodePosition
801
     * @private
802
     * @param {Object} obj jstree move object
803
     * @returns {Object} evaluated object with params
804
     */
805
    _getNodePosition: function _getNodePosition(obj) {
UNCOV
806
        var data = {};
×
UNCOV
807
        var node = this.ui.tree.jstree('get_node', obj.node.parent);
×
808

809
        data.position = obj.position;
×
810

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

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

UNCOV
823
        return data;
×
824
    },
825

826
    /**
827
     * Sets up general tooltips that can have a list of links or content.
828
     *
829
     * @method _setupDropdowns
830
     * @private
831
     */
832
    _setupDropdowns: function _setupDropdowns() {
833
        this._dropdowns = new PageTreeDropdowns({
21✔
834
            container: this.ui.container
835
        });
836
    },
837

838
    /**
839
     * Handles page view click. Usual use case is that after you click
840
     * on view page in the pagetree - sideframe is no longer needed,
841
     * so we close it.
842
     *
843
     * @method _setupPageView
844
     * @private
845
     */
846
    _setupPageView: function _setupPageView() {
847
        var win = Helpers._getWindow();
25✔
848
        var parent = win.parent ? win.parent : win;
25✔
849

850
        this.ui.container.on(this.click, '.js-cms-pagetree-page-view', function() {
25✔
851
            parent.CMS.API.Helpers.setSettings(
3✔
852
                $.extend(true, {}, CMS.settings, {
853
                    sideframe: {
854
                        url: null,
855
                        hidden: true
856
                    }
857
                })
858
            );
859
        });
860
    },
861

862
    /**
863
     * @method _setupStickyHeader
864
     * @private
865
     */
866
    _setupStickyHeader: function _setupStickyHeader() {
867
        var that = this;
21✔
868

869
        that.ui.tree.on('ready.jstree', function() {
21✔
UNCOV
870
            that.header = new PageTreeStickyHeader({
×
871
                container: that.ui.container
872
            });
873
        });
874
    },
875

876
    /**
877
     * Triggers the links `href` as ajax post request.
878
     *
879
     * @method _setAjaxPost
880
     * @private
881
     * @param {jQuery} trigger jQuery link target
882
     */
883
    _setAjaxPost: function _setAjaxPost(trigger) {
884
        var that = this;
63✔
885

886
        this.ui.container.on(this.click, trigger, function(e) {
63✔
887
            e.preventDefault();
×
888

UNCOV
889
            var element = $(this);
×
890

891
            if (element.closest('.cms-pagetree-dropdown-item-disabled').length) {
×
UNCOV
892
                return;
×
893
            }
894
            if (element.attr('target') === '_top') {
×
895
                // Post to target="_top" requires to create a form and submit it
UNCOV
896
                var parent = window;
×
897

898
                if (window.parent) {
×
UNCOV
899
                    parent = window.parent;
×
900
                }
901
                let formToken = document.querySelector('form input[name="csrfmiddlewaretoken"]');
×
UNCOV
902
                let csrfToken = '<input type="hidden" name="csrfmiddlewaretoken" value="' +
×
903
                    ((formToken ? formToken.value : formToken) || window.CMS.config.csrf) + '">';
×
904

UNCOV
905
                $('<form method="post" action="' + element.attr('href') + '">' +
×
906
                    csrfToken + '</form>')
907
                    .appendTo($(parent.document.body))
908
                    .submit();
909
                return;
×
910
            }
UNCOV
911
            try {
×
UNCOV
912
                window.top.CMS.API.Toolbar.showLoader();
×
913
            } catch (err) {}
914

915
            $.ajax({
×
916
                method: 'post',
917
                url: $(this).attr('href')
918
            })
919
                .done(function() {
UNCOV
920
                    try {
×
921
                        window.top.CMS.API.Toolbar.hideLoader();
×
922
                    } catch (err) {}
923

924
                    if (window.self === window.top) {
×
925
                        // simply reload the page
UNCOV
926
                        that._reloadHelper();
×
927
                    } else {
UNCOV
928
                        Helpers.reloadBrowser('REFRESH_PAGE');
×
929
                    }
930
                })
931
                .fail(function(error) {
UNCOV
932
                    try {
×
UNCOV
933
                        window.top.CMS.API.Toolbar.hideLoader();
×
934
                    } catch (err) {}
UNCOV
935
                    that.showError(error.responseText ? error.responseText : error.statusText);
×
936
                });
937
        });
938
    },
939

940
    /**
941
     * Sets events for the search on the header.
942
     *
943
     * @method _setupSearch
944
     * @private
945
     */
946
    _setupSearch: function _setupSearch() {
947
        var that = this;
21✔
948
        var click = this.click + '.search';
21✔
949

950
        var filterActive = false;
21✔
951
        var filterTrigger = this.ui.container.find('.js-cms-pagetree-header-filter-trigger');
21✔
952
        var filterContainer = this.ui.container.find('.js-cms-pagetree-header-filter-container');
21✔
953
        var filterClose = filterContainer.find('.js-cms-pagetree-header-search-close');
21✔
954
        var filterClass = 'cms-pagetree-header-filter-active';
21✔
955
        var pageTreeHeader = $('.cms-pagetree-header');
21✔
956

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

960
        var searchContainer = this.ui.container.find('.cms-pagetree-header-filter');
21✔
961
        var searchField = searchContainer.find('#field-searchbar');
21✔
962
        var timeout = 200;
21✔
963

964
        // add active class when focusing the search field
965
        searchField.on('focus', function(e) {
21✔
UNCOV
966
            e.stopImmediatePropagation();
×
967
            pageTreeHeader.addClass(filterClass);
×
968
        });
969
        searchField.on('blur', function(e) {
21✔
UNCOV
970
            e.stopImmediatePropagation();
×
971
            // timeout is required to prevent the search field from jumping
972
            // between enlarging and shrinking
973
            setTimeout(function() {
×
974
                if (!filterActive) {
×
975
                    pageTreeHeader.removeClass(filterClass);
×
976
                }
977
            }, timeout);
978
            that.ui.document.off(click);
×
979
        });
980

981
        // shows/hides filter box
982
        filterTrigger.add(filterClose).on(click, function(e) {
21✔
983
            e.preventDefault();
×
984
            e.stopImmediatePropagation();
×
UNCOV
985
            if (filterActive) {
×
986
                filterContainer.hide();
×
UNCOV
987
                pageTreeHeader.removeClass(filterClass);
×
UNCOV
988
                that.ui.document.off(click);
×
UNCOV
989
                filterActive = false;
×
990
            } else {
UNCOV
991
                filterContainer.show();
×
992
                pageTreeHeader.addClass(filterClass);
×
UNCOV
993
                that.ui.document.on(click, function() {
×
UNCOV
994
                    filterActive = true;
×
UNCOV
995
                    filterTrigger.trigger(click);
×
996
                });
UNCOV
997
                filterActive = true;
×
998
            }
999
        });
1000

1001
        // prevent closing when on filter container
1002
        filterContainer.on('click', function(e) {
21✔
UNCOV
1003
            e.stopImmediatePropagation();
×
1004
        });
1005

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

1010
    /**
1011
     * Shows paste helpers.
1012
     *
1013
     * @method _enablePaste
1014
     * @param {String} [selector=this.options.pasteSelector] jquery selector
1015
     * @private
1016
     */
1017
    _enablePaste: function _enablePaste(selector) {
1018
        var sel = typeof selector === 'undefined'
2✔
1019
            ? this.options.pasteSelector
1020
            : selector + ' ' + this.options.pasteSelector;
1021
        var dropdownSel = '.js-cms-pagetree-actions-dropdown';
2✔
1022

1023
        if (typeof selector !== 'undefined') {
2✔
1024
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1025
        }
1026

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

1031
        var data = {};
2✔
1032

1033
        if (this.clipboard.type === 'cut') {
2!
UNCOV
1034
            data.has_cut = true;
×
1035
        } else {
1036
            data.has_copy = true;
2✔
1037
        }
1038
        // not loaded actions dropdown have to be updated as well
1039
        $(dropdownSel).data('lazyUrlData', data);
2✔
1040
    },
1041

1042
    /**
1043
     * Hides paste helpers.
1044
     *
1045
     * @method _disablePaste
1046
     * @param {String} [selector=this.options.pasteSelector] jquery selector
1047
     * @private
1048
     */
1049
    _disablePaste: function _disablePaste(selector) {
1050
        var sel = typeof selector === 'undefined'
2✔
1051
            ? this.options.pasteSelector
1052
            : selector + ' ' + this.options.pasteSelector;
1053
        var dropdownSel = '.js-cms-pagetree-actions-dropdown';
2✔
1054

1055
        if (typeof selector !== 'undefined') {
2✔
1056
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1057
        }
1058

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

1063
        // not loaded actions dropdown have to be updated as well
1064
        $(dropdownSel).removeData('lazyUrlData');
2✔
1065
    },
1066

1067
    /**
1068
     * Updates the current state of the helpers after `after_open.jstree`
1069
     * or `_cutOrCopy` is triggered.
1070
     *
1071
     * @method _updatePasteHelpersState
1072
     * @private
1073
     */
1074
    _updatePasteHelpersState: function _updatePasteHelpersState() {
1075
        var that = this;
4✔
1076

1077
        if (this.clipboard.type && this.clipboard.id) {
4✔
1078
            this._enablePaste();
3✔
1079
        }
1080

1081
        // hide cut element and it's descendants' paste helpers if it is visible
1082
        if (
4✔
1083
            this.clipboard.type === 'cut' &&
8✔
1084
            this.clipboard.origin &&
1085
            this.options.site === this.clipboard.source_site
1086
        ) {
1087
            var descendantIds = this._getDescendantsIds(this.clipboard.id);
2✔
1088
            var nodes = [this.clipboard.id];
2✔
1089

1090
            if (descendantIds && descendantIds.length) {
2✔
1091
                nodes = nodes.concat(descendantIds);
1✔
1092
            }
1093

1094
            nodes.forEach(function(id) {
2✔
1095
                that._disablePaste('.jsgrid_' + id + '_col');
4✔
1096
            });
1097
        }
1098
    },
1099

1100
    /**
1101
     * Shows success message on node after successful action.
1102
     *
1103
     * @method _showSuccess
1104
     * @param {Number} id id of the element to add the success class
1105
     * @private
1106
     */
1107
    _showSuccess: function _showSuccess(id) {
UNCOV
1108
        var element = this.ui.tree.find('li[data-id="' + id + '"]');
×
1109

UNCOV
1110
        element.addClass('cms-tree-node-success');
×
UNCOV
1111
        setTimeout(function() {
×
UNCOV
1112
            element.removeClass('cms-tree-node-success');
×
1113
        }, this.successTimer);
1114
        // hide elements
UNCOV
1115
        this._disablePaste();
×
1116
        this.clipboard.id = null;
×
1117
    },
1118

1119
    /**
1120
     * Checks if we should reload the iframe or entire window. For this we
1121
     * need to skip `CMS.API.Helpers.reloadBrowser();`.
1122
     *
1123
     * @method _reloadHelper
1124
     * @private
1125
     */
1126
    _reloadHelper: function _reloadHelper() {
UNCOV
1127
        if (window.self === window.top) {
×
UNCOV
1128
            Helpers.reloadBrowser();
×
1129
        } else {
1130
            window.location.reload();
×
1131
        }
1132
    },
1133

1134
    /**
1135
     * Displays an error within the django UI.
1136
     *
1137
     * @method showError
1138
     * @param {String} message string message to display
1139
     */
1140
    showError: function showError(message) {
UNCOV
1141
        var messages = $('.messagelist');
×
UNCOV
1142
        var breadcrumb = $('.breadcrumbs');
×
1143
        var reload = this.options.lang.reload;
×
1144
        var tpl =
1145
            '' +
×
1146
            '<ul class="messagelist">' +
1147
            '   <li class="error">' +
1148
            '       {msg} ' +
1149
            '       <a href="#reload" class="cms-tree-reload"> ' +
1150
            reload +
1151
            ' </a>' +
1152
            '   </li>' +
1153
            '</ul>';
UNCOV
1154
        var msg = tpl.replace('{msg}', '<strong>' + this.options.lang.error + '</strong> ' + message);
×
1155

UNCOV
1156
        if (messages.length) {
×
UNCOV
1157
            messages.replaceWith(msg);
×
1158
        } else {
UNCOV
1159
            breadcrumb.after(msg);
×
1160
        }
1161
    },
1162

1163
    /**
1164
     * @method _getDescendantsIds
1165
     * @private
1166
     * @param {String} nodeId jstree id of the node, e.g. j1_3
1167
     * @returns {String[]} array of ids
1168
     */
1169
    _getDescendantsIds: function _getDescendantsIds(nodeId) {
1170
        return this.ui.tree.jstree(true).get_node(nodeId).children_d;
2✔
1171
    },
1172

1173
    /**
1174
     * @method _hasPermision
1175
     * @private
1176
     * @param {Object} node jstree node
1177
     * @param {String} permission move / add
1178
     * @returns {Boolean}
1179
     */
1180
    _hasPermission: function _hasPermision(node, permission) {
UNCOV
1181
        if (node.id === '#' && permission === 'add') {
×
UNCOV
1182
            return this.options.hasAddRootPermission;
×
UNCOV
1183
        } else if (node.id === '#') {
×
UNCOV
1184
            return false;
×
1185
        }
1186

UNCOV
1187
        return node.li_attr['data-' + permission + '-permission'] === 'true';
×
1188
    }
1189
});
1190

1191
PageTree._init = function() {
1✔
1192
    new PageTree();
1✔
1193
};
1194

1195
// shorthand for jQuery(document).ready();
1196
$(function() {
1✔
1197
    // load cms settings beforehand
1198
    // have to set toolbar to "expanded" by default
1199
    // otherwise initialization will be incorrect when you
1200
    // go first to pages and then to normal page
1201
    window.CMS.config = {
1✔
1202
        isPageTree: true,
1203
        settings: {
1204
            toolbar: 'expanded',
1205
            version: __CMS_VERSION__
1206
        },
1207
        urls: {
1208
            settings: $('.js-cms-pagetree').data('settings-url')
1209
        }
1210
    };
1211
    window.CMS.settings = Helpers.getSettings();
1✔
1212
    // autoload the pagetree
1213
    CMS.PageTree._init();
1✔
1214
});
1215

1216
export default PageTree;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc