lila/ui/analyse/src/study/chapterNewForm.ts

351 lines
11 KiB
TypeScript

import { h, VNode } from 'snabbdom';
import { defined, prop, Prop } from 'common';
import { storedProp, StoredProp } from 'common/storage';
import * as xhr from 'common/xhr';
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 { ChapterData, ChapterMode, Orientation, StudyChapterMeta } from './interfaces';
import { Redraw } from '../interfaces';
import AnalyseCtrl from '../ctrl';
import { StudySocketSend } from '../socket';
import { parseFen } from 'chessops/fen';
export const modeChoices = [
['normal', 'normalAnalysis'],
['practice', 'practiceWithComputer'],
['conceal', 'hideNextMoves'],
['gamebook', 'interactiveLesson'],
];
export const fieldValue = (e: Event, id: string) =>
((e.target as HTMLElement).querySelector('#chapter-' + id) as HTMLInputElement)?.value;
export interface StudyChapterNewFormCtrl {
root: AnalyseCtrl;
vm: {
variants: Variant[];
open: boolean;
initial: Prop<boolean>;
tab: StoredProp<string>;
editor: LichessEditor | null;
editorFen: Prop<Fen | null>;
isDefaultName: boolean;
};
open(): void;
openInitial(): void;
close(): void;
toggle(): void;
submit(d: Omit<ChapterData, 'initial'>): void;
chapters: Prop<StudyChapterMeta[]>;
startTour(): void;
multiPgnMax: number;
redraw: Redraw;
}
export function ctrl(
send: StudySocketSend,
chapters: Prop<StudyChapterMeta[]>,
setTab: () => void,
root: AnalyseCtrl
): StudyChapterNewFormCtrl {
const multiPgnMax = 20;
const vm = {
variants: [],
open: false,
initial: prop(false),
tab: storedProp('study.form.tab', 'init'),
editor: null,
editorFen: prop(null),
isDefaultName: true,
};
function loadVariants() {
if (!vm.variants.length)
xhrVariants().then(function (vs) {
vm.variants = vs;
root.redraw();
});
}
function open() {
vm.open = true;
loadVariants();
vm.initial(false);
}
function close() {
vm.open = false;
}
return {
vm,
open,
root,
openInitial() {
open();
vm.initial(true);
},
close,
toggle() {
if (vm.open) close();
else open();
},
submit(d) {
const study = root.study!;
const dd = {
...d,
sticky: study.vm.mode.sticky,
initial: vm.initial(),
};
if (!dd.pgn) send('addChapter', dd);
else importPgn(study.data.id, dd);
close();
setTab();
},
chapters,
startTour: () =>
chapterTour(tab => {
vm.tab(tab);
root.redraw();
}),
multiPgnMax,
redraw: root.redraw,
};
}
export function view(ctrl: StudyChapterNewFormCtrl): VNode {
const trans = ctrl.root.trans;
const activeTab = ctrl.vm.tab();
const makeTab = function (key: string, name: string, title: string) {
return h(
'span.' + key,
{
class: { active: activeTab === key },
attrs: { title },
hook: bind('click', () => ctrl.vm.tab(key), ctrl.root.redraw),
},
name
);
};
const gameOrPgn = activeTab === 'game' || activeTab === 'pgn';
const currentChapter = ctrl.root.study!.data.chapter;
const mode = currentChapter.practice
? 'practice'
: defined(currentChapter.conceal)
? 'conceal'
: currentChapter.gamebook
? 'gamebook'
: 'normal';
const noarg = trans.noarg;
return modal.modal({
class: 'chapter-new',
onClose() {
ctrl.close();
ctrl.redraw();
},
noClickAway: true,
content: [
activeTab === 'edit'
? null
: h('h2', [
noarg('newChapter'),
h('i.help', {
attrs: { 'data-icon': '' },
hook: bind('click', ctrl.startTour),
}),
]),
h(
'form.form3',
{
hook: bindSubmit(e => {
ctrl.submit({
name: fieldValue(e, 'name'),
game: fieldValue(e, 'game'),
variant: fieldValue(e, 'variant') as VariantKey,
pgn: fieldValue(e, 'pgn'),
orientation: fieldValue(e, 'orientation') as Orientation,
mode: fieldValue(e, 'mode') as ChapterMode,
fen: fieldValue(e, 'fen') || (ctrl.vm.tab() === 'edit' ? ctrl.vm.editorFen() : null),
isDefaultName: ctrl.vm.isDefaultName,
});
}, ctrl.redraw),
},
[
h('div.form-group', [
h(
'label.form-label',
{
attrs: { for: 'chapter-name' },
},
noarg('name')
),
h('input#chapter-name.form-control', {
attrs: {
minlength: 2,
maxlength: 80,
},
hook: onInsert<HTMLInputElement>(el => {
if (!el.value) {
el.value = trans('chapterX', ctrl.vm.initial() ? 1 : ctrl.chapters().length + 1);
el.onchange = function () {
ctrl.vm.isDefaultName = false;
};
el.select();
el.focus();
}
}),
}),
]),
h('div.tabs-horiz', [
makeTab('init', noarg('empty'), noarg('startFromInitialPosition')),
makeTab('edit', noarg('editor'), noarg('startFromCustomPosition')),
makeTab('game', 'URL', noarg('loadAGameByUrl')),
makeTab('fen', 'FEN', noarg('loadAPositionFromFen')),
makeTab('pgn', 'PGN', noarg('loadAGameFromPgn')),
]),
activeTab === 'edit'
? h(
'div.board-editor-wrap',
{
hook: {
insert(vnode) {
Promise.all([
lichess.loadModule('editor'),
xhr.json(xhr.url('/editor.json', { fen: ctrl.root.node.fen })),
]).then(([_, data]) => {
data.embed = true;
data.options = {
inlineCastling: true,
orientation: currentChapter.setup.orientation,
onChange: ctrl.vm.editorFen,
};
ctrl.vm.editor = window.LichessEditor!(vnode.elm as HTMLElement, data);
ctrl.vm.editorFen(ctrl.vm.editor.getFen());
});
},
destroy: _ => {
ctrl.vm.editor = null;
},
},
},
[spinner()]
)
: null,
activeTab === 'game'
? h('div.form-group', [
h(
'label.form-label',
{
attrs: { for: 'chapter-game' },
},
trans('loadAGameFromXOrY', 'lichess.org', 'chessgames.com')
),
h('textarea#chapter-game.form-control', {
attrs: { placeholder: noarg('urlOfTheGame') },
}),
])
: null,
activeTab === 'fen'
? h('div.form-group', [
h('input#chapter-fen.form-control', {
attrs: {
value: ctrl.root.node.fen,
placeholder: noarg('loadAPositionFromFen'),
},
hook: onInsert((el: HTMLInputElement) => {
el.addEventListener('change', () => el.reportValidity());
el.addEventListener('input', _ =>
el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN')
);
}),
}),
])
: null,
activeTab === 'pgn'
? h('div.form-groupabel', [
h('textarea#chapter-pgn.form-control', {
attrs: { placeholder: trans.plural('pasteYourPgnTextHereUpToNbGames', ctrl.multiPgnMax) },
}),
window.FileReader
? h('input#chapter-pgn-file.form-control', {
attrs: {
type: 'file',
accept: '.pgn',
},
hook: bind('change', e => {
const file = (e.target as HTMLInputElement).files![0];
if (!file) return;
const reader = new FileReader();
reader.onload = function () {
(document.getElementById('chapter-pgn') as HTMLTextAreaElement).value =
reader.result as string;
};
reader.readAsText(file);
}),
})
: null,
])
: null,
h('div.form-split', [
h('div.form-group.form-half', [
h(
'label.form-label',
{
attrs: { for: 'chapter-variant' },
},
noarg('Variant')
),
h(
'select#chapter-variant.form-control',
{
attrs: { disabled: gameOrPgn },
},
gameOrPgn
? [h('option', noarg('automatic'))]
: ctrl.vm.variants.map(v => option(v.key, currentChapter.setup.variant.key, v.name))
),
]),
h('div.form-group.form-half', [
h(
'label.form-label',
{
attrs: { for: 'chapter-orientation' },
},
noarg('orientation')
),
h(
'select#chapter-orientation.form-control',
{
hook: bind('change', e => {
ctrl.vm.editor && ctrl.vm.editor.setOrientation((e.target as HTMLInputElement).value as Color);
}),
},
[...(activeTab === 'pgn' ? ['automatic'] : []), 'white', 'black'].map(c =>
option(c, currentChapter.setup.orientation, noarg(c))
)
),
]),
]),
h('div.form-group', [
h(
'label.form-label',
{
attrs: { for: 'chapter-mode' },
},
noarg('analysisMode')
),
h(
'select#chapter-mode.form-control',
modeChoices.map(c => option(c[0], mode, noarg(c[1])))
),
]),
modal.button(noarg('createChapter')),
]
),
],
});
}