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

divio / django-cms / #30101

12 Nov 2025 11:07AM UTC coverage: 90.532%. Remained the same
#30101

push

travis-ci

web-flow
Merge 12b8c6dbd into c38b75715

1306 of 2044 branches covered (63.89%)

294 of 321 new or added lines in 13 files covered. (91.59%)

458 existing lines in 7 files now uncovered.

9151 of 10108 relevant lines covered (90.53%)

11.16 hits per line

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

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

5
import $ from 'jquery';
6

7
import Class from 'classjs';
8
import { Helpers, KEYS } from './cms.base';
9
import PageTreeDropdowns from './cms.pagetree.dropdown';
10
import PageTreeStickyHeader from './cms.pagetree.stickyheader';
11
// switched from commonjs 'lodash' bundle to per-method ESM imports for better tree-shaking
12
import debounce from 'lodash-es/debounce.js';
13
import without from 'lodash-es/without.js';
14

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

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

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

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

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

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

50
        this._setupLanguages();
21✔
51

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

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

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

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

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

93
            // eslint-disable-next-line no-undef
NEW
94
            const url = new URL(window.location.href);
×
95

NEW
96
            url.searchParams.delete('language');
×
NEW
97
            url.searchParams.set('language', newLanguage);
×
98

NEW
99
            window.location.href = url.toString();
×
100
        });
101
    },
102

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

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

145
                        return '';
×
146
                    },
147
                    width: obj.width || '1%',
360✔
148
                    wideCellClass: obj.cls
149
                });
150
            }
151
        });
152

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

168
                    // we need to store the opened items inside the localstorage
169
                    // as we have to load the pagetree with the previous opened
170
                    // state
171
                    obj.openNodes = that._getStoredNodeIds();
20✔
172

173
                    // we need to set the site id to get the correct tree
174
                    obj.site = that.options.site;
20✔
175

176
                    return obj;
20✔
177
                }
178
            };
179
        }
180

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

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

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

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

255
        // set events for the nodeId updates
256
        this.ui.tree.on('after_close.jstree', function(e, el) {
21✔
UNCOV
257
            that._removeNodeId(el.node.data.nodeId);
×
258
        });
259

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

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

271
        this.ui.document.on('keydown.pagetree.alt-mode', function(e) {
21✔
272
            if (e.keyCode === KEYS.SHIFT) {
466!
UNCOV
273
                that.ui.container.addClass('cms-pagetree-alt-mode');
×
274
            }
275
        });
276

277
        this.ui.document.on('keyup.pagetree.alt-mode', function(e) {
21✔
278
            if (e.keyCode === KEYS.SHIFT) {
463!
UNCOV
279
                that.ui.container.removeClass('cms-pagetree-alt-mode');
×
280
            }
281
        });
282

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

