mirror of
https://github.com/go-gitea/gitea.git
synced 2025-05-03 22:18:12 +00:00
Fix dynamic content loading init problem (#33748)
1. Rewrite `dirauto.ts` to `observer.ts`. * We have been using MutationObserver for long time, it's proven that it is quite performant. * Now we extend its ability to handle more "init" works. 2. Use `observeAddedElement` to init all non-custom "dropdown". 3. Use `data-global-click` to handle click events from dynamically loaded elements. * By this new approach, the old fragile selector-based (`.comment-reaction-button`) mechanism is removed. 4. By the way, remove unused `.diff-box` selector, it was abused and never really used. A lot of FIXMEs in "repo-diff.ts" are completely fixed, newly loaded contents could work as expected.
This commit is contained in:
parent
f3ada61097
commit
698ae7aa5b
@ -1,6 +1,6 @@
|
|||||||
{{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
|
{{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
|
||||||
<div>
|
<div>
|
||||||
<div class="diff-detail-box diff-box">
|
<div class="diff-detail-box">
|
||||||
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-ml-0.5">
|
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-ml-0.5">
|
||||||
{{if $showFileTree}}
|
{{if $showFileTree}}
|
||||||
<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
|
<button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}">
|
||||||
@ -80,7 +80,7 @@
|
|||||||
{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
|
{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
|
||||||
{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
|
{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
|
||||||
{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.Repository.IsArchived) $.IsShowingAllCommits}}
|
{{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.Repository.IsArchived) $.IsShowingAllCommits}}
|
||||||
<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
|
<div class="diff-file-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
|
||||||
<h4 class="diff-file-header sticky-2nd-row ui top attached header">
|
<h4 class="diff-file-header sticky-2nd-row ui top attached header">
|
||||||
<div class="diff-file-name tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-flex-wrap">
|
<div class="diff-file-name tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-flex-wrap">
|
||||||
<button class="fold-file btn interact-bg tw-p-1{{if not $isExpandable}} tw-invisible{{end}}">
|
<button class="fold-file btn interact-bg tw-p-1{{if not $isExpandable}} tw-invisible{{end}}">
|
||||||
@ -209,7 +209,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Diff.IsIncomplete}}
|
{{if .Diff.IsIncomplete}}
|
||||||
<div class="diff-file-box diff-box file-content tw-mt-2" id="diff-incomplete">
|
<div class="diff-file-box file-content tw-mt-2" id="diff-incomplete">
|
||||||
<h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between">
|
<h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between">
|
||||||
{{ctx.Locale.Tr "repo.diff.too_many_files"}}
|
{{ctx.Locale.Tr "repo.diff.too_many_files"}}
|
||||||
<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
|
<a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<a class="muted">{{svg "octicon-smiley"}}</a>
|
<a class="muted">{{svg "octicon-smiley"}}</a>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{{range $value := AllowedReactions}}
|
{{range $value := AllowedReactions}}
|
||||||
<a class="item emoji comment-reaction-button" data-tooltip-content="{{$value}}" aria-label="{{$value}}" data-reaction-content="{{$value}}">{{ReactionToEmoji $value}}</a>
|
<a class="item emoji" data-tooltip-content="{{$value}}" aria-label="{{$value}}" data-reaction-content="{{$value}}" data-global-click="onCommentReactionButtonClick">{{ReactionToEmoji $value}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
{{if $diff}}
|
{{if $diff}}
|
||||||
{{$file := (index $diff.Files 0)}}
|
{{$file := (index $diff.Files 0)}}
|
||||||
<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}">
|
<div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}">
|
||||||
<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
|
<div class="diff-file-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
|
||||||
<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
|
<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<div class="bottom-reactions" data-action-url="{{$.ActionURL}}">
|
<div class="bottom-reactions" data-action-url="{{$.ActionURL}}">
|
||||||
{{range $key, $value := .Reactions}}
|
{{range $key, $value := .Reactions}}
|
||||||
{{$hasReacted := $value.HasUser ctx.RootData.SignedUserID}}
|
{{$hasReacted := $value.HasUser ctx.RootData.SignedUserID}}
|
||||||
<a role="button" class="ui label basic{{if $hasReacted}} primary{{end}}{{if not ctx.RootData.IsSigned}} disabled{{end}} comment-reaction-button"
|
<a role="button" class="ui label basic{{if $hasReacted}} primary{{end}}{{if not ctx.RootData.IsSigned}} disabled{{end}}"
|
||||||
data-tooltip-content
|
data-global-click="onCommentReactionButtonClick"
|
||||||
title="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ctx.Locale.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}"
|
data-tooltip-content title="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ctx.Locale.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}"
|
||||||
aria-label="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ctx.Locale.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}"
|
aria-label="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ctx.Locale.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}"
|
||||||
data-tooltip-placement="bottom-start"
|
data-tooltip-placement="bottom-start"
|
||||||
data-reaction-content="{{$key}}" data-has-reacted="{{$hasReacted}}">
|
data-reaction-content="{{$key}}" data-has-reacted="{{$hasReacted}}">
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<div class="repository search">
|
<div class="repository search">
|
||||||
{{range $result := .SearchResults}}
|
{{range $result := .SearchResults}}
|
||||||
{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
|
{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
|
||||||
<div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result">
|
<div class="diff-file-box file-content non-diff-file-content repo-search-result">
|
||||||
<h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap">
|
<h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap">
|
||||||
{{if not $.Repo}}
|
{{if not $.Repo}}
|
||||||
<span class="file tw-flex-1">
|
<span class="file tw-flex-1">
|
||||||
|
@ -1085,10 +1085,6 @@ td .commit-summary {
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .diff-box .resolved-placeholder .button {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository .diff-file-box .header {
|
.repository .diff-file-box .header {
|
||||||
background-color: var(--color-box-header);
|
background-color: var(--color-box-header);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import {GET} from '../modules/fetch.ts';
|
|||||||
import {showGlobalErrorMessage} from '../bootstrap.ts';
|
import {showGlobalErrorMessage} from '../bootstrap.ts';
|
||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
import {queryElems} from '../utils/dom.ts';
|
import {queryElems} from '../utils/dom.ts';
|
||||||
|
import {observeAddedElement} from '../modules/observer.ts';
|
||||||
|
|
||||||
const {appUrl} = window.config;
|
const {appUrl} = window.config;
|
||||||
|
|
||||||
@ -28,47 +29,51 @@ export function initFootLanguageMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initGlobalDropdown() {
|
export function initGlobalDropdown() {
|
||||||
// Semantic UI modules.
|
|
||||||
const $uiDropdowns = fomanticQuery('.ui.dropdown');
|
|
||||||
|
|
||||||
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
|
||||||
$uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
|
observeAddedElement('.ui.dropdown:not(.custom)', (el) => {
|
||||||
|
const $dropdown = fomanticQuery(el);
|
||||||
|
if ($dropdown.data('module-dropdown')) return; // do not re-init if other code has already initialized it.
|
||||||
|
|
||||||
// The "jump" means this dropdown is mainly used for "menu" purpose,
|
$dropdown.dropdown('setting', {hideDividers: 'empty'});
|
||||||
// clicking an item will jump to somewhere else or trigger an action/function.
|
|
||||||
// When a dropdown is used for non-refresh actions with tippy,
|
|
||||||
// it must have this "jump" class to hide the tippy when dropdown is closed.
|
|
||||||
$uiDropdowns.filter('.jump').dropdown('setting', {
|
|
||||||
action: 'hide',
|
|
||||||
onShow() {
|
|
||||||
// hide associated tooltip while dropdown is open
|
|
||||||
this._tippy?.hide();
|
|
||||||
this._tippy?.disable();
|
|
||||||
},
|
|
||||||
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
|
if (el.classList.contains('jump')) {
|
||||||
setTimeout(() => {
|
// The "jump" means this dropdown is mainly used for "menu" purpose,
|
||||||
const $dropdown = fomanticQuery(elDropdown);
|
// clicking an item will jump to somewhere else or trigger an action/function.
|
||||||
if ($dropdown.dropdown('is hidden')) {
|
// When a dropdown is used for non-refresh actions with tippy,
|
||||||
queryElems(elDropdown, '.menu > .item', (el) => el._tippy?.hide());
|
// it must have this "jump" class to hide the tippy when dropdown is closed.
|
||||||
}
|
$dropdown.dropdown('setting', {
|
||||||
}, 2000);
|
action: 'hide',
|
||||||
},
|
onShow() {
|
||||||
|
// hide associated tooltip while dropdown is open
|
||||||
|
this._tippy?.hide();
|
||||||
|
this._tippy?.disable();
|
||||||
|
},
|
||||||
|
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 = fomanticQuery(elDropdown);
|
||||||
|
if ($dropdown.dropdown('is hidden')) {
|
||||||
|
queryElems(elDropdown, '.menu > .item', (el) => el._tippy?.hide());
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special popup-directions, prevent Fomantic from guessing the popup direction.
|
||||||
|
// With default "direction: auto", if the viewport height is small, Fomantic would show the popup upward,
|
||||||
|
// if the dropdown is at the beginning of the page, then the top part would be clipped by the window view.
|
||||||
|
// eg: Issue List "Sort" dropdown
|
||||||
|
// But we can not set "direction: downward" for all dropdowns, because there is a bug in dropdown menu positioning when calculating the "left" position,
|
||||||
|
// which would make some dropdown popups slightly shift out of the right viewport edge in some cases.
|
||||||
|
// eg: the "Create New Repo" menu on the navbar.
|
||||||
|
if (el.classList.contains('upward')) $dropdown.dropdown('setting', 'direction', 'upward');
|
||||||
|
if (el.classList.contains('downward')) $dropdown.dropdown('setting', 'direction', 'downward');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Special popup-directions, prevent Fomantic from guessing the popup direction.
|
|
||||||
// With default "direction: auto", if the viewport height is small, Fomantic would show the popup upward,
|
|
||||||
// if the dropdown is at the beginning of the page, then the top part would be clipped by the window view.
|
|
||||||
// eg: Issue List "Sort" dropdown
|
|
||||||
// But we can not set "direction: downward" for all dropdowns, because there is a bug in dropdown menu positioning when calculating the "left" position,
|
|
||||||
// which would make some dropdown popups slightly shift out of the right viewport edge in some cases.
|
|
||||||
// eg: the "Create New Repo" menu on the navbar.
|
|
||||||
$uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward');
|
|
||||||
$uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initGlobalTabularMenu() {
|
export function initGlobalTabularMenu() {
|
||||||
|
@ -1,37 +1,31 @@
|
|||||||
import {POST} from '../../modules/fetch.ts';
|
import {POST} from '../../modules/fetch.ts';
|
||||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
|
||||||
import type {DOMEvent} from '../../utils/dom.ts';
|
import type {DOMEvent} from '../../utils/dom.ts';
|
||||||
|
import {registerGlobalEventFunc} from '../../modules/observer.ts';
|
||||||
|
|
||||||
export function initCompReactionSelector(parent: ParentNode = document) {
|
export function initCompReactionSelector() {
|
||||||
for (const container of parent.querySelectorAll<HTMLElement>('.issue-content, .diff-file-body')) {
|
registerGlobalEventFunc('click', 'onCommentReactionButtonClick', async (target: HTMLElement, e: DOMEvent<MouseEvent>) => {
|
||||||
container.addEventListener('click', async (e: DOMEvent<MouseEvent>) => {
|
// there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment
|
||||||
// there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment
|
e.preventDefault();
|
||||||
const target = e.target.closest('.comment-reaction-button');
|
|
||||||
if (!target) return;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (target.classList.contains('disabled')) return;
|
if (target.classList.contains('disabled')) return;
|
||||||
|
|
||||||
const actionUrl = target.closest('[data-action-url]').getAttribute('data-action-url');
|
const actionUrl = target.closest('[data-action-url]').getAttribute('data-action-url');
|
||||||
const reactionContent = target.getAttribute('data-reaction-content');
|
const reactionContent = target.getAttribute('data-reaction-content');
|
||||||
|
|
||||||
const commentContainer = target.closest('.comment-container');
|
const commentContainer = target.closest('.comment-container');
|
||||||
|
|
||||||
const bottomReactions = commentContainer.querySelector('.bottom-reactions'); // may not exist if there is no reaction
|
const bottomReactions = commentContainer.querySelector('.bottom-reactions'); // may not exist if there is no reaction
|
||||||
const bottomReactionBtn = bottomReactions?.querySelector(`a[data-reaction-content="${CSS.escape(reactionContent)}"]`);
|
const bottomReactionBtn = bottomReactions?.querySelector(`a[data-reaction-content="${CSS.escape(reactionContent)}"]`);
|
||||||
const hasReacted = bottomReactionBtn?.getAttribute('data-has-reacted') === 'true';
|
const hasReacted = bottomReactionBtn?.getAttribute('data-has-reacted') === 'true';
|
||||||
|
|
||||||
const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
|
const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
|
||||||
data: new URLSearchParams({content: reactionContent}),
|
data: new URLSearchParams({content: reactionContent}),
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
bottomReactions?.remove();
|
|
||||||
if (data.html) {
|
|
||||||
commentContainer.insertAdjacentHTML('beforeend', data.html);
|
|
||||||
const bottomReactionsDropdowns = commentContainer.querySelectorAll('.bottom-reactions .dropdown.select-reaction');
|
|
||||||
fomanticQuery(bottomReactionsDropdowns).dropdown(); // re-init the dropdown
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
const data = await res.json();
|
||||||
|
bottomReactions?.remove();
|
||||||
|
if (data.html) {
|
||||||
|
commentContainer.insertAdjacentHTML('beforeend', data.html);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import {initCompReactionSelector} from './comp/ReactionSelector.ts';
|
|
||||||
import {initRepoIssueContentHistory} from './repo-issue-content.ts';
|
import {initRepoIssueContentHistory} from './repo-issue-content.ts';
|
||||||
import {initDiffFileTree} from './repo-diff-filetree.ts';
|
import {initDiffFileTree} from './repo-diff-filetree.ts';
|
||||||
import {initDiffCommitSelect} from './repo-diff-commitselect.ts';
|
import {initDiffCommitSelect} from './repo-diff-commitselect.ts';
|
||||||
@ -8,17 +7,16 @@ import {initImageDiff} from './imagediff.ts';
|
|||||||
import {showErrorToast} from '../modules/toast.ts';
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts';
|
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts';
|
||||||
import {POST, GET} from '../modules/fetch.ts';
|
import {POST, GET} from '../modules/fetch.ts';
|
||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
|
||||||
import {createTippy} from '../modules/tippy.ts';
|
import {createTippy} from '../modules/tippy.ts';
|
||||||
import {invertFileFolding} from './file-fold.ts';
|
import {invertFileFolding} from './file-fold.ts';
|
||||||
import {parseDom} from '../utils.ts';
|
import {parseDom} from '../utils.ts';
|
||||||
|
import {observeAddedElement} from '../modules/observer.ts';
|
||||||
|
|
||||||
const {i18n} = window.config;
|
const {i18n} = window.config;
|
||||||
|
|
||||||
function initRepoDiffFileViewToggle() {
|
function initRepoDiffFileBox(el: HTMLElement) {
|
||||||
// switch between "rendered" and "source", for image and CSV files
|
// switch between "rendered" and "source", for image and CSV files
|
||||||
// FIXME: this event listener is not correctly added to "load more files"
|
queryElems(el, '.file-view-toggle', (btn) => btn.addEventListener('click', () => {
|
||||||
queryElems(document, '.file-view-toggle', (btn) => btn.addEventListener('click', () => {
|
|
||||||
queryElemSiblings(btn, '.file-view-toggle', (el) => el.classList.remove('active'));
|
queryElemSiblings(btn, '.file-view-toggle', (el) => el.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
|
|
||||||
@ -75,7 +73,6 @@ function initRepoDiffConversationForm() {
|
|||||||
el.classList.add('tw-invisible');
|
el.classList.add('tw-invisible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fomanticQuery(newConversationHolder.querySelectorAll('.ui.dropdown')).dropdown();
|
|
||||||
|
|
||||||
// the default behavior is to add a pending review, so if no submitter, it also means "pending_review"
|
// the default behavior is to add a pending review, so if no submitter, it also means "pending_review"
|
||||||
if (!submitter || submitter?.matches('button[name="pending_review"]')) {
|
if (!submitter || submitter?.matches('button[name="pending_review"]')) {
|
||||||
@ -110,8 +107,6 @@ function initRepoDiffConversationForm() {
|
|||||||
if (elConversationHolder) {
|
if (elConversationHolder) {
|
||||||
const elNewConversation = createElementFromHTML(data);
|
const elNewConversation = createElementFromHTML(data);
|
||||||
elConversationHolder.replaceWith(elNewConversation);
|
elConversationHolder.replaceWith(elNewConversation);
|
||||||
queryElems(elConversationHolder, '.ui.dropdown:not(.custom)', (el) => fomanticQuery(el).dropdown());
|
|
||||||
initCompReactionSelector(elNewConversation);
|
|
||||||
} else {
|
} else {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
@ -149,7 +144,7 @@ function initDiffHeaderPopup() {
|
|||||||
|
|
||||||
// Will be called when the show more (files) button has been pressed
|
// Will be called when the show more (files) button has been pressed
|
||||||
function onShowMoreFiles() {
|
function onShowMoreFiles() {
|
||||||
// FIXME: here the init calls are incomplete: at least it misses dropdown & initCompReactionSelector & initRepoDiffFileViewToggle
|
// TODO: replace these calls with the "observer.ts" methods
|
||||||
initRepoIssueContentHistory();
|
initRepoIssueContentHistory();
|
||||||
initViewedCheckboxListenerFor();
|
initViewedCheckboxListenerFor();
|
||||||
countAndUpdateViewedFiles();
|
countAndUpdateViewedFiles();
|
||||||
@ -255,11 +250,11 @@ export function initRepoDiffView() {
|
|||||||
initDiffCommitSelect();
|
initDiffCommitSelect();
|
||||||
initRepoDiffShowMore();
|
initRepoDiffShowMore();
|
||||||
initDiffHeaderPopup();
|
initDiffHeaderPopup();
|
||||||
initRepoDiffFileViewToggle();
|
|
||||||
initViewedCheckboxListenerFor();
|
initViewedCheckboxListenerFor();
|
||||||
initExpandAndCollapseFilesButton();
|
initExpandAndCollapseFilesButton();
|
||||||
initRepoDiffHashChangeListener();
|
initRepoDiffHashChangeListener();
|
||||||
|
|
||||||
|
observeAddedElement('#diff-file-boxes .diff-file-box', initRepoDiffFileBox);
|
||||||
addDelegatedEventListener(document, 'click', '.fold-file', (el) => {
|
addDelegatedEventListener(document, 'click', '.fold-file', (el) => {
|
||||||
invertFileFolding(el.closest('.file-content'), el);
|
invertFileFolding(el.closest('.file-content'), el);
|
||||||
});
|
});
|
||||||
|
@ -83,8 +83,8 @@ export function initRepoGraphGit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const flowSelectRefsDropdown = document.querySelector('#flow-select-refs-dropdown');
|
const flowSelectRefsDropdown = document.querySelector('#flow-select-refs-dropdown');
|
||||||
fomanticQuery(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
|
const $dropdown = fomanticQuery(flowSelectRefsDropdown);
|
||||||
fomanticQuery(flowSelectRefsDropdown).dropdown({
|
$dropdown.dropdown({
|
||||||
clearable: true,
|
clearable: true,
|
||||||
fullTextSeach: 'exact',
|
fullTextSeach: 'exact',
|
||||||
onRemove(toRemove: string) {
|
onRemove(toRemove: string) {
|
||||||
@ -110,6 +110,7 @@ export function initRepoGraphGit() {
|
|||||||
updateGraph();
|
updateGraph();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
$dropdown.dropdown('set selected', dropdownSelected);
|
||||||
|
|
||||||
graphContainer.addEventListener('mouseenter', (e: DOMEvent<MouseEvent>) => {
|
graphContainer.addEventListener('mouseenter', (e: DOMEvent<MouseEvent>) => {
|
||||||
if (e.target.matches('#rev-list li')) {
|
if (e.target.matches('#rev-list li')) {
|
||||||
|
@ -62,7 +62,7 @@ import {initRepoContributors} from './features/contributors.ts';
|
|||||||
import {initRepoCodeFrequency} from './features/code-frequency.ts';
|
import {initRepoCodeFrequency} from './features/code-frequency.ts';
|
||||||
import {initRepoRecentCommits} from './features/recent-commits.ts';
|
import {initRepoRecentCommits} from './features/recent-commits.ts';
|
||||||
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts';
|
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts';
|
||||||
import {initDirAuto} from './modules/dirauto.ts';
|
import {initAddedElementObserver} from './modules/observer.ts';
|
||||||
import {initRepositorySearch} from './features/repo-search.ts';
|
import {initRepositorySearch} from './features/repo-search.ts';
|
||||||
import {initColorPickers} from './features/colorpicker.ts';
|
import {initColorPickers} from './features/colorpicker.ts';
|
||||||
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
|
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
|
||||||
@ -86,7 +86,7 @@ import {
|
|||||||
} from './features/common-form.ts';
|
} from './features/common-form.ts';
|
||||||
|
|
||||||
initGiteaFomantic();
|
initGiteaFomantic();
|
||||||
initDirAuto();
|
initAddedElementObserver();
|
||||||
initSubmitEventPolyfill();
|
initSubmitEventPolyfill();
|
||||||
|
|
||||||
function callInitFunctions(functions: (() => any)[]) {
|
function callInitFunctions(functions: (() => any)[]) {
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
|
||||||
|
|
||||||
type DirElement = HTMLInputElement | HTMLTextAreaElement;
|
|
||||||
|
|
||||||
// for performance considerations, it only uses performant syntax
|
|
||||||
function attachDirAuto(el: DirElement) {
|
|
||||||
if (el.type !== 'hidden' &&
|
|
||||||
el.type !== 'checkbox' &&
|
|
||||||
el.type !== 'radio' &&
|
|
||||||
el.type !== 'range' &&
|
|
||||||
el.type !== 'color') {
|
|
||||||
el.dir = 'auto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initDirAuto(): void {
|
|
||||||
const observer = new MutationObserver((mutationList) => {
|
|
||||||
const len = mutationList.length;
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const mutation = mutationList[i];
|
|
||||||
const len = mutation.addedNodes.length;
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const addedNode = mutation.addedNodes[i] as HTMLElement;
|
|
||||||
if (!isDocumentFragmentOrElementNode(addedNode)) continue;
|
|
||||||
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') {
|
|
||||||
attachDirAuto(addedNode as DirElement);
|
|
||||||
}
|
|
||||||
const children = addedNode.querySelectorAll<DirElement>('input, textarea');
|
|
||||||
const len = children.length;
|
|
||||||
for (let childIdx = 0; childIdx < len; childIdx++) {
|
|
||||||
attachDirAuto(children[childIdx]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const docNodes = document.querySelectorAll<DirElement>('input, textarea');
|
|
||||||
const len = docNodes.length;
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
attachDirAuto(docNodes[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.observe(document, {subtree: true, childList: true});
|
|
||||||
}
|
|
89
web_src/js/modules/observer.ts
Normal file
89
web_src/js/modules/observer.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||||
|
|
||||||
|
type DirElement = HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
|
||||||
|
// for performance considerations, it only uses performant syntax
|
||||||
|
function attachDirAuto(el: Partial<DirElement>) {
|
||||||
|
if (el.type !== 'hidden' &&
|
||||||
|
el.type !== 'checkbox' &&
|
||||||
|
el.type !== 'radio' &&
|
||||||
|
el.type !== 'range' &&
|
||||||
|
el.type !== 'color') {
|
||||||
|
el.dir = 'auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalInitFunc<T extends HTMLElement> = (el: T) => void | Promise<void>;
|
||||||
|
const globalInitFuncs: Record<string, GlobalInitFunc<HTMLElement>> = {};
|
||||||
|
function attachGlobalInit(el: HTMLElement) {
|
||||||
|
const initFunc = el.getAttribute('data-global-init');
|
||||||
|
const func = globalInitFuncs[initFunc];
|
||||||
|
if (!func) throw new Error(`Global init function "${initFunc}" not found`);
|
||||||
|
func(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalEventFunc<T extends HTMLElement, E extends Event> = (el: T, e: E) => (void | Promise<void>);
|
||||||
|
const globalEventFuncs: Record<string, GlobalEventFunc<HTMLElement, Event>> = {};
|
||||||
|
export function registerGlobalEventFunc<T extends HTMLElement, E extends Event>(event: string, name: string, func: GlobalEventFunc<T, E>) {
|
||||||
|
globalEventFuncs[`${event}:${name}`] = func as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectorHandler = {
|
||||||
|
selector: string,
|
||||||
|
handler: (el: HTMLElement) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectorHandlers: SelectorHandler[] = [
|
||||||
|
{selector: 'input, textarea', handler: attachDirAuto},
|
||||||
|
{selector: '[data-global-init]', handler: attachGlobalInit},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function observeAddedElement(selector: string, handler: (el: HTMLElement) => void) {
|
||||||
|
selectorHandlers.push({selector, handler});
|
||||||
|
const docNodes = document.querySelectorAll<HTMLElement>(selector);
|
||||||
|
for (const el of docNodes) {
|
||||||
|
handler(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initAddedElementObserver(): void {
|
||||||
|
const observer = new MutationObserver((mutationList) => {
|
||||||
|
const len = mutationList.length;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const mutation = mutationList[i];
|
||||||
|
const len = mutation.addedNodes.length;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const addedNode = mutation.addedNodes[i] as HTMLElement;
|
||||||
|
if (!isDocumentFragmentOrElementNode(addedNode)) continue;
|
||||||
|
|
||||||
|
for (const {selector, handler} of selectorHandlers) {
|
||||||
|
if (addedNode.matches(selector)) {
|
||||||
|
handler(addedNode);
|
||||||
|
}
|
||||||
|
const children = addedNode.querySelectorAll<HTMLElement>(selector);
|
||||||
|
for (const el of children) {
|
||||||
|
handler(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const {selector, handler} of selectorHandlers) {
|
||||||
|
const docNodes = document.querySelectorAll<HTMLElement>(selector);
|
||||||
|
for (const el of docNodes) {
|
||||||
|
handler(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.observe(document, {subtree: true, childList: true});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const elem = (e.target as HTMLElement).closest<HTMLElement>('[data-global-click]');
|
||||||
|
if (!elem) return;
|
||||||
|
const funcName = elem.getAttribute('data-global-click');
|
||||||
|
const func = globalEventFuncs[`click:${funcName}`];
|
||||||
|
if (!func) throw new Error(`Global event function "click:${funcName}" not found`);
|
||||||
|
func(elem, e);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user