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

com.vaadin.flow.component.richtexteditor.RichTextEditor Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See {@literal } for the full
 * license.
 */
package com.vaadin.flow.component.richtexteditor;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import com.vaadin.flow.component.AbstractSinglePropertyField;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.CompositionNotifier;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.InputNotifier;
import com.vaadin.flow.component.KeyNotifier;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.HasThemeVariant;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.value.HasValueChangeMode;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.dom.PropertyChangeListener;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.internal.JsonSerializer;
import com.vaadin.flow.shared.Registration;

import elemental.json.JsonArray;
import elemental.json.JsonObject;

/**
 * Rich Text Editor is an input field for entering rich text. It allows you to
 * format and style your text using boldface, italics, headings, lists, images,
 * links etc.
 * 

* The value of the rich text editor is in the HTML format. The * {@link #setValue(String) setValue} and {@link #getValue() getValue} methods * use the HTML format by default. *

* To get and set the value in the * Quill Delta format, use * {@link #asDelta()}, {@link AsDelta#getValue()} and * {@link AsDelta#setValue(String)}. * * @author Vaadin Ltd * */ @Tag("vaadin-rich-text-editor") @NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.5.3") @JsModule("@vaadin/polymer-legacy-adapter/style-modules.js") @NpmPackage(value = "@vaadin/rich-text-editor", version = "24.5.3") @JsModule("@vaadin/rich-text-editor/src/vaadin-rich-text-editor.js") public class RichTextEditor extends AbstractSinglePropertyField implements CompositionNotifier, InputNotifier, KeyNotifier, HasSize, HasStyle, HasValueChangeMode, HasThemeVariant { private ValueChangeMode currentMode; private RichTextEditorI18n i18n; private AsHtml asHtml; private AsDelta asDelta; private boolean pendingPresentationUpdate = false; /** * Gets the internationalization object previously set for this component. *

