diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae9dbd920978c..4838b9c1d4e1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -328,6 +328,12 @@ importers: ui/common: dependencies: + '@types/debounce-promise': + specifier: ^3.1.9 + version: 3.1.9 + debounce-promise: + specifier: ^3.1.2 + version: 3.1.2 lichess-pgn-viewer: specifier: ^2.1.0 version: 2.1.0 diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index 69b2a576e258d..5f746ef2ef149 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -27,7 +27,6 @@ interface Site { jsModule(name: string): string; loadIife(path: string, opts?: AssetUrlOpts): Promise; loadEsm(key: string, opts?: EsmModuleOpts): Promise; - userComplete(opts: UserCompleteOpts): Promise; }; pubsub: Pubsub; // file://./../../site/src/pubsub.ts unload: { expected: boolean }; @@ -88,8 +87,6 @@ type Flair = string; type RedirectTo = string | { url: string; cookie: Cookie }; -type UserComplete = (opts: UserCompleteOpts) => void; - interface LichessMousetrap { // file://./../../site/src/mousetrap.ts bind( @@ -108,19 +105,6 @@ interface LichessPowertip { manualUserIn(parent: HTMLElement): void; } -interface UserCompleteOpts { - input: HTMLInputElement; - tag?: 'a' | 'span'; - minLength?: number; - populate?: (result: LightUser) => string; - onSelect?: (result: LightUser) => void; - focus?: boolean; - friend?: boolean; - tour?: string; - swiss?: string; - team?: string; -} - interface QuestionChoice { // file://./../../round/src/ctrl.ts action: () => void; diff --git a/ui/analyse/src/explorer/explorerConfig.ts b/ui/analyse/src/explorer/explorerConfig.ts index c8e6d5b2e0a11..fab5b8913431b 100644 --- a/ui/analyse/src/explorer/explorerConfig.ts +++ b/ui/analyse/src/explorer/explorerConfig.ts @@ -11,6 +11,7 @@ import { ucfirst } from './explorerUtil'; import { Color } from 'chessground/types'; import { opposite } from 'chessground/util'; import { Redraw } from '../interfaces'; +import { userComplete } from 'common/userComplete'; const allSpeeds: ExplorerSpeed[] = ['ultraBullet', 'bullet', 'blitz', 'rapid', 'classical', 'correspondence']; const allModes: ExplorerMode[] = ['casual', 'rated']; @@ -349,10 +350,7 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => { spellcheck: 'false', }, hook: onInsert(input => - site.asset - .userComplete({ input, tag: 'span', onSelect: v => onSelect(v.name) }) - .then(() => input.focus()), - ), + userComplete({ input, focus: true, tag: 'span', onSelect: v => onSelect(v.name) })), }), ]), h( diff --git a/ui/analyse/src/study/inviteForm.ts b/ui/analyse/src/study/inviteForm.ts index 06daf75e541ae..9505bdf4e8f33 100644 --- a/ui/analyse/src/study/inviteForm.ts +++ b/ui/analyse/src/study/inviteForm.ts @@ -7,6 +7,7 @@ import { StudyMemberMap } from './interfaces'; import { AnalyseSocketSend } from '../socket'; import { storedSet, StoredSet } from 'common/storage'; import { snabDialog } from 'common/dialog'; +import { userComplete } from 'common/userComplete'; export interface StudyInviteFormCtrl { open: Prop; @@ -77,18 +78,17 @@ export function view(ctrl: ReturnType): VNode { // because typeahead messes up with snabbdom h('input', { attrs: { placeholder: ctrl.trans.noarg('searchByUsername'), spellcheck: 'false' }, - hook: onInsert(input => - site.asset - .userComplete({ - input, - tag: 'span', - onSelect(v) { - input.value = ''; - ctrl.invite(v.name); - ctrl.redraw(); - }, - }) - .then(() => input.focus()), + hook: onInsert(input => + userComplete({ + input, + focus: true, + tag: 'span', + onSelect(v) { + input.value = ''; + ctrl.invite(v.name); + ctrl.redraw(); + }, + }) ), }), ]), diff --git a/ui/bits/css/build/bits.complete.scss b/ui/bits/css/build/bits.complete.scss deleted file mode 100644 index 7ea9862f7bbfa..0000000000000 --- a/ui/bits/css/build/bits.complete.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '../../../common/css/plugin'; -@import '../complete'; diff --git a/ui/bits/package.json b/ui/bits/package.json index 77ed630da87ac..59f55e6d582b1 100644 --- a/ui/bits/package.json +++ b/ui/bits/package.json @@ -71,6 +71,7 @@ "src/bits.lpv.ts", "src/bits.passwordComplexity.ts", "src/bits.plan.ts", + "src/bits.polyglot.ts", "src/bits.publicChats.ts", "src/bits.qrcode.ts", "src/bits.relayForm.ts", @@ -78,15 +79,13 @@ "src/bits.streamer.ts", "src/bits.team.ts", "src/bits.teamBattleForm.ts", + "src/bits.titleRequest.ts", "src/bits.tourForm.ts", "src/bits.tvGames.ts", "src/bits.ublog.ts", "src/bits.ublogForm.ts", "src/bits.user.ts", - "src/bits.userComplete.ts", - "src/bits.userGamesDownload.ts", - "src/bits.titleRequest.ts", - "src/bits.polyglot.ts" + "src/bits.userGamesDownload.ts" ], "sync": { "node_modules/cropperjs/dist/cropper.min.css": "public/npm", diff --git a/ui/bits/src/bits.challengePage.ts b/ui/bits/src/bits.challengePage.ts index dae947b8c2eb9..858a970a84ce2 100644 --- a/ui/bits/src/bits.challengePage.ts +++ b/ui/bits/src/bits.challengePage.ts @@ -1,5 +1,6 @@ import * as xhr from 'common/xhr'; import StrongSocket from 'common/socket'; +import { userComplete } from 'common/userComplete'; interface ChallengeOpts { xhrUrl: string; @@ -45,7 +46,7 @@ export function initModule(opts: ChallengeOpts): void { .find('input.friend-autocomplete') .each(function(this: HTMLInputElement) { const input = this; - site.asset.userComplete({ + userComplete({ input: input, friend: true, tag: 'span', diff --git a/ui/bits/src/bits.clas.ts b/ui/bits/src/bits.clas.ts index 235b9feaa4ef3..0279554c52922 100644 --- a/ui/bits/src/bits.clas.ts +++ b/ui/bits/src/bits.clas.ts @@ -4,7 +4,7 @@ import * as xhr from 'common/xhr'; import { Textcomplete } from '@textcomplete/core'; import { TextareaEditor } from '@textcomplete/textarea'; -import type { Result as UserCompleteResult } from './bits.userComplete'; +import type { UserCompleteResult } from 'common/userComplete'; site.load.then(() => { $('table.sortable').each(function(this: HTMLElement) { diff --git a/ui/cli/src/cli.ts b/ui/cli/src/cli.ts index df390d34d0166..cfd98bf483e0d 100644 --- a/ui/cli/src/cli.ts +++ b/ui/cli/src/cli.ts @@ -1,9 +1,10 @@ import { load as loadDasher } from 'dasher'; import { domDialog } from 'common/dialog'; import { escapeHtml } from 'common'; +import { userComplete } from 'common/userComplete'; export function initModule({ input }: { input: HTMLInputElement }) { - site.asset.userComplete({ + userComplete({ input, friend: true, focus: true, diff --git a/ui/common/css/_lichess.scss b/ui/common/css/_lichess.scss index b16ea4ff68ae1..73fc6a44d70ff 100644 --- a/ui/common/css/_lichess.scss +++ b/ui/common/css/_lichess.scss @@ -22,6 +22,7 @@ @import 'component/board'; @import 'component/box'; @import 'component/button'; +@import 'component/complete'; @import 'component/user-link'; @import 'component/blind-mode'; @import 'component/friend-box'; diff --git a/ui/bits/css/_complete.scss b/ui/common/css/component/_complete.scss similarity index 100% rename from ui/bits/css/_complete.scss rename to ui/common/css/component/_complete.scss diff --git a/ui/common/package.json b/ui/common/package.json index 4fa502ae7ca27..fd8056e92d053 100644 --- a/ui/common/package.json +++ b/ui/common/package.json @@ -3,6 +3,12 @@ "version": "2.0.0", "private": true, "description": "lichess.org common utils", + "keywords": [ + "chess", + "lichess" + ], + "author": "Thibault Duplessis", + "license": "AGPL-3.0-or-later", "module": "common.js", "type": "module", "typings": "common", @@ -17,13 +23,9 @@ ] } }, - "keywords": [ - "chess", - "lichess" - ], - "author": "Thibault Duplessis", - "license": "AGPL-3.0-or-later", "dependencies": { + "@types/debounce-promise": "^3.1.9", + "debounce-promise": "^3.1.2", "lichess-pgn-viewer": "^2.1.0", "snabbdom": "3.5.1", "tablesort": "^5.3.0" diff --git a/ui/bits/src/bits.userComplete.ts b/ui/common/src/userComplete.ts similarity index 94% rename from ui/bits/src/bits.userComplete.ts rename to ui/common/src/userComplete.ts index 1f64adb99b507..664c98503b4dc 100644 --- a/ui/bits/src/bits.userComplete.ts +++ b/ui/common/src/userComplete.ts @@ -1,16 +1,16 @@ -import * as xhr from 'common/xhr'; +import * as xhr from './xhr'; import debounce from 'debounce-promise'; -export interface Result { +export interface UserCompleteResult { result: LightUserOnline[]; } -interface Opts { +export interface UserCompleteOpts { input: HTMLInputElement; tag?: 'a' | 'span'; minLength?: number; - populate?: (result: LightUserOnline) => string; - onSelect?: (result: LightUserOnline) => void; + populate?: (result: LightUser) => string; + onSelect?: (result: LightUser) => void; focus?: boolean; friend?: boolean; tour?: string; @@ -18,7 +18,7 @@ interface Opts { team?: string; } -export function initModule(opts: Opts): void { +export function userComplete(opts: UserCompleteOpts): void { const debounced = debounce( (term: string) => xhr @@ -32,7 +32,7 @@ export function initModule(opts: Opts): void { object: 1, }), ) - .then((r: Result) => ({ term, ...r })), + .then((r: UserCompleteResult) => ({ term, ...r })), 150, ); @@ -73,6 +73,7 @@ export function initModule(opts: Opts): void { onSelect: opts.onSelect, regex: /^[a-z][\w-]{2,29}$/i, }); + if (opts.focus) opts.input.focus(); } type Fetch = (term: string) => Promise; diff --git a/ui/mod/src/mod.teamAdmin.ts b/ui/mod/src/mod.teamAdmin.ts index 2163736a1bcca..131fbb553938e 100644 --- a/ui/mod/src/mod.teamAdmin.ts +++ b/ui/mod/src/mod.teamAdmin.ts @@ -1,6 +1,7 @@ import Tagify from '@yaireo/tagify'; import debounce from 'debounce-promise'; import * as xhr from 'common/xhr'; +import { userComplete } from 'common/userComplete'; site.load.then(() => { $('#form3-leaders').each(function(this: HTMLInputElement) { @@ -10,7 +11,7 @@ site.load.then(() => { initTagify(this, 100); }); $('form.team-add-leader input[name="name"]').each(function(this: HTMLInputElement) { - site.asset.userComplete({ + userComplete({ input: this, team: this.dataset.teamId, tag: 'span', @@ -20,7 +21,7 @@ site.load.then(() => { permissionsTable(this); }); $('form.team-declined-request input[name="search"]').each(function(this: HTMLInputElement) { - site.asset.userComplete({ + userComplete({ input: this, tag: 'span', }); diff --git a/ui/site/src/asset.ts b/ui/site/src/asset.ts index 3ca209788f3e7..631d5286ef05a 100644 --- a/ui/site/src/asset.ts +++ b/ui/site/src/asset.ts @@ -74,14 +74,6 @@ export const loadPageEsm = async(name: string) => { module.initModule ? module.initModule(opts) : module.default(opts); }; -export const userComplete = async(opts: UserCompleteOpts): Promise => { - const [userComplete] = await Promise.all([ - loadEsm('bits.userComplete', { init: opts }), - loadCssPath('bits.complete'), - ]); - return userComplete as UserComplete; -}; - export function embedChessground() { return import(url('npm/chessground.min.js')); } diff --git a/ui/site/src/boot.ts b/ui/site/src/boot.ts index dbdc4a44c87aa..9cf7aa4fc9e72 100644 --- a/ui/site/src/boot.ts +++ b/ui/site/src/boot.ts @@ -15,6 +15,7 @@ import watchers from './watchers'; import { isIOS } from 'common/device'; import { scrollToInnerSelector, requestIdleCallback } from 'common'; import { dispatchChessgroundResize } from 'common/resize'; +import { userComplete } from 'common/userComplete'; export function boot() { $('#user_tag').removeAttr('href'); @@ -77,7 +78,7 @@ export function boot() { $('.user-autocomplete').each(function(this: HTMLInputElement) { const focus = !!this.autofocus; const start = () => - site.asset.userComplete({ + userComplete({ input: this, friend: !!this.dataset.friend, tag: this.dataset.tag as any, diff --git a/ui/swiss/src/search.ts b/ui/swiss/src/search.ts index d39c458025c7b..f98013a2e6a28 100644 --- a/ui/swiss/src/search.ts +++ b/ui/swiss/src/search.ts @@ -2,6 +2,7 @@ import { h, VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { bind, onInsert } from 'common/snabbdom'; import TournamentController from './ctrl'; +import { userComplete } from 'common/userComplete'; export function button(ctrl: TournamentController): VNode { return h('button.fbt', { @@ -17,18 +18,16 @@ export function input(ctrl: TournamentController): VNode { h('input', { attrs: { spellcheck: 'false' }, hook: onInsert((el: HTMLInputElement) => { - site.asset - .userComplete({ - input: el, - swiss: ctrl.data.id, - tag: 'span', - focus: true, - onSelect(r) { - ctrl.jumpToPageOf(r.id); - ctrl.redraw(); - }, - }) - .then(() => el.focus()); + userComplete({ + input: el, + swiss: ctrl.data.id, + tag: 'span', + focus: true, + onSelect(r) { + ctrl.jumpToPageOf(r.id); + ctrl.redraw(); + }, + }); $(el).on('keydown', e => { if (e.code === 'Enter') { const rank = parseInt(e.target.value); diff --git a/ui/tournament/src/search.ts b/ui/tournament/src/search.ts index b6c20015f8b20..d88c8a91b96f5 100644 --- a/ui/tournament/src/search.ts +++ b/ui/tournament/src/search.ts @@ -2,6 +2,7 @@ import { h, VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { bind, onInsert } from 'common/snabbdom'; import TournamentController from './ctrl'; +import { userComplete } from 'common/userComplete'; export function button(ctrl: TournamentController): VNode { return h('button.fbt', { @@ -17,19 +18,16 @@ export function input(ctrl: TournamentController): VNode { h('input', { attrs: { spellcheck: 'false' }, hook: onInsert((el: HTMLInputElement) => { - site.asset - .userComplete({ - input: el, - tour: ctrl.data.id, - tag: 'span', - focus: true, - onSelect(v) { - ctrl.jumpToPageOf(v.id); - ctrl.redraw(); - }, - }) - .then(() => el.focus()); - + userComplete({ + input: el, + tour: ctrl.data.id, + tag: 'span', + focus: true, + onSelect(v) { + ctrl.jumpToPageOf(v.id); + ctrl.redraw(); + }, + }); $(el).on('keydown', e => { if (e.code === 'Enter') { const rank = parseInt(e.target.value.replace('#', '').trim());