mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00:00 
			
		
		
		
	Replace tribute with text-expander-element for textarea (#23985)
The completion popup now behaves now much more as expected than before for the raw textarea: - You can press <kbd>Tab</kbd> or <kbd>Enter</kbd> once the completion popup is open to accept the selected item - The menu does not close automatically when moving the cursor - When you delete text, previously correct suggestions are shown again - If you delete all text until the opening char (`@` or `:`) after applying a suggestion, the popup reappears again - Menu UI has been improved <img width="278" alt="Screenshot 2023-04-07 at 19 43 42" src="https://user-images.githubusercontent.com/115237/230653601-d6517b9f-0988-445e-aa57-5ebfaf5039f3.png">
This commit is contained in:
		
							
								
								
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -13,6 +13,7 @@ | |||||||
|         "@citation-js/plugin-software-formats": "0.6.1", |         "@citation-js/plugin-software-formats": "0.6.1", | ||||||
|         "@claviska/jquery-minicolors": "2.3.6", |         "@claviska/jquery-minicolors": "2.3.6", | ||||||
|         "@github/markdown-toolbar-element": "2.1.1", |         "@github/markdown-toolbar-element": "2.1.1", | ||||||
|  |         "@github/text-expander-element": "2.3.0", | ||||||
|         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", |         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", | ||||||
|         "@primer/octicons": "18.3.0", |         "@primer/octicons": "18.3.0", | ||||||
|         "@vue/compiler-sfc": "3.2.47", |         "@vue/compiler-sfc": "3.2.47", | ||||||
| @@ -840,11 +841,24 @@ | |||||||
|         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" |         "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@github/combobox-nav": { | ||||||
|  |       "version": "2.1.7", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.1.7.tgz", | ||||||
|  |       "integrity": "sha512-Webx0W5iTpkk5Chy9dB/1BEUORQ0qrwui8HaaVBiy75W2VOJg96WTuKj1rXENAJ3XTMhdEF53bn0LYfvP0EKvg==" | ||||||
|  |     }, | ||||||
|     "node_modules/@github/markdown-toolbar-element": { |     "node_modules/@github/markdown-toolbar-element": { | ||||||
|       "version": "2.1.1", |       "version": "2.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.1.1.tgz", |       "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.1.1.tgz", | ||||||
|       "integrity": "sha512-J++rpd5H9baztabJQB82h26jtueOeBRSTqetk9Cri+Lj/s28ndu6Tovn0uHQaOKtBWDobFunk9b5pP5vcqt7cA==" |       "integrity": "sha512-J++rpd5H9baztabJQB82h26jtueOeBRSTqetk9Cri+Lj/s28ndu6Tovn0uHQaOKtBWDobFunk9b5pP5vcqt7cA==" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@github/text-expander-element": { | ||||||
|  |       "version": "2.3.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz", | ||||||
|  |       "integrity": "sha512-E1KCxTOA/7Y4dP5g7vXbfRDFU6/SjU0TuJxx6JMwvxzI/NlBrXyXsx/fjFskD2T/un6i6CNKnXu1ZwExDLjcqw==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@github/combobox-nav": "^2.0.2" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@humanwhocodes/config-array": { |     "node_modules/@humanwhocodes/config-array": { | ||||||
|       "version": "0.11.8", |       "version": "0.11.8", | ||||||
|       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", |       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ | |||||||
|     "@citation-js/plugin-software-formats": "0.6.1", |     "@citation-js/plugin-software-formats": "0.6.1", | ||||||
|     "@claviska/jquery-minicolors": "2.3.6", |     "@claviska/jquery-minicolors": "2.3.6", | ||||||
|     "@github/markdown-toolbar-element": "2.1.1", |     "@github/markdown-toolbar-element": "2.1.1", | ||||||
|  |     "@github/text-expander-element": "2.3.0", | ||||||
|     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", |     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", | ||||||
|     "@primer/octicons": "18.3.0", |     "@primer/octicons": "18.3.0", | ||||||
|     "@vue/compiler-sfc": "3.2.47", |     "@vue/compiler-sfc": "3.2.47", | ||||||
|   | |||||||
| @@ -39,7 +39,9 @@ Template Attributes: | |||||||
| 				<span class="markdown-toolbar-button markdown-switch-easymde">{{svg "octicon-arrow-switch"}}</span> | 				<span class="markdown-toolbar-button markdown-switch-easymde">{{svg "octicon-arrow-switch"}}</span> | ||||||
| 			</div> | 			</div> | ||||||
| 		</markdown-toolbar> | 		</markdown-toolbar> | ||||||
| 		<textarea class="markdown-text-editor js-quick-submit" name="{{.TextareaName}}" placeholder="{{.TextareaPlaceholder}}">{{.TextareaContent}}</textarea> | 		<text-expander keys=": @"> | ||||||
|  | 			<textarea class="markdown-text-editor js-quick-submit" name="{{.TextareaName}}" placeholder="{{.TextareaPlaceholder}}">{{.TextareaContent}}</textarea> | ||||||
|  | 		</text-expander> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="ui tab markup" data-tab-panel="markdown-previewer"> | 	<div class="ui tab markup" data-tab-panel="markdown-previewer"> | ||||||
| 		{{.locale.Tr "loading"}} | 		{{.locale.Tr "loading"}} | ||||||
|   | |||||||
| @@ -30,3 +30,66 @@ | |||||||
| .combo-markdown-editor .CodeMirror-scroll { | .combo-markdown-editor .CodeMirror-scroll { | ||||||
|   max-height: calc(100vh - 200px); |   max-height: calc(100vh - 200px); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | text-expander { | ||||||
|  |   display: block; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions { | ||||||
|  |   position: absolute; | ||||||
|  |   min-width: 180px; | ||||||
|  |   padding: 0; | ||||||
|  |   margin-top: 24px; | ||||||
|  |   list-style: none; | ||||||
|  |   background: var(--color-box-body); | ||||||
|  |   border-radius: 5px; | ||||||
|  |   border: 1px solid var(--color-secondary); | ||||||
|  |   box-shadow: 0 .5rem 1rem var(--color-shadow); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions li { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   cursor: pointer; | ||||||
|  |   padding: 4px 8px; | ||||||
|  |   font-weight: 500; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions li + li { | ||||||
|  |   border-top: 1px solid var(--color-secondary-alpha-40); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions li:first-child { | ||||||
|  |   border-radius: 4px 4px 0 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions li:last-child { | ||||||
|  |   border-radius: 0 0 4px 4px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions li:only-child { | ||||||
|  |   border-radius: 4px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions li:hover { | ||||||
|  |   background: var(--color-hover); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions .fullname { | ||||||
|  |   font-weight: normal; | ||||||
|  |   margin-left: 4px; | ||||||
|  |   color: var(--color-text-light-1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions li[aria-selected="true"], | ||||||
|  | text-expander .suggestions li[aria-selected="true"] span { | ||||||
|  |   background: var(--color-primary); | ||||||
|  |   color: var(--color-primary-contrast); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | text-expander .suggestions img { | ||||||
|  |   width: 24px; | ||||||
|  |   height: 24px; | ||||||
|  |   margin-right: 8px; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,3 +1,20 @@ | |||||||
|  | .ui.input textarea, | ||||||
|  | .ui.form textarea, | ||||||
|  | .ui.form input:not([type]), | ||||||
|  | .ui.form input[type="date"], | ||||||
|  | .ui.form input[type="datetime-local"], | ||||||
|  | .ui.form input[type="email"], | ||||||
|  | .ui.form input[type="number"], | ||||||
|  | .ui.form input[type="password"], | ||||||
|  | .ui.form input[type="search"], | ||||||
|  | .ui.form input[type="tel"], | ||||||
|  | .ui.form input[type="time"], | ||||||
|  | .ui.form input[type="text"], | ||||||
|  | .ui.form input[type="file"], | ||||||
|  | .ui.form input[type="url"] { | ||||||
|  |   transition: none; | ||||||
|  | } | ||||||
|  |  | ||||||
| input, | input, | ||||||
| textarea, | textarea, | ||||||
| .ui.input > input, | .ui.input > input, | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import '@github/markdown-toolbar-element'; | import '@github/markdown-toolbar-element'; | ||||||
|  | import '@github/text-expander-element'; | ||||||
| import $ from 'jquery'; | import $ from 'jquery'; | ||||||
| import {attachTribute} from '../tribute.js'; | import {attachTribute} from '../tribute.js'; | ||||||
| import {hideElem, showElem, autosize} from '../../utils/dom.js'; | import {hideElem, showElem, autosize} from '../../utils/dom.js'; | ||||||
| @@ -6,8 +7,10 @@ import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js'; | |||||||
| import {initMarkupContent} from '../../markup/content.js'; | import {initMarkupContent} from '../../markup/content.js'; | ||||||
| import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; | import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; | ||||||
| import {attachRefIssueContextPopup} from '../contextpopup.js'; | import {attachRefIssueContextPopup} from '../contextpopup.js'; | ||||||
|  | import {emojiKeys, emojiString} from '../emoji.js'; | ||||||
|  |  | ||||||
| let elementIdCounter = 0; | let elementIdCounter = 0; | ||||||
|  | const maxExpanderMatches = 6; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * validate if the given textarea is non-empty. |  * validate if the given textarea is non-empty. | ||||||
| @@ -40,13 +43,10 @@ class ComboMarkdownEditor { | |||||||
|  |  | ||||||
|   async init() { |   async init() { | ||||||
|     this.prepareEasyMDEToolbarActions(); |     this.prepareEasyMDEToolbarActions(); | ||||||
|  |  | ||||||
|     this.setupTab(); |     this.setupTab(); | ||||||
|     this.setupDropzone(); |     this.setupDropzone(); | ||||||
|  |  | ||||||
|     this.setupTextarea(); |     this.setupTextarea(); | ||||||
|  |     this.setupExpander(); | ||||||
|     await attachTribute(this.textarea, {mentions: true, emoji: true}); |  | ||||||
|  |  | ||||||
|     if (this.userPreferredEditor === 'easymde') { |     if (this.userPreferredEditor === 'easymde') { | ||||||
|       await this.switchToEasyMDE(); |       await this.switchToEasyMDE(); | ||||||
| @@ -83,6 +83,76 @@ class ComboMarkdownEditor { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   setupExpander() { | ||||||
|  |     const expander = this.container.querySelector('text-expander'); | ||||||
|  |     expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { | ||||||
|  |       if (key === ':') { | ||||||
|  |         const matches = []; | ||||||
|  |         for (const name of emojiKeys) { | ||||||
|  |           if (name.includes(text)) { | ||||||
|  |             matches.push(name); | ||||||
|  |             if (matches.length >= maxExpanderMatches) break; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (!matches.length) return provide({matched: false}); | ||||||
|  |  | ||||||
|  |         const ul = document.createElement('ul'); | ||||||
|  |         ul.classList.add('suggestions'); | ||||||
|  |         for (const name of matches) { | ||||||
|  |           const emoji = emojiString(name); | ||||||
|  |           const li = document.createElement('li'); | ||||||
|  |           li.setAttribute('role', 'option'); | ||||||
|  |           li.setAttribute('data-value', emoji); | ||||||
|  |           li.textContent = `${emoji} ${name}`; | ||||||
|  |           ul.append(li); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         provide({matched: true, fragment: ul}); | ||||||
|  |       } else if (key === '@') { | ||||||
|  |         const matches = []; | ||||||
|  |         for (const obj of window.config.tributeValues) { | ||||||
|  |           if (obj.key.includes(text)) { | ||||||
|  |             matches.push(obj); | ||||||
|  |             if (matches.length >= maxExpanderMatches) break; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (!matches.length) return provide({matched: false}); | ||||||
|  |  | ||||||
|  |         const ul = document.createElement('ul'); | ||||||
|  |         ul.classList.add('suggestions'); | ||||||
|  |         for (const {value, name, fullname, avatar} of matches) { | ||||||
|  |           const li = document.createElement('li'); | ||||||
|  |           li.setAttribute('role', 'option'); | ||||||
|  |           li.setAttribute('data-value', `${key}${value}`); | ||||||
|  |  | ||||||
|  |           const img = document.createElement('img'); | ||||||
|  |           img.src = avatar; | ||||||
|  |           li.append(img); | ||||||
|  |  | ||||||
|  |           const nameSpan = document.createElement('span'); | ||||||
|  |           nameSpan.textContent = name; | ||||||
|  |           li.append(nameSpan); | ||||||
|  |  | ||||||
|  |           if (fullname && fullname.toLowerCase() !== name) { | ||||||
|  |             const fullnameSpan = document.createElement('span'); | ||||||
|  |             fullnameSpan.classList.add('fullname'); | ||||||
|  |             fullnameSpan.textContent = fullname; | ||||||
|  |             li.append(fullnameSpan); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           ul.append(li); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         provide({matched: true, fragment: ul}); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     expander?.addEventListener('text-expander-value', ({detail}) => { | ||||||
|  |       if (detail?.item) { | ||||||
|  |         detail.value = detail.item.getAttribute('data-value'); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   setupDropzone() { |   setupDropzone() { | ||||||
|     const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container'); |     const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container'); | ||||||
|     if (dropzoneParentContainer) { |     if (dropzoneParentContainer) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user