-
Notifications
You must be signed in to change notification settings - Fork 152
/
link.ex
247 lines (197 loc) · 6.24 KB
/
link.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
defmodule Surface.Components.Link do
@moduledoc """
> #### Deprecation warning {: .warning}
>
> This component has been deprecated in favor of liveview's built-in `<.link>`
> and will be removed in `v0.13`. See https://hexdocs.pm/phoenix_live_view/live-navigation.html for
> more info and usage.
Generates a link to the given URL.
Provides similar capabilities to Phoenix's built-in `link/2` function.
Options `label` and `class` can be set directly and will override anything in `opts`.
All other options are forwarded to the underlying <a> tag.
## Examples
```
<Link
label="user"
to="/users/1"
class="is-danger"
method={:delete}
opts={data: [confirm: "Really?"]}
/>
<Link
to="/users/1"
class="is-link"
>
<span>user</span>
</Link>
```
"""
@moduledoc deprecated: "Use liveview's built-in `<.link>` instead"
use Surface.Component
@valid_uri_schemes [
"http:",
"https:",
"ftp:",
"ftps:",
"mailto:",
"news:",
"irc:",
"gopher:",
"nntp:",
"feed:",
"telnet:",
"mms:",
"rtsp:",
"svn:",
"tel:",
"fax:",
"xmpp:"
]
@doc "The page to link to"
prop to, :any, required: true
@doc "The method to use with the link"
prop method, :atom, default: :get
@doc "Id to apply to the link"
prop id, :string
@doc "Class or classes to apply to the link"
prop class, :css_class
@doc """
The label for the generated `<a>` element, if no content (default slot) is provided.
"""
prop label, :string
@doc """
Additional attributes to add onto the generated element
"""
prop opts, :keyword, default: []
@doc "Triggered when the component receives click"
prop click, :event
@doc "Triggered when a click event happens outside of the element"
prop click_away, :event
# TODO: Remove this when LV min is >= v0.20.15
@doc "Triggered when the component captures click"
prop capture_click, :event
@doc "Triggered when the component loses focus"
prop blur, :event
@doc "Triggered when the component receives focus"
prop focus, :event
@doc "Triggered when the page loses focus"
prop window_blur, :event
@doc "Triggered when the page receives focus"
prop window_focus, :event
@doc "Triggered when a key on the keyboard is pressed"
prop keydown, :event
@doc "Triggered when a key on the keyboard is released"
prop keyup, :event
@doc "Triggered when a key on the keyboard is pressed (window-level)"
prop window_keydown, :event
@doc "Triggered when a key on the keyboard is released (window-level)"
prop window_keyup, :event
@doc "List values that will be sent as part of the payload triggered by an event"
prop values, :keyword, default: []
@doc """
The content of the generated `<a>` element. If no content is provided,
the value of property `label` is used instead.
"""
slot default
if Mix.env() != :test do
@deprecated "Use liveview's built-in `<.link>` instead"
end
def render(assigns) do
unless assigns[:default] || assigns[:label] || Keyword.get(assigns.opts, :label) do
raise ArgumentError, "<Link /> requires a label prop or contents in the default slot"
end
to = valid_destination!(assigns.to, "<Link />")
events = events_to_opts(assigns)
opts = link_method(assigns.method, to, assigns.opts)
assigns = assign(assigns, to: to, opts: events ++ opts)
~F"""
<a id={@id} class={@class} href={@to} :attrs={@opts}><#slot>{@label}</#slot></a>
"""
end
defp link_method(method, to, opts) do
if method == :get do
skip_csrf(opts)
else
{csrf_data, opts} = csrf_data(to, opts)
data =
opts
|> Keyword.get(:data, [])
|> Keyword.merge(csrf_data)
|> Keyword.merge(method: method, to: to)
Keyword.merge(opts, data: data, rel: "nofollow")
end
end
def csrf_data(to, opts) do
case Keyword.pop(opts, :csrf_token, true) do
{csrf, opts} when is_binary(csrf) ->
{[csrf: csrf], opts}
{true, opts} ->
{[csrf: csrf_token(to)], opts}
{false, opts} ->
{[], opts}
end
end
defp csrf_token(to) do
{mod, fun, args} = Application.fetch_env!(:surface, :csrf_token_reader)
apply(mod, fun, [to | args])
end
def valid_destination!(%URI{} = uri, context) do
valid_destination!(URI.to_string(uri), context)
end
def valid_destination!({:safe, to}, context) do
{:safe, valid_string_destination!(IO.iodata_to_binary(to), context)}
end
def valid_destination!({other, to}, _context) when is_atom(other) do
[Atom.to_string(other), ?:, to]
end
def valid_destination!(to, context) do
valid_string_destination!(IO.iodata_to_binary(to), context)
end
for scheme <- @valid_uri_schemes do
def valid_string_destination!(unquote(scheme) <> _ = string, _context), do: string
end
def valid_string_destination!(to, context) do
if not match?("/" <> _, to) and String.contains?(to, ":") do
raise ArgumentError, """
unsupported scheme given to #{context}. In case you want to link to an
unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}
"""
else
to
end
end
def events_to_opts(assigns) do
[
event_to_opts(assigns.capture_click, :"phx-capture-click"),
event_to_opts(assigns.click, :"phx-click"),
event_to_opts(assigns.click_away, :"phx-click-away"),
event_to_opts(assigns.window_focus, :"phx-window-focus"),
event_to_opts(assigns.window_blur, :"phx-window-blur"),
event_to_opts(assigns.focus, :"phx-focus"),
event_to_opts(assigns.blur, :"phx-blur"),
event_to_opts(assigns.window_keyup, :"phx-window-keyup"),
event_to_opts(assigns.window_keydown, :"phx-window-keydown"),
event_to_opts(assigns.keyup, :"phx-keyup"),
event_to_opts(assigns.keydown, :"phx-keydown"),
values_to_opts(assigns.values)
]
|> List.flatten()
end
defp values_to_opts([]) do
[]
end
defp values_to_opts(values) when is_list(values) do
values_to_attrs(values)
end
defp values_to_opts(_values) do
[]
end
defp values_to_attrs(values) when is_list(values) do
for {key, value} <- values do
{:"phx-value-#{key}", value}
end
end
def skip_csrf(opts) do
Keyword.delete(opts, :csrf_token)
end
end