Skip to content

Commit 0497b26

Browse files
yardenshohamsilverwindwxiaoguang
authored
Remove most jQuery function calls from the repository topic box (#30191)
Remove most jQuery function calls --------- Signed-off-by: Yarden Shoham <[email protected]> Co-authored-by: silverwind <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 8da9130 commit 0497b26

File tree

5 files changed

+54
-75
lines changed

5 files changed

+54
-75
lines changed

templates/repo/home.tmpl

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,21 @@
1818
</div>
1919
</form>
2020
</div>
21-
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics">
22-
{{range .Topics}}<a class="ui repo-topic large label topic tw-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
21+
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
22+
{{/* it should match the code in issue-home.js */}}
23+
{{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
2324
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
2425
</div>
2526
{{end}}
2627
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
27-
<div class="ui form tw-hidden tw-flex tw-flex-col tw-mt-4" id="topic_edit">
28-
<div class="field tw-flex-1 tw-mb-1">
29-
<div class="ui fluid multiple search selection dropdown tw-flex-wrap" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
30-
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
31-
{{range .Topics}}
32-
{{/* keey the same layout as Fomantic UI generated labels */}}
33-
<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
34-
{{end}}
35-
<div class="text"></div>
36-
</div>
28+
<div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit">
29+
<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
30+
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
31+
{{range .Topics}}
32+
{{/* keep the same layout as Fomantic UI generated labels */}}
33+
<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
34+
{{end}}
35+
<div class="text"></div>
3736
</div>
3837
<div>
3938
<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>

web_src/css/repo.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2437,6 +2437,7 @@ tbody.commit-list {
24372437
#repo-topics .repo-topic {
24382438
font-weight: var(--font-weight-normal);
24392439
cursor: pointer;
2440+
margin: 0;
24402441
}
24412442

24422443
#new-dependency-drop-list.ui.selection.dropdown {

web_src/js/features/repo-home.js

Lines changed: 25 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,53 @@
11
import $ from 'jquery';
22
import {stripTags} from '../utils.js';
3-
import {hideElem, showElem} from '../utils/dom.js';
3+
import {hideElem, queryElemChildren, showElem} from '../utils/dom.js';
44
import {POST} from '../modules/fetch.js';
5+
import {showErrorToast} from '../modules/toast.js';
56

67
const {appSubUrl} = window.config;
78

89
export function initRepoTopicBar() {
910
const mgrBtn = document.getElementById('manage_topic');
1011
if (!mgrBtn) return;
12+
1113
const editDiv = document.getElementById('topic_edit');
1214
const viewDiv = document.getElementById('repo-topics');
13-
const saveBtn = document.getElementById('save_topic');
14-
const topicDropdown = editDiv.querySelector('.dropdown');
15-
const $topicDropdown = $(topicDropdown);
16-
const $topicForm = $(editDiv);
17-
const $topicDropdownSearch = $topicDropdown.find('input.search');
18-
const topicPrompts = {
19-
countPrompt: topicDropdown.getAttribute('data-text-count-prompt') ?? undefined,
20-
formatPrompt: topicDropdown.getAttribute('data-text-format-prompt') ?? undefined,
21-
};
15+
const topicDropdown = editDiv.querySelector('.ui.dropdown');
16+
let lastErrorToast;
2217

2318
mgrBtn.addEventListener('click', () => {
2419
hideElem(viewDiv);
2520
showElem(editDiv);
26-
$topicDropdownSearch.trigger('focus');
21+
topicDropdown.querySelector('input.search').focus();
2722
});
2823

29-
$('#cancel_topic_edit').on('click', () => {
24+
document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
25+
lastErrorToast?.hideToast();
3026
hideElem(editDiv);
3127
showElem(viewDiv);
3228
mgrBtn.focus();
3329
});
3430

35-
saveBtn.addEventListener('click', async () => {
36-
const topics = $('input[name=topics]').val();
31+
document.getElementById('save_topic').addEventListener('click', async (e) => {
32+
lastErrorToast?.hideToast();
33+
const topics = editDiv.querySelector('input[name=topics]').value;
3734

3835
const data = new FormData();
3936
data.append('topics', topics);
4037

41-
const response = await POST(saveBtn.getAttribute('data-link'), {data});
38+
const response = await POST(e.target.getAttribute('data-link'), {data});
4239

4340
if (response.ok) {
4441
const responseData = await response.json();
4542
if (responseData.status === 'ok') {
46-
$(viewDiv).children('.topic').remove();
43+
queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove());
4744
if (topics.length) {
4845
const topicArray = topics.split(',');
4946
topicArray.sort();
5047
for (const topic of topicArray) {
48+
// it should match the code in repo/home.tmpl
5149
const link = document.createElement('a');
52-
link.classList.add('ui', 'repo-topic', 'large', 'label', 'topic', 'tw-m-0');
50+
link.classList.add('repo-topic', 'ui', 'large', 'label');
5351
link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
5452
link.textContent = topic;
5553
mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button
@@ -59,27 +57,23 @@ export function initRepoTopicBar() {
5957
showElem(viewDiv);
6058
}
6159
} else if (response.status === 422) {
60+
// how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save"
6261
const responseData = await response.json();
62+
lastErrorToast = showErrorToast(responseData.message, {duration: 5000});
6363
if (responseData.invalidTopics.length > 0) {
64-
topicPrompts.formatPrompt = responseData.message;
65-
6664
const {invalidTopics} = responseData;
67-
const $topicLabels = $topicDropdown.children('a.ui.label');
65+
const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label');
6866
for (const [index, value] of topics.split(',').entries()) {
6967
if (invalidTopics.includes(value)) {
70-
$topicLabels.eq(index).removeClass('green').addClass('red');
68+
topicLabels[index].classList.remove('green');
69+
topicLabels[index].classList.add('red');
7170
}
7271
}
73-
} else {
74-
topicPrompts.countPrompt = responseData.message;
7572
}
7673
}
77-
78-
// Always validate the form
79-
$topicForm.form('validate form');
8074
});
8175

82-
$topicDropdown.dropdown({
76+
$(topicDropdown).dropdown({
8377
allowAdditions: true,
8478
forceSelection: false,
8579
fullTextSearch: 'exact',
@@ -102,9 +96,9 @@ export function initRepoTopicBar() {
10296
const query = stripTags(this.urlData.query.trim());
10397
let found_query = false;
10498
const current_topics = [];
105-
$topicDropdown.find('a.label.visible').each((_, el) => {
99+
for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) {
106100
current_topics.push(el.getAttribute('data-value'));
107-
});
101+
}
108102

109103
if (res.topics) {
110104
let found = false;
@@ -146,38 +140,8 @@ export function initRepoTopicBar() {
146140
},
147141
onAdd(addedValue, _addedText, $addedChoice) {
148142
addedValue = addedValue.toLowerCase().trim();
149-
$($addedChoice)[0].setAttribute('data-value', addedValue);
150-
$($addedChoice)[0].setAttribute('data-text', addedValue);
151-
},
152-
});
153-
154-
$.fn.form.settings.rules.validateTopic = function (_values, regExp) {
155-
const $topics = $topicDropdown.children('a.ui.label');
156-
const status = !$topics.length || $topics.last()[0].getAttribute('data-value').match(regExp);
157-
if (!status) {
158-
$topics.last().removeClass('green').addClass('red');
159-
}
160-
return status && !$topicDropdown.children('a.ui.label.red').length;
161-
};
162-
163-
$topicForm.form({
164-
on: 'change',
165-
inline: true,
166-
fields: {
167-
topics: {
168-
identifier: 'topics',
169-
rules: [
170-
{
171-
type: 'validateTopic',
172-
value: /^\s*[a-z0-9][-.a-z0-9]{0,35}\s*$/,
173-
prompt: topicPrompts.formatPrompt,
174-
},
175-
{
176-
type: 'maxCount[25]',
177-
prompt: topicPrompts.countPrompt,
178-
},
179-
],
180-
},
143+
$addedChoice[0].setAttribute('data-value', addedValue);
144+
$addedChoice[0].setAttribute('data-text', addedValue);
181145
},
182146
});
183147
}

web_src/js/modules/toast.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, ..
3939

4040
toast.showToast();
4141
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
42+
return toast;
4243
}
4344

4445
export function showInfoToast(message, opts) {

web_src/js/utils/dom.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,22 @@ export function isElemHidden(el) {
5151
return res[0];
5252
}
5353

54-
export function queryElemSiblings(el, selector = '*') {
55-
return Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector));
54+
function applyElemsCallback(elems, fn) {
55+
if (fn) {
56+
for (const el of elems) {
57+
fn(el);
58+
}
59+
}
60+
return elems;
61+
}
62+
63+
export function queryElemSiblings(el, selector = '*', fn) {
64+
return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn);
65+
}
66+
67+
// it works like jQuery.children: only the direct children are selected
68+
export function queryElemChildren(parent, selector = '*', fn) {
69+
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
5670
}
5771

5872
export function onDomReady(cb) {

0 commit comments

Comments
 (0)