forked from vuejs/vue
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtransclude.js
232 lines (216 loc) · 6.25 KB
/
transclude.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
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) {
// extract container attributes to pass them down
// to compiler, because they need to be compiled in
// parent scope. we are mutating the options object here
// assuming the same object will be used for compile
// right after this.
options._containerAttrs = extractAttrs(el)
// 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)
var replacer = frag.firstChild
if (options.replace) {
if (
frag.childNodes.length > 1 ||
replacer.nodeType !== 1 ||
// when root node has v-repeat, the instance ends up
// having multiple top-level nodes, thus becoming a
// block instance. (#835)
replacer.hasAttribute(config.prefix + 'repeat')
) {
transcludeContent(frag, rawContent)
return frag
} else {
options._replacerAttrs = extractAttrs(replacer)
mergeAttrs(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. The resulting map will be used in compiler to
* determine whether an attribute is transcluded.
*
* @param {Element} el
*/
function extractAttrs (el) {
var attrs = el.attributes
var res = {}
var i = attrs.length
while (i--) {
res[attrs[i].name] = attrs[i].value
}
return res
}
/**
* Merge the attributes of two elements, and make sure
* the class names are merged properly.
*
* @param {Element} from
* @param {Element} to
*/
function mergeAttrs (from, to) {
var attrs = from.attributes
var i = attrs.length
var name, value
while (i--) {
name = attrs[i].name
value = attrs[i].value
if (!to.hasAttribute(name)) {
to.setAttribute(name, value)
} else if (name === 'class') {
to.className = to.className + ' ' + value
}
}
}