mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 05:53:23 +09:00
refactor: replace jquery.are-you-sure with first-party code (#39233)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import {applyAreYouSure, ignoreAreYouSure, initGlobalFormDirtyLeaveConfirm, shouldTriggerAreYouSure} from './are-you-sure.ts';
|
||||
|
||||
function createForm(fieldsHtml: string, wrapperClass = ''): HTMLFormElement {
|
||||
document.body.insertAdjacentHTML('beforeend', `<div class="${wrapperClass}"><form>${fieldsHtml}</form></div>`);
|
||||
return document.body.lastElementChild!.querySelector('form')!;
|
||||
}
|
||||
|
||||
const field = (form: HTMLFormElement, name: string) => form.elements.namedItem(name) as HTMLInputElement;
|
||||
|
||||
function set(field: HTMLInputElement, props: Partial<HTMLInputElement>, eventType = 'change') {
|
||||
Object.assign(field, props);
|
||||
field.dispatchEvent(new Event(eventType, {bubbles: true}));
|
||||
}
|
||||
|
||||
const isBeforeUnloadPrevented = () => !window.dispatchEvent(new Event('beforeunload', {cancelable: true}));
|
||||
|
||||
afterEach(() => document.body.replaceChildren());
|
||||
|
||||
test('tracks field changes, clears on submit and reset', () => {
|
||||
const form = createForm(`
|
||||
<input name="title" value="a">
|
||||
<input type="checkbox" name="flag">
|
||||
<select name="pick"><option value="a" selected>a</option><option value="b">b</option></select>
|
||||
`);
|
||||
const onDirtyChange = vi.fn();
|
||||
applyAreYouSure(form, onDirtyChange);
|
||||
set(field(form, 'title'), {value: 'b'}, 'input');
|
||||
set(field(form, 'title'), {value: 'c'}, 'input');
|
||||
set(field(form, 'title'), {value: 'a'}, 'keyup');
|
||||
set(field(form, 'flag'), {checked: true});
|
||||
set(field(form, 'flag'), {checked: false});
|
||||
set(field(form, 'pick'), {value: 'b'});
|
||||
expect(onDirtyChange.mock.calls).toEqual([[true], [false], [true], [false], [true]]);
|
||||
expect(shouldTriggerAreYouSure()).toBe(true);
|
||||
form.addEventListener('submit', (e) => e.preventDefault());
|
||||
form.dispatchEvent(new SubmitEvent('submit', {cancelable: true}));
|
||||
expect(shouldTriggerAreYouSure()).toBe(false);
|
||||
set(field(form, 'title'), {value: 'b'});
|
||||
form.reset();
|
||||
expect(shouldTriggerAreYouSure()).toBe(false);
|
||||
});
|
||||
|
||||
test('ignores untracked fields', () => {
|
||||
const form = createForm(`
|
||||
<input value="a">
|
||||
<input name="dummy" class="ays-ignore" value="a">
|
||||
<div class="ays-ignore"><input name="summary" value="a"></div>
|
||||
<input type="submit" name="send" value="a">
|
||||
<input type="button" name="act" value="a">
|
||||
`);
|
||||
applyAreYouSure(form);
|
||||
form.insertAdjacentHTML('beforeend', '<input name="later" value="a">');
|
||||
for (const input of form.querySelectorAll('input')) set(input, {value: 'b'});
|
||||
expect(shouldTriggerAreYouSure()).toBe(false);
|
||||
});
|
||||
|
||||
test('applying again resets the baseline', () => {
|
||||
const form = createForm(`
|
||||
<input name="title" value="a">
|
||||
<input name="extra" value="a">
|
||||
<div class="wrapper"><input name="summary" value="a"></div>
|
||||
`);
|
||||
const oldOnDirtyChange = vi.fn();
|
||||
const onDirtyChange = vi.fn();
|
||||
applyAreYouSure(form, oldOnDirtyChange);
|
||||
set(field(form, 'title'), {value: 'b'});
|
||||
form.querySelector('.wrapper')!.classList.add('ays-ignore');
|
||||
applyAreYouSure(form, onDirtyChange);
|
||||
set(field(form, 'summary'), {value: 'b'});
|
||||
set(field(form, 'extra'), {value: 'b'});
|
||||
field(form, 'extra').remove();
|
||||
set(field(form, 'title'), {value: 'b'});
|
||||
set(field(form, 'title'), {value: 'a'});
|
||||
expect(oldOnDirtyChange.mock.calls).toEqual([[true]]);
|
||||
expect(onDirtyChange.mock.calls).toEqual([[true], [false], [true]]);
|
||||
});
|
||||
|
||||
test('initGlobalFormDirtyLeaveConfirm guards beforeunload', () => {
|
||||
const signin = createForm('<input name="user_name" value="a">', 'page-content user signin');
|
||||
initGlobalFormDirtyLeaveConfirm();
|
||||
set(field(signin, 'user_name'), {value: 'b'});
|
||||
expect(shouldTriggerAreYouSure()).toBe(false);
|
||||
signin.parentElement!.remove();
|
||||
const form = createForm('<input name="title" value="a">');
|
||||
const ignored = createForm('<input name="q" value="a">');
|
||||
ignored.classList.add('ignore-dirty');
|
||||
initGlobalFormDirtyLeaveConfirm();
|
||||
set(field(ignored, 'q'), {value: 'b'});
|
||||
ignored.classList.remove('ignore-dirty');
|
||||
expect(isBeforeUnloadPrevented()).toBe(false);
|
||||
set(field(form, 'title'), {value: 'b'});
|
||||
expect(isBeforeUnloadPrevented()).toBe(true);
|
||||
form.parentElement!.classList.add('tw-hidden');
|
||||
expect(isBeforeUnloadPrevented()).toBe(false);
|
||||
form.parentElement!.classList.remove('tw-hidden');
|
||||
ignoreAreYouSure(form);
|
||||
expect(isBeforeUnloadPrevented()).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
type FormField = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
type AreYouSureState = {onDirtyChange?: (dirty: boolean) => void, dirty: boolean};
|
||||
|
||||
const areYouSureStates = new WeakMap<HTMLFormElement, AreYouSureState>();
|
||||
const originalValues = new WeakMap<FormField, string>();
|
||||
|
||||
function fieldValue(field: FormField): string {
|
||||
if (field instanceof HTMLSelectElement) {
|
||||
return JSON.stringify(Array.from(field.selectedOptions, (option) => option.value));
|
||||
}
|
||||
if (field instanceof HTMLInputElement && ['checkbox', 'radio'].includes(field.type)) return String(field.checked);
|
||||
return field.value;
|
||||
}
|
||||
|
||||
function trackedFields(form: HTMLFormElement): FormField[] {
|
||||
return Array.from(form.querySelectorAll<FormField>('input:not([type=submit], [type=button]), select, textarea')).filter((field) => {
|
||||
return field.name && !field.closest('.ays-ignore');
|
||||
});
|
||||
}
|
||||
|
||||
function setDirty(form: HTMLFormElement, dirty: boolean) {
|
||||
const state = areYouSureStates.get(form)!;
|
||||
if (state.dirty === dirty) return;
|
||||
state.dirty = dirty;
|
||||
state.onDirtyChange?.(dirty);
|
||||
}
|
||||
|
||||
export function applyAreYouSure(form: HTMLFormElement, onDirtyChange?: (dirty: boolean) => void) {
|
||||
if (!areYouSureStates.has(form)) {
|
||||
const isFieldDirty = (field: FormField) => originalValues.has(field) && originalValues.get(field) !== fieldValue(field);
|
||||
const checkDirty = () => setDirty(form, trackedFields(form).some(isFieldDirty));
|
||||
// keyup: some keydown handlers set values without an input event
|
||||
for (const eventType of ['input', 'change', 'keyup']) form.addEventListener(eventType, checkDirty);
|
||||
for (const eventType of ['submit', 'reset']) form.addEventListener(eventType, () => setDirty(form, false));
|
||||
}
|
||||
areYouSureStates.set(form, {onDirtyChange, dirty: false});
|
||||
for (const field of trackedFields(form)) originalValues.set(field, fieldValue(field));
|
||||
}
|
||||
|
||||
export function ignoreAreYouSure(el: Element) {
|
||||
el.classList.add('ignore-dirty');
|
||||
}
|
||||
|
||||
export function shouldTriggerAreYouSure(): boolean {
|
||||
return Array.from(document.querySelectorAll<HTMLFormElement>('form:not(.ignore-dirty)')).some((form) => {
|
||||
return areYouSureStates.get(form)?.dirty && !form.closest('.tw-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
export function initGlobalFormDirtyLeaveConfirm() {
|
||||
// TODO: refactor the "signin" forms to use "ignore-dirty" class directly, then decouple this module
|
||||
if (document.querySelector('.page-content.user.signin')) return;
|
||||
for (const form of document.querySelectorAll<HTMLFormElement>('form:not(.ignore-dirty)')) applyAreYouSure(form);
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
if (shouldTriggerAreYouSure()) e.preventDefault();
|
||||
});
|
||||
}
|
||||
@@ -262,7 +262,7 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn
|
||||
cm.view.EditorView.updateListener.of((update: ViewUpdate) => {
|
||||
if (update.docChanged) {
|
||||
textarea.value = update.state.doc.toString();
|
||||
textarea.dispatchEvent(new Event('change')); // needed for jquery-are-you-sure
|
||||
textarea.dispatchEvent(new Event('change', {bubbles: true})); // for the areYouSure dirty tracking
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@ import {hideToastsAll, showErrorToast} from './toast.ts';
|
||||
import {activePageTimerRefresh, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts';
|
||||
import {errorMessage, errorName} from './errors.ts';
|
||||
import {confirmModal, createConfirmModal} from '../features/comp/ConfirmModal.ts';
|
||||
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
import {ignoreAreYouSure} from './are-you-sure.ts';
|
||||
import {registerGlobalSelectorFunc} from './observer.ts';
|
||||
import {Idiomorph} from 'idiomorph';
|
||||
import {parseDom} from '../utils.ts';
|
||||
|
||||
Reference in New Issue
Block a user