diff --git a/client/src/main/scala/com/v6ak/scalajs/tables/Column.scala b/client/src/main/scala/com/v6ak/scalajs/tables/Column.scala index aad61eb..6e43ae1 100644 --- a/client/src/main/scala/com/v6ak/scalajs/tables/Column.scala +++ b/client/src/main/scala/com/v6ak/scalajs/tables/Column.scala @@ -1,12 +1,16 @@ package com.v6ak.scalajs.tables +import com.v6ak.zbdb.HtmlUtils.EmptyHtml import org.scalajs.dom.Node +import scalatags.JsDom.all.* -import scalatags.JsDom.all -import scalatags.JsDom.all._ - -final case class TableHeadColumn(content: Node, colCount: Int = 1, rowCountOption: Option[Int] = None, className: String) { +final case class TableHeadColumn( + content: Node, + colCount: Int = 1, + rowCountOption: Option[Int] = None, + className: String, +): def create(headRows: Int) = th( `class` := className, colspan := colCount, @@ -14,32 +18,29 @@ final case class TableHeadColumn(content: Node, colCount: Int = 1, rowCountOptio content ) -} -abstract sealed class TableHeadCell{ +abstract sealed class TableHeadCell: def rowCount: Int def build(className: String): Option[TableHeadColumn] -} -object TableHeadCell{ +object TableHeadCell: def apply(frag: Frag, colCount: Int = 1, rowCount: Int = 1, additionalClass: String = "") = Full( content = frag.render, colCount = colCount, rowCount = rowCount, additionalClass = additionalClass ) - final case class Full(content: Node, colCount: Int, rowCount: Int, additionalClass: String) extends TableHeadCell{ + final case class Full(content: Node, colCount: Int, rowCount: Int, additionalClass: String) extends TableHeadCell: def build(className: String) = Some(TableHeadColumn( content = content, colCount = colCount, rowCountOption = Some(rowCount), className = className + " " + additionalClass )) - } - case object Empty extends TableHeadCell{ + + case object Empty extends TableHeadCell: override def rowCount: Int = 0 override def build(className: String): Option[TableHeadColumn] = None - } -} -object Column{ + +object Column: def apply[T](header: Frag)(cellRenderer: T => Frag) = new Column[T] { override def rowCount: Int = 1 @@ -48,27 +49,34 @@ object Column{ case _ => None } private def renderContent(data: T): Node = cellRenderer(data).render - override def createContentCell(row: T): Frag = td(renderContent(row)) + override def createContentCell(row: T, pos: Int, size: Int): Frag = td(renderContent(row)) } def apply[T](headers: TableHeadCell*)(cellRenderer: T => Frag)(className: String = "") = new Column[T] { override def rowCount: Int = headers.map(_.rowCount).sum override def renderHeader(i: Int): Option[TableHeadColumn] = headers.lift(i).flatMap(_.build(className = className)) private def renderContent(data: T): Node = cellRenderer(data).render - override def createContentCell(row: T): Frag = td(`class` := className, renderContent(row)) + override def createContentCell(row: T, pos: Int, size: Int): Frag = td(`class` := className, renderContent(row)) + } + + def singleCell[T](headers: TableHeadCell*)(cell: Frag)(className: String = "") = new Column[T] { + override def rowCount: Int = headers.map(_.rowCount).sum + override def renderHeader(i: Int): Option[TableHeadColumn] = headers.lift(i).flatMap(_.build(className = className)) + private def renderContent(data: T): Node = cell.render + override def createContentCell(row: T, pos: Int, size: Int): Frag = + if(pos == 0) td(`class` := className, rowspan := size, renderContent(row)) + else EmptyHtml } def rich[T](headers: TableHeadCell*)(cellRenderer: T => Seq[Modifier])(className: String = "") = new Column[T] { override def rowCount: Int = headers.map(_.rowCount).sum override def renderHeader(i: Int): Option[TableHeadColumn] = headers.lift(i).flatMap(_.build(className = className)) private def renderContent(data: T): Modifier = cellRenderer(data) - override def createContentCell(row: T): Frag = td(`class` := className, renderContent(row)) + override def createContentCell(row: T, pos: Int, size: Int): Frag = td(`class` := className, renderContent(row)) } -} -abstract class Column[T]{ +abstract class Column[T]: def rowCount: Int def renderHeader(i: Int): Option[TableHeadColumn] - def createContentCell(row: T): Frag -} + def createContentCell(row: T, pos: Int, size: Int): Frag diff --git a/client/src/main/scala/com/v6ak/scalajs/tables/TableRenderer.scala b/client/src/main/scala/com/v6ak/scalajs/tables/TableRenderer.scala index 3cc2f68..2ab48f4 100644 --- a/client/src/main/scala/com/v6ak/scalajs/tables/TableRenderer.scala +++ b/client/src/main/scala/com/v6ak/scalajs/tables/TableRenderer.scala @@ -1,17 +1,18 @@ package com.v6ak.scalajs.tables import org.scalajs.dom.html.TableRow - import scalatags.JsDom.TypedTag -import scalatags.JsDom.all._ +import scalatags.JsDom.all.* -final class TableRenderer[T](headRows: Int = 1, tableModifiers: Seq[Modifier] = Seq(), trWrapper: (TypedTag[TableRow], T) => TypedTag[TableRow] = {(a: TypedTag[TableRow], _: T) => a})(columns: Seq[Column[T]]){ +final class TableRenderer[T]( + headRows: Int = 1, + tableModifiers: Seq[Modifier] = Seq(), + trWrapper: (TypedTag[TableRow], T) => TypedTag[TableRow] = {(a: TypedTag[TableRow], _: T) => a}, +)(columns: Seq[Column[T]]): - columns foreach { c => - if(c.rowCount > headRows){ // TODO: consider !=; however, I am not sure about multiple-row cells + columns foreach: c => + if(c.rowCount > headRows) // TODO: consider !=; however, I am not sure about multiple-row cells sys.error(s"bad rowCount ${c.rowCount} for $c") - } - } def renderTableHead = thead(0 until headRows map { headRowIndex => tr(columns.map{col => @@ -21,13 +22,21 @@ final class TableRenderer[T](headRows: Int = 1, tableModifiers: Seq[Modifier] = }) }) - def renderTableBody(data: Seq[T]) = tbody(data.map(row => trWrapper(tr(columns.map(col => col.createContentCell(row))), row))) + def renderTableBody(data: Seq[T]) = tbody( + data.zipWithIndex.map( (row, i) => + trWrapper( + tr( + columns.map(col => + col.createContentCell(row, i, data.size) + ) + ), + row + ) + ) + ) def renderTable(data: Seq[T]) = table( renderTableHead, - renderTableBody(data) - )( - tableModifiers : _* + renderTableBody(data), + tableModifiers, ).render - -} diff --git a/client/src/main/scala/com/v6ak/zbdb/ClassSwitches.scala b/client/src/main/scala/com/v6ak/zbdb/ClassSwitches.scala index 3c3fcc2..cea9778 100644 --- a/client/src/main/scala/com/v6ak/zbdb/ClassSwitches.scala +++ b/client/src/main/scala/com/v6ak/zbdb/ClassSwitches.scala @@ -5,7 +5,10 @@ import org.scalajs.dom.* import scalatags.JsDom.all.{button as buttonTag, *} -final class ClassSwitches(initialSwitchesState: Map[String, String]) { +final class ClassSwitches( + initialSwitchesState: Map[String, String], + alreadySwitchedClasses: Map[String, String] = Map(), +) { private val switchesState = collection.mutable.Map(initialSwitchesState.toSeq: _*) def values = switchesState.values @@ -16,6 +19,7 @@ final class ClassSwitches(initialSwitchesState: Map[String, String]) { classList.add(newClass) oldClasses.foreach(classList.remove) switchesState(switchName) = newClass + alreadySwitchedClasses.get(switchName).foreach(classList.add) } def classSelect(switchName: String)(items: (String, String)*) = select( diff --git a/client/src/main/scala/com/v6ak/zbdb/Renderer.scala b/client/src/main/scala/com/v6ak/zbdb/Renderer.scala index c3473e6..40c6dc9 100644 --- a/client/src/main/scala/com/v6ak/zbdb/Renderer.scala +++ b/client/src/main/scala/com/v6ak/zbdb/Renderer.scala @@ -52,7 +52,10 @@ final class Renderer private( private val plotRenderer = new PlotRenderer(participantTable) private val timeLineRenderer = new TimeLineRenderer(participantTable, plotRenderer) - private val switches = new ClassSwitches(Map("details" -> "without-details")) + private val switches = new ClassSwitches( + Map("details" -> "without-details"), + Map("details" -> "details-switched"), + ) import ChartJsUtils._ import Renderer._ @@ -117,29 +120,32 @@ final class Renderer private( val expandButton = switches.button("details", "with-details", detailsValues)(expandCollapseStyle)("") val collapseButton = switches.button("details", "without-details", detailsValues)( expandCollapseStyle, `class`:="btn btn-secondary" - )("«") + )("Zobrazit stručně") private val renderer = new TableRenderer[Participant]( headRows = 2, tableModifiers = Seq(`class` := "table table-sm table-hover participants-table"), trWrapper = {(tableRow, row) => tableRow(id := row.trId)} )(Seq[Column[Participant]]( - Column(TableHeadCell(yearSelection), TableHeadCell("id, jméno"))(renderParticipantColumn)(className = "participant-header"), + Column( + TableHeadCell(fseq( + collapseButton(`class` := "detailed-only"), + yearSelection, + )), + TableHeadCell("id, jméno") + )(renderParticipantColumn)(className = "participant-header"), Column[Participant](TableHeadCell(""), TableHeadCell("Kat."))(p => Seq[Frag](span(cls:="gender")(Genders(p.gender)), " ", span(cls:="age")(p.age)))(className = "category") ) ++ Seq[Option[Column[Participant]]]( if(formatVersion.ageType == AgeType.BirthYear) Some(Column[Participant]("Roč.")(p => Seq[Frag](p.birthYear.get))) else None ).flatten ++ Seq( - Column[Participant](TableHeadCell(expandButton, rowCount = 2))(_ => expandButton)( + Column.singleCell[Participant](TableHeadCell(fseq(expandButton), rowCount = 2))(expandButton)( className = "without-details-only expand-columns" ), - Column[Participant](TableHeadCell(collapseButton, rowCount = 2))(_ => collapseButton)( - className = "detailed-only" - ), ) ++ parts.zipWithIndex.flatMap{case (part, i) => createTrackPartColumns(part, i) } ++ Seq[Column[Participant]]( Column( - TableHeadCell("", additionalClass = "without-details-only"), + TableHeadCell("", additionalClass = "without-details-only dont-highlight"), TableHeadCell(span(title := "Celkový čas")("Celk.")) ){(p: Participant) => if(p.hasFinished) p.totalTime.toString else "" diff --git a/server/app/assets/main.scss b/server/app/assets/main.scss index 1600937..3e784dc 100644 --- a/server/app/assets/main.scss +++ b/server/app/assets/main.scss @@ -50,17 +50,29 @@ $color1: lightgray; $color2: transparent; background-color: transparent; - background-image: linear-gradient( - 90deg, - $color1 8.33%, - $color2 8.33%, - $color2 50%, - $color1 50%, - $color1 58.33%, - $color2 58.33%, - $color2 100% - ); - background-size: 12px 12px; + background-image: + url("other-columns.svg?inline"), + linear-gradient( + 90deg, + $color1 8.33%, + $color2 8.33%, + $color2 50%, + $color1 50%, + $color1 58.33%, + $color2 58.33%, + $color2 100%, + ), + ; + background-size: + 40% auto, + 12px 12px, + ; + background-repeat: + repeat-y, + repeat; + background-position: + center 10px, + left; position: absolute; width: 100%; top: -1px; // over border @@ -321,6 +333,12 @@ body{ margin-bottom: 20px; } +@keyframes highlight-background { + from { + background-color: yellow; + color: black; + } +} body.with-details { .without-details-only { @@ -337,6 +355,11 @@ body.without-details { display: none; } } +body.details-switched { + .detailed-only:not(.dont-highlight), .without-details-only:not(.dont-highlight) { + animation: 1s linear highlight-background; + } +} .with-relative-time .clock-time, .with-clock-time .relative-time, diff --git a/server/app/assets/other-columns.svg b/server/app/assets/other-columns.svg new file mode 100644 index 0000000..7f69d26 --- /dev/null +++ b/server/app/assets/other-columns.svg @@ -0,0 +1,54 @@ + + + + + + + + další sloupce + +