quasar 2.21.4 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +1 -1
  2. package/dist/api/QUploader.json +1 -1
  3. package/dist/icon-set/bootstrap-icons.umd.prod.js +1 -1
  4. package/dist/icon-set/eva-icons.umd.prod.js +1 -1
  5. package/dist/icon-set/fontawesome-v5-pro.umd.prod.js +1 -1
  6. package/dist/icon-set/fontawesome-v5.umd.prod.js +1 -1
  7. package/dist/icon-set/fontawesome-v6-pro.umd.prod.js +1 -1
  8. package/dist/icon-set/fontawesome-v6.umd.prod.js +1 -1
  9. package/dist/icon-set/fontawesome-v7-pro.umd.prod.js +1 -1
  10. package/dist/icon-set/fontawesome-v7.umd.prod.js +1 -1
  11. package/dist/icon-set/ionicons-v4.umd.prod.js +1 -1
  12. package/dist/icon-set/line-awesome.umd.prod.js +1 -1
  13. package/dist/icon-set/material-icons-outlined.umd.prod.js +1 -1
  14. package/dist/icon-set/material-icons-round.umd.prod.js +1 -1
  15. package/dist/icon-set/material-icons-sharp.umd.prod.js +1 -1
  16. package/dist/icon-set/material-icons.umd.prod.js +1 -1
  17. package/dist/icon-set/material-symbols-outlined.umd.prod.js +1 -1
  18. package/dist/icon-set/material-symbols-rounded.umd.prod.js +1 -1
  19. package/dist/icon-set/material-symbols-sharp.umd.prod.js +1 -1
  20. package/dist/icon-set/mdi-v3.umd.prod.js +1 -1
  21. package/dist/icon-set/mdi-v4.umd.prod.js +1 -1
  22. package/dist/icon-set/mdi-v5.umd.prod.js +1 -1
  23. package/dist/icon-set/mdi-v6.umd.prod.js +1 -1
  24. package/dist/icon-set/mdi-v7.umd.prod.js +1 -1
  25. package/dist/icon-set/svg-bootstrap-icons.umd.prod.js +1 -1
  26. package/dist/icon-set/svg-eva-icons.umd.prod.js +1 -1
  27. package/dist/icon-set/svg-fontawesome-v5.umd.prod.js +1 -1
  28. package/dist/icon-set/svg-fontawesome-v6.umd.prod.js +1 -1
  29. package/dist/icon-set/svg-fontawesome-v7.umd.prod.js +1 -1
  30. package/dist/icon-set/svg-ionicons-v4.umd.prod.js +1 -1
  31. package/dist/icon-set/svg-ionicons-v5.umd.prod.js +1 -1
  32. package/dist/icon-set/svg-ionicons-v6.umd.prod.js +1 -1
  33. package/dist/icon-set/svg-ionicons-v7.umd.prod.js +1 -1
  34. package/dist/icon-set/svg-ionicons-v8.umd.prod.js +1 -1
  35. package/dist/icon-set/svg-line-awesome.umd.prod.js +1 -1
  36. package/dist/icon-set/svg-material-icons-outlined.umd.prod.js +1 -1
  37. package/dist/icon-set/svg-material-icons-round.umd.prod.js +1 -1
  38. package/dist/icon-set/svg-material-icons-sharp.umd.prod.js +1 -1
  39. package/dist/icon-set/svg-material-icons.umd.prod.js +1 -1
  40. package/dist/icon-set/svg-material-symbols-outlined.umd.prod.js +1 -1
  41. package/dist/icon-set/svg-material-symbols-rounded.umd.prod.js +1 -1
  42. package/dist/icon-set/svg-material-symbols-sharp.umd.prod.js +1 -1
  43. package/dist/icon-set/svg-mdi-v6.umd.prod.js +1 -1
  44. package/dist/icon-set/svg-mdi-v7.umd.prod.js +1 -1
  45. package/dist/icon-set/svg-themify.umd.prod.js +1 -1
  46. package/dist/icon-set/themify.umd.prod.js +1 -1
  47. package/dist/lang/ar-TN.umd.prod.js +1 -1
  48. package/dist/lang/ar.umd.prod.js +1 -1
  49. package/dist/lang/az-Latn.umd.prod.js +1 -1
  50. package/dist/lang/bg.umd.prod.js +1 -1
  51. package/dist/lang/bn.umd.prod.js +1 -1
  52. package/dist/lang/bs-BA.umd.prod.js +1 -1
  53. package/dist/lang/ca.umd.prod.js +1 -1
  54. package/dist/lang/cs.umd.prod.js +1 -1
  55. package/dist/lang/da.umd.prod.js +1 -1
  56. package/dist/lang/de-CH.umd.prod.js +1 -1
  57. package/dist/lang/de-DE.umd.prod.js +1 -1
  58. package/dist/lang/de.umd.prod.js +1 -1
  59. package/dist/lang/el.umd.prod.js +1 -1
  60. package/dist/lang/en-GB.umd.prod.js +1 -1
  61. package/dist/lang/en-US.umd.prod.js +1 -1
  62. package/dist/lang/eo.umd.prod.js +1 -1
  63. package/dist/lang/es.umd.prod.js +1 -1
  64. package/dist/lang/et.umd.prod.js +1 -1
  65. package/dist/lang/eu.umd.prod.js +1 -1
  66. package/dist/lang/fa-IR.umd.prod.js +1 -1
  67. package/dist/lang/fa.umd.prod.js +1 -1
  68. package/dist/lang/fi.umd.prod.js +1 -1
  69. package/dist/lang/fr.umd.prod.js +1 -1
  70. package/dist/lang/gn.umd.prod.js +1 -1
  71. package/dist/lang/he.umd.prod.js +1 -1
  72. package/dist/lang/hi.umd.prod.js +1 -1
  73. package/dist/lang/hr.umd.prod.js +1 -1
  74. package/dist/lang/hu.umd.prod.js +1 -1
  75. package/dist/lang/id.umd.prod.js +1 -1
  76. package/dist/lang/is.umd.prod.js +1 -1
  77. package/dist/lang/it.umd.prod.js +1 -1
  78. package/dist/lang/ja.umd.prod.js +1 -1
  79. package/dist/lang/kk.umd.prod.js +1 -1
  80. package/dist/lang/km.umd.prod.js +1 -1
  81. package/dist/lang/ko-KR.umd.prod.js +1 -1
  82. package/dist/lang/kur-CKB.umd.prod.js +1 -1
  83. package/dist/lang/lb.umd.prod.js +1 -1
  84. package/dist/lang/lt.umd.prod.js +1 -1
  85. package/dist/lang/lu.umd.prod.js +1 -1
  86. package/dist/lang/lv.umd.prod.js +1 -1
  87. package/dist/lang/mk.umd.prod.js +1 -1
  88. package/dist/lang/ml.umd.prod.js +1 -1
  89. package/dist/lang/mm.umd.prod.js +1 -1
  90. package/dist/lang/ms-MY.umd.prod.js +1 -1
  91. package/dist/lang/ms.umd.prod.js +1 -1
  92. package/dist/lang/my.umd.prod.js +1 -1
  93. package/dist/lang/nb-NO.umd.prod.js +1 -1
  94. package/dist/lang/nl.umd.prod.js +1 -1
  95. package/dist/lang/pl.umd.prod.js +1 -1
  96. package/dist/lang/pt-BR.umd.prod.js +1 -1
  97. package/dist/lang/pt.umd.prod.js +1 -1
  98. package/dist/lang/ro.umd.prod.js +1 -1
  99. package/dist/lang/ru.umd.prod.js +1 -1
  100. package/dist/lang/sk.umd.prod.js +1 -1
  101. package/dist/lang/sl.umd.prod.js +1 -1
  102. package/dist/lang/sm.umd.prod.js +1 -1
  103. package/dist/lang/sq.umd.prod.js +1 -1
  104. package/dist/lang/sr-CYR.umd.prod.js +1 -1
  105. package/dist/lang/sr.umd.prod.js +1 -1
  106. package/dist/lang/sv.umd.prod.js +1 -1
  107. package/dist/lang/ta.umd.prod.js +1 -1
  108. package/dist/lang/th.umd.prod.js +1 -1
  109. package/dist/lang/tl.umd.prod.js +1 -1
  110. package/dist/lang/tr.umd.prod.js +1 -1
  111. package/dist/lang/ug.umd.prod.js +1 -1
  112. package/dist/lang/uk.umd.prod.js +1 -1
  113. package/dist/lang/ur-PK.umd.prod.js +1 -1
  114. package/dist/lang/uz-Cyrl.umd.prod.js +1 -1
  115. package/dist/lang/uz-Latn.umd.prod.js +1 -1
  116. package/dist/lang/vi.umd.prod.js +1 -1
  117. package/dist/lang/zh-CN.umd.prod.js +1 -1
  118. package/dist/lang/zh-TW.umd.prod.js +1 -1
  119. package/dist/quasar.client.js +572 -187
  120. package/dist/quasar.css +4 -0
  121. package/dist/quasar.prod.css +1 -1
  122. package/dist/quasar.rtl.css +5 -0
  123. package/dist/quasar.rtl.prod.css +1 -1
  124. package/dist/quasar.sass +4 -1
  125. package/dist/quasar.server.prod.cjs +35 -35
  126. package/dist/quasar.server.prod.js +25 -25
  127. package/dist/quasar.umd.js +572 -187
  128. package/dist/quasar.umd.prod.js +30 -30
  129. package/dist/types/composables.d.ts +10 -5
  130. package/dist/types/index.d.ts +4 -4
  131. package/dist/web-types/web-types.json +1 -1
  132. package/package.json +6 -6
  133. package/src/components/carousel/QCarousel.js +2 -0
  134. package/src/components/chip/QChip.js +20 -4
  135. package/src/components/date/QDate.js +9 -2
  136. package/src/components/dialog/QDialog.js +9 -2
  137. package/src/components/editor/QEditor.js +18 -3
  138. package/src/components/expansion-item/QExpansionItem.js +6 -1
  139. package/src/components/input/QInput.js +42 -1
  140. package/src/components/input/QInput.json +7 -0
  141. package/src/components/input/use-mask.js +12 -4
  142. package/src/components/item/QItem.js +10 -1
  143. package/src/components/menu/QMenu.js +9 -2
  144. package/src/components/range/QRange.js +2 -1
  145. package/src/components/rating/QRating.js +27 -8
  146. package/src/components/rating/QRating.sass +4 -0
  147. package/src/components/resize-observer/QResizeObserver.js +14 -3
  148. package/src/components/scroll-area/QScrollArea.js +69 -17
  149. package/src/components/slide-item/QSlideItem.js +3 -2
  150. package/src/components/slider/use-slider.js +7 -3
  151. package/src/components/stepper/StepHeader.js +11 -2
  152. package/src/components/table/QTable.js +11 -9
  153. package/src/components/table/QTh.js +20 -1
  154. package/src/components/table/table-column-selection.js +9 -1
  155. package/src/components/table/table-pagination.js +5 -2
  156. package/src/components/table/table-row-selection.js +10 -10
  157. package/src/components/tabs/QTabs.js +5 -6
  158. package/src/components/time/QTime.js +35 -10
  159. package/src/components/tooltip/QTooltip.js +112 -6
  160. package/src/components/tree/QTree.js +124 -4
  161. package/src/components/uploader/uploader-core.js +19 -4
  162. package/src/components/uploader/xhr-uploader-plugin.js +93 -43
  163. package/src/components/uploader/xhr-uploader-plugin.json +4 -4
  164. package/src/composables/private.use-field/use-field.js +2 -0
  165. package/src/composables/private.use-file/use-file.js +21 -12
  166. package/src/composables/private.use-fullscreen/use-fullscreen.js +8 -3
  167. package/src/composables/private.use-model-toggle/use-model-toggle.js +2 -0
  168. package/src/composables/private.use-panel/use-panel.js +6 -2
  169. package/src/composables/use-interval/use-interval.js +8 -0
  170. package/src/composables/use-render-cache/use-render-cache.js +15 -10
  171. package/src/composables/use-timeout/use-timeout.js +8 -0
  172. package/src/directives/intersection/Intersection.js +2 -2
  173. package/src/directives/touch-repeat/TouchRepeat.js +3 -1
  174. package/src/plugins/cookies/Cookies.js +29 -13
  175. package/src/plugins/meta/Meta.js +57 -9
  176. package/src/utils/extend/extend.js +4 -0
  177. package/src/utils/extend/extend.test.js +29 -0
  178. package/src/utils/scroll/prevent-scroll.js +4 -4