298
        this.ui.document.on('dnd_start.vakata', function(e, data) {
21✔
UNCOV
299
            var element = $(data.element);
×
UNCOV
300
            var node = element.parent();
×
301

UNCOV
302
            that._dropdowns.closeAllDropdowns();
×
303

304
            node.addClass('jstree-is-dragging');
×
UNCOV
305
            data.data.nodes.forEach(function(nodeId) {
×
306
                var descendantIds = that._getDescendantsIds(nodeId);
×
307

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

313
            if (!node.hasClass('jstree-leaf')) {
×
UNCOV
314
                data.helper.addClass('is-stacked');
×
315
            }
316
        });
317

318
        var isCopyClassAdded = false;
21✔
319

320
        this.ui.document.on('dnd_move.vakata', function(e, data) {
21✔
321
            var isMovingCopy =
UNCOV
322
                data.data.origin &&
×
323
                (data.data.origin.settings.dnd.always_copy ||
324
                    (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)));
325

326
            if (isMovingCopy) {
×
UNCOV
327
                if (!isCopyClassAdded) {
×
UNCOV
328
                    $('.jstree-is-dragging').addClass('jstree-is-dragging-copy');
×
UNCOV
329
                    isCopyClassAdded = true;
×
330
                }
331
            } else if (isCopyClassAdded) {
×
332
                $('.jstree-is-dragging').removeClass('jstree-is-dragging-copy');
×
333
                isCopyClassAdded = false;
×
334
            }
335

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

341
            // make sure we're hovering over a tree node
UNCOV
342
            if (ins) {
×
343
                var marker = $('#jstree-marker');
×
UNCOV
344
                var root = $('#changelist');
×
UNCOV
345
                var column = $(data.data.origin.element);
×
346

347
                var hover = ins.settings.dnd.large_drop_target ?
×
348
                    $(data.event.target)
349
                        .closest('.jstree-node') :
350
                    $(data.event.target)
351
                        .closest('.jstree-anchor').parent();
352

UNCOV
353
                var width = root.width() - (column.width() - hover.width());
×
354

UNCOV
355
                marker.css({
×
356
                    left: `${root.offset().left}px`,
357
                    width: `${width}px`
358
                });
359
            }
360
        });
361

362
        this.ui.document.on('dnd_stop.vakata', function(e, data) {
21✔
UNCOV
363
            var element = $(data.element);
×
UNCOV
364
            var node = element.parent();
×
365

UNCOV
366
            node.removeClass('jstree-is-dragging jstree-is-dragging-copy');
×
367
            data.data.nodes.forEach(function(nodeId) {
×
368
                var descendantIds = that._getDescendantsIds(nodeId);
×
369

370
                [nodeId].concat(descendantIds).forEach(function(id) {
×
371
                    $('.jsgrid_' + id + '_col').removeClass('jstree-is-dragging jstree-is-dragging-copy');
×
372
                });
373
            });
374
        });
375

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

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

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

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

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

420
        // advanced settings link handling
421
        this.ui.container.on(this.click, '.js-cms-tree-advanced-settings', function(e) {
21✔
UNCOV
422
            if (e.shiftKey) {
×
UNCOV
423
                e.preventDefault();
×
UNCOV
424
                var link = $(this);
×
425

426
                if (link.data('url')) {
×
427
                    window.location.href = link.data('url');
×
428
                }
429
            }
430
        });
431

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

UNCOV
436
            const nodeData = this.ui.tree.jstree('get_node', treeId);
×
437

438
            this._storeNodeId(nodeData.data.id);
×
439
        });
440

441
        // add events for error reload (messagelist)
442
        this.ui.document.on(this.click, '.messagelist .cms-tree-reload', function(e) {
21✔
UNCOV
443
            e.preventDefault();
×
UNCOV
444
            that._reloadHelper();
×
445
        });
446

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

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

459
        // additional event handlers
460
        this._setupDropdowns();
21✔
461
        this._setupSearch();
21✔
462

463
        // make sure ajax post requests are working
464
        this._setAjaxPost('.js-cms-tree-item-menu a');
21✔
465
        this._setAjaxPost('.js-cms-tree-lang-trigger');
21✔
466
        this._setAjaxPost('.js-cms-tree-item-set-home a');
21✔
467

468
        this._setupPageView();
21✔
469
        this._setupStickyHeader();
21✔
470

471
        this.ui.tree.on('ready.jstree', () => this._getClipboard());
21✔
472
    },
473

474
    _getClipboard: function _getClipboard() {
475
        this.clipboard = CMS.settings.pageClipboard || this.clipboard;
1✔
476

477
        if (this.clipboard.type && this.clipboard.origin) {
1!
UNCOV
478
            this._enablePaste();
×
UNCOV
479
            this._updatePasteHelpersState();
×
480
        }
481
    },
482

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

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

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

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

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

538
        if (this.clipboard.source_site === this.options.site) {
5!
UNCOV
539
            if (this.clipboard.type === 'cut') {
×
UNCOV
540
                this.ui.tree.jstree('cut', copyFromId);
×
541
            } else {
UNCOV
542
                this.ui.tree.jstree('copy', copyFromId);
×
543
            }
544

UNCOV
545
            this.clipboard.isPasting = true;
×
546
            this.ui.tree.jstree('paste', copyToId, 'last');
×
547
        } else {
548
            const dummyId = this.ui.tree.jstree('create_node', copyToId, 'Loading', 'last');
5✔
549

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

564
        this.clipboard.id = null;
5✔
565
        this.clipboard.type = null;
5✔
566
        this.clipboard.origin = null;
5✔
567
        this.clipboard.isPasting = false;
5✔
568
        CMS.settings.pageClipboard = this.clipboard;
5✔
569
        Helpers.setSettings(CMS.settings);
5✔
570
    },
571

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

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

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

600
        CMS.settings.pagetree = storage;
×
601
        Helpers.setSettings(CMS.settings);
×
602

UNCOV
603
        return number;
×
604
    },
605

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

620
        const idsToRemove = [id].concat(
×
621
            childrenIds.map(childId => {
UNCOV
622
                const node = instance.get_node({ id: childId });
×
623

624
                if (!node || !node.data) {
×
UNCOV
625
                    return node;
×
626
                }
627

628
                return node.data.nodeId;
×
629
            })
630
        );
631

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

UNCOV
634
        CMS.settings.pagetree = storage;
×
UNCOV
635
        Helpers.setSettings(CMS.settings);
×
636

UNCOV
637
        return id;
×
638
    },
639

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

UNCOV
654
        if (!obj.id && this.clipboard.type === 'cut' && this.clipboard.origin) {
×
UNCOV
655
            obj.id = this.clipboard.origin;
×
656
            obj.source_site = this.clipboard.source_site;
×
657
        } else {
658
            obj.site = that.options.site;
×
659
        }
660

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

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

UNCOV
689
        if (obj) {
×
690
            node = that._getNodePosition(obj);
×
691
        }
692

693
        var data = {
×
694
            // obj.original.data.id is for drag copy
695
            id: this.clipboard.origin || obj.original.data.id,
×
696
            position: node.position
697
        };
698

UNCOV
699
        if (this.clipboard.source_site) {
×
UNCOV
700
            data.source_site = this.clipboard.source_site;
×
701
        } else {
UNCOV
702
            data.source_site = this.options.site;
×
703
        }
704

705
        // if there is no target provided, the node lands in root
706
        if (node.target) {
×
UNCOV
707
            data.target = node.target;
×
708
        }
709

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

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

743
                    submitButton.prop('disabled', true);
×
744

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

750
                    that._saveCopiedNode(data);
×
751
                });
752
        } else {
UNCOV
753
            this._saveCopiedNode(data);
×
754
        }
755
    },
756

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

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

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

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

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

UNCOV
813
        data.position = obj.position;
×
814

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

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

827
        return data;
×
828
    },
829

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

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

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

866
    /**
867
     * @method _setupStickyHeader
868
     * @private
869
     */
870
    _setupStickyHeader: function _setupStickyHeader() {
871
        var that = this;
21✔
872

873
        that.ui.tree.on('ready.jstree', function() {
21✔
UNCOV
874
            that.header = new PageTreeStickyHeader({
×
875
                container: that.ui.container
876
            });
877
        });
878
    },
879

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

890
        this.ui.container.on(this.click, trigger, function(e) {
63✔
UNCOV
891
            e.preventDefault();
×
892

UNCOV
893
            var element = $(this);
×
894

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

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

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

919
            $.ajax({
×
920
                method: 'post',
921
                url: $(this).attr('href')
922
            })
923
                .done(function() {
UNCOV
924
                    try {
×
UNCOV
925
                        window.top.CMS.API.Toolbar.hideLoader();
×
926
                    } catch {}
927

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

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

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

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

964
        var searchContainer = this.ui.container.find('.cms-pagetree-header-filter');
21✔
965
        var searchField = searchContainer.find('#field-searchbar');
21✔
966
        var timeout = 200;
21✔
967

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

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

1005
        // prevent closing when on filter container
1006
        filterContainer.on('click', function(e) {
21✔
UNCOV
1007
            e.stopImmediatePropagation();
×
1008
        });
1009

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

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

1027
        if (typeof selector !== 'undefined') {
2✔
1028
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1029
        }
1030

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

1035
        var data = {};
2✔
1036

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

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

1059
        if (typeof selector !== 'undefined') {
2✔
1060
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1061
        }
1062

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

1067
        // not loaded actions dropdown have to be updated as well
1068
        $(dropdownSel).removeData('lazyUrlData');
2✔
1069
    },
1070

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

1081
        if (this.clipboard.type && this.clipboard.id) {
4✔
1082
            this._enablePaste();
3✔
1083
        }
1084

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

1094
            if (descendantIds && descendantIds.length) {
2✔
1095
                nodes = nodes.concat(descendantIds);
1✔
1096
            }
1097

1098
            nodes.forEach(function(id) {
2✔
1099
                that._disablePaste('.jsgrid_' + id + '_col');
4✔
1100
            });
1101
        }
1102
    },
1103

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

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

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

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

UNCOV
1160
        if (messages.length) {
×
UNCOV
1161
            messages.replaceWith(msg);
×
1162
        } else {
UNCOV
1163
            breadcrumb.after(msg);
×
1164
        }
1165
    },
1166

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

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

1191
        return node.li_attr['data-' + permission + '-permission'] === 'true';
×
1192
    }
1193
});
1194

1195
PageTree._init = function() {
1✔
1196
    new PageTree();
1✔
1197
};
1198

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

1220
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