Java tutorial
/** * Copyright 2014 ArcBees Inc. * * 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.arcbees.gquery.elastic.client; import com.arcbees.gquery.elastic.client.MutationObserver.DomMutationCallback; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.event.logical.shared.ResizeEvent; import com.google.gwt.event.logical.shared.ResizeHandler; import com.google.gwt.query.client.Function; import com.google.gwt.query.client.GQuery; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Window; import com.google.web.bindery.event.shared.HandlerRegistration; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.PriorityQueue; import java.util.Queue; import static com.arcbees.gquery.elastic.client.ElasticOption.PlacementStrategy.AVAILABLE_SPACE; import static com.google.gwt.query.client.GQuery.$; import static java.lang.Math.max; import static java.lang.Math.min; public class ElasticImpl { /** * Object where we store some style info in order to avoid browser reflows */ private static class StyleInfo { int span; double marginRight; double marginLeft; double borderTopWidth; double borderBottomWidth; double marginTop; double marginBottom; int height; double width; Integer floatColumn; boolean rowSpanAll; } private class LayoutCommand implements RepeatingCommand { private boolean canceled; @Override public boolean execute() { if (!canceled) { update(false); } return false; } public void cancel() { canceled = true; } } private class ColumnHeightComparator implements Comparator<Integer> { @Override public int compare(Integer col1, Integer col2) { int result = Double.compare(columnHeights.get(col1), columnHeights.get(col2)); if (result == 0) { return Integer.compare(col1, col2); } return result; } } private static final String STYLE_INFO_KEY = "__ELASTIC_STYLE_INFO"; private static final String FIRST = "first"; private static final String LAST = "last"; private static final CssFeatureDetector CSS_FEATURE_DETECTOR = GWT.create(CssFeatureDetector.class); private static final String CSS_TRANSFORM = CSS_FEATURE_DETECTOR.getPrefixedTransform(); private static final String CSS_TRANSLATE_3D = CSS_FEATURE_DETECTOR.getPrefixedTranslate3d(); private static final String CSS_CALC = CSS_FEATURE_DETECTOR.getPrefixedCalc(); private final ElasticOption options; private Element container; private LayoutCommand layoutCommand; // Deque interfaces not supported by gwt private Queue<Integer> columnPriorities; private List<Double> columnHeights; private List<Boolean> ignoredColumn; private boolean useTranslate3d; private boolean useCalc; private double columnWidth; private double containerPaddingBottom; private double containerPaddingTop; private double containerPaddingLeft; private double containerPaddingRight; private HandlerRegistration resizeHandlerRegistration; private MutationObserver mutationObserver; public ElasticImpl(Element container, ElasticOption options) { this.container = container; this.options = options; columnHeights = new ArrayList<Double>(); ignoredColumn = new ArrayList<Boolean>(); if (options.getPlacementStrategy() == AVAILABLE_SPACE) { columnPriorities = new PriorityQueue<Integer>(10, new ColumnHeightComparator()); } else { columnPriorities = new LinkedList<Integer>(); } useTranslate3d = CSS_TRANSLATE_3D != null; useCalc = CSS_CALC != null; init(); } void destroy() { // just unbind event to release resources. I don't think we have to destroy the layout if (resizeHandlerRegistration != null) { resizeHandlerRegistration.removeHandler(); resizeHandlerRegistration = null; } if (mutationObserver != null) { mutationObserver.disconnect(); mutationObserver = null; } else { $(container).off("DOMNodeInserted DOMNodeRemoved"); } } void update(boolean fullUpdate) { int prevColumnNumber = columnHeights.size(); columnHeights.clear(); columnPriorities.clear(); ignoredColumn.clear(); GQuery $container = $(container); // check if children returns text elements GQuery items = $container.children(); containerPaddingLeft = $container.cur("paddingLeft", true); containerPaddingRight = $container.cur("paddingRight", true); containerPaddingTop = $container.cur("paddingTop", true); containerPaddingBottom = $container.cur("paddingBottom", true); double totalColumnWidth = $container.innerWidth() - containerPaddingLeft - containerPaddingRight; int colNumber = calculateNumberOfColumn(totalColumnWidth); columnWidth = (totalColumnWidth - ((colNumber - 1) * options.getInnerColumnMargin())) / colNumber; columnWidth = max(columnWidth, options.getMinimumColumnWidth()); if (options.getMaximumColumnWidth() != -1) { int maxWidth = max(options.getMinimumColumnWidth(), options.getMaximumColumnWidth()); columnWidth = min(columnWidth, maxWidth); } double initialTop = useTranslate3d ? 0 : containerPaddingTop; for (int i = 0; i < colNumber; i++) { columnHeights.add(initialTop); columnPriorities.add(i); ignoredColumn.add(false); } // Use four different loops in order to avoid browser reflows if (fullUpdate) { for (Element e : items.elements()) { initItem(e); } } if (!canUseCalc() || prevColumnNumber != colNumber) { for (Element e : items.elements()) { setItemWidth(e, colNumber); } } for (Element e : items.elements()) { readItemHeight(e); } for (Element e : items.elements()) { placeItem(e, colNumber); } setHeightContainer(); } private boolean canUseCalc() { return useCalc && options.getMaximumColumnWidth() == -1; } private void init() { GQuery $container = $(container); $container.css("minHeight", $container.height() + "px"); if ("static".equals($container.css("position", true))) { $container.css("position", "relative"); } update(true); bind(); } private void setHeightContainer() { double top = useTranslate3d ? containerPaddingTop : 0; double height = top + containerPaddingBottom + getMaxHeight(0, columnHeights.size()); $(container).css("minHeight", height + "px"); } private void readItemHeight(Element e) { StyleInfo si = getStyleInfo(e); si.height = e.getClientHeight(); } private StyleInfo getStyleInfo(Element e) { return $(e).data(STYLE_INFO_KEY, StyleInfo.class); } private void placeItem(Element e, int numberOfCol) { StyleInfo si = getStyleInfo(e); int column; double minHeight; int span = min(si.span, numberOfCol); Integer floatColumn = si.floatColumn; if (floatColumn != null && floatColumn != 0) { floatColumn = floatColumn > 0 ? min(floatColumn, numberOfCol - span) : numberOfCol + floatColumn; } if (span == 1) { if (floatColumn == null) { column = columnPriorities.poll(); } else { column = floatColumn; columnPriorities.remove(column); } minHeight = columnHeights.get(column); } else if (span >= numberOfCol) { // span all column = 0; minHeight = getMaxHeight(column, column + span); columnPriorities.clear(); } else { if (floatColumn != null) { column = floatColumn; minHeight = getMaxHeight(column, column + span); } else { minHeight = Double.MAX_VALUE; column = 0; if (options.getPlacementStrategy() == AVAILABLE_SPACE) { for (int i = 0; i <= columnHeights.size() - span; i++) { double maxHeight = getMaxHeight(i, i + span); if (maxHeight < minHeight) { column = i; minHeight = maxHeight; } } } else { column = columnPriorities.peek(); if (column + span > columnHeights.size()) { // not enough remaining columns, restart from column 0 for (int i = column; i < columnHeights.size(); i++) { columnPriorities.remove(i); columnPriorities.add(i); } column = 0; } minHeight = getMaxHeight(column, column + span); } } for (int i = column; i < column + span; i++) { columnPriorities.remove(i); } } if (useTranslate3d) { String translate3d; if (canUseCalc()) { double weight = (double) column / span; String offset = (100 * weight) + "%" + " + " + ((si.marginLeft + si.marginRight) * weight + options.getInnerColumnMargin() * column) + "px" + " - " + options.getInnerColumnMargin() * (span - 1) * weight + "px"; translate3d = CSS_TRANSLATE_3D + "(" + CSS_CALC + "(" + offset + "), " + minHeight + "px, 0)"; } else { translate3d = CSS_TRANSLATE_3D + "(" + ((columnWidth + options.getInnerColumnMargin()) * column) + "px, " + minHeight + "px, 0)"; } setPrefixedStyle(e, CSS_TRANSFORM, translate3d); } else { double left = (columnWidth + options.getInnerColumnMargin()) * column + containerPaddingLeft; $(e).css("top", minHeight + "px").css("left", left + "px"); } double newHeight = minHeight + si.height + si.borderTopWidth + si.borderBottomWidth + si.marginBottom + si.marginTop + options.getInnerRowMargin(); for (int i = column; i < column + span; i++) { columnHeights.set(i, newHeight); if (si.rowSpanAll) { ignoredColumn.set(i, true); } if (!ignoredColumn.get(i)) { columnPriorities.add(i); } } } private double getMaxHeight(int start, int end) { double maxHeight = columnHeights.get(start); for (int i = start + 1; i < end; i++) { double tmpHeight = columnHeights.get(i); if (tmpHeight > maxHeight) { maxHeight = tmpHeight; } } return maxHeight; } private int calculateNumberOfColumn(double totalColumnWidth) { int innerMargin = options.getInnerColumnMargin(); int columnWidth = options.getMinimumColumnWidth(); int columnNbr = (int) ((totalColumnWidth + innerMargin) / (columnWidth + innerMargin)); return max(options.getMinimalNumberOfColumn(), min(columnNbr, options.getMaximalNumberOfColumn())); } private void setItemWidth(Element e, int nbrOfCol) { StyleInfo si = getStyleInfo(e); int span = min(si.span, nbrOfCol); String width; if (canUseCalc()) { double weight = (double) span / nbrOfCol; si.width = 100 * weight; double innerMarginPart = (double) (options.getInnerColumnMargin() * (nbrOfCol - 1)) * weight; width = CSS_CALC + "(" + si.width + "%" + " - " + (si.marginLeft + si.marginRight + innerMarginPart + (containerPaddingLeft + containerPaddingRight) * weight) + "px" + " + " + options.getInnerColumnMargin() * (span - 1) + "px)"; } else { si.width = columnWidth * span + options.getInnerColumnMargin() * (span - 1) - si.marginLeft - si.marginRight; width = si.width + "px"; } $(e).css("width", width); } private StyleInfo initItem(Element e) { int span = getSpan(e); Integer floatColumn = null; String floatValue = e.getAttribute(Elastic.COLUMN_ATTRIBUTE); if (FIRST.equalsIgnoreCase(floatValue)) { floatColumn = 0; } else if (LAST.equalsIgnoreCase(floatValue)) { floatColumn = -span; } else { try { floatColumn = Integer.parseInt(floatValue) - 1; } catch (NumberFormatException ignored) { } } GQuery $e = $(e); StyleInfo styleInfo = new StyleInfo(); styleInfo.span = getSpan(e); styleInfo.rowSpanAll = "all".equals(e.getAttribute(Elastic.ROW_SPAN_ATTRIBUTE)); styleInfo.floatColumn = floatColumn; styleInfo.marginRight = $e.cur("marginRight", true); styleInfo.marginLeft = $e.cur("marginLeft", true); styleInfo.borderTopWidth = $e.cur("borderTopWidth", true); styleInfo.borderBottomWidth = $e.cur("borderBottomWidth", true); styleInfo.marginTop = $e.cur("marginTop", true); styleInfo.marginBottom = $e.cur("marginBottom", true); $e.data(STYLE_INFO_KEY, styleInfo); $e.css("position", "absolute"); // TODO Ease next width computation but check the impact of this in the content of the item if (GQuery.browser.mozilla) { $e.css("moz-box-sizing", "border-box"); } else { $e.css("box-sizing", "border-box"); } return styleInfo; } private int getSpan(Element element) { String attributeValue = element.getAttribute(Elastic.SPAN_ATTRIBUTE); if (attributeValue != null && !attributeValue.isEmpty()) { if ("all".equals(attributeValue)) { return Integer.MAX_VALUE; } try { return max(1, Integer.parseInt(attributeValue)); } catch (NumberFormatException ignored) { } } return 1; } private void bind() { resizeHandlerRegistration = Window.addResizeHandler(new ResizeHandler() { @Override public void onResize(ResizeEvent event) { if (options.isAutoResize()) { layout(); } } }); if (MutationObserver.isSupported()) { mutationObserver = new MutationObserver(new DomMutationCallback() { @Override public void onNodesRemoved(JsArray<Node> removedNodes) { onItemsRemoved(); } @Override public void onNodesInserted(JsArray<Node> addedNodes, Node nextSibling) { onItemsInserted(toElementList(addedNodes)); } @Override public void onNodesAppended(JsArray<Node> addedNodes) { onItemsAppended(toElementList(addedNodes)); } }); mutationObserver.observe(container); } else { // try old api with DomMutationEvent $(container).on("DOMNodeInserted", new Function() { @Override public boolean f(Event event) { Node node = event.getEventTarget().cast(); if (node.getNodeType() != Node.ELEMENT_NODE || node.getParentElement() != container) { return false; } final Element element = node.cast(); Element prevSibling = element.getPreviousSiblingElement(); Element nextSibling = element.getNextSiblingElement(); if (prevSibling != null && getStyleInfo(prevSibling) != null && (nextSibling == null || getStyleInfo(nextSibling) == null)) { onItemsAppended(new ArrayList<Element>() { { this.add(element); } }); } else { onItemsInserted(new ArrayList<Element>() { { this.add(element); } }); } return false; } }).on("DOMNodeRemoved", new Function() { @Override public boolean f(Event event) { Node node = event.getEventTarget().cast(); if (node.getNodeType() != Node.ELEMENT_NODE || node.getParentElement() != container) { return false; } onItemsRemoved(); return false; } }); } } private void onItemsRemoved() { layout(); } private void onItemsInserted(List<Element> newItems) { // use several loops in order to avoid browsers reflow for (Element e : newItems) { initItem(e); } for (Element e : newItems) { setItemWidth(e, columnHeights.size()); } layout(); } private void onItemsAppended(List<Element> newItems) { // use several loops in order to avoid browsers reflow for (Element e : newItems) { initItem(e); } for (Element e : newItems) { setItemWidth(e, columnHeights.size()); } for (Element e : newItems) { readItemHeight(e); } for (Element e : newItems) { placeItem(e, columnHeights.size()); } setHeightContainer(); } private List<Element> toElementList(JsArray<Node> nodes) { List<Element> elements = new ArrayList<Element>(); for (int i = 0; i < nodes.length(); i++) { Node n = nodes.get(i); if (n.getNodeType() == Node.ELEMENT_NODE) { elements.add(n.<Element>cast()); } } return elements; } private void layout() { if (layoutCommand != null) { layoutCommand.cancel(); } layoutCommand = new LayoutCommand(); Scheduler.get().scheduleFixedPeriod(layoutCommand, 45); } /** * Cannot use <code>$(e).css(styleProperty, styleValue)</code> because GWT throws AssertionError in dev mode if * the <code>styleProperty</code> contains a hyphen. * <p/> * In our case we are sure to use this method on modern browser, so set the style is simple. */ private native void setPrefixedStyle(Element e, String styleProperty, String styleValue) /*-{ e.style[styleProperty] = styleValue; }-*/; }