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

divio / django-cms / #29546

24 Nov 2022 11:39AM UTC coverage: 77.399%. Remained the same
#29546

push

travis-ci

web-flow
build: bump loader-utils from 1.4.0 to 1.4.2 (#7435)

Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Fabian Braun <fsbraun@gmx.de>

1074 of 1547 branches covered (69.42%)

2565 of 3314 relevant lines covered (77.4%)

33.05 hits per line

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

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

318
        this.ui.document.on('dnd_stop.vakata', function(e, data) {
21✔
319
            var element = $(data.element);
×
320
            var node = element.parent();
×
321

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

326
                [nodeId].concat(descendantIds).forEach(function(id) {
×
327
                    $('.jsgrid_' + id + '_col').removeClass('jstree-is-dragging jstree-is-dragging-copy');
×
328
                });
329
            });
330
        });
331

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

338
                    instance._hide_grid(instance.get_node(obj.parent));
×
339
                    if (obj.parent === '#' || (obj.node && obj.node.data && obj.node.data.isHome)) {
×
340
                        instance.refresh();
×
341
                    } else {
342
                        // have to refresh parent, because refresh only
343
                        // refreshes children of the node, never the node itself
344
                        instance.refresh_node(obj.parent);
×
345
                    }
346
                });
347
            } else {
348
                that._copyNode(obj);
×
349
            }
350
            // we need to open the parent node if we trigger an element
351
            // if not already opened
352
            that.ui.tree.jstree('open_node', obj.parent);
×
353
        });
354

355
        // set event for cut and paste
356
        this.ui.container.on(this.click, '.js-cms-tree-item-cut', function(e) {
21✔
357
            e.preventDefault();
×
358
            that._cutOrCopy({ type: 'cut', element: $(this) });
×
359
        });
360

361
        // set event for cut and paste
362
        this.ui.container.on(this.click, '.js-cms-tree-item-copy', function(e) {
21✔
363
            e.preventDefault();
×
364
            that._cutOrCopy({ type: 'copy', element: $(this) });
×
365
        });
366

367
        // attach events to paste
368
        this.ui.container.on(this.click, this.options.pasteSelector, function(e) {
21✔
369
            e.preventDefault();
×
370
            if ($(this).hasClass('cms-pagetree-dropdown-item-disabled')) {
×
371
                return;
×
372
            }
373
            that._paste(e);
×
374
        });
375

376
        // advanced settings link handling
377
        this.ui.container.on(this.click, '.js-cms-tree-advanced-settings', function(e) {
21✔
378
            if (e.shiftKey) {
×
379
                e.preventDefault();
×
380
                var link = $(this);
×
381

382
                if (link.data('url')) {
×
383
                    window.location.href = link.data('url');
×
384
                }
385
            }
386
        });
387

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

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

394
            this._storeNodeId(nodeData.data.id);
×
395
        });
396

397
        // add events for error reload (messagelist)
398
        this.ui.document.on(this.click, '.messagelist .cms-tree-reload', function(e) {
21✔
399
            e.preventDefault();
×
400
            that._reloadHelper();
×
401
        });
402

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

408
            // prevent if parent is active
409
            if (el.parent().hasClass('active')) {
×
410
                return false;
×
411
            }
412
            that.ui.siteForm.find('select').val(el.data().id).end().submit();
×
413
        });
414

415
        // additional event handlers
416
        this._setupDropdowns();
21✔
417
        this._setupSearch();
21✔
418

419
        // make sure ajax post requests are working
420
        this._setAjaxPost('.js-cms-tree-item-menu a');
21✔
421
        this._setAjaxPost('.js-cms-tree-lang-trigger');
21✔
422
        this._setAjaxPost('.js-cms-tree-item-set-home a');
21✔
423

424
        this._setupPageView();
21✔
425
        this._setupStickyHeader();
21✔
426

427
        this.ui.tree.on('ready.jstree', () => this._getClipboard());
21✔
428
    },
429

430
    _getClipboard: function _getClipboard() {
431
        this.clipboard = CMS.settings.pageClipboard || this.clipboard;
1✔
432

433
        if (this.clipboard.type && this.clipboard.origin) {
1!
434
            this._enablePaste();
×
435
            this._updatePasteHelpersState();
×
436
        }
437
    },
438

439
    /**
440
     * Helper to process the cut and copy events.
441
     *
442
     * @method _cutOrCopy
443
     * @param {Object} [obj]
444
     * @param {Number} [obj.type] either 'cut' or 'copy'
445
     * @param {Number} [obj.element] originated trigger element
446
     * @private
447
     * @returns {Boolean|void}
448
     */
449
    _cutOrCopy: function _cutOrCopy(obj) {
450
        // prevent actions if you try to copy a page with an apphook
451
        if (obj.type === 'copy' && obj.element.data().apphook) {
×
452
            this.showError(this.options.lang.apphook);
×
453
            return false;
×
454
        }
455

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

458
        // resets if we click again
459
        if (this.clipboard.type === obj.type && jsTreeId === this.clipboard.id) {
×
460
            this.clipboard.type = null;
×
461
            this.clipboard.id = null;
×
462
            this.clipboard.origin = null;
×
463
            this.clipboard.source_site = null;
×
464
            this._disablePaste();
×
465
        } else {
466
            this.clipboard.origin = obj.element.data().id; // this._getNodeId(obj.element);
×
467
            this.clipboard.type = obj.type;
×
468
            this.clipboard.id = jsTreeId;
×
469
            this.clipboard.source_site = this.options.site;
×
470
            this._updatePasteHelpersState();
×
471
        }
472
        if (this.clipboard.type === 'copy' || !this.clipboard.type) {
×
473
            CMS.settings.pageClipboard = this.clipboard;
×
474
            Helpers.setSettings(CMS.settings);
×
475
        }
476
    },
477

478
    /**
479
     * Helper to process the paste event.
480
     *
481
     * @method _paste
482
     * @param {$.Event} event click event
483
     * @private
484
     */
485
    _paste: function _paste(event) {
486
        // hide helpers after we picked one
487
        this._disablePaste();
5✔
488

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

494
        if (this.clipboard.source_site === this.options.site) {
5!
495
            if (this.clipboard.type === 'cut') {
×
496
                this.ui.tree.jstree('cut', copyFromId);
×
497
            } else {
498
                this.ui.tree.jstree('copy', copyFromId);
×
499
            }
500

501
            this.clipboard.isPasting = true;
×
502
            this.ui.tree.jstree('paste', copyToId, 'last');
×
503
        } else {
504
            const dummyId = this.ui.tree.jstree('create_node', copyToId, 'Loading', 'last');
5✔
505

506
            if (this.ui.tree.length) {
5!
507
                this.ui.tree.jstree('cut', dummyId);
5✔
508
                this.clipboard.isPasting = true;
5✔
509
                this.ui.tree.jstree('paste', copyToId, 'last');
5✔
510
            } else {
511
                if (this.clipboard.type === 'copy') {
×
512
                    this._copyNode();
×
513
                }
514
                if (this.clipboard.type === 'cut') {
×
515
                    this._moveNode();
×
516
                }
517
            }
518
        }
519

520
        this.clipboard.id = null;
5✔
521
        this.clipboard.type = null;
5✔
522
        this.clipboard.origin = null;
5✔
523
        this.clipboard.isPasting = false;
5✔
524
        CMS.settings.pageClipboard = this.clipboard;
5✔
525
        Helpers.setSettings(CMS.settings);
5✔
526
    },
527

528
    /**
529
     * Retreives a list of nodes from local storage.
530
     *
531
     * @method _getStoredNodeIds
532
     * @private
533
     * @returns {Array} list of ids
534
     */
535
    _getStoredNodeIds: function _getStoredNodeIds() {
536
        return CMS.settings.pagetree || [];
20✔
537
    },
538

539
    /**
540
     * Stores a node in local storage.
541
     *
542
     * @method _storeNodeId
543
     * @private
544
     * @param {String} id to be stored
545
     * @returns {String} id that has been stored
546
     */
547
    _storeNodeId: function _storeNodeId(id) {
548
        var number = id;
×
549
        var storage = this._getStoredNodeIds();
×
550

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

556
        CMS.settings.pagetree = storage;
×
557
        Helpers.setSettings(CMS.settings);
×
558

559
        return number;
×
560
    },
561

562
    /**
563
     * Removes a node in local storage.
564
     *
565
     * @method _removeNodeId
566
     * @private
567
     * @param {String} id to be stored
568
     * @returns {String} id that has been removed
569
     */
