pakyow 0.9.1 → 0.10.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +20 -0
  4. data/README.md +86 -0
  5. data/lib/commands/console.rb +1 -1
  6. data/lib/commands/server.rb +1 -1
  7. data/lib/generators/pakyow/app/templates/Gemfile +13 -4
  8. data/lib/generators/pakyow/app/templates/README.md +9 -9
  9. data/lib/generators/pakyow/app/templates/app/lib/routes.rb +4 -5
  10. data/lib/generators/pakyow/app/templates/{app.rb → app/setup.rb} +7 -1
  11. data/lib/generators/pakyow/app/templates/app/views/_templates/default.html +31 -0
  12. data/lib/generators/pakyow/app/templates/app/views/index.html +96 -0
  13. data/lib/generators/pakyow/app/templates/config.ru +1 -1
  14. data/lib/generators/pakyow/app/templates/public/apple-touch-icon-precomposed.png +0 -0
  15. data/lib/generators/pakyow/app/templates/public/apple-touch-icon.png +0 -0
  16. data/lib/generators/pakyow/app/templates/public/favicon.ico +0 -0
  17. data/{MIT-LICENSE → lib/generators/pakyow/app/templates/public/scripts/ring/LICENSE} +2 -2
  18. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/fastlink.js +13 -0
  19. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/fastlink.min.js +1 -0
  20. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/loader.js +9 -0
  21. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/loader.min.js +1 -0
  22. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/modal.js +78 -0
  23. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/modal.min.js +1 -0
  24. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/mutable.js +61 -0
  25. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/mutable.min.js +1 -0
  26. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/navigator.js +142 -0
  27. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/navigator.min.js +1 -0
  28. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/notifier.js +20 -0
  29. data/lib/generators/pakyow/app/templates/public/scripts/ring/components/notifier.min.js +1 -0
  30. data/lib/generators/pakyow/app/templates/public/scripts/ring/pakyow.js +1801 -0
  31. data/lib/generators/pakyow/app/templates/public/scripts/ring/pakyow.min.js +1 -0
  32. data/lib/generators/pakyow/app/templates/public/styles/pakyow-css/LICENSE +20 -0
  33. data/lib/generators/pakyow/app/templates/public/styles/pakyow-css/VERSION +1 -0
  34. data/lib/generators/pakyow/app/templates/public/styles/pakyow-css/reset.css +2 -0
  35. data/lib/generators/pakyow/app/templates/public/styles/pakyow-css/structure.css +2 -0
  36. data/lib/generators/pakyow/app/templates/public/styles/pakyow-css/syntax.css +2 -0
  37. data/lib/generators/pakyow/app/templates/public/styles/pakyow-css/theme.css +2 -0
  38. data/lib/generators/pakyow/app/templates/spec/integration/app_spec.rb +17 -0
  39. data/lib/generators/pakyow/app/templates/spec/spec_helper.rb +7 -0
  40. data/lib/pakyow.rb +6 -4
  41. data/lib/version.rb +3 -0
  42. metadata +93 -36
  43. data/lib/generators/pakyow/app/templates/app/views/_templates/pakyow.html +0 -17
  44. data/lib/generators/pakyow/app/templates/public/pakyow-css/CHANGES +0 -7
  45. data/lib/generators/pakyow/app/templates/public/pakyow-css/README.md +0 -3
  46. data/lib/generators/pakyow/app/templates/public/pakyow-css/VERSION +0 -1
  47. data/lib/generators/pakyow/app/templates/public/pakyow-css/examples/extension.css +0 -7
  48. data/lib/generators/pakyow/app/templates/public/pakyow-css/examples/structure-fluid.html +0 -151
  49. data/lib/generators/pakyow/app/templates/public/pakyow-css/examples/structure.html +0 -157
  50. data/lib/generators/pakyow/app/templates/public/pakyow-css/examples/styled.html +0 -114
  51. data/lib/generators/pakyow/app/templates/public/pakyow-css/reset.css +0 -46
  52. data/lib/generators/pakyow/app/templates/public/pakyow-css/structure.css +0 -199
  53. data/lib/generators/pakyow/app/templates/public/pakyow-css/style.css +0 -191
  54. data/lib/generators/pakyow/app/templates/public/pakyow-css/syntax.css +0 -279
@@ -0,0 +1 @@
1
+ pw.component.register("modal",function(n,t,e,i){var o,a,d=this,c="modal:"+i;this.listen(c+":navigator:enter",function(n){o||(o=document.createElement("DIV"),o.classList.add("ui-modal-blinder"),a=document.createElement("DIV"),a.classList.add("ui-modal"),o.appendChild(a),document.body.appendChild(o),o.addEventListener("click",function(n){if(n.target===o){n.preventDefault(),d.close();var t=window.location.pathname,e={uri:t};window.history.pushState(e,t,t)}})),a.innerHTML=n.body,pw.component.findAndInit(o),o.classList.add("ui-appear")}),this.listen(c+":navigator:exit",function(){console.log("exit"),d.close()}),this.listen(c+":navigator:boot",function(n){d.load(n)}),n.node.addEventListener("click",function(n){return n.preventDefault(),d.load(this.href),!1}),this.load=function(n){if(!window.socket)return void(document.location=n);var e={uri:n,context:"modal:"+i};t.container&&(e.container=t.container),window.history.pushState(e,n,n)},this.close=function(){pw.node.remove(o),o=null,a=null}});
@@ -0,0 +1,61 @@
1
+ pw.component.register('mutable', function (view, config) {
2
+ this.mutation = function (mutation) {
3
+ // no socket, just submit the form
4
+ if (!window.socket) {
5
+ view.node.submit();
6
+ return;
7
+ }
8
+
9
+ var datum = pw.util.dup(mutation);
10
+ delete datum.__nested;
11
+ delete datum.scope;
12
+ delete datum.id;
13
+
14
+ var message = {
15
+ action: 'call-route'
16
+ };
17
+
18
+ if (view.node.tagName === 'FORM') {
19
+ if (view.node.querySelector('input[type="file"]')) {
20
+ // file uploads over websocket are not supported
21
+ view.node.submit();
22
+ return;
23
+ }
24
+
25
+ var method;
26
+ var $methodOverride = view.node.querySelector('input[name="_method"]');
27
+ if ($methodOverride) {
28
+ method = $methodOverride.value;
29
+ } else {
30
+ method = view.node.getAttribute('method');
31
+ }
32
+
33
+ message.method = method;
34
+ message.uri = view.node.getAttribute('action');
35
+ message.input = pw.node.serialize(view.node);
36
+ } else {
37
+ //TODO deduce uri / method
38
+
39
+ var input = {};
40
+ input[mutation.scope] = datum;
41
+ message.input = input;
42
+ }
43
+
44
+ var self = this;
45
+ window.socket.send(message, function (res) {
46
+ if (res.status === 302 && res.headers.Location !== window.location.pathname) {
47
+ var dest = res.headers.Location;
48
+ //TODO trigger a response:redirect instead and let navigator subscribe
49
+ history.pushState({ uri: dest }, dest, dest);
50
+ return;
51
+ } else if (res.status === 400) {
52
+ // bad request
53
+ } else {
54
+ self.state.rollback();
55
+ }
56
+
57
+ pw.component.broadcast('response:received', { response: res });
58
+ self.revert();
59
+ });
60
+ }
61
+ });
@@ -0,0 +1 @@
1
+ pw.component.register("mutable",function(e,t){this.mutation=function(t){if(!window.socket)return void e.node.submit();var o=pw.util.dup(t);delete o.__nested,delete o.scope,delete o.id;var n={action:"call-route"};if("FORM"===e.node.tagName){if(e.node.querySelector('input[type="file"]'))return void e.node.submit();var i,r=e.node.querySelector('input[name="_method"]');i=r?r.value:e.node.getAttribute("method"),n.method=i,n.uri=e.node.getAttribute("action"),n.input=pw.node.serialize(e.node)}else{var a={};a[t.scope]=o,n.input=a}var d=this;window.socket.send(n,function(e){if(302===e.status&&e.headers.Location!==window.location.pathname){var t=e.headers.Location;return void history.pushState({uri:t},t,t)}400===e.status||d.state.rollback(),pw.component.broadcast("response:received",{response:e}),d.revert()})}});
@@ -0,0 +1,142 @@
1
+ function boot() {
2
+ if (!window.socket) {
3
+ setTimeout(boot, 100);
4
+ return;
5
+ }
6
+
7
+ if (window.location.hash) {
8
+ var arr = window.location.hash.split('#:')[1].split('/');
9
+ var context = arr.shift();
10
+ var uri = arr.join('/');
11
+
12
+ pw.component.broadcast(context + ':navigator:boot', uri);
13
+ }
14
+ }
15
+
16
+ (function(history) {
17
+ pw.init.register(boot);
18
+
19
+ if (history) {
20
+ var hasPushed = false;
21
+ var pushState = history.pushState;
22
+
23
+ history.pushState = function(state, title, uri) {
24
+ hasPushed = true;
25
+
26
+ if (typeof history.onpushstate == "function") {
27
+ history.onpushstate({ state: state });
28
+ }
29
+
30
+ if (uri == window.location.pathname) {
31
+ window.context = {
32
+ _state: state,
33
+ name: 'default',
34
+ uri: window.location.href
35
+ };
36
+
37
+ state.r_uri = uri;
38
+ } else {
39
+ handleState(state, 'forward');
40
+ }
41
+
42
+ return pushState.apply(history, [state, title, state.r_uri]);
43
+ }
44
+
45
+ window.onpopstate = function (evt) {
46
+ if (!hasPushed) {
47
+ return;
48
+ }
49
+
50
+ var state = evt.state;
51
+
52
+ if (!state) {
53
+ state = {};
54
+ }
55
+
56
+ if (!state.uri) {
57
+ state.uri = window.context.uri;
58
+ }
59
+
60
+ handleState(state, 'back');
61
+ }
62
+ } else {
63
+ // unsupported
64
+ }
65
+ })(window.history);
66
+
67
+ window.context = {
68
+ name: 'default',
69
+ uri: window.location.href
70
+ };
71
+
72
+ function handleState(state, direction) {
73
+ var uri = state.uri || state.url;
74
+
75
+ // socket isn't ready, so just send 'em to the url
76
+ if (!window.socket) {
77
+ document.location = uri;
78
+ return;
79
+ }
80
+
81
+ if (state.context) {
82
+ state.r_uri = document.location.pathname + '#:' + state.context + '/' + uri;
83
+
84
+ window.context = {
85
+ _state: state,
86
+ name: state.context,
87
+ uri: state.r_uri,
88
+ container: state.container
89
+ };
90
+ } else {
91
+ state.r_uri = uri;
92
+
93
+ if (window.context.name !== 'default') {
94
+ if (direction === 'back') {
95
+ // we are leaving a context
96
+ pw.component.broadcast(window.context.name + ':navigator:exit');
97
+
98
+ window.context = {
99
+ name: 'default',
100
+ uri: state.uri
101
+ };
102
+
103
+ return;
104
+ } else {
105
+ // navigate in context
106
+ state.r_uri = document.location.pathname + '#:' + window.context.name + '/' + uri;
107
+ state.context = window.context.name;
108
+ state.container = window.context.container;
109
+ }
110
+ }
111
+ }
112
+
113
+ var opts = {
114
+ uri: uri,
115
+ action: 'call-route',
116
+ method: 'get'
117
+ };
118
+
119
+ if (state.container) {
120
+ opts.container = state.container;
121
+ }
122
+
123
+ window.socket.send(opts, function (payload) {
124
+ if (state.context) {
125
+ pw.component.broadcast(state.context + ':navigator:enter', payload);
126
+ } else {
127
+ var body = payload.body[0];
128
+
129
+ if (body.match(/<title>/)) {
130
+ document.title = body.split(/<title>/)[1].split('</title>')[0];
131
+ }
132
+
133
+ if (body.match(/<body [^>]*>/)) {
134
+ document.body.innerHTML = body.split(/<body [^>]*>/)[1].split('</body>')[0];
135
+ } else {
136
+ document.body.innerHTML = body;
137
+ }
138
+
139
+ pw.component.findAndInit(document.querySelectorAll('body')[0]);
140
+ }
141
+ });
142
+ }
@@ -0,0 +1 @@
1
+ function boot(){if(!window.socket)return void setTimeout(boot,100);if(window.location.hash){var t=window.location.hash.split("#:")[1].split("/"),n=t.shift(),o=t.join("/");pw.component.broadcast(n+":navigator:boot",o)}}function handleState(t,n){var o=t.uri||t.url;if(!window.socket)return void(document.location=o);if(t.context)t.r_uri=document.location.pathname+"#:"+t.context+"/"+o,window.context={_state:t,name:t.context,uri:t.r_uri,container:t.container};else if(t.r_uri=o,"default"!==window.context.name){if("back"===n)return pw.component.broadcast(window.context.name+":navigator:exit"),void(window.context={name:"default",uri:t.uri});t.r_uri=document.location.pathname+"#:"+window.context.name+"/"+o,t.context=window.context.name,t.container=window.context.container}var e={uri:o,action:"call-route",method:"get"};t.container&&(e.container=t.container),window.socket.send(e,function(n){if(t.context)pw.component.broadcast(t.context+":navigator:enter",n);else{var o=n.body[0];o.match(/<title>/)&&(document.title=o.split(/<title>/)[1].split("</title>")[0]),o.match(/<body [^>]*>/)?document.body.innerHTML=o.split(/<body [^>]*>/)[1].split("</body>")[0]:document.body.innerHTML=o,pw.component.findAndInit(document.querySelectorAll("body")[0])}})}!function(t){if(pw.init.register(boot),t){var n=!1,o=t.pushState;t.pushState=function(e,i,a){return n=!0,"function"==typeof t.onpushstate&&t.onpushstate({state:e}),a==window.location.pathname?(window.context={_state:e,name:"default",uri:window.location.href},e.r_uri=a):handleState(e,"forward"),o.apply(t,[e,i,e.r_uri])},window.onpopstate=function(t){if(n){var o=t.state;o||(o={}),o.uri||(o.uri=window.context.uri),handleState(o,"back")}}}}(window.history),window.context={name:"default",uri:window.location.href};
@@ -0,0 +1,20 @@
1
+ pw.component.register('notifier', function (view, config) {
2
+ this.listen('notification:published', function (payload) {
3
+ view.node.innerText = payload.notification;
4
+ view.node.classList.remove('hide');
5
+ });
6
+
7
+ this.listen('response:received', function (payload) {
8
+ //TODO support notification type and add a class based on it for styling
9
+ var notification = payload.response.headers['Pakyow-Notify'];
10
+
11
+ if (notification) {
12
+ view.node.innerText = notification;
13
+ view.node.classList.remove('hide');
14
+ }
15
+ });
16
+
17
+ view.node.addEventListener('click', function (evt) {
18
+ view.node.classList.add('hide');
19
+ });
20
+ });
@@ -0,0 +1 @@
1
+ pw.component.register("notifier",function(e,n){this.listen("notification:published",function(n){e.node.innerText=n.notification,e.node.classList.remove("hide")}),this.listen("response:received",function(n){var i=n.response.headers["Pakyow-Notify"];i&&(e.node.innerText=i,e.node.classList.remove("hide"))}),e.node.addEventListener("click",function(n){e.node.classList.add("hide")})});
@@ -0,0 +1,1801 @@
1
+ var pw = {
2
+ version: '0.1.0'
3
+ };
4
+
5
+ (function() {
6
+ pw.util = {
7
+ guid: function () {
8
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
9
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
10
+ return v.toString(16);
11
+ });
12
+ },
13
+
14
+ dup: function (object) {
15
+ return JSON.parse(JSON.stringify(object));
16
+ }
17
+ };
18
+ var fns = [];
19
+
20
+ pw.init = {
21
+ register: function (fn) {
22
+ fns.push(fn);
23
+ }
24
+ };
25
+
26
+ document.addEventListener("DOMContentLoaded", function() {
27
+ fns.forEach(function (fn) {
28
+ fn();
29
+ });
30
+ });
31
+ var sigAttrs = ['data-scope', 'data-prop'];
32
+ var valuelessTags = ['SELECT'];
33
+ var selfClosingTags = ['AREA', 'BASE', 'BASEFONT', 'BR', 'HR', 'INPUT', 'IMG', 'LINK', 'META'];
34
+
35
+ pw.node = {
36
+ // returns the value of the node
37
+ value: function (node) {
38
+ if (node.tagName === 'INPUT') {
39
+ if (node.type === 'checkbox') {
40
+ if (node.checked) {
41
+ return node.value ? node.value : true;
42
+ } else {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ return node.value;
48
+ } else if (node.tagName === 'TEXTAREA') {
49
+ return node.value;
50
+ } else if (node.tagName === 'SELECT') {
51
+ return node.value;
52
+ }
53
+
54
+ return node.textContent.trim();
55
+ },
56
+
57
+ /*
58
+ Returns a representation of the node's state. Example:
59
+
60
+ <div data-scope="list" data-id="1">
61
+ <div data-scope="task" data-id="1">
62
+ <label data-prop="desc">
63
+ foo
64
+ </label>
65
+ </div>
66
+ </div>
67
+
68
+ [ [ { node: ..., id: '1', scope: 'list' }, [ { node: ..., id: '1', scope: 'task' }, [ { node: ..., prop: 'body' } ] ] ] ]
69
+ */
70
+
71
+ significant: function(node, arr) {
72
+ if(node === document) {
73
+ node = document.getElementsByTagName('body')[0];
74
+ }
75
+
76
+ if(arr === undefined) {
77
+ arr = [];
78
+ }
79
+
80
+ var sig, nArr;
81
+
82
+ if(sig = pw.node.isSignificant(node)) {
83
+ nArr = [];
84
+ arr.push([{ node: sig[0], type: sig[1] }, nArr]);
85
+ } else {
86
+ nArr = arr;
87
+ }
88
+
89
+ pw.node.toA(node.children).forEach(function (child) {
90
+ pw.node.significant(child, nArr);
91
+ });
92
+
93
+ return arr;
94
+ },
95
+
96
+ // returns node and an indication of it's significance
97
+ // (e.g value of scope/prop); returns false otherwise
98
+ isSignificant: function(node) {
99
+ var attr = sigAttrs.find(function (a) {
100
+ return node.hasAttribute(a);
101
+ });
102
+
103
+ if (attr) {
104
+ return [node, attr.split('-')[1]];
105
+ } else {
106
+ return false;
107
+ }
108
+ },
109
+
110
+ mutable: function (node) {
111
+ pw.node.significant(node).flatten().filter(function (n) {
112
+ return pw.node.isMutable(n.node);
113
+ }).map(function (n) {
114
+ return n.node;
115
+ });
116
+ },
117
+
118
+ // returns true if the node can mutate via interaction
119
+ isMutable: function(node) {
120
+ var tag = node.tagName;
121
+ return tag === 'FORM' || (tag === 'INPUT' && !node.disabled);
122
+ },
123
+
124
+ // triggers event name on node with data
125
+ trigger: function (evtName, node, data) {
126
+ var evt = document.createEvent('Event');
127
+ evt.initEvent(evtName, true, true);
128
+
129
+ node._evtData = data;
130
+ node.dispatchEvent(evt);
131
+ },
132
+
133
+ // replaces an event listener's callback for node by name
134
+ replaceEventListener: function (eventName, node, cb) {
135
+ node.removeEventListener(eventName);
136
+ node.addEventListener(eventName, cb);
137
+ },
138
+
139
+ inForm: function (node) {
140
+ if (node.tagName === 'FORM') {
141
+ return true;
142
+ }
143
+
144
+ var next = node.parentNode;
145
+ if (next !== document) {
146
+ return pw.node.inForm(next);
147
+ }
148
+ },
149
+
150
+ // finds and returns component for node
151
+ component: function (node) {
152
+ if (node.getAttribute('data-ui')) {
153
+ return node;
154
+ }
155
+
156
+ var next = node.parentNode;
157
+ if (next !== document) {
158
+ return pw.node.component(next);
159
+ }
160
+ },
161
+
162
+ // finds and returns scope for node
163
+ scope: function (node) {
164
+ if (node.getAttribute('data-scope')) {
165
+ return node;
166
+ }
167
+
168
+ var next = node.parentNode;
169
+ if (next !== document) {
170
+ return pw.node.scope(next);
171
+ }
172
+ },
173
+
174
+ // returns the name of the scope for node
175
+ scopeName: function (node) {
176
+ if (node.getAttribute('data-scope')) {
177
+ return node.getAttribute('data-scope');
178
+ }
179
+
180
+ var next = node.parentNode;
181
+ if (next !== document) {
182
+ return pw.node.scopeName(next);
183
+ }
184
+ },
185
+
186
+ // finds and returns prop for node
187
+ prop: function (node) {
188
+ if (node.getAttribute('data-prop')) {
189
+ return node;
190
+ }
191
+
192
+ var next = node.parentNode;
193
+ if (next !== document) {
194
+ return pw.node.prop(next);
195
+ }
196
+ },
197
+
198
+ // returns the name of the prop for node
199
+ propName: function (node) {
200
+ if (node.getAttribute('data-prop')) {
201
+ return node.getAttribute('data-prop');
202
+ }
203
+
204
+ var next = node.parentNode;
205
+ if (next !== document) {
206
+ return pw.node.propName(next);
207
+ }
208
+ },
209
+
210
+ // returns the name of the version for node
211
+ versionName: function (node) {
212
+ if (node.hasAttribute('data-version')) {
213
+ return node.getAttribute('data-version');
214
+ }
215
+ },
216
+
217
+ // creates a context in which view manipulations can occur
218
+ with: function(node, cb) {
219
+ cb.call(node);
220
+ },
221
+
222
+ for: function(node, data, cb) {
223
+ if (pw.node.isNodeList(node)) {
224
+ node = pw.node.toA(node);
225
+ }
226
+
227
+ node = Array.ensure(node);
228
+ data = Array.ensure(data);
229
+
230
+ node.forEach(function (e, i) {
231
+ cb.call(e, data[i]);
232
+ });
233
+ },
234
+
235
+ match: function(node, data) {
236
+ if (pw.node.isNodeList(node)) {
237
+ node = pw.node.toA(node);
238
+ }
239
+
240
+ node = Array.ensure(node);
241
+ data = Array.ensure(data);
242
+
243
+ var collection = data.reduce(function (c, dm, i) {
244
+ // get the view, or if we're out just use the last one
245
+ var v = n[i] || n[n.length - 1];
246
+
247
+ var dv = v.cloneNode(true);
248
+ v.parentNode.insertBefore(dv);
249
+ return c.concat([dv])
250
+ }, []);
251
+
252
+ node.forEach(function (o) {
253
+ o.parentNode.removeChild(o);
254
+ });
255
+
256
+ return collection;
257
+ },
258
+
259
+ repeat: function(node, data, cb) {
260
+ pw.node.for(pw.node.match(node, data), data, cb);
261
+ },
262
+
263
+ // binds an object to a node
264
+ bind: function (data, node, cb) {
265
+ var scope = pw.node.findBindings(node)[0];
266
+
267
+ pw.node.for(node, data, function(dm) {
268
+ if (!dm) {
269
+ return;
270
+ }
271
+
272
+ if(dm.id) {
273
+ this.setAttribute('data-id', dm.id);
274
+ }
275
+
276
+ pw.node.bindDataToScope(dm, scope, node);
277
+
278
+ if(!(typeof cb === 'undefined')) {
279
+ cb.call(this, dm);
280
+ }
281
+ });
282
+ },
283
+
284
+ apply: function (data, node, cb) {
285
+ var c = pw.node.match(node, data);
286
+ pw.node.bind(data, c, cb);
287
+ return c;
288
+ },
289
+
290
+ findBindings: function (node) {
291
+ var bindings = [];
292
+ pw.node.breadthFirst(node, function() {
293
+ var o = this;
294
+
295
+ var scope = o.getAttribute('data-scope');
296
+
297
+ if(!scope) {
298
+ return;
299
+ }
300
+
301
+ var props = [];
302
+ pw.node.breadthFirst(o, function() {
303
+ var so = this;
304
+
305
+ // don't go into deeper scopes
306
+ if(o != so && so.getAttribute('data-scope')) {
307
+ return;
308
+ }
309
+
310
+ var prop = so.getAttribute('data-prop');
311
+
312
+ if(!prop) {
313
+ return;
314
+ }
315
+
316
+ props.push({
317
+ prop: prop,
318
+ doc: so
319
+ });
320
+ });
321
+
322
+ bindings.push({
323
+ scope: scope,
324
+ props: props,
325
+ doc: o,
326
+ });
327
+ });
328
+
329
+ return bindings;
330
+ },
331
+
332
+ bindDataToScope: function (data, scope, node) {
333
+ if(!data || !scope) {
334
+ return;
335
+ }
336
+
337
+ scope['props'].forEach(function (p) {
338
+ k = p['prop'];
339
+ v = data[k];
340
+
341
+ if(!v) {
342
+ v = '';
343
+ }
344
+
345
+ if(typeof v === 'object') {
346
+ pw.node.bindValueToNode(v['__content'], p['doc']);
347
+ pw.node.bindAttributesToNode(v['__attrs'], p['doc']);
348
+ } else {
349
+ pw.node.bindValueToNode(v, p['doc']);
350
+ }
351
+ });
352
+ },
353
+
354
+ bindAttributesToNode: function (attrs, node) {
355
+ var nAtrs = pw.attrs.init(pw.view.init(node));
356
+
357
+ for(var attr in attrs) {
358
+ var v = attrs[attr];
359
+ if(typeof v === 'function') {
360
+ v = v.call(node.getAttribute(attr));
361
+ }
362
+
363
+ if (v) {
364
+ if (v instanceof Array) {
365
+ v.forEach(function (attrInstruction) {
366
+ nAtrs[attrInstruction[0]](attr, attrInstruction[1]);
367
+ });
368
+ } else {
369
+ nAtrs.set(attr, v);
370
+ }
371
+ } else {
372
+ nAtrs.remove(attr);
373
+ }
374
+ }
375
+ },
376
+
377
+ bindValueToNode: function (value, node) {
378
+ if(pw.node.isTagWithoutValue(node)) {
379
+ return;
380
+ }
381
+
382
+ //TODO handle other form fields (port from pakyow-presenter)
383
+ if (node.tagName === 'INPUT' && node.type === 'checkbox') {
384
+ if (value === true || (node.value && value === node.value)) {
385
+ node.checked = true;
386
+ } else {
387
+ node.checked = false;
388
+ }
389
+ } else {
390
+ if (pw.node.isSelfClosingTag(node)) {
391
+ node.value = value;
392
+ } else {
393
+ node.innerHTML = value;
394
+ }
395
+ }
396
+ },
397
+
398
+ isTagWithoutValue: function(node) {
399
+ return valuelessTags.indexOf(node.tagName) != -1 ? true : false;
400
+ },
401
+
402
+ isSelfClosingTag: function(node) {
403
+ return selfClosingTags.indexOf(node.tagName) != -1 ? true : false;
404
+ },
405
+
406
+ breadthFirst: function (node, cb) {
407
+ var queue = [node];
408
+ while (queue.length > 0) {
409
+ var subNode = queue.shift();
410
+ if (!subNode) continue;
411
+ if(typeof subNode == "object" && "nodeType" in subNode && subNode.nodeType === 1 && subNode.cloneNode) {
412
+ cb.call(subNode);
413
+ }
414
+
415
+ var children = subNode.childNodes;
416
+ if (children) {
417
+ for(var i = 0; i < children.length; i++) {
418
+ queue.push(children[i]);
419
+ }
420
+ }
421
+ }
422
+ },
423
+
424
+ isNodeList: function(nodes) {
425
+ return typeof nodes.length !== 'undefined';
426
+ },
427
+
428
+ byAttr: function (node, attr, value) {
429
+ return pw.node.all(node).filter(function (o) {
430
+ var ov = o.getAttribute(attr);
431
+ return ov !== null && ((typeof value) === 'undefined' || ov == value);
432
+ });
433
+ },
434
+
435
+ setAttr: function (node, attr, value) {
436
+ if (attr === 'style') {
437
+ value.pairs().forEach(function (kv) {
438
+ node.style[kv[0]] = kv[1];
439
+ });
440
+ } else {
441
+ if (attr === 'class') {
442
+ value = value.join(' ');
443
+ }
444
+
445
+ if (attr === 'checked') {
446
+ if (value) {
447
+ value = 'checked';
448
+ } else {
449
+ value = '';
450
+ }
451
+
452
+ node.checked = value;
453
+ }
454
+
455
+ node.setAttribute(attr, value);
456
+ }
457
+ },
458
+
459
+ all: function (node) {
460
+ var arr = [];
461
+
462
+ if (!node) {
463
+ return arr;
464
+ }
465
+
466
+ if(document !== node) {
467
+ arr.push(node);
468
+ }
469
+
470
+ return arr.concat(pw.node.toA(node.getElementsByTagName('*')));
471
+ },
472
+
473
+ before: function (node, newNode) {
474
+ node.parentNode.insertBefore(newNode, node);
475
+ },
476
+
477
+ after: function (node, newNode) {
478
+ node.parentNode.insertBefore(newNode, this.nextSibling);
479
+ },
480
+
481
+ replace: function (node, newNode) {
482
+ node.parentNode.replaceChild(newNode, node);
483
+ },
484
+
485
+ append: function (node, newNode) {
486
+ node.appendChild(newNode);
487
+ },
488
+
489
+ prepend: function (node, newNode) {
490
+ node.insertBefore(newNode, node.firstChild);
491
+ },
492
+
493
+ remove: function (node) {
494
+ node.parentNode.removeChild(node);
495
+ },
496
+
497
+ clear: function (node) {
498
+ while (node.firstChild) {
499
+ pw.node.remove(node.firstChild);
500
+ }
501
+ },
502
+
503
+ title: function (node, value) {
504
+ var titleNode;
505
+ if (titleNode = node.getElementsByTagName('title')[0]) {
506
+ titleNode.innerText = value;
507
+ }
508
+ },
509
+
510
+ toA: function (nodeSet) {
511
+ return Array.prototype.slice.call(nodeSet);
512
+ },
513
+
514
+ serialize: function (node) {
515
+ var json = {};
516
+ var working;
517
+ var value;
518
+ var split, last;
519
+ var previous, previous_name;
520
+ node.querySelectorAll('input, select, textarea').forEach(function (input) {
521
+ working = json;
522
+ split = input.name.split('[');
523
+ last = split[split.length - 1];
524
+ split.forEach(function (name) {
525
+ value = pw.node.value(input);
526
+
527
+ if (name == ']') {
528
+ if (!(previous[previous_name] instanceof Array)) {
529
+ previous[previous_name] = [];
530
+ }
531
+
532
+ if (value) {
533
+ previous[previous_name].push(value);
534
+ }
535
+ }
536
+
537
+ if (name != last) {
538
+ value = {};
539
+ }
540
+
541
+ name = name.replace(']', '');
542
+
543
+ if (name == '' || name == '_method') {
544
+ return;
545
+ }
546
+
547
+ if (!working[name]) {
548
+ working[name] = value;
549
+ }
550
+
551
+ previous = working;
552
+ previous_name = name;
553
+ working = working[name];
554
+ });
555
+ });
556
+
557
+ return json;
558
+ }
559
+ };
560
+ pw.attrs = {
561
+ init: function (v_or_vs) {
562
+ return new pw_Attrs(pw.collection.init(v_or_vs));
563
+ }
564
+ };
565
+
566
+ var attrTypes = {
567
+ hash: ['style'],
568
+ bool: ['selected', 'checked', 'disabled', 'readonly', 'multiple'],
569
+ mult: ['class']
570
+ };
571
+
572
+ var pw_Attrs = function (collection) {
573
+ this.views = collection.views;
574
+ };
575
+
576
+ pw_Attrs.prototype = {
577
+ findType: function (attr) {
578
+ if (attrTypes.hash.indexOf(attr) > -1) return 'hash';
579
+ if (attrTypes.bool.indexOf(attr) > -1) return 'bool';
580
+ if (attrTypes.mult.indexOf(attr) > -1) return 'mult';
581
+ return 'text';
582
+ },
583
+
584
+ findValue: function (view, attr) {
585
+ switch (attr) {
586
+ case 'class':
587
+ return view.node.classList;
588
+ case 'style':
589
+ return view.node.style;
590
+ }
591
+
592
+ if (this.findType(attr) === 'bool') {
593
+ return view.node.hasAttribute(attr);
594
+ } else {
595
+ return view.node.getAttribute(attr);
596
+ }
597
+ },
598
+
599
+ set: function (attr, value) {
600
+ this.views.forEach(function (view) {
601
+ pw.node.setAttr(view.node, attr, value);
602
+ });
603
+ },
604
+
605
+ remove: function (attr) {
606
+ this.views.forEach(function (view) {
607
+ view.node.removeAttribute(attr);
608
+ });
609
+ },
610
+
611
+ ensure: function (attr, value) {
612
+ this.views.forEach(function (view) {
613
+ var currentValue = this.findValue(view, attr);
614
+
615
+ if (attr === 'class') {
616
+ if (!currentValue.contains(value)) {
617
+ currentValue.add(value);
618
+ }
619
+ } else if (attr === 'style') {
620
+ value.pairs().forEach(function (kv) {
621
+ view.node.style[kv[0]] = kv[1];
622
+ });
623
+ } else if (this.findType(attr) === 'bool') {
624
+ if (!view.node.hasAttribute(attr)) {
625
+ pw.node.setAttr(view.node, attr, attr);
626
+ }
627
+ } else { // just a text attr
628
+ var currentValue = view.node.getAttribute(attr) || '';
629
+ if (!currentValue.match(value)) {
630
+ pw.node.setAttr(view.node, attr, currentValue + value);
631
+ }
632
+ }
633
+ }, this);
634
+ },
635
+
636
+ deny: function (attr, value) {
637
+ this.views.forEach(function (view) {
638
+ var currentValue = this.findValue(view, attr);
639
+ if (attr === 'class') {
640
+ if (currentValue.contains(value)) {
641
+ currentValue.remove(value);
642
+ }
643
+ } else if (attr === 'style') {
644
+ value.pairs().forEach(function (kv) {
645
+ view.node.style[kv[0]] = view.node.style[kv[0]].replace(kv[1], '');
646
+ });
647
+ } else if (this.findType(attr) === 'bool') {
648
+ if (view.node.hasAttribute(attr)) {
649
+ view.node.removeAttribute(attr);
650
+ }
651
+ } else { // just a text attr
652
+ pw.node.setAttr(view.node, attr, view.node.getAttribute(attr).replace(value, ''));
653
+ }
654
+ }, this);
655
+ },
656
+
657
+ insert: function (attr, value) {
658
+ this.views.forEach(function (view) {
659
+ var currentValue = this.findValue(view, attr);
660
+
661
+ switch (attr) {
662
+ case 'class':
663
+ currentValue.add(value);
664
+ break;
665
+ default:
666
+ pw.node.setAttr(view.node, attr, currentValue + value);
667
+ break;
668
+ }
669
+ }, this);
670
+ }
671
+ };
672
+ /*
673
+ State related functions.
674
+ */
675
+
676
+ pw.state = {
677
+ build: function (sigArr, parentObj) {
678
+ var nodeState;
679
+ return sigArr.reduce(function (acc, sig) {
680
+ if (nodeState = pw.state.buildForNode(sig, parentObj)) {
681
+ acc.push(nodeState);
682
+ }
683
+
684
+ return acc;
685
+ }, []);
686
+ },
687
+
688
+ buildForNode: function (sigTuple, parentObj) {
689
+ var sig = sigTuple[0];
690
+ var obj = {};
691
+
692
+ if (sig.type === 'scope') {
693
+ obj.id = sig.node.getAttribute('data-id');
694
+ obj.scope = sig.node.getAttribute('data-scope');
695
+ } else if (sig.type === 'prop' && parentObj) {
696
+ parentObj[sig.node.getAttribute('data-prop')] = pw.node.value(sig.node);
697
+ return;
698
+ }
699
+
700
+ obj['__nested'] = pw.state.build(sigTuple[1], obj);
701
+
702
+ return obj;
703
+ },
704
+
705
+ // creates and returns a new pw_State for the document or node
706
+ init: function (node, observer) {
707
+ return new pw_State(node, observer);
708
+ }
709
+ };
710
+
711
+
712
+ /*
713
+ pw_State represents the state for a document or node.
714
+ */
715
+
716
+ var pw_State = function (node) {
717
+ this.node = node;
718
+ //FIXME storing diffs is probably better than full snapshots
719
+ this.snapshots = [];
720
+ this.update();
721
+ }
722
+
723
+ pw_State.prototype = {
724
+ update: function () {
725
+ this.snapshots.push(pw.state.build(pw.node.significant(this.node)));
726
+ },
727
+
728
+ // gets the current represented state from the node and diffs it with the current state
729
+ diffNode: function (node) {
730
+ return pw.state.build(pw.node.significant(pw.node.scope(node)))[0];
731
+ },
732
+
733
+ revert: function () {
734
+ var initial = pw.util.dup(this.snapshots[0]);
735
+ this.snapshots = [initial];
736
+ return initial;
737
+ },
738
+
739
+ rollback: function () {
740
+ this.snapshots.pop();
741
+ return this.current();
742
+ },
743
+
744
+ // returns the current state for a node
745
+ node: function (nodeState) {
746
+ return this.current.flatten().find(function (state) {
747
+ return state.scope === nodeState.scope && state.id === nodeState.id;
748
+ });
749
+ },
750
+
751
+ append: function (state) {
752
+ var copy = this.copy();
753
+ copy.push(state);
754
+ this.snapshots.push(copy);
755
+ },
756
+
757
+ prepend: function (state) {
758
+ var copy = this.copy();
759
+ copy.unshift(state);
760
+ this.snapshots.push(copy);
761
+ },
762
+
763
+ delete: function (state) {
764
+ var copy = this.copy();
765
+ var match = copy.find(function (s) {
766
+ return s.id === state.id;
767
+ });
768
+
769
+ if (match) {
770
+ copy.splice(copy.indexOf(match), 1);
771
+ this.snapshots.push(copy);
772
+ }
773
+ },
774
+
775
+ copy: function () {
776
+ return pw.util.dup(this.current());
777
+ },
778
+
779
+ current: function () {
780
+ return this.snapshots[this.snapshots.length - 1];
781
+ },
782
+
783
+ initial: function () {
784
+ return this.snapshots[0];
785
+ }
786
+ };
787
+ /*
788
+ View related functions.
789
+ */
790
+
791
+ pw.view = {
792
+ // creates and returns a new pw_View for the document or node
793
+ init: function (node) {
794
+ return new pw_View(node);
795
+ },
796
+
797
+ fromStr: function (str) {
798
+ var e = document.createElement("div");
799
+ e.innerHTML = str;
800
+ return pw.view.init(e.childNodes[0]);
801
+ }
802
+ };
803
+
804
+ /*
805
+ pw_View contains a document with state. It watches for
806
+ interactions with the document that trigger mutations
807
+ in state. It can also apply state to the view.
808
+ */
809
+
810
+ var pw_View = function (node) {
811
+ this.node = node;
812
+ }
813
+
814
+ pw_View.prototype = {
815
+ clone: function () {
816
+ return pw.view.init(this.node.cloneNode(true));
817
+ },
818
+
819
+ // pakyow api
820
+
821
+ title: function (value) {
822
+ pw.node.title(this.node, value);
823
+ },
824
+
825
+ text: function (value) {
826
+ this.node.innerText = value;
827
+ },
828
+
829
+ html: function (value) {
830
+ this.node.innerHTML = value
831
+ },
832
+
833
+ component: function (name) {
834
+ return pw.collection.init(
835
+ pw.node.byAttr(this.node, 'data-ui', name).reduce(function (views, node) {
836
+ return views.concat(pw.view.init(node));
837
+ }, []), this);
838
+ },
839
+
840
+ attrs: function () {
841
+ return pw.attrs.init(this);
842
+ },
843
+
844
+ with: function (cb) {
845
+ pw.node.with(this.node, cb);
846
+ },
847
+
848
+ match: function (data) {
849
+ pw.node.match(this.node, data);
850
+ },
851
+
852
+ for: function (data, cb) {
853
+ pw.node.for(this.node, data, cb);
854
+ },
855
+
856
+ repeat: function (data, cb) {
857
+ pw.node.repeat(this.node, data, cb);
858
+ },
859
+
860
+ bind: function (data, cb) {
861
+ pw.node.bind(data, this.node, cb);
862
+ },
863
+
864
+ apply: function (data, cb) {
865
+ pw.node.apply(data, this.node, cb);
866
+ }
867
+ };
868
+
869
+ // pass through lookups
870
+ ['scope', 'prop'].forEach(function (method) {
871
+ pw_View.prototype[method] = function (name) {
872
+ return pw.collection.init(
873
+ pw.node.byAttr(this.node, 'data-' + method, name).reduce(function (views, node) {
874
+ return views.concat(pw.view.init(node));
875
+ }, []), this, name);
876
+ };
877
+ });
878
+
879
+ // pass through functions without view
880
+ ['remove', 'clear', 'versionNode'].forEach(function (method) {
881
+ pw_View.prototype[method] = function () {
882
+ return pw.node[method](this.node);
883
+ };
884
+ });
885
+
886
+ // pass through functions with view
887
+ ['after', 'before', 'replace', 'append', 'prepend', 'insert'].forEach(function (method) {
888
+ pw_View.prototype[method] = function (view) {
889
+ return pw.node[method](this.node, view.node);
890
+ };
891
+ });
892
+ pw.collection = {
893
+ init: function (view_or_views, parent, scope) {
894
+ if (view_or_views instanceof pw_Collection) {
895
+ return view_or_views
896
+ } else if (view_or_views.constructor !== Array) {
897
+ view_or_views = [view_or_views];
898
+ }
899
+
900
+ return new pw_Collection(view_or_views, parent, scope);
901
+ },
902
+
903
+ fromNodes: function (nodes, parent, scope) {
904
+ return pw.collection.init(nodes.map(function (node) {
905
+ return pw.view.init(node);
906
+ }), parent, scope);
907
+ }
908
+ };
909
+
910
+ var pw_Collection = function (views, parent, scope) {
911
+ this.views = views;
912
+ this.parent = parent;
913
+ this.scope = scope;
914
+ };
915
+
916
+ pw_Collection.prototype = {
917
+ clone: function () {
918
+ return pw.collection.init(this.views.map(function (view) {
919
+ return view.clone();
920
+ }));
921
+ },
922
+
923
+ last: function () {
924
+ return this.views[this.length() - 1];
925
+ },
926
+
927
+ first: function () {
928
+ return this.views[0];
929
+ },
930
+
931
+ removeView: function(view) {
932
+ var index = this.views.indexOf(view);
933
+
934
+ if (index > -1) {
935
+ this.views.splice(index, 1)[0].remove();
936
+ }
937
+ },
938
+
939
+ addView: function(view_or_views) {
940
+ var views = [];
941
+
942
+ if (view_or_views instanceof pw_Collection) {
943
+ views = view_or_views.views;
944
+ } else {
945
+ views.push(view_or_views);
946
+ }
947
+
948
+ if (this.length() > 0) {
949
+ views.forEach(function (view) {
950
+ pw.node.after(this.last().node, view.node);
951
+ }, this);
952
+ } else if (this.parent) {
953
+ views.forEach(function (view) {
954
+ this.parent.append(view);
955
+ }, this);
956
+ }
957
+
958
+ this.views = this.views.concat(views);
959
+ },
960
+
961
+ order: function (orderedIds) {
962
+ orderedIds.forEach(function (id) {
963
+ if (!id) {
964
+ return;
965
+ }
966
+
967
+ var match = this.views.find(function (view) {
968
+ return view.node.getAttribute('data-id') == id.toString();
969
+ });
970
+
971
+ if (match) {
972
+ match.node.parentNode.appendChild(match.node);
973
+
974
+ // also reorder the list of views
975
+ var i = this.views.indexOf(match);
976
+ this.views.splice(i, 1);
977
+ this.views.push(match);
978
+ }
979
+ }, this);
980
+ },
981
+
982
+ length: function () {
983
+ return this.views.length;
984
+ },
985
+
986
+ // pakyow api
987
+
988
+ attrs: function () {
989
+ return pw.attrs.init(this.views);
990
+ },
991
+
992
+ append: function (data) {
993
+ data = Array.ensure(data);
994
+
995
+ var last = this.last();
996
+ this.views.push(last.append(data));
997
+ return last;
998
+ },
999
+
1000
+ prepend: function(data) {
1001
+ data = Array.ensure(data);
1002
+
1003
+ var prependedViews = data.map(function (datum) {
1004
+ var view = this.first().prepend(datum);
1005
+ this.views.push(view);
1006
+ return view;
1007
+ }, this);
1008
+
1009
+ return pw.collection.init(prependedViews);
1010
+ },
1011
+
1012
+ with: function (cb) {
1013
+ pw.node.with(this.views, cb);
1014
+ },
1015
+
1016
+ for: function(data, fn) {
1017
+ data = Array.ensure(data);
1018
+
1019
+ this.views.forEach(function (view, i) {
1020
+ fn.call(view, data[i]);
1021
+ });
1022
+ },
1023
+
1024
+ match: function (data, fn) {
1025
+ data = Array.ensure(data);
1026
+
1027
+ if (data.length === 0) {
1028
+ this.remove();
1029
+ return fn.call(this);
1030
+ } else {
1031
+ var firstView;
1032
+ var firstParent;
1033
+
1034
+ if (this.views[0]) {
1035
+ firstView = this.views[0].clone();
1036
+ firstParent = this.views[0].node.parentNode;
1037
+ }
1038
+
1039
+ this.views.slice(0).forEach(function (view) {
1040
+ var id = view.node.getAttribute('data-id');
1041
+
1042
+ if (!id) {
1043
+ return;
1044
+ }
1045
+
1046
+ if (!data.find(function (datum) { return datum.id.toString() === id })) {
1047
+ this.removeView(view);
1048
+ }
1049
+ }, this);
1050
+
1051
+ if (data.length > this.length()) {
1052
+ var self = this;
1053
+ this.endpoint.template(this, function (view) {
1054
+ if (!view) {
1055
+ view = firstView.clone();
1056
+ self.parent = pw.view.init(firstParent);
1057
+ }
1058
+
1059
+ data.forEach(function (datum) {
1060
+ if (!self.views.find(function (view) {
1061
+ return view.node.getAttribute('data-id') === (datum.id || '').toString()
1062
+ })) {
1063
+ var viewToAdd = view.clone();
1064
+
1065
+ if (viewToAdd instanceof pw_Collection) {
1066
+ viewToAdd = viewToAdd.views[0];
1067
+ }
1068
+
1069
+ viewToAdd.node.setAttribute('data-id', datum.id);
1070
+ self.addView(viewToAdd);
1071
+
1072
+ pw.component.findAndInit(viewToAdd.node);
1073
+ }
1074
+ }, self);
1075
+
1076
+ return fn.call(self);
1077
+ });
1078
+ } else {
1079
+ return fn.call(this);
1080
+ }
1081
+ }
1082
+
1083
+ return this;
1084
+ },
1085
+
1086
+ repeat: function (data, fn) {
1087
+ this.match(data, function () {
1088
+ this.for(data, fn);
1089
+ });
1090
+ },
1091
+
1092
+ bind: function (data, fn) {
1093
+ this.for(data, function(datum) {
1094
+ this.bind(datum);
1095
+
1096
+ if(!(typeof fn === 'undefined')) {
1097
+ fn.call(this, datum);
1098
+ }
1099
+ });
1100
+
1101
+ return this;
1102
+ },
1103
+
1104
+ apply: function (data, fn) {
1105
+ this.match(data, function () {
1106
+ var id;
1107
+
1108
+ this.order(data.map(function (datum) {
1109
+ if (id = datum.id) {
1110
+ return id.toString();
1111
+ }
1112
+ }));
1113
+
1114
+ this.bind(data, fn);
1115
+ });
1116
+ },
1117
+
1118
+ endpoint: function (endpoint) {
1119
+ this.endpoint = endpoint;
1120
+ return this;
1121
+ }
1122
+ };
1123
+
1124
+ // lookup functions
1125
+ ['scope', 'prop', 'component'].forEach(function (method) {
1126
+ pw_Collection.prototype[method] = function (name) {
1127
+ return pw.collection.init(
1128
+ this.views.reduce(function (views, view) {
1129
+ return views.concat(view[method](name).views);
1130
+ }, [])
1131
+ );
1132
+ };
1133
+ });
1134
+
1135
+ // pass through functions
1136
+ ['remove', 'clear', 'text', 'html'].forEach(function (method) {
1137
+ pw_Collection.prototype[method] = function (arg) {
1138
+ this.views.forEach(function (view) {
1139
+ view[method](arg);
1140
+ });
1141
+ };
1142
+ });
1143
+ /*
1144
+ Component init.
1145
+ */
1146
+
1147
+ pw.init.register(function () {
1148
+ pw.component.findAndInit(document.querySelectorAll('body')[0]);
1149
+ });
1150
+
1151
+ /*
1152
+ Component related functions.
1153
+ */
1154
+
1155
+ // stores component functions by name
1156
+ var components = {};
1157
+
1158
+ // stores component instances by channel
1159
+ var channelComponents = {};
1160
+ var channelBroadcasts = {};
1161
+
1162
+ // component instances
1163
+ var componentInstances = {};
1164
+
1165
+ pw.component = {
1166
+ init: function (view, config) {
1167
+ return new pw_Component(view, config);
1168
+ },
1169
+
1170
+ resetChannels: function () {
1171
+ channelComponents = {};
1172
+ },
1173
+
1174
+ findAndInit: function (node) {
1175
+ pw.node.byAttr(node, 'data-ui').forEach(function (uiNode) {
1176
+ if (uiNode._ui) {
1177
+ return;
1178
+ }
1179
+
1180
+ var name = uiNode.getAttribute('data-ui');
1181
+ var cfn = components[name] || pw.component.init;
1182
+
1183
+ if (!componentInstances[name]) {
1184
+ componentInstances[name] = [];
1185
+ }
1186
+
1187
+ var channel = uiNode.getAttribute('data-channel');
1188
+ var config = uiNode.getAttribute('data-config');
1189
+ var view = pw.view.init(uiNode);
1190
+ var id = componentInstances[name].length;
1191
+
1192
+ var component = new cfn(view, pw.component.buildConfigObject(config), name, id);
1193
+ component.init(view, config, name);
1194
+
1195
+ pw.component.registerForChannel(component, channel);
1196
+ componentInstances[name].push(component);
1197
+
1198
+ uiNode._ui = true;
1199
+ });
1200
+ },
1201
+
1202
+ push: function (packet) {
1203
+ var channel = packet.channel;
1204
+ var payload = packet.payload;
1205
+ var instruct = payload.instruct;
1206
+
1207
+ (channelComponents[channel] || []).forEach(function (component) {
1208
+ if (instruct) {
1209
+ component.instruct(channel, instruct);
1210
+ } else {
1211
+ component.message(channel, payload);
1212
+ }
1213
+ });
1214
+ },
1215
+
1216
+ register: function (name, fn) {
1217
+ var proto = pw_Component.prototype;
1218
+
1219
+ Object.getOwnPropertyNames(proto).forEach(function (method) {
1220
+ fn.prototype[method] = proto[method];
1221
+ });
1222
+
1223
+ components[name] = fn;
1224
+ },
1225
+
1226
+ buildConfigObject: function(configString) {
1227
+ if (!configString) {
1228
+ return {};
1229
+ }
1230
+
1231
+ return configString.split(';').reduce(function (config, option) {
1232
+ var kv = option.trim().split(':');
1233
+ config[kv[0].trim()] = kv[1].trim();
1234
+ return config;
1235
+ }, {});
1236
+ },
1237
+
1238
+ registerForChannel: function (component, channel) {
1239
+ // store component instance by channel for messaging
1240
+ if (!channelComponents[channel]) {
1241
+ channelComponents[channel] = [];
1242
+ }
1243
+
1244
+ channelComponents[channel].push(component);
1245
+ },
1246
+
1247
+ registerForBroadcast: function (channel, cb, component) {
1248
+ if (!channelBroadcasts[channel]) {
1249
+ channelBroadcasts[channel] = [];
1250
+ }
1251
+
1252
+ channelBroadcasts[channel].push([cb, component]);
1253
+ },
1254
+
1255
+ deregisterForBroadcast: function (channel, component) {
1256
+ var components = channelBroadcasts[channel];
1257
+
1258
+ var instanceTuple = components.find(function (tuple) {
1259
+ return tuple[1] == component;
1260
+ });
1261
+
1262
+ var i = components.indexOf(instanceTuple);
1263
+ components.splice(i, 1);
1264
+ },
1265
+
1266
+ broadcast: function (channel, payload) {
1267
+ (channelBroadcasts[channel] || []).forEach(function (cbTuple) {
1268
+ cbTuple[0].call(cbTuple[1], payload);
1269
+ });
1270
+ }
1271
+ };
1272
+
1273
+ /*
1274
+ pw_Component makes it possible to build custom controls.
1275
+ */
1276
+
1277
+ var pw_Component = function (view, config, name) {
1278
+ // placeholder
1279
+ };
1280
+
1281
+ pw_Component.prototype = {
1282
+ init: function (view, config, name) {
1283
+ var node = view.node;
1284
+ this.view = view;
1285
+ this.node = node;
1286
+ this.config = config;
1287
+ this.name = name;
1288
+ this.templates = {};
1289
+ var self = this;
1290
+
1291
+ // setup templates
1292
+ pw.node.toA(node.querySelectorAll(':scope > *[data-template]')).forEach(function (templateNode) {
1293
+ var cloned = templateNode.cloneNode(true);
1294
+ pw.node.remove(templateNode);
1295
+
1296
+ var scope = cloned.getAttribute('data-scope');
1297
+
1298
+ if (this.templates[scope]) {
1299
+ this.templates[scope].views.push(pw.view.init(cloned));
1300
+ } else {
1301
+ this.templates[scope] = pw.collection.init(pw.view.init(cloned));
1302
+ }
1303
+
1304
+ cloned.removeAttribute('data-template');
1305
+ }, this);
1306
+
1307
+ // setup our initial state
1308
+ this.state = pw.state.init(this.node);
1309
+
1310
+ // register as a dependent to the parent component
1311
+ if (this.dCb) {
1312
+ var parentComponent = pw.node.component(this.node.parentNode);
1313
+
1314
+ if (parentComponent) {
1315
+ parentComponent.addEventListener('mutated', function (evt) {
1316
+ self.transform(self.dCb(evt.target._evtData));
1317
+ });
1318
+
1319
+ self.transform(self.dCb(pw.state.init(parentComponent).current()));
1320
+ }
1321
+ }
1322
+
1323
+ // make it mutable
1324
+ var mutableCb = function (evt) {
1325
+ evt.preventDefault();
1326
+
1327
+ var scope = pw.node.scope(evt.target);
1328
+
1329
+ if (scope) {
1330
+ self.mutated(scope);
1331
+ }
1332
+ };
1333
+
1334
+ node.addEventListener('submit', mutableCb);
1335
+ node.addEventListener('change', function (evt) {
1336
+ if (!pw.node.inForm(evt.target)) {
1337
+ mutableCb(evt);
1338
+ }
1339
+ });
1340
+
1341
+ //TODO define other mutable things
1342
+
1343
+ if (this.inited) {
1344
+ this.inited();
1345
+ }
1346
+ },
1347
+
1348
+ listen: function (channel, cb) {
1349
+ pw.component.registerForBroadcast(channel, cb, this);
1350
+ },
1351
+
1352
+ ignore: function (channel) {
1353
+ pw.component.deregisterForBroadcast(channel, this);
1354
+ },
1355
+
1356
+ //TODO this is pretty similary to processing instructions
1357
+ // for views in that we also have to handle the empty case
1358
+ //
1359
+ // there might be an opportunity for some refactoring
1360
+ instruct: function (channel, instructions) {
1361
+ this.endpoint = pw.instruct;
1362
+
1363
+ var current = this.state.current();
1364
+ if (current.length === 1) {
1365
+ var view = this.view.scope(current[0].scope);
1366
+ var node = view.views[0].node;
1367
+ if (node.getAttribute('data-version') === 'empty') {
1368
+ var self = this;
1369
+ pw.instruct.template(view, function (rview) {
1370
+ var parent = node.parentNode;
1371
+ parent.replaceChild(rview.node, node);
1372
+
1373
+ instructions.forEach(function (instruction) {
1374
+ self[instruction[0]](instruction[1]);
1375
+ });
1376
+ });
1377
+
1378
+ return;
1379
+ }
1380
+ }
1381
+
1382
+ instructions.forEach(function (instruction) {
1383
+ this[instruction[0]](instruction[1]);
1384
+ }, this);
1385
+ },
1386
+
1387
+ message: function (channel, payload) {
1388
+ // placeholder
1389
+ },
1390
+
1391
+ mutated: function (node) {
1392
+ this.mutation(this.state.diffNode(node));
1393
+ this.state.update();
1394
+
1395
+ pw.node.trigger('mutated', this.node, this.state.current());
1396
+ },
1397
+
1398
+ mutation: function (mutation) {
1399
+ // placeholder
1400
+ },
1401
+
1402
+ transform: function (state) {
1403
+ this._transform(state);
1404
+ },
1405
+
1406
+ _transform: function (state) {
1407
+ if (!state) {
1408
+ return;
1409
+ }
1410
+
1411
+ if (state.length > 0) {
1412
+ this.view.scope(state[0].scope).endpoint(this.endpoint || this).apply(state);
1413
+ } else {
1414
+ pw.node.breadthFirst(this.view.node, function () {
1415
+ if (this.hasAttribute('data-scope')) {
1416
+ pw.node.remove(this);
1417
+ }
1418
+ });
1419
+ }
1420
+
1421
+ pw.node.trigger('mutated', this.node, this.state.current());
1422
+ },
1423
+
1424
+ revert: function () {
1425
+ this.transform(this.state.revert());
1426
+ },
1427
+
1428
+ rollback: function () {
1429
+ this.transform(this.state.rollback());
1430
+ },
1431
+
1432
+ template: function (view, cb) {
1433
+ var template;
1434
+
1435
+ if (template = this.templates[view.scope]) {
1436
+ cb(template);
1437
+ }
1438
+ },
1439
+
1440
+ delete: function (data) {
1441
+ this.state.delete(data);
1442
+ this.transform(this.state.current());
1443
+ },
1444
+
1445
+ append: function (data) {
1446
+ this.state.append(data);
1447
+ this.transform(this.state.current());
1448
+ },
1449
+
1450
+ prepend: function (data) {
1451
+ this.state.prepend(data);
1452
+ this.transform(this.state.current());
1453
+ },
1454
+
1455
+ parent: function () {
1456
+ var parent = pw.node.scope(this.node);
1457
+
1458
+ if (parent) {
1459
+ return pw.state.init(parent).current()[0];
1460
+ }
1461
+ },
1462
+
1463
+ dependent: function (cb) {
1464
+ this.dCb = cb;
1465
+ }
1466
+ };
1467
+ /*
1468
+ Socket init.
1469
+ */
1470
+
1471
+ pw.init.register(function () {
1472
+ pw.socket.init({
1473
+ cb: function (socket) {
1474
+ window.socket = socket;
1475
+ }
1476
+ });
1477
+ });
1478
+
1479
+ /*
1480
+ Socket related functions.
1481
+ */
1482
+
1483
+ pw.socket = {
1484
+ init: function (options) {
1485
+ return pw.socket.connect(
1486
+ options.host,
1487
+ options.port,
1488
+ options.protocol,
1489
+ options.connId,
1490
+ options.cb
1491
+ );
1492
+ },
1493
+
1494
+ connect: function (host, port, protocol, connId, cb) {
1495
+ if(typeof host === 'undefined') host = window.location.hostname;
1496
+ if(typeof port === 'undefined') port = window.location.port;
1497
+ if(typeof protocol === 'undefined') protocol = window.location.protocol;
1498
+ if(typeof connId === 'undefined') connId = document.getElementsByTagName('body')[0].getAttribute('data-socket-connection-id');
1499
+
1500
+ if (!connId) {
1501
+ return;
1502
+ }
1503
+
1504
+ var wsUrl = '';
1505
+
1506
+ if (protocol === 'http:') {
1507
+ wsUrl += 'ws://';
1508
+ } else if (protocol === 'https:') {
1509
+ wsUrl += 'wss://';
1510
+ }
1511
+
1512
+ wsUrl += host;
1513
+
1514
+ if (port) {
1515
+ wsUrl += ':' + port;
1516
+ }
1517
+
1518
+ wsUrl += '/?socket_connection_id=' + connId;
1519
+
1520
+ return new pw_Socket(wsUrl, cb);
1521
+ }
1522
+ };
1523
+
1524
+ var pw_Socket = function (url, cb) {
1525
+ var self = this;
1526
+
1527
+ this.callbacks = {};
1528
+
1529
+ this.url = url;
1530
+ this.initCb = cb;
1531
+
1532
+ this.ws = new WebSocket(url);
1533
+
1534
+ this.id = url.split('socket_connection_id=')[1]
1535
+
1536
+ this.ws.onmessage = function (evt) {
1537
+ pw.component.broadcast('socket:loaded');
1538
+
1539
+ var data = JSON.parse(evt.data);
1540
+ if (data.id) {
1541
+ var cb = self.callbacks[data.id];
1542
+ if (cb) {
1543
+ cb.call(this, data);
1544
+ return;
1545
+ }
1546
+ }
1547
+
1548
+ self.message(data);
1549
+ };
1550
+
1551
+ this.ws.onclose = function (evt) {
1552
+ console.log('socket closed');
1553
+ self.reconnect();
1554
+ };
1555
+
1556
+ this.ws.onopen = function (evt) {
1557
+ console.log('socket open');
1558
+
1559
+ if(self.initCb) {
1560
+ self.initCb(self);
1561
+ }
1562
+ }
1563
+ };
1564
+
1565
+ pw_Socket.prototype = {
1566
+ send: function (message, cb) {
1567
+ pw.component.broadcast('socket:loading');
1568
+
1569
+ message.id = pw.util.guid();
1570
+ if (!message.input) {
1571
+ message.input = {};
1572
+ }
1573
+ message.input.socket_connection_id = this.id;
1574
+ this.callbacks[message.id] = cb;
1575
+ this.ws.send(JSON.stringify(message));
1576
+ },
1577
+
1578
+ //TODO handle custom messages (e.g. not pakyow specific)
1579
+ message: function (packet) {
1580
+ console.log('received message');
1581
+ console.log(packet);
1582
+
1583
+ var selector = '*[data-channel="' + packet.channel + '"]';
1584
+
1585
+ if (packet.channel.split(':')[0] === 'component') {
1586
+ pw.component.push(packet);
1587
+ return;
1588
+ }
1589
+
1590
+ var nodes = pw.node.toA(document.querySelectorAll(selector));
1591
+
1592
+ if (nodes.length === 0) {
1593
+ //TODO decide how to handle this condition; there are times where this
1594
+ // is going to be the case and not an error; at one point we were simply
1595
+ // reloading the page, but that doesn't work in all cases
1596
+ return;
1597
+ }
1598
+
1599
+ pw.instruct.process(pw.collection.fromNodes(nodes, selector), packet, this);
1600
+ },
1601
+
1602
+ reconnect: function () {
1603
+ var self = this;
1604
+
1605
+ if (!self.wait) {
1606
+ self.wait = 100;
1607
+ } else {
1608
+ self.wait *= 1.25;
1609
+ }
1610
+
1611
+ console.log('reconnecting socket in ' + self.wait + 'ms');
1612
+
1613
+ setTimeout(function () {
1614
+ pw.socket.init({ cb: self.initCb });
1615
+ }, self.wait);
1616
+ },
1617
+
1618
+ fetchView: function (lookup, cb) {
1619
+ var uri;
1620
+
1621
+ if (window.location.hash) {
1622
+ var arr = window.location.hash.split('#:')[1].split('/');
1623
+ arr.shift();
1624
+ uri = arr.join('/');
1625
+ } else {
1626
+ uri = window.location.pathname + window.location.search;
1627
+ }
1628
+
1629
+ this.send({
1630
+ action: 'fetch-view',
1631
+ lookup: lookup,
1632
+ uri: uri
1633
+ }, function (res) {
1634
+ var view = pw.view.fromStr(res.body);
1635
+
1636
+ if (view.node) {
1637
+ view.node.removeAttribute('data-id');
1638
+ cb(view);
1639
+ } else {
1640
+ cb();
1641
+ }
1642
+ });
1643
+ }
1644
+ };
1645
+ pw.instruct = {
1646
+ process: function (collection, packet, socket) {
1647
+ if (collection.length() === 1 && collection.views[0].node.getAttribute('data-version') === 'empty') {
1648
+ pw.instruct.fetchView(packet, socket, collection.views[0].node);
1649
+ } else {
1650
+ pw.instruct.perform(collection, packet.payload);
1651
+ }
1652
+ },
1653
+
1654
+ fetchView: function (packet, socket, node) {
1655
+ socket.fetchView({ channel: packet.channel }, function (view) {
1656
+ if (view) {
1657
+ var parent = node.parentNode;
1658
+ parent.replaceChild(view.node, node);
1659
+
1660
+ var selector = '*[data-channel="' + packet.channel + '"]';
1661
+ var nodes = pw.node.toA(parent.querySelectorAll(selector));
1662
+ pw.instruct.perform(pw.collection.fromNodes(nodes, selector), packet.payload);
1663
+ } else {
1664
+ console.log('trouble fetching view :(');
1665
+ }
1666
+ });
1667
+ },
1668
+
1669
+ // TODO: make this smart and cache results
1670
+ template: function (view, cb) {
1671
+ var lookup = {};
1672
+
1673
+ if (!view || !view.first()) {
1674
+ return cb();
1675
+ }
1676
+
1677
+ var node = view.first().node;
1678
+
1679
+ if (node.hasAttribute('data-channel')) {
1680
+ lookup.channel = view.first().node.getAttribute('data-channel');
1681
+ } else if (node.hasAttribute('data-ui') && node.hasAttribute('data-scope')) {
1682
+ lookup.component = pw.node.component(node).getAttribute('data-ui');
1683
+ lookup.scope = node.getAttribute('data-scope');
1684
+ } else {
1685
+ cb();
1686
+ return;
1687
+ }
1688
+
1689
+ window.socket.fetchView(lookup, function (view) {
1690
+ cb(view);
1691
+ });
1692
+ },
1693
+
1694
+ perform: function (collection, instructions) {
1695
+ var self = this;
1696
+
1697
+ (instructions || []).forEach(function (instruction, i) {
1698
+ var method = instruction[0];
1699
+ var value = instruction[1];
1700
+ var nested = instruction[2];
1701
+
1702
+ if (collection[method]) {
1703
+ if (method == 'with' || method == 'for' || method == 'bind' || method == 'repeat' || method == 'apply') {
1704
+ collection.endpoint(self)[method].call(collection, value, function (datum) {
1705
+ pw.instruct.perform(this, nested[value.indexOf(datum)]);
1706
+ });
1707
+ return;
1708
+ } else if (method == 'attrs') {
1709
+ self.performAttr(collection.attrs(), nested);
1710
+ return;
1711
+ } else {
1712
+ var mutatedViews = collection[method].call(collection, value);
1713
+ }
1714
+ } else {
1715
+ console.log('could not find method named: ' + method);
1716
+ return;
1717
+ }
1718
+
1719
+ if (nested instanceof Array) {
1720
+ pw.instruct.perform(mutatedViews, nested);
1721
+ } else if (mutatedViews) {
1722
+ collection = mutatedViews;
1723
+ }
1724
+ });
1725
+
1726
+ pw.component.findAndInit(collection.node);
1727
+ },
1728
+
1729
+ performAttr: function (context, attrInstructions) {
1730
+ attrInstructions.forEach(function (attrInstruct) {
1731
+ var attr = attrInstruct[0];
1732
+ var value = attrInstruct[1];
1733
+ var nested = attrInstruct[2];
1734
+
1735
+ if (value) {
1736
+ context.set(attr, value);
1737
+ } else {
1738
+ context[nested[0][0]](attr, nested[0][1]);
1739
+ }
1740
+ });
1741
+ }
1742
+ };
1743
+ if (!Array.prototype.flatten) {
1744
+ Array.prototype.flatten = function () {
1745
+ return this.reduce(function (flat, toFlatten) {
1746
+ return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten);
1747
+ }, []);
1748
+ };
1749
+ }
1750
+
1751
+ if (!Array.prototype.find) {
1752
+ Array.prototype.find = function(predicate) {
1753
+ if (this == null) {
1754
+ throw new TypeError('Array.prototype.find called on null or undefined');
1755
+ }
1756
+ if (typeof predicate !== 'function') {
1757
+ throw new TypeError('predicate must be a function');
1758
+ }
1759
+ var list = Object(this);
1760
+ var length = list.length >>> 0;
1761
+ var thisArg = arguments[1];
1762
+ var value;
1763
+
1764
+ for (var i = 0; i < length; i++) {
1765
+ value = list[i];
1766
+ if (predicate.call(thisArg, value, i, list)) {
1767
+ return value;
1768
+ }
1769
+ }
1770
+ return undefined;
1771
+ };
1772
+ }
1773
+
1774
+ Array.ensure = function (value) {
1775
+ if(!(value instanceof Array)) {
1776
+ return [value];
1777
+ }
1778
+
1779
+ return value
1780
+ }
1781
+
1782
+ NodeList.prototype.forEach = Array.prototype.forEach;
1783
+ if (!Object.prototype.pairs) {
1784
+ Object.defineProperty(Object.prototype, "pairs", {
1785
+ value: function() {
1786
+ return Object.keys(this).map(function (key) {
1787
+ return [key, this[key]];
1788
+ }, this);
1789
+ },
1790
+ enumerable: false
1791
+ });
1792
+ }
1793
+
1794
+ if (typeof define === "function" && define.amd) {
1795
+ define(pw);
1796
+ } else if (typeof module === "object" && module.exports) {
1797
+ module.exports = pw;
1798
+ } else {
1799
+ this.pw = pw;
1800
+ }
1801
+ })();