All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.vaadin.tinymce.TinyMce Maven / Gradle / Ivy

/*
 * Copyright 2020 Matti Tahvonen.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.vaadin.tinymce;

import com.vaadin.flow.component.AbstractCompositeField;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.Focusable;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JavaScript;
import com.vaadin.flow.component.dependency.StyleSheet;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.dom.DomEventListener;
import com.vaadin.flow.dom.DomListenerRegistration;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ShadowRoot;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.shared.Registration;
import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import elemental.json.JsonValue;

import java.util.Arrays;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * A Rich Text editor, based on TinyMCE Web Component.
 * 

* Some configurations has Java shorthand, some must be adjusted via * getElement().setAttribute(String, String). See full options via * https://www.tiny.cloud/docs/integrations/webcomponent/ * * @author mstahv */ @Tag("div") @JavaScript("context://frontend/tinymceConnector.js") @StyleSheet("context://frontend/tinymceLumo.css") public class TinyMce extends AbstractCompositeField implements HasSize, Focusable { private final DomListenerRegistration domListenerRegistration; private String id; private boolean initialContentSent; private String currentValue = ""; private String rawConfig; JsonObject config = Json.createObject(); private Element ta = new Element("div"); private int debounceTimeout = 0; private boolean basicTinyMCECreated; private boolean enabled = true; private boolean readOnly = false; /** * Creates a new TinyMce editor with shadowroot set or disabled. The shadow * root should be used if the editor is in used in Dialog component, * otherwise menu's and certain other features don't work. On the other * hand, the shadow root must not be on when for example used in inline * mode. * * @deprecated No longer needed since version x.x * * @param shadowRoot * true of shadow root hack should be used */ @Deprecated public TinyMce(boolean shadowRoot) { super(""); setHeight("500px"); ta.getStyle().set("height", "100%"); if (shadowRoot) { ShadowRoot shadow = getElement().attachShadow(); shadow.appendChild(ta); } else { getElement().appendChild(ta); } domListenerRegistration = getElement().addEventListener("tchange", (DomEventListener) event -> { boolean value = event.getEventData() .hasKey("event.htmlString"); String htmlString = event.getEventData() .getString("event.htmlString"); currentValue = htmlString; setModelValue(htmlString, true); }); domListenerRegistration.addEventData("event.htmlString"); domListenerRegistration.debounce(debounceTimeout); } /** * Define the mode of value change triggering. BLUR: Value is triggered only * when TinyMce loses focus, TIMEOUT: TinyMce will send value change eagerly * but debounced with timeout, CHANGE: value change is sent when TinyMce * emits change event (e.g. enter, tab) * * @see setDebounceTimeout(int) * @param mode * The mode. */ public void setValueChangeMode(ValueChangeMode mode) { if (mode == ValueChangeMode.BLUR) { runBeforeClientResponse(ui -> { getElement().callJsFunction("$connector.setMode", "blur"); }); } else if (mode == ValueChangeMode.TIMEOUT) { runBeforeClientResponse(ui -> { getElement().callJsFunction("$connector.setMode", "timeout"); }); } else if (mode == ValueChangeMode.CHANGE) { runBeforeClientResponse(ui -> { getElement().callJsFunction("$connector.setMode", "change"); }); } } /** * Sets the debounce timeout for the value change event. The default is 0, * when value change is triggered on blur and enter key presses. When value * is more than 0 the value change is emitted with delay of given timeout * milliseconds after last keystroke. * * @see setValueChangeMode(ValueChangeMode) * @param debounceTimeout * the debounce timeout in milliseconds */ public void setDebounceTimeout(int debounceTimeout) { if (debounceTimeout > 0) { runBeforeClientResponse(ui -> { getElement().callJsFunction("$connector.setEager", "timeout"); }); } else { runBeforeClientResponse(ui -> { getElement().callJsFunction("$connector.setEager", "change"); }); } domListenerRegistration.debounce(debounceTimeout); } public TinyMce() { this(false); } /** * Old public method from era when this component didn't properly implement * the HasValue interfaces. Don't use this but the standard setValue method * instead. * * @param html * @deprecated use {@link #setValue(Object)} instead */ @Deprecated(forRemoval = true) public void setEditorContent(String html) { setPresentationValue(html); } @Override protected void onAttach(AttachEvent attachEvent) { if (id == null) { id = UUID.randomUUID().toString(); ta.setAttribute("id", id); } if (!getEventBus().hasListener(BlurEvent.class)) { // adding fake blur listener so throttled value // change events happen by latest at blur addBlurListener(e -> { }); } if (!attachEvent.isInitialAttach()) { // Value after initial attach should be set via TinyMCE JavaScript // API, otherwise value is not updated upon reattach initialContentSent = true; } super.onAttach(attachEvent); if (attachEvent.isInitialAttach()) injectTinyMceScript(); initConnector(); saveOnClose(); } @Override protected void onDetach(DetachEvent detachEvent) { // See https://github.com/parttio/tinymce-for-flow/issues/33 if (isVisible()) { detachEvent.getUI().getPage().executeJs(""" tinymce.get($0).remove(); """, id); } super.onDetach(detachEvent); initialContentSent = false; // save the current value to the dom element in case the component gets // reattached } @SuppressWarnings("deprecation") private void initConnector() { runBeforeClientResponse(ui -> { ui.getPage().executeJs( "window.Vaadin.Flow.tinymceConnector.initLazy($0, $1, $2, $3, $4, $5)", rawConfig, getElement(), ta, config, currentValue, (enabled && !readOnly)) .then(res -> initialContentSent = true); }); } private void saveOnClose(){ runBeforeClientResponse(ui -> { getElement().callJsFunction("$connector.saveOnClose");}); } void runBeforeClientResponse(SerializableConsumer command) { getElement().getNode().runWhenAttached(ui -> ui .beforeClientResponse(this, context -> command.accept(ui))); } public String getCurrentValue() { return currentValue; } public void setConfig(String jsonConfig) { this.rawConfig = jsonConfig; } public TinyMce configure(String configurationKey, String value) { config.put(configurationKey, value); return this; } public TinyMce configure(String configurationKey, String... value) { JsonArray array = Json.createArray(); for (int i = 0; i < value.length; i++) { array.set(i, value[i]); } config.put(configurationKey, array); return this; } public TinyMce configure(String configurationKey, boolean value) { config.put(configurationKey, value); return this; } public TinyMce configure(String configurationKey, double value) { config.put(configurationKey, value); return this; } public TinyMce configureLanguage(Language language) { config.put("language", language.toString()); return this; } /** * Replaces text in the editors selection (can be just a caret position). * * @param htmlString * the html snippet to be inserted */ public void replaceSelectionContent(String htmlString) { runBeforeClientResponse(ui -> getElement().callJsFunction( "$connector.replaceSelectionContent", htmlString)); } /** * Injects actual editor script to the host page from the add-on bundle. *

* Override this with an empty implementation if you to use the cloud hosted * version, or own custom script if needed. */ protected void injectTinyMceScript() { getUI().get().getPage().addJavaScript( "context://frontend/tinymce_addon/tinymce/tinymce.min.js"); } @Override public void focus() { runBeforeClientResponse(ui -> { // Dialog has timing issues... getElement().executeJs(""" const el = this; if(el.$connector.isInDialog()) { setTimeout(() => { el.$connector.focus() }, 150); } else { el.$connetor.focus(); } """); ; }); } @Override public Registration addFocusListener( ComponentEventListener> listener) { DomListenerRegistration domListenerRegistration = getElement() .addEventListener("tfocus", event -> listener .onComponentEvent(new FocusEvent<>(this, false))); return domListenerRegistration; } @Override public Registration addBlurListener( ComponentEventListener> listener) { DomListenerRegistration domListenerRegistration = getElement() .addEventListener("tblur", event -> listener .onComponentEvent(new BlurEvent<>(this, false))); return domListenerRegistration; } @Override public void blur() { throw new RuntimeException( "Not implemented, TinyMce does not support programmatic blur."); } @Override public void setEnabled(boolean enabled) { this.enabled = enabled; adjustEnabledState(); } private void adjustEnabledState() { boolean reallyEnabled = this.enabled && !this.readOnly; super.setEnabled(reallyEnabled); runBeforeClientResponse(ui -> getElement() .callJsFunction("$connector.setEnabled", reallyEnabled)); } @Override public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; super.setReadOnly(readOnly); adjustEnabledState(); } @Override protected void setPresentationValue(String html) { this.currentValue = html; if (initialContentSent) { runBeforeClientResponse(ui -> getElement() .callJsFunction("$connector.setEditorContent", html)); } } private TinyMce createBasicTinyMce() { setValue(""); this.configure("branding", false); this.basicTinyMCECreated = true; this.configurePlugin(false, Plugin.ADVLIST, Plugin.AUTOLINK, Plugin.LISTS, Plugin.SEARCH_REPLACE); this.configureMenubar(false, Menubar.FILE, Menubar.EDIT, Menubar.VIEW, Menubar.FORMAT); this.configureToolbar(false, Toolbar.UNDO, Toolbar.REDO, Toolbar.SEPARATOR, Toolbar.FORMAT_SELECT, Toolbar.SEPARATOR, Toolbar.BOLD, Toolbar.ITALIC, Toolbar.SEPARATOR, Toolbar.ALIGN_LEFT, Toolbar.ALIGN_CENTER, Toolbar.ALIGN_RIGHT, Toolbar.ALIGN_JUSTIFY, Toolbar.SEPARATOR, Toolbar.OUTDENT, Toolbar.INDENT); return this; } public TinyMce configurePlugin(boolean basicTinyMCE, Plugin... plugins) { if (basicTinyMCE && !basicTinyMCECreated) { createBasicTinyMce(); } JsonArray jsonArray = config.get("plugins"); int initialIndex = 0; if (jsonArray != null) { initialIndex = jsonArray.length(); } else { jsonArray = Json.createArray(); } for (int i = 0; i < plugins.length; i++) { jsonArray.set(initialIndex, plugins[i].pluginLabel); initialIndex++; } config.put("plugins", jsonArray); return this; } public TinyMce configureMenubar(boolean basicTinyMCE, Menubar... menubars) { if (basicTinyMCE && !basicTinyMCECreated) { createBasicTinyMce(); } String newconfig = Arrays.stream(menubars).map(m -> m.menubarLabel) .collect(Collectors.joining(" ")); String menubar; if (config.hasKey("menubar")) { menubar = config.getString("menubar"); menubar = menubar + " " + newconfig; } else { menubar = newconfig; } config.put("menubar", menubar); return this; } public TinyMce configureToolbar(boolean basicTinyMCE, Toolbar... toolbars) { if (basicTinyMCE && !basicTinyMCECreated) { createBasicTinyMce(); } JsonValue jsonValue = config.get("toolbar"); String toolbarStr = ""; if (jsonValue != null) { toolbarStr = toolbarStr.concat(jsonValue.asString()); } for (int i = 0; i < toolbars.length; i++) { toolbarStr = toolbarStr.concat(" ").concat(toolbars[i].toolbarLabel) .concat(" "); } config.put("toolbar", toolbarStr); return this; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy