From a2304cb163ce5e097078e71f49d4d5cb4c8b20d9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 10 Jun 2024 12:12:31 +0200 Subject: 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 --- web_src/js/features/common-global.js | 93 +++++++++++++++------------ web_src/js/features/imagediff.js | 10 +-- web_src/js/features/notification.js | 14 ++--- web_src/js/features/repo-editor.js | 111 +++++++++++++-------------------- web_src/js/features/repo-issue-edit.js | 7 ++- web_src/js/features/repo-issue.js | 22 ++++--- web_src/js/features/repo-legacy.js | 4 +- web_src/js/features/repo-settings.js | 45 +++++++------ 8 files changed, 149 insertions(+), 157 deletions(-) (limited to 'web_src/js/features') 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="..."` => `...` + 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 - $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 - $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) { - $(`${htmlEscape(value)}`).insertBefore($(this)); - $('').insertBefore($(this)); + $(`${htmlEscape(value)}`).insertBefore($(filenameInput)); + $('').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(` - + ${icon} - ${htmlEscape($(this).text())} + ${htmlEscape(this.textContent)} `); 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() { -- cgit v1.2.3