001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.acacia.client.widgets; 029 030import org.opencms.acacia.client.CmsEditorBase; 031import org.opencms.acacia.client.css.I_CmsLayoutBundle; 032import org.opencms.gwt.client.util.CmsDomUtil; 033import org.opencms.gwt.client.util.CmsDomUtil.Style; 034 035import com.google.gwt.core.client.JavaScriptObject; 036import com.google.gwt.core.client.Scheduler; 037import com.google.gwt.core.client.Scheduler.ScheduledCommand; 038import com.google.gwt.dom.client.Document; 039import com.google.gwt.dom.client.Element; 040import com.google.gwt.dom.client.NativeEvent; 041import com.google.gwt.event.dom.client.ClickEvent; 042import com.google.gwt.event.dom.client.ClickHandler; 043import com.google.gwt.event.dom.client.DomEvent; 044import com.google.gwt.event.logical.shared.HasResizeHandlers; 045import com.google.gwt.event.logical.shared.ResizeEvent; 046import com.google.gwt.event.logical.shared.ResizeHandler; 047import com.google.gwt.event.logical.shared.ValueChangeEvent; 048import com.google.gwt.event.logical.shared.ValueChangeHandler; 049import com.google.gwt.event.shared.HandlerRegistration; 050import com.google.gwt.user.client.DOM; 051 052/** 053 * This class is used to start TinyMCE for editing the content of an element.<p> 054 * 055 * After constructing the instance, the actual editor is opened using the init() method, and destroyed with the close() 056 * method. While the editor is opened, the edited contents can be accessed using the methods of the HasValue interface. 057 */ 058public final class CmsTinyMCEWidget extends A_CmsEditWidget implements HasResizeHandlers, I_CmsHasDisplayDirection { 059 060 /** Use as option to disallow any HTML or formatting the content. */ 061 public static final String NO_HTML_EDIT = "no_html_edit"; 062 063 /** The disabled style element id. */ 064 private static final String DISABLED_STYLE_ID = "editorDisabledStyle"; 065 066 /** The minimum editor height. */ 067 private static final int MIN_EDITOR_HEIGHT = 70; 068 069 /** A flag which indicates whether the editor is currently active. */ 070 protected boolean m_active; 071 072 /** The current content. */ 073 protected String m_currentContent; 074 075 /** The TinyMCE editor instance. */ 076 protected JavaScriptObject m_editor; 077 078 /** The DOM ID of the editable element. */ 079 protected String m_id; 080 081 /** The original HTML content of the editable element. */ 082 protected String m_originalContent; 083 084 /** The maximal width of the widget. */ 085 protected int m_width; 086 087 /** The editor height to set. */ 088 int m_editorHeight; 089 090 /** Flag indicating the editor has been initialized. */ 091 boolean m_initialized; 092 093 /** The element to store the widget content in. */ 094 private Element m_contentElement; 095 096 /** Indicates the value has been set from external, not from within the widget. */ 097 private boolean m_externalValueChange; 098 099 /** Indicating if the widget has been attached yet. */ 100 private boolean m_hasBeenAttached; 101 102 /** Flag indicating if in line editing is used. */ 103 private boolean m_inline; 104 105 /** The editor options. */ 106 private Object m_options; 107 108 /** 109 * Creates a new instance for the given element. Use this constructor for in line editing.<p> 110 * 111 * @param element the DOM element 112 * @param options the tinyMCE editor options to extend the default settings 113 */ 114 public CmsTinyMCEWidget(Element element, Object options) { 115 116 this(element, options, true); 117 } 118 119 /** 120 * Creates a new instance with the given options. Use this constructor for form based editing.<p> 121 * 122 * @param options the tinyMCE editor options to extend the default settings 123 */ 124 public CmsTinyMCEWidget(Object options) { 125 126 this(DOM.createDiv(), options, false); 127 } 128 129 /** 130 * Constructor.<p> 131 * 132 * @param element the DOM element 133 * @param options the tinyMCE editor options to extend the default settings 134 * @param inline flag indicating if in line editing is used 135 */ 136 private CmsTinyMCEWidget(Element element, Object options, boolean inline) { 137 138 super(element); 139 m_originalContent = ""; 140 m_options = options; 141 m_active = true; 142 m_inline = inline; 143 if (m_inline) { 144 m_contentElement = element; 145 } else { 146 // using a child DIV as content element 147 m_contentElement = getElement().appendChild(DOM.createDiv()); 148 } 149 } 150 151 /** 152 * Returns the disabled text color.<p> 153 * 154 * @return the disabled text color 155 */ 156 private static String getDisabledTextColor() { 157 158 return I_CmsLayoutBundle.INSTANCE.constants().css().textColorDisabled(); 159 } 160 161 /** 162 * @see com.google.gwt.event.logical.shared.HasResizeHandlers#addResizeHandler(com.google.gwt.event.logical.shared.ResizeHandler) 163 */ 164 public HandlerRegistration addResizeHandler(ResizeHandler handler) { 165 166 return addHandler(handler, ResizeEvent.getType()); 167 } 168 169 /** 170 * @see org.opencms.acacia.client.widgets.A_CmsEditWidget#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler) 171 */ 172 @Override 173 public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) { 174 175 return addHandler(handler, ValueChangeEvent.getType()); 176 } 177 178 /** 179 * @see org.opencms.acacia.client.widgets.I_CmsHasDisplayDirection#getDisplayingDirection() 180 */ 181 public Direction getDisplayingDirection() { 182 183 return Direction.above; 184 } 185 186 /** 187 * Gets the main editable element.<p> 188 * 189 * @return the editable element 190 */ 191 public Element getMainElement() { 192 193 return m_contentElement; 194 } 195 196 /** 197 * @see com.google.gwt.user.client.ui.HasValue#getValue() 198 */ 199 @Override 200 public String getValue() { 201 202 if (m_editor != null) { 203 return getContent().trim(); 204 } 205 return m_originalContent.trim(); 206 } 207 208 /** 209 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#isActive() 210 */ 211 public boolean isActive() { 212 213 return m_active; 214 } 215 216 /** 217 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setActive(boolean) 218 */ 219 public void setActive(boolean active) { 220 221 if (m_active == active) { 222 return; 223 } 224 m_active = active; 225 if (m_editor != null) { 226 if (m_active) { 227 getElement().removeClassName(I_CmsLayoutBundle.INSTANCE.form().inActive()); 228 removeEditorDisabledStyle(); 229 fireValueChange(true); 230 } else { 231 getElement().addClassName(I_CmsLayoutBundle.INSTANCE.form().inActive()); 232 setEditorDisabledStyle(); 233 } 234 } 235 } 236 237 /** 238 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setName(java.lang.String) 239 */ 240 public void setName(String name) { 241 242 // no input field so nothing to do 243 244 } 245 246 /** 247 * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object) 248 */ 249 public void setValue(String value) { 250 251 setValue(value, false); 252 } 253 254 /** 255 * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object, boolean) 256 */ 257 public void setValue(String value, boolean fireEvents) { 258 259 if (value != null) { 260 value = value.trim(); 261 } 262 setPreviousValue(value); 263 if (m_editor == null) { 264 // editor has not been initialized yet 265 m_originalContent = value; 266 } else { 267 m_externalValueChange = true; 268 setContent(value); 269 } 270 if (fireEvents) { 271 fireValueChange(true); 272 } 273 } 274 275 /** 276 * Checks whether the necessary Javascript libraries are available by accessing them. 277 */ 278 protected native void checkLibraries() /*-{ 279 // fail early if tinymce is not available 280 var w = $wnd; 281 var init = w.tinyMCE.init; 282 }-*/; 283 284 /** 285 * Gives an element an id if it doesn't already have an id, and then returns the element's id.<p> 286 * 287 * @param element the element for which we want to add the id 288 * 289 * @return the id 290 */ 291 protected String ensureId(Element element) { 292 293 String id = element.getId(); 294 if ((id == null) || "".equals(id)) { 295 id = Document.get().createUniqueId(); 296 element.setId(id); 297 } 298 return id; 299 } 300 301 /** 302 * Returns the editor parent element.<p> 303 * 304 * @return the editor parent element 305 */ 306 protected Element getEditorParentElement() { 307 308 String parentId = m_id + "_parent"; 309 Element result = getElementById(parentId); 310 return result; 311 } 312 313 /** 314 * Gets an element by its id.<p> 315 * 316 * @param id the id 317 * @return the element with the given id 318 */ 319 protected native Element getElementById(String id) /*-{ 320 return $doc.getElementById(id); 321 }-*/; 322 323 /** 324 * Gets the toolbar element.<p> 325 * 326 * @return the toolbar element 327 */ 328 protected Element getToolbarElement() { 329 330 String toolbarId = m_id + "_external"; 331 Element result = getElementById(toolbarId); 332 return result; 333 } 334 335 /** 336 * Returns if the widget is used in inline mode.<p> 337 * 338 * @return <code>true</code> if the widget is used in inline mode 339 */ 340 protected boolean isInline() { 341 342 return m_inline; 343 } 344 345 /** 346 * @see com.google.gwt.user.client.ui.FocusWidget#onAttach() 347 */ 348 @Override 349 protected void onAttach() { 350 351 super.onAttach(); 352 if (!m_hasBeenAttached) { 353 m_hasBeenAttached = true; 354 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 355 356 public void execute() { 357 358 if (isAttached()) { 359 m_editorHeight = calculateEditorHeight(); 360 m_id = ensureId(getMainElement()); 361 m_width = calculateWidth(); 362 checkLibraries(); 363 if (isInline()) { 364 if (CmsDomUtil.getCurrentStyleInt(getElement(), Style.zIndex) < 1) { 365 getElement().getStyle().setZIndex(1); 366 } 367 addDomHandler(new ClickHandler() { 368 369 public void onClick(ClickEvent event) { 370 371 // prevent event propagation while editing inline, to avoid following links in ancestor nodes 372 event.stopPropagation(); 373 event.preventDefault(); 374 } 375 }, ClickEvent.getType()); 376 } 377 initNative(); 378 if (!m_active) { 379 getElement().addClassName(I_CmsLayoutBundle.INSTANCE.form().inActive()); 380 } 381 } else { 382 resetAtachedFlag(); 383 } 384 } 385 }); 386 } 387 } 388 389 /** 390 * @see com.google.gwt.user.client.ui.Widget#onDetach() 391 */ 392 @Override 393 protected void onDetach() { 394 395 try { 396 detachEditor(); 397 } catch (Throwable t) { 398 // may happen in rare cases, can be ignored 399 } 400 super.onDetach(); 401 } 402 403 /** 404 * Propagates the a focus event.<p> 405 */ 406 protected void propagateFocusEvent() { 407 408 if (m_initialized) { 409 NativeEvent nativeEvent = Document.get().createFocusEvent(); 410 DomEvent.fireNativeEvent(nativeEvent, this, getElement()); 411 } 412 } 413 414 /** 415 * Propagates a native mouse event.<p> 416 * 417 * @param eventType the mouse event type 418 * @param eventSource the event source 419 */ 420 protected native void propagateMouseEvent(String eventType, Element eventSource) /*-{ 421 var doc = $wnd.document; 422 var event; 423 if (doc.createEvent) { 424 event = doc.createEvent("MouseEvents"); 425 event.initEvent(eventType, true, true); 426 eventSource.dispatchEvent(event); 427 } else { 428 eventSource.fireEvent("on" + eventType); 429 } 430 }-*/; 431 432 /** 433 * Removes the editor instance.<p> 434 */ 435 protected native void removeEditor() /*-{ 436 var editor = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editor; 437 editor.remove(); 438 }-*/; 439 440 /** 441 * Sets the main content of the element which is inline editable.<p> 442 * 443 * @param html the new content html 444 */ 445 protected native void setMainElementContent(String html) /*-{ 446 var instance = this; 447 var elementId = instance.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_id; 448 var mainElement = $wnd.document.getElementById(elementId); 449 mainElement.innerHTML = html; 450 }-*/; 451 452 /** 453 * Checks if the main element contains the current text selection.<p> 454 * 455 * @return <code>true</code> if the main element contains the current text selection 456 */ 457 protected boolean shouldReceiveFocus() { 458 459 return m_inline && CmsEditorBase.shouldFocusOnInlineEdit(getElement()); 460 } 461 462 /** 463 * Calculates the needed editor height.<p> 464 * 465 * @return the calculated editor height 466 */ 467 int calculateEditorHeight() { 468 469 int result = getElement().getOffsetHeight() + 30; 470 return result > MIN_EDITOR_HEIGHT ? result : MIN_EDITOR_HEIGHT; 471 } 472 473 /** 474 * Calculates the widget width.<p> 475 * 476 * @return the widget width 477 */ 478 int calculateWidth() { 479 480 int result; 481 if (m_inline && CmsDomUtil.getCurrentStyle(getElement(), Style.display).equals("inline")) { 482 com.google.gwt.dom.client.Element parentBlock = getElement().getParentElement(); 483 while (CmsDomUtil.getCurrentStyle(parentBlock, Style.display).equals("inline")) { 484 parentBlock = parentBlock.getParentElement(); 485 } 486 result = parentBlock.getOffsetWidth(); 487 } else { 488 result = getElement().getOffsetWidth(); 489 } 490 return result - 2; 491 } 492 493 /** 494 * Initializes the TinyMCE instance. 495 */ 496 native void initNative() /*-{ 497 498 function merge() { 499 var result = {}, length = arguments.length; 500 for (i = 0; i < length; i++) { 501 for (key in arguments[i]) { 502 if (arguments[i].hasOwnProperty(key)) { 503 result[key] = arguments[i][key]; 504 } 505 } 506 } 507 return result; 508 } 509 510 var self = this; 511 var needsRefocus = self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::shouldReceiveFocus()(); 512 var elementId = self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_id; 513 var mainElement = $wnd.document.getElementById(elementId); 514 var editorHeight = self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editorHeight; 515 516 var fireChange = function() { 517 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::fireChangeFromNative()(); 518 }; 519 var options = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_options; 520 if (options != null && options.editorHeight) { 521 editorHeight = options.editorHeight; 522 delete options.editorHeight; 523 } 524 // default options: 525 var defaults; 526 if (@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::NO_HTML_EDIT == options) { 527 // disallow any formatting 528 defaults = { 529 selector : mainElement.tagName + "#" + elementId, 530 entity_encoding : "raw", 531 mode : "exact", 532 theme : "silver", 533 plugins : "paste", 534 paste_as_text : true, 535 toolbar : "undo redo", 536 menubar : false, 537 forced_root_block : false 538 }; 539 options = null; 540 } else { 541 defaults = { 542 selector : mainElement.tagName + "#" + elementId, 543 relative_urls : false, 544 remove_script_host : false, 545 entity_encoding : "raw", 546 skin_variant : 'ocms', 547 mode : "exact", 548 theme : "silver", 549 plugins : "autolink lists pagebreak table save codemirror hr image link emoticons spellchecker insertdatetime preview media searchreplace print paste directionality noneditable visualchars nonbreaking template wordcount advlist", 550 paste_as_text : true, 551 menubar : false, 552 }; 553 } 554 if (this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_inline) { 555 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_currentContent = mainElement.innerHTML; 556 defaults.inline = true; 557 defaults.width = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_width; 558 } else { 559 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_currentContent = self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_originalContent; 560 defaults.min_height = 100; 561 defaults.max_height = editorHeight; 562 defaults.width = '100%'; 563 defaults.resize = 'both'; 564 } 565 // extend the defaults with any given options 566 if (options != null) { 567 if (options.style_formats) { 568 // tinymce performs a type test for arrays wich fails in case the array was not created in the same window context 569 var formats = new $wnd.Array(); 570 for (var i = 0; i < options.style_formats.length; i++) { 571 formats[i] = options.style_formats[i]; 572 } 573 options.style_formats = formats; 574 } 575 defaults = merge(defaults, options); 576 } 577 if (this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_inline) { 578 delete defaults.content_css; 579 } else { 580 // enable autoresize 581 defaults.plugins = "autoresize " + defaults.plugins; 582 } 583 if (needsRefocus) { 584 defaults.auto_focus = elementId; 585 } 586 587 // add the setup function 588 defaults.setup = function(ed) { 589 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editor = ed; 590 ed.on('SetContent', fireChange); 591 ed.on('change', fireChange); 592 ed.on('KeyDown', fireChange); 593 ed 594 .on( 595 'LoadContent', 596 function() { 597 if (!self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_inline) { 598 // firing resize event on resize of the editor iframe 599 ed.dom 600 .bind( 601 ed.getWin(), 602 'resize', 603 function() { 604 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::fireResizeEvent()(); 605 }); 606 var content = self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_originalContent; 607 if (content != null) { 608 ed.setContent(content); 609 } 610 } 611 }); 612 ed 613 .on( 614 'init', 615 function() { 616 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::scheduleInitializationDone()(); 617 }); 618 619 if (!self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_inline) { 620 621 ed 622 .on( 623 'Click', 624 function(event) { 625 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::propagateFocusEvent()(); 626 }); 627 ed 628 .on( 629 'activate', 630 function(event) { 631 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::propagateFocusEvent()(); 632 }); 633 ed 634 .on( 635 'focus', 636 function(event) { 637 self.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::propagateFocusEvent()(); 638 }); 639 } 640 }; 641 642 // initialize tinyMCE 643 $wnd.tinymce.init(defaults); 644 }-*/; 645 646 /** 647 * Removes the disabled editor styling.<p> 648 */ 649 native void removeEditorDisabledStyle()/*-{ 650 var ed = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editor; 651 var styleEl = ed 652 .getDoc() 653 .getElementById( 654 @org.opencms.acacia.client.widgets.CmsTinyMCEWidget::DISABLED_STYLE_ID); 655 if (styleEl != null) { 656 ed.getDoc().head.removeChild(styleEl); 657 } 658 }-*/; 659 660 /** 661 * Resets the attached flag.<p> 662 */ 663 void resetAtachedFlag() { 664 665 m_hasBeenAttached = false; 666 } 667 668 /** 669 * Scheduling to set the initialized flag.<p> 670 */ 671 void scheduleInitializationDone() { 672 673 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 674 675 public void execute() { 676 677 m_initialized = true; 678 if (m_active) { 679 removeEditorDisabledStyle(); 680 } else { 681 setEditorDisabledStyle(); 682 } 683 } 684 }); 685 } 686 687 /** 688 * Sets the editor disabled styling.<p> 689 */ 690 native void setEditorDisabledStyle()/*-{ 691 var ed = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editor; 692 if (ed 693 .getDoc() 694 .getElementById( 695 @org.opencms.acacia.client.widgets.CmsTinyMCEWidget::DISABLED_STYLE_ID) == null) { 696 var styleEl = ed.getDoc().createElement("style"); 697 styleEl 698 .setAttribute("id", 699 @org.opencms.acacia.client.widgets.CmsTinyMCEWidget::DISABLED_STYLE_ID); 700 var styleText = ed 701 .getDoc() 702 .createTextNode( 703 "body, body *{ color: " 704 + @org.opencms.acacia.client.widgets.CmsTinyMCEWidget::getDisabledTextColor()() 705 + " !important;}"); 706 styleEl.appendChild(styleText); 707 ed.getDoc().head.appendChild(styleEl); 708 } 709 }-*/; 710 711 /** 712 * Removes the editor.<p> 713 */ 714 private native void detachEditor() /*-{ 715 716 var ed = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editor; 717 if (ed != null) { 718 ed.remove(); 719 } 720 // in IE somehow the whole document will be selected, empty the selection to resolve that 721 if ($wnd.document.selection != null) { 722 $wnd.document.selection.empty(); 723 } 724 }-*/; 725 726 /** 727 * Used to fire the value changed event from native code.<p> 728 */ 729 private void fireChangeFromNative() { 730 731 // skip firing the change event, if the external flag is set 732 if (m_initialized && !m_externalValueChange && m_active) { 733 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 734 735 public void execute() { 736 737 try { 738 fireValueChange(false); 739 } catch (Throwable t) { 740 // this may happen when returning from full screen mode, nothing to be done 741 } 742 } 743 }); 744 } 745 // reset the external flag 746 m_externalValueChange = false; 747 } 748 749 /** 750 * Fires the resize event.<p> 751 */ 752 private void fireResizeEvent() { 753 754 ResizeEvent.fire(this, getOffsetWidth(), getOffsetHeight()); 755 } 756 757 /** 758 * Returns the editor content.<p> 759 * 760 * @return the editor content 761 */ 762 private native String getContent() /*-{ 763 var editor = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editor; 764 return editor.getContent(); 765 }-*/; 766 767 /** 768 * Sets the content of the TinyMCE editor.<p> 769 * 770 * @param newContent the new content 771 */ 772 private native void setContent(String newContent) /*-{ 773 var editor = this.@org.opencms.acacia.client.widgets.CmsTinyMCEWidget::m_editor; 774 editor.setContent(newContent); 775 }-*/; 776 777}