mirror of
https://github.com/go-gitea/gitea
synced 2025-07-21 09:48:37 +00:00
Merge branch 'main' into lunny/automerge_support_delete_branch
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {createApp, nextTick} from 'vue';
|
||||
import $ from 'jquery';
|
||||
import {SvgIcon} from '../svg.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
|
||||
|
||||
@@ -102,7 +102,7 @@ const sfc = {
|
||||
mounted() {
|
||||
const el = document.querySelector('#dashboard-repo-list');
|
||||
this.changeReposFilter(this.reposFilter);
|
||||
$(el).find('.dropdown').dropdown();
|
||||
fomanticQuery(el.querySelector('.ui.dropdown')).dropdown();
|
||||
nextTick(() => {
|
||||
this.$refs.search.focus();
|
||||
});
|
||||
@@ -471,7 +471,7 @@ export default sfc; // activate the IDE's Vue plugin
|
||||
<li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name">
|
||||
<a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
|
||||
<svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/>
|
||||
<div class="text truncate">{{ org.name }}</div>
|
||||
<div class="text truncate">{{ org.full_name ? `${org.full_name} (${org.name})` : org.name }}</div>
|
||||
<div><!-- div to prevent underline of label on hover -->
|
||||
<span class="ui tiny basic label" v-if="org.org_visibility !== 'public'">
|
||||
{{ org.org_visibility === 'limited' ? textOrgVisibilityLimited: textOrgVisibilityPrivate }}
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
import {chartJsColors} from '../utils/color.ts';
|
||||
import {sleep} from '../utils.ts';
|
||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
||||
import $ from 'jquery';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const customEventListener = {
|
||||
id: 'customEventListener',
|
||||
@@ -77,7 +77,7 @@ export default {
|
||||
mounted() {
|
||||
this.fetchGraphData();
|
||||
|
||||
$('#repo-contributors').dropdown({
|
||||
fomanticQuery('#repo-contributors').dropdown({
|
||||
onChange: (val) => {
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
|
@@ -5,15 +5,15 @@ import {POST} from '../../modules/fetch.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
function onSecurityProtocolChange() {
|
||||
if (Number(document.querySelector('#security_protocol')?.value) > 0) {
|
||||
function onSecurityProtocolChange(): void {
|
||||
if (Number(document.querySelector<HTMLInputElement>('#security_protocol')?.value) > 0) {
|
||||
showElem('.has-tls');
|
||||
} else {
|
||||
hideElem('.has-tls');
|
||||
}
|
||||
}
|
||||
|
||||
export function initAdminCommon() {
|
||||
export function initAdminCommon(): void {
|
||||
if (!document.querySelector('.page-content.admin')) return;
|
||||
|
||||
// check whether appUrl(ROOT_URL) is correct, if not, show an error message
|
||||
@@ -21,34 +21,34 @@ export function initAdminCommon() {
|
||||
|
||||
// New user
|
||||
if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) {
|
||||
document.querySelector('#login_type')?.addEventListener('change', function () {
|
||||
if (this.value?.substring(0, 1) === '0') {
|
||||
document.querySelector('#user_name')?.removeAttribute('disabled');
|
||||
document.querySelector('#login_name')?.removeAttribute('required');
|
||||
document.querySelector<HTMLInputElement>('#login_type')?.addEventListener('change', function () {
|
||||
if (this.value?.startsWith('0')) {
|
||||
document.querySelector<HTMLInputElement>('#user_name')?.removeAttribute('disabled');
|
||||
document.querySelector<HTMLInputElement>('#login_name')?.removeAttribute('required');
|
||||
hideElem('.non-local');
|
||||
showElem('.local');
|
||||
document.querySelector('#user_name')?.focus();
|
||||
document.querySelector<HTMLInputElement>('#user_name')?.focus();
|
||||
|
||||
if (this.getAttribute('data-password') === 'required') {
|
||||
document.querySelector('#password')?.setAttribute('required', 'required');
|
||||
}
|
||||
} else {
|
||||
if (document.querySelector('.admin.edit.user')) {
|
||||
document.querySelector('#user_name')?.setAttribute('disabled', 'disabled');
|
||||
if (document.querySelector<HTMLDivElement>('.admin.edit.user')) {
|
||||
document.querySelector<HTMLInputElement>('#user_name')?.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
document.querySelector('#login_name')?.setAttribute('required', 'required');
|
||||
document.querySelector<HTMLInputElement>('#login_name')?.setAttribute('required', 'required');
|
||||
showElem('.non-local');
|
||||
hideElem('.local');
|
||||
document.querySelector('#login_name')?.focus();
|
||||
document.querySelector<HTMLInputElement>('#login_name')?.focus();
|
||||
|
||||
document.querySelector('#password')?.removeAttribute('required');
|
||||
document.querySelector<HTMLInputElement>('#password')?.removeAttribute('required');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onUsePagedSearchChange() {
|
||||
const searchPageSizeElements = document.querySelectorAll('.search-page-size');
|
||||
if (document.querySelector('#use_paged_search').checked) {
|
||||
const searchPageSizeElements = document.querySelectorAll<HTMLDivElement>('.search-page-size');
|
||||
if (document.querySelector<HTMLInputElement>('#use_paged_search').checked) {
|
||||
showElem('.search-page-size');
|
||||
for (const el of searchPageSizeElements) {
|
||||
el.querySelector('input')?.setAttribute('required', 'required');
|
||||
@@ -61,20 +61,20 @@ export function initAdminCommon() {
|
||||
}
|
||||
}
|
||||
|
||||
function onOAuth2Change(applyDefaultValues) {
|
||||
function onOAuth2Change(applyDefaultValues: boolean) {
|
||||
hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url');
|
||||
for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.open_id_connect_auto_discovery_url input[required]')) {
|
||||
input.removeAttribute('required');
|
||||
}
|
||||
|
||||
const provider = document.querySelector('#oauth2_provider').value;
|
||||
const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value;
|
||||
switch (provider) {
|
||||
case 'openidConnect':
|
||||
document.querySelector('.open_id_connect_auto_discovery_url input').setAttribute('required', 'required');
|
||||
document.querySelector<HTMLInputElement>('.open_id_connect_auto_discovery_url input').setAttribute('required', 'required');
|
||||
showElem('.open_id_connect_auto_discovery_url');
|
||||
break;
|
||||
default: {
|
||||
const elProviderCustomUrlSettings = document.querySelector(`#${provider}_customURLSettings`);
|
||||
const elProviderCustomUrlSettings = document.querySelector<HTMLInputElement>(`#${provider}_customURLSettings`);
|
||||
if (!elProviderCustomUrlSettings) break; // some providers do not have custom URL settings
|
||||
const couldChangeCustomURLs = elProviderCustomUrlSettings.getAttribute('data-available') === 'true';
|
||||
const mustProvideCustomURLs = elProviderCustomUrlSettings.getAttribute('data-required') === 'true';
|
||||
@@ -82,7 +82,7 @@ export function initAdminCommon() {
|
||||
showElem('.oauth2_use_custom_url'); // show the checkbox
|
||||
}
|
||||
if (mustProvideCustomURLs) {
|
||||
document.querySelector('#oauth2_use_custom_url').checked = true; // make the checkbox checked
|
||||
document.querySelector<HTMLInputElement>('#oauth2_use_custom_url').checked = true; // make the checkbox checked
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -91,17 +91,17 @@ export function initAdminCommon() {
|
||||
}
|
||||
|
||||
function onOAuth2UseCustomURLChange(applyDefaultValues) {
|
||||
const provider = document.querySelector('#oauth2_provider').value;
|
||||
const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value;
|
||||
hideElem('.oauth2_use_custom_url_field');
|
||||
for (const input of document.querySelectorAll('.oauth2_use_custom_url_field input[required]')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) {
|
||||
input.removeAttribute('required');
|
||||
}
|
||||
|
||||
const elProviderCustomUrlSettings = document.querySelector(`#${provider}_customURLSettings`);
|
||||
if (elProviderCustomUrlSettings && document.querySelector('#oauth2_use_custom_url').checked) {
|
||||
if (elProviderCustomUrlSettings && document.querySelector<HTMLInputElement>('#oauth2_use_custom_url').checked) {
|
||||
for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
|
||||
if (applyDefaultValues) {
|
||||
document.querySelector(`#oauth2_${custom}`).value = document.querySelector(`#${provider}_${custom}`).value;
|
||||
document.querySelector<HTMLInputElement>(`#oauth2_${custom}`).value = document.querySelector<HTMLInputElement>(`#${provider}_${custom}`).value;
|
||||
}
|
||||
const customInput = document.querySelector(`#${provider}_${custom}`);
|
||||
if (customInput && customInput.getAttribute('data-available') === 'true') {
|
||||
@@ -115,25 +115,26 @@ export function initAdminCommon() {
|
||||
}
|
||||
|
||||
function onEnableLdapGroupsChange() {
|
||||
toggleElem(document.querySelector('#ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
|
||||
const checked = document.querySelector<HTMLInputElement>('.js-ldap-group-toggle')?.checked;
|
||||
toggleElem(document.querySelector('#ldap-group-options'), checked);
|
||||
}
|
||||
|
||||
// New authentication
|
||||
if (document.querySelector('.admin.new.authentication')) {
|
||||
document.querySelector('#auth_type')?.addEventListener('change', function () {
|
||||
if (document.querySelector<HTMLDivElement>('.admin.new.authentication')) {
|
||||
document.querySelector<HTMLInputElement>('#auth_type')?.addEventListener('change', function () {
|
||||
hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi');
|
||||
|
||||
for (const input of document.querySelectorAll('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) {
|
||||
input.removeAttribute('required');
|
||||
}
|
||||
|
||||
document.querySelector('.binddnrequired')?.classList.remove('required');
|
||||
document.querySelector<HTMLDivElement>('.binddnrequired')?.classList.remove('required');
|
||||
|
||||
const authType = this.value;
|
||||
switch (authType) {
|
||||
case '2': // LDAP
|
||||
showElem('.ldap');
|
||||
for (const input of document.querySelectorAll('.binddnrequired input, .ldap div.required:not(.dldap) input')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.binddnrequired input, .ldap div.required:not(.dldap) input')) {
|
||||
input.setAttribute('required', 'required');
|
||||
}
|
||||
document.querySelector('.binddnrequired')?.classList.add('required');
|
||||
@@ -141,32 +142,32 @@ export function initAdminCommon() {
|
||||
case '3': // SMTP
|
||||
showElem('.smtp');
|
||||
showElem('.has-tls');
|
||||
for (const input of document.querySelectorAll('.smtp div.required input, .has-tls')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.smtp div.required input, .has-tls')) {
|
||||
input.setAttribute('required', 'required');
|
||||
}
|
||||
break;
|
||||
case '4': // PAM
|
||||
showElem('.pam');
|
||||
for (const input of document.querySelectorAll('.pam input')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.pam input')) {
|
||||
input.setAttribute('required', 'required');
|
||||
}
|
||||
break;
|
||||
case '5': // LDAP
|
||||
showElem('.dldap');
|
||||
for (const input of document.querySelectorAll('.dldap div.required:not(.ldap) input')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.dldap div.required:not(.ldap) input')) {
|
||||
input.setAttribute('required', 'required');
|
||||
}
|
||||
break;
|
||||
case '6': // OAuth2
|
||||
showElem('.oauth2');
|
||||
for (const input of document.querySelectorAll('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input')) {
|
||||
input.setAttribute('required', 'required');
|
||||
}
|
||||
onOAuth2Change(true);
|
||||
break;
|
||||
case '7': // SSPI
|
||||
showElem('.sspi');
|
||||
for (const input of document.querySelectorAll('.sspi div.required input')) {
|
||||
for (const input of document.querySelectorAll<HTMLInputElement>('.sspi div.required input')) {
|
||||
input.setAttribute('required', 'required');
|
||||
}
|
||||
break;
|
||||
@@ -180,39 +181,39 @@ export function initAdminCommon() {
|
||||
}
|
||||
});
|
||||
$('#auth_type').trigger('change');
|
||||
document.querySelector('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
|
||||
document.querySelector('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
|
||||
document.querySelector('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
|
||||
document.querySelector('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true));
|
||||
document.querySelector<HTMLInputElement>('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
|
||||
document.querySelector<HTMLInputElement>('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
|
||||
document.querySelector<HTMLInputElement>('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
|
||||
document.querySelector<HTMLInputElement>('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true));
|
||||
$('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
|
||||
}
|
||||
// Edit authentication
|
||||
if (document.querySelector('.admin.edit.authentication')) {
|
||||
const authType = document.querySelector('#auth_type')?.value;
|
||||
if (document.querySelector<HTMLDivElement>('.admin.edit.authentication')) {
|
||||
const authType = document.querySelector<HTMLInputElement>('#auth_type')?.value;
|
||||
if (authType === '2' || authType === '5') {
|
||||
document.querySelector('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
|
||||
document.querySelector<HTMLInputElement>('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
|
||||
$('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
|
||||
onEnableLdapGroupsChange();
|
||||
if (authType === '2') {
|
||||
document.querySelector('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
|
||||
document.querySelector<HTMLInputElement>('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
|
||||
}
|
||||
} else if (authType === '6') {
|
||||
document.querySelector('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
|
||||
document.querySelector('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
|
||||
document.querySelector<HTMLInputElement>('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
|
||||
document.querySelector<HTMLInputElement>('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
|
||||
onOAuth2Change(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.querySelector('.admin.authentication')) {
|
||||
if (document.querySelector<HTMLDivElement>('.admin.authentication')) {
|
||||
$('#auth_name').on('input', function () {
|
||||
// appSubUrl is either empty or is a path that starts with `/` and doesn't have a trailing slash.
|
||||
document.querySelector('#oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent(this.value)}/callback`;
|
||||
document.querySelector('#oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent((this as HTMLInputElement).value)}/callback`;
|
||||
}).trigger('input');
|
||||
}
|
||||
|
||||
// Notice
|
||||
if (document.querySelector('.admin.notice')) {
|
||||
const detailModal = document.querySelector('#detail-modal');
|
||||
if (document.querySelector<HTMLDivElement>('.admin.notice')) {
|
||||
const detailModal = document.querySelector<HTMLDivElement>('#detail-modal');
|
||||
|
||||
// Attach view detail modals
|
||||
$('.view-detail').on('click', function () {
|
||||
@@ -223,7 +224,7 @@ export function initAdminCommon() {
|
||||
});
|
||||
|
||||
// Select actions
|
||||
const checkboxes = document.querySelectorAll('.select.table .ui.checkbox input');
|
||||
const checkboxes = document.querySelectorAll<HTMLInputElement>('.select.table .ui.checkbox input');
|
||||
|
||||
$('.select.action').on('click', function () {
|
||||
switch ($(this).data('action')) {
|
||||
@@ -244,7 +245,7 @@ export function initAdminCommon() {
|
||||
break;
|
||||
}
|
||||
});
|
||||
document.querySelector('#delete-selection')?.addEventListener('click', async function (e) {
|
||||
document.querySelector<HTMLButtonElement>('#delete-selection')?.addEventListener('click', async function (e) {
|
||||
e.preventDefault();
|
||||
this.classList.add('is-loading', 'disabled');
|
||||
const data = new FormData();
|
||||
|
@@ -3,17 +3,17 @@ import {POST} from '../../modules/fetch.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
export function initAdminConfigs() {
|
||||
const elAdminConfig = document.querySelector('.page-content.admin.config');
|
||||
export function initAdminConfigs(): void {
|
||||
const elAdminConfig = document.querySelector<HTMLDivElement>('.page-content.admin.config');
|
||||
if (!elAdminConfig) return;
|
||||
|
||||
for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) {
|
||||
for (const el of elAdminConfig.querySelectorAll<HTMLInputElement>('input[type="checkbox"][data-config-dyn-key]')) {
|
||||
el.addEventListener('change', async () => {
|
||||
try {
|
||||
const resp = await POST(`${appSubUrl}/-/admin/config`, {
|
||||
data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key'), value: el.checked}),
|
||||
data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key'), value: String(el.checked)}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
const json: Record<string, any> = await resp.json();
|
||||
if (json.errorMessage) throw new Error(json.errorMessage);
|
||||
} catch (ex) {
|
||||
showTemporaryTooltip(el, ex.toString());
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
export function initAdminEmails() {
|
||||
function linkEmailAction(e) {
|
||||
export function initAdminEmails(): void {
|
||||
$('.link-email-action').on('click', (e) => {
|
||||
const $this = $(this);
|
||||
$('#form-uid').val($this.data('uid'));
|
||||
$('#form-email').val($this.data('email'));
|
||||
@@ -9,6 +9,5 @@ export function initAdminEmails() {
|
||||
$('#form-activate').val($this.data('activate'));
|
||||
$('#change-email-modal').modal('show');
|
||||
e.preventDefault();
|
||||
}
|
||||
$('.link-email-action').on('click', linkEmailAction);
|
||||
});
|
||||
}
|
||||
|
@@ -7,16 +7,16 @@ export async function initAdminSelfCheck() {
|
||||
const elCheckByFrontend = document.querySelector('#self-check-by-frontend');
|
||||
if (!elCheckByFrontend) return;
|
||||
|
||||
const elContent = document.querySelector('.page-content.admin .admin-setting-content');
|
||||
const elContent = document.querySelector<HTMLDivElement>('.page-content.admin .admin-setting-content');
|
||||
|
||||
// send frontend self-check request
|
||||
const resp = await POST(`${appSubUrl}/-/admin/self_check`, {
|
||||
data: new URLSearchParams({
|
||||
location_origin: window.location.origin,
|
||||
now: Date.now(), // TODO: check time difference between server and client
|
||||
now: String(Date.now()), // TODO: check time difference between server and client
|
||||
}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
const json: Record<string, any> = await resp.json();
|
||||
toggleElem(elCheckByFrontend, Boolean(json.problems?.length));
|
||||
for (const problem of json.problems ?? []) {
|
||||
const elProblem = document.createElement('div');
|
||||
|
@@ -1,8 +1,8 @@
|
||||
export function initAdminUserListSearchForm() {
|
||||
export function initAdminUserListSearchForm(): void {
|
||||
const searchForm = window.config.pageData.adminUserListSearchForm;
|
||||
if (!searchForm) return;
|
||||
|
||||
const form = document.querySelector('#user-list-search-form');
|
||||
const form = document.querySelector<HTMLFormElement>('#user-list-search-form');
|
||||
if (!form) return;
|
||||
|
||||
for (const button of form.querySelectorAll(`button[name=sort][value="${searchForm.SortType}"]`)) {
|
||||
@@ -12,23 +12,23 @@ export function initAdminUserListSearchForm() {
|
||||
if (searchForm.StatusFilterMap) {
|
||||
for (const [k, v] of Object.entries(searchForm.StatusFilterMap)) {
|
||||
if (!v) continue;
|
||||
for (const input of form.querySelectorAll(`input[name="status_filter[${k}]"][value="${v}"]`)) {
|
||||
for (const input of form.querySelectorAll<HTMLInputElement>(`input[name="status_filter[${k}]"][value="${v}"]`)) {
|
||||
input.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const radio of form.querySelectorAll('input[type=radio]')) {
|
||||
for (const radio of form.querySelectorAll<HTMLInputElement>('input[type=radio]')) {
|
||||
radio.addEventListener('click', () => {
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
const resetButtons = form.querySelectorAll('.j-reset-status-filter');
|
||||
const resetButtons = form.querySelectorAll<HTMLAnchorElement>('.j-reset-status-filter');
|
||||
for (const button of resetButtons) {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
for (const input of form.querySelectorAll('input[type=radio]')) {
|
||||
for (const input of form.querySelectorAll<HTMLInputElement>('input[type=radio]')) {
|
||||
if (input.name.startsWith('status_filter[')) {
|
||||
input.checked = false;
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
export function initAutoFocusEnd() {
|
||||
for (const el of document.querySelectorAll('.js-autofocus-end')) {
|
||||
for (const el of document.querySelectorAll<HTMLInputElement>('.js-autofocus-end')) {
|
||||
el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus.
|
||||
el.setSelectionRange(el.value.length, el.value.length);
|
||||
}
|
||||
|
@@ -35,9 +35,11 @@ export async function initCaptcha() {
|
||||
}
|
||||
case 'm-captcha': {
|
||||
const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
|
||||
// @ts-expect-error
|
||||
mCaptcha.INPUT_NAME = 'm-captcha-response';
|
||||
const instanceURL = captchaEl.getAttribute('data-instance-url');
|
||||
|
||||
// @ts-expect-error
|
||||
mCaptcha.default({
|
||||
siteKey: {
|
||||
instanceUrl: new URL(instanceURL),
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import $ from 'jquery';
|
||||
import {getCurrentLocale} from '../utils.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {pageData} = window.config;
|
||||
|
||||
async function initInputCitationValue(citationCopyApa, citationCopyBibtex) {
|
||||
async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) {
|
||||
const [{Cite, plugins}] = await Promise.all([
|
||||
import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
|
||||
import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
|
||||
@@ -27,9 +27,9 @@ export async function initCitationFileCopyContent() {
|
||||
|
||||
if (!pageData.citationFileContent) return;
|
||||
|
||||
const citationCopyApa = document.querySelector('#citation-copy-apa');
|
||||
const citationCopyBibtex = document.querySelector('#citation-copy-bibtex');
|
||||
const inputContent = document.querySelector('#citation-copy-content');
|
||||
const citationCopyApa = document.querySelector<HTMLButtonElement>('#citation-copy-apa');
|
||||
const citationCopyBibtex = document.querySelector<HTMLButtonElement>('#citation-copy-bibtex');
|
||||
const inputContent = document.querySelector<HTMLInputElement>('#citation-copy-content');
|
||||
|
||||
if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return;
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function initCitationFileCopyContent() {
|
||||
citationCopyApa.classList.toggle('primary', !isBibtex);
|
||||
};
|
||||
|
||||
document.querySelector('#cite-repo-button')?.addEventListener('click', async (e) => {
|
||||
document.querySelector('#cite-repo-button')?.addEventListener('click', async (e: MouseEvent & {target: HTMLAnchorElement}) => {
|
||||
const dropdownBtn = e.target.closest('.ui.dropdown.button');
|
||||
dropdownBtn.classList.add('is-loading');
|
||||
|
||||
@@ -71,6 +71,6 @@ export async function initCitationFileCopyContent() {
|
||||
dropdownBtn.classList.remove('is-loading');
|
||||
}
|
||||
|
||||
$('#cite-repo-modal').modal('show');
|
||||
fomanticQuery('#cite-repo-modal').modal('show');
|
||||
});
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ const {copy_success, copy_error} = window.config.i18n;
|
||||
// - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied
|
||||
// - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
|
||||
export function initGlobalCopyToClipboardListener() {
|
||||
document.addEventListener('click', async (e) => {
|
||||
document.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
|
||||
const target = e.target.closest('[data-clipboard-text], [data-clipboard-target]');
|
||||
if (!target) return;
|
||||
|
||||
@@ -17,7 +17,7 @@ export function initGlobalCopyToClipboardListener() {
|
||||
|
||||
let text = target.getAttribute('data-clipboard-text');
|
||||
if (!text) {
|
||||
text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
|
||||
text = document.querySelector<HTMLInputElement>(target.getAttribute('data-clipboard-target'))?.value;
|
||||
}
|
||||
|
||||
if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
|
||||
|
@@ -21,7 +21,7 @@ const baseOptions = {
|
||||
automaticLayout: true,
|
||||
};
|
||||
|
||||
function getEditorconfig(input) {
|
||||
function getEditorconfig(input: HTMLInputElement) {
|
||||
try {
|
||||
return JSON.parse(input.getAttribute('data-editorconfig'));
|
||||
} catch {
|
||||
@@ -58,7 +58,7 @@ function exportEditor(editor) {
|
||||
if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor);
|
||||
}
|
||||
|
||||
export async function createMonaco(textarea, filename, editorOpts) {
|
||||
export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, editorOpts: Record<string, any>) {
|
||||
const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor');
|
||||
|
||||
initLanguages(monaco);
|
||||
@@ -72,7 +72,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
|
||||
// https://github.com/microsoft/monaco-editor/issues/2427
|
||||
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
|
||||
const styles = window.getComputedStyle(document.documentElement);
|
||||
const getColor = (name) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6');
|
||||
const getColor = (name: string) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6');
|
||||
|
||||
monaco.editor.defineTheme('gitea', {
|
||||
base: isDarkTheme() ? 'vs-dark' : 'vs',
|
||||
@@ -127,13 +127,13 @@ export async function createMonaco(textarea, filename, editorOpts) {
|
||||
return {monaco, editor};
|
||||
}
|
||||
|
||||
function getFileBasedOptions(filename, lineWrapExts) {
|
||||
function getFileBasedOptions(filename: string, lineWrapExts: string[]) {
|
||||
return {
|
||||
wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off',
|
||||
};
|
||||
}
|
||||
|
||||
function togglePreviewDisplay(previewable) {
|
||||
function togglePreviewDisplay(previewable: boolean) {
|
||||
const previewTab = document.querySelector('a[data-tab="preview"]');
|
||||
if (!previewTab) return;
|
||||
|
||||
@@ -152,7 +152,7 @@ function togglePreviewDisplay(previewable) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCodeEditor(textarea, filenameInput) {
|
||||
export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement) {
|
||||
const filename = basename(filenameInput.value);
|
||||
const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(','));
|
||||
const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(',');
|
||||
@@ -177,10 +177,10 @@ export async function createCodeEditor(textarea, filenameInput) {
|
||||
return editor;
|
||||
}
|
||||
|
||||
function getEditorConfigOptions(ec) {
|
||||
function getEditorConfigOptions(ec: Record<string, any>): Record<string, any> {
|
||||
if (!isObject(ec)) return {};
|
||||
|
||||
const opts = {};
|
||||
const opts: Record<string, any> = {};
|
||||
opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec);
|
||||
if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size);
|
||||
if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
|
||||
export async function initColorPickers() {
|
||||
const els = document.querySelectorAll('.js-color-picker-input');
|
||||
const els = document.querySelectorAll<HTMLElement>('.js-color-picker-input');
|
||||
if (!els.length) return;
|
||||
|
||||
await Promise.all([
|
||||
@@ -14,15 +14,15 @@ export async function initColorPickers() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSquare(el, newValue) {
|
||||
function updateSquare(el: HTMLElement, newValue: string): void {
|
||||
el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
|
||||
}
|
||||
|
||||
function updatePicker(el, newValue) {
|
||||
function updatePicker(el: HTMLElement, newValue: string): void {
|
||||
el.setAttribute('color', newValue);
|
||||
}
|
||||
|
||||
function initPicker(el) {
|
||||
function initPicker(el: HTMLElement): void {
|
||||
const input = el.querySelector('input');
|
||||
|
||||
const square = document.createElement('div');
|
||||
@@ -37,7 +37,7 @@ function initPicker(el) {
|
||||
updateSquare(square, e.detail.value);
|
||||
});
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
input.addEventListener('input', (e: Event & {target: HTMLInputElement}) => {
|
||||
updateSquare(square, e.target.value);
|
||||
updatePicker(picker, e.target.value);
|
||||
});
|
||||
@@ -56,7 +56,7 @@ function initPicker(el) {
|
||||
|
||||
// init precolors
|
||||
for (const colorEl of el.querySelectorAll('.precolors .color')) {
|
||||
colorEl.addEventListener('click', (e) => {
|
||||
colorEl.addEventListener('click', (e: MouseEvent & {target: HTMLAnchorElement}) => {
|
||||
const newValue = e.target.getAttribute('data-color-hex');
|
||||
input.value = newValue;
|
||||
input.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
|
@@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
|
||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
|
||||
export function initGlobalButtonClickOnEnter() {
|
||||
export function initGlobalButtonClickOnEnter(): void {
|
||||
$(document).on('keypress', 'div.ui.button,span.ui.button', (e) => {
|
||||
if (e.code === ' ' || e.code === 'Enter') {
|
||||
$(e.target).trigger('click');
|
||||
@@ -12,13 +12,13 @@ export function initGlobalButtonClickOnEnter() {
|
||||
});
|
||||
}
|
||||
|
||||
export function initGlobalDeleteButton() {
|
||||
export function initGlobalDeleteButton(): void {
|
||||
// ".delete-button" shows a confirmation modal defined by `data-modal-id` attribute.
|
||||
// Some model/form elements will be filled by `data-id` / `data-name` / `data-data-xxx` attributes.
|
||||
// If there is a form defined by `data-form`, then the form will be submitted as-is (without any modification).
|
||||
// If there is no form, then the data will be posted to `data-url`.
|
||||
// TODO: it's not encouraged to use this method. `show-modal` does far better than this.
|
||||
for (const btn of document.querySelectorAll('.delete-button')) {
|
||||
for (const btn of document.querySelectorAll<HTMLElement>('.delete-button')) {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -46,7 +46,7 @@ export function initGlobalDeleteButton() {
|
||||
// if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."`
|
||||
if (btn.getAttribute('data-type') === 'form') {
|
||||
const formSelector = btn.getAttribute('data-form');
|
||||
const form = document.querySelector(formSelector);
|
||||
const form = document.querySelector<HTMLFormElement>(formSelector);
|
||||
if (!form) throw new Error(`no form named ${formSelector} found`);
|
||||
form.submit();
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export function initGlobalDeleteButton() {
|
||||
}
|
||||
}
|
||||
|
||||
export function initGlobalButtons() {
|
||||
export function initGlobalButtons(): void {
|
||||
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
|
||||
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
|
||||
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import $ from 'jquery';
|
||||
import {initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts';
|
||||
|
||||
export function initGlobalFormDirtyLeaveConfirm() {
|
||||
initAreYouSure(window.jQuery);
|
||||
// Warn users that try to leave a page after entering data into a form.
|
||||
// Except on sign-in pages, and for forms marked as 'ignore-dirty'.
|
||||
if (!$('.user.signin').length) {
|
||||
$('form:not(.ignore-dirty)').areYouSure();
|
||||
if (!document.querySelector('.page-content.user.signin')) {
|
||||
applyAreYouSure('form:not(.ignore-dirty)');
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -74,7 +74,6 @@ export class ComboMarkdownEditor {
|
||||
previewUrl: string;
|
||||
previewContext: string;
|
||||
previewMode: string;
|
||||
previewWiki: boolean;
|
||||
|
||||
constructor(container, options = {}) {
|
||||
container._giteaComboMarkdownEditor = this;
|
||||
@@ -213,13 +212,11 @@ export class ComboMarkdownEditor {
|
||||
this.previewUrl = this.tabPreviewer.getAttribute('data-preview-url');
|
||||
this.previewContext = this.tabPreviewer.getAttribute('data-preview-context');
|
||||
this.previewMode = this.options.previewMode ?? 'comment';
|
||||
this.previewWiki = this.options.previewWiki ?? false;
|
||||
this.tabPreviewer.addEventListener('click', async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('mode', this.previewMode);
|
||||
formData.append('context', this.previewContext);
|
||||
formData.append('text', this.value());
|
||||
formData.append('wiki', String(this.previewWiki));
|
||||
const response = await POST(this.previewUrl, {data: formData});
|
||||
const data = await response.text();
|
||||
renderPreviewPanelContent($(panelPreviewer), data);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import $ from 'jquery';
|
||||
import {svg} from '../../svg.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {createElementFromHTML} from '../../utils/dom.ts';
|
||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
|
||||
const {i18n} = window.config;
|
||||
|
||||
@@ -17,7 +17,7 @@ export function confirmModal(content, {confirmButtonColor = 'primary'} = {}) {
|
||||
</div>
|
||||
`);
|
||||
document.body.append(modal);
|
||||
const $modal = $(modal);
|
||||
const $modal = fomanticQuery(modal);
|
||||
$modal.modal({
|
||||
onApprove() {
|
||||
resolve(true);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import {POST} from '../../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
|
||||
export function initCompReactionSelector() {
|
||||
for (const container of document.querySelectorAll('.issue-content, .diff-file-body')) {
|
||||
@@ -29,7 +29,7 @@ export function initCompReactionSelector() {
|
||||
if (data.html) {
|
||||
commentContainer.insertAdjacentHTML('beforeend', data.html);
|
||||
const bottomReactionsDropdowns = commentContainer.querySelectorAll('.bottom-reactions .dropdown.select-reaction');
|
||||
$(bottomReactionsDropdowns).dropdown(); // re-init the dropdown
|
||||
fomanticQuery(bottomReactionsDropdowns).dropdown(); // re-init the dropdown
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
const looksLikeEmailAddressCheck = /^\S+@\S+$/;
|
||||
@@ -10,7 +10,7 @@ export function initCompSearchUserBox() {
|
||||
|
||||
const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true';
|
||||
const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined;
|
||||
$(searchUserBox).search({
|
||||
fomanticQuery(searchUserBox).search({
|
||||
minCharacters: 2,
|
||||
apiSettings: {
|
||||
url: `${appSubUrl}/user/search_candidates?q={query}`,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import {toggleElem} from '../utils/dom.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
export function initRepoBranchButton() {
|
||||
initRepoCreateBranchButton();
|
||||
@@ -18,7 +18,7 @@ function initRepoCreateBranchButton() {
|
||||
const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
|
||||
document.querySelector(fromSpanName).textContent = el.getAttribute('data-branch-from');
|
||||
|
||||
$(el.getAttribute('data-modal')).modal('show');
|
||||
fomanticQuery(el.getAttribute('data-modal')).modal('show');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -7,30 +7,20 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.ts';
|
||||
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.ts';
|
||||
import {initImageDiff} from './imagediff.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce} from '../utils/dom.ts';
|
||||
import {
|
||||
submitEventSubmitter,
|
||||
queryElemSiblings,
|
||||
hideElem,
|
||||
showElem,
|
||||
animateOnce,
|
||||
addDelegatedEventListener,
|
||||
createElementFromHTML,
|
||||
} from '../utils/dom.ts';
|
||||
import {POST, GET} from '../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {pageData, i18n} = window.config;
|
||||
|
||||
function initRepoDiffReviewButton() {
|
||||
const reviewBox = document.querySelector('#review-box');
|
||||
if (!reviewBox) return;
|
||||
|
||||
const counter = reviewBox.querySelector('.review-comments-counter');
|
||||
if (!counter) return;
|
||||
|
||||
$(document).on('click', 'button[name="pending_review"]', (e) => {
|
||||
const $form = $(e.target).closest('form');
|
||||
// Watch for the form's submit event.
|
||||
$form.on('submit', () => {
|
||||
const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
|
||||
counter.setAttribute('data-pending-comment-number', num);
|
||||
counter.textContent = num;
|
||||
animateOnce(reviewBox, 'pulse-1p5-200');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initRepoDiffFileViewToggle() {
|
||||
$('.file-view-toggle').on('click', function () {
|
||||
for (const el of queryElemSiblings(this)) {
|
||||
@@ -47,19 +37,15 @@ function initRepoDiffFileViewToggle() {
|
||||
}
|
||||
|
||||
function initRepoDiffConversationForm() {
|
||||
$(document).on('submit', '.conversation-holder form', async (e) => {
|
||||
addDelegatedEventListener<HTMLFormElement>(document, 'submit', '.conversation-holder form', async (form, e) => {
|
||||
e.preventDefault();
|
||||
const textArea = form.querySelector<HTMLTextAreaElement>('textarea');
|
||||
if (!validateTextareaNonEmpty(textArea)) return;
|
||||
if (form.classList.contains('is-loading')) return;
|
||||
|
||||
const $form = $(e.target);
|
||||
const textArea = e.target.querySelector('textarea');
|
||||
if (!validateTextareaNonEmpty(textArea)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('is-loading')) return;
|
||||
try {
|
||||
e.target.classList.add('is-loading');
|
||||
const formData = new FormData($form[0]);
|
||||
form.classList.add('is-loading');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// if the form is submitted by a button, append the button's name and value to the form data
|
||||
const submitter = submitEventSubmitter(e);
|
||||
@@ -68,26 +54,42 @@ function initRepoDiffConversationForm() {
|
||||
formData.append(submitter.name, submitter.value);
|
||||
}
|
||||
|
||||
const response = await POST(e.target.getAttribute('action'), {data: formData});
|
||||
const $newConversationHolder = $(await response.text());
|
||||
const {path, side, idx} = $newConversationHolder.data();
|
||||
const trLineType = form.closest('tr').getAttribute('data-line-type');
|
||||
const response = await POST(form.getAttribute('action'), {data: formData});
|
||||
const newConversationHolder = createElementFromHTML(await response.text());
|
||||
const path = newConversationHolder.getAttribute('data-path');
|
||||
const side = newConversationHolder.getAttribute('data-side');
|
||||
const idx = newConversationHolder.getAttribute('data-idx');
|
||||
|
||||
form.closest('.conversation-holder').replaceWith(newConversationHolder);
|
||||
form = null; // prevent further usage of the form because it should have been replaced
|
||||
|
||||
$form.closest('.conversation-holder').replaceWith($newConversationHolder);
|
||||
let selector;
|
||||
if ($form.closest('tr').data('line-type') === 'same') {
|
||||
if (trLineType === 'same') {
|
||||
selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`;
|
||||
} else {
|
||||
selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`;
|
||||
}
|
||||
for (const el of document.querySelectorAll(selector)) {
|
||||
el.classList.add('tw-invisible');
|
||||
el.classList.add('tw-invisible'); // TODO need to figure out why
|
||||
}
|
||||
fomanticQuery(newConversationHolder.querySelectorAll('.ui.dropdown')).dropdown();
|
||||
|
||||
// the default behavior is to add a pending review, so if no submitter, it also means "pending_review"
|
||||
if (!submitter || submitter?.matches('button[name="pending_review"]')) {
|
||||
const reviewBox = document.querySelector('#review-box');
|
||||
const counter = reviewBox?.querySelector('.review-comments-counter');
|
||||
if (!counter) return;
|
||||
const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
|
||||
counter.setAttribute('data-pending-comment-number', String(num));
|
||||
counter.textContent = String(num);
|
||||
animateOnce(reviewBox, 'pulse-1p5-200');
|
||||
}
|
||||
$newConversationHolder.find('.dropdown').dropdown();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showErrorToast(i18n.network_error);
|
||||
} finally {
|
||||
e.target.classList.remove('is-loading');
|
||||
form?.classList.remove('is-loading');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -219,7 +221,6 @@ export function initRepoDiffView() {
|
||||
initDiffFileList();
|
||||
initDiffCommitSelect();
|
||||
initRepoDiffShowMore();
|
||||
initRepoDiffReviewButton();
|
||||
initRepoDiffFileViewToggle();
|
||||
initViewedCheckboxListenerFor();
|
||||
initExpandAndCollapseFilesButton();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import $ from 'jquery';
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
export function initRepoGraphGit() {
|
||||
const graphContainer = document.querySelector('#git-graph-container');
|
||||
@@ -83,8 +83,8 @@ export function initRepoGraphGit() {
|
||||
}
|
||||
|
||||
const flowSelectRefsDropdown = document.querySelector('#flow-select-refs-dropdown');
|
||||
$(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
|
||||
$(flowSelectRefsDropdown).dropdown({
|
||||
fomanticQuery(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
|
||||
fomanticQuery(flowSelectRefsDropdown).dropdown({
|
||||
clearable: true,
|
||||
fullTextSeach: 'exact',
|
||||
onRemove(toRemove) {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import $ from 'jquery';
|
||||
import {stripTags} from '../utils.ts';
|
||||
import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
@@ -73,7 +73,7 @@ export function initRepoTopicBar() {
|
||||
}
|
||||
});
|
||||
|
||||
$(topicDropdown).dropdown({
|
||||
fomanticQuery(topicDropdown).dropdown({
|
||||
allowAdditions: true,
|
||||
forceSelection: false,
|
||||
fullTextSearch: 'exact',
|
||||
@@ -136,7 +136,7 @@ export function initRepoTopicBar() {
|
||||
onLabelCreate(value) {
|
||||
value = value.toLowerCase().trim();
|
||||
this.attr('data-value', value).contents().first().replaceWith(value);
|
||||
return $(this);
|
||||
return fomanticQuery(this);
|
||||
},
|
||||
onAdd(addedValue, _addedText, $addedChoice) {
|
||||
addedValue = addedValue.toLowerCase().trim();
|
||||
|
@@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
|
||||
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
||||
|
||||
// if there are draft comments, confirm before reloading, to avoid losing comments
|
||||
export function issueSidebarReloadConfirmDraftComment() {
|
||||
function issueSidebarReloadConfirmDraftComment() {
|
||||
const commentTextareas = [
|
||||
document.querySelector<HTMLTextAreaElement>('.edit-content-zone:not(.tw-hidden) textarea'),
|
||||
document.querySelector<HTMLTextAreaElement>('#comment-form textarea'),
|
||||
@@ -22,84 +22,138 @@ export function issueSidebarReloadConfirmDraftComment() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function collectCheckedValues(elDropdown: HTMLElement) {
|
||||
return Array.from(elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
|
||||
}
|
||||
class IssueSidebarComboList {
|
||||
updateUrl: string;
|
||||
updateAlgo: string;
|
||||
selectionMode: string;
|
||||
elDropdown: HTMLElement;
|
||||
elList: HTMLElement;
|
||||
elComboValue: HTMLInputElement;
|
||||
initialValues: string[];
|
||||
|
||||
export function initIssueSidebarComboList(container: HTMLElement) {
|
||||
const updateUrl = container.getAttribute('data-update-url');
|
||||
const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
|
||||
const elList = container.querySelector<HTMLElement>(':scope > .ui.list');
|
||||
const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
|
||||
let initialValues = collectCheckedValues(elDropdown);
|
||||
constructor(private container: HTMLElement) {
|
||||
this.updateUrl = this.container.getAttribute('data-update-url');
|
||||
this.updateAlgo = container.getAttribute('data-update-algo');
|
||||
this.selectionMode = container.getAttribute('data-selection-mode');
|
||||
if (!['single', 'multiple'].includes(this.selectionMode)) throw new Error(`Invalid data-update-on: ${this.selectionMode}`);
|
||||
if (!['diff', 'all'].includes(this.updateAlgo)) throw new Error(`Invalid data-update-algo: ${this.updateAlgo}`);
|
||||
this.elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
|
||||
this.elList = container.querySelector<HTMLElement>(':scope > .ui.list');
|
||||
this.elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
|
||||
}
|
||||
|
||||
elDropdown.addEventListener('click', (e) => {
|
||||
collectCheckedValues() {
|
||||
return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value'));
|
||||
}
|
||||
|
||||
updateUiList(changedValues) {
|
||||
const elEmptyTip = this.elList.querySelector('.item.empty-list');
|
||||
queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove());
|
||||
for (const value of changedValues) {
|
||||
const el = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||
if (!el) continue;
|
||||
const listItem = el.cloneNode(true) as HTMLElement;
|
||||
queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
|
||||
this.elList.append(listItem);
|
||||
}
|
||||
const hasItems = Boolean(this.elList.querySelector('.item:not(.empty-list)'));
|
||||
toggleElem(elEmptyTip, !hasItems);
|
||||
}
|
||||
|
||||
async updateToBackend(changedValues) {
|
||||
if (this.updateAlgo === 'diff') {
|
||||
for (const value of this.initialValues) {
|
||||
if (!changedValues.includes(value)) {
|
||||
await POST(this.updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
|
||||
}
|
||||
}
|
||||
for (const value of changedValues) {
|
||||
if (!this.initialValues.includes(value)) {
|
||||
await POST(this.updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await POST(this.updateUrl, {data: new URLSearchParams({id: changedValues.join(',')})});
|
||||
}
|
||||
issueSidebarReloadConfirmDraftComment();
|
||||
}
|
||||
|
||||
async doUpdate() {
|
||||
const changedValues = this.collectCheckedValues();
|
||||
if (this.initialValues.join(',') === changedValues.join(',')) return;
|
||||
this.updateUiList(changedValues);
|
||||
if (this.updateUrl) await this.updateToBackend(changedValues);
|
||||
this.initialValues = changedValues;
|
||||
}
|
||||
|
||||
async onChange() {
|
||||
if (this.selectionMode === 'single') {
|
||||
await this.doUpdate();
|
||||
fomanticQuery(this.elDropdown).dropdown('hide');
|
||||
}
|
||||
}
|
||||
|
||||
async onItemClick(e) {
|
||||
const elItem = (e.target as HTMLElement).closest('.item');
|
||||
if (!elItem) return;
|
||||
e.preventDefault();
|
||||
if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
|
||||
|
||||
if (elItem.matches('.clear-selection')) {
|
||||
queryElems(elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
|
||||
elComboValue.value = '';
|
||||
queryElems(this.elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
|
||||
this.elComboValue.value = '';
|
||||
this.onChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = elItem.getAttribute('data-scope');
|
||||
if (scope) {
|
||||
// scoped items could only be checked one at a time
|
||||
const elSelected = elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
|
||||
const elSelected = this.elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
|
||||
if (elSelected === elItem) {
|
||||
elItem.classList.toggle('checked');
|
||||
} else {
|
||||
queryElems(elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
|
||||
queryElems(this.elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
|
||||
elItem.classList.toggle('checked', true);
|
||||
}
|
||||
} else {
|
||||
elItem.classList.toggle('checked');
|
||||
}
|
||||
elComboValue.value = collectCheckedValues(elDropdown).join(',');
|
||||
});
|
||||
|
||||
const updateToBackend = async (changedValues) => {
|
||||
let changed = false;
|
||||
for (const value of initialValues) {
|
||||
if (!changedValues.includes(value)) {
|
||||
await POST(updateUrl, {data: new URLSearchParams({action: 'detach', id: value})});
|
||||
changed = true;
|
||||
if (this.selectionMode === 'multiple') {
|
||||
elItem.classList.toggle('checked');
|
||||
} else {
|
||||
queryElems(this.elDropdown, `.menu > .item.checked`, (el) => el.classList.remove('checked'));
|
||||
elItem.classList.toggle('checked', true);
|
||||
}
|
||||
}
|
||||
for (const value of changedValues) {
|
||||
if (!initialValues.includes(value)) {
|
||||
await POST(updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
|
||||
changed = true;
|
||||
this.elComboValue.value = this.collectCheckedValues().join(',');
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
async onHide() {
|
||||
if (this.selectionMode === 'multiple') this.doUpdate();
|
||||
}
|
||||
|
||||
init() {
|
||||
// init the checked items from initial value
|
||||
if (this.elComboValue.value && this.elComboValue.value !== '0' && !queryElems(this.elDropdown, `.menu > .item.checked`).length) {
|
||||
const values = this.elComboValue.value.split(',');
|
||||
for (const value of values) {
|
||||
const elItem = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||
elItem?.classList.add('checked');
|
||||
}
|
||||
this.updateUiList(values);
|
||||
}
|
||||
if (changed) issueSidebarReloadConfirmDraftComment();
|
||||
};
|
||||
this.initialValues = this.collectCheckedValues();
|
||||
|
||||
const syncUiList = (changedValues) => {
|
||||
const elEmptyTip = elList.querySelector('.item.empty-list');
|
||||
queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove());
|
||||
for (const value of changedValues) {
|
||||
const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||
const listItem = el.cloneNode(true) as HTMLElement;
|
||||
queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
|
||||
elList.append(listItem);
|
||||
}
|
||||
const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
|
||||
toggleElem(elEmptyTip, !hasItems);
|
||||
};
|
||||
this.elDropdown.addEventListener('click', (e) => this.onItemClick(e));
|
||||
|
||||
fomanticQuery(elDropdown).dropdown('setting', {
|
||||
action: 'nothing', // do not hide the menu if user presses Enter
|
||||
fullTextSearch: 'exact',
|
||||
async onHide() {
|
||||
// TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs.
|
||||
const changedValues = collectCheckedValues(elDropdown);
|
||||
syncUiList(changedValues);
|
||||
if (updateUrl) await updateToBackend(changedValues);
|
||||
initialValues = changedValues;
|
||||
},
|
||||
});
|
||||
fomanticQuery(this.elDropdown).dropdown('setting', {
|
||||
action: 'nothing', // do not hide the menu if user presses Enter
|
||||
fullTextSearch: 'exact',
|
||||
onHide: () => this.onHide(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initIssueSidebarComboList(container: HTMLElement) {
|
||||
new IssueSidebarComboList(container).init();
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
A sidebar combo (dropdown+list) is like this:
|
||||
|
||||
```html
|
||||
<div class="issue-sidebar-combo" data-update-url="...">
|
||||
<div class="issue-sidebar-combo" data-selection-mode="..." data-update-url="...">
|
||||
<input class="combo-value" name="..." type="hidden" value="...">
|
||||
<div class="ui dropdown">
|
||||
<div class="menu">
|
||||
@@ -25,3 +25,7 @@ If there is `data-update-url`, it also calls backend to attach/detach the change
|
||||
Also, the changed items will be syncronized to the `ui list` items.
|
||||
|
||||
The items with the same data-scope only allow one selected at a time.
|
||||
|
||||
The dropdown selection could work in 2 modes:
|
||||
* single: only one item could be selected, it updates immediately when the item is selected.
|
||||
* multiple: multiple items could be selected, it defers the update until the dropdown is hidden.
|
||||
|
@@ -1,10 +1,7 @@
|
||||
import $ from 'jquery';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {updateIssuesMeta} from './repo-common.ts';
|
||||
import {svg} from '../svg.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {queryElems, toggleElem} from '../utils/dom.ts';
|
||||
import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts';
|
||||
import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
|
||||
|
||||
function initBranchSelector() {
|
||||
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
|
||||
@@ -34,212 +31,6 @@ function initBranchSelector() {
|
||||
});
|
||||
}
|
||||
|
||||
// List submits
|
||||
function initListSubmits(selector, outerSelector) {
|
||||
const $list = $(`.ui.${outerSelector}.list`);
|
||||
const $noSelect = $list.find('.no-select');
|
||||
const $listMenu = $(`.${selector} .menu`);
|
||||
let hasUpdateAction = $listMenu.data('action') === 'update';
|
||||
const items = {};
|
||||
|
||||
$(`.${selector}`).dropdown({
|
||||
'action': 'nothing', // do not hide the menu if user presses Enter
|
||||
fullTextSearch: 'exact',
|
||||
async onHide() {
|
||||
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
|
||||
if (hasUpdateAction) {
|
||||
// TODO: Add batch functionality and make this 1 network request.
|
||||
const itemEntries = Object.entries(items);
|
||||
for (const [elementId, item] of itemEntries) {
|
||||
await updateIssuesMeta(
|
||||
item['update-url'],
|
||||
item['action'],
|
||||
item['issue-id'],
|
||||
elementId,
|
||||
);
|
||||
}
|
||||
if (itemEntries.length) {
|
||||
issueSidebarReloadConfirmDraftComment();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
$listMenu.find('.item:not(.no-select)').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (this.classList.contains('ban-change')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
|
||||
|
||||
const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment
|
||||
const scope = this.getAttribute('data-scope');
|
||||
|
||||
$(this).parent().find('.item').each(function () {
|
||||
if (scope) {
|
||||
// Enable only clicked item for scoped labels
|
||||
if (this.getAttribute('data-scope') !== scope) {
|
||||
return;
|
||||
}
|
||||
if (this !== clickedItem && !this.classList.contains('checked')) {
|
||||
return;
|
||||
}
|
||||
} else if (this !== clickedItem) {
|
||||
// Toggle for other labels
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.classList.contains('checked')) {
|
||||
$(this).removeClass('checked');
|
||||
$(this).find('.octicon-check').addClass('tw-invisible');
|
||||
if (hasUpdateAction) {
|
||||
if (!($(this).data('id') in items)) {
|
||||
items[$(this).data('id')] = {
|
||||
'update-url': $listMenu.data('update-url'),
|
||||
action: 'detach',
|
||||
'issue-id': $listMenu.data('issue-id'),
|
||||
};
|
||||
} else {
|
||||
delete items[$(this).data('id')];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$(this).addClass('checked');
|
||||
$(this).find('.octicon-check').removeClass('tw-invisible');
|
||||
if (hasUpdateAction) {
|
||||
if (!($(this).data('id') in items)) {
|
||||
items[$(this).data('id')] = {
|
||||
'update-url': $listMenu.data('update-url'),
|
||||
action: 'attach',
|
||||
'issue-id': $listMenu.data('issue-id'),
|
||||
};
|
||||
} else {
|
||||
delete items[$(this).data('id')];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Which thing should be done for choosing review requests
|
||||
// to make chosen items be shown on time here?
|
||||
if (selector === 'select-assignees-modify') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const listIds = [];
|
||||
$(this).parent().find('.item').each(function () {
|
||||
if (this.classList.contains('checked')) {
|
||||
listIds.push($(this).data('id'));
|
||||
$($(this).data('id-selector')).removeClass('tw-hidden');
|
||||
} else {
|
||||
$($(this).data('id-selector')).addClass('tw-hidden');
|
||||
}
|
||||
});
|
||||
if (!listIds.length) {
|
||||
$noSelect.removeClass('tw-hidden');
|
||||
} else {
|
||||
$noSelect.addClass('tw-hidden');
|
||||
}
|
||||
$($(this).parent().data('id')).val(listIds.join(','));
|
||||
return false;
|
||||
});
|
||||
$listMenu.find('.no-select.item').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (hasUpdateAction) {
|
||||
(async () => {
|
||||
await updateIssuesMeta(
|
||||
$listMenu.data('update-url'),
|
||||
'clear',
|
||||
$listMenu.data('issue-id'),
|
||||
'',
|
||||
);
|
||||
issueSidebarReloadConfirmDraftComment();
|
||||
})();
|
||||
}
|
||||
|
||||
$(this).parent().find('.item').each(function () {
|
||||
$(this).removeClass('checked');
|
||||
$(this).find('.octicon-check').addClass('tw-invisible');
|
||||
});
|
||||
|
||||
if (selector === 'select-assignees-modify') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$list.find('.item').each(function () {
|
||||
$(this).addClass('tw-hidden');
|
||||
});
|
||||
$noSelect.removeClass('tw-hidden');
|
||||
$($(this).parent().data('id')).val('');
|
||||
});
|
||||
}
|
||||
|
||||
function selectItem(select_id, input_id) {
|
||||
const $menu = $(`${select_id} .menu`);
|
||||
const $list = $(`.ui${select_id}.list`);
|
||||
const hasUpdateAction = $menu.data('action') === 'update';
|
||||
|
||||
$menu.find('.item:not(.no-select)').on('click', function () {
|
||||
$(this).parent().find('.item').each(function () {
|
||||
$(this).removeClass('selected active');
|
||||
});
|
||||
|
||||
$(this).addClass('selected active');
|
||||
if (hasUpdateAction) {
|
||||
(async () => {
|
||||
await updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
);
|
||||
issueSidebarReloadConfirmDraftComment();
|
||||
})();
|
||||
}
|
||||
|
||||
let icon = '';
|
||||
if (input_id === '#milestone_id') {
|
||||
icon = svg('octicon-milestone', 18, 'tw-mr-2');
|
||||
} else if (input_id === '#project_id') {
|
||||
icon = svg('octicon-project', 18, 'tw-mr-2');
|
||||
} else if (input_id === '#assignee_id') {
|
||||
icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
|
||||
}
|
||||
|
||||
$list.find('.selected').html(`
|
||||
<a class="item muted sidebar-item-link" href="${htmlEscape(this.getAttribute('data-href'))}">
|
||||
${icon}
|
||||
${htmlEscape(this.textContent)}
|
||||
</a>
|
||||
`);
|
||||
|
||||
$(`.ui${select_id}.list .no-select`).addClass('tw-hidden');
|
||||
$(input_id).val($(this).data('id'));
|
||||
});
|
||||
$menu.find('.no-select.item').on('click', function () {
|
||||
$(this).parent().find('.item:not(.no-select)').each(function () {
|
||||
$(this).removeClass('selected active');
|
||||
});
|
||||
|
||||
if (hasUpdateAction) {
|
||||
(async () => {
|
||||
await updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
);
|
||||
issueSidebarReloadConfirmDraftComment();
|
||||
})();
|
||||
}
|
||||
|
||||
$list.find('.selected').html('');
|
||||
$list.find('.no-select').removeClass('tw-hidden');
|
||||
$(input_id).val('');
|
||||
});
|
||||
}
|
||||
|
||||
function initRepoIssueDue() {
|
||||
const form = document.querySelector<HTMLFormElement>('.issue-due-form');
|
||||
if (!form) return;
|
||||
@@ -257,14 +48,6 @@ export function initRepoIssueSidebar() {
|
||||
initBranchSelector();
|
||||
initRepoIssueDue();
|
||||
|
||||
// TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
|
||||
initListSubmits('select-assignees', 'assignees');
|
||||
initListSubmits('select-assignees-modify', 'assignees');
|
||||
selectItem('.select-assignee', '#assignee_id');
|
||||
|
||||
selectItem('.select-project', '#project_id');
|
||||
selectItem('.select-milestone', '#milestone_id');
|
||||
|
||||
// init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
|
||||
queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import $ from 'jquery';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
|
||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {addDelegatedEventListener, createElementFromHTML, hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {setFileFolding} from './file-fold.ts';
|
||||
import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||
import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts';
|
||||
@@ -443,21 +443,19 @@ export function initRepoPullRequestReview() {
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '.add-code-comment', async function (e) {
|
||||
if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
|
||||
addDelegatedEventListener(document, 'click', '.add-code-comment', async (el, e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
|
||||
const side = this.getAttribute('data-side');
|
||||
const idx = this.getAttribute('data-idx');
|
||||
const path = this.closest('[data-path]')?.getAttribute('data-path');
|
||||
const tr = this.closest('tr');
|
||||
const isSplit = el.closest('.code-diff')?.classList.contains('code-diff-split');
|
||||
const side = el.getAttribute('data-side');
|
||||
const idx = el.getAttribute('data-idx');
|
||||
const path = el.closest('[data-path]')?.getAttribute('data-path');
|
||||
const tr = el.closest('tr');
|
||||
const lineType = tr.getAttribute('data-line-type');
|
||||
|
||||
const ntr = tr.nextElementSibling;
|
||||
let $ntr = $(ntr);
|
||||
let ntr = tr.nextElementSibling;
|
||||
if (!ntr?.classList.contains('add-comment')) {
|
||||
$ntr = $(`
|
||||
ntr = createElementFromHTML(`
|
||||
<tr class="add-comment" data-line-type="${lineType}">
|
||||
${isSplit ? `
|
||||
<td class="add-comment-left" colspan="4"></td>
|
||||
@@ -466,24 +464,18 @@ export function initRepoPullRequestReview() {
|
||||
<td class="add-comment-left add-comment-right" colspan="5"></td>
|
||||
`}
|
||||
</tr>`);
|
||||
$(tr).after($ntr);
|
||||
tr.after(ntr);
|
||||
}
|
||||
|
||||
const $td = $ntr.find(`.add-comment-${side}`);
|
||||
const $commentCloud = $td.find('.comment-code-cloud');
|
||||
if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
|
||||
try {
|
||||
const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
|
||||
const html = await response.text();
|
||||
$td.html(html);
|
||||
$td.find("input[name='line']").val(idx);
|
||||
$td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
|
||||
$td.find("input[name='path']").val(path);
|
||||
const editor = await initComboMarkdownEditor($td[0].querySelector('.combo-markdown-editor'));
|
||||
editor.focus();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
const td = ntr.querySelector(`.add-comment-${side}`);
|
||||
const commentCloud = td.querySelector('.comment-code-cloud');
|
||||
if (!commentCloud && !ntr.querySelector('button[name="pending_review"]')) {
|
||||
const response = await GET(el.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
|
||||
td.innerHTML = await response.text();
|
||||
td.querySelector<HTMLInputElement>("input[name='line']").value = idx;
|
||||
td.querySelector<HTMLInputElement>("input[name='side']").value = (side === 'left' ? 'previous' : 'proposed');
|
||||
td.querySelector<HTMLInputElement>("input[name='path']").value = path;
|
||||
const editor = await initComboMarkdownEditor(td.querySelector<HTMLElement>('.combo-markdown-editor'));
|
||||
editor.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -26,7 +26,6 @@ async function initRepoWikiFormEditor() {
|
||||
formData.append('mode', editor.previewMode);
|
||||
formData.append('context', editor.previewContext);
|
||||
formData.append('text', newContent);
|
||||
formData.append('wiki', editor.previewWiki);
|
||||
try {
|
||||
const response = await POST(editor.previewUrl, {data: formData});
|
||||
const data = await response.text();
|
||||
@@ -51,8 +50,7 @@ async function initRepoWikiFormEditor() {
|
||||
// And another benefit is that we only need to write the style once for both editors.
|
||||
// TODO: Move height style to CSS after EasyMDE removal.
|
||||
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
|
||||
previewMode: 'gfm',
|
||||
previewWiki: true,
|
||||
previewMode: 'wiki',
|
||||
easyMDEOptions: {
|
||||
previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
|
||||
toolbar: ['bold', 'italic', 'strikethrough', '|',
|
||||
|
4
web_src/js/globals.d.ts
vendored
4
web_src/js/globals.d.ts
vendored
@@ -58,4 +58,8 @@ interface Window {
|
||||
push: (e: ErrorEvent & PromiseRejectionEvent) => void | number,
|
||||
},
|
||||
__webpack_public_path__: string;
|
||||
grecaptcha: any,
|
||||
turnstile: any,
|
||||
hcaptcha: any,
|
||||
codeEditors: any[],
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import {svg} from '../svg.ts';
|
||||
|
||||
const addPrefix = (str) => `user-content-${str}`;
|
||||
const removePrefix = (str) => str.replace(/^user-content-/, '');
|
||||
const hasPrefix = (str) => str.startsWith('user-content-');
|
||||
const addPrefix = (str: string): string => `user-content-${str}`;
|
||||
const removePrefix = (str: string): string => str.replace(/^user-content-/, '');
|
||||
const hasPrefix = (str: string): boolean => str.startsWith('user-content-');
|
||||
|
||||
// scroll to anchor while respecting the `user-content` prefix that exists on the target
|
||||
function scrollToAnchor(encodedId) {
|
||||
function scrollToAnchor(encodedId: string): void {
|
||||
if (!encodedId) return;
|
||||
const id = decodeURIComponent(encodedId);
|
||||
const prefixedId = addPrefix(id);
|
||||
@@ -24,7 +24,7 @@ function scrollToAnchor(encodedId) {
|
||||
el?.scrollIntoView();
|
||||
}
|
||||
|
||||
export function initMarkupAnchors() {
|
||||
export function initMarkupAnchors(): void {
|
||||
const markupEls = document.querySelectorAll('.markup');
|
||||
if (!markupEls.length) return;
|
||||
|
||||
@@ -39,7 +39,7 @@ export function initMarkupAnchors() {
|
||||
}
|
||||
|
||||
// remove `user-content-` prefix from links so they don't show in url bar when clicked
|
||||
for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
|
||||
for (const a of markupEl.querySelectorAll<HTMLAnchorElement>('a[href^="#"]')) {
|
||||
const href = a.getAttribute('href');
|
||||
if (!href.startsWith('#user-content-')) continue;
|
||||
a.setAttribute('href', `#${removePrefix(href.substring(1))}`);
|
||||
@@ -47,15 +47,15 @@ export function initMarkupAnchors() {
|
||||
|
||||
// add `user-content-` prefix to user-generated `a[name]` link targets
|
||||
// TODO: this prefix should be added in backend instead
|
||||
for (const a of markupEl.querySelectorAll('a[name]')) {
|
||||
for (const a of markupEl.querySelectorAll<HTMLAnchorElement>('a[name]')) {
|
||||
const name = a.getAttribute('name');
|
||||
if (!name) continue;
|
||||
a.setAttribute('name', addPrefix(a.name));
|
||||
a.setAttribute('name', addPrefix(name));
|
||||
}
|
||||
|
||||
for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
|
||||
for (const a of markupEl.querySelectorAll<HTMLAnchorElement>('a[href^="#"]')) {
|
||||
a.addEventListener('click', (e) => {
|
||||
scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1));
|
||||
scrollToAnchor((e.currentTarget as HTMLAnchorElement).getAttribute('href')?.substring(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import {svg} from '../svg.ts';
|
||||
|
||||
export function makeCodeCopyButton() {
|
||||
export function makeCodeCopyButton(): HTMLButtonElement {
|
||||
const button = document.createElement('button');
|
||||
button.classList.add('code-copy', 'ui', 'button');
|
||||
button.innerHTML = svg('octicon-copy');
|
||||
return button;
|
||||
}
|
||||
|
||||
export function renderCodeCopy() {
|
||||
export function renderCodeCopy(): void {
|
||||
const els = document.querySelectorAll('.markup .code-block code');
|
||||
if (!els.length) return;
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
export function displayError(el, err) {
|
||||
export function displayError(el: Element, err: Error): void {
|
||||
el.classList.remove('is-loading');
|
||||
const errorNode = document.createElement('pre');
|
||||
errorNode.setAttribute('class', 'ui message error markup-block-error');
|
||||
errorNode.textContent = err.str || err.message || String(err);
|
||||
errorNode.textContent = err.message || String(err);
|
||||
el.before(errorNode);
|
||||
el.setAttribute('data-render-done', 'true');
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import {renderAsciicast} from './asciicast.ts';
|
||||
import {initMarkupTasklist} from './tasklist.ts';
|
||||
|
||||
// code that runs for all markup content
|
||||
export function initMarkupContent() {
|
||||
export function initMarkupContent(): void {
|
||||
renderMermaid();
|
||||
renderMath();
|
||||
renderCodeCopy();
|
||||
@@ -13,6 +13,6 @@ export function initMarkupContent() {
|
||||
}
|
||||
|
||||
// code that only runs for comments
|
||||
export function initCommentContent() {
|
||||
export function initCommentContent(): void {
|
||||
initMarkupTasklist();
|
||||
}
|
||||
|
@@ -12,21 +12,20 @@ type ProcessorContext = {
|
||||
|
||||
function prepareProcessors(ctx:ProcessorContext): Processors {
|
||||
const processors = {
|
||||
H1(el) {
|
||||
H1(el: HTMLHeadingElement) {
|
||||
const level = parseInt(el.tagName.slice(1));
|
||||
el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`;
|
||||
},
|
||||
STRONG(el) {
|
||||
STRONG(el: HTMLElement) {
|
||||
return `**${el.textContent}**`;
|
||||
},
|
||||
EM(el) {
|
||||
EM(el: HTMLElement) {
|
||||
return `_${el.textContent}_`;
|
||||
},
|
||||
DEL(el) {
|
||||
DEL(el: HTMLElement) {
|
||||
return `~~${el.textContent}~~`;
|
||||
},
|
||||
|
||||
A(el) {
|
||||
A(el: HTMLAnchorElement) {
|
||||
const text = el.textContent || 'link';
|
||||
const href = el.getAttribute('href');
|
||||
if (/^https?:/.test(text) && text === href) {
|
||||
@@ -34,7 +33,7 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
|
||||
}
|
||||
return href ? `[${text}](${href})` : text;
|
||||
},
|
||||
IMG(el) {
|
||||
IMG(el: HTMLImageElement) {
|
||||
const alt = el.getAttribute('alt') || 'image';
|
||||
const src = el.getAttribute('src');
|
||||
const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : '';
|
||||
@@ -44,32 +43,29 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
|
||||
}
|
||||
return ``;
|
||||
},
|
||||
|
||||
P(el) {
|
||||
P(el: HTMLParagraphElement) {
|
||||
el.textContent = `${el.textContent}\n`;
|
||||
},
|
||||
BLOCKQUOTE(el) {
|
||||
BLOCKQUOTE(el: HTMLElement) {
|
||||
el.textContent = `${el.textContent.replace(/^/mg, '> ')}\n`;
|
||||
},
|
||||
|
||||
OL(el) {
|
||||
OL(el: HTMLElement) {
|
||||
const preNewLine = ctx.listNestingLevel ? '\n' : '';
|
||||
el.textContent = `${preNewLine}${el.textContent}\n`;
|
||||
},
|
||||
LI(el) {
|
||||
LI(el: HTMLElement) {
|
||||
const parent = el.parentNode;
|
||||
const bullet = parent.tagName === 'OL' ? `1. ` : '* ';
|
||||
const bullet = (parent as HTMLElement).tagName === 'OL' ? `1. ` : '* ';
|
||||
const nestingIdentLevel = Math.max(0, ctx.listNestingLevel - 1);
|
||||
el.textContent = `${' '.repeat(nestingIdentLevel * 4)}${bullet}${el.textContent}${ctx.elementIsLast ? '' : '\n'}`;
|
||||
return el;
|
||||
},
|
||||
INPUT(el) {
|
||||
INPUT(el: HTMLInputElement) {
|
||||
return el.checked ? '[x] ' : '[ ] ';
|
||||
},
|
||||
|
||||
CODE(el) {
|
||||
CODE(el: HTMLElement) {
|
||||
const text = el.textContent;
|
||||
if (el.parentNode && el.parentNode.tagName === 'PRE') {
|
||||
if (el.parentNode && (el.parentNode as HTMLElement).tagName === 'PRE') {
|
||||
el.textContent = `\`\`\`\n${text}\n\`\`\`\n`;
|
||||
return el;
|
||||
}
|
||||
@@ -86,7 +82,7 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
|
||||
return processors;
|
||||
}
|
||||
|
||||
function processElement(ctx :ProcessorContext, processors: Processors, el: HTMLElement) {
|
||||
function processElement(ctx :ProcessorContext, processors: Processors, el: HTMLElement): string | void {
|
||||
if (el.hasAttribute('data-markdown-generated-content')) return el.textContent;
|
||||
if (el.tagName === 'A' && el.children.length === 1 && el.children[0].tagName === 'IMG') {
|
||||
return processElement(ctx, processors, el.children[0] as HTMLElement);
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import {displayError} from './common.ts';
|
||||
|
||||
function targetElement(el) {
|
||||
function targetElement(el: Element) {
|
||||
// The target element is either the current element if it has the
|
||||
// `is-loading` class or the pre that contains it
|
||||
return el.classList.contains('is-loading') ? el : el.closest('pre');
|
||||
}
|
||||
|
||||
export async function renderMath() {
|
||||
export async function renderMath(): Promise<void> {
|
||||
const els = document.querySelectorAll('.markup code.language-math');
|
||||
if (!els.length) return;
|
||||
|
||||
|
@@ -10,7 +10,7 @@ body {margin: 0; padding: 0; overflow: hidden}
|
||||
#mermaid {display: block; margin: 0 auto}
|
||||
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
|
||||
|
||||
export async function renderMermaid() {
|
||||
export async function renderMermaid(): Promise<void> {
|
||||
const els = document.querySelectorAll('.markup code.language-mermaid');
|
||||
if (!els.length) return;
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
|
||||
const preventListener = (e) => e.preventDefault();
|
||||
const preventListener = (e: Event) => e.preventDefault();
|
||||
|
||||
/**
|
||||
* Attaches `input` handlers to markdown rendered tasklist checkboxes in comments.
|
||||
@@ -10,10 +10,10 @@ const preventListener = (e) => e.preventDefault();
|
||||
* is set accordingly and sent to the server. On success it updates the raw-content on
|
||||
* error it resets the checkbox to its original value.
|
||||
*/
|
||||
export function initMarkupTasklist() {
|
||||
export function initMarkupTasklist(): void {
|
||||
for (const el of document.querySelectorAll(`.markup[data-can-edit=true]`) || []) {
|
||||
const container = el.parentNode;
|
||||
const checkboxes = el.querySelectorAll(`.task-list-item input[type=checkbox]`);
|
||||
const checkboxes = el.querySelectorAll<HTMLInputElement>(`.task-list-item input[type=checkbox]`);
|
||||
|
||||
for (const checkbox of checkboxes) {
|
||||
if (checkbox.hasAttribute('data-editable')) {
|
||||
@@ -52,7 +52,7 @@ export function initMarkupTasklist() {
|
||||
}
|
||||
|
||||
try {
|
||||
const editContentZone = container.querySelector('.edit-content-zone');
|
||||
const editContentZone = container.querySelector<HTMLDivElement>('.edit-content-zone');
|
||||
const updateUrl = editContentZone.getAttribute('data-update-url');
|
||||
const context = editContentZone.getAttribute('data-context');
|
||||
const contentVersion = editContentZone.getAttribute('data-content-version');
|
||||
|
@@ -13,7 +13,7 @@ function attachDirAuto(el: DirElement) {
|
||||
}
|
||||
}
|
||||
|
||||
export function initDirAuto() {
|
||||
export function initDirAuto(): void {
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
const len = mutationList.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
@@ -9,7 +9,7 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
||||
// fetch wrapper, use below method name functions and the `data` option to pass in data
|
||||
// which will automatically set an appropriate headers. For json content, only object
|
||||
// and array types are currently supported.
|
||||
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}) {
|
||||
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}): Promise<Response> {
|
||||
let body: RequestData;
|
||||
let contentType: string;
|
||||
if (data instanceof FormData || data instanceof URLSearchParams) {
|
||||
|
@@ -19,7 +19,7 @@ export function initGiteaFomantic() {
|
||||
// Do not use "cursor: pointer" for dropdown labels
|
||||
$.fn.dropdown.settings.className.label += ' tw-cursor-default';
|
||||
// Always use Gitea's SVG icons
|
||||
$.fn.dropdown.settings.templates.label = function(_value, text, preserveHTML, className) {
|
||||
$.fn.dropdown.settings.templates.label = function(_value: any, text: any, preserveHTML: any, className: Record<string, string>) {
|
||||
const escape = $.fn.dropdown.settings.templates.escape;
|
||||
return escape(text, preserveHTML) + svg('octicon-x', 16, `${className.delete} icon`);
|
||||
};
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
|
||||
export function initFomanticApiPatch() {
|
||||
//
|
||||
@@ -15,7 +16,7 @@ export function initFomanticApiPatch() {
|
||||
//
|
||||
const patchKey = '_giteaFomanticApiPatch';
|
||||
const oldApi = $.api;
|
||||
$.api = $.fn.api = function(...args) {
|
||||
$.api = $.fn.api = function(...args: Parameters<FomanticInitFunction>) {
|
||||
const apiCall = oldApi.bind(this);
|
||||
const ret = oldApi.apply(this, args);
|
||||
|
||||
@@ -23,7 +24,7 @@ export function initFomanticApiPatch() {
|
||||
const internalGet = apiCall('internal', 'get');
|
||||
if (!internalGet.urlEncodedValue[patchKey]) {
|
||||
const oldUrlEncodedValue = internalGet.urlEncodedValue;
|
||||
internalGet.urlEncodedValue = function (value) {
|
||||
internalGet.urlEncodedValue = function (value: any) {
|
||||
try {
|
||||
return oldUrlEncodedValue(value);
|
||||
} catch {
|
||||
|
@@ -5,7 +5,7 @@ export function generateAriaId() {
|
||||
return `_aria_auto_id_${ariaIdCounter++}`;
|
||||
}
|
||||
|
||||
export function linkLabelAndInput(label, input) {
|
||||
export function linkLabelAndInput(label: Element, input: Element) {
|
||||
const labelFor = label.getAttribute('for');
|
||||
const inputId = input.getAttribute('id');
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import {queryElemChildren} from '../../utils/dom.ts';
|
||||
|
||||
export function initFomanticDimmer() {
|
||||
// stand-in for removed dimmer module
|
||||
$.fn.dimmer = function (arg0, arg1) {
|
||||
$.fn.dimmer = function (arg0: string, arg1: any) {
|
||||
if (arg0 === 'add content') {
|
||||
const $el = arg1;
|
||||
const existingDimmer = document.querySelector('body > .ui.dimmer');
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import $ from 'jquery';
|
||||
import {generateAriaId} from './base.ts';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
|
||||
const ariaPatchKey = '_giteaAriaPatchDropdown';
|
||||
const fomanticDropdownFn = $.fn.dropdown;
|
||||
@@ -8,13 +9,13 @@ const fomanticDropdownFn = $.fn.dropdown;
|
||||
export function initAriaDropdownPatch() {
|
||||
if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
|
||||
$.fn.dropdown = ariaDropdownFn;
|
||||
ariaDropdownFn.settings = fomanticDropdownFn.settings;
|
||||
(ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings;
|
||||
}
|
||||
|
||||
// the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
|
||||
// * it does the one-time attaching on the first call
|
||||
// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
|
||||
function ariaDropdownFn(...args) {
|
||||
function ariaDropdownFn(...args: Parameters<FomanticInitFunction>) {
|
||||
const ret = fomanticDropdownFn.apply(this, args);
|
||||
|
||||
// if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
|
||||
@@ -33,7 +34,7 @@ function ariaDropdownFn(...args) {
|
||||
|
||||
// make the item has role=option/menuitem, add an id if there wasn't one yet, make items as non-focusable
|
||||
// the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
|
||||
function updateMenuItem(dropdown, item) {
|
||||
function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) {
|
||||
if (!item.id) item.id = generateAriaId();
|
||||
item.setAttribute('role', dropdown[ariaPatchKey].listItemRole);
|
||||
item.setAttribute('tabindex', '-1');
|
||||
@@ -43,7 +44,7 @@ function updateMenuItem(dropdown, item) {
|
||||
* make the label item and its "delete icon" have correct aria attributes
|
||||
* @param {HTMLElement} label
|
||||
*/
|
||||
function updateSelectionLabel(label) {
|
||||
function updateSelectionLabel(label: HTMLElement) {
|
||||
// the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
|
||||
if (!label.id) {
|
||||
label.id = generateAriaId();
|
||||
@@ -59,7 +60,7 @@ function updateSelectionLabel(label) {
|
||||
}
|
||||
|
||||
// delegate the dropdown's template functions and callback functions to add aria attributes.
|
||||
function delegateOne($dropdown) {
|
||||
function delegateOne($dropdown: any) {
|
||||
const dropdownCall = fomanticDropdownFn.bind($dropdown);
|
||||
|
||||
// If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked.
|
||||
@@ -74,7 +75,7 @@ function delegateOne($dropdown) {
|
||||
// the "template" functions are used for dynamic creation (eg: AJAX)
|
||||
const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()};
|
||||
const dropdownTemplatesMenuOld = dropdownTemplates.menu;
|
||||
dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
|
||||
dropdownTemplates.menu = function(response: any, fields: any, preserveHTML: any, className: Record<string, string>) {
|
||||
// when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
|
||||
const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
|
||||
const div = document.createElement('div');
|
||||
@@ -89,7 +90,7 @@ function delegateOne($dropdown) {
|
||||
|
||||
// the `onLabelCreate` is used to add necessary aria attributes for dynamically created selection labels
|
||||
const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate');
|
||||
dropdownCall('setting', 'onLabelCreate', function(value, text) {
|
||||
dropdownCall('setting', 'onLabelCreate', function(value: any, text: string) {
|
||||
const $label = dropdownOnLabelCreateOld.call(this, value, text);
|
||||
updateSelectionLabel($label[0]);
|
||||
return $label;
|
||||
@@ -97,7 +98,7 @@ function delegateOne($dropdown) {
|
||||
|
||||
const oldSet = dropdownCall('internal', 'set');
|
||||
const oldSetDirection = oldSet.direction;
|
||||
oldSet.direction = function($menu) {
|
||||
oldSet.direction = function($menu: any) {
|
||||
oldSetDirection.call(this, $menu);
|
||||
const classNames = dropdownCall('setting', 'className');
|
||||
$menu = $menu || $dropdown.find('> .menu');
|
||||
@@ -113,7 +114,7 @@ function delegateOne($dropdown) {
|
||||
}
|
||||
|
||||
// for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes
|
||||
function attachStaticElements(dropdown, focusable, menu) {
|
||||
function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, menu: HTMLElement) {
|
||||
// prepare static dropdown menu list popup
|
||||
if (!menu.id) {
|
||||
menu.id = generateAriaId();
|
||||
@@ -125,7 +126,7 @@ function attachStaticElements(dropdown, focusable, menu) {
|
||||
menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole);
|
||||
|
||||
// prepare selection label items
|
||||
for (const label of dropdown.querySelectorAll('.ui.label')) {
|
||||
for (const label of dropdown.querySelectorAll<HTMLElement>('.ui.label')) {
|
||||
updateSelectionLabel(label);
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ function attachStaticElements(dropdown, focusable, menu) {
|
||||
}
|
||||
}
|
||||
|
||||
function attachInit(dropdown) {
|
||||
function attachInit(dropdown: HTMLElement) {
|
||||
dropdown[ariaPatchKey] = {};
|
||||
if (dropdown.classList.contains('custom')) return;
|
||||
|
||||
@@ -161,7 +162,7 @@ function attachInit(dropdown) {
|
||||
|
||||
// TODO: multiple selection is only partially supported. Check and test them one by one in the future.
|
||||
|
||||
const textSearch = dropdown.querySelector('input.search');
|
||||
const textSearch = dropdown.querySelector<HTMLElement>('input.search');
|
||||
const focusable = textSearch || dropdown; // the primary element for focus, see comment above
|
||||
if (!focusable) return;
|
||||
|
||||
@@ -191,7 +192,7 @@ function attachInit(dropdown) {
|
||||
attachStaticElements(dropdown, focusable, menu);
|
||||
}
|
||||
|
||||
function attachDomEvents(dropdown, focusable, menu) {
|
||||
function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HTMLElement) {
|
||||
// when showing, it has class: ".animating.in"
|
||||
// when hiding, it has class: ".visible.animating.out"
|
||||
const isMenuVisible = () => (menu.classList.contains('visible') && !menu.classList.contains('out')) || menu.classList.contains('in');
|
||||
@@ -215,7 +216,7 @@ function attachDomEvents(dropdown, focusable, menu) {
|
||||
}
|
||||
};
|
||||
|
||||
dropdown.addEventListener('keydown', (e) => {
|
||||
dropdown.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
// here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
|
||||
if (e.key === 'Enter') {
|
||||
const dropdownCall = fomanticDropdownFn.bind($(dropdown));
|
||||
@@ -260,7 +261,7 @@ function attachDomEvents(dropdown, focusable, menu) {
|
||||
deferredRefreshAriaActiveItem(100);
|
||||
}, 0);
|
||||
}, true);
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
dropdown.addEventListener('click', (e: MouseEvent) => {
|
||||
if (isMenuVisible() &&
|
||||
ignoreClickPreVisible !== 2 && // dropdown is switch from invisible to visible
|
||||
ignoreClickPreEvents === 2 // the click event is related to mousedown+focus
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
|
||||
const fomanticModalFn = $.fn.modal;
|
||||
|
||||
@@ -6,12 +7,12 @@ const fomanticModalFn = $.fn.modal;
|
||||
export function initAriaModalPatch() {
|
||||
if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
|
||||
$.fn.modal = ariaModalFn;
|
||||
ariaModalFn.settings = fomanticModalFn.settings;
|
||||
(ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
|
||||
}
|
||||
|
||||
// the patched `$.fn.modal` modal function
|
||||
// * it does the one-time attaching on the first call
|
||||
function ariaModalFn(...args) {
|
||||
function ariaModalFn(...args: Parameters<FomanticInitFunction>) {
|
||||
const ret = fomanticModalFn.apply(this, args);
|
||||
if (args[0] === 'show' || args[0]?.autoShow) {
|
||||
for (const el of this) {
|
||||
|
@@ -8,13 +8,13 @@ export function initFomanticTransition() {
|
||||
'set duration', 'save conditions', 'restore conditions',
|
||||
]);
|
||||
// stand-in for removed transition module
|
||||
$.fn.transition = function (arg0, arg1, arg2) {
|
||||
$.fn.transition = function (arg0: any, arg1: any, arg2: any) {
|
||||
if (arg0 === 'is supported') return true;
|
||||
if (arg0 === 'is animating') return false;
|
||||
if (arg0 === 'is inward') return false;
|
||||
if (arg0 === 'is outward') return false;
|
||||
|
||||
let argObj;
|
||||
let argObj: Record<string, any>;
|
||||
if (typeof arg0 === 'string') {
|
||||
// many behaviors are no-op now. https://fomantic-ui.com/modules/transition.html#/usage
|
||||
if (transitionNopBehaviors.has(arg0)) return this;
|
||||
|
@@ -1,17 +1,18 @@
|
||||
import type {SortableOptions} from 'sortablejs';
|
||||
import type {SortableOptions, SortableEvent} from 'sortablejs';
|
||||
|
||||
export async function createSortable(el, opts: {handle?: string} & SortableOptions = {}) {
|
||||
export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}) {
|
||||
// @ts-expect-error: wrong type derived by typescript
|
||||
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
|
||||
|
||||
return new Sortable(el, {
|
||||
animation: 150,
|
||||
ghostClass: 'card-ghost',
|
||||
onChoose: (e) => {
|
||||
onChoose: (e: SortableEvent) => {
|
||||
const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
|
||||
handle.classList.add('tw-cursor-grabbing');
|
||||
opts.onChoose?.(e);
|
||||
},
|
||||
onUnchoose: (e) => {
|
||||
onUnchoose: (e: SortableEvent) => {
|
||||
const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
|
||||
handle.classList.remove('tw-cursor-grabbing');
|
||||
opts.onUnchoose?.(e);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import tippy, {followCursor} from 'tippy.js';
|
||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
import {formatDatetime} from '../utils/time.ts';
|
||||
import type {Content, Instance, Props} from 'tippy.js';
|
||||
import type {Content, Instance, Placement, Props} from 'tippy.js';
|
||||
|
||||
type TippyOpts = {
|
||||
role?: string,
|
||||
@@ -16,6 +16,7 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||
|
||||
// @ts-expect-error: wrong type derived by typescript
|
||||
const instance: Instance = tippy(target, {
|
||||
appendTo: document.body,
|
||||
animation: false,
|
||||
@@ -65,7 +66,7 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
*
|
||||
* Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation.
|
||||
*/
|
||||
function attachTooltip(target: Element, content: Content = null) {
|
||||
function attachTooltip(target: Element, content: Content = null): Instance {
|
||||
switchTitleToTooltip(target);
|
||||
|
||||
content = content ?? target.getAttribute('data-tooltip-content');
|
||||
@@ -77,16 +78,16 @@ function attachTooltip(target: Element, content: Content = null) {
|
||||
const hasClipboardTarget = target.hasAttribute('data-clipboard-target');
|
||||
const hideOnClick = !hasClipboardTarget;
|
||||
|
||||
const props = {
|
||||
const props: TippyOpts = {
|
||||
content,
|
||||
delay: 100,
|
||||
role: 'tooltip',
|
||||
theme: 'tooltip',
|
||||
hideOnClick,
|
||||
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
|
||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
|
||||
placement: target.getAttribute('data-tooltip-placement') as Placement || 'top-start',
|
||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') as Props['followCursor'] || false,
|
||||
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
|
||||
} as TippyOpts;
|
||||
};
|
||||
|
||||
if (!target._tippy) {
|
||||
createTippy(target, props);
|
||||
@@ -96,7 +97,7 @@ function attachTooltip(target: Element, content: Content = null) {
|
||||
return target._tippy;
|
||||
}
|
||||
|
||||
function switchTitleToTooltip(target: Element) {
|
||||
function switchTitleToTooltip(target: Element): void {
|
||||
let title = target.getAttribute('title');
|
||||
if (title) {
|
||||
// apply custom formatting to relative-time's tooltips
|
||||
@@ -121,14 +122,14 @@ function switchTitleToTooltip(target: Element) {
|
||||
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
|
||||
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
||||
*/
|
||||
function lazyTooltipOnMouseHover(e: MouseEvent) {
|
||||
function lazyTooltipOnMouseHover(e: MouseEvent): void {
|
||||
e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
|
||||
attachTooltip(this);
|
||||
}
|
||||
|
||||
// Activate the tooltip for current element.
|
||||
// If the element has no aria-label, use the tooltip content as aria-label.
|
||||
function attachLazyTooltip(el: Element) {
|
||||
function attachLazyTooltip(el: Element): void {
|
||||
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
|
||||
|
||||
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
|
||||
@@ -141,13 +142,13 @@ function attachLazyTooltip(el: Element) {
|
||||
}
|
||||
|
||||
// Activate the tooltip for all children elements.
|
||||
function attachChildrenLazyTooltip(target: Element) {
|
||||
function attachChildrenLazyTooltip(target: Element): void {
|
||||
for (const el of target.querySelectorAll<Element>('[data-tooltip-content]')) {
|
||||
attachLazyTooltip(el);
|
||||
}
|
||||
}
|
||||
|
||||
export function initGlobalTooltips() {
|
||||
export function initGlobalTooltips(): void {
|
||||
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
|
||||
const observerConnect = (observer: MutationObserver) => observer.observe(document, {
|
||||
subtree: true,
|
||||
@@ -178,7 +179,7 @@ export function initGlobalTooltips() {
|
||||
attachChildrenLazyTooltip(document.documentElement);
|
||||
}
|
||||
|
||||
export function showTemporaryTooltip(target: Element, content: Content) {
|
||||
export function showTemporaryTooltip(target: Element, content: Content): void {
|
||||
// if the target is inside a dropdown, the menu will be hidden soon
|
||||
// so display the tooltip on the dropdown instead
|
||||
target = target.closest('.ui.dropdown') || target;
|
||||
|
@@ -2,7 +2,7 @@ import {sleep} from '../utils.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
export async function logoutFromWorker() {
|
||||
export async function logoutFromWorker(): Promise<void> {
|
||||
// wait for a while because other requests (eg: logout) may be in the flight
|
||||
await sleep(5000);
|
||||
window.location.href = `${appSubUrl}/`;
|
||||
|
@@ -153,7 +153,7 @@ export type SvgName = keyof typeof svgs;
|
||||
// most of the SVG icons in assets couldn't be used directly.
|
||||
|
||||
// retrieve an HTML string for given SVG icon name, size and additional classes
|
||||
export function svg(name: SvgName, size = 16, classNames: string|string[]): string {
|
||||
export function svg(name: SvgName, size = 16, classNames?: string|string[]): string {
|
||||
const className = Array.isArray(classNames) ? classNames.join(' ') : classNames;
|
||||
if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
|
||||
if (size === 16 && !className) return svgs[name];
|
||||
|
@@ -54,3 +54,8 @@ export type Issue = {
|
||||
merged: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type FomanticInitFunction = {
|
||||
settings?: Record<string, any>,
|
||||
(...args: any[]): any,
|
||||
}
|
||||
|
@@ -118,7 +118,7 @@ test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
|
||||
});
|
||||
|
||||
test('file detection', () => {
|
||||
for (const name of ['a.jpg', '/a.jpeg', '.file.png', '.webp', 'file.svg']) {
|
||||
for (const name of ['a.avif', 'a.jpg', '/a.jpeg', '.file.png', '.webp', 'file.svg']) {
|
||||
expect(isImageFile({name})).toBeTruthy();
|
||||
}
|
||||
for (const name of ['', 'a.jpg.x', '/path.png/x', 'webp']) {
|
||||
|
@@ -165,7 +165,7 @@ export function sleep(ms: number): Promise<void> {
|
||||
}
|
||||
|
||||
export function isImageFile({name, type}: {name: string, type?: string}): boolean {
|
||||
return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
|
||||
return /\.(avif|jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
|
||||
}
|
||||
|
||||
export function isVideoFile({name, type}: {name: string, type?: string}): boolean {
|
||||
|
@@ -2,6 +2,7 @@ import {createElementFromAttrs, createElementFromHTML, querySingleVisibleElem} f
|
||||
|
||||
test('createElementFromHTML', () => {
|
||||
expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
|
||||
expect(createElementFromHTML('<tr data-x="1"><td>foo</td></tr>').outerHTML).toEqual('<tr data-x="1"><td>foo</td></tr>');
|
||||
});
|
||||
|
||||
test('createElementFromAttrs', () => {
|
||||
|
@@ -301,10 +301,17 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st
|
||||
}
|
||||
|
||||
// Warning: Do not enter any unsanitized variables here
|
||||
export function createElementFromHTML(htmlString: string): HTMLElement {
|
||||
export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T {
|
||||
htmlString = htmlString.trim();
|
||||
// some tags like "tr" are special, it must use a correct parent container to create
|
||||
if (htmlString.startsWith('<tr')) {
|
||||
const container = document.createElement('table');
|
||||
container.innerHTML = htmlString;
|
||||
return container.querySelector<T>('tr');
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = htmlString.trim();
|
||||
return div.firstChild as HTMLElement;
|
||||
div.innerHTML = htmlString;
|
||||
return div.firstChild as T;
|
||||
}
|
||||
|
||||
export function createElementFromAttrs(tagName: string, attrs: Record<string, any>, ...children: (Node|string)[]): HTMLElement {
|
||||
@@ -340,3 +347,11 @@ export function querySingleVisibleElem<T extends HTMLElement>(parent: Element, s
|
||||
if (candidates.length > 1) throw new Error(`Expected exactly one visible element matching selector "${selector}", but found ${candidates.length}`);
|
||||
return candidates.length ? candidates[0] as T : null;
|
||||
}
|
||||
|
||||
export function addDelegatedEventListener<T extends HTMLElement>(parent: Node, type: string, selector: string, listener: (elem: T, e: Event) => void | Promise<any>, options?: boolean | AddEventListenerOptions) {
|
||||
parent.addEventListener(type, (e: Event) => {
|
||||
const elem = (e.target as HTMLElement).closest(selector);
|
||||
if (!elem) return;
|
||||
listener(elem as T, e);
|
||||
}, options);
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import emojis from '../../../assets/emoji.json';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import type {Issue} from '../features/issue.ts';
|
||||
import type {Issue} from '../types.ts';
|
||||
|
||||
const maxMatches = 6;
|
||||
|
||||
|
4
web_src/js/vendor/jquery.are-you-sure.ts
vendored
4
web_src/js/vendor/jquery.are-you-sure.ts
vendored
@@ -195,3 +195,7 @@ export function initAreYouSure($) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAreYouSure(selector: string) {
|
||||
$(selector).areYouSure();
|
||||
}
|
||||
|
Reference in New Issue
Block a user