summaryrefslogtreecommitdiff
path: root/modules/field_ui/field_ui.js
blob: b63399f3d28dab70a58958f9bc87a1cdbfdc1c87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330

(function($) {

Drupal.behaviors.fieldUIFieldOverview = {
  attach: function (context, settings) {
    $('table#field-overview', context).once('field-overview', function () {
      Drupal.fieldUIFieldOverview.attachUpdateSelects(this, settings);
    });
  }
};

Drupal.fieldUIFieldOverview = {
  /**
   * Implements dependent select dropdowns on the 'Manage fields' screen.
   */
  attachUpdateSelects: function(table, settings) {
    var widgetTypes = settings.fieldWidgetTypes;
    var fields = settings.fields;

    // Store the default text of widget selects.
    $('.widget-type-select', table).each(function () {
      this.initialValue = this.options[0].text;
    });

    // 'Field type' select updates its 'Widget' select.
    $('.field-type-select', table).each(function () {
      this.targetSelect = $('.widget-type-select', $(this).parents('tr').eq(0));

      $(this).bind('change keyup', function () {
        var selectedFieldType = this.options[this.selectedIndex].value;
        var options = (selectedFieldType in widgetTypes ? widgetTypes[selectedFieldType] : []);
        this.targetSelect.fieldUIPopulateOptions(options);
      });

      // Trigger change on initial pageload to get the right widget options
      // when field type comes pre-selected (on failed validation).
      $(this).trigger('change', false);
    });

    // 'Existing field' select updates its 'Widget' select and 'Label' textfield.
    $('.field-select', table).each(function () {
      this.targetSelect = $('.widget-type-select', $(this).parents('tr').eq(0));
      this.targetTextfield = $('.label-textfield', $(this).parents('tr').eq(0));

      $(this).bind('change keyup', function (e, updateText) {
        var updateText = (typeof updateText == 'undefined' ? true : updateText);
        var selectedField = this.options[this.selectedIndex].value;
        var selectedFieldType = (selectedField in fields ? fields[selectedField].type : null);
        var selectedFieldWidget = (selectedField in fields ? fields[selectedField].widget : null);
        var options = (selectedFieldType && (selectedFieldType in widgetTypes) ? widgetTypes[selectedFieldType] : []);
        this.targetSelect.fieldUIPopulateOptions(options, selectedFieldWidget);

        if (updateText) {
          $(this.targetTextfield).attr('value', (selectedField in fields ? fields[selectedField].label : ''));
        }
      });

      // Trigger change on initial pageload to get the right widget options
      // and label when field type comes pre-selected (on failed validation).
      $(this).trigger('change', false);
    });
  }
};

/**
 * Populates options in a select input.
 */
jQuery.fn.fieldUIPopulateOptions = function (options, selected) {
  return this.each(function () {
    var disabled = false;
    if (options.length == 0) {
      options = [this.initialValue];
      disabled = true;
    }

    // If possible, keep the same widget selected when changing field type.
    // This is based on textual value, since the internal value might be
    // different (options_buttons vs. node_reference_buttons).
    var previousSelectedText = this.options[this.selectedIndex].text;

    var html = '';
    jQuery.each(options, function (value, text) {
      // Figure out which value should be selected. The 'selected' param
      // takes precedence.
      var is_selected = ((typeof selected != 'undefined' && value == selected) || (typeof selected == 'undefined' && text == previousSelectedText));
      html += '<option value="' + value + '"' + (is_selected ? ' selected="selected"' : '') + '>' + text + '</option>';
    });

    $(this).html(html).attr('disabled', disabled ? 'disabled' : '');
  });
};

Drupal.behaviors.fieldUIDisplayOverview = {
  attach: function (context, settings) {
    $('table#field-display-overview', context).once('field-display-overview', function() {
      Drupal.fieldUIOverview.attach(this, settings.fieldUIRowsData, Drupal.fieldUIDisplayOverview);
    });
  }
};

Drupal.fieldUIOverview = {
  /**
   * Attaches the fieldUIOverview behavior.
   */
  attach: function (table, rowsData, rowHandlers) {
    var tableDrag = Drupal.tableDrag[table.id];

    // Add custom tabledrag callbacks.
    tableDrag.onDrop = this.onDrop;
    tableDrag.row.prototype.onSwap = this.onSwap;

    // Create row handlers.
    $('tr.draggable', table).each(function () {
      // Extract server-side data for the row.
      var row = this;
      if (row.id in rowsData) {
        var data = rowsData[row.id];
        data.tableDrag = tableDrag;

        // Create the row handler, make it accessible from the DOM row element.
        var rowHandler = eval('new rowHandlers.' + data.rowHandler + '(row, data);');
        $(row).data('fieldUIRowHandler', rowHandler);
      }
    });
  },

  /**
   * Event handler to be attached to form inputs triggering a region change.
   */
  onChange: function () {
    var $trigger = $(this);
    var row = $trigger.parents('tr:first').get(0);
    var rowHandler = $(row).data('fieldUIRowHandler');

    var refreshRows = {};
    refreshRows[rowHandler.name] = $trigger.get(0);

    // Handle region change.
    var region = rowHandler.getRegion();
    if (region != rowHandler.region) {
      // Remove parenting.
      $('select.field-parent', row).val('');
      // Let the row handler deal with the region change.
      $.extend(refreshRows, rowHandler.regionChange(region));
      // Update the row region.
      rowHandler.region = region;
    }

    // Ajax-update the rows.
    Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
  },

  /**
   * Lets row handlers react when a row is dropped into a new region.
   */
  onDrop: function () {
    var dragObject = this;
    var row = dragObject.rowObject.element;
    var rowHandler = $(row).data('fieldUIRowHandler');
    if (rowHandler !== undefined) {
      var regionRow = $(row).prevAll('tr.region-message').get(0);
      var region = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2');

      if (region != rowHandler.region) {
        // Let the row handler deal with the region change.
        refreshRows = rowHandler.regionChange(region);
        // Update the row region.
        rowHandler.region = region;
        // Ajax-update the rows.
        Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
      }
    }
  },

  /**
   * Refreshes placeholder rows in empty regions while a row is being dragged.
   *
   * Copied from block.js.
   *
   * @param table
   *   The table DOM element.
   * @param rowObject
   *   The tableDrag rowObject for the row being dragged.
   */
  onSwap: function (draggedRow) {
    var rowObject = this;
    $('tr.region-message', rowObject.table).each(function () {
      // If the dragged row is in this region, but above the message row, swap
      // it down one space.
      if ($(this).prev('tr').get(0) == rowObject.group[rowObject.group.length - 1]) {
        // Prevent a recursion problem when using the keyboard to move rows up.
        if ((rowObject.method != 'keyboard' || rowObject.direction == 'down')) {
          rowObject.swap('after', this);
        }
      }
      // This region has become empty.
      if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').length == 0) {
        $(this).removeClass('region-populated').addClass('region-empty');
      }
      // This region has become populated.
      else if ($(this).is('.region-empty')) {
        $(this).removeClass('region-empty').addClass('region-populated');
      }
    });
  },

  /**
   * Triggers Ajax refresh of selected rows.
   *
   * The 'format type' selects can trigger a series of changes in child rows.
   * The #ajax behavior is therefore not attached directly to the selects, but
   * triggered manually through a hidden #ajax 'Refresh' button.
   *
   * @param rows
   *   A hash object, whose keys are the names of the rows to refresh (they
   *   will receive the 'ajax-new-content' effect on the server side), and
   *   whose values are the DOM element in the row that should get an Ajax
   *   throbber.
   */
  AJAXRefreshRows: function (rows) {
    // Separate keys and values.
    var rowNames = [];
    var ajaxElements = [];
    $.each(rows, function (rowName, ajaxElement) {
      rowNames.push(rowName);
      ajaxElements.push(ajaxElement);
    });

    if (rowNames.length) {
      // Add a throbber next each of the ajaxElements.
      var $throbber = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
      $(ajaxElements)
        .addClass('progress-disabled')
        .after($throbber);

      // Fire the Ajax update.
      $('input[name=refresh_rows]').val(rowNames.join(' '));
      $('input#edit-refresh').mousedown();

      // Disabled elements do not appear in POST ajax data, so we mark the
      // elements disabled only after firing the request.
      $(ajaxElements).attr('disabled', true);
    }
  }
};


/**
 * Row handlers for the 'Manage display' screen.
 */
Drupal.fieldUIDisplayOverview = {};

/**
 * Constructor for a 'field' row handler.
 *
 * This handler is used for both fields and 'extra fields' rows.
 *
 * @param row
 *   The row DOM element.
 * @param data
 *   Additional data to be populated in the constructed object.
 */
Drupal.fieldUIDisplayOverview.field = function (row, data) {
  this.row = row;
  this.name = data.name;
  this.region = data.region;
  this.tableDrag = data.tableDrag;

  // Attach change listener to the 'formatter type' select.
  this.$formatSelect = $('select.field-formatter-type', row);
  this.$formatSelect.change(Drupal.fieldUIOverview.onChange);

  return this;
};

Drupal.fieldUIDisplayOverview.field.prototype = {
  /**
   * Returns the region corresponding to the current form values of the row.
   */
  getRegion: function () {
    return (this.$formatSelect.val() == 'hidden') ? 'hidden' : 'visible';
  },

  /**
   * Reacts to a row being changed regions.
   *
   * This function is called when the row is moved to a different region, as a
   * result of either :
   * - a drag-and-drop action (the row's form elements then probably need to be
   *   updated accordingly)
   * - user input in one of the form elements watched by the
   *   Drupal.fieldUIOverview.onChange change listener.
   *
   * @param region
   *   The name of the new region for the row.
   * @return
   *   A hash object indicating which rows should be Ajax-updated as a result
   *   of the change, in the format expected by
   *   Drupal.displayOverview.AJAXRefreshRows().
   */
  regionChange: function (region) {

    // When triggered by a row drag, the 'format' select needs to be adjusted
    // to the new region.
    var currentValue = this.$formatSelect.val();
    switch (region) {
      case 'visible':
        if (currentValue == 'hidden') {
          // Restore the formatter back to the default formatter. Pseudo-fields do
          // not have default formatters, we just return to 'visible' for those.
          var value = (this.defaultFormatter != undefined) ? this.defaultFormatter : 'visible';
        }
        break;

      default:
        var value = 'hidden';
        break;
    }
    if (value != undefined) {
      this.$formatSelect.val(value);
    }

    var refreshRows = {};
    refreshRows[this.name] = this.$formatSelect.get(0);

    return refreshRows;
  }
};

})(jQuery);