test fixes, I feel it's caching issue
[civicrm-core.git] / js / Common.js
index d219d60c5560baa77919c29e5e39894f9dc22ec3..70b02c2c8390eb6c2bf78dd137af9ee14327a146 100644 (file)
@@ -1,45 +1,14 @@
-/*
- +--------------------------------------------------------------------+
- | CiviCRM version 4.4                                                |
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2013                                |
- +--------------------------------------------------------------------+
- | This file is a part of CiviCRM.                                    |
- |                                                                    |
- | CiviCRM is free software; you can copy, modify, and distribute it  |
- | under the terms of the GNU Affero General Public License           |
- | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
- |                                                                    |
- | CiviCRM is distributed in the hope that it will be useful, but     |
- | WITHOUT ANY WARRANTY; without even the implied warranty of         |
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
- | See the GNU Affero General Public License for more details.        |
- |                                                                    |
- | You should have received a copy of the GNU Affero General Public   |
- | License and the CiviCRM Licensing Exception along                  |
- | with this program; if not, contact CiviCRM LLC                     |
- | at info[AT]civicrm[DOT]org. If you have questions about the        |
- | GNU Affero General Public License or the licensing of CiviCRM,     |
- | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
- +--------------------------------------------------------------------+
- */
-
-/**
- * @file: global functions for CiviCRM
- * FIXME: We are moving away from using global functions. DO NOT ADD MORE.
- * @see CRM object - the better alternative to adding global functions
- */
-
+// https://civicrm.org/licensing
 var CRM = CRM || {};
 var cj = jQuery;
 
 /**
  * Short-named function for string translation, defined in global scope so it's available everywhere.
  *
- * @param  $text   string  string for translating
- * @param  $params object  key:value of additional parameters
+ * @param text string for translating
+ * @param params object key:value of additional parameters
  *
- * @return         string  the translated string
+ * @return string
  */
 function ts(text, params) {
   "use strict";
@@ -62,11 +31,10 @@ function ts(text, params) {
  *  a list of 'blocks to show' and 'blocks to hide' and the template passes these parameters to
  *  this function.
  *
- * @access public
+ * @deprecated
  * @param  showBlocks Array of element Id's to be displayed
  * @param  hideBlocks Array of element Id's to be hidden
  * @param elementType Value to set display style to for showBlocks (e.g. 'block' or 'table-row' or ...)
- * @return none
  */
 function on_load_init_blocks(showBlocks, hideBlocks, elementType) {
   if (elementType == null) {
@@ -102,14 +70,13 @@ function on_load_init_blocks(showBlocks, hideBlocks, elementType) {
  *  This function is called when we need to show or hide a related form element (target_element)
  *  based on the value (trigger_value) of another form field (trigger_field).
  *
- * @access public
+ * @deprecated
  * @param  trigger_field_id     HTML id of field whose onchange is the trigger
  * @param  trigger_value        List of integers - option value(s) which trigger show-element action for target_field
  * @param  target_element_id    HTML id of element to be shown or hidden
  * @param  target_element_type  Type of element to be shown or hidden ('block' or 'table-row')
  * @param  field_type           Type of element radio/select
  * @param  invert               Boolean - if true, we HIDE target on value match; if false, we SHOW target on value match
- * @return none
  */
 function showHideByValue(trigger_field_id, trigger_value, target_element_id, target_element_type, field_type, invert) {
   if (target_element_type == null) {
@@ -123,7 +90,7 @@ function showHideByValue(trigger_field_id, trigger_value, target_element_id, tar
 
   if (field_type == 'select') {
     var trigger = trigger_value.split("|");
-    var selectedOptionValue = document.getElementById(trigger_field_id).options[document.getElementById(trigger_field_id).selectedIndex].value;
+    var selectedOptionValue = cj('#' + trigger_field_id).val();
 
     var target = target_element_id.split("|");
     for (var j = 0; j < target.length; j++) {
@@ -150,7 +117,7 @@ function showHideByValue(trigger_field_id, trigger_value, target_element_id, tar
     if (field_type == 'radio') {
       var target = target_element_id.split("|");
       for (var j = 0; j < target.length; j++) {
-        if (document.getElementsByName(trigger_field_id)[0].checked) {
+        if (cj('[name="' + trigger_field_id + '"]').is(':checked')) {
           if (invert) {
             cj('#' + target[j]).hide();
           }
@@ -171,29 +138,13 @@ function showHideByValue(trigger_field_id, trigger_value, target_element_id, tar
   }
 }
 
-/**
- * reset all the radio buttons with a given name
- *
- * @param string fieldName
- * @param object form
- * @return null
- */
-function unselectRadio(fieldName, form) {
-  for (i = 0; i < document.forms[form].elements.length; i++) {
-    if (document.forms[form].elements[i].name == fieldName) {
-      document.forms[form].elements[i].checked = false;
-    }
-  }
-  return;
-}
-
 /**
  * Function to change button text and disable one it is clicked
- *
+ * @deprecated
  * @param obj object - the button clicked
  * @param formID string - the id of the form being submitted
  * @param string procText - button text after user clicks it
- * @return null
+ * @return bool
  */
 var submitcount = 0;
 /* Changes button label on submit, and disables button after submit for newer browsers.
@@ -219,7 +170,9 @@ function submitOnce(obj, formId, procText) {
     }
   }
 }
-
+/**
+ * @deprecated
+ */
 function popUp(URL) {
   day = new Date();
   id = day.getTime();
@@ -228,8 +181,8 @@ function popUp(URL) {
 
 /**
  * Function to show / hide the row in optionFields
- *
- * @param element name index, that whose innerHTML is to hide else will show the hidden row.
+ * @deprecated
+ * @param index string, element whose innerHTML is to hide else will show the hidden row.
  */
 function showHideRow(index) {
   if (index) {
@@ -247,6 +200,7 @@ function showHideRow(index) {
   return false;
 }
 
+CRM.utils = CRM.utils || {};
 CRM.strings = CRM.strings || {};
 CRM.validate = CRM.validate || {
   params: {},
@@ -256,8 +210,7 @@ CRM.validate = CRM.validate || {
 (function ($, undefined) {
   "use strict";
 
-  // Set select2 defaults
-  $.fn.select2.defaults.minimumResultsForSearch = 10;
+  $.fn.select2.defaults.dropdownCssClass = 'crm-container';
   // https://github.com/ivaynberg/select2/pull/2090
   $.fn.select2.defaults.width = 'resolve';
 
@@ -266,54 +219,221 @@ CRM.validate = CRM.validate || {
     return !!$(e.target).closest('.ui-dialog, .ui-datepicker, .select2-drop').length;
   };
 
-  // Initialize widgets
-  $(document).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');
-        if ($(this).hasClass('select-rows')) {
-          target = $('tbody tr', table);
-          $('input.select-row', table).prop('checked', $(this).prop('checked'));
-        }
-        else {
-          target = $(this).closest('tr');
-          $('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', e.target).each(function() {
+  /**
+   * Populate a select list, overwriting the existing options except for the placeholder.
+   * @param $el jquery collection - 1 or more select elements
+   * @param options array in format returned by api.getoptions
+   * @param removePlaceholder bool
+   */
+  CRM.utils.setOptions = function($el, options, removePlaceholder) {
+    $el.each(function() {
+      var
+        $elect = $(this),
+        val = $elect.val() || [],
+        opts = removePlaceholder ? '' : '[value!=""]';
+      if (typeof(val) !== 'array') {
+        val = [val];
+      }
+      $elect.find('option' + opts).remove();
+      _.each(options, function(option) {
+        var selected = ($.inArray(''+option.key, val) > -1) ? 'selected="selected"' : '';
+        $elect.append('<option value="' + option.key + '"' + selected + '>' + option.value + '</option>');
+      });
+      $elect.trigger('crmOptionsUpdated', $.extend({}, options)).trigger('change');
+    });
+  };
+
+  /**
+   * Wrapper for select2 initialization function; supplies defaults
+   * @param options object
+   */
+  $.fn.crmSelect2 = function(options) {
+    return $(this).each(function () {
+      var
+        $el = $(this),
+        defaults = {allowClear: !$el.hasClass('required')};
       // quickform doesn't support optgroups so here's a hack :(
-      $('option[value^=crm_optgroup]', this).each(function() {
+      $('option[value^=crm_optgroup]', this).each(function () {
         $(this).nextUntil('option[value^=crm_optgroup]').wrapAll('<optgroup label="' + $(this).text() + '" />');
         $(this).remove();
       });
-      var options = $(this).data('select-params') || {};
-      // Api-based searching
-      if ($(this).data('api-params')) {
-        $(this).addClass('crm-ajax-select')
-        options.query = function(info) {
-          var api = $(info.element).data('api-params');
-          var params = api.params || {};
-          params[api.search] = info.term;
-          CRM.api3(api.entity, api.action, params).done(function(data) {
-            var results = {context: info.context, results: []};
-            if (typeof(data.values) === 'object') {
-              $.each(data.values, function(k, v) {
-                results.results.push({id: v[api.key], text: v[api.label]});
-              });
-            }
-            info.callback(results);
-          });
+      // Defaults for single-selects
+      if ($el.is('select:not([multiple])')) {
+        defaults.minimumResultsForSearch = 10;
+        if ($('option:first', this).val() === '') {
+          defaults.placeholderOption = 'first';
+        }
+      }
+      $el.select2($.extend(defaults, $el.data('select-params') || {}, options || {}));
+    });
+  };
+
+  /**
+   * @see CRM_Core_Form::addEntityRef for docs
+   * @param options object
+   */
+  $.fn.crmEntityRef = function(options) {
+    options = options || {};
+    options.select = options.select || {};
+    return $(this).each(function() {
+      var
+        $el = $(this),
+        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');
+      var settings = {
+        // Use select2 ajax helper instead of CRM.api because it provides more value
+        ajax: {
+          url: CRM.url('civicrm/ajax/rest'),
+          data: function (input, page_num) {
+            var params = $el.data('api-params') || {};
+            params.input = input;
+            params.page_num = page_num;
+            return {
+              entity: $el.data('api-entity'),
+              action: 'getlist',
+              json: JSON.stringify(params)
+            };
+          },
+          results: function(data) {
+            return {more: data.more_results, results: data.values || []};
+          }
+        },
+        minimumInputLength: 1,
+        formatResult: formatSelect2Result,
+        formatSelection: function(row) {
+          return row.label;
+        },
+        escapeMarkup: function (m) {return m;},
+        initSelection: function($el, callback) {
+          var
+            multiple = !!$el.data('select-params').multiple,
+            val = $el.val(),
+            stored = $el.data('entity-value') || [];
+          if (val === '') {
+            return;
+          }
+          // If we already have this data, just return it
+          if (!_.xor(val.split(','), _.pluck(stored, 'id')).length) {
+            callback(multiple ? stored : stored[0]);
+          } else {
+            var params = $.extend({}, $el.data('api-params') || {}, {id: val});
+            CRM.api3($el.data('api-entity'), 'getlist', params).done(function(result) {
+              callback(multiple ? result.values : result.values[0])
+            });
+          }
+        }
+      };
+      if ($el.data('create-links')) {
+        selectParams.formatInputTooShort = function() {
+          var txt = $el.data('select-params').formatInputTooShort || $.fn.select2.defaults.formatInputTooShort.call(this);
+          if ($el.data('create-links')) {
+            txt += ' ' + ts('or') + '<br />' + formatSelect2CreateLinks($el);
+          }
+          return txt;
         };
-        options.initSelection = function(el, callback) {
-          callback(el.data('entity-value'));
+        selectParams.formatNoMatches = function() {
+          var txt = $el.data('select-params').formatNoMatches || $.fn.select2.defaults.formatNoMatches;
+          return txt + '<br />' + formatSelect2CreateLinks($el);
         };
+        $el.off('.createLinks').on('select2-open.createLinks', function() {
+          var $el = $(this);
+          $('#select2-drop').off('.crmEntity').on('click.crmEntity', 'a.crm-add-entity', function(e) {
+            $el.select2('close');
+            CRM.loadForm($(this).attr('href'), {
+              dialog: {width: 500, height: 'auto'}
+            }).on('crmFormSuccess', function(e, data) {
+              if (data.status === 'success' && data.id) {
+                if ($el.select2('container').hasClass('select2-container-multi')) {
+                  var selection = $el.select2('data');
+                  selection.push(data);
+                  $el.select2('data', selection, true);
+                } else {
+                  $el.select2('data', data, true);
+                }
+              }
+            });
+            return false;
+          });
+        });
+      }
+      $el.crmSelect2($.extend(settings, $el.data('select-params'), selectParams));
+    });
+  };
+
+  function formatSelect2Result(row) {
+    var markup = '<div class="crm-select2-row">';
+    if (row.image !== undefined) {
+      markup += '<div class="crm-select2-image"><img src="' + row.image + '"/></div>';
+    }
+    else if (row.icon_class) {
+      markup += '<div class="crm-select2-icon"><div class="crm-icon ' + row.icon_class + '-icon"></div></div>';
+    }
+    markup += '<div><div class="crm-select2-row-label">' + row.label + '</div>';
+    markup += '<div class="crm-select2-row-description">';
+    $.each(row.description || [], function(k, text) {
+      markup += '<p>' + text + '</p>';
+    });
+    markup += '</div></div></div>';
+    return markup;
+  }
+
+  function formatSelect2CreateLinks($el) {
+    var
+      createLinks = $el.data('create-links'),
+      api = $el.data('api-params') || {},
+      type = api.params ? api.params.contact_type : null;
+    if (createLinks === true) {
+      createLinks = type ? _.where(CRM.profile.contactCreate, {type: type}) : CRM.profile.contactCreate;
+    }
+    var markup = '';
+    _.each(createLinks, function(link) {
+      markup += ' <a class="crm-add-entity crm-hover-button" href="' + link.url + '">';
+      if (link.type) {
+        markup += '<span class="icon ' + link.type + '-profile-icon"></span> ';
+      }
+      markup += link.label + '</a>';
+    });
+    return markup;
+  }
+
+  // Initialize widgets
+  $(document)
+    .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');
+          if ($(this).hasClass('select-rows')) {
+            target = $('tbody tr', table);
+            $('input.select-row', table).prop('checked', $(this).prop('checked'));
+          }
+          else {
+            target = $(this).closest('tr');
+            $('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)', e.target).crmSelect2();
+      $('.crm-form-entityref:not(.select2-offscreen)', e.target).crmEntityRef();
+    })
+    // Modal dialogs should disable scrollbars
+    .on('dialogopen', function(e) {
+      if ($(e.target).dialog('option', 'modal')) {
+        $(e.target).addClass('modal-dialog');
+        $('body').css({overflow: 'hidden'});
+      }
+    })
+    .on('dialogclose', function(e) {
+      if ($('.ui-dialog .modal-dialog').not(e.target).length < 1) {
+        $('body').css({overflow: ''});
       }
-      $(this).select2(options).removeClass('crm-select2');
     });
-  });
 
   /**
    * Function to make multiselect boxes behave as fields in small screens
@@ -397,15 +517,20 @@ CRM.validate = CRM.validate || {
       .on('click', 'a.crm-summary-link', false);
   };
 
-  var h;
+  var helpDisplay, helpPrevious;
   CRM.help = function (title, params, url) {
-    h && h.close && h.close();
-    var options = {
-      expires: 0
-    };
-    h = CRM.alert('...', title, 'crm-help crm-msg-loading', options);
+    if (helpDisplay && helpDisplay.close) {
+      // If the same link is clicked twice, just close the display - todo use underscore method for this comparison
+      if (helpDisplay.isOpen && helpPrevious === JSON.stringify(params)) {
+        helpDisplay.close();
+        return;
+      }
+      helpDisplay.close();
+    }
+    helpPrevious = JSON.stringify(params);
     params.class_name = 'CRM_Core_Page_Inline_Help';
     params.type = 'page';
+    helpDisplay = CRM.alert('...', title, 'crm-help crm-msg-loading', {expires: 0});
     $.ajax(url || CRM.url('civicrm/ajax/inline'),
       {
         data: params,
@@ -512,6 +637,7 @@ CRM.validate = CRM.validate || {
    *  passing in a function instead of an object is a shortcut for a sinlgle button labeled "Continue"
    * @param options {object|void} Override defaults, keys include 'title', 'message',
    *  see jQuery.dialog for full list of available params
+   * @param cancelLabel {string}
    */
   CRM.confirm = function (buttons, options, cancelLabel) {
     var dialog, callbacks = {};
@@ -541,8 +667,9 @@ CRM.validate = CRM.validate || {
     }
     $.each(callbacks, function (label, callback) {
       settings.buttons[label] = function () {
-        callback.call(dialog);
-        dialog.dialog('close');
+        if (callback.call(dialog) !== false) {
+          dialog.dialog('close');
+        }
       };
     });
     dialog = $('<div class="crm-container crm-confirm-dialog"></div>')
@@ -742,10 +869,7 @@ CRM.validate = CRM.validate || {
       settings.dialog = {
         modal: true,
         width: '65%',
-        height: parseInt($(window).height() * .75),
-        close: function() {
-          $(this).dialog('destroy').remove();
-        }
+        height: parseInt($(window).height() * .75)
       };
     }
     options && $.extend(true, settings, options);
@@ -753,10 +877,15 @@ CRM.validate = CRM.validate || {
     // Create new dialog
     if (settings.dialog) {
       $('<div id="'+ settings.target.substring(1) +'"><div class="crm-loading-element">' + ts('Loading') + '...</div></div>').dialog(settings.dialog);
+      $(settings.target).on('dialogclose', function() {
+        $(this).dialog('destroy').remove();
+      });
     }
     if (settings.dialog && !settings.dialog.title) {
-      $(settings.target).on('crmLoad', function(event, data) {
-        data.title && $(this).dialog('option', 'title', data.title);
+      $(settings.target).on('crmLoad', function(e, data) {
+        if (e.target === $(settings.target)[0] && data && data.title) {
+          $(this).dialog('option', 'title', data.title);
+        }
       });
     }
     $(settings.target).crmSnippet(settings).crmSnippet('refresh');
@@ -864,6 +993,8 @@ CRM.validate = CRM.validate || {
           return false;
         });
       }
+      // For convenience, focus the first field
+      $('input[type=text], textarea, select', this).filter(':visible').first().focus();
     });
     return widget;
   };
@@ -900,26 +1031,59 @@ CRM.validate = CRM.validate || {
     }
 
     // bind the event for image popup
-    $('body').on('click', 'a.crm-image-popup', function() {
-      var o = $('<div class="crm-container crm-custom-image-popup"><img src=' + $(this).attr('href') + '></div>');
+    $('body')
+      .on('click', 'a.crm-image-popup', function() {
+        var o = $('<div class="crm-container crm-custom-image-popup"><img src=' + $(this).attr('href') + '></div>');
 
-      CRM.confirm('',
-        {
-          title: ts('Preview'),
-          message: o
-        },
-        ts('Done')
-      );
-      return false;
-    });
+        CRM.confirm('',
+          {
+            title: ts('Preview'),
+            message: o
+          },
+          ts('Done')
+        );
+        return false;
+      })
 
+      .on('click', function (event) {
+        $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
+        if ($(event.target).is('.btn-slide')) {
+          $(event.target).addClass('btn-slide-active').find('.panel').show();
+        }
+      })
+
+      .on('click', 'a.crm-option-edit-link', function() {
+        var
+          link = $(this),
+          optionsChanged = false;
+        CRM.loadForm(this.href, {openInline: 'a:not("[href=#], .no-popup")'})
+          .on('crmFormSuccess', function() {
+            optionsChanged = true;
+          })
+          .on('dialogclose', function() {
+            if (optionsChanged) {
+              link.trigger('crmOptionsEdited');
+              var $elects = $('select[data-option-edit-path="' + link.data('option-edit-path') + '"]');
+              if ($elects.data('api-entity') && $elects.data('api-field')) {
+                CRM.api3($elects.data('api-entity'), 'getoptions', {sequential: 1, field: $elects.data('api-field')})
+                  .done(function (data) {
+                    CRM.utils.setOptions($elects, data.values);
+                  });
+              }
+            }
+          });
+        return false;
+      })
+      // Handle clear button for form elements
+      .on('click', 'a.crm-clear-link', function() {
+        $(this).css({visibility: 'hidden'}).siblings('.crm-form-radio:checked').prop('checked', false).change();
+        $(this).siblings('input:text').val('').change();
+        return false;
+      })
+      .on('change', 'input.crm-form-radio:checked', function() {
+        $(this).siblings('.crm-clear-link').css({visibility: ''});
+      });
     $().crmtooltip();
-    $('body').on('click', function (event) {
-      $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
-      if ($(event.target).is('.btn-slide')) {
-        $(event.target).addClass('btn-slide-active').find('.panel').show();
-      }
-    });
   });
 
   $.fn.crmAccordions = function (speed) {