Просмотр исходного кода

Merge branch 'dev' of https://github.com/yyx990803/vue into dev

fergaldoyle 10 лет назад
Родитель
Сommit
b73e77c883

+ 1 - 1
CONTRIBUTING.md

@@ -81,4 +81,4 @@ The default task (by simply running `grunt`) will do the following: lint -> buil
 
 The unit tests are written with Jasmine and run with Karma. The e2e tests are written for and run with CasperJS.
 
-Note that the unit tests will automatically be run in Chrome, Firefox and Safari. If you are not on a Mac, or don't have one of the browsers installed on your system, you can modify the [karma config in gruntfile.js](https://github.com/yyx990803/vue/blob/dev/gruntfile.js#L42) to only run Karma tests in browsers that are available on your system. Just make sure don’t check in the gruntfile changes for the commit.
+Note that the unit tests will automatically be run in Chrome, Firefox and Safari. If you are not on a Mac, or don't have one of the browsers installed on your system, you can modify the [karma config in gruntfile.js](https://github.com/yyx990803/vue/blob/dev/gruntfile.js#L38) to only run Karma tests in browsers that are available on your system. Just make sure don’t check in the gruntfile changes for the commit.

+ 1 - 1
build/grunt-tasks/casper.js

@@ -9,7 +9,7 @@ module.exports = function (grunt) {
     var file = id ? id + '.js' : ''
     // let casperjs use local phantomjs
     process.env.PHANTOMJS_EXECUTABLE =
-      '../../node_modules/karma-phantomjs-launcher/node_modules/.bin/phantomjs'
+      '../../node_modules/.bin/phantomjs'
     grunt.util.spawn({
       cmd: '../../node_modules/.bin/casperjs',
       args: ['test', '--concise', './' + file],

+ 1 - 1
component.json

@@ -1,6 +1,6 @@
 {
   "name": "vue",
-  "version": "0.12.9",
+  "version": "0.12.10",
   "main": "src/vue.js",
   "author": "Evan You <yyx990803@gmail.com>",
   "description": "Simple, Fast & Composable MVVM for building interative interfaces",

Разница между файлами не показана из-за своего большого размера
+ 1092 - 1567
dist/vue.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
dist/vue.min.js


+ 6 - 1
gruntfile.js

@@ -84,7 +84,12 @@ module.exports = function (grunt) {
   grunt.registerTask('cover', ['karma:coverage'])
   grunt.registerTask('test', ['unit', 'cover', 'casper'])
   grunt.registerTask('sauce', ['karma:sauce1', 'karma:sauce2', 'karma:sauce3'])
-  grunt.registerTask('ci', ['eslint', 'cover', 'codecov', 'build', 'casper', 'sauce'])
   grunt.registerTask('default', ['eslint', 'build', 'test'])
 
+  // CI
+  if (process.env.CI_PULL_REQUEST) {
+    grunt.registerTask('ci', ['eslint', 'cover', 'build', 'casper'])
+  } else {
+    grunt.registerTask('ci', ['eslint', 'cover', 'codecov', 'build', 'casper', 'sauce'])
+  }
 }

+ 15 - 14
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vue",
-  "version": "0.12.9",
+  "version": "0.12.10",
   "author": "Evan You <yyx990803@gmail.com>",
   "license": "MIT",
   "description": "Simple, Fast & Composable MVVM for building interative interfaces",
@@ -32,21 +32,22 @@
     "casperjs": "^1.1.0-beta3",
     "codecov.io": "^0.1.2",
     "grunt": "^0.4.5",
-    "grunt-eslint": "^17.0.0",
-    "grunt-karma": "^0.8.3",
+    "grunt-eslint": "^17.1.0",
+    "grunt-karma": "^0.12.0",
     "jasmine-core": "^2.3.4",
-    "karma": "^0.12.31",
-    "karma-chrome-launcher": "^0.1.7",
-    "karma-commonjs": "^0.0.10",
-    "karma-coverage": "^0.2.7",
-    "karma-firefox-launcher": "^0.1.4",
-    "karma-jasmine": "^0.3.5",
-    "karma-phantomjs-launcher": "^0.1.4",
+    "karma": "^0.13.8",
+    "karma-chrome-launcher": "^0.2.0",
+    "karma-commonjs": "^0.0.13",
+    "karma-coverage": "^0.5.0",
+    "karma-firefox-launcher": "^0.1.6",
+    "karma-jasmine": "^0.3.6",
+    "karma-phantomjs-launcher": "^0.2.1",
     "karma-safari-launcher": "^0.1.1",
-    "karma-sauce-launcher": "^0.2.10",
-    "semver": "^4.2.0",
+    "karma-sauce-launcher": "^0.2.14",
+    "phantomjs": "^1.9.17",
+    "semver": "^5.0.1",
     "shell-task": "^1.0.0",
-    "uglify-js": "^2.4.20",
-    "webpack": "^1.9.10"
+    "uglify-js": "^2.4.24",
+    "webpack": "^1.11.0"
   }
 }

+ 6 - 0
src/directive.js

@@ -198,10 +198,16 @@ p._teardown = function () {
  */
 
 p.set = function (value) {
+  /* istanbul ignore else */
   if (this.twoWay) {
     this._withLock(function () {
       this._watcher.set(value)
     })
+  } else if (process.env.NODE_ENV !== 'production') {
+    _.warn(
+      'Directive.set() can only be used inside twoWay' +
+      'directives.'
+    )
   }
 }
 

+ 51 - 35
src/directives/component.js

@@ -28,7 +28,7 @@ module.exports = {
       // cache object, with its constructor id as the key.
       this.keepAlive = this._checkParam('keep-alive') != null
       // wait for event before insertion
-      this.readyEvent = this._checkParam('wait-for')
+      this.waitForEvent = this._checkParam('wait-for')
       // check ref
       this.refID = this._checkParam(config.prefix + 'ref')
       if (this.keepAlive) {
@@ -41,7 +41,6 @@ module.exports = {
       }
       // component resolution related state
       this.pendingComponentCb =
-      this.componentID =
       this.Component = null
       // transition related state
       this.pendingRemovals = 0
@@ -66,15 +65,23 @@ module.exports = {
    */
 
   initStatic: function () {
-    var child = this.build()
+    // wait-for
     var anchor = this.anchor
+    var options
+    var waitFor = this.waitForEvent
+    if (waitFor) {
+      options = {
+        created: function () {
+          this.$once(waitFor, function () {
+            this.$before(anchor)
+          })
+        }
+      }
+    }
+    var child = this.build(options)
     this.setCurrent(child)
-    if (!this.readyEvent) {
+    if (!this.waitForEvent) {
       child.$before(anchor)
-    } else {
-      child.$once(this.readyEvent, function () {
-        child.$before(anchor)
-      })
     }
   },
 
@@ -93,32 +100,38 @@ module.exports = {
    * specified transition mode. Accepts a few additional
    * arguments specifically for vue-router.
    *
+   * The callback is called when the full transition is
+   * finished.
+   *
    * @param {String} value
-   * @param {Object} data
-   * @param {Function} afterBuild
-   * @param {Function} afterTransition
+   * @param {Function} [cb]
    */
 
-  setComponent: function (value, data, afterBuild, afterTransition) {
+  setComponent: function (value, cb) {
     this.invalidatePending()
     if (!value) {
       // just remove current
       this.unbuild(true)
-      this.remove(this.childVM, afterTransition)
+      this.remove(this.childVM, cb)
       this.unsetCurrent()
     } else {
       this.resolveComponent(value, _.bind(function () {
         this.unbuild(true)
-        var newComponent = this.build(data)
-        /* istanbul ignore if */
-        if (afterBuild) afterBuild(newComponent)
+        var options
         var self = this
-        if (this.readyEvent) {
-          newComponent.$once(this.readyEvent, function () {
-            self.transition(newComponent, afterTransition)
-          })
-        } else {
-          this.transition(newComponent, afterTransition)
+        var waitFor = this.waitForEvent
+        if (waitFor) {
+          options = {
+            created: function () {
+              this.$once(waitFor, function () {
+                self.transition(this, cb)
+              })
+            }
+          }
+        }
+        var newComponent = this.build(options)
+        if (!waitFor) {
+          this.transition(newComponent, cb)
         }
       }, this))
     }
@@ -131,9 +144,8 @@ module.exports = {
 
   resolveComponent: function (id, cb) {
     var self = this
-    this.pendingComponentCb = _.cancellable(function (component) {
-      self.componentID = id
-      self.Component = component
+    this.pendingComponentCb = _.cancellable(function (Component) {
+      self.Component = Component
       cb()
     })
     this.vm._resolveComponent(id, this.pendingComponentCb)
@@ -157,23 +169,21 @@ module.exports = {
    * If keep alive and has cached instance, insert that
    * instance; otherwise build a new one and cache it.
    *
-   * @param {Object} [data]
+   * @param {Object} [extraOptions]
    * @return {Vue} - the created instance
    */
 
-  build: function (data) {
+  build: function (extraOptions) {
     if (this.keepAlive) {
-      var cached = this.cache[this.componentID]
+      var cached = this.cache[this.Component.cid]
       if (cached) {
         return cached
       }
     }
     if (this.Component) {
-      var parent = this._host || this.vm
-      var el = templateParser.clone(this.el)
-      var child = parent.$addChild({
-        el: el,
-        data: data,
+      // default options
+      var options = {
+        el: templateParser.clone(this.el),
         template: this.template,
         // if no inline-template, then the compiled
         // linker can be cached for better performance.
@@ -181,9 +191,15 @@ module.exports = {
         _asComponent: true,
         _isRouterView: this._isRouterView,
         _context: this.vm
-      }, this.Component)
+      }
+      // extra options
+      if (extraOptions) {
+        _.extend(options, extraOptions)
+      }
+      var parent = this._host || this.vm
+      var child = parent.$addChild(options, this.Component)
       if (this.keepAlive) {
-        this.cache[this.componentID] = child
+        this.cache[this.Component.cid] = child
       }
       return child
     }

+ 45 - 55
src/directives/model/text.js

@@ -5,6 +5,7 @@ module.exports = {
   bind: function () {
     var self = this
     var el = this.el
+    var isRange = el.type === 'range'
 
     // check params
     // - lazy: update model on "change" instead of "input"
@@ -22,7 +23,7 @@ module.exports = {
     // Chinese, but instead triggers them for spelling
     // suggestions... (see Discussion/#162)
     var composing = false
-    if (!_.isAndroid) {
+    if (!_.isAndroid && !isRange) {
       this.onComposeStart = function () {
         composing = true
       }
@@ -37,63 +38,38 @@ module.exports = {
       _.on(el, 'compositionend', this.onComposeEnd)
     }
 
-    function syncToModel () {
-      var val = number
-        ? _.toNumber(el.value)
-        : el.value
-      self.set(val)
-    }
-
-    // if the directive has filters, we need to
-    // record cursor position and restore it after updating
-    // the input with the filtered value.
-    // also force update for type="range" inputs to enable
-    // "lock in range" (see #506)
-    if (this.hasRead || el.type === 'range') {
-      this.listener = function () {
-        if (composing) return
-        var charsOffset
-        // some HTML5 input types throw error here
-        try {
-          // record how many chars from the end of input
-          // the cursor was at
-          charsOffset = el.value.length - el.selectionStart
-        } catch (e) {}
-        // Fix IE10/11 infinite update cycle
-        // https://github.com/yyx990803/vue/issues/592
-        /* istanbul ignore if */
-        if (charsOffset < 0) {
-          return
-        }
-        syncToModel()
-        _.nextTick(function () {
-          // force a value update, because in
-          // certain cases the write filters output the
-          // same result for different input values, and
-          // the Observer set events won't be triggered.
-          var newVal = self._watcher.value
-          self.update(newVal)
-          if (charsOffset != null) {
-            var cursorPos =
-              _.toString(newVal).length - charsOffset
-            el.setSelectionRange(cursorPos, cursorPos)
-          }
-        })
+    // prevent messing with the input when user is typing,
+    // and force update on blur.
+    this.focused = false
+    if (!isRange) {
+      this.onFocus = function () {
+        self.focused = true
       }
-    } else {
-      this.listener = function () {
-        if (composing) return
-        syncToModel()
+      this.onBlur = function () {
+        self.focused = false
+        self.listener()
       }
+      _.on(el, 'focus', this.onFocus)
+      _.on(el, 'blur', this.onBlur)
     }
 
+    // Now attach the main listener
+    this.listener = function () {
+      if (composing) return
+      var val = number || isRange
+        ? _.toNumber(el.value)
+        : el.value
+      self.set(val)
+      // force update here, because the watcher may not
+      // run when the value is the same.
+      _.nextTick(function () {
+        self.update(self._watcher.value)
+      })
+    }
     if (debounce) {
       this.listener = _.debounce(this.listener, debounce)
     }
 
-    // Now attach the main listener
-
-    this.event = lazy ? 'change' : 'input'
     // Support jQuery events, since jQuery.trigger() doesn't
     // trigger native events in some cases and some plugins
     // rely on $.trigger()
@@ -106,9 +82,15 @@ module.exports = {
     // jQuery variable in tests.
     this.hasjQuery = typeof jQuery === 'function'
     if (this.hasjQuery) {
-      jQuery(el).on(this.event, this.listener)
+      jQuery(el).on('change', this.listener)
+      if (!lazy) {
+        jQuery(el).on('input', this.listener)
+      }
     } else {
-      _.on(el, this.event, this.listener)
+      _.on(el, 'change', this.listener)
+      if (!lazy) {
+        _.on(el, 'input', this.listener)
+      }
     }
 
     // IE9 doesn't fire input event on backspace/del/cut
@@ -137,15 +119,19 @@ module.exports = {
   },
 
   update: function (value) {
-    this.el.value = _.toString(value)
+    if (!this.focused) {
+      this.el.value = _.toString(value)
+    }
   },
 
   unbind: function () {
     var el = this.el
     if (this.hasjQuery) {
-      jQuery(el).off(this.event, this.listener)
+      jQuery(el).off('change', this.listener)
+      jQuery(el).off('input', this.listener)
     } else {
-      _.off(el, this.event, this.listener)
+      _.off(el, 'change', this.listener)
+      _.off(el, 'input', this.listener)
     }
     if (this.onComposeStart) {
       _.off(el, 'compositionstart', this.onComposeStart)
@@ -155,5 +141,9 @@ module.exports = {
       _.off(el, 'cut', this.onCut)
       _.off(el, 'keyup', this.onDel)
     }
+    if (this.onFocus) {
+      _.off(el, 'focus', this.onFocus)
+      _.off(el, 'blur', this.onBlur)
+    }
   }
 }

+ 9 - 1
src/filters/index.js

@@ -61,7 +61,7 @@ var digitsRE = /(\d{3})(?=\d)/g
 exports.currency = function (value, currency) {
   value = parseFloat(value)
   if (!isFinite(value) || (!value && value !== 0)) return ''
-  currency = currency || '$'
+  currency = currency != null ? currency : '$'
   var stringified = Math.abs(value).toFixed(2)
   var _int = stringified.slice(0, -3)
   var i = _int.length % 3
@@ -130,6 +130,14 @@ exports.key = function (handler, key) {
 // expose keycode hash
 exports.key.keyCodes = keyCodes
 
+exports.debounce = function (handler, delay) {
+  if (!handler) return
+  if (!delay) {
+    delay = 300
+  }
+  return _.debounce(handler, delay)
+}
+
 /**
  * Install special array filters
  */

+ 1 - 1
src/instance/scope.js

@@ -36,7 +36,7 @@ exports._initProps = function () {
   }
   // make sure to convert string selectors into element now
   el = options.el = _.query(el)
-  this._propsUnlinkFn = el && props
+  this._propsUnlinkFn = el && el.nodeType === 1 && props
     ? compiler.compileAndLinkProps(
         this, el, props
       )

+ 4 - 3
src/parsers/directive.js

@@ -2,7 +2,7 @@ var _ = require('../util')
 var Cache = require('../cache')
 var cache = new Cache(1000)
 var argRE = /^[^\{\?]+$|^'[^']*'$|^"[^"]*"$/
-var filterTokenRE = /[^\s'"]+|'[^']+'|"[^"]+"/g
+var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g
 var reservedArgRE = /^in$|^-?\d+/
 
 /**
@@ -71,9 +71,10 @@ function processFilterArg (arg) {
   var stripped = reservedArgRE.test(arg)
     ? arg
     : _.stripQuotes(arg)
+  var dynamic = stripped === false
   return {
-    value: stripped || arg,
-    dynamic: !stripped
+    value: dynamic ? arg : stripped,
+    dynamic: dynamic
   }
 }
 

+ 2 - 6
test/unit/specs/api/global_spec.js

@@ -62,12 +62,8 @@ describe('Global API', function () {
     var Test = Vue.extend()
 
     it('directive / elementDirective / filter / transition', function () {
-      [
-        'directive',
-        'elementDirective',
-        'filter',
-        'transition'
-      ].forEach(function (type) {
+      var assets = ['directive', 'elementDirective', 'filter', 'transition']
+      assets.forEach(function (type) {
         var def = {}
         Test[type]('test', def)
         expect(Test.options[type + 's'].test).toBe(def)

+ 4 - 4
test/unit/specs/async_component_spec.js

@@ -111,8 +111,8 @@ describe('Async components', function () {
       vm.view = 'view-b'
       function step1 () {
         // called after A resolves, but A should have been
-        // invalidated so not cotrId should be set
-        expect(vm._directives[0].componentID).toBe(null)
+        // invalidated so no Ctor should be set
+        expect(vm._directives[0].Component).toBe(null)
       }
       function step2 () {
         // B should resolve successfully
@@ -145,8 +145,8 @@ describe('Async components', function () {
       vm.$destroy()
       function next () {
         // called after A resolves, but A should have been
-        // invalidated so not cotrId should be set
-        expect(dir.componentID).toBe(null)
+        // invalidated so no Ctor should be set
+        expect(dir.Component).toBe(null)
         done()
       }
     })

+ 17 - 0
test/unit/specs/directives/component_spec.js

@@ -278,6 +278,23 @@ if (_.inBrowser) {
       expect(el.textContent).toBe('AAA')
     })
 
+    it('sync wait-for inside compiled hook', function () {
+      new Vue({
+        el: el,
+        template: '<view-a wait-for="ok"></view-a>',
+        components: {
+          'view-a': {
+            template: 'AAA',
+            compiled: function () {
+              expect(el.textContent).toBe('')
+              this.$emit('ok')
+            }
+          }
+        }
+      })
+      expect(el.textContent).toBe('AAA')
+    })
+
     it('wait-for for dynamic components', function (done) {
       var vm = new Vue({
         el: el,

+ 16 - 6
test/unit/specs/directives/model_spec.js

@@ -536,17 +536,21 @@ if (_.inBrowser) {
         template: '<input v-model="test | uppercase | test">'
       })
       expect(el.firstChild.value).toBe('B')
+      trigger(el.firstChild, 'focus')
       el.firstChild.value = 'cc'
       trigger(el.firstChild, 'input')
       _.nextTick(function () {
-        expect(el.firstChild.value).toBe('CC')
+        expect(el.firstChild.value).toBe('cc')
         expect(vm.test).toBe('cc')
-        done()
+        trigger(el.firstChild, 'blur')
+        _.nextTick(function () {
+          expect(el.firstChild.value).toBe('CC')
+          expect(vm.test).toBe('cc')
+          done()
+        })
       })
     })
 
-    // when there's only write filter, should allow
-    // out of sync between the input field and actual data
     it('text with only write filter', function (done) {
       var vm = new Vue({
         el: el,
@@ -562,12 +566,18 @@ if (_.inBrowser) {
         },
         template: '<input v-model="test | test">'
       })
+      trigger(el.firstChild, 'focus')
       el.firstChild.value = 'cc'
       trigger(el.firstChild, 'input')
       _.nextTick(function () {
         expect(el.firstChild.value).toBe('cc')
         expect(vm.test).toBe('CC')
-        done()
+        trigger(el.firstChild, 'blur')
+        _.nextTick(function () {
+          expect(el.firstChild.value).toBe('CC')
+          expect(vm.test).toBe('CC')
+          done()
+        })
       })
     })
 
@@ -703,7 +713,7 @@ if (_.inBrowser) {
         data: {
           test: 'b'
         },
-        template: '<input v-model="test" lazy>'
+        template: '<input v-model="test">'
       })
       expect(el.firstChild.value).toBe('b')
       vm.test = 'a'

+ 27 - 0
test/unit/specs/filters/filters_spec.js

@@ -66,6 +66,8 @@ describe('Filters', function () {
     expect(filter(0.76)).toBe('$0.76')
     // sign arg
     expect(filter(2134, '@')).toBe('@2,134.00')
+    // no symbol
+    expect(filter(2134, '')).toBe('2,134.00')
     // falsy, infinity and 0
     expect(filter(0)).toBe('$0.00')
     expect(filter(false)).toBe('')
@@ -96,6 +98,31 @@ describe('Filters', function () {
     expect(spy).toHaveBeenCalled()
   })
 
+  it('debounce', function (done) {
+    var filter = filters.debounce
+    expect(filter(null)).toBeUndefined()
+    var spy = jasmine.createSpy('filter:debounce')
+    var handler = filter(spy)
+    handler()
+    expect(spy).not.toHaveBeenCalled()
+    handler = filter(spy)
+    handler()
+    setTimeout(function () {
+      expect(spy).toHaveBeenCalled()
+    }, 400)
+    var spy2 = jasmine.createSpy('filter:debounce')
+    handler = filter(spy2, 450)
+    handler()
+    handler()
+    setTimeout(function () {
+      expect(spy2).not.toHaveBeenCalled()
+    }, 400)
+    setTimeout(function () {
+      expect(spy2.calls.count()).toBe(1)
+      done()
+    }, 500)
+  })
+
   it('filterBy', function () {
     var filter = filters.filterBy
     var arr = [

+ 5 - 3
test/unit/specs/parsers/directive_spec.js

@@ -18,18 +18,20 @@ describe('Directive Parser', function () {
   })
 
   it('with filters', function () {
-    var res = parse(' arg : exp | abc de \'ok\' | bcd')
+    var res = parse(' arg : exp | abc de \'ok\' \'\' | bcd')
     expect(res.length).toBe(1)
     expect(res[0].expression).toBe('exp')
     expect(res[0].arg).toBe('arg')
-    expect(res[0].raw).toBe('arg : exp | abc de \'ok\' | bcd')
+    expect(res[0].raw).toBe('arg : exp | abc de \'ok\' \'\' | bcd')
     expect(res[0].filters.length).toBe(2)
     expect(res[0].filters[0].name).toBe('abc')
-    expect(res[0].filters[0].args.length).toBe(2)
+    expect(res[0].filters[0].args.length).toBe(3)
     expect(res[0].filters[0].args[0].value).toBe('de')
     expect(res[0].filters[0].args[0].dynamic).toBe(true)
     expect(res[0].filters[0].args[1].value).toBe('ok')
     expect(res[0].filters[0].args[1].dynamic).toBe(false)
+    expect(res[0].filters[0].args[2].value).toBe('')
+    expect(res[0].filters[0].args[2].dynamic).toBe(false)
     expect(res[0].filters[1].name).toBe('bcd')
     expect(res[0].filters[1].args).toBeUndefined()
   })

Некоторые файлы не были показаны из-за большого количества измененных файлов