Merge pull request #3077 from adamwight/test_fixtures
[civicrm-core.git] / js / Common.js
index 819cff3df78f457c503bdded6a6e2123bf628ff3..2818abc18cebdc0a8b232ab486646b9f34db71d1 100644 (file)
@@ -1,6 +1,7 @@
 // https://civicrm.org/licensing
 var CRM = CRM || {};
-var cj = jQuery;
+var cj = CRM.$ = jQuery;
+CRM._ = _;
 
 /**
  * Short-named function for string translation, defined in global scope so it's available everywhere.
@@ -199,7 +200,7 @@ CRM.validate = CRM.validate || {
   functions: []
 };
 
-(function ($, undefined) {
+(function ($, _, undefined) {
   "use strict";
 
   // Theme classes for unattached elements
@@ -225,7 +226,7 @@ CRM.validate = CRM.validate || {
         $elect = $(this),
         val = $elect.val() || [],
         opts = removePlaceholder ? '' : '[value!=""]';
-      if (typeof(val) !== 'array') {
+      if (!$.isArray(val)) {
         val = [val];
       }
       $elect.find('option' + opts).remove();
@@ -237,6 +238,22 @@ CRM.validate = CRM.validate || {
     });
   };
 
+/**
+ * Compare Form Input values against cached initial value.
+ *
+ * @return {Boolean} true if changes have been made.
+ */
+  CRM.utils.initialValueChanged = function(el) {
+    var isDirty = false;
+    $(':input:visible, :input.select2-offscreen', el).each(function () {
+      var initialValue = $(this).data('crm-initial-value');
+      if (initialValue !== undefined && initialValue != $(this).val()) {
+        isDirty = true;
+      }
+    });
+    return isDirty;
+  }
+
   /**
    * Wrapper for select2 initialization function; supplies defaults
    * @param options object
@@ -245,7 +262,7 @@ CRM.validate = CRM.validate || {
     return $(this).each(function () {
       var
         $el = $(this),
-        defaults = {allowClear: !$el.hasClass('required')};
+        settings = {allowClear: !$el.hasClass('required')};
       // quickform doesn't support optgroups so here's a hack :(
       $('option[value^=crm_optgroup]', this).each(function () {
         $(this).nextUntil('option[value^=crm_optgroup]').wrapAll('<optgroup label="' + $(this).text() + '" />');
@@ -253,12 +270,16 @@ CRM.validate = CRM.validate || {
       });
       // Defaults for single-selects
       if ($el.is('select:not([multiple])')) {
-        defaults.minimumResultsForSearch = 10;
+        settings.minimumResultsForSearch = 10;
         if ($('option:first', this).val() === '') {
-          defaults.placeholderOption = 'first';
+          settings.placeholderOption = 'first';
         }
       }
-      $el.select2($.extend(defaults, $el.data('select-params') || {}, options || {}));
+      $.extend(settings, $el.data('select-params') || {}, options || {});
+      if (settings.ajax) {
+        $el.addClass('crm-ajax-select');
+      }
+      $el.select2(settings);
     });
   };
 
@@ -271,14 +292,14 @@ CRM.validate = CRM.validate || {
     options.select = options.select || {};
     return $(this).each(function() {
       var
-        $el = $(this),
+        $el = $(this).off('.crmEntity'),
         entity = options.entity || $el.data('api-entity') || 'contact',
         selectParams = {};
       $el.data('api-entity', entity);
       $el.data('select-params', $.extend({}, $el.data('select-params') || {}, options.select));
       $el.data('api-params', $.extend({}, $el.data('api-params') || {}, options.api));
       $el.data('create-links', options.create || $el.data('create-links'));
-      $el.addClass('crm-ajax-select crm-' + entity + '-ref');
+      $el.addClass('crm-form-entityref crm-' + entity + '-ref');
       var settings = {
         // Use select2 ajax helper instead of CRM.api because it provides more value
         ajax: {
@@ -322,7 +343,7 @@ CRM.validate = CRM.validate || {
           }
         }
       };
-      if ($el.data('create-links')) {
+      if ($el.data('create-links') && entity.toLowerCase() === 'contact') {
         selectParams.formatInputTooShort = function() {
           var txt = $el.data('select-params').formatInputTooShort || $.fn.select2.defaults.formatInputTooShort.call(this);
           if ($el.data('create-links')) {
@@ -334,7 +355,7 @@ CRM.validate = CRM.validate || {
           var txt = $el.data('select-params').formatNoMatches || $.fn.select2.defaults.formatNoMatches;
           return txt + '<br />' + formatSelect2CreateLinks($el);
         };
-        $el.off('.createLinks').on('select2-open.createLinks', function() {
+        $el.on('select2-open.crmEntity', function() {
           var $el = $(this);
           $('#select2-drop').off('.crmEntity').on('click.crmEntity', 'a.crm-add-entity', function(e) {
             $el.select2('close');
@@ -356,7 +377,38 @@ CRM.validate = CRM.validate || {
           });
         });
       }
-      $el.crmSelect2($.extend(settings, $el.data('select-params'), selectParams));
+      // Create new items inline - works for tags
+      else if ($el.data('create-links')) {
+        selectParams.createSearchChoice = function(term, data) {
+          if (!_.findKey(data, {label: term})) {
+            return {id: "0", term: term, label: term + ' (' + ts('new tag') + ')'};
+          }
+        };
+        selectParams.tokenSeparators = [','];
+        selectParams.createSearchChoicePosition = 'bottom';
+      }
+      $el.crmSelect2($.extend(settings, $el.data('select-params'), selectParams))
+        .on('select2-selecting.crmEntity', function(e) {
+          if (e.val === "0") {
+            e.object.label = e.object.term;
+            CRM.api3(entity, 'create', $.extend({name: e.object.term}, $el.data('api-params').params || {}))
+              .done(function(created) {
+                var
+                  multiple = !!$el.data('select-params').multiple,
+                  val = $el.select2('val'),
+                  data = $el.select2('data'),
+                  item = {id: created.id, label: e.object.term};
+                if (val === "0") {
+                  $el.select2('data', item, true);
+                }
+                else if ($.isArray(val) && $.inArray("0", val) > -1) {
+                  _.remove(data, {id: "0"});
+                  data.push(item);
+                  $el.select2('data', data, true);
+                }
+              });
+          }
+        });
     });
   };
 
@@ -401,34 +453,72 @@ CRM.validate = CRM.validate || {
     .on('crmLoad', function(e) {
       $('table.row-highlight', e.target)
         .off('.rowHighlight')
-        .on('change.rowHighlight', 'input.select-row, input.select-rows', function () {
-          var target, table = $(this).closest('table');
+        .on('change.rowHighlight', 'input.select-row, input.select-rows', function (e, data) {
+          var filter, $table = $(this).closest('table');
           if ($(this).hasClass('select-rows')) {
-            target = $('tbody tr', table);
-            $('input.select-row', table).prop('checked', $(this).prop('checked'));
+            filter = $(this).prop('checked') ? ':not(:checked)' : ':checked';
+            $('input.select-row' + filter, $table).prop('checked', $(this).prop('checked')).trigger('change', 'master-selected');
           }
           else {
-            target = $(this).closest('tr');
-            $('input.select-rows', table).prop('checked', $(".select-row:not(':checked')", table).length < 1);
+            $(this).closest('tr').toggleClass('crm-row-selected', $(this).prop('checked'));
+            if (data !== 'master-selected') {
+              $('input.select-rows', $table).prop('checked', $(".select-row:not(':checked')", $table).length < 1);
+            }
           }
-          target.toggleClass('crm-row-selected', $(this).is(':checked'));
         })
         .find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
       $('.crm-select2:not(.select2-offscreen, .select2-container)', e.target).crmSelect2();
       $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e.target).crmEntityRef();
+      // Cache Form Input initial values
+      $('form[data-warn-changes] :input', e.target).each(function() {
+        $(this).data('crm-initial-value', $(this).val());
+      });
     })
-    // Modal dialogs should disable scrollbars
     .on('dialogopen', function(e) {
-      if ($(e.target).dialog('option', 'modal')) {
-        $(e.target).addClass('modal-dialog');
+      var $el = $(e.target);
+      // Modal dialogs should disable scrollbars
+      if ($el.dialog('option', 'modal')) {
+        $el.addClass('modal-dialog');
         $('body').css({overflow: 'hidden'});
       }
+      $el.parent().find('.ui-dialog-titlebar-close').attr('title', ts('Close'));
+      // Add resize button
+      if ($el.parent().hasClass('crm-container') && $el.dialog('option', 'resizable')) {
+        $el.parent().find('.ui-dialog-titlebar').append($('<button class="crm-dialog-titlebar-resize ui-dialog-titlebar-close" title="'+ts('Toggle fullscreen')+'" style="right:2em;"/>').button({icons: {primary: 'ui-icon-newwin'}, text: false}));
+        $('.crm-dialog-titlebar-resize', $el.parent()).click(function(e) {
+          if ($el.data('origSize')) {
+            $el.dialog('option', $el.data('origSize'));
+            $el.data('origSize', null);
+          } else {
+            $el.data('origSize', {
+              position: 'center',
+              width: $el.dialog('option', 'width'),
+              height: $el.dialog('option', 'height')
+            });
+            var menuHeight = $('#civicrm-menu').height();
+            $el.dialog('option', {width: '100%', height: ($(window).height() - menuHeight), position: [0, menuHeight]});
+          }
+          e.preventDefault();
+        });
+      }
     })
     .on('dialogclose', function(e) {
+      // Restore scrollbars when closing modal
       if ($('.ui-dialog .modal-dialog').not(e.target).length < 1) {
         $('body').css({overflow: ''});
       }
-    });
+    })
+    .on('submit', function(e) {
+      // CRM-14353 - disable changes warn when submitting the form
+      $(this).removeAttr('data-warn-changes');
+    })
+    ;
+    
+    window.onbeforeunload = function() {
+      if (CRM.utils.initialValueChanged($('form[data-warn-changes]'))) {
+        return ts('You have unsaved changes.');
+       }
+    };
 
   /**
    * Function to make multiselect boxes behave as fields in small screens
@@ -631,48 +721,45 @@ CRM.validate = CRM.validate || {
   /**
    * @see https://wiki.civicrm.org/confluence/display/CRMDOC/Notification+Reference
    */
