Merge branch 'master' into l10n_master

pull/3521/head
Thibault Duplessis 2017-08-24 20:58:39 -05:00 committed by GitHub
commit c480513aaa
46 changed files with 352 additions and 245 deletions

View File

@ -67,7 +67,6 @@ public/piece-src/spatial | [Maurizio Monge](http://poisson.phc.unipi.it/~monge/c
public/piece-src/reillycraig | [Reilly Craig](https://instagram.com/fader_) |
public/piece/other | [Twitch](http://lazythunk.com/kappa/) |
public/images/staunton/staunton/Staunton | [Eden Murs](https://userstyles.org/styles/134558/lichess-pieces-3d-staunton) (?) |
public/vendor/ChessPursuit | [Saturnyn](https://github.com/Saturnyn) | [CC BY-NC-SA](https://github.com/Saturnyn/ChessPursuit/issues/1#issuecomment-141067515)
* The other sounds in public/sound.
* The other artwork in public/images.

View File

@ -94,11 +94,23 @@ object Auth extends LilaController {
implicit val req = ctx.req
req.session get "sessionId" foreach lila.security.Store.delete
negotiate(
html = fuccess(Redirect(routes.Main.mobile)),
api = apiVersion => Ok(Json.obj("ok" -> true)).fuccess
html = Redirect(routes.Main.mobile).fuccess,
api = _ => Ok(Json.obj("ok" -> true)).fuccess
) map (_ withCookies LilaCookie.newSession)
}
// mobile app BC logout with GET
def logoutGet = Open { implicit ctx =>
implicit val req = ctx.req
negotiate(
html = notFound,
api = _ => {
req.session get "sessionId" foreach lila.security.Store.delete
Ok(Json.obj("ok" -> true)).withCookies(LilaCookie.newSession).fuccess
}
)
}
def signup = Open { implicit ctx =>
NoTor {
Ok(html.auth.signup(forms.signup.website, env.RecaptchaPublicKey)).fuccess

View File

@ -142,7 +142,7 @@ object Main extends LilaController {
Ok(html.base.fpmenu()).fuccess
}
def fishnet = Open { implicit ctx =>
Ok(html.site.fishnet()).fuccess
def getFishnet = Open { implicit ctx =>
Ok(html.site.getFishnet()).fuccess
}
}

View File

@ -177,8 +177,8 @@ object Tournament extends LilaController {
}
def terminate(id: String) = Secure(_.TerminateTournament) { implicit ctx => me =>
OptionResult(repo startedById id) { tour =>
env.api finish tour
OptionResult(repo byId id) { tour =>
env.api kill tour
Env.mod.logApi.terminateTournament(me.id, tour.fullName)
Redirect(routes.Tournament show tour.id)
}

View File

@ -6,7 +6,7 @@ import chess.{ Status => S, Color, Clock, Mode }
import controllers.routes
import play.twirl.api.Html
import lila.common.String.html.{ escape => escapeHtml }
import lila.common.String.html.escapeHtmlUnsafe
import lila.game.{ Game, Player, Namer, Pov }
import lila.user.{ User, UserContext }
import lila.i18n.{ I18nKeys, enLang }
@ -126,7 +126,7 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
val klass = cssClass.??(" " + _)
val content = (player.aiLevel, player.name) match {
case (Some(level), _) => aiNameHtml(level, withRating).body
case (_, Some(name)) => escapeHtml(name)
case (_, Some(name)) => escapeHtmlUnsafe(name)
case _ => User.anonymous
}
s"""<span class="user_link$klass">$content$statusIcon</span>"""

View File

@ -14,91 +14,12 @@ trait StringHelper { self: NumberHelper =>
val slugify = lila.common.String.slugify _
val escapeHtml = lila.common.String.html.escape _
private val escapeHtmlUnsafe = lila.common.String.html.escapeUnsafe _
def shorten(text: String, length: Int, sep: String = "…"): Html = {
val t = text.replace("\n", " ")
if (t.size > (length + sep.size)) Html(escapeHtmlUnsafe(t take length) ++ sep)
else escapeHtml(t)
}
def shortenWithBr(text: String, length: Int) = Html {
nl2brUnsafe(escapeHtmlUnsafe(text).take(length)).replace("<br /><br />", "<br />")
}
def pluralize(s: String, n: Int) = s"$n $s${if (n > 1) "s" else ""}"
def autoLink(text: String): Html = nl2br(addUserProfileLinksUnsafe(addLinksUnsafe(escapeHtmlUnsafe(text))))
private def nl2brUnsafe(text: String): String =
text.replace("\r\n", "<br />").replace("\n", "<br />")
def nl2br(text: String) = Html(nl2brUnsafe(text))
private val markdownLinkRegex = """\[([^\[]+)\]\(([^\)]+)\)""".r
def markdownLinks(text: String): Html = nl2br {
markdownLinkRegex.replaceAllIn(escapeHtmlUnsafe(text), m => {
s"""<a href="${m group 2}">${m group 1}</a>"""
})
}
def repositionTooltip(link: Html, position: String) = Html {
link.body.replace("<a ", s"""<a data-pt-pos="$position" """)
}
private val urlRegex = """(?i)\b((https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,6}\/)((?:[`!\[\]{};:'".,<>?«»“”‘’]*[^\s`!\[\]{}\(\);:'".,<>?«»“”‘’])*))""".r
/**
* Creates hyperlinks to user profiles mentioned using the '@' prefix. e.g. @ornicar
* @param text The text to regex match
* @return The text as a HTML hyperlink
*/
def addUserProfileLinks(text: String) = Html(addUserProfileLinksUnsafe(text))
private def addUserProfileLinksUnsafe(text: String): String =
User.atUsernameRegex.replaceAllIn(text, m => {
val user = m group 1
val url = s"//$netDomain/@/$user"
s"""<a href="$url">@$user</a>"""
})
def addLinks(text: String) = Html(addLinksUnsafe(text))
private def addLinksUnsafe(text: String): String = try {
urlRegex.replaceAllIn(text, m => {
if (m.group(0) contains "&quot") m.group(0)
else if (m.group(2) == "http://" || m.group(2) == "https://") {
if (s"${m.group(3)}/" startsWith s"$netDomain/") {
// internal
val link = m.group(3)
s"""<a rel="nofollow" href="//$link">$link</a>"""
} else {
// external
val link = m.group(1)
s"""<a rel="nofollow" href="$link" target="_blank">$link</a>"""
}
} else {
if (s"${m.group(2)}/" startsWith s"$netDomain/") {
// internal
val link = m.group(1)
s"""<a rel="nofollow" href="//$link">$link</a>"""
} else {
// external
val link = m.group(1)
s"""<a rel="nofollow" href="http://$link" target="_blank">$link</a>"""
}
}
})
} catch {
case e: IllegalArgumentException =>
lila.log("templating").error(s"addLinks($text)", e)
text
}
def showNumber(n: Int): String = if (n > 0) s"+$n" else n.toString
implicit def lilaRichString(str: String) = new {

View File

@ -6,7 +6,7 @@ import play.twirl.api.Html
import lila.api.Context
import lila.team.Env.{ current => teamEnv }
import lila.common.String.html.{ escape => escapeHtml }
import lila.common.String.html.escapeHtml
trait TeamHelper {

View File

@ -1,7 +1,7 @@
package lila.app
package ui
import lila.common.String.html.{ escape => escapeHtml }
import lila.common.String.html.escapeHtmlUnsafe
import play.twirl.api.Html
case class OpenGraph(
@ -19,7 +19,7 @@ case class OpenGraph(
object og {
private def tag(name: String, value: String) =
s"""<meta property="og:$name" content="${escapeHtml(value)}"/>"""
s"""<meta property="og:$name" content="${escapeHtmlUnsafe(value)}"/>"""
private val tupledTag = (tag _).tupled
@ -37,7 +37,7 @@ case class OpenGraph(
object twitter {
private def tag(name: String, value: String) =
s"""<meta name="twitter:$name" content="${escapeHtml(value)}"/>"""
s"""<meta name="twitter:$name" content="${escapeHtmlUnsafe(value)}"/>"""
private val tupledTag = (tag _).tupled

View File

@ -25,7 +25,7 @@ searchText = text
<tr class="paginated_element">
<td>
<a class="post" href="@routes.ForumPost.redirect(view.post.id)">@view.categ.name - @view.topic.name #@view.post.number</a>
<p>@shortenWithBr(view.post.text, 200)</p>
<p>@lila.common.String.html.shortenWithBr(view.post.text, 200)</p>
</td>
<td class="info">
@momentFromNow(view.post.createdAt) by @authorLink(view.post)

View File

@ -21,7 +21,7 @@
</div>
@tour.spotlight.map { s =>
<div class="game_infos spotlight">
@markdownLinks(s.description)
@lila.common.String.html.markdownLinks(s.description)
</div>
}
@if(verdicts.relevant) {
@ -52,11 +52,11 @@
<br /><br />
@trans.winner(): @userIdLink(userId.some)
}
@if(isGranted(_.TerminateTournament) && tour.isStarted) {
@if(isGranted(_.TerminateTournament)) {
<form class="terminate" method="post" action="@routes.Tournament.terminate(tour.id)"
style="margin-top: 7px; text-align: right;">
<button data-icon="j" class="submit text button thin confirm" type="submit"
title="Terminates the tournament immediately! Use only if the tournament is killing lichess!">Terminate now</button>
title="Terminates the tournament immediately">Terminate now</button>
</form>
}
</div>

View File

@ -46,7 +46,8 @@ TwirlKeys.templateImports ++= Seq(
"lila.app.templating.Environment._",
"lila.api.Context",
"lila.i18n.{ I18nKeys => trans }",
"lila.common.paginator.Paginator"
"lila.common.paginator.Paginator",
"lila.common.String.html._"
)
resourceDirectory in Assets := (sourceDirectory in Compile).value / "assets"

View File

@ -329,7 +329,8 @@ POST /translation/select controllers.I18n.select
# Authentication
GET /login controllers.Auth.login
POST /login controllers.Auth.authenticate
GET /logout controllers.Auth.logout
GET /logout controllers.Auth.logoutGet
POST /logout controllers.Auth.logout
GET /signup controllers.Auth.signup
POST /signup controllers.Auth.signupPost
GET /signup/check-your-email/:name controllers.Auth.checkYourEmail(name: String)
@ -502,7 +503,7 @@ GET /embed controllers.Main.embed
GET /mobile controllers.Main.mobile
GET /lag controllers.Main.lag
GET /fpmenu controllers.Main.fpmenu
GET /fishnet controllers.Main.fishnet
GET /get-fishnet controllers.Main.getFishnet
# Mobile Push
POST /mobile/register/:platform/:deviceId controllers.Main.mobileRegister(platform: String, deviceId: String)

@ -1 +1 @@
Subproject commit f9ab22441c8e095c149c1e4e68f08cf64428fd8e
Subproject commit 01843a5e6cadb0404432e79cb815e06de89764c5

View File

@ -41,14 +41,96 @@ object String {
}
}
// Matches a lichess username with an '@' prefix if it is used as a single
// word (i.e. preceded and followed by space or appropriate punctuation):
// Yes: everyone says @ornicar is a pretty cool guy
// No: contact@lichess.org, @1, http://example.com/@happy0
val atUsernameRegex = """(?<=\s|^)@(?>([a-zA-Z_-][\w-]{1,19}))(?![\w-])""".r
object html {
private def nl2brUnsafe(text: String): String =
text.replace("\r\n", "<br />").replace("\n", "<br />")
def nl2br(text: String) = Html(nl2brUnsafe(text))
def shortenWithBr(text: String, length: Int) = Html {
nl2brUnsafe(escapeHtmlUnsafe(text).take(length)).replace("<br /><br />", "<br />")
}
def shorten(text: String, length: Int, sep: String = "…"): Html = {
val t = text.replace("\n", " ")
if (t.size > (length + sep.size)) Html(escapeHtmlUnsafe(t take length) ++ sep)
else escapeHtml(t)
}
def autoLink(text: String): Html = nl2br(addUserProfileLinksUnsafe(addLinksUnsafe(escapeHtmlUnsafe(text))))
private val urlRegex = """(?i)\b((https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,6}\/)((?:[`!\[\]{};:'".,<>?«»“”‘’]*[^\s`!\[\]{}\(\);:'".,<>?«»“”‘’])*))""".r
// private val imgRegex = """(?:(?:https?:\/\/))[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/=]*(\.jpg|\.png|\.jpeg))""".r
private val netDomain = "lichess.org" // whatever...
/**
* Creates hyperlinks to user profiles mentioned using the '@' prefix. e.g. @ornicar
* @param text The text to regex match
* @return The text as a HTML hyperlink
*/
def addUserProfileLinks(text: String) = Html(addUserProfileLinksUnsafe(text))
private def addUserProfileLinksUnsafe(text: String): String =
atUsernameRegex.replaceAllIn(text, m => {
val user = m group 1
val url = s"//$netDomain/@/$user"
s"""<a href="$url">@$user</a>"""
})
def addLinks(text: String) = Html(addLinksUnsafe(text))
private def addLinksUnsafe(text: String): String = try {
urlRegex.replaceAllIn(text, m => {
if (m.group(0) contains "&quot") m.group(0)
else if (m.group(2) == "http://" || m.group(2) == "https://") {
if (s"${m.group(3)}/" startsWith s"$netDomain/") {
// internal
val link = m.group(3)
s"""<a rel="nofollow" href="//$link">${urlOrImgUnsafe(link)}</a>"""
} else {
// external
val link = m.group(1)
s"""<a rel="nofollow" href="$link" target="_blank">${urlOrImgUnsafe(link)}</a>"""
}
} else {
if (s"${m.group(2)}/" startsWith s"$netDomain/") {
// internal
val link = m.group(1)
s"""<a rel="nofollow" href="//$link">${urlOrImgUnsafe(link)}</a>"""
} else {
// external
val link = m.group(1)
s"""<a rel="nofollow" href="http://$link" target="_blank">${urlOrImgUnsafe(link)}</a>"""
}
}
})
} catch {
case e: IllegalArgumentException =>
lila.log("templating").error(s"addLinks($text)", e)
text
}
private val imgUrlPattern = """.*\.(jpg|jpeg|png|gif)$""".r.pattern
private def urlToImgUnsafe(url: String): Option[String] = {
imgUrlPattern.matcher(url).matches && !url.contains(s"://$netDomain")
} option s"""<img class="embed" src="$url"/>"""
private def urlOrImgUnsafe(url: String) = urlToImgUnsafe(url) getOrElse url
// from https://github.com/android/platform_frameworks_base/blob/d59921149bb5948ffbcb9a9e832e9ac1538e05a0/core/java/android/text/TextUtils.java#L1361
def escape(s: String): Html = Html(escapeUnsafe(s))
def escapeHtml(s: String): Html = Html(escapeHtmlUnsafe(s))
private val badChars = "[<>&\"']".r.pattern
def escapeUnsafe(s: String): String = {
def escapeHtmlUnsafe(s: String): String = {
if (badChars.matcher(s).find) {
val sb = new StringBuilder(s.size + 10) // wet finger style
var i = 0
@ -68,5 +150,13 @@ object String {
sb.toString
} else s
}
private val markdownLinkRegex = """\[([^\[]+)\]\(([^\)]+)\)""".r
def markdownLinks(text: String): Html = nl2br {
markdownLinkRegex.replaceAllIn(escapeHtmlUnsafe(text), m => {
s"""<a href="${m group 2}">${m group 1}</a>"""
})
}
}
}

View File

@ -0,0 +1,31 @@
package lila.common
import org.specs2.mutable.Specification
import play.twirl.api.Html
class HtmlTest extends Specification {
import String.html._
"add links" should {
"detect link" in {
val url = "http://zombo.com"
addLinks(s"""link to $url here""") must_== Html {
s"""link to <a rel="nofollow" href="$url" target="_blank">$url</a> here"""
}
}
"detect image" in {
val url = "http://zombo.com/pic.jpg"
addLinks(s"""img to $url here""") must_== Html {
val img = s"""<img src="$url" width="100%" style="max-width:100%" />"""
s"""img to <a rel="nofollow" href="$url" target="_blank">$img</a> here"""
}
}
// "skip markdown images" in {
// val url = "http://zombo.com"
// addLinks(s"""img of ![some alt]($url) here""") must_== Html {
// s"""img of ![some alt]($url) here"""
// }
// }
}
}

View File

@ -54,6 +54,6 @@ final class MentionNotifier(notifyApi: NotifyApi, relationApi: RelationApi) {
}
private def extractMentionedUsers(post: Post): Set[String] = {
User.atUsernameRegex.findAllMatchIn(post.text).map(_.matched.tail).toSet
lila.common.String.atUsernameRegex.findAllMatchIn(post.text).map(_.matched.tail).toSet
}
}

View File

@ -2,7 +2,7 @@ package lila.i18n
import play.twirl.api.Html
import lila.common.String.html.{ escape => escapeHtml }
import lila.common.String.html.escapeHtml
private sealed trait Translation

View File

@ -3,7 +3,7 @@ package lila.i18n
import play.api.i18n.Lang
import play.twirl.api.Html
import lila.common.String.html.{ escape => escapeHtml }
import lila.common.String.html.escapeHtml
object Translator {

View File

@ -4,9 +4,7 @@ import play.api.libs.json._
import actorApi.Member
import chess.Color
import lila.common.ApiVersion
import lila.game.Event
import lila.common.String.html.{ escape => escapeHtml }
case class VersionedEvent(
version: Int,

View File

@ -32,11 +32,13 @@ blumpkin
bollock
boner
boob
bot
bugger
buk?kake
bull?shit
cancer
cawk
cheat(er|)
chess(|-|_)bot(.?com)?
chink
choad
@ -56,6 +58,7 @@ dogg?ystyle
dong
douche(bag|)
dyke
engine
(f|ph)ag
(f|ph)agg?ot
fanny
@ -78,7 +81,7 @@ horny
humping
idiot
incest
jerks?
jerk
jizz?(um|)
kaffir
kike
@ -144,11 +147,10 @@ spunk
stfu
stripper
stupid
suc?k
taint
tart
terrorist
tit(s|ies|ties|ty)(fuc?k)
tit(|t?ies|ty)(fuc?k)
tosser
turd
twat

View File

@ -46,7 +46,7 @@ abstract class SocketActor[M <: SocketMember](uidTtl: Duration) extends Socket w
// generic message handler
def receiveGeneric: Receive = {
case Ping(uid, None, lagCentis) => ping(uid, lagCentis)
case Ping(uid, _, lagCentis) => ping(uid, lagCentis)
case Broom => broom
@ -87,11 +87,11 @@ abstract class SocketActor[M <: SocketMember](uidTtl: Duration) extends Socket w
def ping(uid: String, lagCentis: Option[Centis]) {
setAlive(uid)
withMember(uid) { member =>
member push pong
for {
lc <- lagCentis
user <- member.userId
} UserLagCache.put(user, lc)
member push pong
}
}

View File

@ -167,6 +167,11 @@ final class TournamentApi(
}
}
def kill(tour: Tournament) {
if (tour.isStarted) finish(tour)
else if (tour.isCreated) wipe(tour)
}
private def awardTrophies(tour: Tournament): Funit =
tour.schedule.??(_.freq == Schedule.Freq.Marathon) ?? {
PlayerRepo.bestByTourWithRank(tour.id, 100).flatMap {

View File

@ -134,18 +134,12 @@ object User {
}
implicit def playTimeHandler = reactivemongo.bson.Macros.handler[PlayTime]
// Matches a lichess username with an '@' prefix if it is used as a single
// word (i.e. preceded and followed by space or appropriate punctuation):
// Yes: everyone says @ornicar is a pretty cool guy
// No: contact@lichess.org, @1, http://example.com/@happy0
val atUsernameRegex = """(?<=\s|^)@(?>([a-zA-Z_-][\w-]{1,19}))(?![\w-])""".r
// what existing usernames are like
val historicalUsernameRegex = """(?i)[\w-]*[a-z0-9]""".r
val historicalUsernameRegex = """(?i)[a-z0-9][\w-]*[a-z0-9]""".r
// what new usernames should be like
val newUsernameRegex = """(?i)[a-z][\w-]*[a-z0-9]""".r
def couldBeUsername(str: String) = historicalUsernameRegex.pattern.matcher(str).matches
def couldBeUsername(str: User.ID) = historicalUsernameRegex.pattern.matcher(str).matches
def normalize(username: String) = username.toLowerCase

View File

@ -3,15 +3,26 @@ package lila.user
import org.specs2.mutable.Specification
class UserTest extends Specification {
def canSignup(str: User.ID) =
User.newUsernameRegex.pattern.matcher(str).matches
"username regex" in {
import User.couldBeUsername
"bad prefix" in {
couldBeUsername("000") must beFalse
couldBeUsername("0foo") must beFalse
"bad prefix: can login" in {
couldBeUsername("000") must beTrue
couldBeUsername("0foo") must beTrue
couldBeUsername("_foo") must beFalse
couldBeUsername("-foo") must beFalse
}
"bad prefix: cannot signup" in {
canSignup("000") must beFalse
canSignup("0foo") must beFalse
canSignup("_foo") must beFalse
canSignup("-foo") must beFalse
}
"bad suffix" in {
couldBeUsername("a_") must beFalse
couldBeUsername("a_") must beFalse
@ -24,4 +35,4 @@ class UserTest extends Specification {
}
}
}
}

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><defs><filter id="shadow-3" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="#ffffff" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="13" dy="17" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><path d="M0 0h512v512H0z" fill="#d0021b" opacity="0"></path><g class="" transform="translate(0,0)" style="touch-action: none;"><path fill="#3893e8" d="M150.25 19.97c-114.48-.574-139.972 184.95 20.563 212.124-29.5.534-55.382 8.11-91.75 25.97C-19.2 306.313.665 462.966 100.874 446c34.48-5.838 51.21-50.325.875-65.375 16.515 29.61-27.968 47.1-41.906 1.938-11.262-36.49 21.145-74.914 52.468-85 30.5-9.82 55.244-10.86 82.47-5.844-36.585 34.247-56.547 80.465-42.376 123.624 44.522 135.595 192.146 82.52 162.844-6.72-10.346-31.506-41.408-46.505-68-10.155 35.164-8.854 50.45 38.75 18.188 49.342-26.355 8.655-60.212-13.527-66.032-41.343-7.82-37.39 19.77-77.195 54.78-95.25 22.176 35.37 38.812 48.68 83.22 72.186 85.843 45.436 212.957-36.54 143.906-110.53-22.626-24.244-54.574-30.02-67.5 13.124 30.188-20.09 60.748 26.8 33.875 47.563-21.95 16.96-61.503 19.135-86.437 5.5-30.797-16.842-53.79-37.798-70.188-66.532 57.07 13.69 119.584-1.065 143-45.342 45.72-86.45-7.046-152.467-59.125-153.375-20.378-.356-40.654 9.237-54.875 31.5-17.85 27.946-9.815 61.533 35.157 59.124-29.11-21.628-1.9-63.623 26.717-45.343 23.378 14.932 22.494 51.88 9.75 77.28-15.165 30.23-60.573 50.738-95.062 24.657-3.008-5.71-5.563-11.683-7.78-17.843 8.99-6.49 14.874-17.028 14.874-28.875 0-17.772-13.252-32.64-30.345-35.218-9.763-47.134-23.34-92.648-84.844-112.594-13.64-4.424-26.437-6.472-38.28-6.53zm117.844 137.405c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.473 16.937-16.936 16.937-9.463 0-16.906-7.474-16.906-16.938 0-9.463 7.443-16.937 16.906-16.937zm-65.406 10.5c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.474 16.937-16.938 16.937-9.463 0-16.937-7.474-16.937-16.938 0-9.463 7.474-16.937 16.938-16.937z"></path></g><!-- react-empty: 13 --></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><path d="M0 0h512v512H0z" fill="transparent" opacity="0"></path><g class="" transform="translate(0,0)" style="touch-action: none;"><path fill="#3893e8" d="M150.25 19.97c-114.48-.574-139.972 184.95 20.563 212.124-29.5.534-55.382 8.11-91.75 25.97C-19.2 306.313.665 462.966 100.874 446c34.48-5.838 51.21-50.325.875-65.375 16.515 29.61-27.968 47.1-41.906 1.938-11.262-36.49 21.145-74.914 52.468-85 30.5-9.82 55.244-10.86 82.47-5.844-36.585 34.247-56.547 80.465-42.376 123.624 44.522 135.595 192.146 82.52 162.844-6.72-10.346-31.506-41.408-46.505-68-10.155 35.164-8.854 50.45 38.75 18.188 49.342-26.355 8.655-60.212-13.527-66.032-41.343-7.82-37.39 19.77-77.195 54.78-95.25 22.176 35.37 38.812 48.68 83.22 72.186 85.843 45.436 212.957-36.54 143.906-110.53-22.626-24.244-54.574-30.02-67.5 13.124 30.188-20.09 60.748 26.8 33.875 47.563-21.95 16.96-61.503 19.135-86.437 5.5-30.797-16.842-53.79-37.798-70.188-66.532 57.07 13.69 119.584-1.065 143-45.342 45.72-86.45-7.046-152.467-59.125-153.375-20.378-.356-40.654 9.237-54.875 31.5-17.85 27.946-9.815 61.533 35.157 59.124-29.11-21.628-1.9-63.623 26.717-45.343 23.378 14.932 22.494 51.88 9.75 77.28-15.165 30.23-60.573 50.738-95.062 24.657-3.008-5.71-5.563-11.683-7.78-17.843 8.99-6.49 14.874-17.028 14.874-28.875 0-17.772-13.252-32.64-30.345-35.218-9.763-47.134-23.34-92.648-84.844-112.594-13.64-4.424-26.437-6.472-38.28-6.53zm117.844 137.405c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.473 16.937-16.936 16.937-9.463 0-16.906-7.474-16.906-16.938 0-9.463 7.443-16.937 16.906-16.937zm-65.406 10.5c9.463 0 16.937 7.474 16.937 16.938 0 9.463-7.474 16.937-16.938 16.937-9.463 0-16.937-7.474-16.937-16.938 0-9.463 7.474-16.937 16.938-16.937z" transform="translate(512, 0) scale(-1, 1) rotate(0, 256, 256)"></path></g><!-- react-empty: 6 --></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -411,10 +411,22 @@ body.piece_letter .fork move {
}
@media (min-width: 1050px) {
div.lichess_game div.lichess_ground {
#puzzle div.lichess_game div.lichess_ground {
width: 280px;
min-width: 280px;
}
#puzzle .fork { left: 90px; }
div.analyse div.lichess_game div.lichess_ground {
width: 25vw;
min-width: 25vw;
}
.fork { left: 90px; }
div.analyse .fork { left: 90px; }
.pocket piece {
flex: 0 0 50px;
}
}
.explorer_box {
@ -1201,8 +1213,8 @@ body.dark .aclock.active {
font-size: 13.5px;
line-height: 1.25;
}
.tview2.literal {
padding: 7px 0px 7px 5px;
.tview2.inline {
padding: 7px 0px 7px 7px;
font-size: 18px;
line-height: 1.05em;
}
@ -1220,7 +1232,7 @@ body.piece_letter .tview2 move {
font-size: 1.092em;
padding: 0 2px;
}
.tview2.literal move {
.tview2.inline move {
padding: 0.25em 0.17em;
white-space: nowrap;
}
@ -1242,7 +1254,7 @@ body.piece_letter .tview2 move {
background: #3893e8;
color: #fff!important;
}
.tview2.literal move:hover {
.tview2.inline move:hover {
border-radius: 3px;
}
.tview2 move.empty {
@ -1263,7 +1275,7 @@ body.piece_letter .tview2 move {
.tview2.column move index:first-child {
padding-left: 0;
}
.tview2.literal move index {
.tview2.inline move index {
padding-right: 0.2em;
}
.tview2 line move {
@ -1311,20 +1323,22 @@ body.piece_letter .tview2 move {
color: #c0c0c0;
}
.tview2 > interrupt {
flex: 0 0 100%;
font-size: 95%;
}
.tview2.column > interrupt {
flex: 0 0 100%;
background: #f8f8f8;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
box-sizing: border-box;
max-width: 100%;
}
.tview2 > interrupt > comment {
.tview2.column > interrupt > comment {
display: block;
padding: 3px 5px;
word-wrap: break-word;
}
.tview2.literal comment {
.tview2.inline comment {
vertical-align: 0.62em;
word-wrap: break-word;
color: #7492bf;
@ -1378,10 +1392,13 @@ body.piece_letter .tview2 move {
}
.tview2 lines {
display: block;
padding-top: 2px;
margin-left: 5px;
margin-top: 2px;
margin-left: 6px;
margin-bottom: 0.8em;
border-left: 1px solid #ccc;
border-left: 2px solid #ccc;
}
.tview2 > interrupt > lines {
margin-left: 0px;
}
.tview2 lines.single {
border-left: none;
@ -1391,7 +1408,7 @@ body.piece_letter .tview2 move {
}
.tview2 line {
display: block;
padding-left: 4px;
padding-left: 7px;
}
.tview2.column line {
margin: 2px 0;
@ -1404,20 +1421,19 @@ body.piece_letter .tview2 move {
}
.tview2 lines lines::before {
content: ' ';
border-top: 1px solid #c0c0c0;
border-top: 2px solid #ccc;
position: absolute;
margin-left: -5.5px;
margin-top: -2px;
width: 5px;
margin-left: -11px;
width: 9px;
height: 6px;
}
.tview2 lines line::before {
margin-top: 0.7em;
margin-left: -4px;
margin-top: 0.65em;
margin-left: -8px;
content: ' ';
border-top: 1px solid #c0c0c0;
border-top: 2px solid #ccc;
position: absolute;
width: 5px;
width: 8px;
height: 6px;
}
.tview2 lines lines:last-child {
@ -1443,8 +1459,8 @@ body.piece_letter .tview2 move {
content: ')';
margin-right: 2px;
}
.tview2.literal inline::before,
.tview2.literal inline::after {
.tview2.inline inline::before,
.tview2.inline inline::after {
vertical-align: 0.7em;
}
.tview2 .conceal {
@ -1468,12 +1484,13 @@ body.dark .tview2.column comment {
body.dark .tview2.column line comment {
color: #888;
}
body.dark .tview2.literal comment {
body.dark .tview2.inline comment {
color: #869fc3;
}
body.dark .tview2 lines,
body.dark .tview2 lines lines::before,
body.dark .tview2 lines line::before {
border-color: #606060;
border-color: #666;
}
.lichess_ground .areplay .result,
.lichess_ground .areplay .status {

View File

@ -1901,6 +1901,12 @@ iframe.video {
border: none;
margin: 1em 0;
}
img.embed {
max-height: 400px;
max-width: 100%;
background: rgba(127, 127, 127, 0.05);
box-shadow: 0 0 5px rgba(0,0,0,0.2);
}
body ::-webkit-scrollbar,
body ::-webkit-scrollbar-corner {
width: 8px;

View File

@ -139,8 +139,8 @@ body.dark .lichess_ground .areplay,
body.dark .lichess_ground .result,
body.dark .lichess_ground .replay .buttons,
body.dark .lichess_ground .replay .moves,
body.dark .tview2 > index,
body.dark .tview2 > index + move,
body.dark .tview2.column > index,
body.dark .tview2.column > index + move,
body.dark div.analysis_menu,
body.dark div.analysis_menu > a,
body.dark .explorer_box,
@ -184,8 +184,8 @@ body.dark div.mchat .chat_tabs .tab,
body.dark #team table.requests.for-team {
border-color: #3d3d3d;
}
body.dark .tview2 > index,
body.dark .tview2 > interrupt,
body.dark .tview2.column > index,
body.dark .tview2.column > interrupt,
body.dark .lichess_ground .areplay .result,
body.dark .lichess_ground .areplay .status,
body.dark .explorer_box .config .choices span:hover,
@ -204,7 +204,6 @@ body.dark #modal-wrap .close:hover {
body.dark hr,
body.dark .fork move,
body.dark .tview2 lines,
body.dark #adv_chart_loader,
body.dark .pull-quote p,
body.dark .pull-quote p:after,

View File

@ -10,6 +10,7 @@
padding: 5px 0;
}
#dasher_app .links a,
#dasher_app .links button,
#dasher_app .subs .sub {
display: block;
padding: 5px 10px;
@ -17,11 +18,18 @@
white-space: nowrap;
}
#dasher_app .links a:hover,
#dasher_app .links button:hover,
#dasher_app .subs .sub:hover,
#dasher_app .langs form > *:hover {
background: #F0F0F0;
color: #444;
}
#dasher_app .links button {
width: 100%;
text-align: left;
border: 0;
background: none;
}
#dasher_app .subs {
border-top: 1px solid #ddd;
}
@ -400,12 +408,16 @@ body.dark #dasher_app .board .zoom {
}
body.dark #dasher_app .head,
body.dark #dasher_app .links a:hover,
body.dark #dasher_app .links button:hover,
body.dark #dasher_app .subs a:hover,
body.dark #dasher_app .langs form > *:hover,
body.dark #dasher_app .selector a:hover {
background: #3e3e3e;
color: #b0b0b0;
}
body.dark #dasher_app .links button {
color: #8f8f8f;
}
body.dark #dasher_app .selector a:hover::before {
color: #555;
}

View File

@ -34,6 +34,7 @@
.gb_edit .gamebook .legend {
display: flex;
border-bottom: 1px solid #ccc;
min-height: 3.5em;
}
.gb_edit .gamebook .legend i {
flex: 0 0 50px;
@ -54,6 +55,8 @@
.gb_edit .gamebook .legend p {
flex: 1 1 100%;
padding: 8px 10px;
display: flex;
align-items: center;
}
.gb_edit .gamebook .todo i {
background: #dc322f;

View File

@ -1,5 +1,5 @@
.gb_play .lichess_game .lichess_ground {
margin-bottom: -140px;
margin-bottom: 0px;
}
.gb_play .gamebook {
height: 100%;
@ -21,13 +21,13 @@
position: absolute;
content: '';
bottom: -9px;
left: 40%;
right: 20%;
width: 15px;
height: 15px;
background: #fff;
border-right: 1.5px solid #aaa;
border-bottom: 1px solid #aaa;
transform: skew(-45deg) rotate(45deg);
border-right: 1px solid #aaa;
border-bottom: 1.5px solid #aaa;
transform: skew(45deg) rotate(45deg);
z-index: 1;
}
.gb_play .comment .content {
@ -44,13 +44,17 @@
z-index: 2;
}
.gb_play .floor {
margin-top: 20px;
flex: 0 0 120px;
display: flex;
}
.gb_play .mascot {
margin: 10px 0 -7px 0;
cursor: pointer;
flex: 0 0 120px;
margin-left: 20px;
}
.gb_play .feedback {
flex: 0 0 120px;
margin-top: 20px;
flex: 1 1 100%;
height: 120px;
text-align: center;
display: flex;
@ -61,7 +65,7 @@
background: #e0e0e0;
font-size: 1.6em;
}
.gb_play .feedback.init {
.gb_play .feedback.good.init {
visibility: hidden;
}
.gb_play .feedback.act {
@ -88,6 +92,7 @@
.gb_play .feedback.end {
flex-flow: row;
justify-content: space-between;
font-size: 0.8em;
}
.gb_play .feedback.end a {
background: rgba(56,147,232,0.8);
@ -103,7 +108,7 @@
transition: 0.13s;
}
.gb_play .feedback.end a::before {
font-size: 2em;
font-size: 2.5em;
margin: 12px 0 8px 0;
}
.gb_play .feedback.end a:first-child {

View File

@ -108,8 +108,8 @@ div.underboard .notif.error {
}
.study_comment_form form.material.form textarea,
.chapter_desc_form form.material.form textarea {
height: 80px;
font-size: 1.2em;
height: 100px;
font-size: 1.1em;
line-height: 1.3em;
resize: vertical;
}
@ -128,7 +128,8 @@ div.underboard .notif.error {
word-wrap: break-word;
display: inline-block;
}
.study_comments .text a {
.study_comments .text a,
.chapter_desc .text a {
color: #3893E8!important;
}
.study_comments .edit {

View File

@ -33,7 +33,7 @@ body.transp::before {
z-index: -1;
}
body.transp #timeline,
body.dark .tview2,
body.dark .areplay,
body.transp div.training .box,
body.transp div.side_box,
body.transp div.box,
@ -73,6 +73,7 @@ body.transp form.translation_form div.message,
body.transp #video_side .tag_list a.checked,
body.transp #powerTip,
body.transp .study_comment_form .material.form .form-group textarea,
body.transp .chapter_desc,
body.transp div.vstext,
body.transp .action_menu {
background: rgba(0, 0, 0, 0.6);
@ -187,3 +188,6 @@ body.transp ::-webkit-scrollbar-corner {
body.transp .action_menu .setting {
margin-right: 10px;
}
body.transp #analyse-cm {
color: #888;
}

@ -1 +1 @@
Subproject commit 356b53cc6a45263956df758877af72eff16a9437
Subproject commit 1783d134e76e61bb1a64c1d3860740dadee0cbcd

View File

@ -13,7 +13,7 @@ export const modeChoices = [
['normal', "Normal analysis"],
['practice', "Practice with computer"],
['conceal', "Hide next moves"],
['gamebook', "Gamebook: interactive lesson [BETA]"]
['gamebook', "Interactive lesson"]
];
export function fieldValue(e: Event, id: string) {

View File

@ -1,6 +1,5 @@
import AnalyseCtrl from '../../ctrl';
import { path as treePath, ops as treeOps } from 'tree';
import Mascot from './mascot';
import { makeShapesFromUci } from '../../autoShape';
type Feedback = 'play' | 'good' | 'bad' | 'end';
@ -15,7 +14,6 @@ export interface State {
export default class GamebookPlayCtrl {
mascot = new Mascot();
state: State;
constructor(readonly root: AnalyseCtrl, readonly chapterId: string, readonly redraw: () => void) {

View File

@ -8,7 +8,7 @@ import { State } from './gamebookPlayCtrl';
const defaultComments = {
play: 'What would you play in this position?',
bad: 'That\'s not the right move.',
end: 'Congratulations! You completed this gamebook.'
end: 'Congratulations! You completed this lesson.'
};
export function render(ctrl: GamebookPlayCtrl): VNode {
@ -23,16 +23,16 @@ export function render(ctrl: GamebookPlayCtrl): VNode {
h('div.content', { hook: richHTML(comment) }),
state.showHint ? h('div.hint', { hook: richHTML(state.hint!) }) : undefined
]) : undefined,
h('img.mascot', {
attrs: {
width: 120,
height: 120,
src: ctrl.mascot.url(),
title: 'Click to choose your teacher'
},
hook: bind('click', ctrl.mascot.switch, ctrl.redraw)
}),
renderFeedback(ctrl, state)
h('div.floor', [
renderFeedback(ctrl, state),
h('img.mascot', {
attrs: {
width: 120,
height: 120,
src: window.lichess.assetUrl('/assets/images/mascot/octopus.svg')
}
})
])
]);
}

View File

@ -1,20 +0,0 @@
export default class Mascot {
private list = [
'octopus',
'parrot-head',
'camel-head',
'owl'
];
private storage = window.lichess.storage.make('gamebook.mascot');
current = this.storage.get() || this.list[0];
switch = () => {
this.current = this.list[(this.list.indexOf(this.current) + 1) % this.list.length];
this.storage.set(this.current);
}
url = () => window.lichess.assetUrl(`/assets/images/mascot/${this.current}.svg`);
}

View File

@ -24,7 +24,7 @@ function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes |
isMainline: true,
withIndex: opts.withIndex
}),
...renderInlineCommentsOf(ctx, main),
...renderInlineCommentsOf(ctx, main, true),
h('interrupt', renderLines(ctx, cs.slice(1), {
parentPath: opts.parentPath,
isMainline: true
@ -65,7 +65,7 @@ function renderLines(ctx: Ctx, nodes: Tree.Node[], opts: Opts): VNode {
function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes {
const path = opts.parentPath + node.id,
comments = renderInlineCommentsOf(ctx, node);
comments = renderInlineCommentsOf(ctx, node, true);
if (opts.truncate === 0) return [
h('move', { attrs: { p: path } }, '[...]')
];
@ -110,8 +110,8 @@ export default function(ctrl: AnalyseCtrl): VNode {
showGlyphs: !!ctrl.study || ctrl.showComputer(),
showEval: !!ctrl.study || ctrl.showComputer()
};
const commentTags = renderInlineCommentsOf(ctx, root);
return h('div.tview2.literal', {
const commentTags = renderInlineCommentsOf(ctx, root, true);
return h('div.tview2.inline', {
hook: mainHook(ctrl)
}, [
...commentTags,

View File

@ -6,17 +6,18 @@ import AnalyseCtrl from '../ctrl';
import contextMenu from '../contextMenu';
import { MaybeVNodes, ConcealOf } from '../interfaces';
import { authorText as commentAuthorText } from '../study/studyComments';
import { enrichText, innerHTML } from '../util';
import { path as treePath } from 'tree';
import column from './columnView';
import literate from './literateView';
import inline from './inlineView';
import { empty, defined, dropThrottle, storedProp, StoredProp } from 'common';
export interface Ctx {
ctrl: AnalyseCtrl;
truncateComments: boolean;
showComputer: boolean;
showGlyphs: boolean;
showEval: boolean;
truncateComments: boolean;
}
export interface Opts {
@ -35,7 +36,7 @@ export interface NodeClasses {
[key: string]: boolean;
}
export type TreeViewKey = 'column' | 'literate';
export type TreeViewKey = 'column' | 'inline';
export interface TreeView {
get: StoredProp<TreeViewKey>;
@ -47,7 +48,7 @@ export function ctrl(): TreeView {
return {
get: value,
toggle() {
value(value() === 'column' ? 'literate' : 'column');
value(value() === 'column' ? 'inline' : 'column');
}
};
}
@ -56,7 +57,7 @@ export function ctrl(): TreeView {
// entry point, dispatching to selected view
export function render(ctrl: AnalyseCtrl, concealOf?: ConcealOf): VNode {
if (ctrl.treeView.get() === 'column') return column(ctrl, concealOf);
return literate(ctrl);
return inline(ctrl);
}
export function nodeClasses(c: AnalyseCtrl, path: Tree.Path): NodeClasses {
@ -71,12 +72,15 @@ export function nodeClasses(c: AnalyseCtrl, path: Tree.Path): NodeClasses {
};
}
export function renderInlineCommentsOf(ctx: Ctx, node: Tree.Node): MaybeVNodes {
export function renderInlineCommentsOf(ctx: Ctx, node: Tree.Node, rich?: boolean): MaybeVNodes {
if (!ctx.ctrl.showComments || empty(node.comments)) return [];
return node.comments!.map(comment => {
if (comment.by === 'lichess' && !ctx.showComputer) return;
return h('comment', [
node.comments![1] ? h('span.by', commentAuthorText(comment.by)) : null,
const by = node.comments![1] ? h('span.by', commentAuthorText(comment.by)) : null;
return rich ? h('comment', {
hook: innerHTML(comment.text, text => enrichText(text, true))
}) : h('comment', [
by,
truncateComment(comment.text, 300, ctx)
]);
}).filter(nonEmpty);

View File

@ -67,8 +67,7 @@ export function plural(noun: string, nb: number): string {
export function titleNameToId(titleName: string): string {
const split = titleName.split(' ');
const name = split.length == 1 ? split[0] : split[1];
return name.toLowerCase();
return (split.length === 1 ? split[0] : split[1]).toLowerCase();
}
export function spinner() {
@ -95,30 +94,35 @@ export function innerHTML<A>(a: A, toHtml: (a: A) => string): Hooks {
}
export function toYouTubeEmbed(url: string, height: number = 300): string | undefined {
let embedUrl = window.lichess.toYouTubeEmbedUrl(url);
const embedUrl = window.lichess.toYouTubeEmbedUrl(url);
if (embedUrl) return `<iframe width="100%" height="${height}" src="${embedUrl}" frameborder=0 allowfullscreen></iframe>`;
}
const commentYoutubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:.*?(?:[?&]v=)|v\/)|youtu\.be\/)(?:[^"&?\/ ]{11})\b/i;
const imgUrlRegex = /\.(jpg|jpeg|png|gif)$/;
const newLineRegex = /\n/g;
function imageTag(url: string): string | undefined {
if (imgUrlRegex.test(url)) return `<img src="${url}" class="embed"/>`;
}
function toLink(url: string) {
if (commentYoutubeRegex.test(url)) return toYouTubeEmbed(url) || url;
const show = imageTag(url) || url.replace(/https?:\/\//, '');
return '<a target="_blank" rel="nofollow" href="' + url + '">' + show + '</a>';
}
export function enrichText(text: string, allowNewlines: boolean): string {
let html = autolink(window.lichess.escapeHtml(text), url => {
if (commentYoutubeRegex.test(url)) {
return toYouTubeEmbed(url) || url;
}
const show = url.replace(/https?:\/\//, '');
return '<a target="_blank" rel="nofollow" href="' + url + '">' + show + '</a>';
});
if (allowNewlines) html = html.replace(/\n/g, '<br>');
let html = autolink(window.lichess.escapeHtml(text), toLink);
if (allowNewlines) html = html.replace(newLineRegex, '<br>');
return html;
}
// from https://github.com/bryanwoods/autolink-js/blob/master/autolink.js
const linkRegex = /(^|[\s\n]|<[A-Za-z]*\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
export function autolink(str: string, callback: (str: string) => string): string {
const pattern = /(^|[\s\n]|<[A-Za-z]*\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
return str.replace(pattern, function(_, space, url) {
return "" + space + callback(url);
});
return str.replace(linkRegex, (_, space, url) => space + callback(url));
}
export function option(value: string, current: string | undefined, name: string) {

View File

@ -172,7 +172,7 @@ export default function(opts: CevalOpts): CevalCtrl {
hashSize,
infinite,
hovering,
setHovering(fen: Fen, uci: Uci) {
setHovering(fen: Fen, uci?: Uci) {
hovering(uci ? {
fen,
uci

View File

@ -65,7 +65,7 @@ export interface CevalCtrl {
possible: boolean;
isComputing(): boolean;
variant: Variant;
setHovering: (fen: string, uci: string | null) => void;
setHovering: (fen: string, uci?: string) => void;
multiPv: StoredProp<number>;
start: (path: string, steps: Step[], threatMode: boolean, deeper: boolean) => void;
stop(): void;

View File

@ -192,17 +192,26 @@ export function renderCeval(ctrl: ParentCtrl): VNode | undefined {
class: {
computing: percent < 100 && instance.isComputing()
}
}, [progressBar].concat(body).concat(switchButton).concat(threatButton(ctrl)));
}, [
progressBar,
...body,
switchButton,
threatButton(ctrl)
]);
}
function getElFen(el: HTMLElement): string {
return el.getAttribute('data-fen')!;
}
function getElUci(e: MouseEvent): string | undefined {
return $(e.target).closest('div.pv').attr('data-uci');
}
function checkHover(el: HTMLElement, instance: CevalCtrl): void {
setTimeout(function() {
window.lichess.requestIdleCallback(() => {
instance.setHovering(getElFen(el), $(el).find('div.pv:hover').attr('data-uci'));
}, 100);
});
}
export function renderPvs(ctrl: ParentCtrl) {
@ -221,14 +230,14 @@ export function renderPvs(ctrl: ParentCtrl) {
hook: {
insert: vnode => {
const el = vnode.elm as HTMLElement;
el.addEventListener('mouseover', function(e) {
instance.setHovering(getElFen(el), $(e.target).closest('div.pv').attr('data-uci'));
el.addEventListener('mouseover', (e: MouseEvent) => {
instance.setHovering(getElFen(el), getElUci(e));
});
el.addEventListener('mouseout', function() {
instance.setHovering(getElFen(el), null);
el.addEventListener('mouseout', () => {
instance.setHovering(getElFen(el));
});
el.addEventListener('mousedown', function(e) {
const uci = $(e.target).closest('div.pv').attr('data-uci');
el.addEventListener('mousedown', (e: MouseEvent) => {
const uci = getElUci(e);
if (uci) ctrl.playUci(uci);
});
checkHover(el, instance);

View File

@ -184,7 +184,7 @@ export function render(ctrl): VNode {
ctrl: ctrl,
showComputer: false
};
return h('div.tview2', {
return h('div.tview2.column', {
hook: {
insert: vnode => {
const el = vnode.elm as HTMLElement;