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

divio / django-cms / #29584

05 Apr 2025 05:35AM UTC coverage: 76.981% (+0.6%) from 76.342%
#29584

push

travis-ci

web-flow
Merge 7cb11e69a into 7cb72c06b

1081 of 1571 branches covered (68.81%)

2555 of 3319 relevant lines covered (76.98%)

32.66 hits per line

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

36.57
/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
import { debounce, without } from 'lodash';
12

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

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

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

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

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

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

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

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

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

75
        this.ui = {
21✔
76
            container: pagetree,
77
            document: $(document),
78
            tree: pagetree.find('.js-cms-pagetree'),
79
            dialog: $('.js-cms-tree-dialog'),
80
            siteForm: $('.js-cms-pagetree-site-form')
81
        };
82
    },
83

84
    /**
85
     * Setting up the jstree and the related columns.
86
     *
87
     * @method _setup
88
     * @private
89
     */
90
    _setup: function _setup() {
91
        var that = this;
20✔
92
        var columns = [];
20✔
93
        var obj = {
20✔
94
            language: this.options.lang.code,
95
            openNodes: []
96
        };
97
        var data = false;
20✔
98

99
        // setup column headings
100
        // eslint-disable-next-line no-shadow
101
        $.each(this.options.columns, function(index, obj) {
20✔
102
            if (obj.key === '') {
200✔
103
                // the first row is already populated, to avoid overwrites
104
                // just leave the "key" param empty
105
                columns.push({
20✔
106
                    wideValueClass: obj.wideValueClass,
107
                    wideValueClassPrefix: obj.wideValueClassPrefix,
108
                    header: obj.title,
109
                    width: obj.width || '1%',
20!
110
                    wideCellClass: obj.cls
111
                });
112
            } else {
113
                columns.push({
180✔
114
                    wideValueClass: obj.wideValueClass,
115
                    wideValueClassPrefix: obj.wideValueClassPrefix,
116
                    header: obj.title,
117
                    value: function(node) {
118
                        // it needs to have the "colde" format and not "col-de"
119
                        // as jstree will convert "col-de" to "colde"
120
                        // also we strip dashes, in case language code contains it
121
                        // e.g. zh-hans, zh-cn etc
122
                        if (node.data) {
×
123
                            return node.data['col' + obj.key.replace('-', '')];
×
124
                        }
125

126
                        return '';
×
127
                    },
128
                    width: obj.width || '1%',
360✔
129
                    wideCellClass: obj.cls
130
                });
131
            }
132
        });
133

134
        // prepare data
135
        if (!this.options.filtered) {
20!
136
            data = {
20✔
137
                url: this.options.urls.tree,
138
                cache: false,
139
                data: function(node) {
140
                    // '#' is rendered if its the root node, there we only
141
                    // care about `obj.openNodes`, in the following case
142
                    // we are requesting a specific node
143
                    if (node.id === '#') {
20!
144
                        obj.nodeId = null;
20✔
145
                    } else {
146
                        obj.nodeId = that._storeNodeId(node.data.nodeId);
×
147
                    }
148

149
                    // we need to store the opened items inside the localstorage
150
                    // as we have to load the pagetree with the previous opened
151
                    // state
152
                    obj.openNodes = that._getStoredNodeIds();
20✔
153

154
                    // we need to set the site id to get the correct tree
155
                    obj.site = that.options.site;
20✔
156

157
                    return obj;
20✔
158
                }
159
            };
160
        }
161

162
        // bind options to the jstree instance
163
        this.ui.tree.jstree({
20✔
164
            core: {
165
                // disable open/close animations
166
                animation: 0,
167
                // core setting to allow actions
168
                // eslint-disable-next-line max-params
169
                check_callback: function(operation, node, node_parent, node_position, more) {
170
                    if ((operation === 'move_node' || operation === 'copy_node') && more && more.pos) {
×
171
                        if (more.pos === 'i') {
×
172
                            $('#jstree-marker').addClass('jstree-marker-child');
×
173
                        } else {
174
                            $('#jstree-marker').removeClass('jstree-marker-child');
×
175
                        }
176
                    }
177

178
                    return that._hasPermission(node_parent, 'add');
×
179
                },
180
                // https://www.jstree.com/api/#/?f=$.jstree.defaults.core.data
181
                data: data,
182
                // strings used within jstree that are called using `get_string`
183
                strings: {
184
                    'Loading ...': this.options.lang.loading,
185
                    'New node': this.options.lang.newNode,
186
                    nodes: this.options.lang.nodes
187
                },
188
                error: function(error) {
189
                    // ignore warnings about dragging parent into child
190
                    var errorData = JSON.parse(error.data);
×
191

192
                    if (error.error === 'check' && errorData && errorData.chk === 'move_node') {
×
193
                        return;
×
194
                    }
195
                    that.showError(error.reason);
×
196
                },
197
                themes: {
198
                    name: 'django-cms'
199
                },
200
                // disable the multi selection of nodes for now
201
                multiple: false
202
            },
203
            // activate drag and drop plugin
204
            plugins: ['dnd', 'search', 'grid'],
205
            // https://www.jstree.com/api/#/?f=$.jstree.defaults.dnd
206
            dnd: {
207
                inside_pos: 'last',
208
                // disable the multi selection of nodes for now
209
                drag_selection: false,
210
                // disable dragging if filtered
211
                is_draggable: function(nodes) {
212
                    return that._hasPermission(nodes[0], 'move') && !that.options.filtered;
×
213
                },
214
                large_drop_target: true,
215
                copy: true,
216
                touch: 'selected'
217
            },
218
            // https://github.com/deitch/jstree-grid
219
            grid: {
220
                // columns are provided from base.html options
221
                width: '100%',
222
                columns: columns
223
            }
224
        });
225
    },
