瀏覽代碼

Add routing via page.js

crobi 9 年之前
父節點
當前提交
1685a0175d
共有 7 個文件被更改,包括 1318 次插入121 次删除
  1. 1
    0
      client/index.html
  2. 922
    0
      client/js/page.js
  3. 76
    119
      client/src/app.ts
  4. 216
    0
      client/src/external/page/page.d.ts
  5. 90
    0
      client/src/mock.ts
  6. 8
    0
      client/src/views/view.ts
  7. 5
    2
      client/tsconfig.json

+ 1
- 0
client/index.html 查看文件

@@ -10,6 +10,7 @@
10 10
       <div id="cardeditor"></div>
11 11
       <!-- libraries -->
12 12
       <script src="js/react.js"></script>
13
+      <script src="js/page.js"></script>
13 14
 
14 15
       <!-- teh app -->
15 16
       <script src="js/app.js"></script>

+ 922
- 0
client/js/page.js 查看文件

@@ -0,0 +1,922 @@
1
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.page=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
2
+(function (process){
3
+  /* globals require, module */
4
+
5
+  'use strict';
6
+
7
+  /**
8
+   * Module dependencies.
9
+   */
10
+
11
+  var pathtoRegexp = require('path-to-regexp');
12
+
13
+  /**
14
+   * Module exports.
15
+   */
16
+
17
+  module.exports = page;
18
+
19
+  /**
20
+   * Detect click event
21
+   */
22
+  var clickEvent = ('undefined' !== typeof document) && document.ontouchstart ? 'touchstart' : 'click';
23
+
24
+  /**
25
+   * To work properly with the URL
26
+   * history.location generated polyfill in https://github.com/devote/HTML5-History-API
27
+   */
28
+
29
+  var location = ('undefined' !== typeof window) && (window.history.location || window.location);
30
+
31
+  /**
32
+   * Perform initial dispatch.
33
+   */
34
+
35
+  var dispatch = true;
36
+
37
+
38
+  /**
39
+   * Decode URL components (query string, pathname, hash).
40
+   * Accommodates both regular percent encoding and x-www-form-urlencoded format.
41
+   */
42
+  var decodeURLComponents = true;
43
+
44
+  /**
45
+   * Base path.
46
+   */
47
+
48
+  var base = '';
49
+
50
+  /**
51
+   * Running flag.
52
+   */
53
+
54
+  var running;
55
+
56
+  /**
57
+   * HashBang option
58
+   */
59
+
60
+  var hashbang = false;
61
+
62
+  /**
63
+   * Previous context, for capturing
64
+   * page exit events.
65
+   */
66
+
67
+  var prevContext;
68
+
69
+  /**
70
+   * Register `path` with callback `fn()`,
71
+   * or route `path`, or redirection,
72
+   * or `page.start()`.
73
+   *
74
+   *   page(fn);
75
+   *   page('*', fn);
76
+   *   page('/user/:id', load, user);
77
+   *   page('/user/' + user.id, { some: 'thing' });
78
+   *   page('/user/' + user.id);
79
+   *   page('/from', '/to')
80
+   *   page();
81
+   *
82
+   * @param {String|Function} path
83
+   * @param {Function} fn...
84
+   * @api public
85
+   */
86
+
87
+  function page(path, fn) {
88
+    // <callback>
89
+    if ('function' === typeof path) {
90
+      return page('*', path);
91
+    }
92
+
93
+    // route <path> to <callback ...>
94
+    if ('function' === typeof fn) {
95
+      var route = new Route(path);
96
+      for (var i = 1; i < arguments.length; ++i) {
97
+        page.callbacks.push(route.middleware(arguments[i]));
98
+      }
99
+      // show <path> with [state]
100
+    } else if ('string' === typeof path) {
101
+      page['string' === typeof fn ? 'redirect' : 'show'](path, fn);
102
+      // start [options]
103
+    } else {
104
+      page.start(path);
105
+    }
106
+  }
107
+
108
+  /**
109
+   * Callback functions.
110
+   */
111
+
112
+  page.callbacks = [];
113
+  page.exits = [];
114
+
115
+  /**
116
+   * Current path being processed
117
+   * @type {String}
118
+   */
119
+  page.current = '';
120
+
121
+  /**
122
+   * Number of pages navigated to.
123
+   * @type {number}
124
+   *
125
+   *     page.len == 0;
126
+   *     page('/login');
127
+   *     page.len == 1;
128
+   */
129
+
130
+  page.len = 0;
131
+
132
+  /**
133
+   * Get or set basepath to `path`.
134
+   *
135
+   * @param {String} path
136
+   * @api public
137
+   */
138
+
139
+  page.base = function(path) {
140
+    if (0 === arguments.length) return base;
141
+    base = path;
142
+  };
143
+
144
+  /**
145
+   * Bind with the given `options`.
146
+   *
147
+   * Options:
148
+   *
149
+   *    - `click` bind to click events [true]
150
+   *    - `popstate` bind to popstate [true]
151
+   *    - `dispatch` perform initial dispatch [true]
152
+   *
153
+   * @param {Object} options
154
+   * @api public
155
+   */
156
+
157
+  page.start = function(options) {
158
+    options = options || {};
159
+    if (running) return;
160
+    running = true;
161
+    if (false === options.dispatch) dispatch = false;
162
+    if (false === options.decodeURLComponents) decodeURLComponents = false;
163
+    if (false !== options.popstate) window.addEventListener('popstate', onpopstate, false);
164
+    if (false !== options.click) {
165
+      document.addEventListener(clickEvent, onclick, false);
166
+    }
167
+    if (true === options.hashbang) hashbang = true;
168
+    if (!dispatch) return;
169
+    var url = (hashbang && ~location.hash.indexOf('#!')) ? location.hash.substr(2) + location.search : location.pathname + location.search + location.hash;
170
+    page.replace(url, null, true, dispatch);
171
+  };
172
+
173
+  /**
174
+   * Unbind click and popstate event handlers.
175
+   *
176
+   * @api public
177
+   */
178
+
179
+  page.stop = function() {
180
+    if (!running) return;
181
+    page.current = '';
182
+    page.len = 0;
183
+    running = false;
184
+    document.removeEventListener(clickEvent, onclick, false);
185
+    window.removeEventListener('popstate', onpopstate, false);
186
+  };
187
+
188
+  /**
189
+   * Show `path` with optional `state` object.
190
+   *
191
+   * @param {String} path
192
+   * @param {Object} state
193
+   * @param {Boolean} dispatch
194
+   * @return {Context}
195
+   * @api public
196
+   */
197
+
198
+  page.show = function(path, state, dispatch, push) {
199
+    var ctx = new Context(path, state);
200
+    page.current = ctx.path;
201
+    if (false !== dispatch) page.dispatch(ctx);
202
+    if (false !== ctx.handled && false !== push) ctx.pushState();
203
+    return ctx;
204
+  };
205
+
206
+  /**
207
+   * Goes back in the history
208
+   * Back should always let the current route push state and then go back.
209
+   *
210
+   * @param {String} path - fallback path to go back if no more history exists, if undefined defaults to page.base
211
+   * @param {Object} [state]
212
+   * @api public
213
+   */
214
+
215
+  page.back = function(path, state) {
216
+    if (page.len > 0) {
217
+      // this may need more testing to see if all browsers
218
+      // wait for the next tick to go back in history
219
+      history.back();
220
+      page.len--;
221
+    } else if (path) {
222
+      setTimeout(function() {
223
+        page.show(path, state);
224
+      });
225
+    }else{
226
+      setTimeout(function() {
227
+        page.show(base, state);
228
+      });
229
+    }
230
+  };
231
+
232
+
233
+  /**
234
+   * Register route to redirect from one path to other
235
+   * or just redirect to another route
236
+   *
237
+   * @param {String} from - if param 'to' is undefined redirects to 'from'
238
+   * @param {String} [to]
239
+   * @api public
240
+   */
241
+  page.redirect = function(from, to) {
242
+    // Define route from a path to another
243
+    if ('string' === typeof from && 'string' === typeof to) {
244
+      page(from, function(e) {
245
+        setTimeout(function() {
246
+          page.replace(to);
247
+        }, 0);
248
+      });
249
+    }
250
+
251
+    // Wait for the push state and replace it with another
252
+    if ('string' === typeof from && 'undefined' === typeof to) {
253
+      setTimeout(function() {
254
+        page.replace(from);
255
+      }, 0);
256
+    }
257
+  };
258
+
259
+  /**
260
+   * Replace `path` with optional `state` object.
261
+   *
262
+   * @param {String} path
263
+   * @param {Object} state
264
+   * @return {Context}
265
+   * @api public
266
+   */
267
+
268
+
269
+  page.replace = function(path, state, init, dispatch) {
270
+    var ctx = new Context(path, state);
271
+    page.current = ctx.path;
272
+    ctx.init = init;
273
+    ctx.save(); // save before dispatching, which may redirect
274
+    if (false !== dispatch) page.dispatch(ctx);
275
+    return ctx;
276
+  };
277
+
278
+  /**
279
+   * Dispatch the given `ctx`.
280
+   *
281
+   * @param {Object} ctx
282
+   * @api private
283
+   */
284
+
285
+  page.dispatch = function(ctx) {
286
+    var prev = prevContext,
287
+      i = 0,
288
+      j = 0;
289
+
290
+    prevContext = ctx;
291
+
292
+    function nextExit() {
293
+      var fn = page.exits[j++];
294
+      if (!fn) return nextEnter();
295
+      fn(prev, nextExit);
296
+    }
297
+
298
+    function nextEnter() {
299
+      var fn = page.callbacks[i++];
300
+
301
+      if (ctx.path !== page.current) {
302
+        ctx.handled = false;
303
+        return;
304
+      }
305
+      if (!fn) return unhandled(ctx);
306
+      fn(ctx, nextEnter);
307
+    }
308
+
309
+    if (prev) {
310
+      nextExit();
311
+    } else {
312
+      nextEnter();
313
+    }
314
+  };
315
+
316
+  /**
317
+   * Unhandled `ctx`. When it's not the initial
318
+   * popstate then redirect. If you wish to handle
319
+   * 404s on your own use `page('*', callback)`.
320
+   *
321
+   * @param {Context} ctx
322
+   * @api private
323
+   */
324
+
325
+  function unhandled(ctx) {
326
+    if (ctx.handled) return;
327
+    var current;
328
+
329
+    if (hashbang) {
330
+      current = base + location.hash.replace('#!', '');
331
+    } else {
332
+      current = location.pathname + location.search;
333
+    }
334
+
335
+    if (current === ctx.canonicalPath) return;
336
+    page.stop();
337
+    ctx.handled = false;
338
+    location.href = ctx.canonicalPath;
339
+  }
340
+
341
+  /**
342
+   * Register an exit route on `path` with
343
+   * callback `fn()`, which will be called
344
+   * on the previous context when a new
345
+   * page is visited.
346
+   */
347
+  page.exit = function(path, fn) {
348
+    if (typeof path === 'function') {
349
+      return page.exit('*', path);
350
+    }
351
+
352
+    var route = new Route(path);
353
+    for (var i = 1; i < arguments.length; ++i) {
354
+      page.exits.push(route.middleware(arguments[i]));
355
+    }
356
+  };
357
+
358
+  /**
359
+   * Remove URL encoding from the given `str`.
360
+   * Accommodates whitespace in both x-www-form-urlencoded
361
+   * and regular percent-encoded form.
362
+   *
363
+   * @param {str} URL component to decode
364
+   */
365
+  function decodeURLEncodedURIComponent(val) {
366
+    if (typeof val !== 'string') { return val; }
367
+    return decodeURLComponents ? decodeURIComponent(val.replace(/\+/g, ' ')) : val;
368
+  }
369
+
370
+  /**
371
+   * Initialize a new "request" `Context`
372
+   * with the given `path` and optional initial `state`.
373
+   *
374
+   * @param {String} path
375
+   * @param {Object} state
376
+   * @api public
377
+   */
378
+
379
+  function Context(path, state) {
380
+    if ('/' === path[0] && 0 !== path.indexOf(base)) path = base + (hashbang ? '#!' : '') + path;
381
+    var i = path.indexOf('?');
382
+
383
+    this.canonicalPath = path;
384
+    this.path = path.replace(base, '') || '/';
385
+    if (hashbang) this.path = this.path.replace('#!', '') || '/';
386
+
387
+    this.title = document.title;
388
+    this.state = state || {};
389
+    this.state.path = path;
390
+    this.querystring = ~i ? decodeURLEncodedURIComponent(path.slice(i + 1)) : '';
391
+    this.pathname = decodeURLEncodedURIComponent(~i ? path.slice(0, i) : path);
392
+    this.params = {};
393
+
394
+    // fragment
395
+    this.hash = '';
396
+    if (!hashbang) {
397
+      if (!~this.path.indexOf('#')) return;
398
+      var parts = this.path.split('#');
399
+      this.path = parts[0];
400
+      this.hash = decodeURLEncodedURIComponent(parts[1]) || '';
401
+      this.querystring = this.querystring.split('#')[0];
402
+    }
403
+  }
404
+
405
+  /**
406
+   * Expose `Context`.
407
+   */
408
+
409
+  page.Context = Context;
410
+
411
+  /**
412
+   * Push state.
413
+   *
414
+   * @api private
415
+   */
416
+
417
+  Context.prototype.pushState = function() {
418
+    page.len++;
419
+    history.pushState(this.state, this.title, hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
420
+  };
421
+
422
+  /**
423
+   * Save the context state.
424
+   *
425
+   * @api public
426
+   */
427
+
428
+  Context.prototype.save = function() {
429
+    history.replaceState(this.state, this.title, hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
430
+  };
431
+
432
+  /**
433
+   * Initialize `Route` with the given HTTP `path`,
434
+   * and an array of `callbacks` and `options`.
435
+   *
436
+   * Options:
437
+   *
438
+   *   - `sensitive`    enable case-sensitive routes
439
+   *   - `strict`       enable strict matching for trailing slashes
440
+   *
441
+   * @param {String} path
442
+   * @param {Object} options.
443
+   * @api private
444
+   */
445
+
446
+  function Route(path, options) {
447
+    options = options || {};
448
+    this.path = (path === '*') ? '(.*)' : path;
449
+    this.method = 'GET';
450
+    this.regexp = pathtoRegexp(this.path,
451
+      this.keys = [],
452
+      options.sensitive,
453
+      options.strict);
454
+  }
455
+
456
+  /**
457
+   * Expose `Route`.
458
+   */
459
+
460
+  page.Route = Route;
461
+
462
+  /**
463
+   * Return route middleware with
464
+   * the given callback `fn()`.
465
+   *
466
+   * @param {Function} fn
467
+   * @return {Function}
468
+   * @api public
469
+   */
470
+
471
+  Route.prototype.middleware = function(fn) {
472
+    var self = this;
473
+    return function(ctx, next) {
474
+      if (self.match(ctx.path, ctx.params)) return fn(ctx, next);
475
+      next();
476
+    };
477
+  };
478
+
479
+  /**
480
+   * Check if this route matches `path`, if so
481
+   * populate `params`.
482
+   *
483
+   * @param {String} path
484
+   * @param {Object} params
485
+   * @return {Boolean}
486
+   * @api private
487
+   */
488
+
489
+  Route.prototype.match = function(path, params) {
490
+    var keys = this.keys,
491
+      qsIndex = path.indexOf('?'),
492
+      pathname = ~qsIndex ? path.slice(0, qsIndex) : path,
493
+      m = this.regexp.exec(decodeURIComponent(pathname));
494
+
495
+    if (!m) return false;
496
+
497
+    for (var i = 1, len = m.length; i < len; ++i) {
498
+      var key = keys[i - 1];
499
+      var val = decodeURLEncodedURIComponent(m[i]);
500
+      if (val !== undefined || !(hasOwnProperty.call(params, key.name))) {
501
+        params[key.name] = val;
502
+      }
503
+    }
504
+
505
+    return true;
506
+  };
507
+
508
+
509
+  /**
510
+   * Handle "populate" events.
511
+   */
512
+
513
+  var onpopstate = (function () {
514
+    var loaded = false;
515
+    if ('undefined' === typeof window) {
516
+      return;
517
+    }
518
+    if (document.readyState === 'complete') {
519
+      loaded = true;
520
+    } else {
521
+      window.addEventListener('load', function() {
522
+        setTimeout(function() {
523
+          loaded = true;
524
+        }, 0);
525
+      });
526
+    }
527
+    return function onpopstate(e) {
528
+      if (!loaded) return;
529
+      if (e.state) {
530
+        var path = e.state.path;
531
+        page.replace(path, e.state);
532
+      } else {
533
+        page.show(location.pathname + location.hash, undefined, undefined, false);
534
+      }
535
+    };
536
+  })();
537
+  /**
538
+   * Handle "click" events.
539
+   */
540
+
541
+  function onclick(e) {
542
+
543
+    if (1 !== which(e)) return;
544
+
545
+    if (e.metaKey || e.ctrlKey || e.shiftKey) return;
546
+    if (e.defaultPrevented) return;
547
+
548
+
549
+
550
+    // ensure link
551
+    var el = e.target;
552
+    while (el && 'A' !== el.nodeName) el = el.parentNode;
553
+    if (!el || 'A' !== el.nodeName) return;
554
+
555
+
556
+
557
+    // Ignore if tag has
558
+    // 1. "download" attribute
559
+    // 2. rel="external" attribute
560
+    if (el.hasAttribute('download') || el.getAttribute('rel') === 'external') return;
561
+
562
+    // ensure non-hash for the same path
563
+    var link = el.getAttribute('href');
564
+    if (!hashbang && el.pathname === location.pathname && (el.hash || '#' === link)) return;
565
+
566
+
567
+
568
+    // Check for mailto: in the href
569
+    if (link && link.indexOf('mailto:') > -1) return;
570
+
571
+    // check target
572
+    if (el.target) return;
573
+
574
+    // x-origin
575
+    if (!sameOrigin(el.href)) return;
576
+
577
+
578
+
579
+    // rebuild path
580
+    var path = el.pathname + el.search + (el.hash || '');
581
+
582
+    // strip leading "/[drive letter]:" on NW.js on Windows
583
+    if (typeof process !== 'undefined' && path.match(/^\/[a-zA-Z]:\//)) {
584
+      path = path.replace(/^\/[a-zA-Z]:\//, '/');
585
+    }
586
+
587
+    // same page
588
+    var orig = path;
589
+
590
+    if (path.indexOf(base) === 0) {
591
+      path = path.substr(base.length);
592
+    }
593
+
594
+    if (hashbang) path = path.replace('#!', '');
595
+
596
+    if (base && orig === path) return;
597
+
598
+    e.preventDefault();
599
+    page.show(orig);
600
+  }
601
+
602
+  /**
603
+   * Event button.
604
+   */
605
+
606
+  function which(e) {
607
+    e = e || window.event;
608
+    return null === e.which ? e.button : e.which;
609
+  }
610
+
611
+  /**
612
+   * Check if `href` is the same origin.
613
+   */
614
+
615
+  function sameOrigin(href) {
616
+    var origin = location.protocol + '//' + location.hostname;
617
+    if (location.port) origin += ':' + location.port;
618
+    return (href && (0 === href.indexOf(origin)));
619
+  }
620
+
621
+  page.sameOrigin = sameOrigin;
622
+
623
+}).call(this,require('_process'))
624
+},{"_process":2,"path-to-regexp":3}],2:[function(require,module,exports){
625
+// shim for using process in browser
626
+
627
+var process = module.exports = {};
628
+
629
+process.nextTick = (function () {
630
+    var canSetImmediate = typeof window !== 'undefined'
631
+    && window.setImmediate;
632
+    var canMutationObserver = typeof window !== 'undefined'
633
+    && window.MutationObserver;
634
+    var canPost = typeof window !== 'undefined'
635
+    && window.postMessage && window.addEventListener
636
+    ;
637
+
638
+    if (canSetImmediate) {
639
+        return function (f) { return window.setImmediate(f) };
640
+    }
641
+
642
+    var queue = [];
643
+
644
+    if (canMutationObserver) {
645
+        var hiddenDiv = document.createElement("div");
646
+        var observer = new MutationObserver(function () {
647
+            var queueList = queue.slice();
648
+            queue.length = 0;
649
+            queueList.forEach(function (fn) {
650
+                fn();
651
+            });
652
+        });
653
+
654
+        observer.observe(hiddenDiv, { attributes: true });
655
+
656
+        return function nextTick(fn) {
657
+            if (!queue.length) {
658
+                hiddenDiv.setAttribute('yes', 'no');
659
+            }
660
+            queue.push(fn);
661
+        };
662
+    }
663
+
664
+    if (canPost) {
665
+        window.addEventListener('message', function (ev) {
666
+            var source = ev.source;
667
+            if ((source === window || source === null) && ev.data === 'process-tick') {
668
+                ev.stopPropagation();
669
+                if (queue.length > 0) {
670
+                    var fn = queue.shift();
671
+                    fn();
672
+                }
673
+            }
674
+        }, true);
675
+
676
+        return function nextTick(fn) {
677
+            queue.push(fn);
678
+            window.postMessage('process-tick', '*');
679
+        };
680
+    }
681
+
682
+    return function nextTick(fn) {
683
+        setTimeout(fn, 0);
684
+    };
685
+})();
686
+
687
+process.title = 'browser';
688
+process.browser = true;
689
+process.env = {};
690
+process.argv = [];
691
+
692
+function noop() {}
693
+
694
+process.on = noop;
695
+process.addListener = noop;
696
+process.once = noop;
697
+process.off = noop;
698
+process.removeListener = noop;
699
+process.removeAllListeners = noop;
700
+process.emit = noop;
701
+
702
+process.binding = function (name) {
703
+    throw new Error('process.binding is not supported');
704
+};
705
+
706
+// TODO(shtylman)
707
+process.cwd = function () { return '/' };
708
+process.chdir = function (dir) {
709
+    throw new Error('process.chdir is not supported');
710
+};
711
+
712
+},{}],3:[function(require,module,exports){
713
+var isArray = require('isarray');
714
+
715
+/**
716
+ * Expose `pathToRegexp`.
717
+ */
718
+module.exports = pathToRegexp;
719
+
720
+/**
721
+ * The main path matching regexp utility.
722
+ *
723
+ * @type {RegExp}
724
+ */
725
+var PATH_REGEXP = new RegExp([
726
+  // Match escaped characters that would otherwise appear in future matches.
727
+  // This allows the user to escape special characters that won't transform.
728
+  '(\\\\.)',
729
+  // Match Express-style parameters and un-named parameters with a prefix
730
+  // and optional suffixes. Matches appear as:
731
+  //
732
+  // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
733
+  // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
734
+  '([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?',
735
+  // Match regexp special characters that are always escaped.
736
+  '([.+*?=^!:${}()[\\]|\\/])'
737
+].join('|'), 'g');
738
+
739
+/**
740
+ * Escape the capturing group by escaping special characters and meaning.
741
+ *
742
+ * @param  {String} group
743
+ * @return {String}
744
+ */
745
+function escapeGroup (group) {
746
+  return group.replace(/([=!:$\/()])/g, '\\$1');
747
+}
748
+
749
+/**
750
+ * Attach the keys as a property of the regexp.
751
+ *
752
+ * @param  {RegExp} re
753
+ * @param  {Array}  keys
754
+ * @return {RegExp}
755
+ */
756
+function attachKeys (re, keys) {
757
+  re.keys = keys;
758
+  return re;
759
+}
760
+
761
+/**
762
+ * Get the flags for a regexp from the options.
763
+ *
764
+ * @param  {Object} options
765
+ * @return {String}
766
+ */
767
+function flags (options) {
768
+  return options.sensitive ? '' : 'i';
769
+}
770
+
771
+/**
772
+ * Pull out keys from a regexp.
773
+ *
774
+ * @param  {RegExp} path
775
+ * @param  {Array}  keys
776
+ * @return {RegExp}
777
+ */
778
+function regexpToRegexp (path, keys) {
779
+  // Use a negative lookahead to match only capturing groups.
780
+  var groups = path.source.match(/\((?!\?)/g);
781
+
782
+  if (groups) {
783
+    for (var i = 0; i < groups.length; i++) {
784
+      keys.push({
785
+        name:      i,
786
+        delimiter: null,
787
+        optional:  false,
788
+        repeat:    false
789
+      });
790
+    }
791
+  }
792
+
793
+  return attachKeys(path, keys);
794
+}
795
+
796
+/**
797
+ * Transform an array into a regexp.
798
+ *
799
+ * @param  {Array}  path
800
+ * @param  {Array}  keys
801
+ * @param  {Object} options
802
+ * @return {RegExp}
803
+ */
804
+function arrayToRegexp (path, keys, options) {
805
+  var parts = [];
806
+
807
+  for (var i = 0; i < path.length; i++) {
808
+    parts.push(pathToRegexp(path[i], keys, options).source);
809
+  }
810
+
811
+  var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options));
812
+  return attachKeys(regexp, keys);
813
+}
814
+
815
+/**
816
+ * Replace the specific tags with regexp strings.
817
+ *
818
+ * @param  {String} path
819
+ * @param  {Array}  keys
820
+ * @return {String}
821
+ */
822
+function replacePath (path, keys) {
823
+  var index = 0;
824
+
825
+  function replace (_, escaped, prefix, key, capture, group, suffix, escape) {
826
+    if (escaped) {
827
+      return escaped;
828
+    }
829
+
830
+    if (escape) {
831
+      return '\\' + escape;
832
+    }
833
+
834
+    var repeat   = suffix === '+' || suffix === '*';
835
+    var optional = suffix === '?' || suffix === '*';
836
+
837
+    keys.push({
838
+      name:      key || index++,
839
+      delimiter: prefix || '/',
840
+      optional:  optional,
841
+      repeat:    repeat
842
+    });
843
+
844
+    prefix = prefix ? ('\\' + prefix) : '';
845
+    capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?');
846
+
847
+    if (repeat) {
848
+      capture = capture + '(?:' + prefix + capture + ')*';
849
+    }
850
+
851
+    if (optional) {
852
+      return '(?:' + prefix + '(' + capture + '))?';
853
+    }
854
+
855
+    // Basic parameter support.
856
+    return prefix + '(' + capture + ')';
857
+  }
858
+
859
+  return path.replace(PATH_REGEXP, replace);
860
+}
861
+
862
+/**
863
+ * Normalize the given path string, returning a regular expression.
864
+ *
865
+ * An empty array can be passed in for the keys, which will hold the
866
+ * placeholder key descriptions. For example, using `/user/:id`, `keys` will
867
+ * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
868
+ *
869
+ * @param  {(String|RegExp|Array)} path
870
+ * @param  {Array}                 [keys]
871
+ * @param  {Object}                [options]
872
+ * @return {RegExp}
873
+ */
874
+function pathToRegexp (path, keys, options) {
875
+  keys = keys || [];
876
+
877
+  if (!isArray(keys)) {
878
+    options = keys;
879
+    keys = [];
880
+  } else if (!options) {
881
+    options = {};
882
+  }
883
+
884
+  if (path instanceof RegExp) {
885
+    return regexpToRegexp(path, keys, options);
886
+  }
887
+
888
+  if (isArray(path)) {
889
+    return arrayToRegexp(path, keys, options);
890
+  }
891
+
892
+  var strict = options.strict;
893
+  var end = options.end !== false;
894
+  var route = replacePath(path, keys);
895
+  var endsWithSlash = path.charAt(path.length - 1) === '/';
896
+
897
+  // In non-strict mode we allow a slash at the end of match. If the path to
898
+  // match already ends with a slash, we remove it for consistency. The slash
899
+  // is valid at the end of a path match, not in the middle. This is important
900
+  // in non-ending mode, where "/test/" shouldn't match "/test//route".
901
+  if (!strict) {
902
+    route = (endsWithSlash ? route.slice(0, -2) : route) + '(?:\\/(?=$))?';
903
+  }
904
+
905
+  if (end) {
906
+    route += '$';
907
+  } else {
908
+    // In non-ending mode, we need the capturing groups to match as much as
909
+    // possible by using a positive lookahead to the end or next path segment.
910
+    route += strict && endsWithSlash ? '' : '(?=\\/|$)';
911
+  }
912
+
913
+  return attachKeys(new RegExp('^' + route, flags(options)), keys);
914
+}
915
+
916
+},{"isarray":4}],4:[function(require,module,exports){
917
+module.exports = Array.isArray || function (arr) {
918
+  return Object.prototype.toString.call(arr) == '[object Array]';
919
+};
920
+
921
+},{}]},{},[1])(1)
922
+});

+ 76
- 119
client/src/app.ts 查看文件

@@ -2,134 +2,91 @@
2 2
 /// <reference path="./stores/store.ts"/>
3 3
 /// <reference path="./actions/actions.ts"/>
4 4
 /// <reference path="./views/header.ts"/>
5
+/// <reference path="./views/view.ts"/>
6
+/// <reference path="./external/page/page.d.ts"/>
5 7
 
6
-module rpgcards {
7
-
8
-    // Global objects, only accessible within this file
9
-    var appDispatcher: Dispatcher = null;
10
-    var appActions: Actions = null;
11
-    var appStore: Store = null;
12
-
13
-    // Access to the global objects, to be used from the browser debug window
14
-    export function debug() {
15
-        return {
16
-            dispatcher: appDispatcher,
17
-            actions: appActions,
18
-            store: appStore,
19
-            refresh: refresh,
20
-            testState: setupTestState
21
-        }
22
-    }
23
-
24
-    // Initializes the application
25
-    // Creates and links the global objects
26
-    export function bootstrap() {
27
-        appDispatcher = new Dispatcher();
28
-        appActions = new Actions(appDispatcher);
29
-        appStore = new Store(appDispatcher);
8
+/// <reference path="./mock.ts"/>
30 9
 
31
-        // Set up some initial state for debugging
32
-        // Remove this line once development is finished
33
-        setupTestState();
34
-
35
-        // Start react rendering
36
-        appStore.addChangeListener(refresh);
37
-        refresh();
38
-    }
39
-
40
-    // Renders the whole application
41
-    // In this project, render*() functions are pure functions that map
42
-    // the application state (Store) to React elements
43
-    function renderApp(store: Store) {
44
-        return React.DOM.div(
45
-            {},
46
-            renderHeader(store),
47
-            renderDecks(store)
48
-            );
49
-    }
50
-
51
-    function refresh() {
52
-        React.render(renderApp(appStore), document.body);
53
-    }
10
+module rpgcards {
54 11
 
55
-    function setupTestState() {
56
-        console.log(
57
-            "This function resets the current application state"
58
-            + " and then triggers a series of hardcoded user actions,"
59
-            + " in order to set up some application state that can be used"
60
-            + " for testing");
61
-        var withDeckId = (i: number, fn: (deckId: string)=>void) => {
62
-            appStore.getDeckList().lift(ids => fn(ids[i]));
63
-        }
64
-        var withDatasetId = (i: number, fn: (datasetId: string)=>void) => {
65
-            appStore.getDatasetList().lift(ids => fn(ids[i]));
66
-        }
67
-        var withTemplateId = (i: number, fn: (datasetId: string)=>void) => {
68
-            appStore.getTemplateList().lift(ids => fn(ids[i]));
69
-        }
70
-        appActions.reset();
12
+    /**
13
+     * An object that holds all global data for our application
14
+     */
15
+    class App {
16
+        public dispatcher: Dispatcher;
17
+        public actions: Actions;
18
+        public store: Store;
19
+        public view: View;
20
+        public container: ()=>HTMLElement;
71 21
         
72
-        // Dataset 1
73
-        appActions.newDataset();
74
-        withDatasetId(0, id=> {
75
-            appActions.setDatasetName(id, "Player's Basic Rules spells");
76
-            appActions.newRecord(id);
77
-            appActions.newRecord(id);
78
-            appActions.newRecord(id);
79
-        });
80
-
81
-        // Dataset 2
82
-        appActions.newDataset();
83
-        withDatasetId(1, id=> {
84
-            appActions.setDatasetName(id, "Elemental Evil spells");
85
-            appActions.newRecord(id);
86
-            appActions.newRecord(id);
87
-            appActions.newRecord(id);
88
-        });
22
+        constructor() {
23
+            this.dispatcher = null;
24
+            this.actions = null;
25
+            this.store = null;
26
+            this.view = null;
27
+            this.container = null;
28
+        }
89 29
         
90
-        // Dataset 3
91
-        appActions.newDataset();
92
-        withDatasetId(2, id=> {
93
-            appActions.setDatasetName(id, "DM's Basic Rules creatures");
94
-            appActions.newRecord(id);
95
-            appActions.newRecord(id);
96
-            appActions.newRecord(id);
97
-        });
30
+        // Initializes the application
31
+        // Creates and links the global objects
32
+        bootstrap() {
33
+            this.dispatcher = new Dispatcher();
34
+            this.actions = new Actions(this.dispatcher);
35
+            this.store = new Store(this.dispatcher);
36
+            this.container = ()=>document.body;
37
+            
38
+            // Map URLs to views
39
+            this.setupRoutes();
40
+    
41
+            // Each state change triggers a re-render
42
+            this.store.addChangeListener(() => this.refresh());
43
+            
44
+            // Trigger an initial action
45
+            this.actions.reset();
46
+        }
98 47
         
99
-        // Template 1
100
-        appActions.newTemplate();
48
+        // Refresh
49
+        refresh() {
50
+            // In this project, views are pure functions that map
51
+            // the application state (Store) to React elements
52
+            React.render(this.view(this.store), this.container());
53
+        }
101 54
         
102
-        // Template 2
103
-        appActions.newTemplate();
104
-
105
-        // Deck 1
106
-        appActions.newDeck();
107
-        withDeckId(0, id=>{
108
-            appActions.setDeckName(id, "Wizard spells");
109
-            appActions.setDeckDesc(id, "This deck contains wizard and sorcerer spells.");
110
-            withTemplateId(0, id2=>appActions.setDeckTemplate(id, id2));
111
-            withDatasetId(0, id2=>appActions.addDeckDataset(id, id2));
112
-            withDatasetId(1, id2=>appActions.addDeckDataset(id, id2));
113
-        });
55
+        // Routing
56
+        setupRoutes() {
57
+            page('/', () => {
58
+                this.view = renderDecks;
59
+            });
60
+            page('/decks', () => {
61
+                this.view = renderDecks;
62
+            });
63
+            page('*', () => {
64
+                this.view = renderDecks;
65
+            });
66
+            
67
+            page();
68
+        }
69
+    }
70
+    
114 71
 
115
-        // Deck 2
116
-        appActions.newDeck();
117
-        withDeckId(1, id=>{
118
-            appActions.setDeckName(id, "Cleric spells");
119
-            appActions.setDeckDesc(id, "This deck contains cleric spells.");
120
-            withTemplateId(0, id2=>appActions.setDeckTemplate(id, id2));
121
-            withDatasetId(0, id2=>appActions.addDeckDataset(id, id2));
122
-            withDatasetId(1, id2=>appActions.addDeckDataset(id, id2));
123
-        });
72
+    // All the global data (only accessible from within this file)
73
+    var app: App;
74
+    
75
+    // Access to the global data, to be used from the browser debug window
76
+    export function _debug(): App { return app; }
124 77
 
125
-        // Deck 3
126
-        appActions.newDeck();
127
-        withDeckId(2, id=>{
128
-            appActions.setDeckName(id, "Creatures");
129
-            appActions.setDeckDesc(id, "This deck contains creatures.");
130
-            withTemplateId(1, id2=>appActions.setDeckTemplate(id, id2));
131
-            withDatasetId(2, id2=>appActions.addDeckDataset(id, id2));
132
-        });
78
+    // Bootstrap the application
79
+    export function bootstrap() {
80
+        app = new App();
81
+        app.bootstrap();
82
+        
83
+        _mockData();
84
+    }
85
+    
86
+    // Setup some test state
87
+    export function _mockData() {
88
+        setupTestState(app.store, app.actions);
133 89
     }
134 90
 
91
+
135 92
 }

+ 216
- 0
client/src/external/page/page.d.ts 查看文件

@@ -0,0 +1,216 @@
1
+// Type definitions for page v1.5.0
2
+// Project: http://visionmedia.github.io/page.js/
3
+// Definitions by: Alan Norbauer <http://alan.norbauer.com/>
4
+// Definitions: https://github.com/borisyankov/DefinitelyTyped
5
+
6
+declare module PageJS {
7
+    interface Static {
8
+        /**
9
+         *  Defines a route mapping path to the given callback(s).
10
+         *
11
+         *      page('/', user.list)
12
+         *      page('/user/:id', user.load, user.show)
13
+         *      page('/user/:id/edit', user.load, user.edit)
14
+         *      page('*', notfound)
15
+         *
16
+         *  Links that are not of the same origin are disregarded and will not be dispatched.
17
+         */
18
+        (path: string, ...callbacks: Callback[]): void;
19
+        /**
20
+         * This is equivalent to page('*', callback) for generic "middleware".
21
+         */
22
+        (callback: Callback): void;
23
+        /**
24
+         *  Navigate to the given path.
25
+         *
26
+         *      $('.view').click(function(e){
27
+         *        page('/user/12')
28
+         *        e.preventDefault()
29
+         *      })
30
+         */
31
+        (path: string): void;
32
+        /**
33
+         * Setup redirect form one path to other.
34
+         */
35
+        (fromPath: string, toPath: string): void;
36
+        /**
37
+         * Register page's popstate / click bindings. If you're doing selective binding you'll like want to pass { click: false } to specify this yourself. The following options are available:
38
+         * 
39
+         *     - click bind to click events [true]
40
+         *     - popstate bind to popstate[true]
41
+         *     - dispatch perform initial dispatch[true]
42
+         *     - hashbang add #!before urls[false]
43
+         * 
44
+         * If you wish to load serve initial content from the server you likely will want to set dispatch to false.
45
+         */
46
+        (options: Options): void;
47
+        /**
48
+         * Register page's popstate / click bindings. If you're doing selective binding you'll like want to pass { click: false } to specify this yourself. The following options are available:
49
+         * 
50
+         *     - click bind to click events [true]
51
+         *     - popstate bind to popstate[true]
52
+         *     - dispatch perform initial dispatch[true]
53
+         *     - hashbang add #!before urls[false]
54
+         * 
55
+         * If you wish to load serve initial content from the server you likely will want to set dispatch to false.
56
+         */
57
+        (): void;
58
+
59
+        /**
60
+         * Identical to page(fromPath, toPath)
61
+         */
62
+        redirect(fromPath: string, toPath: string): void;
63
+        /**
64
+         *  Calling page.redirect with only a string as the first parameter redirects to another route. Waits for the current route to push state and after replaces it with the new one leaving the browser history clean.
65
+         *
66
+         *      page('/default', function(){
67
+         *        // some logic to decide which route to redirect to
68
+         *        if(admin) {
69
+         *          page.redirect('/admin');
70
+         *        } else {
71
+         *          page.redirect('/guest');
72
+         *        }
73
+         *      });
74
+         *
75
+         *      page('/default');
76
+         *
77
+         */
78
+        redirect(page: string): void;
79
+        /**
80
+         *  Navigate to the given path.
81
+         *
82
+         *      $('.view').click(function(e){
83
+         *        page('/user/12')
84
+         *        e.preventDefault()
85
+         *      })
86
+         * 
87
+         * Identical to page(path).
88
+         */
89
+        show(path: string): void;
90
+        /**
91
+         * Register page's popstate / click bindings. If you're doing selective binding you'll like want to pass { click: false } to specify this yourself. The following options are available:
92
+         * 
93
+         *     - click bind to click events [true]
94
+         *     - popstate bind to popstate[true]
95
+         *     - dispatch perform initial dispatch[true]
96
+         *     - hashbang add #!before urls[false]
97
+         * 
98
+         * If you wish to load serve initial content from the server you likely will want to set dispatch to false.
99
+         * 
100
+         * Identical to page([options]).
101
+         */
102
+        start(options: Options): void;
103
+        /**
104
+         * Register page's popstate / click bindings. If you're doing selective binding you'll like want to pass { click: false } to specify this yourself. The following options are available:
105
+         * 
106
+         *     - click bind to click events [true]
107
+         *     - popstate bind to popstate[true]
108
+         *     - dispatch perform initial dispatch[true]
109
+         *     - hashbang add #!before urls[false]
110
+         * 
111
+         * If you wish to load serve initial content from the server you likely will want to set dispatch to false.
112
+         */
113
+        start(): void;
114
+        /**
115
+         * Unbind both the popstate and click handlers.
116
+         */
117
+        stop(): void;
118
+        /**
119
+         * Get or set the base path. For example if page.js is operating within /blog/* set the base path to "/blog".
120
+         */
121
+        base(path?: string): void;
122
+        /**
123
+         * Defines an exit route mapping path to the given callback(s).
124
+         *  
125
+         * Exit routes are called when a page changes, using the context from the previous change. For example:
126
+         *     
127
+         *     page('/sidebar', function(ctx, next) {
128
+         *       sidebar.open = true
129
+         *       next()
130
+         *     })
131
+         *     
132
+         *     page.exit('/sidebar', function(next) {
133
+         *       sidebar.open = false
134
+         *       next()
135
+         *     })
136
+         */
137
+        exit(path: string, callback: Callback, moreCallbacks?: Callback[]): void;
138
+        /**
139
+         * Equivalent to page.exit('*', callback).
140
+         */
141
+        exit(callback: Callback): void;
142
+    }
143
+
144
+    interface Options {
145
+        /**
146
+         * bind to click events (default = true)
147
+         */
148
+        click?: boolean;
149
+        /**
150
+         * bind to popstate (default = true)
151
+         */
152
+        popstate?: boolean;
153
+        /**
154
+         * perform initial dispatch (default = true)
155
+         */
156
+        dispatch?: boolean;
157
+        /**
158
+         * add #!before urls (default = false)
159
+         */
160
+        hashbang?: boolean;
161
+    }
162
+
163
+    interface Callback {
164
+        (ctx: Context, next: () => any): any;
165
+    }
166
+
167
+    /**
168
+     * Routes are passed Context objects, these may be used to share state, for example ctx.user =, as well as the history "state" ctx.state that the pushState API provides.
169
+     */
170
+    interface Context {
171
+        [idx: string]: any;
172
+        /**
173
+         * Saves the context using replaceState(). For example this is useful for caching HTML or other resources that were loaded for when a user presses "back".
174
+         */
175
+        save: () => void;
176
+        /**
177
+         *  If true, marks the context as handled to prevent default 404 behaviour. For example this is useful for the routes with interminate quantity of the callbacks.
178
+         */
179
+        handled: boolean;
180
+        /**
181
+         *  Pathname including the "base" (if any) and query string "/admin/login?foo=bar".
182
+         */
183
+        canonicalPath: string;
184
+        /**
185
+         *  Pathname and query string "/login?foo=bar".
186
+         */
187
+        path: string;
188
+        /**
189
+         *  Query string void of leading ? such as "foo=bar", defaults to "".
190
+         */
191
+        querystring: string;
192
+        /**
193
+         *  The pathname void of query string "/login".
194
+         */
195
+        pathname: string;
196
+        /**
197
+         *  The pushState state object.
198
+         */
199
+        state: any;
200
+        /**
201
+         * The pushState title.
202
+         */
203
+        title: string;
204
+        /**
205
+         * The parameters from the url, e.g. /user/:id => Context.params.id
206
+         */
207
+        params: any;
208
+    }
209
+}
210
+
211
+declare module "page" {
212
+    var page: PageJS.Static;
213
+    //export = page;
214
+}
215
+
216
+declare var page: PageJS.Static;

+ 90
- 0
client/src/mock.ts 查看文件

@@ -0,0 +1,90 @@
1
+/// <reference path="./stores/store.ts"/>
2
+/// <reference path="./actions/actions.ts"/>
3
+
4
+module rpgcards {
5
+
6
+    export function setupTestState(store: Store, actions: Actions) {
7
+        console.log(
8
+            "This function resets the current application state"
9
+            + " and then triggers a series of hardcoded user actions,"
10
+            + " in order to set up some application state that can be used"
11
+            + " for testing");
12
+        
13
+        // Functions for converting entity indices to entity IDs
14
+        var withDeckId = (i: number, fn: (deckId: string)=>void) => {
15
+            store.getDeckList().lift(ids => fn(ids[i]));
16
+        }
17
+        var withDatasetId = (i: number, fn: (datasetId: string)=>void) => {
18
+            store.getDatasetList().lift(ids => fn(ids[i]));
19
+        }
20
+        var withTemplateId = (i: number, fn: (datasetId: string)=>void) => {
21
+            store.getTemplateList().lift(ids => fn(ids[i]));
22
+        }
23
+        
24
+        // Reset the state
25
+        actions.reset();
26
+        
27
+        // Dataset 1
28
+        actions.newDataset();
29
+        withDatasetId(0, id=> {
30
+            actions.setDatasetName(id, "Player's Basic Rules spells");
31
+            actions.newRecord(id);
32
+            actions.newRecord(id);
33
+            actions.newRecord(id);
34
+        });
35
+
36
+        // Dataset 2
37
+        actions.newDataset();
38
+        withDatasetId(1, id=> {
39
+            actions.setDatasetName(id, "Elemental Evil spells");
40
+            actions.newRecord(id);
41
+            actions.newRecord(id);
42
+            actions.newRecord(id);
43
+        });
44
+        
45
+        // Dataset 3
46
+        actions.newDataset();
47
+        withDatasetId(2, id=> {
48
+            actions.setDatasetName(id, "DM's Basic Rules creatures");
49
+            actions.newRecord(id);
50
+            actions.newRecord(id);
51
+            actions.newRecord(id);
52
+        });
53
+        
54
+        // Template 1
55
+        actions.newTemplate();
56
+        
57
+        // Template 2
58
+        actions.newTemplate();
59
+
60
+        // Deck 1
61
+        actions.newDeck();
62
+        withDeckId(0, id=>{
63
+            actions.setDeckName(id, "Wizard spells");
64
+            actions.setDeckDesc(id, "This deck contains wizard and sorcerer spells.");
65
+            withTemplateId(0, id2=>actions.setDeckTemplate(id, id2));
66
+            withDatasetId(0, id2=>actions.addDeckDataset(id, id2));
67
+            withDatasetId(1, id2=>actions.addDeckDataset(id, id2));
68
+        });
69
+
70
+        // Deck 2
71
+        actions.newDeck();
72
+        withDeckId(1, id=>{
73
+            actions.setDeckName(id, "Cleric spells");
74
+            actions.setDeckDesc(id, "This deck contains cleric spells.");
75
+            withTemplateId(0, id2=>actions.setDeckTemplate(id, id2));
76
+            withDatasetId(0, id2=>actions.addDeckDataset(id, id2));
77
+            withDatasetId(1, id2=>actions.addDeckDataset(id, id2));
78
+        });
79
+
80
+        // Deck 3
81
+        actions.newDeck();
82
+        withDeckId(2, id=>{
83
+            actions.setDeckName(id, "Creatures");
84
+            actions.setDeckDesc(id, "This deck contains creatures.");
85
+            withTemplateId(1, id2=>actions.setDeckTemplate(id, id2));
86
+            withDatasetId(2, id2=>actions.addDeckDataset(id, id2));
87
+        });
88
+    }
89
+
90
+}

+ 8
- 0
client/src/views/view.ts 查看文件

@@ -0,0 +1,8 @@
1
+/// <reference path="../external/react/react.d.ts"/>
2
+/// <reference path="../stores/store.ts"/>
3
+
4
+module rpgcards {
5
+	
6
+	export type View = (store: Store) => React.ReactElement<any>;
7
+
8
+}

+ 5
- 2
client/tsconfig.json 查看文件

@@ -16,12 +16,14 @@
16 16
         "!./node_modules/**/*.ts"
17 17
     ],
18 18
     "files": [
19
-        "./src/actions/actions.ts",
20 19
         "./src/app.ts",
20
+        "./src/mock.ts",
21
+        "./src/actions/actions.ts",
21 22
         "./src/dispatcher/dispatcher.ts",
22 23
         "./src/dispatcher/invariant.ts",
23 24
         "./src/external/react/react-addons.d.ts",
24 25
         "./src/external/react/react.d.ts",
26
+        "./src/external/page/page.d.ts",
25 27
         "./src/stores/asyncT.ts",
26 28
         "./src/stores/card.ts",
27 29
         "./src/stores/dataset.ts",
@@ -33,6 +35,7 @@
33 35
         "./src/stores/template.ts",
34 36
         "./src/views/components/deck.ts",
35 37
         "./src/views/decks.ts",
36
-        "./src/views/header.ts"
38
+        "./src/views/header.ts",
39
+        "./src/views/view.ts"
37 40
     ]
38 41
 }

Loading…
取消
儲存