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