226

227
    /**
228
     * Sets up all the event handlers, such as opening and moving.
229
     *
230
     * @method _events
231
     * @private
232
     */
233
    _events: function _events() {
234
        var that = this;
21✔
235

236
        // set events for the nodeId updates
237
        this.ui.tree.on('after_close.jstree', function(e, el) {
21✔
238
            that._removeNodeId(el.node.data.nodeId);
×
239
        });
240

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

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

252
        this.ui.document.on('keydown.pagetree.alt-mode', function(e) {
21✔
253
            if (e.keyCode === KEYS.SHIFT) {
466!
254
                that.ui.container.addClass('cms-pagetree-alt-mode');
×
255
            }
256
        });
257

258
        this.ui.document.on('keyup.pagetree.alt-mode', function(e) {
21✔
259
            if (e.keyCode === KEYS.SHIFT) {
463!
260
                that.ui.container.removeClass('cms-pagetree-alt-mode');
×
261
            }
262
        });
263

264
        $(window)
21✔
265
            .on(
266
                'mousemove.pagetree.alt-mode',
267
                debounce(function(e) {
268
                    if (e.shiftKey) {
1!
269
                        that.ui.container.addClass('cms-pagetree-alt-mode');
×
270
                    } else {
271
                        that.ui.container.removeClass('cms-pagetree-alt-mode');
1✔
272
                    }
273
                }, 200) // eslint-disable-line no-magic-numbers
274
            )
275
            .on('blur.cms', () => {
276
                that.ui.container.removeClass('cms-pagetree-alt-mode');
21✔
277
            });
278

279
        this.ui.document.on('dnd_start.vakata', function(e, data) {
21✔
280
            var element = $(data.element);
×
281
            var node = element.parent();
×
282

283
            that._dropdowns.closeAllDropdowns();
×
284

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

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

294
            if (!node.hasClass('jstree-leaf')) {
×
295
                data.helper.addClass('is-stacked');
×
296
            }
297
        });
298

299
        var isCopyClassAdded = false;
21✔
300

301
        this.ui.document.on('dnd_move.vakata', function(e, data) {
21✔
302
            var isMovingCopy =
303
                data.data.origin &&
×
304
                (data.data.origin.settings.dnd.always_copy ||
305
                    (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)));
306

307
            if (isMovingCopy) {
×
308
                if (!isCopyClassAdded) {
×
309
                    $('.jstree-is-dragging').addClass('jstree-is-dragging-copy');
×
310
                    isCopyClassAdded = true;
×
311
                }
312
            } else if (isCopyClassAdded) {
×
313
                $('.jstree-is-dragging').removeClass('jstree-is-dragging-copy');
×
314
                isCopyClassAdded = false;
×
315
            }
316

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

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

328
                var hover = ins.settings.dnd.large_drop_target ?
×
329
                                $(data.event.target)
330
                                    .closest('.jstree-node') :
331
                                $(data.event.target)
332
                                    .closest('.jstree-anchor').parent();
333

334
                var width = root.width() - (column.width() - hover.width());
×
335

336
                marker.css({
×
337
                    left: `${root.offset().left}px`,
338
                    width: `${width}px`
339
                });
340
            }
341
        });
342

343
        this.ui.document.on('dnd_stop.vakata', function(e, data) {
21✔
344
            var element = $(data.element);
×
345
            var node = element.parent();
×
346

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

351
                [nodeId].concat(descendantIds).forEach(function(id) {
×
352
                    $('.jsgrid_' + id + '_col').removeClass('jstree-is-dragging jstree-is-dragging-copy');
×
353
                });
354
            });
355
        });
356

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

363
                    instance._hide_grid(instance.get_node(obj.parent));
×
364
                    if (obj.parent === '#' || (obj.node && obj.node.data && obj.node.data.isHome)) {
×
365
                        instance.refresh();
×
366
                    } else {
367
                        // have to refresh parent, because refresh only
368
                        // refreshes children of the node, never the node itself
369
                        instance.refresh_node(obj.parent);
×
370
                    }
371
                });
372
            } else {
373
                that._copyNode(obj);
×
374
            }
375
            // we need to open the parent node if we trigger an element
376
            // if not already opened
377
            that.ui.tree.jstree('open_node', obj.parent);
×
378
        });
379

380
        // set event for cut and paste
381
        this.ui.container.on(this.click, '.js-cms-tree-item-cut', function(e) {
21✔
382
            e.preventDefault();
×
383
            that._cutOrCopy({ type: 'cut', element: $(this) });
×
384
        });
385

386
        // set event for cut and paste
387
        this.ui.container.on(this.click, '.js-cms-tree-item-copy', function(e) {
21✔
388
            e.preventDefault();
×
389
            that._cutOrCopy({ type: 'copy', element: $(this) });
×
390
        });
391

392
        // attach events to paste
393
        this.ui.container.on(this.click, this.options.pasteSelector, function(e) {
21✔
394
            e.preventDefault();
×
395
            if ($(this).hasClass('cms-pagetree-dropdown-item-disabled')) {
×
396
                return;
×
397
            }
398
            that._paste(e);
×
399
        });
400

401
        // advanced settings link handling
402
        this.ui.container.on(this.click, '.js-cms-tree-advanced-settings', function(e) {
21✔
403
            if (e.shiftKey) {
×
404
                e.preventDefault();
×
405
                var link = $(this);
×
406

407
                if (link.data('url')) {
×
408
                    window.location.href = link.data('url');
×
409
                }
410
            }
411
        });
412

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

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

419
            this._storeNodeId(nodeData.data.id);
×
420
        });
421

422
        // add events for error reload (messagelist)
423
        this.ui.document.on(this.click, '.messagelist .cms-tree-reload', function(e) {
21✔
424
            e.preventDefault();
×
425
            that._reloadHelper();
×
426
        });
427

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

433
            // prevent if parent is active
434
            if (el.parent().hasClass('active')) {
×
435
                return false;
×
436
            }
437
            that.ui.siteForm.find('select').val(el.data().id).end().submit();
×
438
        });
439

440
        // additional event handlers
441
        this._setupDropdowns();
21✔
442
        this._setupSearch();
21✔
443

444
        // make sure ajax post requests are working
445
        this._setAjaxPost('.js-cms-tree-item-menu a');
21✔
446
        this._setAjaxPost('.js-cms-tree-lang-trigger');
21✔
447
        this._setAjaxPost('.js-cms-tree-item-set-home a');
21✔
448

449
        this._setupPageView();
21✔
450
        this._setupStickyHeader();
21✔
451

452
        this.ui.tree.on('ready.jstree', () => this._getClipboard());
21✔
453
    },
454

455
    _getClipboard: function _getClipboard() {
456
        this.clipboard = CMS.settings.pageClipboard || this.clipboard;
1✔
457

458
        if (this.clipboard.type && this.clipboard.origin) {
1!
459
            this._enablePaste();
×
460
            this._updatePasteHelpersState();
×
461
        }
462
    },
463

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

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

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

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

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

519
        if (this.clipboard.source_site === this.options.site) {
5!
520
            if (this.clipboard.type === 'cut') {
×
521
                this.ui.tree.jstree('cut', copyFromId);
×
522
            } else {
523
                this.ui.tree.jstree('copy', copyFromId);
×
524
            }
525

526
            this.clipboard.isPasting = true;
×
527
            this.ui.tree.jstree('paste', copyToId, 'last');
×
528
        } else {
529
            const dummyId = this.ui.tree.jstree('create_node', copyToId, 'Loading', 'last');
5✔
530

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

545
        this.clipboard.id = null;
5✔
546
        this.clipboard.type = null;
5✔
547
        this.clipboard.origin = null;
5✔
548
        this.clipboard.isPasting = false;
5✔
549
        CMS.settings.pageClipboard = this.clipboard;
5✔
550
        Helpers.setSettings(CMS.settings);
5✔
551
    },
552

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

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

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

581
        CMS.settings.pagetree = storage;
