// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.
package com.google.collide.client.ui.slider;
import com.google.collide.client.testing.DebugAttributeSetter;
import com.google.collide.client.util.AnimationUtils;
import com.google.collide.client.util.CssUtils;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.ResizeController;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import elemental.css.CSSStyleDeclaration;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.html.Element;
/**
* Slider UI component.
*/
public class Slider extends UiComponent<Slider.View> {
private static final String SLIDER_MODE = "slidermode";
/**
* Css selectors applied to DOM elements in the slider.
*/
public interface Css extends CssResource {
String sliderRoot();
String sliderLeft();
String sliderSplitter();
String sliderRight();
String sliderFlex();
String paddingForBorderRadius();
}
/**
* Resources used by the Slider.
*
* In order to theme the Slider, you extend this interface and override
* {@link Slider.Resources#sliderCss()}.
*/
public interface Resources extends ClientBundle, ResizeController.Resources {
// Default Stylesheet.
@Source("Slider.css")
Css sliderCss();
}
/**
* Listener interface for being notified about slider events.
*/
public interface Listener {
public void onStateChanged(boolean active);
}
/**
* The view for a Slider.
*/
public static class View extends CompositeView<ViewEvents> {
private static final double DURATION = AnimationUtils.SHORT_TRANSITION_DURATION;
private final Resources resources;
private final Css css;
private final Element sliderLeft;
private final Element sliderSplitter;
private final Element sliderRight;
private final int paddingForBorderRadius;
private boolean animating;
private final EventListener toggleSliderListener = new EventListener() {
@Override
public void handleEvent(Event evt) {
if (!animating) {
animateSlider();
}
}
};
private class SplitterController extends ResizeController {
private boolean active;
private int value;
private int lastDelta;
private SplitterController() {
super(resources, sliderSplitter, new ElementInfo(sliderLeft, ResizeProperty.RIGHT),
new ElementInfo(sliderSplitter, ResizeProperty.RIGHT),
new ElementInfo(sliderRight, ResizeProperty.RIGHT));
setNegativeDelta(true);
showResizingCursor(false);
}
@Override
protected boolean canStartResizing() {
return !animating;
}
@Override
protected void resizeStarted() {
animating = true;
active = CssUtils.isVisible(sliderLeft);
value = prepareForAnimation(active);
showResizingCursor(true);
setRestrictions();
super.resizeStarted();
}
@Override
protected void resizeEnded() {
super.resizeEnded();
if (lastDelta > 0) {
active = true;
} else if (lastDelta < 0) {
active = false;
}
runPreparedAnimation(active, value);
showResizingCursor(false);
lastDelta = 0;
}
@Override
protected void applyDelta(int delta) {
if (delta != 0) {
lastDelta = delta;
}
super.applyDelta(delta);
}
private void setRestrictions() {
for (ElementInfo elementInfo : getElementInfos()) {
elementInfo.setPropertyMinValue(0);
elementInfo.setPropertyMaxValue(value - paddingForBorderRadius);
}
}
private void showResizingCursor(boolean show) {
CssUtils.setClassNameEnabled(sliderSplitter, getCss().hSplitter(), show);
}
}
public View(Resources resources) {
this.resources = resources;
css = resources.sliderCss();
sliderLeft = Elements.createDivElement(css.sliderLeft());
sliderSplitter = Elements.createDivElement(css.sliderSplitter());
sliderRight = Elements.createDivElement(css.sliderRight());
paddingForBorderRadius = CssUtils.parsePixels(css.paddingForBorderRadius());
Element rootElement = Elements.createDivElement(css.sliderRoot());
rootElement.appendChild(sliderLeft);
rootElement.appendChild(sliderSplitter);
rootElement.appendChild(sliderRight);
setElement(rootElement);
rootElement.addEventListener(Event.CLICK, toggleSliderListener, false);
new SplitterController().start();
}
private void setActive(boolean active) {
CssUtils.setDisplayVisibility(sliderLeft, active);
CssUtils.setDisplayVisibility(sliderRight, !active);
sliderLeft.getStyle().removeProperty("width");
sliderRight.getStyle().removeProperty("width");
sliderLeft.getStyle().removeProperty("right");
sliderSplitter.getStyle().removeProperty("right");
sliderRight.getStyle().removeProperty("right");
CssUtils.setClassNameEnabled(sliderLeft, css.sliderFlex(), active);
CssUtils.setClassNameEnabled(sliderRight, css.sliderFlex(), !active);
new DebugAttributeSetter().add(SLIDER_MODE, String.valueOf(active)).on(getElement());
}
private void animateSlider() {
final boolean active = CssUtils.isVisible(sliderLeft);
final int value = prepareForAnimation(active);
// Need to defer the animation, until both elements are visible.
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
runPreparedAnimation(active, value);
}
});
animating = true;
}
private int prepareForAnimation(boolean active) {
Element visibleButton = active ? sliderLeft : sliderRight;
int value = visibleButton.getOffsetWidth();
String rightStart = (active ? 0 : value - paddingForBorderRadius)
+ CSSStyleDeclaration.Unit.PX;
sliderLeft.getStyle().setWidth(value + CSSStyleDeclaration.Unit.PX);
sliderRight.getStyle().setWidth(value + CSSStyleDeclaration.Unit.PX);
sliderLeft.removeClassName(css.sliderFlex());
sliderRight.removeClassName(css.sliderFlex());
CssUtils.setDisplayVisibility(sliderLeft, true);
CssUtils.setDisplayVisibility(sliderRight, true);
sliderLeft.getStyle().setRight(rightStart);
sliderSplitter.getStyle().setRight(rightStart);
sliderRight.getStyle().setRight(rightStart);
return value;
}
private void runPreparedAnimation(final boolean active, int value) {
String rightEnd = (active ? value - paddingForBorderRadius : 0) + CSSStyleDeclaration.Unit.PX;
if (value <= 0 || rightEnd.equals(sliderRight.getStyle().getRight())) {
setActive(!active);
getDelegate().onStateChanged(!active);
// We should be "animating" until the event queue is processed, so that,
// for example, a mouse CLICK event should be skipped after a resizing.
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
animating = false;
}
});
return;
}
AnimationUtils.animatePropertySet(sliderLeft, "right", rightEnd, DURATION);
AnimationUtils.animatePropertySet(sliderSplitter, "right", rightEnd, DURATION);
AnimationUtils.animatePropertySet(sliderRight, "right", rightEnd, DURATION,
new EventListener() {
@Override
public void handleEvent(Event evt) {
setActive(!active);
getDelegate().onStateChanged(!active);
animating = false;
}
});
// In case the previous commands call an old event listener that would
// set the animating flag to false.
animating = true;
}
private void setSliderStrings(String activatedSlider, String deactivatedSlider) {
sliderLeft.setTextContent(activatedSlider);
sliderRight.setTextContent(deactivatedSlider);
}
}
private interface ViewEvents {
void onStateChanged(boolean active);
}
/**
* Creates an instance of the Slider with its default View.
*
* @return a {@link Slider} instance with default View
*/
public static Slider create(View view) {
return new Slider(view);
}
private Listener listener;
private Slider(View view) {
super(view);
getView().setDelegate(new ViewEvents() {
@Override
public void onStateChanged(boolean active) {
if (Slider.this.listener != null) {
Slider.this.listener.onStateChanged(active);
}
}
});
setActive(true);
}
public void setListener(Listener listener) {
this.listener = listener;
}
public void setActive(boolean active) {
getView().setActive(active);
}
/**
* Sets UI strings to display on the slider when it is activated and
* deactivated correspondingly.
*/
public void setSliderStrings(String activatedSlider, String deactivatedSlider) {
getView().setSliderStrings(activatedSlider, deactivatedSlider);
}
}