mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Frontend refactor: move Vue related code from index.js to components dir, and remove unused codes. (#17301)
				
					
				
			* frontend refactor
* Apply suggestions from code review
Co-authored-by: delvh <dev.lh@web.de>
* Update templates/base/head.tmpl
Co-authored-by: delvh <dev.lh@web.de>
* Update docs/content/doc/developers/guidelines-frontend.md
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
* fix typo
* fix typo
* refactor PageData to pageData
* Apply suggestions from code review
Co-authored-by: delvh <dev.lh@web.de>
* Simply for the visual difference.
Co-authored-by: delvh <dev.lh@web.de>
* Revert "Apply suggestions from code review"
This reverts commit 4d78ad9b0e.
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: 6543 <6543@obermui.de>
			
			
This commit is contained in:
		| @@ -70,4 +70,3 @@ export default { | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <style scoped/> | ||||
|   | ||||
							
								
								
									
										370
									
								
								web_src/js/components/DashboardRepoList.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										370
									
								
								web_src/js/components/DashboardRepoList.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,370 @@ | ||||
| import Vue from 'vue'; | ||||
| import {initVueSvg, vueDelimiters} from './VueComponentLoader.js'; | ||||
|  | ||||
| const {AppSubUrl, AssetUrlPrefix, pageData} = window.config; | ||||
|  | ||||
| function initVueComponents() { | ||||
|   Vue.component('repo-search', { | ||||
|     delimiters: vueDelimiters, | ||||
|     props: { | ||||
|       searchLimit: { | ||||
|         type: Number, | ||||
|         default: 10 | ||||
|       }, | ||||
|       subUrl: { | ||||
|         type: String, | ||||
|         required: true | ||||
|       }, | ||||
|       uid: { | ||||
|         type: Number, | ||||
|         default: 0 | ||||
|       }, | ||||
|       teamId: { | ||||
|         type: Number, | ||||
|         required: false, | ||||
|         default: 0 | ||||
|       }, | ||||
|       organizations: { | ||||
|         type: Array, | ||||
|         default: () => [], | ||||
|       }, | ||||
|       isOrganization: { | ||||
|         type: Boolean, | ||||
|         default: true | ||||
|       }, | ||||
|       canCreateOrganization: { | ||||
|         type: Boolean, | ||||
|         default: false | ||||
|       }, | ||||
|       organizationsTotalCount: { | ||||
|         type: Number, | ||||
|         default: 0 | ||||
|       }, | ||||
|       moreReposLink: { | ||||
|         type: String, | ||||
|         default: '' | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|       const params = new URLSearchParams(window.location.search); | ||||
|  | ||||
|       let tab = params.get('repo-search-tab'); | ||||
|       if (!tab) { | ||||
|         tab = 'repos'; | ||||
|       } | ||||
|  | ||||
|       let reposFilter = params.get('repo-search-filter'); | ||||
|       if (!reposFilter) { | ||||
|         reposFilter = 'all'; | ||||
|       } | ||||
|  | ||||
|       let privateFilter = params.get('repo-search-private'); | ||||
|       if (!privateFilter) { | ||||
|         privateFilter = 'both'; | ||||
|       } | ||||
|  | ||||
|       let archivedFilter = params.get('repo-search-archived'); | ||||
|       if (!archivedFilter) { | ||||
|         archivedFilter = 'unarchived'; | ||||
|       } | ||||
|  | ||||
|       let searchQuery = params.get('repo-search-query'); | ||||
|       if (!searchQuery) { | ||||
|         searchQuery = ''; | ||||
|       } | ||||
|  | ||||
|       let page = 1; | ||||
|       try { | ||||
|         page = parseInt(params.get('repo-search-page')); | ||||
|       } catch { | ||||
|         // noop | ||||
|       } | ||||
|       if (!page) { | ||||
|         page = 1; | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|         tab, | ||||
|         repos: [], | ||||
|         reposTotalCount: 0, | ||||
|         reposFilter, | ||||
|         archivedFilter, | ||||
|         privateFilter, | ||||
|         page, | ||||
|         finalPage: 1, | ||||
|         searchQuery, | ||||
|         isLoading: false, | ||||
|         staticPrefix: AssetUrlPrefix, | ||||
|         counts: {}, | ||||
|         repoTypes: { | ||||
|           all: { | ||||
|             searchMode: '', | ||||
|           }, | ||||
|           forks: { | ||||
|             searchMode: 'fork', | ||||
|           }, | ||||
|           mirrors: { | ||||
|             searchMode: 'mirror', | ||||
|           }, | ||||
|           sources: { | ||||
|             searchMode: 'source', | ||||
|           }, | ||||
|           collaborative: { | ||||
|             searchMode: 'collaborative', | ||||
|           }, | ||||
|         } | ||||
|       }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|       // used in `repolist.tmpl` | ||||
|       showMoreReposLink() { | ||||
|         return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||
|       }, | ||||
|       searchURL() { | ||||
|         return `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery | ||||
|         }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode | ||||
|         }${this.reposFilter !== 'all' ? '&exclusive=1' : '' | ||||
|         }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' | ||||
|         }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : '' | ||||
|         }`; | ||||
|       }, | ||||
|       repoTypeCount() { | ||||
|         return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     mounted() { | ||||
|       this.changeReposFilter(this.reposFilter); | ||||
|       $(this.$el).find('.poping.up').popup(); | ||||
|       $(this.$el).find('.dropdown').dropdown(); | ||||
|       this.setCheckboxes(); | ||||
|       Vue.nextTick(() => { | ||||
|         this.$refs.search.focus(); | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|       changeTab(t) { | ||||
|         this.tab = t; | ||||
|         this.updateHistory(); | ||||
|       }, | ||||
|  | ||||
|       setCheckboxes() { | ||||
|         switch (this.archivedFilter) { | ||||
|           case 'unarchived': | ||||
|             $('#archivedFilterCheckbox').checkbox('set unchecked'); | ||||
|             break; | ||||
|           case 'archived': | ||||
|             $('#archivedFilterCheckbox').checkbox('set checked'); | ||||
|             break; | ||||
|           case 'both': | ||||
|             $('#archivedFilterCheckbox').checkbox('set indeterminate'); | ||||
|             break; | ||||
|           default: | ||||
|             this.archivedFilter = 'unarchived'; | ||||
|             $('#archivedFilterCheckbox').checkbox('set unchecked'); | ||||
|             break; | ||||
|         } | ||||
|         switch (this.privateFilter) { | ||||
|           case 'public': | ||||
|             $('#privateFilterCheckbox').checkbox('set unchecked'); | ||||
|             break; | ||||
|           case 'private': | ||||
|             $('#privateFilterCheckbox').checkbox('set checked'); | ||||
|             break; | ||||
|           case 'both': | ||||
|             $('#privateFilterCheckbox').checkbox('set indeterminate'); | ||||
|             break; | ||||
|           default: | ||||
|             this.privateFilter = 'both'; | ||||
|             $('#privateFilterCheckbox').checkbox('set indeterminate'); | ||||
|             break; | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       changeReposFilter(filter) { | ||||
|         this.reposFilter = filter; | ||||
|         this.repos = []; | ||||
|         this.page = 1; | ||||
|         Vue.set(this.counts, `${filter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|       updateHistory() { | ||||
|         const params = new URLSearchParams(window.location.search); | ||||
|  | ||||
|         if (this.tab === 'repos') { | ||||
|           params.delete('repo-search-tab'); | ||||
|         } else { | ||||
|           params.set('repo-search-tab', this.tab); | ||||
|         } | ||||
|  | ||||
|         if (this.reposFilter === 'all') { | ||||
|           params.delete('repo-search-filter'); | ||||
|         } else { | ||||
|           params.set('repo-search-filter', this.reposFilter); | ||||
|         } | ||||
|  | ||||
|         if (this.privateFilter === 'both') { | ||||
|           params.delete('repo-search-private'); | ||||
|         } else { | ||||
|           params.set('repo-search-private', this.privateFilter); | ||||
|         } | ||||
|  | ||||
|         if (this.archivedFilter === 'unarchived') { | ||||
|           params.delete('repo-search-archived'); | ||||
|         } else { | ||||
|           params.set('repo-search-archived', this.archivedFilter); | ||||
|         } | ||||
|  | ||||
|         if (this.searchQuery === '') { | ||||
|           params.delete('repo-search-query'); | ||||
|         } else { | ||||
|           params.set('repo-search-query', this.searchQuery); | ||||
|         } | ||||
|  | ||||
|         if (this.page === 1) { | ||||
|           params.delete('repo-search-page'); | ||||
|         } else { | ||||
|           params.set('repo-search-page', `${this.page}`); | ||||
|         } | ||||
|  | ||||
|         const queryString = params.toString(); | ||||
|         if (queryString) { | ||||
|           window.history.replaceState({}, '', `?${queryString}`); | ||||
|         } else { | ||||
|           window.history.replaceState({}, '', window.location.pathname); | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       toggleArchivedFilter() { | ||||
|         switch (this.archivedFilter) { | ||||
|           case 'both': | ||||
|             this.archivedFilter = 'unarchived'; | ||||
|             break; | ||||
|           case 'unarchived': | ||||
|             this.archivedFilter = 'archived'; | ||||
|             break; | ||||
|           case 'archived': | ||||
|             this.archivedFilter = 'both'; | ||||
|             break; | ||||
|           default: | ||||
|             this.archivedFilter = 'unarchived'; | ||||
|             break; | ||||
|         } | ||||
|         this.page = 1; | ||||
|         this.repos = []; | ||||
|         this.setCheckboxes(); | ||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|       togglePrivateFilter() { | ||||
|         switch (this.privateFilter) { | ||||
|           case 'both': | ||||
|             this.privateFilter = 'public'; | ||||
|             break; | ||||
|           case 'public': | ||||
|             this.privateFilter = 'private'; | ||||
|             break; | ||||
|           case 'private': | ||||
|             this.privateFilter = 'both'; | ||||
|             break; | ||||
|           default: | ||||
|             this.privateFilter = 'both'; | ||||
|             break; | ||||
|         } | ||||
|         this.page = 1; | ||||
|         this.repos = []; | ||||
|         this.setCheckboxes(); | ||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|  | ||||
|       changePage(page) { | ||||
|         this.page = page; | ||||
|         if (this.page > this.finalPage) { | ||||
|           this.page = this.finalPage; | ||||
|         } | ||||
|         if (this.page < 1) { | ||||
|           this.page = 1; | ||||
|         } | ||||
|         this.repos = []; | ||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|       searchRepos() { | ||||
|         this.isLoading = true; | ||||
|  | ||||
|         if (!this.reposTotalCount) { | ||||
|           const totalCountSearchURL = `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; | ||||
|           $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => { | ||||
|             this.reposTotalCount = request.getResponseHeader('X-Total-Count'); | ||||
|           }); | ||||
|         } | ||||
|  | ||||
|         const searchedMode = this.repoTypes[this.reposFilter].searchMode; | ||||
|         const searchedURL = this.searchURL; | ||||
|         const searchedQuery = this.searchQuery; | ||||
|  | ||||
|         $.getJSON(searchedURL, (result, _textStatus, request) => { | ||||
|           if (searchedURL === this.searchURL) { | ||||
|             this.repos = result.data; | ||||
|             const count = request.getResponseHeader('X-Total-Count'); | ||||
|             if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { | ||||
|               this.reposTotalCount = count; | ||||
|             } | ||||
|             Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, count); | ||||
|             this.finalPage = Math.ceil(count / this.searchLimit); | ||||
|             this.updateHistory(); | ||||
|           } | ||||
|         }).always(() => { | ||||
|           if (searchedURL === this.searchURL) { | ||||
|             this.isLoading = false; | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
|  | ||||
|       repoIcon(repo) { | ||||
|         if (repo.fork) { | ||||
|           return 'octicon-repo-forked'; | ||||
|         } else if (repo.mirror) { | ||||
|           return 'octicon-mirror'; | ||||
|         } else if (repo.template) { | ||||
|           return `octicon-repo-template`; | ||||
|         } else if (repo.private) { | ||||
|           return 'octicon-lock'; | ||||
|         } else if (repo.internal) { | ||||
|           return 'octicon-repo'; | ||||
|         } | ||||
|         return 'octicon-repo'; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
|  | ||||
| function initDashboardRepoList() { | ||||
|   const el = document.getElementById('dashboard-repo-list'); | ||||
|   const dashboardRepoListData = pageData.dashboardRepoList || null; | ||||
|   if (!el || !dashboardRepoListData) return; | ||||
|  | ||||
|   initVueSvg(); | ||||
|   initVueComponents(); | ||||
|   new Vue({ | ||||
|     el, | ||||
|     delimiters: vueDelimiters, | ||||
|     data: () => { | ||||
|       return { | ||||
|         searchLimit: dashboardRepoListData.searchLimit || 0, | ||||
|         subUrl: AppSubUrl, | ||||
|       }; | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export {initDashboardRepoList}; | ||||
| @@ -1,9 +1,9 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="activity-bar-graph" ref="style" style="width:0px;height:0px"/> | ||||
|     <div class="activity-bar-graph-alt" ref="altStyle" style="width:0px;height:0px"/> | ||||
|     <div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/> | ||||
|     <div class="activity-bar-graph-alt" ref="altStyle" style="width: 0; height: 0;"/> | ||||
|     <vue-bar-graph | ||||
|       :points="graphData" | ||||
|       :points="graphPoints" | ||||
|       :show-x-axis="true" | ||||
|       :show-y-axis="false" | ||||
|       :show-values="true" | ||||
| @@ -15,9 +15,9 @@ | ||||
|       :label-height="20" | ||||
|     > | ||||
|       <template #label="opt"> | ||||
|         <g v-for="(author, idx) in authors" :key="author.position"> | ||||
|         <g v-for="(author, idx) in graphAuthors" :key="author.position"> | ||||
|           <a | ||||
|             v-if="opt.bar.index === idx && author.home_link !== ''" | ||||
|             v-if="opt.bar.index === idx && author.home_link" | ||||
|             :href="author.home_link" | ||||
|           > | ||||
|             <image | ||||
| @@ -39,7 +39,7 @@ | ||||
|         </g> | ||||
|       </template> | ||||
|       <template #title="opt"> | ||||
|         <tspan v-for="(author, idx) in authors" :key="author.position"> | ||||
|         <tspan v-for="(author, idx) in graphAuthors" :key="author.position"> | ||||
|           <tspan v-if="opt.bar.index === idx"> | ||||
|             {{ author.name }} | ||||
|           </tspan> | ||||
| @@ -48,32 +48,39 @@ | ||||
|     </vue-bar-graph> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import VueBarGraph from 'vue-bar-graph'; | ||||
| import {initVueApp} from './VueComponentLoader.js'; | ||||
| 
 | ||||
| export default { | ||||
| const sfc = { | ||||
|   components: {VueBarGraph}, | ||||
|   props: { | ||||
|     data: {type: Array, default: () => []}, | ||||
|   }, | ||||
|   data: () => ({ | ||||
|     colors: { | ||||
|       barColor: 'green', | ||||
|       textColor: 'black', | ||||
|       textAltColor: 'white', | ||||
|     }, | ||||
| 
 | ||||
|     // possible keys: | ||||
|     // * avatar_link: (...) | ||||
|     // * commits: (...) | ||||
|     // * home_link: (...) | ||||
|     // * login: (...) | ||||
|     // * name: (...) | ||||
|     activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [], | ||||
|   }), | ||||
|   computed: { | ||||
|     graphData() { | ||||
|       return this.data.map((item) => { | ||||
|     graphPoints() { | ||||
|       return this.activityTopAuthors.map((item) => { | ||||
|         return { | ||||
|           value: item.commits, | ||||
|           label: item.name, | ||||
|         }; | ||||
|       }); | ||||
|     }, | ||||
|     authors() { | ||||
|       return this.data.map((item, idx) => { | ||||
|     graphAuthors() { | ||||
|       return this.activityTopAuthors.map((item, idx) => { | ||||
|         return { | ||||
|           position: idx + 1, | ||||
|           ...item, | ||||
| @@ -81,21 +88,23 @@ export default { | ||||
|       }); | ||||
|     }, | ||||
|     graphWidth() { | ||||
|       return this.data.length * 40; | ||||
|       return this.activityTopAuthors.length * 40; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     const st = window.getComputedStyle(this.$refs.style); | ||||
|     const stalt = window.getComputedStyle(this.$refs.altStyle); | ||||
|     const refStyle = window.getComputedStyle(this.$refs.style); | ||||
|     const refAltStyle = window.getComputedStyle(this.$refs.altStyle); | ||||
| 
 | ||||
|     this.colors.barColor = st.backgroundColor; | ||||
|     this.colors.textColor = st.color; | ||||
|     this.colors.textAltColor = stalt.color; | ||||
|   }, | ||||
|   methods: { | ||||
|     hasHomeLink(i) { | ||||
|       return this.graphData[i].homeLink !== '' && this.graphData[i].homeLink !== null; | ||||
|     }, | ||||
|     this.colors.barColor = refStyle.backgroundColor; | ||||
|     this.colors.textColor = refStyle.color; | ||||
|     this.colors.textAltColor = refAltStyle.color; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| function initRepoActivityTopAuthorsChart() { | ||||
|   initVueApp('#repo-activity-top-authors-chart', sfc); | ||||
| } | ||||
| 
 | ||||
| export default sfc; | ||||
| export {initRepoActivityTopAuthorsChart}; | ||||
| </script> | ||||
							
								
								
									
										161
									
								
								web_src/js/components/RepoBranchTagDropdown.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								web_src/js/components/RepoBranchTagDropdown.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| function initRepoBranchTagDropdown(selector) { | ||||
|   $(selector).each(function () { | ||||
|     const $dropdown = $(this); | ||||
|     const $data = $dropdown.find('.data'); | ||||
|     const data = { | ||||
|       items: [], | ||||
|       mode: $data.data('mode'), | ||||
|       searchTerm: '', | ||||
|       noResults: '', | ||||
|       canCreateBranch: false, | ||||
|       menuVisible: false, | ||||
|       createTag: false, | ||||
|       active: 0 | ||||
|     }; | ||||
|     $data.find('.item').each(function () { | ||||
|       data.items.push({ | ||||
|         name: $(this).text(), | ||||
|         url: $(this).data('url'), | ||||
|         branch: $(this).hasClass('branch'), | ||||
|         tag: $(this).hasClass('tag'), | ||||
|         selected: $(this).hasClass('selected') | ||||
|       }); | ||||
|     }); | ||||
|     $data.remove(); | ||||
|     new Vue({ | ||||
|       el: this, | ||||
|       delimiters: ['${', '}'], | ||||
|       data, | ||||
|       computed: { | ||||
|         filteredItems() { | ||||
|           const items = this.items.filter((item) => { | ||||
|             return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) && | ||||
|               (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase())); | ||||
|           }); | ||||
|  | ||||
|           // no idea how to fix this so linting rule is disabled instead | ||||
|           this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); // eslint-disable-line vue/no-side-effects-in-computed-properties | ||||
|           return items; | ||||
|         }, | ||||
|         showNoResults() { | ||||
|           return this.filteredItems.length === 0 && !this.showCreateNewBranch; | ||||
|         }, | ||||
|         showCreateNewBranch() { | ||||
|           if (!this.canCreateBranch || !this.searchTerm) { | ||||
|             return false; | ||||
|           } | ||||
|  | ||||
|           return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       watch: { | ||||
|         menuVisible(visible) { | ||||
|           if (visible) { | ||||
|             this.focusSearchField(); | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       beforeMount() { | ||||
|         this.noResults = this.$el.getAttribute('data-no-results'); | ||||
|         this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true'; | ||||
|  | ||||
|         document.body.addEventListener('click', (event) => { | ||||
|           if (this.$el.contains(event.target)) return; | ||||
|           if (this.menuVisible) { | ||||
|             Vue.set(this, 'menuVisible', false); | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
|  | ||||
|       methods: { | ||||
|         selectItem(item) { | ||||
|           const prev = this.getSelected(); | ||||
|           if (prev !== null) { | ||||
|             prev.selected = false; | ||||
|           } | ||||
|           item.selected = true; | ||||
|           window.location.href = item.url; | ||||
|         }, | ||||
|         createNewBranch() { | ||||
|           if (!this.showCreateNewBranch) return; | ||||
|           $(this.$refs.newBranchForm).trigger('submit'); | ||||
|         }, | ||||
|         focusSearchField() { | ||||
|           Vue.nextTick(() => { | ||||
|             this.$refs.searchField.focus(); | ||||
|           }); | ||||
|         }, | ||||
|         getSelected() { | ||||
|           for (let i = 0, j = this.items.length; i < j; ++i) { | ||||
|             if (this.items[i].selected) return this.items[i]; | ||||
|           } | ||||
|           return null; | ||||
|         }, | ||||
|         getSelectedIndexInFiltered() { | ||||
|           for (let i = 0, j = this.filteredItems.length; i < j; ++i) { | ||||
|             if (this.filteredItems[i].selected) return i; | ||||
|           } | ||||
|           return -1; | ||||
|         }, | ||||
|         scrollToActive() { | ||||
|           let el = this.$refs[`listItem${this.active}`]; | ||||
|           if (!el || !el.length) return; | ||||
|           if (Array.isArray(el)) { | ||||
|             el = el[0]; | ||||
|           } | ||||
|  | ||||
|           const cont = this.$refs.scrollContainer; | ||||
|           if (el.offsetTop < cont.scrollTop) { | ||||
|             cont.scrollTop = el.offsetTop; | ||||
|           } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { | ||||
|             cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; | ||||
|           } | ||||
|         }, | ||||
|         keydown(event) { | ||||
|           if (event.keyCode === 40) { // arrow down | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             if (this.active === -1) { | ||||
|               this.active = this.getSelectedIndexInFiltered(); | ||||
|             } | ||||
|  | ||||
|             if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) { | ||||
|               return; | ||||
|             } | ||||
|             this.active++; | ||||
|             this.scrollToActive(); | ||||
|           } else if (event.keyCode === 38) { // arrow up | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             if (this.active === -1) { | ||||
|               this.active = this.getSelectedIndexInFiltered(); | ||||
|             } | ||||
|  | ||||
|             if (this.active <= 0) { | ||||
|               return; | ||||
|             } | ||||
|             this.active--; | ||||
|             this.scrollToActive(); | ||||
|           } else if (event.keyCode === 13) { // enter | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             if (this.active >= this.filteredItems.length) { | ||||
|               this.createNewBranch(); | ||||
|             } else if (this.active >= 0) { | ||||
|               this.selectItem(this.filteredItems[this.active]); | ||||
|             } | ||||
|           } else if (event.keyCode === 27) { // escape | ||||
|             event.preventDefault(); | ||||
|             this.menuVisible = false; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export {initRepoBranchTagDropdown}; | ||||
							
								
								
									
										52
									
								
								web_src/js/components/VueComponentLoader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								web_src/js/components/VueComponentLoader.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| import Vue from 'vue'; | ||||
| import {svgs} from '../svg.js'; | ||||
|  | ||||
| const vueDelimiters = ['${', '}']; | ||||
|  | ||||
| let vueEnvInited = false; | ||||
| function initVueEnv() { | ||||
|   if (vueEnvInited) return; | ||||
|   vueEnvInited = true; | ||||
|  | ||||
|   const isProd = window.config.IsProd; | ||||
|   Vue.config.productionTip = false; | ||||
|   Vue.config.devtools = !isProd; | ||||
| } | ||||
|  | ||||
| let vueSvgInited = false; | ||||
| function initVueSvg() { | ||||
|   if (vueSvgInited) return; | ||||
|   vueSvgInited = true; | ||||
|  | ||||
|   // register svg icon vue components, e.g. <octicon-repo size="16"/> | ||||
|   for (const [name, htmlString] of Object.entries(svgs)) { | ||||
|     const template = htmlString | ||||
|       .replace(/height="[0-9]+"/, 'v-bind:height="size"') | ||||
|       .replace(/width="[0-9]+"/, 'v-bind:width="size"'); | ||||
|  | ||||
|     Vue.component(name, { | ||||
|       props: { | ||||
|         size: { | ||||
|           type: String, | ||||
|           default: '16', | ||||
|         }, | ||||
|       }, | ||||
|       template, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| function initVueApp(el, opts = {}) { | ||||
|   if (typeof el === 'string') { | ||||
|     el = document.querySelector(el); | ||||
|   } | ||||
|   if (!el) return null; | ||||
|  | ||||
|   return new Vue(Object.assign({ | ||||
|     el, | ||||
|     delimiters: vueDelimiters, | ||||
|   }, opts)); | ||||
| } | ||||
|  | ||||
| export {vueDelimiters, initVueEnv, initVueSvg, initVueApp}; | ||||
| @@ -1,5 +1,5 @@ | ||||
| export function initAdminUserListSearchForm() { | ||||
|   const searchForm = window.config.PageData.adminUserListSearchForm; | ||||
|   const searchForm = window.config.pageData.adminUserListSearchForm; | ||||
|   if (!searchForm) return; | ||||
|  | ||||
|   const $form = $('#user-list-search-form'); | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| import './publicpath.js'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import 'jquery.are-you-sure'; | ||||
|  | ||||
| import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | ||||
| import {initVueEnv} from './components/VueComponentLoader.js'; | ||||
| import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue'; | ||||
| import {initDashboardRepoList} from './components/DashboardRepoList.js'; | ||||
| import {initRepoBranchTagDropdown} from './components/RepoBranchTagDropdown.js'; | ||||
|  | ||||
| import attachTribute from './features/tribute.js'; | ||||
| import createColorPicker from './features/colorpicker.js'; | ||||
| import createDropzone from './features/dropzone.js'; | ||||
| @@ -27,20 +30,16 @@ import {initStopwatch} from './features/stopwatch.js'; | ||||
| import {showLineButton} from './code/linebutton.js'; | ||||
| import {initMarkupContent, initCommentContent} from './markup/content.js'; | ||||
| import {stripTags, mqBinarySearch} from './utils.js'; | ||||
| import {svg, svgs} from './svg.js'; | ||||
| import {svg} from './svg.js'; | ||||
|  | ||||
| const {AppSubUrl, AssetUrlPrefix, csrf} = window.config; | ||||
| const {AppSubUrl, csrf} = window.config; | ||||
|  | ||||
| let previewFileModes; | ||||
| const commentMDEditors = {}; | ||||
|  | ||||
| // Silence fomantic's error logging when tabs are used without a target content element | ||||
| $.fn.tab.settings.silent = true; | ||||
|  | ||||
| // Silence Vue's console advertisements in dev mode | ||||
| // To use the Vue browser extension, enable the devtools option temporarily | ||||
| Vue.config.productionTip = false; | ||||
| Vue.config.devtools = false; | ||||
| initVueEnv(); | ||||
|  | ||||
| function initCommentPreviewTab($form) { | ||||
|   const $tabMenu = $form.find('.tabular.menu'); | ||||
| @@ -806,7 +805,7 @@ async function initRepository() { | ||||
|   // File list and commits | ||||
|   if ($('.repository.file.list').length > 0 || | ||||
|     $('.repository.commits').length > 0 || $('.repository.release').length > 0) { | ||||
|     initFilterBranchTagDropdown('.choose.reference .dropdown'); | ||||
|     initRepoBranchTagDropdown('.choose.reference .dropdown'); | ||||
|   } | ||||
|  | ||||
|   // Wiki | ||||
| @@ -2858,7 +2857,8 @@ $(document).ready(async () => { | ||||
|   initWebhook(); | ||||
|   initAdmin(); | ||||
|   initCodeView(); | ||||
|   initVueApp(); | ||||
|   initRepoActivityTopAuthorsChart(); | ||||
|   initDashboardRepoList(); | ||||
|   initTeamSettings(); | ||||
|   initCtrlEnterSubmit(); | ||||
|   initNavbarContentToggle(); | ||||
| @@ -3105,369 +3105,6 @@ function linkEmailAction(e) { | ||||
|   e.preventDefault(); | ||||
| } | ||||
|  | ||||
| function initVueComponents() { | ||||
|   // register svg icon vue components, e.g. <octicon-repo size="16"/> | ||||
|   for (const [name, htmlString] of Object.entries(svgs)) { | ||||
|     const template = htmlString | ||||
|       .replace(/height="[0-9]+"/, 'v-bind:height="size"') | ||||
|       .replace(/width="[0-9]+"/, 'v-bind:width="size"'); | ||||
|  | ||||
|     Vue.component(name, { | ||||
|       props: { | ||||
|         size: { | ||||
|           type: String, | ||||
|           default: '16', | ||||
|         }, | ||||
|       }, | ||||
|       template, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   const vueDelimeters = ['${', '}']; | ||||
|  | ||||
|   Vue.component('repo-search', { | ||||
|     delimiters: vueDelimeters, | ||||
|  | ||||
|     props: { | ||||
|       searchLimit: { | ||||
|         type: Number, | ||||
|         default: 10 | ||||
|       }, | ||||
|       suburl: { | ||||
|         type: String, | ||||
|         required: true | ||||
|       }, | ||||
|       uid: { | ||||
|         type: Number, | ||||
|         required: true | ||||
|       }, | ||||
|       teamId: { | ||||
|         type: Number, | ||||
|         required: false, | ||||
|         default: 0 | ||||
|       }, | ||||
|       organizations: { | ||||
|         type: Array, | ||||
|         default: () => [], | ||||
|       }, | ||||
|       isOrganization: { | ||||
|         type: Boolean, | ||||
|         default: true | ||||
|       }, | ||||
|       canCreateOrganization: { | ||||
|         type: Boolean, | ||||
|         default: false | ||||
|       }, | ||||
|       organizationsTotalCount: { | ||||
|         type: Number, | ||||
|         default: 0 | ||||
|       }, | ||||
|       moreReposLink: { | ||||
|         type: String, | ||||
|         default: '' | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|       const params = new URLSearchParams(window.location.search); | ||||
|  | ||||
|       let tab = params.get('repo-search-tab'); | ||||
|       if (!tab) { | ||||
|         tab = 'repos'; | ||||
|       } | ||||
|  | ||||
|       let reposFilter = params.get('repo-search-filter'); | ||||
|       if (!reposFilter) { | ||||
|         reposFilter = 'all'; | ||||
|       } | ||||
|  | ||||
|       let privateFilter = params.get('repo-search-private'); | ||||
|       if (!privateFilter) { | ||||
|         privateFilter = 'both'; | ||||
|       } | ||||
|  | ||||
|       let archivedFilter = params.get('repo-search-archived'); | ||||
|       if (!archivedFilter) { | ||||
|         archivedFilter = 'unarchived'; | ||||
|       } | ||||
|  | ||||
|       let searchQuery = params.get('repo-search-query'); | ||||
|       if (!searchQuery) { | ||||
|         searchQuery = ''; | ||||
|       } | ||||
|  | ||||
|       let page = 1; | ||||
|       try { | ||||
|         page = parseInt(params.get('repo-search-page')); | ||||
|       } catch { | ||||
|         // noop | ||||
|       } | ||||
|       if (!page) { | ||||
|         page = 1; | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|         tab, | ||||
|         repos: [], | ||||
|         reposTotalCount: 0, | ||||
|         reposFilter, | ||||
|         archivedFilter, | ||||
|         privateFilter, | ||||
|         page, | ||||
|         finalPage: 1, | ||||
|         searchQuery, | ||||
|         isLoading: false, | ||||
|         staticPrefix: AssetUrlPrefix, | ||||
|         counts: {}, | ||||
|         repoTypes: { | ||||
|           all: { | ||||
|             searchMode: '', | ||||
|           }, | ||||
|           forks: { | ||||
|             searchMode: 'fork', | ||||
|           }, | ||||
|           mirrors: { | ||||
|             searchMode: 'mirror', | ||||
|           }, | ||||
|           sources: { | ||||
|             searchMode: 'source', | ||||
|           }, | ||||
|           collaborative: { | ||||
|             searchMode: 'collaborative', | ||||
|           }, | ||||
|         } | ||||
|       }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|       showMoreReposLink() { | ||||
|         return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||
|       }, | ||||
|       searchURL() { | ||||
|         return `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery | ||||
|         }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode | ||||
|         }${this.reposFilter !== 'all' ? '&exclusive=1' : '' | ||||
|         }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' | ||||
|         }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : '' | ||||
|         }`; | ||||
|       }, | ||||
|       repoTypeCount() { | ||||
|         return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     mounted() { | ||||
|       this.changeReposFilter(this.reposFilter); | ||||
|       $(this.$el).find('.poping.up').popup(); | ||||
|       $(this.$el).find('.dropdown').dropdown(); | ||||
|       this.setCheckboxes(); | ||||
|       Vue.nextTick(() => { | ||||
|         this.$refs.search.focus(); | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|       changeTab(t) { | ||||
|         this.tab = t; | ||||
|         this.updateHistory(); | ||||
|       }, | ||||
|  | ||||
|       setCheckboxes() { | ||||
|         switch (this.archivedFilter) { | ||||
|           case 'unarchived': | ||||
|             $('#archivedFilterCheckbox').checkbox('set unchecked'); | ||||
|             break; | ||||
|           case 'archived': | ||||
|             $('#archivedFilterCheckbox').checkbox('set checked'); | ||||
|             break; | ||||
|           case 'both': | ||||
|             $('#archivedFilterCheckbox').checkbox('set indeterminate'); | ||||
|             break; | ||||
|           default: | ||||
|             this.archivedFilter = 'unarchived'; | ||||
|             $('#archivedFilterCheckbox').checkbox('set unchecked'); | ||||
|             break; | ||||
|         } | ||||
|         switch (this.privateFilter) { | ||||
|           case 'public': | ||||
|             $('#privateFilterCheckbox').checkbox('set unchecked'); | ||||
|             break; | ||||
|           case 'private': | ||||
|             $('#privateFilterCheckbox').checkbox('set checked'); | ||||
|             break; | ||||
|           case 'both': | ||||
|             $('#privateFilterCheckbox').checkbox('set indeterminate'); | ||||
|             break; | ||||
|           default: | ||||
|             this.privateFilter = 'both'; | ||||
|             $('#privateFilterCheckbox').checkbox('set indeterminate'); | ||||
|             break; | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       changeReposFilter(filter) { | ||||
|         this.reposFilter = filter; | ||||
|         this.repos = []; | ||||
|         this.page = 1; | ||||
|         Vue.set(this.counts, `${filter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|       updateHistory() { | ||||
|         const params = new URLSearchParams(window.location.search); | ||||
|  | ||||
|         if (this.tab === 'repos') { | ||||
|           params.delete('repo-search-tab'); | ||||
|         } else { | ||||
|           params.set('repo-search-tab', this.tab); | ||||
|         } | ||||
|  | ||||
|         if (this.reposFilter === 'all') { | ||||
|           params.delete('repo-search-filter'); | ||||
|         } else { | ||||
|           params.set('repo-search-filter', this.reposFilter); | ||||
|         } | ||||
|  | ||||
|         if (this.privateFilter === 'both') { | ||||
|           params.delete('repo-search-private'); | ||||
|         } else { | ||||
|           params.set('repo-search-private', this.privateFilter); | ||||
|         } | ||||
|  | ||||
|         if (this.archivedFilter === 'unarchived') { | ||||
|           params.delete('repo-search-archived'); | ||||
|         } else { | ||||
|           params.set('repo-search-archived', this.archivedFilter); | ||||
|         } | ||||
|  | ||||
|         if (this.searchQuery === '') { | ||||
|           params.delete('repo-search-query'); | ||||
|         } else { | ||||
|           params.set('repo-search-query', this.searchQuery); | ||||
|         } | ||||
|  | ||||
|         if (this.page === 1) { | ||||
|           params.delete('repo-search-page'); | ||||
|         } else { | ||||
|           params.set('repo-search-page', `${this.page}`); | ||||
|         } | ||||
|  | ||||
|         const queryString = params.toString(); | ||||
|         if (queryString) { | ||||
|           window.history.replaceState({}, '', `?${queryString}`); | ||||
|         } else { | ||||
|           window.history.replaceState({}, '', window.location.pathname); | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       toggleArchivedFilter() { | ||||
|         switch (this.archivedFilter) { | ||||
|           case 'both': | ||||
|             this.archivedFilter = 'unarchived'; | ||||
|             break; | ||||
|           case 'unarchived': | ||||
|             this.archivedFilter = 'archived'; | ||||
|             break; | ||||
|           case 'archived': | ||||
|             this.archivedFilter = 'both'; | ||||
|             break; | ||||
|           default: | ||||
|             this.archivedFilter = 'unarchived'; | ||||
|             break; | ||||
|         } | ||||
|         this.page = 1; | ||||
|         this.repos = []; | ||||
|         this.setCheckboxes(); | ||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|       togglePrivateFilter() { | ||||
|         switch (this.privateFilter) { | ||||
|           case 'both': | ||||
|             this.privateFilter = 'public'; | ||||
|             break; | ||||
|           case 'public': | ||||
|             this.privateFilter = 'private'; | ||||
|             break; | ||||
|           case 'private': | ||||
|             this.privateFilter = 'both'; | ||||
|             break; | ||||
|           default: | ||||
|             this.privateFilter = 'both'; | ||||
|             break; | ||||
|         } | ||||
|         this.page = 1; | ||||
|         this.repos = []; | ||||
|         this.setCheckboxes(); | ||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|  | ||||
|       changePage(page) { | ||||
|         this.page = page; | ||||
|         if (this.page > this.finalPage) { | ||||
|           this.page = this.finalPage; | ||||
|         } | ||||
|         if (this.page < 1) { | ||||
|           this.page = 1; | ||||
|         } | ||||
|         this.repos = []; | ||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||
|         this.searchRepos(); | ||||
|       }, | ||||
|  | ||||
|       searchRepos() { | ||||
|         this.isLoading = true; | ||||
|  | ||||
|         if (!this.reposTotalCount) { | ||||
|           const totalCountSearchURL = `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; | ||||
|           $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => { | ||||
|             this.reposTotalCount = request.getResponseHeader('X-Total-Count'); | ||||
|           }); | ||||
|         } | ||||
|  | ||||
|         const searchedMode = this.repoTypes[this.reposFilter].searchMode; | ||||
|         const searchedURL = this.searchURL; | ||||
|         const searchedQuery = this.searchQuery; | ||||
|  | ||||
|         $.getJSON(searchedURL, (result, _textStatus, request) => { | ||||
|           if (searchedURL === this.searchURL) { | ||||
|             this.repos = result.data; | ||||
|             const count = request.getResponseHeader('X-Total-Count'); | ||||
|             if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { | ||||
|               this.reposTotalCount = count; | ||||
|             } | ||||
|             Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, count); | ||||
|             this.finalPage = Math.ceil(count / this.searchLimit); | ||||
|             this.updateHistory(); | ||||
|           } | ||||
|         }).always(() => { | ||||
|           if (searchedURL === this.searchURL) { | ||||
|             this.isLoading = false; | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
|  | ||||
|       repoIcon(repo) { | ||||
|         if (repo.fork) { | ||||
|           return 'octicon-repo-forked'; | ||||
|         } else if (repo.mirror) { | ||||
|           return 'octicon-mirror'; | ||||
|         } else if (repo.template) { | ||||
|           return `octicon-repo-template`; | ||||
|         } else if (repo.private) { | ||||
|           return 'octicon-lock'; | ||||
|         } else if (repo.internal) { | ||||
|           return 'octicon-repo'; | ||||
|         } | ||||
|         return 'octicon-repo'; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function initCtrlEnterSubmit() { | ||||
|   $('.js-quick-submit').on('keydown', function (e) { | ||||
|     if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.keyCode === 13 || e.keyCode === 10)) { | ||||
| @@ -3476,31 +3113,6 @@ function initCtrlEnterSubmit() { | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function initVueApp() { | ||||
|   const el = document.getElementById('app'); | ||||
|   if (!el) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   initVueComponents(); | ||||
|  | ||||
|   new Vue({ | ||||
|     el, | ||||
|     delimiters: ['${', '}'], | ||||
|     components: { | ||||
|       ActivityTopAuthors, | ||||
|     }, | ||||
|     data: () => { | ||||
|       return { | ||||
|         searchLimit: Number((document.querySelector('meta[name=_search_limit]') || {}).content), | ||||
|         suburl: AppSubUrl, | ||||
|         uid: Number((document.querySelector('meta[name=_context_uid]') || {}).content), | ||||
|         activityTopAuthors: window.ActivityTopAuthors || [], | ||||
|       }; | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function initIssueTimetracking() { | ||||
|   $(document).on('click', '.issue-add-time', () => { | ||||
|     $('.issue-start-time-modal').modal({ | ||||
| @@ -3543,163 +3155,6 @@ function initBranchOrTagDropdown(selector) { | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function initFilterBranchTagDropdown(selector) { | ||||
|   $(selector).each(function () { | ||||
|     const $dropdown = $(this); | ||||
|     const $data = $dropdown.find('.data'); | ||||
|     const data = { | ||||
|       items: [], | ||||
|       mode: $data.data('mode'), | ||||
|       searchTerm: '', | ||||
|       noResults: '', | ||||
|       canCreateBranch: false, | ||||
|       menuVisible: false, | ||||
|       createTag: false, | ||||
|       active: 0 | ||||
|     }; | ||||
|     $data.find('.item').each(function () { | ||||
|       data.items.push({ | ||||
|         name: $(this).text(), | ||||
|         url: $(this).data('url'), | ||||
|         branch: $(this).hasClass('branch'), | ||||
|         tag: $(this).hasClass('tag'), | ||||
|         selected: $(this).hasClass('selected') | ||||
|       }); | ||||
|     }); | ||||
|     $data.remove(); | ||||
|     new Vue({ | ||||
|       el: this, | ||||
|       delimiters: ['${', '}'], | ||||
|       data, | ||||
|       computed: { | ||||
|         filteredItems() { | ||||
|           const items = this.items.filter((item) => { | ||||
|             return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) && | ||||
|               (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase())); | ||||
|           }); | ||||
|  | ||||
|           // no idea how to fix this so linting rule is disabled instead | ||||
|           this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); // eslint-disable-line vue/no-side-effects-in-computed-properties | ||||
|           return items; | ||||
|         }, | ||||
|         showNoResults() { | ||||
|           return this.filteredItems.length === 0 && !this.showCreateNewBranch; | ||||
|         }, | ||||
|         showCreateNewBranch() { | ||||
|           if (!this.canCreateBranch || !this.searchTerm) { | ||||
|             return false; | ||||
|           } | ||||
|  | ||||
|           return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       watch: { | ||||
|         menuVisible(visible) { | ||||
|           if (visible) { | ||||
|             this.focusSearchField(); | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       beforeMount() { | ||||
|         this.noResults = this.$el.getAttribute('data-no-results'); | ||||
|         this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true'; | ||||
|  | ||||
|         document.body.addEventListener('click', (event) => { | ||||
|           if (this.$el.contains(event.target)) return; | ||||
|           if (this.menuVisible) { | ||||
|             Vue.set(this, 'menuVisible', false); | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
|  | ||||
|       methods: { | ||||
|         selectItem(item) { | ||||
|           const prev = this.getSelected(); | ||||
|           if (prev !== null) { | ||||
|             prev.selected = false; | ||||
|           } | ||||
|           item.selected = true; | ||||
|           window.location.href = item.url; | ||||
|         }, | ||||
|         createNewBranch() { | ||||
|           if (!this.showCreateNewBranch) return; | ||||
|           $(this.$refs.newBranchForm).trigger('submit'); | ||||
|         }, | ||||
|         focusSearchField() { | ||||
|           Vue.nextTick(() => { | ||||
|             this.$refs.searchField.focus(); | ||||
|           }); | ||||
|         }, | ||||
|         getSelected() { | ||||
|           for (let i = 0, j = this.items.length; i < j; ++i) { | ||||
|             if (this.items[i].selected) return this.items[i]; | ||||
|           } | ||||
|           return null; | ||||
|         }, | ||||
|         getSelectedIndexInFiltered() { | ||||
|           for (let i = 0, j = this.filteredItems.length; i < j; ++i) { | ||||
|             if (this.filteredItems[i].selected) return i; | ||||
|           } | ||||
|           return -1; | ||||
|         }, | ||||
|         scrollToActive() { | ||||
|           let el = this.$refs[`listItem${this.active}`]; | ||||
|           if (!el || !el.length) return; | ||||
|           if (Array.isArray(el)) { | ||||
|             el = el[0]; | ||||
|           } | ||||
|  | ||||
|           const cont = this.$refs.scrollContainer; | ||||
|           if (el.offsetTop < cont.scrollTop) { | ||||
|             cont.scrollTop = el.offsetTop; | ||||
|           } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { | ||||
|             cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; | ||||
|           } | ||||
|         }, | ||||
|         keydown(event) { | ||||
|           if (event.keyCode === 40) { // arrow down | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             if (this.active === -1) { | ||||
|               this.active = this.getSelectedIndexInFiltered(); | ||||
|             } | ||||
|  | ||||
|             if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) { | ||||
|               return; | ||||
|             } | ||||
|             this.active++; | ||||
|             this.scrollToActive(); | ||||
|           } else if (event.keyCode === 38) { // arrow up | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             if (this.active === -1) { | ||||
|               this.active = this.getSelectedIndexInFiltered(); | ||||
|             } | ||||
|  | ||||
|             if (this.active <= 0) { | ||||
|               return; | ||||
|             } | ||||
|             this.active--; | ||||
|             this.scrollToActive(); | ||||
|           } else if (event.keyCode === 13) { // enter | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             if (this.active >= this.filteredItems.length) { | ||||
|               this.createNewBranch(); | ||||
|             } else if (this.active >= 0) { | ||||
|               this.selectItem(this.filteredItems[this.active]); | ||||
|             } | ||||
|           } else if (event.keyCode === 27) { // escape | ||||
|             event.preventDefault(); | ||||
|             this.menuVisible = false; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| $('.commit-button').on('click', function (e) { | ||||
|   e.preventDefault(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user