×
582
        Helpers.setSettings(CMS.settings);
×
583

584
        return number;
×
585
    },
586

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

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

605
                if (!node || !node.data) {
×
606
                    return node;
×
607
                }
608

609
                return node.data.nodeId;
×
610
            })
611
        );
612

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

615
        CMS.settings.pagetree = storage;
×
616
        Helpers.setSettings(CMS.settings);
×
617

618
        return id;
×
619
    },
620

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

635
        if (!obj.id && this.clipboard.type === 'cut' && this.clipboard.origin) {
×
636
            obj.id = this.clipboard.origin;
×
637
            obj.source_site = this.clipboard.source_site;
×
638
        } else {
639
            obj.site = that.options.site;
×
640
        }
641

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

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

670
        if (obj) {
×
671
            node = that._getNodePosition(obj);
×
672
        }
673

674
        var data = {
×
675
            // obj.original.data.id is for drag copy
676
            id: this.clipboard.origin || obj.original.data.id,
×
677
            position: node.position
678
        };
679

680
        if (this.clipboard.source_site) {
×
681
            data.source_site = this.clipboard.source_site;
×
682
        } else {
683
            data.source_site = this.options.site;
×
684
        }
685

686
        // if there is no target provided, the node lands in root
687
        if (node.target) {
×
688
            data.target = node.target;
×
689
        }
690

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

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

724
                    submitButton.prop('disabled', true);
×
725

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

731
                    that._saveCopiedNode(data);
×
732
                });
733
        } else {
734
            this._saveCopiedNode(data);
×
735
        }
736
    },
737

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

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

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

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

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

794
        data.position = obj.position;
×
795

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

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

808
        return data;
×
809
    },
810

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

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

835
        this.ui.container.on(this.click, '.js-cms-pagetree-page-view', function() {
25✔
836
            // check if the CM is running inside iframe or not. For Cypress tests
837
            // this might not always be the case.
838
            if (parent.CMS && parent.CMS.API && parent.CMS.API.Helpers) {
3!
839
                parent.CMS.API.Helpers.setSettings(
3✔
840
                    $.extend(true, {}, CMS.settings, {
841
                        sideframe: {
842
                            url: null,
843
                            hidden: true
844
                        }
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

884
            try {
×
885
                window.top.CMS.API.Toolbar.showLoader();
×
886
            } catch (err) {}
887

888
            $.ajax({
×
889
                method: 'post',
890
                url: $(this).attr('href')
891
            })
892
                .done(function() {
893
                    try {
×
894
                        window.top.CMS.API.Toolbar.hideLoader();
×
895
                    } catch (err) {}
896

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

916
                        Helpers.reloadBrowser('REFRESH_PAGE', false, true, data);
×
917
                    }
918
                })
919
                .fail(function(error) {
920
                    that.showError(error.statusText);
×
921
                });
922
        });
923
    },
924

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

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

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

945
        var searchContainer = this.ui.container.find('.cms-pagetree-header-filter');
21✔
946
        var searchField = searchContainer.find('#field-searchbar');
21✔
947
        var timeout = 200;
21✔
948

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

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

986
        // prevent closing when on filter container
987
        filterContainer.on('click', function(e) {
21✔
988
            e.stopImmediatePropagation();
×
989
        });
990

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

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

1008
        if (typeof selector !== 'undefined') {
2✔
1009
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1010
        }
1011

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

1016
        var data = {};
2✔
1017

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

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

1040
        if (typeof selector !== 'undefined') {
2✔
1041
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
1042
        }
1043

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

1048
        // not loaded actions dropdown have to be updated as well
1049
        $(dropdownSel).removeData('lazyUrlData');
2✔
1050
    },
1051

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

1062
        if (this.clipboard.type && this.clipboard.id) {
4✔
1063
            this._enablePaste();
3✔
1064
        }
1065

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

1075
            if (descendantIds && descendantIds.length) {
2✔
1076
                nodes = nodes.concat(descendantIds);
1✔
1077
            }
1078

1079
            nodes.forEach(function(id) {
2✔
1080
                that._disablePaste('.jsgrid_' + id + '_col');
4✔
1081
            });
1082
        }
1083
    },
1084

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

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

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

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

1141
        if (messages.length) {
×
1142
            messages.replaceWith(msg);
×
1143
        } else {
1144
            breadcrumb.after(msg);
×
1145
        }
1146
    },
1147

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

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

1172
        return node.li_attr['data-' + permission + '-permission'] === 'true';
×
1173
    }
1174
});
1175

1176
PageTree._init = function() {
1✔
1177
    new PageTree();
1✔
1178
};
1179

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

1201
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