| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- var _ = require('../util')
- var config = require('../config')
- var textParser = require('../parsers/text')
- var dirParser = require('../parsers/directive')
- var templateParser = require('../parsers/template')
- var resolveAsset = _.resolveAsset
- // internal directives
- var propDef = require('../directives/prop')
- var componentDef = require('../directives/component')
- // terminal directives
- var terminalDirectives = [
- 'repeat',
- 'if'
- ]
- /**
- * Compile a template and return a reusable composite link
- * function, which recursively contains more link functions
- * inside. This top level compile function would normally
- * be called on instance root nodes, but can also be used
- * for partial compilation if the partial argument is true.
- *
- * The returned composite link function, when called, will
- * return an unlink function that tearsdown all directives
- * created during the linking phase.
- *
- * @param {Element|DocumentFragment} el
- * @param {Object} options
- * @param {Boolean} partial
- * @param {Vue} [host] - host vm of transcluded content
- * @return {Function}
- */
- exports.compile = function (el, options, partial, host) {
- // link function for the node itself.
- var nodeLinkFn = partial || !options._asComponent
- ? compileNode(el, options)
- : null
- // link function for the childNodes
- var childLinkFn =
- !(nodeLinkFn && nodeLinkFn.terminal) &&
- el.tagName !== 'SCRIPT' &&
- el.hasChildNodes()
- ? compileNodeList(el.childNodes, options)
- : null
- /**
- * A composite linker function to be called on a already
- * compiled piece of DOM, which instantiates all directive
- * instances.
- *
- * @param {Vue} vm
- * @param {Element|DocumentFragment} el
- * @return {Function|undefined}
- */
- return function compositeLinkFn (vm, el) {
- // cache childNodes before linking parent, fix #657
- var childNodes = _.toArray(el.childNodes)
- // link
- var dirs = linkAndCapture(function () {
- if (nodeLinkFn) nodeLinkFn(vm, el, host)
- if (childLinkFn) childLinkFn(vm, childNodes, host)
- }, vm)
- /**
- * The linker function returns an unlink function that
- * tearsdown all directives instances generated during
- * the process.
- *
- * @param {Boolean} destroying
- */
- return function unlink (destroying) {
- teardownDirs(vm, dirs, destroying)
- }
- }
- }
- /**
- * Apply a linker to a vm/element pair and capture the
- * directives created during the process.
- *
- * @param {Function} linker
- * @param {Vue} vm
- */
- function linkAndCapture (linker, vm) {
- var originalDirCount = vm._directives.length
- linker()
- return vm._directives.slice(originalDirCount)
- }
- /**
- * Teardown partial linked directives.
- *
- * @param {Vue} vm
- * @param {Array} dirs
- * @param {Boolean} destroying
- */
- function teardownDirs (vm, dirs, destroying) {
- if (!dirs) return
- var i = dirs.length
- while (i--) {
- dirs[i]._teardown()
- if (!destroying) {
- vm._directives.$remove(dirs[i])
- }
- }
- }
- /**
- * Compile the root element of an instance. There are
- * 3 types of things to process here:
- *
- * 1. props on parent container (child scope)
- * 2. other attrs on parent container (parent scope)
- * 3. attrs on the component template root node, if
- * replace:true (child scope)
- *
- * Also, if this is a block instance, we only need to
- * compile 1 & 2 here.
- *
- * This function does compile and link at the same time,
- * since root linkers can not be reused. It returns the
- * unlink function for potential parent directives on the
- * container.
- *
- * @param {Vue} vm
- * @param {Element} el
- * @param {Object} options
- * @return {Function}
- */
- exports.compileAndLinkRoot = function (vm, el, options) {
- var containerAttrs = options._containerAttrs
- var replacerAttrs = options._replacerAttrs
- var props = options.props
- var propsLinkFn, parentLinkFn, replacerLinkFn
- // 1. props
- propsLinkFn = props && containerAttrs
- ? compileProps(el, containerAttrs, props)
- : null
- // only need to compile other attributes for
- // non-block instances
- if (el.nodeType !== 11) {
- // for components, container and replacer need to be
- // compiled separately and linked in different scopes.
- if (options._asComponent) {
- // 2. container attributes
- if (containerAttrs) {
- parentLinkFn = compileDirectives(containerAttrs, options)
- }
- if (replacerAttrs) {
- // 3. replacer attributes
- replacerLinkFn = compileDirectives(replacerAttrs, options)
- }
- } else {
- // non-component, just compile as a normal element.
- replacerLinkFn = compileDirectives(el, options)
- }
- }
- // link parent dirs
- var parent = vm.$parent
- var parentDirs
- if (parent && parentLinkFn) {
- parentDirs = linkAndCapture(function () {
- parentLinkFn(parent, el)
- }, parent)
- }
- // link self
- var selfDirs = linkAndCapture(function () {
- if (propsLinkFn) propsLinkFn(vm, null)
- if (replacerLinkFn) replacerLinkFn(vm, el)
- }, vm)
- // return the unlink function that tearsdown parent
- // container directives.
- return function rootUnlinkFn () {
- teardownDirs(parent, parentDirs)
- teardownDirs(vm, selfDirs)
- }
- }
- /**
- * Compile a node and return a nodeLinkFn based on the
- * node type.
- *
- * @param {Node} node
- * @param {Object} options
- * @return {Function|null}
- */
- function compileNode (node, options) {
- var type = node.nodeType
- if (type === 1 && node.tagName !== 'SCRIPT') {
- return compileElement(node, options)
- } else if (type === 3 && config.interpolate && node.data.trim()) {
- return compileTextNode(node, options)
- } else {
- return null
- }
- }
- /**
- * Compile an element and return a nodeLinkFn.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Function|null}
- */
- function compileElement (el, options) {
- var hasAttrs = el.hasAttributes()
- // check element directives
- var linkFn = checkElementDirectives(el, options)
- // check terminal directives (repeat & if)
- if (!linkFn && hasAttrs) {
- linkFn = checkTerminalDirectives(el, options)
- }
- // check component
- if (!linkFn) {
- linkFn = checkComponent(el, options)
- }
- // normal directives
- if (!linkFn && hasAttrs) {
- linkFn = compileDirectives(el, options)
- }
- // if the element is a textarea, we need to interpolate
- // its content on initial render.
- if (el.tagName === 'TEXTAREA') {
- var realLinkFn = linkFn
- linkFn = function (vm, el) {
- el.value = vm.$interpolate(el.value)
- if (realLinkFn) realLinkFn(vm, el)
- }
- linkFn.terminal = true
- }
- return linkFn
- }
- /**
- * Compile a textNode and return a nodeLinkFn.
- *
- * @param {TextNode} node
- * @param {Object} options
- * @return {Function|null} textNodeLinkFn
- */
- function compileTextNode (node, options) {
- var tokens = textParser.parse(node.data)
- if (!tokens) {
- return null
- }
- var frag = document.createDocumentFragment()
- var el, token
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i]
- el = token.tag
- ? processTextToken(token, options)
- : document.createTextNode(token.value)
- frag.appendChild(el)
- }
- return makeTextNodeLinkFn(tokens, frag, options)
- }
- /**
- * Process a single text token.
- *
- * @param {Object} token
- * @param {Object} options
- * @return {Node}
- */
- function processTextToken (token, options) {
- var el
- if (token.oneTime) {
- el = document.createTextNode(token.value)
- } else {
- if (token.html) {
- el = document.createComment('v-html')
- setTokenType('html')
- } else {
- // IE will clean up empty textNodes during
- // frag.cloneNode(true), so we have to give it
- // something here...
- el = document.createTextNode(' ')
- setTokenType('text')
- }
- }
- function setTokenType (type) {
- token.type = type
- token.def = resolveAsset(options, 'directives', type)
- token.descriptor = dirParser.parse(token.value)[0]
- }
- return el
- }
- /**
- * Build a function that processes a textNode.
- *
- * @param {Array<Object>} tokens
- * @param {DocumentFragment} frag
- */
- function makeTextNodeLinkFn (tokens, frag) {
- return function textNodeLinkFn (vm, el) {
- var fragClone = frag.cloneNode(true)
- var childNodes = _.toArray(fragClone.childNodes)
- var token, value, node
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i]
- value = token.value
- if (token.tag) {
- node = childNodes[i]
- if (token.oneTime) {
- value = vm.$eval(value)
- if (token.html) {
- _.replace(node, templateParser.parse(value, true))
- } else {
- node.data = value
- }
- } else {
- vm._bindDir(token.type, node,
- token.descriptor, token.def)
- }
- }
- }
- _.replace(el, fragClone)
- }
- }
- /**
- * Compile a node list and return a childLinkFn.
- *
- * @param {NodeList} nodeList
- * @param {Object} options
- * @return {Function|undefined}
- */
- function compileNodeList (nodeList, options) {
- var linkFns = []
- var nodeLinkFn, childLinkFn, node
- for (var i = 0, l = nodeList.length; i < l; i++) {
- node = nodeList[i]
- nodeLinkFn = compileNode(node, options)
- childLinkFn =
- !(nodeLinkFn && nodeLinkFn.terminal) &&
- node.tagName !== 'SCRIPT' &&
- node.hasChildNodes()
- ? compileNodeList(node.childNodes, options)
- : null
- linkFns.push(nodeLinkFn, childLinkFn)
- }
- return linkFns.length
- ? makeChildLinkFn(linkFns)
- : null
- }
- /**
- * Make a child link function for a node's childNodes.
- *
- * @param {Array<Function>} linkFns
- * @return {Function} childLinkFn
- */
- function makeChildLinkFn (linkFns) {
- return function childLinkFn (vm, nodes, host) {
- var node, nodeLinkFn, childrenLinkFn
- for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
- node = nodes[n]
- nodeLinkFn = linkFns[i++]
- childrenLinkFn = linkFns[i++]
- // cache childNodes before linking parent, fix #657
- var childNodes = _.toArray(node.childNodes)
- if (nodeLinkFn) {
- nodeLinkFn(vm, node, host)
- }
- if (childrenLinkFn) {
- childrenLinkFn(vm, childNodes, host)
- }
- }
- }
- }
- /**
- * Compile param attributes on a root element and return
- * a props link function.
- *
- * @param {Element|DocumentFragment} el
- * @param {Object} attrs
- * @param {Array} propNames
- * @return {Function} propsLinkFn
- */
- var dataAttrRE = /^data-/
- var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/
- var literalValueRE = /^true|false|\d+$/
- var identRE = require('../parsers/path').identRE
- function compileProps (el, attrs, propNames) {
- var props = []
- var i = propNames.length
- var name, value, path, prop, settable, single
- while (i--) {
- name = propNames[i]
- // props could contain dashes, which will be
- // interpreted as minus calculations by the parser
- // so we need to camelize the path here
- path = _.camelize(name.replace(dataAttrRE, ''))
- if (/[A-Z]/.test(name)) {
- _.warn(
- 'You seem to be using camelCase for a component prop, ' +
- 'but HTML doesn\'t differentiate between upper and ' +
- 'lower case. You should use hyphen-delimited ' +
- 'attribute names. For more info see ' +
- 'http://vuejs.org/api/options.html#props'
- )
- }
- if (!identRE.test(path)) {
- _.warn(
- 'Invalid prop key: "' + name + '". Prop keys ' +
- 'must be valid identifiers.'
- )
- }
- value = attrs[name]
- /* jshint eqeqeq:false */
- if (value != null) {
- prop = {
- name: name,
- raw: value,
- path: path
- }
- var tokens = textParser.parse(value)
- if (tokens) {
- if (el && el.nodeType === 1) {
- el.removeAttribute(name)
- }
- // important so that this doesn't get compiled
- // again as a normal attribute binding
- attrs[name] = null
- prop.dynamic = true
- prop.parentPath = textParser.tokensToExp(tokens)
- // check prop binding type.
- single = tokens.length === 1
- settable =
- settablePathRE.test(prop.parentPath) &&
- !literalValueRE.test(prop.parentPath)
- // one time: {{* prop}}
- prop.oneTime =
- !single ||
- !settable ||
- tokens[0].oneTime
- // one way down: {{> prop}}
- prop.oneWayDown =
- single &&
- tokens[0].oneWay === 62 // >
- // one way up: {{< prop}}
- prop.oneWayUp =
- tokens[0].oneWay === 60 && // <
- settable
- }
- props.push(prop)
- }
- }
- return makePropsLinkFn(props)
- }
- /**
- * Build a function that applies props to a vm.
- *
- * @param {Array} props
- * @return {Function} propsLinkFn
- */
- function makePropsLinkFn (props) {
- return function propsLinkFn (vm, el) {
- var i = props.length
- var prop, path
- while (i--) {
- prop = props[i]
- path = prop.path
- if (prop.dynamic) {
- if (vm.$parent) {
- if (prop.onetime) {
- // one time binding
- vm.$set(path, vm.$parent.$get(prop.parentPath))
- } else {
- // dynamic binding
- vm._bindDir('prop', el, prop, propDef)
- }
- } else {
- _.warn(
- 'Cannot bind dynamic prop on a root instance' +
- ' with no parent: ' + prop.name + '="' +
- prop.raw + '"'
- )
- }
- } else {
- // literal, just set once
- vm.$set(path, _.toNumber(prop.raw))
- }
- }
- }
- }
- /**
- * Check for element directives (custom elements that should
- * be resovled as terminal directives).
- *
- * @param {Element} el
- * @param {Object} options
- */
- function checkElementDirectives (el, options) {
- var tag = el.tagName.toLowerCase()
- var def = resolveAsset(options, 'elementDirectives', tag)
- if (def) {
- return makeTerminalNodeLinkFn(el, tag, '', options, def)
- }
- }
- /**
- * Check if an element is a component. If yes, return
- * a component link function.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Function|undefined}
- */
- function checkComponent (el, options) {
- var componentId = _.checkComponent(el, options)
- if (componentId) {
- var componentLinkFn = function (vm, el, host) {
- vm._bindDir('component', el, {
- expression: componentId
- }, componentDef, host)
- }
- componentLinkFn.terminal = true
- return componentLinkFn
- }
- }
- /**
- * Check an element for terminal directives in fixed order.
- * If it finds one, return a terminal link function.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Function} terminalLinkFn
- */
- function checkTerminalDirectives (el, options) {
- if (_.attr(el, 'pre') !== null) {
- return skip
- }
- var value, dirName
- /* jshint boss: true */
- for (var i = 0, l = terminalDirectives.length; i < l; i++) {
- dirName = terminalDirectives[i]
- if ((value = _.attr(el, dirName)) !== null) {
- return makeTerminalNodeLinkFn(el, dirName, value, options)
- }
- }
- }
- function skip () {}
- skip.terminal = true
- /**
- * Build a node link function for a terminal directive.
- * A terminal link function terminates the current
- * compilation recursion and handles compilation of the
- * subtree in the directive.
- *
- * @param {Element} el
- * @param {String} dirName
- * @param {String} value
- * @param {Object} options
- * @param {Object} [def]
- * @return {Function} terminalLinkFn
- */
- function makeTerminalNodeLinkFn (el, dirName, value, options, def) {
- var descriptor = dirParser.parse(value)[0]
- // no need to call resolveAsset since terminal directives
- // are always internal
- def = def || options.directives[dirName]
- var fn = function terminalNodeLinkFn (vm, el, host) {
- vm._bindDir(dirName, el, descriptor, def, host)
- }
- fn.terminal = true
- return fn
- }
- /**
- * Compile the directives on an element and return a linker.
- *
- * @param {Element|Object} elOrAttrs
- * - could be an object of already-extracted
- * container attributes.
- * @param {Object} options
- * @return {Function}
- */
- function compileDirectives (elOrAttrs, options) {
- var attrs = _.isPlainObject(elOrAttrs)
- ? mapToList(elOrAttrs)
- : elOrAttrs.attributes
- var i = attrs.length
- var dirs = []
- var attr, name, value, dir, dirName, dirDef
- while (i--) {
- attr = attrs[i]
- name = attr.name
- value = attr.value
- if (value === null) continue
- if (name.indexOf(config.prefix) === 0) {
- dirName = name.slice(config.prefix.length)
- dirDef = resolveAsset(options, 'directives', dirName)
- _.assertAsset(dirDef, 'directive', dirName)
- if (dirDef) {
- dirs.push({
- name: dirName,
- descriptors: dirParser.parse(value),
- def: dirDef
- })
- }
- } else if (config.interpolate) {
- dir = collectAttrDirective(name, value, options)
- if (dir) {
- dirs.push(dir)
- }
- }
- }
- // sort by priority, LOW to HIGH
- if (dirs.length) {
- dirs.sort(directiveComparator)
- return makeNodeLinkFn(dirs)
- }
- }
- /**
- * Convert a map (Object) of attributes to an Array.
- *
- * @param {Object} map
- * @return {Array}
- */
- function mapToList (map) {
- var list = []
- for (var key in map) {
- list.push({
- name: key,
- value: map[key]
- })
- }
- return list
- }
- /**
- * Build a link function for all directives on a single node.
- *
- * @param {Array} directives
- * @return {Function} directivesLinkFn
- */
- function makeNodeLinkFn (directives) {
- return function nodeLinkFn (vm, el, host) {
- // reverse apply because it's sorted low to high
- var i = directives.length
- var dir, j, k
- while (i--) {
- dir = directives[i]
- if (dir._link) {
- // custom link fn
- dir._link(vm, el)
- } else {
- k = dir.descriptors.length
- for (j = 0; j < k; j++) {
- vm._bindDir(dir.name, el,
- dir.descriptors[j], dir.def, host)
- }
- }
- }
- }
- }
- /**
- * Check an attribute for potential dynamic bindings,
- * and return a directive object.
- *
- * @param {String} name
- * @param {String} value
- * @param {Object} options
- * @return {Object}
- */
- function collectAttrDirective (name, value, options) {
- var tokens = textParser.parse(value)
- if (tokens) {
- var def = options.directives.attr
- var i = tokens.length
- var allOneTime = true
- while (i--) {
- var token = tokens[i]
- if (token.tag && !token.oneTime) {
- allOneTime = false
- }
- }
- return {
- def: def,
- _link: allOneTime
- ? function (vm, el) {
- el.setAttribute(name, vm.$interpolate(value))
- }
- : function (vm, el) {
- var value = textParser.tokensToExp(tokens, vm)
- var desc = dirParser.parse(name + ':' + value)[0]
- vm._bindDir('attr', el, desc, def)
- }
- }
- }
- }
- /**
- * Directive priority sort comparator
- *
- * @param {Object} a
- * @param {Object} b
- */
- function directiveComparator (a, b) {
- a = a.def.priority || 0
- b = b.def.priority || 0
- return a > b ? 1 : -1
- }
|