diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-18 18:10:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-18 18:10:54 +0000 |
commit | 042cd704b8177e7997af4a2ca90967d3178ccc3f (patch) | |
tree | 06fec320acf76fbb87df66810cd75fe4e7f2357c /vendor | |
parent | 346c2ebb5a818524c5d8d95dc6b9fc8c892e3b5c (diff) | |
download | gitlab-ce-042cd704b8177e7997af4a2ca90967d3178ccc3f.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'vendor')
9 files changed, 1303 insertions, 0 deletions
diff --git a/vendor/assets/javascripts/vue-virtual-scroller/package.json b/vendor/assets/javascripts/vue-virtual-scroller/package.json new file mode 100644 index 00000000000..0c6eec36ea5 --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/package.json @@ -0,0 +1,40 @@ +{ + "name": "vue-virtual-scroller", + "description": "Smooth scrolling for any amount of data", + "version": "1.0.10", + "author": { + "name": "Guillaume Chau", + "email": "guillaume.b.chau@gmail.com" + }, + "keywords": [ + "vue", + "vuejs", + "plugin" + ], + "license": "MIT", + "main": "src/index.js", + "scripts": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/Akryum/vue-virtual-scroller.git" + }, + "bugs": { + "url": "https://github.com/Akryum/vue-virtual-scroller/issues" + }, + "homepage": "https://github.com/Akryum/vue-virtual-scroller#readme", + "dependencies": { + "scrollparent": "^2.0.1", + "vue-observe-visibility": "^0.4.4", + "vue-resize": "^0.4.5" + }, + "peerDependencies": { + "vue": "^2.6.11" + }, + "devDependencies": { + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue new file mode 100644 index 00000000000..e9f3acea9d8 --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue @@ -0,0 +1,212 @@ +<template> + <RecycleScroller + ref="scroller" + :items="itemsWithSize" + :min-item-size="minItemSize" + :direction="direction" + key-field="id" + v-bind="$attrs" + @resize="onScrollerResize" + @visible="onScrollerVisible" + v-on="listeners" + > + <template slot-scope="{ item: itemWithSize, index, active }"> + <slot + v-bind="{ + item: itemWithSize.item, + index, + active, + itemWithSize + }" + /> + </template> + <template slot="before"> + <slot name="before" /> + </template> + <template slot="after"> + <slot name="after" /> + </template> + </RecycleScroller> +</template> + +<script> +import RecycleScroller from './RecycleScroller.vue' +import { props, simpleArray } from './common' + +export default { + name: 'DynamicScroller', + + components: { + RecycleScroller, + }, + + inheritAttrs: false, + + provide () { + if (typeof ResizeObserver !== 'undefined') { + this.$_resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + if (entry.target) { + const event = new CustomEvent( + 'resize', + { + detail: { + contentRect: entry.contentRect, + }, + }, + ) + entry.target.dispatchEvent(event) + } + } + }) + } + + return { + vscrollData: this.vscrollData, + vscrollParent: this, + vscrollResizeObserver: this.$_resizeObserver, + } + }, + + props: { + ...props, + + minItemSize: { + type: [Number, String], + required: true, + }, + }, + + data () { + return { + vscrollData: { + active: true, + sizes: {}, + validSizes: {}, + keyField: this.keyField, + simpleArray: false, + }, + } + }, + + computed: { + simpleArray, + + itemsWithSize () { + const result = [] + const { items, keyField, simpleArray } = this + const sizes = this.vscrollData.sizes + for (let i = 0; i < items.length; i++) { + const item = items[i] + const id = simpleArray ? i : item[keyField] + let size = sizes[id] + if (typeof size === 'undefined' && !this.$_undefinedMap[id]) { + size = 0 + } + result.push({ + item, + id, + size, + }) + } + return result + }, + + listeners () { + const listeners = {} + for (const key in this.$listeners) { + if (key !== 'resize' && key !== 'visible') { + listeners[key] = this.$listeners[key] + } + } + return listeners + }, + }, + + watch: { + items () { + this.forceUpdate(false) + }, + + simpleArray: { + handler (value) { + this.vscrollData.simpleArray = value + }, + immediate: true, + }, + + direction (value) { + this.forceUpdate(true) + }, + }, + + created () { + this.$_updates = [] + this.$_undefinedSizes = 0 + this.$_undefinedMap = {} + }, + + activated () { + this.vscrollData.active = true + }, + + deactivated () { + this.vscrollData.active = false + }, + + methods: { + onScrollerResize () { + const scroller = this.$refs.scroller + if (scroller) { + this.forceUpdate() + } + this.$emit('resize') + }, + + onScrollerVisible () { + this.$emit('vscroll:update', { force: false }) + this.$emit('visible') + }, + + forceUpdate (clear = true) { + if (clear || this.simpleArray) { + this.vscrollData.validSizes = {} + } + this.$emit('vscroll:update', { force: true }) + }, + + scrollToItem (index) { + const scroller = this.$refs.scroller + if (scroller) scroller.scrollToItem(index) + }, + + getItemSize (item, index = undefined) { + const id = this.simpleArray ? (index != null ? index : this.items.indexOf(item)) : item[this.keyField] + return this.vscrollData.sizes[id] || 0 + }, + + scrollToBottom () { + if (this.$_scrollingToBottom) return + this.$_scrollingToBottom = true + const el = this.$el + // Item is inserted to the DOM + this.$nextTick(() => { + el.scrollTop = el.scrollHeight + 5000 + // Item sizes are computed + const cb = () => { + el.scrollTop = el.scrollHeight + 5000 + requestAnimationFrame(() => { + el.scrollTop = el.scrollHeight + 5000 + if (this.$_undefinedSizes === 0) { + this.$_scrollingToBottom = false + } else { + requestAnimationFrame(cb) + } + }) + } + requestAnimationFrame(cb) + }) + }, + }, +} +</script> diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScrollerItem.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScrollerItem.vue new file mode 100644 index 00000000000..3db24018ad0 --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScrollerItem.vue @@ -0,0 +1,218 @@ +<script> +export default { + name: 'DynamicScrollerItem', + + inject: [ + 'vscrollData', + 'vscrollParent', + 'vscrollResizeObserver', + ], + + props: { + // eslint-disable-next-line vue/require-prop-types + item: { + required: true, + }, + + watchData: { + type: Boolean, + default: false, + }, + + /** + * Indicates if the view is actively used to display an item. + */ + active: { + type: Boolean, + required: true, + }, + + index: { + type: Number, + default: undefined, + }, + + sizeDependencies: { + type: [Array, Object], + default: null, + }, + + emitResize: { + type: Boolean, + default: false, + }, + + tag: { + type: String, + default: 'div', + }, + }, + + computed: { + id () { + return this.vscrollData.simpleArray ? this.index : this.item[this.vscrollData.keyField] + }, + + size () { + return (this.vscrollData.validSizes[this.id] && this.vscrollData.sizes[this.id]) || 0 + }, + + finalActive () { + return this.active && this.vscrollData.active + }, + }, + + watch: { + watchData: 'updateWatchData', + + id () { + if (!this.size) { + this.onDataUpdate() + } + }, + + finalActive (value) { + if (!this.size) { + if (value) { + if (!this.vscrollParent.$_undefinedMap[this.id]) { + this.vscrollParent.$_undefinedSizes++ + this.vscrollParent.$_undefinedMap[this.id] = true + } + } else { + if (this.vscrollParent.$_undefinedMap[this.id]) { + this.vscrollParent.$_undefinedSizes-- + this.vscrollParent.$_undefinedMap[this.id] = false + } + } + } + + if (this.vscrollResizeObserver) { + if (value) { + this.observeSize() + } else { + this.unobserveSize() + } + } else if (value && this.$_pendingVScrollUpdate === this.id) { + this.updateSize() + } + }, + }, + + created () { + if (this.$isServer) return + + this.$_forceNextVScrollUpdate = null + this.updateWatchData() + + if (!this.vscrollResizeObserver) { + for (const k in this.sizeDependencies) { + this.$watch(() => this.sizeDependencies[k], this.onDataUpdate) + } + + this.vscrollParent.$on('vscroll:update', this.onVscrollUpdate) + this.vscrollParent.$on('vscroll:update-size', this.onVscrollUpdateSize) + } + }, + + mounted () { + if (this.vscrollData.active) { + this.updateSize() + this.observeSize() + } + }, + + beforeDestroy () { + this.vscrollParent.$off('vscroll:update', this.onVscrollUpdate) + this.vscrollParent.$off('vscroll:update-size', this.onVscrollUpdateSize) + this.unobserveSize() + }, + + methods: { + updateSize () { + if (this.finalActive) { + if (this.$_pendingSizeUpdate !== this.id) { + this.$_pendingSizeUpdate = this.id + this.$_forceNextVScrollUpdate = null + this.$_pendingVScrollUpdate = null + this.computeSize(this.id) + } + } else { + this.$_forceNextVScrollUpdate = this.id + } + }, + + updateWatchData () { + if (this.watchData) { + this.$_watchData = this.$watch('data', () => { + this.onDataUpdate() + }, { + deep: true, + }) + } else if (this.$_watchData) { + this.$_watchData() + this.$_watchData = null + } + }, + + onVscrollUpdate ({ force }) { + // If not active, sechedule a size update when it becomes active + if (!this.finalActive && force) { + this.$_pendingVScrollUpdate = this.id + } + + if (this.$_forceNextVScrollUpdate === this.id || force || !this.size) { + this.updateSize() + } + }, + + onDataUpdate () { + this.updateSize() + }, + + computeSize (id) { + this.$nextTick(() => { + if (this.id === id) { + const width = this.$el.offsetWidth + const height = this.$el.offsetHeight + this.applySize(width, height) + } + this.$_pendingSizeUpdate = null + }) + }, + + applySize (width, height) { + const size = Math.round(this.vscrollParent.direction === 'vertical' ? height : width) + if (size && this.size !== size) { + if (this.vscrollParent.$_undefinedMap[this.id]) { + this.vscrollParent.$_undefinedSizes-- + this.vscrollParent.$_undefinedMap[this.id] = undefined + } + this.$set(this.vscrollData.sizes, this.id, size) + this.$set(this.vscrollData.validSizes, this.id, true) + if (this.emitResize) this.$emit('resize', this.id) + } + }, + + observeSize () { + if (!this.vscrollResizeObserver) return + this.vscrollResizeObserver.observe(this.$el.parentNode) + this.$el.parentNode.addEventListener('resize', this.onResize) + }, + + unobserveSize () { + if (!this.vscrollResizeObserver) return + this.vscrollResizeObserver.unobserve(this.$el.parentNode) + this.$el.parentNode.removeEventListener('resize', this.onResize) + }, + + onResize (event) { + const { width, height } = event.detail.contentRect + this.applySize(width, height) + }, + }, + + render (h) { + return h(this.tag, this.$slots.default) + }, +} +</script> diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue new file mode 100644 index 00000000000..5e9661a53c8 --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue @@ -0,0 +1,657 @@ +<template> + <div + v-observe-visibility="handleVisibilityChange" + class="vue-recycle-scroller" + :class="{ + ready, + 'page-mode': pageMode, + [`direction-${direction}`]: true, + }" + @scroll.passive="handleScroll" + > + <div + v-if="$slots.before" + class="vue-recycle-scroller__slot" + > + <slot + name="before" + /> + </div> + + <div + ref="wrapper" + :style="{ [direction === 'vertical' ? 'minHeight' : 'minWidth']: totalSize + 'px' }" + class="vue-recycle-scroller__item-wrapper" + > + <div + v-for="view of pool" + :key="view.nr.id" + :style="ready ? { + transform: useTransform ? `translate${direction === 'vertical' ? 'Y' : 'X'}(${view.position}px)` : null, + top: !useTransform && direction === 'vertical' ? `${view.position}px` : null, + left: !useTransform && direction !== 'vertical' ? `${view.position}px` : null, + } : null" + class="vue-recycle-scroller__item-view" + :class="{ hover: hoverKey === view.nr.key }" + @mouseenter="hoverKey = view.nr.key" + @mouseleave="hoverKey = null" + > + <slot + :item="view.item" + :index="view.nr.index" + :active="view.nr.used" + /> + </div> + </div> + + <div + v-if="$slots.after" + class="vue-recycle-scroller__slot" + > + <slot + name="after" + /> + </div> + + <ResizeObserver @notify="handleResize" /> + </div> +</template> + +<script> +import { ResizeObserver } from 'vue-resize' +import { ObserveVisibility } from 'vue-observe-visibility' +import ScrollParent from 'scrollparent' +import config from '../config' +import { props, simpleArray } from './common' +import { supportsPassive } from '../utils' + +let uid = 0 + +export default { + name: 'RecycleScroller', + + components: { + ResizeObserver, + }, + + directives: { + ObserveVisibility, + }, + + props: { + ...props, + + itemSize: { + type: Number, + default: null, + }, + + minItemSize: { + type: [Number, String], + default: null, + }, + + sizeField: { + type: String, + default: 'size', + }, + + typeField: { + type: String, + default: 'type', + }, + + buffer: { + type: Number, + default: 200, + }, + + pageMode: { + type: Boolean, + default: false, + }, + + prerender: { + type: Number, + default: 0, + }, + + emitUpdate: { + type: Boolean, + default: false, + }, + + useTransform: { + type: Boolean, + default: true, + } + }, + + data () { + return { + pool: [], + totalSize: 0, + ready: false, + hoverKey: null, + } + }, + + computed: { + sizes () { + if (this.itemSize === null) { + const sizes = { + '-1': { accumulator: 0 }, + } + const items = this.items + const field = this.sizeField + const minItemSize = this.minItemSize + let computedMinSize = 10000 + let accumulator = 0 + let current + for (let i = 0, l = items.length; i < l; i++) { + current = items[i][field] || minItemSize + if (current < computedMinSize) { + computedMinSize = current + } + accumulator += current + sizes[i] = { accumulator, size: current } + } + // eslint-disable-next-line + this.$_computedMinItemSize = computedMinSize + return sizes + } + return [] + }, + + simpleArray, + }, + + watch: { + items () { + this.updateVisibleItems(true) + }, + + pageMode () { + this.applyPageMode() + this.updateVisibleItems(false) + }, + + sizes: { + handler () { + this.updateVisibleItems(false) + }, + deep: true, + }, + }, + + created () { + this.$_startIndex = 0 + this.$_endIndex = 0 + this.$_views = new Map() + this.$_unusedViews = new Map() + this.$_scrollDirty = false + this.$_lastUpdateScrollPosition = 0 + + // In SSR mode, we also prerender the same number of item for the first render + // to avoir mismatch between server and client templates + if (this.prerender) { + this.$_prerender = true + this.updateVisibleItems(false) + } + }, + + mounted () { + this.applyPageMode() + this.$nextTick(() => { + // In SSR mode, render the real number of visible items + this.$_prerender = false + this.updateVisibleItems(true) + this.ready = true + }) + }, + + beforeDestroy () { + this.removeListeners() + }, + + methods: { + addView (pool, index, item, key, type) { + const view = { + item, + position: 0, + } + const nonReactive = { + id: uid++, + index, + used: true, + key, + type, + } + Object.defineProperty(view, 'nr', { + configurable: false, + value: nonReactive, + }) + pool.push(view) + return view + }, + + unuseView (view, fake = false) { + const unusedViews = this.$_unusedViews + const type = view.nr.type + let unusedPool = unusedViews.get(type) + if (!unusedPool) { + unusedPool = [] + unusedViews.set(type, unusedPool) + } + unusedPool.push(view) + if (!fake) { + view.nr.used = false + view.position = -9999 + this.$_views.delete(view.nr.key) + } + }, + + handleResize () { + this.$emit('resize') + if (this.ready) this.updateVisibleItems(false) + }, + + handleScroll (event) { + if (!this.$_scrollDirty) { + this.$_scrollDirty = true + requestAnimationFrame(() => { + this.$_scrollDirty = false + const { continuous } = this.updateVisibleItems(false, true) + + // It seems sometimes chrome doesn't fire scroll event :/ + // When non continous scrolling is ending, we force a refresh + if (!continuous) { + clearTimeout(this.$_refreshTimout) + this.$_refreshTimout = setTimeout(this.handleScroll, 100) + } + }) + } + }, + + handleVisibilityChange (isVisible, entry) { + if (this.ready) { + if (isVisible || entry.boundingClientRect.width !== 0 || entry.boundingClientRect.height !== 0) { + this.$emit('visible') + requestAnimationFrame(() => { + this.updateVisibleItems(false) + }) + } else { + this.$emit('hidden') + } + } + }, + + updateVisibleItems (checkItem, checkPositionDiff = false) { + const itemSize = this.itemSize + const minItemSize = this.$_computedMinItemSize + const typeField = this.typeField + const keyField = this.simpleArray ? null : this.keyField + const items = this.items + const count = items.length + const sizes = this.sizes + const views = this.$_views + const unusedViews = this.$_unusedViews + const pool = this.pool + let startIndex, endIndex + let totalSize + + if (!count) { + startIndex = endIndex = totalSize = 0 + } else if (this.$_prerender) { + startIndex = 0 + endIndex = this.prerender + totalSize = null + } else { + const scroll = this.getScroll() + + // Skip update if use hasn't scrolled enough + if (checkPositionDiff) { + let positionDiff = scroll.start - this.$_lastUpdateScrollPosition + if (positionDiff < 0) positionDiff = -positionDiff + if ((itemSize === null && positionDiff < minItemSize) || positionDiff < itemSize) { + return { + continuous: true, + } + } + } + this.$_lastUpdateScrollPosition = scroll.start + + const buffer = this.buffer + scroll.start -= buffer + scroll.end += buffer + + // Variable size mode + if (itemSize === null) { + let h + let a = 0 + let b = count - 1 + let i = ~~(count / 2) + let oldI + + // Searching for startIndex + do { + oldI = i + h = sizes[i].accumulator + if (h < scroll.start) { + a = i + } else if (i < count - 1 && sizes[i + 1].accumulator > scroll.start) { + b = i + } + i = ~~((a + b) / 2) + } while (i !== oldI) + i < 0 && (i = 0) + startIndex = i + + // For container style + totalSize = sizes[count - 1].accumulator + + // Searching for endIndex + for (endIndex = i; endIndex < count && sizes[endIndex].accumulator < scroll.end; endIndex++); + if (endIndex === -1) { + endIndex = items.length - 1 + } else { + endIndex++ + // Bounds + endIndex > count && (endIndex = count) + } + } else { + // Fixed size mode + startIndex = ~~(scroll.start / itemSize) + endIndex = Math.ceil(scroll.end / itemSize) + + // Bounds + startIndex < 0 && (startIndex = 0) + endIndex > count && (endIndex = count) + + totalSize = count * itemSize + } + } + + if (endIndex - startIndex > config.itemsLimit) { + this.itemsLimitError() + } + + this.totalSize = totalSize + + let view + + const continuous = startIndex <= this.$_endIndex && endIndex >= this.$_startIndex + + if (this.$_continuous !== continuous) { + if (continuous) { + views.clear() + unusedViews.clear() + for (let i = 0, l = pool.length; i < l; i++) { + view = pool[i] + this.unuseView(view) + } + } + this.$_continuous = continuous + } else if (continuous) { + for (let i = 0, l = pool.length; i < l; i++) { + view = pool[i] + if (view.nr.used) { + // Update view item index + if (checkItem) { + view.nr.index = items.findIndex( + item => keyField ? item[keyField] === view.item[keyField] : item === view.item, + ) + } + + // Check if index is still in visible range + if ( + view.nr.index === -1 || + view.nr.index < startIndex || + view.nr.index >= endIndex + ) { + this.unuseView(view) + } + } + } + } + + const unusedIndex = continuous ? null : new Map() + + let item, type, unusedPool + let v + for (let i = startIndex; i < endIndex; i++) { + item = items[i] + const key = keyField ? item[keyField] : item + if (key == null) { + throw new Error(`Key is ${key} on item (keyField is '${keyField}')`) + } + view = views.get(key) + + if (!itemSize && !sizes[i].size) { + if (view) this.unuseView(view) + continue + } + + // No view assigned to item + if (!view) { + type = item[typeField] + unusedPool = unusedViews.get(type) + + if (continuous) { + // Reuse existing view + if (unusedPool && unusedPool.length) { + view = unusedPool.pop() + view.item = item + view.nr.used = true + view.nr.index = i + view.nr.key = key + view.nr.type = type + } else { + view = this.addView(pool, i, item, key, type) + } + } else { + // Use existing view + // We don't care if they are already used + // because we are not in continous scrolling + v = unusedIndex.get(type) || 0 + + if (!unusedPool || v >= unusedPool.length) { + view = this.addView(pool, i, item, key, type) + this.unuseView(view, true) + unusedPool = unusedViews.get(type) + } + + view = unusedPool[v] + view.item = item + view.nr.used = true + view.nr.index = i + view.nr.key = key + view.nr.type = type + unusedIndex.set(type, v + 1) + v++ + } + views.set(key, view) + } else { + view.nr.used = true + view.item = item + } + + // Update position + if (itemSize === null) { + view.position = sizes[i - 1].accumulator + } else { + view.position = i * itemSize + } + } + + this.$_startIndex = startIndex + this.$_endIndex = endIndex + + if (this.emitUpdate) this.$emit('update', startIndex, endIndex) + + // After the user has finished scrolling + // Sort views so text selection is correct + clearTimeout(this.$_sortTimer) + this.$_sortTimer = setTimeout(this.sortViews, 300) + + return { + continuous, + } + }, + + getListenerTarget () { + let target = ScrollParent(this.$el) + // Fix global scroll target for Chrome and Safari + if (window.document && (target === window.document.documentElement || target === window.document.body)) { + target = window + } + return target + }, + + getScroll () { + const { $el: el, direction } = this + const isVertical = direction === 'vertical' + let scrollState + + if (this.pageMode) { + const bounds = el.getBoundingClientRect() + const boundsSize = isVertical ? bounds.height : bounds.width + let start = -(isVertical ? bounds.top : bounds.left) + let size = isVertical ? window.innerHeight : window.innerWidth + if (start < 0) { + size += start + start = 0 + } + if (start + size > boundsSize) { + size = boundsSize - start + } + scrollState = { + start, + end: start + size, + } + } else if (isVertical) { + scrollState = { + start: el.scrollTop, + end: el.scrollTop + el.clientHeight, + } + } else { + scrollState = { + start: el.scrollLeft, + end: el.scrollLeft + el.clientWidth, + } + } + + return scrollState + }, + + applyPageMode () { + if (this.pageMode) { + this.addListeners() + } else { + this.removeListeners() + } + }, + + addListeners () { + this.listenerTarget = this.getListenerTarget() + this.listenerTarget.addEventListener('scroll', this.handleScroll, supportsPassive ? { + passive: true, + } : false) + this.listenerTarget.addEventListener('resize', this.handleResize) + }, + + removeListeners () { + if (!this.listenerTarget) { + return + } + + this.listenerTarget.removeEventListener('scroll', this.handleScroll) + this.listenerTarget.removeEventListener('resize', this.handleResize) + + this.listenerTarget = null + }, + + scrollToItem (index) { + let scroll + if (this.itemSize === null) { + scroll = index > 0 ? this.sizes[index - 1].accumulator : 0 + } else { + scroll = index * this.itemSize + } + this.scrollToPosition(scroll) + }, + + scrollToPosition (position) { + if (this.direction === 'vertical') { + this.$el.scrollTop = position + } else { + this.$el.scrollLeft = position + } + }, + + itemsLimitError () { + setTimeout(() => { + console.log('It seems the scroller element isn\'t scrolling, so it tries to render all the items at once.', 'Scroller:', this.$el) + console.log('Make sure the scroller has a fixed height (or width) and \'overflow-y\' (or \'overflow-x\') set to \'auto\' so it can scroll correctly and only render the items visible in the scroll viewport.') + }) + throw new Error('Rendered items limit reached') + }, + + sortViews () { + this.pool.sort((viewA, viewB) => viewA.nr.index - viewB.nr.index) + }, + }, +} +</script> + +<style> +.vue-recycle-scroller { + position: relative; +} + +.vue-recycle-scroller.direction-vertical:not(.page-mode) { + overflow-y: auto; +} + +.vue-recycle-scroller.direction-horizontal:not(.page-mode) { + overflow-x: auto; +} + +.vue-recycle-scroller.direction-horizontal { + display: flex; +} + +.vue-recycle-scroller__slot { + flex: auto 0 0; +} + +.vue-recycle-scroller__item-wrapper { + flex: 1; + box-sizing: border-box; + overflow: hidden; + position: relative; +} + +.vue-recycle-scroller.ready .vue-recycle-scroller__item-view { + position: absolute; + top: 0; + left: 0; + will-change: transform; +} + +.vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper { + width: 100%; +} + +.vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper { + height: 100%; +} + +.vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view { + width: 100%; +} + +.vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view { + height: 100%; +} +</style> diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/common.js b/vendor/assets/javascripts/vue-virtual-scroller/src/components/common.js new file mode 100644 index 00000000000..2121942152e --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/common.js @@ -0,0 +1,21 @@ +export const props = { + items: { + type: Array, + required: true, + }, + + keyField: { + type: String, + default: 'id', + }, + + direction: { + type: String, + default: 'vertical', + validator: (value) => ['vertical', 'horizontal'].includes(value), + }, +} + +export function simpleArray () { + return this.items.length && typeof this.items[0] !== 'object' +} diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/config.js b/vendor/assets/javascripts/vue-virtual-scroller/src/config.js new file mode 100644 index 00000000000..898ca7e027d --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/config.js @@ -0,0 +1,3 @@ +export default { + itemsLimit: 1000, +} diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/index.js b/vendor/assets/javascripts/vue-virtual-scroller/src/index.js new file mode 100644 index 00000000000..aa9733338f6 --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/index.js @@ -0,0 +1,60 @@ +/** + * See https://gitlab.com/gitlab-org/gitlab/-/issues/331267 for more information on this vendored + * dependency + */ + +import config from './config' + +import RecycleScroller from './components/RecycleScroller.vue' +import DynamicScroller from './components/DynamicScroller.vue' +import DynamicScrollerItem from './components/DynamicScrollerItem.vue' + +export { default as IdState } from './mixins/IdState' + +export { + RecycleScroller, + DynamicScroller, + DynamicScrollerItem, +} + +function registerComponents (Vue, prefix) { + Vue.component(`${prefix}recycle-scroller`, RecycleScroller) + Vue.component(`${prefix}RecycleScroller`, RecycleScroller) + Vue.component(`${prefix}dynamic-scroller`, DynamicScroller) + Vue.component(`${prefix}DynamicScroller`, DynamicScroller) + Vue.component(`${prefix}dynamic-scroller-item`, DynamicScrollerItem) + Vue.component(`${prefix}DynamicScrollerItem`, DynamicScrollerItem) +} + +const plugin = { + // eslint-disable-next-line no-undef + install (Vue, options) { + const finalOptions = Object.assign({}, { + installComponents: true, + componentsPrefix: '', + }, options) + + for (const key in finalOptions) { + if (typeof finalOptions[key] !== 'undefined') { + config[key] = finalOptions[key] + } + } + + if (finalOptions.installComponents) { + registerComponents(Vue, finalOptions.componentsPrefix) + } + }, +} + +export default plugin + +// Auto-install +let GlobalVue = null +if (typeof window !== 'undefined') { + GlobalVue = window.Vue +} else if (typeof global !== 'undefined') { + GlobalVue = global.Vue +} +if (GlobalVue) { + GlobalVue.use(plugin) +} diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/mixins/IdState.js b/vendor/assets/javascripts/vue-virtual-scroller/src/mixins/IdState.js new file mode 100644 index 00000000000..9b5bc57ab92 --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/mixins/IdState.js @@ -0,0 +1,79 @@ +import Vue from 'vue' + +export default function ({ + idProp = vm => vm.item.id, +} = {}) { + const store = {} + const vm = new Vue({ + data () { + return { + store, + } + }, + }) + + // @vue/component + return { + data () { + return { + idState: null, + } + }, + + created () { + this.$_id = null + if (typeof idProp === 'function') { + this.$_getId = () => idProp.call(this, this) + } else { + this.$_getId = () => this[idProp] + } + this.$watch(this.$_getId, { + handler (value) { + this.$nextTick(() => { + this.$_id = value + }) + }, + immediate: true, + }) + this.$_updateIdState() + }, + + beforeUpdate () { + this.$_updateIdState() + }, + + methods: { + /** + * Initialize an idState + * @param {number|string} id Unique id for the data + */ + $_idStateInit (id) { + const factory = this.$options.idState + if (typeof factory === 'function') { + const data = factory.call(this, this) + vm.$set(store, id, data) + this.$_id = id + return data + } else { + throw new Error('[mixin IdState] Missing `idState` function on component definition.') + } + }, + + /** + * Ensure idState is created and up-to-date + */ + $_updateIdState () { + const id = this.$_getId() + if (id == null) { + console.warn(`No id found for IdState with idProp: '${idProp}'.`) + } + if (id !== this.$_id) { + if (!store[id]) { + this.$_idStateInit(id) + } + this.idState = store[id] + } + }, + }, + } +} diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/utils.js b/vendor/assets/javascripts/vue-virtual-scroller/src/utils.js new file mode 100644 index 00000000000..40da6793e67 --- /dev/null +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/utils.js @@ -0,0 +1,13 @@ +export let supportsPassive = false + +if (typeof window !== 'undefined') { + supportsPassive = false + try { + var opts = Object.defineProperty({}, 'passive', { + get () { + supportsPassive = true + }, + }) + window.addEventListener('test', null, opts) + } catch (e) {} +} |