quasar 2.21.4 → 2.23.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 (193) hide show
  1. package/README.md +1 -1
  2. package/dist/api/Mutation.json +1 -1
  3. package/dist/api/QLinearProgress.json +1 -1
  4. package/dist/api/QUploader.json +1 -1
  5. package/dist/icon-set/bootstrap-icons.umd.prod.js +1 -1
  6. package/dist/icon-set/eva-icons.umd.prod.js +1 -1
  7. package/dist/icon-set/fontawesome-v5-pro.umd.prod.js +1 -1
  8. package/dist/icon-set/fontawesome-v5.umd.prod.js +1 -1
  9. package/dist/icon-set/fontawesome-v6-pro.umd.prod.js +1 -1
  10. package/dist/icon-set/fontawesome-v6.umd.prod.js +1 -1
  11. package/dist/icon-set/fontawesome-v7-pro.umd.prod.js +1 -1
  12. package/dist/icon-set/fontawesome-v7.umd.prod.js +1 -1
  13. package/dist/icon-set/ionicons-v4.umd.prod.js +1 -1
  14. package/dist/icon-set/line-awesome.umd.prod.js +1 -1
  15. package/dist/icon-set/material-icons-outlined.umd.prod.js +1 -1
  16. package/dist/icon-set/material-icons-round.umd.prod.js +1 -1
  17. package/dist/icon-set/material-icons-sharp.umd.prod.js +1 -1
  18. package/dist/icon-set/material-icons.umd.prod.js +1 -1
  19. package/dist/icon-set/material-symbols-outlined.umd.prod.js +1 -1
  20. package/dist/icon-set/material-symbols-rounded.umd.prod.js +1 -1
  21. package/dist/icon-set/material-symbols-sharp.umd.prod.js +1 -1
  22. package/dist/icon-set/mdi-v3.umd.prod.js +1 -1
  23. package/dist/icon-set/mdi-v4.umd.prod.js +1 -1
  24. package/dist/icon-set/mdi-v5.umd.prod.js +1 -1
  25. package/dist/icon-set/mdi-v6.umd.prod.js +1 -1
  26. package/dist/icon-set/mdi-v7.umd.prod.js +1 -1
  27. package/dist/icon-set/svg-bootstrap-icons.umd.prod.js +1 -1
  28. package/dist/icon-set/svg-eva-icons.umd.prod.js +1 -1
  29. package/dist/icon-set/svg-fontawesome-v5.umd.prod.js +1 -1
  30. package/dist/icon-set/svg-fontawesome-v6.umd.prod.js +1 -1
  31. package/dist/icon-set/svg-fontawesome-v7.umd.prod.js +1 -1
  32. package/dist/icon-set/svg-ionicons-v4.umd.prod.js +1 -1
  33. package/dist/icon-set/svg-ionicons-v5.umd.prod.js +1 -1
  34. package/dist/icon-set/svg-ionicons-v6.umd.prod.js +1 -1
  35. package/dist/icon-set/svg-ionicons-v7.umd.prod.js +1 -1
  36. package/dist/icon-set/svg-ionicons-v8.umd.prod.js +1 -1
  37. package/dist/icon-set/svg-line-awesome.umd.prod.js +1 -1
  38. package/dist/icon-set/svg-material-icons-outlined.umd.prod.js +1 -1
  39. package/dist/icon-set/svg-material-icons-round.umd.prod.js +1 -1
  40. package/dist/icon-set/svg-material-icons-sharp.umd.prod.js +1 -1
  41. package/dist/icon-set/svg-material-icons.umd.prod.js +1 -1
  42. package/dist/icon-set/svg-material-symbols-outlined.umd.prod.js +1 -1
  43. package/dist/icon-set/svg-material-symbols-rounded.umd.prod.js +1 -1
  44. package/dist/icon-set/svg-material-symbols-sharp.umd.prod.js +1 -1
  45. package/dist/icon-set/svg-mdi-v6.umd.prod.js +1 -1
  46. package/dist/icon-set/svg-mdi-v7.umd.prod.js +1 -1
  47. package/dist/icon-set/svg-themify.umd.prod.js +1 -1
  48. package/dist/icon-set/themify.umd.prod.js +1 -1
  49. package/dist/lang/ar-TN.umd.prod.js +1 -1
  50. package/dist/lang/ar.umd.prod.js +1 -1
  51. package/dist/lang/az-Latn.umd.prod.js +1 -1
  52. package/dist/lang/bg.umd.prod.js +1 -1
  53. package/dist/lang/bn.umd.prod.js +1 -1
  54. package/dist/lang/bs-BA.umd.prod.js +1 -1
  55. package/dist/lang/ca.umd.prod.js +1 -1
  56. package/dist/lang/cs.umd.prod.js +1 -1
  57. package/dist/lang/da.umd.prod.js +1 -1
  58. package/dist/lang/de-CH.umd.prod.js +1 -1
  59. package/dist/lang/de-DE.umd.prod.js +1 -1
  60. package/dist/lang/de.umd.prod.js +1 -1
  61. package/dist/lang/el.umd.prod.js +1 -1
  62. package/dist/lang/en-GB.umd.prod.js +1 -1
  63. package/dist/lang/en-US.umd.prod.js +1 -1
  64. package/dist/lang/eo.umd.prod.js +1 -1
  65. package/dist/lang/es.umd.prod.js +1 -1
  66. package/dist/lang/et.umd.prod.js +1 -1
  67. package/dist/lang/eu.umd.prod.js +1 -1
  68. package/dist/lang/fa-IR.umd.prod.js +1 -1
  69. package/dist/lang/fa.umd.prod.js +1 -1
  70. package/dist/lang/fi.umd.prod.js +1 -1
  71. package/dist/lang/fr.umd.prod.js +1 -1
  72. package/dist/lang/gn.umd.prod.js +1 -1
  73. package/dist/lang/he.umd.prod.js +1 -1
  74. package/dist/lang/hi.umd.prod.js +1 -1
  75. package/dist/lang/hr.umd.prod.js +1 -1
  76. package/dist/lang/hu.umd.prod.js +1 -1
  77. package/dist/lang/id.umd.prod.js +1 -1
  78. package/dist/lang/is.umd.prod.js +1 -1
  79. package/dist/lang/it.umd.prod.js +1 -1
  80. package/dist/lang/ja.umd.prod.js +1 -1
  81. package/dist/lang/kk.umd.prod.js +1 -1
  82. package/dist/lang/km.umd.prod.js +1 -1
  83. package/dist/lang/ko-KR.umd.prod.js +1 -1
  84. package/dist/lang/kur-CKB.umd.prod.js +1 -1
  85. package/dist/lang/lb.umd.prod.js +1 -1
  86. package/dist/lang/lt.umd.prod.js +1 -1
  87. package/dist/lang/lu.umd.prod.js +1 -1
  88. package/dist/lang/lv.umd.prod.js +1 -1
  89. package/dist/lang/mk.umd.prod.js +1 -1
  90. package/dist/lang/ml.umd.prod.js +1 -1
  91. package/dist/lang/mm.umd.prod.js +1 -1
  92. package/dist/lang/ms-MY.umd.prod.js +1 -1
  93. package/dist/lang/ms.umd.prod.js +1 -1
  94. package/dist/lang/my.umd.prod.js +1 -1
  95. package/dist/lang/nb-NO.umd.prod.js +1 -1
  96. package/dist/lang/nl.umd.prod.js +1 -1
  97. package/dist/lang/pl.umd.prod.js +1 -1
  98. package/dist/lang/pt-BR.umd.prod.js +1 -1
  99. package/dist/lang/pt.umd.prod.js +1 -1
  100. package/dist/lang/ro.umd.prod.js +1 -1
  101. package/dist/lang/ru.umd.prod.js +1 -1
  102. package/dist/lang/sk.umd.prod.js +1 -1
  103. package/dist/lang/sl.umd.prod.js +1 -1
  104. package/dist/lang/sm.umd.prod.js +1 -1
  105. package/dist/lang/sq.umd.prod.js +1 -1
  106. package/dist/lang/sr-CYR.umd.prod.js +1 -1
  107. package/dist/lang/sr.umd.prod.js +1 -1
  108. package/dist/lang/sv.umd.prod.js +1 -1
  109. package/dist/lang/ta.umd.prod.js +1 -1
  110. package/dist/lang/th.umd.prod.js +1 -1
  111. package/dist/lang/tl.umd.prod.js +1 -1
  112. package/dist/lang/tr.umd.prod.js +1 -1
  113. package/dist/lang/ug.umd.prod.js +1 -1
  114. package/dist/lang/uk.umd.prod.js +1 -1
  115. package/dist/lang/ur-PK.umd.prod.js +1 -1
  116. package/dist/lang/uz-Cyrl.umd.prod.js +1 -1
  117. package/dist/lang/uz-Latn.umd.prod.js +1 -1
  118. package/dist/lang/vi.umd.prod.js +1 -1
  119. package/dist/lang/zh-CN.umd.prod.js +1 -1
  120. package/dist/lang/zh-TW.umd.prod.js +1 -1
  121. package/dist/quasar.client.js +654 -207
  122. package/dist/quasar.css +20 -11
  123. package/dist/quasar.prod.css +1 -1
  124. package/dist/quasar.rtl.css +25 -20
  125. package/dist/quasar.rtl.prod.css +1 -1
  126. package/dist/quasar.sass +21 -12
  127. package/dist/quasar.server.prod.cjs +35 -35
  128. package/dist/quasar.server.prod.js +25 -25
  129. package/dist/quasar.umd.js +654 -206
  130. package/dist/quasar.umd.prod.js +19 -19
  131. package/dist/transforms/import-map.json +1 -1
  132. package/dist/types/composables.d.ts +15 -5
  133. package/dist/types/index.d.ts +5 -5
  134. package/dist/web-types/web-types.json +1 -1
  135. package/package.json +6 -6
  136. package/src/components/btn/QBtn.js +1 -1
  137. package/src/components/carousel/QCarousel.js +2 -0
  138. package/src/components/chip/QChip.js +20 -4
  139. package/src/components/date/QDate.js +9 -2
  140. package/src/components/dialog/QDialog.js +9 -2
  141. package/src/components/editor/QEditor.js +18 -3
  142. package/src/components/expansion-item/QExpansionItem.js +6 -1
  143. package/src/components/fab/QFab.sass +11 -9
  144. package/src/components/input/QInput.js +42 -1
  145. package/src/components/input/QInput.json +7 -0
  146. package/src/components/input/use-mask.js +12 -4
  147. package/src/components/item/QItem.js +10 -1
  148. package/src/components/linear-progress/QLinearProgress.js +4 -11
  149. package/src/components/linear-progress/QLinearProgress.json +1 -1
  150. package/src/components/linear-progress/QLinearProgress.sass +7 -2
  151. package/src/components/menu/QMenu.js +9 -2
  152. package/src/components/range/QRange.js +2 -1
  153. package/src/components/rating/QRating.js +27 -8
  154. package/src/components/rating/QRating.sass +4 -0
  155. package/src/components/resize-observer/QResizeObserver.js +14 -3
  156. package/src/components/scroll-area/QScrollArea.js +69 -17
  157. package/src/components/slide-item/QSlideItem.js +3 -2
  158. package/src/components/slider/use-slider.js +7 -3
  159. package/src/components/stepper/StepHeader.js +11 -2
  160. package/src/components/table/QTable.js +11 -9
  161. package/src/components/table/QTh.js +20 -1
  162. package/src/components/table/table-column-selection.js +9 -1
  163. package/src/components/table/table-pagination.js +5 -2
  164. package/src/components/table/table-row-selection.js +10 -10
  165. package/src/components/tabs/QTabs.js +5 -6
  166. package/src/components/time/QTime.js +35 -10
  167. package/src/components/tooltip/QTooltip.js +112 -6
  168. package/src/components/tree/QTree.js +124 -4
  169. package/src/components/uploader/uploader-core.js +19 -4
  170. package/src/components/uploader/xhr-uploader-plugin.js +93 -43
  171. package/src/components/uploader/xhr-uploader-plugin.json +4 -4
  172. package/src/composables/private.use-field/use-field.js +2 -0
  173. package/src/composables/private.use-file/use-file.js +21 -12
  174. package/src/composables/private.use-fullscreen/use-fullscreen.js +71 -11
  175. package/src/composables/private.use-fullscreen/use-fullscreen.test.js +158 -0
  176. package/src/composables/private.use-model-toggle/use-model-toggle.js +2 -0
  177. package/src/composables/private.use-panel/use-panel.js +6 -2
  178. package/src/composables/use-animation-frame/use-animation-frame.js +47 -0
  179. package/src/composables/use-animation-frame/use-animation-frame.test.js +122 -0
  180. package/src/composables/use-interval/use-interval.js +8 -0
  181. package/src/composables/use-render-cache/use-render-cache.js +15 -10
  182. package/src/composables/use-timeout/use-timeout.js +8 -0
  183. package/src/composables.js +1 -1
  184. package/src/directives/intersection/Intersection.js +2 -2
  185. package/src/directives/mutation/Mutation.json +1 -1
  186. package/src/directives/touch-repeat/TouchRepeat.js +3 -1
  187. package/src/plugins/cookies/Cookies.js +29 -13
  188. package/src/plugins/meta/Meta.js +57 -9
  189. package/src/utils/extend/extend.js +4 -0
  190. package/src/utils/extend/extend.test.js +29 -0
  191. package/src/utils/private.click-outside/click-outside.js +27 -12
  192. package/src/utils/private.click-outside/click-outside.test.js +132 -0
  193. package/src/utils/scroll/prevent-scroll.js +4 -4
@@ -0,0 +1,122 @@
1
+ import { afterEach, describe, expect, test, vi } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { defineComponent } from 'vue'
4
+
5
+ import useAnimationFrame from './use-animation-frame.js'
6
+
7
+ let wrapper
8
+
9
+ afterEach(() => {
10
+ if (wrapper !== null) {
11
+ wrapper.unmount()
12
+ wrapper = null
13
+ }
14
+ })
15
+
16
+ function nextAnimationFrame() {
17
+ const { promise, resolve } = Promise.withResolvers()
18
+ requestAnimationFrame(resolve)
19
+ return promise
20
+ }
21
+
22
+ describe('[useAnimationFrame API]', () => {
23
+ describe('[Functions]', () => {
24
+ describe('[(function)default]', () => {
25
+ test('correctly registers an animation frame', async () => {
26
+ const fn = vi.fn()
27
+
28
+ wrapper = mount(
29
+ defineComponent({
30
+ template: '<div />',
31
+ setup() {
32
+ const { registerAnimationFrame } = useAnimationFrame()
33
+
34
+ registerAnimationFrame(fn)
35
+ return {}
36
+ }
37
+ })
38
+ )
39
+
40
+ expect(fn).not.toHaveBeenCalled()
41
+ await nextAnimationFrame()
42
+ expect(fn).toHaveBeenCalledTimes(1)
43
+ })
44
+
45
+ test('removeAnimationFrame works correctly', async () => {
46
+ const fn = vi.fn()
47
+
48
+ wrapper = mount(
49
+ defineComponent({
50
+ template: '<div />',
51
+ setup() {
52
+ const { registerAnimationFrame, removeAnimationFrame } =
53
+ useAnimationFrame()
54
+
55
+ registerAnimationFrame(fn)
56
+ return { registerAnimationFrame, removeAnimationFrame }
57
+ }
58
+ })
59
+ )
60
+
61
+ expect(fn).not.toHaveBeenCalled()
62
+ wrapper.vm.removeAnimationFrame()
63
+
64
+ await nextAnimationFrame()
65
+ expect(fn).not.toHaveBeenCalled()
66
+ })
67
+
68
+ test('unmount stops animation frame', async () => {
69
+ const fn = vi.fn()
70
+
71
+ wrapper = mount(
72
+ defineComponent({
73
+ template: '<div />',
74
+ setup() {
75
+ const { registerAnimationFrame } = useAnimationFrame()
76
+
77
+ registerAnimationFrame(fn)
78
+ return {}
79
+ }
80
+ })
81
+ )
82
+
83
+ expect(fn).not.toHaveBeenCalled()
84
+
85
+ wrapper.unmount()
86
+ wrapper = null
87
+
88
+ await nextAnimationFrame()
89
+
90
+ expect(fn).not.toHaveBeenCalled()
91
+ })
92
+
93
+ test('can override animation frame', async () => {
94
+ const fn1 = vi.fn()
95
+ const fn2 = vi.fn()
96
+
97
+ wrapper = mount(
98
+ defineComponent({
99
+ template: '<div />',
100
+ setup() {
101
+ const { registerAnimationFrame } = useAnimationFrame()
102
+ return { registerAnimationFrame }
103
+ }
104
+ })
105
+ )
106
+
107
+ expect(fn1).not.toHaveBeenCalled()
108
+ wrapper.vm.registerAnimationFrame(fn1)
109
+ expect(fn1).not.toHaveBeenCalled()
110
+
111
+ wrapper.vm.registerAnimationFrame(fn2)
112
+ expect(fn1).not.toHaveBeenCalled()
113
+ expect(fn2).not.toHaveBeenCalled()
114
+
115
+ await nextAnimationFrame()
116
+
117
+ expect(fn1).not.toHaveBeenCalled()
118
+ expect(fn2).toHaveBeenCalledTimes(1)
119
+ })
120
+ })
121
+ })
122
+ })
@@ -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
 
@@ -1,8 +1,8 @@
1
+ export { default as useAnimationFrame } from './composables/use-animation-frame/use-animation-frame.js'
1
2
  export { default as useDialogPluginComponent } from './composables/use-dialog-plugin-component/use-dialog-plugin-component.js'
2
3
  export { default as useFormChild } from './composables/use-form/use-form-child.js'
3
4
  export { default as useMeta } from './composables/use-meta/use-meta.js'
4
5
  export { default as useQuasar } from './composables/use-quasar/use-quasar.js'
5
-
6
6
  export { default as useHydration } from './composables/use-hydration/use-hydration.js'
7
7
  export { default as useId } from './composables/use-id/use-id.js'
8
8
  export { default as useInterval } from './composables/use-interval/use-interval.js'
@@ -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
  }
@@ -6,7 +6,7 @@
6
6
  "value": {
7
7
  "type": ["Function"],
8
8
  "desc": "Function to call when mutation occurs; It is HIGHLY recommended to reference it from your vue component scope, otherwise the directive update cycle will continuously recreate the observer which hits performance hard",
9
- "examples": ["# v-intersection=\"myHandler\""],
9
+ "examples": ["# v-mutation=\"myHandler\""],
10
10
 
11
11
  "returns": {
12
12
  "type": "Boolean",
@@ -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
  })
@@ -22,31 +22,46 @@ function globalHandler(evt) {
22
22
  return
23
23
  }
24
24
 
25
- // check last portal vm if it's
26
- // a QDialog and not in seamless mode
27
- let portalIndex = portalProxyList.length - 1
25
+ // a modal (non-seamless) QDialog shields popups opened before it,
26
+ // so only popups opened after it can be closed by this click (#18091)
27
+ let closableContentEls = null
28
+ let sawClosablePortal = false
28
29
 
29
- while (portalIndex >= 0) {
30
- const proxy = portalProxyList[portalIndex].$
30
+ for (let i = portalProxyList.length - 1; i >= 0; i--) {
31
+ const proxy = portalProxyList[i].$
32
+ const name = proxy.type.name
31
33
 
32
34
  // skip QTooltip portals
33
- if (proxy.type.name === 'QTooltip') {
34
- portalIndex--
35
+ if (name === 'QTooltip') {
35
36
  continue
36
37
  }
37
38
 
38
- if (proxy.type.name !== 'QDialog') {
39
- break
40
- }
39
+ if (name === 'QDialog') {
40
+ if (proxy.props.seamless !== true) {
41
+ if (sawClosablePortal === false) return
42
+
43
+ closableContentEls = new Set(
44
+ portalProxyList.slice(i + 1).map(vm => vm.contentEl)
45
+ )
46
+ break
47
+ }
41
48
 
42
- if (!proxy.props.seamless) return
49
+ continue
50
+ }
43
51
 
44
- portalIndex--
52
+ sawClosablePortal = true
45
53
  }
46
54
 
47
55
  for (let i = registeredList.length - 1; i >= 0; i--) {
48
56
  const state = registeredList[i]
49
57
 
58
+ if (
59
+ closableContentEls !== null &&
60
+ !closableContentEls.has(state.innerRef.value)
61
+ ) {
62
+ return
63
+ }
64
+
50
65
  if (
51
66
  (state.anchorEl.value === null ||
52
67
  !state.anchorEl.value.contains(target)) &&