mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Enhance autocomplete and choice colors
Summary: Choice columns can now add new choices directly from the autocomplete menu. The autocomplete will now highlight the first matching item, even if there are equally ranked alternatives. No changes have been made to how the autocomplete index is created, or how it scores items. For choice and choice list columns, the filter menu will now display values using their configured colors, similar to the rest of the UI. Choice tokens throughout the UI now do a better job of handling text overflow by showing an ellipsis whenever there isn't enough space to show the full text of a choice. Test Plan: Browser tests. Reviewers: cyprien Reviewed By: cyprien Differential Revision: https://phab.getgrist.com/D2904
This commit is contained in:
@@ -132,12 +132,10 @@ export class ACIndexImpl<Item extends ACItem> implements ACIndex<Item> {
|
||||
|
||||
const highlightFunc = highlightMatches.bind(null, searchWords);
|
||||
|
||||
// The best match is the first item. If it actually starts with the search text, AND has a
|
||||
// strictly better score than other items, highlight it as a default selection. Otherwise, no
|
||||
// item will be auto-selected.
|
||||
// The best match is the first item. If any word in the item actually starts with the search
|
||||
// text, highlight it as a default selection. Otherwise, no item will be auto-selected.
|
||||
let selectIndex = -1;
|
||||
if (items.length > 0 && items[0].cleanText.startsWith(cleanedSearchText) &&
|
||||
(sortedMatches.length <= 1 || sortedMatches[1][1] < sortedMatches[0][1])) {
|
||||
if (items.length > 0 && sortedMatches.length > 0 && startsWithText(items[0], cleanedSearchText)) {
|
||||
selectIndex = 0;
|
||||
}
|
||||
return {items, highlightFunc, selectIndex};
|
||||
@@ -248,3 +246,13 @@ function findCommonPrefixLength(text1: string, text2: string): number {
|
||||
while (i < text1.length && text1[i] === text2[i]) { ++i; }
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `item` starts with `text`, or has any words that start with `text`.
|
||||
*/
|
||||
function startsWithText(item: ACItem, text: string): boolean {
|
||||
if (item.cleanText.startsWith(text)) { return true; }
|
||||
|
||||
const words = item.cleanText.split(wordSepRegexp);
|
||||
return words.some(w => w.startsWith(text));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface ITokenFieldOptions<Token extends IToken> {
|
||||
openAutocompleteOnFocus?: boolean;
|
||||
styles?: ITokenFieldStyles;
|
||||
readonly?: boolean;
|
||||
trimLabels?: boolean;
|
||||
keyBindings?: ITokenFieldKeyBindings;
|
||||
|
||||
// Allows overriding how tokens are copied to the clipboard, or retrieved from it.
|
||||
@@ -101,7 +102,10 @@ export class TokenField<Token extends IToken = IToken> extends Disposable {
|
||||
const addSelectedItem = this._addSelectedItem.bind(this);
|
||||
const openAutocomplete = this._openAutocomplete.bind(this);
|
||||
this._acOptions = _options.acOptions && {..._options.acOptions, onClick: addSelectedItem};
|
||||
this._tokens.set(_options.initialValue.map(t => new TokenWrap(t)));
|
||||
|
||||
const initialTokens = _options.initialValue;
|
||||
this._maybeTrimLabels(initialTokens);
|
||||
this._tokens.set(initialTokens.map(t => new TokenWrap(t)));
|
||||
this.tokensObs = this.autoDispose(computedArray(this._tokens, t => t.token));
|
||||
this._keyBindings = {...defaultKeyBindings, ..._options.keyBindings};
|
||||
|
||||
@@ -191,12 +195,19 @@ export class TokenField<Token extends IToken = IToken> extends Disposable {
|
||||
return this._textInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of the text input.
|
||||
*/
|
||||
public getTextInputValue(): string {
|
||||
return this._options.trimLabels ? this._textInput.value.trim() : this._textInput.value;
|
||||
}
|
||||
|
||||
// The invisible input that has focus while we have some tokens selected.
|
||||
public getHiddenInput(): HTMLInputElement {
|
||||
return this._hiddenInput;
|
||||
}
|
||||
|
||||
// Replaces a token (if it exists)
|
||||
// Replaces a token (if it exists).
|
||||
public replaceToken(label: string, newToken: Token): void {
|
||||
const tokenIdx = this._tokens.get().findIndex(t => t.token.label === label);
|
||||
if (tokenIdx === -1) { return; }
|
||||
@@ -216,8 +227,9 @@ export class TokenField<Token extends IToken = IToken> extends Disposable {
|
||||
// that; otherwise if options.createToken is present, creates a token from text input value.
|
||||
private _addSelectedItem(): boolean {
|
||||
let item: Token|undefined = this._acHolder.get()?.getSelectedItem();
|
||||
if (!item && this._options.createToken && this._textInput.value) {
|
||||
item = this._options.createToken(this._textInput.value);
|
||||
const textInput = this.getTextInputValue();
|
||||
if (!item && this._options.createToken && textInput) {
|
||||
item = this._options.createToken(textInput);
|
||||
}
|
||||
if (item) {
|
||||
this._tokens.push(new TokenWrap(item));
|
||||
@@ -421,6 +433,8 @@ export class TokenField<Token extends IToken = IToken> extends Disposable {
|
||||
tokens = values.map(v => this._options.createToken(v)).filter((t): t is Token => Boolean(t));
|
||||
}
|
||||
if (!tokens.length) { return; }
|
||||
this._maybeTrimLabels(tokens);
|
||||
tokens = this._getNonEmptyTokens(tokens);
|
||||
const wrappedTokens = tokens.map(t => new TokenWrap(t));
|
||||
this._combineUndo(() => {
|
||||
this._deleteTokens(this._selection.get(), 1);
|
||||
@@ -566,6 +580,25 @@ export class TokenField<Token extends IToken = IToken> extends Disposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims all labels in `tokens` if the option is set.
|
||||
*
|
||||
* Note: mutates `tokens`.
|
||||
*/
|
||||
private _maybeTrimLabels(tokens: Token[]): void {
|
||||
if (!this._options.trimLabels) { return; }
|
||||
tokens.forEach(t => {
|
||||
t.label = t.label.trim();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filtered array of tokens that don't have empty labels.
|
||||
*/
|
||||
private _getNonEmptyTokens(tokens: Token[]): Token[] {
|
||||
return tokens.filter(t => t.label !== '');
|
||||
}
|
||||
}
|
||||
|
||||
const cssTokenField = styled('div', `
|
||||
|
||||
Reference in New Issue
Block a user