diff --git a/github-users-web-client/pom.xml b/github-users-web-client/pom.xml index 2913271..72f9802 100644 --- a/github-users-web-client/pom.xml +++ b/github-users-web-client/pom.xml @@ -70,6 +70,16 @@ dagger-gwt + + + com.google.guava + guava + + + com.google.guava + guava-gwt + + com.intendia.gwt.autorest @@ -83,6 +93,18 @@ com.intendia.gwt.rxgwt rxgwt + + + + junit + junit + test + + + org.mockito + mockito-core + test + @@ -119,6 +141,23 @@ gwt-maven-plugin ${gwt.version} + + org.apache.maven.plugins + maven-surefire-plugin + + + + + test + + test + + + diff --git a/github-users-web-client/src/main/java/com/xemantic/ankh/Ankh.gwt.xml b/github-users-web-client/src/main/java/com/xemantic/ankh/web/Ankh.gwt.xml similarity index 92% rename from github-users-web-client/src/main/java/com/xemantic/ankh/Ankh.gwt.xml rename to github-users-web-client/src/main/java/com/xemantic/ankh/web/Ankh.gwt.xml index e70ee79..f263b97 100644 --- a/github-users-web-client/src/main/java/com/xemantic/ankh/Ankh.gwt.xml +++ b/github-users-web-client/src/main/java/com/xemantic/ankh/web/Ankh.gwt.xml @@ -27,6 +27,8 @@ + + diff --git a/github-users-web-client/src/main/java/com/xemantic/ankh/Elements.java b/github-users-web-client/src/main/java/com/xemantic/ankh/web/Elements.java similarity index 98% rename from github-users-web-client/src/main/java/com/xemantic/ankh/Elements.java rename to github-users-web-client/src/main/java/com/xemantic/ankh/web/Elements.java index de09fff..ed61365 100644 --- a/github-users-web-client/src/main/java/com/xemantic/ankh/Elements.java +++ b/github-users-web-client/src/main/java/com/xemantic/ankh/web/Elements.java @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -package com.xemantic.ankh; +package com.xemantic.ankh.web; import com.intendia.rxgwt.elemental2.RxElemental2; import com.xemantic.githubusers.logic.event.Trigger; diff --git a/github-users-web-client/src/main/java/com/xemantic/ankh/web/Images.java b/github-users-web-client/src/main/java/com/xemantic/ankh/web/Images.java new file mode 100644 index 0000000..0a16e98 --- /dev/null +++ b/github-users-web-client/src/main/java/com/xemantic/ankh/web/Images.java @@ -0,0 +1,55 @@ +/* + * github-users-web - lists GitHub users. Minimal app demonstrating + * cross-platform app development (Web, Android, iOS) where core + * logic is shared and transpiled from Java to JavaScript and + * Objective-C. This project delivers Web version. + * + * Copyright (C) 2017 Kazimierz Pogoda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.xemantic.ankh.web; + +import elemental2.dom.Image; +import rx.Single; + +import java.util.Objects; + +/** + * Image utilities. + * + * @author morisil + */ +public final class Images { + + private Images() { /* util class, non-instantiable */ } + + public static Single preload(String url) { + Objects.requireNonNull(url); + return Single.create(subscriber -> { + Image image = new Image(); + image.src = url; + image.onload = p0 -> { + subscriber.onSuccess(image); + return null; + }; + image.onerror = p0 -> { + subscriber.onError(new RuntimeException("Could not load image: " + url)); + return null; + }; + }); + } + +} diff --git a/github-users-web-client/src/main/java/com/xemantic/ankh/IncrementalDom.java b/github-users-web-client/src/main/java/com/xemantic/ankh/web/IncrementalDom.java similarity index 90% rename from github-users-web-client/src/main/java/com/xemantic/ankh/IncrementalDom.java rename to github-users-web-client/src/main/java/com/xemantic/ankh/web/IncrementalDom.java index afa0925..05f1a97 100644 --- a/github-users-web-client/src/main/java/com/xemantic/ankh/IncrementalDom.java +++ b/github-users-web-client/src/main/java/com/xemantic/ankh/web/IncrementalDom.java @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -package com.xemantic.ankh; +package com.xemantic.ankh.web; import elemental2.dom.DocumentFragment; import elemental2.dom.DomGlobal; @@ -47,10 +47,10 @@ public static E create(Patcher patcher) { return (E) fragment.firstChild; } - @JsMethod(namespace = "com.xemantic.ankh.incrementaldom", name = "patch") + @JsMethod(namespace = "com.xemantic.ankh.web.incrementaldom", name = "patch") public static native void patch(Node element, Patcher patcher); - @JsMethod(namespace = "com.xemantic.ankh.incrementaldom", name = "patchOuter") + @JsMethod(namespace = "com.xemantic.ankh.web.incrementaldom", name = "patchOuter") public static native void patchOuter(Node element, Patcher patcher); @JsFunction diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/driver/WebUrlOpener.java b/github-users-web-client/src/main/java/com/xemantic/ankh/web/driver/WebUrlOpener.java similarity index 96% rename from github-users-web-client/src/main/java/com/xemantic/githubusers/web/driver/WebUrlOpener.java rename to github-users-web-client/src/main/java/com/xemantic/ankh/web/driver/WebUrlOpener.java index db5a36e..be56ccb 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/driver/WebUrlOpener.java +++ b/github-users-web-client/src/main/java/com/xemantic/ankh/web/driver/WebUrlOpener.java @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -package com.xemantic.githubusers.web.driver; +package com.xemantic.ankh.web.driver; import com.xemantic.githubusers.logic.driver.UrlOpener; import elemental2.dom.DomGlobal; diff --git a/github-users-web-client/src/main/java/com/xemantic/ankh/web/mdc/MdcElevator.java b/github-users-web-client/src/main/java/com/xemantic/ankh/web/mdc/MdcElevator.java new file mode 100644 index 0000000..322c2b7 --- /dev/null +++ b/github-users-web-client/src/main/java/com/xemantic/ankh/web/mdc/MdcElevator.java @@ -0,0 +1,110 @@ +/* + * github-users-web - lists GitHub users. Minimal app demonstrating + * cross-platform app development (Web, Android, iOS) where core + * logic is shared and transpiled from Java to JavaScript and + * Objective-C. This project delivers Web version. + * + * Copyright (C) 2017 Kazimierz Pogoda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.xemantic.ankh.web.mdc; + +import com.google.common.base.Preconditions; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; +import elemental2.dom.Element; + +import java.util.Objects; + +/** + * Controls {@code mdc-elevation} class. + * + * @author morisil + */ +public class MdcElevator { + + private final static RegExp ELEVATION_CLASS_REG_EXP = RegExp.compile("^mdc-elevation--z([0-9][0-9]?)$"); + + private final Element element; + + private final int initialLevel; + + private int currentLevel; + + public MdcElevator(Element element) { + this.element = Objects.requireNonNull(element); + initialLevel = getLevel(element); + currentLevel = initialLevel; + } + + public static int getLevel(Element element) { + int level = 0; + for (int i = 0, length = element.classList.getLength(); i < length; i++) { + String klass = element.classList.getAt(i); + level += getLevel(klass); + if (level > 0) { + break; + } + } + return level; + } + + public void liftTo(int level) { + Preconditions.checkArgument((level >= 0) && (level <= 24), "level range: 0..24"); + if (currentLevel == level) { + return; + } + if (currentLevel > 0) { + element.classList.remove("mdc-elevation--z" + currentLevel); + } + if (level > 0) { + element.classList.add("mdc-elevation--z" + level); + } + currentLevel = level; + } + + public void liftToInitialLevel() { + liftTo(initialLevel); + } + + private static int getLevel(String klass) { + MatchResult result = ELEVATION_CLASS_REG_EXP.exec(klass); + return result != null + ? Integer.parseInt(result.getGroup(1)) + : 0; + } + + public static OverBuilder whenOver(Element element) { + return new OverBuilder(element); + } + + public static class OverBuilder { + + private final Element element; + + private OverBuilder(Element element) { + this.element = element; + } + + public void liftTo(int level) { + MdcElevator elevator = new MdcElevator(element); + element.addEventListener("mouseenter", e -> elevator.liftTo(level)); + element.addEventListener("mouseleave", e -> elevator.liftToInitialLevel()); + } + + } + +} diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/GitHubUsersWeb.gwt.xml b/github-users-web-client/src/main/java/com/xemantic/githubusers/GitHubUsersWeb.gwt.xml index b5248fb..bfff65a 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/GitHubUsersWeb.gwt.xml +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/GitHubUsersWeb.gwt.xml @@ -29,7 +29,7 @@ - + diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/GitHubUsersModule.java b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/GitHubUsersModule.java index 81600f0..7047b51 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/GitHubUsersModule.java +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/GitHubUsersModule.java @@ -31,7 +31,7 @@ import com.xemantic.githubusers.logic.event.UserSelectedEvent; import com.xemantic.githubusers.logic.service.UserService; import com.xemantic.githubusers.logic.view.*; -import com.xemantic.githubusers.web.driver.WebUrlOpener; +import com.xemantic.ankh.web.driver.WebUrlOpener; import com.xemantic.githubusers.web.error.DefaultErrorAnalyzer; import com.xemantic.githubusers.web.view.*; import com.xemantic.githubusers.web.service.WebUserService; diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebDrawerView.java b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebDrawerView.java index edbefd6..aa941c1 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebDrawerView.java +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebDrawerView.java @@ -22,8 +22,8 @@ package com.xemantic.githubusers.web.view; -import com.xemantic.ankh.Elements; -import com.xemantic.ankh.IncrementalDom; +import com.xemantic.ankh.web.Elements; +import com.xemantic.ankh.web.IncrementalDom; import com.xemantic.githubusers.logic.event.Trigger; import com.xemantic.githubusers.logic.view.DrawerView; import elemental2.dom.Element; diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebScreen.java b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebScreen.java index 28b3f0e..d2d68ec 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebScreen.java +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebScreen.java @@ -22,7 +22,7 @@ package com.xemantic.githubusers.web.view; -import com.xemantic.ankh.IncrementalDom; +import com.xemantic.ankh.web.IncrementalDom; import elemental2.dom.DomGlobal; import elemental2.dom.Element; import elemental2.dom.HTMLInputElement; diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebSnackbarView.java b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebSnackbarView.java index ad72fb1..4710ae0 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebSnackbarView.java +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebSnackbarView.java @@ -22,7 +22,7 @@ package com.xemantic.githubusers.web.view; -import com.xemantic.ankh.IncrementalDom; +import com.xemantic.ankh.web.IncrementalDom; import com.xemantic.githubusers.logic.view.SnackbarView; import elemental2.dom.Element; import mdc.snackbar.MDCSnackbar; diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserListView.java b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserListView.java index 10a70e4..bb8a9a1 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserListView.java +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserListView.java @@ -22,15 +22,14 @@ package com.xemantic.githubusers.web.view; -import com.xemantic.ankh.Elements; -import com.xemantic.ankh.IncrementalDom; +import com.xemantic.ankh.web.Elements; +import com.xemantic.ankh.web.IncrementalDom; import com.xemantic.githubusers.logic.event.Trigger; import com.xemantic.githubusers.logic.view.UserListView; import com.xemantic.githubusers.logic.view.UserView; import elemental2.dom.Element; import elemental2.dom.HTMLButtonElement; import elemental2.dom.HTMLElement; -import mdc.gridList.MDCGridList; import rx.Observable; import javax.inject.Inject; @@ -54,8 +53,6 @@ public class WebUserListView implements UserListView, WebView { public WebUserListView() { element = IncrementalDom.create(Templates::userList); Elements elements = new Elements(element); - /* it will center the grid and make it react to window resizing */ - MDCGridList.attachTo(elements.get(".mdc-grid-list")); userTiles = elements.get(".user-tiles"); loadMoreButton = elements.getButton(".load-more-action"); loadMore$ = Elements.observeClicksOf(loadMoreButton); diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserView.java b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserView.java index e7d8262..99afa2c 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserView.java +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/WebUserView.java @@ -21,12 +21,15 @@ */ package com.xemantic.githubusers.web.view; -import com.xemantic.ankh.Elements; +import com.xemantic.ankh.web.Elements; +import com.xemantic.ankh.web.Images; +import com.xemantic.ankh.web.mdc.MdcElevator; import com.xemantic.githubusers.logic.event.Trigger; import com.xemantic.githubusers.logic.model.User; import com.xemantic.githubusers.logic.view.UserView; -import com.xemantic.ankh.IncrementalDom; +import com.xemantic.ankh.web.IncrementalDom; import elemental2.dom.Element; +import mdc.ripple.MDCRipple; import rx.Observable; /** @@ -43,14 +46,21 @@ public class WebUserView implements UserView, WebView { public WebUserView() { element = IncrementalDom.create(() -> Templates.user(new Templates.UserParams())); userClicks$ = Elements.observeClicksOf(element); + Element userCard = element.querySelector(".user-card"); + MDCRipple.attachTo(userCard); + MdcElevator.whenOver(userCard).liftTo(8); } @Override public void displayUser(User user) { Templates.UserParams params = new Templates.UserParams(); params.login = user.getLogin(); - params.avatarUrl = user.getAvatarUrl() + "&s=200"; // we can rescale server side here - IncrementalDom.patchOuter(element, () -> Templates.user(params)); + patchUser(params); + Images.preload(user.getAvatarUrl()) + .subscribe(image -> { + params.avatarUrl = image.src; + patchUser(params); + }); } @Override @@ -63,4 +73,8 @@ public Element asElement() { return element; } + private void patchUser(Templates.UserParams params) { + IncrementalDom.patchOuter(element, () -> Templates.user(params)); + } + } diff --git a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/templates.soy b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/templates.soy index 5630a69..a7826e4 100644 --- a/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/templates.soy +++ b/github-users-web-client/src/main/java/com/xemantic/githubusers/web/view/templates.soy @@ -51,27 +51,35 @@ {/template} -{template .user} - {@param login: any} - {@param avatarUrl: any} -
  • -
    - -
    -
    - star_border -
    {$login}
    -
    -
  • -{/template} - {template .userList}
    -
    -
      +
      +
      +
      - + +
      +
      +{/template} + +{template .user} + {@param login: any} + {@param avatarUrl: any} +
      +
      +
      +
      + {if $avatarUrl != null} + {$login} avatar + {/if} +
      +
      +
      +

      {$login}

      +
      {/template} diff --git a/github-users-web-client/src/test/java/com/xemantic/ankh/web/ImagesTest.java b/github-users-web-client/src/test/java/com/xemantic/ankh/web/ImagesTest.java new file mode 100644 index 0000000..f476beb --- /dev/null +++ b/github-users-web-client/src/test/java/com/xemantic/ankh/web/ImagesTest.java @@ -0,0 +1,60 @@ +/* + * github-users-web - lists GitHub users. Minimal app demonstrating + * cross-platform app development (Web, Android, iOS) where core + * logic is shared and transpiled from Java to JavaScript and + * Objective-C. This project delivers Web version. + * + * Copyright (C) 2017 Kazimierz Pogoda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.xemantic.ankh.web; + +import elemental2.dom.Image; +import org.hamcrest.CoreMatchers; +import org.junit.Test; +import rx.Single; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +/** + * Test of the {@link Images}. + * + * @author morisil + */ +public class ImagesTest { + + @Test(expected = NullPointerException.class) + public void preload_nullUrl_shouldThrowException() { + // when + Images.preload(null); + + // then should fail + } + + @Test + public void preload_properUrl_shouldReturnSingle() { + // given + String url = "http://foo.com/image.png"; + + // when + Single single = Images.preload(url); + + // then + assertThat(single, notNullValue()); + } + +} diff --git a/github-users-web-client/src/test/java/com/xemantic/ankh/web/mdc/MdcElevatorTest.java b/github-users-web-client/src/test/java/com/xemantic/ankh/web/mdc/MdcElevatorTest.java new file mode 100644 index 0000000..1243c79 --- /dev/null +++ b/github-users-web-client/src/test/java/com/xemantic/ankh/web/mdc/MdcElevatorTest.java @@ -0,0 +1,337 @@ +/* + * github-users-web - lists GitHub users. Minimal app demonstrating + * cross-platform app development (Web, Android, iOS) where core + * logic is shared and transpiled from Java to JavaScript and + * Objective-C. This project delivers Web version. + * + * Copyright (C) 2017 Kazimierz Pogoda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.xemantic.ankh.web.mdc; + +import elemental2.dom.DOMTokenList; +import elemental2.dom.Element; +import elemental2.dom.EventListener; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +/** + * Test of the {@link MdcElevator}. + * + * @author morisil + */ +public class MdcElevatorTest { + + @SuppressWarnings("ConstantConditions") + @Test(expected = NullPointerException.class) + public void new_nullElement_shouldThrowException() { + // given + Element element = null; + + // when + new MdcElevator(element); + + // then exception should be thrown + } + + @Test + public void new_noInteraction_shouldNotChangeCssClasses() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(0); + + // when + new MdcElevator(element); // and no interaction + + // then + verify(element.classList).getLength(); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void getLevel_noCssClasses_shouldReturnLevel0() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(0); + + // when + int level = MdcElevator.getLevel(element); + + // then + verify(element.classList).getLength(); + verifyNoMoreInteractions(element, element.classList); + assertThat(level, is(0)); + } + + @Test + public void getLevel_1NonElevationCssClass_shouldReturnLevel0() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(1); + given(element.classList.getAt(0)).willReturn("foo"); + + // when + int level = MdcElevator.getLevel(element); + + // then + verify(element.classList).getLength(); + verify(element.classList).getAt(0); + verifyNoMoreInteractions(element, element.classList); + assertThat(level, is(0)); + } + + @Test + public void getLevel_1ElevationCssClass_shouldReturnDefinedElevation() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(1); + given(element.classList.getAt(0)).willReturn("mdc-elevation--z12"); + + // when + int level = MdcElevator.getLevel(element); + + // then + verify(element.classList).getLength(); + verify(element.classList).getAt(0); + verifyNoMoreInteractions(element, element.classList); + assertThat(level, is(12)); + } + + @Test + public void liftTo_fromLevel0_shouldAddElevationClassName() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + MdcElevator elevator = new MdcElevator(element); + + // when + elevator.liftTo(8); + + // then + InOrder inOrder = inOrder(element.classList); + inOrder.verify(element.classList).getLength(); + inOrder.verify(element.classList).add("mdc-elevation--z8"); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void liftTo_fromNon0Level_shouldReplaceElevationClassNames() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(1); + given(element.classList.getAt(0)).willReturn("mdc-elevation--z2"); + MdcElevator elevator = new MdcElevator(element); + + // when + elevator.liftTo(8); + + // then + InOrder inOrder = inOrder(element.classList); + inOrder.verify(element.classList).getLength(); + inOrder.verify(element.classList).getAt(0); + inOrder.verify(element.classList).remove("mdc-elevation--z2"); + inOrder.verify(element.classList).add("mdc-elevation--z8"); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void liftTo_sameLevel_shouldDoNothing() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(1); + given(element.classList.getAt(0)).willReturn("mdc-elevation--z2"); + MdcElevator elevator = new MdcElevator(element); + + // when + elevator.liftTo(2); + + // then + verify(element.classList).getLength(); + verify(element.classList).getAt(0); + verifyNoMoreInteractions(element, element.classList); + } + + @Test(expected = IllegalArgumentException.class) + public void liftTo_negativeLevel_shouldThrowException() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(0); + MdcElevator elevator = new MdcElevator(element); + + // when + elevator.liftTo(-1); + + // then exception should be thrown + } + + @Test(expected = IllegalArgumentException.class) + public void liftTo_levelAboveLimit_shouldThrowException() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(0); + MdcElevator elevator = new MdcElevator(element); + + // when + elevator.liftTo(25); + + // then exception should be thrown + } + + @Test + public void liftToInitialLevel_alreadyOnInitialLevel0_shouldNotChangeCssClasses() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(0); + MdcElevator elevator = new MdcElevator(element); + + // when + elevator.liftToInitialLevel(); + + // then + verify(element.classList).getLength(); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void liftToInitialLevel_alreadyOnInitialLevelNon0_shouldNotChangeCssClasses() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(1); + given(element.classList.getAt(0)).willReturn("mdc-elevation--z1"); + MdcElevator elevator = new MdcElevator(element); + + // when + elevator.liftToInitialLevel(); + + // then + verify(element.classList).getLength(); + verify(element.classList).getAt(0); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void liftToInitialLevel_whereInitialLevel0_shouldRemoveElevationCssClass() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(0); + MdcElevator elevator = new MdcElevator(element); + elevator.liftTo(1); + + // when + elevator.liftToInitialLevel(); + + // then + InOrder inOrder = inOrder(element.classList); + inOrder.verify(element.classList).getLength(); + inOrder.verify(element.classList).add("mdc-elevation--z1"); + inOrder.verify(element.classList).remove("mdc-elevation--z1"); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void liftToInitialLevel_whereInitialLevelNon0_shouldReplaceElevationCssClass() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(1); + given(element.classList.getAt(0)).willReturn("mdc-elevation--z1"); + MdcElevator elevator = new MdcElevator(element); + elevator.liftTo(2); + + // when + elevator.liftToInitialLevel(); + + // then + InOrder inOrder = inOrder(element.classList); + inOrder.verify(element.classList).getLength(); + inOrder.verify(element.classList).getAt(0); + inOrder.verify(element.classList).remove("mdc-elevation--z1"); + inOrder.verify(element.classList).add("mdc-elevation--z2"); + inOrder.verify(element.classList).remove("mdc-elevation--z2"); + inOrder.verify(element.classList).add("mdc-elevation--z1"); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void whenOver_noInitialCssElevationSpecified_shouldAddAndRemoveElevation() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(0); + MdcElevator.whenOver(element).liftTo(8); + ArgumentCaptor enterCaptor = ArgumentCaptor.forClass(EventListener.class); + ArgumentCaptor leaveCaptor = ArgumentCaptor.forClass(EventListener.class); + verify(element).addEventListener(eq("mouseenter"), enterCaptor.capture()); + verify(element).addEventListener(eq("mouseleave"), leaveCaptor.capture()); + + // when + enterCaptor.getValue().handleEvent(null); // event payload doesn't matter + leaveCaptor.getValue().handleEvent(null); + + // then + InOrder inOrder = inOrder(element.classList); + inOrder.verify(element.classList).getLength(); + inOrder.verify(element.classList).add("mdc-elevation--z8"); + inOrder.verify(element.classList).remove("mdc-elevation--z8"); + verifyNoMoreInteractions(element, element.classList); + } + + @Test + public void whenOver_initialCssElevationSpecified_shouldReplaceAndRestoreElevation() { + // given + Element element = mock(Element.class); + element.classList = mock(DOMTokenList.class); + given(element.classList.getLength()).willReturn(1); + given(element.classList.getAt(0)).willReturn("mdc-elevation--z2"); + MdcElevator.whenOver(element).liftTo(8); + ArgumentCaptor enterCaptor = ArgumentCaptor.forClass(EventListener.class); + ArgumentCaptor leaveCaptor = ArgumentCaptor.forClass(EventListener.class); + verify(element).addEventListener(eq("mouseenter"), enterCaptor.capture()); + verify(element).addEventListener(eq("mouseleave"), leaveCaptor.capture()); + + // when + enterCaptor.getValue().handleEvent(null); // event payload doesn't matter + leaveCaptor.getValue().handleEvent(null); + + // then + InOrder inOrder = inOrder(element.classList); + inOrder.verify(element.classList).getLength(); + inOrder.verify(element.classList).getAt(0); + inOrder.verify(element.classList).remove("mdc-elevation--z2"); + inOrder.verify(element.classList).add("mdc-elevation--z8"); + inOrder.verify(element.classList).remove("mdc-elevation--z8"); + inOrder.verify(element.classList).add("mdc-elevation--z2"); + verifyNoMoreInteractions(element, element.classList); + } + +} diff --git a/github-users-web-server/src/main/webapp/assets/person.png b/github-users-web-server/src/main/webapp/assets/person.png new file mode 100644 index 0000000..72e8116 Binary files /dev/null and b/github-users-web-server/src/main/webapp/assets/person.png differ diff --git a/github-users-web-server/src/main/webapp/index-dev.html b/github-users-web-server/src/main/webapp/index-dev.html index 74babfc..144891e 100644 --- a/github-users-web-server/src/main/webapp/index-dev.html +++ b/github-users-web-server/src/main/webapp/index-dev.html @@ -43,14 +43,11 @@
      -
      - - diff --git a/github-users-web-server/src/main/webapp/js/deps.js b/github-users-web-server/src/main/webapp/js/deps.js index b8ea12a..107dcf7 100644 --- a/github-users-web-server/src/main/webapp/js/deps.js +++ b/github-users-web-server/src/main/webapp/js/deps.js @@ -27,6 +27,6 @@ goog.addDependency("../../../../js/lib/incemental-dom/incremental-dom-closure.js", ['incrementaldom'], [], true); goog.addDependency("../../../../js/lib/closure-templates/soyutils_idom.js", ['soy.idom'], ['goog.soy.data.SanitizedHtml', 'incrementaldom', 'goog.soy'], true); goog.addDependency("../../../../js/generated/templates.js", ['com.xemantic.githubusers.web.view.template.incrementaldom'], ['incrementaldom', 'soy.idom'], true); -goog.addDependency("../../../../js/modules/com/xemantic/ankh/incrementaldom.js", ['com.xemantic.ankh.incrementaldom'], ['incrementaldom'], true); +goog.addDependency("../../../../js/modules/com/xemantic/ankh/web/incrementaldom.js", ['com.xemantic.ankh.web.incrementaldom'], ['incrementaldom'], true); goog.addDependency("../../../../js/modules/com/xemantic/githubusers/web/view/template.js", ['com.xemantic.githubusers.web.view.template'], ['com.xemantic.githubusers.web.view.template.incrementaldom'], true); -goog.addDependency("../../../../js/modules/com/xemantic/githubusers/web/app.js", ['com.xemantic.githubusers.web.app'], ['com.xemantic.githubusers.web.view.template', 'com.xemantic.ankh.incrementaldom'], true); +goog.addDependency("../../../../js/modules/com/xemantic/githubusers/web/app.js", ['com.xemantic.githubusers.web.app'], ['com.xemantic.githubusers.web.view.template', 'com.xemantic.ankh.web.incrementaldom'], true); diff --git a/github-users-web-server/src/main/webapp/js/modules/com/xemantic/ankh/incrementaldom.js b/github-users-web-server/src/main/webapp/js/modules/com/xemantic/ankh/web/incrementaldom.js similarity index 85% rename from github-users-web-server/src/main/webapp/js/modules/com/xemantic/ankh/incrementaldom.js rename to github-users-web-server/src/main/webapp/js/modules/com/xemantic/ankh/web/incrementaldom.js index 85a03fd..4c1bcbb 100644 --- a/github-users-web-server/src/main/webapp/js/modules/com/xemantic/ankh/incrementaldom.js +++ b/github-users-web-server/src/main/webapp/js/modules/com/xemantic/ankh/web/incrementaldom.js @@ -25,7 +25,7 @@ In the GWT code it is represented as IncrementalDom util class. */ -goog.module("com.xemantic.ankh.incrementaldom"); +goog.module("com.xemantic.ankh.web.incrementaldom"); goog.module.declareLegacyNamespace(); const _mod = goog.require("incrementaldom"); @@ -33,5 +33,5 @@ const _mod = goog.require("incrementaldom"); exports.patch = _mod.patch; exports.patchOuter = _mod.patchOuter; -goog.exportSymbol("com.xemantic.ankh.incrementaldom.patch", _mod.patch); -goog.exportSymbol("com.xemantic.ankh.incrementaldom.patchOuter", _mod.patchOuter); +goog.exportSymbol("com.xemantic.ankh.web.incrementaldom.patch", _mod.patch); +goog.exportSymbol("com.xemantic.ankh.web.incrementaldom.patchOuter", _mod.patchOuter); diff --git a/github-users-web-server/src/main/webapp/js/modules/com/xemantic/githubusers/web/app.js b/github-users-web-server/src/main/webapp/js/modules/com/xemantic/githubusers/web/app.js index a2839c0..40a460c 100644 --- a/github-users-web-server/src/main/webapp/js/modules/com/xemantic/githubusers/web/app.js +++ b/github-users-web-server/src/main/webapp/js/modules/com/xemantic/githubusers/web/app.js @@ -36,7 +36,7 @@ goog.module.declareLegacyNamespace(); This module is used as an entry point in the index-dev.html and when referenced it will cause all the dependant modules to be loaded as well. */ -goog.require("com.xemantic.ankh.incrementaldom"); +goog.require("com.xemantic.ankh.web.incrementaldom"); goog.require("com.xemantic.githubusers.web.view.template"); function start() { diff --git a/github-users-web-server/src/main/webapp/main.css b/github-users-web-server/src/main/webapp/main.css index 93a302a..9a7564c 100644 --- a/github-users-web-server/src/main/webapp/main.css +++ b/github-users-web-server/src/main/webapp/main.css @@ -27,6 +27,17 @@ body { font-family: Roboto, Helvetica, Arial, sans-serif; + background: #e8e8e8; +} + +/* preload images */ +body::after { + position: absolute; + width: 0; + height: 0; + overflow: hidden; + z-index: -1; + content: url("assets/person.png"); } .mdc-toolbar a, .mdc-toolbar a:visited { @@ -64,28 +75,77 @@ body { padding-top: 8px; } -.user-list .user-tiles > li { - cursor: pointer; +.user-list .mdc-layout-grid { + max-width: 1280px; +} + +.user-card { + background: white; } -.user-list .user-tiles .mdc-grid-tile__secondary { - background: rgba(0, 0, 0, 0.3); +.user-card .mdc-card__media { + padding: 0; +} + +.image-placeholder-1x1 img { + /*width: 460px; + height: auto; + display: block;*/ + animation: material-design__image 3s; + animation-fill-mode: forward; +} + +.user-card .mdc-card__primary h1 { + text-align: center; } .user-list .user-list-actions { padding-top: 20px; text-align: center; - margin-bottom: 20px; + margin-bottom: 40px; } .user-list .user-list-actions .load-more-action { margin: auto; } -/* FIXES */ +.image-placeholder-person { + background-image: url("assets/person.png"); + background-size: 100% 100%; +} + +.image-progressive-fade-in { + animation: material-design__image 3s; + animation-fill-mode: forwards; +} + +.image-wrapper-1x1 { + padding-bottom: 100%; +} -/* temporary drawers should cover header, should be fixed in 0.16.0 */ +.image-wrapper-1x1 img { + background: white; + width: 100%; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} -.mdc-temporary-drawer { - z-index: 5; +/* Approximation of Material Design specifications */ +@keyframes material-design__image { + 0% { + opacity: 0; + filter: saturate(20%) brightness(125%); + } + 66.7% { + opacity: 1; + } + 83.3% { + filter: saturate(87%) brightness(100%); + } + 100% { + filter: saturate(100%) brightness(100%); + } } diff --git a/github-users-web-tools/src/main/java/com/xemantic/githubusers/web/tool/PrepareGwtJs.java b/github-users-web-tools/src/main/java/com/xemantic/githubusers/web/tool/PrepareGwtJs.java index 066d539..e40de67 100644 --- a/github-users-web-tools/src/main/java/com/xemantic/githubusers/web/tool/PrepareGwtJs.java +++ b/github-users-web-tools/src/main/java/com/xemantic/githubusers/web/tool/PrepareGwtJs.java @@ -83,7 +83,7 @@ private void writeHeader(BufferedWriter out) throws IOException { out.write(" */\n"); out.write("goog.module('app');\n"); out.write("goog.require('com.xemantic.githubusers.web.app');\n"); - out.write("goog.require('com.xemantic.ankh.incrementaldom');\n\n"); + out.write("goog.require('com.xemantic.ankh.web.incrementaldom');\n\n"); } private boolean shouldSkip(String line) { diff --git a/pom.xml b/pom.xml index 9a11d07..5979cb4 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ 1.0.0-SNAPSHOT 2.8.1 2.11 + 23.0 v8.1.2 @@ -153,6 +154,16 @@ elemental2-dom 1.0.0-beta-1 + + com.google.guava + guava + ${guava.version} + + + com.google.guava + guava-gwt + ${guava.version} + com.google.dagger dagger-gwt @@ -203,6 +214,18 @@ closure-compiler-unshaded v20170626 + + + + junit + junit + 4.12 + + + org.mockito + mockito-core + 2.8.47 +