Fix problems table height
[vchess.git] / client / src / views / Problems.vue
1 <template lang="pug">
2 main
3 input#modalNewprob.modal(
4 type="checkbox"
5 @change="infoMsg=''"
6 )
7 div#newprobDiv(
8 role="dialog"
9 data-checkbox="modalNewprob"
10 )
11 .card
12 label#closeNewprob.modal-close(for="modalNewprob")
13 fieldset
14 label(for="selectVariant") {{ st.tr["Variant"] }}
15 select#selectVariant(
16 v-model="curproblem.vid"
17 @change="changeVariant(curproblem)"
18 )
19 option(
20 v-for="v in [emptyVar].concat(st.variants)"
21 :value="v.id"
22 :selected="curproblem.vid==v.id"
23 )
24 | {{ v.name }}
25 fieldset
26 input#inputFen(
27 type="text"
28 placeholder="FEN"
29 v-model="curproblem.fen"
30 @input="trySetDiagram(curproblem)"
31 )
32 #diagram(v-html="curproblem.diag")
33 fieldset
34 textarea(
35 :placeholder="st.tr['Instructions']"
36 v-model="curproblem.instruction"
37 )
38 p(v-html="parseHtml(curproblem.instruction)")
39 fieldset
40 textarea(
41 :placeholder="st.tr['Solution']"
42 v-model="curproblem.solution"
43 )
44 p(v-html="parseHtml(curproblem.solution)")
45 button(@click="sendProblem()") {{ st.tr["Send"] }}
46 #dialog.text-center {{ st.tr[infoMsg] }}
47 .row(v-if="showOne")
48 .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
49 #topPage
50 span.vname {{ curproblem.vname }}
51 span.uname ({{ curproblem.uname }})
52 button.marginleft(@click="backToList()") {{ st.tr["Back to list"] }}
53 button.nomargin(
54 v-if="st.user.id == curproblem.uid"
55 @click="editProblem(curproblem)"
56 )
57 | {{ st.tr["Edit"] }}
58 button.nomargin(
59 v-if="st.user.id == curproblem.uid"
60 @click="deleteProblem(curproblem)"
61 )
62 | {{ st.tr["Delete"] }}
63 p.oneInstructions.clickable(
64 v-html="parseHtml(curproblem.instruction)"
65 @click="curproblem.showSolution=!curproblem.showSolution"
66 )
67 | {{ st.tr["Show solution"] }}
68 p(
69 v-show="curproblem.showSolution"
70 v-html="parseHtml(curproblem.solution)"
71 )
72 .row(v-else)
73 .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
74 #controls
75 button#newProblem(@click="prepareNewProblem()")
76 | {{ st.tr["New problem"] }}
77 label(for="checkboxMine") {{ st.tr["My problems"] }}
78 input#checkboxMine(
79 type="checkbox"
80 v-model="onlyMines"
81 )
82 label(for="selectVariant") {{ st.tr["Variant"] }}
83 select#selectVariant(v-model="selectedVar")
84 option(
85 v-for="v in [emptyVar].concat(st.variants)"
86 :value="v.id"
87 )
88 | {{ v.name }}
89 table#tProblems
90 tr
91 th {{ st.tr["Variant"] }}
92 th {{ st.tr["Instructions"] }}
93 th {{ st.tr["Number"] }}
94 tr(
95 v-for="p in problems"
96 v-show="displayProblem(p)"
97 @click="setHrefPid(p)"
98 )
99 td {{ p.vname }}
100 td {{ firstChars(p.instruction) }}
101 td {{ p.id }}
102 BaseGame(
103 v-if="showOne"
104 :game="game"
105 :vr="vr"
106 )
107 </template>
108
109 <script>
110 import { store } from "@/store";
111 import { ajax } from "@/utils/ajax";
112 import { checkProblem } from "@/data/problemCheck";
113 import { getDiagram } from "@/utils/printDiagram";
114 import { processModalClick } from "@/utils/modalClick";
115 import { ArrayFun } from "@/utils/array";
116 import BaseGame from "@/components/BaseGame.vue";
117 export default {
118 name: "my-problems",
119 components: {
120 BaseGame
121 },
122 data: function() {
123 return {
124 st: store.state,
125 emptyVar: {
126 vid: 0,
127 vname: ""
128 },
129 // Problem currently showed, or edited:
130 curproblem: {
131 id: 0, //used in case of edit
132 vid: 0,
133 fen: "",
134 diag: "",
135 instruction: "",
136 solution: "",
137 showSolution: false
138 },
139 loadedVar: 0, //corresponding to loaded V
140 selectedVar: 0, //to filter problems based on variant
141 problems: [],
142 onlyMines: false,
143 showOne: false,
144 infoMsg: "",
145 vr: null, //"variant rules" object initialized from FEN
146 game: {
147 players: [{ name: "Problem" }, { name: "Problem" }],
148 mode: "analyze"
149 }
150 };
151 },
152 created: function() {
153 ajax("/problems", "GET", res => {
154 // Show newest problem first:
155 this.problems = res.problems.sort((p1, p2) => p2.added - p1.added);
156 if (this.st.variants.length > 0)
157 this.problems.forEach(p => this.setVname(p));
158 // Retrieve all problems' authors' names
159 let names = {};
160 this.problems.forEach(p => {
161 if (p.uid != this.st.user.id) names[p.uid] = "";
162 else p.uname = this.st.user.name;
163 });
164 const showOneIfPid = () => {
165 const pid = this.$route.query["id"];
166 if (pid) this.showProblem(this.problems.find(p => p.id == pid));
167 };
168 if (Object.keys(names).length > 0) {
169 ajax("/users", "GET", { ids: Object.keys(names).join(",") }, res2 => {
170 res2.users.forEach(u => {
171 names[u.id] = u.name;
172 });
173 this.problems.forEach(p => {
174 if (!p.uname)
175 p.uname = names[p.uid];
176 });
177 showOneIfPid();
178 });
179 } else showOneIfPid();
180 });
181 },
182 mounted: function() {
183 document
184 .getElementById("newprobDiv")
185 .addEventListener("click", processModalClick);
186 },
187 watch: {
188 // st.variants changes only once, at loading from [] to [...]
189 "st.variants": function() {
190 // Set problems vname (either all are set or none)
191 if (this.problems.length > 0 && this.problems[0].vname == "")
192 this.problems.forEach(p => this.setVname(p));
193 },
194 $route: function(to) {
195 const pid = to.query["id"];
196 if (pid) this.showProblem(this.problems.find(p => p.id == pid));
197 else this.showOne = false;
198 }
199 },
200 methods: {
201 setVname: function(prob) {
202 prob.vname = this.st.variants.find(v => v.id == prob.vid).name;
203 },
204 firstChars: function(text) {
205 let preparedText = text
206 // Replace line jumps and <br> by spaces
207 .replace(/\n/g, " ")
208 .replace(/<br\/?>/g, " ")
209 .replace(/<[^>]+>/g, "") //remove remaining HTML tags
210 .replace(/[ ]+/g, " ") //remove series of spaces by only one
211 .trim();
212 const maxLength = 32; //arbitrary...
213 if (preparedText.length > maxLength)
214 return preparedText.substr(0, 32) + "...";
215 return preparedText;
216 },
217 copyProblem: function(p1, p2) {
218 for (let key in p1) p2[key] = p1[key];
219 },
220 setHrefPid: function(p) {
221 // Change href => $route changes, watcher notices, call showProblem
222 const curHref = document.location.href;
223 document.location.href = curHref.split("?")[0] + "?id=" + p.id;
224 },
225 backToList: function() {
226 // Change href => $route change, watcher notices, reset showOne to false
227 document.location.href = document.location.href.split("?")[0];
228 },
229 resetCurProb: function() {
230 this.curproblem.id = 0;
231 this.curproblem.uid = 0;
232 this.curproblem.vid = "";
233 this.curproblem.vname = "";
234 this.curproblem.fen = "";
235 this.curproblem.diag = "";
236 this.curproblem.instruction = "";
237 this.curproblem.solution = "";
238 this.curproblem.showSolution = false;
239 },
240 parseHtml: function(txt) {
241 return !txt.match(/<[/a-zA-Z]+>/)
242 ? txt.replace(/\n/g, "<br/>") //no HTML tag
243 : txt;
244 },
245 changeVariant: function(prob) {
246 this.setVname(prob);
247 this.loadVariant(prob.vid, () => {
248 // Set FEN if possible (might not be correct yet)
249 if (V.IsGoodFen(prob.fen)) this.setDiagram(prob);
250 });
251 },
252 loadVariant: async function(vid, cb) {
253 // Condition: vid is a valid variant ID
254 this.loadedVar = 0;
255 const variant = this.st.variants.find(v => v.id == vid);
256 const vModule = await import("@/variants/" + variant.name + ".js");
257 window.V = vModule.VariantRules;
258 this.loadedVar = vid;
259 cb();
260 },
261 trySetDiagram: function(prob) {
262 // Problem edit: FEN could be wrong or incomplete,
263 // variant could not be ready, or not defined
264 if (prob.vid > 0 && this.loadedVar == prob.vid && V.IsGoodFen(prob.fen))
265 this.setDiagram(prob);
266 },
267 setDiagram: function(prob) {
268 // Condition: prob.fen is correct and global V is ready
269 const parsedFen = V.ParseFen(prob.fen);
270 const args = {
271 position: parsedFen.position,
272 orientation: parsedFen.turn
273 };
274 prob.diag = getDiagram(args);
275 },
276 displayProblem: function(p) {
277 return (
278 (!this.selectedVar || p.vid == this.selectedVar) &&
279 ((this.onlyMines && p.uid == this.st.user.id) ||
280 (!this.onlyMines && p.uid != this.st.user.id))
281 );
282 },
283 showProblem: function(p) {
284 this.loadVariant(p.vid, () => {
285 // The FEN is already checked at this stage:
286 this.vr = new V(p.fen);
287 this.game.vname = p.vname;
288 this.game.mycolor = this.vr.turn; //diagram orientation
289 this.game.fen = p.fen;
290 this.$set(this.game, "fenStart", p.fen);
291 this.copyProblem(p, this.curproblem);
292 this.showOne = true;
293 });
294 },
295 prepareNewProblem: function() {
296 this.resetCurProb();
297 window.doClick("modalNewprob");
298 },
299 sendProblem: function() {
300 const error = checkProblem(this.curproblem);
301 if (error) {
302 alert(this.st.tr[error]);
303 return;
304 }
305 const edit = this.curproblem.id > 0;
306 this.infoMsg = "Processing... Please wait";
307 ajax(
308 "/problems",
309 edit ? "PUT" : "POST",
310 { prob: this.curproblem },
311 ret => {
312 if (edit) {
313 let editedP = this.problems.find(p => p.id == this.curproblem.id);
314 this.copyProblem(this.curproblem, editedP);
315 this.showProblem(editedP);
316 }
317 else {
318 let newProblem = Object.assign({}, this.curproblem);
319 newProblem.id = ret.id;
320 newProblem.uid = this.st.user.id;
321 newProblem.uname = this.st.user.name;
322 this.problems = [newProblem].concat(this.problems);
323 }
324 document.getElementById("modalNewprob").checked = false;
325 this.infoMsg = "";
326 }
327 );
328 },
329 editProblem: function(prob) {
330 // prob.diag might correspond to some other problem or be empty:
331 this.setDiagram(prob); //V is loaded at this stage
332 this.copyProblem(prob, this.curproblem);
333 window.doClick("modalNewprob");
334 },
335 deleteProblem: function(prob) {
336 if (confirm(this.st.tr["Are you sure?"])) {
337 ajax("/problems", "DELETE", { id: prob.id }, () => {
338 ArrayFun.remove(this.problems, p => p.id == prob.id);
339 this.backToList();
340 });
341 }
342 }
343 }
344 };
345 </script>
346
347 <style lang="sass" scoped>
348 [type="checkbox"].modal+div .card
349 max-width: 767px
350 max-height: 100%
351
352 #inputFen
353 width: 100%
354
355 textarea
356 width: 100%
357
358 #diagram
359 margin: 0 auto
360 max-width: 400px
361
362 table#tProblems
363 max-height: 100%
364
365 #controls
366 margin: 0
367 width: 100%
368 text-align: center
369 & > *
370 margin: 0
371
372 p.oneInstructions
373 margin: 0
374 padding: 2px 5px
375 background-color: lightgreen
376
377 #topPage
378 span.vname
379 font-weight: bold
380 padding-left: var(--universal-margin)
381 span.uname
382 padding-left: var(--universal-margin)
383 margin: 0 auto
384 & > .nomargin
385 margin: 0
386 & > .marginleft
387 margin: 0 0 0 15px
388
389 @media screen and (max-width: 767px)
390 #topPage
391 text-align: center
392 </style>