-  CRM.confirm = function (buttons, options, cancelLabel) {
-    var dialog, callbacks = {};
-    cancelLabel = cancelLabel || ts('Cancel');
-    var settings = {
+  CRM.confirm = function (options) {
+    var dialog, settings = {
       title: ts('Confirm Action'),
       message: ts('Are you sure you want to continue?'),
-      resizable: false,
-      modal: true,
       width: 'auto',
+      modal: true,
+      resizable: false,
+      dialogClass: 'crm-container crm-confirm',
       close: function () {
-        $(dialog).dialog('destroy').remove();
+        $(this).dialog('destroy').remove();
       },
-      buttons: {}
-    };
-
-    settings.buttons[cancelLabel] = function () {
-      dialog.trigger('crmConfirmNo').dialog('close');
+      options: {
+        no: ts('Cancel'),
+        yes: ts('Continue')
+      }
     };
-    options = options || {};
-    $.extend(settings, options);
-    if ($.isFunction(buttons)) {
-      callbacks[ts('Continue')] = buttons;
-    }
-    else if (_.isString(buttons) || !buttons) {
-      callbacks[buttons || ts('Continue')] = function() {};
+    $.extend(settings, ($.isFunction(options) ? arguments[1] : options) || {});
+    if (!settings.buttons && $.isPlainObject(settings.options)) {
+      settings.buttons = [];
+      $.each(settings.options, function(key, label) {
+        settings.buttons.push({
+          text: label,
+          click: function() {
+            var event = $.Event('crmConfirm:' + key);
+            $(this).trigger(event);
+            if (!event.isDefaultPrevented()) {
+              dialog.dialog('close');
+            }
+          }
+        });
+      });
     }
-    else {
-      callbacks = buttons;
+    dialog = $('<div class="crm-confirm-dialog"></div>').html(settings.message);
+    delete settings.options;
+    delete settings.message;
+    if ($.isFunction(options)) {
+      dialog.on('crmConfirm:yes', options);
     }
-    $.each(callbacks, function (label, callback) {
-      settings.buttons[label] = function () {
-        dialog.trigger('crmConfirmYes');
-        if (callback.call(dialog) !== false) {
-          dialog.dialog('close');
-        }
-      };
-    });
-    dialog = $('<div class="crm-confirm-dialog"></div>')
-      .html(options.message)
-      .dialog(settings)
-      .trigger('crmLoad');
-    return dialog;
+    return dialog.dialog(settings).trigger('crmLoad');
   };
 
   /**
@@ -738,11 +825,6 @@ CRM.validate = CRM.validate || {
     });
     // Handle qf form errors
     $('form :input.error', this).one('blur', function() {
-      // ignore autocomplete fields
-      if ($(this).is('.ac_input')) {
-        return;
-      }
-
       $('.ui-notify-message.error a.ui-notify-close').click();
       $(this).removeClass('error');
       $(this).next('span.crm-error').remove();
@@ -784,19 +866,16 @@ CRM.validate = CRM.validate || {
       messagesFromMarkup.call($('#crm-container'));
     }
 
-    // bind the event for image popup
     $('body')
-      .on('click', 'a.crm-image-popup', function() {
-        var o = $('<div class="crm-custom-image-popup"><img src=' + $(this).attr('href') + '></div>');
-
-        CRM.confirm('',
-          {
-            title: ts('Preview'),
-            message: o
-          },
-          ts('Done')
-        );
-        return false;
+      // bind the event for image popup
+      .on('click', 'a.crm-image-popup', function(e) {
+        CRM.confirm({
+          title: ts('Preview'),
+          resizable: true,
+          message: '<div class="crm-custom-image-popup"><img src=' + $(this).attr('href') + '></div>',
+          options: null
+        });
+        e.preventDefault();
       })
 
       .on('click', function (event) {
@@ -814,31 +893,34 @@ CRM.validate = CRM.validate || {
       })
       .on('change', 'input.crm-form-radio:checked', function() {
         $(this).siblings('.crm-clear-link').css({visibility: ''});
-      });
-
-    $().crmtooltip();
-  });
+      })
 
-  $.fn.crmAccordions = function (speed) {
-    var container = $(this).length > 0 ? $(this) : $('.crm-container');
-    speed = speed === undefined ? 200 : speed;
-    container
-      .off('click.crmAccordions')
-      // Allow normal clicking of links
+      // Allow normal clicking of links within accordions
       .on('click.crmAccordions', 'div.crm-accordion-header a', function (e) {
-        e.stopPropagation && e.stopPropagation();
+        e.stopPropagation();
       })
-      .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function () {
+      // Handle accordions
+      .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function (e) {
         if ($(this).parent().hasClass('collapsed')) {
-          $(this).next().css('display', 'none').slideDown(speed);
+          $(this).next().css('display', 'none').slideDown(200);
         }
         else {
-          $(this).next().css('display', 'block').slideUp(speed);
+          $(this).next().css('display', 'block').slideUp(200);
         }
         $(this).parent().toggleClass('collapsed');
-        return false;
+        e.preventDefault();
       });
-  };
+
+    $().crmtooltip();
+  });
+  /**
+   * @deprecated
+   */
+  $.fn.crmAccordions = function () {};
+  /**
+   * Collapse or expand an accordion
+   * @param speed
+   */
   $.fn.crmAccordionToggle = function (speed) {
     $(this).each(function () {
       if ($(this).hasClass('collapsed')) {
@@ -879,4 +961,4 @@ CRM.validate = CRM.validate || {
     result = sign + (j ? i.substr(0, j) + separator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + separator) + (2 ? decimal + Math.abs(value - i).toFixed(2).slice(2) : '');
     return format.replace(/1.*234.*56/, result);
   };
-})(jQuery);
+})(jQuery, _);