pakyow 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ })();