Linux websever 5.15.0-153-generic #163-Ubuntu SMP Thu Aug 7 16:37:18 UTC 2025 x86_64
Apache/2.4.52 (Ubuntu)
: 192.168.3.70 | : 192.168.1.99
Cant Read [ /etc/named.conf ]
8.1.2-1ubuntu2.23
urlab
www.github.com/MadExploits
Terminal
AUTO ROOT
Adminer
Backdoor Destroyer
Linux Exploit
Lock Shell
Lock File
Create User
CREATE RDP
PHP Mailer
BACKCONNECT
UNLOCK SHELL
HASH IDENTIFIER
CPANEL RESET
CREATE WP USER
README
+ Create Folder
+ Create File
/
var /
www /
html /
vicas-dev /
static /
cms /
js /
modules /
[ HOME SHELL ]
Name
Size
Permission
Action
shortcuts
[ DIR ]
drwxr-xr-x
cms.base.js
15.03
KB
-rw-r--r--
cms.changeform.js
1.34
KB
-rw-r--r--
cms.changetracker.js
4.36
KB
-rw-r--r--
cms.clipboard.js
7.44
KB
-rw-r--r--
cms.messages.js
4.1
KB
-rw-r--r--
cms.modal.js
42.13
KB
-rw-r--r--
cms.navigation.js
11.01
KB
-rw-r--r--
cms.pagetree.dropdown.js
3.49
KB
-rw-r--r--
cms.pagetree.js
38.41
KB
-rw-r--r--
cms.pagetree.stickyheader.js
4.85
KB
-rw-r--r--
cms.plugins.js
76.19
KB
-rw-r--r--
cms.sideframe.js
13.79
KB
-rw-r--r--
cms.structureboard.js
52.79
KB
-rw-r--r--
cms.toolbar.js
25.56
KB
-rw-r--r--
cms.tooltip.js
4.64
KB
-rw-r--r--
cms.wizards.js
1.61
KB
-rw-r--r--
dropdown.js
4.72
KB
-rw-r--r--
get-dist-path.js
1.16
KB
-rw-r--r--
jquery.noconflict.post.js
133
B
-rw-r--r--
jquery.noconflict.pre.js
159
B
-rw-r--r--
jquery.transition.js
2.12
KB
-rw-r--r--
jquery.trap.js
6.21
KB
-rw-r--r--
jquery.ui.custom.js
49.58
KB
-rw-r--r--
jquery.ui.nestedsortable.js
26.19
KB
-rw-r--r--
jquery.ui.touchpunch.js
1.26
KB
-rw-r--r--
keyboard.js
826
B
-rw-r--r--
loader.js
1.05
KB
-rw-r--r--
nextuntil.js
808
B
-rw-r--r--
preload-images.js
680
B
-rw-r--r--
scrollbar.js
402
B
-rw-r--r--
slug.js
1.11
KB
-rw-r--r--
tmpl.js
1.51
KB
-rw-r--r--
Delete
Unzip
Zip
${this.title}
Close
Code Editor : cms.plugins.js
/* * Copyright https://github.com/divio/django-cms */ import Modal from './cms.modal'; import StructureBoard from './cms.structureboard'; import $ from 'jquery'; import '../polyfills/array.prototype.findindex'; import nextUntil from './nextuntil'; import { includes, toPairs, isNaN, debounce, findIndex, find, every, uniqWith, once, difference, isEqual } from 'lodash'; import Class from 'classjs'; import { Helpers, KEYS, $window, $document, uid } from './cms.base'; import { showLoader, hideLoader } from './loader'; import { filter as fuzzyFilter } from 'fuzzaldrin'; var clipboardDraggable; var path = window.location.pathname + window.location.search; var pluginUsageMap = Helpers._isStorageSupported ? JSON.parse(localStorage.getItem('cms-plugin-usage') || '{}') : {}; const isStructureReady = () => CMS.config.settings.mode === 'structure' || CMS.config.settings.legacy_mode || CMS.API.StructureBoard._loadedStructure; const isContentReady = () => CMS.config.settings.mode !== 'structure' || CMS.config.settings.legacy_mode || CMS.API.StructureBoard._loadedContent; /** * Class for handling Plugins / Placeholders or Generics. * Handles adding / moving / copying / pasting / menus etc * in structureboard. * * @class Plugin * @namespace CMS * @uses CMS.API.Helpers */ var Plugin = new Class({ implement: [Helpers], options: { type: '', // bar, plugin or generic placeholder_id: null, plugin_type: '', plugin_id: null, plugin_parent: null, plugin_order: null, plugin_restriction: [], plugin_parent_restriction: [], urls: { add_plugin: '', edit_plugin: '', move_plugin: '', copy_plugin: '', delete_plugin: '' } }, // these properties will be filled later modal: null, initialize: function initialize(container, options) { this.options = $.extend(true, {}, this.options, options); // create an unique for this component to use it internally this.uid = uid(); this._setupUI(container); this._ensureData(); if (this.options.type === 'plugin' && Plugin.aliasPluginDuplicatesMap[this.options.plugin_id]) { return; } if (this.options.type === 'placeholder' && Plugin.staticPlaceholderDuplicatesMap[this.options.placeholder_id]) { return; } // determine type of plugin switch (this.options.type) { case 'placeholder': // handler for placeholder bars Plugin.staticPlaceholderDuplicatesMap[this.options.placeholder_id] = true; this.ui.container.data('cms', this.options); this._setPlaceholder(); if (isStructureReady()) { this._collapsables(); } break; case 'plugin': // handler for all plugins this.ui.container.data('cms').push(this.options); Plugin.aliasPluginDuplicatesMap[this.options.plugin_id] = true; this._setPlugin(); if (isStructureReady()) { this._collapsables(); } break; default: // handler for static content this.ui.container.data('cms').push(this.options); this._setGeneric(); } }, _ensureData: function _ensureData() { // bind data element to the container (mutating!) if (!this.ui.container.data('cms')) { this.ui.container.data('cms', []); } }, /** * Caches some jQuery references and sets up structure for * further initialisation. * * @method _setupUI * @private * @param {String} container `cms-plugin-${id}` */ _setupUI: function setupUI(container) { var wrapper = $(`.${container}`); var contents; // have to check for cms-plugin, there can be a case when there are multiple // static placeholders or plugins rendered twice, there could be multiple wrappers on same page if (wrapper.length > 1 && container.match(/cms-plugin/)) { // so it's possible that multiple plugins (more often generics) are rendered // in different places. e.g. page menu in the header and in the footer // so first, we find all the template tags, then put them in a structure like this: // [[start, end], [start, end]...] // // in case of plugins it means that it's aliased plugin or a plugin in a duplicated // static placeholder (for whatever reason) var contentWrappers = wrapper.toArray().reduce((wrappers, elem, index) => { if (index === 0) { wrappers[0].push(elem); return wrappers; } var lastWrapper = wrappers[wrappers.length - 1]; var lastItemInWrapper = lastWrapper[lastWrapper.length - 1]; if ($(lastItemInWrapper).is('.cms-plugin-end')) { wrappers.push([elem]); } else { lastWrapper.push(elem); } return wrappers; }, [[]]); // then we map that structure into an array of jquery collections // from which we filter out empty ones contents = contentWrappers .map(items => { var templateStart = $(items[0]); var className = templateStart.attr('class').replace('cms-plugin-start', ''); var itemContents = $(nextUntil(templateStart[0], container)); $(items).filter('template').remove(); itemContents.each((index, el) => { // Due to the way browsers interact with plugins and external code, the .data() // method cannot be used on <object> (unless it's a Flash plugin), <applet> or <embed> elements, // so we have to wrap them if (includes(['OBJECT', 'EMBED', 'APPLET'], el.nodeName)) { const element = $(el); element.wrap('<cms-plugin class="cms-plugin-object-node"></cms-plugin>'); itemContents[index] = element.parent()[0]; } // if it's a non-space top-level text node - wrap it in `cms-plugin` if (el.nodeType === Node.TEXT_NODE && !el.textContent.match(/^\s*$/)) { const element = $(el); element.wrap('<cms-plugin class="cms-plugin-text-node"></cms-plugin>'); itemContents[index] = element.parent()[0]; } }); // otherwise we don't really need text nodes or comment nodes or empty text nodes itemContents = itemContents.filter(function() { return this.nodeType !== Node.TEXT_NODE && this.nodeType !== Node.COMMENT_NODE; }); itemContents.addClass(`cms-plugin ${className}`); return itemContents; }) .filter(v => v.length); if (contents.length) { // and then reduce it to one big collection contents = contents.reduce((collection, items) => collection.add(items), $()); } } else { contents = wrapper; } // in clipboard can be non-existent if (!contents.length) { contents = $('<div></div>'); } this.ui = this.ui || {}; this.ui.container = contents; }, /** * Sets up behaviours and ui for placeholder. * * @method _setPlaceholder * @private */ _setPlaceholder: function() { var that = this; this.ui.dragbar = $('.cms-dragbar-' + this.options.placeholder_id); this.ui.draggables = this.ui.dragbar.closest('.cms-dragarea').find('> .cms-draggables'); this.ui.submenu = this.ui.dragbar.find('.cms-submenu-settings'); var title = this.ui.dragbar.find('.cms-dragbar-title'); var togglerLinks = this.ui.dragbar.find('.cms-dragbar-toggler a'); var expanded = 'cms-dragbar-title-expanded'; // register the subnav on the placeholder this._setSettingsMenu(this.ui.submenu); this._setAddPluginModal(this.ui.dragbar.find('.cms-submenu-add')); // istanbul ignore next CMS.settings.dragbars = CMS.settings.dragbars || []; // expanded dragbars array // enable expanding/collapsing globally within the placeholder togglerLinks.off(Plugin.click).on(Plugin.click, function(e) { e.preventDefault(); if (title.hasClass(expanded)) { that._collapseAll(title); } else { that._expandAll(title); } }); if ($.inArray(this.options.placeholder_id, CMS.settings.dragbars) !== -1) { title.addClass(expanded); } this._checkIfPasteAllowed(); }, /** * Sets up behaviours and ui for plugin. * * @method _setPlugin * @private */ _setPlugin: function() { if (isStructureReady()) { this._setPluginStructureEvents(); } if (isContentReady()) { this._setPluginContentEvents(); } }, _setPluginStructureEvents: function _setPluginStructureEvents() { var that = this; // filling up ui object this.ui.draggable = $('.cms-draggable-' + this.options.plugin_id); this.ui.dragitem = this.ui.draggable.find('> .cms-dragitem'); this.ui.draggables = this.ui.draggable.find('> .cms-draggables'); this.ui.submenu = this.ui.dragitem.find('.cms-submenu'); this.ui.draggable.data('cms', this.options); this.ui.dragitem.on(Plugin.doubleClick, this._dblClickToEditHandler.bind(this)); // adds listener for all plugin updates this.ui.draggable.off('cms-plugins-update').on('cms-plugins-update', function(e, eventData) { e.stopPropagation(); that.movePlugin(null, eventData); }); // adds listener for copy/paste updates this.ui.draggable.off('cms-paste-plugin-update').on('cms-paste-plugin-update', function(e, eventData) { e.stopPropagation(); var dragitem = $(`.cms-draggable-${eventData.id}:last`); // find out new placeholder id var placeholder_id = that._getId(dragitem.closest('.cms-dragarea')); // if placeholder_id is empty, cancel if (!placeholder_id) { return false; } var data = dragitem.data('cms'); data.target = placeholder_id; data.parent = that._getId(dragitem.parent().closest('.cms-draggable')); data.move_a_copy = true; // expand the plugin we paste to CMS.settings.states.push(data.parent); Helpers.setSettings(CMS.settings); that.movePlugin(data); }); setTimeout(() => { this.ui.dragitem .on('mouseenter', e => { e.stopPropagation(); if (!$document.data('expandmode')) { return; } if (this.ui.draggable.find('> .cms-dragitem > .cms-plugin-disabled').length) { return; } if (!CMS.API.StructureBoard.ui.container.hasClass('cms-structure-condensed')) { return; } if (CMS.API.StructureBoard.dragging) { return; } // eslint-disable-next-line no-magic-numbers Plugin._highlightPluginContent(this.options.plugin_id, { successTimeout: 0, seeThrough: true }); }) .on('mouseleave', e => { if (!CMS.API.StructureBoard.ui.container.hasClass('cms-structure-condensed')) { return; } e.stopPropagation(); // eslint-disable-next-line no-magic-numbers Plugin._removeHighlightPluginContent(this.options.plugin_id); }); // attach event to the plugin menu this._setSettingsMenu(this.ui.submenu); // attach events for the "Add plugin" modal this._setAddPluginModal(this.ui.dragitem.find('.cms-submenu-add')); // clickability of "Paste" menu item this._checkIfPasteAllowed(); }); }, _dblClickToEditHandler: function _dblClickToEditHandler(e) { var that = this; e.preventDefault(); e.stopPropagation(); that.editPlugin( Helpers.updateUrlWithPath(that.options.urls.edit_plugin), that.options.plugin_name, that._getPluginBreadcrumbs() ); }, _setPluginContentEvents: function _setPluginContentEvents() { const pluginDoubleClickEvent = this._getNamepacedEvent(Plugin.doubleClick); this.ui.container .off('mouseover.cms.plugins') .on('mouseover.cms.plugins', e => { if (!$document.data('expandmode')) { return; } if (CMS.settings.mode !== 'structure') { return; } e.stopPropagation(); $('.cms-dragitem-success').remove(); $('.cms-draggable-success').removeClass('cms-draggable-success'); CMS.API.StructureBoard._showAndHighlightPlugin(0, true); // eslint-disable-line no-magic-numbers }) .off('mouseout.cms.plugins') .on('mouseout.cms.plugins', e => { if (CMS.settings.mode !== 'structure') { return; } e.stopPropagation(); if (this.ui.draggable && this.ui.draggable.length) { this.ui.draggable.find('.cms-dragitem-success').remove(); this.ui.draggable.removeClass('cms-draggable-success'); } // Plugin._removeHighlightPluginContent(this.options.plugin_id); }); if (!Plugin._isContainingMultiplePlugins(this.ui.container)) { $document .off(pluginDoubleClickEvent, `.cms-plugin-${this.options.plugin_id}`) .on( pluginDoubleClickEvent, `.cms-plugin-${this.options.plugin_id}`, this._dblClickToEditHandler.bind(this) ); } }, /** * Sets up behaviours and ui for generics. * Generics do not show up in structure board. * * @method _setGeneric * @private */ _setGeneric: function() { var that = this; // adds double click to edit this.ui.container.off(Plugin.doubleClick).on(Plugin.doubleClick, function(e) { e.preventDefault(); e.stopPropagation(); that.editPlugin(Helpers.updateUrlWithPath(that.options.urls.edit_plugin), that.options.plugin_name, []); }); // adds edit tooltip this.ui.container .off(Plugin.pointerOverAndOut + ' ' + Plugin.touchStart) .on(Plugin.pointerOverAndOut + ' ' + Plugin.touchStart, function(e) { if (e.type !== 'touchstart') { e.stopPropagation(); } var name = that.options.plugin_name; var id = that.options.plugin_id; CMS.API.Tooltip.displayToggle(e.type === 'pointerover' || e.type === 'touchstart', e, name, id); }); }, /** * Checks if paste is allowed into current plugin/placeholder based * on restrictions we have. Also determines which tooltip to show. * * WARNING: this relies on clipboard plugins always being instantiated * first, so they have data('cms') by the time this method is called. * * @method _checkIfPasteAllowed * @private * @returns {Boolean} */ _checkIfPasteAllowed: function _checkIfPasteAllowed() { var pasteButton = this.ui.dropdown.find('[data-rel=paste]'); var pasteItem = pasteButton.parent(); if (!clipboardDraggable.length) { pasteItem.addClass('cms-submenu-item-disabled'); pasteItem.find('a').attr('tabindex', '-1').attr('aria-disabled', 'true'); pasteItem.find('.cms-submenu-item-paste-tooltip-empty').css('display', 'block'); return false; } if (this.ui.draggable && this.ui.draggable.hasClass('cms-draggable-disabled')) { pasteItem.addClass('cms-submenu-item-disabled'); pasteItem.find('a').attr('tabindex', '-1').attr('aria-disabled', 'true'); pasteItem.find('.cms-submenu-item-paste-tooltip-disabled').css('display', 'block'); return false; } var bounds = this.options.plugin_restriction; if (clipboardDraggable.data('cms')) { var clipboardPluginData = clipboardDraggable.data('cms'); var type = clipboardPluginData.plugin_type; var parent_bounds = $.grep(clipboardPluginData.plugin_parent_restriction, function(restriction) { // special case when PlaceholderPlugin has a parent restriction named "0" return restriction !== '0'; }); var currentPluginType = this.options.plugin_type; if ( (bounds.length && $.inArray(type, bounds) === -1) || (parent_bounds.length && $.inArray(currentPluginType, parent_bounds) === -1) ) { pasteItem.addClass('cms-submenu-item-disabled'); pasteItem.find('a').attr('tabindex', '-1').attr('aria-disabled', 'true'); pasteItem.find('.cms-submenu-item-paste-tooltip-restricted').css('display', 'block'); return false; } } else { return false; } pasteItem.find('a').removeAttr('tabindex').removeAttr('aria-disabled'); pasteItem.removeClass('cms-submenu-item-disabled'); return true; }, /** * Calls api to create a plugin and then proceeds to edit it. * * @method addPlugin * @param {String} type type of the plugin, e.g "Bootstrap3ColumnCMSPlugin" * @param {String} name name of the plugin, e.g. "Column" * @param {String} parent id of a parent plugin */ addPlugin: function(type, name, parent) { var params = { placeholder_id: this.options.placeholder_id, plugin_type: type, cms_path: path, plugin_language: CMS.config.request.language }; if (parent) { params.plugin_parent = parent; } var url = this.options.urls.add_plugin + '?' + $.param(params); var modal = new Modal({ onClose: this.options.onClose || false, redirectOnClose: this.options.redirectOnClose || false }); modal.open({ url: url, title: name }); this.modal = modal; Helpers.removeEventListener('modal-closed.add-plugin'); Helpers.addEventListener('modal-closed.add-plugin', (e, { instance }) => { if (instance !== modal) { return; } Plugin._removeAddPluginPlaceholder(); }); }, /** * Opens the modal for editing a plugin. * * @method editPlugin * @param {String} url editing url * @param {String} name Name of the plugin, e.g. "Column" * @param {Object[]} breadcrumb array of objects representing a breadcrumb, * each item is `{ title: 'string': url: 'string' }` */ editPlugin: function(url, name, breadcrumb) { // trigger modal window var modal = new Modal({ onClose: this.options.onClose || false, redirectOnClose: this.options.redirectOnClose || false }); this.modal = modal; Helpers.removeEventListener('modal-closed.edit-plugin modal-loaded.edit-plugin'); Helpers.addEventListener('modal-closed.edit-plugin modal-loaded.edit-plugin', (e, { instance }) => { if (instance === modal) { // cannot be cached Plugin._removeAddPluginPlaceholder(); } }); modal.open({ url: url, title: name, breadcrumbs: breadcrumb, width: 850 }); }, /** * Used for copying _and_ pasting a plugin. If either of params * is present method assumes that it's "paste" and will make a call * to api to insert current plugin to specified `options.target_plugin_id` * or `options.target_placeholder_id`. Copying a plugin also first * clears the clipboard. * * @method copyPlugin * @param {Object} [opts=this.options] * @param {String} source_language * @returns {Boolean|void} */ // eslint-disable-next-line complexity copyPlugin: function(opts, source_language) { // cancel request if already in progress if (CMS.API.locked) { return false; } CMS.API.locked = true; // set correct options (don't mutate them) var options = $.extend({}, opts || this.options); var sourceLanguage = source_language; let copyingFromLanguage = false; if (sourceLanguage) { copyingFromLanguage = true; options.target = options.placeholder_id; options.plugin_id = ''; options.parent = ''; } else { sourceLanguage = CMS.config.request.language; } var data = { source_placeholder_id: options.placeholder_id, source_plugin_id: options.plugin_id || '', source_language: sourceLanguage, target_plugin_id: options.parent || '', target_placeholder_id: options.target || CMS.config.clipboard.id, csrfmiddlewaretoken: CMS.config.csrf, target_language: CMS.config.request.language }; var request = { type: 'POST', url: Helpers.updateUrlWithPath(options.urls.copy_plugin), data: data, success: function(response) { CMS.API.Messages.open({ message: CMS.config.lang.success }); if (copyingFromLanguage) { CMS.API.StructureBoard.invalidateState('PASTE', $.extend({}, data, response)); } else { CMS.API.StructureBoard.invalidateState('COPY', response); } CMS.API.locked = false; hideLoader(); }, error: function(jqXHR) { CMS.API.locked = false; var msg = CMS.config.lang.error; // trigger error CMS.API.Messages.open({ message: msg + jqXHR.responseText || jqXHR.status + ' ' + jqXHR.statusText, error: true }); } }; $.ajax(request); }, /** * Essentially clears clipboard and moves plugin to a clipboard * placholder through `movePlugin`. * * @method cutPlugin * @returns {Boolean|void} */ cutPlugin: function() { // if cut is once triggered, prevent additional actions if (CMS.API.locked) { return false; } CMS.API.locked = true; var that = this; var data = { placeholder_id: CMS.config.clipboard.id, plugin_id: this.options.plugin_id, plugin_parent: '', plugin_order: [this.options.plugin_id], target_language: CMS.config.request.language, csrfmiddlewaretoken: CMS.config.csrf }; // move plugin $.ajax({ type: 'POST', url: Helpers.updateUrlWithPath(that.options.urls.move_plugin), data: data, success: function(response) { CMS.API.locked = false; CMS.API.Messages.open({ message: CMS.config.lang.success }); CMS.API.StructureBoard.invalidateState('CUT', $.extend({}, data, response)); hideLoader(); }, error: function(jqXHR) { CMS.API.locked = false; var msg = CMS.config.lang.error; // trigger error CMS.API.Messages.open({ message: msg + jqXHR.responseText || jqXHR.status + ' ' + jqXHR.statusText, error: true }); hideLoader(); } }); }, /** * Method is called when you click on the paste button on the plugin. * Uses existing solution of `copyPlugin(options)` * * @method pastePlugin */ pastePlugin: function() { var id = this._getId(clipboardDraggable); var eventData = { id: id }; const clipboardDraggableClone = clipboardDraggable.clone(true, true); clipboardDraggableClone.appendTo(this.ui.draggables); if (this.options.plugin_id) { StructureBoard.actualizePluginCollapseStatus(this.options.plugin_id); } this.ui.draggables.trigger('cms-structure-update', [eventData]); clipboardDraggableClone.trigger('cms-paste-plugin-update', [eventData]); }, /** * Moves plugin by querying the API and then updates some UI parts * to reflect that the page has changed. * * @method movePlugin * @param {Object} [opts=this.options] * @param {String} [opts.placeholder_id] * @param {String} [opts.plugin_id] * @param {String} [opts.plugin_parent] * @param {Boolean} [opts.move_a_copy] * @returns {Boolean|void} */ movePlugin: function(opts) { // cancel request if already in progress if (CMS.API.locked) { return false; } CMS.API.locked = true; // set correct options var options = opts || this.options; var dragitem = $(`.cms-draggable-${options.plugin_id}:last`); // SAVING POSITION var placeholder_id = this._getId(dragitem.parents('.cms-draggables').last().prevAll('.cms-dragbar').first()); var plugin_parent = this._getId(dragitem.parent().closest('.cms-draggable')); var plugin_order = this._getIds(dragitem.siblings('.cms-draggable').andSelf()); if (options.move_a_copy) { plugin_order = plugin_order.map(function(pluginId) { var id = pluginId; // correct way would be to check if it's actually a // pasted plugin and only then replace the id with copy token // otherwise if we would copy from the same placeholder we would get // two copy tokens instead of original and a copy. // it's ok so far, as long as we copy only from clipboard if (id === options.plugin_id) { id = '__COPY__'; } return id; }); } // cancel here if we have no placeholder id if (placeholder_id === false) { return false; } // gather the data for ajax request var data = { placeholder_id: placeholder_id, plugin_id: options.plugin_id, plugin_parent: plugin_parent || '', target_language: CMS.config.request.language, plugin_order: plugin_order, csrfmiddlewaretoken: CMS.config.csrf, move_a_copy: options.move_a_copy }; showLoader(); $.ajax({ type: 'POST', url: Helpers.updateUrlWithPath(options.urls.move_plugin), data: data, success: function(response) { CMS.API.StructureBoard.invalidateState( data.move_a_copy ? 'PASTE' : 'MOVE', $.extend({}, data, response) ); // enable actions again CMS.API.locked = false; hideLoader(); }, error: function(jqXHR) { CMS.API.locked = false; var msg = CMS.config.lang.error; // trigger error CMS.API.Messages.open({ message: msg + jqXHR.responseText || jqXHR.status + ' ' + jqXHR.statusText, error: true }); hideLoader(); } }); }, /** * Changes the settings attributes on an initialised plugin. * * @method _setSettings * @param {Object} oldSettings current settings * @param {Object} newSettings new settings to be applied * @private */ _setSettings: function _setSettings(oldSettings, newSettings) { var settings = $.extend(true, {}, oldSettings, newSettings); var plugin = $('.cms-plugin-' + settings.plugin_id); var draggable = $('.cms-draggable-' + settings.plugin_id); // set new setting on instance and plugin data this.options = settings; if (plugin.length) { var index = plugin.data('cms').findIndex(function(pluginData) { return pluginData.plugin_id === settings.plugin_id; }); plugin.each(function() { $(this).data('cms')[index] = settings; }); } if (draggable.length) { draggable.data('cms', settings); } }, /** * Opens a modal to delete a plugin. * * @method deletePlugin * @param {String} url admin url for deleting a page * @param {String} name plugin name, e.g. "Column" * @param {Object[]} breadcrumb array of objects representing a breadcrumb, * each item is `{ title: 'string': url: 'string' }` */ deletePlugin: function(url, name, breadcrumb) { // trigger modal window var modal = new Modal({ onClose: this.options.onClose || false, redirectOnClose: this.options.redirectOnClose || false }); this.modal = modal; Helpers.removeEventListener('modal-loaded.delete-plugin'); Helpers.addEventListener('modal-loaded.delete-plugin', (e, { instance }) => { if (instance === modal) { Plugin._removeAddPluginPlaceholder(); } }); modal.open({ url: url, title: name, breadcrumbs: breadcrumb }); }, /** * Destroys the current plugin instance removing only the DOM listeners * * @method destroy * @param {Object} options - destroy config options * @param {Boolean} options.mustCleanup - if true it will remove also the plugin UI components from the DOM * @returns {void} */ destroy(options = {}) { const mustCleanup = options.mustCleanup || false; // close the plugin modal if it was open if (this.modal) { this.modal.close(); // unsubscribe to all the modal events this.modal.off(); } if (mustCleanup) { this.cleanup(); } // remove event bound to global elements like document or window $document.off(`.${this.uid}`); $window.off(`.${this.uid}`); }, /** * Remove the plugin specific ui elements from the DOM * * @method cleanup * @returns {void} */ cleanup() { // remove all the plugin UI DOM elements // notice that $.remove will remove also all the ui specific events // previously attached to them Object.keys(this.ui).forEach(el => this.ui[el].remove()); }, /** * Called after plugin is added through ajax. * * @method editPluginPostAjax * @param {Object} toolbar CMS.API.Toolbar instance (not used) * @param {Object} response response from server */ editPluginPostAjax: function(toolbar, response) { this.editPlugin(Helpers.updateUrlWithPath(response.url), this.options.plugin_name, response.breadcrumb); }, /** * _setSettingsMenu sets up event handlers for settings menu. * * @method _setSettingsMenu * @private * @param {jQuery} nav */ _setSettingsMenu: function _setSettingsMenu(nav) { var that = this; this.ui.dropdown = nav.siblings('.cms-submenu-dropdown-settings'); var dropdown = this.ui.dropdown; nav .off(Plugin.pointerUp) .on(Plugin.pointerUp, function(e) { e.preventDefault(); e.stopPropagation(); var trigger = $(this); if (trigger.hasClass('cms-btn-active')) { Plugin._hideSettingsMenu(trigger); } else { Plugin._hideSettingsMenu(); that._showSettingsMenu(trigger); } }) .off(Plugin.touchStart) .on(Plugin.touchStart, function(e) { // required on some touch devices so // ui touch punch is not triggering mousemove // which in turn results in pep triggering pointercancel e.stopPropagation(); }); dropdown .off(Plugin.mouseEvents) .on(Plugin.mouseEvents, function(e) { e.stopPropagation(); }) .off(Plugin.touchStart) .on(Plugin.touchStart, function(e) { // required for scrolling on mobile e.stopPropagation(); }); that._setupActions(nav); // prevent propagation nav .on([Plugin.pointerUp, Plugin.pointerDown, Plugin.click, Plugin.doubleClick].join(' ')) .on([Plugin.pointerUp, Plugin.pointerDown, Plugin.click, Plugin.doubleClick].join(' '), function(e) { e.stopPropagation(); }); nav .siblings('.cms-quicksearch, .cms-submenu-dropdown-settings') .off([Plugin.pointerUp, Plugin.click, Plugin.doubleClick].join(' ')) .on([Plugin.pointerUp, Plugin.click, Plugin.doubleClick].join(' '), function(e) { e.stopPropagation(); }); }, /** * Simplistic implementation, only scrolls down, only works in structuremode * and highly depends on the styles of the structureboard to work correctly * * @method _scrollToElement * @private * @param {jQuery} el element to scroll to * @param {Object} [opts] * @param {Number} [opts.duration=200] time to scroll * @param {Number} [opts.offset=50] distance in px to the bottom of the screen */ _scrollToElement: function _scrollToElement(el, opts) { var DEFAULT_DURATION = 200; var DEFAULT_OFFSET = 50; var duration = opts && opts.duration !== undefined ? opts.duration : DEFAULT_DURATION; var offset = opts && opts.offset !== undefined ? opts.offset : DEFAULT_OFFSET; var scrollable = el.offsetParent(); var scrollHeight = $window.height(); var scrollTop = scrollable.scrollTop(); var elPosition = el.position().top; var elHeight = el.height(); var isInViewport = elPosition + elHeight + offset <= scrollHeight; if (!isInViewport) { scrollable.animate( { scrollTop: elPosition + offset + elHeight + scrollTop - scrollHeight }, duration ); } }, /** * Opens a modal with traversable plugins list, adds a placeholder to where * the plugin will be added. * * @method _setAddPluginModal * @private * @param {jQuery} nav modal trigger element * @returns {Boolean|void} */ _setAddPluginModal: function _setAddPluginModal(nav) { if (nav.hasClass('cms-btn-disabled')) { return false; } var that = this; var modal; var isTouching; var plugins; var initModal = once(function initModal() { var placeholder = $( '<div class="cms-add-plugin-placeholder">' + CMS.config.lang.addPluginPlaceholder + '</div>' ); var dragItem = nav.closest('.cms-dragitem'); var isPlaceholder = !dragItem.length; var childrenList; modal = new Modal({ minWidth: 400, minHeight: 400 }); if (isPlaceholder) { childrenList = nav.closest('.cms-dragarea').find('> .cms-draggables'); } else { childrenList = nav.closest('.cms-draggable').find('> .cms-draggables'); } Helpers.addEventListener('modal-loaded', (e, { instance }) => { if (instance !== modal) { return; } that._setupKeyboardTraversing(); if (childrenList.hasClass('cms-hidden') && !isPlaceholder) { that._toggleCollapsable(dragItem); } Plugin._removeAddPluginPlaceholder(); placeholder.appendTo(childrenList); that._scrollToElement(placeholder); }); Helpers.addEventListener('modal-closed', (e, { instance }) => { if (instance !== modal) { return; } Plugin._removeAddPluginPlaceholder(); }); Helpers.addEventListener('modal-shown', (e, { instance }) => { if (modal !== instance) { return; } var dropdown = $('.cms-modal-markup .cms-plugin-picker'); if (!isTouching) { // only focus the field if using mouse // otherwise keyboard pops up dropdown.find('input').trigger('focus'); } isTouching = false; }); plugins = nav.siblings('.cms-plugin-picker'); that._setupQuickSearch(plugins); }); nav .on(Plugin.touchStart, function(e) { isTouching = true; // required on some touch devices so // ui touch punch is not triggering mousemove // which in turn results in pep triggering pointercancel e.stopPropagation(); }) .on(Plugin.pointerUp, function(e) { e.preventDefault(); e.stopPropagation(); Plugin._hideSettingsMenu(); initModal(); // since we don't know exact plugin parent (because dragndrop) // we need to know the parent id by the time we open "add plugin" dialog var pluginsCopy = that._updateWithMostUsedPlugins( plugins .clone(true, true) .data('parentId', that._getId(nav.closest('.cms-draggable'))) .append(that._getPossibleChildClasses()) ); modal.open({ title: that.options.addPluginHelpTitle, html: pluginsCopy, width: 530, height: 400 }); }); // prevent propagation nav.on([Plugin.pointerUp, Plugin.pointerDown, Plugin.click, Plugin.doubleClick].join(' '), function(e) { e.stopPropagation(); }); nav .siblings('.cms-quicksearch, .cms-submenu-dropdown') .on([Plugin.pointerUp, Plugin.click, Plugin.doubleClick].join(' '), function(e) { e.stopPropagation(); }); }, _updateWithMostUsedPlugins: function _updateWithMostUsedPlugins(plugins) { const items = plugins.find('.cms-submenu-item'); // eslint-disable-next-line no-unused-vars const mostUsedPlugins = toPairs(pluginUsageMap).sort(([x, a], [y, b]) => a - b).reverse(); const MAX_MOST_USED_PLUGINS = 5; let count = 0; if (items.filter(':not(.cms-submenu-item-title)').length <= MAX_MOST_USED_PLUGINS) { return plugins; } let ref = plugins.find('.cms-quicksearch'); mostUsedPlugins.forEach(([name]) => { if (count === MAX_MOST_USED_PLUGINS) { return; } const item = items.find(`[href=${name}]`); if (item.length) { const clone = item.closest('.cms-submenu-item').clone(true, true); ref.after(clone); ref = clone; count += 1; } }); if (count) { plugins.find('.cms-quicksearch').after( $(`<div class="cms-submenu-item cms-submenu-item-title" data-cms-most-used> <span>${CMS.config.lang.mostUsed}</span> </div>`) ); } return plugins; }, /** * Returns a specific plugin namespaced event postfixing the plugin uid to it * in order to properly manage it via jQuery $.on and $.off * * @method _getNamepacedEvent * @private * @param {String} base - plugin event type * @param {String} additionalNS - additional namespace (like '.traverse' for example) * @returns {String} a specific plugin event * * @example * * plugin._getNamepacedEvent(Plugin.click); // 'click.cms.plugin.42' * plugin._getNamepacedEvent(Plugin.keyDown, '.traverse'); // 'keydown.cms.plugin.traverse.42' */ _getNamepacedEvent(base, additionalNS = '') { return `${base}${additionalNS ? '.'.concat(additionalNS) : ''}.${this.uid}`; }, /** * Returns available plugin/placeholder child classes markup * for "Add plugin" modal * * @method _getPossibleChildClasses * @private * @returns {jQuery} "add plugin" menu */ _getPossibleChildClasses: function _getPossibleChildClasses() { var that = this; var childRestrictions = this.options.plugin_restriction; // have to check the placeholder every time, since plugin could've been // moved as part of another plugin var placeholderId = that._getId(that.ui.submenu.closest('.cms-dragarea')); var resultElements = $($('#cms-plugin-child-classes-' + placeholderId).html()); if (childRestrictions && childRestrictions.length) { resultElements = resultElements.filter(function() { var item = $(this); return ( item.hasClass('cms-submenu-item-title') || childRestrictions.indexOf(item.find('a').attr('href')) !== -1 ); }); resultElements = resultElements.filter(function(index) { var item = $(this); return ( !item.hasClass('cms-submenu-item-title') || (item.hasClass('cms-submenu-item-title') && (!resultElements.eq(index + 1).hasClass('cms-submenu-item-title') && resultElements.eq(index + 1).length)) ); }); } resultElements.find('a').on(Plugin.click, e => this._delegate(e)); return resultElements; }, /** * Sets up event handlers for quicksearching in the plugin picker. * * @method _setupQuickSearch * @private * @param {jQuery} plugins plugins picker element */ _setupQuickSearch: function _setupQuickSearch(plugins) { var that = this; var FILTER_DEBOUNCE_TIMER = 100; var FILTER_PICK_DEBOUNCE_TIMER = 110; var handler = debounce(function() { var input = $(this); // have to always find the pluginsPicker in the handler // because of how we move things into/out of the modal var pluginsPicker = input.closest('.cms-plugin-picker'); that._filterPluginsList(pluginsPicker, input); }, FILTER_DEBOUNCE_TIMER); plugins.find('> .cms-quicksearch').find('input').on(Plugin.keyUp, handler).on( Plugin.keyUp, debounce(function(e) { var input; var pluginsPicker; if (e.keyCode === KEYS.ENTER) { input = $(this); pluginsPicker = input.closest('.cms-plugin-picker'); pluginsPicker .find('.cms-submenu-item') .not('.cms-submenu-item-title') .filter(':visible') .first() .find('> a') .focus() .trigger('click'); } }, FILTER_PICK_DEBOUNCE_TIMER) ); }, /** * Sets up click handlers for various plugin/placeholder items. * Items can be anywhere in the plugin dragitem, not only in dropdown. * * @method _setupActions * @private * @param {jQuery} nav dropdown trigger with the items */ _setupActions: function _setupActions(nav) { var items = '.cms-submenu-edit, .cms-submenu-item a'; var parent = nav.parent(); parent.find('.cms-submenu-edit').off(Plugin.touchStart).on(Plugin.touchStart, function(e) { // required on some touch devices so // ui touch punch is not triggering mousemove // which in turn results in pep triggering pointercancel e.stopPropagation(); }); parent.find(items).off(Plugin.click).on(Plugin.click, nav, e => this._delegate(e)); }, /** * Handler for the "action" items * * @method _delegate * @param {$.Event} e event * @private */ // eslint-disable-next-line complexity _delegate: function _delegate(e) { e.preventDefault(); e.stopPropagation(); var nav; var that = this; if (e.data && e.data.nav) { nav = e.data.nav; } // show loader and make sure scroll doesn't jump showLoader(); var items = '.cms-submenu-edit, .cms-submenu-item a'; var el = $(e.target).closest(items); Plugin._hideSettingsMenu(nav); // set switch for subnav entries switch (el.attr('data-rel')) { // eslint-disable-next-line no-case-declarations case 'add': const pluginType = el.attr('href').replace('#', ''); Plugin._updateUsageCount(pluginType); that.addPlugin(pluginType, el.text(), el.closest('.cms-plugin-picker').data('parentId')); break; case 'ajax_add': CMS.API.Toolbar.openAjax({ url: el.attr('href'), post: JSON.stringify(el.data('post')), text: el.data('text'), callback: $.proxy(that.editPluginPostAjax, that), onSuccess: el.data('on-success') }); break; case 'edit': that.editPlugin( Helpers.updateUrlWithPath(that.options.urls.edit_plugin), that.options.plugin_name, that._getPluginBreadcrumbs() ); break; case 'copy-lang': that.copyPlugin(that.options, el.attr('data-language')); break; case 'copy': if (el.parent().hasClass('cms-submenu-item-disabled')) { hideLoader(); } else { that.copyPlugin(); } break; case 'cut': that.cutPlugin(); break; case 'paste': hideLoader(); if (!el.parent().hasClass('cms-submenu-item-disabled')) { that.pastePlugin(); } break; case 'delete': that.deletePlugin( Helpers.updateUrlWithPath(that.options.urls.delete_plugin), that.options.plugin_name, that._getPluginBreadcrumbs() ); break; case 'highlight': hideLoader(); // eslint-disable-next-line no-magic-numbers window.location.hash = `cms-plugin-${this.options.plugin_id}`; Plugin._highlightPluginContent(this.options.plugin_id, { seeThrough: true }); e.stopImmediatePropagation(); break; default: hideLoader(); CMS.API.Toolbar._delegate(el); } }, /** * Sets up keyboard traversing of plugin picker. * * @method _setupKeyboardTraversing * @private */ _setupKeyboardTraversing: function _setupKeyboardTraversing() { var dropdown = $('.cms-modal-markup .cms-plugin-picker'); const keyDownTraverseEvent = this._getNamepacedEvent(Plugin.keyDown, 'traverse'); if (!dropdown.length) { return; } // add key events $document.off(keyDownTraverseEvent); // istanbul ignore next: not really possible to reproduce focus state in unit tests $document.on(keyDownTraverseEvent, function(e) { var anchors = dropdown.find('.cms-submenu-item:visible a'); var index = anchors.index(anchors.filter(':focus')); // bind arrow down and tab keys if (e.keyCode === KEYS.DOWN || (e.keyCode === KEYS.TAB && !e.shiftKey)) { e.preventDefault(); if (index >= 0 && index < anchors.length - 1) { anchors.eq(index + 1).focus(); } else { anchors.eq(0).focus(); } } // bind arrow up and shift+tab keys if (e.keyCode === KEYS.UP || (e.keyCode === KEYS.TAB && e.shiftKey)) { e.preventDefault(); if (anchors.is(':focus')) { anchors.eq(index - 1).focus(); } else { anchors.eq(anchors.length).focus(); } } }); }, /** * Opens the settings menu for a plugin. * * @method _showSettingsMenu * @private * @param {jQuery} nav trigger element */ _showSettingsMenu: function(nav) { this._checkIfPasteAllowed(); var dropdown = this.ui.dropdown; var parents = nav.parentsUntil('.cms-dragarea').last(); var MIN_SCREEN_MARGIN = 10; nav.addClass('cms-btn-active'); parents.addClass('cms-z-index-9999'); // set visible states dropdown.show(); // calculate dropdown positioning if ( $window.height() + $window.scrollTop() - nav.offset().top - dropdown.height() <= MIN_SCREEN_MARGIN && nav.offset().top - dropdown.height() >= 0 ) { dropdown.removeClass('cms-submenu-dropdown-top').addClass('cms-submenu-dropdown-bottom'); } else { dropdown.removeClass('cms-submenu-dropdown-bottom').addClass('cms-submenu-dropdown-top'); } }, /** * Filters given plugins list by a query. * * @method _filterPluginsList * @private * @param {jQuery} list plugins picker element * @param {jQuery} input input, which value to filter plugins with * @returns {Boolean|void} */ _filterPluginsList: function _filterPluginsList(list, input) { var items = list.find('.cms-submenu-item'); var titles = list.find('.cms-submenu-item-title'); var query = input.val(); // cancel if query is zero if (query === '') { items.add(titles).show(); return false; } var mostRecentItems = list.find('.cms-submenu-item[data-cms-most-used]'); mostRecentItems = mostRecentItems.add(mostRecentItems.nextUntil('.cms-submenu-item-title')); var itemsToFilter = items.toArray().map(function(el) { var element = $(el); return { value: element.text(), element: element }; }); var filteredItems = fuzzyFilter(itemsToFilter, query, { key: 'value' }); items.hide(); filteredItems.forEach(function(item) { item.element.show(); }); // check if a title is matching titles.filter(':visible').each(function(index, item) { titles.hide(); $(item).nextUntil('.cms-submenu-item-title').show(); }); // always display title of a category items.filter(':visible').each(function(index, titleItem) { var item = $(titleItem); if (item.prev().hasClass('cms-submenu-item-title')) { item.prev().show(); } else { item.prevUntil('.cms-submenu-item-title').last().prev().show(); } }); mostRecentItems.hide(); }, /** * Toggles collapsable item. * * @method _toggleCollapsable * @private * @param {jQuery} el element to toggle * @returns {Boolean|void} */ _toggleCollapsable: function toggleCollapsable(el) { var that = this; var id = that._getId(el.parent()); var draggable = el.closest('.cms-draggable'); var items; var settings = CMS.settings; settings.states = settings.states || []; if (!draggable || !draggable.length) { return; } // collapsable function and save states if (el.hasClass('cms-dragitem-expanded')) { settings.states.splice($.inArray(id, settings.states), 1); el .removeClass('cms-dragitem-expanded') .parent() .find('> .cms-collapsable-container') .addClass('cms-hidden'); if ($document.data('expandmode')) { items = draggable.find('.cms-draggable').find('.cms-dragitem-collapsable'); if (!items.length) { return false; } items.each(function() { var item = $(this); if (item.hasClass('cms-dragitem-expanded')) { that._toggleCollapsable(item); } }); } } else { settings.states.push(id); el .addClass('cms-dragitem-expanded') .parent() .find('> .cms-collapsable-container') .removeClass('cms-hidden'); if ($document.data('expandmode')) { items = draggable.find('.cms-draggable').find('.cms-dragitem-collapsable'); if (!items.length) { return false; } items.each(function() { var item = $(this); if (!item.hasClass('cms-dragitem-expanded')) { that._toggleCollapsable(item); } }); } } this._updatePlaceholderCollapseState(); // make sure structurboard gets updated after expanding $document.trigger('resize.sideframe'); // save settings Helpers.setSettings(settings); }, _updatePlaceholderCollapseState() { if (this.options.type !== 'plugin' || !this.options.placeholder_id) { return; } const pluginsOfCurrentPlaceholder = CMS._plugins .filter(([, o]) => o.placeholder_id === this.options.placeholder_id && o.type === 'plugin') .map(([, o]) => o.plugin_id); const openedPlugins = CMS.settings.states; const closedPlugins = difference(pluginsOfCurrentPlaceholder, openedPlugins); const areAllRemainingPluginsLeafs = every(closedPlugins, id => { return !find( CMS._plugins, ([, o]) => o.placeholder_id === this.options.placeholder_id && o.plugin_parent === id ); }); const el = $(`.cms-dragarea-${this.options.placeholder_id} .cms-dragbar-title`); var settings = CMS.settings; if (areAllRemainingPluginsLeafs) { // meaning that all plugins in current placeholder are expanded el.addClass('cms-dragbar-title-expanded'); settings.dragbars = settings.dragbars || []; settings.dragbars.push(this.options.placeholder_id); } else { el.removeClass('cms-dragbar-title-expanded'); settings.dragbars = settings.dragbars || []; settings.dragbars.splice($.inArray(this.options.placeholder_id, settings.states), 1); } }, /** * Sets up collabspable event handlers. * * @method _collapsables * @private * @returns {Boolean|void} */ _collapsables: function() { // one time setup var that = this; this.ui.draggable = $('.cms-draggable-' + this.options.plugin_id); // cancel here if its not a draggable if (!this.ui.draggable.length) { return false; } var dragitem = this.ui.draggable.find('> .cms-dragitem'); // check which button should be shown for collapsemenu var els = this.ui.draggable.find('.cms-dragitem-collapsable'); var open = els.filter('.cms-dragitem-expanded'); if (els.length === open.length && els.length + open.length !== 0) { this.ui.draggable.find('.cms-dragbar-title').addClass('cms-dragbar-title-expanded'); } // attach events to draggable // debounce here required because on some devices click is not triggered, // so we consolidate latest click and touch event to run the collapse only once dragitem.find('> .cms-dragitem-text').on( Plugin.touchEnd + ' ' + Plugin.click, debounce(function() { if (!dragitem.hasClass('cms-dragitem-collapsable')) { return; } that._toggleCollapsable(dragitem); }, 0) ); }, /** * Expands all the collapsables in the given placeholder. * * @method _expandAll * @private * @param {jQuery} el trigger element that is a child of a placeholder * @returns {Boolean|void} */ _expandAll: function(el) { var that = this; var items = el.closest('.cms-dragarea').find('.cms-dragitem-collapsable'); // cancel if there are no items if (!items.length) { return false; } items.each(function() { var item = $(this); if (!item.hasClass('cms-dragitem-expanded')) { that._toggleCollapsable(item); } }); el.addClass('cms-dragbar-title-expanded'); var settings = CMS.settings; settings.dragbars = settings.dragbars || []; settings.dragbars.push(this.options.placeholder_id); Helpers.setSettings(settings); }, /** * Collapses all the collapsables in the given placeholder. * * @method _collapseAll * @private * @param {jQuery} el trigger element that is a child of a placeholder */ _collapseAll: function(el) { var that = this; var items = el.closest('.cms-dragarea').find('.cms-dragitem-collapsable'); items.each(function() { var item = $(this); if (item.hasClass('cms-dragitem-expanded')) { that._toggleCollapsable(item); } }); el.removeClass('cms-dragbar-title-expanded'); var settings = CMS.settings; settings.dragbars = settings.dragbars || []; settings.dragbars.splice($.inArray(this.options.placeholder_id, settings.states), 1); Helpers.setSettings(settings); }, /** * Gets the id of the element, uses CMS.StructureBoard instance. * * @method _getId * @private * @param {jQuery} el element to get id from * @returns {String} */ _getId: function(el) { return CMS.API.StructureBoard.getId(el); }, /** * Gets the ids of the list of elements, uses CMS.StructureBoard instance. * * @method _getIds * @private * @param {jQuery} els elements to get id from * @returns {String[]} */ _getIds: function(els) { return CMS.API.StructureBoard.getIds(els); }, /** * Traverses the registry to find plugin parents * * @method _getPluginBreadcrumbs * @returns {Object[]} array of breadcrumbs in `{ url, title }` format * @private */ _getPluginBreadcrumbs: function _getPluginBreadcrumbs() { var breadcrumbs = []; breadcrumbs.unshift({ title: this.options.plugin_name, url: this.options.urls.edit_plugin }); var findParentPlugin = function(id) { return $.grep(CMS._plugins || [], function(pluginOptions) { return pluginOptions[0] === 'cms-plugin-' + id; })[0]; }; var id = this.options.plugin_parent; var data; while (id && id !== 'None') { data = findParentPlugin(id); if (!data) { break; } breadcrumbs.unshift({ title: data[1].plugin_name, url: data[1].urls.edit_plugin }); id = data[1].plugin_parent; } return breadcrumbs; } }); Plugin.click = 'click.cms.plugin'; Plugin.pointerUp = 'pointerup.cms.plugin'; Plugin.pointerDown = 'pointerdown.cms.plugin'; Plugin.pointerOverAndOut = 'pointerover.cms.plugin pointerout.cms.plugin'; Plugin.doubleClick = 'dblclick.cms.plugin'; Plugin.keyUp = 'keyup.cms.plugin'; Plugin.keyDown = 'keydown.cms.plugin'; Plugin.mouseEvents = 'mousedown.cms.plugin mousemove.cms.plugin mouseup.cms.plugin'; Plugin.touchStart = 'touchstart.cms.plugin'; Plugin.touchEnd = 'touchend.cms.plugin'; /** * Updates plugin data in CMS._plugins / CMS._instances or creates new * plugin instances if they didn't exist * * @method _updateRegistry * @private * @static * @param {Object[]} plugins plugins data */ Plugin._updateRegistry = function _updateRegistry(plugins) { plugins.forEach(pluginData => { const pluginContainer = `cms-plugin-${pluginData.plugin_id}`; const pluginIndex = findIndex(CMS._plugins, ([pluginStr]) => pluginStr === pluginContainer); if (pluginIndex === -1) { CMS._plugins.push([pluginContainer, pluginData]); CMS._instances.push(new Plugin(pluginContainer, pluginData)); } else { Plugin.aliasPluginDuplicatesMap[pluginData.plugin_id] = false; CMS._plugins[pluginIndex] = [pluginContainer, pluginData]; CMS._instances[pluginIndex] = new Plugin(pluginContainer, pluginData); } }); }; /** * Hides the opened settings menu. By default looks for any open ones. * * @method _hideSettingsMenu * @static * @private * @param {jQuery} [navEl] element representing the subnav trigger */ Plugin._hideSettingsMenu = function(navEl) { var nav = navEl || $('.cms-submenu-btn.cms-btn-active'); if (!nav.length) { return; } nav.removeClass('cms-btn-active'); // set correct active state nav.closest('.cms-draggable').data('active', false); $('.cms-z-index-9999').removeClass('cms-z-index-9999'); nav.siblings('.cms-submenu-dropdown').hide(); nav.siblings('.cms-quicksearch').hide(); // reset search nav.siblings('.cms-quicksearch').find('input').val('').trigger(Plugin.keyUp).blur(); // reset relativity $('.cms-dragbar').css('position', ''); }; /** * Initialises handlers that affect all plugins and don't make sense * in context of each own plugin instance, e.g. listening for a click on a document * to hide plugin settings menu should only be applied once, and not every time * CMS.Plugin is instantiated. * * @method _initializeGlobalHandlers * @static * @private */ Plugin._initializeGlobalHandlers = function _initializeGlobalHandlers() { var timer; var clickCounter = 0; Plugin._updateClipboard(); // Structureboard initialized too late setTimeout(function() { var pluginData = {}; var html = ''; if (clipboardDraggable.length) { pluginData = find( CMS._plugins, ([desc]) => desc === `cms-plugin-${CMS.API.StructureBoard.getId(clipboardDraggable)}` )[1]; html = clipboardDraggable.parent().html(); } if (CMS.API && CMS.API.Clipboard) { CMS.API.Clipboard.populate(html, pluginData); } }, 0); $document .off(Plugin.pointerUp) .off(Plugin.keyDown) .off(Plugin.keyUp) .off(Plugin.click, '.cms-plugin a, a:has(.cms-plugin), a.cms-plugin') .on(Plugin.pointerUp, function() { // call it as a static method, because otherwise we trigger it the // amount of times CMS.Plugin is instantiated, // which does not make much sense. Plugin._hideSettingsMenu(); }) .on(Plugin.keyDown, function(e) { if (e.keyCode === KEYS.SHIFT) { $document.data('expandmode', true); try { $('.cms-plugin:hover').last().trigger('mouseenter'); $('.cms-dragitem:hover').last().trigger('mouseenter'); } catch (err) {} } }) .on(Plugin.keyUp, function(e) { if (e.keyCode === KEYS.SHIFT) { $document.data('expandmode', false); try { $(':hover').trigger('mouseleave'); } catch (err) {} } }) .on(Plugin.click, '.cms-plugin a, a:has(.cms-plugin), a.cms-plugin', function(e) { var DOUBLECLICK_DELAY = 300; // prevents single click from messing up the edit call // don't go to the link if there is custom js attached to it // or if it's clicked along with shift, ctrl, cmd if (e.shiftKey || e.ctrlKey || e.metaKey || e.isDefaultPrevented()) { return; } e.preventDefault(); if (++clickCounter === 1) { timer = setTimeout(function() { var anchor = $(e.target).closest('a'); clickCounter = 0; window.open(anchor.attr('href'), anchor.attr('target') || '_self'); }, DOUBLECLICK_DELAY); } else { clearTimeout(timer); clickCounter = 0; } }); // have to delegate here because there might be plugins that // have their content replaced by something dynamic. in case that tool // copies the classes - double click to edit would still work // also - do not try to highlight render_model_blocks, only actual plugins $document.on(Plugin.click, '.cms-plugin:not([class*=cms-render-model])', Plugin._clickToHighlightHandler); $document.on(`${Plugin.pointerOverAndOut} ${Plugin.touchStart}`, '.cms-plugin', function(e) { // required for both, click and touch // otherwise propagation won't work to the nested plugin e.stopPropagation(); const pluginContainer = $(e.target).closest('.cms-plugin'); const allOptions = pluginContainer.data('cms'); if (!allOptions || !allOptions.length) { return; } const options = allOptions[0]; if (e.type === 'touchstart') { CMS.API.Tooltip._forceTouchOnce(); } var name = options.plugin_name; var id = options.plugin_id; var type = options.type; if (type === 'generic') { return; } var placeholderId = CMS.API.StructureBoard.getId($(`.cms-draggable-${id}`).closest('.cms-dragarea')); var placeholder = $('.cms-placeholder-' + placeholderId); if (placeholder.length && placeholder.data('cms')) { name = placeholder.data('cms').name + ': ' + name; } CMS.API.Tooltip.displayToggle(e.type === 'pointerover' || e.type === 'touchstart', e, name, id); }); $document.on(Plugin.click, '.cms-dragarea-static .cms-dragbar', e => { const placeholder = $(e.target).closest('.cms-dragarea'); if (placeholder.hasClass('cms-dragarea-static-expanded') && e.isDefaultPrevented()) { return; } placeholder.toggleClass('cms-dragarea-static-expanded'); }); $window.on('blur.cms', () => { $document.data('expandmode', false); }); }; /** * @method _isContainingMultiplePlugins * @param {jQuery} node to check * @static * @private * @returns {Boolean} */ Plugin._isContainingMultiplePlugins = function _isContainingMultiplePlugins(node) { var currentData = node.data('cms'); // istanbul ignore if if (!currentData) { throw new Error('Provided node is not a cms plugin.'); } var pluginIds = currentData.map(function(pluginData) { return pluginData.plugin_id; }); if (pluginIds.length > 1) { // another plugin already lives on the same node // this only works because the plugins are rendered from // the bottom to the top (leaf to root) // meaning the deepest plugin is always first return true; } return false; }; /** * Shows and immediately fades out a success notification (when * plugin was successfully moved. * * @method _highlightPluginStructure * @private * @static * @param {jQuery} el draggable element */ // eslint-disable-next-line no-magic-numbers Plugin._highlightPluginStructure = function _highlightPluginStructure( el, // eslint-disable-next-line no-magic-numbers { successTimeout = 200, delay = 1500, seeThrough = false } ) { const tpl = $(` <div class="cms-dragitem-success ${seeThrough ? 'cms-plugin-overlay-see-through' : ''}"> </div> `); el.addClass('cms-draggable-success').append(tpl); // start animation if (successTimeout) { setTimeout(() => { tpl.fadeOut(successTimeout, function() { $(this).remove(); el.removeClass('cms-draggable-success'); }); }, delay); } // make sure structurboard gets updated after success $(Helpers._getWindow()).trigger('resize.sideframe'); }; /** * Highlights plugin in content mode * * @method _highlightPluginContent * @private * @static * @param {String|Number} pluginId */ Plugin._highlightPluginContent = function _highlightPluginContent( pluginId, // eslint-disable-next-line no-magic-numbers { successTimeout = 200, seeThrough = false, delay = 1500, prominent = false } = {} ) { var coordinates = {}; var positions = []; var OVERLAY_POSITION_TO_WINDOW_HEIGHT_RATIO = 0.2; $('.cms-plugin-' + pluginId).each(function() { var el = $(this); var offset = el.offset(); var ml = parseInt(el.css('margin-left'), 10); var mr = parseInt(el.css('margin-right'), 10); var mt = parseInt(el.css('margin-top'), 10); var mb = parseInt(el.css('margin-bottom'), 10); var width = el.outerWidth(); var height = el.outerHeight(); if (width === 0 && height === 0) { return; } if (isNaN(ml)) { ml = 0; } if (isNaN(mr)) { mr = 0; } if (isNaN(mt)) { mt = 0; } if (isNaN(mb)) { mb = 0; } positions.push({ x1: offset.left - ml, x2: offset.left + width + mr, y1: offset.top - mt, y2: offset.top + height + mb }); }); if (positions.length === 0) { return; } // turns out that offset calculation will be off by toolbar height if // position is set to "relative" on html element. var html = $('html'); var htmlMargin = html.css('position') === 'relative' ? parseInt($('html').css('margin-top'), 10) : 0; coordinates.left = Math.min(...positions.map(pos => pos.x1)); coordinates.top = Math.min(...positions.map(pos => pos.y1)) - htmlMargin; coordinates.width = Math.max(...positions.map(pos => pos.x2)) - coordinates.left; coordinates.height = Math.max(...positions.map(pos => pos.y2)) - coordinates.top - htmlMargin; $window.scrollTop(coordinates.top - $window.height() * OVERLAY_POSITION_TO_WINDOW_HEIGHT_RATIO); $( ` <div class=" cms-plugin-overlay cms-dragitem-success cms-plugin-overlay-${pluginId} ${seeThrough ? 'cms-plugin-overlay-see-through' : ''} ${prominent ? 'cms-plugin-overlay-prominent' : ''} " data-success-timeout="${successTimeout}" > </div> ` ) .css(coordinates) .css({ zIndex: 9999 }) .appendTo($('body')); if (successTimeout) { setTimeout(() => { $(`.cms-plugin-overlay-${pluginId}`).fadeOut(successTimeout, function() { $(this).remove(); }); }, delay); } }; Plugin._clickToHighlightHandler = function _clickToHighlightHandler(e) { if (CMS.settings.mode !== 'structure') { return; } e.preventDefault(); e.stopPropagation(); // FIXME refactor into an object CMS.API.StructureBoard._showAndHighlightPlugin(200, true); // eslint-disable-line no-magic-numbers }; Plugin._removeHighlightPluginContent = function(pluginId) { $(`.cms-plugin-overlay-${pluginId}[data-success-timeout=0]`).remove(); }; Plugin.aliasPluginDuplicatesMap = {}; Plugin.staticPlaceholderDuplicatesMap = {}; // istanbul ignore next Plugin._initializeTree = function _initializeTree() { CMS._plugins = uniqWith(CMS._plugins, ([x], [y]) => x === y); CMS._instances = CMS._plugins.map(function(args) { return new CMS.Plugin(args[0], args[1]); }); // return the cms plugin instances just created return CMS._instances; }; Plugin._updateClipboard = function _updateClipboard() { clipboardDraggable = $('.cms-draggable-from-clipboard:first'); }; Plugin._updateUsageCount = function _updateUsageCount(pluginType) { var currentValue = pluginUsageMap[pluginType] || 0; pluginUsageMap[pluginType] = currentValue + 1; if (Helpers._isStorageSupported) { localStorage.setItem('cms-plugin-usage', JSON.stringify(pluginUsageMap)); } }; Plugin._removeAddPluginPlaceholder = function removeAddPluginPlaceholder() { // this can't be cached since they are created and destroyed all over the place $('.cms-add-plugin-placeholder').remove(); }; Plugin._refreshPlugins = function refreshPlugins() { Plugin.aliasPluginDuplicatesMap = {}; Plugin.staticPlaceholderDuplicatesMap = {}; CMS._plugins = uniqWith(CMS._plugins, isEqual); CMS._instances.forEach(instance => { if (instance.options.type === 'placeholder') { instance._setupUI(`cms-placeholder-${instance.options.placeholder_id}`); instance._ensureData(); instance.ui.container.data('cms', instance.options); instance._setPlaceholder(); } }); CMS._instances.forEach(instance => { if (instance.options.type === 'plugin') { instance._setupUI(`cms-plugin-${instance.options.plugin_id}`); instance._ensureData(); instance.ui.container.data('cms').push(instance.options); instance._setPluginContentEvents(); } }); CMS._plugins.forEach(([type, opts]) => { if (opts.type !== 'placeholder' && opts.type !== 'plugin') { const instance = find( CMS._instances, i => i.options.type === opts.type && Number(i.options.plugin_id) === Number(opts.plugin_id) ); if (instance) { // update instance._setupUI(type); instance._ensureData(); instance.ui.container.data('cms').push(instance.options); instance._setGeneric(); } else { // create CMS._instances.push(new Plugin(type, opts)); } } }); }; // shorthand for jQuery(document).ready(); $(Plugin._initializeGlobalHandlers); export default Plugin;
Close