├── .gitignore
├── .travis.yml
├── Makefile
├── README.md
├── example
├── bundle.css
├── bundle.js
├── components
│ ├── bar
│ │ ├── index.html
│ │ └── index.js
│ ├── baz
│ │ ├── components
│ │ │ ├── abc
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ └── xyz
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ ├── index.html
│ │ └── index.js
│ ├── foo
│ │ ├── index.html
│ │ └── index.js
│ └── menu
│ │ ├── index.html
│ │ └── index.js
├── directives
│ └── go.js
├── index.html
├── index.js
├── index.styl
└── search
│ ├── index.html
│ └── index.js
├── index.js
├── package.json
└── test
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.11"
4 | - "0.10"
5 | before_install:
6 | - "export DISPLAY=:99.0"
7 | - "sh -e /etc/init.d/xvfb start"
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | npm --silent test
3 |
4 | test-browser:
5 | mkdir -p test-browser
6 | node_modules/.bin/browserify -t brfs test/index.js > test-browser/bundle.js
7 | printf '\
8 | \n\
9 | \n\
10 |
\n\
11 | \n\
12 | test\n\
13 | \n\
14 | \n\
15 | \n\
16 | \n\
17 | ' > test-browser/index.html
18 |
19 | cover:
20 | npm --silent run cover
21 |
22 | example:
23 | node_modules/.bin/browserify -t brfs example/index.js -o example/bundle.js
24 | node_modules/.bin/stylus < example/index.styl > example/bundle.css
25 |
26 | .PHONY: test cover example test-browser
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue Lanes [](https://travis-ci.org/bpierre/vue-lanes)
2 |
3 | Event-based routing system for [Vue.js](http://vuejs.org).
4 |
5 | 
6 |
7 |
8 | ## Example
9 |
10 | Vue Lanes need to be initialized first. The `Lanes` extended Vue will let you create _Vue Lanes_ components, or can be directly instantiated.
11 |
12 | See the [example](example) directory for a more complete example.
13 |
14 | ```js
15 | var Vue = require('vue');
16 | var vueLanes = require('vue-lanes');
17 |
18 | var Lanes = vueLanes(Vue, {
19 |
20 | prefix: '!/', // The path prefix
21 |
22 | routes: function(route) {
23 |
24 | // Add routes with the route() function
25 | route(
26 | 'index', // Route name
27 | /^$/ // Route regex
28 | );
29 |
30 | // Use capturing groups to retrieve parameters
31 | route('search', /^search\/(.+)$/);
32 | }
33 | });
34 |
35 | var app = new Lanes({
36 |
37 | created: function() {
38 |
39 | // The lanes:route event will be emitted each time the route has changed
40 | this.$on('lanes:route', function(route) {
41 | // do something
42 | });
43 |
44 | },
45 | components: {
46 | search: Lanes.extend({
47 | data: { query: '' },
48 | created: function() {
49 |
50 | // Dispatch the lanes:path event to the root VM to change the path,
51 | // which will automatically change the current route
52 | this.$watch('query', function(query) {
53 | this.$dispatch('lanes:path', 'search/' + query);
54 | });
55 |
56 | // The lanes:update:search event is broadcasted from the root Lanes Vue.
57 | // You can safely use it to update your value, even if it’s watched,
58 | // because Vue Lanes will prevent infinite loops in most cases.
59 | this.$on('lanes:update:search', function(route) {
60 | this.query = route.params[0];
61 | });
62 |
63 | // The lanes:route event is broadcasted each time a new route is set.
64 | this.$on('lanes:route', function(route) {
65 | // This function will be called on every route change.
66 | });
67 | }
68 | })
69 | }
70 | });
71 | ```
72 |
73 | ## Installation
74 |
75 | ```
76 | $ npm install vue-lanes
77 | ```
78 |
79 | ## Events
80 |
81 | Inside a `Lanes` extended Vue, you can _listen_ for the `lanes:route` event, and _dispatch_ a `lanes:path` event to change the path.
82 |
83 | If you are interested by a specific route, you can _listen_ for the `lanes:update:` and `lanes:leave:{route_name}` events.
84 |
85 | ### lanes:route
86 |
87 | The `lanes:route` event will send a `route` paramater, which is the route object provided by [miniroutes](https://github.com/bpierre/miniroutes).
88 |
89 | ### lanes:update:{route_name}
90 |
91 | Where `{route_name}` is the name of a registered route.
92 |
93 | The `lanes:update:{route_name}` acts exactly as the `lanes:route` event, except it is for a specific route. This is useful if you want to do something when a specific route is active.
94 |
95 | ### lanes:leave:{route_name}
96 |
97 | Where `{route_name}` is the name of a registered route.
98 |
99 | The `lanes:leave:{route_name}` is triggered everytime another route is set. This event is not triggered if a route is just updated (different path).
100 |
101 | ### lanes:path
102 |
103 | The `lanes:path` event must be dispatched from a Vue Lanes instance in order to update the path. The root _Vue Lanes_ instance will then broadcast a `lanes:route`.
104 |
105 | ## TODO
106 |
107 | - Add an `history.pushState` mode.
108 |
109 | ## Browser compatibility
110 |
111 | IE9+ and modern browsers.
112 |
113 | [](https://ci.testling.com/bpierre/vue-lanes)
114 |
115 | ## License
116 |
117 | [MIT](http://pierre.mit-license.org/)
118 |
119 | ## Special thanks
120 |
121 | Illustration made by [Raphaël Bastide](http://raphaelbastide.com/) with [scri.ch](http://scri.ch/).
122 |
--------------------------------------------------------------------------------
/example/bundle.css:
--------------------------------------------------------------------------------
1 | #app {
2 | width: 600px;
3 | margin: 0 auto;
4 | padding: 40px;
5 | border: 1px solid #000;
6 | }
7 | #app h1 {
8 | margin: 0;
9 | }
10 | section {
11 | margin: 20px;
12 | padding: 40px;
13 | border: 1px solid #000;
14 | }
15 | nav a {
16 | display: inline-block;
17 | margin: 2px;
18 | padding: 5px;
19 | border: 1px solid #000;
20 | text-decoration: none;
21 | }
22 | nav a.active {
23 | text-decoration: underline;
24 | }
25 | a {
26 | text-decoration: underline;
27 | cursor: pointer;
28 | }
29 | a.active {
30 | font-weight: bold;
31 | }
32 | .search {
33 | margin-top: 20px;
34 | }
35 | .results {
36 | position: relative;
37 | }
38 | .results ol {
39 | overflow: hidden;
40 | min-height: 500px;
41 | margin: 0;
42 | padding: 0;
43 | list-style-position: inside;
44 | }
45 | .results li {
46 | margin: 20px 0;
47 | padding: 20px;
48 | border: 1px solid #ccc;
49 | }
50 | .results li:first-child {
51 | margin-top: 0;
52 | }
53 | .results li:last-child {
54 | margin-bottom: 0;
55 | }
56 | .results .loader {
57 | position: absolute;
58 | top: 0;
59 | left: 0;
60 | width: calc(100% - 2px);
61 | height: calc(100% - 2px);
62 | margin: 0;
63 | line-height: 200px;
64 | text-align: center;
65 | font-size: 24px;
66 | background: rgba(0,0,0,0.1);
67 | border: 1px solid #ccc;
68 | opacity: 1;
69 | transition: opacity 150ms ease-in-out;
70 | }
71 | .results .loader.v-enter,
72 | .results .loader.v-leave {
73 | opacity: 0;
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/example/bundle.js:
--------------------------------------------------------------------------------
1 | (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);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o\n bar component
\n \n\n"
6 | };
7 |
8 | },{"fs":10}],2:[function(require,module,exports){
9 | var fs = require('fs');
10 |
11 | module.exports = {
12 | template: "\n"
13 | };
14 |
15 | },{"fs":10}],3:[function(require,module,exports){
16 | var fs = require('fs');
17 |
18 | module.exports = {
19 | template: "\n"
20 | };
21 |
22 | },{"fs":10}],4:[function(require,module,exports){
23 | var fs = require('fs');
24 |
25 | module.exports = {
26 | template: "\n baz component
\n \n \n \n\n",
27 | components: {
28 | 'abc': require('./components/abc'),
29 | 'xyz': require('./components/xyz')
30 | }
31 | }
32 |
33 | },{"./components/abc":2,"./components/xyz":3,"fs":10}],5:[function(require,module,exports){
34 | var fs = require('fs');
35 |
36 | function shuffle(str) {
37 | return str.split('').sort(function(){
38 | return 0.5 - Math.random();
39 | }).join('');
40 | }
41 |
42 | module.exports = {
43 | template: "\n foo component
\n \n \n
Type something here:
\n
\n
loading…
\n
\n - {{$value}}
\n
\n
\n
\n\n",
44 | created: function() {
45 | var self = this;
46 | var active = null;
47 |
48 | // The current route has been updated
49 | this.$on('lanes:update:foo', function(route) {
50 | active = true;
51 | this.keywords = route.params[0] || '';
52 | });
53 |
54 | // Another route is set
55 | this.$on('lanes:leave:foo', function() {
56 | active = false;
57 | });
58 |
59 | var typeTimeout = null;
60 | this.$watch('keywords', function(keywords) {
61 | if (typeTimeout) clearTimeout(typeTimeout);
62 | if (!active) return;
63 |
64 | // Update the current path (which will update the current route)
65 | var path = 'foo' + (keywords? '/' + keywords : '');
66 | this.$dispatch('lanes:path', path);
67 |
68 | this.results = [];
69 |
70 | if (!keywords) {
71 | this.loading = false;
72 | return;
73 | }
74 |
75 | this.loading = true;
76 |
77 | typeTimeout = setTimeout(function() {
78 | self.results = [];
79 | self.loading = false;
80 | var i = 5;
81 | while (i--) self.results.push(shuffle(keywords));
82 | }, 500);
83 | });
84 | },
85 | data: {
86 | loading: false,
87 | keywords: '',
88 | results: []
89 | }
90 | };
91 |
92 | },{"fs":10}],6:[function(require,module,exports){
93 | var fs = require('fs');
94 |
95 | module.exports = {
96 | template: "\n",
97 | data: {
98 | path: null,
99 | paths: [
100 | 'foo',
101 | 'foo/xyz',
102 | 'bar',
103 | 'baz',
104 | 'baz/abc',
105 | 'baz/xyz'
106 | ]
107 | },
108 | created: function() {
109 | this.$on('lanes:route', function(route) {
110 | this.path = route.path;
111 | });
112 | }
113 | }
114 |
115 | },{"fs":10}],7:[function(require,module,exports){
116 | module.exports = {
117 | update: function(value) {
118 | var self = this;
119 | this.reset();
120 |
121 | // Send the path to the current vm
122 | this.el.addEventListener('click', this.onclick = function() {
123 | self.vm.$dispatch('lanes:path', value);
124 | });
125 | },
126 | unbind: function() {
127 | this.reset();
128 | },
129 | reset: function() {
130 | if (!this.onclick) return;
131 | this.el.removeEventListener('click', this.onclick);
132 | }
133 | };
134 |
135 | },{}],8:[function(require,module,exports){
136 | var Vue = require('vue');
137 | var vueLanes = require('../');
138 |
139 | var Lanes = vueLanes(Vue, {
140 | debug: true,
141 | prefix: '!/',
142 | routes: function(route) {
143 |
144 | // Match '', 'foo', 'foo/'
145 | route('foo', /^(?:foo(?:\/(.+))?)?$/);
146 |
147 | // Match 'bar'
148 | route('bar', /^bar\/*$/);
149 |
150 | // Match 'baz', 'baz/'
151 | route('baz', /^baz(?:\/(.+))?\/*$/);
152 | }
153 | });
154 |
155 | /*
156 | // Same example with the array syntax:
157 | var Lanes = vueLanes(Vue, {
158 | prefix: '!/',
159 | routes: [
160 |
161 | // Match '', 'foo', 'foo/'
162 | [ 'foo', /^(?:foo(?:\/(.+))?)?$/ ],
163 |
164 | // Match 'bar'
165 | [ 'bar', /^bar\/*$/ ],
166 |
167 | // Match 'baz', 'baz/'
168 | [ 'baz', /^baz(?:\/(.+))?\/*$/ ]
169 | ]
170 | });
171 | */
172 |
173 | var app = new Lanes({
174 | el: 'body',
175 | data: {
176 | route: null
177 | },
178 | created: function() {
179 | this.$on('lanes:route', function(route) {
180 | this.route = {
181 | name: route.name,
182 | path: route.path,
183 | params: route.params
184 | };
185 | this.$emit('lanes:path', route.path);
186 | });
187 | },
188 | directives: {
189 | go: require('./directives/go')
190 | },
191 | components: {
192 | foo: require('./components/foo'),
193 | bar: require('./components/bar'),
194 | baz: require('./components/baz'),
195 | menu: require('./components/menu')
196 | }
197 | });
198 |
199 | },{"../":9,"./components/bar":1,"./components/baz":4,"./components/foo":5,"./components/menu":6,"./directives/go":7,"vue":30}],9:[function(require,module,exports){
200 | var minihash = require('minihash');
201 | var miniroutes = require('miniroutes');
202 |
203 | // if $compiler.init is false, the ready hook has been triggered
204 | function ensureReady(vm, cb) {
205 | if (!vm.$compiler.init) return cb();
206 | vm.$once('hook:ready', cb);
207 | }
208 |
209 | function createLog(debug) {
210 | return function() {
211 | if (debug) console.log.apply(console, [].slice.call(arguments));
212 | }
213 | }
214 |
215 | function routesEqual(route1, route2) {
216 | if (!(route1 && route2) ||
217 | route1.name !== route2.name ||
218 | route1.params.length !== route2.params.length ||
219 | route1.path !== route2.path) {
220 | return false;
221 | }
222 | for (var i = route1.params.length - 1; i >= 0; i--) {
223 | if (route1.params[i] !== route2.params[i]) return false;
224 | }
225 | return true;
226 | }
227 |
228 | function initRoot(vm, routes, options) {
229 | var currentRoute = null;
230 | var hash = null;
231 | var log = createLog(options.debug);
232 |
233 | // Update the current path on update event
234 | vm.$on('lanes:route', function(route, except) {
235 | log('lanes:route received', route);
236 | currentRoute = route;
237 | vm.$broadcast('lanes:route', route, except);
238 | });
239 |
240 | // New path received: update the hash value (triggers a route update)
241 | vm.$on('lanes:path', function(path) {
242 | log('lanes:path received', path);
243 | ensureReady(vm, function() {
244 | log('change the hash value', path);
245 | hash.value = path;
246 | });
247 | });
248 |
249 | // Routing mechanism
250 | hash = minihash(options.prefix, miniroutes(routes, function(route, previous) {
251 | log('hash->route received', route);
252 | ensureReady(vm, function() {
253 | if (!currentRoute || !routesEqual(currentRoute, route)) {
254 | log('emits a lanes:route event', route);
255 | vm.$emit('lanes:route', route);
256 | vm.$broadcast('lanes:update:' + route.name, route);
257 | if (previous && previous.name !== route.name) {
258 | vm.$broadcast('lanes:leave:' + previous.name, previous);
259 | }
260 | }
261 | });
262 | }));
263 |
264 | vm.$on('hook:beforeDestroy', function() {
265 | hash.stop();
266 | });
267 | }
268 |
269 | function makeRoutes(routes) {
270 | if (Array.isArray(routes)) return routes;
271 | if (typeof routes !== 'function') return [];
272 | var finalRoutes = [];
273 | routes(function(name, re) {
274 | finalRoutes.push([name, re]);
275 | });
276 | return finalRoutes;
277 | }
278 |
279 | module.exports = function(Vue, options) {
280 | return Vue.extend({
281 | created: function() {
282 | if (this.$root === this) {
283 | initRoot(this, makeRoutes(options.routes), {
284 | prefix: options.prefix || '',
285 | debug: options.debug || false
286 | });
287 | }
288 | }
289 | });
290 | };
291 |
292 | },{"minihash":11,"miniroutes":12}],10:[function(require,module,exports){
293 |
294 | },{}],11:[function(require,module,exports){
295 | /*
296 | * Mini location.hash update system
297 | *
298 | * Usage:
299 | *
300 | * var minihash = require('minihash');
301 | * var hash = minihash('!/', function(value) {
302 | * // Value updated
303 | * });
304 | *
305 | * // Update the location.hash value and trigger the update
306 | * hash.value = 'foo';
307 | *
308 | */
309 |
310 | module.exports = function createHash(prefix, update) {
311 |
312 | // Callback first
313 | if (!update && typeof prefix === 'function') {
314 | update = prefix;
315 | prefix = '';
316 | }
317 |
318 | if (!prefix) prefix = '';
319 | if (!update) update = function(){};
320 |
321 | var hash = {};
322 | var _value = getHash(prefix);
323 |
324 | Object.defineProperty(hash, 'value', {
325 | enumerable: false,
326 | get: function() {
327 | return _value;
328 | },
329 | set: function(value) {
330 | if (value === _value) return;
331 | _value = setHash(prefix, value);
332 | update(_value);
333 | }
334 | });
335 |
336 | var rmHashChange = hashChange(prefix, function() {
337 | var value = getHash(prefix);
338 | _value = setHash(prefix, value);
339 | update(_value);
340 | });
341 |
342 | hash.stop = rmHashChange;
343 |
344 | update(_value);
345 |
346 | return hash;
347 | };
348 |
349 | function getHash(prefix) {
350 | var hash = window.location.hash.slice(1);
351 | if (hash.indexOf(prefix) !== 0) return hash;
352 | return hash.slice(prefix.length);
353 | }
354 | function setHash(prefix, value) {
355 | window.location.hash = prefix + value;
356 | return value;
357 | }
358 | function hashChange(prefix, cb) {
359 | window.addEventListener('hashchange', cb);
360 | return function rmHashChange() {
361 | window.removeEventListener('hashchange', cb);
362 | };
363 | }
364 |
365 | },{}],12:[function(require,module,exports){
366 | /*
367 | * Mini routing system
368 | *
369 | * Usage:
370 | *
371 | * var miniroutes = require('miniroutes');
372 | *
373 | * var paths = [
374 | *
375 | * // Match '', 'search', 'search/'
376 | * [ 'search', /^(?:search(?:\/(.+))?)?$/ ],
377 | *
378 | * // Match 'page2'
379 | * [ 'page2', /^page2$/ ]
380 | *
381 | * ];
382 | *
383 | * var routing = miniroutes(paths, function(route) {
384 | * console.log(route); // matched route
385 | * });
386 | *
387 | * routing('search'); // { 'name': 'search', params: [] }
388 | * routing('search/test'); // { 'name': 'search', params: ['test'] }
389 | *
390 | * Use the minihash module to feed miniroutes:
391 | *
392 | * var minihash = require('minihash');
393 | * var hash = minihash('!/', routing);
394 | *
395 | */
396 |
397 | module.exports = function createRouting(paths, cb) {
398 | var route = null;
399 | var previous = null;
400 | return function updatePath(path) {
401 | previous = route;
402 | route = getRoute(paths, path);
403 | cb(route, previous);
404 | };
405 | }
406 |
407 | function matches(re, path) {
408 | var matches = re.exec(path);
409 | if (matches === null) return null;
410 | matches = matches.slice(1);
411 | matches = matches.map(function(val) {
412 | if (typeof val === 'undefined') return null;
413 | return val;
414 | });
415 | return matches;
416 | }
417 |
418 | function getRoute(paths, path) {
419 | var route = {
420 | name: null,
421 | params: [],
422 | path: path
423 | };
424 | for (var i=0, l = paths.length, params; i < l; i++) {
425 | // Valid path found
426 | params = matches(paths[i][1], path);
427 | if (params !== null) {
428 | route.name = paths[i][0];
429 | route.params = params;
430 | break;
431 | }
432 | }
433 | return route;
434 | };
435 |
436 | },{}],13:[function(require,module,exports){
437 | var utils = require('./utils')
438 |
439 | function Batcher () {
440 | this.reset()
441 | }
442 |
443 | var BatcherProto = Batcher.prototype
444 |
445 | BatcherProto.push = function (job) {
446 | if (!job.id || !this.has[job.id]) {
447 | this.queue.push(job)
448 | this.has[job.id] = job
449 | if (!this.waiting) {
450 | this.waiting = true
451 | utils.nextTick(utils.bind(this.flush, this))
452 | }
453 | } else if (job.override) {
454 | var oldJob = this.has[job.id]
455 | oldJob.cancelled = true
456 | this.queue.push(job)
457 | this.has[job.id] = job
458 | }
459 | }
460 |
461 | BatcherProto.flush = function () {
462 | // before flush hook
463 | if (this._preFlush) this._preFlush()
464 | // do not cache length because more jobs might be pushed
465 | // as we execute existing jobs
466 | for (var i = 0; i < this.queue.length; i++) {
467 | var job = this.queue[i]
468 | if (job.cancelled) continue
469 | if (job.execute() !== false) {
470 | this.has[job.id] = false
471 | }
472 | }
473 | this.reset()
474 | }
475 |
476 | BatcherProto.reset = function () {
477 | this.has = utils.hash()
478 | this.queue = []
479 | this.waiting = false
480 | }
481 |
482 | module.exports = Batcher
483 | },{"./utils":34}],14:[function(require,module,exports){
484 | var Batcher = require('./batcher'),
485 | bindingBatcher = new Batcher(),
486 | bindingId = 1
487 |
488 | /**
489 | * Binding class.
490 | *
491 | * each property on the viewmodel has one corresponding Binding object
492 | * which has multiple directive instances on the DOM
493 | * and multiple computed property dependents
494 | */
495 | function Binding (compiler, key, isExp, isFn) {
496 | this.id = bindingId++
497 | this.value = undefined
498 | this.isExp = !!isExp
499 | this.isFn = isFn
500 | this.root = !this.isExp && key.indexOf('.') === -1
501 | this.compiler = compiler
502 | this.key = key
503 | this.dirs = []
504 | this.subs = []
505 | this.deps = []
506 | this.unbound = false
507 | }
508 |
509 | var BindingProto = Binding.prototype
510 |
511 | /**
512 | * Update value and queue instance updates.
513 | */
514 | BindingProto.update = function (value) {
515 | if (!this.isComputed || this.isFn) {
516 | this.value = value
517 | }
518 | if (this.dirs.length || this.subs.length) {
519 | var self = this
520 | bindingBatcher.push({
521 | id: this.id,
522 | execute: function () {
523 | if (!self.unbound) {
524 | self._update()
525 | } else {
526 | return false
527 | }
528 | }
529 | })
530 | }
531 | }
532 |
533 | /**
534 | * Actually update the directives.
535 | */
536 | BindingProto._update = function () {
537 | var i = this.dirs.length,
538 | value = this.val()
539 | while (i--) {
540 | this.dirs[i].update(value)
541 | }
542 | this.pub()
543 | }
544 |
545 | /**
546 | * Return the valuated value regardless
547 | * of whether it is computed or not
548 | */
549 | BindingProto.val = function () {
550 | return this.isComputed && !this.isFn
551 | ? this.value.$get()
552 | : this.value
553 | }
554 |
555 | /**
556 | * Notify computed properties that depend on this binding
557 | * to update themselves
558 | */
559 | BindingProto.pub = function () {
560 | var i = this.subs.length
561 | while (i--) {
562 | this.subs[i].update()
563 | }
564 | }
565 |
566 | /**
567 | * Unbind the binding, remove itself from all of its dependencies
568 | */
569 | BindingProto.unbind = function () {
570 | // Indicate this has been unbound.
571 | // It's possible this binding will be in
572 | // the batcher's flush queue when its owner
573 | // compiler has already been destroyed.
574 | this.unbound = true
575 | var i = this.dirs.length
576 | while (i--) {
577 | this.dirs[i].unbind()
578 | }
579 | i = this.deps.length
580 | var subs
581 | while (i--) {
582 | subs = this.deps[i].subs
583 | subs.splice(subs.indexOf(this), 1)
584 | }
585 | }
586 |
587 | module.exports = Binding
588 | },{"./batcher":13}],15:[function(require,module,exports){
589 | var Emitter = require('./emitter'),
590 | Observer = require('./observer'),
591 | config = require('./config'),
592 | utils = require('./utils'),
593 | Binding = require('./binding'),
594 | Directive = require('./directive'),
595 | TextParser = require('./text-parser'),
596 | DepsParser = require('./deps-parser'),
597 | ExpParser = require('./exp-parser'),
598 |
599 | // cache methods
600 | slice = [].slice,
601 | log = utils.log,
602 | makeHash = utils.hash,
603 | extend = utils.extend,
604 | def = utils.defProtected,
605 | hasOwn = ({}).hasOwnProperty,
606 |
607 | // hooks to register
608 | hooks = [
609 | 'created', 'ready',
610 | 'beforeDestroy', 'afterDestroy',
611 | 'attached', 'detached'
612 | ]
613 |
614 | /**
615 | * The DOM compiler
616 | * scans a DOM node and compile bindings for a ViewModel
617 | */
618 | function Compiler (vm, options) {
619 |
620 | var compiler = this
621 |
622 | // default state
623 | compiler.init = true
624 | compiler.repeat = false
625 | compiler.destroyed = false
626 | compiler.delayReady = false
627 |
628 | // process and extend options
629 | options = compiler.options = options || makeHash()
630 | utils.processOptions(options)
631 |
632 | // copy data, methods & compiler options
633 | var data = compiler.data = options.data || {}
634 | extend(vm, data, true)
635 | extend(vm, options.methods, true)
636 | extend(compiler, options.compilerOptions)
637 |
638 | // initialize element
639 | var el = compiler.el = compiler.setupElement(options)
640 | log('\nnew VM instance: ' + el.tagName + '\n')
641 |
642 | // set compiler properties
643 | compiler.vm = el.vue_vm = vm
644 | compiler.bindings = makeHash()
645 | compiler.dirs = []
646 | compiler.deferred = []
647 | compiler.exps = []
648 | compiler.computed = []
649 | compiler.children = []
650 | compiler.emitter = new Emitter()
651 | compiler.emitter._ctx = vm
652 | compiler.delegators = makeHash()
653 |
654 | // set inenumerable VM properties
655 | def(vm, '$', makeHash())
656 | def(vm, '$el', el)
657 | def(vm, '$options', options)
658 | def(vm, '$compiler', compiler)
659 |
660 | // set parent VM
661 | // and register child id on parent
662 | var parentVM = options.parent,
663 | childId = utils.attr(el, 'ref')
664 | if (parentVM) {
665 | compiler.parent = parentVM.$compiler
666 | parentVM.$compiler.children.push(compiler)
667 | def(vm, '$parent', parentVM)
668 | if (childId) {
669 | compiler.childId = childId
670 | parentVM.$[childId] = vm
671 | }
672 | }
673 |
674 | // set root
675 | def(vm, '$root', getRoot(compiler).vm)
676 |
677 | // setup observer
678 | compiler.setupObserver()
679 |
680 | // create bindings for computed properties
681 | var computed = options.computed
682 | if (computed) {
683 | for (var key in computed) {
684 | compiler.createBinding(key)
685 | }
686 | }
687 |
688 | // copy paramAttributes
689 | if (options.paramAttributes) {
690 | options.paramAttributes.forEach(function (attr) {
691 | var val = el.getAttribute(attr)
692 | vm[attr] = (isNaN(val) || val === null)
693 | ? val
694 | : Number(val)
695 | })
696 | }
697 |
698 | // beforeCompile hook
699 | compiler.execHook('created')
700 |
701 | // the user might have set some props on the vm
702 | // so copy it back to the data...
703 | extend(data, vm)
704 |
705 | // observe the data
706 | compiler.observeData(data)
707 |
708 | // for repeated items, create index/key bindings
709 | // because they are ienumerable
710 | if (compiler.repeat) {
711 | compiler.createBinding('$index')
712 | if (data.$key) compiler.createBinding('$key')
713 | }
714 |
715 | // now parse the DOM, during which we will create necessary bindings
716 | // and bind the parsed directives
717 | compiler.compile(el, true)
718 |
719 | // bind deferred directives (child components)
720 | compiler.deferred.forEach(compiler.bindDirective, compiler)
721 |
722 | // extract dependencies for computed properties
723 | compiler.parseDeps()
724 |
725 | // done!
726 | compiler.rawContent = null
727 | compiler.init = false
728 |
729 | // post compile / ready hook
730 | if (!compiler.delayReady) {
731 | compiler.execHook('ready')
732 | }
733 | }
734 |
735 | var CompilerProto = Compiler.prototype
736 |
737 | /**
738 | * Initialize the VM/Compiler's element.
739 | * Fill it in with the template if necessary.
740 | */
741 | CompilerProto.setupElement = function (options) {
742 | // create the node first
743 | var el = typeof options.el === 'string'
744 | ? document.querySelector(options.el)
745 | : options.el || document.createElement(options.tagName || 'div')
746 |
747 | var template = options.template
748 | if (template) {
749 | // collect anything already in there
750 | /* jshint boss: true */
751 | var child,
752 | frag = this.rawContent = document.createDocumentFragment()
753 | while (child = el.firstChild) {
754 | frag.appendChild(child)
755 | }
756 | // replace option: use the first node in
757 | // the template directly
758 | if (options.replace && template.childNodes.length === 1) {
759 | var replacer = template.childNodes[0].cloneNode(true)
760 | if (el.parentNode) {
761 | el.parentNode.insertBefore(replacer, el)
762 | el.parentNode.removeChild(el)
763 | }
764 | el = replacer
765 | } else {
766 | el.appendChild(template.cloneNode(true))
767 | }
768 | }
769 |
770 | // apply element options
771 | if (options.id) el.id = options.id
772 | if (options.className) el.className = options.className
773 | var attrs = options.attributes
774 | if (attrs) {
775 | for (var attr in attrs) {
776 | el.setAttribute(attr, attrs[attr])
777 | }
778 | }
779 |
780 | return el
781 | }
782 |
783 | /**
784 | * Setup observer.
785 | * The observer listens for get/set/mutate events on all VM
786 | * values/objects and trigger corresponding binding updates.
787 | * It also listens for lifecycle hooks.
788 | */
789 | CompilerProto.setupObserver = function () {
790 |
791 | var compiler = this,
792 | bindings = compiler.bindings,
793 | options = compiler.options,
794 | observer = compiler.observer = new Emitter()
795 |
796 | // a hash to hold event proxies for each root level key
797 | // so they can be referenced and removed later
798 | observer.proxies = makeHash()
799 | observer._ctx = compiler.vm
800 |
801 | // add own listeners which trigger binding updates
802 | observer
803 | .on('get', onGet)
804 | .on('set', onSet)
805 | .on('mutate', onSet)
806 |
807 | // register hooks
808 | hooks.forEach(function (hook) {
809 | var fns = options[hook]
810 | if (Array.isArray(fns)) {
811 | var i = fns.length
812 | // since hooks were merged with child at head,
813 | // we loop reversely.
814 | while (i--) {
815 | registerHook(hook, fns[i])
816 | }
817 | } else if (fns) {
818 | registerHook(hook, fns)
819 | }
820 | })
821 |
822 | // broadcast attached/detached hooks
823 | observer
824 | .on('hook:attached', function () {
825 | broadcast(1)
826 | })
827 | .on('hook:detached', function () {
828 | broadcast(0)
829 | })
830 |
831 | function onGet (key) {
832 | check(key)
833 | DepsParser.catcher.emit('get', bindings[key])
834 | }
835 |
836 | function onSet (key, val, mutation) {
837 | observer.emit('change:' + key, val, mutation)
838 | check(key)
839 | bindings[key].update(val)
840 | }
841 |
842 | function registerHook (hook, fn) {
843 | observer.on('hook:' + hook, function () {
844 | fn.call(compiler.vm)
845 | })
846 | }
847 |
848 | function broadcast (event) {
849 | var children = compiler.children
850 | if (children) {
851 | var child, i = children.length
852 | while (i--) {
853 | child = children[i]
854 | if (child.el.parentNode) {
855 | event = 'hook:' + (event ? 'attached' : 'detached')
856 | child.observer.emit(event)
857 | child.emitter.emit(event)
858 | }
859 | }
860 | }
861 | }
862 |
863 | function check (key) {
864 | if (!bindings[key]) {
865 | compiler.createBinding(key)
866 | }
867 | }
868 | }
869 |
870 | CompilerProto.observeData = function (data) {
871 |
872 | var compiler = this,
873 | observer = compiler.observer
874 |
875 | // recursively observe nested properties
876 | Observer.observe(data, '', observer)
877 |
878 | // also create binding for top level $data
879 | // so it can be used in templates too
880 | var $dataBinding = compiler.bindings['$data'] = new Binding(compiler, '$data')
881 | $dataBinding.update(data)
882 |
883 | // allow $data to be swapped
884 | defGetSet(compiler.vm, '$data', {
885 | enumerable: false,
886 | get: function () {
887 | compiler.observer.emit('get', '$data')
888 | return compiler.data
889 | },
890 | set: function (newData) {
891 | var oldData = compiler.data
892 | Observer.unobserve(oldData, '', observer)
893 | compiler.data = newData
894 | Observer.copyPaths(newData, oldData)
895 | Observer.observe(newData, '', observer)
896 | compiler.observer.emit('set', '$data', newData)
897 | }
898 | })
899 |
900 | // emit $data change on all changes
901 | observer
902 | .on('set', onSet)
903 | .on('mutate', onSet)
904 |
905 | function onSet (key) {
906 | if (key !== '$data') {
907 | $dataBinding.update(compiler.data)
908 | }
909 | }
910 | }
911 |
912 | /**
913 | * Compile a DOM node (recursive)
914 | */
915 | CompilerProto.compile = function (node, root) {
916 |
917 | var compiler = this,
918 | nodeType = node.nodeType,
919 | tagName = node.tagName
920 |
921 | if (nodeType === 1 && tagName !== 'SCRIPT') { // a normal node
922 |
923 | // skip anything with v-pre
924 | if (utils.attr(node, 'pre') !== null) return
925 |
926 | // special attributes to check
927 | var repeatExp,
928 | withExp,
929 | partialId,
930 | directive,
931 | componentId = utils.attr(node, 'component') || tagName.toLowerCase(),
932 | componentCtor = compiler.getOption('components', componentId)
933 |
934 | // It is important that we access these attributes
935 | // procedurally because the order matters.
936 | //
937 | // `utils.attr` removes the attribute once it gets the
938 | // value, so we should not access them all at once.
939 |
940 | // v-repeat has the highest priority
941 | // and we need to preserve all other attributes for it.
942 | /* jshint boss: true */
943 | if (repeatExp = utils.attr(node, 'repeat')) {
944 |
945 | // repeat block cannot have v-id at the same time.
946 | directive = Directive.parse('repeat', repeatExp, compiler, node)
947 | if (directive) {
948 | directive.Ctor = componentCtor
949 | // defer child component compilation
950 | // so by the time they are compiled, the parent
951 | // would have collected all bindings
952 | compiler.deferred.push(directive)
953 | }
954 |
955 | // v-with has 2nd highest priority
956 | } else if (root !== true && ((withExp = utils.attr(node, 'with')) || componentCtor)) {
957 |
958 | withExp = Directive.split(withExp || '')
959 | withExp.forEach(function (exp, i) {
960 | var directive = Directive.parse('with', exp, compiler, node)
961 | if (directive) {
962 | directive.Ctor = componentCtor
963 | // notify the directive that this is the
964 | // last expression in the group
965 | directive.last = i === withExp.length - 1
966 | compiler.deferred.push(directive)
967 | }
968 | })
969 |
970 | } else {
971 |
972 | // check transition & animation properties
973 | node.vue_trans = utils.attr(node, 'transition')
974 | node.vue_anim = utils.attr(node, 'animation')
975 | node.vue_effect = utils.attr(node, 'effect')
976 |
977 | // replace innerHTML with partial
978 | partialId = utils.attr(node, 'partial')
979 | if (partialId) {
980 | var partial = compiler.getOption('partials', partialId)
981 | if (partial) {
982 | node.innerHTML = ''
983 | node.appendChild(partial.cloneNode(true))
984 | }
985 | }
986 |
987 | // finally, only normal directives left!
988 | compiler.compileNode(node)
989 | }
990 |
991 | } else if (nodeType === 3 && config.interpolate) { // text node
992 |
993 | compiler.compileTextNode(node)
994 |
995 | }
996 |
997 | }
998 |
999 | /**
1000 | * Compile a normal node
1001 | */
1002 | CompilerProto.compileNode = function (node) {
1003 | var i, j,
1004 | attrs = slice.call(node.attributes),
1005 | prefix = config.prefix + '-'
1006 | // parse if has attributes
1007 | if (attrs && attrs.length) {
1008 | var attr, isDirective, exps, exp, directive, dirname
1009 | // loop through all attributes
1010 | i = attrs.length
1011 | while (i--) {
1012 | attr = attrs[i]
1013 | isDirective = false
1014 |
1015 | if (attr.name.indexOf(prefix) === 0) {
1016 | // a directive - split, parse and bind it.
1017 | isDirective = true
1018 | exps = Directive.split(attr.value)
1019 | // loop through clauses (separated by ",")
1020 | // inside each attribute
1021 | j = exps.length
1022 | while (j--) {
1023 | exp = exps[j]
1024 | dirname = attr.name.slice(prefix.length)
1025 | directive = Directive.parse(dirname, exp, this, node)
1026 | if (directive) {
1027 | this.bindDirective(directive)
1028 | }
1029 | }
1030 | } else if (config.interpolate) {
1031 | // non directive attribute, check interpolation tags
1032 | exp = TextParser.parseAttr(attr.value)
1033 | if (exp) {
1034 | directive = Directive.parse('attr', attr.name + ':' + exp, this, node)
1035 | if (directive) {
1036 | this.bindDirective(directive)
1037 | }
1038 | }
1039 | }
1040 |
1041 | if (isDirective && dirname !== 'cloak') {
1042 | node.removeAttribute(attr.name)
1043 | }
1044 | }
1045 | }
1046 | // recursively compile childNodes
1047 | if (node.childNodes.length) {
1048 | slice.call(node.childNodes).forEach(this.compile, this)
1049 | }
1050 | }
1051 |
1052 | /**
1053 | * Compile a text node
1054 | */
1055 | CompilerProto.compileTextNode = function (node) {
1056 |
1057 | var tokens = TextParser.parse(node.nodeValue)
1058 | if (!tokens) return
1059 | var el, token, directive, partial, partialId, partialNodes
1060 |
1061 | for (var i = 0, l = tokens.length; i < l; i++) {
1062 | token = tokens[i]
1063 | directive = partialNodes = null
1064 | if (token.key) { // a binding
1065 | if (token.key.charAt(0) === '>') { // a partial
1066 | partialId = token.key.slice(1).trim()
1067 | if (partialId === 'yield') {
1068 | el = this.rawContent
1069 | } else {
1070 | partial = this.getOption('partials', partialId)
1071 | if (partial) {
1072 | el = partial.cloneNode(true)
1073 | } else {
1074 | utils.warn('Unknown partial: ' + partialId)
1075 | continue
1076 | }
1077 | }
1078 | if (el) {
1079 | // save an Array reference of the partial's nodes
1080 | // so we can compile them AFTER appending the fragment
1081 | partialNodes = slice.call(el.childNodes)
1082 | }
1083 | } else { // a real binding
1084 | if (!token.html) { // text binding
1085 | el = document.createTextNode('')
1086 | directive = Directive.parse('text', token.key, this, el)
1087 | } else { // html binding
1088 | el = document.createComment(config.prefix + '-html')
1089 | directive = Directive.parse('html', token.key, this, el)
1090 | }
1091 | }
1092 | } else { // a plain string
1093 | el = document.createTextNode(token)
1094 | }
1095 |
1096 | // insert node
1097 | node.parentNode.insertBefore(el, node)
1098 |
1099 | // bind directive
1100 | if (directive) {
1101 | this.bindDirective(directive)
1102 | }
1103 |
1104 | // compile partial after appending, because its children's parentNode
1105 | // will change from the fragment to the correct parentNode.
1106 | // This could affect directives that need access to its element's parentNode.
1107 | if (partialNodes) {
1108 | partialNodes.forEach(this.compile, this)
1109 | }
1110 |
1111 | }
1112 | node.parentNode.removeChild(node)
1113 | }
1114 |
1115 | /**
1116 | * Add a directive instance to the correct binding & viewmodel
1117 | */
1118 | CompilerProto.bindDirective = function (directive) {
1119 |
1120 | // keep track of it so we can unbind() later
1121 | this.dirs.push(directive)
1122 |
1123 | // for empty or literal directives, simply call its bind()
1124 | // and we're done.
1125 | if (directive.isEmpty || directive.isLiteral) {
1126 | if (directive.bind) directive.bind()
1127 | return
1128 | }
1129 |
1130 | // otherwise, we got more work to do...
1131 | var binding,
1132 | compiler = this,
1133 | key = directive.key
1134 |
1135 | if (directive.isExp) {
1136 | // expression bindings are always created on current compiler
1137 | binding = compiler.createBinding(key, true, directive.isFn)
1138 | } else {
1139 | // recursively locate which compiler owns the binding
1140 | while (compiler) {
1141 | if (compiler.hasKey(key)) {
1142 | break
1143 | } else {
1144 | compiler = compiler.parent
1145 | }
1146 | }
1147 | compiler = compiler || this
1148 | binding = compiler.bindings[key] || compiler.createBinding(key)
1149 | }
1150 | binding.dirs.push(directive)
1151 | directive.binding = binding
1152 |
1153 | var value = binding.val()
1154 | // invoke bind hook if exists
1155 | if (directive.bind) {
1156 | directive.bind(value)
1157 | }
1158 | // set initial value
1159 | directive.update(value, true)
1160 | }
1161 |
1162 | /**
1163 | * Create binding and attach getter/setter for a key to the viewmodel object
1164 | */
1165 | CompilerProto.createBinding = function (key, isExp, isFn) {
1166 |
1167 | log(' created binding: ' + key)
1168 |
1169 | var compiler = this,
1170 | bindings = compiler.bindings,
1171 | computed = compiler.options.computed,
1172 | binding = new Binding(compiler, key, isExp, isFn)
1173 |
1174 | if (isExp) {
1175 | // expression bindings are anonymous
1176 | compiler.defineExp(key, binding)
1177 | } else {
1178 | bindings[key] = binding
1179 | if (binding.root) {
1180 | // this is a root level binding. we need to define getter/setters for it.
1181 | if (computed && computed[key]) {
1182 | // computed property
1183 | compiler.defineComputed(key, binding, computed[key])
1184 | } else if (key.charAt(0) !== '$') {
1185 | // normal property
1186 | compiler.defineProp(key, binding)
1187 | } else {
1188 | compiler.defineMeta(key, binding)
1189 | }
1190 | } else {
1191 | // ensure path in data so it can be observed
1192 | Observer.ensurePath(compiler.data, key)
1193 | var parentKey = key.slice(0, key.lastIndexOf('.'))
1194 | if (!bindings[parentKey]) {
1195 | // this is a nested value binding, but the binding for its parent
1196 | // has not been created yet. We better create that one too.
1197 | compiler.createBinding(parentKey)
1198 | }
1199 | }
1200 | }
1201 | return binding
1202 | }
1203 |
1204 | /**
1205 | * Define the getter/setter for a root-level property on the VM
1206 | * and observe the initial value
1207 | */
1208 | CompilerProto.defineProp = function (key, binding) {
1209 |
1210 | var compiler = this,
1211 | data = compiler.data,
1212 | ob = data.__emitter__
1213 |
1214 | // make sure the key is present in data
1215 | // so it can be observed
1216 | if (!(key in data)) {
1217 | data[key] = undefined
1218 | }
1219 |
1220 | // if the data object is already observed, but the key
1221 | // is not observed, we need to add it to the observed keys.
1222 | if (ob && !(key in ob.values)) {
1223 | Observer.convertKey(data, key)
1224 | }
1225 |
1226 | binding.value = data[key]
1227 |
1228 | defGetSet(compiler.vm, key, {
1229 | get: function () {
1230 | return compiler.data[key]
1231 | },
1232 | set: function (val) {
1233 | compiler.data[key] = val
1234 | }
1235 | })
1236 | }
1237 |
1238 | /**
1239 | * Define a meta property, e.g. $index or $key,
1240 | * which is bindable but only accessible on the VM,
1241 | * not in the data.
1242 | */
1243 | CompilerProto.defineMeta = function (key, binding) {
1244 | var vm = this.vm,
1245 | ob = this.observer,
1246 | value = binding.value = vm[key] || this.data[key]
1247 | // remove initital meta in data, since the same piece
1248 | // of data can be observed by different VMs, each have
1249 | // its own associated meta info.
1250 | delete this.data[key]
1251 | defGetSet(vm, key, {
1252 | get: function () {
1253 | if (Observer.shouldGet) ob.emit('get', key)
1254 | return value
1255 | },
1256 | set: function (val) {
1257 | ob.emit('set', key, val)
1258 | value = val
1259 | }
1260 | })
1261 | }
1262 |
1263 | /**
1264 | * Define an expression binding, which is essentially
1265 | * an anonymous computed property
1266 | */
1267 | CompilerProto.defineExp = function (key, binding) {
1268 | var getter = ExpParser.parse(key, this)
1269 | if (getter) {
1270 | this.markComputed(binding, getter)
1271 | this.exps.push(binding)
1272 | }
1273 | }
1274 |
1275 | /**
1276 | * Define a computed property on the VM
1277 | */
1278 | CompilerProto.defineComputed = function (key, binding, value) {
1279 | this.markComputed(binding, value)
1280 | defGetSet(this.vm, key, {
1281 | get: binding.value.$get,
1282 | set: binding.value.$set
1283 | })
1284 | }
1285 |
1286 | /**
1287 | * Process a computed property binding
1288 | * so its getter/setter are bound to proper context
1289 | */
1290 | CompilerProto.markComputed = function (binding, value) {
1291 | binding.isComputed = true
1292 | // bind the accessors to the vm
1293 | if (binding.isFn) {
1294 | binding.value = value
1295 | } else {
1296 | if (typeof value === 'function') {
1297 | value = { $get: value }
1298 | }
1299 | binding.value = {
1300 | $get: utils.bind(value.$get, this.vm),
1301 | $set: value.$set
1302 | ? utils.bind(value.$set, this.vm)
1303 | : undefined
1304 | }
1305 | }
1306 | // keep track for dep parsing later
1307 | this.computed.push(binding)
1308 | }
1309 |
1310 | /**
1311 | * Retrive an option from the compiler
1312 | */
1313 | CompilerProto.getOption = function (type, id) {
1314 | var opts = this.options,
1315 | parent = this.parent,
1316 | globalAssets = config.globalAssets
1317 | return (opts[type] && opts[type][id]) || (
1318 | parent
1319 | ? parent.getOption(type, id)
1320 | : globalAssets[type] && globalAssets[type][id]
1321 | )
1322 | }
1323 |
1324 | /**
1325 | * Emit lifecycle events to trigger hooks
1326 | */
1327 | CompilerProto.execHook = function (event) {
1328 | event = 'hook:' + event
1329 | this.observer.emit(event)
1330 | this.emitter.emit(event)
1331 | }
1332 |
1333 | /**
1334 | * Check if a compiler's data contains a keypath
1335 | */
1336 | CompilerProto.hasKey = function (key) {
1337 | var baseKey = key.split('.')[0]
1338 | return hasOwn.call(this.data, baseKey) ||
1339 | hasOwn.call(this.vm, baseKey)
1340 | }
1341 |
1342 | /**
1343 | * Collect dependencies for computed properties
1344 | */
1345 | CompilerProto.parseDeps = function () {
1346 | if (!this.computed.length) return
1347 | DepsParser.parse(this.computed)
1348 | }
1349 |
1350 | /**
1351 | * Add an event delegation listener
1352 | * listeners are instances of directives with `isFn:true`
1353 | */
1354 | CompilerProto.addListener = function (listener) {
1355 | var event = listener.arg,
1356 | delegator = this.delegators[event]
1357 | if (!delegator) {
1358 | // initialize a delegator
1359 | delegator = this.delegators[event] = {
1360 | targets: [],
1361 | handler: function (e) {
1362 | var i = delegator.targets.length,
1363 | target
1364 | while (i--) {
1365 | target = delegator.targets[i]
1366 | if (target.el.contains(e.target) && target.handler) {
1367 | target.handler(e)
1368 | }
1369 | }
1370 | }
1371 | }
1372 | this.el.addEventListener(event, delegator.handler)
1373 | }
1374 | delegator.targets.push(listener)
1375 | }
1376 |
1377 | /**
1378 | * Remove an event delegation listener
1379 | */
1380 | CompilerProto.removeListener = function (listener) {
1381 | var targets = this.delegators[listener.arg].targets
1382 | targets.splice(targets.indexOf(listener), 1)
1383 | }
1384 |
1385 | /**
1386 | * Unbind and remove element
1387 | */
1388 | CompilerProto.destroy = function () {
1389 |
1390 | // avoid being called more than once
1391 | // this is irreversible!
1392 | if (this.destroyed) return
1393 |
1394 | var compiler = this,
1395 | i, key, dir, dirs, binding,
1396 | vm = compiler.vm,
1397 | el = compiler.el,
1398 | directives = compiler.dirs,
1399 | exps = compiler.exps,
1400 | bindings = compiler.bindings,
1401 | delegators = compiler.delegators,
1402 | children = compiler.children,
1403 | parent = compiler.parent
1404 |
1405 | compiler.execHook('beforeDestroy')
1406 |
1407 | // unobserve data
1408 | Observer.unobserve(compiler.data, '', compiler.observer)
1409 |
1410 | // unbind all direcitves
1411 | i = directives.length
1412 | while (i--) {
1413 | dir = directives[i]
1414 | // if this directive is an instance of an external binding
1415 | // e.g. a directive that refers to a variable on the parent VM
1416 | // we need to remove it from that binding's directives
1417 | // * empty and literal bindings do not have binding.
1418 | if (dir.binding && dir.binding.compiler !== compiler) {
1419 | dirs = dir.binding.dirs
1420 | if (dirs) dirs.splice(dirs.indexOf(dir), 1)
1421 | }
1422 | dir.unbind()
1423 | }
1424 |
1425 | // unbind all expressions (anonymous bindings)
1426 | i = exps.length
1427 | while (i--) {
1428 | exps[i].unbind()
1429 | }
1430 |
1431 | // unbind all own bindings
1432 | for (key in bindings) {
1433 | binding = bindings[key]
1434 | if (binding) {
1435 | binding.unbind()
1436 | }
1437 | }
1438 |
1439 | // remove all event delegators
1440 | for (key in delegators) {
1441 | el.removeEventListener(key, delegators[key].handler)
1442 | }
1443 |
1444 | // destroy all children
1445 | i = children.length
1446 | while (i--) {
1447 | children[i].destroy()
1448 | }
1449 |
1450 | // remove self from parent
1451 | if (parent) {
1452 | parent.children.splice(parent.children.indexOf(compiler), 1)
1453 | if (compiler.childId) {
1454 | delete parent.vm.$[compiler.childId]
1455 | }
1456 | }
1457 |
1458 | // finally remove dom element
1459 | if (el === document.body) {
1460 | el.innerHTML = ''
1461 | } else {
1462 | vm.$remove()
1463 | }
1464 | el.vue_vm = null
1465 |
1466 | this.destroyed = true
1467 | // emit destroy hook
1468 | compiler.execHook('afterDestroy')
1469 |
1470 | // finally, unregister all listeners
1471 | compiler.observer.off()
1472 | compiler.emitter.off()
1473 | }
1474 |
1475 | // Helpers --------------------------------------------------------------------
1476 |
1477 | /**
1478 | * shorthand for getting root compiler
1479 | */
1480 | function getRoot (compiler) {
1481 | while (compiler.parent) {
1482 | compiler = compiler.parent
1483 | }
1484 | return compiler
1485 | }
1486 |
1487 | /**
1488 | * for convenience & minification
1489 | */
1490 | function defGetSet (obj, key, def) {
1491 | Object.defineProperty(obj, key, def)
1492 | }
1493 |
1494 | module.exports = Compiler
1495 | },{"./binding":14,"./config":16,"./deps-parser":17,"./directive":18,"./emitter":27,"./exp-parser":28,"./observer":31,"./text-parser":32,"./utils":34}],16:[function(require,module,exports){
1496 | var prefix = 'v',
1497 | specialAttributes = [
1498 | 'pre',
1499 | 'ref',
1500 | 'with',
1501 | 'text',
1502 | 'repeat',
1503 | 'partial',
1504 | 'component',
1505 | 'animation',
1506 | 'transition',
1507 | 'effect'
1508 | ],
1509 | config = module.exports = {
1510 |
1511 | debug : false,
1512 | silent : false,
1513 | enterClass : 'v-enter',
1514 | leaveClass : 'v-leave',
1515 | interpolate : true,
1516 | attrs : {},
1517 |
1518 | get prefix () {
1519 | return prefix
1520 | },
1521 | set prefix (val) {
1522 | prefix = val
1523 | updatePrefix()
1524 | }
1525 |
1526 | }
1527 |
1528 | function updatePrefix () {
1529 | specialAttributes.forEach(function (attr) {
1530 | config.attrs[attr] = prefix + '-' + attr
1531 | })
1532 | }
1533 |
1534 | updatePrefix()
1535 | },{}],17:[function(require,module,exports){
1536 | var Emitter = require('./emitter'),
1537 | utils = require('./utils'),
1538 | Observer = require('./observer'),
1539 | catcher = new Emitter()
1540 |
1541 | /**
1542 | * Auto-extract the dependencies of a computed property
1543 | * by recording the getters triggered when evaluating it.
1544 | */
1545 | function catchDeps (binding) {
1546 | if (binding.isFn) return
1547 | utils.log('\n- ' + binding.key)
1548 | var got = utils.hash()
1549 | binding.deps = []
1550 | catcher.on('get', function (dep) {
1551 | var has = got[dep.key]
1552 | if (has && has.compiler === dep.compiler) return
1553 | got[dep.key] = dep
1554 | utils.log(' - ' + dep.key)
1555 | binding.deps.push(dep)
1556 | dep.subs.push(binding)
1557 | })
1558 | binding.value.$get()
1559 | catcher.off('get')
1560 | }
1561 |
1562 | module.exports = {
1563 |
1564 | /**
1565 | * the observer that catches events triggered by getters
1566 | */
1567 | catcher: catcher,
1568 |
1569 | /**
1570 | * parse a list of computed property bindings
1571 | */
1572 | parse: function (bindings) {
1573 | utils.log('\nparsing dependencies...')
1574 | Observer.shouldGet = true
1575 | bindings.forEach(catchDeps)
1576 | Observer.shouldGet = false
1577 | utils.log('\ndone.')
1578 | }
1579 |
1580 | }
1581 | },{"./emitter":27,"./observer":31,"./utils":34}],18:[function(require,module,exports){
1582 | var utils = require('./utils'),
1583 | directives = require('./directives'),
1584 | filters = require('./filters'),
1585 |
1586 | // Regexes!
1587 |
1588 | // regex to split multiple directive expressions
1589 | // split by commas, but ignore commas within quotes, parens and escapes.
1590 | SPLIT_RE = /(?:['"](?:\\.|[^'"])*['"]|\((?:\\.|[^\)])*\)|\\.|[^,])+/g,
1591 |
1592 | // match up to the first single pipe, ignore those within quotes.
1593 | KEY_RE = /^(?:['"](?:\\.|[^'"])*['"]|\\.|[^\|]|\|\|)+/,
1594 |
1595 | ARG_RE = /^([\w-$ ]+):(.+)$/,
1596 | FILTERS_RE = /\|[^\|]+/g,
1597 | FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g,
1598 | NESTING_RE = /^\$(parent|root)\./,
1599 | SINGLE_VAR_RE = /^[\w\.$]+$/
1600 |
1601 | /**
1602 | * Directive class
1603 | * represents a single directive instance in the DOM
1604 | */
1605 | function Directive (definition, expression, rawKey, compiler, node) {
1606 |
1607 | this.compiler = compiler
1608 | this.vm = compiler.vm
1609 | this.el = node
1610 |
1611 | var isEmpty = expression === ''
1612 |
1613 | // mix in properties from the directive definition
1614 | if (typeof definition === 'function') {
1615 | this[isEmpty ? 'bind' : '_update'] = definition
1616 | } else {
1617 | for (var prop in definition) {
1618 | if (prop === 'unbind' || prop === 'update') {
1619 | this['_' + prop] = definition[prop]
1620 | } else {
1621 | this[prop] = definition[prop]
1622 | }
1623 | }
1624 | }
1625 |
1626 | // empty expression, we're done.
1627 | if (isEmpty || this.isEmpty) {
1628 | this.isEmpty = true
1629 | return
1630 | }
1631 |
1632 | this.expression = expression.trim()
1633 | this.rawKey = rawKey
1634 |
1635 | parseKey(this, rawKey)
1636 |
1637 | this.isExp = !SINGLE_VAR_RE.test(this.key) || NESTING_RE.test(this.key)
1638 |
1639 | var filterExps = this.expression.slice(rawKey.length).match(FILTERS_RE)
1640 | if (filterExps) {
1641 | this.filters = []
1642 | for (var i = 0, l = filterExps.length, filter; i < l; i++) {
1643 | filter = parseFilter(filterExps[i], this.compiler)
1644 | if (filter) this.filters.push(filter)
1645 | }
1646 | if (!this.filters.length) this.filters = null
1647 | } else {
1648 | this.filters = null
1649 | }
1650 | }
1651 |
1652 | var DirProto = Directive.prototype
1653 |
1654 | /**
1655 | * parse a key, extract argument and nesting/root info
1656 | */
1657 | function parseKey (dir, rawKey) {
1658 | var key = rawKey
1659 | if (rawKey.indexOf(':') > -1) {
1660 | var argMatch = rawKey.match(ARG_RE)
1661 | key = argMatch
1662 | ? argMatch[2].trim()
1663 | : key
1664 | dir.arg = argMatch
1665 | ? argMatch[1].trim()
1666 | : null
1667 | }
1668 | dir.key = key
1669 | }
1670 |
1671 | /**
1672 | * parse a filter expression
1673 | */
1674 | function parseFilter (filter, compiler) {
1675 |
1676 | var tokens = filter.slice(1).match(FILTER_TOKEN_RE)
1677 | if (!tokens) return
1678 | tokens = tokens.map(function (token) {
1679 | return token.replace(/'/g, '').trim()
1680 | })
1681 |
1682 | var name = tokens[0],
1683 | apply = compiler.getOption('filters', name) || filters[name]
1684 | if (!apply) {
1685 | utils.warn('Unknown filter: ' + name)
1686 | return
1687 | }
1688 |
1689 | return {
1690 | name : name,
1691 | apply : apply,
1692 | args : tokens.length > 1
1693 | ? tokens.slice(1)
1694 | : null
1695 | }
1696 | }
1697 |
1698 | /**
1699 | * called when a new value is set
1700 | * for computed properties, this will only be called once
1701 | * during initialization.
1702 | */
1703 | DirProto.update = function (value, init) {
1704 | var type = utils.typeOf(value)
1705 | if (init || value !== this.value || type === 'Object' || type === 'Array') {
1706 | this.value = value
1707 | if (this._update) {
1708 | this._update(
1709 | this.filters
1710 | ? this.applyFilters(value)
1711 | : value,
1712 | init
1713 | )
1714 | }
1715 | }
1716 | }
1717 |
1718 | /**
1719 | * pipe the value through filters
1720 | */
1721 | DirProto.applyFilters = function (value) {
1722 | var filtered = value, filter
1723 | for (var i = 0, l = this.filters.length; i < l; i++) {
1724 | filter = this.filters[i]
1725 | filtered = filter.apply.call(this.vm, filtered, filter.args)
1726 | }
1727 | return filtered
1728 | }
1729 |
1730 | /**
1731 | * Unbind diretive
1732 | */
1733 | DirProto.unbind = function () {
1734 | // this can be called before the el is even assigned...
1735 | if (!this.el || !this.vm) return
1736 | if (this._unbind) this._unbind()
1737 | this.vm = this.el = this.binding = this.compiler = null
1738 | }
1739 |
1740 | // exposed methods ------------------------------------------------------------
1741 |
1742 | /**
1743 | * split a unquoted-comma separated expression into
1744 | * multiple clauses
1745 | */
1746 | Directive.split = function (exp) {
1747 | return exp.indexOf(',') > -1
1748 | ? exp.match(SPLIT_RE) || ['']
1749 | : [exp]
1750 | }
1751 |
1752 | /**
1753 | * make sure the directive and expression is valid
1754 | * before we create an instance
1755 | */
1756 | Directive.parse = function (dirname, expression, compiler, node) {
1757 |
1758 | var dir = compiler.getOption('directives', dirname) || directives[dirname]
1759 | if (!dir) return utils.warn('unknown directive: ' + dirname)
1760 |
1761 | var rawKey
1762 | if (expression.indexOf('|') > -1) {
1763 | var keyMatch = expression.match(KEY_RE)
1764 | if (keyMatch) {
1765 | rawKey = keyMatch[0].trim()
1766 | }
1767 | } else {
1768 | rawKey = expression.trim()
1769 | }
1770 |
1771 | // have a valid raw key, or be an empty directive
1772 | return (rawKey || expression === '')
1773 | ? new Directive(dir, expression, rawKey, compiler, node)
1774 | : utils.warn('invalid directive expression: ' + expression)
1775 | }
1776 |
1777 | module.exports = Directive
1778 | },{"./directives":21,"./filters":29,"./utils":34}],19:[function(require,module,exports){
1779 | var toText = require('../utils').toText,
1780 | slice = [].slice
1781 |
1782 | module.exports = {
1783 |
1784 | bind: function () {
1785 | // a comment node means this is a binding for
1786 | // {{{ inline unescaped html }}}
1787 | if (this.el.nodeType === 8) {
1788 | // hold nodes
1789 | this.holder = document.createElement('div')
1790 | this.nodes = []
1791 | }
1792 | },
1793 |
1794 | update: function (value) {
1795 | value = toText(value)
1796 | if (this.holder) {
1797 | this.swap(value)
1798 | } else {
1799 | this.el.innerHTML = value
1800 | }
1801 | },
1802 |
1803 | swap: function (value) {
1804 | var parent = this.el.parentNode,
1805 | holder = this.holder,
1806 | nodes = this.nodes,
1807 | i = nodes.length, l
1808 | while (i--) {
1809 | parent.removeChild(nodes[i])
1810 | }
1811 | holder.innerHTML = value
1812 | nodes = this.nodes = slice.call(holder.childNodes)
1813 | for (i = 0, l = nodes.length; i < l; i++) {
1814 | parent.insertBefore(nodes[i], this.el)
1815 | }
1816 | }
1817 | }
1818 | },{"../utils":34}],20:[function(require,module,exports){
1819 | var config = require('../config'),
1820 | transition = require('../transition')
1821 |
1822 | module.exports = {
1823 |
1824 | bind: function () {
1825 | this.parent = this.el.parentNode || this.el.vue_if_parent
1826 | this.ref = document.createComment(config.prefix + '-if-' + this.key)
1827 | var detachedRef = this.el.vue_if_ref
1828 | if (detachedRef) {
1829 | this.parent.insertBefore(this.ref, detachedRef)
1830 | }
1831 | this.el.vue_if_ref = this.ref
1832 | },
1833 |
1834 | update: function (value) {
1835 |
1836 | var el = this.el
1837 |
1838 | // sometimes we need to create a VM on a detached node,
1839 | // e.g. in v-repeat. In that case, store the desired v-if
1840 | // state on the node itself so we can deal with it elsewhere.
1841 | el.vue_if = !!value
1842 |
1843 | var parent = this.parent,
1844 | ref = this.ref,
1845 | compiler = this.compiler
1846 |
1847 | if (!parent) {
1848 | if (!el.parentNode) {
1849 | return
1850 | } else {
1851 | parent = this.parent = el.parentNode
1852 | }
1853 | }
1854 |
1855 | if (!value) {
1856 | transition(el, -1, remove, compiler)
1857 | } else {
1858 | transition(el, 1, insert, compiler)
1859 | }
1860 |
1861 | function remove () {
1862 | if (!el.parentNode) return
1863 | // insert the reference node
1864 | var next = el.nextSibling
1865 | if (next) {
1866 | parent.insertBefore(ref, next)
1867 | } else {
1868 | parent.appendChild(ref)
1869 | }
1870 | parent.removeChild(el)
1871 | }
1872 |
1873 | function insert () {
1874 | if (el.parentNode) return
1875 | parent.insertBefore(el, ref)
1876 | parent.removeChild(ref)
1877 | }
1878 | },
1879 |
1880 | unbind: function () {
1881 | this.el.vue_if_ref = this.el.vue_if_parent = null
1882 | var ref = this.ref
1883 | if (ref.parentNode) {
1884 | ref.parentNode.removeChild(ref)
1885 | }
1886 | }
1887 | }
1888 | },{"../config":16,"../transition":33}],21:[function(require,module,exports){
1889 | var utils = require('../utils'),
1890 | config = require('../config'),
1891 | transition = require('../transition')
1892 |
1893 | module.exports = {
1894 |
1895 | on : require('./on'),
1896 | repeat : require('./repeat'),
1897 | model : require('./model'),
1898 | 'if' : require('./if'),
1899 | 'with' : require('./with'),
1900 | html : require('./html'),
1901 | style : require('./style'),
1902 |
1903 | attr: function (value) {
1904 | if (value || value === 0) {
1905 | this.el.setAttribute(this.arg, value)
1906 | } else {
1907 | this.el.removeAttribute(this.arg)
1908 | }
1909 | },
1910 |
1911 | text: function (value) {
1912 | this.el.textContent = utils.toText(value)
1913 | },
1914 |
1915 | show: function (value) {
1916 | var el = this.el,
1917 | target = value ? '' : 'none',
1918 | change = function () {
1919 | el.style.display = target
1920 | }
1921 | transition(el, value ? 1 : -1, change, this.compiler)
1922 | },
1923 |
1924 | 'class': function (value) {
1925 | if (this.arg) {
1926 | utils[value ? 'addClass' : 'removeClass'](this.el, this.arg)
1927 | } else {
1928 | if (this.lastVal) {
1929 | utils.removeClass(this.el, this.lastVal)
1930 | }
1931 | if (value) {
1932 | utils.addClass(this.el, value)
1933 | this.lastVal = value
1934 | }
1935 | }
1936 | },
1937 |
1938 | cloak: {
1939 | isEmpty: true,
1940 | bind: function () {
1941 | var el = this.el
1942 | this.compiler.observer.once('hook:ready', function () {
1943 | el.removeAttribute(config.prefix + '-cloak')
1944 | })
1945 | }
1946 | }
1947 |
1948 | }
1949 | },{"../config":16,"../transition":33,"../utils":34,"./html":19,"./if":20,"./model":22,"./on":23,"./repeat":24,"./style":25,"./with":26}],22:[function(require,module,exports){
1950 | var utils = require('../utils'),
1951 | isIE9 = navigator.userAgent.indexOf('MSIE 9.0') > 0,
1952 | filter = [].filter
1953 |
1954 | /**
1955 | * Returns an array of values from a multiple select
1956 | */
1957 | function getMultipleSelectOptions (select) {
1958 | return filter
1959 | .call(select.options, function (option) {
1960 | return option.selected
1961 | })
1962 | .map(function (option) {
1963 | return option.value || option.text
1964 | })
1965 | }
1966 |
1967 | module.exports = {
1968 |
1969 | bind: function () {
1970 |
1971 | var self = this,
1972 | el = self.el,
1973 | type = el.type,
1974 | tag = el.tagName
1975 |
1976 | self.lock = false
1977 | self.ownerVM = self.binding.compiler.vm
1978 |
1979 | // determine what event to listen to
1980 | self.event =
1981 | (self.compiler.options.lazy ||
1982 | tag === 'SELECT' ||
1983 | type === 'checkbox' || type === 'radio')
1984 | ? 'change'
1985 | : 'input'
1986 |
1987 | // determine the attribute to change when updating
1988 | self.attr = type === 'checkbox'
1989 | ? 'checked'
1990 | : (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA')
1991 | ? 'value'
1992 | : 'innerHTML'
1993 |
1994 | // select[multiple] support
1995 | if(tag === 'SELECT' && el.hasAttribute('multiple')) {
1996 | this.multi = true
1997 | }
1998 |
1999 | var compositionLock = false
2000 | self.cLock = function () {
2001 | compositionLock = true
2002 | }
2003 | self.cUnlock = function () {
2004 | compositionLock = false
2005 | }
2006 | el.addEventListener('compositionstart', this.cLock)
2007 | el.addEventListener('compositionend', this.cUnlock)
2008 |
2009 | // attach listener
2010 | self.set = self.filters
2011 | ? function () {
2012 | if (compositionLock) return
2013 | // if this directive has filters
2014 | // we need to let the vm.$set trigger
2015 | // update() so filters are applied.
2016 | // therefore we have to record cursor position
2017 | // so that after vm.$set changes the input
2018 | // value we can put the cursor back at where it is
2019 | var cursorPos
2020 | try { cursorPos = el.selectionStart } catch (e) {}
2021 |
2022 | self._set()
2023 |
2024 | // since updates are async
2025 | // we need to reset cursor position async too
2026 | utils.nextTick(function () {
2027 | if (cursorPos !== undefined) {
2028 | el.setSelectionRange(cursorPos, cursorPos)
2029 | }
2030 | })
2031 | }
2032 | : function () {
2033 | if (compositionLock) return
2034 | // no filters, don't let it trigger update()
2035 | self.lock = true
2036 |
2037 | self._set()
2038 |
2039 | utils.nextTick(function () {
2040 | self.lock = false
2041 | })
2042 | }
2043 | el.addEventListener(self.event, self.set)
2044 |
2045 | // fix shit for IE9
2046 | // since it doesn't fire input on backspace / del / cut
2047 | if (isIE9) {
2048 | self.onCut = function () {
2049 | // cut event fires before the value actually changes
2050 | utils.nextTick(function () {
2051 | self.set()
2052 | })
2053 | }
2054 | self.onDel = function (e) {
2055 | if (e.keyCode === 46 || e.keyCode === 8) {
2056 | self.set()
2057 | }
2058 | }
2059 | el.addEventListener('cut', self.onCut)
2060 | el.addEventListener('keyup', self.onDel)
2061 | }
2062 | },
2063 |
2064 | _set: function () {
2065 | this.ownerVM.$set(
2066 | this.key, this.multi
2067 | ? getMultipleSelectOptions(this.el)
2068 | : this.el[this.attr]
2069 | )
2070 | },
2071 |
2072 | update: function (value, init) {
2073 | /* jshint eqeqeq: false */
2074 | // sync back inline value if initial data is undefined
2075 | if (init && value === undefined) {
2076 | return this._set()
2077 | }
2078 | if (this.lock) return
2079 | var el = this.el
2080 | if (el.tagName === 'SELECT') { // select dropdown
2081 | el.selectedIndex = -1
2082 | if(this.multi && Array.isArray(value)) {
2083 | value.forEach(this.updateSelect, this)
2084 | } else {
2085 | this.updateSelect(value)
2086 | }
2087 | } else if (el.type === 'radio') { // radio button
2088 | el.checked = value == el.value
2089 | } else if (el.type === 'checkbox') { // checkbox
2090 | el.checked = !!value
2091 | } else {
2092 | el[this.attr] = utils.toText(value)
2093 | }
2094 | },
2095 |
2096 | updateSelect: function (value) {
2097 | /* jshint eqeqeq: false */
2098 | // setting