• 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

60.69
/cms/static/cms/js/modules/cms.plugins.js
1
/*
2
 * Copyright https://github.com/divio/django-cms
3
 */
4
import Modal from './cms.modal';
5
import StructureBoard from './cms.structureboard';
6
import $ from 'jquery';
7
import '../polyfills/array.prototype.findindex';
8
import nextUntil from './nextuntil';
9

10
import {
11
    includes,
12
    toPairs,
13
    isNaN,
14
    debounce,
15
    findIndex,
16
    find,
17
    every,
18
    uniqWith,
19
    once,
20
    difference,
21
    isEqual
22
} from 'lodash';
23

24
import Class from 'classjs';
25
import { Helpers, KEYS, $window, $document, uid } from './cms.base';
26
import { showLoader, hideLoader } from './loader';
27
import { filter as fuzzyFilter } from 'fuzzaldrin';
28

29
var clipboardDraggable;
30
var path = window.location.pathname + window.location.search;
1✔
31

32
var pluginUsageMap = Helpers._isStorageSupported ? JSON.parse(localStorage.getItem('cms-plugin-usage') || '{}') : {};
1!
33

34
const isStructureReady = () =>
1✔
35
    CMS.config.settings.mode === 'structure' ||
×
36
    CMS.config.settings.legacy_mode ||
37
    CMS.API.StructureBoard._loadedStructure;
38
const isContentReady = () =>
1✔
39
    CMS.config.settings.mode !== 'structure' ||
×
40
    CMS.config.settings.legacy_mode ||
41
    CMS.API.StructureBoard._loadedContent;
42

43
/**
44
 * Class for handling Plugins / Placeholders or Generics.
45
 * Handles adding / moving / copying / pasting / menus etc
46
 * in structureboard.
47
 *
48
 * @class Plugin
49
 * @namespace CMS
50
 * @uses CMS.API.Helpers
51
 */
