simplyview 1.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +9 -6
  2. package/dist/simply.everything.js +1596 -1139
  3. package/docs/examples.md +82 -0
  4. package/docs/readme.md +33 -0
  5. package/docs/simply.action.md +42 -0
  6. package/docs/simply.activate.md +27 -0
  7. package/docs/simply.api.md +188 -0
  8. package/docs/simply.app.md +27 -0
  9. package/docs/simply.collect.md +64 -0
  10. package/docs/simply.command.md +110 -0
  11. package/docs/simply.include.md +61 -0
  12. package/docs/simply.keyboard.md +60 -0
  13. package/docs/simply.path.md +3 -0
  14. package/docs/simply.route.md +133 -0
  15. package/docs/simply.view.md +53 -0
  16. package/docs/simply.viewmodel.md +3 -0
  17. package/examples/counter.html +1 -0
  18. package/examples/github.html +39 -0
  19. package/examples/githubv4.html +107 -0
  20. package/examples/graphql.html +51 -0
  21. package/examples/graphql.html~ +35 -0
  22. package/examples/keyboard.html +41 -0
  23. package/examples/viewmodel.html +359 -0
  24. package/js/simply.action.js +14 -5
  25. package/js/simply.activate.js +12 -4
  26. package/js/simply.api.js +229 -0
  27. package/js/simply.app.js +27 -9
  28. package/js/simply.collect.js +13 -5
  29. package/js/simply.command.js +36 -6
  30. package/js/simply.include.js +38 -17
  31. package/js/simply.keyboard.js +45 -0
  32. package/js/simply.modules.js +22 -0
  33. package/js/simply.observe.js +13 -4
  34. package/js/simply.path.js +12 -4
  35. package/js/simply.render.js +12 -6
  36. package/js/simply.resize.js +28 -20
  37. package/js/simply.route.js +149 -19
  38. package/js/simply.view.js +16 -9
  39. package/js/simply.viewmodel.js +174 -0
  40. package/make +16 -2
  41. package/make~ +17 -0
  42. package/package.json +6 -3
  43. package/package.json~ +8 -5
  44. package/test/simply.route.test.js +102 -0
  45. package/examples/todo.html~ +0 -50
  46. package/js/simply.app.js~ +0 -44
  47. package/js/simply.bind.js +0 -253
  48. package/js/simply.command.js~ +0 -166
  49. package/js/simply.resize.js~ +0 -69
