Skip to content

Commit ab01dff

Browse files
committed
Pulled over from Scrivener pull request.
0 parents  commit ab01dff

File tree

10 files changed

+501
-0
lines changed

10 files changed

+501
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/_build
2+
/cover
3+
/deps
4+
erl_crash.dump
5+
*.ez

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# ScrivenerHtml
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
1. Add scrivener_html to your list of dependencies in mix.exs:
8+
9+
def deps do
10+
[{:scrivener_html, "~> 0.0.1"}]
11+
end
12+
13+
2. Ensure scrivener_html is started before your application:
14+
15+
def application do
16+
[applications: [:scrivener_html]]
17+
end

config/config.exs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for third-
9+
# party users, it should be done in your mix.exs file.
10+
11+
# Sample configuration:
12+
#
13+
# config :logger,
14+
# level: :info
15+
#
16+
# config :logger, :console,
17+
# format: "$date $time [$level] $metadata$message\n",
18+
# metadata: [:user_id]
19+
20+
# It is also possible to import configuration files, relative to this
21+
# directory. For example, you can emulate configuration per environment
22+
# by uncommenting the line below and defining dev.exs, test.exs and such.
23+
# Configuration from the imported file will override the ones defined
24+
# here (which is why it is important to import them last).
25+
#
26+
# import_config "#{Mix.env}.exs"

