diff --git a/app/controllers/Editor.scala b/app/controllers/Editor.scala index ffbe8f1882..45ea5e62d4 100644 --- a/app/controllers/Editor.scala +++ b/app/controllers/Editor.scala @@ -33,17 +33,14 @@ final class Editor(env: Env) extends LilaController(env) { def load(urlFen: String) = Open { implicit ctx => - val fenStr = lila.common.String + val fen = lila.common.String .decodeUriPath(urlFen) .map(_.replace('_', ' ').trim) .filter(_.nonEmpty) - .orElse(get("fen")) fuccess { - val situation = readFen(fenStr) Ok( html.board.editor( - sit = situation, - fen = Forsyth >> situation, + fen, positionsJson, endgamePositionsJson ) @@ -54,20 +51,12 @@ final class Editor(env: Env) extends LilaController(env) { def data = Open { implicit ctx => fuccess { - val situation = readFen(get("fen")) JsonOk( - html.board.bits.jsData( - sit = situation, - fen = Forsyth >> situation - ) + html.board.bits.jsData() ) } } - private def readFen(fen: Option[String]): Situation = - fen.map(_.trim).filter(_.nonEmpty).map(FEN.clean).flatMap(Forsyth.<<<).map(_.situation) | - Situation(chess.variant.Standard) - def game(id: String) = Open { implicit ctx => OptionResult(env.game.gameRepo game id) { game => diff --git a/app/views/board/bits.scala b/app/views/board/bits.scala index 650065bde6..0066509ad4 100644 --- a/app/views/board/bits.scala +++ b/app/views/board/bits.scala @@ -32,24 +32,14 @@ object bits { def miniSpan(fen: chess.format.FEN, color: chess.Color = chess.White, lastMove: String = "") = mini(fen, color, lastMove)(span) - def jsData( - sit: chess.Situation, - fen: FEN - )(implicit ctx: Context) = + def jsData(fen: Option[String] = None)(implicit ctx: Context) = Json.obj( - "fen" -> fen.value.split(" ").take(4).mkString(" "), - "baseUrl" -> s"$netBaseUrl${routes.Editor.load("")}", - "color" -> sit.color.letter.toString, - "castles" -> Json.obj( - "K" -> (sit canCastle chess.White on chess.KingSide), - "Q" -> (sit canCastle chess.White on chess.QueenSide), - "k" -> (sit canCastle chess.Black on chess.KingSide), - "q" -> (sit canCastle chess.Black on chess.QueenSide) - ), + "baseUrl" -> s"$netBaseUrl${routes.Editor.load("")}", "animation" -> Json.obj("duration" -> ctx.pref.animationMillis), "is3d" -> ctx.pref.is3d, "i18n" -> i18nJsObject(i18nKeyes) ) + .add("fen" -> fen) private val i18nKeyes = List( trans.setTheBoard, diff --git a/app/views/board/editor.scala b/app/views/board/editor.scala index c99bb78136..39b148c188 100644 --- a/app/views/board/editor.scala +++ b/app/views/board/editor.scala @@ -1,6 +1,5 @@ package views.html.board -import chess.format.FEN import controllers.routes import lila.api.Context @@ -11,8 +10,7 @@ import lila.common.String.html.safeJsonValue object editor { def apply( - sit: chess.Situation, - fen: FEN, + fen: Option[String], positionsJson: String, endgamePositionsJson: String )(implicit ctx: Context) = @@ -21,7 +19,7 @@ object editor { moreJs = frag( jsModule("editor"), embedJsUnsafeLoadThen( - s"""const data=${safeJsonValue(bits.jsData(sit, fen))};data.positions=$positionsJson; + s"""const data=${safeJsonValue(bits.jsData(fen))};data.positions=$positionsJson; data.endgamePositions=$endgamePositionsJson;LichessEditor(document.getElementById('board-editor'), data);""" ) ), diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index 50a289d2f6..5f6d3fe036 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -206,7 +206,7 @@ interface LichessEditor { declare namespace Editor { export interface Config { baseUrl: string; - fen: string; + fen?: string; options?: Editor.Options; is3d: boolean; animation: { diff --git a/ui/analyse/src/actionMenu.ts b/ui/analyse/src/actionMenu.ts index 87cd55f009..ce81d729ba 100644 --- a/ui/analyse/src/actionMenu.ts +++ b/ui/analyse/src/actionMenu.ts @@ -183,7 +183,9 @@ export function view(ctrl: AnalyseCtrl): VNode { 'a.button.button-empty', { attrs: { - href: d.userAnalysis ? '/editor?fen=' + ctrl.node.fen : '/' + d.game.id + '/edit?fen=' + ctrl.node.fen, + href: d.userAnalysis + ? '/editor?' + new URLSearchParams({ fen: ctrl.node.fen, variant: d.game.variant.key }) + : '/' + d.game.id + '/edit?fen=' + ctrl.node.fen, 'data-icon': '', ...(ctrl.embed ? { diff --git a/ui/analyse/src/study/chapterNewForm.ts b/ui/analyse/src/study/chapterNewForm.ts index 1790695574..bb09063396 100644 --- a/ui/analyse/src/study/chapterNewForm.ts +++ b/ui/analyse/src/study/chapterNewForm.ts @@ -213,19 +213,19 @@ export function view(ctrl: StudyChapterNewFormCtrl): VNode { { 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()); - }); + Promise.all([lichess.loadModule('editor'), xhr.json('/editor.json')]).then( + ([_, data]: [unknown, Editor.Config]) => { + data.fen = ctrl.root.node.fen; + 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; diff --git a/ui/editor/src/chessground.ts b/ui/editor/src/chessground.ts index bb686efdcc..f61c7bef4e 100644 --- a/ui/editor/src/chessground.ts +++ b/ui/editor/src/chessground.ts @@ -119,7 +119,7 @@ function deletePiece(ctrl: EditorCtrl, key: Key): void { function makeConfig(ctrl: EditorCtrl): CgConfig { return { - fen: ctrl.cfg.fen, + fen: ctrl.initialFen, orientation: ctrl.options.orientation || 'white', coordinates: !ctrl.cfg.embed, autoCastle: false, diff --git a/ui/editor/src/chessops.ts b/ui/editor/src/chessops.ts new file mode 100644 index 0000000000..7ae71cd242 --- /dev/null +++ b/ui/editor/src/chessops.ts @@ -0,0 +1,37 @@ +import { Rules } from 'chessops/types'; + +export function variantToRules(variant?: string | null): Rules { + switch (variant) { + case 'threeCheck': + return '3check'; + case 'kingOfTheHill': + return 'kingofthehill'; + case 'racingKings': + return 'racingkings'; + case 'antichess': + case 'atomic': + case 'horde': + case 'crazyhouse': + return variant; + default: + return 'chess'; + } +} + +export function rulesToVariant(rules: Rules): VariantKey { + switch (rules) { + case 'chess': + return 'standard'; + case '3check': + return 'threeCheck'; + case 'kingofthehill': + return 'kingOfTheHill'; + case 'racingkings': + return 'racingKings'; + case 'antichess': + case 'atomic': + case 'horde': + case 'crazyhouse': + return rules; + } +} diff --git a/ui/editor/src/ctrl.ts b/ui/editor/src/ctrl.ts index 9bdc036d1a..9a16823115 100644 --- a/ui/editor/src/ctrl.ts +++ b/ui/editor/src/ctrl.ts @@ -7,6 +7,7 @@ import { Setup, Material, RemainingChecks } from 'chessops/setup'; import { Castles, setupPosition } from 'chessops/variant'; import { makeFen, parseFen, parseCastlingFen, INITIAL_FEN, EMPTY_FEN, INITIAL_EPD } from 'chessops/fen'; import { defined, prop, Prop } from 'common'; +import { rulesToVariant, variantToRules } from './chessops'; export default class EditorCtrl { cfg: Editor.Config; @@ -18,6 +19,7 @@ export default class EditorCtrl { selected: Prop; + initialFen: string; pockets: Material | undefined; turn: Color; unmovedRooks: SquareSet | undefined; @@ -62,20 +64,23 @@ export default class EditorCtrl { }); this.castlingToggles = { K: false, Q: false, k: false, q: false }; - this.rules = - !this.cfg.embed && window.history.state && window.history.state.rules ? window.history.state.rules : 'chess'; + const params = new URLSearchParams(location.search); + this.rules = this.cfg.embed ? 'chess' : variantToRules(params.get('variant')); + this.initialFen = (cfg.fen || params.get('fen') || INITIAL_FEN).replace(/_/g, ' '); this.redraw = () => {}; - this.setFen(cfg.fen); + this.setFen(this.initialFen); this.redraw = redraw; } onChange(): void { const fen = this.getFen(); if (!this.cfg.embed) { - const state = { rules: this.rules }; - if (fen == INITIAL_FEN) window.history.replaceState(state, '', '/editor'); - else window.history.replaceState(state, '', this.makeUrl('/editor/', fen)); + const params = new URLSearchParams(); + if (fen !== INITIAL_FEN || this.rules !== 'chess') params.set('fen', fen); + if (this.rules !== 'chess') params.set('variant', rulesToVariant(this.rules)); + const paramsString = params.toString(); + window.history.replaceState(null, '', '/editor' + (paramsString ? '?' + paramsString : '')); } this.options.onChange?.(fen); this.redraw(); @@ -90,7 +95,7 @@ export default class EditorCtrl { } private getSetup(): Setup { - const boardFen = this.chessground ? this.chessground.getFen() : this.cfg.fen; + const boardFen = this.chessground?.getFen() || this.initialFen; const board = parseFen(boardFen).unwrap( setup => setup.board, _ => Board.empty() @@ -136,21 +141,8 @@ export default class EditorCtrl { } makeAnalysisUrl(legalFen: string): string { - switch (this.rules) { - case 'chess': - return this.makeUrl('/analysis/', legalFen); - case '3check': - return this.makeUrl('/analysis/threeCheck/', legalFen); - case 'kingofthehill': - return this.makeUrl('/analysis/kingOfTheHill/', legalFen); - case 'racingkings': - return this.makeUrl('/analysis/racingKings/', legalFen); - case 'antichess': - case 'atomic': - case 'horde': - case 'crazyhouse': - return this.makeUrl(`/analysis/${this.rules}/`, legalFen); - } + const variant = this.rules === 'chess' ? '' : rulesToVariant(this.rules) + '/'; + return this.makeUrl(`/analysis/${variant}`, legalFen); } makeUrl(baseUrl: string, fen: string): string {