mirror of https://github.com/go-gitea/gitea
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>
pull/16829/head
parent
96ff3e310f
commit
56362043d3
@ -0,0 +1,51 @@ |
|||||||
|
--- |
||||||
|
date: "2021-10-13T16:00:00+02:00" |
||||||
|
title: "Guidelines for Frontend Development" |
||||||
|
slug: "guidelines-frontend" |
||||||
|
weight: 20 |
||||||
|
toc: false |
||||||
|
draft: false |
||||||
|
menu: |
||||||
|
sidebar: |
||||||
|
parent: "developers" |
||||||
|
name: "Guidelines for Frontend" |
||||||
|
weight: 20 |
||||||
|
identifier: "guidelines-frontend" |
||||||
|
--- |
||||||
|
|
||||||
|
# Guidelines for Frontend Development |
||||||
|
|
||||||
|
**Table of Contents** |
||||||
|
|
||||||
|
{{< toc >}} |
||||||
|
|
||||||
|
## Background |
||||||
|
|
||||||
|
Gitea uses [Less CSS](https://lesscss.org), [Fomantic-UI](https://fomantic-ui.com/introduction/getting-started.html) (based on [jQuery](https://api.jquery.com)) and [Vue2](https://vuejs.org/v2/guide/) for its frontend. |
||||||
|
|
||||||
|
The HTML pages are rendered by [Go HTML Template](https://pkg.go.dev/html/template) |
||||||
|
|
||||||
|
## General Guidelines |
||||||
|
|
||||||
|
We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) |
||||||
|
|
||||||
|
### Gitea specific guidelines: |
||||||
|
|
||||||
|
1. Every feature (Fomantic-UI/jQuery module) should be put in separate files/directories. |
||||||
|
2. HTML ids and classes should use kebab-case. |
||||||
|
3. HTML ids and classes used in JavaScript should be unique for the whole project, and should contain 2-3 feature related keywords. We recommend to use the `js-` prefix for classes that are only used in JavaScript. |
||||||
|
4. jQuery events across different features should use their own namespaces. |
||||||
|
5. CSS styling for classes provided by frameworks should not be overwritten. Always use new class-names to overwrite framework styles. We recommend to use the `us-` prefix for user defined styles. |
||||||
|
6. The backend can pass complex data to the frontend by using `ctx.PageData["myModuleData"] = map[]{}` |
||||||
|
7. Simple pages and SEO-related pages use Go HTML Template render to generate static Fomantic-UI HTML output. Complex pages can use Vue2 (or Vue3 in future). |
||||||
|
|
||||||
|
## Legacy Problems and Solutions |
||||||
|
|
||||||
|
### Too much code in `web_src/index.js` |
||||||
|
|
||||||
|
Previously, most JavaScript code was written into `web_src/index.js` directly, making the file unmaintainable. |
||||||
|
Try to keep this file small by creating new modules instead. These modules can be put in the `web_src/js/features` directory for now. |
||||||
|
|
||||||
|
### Vue2/Vue3 and JSX |
||||||
|
|
||||||
|
Gitea is using Vue2 now, we plan to upgrade to Vue3. We decided not to introduce JSX to keep the HTML and the JavaScript code separated. |
@ -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}; |
@ -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}; |
@ -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}; |
Loading…
Reference in new issue