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

divio / django-cms / #30058

05 Nov 2025 12:06AM UTC coverage: 75.234%. Remained the same
#30058

push

travis-ci

web-flow
Merge 93a92eecf into 81729afcf

1079 of 1624 branches covered (66.44%)

2567 of 3412 relevant lines covered (75.23%)

26.26 hits per line

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

36.02
/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

444
        // additional event handlers
445
        this._setupDropdowns();
21✔
446
        this._setupSearch();
21✔
447

448
        // make sure ajax post requests are working
449
        this._setAjaxPost('.js-cms-tree-item-menu a');
21✔
450
        this._setAjaxPost('.js-cms-tree-lang-trigger');
21✔
451
        this._setAjaxPost('.js-cms-tree-item-set-home a');
21✔
452

453
        this._setupPageView();
21✔
454
        this._setupStickyHeader();
21✔
455

456
        this.ui.tree.on('ready.jstree', () => this._getClipboard());
21✔
457
    },
458

459
    _getClipboard: function _getClipboard() {
460
        this.clipboard = CMS.settings.pageClipboard || this.clipboard;
1✔
461

462
        if (this.clipboard.type && this.clipboard.origin) {
1!
463
            this._enablePaste();
×
464
            this._updatePasteHelpersState();
×
465
        }
466
    },
467

468
    /**
469
     * Helper to process the cut and copy events.
470
     *
471
     * @method _cutOrCopy
472
     * @param {Object} [obj]
473
     * @param {Number} [obj.type] either 'cut' or 'copy'
474
     * @param {Number} [obj.element] originated trigger element
475
     * @private
476
     * @returns {Boolean|void}
477
     */
478
    _cutOrCopy: function _cutOrCopy(obj) {
479
        // prevent actions if you try to copy a page with an apphook
480
        if (obj.type === 'copy' && obj.element.data().apphook) {
×
481
            this.showError(this.options.lang.apphook);
×
482
            return false;
×
483
        }
484

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

487
        // resets if we click again
488
        if (this.clipboard.type === obj.type && jsTreeId === this.clipboard.id) {
×
489
            this.clipboard.type = null;
×
490
            this.clipboard.id = null;
×
491
            this.clipboard.origin = null;
×
492
            this.clipboard.source_site = null;
×
493
            this._disablePaste();
×
494
        } else {
495
            this.clipboard.origin = obj.element.data().id; // this._getNodeId(obj.element);
×
496
            this.clipboard.type = obj.type;
×
497
            this.clipboard.id = jsTreeId;
×
498
            this.clipboard.source_site = this.options.site;
×
499
            this._updatePasteHelpersState();
×
500
        }
501
        if (this.clipboard.type === 'copy' || !this.clipboard.type) {
×
502
            CMS.settings.pageClipboard = this.clipboard;
×
503
            Helpers.setSettings(CMS.settings);
×
504
        }
505
    },
506

507
    /**
508
     * Helper to process the paste event.
509
     *
510
     * @method _paste
511
     * @param {$.Event} event click event
512
     * @private
513
     */
514
    _paste: function _paste(event) {
515
        // hide helpers after we picked one
516
        this._disablePaste();
5✔
517

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

523
        if (this.clipboard.source_site === this.options.site) {
5!
524
            if (this.clipboard.type === 'cut') {
×
525
                this.ui.tree.jstree('cut', copyFromId);
×
526
            } else {
527
                this.ui.tree.jstree('copy', copyFromId);
×
528
            }
529

530
            this.clipboard.isPasting = true;
×
531
            this.ui.tree.jstree('paste', copyToId, 'last');
×
532
        } else {
533
            const dummyId = this.ui.tree.jstree('create_node', copyToId, 'Loading', 'last');
5✔
534

535
            if (this.ui.tree.length) {
5!
536
                this.ui.tree.jstree('cut', dummyId);
5✔
537
                this.clipboard.isPasting = true;
5✔
538
                this.ui.tree.jstree('paste', copyToId, 'last');
5✔
539
            } else {
540
                if (this.clipboard.type === 'copy') {
×
541
                    this._copyNode();
×
542
                }
543
                if (this.clipboard.type === 'cut') {
×
544
                    this._moveNode();
×
545
                }
546
            }
547
        }
548

549
        this.clipboard.id = null;
5✔
550
        this.clipboard.type = null;
5✔
551
        this.clipboard.origin = null;
5✔
552
        this.clipboard.isPasting = false;
5✔
553
        CMS.settings.pageClipboard = this.clipboard;
5✔
554
        Helpers.setSettings(CMS.settings);
5✔
555
    },
