Skip to content

Commit a958b25

Browse files
authored
Embed editor state in URL; remove session storage (#299)
* Embed editor state in URL; remove session storage * Add to CHANGELOG * Add "Share URL" button * Update README
1 parent 25a9095 commit a958b25

9 files changed

+100
-103
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Notable changes to this project are documented in this file. The format is based
77
Breaking changes:
88

99
New features:
10+
- Remove `localStorage` for session storage, persist editor state in URL query param (#299 by @ptrfrncsmrph)
1011

1112
Bugfixes:
1213

README.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
- PureScript syntax highlighting
1212
- Run and print output or show resulting JavaScript
1313
- Multiple view modes: code, output or both
14-
- Persistent session
14+
- Shareable code and editor state via URL
1515
- Load PureScript code from GitHub Gists or repository files
1616

1717
### Control Features via the Query String
@@ -28,6 +28,11 @@ Most of these features can be controlled not only from the toolbar, but also usi
2828
- Example: `gist=37c3c97f47a43f20c548`
2929
- Notes: the file should be named `Main.purs` with the module name `Main`.
3030

31+
- **Load From URL**: Load compressed PureScript code using the `code` parameter
32+
- Managed by Try PureScript and updated on editor state change to create shareable URLs
33+
- Format: `code=<compressed string>`
34+
- Example: `code=LYewJgrgNgpgBAWQIYEsB2cDuALGAnGIA` will set the editor state to the single line `module Main where`
35+
3136
- **View Mode**: Control the view mode using the `view` parameter
3237
- Options are: `code`, `output`, `both` (default)
3338
- Example: `view=output` will only display the output
@@ -40,11 +45,6 @@ Most of these features can be controlled not only from the toolbar, but also usi
4045
- Options are: `true`, `false` (default)
4146
- Example: `js=true` will print JavaScript code instead of the program's output
4247

43-
- **Session**: Load code from a session which is stored with [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) using the `session` parameter
44-
- Usually managed by Try PureScript
45-
- Example: `session=9162f098-070f-4053-60ea-eba47021450d` (Note: will probably not work for you)
46-
- When used with the `gist` or `github` query parameters the code will be loaded from the source file and not the session
47-
4848
### Which Libraries Are Available?
4949

5050
Try PureScript aims to provide a complete, recent package set from <https://github.com/purescript/package-sets>. The available libraries are those listed in [`staging/spago.dhall`](./staging/spago.dhall), at the versions in the package set mentioned in [`staging/packages.dhall`](./staging/packages.dhall).

client/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"dependencies": {
2525
"ace-builds": "^1.5.0",
26-
"jquery": "^1.12.4"
26+
"jquery": "^1.12.4",
27+
"lz-string": "^1.4.4"
2728
}
2829
}

client/src/Try/Container.js

+11
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,14 @@ export function setupIFrame(data, loadCb, failCb) {
5757

5858
return $iframe;
5959
}
60+
61+
export function copyToClipboard(string, copyCb, failCb) {
62+
try {
63+
navigator.clipboard.writeText(string).then(
64+
() => copyCb(),
65+
() => failCb()
66+
);
67+
} catch (_error) {
68+
failCb();
69+
}
70+
}

client/src/Try/Container.purs

+63-25
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import Ace (Annotation)
66
import Control.Monad.Except (runExceptT)
77
import Data.Array as Array
88
import Data.Either (Either(..), either)
9-
import Data.Foldable (for_, oneOf, fold)
9+
import Data.Foldable (for_, oneOf, fold, traverse_)
1010
import Data.FoldableWithIndex (foldMapWithIndex)
1111
import Data.Maybe (Maybe(..), fromMaybe, isNothing)
1212
import Data.String as String
1313
import Data.String (Pattern(..))
1414
import Data.String.Regex as Regex
1515
import Data.String.Regex.Flags as RegexFlags
1616
import Effect (Effect)
17-
import Effect.Aff (Aff, makeAff)
17+
import Effect.Aff (Aff, Milliseconds(..), delay, makeAff)
1818
import Effect.Aff as Aff
1919
import Effect.Class.Console (error)
2020
import Effect.Uncurried (EffectFn3, runEffectFn3)
@@ -30,14 +30,14 @@ import Try.Editor (MarkerType(..), toStringMarkerType)
3030
import Try.Editor as Editor
3131
import Try.Gist (getGistById, tryLoadFileFromGist)
3232
import Try.GitHub (getRawGitHubFile)
33-
import Try.QueryString (getQueryStringMaybe)
34-
import Try.Session (createSessionIdIfNecessary, storeSession, tryRetrieveSession)
33+
import Try.QueryString (compressToEncodedURIComponent, decompressFromEncodedURIComponent, getQueryStringMaybe, setQueryString)
3534
import Try.SharedConfig as SharedConfig
3635
import Type.Proxy (Proxy(..))
3736
import Web.HTML (window)
38-
import Web.HTML.Window (alert)
37+
import Web.HTML.Location (href)
38+
import Web.HTML.Window (alert, location)
3939

40-
type Slots = ( editor :: Editor.Slot Unit )
40+
type Slots = ( editor :: Editor.Slot Unit, shareButton :: forall q o. H.Slot q o Unit )
4141

4242
data SourceFile = GitHub String | Gist String
4343

@@ -76,18 +76,19 @@ parseViewModeParam = case _ of
7676

7777
data Action
7878
= Initialize
79-
| Cache String
79+
| EncodeInURL String
8080
| UpdateSettings (Settings -> Settings)
8181
| Compile (Maybe String)
8282
| HandleEditor Editor.Output
8383

8484
_editor :: Proxy "editor"
8585
_editor = Proxy
8686

87-
type LoadCb = Effect Unit
87+
type SucceedCb = Effect Unit
8888
type FailCb = Effect Unit
89-
foreign import setupIFrame :: EffectFn3 { code :: String } LoadCb FailCb Unit
89+
foreign import setupIFrame :: EffectFn3 { code :: String } SucceedCb FailCb Unit
9090
foreign import teardownIFrame :: Effect Unit
91+
foreign import copyToClipboard :: EffectFn3 String SucceedCb FailCb Unit
9192

9293
component :: forall q i o. H.Component q i o Aff
9394
component = H.mkComponent
@@ -109,8 +110,7 @@ component = H.mkComponent
109110
handleAction :: Action -> H.HalogenM State Action Slots o Aff Unit
110111
handleAction = case _ of
111112
Initialize -> do
112-
sessionId <- H.liftEffect $ createSessionIdIfNecessary
113-
{ code, sourceFile } <- H.liftAff $ withSession sessionId
113+
{ code, sourceFile } <- H.liftAff withSession
114114

115115
-- Load parameters
116116
mbViewModeParam <- H.liftEffect $ getQueryStringMaybe "view"
@@ -140,13 +140,8 @@ component = H.mkComponent
140140
else
141141
handleAction $ Compile Nothing
142142

143-
Cache text -> H.liftEffect do
144-
sessionId <- getQueryStringMaybe "session"
145-
case sessionId of
146-
Just sessionId_ -> do
147-
storeSession sessionId_ { code: text }
148-
Nothing ->
149-
error "No session ID"
143+
EncodeInURL text -> H.liftEffect do
144+
setQueryString "code" $ compressToEncodedURIComponent text
150145

151146
Compile mbCode -> do
152147
H.modify_ _ { compiled = Nothing }
@@ -209,7 +204,7 @@ component = H.mkComponent
209204
H.modify_ _ { compiled = Just res }
210205

211206
HandleEditor (Editor.TextChanged text) -> do
212-
_ <- H.fork $ handleAction $ Cache text
207+
_ <- H.fork $ handleAction $ EncodeInURL text
213208
{ autoCompile } <- H.gets _.settings
214209
when autoCompile $ handleAction $ Compile $ Just text
215210

@@ -340,6 +335,7 @@ component = H.mkComponent
340335
]
341336
[ HH.text "Show JS" ]
342337
]
338+
, HH.slot_ (Proxy :: _ "shareButton") unit shareButton unit
343339
, HH.li
344340
[ HP.class_ $ HH.ClassName "menu-item" ]
345341
[ HH.a
@@ -442,6 +438,48 @@ renderCompilerErrors errors = do
442438
, renderPlaintext message
443439
]
444440

