transclude.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. var _ = require('../util')
  2. var templateParser = require('../parse/template')
  3. /**
  4. * Process an element or a DocumentFragment based on a
  5. * instance option object. This allows us to transclude
  6. * a template node/fragment before the instance is created,
  7. * so the processed fragment can then be cloned and reused
  8. * in v-repeat.
  9. *
  10. * @param {Element} el
  11. * @param {Object} options
  12. * @return {Element|DocumentFragment}
  13. */
  14. module.exports = function transclude (el, options) {
  15. if (typeof el === 'string') {
  16. var selector = el
  17. el = document.querySelector(el)
  18. if (!el) {
  19. _.warn('Cannot find element: ' + selector)
  20. }
  21. }
  22. if (el instanceof DocumentFragment) {
  23. return transcludeBlock(el)
  24. } else if (options.template) {
  25. return transcludeTemplate(el, options)
  26. } else {
  27. return el
  28. }
  29. }
  30. /**
  31. * Mark a block fragment that manages a group of nodes
  32. * instead of one element. The group is denoted by
  33. * a starting node and an ending node.
  34. *
  35. * @param {DocumentFragment} frag
  36. */
  37. function transcludeBlock (frag) {
  38. _.prepend(document.createComment('v-block-start'), frag)
  39. frag.appendChild(document.createComment('v-block-end'))
  40. return frag
  41. }
  42. /**
  43. * Process the template option.
  44. * If the replace option is true this will swap the $el.
  45. *
  46. * @param {Element} el
  47. * @param {Object} options
  48. * @return {Element|DocumentFragment}
  49. */
  50. function transcludeTemplate (el, options) {
  51. var template = options.template
  52. var frag = templateParser.parse(template, true)
  53. if (!frag) {
  54. _.warn('Invalid template option: ' + template)
  55. } else {
  56. collectRawContent(el)
  57. if (options.replace) {
  58. if (frag.childNodes.length > 1) {
  59. transcludeBlock(frag)
  60. transcludeContent(_.toArray(frag.childNodes))
  61. return frag
  62. } else {
  63. var replacer = frag.firstChild
  64. _.copyAttributes(el, replacer)
  65. transcludeContent(replacer)
  66. return replacer
  67. }
  68. } else {
  69. el.appendChild(frag)
  70. return el
  71. }
  72. }
  73. }
  74. /**
  75. * Collect raw content inside $el before they are
  76. * replaced by template content.
  77. */
  78. var rawContent
  79. function collectRawContent (el) {
  80. var child
  81. rawContent = null
  82. if (el.hasChildNodes()) {
  83. rawContent = document.createElement('div')
  84. /* jshint boss:true */
  85. while (child = el.firstChild) {
  86. rawContent.appendChild(child)
  87. }
  88. }
  89. }
  90. /**
  91. * Resolve <content> insertion points mimicking the behavior
  92. * of the Shadow DOM spec:
  93. *
  94. * http://w3c.github.io/webcomponents/spec/shadow/#insertion-points
  95. *
  96. * @param {Element|DocumentFragment} el
  97. */
  98. function transcludeContent (el) {
  99. var outlets = getOutlets(el)
  100. var i = outlets.length
  101. if (!i) return
  102. var outlet, select, j, main
  103. // first pass, collect corresponding content
  104. // for each outlet.
  105. while (i--) {
  106. outlet = outlets[i]
  107. if (rawContent) {
  108. select = outlet.getAttribute('select')
  109. if (select) { // select content
  110. outlet.content = _.toArray(
  111. rawContent.querySelectorAll(select)
  112. )
  113. } else { // default content
  114. main = outlet
  115. }
  116. } else { // fallback content
  117. outlet.content = _.toArray(outlet.childNodes)
  118. }
  119. }
  120. // second pass, actually insert the contents
  121. for (i = 0, j = outlets.length; i < j; i++) {
  122. outlet = outlets[i]
  123. if (outlet !== main) {
  124. insertContentAt(outlet, outlet.content)
  125. }
  126. }
  127. // finally insert the main content
  128. if (main) {
  129. insertContentAt(main, _.toArray(rawContent.childNodes))
  130. }
  131. }
  132. /**
  133. * Get <content> outlets from the element/list
  134. *
  135. * @param {Element|Array} el
  136. * @return {Array}
  137. */
  138. var concat = [].concat
  139. function getOutlets (el) {
  140. return _.isArray(el)
  141. ? concat.apply([], el.map(getOutlets))
  142. : _.toArray(el.querySelectorAll('content'))
  143. }
  144. /**
  145. * Insert an array of nodes at outlet,
  146. * then remove the outlet.
  147. *
  148. * @param {Element} outlet
  149. * @param {Array} contents
  150. */
  151. function insertContentAt (outlet, contents) {
  152. // not using util DOM methods here because
  153. // parentNode can be cached
  154. var parent = outlet.parentNode
  155. for (var i = 0, j = contents.length; i < j; i++) {
  156. parent.insertBefore(contents[i], outlet)
  157. }
  158. parent.removeChild(outlet)
  159. }