570
    _removeNodeId: function _removeNodeId(id) {
571
        const instance = this.ui.tree.jstree(true);
×
572
        const childrenIds = instance.get_node({
×
573
            id: CMS.$(`[data-node-id=${id}]`).attr('id')
574
        }).children_d;
575

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

580
                if (!node || !node.data) {
×
581
                    return node;
×
582
                }
583

584
                return node.data.nodeId;
×
585
            })
586
        );
587

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

590
        CMS.settings.pagetree = storage;
×
591
        Helpers.setSettings(CMS.settings);
×
592

593
        return id;
×
594
    },
595

596
    /**
597
     * Moves a node after drag & drop.
598
     *
599
     * @method _moveNode
600
     * @param {Object} [obj]
601
     * @param {Number} [obj.id] current element id for url matching
602
     * @param {Number} [obj.target] target sibling or parent
603
     * @param {Number} [obj.position] either `left`, `right` or `last-child`
604
     * @returns {$.Deferred} ajax request object
605
     * @private
606
     */
607
    _moveNode: function _moveNode(obj) {
608
        var that = this;
×
609

610
        if (!obj.id && this.clipboard.type === 'cut' && this.clipboard.origin) {
×
611
            obj.id = this.clipboard.origin;
×
612
            obj.source_site = this.clipboard.source_site;
×
613
        } else {
614
            obj.site = that.options.site;
×
615
        }
616

617
        return $.ajax({
×
618
            method: 'post',
619
            url: that.options.urls.move.replace('{id}', obj.id),
620
            data: obj
621
        })
622
            .done(function(r) {
623
                if (r.status && r.status === 400) { // eslint-disable-line
×
624
                    that.showError(r.content);
×
625
                } else {
626
                    that._showSuccess(obj.id);
×
627
                }
628
            })
629
            .fail(function(error) {
630
                that.showError(error.statusText);
×
631
            });
632
    },
633

634
    /**
635
     * Copies a node into the selected node.
636
     *
637
     * @method _copyNode
638
     * @param {Object} obj page obj
639
     * @private
640
     */
641
    _copyNode: function _copyNode(obj) {
642
        var that = this;
×
643
        var node = { position: 0 };
×
644

645
        if (obj) {
×
646
            node = that._getNodePosition(obj);
×
647
        }
648

649
        var data = {
×
650
            // obj.original.data.id is for drag copy
651
            id: this.clipboard.origin || obj.original.data.id,
×
652
            position: node.position
653
        };
654

655
        if (this.clipboard.source_site) {
×
656
            data.source_site = this.clipboard.source_site;
×
657
        } else {
658
            data.source_site = this.options.site;
×
659
        }
660

661
        // if there is no target provided, the node lands in root
662
        if (node.target) {
×
663
            data.target = node.target;
×
664
        }
665

666
        if (that.options.permission) {
×
667
            // we need to load a dialog first, to check if permissions should
668
            // be copied or not
669
            $.ajax({
×
670
                method: 'post',
671
                url: that.options.urls.copyPermission.replace('{id}', data.id),
672
                data: data
673
                // the dialog is loaded via the ajax respons originating from
674
                // `templates/admin/cms/page/tree/copy_premissions.html`
675
            })
676
                .done(function(dialog) {
677
                    that.ui.dialog.append(dialog);
×
678
                })
679
                .fail(function(error) {
680
                    that.showError(error.statusText);
×
681
                });
682

683
            // attach events to the permission dialog
684
            this.ui.dialog
×
685
                .off(this.click, '.cancel')
686
                .on(this.click, '.cancel', function(e) {
687
                    e.preventDefault();
×
688
                    // remove just copied node
689
                    that.ui.tree.jstree('delete_node', obj.node.id);
×
690
                    $('.js-cms-dialog').remove();
×
691
                    $('.js-cms-dialog-dimmer').remove();
×
692
                })
693
                .off(this.click, '.submit')
694
                .on(this.click, '.submit', function(e) {
695
                    e.preventDefault();
×
696
                    var submitButton = $(this);
×
697
                    var formData = submitButton.closest('form').serialize().split('&');
×
698

699
                    submitButton.prop('disabled', true);
×
700

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

706
                    that._saveCopiedNode(data);
×
707
                });
708
        } else {
709
            this._saveCopiedNode(data);
×
710
        }
711
    },
712

713
    /**
714
     * Sends the request to copy a node.
715
     *
716
     * @method _saveCopiedNode
717
     * @private
718
     * @param {Object} data node position information
719
     * @returns {$.Deferred}
720
     */
