mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-21 09:31:19 +02:00
Compare commits
3 Commits
70685a9489
...
c05082669b
Author | SHA1 | Date | |
---|---|---|---|
|
c05082669b | ||
|
6033c67a1a | ||
|
555735936f |
54
SECURITY.md
54
SECURITY.md
@@ -14,12 +14,12 @@ Please **DO NOT** file a public issue, instead send your report privately to `se
|
||||
|
||||
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
|
||||
|
||||
The PGP key is valid until July 9, 2025.
|
||||
The PGP key is valid until July 4, 2026.
|
||||
|
||||
```
|
||||
Key ID: 6FCD2D5B
|
||||
Key Type: RSA
|
||||
Expires: 7/9/2025
|
||||
Expires: 7/4/2026
|
||||
Key Size: 4096/4096
|
||||
Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
|
||||
```
|
||||
@@ -42,18 +42,18 @@ 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
|
||||
@@ -65,19 +65,19 @@ s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
|
||||
axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
|
||||
D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
|
||||
Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
|
||||
WwUCZoyyjQUJBbh+DwAKCRCq/SOBb80tW18XD/9MXztmf01MT+1kZdBouZ/7Rp/7
|
||||
9kuqo//B1G+RXau4oFtPqb67kNe2WaIc3u5B73PUHsMf3i6z4ib2KbMhZZerLn0O
|
||||
dRglcuPeNWmsASY3dH/XVG0cT0zvvWegagd12TJEl3Vs+7XNrOw4cwDj9L1+GH9m
|
||||
kSt4uaANWn/6a3RvMRhiVEYuNwhAzcKaactPmYqrLJgoVLbRSDkgyHaMQ2jKgLxk
|
||||
ifS/fvluGV0ub2Po6DJiqfRpd1tDvPhe9y1+r1WFDZsOcvTcZUfSt/7dXMGfqGu0
|
||||
2daVFlfeSXSALrDE5uc0UxodHCpP3sqRYDZevGLBRaaTkIjYXG/+N898+7K5WJF4
|
||||
xXOLWxM2cwGkG7eC9pugcDnBp9XlF7O+GBiZ05JUe5flXDQFZ+h3exjopu6KHF1B
|
||||
RnzNy8LC0UKb+AuvRIOLV92a9Q9wGWU/jaVDu6nZ0umAeuSzxiHoDsonm0Fl9QAz
|
||||
2/xCokebuoeLrEK7R2af3X86mqq3sVO4ax+HPYChzOaVQBiHUW/TAldWcldYYphR
|
||||
/e2WsbmQfvCRtz/bZfo+aUVnrHNjzVMtF2SszdVmA/04Y8pS28MqtuRqhm5DPOOd
|
||||
g1YeUywK5jRZ1twyo1kzJEFPLaoeaXaycsR1PMVBW0Urik5mrR/pOWq7PPoZoKb2
|
||||
lXYLE8bwkuQTmsyL1g==
|
||||
=9i7d
|
||||
WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
|
||||
f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
|
||||
vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
|
||||
zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
|
||||
NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
|
||||
WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
|
||||
bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
|
||||
U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
|
||||
RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
|
||||
kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
|
||||
sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
|
||||
9M2VbqL9C51z/wyHLg==
|
||||
=SfZA
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
```
|
||||
|
@@ -26,6 +26,7 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request)
|
||||
path := chiCtx.URLParam(g.pathParam)
|
||||
for _, m := range g.matchers {
|
||||
if m.matchPath(chiCtx, path) {
|
||||
chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern)
|
||||
handler := m.handlerFunc
|
||||
for i := len(m.middlewares) - 1; i >= 0; i-- {
|
||||
handler = m.middlewares[i](handler).ServeHTTP
|
||||
@@ -38,6 +39,7 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
type RouterPathGroupPattern struct {
|
||||
pattern string
|
||||
re *regexp.Regexp
|
||||
params []routerPathParam
|
||||
middlewares []any
|
||||
@@ -62,6 +64,7 @@ type routerPathParam struct {
|
||||
|
||||
type routerPathMatcher struct {
|
||||
methods container.Set[string]
|
||||
pattern string
|
||||
re *regexp.Regexp
|
||||
params []routerPathParam
|
||||
middlewares []func(http.Handler) http.Handler
|
||||
@@ -117,7 +120,7 @@ func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern,
|
||||
}
|
||||
p.methods.Add(method)
|
||||
}
|
||||
p.re, p.params = patternRegexp.re, patternRegexp.params
|
||||
p.pattern, p.re, p.params = patternRegexp.pattern, patternRegexp.re, patternRegexp.params
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@ func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
|
||||
p.params = append(p.params, param)
|
||||
}
|
||||
re = append(re, '$')
|
||||
p.re = regexp.MustCompile(string(re))
|
||||
p.pattern, p.re = pattern, regexp.MustCompile(string(re))
|
||||
return p
|
||||
}
|
||||
|
||||
|
@@ -56,17 +56,20 @@ func TestRouter(t *testing.T) {
|
||||
recorder.Body = buff
|
||||
|
||||
type resultStruct struct {
|
||||
method string
|
||||
pathParams map[string]string
|
||||
handlerMarks []string
|
||||
method string
|
||||
pathParams map[string]string
|
||||
handlerMarks []string
|
||||
chiRoutePattern *string
|
||||
}
|
||||
|
||||
var res resultStruct
|
||||
h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
|
||||
mark := util.OptionalArg(optMark, "")
|
||||
return func(resp http.ResponseWriter, req *http.Request) {
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
res.method = req.Method
|
||||
res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context()))
|
||||
res.pathParams = chiURLParamsToMap(chiCtx)
|
||||
res.chiRoutePattern = util.ToPointer(chiCtx.RoutePattern())
|
||||
if mark != "" {
|
||||
res.handlerMarks = append(res.handlerMarks, mark)
|
||||
}
|
||||
@@ -125,21 +128,29 @@ func TestRouter(t *testing.T) {
|
||||
req, err := http.NewRequest(methodPathFields[0], methodPathFields[1], nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
if expected.chiRoutePattern == nil {
|
||||
res.chiRoutePattern = nil
|
||||
}
|
||||
assert.Equal(t, expected, res)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("RootRouter", func(t *testing.T) {
|
||||
testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}})
|
||||
testRoute(t, "GET /the-user/the-repo/other", resultStruct{
|
||||
method: "GET",
|
||||
handlerMarks: []string{"not-found:/"},
|
||||
chiRoutePattern: util.ToPointer(""),
|
||||
})
|
||||
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
||||
handlerMarks: []string{"list-issues-b"},
|
||||
})
|
||||
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||
handlerMarks: []string{"view-issue"},
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||
handlerMarks: []string{"view-issue"},
|
||||
chiRoutePattern: util.ToPointer("/{username}/{reponame}/{type:issues|pulls}/{index}"),
|
||||
})
|
||||
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
||||
method: "GET",
|
||||
@@ -154,7 +165,10 @@ func TestRouter(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Sub Router", func(t *testing.T) {
|
||||
testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}})
|
||||
testRoute(t, "GET /api/v1/other", resultStruct{
|
||||
method: "GET",
|
||||
handlerMarks: []string{"not-found:/api/v1"},
|
||||
})
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
||||
@@ -211,9 +225,10 @@ func TestRouter(t *testing.T) {
|
||||
})
|
||||
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||
handlerMarks: []string{"s1", "s2", "s3"},
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||
handlerMarks: []string{"s1", "s2", "s3"},
|
||||
chiRoutePattern: util.ToPointer("/api/v1/repos/{username}/{reponame}/branches/<dir:*>/<file:[a-z]{1,2}>"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -2334,8 +2334,8 @@ settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server
|
||||
settings.webhook_deletion = Remove Webhook
|
||||
settings.webhook_deletion_desc = Removing a webhook deletes its settings and delivery history. Continue?
|
||||
settings.webhook_deletion_success = The webhook has been removed.
|
||||
settings.webhook.test_delivery = Test Delivery
|
||||
settings.webhook.test_delivery_desc = Test this webhook with a fake event.
|
||||
settings.webhook.test_delivery = Test Push Event
|
||||
settings.webhook.test_delivery_desc = Test this webhook with a fake push event.
|
||||
settings.webhook.test_delivery_desc_disabled = To test this webhook with a fake event, activate it.
|
||||
settings.webhook.request = Request
|
||||
settings.webhook.response = Response
|
||||
|
@@ -51,7 +51,7 @@ func ListOrGetPackages(ctx *context.Context) {
|
||||
DownloadPackageFile(ctx)
|
||||
return
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
http.NotFound(ctx.Resp, ctx.Req)
|
||||
}
|
||||
|
||||
func EnumeratePackages(ctx *context.Context) {
|
||||
|
@@ -68,7 +68,7 @@
|
||||
<a class="text primary" href="{{$.Link}}">
|
||||
gitea-org / gitea
|
||||
</a>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.fork"}}">{{svg "octicon-repo-forked"}}</span>
|
||||
<span class="flex-text-inline" data-tooltip-content="{{ctx.Locale.Tr "repo.fork"}}">{{svg "octicon-repo-forked"}}</span>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
<a class="muted" href="{{$.Link}}">
|
||||
|
@@ -26,7 +26,7 @@
|
||||
{{else}}
|
||||
<span class="text red">{{svg "octicon-alert"}}</span>
|
||||
{{end}}
|
||||
<a class="ui primary sha label toggle button show-panel" data-panel="#info-{{.ID}}">{{.UUID}}</a>
|
||||
<button class="btn interact-bg tw-p-2 toggle show-panel" data-panel="#info-{{.ID}}">{{.UUID}}</button>
|
||||
</div>
|
||||
<span class="text grey">
|
||||
{{DateUtils.TimeSince .Delivered}}
|
||||
|
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body tw-mt-1">
|
||||
<div class="flex-item-body">
|
||||
<a class="index" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
|
||||
{{if eq $.listType "dashboard"}}
|
||||
{{.Repo.FullName}}#{{.Index}}
|
||||
|
@@ -17,6 +17,7 @@
|
||||
.flex-item .flex-item-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
flex-grow: 1;
|
||||
flex-basis: 60%; /* avoid wrapping the "flex-item-trailing" too aggressively */
|
||||
min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import {defineComponent} from 'vue';
|
||||
import {SvgIcon} from '../svg.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {generateAriaId} from '../modules/fomantic/base.ts';
|
||||
import {generateElemId} from '../utils/dom.ts';
|
||||
|
||||
type Commit = {
|
||||
id: string,
|
||||
@@ -35,8 +35,8 @@ export default defineComponent({
|
||||
commits: [] as Array<Commit>,
|
||||
hoverActivated: false,
|
||||
lastReviewCommitSha: '',
|
||||
uniqueIdMenu: generateAriaId(),
|
||||
uniqueIdShowAll: generateAriaId(),
|
||||
uniqueIdMenu: generateElemId('diff-commit-selector-menu-'),
|
||||
uniqueIdShowAll: generateElemId('diff-commit-selector-show-all-'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import '@github/markdown-toolbar-element';
|
||||
import '@github/text-expander-element';
|
||||
import {attachTribute} from '../tribute.ts';
|
||||
import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts';
|
||||
import {hideElem, showElem, autosize, isElemVisible, generateElemId} from '../../utils/dom.ts';
|
||||
import {
|
||||
EventUploadStateChanged,
|
||||
initEasyMDEPaste,
|
||||
@@ -25,8 +25,6 @@ import {createTippy} from '../../modules/tippy.ts';
|
||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
import type EasyMDE from 'easymde';
|
||||
|
||||
let elementIdCounter = 0;
|
||||
|
||||
/**
|
||||
* validate if the given textarea is non-empty.
|
||||
* @param {HTMLTextAreaElement} textarea - The textarea element to be validated.
|
||||
@@ -125,7 +123,7 @@ export class ComboMarkdownEditor {
|
||||
setupTextarea() {
|
||||
this.textarea = this.container.querySelector('.markdown-text-editor');
|
||||
this.textarea._giteaComboMarkdownEditor = this;
|
||||
this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`;
|
||||
this.textarea.id = generateElemId(`_combo_markdown_editor_`);
|
||||
this.textarea.addEventListener('input', () => triggerEditorContentChanged(this.container));
|
||||
this.applyEditorHeights(this.textarea, this.options.editorHeights);
|
||||
|
||||
@@ -213,16 +211,16 @@ export class ComboMarkdownEditor {
|
||||
|
||||
// Fomantic Tab requires the "data-tab" to be globally unique.
|
||||
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
|
||||
const tabIdSuffix = generateElemId();
|
||||
this.tabEditor = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-writer');
|
||||
this.tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer');
|
||||
this.tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
|
||||
this.tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
|
||||
this.tabEditor.setAttribute('data-tab', `markdown-writer-${tabIdSuffix}`);
|
||||
this.tabPreviewer.setAttribute('data-tab', `markdown-previewer-${tabIdSuffix}`);
|
||||
|
||||
const panelEditor = this.container.querySelector('.ui.tab[data-tab-panel="markdown-writer"]');
|
||||
const panelPreviewer = this.container.querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
|
||||
panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
|
||||
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
|
||||
elementIdCounter++;
|
||||
panelEditor.setAttribute('data-tab', `markdown-writer-${tabIdSuffix}`);
|
||||
panelPreviewer.setAttribute('data-tab', `markdown-previewer-${tabIdSuffix}`);
|
||||
|
||||
this.tabEditor.addEventListener('click', () => {
|
||||
requestAnimationFrame(() => {
|
||||
|
@@ -18,7 +18,7 @@ export function initRepoGraphGit() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('mode', mode);
|
||||
window.history.replaceState(null, '', `?${params.toString()}`);
|
||||
for (const link of document.querySelectorAll('#pagination .pagination a')) {
|
||||
for (const link of document.querySelectorAll('#git-graph-body .pagination a')) {
|
||||
const href = link.getAttribute('href');
|
||||
if (!href) continue;
|
||||
const url = new URL(href, window.location.href);
|
||||
|
@@ -1,9 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
let ariaIdCounter = 0;
|
||||
|
||||
export function generateAriaId() {
|
||||
return `_aria_auto_id_${ariaIdCounter++}`;
|
||||
}
|
||||
import {generateElemId} from '../../utils/dom.ts';
|
||||
|
||||
export function linkLabelAndInput(label: Element, input: Element) {
|
||||
const labelFor = label.getAttribute('for');
|
||||
@@ -12,7 +8,7 @@ export function linkLabelAndInput(label: Element, input: Element) {
|
||||
if (inputId && !labelFor) { // missing "for"
|
||||
label.setAttribute('for', inputId);
|
||||
} else if (!inputId && !labelFor) { // missing both "id" and "for"
|
||||
const id = generateAriaId();
|
||||
const id = generateElemId('_aria_label_input_');
|
||||
input.setAttribute('id', id);
|
||||
label.setAttribute('for', id);
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import $ from 'jquery';
|
||||
import {generateAriaId} from './base.ts';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
import {queryElems} from '../../utils/dom.ts';
|
||||
import {generateElemId, queryElems} from '../../utils/dom.ts';
|
||||
|
||||
const ariaPatchKey = '_giteaAriaPatchDropdown';
|
||||
const fomanticDropdownFn = $.fn.dropdown;
|
||||
@@ -47,7 +46,7 @@ function ariaDropdownFn(this: any, ...args: Parameters<FomanticInitFunction>) {
|
||||
// make the item has role=option/menuitem, add an id if there wasn't one yet, make items as non-focusable
|
||||
// the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
|
||||
function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) {
|
||||
if (!item.id) item.id = generateAriaId();
|
||||
if (!item.id) item.id = generateElemId('_aria_dropdown_item_');
|
||||
item.setAttribute('role', (dropdown as any)[ariaPatchKey].listItemRole);
|
||||
item.setAttribute('tabindex', '-1');
|
||||
for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1');
|
||||
@@ -59,7 +58,7 @@ function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) {
|
||||
function updateSelectionLabel(label: HTMLElement) {
|
||||
// the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
|
||||
if (!label.id) {
|
||||
label.id = generateAriaId();
|
||||
label.id = generateElemId('_aria_dropdown_label_');
|
||||
}
|
||||
label.tabIndex = -1;
|
||||
|
||||
@@ -127,7 +126,7 @@ function delegateDropdownModule($dropdown: any) {
|
||||
function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, menu: HTMLElement) {
|
||||
// prepare static dropdown menu list popup
|
||||
if (!menu.id) {
|
||||
menu.id = generateAriaId();
|
||||
menu.id = generateElemId('_aria_dropdown_menu_');
|
||||
}
|
||||
|
||||
$(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item));
|
||||
|
@@ -290,14 +290,12 @@ export function isElemVisible(el: HTMLElement): boolean {
|
||||
export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) {
|
||||
const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
|
||||
const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
|
||||
let success = true;
|
||||
let success = false;
|
||||
|
||||
textarea.contentEditable = 'true';
|
||||
try {
|
||||
success = document.execCommand('insertText', false, text); // eslint-disable-line @typescript-eslint/no-deprecated
|
||||
} catch {
|
||||
success = false;
|
||||
}
|
||||
} catch {} // ignore the error if execCommand is not supported or failed
|
||||
textarea.contentEditable = 'false';
|
||||
|
||||
if (success && !textarea.value.slice(0, textarea.selectionStart ?? undefined).endsWith(text)) {
|
||||
@@ -310,11 +308,10 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st
|
||||
}
|
||||
}
|
||||
|
||||
// Warning: Do not enter any unsanitized variables here
|
||||
export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T {
|
||||
htmlString = htmlString.trim();
|
||||
// some tags like "tr" are special, it must use a correct parent container to create
|
||||
// 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
|
||||
// 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
|
||||
// eslint-disable-next-line github/unescaped-html-literal
|
||||
if (htmlString.startsWith('<tr')) {
|
||||
const container = document.createElement('table');
|
||||
container.innerHTML = htmlString;
|
||||
@@ -364,14 +361,19 @@ export function addDelegatedEventListener<T extends HTMLElement, E extends Event
|
||||
const elem = (e.target as HTMLElement).closest(selector);
|
||||
// It strictly checks "parent contains the target elem" to avoid side effects of selector running on outside the parent.
|
||||
// Keep in mind that the elem could have been removed from parent by other event handlers before this event handler is called.
|
||||
// For example: tippy popup item, the tippy popup could be hidden and removed from DOM before this.
|
||||
// It is caller's responsibility make sure the elem is still in parent's DOM when this event handler is called.
|
||||
// For example, tippy popup item, the tippy popup could be hidden and removed from DOM before this.
|
||||
// It is the caller's responsibility to make sure the elem is still in parent's DOM when this event handler is called.
|
||||
if (!elem || (parent !== document && !parent.contains(elem))) return;
|
||||
listener(elem as T, e as E);
|
||||
}, options);
|
||||
}
|
||||
|
||||
/** Returns whether a click event is a left-click without any modifiers held */
|
||||
// Returns whether a click event is a left-click without any modifiers held
|
||||
export function isPlainClick(e: MouseEvent) {
|
||||
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
|
||||
}
|
||||
|
||||
let elemIdCounter = 0;
|
||||
export function generateElemId(prefix: string = ''): string {
|
||||
return `${prefix}${elemIdCounter++}`;
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ export function html(tmpl: TemplateStringsArray, ...parts: Array<any>): string {
|
||||
let output = tmpl[0];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const value = parts[i];
|
||||
const valueEscaped = (value instanceof rawObject) ? value.toString() : htmlEscape(String(parts[i]));
|
||||
const valueEscaped = (value instanceof rawObject) ? value.toString() : htmlEscape(String(value));
|
||||
output = output + valueEscaped + tmpl[i + 1];
|
||||
}
|
||||
return output;
|
||||
|
Reference in New Issue
Block a user