panels_ipe.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. // Ensure the $ alias is owned by jQuery.
  2. (function($) {
  3. Drupal.settings.Panels = Drupal.settings.Panels || {};
  4. Drupal.PanelsIPE = {
  5. editors: {},
  6. bindClickDelete: function(context) {
  7. $('a.pane-delete:not(.pane-delete-processed)', context)
  8. .addClass('pane-delete-processed')
  9. .click(function() {
  10. if (confirm(Drupal.t('Remove this pane?'))) {
  11. $(this).parents('div.panels-ipe-portlet-wrapper').fadeOut('medium', function() {
  12. var $sortable = $(this).closest('.ui-sortable');
  13. $(this).empty().remove();
  14. $sortable.trigger('sortremove');
  15. });
  16. $(this).parents('div.panels-ipe-display-container').addClass('changed');
  17. }
  18. return false;
  19. });
  20. }
  21. }
  22. Drupal.behaviors.PanelsIPE = {
  23. attach: function(context) {
  24. // Remove any old editors.
  25. for (var i in Drupal.PanelsIPE.editors) {
  26. if (Drupal.settings.PanelsIPECacheKeys.indexOf(i) === -1) {
  27. // Clean-up a little bit and remove it.
  28. Drupal.PanelsIPE.editors[i].editing = false;
  29. Drupal.PanelsIPE.editors[i].changed = false;
  30. delete Drupal.PanelsIPE.editors[i];
  31. }
  32. }
  33. // Initialize new editors.
  34. for (var i in Drupal.settings.PanelsIPECacheKeys) {
  35. var key = Drupal.settings.PanelsIPECacheKeys[i];
  36. $('div#panels-ipe-display-' + key + ':not(.panels-ipe-processed)')
  37. .addClass('panels-ipe-processed')
  38. .each(function() {
  39. // If we're replacing an old IPE, clean it up a little.
  40. if (Drupal.PanelsIPE.editors[key]) {
  41. Drupal.PanelsIPE.editors[key].editing = false;
  42. }
  43. Drupal.PanelsIPE.editors[key] = new DrupalPanelsIPE(key);
  44. Drupal.PanelsIPE.editors[key].showContainer();
  45. });
  46. }
  47. $('.panels-ipe-hide-bar').once('panels-ipe-hide-bar-processed').click(function() {
  48. Drupal.PanelsIPE.editors[key].hideContainer();
  49. });
  50. Drupal.PanelsIPE.bindClickDelete(context);
  51. }
  52. };
  53. /**
  54. * Base object (class) definition for the Panels In-Place Editor.
  55. *
  56. * A new instance of this object is instanciated for every unique IPE on a given
  57. * page.
  58. *
  59. * Note that this form is provisional, and we hope to replace it with a more
  60. * flexible, loosely-coupled model that utilizes separate controllers for the
  61. * discrete IPE elements. This will result in greater IPE flexibility.
  62. */
  63. function DrupalPanelsIPE(cache_key, cfg) {
  64. cfg = cfg || {};
  65. var ipe = this;
  66. this.key = cache_key;
  67. this.lockPath = null;
  68. this.state = {};
  69. this.container = $('#panels-ipe-control-container');
  70. this.control = $('div#panels-ipe-control-' + cache_key);
  71. this.initButton = $('div.panels-ipe-startedit', this.control);
  72. this.cfg = cfg;
  73. this.changed = false;
  74. this.sortableOptions = $.extend({
  75. opacity: 0.75, // opacity of sortable while sorting
  76. items: 'div.panels-ipe-portlet-wrapper',
  77. handle: 'div.panels-ipe-draghandle',
  78. cancel: '.panels-ipe-nodrag',
  79. dropOnEmpty: true
  80. }, cfg.sortableOptions || {});
  81. this.regions = [];
  82. this.sortables = {};
  83. $(document).bind('CToolsDetachBehaviors', function() {
  84. // If the IPE is off and the container is not visible, then we need
  85. // to reshow the container on modal close.
  86. if (!$('.panels-ipe-form-container', ipe.control).html() && !ipe.container.is(':visible')) {
  87. ipe.showContainer();
  88. ipe.cancelLock();
  89. }
  90. // If the IPE is on and we've hidden the bar for a modal, we need to
  91. // re-display it.
  92. if (ipe.topParent && ipe.topParent.hasClass('panels-ipe-editing') && ipe.container.is(':not(visible)')) {
  93. ipe.showContainer();
  94. }
  95. });
  96. // If a user navigates away from a locked IPE, cancel the lock in the background.
  97. $(window).bind('beforeunload', function() {
  98. if (!ipe.editing) {
  99. return;
  100. }
  101. if (ipe.topParent && ipe.topParent.hasClass('changed')) {
  102. ipe.changed = true;
  103. }
  104. if (ipe.changed) {
  105. return Drupal.t('This will discard all unsaved changes. Are you sure?');
  106. }
  107. });
  108. // If a user navigates away from a locked IPE, cancel the lock in the background.
  109. $(window).bind('unload', function() {
  110. ipe.cancelLock(true);
  111. });
  112. /**
  113. * If something caused us to abort what we were doing, send a background
  114. * cancel lock request to the server so that we do not leave stale locks
  115. * hanging around.
  116. */
  117. this.cancelLock = function(sync) {
  118. // If there's a lockpath and an ajax available, inform server to clear lock.
  119. // We borrow the ajax options from the customize this page link.
  120. if (ipe.lockPath && Drupal.ajax['panels-ipe-customize-page']) {
  121. var ajaxOptions = {
  122. type: 'POST',
  123. url: ipe.lockPath
  124. }
  125. if (sync) {
  126. ajaxOptions.async = false;
  127. }
  128. // Make sure we don't somehow get another one:
  129. ipe.lockPath = null;
  130. // Send the request. This is synchronous to prevent being cancelled.
  131. $.ajax(ajaxOptions);
  132. }
  133. }
  134. this.activateSortable = function(event, ui) {
  135. if (!Drupal.settings.Panels || !Drupal.settings.Panels.RegionLock) {
  136. // don't bother if there are no region locks in play.
  137. return;
  138. }
  139. var region = event.data.region;
  140. var paneId = ui.item.attr('id').replace('panels-ipe-paneid-', '');
  141. var disabledRegions = false;
  142. // Determined if this pane is locked out of this region.
  143. if (!Drupal.settings.Panels.RegionLock[paneId] || Drupal.settings.Panels.RegionLock[paneId][region]) {
  144. ipe.sortables[region].sortable('enable');
  145. ipe.sortables[region].sortable('refresh');
  146. }
  147. else {
  148. disabledRegions = true;
  149. ipe.sortables[region].sortable('disable');
  150. ipe.sortables[region].sortable('refresh');
  151. }
  152. // If we disabled regions, we need to
  153. if (disabledRegions) {
  154. $(event.srcElement).bind('dragstop', function(event, ui) {
  155. // Go through
  156. });
  157. }
  158. };
  159. // When dragging is stopped, we need to ensure all sortable regions are enabled.
  160. this.enableRegions = function(event, ui) {
  161. for (var i in ipe.regions) {
  162. ipe.sortables[ipe.regions[i]].sortable('enable');
  163. ipe.sortables[ipe.regions[i]].sortable('refresh');
  164. }
  165. }
  166. this.initSorting = function() {
  167. var $region = $(this).parents('.panels-ipe-region');
  168. var region = $region.attr('id').replace('panels-ipe-regionid-', '');
  169. ipe.sortables[region] = $(this).sortable(ipe.sortableOptions);
  170. ipe.regions.push(region);
  171. $(this).bind('sortactivate', {region: region}, ipe.activateSortable);
  172. };
  173. this.initEditing = function(formdata) {
  174. ipe.editing = true;
  175. ipe.topParent = $('div#panels-ipe-display-' + cache_key);
  176. ipe.backup = this.topParent.clone();
  177. // See http://jqueryui.com/demos/sortable/ for details on the configuration
  178. // parameters used here.
  179. ipe.changed = false;
  180. $('div.panels-ipe-sort-container', ipe.topParent).each(ipe.initSorting);
  181. // Since the connectWith option only does a one-way hookup, iterate over
  182. // all sortable regions to connect them with one another.
  183. $('div.panels-ipe-sort-container', ipe.topParent)
  184. .sortable('option', 'connectWith', ['div.panels-ipe-sort-container']);
  185. $('div.panels-ipe-sort-container', ipe.topParent).bind('sortupdate', function() {
  186. ipe.changed = true;
  187. });
  188. $('div.panels-ipe-sort-container', ipe.topParent).bind('sortstop', this.enableRegions);
  189. // Refresh the control jQuery object.
  190. ipe.control = $(ipe.control.selector);
  191. $('.panels-ipe-form-container', ipe.control).append(formdata);
  192. $('input:submit:not(.ajax-processed), button:not(.ajax-processed)', ipe.control).addClass('ajax-processed').each(function() {
  193. var element_settings = {};
  194. element_settings.url = $(this.form).attr('action');
  195. element_settings.setClick = true;
  196. element_settings.event = 'click';
  197. element_settings.progress = { 'type': 'throbber' };
  198. element_settings.ipe_cache_key = cache_key;
  199. var base = $(this).attr('id');
  200. Drupal.ajax[ipe.base] = new Drupal.ajax(base, this, element_settings);
  201. });
  202. // Perform visual effects in a particular sequence.
  203. // .show() + .hide() cannot have speeds associated with them, otherwise
  204. // it clears out inline styles.
  205. $('.panels-ipe-on').show();
  206. ipe.showForm();
  207. $('body').add(ipe.topParent).addClass('panels-ipe-editing');
  208. };
  209. this.hideContainer = function() {
  210. ipe.container.slideUp('fast');
  211. };
  212. this.showContainer = function() {
  213. ipe.container.slideDown('normal');
  214. };
  215. this.showButtons = function() {
  216. $('.panels-ipe-form-container').hide();
  217. $('.panels-ipe-button-container').show();
  218. ipe.showContainer();
  219. };
  220. this.showForm = function() {
  221. $('.panels-ipe-button-container').hide();
  222. $('.panels-ipe-form-container').show();
  223. ipe.showContainer();
  224. };
  225. this.endEditing = function() {
  226. ipe.editing = false;
  227. ipe.lockPath = null;
  228. $('.panels-ipe-form-container').empty();
  229. // Re-show all the IPE non-editing meta-elements
  230. $('div.panels-ipe-off').show('fast');
  231. // Refresh the container and control jQuery objects.
  232. ipe.container = $(ipe.container.selector);
  233. ipe.control = $(ipe.control.selector);
  234. ipe.showButtons();
  235. // Re-hide all the IPE meta-elements
  236. $('div.panels-ipe-on').hide();
  237. $('.panels-ipe-editing').removeClass('panels-ipe-editing');
  238. $('div.panels-ipe-sort-container.ui-sortable', ipe.topParent).sortable("destroy");
  239. };
  240. this.saveEditing = function() {
  241. $('div.panels-ipe-region', ipe.topParent).each(function() {
  242. var val = '';
  243. var region = $(this).attr('id').split('panels-ipe-regionid-')[1];
  244. $(this).find('div.panels-ipe-portlet-wrapper').each(function() {
  245. var id = $(this).attr('id').split('panels-ipe-paneid-')[1];
  246. if (id) {
  247. if (val) {
  248. val += ',';
  249. }
  250. val += id;
  251. }
  252. });
  253. $('[name="panel[pane][' + region + ']"]', ipe.control).val(val);
  254. });
  255. }
  256. this.cancelIPE = function() {
  257. ipe.hideContainer();
  258. ipe.topParent.fadeOut('medium', function() {
  259. ipe.topParent.replaceWith(ipe.backup.clone());
  260. ipe.topParent = $('div#panels-ipe-display-' + ipe.key);
  261. // Processing of these things got lost in the cloning, but the classes remained behind.
  262. // @todo this isn't ideal but I can't seem to figure out how to keep an unprocessed backup
  263. // that will later get processed.
  264. $('.ctools-use-modal-processed', ipe.topParent).removeClass('ctools-use-modal-processed');
  265. $('.pane-delete-processed', ipe.topParent).removeClass('pane-delete-processed');
  266. ipe.topParent.fadeIn('medium');
  267. Drupal.attachBehaviors();
  268. });
  269. };
  270. this.cancelEditing = function() {
  271. if (ipe.topParent.hasClass('changed')) {
  272. ipe.changed = true;
  273. }
  274. if (!ipe.changed || confirm(Drupal.t('This will discard all unsaved changes. Are you sure?'))) {
  275. this.cancelIPE();
  276. return true;
  277. }
  278. else {
  279. // Cancel the submission.
  280. return false;
  281. }
  282. };
  283. this.createSortContainers = function() {
  284. $('div.panels-ipe-region', this.topParent).each(function() {
  285. $(this).children('div.panels-ipe-portlet-marker').parent()
  286. .wrapInner('<div class="panels-ipe-sort-container" />');
  287. // Move our gadgets outside of the sort container so that sortables
  288. // cannot be placed after them.
  289. $('div.panels-ipe-portlet-static', this).each(function() {
  290. $(this).prependTo($(this).parent().parent());
  291. });
  292. });
  293. }
  294. this.createSortContainers();
  295. };
  296. $(function() {
  297. Drupal.ajax.prototype.commands.initIPE = function(ajax, data, status) {
  298. if (Drupal.PanelsIPE.editors[data.key]) {
  299. Drupal.PanelsIPE.editors[data.key].initEditing(data.data);
  300. Drupal.PanelsIPE.editors[data.key].lockPath = data.lockPath;
  301. }
  302. Drupal.attachBehaviors();
  303. };
  304. Drupal.ajax.prototype.commands.IPEsetLockState = function(ajax, data, status) {
  305. if (Drupal.PanelsIPE.editors[data.key]) {
  306. Drupal.PanelsIPE.editors[data.key].lockPath = data.lockPath;
  307. }
  308. };
  309. Drupal.ajax.prototype.commands.addNewPane = function(ajax, data, status) {
  310. if (Drupal.PanelsIPE.editors[data.key]) {
  311. Drupal.PanelsIPE.editors[data.key].changed = true;
  312. }
  313. };
  314. Drupal.ajax.prototype.commands.cancelIPE = function(ajax, data, status) {
  315. if (Drupal.PanelsIPE.editors[data.key]) {
  316. Drupal.PanelsIPE.editors[data.key].cancelIPE();
  317. Drupal.PanelsIPE.editors[data.key].endEditing();
  318. }
  319. };
  320. Drupal.ajax.prototype.commands.unlockIPE = function(ajax, data, status) {
  321. if (confirm(data.message)) {
  322. var ajaxOptions = ajax.options;
  323. ajaxOptions.url = data.break_path;
  324. $.ajax(ajaxOptions);
  325. }
  326. else {
  327. Drupal.PanelsIPE.editors[data.key].endEditing();
  328. }
  329. };
  330. Drupal.ajax.prototype.commands.endIPE = function(ajax, data, status) {
  331. if (Drupal.PanelsIPE.editors[data.key]) {
  332. Drupal.PanelsIPE.editors[data.key].endEditing();
  333. }
  334. };
  335. Drupal.ajax.prototype.commands.insertNewPane = function(ajax, data, status) {
  336. IPEContainerSelector = '#panels-ipe-regionid-' + data.regionId + ' div.panels-ipe-sort-container';
  337. firstPaneSelector = IPEContainerSelector + ' div.panels-ipe-portlet-wrapper:first';
  338. // Insert the new pane before the first existing pane in the region, if
  339. // any.
  340. if ($(firstPaneSelector).length) {
  341. insertData = {
  342. 'method': 'before',
  343. 'selector': firstPaneSelector,
  344. 'data': data.renderedPane,
  345. 'settings': null
  346. }
  347. Drupal.ajax.prototype.commands.insert(ajax, insertData, status);
  348. }
  349. // Else, insert it as a first child of the container. Doing so might fall
  350. // outside of the wrapping markup for the style, but it's the best we can
  351. // do.
  352. else {
  353. insertData = {
  354. 'method': 'prepend',
  355. 'selector': IPEContainerSelector,
  356. 'data': data.renderedPane,
  357. 'settings': null
  358. }
  359. Drupal.ajax.prototype.commands.insert(ajax, insertData, status);
  360. }
  361. };
  362. /**
  363. * Override the eventResponse on ajax.js so we can add a little extra
  364. * behavior.
  365. */
  366. Drupal.ajax.prototype.ipeReplacedEventResponse = Drupal.ajax.prototype.eventResponse;
  367. Drupal.ajax.prototype.eventResponse = function (element, event) {
  368. if (element.ipeCancelThis) {
  369. element.ipeCancelThis = null;
  370. return false;
  371. }
  372. if ($(this.element).attr('id') == 'panels-ipe-cancel') {
  373. if (!Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].cancelEditing()) {
  374. return false;
  375. }
  376. }
  377. var retval = this.ipeReplacedEventResponse(element, event);
  378. if (this.ajaxing && this.element_settings.ipe_cache_key) {
  379. // Move the throbber so that it appears outside our container.
  380. if (this.progress.element) {
  381. $(this.progress.element).addClass('ipe-throbber').appendTo($('body'));
  382. }
  383. Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].hideContainer();
  384. }
  385. // @TODO $('#panels-ipe-throbber-backdrop').remove();
  386. return retval;
  387. };
  388. /**
  389. * Override the eventResponse on ajax.js so we can add a little extra
  390. * behavior.
  391. */
  392. Drupal.ajax.prototype.ipeReplacedError = Drupal.ajax.prototype.error;
  393. Drupal.ajax.prototype.error = function (response, uri) {
  394. var retval = this.ipeReplacedError(response, uri);
  395. if (this.element_settings.ipe_cache_key) {
  396. Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].showContainer();
  397. }
  398. };
  399. Drupal.ajax.prototype.ipeReplacedBeforeSerialize = Drupal.ajax.prototype.beforeSerialize;
  400. Drupal.ajax.prototype.beforeSerialize = function (element_settings, options) {
  401. if ($(this.element).hasClass('panels-ipe-save')) {
  402. Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].saveEditing();
  403. };
  404. return this.ipeReplacedBeforeSerialize(element_settings, options);
  405. };
  406. });
  407. /**
  408. * Apply margin to bottom of the page.
  409. *
  410. * Note that directly applying marginBottom does not work in IE. To prevent
  411. * flickering/jumping page content with client-side caching, this is a regular
  412. * Drupal behavior.
  413. *
  414. * @see admin_menu.js via https://drupal.org/project/admin_menu
  415. */
  416. Drupal.behaviors.panelsIpeMarginBottom = {
  417. attach: function () {
  418. $('body:not(.panels-ipe)').addClass('panels-ipe');
  419. }
  420. };
  421. })(jQuery);