441+
type ShareButtonState =
442+
{ forkId :: Maybe H.ForkId
443+
, showCopySucceeded :: Maybe Boolean
444+
}
445+
446+
shareButton :: forall q i o. H.Component q i o Aff
447+
shareButton = H.mkComponent
448+
{ initialState: \_ -> { forkId: Nothing, showCopySucceeded: Nothing }
449+
, render
450+
, eval: H.mkEval $ H.defaultEval
451+
{ handleAction = handleAction
452+
}
453+
}
454+
where
455+
handleAction :: Unit -> H.HalogenM ShareButtonState Unit () o Aff Unit
456+
handleAction _ = do
457+
H.gets _.forkId >>= traverse_ H.kill
458+
url <- H.liftEffect $ window >>= location >>= href
459+
copySucceeded <- H.liftAff $ makeAff \f -> do
460+
runEffectFn3 copyToClipboard url (f (Right true)) (f (Right false))
461+
mempty
462+
forkId <- H.fork do
463+
H.liftAff $ delay (1_500.0 # Milliseconds)
464+
H.modify_ _ { showCopySucceeded = Nothing }
465+
H.put { showCopySucceeded: Just copySucceeded, forkId: Just forkId }
466+
render :: ShareButtonState -> H.ComponentHTML Unit () Aff
467+
render { showCopySucceeded } = do
468+
let
469+
message = case showCopySucceeded of
470+
Just true -> "️✅ Copied to clipboard"
471+
Just false -> "️❌ Failed to copy"
472+
Nothing -> "Share URL"
473+
HH.li
474+
[ HP.class_ $ HH.ClassName "menu-item no-mobile" ]
475+
[ HH.label
476+
[ HP.id "share_label"
477+
, HP.title "Share URL"
478+
, HE.onClick \_ -> unit
479+
]
480+
[ HH.text message ]
481+
]
482+
445483
menuRadio
446484
:: forall w
447485
. { name :: String
@@ -505,13 +543,13 @@ toAnnotation markerType { position, message } =
505543
, text: message
506544
}
507545

508-
withSession :: String -> Aff { sourceFile :: Maybe SourceFile, code :: String }
509-
withSession sessionId = do
510-
state <- H.liftEffect $ tryRetrieveSession sessionId
511-
githubId <- H.liftEffect $ getQueryStringMaybe "github"
546+
withSession :: Aff { sourceFile :: Maybe SourceFile, code :: String }
547+
withSession = do
548+
state <- H.liftEffect $ getQueryStringMaybe "code"
549+
githubId <- H.liftEffect $ getQueryStringMaybe "github"
512550
gistId <- H.liftEffect $ getQueryStringMaybe "gist"
513-
code <- case state of
514-
Just { code } -> pure code
551+
code <- case state >>= decompressFromEncodedURIComponent of
552+
Just code -> pure code
515553
Nothing -> do
516554
let
517555
action = oneOf

client/src/Try/QueryString.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export {
2+
compressToEncodedURIComponent,
3+
decompressFromEncodedURIComponent as decompressFromEncodedURIComponent_,
4+
} from 'lz-string';
5+
16
export function getQueryString() {
27
return window.location.search;
38
}

client/src/Try/QueryString.purs

+12
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ module Try.QueryString
33
, getQueryStringMaybe
44
, setQueryString
55
, setQueryStrings
6+
, compressToEncodedURIComponent
7+
, decompressFromEncodedURIComponent
68
) where
79

810
import Prelude
911

1012
import Data.Array as Array
1113
import Data.Maybe (Maybe(..))
1214
import Data.Newtype (wrap)
15+
import Data.Nullable (Nullable, toMaybe)
1316
import Data.String as String
1417
import Data.Tuple (Tuple(..))
1518
import Effect (Effect)
@@ -58,3 +61,12 @@ setQueryStrings :: Object.Object String -> Effect Unit
5861
setQueryStrings ss = do
5962
params <- getQueryParams
6063
runEffectFn1 setQueryParameters (Object.union ss params)
64+
65+
-- | Compress a string to a URI-encoded string using LZ-based compression algorithm
66+
foreign import compressToEncodedURIComponent :: String -> String
67+
68+
foreign import decompressFromEncodedURIComponent_ :: String -> Nullable String
69+
70+
-- | Decompress a string from a URI-encoded string using LZ-based compression algorithm
71+
decompressFromEncodedURIComponent :: String -> Maybe String
72+
decompressFromEncodedURIComponent = toMaybe <$> decompressFromEncodedURIComponent_

client/src/Try/Session.js

-16
This file was deleted.

client/src/Try/Session.purs

-55
This file was deleted.

0 commit comments

Comments
 (0)