refactor ui modals - after #9380
parent
219e5d5b82
commit
c801cd46d3
|
@ -30,25 +30,25 @@ lichess.studyTourChapter = function (study) {
|
||||||
'A study can have several chapters.<br>' +
|
'A study can have several chapters.<br>' +
|
||||||
'Each chapter has a distinct move tree,<br>' +
|
'Each chapter has a distinct move tree,<br>' +
|
||||||
'and can be created in various ways.',
|
'and can be created in various ways.',
|
||||||
attachTo: '.study__modal label[for=chapter-name] left',
|
attachTo: '#modal-wrap label[for=chapter-name] left',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'From initial position',
|
title: 'From initial position',
|
||||||
text: 'Just a board setup for a new game.<br>' + 'Suited to explore openings.',
|
text: 'Just a board setup for a new game.<br>' + 'Suited to explore openings.',
|
||||||
attachTo: '.study__modal .tabs-horiz .init top',
|
attachTo: '#modal-wrap .tabs-horiz .init top',
|
||||||
when: onTab('init'),
|
when: onTab('init'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Custom position',
|
title: 'Custom position',
|
||||||
text: 'Setup the board your way.<br>' + 'Suited to explore endgames.',
|
text: 'Setup the board your way.<br>' + 'Suited to explore endgames.',
|
||||||
attachTo: '.study__modal .tabs-horiz .edit bottom',
|
attachTo: '#modal-wrap .tabs-horiz .edit bottom',
|
||||||
when: onTab('edit'),
|
when: onTab('edit'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Load an existing lichess game',
|
title: 'Load an existing lichess game',
|
||||||
text:
|
text:
|
||||||
'Paste a lichess game URL<br>' + '(like lichess.org/7fHIU0XI)<br>' + 'to load the game moves in the chapter.',
|
'Paste a lichess game URL<br>' + '(like lichess.org/7fHIU0XI)<br>' + 'to load the game moves in the chapter.',
|
||||||
attachTo: '.study__modal .tabs-horiz .game top',
|
attachTo: '#modal-wrap .tabs-horiz .game top',
|
||||||
when: onTab('game'),
|
when: onTab('game'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -57,19 +57,19 @@ lichess.studyTourChapter = function (study) {
|
||||||
'Paste a position in FEN format<br>' +
|
'Paste a position in FEN format<br>' +
|
||||||
'<i>4k3/4rb2/8/7p/8/5Q2/1PP5/1K6 w</i><br>' +
|
'<i>4k3/4rb2/8/7p/8/5Q2/1PP5/1K6 w</i><br>' +
|
||||||
'to start the chapter from a position.',
|
'to start the chapter from a position.',
|
||||||
attachTo: '.study__modal .tabs-horiz .fen top',
|
attachTo: '#modal-wrap .tabs-horiz .fen top',
|
||||||
when: onTab('fen'),
|
when: onTab('fen'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'From a PGN game',
|
title: 'From a PGN game',
|
||||||
text: 'Paste a game in PGN format.<br>' + 'to load moves, comments and variations in the chapter.',
|
text: 'Paste a game in PGN format.<br>' + 'to load moves, comments and variations in the chapter.',
|
||||||
attachTo: '.study__modal .tabs-horiz .pgn top',
|
attachTo: '#modal-wrap .tabs-horiz .pgn top',
|
||||||
when: onTab('pgn'),
|
when: onTab('pgn'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Studies support variants',
|
title: 'Studies support variants',
|
||||||
text: 'Yes, you can study crazyhouse,<br>' + 'and all lichess variants!',
|
text: 'Yes, you can study crazyhouse,<br>' + 'and all lichess variants!',
|
||||||
attachTo: '.study__modal label[for=chapter-variant] left',
|
attachTo: '#modal-wrap label[for=chapter-variant] left',
|
||||||
when: onTab('init'),
|
when: onTab('init'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -81,7 +81,7 @@ lichess.studyTourChapter = function (study) {
|
||||||
action: tour.next,
|
action: tour.next,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
attachTo: '.study__modal .help bottom',
|
attachTo: '#modal-wrap .help bottom',
|
||||||
},
|
},
|
||||||
].forEach(function (s) {
|
].forEach(function (s) {
|
||||||
tour.addStep(s.title, s);
|
tour.addStep(s.title, s);
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
.study__modal {
|
#modal-wrap {
|
||||||
&#modal-wrap {
|
padding: 1rem;
|
||||||
padding: 1rem;
|
min-width: 80vw;
|
||||||
min-width: 80vw;
|
|
||||||
|
|
||||||
@include breakpoint($mq-x-small) {
|
@include breakpoint($mq-x-small) {
|
||||||
min-width: 500px;
|
min-width: 500px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
|
|
|
@ -200,7 +200,11 @@ export function view(ctrl: AnalyseCtrl): VNode {
|
||||||
? h(
|
? h(
|
||||||
'a.button.button-empty',
|
'a.button.button-empty',
|
||||||
{
|
{
|
||||||
hook: bind('click', _ => modal($('.continue-with.g_' + d.game.id))),
|
hook: bind('click', _ =>
|
||||||
|
modal({
|
||||||
|
content: $('.continue-with.g_' + d.game.id),
|
||||||
|
})
|
||||||
|
),
|
||||||
attrs: dataIcon(''),
|
attrs: dataIcon(''),
|
||||||
},
|
},
|
||||||
noarg('continueFromHere')
|
noarg('continueFromHere')
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as control from './control';
|
||||||
import * as xhr from 'common/xhr';
|
import * as xhr from 'common/xhr';
|
||||||
import AnalyseCtrl from './ctrl';
|
import AnalyseCtrl from './ctrl';
|
||||||
import { h, VNode } from 'snabbdom';
|
import { h, VNode } from 'snabbdom';
|
||||||
import { modal } from './modal';
|
import { snabModal } from 'common/modal';
|
||||||
import { spinner } from './util';
|
import { spinner } from './util';
|
||||||
|
|
||||||
export const bind = (ctrl: AnalyseCtrl) => {
|
export const bind = (ctrl: AnalyseCtrl) => {
|
||||||
|
@ -93,12 +93,12 @@ export const bind = (ctrl: AnalyseCtrl) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function view(ctrl: AnalyseCtrl): VNode {
|
export function view(ctrl: AnalyseCtrl): VNode {
|
||||||
return modal({
|
return snabModal({
|
||||||
class: 'keyboard-help',
|
class: 'keyboard-help',
|
||||||
onInsert(el: HTMLElement) {
|
onInsert($wrap: Cash) {
|
||||||
lichess.loadCssPath('analyse.keyboard');
|
lichess.loadCssPath('analyse.keyboard');
|
||||||
xhr.text(xhr.url('/analysis/help', { study: !!ctrl.study })).then(html => {
|
xhr.text(xhr.url('/analysis/help', { study: !!ctrl.study })).then(html => {
|
||||||
el.querySelector('.scrollable')!.innerHTML = html;
|
$wrap.find('.scrollable').html(html);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onClose() {
|
onClose() {
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { h, VNode } from 'snabbdom';
|
|
||||||
import { focusFirstChild } from 'common/modal';
|
|
||||||
import { MaybeVNodes } from './interfaces';
|
|
||||||
import { bind, onInsert } from './util';
|
|
||||||
|
|
||||||
interface Modal {
|
|
||||||
class?: string;
|
|
||||||
content: MaybeVNodes;
|
|
||||||
onInsert?: (el: HTMLElement) => void;
|
|
||||||
onClose(): void;
|
|
||||||
noClickAway?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function modal(d: Modal): VNode {
|
|
||||||
return h(
|
|
||||||
'div#modal-overlay',
|
|
||||||
{
|
|
||||||
...(!d.noClickAway ? { hook: bind('mousedown', d.onClose) } : {}),
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h(
|
|
||||||
'div#modal-wrap.study__modal.' + d.class,
|
|
||||||
{
|
|
||||||
hook: onInsert(el => {
|
|
||||||
focusFirstChild($(el));
|
|
||||||
el.addEventListener('mousedown', e => e.stopPropagation());
|
|
||||||
d.onInsert && d.onInsert(el);
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h('span.close', {
|
|
||||||
attrs: {
|
|
||||||
'data-icon': '',
|
|
||||||
role: 'button',
|
|
||||||
'aria-label': 'Close',
|
|
||||||
tabindex: '0',
|
|
||||||
},
|
|
||||||
hook: onInsert(el => {
|
|
||||||
el.addEventListener('click', d.onClose);
|
|
||||||
el.addEventListener('keydown', e => (e.code === 'Enter' || e.code === 'Space' ? d.onClose() : true));
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
h('div', d.content),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function button(name: string): VNode {
|
|
||||||
return h(
|
|
||||||
'div.form-actions.single',
|
|
||||||
h(
|
|
||||||
'button.button',
|
|
||||||
{
|
|
||||||
attrs: { type: 'submit' },
|
|
||||||
},
|
|
||||||
name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -136,8 +136,8 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) {
|
||||||
$panels.on('click', '.embed-howto', function (this: HTMLElement) {
|
$panels.on('click', '.embed-howto', function (this: HTMLElement) {
|
||||||
const url = `${baseUrl()}/embed/${data.game.id}${location.hash}`;
|
const url = `${baseUrl()}/embed/${data.game.id}${location.hash}`;
|
||||||
const iframe = '<iframe src="' + url + '?theme=auto&bg=auto"\nwidth=600 height=397 frameborder=0></iframe>';
|
const iframe = '<iframe src="' + url + '?theme=auto&bg=auto"\nwidth=600 height=397 frameborder=0></iframe>';
|
||||||
modal(
|
modal({
|
||||||
$(
|
content: $(
|
||||||
'<strong style="font-size:1.5em">' +
|
'<strong style="font-size:1.5em">' +
|
||||||
$(this).html() +
|
$(this).html() +
|
||||||
'</strong><br /><br />' +
|
'</strong><br /><br />' +
|
||||||
|
@ -147,7 +147,7 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) {
|
||||||
iframe +
|
iframe +
|
||||||
'<br /><br />' +
|
'<br /><br />' +
|
||||||
'<a class="text" data-icon="" href="/developers#embed-game">Read more about embedding games</a>'
|
'<a class="text" data-icon="" href="/developers#embed-game">Read more about embedding games</a>'
|
||||||
)
|
),
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { h, VNode } from 'snabbdom';
|
|
||||||
import { defined, prop, Prop } from 'common';
|
|
||||||
import { Redraw } from '../interfaces';
|
|
||||||
import { bind, bindSubmit, spinner, option, onInsert, emptyRedButton } from '../util';
|
|
||||||
import * as modal from '../modal';
|
|
||||||
import * as chapterForm from './chapterNewForm';
|
import * as chapterForm from './chapterNewForm';
|
||||||
|
import { bind, bindSubmit, spinner, option, onInsert, emptyRedButton } from '../util';
|
||||||
import { ChapterMode, EditChapterData, Orientation, StudyChapterConfig, StudyChapterMeta } from './interfaces';
|
import { ChapterMode, EditChapterData, Orientation, StudyChapterConfig, StudyChapterMeta } from './interfaces';
|
||||||
|
import { defined, prop, Prop } from 'common';
|
||||||
|
import { h, VNode } from 'snabbdom';
|
||||||
|
import { modalButton } from './chapterNewForm';
|
||||||
|
import { Redraw } from '../interfaces';
|
||||||
|
import { snabModal } from 'common/modal';
|
||||||
import { StudySocketSend } from '../socket';
|
import { StudySocketSend } from '../socket';
|
||||||
|
|
||||||
export interface StudyChapterEditFormCtrl {
|
export interface StudyChapterEditFormCtrl {
|
||||||
|
@ -75,7 +76,7 @@ export function ctrl(
|
||||||
export function view(ctrl: StudyChapterEditFormCtrl): VNode | undefined {
|
export function view(ctrl: StudyChapterEditFormCtrl): VNode | undefined {
|
||||||
const data = ctrl.current();
|
const data = ctrl.current();
|
||||||
return data
|
return data
|
||||||
? modal.modal({
|
? snabModal({
|
||||||
class: 'edit-' + data.id, // full redraw when changing chapter
|
class: 'edit-' + data.id, // full redraw when changing chapter
|
||||||
onClose() {
|
onClose() {
|
||||||
ctrl.current(null);
|
ctrl.current(null);
|
||||||
|
@ -201,6 +202,6 @@ function viewLoaded(ctrl: StudyChapterEditFormCtrl, data: StudyChapterConfig): V
|
||||||
].map(v => option(v[0], data.description ? '1' : '', v[1]))
|
].map(v => option(v[0], data.description ? '1' : '', v[1]))
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
modal.button(ctrl.trans.noarg('saveChapter')),
|
modalButton(ctrl.trans.noarg('saveChapter')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { h, VNode } from 'snabbdom';
|
|
||||||
import { defined, prop, Prop } from 'common';
|
|
||||||
import { storedProp, StoredProp } from 'common/storage';
|
|
||||||
import * as xhr from 'common/xhr';
|
import * as xhr from 'common/xhr';
|
||||||
|
import AnalyseCtrl from '../ctrl';
|
||||||
import { bind, bindSubmit, spinner, option, onInsert } from '../util';
|
import { bind, bindSubmit, spinner, option, onInsert } from '../util';
|
||||||
import { variants as xhrVariants, importPgn } from './studyXhr';
|
|
||||||
import * as modal from '../modal';
|
|
||||||
import { chapter as chapterTour } from './studyTour';
|
import { chapter as chapterTour } from './studyTour';
|
||||||
import { ChapterData, ChapterMode, Orientation, StudyChapterMeta } from './interfaces';
|
import { ChapterData, ChapterMode, Orientation, StudyChapterMeta } from './interfaces';
|
||||||
import { Redraw } from '../interfaces';
|
import { defined, prop, Prop } from 'common';
|
||||||
import AnalyseCtrl from '../ctrl';
|
import { h, VNode } from 'snabbdom';
|
||||||
import { StudySocketSend } from '../socket';
|
|
||||||
import { parseFen } from 'chessops/fen';
|
import { parseFen } from 'chessops/fen';
|
||||||
|
import { Redraw } from '../interfaces';
|
||||||
|
import { snabModal } from 'common/modal';
|
||||||
|
import { storedProp, StoredProp } from 'common/storage';
|
||||||
|
import { StudySocketSend } from '../socket';
|
||||||
|
import { variants as xhrVariants, importPgn } from './studyXhr';
|
||||||
|
|
||||||
export const modeChoices = [
|
export const modeChoices = [
|
||||||
['normal', 'normalAnalysis'],
|
['normal', 'normalAnalysis'],
|
||||||
|
@ -140,7 +140,7 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode {
|
||||||
: 'normal';
|
: 'normal';
|
||||||
const noarg = trans.noarg;
|
const noarg = trans.noarg;
|
||||||
|
|
||||||
return modal.modal({
|
return snabModal({
|
||||||
class: 'chapter-new',
|
class: 'chapter-new',
|
||||||
onClose() {
|
onClose() {
|
||||||
ctrl.close();
|
ctrl.close();
|
||||||
|
@ -342,9 +342,22 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode {
|
||||||
modeChoices.map(c => option(c[0], mode, noarg(c[1])))
|
modeChoices.map(c => option(c[0], mode, noarg(c[1])))
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
modal.button(noarg('createChapter')),
|
modalButton(noarg('createChapter')),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function modalButton(name: string): VNode {
|
||||||
|
return h(
|
||||||
|
'div.form-actions.single',
|
||||||
|
h(
|
||||||
|
'button.button',
|
||||||
|
{
|
||||||
|
attrs: { type: 'submit' },
|
||||||
|
},
|
||||||
|
name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { bind, titleNameToId, onInsert } from '../util';
|
import { bind, titleNameToId, onInsert } from '../util';
|
||||||
import { h, VNode } from 'snabbdom';
|
import { h, VNode } from 'snabbdom';
|
||||||
import { modal } from '../modal';
|
import { snabModal } from 'common/modal';
|
||||||
import { prop, Prop } from 'common';
|
import { prop, Prop } from 'common';
|
||||||
import { StudyMemberMap } from './interfaces';
|
import { StudyMemberMap } from './interfaces';
|
||||||
import { AnalyseSocketSend } from '../socket';
|
import { AnalyseSocketSend } from '../socket';
|
||||||
|
@ -45,7 +45,7 @@ export function view(ctrl: ReturnType<typeof makeCtrl>): VNode {
|
||||||
.spectators()
|
.spectators()
|
||||||
.filter(s => !ctrl.members()[titleNameToId(s)]) // remove existing members
|
.filter(s => !ctrl.members()[titleNameToId(s)]) // remove existing members
|
||||||
.sort();
|
.sort();
|
||||||
return modal({
|
return snabModal({
|
||||||
class: 'study__invite',
|
class: 'study__invite',
|
||||||
onClose() {
|
onClose() {
|
||||||
ctrl.open(false);
|
ctrl.open(false);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { h, VNode } from 'snabbdom';
|
import { h, VNode } from 'snabbdom';
|
||||||
import * as modal from '../modal';
|
import { snabModal } from 'common/modal';
|
||||||
import { prop, Prop } from 'common';
|
import { prop, Prop } from 'common';
|
||||||
import { bind, bindSubmit, emptyRedButton } from '../util';
|
import { bind, bindSubmit, emptyRedButton } from '../util';
|
||||||
import { StudyData } from './interfaces';
|
import { StudyData } from './interfaces';
|
||||||
|
@ -114,7 +114,7 @@ export function view(ctrl: StudyFormCtrl): VNode {
|
||||||
['member', ctrl.trans.noarg('members')],
|
['member', ctrl.trans.noarg('members')],
|
||||||
['everyone', ctrl.trans.noarg('everyone')],
|
['everyone', ctrl.trans.noarg('everyone')],
|
||||||
];
|
];
|
||||||
return modal.modal({
|
return snabModal({
|
||||||
class: 'study-edit',
|
class: 'study-edit',
|
||||||
onClose() {
|
onClose() {
|
||||||
ctrl.open(false);
|
ctrl.open(false);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as modal from '../modal';
|
import { snabModal } from 'common/modal';
|
||||||
import * as xhr from 'common/xhr';
|
import * as xhr from 'common/xhr';
|
||||||
import { bind, bindSubmit, onInsert } from '../util';
|
import { bind, bindSubmit, onInsert } from '../util';
|
||||||
import { h, VNode } from 'snabbdom';
|
import { h, VNode } from 'snabbdom';
|
||||||
|
@ -61,7 +61,7 @@ export function view(ctrl: StudyCtrl): VNode {
|
||||||
let tagify: Tagify | undefined;
|
let tagify: Tagify | undefined;
|
||||||
|
|
||||||
export function formView(ctrl: TopicsCtrl, userId?: string): VNode {
|
export function formView(ctrl: TopicsCtrl, userId?: string): VNode {
|
||||||
return modal.modal({
|
return snabModal({
|
||||||
class: 'study-topics',
|
class: 'study-topics',
|
||||||
onClose() {
|
onClose() {
|
||||||
ctrl.open(false);
|
ctrl.open(false);
|
||||||
|
|
|
@ -61,8 +61,8 @@ function commandHelp(aliases: string, args: string, desc: string) {
|
||||||
|
|
||||||
function help() {
|
function help() {
|
||||||
lichess.loadCssPath('clinput.help');
|
lichess.loadCssPath('clinput.help');
|
||||||
modal(
|
modal({
|
||||||
$(
|
content: $(
|
||||||
'<h3>Commands</h3>' +
|
'<h3>Commands</h3>' +
|
||||||
commandHelp('/tv /follow', ' <user>', 'Watch someone play') +
|
commandHelp('/tv /follow', ' <user>', 'Watch someone play') +
|
||||||
commandHelp('/play /challenge /match', ' <user>', 'Challenge someone to play') +
|
commandHelp('/play /challenge /match', ' <user>', 'Challenge someone to play') +
|
||||||
|
@ -74,6 +74,6 @@ function help() {
|
||||||
commandHelp('c', '', 'Focus the chat input') +
|
commandHelp('c', '', 'Focus the chat input') +
|
||||||
commandHelp('esc', '', 'Close modals like this one')
|
commandHelp('esc', '', 'Close modals like this one')
|
||||||
),
|
),
|
||||||
'clinput-help'
|
class: 'clinput-help',
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,42 @@
|
||||||
export default function modal(content: Cash, cls?: string, onClose?: () => void) {
|
import { h, VNode } from 'snabbdom';
|
||||||
|
import { bind, MaybeVNodes, onInsert } from './snabbdom';
|
||||||
|
|
||||||
|
interface BaseModal {
|
||||||
|
class?: string;
|
||||||
|
onInsert?: ($wrap: Cash) => void;
|
||||||
|
onClose?(): void;
|
||||||
|
noClickAway?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Modal extends BaseModal {
|
||||||
|
content: Cash;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SnabModal extends BaseModal {
|
||||||
|
content: MaybeVNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function modal(opts: Modal) {
|
||||||
modal.close();
|
modal.close();
|
||||||
const $wrap = $(
|
const $wrap = $(
|
||||||
'<div id="modal-wrap"><span class="close" role="button" aria-label="Close" data-icon="" tabindex="0"></span></div>'
|
'<div id="modal-wrap"><span class="close" role="button" aria-label="Close" data-icon="" tabindex="0"></span></div>'
|
||||||
);
|
);
|
||||||
const $overlay = $(`<div id="modal-overlay" class="${cls}">`).on('click', modal.close);
|
const $overlay = $(`<div id="modal-overlay" class="${opts.class}">`);
|
||||||
|
if (!opts.noClickAway) $overlay.on('click', modal.close);
|
||||||
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
|
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
|
||||||
$wrap.appendTo($overlay);
|
$wrap.appendTo($overlay);
|
||||||
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
|
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
|
||||||
content.clone().removeClass('none').appendTo($wrap);
|
opts.content.clone().removeClass('none').appendTo($wrap);
|
||||||
modal.onClose = onClose;
|
opts.onInsert && opts.onInsert($wrap);
|
||||||
$wrap
|
modal.onClose = opts.onClose;
|
||||||
.find('.close')
|
$wrap.find('.close').each(function (this: HTMLElement) {
|
||||||
.on('click', modal.close)
|
bindClose(this, modal.close);
|
||||||
.on('keydown', (e: KeyboardEvent) => (e.code === 'Space' || e.code === 'Enter' ? modal.close() : true));
|
});
|
||||||
$wrap.on('click', (e: Event) => e.stopPropagation());
|
|
||||||
$('body').addClass('overlayed').prepend($overlay);
|
$('body').addClass('overlayed').prepend($overlay);
|
||||||
focusFirstChild($wrap);
|
bindWrap($wrap);
|
||||||
return $wrap;
|
return $wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.close = () => {
|
modal.close = () => {
|
||||||
$('body').removeClass('overlayed');
|
$('body').removeClass('overlayed');
|
||||||
$('#modal-overlay').each(function (this: HTMLElement) {
|
$('#modal-overlay').each(function (this: HTMLElement) {
|
||||||
|
@ -26,14 +45,57 @@ modal.close = () => {
|
||||||
});
|
});
|
||||||
delete modal.onClose;
|
delete modal.onClose;
|
||||||
};
|
};
|
||||||
|
|
||||||
modal.onClose = undefined as (() => void) | undefined;
|
modal.onClose = undefined as (() => void) | undefined;
|
||||||
|
|
||||||
|
export function snabModal(opts: SnabModal): VNode {
|
||||||
|
const close = opts.onClose!;
|
||||||
|
return h(
|
||||||
|
'div#modal-overlay',
|
||||||
|
{
|
||||||
|
...(opts.onClose && !opts.noClickAway ? { hook: bind('click', close) } : {}),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
'div#modal-wrap.' + opts.class,
|
||||||
|
{
|
||||||
|
hook: onInsert(el => {
|
||||||
|
bindWrap($(el));
|
||||||
|
opts.onInsert && opts.onInsert($(el));
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h('span.close', {
|
||||||
|
attrs: {
|
||||||
|
'data-icon': '',
|
||||||
|
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 =
|
const focusableSelectors =
|
||||||
'button:not(:disabled), [href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]';
|
'button:not(:disabled), [href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]';
|
||||||
|
|
||||||
export function trapFocus(event: FocusEvent) {
|
export function trapFocus(event: FocusEvent) {
|
||||||
const wrap: HTMLElement | undefined = $('#modal-wrap').get(0);
|
const wrap: HTMLElement | undefined = $('#modal-wrap')[0];
|
||||||
console.log(wrap);
|
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
const position = wrap.compareDocumentPosition(event.target as HTMLElement);
|
const position = wrap.compareDocumentPosition(event.target as HTMLElement);
|
||||||
if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) return;
|
if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) return;
|
||||||
|
@ -43,8 +105,8 @@ export function trapFocus(event: FocusEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusFirstChild(parent: Cash) {
|
export const focusFirstChild = (parent: Cash) => {
|
||||||
const children = parent.find(focusableSelectors);
|
const children = parent.find(focusableSelectors);
|
||||||
// prefer child 1 over child 0 because child 0 should be a close button
|
// prefer child 1 over child 0 because child 0 should be a close button
|
||||||
(children.get(1) ?? children.get(0))?.focus();
|
(children[1] ?? children[0])?.focus();
|
||||||
}
|
};
|
||||||
|
|
|
@ -285,7 +285,10 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode {
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
click: () => {
|
click: () => {
|
||||||
if (state.playable) modal($('.continue-with'));
|
if (state.playable)
|
||||||
|
modal({
|
||||||
|
content: $('.continue-with'),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -90,7 +90,13 @@ export default function LichessLobby(opts: LobbyOpts) {
|
||||||
res.text().then(text => {
|
res.text().then(text => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
lobby.setup.prepareForm(
|
lobby.setup.prepareForm(
|
||||||
modal($(text), 'game-setup', () => $startButtons.find('.active').removeClass('active'))
|
modal({
|
||||||
|
content: $(text),
|
||||||
|
class: 'game-setup',
|
||||||
|
onClose() {
|
||||||
|
$startButtons.find('.active').removeClass('active');
|
||||||
|
},
|
||||||
|
})
|
||||||
);
|
);
|
||||||
lichess.contentLoaded();
|
lichess.contentLoaded();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -153,12 +153,12 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
|
||||||
},
|
},
|
||||||
simulEnd(simul: game.Simul) {
|
simulEnd(simul: game.Simul) {
|
||||||
lichess.loadCssPath('modal');
|
lichess.loadCssPath('modal');
|
||||||
modal(
|
modal({
|
||||||
$(
|
content: $(
|
||||||
'<p>Simul complete!</p><br /><br />' +
|
'<p>Simul complete!</p><br /><br />' +
|
||||||
`<a class="button" href="/simul/${simul.id}">Back to ${simul.name} simul</a>`
|
`<a class="button" href="/simul/${simul.id}">Back to ${simul.name} simul</a>`
|
||||||
)
|
),
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -49,10 +49,14 @@ export default function (showText: (ctrl: SimulCtrl) => VNode) {
|
||||||
: util.bind('click', () => {
|
: util.bind('click', () => {
|
||||||
if (ctrl.data.variants.length === 1) xhr.join(ctrl.data.id, ctrl.data.variants[0].key);
|
if (ctrl.data.variants.length === 1) xhr.join(ctrl.data.id, ctrl.data.variants[0].key);
|
||||||
else {
|
else {
|
||||||
modal($('.simul .continue-with'));
|
modal({
|
||||||
$('#modal-wrap .continue-with a').on('click', function (this: HTMLElement) {
|
content: $('.simul .continue-with'),
|
||||||
modal.close();
|
onInsert($wrap) {
|
||||||
xhr.join(ctrl.data.id, $(this).data('variant'));
|
$wrap.find('button').on('click', function (this: HTMLElement) {
|
||||||
|
modal.close();
|
||||||
|
xhr.join(ctrl.data.id, $(this).data('variant'));
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -200,7 +204,7 @@ export default function (showText: (ctrl: SimulCtrl) => VNode) {
|
||||||
'div.continue-with.none',
|
'div.continue-with.none',
|
||||||
ctrl.data.variants.map(function (variant) {
|
ctrl.data.variants.map(function (variant) {
|
||||||
return h(
|
return h(
|
||||||
'a.button',
|
'button.button',
|
||||||
{
|
{
|
||||||
attrs: {
|
attrs: {
|
||||||
'data-variant': variant.key,
|
'data-variant': variant.key,
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default function (publicKey: string, pricing: Pricing) {
|
||||||
.redirectToCheckout({
|
.redirectToCheckout({
|
||||||
sessionId: data.session.id,
|
sessionId: data.session.id,
|
||||||
})
|
})
|
||||||
.then(result => showErrorThenReload(result.error.message));
|
.then((result: any) => showErrorThenReload(result.error.message));
|
||||||
} else location.assign('/patron');
|
} else location.assign('/patron');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,16 +5,20 @@ lichess.load.then(() => {
|
||||||
$('.forum')
|
$('.forum')
|
||||||
.on('click', 'a.delete', function (this: HTMLAnchorElement) {
|
.on('click', 'a.delete', function (this: HTMLAnchorElement) {
|
||||||
const link = this;
|
const link = this;
|
||||||
const $wrap = modal($('.forum-delete-modal'));
|
modal({
|
||||||
$wrap
|
content: $('.forum-delete-modal'),
|
||||||
.find('form')
|
onInsert($wrap) {
|
||||||
.attr('action', link.href)
|
$wrap
|
||||||
.on('submit', function (this: HTMLFormElement, e: Event) {
|
.find('form')
|
||||||
e.preventDefault();
|
.attr('action', link.href)
|
||||||
xhr.formToXhr(this);
|
.on('submit', function (this: HTMLFormElement, e: Event) {
|
||||||
modal.close();
|
e.preventDefault();
|
||||||
$(link).closest('.forum-post').hide();
|
xhr.formToXhr(this);
|
||||||
});
|
modal.close();
|
||||||
|
$(link).closest('.forum-post').hide();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.on('click', 'form.unsub button', function (this: HTMLButtonElement) {
|
.on('click', 'form.unsub button', function (this: HTMLButtonElement) {
|
||||||
|
|
|
@ -40,23 +40,27 @@ lichess.load.then(() => {
|
||||||
|
|
||||||
$('#communication').on('click', '.line:not(.lichess)', function (this: HTMLDivElement) {
|
$('#communication').on('click', '.line:not(.lichess)', function (this: HTMLDivElement) {
|
||||||
const $l = $(this);
|
const $l = $(this);
|
||||||
const roomId = $l.parents('.game').data('room');
|
modal({
|
||||||
const chan = $l.parents('.game').data('chan');
|
content: $('.timeout-modal'),
|
||||||
const $wrap = modal($('.timeout-modal'));
|
onInsert($wrap) {
|
||||||
$wrap.find('.username').text($l.find('.user-link').text());
|
$wrap.find('.username').text($l.find('.user-link').text());
|
||||||
$wrap.find('.text').text($l.text().split(' ').slice(1).join(' '));
|
$wrap.find('.text').text($l.text().split(' ').slice(1).join(' '));
|
||||||
$wrap.on('click', '.button', function (this: HTMLButtonElement) {
|
$wrap.on('click', '.button', function (this: HTMLButtonElement) {
|
||||||
modal.close();
|
const roomId = $l.parents('.game').data('room');
|
||||||
text('/mod/public-chat/timeout', {
|
const chan = $l.parents('.game').data('chan');
|
||||||
method: 'post',
|
text('/mod/public-chat/timeout', {
|
||||||
body: form({
|
method: 'post',
|
||||||
roomId,
|
body: form({
|
||||||
chan,
|
roomId,
|
||||||
userId: $wrap.find('.username').text().toLowerCase(),
|
chan,
|
||||||
reason: this.value,
|
userId: $wrap.find('.username').text().toLowerCase(),
|
||||||
text: $wrap.find('.text').text(),
|
reason: this.value,
|
||||||
}),
|
text: $wrap.find('.text').text(),
|
||||||
}).then(_ => setTimeout(reloadNow, 1000));
|
}),
|
||||||
|
}).then(_ => setTimeout(reloadNow, 1000));
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -141,8 +141,8 @@ team {
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-battle__choice {
|
.team-battle__choice {
|
||||||
a {
|
button {
|
||||||
display: block;
|
width: 100%;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import TournamentController from '../ctrl';
|
import TournamentController from '../ctrl';
|
||||||
import { bind, onInsert, playerName } from './util';
|
import { bind, playerName } from './util';
|
||||||
import { h, VNode } from 'snabbdom';
|
import { h, VNode } from 'snabbdom';
|
||||||
import { TeamBattle, RankedTeam, MaybeVNode } from '../interfaces';
|
import { TeamBattle, RankedTeam, MaybeVNode } from '../interfaces';
|
||||||
import modal from 'common/modal';
|
import modal, { snabModal } from 'common/modal';
|
||||||
|
|
||||||
export function joinWithTeamSelector(ctrl: TournamentController) {
|
export function joinWithTeamSelector(ctrl: TournamentController) {
|
||||||
const onClose = () => {
|
|
||||||
ctrl.joinWithTeamSelector = false;
|
|
||||||
ctrl.redraw();
|
|
||||||
};
|
|
||||||
const tb = ctrl.data.teamBattle!;
|
const tb = ctrl.data.teamBattle!;
|
||||||
return h(
|
return snabModal({
|
||||||
'div.none',
|
class: 'team-battle__choice',
|
||||||
{
|
onInsert($el) {
|
||||||
hook: onInsert(el => modal($(el), 'team-battle__choice', onClose)),
|
$el.on('click', '.team-picker__team', e => {
|
||||||
|
ctrl.join(e.target.dataset['id']);
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[
|
onClose() {
|
||||||
|
ctrl.joinWithTeamSelector = false;
|
||||||
|
ctrl.redraw();
|
||||||
|
},
|
||||||
|
content: [
|
||||||
h('div.team-picker', [
|
h('div.team-picker', [
|
||||||
h('h2', 'Pick your team'),
|
h('h2', 'Pick your team'),
|
||||||
h('br'),
|
h('br'),
|
||||||
|
@ -24,9 +27,11 @@ export function joinWithTeamSelector(ctrl: TournamentController) {
|
||||||
h('p', 'Which team will you represent in this battle?'),
|
h('p', 'Which team will you represent in this battle?'),
|
||||||
...tb.joinWith.map(id =>
|
...tb.joinWith.map(id =>
|
||||||
h(
|
h(
|
||||||
'a.button',
|
'button.button.team-picker__team',
|
||||||
{
|
{
|
||||||
hook: bind('click', () => ctrl.join(id), ctrl.redraw),
|
attrs: {
|
||||||
|
'data-id': id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tb.teams[id]
|
tb.teams[id]
|
||||||
)
|
)
|
||||||
|
@ -51,8 +56,8 @@ export function joinWithTeamSelector(ctrl: TournamentController) {
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
]
|
],
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function teamStanding(ctrl: TournamentController, klass?: string): VNode | null {
|
export function teamStanding(ctrl: TournamentController, klass?: string): VNode | null {
|
||||||
|
|
Loading…
Reference in New Issue