556

557
    /**
558
     * Retreives a list of nodes from local storage.
559
     *
560
     * @method _getStoredNodeIds
561
     * @private
562
     * @returns {Array} list of ids
563
     */
564
    _getStoredNodeIds: function _getStoredNodeIds() {
565
        return CMS.settings.pagetree || [];
20✔
566
    },
567

568
    /**
569
     * Stores a node in local storage.
570
     *
571
     * @method _storeNodeId
572
     * @private
573
     * @param {String} id to be stored
574
     * @returns {String} id that has been stored
575
     */
576
    _storeNodeId: function _storeNodeId(id) {
577
        var number = id;
×
578
        var storage = this._getStoredNodeIds();
×
579

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

585
        CMS.settings.pagetree = storage;
×
586
        Helpers.setSettings(CMS.settings);
×
587

588
        return number;
×
589
    },
590

591
    /**
592
     * Removes a node in local storage.
593
     *
594
     * @method _removeNodeId
595
     * @private
596
     * @param {String} id to be stored
597
     * @returns {String} id that has been removed
598
     */
599
    _removeNodeId: function _removeNodeId(id) {
600
        const instance = this.ui.tree.jstree(true);
×
601
        const childrenIds = instance.get_node({
×
602
            id: CMS.$(`[data-node-id=${id}]`).attr('id')
603
        }).children_d;
604

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

609
                if (!node || !node.data) {
×
610
                    return node;
×
611
                }
612

613
                return node.data.nodeId;
×
614
            })
615
        );
616

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

619
        CMS.settings.pagetree = storage;
×
620
        Helpers.setSettings(CMS.settings);
×
621

622
        return id;
×
623
    },
624

625
    /**
626
     * Moves a node after drag & drop.
627
     *
628
     * @method _moveNode
629
     * @param {Object} [obj]
630
     * @param {Number} [obj.id] current element id for url matching
631
     * @param {Number} [obj.target] target sibling or parent
632
     * @param {Number} [obj.position] either `left`, `right` or `last-child`
633
     * @returns {$.Deferred} ajax request object
634
     * @private
635
     */
636
    _moveNode: function _moveNode(obj) {
637
        var that = this;
×
638

639
        if (!obj.id && this.clipboard.type === 'cut' && this.clipboard.origin) {
×
640
            obj.id = this.clipboard.origin;
×
641
            obj.source_site = this.clipboard.source_site;
×
642
        } else {
643
            obj.site = that.options.site;
×
644
        }
645

646
        return $.ajax({
×
647
            method: 'post',
648
            url: that.options.urls.move.replace('{id}', obj.id),
649
            data: obj
650
        })
651
            .done(function(r) {
652
                if (r.status && r.status === 400) { // eslint-disable-line
×
653
                    that.showError(r.content);
×
654
                } else {
655
                    that._showSuccess(obj.id);
×
656
                }
657
            })
658
            .fail(function(error) {
659
                that.showError(error.statusText);
×
660
            });
661
    },
662

663
    /**
664
     * Copies a node into the selected node.
665
     *
666
     * @method _copyNode
667
     * @param {Object} obj page obj
668
     * @private
669
     */
