summaryrefslogtreecommitdiff
path: root/vendor
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-18 18:10:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-18 18:10:54 +0000
commit042cd704b8177e7997af4a2ca90967d3178ccc3f (patch)
tree06fec320acf76fbb87df66810cd75fe4e7f2357c /vendor
parent346c2ebb5a818524c5d8d95dc6b9fc8c892e3b5c (diff)
downloadgitlab-ce-042cd704b8177e7997af4a2ca90967d3178ccc3f.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'vendor')
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/package.json40
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue212
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScrollerItem.vue218
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue657
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/components/common.js21
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/config.js3
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/index.js60
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/mixins/IdState.js79
-rw-r--r--vendor/assets/javascripts/vue-virtual-scroller/src/utils.js13
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) {}
+}