region.js

Maintainability

64.40

Lines of code

369

Created with Raphaël 2.1.002550751002016-7-102016-6-102016-5-112016-4-112016-3-122016-2-112016-1-12

2016-8-9
Maintainability: 64.4

Created with Raphaël 2.1.001002003004002016-7-102016-6-102016-5-112016-4-112016-3-122016-2-112016-1-12

2016-8-9
Lines of Code: 369

Difficulty

47.02

Estimated Errors

2.26

Function weight

By Complexity

Created with Raphaël 2.1.0<anonymous>19

By SLOC

Created with Raphaël 2.1.0<anonymous>108
1
/* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */
2
 
3
// Region
4
// ------
5
 
6
// Manage the visual regions of your composite application. See
7
// http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
8
 
9
Marionette.Region = Marionette.Object.extend({
10
  constructor: function(options) {
11
 
12
    // set options temporarily so that we can get `el`.
13
    // options will be overriden by Object.constructor
14
    this.options = options || {};
15
    this.el = this.getOption('el');
16
 
17
    // Handle when this.el is passed in as a $ wrapped element.
18
    this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el;
19
 
20
    if (!this.el) {
21
      throw new Marionette.Error({
22
        name: 'NoElError',
23
        message: 'An "el" must be specified for a region.'
24
      });
25
    }
26
 
27
    this.$el = this.getEl(this.el);
28
    Marionette.Object.call(this, options);
29
  },
30
 
31
  // Displays a backbone view instance inside of the region.
32
  // Handles calling the `render` method for you. Reads content
33
  // directly from the `el` attribute. Also calls an optional
34
  // `onShow` and `onDestroy` method on your view, just after showing
35
  // or just before destroying the view, respectively.
36
  // The `preventDestroy` option can be used to prevent a view from
37
  // the old view being destroyed on show.
38
  // The `forceShow` option can be used to force a view to be
39
  // re-rendered if it's already shown in the region.
40
  show: function(view, options) {
41
    if (!this._ensureElement()) {
42
      return;
43
    }
44
 
45
    this._ensureViewIsIntact(view);
46
    Marionette.MonitorDOMRefresh(view);
47
 
48
    var showOptions     = options || {};
49
    var isDifferentView = view !== this.currentView;
50
    var preventDestroy  = !!showOptions.preventDestroy;
51
    var forceShow       = !!showOptions.forceShow;
52
 
53
    // We are only changing the view if there is a current view to change to begin with
54
    var isChangingView = !!this.currentView;
55
 
56
    // Only destroy the current view if we don't want to `preventDestroy` and if
57
    // the view given in the first argument is different than `currentView`
58
    var _shouldDestroyView = isDifferentView && !preventDestroy;
59
 
60
    // Only show the view given in the first argument if it is different than
61
    // the current view or if we want to re-show the view. Note that if
62
    // `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true.
63
    var _shouldShowView = isDifferentView || forceShow;
64
 
65
    if (isChangingView) {
66
      this.triggerMethod('before:swapOut', this.currentView, this, options);
67
    }
68
 
69
    if (this.currentView && isDifferentView) {
70
      delete this.currentView._parent;
71
    }
72
 
73
    if (_shouldDestroyView) {
74
      this.empty();
75
 
76
    // A `destroy` event is attached to the clean up manually removed views.
77
    // We need to detach this event when a new view is going to be shown as it
78
    // is no longer relevant.
79
    } else if (isChangingView && _shouldShowView) {
80
      this.currentView.off('destroy', this.empty, this);
81
    }
82
 
83
    if (_shouldShowView) {
84
 
85
      // We need to listen for if a view is destroyed
86
      // in a way other than through the region.
87
      // If this happens we need to remove the reference
88
      // to the currentView since once a view has been destroyed
89
      // we can not reuse it.
90
      view.once('destroy', this.empty, this);
91
 
92
      // make this region the view's parent,
93
      // It's important that this parent binding happens before rendering
94
      // so that any events the child may trigger during render can also be
95
      // triggered on the child's ancestor views
96
      view._parent = this;
97
      this._renderView(view);
98
 
99
      if (isChangingView) {
100
        this.triggerMethod('before:swap', view, this, options);
101
      }
102
 
103
      this.triggerMethod('before:show', view, this, options);
104
      Marionette.triggerMethodOn(view, 'before:show', view, this, options);
105
 
106
      if (isChangingView) {
107
        this.triggerMethod('swapOut', this.currentView, this, options);
108
      }
109
 
110
      // An array of views that we're about to display
111
      var attachedRegion = Marionette.isNodeAttached(this.el);
112
 
113
      // The views that we're about to attach to the document
114
      // It's important that we prevent _getNestedViews from being executed unnecessarily
115
      // as it's a potentially-slow method
116
      var displayedViews = [];
117
 
118
      var attachOptions = _.extend({
119
        triggerBeforeAttach: this.triggerBeforeAttach,
120
        triggerAttach: this.triggerAttach
121
      }, showOptions);
122
 
123
      if (attachedRegion && attachOptions.triggerBeforeAttach) {
124
        displayedViews = this._displayedViews(view);
125
        this._triggerAttach(displayedViews, 'before:');
126
      }
127
 
128
      this.attachHtml(view);
129
      this.currentView = view;
130
 
131
      if (attachedRegion && attachOptions.triggerAttach) {
132
        displayedViews = this._displayedViews(view);
133
        this._triggerAttach(displayedViews);
134
      }
135
 
136
      if (isChangingView) {
137
        this.triggerMethod('swap', view, this, options);
138
      }
139
 
140
      this.triggerMethod('show', view, this, options);
141
      Marionette.triggerMethodOn(view, 'show', view, this, options);
142
 
143
      return this;
144
    }
145
 
146
    return this;
147
  },
148
 
149
  triggerBeforeAttach: true,
150
  triggerAttach: true,
151
 
152
  _triggerAttach: function(views, prefix) {
153
    var eventName = (prefix || '') + 'attach';
154
    _.each(views, function(view) {
155
      Marionette.triggerMethodOn(view, eventName, view, this);
156
    }, this);
157
  },
158
 
159
  _displayedViews: function(view) {
160
    return _.union([view], _.result(view, '_getNestedViews') || []);
161
  },
162
 
163
  _renderView: function(view) {
164
    if (!view.supportsRenderLifecycle) {
165
      Marionette.triggerMethodOn(view, 'before:render', view);
166
    }
167
    view.render();
168
    if (!view.supportsRenderLifecycle) {
169
      Marionette.triggerMethodOn(view, 'render', view);
170
    }
171
  },
172
 
173
  _ensureElement: function() {
174
    if (!_.isObject(this.el)) {
175
      this.$el = this.getEl(this.el);
176
      this.el = this.$el[0];
177
    }
178
 
179
    if (!this.$el || this.$el.length === 0) {
180
      if (this.getOption('allowMissingEl')) {
181
        return false;
182
      } else {
183
        throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM');
184
      }
185
    }
186
    return true;
187
  },
188
 
189
  _ensureViewIsIntact: function(view) {
190
    if (!view) {
191
      throw new Marionette.Error({
192
        name: 'ViewNotValid',
193
        message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.'
194
      });
195
    }
196
 
197
    if (view.isDestroyed) {
198
      throw new Marionette.Error({
199
        name: 'ViewDestroyedError',
200
        message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.'
201
      });
202
    }
203
  },
204
 
205
  // Override this method to change how the region finds the DOM
206
  // element that it manages. Return a jQuery selector object scoped
207
  // to a provided parent el or the document if none exists.
208
  getEl: function(el) {
209
    return Backbone.$(el, Marionette._getValue(this.options.parentEl, this));
210
  },
211
 
212
  // Override this method to change how the new view is
213
  // appended to the `$el` that the region is managing
214
  attachHtml: function(view) {
215
    this.$el.contents().detach();
216
 
217
    this.el.appendChild(view.el);
218
  },
219
 
220
  // Destroy the current view, if there is one. If there is no
221
  // current view, it does nothing and returns immediately.
222
  empty: function(options) {
223
    var view = this.currentView;
224
 
225
    var emptyOptions = options || {};
226
    var preventDestroy  = !!emptyOptions.preventDestroy;
227
    // If there is no view in the region
228
    // we should not remove anything
229
    if (!view) { return this; }
230
 
231
    view.off('destroy', this.empty, this);
232
    this.triggerMethod('before:empty', view);
233
    if (!preventDestroy) {
234
      this._destroyView();
235
    }
236
    this.triggerMethod('empty', view);
237
 
238
    // Remove region pointer to the currentView
239
    delete this.currentView;
240
 
241
    if (preventDestroy) {
242
      this.$el.contents().detach();
243
    }
244
 
245
    return this;
246
  },
247
 
248
  // call 'destroy' or 'remove', depending on which is found
249
  // on the view (if showing a raw Backbone view or a Marionette View)
250
  _destroyView: function() {
251
    var view = this.currentView;
252
    if (view.isDestroyed) { return; }
253
 
254
    if (!view.supportsDestroyLifecycle) {
255
      Marionette.triggerMethodOn(view, 'before:destroy', view);
256
    }
257
    if (view.destroy) {
258
      view.destroy();
259
    } else {
260
      view.remove();
261
 
262
      // appending isDestroyed to raw Backbone View allows regions
263
      // to throw a ViewDestroyedError for this view
264
      view.isDestroyed = true;
265
    }
266
    if (!view.supportsDestroyLifecycle) {
267
      Marionette.triggerMethodOn(view, 'destroy', view);
268
    }
269
  },
270
 
271
  // Attach an existing view to the region. This
272
  // will not call `render` or `onShow` for the new view,
273
  // and will not replace the current HTML for the `el`
274
  // of the region.
275
  attachView: function(view) {
276
    if (this.currentView) {
277
      delete this.currentView._parent;
278
    }
279
    view._parent = this;
280
    this.currentView = view;
281
    return this;
282
  },
283
 
284
  // Checks whether a view is currently present within
285
  // the region. Returns `true` if there is and `false` if
286
  // no view is present.
287
  hasView: function() {
288
    return !!this.currentView;
289
  },
290
 
291
  // Reset the region by destroying any existing view and
292
  // clearing out the cached `$el`. The next time a view
293
  // is shown via this region, the region will re-query the
294
  // DOM for the region's `el`.
295
  reset: function() {
296
    this.empty();
297
 
298
    if (this.$el) {
299
      this.el = this.$el.selector;
300
    }
301
 
302
    delete this.$el;
303
    return this;
304
  }
305
 
306
},
307
 
308
// Static Methods
309
{
310
 
311
  // Build an instance of a region by passing in a configuration object
312
  // and a default region class to use if none is specified in the config.
313
  //
314
  // The config object should either be a string as a jQuery DOM selector,
315
  // a Region class directly, or an object literal that specifies a selector,
316
  // a custom regionClass, and any options to be supplied to the region:
317
  //
318
  // ```js
319
  // {
320
  //   selector: "#foo",
321
  //   regionClass: MyCustomRegion,
322
  //   allowMissingEl: false
323
  // }
324
  // ```
325
  //
326
  buildRegion: function(regionConfig, DefaultRegionClass) {
327
    if (_.isString(regionConfig)) {
328
      return this._buildRegionFromSelector(regionConfig, DefaultRegionClass);
329
    }
330
 
331
    if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) {
332
      return this._buildRegionFromObject(regionConfig, DefaultRegionClass);
333
    }
334
 
335
    if (_.isFunction(regionConfig)) {
336
      return this._buildRegionFromRegionClass(regionConfig);
337
    }
338
 
339
    throw new Marionette.Error({
340
      message: 'Improper region configuration type.',
341
      url: 'marionette.region.html#region-configuration-types'
342
    });
343
  },
344
 
345
  // Build the region from a string selector like '#foo-region'
346
  _buildRegionFromSelector: function(selector, DefaultRegionClass) {
347
    return new DefaultRegionClass({el: selector});
348
  },
349
 
350
  // Build the region from a configuration object
351
  // ```js
352
  // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false }
353
  // ```
354
  _buildRegionFromObject: function(regionConfig, DefaultRegionClass) {
355
    var RegionClass = regionConfig.regionClass || DefaultRegionClass;
356
    var options = _.omit(regionConfig, 'selector', 'regionClass');
357
 
358
    if (regionConfig.selector && !options.el) {
359
      options.el = regionConfig.selector;
360
    }
361
 
362
    return new RegionClass(options);
363
  },
364
 
365
  // Build the region directly from a given `RegionClass`
366
  _buildRegionFromRegionClass: function(RegionClass) {
367
    return new RegionClass();
368
  }
369
});