template.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. var _ = require('../util')
  2. var Cache = require('../cache')
  3. var templateCache = new Cache(1000)
  4. var idSelectorCache = new Cache(1000)
  5. var map = {
  6. _default: [0, '', ''],
  7. legend: [1, '<fieldset>', '</fieldset>'],
  8. tr: [2, '<table><tbody>', '</tbody></table>'],
  9. col: [
  10. 2,
  11. '<table><tbody></tbody><colgroup>',
  12. '</colgroup></table>'
  13. ]
  14. }
  15. map.td =
  16. map.th = [
  17. 3,
  18. '<table><tbody><tr>',
  19. '</tr></tbody></table>'
  20. ]
  21. map.option =
  22. map.optgroup = [
  23. 1,
  24. '<select multiple="multiple">',
  25. '</select>'
  26. ]
  27. map.thead =
  28. map.tbody =
  29. map.colgroup =
  30. map.caption =
  31. map.tfoot = [1, '<table>', '</table>']
  32. map.g =
  33. map.defs =
  34. map.symbol =
  35. map.use =
  36. map.image =
  37. map.text =
  38. map.circle =
  39. map.ellipse =
  40. map.line =
  41. map.path =
  42. map.polygon =
  43. map.polyline =
  44. map.rect = [
  45. 1,
  46. '<svg ' +
  47. 'xmlns="http://www.w3.org/2000/svg" ' +
  48. 'xmlns:xlink="http://www.w3.org/1999/xlink" ' +
  49. 'xmlns:ev="http://www.w3.org/2001/xml-events"' +
  50. 'version="1.1">',
  51. '</svg>'
  52. ]
  53. var tagRE = /<([\w:]+)/
  54. var entityRE = /&\w+;/
  55. /**
  56. * Convert a string template to a DocumentFragment.
  57. * Determines correct wrapping by tag types. Wrapping
  58. * strategy found in jQuery & component/domify.
  59. *
  60. * @param {String} templateString
  61. * @return {DocumentFragment}
  62. */
  63. function stringToFragment (templateString) {
  64. // try a cache hit first
  65. var hit = templateCache.get(templateString)
  66. if (hit) {
  67. return hit
  68. }
  69. var frag = document.createDocumentFragment()
  70. var tagMatch = templateString.match(tagRE)
  71. var entityMatch = entityRE.test(templateString)
  72. if (!tagMatch && !entityMatch) {
  73. // text only, return a single text node.
  74. frag.appendChild(
  75. document.createTextNode(templateString)
  76. )
  77. } else {
  78. var tag = tagMatch && tagMatch[1]
  79. var wrap = map[tag] || map._default
  80. var depth = wrap[0]
  81. var prefix = wrap[1]
  82. var suffix = wrap[2]
  83. var node = document.createElement('div')
  84. node.innerHTML = prefix + templateString.trim() + suffix
  85. while (depth--) {
  86. node = node.lastChild
  87. }
  88. var child
  89. /* eslint-disable no-cond-assign */
  90. while (child = node.firstChild) {
  91. /* eslint-enable no-cond-assign */
  92. frag.appendChild(child)
  93. }
  94. }
  95. templateCache.put(templateString, frag)
  96. return frag
  97. }
  98. /**
  99. * Convert a template node to a DocumentFragment.
  100. *
  101. * @param {Node} node
  102. * @return {DocumentFragment}
  103. */
  104. function nodeToFragment (node) {
  105. // if its a template tag and the browser supports it,
  106. // its content is already a document fragment.
  107. if (
  108. _.isTemplate(node) &&
  109. node.content instanceof DocumentFragment
  110. ) {
  111. return node.content
  112. }
  113. // script template
  114. if (node.tagName === 'SCRIPT') {
  115. return stringToFragment(node.textContent)
  116. }
  117. // normal node, clone it to avoid mutating the original
  118. var clone = exports.clone(node)
  119. var frag = document.createDocumentFragment()
  120. var child
  121. /* eslint-disable no-cond-assign */
  122. while (child = clone.firstChild) {
  123. /* eslint-enable no-cond-assign */
  124. frag.appendChild(child)
  125. }
  126. return frag
  127. }
  128. // Test for the presence of the Safari template cloning bug
  129. // https://bugs.webkit.org/show_bug.cgi?id=137755
  130. var hasBrokenTemplate = _.inBrowser
  131. ? (function () {
  132. var a = document.createElement('div')
  133. a.innerHTML = '<template>1</template>'
  134. return !a.cloneNode(true).firstChild.innerHTML
  135. })()
  136. : false
  137. // Test for IE10/11 textarea placeholder clone bug
  138. var hasTextareaCloneBug = _.inBrowser
  139. ? (function () {
  140. var t = document.createElement('textarea')
  141. t.placeholder = 't'
  142. return t.cloneNode(true).value === 't'
  143. })()
  144. : false
  145. /**
  146. * 1. Deal with Safari cloning nested <template> bug by
  147. * manually cloning all template instances.
  148. * 2. Deal with IE10/11 textarea placeholder bug by setting
  149. * the correct value after cloning.
  150. *
  151. * @param {Element|DocumentFragment} node
  152. * @return {Element|DocumentFragment}
  153. */
  154. exports.clone = function (node) {
  155. var res = node.cloneNode(true)
  156. var i, original, cloned
  157. /* istanbul ignore if */
  158. if (hasBrokenTemplate) {
  159. original = node.querySelectorAll('template')
  160. if (original.length) {
  161. cloned = res.querySelectorAll('template')
  162. i = cloned.length
  163. while (i--) {
  164. cloned[i].parentNode.replaceChild(
  165. original[i].cloneNode(true),
  166. cloned[i]
  167. )
  168. }
  169. }
  170. }
  171. /* istanbul ignore if */
  172. if (hasTextareaCloneBug) {
  173. if (node.tagName === 'TEXTAREA') {
  174. res.value = node.value
  175. } else {
  176. original = node.querySelectorAll('textarea')
  177. if (original.length) {
  178. cloned = res.querySelectorAll('textarea')
  179. i = cloned.length
  180. while (i--) {
  181. cloned[i].value = original[i].value
  182. }
  183. }
  184. }
  185. }
  186. return res
  187. }
  188. /**
  189. * Process the template option and normalizes it into a
  190. * a DocumentFragment that can be used as a partial or a
  191. * instance template.
  192. *
  193. * @param {*} template
  194. * Possible values include:
  195. * - DocumentFragment object
  196. * - Node object of type Template
  197. * - id selector: '#some-template-id'
  198. * - template string: '<div><span>{{msg}}</span></div>'
  199. * @param {Boolean} clone
  200. * @param {Boolean} noSelector
  201. * @return {DocumentFragment|undefined}
  202. */
  203. exports.parse = function (template, clone, noSelector) {
  204. var node, frag
  205. // if the template is already a document fragment,
  206. // do nothing
  207. if (template instanceof DocumentFragment) {
  208. return clone
  209. ? template.cloneNode(true)
  210. : template
  211. }
  212. if (typeof template === 'string') {
  213. // id selector
  214. if (!noSelector && template.charAt(0) === '#') {
  215. // id selector can be cached too
  216. frag = idSelectorCache.get(template)
  217. if (!frag) {
  218. node = document.getElementById(template.slice(1))
  219. if (node) {
  220. frag = nodeToFragment(node)
  221. // save selector to cache
  222. idSelectorCache.put(template, frag)
  223. }
  224. }
  225. } else {
  226. // normal string template
  227. frag = stringToFragment(template)
  228. }
  229. } else if (template.nodeType) {
  230. // a direct node
  231. frag = nodeToFragment(template)
  232. }
  233. return frag && clone
  234. ? exports.clone(frag)
  235. : frag
  236. }