Skip to content

Commit 9f6bc7c

Browse files
authored
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">
1 parent 8bc8ca1 commit 9f6bc7c

File tree

6 files changed

+172
-5
lines changed

6 files changed

+172
-5
lines changed

package-lock.json

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@citation-js/plugin-software-formats": "0.6.1",
1414
"@claviska/jquery-minicolors": "2.3.6",
1515
"@github/markdown-toolbar-element": "2.1.1",
16+
"@github/text-expander-element": "2.3.0",
1617
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
1718
"@primer/octicons": "18.3.0",
1819
"@vue/compiler-sfc": "3.2.47",

templates/shared/combomarkdowneditor.tmpl

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ Template Attributes:
3939
<span class="markdown-toolbar-button markdown-switch-easymde">{{svg "octicon-arrow-switch"}}</span>
4040
</div>
4141
</markdown-toolbar>
42-
<textarea class="markdown-text-editor js-quick-submit" name="{{.TextareaName}}" placeholder="{{.TextareaPlaceholder}}">{{.TextareaContent}}</textarea>
42+
<text-expander keys=": @">
43+
<textarea class="markdown-text-editor js-quick-submit" name="{{.TextareaName}}" placeholder="{{.TextareaPlaceholder}}">{{.TextareaContent}}</textarea>
44+
</text-expander>
4345
</div>
4446
<div class="ui tab markup" data-tab-panel="markdown-previewer">
4547
{{.locale.Tr "loading"}}

web_src/css/editor-markdown.css

+63
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,66 @@
3030
.combo-markdown-editor .CodeMirror-scroll {
3131
max-height: calc(100vh - 200px);
3232
}
33+
34+
text-expander {
35+
display: block;
36+
position: relative;
37+
}
38+
39+
text-expander .suggestions {
40+
position: absolute;
41+
min-width: 180px;
42+
padding: 0;
43+
margin-top: 24px;
44+
list-style: none;
45+
background: var(--color-box-body);
46+
border-radius: 5px;
47+
border: 1px solid var(--color-secondary);
48+
box-shadow: 0 .5rem 1rem var(--color-shadow);
49+
}
50+
51+
text-expander .suggestions li {
52+
display: flex;
53+
align-items: center;
54+
cursor: pointer;
55+
padding: 4px 8px;
56+
font-weight: 500;
57+
}
58+
59+
text-expander .suggestions li + li {
60+
border-top: 1px solid var(--color-secondary-alpha-40);
61+
}
62+
63+
text-expander .suggestions li:first-child {
64+
border-radius: 4px 4px 0 0;
65+
}
66+
67+
text-expander .suggestions li:last-child {
68+
border-radius: 0 0 4px 4px;
69+
}
70+
71+
text-expander .suggestions li:only-child {
72+
border-radius: 4px;
73+
}
74+
75+
text-expander .suggestions li:hover {
76+
background: var(--color-hover);
77+
}
78+
79+
text-expander .suggestions .fullname {
80+
font-weight: normal;
81+
margin-left: 4px;
82+
color: var(--color-text-light-1);
83+
}
84+
85+
text-expander .suggestions li[aria-selected="true"],
86+
text-expander .suggestions li[aria-selected="true"] span {
87+
background: var(--color-primary);
88+
color: var(--color-primary-contrast);
89+
}
90+
91+
text-expander .suggestions img {
92+
width: 24px;
93+
height: 24px;
94+
margin-right: 8px;
95+
}

web_src/css/form.css

+17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
.ui.input textarea,
2+
.ui.form textarea,
3+
.ui.form input:not([type]),
4+
.ui.form input[type="date"],
5+
.ui.form input[type="datetime-local"],
6+
.ui.form input[type="email"],
7+
.ui.form input[type="number"],
8+
.ui.form input[type="password"],
9+
.ui.form input[type="search"],
10+
.ui.form input[type="tel"],
11+
.ui.form input[type="time"],
12+
.ui.form input[type="text"],
13+
.ui.form input[type="file"],
14+
.ui.form input[type="url"] {
15+
transition: none;
16+
}
17+
118
input,
219
textarea,
320
.ui.input > input,

