refactor dialogs

This commit is contained in:
Jonathan Gamble
2023-09-06 18:15:29 -05:00
committed by Jonathan Gamble
parent 8d769b28e1
commit db1b01ac8c
105 changed files with 684 additions and 684 deletions
+2 -7
View File
@@ -14,13 +14,7 @@ project/metals.sbt
project/project
project/target
public/compiled
public/vendor/highcharts-4.2.5
public/vendor/hopscotch
public/vendor/tagify
public/vendor/stockfish.wasm
public/vendor/stockfish-nnue.wasm
public/vendor/stockfish-mv.wasm
public/vendor/stockfish.js
public/npm
public/lifat
public/css/
target
@@ -45,6 +39,7 @@ hs_*.log
dependency-graph.png
RUNNING_PID
.DS_Store
# Eclipse auto-generated files
/modules/*/.classpath
+3 -2
View File
@@ -84,8 +84,9 @@ if (window.matchMedia('(prefers-color-scheme: dark)').media === 'not all')
def captchaTag = jsModule("captcha")
def cashTag = iifeModule("javascripts/vendor/cash.min.js")
def fingerprintTag = iifeModule("javascripts/fipr.js")
def highchartsLatestTag = iifeModule("vendor/highcharts-4.2.5/highcharts.js")
def highchartsMoreTag = iifeModule("vendor/highcharts-4.2.5/highcharts-more.js")
def highchartsLatestTag = iifeModule("npm/highcharts-4.2.5/highcharts.js")
def highchartsMoreTag = iifeModule("npm/highcharts-4.2.5/highcharts-more.js")
def chessgroundTag = script(tpe := "module", src := assetUrl("npm/chessground.min.js"))
def prismicJs(using PageContext): Frag =
raw:
+1 -1
View File
@@ -237,7 +237,7 @@ object help:
row(voice("help"), trans.showHelpDialog()),
tr(
td,
td(button(cls := "button", id := "all-phrases-button")("Show me everything"))
td(button(cls := "button", cls := "all-phrases-button")("Show me everything"))
)
)
)
+8 -1
View File
@@ -1,4 +1,4 @@
lockfileVersion: '6.0'
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
@@ -214,6 +214,9 @@ importers:
ui/common:
dependencies:
dialog-polyfill:
specifier: 0.5.6
version: 0.5.6
lichess-pgn-viewer:
specifier: ^2.0.0
version: 2.0.0
@@ -2683,6 +2686,10 @@ packages:
engines: {node: '>=8'}
dev: false
/dialog-polyfill@0.5.6:
resolution: {integrity: sha512-ZbVDJI9uvxPAKze6z146rmfUZjBqNEwcnFTVamQzXH+svluiV7swmVIGr7miwADgfgt1G2JQIytypM9fbyhX4w==}
dev: false
/diff-sequences@29.3.1:
resolution: {integrity: sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+9 -8
View File
@@ -18,6 +18,7 @@ function loadShepherd(f) {
}
lichess.studyTourChapter = function (study) {
loadShepherd(function (theme) {
const viewSel = 'dialog div.dialog-content';
var onTab = function (tab) {
return {
'before-show': function () {
@@ -39,18 +40,18 @@ lichess.studyTourChapter = function (study) {
'A study can have several chapters.<br>' +
'Each chapter has a distinct move tree,<br>' +
'and can be created in various ways.',
attachTo: '#modal-wrap label[for=chapter-name] left',
attachTo: `${viewSel} label[for=chapter-name] left`,
},
{
title: 'From initial position',
text: 'Just a board setup for a new game.<br>' + 'Suited to explore openings.',
attachTo: '#modal-wrap .tabs-horiz .init top',
attachTo: `${viewSel} .tabs-horiz .init top`,
when: onTab('init'),
},
{
title: 'Custom position',
text: 'Setup the board your way.<br>' + 'Suited to explore endgames.',
attachTo: '#modal-wrap .tabs-horiz .edit bottom',
attachTo: `${viewSel} .tabs-horiz .edit bottom`,
when: onTab('edit'),
},
{
@@ -59,7 +60,7 @@ lichess.studyTourChapter = function (study) {
'Paste a lichess game URL<br>' +
'(like lichess.org/7fHIU0XI)<br>' +
'to load the game moves in the chapter.',
attachTo: '#modal-wrap .tabs-horiz .game top',
attachTo: `${viewSel} .tabs-horiz .game top`,
when: onTab('game'),
},
{
@@ -68,19 +69,19 @@ lichess.studyTourChapter = function (study) {
'Paste a position in FEN format<br>' +
'<i>4k3/4rb2/8/7p/8/5Q2/1PP5/1K6 w</i><br>' +
'to start the chapter from a position.',
attachTo: '#modal-wrap .tabs-horiz .fen top',
attachTo: `${viewSel} .tabs-horiz .fen top`,
when: onTab('fen'),
},
{
title: 'From a PGN game',
text: 'Paste a game in PGN format.<br>' + 'to load moves, comments and variations in the chapter.',
attachTo: '#modal-wrap .tabs-horiz .pgn top',
attachTo: `${viewSel} .tabs-horiz .pgn top`,
when: onTab('pgn'),
},
{
title: 'Studies support variants',
text: 'Yes, you can study crazyhouse,<br>' + 'and all lichess variants!',
attachTo: '#modal-wrap label[for=chapter-variant] left',
attachTo: `${viewSel} label[for=chapter-variant] left`,
when: onTab('init'),
},
{
@@ -92,7 +93,7 @@ lichess.studyTourChapter = function (study) {
action: tour.next,
},
],
attachTo: '#modal-wrap .help bottom',
attachTo: `${viewSel} .help bottom`,
},
].forEach(function (s) {
tour.addStep(s.title, s);
+2 -1
View File
@@ -9,12 +9,13 @@ const typeBundles = new Map<string, Map<string, string>>();
export async function esbuild(): Promise<void> {
if (!env.esbuild) return;
const define = {
const define: { [_: string]: string } = {
__info__: JSON.stringify({
date: new Date(new Date().toUTCString()).toISOString().split('.')[0] + '+00:00',
commit: cps.execSync('git rev-parse -q HEAD', { encoding: 'utf-8' }).trim(),
message: cps.execSync('git log -1 --pretty=%s', { encoding: 'utf-8' }).trim(),
}),
__debug__: String(env.debug),
};
for (const mod of buildModules) {
+3 -5
View File
@@ -7,7 +7,7 @@ import { build, postBuild } from './build';
export function main() {
const configPath = path.resolve(__dirname, '../build-config.json');
const config: BuildOpts = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : {};
const oneDashArgs = ps.argv.filter(x => /^-([hpsw]+)$/.test(x))?.flatMap(x => x.slice(1).split(''));
const oneDashArgs = ps.argv.filter(x => /^-([hpdsw]+)$/.test(x))?.flatMap(x => x.slice(1).split(''));
if (ps.argv.includes('--tsc') || ps.argv.includes('--sass') || ps.argv.includes('--esbuild')) {
// cli args override json, including any of these flags sets those not present to false
@@ -28,11 +28,8 @@ export function main() {
env.watch = ps.argv.includes('--watch') || oneDashArgs.includes('w');
env.prod = ps.argv.includes('--prod') || oneDashArgs.includes('p');
env.split = ps.argv.includes('--split') || oneDashArgs.includes('s');
env.debug = ps.argv.includes('--debug') || oneDashArgs.includes('d');
if (env.prod && env.watch) {
env.error('You cannot watch prod builds! Think of the children');
return;
}
build(ps.argv.slice(2).filter(x => !x.startsWith('-')));
}
@@ -115,6 +112,7 @@ class Env {
watch = false;
prod = false;
split = false;
debug = false;
exitCode = new Map<'sass' | 'tsc' | 'esbuild', number | false>();
startTime: number | undefined = Date.now();
+2 -1
View File
@@ -4,6 +4,7 @@
interface Lichess {
load: Promise<void>; // DOMContentLoaded promise
info: any;
debug: boolean;
requestIdleCallback(f: () => void, timeout?: number): void;
sri: string;
storage: LichessStorageHelper;
@@ -580,4 +581,4 @@ interface Dictionary<T> {
type SocketHandlers = Dictionary<(d: any) => void>;
declare const lichess: Lichess;
declare const $as: <T>(cash: Cash) => T;
declare const $as: <T>(cashOrHtml: Cash | string) => T;
+1
View File
@@ -19,3 +19,4 @@
@import 'context-menu';
@import 'player-clock';
@import 'persistence';
@import 'keyboard';
-2
View File
@@ -1,6 +1,4 @@
.keyboard-help {
@extend %help-modal;
td.mouse li {
list-style: disc inside;
margin-#{$start-direction}: 1em;
@@ -3,7 +3,6 @@
@import '../../../common/css/layout/uniboard';
@import '../../../common/css/base/scrollbar';
@import '../../../common/css/component/board-resize';
@import '../../../common/css/component/modal';
@import '../../../common/css/component/continue-with';
@import '../../../common/css/component/color-icon';
@import '../../../common/css/component/crosstable';
+5
View File
@@ -15,6 +15,11 @@
grid-template-columns: 280px 2vmin 210px;
grid-template-rows: min-content auto min-content;
grid-template-areas: '. . e-tools' 'spare-top . e-tools' 'e-board . e-tools' 'spare-bottom . e-tools' '. . e-tools';
@media (max-width: 576px) {
grid-template-columns: 100%;
grid-template-rows: auto min-content;
grid-template-areas: 'e-board' 'e-tools';
}
user-select: none;
.main-board {
+1 -6
View File
@@ -1,11 +1,6 @@
.study__invite {
max-width: 50em;
> div {
overflow: visible !important;
// required by user autocomplete
}
overflow: visible !important;
.info {
@extend %box-radius, %flex-center-nowrap;
+1 -1
View File
@@ -1,4 +1,4 @@
#modal-wrap {
.dialog-content {
min-width: 80vw;
@include breakpoint($mq-x-small) {
+3 -3
View File
@@ -4,7 +4,7 @@ import * as licon from 'common/licon';
import { bind, dataIcon, iconTag, onInsert } from 'common/snabbdom';
import { storedProp, storedJsonProp, StoredJsonProp, StoredProp, storedStringProp } from 'common/storage';
import { ExplorerDb, ExplorerSpeed, ExplorerMode } from './interfaces';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import AnalyseCtrl from '../ctrl';
import { perf } from 'game/perf';
import { ucfirst } from './explorerUtil';
@@ -327,13 +327,13 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => {
}
return '.button-metal';
};
return snabModal({
return snabDialog({
class: 'explorer__config__player__choice',
onClose() {
ctrl.data.playerName.open(false);
ctrl.root.redraw();
},
content: [
vnodes: [
h('h2', 'Personal opening explorer'),
h('div.input-wrapper', [
h('input', {
+5 -14
View File
@@ -1,9 +1,7 @@
import * as control from './control';
import * as xhr from 'common/xhr';
import AnalyseCtrl from './ctrl';
import { h, VNode } from 'snabbdom';
import { snabModal } from 'common/modal';
import { spinnerVdom as spinner } from 'common/spinner';
import { VNode } from 'snabbdom';
import { snabDialog } from 'common/dialog';
export const bind = (ctrl: AnalyseCtrl) => {
const kbd = window.lichess.mousetrap;
@@ -116,19 +114,12 @@ export const bind = (ctrl: AnalyseCtrl) => {
};
export function view(ctrl: AnalyseCtrl): VNode {
return snabModal({
class: 'keyboard-help',
onInsert: async ($wrap: Cash) => {
const [, html] = await Promise.all([
lichess.loadCssPath('analyse.keyboard'),
xhr.text(xhr.url('/analysis/help', { study: !!ctrl.study })),
]);
$wrap.find('.scrollable').html(html);
},
return snabDialog({
class: 'help.keyboard-help',
htmlUrl: '/analysis/help',
onClose() {
ctrl.keyboardHelp = false;
ctrl.redraw();
},
content: [h('div.scrollable', spinner())],
});
}
+13 -12
View File
@@ -1,7 +1,7 @@
import AnalyseCtrl from './ctrl';
import { baseUrl } from './view/util';
import * as licon from 'common/licon';
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
import { url as xhrUrl, textRaw as xhrTextRaw } from 'common/xhr';
import { AnalyseData } from './interfaces';
import { ChartGame, AcplChart } from 'chart';
@@ -137,23 +137,24 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) {
selection!.removeAllRanges();
selection!.addRange(range);
});
$panels.on('click', '.embed-howto', function (this: HTMLElement) {
// location.hash is percent encoded, so no need to escape and make &bg=...
// uglier in the process.
const url = `${baseUrl()}/embed/game/${data.game.id}?theme=auto&bg=auto${location.hash}`;
const iframe = `<iframe src="${url}"\nwidth=600 height=397 frameborder=0></iframe>`;
modal({
content: $(
domDialog({
show: 'modal',
htmlText:
'<div><strong style="font-size:1.5em">' +
$(this).html() +
'</strong><br /><br />' +
'<pre>' +
lichess.escapeHtml(iframe) +
'</pre><br />' +
iframe +
'<br /><br />' +
`<a class="text" data-icon="${licon.InfoCircle}" href="/developers#embed-game">Read more about embedding games</a></div>`,
),
$(this).html() +
'</strong><br /><br />' +
'<pre>' +
lichess.escapeHtml(iframe) +
'</pre><br />' +
iframe +
'<br /><br />' +
`<a class="text" data-icon="${licon.InfoCircle}" href="/developers#embed-game">Read more about embedding games</a></div>`,
});
});
}
+3 -3
View File
@@ -12,7 +12,7 @@ import {
import { defined, prop, Prop } from 'common';
import { h, VNode } from 'snabbdom';
import { Redraw } from '../interfaces';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import { StudySocketSend } from '../socket';
export interface StudyChapterEditFormCtrl {
@@ -88,13 +88,13 @@ export function view(ctrl: StudyChapterEditFormCtrl): VNode | undefined {
const data = ctrl.current(),
noarg = ctrl.trans.noarg;
return data
? snabModal({
? snabDialog({
class: 'edit-' + data.id, // full redraw when changing chapter
onClose() {
ctrl.current(null);
ctrl.redraw();
},
content: [
vnodes: [
h('h2', noarg('editChapter')),
h(
'form.form3',
+15 -5
View File
@@ -1,7 +1,7 @@
import { parseFen } from 'chessops/fen';
import { defined, prop, Prop } from 'common';
import * as licon from 'common/licon';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import { bind, bindSubmit, onInsert } from 'common/snabbdom';
import { StoredProp, storedStringProp } from 'common/storage';
import * as xhr from 'common/xhr';
@@ -130,8 +130,18 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode {
'span.' + key,
{
class: { active: activeTab === key },
attrs: { role: 'tab', title },
hook: bind('click', () => ctrl.vm.tab(key), ctrl.root.redraw),
attrs: { role: 'tab', title, tabindex: '0' },
hook: onInsert(el => {
const select = (e: Event) => {
ctrl.vm.tab(key);
ctrl.root.redraw();
e.preventDefault();
};
el.addEventListener('click', select);
el.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') select(e);
});
}),
},
name,
);
@@ -147,14 +157,14 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode {
: 'normal';
const noarg = trans.noarg;
return snabModal({
return snabDialog({
class: 'chapter-new',
onClose() {
ctrl.close();
ctrl.redraw();
},
noClickAway: true,
content: [
vnodes: [
activeTab === 'edit'
? null
: h('h2', [
+3 -3
View File
@@ -2,7 +2,7 @@ import * as licon from 'common/licon';
import { bind, onInsert } from 'common/snabbdom';
import { titleNameToId } from '../view/util';
import { h, VNode } from 'snabbdom';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import { prop, Prop } from 'common';
import { StudyMemberMap } from './interfaces';
import { AnalyseSocketSend } from '../socket';
@@ -59,13 +59,13 @@ export function view(ctrl: ReturnType<typeof makeCtrl>): VNode {
const candidates = [...new Set([...ctrl.spectators(), ...ctrl.previouslyInvited()])]
.filter(s => !ctrl.members()[titleNameToId(s)]) // remove existing members
.sort();
return snabModal({
return snabDialog({
class: 'study__invite',
onClose() {
ctrl.open(false);
ctrl.redraw();
},
content: [
vnodes: [
h('h2', ctrl.trans.noarg('inviteToTheStudy')),
h(
'p.info',
+3 -3
View File
@@ -1,6 +1,6 @@
import { h, VNode } from 'snabbdom';
import * as licon from 'common/licon';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import { prop, Prop } from 'common';
import { bindSubmit, bindNonPassive } from 'common/snabbdom';
import { emptyRedButton } from '../view/util';
@@ -116,13 +116,13 @@ export function view(ctrl: StudyFormCtrl): VNode {
['member', ctrl.trans.noarg('members')],
['everyone', ctrl.trans.noarg('everyone')],
];
return snabModal({
return snabDialog({
class: 'study-edit',
onClose() {
ctrl.open(false);
ctrl.redraw();
},
content: [
vnodes: [
h('h2', ctrl.trans.noarg(ctrl.relay ? 'editRoundStudy' : isNew ? 'createStudy' : 'editStudy')),
h(
'form.form3',
+3 -3
View File
@@ -1,6 +1,6 @@
import { Prop, propWithEffect } from 'common';
import * as licon from 'common/licon';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import { bind, dataIcon, onInsert } from 'common/snabbdom';
import { h, VNode } from 'snabbdom';
import { Redraw } from '../interfaces';
@@ -49,12 +49,12 @@ const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); //
export function view(ctrl: SearchCtrl) {
const cleanQuery = ctrl.cleanQuery();
const highlightRegex = cleanQuery && new RegExp(escapeRegExp(cleanQuery), 'gi');
return snabModal({
return snabDialog({
class: 'study-search',
onClose() {
ctrl.open(false);
},
content: [
vnodes: [
h('input', {
attrs: { autofocus: 1, placeholder: `Search in ${ctrl.studyName}`, value: ctrl.query() },
hook: onInsert((el: HTMLInputElement) => {
+5 -5
View File
@@ -1,6 +1,6 @@
import type Tagify from '@yaireo/tagify';
import { prop } from 'common';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import { bind, bindSubmit, onInsert } from 'common/snabbdom';
import * as xhr from 'common/xhr';
import { h, VNode } from 'snabbdom';
@@ -43,13 +43,13 @@ export const view = (ctrl: StudyCtrl): VNode =>
let tagify: Tagify | undefined;
export const formView = (ctrl: TopicsCtrl, userId?: string): VNode =>
snabModal({
snabDialog({
class: 'study-topics',
onClose() {
ctrl.open(false);
ctrl.redraw();
},
content: [
vnodes: [
h('h2', ctrl.trans.noarg('topics')),
h(
'form',
@@ -84,14 +84,14 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode =>
function setupTagify(elm: HTMLInputElement | HTMLTextAreaElement, userId?: string) {
lichess.loadCssPath('tagify');
lichess.loadIife('vendor/tagify/tagify.min.js').then(() => {
lichess.loadIife('npm/tagify/tagify.min.js').then(() => {
const tagi = (tagify = new (window.Tagify as typeof Tagify)(elm, {
pattern: /.{2,}/,
maxTags: 30,
}));
let abortCtrl: AbortController | undefined; // for aborting the call
tagi.on('input', e => {
const term = e.detail.value.trim();
const term = (e.detail as Tagify.TagData).value.trim();
if (term.length < 2) return;
tagi.settings.whitelist!.length = 0; // reset the whitelist
abortCtrl && abortCtrl.abort();
+2 -6
View File
@@ -1,6 +1,6 @@
import { isEmpty } from 'common';
import * as licon from 'common/licon';
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
import { bind, dataIcon, MaybeVNodes } from 'common/snabbdom';
import { h, VNode } from 'snabbdom';
import { AutoplayDelay } from '../autoplay';
@@ -161,11 +161,7 @@ export function view(ctrl: AnalyseCtrl): VNode {
? h(
'a.button.button-empty',
{
hook: bind('click', _ =>
modal({
content: $('.continue-with.g_' + d.game.id),
}),
),
hook: bind('click', () => domDialog({ cash: $('.continue-with.g_' + d.game.id) })),
attrs: dataIcon(licon.Swords),
},
noarg('continueFromHere'),
+4 -4
View File
@@ -177,7 +177,7 @@ export default class CevalCtrl {
else if (this.technology == 'nnue')
this.worker = new ThreadedWasmWorker(
{
baseUrl: 'vendor/stockfish-nnue.wasm/',
baseUrl: 'npm/stockfish-nnue.wasm/',
module: 'Stockfish',
downloadProgress: throttle(200, mb => {
this.downloadProgress(mb);
@@ -192,7 +192,7 @@ export default class CevalCtrl {
else if (this.technology == 'hce')
this.worker = new ThreadedWasmWorker(
{
baseUrl: this.officialStockfish ? 'vendor/stockfish.wasm/' : 'vendor/stockfish-mv.wasm/',
baseUrl: this.officialStockfish ? 'npm/stockfish.wasm/' : 'npm/stockfish-mv.wasm/',
module: this.officialStockfish ? 'Stockfish' : 'StockfishMv',
version: 'a022fa',
wasmMemory: sharedWasmMemory(1024, this.platform.maxWasmPages(1088)),
@@ -204,8 +204,8 @@ export default class CevalCtrl {
{
url:
this.technology == 'wasm'
? 'vendor/stockfish.js/stockfish.wasm.js'
: 'vendor/stockfish.js/stockfish.js',
? 'npm/stockfish.js/stockfish.wasm.js'
: 'npm/stockfish.js/stockfish.js',
},
this.opts.redraw,
);
+1 -1
View File
@@ -20,7 +20,7 @@ export function selectPly(this: PlyChart, ply: number, onMainline: boolean) {
export async function loadHighcharts(tpe: string) {
if (highchartsPromise) return highchartsPromise;
const file = tpe === 'highstock' ? 'highstock.js' : 'highcharts.js';
highchartsPromise = lichess.loadIife('vendor/highcharts-4.2.5/' + file, {
highchartsPromise = lichess.loadIife('npm/highcharts-4.2.5/' + file, {
noVersion: true,
});
await highchartsPromise;
-1
View File
@@ -1,3 +1,2 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/modal';
@import '../help';
+16 -16
View File
@@ -1,4 +1,4 @@
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
import { load as loadDasher } from 'dasher';
export function initModule({ input }: { input: HTMLInputElement }) {
@@ -54,21 +54,21 @@ function commandHelp(aliases: string, args: string, desc: string) {
}
function help() {
lichess.loadCssPath('clinput.help');
modal({
content: $(
'<div><h3>Commands</h3>' +
commandHelp('/tv /follow', ' <user>', 'Watch someone play') +
commandHelp('/play /challenge /match', ' <user>', 'Challenge someone to play') +
commandHelp('/light /dark /transp /system', '', 'Change the background theme') +
commandHelp('/stream', '<user>', 'Watch someone stream') +
'<h3>Global hotkeys</h3>' +
commandHelp('s', '', 'Search for a user') +
commandHelp('/', '', 'Type a command') +
commandHelp('c', '', 'Focus the chat input') +
commandHelp('esc', '', 'Close modals like this one') +
'</div>',
),
domDialog({
cssPath: 'clinput.help',
class: 'clinput-help',
show: 'modal',
htmlText:
'<div><h3>Commands</h3>' +
commandHelp('/tv /follow', ' <user>', 'Watch someone play') +
commandHelp('/play /challenge /match', ' <user>', 'Challenge someone to play') +
commandHelp('/light /dark /transp /system', '', 'Change the background theme') +
commandHelp('/stream', '<user>', 'Watch someone stream') +
'<h3>Global hotkeys</h3>' +
commandHelp('s', '', 'Search for a user') +
commandHelp('/', '', 'Type a command') +
commandHelp('c', '', 'Focus the chat input') +
commandHelp('esc', '', 'Close modals like this one') +
'</div>',
});
}
+2
View File
@@ -36,3 +36,5 @@
@import 'component/reconnecting';
@import 'component/announce';
@import 'component/agreement';
@import 'component/dialog';
@import 'component/help';
+1 -1
View File
@@ -26,7 +26,7 @@
background: lighten($c-primary, 7);
}
&:not(.disabled):focus {
&:not(.disabled):focus-visible {
@extend %focus-shadow;
}
+3 -1
View File
@@ -1,6 +1,8 @@
.continue-with {
@extend %flex-column;
@media (hover: none) {
padding: 2em;
}
> *:not(:first-child) {
margin-top: 1em;
}
+77
View File
@@ -0,0 +1,77 @@
dialog {
@extend %box-radius, %popup-shadow;
position: fixed;
max-height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: z('modal');
padding: 0;
border: none;
background: $c-bg-high;
&::backdrop {
background: $c-page-mask;
}
> div.scrollable {
max-height: calc(100 * var(--vh) - 16px);
overflow-x: clip;
overflow-y: auto;
}
div.close-button-anchor {
position: relative;
width: 100%;
height: 0;
}
button.close-button {
@extend %flex-around;
position: absolute;
top: 4px;
#{$end-direction}: 4px;
width: 40px; // bigger for phones
height: 40px;
z-index: z('modal') + 1;
background: $c-bg-high;
color: $c-font;
border-radius: 6px;
border: 1px solid $c-border;
font-size: 20px;
text-align: center;
cursor: pointer;
&:not(:focus) {
outline: none;
}
}
&:not(.touch-scroll) {
margin-top: 16px;
overflow: visible;
button.close-button {
transform: translate($transform-direction * 18px, -18px);
width: 32px;
height: 32px;
font-size: 16px;
border-radius: 50%;
border: none;
&:hover {
box-shadow: $box-shadow;
background: $c-bad;
color: #fff;
}
}
}
}
// top level to reduce specificity, allow easy overrides
.dialog-content {
text-align: center;
padding: 2em;
color: $c-font;
}
+18 -68
View File
@@ -1,5 +1,15 @@
%help {
@extend %flex-column;
dialog .help {
align-items: center;
text-align: center;
padding: 0.8em 0;
h2 {
margin: 0.5em 0 0 0;
}
table {
width: 100%;
}
th p {
margin: 1.2em 0 0.6em 0;
@@ -14,16 +24,6 @@
text-align: left;
}
.keys {
padding-#{$end-direction}: 1em;
text-align: right;
white-space: nowrap;
}
.desc:first-letter {
text-transform: uppercase;
}
or {
margin-#{$start-direction}: 0.2em;
opacity: 0.5;
@@ -43,64 +43,14 @@
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb;
}
}
%help-modal {
@extend %help;
table {
width: 100%;
.keys {
padding-#{$end-direction}: 1em;
text-align: right;
white-space: nowrap;
}
> div {
padding: 0 !important;
}
h2 {
margin: 0.5em 0 0 0;
}
.scrollable {
overflow-y: auto;
padding: 0.8em 0;
}
}
.help-ephemeral {
@extend %help, %box-radius, %popup-shadow;
position: absolute;
z-index: 100;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
align-items: center;
background: $c-bg-popup;
padding: 1em 2em;
white-space: nowrap;
pointer-events: none;
kbd {
font-family: roboto, sans-serif;
}
table {
display: inline-block;
width: auto;
}
ul {
margin-bottom: 1em;
display: inline-block;
width: auto;
font-size: larger;
li {
padding-left: 0.5em;
list-style: disc;
}
}
&.fade-out {
transition: opacity 0.5s linear;
opacity: 0;
.desc:first-letter {
text-transform: uppercase;
}
}
+7
View File
@@ -24,6 +24,7 @@
"author": "Thibault Duplessis",
"license": "AGPL-3.0-or-later",
"dependencies": {
"dialog-polyfill": "0.5.6",
"lichess-pgn-viewer": "^2.0.0",
"snabbdom": "^3.5.1",
"tablesort": "^5.3.0"
@@ -32,5 +33,11 @@
"compile": "tsc",
"dev": "tsc",
"prod": "tsc"
},
"lichess": {
"copy": {
"src": "node_modules/dialog-polyfill/dist/dialog-polyfill.esm.js",
"dest": "../../public/npm"
}
}
}
+223
View File
@@ -0,0 +1,223 @@
import { VNode, Attrs } from 'snabbdom';
import { onInsert, lh as h, MaybeVNodes } from './snabbdom';
import { isTouchDevice, isIOS } from './mobile';
import * as xhr from './xhr';
import * as licon from './licon';
let dialogPolyfill: { registerDialog: (dialog: HTMLDialogElement) => void };
lichess.load.then(async () => {
window.addEventListener('resize', onResize);
if (isIOS({ below: 15.4 })) dialogPolyfill = await import(lichess.assetUrl('npm/dialog-polyfill.esm.js'));
});
export interface Dialog {
readonly open: boolean; // is this dialog visible?
readonly view: HTMLElement; // your content div
readonly returnValue?: 'ok' | 'cancel' | string; // how did it close?
showModal(): void;
show(): void;
close(): void;
}
interface DialogOpts {
class?: string; // zero or more classes (period separated) for your view div
cssPath?: string; // for themed css craplets
cash?: Cash; // content, will be cloned and any 'none' class removed
htmlUrl?: string; // content, url will be xhr'd
htmlText?: string; // content, text will be used as-is
attrs?: { dialog?: Attrs; view?: Attrs }; // optional attrs for dialog and view div
onClose?: (dialog: Dialog) => void; // called when dialog closes
noCloseButton?: boolean; // if true, no upper right corener close button
noClickAway?: boolean; // if true, no click-away-to-close
}
export interface DomDialogOpts extends DialogOpts {
parent?: Element; // for centering and dom placement, otherwise fixed on document.body
show?: 'modal' | boolean; // if not falsy, auto-show, and if 'modal' remove from dom on close
}
export interface SnabDialogOpts extends DialogOpts {
vnodes?: MaybeVNodes; // snabDialog auto-shows by default, but you must still call redraw
onInsert?: (dialog: Dialog) => void; // prevents showModal, caller must do so manually
}
// if no 'show' in opts, you must call show or showModal on the resolved promise
export async function domDialog(o: DomDialogOpts): Promise<Dialog> {
const [html] = await assets(o);
const dialog = document.createElement('dialog');
for (const [k, v] of Object.entries(o.attrs?.dialog ?? {})) dialog.setAttribute(k, String(v));
if (isTouchDevice()) dialog.classList.add('touch-scroll');
if (o.parent) dialog.style.position = 'absolute';
if (!o.noCloseButton) {
const anchor = $as<Element>('<div class="close-button-anchor">');
anchor.innerHTML = `<button class="close-button" aria-label="Close" data-icon="${licon.X}">`;
dialog.appendChild(anchor);
}
const view = $as<HTMLElement>('<div class="dialog-content">');
if (o.class) view.classList.add(...o.class.split('.'));
for (const [k, v] of Object.entries(o.attrs?.view ?? {})) view.setAttribute(k, String(v));
if (html) view.innerHTML = html;
const scrollable = $as<Element>('<div class="scrollable">');
scrollable.appendChild(view);
dialog.appendChild(scrollable);
(o.parent ?? document.body).appendChild(dialog);
const wrapper = new DialogWrapper(dialog, view, o);
if (o.show && o.show === 'modal') wrapper.showModal();
else if (o.show) wrapper.show();
return wrapper;
}
// snab dialogs are shown by default, to suppress this pass onInsert callback
export function snabDialog(o: SnabDialogOpts): VNode {
const ass = assets(o);
let dialog: HTMLDialogElement;
return h(
`dialog${isTouchDevice() ? '.touch-scroll' : ''}`,
{
key: o.class ?? 'dialog',
attrs: o.attrs?.dialog,
hook: onInsert(el => (dialog = el as HTMLDialogElement)),
},
[
o.noCloseButton ||
h(
'div.close-button-anchor',
h('button.close-button', { attrs: { 'data-icon': licon.X, 'aria-label': 'Close' } }),
),
h(
'div.scrollable',
h(
'div.dialog-content' + (o.class ? `.${o.class}` : ''),
{
attrs: o.attrs?.view,
hook: onInsert(async view => {
const [html] = await ass;
if (html && !o.vnodes) view.innerHTML = html;
const wrapper = new DialogWrapper(dialog, view, o);
if (o.onInsert) o.onInsert(wrapper);
else wrapper.showModal();
}),
},
o.vnodes,
),
),
],
);
}
class DialogWrapper implements Dialog {
restoreFocus?: HTMLElement;
constructor(
readonly dialog: HTMLDialogElement,
readonly view: HTMLElement,
readonly o: DialogOpts,
) {
if (dialogPolyfill) dialogPolyfill.registerDialog(dialog); // ios < 15.4
view.parentElement?.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); // sigh
view.addEventListener('click', e => e.stopPropagation());
dialog.addEventListener('cancel', () => !this.returnValue && (this.returnValue = 'cancel'));
dialog.addEventListener('close', this.onClose);
dialog
.querySelector('.close-button-anchor > .close-button')
?.addEventListener('click', () => this.close('cancel'));
if (!o.noClickAway) setTimeout(() => document.addEventListener('click', () => this.close('cancel')), 0);
}
get open() {
return this.dialog.open;
}
get returnValue() {
return this.dialog.returnValue;
}
set returnValue(v: string) {
this.dialog.returnValue = v;
}
show = () => {
this.returnValue = '';
this.dialog.show();
};
close = (v?: string) => {
this.dialog.close(this.returnValue || v || 'ok');
};
onClose = () => {
this.o.onClose?.(this);
if ('show' in this.o && this.o.show === 'modal') {
this.dialog.remove();
this.restoreFocus?.focus();
}
if (!this.dialog.returnValue) this.dialog.returnValue = 'cancel';
this.restoreFocus = undefined;
};
showModal = () => {
this.restoreFocus = document.activeElement as HTMLElement;
$(focusQuery, this.view)[1]?.focus();
this.view.scrollTop = 0;
this.addModalListeners?.();
this.returnValue = '';
this.dialog.showModal();
};
addModalListeners? = () => {
this.dialog.addEventListener('keydown', onModalKeydown);
this.addModalListeners = undefined; // only do this once per HTMLDialogElement
};
}
function assets(o: DialogOpts) {
return Promise.all([
o.htmlUrl
? xhr.text(o.htmlUrl)
: Promise.resolve(
o.cash ? $as<HTMLElement>($(o.cash).clone().removeClass('none')).outerHTML : o.htmlText,
),
o.cssPath ? lichess.loadCssPath(o.cssPath) : Promise.resolve(),
]);
}
function onModalKeydown(e: KeyboardEvent) {
if (e.key === 'Tab') {
const $focii = $(focusQuery, e.currentTarget as Element),
first = $as<HTMLElement>($focii.first()),
last = $as<HTMLElement>($focii.last()),
focus = document.activeElement as HTMLElement;
if (focus === last && !e.shiftKey) first.focus();
else if (focus === first && e.shiftKey) last.focus();
else return;
e.preventDefault();
}
e.stopPropagation();
}
function onResize() {
const vh = window.innerHeight * 0.01; // ios safari vh behavior not helpful to us
$('dialog > div.scrollable').css('--vh', `${vh}px`);
}
const focusQuery = ['button', 'input', 'select', 'textarea']
.map(sel => `${sel}:not(:disabled)`)
.concat(['[href]', '[tabindex="0"]', '[role="tab"]'])
.join(',');
+22 -23
View File
@@ -1,4 +1,4 @@
import modal from './modal';
import { domDialog } from './dialog';
export const makeLinkPopups = (dom: HTMLElement | Cash, trans: Trans, selector = 'a[href^="http"]') =>
$(dom).on('click', selector, function (this: HTMLLinkElement) {
@@ -8,29 +8,28 @@ export const makeLinkPopups = (dom: HTMLElement | Cash, trans: Trans, selector =
export const onClick = (a: HTMLLinkElement, trans: Trans): boolean => {
const url = new URL(a.href);
if (isPassList(url)) return true;
lichess.loadCssPath('linkPopup').then(() =>
modal({
content: $(
`<div class="link-popup">
<div class="link-popup__content">
<div class="link-popup__content__title">
<h2>${trans('youAreLeavingLichess')}</h2>
<p class="link-popup__content__advice">${trans('neverTypeYourPassword')}</p>
</div>
domDialog({
cssPath: 'linkPopup',
htmlText: `<div class="link-popup">
<div class="link-popup__content">
<div class="link-popup__content__title">
<h2>${trans('youAreLeavingLichess')}</h2>
<p class="link-popup__content__advice">${trans('neverTypeYourPassword')}</p>
</div>
<div class="link-popup__actions">
<button class="cancel button-link" type="button">${trans('cancel')}</button>
<a href="${a.href}" target="_blank" class="button button-red button-no-upper">
${trans('proceedToX', url.host)}
</a>
</div>
</div>`,
),
onInsert($wrap) {
$wrap.find('.cancel').on('click', modal.close);
},
}),
);
</div>
<div class="link-popup__actions">
<button class="cancel button-link" type="button">${trans('cancel')}</button>
<a href="${a.href}" target="_blank" class="button button-red button-no-upper">
${trans('proceedToX', url.host)}
</a>
</div>
</div>`,
}).then(dlg => {
$('.cancel', dlg.view).on('click', dlg.close);
$('a', dlg.view).on('click', () => setTimeout(dlg.close, 1000));
dlg.showModal();
});
return false;
};
-121
View File
@@ -1,121 +0,0 @@
import { h, VNode } from 'snabbdom';
import { bind, MaybeVNodes, onInsert } from './snabbdom';
import * as licon from './licon';
interface BaseModal {
class?: string;
onInsert?: ($wrap: Cash) => void;
onClose?(): void;
noClickAway?: boolean;
}
interface Modal extends BaseModal {
content: Cash;
}
interface SnabModal extends BaseModal {
content: MaybeVNodes;
onClose(): void;
}
const overlayId = 'modal-overlay';
export default function modal(opts: Modal) {
modal.close();
const $wrap = $(
`<div id="modal-wrap"><span class="close" role="button" aria-label="Close" data-icon="${licon.X}" tabindex="0"></span></div>`,
);
const $overlay = $(`<div id="${overlayId}" class="${opts.class}">`);
if (!opts.noClickAway) $overlay.on('click', modal.close);
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
$wrap.appendTo($overlay);
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
opts.content.clone().removeClass('none').appendTo($wrap);
opts.onInsert && opts.onInsert($wrap);
modal.onClose = opts.onClose;
$wrap.find('.close,.cancel').each(function (this: HTMLElement) {
bindClose(this, modal.close);
});
$('body').addClass('overlayed').prepend($overlay);
bindWrap($wrap);
return $wrap;
}
modal.close = () => {
$('body').removeClass('overlayed');
$(`#${overlayId}`).each(function (this: HTMLElement) {
if (modal.onClose) modal.onClose();
$(this).remove();
});
delete modal.onClose;
};
modal.onClose = undefined as (() => void) | undefined;
export function snabModal(opts: SnabModal): VNode {
const close = opts.onClose!;
return h(
`div#${overlayId}`,
opts.noClickAway
? {}
: {
hook: bind('mousedown', (event: MouseEvent) => {
if ((event.target as HTMLElement).id == overlayId) close();
}),
},
[
h(
'div#modal-wrap.' + opts.class,
{
hook: onInsert(el => {
bindWrap($(el));
opts.onInsert && opts.onInsert($(el));
}),
},
[
h('span.close', {
attrs: {
'data-icon': licon.X,
role: 'button',
'aria-label': 'Close',
tabindex: '0',
},
hook: onInsert(el => bindClose(el, close)),
}),
h('div', opts.content),
],
),
],
);
}
const bindClose = (el: HTMLElement, close: () => void) => {
el.addEventListener('click', close);
el.addEventListener('keydown', e => (e.code === 'Enter' || e.code === 'Space' ? close() : true));
};
const bindWrap = ($wrap: Cash) => {
$wrap.on('click', (e: Event) => e.stopPropagation());
focusFirstChild($wrap);
};
const focusableSelectors =
'button:not(:disabled), [href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]';
export function trapFocus(event: FocusEvent) {
const wrap: HTMLElement | undefined = $('#modal-wrap')[0];
if (!wrap) return;
const position = wrap.compareDocumentPosition(event.target as HTMLElement);
if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) return;
const focusableChildren = $(wrap).find(focusableSelectors);
const index = position & Node.DOCUMENT_POSITION_FOLLOWING ? 0 : focusableChildren.length - 1;
focusableChildren.get(index)?.focus();
event.preventDefault();
}
export const focusFirstChild = (parent: Cash) => {
const children = parent.find(focusableSelectors);
// prefer child 1 over child 0 because child 0 should be a close button
// use setTimeout to avoid race conditions with snabbdom
setTimeout(() => (children[1] ?? children[0])?.focus());
};
+30 -2
View File
@@ -1,4 +1,4 @@
import { h, VNode, Hooks, Attrs } from 'snabbdom';
import { h as snabH, VNode, VNodeData, VNodeChildElement, Hooks, Attrs } from 'snabbdom';
export type Redraw = () => void;
export type MaybeVNode = VNode | string | null | undefined;
@@ -36,4 +36,32 @@ export const dataIcon = (icon: string): Attrs => ({
'data-icon': icon,
});
export const iconTag = (icon: string) => h('i', { attrs: dataIcon(icon) });
export const iconTag = (icon: string) => snabH('i', { attrs: dataIcon(icon) });
type LooseVNode = VNodeChildElement | boolean;
type VNodeKids = LooseVNode | LooseVNode[];
function filterKids(children: VNodeKids): VNodeChildElement[] {
return (
typeof children === 'boolean'
? []
: Array.isArray(children)
? children.filter(x => typeof x !== 'boolean')
: [children]
) as VNodeChildElement[];
}
/* obviate need for some ternary expressions in renders. Allows
lh('div', [ kids && h('div', 'kid') ])
lh('div', [ noKids || h('div', 'kid') ])
instead of
h('div', [ isKid ? h('div', 'kid') : null ])
'true' values are filtered out of children array same as 'false' (for || case)
*/
export function lh(sel: string, dataOrKids?: VNodeData | null | VNodeKids, kids?: VNodeKids): VNode {
if (kids) return snabH(sel, dataOrKids as VNodeData, filterKids(kids));
if (!dataOrKids) return snabH(sel);
if (Array.isArray(dataOrKids) || (typeof dataOrKids === 'object' && 'sel' in dataOrKids))
return snabH(sel, filterKids(dataOrKids as VNodeKids));
else return snabH(sel, dataOrKids as VNodeData);
}
-1
View File
@@ -2,6 +2,5 @@
@import '../../../common/css/vendor/chessground/coords';
@import '../../../common/css/layout/uniboard';
@import '../../../common/css/component/board-resize';
@import '../../../common/css/component/modal';
@import '../../../common/css/component/continue-with';
@import '../editor';
+2 -5
View File
@@ -5,7 +5,7 @@ import { dragNewPiece } from 'chessground/drag';
import { eventPosition, opposite } from 'chessground/util';
import { Rules } from 'chessops/types';
import { parseFen } from 'chessops/fen';
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
import EditorCtrl from './ctrl';
import chessground from './chessground';
import { Selected, CastlingToggle, EditorState } from './interfaces';
@@ -274,10 +274,7 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode {
},
on: {
click: () => {
if (state.playable)
modal({
content: $('.continue-with'),
});
if (state.playable) domDialog({ cash: $('.continue-with'), show: 'modal' });
},
},
},
-1
View File
@@ -1,6 +1,5 @@
@import '../../../common/css/plugin';
@import '../../../common/css/base/scrollbar';
@import '../../../common/css/component/slist';
@import '../../../common/css/component/modal';
@import '../../../common/css/vendor/multiple-select';
@import '../insight';
@@ -1,13 +0,0 @@
.keyboard-move-help {
@extend %help-modal;
td.tips li {
list-style: disc;
margin-#{$start-direction}: 2em;
margin-#{$end-direction}: 1em;
}
a {
margin-#{$start-direction}: 0.25em;
}
}
+12
View File
@@ -21,3 +21,15 @@
color: $c-font-dim;
}
}
.keyboard-move-help {
td.tips li {
list-style: disc;
margin-#{$start-direction}: 2em;
margin-#{$end-direction}: 1em;
}
a {
margin-#{$start-direction}: 0.25em;
}
}
@@ -1,4 +0,0 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/modal';
@import '../../../common/css/component/help';
@import '../keyboardMove.help';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/dark';
@import 'keyboardMove.help';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/light';
@import 'keyboardMove.help';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/transp';
@import 'keyboardMove.help';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/dark';
@import 'keyboardMove.help';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/light';
@import 'keyboardMove.help';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/transp';
@import 'keyboardMove.help';
+4 -13
View File
@@ -1,11 +1,9 @@
import * as cg from 'chessground/types';
import * as xhr from 'common/xhr';
import { Api as CgApi } from 'chessground/api';
import { h } from 'snabbdom';
import { onInsert } from 'common/snabbdom';
import { promote } from 'chess/promotion';
import { snabModal } from 'common/modal';
import { spinnerVdom as spinner } from 'common/spinner';
import { snabDialog } from 'common/dialog';
import { propWithEffect, Prop } from 'common';
import { load as loadKeyboardMove } from './plugins/keyboardMove';
import KeyboardChecker from './plugins/keyboardChecker';
@@ -164,17 +162,10 @@ export function render(ctrl: KeyboardMove) {
? h('em', 'Enter SAN (Nc3), ICCF (2133) or UCI (b1c3) moves, type ? to learn more')
: h('strong', 'Press <enter> to focus'),
ctrl.helpModalOpen()
? snabModal({
class: 'keyboard-move-help',
content: [h('div.scrollable', spinner())],
? snabDialog({
class: 'help.keyboard-move-help',
htmlUrl: '/help/keyboard-move',
onClose: () => ctrl.helpModalOpen(false),
onInsert: async ($wrap: Cash) => {
const [, html] = await Promise.all([
lichess.loadCssPath('keyboardMove.help'),
xhr.text(xhr.url('/help/keyboard-move', {})),
]);
$wrap.find('.scrollable').html(html);
},
})
: null,
]);
-4
View File
@@ -66,10 +66,6 @@ body {
}
}
.game-setup {
display: none;
}
.about-side {
@extend %page-text-shadow !optional;
-2
View File
@@ -1,2 +0,0 @@
@import 'setup';
@import 'app/hook-filter';
+48 -49
View File
@@ -1,15 +1,11 @@
$c-setup: $c-secondary;
$c-slider: $c-setup;
.game-setup#modal-wrap {
display: block;
.game-setup {
width: 30em;
text-align: center;
padding: 0;
> div {
padding: 0;
max-height: 96vh;
}
text-align: center;
h2 {
margin: 1.5rem 0;
@@ -55,11 +51,6 @@ $c-slider: $c-setup;
.label-select {
@extend %flex-center;
&.variant {
margin-bottom: 1em;
padding-bottom: 0;
}
label {
flex: 0 0 33%;
text-align: right;
@@ -91,45 +82,9 @@ $c-slider: $c-setup;
border-radius: 0.5em;
}
.range {
padding-top: 1em;
span {
font-weight: bold;
}
input {
font-size: 1.5em;
margin-top: 0.5em;
padding: 0;
width: 90%;
}
}
.rating-range {
@extend %flex-center-nowrap;
justify-content: center;
.rating-min,
.rating-max {
flex: 0 0 7ch;
}
input {
width: 30%;
padding: 0.6em 0;
}
}
.ratings {
padding: 1em;
width: 100%;
text-align: center;
strong {
margin-#{$end-direction}: 0.25em;
}
}
.color-submits {
display: flex;
flex-flow: row nowrap;
align-items: flex-end;
justify-content: center;
margin: 1em auto;
@@ -182,4 +137,48 @@ $c-slider: $c-setup;
margin: 10px auto 20px auto;
}
}
.label-select {
&.variant {
margin-bottom: 1em;
padding-bottom: 0;
}
}
.range {
padding-top: 1em;
span {
font-weight: bold;
}
input {
font-size: 1.5em;
margin-top: 0.5em;
padding: 0;
width: 90%;
}
}
.rating-range {
@extend %flex-center-nowrap;
justify-content: center;
.rating-min,
.rating-max {
flex: 0 0 7ch;
}
input {
width: 30%;
padding: 0.6em 0;
}
}
.ratings {
padding: 1em;
width: 100%;
text-align: center;
strong {
margin-#{$end-direction}: 0.25em;
}
}
}
+1 -5
View File
@@ -1,5 +1,6 @@
@import 'pool';
@import 'hook-chart';
@import 'hook-filters';
@import 'hook-list';
.lobby__app {
@@ -86,8 +87,3 @@
padding: 0.4em;
}
}
.hook__filters {
// will be overridden by _hook-filter.scss once it's loaded
display: none;
}
+1 -2
View File
@@ -1,5 +1,4 @@
@import '../../../common/css/plugin';
@import '../../../common/css/form/range';
@import '../../../common/css/form/radio';
@import '../../../common/css/component/modal';
@import '../lobby.setup';
@import '../setup';
-1
View File
@@ -87,7 +87,6 @@ export const render = (ctrl: LobbyController) =>
insert(vnode) {
const el = vnode.elm as FilterNode;
if (el.filterLoaded) return;
lichess.loadCssPath('lobby.setup');
xhr.text('/setup/filter').then(html => {
el.innerHTML = html;
el.filterLoaded = true;
+4 -4
View File
@@ -1,5 +1,5 @@
import { MaybeVNode } from 'common/snabbdom';
import { snabModal } from 'common/modal';
import { snabDialog } from 'common/dialog';
import LobbyController from '../../ctrl';
import hookContent from './hookContent';
import friendContent from './friendContent';
@@ -15,10 +15,10 @@ export default function setupModal(ctrl: LobbyController): MaybeVNode {
const { setupCtrl } = ctrl;
if (!setupCtrl.gameType) return null;
const renderContent = gameTypeToRenderer[setupCtrl.gameType];
return snabModal({
return snabDialog({
class: 'game-setup',
onInsert: () => lichess.loadCssPath('lobby.setup'),
cssPath: 'lobby.setup',
onClose: setupCtrl.closeModal,
content: renderContent(ctrl),
vnodes: renderContent(ctrl),
});
}
-3
View File
@@ -1,3 +0,0 @@
.keyboard-help {
@extend %help-modal;
}
@@ -1,3 +0,0 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/help';
@import '../keyboard';
-1
View File
@@ -6,7 +6,6 @@
@import '../../../common/css/component/color-icon';
@import '../../../common/css/component/fbt';
@import '../../../common/css/component/glowing';
@import '../../../common/css/component/modal';
@import '../../../common/css/component/zen-toggle';
@import '../../../common/css/form/cmn-toggle';
@import '../../../common/css/base/scrollbar';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/dark';
@import 'puzzle.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/light';
@import 'puzzle.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/transp';
@import 'puzzle.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/dark';
@import 'puzzle.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/light';
@import 'puzzle.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/transp';
@import 'puzzle.keyboard';
+5 -15
View File
@@ -1,9 +1,6 @@
import * as control from './control';
import * as xhr from 'common/xhr';
import { Controller, KeyboardController } from './interfaces';
import { h, VNode } from 'snabbdom';
import { snabModal } from 'common/modal';
import { spinnerVdom as spinner } from 'common/spinner';
import { snabDialog } from 'common/dialog';
export default (ctrl: KeyboardController) =>
lichess.mousetrap
@@ -36,16 +33,9 @@ export default (ctrl: KeyboardController) =>
.bind('f', ctrl.flip)
.bind('n', ctrl.nextPuzzle);
export const view = (ctrl: Controller): VNode =>
snabModal({
class: 'keyboard-help',
onInsert: async ($wrap: Cash) => {
const [, html] = await Promise.all([
lichess.loadCssPath('puzzle.keyboard'),
xhr.text(xhr.url('/training/help', {})),
]);
$wrap.find('.scrollable').html(html);
},
export const view = (ctrl: Controller) =>
snabDialog({
class: 'help',
htmlUrl: '/training/help',
onClose: () => ctrl.keyboardHelp(false),
content: [h('div.scrollable', spinner())],
});
-3
View File
@@ -1,3 +0,0 @@
.keyboard-help {
@extend %help-modal;
}
-3
View File
@@ -1,3 +0,0 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/help';
@import '../keyboard';
-1
View File
@@ -11,7 +11,6 @@
@import '../../../common/css/component/context-streamer';
@import '../../../common/css/component/zen-toggle';
@import '../../../common/css/form/cmn-toggle';
@import '../../../common/css/component/modal';
@import '../../../chess/css/variant-style';
@import '../../../chess/css/promotion';
@import '../../../chess/css/blindfold';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/dark';
@import 'round.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/light';
@import 'round.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/transp';
@import 'round.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/dark';
@import 'round.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/light';
@import 'round.keyboard';
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/transp';
@import 'round.keyboard';
+5 -14
View File
@@ -1,8 +1,6 @@
import RoundController from './ctrl';
import { h, VNode } from 'snabbdom';
import * as xhr from 'common/xhr';
import { snabModal } from 'common/modal';
import { spinnerVdom as spinner } from 'common/spinner';
import { VNode } from 'snabbdom';
import { snabDialog } from 'common/dialog';
export const prev = (ctrl: RoundController) => ctrl.userJump(ctrl.ply - 1);
@@ -34,18 +32,11 @@ export const init = (ctrl: RoundController) =>
});
export const view = (ctrl: RoundController): VNode =>
snabModal({
class: 'keyboard-help',
onInsert: async ($wrap: Cash) => {
const [, html] = await Promise.all([
lichess.loadCssPath('round.keyboard'),
xhr.text(xhr.url('/round/help', {})),
]);
$wrap.find('.scrollable').html(html);
},
snabDialog({
class: 'help',
htmlUrl: '/round/help',
onClose() {
ctrl.keyboardHelp = false;
ctrl.redraw();
},
content: [h('div.scrollable', spinner())],
});
+5 -6
View File
@@ -1,6 +1,6 @@
import * as game from 'game';
import throttle from 'common/throttle';
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
import * as xhr from './xhr';
import RoundController from './ctrl';
import { defined } from 'common';
@@ -145,11 +145,10 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
}
},
simulEnd(simul: game.Simul) {
lichess.loadCssPath('modal');
modal({
content: $(
`<div><p>Simul complete!</p><br /><br /><a class="button" href="/simul/${simul.id}">Back to ${simul.name} simul</a></div>`,
),
domDialog({
htmlText:
'<div><p>Simul complete!</p><br /><br />' +
`<a class="button" href="/simul/${simul.id}">Back to ${simul.name} simul</a></div>`,
});
},
};
-1
View File
@@ -2,7 +2,6 @@
@import '../../../common/css/component/quote';
@import '../../../common/css/component/slist';
@import '../../../common/css/component/now-playing';
@import '../../../common/css/component/modal';
@import '../../../common/css/component/continue-with';
@import '../../../common/css/component/context-streamer';
@import '../../../chat/css/chat';
+6 -11
View File
@@ -5,7 +5,7 @@ import SimulCtrl from '../ctrl';
import { Applicant } from '../interfaces';
import xhr from '../xhr';
import * as util from './util';
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) {
return (ctrl: SimulCtrl) => {
@@ -51,17 +51,12 @@ export default function (showText: (ctrl: SimulCtrl) => MaybeVNode) {
? bind('click', () => {
if (ctrl.data.variants.length === 1)
xhr.join(ctrl.data.id, ctrl.data.variants[0].key);
else {
modal({
content: $('.simul .continue-with'),
onInsert($wrap) {
$wrap.find('button').on('click', function (this: HTMLElement) {
modal.close();
xhr.join(ctrl.data.id, $(this).data('variant'));
});
},
else
domDialog({
show: 'modal',
cash: $('.simul .continue-with'),
onClose: dlg => xhr.join(ctrl.data.id, $(dlg.view).data('variant')),
});
}
})
: {},
},
-1
View File
@@ -1,7 +1,6 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/slist';
@import '../../../common/css/component/pagination';
@import '../../../common/css/component/modal';
@import '../../../common/css/component/lichess-pgn-viewer';
@import '../../../common/css/form/form3';
@import '../../../common/css/form/cmn-toggle';
-1
View File
@@ -1,3 +1,2 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/modal';
@import '../linkPopup';
-1
View File
@@ -1,6 +1,5 @@
@import '../../../common/css/plugin';
@import '../../../common/css/base/scrollbar';
@import '../../../common/css/component/slist';
@import '../../../common/css/component/modal';
@import '../../../common/css/component/continue-with';
@import '../mod/publicChats';
-2
View File
@@ -1,2 +0,0 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/modal';
-3
View File
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/dark';
@import 'modal';
-3
View File
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/light';
@import 'modal';
-3
View File
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/ltr';
@import '../../../common/css/theme/transp';
@import 'modal';
-3
View File
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/dark';
@import 'modal';
-3
View File
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/light';
@import 'modal';
-3
View File
@@ -1,3 +0,0 @@
@import '../../../common/css/dir/rtl';
@import '../../../common/css/theme/transp';
@import 'modal';
+8 -4
View File
@@ -77,19 +77,23 @@
"copy": [
{
"src": "node_modules/hopscotch/dist/**",
"dest": "../../public/vendor/hopscotch/dist"
"dest": "../../public/npm/hopscotch/dist"
},
{
"src": "node_modules/highcharts/*.js",
"dest": "../../public/vendor/highcharts-4.2.5"
"dest": "../../public/npm/highcharts-4.2.5"
},
{
"src": "node_modules/@yaireo/tagify/dist/tagify.min.js",
"dest": "../../public/vendor/tagify"
"dest": "../../public/npm/tagify"
},
{
"src": "node_modules/stockfish*/*.{js,wasm}",
"dest": "../../public/vendor"
"dest": "../../public/npm"
},
{
"src": "../../node_modules/chessground/dist/chessground.min.js",
"dest": "../../public/npm"
}
]
}
+4 -5
View File
@@ -13,7 +13,7 @@ export const loadCss = (url: string, media?: 'dark' | 'light'): Promise<void> =>
if (!loadedCss.has(url)) {
const el = document.createElement('link');
el.rel = 'stylesheet';
el.href = assetUrl(url);
el.href = assetUrl(lichess.debug ? `${url}?_=${Date.now()}` : url);
if (media) el.media = `(prefers-color-scheme: ${media})`;
loadedCss.set(
url,
@@ -35,8 +35,7 @@ export const loadCssPath = async (key: string): Promise<void> => {
);
if (theme === 'system') {
if (supportsSystemTheme()) {
await load('dark', 'dark');
await load('light', 'light');
await Promise.all([load('dark', 'dark'), load('light', 'light')]);
} else {
await load('dark');
}
@@ -66,8 +65,8 @@ export const userComplete = async (opts: UserCompleteOpts): Promise<UserComplete
};
export const hopscotch = () => {
loadCss('vendor/hopscotch/dist/css/hopscotch.min.css');
return loadIife('vendor/hopscotch/dist/js/hopscotch.min.js', {
loadCss('npm/hopscotch/dist/css/hopscotch.min.css');
return loadIife('npm/hopscotch/dist/js/hopscotch.min.js', {
noVersion: true,
});
};
+11 -14
View File
@@ -1,23 +1,20 @@
import * as xhr from 'common/xhr';
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
lichess.load.then(() => {
$('.forum')
.on('click', 'a.delete', function (this: HTMLAnchorElement) {
const link = this;
modal({
content: $('.forum-delete-modal'),
onInsert($wrap) {
$wrap
.find('form')
.attr('action', link.href)
.on('submit', function (this: HTMLFormElement, e: Event) {
e.preventDefault();
xhr.formToXhr(this);
modal.close();
$(link).closest('.forum-post').hide();
});
},
domDialog({
cash: $('.forum-delete-modal'),
attrs: { view: { action: link.href } },
}).then(dlg => {
$('form', dlg.view).on('submit', () => {
//e.preventDefault();
xhr.text(link.href, { method: 'post' });
$(link).closest('.forum-post').hide();
});
dlg.showModal();
});
return false;
})
-2
View File
@@ -1,5 +1,3 @@
export {}; // for tsc isolatedModules
lichess.load.then(() => {
const form = document.querySelector('.search__form') as HTMLFormElement,
$form = $(form),
+20 -22
View File
@@ -1,5 +1,5 @@
import { text, form } from 'common/xhr';
import modal from 'common/modal';
import { domDialog } from 'common/dialog';
lichess.load.then(() => {
let autoRefreshEnabled = true;
@@ -40,27 +40,25 @@ lichess.load.then(() => {
$('#communication').on('click', '.line:not(.lichess)', function (this: HTMLDivElement) {
const $l = $(this);
modal({
content: $('.timeout-modal'),
onInsert($wrap) {
$wrap.find('.username').text($l.find('.user-link').text());
$wrap.find('.text').text($l.text().split(' ').slice(1).join(' '));
$wrap.on('click', '.button', function (this: HTMLButtonElement) {
const roomId = $l.parents('.game').data('room');
const chan = $l.parents('.game').data('chan');
text('/mod/public-chat/timeout', {
method: 'post',
body: form({
roomId,
chan,
userId: $wrap.find('.username').text().toLowerCase(),
reason: this.value,
text: $wrap.find('.text').text(),
}),
}).then(_ => setTimeout(reloadNow, 1000));
modal.close();
});
},
domDialog({ cash: $('.timeout-modal') }).then(dlg => {
$('.username', dlg.view).text($l.find('.user-link').text());
$('.text', dlg.view).text($l.text().split(' ').slice(1).join(' '));
$('.button', dlg.view).on('click', function (this: HTMLButtonElement) {
const roomId = $l.parents('.game').data('room');
const chan = $l.parents('.game').data('chan');
text('/mod/public-chat/timeout', {
method: 'post',
body: form({
roomId,
chan,
userId: $('.username', dlg.view).text().toLowerCase(),
reason: this.value,
text: $('.text', dlg.view).text(),
}),
}).then(_ => setTimeout(reloadNow, 1000));
dlg.close();
});
dlg.showModal();
});
});
};
+3 -1
View File
@@ -30,9 +30,11 @@ import { format as timeago, formatter as dateFormat } from './component/timeago'
import watchers from './component/watchers';
import { Chessground } from 'chessground';
declare const __debug__: boolean;
export default () => {
window.$as = <T>(cash: Cash) => cash[0] as T;
const l = window.lichess;
l.debug = __debug__;
l.StrongSocket = StrongSocket;
l.mousetrap = new Mousetrap(document);
l.requestIdleCallback = requestIdleCallback;
+2 -12
View File
@@ -18,9 +18,10 @@ import { reload } from './component/reload';
import { requestIdleCallback } from './component/functions';
import { userComplete } from './component/assets';
import { siteTrans } from './component/trans';
import { trapFocus } from 'common/modal';
import { isIOS } from 'common/mobile';
window.$as = <T>(cashOrHtml: Cash | string) =>
(typeof cashOrHtml === 'string' ? $(cashOrHtml) : cashOrHtml)[0] as T;
exportLichessGlobals();
lichess.info = info;
@@ -116,17 +117,6 @@ lichess.load.then(() => {
return false;
});
$('body').on('focusin', trapFocus);
lichess.mousetrap.bind('esc', () => {
const $oc = $('#modal-wrap .close');
if ($oc.length) $oc.trigger('click');
else {
const $input = $(':focus');
if ($input.length) $input.trigger('blur');
}
});
/* Edge randomly fails to rasterize SVG on page load
* A different SVG must be loaded so a new image can be rasterized */
if (navigator.userAgent.includes('Edge/'))
@@ -1,3 +1,2 @@
@import 'tournament.show';
@import '../../../common/css/component/modal';
@import '../team-battle';

Some files were not shown because too many files have changed in this diff Show More