]> source.dussan.org Git - gitea.git/commitdiff
Correctly query the primary button in a form (#32438)
authorwxiaoguang <wxiaoguang@gmail.com>
Wed, 6 Nov 2024 20:21:53 +0000 (04:21 +0800)
committerGitHub <noreply@github.com>
Wed, 6 Nov 2024 20:21:53 +0000 (04:21 +0800)
The "primary button" is used at many places, but sometimes they might
conflict (due to button switch, hidden panel, dropdown menu, etc).

Sometimes we could add a special CSS class for the buttons, but
sometimes not (see the comment of QuickSubmit)

This PR introduces `querySingleVisibleElem` to help to get the correct
primary button (the only visible one), and prevent from querying the
wrong buttons.

Fix #32437

---------

Co-authored-by: silverwind <me@silverwind.io>
web_src/js/features/comp/QuickSubmit.ts
web_src/js/features/repo-issue-edit.ts
web_src/js/utils/dom.test.ts
web_src/js/utils/dom.ts

index 3ff29f4fac5c317a23fed47f8f67f25d6d7db49a..385acb319f7d0a6308b73034e42984cb4ae1f8c4 100644 (file)
@@ -1,3 +1,5 @@
+import {querySingleVisibleElem} from '../../utils/dom.ts';
+
 export function handleGlobalEnterQuickSubmit(target) {
   let form = target.closest('form');
   if (form) {
@@ -12,7 +14,11 @@ export function handleGlobalEnterQuickSubmit(target) {
   }
   form = target.closest('.ui.form');
   if (form) {
-    form.querySelector('.ui.primary.button')?.click();
+    // A form should only have at most one "primary" button to do quick-submit.
+    // Here we don't use a special class to mark the primary button,
+    // because there could be a lot of forms with a primary button, the quick submit should work out-of-box,
+    // but not keeps asking developers to add that special class again and again (it could be forgotten easily)
+    querySingleVisibleElem<HTMLButtonElement>(form, '.ui.primary.button')?.click();
     return true;
   }
   return false;
index 77a76ad3ca69740db37008013b1c960f13648e25..af97ee4eab140d63b1875e8f543870665b74c366 100644 (file)
@@ -3,7 +3,7 @@ import {handleReply} from './repo-issue.ts';
 import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
 import {POST} from '../modules/fetch.ts';
 import {showErrorToast} from '../modules/toast.ts';
-import {hideElem, showElem} from '../utils/dom.ts';
+import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts';
 import {attachRefIssueContextPopup} from './contextpopup.ts';
 import {initCommentContent, initMarkupContent} from '../markup/content.ts';
 import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
@@ -77,20 +77,22 @@ async function onEditContent(event) {
     }
   };
 
+  // Show write/preview tab and copy raw content as needed
+  showElem(editContentZone);
+  hideElem(renderContent);
+
   comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
   if (!comboMarkdownEditor) {
     editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML;
-    const saveButton = editContentZone.querySelector('.ui.primary.button');
+    const saveButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.primary.button');
+    const cancelButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.cancel.button');
     comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
     const syncUiState = () => saveButton.disabled = comboMarkdownEditor.isUploading();
     comboMarkdownEditor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState);
-    editContentZone.querySelector('.ui.cancel.button').addEventListener('click', cancelAndReset);
+    cancelButton.addEventListener('click', cancelAndReset);
     saveButton.addEventListener('click', saveAndRefresh);
   }
 
-  // Show write/preview tab and copy raw content as needed
-  showElem(editContentZone);
-  hideElem(renderContent);
   // FIXME: ideally here should reload content and attachment list from backend for existing editor, to avoid losing data
   if (!comboMarkdownEditor.value()) {
     comboMarkdownEditor.value(rawContent.textContent);
index 5c235795fd45482a3eeb4edbf86fe615a73a0c4a..d7e3a4939e23b1429445420024230cc1c1d977bd 100644 (file)
@@ -1,4 +1,4 @@
-import {createElementFromAttrs, createElementFromHTML} from './dom.ts';
+import {createElementFromAttrs, createElementFromHTML, querySingleVisibleElem} from './dom.ts';
 
 test('createElementFromHTML', () => {
   expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
@@ -16,3 +16,12 @@ test('createElementFromAttrs', () => {
   }, 'txt', createElementFromHTML('<span>inner</span>'));
   expect(el.outerHTML).toEqual('<button id="the-id" class="cls-1 cls-2" disabled="" tabindex="0" data-foo="the-data">txt<span>inner</span></button>');
 });
+
+test('querySingleVisibleElem', () => {
+  let el = createElementFromHTML('<div><span>foo</span></div>');
+  expect(querySingleVisibleElem(el, 'span').textContent).toEqual('foo');
+  el = createElementFromHTML('<div><span style="display: none;">foo</span><span>bar</span></div>');
+  expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar');
+  el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>');
+  expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element');
+});
index a6e0fe28543aefb1dcdcf673c606242e4ebc50e3..e2a4c60e84aad47cb19a1e2bc59f3545140a8cc8 100644 (file)
@@ -269,8 +269,8 @@ export function initSubmitEventPolyfill() {
  */
 export function isElemVisible(element: HTMLElement): boolean {
   if (!element) return false;
-
-  return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
+  // checking element.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout
+  return Boolean((element.offsetWidth || element.offsetHeight || element.getClientRects().length) && element.style.display !== 'none');
 }
 
 // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
@@ -330,3 +330,10 @@ export function animateOnce(el: Element, animationClassName: string): Promise<vo
     el.classList.add(animationClassName);
   });
 }
+
+export function querySingleVisibleElem<T extends HTMLElement>(parent: Element, selector: string): T | null {
+  const elems = parent.querySelectorAll<HTMLElement>(selector);
+  const candidates = Array.from(elems).filter(isElemVisible);
+  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;
+}