Skip to content

Commit c050826

Browse files
authored
Refactor frontend unique id & comment (#34958)
* there is no bug of the "unique element id", but duplicate code, this PR just merges the duplicate "element id" logic and move the function from "fomaintic" to "dom" * improve comments * make "git commit graph" page update pagination links correctly
1 parent 6033c67 commit c050826

File tree

7 files changed

+30
-35
lines changed

7 files changed

+30
-35
lines changed

web_src/js/components/DiffCommitSelector.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import {defineComponent} from 'vue';
33
import {SvgIcon} from '../svg.ts';
44
import {GET} from '../modules/fetch.ts';
5-
import {generateAriaId} from '../modules/fomantic/base.ts';
5+
import {generateElemId} from '../utils/dom.ts';
66
77
type Commit = {
88
id: string,
@@ -35,8 +35,8 @@ export default defineComponent({
3535
commits: [] as Array<Commit>,
3636
hoverActivated: false,
3737
lastReviewCommitSha: '',
38-
uniqueIdMenu: generateAriaId(),
39-
uniqueIdShowAll: generateAriaId(),
38+
uniqueIdMenu: generateElemId('diff-commit-selector-menu-'),
39+
uniqueIdShowAll: generateElemId('diff-commit-selector-show-all-'),
4040
};
4141
},
4242
computed: {

web_src/js/features/comp/ComboMarkdownEditor.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import '@github/markdown-toolbar-element';
22
import '@github/text-expander-element';
33
import {attachTribute} from '../tribute.ts';
4-
import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts';
4+
import {hideElem, showElem, autosize, isElemVisible, generateElemId} from '../../utils/dom.ts';
55
import {
66
EventUploadStateChanged,
77
initEasyMDEPaste,
@@ -25,8 +25,6 @@ import {createTippy} from '../../modules/tippy.ts';
2525
import {fomanticQuery} from '../../modules/fomantic/base.ts';
2626
import type EasyMDE from 'easymde';
2727

28-
let elementIdCounter = 0;
29-
3028
/**
3129
* validate if the given textarea is non-empty.
3230
* @param {HTMLTextAreaElement} textarea - The textarea element to be validated.
@@ -125,7 +123,7 @@ export class ComboMarkdownEditor {
125123
setupTextarea() {
126124
this.textarea = this.container.querySelector('.markdown-text-editor');
127125
this.textarea._giteaComboMarkdownEditor = this;
128-
this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`;
126+
this.textarea.id = generateElemId(`_combo_markdown_editor_`);
129127
this.textarea.addEventListener('input', () => triggerEditorContentChanged(this.container));
130128
this.applyEditorHeights(this.textarea, this.options.editorHeights);
131129

@@ -213,16 +211,16 @@ export class ComboMarkdownEditor {
213211

214212
// Fomantic Tab requires the "data-tab" to be globally unique.
215213
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
214+
const tabIdSuffix = generateElemId();
216215
this.tabEditor = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-writer');
217216
this.tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer');
218-
this.tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
219-
this.tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
217+
this.tabEditor.setAttribute('data-tab', `markdown-writer-${tabIdSuffix}`);
218+
this.tabPreviewer.setAttribute('data-tab', `markdown-previewer-${tabIdSuffix}`);
220219

221220
const panelEditor = this.container.querySelector('.ui.tab[data-tab-panel="markdown-writer"]');
222221
const panelPreviewer = this.container.querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
223-
panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
224-
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
225-
elementIdCounter++;
222+
panelEditor.setAttribute('data-tab', `markdown-writer-${tabIdSuffix}`);
223+
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${tabIdSuffix}`);
226224

227225
this.tabEditor.addEventListener('click', () => {
228226
requestAnimationFrame(() => {

web_src/js/features/repo-graph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function initRepoGraphGit() {
1818
const params = new URLSearchParams(window.location.search);
1919
params.set('mode', mode);
2020
window.history.replaceState(null, '', `?${params.toString()}`);
21-
for (const link of document.querySelectorAll('#pagination .pagination a')) {
21+
for (const link of document.querySelectorAll('#git-graph-body .pagination a')) {
2222
const href = link.getAttribute('href');
2323
if (!href) continue;
2424
const url = new URL(href, window.location.href);

web_src/js/modules/fomantic/base.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import $ from 'jquery';
2-
let ariaIdCounter = 0;
3-
4-
export function generateAriaId() {
5-
return `_aria_auto_id_${ariaIdCounter++}`;
6-
}
2+
import {generateElemId} from '../../utils/dom.ts';
73

84
export function linkLabelAndInput(label: Element, input: Element) {
95
const labelFor = label.getAttribute('for');
@@ -12,7 +8,7 @@ export function linkLabelAndInput(label: Element, input: Element) {
128
if (inputId && !labelFor) { // missing "for"
139
label.setAttribute('for', inputId);
1410
} else if (!inputId && !labelFor) { // missing both "id" and "for"
15-
const id = generateAriaId();
11+
const id = generateElemId('_aria_label_input_');
1612
input.setAttribute('id', id);
1713
label.setAttribute('for', id);
1814
}

web_src/js/modules/fomantic/dropdown.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import $ from 'jquery';
2-
import {generateAriaId} from './base.ts';
32
import type {FomanticInitFunction} from '../../types.ts';
4-
import {queryElems} from '../../utils/dom.ts';
3+
import {generateElemId, queryElems} from '../../utils/dom.ts';
54

65
const ariaPatchKey = '_giteaAriaPatchDropdown';
76
const fomanticDropdownFn = $.fn.dropdown;
@@ -47,7 +46,7 @@ function ariaDropdownFn(this: any, ...args: Parameters<FomanticInitFunction>) {
4746
// make the item has role=option/menuitem, add an id if there wasn't one yet, make items as non-focusable
4847
// the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
4948
function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) {
50-
if (!item.id) item.id = generateAriaId();
49+
if (!item.id) item.id = generateElemId('_aria_dropdown_item_');
5150
item.setAttribute('role', (dropdown as any)[ariaPatchKey].listItemRole);
5251
item.setAttribute('tabindex', '-1');
5352
for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1');
@@ -59,7 +58,7 @@ function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) {
5958
function updateSelectionLabel(label: HTMLElement) {
6059
// the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
6160
if (!label.id) {
62-
label.id = generateAriaId();
61+
label.id = generateElemId('_aria_dropdown_label_');
6362
}
6463
label.tabIndex = -1;
6564

@@ -127,7 +126,7 @@ function delegateDropdownModule($dropdown: any) {
127126
function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, menu: HTMLElement) {
128127
// prepare static dropdown menu list popup
129128
if (!menu.id) {
130-
menu.id = generateAriaId();
129+
menu.id = generateElemId('_aria_dropdown_menu_');
131130
}
132131

133132
$(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item));

web_src/js/utils/dom.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,12 @@ export function isElemVisible(el: HTMLElement): boolean {
290290
export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) {
291291
const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
292292
const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
293-
let success = true;
293+
let success = false;
294294

295295
textarea.contentEditable = 'true';
296296
try {
297297
success = document.execCommand('insertText', false, text); // eslint-disable-line @typescript-eslint/no-deprecated
298-
} catch {
299-
success = false;
300-
}
298+
} catch {} // ignore the error if execCommand is not supported or failed
301299
textarea.contentEditable = 'false';
302300

303301
if (success && !textarea.value.slice(0, textarea.selectionStart ?? undefined).endsWith(text)) {
@@ -310,11 +308,10 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st
310308
}
311309
}
312310

313-
// Warning: Do not enter any unsanitized variables here
314311
export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T {
315312
htmlString = htmlString.trim();
316-
// some tags like "tr" are special, it must use a correct parent container to create
317-
// eslint-disable-next-line github/unescaped-html-literal -- FIXME: maybe we need to use other approaches to create elements from HTML, e.g. using DOMParser
313+
// There is no way to create some elements without a proper parent, jQuery's approach: https://github.com/jquery/jquery/blob/main/src/manipulation/wrapMap.js
314+
// eslint-disable-next-line github/unescaped-html-literal
318315
if (htmlString.startsWith('<tr')) {
319316
const container = document.createElement('table');
320317
container.innerHTML = htmlString;
@@ -364,14 +361,19 @@ export function addDelegatedEventListener<T extends HTMLElement, E extends Event
364361
const elem = (e.target as HTMLElement).closest(selector);
365362
// It strictly checks "parent contains the target elem" to avoid side effects of selector running on outside the parent.
366363
// Keep in mind that the elem could have been removed from parent by other event handlers before this event handler is called.
367-
// For example: tippy popup item, the tippy popup could be hidden and removed from DOM before this.
368-
// It is caller's responsibility make sure the elem is still in parent's DOM when this event handler is called.
364+
// For example, tippy popup item, the tippy popup could be hidden and removed from DOM before this.
365+
// It is the caller's responsibility to make sure the elem is still in parent's DOM when this event handler is called.
369366
if (!elem || (parent !== document && !parent.contains(elem))) return;
370367
listener(elem as T, e as E);
371368
}, options);
372369
}
373370

374-
/** Returns whether a click event is a left-click without any modifiers held */
371+
// Returns whether a click event is a left-click without any modifiers held
375372
export function isPlainClick(e: MouseEvent) {
376373
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
377374
}
375+
376+
let elemIdCounter = 0;
377+
export function generateElemId(prefix: string = ''): string {
378+
return `${prefix}${elemIdCounter++}`;
379+
}

web_src/js/utils/html.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function html(tmpl: TemplateStringsArray, ...parts: Array<any>): string {
1717
let output = tmpl[0];
1818
for (let i = 0; i < parts.length; i++) {
1919
const value = parts[i];
20-
const valueEscaped = (value instanceof rawObject) ? value.toString() : htmlEscape(String(parts[i]));
20+
const valueEscaped = (value instanceof rawObject) ? value.toString() : htmlEscape(String(value));
2121
output = output + valueEscaped + tmpl[i + 1];
2222
}
2323
return output;

0 commit comments

Comments
 (0)