silk-gui
Version:
GUI for developers and Node OS
214 lines (200 loc) • 6.19 kB
JavaScript
var _ = require('../util')
var config = require('../config')
var templateParser = require('../parsers/template')
var transcludedFlagAttr = '__vue__transcluded'
/**
* Process an element or a DocumentFragment based on a
* instance option object. This allows us to transclude
* a template node/fragment before the instance is created,
* so the processed fragment can then be cloned and reused
* in v-repeat.
*
* @param {Element} el
* @param {Object} options
* @return {Element|DocumentFragment}
*/
module.exports = function transclude (el, options) {
if (options && options._asComponent) {
// mutating the options object here assuming the same
// object will be used for compile right after this
options._transcludedAttrs = extractAttrs(el.attributes)
// Mark content nodes and attrs so that the compiler
// knows they should be compiled in parent scope.
var i = el.childNodes.length
while (i--) {
var node = el.childNodes[i]
if (node.nodeType === 1) {
node.setAttribute(transcludedFlagAttr, '')
} else if (node.nodeType === 3 && node.data.trim()) {
// wrap transcluded textNodes in spans, because
// raw textNodes can't be persisted through clones
// by attaching attributes.
var wrapper = document.createElement('span')
wrapper.textContent = node.data
wrapper.setAttribute('__vue__wrap', '')
wrapper.setAttribute(transcludedFlagAttr, '')
el.replaceChild(wrapper, node)
}
}
}
// for template tags, what we want is its content as
// a documentFragment (for block instances)
if (el.tagName === 'TEMPLATE') {
el = templateParser.parse(el)
}
if (options && options.template) {
el = transcludeTemplate(el, options)
}
if (el instanceof DocumentFragment) {
_.prepend(document.createComment('v-start'), el)
el.appendChild(document.createComment('v-end'))
}
return el
}
/**
* Process the template option.
* If the replace option is true this will swap the $el.
*
* @param {Element} el
* @param {Object} options
* @return {Element|DocumentFragment}
*/
function transcludeTemplate (el, options) {
var template = options.template
var frag = templateParser.parse(template, true)
if (!frag) {
_.warn('Invalid template option: ' + template)
} else {
var rawContent = options._content || _.extractContent(el)
if (options.replace) {
if (frag.childNodes.length > 1) {
// this is a block instance which has no root node.
// however, the container in the parent template
// (which is replaced here) may contain v-with and
// paramAttributes that still need to be compiled
// for the child. we store all the container
// attributes on the options object and pass it down
// to the compiler.
var containerAttrs = options._containerAttrs = {}
var i = el.attributes.length
while (i--) {
var attr = el.attributes[i]
containerAttrs[attr.name] = attr.value
}
transcludeContent(frag, rawContent)
return frag
} else {
var replacer = frag.firstChild
_.copyAttributes(el, replacer)
transcludeContent(replacer, rawContent)
return replacer
}
} else {
el.appendChild(frag)
transcludeContent(el, rawContent)
return el
}
}
}
/**
* Resolve <content> insertion points mimicking the behavior
* of the Shadow DOM spec:
*
* http://w3c.github.io/webcomponents/spec/shadow/#insertion-points
*
* @param {Element|DocumentFragment} el
* @param {Element} raw
*/
function transcludeContent (el, raw) {
var outlets = getOutlets(el)
var i = outlets.length
if (!i) return
var outlet, select, selected, j, main
function isDirectChild (node) {
return node.parentNode === raw
}
// first pass, collect corresponding content
// for each outlet.
while (i--) {
outlet = outlets[i]
if (raw) {
select = outlet.getAttribute('select')
if (select) { // select content
selected = raw.querySelectorAll(select)
if (selected.length) {
// according to Shadow DOM spec, `select` can
// only select direct children of the host node.
// enforcing this also fixes #786.
selected = [].filter.call(selected, isDirectChild)
}
outlet.content = selected.length
? selected
: _.toArray(outlet.childNodes)
} else { // default content
main = outlet
}
} else { // fallback content
outlet.content = _.toArray(outlet.childNodes)
}
}
// second pass, actually insert the contents
for (i = 0, j = outlets.length; i < j; i++) {
outlet = outlets[i]
if (outlet !== main) {
insertContentAt(outlet, outlet.content)
}
}
// finally insert the main content
if (main) {
insertContentAt(main, _.toArray(raw.childNodes))
}
}
/**
* Get <content> outlets from the element/list
*
* @param {Element|Array} el
* @return {Array}
*/
var concat = [].concat
function getOutlets (el) {
return _.isArray(el)
? concat.apply([], el.map(getOutlets))
: el.querySelectorAll
? _.toArray(el.querySelectorAll('content'))
: []
}
/**
* Insert an array of nodes at outlet,
* then remove the outlet.
*
* @param {Element} outlet
* @param {Array} contents
*/
function insertContentAt (outlet, contents) {
// not using util DOM methods here because
// parentNode can be cached
var parent = outlet.parentNode
for (var i = 0, j = contents.length; i < j; i++) {
parent.insertBefore(contents[i], outlet)
}
parent.removeChild(outlet)
}
/**
* Helper to extract a component container's attribute names
* into a map, and filtering out `v-with` in the process.
* The resulting map will be used in compiler/compile to
* determine whether an attribute is transcluded.
*
* @param {NameNodeMap} attrs
*/
function extractAttrs (attrs) {
if (!attrs) return null
var res = {}
var vwith = config.prefix + 'with'
var i = attrs.length
while (i--) {
var name = attrs[i].name
if (name !== vwith) res[name] = true
}
return res
}