670
    _copyNode: function _copyNode(obj) {
671
        var that = this;
×
672
        var node = { position: 0 };
×
673

674
        if (obj) {
×
675
            node = that._getNodePosition(obj);
×
676
        }
677

678
        var data = {
×
679
            // obj.original.data.id is for drag copy
680
            id: this.clipboard.origin || obj.original.data.id,
×
681
            position: node.position
682
        };
683

684
        if (this.clipboard.source_site) {
×
685
            data.source_site = this.clipboard.source_site;
×
686
        } else {
687
            data.source_site = this.options.site;
×
688
        }
689

690
        // if there is no target provided, the node lands in root
691
        if (node.target) {
×
692
            data.target = node.target;
×
693
        }
694

695
        if (that.options.permission) {
×
696
            // we need to load a dialog first, to check if permissions should
697
            // be copied or not
698
            $.ajax({
×
699
                method: 'post',
700
                url: that.options.urls.copyPermission.replace('{id}', data.id),
701
                data: data
702
                // the dialog is loaded via the ajax respons originating from
703
                // `templates/admin/cms/page/tree/copy_premissions.html`
704
            })
705
                .done(function(dialog) {
706
                    that.ui.dialog.append(dialog);
×
707
                })
708
                .fail(function(error) {
709
                    that.showError(error.statusText);
×
710
                });
711

712
            // attach events to the permission dialog
713
            this.ui.dialog
×
714
                .off(this.click, '.cancel')
715
                .on(this.click, '.cancel', function(e) {
716
                    e.preventDefault();
×
717
                    // remove just copied node
718
                    that.ui.tree.jstree('delete_node', obj.node.id);
×
719
                    $('.js-cms-dialog').remove();
×
720
                    $('.js-cms-dialog-dimmer').remove();
×
721
                })
722
                .off(this.click, '.submit')
723
                .on(this.click, '.submit', function(e) {
724
                    e.preventDefault();
×
725
                    var submitButton = $(this);
×
726
                    var formData = submitButton.closest('form').serialize().split('&');
×
727

728
                    submitButton.prop('disabled', true);
×
729

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

735
                    that._saveCopiedNode(data);
×
736
                });
737
        } else {
738
            this._saveCopiedNode(data);
×
739
        }
740
    },
741

742
    /**
743
     * Sends the request to copy a node.
744
     *
745
     * @method _saveCopiedNode
746
     * @private
747
     * @param {Object} data node position information
748
     * @returns {$.Deferred}
749
     */
750
    _saveCopiedNode: function _saveCopiedNode(data) {
751
        var that = this;
×
752

753
        // send the real ajax request for copying the plugin
754
        return $.ajax({
×
755
            method: 'post',
756
            url: that.options.urls.copy.replace('{id}', data.id),
757
            data: data
758
        })
759
            .done(function(r) {
760
                if (r.status && r.status === 400) { // eslint-disable-line
×
761
                    that.showError(r.content);
×
762
                } else {
763
                    that._reloadHelper();
×
764
                }
765
            })
766
            .fail(function(error) {
767
                that.showError(error.statusText);
×
768
            });
769
    },
770

771
    /**
772
     * Returns element from any sub nodes.
773
     *
774
     * @method _getElement
775
     * @private
776
     * @param {jQuery} el jQuery node form where to search
777
     * @returns {String} jsTree node element id
778
     */
