|
| 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\">", "<<", "</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\">", ">>", "</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 |
0 commit comments