refactor ui modals - after #9380

commonModal2
Thibault Duplessis 2021-07-16 14:56:47 +02:00
parent 219e5d5b82
commit c801cd46d3
22 changed files with 226 additions and 183 deletions

View File

@ -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);

View File

@ -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 {

View File

@ -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')

View File

@ -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() {

View File

@ -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
)
);
}

View File

@ -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>'
) ),
); });
}); });
} }

View File

@ -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')),
]; ];
} }

View File

@ -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
)
);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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',
); });
} }

View File

@ -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();
} };

View File

@ -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'),
});
}, },
}, },
}, },

View File

@ -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 {

View File

@ -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>`
) ),
); });
}, },
}; };

View File

@ -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,

View File

@ -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');
}); });
}); });

View File

@ -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) {

View File

@ -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();
});
},
}); });
}); });
}; };

View File

@ -141,8 +141,8 @@ team {
} }
.team-battle__choice { .team-battle__choice {
a { button {
display: block; width: 100%;
margin-top: 1rem; margin-top: 1rem;
} }
} }

View File

@ -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 {