web_src/js/features/comp/ComboMarkdownEditor.js

+74-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import '@github/markdown-toolbar-element';
2+
import '@github/text-expander-element';
23
import $ from 'jquery';
34
import {attachTribute} from '../tribute.js';
45
import {hideElem, showElem, autosize} from '../../utils/dom.js';
56
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
67
import {initMarkupContent} from '../../markup/content.js';
78
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
89
import {attachRefIssueContextPopup} from '../contextpopup.js';
10+
import {emojiKeys, emojiString} from '../emoji.js';
911

1012
let elementIdCounter = 0;
13+
const maxExpanderMatches = 6;
1114

1215
/**
1316
* validate if the given textarea is non-empty.
@@ -40,13 +43,10 @@ class ComboMarkdownEditor {
4043

4144
async init() {
4245
this.prepareEasyMDEToolbarActions();
43-
4446
this.setupTab();
4547
this.setupDropzone();
46-
4748
this.setupTextarea();
48-
49-
await attachTribute(this.textarea, {mentions: true, emoji: true});
49+
this.setupExpander();
5050

5151
if (this.userPreferredEditor === 'easymde') {
5252
await this.switchToEasyMDE();
@@ -83,6 +83,76 @@ class ComboMarkdownEditor {
8383
}
8484
}
8585

86+
setupExpander() {
87+
const expander = this.container.querySelector('text-expander');
88+
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
89+
if (key === ':') {
90+
const matches = [];
91+
for (const name of emojiKeys) {
92+
if (name.includes(text)) {
93+
matches.push(name);
94+
if (matches.length >= maxExpanderMatches) break;
95+
}
96+
}
97+
if (!matches.length) return provide({matched: false});
98+
99+
const ul = document.createElement('ul');
100+
ul.classList.add('suggestions');
101+
for (const name of matches) {
102+
const emoji = emojiString(name);
103+
const li = document.createElement('li');
104+
li.setAttribute('role', 'option');
105+
li.setAttribute('data-value', emoji);
106+
li.textContent = `${emoji} ${name}`;
107+
ul.append(li);
108+
}
109+
110+
provide({matched: true, fragment: ul});
111+
} else if (key === '@') {
112+
const matches = [];
113+
for (const obj of window.config.tributeValues) {
114+
if (obj.key.includes(text)) {
115+
matches.push(obj);
116+
if (matches.length >= maxExpanderMatches) break;
117+
}
118+
}
119+
if (!matches.length) return provide({matched: false});
120+
121+
const ul = document.createElement('ul');
122+
ul.classList.add('suggestions');
123+
for (const {value, name, fullname, avatar} of matches) {
124+
const li = document.createElement('li');
125+
li.setAttribute('role', 'option');
126+
li.setAttribute('data-value', `${key}${value}`);
127+
128+
const img = document.createElement('img');
129+
img.src = avatar;
130+
li.append(img);
131+
132+
const nameSpan = document.createElement('span');
133+
nameSpan.textContent = name;
134+
li.append(nameSpan);
135+
136+
if (fullname && fullname.toLowerCase() !== name) {
137+
const fullnameSpan = document.createElement('span');
138+
fullnameSpan.classList.add('fullname');
139+
fullnameSpan.textContent = fullname;
140+
li.append(fullnameSpan);
141+
}
142+
143+
ul.append(li);
144+
}
145+
146+
provide({matched: true, fragment: ul});
147+
}
148+
});
149+
expander?.addEventListener('text-expander-value', ({detail}) => {
150+
if (detail?.item) {
151+
detail.value = detail.item.getAttribute('data-value');
152+
}
153+
});
154+
}
155+
86156
setupDropzone() {
87157
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
88158
if (dropzoneParentContainer) {

0 commit comments

Comments
 (0)