%% @author Marc Worrell %% @copyright 2009-2021 Marc Worrell %% @doc Show the pager for the search result %% Copyright 2009-2021 Marc Worrell %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -module(scomp_base_pager). -behaviour(zotonic_scomp). -export([vary/2, render/3]). -include_lib("zotonic_core/include/zotonic.hrl"). % Pages before/after the current page -define(DELTA, 2). -define(SLIDE, (?DELTA + ?DELTA + 1)). vary(_Params, _Context) -> nocache. render(Params, _Vars, Context) -> Result = proplists:get_value(result, Params), Dispatch = case proplists:get_value(dispatch, Params) of undefined -> z_context:get(zotonic_dispatch, Context, search); Dp -> Dp end, HideSinglePage = z_convert:to_bool(proplists:get_value(hide_single_page, Params, false)), Template = proplists:get_value(template, Params, "_pager.tpl"), DispatchArgs = lists:foldl( fun(Arg, Acc) -> proplists:delete(Arg, Acc) end, Params, [dispatch, result, hide_single_page, template]), case Result of #search_result{page=Page, pages=Pages} -> Html = build_html(Template, Page, Pages, HideSinglePage, Dispatch, DispatchArgs, Context), {ok, Html}; [ Chunk | _ ] = List when is_list(Chunk) -> % Paginated list with page chunks Page = lookup_arg(page, 1, Params, Context), Pages = length(List), {ok, build_html(Template, Page, Pages, HideSinglePage, Dispatch, DispatchArgs, Context)}; List when is_list(List) -> % Flat list render_list(Template, List, Params, HideSinglePage, Dispatch, DispatchArgs, Context); #rsc_list{list=Ids} -> render_list(Template, Ids, Params, HideSinglePage, Dispatch, DispatchArgs, Context); _ -> {error, <<"scomp_pager: search result is not a #search_result{} or list">>} end. render_list(_Template, [], _Params, _HideSinglePage, _Dispatch, _DispatchArgs, _Context) -> {ok, <<>>}; render_list(Template, List, Params, HideSinglePage, Dispatch, DispatchArgs, Context) -> PageLen = lookup_arg(pagelen, ?SEARCH_PAGELEN, Params, Context), Page = lookup_arg(page, 1, Params, Context), Pages = (length(List) - 1) div PageLen + 1, {ok, build_html(Template, Page, Pages, HideSinglePage, Dispatch, DispatchArgs, Context)}. lookup_arg(Name, Default, Params, Context) -> V = case proplists:get_value(Name, Params) of undefined -> undefined; P -> try z_convert:to_integer(P) catch _:_ -> undefined end end, V1 = case V of undefined -> case z_context:get_q(Name, Context) of undefined -> undefined; Q -> try z_convert:to_integer(Q) catch _:_ -> undefined end end; _ -> V end, case V1 of undefined -> Default; N when N =< 0 -> Default; _ -> V1 end. build_html(_Template, _Page, Pages, true, _Dispatch, _DispatchArgs, _Context) when Pages =< 1 -> <<>>; build_html(Template, Page, Pages, _HideSinglePage, Dispatch, DispatchArgs, Context) -> {S,M,E} = pages(Page, Pages), DispatchQArgs = append_qargs(DispatchArgs, Context), Urls = urls(S, M, E, Dispatch, DispatchQArgs, Context), Props = [ {prev_url, case Page =< 1 of true -> undefined; false -> url_for(Dispatch, [{page,Page-1}|DispatchQArgs], Context) end}, {next_url, case Page >= Pages of true -> undefined; false -> url_for(Dispatch, [{page,Page+1}|DispatchQArgs], Context) end}, {pages, Urls}, {page, Page}, {dispatch, Dispatch} | DispatchArgs ], {Html, _} = z_template:render_to_iolist(Template, Props, Context), Html. url_for(page, Args, Context) -> case proplists:get_value(id, Args) of undefined -> z_dispatcher:url_for(page, Args, Context); Id -> Url = m_rsc:p(Id, page_url, Context), ArgsEncoded = encode_args(proplists:delete(slug, proplists:delete(id, Args))), case binary:match(Url, <<"?">>) of {_,_} -> iolist_to_binary([ Url, $&, ArgsEncoded ]); nomatch -> iolist_to_binary([ Url, $?, ArgsEncoded ]) end end; url_for(Dispatch, Args, Context) -> z_dispatcher:url_for(Dispatch, Args, Context). %% @doc Append all query arguments iff they are not mentioned in the arglist and if qargs parameter is set. append_qargs(Args, Context) -> case proplists:get_value(qargs, Args) of undefined -> Args; false -> proplists:delete(qargs, Args); true -> Args1 = proplists:delete(qargs, Args), merge_qargs(z_context:get_qargs(Context), Args1); L when is_list(L) -> Args1 = proplists:delete(qargs, Args), merge_qargs(L, Args1); M when is_map(M) -> Args1 = proplists:delete(qargs, Args), merge_qargs(maps:to_list(M), Args1) end. merge_qargs([], Args) -> Args; merge_qargs(Qs, Args) -> Ks = [ z_convert:to_binary(A) || {A, _} <- Args ], lists:foldr( fun({A, _} = AV, Acc) -> case lists:member(A, Ks) of true -> Acc; false -> [ AV | Acc ] end end, Args, Qs). encode_args(Args) -> lists:join($&, [ [z_url:url_encode(K), $=, z_url:url_encode(V)] || {K,V} <- Args ]). pages(Page, Pages) -> AtStart = (not (Page == Pages)) and (Page < ?SLIDE), AtEnd = (not AtStart) and (Page > (Pages - (?SLIDE - 1))), Start = case AtStart of true -> % Together "1 .. " seq(1, erlang:min(?SLIDE, Pages - 1)); false -> % Separate "1 ... 3" [1] end, End = case AtEnd of true -> % Together "10 .. 15" seq(erlang:max(2, Pages - ?SLIDE + 1), Pages); false -> [Pages] end, Middle = case (not AtStart) and (not AtEnd) of true -> seq(erlang:max(2, Page - ?DELTA), erlang:min(Pages - 1, Page + ?DELTA)); false -> [] end, {Start, Middle, End}. urls(Start, Middle, End, Dispatch, DispatchArgs, Context) -> UrlStart = [ {N, url_for(Dispatch, [{page,N}|DispatchArgs], Context)} || N <- Start ], UrlMiddle = [ {N, url_for(Dispatch, [{page,N}|DispatchArgs], Context)} || N <- Middle ], UrlEnd = [ {N, url_for(Dispatch, [{page,N}|DispatchArgs], Context)} || N <- End ], {Part1,Next} = case Middle of [] -> {UrlStart, max(Start) + 1}; [N|_] when N == 2 -> % Now Start is always of the format [1] {UrlStart ++ UrlMiddle, lists:max(Middle) + 1}; _ -> {UrlStart ++ [{undefined, sep}|UrlMiddle], lists:max(Middle) + 1} end, case End of [] -> Part1; [M|_] -> if M == Next -> Part1 ++ UrlEnd; true -> Part1 ++ [{undefined, sep}|UrlEnd] end end. max([]) -> 0; max(L) -> lists:max(L). seq(A,B) when B < A -> []; seq(A,B) -> lists:seq(A,B). % test() -> % C = z_context:new(default), % R = #search_result{result=[a], pages=100, page=10}, % {ok, H} = render([{result,R}], [], C), % list_to_binary(H).