/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.api.editor.annotation;
import elemental.css.CSSStyleDeclaration;
import elemental.dom.Element;
import elemental.dom.Node;
import elemental.html.DivElement;
import elemental.html.HTMLCollection;
import elemental.util.Mappable;
import org.eclipse.che.ide.api.editor.text.annotation.Annotation;
import org.eclipse.che.ide.util.dom.Elements;
import java.util.ArrayList;
import java.util.List;
import static elemental.css.CSSStyleDeclaration.Position.ABSOLUTE;
import static elemental.css.CSSStyleDeclaration.Position.STATIC;
/**
* A composite of one or more annotation displays.<br>
* All annotation are positioned at the same place and the one that is visible is determined by their relative "layer" (
* {@link Annotation#getLayer()}).<br>
* All annotation texts ({@link Annotation#getText()}) are aggregated and shown in the annotation tooltip.
*/
class AnnotationGroupImpl implements AnnotationGroup {
private static final String ELEMENT_ROLE_VALUE_ANNOTATION = "annotation";
private static final String ELEMENT_ROLE_DATA_PROPERTY = "eltRole";
/*
* This implementation - relies on z-index to display the higher priority annotation - uses data-* attribute (dataset) to store the
* tooltips
*/
private static final String MESSAGE_DATASET_NAME = "annotationMessage";
private static final String TYPE_DATASET_NAME = "annotationType";
private static final String LAYER_DATASET_NAME = "annotationLayer";
private static final String OFFSET_DATASET_NAME = "annotationOffset";
private elemental.dom.Element mainElement;
public final static AnnotationGroupImpl create() {
final AnnotationGroupImpl result = new AnnotationGroupImpl();
final DivElement element = Elements.createDivElement();
element.getStyle().setPosition(STATIC);
result.mainElement = element;
element.getDataset().setAt(ELEMENT_ROLE_DATA_PROPERTY, ELEMENT_ROLE_VALUE_ANNOTATION);
return result;
}
public final static AnnotationGroupImpl create(final Element existingElement) {
final AnnotationGroupImpl result = new AnnotationGroupImpl();
result.mainElement = existingElement;
return result;
}
public elemental.dom.Element asElemental() {
return this.mainElement;
}
@Override
public final void addAnnotation(final Annotation annotation, int offset) {
asElemental().appendChild(buildIncludedElement(annotation, offset));
updateIconVisibility();
}
@Override
public final void removeAnnotation(final Annotation annotation, int offset) {
final HTMLCollection children = asElemental().getChildren();
for (int i = 0; i < children.length(); i++) {
final Node child = (Node)children.at(i);
if (child instanceof elemental.dom.Element) {
final elemental.dom.Element element = (elemental.dom.Element)child;
final Mappable dataset = element.getDataset();
if (compareStrings(getMessage(dataset), annotation.getText())
&& getOffset(dataset) == offset
&& getLayer(dataset) == annotation.getLayer()
&& compareStrings(getType(dataset), annotation.getType())) {
// we may not strictly be on the same annotation instance, but it is not discernible
asElemental().removeChild(element);
updateIconVisibility();
break;
}
}
}
}
private elemental.dom.Element buildIncludedElement(Annotation annotation, int offset) {
final elemental.dom.Element element = annotation.getImageElement();
final CSSStyleDeclaration style = element.getStyle();
int layer = annotation.getLayer();
style.setZIndex(layer);
style.setPosition(ABSOLUTE);
style.setTop("0");
style.setLeft("0");
style.setRight("0");
style.setBottom("0");
element.getDataset().setAt(MESSAGE_DATASET_NAME, annotation.getText());
element.getDataset().setAt(TYPE_DATASET_NAME, annotation.getType());
element.getDataset().setAt(LAYER_DATASET_NAME, Integer.toString(layer));
element.getDataset().setAt(OFFSET_DATASET_NAME, Integer.toString(offset));
return element;
}
private void updateIconVisibility() {
int maxLayer = 0;
final HTMLCollection children = asElemental().getChildren();
for (int i = 0; i < children.length(); i++) {
final Node child = (Node)children.at(i);
if (child instanceof elemental.dom.Element) {
final elemental.dom.Element element = (elemental.dom.Element)child;
final Mappable dataset = element.getDataset();
final int layer = getLayer(dataset);
if(maxLayer < layer){
maxLayer = layer;
}
}
}
for (int i = 0; i < children.length(); i++) {
final Node child = (Node)children.at(i);
if (child instanceof elemental.dom.Element) {
final elemental.dom.Element element = (elemental.dom.Element)child;
final Mappable dataset = element.getDataset();
final int layer = getLayer(dataset);
if(layer >= maxLayer){
element.getStyle().removeProperty("display");
} else {
element.getStyle().setDisplay("none");
}
}
}
}
@Override
public Element getElement() {
return asElemental();
}
@Override
public List<String> getMessages() {
final List<String> result = new ArrayList<>();
final HTMLCollection children = asElemental().getChildren();
for (int i = 0; i < children.length(); i++) {
final Node child = (Node)children.at(i);
if (child instanceof elemental.dom.Element) {
final elemental.dom.Element element = (elemental.dom.Element)child;
final Mappable dataset = element.getDataset();
final String message = getMessage(dataset);
if (message != null) {
result.add(message);
}
}
}
return result;
}
@Override
public int getAnnotationCount() {
final HTMLCollection children = asElemental().getChildren();
return children.getLength();
}
/**
* Null-safe string comparison for equality.
*
* @param s1 first string to compare
* @param s2 second string to compare
* @return true iff both strings are equal
*/
private static boolean compareStrings(final String s1, final String s2) {
if (s1 == s2) {
return true;
}
if (s1 == null) {
return false;
}
return s1.equals(s2);
}
/**
* Read the offset value stored in the dataset.
*
* @param dataset the dataset
* @return the offset value or -1 if no valid value is found
*/
private static int getOffset(final Mappable dataset) {
final String asString = (String)dataset.at(OFFSET_DATASET_NAME);
if (asString == null) {
return -1;
}
try {
return Integer.parseInt(asString);
} catch (final NumberFormatException e) {
return -1;
}
}
/**
* Read the layer value stored in the dataset.
*
* @param dataset the dataset
* @return the layer value or -1 if no valid value is found
*/
private static int getLayer(final Mappable dataset) {
final String asString = (String)dataset.at(LAYER_DATASET_NAME);
if (asString == null) {
return -1;
}
try {
return Integer.parseInt(asString);
} catch (final NumberFormatException e) {
return -1;
}
}
/**
* Read the type value stored in the dataset.
*
* @param dataset the dataset
* @return the type value
*/
private static String getType(final Mappable dataset) {
return (String)dataset.at(TYPE_DATASET_NAME);
}
/**
* Read the message value stored in the dataset.
*
* @param dataset the dataset
* @return the message value
*/
private static String getMessage(final Mappable dataset) {
return (String)dataset.at(MESSAGE_DATASET_NAME);
}
/**
* Checks if the element has the annotation marker set.
* @param element the element to check
* @return true if the element is marked as 'annotation'
*/
public final static boolean isAnnotation(final Element element) {
if (element == null) {
return false;
}
final Object role = element.getDataset().at(ELEMENT_ROLE_DATA_PROPERTY);
return ELEMENT_ROLE_VALUE_ANNOTATION.equals(role);
}
}