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