panels_ipe.js 16 KB

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