mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	@@ -7,7 +7,20 @@ import {getIssueColor, getIssueIcon} from '../issue.ts';
 | 
				
			|||||||
import {debounce} from 'perfect-debounce';
 | 
					import {debounce} from 'perfect-debounce';
 | 
				
			||||||
import type TextExpanderElement from '@github/text-expander-element';
 | 
					import type TextExpanderElement from '@github/text-expander-element';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
 | 
					type TextExpanderProvideResult = {
 | 
				
			||||||
 | 
					  matched: boolean,
 | 
				
			||||||
 | 
					  fragment?: HTMLElement,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TextExpanderChangeEvent = Event & {
 | 
				
			||||||
 | 
					  detail?: {
 | 
				
			||||||
 | 
					    key: string,
 | 
				
			||||||
 | 
					    text: string,
 | 
				
			||||||
 | 
					    provide: (result: TextExpanderProvideResult | Promise<TextExpanderProvideResult>) => void,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function fetchIssueSuggestions(key: string, text: string): Promise<TextExpanderProvideResult> {
 | 
				
			||||||
  const issuePathInfo = parseIssueHref(window.location.href);
 | 
					  const issuePathInfo = parseIssueHref(window.location.href);
 | 
				
			||||||
  if (!issuePathInfo.ownerName) {
 | 
					  if (!issuePathInfo.ownerName) {
 | 
				
			||||||
    const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
 | 
					    const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
 | 
				
			||||||
@@ -15,10 +28,10 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi
 | 
				
			|||||||
    issuePathInfo.repoName = repoOwnerPathInfo.repoName;
 | 
					    issuePathInfo.repoName = repoOwnerPathInfo.repoName;
 | 
				
			||||||
    // then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
 | 
					    // then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (!issuePathInfo.ownerName) return resolve({matched: false});
 | 
					  if (!issuePathInfo.ownerName) return {matched: false};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text);
 | 
					  const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text);
 | 
				
			||||||
  if (!matches.length) return resolve({matched: false});
 | 
					  if (!matches.length) return {matched: false};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ul = createElementFromAttrs('ul', {class: 'suggestions'});
 | 
					  const ul = createElementFromAttrs('ul', {class: 'suggestions'});
 | 
				
			||||||
  for (const issue of matches) {
 | 
					  for (const issue of matches) {
 | 
				
			||||||
@@ -30,11 +43,40 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    ul.append(li);
 | 
					    ul.append(li);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  resolve({matched: true, fragment: ul});
 | 
					  return {matched: true, fragment: ul};
 | 
				
			||||||
}), 100);
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initTextExpander(expander: TextExpanderElement) {
 | 
					export function initTextExpander(expander: TextExpanderElement) {
 | 
				
			||||||
  expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}: Record<string, any>) => {
 | 
					  if (!expander) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const textarea = expander.querySelector<HTMLTextAreaElement>('textarea');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // help to fix the text-expander "multiword+promise" bug: do not show the popup when there is no "#" before current line
 | 
				
			||||||
 | 
					  const shouldShowIssueSuggestions = () => {
 | 
				
			||||||
 | 
					    const posVal = textarea.value.substring(0, textarea.selectionStart);
 | 
				
			||||||
 | 
					    const lineStart = posVal.lastIndexOf('\n');
 | 
				
			||||||
 | 
					    const keyStart = posVal.lastIndexOf('#');
 | 
				
			||||||
 | 
					    return keyStart > lineStart;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const debouncedIssueSuggestions = debounce(async (key: string, text: string): Promise<TextExpanderProvideResult> => {
 | 
				
			||||||
 | 
					    // https://github.com/github/text-expander-element/issues/71
 | 
				
			||||||
 | 
					    // Upstream bug: when using "multiword+promise", TextExpander will get wrong "key" position.
 | 
				
			||||||
 | 
					    // To reproduce, comment out the "shouldShowIssueSuggestions" check, use the "await sleep" below,
 | 
				
			||||||
 | 
					    // then use content "close #20\nclose #20\nclose #20" (3 lines), keep changing the last line `#20` part from the end (including removing the `#`)
 | 
				
			||||||
 | 
					    // There will be a JS error: Uncaught (in promise) IndexSizeError: Failed to execute 'setStart' on 'Range': The offset 28 is larger than the node's length (27).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check the input before the request, to avoid emitting empty query to backend (still related to the upstream bug)
 | 
				
			||||||
 | 
					    if (!shouldShowIssueSuggestions()) return {matched: false};
 | 
				
			||||||
 | 
					    // await sleep(Math.random() * 1000); // help to reproduce the text-expander bug
 | 
				
			||||||
 | 
					    const ret = await fetchIssueSuggestions(key, text);
 | 
				
			||||||
 | 
					    // check the input again to avoid text-expander using incorrect position (upstream bug)
 | 
				
			||||||
 | 
					    if (!shouldShowIssueSuggestions()) return {matched: false};
 | 
				
			||||||
 | 
					    return ret;
 | 
				
			||||||
 | 
					  }, 300); // to match onInputDebounce delay
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  expander.addEventListener('text-expander-change', (e: TextExpanderChangeEvent) => {
 | 
				
			||||||
 | 
					    const {key, text, provide} = e.detail;
 | 
				
			||||||
    if (key === ':') {
 | 
					    if (key === ':') {
 | 
				
			||||||
      const matches = matchEmoji(text);
 | 
					      const matches = matchEmoji(text);
 | 
				
			||||||
      if (!matches.length) return provide({matched: false});
 | 
					      if (!matches.length) return provide({matched: false});
 | 
				
			||||||
@@ -82,10 +124,11 @@ export function initTextExpander(expander: TextExpanderElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      provide({matched: true, fragment: ul});
 | 
					      provide({matched: true, fragment: ul});
 | 
				
			||||||
    } else if (key === '#') {
 | 
					    } else if (key === '#') {
 | 
				
			||||||
      provide(debouncedSuggestIssues(key, text));
 | 
					      provide(debouncedIssueSuggestions(key, text));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
 | 
					
 | 
				
			||||||
 | 
					  expander.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
 | 
				
			||||||
    if (detail?.item) {
 | 
					    if (detail?.item) {
 | 
				
			||||||
      // add a space after @mentions and #issue as it's likely the user wants one
 | 
					      // add a space after @mentions and #issue as it's likely the user wants one
 | 
				
			||||||
      const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';
 | 
					      const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user