template.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import { inBrowser, trimNode, isTemplate } from '../util/index'
  2. import Cache from '../cache'
  3. const templateCache = new Cache(1000)
  4. const idSelectorCache = new Cache(1000)
  5. const map = {
  6. efault: [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. /**
  54. * Check if a node is a supported template node with a
  55. * DocumentFragment content.
  56. *
  57. * @param {Node} node
  58. * @return {Boolean}
  59. */
  60. function isRealTemplate (node) {
  61. return isTemplate(node) &&
  62. node.content instanceof DocumentFragment
  63. }
  64. const tagRE = /<([\w:]+)/
  65. const entityRE = /&#?\w+?;/
  66. /**
  67. * Convert a string template to a DocumentFragment.
  68. * Determines correct wrapping by tag types. Wrapping
  69. * strategy found in jQuery & component/domify.
  70. *
  71. * @param {String} templateString
  72. * @param {Boolean} raw
  73. * @return {DocumentFragment}
  74. */
  75. function stringToFragment (templateString, raw) {
  76. // try a cache hit first
  77. var cacheKey = raw
  78. ? templateString
  79. : templateString.trim()
  80. var hit = templateCache.get(cacheKey)
  81. if (hit) {
  82. return hit
  83. }
  84. var frag = document.createDocumentFragment()
  85. var tagMatch = templateString.match(tagRE)
  86. var entityMatch = entityRE.test(templateString)
  87. if (!tagMatch && !entityMatch) {
  88. // text only, return a single text node.
  89. frag.appendChild(
  90. document.createTextNode(templateString)
  91. )
  92. } else {
  93. var tag = tagMatch && tagMatch[1]
  94. var wrap = map[tag] || map.efault
  95. var depth = wrap[0]
  96. var prefix = wrap[1]
  97. var suffix = wrap[2]
  98. var node = document.createElement('div')
  99. node.innerHTML = prefix + templateString + suffix
  100. while (depth--) {
  101. node = node.lastChild
  102. }
  103. var child
  104. /* eslint-disable no-cond-assign */
  105. while (child = node.firstChild) {
  106. /* eslint-enable no-cond-assign */
  107. frag.appendChild(child)
  108. }
  109. }
  110. if (!raw) {
  111. trimNode(frag)
  112. }
  113. templateCache.put(cacheKey, frag)
  114. return frag
  115. }
  116. /**
  117. * Convert a template node to a DocumentFragment.
  118. *
  119. * @param {Node} node
  120. * @return {DocumentFragment}
  121. */
  122. function nodeToFragment (node) {
  123. // if its a template tag and the browser supports it,
  124. // its content is already a document fragment.
  125. if (isRealTemplate(node)) {
  126. trimNode(node.content)
  127. return node.content
  128. }
  129. // script template
  130. if (node.tagName === 'SCRIPT') {
  131. return stringToFragment(node.textContent)
  132. }
  133. // normal node, clone it to avoid mutating the original
  134. var clonedNode = cloneNode(node)
  135. var frag = document.createDocumentFragment()
  136. var child
  137. /* eslint-disable no-cond-assign */
  138. while (child = clonedNode.firstChild) {
  139. /* eslint-enable no-cond-assign */
  140. frag.appendChild(child)
  141. }
  142. trimNode(frag)
  143. return frag
  144. }
  145. // Test for the presence of the Safari template cloning bug
  146. // https://bugs.webkit.org/showug.cgi?id=137755
  147. var hasBrokenTemplate = (function () {
  148. /* istanbul ignore else */
  149. if (inBrowser) {
  150. var a = document.createElement('div')
  151. a.innerHTML = '<template>1</template>'
  152. return !a.cloneNode(true).firstChild.innerHTML
  153. } else {
  154. return false
  155. }
  156. })()
  157. // Test for IE10/11 textarea placeholder clone bug
  158. var hasTextareaCloneBug = (function () {
  159. /* istanbul ignore else */
  160. if (inBrowser) {
  161. var t = document.createElement('textarea')
  162. t.placeholder = 't'
  163. return t.cloneNode(true).value === 't'
  164. } else {
  165. return false
  166. }
  167. })()
  168. /**
  169. * 1. Deal with Safari cloning nested <template> bug by
  170. * manually cloning all template instances.
  171. * 2. Deal with IE10/11 textarea placeholder bug by setting
  172. * the correct value after cloning.
  173. *
  174. * @param {Element|DocumentFragment} node
  175. * @return {Element|DocumentFragment}
  176. */
  177. export function cloneNode (node) {
  178. if (!node.querySelectorAll) {
  179. return node.cloneNode()
  180. }
  181. var res = node.cloneNode(true)
  182. var i, original, cloned
  183. /* istanbul ignore if */
  184. if (hasBrokenTemplate) {
  185. var tempClone = res
  186. if (isRealTemplate(node)) {
  187. node = node.content
  188. tempClone = res.content
  189. }
  190. original = node.querySelectorAll('template')
  191. if (original.length) {
  192. cloned = tempClone.querySelectorAll('template')
  193. i = cloned.length
  194. while (i--) {
  195. cloned[i].parentNode.replaceChild(
  196. cloneNode(original[i]),
  197. cloned[i]
  198. )
  199. }
  200. }
  201. }
  202. /* istanbul ignore if */
  203. if (hasTextareaCloneBug) {
  204. if (node.tagName === 'TEXTAREA') {
  205. res.value = node.value
  206. } else {
  207. original = node.querySelectorAll('textarea')
  208. if (original.length) {
  209. cloned = res.querySelectorAll('textarea')
  210. i = cloned.length
  211. while (i--) {
  212. cloned[i].value = original[i].value
  213. }
  214. }
  215. }
  216. }
  217. return res
  218. }
  219. /**
  220. * Process the template option and normalizes it into a
  221. * a DocumentFragment that can be used as a partial or a
  222. * instance template.
  223. *
  224. * @param {*} template
  225. * Possible values include:
  226. * - DocumentFragment object
  227. * - Node object of type Template
  228. * - id selector: '#some-template-id'
  229. * - template string: '<div><span>{{msg}}</span></div>'
  230. * @param {Boolean} shouldClone
  231. * @param {Boolean} raw
  232. * inline HTML interpolation. Do not check for id
  233. * selector and keep whitespace in the string.
  234. * @return {DocumentFragment|undefined}
  235. */
  236. export function parseTemplate (template, shouldClone, raw) {
  237. var node, frag
  238. // if the template is already a document fragment,
  239. // do nothing
  240. if (template instanceof DocumentFragment) {
  241. trimNode(template)
  242. return shouldClone
  243. ? cloneNode(template)
  244. : template
  245. }
  246. if (typeof template === 'string') {
  247. // id selector
  248. if (!raw && template.charAt(0) === '#') {
  249. // id selector can be cached too
  250. frag = idSelectorCache.get(template)
  251. if (!frag) {
  252. node = document.getElementById(template.slice(1))
  253. if (node) {
  254. frag = nodeToFragment(node)
  255. // save selector to cache
  256. idSelectorCache.put(template, frag)
  257. }
  258. }
  259. } else {
  260. // normal string template
  261. frag = stringToFragment(template, raw)
  262. }
  263. } else if (template.nodeType) {
  264. // a direct node
  265. frag = nodeToFragment(template)
  266. }
  267. return frag && shouldClone
  268. ? cloneNode(frag)
  269. : frag
  270. }