001/* 002 * (C) Copyright 2006-2008 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Alexandre Russel 016 * 017 * $Id$ 018 */ 019 020package org.nuxeo.ecm.platform.annotations.gwt.client.view; 021 022import java.util.ArrayList; 023import java.util.List; 024 025import org.nuxeo.ecm.platform.annotations.gwt.client.AnnotationConstant; 026import org.nuxeo.ecm.platform.annotations.gwt.client.controler.AnnotationController; 027import org.nuxeo.ecm.platform.annotations.gwt.client.model.Annotation; 028import org.nuxeo.ecm.platform.annotations.gwt.client.model.AnnotationChangeListener; 029import org.nuxeo.ecm.platform.annotations.gwt.client.model.AnnotationModel; 030import org.nuxeo.ecm.platform.annotations.gwt.client.util.CSSClassManager; 031import org.nuxeo.ecm.platform.annotations.gwt.client.util.ImageRangeXPointer; 032import org.nuxeo.ecm.platform.annotations.gwt.client.util.NullRangeXPointer; 033import org.nuxeo.ecm.platform.annotations.gwt.client.util.Point; 034import org.nuxeo.ecm.platform.annotations.gwt.client.util.StringRangeXPointer; 035import org.nuxeo.ecm.platform.annotations.gwt.client.util.Utils; 036import org.nuxeo.ecm.platform.annotations.gwt.client.util.Visitor; 037import org.nuxeo.ecm.platform.annotations.gwt.client.util.XPathUtil; 038import org.nuxeo.ecm.platform.annotations.gwt.client.util.XPointer; 039import org.nuxeo.ecm.platform.annotations.gwt.client.view.decorator.DecoratorVisitor; 040import org.nuxeo.ecm.platform.annotations.gwt.client.view.decorator.DecoratorVisitorFactory; 041import org.nuxeo.ecm.platform.annotations.gwt.client.view.decorator.ImageDecorator; 042 043import com.allen_sauer.gwt.log.client.Log; 044import com.google.gwt.dom.client.BodyElement; 045import com.google.gwt.dom.client.DivElement; 046import com.google.gwt.dom.client.Document; 047import com.google.gwt.dom.client.Element; 048import com.google.gwt.dom.client.ImageElement; 049import com.google.gwt.dom.client.NodeList; 050import com.google.gwt.user.client.Window; 051 052/** 053 * @author <a href="mailto:arussel@nuxeo.com">Alexandre Russel</a> 054 */ 055public class AnnotatedDocument implements AnnotationChangeListener { 056 private List<Annotation> annotations = new ArrayList<Annotation>(); 057 058 private List<Annotation> decoratedAnnotations = new ArrayList<Annotation>(); 059 060 private static XPathUtil xPathUtil = new XPathUtil(); 061 062 private final ImageDecorator decorator; 063 064 private AnnotationController controller; 065 066 public AnnotatedDocument(AnnotationController controller) { 067 this.controller = controller; 068 decorator = new ImageDecorator(controller); 069 } 070 071 public void onChange(AnnotationModel model, ChangeEvent ce) { 072 annotations = model.getAnnotations(); 073 Log.debug("On change: annotations.empty? " + annotations.isEmpty()); 074 if (annotations.isEmpty() || ce == ChangeEvent.annotation) { 075 return; 076 } 077 078 update(); 079 } 080 081 public void update() { 082 update(false); 083 } 084 085 public void update(boolean forceDecorate) { 086 Log.debug("Update annotations - forceDecorate: " + forceDecorate); 087 if (annotations == null) { 088 return; 089 } 090 091 if (forceDecorate) { 092 decoratedAnnotations.clear(); 093 removeAllAnnotatedAreas(); 094 } 095 096 for (Annotation annotation : annotations) { 097 if (!decoratedAnnotations.contains(annotation)) { 098 Log.debug("Decorate annotation"); 099 decorate(annotation); 100 decoratedAnnotations.add(annotation); 101 } 102 } 103 104 int selectedAnnotationIndex = getSelectedAnnotationIndex(); 105 if (selectedAnnotationIndex > -1) { 106 updateSelectedAnnotation(selectedAnnotationIndex); 107 } 108 109 if (!isAnnotationsVisible()) { 110 Log.debug("Hide annotations!"); 111 hideAnnotations(); 112 // disable popup listeners in case we just added a new annotation 113 controller.disablePopupListeners(); 114 } 115 } 116 117 public void preDecorateDocument() { 118 Document document = Document.get(); 119 Log.debug("preDecorateDocument -- isMultiImage? " + controller.isMultiImage()); 120 preDecorateDocument(document); 121 } 122 123 private static void preDecorateDocument(Document document) { 124 Log.debug("Predecorate document !"); 125 NodeList<Element> elements = document.getElementsByTagName("img"); 126 for (int x = 0; x < elements.getLength(); x++) { 127 Element element = elements.getItem(x); 128 DivElement divElement = document.createDivElement(); 129 divElement.getStyle().setProperty("position", "relative"); 130 divElement.setClassName(AnnotationConstant.IGNORED_ELEMENT); 131 String path = xPathUtil.getXPath(element); 132 path = XPathUtil.toIdableName(path); 133 divElement.setId(path); 134 Element nextSibling = element.getNextSiblingElement(); 135 Element parent = element.getParentElement(); 136 if (nextSibling == null) { 137 parent.appendChild(divElement); 138 } else { 139 parent.insertBefore(divElement, nextSibling); 140 } 141 divElement.appendChild(element); 142 } 143 } 144 145 public void decorate(Annotation annotation) { 146 XPointer xpointer = annotation.getXpointer(); 147 if (xpointer instanceof StringRangeXPointer) { 148 decorateStringRange((StringRangeXPointer) xpointer, annotation); 149 } else if (xpointer instanceof ImageRangeXPointer) { 150 decorateImageRange((ImageRangeXPointer) xpointer, annotation); 151 } 152 } 153 154 private void decorateImageRange(ImageRangeXPointer xpointer, Annotation annotation) { 155 ImageElement img = xpointer.getImage(controller.isMultiImage()); 156 if (img == null) { 157 return; 158 } 159 Point[] points = controller.filterAnnotation(xpointer.getTopLeft(), xpointer.getBottomRight()); 160 if (points == null) { 161 return; 162 } 163 decorator.addAnnotatedArea(points[0].getX(), points[0].getY(), points[1].getX(), points[1].getY(), img, 164 annotation, controller); 165 } 166 167 private void decorateStringRange(StringRangeXPointer xpointer, Annotation annotation) { 168 DecoratorVisitor processor = DecoratorVisitorFactory.forAnnotation(annotation, controller); 169 Visitor visitor = new Visitor(processor); 170 visitor.process(xpointer.getOwnerDocument()); 171 } 172 173 public void updateSelectedAnnotation(int index) { 174 Annotation annotation = annotations.get(index); 175 BodyElement bodyElement = Document.get().getBody(); 176 if (!(annotation.getXpointer() instanceof NullRangeXPointer)) { 177 NodeList<Element> spans = bodyElement.getElementsByTagName("span"); 178 NodeList<Element> as = bodyElement.getElementsByTagName("div"); 179 int scrollTop = Integer.MAX_VALUE; 180 int scrollLeft = Integer.MAX_VALUE; 181 for (int x = 0; x < spans.getLength(); x++) { 182 Element element = spans.getItem(x); 183 if (processElement(annotation, element)) { 184 int[] absTopLeft = Utils.getAbsoluteTopLeft(element, Document.get()); 185 if (absTopLeft[0] < scrollTop) { 186 scrollTop = absTopLeft[0]; 187 } 188 if (absTopLeft[1] < scrollLeft) { 189 scrollLeft = absTopLeft[1]; 190 } 191 } 192 } 193 for (int x = 0; x < as.getLength(); x++) { 194 Element element = as.getItem(x); 195 if (processElement(annotation, element)) { 196 int[] absTopLeft = Utils.getAbsoluteTopLeft(element, Document.get()); 197 if (absTopLeft[0] < scrollTop) { 198 scrollTop = absTopLeft[0]; 199 } 200 if (absTopLeft[1] < scrollLeft) { 201 scrollLeft = absTopLeft[1]; 202 } 203 } 204 } 205 206 scrollLeft = scrollLeft == Integer.MAX_VALUE ? 0 : scrollLeft; 207 scrollTop = scrollTop == Integer.MAX_VALUE ? 0 : scrollTop; 208 Window.scrollTo(scrollLeft, scrollTop); 209 } 210 } 211 212 private boolean processElement(Annotation annotation, Element element) { 213 CSSClassManager manager = new CSSClassManager(element); 214 // remove old 215 manager.removeClass(AnnotationConstant.SELECTED_CLASS_NAME); 216 // set new 217 if (manager.isClassPresent(AnnotationConstant.DECORATE_CLASS_NAME + annotation.getId())) { 218 manager.addClass(AnnotationConstant.SELECTED_CLASS_NAME); 219 220 return true; 221 } 222 return false; 223 } 224 225 private native int getSelectedAnnotationIndex() /*-{ 226 if (typeof top['selectedAnnotationIndex'] != "undefined") { 227 return top['selectedAnnotationIndex']; 228 } else { 229 return -1; 230 } 231 }-*/; 232 233 public void hideAnnotations() { 234 BodyElement bodyElement = Document.get().getBody(); 235 NodeList<Element> spans = bodyElement.getElementsByTagName("span"); 236 NodeList<Element> divs = bodyElement.getElementsByTagName("div"); 237 238 for (int x = 0; x < spans.getLength(); x++) { 239 Element element = spans.getItem(x); 240 CSSClassManager manager = new CSSClassManager(element); 241 if (manager.isClassPresent(AnnotationConstant.DECORATE_CLASS_NAME)) { 242 manager.removeClass(AnnotationConstant.DECORATE_CLASS_NAME); 243 manager.addClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME); 244 } 245 } 246 247 for (int x = 0; x < divs.getLength(); x++) { 248 Element element = divs.getItem(x); 249 CSSClassManager manager = new CSSClassManager(element); 250 if (manager.isClassPresent(AnnotationConstant.DECORATE_CLASS_NAME)) { 251 manager.removeClass(AnnotationConstant.DECORATE_CLASS_NAME); 252 manager.addClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME); 253 } 254 } 255 setAnnotationsShown(false); 256 } 257 258 private native void setAnnotationsShown(boolean annotationsShown) /*-{ 259 top['annotationsShown'] = annotationsShown; 260 }-*/; 261 262 public void showAnnotations() { 263 BodyElement bodyElement = Document.get().getBody(); 264 NodeList<Element> spans = bodyElement.getElementsByTagName("span"); 265 NodeList<Element> divs = bodyElement.getElementsByTagName("div"); 266 267 for (int x = 0; x < spans.getLength(); x++) { 268 Element element = spans.getItem(x); 269 CSSClassManager manager = new CSSClassManager(element); 270 if (manager.isClassPresent(AnnotationConstant.DECORATE_NOT_CLASS_NAME)) { 271 manager.removeClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME); 272 manager.addClass(AnnotationConstant.DECORATE_CLASS_NAME); 273 } 274 if (manager.isClassPresent(AnnotationConstant.SELECTED_NOT_CLASS_NAME)) { 275 manager.removeClass(AnnotationConstant.SELECTED_NOT_CLASS_NAME); 276 manager.addClass(AnnotationConstant.SELECTED_CLASS_NAME); 277 } 278 } 279 280 for (int x = 0; x < divs.getLength(); x++) { 281 Element element = divs.getItem(x); 282 CSSClassManager manager = new CSSClassManager(element); 283 if (manager.isClassPresent(AnnotationConstant.DECORATE_NOT_CLASS_NAME)) { 284 manager.removeClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME); 285 manager.addClass(AnnotationConstant.DECORATE_CLASS_NAME); 286 } 287 if (manager.isClassPresent(AnnotationConstant.SELECTED_NOT_CLASS_NAME)) { 288 manager.removeClass(AnnotationConstant.SELECTED_NOT_CLASS_NAME); 289 manager.addClass(AnnotationConstant.SELECTED_CLASS_NAME); 290 } 291 } 292 setAnnotationsShown(true); 293 } 294 295 public native boolean isAnnotationsVisible() /*-{ 296 if (typeof top['annotationsShown'] != "undefined") { 297 return top['annotationsShown']; 298 } else { 299 return true; 300 } 301 }-*/; 302 303 private void removeAllAnnotatedAreas() { 304 String className = isAnnotationsVisible() ? AnnotationConstant.DECORATE_CLASS_NAME 305 : AnnotationConstant.DECORATE_NOT_CLASS_NAME; 306 BodyElement bodyElement = Document.get().getBody(); 307 NodeList<Element> as = bodyElement.getElementsByTagName("div"); 308 removeAnchorAreas(as, className); 309 removeSpanAreas(className); 310 } 311 312 private void removeAnchorAreas(NodeList<Element> nodes, String className) { 313 List<Element> elements = getElementsToRemove(nodes, className); 314 for (Element element : elements) { 315 element.getParentElement().removeChild(element); 316 } 317 } 318 319 private List<Element> getElementsToRemove(NodeList<Element> nodes, String className) { 320 List<Element> elementsToRemove = new ArrayList<Element>(); 321 for (int i = 0; i < nodes.getLength(); ++i) { 322 Element element = nodes.getItem(i); 323 CSSClassManager manager = new CSSClassManager(element); 324 if (manager.isClassPresent(className)) { 325 elementsToRemove.add(element); 326 } 327 } 328 return elementsToRemove; 329 } 330 331 private void removeSpanAreas(String className) { 332 NodeList<Element> spans = Document.get().getBody().getElementsByTagName("span"); 333 List<Element> elements = getElementsToRemove(spans, className); 334 while (!elements.isEmpty()) { 335 Element element = elements.get(0); 336 String elementHtml = element.getInnerHTML(); 337 Element parent = element.getParentElement(); 338 String parentHtml = parent.getInnerHTML(); 339 340 String escapedClassName = element.getClassName().replaceAll("([/\\\\\\.\\*\\+\\?\\|\\(\\)\\[\\]\\{\\}$^])", 341 "\\\\$1"); 342 String escapedElementHtml = elementHtml.replaceAll("([/\\\\\\.\\*\\+\\?\\|\\(\\)\\[\\]\\{\\}$^])", "\\\\$1"); 343 344 parentHtml = parentHtml.replaceFirst("<(span|SPAN) class=(\")?" + escapedClassName + "(\")?.*>" 345 + escapedElementHtml + "</(span|SPAN)>", elementHtml); 346 parent.setInnerHTML(parentHtml); 347 348 spans = Document.get().getBody().getElementsByTagName("span"); 349 elements = getElementsToRemove(spans, className); 350 } 351 } 352 353 public void decorateSelectedText(Annotation annotation) { 354 DecoratorVisitor processor = DecoratorVisitorFactory.forSelectedText(annotation); 355 Visitor visitor = new Visitor(processor); 356 StringRangeXPointer xpointer = (StringRangeXPointer) annotation.getXpointer(); 357 visitor.process(xpointer.getOwnerDocument()); 358 } 359 360 public void removeSelectedTextDecoration(Annotation annotation) { 361 String className = AnnotationConstant.SELECTED_TEXT_CLASS_NAME; 362 removeSpanAreas(className); 363 } 364 365}