loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove most jQuery function calls from the repository topic box (#30191)

Remove most jQuery function calls

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
(cherry picked from commit 0497b2607d1052e771af4017c2c4180adb7d86b2)

authored by

Yarden Shoham
silverwind
wxiaoguang
and committed by
Gergely Nagy
19832265 4be57708

+54 -75
+11 -12
templates/repo/home.tmpl
··· 18 18 </div> 19 19 </form> 20 20 </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}} 23 24 {{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}} 24 25 </div> 25 26 {{end}} 26 27 {{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> 37 36 </div> 38 37 <div> 39 38 <button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
+1
web_src/css/repo.css
··· 2475 2475 #repo-topics .repo-topic { 2476 2476 font-weight: var(--font-weight-normal); 2477 2477 cursor: pointer; 2478 + margin: 0; 2478 2479 } 2479 2480 2480 2481 #new-dependency-drop-list.ui.selection.dropdown {
+25 -61
web_src/js/features/repo-home.js
··· 1 1 import $ from 'jquery'; 2 2 import {stripTags} from '../utils.js'; 3 - import {hideElem, showElem} from '../utils/dom.js'; 3 + import {hideElem, queryElemChildren, showElem} from '../utils/dom.js'; 4 4 import {POST} from '../modules/fetch.js'; 5 + import {showErrorToast} from '../modules/toast.js'; 5 6 6 7 const {appSubUrl} = window.config; 7 8 8 9 export function initRepoTopicBar() { 9 10 const mgrBtn = document.getElementById('manage_topic'); 10 11 if (!mgrBtn) return; 12 + 11 13 const editDiv = document.getElementById('topic_edit'); 12 14 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; 22 17 23 18 mgrBtn.addEventListener('click', () => { 24 19 hideElem(viewDiv); 25 20 showElem(editDiv); 26 - $topicDropdownSearch.trigger('focus'); 21 + topicDropdown.querySelector('input.search').focus(); 27 22 }); 28 23 29 - $('#cancel_topic_edit').on('click', () => { 24 + document.querySelector('#cancel_topic_edit').addEventListener('click', () => { 25 + lastErrorToast?.hideToast(); 30 26 hideElem(editDiv); 31 27 showElem(viewDiv); 32 28 mgrBtn.focus(); 33 29 }); 34 30 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; 37 34 38 35 const data = new FormData(); 39 36 data.append('topics', topics); 40 37 41 - const response = await POST(saveBtn.getAttribute('data-link'), {data}); 38 + const response = await POST(e.target.getAttribute('data-link'), {data}); 42 39 43 40 if (response.ok) { 44 41 const responseData = await response.json(); 45 42 if (responseData.status === 'ok') { 46 - $(viewDiv).children('.topic').remove(); 43 + queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove()); 47 44 if (topics.length) { 48 45 const topicArray = topics.split(','); 49 46 topicArray.sort(); 50 47 for (const topic of topicArray) { 48 + // it should match the code in repo/home.tmpl 51 49 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'); 53 51 link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`; 54 52 link.textContent = topic; 55 53 mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button ··· 59 57 showElem(viewDiv); 60 58 } 61 59 } else if (response.status === 422) { 60 + // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save" 62 61 const responseData = await response.json(); 62 + lastErrorToast = showErrorToast(responseData.message, {duration: 5000}); 63 63 if (responseData.invalidTopics.length > 0) { 64 - topicPrompts.formatPrompt = responseData.message; 65 - 66 64 const {invalidTopics} = responseData; 67 - const $topicLabels = $topicDropdown.children('a.ui.label'); 65 + const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label'); 68 66 for (const [index, value] of topics.split(',').entries()) { 69 67 if (invalidTopics.includes(value)) { 70 - $topicLabels.eq(index).removeClass('green').addClass('red'); 68 + topicLabels[index].classList.remove('green'); 69 + topicLabels[index].classList.add('red'); 71 70 } 72 71 } 73 - } else { 74 - topicPrompts.countPrompt = responseData.message; 75 72 } 76 73 } 77 - 78 - // Always validate the form 79 - $topicForm.form('validate form'); 80 74 }); 81 75 82 - $topicDropdown.dropdown({ 76 + $(topicDropdown).dropdown({ 83 77 allowAdditions: true, 84 78 forceSelection: false, 85 79 fullTextSearch: 'exact', ··· 102 96 const query = stripTags(this.urlData.query.trim()); 103 97 let found_query = false; 104 98 const current_topics = []; 105 - $topicDropdown.find('a.label.visible').each((_, el) => { 99 + for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) { 106 100 current_topics.push(el.getAttribute('data-value')); 107 - }); 101 + } 108 102 109 103 if (res.topics) { 110 104 let found = false; ··· 146 140 }, 147 141 onAdd(addedValue, _addedText, $addedChoice) { 148 142 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); 181 145 }, 182 146 }); 183 147 }
+1
web_src/js/modules/toast.js
··· 39 39 40 40 toast.showToast(); 41 41 toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); 42 + return toast; 42 43 } 43 44 44 45 export function showInfoToast(message, opts) {
+16 -2
web_src/js/utils/dom.js
··· 51 51 return res[0]; 52 52 } 53 53 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); 56 70 } 57 71 58 72 export function onDomReady(cb) {