@@ -105,7 +105,9 @@ export default function useFile({
105
105
  files = filterFiles(files, rejectedFiles, 'accept', file =>
106
106
  extensions.value.some(
107
107
  ext =>
108
- file.type.toUpperCase().startsWith(ext) ||
108
+ (ext.endsWith('/')
109
+ ? file.type.toUpperCase().startsWith(ext)
110
+ : file.type.toUpperCase() === ext) ||
109
111
  file.name.toUpperCase().endsWith(ext)
110
112
  )
111
113
  )
@@ -137,19 +139,23 @@ export default function useFile({
137
139
 
138
140
  // Compute key to use for each file
139
141
  files.forEach(file => {
140
- file.__key =
141
- file.webkitRelativePath + file.lastModified + file.name + file.size
142
+ file.__key = JSON.stringify([
143
+ file.webkitRelativePath,
144
+ file.lastModified,
145
+ file.name,
146
+ file.size
147
+ ])
142
148
  })
143
149
 
144
150
  if (append) {
145
151
  // Avoid duplicate files
146
- const filenameMap = currentFileList.map(entry => entry.__key)
147
- files = filterFiles(
148
- files,
149
- rejectedFiles,
150
- 'duplicate',
151
- file => !filenameMap.includes(file.__key)
152
- )
152
+ const filenameSet = new Set(currentFileList.map(entry => entry.__key))
153
+ files = filterFiles(files, rejectedFiles, 'duplicate', file => {
154
+ if (filenameSet.has(file.__key)) return false
155
+
156
+ filenameSet.add(file.__key)
157
+ return true
158
+ })
153
159
  }
154
160
 
155
161
  if (files.length === 0) {
@@ -162,8 +168,11 @@ export default function useFile({
162
168
  : 0
163
169
 
164
170
  files = filterFiles(files, rejectedFiles, 'max-total-size', file => {
165
- size += file.size
166
- return size <= maxTotalSizeNumber.value
171
+ const newSize = size + file.size
172
+ if (newSize > maxTotalSizeNumber.value) return false
173
+
174
+ size = newSize
175
+ return true
167
176
  })
168
177
 
169
178
  if (files.length === 0) {
@@ -23,7 +23,9 @@ export default function useFullscreen() {
23
23
  const vm = getCurrentInstance()
24
24
  const { props, emit, proxy } = vm
25
25
 
26
- let historyEntry, fullscreenFillerNode
26
+ let historyEntry,
27
+ fullscreenFillerNode,
28
+ isUnmounting = false
27
29
  const inFullscreen = ref(false)
28
30
 
29
31
  if (vmHasRouter(vm)) {
@@ -89,7 +91,7 @@ export default function useFullscreen() {
89
91
  if (counter === 0) {
90
92
  document.body.classList.remove('q-body--fullscreen-mixin')
91
93
 
92
- if (proxy.$el.scrollIntoView !== void 0) {
94
+ if (!isUnmounting && proxy.$el.scrollIntoView !== void 0) {
93
95
  setTimeout(() => {
94
96
  proxy.$el.scrollIntoView()
95
97
  }, 0)
@@ -105,7 +107,10 @@ export default function useFullscreen() {
105
107
  if (props.fullscreen) setFullscreen()
106
108
  })
107
109
 
108
- onBeforeUnmount(exitFullscreen)
110
+ onBeforeUnmount(() => {
111
+ isUnmounting = true
112
+ exitFullscreen()
113
+ })
109
114
 
110
115
  // expose public methods
111
116
  Object.assign(proxy, {
@@ -21,6 +21,7 @@ export default function useModelToggle({
21
21
  hideOnRouteChange, // optional
22
22
  handleShow, // optional
23
23
  handleHide, // optional
24
+ handleRouteChange, // optional
24
25
  processOnMount // optional
25
26
  }) {
26
27
  const vm = getCurrentInstance()
@@ -127,6 +128,7 @@ export default function useModelToggle({
127
128
  () => proxy.$route.fullPath,
128
129
  () => {
129
130
  if (hideOnRouteChange.value && showing.value) {
131
+ handleRouteChange?.()
130
132
  hide()
131
133
  }
132
134
  }
@@ -108,11 +108,15 @@ export default function usePanel() {
108
108
  ])
109
109
 
110
110
  const transitionPrev = computed(
111
- () => props.transitionPrev || `slide-${props.vertical ? 'down' : 'right'}`
111
+ () =>
112
+ props.transitionPrev ||
113
+ `slide-${props.vertical ? 'down' : proxy.$q.lang.rtl ? 'left' : 'right'}`
112
114
  )
113
115
 
114
116
  const transitionNext = computed(
115
- () => props.transitionNext || `slide-${props.vertical ? 'up' : 'left'}`
117
+ () =>
118
+ props.transitionNext ||
119
+ `slide-${props.vertical ? 'up' : proxy.$q.lang.rtl ? 'right' : 'left'}`
116
120
  )
117
121
 
118
122
  const transitionStyle = computed(
@@ -1,6 +1,7 @@
1
1
  import { getCurrentInstance, onBeforeUnmount, onDeactivated } from 'vue'
2
2
 
3
3
  import { vmIsDestroyed } from '../../utils/private.vm/vm.js'
4
+ import { noop } from '../../utils/event/event.js'
4
5
 
5
6
  /*
6
7
  * Usage:
@@ -9,6 +10,13 @@ import { vmIsDestroyed } from '../../utils/private.vm/vm.js'
9
10
  */
10
11
 
11
12
  export default function useInterval() {
13
+ if (__QUASAR_SSR_SERVER__) {
14
+ return {
15
+ removeInterval: noop,
16
+ registerInterval: noop
17
+ }
18
+ }
19
+
12
20
  let timer = null
13
21
  const vm = getCurrentInstance()
14
22
 
@@ -1,17 +1,22 @@
1
1
  export default function useRenderCache() {
2
+ if (__QUASAR_SSR_SERVER__) {
3
+ return {
4
+ getCache: (_, defaultValue) =>
5
+ typeof defaultValue === 'function' ? defaultValue() : defaultValue,
6
+ setCache() {},
7
+ hasCache: () => false,
8
+ clearCache() {}
9
+ }
10
+ }
11
+
2
12
  let cache = Object.create(null)
3
13
 
4
14
  return {
5
- getCache: __QUASAR_SSR_SERVER__
6
- ? (_, defaultValue) =>
7
- typeof defaultValue === 'function' ? defaultValue() : defaultValue
8
- : (key, defaultValue) =>
9
- cache[key] === void 0
10
- ? (cache[key] =
11
- typeof defaultValue === 'function'
12
- ? defaultValue()
13
- : defaultValue)
14
- : cache[key],
15
+ getCache: (key, defaultValue) =>
16
+ Object.hasOwn(cache, key)
17
+ ? cache[key]
18
+ : (cache[key] =
19
+ typeof defaultValue === 'function' ? defaultValue() : defaultValue),
15
20
 
16
21
  setCache(key, obj) {
17
22
  cache[key] = obj
@@ -1,6 +1,7 @@
1
1
  import { getCurrentInstance, onBeforeUnmount, onDeactivated } from 'vue'
2
2
 
3
3
  import { vmIsDestroyed } from '../../utils/private.vm/vm.js'
4
+ import { noop } from '../../utils/event/event.js'
4
5
 
5
6
  /*
6
7
  * Usage:
@@ -9,6 +10,13 @@ import { vmIsDestroyed } from '../../utils/private.vm/vm.js'
9
10
  */
10
11
 
11
12
  export default function useTimeout() {
13
+ if (__QUASAR_SSR_SERVER__) {
14
+ return {
15
+ removeTimeout: noop,
16
+ registerTimeout: noop
17
+ }
18
+ }
19
+
12
20
  let timer = null
13
21
  const vm = getCurrentInstance()
14
22
 
@@ -27,7 +27,7 @@ function update(el, ctx, value) {
27
27
 
28
28
  if (changed) {
29
29
  ctx.cfg = cfg
30
- ctx.observer?.unobserve(el)
30
+ ctx.observer?.disconnect()
31
31
 
32
32
  ctx.observer = new IntersectionObserver(([entry]) => {
33
33
  if (typeof ctx.handler === 'function') {
@@ -55,7 +55,7 @@ function destroy(el) {
55
55
  const ctx = el.__qvisible
56
56
 
57
57
  if (ctx !== void 0) {
58
- ctx.observer?.unobserve(el)
58
+ ctx.observer?.disconnect()
59
59
  delete el.__qvisible
60
60
  }
61
61
  }
@@ -49,7 +49,9 @@ export default createDirective(
49
49
  ? keyCodes[key.toLowerCase()]
50
50
  : parsedKey
51
51
 
52
- if (keyCode >= 0) acc.push(keyCode)
52
+ if (keyCode !== void 0) {
53
+ acc.push(...[keyCode].flat())
54
+ }
53
55
  }
54
56
  return acc
55
57
  }, [])
@@ -20,7 +20,11 @@ function read(string) {
20
20
  // Replace server-side written pluses with spaces.
21
21
  // If we can't decode the cookie, ignore it, it's unusable.
22
22
  // If we can't parse the cookie, ignore it, it's unusable.
23
- string = decodeURIComponent(string.replaceAll('+', ' '))
23
+ try {
24
+ string = decodeURIComponent(string.replaceAll('+', ' '))
25
+ } catch {
26
+ return
27
+ }
24
28
 
25
29
  try {
26
30
  const parsed = JSON.parse(string)
@@ -56,7 +60,12 @@ function parseExpireToSeconds(str) {
56
60
  totalSeconds += value * numberUnitMultiplierMap[unit]
57
61
  }
58
62
 
59
- return hasMatch ? totalSeconds : void 0
63
+ if (hasMatch) return totalSeconds
64
+
65
+ const timestamp = Date.parse(str)
66
+ return Number.isNaN(timestamp)
67
+ ? void 0
68
+ : Math.round((timestamp - Date.now()) / 1000)
60
69
  }
61
70
 
62
71
  // oxlint-disable-next-line default-param-last
@@ -69,9 +78,12 @@ function set(key, val, opts = {}, ssr) {
69
78
  if (Number.isFinite(opts.expires)) {
70
79
  maxAge = Math.round(opts.expires * 86_400) // 86400 seconds in a day
71
80
  } else if (opts.expires instanceof Date) {
72
- maxAge = Math.round((opts.expires.getTime() - Date.now()) / 1000)
81
+ const timestamp = opts.expires.getTime()
82
+ if (Number.isFinite(timestamp)) {
83
+ maxAge = Math.round((timestamp - Date.now()) / 1000)
84
+ }
73
85
  }
74
- // String check (eg. "15m", "1h" -> must return seconds)
86
+ // String check (eg. "15m", "1h", or a date string)
75
87
  else if (typeof opts.expires === 'string') {
76
88
  maxAge = parseExpireToSeconds(opts.expires)
77
89
  }
@@ -104,13 +116,11 @@ function set(key, val, opts = {}, ssr) {
104
116
  let all = ssr.req.headers.cookie || ''
105
117
 
106
118
  if (maxAge !== void 0 && isDeletion) {
107
- const localVal = get(key, ssr)
108
- if (localVal !== void 0) {
109
- all = all
110
- .replace(`${key}=${localVal}; `, '')
111
- .replace(`; ${key}=${localVal}`, '')
112
- .replace(`${key}=${localVal}`, '')
113
- }
119
+ const encodedKey = encodeURIComponent(key)
120
+ all = all
121
+ .split('; ')
122
+ .filter(entry => entry.split('=', 1)[0] !== encodedKey)
123
+ .join('; ')
114
124
  } else {
115
125
  all = all ? `${keyValue}; ${all}` : keyValue
116
126
  }
@@ -135,13 +145,19 @@ function get(key, ssr) {
135
145
 
136
146
  for (; i < l; i++) {
137
147
  parts = cookies[i].split('=')
138
- name = decodeURIComponent(parts.shift())
148
+
149
+ try {
150
+ name = decodeURIComponent(parts.shift())
151
+ } catch {
152
+ continue
153
+ }
154
+
139
155
  cookie = parts.join('=')
140
156
 
141
157
  if (!key) {
142
158
  result[name] = cookie
143
159
  } else if (key === name) {
144
- result = read(cookie)
160
+ result = read(cookie) ?? null
145
161
  break
146
162
  }
147
163
  }
@@ -4,6 +4,48 @@ import extend from '../../utils/extend/extend.js'
4
4
  let updateId = null,
5
5
  currentClientMeta
6
6
 
7
+ const encodeHtmlChars = {
8
+ '&': '&amp;',
9
+ '<': '&lt;',
10
+ '>': '&gt;',
11
+ '"': '&quot;',
12
+ "'": '&#39;'
13
+ }
14
+
15
+ const encodeJsonChars = {
16
+ '<': String.raw`\u003C`,
17
+ '>': String.raw`\u003E`,
18
+ '&': String.raw`\u0026`,
19
+ '\u2028': String.raw`\u2028`,
20
+ '\u2029': String.raw`\u2029`
21
+ }
22
+
23
+ const validAttrName = /^[^\s"'>/=]+$/
24
+
25
+ function isValidAttrName(name) {
26
+ return (
27
+ validAttrName.test(name) && name.includes(String.fromCodePoint(0)) === false
28
+ )
29
+ }
30
+
31
+ function encodeHtml(value) {
32
+ return String(value).replaceAll(/[&<>"']/g, char => encodeHtmlChars[char])
33
+ }
34
+
35
+ function encodeJson(value) {
36
+ return JSON.stringify(value).replaceAll(
37
+ /[<>&\u2028\u2029]/g,
38
+ char => encodeJsonChars[char]
39
+ )
40
+ }
41
+
42
+ function protectRawText(value, tagName) {
43
+ return String(value).replaceAll(
44
+ new RegExp(`</${tagName}`, 'gi'),
45
+ `<\\/${tagName}`
46
+ )
47
+ }
48
+
7
49
  export const clientList = []
8
50
 
9
51
  function normalize(meta) {
@@ -148,15 +190,17 @@ function apply({ add, remove }) {
148
190
 
149
191
  function getAttr(seed) {
150
192
  return att => {
193
+ if (isValidAttrName(att) === false) return
194
+
151
195
  const val = seed[att]
152
- return att + (val !== true && val !== void 0 ? `="${val}"` : '')
196
+ return att + (val !== true && val !== void 0 ? `="${encodeHtml(val)}"` : '')
153
197
  }
154
198
  }
155
199
 
156
200
  function getHead(meta) {
157
201
  let output = ''
158
202
  if (meta.title) {
159
- output += `<title>${meta.title}</title>`
203
+ output += `<title>${encodeHtml(meta.title)}</title>`
160
204
  }
161
205
  ;['meta', 'link', 'script'].forEach(type => {
162
206
  const metaType = meta[type]
@@ -165,10 +209,12 @@ function getHead(meta) {
165
209
  const attrs = Object.keys(metaType[att])
166
210
  .filter(item => item !== 'innerHTML')
167
211
  .map(getAttr(metaType[att]))
212
+ .filter(Boolean)
168
213
 
169
- output += `<${type} ${attrs.join(' ')} data-qmeta="${att}">`
214
+ output += `<${type} ${attrs.join(' ')} data-qmeta="${encodeHtml(att)}">`
170
215
  if (type === 'script') {
171
- output += (metaType[att].innerHTML || '') + '</script>'
216
+ output +=
217
+ protectRawText(metaType[att].innerHTML || '', 'script') + '</script>'
172
218
  }
173
219
  }
174
220
  })
@@ -195,7 +241,9 @@ function injectServerMeta(ssrContext) {
195
241
  normalize(data)
196
242
 
197
243
  const nonce =
198
- ssrContext.nonce !== void 0 ? ` nonce="${ssrContext.nonce}"` : ''
244
+ ssrContext.nonce !== void 0
245
+ ? ` nonce="${encodeHtml(ssrContext.nonce)}"`
246
+ : ''
199
247
 
200
248
  const ctx = ssrContext._meta
201
249
 
@@ -204,7 +252,7 @@ function injectServerMeta(ssrContext) {
204
252
  if (htmlAttr.length !== 0) {
205
253
  ctx.htmlAttrs +=
206
254
  (ctx.htmlAttrs.length !== 0 ? ' ' : '') +
207
- htmlAttr.map(getAttr(data.htmlAttr)).join(' ')
255
+ htmlAttr.map(getAttr(data.htmlAttr)).filter(Boolean).join(' ')
208
256
  }
209
257
 
210
258
  ctx.headTags += getHead(data)
@@ -214,17 +262,17 @@ function injectServerMeta(ssrContext) {
214
262
  if (bodyAttr.length !== 0) {
215
263
  ctx.bodyAttrs +=
216
264
  (ctx.bodyAttrs.length !== 0 ? ' ' : '') +
217
- bodyAttr.map(getAttr(data.bodyAttr)).join(' ')
265
+ bodyAttr.map(getAttr(data.bodyAttr)).filter(Boolean).join(' ')
218
266
  }
219
267
 
220
268
  ctx.bodyTags +=
221
269
  Object.keys(data.noscript)
222
270
  .map(
223
271
  name =>
224
- `<noscript data-qmeta="${name}">${data.noscript[name]}</noscript>`
272
+ `<noscript data-qmeta="${encodeHtml(name)}">${protectRawText(data.noscript[name], 'noscript')}</noscript>`
225
273
  )
226
274
  .join('') +
227
- `<script${nonce} id="qmeta-init">window.__Q_META__=${delete data.noscript && JSON.stringify(data)}</script>`
275
+ `<script${nonce} id="qmeta-init">window.__Q_META__=${delete data.noscript && encodeJson(data)}</script>`
228
276
  }
229
277
 
230
278
  function updateClientMeta() {
@@ -58,6 +58,10 @@ export default function extend(...args) {
58
58
  for (; i < length; i++) {
59
59
  if ((options = args[i]) !== null) {
60
60
  for (name in options) {
61
+ if (name === '__proto__') {
62
+ continue
63
+ }
64
+
61
65
  src = target[name]
62
66
  copy = options[name]
63
67
 
@@ -82,6 +82,35 @@ describe('[extend API]', () => {
82
82
 
83
83
  expect(result.a()).toBe(6)
84
84
  })
85
+
86
+ test.each([true, false])('does not merge __proto__ (deep: %s)', deep => {
87
+ const target = {}
88
+ const source = JSON.parse(
89
+ '{"__proto__":{"polluted":"prototype pollution"}}'
90
+ )
91
+
92
+ extend(deep, target, source)
93
+
94
+ expect(Object.getPrototypeOf(target)).toBe(Object.prototype)
95
+ expect(Object.hasOwn(target, '__proto__')).toBe(false)
96
+ expect(Object.prototype.polluted).toBeUndefined()
97
+ })
98
+
99
+ test('preserves constructor and prototype data properties', () => {
100
+ const result = extend(
101
+ true,
102
+ {},
103
+ {
104
+ constructor: { name: 'custom' },
105
+ prototype: { type: 'custom' }
106
+ }
107
+ )
108
+
109
+ expect(Object.hasOwn(result, 'constructor')).toBe(true)
110
+ expect(Object.hasOwn(result, 'prototype')).toBe(true)
111
+ expect(result.constructor).toStrictEqual({ name: 'custom' })
112
+ expect(result.prototype).toStrictEqual({ type: 'custom' })
113
+ })
85
114
  })
86
115
  })
87
116
  })
@@ -12,7 +12,7 @@ let registered = 0,
12
12
  vpPendingUpdate = false,
13
13
  bodyLeft,
14
14
  bodyTop,
15
- href,
15
+ routePath,
16
16
  closeTimer = null
17
17
 
18
18
  function onAppleScroll(e) {
@@ -59,7 +59,7 @@ function apply(action) {
59
59
  bodyLeft = body.style.left
60
60
  bodyTop = body.style.top
61
61
 
62
- href = window.location.href
62
+ routePath = window.location.pathname
63
63
 
64
64
  body.style.left = `-${scrollPositionX}px`
65
65
  body.style.top = `-${scrollPositionY}px`
@@ -137,8 +137,8 @@ function apply(action) {
137
137
  body.style.left = bodyLeft
138
138
  body.style.top = bodyTop
139
139
 
140
- // scroll back only if route has not changed
141
- if (window.location.href === href) {
140
+ // scroll back only if route path has not changed
141
+ if (window.location.pathname === routePath) {
142
142
  window.scrollTo(scrollPositionX, scrollPositionY)
143
143
  }
144
144