code.gitea.io/gitea@v1.22.3/web_src/js/features/repo-home.js (about) 1 import $ from 'jquery'; 2 import {stripTags} from '../utils.js'; 3 import {hideElem, queryElemChildren, showElem} from '../utils/dom.js'; 4 import {POST} from '../modules/fetch.js'; 5 import {showErrorToast} from '../modules/toast.js'; 6 7 const {appSubUrl} = window.config; 8 9 export function initRepoTopicBar() { 10 const mgrBtn = document.getElementById('manage_topic'); 11 if (!mgrBtn) return; 12 13 const editDiv = document.getElementById('topic_edit'); 14 const viewDiv = document.getElementById('repo-topics'); 15 const topicDropdown = editDiv.querySelector('.ui.dropdown'); 16 let lastErrorToast; 17 18 mgrBtn.addEventListener('click', () => { 19 hideElem(viewDiv); 20 showElem(editDiv); 21 topicDropdown.querySelector('input.search').focus(); 22 }); 23 24 document.querySelector('#cancel_topic_edit').addEventListener('click', () => { 25 lastErrorToast?.hideToast(); 26 hideElem(editDiv); 27 showElem(viewDiv); 28 mgrBtn.focus(); 29 }); 30 31 document.getElementById('save_topic').addEventListener('click', async (e) => { 32 lastErrorToast?.hideToast(); 33 const topics = editDiv.querySelector('input[name=topics]').value; 34 35 const data = new FormData(); 36 data.append('topics', topics); 37 38 const response = await POST(e.target.getAttribute('data-link'), {data}); 39 40 if (response.ok) { 41 const responseData = await response.json(); 42 if (responseData.status === 'ok') { 43 queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove()); 44 if (topics.length) { 45 const topicArray = topics.split(','); 46 topicArray.sort(); 47 for (const topic of topicArray) { 48 // it should match the code in repo/home.tmpl 49 const link = document.createElement('a'); 50 link.classList.add('repo-topic', 'ui', 'large', 'label'); 51 link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`; 52 link.textContent = topic; 53 mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button 54 } 55 } 56 hideElem(editDiv); 57 showElem(viewDiv); 58 } 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" 61 const responseData = await response.json(); 62 lastErrorToast = showErrorToast(responseData.message, {duration: 5000}); 63 if (responseData.invalidTopics && responseData.invalidTopics.length > 0) { 64 const {invalidTopics} = responseData; 65 const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label'); 66 for (const [index, value] of topics.split(',').entries()) { 67 if (invalidTopics.includes(value)) { 68 topicLabels[index].classList.remove('green'); 69 topicLabels[index].classList.add('red'); 70 } 71 } 72 } 73 } 74 }); 75 76 $(topicDropdown).dropdown({ 77 allowAdditions: true, 78 forceSelection: false, 79 fullTextSearch: 'exact', 80 fields: {name: 'description', value: 'data-value'}, 81 saveRemoteData: false, 82 label: { 83 transition: 'horizontal flip', 84 duration: 200, 85 variation: false, 86 }, 87 apiSettings: { 88 url: `${appSubUrl}/explore/topics/search?q={query}`, 89 throttle: 500, 90 cache: false, 91 onResponse(res) { 92 const formattedResponse = { 93 success: false, 94 results: [], 95 }; 96 const query = stripTags(this.urlData.query.trim()); 97 let found_query = false; 98 const current_topics = []; 99 for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) { 100 current_topics.push(el.getAttribute('data-value')); 101 } 102 103 if (res.topics) { 104 let found = false; 105 for (let i = 0; i < res.topics.length; i++) { 106 // skip currently added tags 107 if (current_topics.includes(res.topics[i].topic_name)) { 108 continue; 109 } 110 111 if (res.topics[i].topic_name.toLowerCase() === query.toLowerCase()) { 112 found_query = true; 113 } 114 formattedResponse.results.push({description: res.topics[i].topic_name, 'data-value': res.topics[i].topic_name}); 115 found = true; 116 } 117 formattedResponse.success = found; 118 } 119 120 if (query.length > 0 && !found_query) { 121 formattedResponse.success = true; 122 formattedResponse.results.unshift({description: query, 'data-value': query}); 123 } else if (query.length > 0 && found_query) { 124 formattedResponse.results.sort((a, b) => { 125 if (a.description.toLowerCase() === query.toLowerCase()) return -1; 126 if (b.description.toLowerCase() === query.toLowerCase()) return 1; 127 if (a.description > b.description) return -1; 128 if (a.description < b.description) return 1; 129 return 0; 130 }); 131 } 132 133 return formattedResponse; 134 }, 135 }, 136 onLabelCreate(value) { 137 value = value.toLowerCase().trim(); 138 this.attr('data-value', value).contents().first().replaceWith(value); 139 return $(this); 140 }, 141 onAdd(addedValue, _addedText, $addedChoice) { 142 addedValue = addedValue.toLowerCase().trim(); 143 $addedChoice[0].setAttribute('data-value', addedValue); 144 $addedChoice[0].setAttribute('data-text', addedValue); 145 }, 146 }); 147 }