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