lib/scrivener/html.ex

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
defmodule Scrivener.HTML do
2+
use Phoenix.HTML
3+
4+
defmodule Default do
5+
@doc """
6+
Default path function when none provided. Used when automatic path function
7+
resolution cannot be performed.
8+
"""
9+
def path(_conn, :index, opts) do
10+
Enum.reduce opts, "?", fn {k, v}, s ->
11+
"#{s}#{if(s == "?", do: "", else: "&")}#{k}=#{v}"
12+
end
13+
end
14+
end
15+
16+
@defaults [view_style: :bootstrap]
17+
@doc """
18+
Generates the HTML pagination links for a given paginator returned by Scrivener.
19+
20+
The default options are:
21+
22+
#{inspect @defaults}
23+
24+
The `view_style` indicates which CSS framework you are using. The default is
25+
`:bootstrap`, but you can add your own using the `Scrivener.HTML.raw_pagination_links/2` function
26+
if desired.
27+
28+
An example of the output data:
29+
30+
iex> Scrivener.HTML.pagination_links(%Scrivener.Page{total_pages: 10, page_number: 5})
31+
{:safe,
32+
["<nav>",
33+
["<ul class=\"pagination\">",
34+
[["<li>", ["<a class=\"\" href=\"?page=4\">", "&lt;&lt;", "</a>"], "</li>"],
35+
["<li>", ["<a class=\"\" href=\"?page=1\">", "1", "</a>"], "</li>"],
36+
["<li>", ["<a class=\"\" href=\"?page=2\">", "2", "</a>"], "</li>"],
37+
["<li>", ["<a class=\"\" href=\"?page=3\">", "3", "</a>"], "</li>"],
38+
["<li>", ["<a class=\"\" href=\"?page=4\">", "4", "</a>"], "</li>"],
39+
["<li>", ["<a class=\"active\" href=\"?page=5\">", "5", "</a>"], "</li>"],
40+
["<li>", ["<a class=\"\" href=\"?page=6\">", "6", "</a>"], "</li>"],
41+
["<li>", ["<a class=\"\" href=\"?page=7\">", "7", "</a>"], "</li>"],
42+
["<li>", ["<a class=\"\" href=\"?page=8\">", "8", "</a>"], "</li>"],
43+
["<li>", ["<a class=\"\" href=\"?page=9\">", "9", "</a>"], "</li>"],
44+
["<li>", ["<a class=\"\" href=\"?page=10\">", "10", "</a>"], "</li>"],
45+
["<li>", ["<a class=\"\" href=\"?page=6\">", "&gt;&gt;", "</a>"], "</li>"]],
46+
"</ul>"], "</nav>"]}
47+
48+
In order to generate links with nested objects (such as a list of comments for a given post)
49+
it is necessary to pass those arguments. All arguments in the `args` parameter will be directly
50+
passed to the path helper function. Everything within `opts` which are not options will passed
51+
as `params` to the path helper function. For example, `@post`, which has an index of paginated
52+
`@comments` would look like the following:
53+
54+
Scrivener.HTML.pagination_links(@conn, @comments, [@post.id], view_style: :bootstrap, my_param: "foo")
55+
56+
You'll need to be sure to configure `:scrivener_html` with the `:routes_helper`
57+
module (ex. MyApp.Routes.Helpers) in Phoenix. With that configured, the above would generate calls
58+
to the `post_comment_path(@conn, :index, @post.id, my_param: "foo", page: page)` for each page link.
59+
60+
In times that it is necessary to override the automatic path function resolution, you may supply the
61+
correct path function to use by adding an extra key in the `opts` parameter of `:path`.
62+
For example:
63+
64+
Scrivener.HTML.pagination_links(@conn, @comments, [@post.id], path: &post_comment_path/4)
65+
66+
Be sure to supply the function which accepts query string parameters (starts at arity 3, +1 for each relation),
67+
because the `page` parameter will always be supplied. If you supply the wrong function you will receive a
68+
function undefined exception.
69+
"""
70+
def pagination_links(conn, paginator, args, opts) do
71+
merged_opts = Dict.merge @defaults,
72+
view_style: opts[:view_style] || Application.get_env(:scrivener_html, :view_style, :bootstrap)
73+
74+
path = opts[:path] || find_path_fn(paginator[:entries], args)
75+
params = Dict.drop opts, (Dict.keys(@defaults) ++ [:path])
76+
77+
# Ensure ordering so pattern matching is reliable
78+
_pagination_links paginator,
79+
view_style: merged_opts[:view_style],
80+
path: path,
81+
args: [conn, :index] ++ args,
82+
params: params
83+
end
84+
def pagination_links(%Scrivener.Page{} = paginator), do: pagination_links(nil, paginator, [], [])
85+
def pagination_links(%Scrivener.Page{} = paginator, opts), do: pagination_links(nil, paginator, [], opts)
86+
def pagination_links(conn, %Scrivener.Page{} = paginator), do: pagination_links(conn, paginator, [], [])
87+
def pagination_links(conn, paginator, [{a, _} | _] = opts), do: pagination_links(conn, paginator, [], opts)
88+
def pagination_links(conn, paginator, [_ | _] = args), do: pagination_links(conn, paginator, args, [])
89+
90+
defp find_path_fn(nil, _path_args), do: &Default.path/3
91+
# Define a different version of `find_path_fn` whenever Phoenix is available.
92+
if Code.ensure_loaded(Phoenix.Naming) do
93+
defp find_path_fn(entries, path_args) do
94+
routes_helper_module = Application.get_env(:scrivener_html, :routes_helper) || raise("Scrivener.HTML: Unable to find configured routes_helper module (ex. MyApp.RoutesHelper)")
95+
path = (path_args ++ [entries |> List.first]) |> Enum.reduce &( :"#{&2}_#{Phoenix.Naming.resource_name(&1[:__struct__])}")
96+
{path_fn, []} = Code.eval_quoted(quote do: &unquote(routes_helper_module).unquote(path)/unquote((path_args |> Enum.count) + 3))
97+
path_fn
98+
end
99+
else
100+
defp find_path_fn(_entries, _args), do: &Default/3
101+
end
102+
103+
# Bootstrap implementation
104+
defp _pagination_links(paginator, [view_style: :bootstrap, path: path, args: args, params: params]) do
105+
# Currently nesting content_tag's is broken...
106+
links = raw_pagination_links(paginator)
107+
|> Enum.map fn ({text, page_number})->
108+
classes = []
109+
if paginator[:page_number] == page_number do
110+
classes = ["active"]
111+
end
112+
params_with_page = Dict.merge(params, page: page_number)
113+
l = link("#{text}", to: apply(path, args ++ [params_with_page]), class: Enum.join(classes, " "))
114+
content_tag(:li, l)
115+
end
116+
ul = content_tag(:ul, links, class: "pagination")
117+
content_tag(:nav, ul)
118+
end
119+
120+
defp _pagination_links(_paginator, [view_style: unknown, path: _path, args: _args, params: _params]) do
121+
raise "Scrivener.HTML: Unable to render view_style #{inspect unknown}"
122+
end
123+
124+
@defaults [distance: 5, next: ">>", previous: "<<", first: true, last: true]
125+
@doc """
126+
Returns the raw data in order to generate the proper HTML for pagination links. Data
127+
is returned in a `{text, page_number}` format where `text` is intended to be the text
128+
of the link and `page_number` is the page it should go to. Defaults are already supplied
129+
and they are as follows:
130+
131+
#{inspect @defaults}
132+
133+
`distance` must be a positive non-zero integer or an exception is raised. `next` and `previous` should be
134+
strings but can be anything you want as long as it is truthy, falsey values will remove
135+
them from the output. `first` and `last` are only booleans, and they just include/remove
136+
their respective link from output. An example of the data returned:
137+
138+
iex> Scrivener.HTML.raw_pagination_links(%{total_pages: 10, page_number: 5})
139+
[{"<<", 4}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7}, {8, 8}, {9, 9}, {10, 10}, {">>", 6}]
140+
141+
Simply loop and pattern match over each item and transform it to your custom HTML.
142+
"""
143+
def raw_pagination_links(paginator, options \\ []) do
144+
options = Dict.merge @defaults, options
145+
page_number_list(paginator[:page_number], paginator[:total_pages], options[:distance])
146+
|> add_first(paginator[:page_number], options[:distance], options[:first])
147+
|> add_previous(paginator[:page_number])
148+
|> add_last(paginator[:page_number], paginator[:total_pages], options[:distance], options[:last])
149+
|> add_next(paginator[:page_number], paginator[:total_pages])
150+
|> Enum.map(fn
151+
:next -> if options[:next], do: {options[:next], paginator[:page_number] + 1}
152+
:previous -> if options[:previous], do: {options[:previous], paginator[:page_number] - 1}
153+
num -> {num, num}
154+
end) |> Enum.filter(&(&1))
155+
end
156+
157+
# Computing page number ranges
158+
defp page_number_list(page, total, distance) when is_integer(distance) and distance >= 1 do
159+
Enum.to_list((page - beginning_distance(page, distance))..(page + end_distance(page, total, distance)))
160+
end
161+
defp page_number_list(_page, _total, _distance) do
162+
raise "Scrivener.HTML: Distance cannot be less than one."
163+
end
164+
165+
# Beginning distance computation
166+
defp beginning_distance(page, distance) when page - distance < 1 do
167+
distance + (page - distance - 1)
168+
end
169+
defp beginning_distance(_page, distance) do
170+
distance
171+
end
172+
173+
# End distance computation
174+
defp end_distance(page, total, distance) when page + distance >= total do
175+
total - page
176+
end
177+
defp end_distance(_page, _total, distance) do
178+
distance
179+
end
180+
181+
# Adding next/prev/first/last links
182+
defp add_previous(list, page) when page != 1 do
183+
[:previous] ++ list
184+
end
185+
defp add_previous(list, _page) do
186+
list
187+
end
188+
189+
defp add_first(list, page, distance, true) when page - distance > 1 do
190+
[1] ++ list
191+
end
192+
defp add_first(list, _page, _distance, _included) do
193+
list
194+
end
195+
196+
defp add_last(list, page, total, distance, true) when page + distance < total do
197+
list ++ [total]
198+
end
199+
defp add_last(list, _page, _total, _distance, _included) do
200+
list
201+
end
202+
203+
defp add_next(list, page, total) when page != total do
204+
list ++ [:next]
205+
end
206+
defp add_next(list, _page, _total) do
207+
list
208+
end
209+
210+
end

