diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index d486c5830a..d209f11ab2 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -2,7 +2,7 @@ import '@github/markdown-toolbar-element';
 import '@github/text-expander-element';
 import $ from 'jquery';
 import {attachTribute} from '../tribute.js';
-import {hideElem, showElem, autosize} from '../../utils/dom.js';
+import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js';
 import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
 import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
 import {renderPreviewPanelContent} from '../repo-editor.js';
@@ -14,17 +14,17 @@ let elementIdCounter = 0;
 
 /**
  * validate if the given textarea is non-empty.
- * @param {jQuery} $textarea
+ * @param {HTMLElement} textarea - The textarea element to be validated.
  * @returns {boolean} returns true if validation succeeded.
  */
-export function validateTextareaNonEmpty($textarea) {
+export function validateTextareaNonEmpty(textarea) {
   // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation.
   // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert.
-  if (!$textarea.val()) {
-    if ($textarea.is(':visible')) {
-      $textarea.prop('required', true);
-      const $form = $textarea.parents('form');
-      $form[0]?.reportValidity();
+  if (!textarea.value) {
+    if (isElemVisible(textarea)) {
+      textarea.required = true;
+      const form = textarea.closest('form');
+      form?.reportValidity();
     } else {
       // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places.
       showErrorToast('Require non-empty content');
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 04dd153df2..85cb66c728 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -47,8 +47,8 @@ function initRepoDiffConversationForm() {
     e.preventDefault();
 
     const $form = $(e.target);
-    const $textArea = $form.find('textarea');
-    if (!validateTextareaNonEmpty($textArea)) {
+    const textArea = e.target.querySelector('textarea');
+    if (!validateTextareaNonEmpty(textArea)) {
       return;
     }
 
diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js
index 58036fde37..d51bf35c81 100644
--- a/web_src/js/features/repo-wiki.js
+++ b/web_src/js/features/repo-wiki.js
@@ -1,50 +1,51 @@
-import $ from 'jquery';
 import {initMarkupContent} from '../markup/content.js';
 import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
 import {fomanticMobileScreen} from '../modules/fomantic.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
 
 async function initRepoWikiFormEditor() {
-  const $editArea = $('.repository.wiki .combo-markdown-editor textarea');
-  if (!$editArea.length) return;
+  const editArea = document.querySelector('.repository.wiki .combo-markdown-editor textarea');
+  if (!editArea) return;
 
-  const $form = $('.repository.wiki.new .ui.form');
-  const $editorContainer = $form.find('.combo-markdown-editor');
+  const form = document.querySelector('.repository.wiki.new .ui.form');
+  const editorContainer = form.querySelector('.combo-markdown-editor');
   let editor;
 
   let renderRequesting = false;
   let lastContent;
-  const renderEasyMDEPreview = function () {
+  const renderEasyMDEPreview = async function () {
     if (renderRequesting) return;
 
-    const $previewFull = $editorContainer.find('.EasyMDEContainer .editor-preview-active');
-    const $previewSide = $editorContainer.find('.EasyMDEContainer .editor-preview-active-side');
-    const $previewTarget = $previewSide.length ? $previewSide : $previewFull;
-    const newContent = $editArea.val();
-    if (editor && $previewTarget.length && lastContent !== newContent) {
+    const previewFull = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active');
+    const previewSide = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active-side');
+    const previewTarget = previewSide || previewFull;
+    const newContent = editArea.value;
+    if (editor && previewTarget && lastContent !== newContent) {
       renderRequesting = true;
-      $.post(editor.previewUrl, {
-        _csrf: csrfToken,
-        mode: editor.previewMode,
-        context: editor.previewContext,
-        text: newContent,
-        wiki: editor.previewWiki,
-      }).done((data) => {
+      const formData = new FormData();
+      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();
         lastContent = newContent;
-        $previewTarget.html(`<div class="markup ui segment">${data}</div>`);
+        previewTarget.innerHTML = `<div class="markup ui segment">${data}</div>`;
         initMarkupContent();
-      }).always(() => {
+      } catch (error) {
+        console.error('Error rendering preview:', error);
+      } finally {
         renderRequesting = false;
         setTimeout(renderEasyMDEPreview, 1000);
-      });
+      }
     } else {
       setTimeout(renderEasyMDEPreview, 1000);
     }
   };
   renderEasyMDEPreview();
 
-  editor = await initComboMarkdownEditor($editorContainer, {
+  editor = await initComboMarkdownEditor(editorContainer, {
     useScene: 'wiki',
     // EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
     // And another benefit is that we only need to write the style once for both editors.
@@ -64,9 +65,10 @@ async function initRepoWikiFormEditor() {
     },
   });
 
-  $form.on('submit', () => {
-    if (!validateTextareaNonEmpty($editArea)) {
-      return false;
+  form.addEventListener('submit', (e) => {
+    if (!validateTextareaNonEmpty(editArea)) {
+      e.preventDefault();
+      e.stopPropagation();
     }
   });
 }
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index 4dc55a518a..1f6066acab 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -226,3 +226,15 @@ export function initSubmitEventPolyfill() {
   document.body.addEventListener('click', submitEventPolyfillListener);
   document.body.addEventListener('focus', submitEventPolyfillListener);
 }
+
+/**
+ * Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
+ * Note: This function doesn't account for all possible visibility scenarios.
+ * @param {HTMLElement} element The element to check.
+ * @returns {boolean} True if the element is visible.
+ */
+export function isElemVisible(element) {
+  if (!element) return false;
+
+  return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
+}