panels_ipe.js 15 KB

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