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">tags/v1.20.0-rc0
@@ -13,6 +13,7 @@ | |||
"@citation-js/plugin-software-formats": "0.6.1", | |||
"@claviska/jquery-minicolors": "2.3.6", | |||
"@github/markdown-toolbar-element": "2.1.1", | |||
"@github/text-expander-element": "2.3.0", | |||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", | |||
"@primer/octicons": "18.3.0", | |||
"@vue/compiler-sfc": "3.2.47", | |||
@@ -840,11 +841,24 @@ | |||
"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": { | |||
"version": "2.1.1", | |||
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.1.1.tgz", | |||
"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": { | |||
"version": "0.11.8", | |||
"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", | |||
"@claviska/jquery-minicolors": "2.3.6", | |||
"@github/markdown-toolbar-element": "2.1.1", | |||
"@github/text-expander-element": "2.3.0", | |||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", | |||
"@primer/octicons": "18.3.0", | |||
"@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> | |||
</div> | |||
</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 class="ui tab markup" data-tab-panel="markdown-previewer"> | |||
{{.locale.Tr "loading"}} |
@@ -30,3 +30,66 @@ | |||
.combo-markdown-editor .CodeMirror-scroll { | |||
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, | |||
textarea, | |||
.ui.input > input, |
@@ -1,4 +1,5 @@ | |||
import '@github/markdown-toolbar-element'; | |||
import '@github/text-expander-element'; | |||
import $ from 'jquery'; | |||
import {attachTribute} from '../tribute.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 {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; | |||
import {attachRefIssueContextPopup} from '../contextpopup.js'; | |||
import {emojiKeys, emojiString} from '../emoji.js'; | |||
let elementIdCounter = 0; | |||
const maxExpanderMatches = 6; | |||
/** | |||
* validate if the given textarea is non-empty. | |||
@@ -40,13 +43,10 @@ class ComboMarkdownEditor { | |||
async init() { | |||
this.prepareEasyMDEToolbarActions(); | |||
this.setupTab(); | |||
this.setupDropzone(); | |||
this.setupTextarea(); | |||
await attachTribute(this.textarea, {mentions: true, emoji: true}); | |||
this.setupExpander(); | |||
if (this.userPreferredEditor === 'easymde') { | |||
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() { | |||
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container'); | |||
if (dropzoneParentContainer) { |