-
Notifications
You must be signed in to change notification settings - Fork 42
/
conditioner-core.js
485 lines (390 loc) Β· 14.9 KB
/
conditioner-core.js
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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
/* conditioner-core 2.3.3 */
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['exports'], factory);
} else if (typeof exports !== "undefined") {
factory(exports);
} else {
var mod = {
exports: {}
};
factory(mod.exports);
global.conditioner = mod.exports;
}
})(this, function (exports) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () {
function sliceIterator(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
return function (arr, i) {
if (Array.isArray(arr)) {
return arr;
} else if (Symbol.iterator in Object(arr)) {
return sliceIterator(arr, i);
} else {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
};
}();
function _toConsumableArray(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
arr2[i] = arr[i];
}
return arr2;
} else {
return Array.from(arr);
}
}
// links the module to the element and exposes a callback api object
var bindModule = function bindModule(element, unbind) {
// gets the name of the module from the element, we assume the name is an alias
var alias = runPlugin('moduleGetName', element);
// sets the name of the plugin, this does nothing by default but allows devs to turn an alias into the actual module name
var name = chainPlugins('moduleSetName', alias);
// internal state
var state = {
destruct: null, // holder for unload method (function returned by module constructor)
mounting: false
};
// api wrapped around module object
var boundModule = {
// original name as found on the element
alias: alias,
// transformed name
name: name,
// reference to the element the module is bound to
element: element,
// is the module currently mounted?
mounted: false,
// unmounts the module
unmount: function unmount() {
// can't be unmounted if no destroy method has been supplied
// can't be unmounted if not mounted
if (!state.destruct || !boundModule.mounted) return;
// about to unmount the module
eachPlugins('moduleWillUnmount', boundModule);
// clean up
state.destruct();
// no longer mounted
boundModule.mounted = false;
// done unmounting the module
eachPlugins('moduleDidUnmount', boundModule);
// done unmounting
boundModule.onunmount.apply(element);
},
// requests and loads the module
mount: function mount() {
// can't mount an already mounted module
// can't mount a module that is currently mounting
if (boundModule.mounted || state.mounting) return;
// now mounting module
state.mounting = true;
// about to mount the module
eachPlugins('moduleWillMount', boundModule);
// get the module
runPlugin('moduleImport', name).then(function (module) {
// initialise the module, module can return a destroy mehod
state.destruct = runPlugin('moduleGetDestructor', runPlugin('moduleGetConstructor', module).apply(undefined, _toConsumableArray(runPlugin('moduleSetConstructorArguments', name, element))));
// no longer mounting
state.mounting = false;
// module is now mounted
boundModule.mounted = true;
// did mount the module
eachPlugins('moduleDidMount', boundModule);
// module has now loaded lets fire the onload event so everyone knows about it
boundModule.onmount.apply(element, [boundModule]);
}).catch(function (error) {
// failed to mount so no longer mounting
state.mounting = false;
// failed to mount the module
eachPlugins('moduleDidCatch', error, boundModule);
// callback for this specific module
boundModule.onmounterror.apply(element, [error, boundModule]);
// let dev know
throw new Error('Conditioner: ' + error);
});
// return state object
return boundModule;
},
// unmounts the module and destroys the attached monitors
destroy: function destroy() {
// about to destroy the module
eachPlugins('moduleWillDestroy', boundModule);
// not implemented yet
boundModule.unmount();
// did destroy the module
eachPlugins('moduleDidDestroy', boundModule);
// call public ondestroy so dev can handle it as well
boundModule.ondestroy.apply(element);
// call the destroy callback so monitor can be removed as well
unbind();
},
// called when fails to bind the module
onmounterror: function onmounterror() {},
// called when the module is loaded, receives the state object, scope is set to element
onmount: function onmount() {},
// called when the module is unloaded, scope is set to element
onunmount: function onunmount() {},
// called when the module is destroyed
ondestroy: function ondestroy() {}
};
// done!
return boundModule;
};
var queryParamsRegex = /(was)? ?(not)? ?@([a-z]+) ?(.*)?/;
var queryRegex = /(?:was )?(?:not )?@[a-z]+ ?.*?(?:(?= and (?:was )?(?:not )?@[a-z])|$)/g;
// convert context values to booleans if value is undefined or a boolean described as string
var toContextValue = function toContextValue(value) {
return typeof value === 'undefined' || value === 'true' ? true : value === 'false' ? false : value;
};
var extractParams = function extractParams(query) {
var _query$match = query.match(queryParamsRegex),
_query$match2 = _slicedToArray(_query$match, 5),
retain = _query$match2[1],
invert = _query$match2[2],
name = _query$match2[3],
value = _query$match2[4];
// extract groups, we ignore the first array index which is the entire matches string
return [name, toContextValue(value), invert === 'not', retain === 'was'];
};
// @media (min-width:30em) and was @visible true -> [ ['media', '(min-width:30em)', false, false], ['visible', 'true', false, true] ]
var parseQuery = function parseQuery(query) {
return query.match(queryRegex).map(extractParams);
};
// add intert and retain properties to monitor
var decorateMonitor = function decorateMonitor(monitor, invert, retain) {
monitor.invert = invert;
monitor.retain = retain;
monitor.matched = false;
return monitor;
};
// finds monitor plugins and calls the create method on the first found monitor
var getContextMonitor = function getContextMonitor(element, name, context) {
var monitor = getPlugins('monitor').find(function (monitor) {
return monitor.name === name;
});
// @exclude
if (!monitor) {
throw new Error('Conditioner: Cannot find monitor with name "@' + name + '". Only the "@media" monitor is always available. Custom monitors can be added with the `addPlugin` method using the `monitors` key. The name of the custom monitor should not include the "@" symbol.');
}
// @endexclude
return monitor.create(context, element);
};
// test if monitor contexts are currently valid
var matchMonitors = function matchMonitors(monitors) {
return monitors.reduce(function (matches, monitor) {
// an earlier monitor returned false, so current context will no longer be suitable
if (!matches) return false;
// get current match state, takes "not" into account
var matched = monitor.invert ? !monitor.matches : monitor.matches;
// mark monitor as has been matched in the past
if (matched) monitor.matched = true;
// if retain is enabled with "was" and the monitor has been matched in the past, there's a match
if (monitor.retain && monitor.matched) return true;
// return current match state
return matched;
},
// initial value is always match
true);
};
var monitor = exports.monitor = function monitor(query, element) {
// setup monitor api
var contextMonitor = {
matches: false,
active: false,
onchange: function onchange() {},
start: function start() {
// cannot be activated when already active
if (contextMonitor.active) return;
// now activating
contextMonitor.active = true;
// listen for context changes
monitorSets.forEach(function (monitorSet) {
return monitorSet.forEach(function (monitor) {
return monitor.addListener(onMonitorEvent);
});
});
// get initial state
onMonitorEvent();
},
stop: function stop() {
// disable the monitor
contextMonitor.active = false;
// disable
monitorSets.forEach(function (monitorSet) {
return monitorSet.forEach(function (monitor) {
// stop listening (if possible)
if (!monitor.removeListener) return;
monitor.removeListener(onMonitorEvent);
});
});
},
destroy: function destroy() {
contextMonitor.stop();
monitorSets.length = 0;
}
};
// get different monitor sets (each 'or' creates a separate monitor set) > get monitors for each query
var monitorSets = query.split(' or ').map(function (subQuery) {
return parseQuery(subQuery).map(function (params) {
return decorateMonitor.apply(undefined, [getContextMonitor.apply(undefined, [element].concat(_toConsumableArray(params)))].concat(_toConsumableArray(params.splice(2))));
});
});
// if all monitors return true for .matches getter, we mount the module
var onMonitorEvent = function onMonitorEvent() {
// will keep returning false if one of the monitors does not match, else checks matches property
var matches = monitorSets.reduce(function (matches, monitorSet) {
return (
// if one of the sets is true, it's all fine, no need to match the other sets
matches ? true : matchMonitors(monitorSet)
);
}, false);
// store new state
contextMonitor.matches = matches;
// if matches we mount the module, else we unmount
contextMonitor.onchange(matches);
};
return contextMonitor;
};
// handles contextual loading and unloading
var createContextualModule = function createContextualModule(query, boundModule) {
// setup query monitor
var moduleMonitor = monitor(query, boundModule.element);
moduleMonitor.onchange = function (matches) {
return matches ? boundModule.mount() : boundModule.unmount();
};
// start monitoring
moduleMonitor.start();
// export monitor
return moduleMonitor;
};
// pass in an element and outputs a bound module object, will wrap bound module in a contextual module if required
var createModule = function createModule(element) {
// called when the module is destroyed
var unbindModule = function unbindModule() {
return monitor && monitor.destroy();
};
// bind the module to the element and receive the module wrapper API
var boundModule = bindModule(element, unbindModule);
// get context requirements for this module (if any have been defined)
var query = runPlugin('moduleGetContext', element);
// wait for the right context or load the module immidiately if no context supplied
var monitor = query && createContextualModule(query, boundModule);
// return module
return query ? boundModule : boundModule.mount();
};
// parse a certain section of the DOM and load bound modules
var hydrate = exports.hydrate = function hydrate(context) {
return [].concat(_toConsumableArray(runPlugin('moduleSelector', context))).map(createModule);
};
// all registered plugins
var plugins = [];
// array includes 'polyfill', Array.prototype.includes was the only feature not supported on Edge
var includes = function includes(arr, value) {
return arr.indexOf(value) > -1;
};
// plugins are stored in an array as multiple plugins can subscribe to one hook
var addPlugin = exports.addPlugin = function addPlugin(plugin) {
return plugins.push(plugin);
};
// returns the plugins that match the requested type, as plugins can subscribe to multiple hooks we need to loop over the plugin keys to see if it matches
var getPlugins = function getPlugins(type) {
return plugins.filter(function (plugin) {
return includes(Object.keys(plugin), type);
}).map(function (plugin) {
return plugin[type];
});
};
// run for each of the registered plugins
var eachPlugins = function eachPlugins(type) {
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return getPlugins(type).forEach(function (plugin) {
return plugin.apply(undefined, args);
});
};
// run registered plugins but chain input -> output (sync)
var chainPlugins = function chainPlugins(type) {
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
return getPlugins(type).reduce(function (args, plugin) {
return [plugin.apply(undefined, _toConsumableArray(args))];
}, args).shift();
};
// run on last registered plugin
var runPlugin = function runPlugin(type) {
for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
return getPlugins(type).pop().apply(undefined, args);
};
// default plugin configuration
addPlugin({
// select all elements that have modules assigned to them
moduleSelector: function moduleSelector(context) {
return context.querySelectorAll('[data-module]');
},
// returns the context query as defined on the element
moduleGetContext: function moduleGetContext(element) {
return element.dataset.context;
},
// load the referenced module, by default searches global scope for module name
moduleImport: function moduleImport(name) {
return new Promise(function (resolve, reject) {
if (self[name]) return resolve(self[name]);
// @exclude
reject('Cannot find module with name "' + name + '". By default Conditioner will import modules from the global scope, make sure a function named "' + name + '" is defined on the window object. The scope of a function defined with `let` or `const` is limited to the <script> block in which it is defined.');
// @endexclude
});
},
// returns the module constructor, by default we assume the module returned is a factory function
moduleGetConstructor: function moduleGetConstructor(module) {
return module;
},
// returns the module destrutor, by default we assume the constructor exports a function
moduleGetDestructor: function moduleGetDestructor(moduleExports) {
return moduleExports;
},
// arguments to pass to the module constructor as array
moduleSetConstructorArguments: function moduleSetConstructorArguments(name, element) {
return [element];
},
// where to get name of module
moduleGetName: function moduleGetName(element) {
return element.dataset.module;
},
// default media query monitor
monitor: {
name: 'media',
create: function create(context) {
return self.matchMedia(context);
}
}
});
});