import {
extend,
bind,
on,
off,
getAttr,
getBindAttr,
camelize,
hyphenate,
nextTick,
warn
} from './util/index'
import Watcher from './watcher'
import { parseExpression, isSimplePath } from './parsers/expression'
function noop () {}
/**
* A directive links a DOM element with a piece of data,
* which is the result of evaluating an expression.
* It registers a watcher with the expression and calls
* the DOM update function when a change is triggered.
*
* @param {Object} descriptor
* - {String} name
* - {Object} def
* - {String} expression
* - {Array<Object>} [filters]
* - {Object} [modifiers]
* - {Boolean} literal
* - {String} attr
* - {String} arg
* - {String} raw
* - {String} [ref]
* - {Array<Object>} [interp]
* - {Boolean} [hasOneTime]
* @param {Vue} vm
* @param {Node} el
* @param {Vue} [host] - transclusion host component
* @param {Object} [scope] - v-for scope
* @param {Fragment} [frag] - owner fragment
* @constructor
*/
export default function Directive (descriptor, vm, el, host, scope, frag) {
this.vm = vm
this.el = el
// copy descriptor properties
this.descriptor = descriptor
this.name = descriptor.name
this.expression = descriptor.expression
this.arg = descriptor.arg
this.modifiers = descriptor.modifiers
this.filters = descriptor.filters
this.literal = this.modifiers && this.modifiers.literal
// private
this._locked = false
this._bound = false
this._listeners = null
// link context
this._host = host
this._scope = scope
this._frag = frag
// store directives on node in dev mode
if (process.env.NODE_ENV !== 'production' && this.el) {
this.el._vue_directives = this.el._vue_directives || []
this.el._vue_directives.push(this)
}
}
/**
* Initialize the directive, mixin definition properties,
* setup the watcher, call definition bind() and update()
* if present.
*/
Directive.prototype._bind = function () {
var name = this.name
var descriptor = this.descriptor
// remove attribute
if (
(name !== 'cloak' || this.vm._isCompiled) &&
this.el && this.el.removeAttribute
) {
var attr = descriptor.attr || ('v-' + name)
this.el.removeAttribute(attr)
}
// copy def properties
var def = descriptor.def
if (typeof def === 'function') {
this.update = def
} else {
extend(this, def)
}
// setup directive params
this._setupParams()
// initial bind
if (this.bind) {
this.bind()
}
this._bound = true
if (this.literal) {
this.update && this.update(descriptor.raw)
} else if (
(this.expression || this.modifiers) &&
(this.update || this.twoWay) &&
!this._checkStatement()
) {
// wrapped updater for context
var dir = this
if (this.update) {
this._update = function (val, oldVal) {
if (!dir._locked) {
dir.update(val, oldVal)
}
}
} else {
this._update = noop
}
var preProcess = this._preProcess
? bind(this._preProcess, this)
: null
var postProcess = this._postProcess
? bind(this._postProcess, this)
: null
var watcher = this._watcher = new Watcher(
this.vm,
this.expression,
this._update, // callback
{
filters: this.filters,
twoWay: this.twoWay,
deep: this.deep,
preProcess: preProcess,
postProcess: postProcess,
scope: this._scope
}
)
// v-model with inital inline value need to sync back to
// model instead of update to DOM on init. They would
// set the afterBind hook to indicate that.
if (this.afterBind) {
this.afterBind()
} else if (this.update) {
this.update(watcher.value)
}
}
}
/**
* Setup all param attributes, e.g. track-by,
* transition-mode, etc...
*/
Directive.prototype._setupParams = function () {
if (!this.params) {
return
}
var params = this.params
// swap the params array with a fresh object.
this.params = Object.create(null)
var i = params.length
var key, val, mappedKey
while (i--) {
key = hyphenate(params[i])
mappedKey = camelize(key)
val = getBindAttr(this.el, key)
if (val != null) {
// dynamic
this._setupParamWatcher(mappedKey, val)
} else {
// static
val = getAttr(this.el, key)
if (val != null) {
this.params[mappedKey] = val === '' ? true : val
}
}
}
}
/**
* Setup a watcher for a dynamic param.
*
* @param {String} key
* @param {String} expression
*/
Directive.prototype._setupParamWatcher = function (key, expression) {
var self = this
var called = false
var unwatch = (this._scope || this.vm).$watch(expression, function (val, oldVal) {
self.params[key] = val
// since we are in immediate mode,
// only call the param change callbacks if this is not the first update.
if (called) {
var cb = self.paramWatchers && self.paramWatchers[key]
if (cb) {
cb.call(self, val, oldVal)
}
} else {
called = true
}
}, {
immediate: true,
user: false
})
;(this._paramUnwatchFns || (this._paramUnwatchFns = [])).push(unwatch)
}
/**
* Check if the directive is a function caller
* and if the expression is a callable one. If both true,
* we wrap up the expression and use it as the event
* handler.
*
* e.g. on-click="a++"
*
* @return {Boolean}
*/
Directive.prototype._checkStatement = function () {
var expression = this.expression
if (
expression && this.acceptStatement &&
!isSimplePath(expression)
) {
var fn = parseExpression(expression).get
var scope = this._scope || this.vm
var handler = function (e) {
scope.$event = e
fn.call(scope, scope)
scope.$event = null
}
if (this.filters) {
handler = scope._applyFilters(handler, null, this.filters)
}
this.update(handler)
return true
}
}
/**
* Set the corresponding value with the setter.
* This should only be used in two-way directives
* e.g. v-model.
*
* @param {*} value
* @public
*/
Directive.prototype.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.'
)
}
}
/**
* Execute a function while preventing that function from
* triggering updates on this directive instance.
*
* @param {Function} fn
*/
Directive.prototype._withLock = function (fn) {
var self = this
self._locked = true
fn.call(self)
nextTick(function () {
self._locked = false
})
}
/**
* Convenience method that attaches a DOM event listener
* to the directive element and autometically tears it down
* during unbind.
*
* @param {String} event
* @param {Function} handler
* @param {Boolean} [useCapture]
*/
Directive.prototype.on = function (event, handler, useCapture) {
on(this.el, event, handler, useCapture)
;(this._listeners || (this._listeners = []))
.push([event, handler])
}
/**
* Teardown the watcher and call unbind.
*/
Directive.prototype._teardown = function () {
if (this._bound) {
this._bound = false
if (this.unbind) {
this.unbind()
}
if (this._watcher) {
this._watcher.teardown()
}
var listeners = this._listeners
var i
if (listeners) {
i = listeners.length
while (i--) {
off(this.el, listeners[i][0], listeners[i][1])
}
}
var unwatchFns = this._paramUnwatchFns
if (unwatchFns) {
i = unwatchFns.length
while (i--) {
unwatchFns[i]()
}
}
if (process.env.NODE_ENV !== 'production' && this.el) {
this.el._vue_directives.$remove(this)
}
this.vm = this.el = this._watcher = this._listeners = null
}
}
|