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.gwt.client.ui.input.tinymce; 029 030import org.opencms.gwt.client.I_CmsHasInit; 031import org.opencms.gwt.client.ui.I_CmsAutoHider; 032import org.opencms.gwt.client.ui.input.CmsTextBox; 033import org.opencms.gwt.client.ui.input.I_CmsFormWidget; 034import org.opencms.gwt.client.ui.input.form.CmsWidgetFactoryRegistry; 035import org.opencms.gwt.client.ui.input.form.I_CmsFormWidgetFactory; 036import org.opencms.util.CmsStringUtil; 037 038import java.util.Map; 039 040import com.google.common.base.Objects; 041import com.google.common.base.Optional; 042import com.google.gwt.core.client.JavaScriptObject; 043import com.google.gwt.core.client.Scheduler; 044import com.google.gwt.core.client.Scheduler.ScheduledCommand; 045import com.google.gwt.dom.client.Document; 046import com.google.gwt.dom.client.Element; 047import com.google.gwt.dom.client.EventTarget; 048import com.google.gwt.dom.client.NativeEvent; 049import com.google.gwt.event.dom.client.DomEvent; 050import com.google.gwt.event.logical.shared.HasResizeHandlers; 051import com.google.gwt.event.logical.shared.HasValueChangeHandlers; 052import com.google.gwt.event.logical.shared.ResizeEvent; 053import com.google.gwt.event.logical.shared.ResizeHandler; 054import com.google.gwt.event.logical.shared.ValueChangeEvent; 055import com.google.gwt.event.logical.shared.ValueChangeHandler; 056import com.google.gwt.event.shared.HandlerRegistration; 057import com.google.gwt.user.client.DOM; 058import com.google.gwt.user.client.Event; 059import com.google.gwt.user.client.Event.NativePreviewEvent; 060import com.google.gwt.user.client.Event.NativePreviewHandler; 061import com.google.gwt.user.client.Timer; 062import com.google.gwt.user.client.ui.FlowPanel; 063 064/** 065 * This class is used to start TinyMCE for editing the content of an element.<p> 066 * 067 * After constructing the instance, the actual editor is opened using the init() method, and destroyed with the close() 068 * method. While the editor is opened, the edited contents can be accessed using the methods of the HasValue interface. 069 */ 070public final class CmsTinyMCEWidget extends FlowPanel 071implements I_CmsFormWidget, HasResizeHandlers, I_CmsHasInit, HasValueChangeHandlers<String> { 072 073 /** Use as option to disallow any HTML or formatting the content. */ 074 public static final String NO_HTML_EDIT = "no_html_edit"; 075 076 /** The widget type id.*/ 077 public static final String WIDGET_TYPE = "wysiwyg"; 078 079 /** Counts currently attached widget instances, for use in registering / deregistering preview event listeners. */ 080 static int attachCount; 081 082 /** The minimum editor height. */ 083 private static final int MIN_EDITOR_HEIGHT = 70; 084 085 /** The preview handler registration. */ 086 private static HandlerRegistration previewRegistration; 087 088 /** The current content. */ 089 protected String m_currentContent; 090 091 /** The TinyMCE editor instance. */ 092 protected JavaScriptObject m_editor; 093 094 /** The DOM ID of the editable element. */ 095 protected String m_id; 096 097 /** The original HTML content of the editable element. */ 098 protected String m_originalContent; 099 100 /** The maximal width of the widget. */ 101 protected int m_width; 102 103 /** The editor height to set. */ 104 int m_editorHeight; 105 106 /** The element to store the widget content in. */ 107 private Element m_contentElement; 108 109 /** Flag controlling whether the widget is enabled. */ 110 private boolean m_enabled = true; 111 112 /** Indicates the value has been set from external, not from within the widget. */ 113 private boolean m_externalValueChange; 114 115 /** Indicating if the widget has been attached yet. */ 116 private boolean m_hasBeenAttached; 117 118 /** Flag indicating the editor has been initialized. */ 119 private boolean m_initialized; 120 121 /** The editor options. */ 122 private Object m_options; 123 124 /** The previous value. */ 125 private String m_previousValue; 126 127 /** 128 * Creates a new instance with the given TinyMCE options. Use this constructor for form based editing.<p> 129 * 130 * @param options the tinyMCE editor options to extend the default settings 131 */ 132 public CmsTinyMCEWidget(Object options) { 133 134 // super(element); 135 m_originalContent = ""; 136 m_options = options; 137 // using a child DIV as content element 138 m_contentElement = getElement().appendChild(DOM.createDiv()); 139 } 140 141 /** 142 * Creates a new instance based on configuration data from the server.<p> 143 * 144 * @param config the configuration data 145 */ 146 public CmsTinyMCEWidget(String config) { 147 148 this(CmsTinyMCEHelper.generateOptionsForTiny(config)); 149 } 150 151 /** 152 * Initializes this class.<p> 153 */ 154 public static void initClass() { 155 156 // registers a factory for creating new instances of this widget 157 CmsWidgetFactoryRegistry.instance().registerFactory(WIDGET_TYPE, new I_CmsFormWidgetFactory() { 158 159 /** 160 * @see org.opencms.gwt.client.ui.input.form.I_CmsFormWidgetFactory#createWidget(java.util.Map, com.google.common.base.Optional) 161 */ 162 public I_CmsFormWidget createWidget(Map<String, String> widgetParams, Optional<String> defaultValue) { 163 164 String cfg = widgetParams.get("v"); 165 if (CmsStringUtil.isEmptyOrWhitespaceOnly(cfg)) { 166 // something went wrong, fall back to text box 167 return new CmsTextBox(); 168 } else { 169 return new CmsTinyMCEWidget(decode(cfg)); 170 } 171 } 172 173 private String decode(String string) { 174 175 StringBuffer result = new StringBuffer(); 176 for (String num : string.split(",")) { 177 result.append((char)(Integer.parseInt(num))); 178 } 179 return result.toString(); 180 } 181 }); 182 } 183 184 /** 185 * Decrements the attach count, removes preview event handler when we go from 1 to 0.<p> 186 */ 187 private static void decrementAttached() { 188 189 attachCount -= 1; 190 if ((attachCount < 1) && (previewRegistration != null)) { 191 previewRegistration.removeHandler(); 192 previewRegistration = null; 193 } 194 195 } 196 197 /** 198 * Increments the attach count, installs preview event handler when we go from 0 to 1.<p> 199 */ 200 private static void incrementAttached() { 201 202 attachCount += 1; 203 if (attachCount == 1) { 204 // Prevent events on TinyMCE popups from being cancelled by the PopupPanel containing the property dialog 205 206 previewRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() { 207 208 public void onPreviewNativeEvent(NativePreviewEvent pEvent) { 209 210 Event event = Event.as(pEvent.getNativeEvent()); 211 EventTarget target = event.getEventTarget(); 212 if (Element.is(target)) { 213 Element elem = Element.as(target); 214 while (elem != null) { 215 if (elem.getClassName().contains("mce-floatpanel")) { 216 pEvent.consume(); 217 return; 218 } 219 elem = elem.getParentElement(); 220 } 221 222 } 223 224 } 225 }); 226 227 } 228 } 229 230 /** 231 * @see com.google.gwt.event.logical.shared.HasResizeHandlers#addResizeHandler(com.google.gwt.event.logical.shared.ResizeHandler) 232 */ 233 public HandlerRegistration addResizeHandler(ResizeHandler handler) { 234 235 return addHandler(handler, ResizeEvent.getType()); 236 } 237 238 /** 239 * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler) 240 */ 241 @Override 242 public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) { 243 244 return addHandler(handler, ValueChangeEvent.getType()); 245 } 246 247 /** 248 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getApparentValue() 249 */ 250 public String getApparentValue() { 251 252 return getFormValueAsString(); 253 } 254 255 /** 256 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFieldType() 257 */ 258 public FieldType getFieldType() { 259 260 return FieldType.STRING; 261 } 262 263 /** 264 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFormValue() 265 */ 266 public Object getFormValue() { 267 268 return getFormValueAsString(); 269 } 270 271 /** 272 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFormValueAsString() 273 */ 274 public String getFormValueAsString() { 275 276 return getValue(); 277 278 } 279 280 /** 281 * Gets the main editable element.<p> 282 * 283 * @return the editable element 284 */ 285 public Element getMainElement() { 286 287 return m_contentElement; 288 } 289 290 /** 291 * Gets the value.<p> 292 * 293 * @return the value 294 */ 295 public String getValue() { 296 297 if (m_editor != null) { 298 return getContent().trim(); 299 } 300 return m_originalContent.trim(); 301 } 302 303 /** 304 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#isEnabled() 305 */ 306 public boolean isEnabled() { 307 308 return m_enabled; 309 } 310 311 /** 312 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#reset() 313 */ 314 public void reset() { 315 316 setFormValueAsString(""); 317 } 318 319 /** 320 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setAutoHideParent(org.opencms.gwt.client.ui.I_CmsAutoHider) 321 */ 322 public void setAutoHideParent(I_CmsAutoHider autoHideParent) { 323 324 // not supported 325 } 326 327 /** 328 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setEnabled(boolean) 329 * 330 * Partial support: This only works before the actual TinyMCE instance is loaded. 331 */ 332 public void setEnabled(boolean enabled) { 333 334 m_enabled = enabled; 335 } 336 337 /** 338 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setErrorMessage(java.lang.String) 339 */ 340 public void setErrorMessage(String errorMessage) { 341 342 // not supported 343 } 344 345 /** 346 * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setFormValueAsString(java.lang.String) 347 */ 348 public void setFormValueAsString(String value) { 349 350 setValue(value, false); 351 } 352 353 /** 354 * Sets the value.<p> 355 * 356 * @param value the value 357 * @param fireEvents true if value change event should be fired 358 */ 359 public void setValue(String value, boolean fireEvents) { 360 361 if (value != null) { 362 value = value.trim(); 363 } 364 if (!Objects.equal(value, getValue())) { 365 setPreviousValue(value); 366 if (m_editor == null) { 367 // editor has not been initialized yet 368 m_originalContent = value; 369 } else { 370 m_externalValueChange = true; 371 setContent(value); 372 } 373 if (fireEvents) { 374 fireValueChange(true); 375 } 376 } 377 378 } 379 380 /** 381 * Checks whether the necessary Javascript libraries are available by accessing them. 382 */ 383 protected native void checkLibraries() /*-{ 384 // fail early if tinymce is not available 385 var w = $wnd; 386 var init = w.tinyMCE.init; 387 }-*/; 388 389 /** 390 * Gives an element an id if it doesn't already have an id, and then returns the element's id.<p> 391 * 392 * @param element the element for which we want to add the id 393 * 394 * @return the id 395 */ 396 protected String ensureId(Element element) { 397 398 String id = element.getId(); 399 if ((id == null) || "".equals(id)) { 400 id = Document.get().createUniqueId(); 401 element.setId(id); 402 } 403 return id; 404 } 405 406 /** 407 * Fires a change event.<p> 408 * 409 * @param force true if the event should be fired even if the value does not differ from the previous one 410 */ 411 protected void fireValueChange(boolean force) { 412 413 String currentValue = getValue(); 414 if (force || !currentValue.equals(m_previousValue)) { 415 m_previousValue = currentValue; 416 ValueChangeEvent.fire(this, currentValue); 417 } 418 419 } 420 421 /** 422 * Returns the editor parent element.<p> 423 * 424 * @return the editor parent element 425 */ 426 protected Element getEditorParentElement() { 427 428 String parentId = m_id + "_parent"; 429 Element result = getElementById(parentId); 430 return result; 431 } 432 433 /** 434 * Gets an element by its id.<p> 435 * 436 * @param id the id 437 * @return the element with the given id 438 */ 439 protected native Element getElementById(String id) /*-{ 440 return $doc.getElementById(id); 441 }-*/; 442 443 /** 444 * Gets the toolbar element.<p> 445 * 446 * @return the toolbar element 447 */ 448 protected Element getToolbarElement() { 449 450 String toolbarId = m_id + "_external"; 451 Element result = getElementById(toolbarId); 452 return result; 453 } 454 455 /** 456 * @see com.google.gwt.user.client.ui.Widget#onDetach() 457 */ 458 @Override 459 protected void onDetach() { 460 461 try { 462 detachEditor(); 463 } catch (Throwable t) { 464 // may happen in rare cases, can be ignored 465 } 466 super.onDetach(); 467 } 468 469 /** 470 * @see com.google.gwt.user.client.ui.Widget#onLoad() 471 */ 472 @Override 473 protected void onLoad() { 474 475 incrementAttached(); 476 477 if (!m_hasBeenAttached) { 478 m_hasBeenAttached = true; 479 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 480 481 @SuppressWarnings("synthetic-access") 482 public void execute() { 483 484 if (isAttached()) { 485 m_editorHeight = calculateEditorHeight(); 486 m_id = ensureId(getMainElement()); 487 m_width = calculateWidth(); 488 checkLibraries(); 489 initNative(!m_enabled); 490 } else { 491 resetAtachedFlag(); 492 } 493 } 494 }); 495 } 496 } 497 498 /** 499 * @see com.google.gwt.user.client.ui.Widget#onUnload() 500 */ 501 @Override 502 protected void onUnload() { 503 504 decrementAttached(); 505 } 506 507 /** 508 * Propagates the a focus event.<p> 509 */ 510 protected void propagateFocusEvent() { 511 512 NativeEvent nativeEvent = Document.get().createFocusEvent(); 513 DomEvent.fireNativeEvent(nativeEvent, this, getElement()); 514 } 515 516 /** 517 * Propagates a native mouse event.<p> 518 * 519 * @param eventType the mouse event type 520 * @param eventSource the event source 521 */ 522 protected native void propagateMouseEvent(String eventType, Element eventSource) /*-{ 523 var doc = $wnd.document; 524 var event; 525 if (doc.createEvent) { 526 event = doc.createEvent("MouseEvents"); 527 event.initEvent(eventType, true, true); 528 eventSource.dispatchEvent(event); 529 } else { 530 eventSource.fireEvent("on" + eventType); 531 } 532 }-*/; 533 534 /** 535 * Sets focus to the editor. Use only when in line editing.<p> 536 */ 537 protected native void refocusInlineEditor() /*-{ 538 var elem = $wnd.document 539 .getElementById(this.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_id); 540 elem.blur(); 541 elem.focus(); 542 }-*/; 543 544 /** 545 * Removes the editor instance.<p> 546 */ 547 protected native void removeEditor() /*-{ 548 var editor = this.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_editor; 549 editor.remove(); 550 }-*/; 551 552 /** 553 * Schedules to reset the focus to the main element.<p> 554 */ 555 protected void scheduleRefocus() { 556 557 // this needs to be delayed a bit, otherwise the toolbar is not rendered properly 558 Timer focusTimer = new Timer() { 559 560 @Override 561 public void run() { 562 563 refocusInlineEditor(); 564 } 565 }; 566 focusTimer.schedule(150); 567 } 568 569 /** 570 * Sets the main content of the element which is inline editable.<p> 571 * 572 * @param html the new content html 573 */ 574 protected native void setMainElementContent(String html) /*-{ 575 var instance = this; 576 var elementId = instance.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_id; 577 var mainElement = $wnd.document.getElementById(elementId); 578 mainElement.innerHTML = html; 579 }-*/; 580 581 /** 582 * Sets the previous value.<p> 583 * 584 * @param previousValue the previous value to set 585 */ 586 protected void setPreviousValue(String previousValue) { 587 588 m_previousValue = previousValue; 589 } 590 591 /** 592 * Calculates the needed editor height.<p> 593 * 594 * @return the calculated editor height 595 */ 596 int calculateEditorHeight() { 597 598 int result = getElement().getOffsetHeight() + 30; 599 return result > MIN_EDITOR_HEIGHT ? result : MIN_EDITOR_HEIGHT; 600 } 601 602 /** 603 * Calculates the widget width.<p> 604 * 605 * @return the widget width 606 */ 607 int calculateWidth() { 608 609 return getElement().getOffsetWidth() - 2; 610 } 611 612 /** 613 * Initializes the TinyMCE instance. 614 * 615 * @param readonly if true, initialize TinyMCE in readonly mode 616 */ 617 native void initNative(boolean readonly) /*-{ 618 619 function merge() { 620 var result = {}, length = arguments.length; 621 for (i = 0; i < length; i++) { 622 for (key in arguments[i]) { 623 if (arguments[i].hasOwnProperty(key)) { 624 result[key] = arguments[i][key]; 625 } 626 } 627 } 628 return result; 629 } 630 631 var self = this; 632 var needsRefocus = false; 633 var elementId = self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_id; 634 var mainElement = $wnd.document.getElementById(elementId); 635 var editorHeight = self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_editorHeight 636 + "px"; 637 638 var fireChange = function() { 639 self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::fireChangeFromNative()(); 640 }; 641 var options = this.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_options; 642 if (options != null && options.editorHeight) { 643 editorHeight = options.editorHeight; 644 delete options.editorHeight; 645 } 646 // default options: 647 var defaults = { 648 elements : elementId, 649 relative_urls : false, 650 remove_script_host : false, 651 entity_encoding : "raw", 652 skin_variant : 'ocms', 653 mode : "exact", 654 theme : "silver", 655 plugins : "autolink lists pagebreak table save hr image link emoticons spellchecker insertdatetime preview media searchreplace print paste directionality noneditable visualchars nonbreaking template wordcount advlist", 656 paste_as_text : true, 657 menubar : false, 658 }; 659 660 self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_currentContent = self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_originalContent; 661 defaults.min_height = 100; 662 defaults.max_height = editorHeight; 663 defaults.width = '100%'; 664 defaults.resize = 'both'; 665 666 // extend the defaults with any given options 667 if (options != null) { 668 defaults = merge(defaults, options); 669 } 670 defaults.plugins = "autoresize " + defaults.plugins; 671 // add the setup function 672 defaults.setup = function(ed) { 673 self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_editor = ed; 674 ed.on('change', fireChange); 675 ed.on('KeyDown', fireChange); 676 ed 677 .on( 678 'LoadContent', 679 function() { 680 681 // firing resize event on resize of the editor iframe 682 ed.dom 683 .bind( 684 ed.getWin(), 685 'resize', 686 function() { 687 self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::fireResizeEvent()(); 688 }); 689 var content = self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_originalContent; 690 if (content != null) { 691 ed.setContent(content); 692 } 693 self.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_initialized = true; 694 }); 695 696 }; 697 // initialize tinyMCE 698 if (readonly) { 699 defaults.readonly = 1; 700 } 701 $wnd.tinymce.init(defaults); 702 }-*/; 703 704 /** 705 * Resets the attached flag.<p> 706 */ 707 void resetAtachedFlag() { 708 709 m_hasBeenAttached = false; 710 } 711 712 /** 713 * Removes the editor.<p> 714 */ 715 private native void detachEditor() /*-{ 716 717 var ed = this.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_editor; 718 if (ed != null) { 719 ed.remove(); 720 } 721 // in IE somehow the whole document will be selected, empty the selection to resolve that 722 if ($wnd.document.selection != null) { 723 $wnd.document.selection.empty(); 724 } 725 }-*/; 726 727 /** 728 * Used to fire the value changed event from native code.<p> 729 */ 730 private void fireChangeFromNative() { 731 732 // skip firing the change event, if the external flag is set 733 // String message = "fireChangeFromNative\n"; 734 // message += "init: " + m_initialized + "\n"; 735 // message += "external: " + m_externalValueChange + "\n"; 736 // CmsDebugLog.consoleLog(message); 737 738 if (m_initialized && !m_externalValueChange) { 739 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 740 741 public void execute() { 742 743 try { 744 CmsTinyMCEWidget.this.fireValueChange(false); 745 } catch (Throwable t) { 746 // this may happen when returning from full screen mode, nothing to be done 747 } 748 } 749 }); 750 } 751 // reset the external flag 752 m_externalValueChange = false; 753 } 754 755 /** 756 * Fires the resize event.<p> 757 */ 758 private void fireResizeEvent() { 759 760 ResizeEvent.fire(this, getOffsetWidth(), getOffsetHeight()); 761 } 762 763 /** 764 * Returns the editor content.<p> 765 * 766 * @return the editor content 767 */ 768 private native String getContent() /*-{ 769 var editor = this.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_editor; 770 return editor.getContent(); 771 }-*/; 772 773 /** 774 * Sets the content of the TinyMCE editor.<p> 775 * 776 * @param newContent the new content 777 */ 778 private native void setContent(String newContent) /*-{ 779 var editor = this.@org.opencms.gwt.client.ui.input.tinymce.CmsTinyMCEWidget::m_editor; 780 editor.setContent(newContent); 781 }-*/; 782 783 /** 784 * Sets the editor status to enabled/disabled.<p> 785 * 786 * Warning: This only works before the TinyMCE editor has actually been initialized 787 * @param editor the editor instance 788 * @param enabled true if editor should be enabled 789 */ 790 private native void setEnabled(JavaScriptObject editor, boolean enabled) /*-{ 791 editor.getBody().setAttribute('contenteditable', enabled); 792 }-*/; 793 794}