52
var Plugin = new Class({
1✔
53
    implement: [Helpers],
54

55
    options: {
56
        type: '', // bar, plugin or generic
57
        placeholder_id: null,
58
        plugin_type: '',
59
        plugin_id: null,
60
        plugin_parent: null,
61
        plugin_order: null,
62
        plugin_restriction: [],
63
        plugin_parent_restriction: [],
64
        urls: {
65
            add_plugin: '',
66
            edit_plugin: '',
67
            move_plugin: '',
68
            copy_plugin: '',
69
            delete_plugin: ''
70
        }
71
    },
72

73
    // these properties will be filled later
74
    modal: null,
75

76
    initialize: function initialize(container, options) {
77
        this.options = $.extend(true, {}, this.options, options);
184✔
78

79
        // create an unique for this component to use it internally
80
        this.uid = uid();
184✔
81

82
        this._setupUI(container);
184✔
83
        this._ensureData();
184✔
84

85
        if (this.options.type === 'plugin' && Plugin.aliasPluginDuplicatesMap[this.options.plugin_id]) {
184✔
86
            return;
1✔
87
        }
88
        if (this.options.type === 'placeholder' && Plugin.staticPlaceholderDuplicatesMap[this.options.placeholder_id]) {
183✔
89
            return;
1✔
90
        }
91

92
        // determine type of plugin
93
        switch (this.options.type) {
182✔
94
            case 'placeholder': // handler for placeholder bars
95
                Plugin.staticPlaceholderDuplicatesMap[this.options.placeholder_id] = true;
24✔
96
                this.ui.container.data('cms', this.options);
24✔
97
                this._setPlaceholder();
24✔
98
                if (isStructureReady()) {
24!
99
                    this._collapsables();
24✔
100
                }
101
                break;
24✔
102
            case 'plugin': // handler for all plugins
103
                this.ui.container.data('cms').push(this.options);
133✔
104
                Plugin.aliasPluginDuplicatesMap[this.options.plugin_id] = true;
133✔
105
                this._setPlugin();
133✔
106
                if (isStructureReady()) {
133!
107
                    this._collapsables();
133✔
108
                }
109
                break;
133✔
110
            default:
111
                // handler for static content
112
                this.ui.container.data('cms').push(this.options);
25✔
113
                this._setGeneric();
25✔
114
        }
115
    },
116

117
    _ensureData: function _ensureData() {
118
        // bind data element to the container (mutating!)
119
        if (!this.ui.container.data('cms')) {
184✔
120
            this.ui.container.data('cms', []);
179✔
121
        }
122
    },
123

124
    /**
125
     * Caches some jQuery references and sets up structure for
126
     * further initialisation.
127
     *
128
     * @method _setupUI
129
     * @private
130
     * @param {String} container `cms-plugin-${id}`
131
     */
132
    _setupUI: function setupUI(container) {
133
        var wrapper = $(`.${container}`);
184✔
134
        var contents;
135

136
        // have to check for cms-plugin, there can be a case when there are multiple
137
        // static placeholders or plugins rendered twice, there could be multiple wrappers on same page
138
        if (wrapper.length > 1 && container.match(/cms-plugin/)) {
184✔
139
            // so it's possible that multiple plugins (more often generics) are rendered
140
            // in different places. e.g. page menu in the header and in the footer
141
            // so first, we find all the template tags, then put them in a structure like this:
142
            // [[start, end], [start, end]...]
143
            //
144
            // in case of plugins it means that it's aliased plugin or a plugin in a duplicated
145
            // static placeholder (for whatever reason)
146
            var contentWrappers = wrapper.toArray().reduce((wrappers, elem, index) => {
140✔
147
                if (index === 0) {
282✔
148
                    wrappers[0].push(elem);
140✔
149
                    return wrappers;
140✔
150
                }
151

152
                var lastWrapper = wrappers[wrappers.length - 1];
142✔
153
                var lastItemInWrapper = lastWrapper[lastWrapper.length - 1];
142✔
154

155
                if ($(lastItemInWrapper).is('.cms-plugin-end')) {
142✔
156
                    wrappers.push([elem]);
1✔
157
                } else {
158
                    lastWrapper.push(elem);
141✔
159
                }
160

161
                return wrappers;
142✔
162
            }, [[]]);
163

164
            // then we map that structure into an array of jquery collections
165
            // from which we filter out empty ones
166
            contents = contentWrappers
140✔
167
                .map(items => {
168
                    var templateStart = $(items[0]);
141✔
169
                    var className = templateStart.attr('class').replace('cms-plugin-start', '');
141✔
170

171
                    var itemContents = $(nextUntil(templateStart[0], container));
141✔
172

173
                    $(items).filter('template').remove();
141✔
174

175
                    itemContents.each((index, el) => {
141✔
176
                        // Due to the way browsers interact with plugins and external code, the .data()
177
                        // method cannot be used on <object> (unless it's a Flash plugin), <applet> or <embed> elements,
178
                        // so we have to wrap them
179
                        if (includes(['OBJECT', 'EMBED', 'APPLET'], el.nodeName)) {
164✔
180
                            const element = $(el);
1✔
181

182
                            element.wrap('<cms-plugin class="cms-plugin-object-node"></cms-plugin>');
1✔
183
                            itemContents[index] = element.parent()[0];
1✔
184
                        }
185

186
                        // if it's a non-space top-level text node - wrap it in `cms-plugin`
187
                        if (el.nodeType === Node.TEXT_NODE && !el.textContent.match(/^\s*$/)) {
164✔
188
                            const element = $(el);
10✔
189

190
                            element.wrap('<cms-plugin class="cms-plugin-text-node"></cms-plugin>');
10✔
191
                            itemContents[index] = element.parent()[0];
10✔
192
                        }
193
                    });
194

195
                    // otherwise we don't really need text nodes or comment nodes or empty text nodes
196
                    itemContents = itemContents.filter(function() {
141✔
197
                        return this.nodeType !== Node.TEXT_NODE && this.nodeType !== Node.COMMENT_NODE;
164✔
198
                    });
199

200
                    itemContents.addClass(`cms-plugin ${className}`);
141✔
201

202
                    return itemContents;
141✔
203
                })
204
                .filter(v => v.length);
141✔
205

206
            if (contents.length) {
140!
207
                // and then reduce it to one big collection
208
                contents = contents.reduce((collection, items) => collection.add(items), $());
141✔
209
            }
210
        } else {
211
            contents = wrapper;
44✔
212
        }
213

214
        // in clipboard can be non-existent
215
        if (!contents.length) {
184✔
216
            contents = $('<div></div>');
11✔
217
        }
218

219
        this.ui = this.ui || {};
184✔
220
        this.ui.container = contents;
184✔
221
    },
222

223
    /**
224
     * Sets up behaviours and ui for placeholder.
225
     *
226
     * @method _setPlaceholder
227
     * @private
228
     */
229
    _setPlaceholder: function() {
230
        var that = this;
24✔
231

232
        this.ui.dragbar = $('.cms-dragbar-' + this.options.placeholder_id);
24✔
233
        this.ui.draggables = this.ui.dragbar.closest('.cms-dragarea').find('> .cms-draggables');
24✔
234
        this.ui.submenu = this.ui.dragbar.find('.cms-submenu-settings');
24✔
235
        var title = this.ui.dragbar.find('.cms-dragbar-title');
24✔
236
        var togglerLinks = this.ui.dragbar.find('.cms-dragbar-toggler a');
24✔
237
        var expanded = 'cms-dragbar-title-expanded';
24✔
238

239
        // register the subnav on the placeholder
240
        this._setSettingsMenu(this.ui.submenu);
24✔
241
        this._setAddPluginModal(this.ui.dragbar.find('.cms-submenu-add'));
24✔
242

243
        // istanbul ignore next
244
        CMS.settings.dragbars = CMS.settings.dragbars || []; // expanded dragbars array
245

246
        // enable expanding/collapsing globally within the placeholder
247
        togglerLinks.off(Plugin.click).on(Plugin.click, function(e) {
24✔
248
            e.preventDefault();
×
249
            if (title.hasClass(expanded)) {
×
250
                that._collapseAll(title);
×
251
            } else {
252
                that._expandAll(title);
×
253
            }
254
        });
255

256
        if ($.inArray(this.options.placeholder_id, CMS.settings.dragbars) !== -1) {
24!
257
            title.addClass(expanded);
×
258
        }
259

260
        this._checkIfPasteAllowed();
24✔
261
    },
262

263
    /**
264
     * Sets up behaviours and ui for plugin.
265
     *
266
     * @method _setPlugin
267
     * @private
268
     */
269
    _setPlugin: function() {
270
        if (isStructureReady()) {
133!
271
            this._setPluginStructureEvents();
133✔
272
        }
273
        if (isContentReady()) {
133!
274
            this._setPluginContentEvents();
133✔
275
        }
276
    },
277

278
    _setPluginStructureEvents: function _setPluginStructureEvents() {
279
        var that = this;
133✔
280

281
        // filling up ui object
282
        this.ui.draggable = $('.cms-draggable-' + this.options.plugin_id);
133✔
283
        this.ui.dragitem = this.ui.draggable.find('> .cms-dragitem');
133✔
284
        this.ui.draggables = this.ui.draggable.find('> .cms-draggables');
133✔
285
        this.ui.submenu = this.ui.dragitem.find('.cms-submenu');
133✔
286

287
        this.ui.draggable.data('cms', this.options);
133✔
288

289
        this.ui.dragitem.on(Plugin.doubleClick, this._dblClickToEditHandler.bind(this));
133✔
290

291
        // adds listener for all plugin updates
292
        this.ui.draggable.off('cms-plugins-update').on('cms-plugins-update', function(e, eventData) {
133✔
293
            e.stopPropagation();
×
294
            that.movePlugin(null, eventData);
×
295
        });
296

297
        // adds listener for copy/paste updates
298
        this.ui.draggable.off('cms-paste-plugin-update').on('cms-paste-plugin-update', function(e, eventData) {
133✔
299
            e.stopPropagation();
5✔
300

301
            var dragitem = $(`.cms-draggable-${eventData.id}:last`);
5✔
302

303
            // find out new placeholder id
304
            var placeholder_id = that._getId(dragitem.closest('.cms-dragarea'));
5✔
305

306
            // if placeholder_id is empty, cancel
307
            if (!placeholder_id) {
5!
308
                return false;
×
309
            }
310

311
            var data = dragitem.data('cms');
5✔
312

313
            data.target = placeholder_id;
5✔
314
            data.parent = that._getId(dragitem.parent().closest('.cms-draggable'));
5✔
315
            data.move_a_copy = true;
5✔
316

317
            // expand the plugin we paste to
318
            CMS.settings.states.push(data.parent);
5✔
319
            Helpers.setSettings(CMS.settings);
5✔
320

321
            that.movePlugin(data);
5✔
322
        });
323

324
        setTimeout(() => {
133✔
325
            this.ui.dragitem
133✔
326
                .on('mouseenter', e => {
327
                    e.stopPropagation();
×
328
                    if (!$document.data('expandmode')) {
×
329
                        return;
×
330
                    }
331
                    if (this.ui.draggable.find('> .cms-dragitem > .cms-plugin-disabled').length) {
×
332
                        return;
×
333
                    }
334
                    if (!CMS.API.StructureBoard.ui.container.hasClass('cms-structure-condensed')) {
×
335
                        return;
×
336
                    }
337
                    if (CMS.API.StructureBoard.dragging) {
×
338
                        return;
×
339
                    }
340
                    // eslint-disable-next-line no-magic-numbers
341
                    Plugin._highlightPluginContent(this.options.plugin_id, { successTimeout: 0, seeThrough: true });
×
342
                })
343
                .on('mouseleave', e => {
344
                    if (!CMS.API.StructureBoard.ui.container.hasClass('cms-structure-condensed')) {
×
345
                        return;
×
346
                    }
347
                    e.stopPropagation();
×
348
                    // eslint-disable-next-line no-magic-numbers
349
                    Plugin._removeHighlightPluginContent(this.options.plugin_id);
×
350
                });
351
            // attach event to the plugin menu
352
            this._setSettingsMenu(this.ui.submenu);
133✔
353

354
            // attach events for the "Add plugin" modal
355
            this._setAddPluginModal(this.ui.dragitem.find('.cms-submenu-add'));
133✔
356

357
            // clickability of "Paste" menu item
358
            this._checkIfPasteAllowed();
133✔
359
        });
360
    },
361

362
    _dblClickToEditHandler: function _dblClickToEditHandler(e) {
363
        var that = this;
×
364

365
        e.preventDefault();
×
366
        e.stopPropagation();
×
367

368
        that.editPlugin(
×
369
            Helpers.updateUrlWithPath(that.options.urls.edit_plugin),
370
            that.options.plugin_name,
371
            that._getPluginBreadcrumbs()
372
        );
373
    },
374

375
    _setPluginContentEvents: function _setPluginContentEvents() {
376
        const pluginDoubleClickEvent = this._getNamepacedEvent(Plugin.doubleClick);
133✔
377

378
        this.ui.container
133✔
379
            .off('mouseover.cms.plugins')
380
            .on('mouseover.cms.plugins', e => {
381
                if (!$document.data('expandmode')) {
×
382
                    return;
×
383
                }
384
                if (CMS.settings.mode !== 'structure') {
×
385
                    return;
×
386
                }
387
                e.stopPropagation();
×
388
                $('.cms-dragitem-success').remove();
×
389
                $('.cms-draggable-success').removeClass('cms-draggable-success');
×
390
                CMS.API.StructureBoard._showAndHighlightPlugin(0, true); // eslint-disable-line no-magic-numbers
×
391
            })
392
            .off('mouseout.cms.plugins')
393
            .on('mouseout.cms.plugins', e => {
394
                if (CMS.settings.mode !== 'structure') {
×
395
                    return;
×
396
                }
397
                e.stopPropagation();
×
398
                if (this.ui.draggable && this.ui.draggable.length) {
×
399
                    this.ui.draggable.find('.cms-dragitem-success').remove();
×
400
                    this.ui.draggable.removeClass('cms-draggable-success');
×
401
                }
402
                // Plugin._removeHighlightPluginContent(this.options.plugin_id);
403
            });
404

405
        if (!Plugin._isContainingMultiplePlugins(this.ui.container)) {
133✔
406
            $document
132✔
407
                .off(pluginDoubleClickEvent, `.cms-plugin-${this.options.plugin_id}`)
408
                .on(
409
                    pluginDoubleClickEvent,
410
                    `.cms-plugin-${this.options.plugin_id}`,
411
                    this._dblClickToEditHandler.bind(this)
412
                );
413
        }
414
    },
415

416
    /**
417
     * Sets up behaviours and ui for generics.
418
     * Generics do not show up in structure board.
419
     *
420
     * @method _setGeneric
421
     * @private
422
     */
423
    _setGeneric: function() {
424
        var that = this;
25✔
425

426
        // adds double click to edit
427
        this.ui.container.off(Plugin.doubleClick).on(Plugin.doubleClick, function(e) {
25✔
428
            e.preventDefault();
×
429
            e.stopPropagation();
×
430
            that.editPlugin(Helpers.updateUrlWithPath(that.options.urls.edit_plugin), that.options.plugin_name, []);
×
431
        });
432

433
        // adds edit tooltip
434
        this.ui.container
25✔
435
            .off(Plugin.pointerOverAndOut + ' ' + Plugin.touchStart)
436
            .on(Plugin.pointerOverAndOut + ' ' + Plugin.touchStart, function(e) {
437
                if (e.type !== 'touchstart') {
×
438
                    e.stopPropagation();
×
439
                }
440
                var name = that.options.plugin_name;
×
441
                var id = that.options.plugin_id;
×
442

443
                CMS.API.Tooltip.displayToggle(e.type === 'pointerover' || e.type === 'touchstart', e, name, id);
×
444
            });
445
    },
446

447
    /**
448
     * Checks if paste is allowed into current plugin/placeholder based
449
     * on restrictions we have. Also determines which tooltip to show.
450
     *
451
     * WARNING: this relies on clipboard plugins always being instantiated
452
     * first, so they have data('cms') by the time this method is called.
453
     *
454
     * @method _checkIfPasteAllowed
455
     * @private
456
     * @returns {Boolean}
457
     */
458
    _checkIfPasteAllowed: function _checkIfPasteAllowed() {
459
        var pasteButton = this.ui.dropdown.find('[data-rel=paste]');
155✔
460
        var pasteItem = pasteButton.parent();
155✔
461

462
        if (!clipboardDraggable.length) {
155✔
463
            pasteItem.addClass('cms-submenu-item-disabled');
89✔
464
            pasteItem.find('a').attr('tabindex', '-1').attr('aria-disabled', 'true');
89✔
465
            pasteItem.find('.cms-submenu-item-paste-tooltip-empty').css('display', 'block');
89✔
466
            return false;
89✔
467
        }
468

469
        if (this.ui.draggable && this.ui.draggable.hasClass('cms-draggable-disabled')) {
66✔
470
            pasteItem.addClass('cms-submenu-item-disabled');
46✔
471
            pasteItem.find('a').attr('tabindex', '-1').attr('aria-disabled', 'true');
46✔
472
            pasteItem.find('.cms-submenu-item-paste-tooltip-disabled').css('display', 'block');
46✔
473
            return false;
46✔
474
        }
475

476
        var bounds = this.options.plugin_restriction;
20✔
477

478
        if (clipboardDraggable.data('cms')) {
20!
479
            var clipboardPluginData = clipboardDraggable.data('cms');
20✔
480
            var type = clipboardPluginData.plugin_type;
20✔
481
            var parent_bounds = $.grep(clipboardPluginData.plugin_parent_restriction, function(restriction) {
20✔
482
                // special case when PlaceholderPlugin has a parent restriction named "0"
483
                return restriction !== '0';
20✔
484
            });
485
            var currentPluginType = this.options.plugin_type;
20✔
486

487
            if (
20✔
488
                (bounds.length && $.inArray(type, bounds) === -1) ||
60!
489
                (parent_bounds.length && $.inArray(currentPluginType, parent_bounds) === -1)
490
            ) {
491
                pasteItem.addClass('cms-submenu-item-disabled');
15✔
492
                pasteItem.find('a').attr('tabindex', '-1').attr('aria-disabled', 'true');
15✔
493
                pasteItem.find('.cms-submenu-item-paste-tooltip-restricted').css('display', 'block');
15✔
494
                return false;
15✔
495
            }
496
        } else {
497
            return false;
×
498
        }
499

500
        pasteItem.find('a').removeAttr('tabindex').removeAttr('aria-disabled');
5✔
501
        pasteItem.removeClass('cms-submenu-item-disabled');
5✔
502

503
        return true;
5✔
504
    },
505

506
    /**
507
     * Calls api to create a plugin and then proceeds to edit it.
508
     *
509
     * @method addPlugin
510
     * @param {String} type type of the plugin, e.g "Bootstrap3ColumnCMSPlugin"
511
     * @param {String} name name of the plugin, e.g. "Column"
512
     * @param {String} parent id of a parent plugin
513
     */
514
    addPlugin: function(type, name, parent) {
515
        var params = {
3✔
516
            placeholder_id: this.options.placeholder_id,
517
            plugin_type: type,
518
            cms_path: path,
519
            plugin_language: CMS.config.request.language
520
        };
521

522
        if (parent) {
3✔
523
            params.plugin_parent = parent;
1✔
524
        }
525
        var url = this.options.urls.add_plugin + '?' + $.param(params);
3✔
526
        var modal = new Modal({
3✔
527
            onClose: this.options.onClose || false,
5✔
528
            redirectOnClose: this.options.redirectOnClose || false
5✔
529
        });
530

531
        modal.open({
3✔
532
            url: url,
533
            title: name
534
        });
535

536
        this.modal = modal;
3✔
537

538
        Helpers.removeEventListener('modal-closed.add-plugin');
3✔
539
        Helpers.addEventListener('modal-closed.add-plugin', (e, { instance }) => {
3✔
540
            if (instance !== modal) {
1!
541
                return;
×
542
            }
543
            Plugin._removeAddPluginPlaceholder();
1✔
544
        });
545
    },
546

547
    /**
548
     * Opens the modal for editing a plugin.
549
     *
550
     * @method editPlugin
551
     * @param {String} url editing url
552
     * @param {String} name Name of the plugin, e.g. "Column"
553
     * @param {Object[]} breadcrumb array of objects representing a breadcrumb,
554
     *     each item is `{ title: 'string': url: 'string' }`
555
     */
556
    editPlugin: function(url, name, breadcrumb) {
557
        // trigger modal window
558
        var modal = new Modal({
3✔
559
            onClose: this.options.onClose || false,
6✔
560
            redirectOnClose: this.options.redirectOnClose || false
6✔
561
        });
562

563
        this.modal = modal;
3✔
564

565
        Helpers.removeEventListener('modal-closed.edit-plugin modal-loaded.edit-plugin');
3✔
566
        Helpers.addEventListener('modal-closed.edit-plugin modal-loaded.edit-plugin', (e, { instance }) => {
3✔
567
            if (instance === modal) {
1!
568
                // cannot be cached
569
                Plugin._removeAddPluginPlaceholder();
1✔
570
            }
571
        });
572
        modal.open({
3✔
573
            url: url,
574
            title: name,
575
            breadcrumbs: breadcrumb
576
        });
577
    },
578

579
    /**
580
     * Used for copying _and_ pasting a plugin. If either of params
581
     * is present method assumes that it's "paste" and will make a call
582
     * to api to insert current plugin to specified `options.target_plugin_id`
583
     * or `options.target_placeholder_id`. Copying a plugin also first
584
     * clears the clipboard.
585
     *
586
     * @method copyPlugin
587
     * @param {Object} [opts=this.options]
588
     * @param {String} source_language
589
     * @returns {Boolean|void}
590
     */
591
    // eslint-disable-next-line complexity
592
    copyPlugin: function(opts, source_language) {
593
        // cancel request if already in progress
594
        if (CMS.API.locked) {
9✔
595
            return false;
1✔
596
        }
597
        CMS.API.locked = true;
8✔
598

599
        // set correct options (don't mutate them)
600
        var options = $.extend({}, opts || this.options);
8✔
601
        var sourceLanguage = source_language;
8✔
602
        let copyingFromLanguage = false;
8✔
603

604
        if (sourceLanguage) {
8✔
605
            copyingFromLanguage = true;
1✔
606
            options.target = options.placeholder_id;
1✔
607
            options.plugin_id = '';
1✔
608
            options.parent = '';
1✔
609
        } else {
610
            sourceLanguage = CMS.config.request.language;
7✔
611
        }
612

613
        var data = {
8✔
614
            source_placeholder_id: options.placeholder_id,
615
            source_plugin_id: options.plugin_id || '',
9✔
616
            source_language: sourceLanguage,
617
            target_plugin_id: options.parent || '',
16✔
618
            target_placeholder_id: options.target || CMS.config.clipboard.id,
15✔
619
            csrfmiddlewaretoken: CMS.config.csrf,
620
            target_language: CMS.config.request.language
621
        };
622
        var request = {
8✔
623
            type: 'POST',
624
            url: Helpers.updateUrlWithPath(options.urls.copy_plugin),
625
            data: data,
626
            success: function(response) {
627
                CMS.API.Messages.open({
2✔
628
                    message: CMS.config.lang.success
629
                });
630
                if (copyingFromLanguage) {
2!
631
                    CMS.API.StructureBoard.invalidateState('PASTE', $.extend({}, data, response));
×
632
                } else {
633
                    CMS.API.StructureBoard.invalidateState('COPY', response);
2✔
634
                }
635
                CMS.API.locked = false;
2✔
636
                hideLoader();
2✔
637
            },
638
            error: function(jqXHR) {
639
                CMS.API.locked = false;
3✔
640
                var msg = CMS.config.lang.error;
3✔
641

642
                // trigger error
643
                CMS.API.Messages.open({
3✔
644
                    message: msg + jqXHR.responseText || jqXHR.status + ' ' + jqXHR.statusText,
4✔
645
                    error: true
646
                });
647
            }
648
        };
649

650
        $.ajax(request);
8✔
651
    },
652

653
    /**
654
     * Essentially clears clipboard and moves plugin to a clipboard
655
     * placholder through `movePlugin`.
656
     *
657
     * @method cutPlugin
658
     * @returns {Boolean|void}
659
     */
660
    cutPlugin: function() {
661
        // if cut is once triggered, prevent additional actions
662
        if (CMS.API.locked) {
9✔
663
            return false;
1✔
664
        }
665
        CMS.API.locked = true;
8✔
666

667
        var that = this;
8✔
668
        var data = {
8✔
669
            placeholder_id: CMS.config.clipboard.id,
670
            plugin_id: this.options.plugin_id,
671
            plugin_parent: '',
672
            plugin_order: [this.options.plugin_id],
673
            target_language: CMS.config.request.language,
674
            csrfmiddlewaretoken: CMS.config.csrf
675
        };
676

677
        // move plugin
678
        $.ajax({
8✔
679
            type: 'POST',
680
            url: Helpers.updateUrlWithPath(that.options.urls.move_plugin),
681
            data: data,
682
            success: function(response) {
683
                CMS.API.locked = false;
4✔
684
                CMS.API.Messages.open({
4✔
685
                    message: CMS.config.lang.success
686
                });
687
                CMS.API.StructureBoard.invalidateState('CUT', $.extend({}, data, response));
4✔
688
                hideLoader();
4✔
689
            },
690
            error: function(jqXHR) {
691
                CMS.API.locked = false;
3✔
692
                var msg = CMS.config.lang.error;
3✔
693

694
                // trigger error
695
                CMS.API.Messages.open({
3✔
696
                    message: msg + jqXHR.responseText || jqXHR.status + ' ' + jqXHR.statusText,
4✔
697
                    error: true
698
                });
699
                hideLoader();
3✔
700
            }
701
        });
702
    },
703

704
    /**
705
     * Method is called when you click on the paste button on the plugin.
706
     * Uses existing solution of `copyPlugin(options)`
707
     *
708
     * @method pastePlugin
709
     */
710
    pastePlugin: function() {
711
        var id = this._getId(clipboardDraggable);
5✔
712
        var eventData = {
5✔
713
            id: id
714
        };
715

716
        const clipboardDraggableClone = clipboardDraggable.clone(true, true);
5✔
717

718
        clipboardDraggableClone.appendTo(this.ui.draggables);
5✔
719
        if (this.options.plugin_id) {
5✔
720
            StructureBoard.actualizePluginCollapseStatus(this.options.plugin_id);
4✔
721
        }
722
        this.ui.draggables.trigger('cms-structure-update', [eventData]);
5✔
723
        clipboardDraggableClone.trigger('cms-paste-plugin-update', [eventData]);
5✔
724
    },
725

726
    /**
727
     * Moves plugin by querying the API and then updates some UI parts
728
     * to reflect that the page has changed.
729
     *
730
     * @method movePlugin
731
     * @param {Object} [opts=this.options]
732
     * @param {String} [opts.placeholder_id]
733
     * @param {String} [opts.plugin_id]
734
     * @param {String} [opts.plugin_parent]
735
     * @param {Boolean} [opts.move_a_copy]
736
     * @returns {Boolean|void}
737
     */
738
    movePlugin: function(opts) {
739
        // cancel request if already in progress
740
        if (CMS.API.locked) {
13✔
741
            return false;
1✔
742
        }
743
        CMS.API.locked = true;
12✔
744

745
        // set correct options
746
        var options = opts || this.options;
12✔
747

748
        var dragitem = $(`.cms-draggable-${options.plugin_id}:last`);
12✔
749

750
        // SAVING POSITION
751
        var placeholder_id = this._getId(dragitem.parents('.cms-draggables').last().prevAll('.cms-dragbar').first());
12✔
752

753
        var plugin_parent = this._getId(dragitem.parent().closest('.cms-draggable'));
12✔
754
        var plugin_order = this._getIds(dragitem.siblings('.cms-draggable').andSelf());
12✔
755

756
        if (options.move_a_copy) {
12✔
757
            plugin_order = plugin_order.map(function(pluginId) {
1✔
758
                var id = pluginId;
3✔
759

760
                // correct way would be to check if it's actually a
761
                // pasted plugin and only then replace the id with copy token
762
                // otherwise if we would copy from the same placeholder we would get
763
                // two copy tokens instead of original and a copy.
764
                // it's ok so far, as long as we copy only from clipboard
765
                if (id === options.plugin_id) {
3✔
766
                    id = '__COPY__';
1✔
767
                }
768
                return id;
3✔
769
            });
770
        }
771

772
        // cancel here if we have no placeholder id
773
        if (placeholder_id === false) {
12✔
774
            return false;
1✔
775
        }
776

777
        // gather the data for ajax request
778
        var data = {
11✔
779
            placeholder_id: placeholder_id,
780
            plugin_id: options.plugin_id,
781
            plugin_parent: plugin_parent || '',
22✔
782
            target_language: CMS.config.request.language,
783
            plugin_order: plugin_order,
784
            csrfmiddlewaretoken: CMS.config.csrf,
785
            move_a_copy: options.move_a_copy
786
        };
787

788
        showLoader();
11✔
789

790
        $.ajax({
11✔
791
            type: 'POST',
792
            url: Helpers.updateUrlWithPath(options.urls.move_plugin),
793
            data: data,
794
            success: function(response) {
795
                CMS.API.StructureBoard.invalidateState(
4✔
796
                    data.move_a_copy ? 'PASTE' : 'MOVE',
4!
797
                    $.extend({}, data, response)
798
                );
799

800
                // enable actions again
801
                CMS.API.locked = false;
4✔
802
                hideLoader();
4✔
803
            },
804
            error: function(jqXHR) {
805
                CMS.API.locked = false;
4✔
806
                var msg = CMS.config.lang.error;
4✔
807

808
                // trigger error
809
                CMS.API.Messages.open({
4✔
810
                    message: msg + jqXHR.responseText || jqXHR.status + ' ' + jqXHR.statusText,
5✔
811
                    error: true
812
                });
813
                hideLoader();
4✔
814
            }
815
        });
816
    },
817

818
    /**
819
     * Changes the settings attributes on an initialised plugin.
820
     *
821
     * @method _setSettings
822
     * @param {Object} oldSettings current settings
823
     * @param {Object} newSettings new settings to be applied
824
     * @private
825
     */
826
    _setSettings: function _setSettings(oldSettings, newSettings) {
827
        var settings = $.extend(true, {}, oldSettings, newSettings);
×
828
        var plugin = $('.cms-plugin-' + settings.plugin_id);
×
829
        var draggable = $('.cms-draggable-' + settings.plugin_id);
×
830

831
        // set new setting on instance and plugin data
832
        this.options = settings;
×
833
        if (plugin.length) {
×
834
            var index = plugin.data('cms').findIndex(function(pluginData) {
×
835
                return pluginData.plugin_id === settings.plugin_id;
×
836
            });
837

838
            plugin.each(function() {
×
839
                $(this).data('cms')[index] = settings;
×
840
            });
841
        }
842
        if (draggable.length) {
×
843
            draggable.data('cms', settings);
×
844
        }
845
    },
846

847
    /**
848
     * Opens a modal to delete a plugin.
849
     *
850
     * @method deletePlugin
851
     * @param {String} url admin url for deleting a page
852
     * @param {String} name plugin name, e.g. "Column"
853
     * @param {Object[]} breadcrumb array of objects representing a breadcrumb,
854
     *     each item is `{ title: 'string': url: 'string' }`
855
     */
856
    deletePlugin: function(url, name, breadcrumb) {
857
        // trigger modal window
858
        var modal = new Modal({
2✔
859
            onClose: this.options.onClose || false,
4✔
860
            redirectOnClose: this.options.redirectOnClose || false
4✔
861
        });
862

863
        this.modal = modal;
2✔
864

865
        Helpers.removeEventListener('modal-loaded.delete-plugin');
2✔
866
        Helpers.addEventListener('modal-loaded.delete-plugin', (e, { instance }) => {
2✔
867
            if (instance === modal) {
5✔
868
                Plugin._removeAddPluginPlaceholder();
1✔
869
            }
870
        });
871
        modal.open({
2✔
872
            url: url,
873
            title: name,
874
            breadcrumbs: breadcrumb
875
        });
876
    },
877

878
    /**
879
     * Destroys the current plugin instance removing only the DOM listeners
880
     *
881
     * @method destroy
882
     * @param {Object}  options - destroy config options
883
     * @param {Boolean} options.mustCleanup - if true it will remove also the plugin UI components from the DOM
884
     * @returns {void}
885
     */
886
    destroy(options = {}) {
1✔
887
        const mustCleanup = options.mustCleanup || false;
2✔
888

889
        // close the plugin modal if it was open
890
        if (this.modal) {
2!
891
            this.modal.close();
×
892
            // unsubscribe to all the modal events
893
            this.modal.off();
×
894
        }
895

896
        if (mustCleanup) {
2✔
897
            this.cleanup();
1✔
898
        }
899

900
        // remove event bound to global elements like document or window
901
        $document.off(`.${this.uid}`);
2✔
902
        $window.off(`.${this.uid}`);
2✔
903
    },
904

905
    /**
906
     * Remove the plugin specific ui elements from the DOM
907
     *
908
     * @method cleanup
909
     * @returns {void}
910
     */
911
    cleanup() {
912
        // remove all the plugin UI DOM elements
913
        // notice that $.remove will remove also all the ui specific events
914
        // previously attached to them
915
        Object.keys(this.ui).forEach(el => this.ui[el].remove());
12✔
916
    },
917

918
    /**
919
     * Called after plugin is added through ajax.
920
     *
921
     * @method editPluginPostAjax
922
     * @param {Object} toolbar CMS.API.Toolbar instance (not used)
923
     * @param {Object} response response from server
924
     */
925
    editPluginPostAjax: function(toolbar, response) {
926
        this.editPlugin(Helpers.updateUrlWithPath(response.url), this.options.plugin_name, response.breadcrumb);
1✔
927
    },
928

929
    /**
930
     * _setSettingsMenu sets up event handlers for settings menu.
931
     *
932
     * @method _setSettingsMenu
933
     * @private
934
     * @param {jQuery} nav
935
     */
936
    _setSettingsMenu: function _setSettingsMenu(nav) {
937
        var that = this;
157✔
938

939
        this.ui.dropdown = nav.siblings('.cms-submenu-dropdown-settings');
157✔
940
        var dropdown = this.ui.dropdown;
157✔
941

942
        nav
157✔
943
            .off(Plugin.pointerUp)
944
            .on(Plugin.pointerUp, function(e) {
945
                e.preventDefault();
×
946
                e.stopPropagation();
×
947
                var trigger = $(this);
×
948

949
                if (trigger.hasClass('cms-btn-active')) {
×
950
                    Plugin._hideSettingsMenu(trigger);
×
951
                } else {
952
                    Plugin._hideSettingsMenu();
×
953
                    that._showSettingsMenu(trigger);
×
954
                }
955
            })
956
            .off(Plugin.touchStart)
957
            .on(Plugin.touchStart, function(e) {
958
                // required on some touch devices so
959
                // ui touch punch is not triggering mousemove
960
                // which in turn results in pep triggering pointercancel
961
                e.stopPropagation();
×
962
            });
963

964
        dropdown
157✔
965
            .off(Plugin.mouseEvents)
966
            .on(Plugin.mouseEvents, function(e) {
967
                e.stopPropagation();
×
968
            })
969
            .off(Plugin.touchStart)
970
            .on(Plugin.touchStart, function(e) {
971
                // required for scrolling on mobile
972
                e.stopPropagation();
×
973
            });
974

975
        that._setupActions(nav);
157✔
976
        // prevent propagation
977
        nav
157✔
978
            .on([Plugin.pointerUp, Plugin.pointerDown, Plugin.click, Plugin.doubleClick].join(' '))
979
            .on([Plugin.pointerUp, Plugin.pointerDown, Plugin.click, Plugin.doubleClick].join(' '), function(e) {
980
                e.stopPropagation();
×
981
            });
982

983
        nav
157✔
984
            .siblings('.cms-quicksearch, .cms-submenu-dropdown-settings')
985
            .off([Plugin.pointerUp, Plugin.click, Plugin.doubleClick].join(' '))
986
            .on([Plugin.pointerUp, Plugin.click, Plugin.doubleClick].join(' '), function(e) {
987
                e.stopPropagation();
×
988
            });
989
    },
990

991
    /**
992
     * Simplistic implementation, only scrolls down, only works in structuremode
993
     * and highly depends on the styles of the structureboard to work correctly
994
     *
995
     * @method _scrollToElement
996
     * @private
997
     * @param {jQuery} el element to scroll to
998
     * @param {Object} [opts]
999
     * @param {Number} [opts.duration=200] time to scroll
1000
     * @param {Number} [opts.offset=50] distance in px to the bottom of the screen
1001
     */
1002
    _scrollToElement: function _scrollToElement(el, opts) {
1003
        var DEFAULT_DURATION = 200;
3✔
1004
        var DEFAULT_OFFSET = 50;
3✔
1005
        var duration = opts && opts.duration !== undefined ? opts.duration : DEFAULT_DURATION;
3✔
1006
        var offset = opts && opts.offset !== undefined ? opts.offset : DEFAULT_OFFSET;
3✔
1007
        var scrollable = el.offsetParent();
3✔
1008
        var scrollHeight = $window.height();
3✔
1009
        var scrollTop = scrollable.scrollTop();
3✔
1010
        var elPosition = el.position().top;
3✔
1011
        var elHeight = el.height();
3✔
1012
        var isInViewport = elPosition + elHeight + offset <= scrollHeight;
3✔
1013

1014
        if (!isInViewport) {
3✔
1015
            scrollable.animate(
2✔
1016
                {
1017
                    scrollTop: elPosition + offset + elHeight + scrollTop - scrollHeight
1018
                },
1019
                duration
1020
            );
1021
        }
1022
    },
1023

1024
    /**
1025
     * Opens a modal with traversable plugins list, adds a placeholder to where
1026
     * the plugin will be added.
1027
     *
1028
     * @method _setAddPluginModal
1029
     * @private
1030
     * @param {jQuery} nav modal trigger element
1031
     * @returns {Boolean|void}
1032
     */
1033
    _setAddPluginModal: function _setAddPluginModal(nav) {
1034
        if (nav.hasClass('cms-btn-disabled')) {
157✔
1035
            return false;
89✔
1036
        }
1037
        var that = this;
68✔
1038
        var modal;
1039
        var isTouching;
1040
        var plugins;
1041

1042
        var initModal = once(function initModal() {
68✔
1043
            var placeholder = $(
×
1044
                '<div class="cms-add-plugin-placeholder">' + CMS.config.lang.addPluginPlaceholder + '</div>'
1045
            );
1046
            var dragItem = nav.closest('.cms-dragitem');
×
1047
            var isPlaceholder = !dragItem.length;
×
1048
            var childrenList;
1049

1050
            modal = new Modal({
×
1051
                minWidth: 400,
1052
                minHeight: 400
1053
            });
1054

1055
            if (isPlaceholder) {
×
1056
                childrenList = nav.closest('.cms-dragarea').find('> .cms-draggables');
×
1057
            } else {
1058
                childrenList = nav.closest('.cms-draggable').find('> .cms-draggables');
×
1059
            }
1060

1061
            Helpers.addEventListener('modal-loaded', (e, { instance }) => {
×
1062
                if (instance !== modal) {
×
1063
                    return;
×
1064
                }
1065

1066
                that._setupKeyboardTraversing();
×
1067
                if (childrenList.hasClass('cms-hidden') && !isPlaceholder) {
×
1068
                    that._toggleCollapsable(dragItem);
×
1069
                }
1070
                Plugin._removeAddPluginPlaceholder();
×
1071
                placeholder.appendTo(childrenList);
×
1072
                that._scrollToElement(placeholder);
×
1073
            });
1074

1075
            Helpers.addEventListener('modal-closed', (e, { instance }) => {
×
1076
                if (instance !== modal) {
×
1077
                    return;
×
1078
                }
1079
                Plugin._removeAddPluginPlaceholder();
×
1080
            });
1081

1082
            Helpers.addEventListener('modal-shown', (e, { instance }) => {
×
1083
                if (modal !== instance) {
×
1084
                    return;
×
1085
                }
1086
                var dropdown = $('.cms-modal-markup .cms-plugin-picker');
×
1087

1088
                if (!isTouching) {
×
1089
                    // only focus the field if using mouse
1090
                    // otherwise keyboard pops up
1091
                    dropdown.find('input').trigger('focus');
×
1092
                }
1093
                isTouching = false;
×
1094
            });
1095

1096
            plugins = nav.siblings('.cms-plugin-picker');
×
1097

1098
            that._setupQuickSearch(plugins);
×
1099
        });
1100

1101
        nav
68✔
1102
            .on(Plugin.touchStart, function(e) {
1103
                isTouching = true;
×
1104
                // required on some touch devices so
1105
                // ui touch punch is not triggering mousemove
1106
                // which in turn results in pep triggering pointercancel
1107
                e.stopPropagation();
×
1108
            })
1109
            .on(Plugin.pointerUp, function(e) {
1110
                e.preventDefault();
×
1111
                e.stopPropagation();
×
1112

1113
                Plugin._hideSettingsMenu();
×
1114

1115
                initModal();
×
1116

1117
                // since we don't know exact plugin parent (because dragndrop)
1118
                // we need to know the parent id by the time we open "add plugin" dialog
1119
                var pluginsCopy = that._updateWithMostUsedPlugins(
×
1120
                    plugins
1121
                        .clone(true, true)
1122
                        .data('parentId', that._getId(nav.closest('.cms-draggable')))
1123
                        .append(that._getPossibleChildClasses())
1124
                );
1125

1126
                modal.open({
×
1127
                    title: that.options.addPluginHelpTitle,
1128
                    html: pluginsCopy,
1129
                    width: 530,
1130
                    height: 400
1131
                });
1132
            });
1133

1134
        // prevent propagation
1135
        nav.on([Plugin.pointerUp, Plugin.pointerDown, Plugin.click, Plugin.doubleClick].join(' '), function(e) {
68✔
1136
            e.stopPropagation();
×
1137
        });
1138

1139
        nav
68✔
1140
            .siblings('.cms-quicksearch, .cms-submenu-dropdown')
1141
            .on([Plugin.pointerUp, Plugin.click, Plugin.doubleClick].join(' '), function(e) {
1142
                e.stopPropagation();
×
1143
            });
1144
    },
1145

1146
    _updateWithMostUsedPlugins: function _updateWithMostUsedPlugins(plugins) {
1147
        const items = plugins.find('.cms-submenu-item');
×
1148
        // eslint-disable-next-line no-unused-vars
1149
        const mostUsedPlugins = toPairs(pluginUsageMap).sort(([x, a], [y, b]) => a - b).reverse();
×
1150
        const MAX_MOST_USED_PLUGINS = 5;
×
1151
        let count = 0;
×
1152

1153
        if (items.filter(':not(.cms-submenu-item-title)').length <= MAX_MOST_USED_PLUGINS) {
×
1154
            return plugins;
×
1155
        }
1156

1157
        let ref = plugins.find('.cms-quicksearch');
×
1158

1159
        mostUsedPlugins.forEach(([name]) => {
×
1160
            if (count === MAX_MOST_USED_PLUGINS) {
×
1161
                return;
×
1162
            }
1163
            const item = items.find(`[href=${name}]`);
×
1164

1165
            if (item.length) {
×
1166
                const clone = item.closest('.cms-submenu-item').clone(true, true);
×
1167

1168
                ref.after(clone);
×
1169
                ref = clone;
×
1170
                count += 1;
×
1171
            }
1172
        });
1173

1174
        if (count) {
×
1175
            plugins.find('.cms-quicksearch').after(
×
1176
                $(`<div class="cms-submenu-item cms-submenu-item-title" data-cms-most-used>
1177
                    <span>${CMS.config.lang.mostUsed}</span>
1178
                </div>`)
1179
            );
1180
        }
1181

1182
        return plugins;
×
1183
    },
1184

1185
    /**
1186
     * Returns a specific plugin namespaced event postfixing the plugin uid to it
1187
     * in order to properly manage it via jQuery $.on and $.off
1188
     *
1189
     * @method _getNamepacedEvent
1190
     * @private
1191
     * @param {String} base - plugin event type
1192
     * @param {String} additionalNS - additional namespace (like '.traverse' for example)
1193
     * @returns {String} a specific plugin event
1194
     *
1195
     * @example
1196
     *
1197
     * plugin._getNamepacedEvent(Plugin.click); // 'click.cms.plugin.42'
1198
     * plugin._getNamepacedEvent(Plugin.keyDown, '.traverse'); // 'keydown.cms.plugin.traverse.42'
1199
     */
1200
    _getNamepacedEvent(base, additionalNS = '') {
136✔
1201
        return `${base}${additionalNS ? '.'.concat(additionalNS) : ''}.${this.uid}`;
147✔
1202
    },
1203

1204
    /**
1205
     * Returns available plugin/placeholder child classes markup
1206
     * for "Add plugin" modal
1207
     *
1208
     * @method _getPossibleChildClasses
1209
     * @private
1210
     * @returns {jQuery} "add plugin" menu
1211
     */
1212
    _getPossibleChildClasses: function _getPossibleChildClasses() {
1213
        var that = this;
33✔
1214
        var childRestrictions = this.options.plugin_restriction;
33✔
1215
        // have to check the placeholder every time, since plugin could've been
1216
        // moved as part of another plugin
1217
        var placeholderId = that._getId(that.ui.submenu.closest('.cms-dragarea'));
33✔
1218
        var resultElements = $($('#cms-plugin-child-classes-' + placeholderId).html());
33✔
1219

1220
        if (childRestrictions && childRestrictions.length) {
33✔
1221
            resultElements = resultElements.filter(function() {
29✔
1222
                var item = $(this);
4,727✔
1223

1224
                return (
4,727✔
1225
                    item.hasClass('cms-submenu-item-title') ||
9,106✔
1226
                    childRestrictions.indexOf(item.find('a').attr('href')) !== -1
1227
                );
1228
            });
1229

1230
            resultElements = resultElements.filter(function(index) {
29✔
1231
                var item = $(this);
411✔
1232

1233
                return (
411✔
1234
                    !item.hasClass('cms-submenu-item-title') ||
1,182✔
1235
                    (item.hasClass('cms-submenu-item-title') &&
1236
                        (!resultElements.eq(index + 1).hasClass('cms-submenu-item-title') &&
1237
                            resultElements.eq(index + 1).length))
1238
                );
1239
            });
1240
        }
1241

1242
        resultElements.find('a').on(Plugin.click, e => this._delegate(e));
33✔
1243

1244
        return resultElements;
33✔
1245
    },
1246

1247
    /**
1248
     * Sets up event handlers for quicksearching in the plugin picker.
1249
     *
1250
     * @method _setupQuickSearch
1251
     * @private
1252
     * @param {jQuery} plugins plugins picker element
1253
     */
1254
    _setupQuickSearch: function _setupQuickSearch(plugins) {
1255
        var that = this;
×
1256
        var FILTER_DEBOUNCE_TIMER = 100;
×
1257
        var FILTER_PICK_DEBOUNCE_TIMER = 110;
×
1258

1259
        var handler = debounce(function() {
×
1260
            var input = $(this);
×
1261
            // have to always find the pluginsPicker in the handler
1262
            // because of how we move things into/out of the modal
1263
            var pluginsPicker = input.closest('.cms-plugin-picker');
×
1264

1265
            that._filterPluginsList(pluginsPicker, input);
×
1266
        }, FILTER_DEBOUNCE_TIMER);
1267

1268
        plugins.find('> .cms-quicksearch').find('input').on(Plugin.keyUp, handler).on(
×
1269
            Plugin.keyUp,
1270
            debounce(function(e) {
1271
                var input;
1272
                var pluginsPicker;
1273

1274
                if (e.keyCode === KEYS.ENTER) {
×
1275
                    input = $(this);
×
1276
                    pluginsPicker = input.closest('.cms-plugin-picker');
×
1277
                    pluginsPicker
×
1278
                        .find('.cms-submenu-item')
1279
                        .not('.cms-submenu-item-title')
1280
                        .filter(':visible')
1281
                        .first()
1282
                        .find('> a')
1283
                        .focus()
1284
                        .trigger('click');
1285
                }
1286
            }, FILTER_PICK_DEBOUNCE_TIMER)
1287
        );
1288
    },
1289

1290
    /**
1291
     * Sets up click handlers for various plugin/placeholder items.
1292
     * Items can be anywhere in the plugin dragitem, not only in dropdown.
1293
     *
1294
     * @method _setupActions
1295
     * @private
1296
     * @param {jQuery} nav dropdown trigger with the items
1297
     */
1298
    _setupActions: function _setupActions(nav) {
1299
        var items = '.cms-submenu-edit, .cms-submenu-item a';
167✔
1300
        var parent = nav.parent();
167✔
1301

1302
        parent.find('.cms-submenu-edit').off(Plugin.touchStart).on(Plugin.touchStart, function(e) {
167✔
1303
            // required on some touch devices so
1304
            // ui touch punch is not triggering mousemove
1305
            // which in turn results in pep triggering pointercancel
1306
            e.stopPropagation();
1✔
1307
        });
1308
        parent.find(items).off(Plugin.click).on(Plugin.click, nav, e => this._delegate(e));
167✔
1309
    },
1310

1311
    /**
1312
     * Handler for the "action" items
1313
     *
1314
     * @method _delegate
1315
     * @param {$.Event} e event
1316
     * @private
1317
     */
1318
    // eslint-disable-next-line complexity
1319
    _delegate: function _delegate(e) {
1320
        e.preventDefault();
13✔
1321
        e.stopPropagation();
13✔
1322

1323
        var nav;
1324
        var that = this;
13✔
1325

1326
        if (e.data && e.data.nav) {
13!
1327
            nav = e.data.nav;
×
1328
        }
1329

1330
        // show loader and make sure scroll doesn't jump
1331
        showLoader();
13✔
1332

1333
        var items = '.cms-submenu-edit, .cms-submenu-item a';
13✔
1334
        var el = $(e.target).closest(items);
13✔
1335

1336
        Plugin._hideSettingsMenu(nav);
13✔
1337

1338
        // set switch for subnav entries
1339
        switch (el.attr('data-rel')) {
13!
1340
            // eslint-disable-next-line no-case-declarations
1341
            case 'add':
1342
                const pluginType = el.attr('href').replace('#', '');
2✔
1343

1344
                Plugin._updateUsageCount(pluginType);
2✔
1345
                that.addPlugin(pluginType, el.text(), el.closest('.cms-plugin-picker').data('parentId'));
2✔
1346
                break;
2✔
1347
            case 'ajax_add':
1348
                CMS.API.Toolbar.openAjax({
1✔
1349
                    url: el.attr('href'),
1350
                    post: JSON.stringify(el.data('post')),
1351
                    text: el.data('text'),
1352
                    callback: $.proxy(that.editPluginPostAjax, that),
1353
                    onSuccess: el.data('on-success')
1354
                });
1355
                break;
1✔
1356
            case 'edit':
1357
                that.editPlugin(
1✔
1358
                    Helpers.updateUrlWithPath(that.options.urls.edit_plugin),
1359
                    that.options.plugin_name,
1360
                    that._getPluginBreadcrumbs()
1361
                );
1362
                break;
1✔
1363
            case 'copy-lang':
1364
                that.copyPlugin(that.options, el.attr('data-language'));
1✔
1365
                break;
1✔
1366
            case 'copy':
1367
                if (el.parent().hasClass('cms-submenu-item-disabled')) {
2✔
1368
                    hideLoader();
1✔
1369
                } else {
1370
                    that.copyPlugin();
1✔
1371
                }
1372
                break;
2✔
1373
            case 'cut':
1374
                that.cutPlugin();
1✔
1375
                break;
1✔
1376
            case 'paste':
1377
                hideLoader();
2✔
1378
                if (!el.parent().hasClass('cms-submenu-item-disabled')) {
2✔
1379
                    that.pastePlugin();
1✔
1380
                }
1381
                break;
2✔
1382
            case 'delete':
1383
                that.deletePlugin(
1✔
1384
                    Helpers.updateUrlWithPath(that.options.urls.delete_plugin),
1385
                    that.options.plugin_name,
1386
                    that._getPluginBreadcrumbs()
1387
                );
1388
                break;
1✔
1389
            case 'highlight':
1390
                hideLoader();
×
1391
                // eslint-disable-next-line no-magic-numbers
1392
                window.location.hash = `cms-plugin-${this.options.plugin_id}`;
×
1393
                Plugin._highlightPluginContent(this.options.plugin_id, { seeThrough: true });
×
1394
                e.stopImmediatePropagation();
×
1395
                break;
×
1396
            default:
1397
                hideLoader();
2✔
1398
                CMS.API.Toolbar._delegate(el);
2✔
1399
        }
1400
    },
1401

1402
    /**
1403
     * Sets up keyboard traversing of plugin picker.
1404
     *
1405
     * @method _setupKeyboardTraversing
1406
     * @private
1407
     */
1408
    _setupKeyboardTraversing: function _setupKeyboardTraversing() {
1409
        var dropdown = $('.cms-modal-markup .cms-plugin-picker');
3✔
1410
        const keyDownTraverseEvent = this._getNamepacedEvent(Plugin.keyDown, 'traverse');
3✔
1411

1412
        if (!dropdown.length) {
3✔
1413
            return;
1✔
1414
        }
1415
        // add key events
1416
        $document.off(keyDownTraverseEvent);
2✔
1417
        // istanbul ignore next: not really possible to reproduce focus state in unit tests
1418
        $document.on(keyDownTraverseEvent, function(e) {
1419
            var anchors = dropdown.find('.cms-submenu-item:visible a');
1420
            var index = anchors.index(anchors.filter(':focus'));
1421

1422
            // bind arrow down and tab keys
1423
            if (e.keyCode === KEYS.DOWN || (e.keyCode === KEYS.TAB && !e.shiftKey)) {
1424
                e.preventDefault();
1425
                if (index >= 0 && index < anchors.length - 1) {
1426
                    anchors.eq(index + 1).focus();
1427
                } else {
1428
                    anchors.eq(0).focus();
1429
                }
1430
            }
1431

1432
            // bind arrow up and shift+tab keys
1433
            if (e.keyCode === KEYS.UP || (e.keyCode === KEYS.TAB && e.shiftKey)) {
1434
                e.preventDefault();
1435
                if (anchors.is(':focus')) {
1436
                    anchors.eq(index - 1).focus();
1437
                } else {
1438
                    anchors.eq(anchors.length).focus();
1439
                }
1440
            }
1441
        });
1442
    },
1443

1444
    /**
1445
     * Opens the settings menu for a plugin.
1446
     *
1447
     * @method _showSettingsMenu
1448
     * @private
1449
     * @param {jQuery} nav trigger element
1450
     */
1451
    _showSettingsMenu: function(nav) {
1452
        this._checkIfPasteAllowed();
×
1453

1454
        var dropdown = this.ui.dropdown;
×
1455
        var parents = nav.parentsUntil('.cms-dragarea').last();
×
1456
        var MIN_SCREEN_MARGIN = 10;
×
1457

1458
        nav.addClass('cms-btn-active');
×
1459
        parents.addClass('cms-z-index-9999');
×
1460

1461
        // set visible states
1462
        dropdown.show();
×
1463

1464
        // calculate dropdown positioning
1465
        if (
×
1466
            $window.height() + $window.scrollTop() - nav.offset().top - dropdown.height() <= MIN_SCREEN_MARGIN &&
×
1467
            nav.offset().top - dropdown.height() >= 0
1468
        ) {
1469
            dropdown.removeClass('cms-submenu-dropdown-top').addClass('cms-submenu-dropdown-bottom');
×
1470
        } else {
1471
            dropdown.removeClass('cms-submenu-dropdown-bottom').addClass('cms-submenu-dropdown-top');
×
1472
        }
1473
    },
1474

1475
    /**
1476
     * Filters given plugins list by a query.
1477
     *
1478
     * @method _filterPluginsList
1479
     * @private
1480
     * @param {jQuery} list plugins picker element
1481
     * @param {jQuery} input input, which value to filter plugins with
1482
     * @returns {Boolean|void}
1483
     */
1484
    _filterPluginsList: function _filterPluginsList(list, input) {
1485
        var items = list.find('.cms-submenu-item');
5✔
1486
        var titles = list.find('.cms-submenu-item-title');
5✔
1487
        var query = input.val();
5✔
1488

1489
        // cancel if query is zero
1490
        if (query === '') {
5✔
1491
            items.add(titles).show();
1✔
1492
            return false;
1✔
1493
        }
1494

1495
        var mostRecentItems = list.find('.cms-submenu-item[data-cms-most-used]');
4✔
1496

1497
        mostRecentItems = mostRecentItems.add(mostRecentItems.nextUntil('.cms-submenu-item-title'));
4✔
1498

1499
        var itemsToFilter = items.toArray().map(function(el) {
4✔
1500
            var element = $(el);
72✔
1501

1502
            return {
72✔
1503
                value: element.text(),
1504
                element: element
1505
            };
1506
        });
1507

1508
        var filteredItems = fuzzyFilter(itemsToFilter, query, { key: 'value' });
4✔
1509

1510
        items.hide();
4✔
1511
        filteredItems.forEach(function(item) {
4✔
1512
            item.element.show();
3✔
1513
        });
1514

1515
        // check if a title is matching
1516
        titles.filter(':visible').each(function(index, item) {
4✔
1517
            titles.hide();
1✔
1518
            $(item).nextUntil('.cms-submenu-item-title').show();
1✔
1519
        });
1520

1521
        // always display title of a category
1522
        items.filter(':visible').each(function(index, titleItem) {
4✔
1523
            var item = $(titleItem);
16✔
1524

1525
            if (item.prev().hasClass('cms-submenu-item-title')) {
16✔
1526
                item.prev().show();
2✔
1527
            } else {
1528
                item.prevUntil('.cms-submenu-item-title').last().prev().show();
14✔
1529
            }
1530
        });
1531

1532
        mostRecentItems.hide();
4✔
1533
    },
1534

1535
    /**
1536
     * Toggles collapsable item.
1537
     *
1538
     * @method _toggleCollapsable
1539
     * @private
1540
     * @param {jQuery} el element to toggle
1541
     * @returns {Boolean|void}
1542
     */
1543
    _toggleCollapsable: function toggleCollapsable(el) {
1544
        var that = this;
×
1545
        var id = that._getId(el.parent());
×
1546
        var draggable = el.closest('.cms-draggable');
×
1547
        var items;
1548

1549
        var settings = CMS.settings;
×
1550

1551
        settings.states = settings.states || [];
×
1552

1553
        if (!draggable || !draggable.length) {
×
1554
            return;
×
1555
        }
1556

1557
        // collapsable function and save states
1558
        if (el.hasClass('cms-dragitem-expanded')) {
×
1559
            settings.states.splice($.inArray(id, settings.states), 1);
×
1560
            el
×
1561
                .removeClass('cms-dragitem-expanded')
1562
                .parent()
1563
                .find('> .cms-collapsable-container')
1564
                .addClass('cms-hidden');
1565

1566
            if ($document.data('expandmode')) {
×
1567
                items = draggable.find('.cms-draggable').find('.cms-dragitem-collapsable');
×
1568
                if (!items.length) {
×
1569
                    return false;
×
1570
                }
1571
                items.each(function() {
×
1572
                    var item = $(this);
×
1573

1574
                    if (item.hasClass('cms-dragitem-expanded')) {
×
1575
                        that._toggleCollapsable(item);
×
1576
                    }
1577
                });
1578
            }
1579
        } else {
1580
            settings.states.push(id);
×
1581
            el
×
1582
                .addClass('cms-dragitem-expanded')
1583
                .parent()
1584
                .find('> .cms-collapsable-container')
1585
                .removeClass('cms-hidden');
1586

1587
            if ($document.data('expandmode')) {
×
1588
                items = draggable.find('.cms-draggable').find('.cms-dragitem-collapsable');
×
1589
                if (!items.length) {
×
1590
                    return false;
×
1591
                }
1592
                items.each(function() {
×
1593
                    var item = $(this);
×
1594

1595
                    if (!item.hasClass('cms-dragitem-expanded')) {
×
1596
                        that._toggleCollapsable(item);
×
1597
                    }
1598
                });
1599
            }
1600
        }
1601

1602
        this._updatePlaceholderCollapseState();
×
1603

1604
        // make sure structurboard gets updated after expanding
1605
        $document.trigger('resize.sideframe');
×
1606

1607
        // save settings
1608
        Helpers.setSettings(settings);
×
1609
    },
1610

1611
    _updatePlaceholderCollapseState() {
1612
        if (this.options.type !== 'plugin' || !this.options.placeholder_id) {
×
1613
            return;
×
1614
        }
1615

1616
        const pluginsOfCurrentPlaceholder = CMS._plugins
×
1617
            .filter(([, o]) => o.placeholder_id === this.options.placeholder_id && o.type === 'plugin')
×
1618
            .map(([, o]) => o.plugin_id);
×
1619

1620
        const openedPlugins = CMS.settings.states;
×
1621
        const closedPlugins = difference(pluginsOfCurrentPlaceholder, openedPlugins);
×
1622
        const areAllRemainingPluginsLeafs = every(closedPlugins, id => {
×
1623
            return !find(
×
1624
                CMS._plugins,
1625
                ([, o]) => o.placeholder_id === this.options.placeholder_id && o.plugin_parent === id
×
1626
            );
1627
        });
1628
        const el = $(`.cms-dragarea-${this.options.placeholder_id} .cms-dragbar-title`);
×
1629
        var settings = CMS.settings;
×
1630

1631
        if (areAllRemainingPluginsLeafs) {
×
1632
            // meaning that all plugins in current placeholder are expanded
1633
            el.addClass('cms-dragbar-title-expanded');
×
1634

1635
            settings.dragbars = settings.dragbars || [];
×
1636
            settings.dragbars.push(this.options.placeholder_id);
×
1637
        } else {
1638
            el.removeClass('cms-dragbar-title-expanded');
×
1639

1640
            settings.dragbars = settings.dragbars || [];
×
1641
            settings.dragbars.splice($.inArray(this.options.placeholder_id, settings.states), 1);
×
1642
        }
1643
    },
1644

1645
    /**
1646
     * Sets up collabspable event handlers.
1647
     *
1648
     * @method _collapsables
1649
     * @private
1650
     * @returns {Boolean|void}
1651
     */
1652
    _collapsables: function() {
1653
        // one time setup
1654
        var that = this;
157✔
1655

1656
        this.ui.draggable = $('.cms-draggable-' + this.options.plugin_id);
157✔
1657
        // cancel here if its not a draggable
1658
        if (!this.ui.draggable.length) {
157✔
1659
            return false;
40✔
1660
        }
1661

1662
        var dragitem = this.ui.draggable.find('> .cms-dragitem');
117✔
1663

1664
        // check which button should be shown for collapsemenu
1665
        var els = this.ui.draggable.find('.cms-dragitem-collapsable');
117✔
1666
        var open = els.filter('.cms-dragitem-expanded');
117✔
1667

1668
        if (els.length === open.length && els.length + open.length !== 0) {
117!
1669
            this.ui.draggable.find('.cms-dragbar-title').addClass('cms-dragbar-title-expanded');
×
1670
        }
1671

1672
        // attach events to draggable
1673
        // debounce here required because on some devices click is not triggered,
1674
        // so we consolidate latest click and touch event to run the collapse only once
1675
        dragitem.find('> .cms-dragitem-text').on(
117✔
1676
            Plugin.touchEnd + ' ' + Plugin.click,
1677
            debounce(function() {
1678
                if (!dragitem.hasClass('cms-dragitem-collapsable')) {
×
1679
                    return;
×
1680
                }
1681
                that._toggleCollapsable(dragitem);
×
1682
            }, 0)
1683
        );
1684
    },
1685

1686
    /**
1687
     * Expands all the collapsables in the given placeholder.
1688
     *
1689
     * @method _expandAll
1690
     * @private
1691
     * @param {jQuery} el trigger element that is a child of a placeholder
1692
     * @returns {Boolean|void}
1693
     */
1694
    _expandAll: function(el) {
1695
        var that = this;
×
1696
        var items = el.closest('.cms-dragarea').find('.cms-dragitem-collapsable');
×
1697

1698
        // cancel if there are no items
1699
        if (!items.length) {
×
1700
            return false;
×
1701
        }
1702
        items.each(function() {
×
1703
            var item = $(this);
×
1704

1705
            if (!item.hasClass('cms-dragitem-expanded')) {
×
1706
                that._toggleCollapsable(item);
×
1707
            }
1708
        });
1709

1710
        el.addClass('cms-dragbar-title-expanded');
×
1711

1712
        var settings = CMS.settings;
×
1713

1714
        settings.dragbars = settings.dragbars || [];
×
1715
        settings.dragbars.push(this.options.placeholder_id);
×
1716
        Helpers.setSettings(settings);
×
1717
    },
1718

1719
    /**
1720
     * Collapses all the collapsables in the given placeholder.
1721
     *
1722
     * @method _collapseAll
1723
     * @private
1724
     * @param {jQuery} el trigger element that is a child of a placeholder
1725
     */
1726
    _collapseAll: function(el) {
1727
        var that = this;
×
1728
        var items = el.closest('.cms-dragarea').find('.cms-dragitem-collapsable');
×
1729

1730
        items.each(function() {
×
1731
            var item = $(this);
×
1732

1733
            if (item.hasClass('cms-dragitem-expanded')) {
×
1734
                that._toggleCollapsable(item);
×
1735
            }
1736
        });
1737

1738
        el.removeClass('cms-dragbar-title-expanded');
×
1739

1740
        var settings = CMS.settings;
×
1741

1742
        settings.dragbars = settings.dragbars || [];
×
1743
        settings.dragbars.splice($.inArray(this.options.placeholder_id, settings.states), 1);
×
1744
        Helpers.setSettings(settings);
×
1745
    },
1746

1747
    /**
1748
     * Gets the id of the element, uses CMS.StructureBoard instance.
1749
     *
1750
     * @method _getId
1751
     * @private
1752
     * @param {jQuery} el element to get id from
1753
     * @returns {String}
1754
     */
1755
    _getId: function(el) {
1756
        return CMS.API.StructureBoard.getId(el);
39✔
1757
    },
1758

1759
    /**
1760
     * Gets the ids of the list of elements, uses CMS.StructureBoard instance.
1761
     *
1762
     * @method _getIds
1763
     * @private
1764
     * @param {jQuery} els elements to get id from
1765
     * @returns {String[]}
1766
     */
1767
    _getIds: function(els) {
1768
        return CMS.API.StructureBoard.getIds(els);
12✔
1769
    },
1770

1771
    /**
1772
     * Traverses the registry to find plugin parents
1773
     *
1774
     * @method _getPluginBreadcrumbs
1775
     * @returns {Object[]} array of breadcrumbs in `{ url, title }` format
1776
     * @private
1777
     */
1778
    _getPluginBreadcrumbs: function _getPluginBreadcrumbs() {
1779
        var breadcrumbs = [];
6✔
1780

1781
        breadcrumbs.unshift({
6✔
1782
            title: this.options.plugin_name,
1783
            url: this.options.urls.edit_plugin
1784
        });
1785

1786
        var findParentPlugin = function(id) {
6✔
1787
            return $.grep(CMS._plugins || [], function(pluginOptions) {
6✔
1788
                return pluginOptions[0] === 'cms-plugin-' + id;
10✔
1789
            })[0];
1790
        };
1791

1792
        var id = this.options.plugin_parent;
6✔
1793
        var data;
1794

1795
        while (id && id !== 'None') {
6✔
1796
            data = findParentPlugin(id);
6✔
1797

1798
            if (!data) {
6✔
1799
                break;
1✔
1800
            }
1801

1802
            breadcrumbs.unshift({
5✔
1803
                title: data[1].plugin_name,
1804
                url: data[1].urls.edit_plugin
1805
            });
1806
            id = data[1].plugin_parent;
5✔
1807
        }
1808

1809
        return breadcrumbs;
6✔
1810
    }
1811
});
1812

