Source: cardcontroller.js

/**
 * Родительский "класс" для карточек.
 * На каждую карточку (для каждой записи) должен создаваться отдельный 
 * экземпляр контроллера.
 */
var IrisCardController = IrisController.extend({

  render: function() {},

  /**
   * Обработчики в формате 'event selector': 'handlerName'
   *
   * @example <caption>Обычное поле</caption>
   * events: {
   *   'field:edit #Name': 'onChangeName'
   * }
   *
   * @example <caption>Поле matrix</caption>
   * events: {
   *   'field:edit #Date__d_Contact_Date_Matrix': 'onChangeDateMatrix'
   * }
   */
  events: {},

  /**
   * Включены ли обработчики изменения поля в случае изменения поля программно
   */
  autoEditEventsEnabled: true,

  /**
   * Получить поле по названию поля
   *
   * @param {string} id Код поля
   *
   * @param {Object} [event] Событие. Этот параметр необходимо заполнять
   * только в случае получения поля matrix.
   *
   * @return {jQuery} Найденное поле
   *
   * @example <caption>Обычное поле</caption>
   * var code = this.getField('TaskStateID')
   *     .find('[value=' + this.fieldValue('TaskStateID') + ']').attr('code');
   *
   * @example <caption>Поле matrix</caption>
   * irisControllers.classes.c_Contact = IrisCardController.extend({
   *   events: {
   *     'field:edit #Date__d_Contact_Date_Matrix': 'myEvent'
   *   },
   *  
   *   myEvent: function(event) {
   *     console.log(this.getField('Date', event).val());
   *   }
   * });
   *
   */
  getField: function(id, event) {
    if (event == undefined) {
      return this.$el.find('#' + id);
    }
    // Для полей matrix
    var field_id = event.target.id;
    var parts = field_id.substring(1).split('__');
    var rec_id = parts[1];
    var detailCode = parts[2];
    return this.$el.find('#_' + id + '__' + rec_id + '__' + detailCode);
  },

  /**
   * Получить поля matrix
   *
   * По коду вкладки и названию поля возвращает массив найденных полей.
   * Количество элементов равно количеству строк в matrix. Поля возвращаются
   * в порядке их следования на форме.
   *
   * @param {string} detailCode Код вкладки
   *
   * @param {string} id Код поля
   *
   * @return {jQuery[]} Поля
   *
   * @example
   * var fields = this.getFields('d_Contact_Date_Matrix', 'Date');
   * _.each(fields, function(elem) {
   *   console.log(elem.val());
   * });
   */
  getFields: function(detailCode, id) {
    var detail = this.$el.find('.matrix[detail_code="' + detailCode + '"]');
    var fields = detail.find('.edtText, .edtText_selected, .edtText_empty, .edtText_invalid_data, .button, .id');
    var result = {};

    fields.each(function() {
      var elem = jQuery(this);
      if (elem.attr('id')) {
        var parts = elem.attr('id').substring(1).split('__');
        var fieldName = parts[0];
        var record_id = parts[1];
        if (fieldName == id) {
          result[record_id] = elem;
        }
      }
    });

    return result;
  },

  /**
   * Получить или установить значение поля по названию поля
   *
   * @param {string} id Код поля
   *
   * @param {string} [value] Значение поля. Если параметр
   * не передан, то метод вернет текущее значение поля.
   *
   * @param {Object} [event] Событие. Его необходимо передавать только 
   * для поля matrix. Если метод вызывается для matrix, то
   * можно аргумент event передавать вторым по счету, вместо value.
   *
   * @return {string|this} Значение поля, если метод используется
   * для получения значения поля. this, если присваивается знаечние.
   *
   * @example <caption>Для обычного поля</caption>
   * // Поле Name = Иванов Иван
   * this.fieldValue('Name', 'Иванов Иван');
   * // Вывод: Иванов Иван
   * console.log(this.fieldValue('Name'));
   *
   * @example <caption>Для matrix</caption>
   * events: {
   *   'field:edit #Date__d_Contact_Date_Matrix': 'myEvent'
   * },
   * myEvent: function(event) {
   *   console.log(this.fieldValue('Date', event));
   * }
   */
  fieldValue: function(id, value, event) {
    if (event == undefined && value != undefined && value.target != undefined) {
      return this.fieldValue(id, undefined, value);
    }

    if (event == undefined) {
      var field = this.getField(id);
      if (!field) {
        return undefined;
      }

      var type = field.attr('elem_type');
      // Wysiwyg редактор
      if (field.attr('is_wysiwyg') == 'yes') {
        if (value != undefined) {
          CKEDITOR.instances[field.attr('actualelement')].setData(value);
          this.triggerAutoEditEvents(field);
          return this;
        }
        else {
          if (CKEDITOR.instances[field.attr('actualelement')].checkDirty() == true) {
            // значение из ckeditor
            return CKEDITOR.instances[field.attr('actualelement')].getData();
          }
          else {
            // Если значение не менялось, то возьмем его из textarea    
            return field.val();
          }
        }
      }
      // Поле с номером телефона
      if (type == 'phone') {
        if (value != undefined) {
          return this.setPhoneFieldValue(field, value);
        }
        else {
          return field.attr('phone_value');
        }
      }
      // Для чекбокса возвращаем значение домена
      else if (type == 'checkbox') {
        if (value != undefined) {
          if (value === true || value === false) {
            field.attr('checked', value);
          }
          this.triggerAutoEditEvents(field);
          return this;
        }
        else {
          var domain_json = JSON.parse(field.attr('domain_json'));
          var on_index = parseInt(field.attr('checked_index'), 10);
          var value_index = 0;
          if ((!field.attr('checked') && on_index == 0) || (field.attr('checked') && on_index == 1)) {
            value_index = 1;
          }
          return domain_json.domain_values[value_index];
        }
      }
      // Для lookup поля значение в атрибуте lookup_value
      else if (type == 'lookup') {
        if (value != undefined) {
          if (value.Value != undefined) {
            field.val(value.Caption);
            field.attr('lookup_value', value.Value);
          }
          else {
            field.attr('lookup_value', value);
          }
          this.triggerAutoEditEvents(field);
          return this;
        }
        else {
          return field.attr('lookup_value');
        }
      }
      // Для остальных полей
      else {
        if (value != undefined) {
          field.val(value);
          if (field.attr('is_radio') == 'yes') {
            // TODO: переписать на jQuery (встроить в этот объект)
            refreshRadioButtonValue($(id));
          }
          this.triggerAutoEditEvents(field);
          return this;
        }
        else {
          return field.val();
        }
      }
    }

    // Для полей matrix
    var field_id = event.target.id;
    var parts = field_id.substring(1).split('__');
    var rec_id = parts[1];
    var detailCode = parts[2];
    //return this.$el.find('#_' + id + '__' + rec_id + '__' + detailCode);
    return this.fieldValue('_' + id + '__' + rec_id + '__' + detailCode, value);
  },

  /**
   * Получить или установить отображаемое значение поля. Имеет смысл для
   * lookup-полей.
   *
   * @param {string} id Код поля
   *
   * @param {string} [displayValue] Отображаемое значение поля. Если параметр
   * не передан, то метод вернет текущее значение поля.
   *
   * @param {Object} [event] Событие. Его необходимо передавать только 
   * для поля matrix. Если метод вызывается для matrix, то
   * можно аргумент event передавать вторым по счету, вместо value.
   *
   * @return {string|this} Отображаемое значение поля, если метод используется
   * для получения значения поля. this, если присваивается знаечние.
   *
   * @example <caption>Для обычного поля</caption>
   * // Поле Name = Иванов Иван
   * this.fieldDisplayValue('OwnerID', 'Иванов Иван');
   * // Вывод: Иванов Иван
   * console.log(this.fieldDisplayValue('OwnerID'));
   *
   * @example <caption>Для matrix</caption>
   * events: {
   *   'field:edit #Date__d_Contact_Date_Matrix': 'myEvent'
   * },
   * myEvent: function(event) {
   *   console.log(this.fieldDisplayValue('ProductID', event));
   * }
   */
  fieldDisplayValue: function(id, displayValue, event) {
    if (event == undefined && displayValue != undefined && 
        displayValue.target != undefined) {
      return this.fieldDisplayValue(id, undefined, displayValue);
    }

    var field = this.getField(id, event);
    var type = field.attr('elem_type');
    if (type == 'lookup') {
      if (displayValue == undefined) {
        return field.val();
      }
      else {
        field.val(displayValue);
        return this;
      }
    }
    return this.fieldValue(id, displayValue);
  },

  /**
   * Присвоить значение полю с телефонным номером. При присваивании выполняется
   * форматирование номера к формату "xx xxx xxx-xx-xx". Если присваивается
   * значение полю с дополнительным номером, формат номера будет "xxxx".
   *
   * @param {jQuery} field Поле
   *
   * @param {string} value Телефонный номер
   *
   * @return {this}
   *
   * @example
   * // Поле Phone1 = 7 925 555-55-55
   * this.setPhoneFieldValue(this.getField('Phone1'), '+79255555555');
   */
  setPhoneFieldValue: function(field, value) {
    var cleanDigits = value.replace(/\D/g, "");
    field.attr('phone_value', cleanDigits);
    
    if (field.attr('is_addl') == 'Y') {
      field.val(cleanDigits);
      this.triggerAutoEditEvents(field);
      return this; // для добавочного не форматируем
    }

    var formattedNumber = '';
    var nn = 0;
    for (var i = cleanDigits.length - 1; i >= 0; i--) {
      if (i < cleanDigits.length - 1) {
        if ((nn == 2) || (nn == 4)) {
          formattedNumber = '-' + formattedNumber;
        }
        if ((nn == 7) || (nn == 10)) {
          formattedNumber = ' ' + formattedNumber;
        }
      }
      nn++;
      formattedNumber = cleanDigits.charAt(i) + formattedNumber;
    }
    field.val(formattedNumber);
    this.triggerAutoEditEvents(field);
    return this;
  },

  /**
   * Получить или установить значение параметра карточки
   *
   * @param {string} id Код параметра
   *
   * @param {string} [value] Значение параметра. Если
   * не указан, то метод вернет текущее значение параметра.
   *
   * @param {Object} [event] Событие. Его необходимо передавать только 
   * для matrix. Если метод вызывается для параметра matrix,
   * то можно аргумент event передавать вторым по счету, вместо value.
   *
   * @return {string|this} Значение параметра, если метод используется
   * для получения значения. this, если присваивается знаечние.
   *
   * @example
   * // Если карточку открыли не в режиме добавления новой записи
   * if (this.parameter('mode') != 'insert') {
   *   // ..
   * }
   */
  parameter: function(id, value, event) {
    return this.fieldValue('_' + id, value, event);
  },

  /**
   * Получить или установить значение атрибута поля.
   *
   * Некоторые особенности
   * * В случае изменения атрибута required (или mandatory), изменяется
   * и css-класс заголовка поля.
   * * В случае изменения readonly для lookup-поля, меняется доступ к 
   * кнопке "...".
   * * В случае установки readonly для select-поля, поле становится disabled.
   *
   * @param {string} id Код поля
   *
   * @param {string} property Название атрибута
   *
   * @param {string} [value] Значение атрибута поля. Если параметр
   * не передан, то метод вернет текущее значение поля.
   *
   * @param {Object} [event] Событие. Его необходимо передавать только 
   * для поля matrix. Если метод вызывается для получения значения matrix, то
   * можно аргумент event передавать третьим по счету, вместо value.
   *
   * @return {string|this} Значение атрибута, если метод используется
   * для получения значения. this, если присваивается знаечние.
   *
   * @example
   * // Делаем поле CreateID недоступным для изменения
   * this.fieldProperty('CreateID', 'readonly', true);
   */
  fieldProperty: function(id, property, value, event) {
    if (event == undefined && value != undefined && 
        value.target != undefined) {
      return this.fieldProperty(id, property, undefined, value);
    }

    if (property == 'required') {
      return this.fieldProperty(id, 'mandatory', value);
    }

    var field = this.getField(id, event);
    if (value == undefined) {
      return field.attr(property);
    }
    else {
      if (property == 'mandatory') {
        var caption = field.parents('td.form_table').prev().find('span');
        if (value == 'yes' || value == true || value == 'true' || value == 1) {
          value = 'yes';
          caption.addClass('card_elem_mandatory');
        }
        else {
          value = 'no';
          caption.removeClass('card_elem_mandatory');
        }
      }
      else if (property == 'readonly') {
        var type = field.attr('elem_type');
        if (type == 'lookup') {
          if (value == true) {
            this.getField(id + '_btn').attr('disabled', true);
          }
          else {
            this.getField(id + '_btn').removeAttr('disabled');
          }
        }
      }
      if (type == 'select' || type == 'button') {
        field.attr('disabled', value);
      }
      else {
        field.attr(property, value);
      }
      return this;
    }
  },

  /**
   * Удалить атрибут у поля
   *
   * @param {string} id Код поля
   *
   * @param {string} property Название атрибута
   *
   * @param {Object} [event] Событие. Его необходимо передавать только 
   * для поля matrix.
   */
  fieldRemoveProperty: function(id, property, event) {
    this.getField(id, event).removeAttr(property);
  },

  /**
   * Получить или установить значение атрибута вида data-<атрибут>.
   *
   * @param {string} id Код поля
   *
   * @param {string} property Название атрибута
   *
   * @param {string} [value] Значение атрибута поля. Если параметр
   * не передан, то метод вернет текущее значение поля.
   *
   * @param {Object} [event] Событие. Его необходимо передавать только 
   * для поля matrix. Если метод вызывается для получения значения matrix, то
   * можно аргумент event передавать третьим по счету, вместо value.
   *
   * @return {string|this} Значение атрибута, если метод используется
   * для получения значения. this, если присваивается знаечние.
   *
   * @example
   * // Для поля Name значение атрибута data-mydata = myvalue.
   * this.fieldData('Name', 'mydata', 'myvalue');
   */
  fieldData: function(id, property, value, event) {
    return this.fieldProperty(id, 'data-' + property, value, event);
  },

  /**
   * Получить тип поля
   *
   * @param {string} id Код поля
   *
   * @param {Object} [event] Событие. Этот параметр необходимо заполнять
   * только в случае получения поля matrix.
   *
   * @return {string} Тип поля (значение атрибута elem_type).
   */
  fieldType: function(id, event) {
    return this.getField(id, event).attr('elem_type');
  },

  /**
   * Зависимость полей-справочников
   *
   * Работает только для lookup-lookup и select-select.
   * В случае select-select может быть немколько зависимых полей 
   * для одного master.
   *
   * TODO: Реализовать также связи для lookup-select, select-lookup
   *
   * TODO: Реализация с filter_where нецелесообразна
   *
   * @param masterFieldName Код основного поля
   *
   * @param dependentFieldName Код поля, значение в котором надо фильтровать
   *
   * @param [parentFieldName] Название поля в таблице, по которому 
   * надо фильтровать значения.
   *
   * @example
   * onChangeAccountID: function () {
   *   this.updateName(); 
   *   // Поле Контакт зависит от поля Компания
   *   this.bindFields('AccountID', 'ContactID');
   * },
   * onOpen: function () {
   *   this.getField('Number').attr('readonly', 'readonly');
   *   // Поле Контакт зависит от поля Компания
   *   this.bindFields('AccountID', 'ContactID');
   *   this.onChangePaymentTypeID();
   * }
   */
  bindFields: function(masterFieldName, dependentFieldName, parentFieldName) {
    var master_type = this.fieldType(masterFieldName);
    if (master_type == 'lookup') {
      this._bindLookupFields(masterFieldName, dependentFieldName, parentFieldName);
    }
    else {
      this._bindSelectFields(masterFieldName, dependentFieldName);
    }
  },

  // Связать lookup-поля
  _bindLookupFields: function(masterFieldName, dependentFieldName, parentFieldName) {
    if (parentFieldName == undefined) {
      parentFieldName = masterFieldName;
    }
    var masterId = this.fieldValue(masterFieldName);
    if (masterId == '') {
      this.fieldRemoveProperty(dependentFieldName, 'filter_where');
    }
    else {
      this.fieldProperty(dependentFieldName, 'filter_where', 
          "T0." + parentFieldName + " = '" + masterId + "'");
    }
  },

  // Связать select-поля
  _bindSelectFields: function(masterFieldName, dependentFieldName) {
    var dependent = this.getField(dependentFieldName);
    var master = this.getField(masterFieldName);

    // Создаем json из option, если еще не сделали это
    if (!this.fieldData(dependentFieldName, 'options')) {
      var options = [];
      dependent.find('option').each(function() {
        var option = jQuery(this);
        var attributes = [];
        var attrs = option.get(0).attributes;
        _.each(attrs, function (attr) {
          if (attr.nodeName.toLowerCase() != 'selected') {
            attributes.push([attr.nodeName, attr.nodeValue]);
          }
        });
        options.push([option.attr(masterFieldName.toLowerCase()), option.html(), 
            option.val(), attributes]);
      });
      this.fieldData(dependentFieldName, 'options', JSON.stringify(options));
    }

    if (!this.fieldData(masterFieldName, 'dependent')) {
      this.fieldData(masterFieldName, 'dependent', 
          JSON.stringify([dependentFieldName]));
    }
    else {
      var data = JSON.parse(this.fieldData(masterFieldName, 'dependent'));
      if (_.indexOf(data, dependentFieldName) == -1) {
        data.push(dependentFieldName);
        this.fieldData(masterFieldName, 'dependent', JSON.stringify(data));
      }
    }

    var self = this;
    master.on('field:edited', function () {
      self._filterDependentSelect(masterFieldName);
    });
    this.triggerAutoEditEvents(master);
  },

  // Фильтрация зависимого select
  _filterDependentSelect: function(masterId) {
    var master = this.getField(masterId);
    var masterValue = this.fieldValue(masterId);

    var dependents = JSON.parse(this.fieldData(masterId, 'dependent'));
    var self = this;

    // По всем зависимым полям
    _.each(dependents, function(dependentId) {
      var dependent = self.getField(dependentId);
      var dependentValue = self.fieldValue(dependentId);
      var options = JSON.parse(self.fieldData(dependentId, 'options'));
      // Удаляем список option
      dependent.empty();
      // Добавляем option для выбранного master
      _.each(options, function(option) {
        if ((!option[0]) || (option[0] == masterValue.toLowerCase())) {
          var selectOption = dependent.append(jQuery('<option value="' + 
              option[2] + '">' + option[1] + '</option>'));
          _.each(option[3], function(attr) {
            selectOption.attr(attr[0], attr[1]);
          });
        }
      });

      // При открытии карточки - выберем значение, которое уже установлено
      if (!self.fieldData(dependentId, 'inited')) {
        dependent.find('[value=' + dependentValue + ']')
            .attr('selected', 'selected');
        self.fieldData(dependentId, 'inited', true);
      }
      else {
        dependent.find('option:first').attr('selected', 'selected');
      }
    });

    return;
  },

  /**
   * Стандартный обработчик для случая, когда обработкой события изменения поля
   * занимается сервер. Этот обработчик отправляет запрос для вызова серверного
   * onBeforePost<Код поля>(), который должен быть расположен в файле 
   * с префиксом s\_ для раздела или ds\_ для вкладки.
   *
   * @param {Object} event Событие
   *
   * @example <caption>Назначение обработчика изменения поля</caption>
   * events: {
   *   'change #Price, #Count, #UnitID, #Discount': 'onChangeEvent',
   *   'lookup:changed #ProductID': 'onChangeEvent'
   * }
   *
   * @example <caption>Пример обработчика на PHP</caption>
   * public function onBeforePostPrice($parameters) {
   *     list ($count, $price, $discount) = 
   *             $this->getActualValue($parameters['old_data'], 
   *             $parameters['new_data'], array('count', 'price', 'discount'));
   *
   *     $parameters['new_data'] = FieldValueFormat('Amount', 
   *             ((100 - $discount) * $count * $price) / 100, null, 
   *             $parameters['new_data']);
   *
   *     return $parameters['new_data'];
   * }
   */
  onChangeEvent: function(event) {
    this.serverEvent('onBeforePost', event.target.id);
  },

  /**
   * Вызвать событие на сервере (в файле с s\_ для раздела или ds\_ 
   * для вкладки).
   *
   * Будет выполнена попытка вызова серверного обработчика, 
   * название которого образовано формулой eventName + fieldName
   *
   * @param {string} eventName вызываемое событие
   *
   * @param {string} [fieldName] Изменяемое поле
   */
  serverEvent: function(eventName, fieldName) {
    // На данный момент поддерживается только onBeforePost
    //var self = this;
    var p_form = $(this.el).down('form');
    //var value = c_Common_GetElementValue(p_form.elements[fieldName]);
    var className = this.getField('_code').val();
    if (className.substring(0, 1) == 'c' || className.substring(0, 1) == 'g') {
      className = 's' + className.substring(1);
    }
    else
    if (className.substring(0, 2) == 'dc' || className.substring(0, 1) == 'dg') {
      className = className.substring(0, 1) + 's' + className.substring(2);
    }

    Transport.request({
      section: this.getField('_source_name').val(),
      'class': className, 
      method: eventName + fieldName, 
      parameters: {
        //id: self.getField('_id').val(), //TODO: deprecated
        //value: value, //TODO: deprecated
        old_data: this._getFieldValues(),
        new_data: this._getFieldValues([fieldName])
      }, 
      onSuccess: function(transport) {
        //var response = transport.responseText.evalJSON();
        c_Common_LinkedField_OnChange_end(
            transport, p_form, true, undefined, true);
      }
    });
  },

  // Получить значения полей из карточки
  // @param [fieldNames] Array Список названий полей, которые требуется вернуть.
  // Если не передан, то возвращает все значения.
  _getFieldValues: function(fieldNames) {
    if (fieldNames == undefined) {
      fieldNames = [];
    }
    var fieldValuesArray = GetFieldValues($(this.el).down('form'));
    var fieldValues = {
      FieldValues: []
    };
    for (var i = 0; i < fieldValuesArray.length; i++) {
      if (fieldNames.length == 0 || 
          (fieldNames.length > 0 && fieldValuesArray[i][0] in fieldNames)) {
        fieldValues.FieldValues.push({
          Name: fieldValuesArray[i][0],
          Value: fieldValuesArray[i][1]
        });
      }
    }
    return fieldValues;
  },

  /**
   * Показать/скрыть поле
   *
   * @param {string} id Код поля
   *
   * @param {bool} [show=true] Показать или скрыть поле
   */
  showField: function(id, show, event) {
    if (show == undefined) {
      show = true;
    }
    var td = this.getField(id, event).parents('td.form_table');
    var tr = td.parent();
    if (show) {
      td.show();
      td.prev().show();
      tr.show();
    }
    else {
      td.hide();
      td.prev().hide();
      if (tr.find('td:visible').length == 0) {
        tr.hide();
      }
    }
  },

  /**
   * Скрыть поле
   *
   * @param {string} id Код поля
   */
  hideField: function(id, event) {
    this.showField(id, false, event);
  },

  /**
   * Вызвать событие изменения поля, если это разрешено
   *
   * @param {jQuery} field Поле, которое изменилось программно
   */
  triggerAutoEditEvents: function(field) {
    if (this.autoEditEventsEnabled) {
      field.trigger('field:edited');
    }
  }

});