Skip to content

Refactor language menu and dom utils #32450

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions templates/base/footer_content.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
{{end}}
</div>
<div class="right-links" role="group" aria-label="{{ctx.Locale.Tr "aria.footer.links"}}">
<div class="ui dropdown upward language">
<div class="ui dropdown upward">
<span class="flex-text-inline">{{svg "octicon-globe" 14}} {{ctx.Locale.LangName}}</span>
<div class="menu language-menu">
{{range .AllLangs}}
<a lang="{{.Lang}}" data-url="{{AppSubUrl}}/?lang={{.Lang}}" class="item {{if eq ctx.Locale.Lang .Lang}}active selected{{end}}">{{.Name}}</a>
{{end}}
{{range .AllLangs -}}
<a lang="{{.Lang}}" data-url="{{AppSubUrl}}/?lang={{.Lang}}" class="item {{if eq ctx.Locale.Lang .Lang}}selected{{end}}">{{.Name}}</a>
{{end -}}
</div>
</div>
<a href="{{AssetUrlPrefix}}/licenses.txt">{{ctx.Locale.Tr "licenses"}}</a>
Expand Down
2 changes: 1 addition & 1 deletion web_src/css/home.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
margin-left: 5px;
}

.page-footer .ui.dropdown.language .menu {
.page-footer .ui.dropdown .menu.language-menu {
max-height: min(500px, calc(100vh - 60px));
overflow-y: auto;
margin-bottom: 10px;
Expand Down
27 changes: 14 additions & 13 deletions web_src/js/features/common-page.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import $ from 'jquery';
import {GET} from '../modules/fetch.ts';
import {showGlobalErrorMessage} from '../bootstrap.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {queryElems} from '../utils/dom.ts';

const {appUrl} = window.config;

Expand All @@ -17,18 +18,18 @@ export function initHeadNavbarContentToggle() {
}

export function initFootLanguageMenu() {
async function linkLanguageAction() {
const $this = $(this);
await GET($this.data('url'));
document.querySelector('.ui.dropdown .menu.language-menu')?.addEventListener('click', async (e) => {
const item = (e.target as HTMLElement).closest('.item');
if (!item) return;
e.preventDefault();
await GET(item.getAttribute('data-url'));
window.location.reload();
}

$('.language-menu a[lang]').on('click', linkLanguageAction);
});
}

export function initGlobalDropdown() {
// Semantic UI modules.
const $uiDropdowns = $('.ui.dropdown');
const $uiDropdowns = fomanticQuery('.ui.dropdown');

// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
$uiDropdowns.filter(':not(.custom)').dropdown();
Expand All @@ -46,14 +47,14 @@ export function initGlobalDropdown() {
},
onHide() {
this._tippy?.enable();
// eslint-disable-next-line unicorn/no-this-assignment
const elDropdown = this;

// hide all tippy elements of items after a while. eg: use Enter to click "Copy Link" in the Issue Context Menu
setTimeout(() => {
const $dropdown = $(this);
const $dropdown = fomanticQuery(elDropdown);
if ($dropdown.dropdown('is hidden')) {
$(this).find('.menu > .item').each((_, item) => {
item._tippy?.hide();
});
queryElems(elDropdown, '.menu > .item', (el) => el._tippy?.hide());
}
}, 2000);
},
Expand All @@ -71,7 +72,7 @@ export function initGlobalDropdown() {
}

export function initGlobalTabularMenu() {
$('.ui.menu.tabular:not(.custom) .item').tab({autoTabActivation: false});
fomanticQuery('.ui.menu.tabular:not(.custom) .item').tab({autoTabActivation: false});
}

/**
Expand Down
32 changes: 17 additions & 15 deletions web_src/js/features/imagediff.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import $ from 'jquery';
import {GET} from '../modules/fetch.ts';
import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts';
import {parseDom} from '../utils.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';

function getDefaultSvgBoundsIfUndefined(text, src) {
const defaultSize = 300;
const maxSize = 99999;

const svgDoc = parseDom(text, 'image/svg+xml');
const svg = svgDoc.documentElement;
const svg = (svgDoc.documentElement as unknown) as SVGSVGElement;
const width = svg?.width?.baseVal;
const height = svg?.height?.baseVal;
if (width === undefined || height === undefined) {
Expand Down Expand Up @@ -68,25 +68,27 @@ function createContext(imageAfter, imageBefore) {
}

class ImageDiff {
async init(containerEl) {
containerEl: HTMLElement;
diffContainerWidth: number;

async init(containerEl: HTMLElement) {
this.containerEl = containerEl;
containerEl.setAttribute('data-image-diff-loaded', 'true');

// the only jQuery usage in this file
$(containerEl).find('.ui.menu.tabular .item').tab({autoTabActivation: false});
fomanticQuery(containerEl).find('.ui.menu.tabular .item').tab({autoTabActivation: false});

// the container may be hidden by "viewed" checkbox, so use the parent's width for reference
this.diffContainerWidth = Math.max(containerEl.closest('.diff-file-box').clientWidth - 300, 100);

const imageInfos = [{
path: containerEl.getAttribute('data-path-after'),
mime: containerEl.getAttribute('data-mime-after'),
images: containerEl.querySelectorAll('img.image-after'), // matches 3 <img>
images: containerEl.querySelectorAll<HTMLImageElement>('img.image-after'), // matches 3 <img>
boundsInfo: containerEl.querySelector('.bounds-info-after'),
}, {
path: containerEl.getAttribute('data-path-before'),
mime: containerEl.getAttribute('data-mime-before'),
images: containerEl.querySelectorAll('img.image-before'), // matches 3 <img>
images: containerEl.querySelectorAll<HTMLImageElement>('img.image-before'), // matches 3 <img>
boundsInfo: containerEl.querySelector('.bounds-info-before'),
}];

Expand All @@ -102,8 +104,8 @@ class ImageDiff {
const bounds = getDefaultSvgBoundsIfUndefined(text, info.path);
if (bounds) {
for (const el of info.images) {
el.setAttribute('width', bounds.width);
el.setAttribute('height', bounds.height);
el.setAttribute('width', String(bounds.width));
el.setAttribute('height', String(bounds.height));
}
hideElem(info.boundsInfo);
}
Expand Down Expand Up @@ -151,7 +153,7 @@ class ImageDiff {
const boundsInfoBeforeHeight = this.containerEl.querySelector('.bounds-info-before .bounds-info-height');
if (boundsInfoBeforeHeight) {
boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`;
boundsInfoBeforeHeight.classList.add('red', heightChanged);
boundsInfoBeforeHeight.classList.toggle('red', heightChanged);
}
}

Expand Down Expand Up @@ -205,7 +207,7 @@ class ImageDiff {
}

// extra height for inner "position: absolute" elements
const swipe = this.containerEl.querySelector('.diff-swipe');
const swipe = this.containerEl.querySelector<HTMLElement>('.diff-swipe');
if (swipe) {
swipe.style.width = `${sizes.maxSize.width * factor + 2}px`;
swipe.style.height = `${sizes.maxSize.height * factor + 30}px`;
Expand All @@ -225,7 +227,7 @@ class ImageDiff {
const rect = swipeFrame.getBoundingClientRect();
const value = Math.max(0, Math.min(e.clientX - rect.left, width));
swipeBar.style.left = `${value}px`;
this.containerEl.querySelector('.swipe-container').style.width = `${swipeFrame.clientWidth - value}px`;
this.containerEl.querySelector<HTMLElement>('.swipe-container').style.width = `${swipeFrame.clientWidth - value}px`;
};
const removeEventListeners = () => {
document.removeEventListener('mousemove', onSwipeMouseMove);
Expand Down Expand Up @@ -264,11 +266,11 @@ class ImageDiff {
overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
}

const rangeInput = this.containerEl.querySelector('input[type="range"]');
const rangeInput = this.containerEl.querySelector<HTMLInputElement>('input[type="range"]');

function updateOpacity() {
if (sizes.imageAfter) {
sizes.imageAfter.parentNode.style.opacity = `${rangeInput.value / 100}`;
sizes.imageAfter.parentNode.style.opacity = `${Number(rangeInput.value) / 100}`;
}
}

Expand All @@ -278,7 +280,7 @@ class ImageDiff {
}

export function initImageDiff() {
for (const el of queryElems('.image-diff:not([data-image-diff-loaded])')) {
for (const el of queryElems<HTMLImageElement>(document, '.image-diff:not([data-image-diff-loaded])')) {
(new ImageDiff()).init(el); // it is async, but we don't need to await for it
}
}
2 changes: 1 addition & 1 deletion web_src/js/features/repo-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async function onDownloadArchive(e) {
}

export function initRepoArchiveLinks() {
queryElems('a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive));
queryElems(document, 'a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive));
}

export function initRepoActivityTopAuthorsChart() {
Expand Down
24 changes: 12 additions & 12 deletions web_src/js/features/repo-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ export function initRepoEditor() {
const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone');
if (dropzoneUpload) initDropzone(dropzoneUpload);

const editArea = document.querySelector('.page-content.repository.editor textarea#edit_area');
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
if (!editArea) return;

for (const el of queryElems('.js-quick-pull-choice-option')) {
for (const el of queryElems<HTMLInputElement>(document, '.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;
document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = true;
} else {
hideElem('.quick-pull-branch-name');
document.querySelector('.quick-pull-branch-name input').required = false;
document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = false;
}
document.querySelector('#commit-button').textContent = el.getAttribute('data-button-text');
});
Expand All @@ -71,13 +71,13 @@ export function initRepoEditor() {
if (filenameInput.value) {
parts.push(filenameInput.value);
}
document.querySelector('#tree_path').value = parts.join('/');
document.querySelector<HTMLInputElement>('#tree_path').value = parts.join('/');
}
filenameInput.addEventListener('input', function () {
const parts = filenameInput.value.split('/');
const links = Array.from(document.querySelectorAll('.breadcrumb span.section'));
const dividers = Array.from(document.querySelectorAll('.breadcrumb .breadcrumb-divider'));
let warningDiv = document.querySelector('.ui.warning.message.flash-message.flash-warning.space-related');
let warningDiv = document.querySelector<HTMLDivElement>('.ui.warning.message.flash-message.flash-warning.space-related');
let containSpace = false;
if (parts.length > 1) {
for (let i = 0; i < parts.length; ++i) {
Expand Down Expand Up @@ -110,14 +110,14 @@ export function initRepoEditor() {
filenameInput.value = value;
}
this.setSelectionRange(0, 0);
containSpace |= (trimValue !== value && trimValue !== '');
containSpace = containSpace || (trimValue !== value && trimValue !== '');
}
}
containSpace |= Array.from(links).some((link) => {
containSpace = containSpace || Array.from(links).some((link) => {
const value = link.querySelector('a').textContent;
return value.trim() !== value;
});
containSpace |= parts[parts.length - 1].trim() !== parts[parts.length - 1];
containSpace = containSpace || parts[parts.length - 1].trim() !== parts[parts.length - 1];
if (containSpace) {
if (!warningDiv) {
warningDiv = document.createElement('div');
Expand All @@ -135,8 +135,8 @@ export function initRepoEditor() {
joinTreePath();
});
filenameInput.addEventListener('keydown', function (e) {
const sections = queryElems('.breadcrumb span.section');
const dividers = queryElems('.breadcrumb .breadcrumb-divider');
const sections = queryElems(document, '.breadcrumb span.section');
const dividers = queryElems(document, '.breadcrumb .breadcrumb-divider');
// Jump back to last directory once the filename is empty
if (e.code === 'Backspace' && filenameInput.selectionStart === 0 && sections.length > 0) {
e.preventDefault();
Expand All @@ -159,7 +159,7 @@ export function initRepoEditor() {

// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button
const commitButton = document.querySelector('#commit-button');
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
const $editForm = $('.ui.edit.form');
const dirtyFileClass = 'dirty-file';

Expand Down
2 changes: 1 addition & 1 deletion web_src/js/features/repo-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {appSubUrl, csrfToken} = window.config;

function initRepoSettingsCollaboration() {
// Change collaborator access mode
for (const dropdownEl of queryElems('.page-content.repository .ui.dropdown.access-mode')) {
for (const dropdownEl of queryElems(document, '.page-content.repository .ui.dropdown.access-mode')) {
const textEl = dropdownEl.querySelector(':scope > .text');
$(dropdownEl).dropdown({
async action(text, value) {
Expand Down
4 changes: 4 additions & 0 deletions web_src/js/modules/fomantic/base.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import $ from 'jquery';
let ariaIdCounter = 0;

export function generateAriaId() {
Expand All @@ -16,3 +17,6 @@ export function linkLabelAndInput(label, input) {
label.setAttribute('for', id);
}
}

// eslint-disable-next-line no-jquery/variable-pattern
export const fomanticQuery = $;
8 changes: 3 additions & 5 deletions web_src/js/modules/tippy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,9 @@ export function initGlobalTooltips() {
}

export function showTemporaryTooltip(target: Element, content: Content) {
// if the target is inside a dropdown, don't show the tooltip because when the dropdown
// closes, the tippy would be pushed unsightly to the top-left of the screen like seen
// on the issue comment menu.
if (target.closest('.ui.dropdown > .menu')) return;

// if the target is inside a dropdown, the menu will be hidden soon
// so display the tooltip on the dropdown instead
target = target.closest('.ui.dropdown') || target;
const tippy = target._tippy ?? attachTooltip(target, content);
tippy.setContent(content);
if (!tippy.state.isShown) tippy.show();
Expand Down
23 changes: 13 additions & 10 deletions web_src/js/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type $ from 'jquery';
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
type ElementsCallback = (el: Element) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
type IterableElements = NodeListOf<Element> | Array<Element>;
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array

function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
if (typeof el === 'string' || el instanceof String) {
Expand All @@ -15,7 +15,7 @@ function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: a
func(el, ...args);
} else if (el.length !== undefined) {
// this works for: NodeList, HTMLCollection, Array, jQuery
for (const e of (el as IterableElements)) {
for (const e of (el as ArrayLikeIterable<Element>)) {
func(e, ...args);
}
} else {
Expand Down Expand Up @@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
return res[0];
}

function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) {
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback): ArrayLikeIterable<T> {
if (fn) {
for (const el of elems) {
fn(el);
Expand All @@ -67,19 +67,22 @@ function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) {
return elems;
}

export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback) {
return applyElemsCallback(Array.from(el.parentNode.children).filter((child: Element) => {
export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
const elems = Array.from(el.parentNode.children) as T[];
return applyElemsCallback<T>(elems.filter((child: Element) => {
return child !== el && child.matches(selector);
}), fn);
}

// it works like jQuery.children: only the direct children are selected
export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback) {
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
}

export function queryElems(selector: string, fn?: ElementsCallback) {
return applyElemsCallback(document.querySelectorAll(selector), fn);
// it works like parent.querySelectorAll: all descendants are selected
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
}

export function onDomReady(cb: () => Promisable<void>) {
Expand All @@ -92,7 +95,7 @@ export function onDomReady(cb: () => Promisable<void>) {

// checks whether an element is owned by the current document, and whether it is a document fragment or element node
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
export function isDocumentFragmentOrElementNode(el: Element | Node) {
export function isDocumentFragmentOrElementNode(el: Node) {
try {
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
} catch {
Expand Down
Loading