aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js/features
diff options
context:
space:
mode:
authorsilverwind <me@silverwind.io>2024-06-10 12:12:31 +0200
committerGitHub <noreply@github.com>2024-06-10 18:12:31 +0800
commita2304cb163ce5e097078e71f49d4d5cb4c8b20d9 (patch)
tree82527837cf93b3eba088c4a89df8ef4e19efe96e /web_src/js/features
parent4f7d6feab7e6cb6e8c5914a5b6cd20a64fd49c29 (diff)
downloadgitea-a2304cb163ce5e097078e71f49d4d5cb4c8b20d9.tar.gz
gitea-a2304cb163ce5e097078e71f49d4d5cb4c8b20d9.zip
Remove jQuery `.text()` (#30506)
Remove and forbid [.text()](https://api.jquery.com/text/). Tested some, but not all functionality, but I think these are pretty safe replacements. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'web_src/js/features')
-rw-r--r--web_src/js/features/common-global.js93
-rw-r--r--web_src/js/features/imagediff.js10
-rw-r--r--web_src/js/features/notification.js14
-rw-r--r--web_src/js/features/repo-editor.js111
-rw-r--r--web_src/js/features/repo-issue-edit.js7
-rw-r--r--web_src/js/features/repo-issue.js22
-rw-r--r--web_src/js/features/repo-legacy.js4
-rw-r--r--web_src/js/features/repo-settings.js45
8 files changed, 149 insertions, 157 deletions
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 65eb237dde..5162c71509 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -301,52 +301,65 @@ async function linkAction(e) {
}
}
-export function initGlobalLinkActions() {
- function showDeletePopup(e) {
- e.preventDefault();
- const $this = $(this);
- const dataArray = $this.data();
- let filter = '';
- if (this.getAttribute('data-modal-id')) {
- filter += `#${this.getAttribute('data-modal-id')}`;
- }
+export function initGlobalDeleteButton() {
+ // ".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')) {
+ btn.addEventListener('click', (e) => {
+ e.preventDefault();
- const $dialog = $(`.delete.modal${filter}`);
- $dialog.find('.name').text($this.data('name'));
- for (const [key, value] of Object.entries(dataArray)) {
- if (key && key.startsWith('data')) {
- $dialog.find(`.${key}`).text(value);
- }
- }
+ // eslint-disable-next-line github/no-dataset -- code depends on the camel-casing
+ const dataObj = btn.dataset;
+
+ const modalId = btn.getAttribute('data-modal-id');
+ const modal = document.querySelector(`.delete.modal${modalId ? `#${modalId}` : ''}`);
- $dialog.modal({
- closable: false,
- onApprove: async () => {
- if ($this.data('type') === 'form') {
- $($this.data('form')).trigger('submit');
- return;
+ // set the modal "display name" by `data-name`
+ const modalNameEl = modal.querySelector('.name');
+ if (modalNameEl) modalNameEl.textContent = btn.getAttribute('data-name');
+
+ // fill the modal elements with data-xxx attributes: `data-data-organization-name="..."` => `<span class="dataOrganizationName">...</span>`
+ for (const [key, value] of Object.entries(dataObj)) {
+ if (key.startsWith('data')) {
+ const textEl = modal.querySelector(`.${key}`);
+ if (textEl) textEl.textContent = value;
}
- const postData = new FormData();
- for (const [key, value] of Object.entries(dataArray)) {
- if (key && key.startsWith('data')) {
- postData.append(key.slice(4), value);
+ }
+
+ $(modal).modal({
+ closable: false,
+ onApprove: async () => {
+ // 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);
+ if (!form) throw new Error(`no form named ${formSelector} found`);
+ form.submit();
}
- if (key === 'id') {
- postData.append('id', value);
+
+ // prepare an AJAX form by data attributes
+ const postData = new FormData();
+ for (const [key, value] of Object.entries(dataObj)) {
+ if (key.startsWith('data')) { // for data-data-xxx (HTML) -> dataXxx (form)
+ postData.append(key.slice(4), value);
+ }
+ if (key === 'id') { // for data-id="..."
+ postData.append('id', value);
+ }
}
- }
- const response = await POST($this.data('url'), {data: postData});
- if (response.ok) {
- const data = await response.json();
- window.location.href = data.redirect;
- }
- },
- }).modal('show');
+ const response = await POST(btn.getAttribute('data-url'), {data: postData});
+ if (response.ok) {
+ const data = await response.json();
+ window.location.href = data.redirect;
+ }
+ },
+ }).modal('show');
+ });
}
-
- // Helpers.
- $('.delete-button').on('click', showDeletePopup);
}
function initGlobalShowModal() {
@@ -382,7 +395,7 @@ function initGlobalShowModal() {
} else if ($attrTarget[0].matches('input, textarea')) {
$attrTarget.val(attrib.value); // FIXME: add more supports like checkbox
} else {
- $attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
+ $attrTarget[0].textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
}
}
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index d1b139ffde..2d28b4b526 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -79,20 +79,20 @@ export function initImageDiff() {
path: this.getAttribute('data-path-after'),
mime: this.getAttribute('data-mime-after'),
$images: $container.find('img.image-after'), // matches 3 <img>
- $boundsInfo: $container.find('.bounds-info-after'),
+ boundsInfo: this.querySelector('.bounds-info-after'),
}, {
path: this.getAttribute('data-path-before'),
mime: this.getAttribute('data-mime-before'),
$images: $container.find('img.image-before'), // matches 3 <img>
- $boundsInfo: $container.find('.bounds-info-before'),
+ boundsInfo: this.querySelector('.bounds-info-before'),
}];
await Promise.all(imageInfos.map(async (info) => {
const [success] = await Promise.all(Array.from(info.$images, (img) => {
return loadElem(img, info.path);
}));
- // only the first images is associated with $boundsInfo
- if (!success) info.$boundsInfo.text('(image error)');
+ // only the first images is associated with boundsInfo
+ if (!success && info.boundsInfo) info.boundsInfo.textContent = '(image error)';
if (info.mime === 'image/svg+xml') {
const resp = await GET(info.path);
const text = await resp.text();
@@ -102,7 +102,7 @@ export function initImageDiff() {
this.setAttribute('width', bounds.width);
this.setAttribute('height', bounds.height);
});
- hideElem(info.$boundsInfo);
+ hideElem(info.boundsInfo);
}
}
}));
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 8e5a1f83db..f045879dec 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -47,17 +47,13 @@ async function receiveUpdateCount(event) {
}
export function initNotificationCount() {
- const $notificationCount = $('.notification_count');
-
- if (!$notificationCount.length) {
- return;
- }
+ if (!document.querySelector('.notification_count')) return;
let usingPeriodicPoller = false;
const startPeriodicPoller = (timeout, lastCount) => {
if (timeout <= 0 || !Number.isFinite(timeout)) return;
usingPeriodicPoller = true;
- lastCount = lastCount ?? $notificationCount.text();
+ lastCount = lastCount ?? getCurrentCount();
setTimeout(async () => {
await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
}, timeout);
@@ -121,8 +117,12 @@ export function initNotificationCount() {
startPeriodicPoller(notificationSettings.MinTimeout);
}
+function getCurrentCount() {
+ return document.querySelector('.notification_count').textContent;
+}
+
async function updateNotificationCountWithCallback(callback, timeout, lastCount) {
- const currentCount = $('.notification_count').text();
+ const currentCount = getCurrentCount();
if (lastCount !== currentCount) {
callback(notificationSettings.MinTimeout, currentCount);
return;
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index a5232cb4b6..b4fae4f6aa 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import {htmlEscape} from 'escape-goat';
import {createCodeEditor} from './codeeditor.js';
-import {hideElem, showElem} from '../utils/dom.js';
+import {hideElem, queryElems, showElem} from '../utils/dom.js';
import {initMarkupContent} from '../markup/content.js';
import {attachRefIssueContextPopup} from './contextpopup.js';
import {POST} from '../modules/fetch.js';
@@ -40,98 +40,75 @@ function initEditPreviewTab($form) {
}
}
-function initEditorForm() {
- const $form = $('.repository .edit.form');
- if (!$form) return;
- initEditPreviewTab($form);
-}
-
-function getCursorPosition($e) {
- const el = $e.get(0);
- let pos = 0;
- if ('selectionStart' in el) {
- pos = el.selectionStart;
- } else if ('selection' in document) {
- el.focus();
- const Sel = document.selection.createRange();
- const SelLength = document.selection.createRange().text.length;
- Sel.moveStart('character', -el.value.length);
- pos = Sel.text.length - SelLength;
- }
- return pos;
-}
-
export function initRepoEditor() {
- initEditorForm();
-
- $('.js-quick-pull-choice-option').on('change', function () {
- if ($(this).val() === 'commit-to-new-branch') {
- showElem('.quick-pull-branch-name');
- document.querySelector('.quick-pull-branch-name input').required = true;
- } else {
- hideElem('.quick-pull-branch-name');
- document.querySelector('.quick-pull-branch-name input').required = false;
- }
- $('#commit-button').text(this.getAttribute('button_text'));
- });
+ const $editArea = $('.repository.editor textarea#edit_area');
+ if (!$editArea.length) return;
- const joinTreePath = ($fileNameEl) => {
- const parts = [];
- $('.breadcrumb span.section').each(function () {
- const $element = $(this);
- if ($element.find('a').length) {
- parts.push($element.find('a').text());
+ for (const el of queryElems('.js-quick-pull-choice-option')) {
+ el.addEventListener('input', () => {
+ if (el.value === 'commit-to-new-branch') {
+ showElem('.quick-pull-branch-name');
+ document.querySelector('.quick-pull-branch-name input').required = true;
} else {
- parts.push($element.text());
+ hideElem('.quick-pull-branch-name');
+ document.querySelector('.quick-pull-branch-name input').required = false;
}
+ document.querySelector('#commit-button').textContent = el.getAttribute('data-button-text');
});
- if ($fileNameEl.val()) parts.push($fileNameEl.val());
- $('#tree_path').val(parts.join('/'));
- };
-
- const $editFilename = $('#file-name');
- $editFilename.on('input', function () {
- const parts = $(this).val().split('/');
+ }
+ const filenameInput = document.querySelector('#file-name');
+ function joinTreePath() {
+ const parts = [];
+ for (const el of document.querySelectorAll('.breadcrumb span.section')) {
+ const link = el.querySelector('a');
+ parts.push(link ? link.textContent : el.textContent);
+ }
+ if (filenameInput.value) {
+ parts.push(filenameInput.value);
+ }
+ document.querySelector('#tree_path').value = parts.join('/');
+ }
+ filenameInput.addEventListener('input', function () {
+ const parts = filenameInput.value.split('/');
if (parts.length > 1) {
for (let i = 0; i < parts.length; ++i) {
const value = parts[i];
if (i < parts.length - 1) {
if (value.length) {
- $(`<span class="section"><a href="#">${htmlEscape(value)}</a></span>`).insertBefore($(this));
- $('<div class="breadcrumb-divider">/</div>').insertBefore($(this));
+ $(`<span class="section"><a href="#">${htmlEscape(value)}</a></span>`).insertBefore($(filenameInput));
+ $('<div class="breadcrumb-divider">/</div>').insertBefore($(filenameInput));
}
} else {
- $(this).val(value);
+ filenameInput.value = value;
}
this.setSelectionRange(0, 0);
}
}
-
- joinTreePath($(this));
+ joinTreePath();
});
-
- $editFilename.on('keydown', function (e) {
- const $section = $('.breadcrumb span.section');
-
+ filenameInput.addEventListener('keydown', function (e) {
+ const sections = queryElems('.breadcrumb span.section');
+ const dividers = queryElems('.breadcrumb .breadcrumb-divider');
// Jump back to last directory once the filename is empty
- if (e.code === 'Backspace' && getCursorPosition($(this)) === 0 && $section.length > 0) {
+ if (e.code === 'Backspace' && filenameInput.selectionStart === 0 && sections.length > 0) {
e.preventDefault();
- const $divider = $('.breadcrumb .breadcrumb-divider');
- const value = $section.last().find('a').text();
- $(this).val(value + $(this).val());
+ const lastSection = sections[sections.length - 1];
+ const lastDivider = dividers.length ? dividers[dividers.length - 1] : null;
+ const value = lastSection.querySelector('a').textContent;
+ filenameInput.value = value + filenameInput.value;
this.setSelectionRange(value.length, value.length);
- $section.last().remove();
- $divider.last().remove();
- joinTreePath($(this));
+ lastDivider?.remove();
+ lastSection.remove();
+ joinTreePath();
}
});
- const $editArea = $('.repository.editor textarea#edit_area');
- if (!$editArea.length) return;
+ const $form = $('.repository.editor .edit.form');
+ initEditPreviewTab($form);
(async () => {
- const editor = await createCodeEditor($editArea[0], $editFilename[0]);
+ const editor = await createCodeEditor($editArea[0], filenameInput);
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button
diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js
index 9a8d737e01..29b96f5127 100644
--- a/web_src/js/features/repo-issue-edit.js
+++ b/web_src/js/features/repo-issue-edit.js
@@ -189,11 +189,12 @@ export function initRepoIssueCommentEdit() {
// Quote reply
$(document).on('click', '.quote-reply', async function (event) {
event.preventDefault();
- const target = $(this).data('target');
- const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
+ const target = this.getAttribute('data-target');
+ const quote = document.querySelector(`#${target}`).textContent.replace(/\n/g, '\n> ');
const content = `> ${quote}\n\n`;
+
let editor;
- if ($(this).hasClass('quote-reply-diff')) {
+ if (this.classList.contains('quote-reply-diff')) {
const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
editor = await handleReply($replyBtn);
} else {
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 95910e34bc..3cbbdc41fc 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -278,11 +278,12 @@ export function initRepoPullRequestUpdate() {
$('.update-button > .dropdown').dropdown({
onChange(_text, _value, $choice) {
- const url = $choice[0].getAttribute('data-do');
+ const choiceEl = $choice[0];
+ const url = choiceEl.getAttribute('data-do');
if (url) {
const buttonText = pullUpdateButton.querySelector('.button-text');
if (buttonText) {
- buttonText.textContent = $choice.text();
+ buttonText.textContent = choiceEl.textContent;
}
pullUpdateButton.setAttribute('data-do', url);
}
@@ -567,14 +568,15 @@ export function initRepoPullRequestReview() {
export function initRepoIssueReferenceIssue() {
// Reference issue
$(document).on('click', '.reference-issue', function (event) {
- const $this = $(this);
- const content = $(`#${$this.data('target')}`).text();
- const poster = $this.data('poster-username');
- const reference = toAbsoluteUrl($this.data('reference'));
- const $modal = $($this.data('modal'));
- $modal.find('textarea[name="content"]').val(`${content}\n\n_Originally posted by @${poster} in ${reference}_`);
- $modal.modal('show');
-
+ const target = this.getAttribute('data-target');
+ const content = document.querySelector(`#${target}`)?.textContent ?? '';
+ const poster = this.getAttribute('data-poster-username');
+ const reference = toAbsoluteUrl(this.getAttribute('data-reference'));
+ const modalSelector = this.getAttribute('data-modal');
+ const modal = document.querySelector(modalSelector);
+ const textarea = modal.querySelector('textarea[name="content"]');
+ textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
+ $(modal).modal('show');
event.preventDefault();
});
}
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 2323d818c2..e53d86cca0 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -272,9 +272,9 @@ export function initRepoCommentForm() {
}
$list.find('.selected').html(`
- <a class="item muted sidebar-item-link" href=${$(this).data('href')}>
+ <a class="item muted sidebar-item-link" href=${htmlEscape(this.getAttribute('href'))}>
${icon}
- ${htmlEscape($(this).text())}
+ ${htmlEscape(this.textContent)}
</a>
`);
diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 52c5de2bfa..652f8ac290 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -1,47 +1,46 @@
import $ from 'jquery';
import {minimatch} from 'minimatch';
import {createMonaco} from './codeeditor.js';
-import {onInputDebounce, toggleElem} from '../utils/dom.js';
+import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.js';
import {POST} from '../modules/fetch.js';
const {appSubUrl, csrfToken} = window.config;
export function initRepoSettingsCollaboration() {
// Change collaborator access mode
- $('.page-content.repository .ui.dropdown.access-mode').each((_, el) => {
- const $dropdown = $(el);
- const $text = $dropdown.find('> .text');
- $dropdown.dropdown({
- async action(_text, value) {
- const lastValue = el.getAttribute('data-last-value');
+ for (const dropdownEl of queryElems('.page-content.repository .ui.dropdown.access-mode')) {
+ const textEl = dropdownEl.querySelector(':scope > .text');
+ $(dropdownEl).dropdown({
+ async action(text, value) {
+ dropdownEl.classList.add('is-loading', 'loading-icon-2px');
+ const lastValue = dropdownEl.getAttribute('data-last-value');
+ $(dropdownEl).dropdown('hide');
try {
- el.setAttribute('data-last-value', value);
- $dropdown.dropdown('hide');
- const data = new FormData();
- data.append('uid', el.getAttribute('data-uid'));
- data.append('mode', value);
- await POST(el.getAttribute('data-url'), {data});
+ const uid = dropdownEl.getAttribute('data-uid');
+ await POST(dropdownEl.getAttribute('data-url'), {data: new URLSearchParams({uid, 'mode': value})});
+ textEl.textContent = text;
+ dropdownEl.setAttribute('data-last-value', value);
} catch {
- $text.text('(error)'); // prevent from misleading users when error occurs
- el.setAttribute('data-last-value', lastValue);
+ textEl.textContent = '(error)'; // prevent from misleading users when error occurs
+ dropdownEl.setAttribute('data-last-value', lastValue);
+ } finally {
+ dropdownEl.classList.remove('is-loading');
}
},
- onChange(_value, text, _$choice) {
- $text.text(text); // update the text when using keyboard navigating
- },
onHide() {
- // set to the really selected value, defer to next tick to make sure `action` has finished its work because the calling order might be onHide -> action
+ // set to the really selected value, defer to next tick to make sure `action` has finished
+ // its work because the calling order might be onHide -> action
setTimeout(() => {
- const $item = $dropdown.dropdown('get item', el.getAttribute('data-last-value'));
+ const $item = $(dropdownEl).dropdown('get item', dropdownEl.getAttribute('data-last-value'));
if ($item) {
- $dropdown.dropdown('set selected', el.getAttribute('data-last-value'));
+ $(dropdownEl).dropdown('set selected', dropdownEl.getAttribute('data-last-value'));
} else {
- $text.text('(none)'); // prevent from misleading users when the access mode is undefined
+ textEl.textContent = '(none)'; // prevent from misleading users when the access mode is undefined
}
}, 0);
},
});
- });
+ }
}
export function initRepoSettingSearchTeamBox() {