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
@@ -31,6 +31,7 @@ function injectPlugin({ props, emit, helpers }) {
31
31
  const xhrs = ref([])
32
32
  const promises = ref([])
33
33
  const workingThreads = ref(0)
34
+ const abortedPromises = new WeakSet()
34
35
 
35
36
  const xhrProps = computed(() => ({
36
37
  url: getFn(props.url),
@@ -46,14 +47,17 @@ function injectPlugin({ props, emit, helpers }) {
46
47
  const isUploading = computed(() => workingThreads.value > 0)
47
48
  const isBusy = computed(() => promises.value.length !== 0)
48
49
 
49
- let abortPromises
50
+ const getLiveFiles = files =>
51
+ files.filter(file => helpers.files.value.includes(file))
50
52
 
51
53
  function abort() {
52
54
  xhrs.value.forEach(x => {
53
55
  x.abort()
54
56
  })
55
57
 
56
- if (promises.value.length !== 0) abortPromises = true
58
+ promises.value.forEach(promise => {
59
+ abortedPromises.add(promise)
60
+ })
57
61
  }
58
62
 
59
63
  function upload() {
@@ -72,54 +76,82 @@ function injectPlugin({ props, emit, helpers }) {
72
76
  function runFactory(files) {
73
77
  workingThreads.value++
74
78
 
79
+ const failed = (err, promise) => {
80
+ if (helpers.isAlive()) {
81
+ if (promise !== void 0) {
82
+ promises.value = promises.value.filter(p => p !== promise)
83
+ }
84
+
85
+ const liveFiles = getLiveFiles(files)
86
+
87
+ if (liveFiles.length !== 0) {
88
+ helpers.queuedFiles.value.push(...liveFiles)
89
+ liveFiles.forEach(f => {
90
+ helpers.updateFileStatus(f, 'failed')
91
+ })
92
+
93
+ emit('factoryFailed', err, liveFiles)
94
+ }
95
+
96
+ workingThreads.value--
97
+ }
98
+ }
99
+
75
100
  if (typeof props.factory !== 'function') {
76
- performUpload(files, {})
101
+ startUpload({})
77
102
  return
78
103
  }
79
104
 
80
- const res = props.factory(files)
105
+ let res
106
+ try {
107
+ res = props.factory(files)
108
+ } catch (err) {
109
+ failed(err)
110
+ return
111
+ }
81
112
 
82
- if (!res) {
83
- emit(
84
- 'factoryFailed',
85
- new Error('QUploader: factory() does not return properly'),
86
- files
87
- )
88
- workingThreads.value--
113
+ if (Object(res) !== res) {
114
+ failed(new Error('QUploader: factory() does not return properly'))
89
115
  } else if (
90
116
  typeof res.catch === 'function' &&
91
117
  typeof res.then === 'function'
92
118
  ) {
93
119
  promises.value.push(res)
94
120
 
95
- const failed = err => {
96
- if (helpers.isAlive()) {
97
- promises.value = promises.value.filter(p => p !== res)
98
-
99
- if (promises.value.length === 0) abortPromises = false
100
-
101
- helpers.queuedFiles.value.push(...files)
102
- files.forEach(f => {
103
- helpers.updateFileStatus(f, 'failed')
104
- })
105
-
106
- emit('factoryFailed', err, files)
107
- workingThreads.value--
108
- }
109
- }
110
-
111
121
  res
112
122
  .then(factory => {
113
- if (abortPromises) {
114
- failed(new Error('Aborted'))
123
+ if (abortedPromises.has(res)) {
124
+ failed(new Error('Aborted'), res)
125
+ } else if (Object(factory) !== factory) {
126
+ failed(
127
+ new Error('QUploader: factory() does not return properly'),
128
+ res
129
+ )
115
130
  } else if (helpers.isAlive()) {
116
131
  promises.value = promises.value.filter(p => p !== res)
117
- performUpload(files, factory)
132
+ startUpload(factory)
118
133
  }
119
134
  })
120
- .catch(failed)
135
+ .catch(err => {
136
+ failed(err, res)
137
+ })
121
138
  } else {
122
- performUpload(files, res || {})
139
+ startUpload(res)
140
+ }
141
+
142
+ function startUpload(factory) {
143
+ const liveFiles = getLiveFiles(files)
144
+
145
+ if (liveFiles.length === 0) {
146
+ workingThreads.value--
147
+ return
148
+ }
149
+
150
+ try {
151
+ performUpload(liveFiles, factory)
152
+ } catch (err) {
153
+ failed(err)
154
+ }
123
155
  }
124
156
  }
125
157
 
@@ -136,6 +168,10 @@ function injectPlugin({ props, emit, helpers }) {
136
168
 
137
169
  if (!url) {
138
170
  console.error('q-uploader: invalid or no URL specified')
171
+ helpers.queuedFiles.value.push(...files)
172
+ files.forEach(f => {
173
+ helpers.updateFileStatus(f, 'failed')
174
+ })
139
175
  workingThreads.value--
140
176
  return
141
177
  }
@@ -166,7 +202,7 @@ function injectPlugin({ props, emit, helpers }) {
166
202
  let size = localUploadedSize - uploadIndexSize
167
203
  for (let i = uploadIndex; size > 0 && i < files.length; i++) {
168
204
  const file = files[i],
169
- uploaded = size > file.size
205
+ uploaded = size >= file.size
170
206
 
171
207
  if (uploaded) {
172
208
  size -= file.size
@@ -186,19 +222,33 @@ function injectPlugin({ props, emit, helpers }) {
186
222
  if (xhr.readyState < 4) return
187
223
 
188
224
  if (xhr.status && xhr.status < 400) {
189
- helpers.uploadedFiles.value.push(...files)
190
- files.forEach(f => {
191
- helpers.updateFileStatus(f, 'uploaded')
192
- })
193
- emit('uploaded', { files, xhr })
225
+ const liveFiles = getLiveFiles(files)
226
+ const liveUploadSize = liveFiles.reduce(
227
+ (total, file) => total + file.size,
228
+ 0
229
+ )
230
+
231
+ helpers.uploadedSize.value += liveUploadSize - localUploadedSize
232
+
233
+ if (liveFiles.length !== 0) {
234
+ helpers.uploadedFiles.value.push(...liveFiles)
235
+ liveFiles.forEach(f => {
236
+ helpers.updateFileStatus(f, 'uploaded')
237
+ })
238
+ emit('uploaded', { files: liveFiles, xhr })
239
+ }
194
240
  } else {
195
241
  aborted = true
196
242
  helpers.uploadedSize.value -= localUploadedSize
197
- helpers.queuedFiles.value.push(...files)
198
- files.forEach(f => {
199
- helpers.updateFileStatus(f, 'failed')
200
- })
201
- emit('failed', { files, xhr })
243
+
244
+ const liveFiles = getLiveFiles(files)
245
+ if (liveFiles.length !== 0) {
246
+ helpers.queuedFiles.value.push(...liveFiles)
247
+ liveFiles.forEach(f => {
248
+ helpers.updateFileStatus(f, 'failed')
249
+ })
250
+ emit('failed', { files: liveFiles, xhr })
251
+ }
202
252
  }
203
253
 
204
254
  workingThreads.value--
@@ -103,7 +103,7 @@
103
103
  }
104
104
  },
105
105
  "returns": {
106
- "type": "String",
106
+ "type": "Array",
107
107
  "desc": "An array consisting of objects with header definitions"
108
108
  },
109
109
  "category": "upload"
@@ -138,8 +138,8 @@
138
138
  }
139
139
  },
140
140
  "returns": {
141
- "type": "String",
142
- "desc": "An array consists of objects with additional fields definitions (used by Form to be uploaded)"
141
+ "type": "Array",
142
+ "desc": "An array consisting of objects with additional field definitions (used by FormData to be uploaded)"
143
143
  },
144
144
  "category": "upload"
145
145
  },
@@ -264,7 +264,7 @@
264
264
  },
265
265
 
266
266
  "factory-failed": {
267
- "desc": "Emitted when factory function is supplied with a Promise which is rejected",
267
+ "desc": "Emitted when the factory function throws, returns an invalid value, or supplies a Promise which is rejected or aborted",
268
268
  "params": {
269
269
  "err": {
270
270
  "type": "Error",
@@ -388,6 +388,8 @@ export default function useField(state) {
388
388
  state.inputRef.value.value = null
389
389
  }
390
390
 
391
+ state.onClear?.()
392
+
391
393
  emit('update:modelValue', null)
392
394
  if (state.changeEvent) emit('change', null)
393
395
  emit('clear', props.modelValue)
@@ -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) {
@@ -1,16 +1,25 @@
1
1
  import {
2
2
  getCurrentInstance,
3
+ onActivated,
3
4
  onBeforeMount,
4
5
  onBeforeUnmount,
6
+ onDeactivated,
5
7
  onMounted,
6
8
  ref,
7
9
  watch
8
10
  } from 'vue'
9
11
 
10
12
  import History from '../../plugins/private.history/History.js'
11
- import { vmHasRouter } from '../../utils/private.vm/vm.js'
13
+ import { vmHasRouter, vmIsDestroyed } from '../../utils/private.vm/vm.js'
14
+ import useTick from '../use-tick/use-tick.js'
15
+ import useAnimationFrame from '../use-animation-frame/use-animation-frame.js'
16
+ import {
17
+ getHorizontalScrollPosition,
18
+ getVerticalScrollPosition
19
+ } from '../../utils/scroll/scroll.js'
12
20
 
13
21
  let counter = 0
22
+ let restoreState = null
14
23
 
15
24
  export const useFullscreenProps = {
16
25
  fullscreen: Boolean,
@@ -24,7 +33,10 @@ export default function useFullscreen() {
24
33
  const { props, emit, proxy } = vm
25
34
 
26
35
  let historyEntry, fullscreenFillerNode
36
+
27
37
  const inFullscreen = ref(false)
38
+ const { registerTick, removeTick } = useTick()
39
+ const { registerAnimationFrame, removeAnimationFrame } = useAnimationFrame()
28
40
 
29
41
  if (vmHasRouter(vm)) {
30
42
  watch(
@@ -56,7 +68,19 @@ export default function useFullscreen() {
56
68
  }
57
69
 
58
70
  function setFullscreen() {
59
- if (inFullscreen.value) return
71
+ if (vmIsDestroyed(vm) || inFullscreen.value) return
72
+
73
+ removeAnimationFrame()
74
+ removeTick()
75
+
76
+ if (counter === 0) {
77
+ restoreState = {
78
+ left: getHorizontalScrollPosition(window),
79
+ top: getVerticalScrollPosition(window),
80
+ width: window.innerWidth,
81
+ height: window.innerHeight
82
+ }
83
+ }
60
84
 
61
85
  inFullscreen.value = true
62
86
  proxy.$el.replaceWith(fullscreenFillerNode)
@@ -73,7 +97,7 @@ export default function useFullscreen() {
73
97
  History.add(historyEntry)
74
98
  }
75
99
 
76
- function exitFullscreen() {
100
+ function exitFullscreenImpl(shouldRestoreElement) {
77
101
  if (!inFullscreen.value) return
78
102
 
79
103
  if (historyEntry !== void 0) {
@@ -81,20 +105,48 @@ export default function useFullscreen() {
81
105
  historyEntry = void 0
82
106
  }
83
107
 
84
- fullscreenFillerNode.replaceWith(proxy.$el)
108
+ if (shouldRestoreElement === true) {
109
+ fullscreenFillerNode.replaceWith(proxy.$el)
110
+ } else {
111
+ fullscreenFillerNode.remove()
112
+ }
113
+
85
114
  inFullscreen.value = false
86
115
 
87
116
  counter = Math.max(0, counter - 1)
117
+ if (counter !== 0) return
88
118
 
89
- if (counter === 0) {
90
- document.body.classList.remove('q-body--fullscreen-mixin')
119
+ document.body.classList.remove('q-body--fullscreen-mixin')
91
120
 
92
- if (proxy.$el.scrollIntoView !== void 0) {
93
- setTimeout(() => {
121
+ if (restoreState === null) return
122
+
123
+ const { left, top, width, height } = restoreState
124
+ restoreState = null
125
+
126
+ if (vmIsDestroyed(vm)) return
127
+
128
+ registerTick(() => {
129
+ if (counter !== 0) return
130
+ registerAnimationFrame(() => {
131
+ if (counter !== 0) return
132
+
133
+ if (window.innerWidth !== width || window.innerHeight !== height) {
134
+ /**
135
+ * If user has resized the window while in fullscreen mode,
136
+ * we cannot restore the scroll position because it will be wrong.
137
+ * So we just scroll the element into view instead,
138
+ * which is the best we can do.
139
+ */
94
140
  proxy.$el.scrollIntoView()
95
- }, 0)
96
- }
97
- }
141
+ } else {
142
+ window.scrollTo(left, top)
143
+ }
144
+ })
145
+ })
146
+ }
147
+
148
+ function exitFullscreen() {
149
+ exitFullscreenImpl(true)
98
150
  }
99
151
 
100
152
  onBeforeMount(() => {
@@ -105,6 +157,14 @@ export default function useFullscreen() {
105
157
  if (props.fullscreen) setFullscreen()
106
158
  })
107
159
 
160
+ onDeactivated(() => {
161
+ // A direct child of KeepAlive has already been moved to its storage
162
+ // container at this point. Its filler must not move it back into the DOM.
163
+ exitFullscreenImpl(proxy.$el.isConnected)
164
+ })
165
+ onActivated(() => {
166
+ if (props.fullscreen) setFullscreen()
167
+ })
108
168
  onBeforeUnmount(exitFullscreen)
109
169
 
110
170
  // expose public methods
@@ -0,0 +1,158 @@
1
+ import { afterEach, describe, expect, test } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import { KeepAlive, defineComponent, h, ref } from 'vue'
4
+
5
+ import useFullscreen, {
6
+ useFullscreenEmits,
7
+ useFullscreenProps
8
+ } from './use-fullscreen.js'
9
+
10
+ let wrapper, mountTarget
11
+
12
+ afterEach(() => {
13
+ wrapper?.unmount()
14
+ mountTarget?.remove()
15
+ wrapper = mountTarget = void 0
16
+ })
17
+
18
+ const FullscreenComponent = defineComponent({
19
+ name: 'FullscreenComponent',
20
+ props: useFullscreenProps,
21
+ emits: useFullscreenEmits,
22
+
23
+ setup() {
24
+ const { inFullscreen } = useFullscreen()
25
+
26
+ return () =>
27
+ h(
28
+ 'section',
29
+ {
30
+ 'data-test': 'fullscreen-component',
31
+ 'data-fullscreen': inFullscreen.value
32
+ },
33
+ 'Fullscreen component'
34
+ )
35
+ }
36
+ })
37
+
38
+ const OtherComponent = defineComponent({
39
+ name: 'OtherComponent',
40
+ setup: () => () => h('section', { 'data-test': 'other-component' })
41
+ })
42
+
43
+ describe('[useFullscreen API]', () => {
44
+ describe('[Variables]', () => {
45
+ describe('[(variable)useFullscreenProps]', () => {
46
+ test('is defined correctly', () => {
47
+ expect(useFullscreenProps).toBeTypeOf('object')
48
+ expect(Object.keys(useFullscreenProps)).not.toHaveLength(0)
49
+ })
50
+ })
51
+
52
+ describe('[(variable)useFullscreenEmits]', () => {
53
+ test('is defined correctly', () => {
54
+ expect(Array.isArray(useFullscreenEmits)).toBe(true)
55
+ expect(useFullscreenEmits).not.toHaveLength(0)
56
+ })
57
+ })
58
+ })
59
+
60
+ describe('[Functions]', () => {
61
+ describe('[(function)default]', () => {
62
+ test('does not move a deactivated root back into the live DOM', async () => {
63
+ const showFullscreen = ref(true)
64
+
65
+ mountTarget = document.createElement('div')
66
+ document.body.append(mountTarget)
67
+
68
+ wrapper = mount(
69
+ defineComponent({
70
+ setup: () => () =>
71
+ h(KeepAlive, null, () =>
72
+ showFullscreen.value === true
73
+ ? h(FullscreenComponent, { fullscreen: true })
74
+ : h(OtherComponent)
75
+ )
76
+ }),
77
+ { attachTo: mountTarget }
78
+ )
79
+
80
+ await flushPromises()
81
+
82
+ const getFullscreenElement = () =>
83
+ document.body.querySelector('[data-test="fullscreen-component"]')
84
+
85
+ expect(getFullscreenElement()).not.toBeNull()
86
+ expect(getFullscreenElement().dataset.fullscreen).toBe('true')
87
+ expect(
88
+ document.body.classList.contains('q-body--fullscreen-mixin')
89
+ ).toBe(true)
90
+
91
+ showFullscreen.value = false
92
+ await flushPromises()
93
+
94
+ expect(getFullscreenElement()).toBeNull()
95
+ expect(wrapper.find('[data-test="other-component"]').exists()).toBe(
96
+ true
97
+ )
98
+ expect(
99
+ document.body.classList.contains('q-body--fullscreen-mixin')
100
+ ).toBe(false)
101
+
102
+ showFullscreen.value = true
103
+ await flushPromises()
104
+
105
+ expect(getFullscreenElement()).not.toBeNull()
106
+ expect(getFullscreenElement().dataset.fullscreen).toBe('true')
107
+ expect(
108
+ document.body.classList.contains('q-body--fullscreen-mixin')
109
+ ).toBe(true)
110
+ })
111
+
112
+ test('restores a nested fullscreen component to cached content', async () => {
113
+ const showFullscreen = ref(true)
114
+ const CachedComponent = defineComponent({
115
+ name: 'CachedComponent',
116
+ setup: () => () =>
117
+ h('main', null, [h(FullscreenComponent, { fullscreen: true })])
118
+ })
119
+
120
+ mountTarget = document.createElement('div')
121
+ document.body.append(mountTarget)
122
+
123
+ wrapper = mount(
124
+ defineComponent({
125
+ setup: () => () =>
126
+ h(KeepAlive, null, () =>
127
+ showFullscreen.value === true
128
+ ? h(CachedComponent)
129
+ : h(OtherComponent)
130
+ )
131
+ }),
132
+ { attachTo: mountTarget }
133
+ )
134
+
135
+ await flushPromises()
136
+
137
+ const getFullscreenElement = () =>
138
+ document.body.querySelector('[data-test="fullscreen-component"]')
139
+
140
+ expect(getFullscreenElement()).not.toBeNull()
141
+
142
+ showFullscreen.value = false
143
+ await flushPromises()
144
+
145
+ expect(getFullscreenElement()).toBeNull()
146
+ expect(wrapper.find('[data-test="other-component"]').exists()).toBe(
147
+ true
148
+ )
149
+
150
+ showFullscreen.value = true
151
+ await flushPromises()
152
+
153
+ expect(getFullscreenElement()).not.toBeNull()
154
+ expect(getFullscreenElement().dataset.fullscreen).toBe('true')
155
+ })
156
+ })
157
+ })
158
+ })
@@ -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(
@@ -0,0 +1,47 @@
1
+ import { getCurrentInstance, onBeforeUnmount, onDeactivated } from 'vue'
2
+
3
+ import { vmIsDestroyed } from '../../utils/private.vm/vm.js'
4
+ import { noop } from '../../utils/event/event.js'
5
+
6
+ /*
7
+ * Usage:
8
+ * registerAnimationFrame(fn)
9
+ * removeAnimationFrame()
10
+ */
11
+
12
+ export default function useAnimationFrame() {
13
+ if (__QUASAR_SSR_SERVER__) {
14
+ return {
15
+ removeAnimationFrame: noop,
16
+ registerAnimationFrame: noop
17
+ }
18
+ }
19
+
20
+ let frameId = null
21
+ const vm = getCurrentInstance()
22
+
23
+ function removeAnimationFrame() {
24
+ if (frameId !== null) {
25
+ cancelAnimationFrame(frameId)
26
+ frameId = null
27
+ }
28
+ }
29
+
30
+ onDeactivated(removeAnimationFrame)
31
+ onBeforeUnmount(removeAnimationFrame)
32
+
33
+ return {
34
+ removeAnimationFrame,
35
+
36
+ registerAnimationFrame(fn) {
37
+ removeAnimationFrame()
38
+
39
+ if (!vmIsDestroyed(vm)) {
40
+ frameId = requestAnimationFrame(() => {
41
+ frameId = null
42
+ fn()
43
+ })
44
+ }
45
+ }
46
+ }
47
+ }