CRM-15346 - Also trigger crmUnload when refreshing content
[civicrm-core.git] / js / crm.ajax.js
index d66aad02547b442fbb61669f8fb3f5f8eb46613a..276d02ddfd729d9ebaf9574c37d2baeba7fdd29c 100644 (file)
@@ -3,47 +3,43 @@
  * @see https://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface
  * @see https://wiki.civicrm.org/confluence/display/CRMDOC/Ajax+Pages+and+Forms
  */
-(function($, CRM) {
+(function($, CRM, undefined) {
   /**
-   * Almost like {crmURL} but on the client side
-   * eg: var url = CRM.url('civicrm/contact/view', {reset:1,cid:42});
-   * or: $('a.my-link').crmURL();
+   * @param string path
+   * @param string|object query
+   * @param string mode - optionally specify "front" or "back"
    */
-  var tplURL = '/civicrm/example?placeholder';
-  var urlInitted = false;
-  CRM.url = function (p, params) {
-    if (p == "init") {
-      tplURL = params;
-      urlInitted = true;
-      return;
+  var tplURL;
+  CRM.url = function (path, query, mode) {
+    if (typeof path === 'object') {
+      return tplURL = path;
+    }
+    if (!tplURL) {
+      CRM.console('error', 'Error: CRM.url called before initialization');
     }
-    if (!urlInitted) {
-      console && console.log && console.log('Warning: CRM.url called before initialization');
+    if (!mode) {
+      mode = CRM.config && CRM.config.isFrontend ? 'front' : 'back';
     }
-    params = params || '';
-    var frag = p.split ('?');
-    var url = tplURL.replace("civicrm/example", frag[0]);
+    query = query || '';
+    var frag = path.split('?');
+    var url = tplURL[mode].replace("*path*", frag[0]);
 
-    if (typeof(params) == 'string') {
-      url = url.replace("placeholder", params);
+    if (!query) {
+      url = url.replace(/[?&]\*query\*/, '');
     }
     else {
-      url = url.replace("placeholder", $.param(params));
+      url = url.replace("*query*", typeof query === 'string' ? query : $.param(query));
     }
     if (frag[1]) {
-      url += (url.indexOf('?') === (url.length - 1) ? '' : '&') + frag[1];
-    }
-    // remove trailing "?"
-    if (url.indexOf('?') === (url.length - 1)) {
-      url = url.slice(0, (url.length - 1));
+      url += (url.indexOf('?') < 0 ? '?' : '&') + frag[1];
     }
     return url;
   };
 
-  // Backwards compatible with jQuery fn
+  // @deprecated
   $.extend ({'crmURL':
     function (p, params) {
-      console && console.log && console.log('Calling crmURL from jQuery is deprecated. Please use CRM.url() instead.');
+      CRM.console('warn', 'Calling crmURL from jQuery is deprecated. Please use CRM.url() instead.');
       return CRM.url(p, params);
     }
   });
    * @deprecated
    */
   $.fn.crmAPI = function(entity, action, params, options) {
-    console && console.log && console.log('Calling crmAPI from jQuery is deprecated. Please use CRM.api() instead.');
+    CRM.console('warn', 'Calling crmAPI from jQuery is deprecated. Please use CRM.api3() instead.');
     return CRM.api.call(this, entity, action, params, options);
   };
 
       this.element.trigger('crmAjaxFail', data);
       CRM.alert(ts('Unable to reach the server. Please refresh this page in your browser and try again.'), ts('Network Error'), 'error');
     },
+    _onError: function(data) {
+      this.element.attr('data-unsaved-changes', 'false').trigger('crmAjaxError', data);
+      if (this.options.crmForm && this.options.crmForm.autoClose && this.element.data('uiDialog')) {
+        this.element.dialog('close');
+      }
+    },
     _formatUrl: function(url) {
       // Strip hash
       url = url.split('#')[0];
       if (this._originalContent === null) {
         this._originalContent = this.element.contents().detach();
       }
-      this.options.block && $('.blockOverlay', this.element).length < 1 && this.element.block();
+      this.options.block && this.element.block();
       $.getJSON(url, function(data) {
-        if (typeof(data) != 'object' || typeof(data.content) != 'string') {
+        that.options.block && that.element.unblock();
+        if (!$.isPlainObject(data)) {
           that._onFailure(data);
           return;
         }
+        if (data.status === 'error') {
+          that._onError(data);
+          return;
+        }
         data.url = url;
-        that.element.trigger('crmBeforeLoad', data).html(data.content);
+        that.element.trigger('crmUnload').trigger('crmBeforeLoad', data);
+        that._beforeRemovingContent();
+        that.element.html(data.content);
         that._handleOrderLinks();
         that.element.trigger('crmLoad', data);
         that.options.crmForm && that.element.trigger('crmFormLoad', data);
         that._onFailure();
       });
     },
-    _destroy: function() {
-      this.element.removeClass('crm-ajax-container');
+    // Perform any cleanup needed before removing/replacing content
+    _beforeRemovingContent: function() {
+      var that = this;
+      if (window.tinyMCE && tinyMCE.editors) {
+        $.each(tinyMCE.editors, function(k) {
+          if ($.contains(that.element[0], this.getElement())) {
+            this.remove();
+          }
+        });
+      }
       this.options.crmForm && $('form', this.element).ajaxFormUnbind();
+    },
+    _destroy: function() {
+      this.element.removeClass('crm-ajax-container').trigger('crmUnload');
+      this._beforeRemovingContent();
       if (this._originalContent !== null) {
         this.element.empty().append(this._originalContent);
       }
       if (typeof settings.dialog.height === 'string' && settings.dialog.height.indexOf('%') > 0) {
         settings.dialog.height = parseInt($(window).height() * (parseFloat(settings.dialog.height)/100), 10);
       }
-      $('<div id="'+ settings.target.substring(1) +'"><div class="crm-loading-element">' + ts('Loading') + '...</div></div>').dialog(settings.dialog);
-      $(settings.target).on('dialogclose', function() {
-        if (!$(this).data('hasUnsavedChanges')) {
-          $(this).crmSnippet('destroy').dialog('destroy').remove();
+      // Increase percent width on small screens
+      if (typeof settings.dialog.width === 'string' && settings.dialog.width.indexOf('%') > 0) {
+        var screenWidth = $(window).width(),
+          percentage = parseInt(settings.dialog.width.replace('%', ''), 10),
+          gap = 100-percentage;
+        if (screenWidth < 701) {
+          settings.dialog.width = '100%';
         }
-      });
-    }
-    if (settings.dialog && !settings.dialog.title) {
-      $(settings.target).on('crmLoad', function(e, data) {
-        if (e.target === $(settings.target)[0] && data && data.title) {
-          $(this).dialog('option', 'title', data.title);
+        else if (screenWidth < 1400) {
+          settings.dialog.width = '' + parseInt(percentage+gap-((screenWidth - 700)/7*(gap)/100), 10) + '%';
         }
-      });
+      }
+      $('<div id="'+ settings.target.substring(1) +'"><div class="crm-loading-element">' + ts('Loading') + '...</div></div>').dialog(settings.dialog);
+      $(settings.target)
+        .on('dialogclose', function() {
+          if ($(this).attr('data-unsaved-changes') !== 'true') {
+            $(this).crmSnippet('destroy').dialog('destroy').remove();
+          }
+        })
+        .on('crmLoad', function(e, data) {
+          // Set title
+          if (e.target === $(settings.target)[0] && data && !settings.dialog.title && data.title) {
+            $(this).dialog('option', 'title', data.title);
+          }
+          // Adjust height to fit content (small delay to allow elements to render)
+          window.setTimeout(function() {
+            var currentHeight = $(settings.target).parent().outerHeight(),
+              padding = currentHeight - $(settings.target).height(),
+              newHeight = $(settings.target).prop('scrollHeight') + padding,
+              menuHeight = $('#civicrm-menu').outerHeight(),
+              maxHeight = $(window).height() - menuHeight;
+            newHeight = newHeight > maxHeight ? maxHeight : newHeight;
+            if (newHeight > (currentHeight + 15)) {
+              $(settings.target).dialog('option', {
+                position: {my: 'center', at: 'center center+' + (menuHeight / 2), of: window},
+                height: newHeight
+              });
+            }
+          }, 500);
+        });
     }
     $(settings.target).crmSnippet(settings).crmSnippet('refresh');
     return $(settings.target);
   };
   CRM.loadForm = function(url, options) {
-    var settings = {
+    var formErrors = [], settings = {
       crmForm: {
         ajaxForm: {},
         autoClose: true,
         refreshAction: ['next_new', 'submit_savenext', 'upload_new'],
         cancelButton: '.cancel',
         openInline: 'a.open-inline, a.button, a.action-item',
-        onCancel: function(event) {},
-        onError: function(data) {
-          var $el = $(this);
-          $el.html(data.content).trigger('crmLoad', data).trigger('crmFormLoad', data).trigger('crmFormError', data);
-          if (typeof(data.errors) == 'object') {
-            $.each(data.errors, function(formElement, msg) {
-              $('[name="'+formElement+'"]', $el).crmError(msg);
-            });
-          }
-        }
+        onCancel: function(event) {}
       }
     };
     // Move options that belong to crmForm. Others will be passed through to crmSnippet
 
     var widget = CRM.loadPage(url, settings).off('.crmForm');
 
+    // CRM-14353 - Warn of unsaved changes for all forms except those which have opted out
     function cancelAction() {
-      var dirty = CRM.utils.initialValueChanged(widget),
-        title = widget.dialog('option', 'title');
-      widget.data('hasUnsavedChanges', dirty).dialog('close');
+      var dirty = CRM.utils.initialValueChanged($('form:not([data-warn-changes=false])', widget));
+      widget.attr('data-unsaved-changes', dirty ? 'true' : 'false');
       if (dirty) {
         var id = widget.attr('id') + '-unsaved-alert',
-          alert = CRM.alert('<p>' + ts('%1 has not been saved.', {1: title}) + '</p><p><a href="#" id="' + id + '">' + ts('Restore') + '</a></p>', ts('Unsaved Changes'), alert, {expires: 60000});
+          title = widget.dialog('option', 'title'),
+          alert = CRM.alert('<p>' + ts('%1 has not been saved.', {1: title}) + '</p><p><a href="#" id="' + id + '">' + ts('Restore') + '</a></p>', ts('Unsaved Changes'), 'alert unsaved-dialog', {expires: 60000});
         $('#' + id).button({icons: {primary: 'ui-icon-arrowreturnthick-1-w'}}).click(function(e) {
-          widget.dialog('open');
-          alert.close();
+          widget.attr('data-unsaved-changes', 'false').dialog('open');
           e.preventDefault();
         });
       }
     }
-    if (widget.data('uiDialog')) {
-      // This is a bit harsh but we are removing jQuery UI's event handler from the close button and adding our own
-      $('.ui-dialog-titlebar-close').first().off().click(cancelAction);
-    }
+
+    widget.data('uiDialog') && widget.on('dialogbeforeclose', function(e) {
+      // CRM-14353 - Warn unsaved changes if user clicks close button or presses "esc"
+      if (e.originalEvent) {
+        cancelAction();
+      }
+    });
 
     widget.on('crmFormLoad.crmForm', function(event, data) {
-      var $el = $(this)
-        .data('hasUnsavedChanges', false);
-      var settings = $el.crmSnippet('option', 'crmForm');
+      var $el = $(this).attr('data-unsaved-changes', 'false'),
+        settings = $el.crmSnippet('option', 'crmForm');
       settings.cancelButton && $(settings.cancelButton, this).click(function(e) {
         e.preventDefault();
         var returnVal = settings.onCancel.call($el, e);
           $el.trigger('crmFormCancel', e);
           if ($el.data('uiDialog') && settings.autoClose) {
             cancelAction();
+            $el.dialog('close');
           }
           else if (!settings.autoClose) {
             $el.crmSnippet('resetUrl').crmSnippet('refresh');
         }
       });
       if (settings.validate) {
-        $("form", this).validate(typeof(settings.validate) == 'object' ? settings.validate : CRM.validate.params);
+        $("form", this).crmValidate();
       }
       $("form:not('[data-no-ajax-submit=true]')", this).ajaxForm($.extend({
         url: data.url.replace(/reset=1[&]?/, ''),
         dataType: 'json',
         success: function(response) {
-          if (response.status !== 'form_error') {
-            $el.crmSnippet('option', 'block') && $el.unblock();
+          if (response.content === undefined) {
             $el.trigger('crmFormSuccess', response);
             // Reset form for e.g. "save and new"
             if (response.userContext && (response.status === 'redirect' || (settings.refreshAction && $.inArray(response.buttonName, settings.refreshAction) >= 0))) {
               $el.data('civiCrmSnippet')._originalUrl = response.userContext;
               $el.crmSnippet('resetUrl').crmSnippet('refresh');
             }
-            else if ($el.data('uiDialog') && settings.autoClose) {
+            // Close if we are on the original url or the action was "delete" (in which case returning to view may be inappropriate)
+            else if ($el.data('uiDialog') && (settings.autoClose || response.action === 8)) {
               $el.dialog('close');
             }
             else if (settings.autoClose === false) {
             }
           }
           else {
+            $el.crmSnippet('option', 'block') && $el.unblock();
             response.url = data.url;
-            settings.onError.call($el, response);
+            $el.html(response.content).trigger('crmLoad', response).trigger('crmFormLoad', response);
+            if (response.status === 'form_error') {
+              formErrors = [];
+              $el.trigger('crmFormError', response);
+              $.each(response.errors || [], function(formElement, msg) {
+                formErrors.push($('[name="'+formElement+'"]', $el).crmError(msg));
+              });
+            }
           }
         },
         beforeSerialize: function(form, options) {
               this.updateElement && this.updateElement();
             });
           }
+          if (window.tinyMCE && tinyMCE.editors) {
+            $.each(tinyMCE.editors, function() {
+              this.save();
+            });
+          }
         },
         beforeSubmit: function(submission) {
+          $.each(formErrors, function() {
+            this && this.close && this.close();
+          });
           $el.crmSnippet('option', 'block') && $el.block();
           $el.trigger('crmFormSubmit', submission);
         }
           return false;
         });
       }
+      // Show form buttons as part of the dialog
+      if ($el.data('uiDialog')) {
+        var buttonContainers = '.crm-submit-buttons, .action-link',
+          buttons = [],
+          added = [];
+        $(buttonContainers, $el).find('input.crm-form-submit, a.button').each(function() {
+          var $el = $(this),
+            label = $el.is('input') ? $el.attr('value') : $el.text(),
+            identifier = $el.attr('name') || $el.attr('href');
+          if (!identifier || identifier === '#' || $.inArray(identifier, added) < 0) {
+            var $icon = $el.find('.icon'),
+              button = {'data-identifier': identifier, text: label, click: function() {
+                $el.click();
+              }};
+            if ($icon.length) {
+              button.icons = {primary: $icon.attr('class')};
+            } else {
+              var action = $el.hasClass('cancel') ? 'close' : (identifier.substr(identifier.length-4) === '_new' ? 'plus' : 'check');
+              button.icons = {primary: 'ui-icon-' + action};
+            }
+            buttons.push(button);
+            added.push(identifier);
+          }
+          // display:none causes the form to not submit when pressing "enter"
+          $el.parents(buttonContainers).css({height: 0, padding: 0, margin: 0, overflow: 'hidden'});
+        });
+        $el.dialog('option', 'buttons', buttons);
+      }
+      // Allow a button to prevent ajax submit
+      $('input[data-no-ajax-submit=true]').click(function() {
+        $(this).closest('form').ajaxFormUnbind();
+      });
       // For convenience, focus the first field
       $('input[type=text], textarea, select', this).filter(':visible').first().not('.dateplugin').focus();
     });
     return widget;
   };
   /**
-   * Handler for jQuery click event e.g. $('a').click(CRM.popup)
-   * @returns {boolean}
+   * Handler for jQuery click event e.g. $('a').click(CRM.popup);
    */
   CRM.popup = function(e) {
     var $el = $(this).first(),
     else if ($el.hasClass('medium-popup')) {
       settings.dialog.width = settings.dialog.height = '50%';
     }
-    else if ($el.hasClass('huge-popup')) {
-      settings.dialog.height = '90%';
-    }
     var dialog = popup(url, settings);
     // Trigger events from the dialog on the original link element
     $el.trigger('crmPopupOpen', [dialog]);
     e.preventDefault();
   };
   /**
-   * An event callback for CRM.popup or a standalone function to refresh the content around a popup link
-   * @param e event|selector
+   * An event callback for CRM.popup or a standalone function to refresh the content around a given element
+   * @param e {event|selector}
    */
   CRM.refreshParent = function(e) {
     // Use e.target if input smells like an event, otherwise assume it's a jQuery selector
     var $el = (e.stopPropagation && e.target) ? $(e.target) : $(e),
       $table = $el.closest('.dataTable');
     // Call native refresh method on ajax datatables
-    if ($table && $.fn.DataTable.fnIsDataTable($table[0]) && $table.dataTable().fnSettings().sAjaxSource) {
+    if ($table.length && $.fn.DataTable.fnIsDataTable($table[0]) && $table.dataTable().fnSettings().sAjaxSource) {
       // Refresh ALL datatables - needed for contact relationship tab
       $.each($.fn.dataTable.fnTables(), function() {
         $(this).dataTable().fnSettings().sAjaxSource && $(this).unblock().dataTable().fnDraw();
   };
 
   $(function($) {
-    $('body').on('click', 'a.crm-popup', CRM.popup);
+    $('body')
+      .on('click', 'a.crm-popup', CRM.popup)
+      // Close unsaved dialog messages
+      .on('dialogopen', function(e) {
+        $('.alert.unsaved-dialog .ui-notify-cross', '#crm-notification-container').click();
+      })
+      // Destroy old unsaved dialog
+      .on('dialogcreate', function(e) {
+        $('.ui-dialog-content.crm-ajax-container:hidden[data-unsaved-changes=true]').crmSnippet('destroy').dialog('destroy').remove();
+      });
   });
 
 }(jQuery, CRM));