You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FilesListVirtual.vue 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. <!--
  2. - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. -
  6. - @license AGPL-3.0-or-later
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <RecycleScroller ref="recycleScroller"
  24. class="files-list"
  25. key-field="source"
  26. :items="nodes"
  27. :item-size="55"
  28. :table-mode="true"
  29. item-class="files-list__row"
  30. item-tag="tr"
  31. list-class="files-list__body"
  32. list-tag="tbody"
  33. role="table">
  34. <template #default="{ item, active, index }">
  35. <!-- File row -->
  36. <FileEntry :active="active"
  37. :index="index"
  38. :is-mtime-available="isMtimeAvailable"
  39. :is-size-available="isSizeAvailable"
  40. :files-list-width="filesListWidth"
  41. :nodes="nodes"
  42. :source="item" />
  43. </template>
  44. <template #before>
  45. <!-- Accessibility description -->
  46. <caption class="hidden-visually">
  47. {{ currentView.caption || t('files', 'List of files and folders.') }}
  48. {{ t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') }}
  49. </caption>
  50. <!-- Thead-->
  51. <FilesListHeader :files-list-width="filesListWidth"
  52. :is-mtime-available="isMtimeAvailable"
  53. :is-size-available="isSizeAvailable"
  54. :nodes="nodes" />
  55. </template>
  56. <template #after>
  57. <!-- Tfoot-->
  58. <FilesListFooter :files-list-width="filesListWidth"
  59. :is-mtime-available="isMtimeAvailable"
  60. :is-size-available="isSizeAvailable"
  61. :nodes="nodes"
  62. :summary="summary" />
  63. </template>
  64. </RecycleScroller>
  65. </template>
  66. <script lang="ts">
  67. import { RecycleScroller } from 'vue-virtual-scroller'
  68. import { translate, translatePlural } from '@nextcloud/l10n'
  69. import Vue from 'vue'
  70. import FileEntry from './FileEntry.vue'
  71. import FilesListFooter from './FilesListFooter.vue'
  72. import FilesListHeader from './FilesListHeader.vue'
  73. import filesListWidthMixin from '../mixins/filesListWidth.ts'
  74. export default Vue.extend({
  75. name: 'FilesListVirtual',
  76. components: {
  77. RecycleScroller,
  78. FileEntry,
  79. FilesListHeader,
  80. FilesListFooter,
  81. },
  82. mixins: [
  83. filesListWidthMixin,
  84. ],
  85. props: {
  86. currentView: {
  87. type: Object,
  88. required: true,
  89. },
  90. nodes: {
  91. type: Array,
  92. required: true,
  93. },
  94. },
  95. data() {
  96. return {
  97. FileEntry,
  98. }
  99. },
  100. computed: {
  101. files() {
  102. return this.nodes.filter(node => node.type === 'file')
  103. },
  104. summaryFile() {
  105. const count = this.files.length
  106. return translatePlural('files', '{count} file', '{count} files', count, { count })
  107. },
  108. summaryFolder() {
  109. const count = this.nodes.length - this.files.length
  110. return translatePlural('files', '{count} folder', '{count} folders', count, { count })
  111. },
  112. summary() {
  113. return translate('files', '{summaryFile} and {summaryFolder}', this)
  114. },
  115. isMtimeAvailable() {
  116. // Hide mtime column on narrow screens
  117. if (this.filesListWidth < 768) {
  118. return false
  119. }
  120. return this.nodes.some(node => node.mtime !== undefined)
  121. },
  122. isSizeAvailable() {
  123. // Hide size column on narrow screens
  124. if (this.filesListWidth < 768) {
  125. return false
  126. }
  127. return this.nodes.some(node => node.attributes.size !== undefined)
  128. },
  129. },
  130. mounted() {
  131. // Make the root recycle scroller a table for proper semantics
  132. const slots = this.$el.querySelectorAll('.vue-recycle-scroller__slot')
  133. slots[0].setAttribute('role', 'thead')
  134. slots[1].setAttribute('role', 'tfoot')
  135. },
  136. methods: {
  137. getFileId(node) {
  138. return node.fileid
  139. },
  140. t: translate,
  141. },
  142. })
  143. </script>
  144. <style scoped lang="scss">
  145. .files-list {
  146. --row-height: 55px;
  147. --cell-margin: 14px;
  148. --checkbox-padding: calc((var(--row-height) - var(--checkbox-size)) / 2);
  149. --checkbox-size: 24px;
  150. --clickable-area: 44px;
  151. --icon-preview-size: 32px;
  152. display: block;
  153. overflow: auto;
  154. height: 100%;
  155. &::v-deep {
  156. // Table head, body and footer
  157. tbody, .vue-recycle-scroller__slot {
  158. display: flex;
  159. flex-direction: column;
  160. width: 100%;
  161. // Necessary for virtual scrolling absolute
  162. position: relative;
  163. }
  164. // Table header
  165. .vue-recycle-scroller__slot[role='thead'] {
  166. // Pinned on top when scrolling
  167. position: sticky;
  168. z-index: 10;
  169. top: 0;
  170. height: var(--row-height);
  171. background-color: var(--color-main-background);
  172. }
  173. tr {
  174. position: absolute;
  175. display: flex;
  176. align-items: center;
  177. width: 100%;
  178. border-bottom: 1px solid var(--color-border);
  179. user-select: none;
  180. }
  181. td, th {
  182. display: flex;
  183. align-items: center;
  184. flex: 0 0 auto;
  185. justify-content: left;
  186. width: var(--row-height);
  187. height: var(--row-height);
  188. margin: 0;
  189. padding: 0;
  190. color: var(--color-text-maxcontrast);
  191. border: none;
  192. // Columns should try to add any text
  193. // node wrapped in a span. That should help
  194. // with the ellipsis on overflow.
  195. span {
  196. overflow: hidden;
  197. white-space: nowrap;
  198. text-overflow: ellipsis;
  199. }
  200. }
  201. .files-list__row--failed {
  202. position: absolute;
  203. display: block;
  204. top: 0;
  205. left: 0;
  206. right: 0;
  207. bottom: 0;
  208. opacity: .1;
  209. z-index: -1;
  210. background: var(--color-error);
  211. }
  212. .files-list__row-checkbox {
  213. justify-content: center;
  214. .checkbox-radio-switch {
  215. display: flex;
  216. justify-content: center;
  217. --icon-size: var(--checkbox-size);
  218. label.checkbox-radio-switch__label {
  219. width: var(--clickable-area);
  220. height: var(--clickable-area);
  221. margin: 0;
  222. padding: calc((var(--clickable-area) - var(--checkbox-size)) / 2);
  223. }
  224. .checkbox-radio-switch__icon {
  225. margin: 0 !important;
  226. }
  227. }
  228. }
  229. // Hover state of the row should also change the favorite markers background
  230. .files-list__row:hover .favorite-marker-icon svg path {
  231. stroke: var(--color-background-dark);
  232. }
  233. // Entry preview or mime icon
  234. .files-list__row-icon {
  235. position: relative;
  236. display: flex;
  237. overflow: visible;
  238. align-items: center;
  239. // No shrinking or growing allowed
  240. flex: 0 0 var(--icon-preview-size);
  241. justify-content: center;
  242. width: var(--icon-preview-size);
  243. height: 100%;
  244. // Show same padding as the checkbox right padding for visual balance
  245. margin-right: var(--checkbox-padding);
  246. color: var(--color-primary-element);
  247. // Icon is also clickable
  248. * {
  249. cursor: pointer;
  250. }
  251. & > span {
  252. justify-content: flex-start;
  253. &:not(.files-list__row-icon-favorite) svg {
  254. width: var(--icon-preview-size);
  255. height: var(--icon-preview-size);
  256. }
  257. }
  258. &-preview {
  259. overflow: hidden;
  260. width: var(--icon-preview-size);
  261. height: var(--icon-preview-size);
  262. border-radius: var(--border-radius);
  263. background-repeat: no-repeat;
  264. // Center and contain the preview
  265. background-position: center;
  266. background-size: contain;
  267. }
  268. &-favorite {
  269. position: absolute;
  270. top: 0px;
  271. right: -10px;
  272. }
  273. }
  274. // Entry link
  275. .files-list__row-name {
  276. // Prevent link from overflowing
  277. overflow: hidden;
  278. // Take as much space as possible
  279. flex: 1 1 auto;
  280. a {
  281. display: flex;
  282. align-items: center;
  283. // Fill cell height and width
  284. width: 100%;
  285. height: 100%;
  286. // Necessary for flex grow to work
  287. min-width: 0;
  288. // Already added to the inner text, see rule below
  289. &:focus-visible {
  290. outline: none;
  291. }
  292. // Keyboard indicator a11y
  293. &:focus .files-list__row-name-text,
  294. &:focus-visible .files-list__row-name-text {
  295. outline: 2px solid var(--color-main-text) !important;
  296. border-radius: 20px;
  297. }
  298. }
  299. .files-list__row-name-text {
  300. color: var(--color-main-text);
  301. // Make some space for the outline
  302. padding: 5px 10px;
  303. margin-left: -10px;
  304. // Align two name and ext
  305. display: inline-flex;
  306. }
  307. .files-list__row-name-ext {
  308. color: var(--color-text-maxcontrast);
  309. }
  310. }
  311. // Rename form
  312. .files-list__row-rename {
  313. width: 100%;
  314. max-width: 600px;
  315. input {
  316. width: 100%;
  317. // Align with text, 0 - padding - border
  318. margin-left: -8px;
  319. padding: 2px 6px;
  320. border-width: 2px;
  321. &:invalid {
  322. // Show red border on invalid input
  323. border-color: var(--color-error);
  324. color: red;
  325. }
  326. }
  327. }
  328. .files-list__row-actions {
  329. width: auto;
  330. // Add margin to all cells after the actions
  331. & ~ td,
  332. & ~ th {
  333. margin: 0 var(--cell-margin);
  334. }
  335. button {
  336. .button-vue__text {
  337. // Remove bold from default button styling
  338. font-weight: normal;
  339. }
  340. &:not(:hover, :focus, :active) .button-vue__wrapper {
  341. // Also apply color-text-maxcontrast to non-active button
  342. color: var(--color-text-maxcontrast);
  343. }
  344. }
  345. }
  346. .files-list__row-mtime,
  347. .files-list__row-size {
  348. // Right align text
  349. justify-content: flex-end;
  350. width: calc(var(--row-height) * 1.5);
  351. // opacity varies with the size
  352. color: var(--color-main-text);
  353. // Icon is before text since size is right aligned
  354. .files-list__column-sort-button {
  355. padding: 0 16px 0 4px !important;
  356. .button-vue__wrapper {
  357. flex-direction: row;
  358. }
  359. }
  360. }
  361. .files-list__row-mtime {
  362. width: calc(var(--row-height) * 2);
  363. }
  364. .files-list__row-column-custom {
  365. width: calc(var(--row-height) * 2);
  366. }
  367. }
  368. }
  369. </style>