* NOTE: Updating the instance that is returned from this method will not * update the component if not set again using * {@link #setI18n(RichTextEditorI18n)} * * @return the i18n object or {@code null} if no i18n object has been set */ public RichTextEditorI18n getI18n() { return i18n; } /** * Sets the internationalization object for this component. * * @param i18n * the i18n object, not {@code null} */ public void setI18n(RichTextEditorI18n i18n) { this.i18n = Objects.requireNonNull(i18n, "The i18n properties object should not be null"); runBeforeClientResponse(ui -> { if (i18n == this.i18n) { JsonObject i18nObject = (JsonObject) JsonSerializer .toJson(this.i18n); for (String key : i18nObject.keys()) { getElement().executeJs("this.set('i18n." + key + "', $0)", i18nObject.get(key)); } } }); } void runBeforeClientResponse(SerializableConsumer command) { getElement().getNode().runWhenAttached(ui -> ui .beforeClientResponse(this, context -> command.accept(ui))); } /** * Constructs an empty {@code RichTextEditor}. */ public RichTextEditor() { super("htmlValue", "", String.class, RichTextEditor::presentationToModel, RichTextEditor::modelToPresentation); setPresentationValue(""); setValueChangeMode(ValueChangeMode.ON_CHANGE); } /** * Constructs an empty {@code RichTextEditor} with a value change listener. * * @param listener * the value change listener * * @see #addValueChangeListener(com.vaadin.flow.component.HasValue.ValueChangeListener) */ public RichTextEditor( ValueChangeListener> listener) { this(); addValueChangeListener(listener); } @Override protected void onAttach(AttachEvent attachEvent) { // htmlValue property is not writeable and will not be automatically // initialized on the client-side element. Instead, call set // presentation value to run the necessary JS for initializing the // client-side element setPresentationValue(getValue()); } /** * {@inheritDoc} *

* The default value is {@link ValueChangeMode#ON_CHANGE}. */ @Override public ValueChangeMode getValueChangeMode() { return currentMode; } @Override public void setValueChangeMode(ValueChangeMode valueChangeMode) { currentMode = valueChangeMode; setSynchronizedEvent(ValueChangeMode.eventForMode(valueChangeMode, "html-value-changed")); } /** * Sets the value of this editor in HTML format. If the new value is not * equal to {@code getValue()}, fires a value change event. Throws * {@code NullPointerException}, if the value is null. *

* Note: {@link Binder} will take care of the {@code null} conversion when * integrates with the editor, as long as no new converter is defined. *

* Since v24, this method only accepts values in the HTML format, whereas in * v23 and earlier this method would accept values in the Delta format. In * order to prevent data corruption, passing a value that starts with either * [ or { will now throw an * {@link IllegalArgumentException}, as it might indicate that the value is * in the Delta format. In order to keep using the Delta format, use * {@link #asDelta()}, which allows setting, retrieving, and binding the * value using Binder, in the Delta format. In order to pass an HTML value * starting with either characters, either wrap the value in a valid HTML * tag, such as <p>, or use {@link #asHtml()} which does * not include this check. * * @see #asDelta() * @see AsDelta#setValue(String) * @param value * the new value in HTML format, not {@code null} */ @Override public void setValue(String value) { doSetValue(value, true); } private void doSetValue(String value, boolean withDeltaCheck) { Objects.requireNonNull(value, "Null value is not supported"); if (withDeltaCheck) { checkForDeltaValue(value); } super.setValue(value); } private void checkForDeltaValue(String value) { value = value.trim(); if (value.startsWith("[") || value.startsWith("{")) { throw new IllegalArgumentException( "The value starts with either '[' or '{' which indicates that this might be a value in the Delta format. " + "Since v24, RichTextEditor.setValue only accepts values in the HTML format. " + "Please check the JavaDoc for RichTextEditor.setValue for more information."); } } @Override protected void setPresentationValue(String newPresentationValue) { String presentationValue = modelToPresentation(newPresentationValue); getElement().setProperty("htmlValue", presentationValue); // htmlValue property is not writeable, HTML value needs to be set using // method exposed by web component instead if (!pendingPresentationUpdate) { pendingPresentationUpdate = true; runBeforeClientResponse(ui -> { getElement().callJsFunction("dangerouslySetHtmlValue", getElement().getProperty("htmlValue")); pendingPresentationUpdate = false; }); } } private static String presentationToModel(String htmlValue) { // Sanitize HTML coming from client return sanitize(htmlValue); } private static String modelToPresentation(String htmlValue) { // Sanitize HTML sent to client return sanitize(htmlValue); } /** * Returns the current value of the text editor in HTML format. By default, * the empty editor will return an empty string. * * @see #asDelta() * @see AsDelta#getValue() * @return the current value. */ @Override public String getValue() { return super.getValue(); } /** * The value of the editor in HTML format. * * @see #getValue() * @return the editor value in HTML format * @deprecated since v24 the RichTextEditor uses the HTML value by default. * Use {@link #getValue()} instead. */ @Deprecated public String getHtmlValue() { return getValue(); } /** * The value of the editor in Delta format. *

* This property only exists to force synchronization of the {@code value} * property. * * @return the value of the editor in Delta format */ @Synchronize(property = "value", value = "value-changed") private String getDeltaValue() { return getElement().getProperty("value"); } /** * Gets an unmodifiable list of colors in HEX format used by the text color * picker and background color picker controls of the text editor. *

* Returns {@code null} by default, which means the web component shows a * default color palette. * * @since 24.5 * @return an unmodifiable list of colors options */ public List getColorOptions() { List options = JsonSerializer.toObjects(String.class, (JsonArray) getElement().getPropertyRaw("colorOptions")); return Collections.unmodifiableList(options); } /** * Sets the list of colors in HEX format to use by the text color picker and * background color picker controls of the text editor. * * @since 24.5 * @param colorOptions * the list of colors to set, not null */ public void setColorOptions(List colorOptions) { Objects.requireNonNull(colorOptions, "Color options must not be null"); getElement().setPropertyJson("colorOptions", JsonSerializer.toJson(colorOptions)); } static String sanitize(String html) { var settings = new org.jsoup.nodes.Document.OutputSettings(); settings.prettyPrint(false); var safeHtml = org.jsoup.Jsoup.clean(html, "", org.jsoup.safety.Safelist.basic() .addTags("img", "h1", "h2", "h3", "s") .addAttributes("img", "align", "alt", "height", "src", "title", "width") .addAttributes(":all", "style") .addProtocols("img", "src", "data"), settings); return safeHtml; } /** * The internationalization properties for {@link RichTextEditor}. */ public static class RichTextEditorI18n implements Serializable { private String undo; private String redo; private String bold; private String italic; private String underline; private String strike; private String h1; private String h2; private String h3; private String color; private String background; private String subscript; private String superscript; private String listOrdered; private String listBullet; private String alignLeft; private String alignCenter; private String alignRight; private String image; private String link; private String blockquote; private String codeBlock; private String clean; /** * Gets the translated word for {@code undo} * * @return the translated word for undo */ public String getUndo() { return undo; } /** * Sets the translated word for {@code undo}. * * @param undo * the translated word for undo * @return this instance for method chaining */ public RichTextEditorI18n setUndo(String undo) { this.undo = undo; return this; } /** * Gets the translated word for {@code redo} * * @return the translated word for redo */ public String getRedo() { return redo; } /** * Sets the translated word for {@code redo}. * * @param redo * the translated word for redo * @return this instance for method chaining */ public RichTextEditorI18n setRedo(String redo) { this.redo = redo; return this; } /** * Gets the translated word for {@code bold} * * @return the translated word for bold */ public String getBold() { return bold; } /** * Sets the translated word for {@code bold}. * * @param bold * the translated word for bold * @return this instance for method chaining */ public RichTextEditorI18n setBold(String bold) { this.bold = bold; return this; } /** * Gets the translated word for {@code italic} * * @return the translated word for italic */ public String getItalic() { return italic; } /** * Sets the translated word for {@code italic}. * * @param italic * the translated word for italic * @return this instance for method chaining */ public RichTextEditorI18n setItalic(String italic) { this.italic = italic; return this; } /** * Gets the translated word for {@code underline} * * @return the translated word for underline */ public String getUnderline() { return underline; } /** * Sets the translated word for {@code underline}. * * @param underline * the translated word for underline * @return this instance for method chaining */ public RichTextEditorI18n setUnderline(String underline) { this.underline = underline; return this; } /** * Gets the translated word for {@code strike} * * @return the translated word for strike */ public String getStrike() { return strike; } /** * Sets the translated word for {@code strike}. * * @param strike * the translated word for strike * @return this instance for method chaining */ public RichTextEditorI18n setStrike(String strike) { this.strike = strike; return this; } /** * Gets the translated word for {@code h1} * * @return the translated word for h1 */ public String getH1() { return h1; } /** * Sets the translated word for {@code h1}. * * @param h1 * the translated word for h1 * @return this instance for method chaining */ public RichTextEditorI18n setH1(String h1) { this.h1 = h1; return this; } /** * Gets the translated word for {@code h2} * * @return the translated word for h2 */ public String getH2() { return h2; } /** * Sets the translated word for {@code h2}. * * @param h2 * the translated word for h2 * @return this instance for method chaining */ public RichTextEditorI18n setH2(String h2) { this.h2 = h2; return this; } /** * Gets the translated word for {@code h3} * * @return the translated word for h3 */ public String getH3() { return h3; } /** * Sets the translated word for {@code h3}. * * @param h3 * the translated word for h3 * @return this instance for method chaining */ public RichTextEditorI18n setH3(String h3) { this.h3 = h3; return this; } /** * Gets the translated word for {@code color} * * @return the translated word for color */ public String getColor() { return color; } /** * Sets the translated word for {@code color}. * * @param color * the translated word for color * @return this instance for method chaining */ public RichTextEditorI18n setColor(String color) { this.color = color; return this; } /** * Gets the translated word for {@code background} * * @return the translated word for background */ public String getBackground() { return background; } /** * Sets the translated word for {@code background}. * * @param background * the translated word for background * @return this instance for method chaining */ public RichTextEditorI18n setBackground(String background) { this.background = background; return this; } /** * Gets the translated word for {@code subscript} * * @return the translated word for subscript */ public String getSubscript() { return subscript; } /** * Sets the translated word for {@code subscript}. * * @param subscript * the translated word for subscript * @return this instance for method chaining */ public RichTextEditorI18n setSubscript(String subscript) { this.subscript = subscript; return this; } /** * Gets the translated word for {@code superscript} * * @return the translated word for superscript */ public String getSuperscript() { return superscript; } /** * Sets the translated word for {@code superscript}. * * @param superscript * the translated word for superscript * @return this instance for method chaining */ public RichTextEditorI18n setSuperscript(String superscript) { this.superscript = superscript; return this; } /** * Gets the translated word for {@code listOrdered} * * @return the translated word for listOrdered */ public String getListOrdered() { return listOrdered; } /** * Sets the translated word for {@code listOrdered}. * * @param listOrdered * the translated word for listOrdered * @return this instance for method chaining */ public RichTextEditorI18n setListOrdered(String listOrdered) { this.listOrdered = listOrdered; return this; } /** * Gets the translated word for {@code listBullet} * * @return the translated word for listBullet */ public String getListBullet() { return listBullet; } /** * Sets the translated word for {@code listBullet}. * * @param listBullet * the translated word for listBullet * @return this instance for method chaining */ public RichTextEditorI18n setListBullet(String listBullet) { this.listBullet = listBullet; return this; } /** * Gets the translated word for {@code alignLeft} * * @return the translated word for alignLeft */ public String getAlignLeft() { return alignLeft; } /** * Sets the translated word for {@code alignLeft}. * * @param alignLeft * the translated word for alignLeft * @return this instance for method chaining */ public RichTextEditorI18n setAlignLeft(String alignLeft) { this.alignLeft = alignLeft; return this; } /** * Gets the translated word for {@code alignCenter} * * @return the translated word for alignCenter */ public String getAlignCenter() { return alignCenter; } /** * Sets the translated word for {@code alignCenter}. * * @param alignCenter * the translated word for alignCenter * @return this instance for method chaining */ public RichTextEditorI18n setAlignCenter(String alignCenter) { this.alignCenter = alignCenter; return this; } /** * Gets the translated word for {@code alignRight} * * @return the translated word for alignRight */ public String getAlignRight() { return alignRight; } /** * Sets the translated word for {@code alignRight}. * * @param alignRight * the translated word for alignRight * @return this instance for method chaining */ public RichTextEditorI18n setAlignRight(String alignRight) { this.alignRight = alignRight; return this; } /** * Gets the translated word for {@code image} * * @return the translated word for image */ public String getImage() { return image; } /** * Sets the translated word for {@code image}. * * @param image * the translated word for image * @return this instance for method chaining */ public RichTextEditorI18n setImage(String image) { this.image = image; return this; } /** * Gets the translated word for {@code link} * * @return the translated word for link */ public String getLink() { return link; } /** * Sets the translated word for {@code link}. * * @param link * the translated word for link * @return this instance for method chaining */ public RichTextEditorI18n setLink(String link) { this.link = link; return this; } /** * Gets the translated word for {@code blockquote} * * @return the translated word for blockquote */ public String getBlockquote() { return blockquote; } /** * Sets the translated word for {@code blockquote}. * * @param blockquote * the translated word for blockquote * @return this instance for method chaining */ public RichTextEditorI18n setBlockquote(String blockquote) { this.blockquote = blockquote; return this; } /** * Gets the translated word for {@code codeBlock} * * @return the translated word for codeBlock */ public String getCodeBlock() { return codeBlock; } /** * Sets the translated word for {@code codeBlock}. * * @param codeBlock * the translated word for codeBlock * @return this instance for method chaining */ public RichTextEditorI18n setCodeBlock(String codeBlock) { this.codeBlock = codeBlock; return this; } /** * Gets the translated word for {@code clean} * * @return the translated word for clean */ public String getClean() { return clean; } /** * Sets the translated word for {@code clean}. * * @param clean * the translated word for clean * @return this instance for method chaining */ public RichTextEditorI18n setClean(String clean) { this.clean = clean; return this; } /** * Gets the stringified values of the tooltips. * * @return stringified values of the tooltips */ @Override public String toString() { return "[" + undo + ", " + redo + ", " + bold + ", " + italic + ", " + underline + ", " + strike + ", " + color + ", " + background + ", " + h1 + ", " + h2 + ", " + h3 + ", " + subscript + ", " + superscript + ", " + listOrdered + ", " + listBullet + ", " + alignLeft + ", " + alignCenter + ", " + alignRight + ", " + image + ", " + link + ", " + blockquote + ", " + codeBlock + ", " + clean + "]"; } } /** * Gets an instance of {@code HasValue} for the editor in the HTML format. * Can be used for binding the value with {@link Binder}. *

* Note that since v24, the RichTextEditor uses the HTML value by default. * Instead of using this wrapper, {@link #getValue()} and * {@link #setValue(String)} can be used directly, and * {@link RichTextEditor} can be used for binding the HTML value using * Binder. This method is not intended to be deprecated as it keeps the * legacy behavior that allows passing values starting with either * [ or {, which is not allowed when using * {@link #setValue(String)}. * * @return an instance of {@code HasValue} */ public HasValue, String> asHtml() { if (asHtml == null) { asHtml = new AsHtml(); } return asHtml; } /** * Gets an instance of {@code HasValue} for the editor in the * Quill Delta format. Can be * used for binding the value with {@link Binder}. * * @return an instance of {@code HasValue} */ public HasValue, String> asDelta() { if (asDelta == null) { asDelta = new AsDelta(); } return asDelta; } /** * Use this rich text editor as an editor with html value in {@link Binder}. */ private class AsHtml implements HasValue, String> { /** * Sets the value of the editor in HTML format. * * @see RichTextEditor#setValue(String) * @param value * the HTML string */ @Override public void setValue(String value) { RichTextEditor.this.doSetValue(value, false); } /** * Gets the value of the editor in HTML format. * * @see RichTextEditor#getValue() * @return the current editor value in HTML */ @Override public String getValue() { return RichTextEditor.this.getValue(); } /** * Adds a value change listener. The listener is called when the value * of this {@code HasValue} is changed either by the user or * programmatically. * * @param listener * the value change listener, not null * @return a registration for the listener */ @Override public Registration addValueChangeListener( ValueChangeListener listener) { return RichTextEditor.this.addValueChangeListener(listener); } /** * Sets the editor to be read only. * * @param readOnly * {@code true} to make the editor read only, {@code false} * to make the editor not read only */ @Override public void setReadOnly(boolean readOnly) { RichTextEditor.this.setReadOnly(readOnly); } /** * Gets whether the editor is read only. * * @return {@code true} if the editor is read only, {@code false} if it * is not read only */ @Override public boolean isReadOnly() { return RichTextEditor.this.isReadOnly(); } /** * Sets the editor's required indicator visibility. * * @param requiredIndicatorVisible * {@code true} to make the indicator visible, {@code false} * to hide the indicator */ @Override public void setRequiredIndicatorVisible( boolean requiredIndicatorVisible) { RichTextEditor.this .setRequiredIndicatorVisible(requiredIndicatorVisible); } /** * Gets whether editor's required indicator is visible. * * @return {@code true} if the required indicator is visible, * {@code false} if it is hidden. */ @Override public boolean isRequiredIndicatorVisible() { return RichTextEditor.this.isRequiredIndicatorVisible(); } @Override public String getEmptyValue() { return ""; } } private class AsDelta implements HasValue, String> { private static class DeltaValueChangeEvent extends ComponentEvent implements ValueChangeEvent { private final HasValue hasValue; private final String oldValue; public DeltaValueChangeEvent(RichTextEditor source, HasValue hasValue, String oldValue, boolean fromClient) { super(source, fromClient); this.hasValue = hasValue; this.oldValue = oldValue; } @Override public HasValue getHasValue() { return hasValue; } @Override public boolean isFromClient() { return super.isFromClient(); } @Override public String getOldValue() { return oldValue; } @Override public String getValue() { return hasValue.getValue(); } } private boolean isHtmlValueSync; private Registration deltaValueSyncRegistration; private String oldValue = ""; public AsDelta() { // Initialize empty value RichTextEditor.this.getElement().setProperty("value", ""); // Fire delta value change event when HTML value changes RichTextEditor.this.addValueChangeListener(event -> { // If the component's value changes due to syncing the HTML // value back from the client, after setting the delta value on // the server, then we don't need to do anything. // We already have an up-to-date delta value, and have already // dispatched a server-side delta value change event. if (isHtmlValueSync) { return; } // When the HTML value is set from server-side, then do not // dispatch the delta value changed event immediately, as we // don't have an updated delta value yet. // Instead, wait for delta value to sync back from client and // dispatch delta value change event afterwards if (!event.isFromClient()) { // Clear previous registration in case it is still active if (deltaValueSyncRegistration != null) { deltaValueSyncRegistration.remove(); } // Listen for delta value property to update, then fire // delta value change event as coming from server (as the // original HTML value change event did) PropertyChangeListener valueChangeListener = syncEvent -> { // Sanity check: We are expecting a property change // event from the client here if (syncEvent.isUserOriginated()) { deltaValueSyncRegistration.remove(); fireChangeEvent(false); // Update old value after all change // listeners have been processed oldValue = getValue(); } }; deltaValueSyncRegistration = RichTextEditor.this .getElement().addPropertyChangeListener("value", "value-changed", valueChangeListener); } else { // If the HTML value change event comes from the client, // then we can immediately dispatch the delta value change // event as well, as both properties have been synced to the // server in the same roundtrip. // See the synchronized `getDeltaValue` property on the // component. fireChangeEvent(event.isFromClient()); // Update old value after all change listeners have been // processed oldValue = getValue(); } }); } /** * Sets the value of this editor in the * Quill Delta format. If * the new value is not equal to {@code getValue()}, fires a value * change event. Throws {@code NullPointerException}, if the value is * null. *

* Note: {@link Binder} will take care of the {@code null} conversion * when integrates with the editor, as long as no new converter is * defined. * * @param value * the new value in Delta format, not {@code null} */ @Override public void setValue(String value) { Objects.requireNonNull(value, "Delta value must not be null"); if (!valueEquals(value, getValue())) { RichTextEditor.this.getElement().setProperty("value", value); // After setting delta value, manually sync back the updated // HTML value, which will eventually trigger a server-side value // change event on the component RichTextEditor.this.getElement() .executeJs("return this.htmlValue").then(jsonValue -> { isHtmlValueSync = true; RichTextEditor.this.setValue(jsonValue.asString()); isHtmlValueSync = false; }); fireChangeEvent(false); oldValue = value; } } /** * Returns the current value of this editor in the * Quill Delta format. By * default, the empty editor will return an empty string. * * @return the current value. */ @Override public String getValue() { return RichTextEditor.this.getElement().getProperty("value"); } @Override public Registration addValueChangeListener( ValueChangeListener> valueChangeListener) { return ComponentUtil.addListener(RichTextEditor.this, DeltaValueChangeEvent.class, valueChangeListener::valueChanged); } private void fireChangeEvent(boolean isFromClient) { DeltaValueChangeEvent changeEvent = new DeltaValueChangeEvent( RichTextEditor.this, this, oldValue, isFromClient); ComponentUtil.fireEvent(RichTextEditor.this, changeEvent); } @Override public void setReadOnly(boolean readOnly) { RichTextEditor.this.setReadOnly(readOnly); } @Override public boolean isReadOnly() { return RichTextEditor.this.isReadOnly(); } @Override public void setRequiredIndicatorVisible( boolean requiredIndicatorVisible) { RichTextEditor.this .setRequiredIndicatorVisible(requiredIndicatorVisible); } @Override public boolean isRequiredIndicatorVisible() { return RichTextEditor.this.isRequiredIndicatorVisible(); } @Override public String getEmptyValue() { return ""; } @Override public void clear() { RichTextEditor.this.clear(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy