// Ensure the $ alias is owned by jQuery. (function($) { Drupal.settings.Panels = Drupal.settings.Panels || {}; Drupal.PanelsIPE = { editors: {}, bindClickDelete: function(context) { $('a.pane-delete:not(.pane-delete-processed)', context) .addClass('pane-delete-processed') .click(function() { if (confirm(Drupal.t('Remove this pane?'))) { $(this).parents('div.panels-ipe-portlet-wrapper').fadeOut('medium', function() { var $sortable = $(this).closest('.ui-sortable'); $(this).empty().remove(); $sortable.trigger('sortremove'); }); $(this).parents('div.panels-ipe-display-container').addClass('changed'); } return false; }); } } Drupal.behaviors.PanelsIPE = { attach: function(context) { // Remove any old editors. for (var i in Drupal.PanelsIPE.editors) { if (Drupal.settings.PanelsIPECacheKeys.indexOf(i) === -1) { // Clean-up a little bit and remove it. Drupal.PanelsIPE.editors[i].editing = false; Drupal.PanelsIPE.editors[i].changed = false; delete Drupal.PanelsIPE.editors[i]; } } // Initialize new editors. for (var i in Drupal.settings.PanelsIPECacheKeys) { var key = Drupal.settings.PanelsIPECacheKeys[i]; $('div#panels-ipe-display-' + key + ':not(.panels-ipe-processed)') .addClass('panels-ipe-processed') .each(function() { // If we're replacing an old IPE, clean it up a little. if (Drupal.PanelsIPE.editors[key]) { Drupal.PanelsIPE.editors[key].editing = false; } Drupal.PanelsIPE.editors[key] = new DrupalPanelsIPE(key); Drupal.PanelsIPE.editors[key].showContainer(); }); } $('.panels-ipe-hide-bar').once('panels-ipe-hide-bar-processed').click(function() { Drupal.PanelsIPE.editors[key].hideContainer(); }); Drupal.PanelsIPE.bindClickDelete(context); } }; /** * Base object (class) definition for the Panels In-Place Editor. * * A new instance of this object is instanciated for every unique IPE on a given * page. * * Note that this form is provisional, and we hope to replace it with a more * flexible, loosely-coupled model that utilizes separate controllers for the * discrete IPE elements. This will result in greater IPE flexibility. */ function DrupalPanelsIPE(cache_key, cfg) { cfg = cfg || {}; var ipe = this; this.key = cache_key; this.lockPath = null; this.state = {}; this.container = $('#panels-ipe-control-container'); this.control = $('div#panels-ipe-control-' + cache_key); this.initButton = $('div.panels-ipe-startedit', this.control); this.cfg = cfg; this.changed = false; this.sortableOptions = $.extend({ opacity: 0.75, // opacity of sortable while sorting items: 'div.panels-ipe-portlet-wrapper', handle: 'div.panels-ipe-draghandle', cancel: '.panels-ipe-nodrag', dropOnEmpty: true }, cfg.sortableOptions || {}); this.regions = []; this.sortables = {}; $(document).bind('CToolsDetachBehaviors', function() { // If the IPE is off and the container is not visible, then we need // to reshow the container on modal close. if (!$('.panels-ipe-form-container', ipe.control).html() && !ipe.container.is(':visible')) { ipe.showContainer(); ipe.cancelLock(); } // If the IPE is on and we've hidden the bar for a modal, we need to // re-display it. if (ipe.topParent && ipe.topParent.hasClass('panels-ipe-editing') && ipe.container.is(':not(visible)')) { ipe.showContainer(); } }); // If a user navigates away from a locked IPE, cancel the lock in the background. $(window).bind('beforeunload', function() { if (!ipe.editing) { return; } if (ipe.topParent && ipe.topParent.hasClass('changed')) { ipe.changed = true; } if (ipe.changed) { return Drupal.t('This will discard all unsaved changes. Are you sure?'); } }); // If a user navigates away from a locked IPE, cancel the lock in the background. $(window).bind('unload', function() { ipe.cancelLock(true); }); /** * If something caused us to abort what we were doing, send a background * cancel lock request to the server so that we do not leave stale locks * hanging around. */ this.cancelLock = function(sync) { // If there's a lockpath and an ajax available, inform server to clear lock. // We borrow the ajax options from the customize this page link. if (ipe.lockPath && Drupal.ajax['panels-ipe-customize-page']) { var ajaxOptions = { type: 'POST', url: ipe.lockPath } if (sync) { ajaxOptions.async = false; } // Make sure we don't somehow get another one: ipe.lockPath = null; // Send the request. This is synchronous to prevent being cancelled. $.ajax(ajaxOptions); } } this.activateSortable = function(event, ui) { if (!Drupal.settings.Panels || !Drupal.settings.Panels.RegionLock) { // don't bother if there are no region locks in play. return; } var region = event.data.region; var paneId = ui.item.attr('id').replace('panels-ipe-paneid-', ''); var disabledRegions = false; // Determined if this pane is locked out of this region. if (!Drupal.settings.Panels.RegionLock[paneId] || Drupal.settings.Panels.RegionLock[paneId][region]) { ipe.sortables[region].sortable('enable'); ipe.sortables[region].sortable('refresh'); } else { disabledRegions = true; ipe.sortables[region].sortable('disable'); ipe.sortables[region].sortable('refresh'); } // If we disabled regions, we need to if (disabledRegions) { $(event.srcElement).bind('dragstop', function(event, ui) { // Go through }); } }; // When dragging is stopped, we need to ensure all sortable regions are enabled. this.enableRegions = function(event, ui) { for (var i in ipe.regions) { ipe.sortables[ipe.regions[i]].sortable('enable'); ipe.sortables[ipe.regions[i]].sortable('refresh'); } } this.initSorting = function() { var $region = $(this).parents('.panels-ipe-region'); var region = $region.attr('id').replace('panels-ipe-regionid-', ''); ipe.sortables[region] = $(this).sortable(ipe.sortableOptions); ipe.regions.push(region); $(this).bind('sortactivate', {region: region}, ipe.activateSortable); }; this.initEditing = function(formdata) { ipe.editing = true; ipe.topParent = $('div#panels-ipe-display-' + cache_key); ipe.backup = this.topParent.clone(); // See http://jqueryui.com/demos/sortable/ for details on the configuration // parameters used here. ipe.changed = false; $('div.panels-ipe-sort-container', ipe.topParent).each(ipe.initSorting); // Since the connectWith option only does a one-way hookup, iterate over // all sortable regions to connect them with one another. $('div.panels-ipe-sort-container', ipe.topParent) .sortable('option', 'connectWith', ['div.panels-ipe-sort-container']); $('div.panels-ipe-sort-container', ipe.topParent).bind('sortupdate', function() { ipe.changed = true; }); $('div.panels-ipe-sort-container', ipe.topParent).bind('sortstop', this.enableRegions); // Refresh the control jQuery object. ipe.control = $(ipe.control.selector); $('.panels-ipe-form-container', ipe.control).append(formdata); $('input:submit:not(.ajax-processed), button:not(.ajax-processed)', ipe.control).addClass('ajax-processed').each(function() { var element_settings = {}; element_settings.url = $(this.form).attr('action'); element_settings.setClick = true; element_settings.event = 'click'; element_settings.progress = { 'type': 'throbber' }; element_settings.ipe_cache_key = cache_key; var base = $(this).attr('id'); Drupal.ajax[ipe.base] = new Drupal.ajax(base, this, element_settings); }); // Perform visual effects in a particular sequence. // .show() + .hide() cannot have speeds associated with them, otherwise // it clears out inline styles. $('.panels-ipe-on').show(); ipe.showForm(); $('body').add(ipe.topParent).addClass('panels-ipe-editing'); }; this.hideContainer = function() { ipe.container.slideUp('fast'); }; this.showContainer = function() { ipe.container.slideDown('normal'); }; this.showButtons = function() { $('.panels-ipe-form-container').hide(); $('.panels-ipe-button-container').show(); ipe.showContainer(); }; this.showForm = function() { $('.panels-ipe-button-container').hide(); $('.panels-ipe-form-container').show(); ipe.showContainer(); }; this.endEditing = function() { ipe.editing = false; ipe.lockPath = null; $('.panels-ipe-form-container').empty(); // Re-show all the IPE non-editing meta-elements $('div.panels-ipe-off').show('fast'); // Refresh the container and control jQuery objects. ipe.container = $(ipe.container.selector); ipe.control = $(ipe.control.selector); ipe.showButtons(); // Re-hide all the IPE meta-elements $('div.panels-ipe-on').hide(); $('.panels-ipe-editing').removeClass('panels-ipe-editing'); $('div.panels-ipe-sort-container.ui-sortable', ipe.topParent).sortable("destroy"); }; this.saveEditing = function() { $('div.panels-ipe-region', ipe.topParent).each(function() { var val = ''; var region = $(this).attr('id').split('panels-ipe-regionid-')[1]; $(this).find('div.panels-ipe-portlet-wrapper').each(function() { var id = $(this).attr('id').split('panels-ipe-paneid-')[1]; if (id) { if (val) { val += ','; } val += id; } }); $('[name="panel[pane][' + region + ']"]', ipe.control).val(val); }); } this.cancelIPE = function() { ipe.hideContainer(); ipe.topParent.fadeOut('medium', function() { ipe.topParent.replaceWith(ipe.backup.clone()); ipe.topParent = $('div#panels-ipe-display-' + ipe.key); // Processing of these things got lost in the cloning, but the classes remained behind. // @todo this isn't ideal but I can't seem to figure out how to keep an unprocessed backup // that will later get processed. $('.ctools-use-modal-processed', ipe.topParent).removeClass('ctools-use-modal-processed'); $('.pane-delete-processed', ipe.topParent).removeClass('pane-delete-processed'); ipe.topParent.fadeIn('medium'); Drupal.attachBehaviors(); }); }; this.cancelEditing = function() { if (ipe.topParent.hasClass('changed')) { ipe.changed = true; } if (!ipe.changed || confirm(Drupal.t('This will discard all unsaved changes. Are you sure?'))) { this.cancelIPE(); return true; } else { // Cancel the submission. return false; } }; this.createSortContainers = function() { $('div.panels-ipe-region', this.topParent).each(function() { $(this).children('div.panels-ipe-portlet-marker').parent() .wrapInner('
'); // Move our gadgets outside of the sort container so that sortables // cannot be placed after them. $('div.panels-ipe-portlet-static', this).each(function() { $(this).prependTo($(this).parent().parent()); }); }); } this.createSortContainers(); }; $(function() { Drupal.ajax.prototype.commands.initIPE = function(ajax, data, status) { if (Drupal.PanelsIPE.editors[data.key]) { Drupal.PanelsIPE.editors[data.key].initEditing(data.data); Drupal.PanelsIPE.editors[data.key].lockPath = data.lockPath; } Drupal.attachBehaviors(); }; Drupal.ajax.prototype.commands.IPEsetLockState = function(ajax, data, status) { if (Drupal.PanelsIPE.editors[data.key]) { Drupal.PanelsIPE.editors[data.key].lockPath = data.lockPath; } }; Drupal.ajax.prototype.commands.addNewPane = function(ajax, data, status) { if (Drupal.PanelsIPE.editors[data.key]) { Drupal.PanelsIPE.editors[data.key].changed = true; } }; Drupal.ajax.prototype.commands.cancelIPE = function(ajax, data, status) { if (Drupal.PanelsIPE.editors[data.key]) { Drupal.PanelsIPE.editors[data.key].cancelIPE(); Drupal.PanelsIPE.editors[data.key].endEditing(); } }; Drupal.ajax.prototype.commands.unlockIPE = function(ajax, data, status) { if (confirm(data.message)) { var ajaxOptions = ajax.options; ajaxOptions.url = data.break_path; $.ajax(ajaxOptions); } else { Drupal.PanelsIPE.editors[data.key].endEditing(); } }; Drupal.ajax.prototype.commands.endIPE = function(ajax, data, status) { if (Drupal.PanelsIPE.editors[data.key]) { Drupal.PanelsIPE.editors[data.key].endEditing(); } }; Drupal.ajax.prototype.commands.insertNewPane = function(ajax, data, status) { IPEContainerSelector = '#panels-ipe-regionid-' + data.regionId + ' div.panels-ipe-sort-container'; firstPaneSelector = IPEContainerSelector + ' div.panels-ipe-portlet-wrapper:first'; // Insert the new pane before the first existing pane in the region, if // any. if ($(firstPaneSelector).length) { insertData = { 'method': 'before', 'selector': firstPaneSelector, 'data': data.renderedPane, 'settings': null } Drupal.ajax.prototype.commands.insert(ajax, insertData, status); } // Else, insert it as a first child of the container. Doing so might fall // outside of the wrapping markup for the style, but it's the best we can // do. else { insertData = { 'method': 'prepend', 'selector': IPEContainerSelector, 'data': data.renderedPane, 'settings': null } Drupal.ajax.prototype.commands.insert(ajax, insertData, status); } }; /** * Override the eventResponse on ajax.js so we can add a little extra * behavior. */ Drupal.ajax.prototype.ipeReplacedEventResponse = Drupal.ajax.prototype.eventResponse; Drupal.ajax.prototype.eventResponse = function (element, event) { if (element.ipeCancelThis) { element.ipeCancelThis = null; return false; } if ($(this.element).attr('id') == 'panels-ipe-cancel') { if (!Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].cancelEditing()) { return false; } } var retval = this.ipeReplacedEventResponse(element, event); if (this.ajaxing && this.element_settings.ipe_cache_key) { // Move the throbber so that it appears outside our container. if (this.progress.element) { $(this.progress.element).addClass('ipe-throbber').appendTo($('body')); } Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].hideContainer(); } // @TODO $('#panels-ipe-throbber-backdrop').remove(); return retval; }; /** * Override the eventResponse on ajax.js so we can add a little extra * behavior. */ Drupal.ajax.prototype.ipeReplacedError = Drupal.ajax.prototype.error; Drupal.ajax.prototype.error = function (response, uri) { var retval = this.ipeReplacedError(response, uri); if (this.element_settings.ipe_cache_key) { Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].showContainer(); } }; Drupal.ajax.prototype.ipeReplacedBeforeSerialize = Drupal.ajax.prototype.beforeSerialize; Drupal.ajax.prototype.beforeSerialize = function (element_settings, options) { if ($(this.element).hasClass('panels-ipe-save')) { Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].saveEditing(); }; return this.ipeReplacedBeforeSerialize(element_settings, options); }; }); /** * Apply margin to bottom of the page. * * Note that directly applying marginBottom does not work in IE. To prevent * flickering/jumping page content with client-side caching, this is a regular * Drupal behavior. * * @see admin_menu.js via https://drupal.org/project/admin_menu */ Drupal.behaviors.panelsIpeMarginBottom = { attach: function () { $('body:not(.panels-ipe)').addClass('panels-ipe'); } }; })(jQuery);