1813
Plugin.click = 'click.cms.plugin';
1✔
1814
Plugin.pointerUp = 'pointerup.cms.plugin';
1✔
1815
Plugin.pointerDown = 'pointerdown.cms.plugin';
1✔
1816
Plugin.pointerOverAndOut = 'pointerover.cms.plugin pointerout.cms.plugin';
1✔
1817
Plugin.doubleClick = 'dblclick.cms.plugin';
1✔
1818
Plugin.keyUp = 'keyup.cms.plugin';
1✔
1819
Plugin.keyDown = 'keydown.cms.plugin';
1✔
1820
Plugin.mouseEvents = 'mousedown.cms.plugin mousemove.cms.plugin mouseup.cms.plugin';
1✔
1821
Plugin.touchStart = 'touchstart.cms.plugin';
1✔
1822
Plugin.touchEnd = 'touchend.cms.plugin';
1✔
1823

1824
/**
1825
 * Updates plugin data in CMS._plugins / CMS._instances or creates new
1826
 * plugin instances if they didn't exist
1827
 *
1828
 * @method _updateRegistry
1829
 * @private
1830
 * @static
1831
 * @param {Object[]} plugins plugins data
1832
 */
1833
Plugin._updateRegistry = function _updateRegistry(plugins) {
1✔
1834
    plugins.forEach(pluginData => {
×
1835
        const pluginContainer = `cms-plugin-${pluginData.plugin_id}`;
×
1836
        const pluginIndex = findIndex(CMS._plugins, ([pluginStr]) => pluginStr === pluginContainer);
×
1837

1838
        if (pluginIndex === -1) {
×
1839
            CMS._plugins.push([pluginContainer, pluginData]);
×
1840
            CMS._instances.push(new Plugin(pluginContainer, pluginData));
×
1841
        } else {
1842
            Plugin.aliasPluginDuplicatesMap[pluginData.plugin_id] = false;
×
1843
            CMS._plugins[pluginIndex] = [pluginContainer, pluginData];
×
1844
            CMS._instances[pluginIndex] = new Plugin(pluginContainer, pluginData);
×
1845
        }
1846
    });
1847
};
1848

1849
/**
1850
 * Hides the opened settings menu. By default looks for any open ones.
1851
 *
1852
 * @method _hideSettingsMenu
1853
 * @static
1854
 * @private
1855
 * @param {jQuery} [navEl] element representing the subnav trigger
1856
 */
1857
Plugin._hideSettingsMenu = function(navEl) {
1✔
1858
    var nav = navEl || $('.cms-submenu-btn.cms-btn-active');
20✔
1859

1860
    if (!nav.length) {
20!
1861
        return;
20✔
1862
    }
1863
    nav.removeClass('cms-btn-active');
×
1864

1865
    // set correct active state
1866
    nav.closest('.cms-draggable').data('active', false);
×
1867
    $('.cms-z-index-9999').removeClass('cms-z-index-9999');
×
1868

1869
    nav.siblings('.cms-submenu-dropdown').hide();
×
1870
    nav.siblings('.cms-quicksearch').hide();
×
1871
    // reset search
1872
    nav.siblings('.cms-quicksearch').find('input').val('').trigger(Plugin.keyUp).blur();
×
1873

1874
    // reset relativity
1875
    $('.cms-dragbar').css('position', '');
×
1876
};
1877

1878
/**
1879
 * Initialises handlers that affect all plugins and don't make sense
1880
 * in context of each own plugin instance, e.g. listening for a click on a document
1881
 * to hide plugin settings menu should only be applied once, and not every time
1882
 * CMS.Plugin is instantiated.
1883
 *
1884
 * @method _initializeGlobalHandlers
1885
 * @static
1886
 * @private
1887
 */
1888
Plugin._initializeGlobalHandlers = function _initializeGlobalHandlers() {
1✔
1889
    var timer;
1890
    var clickCounter = 0;
6✔
1891

1892
    Plugin._updateClipboard();
6✔
1893

1894
    // Structureboard initialized too late
1895
    setTimeout(function() {
6✔
1896
        var pluginData = {};
6✔
1897
        var html = '';
6✔
1898

1899
        if (clipboardDraggable.length) {
6✔
1900
            pluginData = find(
5✔
1901
                CMS._plugins,
1902
                ([desc]) => desc === `cms-plugin-${CMS.API.StructureBoard.getId(clipboardDraggable)}`
10✔
1903
            )[1];
1904
            html = clipboardDraggable.parent().html();
5✔
1905
        }
1906
        if (CMS.API && CMS.API.Clipboard) {
6!
1907
            CMS.API.Clipboard.populate(html, pluginData);
6✔
1908
        }
1909
    }, 0);
1910

1911
    $document
6✔
1912
        .off(Plugin.pointerUp)
1913
        .off(Plugin.keyDown)
1914
        .off(Plugin.keyUp)
1915
        .off(Plugin.click, '.cms-plugin a, a:has(.cms-plugin), a.cms-plugin')
1916
        .on(Plugin.pointerUp, function() {
1917
            // call it as a static method, because otherwise we trigger it the
1918
            // amount of times CMS.Plugin is instantiated,
1919
            // which does not make much sense.
1920
            Plugin._hideSettingsMenu();
×
1921
        })
1922
        .on(Plugin.keyDown, function(e) {
1923
            if (e.keyCode === KEYS.SHIFT) {
26!
1924
                $document.data('expandmode', true);
×
1925
                try {
×
1926
                    $('.cms-plugin:hover').last().trigger('mouseenter');
×
1927
                    $('.cms-dragitem:hover').last().trigger('mouseenter');
×
1928
                } catch (err) {}
1929
            }
1930
        })
1931
        .on(Plugin.keyUp, function(e) {
1932
            if (e.keyCode === KEYS.SHIFT) {
23!
1933
                $document.data('expandmode', false);
×
1934
                try {
×
1935
                    $(':hover').trigger('mouseleave');
×
1936
                } catch (err) {}
1937
            }
1938
        })
1939
        .on(Plugin.click, '.cms-plugin a, a:has(.cms-plugin), a.cms-plugin', function(e) {
1940
            var DOUBLECLICK_DELAY = 300;
×
1941

1942
            // prevents single click from messing up the edit call
1943
            // don't go to the link if there is custom js attached to it
1944
            // or if it's clicked along with shift, ctrl, cmd
1945
            if (e.shiftKey || e.ctrlKey || e.metaKey || e.isDefaultPrevented()) {
×
1946
                return;
×
1947
            }
1948
            e.preventDefault();
×
1949
            if (++clickCounter === 1) {
×
1950
                timer = setTimeout(function() {
×
1951
                    var anchor = $(e.target).closest('a');
×
1952

1953
                    clickCounter = 0;
×
1954
                    window.open(anchor.attr('href'), anchor.attr('target') || '_self');
×
1955
                }, DOUBLECLICK_DELAY);
1956
            } else {
1957
                clearTimeout(timer);
×
1958
                clickCounter = 0;
×
1959
            }
1960
        });
1961

1962
    // have to delegate here because there might be plugins that
1963
    // have their content replaced by something dynamic. in case that tool
1964
    // copies the classes - double click to edit would still work
1965
    // also - do not try to highlight render_model_blocks, only actual plugins
1966
    $document.on(Plugin.click, '.cms-plugin:not([class*=cms-render-model])', Plugin._clickToHighlightHandler);
6✔
1967
    $document.on(`${Plugin.pointerOverAndOut} ${Plugin.touchStart}`, '.cms-plugin', function(e) {
6✔
1968
        // required for both, click and touch
1969
        // otherwise propagation won't work to the nested plugin
1970

1971
        e.stopPropagation();
×
1972
        const pluginContainer = $(e.target).closest('.cms-plugin');
×
1973
        const allOptions = pluginContainer.data('cms');
×
1974

1975
        if (!allOptions || !allOptions.length) {
×
1976
            return;
×
1977
        }
1978

1979
        const options = allOptions[0];
×
1980

1981
        if (e.type === 'touchstart') {
×
1982
            CMS.API.Tooltip._forceTouchOnce();
×
1983
        }
1984
        var name = options.plugin_name;
×
1985
        var id = options.plugin_id;
×
1986
        var type = options.type;
×
1987

1988
        if (type === 'generic') {
×
1989
            return;
×
1990
        }
1991
        var placeholderId = CMS.API.StructureBoard.getId($(`.cms-draggable-${id}`).closest('.cms-dragarea'));
×
1992
        var placeholder = $('.cms-placeholder-' + placeholderId);
×
1993

1994
        if (placeholder.length && placeholder.data('cms')) {
×
1995
            name = placeholder.data('cms').name + ': ' + name;
×
1996
        }
1997

1998
        CMS.API.Tooltip.displayToggle(e.type === 'pointerover' || e.type === 'touchstart', e, name, id);
×
1999
    });
2000

2001
    $document.on(Plugin.click, '.cms-dragarea-static .cms-dragbar', e => {
6✔
2002
        const placeholder = $(e.target).closest('.cms-dragarea');
×
2003

2004
        if (placeholder.hasClass('cms-dragarea-static-expanded') && e.isDefaultPrevented()) {
×
2005
            return;
×
2006
        }
2007

2008
        placeholder.toggleClass('cms-dragarea-static-expanded');
×
2009
    });
2010

2011
    $window.on('blur.cms', () => {
6✔
2012
        $document.data('expandmode', false);
6✔
2013
    });
2014
};
2015

2016
/**
2017
 * @method _isContainingMultiplePlugins
2018
 * @param {jQuery} node to check
2019
 * @static
2020
 * @private
2021
 * @returns {Boolean}
2022
 */
2023
Plugin._isContainingMultiplePlugins = function _isContainingMultiplePlugins(node) {
1✔
2024
    var currentData = node.data('cms');
133✔
2025

2026
    // istanbul ignore if
2027
    if (!currentData) {
133✔
2028
        throw new Error('Provided node is not a cms plugin.');
2029
    }
2030

2031
    var pluginIds = currentData.map(function(pluginData) {
133✔
2032
        return pluginData.plugin_id;
134✔
2033
    });
2034

2035
    if (pluginIds.length > 1) {
133✔
2036
        // another plugin already lives on the same node
2037
        // this only works because the plugins are rendered from
2038
        // the bottom to the top (leaf to root)
2039
        // meaning the deepest plugin is always first
2040
        return true;
1✔
2041
    }
2042

2043
    return false;
132✔
2044
};
2045

2046
/**
2047
 * Shows and immediately fades out a success notification (when
2048
 * plugin was successfully moved.
2049
 *
2050
 * @method _highlightPluginStructure
2051
 * @private
2052
 * @static
2053
 * @param {jQuery} el draggable element
2054
 */
2055
// eslint-disable-next-line no-magic-numbers
2056
Plugin._highlightPluginStructure = function _highlightPluginStructure(
1✔
2057
    el,
2058
    // eslint-disable-next-line no-magic-numbers
2059
    { successTimeout = 200, delay = 1500, seeThrough = false }
×
2060
) {
2061
    const tpl = $(`
×
2062
        <div class="cms-dragitem-success ${seeThrough ? 'cms-plugin-overlay-see-through' : ''}">
×
2063
        </div>
2064
    `);
2065

2066
    el.addClass('cms-draggable-success').append(tpl);
×
2067
    // start animation
2068
    if (successTimeout) {
×
2069
        setTimeout(() => {
×
2070
            tpl.fadeOut(successTimeout, function() {
×
2071
                $(this).remove();
×
2072
                el.removeClass('cms-draggable-success');
×
2073
            });
2074
        }, delay);
2075
    }
2076
    // make sure structurboard gets updated after success
2077
    $(Helpers._getWindow()).trigger('resize.sideframe');
×
2078
};
2079

2080
/**
2081
 * Highlights plugin in content mode
2082
 *
2083
 * @method _highlightPluginContent
2084
 * @private
2085
 * @static
2086
 * @param {String|Number} pluginId
2087
 */
2088
Plugin._highlightPluginContent = function _highlightPluginContent(
1✔
2089
    pluginId,
2090
    // eslint-disable-next-line no-magic-numbers
2091
    { successTimeout = 200, seeThrough = false, delay = 1500, prominent = false } = {}
5✔
2092
) {
2093
    var coordinates = {};
1✔
2094
    var positions = [];
1✔
2095
    var OVERLAY_POSITION_TO_WINDOW_HEIGHT_RATIO = 0.2;
1✔
2096

2097
    $('.cms-plugin-' + pluginId).each(function() {
1✔
2098
        var el = $(this);
1✔
2099
        var offset = el.offset();
1✔
2100
        var ml = parseInt(el.css('margin-left'), 10);
1✔
2101
        var mr = parseInt(el.css('margin-right'), 10);
1✔
2102
        var mt = parseInt(el.css('margin-top'), 10);
1✔
2103
        var mb = parseInt(el.css('margin-bottom'), 10);
1✔
2104
        var width = el.outerWidth();
1✔
2105
        var height = el.outerHeight();
1✔
2106

2107
        if (width === 0 && height === 0) {
1!
2108
            return;
×
2109
        }
2110

2111
        if (isNaN(ml)) {
1!
2112
            ml = 0;
×
2113
        }
2114
        if (isNaN(mr)) {
1!
2115
            mr = 0;
×
2116
        }
2117
        if (isNaN(mt)) {
1!
2118
            mt = 0;
×
2119
        }
2120
        if (isNaN(mb)) {
1!
2121
            mb = 0;
×
2122
        }
2123

2124
        positions.push({
1✔
2125
            x1: offset.left - ml,
2126
            x2: offset.left + width + mr,
2127
            y1: offset.top - mt,
2128
            y2: offset.top + height + mb
2129
        });
2130
    });
2131

2132
    if (positions.length === 0) {
1!
2133
        return;
×
2134
    }
2135

2136
    // turns out that offset calculation will be off by toolbar height if
2137
    // position is set to "relative" on html element.
2138
    var html = $('html');
1✔
2139
    var htmlMargin = html.css('position') === 'relative' ? parseInt($('html').css('margin-top'), 10) : 0;
1!
2140

2141
    coordinates.left = Math.min(...positions.map(pos => pos.x1));
1✔
2142
    coordinates.top = Math.min(...positions.map(pos => pos.y1)) - htmlMargin;
1✔
2143
    coordinates.width = Math.max(...positions.map(pos => pos.x2)) - coordinates.left;
1✔
2144
    coordinates.height = Math.max(...positions.map(pos => pos.y2)) - coordinates.top - htmlMargin;
1✔
2145

2146
    $window.scrollTop(coordinates.top - $window.height() * OVERLAY_POSITION_TO_WINDOW_HEIGHT_RATIO);
1✔
2147

2148
    $(
1✔
2149
        `
2150
        <div class="
2151
            cms-plugin-overlay
2152
            cms-dragitem-success
2153
            cms-plugin-overlay-${pluginId}
2154
            ${seeThrough ? 'cms-plugin-overlay-see-through' : ''}
1!
2155
            ${prominent ? 'cms-plugin-overlay-prominent' : ''}
1!
2156
        "
2157
            data-success-timeout="${successTimeout}"
2158
        >
2159
        </div>
2160
    `
2161
    )
2162
        .css(coordinates)
2163
        .css({
2164
            zIndex: 9999
2165
        })
2166
        .appendTo($('body'));
2167

2168
    if (successTimeout) {
1!
2169
        setTimeout(() => {
1✔
2170
            $(`.cms-plugin-overlay-${pluginId}`).fadeOut(successTimeout, function() {
1✔
2171
                $(this).remove();
1✔
2172
            });
2173
        }, delay);
2174
    }
2175
};
2176

2177
Plugin._clickToHighlightHandler = function _clickToHighlightHandler(e) {
1✔
2178
    if (CMS.settings.mode !== 'structure') {
×
2179
        return;
×
2180
    }
2181
    e.preventDefault();
×
2182
    e.stopPropagation();
×
2183
    // FIXME refactor into an object
2184
    CMS.API.StructureBoard._showAndHighlightPlugin(200, true); // eslint-disable-line no-magic-numbers
×
2185
};
2186

2187
Plugin._removeHighlightPluginContent = function(pluginId) {
1✔
2188
    $(`.cms-plugin-overlay-${pluginId}[data-success-timeout=0]`).remove();
×
2189
};
2190

2191
Plugin.aliasPluginDuplicatesMap = {};
1✔
2192
Plugin.staticPlaceholderDuplicatesMap = {};
1✔
2193

2194
// istanbul ignore next
2195
Plugin._initializeTree = function _initializeTree() {
2196
    CMS._plugins = uniqWith(CMS._plugins, ([x], [y]) => x === y);
2197
    CMS._instances = CMS._plugins.map(function(args) {
2198
        return new CMS.Plugin(args[0], args[1]);
2199
    });
2200

2201
    // return the cms plugin instances just created
2202
    return CMS._instances;
2203
};
2204

2205
Plugin._updateClipboard = function _updateClipboard() {
1✔
2206
    clipboardDraggable = $('.cms-draggable-from-clipboard:first');
7✔
2207
};
2208

2209
Plugin._updateUsageCount = function _updateUsageCount(pluginType) {
1✔
2210
    var currentValue = pluginUsageMap[pluginType] || 0;
2✔
2211

2212
    pluginUsageMap[pluginType] = currentValue + 1;
2✔
2213

2214
    if (Helpers._isStorageSupported) {
2!
2215
        localStorage.setItem('cms-plugin-usage', JSON.stringify(pluginUsageMap));
×
2216
    }
2217
};
2218

2219
Plugin._removeAddPluginPlaceholder = function removeAddPluginPlaceholder() {
1✔
2220
    // this can't be cached since they are created and destroyed all over the place
2221
    $('.cms-add-plugin-placeholder').remove();
10✔
2222
};
2223

2224
Plugin._refreshPlugins = function refreshPlugins() {
1✔
2225
    Plugin.aliasPluginDuplicatesMap = {};
4✔
2226
    Plugin.staticPlaceholderDuplicatesMap = {};
4✔
2227
    CMS._plugins = uniqWith(CMS._plugins, isEqual);
4✔
2228

2229
    CMS._instances.forEach(instance => {
4✔
2230
        if (instance.options.type === 'placeholder') {
5✔
2231
            instance._setupUI(`cms-placeholder-${instance.options.placeholder_id}`);
2✔
2232
            instance._ensureData();
2✔
2233
            instance.ui.container.data('cms', instance.options);
2✔
2234
            instance._setPlaceholder();
2✔
2235
        }
2236
    });
2237

2238
    CMS._instances.forEach(instance => {
4✔
2239
        if (instance.options.type === 'plugin') {
5✔
2240
            instance._setupUI(`cms-plugin-${instance.options.plugin_id}`);
2✔
2241
            instance._ensureData();
2✔
2242
            instance.ui.container.data('cms').push(instance.options);
2✔
2243
            instance._setPluginContentEvents();
2✔
2244
        }
2245
    });
2246

2247
    CMS._plugins.forEach(([type, opts]) => {
4✔
2248
        if (opts.type !== 'placeholder' && opts.type !== 'plugin') {
16✔
2249
            const instance = find(
8✔
2250
                CMS._instances,
2251
                i => i.options.type === opts.type && Number(i.options.plugin_id) === Number(opts.plugin_id)
13✔
2252
            );
2253

2254
            if (instance) {
8✔
2255
                // update
2256
                instance._setupUI(type);
1✔
2257
                instance._ensureData();
1✔
2258
                instance.ui.container.data('cms').push(instance.options);
1✔
2259
                instance._setGeneric();
1✔
2260
            } else {
2261
                // create
2262
                CMS._instances.push(new Plugin(type, opts));
7✔
2263
            }
2264
        }
2265
    });
2266
};
2267

2268
// shorthand for jQuery(document).ready();
2269
$(Plugin._initializeGlobalHandlers);
1✔
2270

2271
export default Plugin;
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