Refactor the loading and request management (#230)
This commit is contained in:
parent
ecf71f5932
commit
060daed760
154
src/Main.elm
154
src/Main.elm
|
@ -108,50 +108,68 @@ updateWith toPage toMsg model ( subModel, subCmd ) =
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
submitQuery :
|
attemptQuery : ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
|
||||||
Model
|
attemptQuery (( model, _ ) as pair) =
|
||||||
-> ( Model, Cmd Msg )
|
|
||||||
-> ( Model, Cmd Msg )
|
|
||||||
submitQuery old ( new, cmd ) =
|
|
||||||
let
|
let
|
||||||
triggerSearch _ newModel msg makeRequest =
|
-- We intentially throw away Cmd
|
||||||
if
|
-- because we don't want to perform any effects
|
||||||
(newModel.query /= Nothing)
|
-- in this cases where route itself doesn't change
|
||||||
&& (newModel.query /= Just "")
|
noEffects =
|
||||||
&& List.member newModel.channel Search.channels
|
Tuple.mapSecond (always Cmd.none)
|
||||||
then
|
|
||||||
( new
|
submitQuery msg makeRequest searchModel =
|
||||||
, Cmd.batch
|
Tuple.mapSecond
|
||||||
[ cmd
|
(\cmd ->
|
||||||
, makeRequest
|
Cmd.batch
|
||||||
new.elasticsearch
|
[ cmd
|
||||||
newModel.channel
|
, Cmd.map msg <|
|
||||||
(Maybe.withDefault "" newModel.query)
|
makeRequest
|
||||||
newModel.from
|
model.elasticsearch
|
||||||
newModel.size
|
searchModel.channel
|
||||||
newModel.sort
|
(Maybe.withDefault "" searchModel.query)
|
||||||
|> Cmd.map msg
|
searchModel.from
|
||||||
]
|
searchModel.size
|
||||||
|
searchModel.sort
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
pair
|
||||||
|
in
|
||||||
|
case model.page of
|
||||||
|
Packages searchModel ->
|
||||||
|
if Search.shouldLoad searchModel then
|
||||||
|
submitQuery PackagesMsg Page.Packages.makeRequest searchModel
|
||||||
|
|
||||||
else
|
else
|
||||||
( new, cmd )
|
noEffects pair
|
||||||
in
|
|
||||||
case ( old.page, new.page ) of
|
|
||||||
( Packages oldModel, Packages newModel ) ->
|
|
||||||
triggerSearch oldModel newModel PackagesMsg Page.Packages.makeRequest
|
|
||||||
|
|
||||||
( NotFound, Packages newModel ) ->
|
Options searchModel ->
|
||||||
triggerSearch newModel newModel PackagesMsg Page.Packages.makeRequest
|
if Search.shouldLoad searchModel then
|
||||||
|
submitQuery OptionsMsg Page.Options.makeRequest searchModel
|
||||||
|
|
||||||
( Options oldModel, Options newModel ) ->
|
else
|
||||||
triggerSearch oldModel newModel OptionsMsg Page.Options.makeRequest
|
noEffects pair
|
||||||
|
|
||||||
( NotFound, Options newModel ) ->
|
_ ->
|
||||||
triggerSearch newModel newModel OptionsMsg Page.Options.makeRequest
|
pair
|
||||||
|
|
||||||
( _, _ ) ->
|
|
||||||
( new, cmd )
|
pageMatch : Page -> Page -> Bool
|
||||||
|
pageMatch m1 m2 =
|
||||||
|
case ( m1, m2 ) of
|
||||||
|
( NotFound, NotFound ) ->
|
||||||
|
True
|
||||||
|
|
||||||
|
( Home _, Home _ ) ->
|
||||||
|
True
|
||||||
|
|
||||||
|
( Packages _, Packages _ ) ->
|
||||||
|
True
|
||||||
|
|
||||||
|
( Options _, Options _ ) ->
|
||||||
|
True
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
False
|
||||||
|
|
||||||
|
|
||||||
changeRouteTo :
|
changeRouteTo :
|
||||||
|
@ -159,49 +177,6 @@ changeRouteTo :
|
||||||
-> Url.Url
|
-> Url.Url
|
||||||
-> ( Model, Cmd Msg )
|
-> ( Model, Cmd Msg )
|
||||||
changeRouteTo currentModel url =
|
changeRouteTo currentModel url =
|
||||||
let
|
|
||||||
attempteQuery ( newModel, cmd ) =
|
|
||||||
let
|
|
||||||
-- We intentially throw away Cmd
|
|
||||||
-- because we don't want to perform any effects
|
|
||||||
-- in this cases where route itself doesn't change
|
|
||||||
noEffects =
|
|
||||||
( newModel, Cmd.none )
|
|
||||||
in
|
|
||||||
case ( currentModel.route, newModel.route ) of
|
|
||||||
( Route.Packages arg1, Route.Packages arg2 ) ->
|
|
||||||
if
|
|
||||||
(arg1.channel /= arg2.channel)
|
|
||||||
|| (arg1.query /= arg2.query)
|
|
||||||
|| (arg1.from /= arg2.from)
|
|
||||||
|| (arg1.size /= arg2.size)
|
|
||||||
|| (arg1.sort /= arg2.sort)
|
|
||||||
then
|
|
||||||
submitQuery newModel ( newModel, cmd )
|
|
||||||
|
|
||||||
else
|
|
||||||
noEffects
|
|
||||||
|
|
||||||
( Route.Options arg1, Route.Options arg2 ) ->
|
|
||||||
if
|
|
||||||
(arg1.channel /= arg2.channel)
|
|
||||||
|| (arg1.query /= arg2.query)
|
|
||||||
|| (arg1.from /= arg2.from)
|
|
||||||
|| (arg1.size /= arg2.size)
|
|
||||||
|| (arg1.sort /= arg2.sort)
|
|
||||||
then
|
|
||||||
submitQuery newModel ( newModel, cmd )
|
|
||||||
|
|
||||||
else
|
|
||||||
noEffects
|
|
||||||
|
|
||||||
( a, b ) ->
|
|
||||||
if a /= b then
|
|
||||||
submitQuery newModel ( newModel, cmd )
|
|
||||||
|
|
||||||
else
|
|
||||||
noEffects
|
|
||||||
in
|
|
||||||
case Route.fromUrl url of
|
case Route.fromUrl url of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
( { currentModel | page = NotFound }
|
( { currentModel | page = NotFound }
|
||||||
|
@ -212,6 +187,13 @@ changeRouteTo currentModel url =
|
||||||
let
|
let
|
||||||
model =
|
model =
|
||||||
{ currentModel | route = route }
|
{ currentModel | route = route }
|
||||||
|
|
||||||
|
avoidReinit ( newModel, cmd ) =
|
||||||
|
if pageMatch currentModel.page newModel.page then
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
else
|
||||||
|
( newModel, cmd )
|
||||||
in
|
in
|
||||||
case route of
|
case route of
|
||||||
Route.NotFound ->
|
Route.NotFound ->
|
||||||
|
@ -234,7 +216,8 @@ changeRouteTo currentModel url =
|
||||||
in
|
in
|
||||||
Page.Packages.init searchArgs modelPage
|
Page.Packages.init searchArgs modelPage
|
||||||
|> updateWith Packages PackagesMsg model
|
|> updateWith Packages PackagesMsg model
|
||||||
|> attempteQuery
|
|> avoidReinit
|
||||||
|
|> attemptQuery
|
||||||
|
|
||||||
Route.Options searchArgs ->
|
Route.Options searchArgs ->
|
||||||
let
|
let
|
||||||
|
@ -248,7 +231,8 @@ changeRouteTo currentModel url =
|
||||||
in
|
in
|
||||||
Page.Options.init searchArgs modelPage
|
Page.Options.init searchArgs modelPage
|
||||||
|> updateWith Options OptionsMsg model
|
|> updateWith Options OptionsMsg model
|
||||||
|> attempteQuery
|
|> avoidReinit
|
||||||
|
|> attemptQuery
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
@ -263,7 +247,13 @@ update msg model =
|
||||||
|
|
||||||
Browser.External href ->
|
Browser.External href ->
|
||||||
( model
|
( model
|
||||||
, Browser.Navigation.load href
|
, case href of
|
||||||
|
-- ignore links with no `href` attribute
|
||||||
|
"" ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Browser.Navigation.load href
|
||||||
)
|
)
|
||||||
|
|
||||||
( ChangedUrl url, _ ) ->
|
( ChangedUrl url, _ ) ->
|
||||||
|
|
262
src/Search.elm
262
src/Search.elm
|
@ -12,6 +12,7 @@ module Search exposing
|
||||||
, init
|
, init
|
||||||
, makeRequest
|
, makeRequest
|
||||||
, makeRequestBody
|
, makeRequestBody
|
||||||
|
, shouldLoad
|
||||||
, update
|
, update
|
||||||
, view
|
, view
|
||||||
)
|
)
|
||||||
|
@ -148,10 +149,25 @@ init args model =
|
||||||
|> fromSortId
|
|> fromSortId
|
||||||
|> Maybe.withDefault Relevance
|
|> Maybe.withDefault Relevance
|
||||||
}
|
}
|
||||||
|
|> ensureLoading
|
||||||
, Browser.Dom.focus "search-query-input" |> Task.attempt (\_ -> NoOp)
|
, Browser.Dom.focus "search-query-input" |> Task.attempt (\_ -> NoOp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
shouldLoad : Model a -> Bool
|
||||||
|
shouldLoad model =
|
||||||
|
model.result == RemoteData.Loading
|
||||||
|
|
||||||
|
|
||||||
|
ensureLoading : Model a -> Model a
|
||||||
|
ensureLoading model =
|
||||||
|
if model.query /= Nothing && model.query /= Just "" && List.member model.channel channels then
|
||||||
|
{ model | result = RemoteData.Loading }
|
||||||
|
|
||||||
|
else
|
||||||
|
model
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- ---------------------------
|
-- ---------------------------
|
||||||
-- UPDATE
|
-- UPDATE
|
||||||
|
@ -166,6 +182,7 @@ type Msg a
|
||||||
| QueryInputSubmit
|
| QueryInputSubmit
|
||||||
| QueryResponse (RemoteData.WebData (SearchResult a))
|
| QueryResponse (RemoteData.WebData (SearchResult a))
|
||||||
| ShowDetails String
|
| ShowDetails String
|
||||||
|
| ChangePage Int
|
||||||
|
|
||||||
|
|
||||||
update :
|
update :
|
||||||
|
@ -186,19 +203,15 @@ update toRoute navKey msg model =
|
||||||
| sort = fromSortId sortId |> Maybe.withDefault Relevance
|
| sort = fromSortId sortId |> Maybe.withDefault Relevance
|
||||||
, from = 0
|
, from = 0
|
||||||
}
|
}
|
||||||
|
|> ensureLoading
|
||||||
|> pushUrl toRoute navKey
|
|> pushUrl toRoute navKey
|
||||||
|
|
||||||
ChannelChange channel ->
|
ChannelChange channel ->
|
||||||
{ model
|
{ model
|
||||||
| channel = channel
|
| channel = channel
|
||||||
, result =
|
|
||||||
if model.query == Nothing || model.query == Just "" then
|
|
||||||
RemoteData.NotAsked
|
|
||||||
|
|
||||||
else
|
|
||||||
RemoteData.Loading
|
|
||||||
, from = 0
|
, from = 0
|
||||||
}
|
}
|
||||||
|
|> ensureLoading
|
||||||
|> pushUrl toRoute navKey
|
|> pushUrl toRoute navKey
|
||||||
|
|
||||||
QueryInput query ->
|
QueryInput query ->
|
||||||
|
@ -207,10 +220,8 @@ update toRoute navKey msg model =
|
||||||
)
|
)
|
||||||
|
|
||||||
QueryInputSubmit ->
|
QueryInputSubmit ->
|
||||||
{ model
|
{ model | from = 0 }
|
||||||
| result = RemoteData.Loading
|
|> ensureLoading
|
||||||
, from = 0
|
|
||||||
}
|
|
||||||
|> pushUrl toRoute navKey
|
|> pushUrl toRoute navKey
|
||||||
|
|
||||||
QueryResponse result ->
|
QueryResponse result ->
|
||||||
|
@ -229,6 +240,11 @@ update toRoute navKey msg model =
|
||||||
}
|
}
|
||||||
|> pushUrl toRoute navKey
|
|> pushUrl toRoute navKey
|
||||||
|
|
||||||
|
ChangePage from ->
|
||||||
|
{ model | from = from }
|
||||||
|
|> ensureLoading
|
||||||
|
|> pushUrl toRoute navKey
|
||||||
|
|
||||||
|
|
||||||
pushUrl : Route.SearchRoute -> Browser.Navigation.Key -> Model a -> ( Model a, Cmd msg )
|
pushUrl : Route.SearchRoute -> Browser.Navigation.Key -> Model a -> ( Model a, Cmd msg )
|
||||||
pushUrl toRoute navKey model =
|
pushUrl toRoute navKey model =
|
||||||
|
@ -487,20 +503,26 @@ view { toRoute, categoryName } title model viewSuccess outMsg =
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, case model.result of
|
, div [] <|
|
||||||
RemoteData.NotAsked ->
|
case model.result of
|
||||||
div [] [ text "" ]
|
RemoteData.NotAsked ->
|
||||||
|
[ text "" ]
|
||||||
|
|
||||||
RemoteData.Loading ->
|
RemoteData.Loading ->
|
||||||
div [ class "loader" ] [ text "Loading..." ]
|
[ p [] [ em [] [ text "Searching..." ] ]
|
||||||
|
, div []
|
||||||
|
[ viewSortSelection outMsg model
|
||||||
|
, viewPager outMsg model 0 toRoute
|
||||||
|
]
|
||||||
|
, div [ class "loader-wrapper" ] [ div [ class "loader" ] [ text "Loading..." ] ]
|
||||||
|
, viewPager outMsg model 0 toRoute
|
||||||
|
]
|
||||||
|
|
||||||
RemoteData.Success result ->
|
RemoteData.Success result ->
|
||||||
if result.hits.total.value == 0 then
|
if result.hits.total.value == 0 then
|
||||||
div []
|
|
||||||
[ h4 [] [ text <| "No " ++ categoryName ++ " found!" ] ]
|
[ h4 [] [ text <| "No " ++ categoryName ++ " found!" ] ]
|
||||||
|
|
||||||
else
|
else
|
||||||
div []
|
|
||||||
[ p []
|
[ p []
|
||||||
[ em []
|
[ em []
|
||||||
[ text
|
[ text
|
||||||
|
@ -525,138 +547,128 @@ view { toRoute, categoryName } title model viewSuccess outMsg =
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, form [ class "form-horizontal pull-right" ]
|
, div []
|
||||||
[ div
|
[ viewSortSelection outMsg model
|
||||||
[ class "control-group"
|
, viewPager outMsg model result.hits.total.value toRoute
|
||||||
]
|
|
||||||
[ label [ class "control-label" ] [ text "Sort by:" ]
|
|
||||||
, div
|
|
||||||
[ class "controls" ]
|
|
||||||
[ select
|
|
||||||
[ onInput (\x -> outMsg (SortChange x))
|
|
||||||
]
|
|
||||||
(List.map
|
|
||||||
(\sort ->
|
|
||||||
option
|
|
||||||
[ selected (model.sort == sort)
|
|
||||||
, value (toSortId sort)
|
|
||||||
]
|
|
||||||
[ text <| toSortTitle sort ]
|
|
||||||
)
|
|
||||||
sortBy
|
|
||||||
)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
, viewPager outMsg model result toRoute
|
|
||||||
, viewSuccess model.channel model.show result
|
, viewSuccess model.channel model.show result
|
||||||
, viewPager outMsg model result toRoute
|
, viewPager outMsg model result.hits.total.value toRoute
|
||||||
]
|
]
|
||||||
|
|
||||||
RemoteData.Failure error ->
|
RemoteData.Failure error ->
|
||||||
let
|
let
|
||||||
( errorTitle, errorMessage ) =
|
( errorTitle, errorMessage ) =
|
||||||
case error of
|
case error of
|
||||||
Http.BadUrl text ->
|
Http.BadUrl text ->
|
||||||
( "Bad Url!", text )
|
( "Bad Url!", text )
|
||||||
|
|
||||||
Http.Timeout ->
|
Http.Timeout ->
|
||||||
( "Timeout!", "Request to the server timeout." )
|
( "Timeout!", "Request to the server timeout." )
|
||||||
|
|
||||||
Http.NetworkError ->
|
Http.NetworkError ->
|
||||||
( "Network Error!", "A network request bonsaisearch.net domain failed. This is either due to a content blocker or a networking issue." )
|
( "Network Error!", "A network request bonsaisearch.net domain failed. This is either due to a content blocker or a networking issue." )
|
||||||
|
|
||||||
Http.BadStatus code ->
|
Http.BadStatus code ->
|
||||||
( "Bad Status", "Server returned " ++ String.fromInt code )
|
( "Bad Status", "Server returned " ++ String.fromInt code )
|
||||||
|
|
||||||
Http.BadBody text ->
|
Http.BadBody text ->
|
||||||
( "Bad Body", text )
|
( "Bad Body", text )
|
||||||
in
|
in
|
||||||
div [ class "alert alert-error" ]
|
[ div [ class "alert alert-error" ]
|
||||||
[ h4 [] [ text errorTitle ]
|
[ h4 [] [ text errorTitle ]
|
||||||
, text errorMessage
|
, text errorMessage
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewSortSelection : (Msg a -> b) -> Model a -> Html b
|
||||||
|
viewSortSelection outMsg model =
|
||||||
|
form [ class "form-horizontal pull-right sort-form" ]
|
||||||
|
[ div [ class "control-group sort-group" ]
|
||||||
|
[ label [ class "control-label" ] [ text "Sort by:" ]
|
||||||
|
, div
|
||||||
|
[ class "controls" ]
|
||||||
|
[ select
|
||||||
|
[ onInput (\x -> outMsg (SortChange x))
|
||||||
|
]
|
||||||
|
(List.map
|
||||||
|
(\sort ->
|
||||||
|
option
|
||||||
|
[ selected (model.sort == sort)
|
||||||
|
, value (toSortId sort)
|
||||||
|
]
|
||||||
|
[ text <| toSortTitle sort ]
|
||||||
|
)
|
||||||
|
sortBy
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
viewPager :
|
viewPager :
|
||||||
(Msg a -> b)
|
(Msg a -> b)
|
||||||
-> Model a
|
-> Model a
|
||||||
-> SearchResult a
|
-> Int
|
||||||
-> Route.SearchRoute
|
-> Route.SearchRoute
|
||||||
-> Html b
|
-> Html b
|
||||||
viewPager _ model result toRoute =
|
viewPager outMsg model total toRoute =
|
||||||
ul [ class "pager" ]
|
Html.map outMsg <|
|
||||||
[ li
|
ul [ class "pager" ]
|
||||||
[ classList
|
[ li [ classList [ ( "disabled", model.from == 0 ) ] ]
|
||||||
[ ( "disabled", model.from == 0 )
|
[ a
|
||||||
]
|
[ Html.Events.onClick <|
|
||||||
]
|
if model.from == 0 then
|
||||||
[ a
|
NoOp
|
||||||
[ href <|
|
|
||||||
if model.from == 0 then
|
|
||||||
""
|
|
||||||
|
|
||||||
else
|
else
|
||||||
createUrl toRoute { model | from = 0 }
|
ChangePage 0
|
||||||
|
]
|
||||||
|
[ text "First" ]
|
||||||
]
|
]
|
||||||
[ text "First" ]
|
, li [ classList [ ( "disabled", model.from == 0 ) ] ]
|
||||||
]
|
[ a
|
||||||
, li
|
[ Html.Events.onClick <|
|
||||||
[ classList
|
if model.from - model.size < 0 then
|
||||||
[ ( "disabled", model.from == 0 )
|
NoOp
|
||||||
]
|
|
||||||
]
|
|
||||||
[ a
|
|
||||||
[ href <|
|
|
||||||
if model.from - model.size < 0 then
|
|
||||||
""
|
|
||||||
|
|
||||||
else
|
else
|
||||||
createUrl toRoute { model | from = model.from - model.size }
|
ChangePage <| model.from - model.size
|
||||||
|
]
|
||||||
|
[ text "Previous" ]
|
||||||
]
|
]
|
||||||
[ text "Previous" ]
|
, li [ classList [ ( "disabled", model.from + model.size >= total ) ] ]
|
||||||
]
|
[ a
|
||||||
, li
|
[ Html.Events.onClick <|
|
||||||
[ classList
|
if model.from + model.size >= total then
|
||||||
[ ( "disabled", model.from + model.size >= result.hits.total.value )
|
NoOp
|
||||||
]
|
|
||||||
]
|
|
||||||
[ a
|
|
||||||
[ href <|
|
|
||||||
if model.from + model.size >= result.hits.total.value then
|
|
||||||
""
|
|
||||||
|
|
||||||
else
|
else
|
||||||
createUrl toRoute { model | from = model.from + model.size }
|
ChangePage <| model.from + model.size
|
||||||
|
]
|
||||||
|
[ text "Next" ]
|
||||||
]
|
]
|
||||||
[ text "Next" ]
|
, li [ classList [ ( "disabled", model.from + model.size >= total ) ] ]
|
||||||
]
|
[ a
|
||||||
, li
|
[ Html.Events.onClick <|
|
||||||
[ classList
|
if model.from + model.size >= total then
|
||||||
[ ( "disabled", model.from + model.size >= result.hits.total.value )
|
NoOp
|
||||||
]
|
|
||||||
]
|
|
||||||
[ a
|
|
||||||
[ href <|
|
|
||||||
if model.from + model.size >= result.hits.total.value then
|
|
||||||
""
|
|
||||||
|
|
||||||
else
|
else
|
||||||
let
|
let
|
||||||
remainder =
|
remainder =
|
||||||
if remainderBy model.size result.hits.total.value == 0 then
|
if remainderBy model.size total == 0 then
|
||||||
1
|
1
|
||||||
|
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
in
|
in
|
||||||
createUrl toRoute
|
ChangePage <| ((total // model.size) - remainder) * model.size
|
||||||
{ model | from = ((result.hits.total.value // model.size) - remainder) * model.size }
|
]
|
||||||
|
[ text "Last" ]
|
||||||
]
|
]
|
||||||
[ text "Last" ]
|
|
||||||
]
|
]
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
body {
|
body {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
|
@ -89,6 +90,21 @@ header .navbar.navbar-static-top {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sort-form,
|
||||||
|
.sort-form > .sort-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.pager {
|
||||||
|
& > li > a {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-wrapper {
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.loader,
|
.loader,
|
||||||
.loader:before,
|
.loader:before,
|
||||||
.loader:after {
|
.loader:after {
|
||||||
|
|
Loading…
Reference in a new issue