mix.exs

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule ScrivenerHtml.Mixfile do
2+
use Mix.Project
3+
4+
def project do
5+
[app: :scrivener_html,
6+
version: "0.0.1",
7+
elixir: "~> 1.1-dev",
8+
elixirc_paths: elixirc_paths(Mix.env),
9+
build_embedded: Mix.env == :prod,
10+
start_permanent: Mix.env == :prod,
11+
description: "HTML helpers for Scrivener",
12+
docs: [
13+
main: Scrivener.HTML,
14+
readme: "README.md"
15+
],
16+
deps: deps]
17+
end
18+
19+
# Configuration for the OTP application
20+
#
21+
# Type `mix help compile.app` for more information
22+
def application do
23+
[
24+
applications: [:logger]
25+
]
26+
end
27+
28+
defp elixirc_paths(:test), do: ["lib", "test/support"]
29+
defp elixirc_paths(_), do: ["lib"]
30+
31+
# Dependencies can be Hex packages:
32+
#
33+
# {:mydep, "~> 0.3.0"}
34+
#
35+
# Or git/path repositories:
36+
#
37+
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
38+
#
39+
# Type `mix help deps` for more examples and options
40+
defp deps do
41+
[
42+
{:scrivener, "~> 0.13.0"},
43+
{:phoenix_html, "~> 1.2.0"},
44+
{:phoenix, "~> 0.16.0", optional: true},
45+
{:pavlov, "~> 0.2.3", only: :test}
46+
]
47+
end
48+
end

mix.lock

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
%{"decimal": {:hex, :decimal, "1.1.0"},
2+
"ecto": {:hex, :ecto, "0.16.0"},
3+
"meck": {:hex, :meck, "0.8.3"},
4+
"pavlov": {:hex, :pavlov, "0.2.3"},
5+
"phoenix": {:hex, :phoenix, "0.16.1"},
6+
"phoenix_html": {:hex, :phoenix_html, "1.2.1"},
7+
"plug": {:hex, :plug, "1.0.0"},
8+
"poison": {:hex, :poison, "1.4.0"},
9+
"poolboy": {:hex, :poolboy, "1.5.1"},
10+
"scrivener": {:hex, :scrivener, "0.13.0"}}

0 commit comments

Comments
 (0)