721
    _saveCopiedNode: function _saveCopiedNode(data) {
722
        var that = this;
×
723

724
        // send the real ajax request for copying the plugin
725
        return $.ajax({
×
726
            method: 'post',
727
            url: that.options.urls.copy.replace('{id}', data.id),
728
            data: data
729
        })
730
            .done(function(r) {
731
                if (r.status && r.status === 400) { // eslint-disable-line
×
732
                    that.showError(r.content);
×
733
                } else {
734
                    that._reloadHelper();
×
735
                }
736
            })
737
            .fail(function(error) {
738
                that.showError(error.statusText);
×
739
            });
740
    },
741

742
    /**
743
     * Returns element from any sub nodes.
744
     *
745
     * @method _getElement
746
     * @private
747
     * @param {jQuery} el jQuery node form where to search
748
     * @returns {String} jsTree node element id
749
     */
750
    _getNodeId: function _getNodeId(el) {
751
        var cls = el.closest('.jstree-grid-cell').attr('class');
2✔
752

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

757
    /**
758
     * Gets the new node position after moving.
759
     *
760
     * @method _getNodePosition
761
     * @private
762
     * @param {Object} obj jstree move object
763
     * @returns {Object} evaluated object with params
764
     */
765
    _getNodePosition: function _getNodePosition(obj) {
766
        var data = {};
×
767
        var node = this.ui.tree.jstree('get_node', obj.node.parent);
×
768

769
        data.position = obj.position;
×
770

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

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

783
        return data;
×
784
    },
785

786
    /**
787
     * Sets up general tooltips that can have a list of links or content.
788
     *
789
     * @method _setupDropdowns
790
     * @private
791
     */
792
    _setupDropdowns: function _setupDropdowns() {
793
        this._dropdowns = new PageTreeDropdowns({
21✔
794
            container: this.ui.container
795
        });
796
    },
797

798
    /**
799
     * Handles page view click. Usual use case is that after you click
800
     * on view page in the pagetree - sideframe is no longer needed,
801
     * so we close it.
802
     *
803
     * @method _setupPageView
804
     * @private
805
     */
806
    _setupPageView: function _setupPageView() {
807
        var win = Helpers._getWindow();
25✔
808
        var parent = win.parent ? win.parent : win;
25✔
809

810
        this.ui.container.on(this.click, '.js-cms-pagetree-page-view', function() {
25✔
811
            parent.CMS.API.Helpers.setSettings(
3✔
812
                $.extend(true, {}, CMS.settings, {
813
                    sideframe: {
814
                        url: null,
815
                        hidden: true
816
                    }
817
                })
818
            );
819
        });
820
    },
821

822
    /**
823
     * @method _setupStickyHeader
824
     * @private
825
     */
826
    _setupStickyHeader: function _setupStickyHeader() {
827
        var that = this;
21✔
828

829
        that.ui.tree.on('ready.jstree', function() {
21✔
830
            that.header = new PageTreeStickyHeader({
×
831
                container: that.ui.container
832
            });
833
        });
834
    },
835

836
    /**
837
     * Triggers the links `href` as ajax post request.
838
     *
839
     * @method _setAjaxPost
840
     * @private
841
     * @param {jQuery} trigger jQuery link target
842
     */
843
    _setAjaxPost: function _setAjaxPost(trigger) {
844
        var that = this;
63✔
845

846
        this.ui.container.on(this.click, trigger, function(e) {
63✔
847
            e.preventDefault();
×
848

849
            var element = $(this);
×
850

851
            if (element.closest('.cms-pagetree-dropdown-item-disabled').length) {
×
852
                return;
×
853
            }
854

855
            try {
×
856
                window.top.CMS.API.Toolbar.showLoader();
×
857
            } catch (err) {}
858

859
            $.ajax({
×
860
                method: 'post',
861
                url: $(this).attr('href')
862
            })
863
                .done(function() {
864
                    try {
×
865
                        window.top.CMS.API.Toolbar.hideLoader();
×
866
                    } catch (err) {}
867

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

887
                        Helpers.reloadBrowser('REFRESH_PAGE', false, true, data);
×
888
                    }
889
                })
890
                .fail(function(error) {
891
                    that.showError(error.statusText);
×
892
                });
893
        });
894
    },
895

