Initial Commit
commit
74afa40620
|
@ -0,0 +1,3 @@
|
|||
/out
|
||||
/.bloop
|
||||
/.metals
|
|
@ -0,0 +1,133 @@
|
|||
import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION`
|
||||
import $ivy.`io.bit3:jsass:5.10.4`
|
||||
import mill._
|
||||
import mill.scalalib._
|
||||
import mill.scalalib.publish._
|
||||
import mill.scalajslib._
|
||||
import coursier.maven.{ MavenRepository }
|
||||
import mill.util.Ctx
|
||||
import mill.api.{ Result, PathRef }
|
||||
import io.bit3.jsass.{ Compiler => SassCompiler, Options => SassOptions, OutputStyle => SassOutputStyle }
|
||||
|
||||
|
||||
/*************************************************
|
||||
*** Overarching Target
|
||||
*************************************************/
|
||||
object cells extends Module {
|
||||
|
||||
val scalaVersion = "3.2.1"
|
||||
|
||||
def run() = server.run()
|
||||
|
||||
object shared extends Module
|
||||
|
||||
object server extends ScalaModule
|
||||
{
|
||||
def scalaVersion = cells.scalaVersion
|
||||
|
||||
def mainClass = Some("net.okennedy.cells.CellsServer")
|
||||
|
||||
def ivyDeps = Agg(
|
||||
ivy"com.lihaoyi::cask:0.8.3",
|
||||
ivy"com.typesafe.play::play-json::2.10.0-RC7",
|
||||
)
|
||||
|
||||
def sources = T.sources(
|
||||
millSourcePath,
|
||||
shared.millSourcePath
|
||||
)
|
||||
|
||||
def resources =
|
||||
T.sources(
|
||||
millSourcePath / "shared" / "resources",
|
||||
cells.resources()
|
||||
)
|
||||
}
|
||||
|
||||
object ui extends ScalaJSModule
|
||||
{
|
||||
def scalaVersion = cells.scalaVersion
|
||||
def scalaJSVersion = "1.12.0"
|
||||
|
||||
def ivyDeps = Agg(
|
||||
ivy"org.scala-js::scalajs-dom::2.3.0",
|
||||
ivy"com.raquo::laminar::0.14.5",
|
||||
ivy"com.typesafe.play::play-json::2.10.0-RC7",
|
||||
)
|
||||
|
||||
def sources = T.sources(
|
||||
millSourcePath,
|
||||
shared.millSourcePath
|
||||
)
|
||||
|
||||
def sass =
|
||||
T.sources {
|
||||
os.walk(millSourcePath / "css")
|
||||
.filter { _.ext == "scss" }
|
||||
.map { PathRef(_) }
|
||||
}
|
||||
|
||||
def compiledSass =
|
||||
T {
|
||||
val compiler = new SassCompiler()
|
||||
val options = new SassOptions()
|
||||
val target = T.dest
|
||||
options.setOutputStyle(SassOutputStyle.COMPRESSED)
|
||||
|
||||
val src = sass().filter { _.path.last == "cells.scss" }.head
|
||||
val out = target / "cells.css"
|
||||
println(s"IGNORE THE FOLLOWING DEPRECATION WARNING: https://gitlab.com/jsass/jsass/-/issues/95")
|
||||
val output = compiler.compileFile(
|
||||
new java.net.URI((src.path.toString).toString),
|
||||
new java.net.URI(out.toString),
|
||||
options
|
||||
)
|
||||
output.getCss
|
||||
}
|
||||
|
||||
def html =
|
||||
T.sources {
|
||||
os.walk(millSourcePath / "html")
|
||||
.map { PathRef(_) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def resources =
|
||||
T {
|
||||
val target = T.dest
|
||||
|
||||
// Cells UI binary
|
||||
os.copy.over(
|
||||
ui.fastOpt().path,
|
||||
target / "app" / "cells.js",
|
||||
createFolders = true
|
||||
)
|
||||
os.copy.over(
|
||||
ui.fastOpt().path / os.up / (ui.fastOpt().path.last+".map"),
|
||||
target / "app" / (ui.fastOpt().path.last+".map"),
|
||||
createFolders = true
|
||||
)
|
||||
|
||||
os.write(
|
||||
target / "app" / "cells.css",
|
||||
ui.compiledSass(),
|
||||
createFolders = true
|
||||
)
|
||||
|
||||
val assets =
|
||||
ui.html().map { x => (x.path -> os.rel / x.path.last) }
|
||||
|
||||
for((asset, assetTarget) <- assets){
|
||||
os.copy.over(
|
||||
asset,
|
||||
target / "app" / assetTarget,
|
||||
createFolders = true
|
||||
)
|
||||
}
|
||||
|
||||
println(s"Generated UI resource dir: $target")
|
||||
target
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
import scala.io.Source
|
||||
import play.api.libs.json._
|
||||
|
||||
object CellsServer extends cask.MainRoutes
|
||||
{
|
||||
|
||||
var layout = new state.Canvas()
|
||||
layout.addTable()
|
||||
|
||||
def serveResource(path: String, headers: (String,String)*): cask.Response[String] =
|
||||
{
|
||||
val resource =
|
||||
Source.fromInputStream(
|
||||
this.getClass().getClassLoader().getResourceAsStream(path)
|
||||
).getLines.mkString
|
||||
|
||||
cask.Response.apply(
|
||||
resource,
|
||||
headers = headers
|
||||
)
|
||||
}
|
||||
|
||||
@cask.get("/")
|
||||
def index(request: cask.Request) =
|
||||
serveResource("app/index.html", "Content-Type" -> "text/html")
|
||||
|
||||
@cask.get("/cells.js")
|
||||
def app(request: cask.Request) =
|
||||
serveResource("app/cells.js", "Content-Type" -> "application/javascript")
|
||||
|
||||
@cask.get("/cells.css")
|
||||
def css(request: cask.Request) =
|
||||
serveResource("app/cells.css", "Content-Type" -> "text/css")
|
||||
|
||||
@cask.get("/out.js.map")
|
||||
def appMap(request: cask.Request) =
|
||||
serveResource("app/out.js.map", "Content-Type" -> "application/javascript")
|
||||
|
||||
@cask.websocket("/ws")
|
||||
def websocket(): cask.WebsocketResult =
|
||||
{
|
||||
|
||||
cask.WsHandler { channel =>
|
||||
def send(msg: WebsocketResponse): Unit =
|
||||
{
|
||||
channel.send(
|
||||
cask.Ws.Text(
|
||||
Json.toJson(msg).toString
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
cask.WsActor {
|
||||
case cask.Ws.Text(data) =>
|
||||
Json.parse(data).as[WebsocketRequest] match {
|
||||
case WebsocketHello(_) =>
|
||||
for(table <- layout.tables.values)
|
||||
{
|
||||
send(AddTable(table.serialize))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@cask.staticResources("/app")
|
||||
def staticResourceRoutes() = "app"
|
||||
|
||||
initialize()
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package net.okennedy.cells.state
|
||||
|
||||
import scala.collection.mutable
|
||||
import net.okennedy.cells.Identifier
|
||||
import net.okennedy.cells.serialized
|
||||
|
||||
class Canvas()
|
||||
{
|
||||
val tables = mutable.Map[Identifier, Table]()
|
||||
|
||||
def genSafeTableId(depth: Int = 0): Identifier =
|
||||
{
|
||||
val id = java.util.UUID.randomUUID()
|
||||
if(tables contains id){
|
||||
if(depth > 100) { throw new Exception("Too Many Tables!!!") }
|
||||
return genSafeTableId(depth + 1)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
def addTable(): Unit =
|
||||
{
|
||||
val table = new Table(genSafeTableId())
|
||||
tables.put(table.id, table)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package net.okennedy.cells.state
|
||||
|
||||
import net.okennedy.cells.Identifier
|
||||
import net.okennedy.cells.serialized
|
||||
|
||||
class Table(var id: Identifier)
|
||||
{
|
||||
|
||||
def serialize =
|
||||
serialized.Table(
|
||||
id
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
import play.api.libs.json._
|
||||
|
||||
sealed trait WebsocketRequest
|
||||
|
||||
case class WebsocketHello(client: String) extends WebsocketRequest
|
||||
|
||||
object WebsocketRequest
|
||||
{
|
||||
implicit val WebsocketHelloFormat: Format[WebsocketHello] = Json.format
|
||||
implicit val WebsocketRequestFormat: Format[WebsocketRequest] = Json.format
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
import play.api.libs.json._
|
||||
|
||||
sealed trait WebsocketResponse
|
||||
|
||||
sealed trait CanvasOp extends WebsocketResponse
|
||||
case class AddTable(table: serialized.Table) extends CanvasOp
|
||||
|
||||
sealed trait TableOp extends CanvasOp
|
||||
|
||||
|
||||
object WebsocketResponse
|
||||
{
|
||||
implicit val AddTableFormat: Format[AddTable] = Json.format
|
||||
implicit val WebsocketResponseFormat: Format[WebsocketResponse] = Json.format
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package net.okennedy.cells.serialized
|
||||
|
||||
import play.api.libs.json._
|
||||
|
||||
case class Cell(spec: String, value: JsValue)
|
|
@ -0,0 +1,8 @@
|
|||
package net.okennedy.cells.serialized
|
||||
|
||||
import net.okennedy.cells._
|
||||
|
||||
case class ColSpec(
|
||||
id: Identifier,
|
||||
width: Int
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package net.okennedy.cells.serialized
|
||||
|
||||
import net.okennedy.cells._
|
||||
|
||||
case class RowSpec(
|
||||
id: Identifier,
|
||||
height: Int
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
package net.okennedy.cells.serialized
|
||||
|
||||
import play.api.libs.json._
|
||||
import net.okennedy.cells._
|
||||
|
||||
case class Table(
|
||||
id: Identifier,
|
||||
// x: Int,
|
||||
// y: Int,
|
||||
// rows: Seq[RowSpec],
|
||||
// cols: Seq[ColSpec],
|
||||
// data: Seq[Seq[Cell]]
|
||||
)
|
||||
|
||||
object Table
|
||||
{
|
||||
implicit val tableFormat: Format[Table] = Json.format
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
type Identifier = java.util.UUID
|
|
@ -0,0 +1,110 @@
|
|||
.debug-red
|
||||
{
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.spreadsheetCanvas
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.dataTable
|
||||
{
|
||||
position: absolute;
|
||||
background-color: #cccccc;
|
||||
border-top-left-radius: 4px;
|
||||
user-select: none;
|
||||
|
||||
.columnGutters
|
||||
{
|
||||
position: absolute;
|
||||
|
||||
.columnGutter
|
||||
{
|
||||
position: absolute;
|
||||
border: 1px solid darkgrey;
|
||||
text-align: center;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.rowGutters
|
||||
{
|
||||
position: absolute;
|
||||
|
||||
.rowGutter
|
||||
{
|
||||
position: absolute;
|
||||
border: 1px solid darkgrey;
|
||||
text-align: center;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
|
||||
.valignHack
|
||||
{
|
||||
position: absolute;
|
||||
width: calc(100% - 4px);
|
||||
text-align: right;
|
||||
margin-top: -0.5em;
|
||||
height: 1em;
|
||||
top: 50%;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content
|
||||
{
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
|
||||
.row
|
||||
{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
.cell
|
||||
{
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border-right: solid 1px lightgrey;
|
||||
border-bottom: solid 1px lightgrey;
|
||||
|
||||
.cellBody
|
||||
{
|
||||
position: absolute;
|
||||
height: 1em;
|
||||
top: 50%;
|
||||
margin-top: -0.5em;
|
||||
min-width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widgets
|
||||
{
|
||||
.dragFrame
|
||||
{
|
||||
position: absolute;
|
||||
background-color: rgba(127, 127, 127, .3);
|
||||
margin-left: 2px;
|
||||
margin-top: 2px;
|
||||
border: dashed lightgrey 2px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cells</title>
|
||||
<link rel="stylesheet" type="text/css" href="cells.css">
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="cells.js"></script>
|
||||
<script type="text/javascript">
|
||||
Cells.run()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,37 @@
|
|||
package net.okennedy.cells
|
||||
|
||||
import org.scalajs.dom
|
||||
import scala.scalajs.js.annotation._
|
||||
import com.raquo.laminar.api.L._
|
||||
|
||||
|
||||
@JSExportTopLevel("Cells")
|
||||
object CellsUI
|
||||
{
|
||||
|
||||
// val dataTables = Var[Map[String, sheet.DataTable]](initial = Map.empty)
|
||||
|
||||
@JSExport("run")
|
||||
def run(): Unit =
|
||||
{
|
||||
val conn = new network.Connection("ws://localhost:8080/ws")
|
||||
|
||||
dom.window.onload = { (evt: dom.Event) =>
|
||||
val container: dom.Element =
|
||||
dom.document.querySelector("body")
|
||||
|
||||
// val tableStream =
|
||||
// dataTables.signal
|
||||
// .map { _.values.toSeq }
|
||||
// .split { _.id }
|
||||
|
||||
render(
|
||||
container,
|
||||
div(
|
||||
conn.canvas.root,
|
||||
widgets.Widgets.root
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package net.okennedy.cells.network
|
||||
|
||||
import org.scalajs.dom
|
||||
import play.api.libs.json._
|
||||
import net.okennedy.cells.WebsocketRequest
|
||||
import net.okennedy.cells.WebsocketResponse
|
||||
import net.okennedy.cells.WebsocketHello
|
||||
import net.okennedy.cells.CanvasOp
|
||||
import net.okennedy.cells.sheet
|
||||
|
||||
class Connection(url: String)
|
||||
{
|
||||
val canvas = new sheet.Canvas()
|
||||
|
||||
var socket = getSocket()
|
||||
|
||||
protected[network] def getSocket(): dom.WebSocket =
|
||||
{
|
||||
val s = new dom.WebSocket(url)
|
||||
s.onopen = onConnected(_)
|
||||
s.onclose = onClosed(_)
|
||||
s.onerror = onError(_)
|
||||
s.onmessage = onMessage(_)
|
||||
// keepaliveTimer = setInterval(20000) { keepalive(s) }
|
||||
s
|
||||
}
|
||||
|
||||
def send(msg: WebsocketRequest): Unit =
|
||||
{
|
||||
socket.send(Json.toJson(msg).toString)
|
||||
}
|
||||
|
||||
|
||||
def onConnected(event: dom.Event): Unit =
|
||||
{
|
||||
println("Connected")
|
||||
send(WebsocketHello("itsame"))
|
||||
}
|
||||
|
||||
def onClosed(event: dom.Event): Unit =
|
||||
{
|
||||
println("Closed")
|
||||
}
|
||||
def onError(event: dom.Event): Unit =
|
||||
{
|
||||
println("Error!")
|
||||
}
|
||||
|
||||
def onMessage(message: dom.MessageEvent): Unit =
|
||||
{
|
||||
// println(s"Received: ${message.data.toString.take(200)}")
|
||||
val request = Json.parse(message.data.toString).as[WebsocketResponse]
|
||||
request match {
|
||||
case x:CanvasOp => canvas.process(x)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package net.okennedy.cells.sheet
|
||||
|
||||
import scala.collection.mutable
|
||||
import com.raquo.airstream.state.Var
|
||||
import net.okennedy.cells._
|
||||
import com.raquo.laminar.api.L._
|
||||
|
||||
class Canvas()
|
||||
{
|
||||
val tables = Var[Map[Identifier, Table]](initial = Map.empty)
|
||||
|
||||
def process(op: CanvasOp): Unit =
|
||||
{
|
||||
op match {
|
||||
case AddTable(table) =>
|
||||
tables.set(tables.now() ++ Map(table.id -> new Table(table)))
|
||||
}
|
||||
}
|
||||
|
||||
val tableNodes =
|
||||
tables.signal.map { _.values.toSeq }
|
||||
.split( key = _.id )( (k, tabl, sig) => tabl.root )
|
||||
|
||||
val root = div(
|
||||
className("spreadsheetCanvas"),
|
||||
children <-- tableNodes
|
||||
)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package net.okennedy.cells.sheet
|
||||
|
||||
import com.raquo.laminar.api.L._
|
||||
|
||||
class Cell(val data: String)
|
||||
{
|
||||
val tag =
|
||||
div(
|
||||
className("cellBody text"),
|
||||
data
|
||||
)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package net.okennedy.cells.sheet
|
||||
|
||||
import net.okennedy.cells.Identifier
|
||||
import net.okennedy.cells.serialized
|
||||
|
||||
class ColumnGutter(val spec: serialized.ColSpec, val position: Int, val idx: Int)
|
||||
{
|
||||
def label: String =
|
||||
{
|
||||
var i = idx
|
||||
var l = ""
|
||||
while(true) {
|
||||
l += ('A'.toByte + (i % 26)).toChar.toString
|
||||
if(i < 26) { return l }
|
||||
i /= 26
|
||||
}
|
||||
return "???"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package net.okennedy.cells.sheet
|
||||
|
||||
object Constants
|
||||
{
|
||||
val GUTTER_HEIGHT = 20
|
||||
val GUTTER_WIDTH = 30
|
||||
val BORDER = 1
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package net.okennedy.cells.sheet
|
||||
|
||||
import net.okennedy.cells.Identifier
|
||||
import net.okennedy.cells.serialized
|
||||
|
||||
class RowGutter(val spec: serialized.RowSpec, val position: Int, val idx: Int)
|
||||
{
|
||||
def label = (idx+1).toString
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package net.okennedy.cells.sheet
|
||||
|
||||
import com.raquo.laminar.api.L._
|
||||
import net.okennedy.cells.Identifier
|
||||
import net.okennedy.cells.serialized
|
||||
import com.raquo.airstream.core.Signal
|
||||
import com.raquo.airstream.combine.MergeEventStream
|
||||
import play.api.libs.json._
|
||||
import com.raquo.airstream.core.EventStream
|
||||
import com.raquo.airstream.flatten.ConcurrentEventStream
|
||||
import com.raquo.airstream.flatten.FlattenStrategy.ConcurrentStreamStrategy
|
||||
import com.raquo.airstream.flatten.FlattenStrategy
|
||||
import net.okennedy.cells.widgets.DragFrame
|
||||
import com.raquo.airstream.ownership.OneTimeOwner
|
||||
|
||||
class Table(val id: Identifier)
|
||||
{
|
||||
implicit val tableOwner: Owner =
|
||||
new OneTimeOwner( () => println(s"Accessing owner of table $id after killed") )
|
||||
|
||||
def this(ser: serialized.Table) =
|
||||
{
|
||||
this(
|
||||
ser.id
|
||||
)
|
||||
}
|
||||
|
||||
val columns =
|
||||
Var[Seq[serialized.ColSpec]](initial = Seq(
|
||||
serialized.ColSpec(java.util.UUID.randomUUID(), width = 100),
|
||||
serialized.ColSpec(java.util.UUID.randomUUID(), width = 100),
|
||||
serialized.ColSpec(java.util.UUID.randomUUID(), width = 100),
|
||||
))
|
||||
|
||||
val columnGutters: Signal[Seq[(Identifier, Signal[ColumnGutter])]] =
|
||||
columns.signal
|
||||
.map {
|
||||
_.foldLeft( (0, 0, Seq[ColumnGutter]()) ) {
|
||||
case ((pos, idx, accum), col) =>
|
||||
(pos+col.width, idx+1, accum :+ new ColumnGutter(col, pos, idx))
|
||||
}._3
|
||||
}
|
||||
.split(_.spec.id)( project = (key, _, signal) => key -> signal)
|
||||
|
||||
val rows =
|
||||
Var[Seq[serialized.RowSpec]](initial = Seq(
|
||||
serialized.RowSpec(java.util.UUID.randomUUID(), height = 30),
|
||||
serialized.RowSpec(java.util.UUID.randomUUID(), height = 30),
|
||||
serialized.RowSpec(java.util.UUID.randomUUID(), height = 30),
|
||||
serialized.RowSpec(java.util.UUID.randomUUID(), height = 30),
|
||||
serialized.RowSpec(java.util.UUID.randomUUID(), height = 30),
|
||||
))
|
||||
|
||||
val rowGutters: Signal[Seq[(Identifier, Signal[RowGutter])]] =
|
||||
rows.signal
|
||||
.map {
|
||||
_.foldLeft( (0, 0, Seq[RowGutter]()) ) {
|
||||
case ((pos, idx, accum), col) =>
|
||||
(pos+col.height, idx+1, accum :+ new RowGutter(col, pos, idx))
|
||||
}._3
|
||||
}
|
||||
.split(_.spec.id)( project = (key, _, signal) => key -> signal)
|
||||
|
||||
val cells = Var[Seq[(Identifier, Seq[(Identifier, Cell)])]](initial =
|
||||
rows.now().zipWithIndex.map { case (r, ridx) =>
|
||||
r.id ->
|
||||
columns.now().zipWithIndex.map { case (c, cidx) =>
|
||||
c.id -> new Cell("["+cidx+", "+ridx+"]")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val x = Var(initial = 20)
|
||||
val y = Var(initial = 20)
|
||||
|
||||
val width =
|
||||
columns.signal.map { _.foldLeft(0)( (w, col) => w + col.width) }
|
||||
.observe
|
||||
|
||||
val height =
|
||||
rows.signal.map { _.foldLeft(0)( (h, row) => h + row.height) }
|
||||
.observe
|
||||
|
||||
def root = div(
|
||||
className("dataTable"),
|
||||
|
||||
// Table position and size information (reactive)
|
||||
|
||||
styleAttr <--
|
||||
Signal.combine(x, y, width, height)
|
||||
.map { case (x, y, w, h) =>
|
||||
Seq(
|
||||
s"left: ${x}px;",
|
||||
s"top: ${y}px;",
|
||||
s"width: ${w+Constants.GUTTER_WIDTH}px;",
|
||||
s"height: ${h+Constants.GUTTER_HEIGHT}px;"
|
||||
).mkString(" ")
|
||||
},
|
||||
|
||||
// Initiate a Frame Drag session on click
|
||||
|
||||
onMouseDown --> {
|
||||
(evt) =>
|
||||
DragFrame.start(
|
||||
x.now(), y.now(),
|
||||
width.now()+Constants.GUTTER_WIDTH,
|
||||
height.now()+Constants.GUTTER_HEIGHT,
|
||||
evt
|
||||
) { (x, y) => println(s"Move to : $x, $y")}
|
||||
evt.stopPropagation()
|
||||
},
|
||||
|
||||
// Display column gutters
|
||||
|
||||
div(
|
||||
className("columnGutters"),
|
||||
styleAttr(s"left: ${Constants.GUTTER_WIDTH}px"),
|
||||
children <--
|
||||
columnGutters.map { _.map { case (_, signal) =>
|
||||
div(
|
||||
className("columnGutter"),
|
||||
styleAttr <-- signal.map { case col =>
|
||||
s"width: ${col.spec.width-Constants.BORDER}px; height: ${Constants.GUTTER_HEIGHT-2*Constants.BORDER}px; left: ${col.position}px"
|
||||
},
|
||||
child <-- signal.map { _.label }
|
||||
)
|
||||
}},
|
||||
),
|
||||
|
||||
// Display row gutters
|
||||
|
||||
div(
|
||||
className("rowGutters"),
|
||||
styleAttr(s"top: ${Constants.GUTTER_HEIGHT}px"),
|
||||
children <--
|
||||
rowGutters.map { _.map { case (_, signal) =>
|
||||
div(
|
||||
className("rowGutter"),
|
||||
styleAttr <-- signal.map { case row =>
|
||||
s"width: ${Constants.GUTTER_WIDTH-2*Constants.BORDER}px; height: ${row.spec.height-Constants.BORDER}px; top: ${row.position}px"
|
||||
},
|
||||
div(
|
||||
className("valignHack"),
|
||||
child <-- signal.map { _.label }
|
||||
)
|
||||
)
|
||||
}},
|
||||
),
|
||||
|
||||
// Display the actual content
|
||||
|
||||
div(
|
||||
className("content"),
|
||||
styleAttr <--
|
||||
Signal.combine(width, height)
|
||||
.map { case (w, h) =>
|
||||
Seq(
|
||||
s"width: ${w}px;",
|
||||
s"height: ${h}px;",
|
||||
s"left: ${Constants.GUTTER_WIDTH}px;",
|
||||
s"top: ${Constants.GUTTER_HEIGHT}px",
|
||||
).mkString(" ")
|
||||
},
|
||||
children <--
|
||||
cells.signal.split( key = _._1 ){
|
||||
(rowKey, _, rowCellStream) =>
|
||||
val rowGutterStream: Signal[RowGutter] =
|
||||
rowGutters.flatMap {
|
||||
_.find(_._1 == rowKey).get._2:Signal[RowGutter]
|
||||
}
|
||||
div(
|
||||
className("row"),
|
||||
styleAttr <--
|
||||
rowGutterStream.map { row =>
|
||||
s"height: ${row.spec.height}px; top: ${row.position}px"
|
||||
},
|
||||
children <--
|
||||
rowCellStream.map { _._2 }.split( key = _._1 ) {
|
||||
(colKey, _, cellStream) =>
|
||||
val cellGutterStream: Signal[ColumnGutter] =
|
||||
columnGutters.flatMap {
|
||||
_.find(_._1 == colKey).get._2:Signal[ColumnGutter]
|
||||
}
|
||||
div(
|
||||
className("cell"),
|
||||
styleAttr <--
|
||||
cellGutterStream.map { col =>
|
||||
s"width: ${col.spec.width}px; left: ${col.position}px"
|
||||
},
|
||||
child <--
|
||||
cellStream.map { _._2.tag }
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package net.okennedy.cells.widgets
|
||||
|
||||
import com.raquo.laminar.api.L._
|
||||
import com.raquo.airstream.ownership.OneTimeOwner
|
||||
|
||||
class DragFrame(initX: Int, initY: Int, width: Int, height: Int, cursorX: Double, cursorY: Double, callback: (Int, Int) => Unit)
|
||||
extends Widget
|
||||
{
|
||||
implicit val tableOwner: Owner =
|
||||
new OneTimeOwner( () => println(s"Accessing owner of DragFrame after killed") )
|
||||
|
||||
val curr = Var[(Int, Int)](initial = (initX, initY))
|
||||
|
||||
val root = div(
|
||||
className("dragFrame"),
|
||||
styleAttr <--
|
||||
curr.signal.map { case (x, y) =>
|
||||
s"left: ${x-2}px; top: ${y-2}px; height: ${height-4}px; width: ${width-4}px"
|
||||
},
|
||||
documentEvents.onMouseUp --> {
|
||||
(evt) =>
|
||||
val (x, y) = curr.now()
|
||||
callback( x, y )
|
||||
Widgets.remove(this)
|
||||
},
|
||||
documentEvents.onMouseMove --> {
|
||||
(evt) =>
|
||||
curr.set( (
|
||||
initX + (evt.pageX - cursorX).toInt,
|
||||
initY + (evt.pageY - cursorY).toInt
|
||||
) )
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
object DragFrame
|
||||
{
|
||||
def start(x: Int, y: Int, width: Int, height: Int, evt: org.scalajs.dom.MouseEvent)(callback: (Int, Int) => Unit): Unit =
|
||||
{
|
||||
val frame = new DragFrame(x, y, width, height, evt.pageX, evt.pageY, callback)
|
||||
Widgets.register(frame)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package net.okennedy.cells.widgets
|
||||
|
||||
import com.raquo.laminar.api.L._
|
||||
|
||||
trait Widget
|
||||
{
|
||||
val root: com.raquo.laminar.nodes.ReactiveHtmlElement[org.scalajs.dom.html.Element]
|
||||
}
|
||||
|
||||
object Widgets
|
||||
{
|
||||
val widgets = Var[List[Widget]](initial = Nil)
|
||||
|
||||
val root = div(
|
||||
className("widgets"),
|
||||
children <-- widgets.signal.map { _.map { _.root } }
|
||||
)
|
||||
|
||||
def register(widget: Widget): Unit =
|
||||
{
|
||||
widgets.set(widget :: widgets.now())
|
||||
}
|
||||
|
||||
def remove(widget: Widget): Unit =
|
||||
{
|
||||
|
||||
println(s"Removing: $widget from ${widgets.now()}")
|
||||
widgets.set(widgets.now().filter { _ ne widget })
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue