mirror of
https://github.com/lichess-org/lila.git
synced 2026-05-26 13:51:00 +00:00
refactor dialogs
This commit is contained in:
committed by
Jonathan Gamble
parent
8d769b28e1
commit
db1b01ac8c
+2
-7
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Generated
+8
-1
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -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;
|
||||
|
||||
@@ -19,3 +19,4 @@
|
||||
@import 'context-menu';
|
||||
@import 'player-clock';
|
||||
@import 'persistence';
|
||||
@import 'keyboard';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
#modal-wrap {
|
||||
.dialog-content {
|
||||
min-width: 80vw;
|
||||
|
||||
@include breakpoint($mq-x-small) {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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())],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,3 +1,2 @@
|
||||
@import '../../../common/css/plugin';
|
||||
@import '../../../common/css/component/modal';
|
||||
@import '../help';
|
||||
|
||||
+16
-16
@@ -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>',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,3 +36,5 @@
|
||||
@import 'component/reconnecting';
|
||||
@import 'component/announce';
|
||||
@import 'component/agreement';
|
||||
@import 'component/dialog';
|
||||
@import 'component/help';
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
background: lighten($c-primary, 7);
|
||||
}
|
||||
|
||||
&:not(.disabled):focus {
|
||||
&:not(.disabled):focus-visible {
|
||||
@extend %focus-shadow;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.continue-with {
|
||||
@extend %flex-column;
|
||||
|
||||
@media (hover: none) {
|
||||
padding: 2em;
|
||||
}
|
||||
> *:not(:first-child) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -66,10 +66,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.game-setup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.about-side {
|
||||
@extend %page-text-shadow !optional;
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
@import 'setup';
|
||||
@import 'app/hook-filter';
|
||||
+48
-49
@@ -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 +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,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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.keyboard-help {
|
||||
@extend %help-modal;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/plugin';
|
||||
@import '../../../common/css/component/help';
|
||||
@import '../keyboard';
|
||||
@@ -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';
|
||||
@@ -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())],
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.keyboard-help {
|
||||
@extend %help-modal;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/plugin';
|
||||
@import '../../../common/css/component/help';
|
||||
@import '../keyboard';
|
||||
@@ -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';
|
||||
@@ -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())],
|
||||
});
|
||||
|
||||
@@ -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>`,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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,3 +1,2 @@
|
||||
@import '../../../common/css/plugin';
|
||||
@import '../../../common/css/component/modal';
|
||||
@import '../linkPopup';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
@import '../../../common/css/plugin';
|
||||
@import '../../../common/css/component/modal';
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/dir/ltr';
|
||||
@import '../../../common/css/theme/dark';
|
||||
@import 'modal';
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/dir/ltr';
|
||||
@import '../../../common/css/theme/light';
|
||||
@import 'modal';
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/dir/ltr';
|
||||
@import '../../../common/css/theme/transp';
|
||||
@import 'modal';
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/dir/rtl';
|
||||
@import '../../../common/css/theme/dark';
|
||||
@import 'modal';
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/dir/rtl';
|
||||
@import '../../../common/css/theme/light';
|
||||
@import 'modal';
|
||||
@@ -1,3 +0,0 @@
|
||||
@import '../../../common/css/dir/rtl';
|
||||
@import '../../../common/css/theme/transp';
|
||||
@import 'modal';
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
})
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export {}; // for tsc isolatedModules
|
||||
|
||||
lichess.load.then(() => {
|
||||
const form = document.querySelector('.search__form') as HTMLFormElement,
|
||||
$form = $(form),
|
||||
|
||||
+20
-22
@@ -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();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user