896
    /**
897
     * Sets events for the search on the header.
898
     *
899
     * @method _setupSearch
900
     * @private
901
     */
902
    _setupSearch: function _setupSearch() {
903
        var that = this;
21✔
904
        var click = this.click + '.search';
21✔
905

906
        var filterActive = false;
21✔
907
        var filterTrigger = this.ui.container.find('.js-cms-pagetree-header-filter-trigger');
21✔
908
        var filterContainer = this.ui.container.find('.js-cms-pagetree-header-filter-container');
21✔
909
        var filterClose = filterContainer.find('.js-cms-pagetree-header-search-close');
21✔
910
        var filterClass = 'cms-pagetree-header-filter-active';
21✔
911
        var pageTreeHeader = $('.cms-pagetree-header');
21✔
912

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

916
        var searchContainer = this.ui.container.find('.cms-pagetree-header-filter');
21✔
917
        var searchField = searchContainer.find('#field-searchbar');
21✔
918
        var timeout = 200;
21✔
919

920
        // add active class when focusing the search field
921
        searchField.on('focus', function(e) {
21✔
922
            e.stopImmediatePropagation();
×
923
            pageTreeHeader.addClass(filterClass);
×
924
        });
925
        searchField.on('blur', function(e) {
21✔
926
            e.stopImmediatePropagation();
×
927
            // timeout is required to prevent the search field from jumping
928
            // between enlarging and shrinking
929
            setTimeout(function() {
×
930
                if (!filterActive) {
×
931
                    pageTreeHeader.removeClass(filterClass);
×
932
                }
933
            }, timeout);
934
            that.ui.document.off(click);
×
935
        });
936

937
        // shows/hides filter box
938
        filterTrigger.add(filterClose).on(click, function(e) {
21✔
939
            e.preventDefault();
×
940
            e.stopImmediatePropagation();
×
941
            if (filterActive) {
×
942
                filterContainer.hide();
×
943
                pageTreeHeader.removeClass(filterClass);
×
944
                that.ui.document.off(click);
×
945
                filterActive = false;
×
946
            } else {
947
                filterContainer.show();
×
948
                pageTreeHeader.addClass(filterClass);
×
949
                that.ui.document.on(click, function() {
×
950
                    filterActive = true;
×
951
                    filterTrigger.trigger(click);
×
952
                });
953
                filterActive = true;
×
954
            }
955
        });
956

957
        // prevent closing when on filter container
958
        filterContainer.on('click', function(e) {
21✔
959
            e.stopImmediatePropagation();
×
960
        });
961

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

966
    /**
967
     * Shows paste helpers.
968
     *
969
     * @method _enablePaste
970
     * @param {String} [selector=this.options.pasteSelector] jquery selector
971
     * @private
972
     */
973
    _enablePaste: function _enablePaste(selector) {
974
        var sel = typeof selector === 'undefined'
2✔
975
            ? this.options.pasteSelector
976
            : selector + ' ' + this.options.pasteSelector;
977
        var dropdownSel = '.js-cms-pagetree-actions-dropdown';
2✔
978

979
        if (typeof selector !== 'undefined') {
2✔
980
            dropdownSel = selector + ' .js-cms-pagetree-actions-dropdown';
1✔
981
        }
982

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

987
        var data = {};
2✔
988

989
        if (this.clipboard.type === 'cut') {
2!
990
            data.has_cut = true;
×
991
        } else {
992
            data.has_copy = true;
2✔
993
        }
994
        // not loaded actions dropdown have to be updated as well
995
        $(dropdownSel).data('lazyUrlData', data);
2✔
996
    },
997

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

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

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

1019
        // not loaded actions dropdown have to be updated as well
1020
        $(dropdownSel).removeData('lazyUrlData');
2✔
1021
    },
1022

1023
    /**
1024
     * Updates the current state of the helpers after `after_open.jstree`
1025
     * or `_cutOrCopy` is triggered.
1026
     *
1027
     * @method _updatePasteHelpersState
1028
     * @private
1029
     */
1030
    _updatePasteHelpersState: function _updatePasteHelpersState() {
1031
        var that = this;
4✔
1032

1033
        if (this.clipboard.type && this.clipboard.id) {
4✔
1034
            this._enablePaste();
3✔
1035
        }
1036

1037
        // hide cut element and it's descendants' paste helpers if it is visible
1038
        if (
4✔
1039
            this.clipboard.type === 'cut' &&
8✔
1040
            this.clipboard.origin &&
1041
            this.options.site === this.clipboard.source_site
1042
        ) {
1043
            var descendantIds = this._getDescendantsIds(this.clipboard.id);
2✔
1044
            var nodes = [this.clipboard.id];
2✔
1045

1046
            if (descendantIds && descendantIds.length) {
2✔
1047
                nodes = nodes.concat(descendantIds);
1✔
1048
            }
1049

1050
            nodes.forEach(function(id) {
2✔
1051
                that._disablePaste('.jsgrid_' + id + '_col');
4✔
1052
            });
1053
        }
1054
    },
1055

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

1066
        element.addClass('cms-tree-node-success');
×
1067
        setTimeout(function() {
×
1068
            element.removeClass('cms-tree-node-success');
×
1069
        }, this.successTimer);
1070
        // hide elements
1071
        this._disablePaste();
×
1072
        this.clipboard.id = null;
×
1073
    },
1074

1075
    /**
1076
     * Checks if we should reload the iframe or entire window. For this we
1077
     * need to skip `CMS.API.Helpers.reloadBrowser();`.
1078
     *
1079
     * @method _reloadHelper
1080
     * @private
1081
     */
1082
    _reloadHelper: function _reloadHelper() {
1083
        if (window.self === window.top) {
×
1084
            Helpers.reloadBrowser();
×
1085
        } else {
1086
            window.location.reload();
×
1087
        }
1088
    },
1089

1090
    /**
1091
     * Displays an error within the django UI.
1092
     *
1093
     * @method showError
1094
     * @param {String} message string message to display
1095
     */
1096
    showError: function showError(message) {
1097
        var messages = $('.messagelist');
×
1098
        var breadcrumb = $('.breadcrumbs');
×
1099
        var reload = this.options.lang.reload;
×
1100
        var tpl =
1101
            '' +
×
1102
            '<ul class="messagelist">' +
1103
            '   <li class="error">' +
1104
            '       {msg} ' +
1105
            '       <a href="#reload" class="cms-tree-reload"> ' +
1106
            reload +
1107
            ' </a>' +
1108
            '   </li>' +
1109
            '</ul>';
1110
        var msg = tpl.replace('{msg}', '<strong>' + this.options.lang.error + '</strong> ' + message);
×
1111

1112
        if (messages.length) {
×
1113
            messages.replaceWith(msg);
×
1114
        } else {
1115
            breadcrumb.after(msg);
×
1116
        }
1117
    },
1118

1119
    /**
1120
     * @method _getDescendantsIds
1121
     * @private
1122
     * @param {String} nodeId jstree id of the node, e.g. j1_3
1123
     * @returns {String[]} array of ids
1124
     */
1125
    _getDescendantsIds: function _getDescendantsIds(nodeId) {
1126
        return this.ui.tree.jstree(true).get_node(nodeId).children_d;
2✔
1127
    },
1128

1129
    /**
1130
     * @method _hasPermision
1131
     * @private
1132
     * @param {Object} node jstree node
1133
     * @param {String} permission move / add
1134
     * @returns {Boolean}
1135
     */
1136
    _hasPermission: function _hasPermision(node, permission) {
1137
        if (node.id === '#' && permission === 'add') {
×
1138
            return this.options.hasAddRootPermission;
×
1139
        } else if (node.id === '#') {
×
1140
            return false;
×
1141
        }
1142

1143
        return node.li_attr['data-' + permission + '-permission'] === 'true';
×
1144
    }
1145
});
1146

1147
PageTree._init = function() {
1✔
1148
    new PageTree();
1✔
1149
};
1150

1151
// shorthand for jQuery(document).ready();
1152
$(function() {
1✔
1153
    // load cms settings beforehand
1154
    // have to set toolbar to "expanded" by default
1155
    // otherwise initialization will be incorrect when you
1156
    // go first to pages and then to normal page
1157
    window.CMS.config = {
1✔
1158
        isPageTree: true,
1159
        settings: {
1160
            toolbar: 'expanded',
1161
            version: __CMS_VERSION__
1162
        },
1163
        urls: {
1164
            settings: $('.js-cms-pagetree').data('settings-url')
1165
        }
1166
    };
1167
    window.CMS.settings = Helpers.getSettings();
1✔
1168
    // autoload the pagetree
1169
    CMS.PageTree._init();
1✔
1170
});
1171

1172
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