@@ -1,178 +1,355 @@
1
- this.simply = (function(simply, global) {
2
- var defaultActions = {
3
- 'simply-hide': function(el) {
4
- el.classList.remove('simply-visible');
5
- return Promise.resolve();
6
- },
7
- 'simply-show': function(el) {
8
- el.classList.add('simply-visible');
9
- return Promise.resolve();
10
- },
11
- 'simply-select': function(el,group,target,targetGroup) {
12
- if (group) {
13
- this.call('simply-deselect', this.app.container.querySelectorAll('[data-simply-group='+group+']'));
14
- }
15
- el.classList.add('simply-selected');
16
- if (target) {
17
- this.call('simply-select',target,targetGroup);
18
- }
19
- return Promise.resolve();
20
- },
21
- 'simply-toggle-select': function(el,group,target,targetGroup) {
22
- if (!el.classList.contains('simply-selected')) {
23
- this.call('simply-select',el,group,target,targetGroup);
24
- } else {
25
- this.call('simply-deselect',el,target);
26
- }
27
- return Promise.resolve();
28
- },
29
- 'simply-toggle-class': function(el,className,target) {
30
- if (!target) {
31
- target = el;
32
- }
33
- return Promise.resolve(target.classList.toggle(className));
34
- },
35
- 'simply-deselect': function(el,target) {
36
- if ( typeof el.length=='number' && typeof el.item=='function') {
37
- el = Array.prototype.slice.call(el);
1
+ /**
2
+ * simply.observe
3
+ * This component lets you observe changes in a json compatible data structure
4
+ * It doesn't support linking the same object multiple times
5
+ * It doesn't register deletion of properties using the delete keyword, assign
6
+ * null to the property instead.
7
+ * It doesn't register addition of new properties.
8
+ * It doesn't register directly assigning new entries in an array on a previously
9
+ * non-existant index.
10
+ *
11
+ * usage:
12
+ *
13
+ * (function) simply.observe( (object) model, (string) path, (function) callback)
14
+ *
15
+ * var model = { foo: { bar: 'baz' } };
16
+ * var removeObserver = simply.observe(model, 'foo.bar', function(value, sourcePath) {
17
+ * console.log(sourcePath+': '+value);
18
+ * };
19
+ *
20
+ * The function returns a function that removes the observer when called.
21
+ *
22
+ * The component can observe in place changes in arrays, either by changing
23
+ * an item in a specific index, by calling methods on the array that change
24
+ * the array in place or by reassigning the array with a new value.
25
+ *
26
+ * The sourcePath contains the exact entry that was changed, the value is the
27
+ * value for the path passed to simply.observe.
28
+ * If an array method was called that changes the array in place, the sourcePath
29
+ * also contains that method and its arguments JSON serialized.
30
+ *
31
+ * sourcePath parts are always seperated with '.', even for array indexes.
32
+ * so if foo = [ 'bar' ], the path to 'bar' would be 'foo.0'
33
+ */
34
+
35
+ /*
36
+ FIXME: child properties added after initial observe() call aren't added to the
37
+ childListeners. onMissingChildren can't then find them.
38
+ TODO: onMissingChildren must loop through all fields to get only the direct child
39
+ properties for a given parent, keep seperate index for this?
40
+ */
41
+
42
+ (function (global) {
43
+ 'use strict';
44
+
45
+ var changeListeners = new WeakMap();
46
+ var parentListeners = new WeakMap();
47
+ var childListeners = new WeakMap();
48
+ var changesSignalled = {};
49
+ var observersPaused = 0;
50
+
51
+ function signalChange(model, path, value, sourcePath) {
52
+ if (observersPaused) {
53
+ return;
54
+ }
55
+
56
+ sourcePath = sourcePath ? sourcePath : path;
57
+ changesSignalled = {};
58
+
59
+ var signalRecursion = function(model, path, value, sourcePath) {
60
+ if (changeListeners.has(model) && changeListeners.get(model)[path]) {
61
+ // changeListeners[model][path] contains callback methods
62
+ changeListeners.get(model)[path].forEach(function(callback) {
63
+ changesSignalled[path] = true;
64
+ callback(value, sourcePath);
65
+ });
38
66
  }
39
- if ( Array.isArray(el) ) {
40
- for (var i=0,l=el.length; i<l; i++) {
41
- this.call('simply-deselect',el[i],target);
42
- target = null;
67
+ };
68
+
69
+ //TODO: check if this is correct
70
+ //previous version only triggered parentListeners when no changeListeners were
71
+ //triggered. that created problems with arrays. make an exhaustive unit test.
72
+ signalRecursion(model, path, value, sourcePath);
73
+
74
+ if (parentListeners.has(model) && parentListeners.get(model)[path]) {
75
+ // parentListeners[model][path] contains child paths to signal change on
76
+ // if a parent object is changed, this signals the change to the child objects
77
+ parentListeners.get(model)[path].forEach(function(childPath) {
78
+ if (!changesSignalled[childPath]) {
79
+ var value = getByPath(model, childPath);
80
+ if (value) {
81
+ attach(model, childPath);
82
+ }
83
+ signalRecursion(model, childPath, value, sourcePath);
84
+ changesSignalled[childPath] = true;
43
85
  }
44
- } else {
45
- el.classList.remove('simply-selected');
46
- if (target) {
47
- this.call('simply-deselect',target);
86
+ });
87
+ }
88
+
89
+ if (childListeners.has(model) && childListeners.get(model)[path]) {
90
+ // childListeners[model][path] contains parent paths to signal change on
91
+ // if a child object is changed, this signals the change to the parent objects
92
+ childListeners.get(model)[path].forEach(function(parentPath) {
93
+ if (!changesSignalled[parentPath]) {
94
+ var value = getByPath(model, parentPath);
95
+ signalRecursion(model, parentPath, value, sourcePath);
96
+ changesSignalled[parentPath] = true;
97
+ // check if the parent object still has this child property
98
+ //FIXME: add a setter trigger here to restore observers once the child property get set again
99
+
48
100
  }
101
+ });
102
+ }
103
+
104
+ }
105
+
106
+ function getByPath(model, path) {
107
+ var parts = path.split('.');
108
+ var curr = model;
109
+ do {
110
+ curr = curr[parts.shift()];
111
+ } while (parts.length && curr);
112
+ return curr;
113
+ }
114
+
115
+ function parent(path) {
116
+ var parts = path.split('.');
117
+ parts.pop();
118
+ return parts.join('.');
119
+ }
120
+
121
+ function head(path) {
122
+ return path.split('.').shift();
123
+ }
124
+
125
+ function onParents(model, path, callback) {
126
+ var parent = '';
127
+ var parentOb = model;
128
+ var parents = path.split('.');
129
+ do {
130
+ var head = parents.shift();
131
+ if (parentOb && typeof parentOb[head] != 'undefined') {
132
+ callback(parentOb, head, (parent ? parent + '.' + head : head));
133
+ parentOb = parentOb[head];
49
134
  }
50
- return Promise.resolve();
51
- },
52
- 'simply-fullscreen': function(target) {
53
- var methods = {
54
- 'requestFullscreen':{exit:'exitFullscreen',event:'fullscreenchange',el:'fullscreenElement'},
55
- 'webkitRequestFullScreen':{exit:'webkitCancelFullScreen',event:'webkitfullscreenchange',el:'webkitFullscreenElement'},
56
- 'msRequestFullscreen':{exit:'msExitFullscreen',event:'MSFullscreenChange',el:'msFullscreenElement'},
57
- 'mozRequestFullScreen':{exit:'mozCancelFullScreen',event:'mozfullscreenchange',el:'mozFullScreenElement'}
58
- };
59
- for ( var i in methods ) {
60
- if ( typeof document.documentElement[i] != 'undefined' ) {
61
- var requestMethod = i;
62
- var cancelMethod = methods[i].exit;
63
- var event = methods[i].event;
64
- var element = methods[i].el;
65
- break;
66
- }
135
+ parent = (parent ? parent + '.' + head : head );
136
+ } while (parents.length);
137
+ }
138
+
139
+ function onChildren(model, path, callback) {
140
+ var onChildObjects = function(object, path, callback) {
141
+ if (typeof object != 'object' || object == null) {
142
+ return;
67
143
  }
68
- if ( !requestMethod ) {
144
+ if (Array.isArray(object)) {
69
145
  return;
70
146
  }
71
- if (!target.classList.contains('simply-fullscreen')) {
72
- target.classList.add('simply-fullscreen');
73
- target[requestMethod]();
74
- var exit = function() {
75
- if ( !document[element] ) {
76
- target.classList.remove('simply-fullscreen');
77
- document.removeEventListener(event,exit);
78
- }
79
- };
80
- document.addEventListener(event,exit);
147
+ // register the current keys
148
+ Object.keys(object).forEach(function(key) {
149
+ callback(object, key, path+'.'+key);
150
+ onChildObjects(object[key], path+'.'+key, callback);
151
+ });
152
+ };
153
+ var parent = getByPath(model, path);
154
+ onChildObjects(parent, path, callback);
155
+ }
156
+
157
+ function onMissingChildren(model, path, callback) {
158
+ var allChildren = Object.keys(childListeners.get(model) || []).filter(function(childPath) {
159
+ return childPath.substr(0, path.length)==path && childPath.length>path.length;
160
+ });
161
+ if (!allChildren.length) {
162
+ return;
163
+ }
164
+ var object = getByPath(model, path);
165
+ var keysSeen = {};
166
+ allChildren.forEach(function(childPath) {
167
+ var key = head(childPath.substr(path.length+1));
168
+ if (typeof object[key] == 'undefined') {
169
+ if (!keysSeen[key]) {
170
+ callback(object, key, path+'.'+key);
171
+ keysSeen[key] = true;
172
+ }
81
173
  } else {
82
- target.classList.remove('simply-fullscreen');
83
- document[cancelMethod]();
174
+ onMissingChildren(model, path+'.'+key, callback);
84
175
  }
85
- return Promise.resolve();
176
+ });
177
+ }
178
+
179
+ function addChangeListener(model, path, callback) {
180
+ if (!changeListeners.has(model)) {
181
+ changeListeners.set(model, {});
86
182
  }
87
- };
183
+ if (!changeListeners.get(model)[path]) {
184
+ changeListeners.get(model)[path] = [];
185
+ }
186
+ changeListeners.get(model)[path].push(callback);
88
187
 
89
- simply.action = function(app, inActions) {
90
- var actions = Object.create(defaultActions);
91
- for ( var i in inActions ) {
92
- actions[i] = inActions[i];
188
+ if (!parentListeners.has(model)) {
189
+ parentListeners.set(model, {});
93
190
  }
191
+ var parentPath = parent(path);
192
+ onParents(model, parentPath, function(parentOb, key, currPath) {
193
+ if (!parentListeners.get(model)[currPath]) {
194
+ parentListeners.get(model)[currPath] = [];
195
+ }
196
+ parentListeners.get(model)[currPath].push(path);
197
+ });
94
198
 
95
- actions.app = app;
96
- actions.call = function(name) {
97
- var params = Array.prototype.slice.call(arguments);
98
- params.shift();
99
- return this[name].apply(this, params);
100
- };
101
- return actions;
102
- };
199
+ if (!childListeners.has(model)) {
200
+ childListeners.set(model, {});
201
+ }
202
+ onChildren(model, path, function(childOb, key, currPath) {
203
+ if (!childListeners.get(model)[currPath]) {
204
+ childListeners.get(model)[currPath] = [];
205
+ }
206
+ childListeners.get(model)[currPath].push(path);
207
+ });
208
+ }
103
209
 
104
- return simply;
105
-
106
- })(this.simply || {}, this);
107
- this.simply = (function(simply, global) {
210
+ function removeChangeListener(model, path, callback) {
211
+ if (!changeListeners.has(model)) {
212
+ return;
213
+ }
214
+ if (changeListeners.get(model)[path]) {
215
+ changeListeners.get(model)[path] = changeListeners.get(model)[path].filter(function(f) {
216
+ return f != callback;
217
+ });
218
+ }
219
+ }
108
220
 
109
- var knownCollections = {};
110
-
111
- simply.collect = {
112
- addListener: function(name, callback) {
113
- if (!knownCollections[name]) {
114
- knownCollections[name] = [];
115
- }
116
- if (knownCollections[name].indexOf(callback) == -1) {
117
- knownCollections[name].push(callback);
118
- }
119
- },
120
- removeListener: function(name, callback) {
121
- if (knownCollections[name]) {
122
- var index = knownCollections[name].indexOf(callback);
123
- if (index>=0) {
124
- knownCollections[name].splice(index, 1);
221
+ function pauseObservers() {
222
+ observersPaused++;
223
+ }
224
+
225
+ function resumeObservers() {
226
+ observersPaused--;
227
+ }
228
+
229
+ function attach(model, path, options) {
230
+
231
+ var attachArray = function(object, path) {
232
+ var desc = Object.getOwnPropertyDescriptor(object, 'push');
233
+ if (!desc || desc.configurable) {
234
+ for (var f of ['push','pop','reverse','shift','sort','splice','unshift','copyWithin']) {
235
+ (function(f) {
236
+ try {
237
+ Object.defineProperty(object, f, {
238
+ value: function() {
239
+ pauseObservers();
240
+ var result = Array.prototype[f].apply(this, arguments);
241
+ attach(model, path);
242
+ var args = [].slice.call(arguments).map(function(arg) {
243
+ return JSON.stringify(arg);
244
+ });
245
+ resumeObservers();
246
+ signalChange(model, path, this, path+'.'+f+'('+args.join(',')+')');
247
+ return result;
248
+ },
249
+ readable: false,
250
+ enumerable: false,
251
+ configurable: false
252
+ });
253
+ } catch(e) {
254
+ console.error('simply.observer: Error: Couldn\'t redefine array method '+f+' on '+path, e);
255
+ }
256
+ }(f));
257
+ }
258
+ for (var i=0, l=object.length; i<l; i++) {
259
+ //FIXME: options becomes undefined here somewhere
260
+ // if (options.skipArray) {
261
+ addSetter(object, i, path+'.'+i);
262
+ // } else {
263
+ // attach(model, path+'.'+i, options);
264
+ // }
125
265
  }
126
266
  }
127
- },
128
- update: function(element, value) {
129
- element.value = value;
130
- element.dispatchEvent(new Event('change', {
131
- bubbles: true,
132
- cancelable: true
133
- }));
134
- }
135
- };
267
+ };
268
+
269
+ var addSetTrigger = function(object, key, currPath) {
270
+ Object.defineProperty(object, key, {
271
+ set: function(value) {
272
+ addSetter(object, key, currPath);
273
+ object[key] = value;
274
+ },
275
+ configurable: true,
276
+ readable: false,
277
+ enumerable: false
278
+ });
279
+ };
280
+
281
+ var addSetter = function(object, key, currPath) {
282
+ if (Object.getOwnPropertyDescriptor(object, key).configurable) {
283
+ // assume object keys are only unconfigurable if the
284
+ // following code has already been run on this property
285
+ var _value = object[key];
286
+ Object.defineProperty(object, key, {
287
+ set: function(value) {
288
+ _value = value;
289
+ signalChange(model, currPath, value);
290
+ if (value!=null) {
291
+ onChildren(model, currPath, addSetter);
292
+ onMissingChildren(model, currPath, addSetTrigger);
293
+ }
294
+ },
295
+ get: function() {
296
+ return _value;
297
+ },
298
+ configurable: false,
299
+ readable: true,
300
+ enumerable: true
301
+ });
302
+ }
303
+ if (Array.isArray(object[key])) {
304
+ attachArray(object[key], currPath, options);
305
+ }
306
+ };
136
307
 
137
- function findCollection(el) {
138
- while (el && !el.dataset.simplyCollection) {
139
- el = el.parentElement;
140
- }
141
- return el;
308
+ onParents(model, path, addSetter);
309
+ onChildren(model, path, addSetter);
142
310
  }
143
-
144
- document.addEventListener('change', function(evt) {
145
- var root = null;
146
- var name = '';
147
- if (evt.target.dataset.simplyElement) {
148
- root = findCollection(evt.target);
149
- if (root && root.dataset) {
150
- name = root.dataset.simplyCollection;
151
- }
152
- }
153
- if (name && knownCollections[name]) {
154
- var inputs = root.querySelectorAll('[data-simply-element]');
155
- var elements = [].reduce.call(inputs, function(elements, input) {
156
- elements[input.dataset.simplyElement] = input;
157
- return elements;
158
- }, {});
159
- for (var i=knownCollections[name].length-1; i>=0; i--) {
160
- var result = knownCollections[name][i].call(evt.target.form, elements);
161
- if (result === false) {
162
- break;
163
- }
164
- }
165
- }
166
- }, true);
167
311
 
168
- return simply;
312
+ // FIXME: if you remove a key by reassigning the parent object
313
+ // and then assign that missing key a new value
314
+ // the observer doesn't get triggered
315
+ // var model = { foo: { bar: 'baz' } };
316
+ // simply.observer(model, 'foo.bar', ...)
317
+ // model.foo = { }
318
+ // model.foo.bar = 'zab'; // this should trigger the observer but doesn't
169
319
 
170
- })(this.simply || {}, this);
171
- this.simply = (function(simply, global) {
320
+ var observe = function(model, path, callback, options) {
321
+ if (!path) {
322
+ var keys = Object.keys(model);
323
+ keys.forEach(function(key) {
324
+ attach(model, key, options);
325
+ addChangeListener(model, key, callback);
326
+ });
327
+ return function() {
328
+ keys.forEach(function(key) {
329
+ removeChangeListener(model, key, callback);
330
+ });
331
+ };
332
+ } else {
333
+ attach(model, path, options);
334
+ addChangeListener(model, path, callback);
335
+ return function() {
336
+ removeChangeListener(model, path, callback);
337
+ };
338
+ }
339
+ };
172
340
 
173
- // var templates = new WeakMap();
341
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
342
+ module.exports = observe;
343
+ } else {
344
+ if (!global.simply) {
345
+ global.simply = {};
346
+ }
347
+ global.simply.observe = observe;
348
+ }
349
+ })(this);(function(global) {
350
+ 'use strict';
174
351
 
175
- simply.render = function(options) {
352
+ var render = function(options) {
176
353
  if (!options) {
177
354
  options = {};
178
355
  }
@@ -282,540 +459,451 @@ this.simply = (function(simply, global) {
282
459
  // },
283
460
  }, options.fieldTypes);
284
461
 
285
- return options;
286
- };
287
-
288
- return simply;
289
- })(this.simply || {}, this);
290
- this.simply = (function (simply, global) {
291
-
292
- var throttle = function( callbackFunction, intervalTime ) {
293
- var eventId = 0;
294
- return function() {
295
- var myArguments = arguments;
296
- var me = this;
297
- if ( eventId ) {
298
- return;
299
- } else {
300
- eventId = global.setTimeout( function() {
301
- callbackFunction.apply(me, myArguments);
302
- eventId = 0;
303
- }, intervalTime );
304
- }
305
- };
306
- };
307
-
308
- var runWhenIdle = (function() {
309
- if (global.requestIdleCallback) {
310
- return function(callback) {
311
- global.requestIdleCallback(callback, {timeout: 500});
312
- };
313
- }
314
- return global.requestAnimationFrame;
315
- })();
316
-
317
- var rebaseHref = function(relative, base) {
318
- if (/^[a-z-]*:?\//.test(relative)) {
319
- return relative; // absolute href, no need to rebase
320
- }
321
-
322
- var stack = base.split('/'),
323
- parts = relative.split('/');
324
- stack.pop(); // remove current file name (or empty string)
325
- for (var i=0; i<parts.length; i++) {
326
- if (parts[i] == '.')
327
- continue;
328
- if (parts[i] == '..')
329
- stack.pop();
330
- else
331
- stack.push(parts[i]);
332
- }
333
- return stack.join('/');
334
- };
335
-
336
- var observer, loaded = {};
337
- var head = document.documentElement.querySelector('head');
338
- var currentScript = document.currentScript;
339
-
340
- var waitForPreviousScripts = function() {
341
- // because of the async=false attribute, this script will run after
342
- // the previous scripts have been loaded and run
343
- // simply.include.next.js only fires the simply-next-script event
344
- // that triggers the Promise.resolve method
345
- return new Promise(function(resolve) {
346
- var next = document.createElement('script');
347
- next.src = rebaseHref('simply.include.next.js', currentScript.src);
348
- next.async = false;
349
- document.addEventListener('simply-include-next', function() {
350
- head.removeChild(next);
351
- resolve();
352
- }, { once: true, passive: true});
353
- head.appendChild(next);
354
- });
355
- };
356
-
357
- var scriptLocations = [];
358
-
359
- simply.include = {
360
- scripts: function(scripts, base) {
361
- var arr = [];
362
- for(var i = scripts.length; i--; arr.unshift(scripts[i]));
363
- var importScript = function() {
364
- var script = arr.shift();
365
- if (!script) {
366
- return;
367
- }
368
- var attrs = [].map.call(script.attributes, function(attr) {
369
- return attr.name;
370
- });
371
- var clone = document.createElement('script');
372
- attrs.forEach(function(attr) {
373
- clone.setAttribute(attr, script[attr]);
374
- });
375
- clone.removeAttribute('data-simply-location');
376
- if (!clone.src) {
377
- // this is an inline script, so copy the content and wait for previous scripts to run
378
- clone.innerHTML = script.innerHTML;
379
- waitForPreviousScripts()
380
- .then(function() {
381
- var node = scriptLocations[script.dataset.simplyLocation];
382
- node.parentNode.insertBefore(clone, node);
383
- node.parentNode.removeChild(node);
384
- importScript();
385
- });
386
- } else {
387
- clone.src = rebaseHref(clone.src, base);
388
- if (!clone.hasAttribute('async') && !clone.hasAttribute('defer')) {
389
- clone.async = false; //important! do not use clone.setAttribute('async', false) - it has no effect
390
- }
391
- var node = scriptLocations[script.dataset.simplyLocation];
392
- node.parentNode.insertBefore(clone, node);
393
- node.parentNode.removeChild(node);
394
- loaded[clone.src]=true;
395
- importScript();
396
- }
397
- };
398
- if (arr.length) {
399
- importScript();
400
- }
401
- },
402
- html: function(html, link) {
403
- var fragment = document.createRange().createContextualFragment(html);
404
- var stylesheets = fragment.querySelectorAll('link[rel="stylesheet"],style');
405
- // add all stylesheets to head
406
- [].forEach.call(stylesheets, function(stylesheet) {
407
- if (stylesheet.href) {
408
- stylesheet.href = rebaseHref(stylesheet.href, link.href);
409
- }
410
- head.appendChild(stylesheet);
411
- });
412
- // remove the scripts from the fragment, as they will not run in the
413
- // order in which they are defined
414
- var scriptsFragment = document.createDocumentFragment();
415
- // FIXME: this loses the original position of the script
416
- // should add a placeholder so we can reinsert the clone
417
- var scripts = fragment.querySelectorAll('script');
418
- [].forEach.call(scripts, function(script) {
419
- var placeholder = document.createComment(script.src || 'inline script');
420
- script.parentNode.insertBefore(placeholder, script);
421
- script.dataset.simplyLocation = scriptLocations.length;
422
- scriptLocations.push(placeholder);
423
- scriptsFragment.appendChild(script);
424
- });
425
- // add the remainder before the include link
426
- link.parentNode.insertBefore(fragment, link ? link : null);
427
- global.setTimeout(function() {
428
- if (global.editor && global.editor.data && fragment.querySelector('[data-simply-field],[data-simply-list]')) {
429
- //TODO: remove this dependency and let simply.bind listen for dom node insertions (and simply-edit.js use simply.bind)
430
- global.editor.data.apply(editor.currentData, document);
431
- }
432
- simply.include.scripts(scriptsFragment.childNodes, link ? link.href : global.location.href );
433
- }, 10);
434
- }
435
- };
436
-
437
- var included = {};
438
- var includeLinks = function(links) {
439
- // mark them as in progress, so handleChanges doesn't find them again
440
- var remainingLinks = [].reduce.call(links, function(remainder, link) {
441
- if (link.rel=='simply-include-once' && included[link.href]) {
442
- link.parentNode.removeChild(link);
443
- } else {
444
- included[link.href]=true;
445
- link.rel = 'simply-include-loading';
446
- remainder.push(link);
447
- }
448
- return remainder;
449
- }, []);
450
- [].forEach.call(remainingLinks, function(link) {
451
- if (!link.href) {
452
- return;
453
- }
454
- // fetch the html
455
- fetch(link.href)
456
- .then(function(response) {
457
- if (response.ok) {
458
- console.log('simply-include: loaded '+link.href);
459
- return response.text();
460
- } else {
461
- console.log('simply-include: failed to load '+link.href);
462
- }
463
- })
464
- .then(function(html) {
465
- // if succesfull import the html
466
- simply.include.html(html, link);
467
- // remove the include link
468
- link.parentNode.removeChild(link);
469
- });
470
- });
471
- };
472
-
473
- var handleChanges = throttle(function() {
474
- runWhenIdle(function() {
475
- var links = document.querySelectorAll('link[rel="simply-include"],link[rel="simply-include-once"]');
476
- if (links.length) {
477
- includeLinks(links);
478
- }
479
- });
480
- });
462
+ return options;
463
+ };
481
464
 
482
- var observe = function() {
483
- observer = new MutationObserver(handleChanges);
484
- observer.observe(document, {
485
- subtree: true,
486
- childList: true,
487
- });
465
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
466
+ module.exports = render;
467
+ } else {
468
+ if (!global.simply) {
469
+ global.simply = {};
470
+ }
471
+ global.simply.render = render;
472
+ }
473
+ })(this);
474
+ (function(global) {
475
+ 'us strict';
476
+
477
+ var path = {
478
+ get: function(model, path) {
479
+ if (!path) {
480
+ return model;
481
+ }
482
+ return path.split('.').reduce(function(acc, name) {
483
+ return (acc && acc[name] ? acc[name] : null);
484
+ }, model);
485
+ },
486
+ set: function(model, path, value) {
487
+ var lastName = simply.path.pop(path);
488
+ var parentPath = simply.path.parent(path);
489
+ var parentOb = simply.path.get(model, parentPath);
490
+ parentOb[lastName] = value;
491
+ },
492
+ pop: function(path) {
493
+ return path.split('.').pop();
494
+ },
495
+ push: function(path, name) {
496
+ return (path ? path + '.' : '') + name;
497
+ },
498
+ parent: function(path) {
499
+ var p = path.split('.');
500
+ p.pop();
501
+ return p.join('.');
502
+ },
503
+ parents: function(path) {
504
+ var result = [];
505
+ path.split('.').reduce(function(acc, name) {
506
+ acc.push( (acc.length ? acc[acc.length-1] + '.' : '') + name );
507
+ return acc;
508
+ },result);
509
+ return result;
510
+ }
488
511
  };
489
512
 
490
- observe();
513
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
514
+ module.exports = path;
515
+ } else {
516
+ if (!global.simply) {
517
+ global.simply = {};
518
+ }
519
+ global.simply.path = path;
520
+ }
521
+ })(this);
522
+ (function(global) {
523
+ 'use strict';
491
524
 
492
- return simply;
525
+ var routeInfo = [];
526
+ var listeners = {
527
+ goto: {},
528
+ match: {},
529
+ call: {},
530
+ finish: {}
531
+ };
493
532
 
494
- })(this.simply || {}, this);
495
- this.simply = (function(simply, global) {
533
+ function getRegexpFromRoute(route) {
534
+ return new RegExp('^'+route.replace(/:\w+/g, '([^/]+)').replace(/:\*/, '(.*)'));
535
+ }
496
536
 
497
- var listeners = {};
537
+ function parseRoutes(routes) {
538
+ var paths = Object.keys(routes);
539
+ var matchParams = /:(\w+|\*)/g;
540
+ var matches, params, path;
541
+ for (var i=0; i<paths.length; i++) {
542
+ path = paths[i];
543
+ matches = [];
544
+ params = [];
545
+ do {
546
+ matches = matchParams.exec(path);
547
+ if (matches) {
548
+ params.push(matches[1]);
549
+ }
550
+ } while(matches);
551
+ routeInfo.push({
552
+ match: getRegexpFromRoute(path),
553
+ params: params,
554
+ action: routes[path]
555
+ });
556
+ }
557
+ }
498
558
 
499
- simply.activate = {
500
- addListener: function(name, callback) {
501
- if (!listeners[name]) {
502
- listeners[name] = [];
559
+ var linkHandler = function(evt) {
560
+ if (evt.ctrlKey) {
561
+ return;
562
+ }
563
+ if (evt.which != 1) {
564
+ return; // not a 'left' mouse click
565
+ }
566
+ var link = evt.target;
567
+ while (link && link.tagName!='A') {
568
+ link = link.parentElement;
569
+ }
570
+ if (link
571
+ && link.pathname
572
+ && link.hostname==global.location.hostname
573
+ && !link.link
574
+ && !link.dataset.simplyCommand
575
+ ) {
576
+ let path = getPath(link.pathname+link.hash);
577
+ if ( !route.has(path) ) {
578
+ path = getPath(link.pathname);
503
579
  }
504
- listeners[name].push(callback);
505
- initialCall(name);
506
- },
507
- removeListener: function(name, callback) {
508
- if (!listeners[name]) {
580
+ if ( route.has(path) ) {
581
+ let params = runListeners('goto', { path: path});
582
+ if (params.path) {
583
+ route.goto(params.path);
584
+ }
585
+ evt.preventDefault();
509
586
  return false;
510
587
  }
511
- listeners[name] = listeners[name].filter(function(listener) {
512
- return listener!=callback;
513
- });
514
588
  }
515
589
  };
516
590
 
517
- var initialCall = function(name) {
518
- var nodes = document.querySelectorAll('[data-simply-activate="'+name+'"]');
519
- if (nodes) {
520
- [].forEach.call(nodes, function(node) {
521
- callListeners(node);
522
- });
523
- }
591
+ var options = {
592
+ root: '/'
524
593
  };
525
594
 
526
- var callListeners = function(node) {
527
- if (node && node.dataset.simplyActivate
528
- && listeners[node.dataset.simplyActivate]
595
+ var getPath = function(path) {
596
+ if (path.substring(0,options.root.length)==options.root
597
+ ||
598
+ ( options.root[options.root.length-1]=='/'
599
+ && path.length==(options.root.length-1)
600
+ && path == options.root.substring(0,path.length)
601
+ )
529
602
  ) {
530
- listeners[node.dataset.simplyActivate].forEach(function(callback) {
531
- callback.call(node);
532
- });
603
+ path = path.substring(options.root.length);
604
+ }
605
+ if (path[0]!='/' && path[0]!='#') {
606
+ path = '/'+path;
533
607
  }
608
+ return path;
534
609
  };
535
610
 
536
- var handleChanges = function(changes) {
537
- var activateNodes = [];
538
- for (var change of changes) {
539
- if (change.type=='childList') {
540
- [].forEach.call(change.addedNodes, function(node) {
541
- if (node.querySelectorAll) {
542
- var toActivate = [].slice.call(node.querySelectorAll('[data-simply-activate]'));
543
- if (node.matches('[data-simply-activate]')) {
544
- toActivate.push(node);
545
- }
546
- activateNodes = activateNodes.concat(toActivate);
547
- }
548
- });
549
- }
550
- }
551
- if (activateNodes.length) {
552
- activateNodes.forEach(function(node) {
553
- callListeners(node);
554
- });
611
+ var getUrl = function(path) {
612
+ path = getPath(path);
613
+ if (options.root[options.root.length-1]==='/' && path[0]==='/') {
614
+ path = path.substring(1);
555
615
  }
616
+ return options.root + path;
556
617
  };
557
618
 
558
- var observer = new MutationObserver(handleChanges);
559
- observer.observe(document, {
560
- subtree: true,
561
- childList: true
562
- });
563
-
564
- return simply;
565
- })(this.simply || {}, this);
566
- this.simply = (function(simply, global) {
567
- if (!simply.observe) {
568
- console.error('Error: simply.bind requires simply.observe');
569
- return simply;
619
+ function runListeners(action, params) {
620
+ if (!Object.keys(listeners[action])) {
621
+ return;
622
+ }
623
+ Object.keys(listeners[action]).forEach(function(route) {
624
+ var routeRe = getRegexpFromRoute(route);
625
+ if (routeRe.exec(params.path)) {
626
+ var result;
627
+ listeners[action][route].forEach(function(callback) {
628
+ result = callback.call(global, params);
629
+ if (result) {
630
+ params = result;
631
+ }
632
+ });
633
+ }
634
+ });
635
+ return params;
570
636
  }
571
637
 
572
- function getByPath(model, path) {
573
- var parts = path.split('.');
574
- var curr = model;
575
- do {
576
- curr = curr[parts.shift()];
577
- } while (parts.length && curr);
578
- return curr;
579
- }
638
+ var route = {
639
+ handleEvents: function() {
640
+ global.addEventListener('popstate', function() {
641
+ if (route.match(getPath(document.location.pathname + document.location.hash)) === false) {
642
+ route.match(getPath(document.location.pathname));
643
+ }
644
+ });
645
+ global.document.addEventListener('click', linkHandler);
646
+ },
647
+ load: function(routes) {
648
+ parseRoutes(routes);
649
+ },
650
+ clear: function() {
651
+ routeInfo = [];
652
+ listeners = {
653
+ match: {},
654
+ call: {},
655
+ finish: {}
656
+ };
657
+ },
658
+ match: function(path, options) {
659
+ var args = {
660
+ path: path,
661
+ options: options
662
+ };
663
+ args = runListeners('match',args);
664
+ path = args.path ? args.path : path;
580
665
 
581
- function setByPath(model, path, value) {
582
- var parts = path.split('.');
583
- var curr = model;
584
- while (parts.length>1 && curr) {
585
- var key = parts.shift();
586
- if (typeof curr[key] == 'undefined' || curr[key]==null) {
587
- curr[key] = {};
666
+ var matches;
667
+ if (!path) {
668
+ if (route.match(document.location.pathname+document.location.hash)) {
669
+ return true;
670
+ } else {
671
+ return route.match(document.location.pathname);
672
+ }
588
673
  }
589
- curr = curr[key];
590
- }
591
- curr[parts.shift()] = value;
592
- }
593
-
594
- function setValue(el, value, binding) {
595
- if (el!=focusedElement) {
596
- var fieldType = getFieldType(binding.fieldTypes, el);
597
- if (fieldType) {
598
- fieldType.set.call(el, (typeof value != 'undefined' ? value : ''), binding);
599
- el.dispatchEvent(new Event('simply.bind.resolved', {
600
- bubbles: true,
601
- cancelable: false
602
- }));
674
+ path = getPath(path);
675
+ for ( var i=0; i<routeInfo.length; i++) {
676
+ if (path && path[path.length-1]!='/') {
677
+ matches = routeInfo[i].match.exec(path+'/');
678
+ if (matches) {
679
+ path+='/';
680
+ history.replaceState({}, '', getUrl(path));
681
+ }
682
+ }
683
+ matches = routeInfo[i].match.exec(path);
684
+ if (matches && matches.length) {
685
+ var params = {};
686
+ routeInfo[i].params.forEach(function(key, i) {
687
+ if (key=='*') {
688
+ key = 'remainder';
689
+ }
690
+ params[key] = matches[i+1];
691
+ });
692
+ Object.assign(params, options);
693
+ args.route = route;
694
+ args.params = params;
695
+ args = runListeners('call', args);
696
+ params = args.params ? args.params : params;
697
+ args.result = routeInfo[i].action.call(route, params);
698
+ runListeners('finish', args);
699
+ return args.result;
700
+ }
603
701
  }
604
- }
605
- }
606
-
607
- function getValue(el, binding) {
608
- var setters = Object.keys(binding.fieldTypes);
609
- for(var i=setters.length-1;i>=0;i--) {
610
- if (el.matches(setters[i])) {
611
- return binding.fieldTypes[setters[i]].get.call(el);
702
+ return false;
703
+ },
704
+ goto: function(path) {
705
+ history.pushState({},'',getUrl(path));
706
+ return route.match(path);
707
+ },
708
+ has: function(path) {
709
+ path = getPath(path);
710
+ for ( var i=0; i<routeInfo.length; i++) {
711
+ var matches = routeInfo[i].match.exec(path);
712
+ if (matches && matches.length) {
713
+ return true;
714
+ }
612
715
  }
613
- }
614
- }
615
-
616
- function getFieldType(fieldTypes, el) {
617
- var setters = Object.keys(fieldTypes);
618
- for(var i=setters.length-1;i>=0;i--) {
619
- if (el.matches(setters[i])) {
620
- return fieldTypes[setters[i]];
716
+ return false;
717
+ },
718
+ addListener: function(action, route, callback) {
719
+ if (['goto','match','call','finish'].indexOf(action)==-1) {
720
+ throw new Error('Unknown action '+action);
621
721
  }
622
- }
623
- return null;
624
- }
625
-
626
- function getPath(el, attribute) {
627
- var attributes = attribute.split(',');
628
- for (var attr of attributes) {
629
- if (el.hasAttribute(attr)) {
630
- return el.getAttribute(attr);
722
+ if (!listeners[action][route]) {
723
+ listeners[action][route] = [];
631
724
  }
632
- }
633
- return null;
634
- }
635
-
636
- function throttle( callbackFunction, intervalTime ) {
637
- var eventId = 0;
638
- return function() {
639
- var myArguments = arguments;
640
- var me = this;
641
- if ( eventId ) {
725
+ listeners[action][route].push(callback);
726
+ },
727
+ removeListener: function(action, route, callback) {
728
+ if (['match','call','finish'].indexOf(action)==-1) {
729
+ throw new Error('Unknown action '+action);
730
+ }
731
+ if (!listeners[action][route]) {
642
732
  return;
643
- } else {
644
- eventId = global.setTimeout( function() {
645
- callbackFunction.apply(me, myArguments);
646
- eventId = 0;
647
- }, intervalTime );
648
733
  }
649
- };
650
- }
651
-
652
- var runWhenIdle = (function() {
653
- if (global.requestIdleCallback) {
654
- return function(callback) {
655
- global.requestIdleCallback(callback, {timeout: 500});
656
- };
734
+ listeners[action][route] = listeners[action][route].filter(function(listener) {
735
+ return listener != callback;
736
+ });
737
+ },
738
+ init: function(params) {
739
+ if (params.root) {
740
+ options.root = params.root;
741
+ }
657
742
  }
658
- return global.requestAnimationFrame;
659
- })();
743
+ };
660
744
 
661
- function Binding(config, force) {
662
- this.config = config;
663
- if (!this.config) {
664
- this.config = {};
665
- }
666
- if (!this.config.model) {
667
- this.config.model = {};
668
- }
669
- if (!this.config.attribute) {
670
- this.config.attribute = 'data-simply-bind';
671
- }
672
- if (!this.config.selector) {
673
- this.config.selector = '[data-simply-bind]';
674
- }
675
- if (!this.config.container) {
676
- this.config.container = document;
677
- }
678
- if (typeof this.config.twoway == 'undefined') {
679
- this.config.twoway = true;
745
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
746
+ module.exports = route;
747
+ } else {
748
+ if (!global.simply) {
749
+ global.simply = {};
680
750
  }
681
- this.fieldTypes = {
682
- '*': {
683
- set: function(value) {
684
- this.innerHTML = value;
685
- },
686
- get: function() {
687
- return this.innerHTML;
688
- }
751
+ global.simply.route = route;
752
+ }
753
+ })(this);
754
+ (function(global) {
755
+ 'use strict';
756
+
757
+ var listeners = {};
758
+
759
+ var activate = {
760
+ addListener: function(name, callback) {
761
+ if (!listeners[name]) {
762
+ listeners[name] = [];
763
+ }
764
+ listeners[name].push(callback);
765
+ initialCall(name);
766
+ },
767
+ removeListener: function(name, callback) {
768
+ if (!listeners[name]) {
769
+ return false;
689
770
  }
690
- };
691
- if (this.config.fieldTypes) {
692
- Object.assign(this.fieldTypes, this.config.fieldTypes);
693
- }
694
- this.attach(this.config.container.querySelectorAll(this.config.selector), this.config.model, force);
695
- if (this.config.twoway) {
696
- var self = this;
697
- var observer = new MutationObserver(
698
- throttle(function() {
699
- runWhenIdle(function() {
700
- self.attach(self.config.container.querySelectorAll(self.config.selector), self.config.model);
701
- });
702
- })
703
- );
704
- observer.observe(this.config.container, {
705
- subtree: true,
706
- childList: true
771
+ listeners[name] = listeners[name].filter(function(listener) {
772
+ return listener!=callback;
707
773
  });
708
774
  }
709
- }
710
-
711
- var focusedElement = null;
712
- var initialized = new WeakMap();
713
- var observers = new WeakMap();
714
- var observersPaused = 0;
775
+ };
715
776
 
716
- Binding.prototype.attach = function(el, model, force) {
717
- var illegalNesting = function() {
718
- return (!force && el.parentElement && el.parentElement.closest(self.config.selector));
719
- };
777
+ var initialCall = function(name) {
778
+ var nodes = document.querySelectorAll('[data-simply-activate="'+name+'"]');
779
+ if (nodes) {
780
+ [].forEach.call(nodes, function(node) {
781
+ callListeners(node);
782
+ });
783
+ }
784
+ };
720
785
 
721
- var attachElement = function(jsonPath) {
722
- el.dataset.simplyBound = true;
723
- initElement(el);
724
- setValue(el, getByPath(model, jsonPath), self);
725
- simply.observe(model, jsonPath, function(value) {
726
- if (el != focusedElement) {
727
- setValue(el, value, self);
728
- }
786
+ var callListeners = function(node) {
787
+ if (node && node.dataset.simplyActivate
788
+ && listeners[node.dataset.simplyActivate]
789
+ ) {
790
+ listeners[node.dataset.simplyActivate].forEach(function(callback) {
791
+ callback.call(node);
729
792
  });
730
- };
793
+ }
794
+ };
731
795
 
732
- var addMutationObserver = function(jsonPath) {
733
- if (el.dataset.simplyList) {
734
- return;
735
- }
736
- var update = throttle(function() {
737
- runWhenIdle(function() {
738
- var v = getValue(el, self);
739
- var s = getByPath(model, jsonPath);
740
- if (v != s) {
741
- focusedElement = el;
742
- setByPath(model, jsonPath, v);
743
- focusedElement = null;
796
+ var handleChanges = function(changes) {
797
+ var activateNodes = [];
798
+ for (var change of changes) {
799
+ if (change.type=='childList') {
800
+ [].forEach.call(change.addedNodes, function(node) {
801
+ if (node.querySelectorAll) {
802
+ var toActivate = [].slice.call(node.querySelectorAll('[data-simply-activate]'));
803
+ if (node.matches('[data-simply-activate]')) {
804
+ toActivate.push(node);
805
+ }
806
+ activateNodes = activateNodes.concat(toActivate);
744
807
  }
745
808
  });
746
- }, 250);
747
- var observer = new MutationObserver(function() {
748
- if (observersPaused) {
749
- return;
750
- }
751
- update();
752
- });
753
- observer.observe(el, {
754
- characterData: true,
755
- subtree: true,
756
- childList: true,
757
- attributes: true
758
- });
759
- if (!observers.has(el)) {
760
- observers.set(el, []);
761
809
  }
762
- observers.get(el).push(observer);
763
- return observer;
764
- };
810
+ }
811
+ if (activateNodes.length) {
812
+ activateNodes.forEach(function(node) {
813
+ callListeners(node);
814
+ });
815
+ }
816
+ };
765
817
 
766
- /**
767
- * Runs the init() method of the fieldType, if it is defined.
768
- **/
769
- var initElement = function(el) {
770
- if (initialized.has(el)) {
771
- return;
772
- }
773
- initialized.set(el, true);
774
- var selectors = Object.keys(self.fieldTypes);
775
- for (var i=selectors.length-1; i>=0; i--) {
776
- if (self.fieldTypes[selectors[i]].init && el.matches(selectors[i])) {
777
- self.fieldTypes[selectors[i]].init.call(el, self);
778
- return;
779
- }
780
- }
781
- };
818
+ var observer = new MutationObserver(handleChanges);
819
+ observer.observe(document, {
820
+ subtree: true,
821
+ childList: true
822
+ });
782
823
 
783
- var self = this;
784
- if (el instanceof HTMLElement) {
785
- if (!force && el.dataset.simplyBound) {
786
- return;
824
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
825
+ module.exports = activate;
826
+ } else {
827
+ if (!global.simply) {
828
+ global.simply = {};
829
+ }
830
+ global.simply.activate = activate;
831
+ }
832
+ })(this);
833
+ (function(global) {
834
+ 'use strict';
835
+
836
+ var knownCollections = {};
837
+
838
+ var collect = {
839
+ addListener: function(name, callback) {
840
+ if (!knownCollections[name]) {
841
+ knownCollections[name] = [];
787
842
  }
788
- var jsonPath = getPath(el, this.config.attribute);
789
- if (illegalNesting(el)) {
790
- el.dataset.simplyBound = 'Error: nested binding';
791
- console.error('Error: found nested data-binding element:',el);
792
- return;
843
+ if (knownCollections[name].indexOf(callback) == -1) {
844
+ knownCollections[name].push(callback);
793
845
  }
794
- attachElement(jsonPath);
795
- if (this.config.twoway) {
796
- addMutationObserver(jsonPath);
846
+ },
847
+ removeListener: function(name, callback) {
848
+ if (knownCollections[name]) {
849
+ var index = knownCollections[name].indexOf(callback);
850
+ if (index>=0) {
851
+ knownCollections[name].splice(index, 1);
852
+ }
797
853
  }
798
- } else {
799
- [].forEach.call(el, function(element) {
800
- self.attach(element, model, force);
801
- });
854
+ },
855
+ update: function(element, value) {
856
+ element.value = value;
857
+ element.dispatchEvent(new Event('change', {
858
+ bubbles: true,
859
+ cancelable: true
860
+ }));
802
861
  }
803
862
  };
804
863
 
805
- Binding.prototype.pauseObservers = function() {
806
- observersPaused++;
807
- };
808
-
809
- Binding.prototype.resumeObservers = function() {
810
- observersPaused--;
811
- };
864
+ function findCollection(el) {
865
+ while (el && !el.dataset.simplyCollection) {
866
+ el = el.parentElement;
867
+ }
868
+ return el;
869
+ }
870
+
871
+ global.addEventListener('change', function(evt) {
872
+ var root = null;
873
+ var name = '';
874
+ if (evt.target.dataset.simplyElement) {
875
+ root = findCollection(evt.target);
876
+ if (root && root.dataset) {
877
+ name = root.dataset.simplyCollection;
878
+ }
879
+ }
880
+ if (name && knownCollections[name]) {
881
+ var inputs = root.querySelectorAll('[data-simply-element]');
882
+ var elements = [].reduce.call(inputs, function(elements, input) {
883
+ elements[input.dataset.simplyElement] = input;
884
+ return elements;
885
+ }, {});
886
+ for (var i=knownCollections[name].length-1; i>=0; i--) {
887
+ var result = knownCollections[name][i].call(evt.target.form, elements);
888
+ if (result === false) {
889
+ break;
890
+ }
891
+ }
892
+ }
893
+ }, true);
812
894
 
813
- simply.bind = function(config, force) {
814
- return new Binding(config, force);
815
- };
895
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
896
+ module.exports = collect;
897
+ } else {
898
+ if (!global.simply) {
899
+ global.simply = {};
900
+ }
901
+ global.simply.collect = collect;
902
+ }
816
903
 
817
- return simply;
818
- })(this.simply || {}, this);this.simply = (function(simply, global) {
904
+ })(this);
905
+ (function(global) {
906
+ 'use strict';
819
907
 
820
908
  var defaultCommands = {
821
909
  'simply-hide': function(el, value) {
@@ -830,7 +918,7 @@ this.simply = (function(simply, global) {
830
918
  this.action('simply-show',target);
831
919
  }
832
920
  },
833
- 'simply-select': function(value,el) {
921
+ 'simply-select': function(el, value) {
834
922
  var group = el.dataset.simplyGroup;
835
923
  var target = this.app.get(value);
836
924
  var targetGroup = (target ? target.dataset.simplyGroup : null);
@@ -861,6 +949,16 @@ this.simply = (function(simply, global) {
861
949
  {
862
950
  match: 'input,select,textarea',
863
951
  get: function(el) {
952
+ if (el.tagName==='SELECT' && el.multiple) {
953
+ var values = [], opt;
954
+ for (var i=0,l=el.options.length;i<l;i++) {
955
+ var opt = el.options[i];
956
+ if (opt.selected) {
957
+ values.push(opt.value);
958
+ }
959
+ }
960
+ return values;
961
+ }
864
962
  return el.dataset.simplyValue || el.value;
865
963
  },
866
964
  check: function(el, evt) {
@@ -881,7 +979,19 @@ this.simply = (function(simply, global) {
881
979
  get: function(el) {
882
980
  var data = {};
883
981
  [].forEach.call(el.elements, function(el) {
884
- data[el.name] = el.value;
982
+ if (el.tagName=='INPUT' && (el.type=='checkbox' || el.type=='radio')) {
983
+ if (!el.checked) {
984
+ return;
985
+ }
986
+ }
987
+ if (data[el.name] && !Array.isArray(data[el.name])) {
988
+ data[el.name] = [data[el.name]];
989
+ }
990
+ if (Array.isArray(data[el.name])) {
991
+ data[el.name].push(el.value);
992
+ } else {
993
+ data[el.name] = el.value;
994
+ }
885
995
  });
886
996
  return data;//new FormData(el);
887
997
  },
@@ -927,7 +1037,7 @@ this.simply = (function(simply, global) {
927
1037
  return null;
928
1038
  }
929
1039
 
930
- simply.command = function(app, inCommands) {
1040
+ var command = function(app, inCommands) {
931
1041
 
932
1042
  var commands = Object.create(defaultCommands);
933
1043
  for (var i in inCommands) {
@@ -968,62 +1078,202 @@ this.simply = (function(simply, global) {
968
1078
  return false;
969
1079
  }
970
1080
  }
971
- };
972
-
973
- app.container.addEventListener('click', commandHandler);
974
- app.container.addEventListener('submit', commandHandler);
975
- app.container.addEventListener('change', commandHandler);
976
- app.container.addEventListener('input', commandHandler);
977
-
978
- return commands;
1081
+ };
1082
+
1083
+ app.container.addEventListener('click', commandHandler);
1084
+ app.container.addEventListener('submit', commandHandler);
1085
+ app.container.addEventListener('change', commandHandler);
1086
+ app.container.addEventListener('input', commandHandler);
1087
+
1088
+ return commands;
1089
+ };
1090
+
1091
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1092
+ module.exports = command;
1093
+ } else {
1094
+ if (!global.simply) {
1095
+ global.simply = {};
1096
+ }
1097
+ global.simply.command = command;
1098
+ }
1099
+
1100
+ })(this);
1101
+ (function(global) {
1102
+ 'use strict';
1103
+
1104
+ function keyboard(app, config) {
1105
+ var keys = config;
1106
+
1107
+ if (!app) {
1108
+ app = {};
1109
+ }
1110
+ if (!app.container) {
1111
+ app.container = document.body;
1112
+ }
1113
+ app.container.addEventListener('keydown', (e) => {
1114
+ if (e.isComposing || e.keyCode === 229) {
1115
+ return;
1116
+ }
1117
+ if (e.defaultPrevented) {
1118
+ return;
1119
+ }
1120
+ if (!e.target) {
1121
+ return;
1122
+ }
1123
+
1124
+ let selectedKeyboard = 'default';
1125
+ if (e.target.closest('[data-simply-keyboard]')) {
1126
+ selectedKeyboard = e.target.closest('[data-simply-keyboard]').dataset.simplyKeyboard;
1127
+ }
1128
+ if (keys[selectedKeyboard] && keys[selectedKeyboard][e.code]) {
1129
+ keys[selectedKeyboard][e.code].call(app,e);
1130
+ }
1131
+ });
1132
+
1133
+ return keys;
1134
+ }
1135
+
1136
+
1137
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1138
+ module.exports = keyboard;
1139
+ } else {
1140
+ if (!global.simply) {
1141
+ global.simply = {};
1142
+ }
1143
+ global.simply.keyboard = keyboard;
1144
+ }
1145
+ })(this);
1146
+ (function(global) {
1147
+ 'use strict';
1148
+
1149
+ var defaultActions = {
1150
+ 'simply-hide': function(el) {
1151
+ el.classList.remove('simply-visible');
1152
+ return Promise.resolve();
1153
+ },
1154
+ 'simply-show': function(el) {
1155
+ el.classList.add('simply-visible');
1156
+ return Promise.resolve();
1157
+ },
1158
+ 'simply-select': function(el,group,target,targetGroup) {
1159
+ if (group) {
1160
+ this.call('simply-deselect', this.app.container.querySelectorAll('[data-simply-group='+group+']'));
1161
+ }
1162
+ el.classList.add('simply-selected');
1163
+ if (target) {
1164
+ this.call('simply-select',target,targetGroup);
1165
+ }
1166
+ return Promise.resolve();
1167
+ },
1168
+ 'simply-toggle-select': function(el,group,target,targetGroup) {
1169
+ if (!el.classList.contains('simply-selected')) {
1170
+ this.call('simply-select',el,group,target,targetGroup);
1171
+ } else {
1172
+ this.call('simply-deselect',el,target);
1173
+ }
1174
+ return Promise.resolve();
1175
+ },
1176
+ 'simply-toggle-class': function(el,className,target) {
1177
+ if (!target) {
1178
+ target = el;
1179
+ }
1180
+ return Promise.resolve(target.classList.toggle(className));
1181
+ },
1182
+ 'simply-deselect': function(el,target) {
1183
+ if ( typeof el.length=='number' && typeof el.item=='function') {
1184
+ el = Array.prototype.slice.call(el);
1185
+ }
1186
+ if ( Array.isArray(el) ) {
1187
+ for (var i=0,l=el.length; i<l; i++) {
1188
+ this.call('simply-deselect',el[i],target);
1189
+ target = null;
1190
+ }
1191
+ } else {
1192
+ el.classList.remove('simply-selected');
1193
+ if (target) {
1194
+ this.call('simply-deselect',target);
1195
+ }
1196
+ }
1197
+ return Promise.resolve();
1198
+ },
1199
+ 'simply-fullscreen': function(target) {
1200
+ var methods = {
1201
+ 'requestFullscreen':{exit:'exitFullscreen',event:'fullscreenchange',el:'fullscreenElement'},
1202
+ 'webkitRequestFullScreen':{exit:'webkitCancelFullScreen',event:'webkitfullscreenchange',el:'webkitFullscreenElement'},
1203
+ 'msRequestFullscreen':{exit:'msExitFullscreen',event:'MSFullscreenChange',el:'msFullscreenElement'},
1204
+ 'mozRequestFullScreen':{exit:'mozCancelFullScreen',event:'mozfullscreenchange',el:'mozFullScreenElement'}
1205
+ };
1206
+ for ( var i in methods ) {
1207
+ if ( typeof document.documentElement[i] != 'undefined' ) {
1208
+ var requestMethod = i;
1209
+ var cancelMethod = methods[i].exit;
1210
+ var event = methods[i].event;
1211
+ var element = methods[i].el;
1212
+ break;
1213
+ }
1214
+ }
1215
+ if ( !requestMethod ) {
1216
+ return;
1217
+ }
1218
+ if (!target.classList.contains('simply-fullscreen')) {
1219
+ target.classList.add('simply-fullscreen');
1220
+ target[requestMethod]();
1221
+ var exit = function() {
1222
+ if ( !document[element] ) {
1223
+ target.classList.remove('simply-fullscreen');
1224
+ document.removeEventListener(event,exit);
1225
+ }
1226
+ };
1227
+ document.addEventListener(event,exit);
1228
+ } else {
1229
+ target.classList.remove('simply-fullscreen');
1230
+ document[cancelMethod]();
1231
+ }
1232
+ return Promise.resolve();
1233
+ }
979
1234
  };
980
1235
 
981
- return simply;
982
-
983
- })(this.simply || {}, this);
984
- this.simply = (function(simply, global) {
985
-
986
- simply.view = function(app, view) {
987
-
988
- app.view = view || {};
1236
+ var action = function(app, inActions) {
1237
+ var actions = Object.create(defaultActions);
1238
+ for ( var i in inActions ) {
1239
+ actions[i] = inActions[i];
1240
+ }
989
1241
 
990
- var load = function() {
991
- var data = app.view;
992
- var path = editor.data.getDataPath(app.container);
993
- app.view = editor.currentData[path];
994
- Object.keys(data).forEach(function(key) {
995
- app.view[key] = data[key];
996
- });
1242
+ actions.app = app;
1243
+ actions.call = function(name) {
1244
+ var params = Array.prototype.slice.call(arguments);
1245
+ params.shift();
1246
+ return this[name].apply(this, params);
997
1247
  };
1248
+ return actions;
1249
+ };
998
1250
 
999
- if (global.editor && editor.currentData) {
1000
- load();
1001
- } else {
1002
- document.addEventListener('simply-content-loaded', function() {
1003
- load();
1004
- });
1251
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1252
+ module.exports = action;
1253
+ } else {
1254
+ if (!global.simply) {
1255
+ global.simply = {};
1005
1256
  }
1006
-
1007
- return app.view;
1008
- };
1257
+ global.simply.action = action;
1258
+ }
1009
1259
 
1010
- return simply;
1011
- })(this.simply || {}, this);
1012
- this.simply = (function(simply, global) {
1260
+ })(this);
1261
+ (function(global) {
1262
+ 'use strict';
1013
1263
 
1014
- simply.resize = function(app, config) {
1015
- if (!config) {
1016
- config = {};
1017
- }
1018
- if (!config.sizes) {
1019
- config.sizes = {
1020
- 'simply-tiny' : 0,
1021
- 'simply-xsmall' : 480,
1022
- 'simply-small' : 768,
1023
- 'simply-medium' : 992,
1024
- 'simply-large' : 1200
1025
- };
1026
- }
1264
+ var resize = function(app, config) {
1265
+ if (!config) {
1266
+ config = {};
1267
+ }
1268
+ if (!config.sizes) {
1269
+ config.sizes = {
1270
+ 'simply-tiny' : 0,
1271
+ 'simply-xsmall' : 480,
1272
+ 'simply-small' : 768,
1273
+ 'simply-medium' : 992,
1274
+ 'simply-large' : 1200
1275
+ };
1276
+ }
1027
1277
 
1028
1278
  var lastSize = 0;
1029
1279
  function resizeSniffer() {
@@ -1042,6 +1292,7 @@ this.simply = (function(simply, global) {
1042
1292
  } else {
1043
1293
  if ( !app.container.classList.contains(match) ) {
1044
1294
  app.container.classList.add(match);
1295
+ match = sizes.pop(); // skip to next match to remove these
1045
1296
  }
1046
1297
  break;
1047
1298
  }
@@ -1051,7 +1302,7 @@ this.simply = (function(simply, global) {
1051
1302
  if ( app.container.classList.contains(match)) {
1052
1303
  app.container.classList.remove(match);
1053
1304
  }
1054
- match=sizes.pop();
1305
+ match = sizes.pop();
1055
1306
  }
1056
1307
  var toolbars = app.container.querySelectorAll('.simply-toolbar');
1057
1308
  [].forEach.call(toolbars, function(toolbar) {
@@ -1075,534 +1326,740 @@ this.simply = (function(simply, global) {
1075
1326
  });
1076
1327
  }
1077
1328
 
1078
- return resizeSniffer;
1079
- };
1080
-
1081
- return simply;
1329
+ return resizeSniffer;
1330
+ };
1082
1331
 
1083
- })(this.simply || {}, this);this.simply = (function(simply, global) {
1084
- simply.app = function(options) {
1085
- if (!options) {
1086
- options = {};
1087
- }
1088
- if (!options.container) {
1089
- console.warn('No simply.app application container element specified, using document.body.');
1090
- }
1091
-
1092
- function simplyApp(options) {
1093
- if (!options) {
1094
- options = {};
1095
- }
1096
- if ( options.routes ) {
1097
- simply.route.load(options.routes);
1098
- simply.route.handleEvents();
1099
- global.setTimeout(function() {
1100
- simply.route.match(global.location.pathname);
1101
- });
1102
- }
1103
- this.container = options.container || document.body;
1104
- this.actions = simply.action ? simply.action(this, options.actions) : false;
1105
- this.commands = simply.command ? simply.command(this, options.commands) : false;
1106
- this.resize = simply.resize ? simply.resize(this, options.resize) : false;
1107
- this.view = simply.view ? simply.view(this, options.view) : false;
1108
- if (!(global.editor && global.editor.field) && simply.bind) {
1109
- // skip simplyview databinding if SimplyEdit is loaded
1110
- options.bind = simply.render(options.bind || {});
1111
- options.bind.model = this.view;
1112
- options.bind.container = this.container;
1113
- this.bind = options.bindings = simply.bind(options.bind);
1114
- }
1332
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1333
+ module.exports = resize;
1334
+ } else {
1335
+ if (!global.simply) {
1336
+ global.simply = {};
1115
1337
  }
1338
+ global.simply.resize = resize;
1339
+ }
1340
+ })(this);(function (global) {
1341
+ 'use strict';
1116
1342
 
1117
- simplyApp.prototype.get = function(id) {
1118
- return this.container.querySelector('[data-simply-id='+id+']') || document.getElementById(id);
1343
+ var throttle = function( callbackFunction, intervalTime ) {
1344
+ var eventId = 0;
1345
+ return function() {
1346
+ var myArguments = arguments;
1347
+ var me = this;
1348
+ if ( eventId ) {
1349
+ return;
1350
+ } else {
1351
+ eventId = global.setTimeout( function() {
1352
+ callbackFunction.apply(me, myArguments);
1353
+ eventId = 0;
1354
+ }, intervalTime );
1355
+ }
1119
1356
  };
1120
-
1121
- var app = new simplyApp(options);
1122
-
1123
- return app;
1124
1357
  };
1125
1358
 
1126
- return simply;
1127
- })(this.simply || {}, this);
1128
- this.simply = (function(simply, global) {
1359
+ var runWhenIdle = (function() {
1360
+ if (global.requestIdleCallback) {
1361
+ return function(callback) {
1362
+ global.requestIdleCallback(callback, {timeout: 500});
1363
+ };
1364
+ }
1365
+ return global.requestAnimationFrame;
1366
+ })();
1129
1367
 
1130
- var routeInfo = [];
1368
+ var rebaseHref = function(relative, base) {
1369
+ if (/^[a-z-]*:?\//.test(relative)) {
1370
+ return relative; // absolute href, no need to rebase
1371
+ }
1131
1372
 
1132
- function parseRoutes(routes) {
1133
- var paths = Object.keys(routes);
1134
- var matchParams = /:(\w+|\*)/g;
1135
- var matches, params, path;
1136
- for (var i=0; i<paths.length; i++) {
1137
- path = paths[i];
1138
- matches = [];
1139
- params = [];
1140
- do {
1141
- matches = matchParams.exec(path);
1142
- if (matches) {
1143
- params.push(matches[1]);
1144
- }
1145
- } while(matches);
1146
- routeInfo.push({
1147
- match: new RegExp(path.replace(/:\w+/g, '([^/]+)').replace(/:\*/, '(.*)')),
1148
- params: params,
1149
- action: routes[path]
1150
- });
1373
+ var stack = base.split('/'),
1374
+ parts = relative.split('/');
1375
+ stack.pop(); // remove current file name (or empty string)
1376
+ for (var i=0; i<parts.length; i++) {
1377
+ if (parts[i] == '.')
1378
+ continue;
1379
+ if (parts[i] == '..')
1380
+ stack.pop();
1381
+ else
1382
+ stack.push(parts[i]);
1151
1383
  }
1384
+ return stack.join('/');
1385
+ };
1386
+
1387
+ var observer, loaded = {};
1388
+ var head = global.document.querySelector('head');
1389
+ var currentScript = global.document.currentScript;
1390
+ if (!currentScript) {
1391
+ var getScriptURL = (function() {
1392
+ var scripts = document.getElementsByTagName('script');
1393
+ var index = scripts.length - 1;
1394
+ var myScript = scripts[index];
1395
+ return function() { return myScript.src; };
1396
+ })();
1397
+ var currentScriptURL = getScriptURL();
1398
+ } else {
1399
+ var currentScriptURL = currentScript.src;
1152
1400
  }
1153
1401
 
1154
- var linkHandler = function(evt) {
1155
- if (evt.ctrlKey) {
1156
- return;
1157
- }
1158
- if (evt.which != 1) {
1159
- return; // not a 'left' mouse click
1160
- }
1161
- var link = evt.target;
1162
- while (link && link.tagName!='A') {
1163
- link = link.parentElement;
1164
- }
1165
- if (link
1166
- && link.pathname
1167
- && link.hostname==document.location.hostname
1168
- && !link.link
1169
- && !link.dataset.simplyCommand
1170
- && simply.route.has(link.pathname)
1171
- ) {
1172
- simply.route.goto(link.pathname);
1173
- evt.preventDefault();
1174
- return false;
1175
- }
1402
+ var waitForPreviousScripts = function() {
1403
+ // because of the async=false attribute, this script will run after
1404
+ // the previous scripts have been loaded and run
1405
+ // simply.include.next.js only fires the simply-next-script event
1406
+ // that triggers the Promise.resolve method
1407
+ return new Promise(function(resolve) {
1408
+ var next = global.document.createElement('script');
1409
+ next.src = rebaseHref('simply.include.next.js', currentScriptURL);
1410
+ next.async = false;
1411
+ global.document.addEventListener('simply-include-next', function() {
1412
+ head.removeChild(next);
1413
+ resolve();
1414
+ }, { once: true, passive: true});
1415
+ head.appendChild(next);
1416
+ });
1176
1417
  };
1177
1418
 
1178
- simply.route = {
1179
- handleEvents: function() {
1180
- global.addEventListener('popstate', function() {
1181
- simply.route.match(document.location.pathname);
1182
- });
1183
- document.addEventListener('click', linkHandler);
1184
- },
1185
- load: function(routes) {
1186
- parseRoutes(routes);
1187
- },
1188
- match: function(path, options) {
1189
- var matches;
1190
- for ( var i=0; i<routeInfo.length; i++) {
1191
- if (path[path.length-1]!='/') {
1192
- matches = routeInfo[i].match.exec(path+'/');
1193
- if (matches) {
1194
- path+='/';
1195
- history.replaceState({}, '', path);
1196
- }
1419
+ var scriptLocations = [];
1420
+
1421
+ var include = {
1422
+ scripts: function(scripts, base) {
1423
+ var arr = [];
1424
+ for(var i = scripts.length; i--; arr.unshift(scripts[i]));
1425
+ var importScript = function() {
1426
+ var script = arr.shift();
1427
+ if (!script) {
1428
+ return;
1197
1429
  }
1198
- matches = routeInfo[i].match.exec(path);
1199
- if (matches && matches.length) {
1200
- var params = {};
1201
- routeInfo[i].params.forEach(function(key, i) {
1202
- if (key=='*') {
1203
- key = 'remainder';
1204
- }
1205
- params[key] = matches[i+1];
1206
- });
1207
- Object.assign(params, options);
1208
- return routeInfo[i].action.call(simply.route, params);
1430
+ var attrs = [].map.call(script.attributes, function(attr) {
1431
+ return attr.name;
1432
+ });
1433
+ var clone = global.document.createElement('script');
1434
+ attrs.forEach(function(attr) {
1435
+ clone.setAttribute(attr, script.getAttribute(attr));
1436
+ });
1437
+ clone.removeAttribute('data-simply-location');
1438
+ if (!clone.src) {
1439
+ // this is an inline script, so copy the content and wait for previous scripts to run
1440
+ clone.innerHTML = script.innerHTML;
1441
+ waitForPreviousScripts()
1442
+ .then(function() {
1443
+ var node = scriptLocations[script.dataset.simplyLocation];
1444
+ node.parentNode.insertBefore(clone, node);
1445
+ node.parentNode.removeChild(node);
1446
+ importScript();
1447
+ });
1448
+ } else {
1449
+ clone.src = rebaseHref(clone.src, base);
1450
+ if (!clone.hasAttribute('async') && !clone.hasAttribute('defer')) {
1451
+ clone.async = false; //important! do not use clone.setAttribute('async', false) - it has no effect
1452
+ }
1453
+ var node = scriptLocations[script.dataset.simplyLocation];
1454
+ node.parentNode.insertBefore(clone, node);
1455
+ node.parentNode.removeChild(node);
1456
+ loaded[clone.src]=true;
1457
+ importScript();
1209
1458
  }
1459
+ };
1460
+ if (arr.length) {
1461
+ importScript();
1210
1462
  }
1211
1463
  },
1212
- goto: function(path) {
1213
- history.pushState({},'',path);
1214
- return simply.route.match(path);
1215
- },
1216
- has: function(path) {
1217
- for ( var i=0; i<routeInfo.length; i++) {
1218
- var matches = routeInfo[i].match.exec(path);
1219
- if (matches && matches.length) {
1220
- return true;
1464
+ html: function(html, link) {
1465
+ var fragment = global.document.createRange().createContextualFragment(html);
1466
+ var stylesheets = fragment.querySelectorAll('link[rel="stylesheet"],style');
1467
+ // add all stylesheets to head
1468
+ [].forEach.call(stylesheets, function(stylesheet) {
1469
+ if (stylesheet.href) {
1470
+ stylesheet.href = rebaseHref(stylesheet.href, link.href);
1471
+ }
1472
+ head.appendChild(stylesheet);
1473
+ });
1474
+ // remove the scripts from the fragment, as they will not run in the
1475
+ // order in which they are defined
1476
+ var scriptsFragment = global.document.createDocumentFragment();
1477
+ // FIXME: this loses the original position of the script
1478
+ // should add a placeholder so we can reinsert the clone
1479
+ var scripts = fragment.querySelectorAll('script');
1480
+ [].forEach.call(scripts, function(script) {
1481
+ var placeholder = global.document.createComment(script.src || 'inline script');
1482
+ script.parentNode.insertBefore(placeholder, script);
1483
+ script.dataset.simplyLocation = scriptLocations.length;
1484
+ scriptLocations.push(placeholder);
1485
+ scriptsFragment.appendChild(script);
1486
+ });
1487
+ // add the remainder before the include link
1488
+ link.parentNode.insertBefore(fragment, link ? link : null);
1489
+ global.setTimeout(function() {
1490
+ if (global.editor && global.editor.data && fragment.querySelector('[data-simply-field],[data-simply-list]')) {
1491
+ //TODO: remove this dependency and let simply.bind listen for dom node insertions (and simply-edit.js use simply.bind)
1492
+ global.editor.data.apply(global.editor.currentData, global.document);
1221
1493
  }
1222
- }
1223
- return false;
1494
+ simply.include.scripts(scriptsFragment.childNodes, link ? link.href : global.location.href );
1495
+ }, 10);
1224
1496
  }
1225
1497
  };
1226
1498
 
1227
- return simply;
1499
+ var included = {};
1500
+ var includeLinks = function(links) {
1501
+ // mark them as in progress, so handleChanges doesn't find them again
1502
+ var remainingLinks = [].reduce.call(links, function(remainder, link) {
1503
+ if (link.rel=='simply-include-once' && included[link.href]) {
1504
+ link.parentNode.removeChild(link);
1505
+ } else {
1506
+ included[link.href]=true;
1507
+ link.rel = 'simply-include-loading';
1508
+ remainder.push(link);
1509
+ }
1510
+ return remainder;
1511
+ }, []);
1512
+ [].forEach.call(remainingLinks, function(link) {
1513
+ if (!link.href) {
1514
+ return;
1515
+ }
1516
+ // fetch the html
1517
+ fetch(link.href)
1518
+ .then(function(response) {
1519
+ if (response.ok) {
1520
+ console.log('simply-include: loaded '+link.href);
1521
+ return response.text();
1522
+ } else {
1523
+ console.log('simply-include: failed to load '+link.href);
1524
+ }
1525
+ })
1526
+ .then(function(html) {
1527
+ // if succesfull import the html
1528
+ simply.include.html(html, link);
1529
+ // remove the include link
1530
+ link.parentNode.removeChild(link);
1531
+ });
1532
+ });
1533
+ };
1228
1534
 
1229
- })(this.simply || {}, this);
1230
- /**
1231
- * simply.observe
1232
- * This component lets you observe changes in a json compatible data structure
1233
- * It doesn't support linking the same object multiple times
1234
- * It doesn't register deletion of properties using the delete keyword, assign
1235
- * null to the property instead.
1236
- * It doesn't register addition of new properties.
1237
- * It doesn't register directly assigning new entries in an array on a previously
1238
- * non-existant index.
1239
- *
1240
- * usage:
1241
- *
1242
- * (function) simply.observe( (object) model, (string) path, (function) callback)
1243
- *
1244
- * var model = { foo: { bar: 'baz' } };
1245
- * var removeObserver = simply.observe(model, 'foo.bar', function(value, sourcePath) {
1246
- * console.log(sourcePath+': '+value);
1247
- * };
1248
- *
1249
- * The function returns a function that removes the observer when called.
1250
- *
1251
- * The component can observe in place changes in arrays, either by changing
1252
- * an item in a specific index, by calling methods on the array that change
1253
- * the array in place or by reassigning the array with a new value.
1254
- *
1255
- * The sourcePath contains the exact entry that was changed, the value is the
1256
- * value for the path passed to simply.observe.
1257
- * If an array method was called that changes the array in place, the sourcePath
1258
- * also contains that method and its arguments JSON serialized.
1259
- *
1260
- * sourcePath parts are always seperated with '.', even for array indexes.
1261
- * so if foo = [ 'bar' ], the path to 'bar' would be 'foo.0'
1262
- */
1535
+ var handleChanges = throttle(function() {
1536
+ runWhenIdle(function() {
1537
+ var links = global.document.querySelectorAll('link[rel="simply-include"],link[rel="simply-include-once"]');
1538
+ if (links.length) {
1539
+ includeLinks(links);
1540
+ }
1541
+ });
1542
+ });
1263
1543
 
1264
- /*
1265
- FIXME: child properties added after initial observe() call aren't added to the
1266
- childListeners. onMissingChildren can't then find them.
1267
- TODO: onMissingChildren must loop through all fields to get only the direct child
1268
- properties for a given parent, keep seperate index for this?
1269
- */
1544
+ var observe = function() {
1545
+ observer = new MutationObserver(handleChanges);
1546
+ observer.observe(global.document, {
1547
+ subtree: true,
1548
+ childList: true,
1549
+ });
1550
+ };
1270
1551
 
1271
- this.simply = (function (simply, global) {
1272
- var changeListeners = new WeakMap();
1273
- var parentListeners = new WeakMap();
1274
- var childListeners = new WeakMap();
1275
- var changesSignalled = {};
1276
- var observersPaused = 0;
1552
+ observe();
1553
+ handleChanges(); // check if there are include links in the dom already
1277
1554
 
1278
- function signalChange(model, path, value, sourcePath) {
1279
- if (observersPaused) {
1280
- return;
1555
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1556
+ module.exports = include;
1557
+ } else {
1558
+ if (!global.simply) {
1559
+ global.simply = {};
1281
1560
  }
1561
+ global.simply.include = include;
1562
+ }
1282
1563
 
1283
- sourcePath = sourcePath ? sourcePath : path;
1284
- changesSignalled = {};
1285
1564
 
1286
- var signalRecursion = function(model, path, value, sourcePath) {
1287
- if (changeListeners.has(model) && changeListeners.get(model)[path]) {
1288
- // changeListeners[model][path] contains callback methods
1289
- changeListeners.get(model)[path].forEach(function(callback) {
1290
- changesSignalled[path] = true;
1291
- callback(value, sourcePath);
1292
- });
1293
- }
1294
- };
1565
+ })(this);
1566
+ (function(global) {
1567
+ 'use strict';
1568
+ var view = function(app, view) {
1295
1569
 
1296
- //TODO: check if this is correct
1297
- //previous version only triggered parentListeners when no changeListeners were
1298
- //triggered. that created problems with arrays. make an exhaustive unit test.
1299
- signalRecursion(model, path, value, sourcePath);
1300
-
1301
- if (parentListeners.has(model) && parentListeners.get(model)[path]) {
1302
- // parentListeners[model][path] contains child paths to signal change on
1303
- // if a parent object is changed, this signals the change to the child objects
1304
- parentListeners.get(model)[path].forEach(function(childPath) {
1305
- if (!changesSignalled[childPath]) {
1306
- var value = getByPath(model, childPath);
1307
- if (value) {
1308
- attach(model, childPath);
1309
- }
1310
- signalRecursion(model, childPath, value, sourcePath);
1311
- changesSignalled[childPath] = true;
1312
- }
1313
- });
1314
- }
1570
+ app.view = view || {};
1315
1571
 
1316
- if (childListeners.has(model) && childListeners.get(model)[path]) {
1317
- // childListeners[model][path] contains parent paths to signal change on
1318
- // if a child object is changed, this signals the change to the parent objects
1319
- childListeners.get(model)[path].forEach(function(parentPath) {
1320
- if (!changesSignalled[parentPath]) {
1321
- var value = getByPath(model, parentPath);
1322
- signalRecursion(model, parentPath, value, sourcePath);
1323
- changesSignalled[parentPath] = true;
1324
- // check if the parent object still has this child property
1325
- //FIXME: add a setter trigger here to restore observers once the child property get set again
1572
+ var load = function() {
1573
+ var data = app.view;
1574
+ var path = global.editor.data.getDataPath(app.container);
1575
+ app.view = global.editor.currentData[path];
1576
+ Object.keys(data).forEach(function(key) {
1577
+ app.view[key] = data[key];
1578
+ });
1579
+ };
1326
1580
 
1327
- }
1581
+ if (global.editor && global.editor.currentData) {
1582
+ load();
1583
+ } else {
1584
+ global.document.addEventListener('simply-content-loaded', function() {
1585
+ load();
1328
1586
  });
1329
1587
  }
1588
+
1589
+ return app.view;
1590
+ };
1330
1591
 
1592
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1593
+ module.exports = view;
1594
+ } else {
1595
+ if (!global.simply) {
1596
+ global.simply = {};
1597
+ }
1598
+ global.simply.view = view;
1331
1599
  }
1332
-
1333
- function getByPath(model, path) {
1334
- var parts = path.split('.');
1335
- var curr = model;
1336
- do {
1337
- curr = curr[parts.shift()];
1338
- } while (parts.length && curr);
1339
- return curr;
1340
- }
1341
-
1342
- function parent(path) {
1343
- var parts = path.split('.');
1344
- parts.pop();
1345
- return parts.join('.');
1346
- }
1600
+ })(this);
1601
+ (function(global) {
1602
+ 'use strict';
1347
1603
 
1348
- function head(path) {
1349
- return path.split('.').shift();
1350
- }
1351
-
1352
- function onParents(model, path, callback) {
1353
- var parent = '';
1354
- var parentOb = model;
1355
- var parents = path.split('.');
1356
- do {
1357
- var head = parents.shift();
1358
- if (parentOb && typeof parentOb[head] != 'undefined') {
1359
- callback(parentOb, head, (parent ? parent + '.' + head : head));
1360
- parentOb = parentOb[head];
1361
- }
1362
- parent = (parent ? parent + '.' + head : head );
1363
- } while (parents.length);
1364
- }
1365
-
1366
- function onChildren(model, path, callback) {
1367
- var onChildObjects = function(object, path, callback) {
1368
- if (typeof object != 'object' || object == null) {
1369
- return;
1370
- }
1371
- if (Array.isArray(object)) {
1372
- return;
1373
- }
1374
- // register the current keys
1375
- Object.keys(object).forEach(function(key) {
1376
- callback(object, key, path+'.'+key);
1377
- onChildObjects(object[key], path+'.'+key, callback);
1378
- });
1604
+ function ViewModel(name, data, options) {
1605
+ this.name = name;
1606
+ this.data = data || [];
1607
+ this.view = {
1608
+ options: {},
1609
+ data: [] //Array.from(this.data).slice()
1610
+ };
1611
+ this.options = options || {};
1612
+ this.plugins = {
1613
+ start: [],
1614
+ select: [],
1615
+ order: [],
1616
+ render: [],
1617
+ finish: []
1379
1618
  };
1380
- var parent = getByPath(model, path);
1381
- onChildObjects(parent, path, callback);
1382
1619
  }
1383
1620
 
1384
- function onMissingChildren(model, path, callback) {
1385
- var allChildren = Object.keys(childListeners.get(model) || []).filter(function(childPath) {
1386
- return childPath.substr(0, path.length)==path && childPath.length>path.length;
1387
- });
1388
- if (!allChildren.length) {
1389
- return;
1621
+ ViewModel.prototype.update = function(params) {
1622
+ if (!params) {
1623
+ params = {};
1390
1624
  }
1391
- var object = getByPath(model, path);
1392
- var keysSeen = {};
1393
- allChildren.forEach(function(childPath) {
1394
- var key = head(childPath.substr(path.length+1));
1395
- if (typeof object[key] == 'undefined') {
1396
- if (!keysSeen[key]) {
1397
- callback(object, key, path+'.'+key);
1398
- keysSeen[key] = true;
1399
- }
1400
- } else {
1401
- onMissingChildren(model, path+'.'+key, callback);
1402
- }
1625
+ if (params.data) {
1626
+ // this.data is a reference to the data passed, so that any changes in it will get applied
1627
+ // to the original
1628
+ this.data = params.data;
1629
+ }
1630
+ // the view is a shallow copy of the array, so that changes in sort order and filtering
1631
+ // won't get applied to the original, but databindings on its children will still work
1632
+ this.view.data = Array.from(this.data).slice();
1633
+ var plugins = this.plugins.start.concat(this.plugins.select, this.plugins.order, this.plugins.render, this.plugins.finish);
1634
+ var self = this;
1635
+ plugins.forEach(function(plugin) {
1636
+ plugin.call(self, params);
1403
1637
  });
1404
- }
1405
1638
 
1406
- function addChangeListener(model, path, callback) {
1407
- if (!changeListeners.has(model)) {
1408
- changeListeners.set(model, {});
1639
+ if (global.editor) {
1640
+ global.editor.addDataSource(this.name,{
1641
+ load: function(el, callback) {
1642
+ callback(self.view.data);
1643
+ }
1644
+ });
1645
+ updateDataSource(this.name);
1409
1646
  }
1410
- if (!changeListeners.get(model)[path]) {
1411
- changeListeners.get(model)[path] = [];
1647
+ };
1648
+
1649
+ ViewModel.prototype.addPlugin = function(pipe, plugin) {
1650
+ if (typeof this.plugins[pipe] == 'undefined') {
1651
+ throw new Error('Unknown pipeline '+pipe);
1412
1652
  }
1413
- changeListeners.get(model)[path].push(callback);
1653
+ this.plugins[pipe].push(plugin);
1654
+ };
1655
+
1656
+ ViewModel.prototype.removePlugin = function(pipe, plugin) {
1657
+ if (typeof this.plugins[pipe] == 'undefined') {
1658
+ throw new Error('Unknown pipeline '+pipe);
1659
+ }
1660
+ this.plugins[pipe] = this.plugins[pipe].filter(function(p) {
1661
+ return p != plugin;
1662
+ });
1663
+ };
1664
+
1665
+ var updateDataSource = function(name) {
1666
+ global.document.querySelectorAll('[data-simply-data="'+name+'"]').forEach(function(list) {
1667
+ global.editor.list.applyDataSource(list, name);
1668
+ });
1669
+ };
1670
+
1671
+ var createSort = function(options) {
1672
+ var defaultOptions = {
1673
+ name: 'sort',
1674
+ getSort: function(params) {
1675
+ return Array.prototype.sort;
1676
+ }
1677
+ };
1678
+ options = Object.assign(defaultOptions, options || {});
1414
1679
 
1415
- if (!parentListeners.has(model)) {
1416
- parentListeners.set(model, {});
1417
- }
1418
- var parentPath = parent(path);
1419
- onParents(model, parentPath, function(parentOb, key, currPath) {
1420
- if (!parentListeners.get(model)[currPath]) {
1421
- parentListeners.get(model)[currPath] = [];
1680
+ return function(params) {
1681
+ this.options[options.name] = options;
1682
+ if (params[options.name]) {
1683
+ options = Object.assign(options, params[options.name]);
1422
1684
  }
1423
- parentListeners.get(model)[currPath].push(path);
1424
- });
1685
+ this.view.data.sort(options.getSort.call(this, options));
1686
+ };
1687
+ };
1425
1688
 
1426
- if (!childListeners.has(model)) {
1427
- childListeners.set(model, {});
1428
- }
1429
- onChildren(model, path, function(childOb, key, currPath) {
1430
- if (!childListeners.get(model)[currPath]) {
1431
- childListeners.get(model)[currPath] = [];
1689
+ var createPaging = function(options) {
1690
+ var defaultOptions = {
1691
+ name: 'paging',
1692
+ page: 1,
1693
+ pageSize: 100,
1694
+ max: 1,
1695
+ prev: 0,
1696
+ next: 0
1697
+ };
1698
+ options = Object.assign(defaultOptions, options || {});
1699
+
1700
+ return function(params) {
1701
+ this.options[options.name] = options;
1702
+ if (this.view.data) {
1703
+ options.max = Math.max(1, Math.ceil(Array.from(this.view.data).length / options.pageSize));
1704
+ } else {
1705
+ options.max = 1;
1706
+ }
1707
+ if (this.view.changed) {
1708
+ options.page = 1; // reset to page 1 when something in the view data has changed
1709
+ }
1710
+ if (params[options.name]) {
1711
+ options = Object.assign(options, params[options.name]);
1712
+ }
1713
+ options.page = Math.max(1, Math.min(options.max, options.page)); // clamp page nr
1714
+ options.prev = options.page - 1; // calculate previous page, 0 is allowed
1715
+ if (options.page<options.max) {
1716
+ options.next = options.page + 1;
1717
+ } else {
1718
+ options.next = 0; // no next page
1432
1719
  }
1433
- childListeners.get(model)[currPath].push(path);
1434
- });
1435
- }
1436
1720
 
1437
- function removeChangeListener(model, path, callback) {
1438
- if (!changeListeners.has(model)) {
1439
- return;
1721
+ var start = (options.page - 1) * options.pageSize;
1722
+ var end = start + options.pageSize;
1723
+
1724
+ this.view.data = this.view.data.slice(start, end);
1725
+ };
1726
+ };
1727
+
1728
+ var createFilter = function(options) {
1729
+ var defaultOptions = {
1730
+ name: 'filter',
1731
+ label: 'A filter',
1732
+ getMatch: function(entry) {
1733
+ return false;
1734
+ }
1735
+ };
1736
+ options = Object.assign(defaultOptions, options || {});
1737
+ if (options.init) {
1738
+ options.init.call(this, options);
1440
1739
  }
1441
- if (changeListeners.get(model)[path]) {
1442
- changeListeners.get(model)[path] = changeListeners.get(model)[path].filter(function(f) {
1443
- return f != callback;
1444
- });
1740
+ return function(params) {
1741
+ this.options[options.name] = options;
1742
+ if (params[options.name]) {
1743
+ options = Object.assign(options, params[options.name]);
1744
+ }
1745
+ var match = options.getMatch.call(this, options);
1746
+ if (match) {
1747
+ options.enabled = true;
1748
+ this.view.data = this.view.data.filter(match);
1749
+ } else if (options.enabled) {
1750
+ options.enabled = false;
1751
+ }
1445
1752
  }
1446
1753
  }
1447
1754
 
1448
- function pauseObservers() {
1449
- observersPaused++;
1450
- }
1755
+ var viewmodel = {
1756
+ create: function(name, data, options) {
1757
+ return new ViewModel(name, data, options);
1758
+ },
1759
+ createFilter: createFilter,
1760
+ createSort: createSort,
1761
+ createPaging: createPaging,
1762
+ updateDataSource: updateDataSource
1763
+ };
1451
1764
 
1452
- function resumeObservers() {
1453
- observersPaused--;
1765
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1766
+ module.exports = viewmodel;
1767
+ } else {
1768
+ if (!global.simply) {
1769
+ global.simply = {};
1770
+ }
1771
+ global.simply.viewmodel = viewmodel;
1454
1772
  }
1455
1773
 
1456
- function attach(model, path, options) {
1774
+ })(this);(function(global) {
1775
+ 'use strict';
1776
+
1777
+ var api = {
1778
+ /**
1779
+ * Returns a Proxy object that translates property access to a URL in the api
1780
+ * and method calls to a fetch on that URL.
1781
+ * @param options: a list of options for fetch(),
1782
+ * see the 'init' parameter at https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters
1783
+ * additionally:
1784
+ * - baseURL: (required) the endpoint of the API
1785
+ * - path: the current path in the API, is appended to the baseURL
1786
+ * - verbs: list of http verbs to allow as methods, default ['get','post']
1787
+ * - handlers.fetch: alternative fetch method
1788
+ * - handlers.result: alternative getResult method
1789
+ * - handlers.error: alternative error method
1790
+ * - user (and password): if set, a basic authentication header will be added
1791
+ * - paramsFormat: either 'formData', 'json' or 'search'. Default is search.
1792
+ * - responseFormat: test, formData, blob, json, arrayBuffer or unbuffered. Default is json.
1793
+ * @return Proxy
1794
+ */
1795
+ proxy: function(options) {
1796
+ var cache = () => {};
1797
+ cache.$options = Object.assign({}, options);
1798
+ return new Proxy( cache, getApiHandler(cache.$options) );
1799
+ },
1457
1800
 
1458
- var attachArray = function(object, path) {
1459
- var desc = Object.getOwnPropertyDescriptor(object, 'push');
1460
- if (!desc || desc.configurable) {
1461
- for (var f of ['push','pop','reverse','shift','sort','splice','unshift','copyWithin']) {
1462
- (function(f) {
1463
- try {
1464
- Object.defineProperty(object, f, {
1465
- value: function() {
1466
- pauseObservers();
1467
- var result = Array.prototype[f].apply(this, arguments);
1468
- attach(model, path);
1469
- var args = [].slice.call(arguments).map(function(arg) {
1470
- return JSON.stringify(arg);
1471
- });
1472
- resumeObservers();
1473
- signalChange(model, path, this, path+'.'+f+'('+args.join(',')+')');
1474
- return result;
1475
- },
1476
- readable: false,
1477
- enumerable: false,
1478
- configurable: false
1479
- });
1480
- } catch(e) {
1481
- console.error('simply.observer: Error: Couldn\'t redefine array method '+f+' on '+path, e);
1801
+ /**
1802
+ * Fetches the options.baseURL using the fetch api and returns a promise
1803
+ * Extra options in addition to those of global.fetch():
1804
+ * - user (and password): if set, a basic authentication header will be added
1805
+ * - paramsFormat: either 'formData', 'json' or 'search'
1806
+ * By default params, if set, will be added to the baseURL as searchParams
1807
+ * @param method one of the http verbs, e.g. get, post, etc.
1808
+ * @param options the options for fetch(), with some additions
1809
+ * @param params the parameters to send with the request, as javascript/json data
1810
+ * @return Promise
1811
+ */
1812
+ fetch: function(method, params, options) {
1813
+ if (!options.url) {
1814
+ if (!options.baseURL) {
1815
+ throw new Error('No url or baseURL in options object');
1816
+ }
1817
+ while (options.baseURL[options.baseURL.length-1]=='/') {
1818
+ options.baseURL = options.baseURL.substr(0, options.baseURL.length-1);
1819
+ }
1820
+ var url = new URL(options.baseURL+options.path);
1821
+ } else {
1822
+ var url = options.url;
1823
+ }
1824
+ var fetchOptions = Object.assign({}, options);
1825
+ if (!fetchOptions.headers) {
1826
+ fetchOptions.headers = {};
1827
+ }
1828
+ if (params) {
1829
+ if (method=='GET') {
1830
+ var paramsFormat = 'search';
1831
+ } else {
1832
+ var paramsFormat = options.paramsFormat;
1833
+ }
1834
+ switch(paramsFormat) {
1835
+ case 'formData':
1836
+ var formData = new FormData();
1837
+ for (const name in params) {
1838
+ formData.append(name, params[name]);
1482
1839
  }
1483
- }(f));
1840
+ if (!fetchOptions.headers['Content-Type']) {
1841
+ fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
1842
+ }
1843
+ break;
1844
+ case 'json':
1845
+ var formData = JSON.stringify(params);
1846
+ if (!fetchOptions.headers['Content-Type']) {
1847
+ fetchOptions.headers['Content-Type'] = 'application/json';
1848
+ }
1849
+ break;
1850
+ case 'search':
1851
+ var searchParams = url.searchParams; //new URLSearchParams(url.search.slice(1));
1852
+ for (const name in params) {
1853
+ searchParams.set(name, params[name]);
1854
+ }
1855
+ url.search = searchParams.toString();
1856
+ break;
1857
+ default:
1858
+ throw Error('Unknown options.paramsFormat '+options.paramsFormat+'. Select one of formData, json or search.');
1859
+ break;
1484
1860
  }
1485
- for (var i=0, l=object.length; i<l; i++) {
1486
- //FIXME: options becomes undefined here somewhere
1487
- // if (options.skipArray) {
1488
- addSetter(object, i, path+'.'+i);
1489
- // } else {
1490
- // attach(model, path+'.'+i, options);
1491
- // }
1861
+ }
1862
+ if (formData) {
1863
+ fetchOptions.body = formData
1864
+ }
1865
+ if (options.user) {
1866
+ fetchOptions.headers['Authorization'] = 'Basic '+btoa(options.user+':'+options.password);
1867
+ }
1868
+ fetchOptions.method = method.toUpperCase();
1869
+ var fetchURL = url.toString()
1870
+ return fetch(fetchURL, fetchOptions);
1871
+ },
1872
+ /**
1873
+ * Creates a function to call one or more graphql queries
1874
+ */
1875
+ graphqlQuery: function(url, query, options) {
1876
+ options = Object.assign({ paramsFormat: 'json', url: url, responseFormat: 'json' }, options);
1877
+ return function(params, operationName) {
1878
+ let postParams = {
1879
+ query: query
1880
+ };
1881
+ if (operationName) {
1882
+ postParams.operationName = operationName;
1883
+ }
1884
+ postParams.variables = params || {};
1885
+ return simply.api.fetch('POST', postParams, options )
1886
+ .then(function(response) {
1887
+ return simply.api.getResult(response, options);
1888
+ });
1889
+ }
1890
+ },
1891
+ /**
1892
+ * Handles the response and returns a Promise with the response data as specified
1893
+ * @param response Response
1894
+ * @param options
1895
+ * - responseFormat: one of 'text', 'formData', 'blob', 'arrayBuffer', 'unbuffered' or 'json'.
1896
+ * The default is json.
1897
+ */
1898
+ getResult: function(response, options) {
1899
+ if (response.ok) {
1900
+ switch(options.responseFormat) {
1901
+ case 'text':
1902
+ return response.text();
1903
+ break;
1904
+ case 'formData':
1905
+ return response.formData();
1906
+ break;
1907
+ case 'blob':
1908
+ return response.blob();
1909
+ break;
1910
+ case 'arrayBuffer':
1911
+ return response.arrayBuffer();
1912
+ break;
1913
+ case 'unbuffered':
1914
+ return response.body;
1915
+ break;
1916
+ case 'json':
1917
+ default:
1918
+ return response.json();
1919
+ break;
1920
+ }
1921
+ } else {
1922
+ throw {
1923
+ status: response.status,
1924
+ message: response.statusText,
1925
+ response: response
1492
1926
  }
1493
1927
  }
1494
- };
1928
+ },
1495
1929
 
1496
- var addSetTrigger = function(object, key, currPath) {
1497
- Object.defineProperty(object, key, {
1498
- set: function(value) {
1499
- addSetter(object, key, currPath);
1500
- object[key] = value;
1501
- },
1502
- configurable: true,
1503
- readable: false,
1504
- enumerable: false
1505
- });
1506
- };
1930
+ logError: function(error, options) {
1931
+ console.error(error.status, error.message);
1932
+ }
1933
+ }
1507
1934
 
1508
- var addSetter = function(object, key, currPath) {
1509
- if (Object.getOwnPropertyDescriptor(object, key).configurable) {
1510
- // assume object keys are only unconfigurable if the
1511
- // following code has already been run on this property
1512
- var _value = object[key];
1513
- Object.defineProperty(object, key, {
1514
- set: function(value) {
1515
- _value = value;
1516
- signalChange(model, currPath, value);
1517
- if (value!=null) {
1518
- onChildren(model, currPath, addSetter);
1519
- onMissingChildren(model, currPath, addSetTrigger);
1935
+ var defaultOptions = {
1936
+ path: '',
1937
+ responseFormat: 'json',
1938
+ paramsFormat: 'search',
1939
+ verbs: ['get','post'],
1940
+ handlers: {
1941
+ fetch: api.fetch,
1942
+ result: api.getResult,
1943
+ error: api.logError
1944
+ }
1945
+ };
1946
+
1947
+ function cd(path, name) {
1948
+ name = name.replace(/\//g,'');
1949
+ if (!path.length || path[path.length-1]!=='/') {
1950
+ path+='/';
1951
+ }
1952
+ return path+encodeURIComponent(name);
1953
+ }
1954
+
1955
+ function fetchChain(prop, params) {
1956
+ var options = this;
1957
+ return this.handlers.fetch
1958
+ .call(this, prop, params, options)
1959
+ .then(function(res) {
1960
+ return options.handlers.result.call(options, res, options);
1961
+ })
1962
+ .catch(function(error) {
1963
+ return options.handlers.error.call(options, error, options);
1964
+ });
1965
+ }
1966
+
1967
+ function getApiHandler(options) {
1968
+ options.handlers = Object.assign({}, defaultOptions.handlers, options.handlers);
1969
+ options = Object.assign({}, defaultOptions, options);
1970
+
1971
+ return {
1972
+ get: function(cache, prop) {
1973
+ if (!cache[prop]) {
1974
+ if (options.verbs.indexOf(prop)!=-1) {
1975
+ cache[prop] = function(params) {
1976
+ return fetchChain.call(options, prop, params);
1520
1977
  }
1521
- },
1522
- get: function() {
1523
- return _value;
1524
- },
1525
- configurable: false,
1526
- readable: true,
1527
- enumerable: true
1528
- });
1529
- }
1530
- if (Array.isArray(object[key])) {
1531
- attachArray(object[key], currPath, options);
1978
+ } else {
1979
+ cache[prop] = api.proxy(Object.assign({}, options, {
1980
+ path: cd(options.path, prop)
1981
+ }));
1982
+ }
1983
+ }
1984
+ return cache[prop];
1985
+ },
1986
+ apply: function(cache, thisArg, params) {
1987
+ return fetchChain.call(options, 'get', params[0] ? params[0] : null)
1532
1988
  }
1533
- };
1534
-
1535
- onParents(model, path, addSetter);
1536
- onChildren(model, path, addSetter);
1989
+ }
1537
1990
  }
1538
1991
 
1539
- // FIXME: if you remove a key by reassigning the parent object
1540
- // and then assign that missing key a new value
1541
- // the observer doesn't get triggered
1542
- // var model = { foo: { bar: 'baz' } };
1543
- // simply.observer(model, 'foo.bar', ...)
1544
- // model.foo = { }
1545
- // model.foo.bar = 'zab'; // this should trigger the observer but doesn't
1546
1992
 
1547
- simply.observe = function(model, path, callback, options) {
1548
- if (!path) {
1549
- var keys = Object.keys(model);
1550
- keys.forEach(function(key) {
1551
- attach(model, key, options);
1552
- addChangeListener(model, key, callback);
1553
- });
1554
- return function() {
1555
- keys.forEach(function(key) {
1556
- removeChangeListener(model, key, callback);
1557
- });
1558
- };
1559
- } else {
1560
- attach(model, path, options);
1561
- addChangeListener(model, path, callback);
1562
- return function() {
1563
- removeChangeListener(model, path, callback);
1564
- };
1993
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1994
+ module.exports = api;
1995
+ } else {
1996
+ if (!global.simply) {
1997
+ global.simply = {};
1565
1998
  }
1566
- };
1999
+ global.simply.api = api;
2000
+ }
1567
2001
 
1568
- return simply;
1569
- })(this.simply || {}, this);this.simply = (function(simply) {
2002
+ })(this);
2003
+ (function(global) {
2004
+ 'use strict';
1570
2005
 
1571
- simply.path = {
1572
- get: function(model, path) {
1573
- if (!path) {
1574
- return model;
2006
+ var app = function(options) {
2007
+ if (!options) {
2008
+ options = {};
2009
+ }
2010
+ if (!options.container) {
2011
+ console.warn('No simply.app application container element specified, using document.body.');
2012
+ }
2013
+
2014
+ function simplyApp(options) {
2015
+ if (!options) {
2016
+ options = {};
2017
+ }
2018
+ if ( options.routes ) {
2019
+ simply.route.load(options.routes);
2020
+ if (options.routeEvents) {
2021
+ Object.keys(options.routeEvents).forEach(function(action) {
2022
+ Object.keys(options.routeEvents[action]).forEach(function(route) {
2023
+ options.routeEvents[action][route].forEach(function(callback) {
2024
+ simply.route.addListener(action, route, callback);
2025
+ });
2026
+ });
2027
+ });
2028
+ }
2029
+ simply.route.handleEvents();
2030
+ global.setTimeout(function() {
2031
+ simply.route.match(global.location.pathname+global.location.hash);
2032
+ });
2033
+ }
2034
+ this.container = options.container || document.body;
2035
+ this.keyboard = simply.keyboard ? simply.keyboard(this, options.keyboard || {}) : false;
2036
+ this.actions = simply.action ? simply.action(this, options.actions) : false;
2037
+ this.commands = simply.command ? simply.command(this, options.commands) : false;
2038
+ this.resize = simply.resize ? simply.resize(this, options.resize) : false;
2039
+ this.view = simply.view ? simply.view(this, options.view) : false;
2040
+ if (!(global.editor && global.editor.field) && simply.bind) {
2041
+ // skip simplyview databinding if SimplyEdit is loaded
2042
+ options.bind = simply.render(options.bind || {});
2043
+ options.bind.model = this.view;
2044
+ options.bind.container = this.container;
2045
+ this.bind = options.bindings = simply.bind(options.bind);
1575
2046
  }
1576
- return path.split('.').reduce(function(acc, name) {
1577
- return (acc && acc[name] ? acc[name] : null);
1578
- }, model);
1579
- },
1580
- set: function(model, path, value) {
1581
- var lastName = simply.path.pop(path);
1582
- var parentPath = simply.path.parent(path);
1583
- var parentOb = simply.path.get(model, parentPath);
1584
- parentOb[lastName] = value;
1585
- },
1586
- pop: function(path) {
1587
- return path.split('.').pop();
1588
- },
1589
- push: function(path, name) {
1590
- return (path ? path + '.' : '') + name;
1591
- },
1592
- parent: function(path) {
1593
- var p = path.split('.');
1594
- p.pop();
1595
- return p.join('.');
1596
- },
1597
- parents: function(path) {
1598
- var result = [];
1599
- path.split('.').reduce(function(acc, name) {
1600
- acc.push( (acc.length ? acc[acc.length-1] + '.' : '') + name );
1601
- return acc;
1602
- },result);
1603
- return result;
1604
2047
  }
2048
+
2049
+ simplyApp.prototype.get = function(id) {
2050
+ return this.container.querySelector('[data-simply-id='+id+']') || document.getElementById(id);
2051
+ };
2052
+
2053
+ return new simplyApp(options);
1605
2054
  };
1606
2055
 
1607
- return simply;
1608
- })(this.simply || {});
2056
+ if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
2057
+ module.exports = app;
2058
+ } else {
2059
+ if (!global.simply) {
2060
+ global.simply = {};
2061
+ }
2062
+ global.simply.app = app;
2063
+ }
2064
+
2065
+ })(this);