779
    _getNodeId: function _getNodeId(el) {
780
        var cls = el.closest('.jstree-grid-cell').attr('class');
2✔
781

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

786
    /**
787
     * Gets the new node position after moving.
788
     *
789
     * @method _getNodePosition
790
     * @private
791
     * @param {Object} obj jstree move object
792
     * @returns {Object} evaluated object with params
793
     */
794
    _getNodePosition: function _getNodePosition(obj) {
795
        var data = {};
×
796
        var node = this.ui.tree.jstree('get_node', obj.node.parent);
×
797

798
        data.position = obj.position;
×
799

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

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

812
        return data;
×
813
    },
814

815
    /**
816
     * Sets up general tooltips that can have a list of links or content.
817
     *
818
     * @method _setupDropdowns
819
     * @private
820
     */
821
    _setupDropdowns: function _setupDropdowns() {
822
        this._dropdowns = new PageTreeDropdowns({
21✔
823
            container: this.ui.container
824
        });
825
    },
826

827
    /**
828
     * Handles page view click. Usual use case is that after you click
829
     * on view page in the pagetree - sideframe is no longer needed,
830
     * so we close it.
831
     *
832
     * @method _setupPageView
833
     * @private
834
     */
835
    _setupPageView: function _setupPageView() {
836
        var win = Helpers._getWindow();
25✔
837
        var parent = win.parent ? win.parent : win;
25✔
838

839
        this.ui.container.on(this.click, '.js-cms-pagetree-page-view', function() {
25✔
840
            parent.CMS.API.Helpers.setSettings(
3✔
841
                $.extend(true, {}, CMS.settings, {
842
                    sideframe: {
843
                        url: null,
844
                        hidden: true
845
                    }
846
                })
847
            );
848
        });
849
    },
850

851
    /**
852
     * @method _setupStickyHeader
853
     * @private
854
     */
855
    _setupStickyHeader: function _setupStickyHeader() {
856
        var that = this;
21✔
857

858
        that.ui.tree.on('ready.jstree', function() {
21✔
859
            that.header = new PageTreeStickyHeader({
×
860
                container: that.ui.container
861
            });
862
        });
863
    },
864

865
    /**
866
     * Triggers the links `href` as ajax post request.
867
     *
868
     * @method _setAjaxPost
869
     * @private
870
     * @param {jQuery} trigger jQuery link target
871
     */
872
    _setAjaxPost: function _setAjaxPost(trigger) {
873
        var that = this;
63✔
874

875
        this.ui.container.on(this.click, trigger, function(e) {
63✔
876
            e.preventDefault();
×
877

878
            var element = $(this);
×
879

880
            if (element.closest('.cms-pagetree-dropdown-item-disabled').length) {
×
881
                return;
×
882
            }
883
            if (element.attr('target') === '_top') {
×
884
                // Post to target="_top" requires to create a form and submit it
885
                var parent = window;
×
886

887
                if (window.parent) {
×
888
                    parent = window.parent;
×
889
                }
890
                let formToken = document.querySelector('form input[name="csrfmiddlewaretoken"]');
×
891
                let csrfToken = '<input type="hidden" name="csrfmiddlewaretoken" value="' +
×
892
                    ((formToken ? formToken.value : formToken) || window.CMS.config.csrf) + '">';
×
893

894
                $('<form method="post" action="' + element.attr('href') + '">' +
×
895
                    csrfToken + '</form>')
896
                    .appendTo($(parent.document.body))
897
                    .submit();
898
                return;
×
899
            }
900
            try {
×
901
                window.top.CMS.API.Toolbar.showLoader();
×
902
            } catch (err) {}
903

904
            $.ajax({
×
905
                method: 'post',
906
                url: $(this).attr('href')
907
            })
908
                .done(function() {
909
                    try {
×
910
                        window.top.CMS.API.Toolbar.hideLoader();
×
911
                    } catch (err) {}
912

913
                    if (window.self === window.top) {
×
914
                        // simply reload the page
915
                        that._reloadHelper();
×
916
                    } else {
917
                        Helpers.reloadBrowser('REFRESH_PAGE');
×
918
                    }
919
                })
920
                .fail(function(error) {
921
                    try {
×
922
                        window.top.CMS.API.Toolbar.hideLoader();
×
923
                    } catch (err) {}
924
                    that.showError(error.responseText ? error.responseText : error.statusText);
×
925
                });
926
        });
927
    },
928

929
    /**
930
     * Sets events for the search on the header.
931
     *
932
     * @method _setupSearch
933
     * @private
934
     */
935
    _setupSearch: function _setupSearch() {
936
        var that = this;
21✔
937
        var click = this.click + '.search';
21✔
938

939
        var filterActive = false;
21✔
940
        var filterTrigger = this.ui.container.find('.js-cms-pagetree-header-filter-trigger');
21✔
941
        var filterContainer = this.ui.container.find('.js-cms-pagetree-header-filter-container');
21✔
942
        var filterClose = filterContainer.find('.js-cms-pagetree-header-search-close');
21✔
943
        var filterClass = 'cms-pagetree-header-filter-active';
21✔
944
        var pageTreeHeader = $('.cms-pagetree-header');
21✔
945

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

949
        var searchContainer = this.ui.container.find('.cms-pagetree-header-filter');
21✔
950
        var searchField = searchContainer.find('#field-searchbar');
21✔
951
        var timeout = 200;
21✔
952

953
        // add active class when focusing the search field
954
        searchField.on('focus', function(e) {
21✔
955
            e.stopImmediatePropagation();
×
956
            pageTreeHeader.addClass(filterClass);
×
957
        });
958
        searchField.on('blur', function(e) {
21✔
959
            e.stopImmediatePropagation();
×
960
            // timeout is required to prevent the search field from jumping
961
            // between enlarging and shrinking
962
            setTimeout(function() {
×
963
                if (!filterActive) {
×
964
                    pageTreeHeader.removeClass(filterClass);
×
965
                }
966
            }, timeout);
967
            that.ui.document.off(click);
×
968
        });
969

970
        // shows/hides filter box
971
        filterTrigger.add(filterClose).on(click, function(e) {
21✔
972
            e.preventDefault();
×
973
            e.stopImmediatePropagation();
×
974
            if (filterActive) {
×
975
                filterContainer.hide();
×
976
                pageTreeHeader.removeClass(filterClass);
×
977
                that.ui.document.off(click);
×
978
                filterActive = false;
×
979
            } else {
980
                filterContainer.show();
×
981
                pageTreeHeader.addClass(filterClass);
×
982
                that.ui.document.on(click, function() {
×
983
                    filterActive = true;
×
984
                    filterTrigger.trigger(click);
×
985
                });
986
                filterActive = true;
×
987
            }
988
        });
989

990
        // prevent closing when on filter container
991
        filterContainer.on('click', function(e) {
21✔
992
            e.stopImmediatePropagation();
×
993
        });
994

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

999
    /**
1000
     * Shows paste helpers.
1001
     *
1002
     * @method _enablePaste
1003
     * @param {String} [selector=this.options.pasteSelector] jquery selector
1004
     * @private
1005
     */
1006
    _enablePaste: function _enablePaste(selector) {
1007
        var sel = typeof selector === 'undefined'
2✔
1008
            ? this.options.pasteSelector
1009
            : selector + ' ' + this.options.pasteSelector;
1010
        var dropdownSel = '.js-cms-pagetree-actions-dropdown';
2✔
1011

1012
        if (typeof selector !== 'undefined') {
2✔
1013
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1014
        }
1015

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

1020
        var data = {};
2✔
1021

1022
        if (this.clipboard.type === 'cut') {
2!
1023
            data.has_cut = true;
×
1024
        } else {
1025
            data.has_copy = true;
2✔
1026
        }
1027
        // not loaded actions dropdown have to be updated as well
1028
        $(dropdownSel).data('lazyUrlData', data);
2✔
1029
    },
1030

1031
    /**
1032
     * Hides paste helpers.
1033
     *
1034
     * @method _disablePaste
1035
     * @param {String} [selector=this.options.pasteSelector] jquery selector
1036
     * @private
1037
     */
1038
    _disablePaste: function _disablePaste(selector) {
1039
        var sel = typeof selector === 'undefined'
2✔
1040
            ? this.options.pasteSelector
1041
            : selector + ' ' + this.options.pasteSelector;
1042
        var dropdownSel = '.js-cms-pagetree-actions-dropdown';
2✔
1043

1044
        if (typeof selector !== 'undefined') {
2✔
1045
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1046
        }
1047

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

1052
        // not loaded actions dropdown have to be updated as well
1053
        $(dropdownSel).removeData('lazyUrlData');
2✔
1054
    },
1055

1056
    /**
1057
     * Updates the current state of the helpers after `after_open.jstree`
1058
     * or `_cutOrCopy` is triggered.
1059
     *
1060
     * @method _updatePasteHelpersState
1061
     * @private
1062
     */
1063
    _updatePasteHelpersState: function _updatePasteHelpersState() {
1064
        var that = this;
4✔
1065

1066
        if (this.clipboard.type && this.clipboard.id) {
4✔
1067
            this._enablePaste();
3✔
1068
        }
1069

1070
        // hide cut element and it's descendants' paste helpers if it is visible
1071
        if (
4✔
1072
            this.clipboard.type === 'cut' &&
8✔
1073
            this.clipboard.origin &&
1074
            this.options.site === this.clipboard.source_site
1075
        ) {
1076
            var descendantIds = this._getDescendantsIds(this.clipboard.id);
2✔
1077
            var nodes = [this.clipboard.id];
2✔
1078

1079
            if (descendantIds && descendantIds.length) {
2✔
1080
                nodes = nodes.concat(descendantIds);
1✔
1081
            }
1082

1083
            nodes.forEach(function(id) {
2✔
1084
                that._disablePaste('.jsgrid_' + id + '_col');
4✔
1085
            });
1086
        }
1087
    },
1088

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

1099
        element.addClass('cms-tree-node-success');
×
1100
        setTimeout(function() {
×
1101
            element.removeClass('cms-tree-node-success');
×
1102
        }, this.successTimer);
1103
        // hide elements
1104
        this._disablePaste();
×
1105
        this.clipboard.id = null;
×
1106
    },
1107

1108
    /**
1109
     * Checks if we should reload the iframe or entire window. For this we
1110
     * need to skip `CMS.API.Helpers.reloadBrowser();`.
1111
     *
1112
     * @method _reloadHelper
1113
     * @private
1114
     */
1115
    _reloadHelper: function _reloadHelper() {
1116
        if (window.self === window.top) {
×
1117
            Helpers.reloadBrowser();
×
1118
        } else {
1119
            window.location.reload();
×
1120
        }
1121
    },
1122

1123
    /**
1124
     * Displays an error within the django UI.
1125
     *
1126
     * @method showError
1127
     * @param {String} message string message to display
1128
     */
1129
    showError: function showError(message) {
1130
        var messages = $('.messagelist');
×
1131
        var breadcrumb = $('.breadcrumbs');
×
1132
        var reload = this.options.lang.reload;
×
1133
        var tpl =
1134
            '' +
×
1135
            '<ul class="messagelist">' +
1136
            '   <li class="error">' +
1137
            '       {msg} ' +
1138
            '       <a href="#reload" class="cms-tree-reload"> ' +
1139
            reload +
1140
            ' </a>' +
1141
            '   </li>' +
1142
            '</ul>';
1143
        var msg = tpl.replace('{msg}', '<strong>' + this.options.lang.error + '</strong> ' + message);
×
1144

1145
        if (messages.length) {
×
1146
            messages.replaceWith(msg);
×
1147
        } else {
1148
            breadcrumb.after(msg);
×
1149
        }
1150
    },
1151

1152
    /**
1153
     * @method _getDescendantsIds
1154
     * @private
1155
     * @param {String} nodeId jstree id of the node, e.g. j1_3
1156
     * @returns {String[]} array of ids
1157
     */
1158
    _getDescendantsIds: function _getDescendantsIds(nodeId) {
1159
        return this.ui.tree.jstree(true).get_node(nodeId).children_d;
2✔
1160
    },
1161

1162
    /**
1163
     * @method _hasPermision
1164
     * @private
1165
     * @param {Object} node jstree node
1166
     * @param {String} permission move / add
1167
     * @returns {Boolean}
1168
     */
1169
    _hasPermission: function _hasPermision(node, permission) {
1170
        if (node.id === '#' && permission === 'add') {
×
1171
            return this.options.hasAddRootPermission;
×
1172
        } else if (node.id === '#') {
×
1173
            return false;
×
1174
        }
1175

1176
        return node.li_attr['data-' + permission + '-permission'] === 'true';
×
1177
    }
1178
});
1179

1180
PageTree._init = function() {
1✔
1181
    new PageTree();
1✔
1182
};
1183

1184
// shorthand for jQuery(document).ready();
1185
$(function() {
1✔
1186
    // load cms settings beforehand
1187
    // have to set toolbar to "expanded" by default
1188
    // otherwise initialization will be incorrect when you
1189
    // go first to pages and then to normal page
1190
    window.CMS.config = {
1✔
1191
        isPageTree: true,
1192
        settings: {
1193
            toolbar: 'expanded',
1194
            version: __CMS_VERSION__
1195
        },
1196
        urls: {
1197
            settings: $('.js-cms-pagetree').data('settings-url')
1198
        }
1199
    };
1200
    window.CMS.settings = Helpers.getSettings();
1✔
1201
    // autoload the pagetree
1202
    CMS.PageTree._init();
1✔
1203
});
1204

1205
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