From 430a203855578f9bbf4c851165c6066a741ff1f8 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 30 Jan 2020 11:35:36 +0100
Subject: [PATCH] Smooth scrolling in moves, template to render moveList

---
 client/src/App.vue                     |   8 ++
 client/src/components/BaseGame.vue     |  45 +++--------
 client/src/components/ComputerGame.vue |   3 +-
 client/src/components/MoveList.vue     | 106 +++++++++++++++++++------
 client/src/views/Game.vue              |  63 +++++++++------
 5 files changed, 141 insertions(+), 84 deletions(-)

diff --git a/client/src/App.vue b/client/src/App.vue
index 4fc3b48b..77fed7af 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -140,6 +140,10 @@ nav
           width: 36px
           height: 27px
 
+@media screen and (max-width: 767px)
+  nav
+    border: none
+
 [type="checkbox"].drawer+*
   right: -767px
 
@@ -168,4 +172,8 @@ footer
   & > p
     display: inline-block
     margin: 0 0 0 10px
+
+@media screen and (max-width: 767px)
+  footer
+    border: none
 </style>
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 5c82b0b1..e04f5082 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -22,7 +22,7 @@ div#baseGame(tabindex=-1 @click="() => focusBg()" @keydown="handleKeys")
         a#download(href="#")
         button(@click="download") {{ st.tr["Download PGN"] }}
     .col-sm-12.col-md-3
-      MoveList(v-if="showMoves"
+      MoveList(v-if="showMoves" :score="game.score" :message="game.scoreMsg"
         :moves="moves" :cursor="cursor" @goto-move="gotoMove")
 </template>
 
@@ -51,7 +51,6 @@ export default {
       moves: [],
       cursor: -1, //index of the move just played
       lastMove: null,
-      gameHasEnded: false, //to avoid showing end message twice
     };
   },
   watch: {
@@ -63,13 +62,6 @@ export default {
     "game.moveToPlay": function() {
       this.play(this.game.moveToPlay, "receive", this.game.vname=="Dark");
     },
-    "game.score": function(score) {
-      if (!this.gameHasEnded && score != "*")
-      {
-        // "false" says "don't bubble up": the parent already knows
-        this.endGame(score, this.game.scoreMsg, false);
-      }
-    },
   },
   computed: {
     showMoves: function() {
@@ -77,10 +69,10 @@ export default {
       //return window.innerWidth >= 768;
     },
     showFen: function() {
-      return this.game.vname != "Dark" || this.score != "*";
+      return this.game.vname != "Dark" || this.game.score != "*";
     },
     analyze: function() {
-      return this.game.mode == "analyze" || this.score != "*";
+      return this.game.mode == "analyze" || this.game.score != "*";
     },
   },
   created: function() {
@@ -117,8 +109,6 @@ export default {
     re_setVariables: function() {
       this.endgameMessage = "";
       this.orientation = this.game.mycolor || "w"; //default orientation for observed games
-      this.score = this.game.score || "*"; //mutable (if initially "*")
-      this.gameHasEnded = (this.score != "*");
       this.moves = JSON.parse(JSON.stringify(this.game.moves || []));
       // Post-processing: decorate each move with color + current FEN:
       // (to be able to jump to any position quickly)
@@ -142,8 +132,9 @@ export default {
       this.lastMove = (L > 0 ? this.moves[L-1]  : null);
     },
     gotoFenContent: function(event) {
-      this.$router.push("/analyze/" + this.game.vname +
-        "/?fen=" + event.target.innerText.replace(/ /g, "_"));
+      const newUrl = "#/analyze/" + this.game.vname +
+        "/?fen=" + event.target.innerText.replace(/ /g, "_");
+      window.open(newUrl); //to open in a new tab
     },
     download: function() {
       const content = this.getPgn();
@@ -161,7 +152,7 @@ export default {
       pgn += '[White "' + this.game.players[0].name + '"]\n';
       pgn += '[Black "' + this.game.players[1].name + '"]\n';
       pgn += '[Fen "' + this.game.fenStart + '"]\n';
-      pgn += '[Result "' + this.score + '"]\n\n';
+      pgn += '[Result "' + this.game.score + '"]\n\n';
       let counter = 1;
       let i = 0;
       while (i < this.moves.length)
@@ -203,15 +194,6 @@ export default {
       modalBox.checked = true;
       setTimeout(() => { modalBox.checked = false; }, 2000);
     },
-    endGame: function(score, message, bubbleUp) {
-      this.gameHasEnded = true;
-      this.score = score;
-      if (!message)
-        message = this.getScoreMessage(score);
-      this.showEndgameMsg(score + " . " + message);
-      if (bubbleUp)
-        this.$emit("gameover", score);
-    },
     animateMove: function(move) {
       let startSquare = document.getElementById(getSquareId(move.start));
       let endSquare = document.getElementById(getSquareId(move.end));
@@ -275,7 +257,7 @@ export default {
       if (!navigate)
       {
         move.fen = this.vr.getFen();
-        if (this.score == "*" || this.analyze)
+        if (this.game.score == "*" || this.analyze)
         {
           // Stack move on movesList at current cursor
           if (this.cursor == this.moves.length)
@@ -291,14 +273,11 @@ export default {
       const score = this.vr.getCurrentScore();
       if (score != "*")
       {
+        const message = this.getScoreMessage(score);
         if (!this.analyze)
-          this.endGame(score, undefined, true);
-        else
-        {
-          // Just show score on screen (allow undo)
-          const message = this.getScoreMessage(score);
+          this.$emit("gameover", score, message);
+        else //just show score on screen (allow undo)
           this.showEndgameMsg(score + " . " + message);
-        }
       }
     },
     undo: function(move) {
@@ -366,7 +345,7 @@ export default {
     width: 20%
     margin: 0
 #boardContainer
-  margin-top: 5px
+  //margin-top: 5px
   >div
     margin-left: auto
     margin-right: auto
diff --git a/client/src/components/ComputerGame.vue b/client/src/components/ComputerGame.vue
index 941016b7..46b672bb 100644
--- a/client/src/components/ComputerGame.vue
+++ b/client/src/components/ComputerGame.vue
@@ -123,8 +123,9 @@ export default {
         this.playComputerMove();
       }
     },
-    gameOver: function(score) {
+    gameOver: function(score, scoreMsg) {
       this.game.score = score;
+      this.game.scoreMsg = scoreMsg;
       this.game.mode = "analyze";
       this.$emit("game-over", score); //bubble up to Rules.vue
     },
diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue
index a4e1ca41..70756cc3 100644
--- a/client/src/components/MoveList.vue
+++ b/client/src/components/MoveList.vue
@@ -1,9 +1,65 @@
+<template lang="pug">
+div
+  #scoreInfo(v-if="score!='*'")
+    p {{ score }}
+    p {{ message }}
+  table#movesList
+    tbody
+      tr(v-for="moveIdx in evenNumbers")
+        td {{ moveIdx / 2 + 1 }}
+        td(:class="{'highlight-lm': cursor == moveIdx}"
+            data-label="White move" @click="() => gotoMove(moveIdx)")
+          | {{ moves[moveIdx].notation }}
+        td(v-if="moveIdx < moves.length-1"
+            :class="{'highlight-lm': cursor == moveIdx+1}"
+            data-label="Black move" @click="() => gotoMove(moveIdx+1)")
+          | {{ moves[moveIdx+1].notation }}
+        // Else: just add an empty cell
+        td(v-else)
+</template>
+
 <script>
 // Component for moves list on the right
 export default {
   name: 'my-move-list',
-	props: ["moves","cursor"],
-	render(h) {
+	props: ["moves","cursor","score","message"],
+  watch: {
+    cursor: function(newValue) {
+      // $nextTick to wait for table > tr to be rendered
+      this.$nextTick( () => {
+        let rows = document.querySelectorAll('#movesList tr');
+        if (rows.length > 0)
+        {
+          rows[Math.floor(newValue/2)].scrollIntoView({
+            behavior: 'smooth',
+            block: 'center'
+          });
+        }
+      });
+    },
+  },
+  computed: {
+    evenNumbers: function() {
+      return [...Array(this.moves.length).keys()].filter(i => i%2==0);
+    },
+  },
+  methods: {
+		gotoMove: function(index) {
+			this.$emit("goto-move", index);
+		},
+	},
+};
+</script>
+
+<style lang="sass" scoped>
+.moves-list
+  min-width: 250px
+td.highlight-lm
+  background-color: plum
+</style>
+
+<!-- Old render method:
+  render(h) {
 		if (this.moves.length == 0)
 			return;
 		let tableContent = [];
@@ -65,32 +121,34 @@ export default {
 		}
 		tableRow.children = moveCells;
 		tableContent.push(tableRow);
+    const scoreDiv = h("div",
+      {
+        id: "scoreInfo",
+        style: {
+          display: this.score!="*" ? "block" : "none",
+        },
+      },
+      [
+        h("p", this.score),
+        h("p", this.message),
+      ]
+    );
 		const movesTable = h(
       "div",
       { },
-      [h(
-			  "table",
-			  {
-          "class": {
-            "moves-list": true,
+      [
+        scoreDiv,
+        h(
+          "table",
+          {
+            "class": {
+              "moves-list": true,
+            },
           },
-        },
-			  tableContent
-		  )]
+          tableContent
+		    )
+      ]
     );
 		return movesTable;
 	},
-	methods: {
-		gotoMove: function(index) {
-			this.$emit("goto-move", index);
-		},
-	},
-};
-</script>
-
-<style lang="sass" scoped>
-.moves-list
-  min-width: 250px
-td.highlight-lm
-  background-color: plum
-</style>
+-->
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 00414d91..1ffcda41 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -1,14 +1,11 @@
 <template lang="pug">
 main
   .row
-    .col-sm-12.col-md-3
+    #chat.col-sm-12.col-md-4.col-md-offset-4
       Chat(:players="game.players")
-    .col-sm-12.col-md-9
-      BaseGame(:game="game" :vr="vr" ref="basegame"
-        @newmove="processMove" @gameover="gameOver")
   .row
-    .col-sm-12.col-md-9.col-md-offset-3
-      .button-group(v-if="game.mode!='analyze' && game.score=='*'")
+    .col-sm-12
+      #actions(v-if="game.mode!='analyze' && game.score=='*'")
         button(@click="offerDraw") Draw
         button(@click="abortGame") Abort
         button(@click="resign") Resign
@@ -16,6 +13,8 @@ main
       div(v-if="game.score=='*'") Time: {{ virtualClocks[0] }} - {{ virtualClocks[1] }}
       div(v-if="game.type=='corr'") {{ game.corrMsg }}
       textarea(v-if="game.score=='*'" v-model="corrMsg")
+  BaseGame(:game="game" :vr="vr" ref="basegame"
+    @newmove="processMove" @gameover="gameOver")
 </template>
 
 <script>
@@ -79,7 +78,7 @@ export default {
         {
           clearInterval(clockUpdate);
           if (countdown < 0)
-            this.setScore(this.vr.turn=="w" ? "0-1" : "1-0", "Time");
+            this.gameOver(this.vr.turn=="w" ? "0-1" : "1-0", "Time");
         }
         else
         {
@@ -211,13 +210,13 @@ export default {
           break;
         }
         case "resign":
-          this.setScore(data.side=="b" ? "1-0" : "0-1", "Resign");
+          this.gameOver(data.side=="b" ? "1-0" : "0-1", "Resign");
           break;
         case "abort":
-          this.setScore("?", "Abort");
+          this.gameOver("?", "Abort");
           break;
         case "draw":
-          this.setScore("1/2", "Mutual agreement");
+          this.gameOver("1/2", "Mutual agreement");
           break;
         case "drawoffer":
           this.drawOffer = "received"; //TODO: observers don't know who offered draw
@@ -253,17 +252,13 @@ export default {
         {
           // Opponent resigned or aborted game, or accepted draw offer
           // (this is not a stalemate or checkmate)
-          this.setScore(data.score, "Opponent action");
+          this.gameOver(data.score, "Opponent action");
         }
         this.game.clocks = data.clocks; //TODO: check this?
         if (!!data.lastMove.draw)
           this.drawOffer = "received";
       }
     },
-    setScore: function(score, message) {
-      this.game.scoreMsg = message;
-      this.$set(this.game, "score", score); //TODO: Vue3...
-    },
     offerDraw: function() {
       if (this.drawOffer == "received")
       {
@@ -273,7 +268,7 @@ export default {
           if (p.sid != this.st.user.sid)
             this.st.conn.send(JSON.stringify({code:"draw", target:p.sid}));
         });
-        this.setScore("1/2", "Mutual agreement");
+        this.gameOver("1/2", "Mutual agreement");
       }
       else if (this.drawOffer == "sent")
       {
@@ -297,7 +292,7 @@ export default {
     abortGame: function() {
       if (!confirm(this.st.tr["Terminate game?"]))
         return;
-      this.setScore("?", "Abort");
+      this.gameOver("?", "Abort");
       this.people.forEach(p => {
         if (p.sid != this.st.user.sid)
         {
@@ -318,7 +313,7 @@ export default {
             side:this.game.mycolor, target:p.sid}));
         }
       });
-      this.setScore(this.game.mycolor=="w" ? "0-1" : "1-0", "Resign");
+      this.gameOver(this.game.mycolor=="w" ? "0-1" : "1-0", "Resign");
     },
     // 3 cases for loading a game:
     //  - from indexedDB (running or completed live game I play)
@@ -518,10 +513,10 @@ export default {
       if (this.repeat[repIdx] >= 3)
         this.drawOffer = "received"; //TODO: will print "mutual agreement"...
     },
-    gameOver: function(score) {
+    gameOver: function(score, scoreMsg) {
       this.game.mode = "analyze";
-      this.game.score = score; //until Vue3, this property change isn't seen
-                               //by child (and doesn't need to be)
+      this.game.score = score;
+      this.game.scoreMsg = scoreMsg;
       const myIdx = this.game.players.findIndex(p => {
         return p.sid == this.st.user.sid || p.uid == this.st.user.id;
       });
@@ -535,13 +530,29 @@ export default {
 <style lang="sass">
 .connected
   background-color: green
-
 .disconnected
   background-color: red
 
-.white-turn
-  background-color: white
+@media screen and (min-width: 768px)
+  #actions
+    width: 300px
+@media screen and (max-width: 767px)
+  .game
+    width: 100%
 
-.black-turn
-  background-color: black
+#actions
+  margin-top: 10px
+  margin-left: auto
+  margin-right: auto
+  button
+    display: inline-block
+    width: 33%
+    margin: 0
+#chat
+  margin-top: 5px
+  margin-bottom: 5px
+  >.card
+    max-width: 100%
+    margin: 0;
+  border: none;
 </style>
-- 
2.44.0