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

divio / django-cms / #29287

10 Jan 2025 08:55PM UTC coverage: 76.168% (-1.4%) from 77.576%
#29287

push

travis-ci

web-flow
Merge 16bd3c1ee into ec268c7b2

1053 of 1568 branches covered (67.16%)

16 of 28 new or added lines in 2 files covered. (57.14%)

405 existing lines in 6 files now uncovered.

2528 of 3319 relevant lines covered (76.17%)

26.8 hits per line

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

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

5
import $ from 'jquery';
6
import keyboard from './keyboard';
7
import Plugin from './cms.plugins';
8
import { getPlaceholderIds } from './cms.toolbar';
9
import Clipboard from './cms.clipboard';
10
import { DiffDOM } from 'diff-dom';
11
import PreventParentScroll from 'prevent-parent-scroll';
12
import { find, findIndex, once, remove, compact, isEqual, zip, every } from 'lodash';
13
import ls from 'local-storage';
14

15
import './jquery.ui.custom';
16
import './jquery.ui.touchpunch';
17
import './jquery.ui.nestedsortable';
18

19
import measureScrollbar from './scrollbar';
20
import preloadImagesFromMarkup from './preload-images';
21

22
import { Helpers, KEYS } from './cms.base';
23
import { showLoader, hideLoader } from './loader';
24

25
let dd;
26
const DOMParser = window.DOMParser; // needed only for testing
1✔
27
const storageKey = 'cms-structure';
1✔
28

29
let placeholders;
30
let originalPluginContainer;
31

32
const triggerWindowResize = () => {
1✔
33
    try {
49✔
34
        var evt = document.createEvent('UIEvents');
49✔
35

36
        evt.initUIEvent('resize', true, false, window, 0);
49✔
37
        window.dispatchEvent(evt);
49✔
38
    } catch (e) {}
39
};
40

41
const arrayEquals = (a1, a2) => every(zip(a1, a2), ([a, b]) => a === b);
2✔
42

43
/**
44
 * Handles drag & drop, mode switching and collapsables.
45
 *
46
 * @class StructureBoard
47
 * @namespace CMS
48
 */
