Skip to content

Commit

Permalink
Merge pull request #48 from armanbilge/pr/window
Browse files Browse the repository at this point in the history
`Window` wrapper
  • Loading branch information
armanbilge authored Feb 1, 2023
2 parents b7326d4 + f5c78b3 commit d3a7056
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 69 deletions.
21 changes: 21 additions & 0 deletions dom/src/main/scala-2/fs2/dom/WindowCrossCompat.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2022 Arman Bilge
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fs2.dom

private[dom] trait WindowCrossCompat[F[_]]

private trait WindowImplCrossCompat[F[_]] extends WindowCrossCompat[F]
2 changes: 2 additions & 0 deletions dom/src/main/scala-3/fs2/dom/Dom.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ object Document {
}
}

opaque type HtmlDocument[F[_]] <: Document[F] = dom.HTMLDocument

opaque type Element[F[_]] <: Node[F] = dom.Element
opaque type HtmlElement[F[_]] <: Element[F] = dom.HTMLElement
opaque type HtmlAnchorElement[F[_]] <: HtmlElement[F] = dom.HTMLAnchorElement
Expand Down
38 changes: 38 additions & 0 deletions dom/src/main/scala-3/fs2/dom/WindowCrossCompat.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2022 Arman Bilge
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fs2.dom

import cats.effect.kernel.Async
import org.scalajs.dom

private trait WindowCrossCompat[F[_]] {

implicit def given_Dom_F: Dom[F]

def document: HtmlDocument[F]

}

private trait WindowImplCrossCompat[F[_]](using Async[F]) extends WindowCrossCompat[F] {

private[dom] def window: dom.Window

implicit def given_Dom_F = Dom.forAsync

def document = window.document.asInstanceOf[HtmlDocument[F]]

}
11 changes: 6 additions & 5 deletions dom/src/main/scala/fs2/dom/Clipboard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package fs2.dom

import cats.effect.kernel.Async
import org.scalajs.dom.window
import org.scalajs.dom

abstract class Clipboard[F[_]] private {

Expand All @@ -29,12 +29,13 @@ abstract class Clipboard[F[_]] private {

object Clipboard {

def apply[F[_]](implicit F: Async[F]): Clipboard[F] = new Clipboard[F] {
private[dom] def apply[F[_]](clipboard: dom.Clipboard)(implicit F: Async[F]): Clipboard[F] =
new Clipboard[F] {

def readText = F.fromPromise(F.delay(window.navigator.clipboard.readText()))
def readText = F.fromPromise(F.delay(clipboard.readText()))

def writeText(text: String) = F.fromPromise(F.delay(window.navigator.clipboard.writeText(text)))
def writeText(text: String) = F.fromPromise(F.delay(clipboard.writeText(text)))

}
}

}
30 changes: 16 additions & 14 deletions dom/src/main/scala/fs2/dom/History.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import fs2.concurrent.Signal
import org.scalajs.dom
import org.scalajs.dom.EventListenerOptions
import org.scalajs.dom.ScrollRestoration
import org.scalajs.dom.window

abstract class History[F[_], S] private {

Expand All @@ -48,7 +47,10 @@ abstract class History[F[_], S] private {
}

object History {
def apply[F[_], S](implicit F: Async[F], serializer: Serializer[F, S]): History[F, S] =
private[dom] def apply[F[_], S](window: dom.Window, history: dom.History)(implicit
F: Async[F],
serializer: Serializer[F, S]
): History[F, S] =
new History[F, S] {

def state = new Signal[F, Option[S]] {
Expand All @@ -62,7 +64,7 @@ object History {
(head ++ tail).concurrently(listener)
}

def get = OptionT(F.delay(Option(window.history.state)))
def get = OptionT(F.delay(Option(history.state)))
.semiflatMap(serializer.deserialize(_))
.value

Expand All @@ -71,29 +73,29 @@ object History {

def length = new Signal[F, Int] {
def discrete = state.discrete.evalMap(_ => get)
def get = F.delay(window.history.length)
def get = F.delay(history.length)
def continuous = Stream.repeatEval(get)
}

def scrollRestoration = new WrappedRef(
() => window.history.scrollRestoration,
window.history.scrollRestoration = _
() => history.scrollRestoration,
history.scrollRestoration = _
)

def forward = asyncPopState(window.history.forward())
def back = asyncPopState(window.history.back())
def go = asyncPopState(window.history.go())
def go(delta: Int) = asyncPopState(window.history.go(delta))
def forward = asyncPopState(history.forward())
def back = asyncPopState(history.back())
def go = asyncPopState(history.go())
def go(delta: Int) = asyncPopState(history.go(delta))

def pushState(state: S) =
serializer.serialize(state).flatMap(s => F.delay(window.history.pushState(s, "")))
serializer.serialize(state).flatMap(s => F.delay(history.pushState(s, "")))
def pushState(state: S, url: String) =
serializer.serialize(state).flatMap(s => F.delay(window.history.pushState(s, "", url)))
serializer.serialize(state).flatMap(s => F.delay(history.pushState(s, "", url)))

def replaceState(state: S) =
serializer.serialize(state).flatMap(s => F.delay(window.history.replaceState(s, "")))
serializer.serialize(state).flatMap(s => F.delay(history.replaceState(s, "")))
def replaceState(state: S, url: String) =
serializer.serialize(state).flatMap(s => F.delay(window.history.replaceState(s, "", url)))
serializer.serialize(state).flatMap(s => F.delay(history.replaceState(s, "", url)))

def asyncPopState(thunk: => Unit): F[Unit] = F.async_[Unit] { cb =>
window.addEventListener[dom.PopStateEvent](
Expand Down
5 changes: 1 addition & 4 deletions dom/src/main/scala/fs2/dom/Location.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ abstract class Location[F[_]] private {

object Location {

def apply[F[_]: Sync]: Location[F] =
apply(dom.window.location)

private def apply[F[_]](location: dom.Location)(implicit F: Sync[F]): Location[F] =
private[dom] def apply[F[_]](location: dom.Location)(implicit F: Sync[F]): Location[F] =
new Location[F] {

def href = new WrappedRef(() => location.href, location.href = _)
Expand Down
8 changes: 1 addition & 7 deletions dom/src/main/scala/fs2/dom/LockManager.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ import cats.effect.syntax.all._
import cats.syntax.all._
import fs2.dom.facade.LockRequestOptions
import org.scalajs.dom
import org.scalajs.dom.window

import scala.scalajs.js

abstract class LockManager[F[_]] private {

Expand All @@ -42,10 +39,7 @@ abstract class LockManager[F[_]] private {

object LockManager {

def apply[F[_]: Async]: LockManager[F] =
apply(window.navigator.asInstanceOf[js.Dynamic].locks.asInstanceOf[facade.LockManager])

private def apply[F[_]](manager: facade.LockManager)(implicit F: Async[F]): LockManager[F] =
private[dom] def apply[F[_]](manager: facade.LockManager)(implicit F: Async[F]): LockManager[F] =
new LockManager[F] {

def exclusive(name: String) = request(name, "exclusive", false).void
Expand Down
45 changes: 45 additions & 0 deletions dom/src/main/scala/fs2/dom/Navigator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2022 Arman Bilge
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fs2.dom

import cats.effect.kernel.Async
import org.scalajs.dom

import scala.scalajs.js

abstract class Navigator[F[_]] private {

def clipboard: Clipboard[F]

def locks: LockManager[F]

}

object Navigator {

private[dom] def apply[F[_]](navigator: dom.Navigator)(implicit F: Async[F]): Navigator[F] =
new Navigator[F] {

def clipboard = Clipboard(navigator.clipboard)

def locks = LockManager(
navigator.asInstanceOf[js.Dynamic].locks.asInstanceOf[facade.LockManager]
)

}

}
52 changes: 24 additions & 28 deletions dom/src/main/scala/fs2/dom/Storage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ package fs2
package dom

import cats.syntax.all._
import cats.effect.kernel.Async
import cats.effect.kernel.Sync
import org.scalajs.dom

abstract class Storage[F[_]] private {

def events: Stream[F, Storage.Event]
def events(window: Window[F]): Stream[F, Storage.Event]

def length: F[Int]

Expand All @@ -41,10 +41,6 @@ abstract class Storage[F[_]] private {

object Storage {

def local[F[_]: Async]: Storage[F] = apply(dom.window.localStorage)

def session[F[_]: Async]: Storage[F] = apply(dom.window.sessionStorage)

sealed abstract class Event {
def url: String
}
Expand All @@ -56,9 +52,9 @@ object Storage {
final case class Updated private (key: String, oldValue: String, newValue: String, url: String)
extends Event

private[Storage] def fromStorageEvent(ev: dom.StorageEvent): Event =
Option(ev.key).fold[Event](Cleared(ev.url)) { key =>
(Option(ev.oldValue), Option(ev.newValue)) match {
private[Storage] def fromStorageEvent[F[_]](ev: StorageEvent[F]): Event =
ev.key.fold[Event](Cleared(ev.url)) { key =>
(ev.oldValue, ev.newValue) match {
case (Some(oldValue), None) => Removed(key, oldValue, ev.url)
case (None, Some(newValue)) => Added(key, newValue, ev.url)
case (Some(oldValue), Some(newValue)) => Updated(key, oldValue, newValue, ev.url)
Expand All @@ -67,31 +63,31 @@ object Storage {
}
}

private[dom] def apply[F[_]](storage: dom.Storage)(implicit F: Async[F]): Storage[F] =
new Storage[F] {
private[dom] def apply[F[_]: Sync](storage: dom.Storage): Storage[F] =
new WrappedStorage(storage)

def events =
fs2.dom.events[F, dom.StorageEvent](dom.window, "storage").mapFilter { ev =>
if (ev.storageArea eq storage)
Some(Event.fromStorageEvent(ev))
else
None
}
private final case class WrappedStorage[F[_]](storage: dom.Storage)(implicit F: Sync[F])
extends Storage[F] {

def length = F.delay(storage.length)
def events(window: Window[F]) =
window.storageEvents.mapFilter { ev =>
Option.when(ev.storageArea == this)(Event.fromStorageEvent(ev))
}

def length = F.delay(storage.length)

def getItem(key: String) =
F.delay(Option(storage.getItem(key)))
def getItem(key: String) =
F.delay(Option(storage.getItem(key)))

def setItem(key: String, item: String) =
F.delay(storage.setItem(key, item))
def setItem(key: String, item: String) =
F.delay(storage.setItem(key, item))

def removeItem(key: String) =
F.delay(storage.removeItem(key))
def removeItem(key: String) =
F.delay(storage.removeItem(key))

def key(i: Int) = F.delay(Option(storage.key(i)))
def key(i: Int) = F.delay(Option(storage.key(i)))

def clear = F.delay(storage.clear())
def clear = F.delay(storage.clear())

}
}
}
Loading

0 comments on commit d3a7056

Please sign in to comment.