Skip to content

Commit 4eaf3a7

Browse files
authored
Add Clip component (#116)
* Add Clip component * Clip hook and browser support * Add clip example description
1 parent b0fddd1 commit 4eaf3a7

File tree

5 files changed

+201
-3
lines changed

5 files changed

+201
-3
lines changed

docs/App.jsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,14 @@ const componentLoaders = [
116116
"Wizard"
117117
].map(fromComponentPath);
118118

119-
const componentv2Loaders = ["Box", "Button", "ButtonGroup", "Link", "Slat"].map(
120-
fromComponentPathv2
121-
);
119+
const componentv2Loaders = [
120+
"Box",
121+
"Button",
122+
"ButtonGroup",
123+
"Clip",
124+
"Link",
125+
"Slat"
126+
].map(fromComponentPathv2);
122127

123128
const App = () => {
124129
const [menuOpen, setMenuOpen] = useState(false);

docs/Examples2/Clip.example.purs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module Lumi.Components2.Examples.Clip where
2+
3+
import Prelude
4+
import Lumi.Components (lumiElement)
5+
import Lumi.Components.Example (example)
6+
import Lumi.Components.Spacing (Space(..), vspace)
7+
import Lumi.Components.Text (body_, p_)
8+
import Lumi.Components2.Box (box)
9+
import Lumi.Components2.Clip (clip)
10+
import React.Basic (JSX)
11+
12+
docs :: JSX
13+
docs =
14+
lumiElement box
15+
_
16+
{ content =
17+
[ p_ "The Clip component wraps the provided content with a grey border and a \"Copy\" button, which copies the text content into the system clipboard."
18+
, p_ "If clipboard access is not allowed or not supported the text will be left highlighted, allowing the user to press ctrl+c manually. Only the plain text content is copied, not the HTML."
19+
, vspace S24
20+
, example
21+
$ lumiElement clip
22+
$ _ { content = [ body_ "[email protected]" ] }
23+
, p_ "The Clip behavior is also available as a React hook."
24+
]
25+
}

src/Lumi/Components2/Clip.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use strict";
2+
3+
const selectNodeContents = node => {
4+
const range = document.createRange();
5+
range.selectNodeContents(node);
6+
var sel = window.getSelection();
7+
sel.removeAllRanges();
8+
sel.addRange(range);
9+
return sel;
10+
};
11+
12+
exports.copyNodeContents = (success, failure, node) => {
13+
try {
14+
const sel = selectNodeContents(node);
15+
if (window.navigator.clipboard != null) {
16+
window.navigator.clipboard
17+
.writeText(sel.toString())
18+
.then(() => {
19+
sel.removeAllRanges();
20+
})
21+
.then(success, failure);
22+
return;
23+
} else {
24+
const copyResult = window.document.execCommand("copy");
25+
if (copyResult) {
26+
sel.removeAllRanges();
27+
return success();
28+
} else {
29+
return failure(new Error("Failed to copy"));
30+
}
31+
}
32+
} catch (e) {
33+
failure(e);
34+
}
35+
};

src/Lumi/Components2/Clip.purs

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
module Lumi.Components2.Clip where
2+
3+
import Prelude
4+
import Data.Foldable (for_)
5+
import Data.Monoid (guard)
6+
import Data.Newtype (class Newtype)
7+
import Data.Nullable (Nullable)
8+
import Data.Nullable as Nullable
9+
import Effect (Effect)
10+
import Effect.Aff (Error, Milliseconds(..), delay, message)
11+
import Effect.Class (liftEffect)
12+
import Effect.Console as Console
13+
import Effect.Uncurried (EffectFn1, EffectFn3, mkEffectFn1, runEffectFn3)
14+
import Effect.Unsafe (unsafePerformEffect)
15+
import Lumi.Components (LumiComponent, lumiComponent, lumiElement)
16+
import Lumi.Components.Spacing (Space(..))
17+
import Lumi.Components2.Box (box)
18+
import Lumi.Components2.Button (_linkStyle, button)
19+
import Lumi.Styles (styleModifier_, toCSS)
20+
import Lumi.Styles.Box (FlexAlign(..), _justify)
21+
import Lumi.Styles.Box as Styles.Box
22+
import Lumi.Styles.Clip as Styles.Clip
23+
import Lumi.Styles.Theme (LumiTheme(..), useTheme)
24+
import React.Basic.DOM as R
25+
import React.Basic.Emotion as E
26+
import React.Basic.Hooks (Hook, JSX, Ref, UseState, coerceHook, readRefMaybe, useRef, useState, (/\), type (/\))
27+
import React.Basic.Hooks as React
28+
import React.Basic.Hooks.Aff (UseAff, useAff)
29+
import React.Basic.Hooks.ResetToken (ResetToken, UseResetToken, useResetToken)
30+
import Web.DOM (Node)
31+
32+
type ClipProps
33+
= ( content :: Array JSX
34+
)
35+
36+
clip :: LumiComponent ClipProps
37+
clip =
38+
unsafePerformEffect do
39+
lumiComponent "Clip" defaults \props -> React.do
40+
theme@(LumiTheme { colors }) <- useTheme
41+
ref <- useRef Nullable.null
42+
{ copied, copy } <- useClip ref
43+
let
44+
copyButton =
45+
lumiElement button
46+
$ _linkStyle
47+
$ styleModifier_
48+
( E.merge
49+
[ E.css
50+
{ marginLeft: E.prop S16
51+
, lineHeight: E.str "12px"
52+
}
53+
, guard copied do
54+
E.css
55+
{ color: E.color colors.black1
56+
, "&:hover": E.nested $ E.css { textDecoration: E.none }
57+
}
58+
]
59+
)
60+
$ _
61+
{ content = [ R.text if copied then "Copied!" else "Copy" ]
62+
, onPress = copy
63+
}
64+
pure
65+
$ E.element R.div'
66+
{ className: props.className
67+
, css: toCSS theme props Styles.Clip.clip
68+
, children:
69+
[ E.element R.div'
70+
{ className: ""
71+
, css:
72+
toCSS theme props
73+
$ Styles.Box.box
74+
>>> Styles.Box._justify Center
75+
, ref
76+
, children: props.content
77+
}
78+
, lumiElement box
79+
$ _justify Center
80+
$ _ { content = [ copyButton ] }
81+
]
82+
}
83+
where
84+
defaults = { content: [] }
85+
86+
newtype UseClip hooks
87+
= UseClip (UseAff (ResetToken /\ Boolean) Unit (UseState Boolean (UseResetToken hooks)))
88+
89+
derive instance ntUseClip :: Newtype (UseClip hooks) _
90+
91+
useClip :: Ref (Nullable Node) -> Hook UseClip { copied :: Boolean, copy :: Effect Unit }
92+
useClip nodeRef =
93+
coerceHook React.do
94+
token /\ resetToken <- useResetToken
95+
copied /\ setCopied <- useState false
96+
let
97+
copy = do
98+
node <- readRefMaybe nodeRef
99+
for_ node
100+
$ runEffectFn3 copyNodeContents
101+
( do
102+
setCopied \_ -> true
103+
resetToken
104+
)
105+
(mkEffectFn1 $ Console.error <<< message)
106+
useAff (token /\ copied) do
107+
when copied do
108+
delay $ Milliseconds 5000.0
109+
liftEffect $ setCopied \_ -> false
110+
pure { copied, copy }
111+
112+
foreign import copyNodeContents :: EffectFn3 (Effect Unit) (EffectFn1 Error Unit) Node Unit

src/Lumi/Styles/Clip.purs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Lumi.Styles.Clip where
2+
3+
import Prelude
4+
import Lumi.Components (PropsModifier)
5+
import Lumi.Styles (styleModifier)
6+
import Lumi.Styles.Border (_round, border)
7+
import Lumi.Styles.Box (FlexAlign(..), _justify, _row)
8+
import Lumi.Styles.Theme (LumiTheme(..))
9+
import React.Basic.Emotion (color, css)
10+
11+
clip :: forall props. PropsModifier props
12+
clip =
13+
border
14+
>>> _round
15+
>>> _row
16+
>>> _justify SpaceBetween
17+
>>> styleModifier \(LumiTheme { colors }) ->
18+
css
19+
{ borderColor: color colors.black5
20+
, backgroundColor: color colors.black5
21+
}

0 commit comments

Comments
 (0)