49
class StructureBoard {
50
    constructor() {
51
        // elements
52
        this._setupUI();
126✔
53

54
        // states
55
        this.click = 'click.cms.structure';
126✔
56
        this.keyUpAndDown = 'keyup.cms.structure keydown.cms.structure';
126✔
57
        this.pointerUp = 'pointerup.cms';
126✔
58
        this.state = false;
126✔
59
        this.dragging = false;
126✔
60
        this.latestAction = [];
126✔
61
        ls.remove(storageKey);
126✔
62

63
        dd = new DiffDOM();
126✔
64

65
        // setup initial stuff
66
        const setup = this._setup();
126✔
67

68
        // istanbul ignore if
69
        if (typeof setup === 'undefined' && CMS.config.mode === 'draft') {
126✔
70
            this._preloadOppositeMode();
71
        }
72
        this._setupModeSwitcher();
126✔
73
        this._events();
126✔
74
        StructureBoard.actualizePlaceholders();
126✔
75

76
        setTimeout(() => this.highlightPluginFromUrl(), 0);
126✔
77
        this._listenToExternalUpdates();
126✔
78
    }
79

80
    /**
81
     * Stores all jQuery references within `this.ui`.
82
     *
83
     * @method _setupUI
84
     * @private
85
     */
86
    _setupUI() {
87
        var container = $('.cms-structure');
126✔
88
        var toolbar = $('.cms-toolbar');
126✔
89

90
        this.ui = {
126✔
91
            container: container,
92
            content: $('.cms-structure-content'),
93
            doc: $(document),
94
            window: $(window),
95
            html: $('html'),
96
            toolbar: toolbar,
97
            sortables: $('.cms-draggables'), // global scope to include clipboard
98
            plugins: $('.cms-plugin'),
99
            render_model: $('.cms-render-model'),
100
            placeholders: $('.cms-placeholder'),
101
            dragitems: $('.cms-draggable'),
102
            dragareas: $('.cms-dragarea'),
103
            toolbarModeSwitcher: toolbar.find('.cms-toolbar-item-cms-mode-switcher'),
104
            toolbarModeLinks: toolbar.find('.cms-toolbar-item-cms-mode-switcher a')
105
        };
106

107
        this._preventScroll = new PreventParentScroll(this.ui.content[0]);
126✔
108
    }
109

110
    /**
111
     * Initial setup (and early bail if specific
112
     * elements do not exist).
113
     *
114
     * @method _setup
115
     * @private
116
     * @returns {Boolean|void}
117
     */
118
    _setup() {
119
        var that = this;
126✔
120

121
        // cancel if there is no structure / content switcher
122
        if (!this.ui.toolbarModeSwitcher.length) {
126✔
123
            return false;
19✔
124
        }
125

126
        // setup toolbar mode
127
        if (CMS.config.settings.mode === 'structure') {
107✔
128
            that.show({ init: true });
62✔
129
            that._loadedStructure = true;
62✔
130
            StructureBoard._initializeDragItemsStates();
62✔
131
        } else {
132
            // triggering hide here to switch proper classnames on switcher
133
            that.hide();
45✔
134
            that._loadedContent = true;
45✔
135
        }
136

137
        if (CMS.config.settings.legacy_mode) {
107✔
138
            that._loadedStructure = true;
1✔
139
            that._loadedContent = true;
1✔
140
        }
141

142
        // check if modes should be visible
143
        if (this.ui.dragareas.not('.cms-clipboard .cms-dragarea').length || this.ui.placeholders.length) {
107✔
144
            // eslint-disable-line
145
            this.ui.toolbarModeSwitcher.find('.cms-btn').removeClass('cms-btn-disabled');
105✔
146
        }
147

148
        // add drag & drop functionality
149
        // istanbul ignore next
150
        $('.cms-draggable:not(.cms-drag-disabled)').one(
151
            'pointerover.cms.drag',
152
            once(() => {
153
                $('.cms-draggable').off('pointerover.cms.drag');
154
                this._drag();
155
            })
156
        );
157
    }
158

159
    _preloadOppositeMode() {
160
        if (CMS.config.settings.legacy_mode) {
3✔
161
            return;
1✔
162
        }
163
        const WAIT_BEFORE_PRELOADING = 2000;
2✔
164

165
        $(Helpers._getWindow()).one('load', () => {
2✔
166
            setTimeout(() => {
2✔
167
                if (this._loadedStructure) {
2✔
168
                    this._requestMode('content');
1✔
169
                } else {
170
                    this._requestMode('structure');
1✔
171
                }
172
            }, WAIT_BEFORE_PRELOADING);
173
        });
174
    }
175

176
    _events() {
177
        this.ui.window.on('resize.cms.structureboard', () => {
126✔
178
            if (!this._loadedContent || CMS.config.mode !== 'draft') {
8,853!
179
                return;
8,853✔
180
            }
UNCOV
181
            const width = this.ui.window[0].innerWidth;
×
UNCOV
182
            const BREAKPOINT = 1024;
×
183

UNCOV
184
            if (width > BREAKPOINT && !this.condensed) {
×
185
                this._makeCondensed();
×
186
            }
187

UNCOV
188
            if (width <= BREAKPOINT && this.condensed) {
×
UNCOV
189
                this._makeFullWidth();
×
190
            }
191
        });
192
    }
193

194
    /**
195
     * Sets up events handlers for switching
196
     * structureboard modes.
197
     *
198
     * @method _setupModeSwitcher
199
     * @private
200
     */
201
    _setupModeSwitcher() {
202
        const modes = this.ui.toolbarModeLinks;
126✔
203
        let cmdPressed;
204

205
        $(Helpers._getWindow())
126✔
206
            .on(this.keyUpAndDown, e => {
207
                if (
×
208
                    e.keyCode === KEYS.CMD_LEFT ||
×
209
                    e.keyCode === KEYS.CMD_RIGHT ||
210
                    e.keyCode === KEYS.CMD_FIREFOX ||
211
                    e.keyCode === KEYS.SHIFT ||
212
                    e.keyCode === KEYS.CTRL
213
                ) {
214
                    cmdPressed = true;
×
215
                }
216
                if (e.type === 'keyup') {
×
217
                    cmdPressed = false;
×
218
                }
219
            })
220
            .on('blur', () => {
221
                cmdPressed = false;
×
222
            });
223

224
        // show edit mode
225
        modes.on(this.click, e => {
126✔
226
            e.preventDefault();
4✔
227
            e.stopImmediatePropagation();
4✔
228

229
            if (modes.hasClass('cms-btn-disabled')) {
4!
230
                return;
×
231
            }
232

233
            if (cmdPressed && e.type === 'click') {
4!
234
                // control the behaviour when ctrl/cmd is pressed
235
                Helpers._getWindow().open(modes.attr('href'), '_blank');
×
236
                return;
×
237
            }
238

239
            if (CMS.settings.mode === 'edit') {
4✔
240
                this.show();
2✔
241
            } else {
242
                this.hide();
2✔
243
            }
244
        });
245

246
        // keyboard handling
247
        // only if there is a structure / content switcher
248
        if (
126✔
249
            this.ui.toolbarModeSwitcher.length &&
233✔
250
            !this.ui.toolbarModeSwitcher.find('.cms-btn').is('.cms-btn-disabled')
251
        ) {
252
            keyboard.setContext('cms');
105✔
253
            keyboard.bind('space', e => {
105✔
254
                e.preventDefault();
1✔
255
                this._toggleStructureBoard();
1✔
256
            });
257
            keyboard.bind('shift+space', e => {
105✔
258
                e.preventDefault();
1✔
259
                this._toggleStructureBoard({ useHoveredPlugin: true });
1✔
260
            });
261
        }
262
    }
263

264
    /**
265
     * @method _toggleStructureBoard
266
     * @private
267
     * @param {Object} [options] options
268
     * @param {Boolean} [options.useHoveredPlugin] should the plugin be taken into account
269
     */
270
    _toggleStructureBoard(options = {}) {
2✔
271
        var that = this;
4✔
272

273
        if (options.useHoveredPlugin && CMS.settings.mode !== 'structure') {
4✔
274
            that._showAndHighlightPlugin(options.successTimeout).then($.noop, $.noop);
1✔
275
        } else if (!options.useHoveredPlugin) {
3✔
276
            // eslint-disable-next-line no-lonely-if
277
            if (CMS.settings.mode === 'structure') {
2✔
278
                that.hide();
1✔
279
            } else if (CMS.settings.mode === 'edit') {
1!
280
                /* istanbul ignore else */ that.show();
1✔
281
            }
282
        }
283
    }
284

285
    /**
286
     * Shows structureboard, scrolls into view and highlights hovered plugin.
287
     * Uses CMS.API.Tooltip because it already handles multiple plugins living on
288
     * the same DOM node.
289
     *
290
     * @method _showAndHighlightPlugin
291
     * @private
292
     * @returns {Promise}
293
     */
294
    // eslint-disable-next-line no-magic-numbers
295
    _showAndHighlightPlugin(successTimeout = 200, seeThrough = false) {
8✔
296
        // cancel show if live modus is active
297
        if (CMS.config.mode === 'live') {
4✔
298
            return Promise.resolve(false);
1✔
299
        }
300

301
        if (!CMS.API.Tooltip) {
3✔
302
            return Promise.resolve(false);
1✔
303
        }
304

305
        var tooltip = CMS.API.Tooltip.domElem;
2✔
306
        var HIGHLIGHT_TIMEOUT = 10;
2✔
307
        var DRAGGABLE_HEIGHT = 50; // it's not precisely 50, but it fits
2✔
308

309
        if (!tooltip.is(':visible')) {
2✔
310
            return Promise.resolve(false);
1✔
311
        }
312

313
        var pluginId = tooltip.data('plugin_id');
1✔
314

315
        return this.show().then(function() {
1✔
316
            var draggable = $('.cms-draggable-' + pluginId);
1✔
317
            var doc = $(document);
1✔
318
            var currentExpandmode = doc.data('expandmode');
1✔
319

320
            // expand necessary parents
321
            doc.data('expandmode', false);
1✔
322
            draggable
1✔
323
                .parents('.cms-draggable')
324
                .find('> .cms-dragitem-collapsable:not(".cms-dragitem-expanded") > .cms-dragitem-text')
325
                .each((i, el) => $(el).triggerHandler(Plugin.click));
×
326

327
            setTimeout(() => doc.data('expandmode', currentExpandmode));
1✔
328
            setTimeout(function() {
1✔
329
                var offsetParent = draggable.offsetParent();
1✔
330
                var position = draggable.position().top + offsetParent.scrollTop();
1✔
331

332
                draggable.offsetParent().scrollTop(position - window.innerHeight / 2 + DRAGGABLE_HEIGHT);
1✔
333

334
                Plugin._highlightPluginStructure(draggable.find('.cms-dragitem:first'), { successTimeout, seeThrough });
1✔
335
            }, HIGHLIGHT_TIMEOUT);
336
        });
337
    }
338

339
    /**
340
     * Shows the structureboard. (Structure mode)
341
     *
342
     * @method show
343
     * @public
344
     * @param {Boolean} init true if this is first initialization
345
     * @returns {Promise}
346
     */
347
    show({ init = false } = {}) {
88✔
348
        // cancel show if live modus is active
349
        if (CMS.config.mode === 'live') {
105✔
350
            return Promise.resolve(false);
1✔
351
        }
352

353
        // in order to get consistent positioning
354
        // of the toolbar we have to know if the page
355
        // had the scrollbar and if it had - we adjust
356
        // the toolbar positioning
357
        if (init) {
104✔
358
            var width = this.ui.toolbar.width();
61✔
359
            var scrollBarWidth = this.ui.window[0].innerWidth - width;
61✔
360

361
            if (!scrollBarWidth && init) {
61!
362
                scrollBarWidth = measureScrollbar();
61✔
363
            }
364

365
            if (scrollBarWidth) {
61!
366
                this.ui.toolbar.css('right', scrollBarWidth);
61✔
367
            }
368
        }
369
        // apply new settings
370
        CMS.settings.mode = 'structure';
104✔
371
        Helpers.setSettings(CMS.settings);
104✔
372

373
        return this._loadStructure().then(this._showBoard.bind(this, init));
104✔
374
    }
375

376
    _loadStructure() {
377
        // case when structure mode is already loaded
378
        if (CMS.config.settings.mode === 'structure' || this._loadedStructure) {
100✔
379
            return Promise.resolve();
99✔
380
        }
381

382
        showLoader();
1✔
383
        return this
1✔
384
            ._requestMode('structure')
385
            .done(contentMarkup => {
386
                this._requeststructure = null;
1✔
387
                hideLoader();
1✔
388

389
                CMS.settings.states = Helpers.getSettings().states;
1✔
390

391
                var bodyRegex = /<body[\S\s]*?>([\S\s]*)<\/body>/gi;
1✔
392
                var body = $(bodyRegex.exec(contentMarkup)[1]);
1✔
393

394
                var structure = body.find('.cms-structure-content');
1✔
395
                var toolbar = body.find('.cms-toolbar');
1✔
396
                var scripts = body.filter(function() {
1✔
397
                    var elem = $(this);
1✔
398

399
                    return elem.is('[type="text/cms-template"]'); // cms scripts
1✔
400
                });
401
                const pluginIds = this.getIds(body.find('.cms-draggable'));
1✔
402
                const pluginDataSource = body.filter('script[data-cms]').toArray()
1✔
403
                    .map(script => script.textContent || '').join();
×
404
                const pluginData = StructureBoard._getPluginDataFromMarkup(
1✔
405
                    pluginDataSource,
406
                    pluginIds
407
                );
408

409
                Plugin._updateRegistry(pluginData.map(([, data]) => data));
1✔
410

411
                CMS.API.Toolbar._refreshMarkup(toolbar);
1✔
412

413
                $('body').append(scripts);
1✔
414
                $('.cms-structure-content').html(structure.html());
1✔
415
                triggerWindowResize();
1✔
416

417
                StructureBoard._initializeGlobalHandlers();
1✔
418
                StructureBoard.actualizePlaceholders();
1✔
419
                CMS._instances.forEach(instance => {
1✔
420
                    if (instance.options.type === 'placeholder') {
3✔
421
                        instance._setPlaceholder();
1✔
422
                    }
423
                });
424
                CMS._instances.forEach(instance => {
1✔
425
                    if (instance.options.type === 'plugin') {
3✔
426
                        instance._setPluginStructureEvents();
1✔
427
                        instance._collapsables();
1✔
428
                    }
429
                });
430

431
                this.ui.sortables = $('.cms-draggables');
1✔
432
                this._drag();
1✔
433
                StructureBoard._initializeDragItemsStates();
1✔
434

435
                this._loadedStructure = true;
1✔
436
            })
437
            .fail(function() {
438
                window.location.href = CMS.config.settings.structure;
×
439
            });
440
    }
441

442
    _requestMode(mode) {
443
        let url;
444

445
        if (mode === 'structure') {
4✔
446
            url = CMS.config.settings.structure;
1✔
447
        } else {
448
            url = CMS.config.settings.edit;
3✔
449
        }
450

451
        if (!this[`_request${mode}`]) {
4✔
452
            this[`_request${mode}`] = $.ajax({
3✔
453
                url: url.toString(),
454
                method: 'GET'
455
            }).then(markup => {
456
                preloadImagesFromMarkup(markup);
3✔
457

458
                return markup;
3✔
459
            });
460
        }
461

462
        return this[`_request${mode}`];
4✔
463
    }
464

465
    _loadContent() {
466
        var that = this;
47✔
467

468
        // case when content mode is already loaded
469
        if (CMS.config.settings.mode === 'edit' || this._loadedContent) {
47✔
470
            return Promise.resolve();
46✔
471
        }
472

473
        showLoader();
1✔
474
        return that
1✔
475
            ._requestMode('content')
476
            .done(function(contentMarkup) {
477
                that._requestcontent = null;
1✔
478
                hideLoader();
1✔
479
                var htmlRegex = /<html([\S\s]*?)>[\S\s]*<\/html>/gi;
1✔
480
                var bodyRegex = /<body([\S\s]*?)>([\S\s]*)<\/body>/gi;
1✔
481
                var headRegex = /<head[\S\s]*?>([\S\s]*)<\/head>/gi;
1✔
482
                var matches = bodyRegex.exec(contentMarkup);
1✔
483
                // we don't handle cases where body or html doesn't exist, cause it's highly unlikely
484
                // and will result in way more troubles for cms than this
485
                var bodyAttrs = matches[1];
1✔
486
                var body = $(matches[2]);
1✔
487
                var head = $(headRegex.exec(contentMarkup)[1]);
1✔
488
                var htmlAttrs = htmlRegex.exec(contentMarkup)[1];
1✔
489
                var bodyAttributes = $('<div ' + bodyAttrs + '></div>')[0].attributes;
1✔
490
                var htmlAttributes = $('<div ' + htmlAttrs + '></div>')[0].attributes;
1✔
491
                var newToolbar = body.find('.cms-toolbar');
1✔
492
                var toolbar = $('.cms').add('[data-cms]').detach();
1✔
493
                var title = head.filter('title');
1✔
494
                var bodyElement = $('body');
1✔
495

496
                // istanbul ignore else
497
                if (title) {
1✔
498
                    document.title = title.text();
1✔
499
                }
500

501
                body = body.filter(function() {
1✔
502
                    var elem = $(this);
1✔
503

504
                    return (
1✔
505
                        !elem.is('.cms#cms-top') && !elem.is('[data-cms]:not([data-cms-generic])') // toolbar
2✔
506
                    ); // cms scripts
507
                });
508
                body.find('[data-cms]:not([data-cms-generic])').remove(); // cms scripts
1✔
509

510
                [].slice.call(bodyAttributes).forEach(function(attr) {
1✔
511
                    bodyElement.attr(attr.name, attr.value);
2✔
512
                });
513

514
                [].slice.call(htmlAttributes).forEach(function(attr) {
1✔
515
                    $('html').attr(attr.name, attr.value);
2✔
516
                });
517

518
                bodyElement.append(body);
1✔
519
                $('head').append(head);
1✔
520
                bodyElement.prepend(toolbar);
1✔
521

522
                CMS.API.Toolbar._refreshMarkup(newToolbar);
1✔
523
                $(window).trigger('resize');
1✔
524

525
                Plugin._refreshPlugins();
1✔
526

527
                const scripts = $('script');
1✔
528

529
                // istanbul ignore next
530
                scripts.on('load', function() {
531
                    window.dispatchEvent(new Event('load'));
532
                    window.dispatchEvent(new Event('DOMContentLoaded'));
533
                });
534

535
                const unhandledPlugins = $('body').find('template.cms-plugin');
1✔
536

537
                if (unhandledPlugins.length) {
1!
538
                    CMS.API.Messages.open({
×
539
                        message: CMS.config.lang.unhandledPageChange
540
                    });
541
                    Helpers.reloadBrowser();
×
542
                }
543

544
                that._loadedContent = true;
1✔
545
            })
546
            .fail(function() {
547
                window.location.href = CMS.config.settings.edit;
×
548
            });
549
    }
550

551
    /**
552
     * Hides the structureboard. (Content mode)
553
     *
554
     * @method hide
555
     * @returns {Boolean|void}
556
     */
557
    hide() {
558
        // cancel show if live modus is active
559
        if (CMS.config.mode === 'live') {
49✔
560
            return false;
1✔
561
        }
562

563
        // reset toolbar positioning
564
        this.ui.toolbar.css('right', '');
48✔
565
        $('html').removeClass('cms-overflow');
48✔
566

567
        // set active item
568
        var modes = this.ui.toolbarModeLinks;
48✔
569

570
        modes.removeClass('cms-btn-active').eq(1).addClass('cms-btn-active');
48✔
571
        this.ui.html.removeClass('cms-structure-mode-structure').addClass('cms-structure-mode-content');
48✔
572

573
        CMS.settings.mode = 'edit';
48✔
574

575
        // hide canvas
576
        return this._loadContent().then(this._hideBoard.bind(this));
48✔
577
    }
578

579
    /**
580
     * Gets the id of the element.
581
     * relies on cms-{item}-{id} to always be second in a string of classes (!)
582
     *
583
     * @method getId
584
     * @param {jQuery} el element to get id from
585
     * @returns {String}
586
     */
587
    getId(el) {
588
        // cancel if no element is defined
589
        if (el === undefined || el === null || el.length <= 0) {
73✔
590
            return false;
7✔
591
        }
592

593
        var id = null;
66✔
594
        var cls = el.attr('class').split(' ')[1];
66✔
595

596
        if (el.hasClass('cms-plugin')) {
66✔
597
            id = cls.replace('cms-plugin-', '').trim();
10✔
598
        } else if (el.hasClass('cms-draggable')) {
56✔
599
            id = cls.replace('cms-draggable-', '').trim();
36✔
600
        } else if (el.hasClass('cms-placeholder')) {
20✔
601
            id = cls.replace('cms-placeholder-', '').trim();
2✔
602
        } else if (el.hasClass('cms-dragbar')) {
18✔
603
            id = cls.replace('cms-dragbar-', '').trim();
2✔
604
        } else if (el.hasClass('cms-dragarea')) {
16✔
605
            id = cls.replace('cms-dragarea-', '').trim();
11✔
606
        }
607

608
        return id;
65✔
609
    }
610

611
    /**
612
     * Gets the ids of the list of  elements.
613
     *
614
     * @method getIds
615
     * @param {jQuery} els elements to get id from
616
     * @returns {String[]}
617
     */
618
    getIds(els) {
619
        var that = this;
8✔
620
        var array = [];
8✔
621

622
        els.each(function() {
8✔
623
            array.push(that.getId($(this)));
13✔
624
        });
625
        return array;
8✔
626
    }
627

628
    /**
629
     * Actually shows the board canvas.
630
     *
631
     * @method _showBoard
632
     * @param {Boolean} init init
633
     * @private
634
     */
635
    _showBoard(init) {
636
        // set active item
637
        var modes = this.ui.toolbarModeLinks;
104✔
638

639
        modes.removeClass('cms-btn-active').eq(0).addClass('cms-btn-active');
104✔
640
        this.ui.html.removeClass('cms-structure-mode-content').addClass('cms-structure-mode-structure');
104✔
641

642
        this.ui.container.show();
104✔
643
        hideLoader();
104✔
644

645
        if (!init) {
104✔
646
            this._makeCondensed();
43✔
647
        }
648

649
        if (init && !this._loadedContent) {
104✔
650
            this._makeFullWidth();
61✔
651
        }
652

653
        this._preventScroll.start();
104✔
654
        this.ui.window.trigger('resize');
104✔
655
    }
656

657
    _makeCondensed() {
658
        this.condensed = true;
43✔
659
        this.ui.container.addClass('cms-structure-condensed');
43✔
660

661
        if (CMS.settings.mode === 'structure') {
43✔
662
            history.replaceState({}, '', CMS.config.settings.edit);
42✔
663
        }
664

665
        var width = this.ui.toolbar.width();
43✔
666
        var scrollBarWidth = this.ui.window[0].innerWidth - width;
43✔
667

668
        if (!scrollBarWidth) {
43✔
669
            scrollBarWidth = measureScrollbar();
6✔
670
        }
671

672
        this.ui.html.removeClass('cms-overflow');
43✔
673

674
        if (scrollBarWidth) {
43!
675
            // this.ui.toolbar.css('right', scrollBarWidth);
676
            this.ui.container.css('right', -scrollBarWidth);
43✔
677
        }
678
    }
679

680
    _makeFullWidth() {
681
        this.condensed = false;
61✔
682
        this.ui.container.removeClass('cms-structure-condensed');
61✔
683

684
        if (CMS.settings.mode === 'structure') {
61!
685
            history.replaceState({}, '', CMS.config.settings.structure);
61✔
686
            $('html.cms-structure-mode-structure').addClass('cms-overflow');
61✔
687
        }
688

689
        this.ui.container.css('right', 0);
61✔
690
    }
691

692
    /**
693
     * Hides the board canvas.
694
     *
695
     * @method _hideBoard
696
     * @private
697
     */
698
    _hideBoard() {
699
        // hide elements
700
        this.ui.container.hide();
48✔
701
        this._preventScroll.stop();
48✔
702

703
        // this is sometimes required for user-side scripts to
704
        // render dynamic elements on the page correctly.
705
        // e.g. you have a parallax script that calculates position
706
        // of elements based on document height. but if the page is
707
        // loaded with structureboard active - the document height
708
        // would be same as screen height, which is likely incorrect,
709
        // so triggering resize on window would force user scripts
710
        // to recalculate whatever is required there
711
        // istanbul ignore catch
712
        triggerWindowResize();
48✔
713
    }
714

715
    /**
716
     * Sets up all the sortables.
717
     *
718
     * @method _drag
719
     * @param {jQuery} [elem=this.ui.sortables] which element to initialize
720
     * @private
721
     */
722
    _drag(elem = this.ui.sortables) {
36✔
723
        var that = this;
36✔
724

725
        elem
36✔
726
            .nestedSortable({
727
                items: '> .cms-draggable:not(.cms-draggable-disabled .cms-draggable)',
728
                placeholder: 'cms-droppable',
729
                connectWith: '.cms-draggables:not(.cms-hidden)',
730
                tolerance: 'intersect',
731
                toleranceElement: '> div',
732
                dropOnEmpty: true,
733
                // cloning huge structure is a performance loss compared to cloning just a dragitem
734
                helper: function createHelper(e, item) {
735
                    var clone = item.find('> .cms-dragitem').clone();
8✔
736

737
                    clone.wrap('<div class="' + item[0].className + '"></div>');
8✔
738
                    return clone.parent();
8✔
739
                },
740
                appendTo: '.cms-structure-content',
741
                // appendTo: '.cms',
742
                cursor: 'move',
743
                cursorAt: { left: -15, top: -15 },
744
                opacity: 1,
745
                zIndex: 9999999,
746
                delay: 100,
747
                tabSize: 15,
748
                // nestedSortable
749
                listType: 'div.cms-draggables',
750
                doNotClear: true,
751
                disableNestingClass: 'cms-draggable-disabled',
752
                errorClass: 'cms-draggable-disallowed',
753
                scrollSpeed: 15,
754
                // eslint-disable-next-line no-magic-numbers
755
                scrollSensitivity: that.ui.window.height() * 0.2,
756
                start: function(e, ui) {
757
                    that.ui.content.attr('data-touch-action', 'none');
20✔
758

759
                    originalPluginContainer = ui.item.closest('.cms-draggables');
20✔
760

761
                    that.dragging = true;
20✔
762
                    // show empty
763
                    StructureBoard.actualizePlaceholders();
20✔
764
                    // ensure all menus are closed
765
                    Plugin._hideSettingsMenu();
20✔
766
                    // keep in mind that caching cms-draggables query only works
767
                    // as long as we don't create them on the fly
768
                    that.ui.sortables.each(function() {
20✔
769
                        var element = $(this);
80✔
770

771
                        if (element.children().length === 0) {
80✔
772
                            element.removeClass('cms-hidden');
18✔
773
                        }
774
                    });
775

776
                    // fixes placeholder height
777
                    ui.item.addClass('cms-is-dragging');
20✔
778
                    ui.helper.addClass('cms-draggable-is-dragging');
20✔
779
                    if (ui.item.find('> .cms-draggables').children().length) {
20✔
780
                        ui.helper.addClass('cms-draggable-stack');
1✔
781
                    }
782

783
                    // attach escape event to cancel dragging
784
                    that.ui.doc.on('keyup.cms.interrupt', function(event, cancel) {
20✔
785
                        if ((event.keyCode === KEYS.ESC && that.dragging) || cancel) {
3✔
786
                            that.state = false;
2✔
787
                            $.ui.sortable.prototype._mouseStop();
2✔
788
                            that.ui.sortables.trigger('mouseup');
2✔
789
                        }
790
                    });
791
                },
792

793
                beforeStop: function(event, ui) {
794
                    that.dragging = false;
4✔
795
                    ui.item.removeClass('cms-is-dragging cms-draggable-stack');
4✔
796
                    that.ui.doc.off('keyup.cms.interrupt');
4✔
797
                    that.ui.content.attr('data-touch-action', 'pan-y');
4✔
798
                },
799

800
                update: function(event, ui) {
801
                    // cancel if isAllowed returns false
802
                    if (!that.state) {
12✔
803
                        return false;
1✔
804
                    }
805

806
                    var newPluginContainer = ui.item.closest('.cms-draggables');
11✔
807

808
                    if (originalPluginContainer.is(newPluginContainer)) {
11✔
809
                        // if we moved inside same container,
810
                        // but event is fired on a parent, discard update
811
                        if (!newPluginContainer.is(this)) {
2✔
812
                            return false;
1✔
813
                        }
814
                    } else {
815
                        StructureBoard.actualizePluginsCollapsibleStatus(
9✔
816
                            newPluginContainer.add(originalPluginContainer)
817
                        );
818
                    }
819

820
                    // we pass the id to the updater which checks within the backend the correct place
821
                    var id = that.getId(ui.item);
10✔
822
                    var plugin = $(`.cms-draggable-${id}`);
10✔
823
                    var eventData = {
10✔
824
                        id: id
825
                    };
826
                    var previousParentPlugin = originalPluginContainer.closest('.cms-draggable');
10✔
827

828
                    if (previousParentPlugin.length) {
10✔
829
                        var previousParentPluginId = that.getId(previousParentPlugin);
3✔
830

831
                        eventData.previousParentPluginId = previousParentPluginId;
3✔
832
                    }
833

834
                    // check if we copy/paste a plugin or not
835
                    if (originalPluginContainer.hasClass('cms-clipboard-containers')) {
10✔
836
                        originalPluginContainer.html(plugin.eq(0).clone(true, true));
1✔
837
                        Plugin._updateClipboard();
1✔
838
                        plugin.trigger('cms-paste-plugin-update', [eventData]);
1✔
839
                    } else {
840
                        plugin.trigger('cms-plugins-update', [eventData]);
9✔
841
                    }
842

843
                    // reset placeholder without entries
844
                    that.ui.sortables.each(function() {
10✔
845
                        var element = $(this);
40✔
846

847
                        if (element.children().length === 0) {
40✔
848
                            element.addClass('cms-hidden');
7✔
849
                        }
850
                    });
851

852
                    StructureBoard.actualizePlaceholders();
10✔
853
                },
854
                // eslint-disable-next-line complexity
855
                isAllowed: function(placeholder, placeholderParent, originalItem) {
856
                    // cancel if action is executed
857
                    if (CMS.API.locked) {
14✔
858
                        return false;
1✔
859
                    }
860
                    // getting restriction array
861
                    var bounds = [];
13✔
862
                    var immediateParentType;
863

864
                    if (placeholder && placeholder.closest('.cms-clipboard-containers').length) {
13✔
865
                        return false;
1✔
866
                    }
867

868
                    // if parent has class disabled, dissalow drop
869
                    if (placeholder && placeholder.parent().hasClass('cms-draggable-disabled')) {
12✔
870
                        return false;
1✔
871
                    }
872

873
                    var originalItemId = that.getId(originalItem);
11✔
874
                    // save original state events
875
                    var original = $('.cms-draggable-' + originalItemId);
11✔
876

877
                    // cancel if item has no settings
878
                    if (original.length === 0 || !original.data('cms')) {
11✔
879
                        return false;
2✔
880
                    }
881
                    var originalItemData = original.data('cms');
9✔
882
                    var parent_bounds = $.grep(originalItemData.plugin_parent_restriction, function(r) {
9✔
883
                        // special case when PlaceholderPlugin has a parent restriction named "0"
884
                        return r !== '0';
3✔
885
                    });
886
                    var type = originalItemData.plugin_type;
9✔
887
                    // prepare variables for bound
888
                    var holderId = that.getId(placeholder.closest('.cms-dragarea'));
9✔
889
                    var holder = $('.cms-placeholder-' + holderId);
9✔
890
                    var plugin;
891

892
                    if (placeholderParent && placeholderParent.length) {
9✔
893
                        // placeholderParent is always latest, it maybe that
894
                        // isAllowed is called _before_ placeholder is moved to a child plugin
895
                        plugin = $('.cms-draggable-' + that.getId(placeholderParent.closest('.cms-draggable')));
1✔
896
                    } else {
897
                        plugin = $('.cms-draggable-' + that.getId(placeholder.closest('.cms-draggable')));
8✔
898
                    }
899

900
                    // now set the correct bounds
901
                    // istanbul ignore else
902
                    if (holder.length) {
9✔
903
                        bounds = holder.data('cms').plugin_restriction;
9✔
904
                        immediateParentType = holder.data('cms').plugin_type;
9✔
905
                    }
906
                    if (plugin.length) {
9✔
907
                        bounds = plugin.data('cms').plugin_restriction;
7✔
908
                        immediateParentType = plugin.data('cms').plugin_type;
7✔
909
                    }
910

911
                    // if restrictions is still empty, proceed
912
                    that.state = !(bounds.length && $.inArray(type, bounds) === -1);
9✔
913

914
                    // check if we have a parent restriction
915
                    if (parent_bounds.length) {
9✔
916
                        that.state = $.inArray(immediateParentType, parent_bounds) !== -1;
2✔
917
                    }
918

919
                    return that.state;
9✔
920
                }
921
            })
922
            .on('cms-structure-update', StructureBoard.actualizePlaceholders);
923
    }
924

925
    _dragRefresh() {
926
        this.ui.sortables.each((i, el) => {
11✔
927
            const element = $(el);
41✔
928

929
            if (element.data('mjsNestedSortable')) {
41!
930
                return;
×
931
            }
932

933
            this._drag(element);
41✔
934
        });
935
    }
936

937
    /**
938
     * @method invalidateState
939
     * @param {String} action - action to handle
940
     * @param {Object} data - data required to handle the object
941
     * @param {Object} opts
942
     * @param {Boolean} [opts.propagate=true] - should we propagate the change to other tabs or not
943
     */
944
    // eslint-disable-next-line complexity
945
    invalidateState(action, data, { propagate = true } = {}) {
50✔
946
        // eslint-disable-next-line default-case
947
        switch (action) {
25✔
948
            case 'COPY': {
949
                this.handleCopyPlugin(data);
2✔
950
                break;
2✔
951
            }
952

953
            case 'ADD': {
954
                this.handleAddPlugin(data);
2✔
955
                break;
2✔
956
            }
957

958
            case 'EDIT': {
959
                this.handleEditPlugin(data);
2✔
960
                break;
2✔
961
            }
962

963
            case 'DELETE': {
964
                this.handleDeletePlugin(data);
2✔
965
                break;
2✔
966
            }
967

968
            case 'CLEAR_PLACEHOLDER': {
969
                this.handleClearPlaceholder(data);
2✔
970
                break;
2✔
971
            }
972

973
            case 'PASTE':
974
            case 'MOVE': {
975
                this.handleMovePlugin(data);
4✔
976
                break;
4✔
977
            }
978

979
            case 'CUT': {
980
                this.handleCutPlugin(data);
2✔
981
                break;
2✔
982
            }
983
        }
984

985
        Plugin._recalculatePluginPositions(action, data);
25✔
986

987
        if (!action) {
25✔
988
            CMS.API.Helpers.reloadBrowser();
1✔
989
            return;
1✔
990
        }
991

992
        if (propagate) {
24!
993
            this._propagateInvalidatedState(action, data);
24✔
994
        }
995

996
        // refresh content mode if needed
997
        // refresh toolbar
998
        var currentMode = CMS.settings.mode;
24✔
999

1000
        if (currentMode === 'structure') {
24✔
1001
            this._requestcontent = null;
22✔
1002

1003
            if (this._loadedContent && action !== 'COPY') {
22✔
1004
                this.updateContent();
2✔
1005
                return;  // Toolbar loaded
2✔
1006
            }
1007
        } else if (action !== 'COPY') {
2!
1008
            this._requestcontent = null;
2✔
1009
            this.updateContent();
2✔
1010
            return;  // Toolbar loaded
2✔
1011

1012
        }
1013
        this._loadToolbar()
20✔
1014
            .done(newToolbar => {
1015
                CMS.API.Toolbar._refreshMarkup($(newToolbar).find('.cms-toolbar'));
1✔
1016
            })
1017
            .fail(() => Helpers.reloadBrowser());
1✔
1018
    }
1019

1020
    _propagateInvalidatedState(action, data) {
1021
        this.latestAction = [action, data];
24✔
1022

1023
        ls.set(storageKey, JSON.stringify([action, data, window.location.pathname]));
24✔
1024
    }
1025

1026
    _listenToExternalUpdates() {
1027
        if (!Helpers._isStorageSupported) {
126✔
1028
            return;
3✔
1029
        }
1030

1031
        ls.on(storageKey, this._handleExternalUpdate.bind(this));
123✔
1032
    }
1033

1034
    _handleExternalUpdate(value) {
1035
        // means localstorage was cleared while this page was open
1036
        if (!value) {
×
1037
            return;
×
1038
        }
1039

1040
        const [action, data, pathname] = JSON.parse(value);
×
1041

1042
        if (pathname !== window.location.pathname) {
×
1043
            return;
×
1044
        }
1045

1046
        if (isEqual([action, data], this.latestAction)) {
×
1047
            return;
×
1048
        }
1049

1050
        this.invalidateState(action, data, { propagate: false });
×
1051
    }
1052

1053
    updateContent() {
1054
        const loader = $('<div class="cms-content-reloading"></div>');
4✔
1055

1056
        $('.cms-structure').before(loader);
4✔
1057

1058
        return this._requestMode('content')
4✔
1059
            .done(markup => {
1060
                // eslint-disable-next-line no-magic-numbers
1061
                loader.fadeOut(100, () => loader.remove());
2✔
1062
                this.refreshContent(markup);
2✔
1063
            })
1064
            .fail(() => loader.remove() && Helpers.reloadBrowser());
2✔
1065
    }
1066

1067
    _loadToolbar() {
1068
        const placeholderIds = getPlaceholderIds(CMS._plugins).map(id => `placeholders[]=${id}`).join('&');
1✔
1069

1070
        return $.ajax({
1✔
1071
            url: Helpers.updateUrlWithPath(
1072
                `${CMS.config.request.toolbar}?` +
1073
                    placeholderIds +
1074
                    '&' +
1075
                    `obj_id=${CMS.config.request.pk}&` +
1076
                    `obj_type=${encodeURIComponent(CMS.config.request.model)}`
1077
            )
1078
        });
1079
    }
1080

1081
    // i think this should probably be a separate class at this point that handles all the reloading
1082
    // stuff, it's a bit too much
1083
    // eslint-disable-next-line complexity
1084
    handleMovePlugin(data) {
1085
        if (data.plugin_parent) {
5✔
1086
            if (data.plugin_id) {
1!
1087
                const draggable = $(`.cms-draggable-${data.plugin_id}:last`);
1✔
1088

1089
                if (
1!
1090
                    !draggable.closest(`.cms-draggable-${data.plugin_parent}`).length &&
2✔
1091
                    !draggable.is('.cms-draggable-from-clipboard')
1092
                ) {
1093
                    draggable.remove();
1✔
1094
                }
1095
            }
1096

1097
            // empty the children first because replaceWith takes too much time
1098
            // when it's trying to remove all the data and event handlers from potentially big tree of plugins
1099
            $(`.cms-draggable-${data.plugin_parent}`).html('').replaceWith(data.html);
1✔
1100
        } else {
1101
            // the one in the clipboard is first, so we need to take the second one,
1102
            // that is already visually moved into correct place
1103
            let draggable = $(`.cms-draggable-${data.plugin_id}:last`);
4✔
1104

1105
            // external update, have to move the draggable to correct place first
1106
            if (!draggable.closest('.cms-draggables').parent().is(`.cms-dragarea-${data.placeholder_id}`)) {
4✔
1107
                const pluginOrder = data.plugin_order;
2✔
1108
                const index = findIndex(
2✔
1109
                    pluginOrder,
1110
                    pluginId => Number(pluginId) === Number(data.plugin_id) || pluginId === '__COPY__'
×
1111
                );
1112
                const placeholderDraggables = $(`.cms-dragarea-${data.placeholder_id} > .cms-draggables`);
2✔
1113

1114
                if (draggable.is('.cms-draggable-from-clipboard')) {
2!
1115
                    draggable = draggable.clone();
×
1116
                }
1117

1118
                if (index === 0) {
2!
1119
                    placeholderDraggables.prepend(draggable);
×
1120
                } else if (index !== -1) {
2!
1121
                    placeholderDraggables.find(`.cms-draggable-${pluginOrder[index - 1]}`).after(draggable);
×
1122
                }
1123
            }
1124

1125
            // if we _are_ in the correct placeholder we still need to check if the order is correct
1126
            // since it could be an external update of a plugin moved in the same placeholder. also we are top-level
1127
            if (draggable.closest('.cms-draggables').parent().is(`.cms-dragarea-${data.placeholder_id}`)) {
4✔
1128
                const placeholderDraggables = $(`.cms-dragarea-${data.placeholder_id} > .cms-draggables`);
2✔
1129
                const actualPluginOrder = this.getIds(
2✔
1130
                    placeholderDraggables.find('> .cms-draggable')
1131
                );
1132

1133
                if (!arrayEquals(actualPluginOrder, data.plugin_order)) {
2!
1134
                    // so the plugin order is not correct, means it's an external update and we need to move
1135
                    const pluginOrder = data.plugin_order;
2✔
1136
                    const index = findIndex(
2✔
1137
                        pluginOrder,
1138
                        pluginId => Number(pluginId) === Number(data.plugin_id)
3✔
1139
                    );
1140

1141
                    if (index === 0) {
2✔
1142
                        placeholderDraggables.prepend(draggable);
1✔
1143
                    } else if (index !== -1) {
1!
1144
                        placeholderDraggables.find(`.cms-draggable-${pluginOrder[index - 1]}`).after(draggable);
1✔
1145
                    }
1146
                }
1147
            }
1148

1149
            if (draggable.length) {
4✔
1150
                // empty the children first because replaceWith takes too much time
1151
                // when it's trying to remove all the data and event handlers from potentially big tree of plugins
1152
                draggable.html('').replaceWith(data.html);
3✔
1153
            } else if (data.target_placeholder_id) {
1!
1154
                // copy from language
1155
                $(`.cms-dragarea-${data.target_placeholder_id} > .cms-draggables`).append(data.html);
1✔
1156
            }
1157
        }
1158

1159
        StructureBoard.actualizePlaceholders();
5✔
1160
        Plugin._updateRegistry(data.plugins);
5✔
1161
        data.plugins.forEach(pluginData => {
5✔
1162
            StructureBoard.actualizePluginCollapseStatus(pluginData.plugin_id);
7✔
1163
        });
1164

1165
        StructureBoard._initializeDragItemsStates();
5✔
1166

1167
        this.ui.sortables = $('.cms-draggables');
5✔
1168
        this._dragRefresh();
5✔
1169
    }
1170

1171
    handleCopyPlugin(data) {
1172
        if (CMS.API.Clipboard._isClipboardModalOpen()) {
2✔
1173
            CMS.API.Clipboard.modal.close();
1✔
1174
        }
1175

1176
        $('.cms-clipboard-containers').html(data.html);
2✔
1177
        const cloneClipboard = $('.cms-clipboard').clone();
2✔
1178

1179
        $('.cms-clipboard').replaceWith(cloneClipboard);
2✔
1180

1181
        const pluginData = [`cms-plugin-${data.plugins[0].plugin_id}`, data.plugins[0]];
2✔
1182

1183
        Plugin.aliasPluginDuplicatesMap[pluginData[1].plugin_id] = false;
2✔
1184
        CMS._plugins.push(pluginData);
2✔
1185
        CMS._instances.push(new Plugin(pluginData[0], pluginData[1]));
2✔
1186

1187
        CMS.API.Clipboard = new Clipboard();
2✔
1188

1189
        Plugin._updateClipboard();
2✔
1190

1191
        let html = '';
2✔
1192

1193
        const clipboardDraggable = $('.cms-clipboard .cms-draggable:first');
2✔
1194

1195
        html = clipboardDraggable.parent().html();
2✔
1196

1197
        CMS.API.Clipboard.populate(html, pluginData[1]);
2✔
1198
        CMS.API.Clipboard._enableTriggers();
2✔
1199

1200
        this.ui.sortables = $('.cms-draggables');
2✔
1201
        this._dragRefresh();
2✔
1202
    }
1203

1204
    handleCutPlugin(data) {
1205
        this.handleDeletePlugin(data);
1✔
1206
        this.handleCopyPlugin(data);
1✔
1207
    }
1208

1209
    _extractMessages(doc) {
1210
        let messageList = doc.find('.messagelist');
6✔
1211
        let messages = messageList.find('li');
6✔
1212

1213
        if (!messageList.length || !messages.length) {
6✔
1214
            messageList = doc.find('[data-cms-messages-container]');
5✔
1215
            messages = messageList.find('[data-cms-message]');
5✔
1216
        }
1217

1218
        if (messages.length) {
6✔
1219
            messageList.remove();
3✔
1220

1221
            return compact(
3✔
1222
                messages.toArray().map(el => {
1223
                    const msgEl = $(el);
7✔
1224
                    const message = $(el).text().trim();
7✔
1225

1226
                    if (message) {
7✔
1227
                        return {
6✔
1228
                            message,
1229
                            error: msgEl.data('cms-message-tags') === 'error' || msgEl.hasClass('error')
10✔
1230
                        };
1231
                    }
1232
                })
1233
            );
1234
        }
1235

1236
        return [];
3✔
1237
    }
1238

1239
    refreshContent(contentMarkup) {
1240
        this._requestcontent = null;
3✔
1241
        if (!this._loadedStructure) {
3!
1242
            this._requeststructure = null;
3✔
1243
        }
1244
        var fixedContentMarkup = contentMarkup;
3✔
1245
        var newDoc = new DOMParser().parseFromString(fixedContentMarkup, 'text/html');
3✔
1246

1247
        const structureScrollTop = $('.cms-structure-content').scrollTop();
3✔
1248

1249
        var toolbar = $('#cms-top, [data-cms]').detach();
3✔
1250
        var newToolbar = $(newDoc).find('.cms-toolbar').clone();
3✔
1251

1252
        $(newDoc).find('#cms-top, [data-cms]').remove();
3✔
1253

1254
        const messages = this._extractMessages($(newDoc));
3✔
1255

1256
        if (messages.length) {
3✔
1257
            setTimeout(() =>
1✔
1258
                messages.forEach(message => {
1✔
1259
                    CMS.API.Messages.open(message);
1✔
1260
                })
1261
            );
1262
        }
1263

1264
        var headDiff = dd.diff(document.head, newDoc.head);
3✔
1265

1266
        StructureBoard._replaceBodyWithHTML(newDoc.body.innerHTML);
3✔
1267
        dd.apply(document.head, headDiff);
3✔
1268
        toolbar.prependTo(document.body);
3✔
1269
        CMS.API.Toolbar._refreshMarkup(newToolbar);
3✔
1270

1271
        $('.cms-structure-content').scrollTop(structureScrollTop);
3✔
1272

1273
        Plugin._refreshPlugins();
3✔
1274

1275
        Helpers._getWindow().dispatchEvent(new Event('load'));
3✔
1276
        $(Helpers._getWindow()).trigger('cms-content-refresh');
3✔
1277

1278
        this._loadedContent = true;
3✔
1279
    }
1280

1281
    handleAddPlugin(data) {
1282
        if (data.plugin_parent) {
2✔
1283
            $(`.cms-draggable-${data.plugin_parent}`).replaceWith(data.structure.html);
1✔
1284
        } else {
1285
            // the one in the clipboard is first
1286
            $(`.cms-dragarea-${data.placeholder_id} > .cms-draggables`).append(data.structure.html);
1✔
1287
        }
1288

1289
        StructureBoard.actualizePlaceholders();
2✔
1290
        Plugin._updateRegistry(data.structure.plugins);
2✔
1291
        data.structure.plugins.forEach(pluginData => {
2✔
1292
            StructureBoard.actualizePluginCollapseStatus(pluginData.plugin_id);
3✔
1293
        });
1294

1295
        this.ui.sortables = $('.cms-draggables');
2✔
1296
        this._dragRefresh();
2✔
1297
    }
1298

1299
    handleEditPlugin(data) {
1300
        if (data.plugin_parent) {
2✔
1301
            $(`.cms-draggable-${data.plugin_parent}`).replaceWith(data.structure.html);
1✔
1302
        } else {
1303
            $(`.cms-draggable-${data.plugin_id}`).replaceWith(data.structure.html);
1✔
1304
        }
1305

1306
        Plugin._updateRegistry(data.structure.plugins);
2✔
1307

1308
        data.structure.plugins.forEach(pluginData => {
2✔
1309
            StructureBoard.actualizePluginCollapseStatus(pluginData.plugin_id);
2✔
1310
        });
1311

1312
        this.ui.sortables = $('.cms-draggables');
2✔
1313
        this._dragRefresh();
2✔
1314
    }
1315

1316
    handleDeletePlugin(data) {
1317
        var deletedPluginIds = [data.plugin_id];
2✔
1318
        var draggable = $('.cms-draggable-' + data.plugin_id);
2✔
1319
        var children = draggable.find('.cms-draggable');
2✔
1320
        let parent = draggable.parent().closest('.cms-draggable');
2✔
1321

1322
        if (!parent.length) {
2✔
1323
            parent = draggable.closest('.cms-dragarea');
1✔
1324
        }
1325

1326
        if (children.length) {
2✔
1327
            deletedPluginIds = deletedPluginIds.concat(this.getIds(children));
1✔
1328
        }
1329

1330
        draggable.remove();
2✔
1331

1332
        StructureBoard.actualizePluginsCollapsibleStatus(parent.find('> .cms-draggables'));
2✔
1333
        StructureBoard.actualizePlaceholders();
2✔
1334
        deletedPluginIds.forEach(function(pluginId) {
2✔
1335
            remove(CMS._plugins, settings => settings[0] === `cms-plugin-${pluginId}`);
5✔
1336
            remove(
3✔
1337
                CMS._instances,
1338
                instance => instance.options.plugin_id && Number(instance.options.plugin_id) === Number(pluginId)
5✔
1339
            );
1340
        });
1341
    }
1342

1343
    handleClearPlaceholder(data) {
1344
        var deletedIds = CMS._instances
1✔
1345
            .filter(instance => {
1346
                if (
3✔
1347
                    instance.options.plugin_id &&
6✔
1348
                    Number(instance.options.placeholder_id) === Number(data.placeholder_id)
1349
                ) {
1350
                    return true;
2✔
1351
                }
1352
            })
1353
            .map(instance => instance.options.plugin_id);
2✔
1354

1355
        deletedIds.forEach(id => {
1✔
1356
            remove(CMS._plugins, settings => settings[0] === `cms-plugin-${id}`);
5✔
1357
            remove(
2✔
1358
                CMS._instances,
1359
                instance => instance.options.plugin_id && Number(instance.options.plugin_id) === Number(id)
5✔
1360
            );
1361

1362
            $(`.cms-draggable-${id}`).remove();
2✔
1363
        });
1364

1365
        StructureBoard.actualizePlaceholders();
1✔
1366
    }
1367

1368
    /**
1369
     * Similar to CMS.Plugin populates globally required
1370
     * variables, that only need querying once, e.g. placeholders.
1371
     *
1372
     * @method _initializeGlobalHandlers
1373
     * @static
1374
     * @private
1375
     */
1376
    static _initializeGlobalHandlers() {
1377
        placeholders = $('.cms-dragarea:not(.cms-clipboard-containers)');
77✔
1378
    }
1379

1380
    /**
1381
     * Checks if placeholders are empty and enables/disables certain actions on them, hides or shows the
1382
     * "empty placeholder" placeholder and adapts the location of "Plugin will be added here" placeholder
1383
     *
1384
     * @function actualizePlaceholders
1385
     * @private
1386
     */
1387
    static actualizePlaceholders() {
1388
        placeholders.each(function() {
156✔
1389
            var placeholder = $(this);
465✔
1390
            var copyAll = placeholder.find('.cms-dragbar .cms-submenu-item:has(a[data-rel="copy"]):first');
465✔
1391

1392
            if (
465✔
1393
                placeholder.find('> .cms-draggables').children('.cms-draggable').not('.cms-draggable-is-dragging')
1394
                    .length
1395
            ) {
1396
                placeholder.removeClass('cms-dragarea-empty');
155✔
1397
                copyAll.removeClass('cms-submenu-item-disabled');
155✔
1398
                copyAll.find('> a').removeAttr('aria-disabled');
155✔
1399
            } else {
1400
                placeholder.addClass('cms-dragarea-empty');
310✔
1401
                copyAll.addClass('cms-submenu-item-disabled');
310✔
1402
                copyAll.find('> a').attr('aria-disabled', 'true');
310✔
1403
            }
1404
        });
1405

1406
        const addPluginPlaceholder = $('.cms-dragarea .cms-add-plugin-placeholder');
156✔
1407

1408
        if (addPluginPlaceholder.length && !addPluginPlaceholder.is(':last')) {
156!
1409
            addPluginPlaceholder.appendTo(addPluginPlaceholder.parent());
×
1410
        }
1411
    }
1412

1413
    /**
1414
     * actualizePluginCollapseStatus
1415
     *
1416
     * @public
1417
     * @param {String} pluginId open the plugin if it should be open
1418
     */
1419
    static actualizePluginCollapseStatus(pluginId) {
1420
        const el = $(`.cms-draggable-${pluginId}`);
1✔
1421
        const open = find(CMS.settings.states, openPluginId => Number(openPluginId) === Number(pluginId));
1✔
1422

1423
        // only add this class to elements which have a draggable area
1424
        // istanbul ignore else
1425
        if (open && el.find('> .cms-draggables').length) {
1✔
1426
            el.find('> .cms-collapsable-container').removeClass('cms-hidden');
1✔
1427
            el.find('> .cms-dragitem').addClass('cms-dragitem-expanded');
1✔
1428
        }
1429
    }
1430

1431
    /**
1432
     * @function actualizePluginsCollapsibleStatus
1433
     * @private
1434
     * @param {jQuery} els lists of plugins (.cms-draggables)
1435
     */
1436
    static actualizePluginsCollapsibleStatus(els) {
1437
        els.each(function() {
9✔
1438
            var childList = $(this);
14✔
1439
            var pluginDragItem = childList.closest('.cms-draggable').find('> .cms-dragitem');
14✔
1440

1441
            if (childList.children().length) {
14✔
1442
                pluginDragItem.addClass('cms-dragitem-collapsable');
10✔
1443
                if (childList.children().is(':visible')) {
10!
1444
                    pluginDragItem.addClass('cms-dragitem-expanded');
10✔
1445
                }
1446
            } else {
1447
                pluginDragItem.removeClass('cms-dragitem-collapsable');
4✔
1448
            }
1449
        });
1450
    }
1451

1452
    static _replaceBodyWithHTML(html) {
1453
        document.body.innerHTML = html;
×
1454
    }
1455

1456
    highlightPluginFromUrl() {
1457
        const hash = window.location.hash;
116✔
1458
        const regex = /cms-plugin-(\d+)/;
116✔
1459

1460
        if (!hash || !hash.match(regex)) {
116!
1461
            return;
116✔
1462
        }
1463

1464
        const pluginId = regex.exec(hash)[1];
×
1465

1466
        if (this._loadedContent) {
×
1467
            Plugin._highlightPluginContent(pluginId, {
×
1468
                seeThrough: true,
1469
                prominent: true,
1470
                delay: 3000
1471
            });
1472
        }
1473
    }
1474

1475
    /**
1476
     * Get's plugins data from markup
1477
     *
1478
     * @method _getPluginDataFromMarkup
1479
     * @private
1480
     * @param {String} markup
1481
     * @param {Array<Number | String>} pluginIds
1482
     * @returns {Array<[String, Object]>}
1483
     */
1484
    static _getPluginDataFromMarkup(markup, pluginIds) {
1485
        return compact(
9✔
1486
            pluginIds.map(pluginId => {
1487
                // oh boy
1488
                const regex = new RegExp(`CMS._plugins.push\\((\\["cms\-plugin\-${pluginId}",[\\s\\S]*?\\])\\)`, 'g');
15✔
1489
                const matches = regex.exec(markup);
15✔
1490
                let settings;
1491

1492
                if (matches) {
15✔
1493
                    try {
5✔
1494
                        settings = JSON.parse(matches[1]);
5✔
1495
                    } catch (e) {
1496
                        settings = false;
2✔
1497
                    }
1498
                } else {
1499
                    settings = false;
10✔
1500
                }
1501

1502
                return settings;
15✔
1503
            })
1504
        );
1505
    }
1506

1507
}
1508

1509
/**
1510
 * Initializes the collapsed/expanded states of dragitems in structureboard.
1511
 *
1512
 * @method _initializeDragItemsStates
1513
 * @static
1514
 * @private
1515
 */
1516
// istanbul ignore next
1517
StructureBoard._initializeDragItemsStates = function _initializeDragItemsStates() {
1518
    // removing duplicate entries
1519
    var states = CMS.settings.states || [];
1520
    var sortedArr = states.sort();
1521
    var filteredArray = [];
1522

1523
    for (var i = 0; i < sortedArr.length; i++) {
1524
        if (sortedArr[i] !== sortedArr[i + 1]) {
1525
            filteredArray.push(sortedArr[i]);
1526
        }
1527
    }
1528
    CMS.settings.states = filteredArray;
1529

1530
    // loop through the items
1531
    $.each(CMS.settings.states, function(index, id) {
1532
        var el = $('.cms-draggable-' + id);
1533

1534
        // only add this class to elements which have immediate children
1535
        if (el.find('> .cms-collapsable-container > .cms-draggable').length) {
1536
            el.find('> .cms-collapsable-container').removeClass('cms-hidden');
1537
            el.find('> .cms-dragitem').addClass('cms-dragitem-expanded');
1538
        }
1539
    });
1540
};
1541

1542
// shorthand for jQuery(document).ready();
1543
$(StructureBoard._initializeGlobalHandlers);
1✔
1544

1545
export default StructureBoard;
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